Bladeren bron

fix(cli): make `node` symbol positional optional so `node -f <file>` works (#1044) (#1051)

`codegraph node` was defined with a required `<name>` positional, so
commander.js rejected `codegraph node -f <file>` with "missing required
argument 'name'" before the action ran — making file-read mode (the CLI
face of the codegraph_node MCP tool's file mode) unreachable. The action
body already handled an absent name.

Make `name` optional (`[name]`), validate that a symbol or a file is
supplied (friendly usage hint instead of a cryptic commander error when
neither is), and guard the name-based arg branches so they never run on
undefined. Adds an end-to-end regression test across all four paths.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 10 uur geleden
bovenliggende
commit
0d331b9017
3 gewijzigde bestanden met toevoegingen van 97 en 5 verwijderingen
  1. 1 0
      CHANGELOG.md
  2. 80 0
      __tests__/cli-node-command.test.ts
  3. 16 5
      src/bin/codegraph.ts

+ 1 - 0
CHANGELOG.md

@@ -17,6 +17,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - C++ classes that inherit from a templated base — `class Widget : public Base<int>`, a CRTP base like `class App : public CRTPBase<App>`, or a struct inheriting a template — are now linked to that base class in the graph. Previously the template arguments (`<int>`) made the inheritance go unrecognized, so these classes looked like they inherited from nothing and impact/callers analysis stopped at the boundary; the connection is now followed like any other base class. Thanks @ryancu7 for the report. (#1043)
 - C++ objects constructed on the stack — `Calculator calc(0)` or `Widget w{1, 2}` — now record that the enclosing function instantiates that class, the same as heap construction (`new Calculator(0)`) already did. Previously only the `new` form was tracked, so a function that built objects with the ordinary stack syntax looked like it didn't construct them and the dependency was missing from impact/callers. Thanks @Dshuishui for the report. (#1035)
 - The graph no longer stores duplicate copies of the same relationship. The same dependency between the same two symbols at the same spot could be recorded more than once, which inflated edge counts and let callers/impact results list a relationship twice. Each relationship is now stored exactly once, and existing projects are de-duplicated automatically the next time CodeGraph opens them. Thanks @inth3shadows for the detailed report. (#1034)
+- `codegraph node` can now read a file from the command line. File-read mode — pass `-f`/`--file` to get a file's source with line numbers plus the files that depend on it, the same output as the `codegraph_node` MCP tool — was rejected with "missing required argument 'name'", because the command always demanded a symbol name even though file mode has none, leaving the feature unreachable from the CLI. The symbol name is now optional: `codegraph node -f src/auth.ts` (or `codegraph node src/auth.ts`) reads the file, `codegraph node parseToken` looks up a symbol, and running it with neither prints a short usage hint instead of a cryptic error. Thanks @jcrabapple for the report. (#1044)
 
 
 ## [1.1.2] - 2026-06-28

+ 80 - 0
__tests__/cli-node-command.test.ts

@@ -0,0 +1,80 @@
+/**
+ * `codegraph node` argument handling (#1044).
+ *
+ * File-read mode (`codegraph node -f <file>`) carries no symbol name, but the
+ * command was defined with a REQUIRED `<name>` positional, so commander.js
+ * rejected the call with "missing required argument 'name'" before the action
+ * ever ran — making file mode unreachable from the CLI. `name` is now optional
+ * (`[name]`); the action validates that a symbol OR a file is supplied.
+ *
+ * Exercised end-to-end against the built binary.
+ */
+
+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');
+
+function runNode(cwd: string, extraArgs: string[]): { stdout: string; stderr: string; code: number } {
+  try {
+    const stdout = execFileSync(process.execPath, [BIN, 'node', ...extraArgs, '-p', cwd], {
+      encoding: 'utf-8',
+      env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1' },
+      stdio: ['ignore', 'pipe', 'pipe'],
+    });
+    return { stdout, stderr: '', code: 0 };
+  } catch (err: any) {
+    return { stdout: err.stdout ?? '', stderr: err.stderr ?? '', code: err.status ?? 1 };
+  }
+}
+
+describe('codegraph node — argument handling (#1044)', () => {
+  let tempDir: string;
+
+  beforeEach(async () => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-node-cmd-'));
+    fs.mkdirSync(path.join(tempDir, 'src'));
+    fs.writeFileSync(path.join(tempDir, 'src/util.ts'), 'export function util(x: number){ return x + 1; }\n');
+    const cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.close();
+  });
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('file mode via -f reads the file (was rejected as "missing required argument")', () => {
+    const { stdout, code } = runNode(tempDir, ['-f', 'src/util.ts']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('src/util.ts');
+    expect(stdout).toContain('export function util');
+    // The line-numbered Read-parity shape.
+    expect(stdout).toMatch(/1\s+export function util/);
+  });
+
+  it('a path-like positional still routes to file mode', () => {
+    const { stdout, code } = runNode(tempDir, ['src/util.ts']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('src/util.ts');
+    expect(stdout).toContain('export function util');
+  });
+
+  it('a bare symbol positional still routes to symbol mode', () => {
+    const { stdout, code } = runNode(tempDir, ['util']);
+    expect(code).toBe(0);
+    expect(stdout).toContain('util');
+    expect(stdout).toContain('Location:');
+  });
+
+  it('neither symbol nor file gives a usage error, not commander\'s cryptic one', () => {
+    const { stderr, code } = runNode(tempDir, []);
+    expect(code).not.toBe(0);
+    expect(stderr).toMatch(/symbol name|file/i);
+    expect(stderr).not.toMatch(/missing required argument/);
+  });
+});

+ 16 - 5
src/bin/codegraph.ts

@@ -1140,21 +1140,32 @@ program
   });
 
 /**
- * codegraph node <name>
+ * codegraph node [name]
  *
  * The CLI face of the MCP codegraph_node tool: one symbol's source +
  * caller/callee trail, or a whole file with line numbers + dependents
  * (Read-parity). Same subagent/non-MCP rationale as `explore`.
+ *
+ * `name` is OPTIONAL because `--file` (file-read mode) carries no symbol —
+ * a required `<name>` made `codegraph node -f <file>` unreachable (#1044).
  */
 program
-  .command('node <name>')
+  .command('node [name]')
   .description('One symbol\'s source + caller/callee trail, or read a file with line numbers + dependents (same output as the codegraph_node MCP tool)')
   .option('-p, --path <path>', 'Project path')
   .option('-f, --file <file>', 'Treat as file mode (or disambiguate a symbol to this file)')
   .option('--offset <number>', 'File mode: 1-based start line')
   .option('--limit <number>', 'File mode: maximum lines')
   .option('--symbols-only', 'File mode: just the symbol map + dependents')
-  .action(async (name: string, options: { path?: string; file?: string; offset?: string; limit?: string; symbolsOnly?: boolean }) => {
+  .action(async (name: string | undefined, options: { path?: string; file?: string; offset?: string; limit?: string; symbolsOnly?: boolean }) => {
+    // Need a symbol (positional) OR a file (--file / a path-like positional).
+    // With [name] optional, a bare `codegraph node` reaches here with neither
+    // and must be told what to pass, rather than crashing downstream.
+    if (!name && !options.file) {
+      error("Pass a symbol name (e.g. 'codegraph node parseToken') or a file (e.g. 'codegraph node -f src/auth.ts', or 'codegraph node src/auth.ts').");
+      process.exit(1);
+    }
+
     const projectPath = resolveProjectPath(options.path);
 
     try {
@@ -1177,9 +1188,9 @@ program
       if (options.file) {
         args.file = options.file;
         if (name && name !== options.file) args.symbol = name;
-      } else if (name.includes('/') || name.includes('\\')) {
+      } else if (name && (name.includes('/') || name.includes('\\'))) {
         args.file = name.replace(/\\/g, '/');
-      } else {
+      } else if (name) {
         args.symbol = name;
         args.includeCode = true;
       }