failproofai 0.0.14-beta.0 → 0.0.14-beta.1
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/.next/standalone/.codex/hooks.json +6 -6
- package/.next/standalone/.cursor/hooks.json +6 -6
- package/.next/standalone/.factory/hooks.json +9 -9
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0brjczq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__0wp9w6i._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_12rjfea.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 0d5lypu1zs21r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 0emg30_6svycu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0o9wpnze1ey8y.js} +1 -1
- package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1359o5785rmve.js} +1 -1
- package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 20tuctw38jbgw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0405htbxay9vd.js → 2ldhnaweu9ew3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 2vav_x0_336ds.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
- package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
- package/.next/standalone/.next/static/chunks/{35-90m9py64d4.js → 3cg7x0joi2b98.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2o3ql0n5owqsw.js → 3k87p-tijud-4.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2m_tbl39fcedn.js → 3kmaqcp7rldad.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
- package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
- package/.next/standalone/ci/cli-integration/Dockerfile +47 -0
- package/.next/standalone/ci/cli-integration/README.md +69 -0
- package/.next/standalone/ci/cli-integration/canary-policies.mjs +45 -0
- package/.next/standalone/ci/cli-integration/capture-tokens.sh +48 -0
- package/.next/standalone/ci/cli-integration/inject-tokens.sh +44 -0
- package/.next/standalone/ci/cli-integration/install-clis.sh +91 -0
- package/.next/standalone/ci/cli-integration/probe-cli.sh +208 -0
- package/.next/standalone/ci/cli-integration/report.js +74 -0
- package/.next/standalone/ci/cli-integration/run.sh +107 -0
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
- package/.next/standalone/lib/error-origin.ts +67 -0
- package/.next/standalone/lib/install-check.ts +231 -0
- package/.next/standalone/package.json +1 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -17
- package/bin/failproofai.mjs +11 -1
- package/dist/cli.mjs +479 -217
- package/lib/error-origin.ts +67 -0
- package/lib/install-check.ts +231 -0
- package/package.json +1 -3
- package/scripts/dev-hook.mjs +292 -0
- package/scripts/install-diagnosis.mjs +5 -8
- package/scripts/install-telemetry.mjs +12 -4
- package/scripts/translate-docs/cli.ts +47 -1
- package/scripts/translate-docs/mdx-translator.ts +133 -4
- package/scripts/translate-docs/readme-translator.ts +13 -5
- package/scripts/translate-docs/translator.ts +38 -2
- package/scripts/validate-mdx.ts +12 -4
- package/src/audit/cli.ts +41 -9
- package/src/hooks/tui.ts +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
- package/scripts/postinstall.mjs +0 -242
- package/scripts/preuninstall.mjs +0 -131
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → LkN-JjmhKGmL-WklH7WVi}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Works out whether a browser error actually came from the dashboard.
|
|
3
|
+
*
|
|
4
|
+
* `window`'s "error" and "unhandledrejection" events are page-global: browser
|
|
5
|
+
* extensions inject content scripts into the same page and share the same
|
|
6
|
+
* window, so their failures reach our listeners and get reported as ours. The
|
|
7
|
+
* observed case was MetaMask's "Failed to connect to MetaMask" arriving as a
|
|
8
|
+
* failproofai `unhandled_rejection`. Extensions are user-installed and
|
|
9
|
+
* open-ended, so the noise is unbounded and unrelated to our code.
|
|
10
|
+
*
|
|
11
|
+
* We therefore attribute positively — report an error only when it can be
|
|
12
|
+
* traced to our own origin — rather than maintaining a denylist of extensions
|
|
13
|
+
* we happen to know about. React render errors don't depend on this: the error
|
|
14
|
+
* boundaries (`app/global-error.tsx`, `app/components/error-fallback.tsx`)
|
|
15
|
+
* report `client_error` directly, and React only invokes those for errors thrown
|
|
16
|
+
* inside our own tree.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Any `<vendor>-extension://` URL — `chrome-extension://`, `moz-extension://`,
|
|
21
|
+
* `safari-web-extension://`, `ms-browser-extension://`. Matching the shared
|
|
22
|
+
* suffix rather than an explicit vendor list means a new browser's scheme is
|
|
23
|
+
* filtered the day it ships.
|
|
24
|
+
*/
|
|
25
|
+
const EXTENSION_URL = /\b[\w-]+-extension:\/\//i;
|
|
26
|
+
|
|
27
|
+
export type ErrorOrigin =
|
|
28
|
+
/** Traceable to our own origin — a real dashboard failure. */
|
|
29
|
+
| "app"
|
|
30
|
+
/** Came from a browser extension sharing the page. */
|
|
31
|
+
| "extension"
|
|
32
|
+
/** Not attributable: no stack, or a cross-origin "Script error.". */
|
|
33
|
+
| "unknown";
|
|
34
|
+
|
|
35
|
+
export interface ErrorOriginInput {
|
|
36
|
+
/** `error.stack` / `reason.stack`, when the thrown value was an Error. */
|
|
37
|
+
stack?: string | null;
|
|
38
|
+
/** `ErrorEvent.filename` — the script URL. Absent on rejections. */
|
|
39
|
+
filename?: string | null;
|
|
40
|
+
/** Typically `window.location.origin`. */
|
|
41
|
+
appOrigin: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify where an error came from, using whatever location information the
|
|
46
|
+
* browser gave us.
|
|
47
|
+
*
|
|
48
|
+
* Extension frames are checked first: an error whose stack passes through an
|
|
49
|
+
* extension is that extension's problem even if one of our frames appears
|
|
50
|
+
* further down.
|
|
51
|
+
*/
|
|
52
|
+
export function classifyErrorOrigin({ stack, filename, appOrigin }: ErrorOriginInput): ErrorOrigin {
|
|
53
|
+
const text = [filename, stack].filter(Boolean).join("\n");
|
|
54
|
+
if (!text.trim()) return "unknown";
|
|
55
|
+
if (EXTENSION_URL.test(text)) return "extension";
|
|
56
|
+
if (appOrigin && text.includes(appOrigin)) return "app";
|
|
57
|
+
return "unknown";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error is ours to report. Only positively-attributed errors qualify:
|
|
62
|
+
* `unknown` covers cross-origin "Script error." and stack-less rejections, which
|
|
63
|
+
* are unactionable anyway — there's nothing in them to debug.
|
|
64
|
+
*/
|
|
65
|
+
export function isAppError(input: ErrorOriginInput): boolean {
|
|
66
|
+
return classifyErrorOrigin(input) === "app";
|
|
67
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install reporting, driven from the CLI instead of an npm lifecycle script.
|
|
3
|
+
*
|
|
4
|
+
* This used to live in `scripts/postinstall.mjs`. Every package manager now
|
|
5
|
+
* blocks dependency install scripts by default — npm 12 (`allowScripts`), pnpm,
|
|
6
|
+
* bun, and Yarn ≥4.14 — and a publisher cannot opt its own package in, so a
|
|
7
|
+
* postinstall hook is no longer a channel that reaches users. The first non-hook
|
|
8
|
+
* CLI invocation is.
|
|
9
|
+
*
|
|
10
|
+
* Two constraints shape this module:
|
|
11
|
+
*
|
|
12
|
+
* 1. It must never run on the `--hook` fast path in bin/failproofai.mjs. That
|
|
13
|
+
* path fires on every tool call of every agent session; the check belongs
|
|
14
|
+
* in runCli() only.
|
|
15
|
+
* 2. It must be free on the steady-state path. When the recorded version
|
|
16
|
+
* matches, this returns before touching anything else.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is best-effort: telemetry must never break, delay, or fail a
|
|
19
|
+
* real command.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { platform, arch, release, homedir, hostname } from "node:os";
|
|
24
|
+
import { createHmac } from "node:crypto";
|
|
25
|
+
import { trackInstallEvent } from "../scripts/install-telemetry.mjs";
|
|
26
|
+
|
|
27
|
+
const FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
|
|
28
|
+
const NAMESPACE = "failproofai-telemetry-v1";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cap on the once-per-version report so a hanging network never stalls a command.
|
|
32
|
+
*
|
|
33
|
+
* A ceiling, not a cost: delivery resolves as soon as PostHog answers, measured
|
|
34
|
+
* at ~1.1s from a cold process (these events are the first thing a fresh process
|
|
35
|
+
* sends, so they always pay the DNS + TLS handshake). Raising this only matters
|
|
36
|
+
* when the network is slow — precisely when a tight budget would silently drop
|
|
37
|
+
* the report instead.
|
|
38
|
+
*
|
|
39
|
+
* 5s matches hook-telemetry and the postinstall script this replaces. An earlier
|
|
40
|
+
* 2s left barely any headroom over a 1.85s worst case seen in testing, for a
|
|
41
|
+
* saving nobody experiences on a healthy network.
|
|
42
|
+
*/
|
|
43
|
+
const REPORT_TIMEOUT_MS = 5000;
|
|
44
|
+
|
|
45
|
+
function hashToId(raw: string): string {
|
|
46
|
+
return createHmac("sha256", NAMESPACE).update(raw).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function lastVersionPath(): string {
|
|
50
|
+
return resolve(homedir(), ".failproofai", "last-version");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Semver ordering. A release outranks the same version carrying a prerelease
|
|
55
|
+
* tag (semver §11); within a prerelease, numeric identifiers rank below
|
|
56
|
+
* non-numeric ones.
|
|
57
|
+
*
|
|
58
|
+
* Prerelease identifiers are separated by dots only (semver §9). A hyphen is a
|
|
59
|
+
* legal character *inside* an identifier, so `beta-2` is one non-numeric
|
|
60
|
+
* identifier compared lexically — not `beta` and `2`.
|
|
61
|
+
*/
|
|
62
|
+
export function compareSemver(a: string, b: string): number {
|
|
63
|
+
const parse = (v: string) => {
|
|
64
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v);
|
|
65
|
+
if (!m) return null;
|
|
66
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
67
|
+
};
|
|
68
|
+
const pa = parse(a);
|
|
69
|
+
const pb = parse(b);
|
|
70
|
+
if (!pa || !pb) return a < b ? -1 : a > b ? 1 : 0;
|
|
71
|
+
for (let i = 0; i < 3; i++) {
|
|
72
|
+
if (pa.nums[i] !== pb.nums[i]) return pa.nums[i] < pb.nums[i] ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
if (pa.pre === null && pb.pre === null) return 0;
|
|
75
|
+
if (pa.pre === null) return 1;
|
|
76
|
+
if (pb.pre === null) return -1;
|
|
77
|
+
const ax = pa.pre.split(".");
|
|
78
|
+
const bx = pb.pre.split(".");
|
|
79
|
+
for (let i = 0; i < Math.max(ax.length, bx.length); i++) {
|
|
80
|
+
const ai = ax[i], bi = bx[i];
|
|
81
|
+
if (ai === undefined) return -1;
|
|
82
|
+
if (bi === undefined) return 1;
|
|
83
|
+
const aNum = /^\d+$/.test(ai), bNum = /^\d+$/.test(bi);
|
|
84
|
+
if (aNum && bNum) {
|
|
85
|
+
const d = Number(ai) - Number(bi);
|
|
86
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
87
|
+
} else if (aNum) {
|
|
88
|
+
return -1;
|
|
89
|
+
} else if (bNum) {
|
|
90
|
+
return 1;
|
|
91
|
+
} else if (ai !== bi) {
|
|
92
|
+
return ai < bi ? -1 : 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HooksState {
|
|
99
|
+
configured: boolean;
|
|
100
|
+
registered: boolean;
|
|
101
|
+
policyCount: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Snapshot of the user's hook configuration, carried as properties on
|
|
106
|
+
* package_installed.
|
|
107
|
+
*
|
|
108
|
+
* Only inspects Claude's settings, which understates `registered` for someone
|
|
109
|
+
* running failproofai against another CLI. Preserved as-is from the postinstall
|
|
110
|
+
* script so the event keeps its historical meaning.
|
|
111
|
+
*/
|
|
112
|
+
export function checkHooks(): HooksState {
|
|
113
|
+
const hooksConfigPath = resolve(homedir(), ".failproofai", "policies-config.json");
|
|
114
|
+
if (!existsSync(hooksConfigPath)) {
|
|
115
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let config;
|
|
119
|
+
try {
|
|
120
|
+
config = JSON.parse(readFileSync(hooksConfigPath, "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!Array.isArray(config.enabledPolicies) || config.enabledPolicies.length === 0) {
|
|
126
|
+
return { configured: false, registered: false, policyCount: 0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const policyCount = config.enabledPolicies.length;
|
|
130
|
+
const settingsPath = resolve(homedir(), ".claude", "settings.json");
|
|
131
|
+
if (!existsSync(settingsPath)) return { configured: true, registered: false, policyCount };
|
|
132
|
+
|
|
133
|
+
let settings;
|
|
134
|
+
try {
|
|
135
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
136
|
+
} catch {
|
|
137
|
+
return { configured: true, registered: false, policyCount };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!settings?.hooks) return { configured: true, registered: false, policyCount };
|
|
141
|
+
|
|
142
|
+
for (const matchers of Object.values(settings.hooks)) {
|
|
143
|
+
if (!Array.isArray(matchers)) continue;
|
|
144
|
+
for (const matcher of matchers) {
|
|
145
|
+
if (!matcher?.hooks) continue;
|
|
146
|
+
if (matcher.hooks.some((h: Record<string, unknown>) => h[FAILPROOFAI_HOOK_MARKER] === true)) {
|
|
147
|
+
return { configured: true, registered: true, policyCount };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { configured: true, registered: false, policyCount };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Report an install or upgrade, at most once per version.
|
|
157
|
+
*
|
|
158
|
+
* Fires first_install (nothing recorded yet) or version_changed (recorded
|
|
159
|
+
* version differs), then package_installed, then records `version`. A same-version
|
|
160
|
+
* run is a no-op — unlike the postinstall script this replaces, the CLI has no
|
|
161
|
+
* signal that would distinguish a reinstall from an ordinary invocation, so
|
|
162
|
+
* `direction: "reinstall"` is no longer reported.
|
|
163
|
+
*/
|
|
164
|
+
export async function maybeReportInstall(version: string): Promise<void> {
|
|
165
|
+
try {
|
|
166
|
+
const versionFile = lastVersionPath();
|
|
167
|
+
|
|
168
|
+
let previousVersion: string | null = null;
|
|
169
|
+
try {
|
|
170
|
+
if (existsSync(versionFile)) {
|
|
171
|
+
previousVersion = readFileSync(versionFile, "utf8").trim() || null;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Unreadable marker — treat as first run rather than give up.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Steady state: same version already reported. Costs one stat() per command.
|
|
178
|
+
if (previousVersion === version) return;
|
|
179
|
+
|
|
180
|
+
const hooks = checkHooks();
|
|
181
|
+
|
|
182
|
+
const events: Promise<void>[] = [];
|
|
183
|
+
if (previousVersion === null) {
|
|
184
|
+
events.push(
|
|
185
|
+
trackInstallEvent("first_install", {
|
|
186
|
+
platform: platform(),
|
|
187
|
+
arch: arch(),
|
|
188
|
+
os_release: release(),
|
|
189
|
+
node_version: process.versions.node,
|
|
190
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
191
|
+
);
|
|
192
|
+
} else {
|
|
193
|
+
const cmp = compareSemver(previousVersion, version);
|
|
194
|
+
events.push(
|
|
195
|
+
trackInstallEvent("version_changed", {
|
|
196
|
+
from_version: previousVersion,
|
|
197
|
+
to_version: version,
|
|
198
|
+
direction: cmp < 0 ? "upgrade" : "downgrade",
|
|
199
|
+
platform: platform(),
|
|
200
|
+
arch: arch(),
|
|
201
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
events.push(
|
|
206
|
+
trackInstallEvent("package_installed", {
|
|
207
|
+
platform: platform(),
|
|
208
|
+
arch: arch(),
|
|
209
|
+
os_release: release(),
|
|
210
|
+
node_version: process.versions.node,
|
|
211
|
+
hostname_hash: hashToId(hostname()),
|
|
212
|
+
hooks_configured: hooks.configured,
|
|
213
|
+
hooks_registered: hooks.registered,
|
|
214
|
+
enabled_policy_count: hooks.policyCount,
|
|
215
|
+
}, { version, timeoutMs: REPORT_TIMEOUT_MS }),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Record before awaiting: a dropped event is cheaper than re-reporting the
|
|
219
|
+
// same version on every command because delivery kept failing.
|
|
220
|
+
try {
|
|
221
|
+
mkdirSync(resolve(homedir(), ".failproofai"), { recursive: true });
|
|
222
|
+
writeFileSync(versionFile, version, "utf8");
|
|
223
|
+
} catch {
|
|
224
|
+
// best-effort
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await Promise.all(events.map((p) => p.catch(() => {})));
|
|
228
|
+
} catch {
|
|
229
|
+
// Reporting must never break a command.
|
|
230
|
+
}
|
|
231
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.14-beta.
|
|
3
|
+
"version": "0.0.14-beta.1",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
"test": "vitest",
|
|
31
31
|
"test:run": "vitest run",
|
|
32
32
|
"lint": "eslint . --config eslint.config.mjs",
|
|
33
|
-
"postinstall": "node scripts/postinstall.mjs",
|
|
34
|
-
"preuninstall": "node scripts/preuninstall.mjs",
|
|
35
33
|
"prepare": "bun run build",
|
|
36
34
|
"test:e2e": "vitest run --config vitest.config.e2e.mts",
|
|
37
35
|
"test:e2e:watch": "vitest --config vitest.config.e2e.mts",
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* failproofai DEV-ONLY hook launcher — for THIS repo's dogfood configs only.
|
|
4
|
+
*
|
|
5
|
+
* End users never run this. They install `npx -y failproofai --hook <Event>`
|
|
6
|
+
* (see `src/hooks/integrations.ts`), which resolves the published package.
|
|
7
|
+
* This file exists because this repo dogfoods itself from source, and doing so
|
|
8
|
+
* has two prerequisites that silently rot:
|
|
9
|
+
*
|
|
10
|
+
* 1. `bun` on the hook's PATH. `bin/failproofai.mjs` cannot run under node —
|
|
11
|
+
* it does a bare `import { version } from "../package.json"` (node needs
|
|
12
|
+
* `with { type: "json" }`) and `await import("../src/hooks/handler")`,
|
|
13
|
+
* an extensionless TypeScript specifier node cannot resolve. But bun is
|
|
14
|
+
* routinely absent from a hook's PATH even when it's on yours: `npm i -g
|
|
15
|
+
* bun` installs into ONE nvm version's bin dir, so `nvm use <other>` drops
|
|
16
|
+
* it; a macOS GUI launch gets a launchd PATH built without ~/.zshrc.
|
|
17
|
+
* When that happens every hook exits 127 and the session runs with ZERO
|
|
18
|
+
* policy enforcement — silently.
|
|
19
|
+
*
|
|
20
|
+
* 2. `dist/index.js`. The committed policies in `.failproofai/policies/`
|
|
21
|
+
* `import from 'failproofai'`, which `findDistIndex()` resolves to that
|
|
22
|
+
* bundle. Without it all three fail-open and only builtins enforce.
|
|
23
|
+
*
|
|
24
|
+
* So: locate bun (installing it via npm if we must), make sure the bundle
|
|
25
|
+
* exists, then hand off to the real binary. If we cannot enforce, say so in
|
|
26
|
+
* one line rather than failing silently.
|
|
27
|
+
*
|
|
28
|
+
* INVARIANT — never write to stdout. Most CLIs' deny contracts are JSON on
|
|
29
|
+
* stdout (Copilot `{decision:"block"}`, Cursor `{followup_message}`, Devin,
|
|
30
|
+
* Factory-on-Stop, Antigravity). A single stray byte corrupts the parse and
|
|
31
|
+
* turns a denial into an allow. Diagnostics go to stderr; subprocesses we run
|
|
32
|
+
* for our own purposes get their stdout discarded, never inherited.
|
|
33
|
+
*/
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
36
|
+
import { constants, homedir } from "node:os";
|
|
37
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
|
|
40
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const REPO_ROOT = resolve(HERE, "..");
|
|
42
|
+
const SRC_BIN = resolve(REPO_ROOT, "bin", "failproofai.mjs");
|
|
43
|
+
const BUNDLE = resolve(REPO_ROOT, "dist", "index.js");
|
|
44
|
+
const CACHE_DIR = resolve(REPO_ROOT, "node_modules", ".cache", "failproofai-dev");
|
|
45
|
+
const LOCK_DIR = join(CACHE_DIR, ".lock");
|
|
46
|
+
|
|
47
|
+
/** A lock older than this belonged to a process that died mid-install. */
|
|
48
|
+
const LOCK_STALE_MS = 5 * 60_000;
|
|
49
|
+
/** 120 × 250ms = 30s, under the tightest hook timeout (Factory/Antigravity). */
|
|
50
|
+
const LOCK_WAIT_TRIES = 120;
|
|
51
|
+
const LOCK_WAIT_MS = 250;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {object} Deps
|
|
55
|
+
* @property {Record<string, string | undefined>} [env]
|
|
56
|
+
* @property {string} [platform]
|
|
57
|
+
* @property {string} [execPath]
|
|
58
|
+
* @property {string} [home]
|
|
59
|
+
* @property {(p: string) => boolean} [exists]
|
|
60
|
+
* @property {(p: string) => string[]} [readdir]
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
function warn(msg) {
|
|
64
|
+
process.stderr.write(`[failproofai-dev] ${msg}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sleepSync(ms) {
|
|
68
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readdirSafe(p) {
|
|
72
|
+
try {
|
|
73
|
+
return readdirSync(p);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Executable basenames to probe, most specific first. */
|
|
80
|
+
function exeNames(base, platform) {
|
|
81
|
+
return platform === "win32" ? [`${base}.exe`, `${base}.cmd`, base] : [base];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** First existing `<dir>/<name>` across dirs × names, or null. */
|
|
85
|
+
function probe(dirs, names, exists) {
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const candidate = join(dir, name);
|
|
89
|
+
if (exists(candidate)) return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Absolute path to a bun executable, or null. NEVER spawns a subprocess —
|
|
97
|
+
* this runs on every hook, so it is an existsSync walk and nothing more.
|
|
98
|
+
* @param {Deps} deps
|
|
99
|
+
*/
|
|
100
|
+
export function findBun(deps = {}) {
|
|
101
|
+
const env = deps.env ?? process.env;
|
|
102
|
+
const platform = deps.platform ?? process.platform;
|
|
103
|
+
const execPath = deps.execPath ?? process.execPath;
|
|
104
|
+
const home = deps.home ?? homedir();
|
|
105
|
+
const exists = deps.exists ?? existsSync;
|
|
106
|
+
const readdir = deps.readdir ?? readdirSafe;
|
|
107
|
+
|
|
108
|
+
const dirs = [];
|
|
109
|
+
const push = (d) => {
|
|
110
|
+
if (d && !dirs.includes(d)) dirs.push(d);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 1. PATH — the common case, and the only one that costs nothing.
|
|
114
|
+
for (const p of (env.PATH ?? "").split(delimiter)) push(p);
|
|
115
|
+
// 2. An explicit bun install root.
|
|
116
|
+
if (env.BUN_INSTALL) push(join(env.BUN_INSTALL, "bin"));
|
|
117
|
+
// 3. Where bun.sh's installer puts it. The macOS GUI-launch case: launchd's
|
|
118
|
+
// PATH has /usr/local/bin but never ~/.bun/bin.
|
|
119
|
+
push(join(home, ".bun", "bin"));
|
|
120
|
+
// 4. Package-manager prefixes.
|
|
121
|
+
if (platform !== "win32") {
|
|
122
|
+
push("/opt/homebrew/bin");
|
|
123
|
+
push("/usr/local/bin");
|
|
124
|
+
}
|
|
125
|
+
// 5. Every nvm-managed node version's bin dir. Load-bearing: `npm i -g bun`
|
|
126
|
+
// lands bun in exactly ONE version's bin, so `nvm use <another>` makes it
|
|
127
|
+
// vanish from PATH entirely while it's still perfectly usable on disk.
|
|
128
|
+
// Descending so the newest version wins deterministically.
|
|
129
|
+
const nvmRoot = join(home, ".nvm", "versions", "node");
|
|
130
|
+
for (const v of readdir(nvmRoot).sort().reverse()) push(join(nvmRoot, v, "bin"));
|
|
131
|
+
// 6. Next to the running node. For our own `node scripts/dev-hook.mjs` form
|
|
132
|
+
// this is a strict subset of (1) — node was resolved from PATH — so it is
|
|
133
|
+
// a no-op. Kept only for a caller that spawns node by absolute path.
|
|
134
|
+
push(dirname(execPath));
|
|
135
|
+
|
|
136
|
+
// Cache from a previous npm-install (see installBun). Last, so a real bun
|
|
137
|
+
// that reappears on PATH always wins over our vendored copy.
|
|
138
|
+
push(join(CACHE_DIR, "node_modules", ".bin"));
|
|
139
|
+
|
|
140
|
+
return probe(dirs, exeNames("bun", platform), exists);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Absolute path to npm, or null. PATH only — npm ships beside node. */
|
|
144
|
+
export function findNpm(deps = {}) {
|
|
145
|
+
const env = deps.env ?? process.env;
|
|
146
|
+
const platform = deps.platform ?? process.platform;
|
|
147
|
+
const exists = deps.exists ?? existsSync;
|
|
148
|
+
const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
149
|
+
return probe(dirs, exeNames("npm", platform), exists);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `{ cmd, args }` to run the real binary, or null when bun is unfindable.
|
|
154
|
+
* Shared with `.opencode/plugins/failproofai.mjs`, which spawns bun itself.
|
|
155
|
+
* @param {Deps} deps
|
|
156
|
+
*/
|
|
157
|
+
export function resolveDevSpawn(deps = {}) {
|
|
158
|
+
const bun = findBun(deps);
|
|
159
|
+
return bun ? { cmd: bun, args: [SRC_BIN] } : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** One actionable line. Keep it to one — this can fire on 26 events. */
|
|
163
|
+
export function bunMissingMessage() {
|
|
164
|
+
return (
|
|
165
|
+
"bun not found and npm unavailable to install it — this repo's dogfood hooks are NOT " +
|
|
166
|
+
"enforcing. Install bun: curl -fsSL https://bun.sh/install | bash"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Run `doWork` at most once across concurrently-firing hooks. `mkdirSync` is
|
|
172
|
+
* atomic, so it doubles as the lock: EEXIST means another process got there.
|
|
173
|
+
* Losers wait for the artifact rather than duplicating the work.
|
|
174
|
+
*/
|
|
175
|
+
function ensureOnce(isDone, doWork) {
|
|
176
|
+
if (isDone()) return true;
|
|
177
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < LOCK_WAIT_TRIES; i++) {
|
|
180
|
+
if (isDone()) return true;
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(LOCK_DIR);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
if (err?.code !== "EEXIST") throw err;
|
|
185
|
+
try {
|
|
186
|
+
if (Date.now() - statSync(LOCK_DIR).mtimeMs > LOCK_STALE_MS) {
|
|
187
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
188
|
+
} else {
|
|
189
|
+
sleepSync(LOCK_WAIT_MS);
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
/* lock vanished under us — retry immediately */
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
doWork();
|
|
198
|
+
} finally {
|
|
199
|
+
try {
|
|
200
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
201
|
+
} catch {
|
|
202
|
+
/* best effort; a leaked lock goes stale in LOCK_STALE_MS */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return isDone();
|
|
206
|
+
}
|
|
207
|
+
return isDone();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Vendor bun into node_modules/.cache via npm. bun ships as an npm package, so
|
|
212
|
+
* npm is the one bootstrap that works when bun is genuinely absent — building
|
|
213
|
+
* `dist/cli.mjs` to run under node can't, because `build:cli` is itself
|
|
214
|
+
* `bun build`.
|
|
215
|
+
*/
|
|
216
|
+
function installBun(deps = {}) {
|
|
217
|
+
const npm = findNpm(deps);
|
|
218
|
+
if (!npm) return null;
|
|
219
|
+
ensureOnce(
|
|
220
|
+
() => findBun(deps) !== null,
|
|
221
|
+
() => {
|
|
222
|
+
warn("bun not found — installing it into node_modules/.cache (one time, ~15s)");
|
|
223
|
+
const r = spawnSync(
|
|
224
|
+
npm,
|
|
225
|
+
["install", "bun", "--prefix", CACHE_DIR, "--no-save", "--no-audit", "--no-fund", "--loglevel=error"],
|
|
226
|
+
// NOT "inherit" on stdout: npm prints there, and our stdout is a deny
|
|
227
|
+
// channel. stderr passes through so a failed install is visible.
|
|
228
|
+
{ stdio: ["ignore", "ignore", "inherit"], cwd: REPO_ROOT },
|
|
229
|
+
);
|
|
230
|
+
if (r.status !== 0) warn("npm install bun failed — see the error above");
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
return findBun(deps);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Build dist/index.js if absent, so `.failproofai/policies/*.mjs` can resolve
|
|
238
|
+
* `import ... from 'failproofai'`. Non-fatal: builtins enforce without it.
|
|
239
|
+
*/
|
|
240
|
+
export function ensureBundle(bunPath) {
|
|
241
|
+
return ensureOnce(
|
|
242
|
+
() => existsSync(BUNDLE),
|
|
243
|
+
() => {
|
|
244
|
+
warn("dist/index.js missing — building it so .failproofai/policies/ can load (one time)");
|
|
245
|
+
const r = spawnSync(
|
|
246
|
+
bunPath,
|
|
247
|
+
["build", "--target=node", "--format=cjs", "--outfile=dist/index.js", "src/index.ts"],
|
|
248
|
+
{ stdio: ["ignore", "ignore", "inherit"], cwd: REPO_ROOT },
|
|
249
|
+
);
|
|
250
|
+
if (r.status !== 0) warn("build failed — policies in .failproofai/policies/ will NOT load");
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function main() {
|
|
256
|
+
const bun = findBun() ?? installBun();
|
|
257
|
+
if (!bun) {
|
|
258
|
+
warn(bunMissingMessage());
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
ensureBundle(bun);
|
|
262
|
+
|
|
263
|
+
// stdio:"inherit" hands the child our actual fds: the payload on stdin and
|
|
264
|
+
// any decision JSON on stdout flow byte-exact, unbuffered, unparsed by us.
|
|
265
|
+
// No cwd override — the child must see whatever cwd the agent CLI chose,
|
|
266
|
+
// exactly as it did when the config spawned bun directly.
|
|
267
|
+
const r = spawnSync(bun, [SRC_BIN, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
268
|
+
|
|
269
|
+
// Exit codes are a contract, not a status: 2 means DENY on Claude/Factory/
|
|
270
|
+
// Goose. Propagate verbatim. Never synthesize 2 (would deny) or 0 (would
|
|
271
|
+
// silently allow).
|
|
272
|
+
if (r.error) {
|
|
273
|
+
warn(`could not run bun (${r.error.code ?? r.error.message}) — hooks are NOT enforcing`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
if (r.status === null && r.signal) {
|
|
277
|
+
// Mirror what `sh` reports for a signalled child, which is what the agent
|
|
278
|
+
// CLI saw before this launcher sat in between.
|
|
279
|
+
process.exit(128 + (constants.signals[r.signal] ?? 15));
|
|
280
|
+
}
|
|
281
|
+
process.exit(r.status ?? 1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Only run when executed, not when `.opencode/plugins/failproofai.mjs` imports
|
|
285
|
+
// the resolver. realpath so a symlinked or relative argv[1] still matches.
|
|
286
|
+
let invokedDirectly = false;
|
|
287
|
+
try {
|
|
288
|
+
invokedDirectly = realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
289
|
+
} catch {
|
|
290
|
+
invokedDirectly = false;
|
|
291
|
+
}
|
|
292
|
+
if (invokedDirectly) main();
|
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
* a `bun install -g failproofai` whose prefix sorts ahead of npm's on PATH.
|
|
5
5
|
*
|
|
6
6
|
* Used by:
|
|
7
|
-
* - scripts/
|
|
8
|
-
* the
|
|
9
|
-
*
|
|
10
|
-
* produce a shadow-shaped error if the cause is a shadow rather than a
|
|
11
|
-
* genuinely broken build.
|
|
7
|
+
* - scripts/launch.ts — when .next/standalone/server.js is missing, produce a
|
|
8
|
+
* shadow-shaped error if the cause is a shadow rather than a genuinely
|
|
9
|
+
* broken build.
|
|
12
10
|
*
|
|
13
11
|
* Pure Node.js built-ins, no external dependencies. Every probe is wrapped in
|
|
14
12
|
* try/catch — diagnoseShadow() is guaranteed not to throw.
|
|
@@ -125,9 +123,8 @@ function buildRecommendation(pathFirstBin) {
|
|
|
125
123
|
*
|
|
126
124
|
* @param {{ selfPackageRoot: string, selfVersion: string | null }} self
|
|
127
125
|
* The package root and version of the binary calling diagnoseShadow().
|
|
128
|
-
* Callers (
|
|
129
|
-
*
|
|
130
|
-
* import.meta.url assumptions.
|
|
126
|
+
* Callers (scripts/launch.ts) already have these values; passing them in keeps
|
|
127
|
+
* the helper deterministic and free of import.meta.url assumptions.
|
|
131
128
|
*/
|
|
132
129
|
export function diagnoseShadow(self) {
|
|
133
130
|
const selfPackageRoot = (() => {
|
|
@@ -80,12 +80,20 @@ function getInstanceId() {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Track a named event to PostHog. No-op when telemetry is disabled.
|
|
83
|
-
*
|
|
83
|
+
*
|
|
84
|
+
* `opts.version` must be supplied by callers outside an npm lifecycle script:
|
|
85
|
+
* npm only sets npm_package_version while running one, and these events now
|
|
86
|
+
* fire from the CLI (see lib/install-check.ts) because package managers block
|
|
87
|
+
* install scripts by default.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} event
|
|
90
|
+
* @param {Record<string, unknown>} [properties]
|
|
91
|
+
* @param {{ version?: string, timeoutMs?: number }} [opts]
|
|
84
92
|
*/
|
|
85
|
-
export async function trackInstallEvent(event, properties = {}) {
|
|
93
|
+
export async function trackInstallEvent(event, properties = {}, opts = {}) {
|
|
86
94
|
if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
|
|
87
95
|
|
|
88
|
-
const version = process.env.npm_package_version ?? "unknown";
|
|
96
|
+
const version = opts.version ?? process.env.npm_package_version ?? "unknown";
|
|
89
97
|
const body = JSON.stringify({
|
|
90
98
|
api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
|
|
91
99
|
event,
|
|
@@ -106,7 +114,7 @@ export async function trackInstallEvent(event, properties = {}) {
|
|
|
106
114
|
method: "POST",
|
|
107
115
|
headers: { "Content-Type": "application/json" },
|
|
108
116
|
body,
|
|
109
|
-
signal: AbortSignal.timeout(5000),
|
|
117
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5000),
|
|
110
118
|
}
|
|
111
119
|
);
|
|
112
120
|
}
|