ts-axi 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,7 +86,7 @@ The hook runs `ts-axi` with no arguments: it reads only the file tree and git st
86
86
  | `config` | Show effective settings with their source; `config set <key> <value>` and `config unset <key>` write `.ts-axi.json` |
87
87
  | `setup <hooks\|status\|uninstall>` | Opt-in SessionStart hook for Claude Code, Codex and OpenCode that runs the home view at session start. `--project` for the current repository only |
88
88
 
89
- `ts-axi <command> --help` prints a concise, complete flag reference. `-P <tsconfig>` selects a project on any command that may analyse types; `--json` prints the decoded payload.
89
+ `ts-axi <command> --help` prints a concise, complete flag reference. `-P <tsconfig>` selects a project on any command that may analyse types; `--ref <rev>` answers `def`, `refs`, `callers` and `outline` at a git revision ([below](#reading-another-revision---ref)); `--json` prints the decoded payload.
90
90
 
91
91
  ### A review, end to end
92
92
 
@@ -196,6 +196,31 @@ scope: nx workspace - 30 of 5433 files (seeds 1; importers 29)
196
196
 
197
197
  Files are listed with `git ls-files` (tracked plus untracked, gitignore applied), so nested worktrees and ignored copies are never analysed. Outside git, a directory walk skips `node_modules`, build output, `.claude`, `.cursor`, `.idea`, `.vscode` and any nested checkout.
198
198
 
199
+ ## Reading another revision (`--ref`)
200
+
201
+ `def`, `refs`, `callers` and `outline` take `--ref <rev>` (a branch, tag, `origin/main`, `HEAD~3`, a SHA) and answer from that commit's tree, read straight from git objects. Nothing is checked out, written or fetched, and the working tree has no effect on the answer: a triage agent can ask about the deployed code while the checkout is on another branch or dirty.
202
+
203
+ ```sh
204
+ ts-axi callers Cart.total --ref origin/main
205
+ ```
206
+
207
+ ```
208
+ ref: origin/main @ 5474e0f
209
+ symbol: Cart.total
210
+ ...
211
+ help[1]:
212
+ - Run `ts-axi refs src/lib/cart.ts:32 --ref origin/main` for every Reference (not only calls)
213
+ ```
214
+
215
+ - The rev is resolved once to a SHA (`ref: <rev> @ <sha>`), so a `git fetch` during the run cannot mix versions. Paths in the output look as they do without `--ref`, and every follow-up hint keeps `--ref`.
216
+ - A file that is not in the revision is absent even if it exists locally, and a file deleted locally is still there: `file not in origin/main: src/new.ts`.
217
+ - `node_modules` is not in git, so dependency types come from the working tree; the output says `node_modules: working tree (not in git)` when it read any.
218
+ - A partial clone never fetches: a blob that is not available locally is a `GIT_ERROR` (`git fetch` or a full clone fixes it). Fetch the ref yourself first if it may be stale.
219
+ - `.ts-axi.json` is read from the working tree, like `--test-glob`.
220
+ - `impact` already compares git revisions (`ts-axi impact origin/main...HEAD`) and rejects `--ref`, as do `config` and `setup`.
221
+
222
+ Cost: the tree listing and one batched blob read add about 0.1 s on a small repo and 0.1-0.3 s on a ~5k-file Nx monorepo.
223
+
199
224
  ## Test files
200
225
 
201
226
  A file counts as a test by path pattern. Defaults: `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`, `**/e2e/**`, `**/spec/**`, `**/test.*`, `**/*_test.*`. The home view shows `test_globs` and `test_files` so a wrong rule is visible.
@@ -236,7 +261,7 @@ corepack pnpm install
236
261
  corepack pnpm run check # typecheck, tests, build, skill freshness
237
262
  ```
238
263
 
239
- `skills/ts-axi/SKILL.md` is generated: `corepack pnpm run build:skill`. See `PLAN.md` for the spec, `GLOSSARY.md` for the vocabulary and `docs/adr/` for the two engine decisions (TypeScript LanguageService over ts-morph / tree-sitter; importer subgraph over a configured project list).
264
+ `skills/ts-axi/SKILL.md` is generated: `corepack pnpm run build:skill`. See `PLAN.md` for the spec, `GLOSSARY.md` for the vocabulary and `docs/adr/` for the design decisions (TypeScript LanguageService over ts-morph / tree-sitter; importer subgraph over a configured project list; `--ref` over git objects instead of a worktree).
240
265
 
241
266
  ### Tests
242
267
 
@@ -244,6 +269,7 @@ corepack pnpm run check # typecheck, tests, build, skill freshness
244
269
  | --- | --- | --- |
245
270
  | `test/unit.test.ts`, `test/workspace.test.ts` | `pnpm test` | target parsing, signatures, change classification, diff parsing, settings, glob matching, reference classification, workspace detection, import resolution |
246
271
  | `test/commands.test.ts` | `pnpm test` | the whole CLI through `main()` over five fixtures copied to temp git repos: `test/fixtures/project` (TS, interface + implementations, barrel, JS consumer, `any` access), `test/fixtures/workspace` (Nx + pnpm workspace, `paths`, references-only tsconfigs), `test/fixtures/legacy-js` (AngularJS, Meteor, prototype, IIFE, CommonJS), `test/fixtures/commonjs` (`require('..')` shim, expando methods, `.d.ts` twin), `test/fixtures/esm-js` (`"type": "module"`, `index.js` + `index.d.ts`) |
272
+ | `test/ref.test.ts`, `test/ref-leak.test.ts`, `test/gittree.test.ts` | `pnpm test` | `--ref`: answers on a poisoned working tree equal a clean checkout (four fixtures), no read below the repo root bypasses the git tree (`node:fs` and `ts.sys` spied), tree listing, blob batches, symlinks, submodules, bad revisions |
247
273
  | `test/integration/*.test.ts` | `pnpm run test:integration` | the built binary against the vendored Astro + React sample in `test/integration/sample` (installs its own dependencies once; copied to a temp git repo per run, see `test/integration/README.md`) |
248
274
 
249
275
  CI runs the first two on Node 22 and 24, the built CLI on Node 20, and the integration suite on Node 24.
package/dist/src/args.js CHANGED
@@ -19,6 +19,14 @@ const POSITIONAL = {
19
19
  "--symbol": "pass the symbol as a positional argument",
20
20
  "--name": "pass the symbol as a positional argument",
21
21
  };
22
+ /** Commands that take `--ref <rev>`. */
23
+ export const REF_COMMANDS = ["def", "refs", "callers", "outline"];
24
+ function refHint(command, rev) {
25
+ const value = rev && !rev.startsWith("-") ? rev : "<ref>";
26
+ if (command === "ts-axi impact")
27
+ return `Run \`ts-axi impact ${value}...HEAD\` - impact takes git revisions as its range`;
28
+ return `--ref works with ${REF_COMMANDS.join(", ")} only`;
29
+ }
22
30
  /**
23
31
  * Strict declarative parser: every flag must be declared, unknown flags fail
24
32
  * loud with the valid set inline (AXI principle 6). Supports `--flag value`,
@@ -38,8 +46,11 @@ export function parseArgs(args, specs, command) {
38
46
  positionals: [],
39
47
  };
40
48
  const unknown = [];
49
+ let refValue;
41
50
  for (let i = 0; i < args.length; i++) {
42
51
  const token = args[i];
52
+ if (token === "--ref" || token.startsWith("--ref="))
53
+ refValue = token.startsWith("--ref=") ? token.slice(6) : args[i + 1];
43
54
  if (token === "--") {
44
55
  parsed.positionals.push(...args.slice(i + 1));
45
56
  break;
@@ -98,7 +109,9 @@ export function parseArgs(args, specs, command) {
98
109
  const positional = unknown
99
110
  .filter((flag) => POSITIONAL[flag])
100
111
  .map((flag) => `${flag} is not supported; ${POSITIONAL[flag]}`);
112
+ const ref = unknown.includes("--ref") ? [refHint(command, refValue)] : [];
101
113
  throw usageError(`unknown flag${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} for \`${command}\``, [
114
+ ...ref,
102
115
  ...renamed,
103
116
  ...positional,
104
117
  `valid flags for \`${command}\`: ${valid} (--help always allowed)`,
package/dist/src/cli.js CHANGED
@@ -7,7 +7,9 @@ import { outlineCommand, OUTLINE_HELP } from "./commands/outline.js";
7
7
  import { callersCommand, CALLERS_HELP, refsCommand, REFS_HELP } from "./commands/refs.js";
8
8
  import { setupCommand, SETUP_HELP } from "./commands/setup.js";
9
9
  import { AxiError, exitCodeForError } from "./errors.js";
10
- import { renderErrorOutput, setJsonMode, toJson } from "./render.js";
10
+ import { refHeader } from "./commands/common.js";
11
+ import { diskFs, setSourceFs } from "./fs.js";
12
+ import { out, renderErrorOutput, setHintRef, setJsonMode, toJson } from "./render.js";
11
13
  import { VERSION } from "./version.js";
12
14
  export const DESCRIPTION = "TypeScript/JavaScript symbol CLI for agents - definitions, references, callers, file outlines and change impact (what a diff affects) from the TypeScript compiler. Prefer it over grep for 'who uses this'.";
13
15
  export const TOP_HELP = `usage: ts-axi [command] [args] [flags]
@@ -15,13 +17,14 @@ commands[8]:
15
17
  (none)=project overview, def, refs, callers, outline, impact, config, setup (opt-in session hooks)
16
18
  targets:
17
19
  name (save), member (User.save) or position (src/a.ts:42 or src/a.ts:42:7)
18
- flags[4]:
19
- -P <tsconfig> (after the command; default nearest tsconfig.json), --json (anywhere), --help, -v/-V/--version
20
+ flags[5]:
21
+ -P <tsconfig> (after the command; default nearest tsconfig.json), --ref <rev> (def/refs/callers/outline; read a git revision instead of the working tree), --json (anywhere), --help, -v/-V/--version
20
22
  examples:
21
23
  ts-axi
22
24
  ts-axi def User.save
23
25
  ts-axi refs src/models/user.ts:12
24
26
  ts-axi callers runSync --depth 2
27
+ ts-axi refs User.save --ref origin/main
25
28
  ts-axi outline src/models/user.ts
26
29
  ts-axi impact main...HEAD
27
30
  ts-axi refs --help
@@ -36,7 +39,13 @@ const COMMAND_HELP = {
36
39
  setup: SETUP_HELP,
37
40
  };
38
41
  function command(name, handler, ctx) {
39
- return async (args) => (args.includes("-h") ? (COMMAND_HELP[name] ?? "").trimEnd() : handler(args, ctx));
42
+ return async (args) => {
43
+ if (args.includes("-h"))
44
+ return (COMMAND_HELP[name] ?? "").trimEnd();
45
+ const result = await handler(args, ctx);
46
+ // After the handler: the node_modules note depends on what it read.
47
+ return out(refHeader(), result);
48
+ };
40
49
  }
41
50
  function formatError(error) {
42
51
  const axi = error instanceof AxiError
@@ -84,6 +93,8 @@ export async function main(options = {}) {
84
93
  }
85
94
  finally {
86
95
  setJsonMode(false);
96
+ setSourceFs(diskFs);
97
+ setHintRef(undefined);
87
98
  }
88
99
  if (!json)
89
100
  return;
@@ -1,6 +1,6 @@
1
- // Plumbing shared by def / refs / callers / outline: flags, project opening,
2
- // target resolution and code rendering.
3
- import { existsSync } from "node:fs";
1
+ import { setSourceFs, sourceFs } from "../fs.js";
2
+ import { openGitTree } from "../gittree.js";
3
+ import { block, setHintRef } from "../render.js";
4
4
  import { dirname, posix, relative, resolve, sep } from "node:path";
5
5
  import ts from "typescript";
6
6
  import { enumFlag, flag, flags, bool } from "../args.js";
@@ -18,7 +18,28 @@ export const TARGET_FLAGS = [
18
18
  { name: "--exported", boolean: true },
19
19
  { name: "--fuzzy", boolean: true },
20
20
  { name: "-P" },
21
+ { name: "--ref" },
21
22
  ];
23
+ /**
24
+ * `--ref <rev>`: read the repo from that commit's git tree for the rest of the run. Returns the
25
+ * context with the cwd mapped into the tree (no disk realpath of the working tree).
26
+ */
27
+ export function useRef(ctx, parsed) {
28
+ const rev = flag(parsed, "--ref");
29
+ if (rev === undefined)
30
+ return ctx;
31
+ const fs = openGitTree(ctx.cwd, rev);
32
+ setSourceFs(fs);
33
+ setHintRef(rev);
34
+ return { ...ctx, cwd: fs.ref.cwd };
35
+ }
36
+ /** The header block of a `--ref` run: `ref: <rev> @ <short sha>`, plus where node_modules came from. */
37
+ export function refHeader() {
38
+ const ref = sourceFs().ref;
39
+ if (!ref)
40
+ return undefined;
41
+ return block({ ref: `${ref.rev} @ ${ref.sha.slice(0, 7)}`, ...(ref.nodeModulesRead ? { node_modules: "working tree (not in git)" } : {}) });
42
+ }
22
43
  export function readFilters(parsed, extraKinds = []) {
23
44
  const raw = flag(parsed, "--kind");
24
45
  const kind = raw === undefined ? undefined : enumFlag(parsed, "--kind", [...KINDS, ...extraKinds], KINDS[0]);
@@ -180,8 +201,9 @@ function filterSuffixOf(label, target) {
180
201
  }
181
202
  export function resolvePositionFile(cwd, file) {
182
203
  const abs = resolve(cwd, file);
183
- if (!existsSync(abs)) {
184
- throw usageError(`file not found: ${file}`, ["paths are relative to the current directory"]);
204
+ if (!sourceFs().exists(abs)) {
205
+ const ref = sourceFs().ref;
206
+ throw usageError(ref ? `file not in ${ref.rev}: ${file}` : `file not found: ${file}`, ["paths are relative to the current directory"]);
185
207
  }
186
208
  return abs;
187
209
  }
@@ -2,10 +2,10 @@ import { bool, expectPositionals, flag, parseArgs } from "../args.js";
2
2
  import { semanticFor } from "../scope.js";
3
3
  import { block, fenced, helpBlock, out } from "../render.js";
4
4
  import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
5
- import { BODY_LINES, declarationAt, filterSuffix, MATCH_ROWS, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, } from "./common.js";
5
+ import { BODY_LINES, declarationAt, filterSuffix, MATCH_ROWS, matchRows, narrowHints, numberedLines, open, positionStartDir, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, readFilters, useRef, } from "./common.js";
6
6
  export const DEF_HELP = `usage: ts-axi def <name | Class.member | path:line[:col]> [flags]
7
- flags[7]:
8
- --kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state>, --in <dir|glob> (repeatable; restrict to files), --exported, --fuzzy (substring, case-insensitive), --full (whole body; default first ${BODY_LINES} lines), -P <tsconfig>, --json
7
+ flags[8]:
8
+ --kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state>, --in <dir|glob> (repeatable; restrict to files), --exported, --fuzzy (substring, case-insensitive), --full (whole body; default first ${BODY_LINES} lines), -P <tsconfig>, --ref <rev> (read the code at a git revision; working tree ignored), --json
9
9
  output:
10
10
  1 match = file, line, kind, exported, signature + body fenced with file line numbers
11
11
  several = matches[n]{at,kind,signature} (at = path:line) (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms (--in, --kind, Class.member, --exported) with how many each keeps
@@ -49,6 +49,7 @@ export async function defCommand(args, ctx) {
49
49
  const target = parseTarget(raw, { fuzzy: f.fuzzy });
50
50
  const full = bool(parsed, "--full");
51
51
  const project = flag(parsed, "-P");
52
+ ctx = useRef(ctx, parsed);
52
53
  const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
53
54
  const label = targetLabel(target);
54
55
  if (target.kind === "position") {
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, statSync } from "node:fs";
1
+ import { sourceFs } from "../fs.js";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
3
  import { expectPositionals, flag, flags, parseArgs } from "../args.js";
4
4
  import { AxiError, usageError } from "../errors.js";
@@ -18,20 +18,14 @@ const HOME_SPECS = [
18
18
  ];
19
19
  /** Does `dir` hold a TS/JS source within `depth` levels (shared skip rule)? */
20
20
  function hasSources(dir, depth, root = dir) {
21
- let entries;
22
- try {
23
- entries = readdirSync(dir, { withFileTypes: true });
24
- }
25
- catch {
26
- return false;
27
- }
21
+ const entries = sourceFs().readdir(dir);
28
22
  for (const entry of entries) {
29
- if (entry.isFile() && SOURCE_EXT.test(entry.name))
23
+ if (entry.isFile && SOURCE_EXT.test(entry.name))
30
24
  return true;
31
25
  }
32
26
  if (depth === 0)
33
27
  return false;
34
- return entries.some((e) => e.isDirectory() && !skipDir(e.name, join(dir, e.name), root) && hasSources(join(dir, e.name), depth - 1, root));
28
+ return entries.some((e) => e.isDirectory && !skipDir(e.name, join(dir, e.name), root) && hasSources(join(dir, e.name), depth - 1, root));
35
29
  }
36
30
  /** Implicit project: every source below `root` from the shared listing (git, else a bounded fs walk). */
37
31
  function walkSources(root) {
@@ -41,7 +35,7 @@ function findTsconfig(cwd) {
41
35
  let dir = cwd;
42
36
  for (;;) {
43
37
  const candidate = join(dir, "tsconfig.json");
44
- if (existsSync(candidate))
38
+ if (sourceFs().exists(candidate))
45
39
  return candidate;
46
40
  const parent = dirname(dir);
47
41
  if (parent === dir)
@@ -79,7 +73,7 @@ function resolveTsconfig(cwd, settings) {
79
73
  // -P is relative to cwd, the .ts-axi.json value to the repo root.
80
74
  const base = settings.sources.project === "flag" ? cwd : settings.root;
81
75
  const path = resolve(base, settings.project);
82
- if (!existsSync(path) || !statSync(path).isFile()) {
76
+ if (!sourceFs().isFile(path)) {
83
77
  throw usageError(`tsconfig not found: ${settings.project}`, [
84
78
  settings.sources.project === "flag"
85
79
  ? "Pass an existing file to -P"
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
+ import { sourceFs } from "../fs.js";
2
3
  import { basename, dirname, join, relative, sep } from "node:path";
3
4
  import ts from "typescript";
4
5
  import { bool, flag, intFlag, parseArgs, parseFields } from "../args.js";
@@ -69,6 +70,7 @@ async function readStdin() {
69
70
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
70
71
  return Buffer.concat(chunks).toString("utf8");
71
72
  }
73
+ // Always the working tree: it is the "after" side of the diff (impact rejects --ref).
72
74
  function readText(abs) {
73
75
  try {
74
76
  return readFileSync(abs, "utf8");
@@ -243,6 +245,7 @@ export function textualMatches(opened, semantic, names, known, skipLines, change
243
245
  const files = [...semantic.projectFiles];
244
246
  const projectSet = new Set(files);
245
247
  walkExtra(opened.root, files);
248
+ sourceFs().prefetch(files);
246
249
  const rows = [];
247
250
  for (const abs of files) {
248
251
  const text = opened.index.text(abs);
@@ -311,6 +314,7 @@ export function textualCount(opened, needle, changeSet = new Map(), onlyFile) {
311
314
  const candidates = onlyFile !== undefined ? [onlyFile] : [...project];
312
315
  if (token && onlyFile === undefined)
313
316
  walkExtra(opened.root, candidates);
317
+ sourceFs().prefetch(candidates);
314
318
  for (const abs of new Set(candidates)) {
315
319
  const text = opened.index.text(abs);
316
320
  if (text === undefined || !text.includes(name))
@@ -432,7 +436,7 @@ function specFilesHint(cwd, opened, skipped, noTests) {
432
436
  const dir = dirname(tsconfig);
433
437
  const name = basename(tsconfig);
434
438
  const order = name === "tsconfig.lib.json" || name === "tsconfig.app.json" ? ["tsconfig.json", "tsconfig.spec.json"] : ["tsconfig.spec.json", "tsconfig.json"];
435
- const candidates = order.map((n) => join(dir, n)).filter((c) => c !== tsconfig && existsSync(c));
439
+ const candidates = order.map((n) => join(dir, n)).filter((c) => c !== tsconfig && sourceFs().exists(c));
436
440
  // Prefer the one that really includes a skipped spec file (an Nx tsconfig.json may only hold references).
437
441
  const includes = (c) => {
438
442
  try {
@@ -1,14 +1,15 @@
1
- import { existsSync, statSync } from "node:fs";
1
+ import { sourceFs } from "../fs.js";
2
2
  import { posix, relative, resolve, sep } from "node:path";
3
3
  import { bool, flag, parseArgs, parseFields } from "../args.js";
4
4
  import { usageError } from "../errors.js";
5
5
  import { hasGlobMagic, matchesGlob } from "../glob.js";
6
6
  import { createSyntacticIndex, findRepoRoot, openProject, SOURCE_EXTENSIONS } from "../project.js";
7
7
  import { block, helpBlock, out } from "../render.js";
8
+ import { useRef } from "./common.js";
8
9
  const MAX_ROWS = 300;
9
10
  export const OUTLINE_HELP = `usage: ts-axi outline <file | glob>... [flags]
10
- flags[4]:
11
- --exported (alias --exports; exported Declarations only), --fields exported (adds an exported column), -P <tsconfig> (file list for globs), --json
11
+ flags[5]:
12
+ --exported (alias --exports; exported Declarations only), --fields exported (adds an exported column), -P <tsconfig> (file list for globs), --ref <rev> (the file at a git revision), --json
12
13
  output:
13
14
  one file = file + decls[n]{line,kind,name,signature}; several = decls[n]{at,kind,name,signature} (at = path:line); --exported keeps only exported Declarations, --fields exported adds the column
14
15
  class members follow their class as Class.member; at most ${MAX_ROWS} rows
@@ -27,6 +28,7 @@ export async function outlineCommand(args, ctx) {
27
28
  { name: "--exported", alias: "--exports", boolean: true },
28
29
  { name: "--fields" },
29
30
  { name: "-P" },
31
+ { name: "--ref" },
30
32
  ], COMMAND);
31
33
  if (parsed.positionals.length === 0) {
32
34
  throw usageError("missing argument", [`usage: ${COMMAND} <file | glob>... [--exported]`]);
@@ -34,14 +36,16 @@ export async function outlineCommand(args, ctx) {
34
36
  const exportedOnly = bool(parsed, "--exported");
35
37
  const showExported = parseFields(flag(parsed, "--fields"), ["exported"]).length > 0;
36
38
  const override = flag(parsed, "-P");
39
+ ctx = useRef(ctx, parsed);
37
40
  const root = findRepoRoot(ctx.cwd);
38
41
  const cwdPrefix = relative(root, resolve(ctx.cwd)).split(sep).join("/");
39
42
  const files = [];
40
43
  const globs = parsed.positionals.filter(hasGlobMagic);
41
44
  for (const raw of parsed.positionals.filter((p) => !hasGlobMagic(p))) {
42
45
  const abs = resolve(ctx.cwd, raw);
43
- if (!existsSync(abs) || !statSync(abs).isFile()) {
44
- throw usageError(`file not found: ${raw}`, ["pass a source file or a quoted glob such as \"src/**/*.ts\""]);
46
+ if (!sourceFs().isFile(abs)) {
47
+ const ref = sourceFs().ref;
48
+ throw usageError(ref ? `file not in ${ref.rev}: ${raw}` : `file not found: ${raw}`, ["pass a source file or a quoted glob such as \"src/**/*.ts\""]);
45
49
  }
46
50
  if (!isSource(abs))
47
51
  throw usageError(`not a TypeScript/JavaScript source file: ${raw}`);
@@ -11,11 +11,11 @@ import { byDepthThenPath } from "../walk.js";
11
11
  import { detectWorkspace, isMultiProject } from "../workspace.js";
12
12
  import { isTestFile } from "../settings.js";
13
13
  import { isFunctionLike, kindFromScriptElement, parseTarget } from "../symbols.js";
14
- import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
14
+ import { contextOf, declarationAt, filterSuffix, lineOf, MATCH_ROWS, matchRows, narrowHints, open, positionStartDir, projectFields, readFilters, useRef, resolveNameTarget, resolvePosition, suggestions, TARGET_FLAGS, targetLabel, } from "./common.js";
15
15
  const DEFAULT_LIMIT = 50;
16
16
  const MAX_LIMIT = 500;
17
17
  const DEPTH_ROWS = 50;
18
- const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), --via (adds via rows with a via column), -P <tsconfig>, --json`;
18
+ const SHARED_FLAGS = `--kind <function|class|method|property|variable|interface|type|enum|enum-member|namespace|state> (narrows a name), --in <dir|glob> (repeatable; Declarations in these files), --exported, --fuzzy, --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} rows), --test-glob <glob> (repeatable; extra Test file pattern), --via (adds via rows with a via column), -P <tsconfig>, --ref <rev> (read the code at a git revision; working tree ignored), --json`;
19
19
  const VIA_DOC = `via_interface: n (Iface.member) = References through an interface or base member the Symbol implements or overrides - they MAY reach it; not in refs_total
20
20
  via_implementations: n (Impl.member) = References to implementations or overrides of the Symbol; not in refs_total
21
21
  implementations: n = members that implement or override the Symbol (their Declarations are never refs)
@@ -26,7 +26,7 @@ const VIA_DOC = `via_interface: n (Iface.member) = References through an interfa
26
26
  textual: n matches in m files = whole-word count of the name in Project files (JavaScript declarations and implicit projects; a state counts its full dotted name as a whole token, templates included; not for a state's resolve keys and hooks - injected by name, not imported); 'n matches in this file' for a non-exported local; above refs_total = uses the checker cannot resolve - see refs --grep
27
27
  templates: n usages in m files = template uses (.html .htm .ejs .hbs .njk .pug) of a directive or component (<wap-select / wap-select / data-wap-select), its bindings (fallback-display-property= on that tag), a controller (ng-controller) or a filter (| money); refs --grep lists them in unresolved`;
28
28
  export const REFS_HELP = `usage: ts-axi refs <name | Class.member | path:line[:col]> [flags]
29
- flags[12]:
29
+ flags[13]:
30
30
  ${SHARED_FLAGS}
31
31
  --implementations (lists implementations[n]{file,line,symbol} instead of the count)
32
32
  --grep (adds unresolved[n]{file,line,context} - textual matches of the name that are not References; also searches .astro/.vue/.svelte/.mdx/.html)
@@ -43,7 +43,7 @@ examples[4]:
43
43
  ts-axi refs runSync --limit 200
44
44
  `;
45
45
  export const CALLERS_HELP = `usage: ts-axi callers <name | Class.member | path:line[:col]> [flags]
46
- flags[11]:
46
+ flags[12]:
47
47
  ${SHARED_FLAGS}
48
48
  --depth <1-3> (default 1; levels > 1 print callers_depthN[n]{caller,file,count}, no locations)
49
49
  output:
@@ -86,6 +86,7 @@ async function run(args, ctx, invoked) {
86
86
  grep: invoked === "refs" && bool(parsed, "--grep"),
87
87
  };
88
88
  const started = performance.now();
89
+ ctx = useRef(ctx, parsed);
89
90
  const opened = open(ctx.cwd, parsed, positionStartDir(ctx.cwd, target));
90
91
  const warning = opened.warning ? { warning: opened.warning } : {};
91
92
  // Resolve to exactly one Symbol before building a program.
package/dist/src/fs.js ADDED
@@ -0,0 +1,68 @@
1
+ // The file-system seam: every read of repo content goes through `sourceFs()`. `diskFs` is the
2
+ // working tree; `--ref` swaps in a git-tree view (src/gittree.ts). Node builtins only.
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ export const diskFs = {
7
+ exists: (abs) => existsSync(abs),
8
+ isFile(abs) {
9
+ try {
10
+ return statSync(abs).isFile();
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ },
16
+ isDirectory(abs) {
17
+ try {
18
+ return statSync(abs).isDirectory();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ },
24
+ readFile(abs) {
25
+ try {
26
+ return readFileSync(abs, "utf8");
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ },
32
+ readdir(abs) {
33
+ try {
34
+ return readdirSync(abs, { withFileTypes: true }).map((e) => ({ name: e.name, isFile: e.isFile(), isDirectory: e.isDirectory() }));
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ },
40
+ realpath: (abs) => realpathSync(abs),
41
+ listFiles(root) {
42
+ if (!existsSync(join(root, ".git")))
43
+ return undefined;
44
+ try {
45
+ const raw = execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
46
+ cwd: root,
47
+ encoding: "utf8",
48
+ maxBuffer: 512 * 1024 * 1024,
49
+ stdio: ["ignore", "pipe", "ignore"],
50
+ });
51
+ // A nested repo shows up as one `dir/` entry; deleted-but-tracked files are dropped by the caller's exists check.
52
+ return [...new Set(raw.split("\0").filter((f) => f !== "" && !f.endsWith("/")))];
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ },
58
+ prefetch: () => { },
59
+ };
60
+ // One fs per run, set by the command that parsed `--ref` and reset by `main()`. A module-level
61
+ // value because the reads sit in ~40 free functions (tsconfig chains, workspace detection, TS hosts).
62
+ let active = diskFs;
63
+ export function sourceFs() {
64
+ return active;
65
+ }
66
+ export function setSourceFs(fs) {
67
+ active = fs;
68
+ }
package/dist/src/git.js CHANGED
@@ -7,6 +7,7 @@ const TIMEOUT_MS = 20_000;
7
7
  const MAX_BUFFER = 256 * 1024 * 1024;
8
8
  /** `git diff` target for a repository with no commits yet. */
9
9
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
10
+ export const REV_HINT = "Run `git branch -a` or `git log --oneline -5` to pick an existing revision";
10
11
  /** Run git; resolves with the exit code instead of throwing on non-zero. */
11
12
  async function run(cwd, args) {
12
13
  try {
@@ -44,7 +45,7 @@ function gitError(result, args) {
44
45
  ? ["Run `ts-axi impact -` with a unified diff on stdin instead"]
45
46
  : /unknown revision|bad revision|invalid object name|no merge base/i.test(first)
46
47
  ? [
47
- "Run `git branch -a` or `git log --oneline -5` to pick an existing revision",
48
+ REV_HINT,
48
49
  "Run `ts-axi impact` for uncommitted changes, or `ts-axi impact <base>...HEAD` for a branch",
49
50
  ]
50
51
  : [];