ts-axi 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cristian Mos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,120 @@
1
- # Temporary Holding Version
1
+ # ts-axi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Agent-ergonomic **TypeScript / JavaScript** code CLI, built on the [AXI](https://axi.md) principles: token-efficient [TOON](https://toonformat.dev/) output, minimal schemas, pre-computed aggregates, definitive empty states, structured errors and next-step hints.
4
+
5
+ It is **change-impact first**. When an agent reviews a diff, the expensive question is "what else does this touch?" - normally answered by grepping and reading dozens of files. `ts-axi impact` answers it from the compiler's own symbol resolution: for every declaration the diff changes, it lists the references *outside* the change set, grouped by file, with test coverage counted separately.
6
+
7
+ ```
8
+ # real output: test/integration/sample on a branch that adds a `locale` parameter
9
+ # to formatPrice and edits the body of Cart.total
10
+ $ ts-axi impact main...HEAD
11
+ base: 923fb4d7ddc8f4e5c4df9029be388f8cb29ce095
12
+ head: HEAD
13
+ changes: 2
14
+ changed[2]{symbol,kind,change,exported,at,refs_total,refs_outside,files,tests}:
15
+ formatPrice,function,signature,true,"src/lib/format.ts:5",13,12,6,2
16
+ Cart.total,method,body,true,"src/lib/cart.ts:32",8,8,5,3
17
+ refs[20]{symbol,file,line,context}:
18
+ formatPrice,src/components/Counter.tsx,2,"import { formatPrice } from '../lib';"
19
+ formatPrice,src/components/Counter.tsx,12,"{cups} cups = {formatPrice(cups * CUP_CENTS)}"
20
+ formatPrice,src/lib/analytics.js,4,"return { event: 'purchase', value: formatPrice(cents, 'EUR') };"
21
+ ...
22
+ Cart.total,src/components/CartSummary.tsx,10,setTotal(cart.total());
23
+ Cart.total,src/hooks/useCart.ts,35,"total: cart.total(),"
24
+ ...
25
+ blind_spots[1]{symbol,file,line,reason}:
26
+ Cart.total,src/components/CartSummary.tsx,16,string key 'total' on a type without that member
27
+ elapsed_ms: 679
28
+ help[5]:
29
+ - "Run `ts-axi refs src/lib/format.ts:5` for every Reference of formatPrice"
30
+ - "Run `ts-axi callers src/lib/format.ts:5` for its call sites with the enclosing function"
31
+ - "Run `ts-axi def src/lib/format.ts:5` for the changed declaration"
32
+ - Run `ts-axi impact main...HEAD --grep` for textual matches the checker cannot see
33
+ - Run `ts-axi impact main...HEAD --depth 2` for who calls the callers
34
+ ```
35
+
36
+ (`...` marks rows cut from this README; the command prints all 20.)
37
+
38
+ Read-only: no lint, no refactor, no rename.
39
+
40
+ > **Status: early (v0.1.x).**
41
+
42
+ ## Install
43
+
44
+ Requires Node.js 20+.
45
+
46
+ ```sh
47
+ npm install -g ts-axi
48
+ ```
49
+
50
+ Then tell your agent about it - one line in `CLAUDE.md` / `AGENTS.md`:
51
+
52
+ ```
53
+ 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.
54
+ ```
55
+
56
+ Or install the on-demand Agent Skill (no per-session cost):
57
+
58
+ ```sh
59
+ npx skills add cmosgh/ts-axi --skill ts-axi
60
+ ```
61
+
62
+ ## Commands
63
+
64
+ | Command | What it does |
65
+ | --- | --- |
66
+ | `ts-axi` | Home view: project root, tsconfig used (or implicit), source file count, test-file glob and match counts, git branch and dirty count, next steps. Under a second, exit 0 everywhere |
67
+ | `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` |
68
+ | `refs <target>` | References of one symbol, grouped by file, with test counts. `--limit`, `--kind calls` |
69
+ | `callers <target>` | Incoming calls of a function-like symbol. `--depth 1..3` |
70
+ | `outline <file>` | Top-level declarations in source order, class members nested. `--exports` |
71
+ | `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, test-file counts, `blind_spots`, `local_only`, `file_level` and `skipped` (non-TS files in the diff). `--limit`, `--depth 2..3`, `--grep`. Analyses the current checkout |
72
+ | `config` | Show effective settings with their source; `config set <key> <value>` and `config unset <key>` write `.ts-axi.json` |
73
+
74
+ `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.
75
+
76
+ <!-- TODO(phase 4): paste real examples for def / refs / outline / config from the fixture project. -->
77
+
78
+ ## Test files
79
+
80
+ A file counts as a test by path pattern. Defaults: `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`, `**/e2e/**`. The home view shows `test_globs` and `test_files` so a wrong rule is visible.
81
+
82
+ ## `.ts-axi.json`
83
+
84
+ Optional, at the git root (or the current directory outside a repository). Manage it with `ts-axi config`, or edit it by hand:
85
+
86
+ ```json
87
+ { "testGlobs": ["**/*.it.ts", "packages/*/qa/**"], "testGlobsReplace": false, "project": "tsconfig.app.json" }
88
+ ```
89
+
90
+ | Key | Meaning |
91
+ | --- | --- |
92
+ | `testGlobs` | Extra test-file globs, added to the defaults |
93
+ | `testGlobsReplace` | `true` makes `testGlobs` replace the defaults |
94
+ | `project` | Default tsconfig, relative to the repo root; `-P` overrides it |
95
+
96
+ Any other key is a `VALIDATION_ERROR` naming the valid ones. `--test-glob <glob>` adds a glob for one run.
97
+
98
+ ## How it works
99
+
100
+ One dependency: the `typescript` compiler API (ADR in `docs/adr/0001-typescript-language-service-engine.md`).
101
+
102
+ - `outline`, `def` and the home view only parse files (`ts.createSourceFile`): no tsconfig needed, well under a second.
103
+ - `refs`, `callers` and `impact` build a `LanguageService` over the nearest `tsconfig.json` (or an implicit project of every source file when none exists, with a warning) and ask it for references and call hierarchy. Symbol identity comes from the checker, so `user.save` resolves to `User.save` through imports and re-exports.
104
+ - `impact` reads the "before" version of touched files with `git show` and compares signatures to classify each change (`added`, `removed`, `signature`, `body`, `type`, `modified`). Uses it cannot follow (access through `any`, dynamic string keys) are reported as `blind_spots`, never silently dropped.
105
+ - Type errors in the analysed project never block anything; they are reported as a `diagnostics` count.
106
+
107
+ Every semantic command starts cold and prints `elapsed_ms`.
108
+
109
+ ## Development
110
+
111
+ ```sh
112
+ corepack pnpm install
113
+ corepack pnpm run check # typecheck, tests, build, skill freshness
114
+ ```
115
+
116
+ `skills/ts-axi/SKILL.md` is generated: `corepack pnpm run build:skill`. See `PLAN.md` for the spec and `GLOSSARY.md` for the vocabulary.
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { tryFastPath } from "axi-sdk-js/fast-path";
3
+ import { VERSION } from "../src/version.js";
4
+ if (!tryFastPath(process.argv.slice(2), { version: VERSION })) {
5
+ const { main } = await import("../src/cli.js");
6
+ await main();
7
+ }
@@ -0,0 +1,184 @@
1
+ import { usageError } from "./errors.js";
2
+ /**
3
+ * Flags agents commonly carry over from other CLIs. A targeted hint lets the
4
+ * agent self-correct in one turn instead of scanning the generic flag list.
5
+ */
6
+ const RENAMED = {
7
+ "--project": "-P",
8
+ "--tsconfig": "-P",
9
+ "--type": "--kind",
10
+ "--exports": "--exported",
11
+ "--include": "--in",
12
+ "--path": "--in",
13
+ "--max": "--limit",
14
+ "--all": "--full",
15
+ };
16
+ /** Flags whose value is a positional argument in ts-axi. */
17
+ const POSITIONAL = {
18
+ "--file": "pass the file as a positional argument",
19
+ "--symbol": "pass the symbol as a positional argument",
20
+ "--name": "pass the symbol as a positional argument",
21
+ };
22
+ /**
23
+ * Strict declarative parser: every flag must be declared, unknown flags fail
24
+ * loud with the valid set inline (AXI principle 6). Supports `--flag value`,
25
+ * `--flag=value`, boolean flags, repeatable flags, aliases and a `--`
26
+ * terminator. A bare `-` is a positional (stdin sentinel).
27
+ */
28
+ export function parseArgs(args, specs, command) {
29
+ const byName = new Map();
30
+ for (const spec of specs) {
31
+ byName.set(spec.name, spec);
32
+ if (spec.alias)
33
+ byName.set(spec.alias, spec);
34
+ }
35
+ const parsed = {
36
+ values: new Map(),
37
+ bools: new Set(),
38
+ positionals: [],
39
+ };
40
+ const unknown = [];
41
+ for (let i = 0; i < args.length; i++) {
42
+ const token = args[i];
43
+ if (token === "--") {
44
+ parsed.positionals.push(...args.slice(i + 1));
45
+ break;
46
+ }
47
+ if (token === "-" || !token.startsWith("-")) {
48
+ parsed.positionals.push(token);
49
+ continue;
50
+ }
51
+ const eq = token.indexOf("=");
52
+ const name = eq === -1 ? token : token.slice(0, eq);
53
+ const inlineValue = eq === -1 ? undefined : token.slice(eq + 1);
54
+ if (name === "--help" || name === "-h")
55
+ continue;
56
+ const spec = byName.get(name);
57
+ if (!spec) {
58
+ if (!unknown.includes(name))
59
+ unknown.push(name);
60
+ continue;
61
+ }
62
+ if (spec.boolean) {
63
+ if (inlineValue !== undefined) {
64
+ throw usageError(`${spec.name} does not take a value`, [
65
+ `Run \`${command} --help\``,
66
+ ]);
67
+ }
68
+ parsed.bools.add(spec.name);
69
+ continue;
70
+ }
71
+ let value = inlineValue;
72
+ if (value === undefined) {
73
+ const next = args[i + 1];
74
+ // A following declared flag (or `--`) means the value is missing. Other
75
+ // dash-leading tokens are accepted so bodies like "- item" still work.
76
+ if (next === undefined || next === "--" || byName.has(next)) {
77
+ throw usageError(`${spec.name} requires a value`, [
78
+ `Run \`${command} --help\``,
79
+ ]);
80
+ }
81
+ value = next;
82
+ i++;
83
+ }
84
+ if (value.trim() === "") {
85
+ throw usageError(`${spec.name} requires a non-empty value`);
86
+ }
87
+ const existing = parsed.values.get(spec.name);
88
+ if (existing && !spec.repeatable) {
89
+ throw usageError(`${spec.name} may only be given once`);
90
+ }
91
+ parsed.values.set(spec.name, [...(existing ?? []), value]);
92
+ }
93
+ if (unknown.length > 0) {
94
+ const valid = specs.map((s) => s.name).join(", ") || "(none)";
95
+ const renamed = unknown
96
+ .filter((flag) => RENAMED[flag] && byName.has(RENAMED[flag]))
97
+ .map((flag) => `${flag} is not supported here; use ${RENAMED[flag]} instead`);
98
+ const positional = unknown
99
+ .filter((flag) => POSITIONAL[flag])
100
+ .map((flag) => `${flag} is not supported; ${POSITIONAL[flag]}`);
101
+ throw usageError(`unknown flag${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")} for \`${command}\``, [
102
+ ...renamed,
103
+ ...positional,
104
+ `valid flags for \`${command}\`: ${valid} (--help always allowed)`,
105
+ ]);
106
+ }
107
+ return parsed;
108
+ }
109
+ export function flag(parsed, name) {
110
+ return parsed.values.get(name)?.[0];
111
+ }
112
+ export function flags(parsed, name) {
113
+ return parsed.values.get(name) ?? [];
114
+ }
115
+ export function bool(parsed, name) {
116
+ return parsed.bools.has(name);
117
+ }
118
+ export function requireFlag(parsed, name, usage) {
119
+ const value = flag(parsed, name);
120
+ if (value === undefined) {
121
+ throw usageError(`${name} is required`, [usage]);
122
+ }
123
+ return value;
124
+ }
125
+ export function intFlag(parsed, name, fallback, range) {
126
+ const raw = flag(parsed, name);
127
+ if (raw === undefined)
128
+ return fallback;
129
+ if (!/^\d+$/.test(raw)) {
130
+ throw usageError(`${name} must be a whole number, got '${raw}'`);
131
+ }
132
+ const value = Number(raw);
133
+ if (value < range.min || value > range.max) {
134
+ throw usageError(`${name} must be between ${range.min} and ${range.max}`);
135
+ }
136
+ return value;
137
+ }
138
+ export function enumFlag(parsed, name, allowed, fallback) {
139
+ const raw = flag(parsed, name);
140
+ if (raw === undefined)
141
+ return fallback;
142
+ const value = raw.toLowerCase();
143
+ if (!allowed.includes(value)) {
144
+ throw usageError(`invalid ${name} '${raw}'`, [
145
+ `valid values: ${allowed.join(", ")}`,
146
+ ]);
147
+ }
148
+ return value;
149
+ }
150
+ /** Require exactly `count` positionals; extra or missing ones are usage errors. */
151
+ export function expectPositionals(parsed, count, usage) {
152
+ if (parsed.positionals.length !== count) {
153
+ const problem = parsed.positionals.length < count
154
+ ? "missing argument"
155
+ : `unexpected argument '${parsed.positionals[count]}'`;
156
+ throw usageError(problem, [`usage: ${usage}`]);
157
+ }
158
+ return parsed.positionals;
159
+ }
160
+ export function positiveId(raw, label) {
161
+ const cleaned = raw?.replace(/^#/, "");
162
+ if (!cleaned || !/^\d+$/.test(cleaned) || Number(cleaned) < 1) {
163
+ throw usageError(`invalid ${label} '${raw ?? ""}' - expected a positive number`);
164
+ }
165
+ return Number(cleaned);
166
+ }
167
+ /** Parse a comma-separated `--fields` value against the supported extras. */
168
+ export function parseFields(raw, available) {
169
+ if (raw === undefined)
170
+ return [];
171
+ const requested = [
172
+ ...new Set(raw
173
+ .split(",")
174
+ .map((f) => f.trim())
175
+ .filter(Boolean)),
176
+ ];
177
+ const bad = requested.filter((f) => !available.includes(f));
178
+ if (bad.length > 0) {
179
+ throw usageError(`unknown field${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}`, [
180
+ `available --fields: ${available.join(", ")}`,
181
+ ]);
182
+ }
183
+ return requested;
184
+ }
@@ -0,0 +1,94 @@
1
+ import { runAxiCli } from "axi-sdk-js";
2
+ import { configCommand, CONFIG_HELP } from "./commands/config.js";
3
+ import { defCommand, DEF_HELP } from "./commands/def.js";
4
+ import { homeCommand } from "./commands/home.js";
5
+ import { impactCommand, IMPACT_HELP } from "./commands/impact.js";
6
+ import { outlineCommand, OUTLINE_HELP } from "./commands/outline.js";
7
+ import { callersCommand, CALLERS_HELP, refsCommand, REFS_HELP } from "./commands/refs.js";
8
+ import { AxiError, exitCodeForError } from "./errors.js";
9
+ import { renderErrorOutput, setJsonMode, toJson } from "./render.js";
10
+ import { VERSION } from "./version.js";
11
+ 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
+ export const TOP_HELP = `usage: ts-axi [command] [args] [flags]
13
+ commands[7]:
14
+ (none)=project overview, def, refs, callers, outline, impact, config
15
+ targets:
16
+ 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
19
+ examples:
20
+ ts-axi
21
+ ts-axi def User.save
22
+ ts-axi refs src/models/user.ts:12
23
+ ts-axi callers runSync --depth 2
24
+ ts-axi outline src/models/user.ts
25
+ ts-axi impact main...HEAD
26
+ ts-axi refs --help
27
+ `;
28
+ const COMMAND_HELP = {
29
+ def: DEF_HELP,
30
+ refs: REFS_HELP,
31
+ callers: CALLERS_HELP,
32
+ outline: OUTLINE_HELP,
33
+ impact: IMPACT_HELP,
34
+ config: CONFIG_HELP,
35
+ };
36
+ function command(name, handler, ctx) {
37
+ return async (args) => (args.includes("-h") ? (COMMAND_HELP[name] ?? "").trimEnd() : handler(args, ctx));
38
+ }
39
+ function formatError(error) {
40
+ const axi = error instanceof AxiError
41
+ ? error
42
+ : new AxiError(error instanceof Error ? error.message : String(error), "UNKNOWN");
43
+ return {
44
+ output: `${renderErrorOutput(axi.message, axi.code, axi.suggestions)}\n`,
45
+ exitCode: exitCodeForError(axi),
46
+ };
47
+ }
48
+ export async function main(options = {}) {
49
+ const argv = options.argv ?? process.argv.slice(2);
50
+ // Global flag: stripped here because the SDK rejects flags before the command.
51
+ const json = argv.includes("--json");
52
+ setJsonMode(json);
53
+ const stdout = options.stdout ?? process.stdout;
54
+ const ctx = { cwd: options.cwd ?? process.cwd(), ...(options.stdin !== undefined ? { stdin: options.stdin } : {}) };
55
+ // home/config may take the context as an optional second parameter.
56
+ const home = homeCommand;
57
+ let buffered = "";
58
+ try {
59
+ await runAxiCli({
60
+ argv: json ? argv.filter((a) => a !== "--json") : argv,
61
+ stdout: json ? { write: (chunk) => (buffered += chunk) } : stdout,
62
+ description: DESCRIPTION,
63
+ version: VERSION,
64
+ topLevelHelp: TOP_HELP,
65
+ home: (args) => home(args, ctx),
66
+ commands: {
67
+ def: command("def", defCommand, ctx),
68
+ refs: command("refs", refsCommand, ctx),
69
+ callers: command("callers", callersCommand, ctx),
70
+ outline: command("outline", outlineCommand, ctx),
71
+ impact: command("impact", impactCommand, ctx),
72
+ config: command("config", configCommand, ctx),
73
+ },
74
+ getCommandHelp: (name) => COMMAND_HELP[name],
75
+ renderUnknownCommand: (name) => `${renderErrorOutput(`unknown command '${name}'`, "VALIDATION_ERROR", [
76
+ "commands: def, refs, callers, outline, impact, config (no command = project overview)",
77
+ "Run `ts-axi --help` for examples",
78
+ ])}\n`,
79
+ formatError,
80
+ });
81
+ }
82
+ finally {
83
+ setJsonMode(false);
84
+ }
85
+ if (!json)
86
+ return;
87
+ if (stdout === process.stdout) {
88
+ process.stdout.on("error", (error) => {
89
+ if (error.code !== "EPIPE")
90
+ throw error;
91
+ });
92
+ }
93
+ stdout.write(toJson(buffered));
94
+ }