mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
feat(cli): add gitnexus publish for opt-in understand-quickly registry
Adds a small, opt-in command that fires a single `repository_dispatch` event at `looptech-ai/understand-quickly` to ask the registry for an instant resync of the current repo's entry. No graph file is uploaded; the registry pulls from raw.githubusercontent.com per the protocol at https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md. - Pure helpers (id parsing, payload construction, validation) live in `gitnexus-shared/src/integrations/understand-quickly.ts` so the package stays Node-free and the same logic is testable in isolation. - The CLI command lives in `gitnexus/src/cli/publish.ts`. Without `UNDERSTAND_QUICKLY_TOKEN` it is a no-op (exits 0 with one informational line); with the token it POSTs the dispatch and surfaces 204 / 401 / 404 / 5xx distinctly. - The id defaults to `<owner>/<repo>` parsed from the `origin` remote and can be overridden with `--id`. - Refuses to publish when no `.gitnexus/` index exists, with a `gitnexus analyze` hint. Tests: a new vitest unit covers the pure helpers (8 + 8 + 2 cases) and the no-token no-op path with a `fetch` spy that fails the test if the network is touched. README gets a one-paragraph "Publishing to understand-quickly" section near the existing CLI docs.
This commit is contained in:
parent
0824b96d15
commit
3ea22fddff
6 changed files with 404 additions and 0 deletions
|
|
@ -214,6 +214,7 @@ gitnexus clean --all --force # Delete all indexes
|
|||
gitnexus wiki [path] # Generate repository wiki from knowledge graph
|
||||
gitnexus wiki --model <model> # Wiki with custom LLM model (default: gpt-4o-mini)
|
||||
gitnexus wiki --base-url <url> # Wiki with custom LLM API base URL
|
||||
gitnexus publish # Notify the understand-quickly registry (opt-in, see below)
|
||||
|
||||
# Repository groups (multi-repo / monorepo service tracking)
|
||||
gitnexus group create <name> # Create a repository group
|
||||
|
|
@ -228,6 +229,12 @@ gitnexus group status <name> # Check staleness of repos in a group
|
|||
|
||||
If `analyze` reports a worker parse timeout on a large or unusual repository, it keeps running and falls back safely. To give slow worker jobs more time, use `gitnexus analyze --worker-timeout 60` or set `GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=60000`. For very large files, `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` controls the worker job byte budget.
|
||||
|
||||
#### Publishing to understand-quickly (opt-in)
|
||||
|
||||
[`looptech-ai/understand-quickly`](https://github.com/looptech-ai/understand-quickly) is a public registry of code-knowledge graphs that lists `gitnexus@1` as a first-class format. After registering your repo once (`npx @understand-quickly/cli add` or the [wizard](https://looptech-ai.github.io/understand-quickly/add.html)), `gitnexus publish` fires a single `repository_dispatch` event so the registry resyncs your entry on demand instead of waiting for the nightly job.
|
||||
|
||||
It is opt-in and a no-op without `UNDERSTAND_QUICKLY_TOKEN` — a fine-grained GitHub PAT with `Repository dispatches: write` on the registry repo. Nothing else happens; no graph file is uploaded. See the [protocol spec](https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md) for the full contract.
|
||||
|
||||
### What Your AI Agent Gets
|
||||
|
||||
**16 tools** exposed via MCP (11 per-repo + 5 group):
|
||||
|
|
|
|||
|
|
@ -143,6 +143,17 @@ export type { ScopeTree } from './scope-resolution/scope-tree.js';
|
|||
export { buildPositionIndex } from './scope-resolution/position-index.js';
|
||||
export type { PositionIndex } from './scope-resolution/position-index.js';
|
||||
|
||||
// Understand-Quickly registry integration (opt-in)
|
||||
export {
|
||||
UNDERSTAND_QUICKLY_DISPATCH_URL,
|
||||
UNDERSTAND_QUICKLY_EVENT_TYPE,
|
||||
UNDERSTAND_QUICKLY_TOKEN_ENV,
|
||||
buildUqDispatchPayload,
|
||||
isValidOwnerRepo,
|
||||
parseOwnerRepoFromRemote,
|
||||
} from './integrations/understand-quickly.js';
|
||||
export type { UqDispatchPayload } from './integrations/understand-quickly.js';
|
||||
|
||||
// Shadow-mode diff + aggregation (RFC §6.3; Ring 2 SHARED #918)
|
||||
export { diffResolutions } from './scope-resolution/shadow/diff.js';
|
||||
export type {
|
||||
|
|
|
|||
105
gitnexus-shared/src/integrations/understand-quickly.ts
Normal file
105
gitnexus-shared/src/integrations/understand-quickly.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* Understand-Quickly registry integration helpers.
|
||||
*
|
||||
* Pure, runtime-agnostic logic for opting in to publishing a GitNexus
|
||||
* index to the [`looptech-ai/understand-quickly`](https://github.com/looptech-ai/understand-quickly)
|
||||
* registry. Lives in `gitnexus-shared` so both the Node CLI and any
|
||||
* future browser-side surface can construct identical dispatch payloads.
|
||||
*
|
||||
* Network I/O lives in the CLI command (`gitnexus/src/cli/publish.ts`)
|
||||
* to keep this module free of Node-only imports — see the comment at
|
||||
* the top of `gitnexus-shared/src/graph/types.ts`.
|
||||
*
|
||||
* The protocol contract (single dispatch event, no graph upload) is
|
||||
* documented at:
|
||||
* https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* URL of the registry repo's repository_dispatch endpoint. Hardcoded
|
||||
* because the registry is the canonical home for this integration —
|
||||
* users who want a private registry can fork and patch.
|
||||
*/
|
||||
export const UNDERSTAND_QUICKLY_DISPATCH_URL =
|
||||
'https://api.github.com/repos/looptech-ai/understand-quickly/dispatches';
|
||||
|
||||
/**
|
||||
* Event type the registry's sync workflow listens for.
|
||||
* See `looptech-ai/understand-quickly/.github/workflows/sync.yml`.
|
||||
*/
|
||||
export const UNDERSTAND_QUICKLY_EVENT_TYPE = 'sync-entry';
|
||||
|
||||
/** Environment variable that gates the dispatch. */
|
||||
export const UNDERSTAND_QUICKLY_TOKEN_ENV = 'UNDERSTAND_QUICKLY_TOKEN';
|
||||
|
||||
export interface UqDispatchPayload {
|
||||
event_type: typeof UNDERSTAND_QUICKLY_EVENT_TYPE;
|
||||
client_payload: {
|
||||
/** `<owner>/<repo>` shape — must match the registered entry. */
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the JSON body for the `repository_dispatch` ping. Pure — no
|
||||
* env reads, no network. Validates that `id` looks like `owner/repo`
|
||||
* (one slash, no whitespace, both halves non-empty) so a misconfigured
|
||||
* caller fails loudly before the round-trip.
|
||||
*/
|
||||
export function buildUqDispatchPayload(id: string): UqDispatchPayload {
|
||||
if (!isValidOwnerRepo(id)) {
|
||||
throw new Error(
|
||||
`[understand-quickly] expected id of the form "owner/repo", got "${id}". ` +
|
||||
`The registry uses this string to look up your entry in registry.json — ` +
|
||||
`it must match the GitHub owner/repo of the source code, not a local path.`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
event_type: UNDERSTAND_QUICKLY_EVENT_TYPE,
|
||||
client_payload: { id },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `owner/repo` validation. Conservative on purpose: GitHub's actual
|
||||
* naming rules are looser, but we want to catch local paths
|
||||
* (`/Users/...`), bare slugs (`my-repo`), and accidental whitespace.
|
||||
*/
|
||||
export function isValidOwnerRepo(id: string): boolean {
|
||||
return /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9._-]+$/.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `owner/repo` out of a git remote URL. Mirrors the heuristic in
|
||||
* `gitnexus/src/storage/git.ts:parseRepoNameFromUrl` but keeps both
|
||||
* halves so we can build a registry id. Returns `null` on shapes we
|
||||
* don't recognise.
|
||||
*
|
||||
* Examples:
|
||||
* git@github.com:looptech-ai/understand-quickly.git
|
||||
* https://github.com/looptech-ai/understand-quickly
|
||||
* ssh://git@github.com/looptech-ai/understand-quickly.git
|
||||
*/
|
||||
export function parseOwnerRepoFromRemote(url: string | null | undefined): string | null {
|
||||
if (!url) return null;
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return null;
|
||||
// Strip a trailing `.git` (case-insensitive) and any trailing slashes
|
||||
// so https://h/o/r and https://h/o/r.git collapse to the same id.
|
||||
const stripped = trimmed.replace(/\.git\/*$/i, '').replace(/\/+$/, '');
|
||||
|
||||
// SCP-form SSH (`git@host:owner/repo`).
|
||||
const ssh = stripped.match(/^[^@]+@[^:]+:([^/]+)\/([^/]+)$/);
|
||||
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
||||
|
||||
// URL forms (https://, ssh://, git://, file://) — last two path segments.
|
||||
const url2 = stripped.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/]+\/(.+)$/);
|
||||
if (url2) {
|
||||
const segments = url2[1].split('/').filter(Boolean);
|
||||
if (segments.length >= 2) {
|
||||
const [owner, repo] = segments.slice(-2);
|
||||
return `${owner}/${repo}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -160,6 +160,18 @@ program
|
|||
.description('Augment a search pattern with knowledge graph context (used by hooks)')
|
||||
.action(createLazyAction(() => import('./augment.js'), 'augmentCommand'));
|
||||
|
||||
program
|
||||
.command('publish [path]')
|
||||
.description(
|
||||
'Notify the understand-quickly registry that this repo has a fresh GitNexus index. ' +
|
||||
'Opt-in: requires UNDERSTAND_QUICKLY_TOKEN (fine-grained PAT with ' +
|
||||
'`Repository dispatches: write` on looptech-ai/understand-quickly). ' +
|
||||
'No-op without the token. See https://github.com/looptech-ai/understand-quickly.',
|
||||
)
|
||||
.option('--id <owner/repo>', 'Override the registry id (defaults to the origin remote)')
|
||||
.option('--skip-git', 'Treat cwd as the repo root and skip parent git-root discovery')
|
||||
.action(createLazyAction(() => import('./publish.js'), 'publishCommand'));
|
||||
|
||||
// ─── Direct Tool Commands (no MCP overhead) ────────────────────────
|
||||
// These invoke LocalBackend directly for use in eval, scripts, and CI.
|
||||
|
||||
|
|
|
|||
164
gitnexus/src/cli/publish.ts
Normal file
164
gitnexus/src/cli/publish.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* `gitnexus publish` — opt-in ping to the understand-quickly registry.
|
||||
*
|
||||
* Fires a single `repository_dispatch` event at
|
||||
* `looptech-ai/understand-quickly` so the registry knows to refresh its
|
||||
* entry for the current repo. Does NOT upload anything: per the
|
||||
* understand-quickly protocol, the registry pulls the graph from a
|
||||
* raw-GitHub URL the user controls.
|
||||
*
|
||||
* https://github.com/looptech-ai/understand-quickly/blob/main/docs/integrations/protocol.md
|
||||
*
|
||||
* Defaults:
|
||||
* - Without `UNDERSTAND_QUICKLY_TOKEN` in the env, this is a no-op
|
||||
* (prints one informational line, exit 0). Same shape as the
|
||||
* `--publish` patterns in sibling tools.
|
||||
* - With the token, fires the dispatch and reports the response code.
|
||||
*
|
||||
* The `id` is derived from the repo's `origin` remote unless the caller
|
||||
* passes `--id <owner/repo>` explicitly. We deliberately do NOT auto-add
|
||||
* the repo to the registry — registration is one-time and uses the
|
||||
* `npx @understand-quickly/cli add` path documented in the protocol.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import {
|
||||
UNDERSTAND_QUICKLY_DISPATCH_URL,
|
||||
UNDERSTAND_QUICKLY_TOKEN_ENV,
|
||||
buildUqDispatchPayload,
|
||||
isValidOwnerRepo,
|
||||
parseOwnerRepoFromRemote,
|
||||
} from 'gitnexus-shared';
|
||||
import { getGitRoot, getRemoteOriginUrl, getCurrentCommit } from '../storage/git.js';
|
||||
import { hasIndex } from '../storage/repo-manager.js';
|
||||
import { cliInfo, cliError } from './cli-message.js';
|
||||
|
||||
export interface PublishOptions {
|
||||
/** Override the auto-derived `owner/repo` id. */
|
||||
id?: string;
|
||||
/** Treat the cwd as the repo root (skip git-root walk). */
|
||||
skipGit?: boolean;
|
||||
}
|
||||
|
||||
const REGISTER_HINT =
|
||||
'Register your repo once with: npx @understand-quickly/cli add\n' +
|
||||
'Or use the wizard: https://looptech-ai.github.io/understand-quickly/add.html';
|
||||
|
||||
export const publishCommand = async (
|
||||
inputPath?: string,
|
||||
options: PublishOptions = {},
|
||||
): Promise<void> => {
|
||||
// ── 1. Resolve the repo root (same precedence as `analyze`) ──────────
|
||||
let repoPath: string;
|
||||
if (inputPath) {
|
||||
repoPath = path.resolve(inputPath);
|
||||
} else if (options.skipGit) {
|
||||
repoPath = path.resolve(process.cwd());
|
||||
} else {
|
||||
const gitRoot = getGitRoot(process.cwd());
|
||||
if (!gitRoot) {
|
||||
cliError(
|
||||
'[understand-quickly] not inside a git repository.\n' +
|
||||
'Run from a repo, or pass --skip-git to publish from the current directory.',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
repoPath = gitRoot;
|
||||
}
|
||||
|
||||
// ── 2. Confirm a GitNexus index exists ───────────────────────────────
|
||||
// Publishing without an index is almost always a mistake — the
|
||||
// registry's nightly sync would fetch a stale or missing graph file
|
||||
// and mark the entry `missing`. Refuse loudly with a fix-it hint.
|
||||
if (!(await hasIndex(repoPath))) {
|
||||
cliError(
|
||||
`[understand-quickly] no GitNexus index found at ${repoPath}/.gitnexus.\n` +
|
||||
'Run `gitnexus analyze` first, then re-run `gitnexus publish`.',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 3. Derive the registry id ─────────────────────────────────────────
|
||||
const id =
|
||||
options.id ?? parseOwnerRepoFromRemote(getRemoteOriginUrl(repoPath) ?? undefined) ?? null;
|
||||
if (!id || !isValidOwnerRepo(id)) {
|
||||
cliError(
|
||||
`[understand-quickly] could not derive a registry id from this repo.\n` +
|
||||
`Pass --id <owner/repo> explicitly (e.g. --id looptech-ai/${path.basename(repoPath)}).\n` +
|
||||
REGISTER_HINT,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 4. Token gate: no token → informational no-op (exit 0) ───────────
|
||||
const token = process.env[UNDERSTAND_QUICKLY_TOKEN_ENV];
|
||||
if (!token) {
|
||||
cliInfo(
|
||||
`[understand-quickly] ${UNDERSTAND_QUICKLY_TOKEN_ENV} is not set — skipping dispatch.\n` +
|
||||
`Set it to a fine-grained PAT with "Repository dispatches: write" on ` +
|
||||
`looptech-ai/understand-quickly to enable instant resync.\n` +
|
||||
`(Without the token, the registry's nightly sync still picks up ${id}.)`,
|
||||
{ id, skipped: 'no-token' },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 5. Fire the dispatch ─────────────────────────────────────────────
|
||||
const payload = buildUqDispatchPayload(id);
|
||||
const commit = getCurrentCommit(repoPath);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(UNDERSTAND_QUICKLY_DISPATCH_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
cliError(`[understand-quickly] dispatch network error: ${msg}`, { id });
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// GitHub returns 204 on success, 404 when the token can't reach the
|
||||
// registry repo, 401 when the token is invalid. Surface these
|
||||
// distinctly so users debug without checking the docs.
|
||||
if (response.status === 204) {
|
||||
cliInfo(
|
||||
`[understand-quickly] dispatched sync-entry for ${id}` +
|
||||
(commit ? ` @ ${commit.slice(0, 7)}` : '') +
|
||||
'.\n' +
|
||||
`View the workflow run: ` +
|
||||
`https://github.com/looptech-ai/understand-quickly/actions/workflows/sync.yml`,
|
||||
{ id, commit, status: response.status },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
cliError(
|
||||
`[understand-quickly] dispatch returned 404 — the token cannot reach ` +
|
||||
`looptech-ai/understand-quickly. Verify the PAT has Repository access ` +
|
||||
`to that repo and the "Repository dispatches: write" permission.`,
|
||||
{ id, status: response.status },
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// 401, 403, 422, 5xx → bubble up the body so the user can act.
|
||||
const body = await response.text().catch(() => '');
|
||||
cliError(
|
||||
`[understand-quickly] dispatch failed with HTTP ${response.status}: ${body || '(empty body)'}`,
|
||||
{ id, status: response.status },
|
||||
);
|
||||
process.exitCode = 1;
|
||||
};
|
||||
105
gitnexus/test/unit/publish.test.ts
Normal file
105
gitnexus/test/unit/publish.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import {
|
||||
buildUqDispatchPayload,
|
||||
isValidOwnerRepo,
|
||||
parseOwnerRepoFromRemote,
|
||||
UNDERSTAND_QUICKLY_TOKEN_ENV,
|
||||
} from 'gitnexus-shared';
|
||||
|
||||
describe('understand-quickly helpers (gitnexus-shared)', () => {
|
||||
describe('isValidOwnerRepo', () => {
|
||||
it.each([
|
||||
['looptech-ai/understand-quickly', true],
|
||||
['abhigyanpatwari/GitNexus', true],
|
||||
['Some_Org/Some.Repo-2', true],
|
||||
['', false],
|
||||
['just-a-name', false],
|
||||
['/Users/me/code/repo', false],
|
||||
['org/with spaces', false],
|
||||
['org//double', false],
|
||||
])('returns %s for %j', (id, expected) => {
|
||||
expect(isValidOwnerRepo(id as string)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseOwnerRepoFromRemote', () => {
|
||||
it.each([
|
||||
['git@github.com:looptech-ai/understand-quickly.git', 'looptech-ai/understand-quickly'],
|
||||
['https://github.com/looptech-ai/understand-quickly', 'looptech-ai/understand-quickly'],
|
||||
['https://github.com/looptech-ai/understand-quickly.git', 'looptech-ai/understand-quickly'],
|
||||
['ssh://git@github.com/abhigyanpatwari/GitNexus.git', 'abhigyanpatwari/GitNexus'],
|
||||
['https://gitlab.example.com/group/sub/project.git', 'sub/project'],
|
||||
])('parses %s -> %s', (url, expected) => {
|
||||
expect(parseOwnerRepoFromRemote(url)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([null, undefined, '', ' ', 'not-a-url', 'https://github.com/'])(
|
||||
'returns null for %j',
|
||||
(input) => {
|
||||
expect(parseOwnerRepoFromRemote(input as string | null | undefined)).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('buildUqDispatchPayload', () => {
|
||||
it('wraps the id in the registry-expected event shape', () => {
|
||||
expect(buildUqDispatchPayload('looptech-ai/understand-quickly')).toEqual({
|
||||
event_type: 'sync-entry',
|
||||
client_payload: { id: 'looptech-ai/understand-quickly' },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on a malformed id rather than building an invalid payload', () => {
|
||||
expect(() => buildUqDispatchPayload('just-a-name')).toThrow(/owner\/repo/);
|
||||
expect(() => buildUqDispatchPayload('/Users/me/repo')).toThrow(/owner\/repo/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishCommand (no-token no-op)', () => {
|
||||
let tempDir: string;
|
||||
let originalToken: string | undefined;
|
||||
let exitCodeBefore: number | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-publish-test-'));
|
||||
// Simulate an existing index so hasIndex() returns true.
|
||||
await fs.mkdir(path.join(tempDir, '.gitnexus'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(tempDir, '.gitnexus', 'meta.json'),
|
||||
JSON.stringify({ repoPath: tempDir, lastCommit: '', indexedAt: '' }),
|
||||
'utf-8',
|
||||
);
|
||||
originalToken = process.env[UNDERSTAND_QUICKLY_TOKEN_ENV];
|
||||
delete process.env[UNDERSTAND_QUICKLY_TOKEN_ENV];
|
||||
exitCodeBefore = process.exitCode;
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalToken !== undefined) {
|
||||
process.env[UNDERSTAND_QUICKLY_TOKEN_ENV] = originalToken;
|
||||
} else {
|
||||
delete process.env[UNDERSTAND_QUICKLY_TOKEN_ENV];
|
||||
}
|
||||
process.exitCode = exitCodeBefore;
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('exits 0 without firing a network call when the token is unset', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => {
|
||||
throw new Error('publishCommand should NOT call fetch when the token is missing');
|
||||
});
|
||||
|
||||
const { publishCommand } = await import('../../src/cli/publish.js');
|
||||
await publishCommand(tempDir, { id: 'looptech-ai/understand-quickly', skipGit: true });
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
expect(process.exitCode ?? 0).toBe(0);
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue