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 CHANGED
@@ -1,5 +1,5 @@
1
1
  export const APP_TITLE = "HowToFix";
2
- export const APP_VERSION = "0.7.4";
2
+ export const APP_VERSION = "0.7.6";
3
3
  export const SYSTEM_APPEND = [
4
4
  // House style: RFC 2119 keywords in caps, one claim per line, the
5
5
  // highest-stakes rules at
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(readFileSync(path, "utf8"));
110
+ parsed = JSON.parse(raw);
95
111
  }
96
- catch {
97
- console.log(` no models.json at ${path} — run \`howtofix /connect\` to pull the catalogue`);
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 = error instanceof Error ? error.message : String(error);
12
- const text = message.trim() || "unknown error";
12
+ const { message } = describeError(error);
13
13
  if (!debug)
14
- return text;
14
+ return message;
15
15
  const stack = error instanceof Error ? error.stack : undefined;
16
- return stack && stack.includes("\n") ? `${text}\n${stack}` : text;
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
- const message = error instanceof Error ? error.message : String(error);
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.stderr || result.stdout).trim().split("\n").filter(Boolean).pop() ?? result.errorCode ?? "";
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" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howtofixcli",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
5
5
  "type": "module",
6
6
  "bin": {