failproofai 1.0.0-beta.3 → 1.0.0-beta.5

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 (157) 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/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/run/route.js +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  37. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/index.html +1 -1
  40. package/.next/standalone/.next/server/app/index.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  42. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  43. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  44. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +10 -10
  50. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/page.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +1 -1
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  61. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  62. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[externals]__0h9_83k._.js +3 -0
  66. package/.next/standalone/.next/server/chunks/[externals]__1ifpkx-._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-dh5jy._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0f8nu8d._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0h3lfmp._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l4vuqm._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0longh-._.js → [root-of-the-server]__0szqzl8._.js} +2 -2
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10oa-v9._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__18d-9oi._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__16le-kd._.js → [root-of-the-server]__1p7s8-d._.js} +2 -2
  78. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -1
  79. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -1
  80. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/[externals]__1aq139v._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__10s_z4l._.js → [root-of-the-server]__094nop3._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ls0gsr._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0uno39i._.js → [root-of-the-server]__0n_uvv0._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01od0m6._.js → [root-of-the-server]__0rag5dg._.js} +2 -2
  90. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1apb7k_._.js → [root-of-the-server]__0rtdfof._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ky3im9._.js → [root-of-the-server]__1gak4pd._.js} +2 -2
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1j4u3yd._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0jfts82.js → node_modules_html-to-image_es_index_12c_2cz.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -1
  103. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  104. package/.next/standalone/.next/server/pages/404.html +1 -1
  105. package/.next/standalone/.next/server/pages/500.html +1 -1
  106. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  107. package/.next/standalone/.next/server/server-reference-manifest.json +12 -12
  108. package/.next/standalone/.next/static/chunks/{29g8ykjk6sx_8.js → 0cr85wuc9gc9k.js} +1 -1
  109. package/.next/standalone/.next/static/chunks/{1hdhteui932bq.js → 0qmo2t50kpb30.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/{0cvd6bcdwchc_.js → 1bkynffp3q1f7.js} +1 -1
  111. package/.next/standalone/.next/static/chunks/{2uvyh8y0naofj.js → 1bw4dfzflelcv.js} +1 -1
  112. package/.next/standalone/.next/static/chunks/{1h0_yaoq5f3yx.js → 1tp9dex23zs8y.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/23kev26by1e4f.js +1 -0
  114. package/.next/standalone/.next/static/chunks/2cqk4a9s893oy.js +1 -0
  115. package/.next/standalone/.next/static/chunks/{13-obgukzg_ke.js → 2k285z07qce47.js} +1 -1
  116. package/.next/standalone/.next/static/chunks/{0a7k_x4rp_97z.js → 2s9g1dsde_ts1.js} +1 -1
  117. package/.next/standalone/.next/static/chunks/{2rqm-klet_gbl.js → 2v1rpzrh2i1wv.js} +1 -1
  118. package/.next/standalone/.next/static/chunks/{3p-n_j80zlzi3.js → 34sjdw8lv46e3.js} +2 -2
  119. package/.next/standalone/Cargo.lock +3 -3
  120. package/.next/standalone/Cargo.toml +1 -1
  121. package/.next/standalone/crates/failproofaid/src/main.rs +118 -11
  122. package/.next/standalone/crates/fpai-collect/src/config.rs +8 -0
  123. package/.next/standalone/crates/fpai-collect/src/filetail.rs +4 -1
  124. package/.next/standalone/crates/fpai-collect/src/sources/hooks/mod.rs +5 -1
  125. package/.next/standalone/crates/fpai-collect/src/spool.rs +77 -0
  126. package/.next/standalone/crates/fpai-collect/src/sqlitepoll.rs +4 -1
  127. package/.next/standalone/crates/fpai-collect/tests/claude_source.rs +1 -0
  128. package/.next/standalone/crates/fpai-collect/tests/claude_subagents.rs +1 -0
  129. package/.next/standalone/crates/fpai-collect/tests/codex_source.rs +1 -0
  130. package/.next/standalone/crates/fpai-collect/tests/copilot_source.rs +1 -0
  131. package/.next/standalone/crates/fpai-collect/tests/goose_source.rs +1 -0
  132. package/.next/standalone/crates/fpai-collect/tests/hermes_source.rs +1 -0
  133. package/.next/standalone/crates/fpai-collect/tests/hooks_source.rs +1 -0
  134. package/.next/standalone/crates/fpai-collect/tests/openclaw_source.rs +1 -0
  135. package/.next/standalone/crates/fpai-collect/tests/pi_source.rs +1 -0
  136. package/.next/standalone/package.json +14 -8
  137. package/.next/standalone/server.js +1 -1
  138. package/dist/cli.mjs +81 -28
  139. package/dist/worker.mjs +1 -1
  140. package/package.json +14 -8
  141. package/scripts/build-daemon-packages.mjs +210 -0
  142. package/scripts/daemon-platforms.mjs +42 -0
  143. package/scripts/prune-standalone.mjs +6 -0
  144. package/scripts/publish-aliases.mjs +62 -41
  145. package/src/hooks/cloud-connection.ts +3 -0
  146. package/src/hooks/collector-config.ts +10 -0
  147. package/src/hooks/daemon-download.ts +137 -27
  148. package/src/hooks/daemon-service.ts +23 -8
  149. package/.next/standalone/.next/server/chunks/[externals]__14odj07._.js +0 -3
  150. package/.next/standalone/.next/server/chunks/[externals]__1s61mel._.js +0 -3
  151. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +0 -3
  152. package/.next/standalone/.next/server/chunks/ssr/[externals]__1_g_b3t._.js +0 -3
  153. package/.next/standalone/.next/static/chunks/01h6lfjz1_oie.js +0 -1
  154. package/.next/standalone/.next/static/chunks/2mazwb1qmur1p.js +0 -1
  155. /package/.next/standalone/.next/static/{pM1veX5Llx4YKEaiFc8Sf → LOuwMmfomO6hQYfMQYs4K}/_buildManifest.js +0 -0
  156. /package/.next/standalone/.next/static/{pM1veX5Llx4YKEaiFc8Sf → LOuwMmfomO6hQYfMQYs4K}/_clientMiddlewareManifest.js +0 -0
  157. /package/.next/standalone/.next/static/{pM1veX5Llx4YKEaiFc8Sf → LOuwMmfomO6hQYfMQYs4K}/_ssgManifest.js +0 -0
@@ -2,7 +2,8 @@
2
2
  import { execSync } from 'child_process';
3
3
  import { readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
- import { fileURLToPath } from 'url';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import { daemonOptionalDependencies } from './daemon-platforms.mjs';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const rootPkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -11,7 +12,7 @@ const DRY_RUN = process.argv.includes('--dry-run');
11
12
  const distTagIdx = process.argv.indexOf('--dist-tag');
12
13
  const DIST_TAG = distTagIdx !== -1 ? process.argv[distTagIdx + 1] : (VERSION.includes('-') ? 'beta' : 'latest');
13
14
 
14
- const ALIASES = [
15
+ export const ALIASES = [
15
16
  // Formatting variants — no "ai", or hyphen/underscore separators
16
17
  'failproof',
17
18
  'failproof-ai',
@@ -31,57 +32,77 @@ const ALIASES = [
31
32
  'faliproofai',
32
33
  ];
33
34
 
34
- const warnings = [];
35
-
36
- for (const name of ALIASES) {
37
- const tmpDir = join('/tmp', `npm-alias-${name}-${Date.now()}`);
38
- const binDir = join(tmpDir, 'bin');
39
- mkdirSync(binDir, { recursive: true });
40
-
41
- const pkg = {
35
+ /**
36
+ * The manifest one alias stub publishes.
37
+ *
38
+ * The four `@failproofai/failproofaid-<platform>` pins are the same ones the
39
+ * root package carries. They arrive transitively through the `failproofai`
40
+ * dependency anyway, but naming them here means someone who installs a typo'd
41
+ * name gets the daemon binary for exactly the reason the root package does,
42
+ * rather than by accident of resolution — and a test can assert it.
43
+ */
44
+ export function aliasManifest(name, version = VERSION, pkg = rootPkg) {
45
+ return {
42
46
  name,
43
- version: VERSION,
47
+ version,
44
48
  description: `Alias for failproofai — installs if you typed '${name}' instead of 'failproofai'`,
45
49
  bin: { [name]: './bin/proxy.js' },
46
50
  files: ['bin/'],
47
- dependencies: { failproofai: VERSION },
51
+ dependencies: { failproofai: version },
52
+ optionalDependencies: daemonOptionalDependencies(version),
48
53
  publishConfig: { access: 'public' },
49
- repository: rootPkg.repository,
50
- homepage: rootPkg.homepage,
51
- bugs: rootPkg.bugs,
52
- license: rootPkg.license,
54
+ repository: pkg.repository,
55
+ homepage: pkg.homepage,
56
+ bugs: pkg.bugs,
57
+ license: pkg.license,
53
58
  };
59
+ }
54
60
 
55
- writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
56
- cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
61
+ export function main() {
62
+ const warnings = [];
57
63
 
58
- if (DRY_RUN) {
59
- console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
60
- console.log(JSON.stringify(pkg, null, 2));
61
- console.log('---');
62
- rmSync(tmpDir, { recursive: true, force: true });
63
- continue;
64
- }
64
+ for (const name of ALIASES) {
65
+ const tmpDir = join('/tmp', `npm-alias-${name}-${Date.now()}`);
66
+ const binDir = join(tmpDir, 'bin');
67
+ mkdirSync(binDir, { recursive: true });
68
+
69
+ const pkg = aliasManifest(name);
65
70
 
66
- console.log(`Publishing ${name}@${VERSION}...`);
67
- try {
68
- execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
69
- console.log(`Done: ${name}`);
70
- } catch (err) {
71
- const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
72
- if (output.includes('too similar')) {
73
- warnings.push(`${name}: blocked by npm similarity check — request via npm support`);
74
- } else if (output.includes('cannot publish over')) {
75
- console.log(`[skip] ${name}: already published at ${VERSION}`);
76
- } else {
77
- warnings.push(`${name}: ${output.trim().split('\n').find(l => l.includes('npm error')) ?? 'unknown error'}`);
71
+ writeFileSync(join(tmpDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
72
+ cpSync(join(__dirname, 'alias-proxy.js'), join(binDir, 'proxy.js'));
73
+
74
+ if (DRY_RUN) {
75
+ console.log(`[dry-run] Would publish ${name}@${VERSION} (tag: ${DIST_TAG})`);
76
+ console.log(JSON.stringify(pkg, null, 2));
77
+ console.log('---');
78
+ rmSync(tmpDir, { recursive: true, force: true });
79
+ continue;
80
+ }
81
+
82
+ console.log(`Publishing ${name}@${VERSION}...`);
83
+ try {
84
+ execSync(`npm publish --tag ${DIST_TAG}`, { cwd: tmpDir, stdio: 'pipe' });
85
+ console.log(`Done: ${name}`);
86
+ } catch (err) {
87
+ const output = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
88
+ if (output.includes('too similar')) {
89
+ warnings.push(`${name}: blocked by npm similarity check — request via npm support`);
90
+ } else if (output.includes('cannot publish over')) {
91
+ console.log(`[skip] ${name}: already published at ${VERSION}`);
92
+ } else {
93
+ warnings.push(`${name}: ${output.trim().split('\n').find(l => l.includes('npm error')) ?? 'unknown error'}`);
94
+ }
78
95
  }
96
+
97
+ rmSync(tmpDir, { recursive: true, force: true });
79
98
  }
80
99
 
81
- rmSync(tmpDir, { recursive: true, force: true });
100
+ if (warnings.length > 0) {
101
+ console.log('\n::warning::Some alias packages were not published:');
102
+ for (const w of warnings) console.log(` - ${w}`);
103
+ }
82
104
  }
83
105
 
84
- if (warnings.length > 0) {
85
- console.log('\n::warning::Some alias packages were not published:');
86
- for (const w of warnings) console.log(` - ${w}`);
106
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
107
+ main();
87
108
  }
@@ -143,6 +143,9 @@ export async function connectToCloud(input: ConnectInput): Promise<ConnectOutcom
143
143
  // a terminal, so they stay off unless explicitly asked for.
144
144
  sessions: input.sessions === true,
145
145
  environment: input.environment,
146
+ // So the daemon stamps this machine's id on every collected event and the
147
+ // dashboard groups by machine, not by per-project agent_id.
148
+ machineId: input.machineId,
146
149
  };
147
150
  writeCollectorSettings(settings);
148
151
  outcome.anyConfigured = true;
@@ -45,6 +45,13 @@ export interface CollectorSettings {
45
45
  hooksVerbosity?: "all" | "decisions" | "off";
46
46
  redact?: "minimal" | "off";
47
47
  environment?: string;
48
+ /**
49
+ * The machine's id, so the daemon can stamp it on every collected event and
50
+ * the dashboard groups by machine rather than by `agent_id` (a per-project
51
+ * identity). Written by `--connect --machine-id`; the Rust collector reads it
52
+ * as `collector.machineId`.
53
+ */
54
+ machineId?: string;
48
55
  }
49
56
 
50
57
  export function failproofaiHome(): string {
@@ -130,6 +137,9 @@ export function writeCollectorSettings(settings: CollectorSettings): void {
130
137
  hooksVerbosity: settings.hooksVerbosity ?? "decisions",
131
138
  redact: settings.redact ?? "minimal",
132
139
  environment: settings.environment ?? "local",
140
+ // Omit when unknown rather than writing null: the Rust side treats an
141
+ // absent field as "no machine", which is the correct fallback.
142
+ ...(settings.machineId ? { machineId: settings.machineId } : {}),
133
143
  },
134
144
  });
135
145
  }
@@ -1,20 +1,31 @@
1
1
  /**
2
- * Fetches the compiled `failproofaid` binary for this platform from the
3
- * GitHub Release that matches this CLI's own version.
2
+ * Gets the compiled `failproofaid` binary for this platform onto disk, from
3
+ * whichever of its two channels can supply it.
4
4
  *
5
- * Why a download rather than a dependency: the daemon is a 4-way
6
- * cross-compiled Rust binary, and shipping it through npm means four
7
- * per-platform packages that every install resolves, an npm org to own the
8
- * scope, and a second publish channel that can silently drift from the first
9
- * (it already did — the platform packages were declared as
10
- * `optionalDependencies` and never published, so every install of them 404'd).
11
- * The release assets have to exist for standalone installs regardless, so
12
- * pointing the CLI at the same artifacts leaves exactly one channel.
5
+ * **npm first.** The four binaries publish as
6
+ * `@failproofai/failproofaid-<os>-<arch>` platform packages, pinned as
7
+ * `optionalDependencies` of this package, so `npm install failproofai` has
8
+ * usually already brought down the one matching this machine — `os`/`cpu`
9
+ * make npm skip the other three. Copying that file costs no network at all,
10
+ * which is the only thing that works behind a proxy or air-gapped.
13
11
  *
14
- * The URL is *constructed*, never discovered — no API call, no `releases/latest`
15
- * redirect, no rate limit, and no chance of fetching a daemon built from
16
- * different source than the CLI talking to it: the tag is this package's own
17
- * version, so a CLI upgrade downloads its own matching binary.
12
+ * **The GitHub Release second.** `failproofaid-<os>-<arch>.gz` plus a
13
+ * `SHA256SUMS` manifest are attached to the release tagged with this CLI's own
14
+ * version. That channel has to exist regardless — it is how anyone installs
15
+ * the daemon standalone, and how an install that skipped optional dependencies
16
+ * still gets one.
17
+ *
18
+ * The pair failed once before in a way worth keeping in mind: the platform
19
+ * packages were declared as `optionalDependencies` and never published, so
20
+ * every install resolved four 404s (see CHANGELOG 1.0.0-beta.3). What makes
21
+ * them safe now is that `scripts/build-daemon-packages.mjs` publishes them
22
+ * *before* the root package that pins them, and fails the release if it
23
+ * cannot — the names always exist by the time anything depends on them.
24
+ *
25
+ * The download URL is *constructed*, never discovered — no API call, no
26
+ * `releases/latest` redirect, no rate limit, and no chance of fetching a daemon
27
+ * built from different source than the CLI talking to it: the tag is this
28
+ * package's own version, so a CLI upgrade fetches its own matching binary.
18
29
  *
19
30
  * Everything here fails closed and never throws. A daemon that cannot be
20
31
  * fetched leaves the machine on the in-process path it was already using;
@@ -22,9 +33,10 @@
22
33
  * this writes an executable that a service manager will then run at login.
23
34
  */
24
35
  import { createHash } from "node:crypto";
25
- import { chmodSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
36
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
37
+ import { createRequire } from "node:module";
26
38
  import { homedir } from "node:os";
27
- import { resolve } from "node:path";
39
+ import { dirname, resolve } from "node:path";
28
40
  import { gunzipSync } from "node:zlib";
29
41
  import { version } from "../../package.json";
30
42
  import type { PlatformKey } from "./daemon-service";
@@ -82,6 +94,114 @@ export interface DaemonDownloadResult {
82
94
  error?: string;
83
95
  }
84
96
 
97
+ /** The npm package carrying the daemon binary for one platform. */
98
+ export function platformPackageName(key: PlatformKey): string {
99
+ return `@failproofai/failproofaid-${key}`;
100
+ }
101
+
102
+ /**
103
+ * Locates the binary inside the installed platform package, or null.
104
+ *
105
+ * Resolution is anchored at `FAILPROOFAI_PACKAGE_ROOT` (set by
106
+ * `bin/failproofai.mjs` and the bundled `dist/cli.mjs`) rather than at
107
+ * `import.meta.url`, which does not survive the CJS bundle. Node's algorithm
108
+ * walks up from there, which covers both real layouts: a global install nests
109
+ * its dependencies at `<root>/node_modules/@failproofai/…`, a local one hoists
110
+ * them to `<project>/node_modules/@failproofai/…`.
111
+ *
112
+ * The specifier is *computed*, deliberately. A literal would make the bundler
113
+ * try to resolve a package that is optional, platform-specific, and absent on
114
+ * three machines out of four at build time.
115
+ */
116
+ export function npmPlatformBinaryPath(key: PlatformKey): string | null {
117
+ const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT;
118
+ if (!packageRoot) return null;
119
+ const name = platformPackageName(key);
120
+
121
+ try {
122
+ const requireFromRoot = createRequire(resolve(packageRoot, "package.json"));
123
+ const found = platformPackageBinary(dirname(requireFromRoot.resolve(`${name}/package.json`)));
124
+ if (found) return found;
125
+ } catch {
126
+ // Not installed (the common case on a machine that skipped optional
127
+ // dependencies), or a resolver that cannot see it. The direct check below
128
+ // is the fallback, not a failure.
129
+ }
130
+
131
+ return platformPackageBinary(resolve(packageRoot, "node_modules", name));
132
+ }
133
+
134
+ /**
135
+ * The binary inside one candidate platform-package directory, if it is the
136
+ * right one.
137
+ *
138
+ * The version check is not belt-and-braces. npm pins the exact version, but a
139
+ * workspace holding two `failproofai` versions can hoist the other one's
140
+ * platform package to the top of the tree — and installing that binary under
141
+ * *this* version's filename would put a daemon built from different source
142
+ * behind a CLI that believes it matches, which is the single thing the
143
+ * versioned install path exists to prevent. A mismatch falls through to the
144
+ * download, whose URL is pinned to this version.
145
+ */
146
+ function platformPackageBinary(dir: string): string | null {
147
+ try {
148
+ const manifest = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")) as {
149
+ version?: string;
150
+ };
151
+ if (manifest.version !== version) return null;
152
+ } catch {
153
+ return null;
154
+ }
155
+ const candidate = resolve(dir, "bin", "failproofaid");
156
+ return existsSync(candidate) ? candidate : null;
157
+ }
158
+
159
+ /**
160
+ * Writes the daemon binary to its versioned path.
161
+ *
162
+ * Shared by both channels so a binary lands identically however it arrived:
163
+ * staged in the same directory as the target (so the install is an atomic
164
+ * rename within one filesystem, never a half-written executable a service
165
+ * manager could pick up), mode 0755, pid-suffixed so two concurrent wizards
166
+ * stay off each other's file.
167
+ */
168
+ function installBinaryBytes(binary: Buffer): string {
169
+ const target = installedBinaryPath();
170
+ const tempPath = `${target}.${process.pid}.tmp`;
171
+ try {
172
+ mkdirSync(daemonBinaryDir(), { recursive: true });
173
+ writeFileSync(tempPath, binary);
174
+ chmodSync(tempPath, 0o755);
175
+ renameSync(tempPath, target);
176
+ return target;
177
+ } catch (err) {
178
+ rmSync(tempPath, { force: true });
179
+ throw err;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Installs the daemon from the npm platform package, if one is present.
185
+ *
186
+ * No checksum step, unlike the download: npm verified the tarball's integrity
187
+ * against the registry when it installed it, and this file never crossed the
188
+ * network on its own.
189
+ *
190
+ * Deliberately NOT gated on `FAILPROOFAI_NO_DOWNLOAD` — that variable exists
191
+ * so an air-gapped machine does not reach out, and this is a local copy. On
192
+ * exactly those machines it is the only channel that can work.
193
+ */
194
+ export function installFromNpmPackage(key: PlatformKey): DaemonDownloadResult {
195
+ const source = npmPlatformBinaryPath(key);
196
+ if (!source) return { error: `${platformPackageName(key)} is not installed` };
197
+ try {
198
+ return { path: installBinaryBytes(readFileSync(source)) };
199
+ } catch (err) {
200
+ const message = err instanceof Error ? err.message : String(err);
201
+ return { error: `failed to install failproofaid from ${source}: ${message}` };
202
+ }
203
+ }
204
+
85
205
  async function fetchBytes(url: string): Promise<Buffer> {
86
206
  const response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
87
207
  if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`);
@@ -118,10 +238,6 @@ export async function downloadFailproofaidBinary(key: PlatformKey): Promise<Daem
118
238
  }
119
239
 
120
240
  const assetName = `failproofaid-${key}.gz`;
121
- // Same directory as the target so the install is a rename within one
122
- // filesystem — atomic, and never a half-written binary a service manager
123
- // could pick up. The pid keeps two concurrent wizards off each other's file.
124
- const tempPath = `${target}.${process.pid}.tmp`;
125
241
 
126
242
  try {
127
243
  const [asset, manifest] = await Promise.all([
@@ -138,14 +254,8 @@ export async function downloadFailproofaidBinary(key: PlatformKey): Promise<Daem
138
254
  return { error: `checksum mismatch for ${assetName} (expected ${expected}, got ${actual})` };
139
255
  }
140
256
 
141
- const binary = gunzipSync(asset);
142
- mkdirSync(daemonBinaryDir(), { recursive: true });
143
- writeFileSync(tempPath, binary);
144
- chmodSync(tempPath, 0o755);
145
- renameSync(tempPath, target);
146
- return { path: target };
257
+ return { path: installBinaryBytes(gunzipSync(asset)) };
147
258
  } catch (err) {
148
- rmSync(tempPath, { force: true });
149
259
  const message = err instanceof Error ? err.message : String(err);
150
260
  return { error: `failed to download failproofaid v${version} for ${key}: ${message}` };
151
261
  }
@@ -22,7 +22,7 @@ import { resolve, dirname } from "node:path";
22
22
  import { execFileSync } from "node:child_process";
23
23
  import { hookLogWarn } from "./hook-logger";
24
24
  import { getConfigPathForScope } from "./hooks-config";
25
- import { downloadFailproofaidBinary, installedBinaryPath } from "./daemon-download";
25
+ import { downloadFailproofaidBinary, installFromNpmPackage, installedBinaryPath } from "./daemon-download";
26
26
 
27
27
  /**
28
28
  * Every `systemctl --user` / `launchctl` call is bounded. Both talk to a
@@ -145,13 +145,19 @@ export function resolveFailproofaidBinaryPath(): string | null {
145
145
  }
146
146
 
147
147
  /**
148
- * Resolves the binary, downloading it from this version's release if it is
149
- * not already on disk.
148
+ * Resolves the binary, installing one if it is not already on disk.
150
149
  *
151
- * Only the install path calls this. The npm package ships no binary — it is
152
- * one CLI tarball for every platform — so `failproofai config` choosing the
153
- * global scope is the moment a machine that opted into a daemon actually
154
- * acquires one.
150
+ * Only the install path calls this — `failproofai config` choosing the global
151
+ * scope is the moment a machine that opted into a daemon actually acquires
152
+ * one. Two channels, in this order:
153
+ *
154
+ * 1. The `@failproofai/failproofaid-<os>-<arch>` platform package, which a
155
+ * plain `npm install failproofai` already brought down as an optional
156
+ * dependency. No network, so it is also the only channel that works
157
+ * air-gapped or behind a proxy that blocks github.com.
158
+ * 2. The GitHub Release asset for this exact version, checksum-verified.
159
+ * Covers installs that skipped optional dependencies, older tarballs, and
160
+ * anyone installing the daemon standalone.
155
161
  */
156
162
  export async function ensureFailproofaidBinary(): Promise<{ path?: string; reason?: string }> {
157
163
  const existing = resolveFailproofaidBinaryPath();
@@ -162,9 +168,18 @@ export async function ensureFailproofaidBinary(): Promise<{ path?: string; reaso
162
168
  return { reason: `failproofaid has no prebuilt binary for ${process.platform}/${process.arch}` };
163
169
  }
164
170
 
171
+ const fromNpm = installFromNpmPackage(key);
172
+ if (fromNpm.path) return { path: fromNpm.path };
173
+
165
174
  const result = await downloadFailproofaidBinary(key);
166
175
  if (result.path) return { path: result.path };
167
- return { reason: result.error ?? "failproofaid binary could not be downloaded" };
176
+ // Both channels are worth reporting: "not installed" alone reads as a broken
177
+ // package, and the download error alone hides that npm could have supplied it.
178
+ return {
179
+ reason: result.error
180
+ ? `${result.error} (${fromNpm.error})`
181
+ : "failproofaid binary could not be installed",
182
+ };
168
183
  }
169
184
 
170
185
  /**
@@ -1,3 +0,0 @@
1
- module.exports=[14747,(e,o,r)=>{o.exports=e.x("path",()=>require("path"))},78500,(e,o,r)=>{o.exports=e.x("node:async_hooks",()=>require("node:async_hooks"))},1457,(e,o,r)=>{o.exports=e.x("node:readline",()=>require("node:readline"))}];
2
-
3
- //# sourceMappingURL=%5Bexternals%5D__14odj07._.js.map
@@ -1,3 +0,0 @@
1
- module.exports=[14747,(e,o,r)=>{o.exports=e.x("path",()=>require("path"))},2157,(e,o,r)=>{o.exports=e.x("node:fs",()=>require("node:fs"))},1457,(e,o,r)=>{o.exports=e.x("node:readline",()=>require("node:readline"))},78500,(e,o,r)=>{o.exports=e.x("node:async_hooks",()=>require("node:async_hooks"))}];
2
-
3
- //# sourceMappingURL=%5Bexternals%5D__1s61mel._.js.map