Przeglądaj źródła

fix(cli): register the documented `context` command (#1611)

The usage header has advertised 'codegraph context <task>  Build context for a task' since the first commit, and the ContextBuilder behind the public buildContext API has always shipped in the package — but the command was never registered with commander, so it errored with "unknown command 'context'". External integrations built against the documented contract (Memorix 1.8.1 invokes 'codegraph context --path <root> --format json --max-nodes 8 --no-code <task>') silently fell back to their own heuristics.

Register 'context <task...>' next to the other read commands, mapping flags 1:1 onto BuildContextOptions: --path (resolved like every sibling command), --format markdown|json (validated, default markdown), --max-nodes (positive int, validated), --no-code (includeCode=false). JSON goes to stdout verbatim and unmixed — error()/warnings write to stderr — so the output is machine-parseable. Covered end-to-end against the built binary, including the exact Memorix invocation shape and the uninitialized-project failure path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
Colby McHenry 1 tydzień temu
rodzic
commit
80ef4b8a3c
3 zmienionych plików z 175 dodań i 0 usunięć
  1. 2 0
      CHANGELOG.md
  2. 114 0
      __tests__/cli-context-command.test.ts
  3. 59 0
      src/bin/codegraph.ts

+ 2 - 0
CHANGELOG.md

@@ -72,6 +72,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - C/C++ function-pointer analysis now bounds its compiled-pattern caches, so very large repositories can no longer exhaust the JavaScript engine's regular-expression code space during indexing. (#1559)
 - C/C++ function-pointer analysis now bounds its compiled-pattern caches, so very large repositories can no longer exhaust the JavaScript engine's regular-expression code space during indexing. (#1559)
 - JSX rendering analysis now runs only on JavaScript-family files, so JSX-looking strings in C/C++ (or any other language) no longer create impossible call edges — in pure-C projects and in mixed-language monorepos alike. (#1560)
 - JSX rendering analysis now runs only on JavaScript-family files, so JSX-looking strings in C/C++ (or any other language) no longer create impossible call edges — in pure-C projects and in mixed-language monorepos alike. (#1560)
 
 
+- The `codegraph context <task>` command documented in the CLI help now actually exists — it builds a ready-to-inject context pack for a task (relevant symbols, their relationships, and code) in markdown or JSON, restoring the contract external integrations like Memorix rely on (`--path`, `--format json`, `--max-nodes`, `--no-code`). (#1611)
+
 ## [1.5.0] - 2026-07-21
 ## [1.5.0] - 2026-07-21
 
 
 # ⚡ The Rust engine release — with near-instant sync
 # ⚡ The Rust engine release — with near-instant sync

+ 114 - 0
__tests__/cli-context-command.test.ts

@@ -0,0 +1,114 @@
+/**
+ * `codegraph context` CLI command (#1611).
+ *
+ * The usage header has advertised `codegraph context <task>  Build context for
+ * a task` since the first release, and the ContextBuilder behind the public
+ * `buildContext` API has always shipped in the package — but the command was
+ * never registered with commander, so external integrations built against the
+ * documented contract (`codegraph context --path <root> --format json
+ * --max-nodes 8 --no-code <task>`, e.g. Memorix) got `unknown command
+ * 'context'` and fell back to their own heuristics.
+ *
+ * Exercised end-to-end against the built binary, mirroring
+ * cli-query-command.test.ts.
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { CodeGraph } from '../src';
+
+const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
+
+const ENV = { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' };
+
+function runContext(cwd: string, extraArgs: string[], taskParts: string[] = ['parseToken', 'expiry', 'handling']): string {
+  return execFileSync(process.execPath, [BIN, 'context', ...extraArgs, '-p', cwd, ...taskParts], {
+    encoding: 'utf-8',
+    env: ENV,
+    stdio: ['ignore', 'pipe', 'ignore'], // drop stderr (SQLite experimental warning)
+  });
+}
+
+describe('codegraph context — registered CLI command (#1611)', () => {
+  let tempDir: string;
+
+  beforeEach(async () => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-cmd-'));
+    fs.mkdirSync(path.join(tempDir, 'src'));
+    fs.writeFileSync(
+      path.join(tempDir, 'src/auth.ts'),
+      'export function parseToken(t: string){ return parseTokenExpiry(t) + t.trim().length; }\n' +
+        'export function parseTokenExpiry(t: string){ return Date.parse(t); }\n',
+    );
+    const cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.close();
+  });
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('--format json emits clean machine-parseable JSON on stdout', () => {
+    const parsed = JSON.parse(runContext(tempDir, ['--format', 'json']));
+    expect(parsed.query).toBe('parseToken expiry handling');
+    expect(Array.isArray(parsed.nodes)).toBe(true);
+    expect(parsed.nodes.length).toBeGreaterThan(0);
+    expect(Array.isArray(parsed.codeBlocks)).toBe(true);
+    expect(parsed.codeBlocks.length).toBeGreaterThan(0);
+  });
+
+  it('--max-nodes bounds the returned symbol set', () => {
+    const parsed = JSON.parse(runContext(tempDir, ['--format', 'json', '--max-nodes', '1']));
+    expect(parsed.nodes.length).toBeLessThanOrEqual(1);
+  });
+
+  it('--no-code omits code blocks (the Memorix contract shape)', () => {
+    // The exact documented invocation: --format json --max-nodes 8 --no-code
+    const parsed = JSON.parse(
+      runContext(tempDir, ['--format', 'json', '--max-nodes', '8', '--no-code']),
+    );
+    expect(parsed.codeBlocks).toEqual([]);
+    expect(parsed.nodes.length).toBeGreaterThan(0);
+  });
+
+  it('defaults to markdown output', () => {
+    const out = runContext(tempDir, []);
+    expect(out).toContain('## Code Context');
+    expect(out).toContain('**Query:** parseToken expiry handling');
+  });
+
+  it('fails cleanly on an uninitialized project', () => {
+    const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-context-empty-'));
+    try {
+      execFileSync(process.execPath, [BIN, 'context', '-p', empty, 'some', 'task'], {
+        encoding: 'utf-8',
+        env: ENV,
+        stdio: ['ignore', 'pipe', 'pipe'],
+      });
+      throw new Error('expected non-zero exit');
+    } catch (err: any) {
+      expect(err.status).toBe(1);
+      expect(String(err.stderr)).toContain('not initialized');
+    } finally {
+      fs.rmSync(empty, { recursive: true, force: true });
+    }
+  });
+
+  it('rejects an unknown --format value', () => {
+    try {
+      execFileSync(process.execPath, [BIN, 'context', '--format', 'yaml', '-p', tempDir, 'task'], {
+        encoding: 'utf-8',
+        env: ENV,
+        stdio: ['ignore', 'pipe', 'pipe'],
+      });
+      throw new Error('expected non-zero exit');
+    } catch (err: any) {
+      expect(err.status).toBe(1);
+      expect(String(err.stderr)).toContain('Unknown format');
+    }
+  });
+});

+ 59 - 0
src/bin/codegraph.ts

@@ -1233,6 +1233,65 @@ program
     }
     }
   });
   });
 
 
+/**
+ * codegraph context <task...>
+ *
+ * The CLI face of the public `buildContext` API (ContextBuilder): FTS entry
+ * points + graph expansion + code blocks, formatted as markdown or JSON.
+ * Advertised in the usage header since the first release but never actually
+ * registered (#1611); external integrations (e.g. Memorix) invoke it as
+ * `codegraph context --path <root> --format json --max-nodes 8 --no-code <task>`.
+ */
+program
+  .command('context <task...>')
+  .description('Build context for a task: relevant symbols, relationships, and code blocks')
+  .option('-p, --path <path>', 'Project path')
+  .option('-f, --format <format>', 'Output format: markdown or json', 'markdown')
+  .option('-n, --max-nodes <number>', 'Maximum number of symbols to include')
+  .option('--no-code', 'Omit code blocks (structure only)')
+  .action(async (taskParts: string[], options: { path?: string; format?: string; maxNodes?: string; code?: boolean }) => {
+    const projectPath = resolveProjectPath(options.path);
+
+    const format = options.format ?? 'markdown';
+    if (format !== 'markdown' && format !== 'json') {
+      error(`Unknown format "${options.format}" — use "markdown" or "json".`);
+      process.exit(1);
+    }
+    let maxNodes: number | undefined;
+    if (options.maxNodes !== undefined) {
+      maxNodes = parseInt(options.maxNodes, 10);
+      if (Number.isNaN(maxNodes) || maxNodes < 1) {
+        error(`--max-nodes expects a positive integer, got "${options.maxNodes}".`);
+        process.exit(1);
+      }
+    }
+
+    try {
+      if (!isInitialized(projectPath)) {
+        error(`CodeGraph not initialized in ${projectPath}`);
+        process.exit(1);
+      }
+
+      const { default: CodeGraph } = await loadCodeGraph();
+      const cg = await CodeGraph.open(projectPath);
+
+      const result = await cg.buildContext(taskParts.join(' '), {
+        format,
+        includeCode: options.code !== false,
+        ...(maxNodes !== undefined ? { maxNodes } : {}),
+      });
+
+      // Both supported formats return a formatted string; print it verbatim so
+      // `--format json` stays machine-parseable on stdout (error()/warnings go
+      // to stderr only).
+      console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
+      cg.destroy();
+    } catch (err) {
+      error(`Context build failed: ${err instanceof Error ? err.message : String(err)}`);
+      process.exit(1);
+    }
+  });
+
 /**
 /**
  * codegraph prompt-hook  (hidden)
  * codegraph prompt-hook  (hidden)
  *
  *