ts-axi 0.1.9 → 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
@@ -11,9 +11,9 @@ $ ts-axi impact main...HEAD
11
11
  base: 923fb4d7ddc8f4e5c4df9029be388f8cb29ce095
12
12
  head: HEAD
13
13
  changes: 2
14
- changed[2]{symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}:
15
- formatPrice,function,signature,true,"src/lib/format.ts:5",13,12,0,6,2
16
- Cart.total,method,body,true,"src/lib/cart.ts:32",8,8,0,5,3
14
+ changed[2]{symbol,change,at,refs_outside}:
15
+ formatPrice,signature,"src/lib/format.ts:5",12
16
+ Cart.total,body,"src/lib/cart.ts:32",8
17
17
  refs[20]{symbol,file,line,context}:
18
18
  formatPrice,src/components/Counter.tsx,2,"import { formatPrice } from '../lib';"
19
19
  formatPrice,src/components/Counter.tsx,12,"{cups} cups = {formatPrice(cups * CUP_CENTS)}"
@@ -25,17 +25,18 @@ refs[20]{symbol,file,line,context}:
25
25
  blind_spots[1]{symbol,file,line,reason}:
26
26
  Cart.total,src/components/CartSummary.tsx,16,string key 'total' on a type without that member
27
27
  elapsed_ms: 679
28
- help[5]:
28
+ help[6]:
29
29
  - "Run `ts-axi refs src/lib/format.ts:5` for every Reference of formatPrice"
30
30
  - "Run `ts-axi callers src/lib/format.ts:5` for its call sites with the enclosing function"
31
31
  - "Run `ts-axi def src/lib/format.ts:5` for the changed declaration"
32
+ - Run `ts-axi impact main...HEAD --fields files,tests` for how many files and Test files reference each Symbol
32
33
  - Run `ts-axi impact main...HEAD --grep` for textual matches the checker cannot see
33
34
  - Run `ts-axi impact main...HEAD --depth 2` for who calls the callers
34
35
  ```
35
36
 
36
- (`...` marks rows cut from this README; the command prints all 20.)
37
+ (`...` marks rows cut from this README; the command prints all 20. `--fields all` restores the full row: `{symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}`.)
37
38
 
38
- Read-only: no lint, no refactor, no rename.
39
+ Read-only on your code: no lint, no refactor, no rename. The only files it writes are `.ts-axi.json` (`config set`) and, when you ask for it, your agents' hook config (`setup hooks`).
39
40
 
40
41
  ## Status (v0.1.4)
41
42
 
@@ -63,12 +64,15 @@ Then tell your agent about it - one line in `CLAUDE.md` / `AGENTS.md`:
63
64
  Use `ts-axi` to find definitions, references and callers in TypeScript/JavaScript code, and `ts-axi impact` to see what a diff affects before reviewing it.
64
65
  ```
65
66
 
66
- Or install the on-demand Agent Skill (no per-session cost):
67
+ Or install one of the two integrations (you only need one):
67
68
 
68
69
  ```sh
69
- npx skills add cmosgh/ts-axi --skill ts-axi
70
+ ts-axi setup hooks # SessionStart hook: the project overview as ambient context (Claude Code, Codex, OpenCode)
71
+ npx skills add cmosgh/ts-axi --skill ts-axi # on-demand Agent Skill, no per-session cost
70
72
  ```
71
73
 
74
+ The hook runs `ts-axi` with no arguments: it reads only the file tree and git status (no compiler run, no network) and exits 0 in any directory. `ts-axi setup hooks --project` installs it for the current repository only, `ts-axi setup status` shows what is installed, and `ts-axi setup uninstall` removes only ts-axi's own entries.
75
+
72
76
  ## Commands
73
77
 
74
78
  | Command | What it does |
@@ -77,11 +81,12 @@ npx skills add cmosgh/ts-axi --skill ts-axi
77
81
  | `def <target>` | Declarations of a symbol by name (`save`, `User.save`, `path:line`): one match prints detail and body, several print a compact list, none prints `did_you_mean` |
78
82
  | `refs <target>` | References of one symbol, grouped by file, with test counts. A class member that implements an interface (or overrides a base) member counts only its direct References; calls through the interface are `via_interface: n (Iface.member)`, sibling implementations are dropped, and an interface member reports `implementations: n`. `--limit`, `--kind calls`, `--via` (via rows with a `via` column), `--implementations` |
79
83
  | `callers <target>` | Incoming calls of a function-like symbol, split the same way (`via_interface`, `--via`). `--depth 1..3` |
