ts-axi 0.1.8 → 0.1.10
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 +31 -14
- package/dist/src/args.js +7 -4
- package/dist/src/cli.js +6 -3
- package/dist/src/commands/common.js +2 -3
- package/dist/src/commands/def.js +1 -1
- package/dist/src/commands/home.js +33 -10
- package/dist/src/commands/impact.js +53 -24
- package/dist/src/commands/outline.js +8 -7
- package/dist/src/commands/setup.js +97 -0
- package/dist/src/errors.js +1 -3
- package/dist/src/git.js +6 -1
- package/dist/src/settings.js +1 -1
- 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,9 +81,10 @@ 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
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.
|
|
85
90
|
|
|
@@ -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()
|
|
@@ -243,6 +248,18 @@ corepack pnpm run check # typecheck, tests, build, skill freshness
|
|
|
243
248
|
|
|
244
249
|
CI runs the first two on Node 22 and 24, the built CLI on Node 20, and the integration suite on Node 24.
|
|
245
250
|
|
|
251
|
+
<!-- axi-tools:start -->
|
|
252
|
+
## Other AXI tools
|
|
253
|
+
|
|
254
|
+
- [`atl-axi`](https://www.npmjs.com/package/atl-axi): Atlassian Jira Cloud CLI for agents
|
|
255
|
+
- [`bb-axi`](https://www.npmjs.com/package/bb-axi): Bitbucket Cloud CLI for agents
|
|
256
|
+
- [`hetzner-axi`](https://www.npmjs.com/package/hetzner-axi): Hetzner Cloud CLI for agents
|
|
257
|
+
- [`mongo-axi`](https://www.npmjs.com/package/mongo-axi): Read-only MongoDB CLI for agents
|
|
258
|
+
- [`mongo-rw-axi`](https://www.npmjs.com/package/mongo-rw-axi): Write-capable MongoDB CLI for agents, loopback hosts only
|
|
259
|
+
- [`mysql-axi`](https://www.npmjs.com/package/mysql-axi): Read-only MySQL CLI for agents
|
|
260
|
+
- [`mysql-rw-axi`](https://www.npmjs.com/package/mysql-rw-axi): Write-capable MySQL CLI for agents, loopback hosts only
|
|
261
|
+
<!-- axi-tools:end -->
|
|
262
|
+
|
|
246
263
|
## License
|
|
247
264
|
|
|
248
265
|
MIT
|
package/dist/src/args.js
CHANGED
|
@@ -171,14 +171,17 @@ export function parseFields(raw, available) {
|
|
|
171
171
|
const requested = [
|
|
172
172
|
...new Set(raw
|
|
173
173
|
.split(",")
|
|
174
|
-
.map((f) => f.trim())
|
|
174
|
+
.map((f) => f.trim().toLowerCase())
|
|
175
175
|
.filter(Boolean)),
|
|
176
176
|
];
|
|
177
|
-
const bad = requested.filter((f) => !available.includes(f));
|
|
177
|
+
const bad = requested.filter((f) => f !== "all" && !available.includes(f));
|
|
178
178
|
if (bad.length > 0) {
|
|
179
179
|
throw usageError(`unknown field${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}`, [
|
|
180
|
-
`available --fields: ${available.join(", ")}`,
|
|
180
|
+
`available --fields: ${available.join(", ")} (or all)`,
|
|
181
181
|
]);
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
if (requested.includes("all"))
|
|
184
|
+
return [...available];
|
|
185
|
+
// Columns keep the documented order whatever order they were asked in.
|
|
186
|
+
return available.filter((f) => requested.includes(f));
|
|
184
187
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -5,13 +5,14 @@ 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
10
|
import { renderErrorOutput, setJsonMode, toJson } from "./render.js";
|
|
10
11
|
import { VERSION } from "./version.js";
|
|
11
12
|
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
13
|
export const TOP_HELP = `usage: ts-axi [command] [args] [flags]
|
|
13
|
-
commands[
|
|
14
|
-
(none)=project overview, def, refs, callers, outline, impact, config
|
|
14
|
+
commands[8]:
|
|
15
|
+
(none)=project overview, def, refs, callers, outline, impact, config, setup (opt-in session hooks)
|
|
15
16
|
targets:
|
|
16
17
|
name (save), member (User.save) or position (src/a.ts:42 or src/a.ts:42:7)
|
|
17
18
|
flags[4]:
|
|
@@ -32,6 +33,7 @@ const COMMAND_HELP = {
|
|
|
32
33
|
outline: OUTLINE_HELP,
|
|
33
34
|
impact: IMPACT_HELP,
|
|
34
35
|
config: CONFIG_HELP,
|
|
36
|
+
setup: SETUP_HELP,
|
|
35
37
|
};
|
|
36
38
|
function command(name, handler, ctx) {
|
|
37
39
|
return async (args) => (args.includes("-h") ? (COMMAND_HELP[name] ?? "").trimEnd() : handler(args, ctx));
|
|
@@ -70,10 +72,11 @@ export async function main(options = {}) {
|
|
|
70
72
|
outline: command("outline", outlineCommand, ctx),
|
|
71
73
|
impact: command("impact", impactCommand, ctx),
|
|
72
74
|
config: command("config", configCommand, ctx),
|
|
75
|
+
setup: command("setup", (args) => setupCommand(args), ctx),
|
|
73
76
|
},
|
|
74
77
|
getCommandHelp: (name) => COMMAND_HELP[name],
|
|
75
78
|
renderUnknownCommand: (name) => `${renderErrorOutput(`unknown command '${name}'`, "VALIDATION_ERROR", [
|
|
76
|
-
"commands: def, refs, callers, outline, impact, config (no command = project overview)",
|
|
79
|
+
"commands: def, refs, callers, outline, impact, config, setup (no command = project overview)",
|
|
77
80
|
"Run `ts-axi --help` for examples",
|
|
78
81
|
])}\n`,
|
|
79
82
|
formatError,
|
|
@@ -100,11 +100,10 @@ export function positionOf(group) {
|
|
|
100
100
|
export const MATCH_ROWS = 20;
|
|
101
101
|
/** `matches` (first MATCH_ROWS, shallowest first) plus `matches_total` / `truncated` past that. */
|
|
102
102
|
export function matchRows(groups) {
|
|
103
|
+
// `at` pastes into `ts-axi def`; TS signatures carry `export`, and `--exported` narrows to exported ones.
|
|
103
104
|
const rows = groups.slice(0, MATCH_ROWS).map((g) => ({
|
|
104
|
-
|
|
105
|
-
line: g.decls[0].nameLine,
|
|
105
|
+
at: positionOf(g),
|
|
106
106
|
kind: g.primary.kind,
|
|
107
|
-
exported: g.primary.exported,
|
|
108
107
|
signature: g.decls[0].signature,
|
|
109
108
|
}));
|
|
110
109
|
return { matches: rows, ...(groups.length > MATCH_ROWS ? { matches_total: groups.length, truncated: true } : {}) };
|
package/dist/src/commands/def.js
CHANGED
|
@@ -8,7 +8,7 @@ flags[7]:
|
|
|
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>, --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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
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";
|
|
@@ -95,11 +95,30 @@ export async function homeCommand(args, ctx) {
|
|
|
95
95
|
expectPositionals(parsed, 0, "ts-axi [-P <tsconfig>] [--test-glob <glob>]");
|
|
96
96
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
97
97
|
const project = flag(parsed, "--project");
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
const overrides = { testGlobs: flags(parsed, "--test-glob"), ...(project !== undefined ? { project } : {}) };
|
|
99
|
+
// A broken .ts-axi.json must not fail the session-start view: report it and carry on without the file.
|
|
100
|
+
let broken;
|
|
101
|
+
let settings;
|
|
102
|
+
try {
|
|
103
|
+
settings = loadSettings(cwd, overrides);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!(error instanceof AxiError))
|
|
107
|
+
throw error;
|
|
108
|
+
broken = error.message;
|
|
109
|
+
settings = loadSettings(cwd, { ...overrides, ignoreFile: true });
|
|
110
|
+
}
|
|
111
|
+
let tsconfig;
|
|
112
|
+
try {
|
|
113
|
+
tsconfig = resolveTsconfig(cwd, settings);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!(error instanceof AxiError) || settings.sources.project !== SETTINGS_FILE)
|
|
117
|
+
throw error;
|
|
118
|
+
broken = error.message;
|
|
119
|
+
settings = loadSettings(cwd, { ...overrides, ignoreFile: true });
|
|
120
|
+
tsconfig = resolveTsconfig(cwd, settings);
|
|
121
|
+
}
|
|
103
122
|
// Same rule as the semantic commands: no explicit project + a multi-project workspace = workspace scope.
|
|
104
123
|
let workspace;
|
|
105
124
|
if (settings.project === undefined && (tsconfig === undefined || hasMultiProjectSignal(settings.root))) {
|
|
@@ -121,13 +140,17 @@ export async function homeCommand(args, ctx) {
|
|
|
121
140
|
]);
|
|
122
141
|
const testFiles = counted.files.filter((f) => isTestFile(rel(settings.root, f), settings)).length;
|
|
123
142
|
const plus = counted.capped ? "+" : "";
|
|
124
|
-
const present = readSettingsFile(settings.root) !== undefined;
|
|
143
|
+
const present = broken === undefined && readSettingsFile(settings.root) !== undefined;
|
|
125
144
|
const onDefaultBranch = git.branch === undefined || git.branch === "main" || git.branch === "master";
|
|
126
145
|
const help = [];
|
|
127
|
-
if (
|
|
146
|
+
if (broken !== undefined)
|
|
147
|
+
help.push(`Fix or delete ${SETTINGS_FILE} - def, refs, callers, impact and config refuse to run until then (outline still works)`);
|
|
148
|
+
if (broken === undefined && (git.dirty > 0 || !onDefaultBranch)) {
|
|
128
149
|
help.push("Run `ts-axi impact` to see which other code your changes affect");
|
|
129
150
|
}
|
|
130
|
-
help.push("Run `ts-axi outline <file>` to list the declarations in a file",
|
|
151
|
+
help.push("Run `ts-axi outline <file>` to list the declarations in a file", ...(broken === undefined
|
|
152
|
+
? ["Run `ts-axi def <name>` to find where a symbol is declared", "Run `ts-axi config` to see or change the test-file globs"]
|
|
153
|
+
: []), "Run `ts-axi --help` for all commands");
|
|
131
154
|
return out(block({
|
|
132
155
|
root: settings.root,
|
|
133
156
|
...(workspace ? { workspace: `${workspace.kind} (${workspace.projects.length} projects)` } : {}),
|
|
@@ -140,6 +163,6 @@ export async function homeCommand(args, ctx) {
|
|
|
140
163
|
test_globs: settings.testGlobs.length,
|
|
141
164
|
test_files: counted.capped ? `${testFiles}${plus}` : testFiles,
|
|
142
165
|
git: git.text,
|
|
143
|
-
settings: `${SETTINGS_FILE} (${present ? "present" : "absent"})`,
|
|
166
|
+
settings: broken !== undefined ? `${SETTINGS_FILE} (ignored - ${/not valid JSON/.test(broken) ? "not valid JSON" : broken.replace(`${SETTINGS_FILE} `, "")})` : `${SETTINGS_FILE} (${present ? "present" : "absent"})`,
|
|
144
167
|
}), helpBlock(help));
|
|
145
168
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
-
import { bool, intFlag, parseArgs,
|
|
4
|
+
import { bool, flag, intFlag, parseArgs, parseFields } from "../args.js";
|
|
5
5
|
import { changedRanges, parseUnifiedDiff } from "../diff.js";
|
|
6
6
|
import { AxiError, usageError } from "../errors.js";
|
|
7
7
|
import { diffNameStatus, diffUnified, isGitRepo, resolveBaseRev, showFile, untrackedFiles } from "../git.js";
|
|
@@ -24,10 +24,10 @@ input:
|
|
|
24
24
|
(none) = working tree vs HEAD (staged + unstaged + untracked); --staged = index vs HEAD
|
|
25
25
|
<git-range> = any git diff range (main...HEAD, abc123, v1..v2); - = unified diff on stdin (needs the after side checked out)
|
|
26
26
|
analysis is of the current checkout - for a range whose head is not the working tree, check that head out first
|
|
27
|
-
flags[
|
|
28
|
-
--staged, --via (adds References through an interface or base member to refs[n] with a via column), --depth <1-3> (callers_depthN[n]{file,count} for N > 1), --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} ref rows across the report), --grep (adds unresolved[n]{file,line,context} - textual matches that are not References; also searches .astro/.vue/.svelte/.mdx/.html), --test-glob <glob> (repeatable), -P <tsconfig>, --json
|
|
27
|
+
flags[8]:
|
|
28
|
+
--staged, --via (adds References through an interface or base member to refs[n] with a via column), --depth <1-3> (callers_depthN[n]{file,count} for N > 1), --limit <1-${MAX_LIMIT}> (default ${DEFAULT_LIMIT} ref rows across the report), --grep (adds unresolved[n]{file,line,context} - textual matches that are not References; also searches .astro/.vue/.svelte/.mdx/.html), --test-glob <glob> (repeatable), --fields <kind,exported,refs_total,via,files,tests|all> (extra changed[n] columns), -P <tsconfig>, --json
|
|
29
29
|
output:
|
|
30
|
-
base, head, changes (count), changed[n]{symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}
|
|
30
|
+
base, head, changes (count), changed[n]{symbol,change,at,refs_outside} by default; --fields adds columns in place, --fields all gives {symbol,kind,change,exported,at,refs_total,refs_outside,via,files,tests}
|
|
31
31
|
change = signature | removed | type | added | body | modified (before unavailable) - riskiest first
|
|
32
32
|
textual = column added only when an untyped (JavaScript; implicit project) Symbol has more whole-word matches outside the change set than refs_outside - those are uses the checker cannot resolve (declaring file only for a non-exported local); - for typed rows; such a Change is never local_only
|
|
33
33
|
refs_* / files / tests count direct References only; via = References outside the change set that reach the Symbol through an interface or base member it implements (or name an override of it) - they MAY call it
|
|
@@ -38,7 +38,7 @@ output:
|
|
|
38
38
|
templates = column added only when a changed Symbol is one templates name (directive, component, binding, controller, filter): its usages in .html/.htm/.ejs/.hbs/.njk/.pug files; - for other rows; such a Change is never local_only
|
|
39
39
|
template_changes[n]{file,symbols} = templates in the diff that use a changed directive, binding, controller or filter - review them with that change
|
|
40
40
|
skipped[n]{file,reason} = files in the diff that were not analysed (.astro, .json, outside the Project)
|
|
41
|
-
tests: 0 on an exported signature or body change = no Test file references it
|
|
41
|
+
tests: 0 on an exported signature or body change = no Test file references it (columns from --fields exported,tests)
|
|
42
42
|
scope = in a monorepo without -P the program is the touched files + two levels of importers (scope_partial: true when capped)
|
|
43
43
|
examples[5]:
|
|
44
44
|
ts-axi impact
|
|
@@ -54,8 +54,12 @@ const SPECS = [
|
|
|
54
54
|
{ name: "--limit" },
|
|
55
55
|
{ name: "--grep", boolean: true },
|
|
56
56
|
{ name: "--test-glob", repeatable: true },
|
|
57
|
+
{ name: "--fields" },
|
|
57
58
|
{ name: "-P" },
|
|
58
59
|
];
|
|
60
|
+
// Opt-in columns of changed[n]; the default keeps symbol, change, at, refs_outside.
|
|
61
|
+
const IMPACT_FIELDS = ["kind", "exported", "refs_total", "via", "files", "tests"];
|
|
62
|
+
const OPT_IN = new Set(IMPACT_FIELDS);
|
|
59
63
|
async function readStdin() {
|
|
60
64
|
if (process.stdin.isTTY) {
|
|
61
65
|
throw usageError("`impact -` reads a unified diff from stdin and nothing was piped", ["git diff -U0 | ts-axi impact -"]);
|
|
@@ -383,15 +387,33 @@ function capped(key, rows) {
|
|
|
383
387
|
...(rows.length > SIDE_ROWS ? { [`${key}_total`]: rows.length } : {}),
|
|
384
388
|
};
|
|
385
389
|
}
|
|
390
|
+
/** Quotes a value for a pasteable command. */
|
|
391
|
+
function shellWord(value) {
|
|
392
|
+
return /^[\w@%+=:,.\/-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
393
|
+
}
|
|
394
|
+
/** The same `impact` invocation with every given flag replayed, the one `extra` sets replaced by it. */
|
|
386
395
|
function rerun(arg, parsed, extra) {
|
|
387
396
|
const parts = ["ts-axi impact"];
|
|
388
397
|
if (arg !== undefined)
|
|
389
|
-
parts.push(arg);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
398
|
+
parts.push(shellWord(arg));
|
|
399
|
+
const replaced = extra.split(" ")[0];
|
|
400
|
+
for (const spec of SPECS) {
|
|
401
|
+
if (spec.name === replaced)
|
|
402
|
+
continue;
|
|
403
|
+
if ("boolean" in spec) {
|
|
404
|
+
if (bool(parsed, spec.name))
|
|
405
|
+
parts.push(spec.name);
|
|
406
|
+
}
|
|
407
|
+
else if (spec.name === "--fields") {
|
|
408
|
+
const fields = parseFields(flag(parsed, "--fields"), IMPACT_FIELDS);
|
|
409
|
+
if (fields.length > 0)
|
|
410
|
+
parts.push(`--fields ${fields.length === IMPACT_FIELDS.length ? "all" : fields.join(",")}`);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
for (const value of parsed.values.get(spec.name) ?? [])
|
|
414
|
+
parts.push(`${spec.name} ${shellWord(value)}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
395
417
|
parts.push(extra);
|
|
396
418
|
return `${arg === "-" ? "<diff> | " : ""}${parts.join(" ")}`;
|
|
397
419
|
}
|
|
@@ -434,6 +456,7 @@ export async function impactCommand(args, ctx) {
|
|
|
434
456
|
const staged = bool(parsed, "--staged");
|
|
435
457
|
if (staged && arg !== undefined)
|
|
436
458
|
throw usageError("--staged cannot be combined with a range or -", ["Run `ts-axi impact --staged` alone"]);
|
|
459
|
+
const extras = new Set(parseFields(flag(parsed, "--fields"), IMPACT_FIELDS));
|
|
437
460
|
const limit = intFlag(parsed, "--limit", DEFAULT_LIMIT, { min: 1, max: MAX_LIMIT });
|
|
438
461
|
const depth = intFlag(parsed, "--depth", 1, { min: 1, max: 3 });
|
|
439
462
|
const grep = bool(parsed, "--grep");
|
|
@@ -663,6 +686,8 @@ export async function impactCommand(args, ctx) {
|
|
|
663
686
|
help.push(`Run \`ts-axi callers ${top.at}\` for its call sites with the enclosing function`);
|
|
664
687
|
help.push(`Run \`ts-axi def ${top.at}\` for the changed declaration`);
|
|
665
688
|
}
|
|
689
|
+
if (listed.length > 0 && extras.size === 0)
|
|
690
|
+
help.push(`Run \`${rerun(arg, parsed, "--fields files,tests")}\` for how many files and Test files reference each Symbol`);
|
|
666
691
|
const removedTop = listed.find((r) => r.change.change === "removed");
|
|
667
692
|
if (removedTop)
|
|
668
693
|
help.push(`${removedTop.symbol} was removed but is still referenced - those files break`);
|
|
@@ -697,19 +722,23 @@ export async function impactCommand(args, ctx) {
|
|
|
697
722
|
...header,
|
|
698
723
|
...(listed.length > 0
|
|
699
724
|
? {
|
|
700
|
-
changed: listed.map((r) =>
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
725
|
+
changed: listed.map((r) => {
|
|
726
|
+
// Built in the full column order, then cut to the default plus --fields, so --fields all matches the old output.
|
|
727
|
+
const full = {
|
|
728
|
+
symbol: r.symbol,
|
|
729
|
+
kind: r.kind,
|
|
730
|
+
change: r.change.change,
|
|
731
|
+
exported: r.exported,
|
|
732
|
+
at: r.at,
|
|
733
|
+
refs_total: r.all.length,
|
|
734
|
+
refs_outside: r.outside.length,
|
|
735
|
+
via: r.via.length,
|
|
736
|
+
...(gap > 0 ? { textual: r.textual ? r.textual.matches : "-" } : {}),
|
|
737
|
+
...(rows.some((x) => x.template) ? { templates: r.templates ? r.templates.length : "-" } : {}),
|
|
738
|
+
...fileCounts(r),
|
|
739
|
+
};
|
|
740
|
+
return Object.fromEntries(Object.entries(full).filter(([k]) => !OPT_IN.has(k) || extras.has(k)));
|
|
741
|
+
}),
|
|
713
742
|
...(refRows.length > 0 ? { refs: refRows } : {}),
|
|
714
743
|
}
|
|
715
744
|
: {}),
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { posix, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { bool, flag, parseArgs } from "../args.js";
|
|
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
8
|
const MAX_ROWS = 300;
|
|
9
9
|
export const OUTLINE_HELP = `usage: ts-axi outline <file | glob>... [flags]
|
|
10
|
-
flags[
|
|
11
|
-
--exported (alias --exports; exported Declarations only), -P <tsconfig> (file list for globs), --json
|
|
10
|
+
flags[4]:
|
|
11
|
+
--exported (alias --exports; exported Declarations only), --fields exported (adds an exported column), -P <tsconfig> (file list for globs), --json
|
|
12
12
|
output:
|
|
13
|
-
one file = file + decls[n]{line,kind,name,
|
|
13
|
+
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
14
|
class members follow their class as Class.member; at most ${MAX_ROWS} rows
|
|
15
15
|
kind state = a ui-router / ngRoute state as states.<name> (routes.<path>); its callbacks follow as states.<name>.resolve.<key> / states.<name>.onEnter
|
|
16
16
|
examples[3]:
|
|
@@ -25,12 +25,14 @@ function isSource(path) {
|
|
|
25
25
|
export async function outlineCommand(args, ctx) {
|
|
26
26
|
const parsed = parseArgs(args, [
|
|
27
27
|
{ name: "--exported", alias: "--exports", boolean: true },
|
|
28
|
+
{ name: "--fields" },
|
|
28
29
|
{ name: "-P" },
|
|
29
30
|
], COMMAND);
|
|
30
31
|
if (parsed.positionals.length === 0) {
|
|
31
32
|
throw usageError("missing argument", [`usage: ${COMMAND} <file | glob>... [--exported]`]);
|
|
32
33
|
}
|
|
33
34
|
const exportedOnly = bool(parsed, "--exported");
|
|
35
|
+
const showExported = parseFields(flag(parsed, "--fields"), ["exported"]).length > 0;
|
|
34
36
|
const override = flag(parsed, "-P");
|
|
35
37
|
const root = findRepoRoot(ctx.cwd);
|
|
36
38
|
const cwdPrefix = relative(root, resolve(ctx.cwd)).split(sep).join("/");
|
|
@@ -73,11 +75,10 @@ export async function outlineCommand(args, ctx) {
|
|
|
73
75
|
const shown = decls.slice(0, MAX_ROWS);
|
|
74
76
|
const single = unique.length === 1;
|
|
75
77
|
const rows = shown.map((d) => ({
|
|
76
|
-
...(single ? {} : {
|
|
77
|
-
line: d.nameLine,
|
|
78
|
+
...(single ? { line: d.nameLine } : { at: `${d.file}:${d.nameLine}` }),
|
|
78
79
|
kind: d.kind,
|
|
79
80
|
name: d.qualifiedName,
|
|
80
|
-
exported: d.exported,
|
|
81
|
+
...(showExported ? { exported: d.exported } : {}),
|
|
81
82
|
signature: d.signature,
|
|
82
83
|
}));
|
|
83
84
|
const help = [];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { installSessionStartHooks, sessionStartHookStatus, uninstallSessionStartHooks } from "axi-sdk-js";
|
|
3
|
+
import { bool, expectPositionals, parseArgs } from "../args.js";
|
|
4
|
+
import { axiError, usageError } from "../errors.js";
|
|
5
|
+
import { block, helpBlock, out } from "../render.js";
|
|
6
|
+
export const SETUP_HELP = `usage: ts-axi setup <hooks|status|uninstall> [--project]
|
|
7
|
+
Manage the opt-in SessionStart integration that shows the ts-axi project overview to Claude Code, Codex and OpenCode at session start.
|
|
8
|
+
flags:
|
|
9
|
+
--project (install into the current repository's agent config instead of your user config)
|
|
10
|
+
notes:
|
|
11
|
+
The overview only reads the file tree and git status: no compiler run, no network, exit 0 in any directory (outside a TS/JS project it is a few lines)
|
|
12
|
+
Only these hooks are touched; others in the same files are kept
|
|
13
|
+
examples[3]:
|
|
14
|
+
ts-axi setup hooks
|
|
15
|
+
ts-axi setup hooks --project
|
|
16
|
+
ts-axi setup status
|
|
17
|
+
`;
|
|
18
|
+
// distEntrypoints names the built entrypoint explicitly; the SDK also infers it, so `node dist/bin/ts-axi.js setup hooks` installs either way.
|
|
19
|
+
const IDENTITY = { marker: "ts-axi", binaryNames: ["ts-axi"], distEntrypoints: ["dist/bin/ts-axi.js"] };
|
|
20
|
+
export async function setupCommand(args) {
|
|
21
|
+
const [sub, ...rest] = args;
|
|
22
|
+
if (sub !== "hooks" && sub !== "status" && sub !== "uninstall") {
|
|
23
|
+
throw usageError(sub ? `unknown setup action '${sub}'` : "missing setup action", [
|
|
24
|
+
"Run `ts-axi setup hooks` to install, `ts-axi setup status` to inspect, `ts-axi setup uninstall` to remove",
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
const command = `ts-axi setup ${sub}`;
|
|
28
|
+
const parsed = parseArgs(rest, [{ name: "--project", boolean: true }], command);
|
|
29
|
+
expectPositionals(parsed, 0, `${command} [--project]`);
|
|
30
|
+
const scope = bool(parsed, "--project") ? "project" : "user";
|
|
31
|
+
const flagHint = scope === "project" ? " --project" : "";
|
|
32
|
+
if (sub === "status") {
|
|
33
|
+
const status = sessionStartHookStatus({ ...IDENTITY, scope });
|
|
34
|
+
// Codex runs hooks only with `[features] hooks = true` in the user config.toml.
|
|
35
|
+
const codexOn = status.codex.installed && status.codex.userFeatureEnabled;
|
|
36
|
+
const agents = { claude_code: status.claude.installed, codex: codexOn, opencode: status.opencode.installed };
|
|
37
|
+
const missing = Object.values(agents).some((installed) => !installed);
|
|
38
|
+
return out(block({
|
|
39
|
+
hooks: {
|
|
40
|
+
scope,
|
|
41
|
+
claude_code: agents.claude_code ? "installed" : "not installed",
|
|
42
|
+
codex: codexOn ? "installed" : status.codex.installed ? "installed (hooks feature off)" : "not installed",
|
|
43
|
+
opencode: agents.opencode ? "installed" : "not installed",
|
|
44
|
+
},
|
|
45
|
+
}), helpBlock(missing ? [`Run \`ts-axi setup hooks${flagHint}\` to install the missing ones`] : []));
|
|
46
|
+
}
|
|
47
|
+
if (sub === "uninstall") {
|
|
48
|
+
const errors = [];
|
|
49
|
+
uninstallSessionStartHooks({ ...IDENTITY, scope, onError: (message) => errors.push(message) });
|
|
50
|
+
const left = installedAgents(scope);
|
|
51
|
+
if (errors.length > 0 || left.length > 0) {
|
|
52
|
+
throw axiError(`hooks not fully removed${left.length > 0 ? ` (still installed: ${left.join("; ")})` : ""}`, "SETUP_FAILED", errors);
|
|
53
|
+
}
|
|
54
|
+
return block({ hooks: { scope, status: "removed (or were not installed)" } });
|
|
55
|
+
}
|
|
56
|
+
const before = configSnapshot(scope);
|
|
57
|
+
// The SDK skips exec paths it does not recognise and reports write failures only via onError, so verify by re-reading.
|
|
58
|
+
const errors = [];
|
|
59
|
+
installSessionStartHooks({ ...IDENTITY, scope, onError: (message) => errors.push(message) });
|
|
60
|
+
const installed = installedAgents(scope, true);
|
|
61
|
+
if (installed.length === 0) {
|
|
62
|
+
throw axiError("no hooks were installed", "SETUP_FAILED", errors.length > 0 ? errors : [
|
|
63
|
+
"Hooks are only installed from an installed binary, not npx: run `npm i -g ts-axi`, then `ts-axi setup hooks`",
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
if (errors.length > 0 || installed.length < 3) {
|
|
67
|
+
throw axiError(`hooks only partly installed (installed: ${installed.join("; ")})`, "SETUP_FAILED", errors);
|
|
68
|
+
}
|
|
69
|
+
if (configSnapshot(scope) === before) {
|
|
70
|
+
return block({ hooks: { scope, status: "already installed (no-op)", integrations: installed.join("; ") } });
|
|
71
|
+
}
|
|
72
|
+
return out(block({ hooks: { scope, status: "installed", integrations: installed.join("; ") } }), helpBlock(["Restart the agent session to get the ts-axi project overview as ambient context"]));
|
|
73
|
+
}
|
|
74
|
+
/** Contents of the agent config files the hooks live in; equal before and after = nothing was written. */
|
|
75
|
+
function configSnapshot(scope) {
|
|
76
|
+
const status = sessionStartHookStatus({ ...IDENTITY, scope });
|
|
77
|
+
return [status.claude.path, status.codex.path, status.codex.userFeaturePath, status.opencode.path]
|
|
78
|
+
.map((path) => {
|
|
79
|
+
try {
|
|
80
|
+
return readFileSync(path, "utf8");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.join("\0");
|
|
87
|
+
}
|
|
88
|
+
/** `active`: count Codex only when its hooks feature is on (after an install); uninstall checks the entries alone. */
|
|
89
|
+
function installedAgents(scope, active = false) {
|
|
90
|
+
const status = sessionStartHookStatus({ ...IDENTITY, scope });
|
|
91
|
+
const agents = [
|
|
92
|
+
["Claude Code", status.claude.installed],
|
|
93
|
+
["Codex", status.codex.installed && (!active || status.codex.userFeatureEnabled)],
|
|
94
|
+
["OpenCode", status.opencode.installed],
|
|
95
|
+
];
|
|
96
|
+
return agents.filter(([, installed]) => installed).map(([agent]) => agent);
|
|
97
|
+
}
|
package/dist/src/errors.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { AxiError, exitCodeForError as sdkExitCode } from "axi-sdk-js";
|
|
2
2
|
export { AxiError };
|
|
3
|
-
/** SDK mapping
|
|
3
|
+
/** SDK mapping: 2 for usage errors, 1 otherwise (`GIT_ERROR` included). */
|
|
4
4
|
export function exitCodeForError(error) {
|
|
5
|
-
if (error instanceof AxiError && error.code === "GIT_ERROR")
|
|
6
|
-
return 3;
|
|
7
5
|
return sdkExitCode(error);
|
|
8
6
|
}
|
|
9
7
|
export function usageError(message, suggestions = []) {
|
package/dist/src/git.js
CHANGED
|
@@ -42,7 +42,12 @@ function gitError(result, args) {
|
|
|
42
42
|
const message = first.replace(/^(fatal|error): /, "");
|
|
43
43
|
const suggestions = /not a git repository/i.test(first)
|
|
44
44
|
? ["Run `ts-axi impact -` with a unified diff on stdin instead"]
|
|
45
|
-
:
|
|
45
|
+
: /unknown revision|bad revision|invalid object name|no merge base/i.test(first)
|
|
46
|
+
? [
|
|
47
|
+
"Run `git branch -a` or `git log --oneline -5` to pick an existing revision",
|
|
48
|
+
"Run `ts-axi impact` for uncommitted changes, or `ts-axi impact <base>...HEAD` for a branch",
|
|
49
|
+
]
|
|
50
|
+
: [];
|
|
46
51
|
return new AxiError(message, "GIT_ERROR", suggestions);
|
|
47
52
|
}
|
|
48
53
|
async function runOrThrow(cwd, args) {
|
package/dist/src/settings.js
CHANGED
|
@@ -74,7 +74,7 @@ export function writeSettingsFile(root, data) {
|
|
|
74
74
|
}
|
|
75
75
|
export function loadSettings(cwd, overrides = {}) {
|
|
76
76
|
const root = findRoot(cwd);
|
|
77
|
-
const file = readSettingsFile(root);
|
|
77
|
+
const file = overrides.ignoreFile ? undefined : readSettingsFile(root);
|
|
78
78
|
const fileGlobs = file?.testGlobs ?? [];
|
|
79
79
|
// An empty list cannot replace the defaults: that would turn every file into a non-test.
|
|
80
80
|
const replace = file?.testGlobsReplace === true && fileGlobs.length > 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-axi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "AXI-compliant TypeScript symbol CLI for agents - definitions, references, callers, file outlines and change impact (blast radius) with token-efficient TOON output",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|