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 +47 -16
- package/dist/src/args.js +20 -4
- package/dist/src/cli.js +21 -7
- package/dist/src/commands/common.js +29 -8
- package/dist/src/commands/def.js +5 -4
- package/dist/src/commands/home.js +39 -22
- package/dist/src/commands/impact.js +59 -26
- package/dist/src/commands/outline.js +15 -10
- package/dist/src/commands/refs.js +5 -4
- package/dist/src/commands/setup.js +97 -0
- package/dist/src/errors.js +1 -3
- package/dist/src/fs.js +68 -0
- package/dist/src/git.js +7 -1
- package/dist/src/gittree.js +277 -0
- package/dist/src/project.js +67 -32
- package/dist/src/render.js +14 -1
- package/dist/src/scope.js +4 -3
- package/dist/src/settings.js +1 -1
- package/dist/src/templates.js +9 -8
- package/dist/src/walk.js +12 -30
- package/dist/src/workspace.js +21 -26
- package/package.json +1 -1
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,
|
|
15
|
-
formatPrice,
|
|
16
|
-
Cart.total,
|
|
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[
|
|
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
|
|
67
|
+
Or install one of the two integrations (you only need one):
|
|
67
68
|
|
|
68
69
|
```sh
|
|
69
|
-
|
|
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]{
|
|
139
|
-
src/lib/parse.ts
|
|
140
|
-
src/lib/query.ts
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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[
|
|
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[
|
|
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) =>
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 (!
|
|
185
|
-
|
|
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
|
}
|
package/dist/src/commands/def.js
CHANGED
|
@@ -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[
|
|
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]{
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 (!
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (
|
|
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",
|
|
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
|
}
|