howtofixcli 0.7.5 → 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.5";
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howtofixcli",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "HowToFix coding agent. Workbench TUI, sessions, background agents.",
5
5
  "type": "module",
6
6
  "bin": {