failproofai 0.0.1 → 0.0.2-beta.2

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 (111) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  18. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  21. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  24. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/standalone/.next/server/app/index.html +1 -1
  26. package/.next/standalone/.next/server/app/index.rsc +15 -15
  27. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  29. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  30. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  31. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  32. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  33. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  36. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/chunks/[root-of-the-server]__02nt~6d._.js +1 -1
  50. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0jvf9jj._.js → [root-of-the-server]__0103jwf._.js} +2 -2
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09z7o2x._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0osi8nq._.js → [root-of-the-server]__0okos0k._.js} +3 -3
  58. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0~7bzp~._.js → [root-of-the-server]__0ovwjau._.js} +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +4 -3
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  65. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  66. package/.next/standalone/.next/server/pages/404.html +2 -2
  67. package/.next/standalone/.next/server/pages/500.html +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  70. package/.next/standalone/.next/static/chunks/{0eh2hq9~6bf53.js → 001k0zayn2o.s.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{056c865hodfe7.js → 0jrzwsyo7wo26.js} +1 -1
  72. package/.next/standalone/.next/static/chunks/{0wmsi.tszy~9y.js → 0pdd7~yp8ytu6.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{15lp0u9f5fwae.js → 0sm1iqi3m~xiz.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0o04-obbhh9.s.js → 0tbr0o7vwc~-s.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{0nwr.y4dwla00.js → 0tl2f-3yc.rqc.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{066e8ajzl234v.js → 0uftmw5od9kdz.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/{17q_c2.bbcoh1.js → 0wtcha31~i7rm.js} +1 -1
  78. package/.next/standalone/README.md +18 -10
  79. package/.next/standalone/bin/failproofai.mjs +221 -128
  80. package/.next/standalone/dist/cli.mjs +3005 -0
  81. package/.next/standalone/docs/architecture.md +5 -1
  82. package/.next/standalone/docs/built-in-policies.md +5 -1
  83. package/.next/standalone/docs/cli-reference.md +5 -1
  84. package/.next/standalone/docs/configuration.md +5 -1
  85. package/.next/standalone/docs/custom-hooks.md +5 -1
  86. package/.next/standalone/docs/dashboard.md +5 -1
  87. package/.next/standalone/docs/docs.json +83 -0
  88. package/.next/standalone/docs/favicon.ico +0 -0
  89. package/.next/standalone/docs/getting-started.md +8 -2
  90. package/.next/standalone/docs/introduction.md +47 -0
  91. package/.next/standalone/docs/logo/exosphere-dark.png +0 -0
  92. package/.next/standalone/docs/logo/exosphere-light.png +0 -0
  93. package/.next/standalone/docs/package-aliases.md +7 -1
  94. package/.next/standalone/docs/testing.md +5 -1
  95. package/.next/standalone/package.json +6 -5
  96. package/.next/standalone/scripts/launch.ts +1 -1
  97. package/.next/standalone/scripts/postinstall.mjs +11 -0
  98. package/.next/standalone/src/cli-error.ts +18 -0
  99. package/.next/standalone/src/hooks/manager.ts +17 -3
  100. package/README.md +18 -10
  101. package/bin/failproofai.mjs +221 -128
  102. package/dist/cli.mjs +3005 -0
  103. package/package.json +6 -5
  104. package/scripts/launch.ts +1 -1
  105. package/scripts/postinstall.mjs +11 -0
  106. package/src/cli-error.ts +18 -0
  107. package/src/hooks/manager.ts +17 -3
  108. package/.next/standalone/docs/index.md +0 -48
  109. /package/.next/standalone/.next/static/{odPaRK23IkTvoPxqrn_8P → JksWDLwDoPy6bcczVWlff}/_buildManifest.js +0 -0
  110. /package/.next/standalone/.next/static/{odPaRK23IkTvoPxqrn_8P → JksWDLwDoPy6bcczVWlff}/_clientMiddlewareManifest.js +0 -0
  111. /package/.next/standalone/.next/static/{odPaRK23IkTvoPxqrn_8P → JksWDLwDoPy6bcczVWlff}/_ssgManifest.js +0 -0
@@ -9,6 +9,12 @@
9
9
 
10
10
  # Failproof AI
11
11
 
12
+ [![Docs](https://img.shields.io/badge/docs-befailproof.ai-002CA7?style=flat-square)](https://befailproof.ai)
13
+ [![npm](https://img.shields.io/npm/v/failproofai?style=flat-square&color=CB3837)](https://www.npmjs.com/package/failproofai)
14
+ [![License](https://img.shields.io/badge/license-MIT%20%2B%20Commons%20Clause-blue?style=flat-square)](LICENSE)
15
+ [![CI](https://img.shields.io/github/actions/workflow/status/exospherehost/failproofai/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/exospherehost/failproofai/actions)
16
+ [![Discord](https://img.shields.io/discord/1234567890?style=flat-square&label=Discord&color=5865F2)](https://discord.com/invite/zT92CAgvkj)
17
+
12
18
  Open-source hooks, policies, and project visualization for **Claude Code** & the **Agents SDK**.
13
19
 
14
20
  - **Hooks & Policies** — 35+ built-in security policies that run as Claude Code hooks. Block dangerous commands, sanitize secrets, restrict file access, and more.
@@ -23,7 +29,7 @@ Everything runs locally — no data leaves your machine.
23
29
  ## Requirements
24
30
 
25
31
  - Node.js >= 20.9.0
26
- - Bun >= 1.3.0
32
+ - Bun >= 1.3.0 (optional — only needed for development / building from source)
27
33
 
28
34
  ---
29
35
 
@@ -31,6 +37,8 @@ Everything runs locally — no data leaves your machine.
31
37
 
32
38
  ```bash
33
39
  npm install -g failproofai
40
+ # or
41
+ bun add -g failproofai
34
42
  ```
35
43
 
36
44
  ---
@@ -40,7 +48,7 @@ npm install -g failproofai
40
48
  ### 1. Enable policies globally
41
49
 
42
50
  ```bash
43
- failproofai --install-policies
51
+ failproofai policies --install
44
52
  ```
45
53
 
46
54
  Writes hook entries into `~/.claude/settings.json`. Claude Code will now invoke failproofai before and after each tool call.
@@ -56,7 +64,7 @@ Opens `http://localhost:8020` — browse sessions, inspect logs, manage policies
56
64
  ### 3. Check what's active
57
65
 
58
66
  ```bash
59
- failproofai --list-policies
67
+ failproofai policies
60
68
  ```
61
69
 
62
70
  ---
@@ -67,22 +75,22 @@ failproofai --list-policies
67
75
 
68
76
  | Scope | Command | Where it writes |
69
77
  |-------|---------|-----------------|
70
- | Global (default) | `failproofai --install-policies` | `~/.claude/settings.json` |
71
- | Project | `failproofai --install-policies --scope project` | `.claude/settings.json` |
72
- | Local | `failproofai --install-policies --scope local` | `.claude/settings.local.json` |
78
+ | Global (default) | `failproofai policies --install` | `~/.claude/settings.json` |
79
+ | Project | `failproofai policies --install --scope project` | `.claude/settings.json` |
80
+ | Local | `failproofai policies --install --scope local` | `.claude/settings.local.json` |
73
81
 
74
82
  ### Install specific policies
75
83
 
76
84
  ```bash
77
- failproofai --install-policies block-sudo block-rm-rf sanitize-api-keys
85
+ failproofai policies --install block-sudo block-rm-rf sanitize-api-keys
78
86
  ```
79
87
 
80
88
  ### Remove policies
81
89
 
82
90
  ```bash
83
- failproofai --remove-policies
91
+ failproofai policies --uninstall
84
92
  # or for a specific scope:
85
- failproofai --remove-policies --scope project
93
+ failproofai policies --uninstall --scope project
86
94
  ```
87
95
 
88
96
  ---
@@ -182,7 +190,7 @@ customPolicies.add({
182
190
  Install with:
183
191
 
184
192
  ```bash
185
- failproofai --install-policies --custom ./my-policies.js
193
+ failproofai policies --install --custom ./my-policies.js
186
194
  ```
187
195
 
188
196
  ### Decision helpers
@@ -37,10 +37,40 @@ const args = process.argv.slice(2);
37
37
  // Normalize 'p' → 'policies' (shorthand alias)
38
38
  if (args[0] === "p") args[0] = "policies";
39
39
 
40
- // --help / -h (only when not inside a subcommand that handles its own --help)
41
- const SUBCOMMANDS = ["policies"];
42
- if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
43
- console.log(`
40
+ // --hook <event> called by Claude Code hooks; fast path, outside runCli()
41
+ // because it has its own exit code contract with Claude Code.
42
+ const hookIdx = args.indexOf("--hook");
43
+ if (hookIdx >= 0) {
44
+ if (!args[hookIdx + 1]) {
45
+ console.error("Error: Missing event type after --hook");
46
+ console.error("Usage: failproofai --hook <event> (e.g. PreToolUse, PostToolUse)");
47
+ process.exit(1);
48
+ }
49
+ try {
50
+ const { handleHookEvent } = await import("../src/hooks/handler");
51
+ const exitCode = await handleHookEvent(args[hookIdx + 1]);
52
+ process.exit(exitCode);
53
+ } catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ console.error(`Unexpected error: ${msg}`);
56
+ process.exit(2);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Centralised error handler for all CLI subcommands.
62
+ * CliError → clean message, no stack trace, exit exitCode (1 or 2)
63
+ * Error → unexpected; shows message only, exits 2
64
+ */
65
+ async function runCli() {
66
+ // --help / -h (only when not inside a subcommand that handles its own --help)
67
+ const SUBCOMMANDS = ["policies"];
68
+ if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
69
+ const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
70
+ if (extraArgs.length > 0) {
71
+ throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai --help\` for usage.`);
72
+ }
73
+ console.log(`
44
74
  failproofai v${version}
45
75
 
46
76
  USAGE
@@ -80,33 +110,29 @@ LINKS
80
110
  ⭐ Star us: https://github.com/exospherehost/failproofai
81
111
  📖 Docs: https://befailproof.ai
82
112
  `.trimStart());
83
- process.exit(0);
84
- }
85
-
86
- // --version / -v
87
- if (args.includes("--version") || args.includes("-v")) {
88
- console.log(version);
89
- process.exit(0);
90
- }
113
+ process.exit(0);
114
+ }
91
115
 
92
- // --hook <event> — called by Claude Code hooks; fast path, no dashboard startup
93
- const hookIdx = args.indexOf("--hook");
94
- if (hookIdx >= 0 && args[hookIdx + 1]) {
95
- const { handleHookEvent } = await import("../src/hooks/handler");
96
- const exitCode = await handleHookEvent(args[hookIdx + 1]);
97
- process.exit(exitCode);
98
- }
116
+ // --version / -v
117
+ if ((args.includes("--version") || args.includes("-v")) && !SUBCOMMANDS.includes(args[0])) {
118
+ const extraArgs = args.filter((a) => a !== "--version" && a !== "-v");
119
+ if (extraArgs.length > 0) {
120
+ throw new CliError(`Unexpected argument: ${extraArgs[0]}\nRun \`failproofai --help\` for usage.`);
121
+ }
122
+ console.log(version);
123
+ process.exit(0);
124
+ }
99
125
 
100
- // policies [--install|-i|--uninstall|-u|--help|-h] [names...] [--scope] [--beta] [--custom|-c <path>]
101
- if (args[0] === "policies") {
102
- const subArgs = args.slice(1);
126
+ // policies [--install|-i|--uninstall|-u|--help|-h] [names...] [--scope] [--beta] [--custom|-c <path>]
127
+ if (args[0] === "policies") {
128
+ const subArgs = args.slice(1);
103
129
 
104
- const isInstall = subArgs.includes("--install") || subArgs.includes("-i");
105
- const isUninstall = subArgs.includes("--uninstall") || subArgs.includes("-u");
106
- const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
130
+ const isInstall = subArgs.includes("--install") || subArgs.includes("-i");
131
+ const isUninstall = subArgs.includes("--uninstall") || subArgs.includes("-u");
132
+ const isHelp = subArgs.includes("--help") || subArgs.includes("-h");
107
133
 
108
- if (isHelp) {
109
- console.log(`
134
+ if (isHelp) {
135
+ console.log(`
110
136
  failproofai policies — manage Failproof AI policies
111
137
 
112
138
  USAGE
@@ -137,118 +163,185 @@ EXAMPLES
137
163
  failproofai policies -u
138
164
  failproofai policies --uninstall --custom
139
165
  `.trimStart());
166
+ process.exit(0);
167
+ }
168
+
169
+ if (isInstall) {
170
+ const { installHooks } = await import("../src/hooks/manager");
171
+
172
+ const scopeIdx = subArgs.indexOf("--scope");
173
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
174
+ if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
175
+ throw new CliError("Missing value for --scope. Valid values: user, project, local");
176
+ }
177
+ if (scopeIdx >= 0 && !["user", "project", "local"].includes(scope)) {
178
+ throw new CliError(`Invalid scope: ${scope}. Valid values: user, project, local`);
179
+ }
180
+
181
+ const customIdx = subArgs.includes("--custom") ? subArgs.indexOf("--custom")
182
+ : subArgs.includes("-c") ? subArgs.indexOf("-c")
183
+ : -1;
184
+ const customPoliciesPath = customIdx >= 0 ? subArgs[customIdx + 1] : undefined;
185
+ if (customIdx >= 0 && (!customPoliciesPath || customPoliciesPath.startsWith("-"))) {
186
+ throw new CliError("Missing path after --custom/-c\nUsage: --custom <path> (e.g. --custom ./my-policies.js)");
187
+ }
188
+
189
+ const includeBeta = subArgs.includes("--beta");
190
+
191
+ // Collect positional policy names — args that don't start with - and aren't
192
+ // values consumed by --scope or --custom/-c (tracked by index, not value,
193
+ // so a policy named "user" isn't incorrectly dropped by the default scope).
194
+ const consumedIdxs = new Set();
195
+ if (scopeIdx >= 0) consumedIdxs.add(scopeIdx + 1);
196
+ if (customIdx >= 0) consumedIdxs.add(customIdx + 1);
197
+ const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
198
+ const unknownInstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
199
+ if (unknownInstallFlag) {
200
+ throw new CliError(`Unknown flag: ${unknownInstallFlag}\nRun \`failproofai policies --help\` for usage.`);
201
+ }
202
+
203
+ const explicitPolicyNames = subArgs.filter(
204
+ (a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx)
205
+ );
206
+
207
+ // When --custom/-c is present but no explicit policy names, pass [] so
208
+ // installHooks uses the existing enabled policies and skips the interactive
209
+ // prompt — validation of the custom file happens inside installHooks.
210
+ const policyNames =
211
+ explicitPolicyNames.length > 0 ? explicitPolicyNames
212
+ : customPoliciesPath !== undefined ? []
213
+ : undefined;
214
+
215
+ await installHooks(
216
+ policyNames,
217
+ scope,
218
+ undefined,
219
+ includeBeta,
220
+ undefined,
221
+ customPoliciesPath,
222
+ );
223
+ process.exit(0);
224
+ }
225
+
226
+ if (isUninstall) {
227
+ const { removeHooks } = await import("../src/hooks/manager");
228
+
229
+ const scopeIdx = subArgs.indexOf("--scope");
230
+ const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
231
+ if (scopeIdx >= 0 && (!scope || scope.startsWith("-"))) {
232
+ throw new CliError("Missing value for --scope. Valid values: user, project, local, all");
233
+ }
234
+ if (scopeIdx >= 0 && !["user", "project", "local", "all"].includes(scope)) {
235
+ throw new CliError(`Invalid scope: ${scope}. Valid values: user, project, local, all`);
236
+ }
237
+
238
+ const betaOnly = subArgs.includes("--beta");
239
+ const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
240
+
241
+ const consumedIdxs = new Set();
242
+ if (scopeIdx >= 0) consumedIdxs.add(scopeIdx + 1);
243
+ const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
244
+ const unknownUninstallFlag = subArgs.find((a) => a.startsWith("-") && !flags.has(a));
245
+ if (unknownUninstallFlag) {
246
+ throw new CliError(`Unknown flag: ${unknownUninstallFlag}\nRun \`failproofai policies --help\` for usage.`);
247
+ }
248
+
249
+ const policyNames = subArgs.filter(
250
+ (a, idx) => !a.startsWith("-") && !consumedIdxs.has(idx)
251
+ );
252
+
253
+ await removeHooks(
254
+ policyNames.length > 0 ? policyNames : undefined,
255
+ scope,
256
+ undefined,
257
+ { betaOnly, removeCustomHooks },
258
+ );
259
+ process.exit(0);
260
+ }
261
+
262
+ // Default: list policies
263
+ // Accept --list as a no-op alias (common intuition), reject all other unknown flags
264
+ // and unexpected positional args (e.g. "hi").
265
+ const knownListFlags = new Set(["--install", "-i", "--uninstall", "-u", "--help", "-h", "--list"]);
266
+ const unknownListArg = subArgs.find((a) => a.startsWith("-") && !knownListFlags.has(a));
267
+ if (unknownListArg) {
268
+ throw new CliError(
269
+ `Unknown flag: ${unknownListArg}\n` +
270
+ `Run \`failproofai policies --help\` for usage.`
271
+ );
272
+ }
273
+ const positionalArgs = subArgs.filter((a) => !a.startsWith("-"));
274
+ if (positionalArgs.length > 0) {
275
+ throw new CliError(
276
+ `Unexpected argument: ${positionalArgs[0]}\n` +
277
+ `Run \`failproofai policies --help\` for usage.`
278
+ );
279
+ }
280
+
281
+ const { listHooks } = await import("../src/hooks/manager");
282
+ await listHooks();
140
283
  process.exit(0);
141
284
  }
142
285
 
143
- if (isInstall) {
144
- const { installHooks } = await import("../src/hooks/manager");
145
-
146
- const scopeIdx = subArgs.indexOf("--scope");
147
- const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
148
-
149
- const customIdx = subArgs.includes("--custom") ? subArgs.indexOf("--custom")
150
- : subArgs.includes("-c") ? subArgs.indexOf("-c")
151
- : -1;
152
- const customPoliciesPath = customIdx >= 0 ? subArgs[customIdx + 1] : undefined;
153
-
154
- const includeBeta = subArgs.includes("--beta");
155
-
156
- // Collect positional policy names — args that don't start with - and aren't
157
- // values consumed by --scope or --custom/-c.
158
- const consumed = new Set([scope, customPoliciesPath].filter(Boolean));
159
- const flags = new Set(["--install", "-i", "--scope", "--beta", "--custom", "-c"]);
160
- const explicitPolicyNames = subArgs.filter(
161
- (a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a)
162
- );
163
-
164
- // When --custom/-c is present but no explicit policy names, pass [] so
165
- // installHooks uses the existing enabled policies and skips the interactive
166
- // prompt — validation of the custom file happens inside installHooks.
167
- const policyNames =
168
- explicitPolicyNames.length > 0 ? explicitPolicyNames
169
- : customPoliciesPath !== undefined ? []
170
- : undefined;
171
-
172
- await installHooks(
173
- policyNames,
174
- scope,
175
- undefined,
176
- includeBeta,
177
- undefined,
178
- customPoliciesPath,
286
+ // Unknown flag guard — must appear after all known-flag branches
287
+ const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
288
+ const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
289
+
290
+ if (unknownFlag) {
291
+ function levenshtein(a, b) {
292
+ const m = a.length, n = b.length;
293
+ const dp = Array.from({ length: m + 1 }, (_, i) =>
294
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
295
+ );
296
+ for (let i = 1; i <= m; i++)
297
+ for (let j = 1; j <= n; j++)
298
+ dp[i][j] = a[i - 1] === b[j - 1]
299
+ ? dp[i - 1][j - 1]
300
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
301
+ return dp[m][n];
302
+ }
303
+
304
+ const primary = ["--version", "--help", "--hook", "policies"];
305
+ const closest = primary.reduce((best, flag) => {
306
+ const dist = levenshtein(unknownFlag, flag);
307
+ return dist < best.dist ? { flag, dist } : best;
308
+ }, { flag: primary[0], dist: Infinity });
309
+
310
+ throw new CliError(
311
+ `Unknown flag: ${unknownFlag}\n` +
312
+ `Did you mean: ${closest.flag}?\n` +
313
+ `Run \`failproofai --help\` for usage details.`
179
314
  );
180
- process.exit(0);
181
315
  }
182
316
 
183
- if (isUninstall) {
184
- const { removeHooks } = await import("../src/hooks/manager");
185
-
186
- const scopeIdx = subArgs.indexOf("--scope");
187
- const scope = scopeIdx >= 0 ? subArgs[scopeIdx + 1] : "user";
188
-
189
- const betaOnly = subArgs.includes("--beta");
190
- const removeCustomHooks = subArgs.includes("--custom") || subArgs.includes("-c");
191
-
192
- const consumed = new Set([scope].filter(Boolean));
193
- const flags = new Set(["--uninstall", "-u", "--scope", "--beta", "--custom", "-c"]);
194
- const policyNames = subArgs.filter(
195
- (a) => !a.startsWith("-") && !flags.has(a) && !consumed.has(a)
317
+ // Unknown subcommand guard (non-flag args that aren't "policies")
318
+ const unknownSubcommand = args.find(a => !a.startsWith("-") && a !== "policies");
319
+ if (unknownSubcommand) {
320
+ throw new CliError(
321
+ `Unknown command: ${unknownSubcommand}\n` +
322
+ `Did you mean: failproofai policies?\n` +
323
+ `Run \`failproofai --help\` for usage details.`
196
324
  );
197
-
198
- await removeHooks(
199
- policyNames.length > 0 ? policyNames : undefined,
200
- scope,
201
- undefined,
202
- { betaOnly, removeCustomHooks },
203
- );
204
- process.exit(0);
205
325
  }
206
326
 
207
- // Default: list policies
208
- const { listHooks } = await import("../src/hooks/manager");
209
- await listHooks();
210
- process.exit(0);
327
+ // Dashboard launch — always production mode
328
+ const { launch } = await import("../scripts/launch");
329
+ launch("start");
211
330
  }
212
331
 
213
- // Unknown flag guard must appear after all known-flag branches
214
- const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
215
- const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
332
+ // ── Import CliError for use in the guard above ────────────────────────────────
333
+ const { CliError } = await import("../src/cli-error");
216
334
 
217
- if (unknownFlag) {
218
- function levenshtein(a, b) {
219
- const m = a.length, n = b.length;
220
- const dp = Array.from({ length: m + 1 }, (_, i) =>
221
- Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
222
- );
223
- for (let i = 1; i <= m; i++)
224
- for (let j = 1; j <= n; j++)
225
- dp[i][j] = a[i - 1] === b[j - 1]
226
- ? dp[i - 1][j - 1]
227
- : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
228
- return dp[m][n];
335
+ // ── Run ───────────────────────────────────────────────────────────────────────
336
+ try {
337
+ await runCli();
338
+ } catch (err) {
339
+ if (err instanceof CliError) {
340
+ console.error(`Error: ${err.message}`);
341
+ process.exit(err.exitCode);
229
342
  }
230
-
231
- const primary = ["--version", "--help", "--hook", "policies"];
232
- const closest = primary.reduce((best, flag) => {
233
- const dist = levenshtein(unknownFlag, flag);
234
- return dist < best.dist ? { flag, dist } : best;
235
- }, { flag: primary[0], dist: Infinity });
236
-
237
- console.error(`Unknown flag: ${unknownFlag}`);
238
- console.error(`Did you mean: ${closest.flag}?`);
239
- console.error(`Run \`failproofai --help\` for usage details.`);
240
- process.exit(1);
241
- }
242
-
243
- // Unknown subcommand guard (non-flag args that aren't "policies")
244
- const unknownSubcommand = args.find(a => !a.startsWith("-") && a !== "policies");
245
- if (unknownSubcommand) {
246
- console.error(`Unknown command: ${unknownSubcommand}`);
247
- console.error(`Did you mean: failproofai policies?`);
248
- console.error(`Run \`failproofai --help\` for usage details.`);
249
- process.exit(1);
343
+ // Unexpected internal error — show message only, no stack trace
344
+ const msg = err instanceof Error ? err.message : String(err);
345
+ console.error(`Unexpected error: ${msg}`);
346
+ process.exit(2);
250
347
  }
251
-
252
- // Dashboard launch — always production mode
253
- const { launch } = await import("../scripts/launch");
254
- launch("start");