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.
- package/dist/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-7DTQKDO4.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-AUDMRGWF.js +189 -0
- package/dist/chunk-CPJKKHGH.js +1 -0
- package/dist/chunk-DAPXGSGD.js +12 -0
- package/dist/chunk-DOD7MUGX.js +1 -0
- package/dist/chunk-DV2BBBBJ.js +2 -0
- package/dist/chunk-EIBOS5KO.js +5 -0
- package/dist/chunk-EYWOVCSS.js +6 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-HT3MFLAV.js +34 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-JMKK26AT.js +2 -0
- package/dist/chunk-LAHVHRCS.js +2 -0
- package/dist/chunk-LOOFOBTS.js +1 -0
- package/dist/chunk-LSJC73DG.js +1 -0
- package/dist/chunk-MOKUBKE7.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-ONZLNFQ2.js +3 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-SX5VE6XA.js +110 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-UNK5HRQR.js +1 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V2RPZMLK.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-VLMQSD25.js +1 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-Y7YM5R75.js +5 -0
- package/dist/chunk-YW7NORH3.js +2 -0
- package/dist/main.js +99 -540
- package/package.json +17 -64
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -120
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -294
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -295
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/middle-prune.js +0 -76
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -735
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -429
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-facts.js +0 -35
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -447
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4778
- package/dist/update.js +0 -551
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- package/dist/workflow.js +0 -349
package/dist/update.js
DELETED
|
@@ -1,551 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keeping an npm-installed HowToFix current.
|
|
3
|
-
*
|
|
4
|
-
* The shape is taken from Claude Code's updater, because the pitfalls it solves
|
|
5
|
-
* are not obvious:
|
|
6
|
-
*
|
|
7
|
-
* 1. **The check must run from the home directory.** `npm view` reads the
|
|
8
|
-
* nearest `.npmrc`, so running it inside a project lets a checked-out
|
|
9
|
-
* repository point the CLI at an attacker's registry — and then the update
|
|
10
|
-
* installs whatever that registry serves. Every npm invocation here runs
|
|
11
|
-
* with `cwd` set to the user's home.
|
|
12
|
-
* 2. **The check must never block the session.** It is a network call on the
|
|
13
|
-
* startup path, so it is bounded by a short timeout and its failure is
|
|
14
|
-
* silent: an offline machine must not notice that anything was attempted.
|
|
15
|
-
* 3. **An install must be exclusive.** Two sessions starting at once would
|
|
16
|
-
* otherwise run two `npm install -g` against the same tree; a lock file with
|
|
17
|
-
* a stale timeout keeps one of them from corrupting the other's install.
|
|
18
|
-
* 4. **Only an npm install can be updated this way.** A copy run from a source
|
|
19
|
-
* checkout, or bundled some other way, must be told so rather than have
|
|
20
|
-
* `npm install -g` write a second copy somewhere else.
|
|
21
|
-
*
|
|
22
|
-
* The version comparison is local (no semver dependency) and deliberately
|
|
23
|
-
* forgiving: a version string this cannot parse is treated as "not newer", so a
|
|
24
|
-
* weird tag can never trigger an install.
|
|
25
|
-
*/
|
|
26
|
-
import { execFile } from "node:child_process";
|
|
27
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
28
|
-
import { homedir } from "node:os";
|
|
29
|
-
import { delimiter, dirname, join, sep } from "node:path";
|
|
30
|
-
/** The package an installed copy updates from. */
|
|
31
|
-
export const PACKAGE_NAME = "howtofixcli";
|
|
32
|
-
/** The npm launchers to look for on PATH, in the order npm itself installs them. */
|
|
33
|
-
const NPM_BIN_NAMES = ["npm", "npm.cmd", "npm.exe"];
|
|
34
|
-
/**
|
|
35
|
-
* How long the registry may take before the check is abandoned.
|
|
36
|
-
*
|
|
37
|
-
* This runs on the startup path through `npm view`, and the first `npm` of a
|
|
38
|
-
* session is not a cheap call: node loads `npm-cli.js` and its whole module
|
|
39
|
-
* graph before it touches the network. On a cold Windows box behind a virus
|
|
40
|
-
* scanner that alone can take several seconds, and the registry lookup that
|
|
41
|
-
* follows is a second network round trip on top. The old five seconds was
|
|
42
|
-
* measured on a warm cache; it expired before the answer arrived often enough
|
|
43
|
-
* that the check looked unreliable, which is exactly what it was.
|
|
44
|
-
*/
|
|
45
|
-
export const CHECK_TIMEOUT_MS = 20_000;
|
|
46
|
-
/** How many times a check may be attempted before it is reported as lost. */
|
|
47
|
-
export const CHECK_ATTEMPTS = 2;
|
|
48
|
-
/** An install gets longer than a check: it downloads a tarball. */
|
|
49
|
-
export const INSTALL_TIMEOUT_MS = 120_000;
|
|
50
|
-
/**
|
|
51
|
-
* How long a lock may sit before it is considered abandoned.
|
|
52
|
-
*
|
|
53
|
-
* A killed process leaves its lock behind, and without a staleness rule the
|
|
54
|
-
* next update would never run. Ten minutes is longer than any install here and
|
|
55
|
-
* shorter than a user's patience.
|
|
56
|
-
*/
|
|
57
|
-
export const LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
58
|
-
/**
|
|
59
|
-
* How long a half-written lock is respected.
|
|
60
|
-
*
|
|
61
|
-
* The file is created and then filled with a pid; a reader that catches the gap
|
|
62
|
-
* sees an empty file. Five seconds is far longer than that gap and far shorter
|
|
63
|
-
* than a user's patience.
|
|
64
|
-
*/
|
|
65
|
-
export const BROKEN_LOCK_GRACE_MS = 5 * 1000;
|
|
66
|
-
/**
|
|
67
|
-
* Compare two dotted version strings.
|
|
68
|
-
*
|
|
69
|
-
* Returns -1, 0 or 1. Anything unparseable returns 0 ("equal"), so an odd tag
|
|
70
|
-
* cannot be mistaken for a newer release. Pre-release suffixes are ignored:
|
|
71
|
-
* this updater only ever installs the `latest` tag, which is not a pre-release.
|
|
72
|
-
*/
|
|
73
|
-
export function compareVersions(left, right) {
|
|
74
|
-
const parse = (value) => {
|
|
75
|
-
const core = value.trim().split("-")[0]?.split("+")[0] ?? "";
|
|
76
|
-
if (!/^\d+(\.\d+)*$/.test(core))
|
|
77
|
-
return undefined;
|
|
78
|
-
return core.split(".").map((part) => Number(part));
|
|
79
|
-
};
|
|
80
|
-
const a = parse(left);
|
|
81
|
-
const b = parse(right);
|
|
82
|
-
if (!a || !b)
|
|
83
|
-
return 0;
|
|
84
|
-
const length = Math.max(a.length, b.length);
|
|
85
|
-
for (let index = 0; index < length; index += 1) {
|
|
86
|
-
const x = a[index] ?? 0;
|
|
87
|
-
const y = b[index] ?? 0;
|
|
88
|
-
if (x > y)
|
|
89
|
-
return 1;
|
|
90
|
-
if (x < y)
|
|
91
|
-
return -1;
|
|
92
|
-
}
|
|
93
|
-
return 0;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Whether `candidate` is a strictly newer release than `current`.
|
|
97
|
-
*
|
|
98
|
-
* A pre-release candidate is never treated as an upgrade. The registry's
|
|
99
|
-
* `latest` tag is stable by convention, so a `-beta` string reaching here means
|
|
100
|
-
* the tag was moved by mistake — and a broken beta silently replacing a working
|
|
101
|
-
* install is a far worse outcome than missing one release. Core versions are
|
|
102
|
-
* still compared, so a pre-release of an *older* core stays older.
|
|
103
|
-
*/
|
|
104
|
-
export function isNewer(candidate, current) {
|
|
105
|
-
if (candidate.includes("-"))
|
|
106
|
-
return false;
|
|
107
|
-
return compareVersions(candidate, current) > 0;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Run a command and capture its output, never throwing.
|
|
111
|
-
*
|
|
112
|
-
* `error.code` is reported twice, deliberately: as the numeric exit code when it
|
|
113
|
-
* is a number, and as `errorCode` when it is a string. A spawn that never
|
|
114
|
-
* started — no such file, not executable, refused on Windows — carries a string
|
|
115
|
-
* there and leaves `stderr` empty, which is the only way to tell "the program
|
|
116
|
-
* did not run" apart from "the program ran and failed". Reading `stderr` alone
|
|
117
|
-
* reported both as a registry that did not answer.
|
|
118
|
-
*/
|
|
119
|
-
const run = (command, args, options) => {
|
|
120
|
-
return new Promise((resolve) => {
|
|
121
|
-
execFile(command, args, { cwd: options.cwd, timeout: options.timeoutMs, windowsHide: true }, (error, stdout, stderr) => {
|
|
122
|
-
const raw = error ? error.code : undefined;
|
|
123
|
-
const code = typeof raw === "number" ? raw : error ? 1 : 0;
|
|
124
|
-
const errorCode = typeof raw === "string" ? raw : undefined;
|
|
125
|
-
resolve({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? ""), ...(errorCode ? { errorCode } : {}) });
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
/**
|
|
130
|
-
* How a spawn that never started is told apart from a registry that did not answer.
|
|
131
|
-
*
|
|
132
|
-
* A spawn that fails reports a *string* code in `error.code` — `ENOENT` for a
|
|
133
|
-
* program that is not there, `EACCES` for one that is not executable, `EINVAL`
|
|
134
|
-
* for a `.cmd` refused on Windows — and leaves `stderr` empty. Reading `stderr`
|
|
135
|
-
* therefore classified every one of those as "the registry did not answer",
|
|
136
|
-
* which sends a user to look at their network when the problem is on their disk.
|
|
137
|
-
* The code is the signal; `stderr` is only a fallback for a runner that reports
|
|
138
|
-
* it there.
|
|
139
|
-
*/
|
|
140
|
-
const MISSING_COMMAND = new Set(["ENOENT", "EACCES", "EINVAL", "EPERM"]);
|
|
141
|
-
/**
|
|
142
|
-
* The npm to run, which is not always the one on PATH.
|
|
143
|
-
*
|
|
144
|
-
* `npm install -g howtofixcli` puts the `howtofix` bin next to npm's own, but a
|
|
145
|
-
* GUI launcher, a shim directory, or an nvm-style shell hook can leave `node`
|
|
146
|
-
* resolvable and `npm` not — and then the update check reported a network-style
|
|
147
|
-
* failure for a machine that simply had no npm in front of it.
|
|
148
|
-
*
|
|
149
|
-
* npm's own entry point is preferred over the launcher on PATH, because the
|
|
150
|
-
* launcher on Windows is `npm.cmd`: `execFile` refuses a `.cmd` without a shell
|
|
151
|
-
* (the fix for CVE-2024-27980), so spawning it fails with `EINVAL` before npm
|
|
152
|
-
* ever runs. `npm-cli.js` is a JavaScript file, which `execFile` starts with the
|
|
153
|
-
* running node on every platform. It sits beside node in every layout npm
|
|
154
|
-
* itself installs — POSIX `<node>/../lib/node_modules/npm`, Windows
|
|
155
|
-
* `<node>/node_modules/npm` — so both are probed.
|
|
156
|
-
*
|
|
157
|
-
* The PATH lookup is still the fallback, so a version-managed npm that is not
|
|
158
|
-
* laid out this way is found. Only npm is probed: the updater knows the shape of
|
|
159
|
-
* an npm installation, and guessing at other package managers would be worse
|
|
160
|
-
* than saying plainly that npm is missing.
|
|
161
|
-
*/
|
|
162
|
-
export function resolveNpm() {
|
|
163
|
-
const nodeDir = dirname(process.execPath);
|
|
164
|
-
// POSIX layout, then the Windows layout, which keeps npm under the node dir.
|
|
165
|
-
for (const relative of [
|
|
166
|
-
["..", "lib", "node_modules", "npm", "bin", "npm-cli.js"],
|
|
167
|
-
["node_modules", "npm", "bin", "npm-cli.js"],
|
|
168
|
-
]) {
|
|
169
|
-
const npmCli = join(nodeDir, ...relative);
|
|
170
|
-
if (existsSync(npmCli))
|
|
171
|
-
return { command: process.execPath, args: [npmCli] };
|
|
172
|
-
}
|
|
173
|
-
const onPath = process.env.PATH?.split(delimiter).filter(Boolean) ?? [];
|
|
174
|
-
for (const dir of onPath) {
|
|
175
|
-
for (const name of NPM_BIN_NAMES) {
|
|
176
|
-
const candidate = join(dir, name);
|
|
177
|
-
// A `.cmd`/`.bat` cannot be spawned without a shell, so a shim in one of
|
|
178
|
-
// those forms is skipped rather than returned to fail later.
|
|
179
|
-
if (name.endsWith(".cmd") || name.endsWith(".bat"))
|
|
180
|
-
continue;
|
|
181
|
-
if (existsSync(candidate))
|
|
182
|
-
return { command: candidate, args: [] };
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
// Windows was giving up here. npm's launcher there is `npm.cmd`, which
|
|
186
|
-
// `execFile` refuses to start without a shell, so the loop above skips it — and
|
|
187
|
-
// if the `npm-cli.js` probes missed, the answer was "npm was not found" on a
|
|
188
|
-
// machine whose npm is right there on PATH. The `.cmd` is still not spawnable
|
|
189
|
-
// directly, so it is run *through* cmd.exe, which is how npm itself invokes it.
|
|
190
|
-
if (process.platform === "win32") {
|
|
191
|
-
for (const dir of onPath) {
|
|
192
|
-
for (const name of NPM_BIN_NAMES) {
|
|
193
|
-
if (!name.endsWith(".cmd") && !name.endsWith(".bat"))
|
|
194
|
-
continue;
|
|
195
|
-
const candidate = join(dir, name);
|
|
196
|
-
if (existsSync(candidate)) {
|
|
197
|
-
return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", `"${candidate}"`] };
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
/** A missing npm is a fact about the machine, not a failed registry lookup. */
|
|
205
|
-
export const NPM_MISSING_MESSAGE = "could not check for a newer version: npm was not found, so run `npm install -g howtofixcli@latest` yourself";
|
|
206
|
-
/** A registry that answered nothing is transient, and the wording says so. */
|
|
207
|
-
export const CHECK_FAILED_MESSAGE = "could not check for a newer version: the registry did not answer";
|
|
208
|
-
/**
|
|
209
|
-
* A one-line note that a check did not happen, for the feed.
|
|
210
|
-
*
|
|
211
|
-
* The check itself stays silent when it succeeds and the copy is current — "you
|
|
212
|
-
* are up to date" on every start is noise. A check that *failed* is different:
|
|
213
|
-
* staying silent about it is what made a broken lookup indistinguishable from a
|
|
214
|
-
* working one, and the user's only signal was that the update never appeared.
|
|
215
|
-
* The note is one line, says only what was tried, and does not pretend the
|
|
216
|
-
* absence of an update means the software is current.
|
|
217
|
-
*/
|
|
218
|
-
export function checkFailedNote(check) {
|
|
219
|
-
if (check.latest !== undefined)
|
|
220
|
-
return undefined;
|
|
221
|
-
return `${check.message} (this is why no update was offered; /update retries it)`;
|
|
222
|
-
}
|
|
223
|
-
/** Whether the failure was "there is no npm here" rather than "npm said no". */
|
|
224
|
-
function npmMissing(result) {
|
|
225
|
-
if (result.code === -2)
|
|
226
|
-
return true;
|
|
227
|
-
const code = result.errorCode ?? (result.stderr ?? "").trim();
|
|
228
|
-
return MISSING_COMMAND.has(code);
|
|
229
|
-
}
|
|
230
|
-
/** Where the update lock lives. */
|
|
231
|
-
function lockPath() {
|
|
232
|
-
const configured = process.env.HOWTOFIX_CODING_AGENT_DIR?.trim();
|
|
233
|
-
const dir = configured || join(homedir(), ".howtofix", "agent");
|
|
234
|
-
try {
|
|
235
|
-
mkdirSync(dir, { recursive: true });
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// A read-only home is not fatal: the lock then just never blocks.
|
|
239
|
-
}
|
|
240
|
-
return join(dir, "update.lock");
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Whether a process with this id still exists.
|
|
244
|
-
*
|
|
245
|
-
* `EPERM` is the interesting answer: it means the process is there but belongs
|
|
246
|
-
* to another user. Treating that as "dead" would let a second install start
|
|
247
|
-
* while the first is still writing, which is the race the lock exists to stop.
|
|
248
|
-
*/
|
|
249
|
-
export function pidAlive(pid) {
|
|
250
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
251
|
-
return false;
|
|
252
|
-
try {
|
|
253
|
-
process.kill(pid, 0);
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
return error?.code === "EPERM";
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function readLock(path, now) {
|
|
261
|
-
try {
|
|
262
|
-
const ageMs = now - statSync(path).mtimeMs;
|
|
263
|
-
const raw = readFileSync(path, "utf8").trim();
|
|
264
|
-
const pid = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : undefined;
|
|
265
|
-
return { pid, ageMs };
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Whether a lock still represents work in progress.
|
|
273
|
-
*
|
|
274
|
-
* Age alone is not enough. An update that crashed, or that the user cancelled
|
|
275
|
-
* by closing the terminal, leaves the file behind; waiting the full ten minutes
|
|
276
|
-
* before trying again is exactly the wrong answer, and it produced the message
|
|
277
|
-
* "another HowToFix is already installing an update" when nothing was. The pid
|
|
278
|
-
* written inside the file is the fact that settles it: a dead holder means a
|
|
279
|
-
* free lock, immediately.
|
|
280
|
-
*/
|
|
281
|
-
function lockIsHeld(state) {
|
|
282
|
-
if (!state)
|
|
283
|
-
return false;
|
|
284
|
-
if (state.pid === undefined) {
|
|
285
|
-
// Half-written file: give the writer a moment, then treat it as debris.
|
|
286
|
-
return state.ageMs < BROKEN_LOCK_GRACE_MS;
|
|
287
|
-
}
|
|
288
|
-
// A lock this process wrote is still a lock: the second caller in the same
|
|
289
|
-
// process is the same hazard as a second process, and treating it as free let
|
|
290
|
-
// a check-then-install run two installs from one command.
|
|
291
|
-
if (!pidAlive(state.pid))
|
|
292
|
-
return false;
|
|
293
|
-
// A live holder that has been inside `npm install` for longer than the
|
|
294
|
-
// timeout is stuck, not working. Taking over is better than blocking forever.
|
|
295
|
-
return state.ageMs < LOCK_TIMEOUT_MS;
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Take the update lock, or report that someone else holds it.
|
|
299
|
-
*
|
|
300
|
-
* A lock whose holder is gone is taken over at once; one held by a live process
|
|
301
|
-
* blocks until {@link LOCK_TIMEOUT_MS}. The state is re-read immediately before
|
|
302
|
-
* the file is removed, so two processes that both saw a stale lock cannot both
|
|
303
|
-
* believe they own it.
|
|
304
|
-
*/
|
|
305
|
-
export function acquireUpdateLock(now = Date.now()) {
|
|
306
|
-
const path = lockPath();
|
|
307
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
308
|
-
try {
|
|
309
|
-
// `wx` is the whole lock: the create fails if the file exists, so two
|
|
310
|
-
// processes that start in the same millisecond cannot both believe they
|
|
311
|
-
// own it. Checking `existsSync` first and writing after left exactly that
|
|
312
|
-
// window open, and two `npm install -g` runs then wrote the same tree.
|
|
313
|
-
const fd = openSync(path, "wx", 0o600);
|
|
314
|
-
try {
|
|
315
|
-
writeFileSync(fd, String(process.pid));
|
|
316
|
-
}
|
|
317
|
-
finally {
|
|
318
|
-
closeSync(fd);
|
|
319
|
-
}
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
const code = error.code;
|
|
324
|
-
// An unwritable home means no lock; an update is still better attempted
|
|
325
|
-
// than silently skipped, so this reports success rather than blocking.
|
|
326
|
-
if (code !== "EEXIST")
|
|
327
|
-
return true;
|
|
328
|
-
if (lockIsHeld(readLock(path, now)))
|
|
329
|
-
return false;
|
|
330
|
-
try {
|
|
331
|
-
unlinkSync(path);
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
// Someone took the stale lock over between the read and the unlink.
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return false;
|
|
340
|
-
}
|
|
341
|
-
/** Release the update lock. Never throws, and never removes another's lock. */
|
|
342
|
-
export function releaseUpdateLock() {
|
|
343
|
-
const path = lockPath();
|
|
344
|
-
try {
|
|
345
|
-
const state = readLock(path, Date.now());
|
|
346
|
-
if (state && state.pid !== undefined && state.pid !== process.pid)
|
|
347
|
-
return;
|
|
348
|
-
unlinkSync(path);
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// Already gone, or never written.
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* What the registry says the newest release is, or why it could not be read.
|
|
356
|
-
*
|
|
357
|
-
* Retried once. The failure this catches is a dropped connection or a stalled
|
|
358
|
-
* DNS lookup in the first second of a session — transient by nature, and the
|
|
359
|
-
* only chance to notice it used to be a restart, because nothing else asks
|
|
360
|
-
* again before the process exits.
|
|
361
|
-
*
|
|
362
|
-
* `no-npm` is not retried: npm's absence is a fact about the machine, and
|
|
363
|
-
* running the lookup twice would not change it.
|
|
364
|
-
*/
|
|
365
|
-
async function probeLatest(options) {
|
|
366
|
-
const runner = options?.runner ?? run;
|
|
367
|
-
const npm = resolveNpm();
|
|
368
|
-
// No npm anywhere on this machine: the user can fix that, so it is named.
|
|
369
|
-
if (!npm)
|
|
370
|
-
return { reason: "no-npm" };
|
|
371
|
-
let last = "no-answer";
|
|
372
|
-
for (let attempt = 0; attempt < CHECK_ATTEMPTS; attempt += 1) {
|
|
373
|
-
// The runner is documented never to throw, and `checkForUpdate` is documented
|
|
374
|
-
// never to throw either — but a promise executor that throws (an `execFile`
|
|
375
|
-
// that rejects its own arguments, say) rejects the returned promise, and this
|
|
376
|
-
// runs on the startup path. A spawn that could not start is not an answer from
|
|
377
|
-
// the registry, so it is classified as one, not propagated.
|
|
378
|
-
let result;
|
|
379
|
-
try {
|
|
380
|
-
result = await runner(npm.command, [...npm.args, "view", `${PACKAGE_NAME}@latest`, "version", "--prefer-online"], {
|
|
381
|
-
cwd: homedir(),
|
|
382
|
-
timeoutMs: options?.timeoutMs ?? CHECK_TIMEOUT_MS,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
catch {
|
|
386
|
-
last = "no-answer";
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
if (result.code !== 0) {
|
|
390
|
-
// npm exists but could not run: a launcher that is not executable, or a
|
|
391
|
-
// shim whose target is gone. Both are the same advice as a missing npm,
|
|
392
|
-
// and neither is worth a second attempt.
|
|
393
|
-
if (npmMissing(result))
|
|
394
|
-
return { reason: "no-npm" };
|
|
395
|
-
last = "no-answer";
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
const version = result.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
399
|
-
if (/^\d+\.\d+/.test(version))
|
|
400
|
-
return { version };
|
|
401
|
-
last = "no-answer";
|
|
402
|
-
}
|
|
403
|
-
return { reason: last };
|
|
404
|
-
}
|
|
405
|
-
/** The newest published version, or `undefined` when it could not be read. */
|
|
406
|
-
export async function latestVersion(options) {
|
|
407
|
-
return (await probeLatest(options)).version;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Ask the registry whether a newer release exists.
|
|
411
|
-
*
|
|
412
|
-
* Never throws and never blocks for long: this runs on the startup path, so an
|
|
413
|
-
* offline machine, a slow registry, or a missing `npm` must all be quiet
|
|
414
|
-
* non-events rather than a delay or a crash.
|
|
415
|
-
*/
|
|
416
|
-
export async function checkForUpdate(current, options) {
|
|
417
|
-
const probe = await probeLatest(options);
|
|
418
|
-
if (!probe.version) {
|
|
419
|
-
// The two failures read the same to a user and could not be more different
|
|
420
|
-
// to fix: one is "install npm", the other is "try again". Saying which is
|
|
421
|
-
// the whole value of the message.
|
|
422
|
-
return {
|
|
423
|
-
current,
|
|
424
|
-
available: false,
|
|
425
|
-
message: probe.reason === "no-npm" ? NPM_MISSING_MESSAGE : CHECK_FAILED_MESSAGE,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
const latest = probe.version;
|
|
429
|
-
if (!isNewer(latest, current)) {
|
|
430
|
-
return { current, latest, available: false, message: `HowToFix ${current} is up to date` };
|
|
431
|
-
}
|
|
432
|
-
return {
|
|
433
|
-
current,
|
|
434
|
-
latest,
|
|
435
|
-
available: true,
|
|
436
|
-
message: `HowToFix ${latest} is available (you have ${current})`,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* The line that explains an npm failure.
|
|
441
|
-
*
|
|
442
|
-
* npm prints deprecation warnings on stderr and a real failure after them, so
|
|
443
|
-
* taking the last line reported `npm warn deprecated node-domexception@1.0.0`
|
|
444
|
-
* as the reason an install failed. Warnings are dropped, error lines win, and
|
|
445
|
-
* the code that is left is mapped to the action a user can actually take.
|
|
446
|
-
*/
|
|
447
|
-
export function npmFailureDetail(stdout, stderr, errorCode) {
|
|
448
|
-
const lines = `${stderr}\n${stdout}`
|
|
449
|
-
.split("\n")
|
|
450
|
-
.map((line) => line.trim())
|
|
451
|
-
.filter(Boolean)
|
|
452
|
-
.filter((line) => !/^npm warn\b/i.test(line));
|
|
453
|
-
const errors = lines.filter((line) => /^npm error\b/i.test(line) || /\bERR!/i.test(line));
|
|
454
|
-
const codeLine = errors.find((line) => /^npm error code\b/i.test(line));
|
|
455
|
-
const detail = (codeLine ?? errors.at(-1) ?? lines.at(-1) ?? errorCode ?? "")
|
|
456
|
-
.replace(/^npm error\s*/i, "")
|
|
457
|
-
.trim();
|
|
458
|
-
// The hint reads the whole output: the code that names the cause is often on
|
|
459
|
-
// an earlier line than the path or syscall that failed last.
|
|
460
|
-
const evidence = `${lines.join(" ")} ${errorCode ?? ""}`;
|
|
461
|
-
let hint = "";
|
|
462
|
-
if (/EACCES|EPERM|permission denied/i.test(evidence)) {
|
|
463
|
-
hint = " — npm could not write its files (permission denied); close other HowToFix windows and run the terminal as Administrator";
|
|
464
|
-
}
|
|
465
|
-
else if (/EBUSY|EEXIST|ENOTEMPTY|resource busy|being used by another process/i.test(evidence)) {
|
|
466
|
-
hint = " — a file is in use; close other HowToFix windows and try again";
|
|
467
|
-
}
|
|
468
|
-
else if (/ENOSPC/i.test(evidence)) {
|
|
469
|
-
hint = " — the disk is full";
|
|
470
|
-
}
|
|
471
|
-
else if (/ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ECONNREFUSED/i.test(evidence)) {
|
|
472
|
-
hint = " — npm could not reach the registry";
|
|
473
|
-
}
|
|
474
|
-
return { detail, hint };
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Install the newest release globally.
|
|
478
|
-
*
|
|
479
|
-
* The lock is the point: two sessions starting together would otherwise run two
|
|
480
|
-
* installs over the same tree. A held lock is not a failure — it means another
|
|
481
|
-
* process is already doing exactly this — so it is reported as such and the
|
|
482
|
-
* caller can simply wait for the next start.
|
|
483
|
-
*/
|
|
484
|
-
export async function installUpdate(options) {
|
|
485
|
-
const runner = options?.runner ?? run;
|
|
486
|
-
if (!acquireUpdateLock()) {
|
|
487
|
-
return { ok: false, message: "another HowToFix is already installing an update" };
|
|
488
|
-
}
|
|
489
|
-
try {
|
|
490
|
-
const npm = resolveNpm();
|
|
491
|
-
if (!npm) {
|
|
492
|
-
return { ok: false, message: "npm was not found, so run `npm install -g howtofixcli@latest` yourself" };
|
|
493
|
-
}
|
|
494
|
-
const spec = options?.version ? `${PACKAGE_NAME}@${options.version}` : `${PACKAGE_NAME}@latest`;
|
|
495
|
-
const result = await runner(npm.command, [...npm.args, "install", "-g", spec], {
|
|
496
|
-
cwd: homedir(),
|
|
497
|
-
timeoutMs: options?.timeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
498
|
-
});
|
|
499
|
-
if (result.code !== 0) {
|
|
500
|
-
const { detail, hint } = npmFailureDetail(result.stdout, result.stderr, result.errorCode);
|
|
501
|
-
return { ok: false, message: `the update did not install${hint}${detail ? `: ${detail}` : ""}` };
|
|
502
|
-
}
|
|
503
|
-
return { ok: true, version: options?.version, message: "updated; it takes effect the next time HowToFix starts" };
|
|
504
|
-
}
|
|
505
|
-
catch (cause) {
|
|
506
|
-
return { ok: false, message: `the update failed: ${cause instanceof Error ? cause.message : String(cause)}` };
|
|
507
|
-
}
|
|
508
|
-
finally {
|
|
509
|
-
releaseUpdateLock();
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Whether the running copy is one `npm install -g` can update.
|
|
514
|
-
*
|
|
515
|
-
* The test is on the launcher's path, resolved through symlinks, and *not* on
|
|
516
|
-
* `import.meta.url`. That is not a style preference: esbuild substitutes
|
|
517
|
-
* `import.meta.url` with the **output file's** URL at build time, so in the
|
|
518
|
-
* published bundle it is forever the path of the machine that built it — a
|
|
519
|
-
* release would never recognise itself as installed and could never update.
|
|
520
|
-
* `process.argv[1]` is the file the user actually ran; `realpath` follows the
|
|
521
|
-
* `bin` symlink npm creates into `lib/node_modules/howtofixcli/`.
|
|
522
|
-
*
|
|
523
|
-
* An npm install — global or as a dependency — lives under
|
|
524
|
-
* `node_modules/howtofixcli/`, so that is what the resolved path is tested for.
|
|
525
|
-
* A copy run from a source checkout has no business being replaced by a global
|
|
526
|
-
* install, and the caller is told so instead of having a second copy written.
|
|
527
|
-
*/
|
|
528
|
-
export function isInstalledCopy(launcher = process.argv[1]) {
|
|
529
|
-
if (!launcher)
|
|
530
|
-
return false;
|
|
531
|
-
let path = launcher;
|
|
532
|
-
try {
|
|
533
|
-
// npm's global `bin` entry is a symlink to the package's own file.
|
|
534
|
-
path = realpathSync(launcher);
|
|
535
|
-
}
|
|
536
|
-
catch {
|
|
537
|
-
// A launcher that cannot be resolved is tested as the path it was given.
|
|
538
|
-
}
|
|
539
|
-
return path.includes(`${sep}node_modules${sep}${PACKAGE_NAME}${sep}`);
|
|
540
|
-
}
|
|
541
|
-
/** Whether the user turned the automatic update off. */
|
|
542
|
-
export function autoUpdateEnabled(settingsPath) {
|
|
543
|
-
try {
|
|
544
|
-
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
545
|
-
return parsed.autoUpdate !== false;
|
|
546
|
-
}
|
|
547
|
-
catch {
|
|
548
|
-
// Missing or unreadable settings means the default, which is on.
|
|
549
|
-
return true;
|
|
550
|
-
}
|
|
551
|
-
}
|
package/dist/usage-segment.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { USAGE_CACHE_PATH } from "./paths.js";
|
|
4
|
-
import { usageSegment } from "./hud-line.js";
|
|
5
|
-
import { writeFileAtomicSync } from "./fs-atomic.js";
|
|
6
|
-
import { checkUsage, HOWTOFIX_PROVIDERS, isHowtofixProvider } from "./howtofix-account.js";
|
|
7
|
-
const cachePath = USAGE_CACHE_PATH;
|
|
8
|
-
const cacheTtlMs = 180_000;
|
|
9
|
-
/**
|
|
10
|
-
* The connected account's key, or undefined.
|
|
11
|
-
*
|
|
12
|
-
* Mirrors how `/usage` and `/sync` resolve it: the first configured gateway
|
|
13
|
-
* provider that the runtime knows about, then that provider's key.
|
|
14
|
-
*/
|
|
15
|
-
export async function howtofixKeyFrom(source) {
|
|
16
|
-
const configured = new Set((await source.modelRuntime.getAvailable()).map((model) => model.provider).filter((provider) => isHowtofixProvider(provider)));
|
|
17
|
-
const target = HOWTOFIX_PROVIDERS.find((provider) => configured.has(provider));
|
|
18
|
-
if (!target)
|
|
19
|
-
return undefined;
|
|
20
|
-
const auth = await source.modelRuntime.getAuth(target);
|
|
21
|
-
return auth?.auth?.apiKey;
|
|
22
|
-
}
|
|
23
|
-
function readCache() {
|
|
24
|
-
try {
|
|
25
|
-
const cache = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
26
|
-
if (!cache.data || typeof cache.cachedAt !== "number")
|
|
27
|
-
return undefined;
|
|
28
|
-
return { data: cache.data, cachedAt: cache.cachedAt };
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function format(usage) {
|
|
35
|
-
if (usage.unlimited)
|
|
36
|
-
return usageSegment("unlimited", "", usage.latencyMs);
|
|
37
|
-
const money = usage.unit === "USD" || usage.unit === "CNY";
|
|
38
|
-
const balance = money ? `$${usage.remaining.toFixed(2)}` : `${usage.remaining.toLocaleString()}`;
|
|
39
|
-
return usageSegment(balance, usage.unit, usage.latencyMs);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The HUD segment, or undefined when there is nothing to say.
|
|
43
|
-
*
|
|
44
|
-
* A stale cache is better than nothing: when the gateway cannot be reached the
|
|
45
|
-
* last known figure is shown rather than the segment vanishing, and only a start
|
|
46
|
-
* with no cache and no reachable gateway leaves the HUD without one.
|
|
47
|
-
*/
|
|
48
|
-
export async function loadUsageSegment(source) {
|
|
49
|
-
const cache = readCache();
|
|
50
|
-
if (cache && Date.now() - cache.cachedAt < cacheTtlMs)
|
|
51
|
-
return format(cache.data);
|
|
52
|
-
const key = await howtofixKeyFrom(source);
|
|
53
|
-
if (!key)
|
|
54
|
-
return cache ? format(cache.data) : undefined;
|
|
55
|
-
const started = Date.now();
|
|
56
|
-
const check = await checkUsage(key, { timeoutMs: 3_000 });
|
|
57
|
-
if (!check.ok)
|
|
58
|
-
return cache ? format(cache.data) : undefined;
|
|
59
|
-
const usage = {
|
|
60
|
-
remaining: check.remaining ?? 0,
|
|
61
|
-
limit: check.limit ?? 0,
|
|
62
|
-
unit: check.unit,
|
|
63
|
-
unlimited: check.unlimited,
|
|
64
|
-
latencyMs: Date.now() - started,
|
|
65
|
-
};
|
|
66
|
-
mkdirSync(dirname(cachePath), { recursive: true });
|
|
67
|
-
writeFileAtomicSync(cachePath, JSON.stringify({ data: usage, cachedAt: Date.now() }));
|
|
68
|
-
return format(usage);
|
|
69
|
-
}
|
package/dist/web-tools.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Type } from "@netrodev/hxf-ai";
|
|
2
|
-
import { defineTool } from "@netrodev/hxf-coding-agent";
|
|
3
|
-
import { fetchUrl, searchWeb } from "./web.js";
|
|
4
|
-
function formatHits(hits) {
|
|
5
|
-
if (hits.length === 0)
|
|
6
|
-
return "no results";
|
|
7
|
-
return hits.map((hit, i) => `${i + 1}. ${hit.title}\n ${hit.url}${hit.snippet ? `\n ${hit.snippet}` : ""}`).join("\n");
|
|
8
|
-
}
|
|
9
|
-
export const webSearchTool = defineTool({
|
|
10
|
-
name: "web_search",
|
|
11
|
-
label: "web_search",
|
|
12
|
-
description: "Search the public web. Use for current docs, changelogs, error messages, and anything not in this repo. Returns titles, URLs, and snippets. Follow a URL with web_fetch.",
|
|
13
|
-
promptSnippet: "web_search: search the public web",
|
|
14
|
-
parameters: Type.Object({
|
|
15
|
-
query: Type.String({ description: "Search query" }),
|
|
16
|
-
}),
|
|
17
|
-
async execute(_toolCallId, params) {
|
|
18
|
-
const result = await searchWeb(params.query, { braveKey: process.env.BRAVE_API_KEY });
|
|
19
|
-
if (!result.ok) {
|
|
20
|
-
return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
content: [{ type: "text", text: formatHits(result.hits) }],
|
|
24
|
-
details: { count: result.hits.length, provider: result.provider },
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
export const webFetchTool = defineTool({
|
|
29
|
-
name: "web_fetch",
|
|
30
|
-
label: "web_fetch",
|
|
31
|
-
description: "Fetch a public http(s) URL and return its text. HTML is stripped to visible text. Local, loopback, and private addresses are refused.",
|
|
32
|
-
promptSnippet: "web_fetch: read a public URL as text",
|
|
33
|
-
parameters: Type.Object({
|
|
34
|
-
url: Type.String({ description: "http(s) URL" }),
|
|
35
|
-
}),
|
|
36
|
-
async execute(_toolCallId, params) {
|
|
37
|
-
const result = await fetchUrl(params.url);
|
|
38
|
-
if (!result.ok) {
|
|
39
|
-
return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text", text: result.text }],
|
|
43
|
-
details: { url: result.url, contentType: result.contentType, chars: result.text.length },
|
|
44
|
-
};
|
|
45
|
-
},
|
|
46
|
-
});
|