howtofixcli 0.7.4 → 0.7.6
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/brand.js +1 -1
- package/dist/commands.js +23 -3
- package/dist/errors.js +63 -0
- package/dist/fatal.js +4 -4
- package/dist/main.js +24 -16
- package/dist/update.js +38 -3
- package/package.json +1 -1
package/dist/brand.js
CHANGED
package/dist/commands.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { AGENT_DIR } from "./paths.js";
|
|
5
|
+
import { APP_TITLE } from "./brand.js";
|
|
6
|
+
import { reportError } from "./errors.js";
|
|
5
7
|
import { detectSandbox } from "./sandbox.js";
|
|
6
8
|
const MIN_NODE = [22, 19];
|
|
7
9
|
/** Everything the CLI needs from the machine, checked without a model call. */
|
|
@@ -89,12 +91,30 @@ export async function runDoctor() {
|
|
|
89
91
|
/** Configured providers and their models, straight from models.json. */
|
|
90
92
|
export function runModels(root = AGENT_DIR) {
|
|
91
93
|
const path = join(root, "models.json");
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(path, "utf8");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const code = error.code;
|
|
100
|
+
if (code === "ENOENT") {
|
|
101
|
+
console.log(` no models.json at ${path} — run \`howtofix /connect\` to pull the catalogue`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
reportError(APP_TITLE, "models", error);
|
|
105
|
+
}
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
92
108
|
let parsed;
|
|
93
109
|
try {
|
|
94
|
-
parsed = JSON.parse(
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
95
111
|
}
|
|
96
|
-
catch {
|
|
97
|
-
|
|
112
|
+
catch (error) {
|
|
113
|
+
// A file that exists but does not parse is a different problem from a
|
|
114
|
+
// missing one: /connect will not fix it, and saying "no models.json" sends
|
|
115
|
+
// the user looking for a file that is already there.
|
|
116
|
+
console.error(`${APP_TITLE}: models: ${path} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
117
|
+
console.error(" fix the file, or delete it and run `howtofix /connect` to write a new one");
|
|
98
118
|
return 1;
|
|
99
119
|
}
|
|
100
120
|
const providers = parsed.providers ?? {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One way to tell a user that something failed.
|
|
3
|
+
*
|
|
4
|
+
* Every command used to phrase its own failure, which meant a missing file, a
|
|
5
|
+
* refused connection and a typo in a flag could all arrive in the same shape:
|
|
6
|
+
* a sentence that says what broke but not what to do. This module answers both
|
|
7
|
+
* questions from the error itself, so the answer does not depend on which code
|
|
8
|
+
* path happened to throw.
|
|
9
|
+
*/
|
|
10
|
+
/** The action a user can take, inferred from the code Node attached. */
|
|
11
|
+
function hintFor(code, message) {
|
|
12
|
+
const evidence = `${code ?? ""} ${message}`;
|
|
13
|
+
if (/EACCES|EPERM/.test(evidence))
|
|
14
|
+
return "the file or directory is not writable; check permissions and try again";
|
|
15
|
+
if (/ENOENT/.test(evidence))
|
|
16
|
+
return "a file or directory this command needs is missing";
|
|
17
|
+
if (/ENOSPC/.test(evidence))
|
|
18
|
+
return "the disk is full";
|
|
19
|
+
if (/ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|UND_ERR|fetch failed/i.test(evidence)) {
|
|
20
|
+
return "the network request failed; check the connection and the gateway address";
|
|
21
|
+
}
|
|
22
|
+
if (/EADDRINUSE/.test(evidence))
|
|
23
|
+
return "another process is already using that port";
|
|
24
|
+
if (/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(evidence)) {
|
|
25
|
+
return "a required file or package is missing; reinstall HowToFix";
|
|
26
|
+
}
|
|
27
|
+
if (/JSON/i.test(evidence) && /parse|unexpected token|end of input/i.test(evidence)) {
|
|
28
|
+
return "a configuration file is not valid JSON";
|
|
29
|
+
}
|
|
30
|
+
if (/not a git repository/i.test(evidence))
|
|
31
|
+
return "run the command inside a project, or pass --cwd";
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The message, and the action that follows from it. */
|
|
35
|
+
export function describeError(error) {
|
|
36
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
37
|
+
const message = raw.trim() || "unknown error";
|
|
38
|
+
const code = error?.code;
|
|
39
|
+
return { message, hint: hintFor(code, message) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The line(s) a failure prints.
|
|
43
|
+
*
|
|
44
|
+
* `context` names the thing that failed ("doctor", "update"), because "not
|
|
45
|
+
* permitted" alone does not say what was not permitted. The stack is evidence,
|
|
46
|
+
* and evidence only appears when `HOWTOFIX_DEBUG` asks for it.
|
|
47
|
+
*/
|
|
48
|
+
export function formatError(appTitle, context, error, debug = false) {
|
|
49
|
+
const { message, hint } = describeError(error);
|
|
50
|
+
const head = context ? `${appTitle}: ${context}: ${message}` : `${appTitle}: ${message}`;
|
|
51
|
+
const lines = [head];
|
|
52
|
+
if (hint)
|
|
53
|
+
lines.push(` ${hint}`);
|
|
54
|
+
if (debug && error instanceof Error && error.stack)
|
|
55
|
+
lines.push(error.stack);
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
/** Print a failure the same way everywhere. */
|
|
59
|
+
export function reportError(appTitle, context, error, options) {
|
|
60
|
+
const debug = options?.debug ?? process.env.HOWTOFIX_DEBUG === "1";
|
|
61
|
+
const write = options?.write ?? ((text) => console.error(text));
|
|
62
|
+
write(formatError(appTitle, context, error, debug));
|
|
63
|
+
}
|
package/dist/fatal.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* frames the user cannot act on. The message is what matters; the stack is
|
|
7
7
|
* evidence, and evidence belongs behind a flag.
|
|
8
8
|
*/
|
|
9
|
+
import { describeError } from "./errors.js";
|
|
9
10
|
/** The single line a failure deserves, with the stack only when asked for. */
|
|
10
11
|
export function describeFatal(error, debug = false) {
|
|
11
|
-
const message =
|
|
12
|
-
const text = message.trim() || "unknown error";
|
|
12
|
+
const { message } = describeError(error);
|
|
13
13
|
if (!debug)
|
|
14
|
-
return
|
|
14
|
+
return message;
|
|
15
15
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
16
|
-
return stack && stack.includes("\n") ? `${
|
|
16
|
+
return stack && stack.includes("\n") ? `${message}\n${stack}` : message;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Catch what escapes `main()`.
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { helpText, parseArgs } from "./args.js";
|
|
3
3
|
import { APP_TITLE, APP_VERSION } from "./brand.js";
|
|
4
4
|
import { installCrashHandlers } from "./fatal.js";
|
|
5
|
+
import { reportError } from "./errors.js";
|
|
5
6
|
import { AGENT_DIR, pinAgentDir } from "./paths.js";
|
|
6
7
|
import { jsonLine } from "./json-events.js";
|
|
7
8
|
import { spendCapMessage, unpricedBudgetMessage } from "./spend-cap.js";
|
|
@@ -95,6 +96,28 @@ async function main() {
|
|
|
95
96
|
}
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
99
|
+
// Questions about the installation, answered without a session, a model, or
|
|
100
|
+
// any of the repairs below. They run first on purpose: `howtofix models` used
|
|
101
|
+
// to rewrite settings.json and print two notices about it before listing a
|
|
102
|
+
// single model, which reads like the command did something it did not.
|
|
103
|
+
const subArgv = process.argv.slice(2);
|
|
104
|
+
if (subArgv[0] === "doctor" || subArgv[0] === "models" || subArgv[0] === "sessions") {
|
|
105
|
+
const context = subArgv[0];
|
|
106
|
+
try {
|
|
107
|
+
const commands = await import("./commands.js");
|
|
108
|
+
if (context === "doctor")
|
|
109
|
+
process.exitCode = await commands.runDoctor();
|
|
110
|
+
else if (context === "models")
|
|
111
|
+
process.exitCode = commands.runModels();
|
|
112
|
+
else
|
|
113
|
+
process.exitCode = commands.runSessions(subArgv.slice(1));
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
reportError(APP_TITLE, context, error);
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
98
121
|
// Repair a howtofix gateway entry that predates `compat.supportsDeveloperRole`.
|
|
99
122
|
// The gateway refuses the `developer` role that the SDK selects for every
|
|
100
123
|
// reasoning model, so such an entry fails every prompt silently — the spinner
|
|
@@ -157,20 +180,6 @@ async function main() {
|
|
|
157
180
|
// A start-time re-sync that fails costs the previous list, which still
|
|
158
181
|
// works; `/sync` remains the manual way to try again.
|
|
159
182
|
}
|
|
160
|
-
// Questions about the installation, answered without a session or a model.
|
|
161
|
-
// Dispatched before option parsing so their own flags (--limit) are not read
|
|
162
|
-
// as unknown options for the agent.
|
|
163
|
-
const subArgv = process.argv.slice(2);
|
|
164
|
-
if (subArgv[0] === "doctor" || subArgv[0] === "models" || subArgv[0] === "sessions") {
|
|
165
|
-
const commands = await import("./commands.js");
|
|
166
|
-
if (subArgv[0] === "doctor")
|
|
167
|
-
process.exitCode = await commands.runDoctor();
|
|
168
|
-
else if (subArgv[0] === "models")
|
|
169
|
-
process.exitCode = commands.runModels();
|
|
170
|
-
else
|
|
171
|
-
process.exitCode = commands.runSessions(subArgv.slice(1));
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
183
|
let args;
|
|
175
184
|
try {
|
|
176
185
|
args = parseArgs(process.argv.slice(2));
|
|
@@ -523,7 +532,6 @@ async function main() {
|
|
|
523
532
|
}
|
|
524
533
|
installCrashHandlers(APP_TITLE);
|
|
525
534
|
main().catch((error) => {
|
|
526
|
-
|
|
527
|
-
console.error(`${APP_TITLE}: ${message}`);
|
|
535
|
+
reportError(APP_TITLE, undefined, error);
|
|
528
536
|
process.exitCode = 1;
|
|
529
537
|
});
|
package/dist/update.js
CHANGED
|
@@ -418,6 +418,43 @@ export async function checkForUpdate(current, options) {
|
|
|
418
418
|
message: `HowToFix ${latest} is available (you have ${current})`,
|
|
419
419
|
};
|
|
420
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* The line that explains an npm failure.
|
|
423
|
+
*
|
|
424
|
+
* npm prints deprecation warnings on stderr and a real failure after them, so
|
|
425
|
+
* taking the last line reported `npm warn deprecated node-domexception@1.0.0`
|
|
426
|
+
* as the reason an install failed. Warnings are dropped, error lines win, and
|
|
427
|
+
* the code that is left is mapped to the action a user can actually take.
|
|
428
|
+
*/
|
|
429
|
+
export function npmFailureDetail(stdout, stderr, errorCode) {
|
|
430
|
+
const lines = `${stderr}\n${stdout}`
|
|
431
|
+
.split("\n")
|
|
432
|
+
.map((line) => line.trim())
|
|
433
|
+
.filter(Boolean)
|
|
434
|
+
.filter((line) => !/^npm warn\b/i.test(line));
|
|
435
|
+
const errors = lines.filter((line) => /^npm error\b/i.test(line) || /\bERR!/i.test(line));
|
|
436
|
+
const codeLine = errors.find((line) => /^npm error code\b/i.test(line));
|
|
437
|
+
const detail = (codeLine ?? errors.at(-1) ?? lines.at(-1) ?? errorCode ?? "")
|
|
438
|
+
.replace(/^npm error\s*/i, "")
|
|
439
|
+
.trim();
|
|
440
|
+
// The hint reads the whole output: the code that names the cause is often on
|
|
441
|
+
// an earlier line than the path or syscall that failed last.
|
|
442
|
+
const evidence = `${lines.join(" ")} ${errorCode ?? ""}`;
|
|
443
|
+
let hint = "";
|
|
444
|
+
if (/EACCES|EPERM|permission denied/i.test(evidence)) {
|
|
445
|
+
hint = " — npm could not replace its files; close other HowToFix windows and run the terminal as Administrator";
|
|
446
|
+
}
|
|
447
|
+
else if (/EBUSY|EEXIST|ENOTEMPTY|resource busy|being used by another process/i.test(evidence)) {
|
|
448
|
+
hint = " — a file is in use; close other HowToFix windows and try again";
|
|
449
|
+
}
|
|
450
|
+
else if (/ENOSPC/i.test(evidence)) {
|
|
451
|
+
hint = " — the disk is full";
|
|
452
|
+
}
|
|
453
|
+
else if (/ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ECONNREFUSED/i.test(evidence)) {
|
|
454
|
+
hint = " — npm could not reach the registry";
|
|
455
|
+
}
|
|
456
|
+
return { detail, hint };
|
|
457
|
+
}
|
|
421
458
|
/**
|
|
422
459
|
* Install the newest release globally.
|
|
423
460
|
*
|
|
@@ -442,9 +479,7 @@ export async function installUpdate(options) {
|
|
|
442
479
|
timeoutMs: options?.timeoutMs ?? INSTALL_TIMEOUT_MS,
|
|
443
480
|
});
|
|
444
481
|
if (result.code !== 0) {
|
|
445
|
-
const detail = (result.
|
|
446
|
-
// Permissions are the common failure and the one a user can act on.
|
|
447
|
-
const hint = /EACCES|EPERM|permission/i.test(detail) ? " — npm could not write to its global directory" : "";
|
|
482
|
+
const { detail, hint } = npmFailureDetail(result.stdout, result.stderr, result.errorCode);
|
|
448
483
|
return { ok: false, message: `the update did not install${hint}${detail ? `: ${detail}` : ""}` };
|
|
449
484
|
}
|
|
450
485
|
return { ok: true, version: options?.version, message: "updated; it takes effect the next time HowToFix starts" };
|