howtofixcli 0.7.13 → 0.7.15

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.
Files changed (161) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-6FUWMLWO.js +1 -0
  5. package/dist/chunk-7CHDNTH6.js +1 -0
  6. package/dist/chunk-7DTQKDO4.js +2 -0
  7. package/dist/chunk-AGGIYXN7.js +2 -0
  8. package/dist/chunk-AUDMRGWF.js +189 -0
  9. package/dist/chunk-CPJKKHGH.js +1 -0
  10. package/dist/chunk-DAPXGSGD.js +12 -0
  11. package/dist/chunk-DOD7MUGX.js +1 -0
  12. package/dist/chunk-DV2BBBBJ.js +2 -0
  13. package/dist/chunk-EIBOS5KO.js +5 -0
  14. package/dist/chunk-EYWOVCSS.js +6 -0
  15. package/dist/chunk-FPXVDP32.js +8 -0
  16. package/dist/chunk-GXB7RZHH.js +1 -0
  17. package/dist/chunk-HT3MFLAV.js +34 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-JMKK26AT.js +2 -0
  21. package/dist/chunk-LAHVHRCS.js +2 -0
  22. package/dist/chunk-LOOFOBTS.js +1 -0
  23. package/dist/chunk-LSJC73DG.js +1 -0
  24. package/dist/chunk-MOKUBKE7.js +1 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-SX5VE6XA.js +110 -0
  30. package/dist/chunk-TSEKPGK4.js +1 -0
  31. package/dist/chunk-UNK5HRQR.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V2RPZMLK.js +1 -0
  34. package/dist/chunk-V3R4FYTJ.js +1 -0
  35. package/dist/chunk-V656XVXV.js +8 -0
  36. package/dist/chunk-VLMQSD25.js +1 -0
  37. package/dist/chunk-WKUJ7CFO.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-Y7YM5R75.js +5 -0
  42. package/dist/chunk-YW7NORH3.js +2 -0
  43. package/dist/main.js +99 -540
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/activity.js +0 -124
  48. package/dist/agent-defs.js +0 -237
  49. package/dist/agent-limiter.js +0 -68
  50. package/dist/agents.js +0 -554
  51. package/dist/anim.js +0 -361
  52. package/dist/args.js +0 -228
  53. package/dist/at-file.js +0 -28
  54. package/dist/brand-extension.js +0 -87
  55. package/dist/brand.js +0 -76
  56. package/dist/browser.js +0 -58
  57. package/dist/cache-report.js +0 -139
  58. package/dist/checkpoint.js +0 -437
  59. package/dist/claims.js +0 -167
  60. package/dist/cli-auth.js +0 -142
  61. package/dist/cli-checkin.js +0 -120
  62. package/dist/cli-credentials.js +0 -254
  63. package/dist/clipboard.js +0 -149
  64. package/dist/collapse.js +0 -129
  65. package/dist/commands.js +0 -294
  66. package/dist/compaction-guard.js +0 -81
  67. package/dist/context-extension.js +0 -17
  68. package/dist/dcp/commands.js +0 -172
  69. package/dist/dcp/compress-state.js +0 -139
  70. package/dist/dcp/compress.js +0 -329
  71. package/dist/dcp/dcp-extension.js +0 -295
  72. package/dist/dcp/dedup.js +0 -93
  73. package/dist/dcp/ids.js +0 -189
  74. package/dist/dcp/index.js +0 -25
  75. package/dist/dcp/middle-prune.js +0 -76
  76. package/dist/dcp/prompts.js +0 -68
  77. package/dist/dcp/protected-content.js +0 -221
  78. package/dist/dcp/protected.js +0 -161
  79. package/dist/dcp/prune.js +0 -418
  80. package/dist/dcp/state.js +0 -176
  81. package/dist/device-id.js +0 -90
  82. package/dist/device-id.test.js +0 -23
  83. package/dist/errors.js +0 -63
  84. package/dist/extensions.js +0 -48
  85. package/dist/fatal.js +0 -35
  86. package/dist/feed.js +0 -498
  87. package/dist/fs-atomic.js +0 -54
  88. package/dist/gateway-errors.js +0 -90
  89. package/dist/git.js +0 -230
  90. package/dist/goal-extension.js +0 -88
  91. package/dist/goal.js +0 -222
  92. package/dist/hooks-extension.js +0 -176
  93. package/dist/hooks.js +0 -443
  94. package/dist/howtofix-account.js +0 -735
  95. package/dist/hud-line.js +0 -153
  96. package/dist/hwid.js +0 -130
  97. package/dist/interrupt.js +0 -60
  98. package/dist/json-events.js +0 -50
  99. package/dist/level-style.js +0 -112
  100. package/dist/loop-guard.js +0 -95
  101. package/dist/lsp-tools.js +0 -32
  102. package/dist/lsp.js +0 -429
  103. package/dist/mcp-config.js +0 -85
  104. package/dist/mcp-lifecycle.js +0 -147
  105. package/dist/model-catalog.js +0 -217
  106. package/dist/model-facts.js +0 -35
  107. package/dist/model-resolve.js +0 -65
  108. package/dist/notes.js +0 -78
  109. package/dist/oauth-login.js +0 -568
  110. package/dist/paths.js +0 -61
  111. package/dist/permissions.js +0 -298
  112. package/dist/plan-mode.js +0 -363
  113. package/dist/preset-command.js +0 -227
  114. package/dist/preset-extension.js +0 -35
  115. package/dist/preset-mcp.js +0 -292
  116. package/dist/presets.js +0 -727
  117. package/dist/prompt-files.js +0 -103
  118. package/dist/prompt-text.js +0 -41
  119. package/dist/proxy.js +0 -137
  120. package/dist/quality/ast.js +0 -196
  121. package/dist/quality/cognitive.js +0 -83
  122. package/dist/quality/cyclomatic.js +0 -30
  123. package/dist/quality/halstead.js +0 -83
  124. package/dist/quality/index.js +0 -18
  125. package/dist/quality/length.js +0 -3
  126. package/dist/quality/maintainability.js +0 -15
  127. package/dist/quality/nesting.js +0 -57
  128. package/dist/quality/report.js +0 -803
  129. package/dist/quality/smells.js +0 -67
  130. package/dist/quality-tool.js +0 -63
  131. package/dist/read-only-commands.js +0 -597
  132. package/dist/repo-map.js +0 -292
  133. package/dist/rtk.js +0 -193
  134. package/dist/sandbox.js +0 -105
  135. package/dist/schema.js +0 -214
  136. package/dist/session-lifecycle.js +0 -19
  137. package/dist/session-retention.js +0 -111
  138. package/dist/session.js +0 -447
  139. package/dist/sessions.js +0 -100
  140. package/dist/settings.js +0 -148
  141. package/dist/skills.js +0 -35
  142. package/dist/slop-tool.js +0 -96
  143. package/dist/spend-cap.js +0 -43
  144. package/dist/subagent.js +0 -171
  145. package/dist/tar.js +0 -146
  146. package/dist/theme.js +0 -79
  147. package/dist/thinking.js +0 -45
  148. package/dist/todo.js +0 -114
  149. package/dist/tool-loadout.js +0 -97
  150. package/dist/tool-render.js +0 -769
  151. package/dist/tools.js +0 -176
  152. package/dist/ui.js +0 -4778
  153. package/dist/update.js +0 -551
  154. package/dist/usage-segment.js +0 -69
  155. package/dist/web-tools.js +0 -46
  156. package/dist/web.js +0 -265
  157. package/dist/welcome.js +0 -179
  158. package/dist/workflow-engine.js +0 -246
  159. package/dist/workflow-progress.js +0 -446
  160. package/dist/workflow-view.js +0 -195
  161. package/dist/workflow.js +0 -349