80
- | `outline <file>` | Top-level declarations in source order, class members nested. `--exports` |
81
- | `impact [<git-range> \| -]` | Declarations changed by the working tree (untracked files included; `--staged`), a git range (`main...HEAD`) or a unified diff on stdin (`bb-axi pr diff 42 --raw \| ts-axi impact -`), riskiest first: change class (`signature`, `removed`, `type`, `added`, `body`, `modified`), References outside the change set (direct only), a `via` count of calls through an interface or base member (`--via` lists them), test-file counts, `notes` (a signature change of a member that implements an interface member), `blind_spots`, `local_only`, `file_level` and `skipped` (non-TS files in the diff). `--limit`, `--depth 2..3`, `--grep`. Analyses the current checkout |
84
+ | `outline <file>` | Top-level declarations in source order, class members nested: `{line,kind,name,signature}` (`{at,kind,name,signature}` over several files). `--exports`, `--fields exported` |
85
+ | `impact [<git-range> \| -]` | Declarations changed by the working tree (untracked files included; `--staged`), a git range (`main...HEAD`) or a unified diff on stdin (`bb-axi pr diff 42 --raw \| ts-axi impact -`), riskiest first: change class (`signature`, `removed`, `type`, `added`, `body`, `modified`), References outside the change set (direct only), a `via` count of calls through an interface or base member (`--via` lists them), test-file counts, `notes` (a signature change of a member that implements an interface member), `blind_spots`, `local_only`, `file_level` and `skipped` (non-TS files in the diff). `--limit`, `--depth 2..3`, `--grep`. Rows default to `{symbol,change,at,refs_outside}`; `--fields kind,exported,refs_total,via,files,tests` (or `all`) adds columns. Analyses the current checkout |
82
86
  | `config` | Show effective settings with their source; `config set <key> <value>` and `config unset <key>` write `.ts-axi.json` |
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 |
83
88
 
84
- `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.
85
90
 
86
91
  ### A review, end to end
87
92
 
@@ -135,9 +140,9 @@ The line numbers in front of the body are file lines, so an agent can cite or co
135
140
  ```
136
141
  $ ts-axi def parse
137
142
  symbol: parse
138
- matches[2]{file,line,kind,exported,signature}:
139
- src/lib/parse.ts,1,function,true,"export function parse(input: string): number"
140
- src/lib/query.ts,1,function,true,"export function parse(q: string): Record<string, string>"
143
+ matches[2]{at,kind,signature}:
144
+ "src/lib/parse.ts:1",function,"export function parse(input: string): number"
145
+ "src/lib/query.ts:1",function,"export function parse(q: string): Record<string, string>"
141
146
  help[4]:
142
147
  - "Run `ts-axi def src/lib/parse.ts:1` for the one in src/lib/parse.ts"
143
148
  - "Run `ts-axi def src/lib/query.ts:1` for the one in src/lib/query.ts"
@@ -167,7 +172,7 @@ refs[1]{file,line,context}:
167
172
  ### Script-style JavaScript
168
173
 
169
174
  ```
170
- $ ts-axi outline client/app.js
175
+ $ ts-axi outline client/app.js --fields exported
171
176
  decls[4]{line,kind,name,exported,signature}:
172
177
  6,function,CartCtrl,true,"controller CartCtrl($scope, priceService)"
173
178
  12,function,priceService,true,service priceService()
@@ -191,6 +196,31 @@ scope: nx workspace - 30 of 5433 files (seeds 1; importers 29)
191
196
 
192
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.
193
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
+
194
224
  ## Test files
195
225
 
196
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.
@@ -231,7 +261,7 @@ corepack pnpm install
231
261
  corepack pnpm run check # typecheck, tests, build, skill freshness
232
262
  ```
233
263
 
234
- `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).
235
265
 
236
266
  ### Tests
237
267
 
@@ -239,6 +269,7 @@ corepack pnpm run check # typecheck, tests, build, skill freshness
239
269
  | --- | --- | --- |
240
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 |
241
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 |
242
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`) |
243
274
 
244
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)`,
@@ -171,14 +184,17 @@ export function parseFields(raw, available) {
171
184
  const requested = [
172
185
  ...new Set(raw
173
186
  .split(",")
174
- .map((f) => f.trim())
187
+ .map((f) => f.trim().toLowerCase())
175
188
  .filter(Boolean)),
176
189
  ];
177
- const bad = requested.filter((f) => !available.includes(f));
190
+ const bad = requested.filter((f) => f !== "all" && !available.includes(f));
178
191
  if (bad.length > 0) {
179
192
  throw usageError(`unknown field${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}`, [
180
- `available --fields: ${available.join(", ")}`,
193
+ `available --fields: ${available.join(", ")} (or all)`,
181
194
  ]);
182
195
  }
183
- return requested;
196
+ if (requested.includes("all"))
197
+ return [...available];
198
+ // Columns keep the documented order whatever order they were asked in.
199
+ return available.filter((f) => requested.includes(f));
184
200
  }
package/dist/src/cli.js CHANGED
@@ -5,22 +5,26 @@ import { homeCommand } from "./commands/home.js";
5
5
  import { impactCommand, IMPACT_HELP } from "./commands/impact.js";
6
6
  import { outlineCommand, OUTLINE_HELP } from "./commands/outline.js";
7
7
  import { callersCommand, CALLERS_HELP, refsCommand, REFS_HELP } from "./commands/refs.js";
8
+ import { setupCommand, SETUP_HELP } from "./commands/setup.js";
8
9
  import { AxiError, exitCodeForError } from "./errors.js";
9
- 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";
10
13
  import { VERSION } from "./version.js";
11
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'.";
12
15
  export const TOP_HELP = `usage: ts-axi [command] [args] [flags]