@@ -1,103 +0,0 @@
1
- /**
2
- * Standing instructions the user can add without rebuilding the CLI.
3
- *
4
- * The CLI ships its own rules (`brand.ts` SYSTEM_APPEND). A user who wants a
5
- * standing rule of their own — a house convention, a language to answer in, a
6
- * command that must never run — otherwise has to edit the source or repeat
7
- * themselves every session.
8
- *
9
- * So: `APPEND_SYSTEM.md` in the user's config root, appended after the built-in
10
- * rules.
11
- *
12
- * <AGENT_DIR>/APPEND_SYSTEM.md
13
- *
14
- * **User scope only, deliberately.** A project-scoped file was written first and
15
- * then removed, for two reasons that both held:
16
- *
17
- * - It would break prompt caching. The SDK renders `addendum` *before*
18
- * `project_context`, and `project_context` is where the provider's cache
19
- * breakpoint lands. A cwd-dependent file inside `addendum` therefore gives
20
- * every worktree a different cached prefix — exactly the cost the ordering
21
- * its blocks to avoid.
22
- * - It would duplicate `AGENTS.md`, which the SDK already discovers per project
23
- * and renders into `project_context`. Two files for one job means two sets of
24
- * rules that can disagree, with no way to tell which won.
25
- *
26
- * Also deliberately NOT implemented: a file that *replaces* the prompt. The SDK's
27
- * `customPrompt` swaps out the whole `preamble` section, and that section carries
28
- * the tool list, the per-tool rules and the docs block — so a user file could
29
- * silently take the agent's tools away with it. Replacing is a bigger contract
30
- * than it looks. The append route has no such failure mode: it can only add.
31
- */
32
- import { existsSync, readFileSync } from "node:fs";
33
- import { join } from "node:path";
34
- import { AGENT_DIR } from "./paths.js";
35
- /** The user-scoped file. Named here so the check script and the docs agree. */
36
- export const USER_PROMPT_FILE = join(AGENT_DIR, "APPEND_SYSTEM.md");
37
- /**
38
- * Stated to the model only when no one can answer it.
39
- *
40
- * A bundled skill resolves its mode by asking the user and stopping until they
41
- * reply. In a terminal that is the right behaviour. Under `--print` there is no
42
- * one to reply, so the question becomes a dead end: the model asked, the run
43
- * ended, and the task it was given was never started. The skill cannot know the
44
- * difference and the prompt never said, so the run looks successful — exit 0,
45
- * no error — while producing nothing.
46
- *
47
- * This block closes that gap. It names the tools that stop and wait, so the
48
- * model takes the sensible default instead of asking, and it says why, so it
49
- * does not read the instruction to ask as one it should follow anyway.
50
- */
51
- export const NON_INTERACTIVE_BLOCK = [
52
- "<non-interactive>",
53
- "This run has no user attached: it was started with --print, and there is nobody to answer a question. Anything that stops and waits for a reply — `ask`, a confirmation, a skill that resolves a mode by asking which mode to use — will simply end the run with the task unfinished.",
54
- "So do not ask. Where an instruction tells you to ask the user and wait, take the default it describes and continue, and say which default you took and why in your final answer. The user reads that answer; they cannot answer mid-run.",
55
- "Where a skill offers a choice of modes, prefer the one that applies the rules while you work over the one that audits afterwards: this is the mode for building something now, and the audit mode can still be run later by the same person who reads your answer.",
56
- "</non-interactive>",
57
- ].join("\n");
58
- /**
59
- * A user file is prose, not a payload. One pasted log or a stray directory dump
60
- * would otherwise ride along in every single request; past this size the file is
61
- * skipped rather than truncated, because half a rule is a different rule.
62
- */
63
- const MAX_BYTES = 16 * 1024;
64
- /**
65
- * Read the file, or `undefined`. Never throws: an unreadable or oversized file
66
- * must not stop a session from opening, and a permissions error is the user's to
67
- * fix rather than a reason to refuse to work.
68
- */
69
- export function readPromptFile(path) {
70
- try {
71
- if (!existsSync(path))
72
- return undefined;
73
- const text = readFileSync(path, "utf8").trim();
74
- if (!text)
75
- return undefined;
76
- if (Buffer.byteLength(text, "utf8") > MAX_BYTES)
77
- return undefined;
78
- return text;
79
- }
80
- catch {
81
- return undefined;
82
- }
83
- }
84
- /**
85
- * The append blocks for a session: the built-in rules, then the user's file.
86
- *
87
- * The file gets a heading naming its origin, because a rule whose source is
88
- * invisible is a rule nobody can find again.
89
- *
90
- * `interactive` decides whether the first block also states that nobody can
91
- * answer a question. The bundled skills include one that resolves a mode by
92
- * asking the user and waiting, which is right in a terminal and fatal in
93
- * `--print`: the model asks, nothing answers, and the task it was given never
94
- * runs. The prompt said nothing either way, so the model could not tell the two
95
- * apart. Pass the real value; a wrong one is worse than none.
96
- */
97
- export function promptAppendBlocks(builtIn, interactive) {
98
- const blocks = [interactive ? builtIn : `${builtIn}\n\n${NON_INTERACTIVE_BLOCK}`];
99
- const user = readPromptFile(USER_PROMPT_FILE);
100
- if (user)
101
- blocks.push(`# Standing instructions from ${USER_PROMPT_FILE}\n${user}`);
102
- return blocks;
103
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Read and write the text of a message whose content may be a string or a parts
3
- * array.
4
- *
5
- * The prompt has to be edited in place on the leading system message: a provider
6
- * reads the prompt and the initial tool declarations from that one message, so
7
- * appending a second system message would not reach it. Both prompt extensions
8
- * need the same two operations, and doing it in one place keeps the
9
- * `unknown`-typed SDK content out of their bodies.
10
- */
11
- /** Flatten a message's content to plain text. */
12
- export function readMessageText(content) {
13
- if (typeof content === "string")
14
- return content;
15
- if (Array.isArray(content)) {
16
- return content
17
- .map((part) => {
18
- const candidate = part;
19
- return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
20
- })
21
- .join("\n");
22
- }
23
- return content == null ? "" : String(content);
24
- }
25
- /** Write text back into a message's content, preserving its original shape. */
26
- export function writeMessageText(content, text) {
27
- if (Array.isArray(content)) {
28
- const parts = content.map((part) => ({ ...part }));
29
- const texts = parts.filter((part) => part.type === "text");
30
- if (texts.length > 0) {
31
- // Collapse into the first text part; any others would otherwise duplicate
32
- // or contradict the text we just wrote.
33
- texts[0].text = text;
34
- for (const extra of texts.slice(1))
35
- extra.text = "";
36
- return parts;
37
- }
38
- return [...parts, { type: "text", text }];
39
- }
40
- return text;
41
- }
package/dist/proxy.js DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * Routing the CLI's own requests through a proxy.
3
- *
4
- * HowToFix talks to one upstream, `api.howtofix.id`, and that is the traffic
5
- * this affects: the provider calls, the sign-in exchange, the release check. A
6
- * user behind a corporate gateway, or on a network where that host is reachable
7
- * only through one, otherwise cannot use the CLI at all.
8
- *
9
- * Two sources:
10
- *
11
- * 1. `httpsProxy` in `<agent dir>/settings.json` — what `/proxy` writes, and
12
- * the only one that applies to this CLI alone. Set, it wins outright.
13
- * 2. `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`, with `NO_PROXY` honoured —
14
- * the conventional variables, read exactly the way Node reads them.
15
- *
16
- * The request is routed with an undici dispatcher rather than by replacing
17
- * `fetch`, because the SDK's provider adapters call `globalThis.fetch` and
18
- * swapping that out would be a monkey-patch of a dependency's dependency. The
19
- * dispatcher is what Node uses underneath, so the behaviour is the platform's,
20
- * not an imitation of it.
21
- */
22
- import { readFileSync } from "node:fs";
23
- import { join } from "node:path";
24
- import { EnvHttpProxyAgent, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
25
- /** Where the setting lives, and what it is called. */
26
- export const PROXY_SETTING_KEY = "httpsProxy";
27
- /** The proxy named in settings.json, if there is a usable one there. */
28
- export function configuredProxy(agentDir) {
29
- let parsed;
30
- try {
31
- parsed = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8"));
32
- }
33
- catch {
34
- // A missing or unparseable settings file is "no proxy configured": routing
35
- // must never be the thing that stops the CLI from starting.
36
- return undefined;
37
- }
38
- const value = parsed[PROXY_SETTING_KEY];
39
- if (typeof value !== "string")
40
- return undefined;
41
- const trimmed = value.trim();
42
- // `/proxy off` clears the key; an empty string means the same thing.
43
- return trimmed === "" ? undefined : trimmed;
44
- }
45
- /** Whether the conventional environment variables name a proxy. */
46
- export function environmentProxy() {
47
- const value = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? process.env.ALL_PROXY;
48
- const trimmed = value?.trim();
49
- return trimmed ? trimmed : undefined;
50
- }
51
- /** A URL this CLI can actually dial, or the reason it cannot. */
52
- export function validateProxyUrl(raw) {
53
- const value = raw.trim();
54
- if (value === "")
55
- return { ok: false, problem: "empty" };
56
- let parsed;
57
- try {
58
- parsed = new URL(value);
59
- }
60
- catch {
61
- return { ok: false, problem: `"${value}" is not a URL` };
62
- }
63
- const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
64
- // Only what the runtime can dial without another dependency: a typo is refused
65
- // where it is typed, not at the next request.
66
- if (scheme !== "http" && scheme !== "https") {
67
- return { ok: false, problem: `unsupported scheme "${scheme}"; use http:// or https://` };
68
- }
69
- if (!parsed.hostname)
70
- return { ok: false, problem: "no host in the proxy URL" };
71
- return { ok: true, url: value };
72
- }
73
- /**
74
- * The dispatcher this process started with, captured before the first override.
75
- *
76
- * Kept so "no proxy" can restore the exact object the runtime began with rather
77
- * than a new one that merely behaves the same. Captured lazily, on the first
78
- * call, which is always before any override — so it is the pristine default.
79
- */
80
- let defaultDispatcher;
81
- /**
82
- * Route this process's outbound requests through the configured proxy.
83
- *
84
- * Called once, before any session exists, and again whenever the setting
85
- * changes. Returns what it did, so the caller can say it in one line — a proxy
86
- * that is silently in force is a support question waiting to happen.
87
- *
88
- * Every path sets a dispatcher, including the "none" one, and that is the point:
89
- * a function that only ever installs a proxy cannot undo one. Turning the proxy
90
- * off used to write the setting and leave the dispatcher in place, so the rest
91
- * of that session kept dialling a host the user had just removed — the turn hung
92
- * with nothing on screen to say why, and only a restart fixed it.
93
- *
94
- * A settings value is dialled directly, because the user named one host and
95
- * meant it. Environment variables go through `EnvHttpProxyAgent`, which applies
96
- * `NO_PROXY` and the lowercase spellings the way the platform does; matching
97
- * those rules by hand is how the exceptions get subtly wrong.
98
- */
99
- export function installProxy(agentDir) {
100
- const restore = () => {
101
- if (defaultDispatcher === undefined)
102
- defaultDispatcher = getGlobalDispatcher();
103
- setGlobalDispatcher(defaultDispatcher);
104
- };
105
- const fromSettings = configuredProxy(agentDir);
106
- if (fromSettings) {
107
- const valid = validateProxyUrl(fromSettings);
108
- // A malformed value is reported through `source`, not thrown: the direct
109
- // route is still worth trying, and a stack trace at startup is worse than
110
- // one line naming the bad URL.
111
- if (!valid.ok)
112
- return { source: "settings" };
113
- try {
114
- setGlobalDispatcher(new ProxyAgent(valid.url));
115
- }
116
- catch {
117
- restore();
118
- return { source: "settings" };
119
- }
120
- return { url: valid.url, source: "settings" };
121
- }
122
- const fromEnv = environmentProxy();
123
- if (fromEnv) {
124
- try {
125
- setGlobalDispatcher(new EnvHttpProxyAgent());
126
- }
127
- catch {
128
- restore();
129
- return { source: "env" };
130
- }
131
- return { url: fromEnv, source: "env" };
132
- }
133
- // No proxy anywhere: put the original dispatcher back, so a session that had
134
- // one and then did not is genuinely direct rather than apparently direct.
135
- restore();
136
- return { source: "none" };
137
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * TypeScript AST wrapper: parse a set of files into walkable source files.
3
- *
4
- * Why `typescript/unstable/*`: TypeScript 7.0 ships NO stable programmatic API —
5
- * the package's `.` export resolves to a version string, `createSourceFile` /
6
- * `createProgram` / `forEachChild` are gone, and `unstable/ast` has no parser at
7
- * all (scanner + node types only). Parsing lives in the native binary and is
8
- * reachable only through the unstable session API, which spawns it over a pipe;
9
- * a stable API is expected back in 7.1. `package.json` pins `typescript` to an
10
- * exact version (no caret) precisely so a bump through `unstable/` is a reviewed
11
- * upgrade, not a silent metric change. `scripts/check-quality.mjs` asserts both
12
- * the pin and this comment.
13
- *
14
- * Two properties matter more than speed here (the spec's §4 regression):
15
- *
16
- * 1. Every requested file lands in the returned `Map<path, ParsedFile>` — a
17
- * file that cannot be read or parsed becomes an explicit `{ ok: false }`
18
- * entry, never a missing key. Analysis results are paired to files by path,
19
- * never by array position, so one broken file cannot shift the rest.
20
- * 2. Nested functions are separate units: the metric walkers stop at a
21
- * function boundary and report the inner function on its own.
22
- */
23
- import { API } from "typescript/unstable/async";
24
- import { SyntaxKind, visitEachChild } from "typescript/unstable/ast";
25
- /** Kinds that make a function its own analysis unit. */
26
- export const FUNCTION_KINDS = new Set([SyntaxKind.FunctionDeclaration, SyntaxKind.FunctionExpression, SyntaxKind.ArrowFunction, SyntaxKind.MethodDeclaration, SyntaxKind.Constructor, SyntaxKind.GetAccessor, SyntaxKind.SetAccessor]);
27
- export function isFunctionLike(kind) {
28
- return FUNCTION_KINDS.has(kind);
29
- }
30
- /** `&&`, `||`, `??` — one decision point each in cyclomatic, +1 flat in cognitive. */
31
- export const LOGICAL_OPERATORS = new Set([SyntaxKind.AmpersandAmpersandToken, SyntaxKind.BarBarToken, SyntaxKind.QuestionQuestionToken]);
32
- /**
33
- * A `BinaryExpression` that tests with `&&` / `||` / `??`. TypeScript's AST has
34
- * no `LogicalExpression` (that is an estree name); the operator is a child
35
- * token, so it is looked up by name through {@link childNode}.
36
- */
37
- export function isLogicalBinary(node) {
38
- if (node.kind !== SyntaxKind.BinaryExpression)
39
- return false;
40
- const operator = childNode(node, "operatorToken");
41
- return operator !== undefined && LOGICAL_OPERATORS.has(operator.kind);
42
- }
43
- /**
44
- * Read a named child off a node.
45
- *
46
- * The generated node interfaces are per-kind, so generic code cannot touch
47
- * `thenStatement` without a cast per kind; every metric child access goes
48
- * through here instead, which also keeps `no any` intact.
49
- */
50
- export function childNode(node, key) {
51
- const value = node[key];
52
- if (value && typeof value === "object" && typeof value.kind === "number") {
53
- return value;
54
- }
55
- return undefined;
56
- }
57
- /** The node list stored under a key (`parameters`, `statements`, …), or `[]`. */
58
- export function childNodes(node, key) {
59
- const value = node[key];
60
- return Array.isArray(value) ? value : [];
61
- }
62
- /** Whether the node's modifier list contains the given modifier keyword kind. */
63
- export function hasModifier(node, kind) {
64
- return childNodes(node, "modifiers").some((modifier) => modifier.kind === kind);
65
- }
66
- /** The `.text` of an identifier-like node, or `undefined` when there is none. */
67
- export function nodeText(node) {
68
- if (!node)
69
- return undefined;
70
- const text = node["text"];
71
- return typeof text === "string" ? text : undefined;
72
- }
73
- /** 1-based line of a position in the file. Three call sites share the +1. */
74
- export function lineOf(sourceFile, position) {
75
- return sourceFile.getLineAndCharacterOfPosition(position).line + 1;
76
- }
77
- /**
78
- * Span of a node for reporting: `getStart` skips leading trivia, so a JSDoc
79
- * block does not inflate the printed `L88-142` or the SARIF region, and every
80
- * consumer (the function units and `report.ts`'s findings) computes the same
81
- * four numbers in one place.
82
- */
83
- export function nodeSpan(sourceFile, node) {
84
- const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
85
- const end = sourceFile.getLineAndCharacterOfPosition(node.end);
86
- return {
87
- startLine: start.line + 1,
88
- startColumn: start.character + 1,
89
- endLine: end.line + 1,
90
- endColumn: end.character + 1,
91
- };
92
- }
93
- /**
94
- * Depth-first walk over a node and all of its descendants, nested functions
95
- * included — this is how a file's function units are collected, because every
96
- * nested function is its own unit.
97
- */
98
- export function walk(root, visit) {
99
- visit(root);
100
- visitEachChild(root, (child) => {
101
- walk(child, visit);
102
- return child;
103
- });
104
- }
105
- /**
106
- * Walk a function's body for ITS metrics: the body node itself is visited,
107
- * but a nested function child is not entered — it is a separate unit with its
108
- * own report entry, and counting it twice would make every metric a lie.
109
- */
110
- export function walkOwn(root, visit) {
111
- visit(root);
112
- visitEachChild(root, (child) => {
113
- if (isFunctionLike(child.kind))
114
- return child;
115
- walkOwn(child, visit);
116
- return child;
117
- });
118
- }
119
- const BREAK_AT_FUNCTION = FUNCTION_KINDS;
120
- /**
121
- * Name for a function-like node. Declarations and methods carry one directly;
122
- * an arrow or function expression takes the name of the variable, property, or
123
- * parameter it is being assigned to, climbing only up to the enclosing function
124
- * so a `return () =>` does not borrow its parent's name.
125
- */
126
- export function functionName(node) {
127
- const own = nodeText(childNode(node, "name"));
128
- if (own)
129
- return own;
130
- let parent = childNode(node, "parent");
131
- while (parent && !BREAK_AT_FUNCTION.has(parent.kind)) {
132
- if (parent.kind === SyntaxKind.VariableDeclaration || parent.kind === SyntaxKind.PropertyAssignment) {
133
- const name = nodeText(childNode(parent, "name"));
134
- if (name)
135
- return name;
136
- }
137
- parent = childNode(parent, "parent");
138
- }
139
- return "<anonymous>";
140
- }
141
- function collectFunctions(sourceFile) {
142
- const units = [];
143
- walk(sourceFile, (node) => {
144
- if (!isFunctionLike(node.kind))
145
- return;
146
- const span = nodeSpan(sourceFile, node);
147
- units.push({ name: functionName(node), node, startLine: span.startLine, endLine: span.endLine });
148
- });
149
- return units;
150
- }
151
- /**
152
- * Parse every file into the Map, keyed by the path it was asked for.
153
- *
154
- * A file that fails to read, resolves to no project, or has syntactic
155
- * diagnostics becomes an error entry — explicit, attributed, and never
156
- * dropped. One session serves the whole batch; an empty batch never spawns
157
- * the native compiler at all (an empty directory is a valid analysis).
158
- */
159
- export async function parseFiles(paths) {
160
- const results = new Map();
161
- if (paths.length === 0)
162
- return results;
163
- const api = new API({ cwd: process.cwd() });
164
- try {
165
- const snapshot = await api.updateSnapshot({ openFiles: [...paths] });
166
- for (const path of paths) {
167
- try {
168
- const project = await snapshot.getDefaultProjectForFile(path);
169
- if (!project) {
170
- results.set(path, { ok: false, error: "no TypeScript project contains this file" });
171
- continue;
172
- }
173
- const sourceFile = await project.program.getSourceFile(path);
174
- if (!sourceFile) {
175
- results.set(path, { ok: false, error: "the compiler could not read this file" });
176
- continue;
177
- }
178
- const diagnostics = await project.program.getSyntacticDiagnostics(path);
179
- if (diagnostics.length > 0) {
180
- const first = diagnostics[0];
181
- const where = lineOf(sourceFile, first.pos);
182
- results.set(path, { ok: false, error: `line ${where}: ${first.text} (TS${first.code})` });
183
- continue;
184
- }
185
- results.set(path, { ok: true, sourceFile, functions: collectFunctions(sourceFile) });
186
- }
187
- catch (err) {
188
- results.set(path, { ok: false, error: err instanceof Error ? err.message : String(err) });
189
- }
190
- }
191
- }
192
- finally {
193
- await api.close();
194
- }
195
- return results;
196
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * Cognitive complexity (Sonar method, simplified) — spec §3.2.
3
- *
4
- * What separates this from cyclomatic, and why both exist:
5
- *
6
- * 1. **The nesting penalty.** A structural construct costs `1 + depth`, where
7
- * depth is how many structural constructs enclose it. Three sibling `if`s
8
- * cost 3; three nested `if`s cost 1 + 2 + 3 = 6. `scripts/check-quality.mjs`
9
- * asserts both numbers — without the asymmetry this metric is just a second
10
- * cyclomatic.
11
- * 2. **Flat increments.** `else` / `else if` add 1 with no nesting penalty, and
12
- * `&&` / `||` / `??` add 1 flat. A ternary adds nothing here (cyclomatic
13
- * counts it instead).
14
- *
15
- * A chained `else if` is one flat `+1` for the chain link — it does not get a
16
- * second structural count on top (the parent already paid the marker), and it
17
- * does not deepen the block it guards: code under `if / else if / else` sits at
18
- * the same level regardless of how many links the chain has.
19
- *
20
- * Nested functions are not entered: they are separate units, reported on
21
- * their own.
22
- */
23
- import { SyntaxKind, visitEachChild } from "typescript/unstable/ast";
24
- import { childNode, isFunctionLike, isLogicalBinary } from "./ast.js";
25
- /** Nodes that add `1 + depth` and deepen their children. `try` is deliberately absent (spec §3.2). */
26
- const STRUCTURAL_KINDS = new Set([
27
- SyntaxKind.IfStatement,
28
- SyntaxKind.SwitchStatement,
29
- SyntaxKind.ForStatement,
30
- SyntaxKind.ForInStatement,
31
- SyntaxKind.ForOfStatement,
32
- SyntaxKind.WhileStatement,
33
- SyntaxKind.DoStatement,
34
- SyntaxKind.CatchClause,
35
- ]);
36
- export function cognitive(body) {
37
- let score = 0;
38
- const enterNode = (node, depth) => {
39
- if (isFunctionLike(node.kind))
40
- return;
41
- if (node.kind === SyntaxKind.IfStatement) {
42
- enterIf(node, depth, false);
43
- return;
44
- }
45
- if (STRUCTURAL_KINDS.has(node.kind)) {
46
- score += 1 + depth;
47
- visitEachChild(node, (child) => {
48
- enterNode(child, depth + 1);
49
- return child;
50
- });
51
- return;
52
- }
53
- // `&&` / `||` / `??` add 1 flat; everything else descends unchanged.
54
- if (isLogicalBinary(node))
55
- score += 1;
56
- visitEachChild(node, (child) => {
57
- enterNode(child, depth);
58
- return child;
59
- });
60
- };
61
- const enterIf = (node, depth, chained) => {
62
- // A chained `else if` was already counted as its parent's flat marker.
63
- if (!chained)
64
- score += 1 + depth;
65
- const condition = childNode(node, "expression");
66
- if (condition)
67
- enterNode(condition, depth);
68
- const then = childNode(node, "thenStatement");
69
- if (then)
70
- enterNode(then, depth + 1);
71
- const alternate = childNode(node, "alternate");
72
- if (!alternate)
73
- return;
74
- // `else` and `else if` both add exactly 1, flat (spec §3.2).
75
- score += 1;
76
- if (alternate.kind === SyntaxKind.IfStatement)
77
- enterIf(alternate, depth, true);
78
- else
79
- enterNode(alternate, depth + 1);
80
- };
81
- enterNode(body, 0);
82
- return score;
83
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Cyclomatic complexity (McCabe) — spec §3.1.
3
- *
4
- * `1 + number of decision points`. A decision point is an `if`, a ternary, a
5
- * `&&` / `||` / `??` test, a `case` (never `default`, which tests nothing), a
6
- * loop of any shape, or a `catch`. The walk starts at the function body and
7
- * stops at nested function boundaries: those are their own units and would
8
- * otherwise be counted twice.
9
- */
10
- import { SyntaxKind } from "typescript/unstable/ast";
11
- import { isLogicalBinary, walkOwn } from "./ast.js";
12
- const DECISION_KINDS = new Set([
13
- SyntaxKind.IfStatement,
14
- SyntaxKind.ConditionalExpression,
15
- SyntaxKind.WhileStatement,
16
- SyntaxKind.DoStatement,
17
- SyntaxKind.ForStatement,
18
- SyntaxKind.ForInStatement,
19
- SyntaxKind.ForOfStatement,
20
- SyntaxKind.CatchClause,
21
- SyntaxKind.CaseClause, // DefaultClause is a different kind and is not counted
22
- ]);
23
- export function cyclomatic(body) {
24
- let complexity = 1;
25
- walkOwn(body, (node) => {
26
- if (DECISION_KINDS.has(node.kind) || isLogicalBinary(node))
27
- complexity += 1;
28
- });
29
- return complexity;
30
- }
@@ -1,83 +0,0 @@
1
- /**
2
- * Halstead volume — spec §3.5.
3
- *
4
- * `V = N × log2(n)` where `n` is the number of distinct operators and
5
- * operands and `N` their total occurrences. Volume feeds the Maintainability
6
- * Index and stands alone as a signal: high volume with low branching is a long
7
- * but straight function — a helper-extraction candidate.
8
- *
9
- * The walk starts at the function body and stops at nested function
10
- * boundaries, so an inner function's vocabulary is not charged to its parent.
11
- * An empty body has no vocabulary at all: volume is defined as 0 there, never
12
- * `log2(0)` — the degenerate-input case `check-quality` pins.
13
- */
14
- import { SyntaxKind } from "typescript/unstable/ast";
15
- import { childNode, nodeText, walkOwn } from "./ast.js";
16
- /**
17
- * Kinds Halstead counts as operators. Mapped from the reference implementation
18
- * (estree names) onto TypeScript's AST: estree's `MemberExpression` is
19
- * `PropertyAccessExpression` + `ElementAccessExpression` here, and its
20
- * `LogicalExpression` / `AssignmentExpression` are both `BinaryExpression`,
21
- * distinguished by the operator token in the key.
22
- */
23
- const OPERATOR_KINDS = new Set([
24
- SyntaxKind.BinaryExpression,
25
- SyntaxKind.PrefixUnaryExpression,
26
- SyntaxKind.PostfixUnaryExpression,
27
- SyntaxKind.ConditionalExpression,
28
- SyntaxKind.CallExpression,
29
- SyntaxKind.NewExpression,
30
- SyntaxKind.PropertyAccessExpression,
31
- SyntaxKind.ElementAccessExpression,
32
- SyntaxKind.AwaitExpression,
33
- SyntaxKind.YieldExpression,
34
- SyntaxKind.SpreadElement,
35
- SyntaxKind.IfStatement,
36
- SyntaxKind.WhileStatement,
37
- SyntaxKind.DoStatement,
38
- SyntaxKind.ForStatement,
39
- SyntaxKind.ForInStatement,
40
- SyntaxKind.ForOfStatement,
41
- SyntaxKind.SwitchStatement,
42
- SyntaxKind.ReturnStatement,
43
- SyntaxKind.ThrowStatement,
44
- SyntaxKind.TryStatement,
45
- SyntaxKind.BreakStatement,
46
- SyntaxKind.ContinueStatement,
47
- ]);
48
- /**
49
- * Literal kinds counted as operands. `true` / `false` / `null` are keyword
50
- * tokens in this AST and carry no stored text, so they are deliberately not in
51
- * the set rather than listed and silently skipped.
52
- */
53
- const LITERAL_KINDS = new Set([SyntaxKind.StringLiteral, SyntaxKind.NumericLiteral, SyntaxKind.BigIntLiteral, SyntaxKind.RegularExpressionLiteral, SyntaxKind.NoSubstitutionTemplateLiteral]);
54
- function count(into, key) {
55
- into.set(key, (into.get(key) ?? 0) + 1);
56
- }
57
- export function halsteadVolume(body) {
58
- const operators = new Map();
59
- const operands = new Map();
60
- walkOwn(body, (node) => {
61
- if (OPERATOR_KINDS.has(node.kind)) {
62
- const operator = childNode(node, "operatorToken");
63
- count(operators, SyntaxKind[node.kind] + (operator ? `:${SyntaxKind[operator.kind]}` : ""));
64
- return;
65
- }
66
- const text = nodeText(node);
67
- if (text === undefined)
68
- return;
69
- if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier)
70
- count(operands, `id:${text}`);
71
- else if (LITERAL_KINDS.has(node.kind))
72
- count(operands, `lit:${text}`);
73
- });
74
- const vocabulary = operators.size + operands.size;
75
- if (vocabulary === 0)
76
- return 0;
77
- let total = 0;
78
- for (const seen of operators.values())
79
- total += seen;
80
- for (const seen of operands.values())
81
- total += seen;
82
- return Math.round(total * Math.log2(vocabulary) * 100) / 100;
83
- }