13
- commands[7]:
14
- (none)=project overview, def, refs, callers, outline, impact, config
16
+ commands[8]:
17
+ (none)=project overview, def, refs, callers, outline, impact, config, setup (opt-in session hooks)
15
18
  targets:
16
19
  name (save), member (User.save) or position (src/a.ts:42 or src/a.ts:42:7)
17
- flags[4]:
18
- -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
19
22
  examples:
20
23
  ts-axi
21
24
  ts-axi def User.save
22
25
  ts-axi refs src/models/user.ts:12
23
26
  ts-axi callers runSync --depth 2
27
+ ts-axi refs User.save --ref origin/main
24
28
  ts-axi outline src/models/user.ts
25
29
  ts-axi impact main...HEAD
26
30
  ts-axi refs --help
@@ -32,9 +36,16 @@ const COMMAND_HELP = {
32
36
  outline: OUTLINE_HELP,
33
37
  impact: IMPACT_HELP,
34
38
  config: CONFIG_HELP,
39
+ setup: SETUP_HELP,
35
40
  };
36
41
  function command(name, handler, ctx) {
37
- 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
+ };
38
49
  }
39
50
  function formatError(error) {
40
51
  const axi = error instanceof AxiError
@@ -70,10 +81,11 @@ export async function main(options = {}) {
70
81
  outline: command("outline", outlineCommand, ctx),
71
82
  impact: command("impact", impactCommand, ctx),
72
83
  config: command("config", configCommand, ctx),
84
+ setup: command("setup", (args) => setupCommand(args), ctx),
73
85
  },
74
86
  getCommandHelp: (name) => COMMAND_HELP[name],
75
87
  renderUnknownCommand: (name) => `${renderErrorOutput(`unknown command '${name}'`, "VALIDATION_ERROR", [
76
- "commands: def, refs, callers, outline, impact, config (no command = project overview)",
88
+ "commands: def, refs, callers, outline, impact, config, setup (no command = project overview)",
77
89
  "Run `ts-axi --help` for examples",
78
90
  ])}\n`,
79
91
  formatError,
@@ -81,6 +93,8 @@ export async function main(options = {}) {
81
93
  }
82
94
  finally {
83
95
  setJsonMode(false);
96
+ setSourceFs(diskFs);
97
+ setHintRef(undefined);
84
98
  }
85
99
  if (!json)
86
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]);
@@ -100,11 +121,10 @@ export function positionOf(group) {
100
121
  export const MATCH_ROWS = 20;
101
122
  /** `matches` (first MATCH_ROWS, shallowest first) plus `matches_total` / `truncated` past that. */
102
123
  export function matchRows(groups) {
124
+ // `at` pastes into `ts-axi def`; TS signatures carry `export`, and `--exported` narrows to exported ones.
103
125
  const rows = groups.slice(0, MATCH_ROWS).map((g) => ({
104
- file: g.primary.file,
105
- line: g.decls[0].nameLine,
126
+ at: positionOf(g),
106
127
  kind: g.primary.kind,
107
- exported: g.primary.exported,
108
128
  signature: g.decls[0].signature,
109
129
  }));
110
130
  return { matches: rows, ...(groups.length > MATCH_ROWS ? { matches_total: groups.length, truncated: true } : {}) };
@@ -181,8 +201,9 @@ function filterSuffixOf(label, target) {
181
201
  }
182
202
  export function resolvePositionFile(cwd, file) {
183
203
  const abs = resolve(cwd, file);
184
- if (!existsSync(abs)) {
185
- 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"]);
186
207
  }
187
208
  return abs;
188
209
  }
@@ -2,13 +2,13 @@ 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
- several = matches[n]{file,line,kind,exported,signature} (at most ${MATCH_ROWS}; matches_total + truncated past that) + the narrowing forms (--in, --kind, Class.member, --exported) with how many each keeps
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
12
12
  none = matches: 0 + did_you_mean
13
13
  examples[5]:
14
14
  ts-axi def User
@@ -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,7 +1,7 @@
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
- import { usageError } from "../errors.js";
4
+ import { AxiError, usageError } from "../errors.js";
5
5
  import { currentBranch, dirtyFileCount, isGitRepo } from "../git.js";
6
6
  import { block, helpBlock, out } from "../render.js";
7
7
  import { SETTINGS_FILE, isTestFile, loadSettings, readSettingsFile } from "../settings.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"
@@ -95,11 +89,30 @@ export async function homeCommand(args, ctx) {
95
89
  expectPositionals(parsed, 0, "ts-axi [-P <tsconfig>] [--test-glob <glob>]");
96
90
  const cwd = ctx?.cwd ?? process.cwd();
97
91
  const project = flag(parsed, "--project");
98
- const settings = loadSettings(cwd, {
99
- testGlobs: flags(parsed, "--test-glob"),
100
- ...(project !== undefined ? { project } : {}),
101
- });
102
- const tsconfig = resolveTsconfig(cwd, settings);
92
+ const overrides = { testGlobs: flags(parsed, "--test-glob"), ...(project !== undefined ? { project } : {}) };
93
+ // A broken .ts-axi.json must not fail the session-start view: report it and carry on without the file.
94
+ let broken;
95
+ let settings;
96
+ try {
97
+ settings = loadSettings(cwd, overrides);
98
+ }
99
+ catch (error) {
100
+ if (!(error instanceof AxiError))
101
+ throw error;
102
+ broken = error.message;
103
+ settings = loadSettings(cwd, { ...overrides, ignoreFile: true });
104
+ }
105
+ let tsconfig;
106
+ try {
107
+ tsconfig = resolveTsconfig(cwd, settings);
108
+ }
109
+ catch (error) {
110
+ if (!(error instanceof AxiError) || settings.sources.project !== SETTINGS_FILE)
111
+ throw error;
112
+ broken = error.message;
113
+ settings = loadSettings(cwd, { ...overrides, ignoreFile: true });
114
+ tsconfig = resolveTsconfig(cwd, settings);
115
+ }
103
116
  // Same rule as the semantic commands: no explicit project + a multi-project workspace = workspace scope.
104
117
  let workspace;
105
118
  if (settings.project === undefined && (tsconfig === undefined || hasMultiProjectSignal(settings.root))) {
@@ -121,13 +134,17 @@ export async function homeCommand(args, ctx) {
121
134
  ]);
122
135
  const testFiles = counted.files.filter((f) => isTestFile(rel(settings.root, f), settings)).length;
123
136
  const plus = counted.capped ? "+" : "";
124
- const present = readSettingsFile(settings.root) !== undefined;
137
+ const present = broken === undefined && readSettingsFile(settings.root) !== undefined;
125
138
  const onDefaultBranch = git.branch === undefined || git.branch === "main" || git.branch === "master";
126
139
  const help = [];
127
- if (git.dirty > 0 || !onDefaultBranch) {
140
+ if (broken !== undefined)
141
+ help.push(`Fix or delete ${SETTINGS_FILE} - def, refs, callers, impact and config refuse to run until then (outline still works)`);
142
+ if (broken === undefined && (git.dirty > 0 || !onDefaultBranch)) {
128
143
  help.push("Run `ts-axi impact` to see which other code your changes affect");
129
144
  }
130
- help.push("Run `ts-axi outline <file>` to list the declarations in a file", "Run `ts-axi def <name>` to find where a symbol is declared", "Run `ts-axi config` to see or change the test-file globs", "Run `ts-axi --help` for all commands");
145
+ help.push("Run `ts-axi outline <file>` to list the declarations in a file", ...(broken === undefined
146
+ ? ["Run `ts-axi def <name>` to find where a symbol is declared", "Run `ts-axi config` to see or change the test-file globs"]
147
+ : []), "Run `ts-axi --help` for all commands");
131
148
  return out(block({
132
149
  root: settings.root,
133
150
  ...(workspace ? { workspace: `${workspace.kind} (${workspace.projects.length} projects)` } : {}),
@@ -140,6 +157,6 @@ export async function homeCommand(args, ctx) {
140
157
  test_globs: settings.testGlobs.length,
141
158
  test_files: counted.capped ? `${testFiles}${plus}` : testFiles,
142
159
  git: git.text,
143
- settings: `${SETTINGS_FILE} (${present ? "present" : "absent"})`,
160
+ settings: broken !== undefined ? `${SETTINGS_FILE} (ignored - ${/not valid JSON/.test(broken) ? "not valid JSON" : broken.replace(`${SETTINGS_FILE} `, "")})` : `${SETTINGS_FILE} (${present ? "present" : "absent"})`,
144
161
  }), helpBlock(help));
145
162
  }