failproofai 0.0.9 → 0.0.10-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/.cursor/hooks.json +47 -0
- package/.next/standalone/.gemini/settings.json +147 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +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 +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- 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 +11 -11
- 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/download/[project]/[session]/route.js +2 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +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/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- 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/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 +5 -5
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- 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]__0.~nmr9._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0_b7pgn._.js → [root-of-the-server]__0lkkjl_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__0mb9b9d._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
- package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- 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 +9 -9
- package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 0-wd3kiz5wrsz.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0222q~_4u7p6h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 02y~6tp1j1wkh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 09qdljea8j.3~.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0pt38lwlsaxvs.js} +1 -1
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 0vl201wjmz17m.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 0vl~p17i-4qt2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0xkzmsj-sniqz.js +1 -0
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- package/.next/standalone/.opencode/opencode.json +4 -0
- package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
- package/.next/standalone/.pi/settings.json +5 -0
- package/.next/standalone/app/components/cli-badge.tsx +7 -11
- package/.next/standalone/app/components/project-list.tsx +32 -4
- package/.next/standalone/app/policies/hooks-client.tsx +31 -15
- package/.next/standalone/app/project/[name]/page.tsx +52 -16
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
- package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
- package/.next/standalone/assets/logos/copilot-light.svg +1 -0
- package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
- package/.next/standalone/assets/logos/cursor-light.svg +1 -0
- package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
- package/.next/standalone/assets/logos/gemini-light.svg +13 -0
- package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
- package/.next/standalone/assets/logos/opencode-light.svg +1 -0
- package/.next/standalone/assets/logos/pi-dark.svg +7 -0
- package/.next/standalone/assets/logos/pi-light.svg +7 -0
- package/.next/standalone/lib/cli-registry.ts +107 -0
- package/.next/standalone/lib/codex-projects.ts +3 -3
- package/.next/standalone/lib/copilot-projects.ts +224 -0
- package/.next/standalone/lib/copilot-sessions.ts +395 -0
- package/.next/standalone/lib/cursor-projects.ts +312 -0
- package/.next/standalone/lib/cursor-sessions.ts +467 -0
- package/.next/standalone/lib/gemini-projects.ts +203 -0
- package/.next/standalone/lib/gemini-sessions.ts +365 -0
- package/.next/standalone/lib/opencode-projects.ts +232 -0
- package/.next/standalone/lib/opencode-sessions.ts +237 -0
- package/.next/standalone/lib/pi-projects.ts +230 -0
- package/.next/standalone/lib/pi-sessions.ts +325 -0
- package/.next/standalone/lib/projects.ts +67 -31
- package/.next/standalone/next.config.ts +5 -4
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/pi-extension/index.ts +373 -0
- package/.next/standalone/pi-extension/package.json +12 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +37 -3
- package/bin/failproofai.mjs +61 -21
- package/dist/cli.mjs +2405 -253
- package/lib/cli-registry.ts +107 -0
- package/lib/codex-projects.ts +3 -3
- package/lib/copilot-projects.ts +224 -0
- package/lib/copilot-sessions.ts +395 -0
- package/lib/cursor-projects.ts +312 -0
- package/lib/cursor-sessions.ts +467 -0
- package/lib/gemini-projects.ts +203 -0
- package/lib/gemini-sessions.ts +365 -0
- package/lib/opencode-projects.ts +232 -0
- package/lib/opencode-sessions.ts +237 -0
- package/lib/pi-projects.ts +230 -0
- package/lib/pi-sessions.ts +325 -0
- package/lib/projects.ts +67 -31
- package/package.json +2 -1
- package/pi-extension/index.ts +373 -0
- package/pi-extension/package.json +12 -0
- package/scripts/install-diagnosis.mjs +190 -0
- package/scripts/launch.ts +32 -0
- package/scripts/postinstall.mjs +25 -0
- package/scripts/translate-docs/mdx-translator.ts +56 -2
- package/scripts/translate-docs/translator.ts +1 -1
- package/src/hooks/builtin-policies.ts +84 -14
- package/src/hooks/handler.ts +67 -5
- package/src/hooks/install-prompt.ts +33 -10
- package/src/hooks/integrations.ts +1007 -6
- package/src/hooks/policy-evaluator.ts +299 -3
- package/src/hooks/resolve-permission-mode.ts +23 -0
- package/src/hooks/types.ts +307 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
- package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/095l4hc7-h.~~.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_ssgManifest.js +0 -0
|
@@ -14,6 +14,59 @@ import type { TranslationResult, TranslationCache } from "./types";
|
|
|
14
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const DOCS_DIR = join(__dirname, "..", "..", "docs");
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Strip stray ASCII `"` that appear right after a JSX attribute's closing
|
|
19
|
+
* quote — e.g. `<Tab title="Tab „Richtlinien"">`. The translator sometimes
|
|
20
|
+
* wraps an inner phrase in language-specific typographic quotes (`„…"`,
|
|
21
|
+
* `「…」`, etc.) but uses an ASCII `"` for the closing instead of the
|
|
22
|
+
* proper U+201D, which terminates the attribute and leaves the real
|
|
23
|
+
* closing `"` as a stray character that breaks `mintlify validate`.
|
|
24
|
+
*
|
|
25
|
+
* Also drops unmatched typographic opening quotes inside the same attribute
|
|
26
|
+
* value so the rendered title doesn't end with a dangling `„` after we strip
|
|
27
|
+
* the extras.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeJsxAttributes(content: string): string {
|
|
30
|
+
// Each pair must use an OPENER that is unambiguously an opener — i.e. the
|
|
31
|
+
// codepoint never serves as a CLOSER of a different pair. That's why we
|
|
32
|
+
// skip English curly “…” (U+201C/U+201D): U+201C is also the German
|
|
33
|
+
// closer, so processing English curly after German would strip the very
|
|
34
|
+
// German closer we just preserved.
|
|
35
|
+
const openings: Array<[string, string]> = [
|
|
36
|
+
["„", "“"], // German „ … "
|
|
37
|
+
["«", "»"], // French « … »
|
|
38
|
+
["‹", "›"], // French single ‹ … ›
|
|
39
|
+
["「", "」"], // Japanese 「 … 」
|
|
40
|
+
["『", "』"], // Japanese 『 … 』
|
|
41
|
+
];
|
|
42
|
+
return content.replace(
|
|
43
|
+
/([a-zA-Z_-]+=")([^"\n]*)"+(?=\s|\/|>)/g,
|
|
44
|
+
(match, prefix: string, value: string) => {
|
|
45
|
+
// If the original had exactly one closing " (i.e. no extras),
|
|
46
|
+
// leave it alone — the regex's `"+` would still match a single
|
|
47
|
+
// quote, so we need to re-check the match length to be safe.
|
|
48
|
+
const expectedMinLen = `${prefix}${value}"`.length;
|
|
49
|
+
if (match.length === expectedMinLen) return match;
|
|
50
|
+
let cleaned = value;
|
|
51
|
+
for (const [open, close] of openings) {
|
|
52
|
+
const opens = cleaned.split(open).length - 1;
|
|
53
|
+
const closes = cleaned.split(close).length - 1;
|
|
54
|
+
// Drop only the surplus unmatched openers, removing from the right.
|
|
55
|
+
// A value like `„Foo“ und „Bar` (one matched pair plus one stray
|
|
56
|
+
// opener) keeps the leading `„Foo“` intact and only the dangling
|
|
57
|
+
// `„Bar` opener gets stripped.
|
|
58
|
+
let surplus = opens - closes;
|
|
59
|
+
while (surplus-- > 0) {
|
|
60
|
+
const i = cleaned.lastIndexOf(open);
|
|
61
|
+
if (i < 0) break;
|
|
62
|
+
cleaned = cleaned.slice(0, i) + cleaned.slice(i + open.length);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return `${prefix}${cleaned}"`;
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
17
70
|
/**
|
|
18
71
|
* Rewrite internal doc links to include the language prefix.
|
|
19
72
|
* e.g. href="/built-in-policies" -> href="/es/built-in-policies"
|
|
@@ -94,8 +147,9 @@ export async function translateMdxPage(
|
|
|
94
147
|
options.model,
|
|
95
148
|
);
|
|
96
149
|
|
|
97
|
-
//
|
|
98
|
-
const
|
|
150
|
+
// Strip stray quote artifacts from JSX attribute values, then rewrite links
|
|
151
|
+
const sanitized = sanitizeJsxAttributes(translated);
|
|
152
|
+
const withLinks = rewriteInternalLinks(sanitized, lang);
|
|
99
153
|
|
|
100
154
|
// Write output
|
|
101
155
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
@@ -15,7 +15,7 @@ const SYSTEM_PROMPT = `You are a professional technical documentation translator
|
|
|
15
15
|
## Rules
|
|
16
16
|
|
|
17
17
|
1. **Preserve all code blocks exactly as-is** — never translate content inside backtick-fenced code blocks (\`\`\`...\`\`\`) or inline code (\`...\`).
|
|
18
|
-
2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags.
|
|
18
|
+
2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags. **Never put an ASCII straight \`"\` inside a \`title="…"\` (or any JSX attribute value)** — it terminates the attribute and breaks MDX parsing. If the target language would normally wrap a word in quotation marks (e.g. German „…", Japanese 「…」), drop the inner quotes inside attribute values and rely on the surrounding tag for emphasis.
|
|
19
19
|
3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged.
|
|
20
20
|
4. **Preserve all URLs and paths** — never modify href values, image paths, or links.
|
|
21
21
|
5. **Preserve Markdown structure** — headers (#, ##), lists (-, *), tables (|), bold (**), italic (*), links ([text](url)) must keep their Markdown formatting.
|
|
@@ -12,27 +12,71 @@ import { hookLogWarn } from "./hook-logger";
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Whether `resolved` lives under an agent CLI's home directory
|
|
15
|
-
* (~/.claude
|
|
16
|
-
*
|
|
15
|
+
* (~/.claude/, ~/.codex/, ~/.copilot/, ~/.cursor/, ~/.pi/, ~/.gemini/, or any
|
|
16
|
+
* of OpenCode's three home-side dirs). Used to whitelist agent self-reads of
|
|
17
|
+
* their own config and transcripts.
|
|
18
|
+
*
|
|
19
|
+
* OpenCode splits its data across three locations (verified live on
|
|
20
|
+
* opencode v1.14.33 via `opencode debug paths`):
|
|
21
|
+
* • ~/.config/opencode/ — config + plugins
|
|
22
|
+
* • ~/.local/share/opencode/ — sessions, snapshots, opencode.db (SQLite)
|
|
23
|
+
* • ~/.opencode/ — legacy fallback path
|
|
17
24
|
*/
|
|
18
25
|
function isAgentInternalPath(resolved: string): boolean {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
// Normalize backslashes to forward slashes so the same `startsWith` check
|
|
27
|
+
// works on Windows. `resolve()` returns forward slashes on POSIX but
|
|
28
|
+
// backslashes on Windows; `join(homedir(), ...)` follows the same OS
|
|
29
|
+
// convention. Comparing both sides under a single forward-slash form
|
|
30
|
+
// avoids per-OS branching.
|
|
31
|
+
const normResolved = resolved.replaceAll("\\", "/");
|
|
32
|
+
for (const dir of [".claude", ".codex", ".copilot", ".cursor", ".opencode", ".pi", ".gemini"]) {
|
|
33
|
+
const root = join(homedir(), dir).replaceAll("\\", "/");
|
|
34
|
+
if (normResolved === root || normResolved.startsWith(root + "/")) return true;
|
|
35
|
+
}
|
|
36
|
+
for (const sub of [join(".config", "opencode"), join(".local", "share", "opencode")]) {
|
|
37
|
+
const root = join(homedir(), sub).replaceAll("\\", "/");
|
|
38
|
+
if (normResolved === root || normResolved.startsWith(root + "/")) return true;
|
|
22
39
|
}
|
|
23
40
|
return false;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
/**
|
|
27
44
|
* Whether `resolved` is a settings/hooks file for an agent CLI:
|
|
28
|
-
* • Claude Code:
|
|
29
|
-
* • Codex:
|
|
45
|
+
* • Claude Code: `.claude/settings.json`, `.claude/settings.local.json`, etc.
|
|
46
|
+
* • Codex: `.codex/hooks.json`
|
|
47
|
+
* • Copilot CLI: `.copilot/hooks/*.json`, `.github/hooks/*.json`
|
|
48
|
+
* • Cursor Agent: `.cursor/hooks.json`
|
|
49
|
+
* • OpenCode: `.opencode/opencode.{json,jsonc}`,
|
|
50
|
+
* `.opencode/plugins/*.{mjs,js,ts}`,
|
|
51
|
+
* `~/.config/opencode/{opencode.json,opencode.jsonc,config.json}`,
|
|
52
|
+
* `~/.config/opencode/plugins/*.{mjs,js,ts}`
|
|
53
|
+
* • Pi: `.pi/settings.json` (project) and `.pi/agent/settings.json`
|
|
54
|
+
* (user); also the Pi-managed extension dir
|
|
55
|
+
* `.pi/extensions/` / `.pi/agent/extensions/`.
|
|
56
|
+
* • Gemini CLI: `.gemini/settings.json` (both project and user scope —
|
|
57
|
+
* user is `~/.gemini/settings.json`); also the Gemini-managed
|
|
58
|
+
* hooks scripts dir `.gemini/hooks/`.
|
|
30
59
|
* These must NEVER be edited by the agent itself — that would let it disable
|
|
31
60
|
* its own protections.
|
|
32
61
|
*/
|
|
33
62
|
function isAgentSettingsFile(resolved: string): boolean {
|
|
34
63
|
if (/[\\/]\.claude[\\/]settings(?:\.[^/\\]+)?\.json$/.test(resolved)) return true;
|
|
35
64
|
if (/[\\/]\.codex[\\/]hooks\.json$/.test(resolved)) return true;
|
|
65
|
+
if (/[\\/]\.copilot[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved)) return true;
|
|
66
|
+
if (/[\\/]\.github[\\/]hooks[\\/][^/\\]+\.json$/.test(resolved)) return true;
|
|
67
|
+
if (/[\\/]\.cursor[\\/]hooks\.json$/.test(resolved)) return true;
|
|
68
|
+
// OpenCode: project config + plugins, user config + plugins, legacy config.
|
|
69
|
+
if (/[\\/]\.opencode[\\/]opencode\.jsonc?$/.test(resolved)) return true;
|
|
70
|
+
if (/[\\/]\.opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved)) return true;
|
|
71
|
+
if (/[\\/]\.config[\\/]opencode[\\/]opencode\.jsonc?$/.test(resolved)) return true;
|
|
72
|
+
if (/[\\/]\.config[\\/]opencode[\\/]config\.json$/.test(resolved)) return true;
|
|
73
|
+
if (/[\\/]\.config[\\/]opencode[\\/]plugins[\\/][^/\\]+\.(?:mjs|js|ts)$/.test(resolved)) return true;
|
|
74
|
+
// Pi: settings + extensions dirs (project and user-scope variants).
|
|
75
|
+
if (/[\\/]\.pi[\\/](?:agent[\\/])?settings\.json$/.test(resolved)) return true;
|
|
76
|
+
if (/[\\/]\.pi[\\/](?:agent[\\/])?extensions[\\/]/.test(resolved)) return true;
|
|
77
|
+
// Gemini: settings.json + hooks dir referenced by `command: $GEMINI_PROJECT_DIR/.gemini/hooks/...`.
|
|
78
|
+
if (/[\\/]\.gemini[\\/]settings\.json$/.test(resolved)) return true;
|
|
79
|
+
if (/[\\/]\.gemini[\\/]hooks[\\/]/.test(resolved)) return true;
|
|
36
80
|
return false;
|
|
37
81
|
}
|
|
38
82
|
|
|
@@ -735,7 +779,7 @@ function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
|
|
|
735
779
|
for (const p of paths) {
|
|
736
780
|
const resolved = resolve(cwd, p);
|
|
737
781
|
if (isClaudeSettingsFile(resolved)) {
|
|
738
|
-
return deny(`Reading
|
|
782
|
+
return deny(`Reading agent settings file blocked: ${resolved}`);
|
|
739
783
|
}
|
|
740
784
|
if (isClaudeInternalPath(resolved)) continue; // Whitelist ~/.claude/
|
|
741
785
|
if (resolved === "/dev/null") continue; // Harmless special file
|
|
@@ -758,7 +802,7 @@ function blockReadOutsideCwd(ctx: PolicyContext): PolicyResult {
|
|
|
758
802
|
|
|
759
803
|
// Block settings files in any .claude directory before whitelisting
|
|
760
804
|
if (isClaudeSettingsFile(resolved)) {
|
|
761
|
-
return deny(`Reading
|
|
805
|
+
return deny(`Reading agent settings file blocked: ${resolved}`);
|
|
762
806
|
}
|
|
763
807
|
|
|
764
808
|
// Whitelist ~/.claude/ — Claude Code's own config, plans, memory, and settings
|
|
@@ -1365,17 +1409,44 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1365
1409
|
const branch = getCurrentBranch(cwd);
|
|
1366
1410
|
if (!branch || branch === "HEAD") return allow("Detached HEAD, skipping CI check.");
|
|
1367
1411
|
|
|
1368
|
-
//
|
|
1412
|
+
// Resolve HEAD up front — the workflow-runs filter below uses it to
|
|
1413
|
+
// ignore runs targeting prior commits on the same branch (otherwise a
|
|
1414
|
+
// stale failure on commit X is still reported after the fix on Y lands).
|
|
1415
|
+
// Third-party checks and commit statuses (queried by SHA below) already
|
|
1416
|
+
// scope to HEAD via getThirdPartyCheckRuns / getCommitStatuses.
|
|
1417
|
+
const sha = getHeadSha(cwd);
|
|
1418
|
+
|
|
1419
|
+
// 1. GitHub Actions workflow runs (filtered to current HEAD, deduped by name)
|
|
1369
1420
|
let workflowRuns: CiCheck[] = [];
|
|
1370
1421
|
try {
|
|
1422
|
+
// --limit 20 (was 5): a busy branch can push the latest run for some
|
|
1423
|
+
// workflow out of the top-5 window after the SHA filter. 20 covers
|
|
1424
|
+
// ~4 commits worth of runs for a 5-workflow repo without being slow.
|
|
1371
1425
|
const runsJson = execFileSync(
|
|
1372
1426
|
"gh",
|
|
1373
|
-
["run", "list", "--branch", branch, "--limit", "
|
|
1427
|
+
["run", "list", "--branch", branch, "--limit", "20", "--json", "status,conclusion,name,headSha"],
|
|
1374
1428
|
{ cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 },
|
|
1375
1429
|
).trim();
|
|
1376
1430
|
|
|
1377
1431
|
if (runsJson && runsJson !== "[]") {
|
|
1378
|
-
|
|
1432
|
+
const allWorkflowRuns = JSON.parse(runsJson) as Array<CiCheck & { headSha?: string }>;
|
|
1433
|
+
// Filter to runs targeting the current HEAD commit only — not
|
|
1434
|
+
// historical runs for prior commits on the same branch. When `sha`
|
|
1435
|
+
// is unavailable (e.g. brand-new repo with no commits) fall back
|
|
1436
|
+
// to the unfiltered list so the policy still has something to act on.
|
|
1437
|
+
const headRuns = sha
|
|
1438
|
+
? allWorkflowRuns.filter((r) => r.headSha === sha)
|
|
1439
|
+
: allWorkflowRuns;
|
|
1440
|
+
// Dedupe by workflow name, keeping the first occurrence (gh run list
|
|
1441
|
+
// returns newest-first). This handles GitHub's "Re-run all jobs" which
|
|
1442
|
+
// creates a fresh run record with the same name + headSha — without
|
|
1443
|
+
// dedupe the older failed record would still trip the deny.
|
|
1444
|
+
const seen = new Set<string>();
|
|
1445
|
+
workflowRuns = headRuns.filter((r) => {
|
|
1446
|
+
if (seen.has(r.name)) return false;
|
|
1447
|
+
seen.add(r.name);
|
|
1448
|
+
return true;
|
|
1449
|
+
});
|
|
1379
1450
|
}
|
|
1380
1451
|
} catch {
|
|
1381
1452
|
// fail-open for workflow runs; continue to check third-party checks
|
|
@@ -1384,7 +1455,6 @@ function requireCiGreenBeforeStop(ctx: PolicyContext): PolicyResult {
|
|
|
1384
1455
|
// 2. Third-party check runs (CodeRabbit, SonarCloud, Codecov, etc.)
|
|
1385
1456
|
let thirdPartyChecks: CiCheck[] = [];
|
|
1386
1457
|
let commitStatuses: CiCheck[] = [];
|
|
1387
|
-
const sha = getHeadSha(cwd);
|
|
1388
1458
|
if (sha) {
|
|
1389
1459
|
thirdPartyChecks = getThirdPartyCheckRuns(cwd, sha);
|
|
1390
1460
|
commitStatuses = getCommitStatuses(cwd, sha);
|
|
@@ -1879,7 +1949,7 @@ export const BUILTIN_POLICIES: BuiltinPolicyDefinition[] = [
|
|
|
1879
1949
|
},
|
|
1880
1950
|
{
|
|
1881
1951
|
name: "require-ci-green-before-stop",
|
|
1882
|
-
description: "Require CI checks to pass on the current
|
|
1952
|
+
description: "Require CI checks to pass on the current HEAD commit before Claude stops (ignores stale runs on prior commits)",
|
|
1883
1953
|
fn: requireCiGreenBeforeStop,
|
|
1884
1954
|
match: { events: ["Stop"] },
|
|
1885
1955
|
defaultEnabled: false,
|
package/src/hooks/handler.ts
CHANGED
|
@@ -5,8 +5,16 @@
|
|
|
5
5
|
* ~/.failproofai/policies-config.json, evaluates matching policies, persists
|
|
6
6
|
* activity to disk, and returns the appropriate exit code + stdout response.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
|
|
8
|
+
import type {
|
|
9
|
+
HookEventType,
|
|
10
|
+
IntegrationType,
|
|
11
|
+
SessionMetadata,
|
|
12
|
+
CodexHookEventType,
|
|
13
|
+
CursorHookEventType,
|
|
14
|
+
PiHookEventType,
|
|
15
|
+
GeminiHookEventType,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { CODEX_EVENT_MAP, CURSOR_EVENT_MAP, PI_EVENT_MAP, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP } from "./types";
|
|
10
18
|
import type { PolicyFunction, PolicyResult } from "./policy-types";
|
|
11
19
|
import { readMergedHooksConfig } from "./hooks-config";
|
|
12
20
|
import { registerBuiltinPolicies } from "./builtin-policies";
|
|
@@ -22,18 +30,56 @@ import { hookLogInfo, hookLogWarn } from "./hook-logger";
|
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Canonicalize an event name to PascalCase. Codex sends snake_case event names
|
|
25
|
-
* on stdin and as the --hook arg;
|
|
26
|
-
*
|
|
33
|
+
* on stdin and as the --hook arg; Cursor sends camelCase (`preToolUse`,
|
|
34
|
+
* `beforeSubmitPrompt`); Pi sends underscore_lower_snake_case (`tool_call`,
|
|
35
|
+
* `session_start`); Claude Code sends PascalCase. Copilot CLI is installed
|
|
36
|
+
* in "VS Code compatible" PascalCase mode (see integrations.ts), so its events
|
|
37
|
+
* arrive PascalCase already. Gemini also sends PascalCase but with different
|
|
38
|
+
* names (`BeforeTool`, `BeforeAgent`, `AfterAgent`); we map via GEMINI_EVENT_MAP.
|
|
39
|
+
* The internal registry, builtin policies, and policy.match.events all key on
|
|
40
|
+
* PascalCase.
|
|
27
41
|
*/
|
|
28
42
|
function canonicalizeEventType(raw: string, cli: IntegrationType): HookEventType {
|
|
29
43
|
if (cli === "codex") {
|
|
30
44
|
const mapped = CODEX_EVENT_MAP[raw as CodexHookEventType];
|
|
31
45
|
if (mapped) return mapped;
|
|
32
46
|
}
|
|
33
|
-
|
|
47
|
+
if (cli === "cursor") {
|
|
48
|
+
const mapped = CURSOR_EVENT_MAP[raw as CursorHookEventType];
|
|
49
|
+
if (mapped) return mapped;
|
|
50
|
+
}
|
|
51
|
+
if (cli === "pi") {
|
|
52
|
+
const mapped = PI_EVENT_MAP[raw as PiHookEventType];
|
|
53
|
+
if (mapped) return mapped;
|
|
54
|
+
}
|
|
55
|
+
if (cli === "gemini") {
|
|
56
|
+
const mapped = GEMINI_EVENT_MAP[raw as GeminiHookEventType];
|
|
57
|
+
if (mapped) return mapped;
|
|
58
|
+
}
|
|
59
|
+
// claude / copilot / unknown — already PascalCase, pass through.
|
|
60
|
+
// HOOK_EVENT_TYPES type-checks downstream.
|
|
34
61
|
return raw as HookEventType;
|
|
35
62
|
}
|
|
36
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Canonicalize a per-CLI tool name to the Claude PascalCase form that builtin
|
|
66
|
+
* policies match on (e.g. `Bash`, `Read`, `Write`, `Edit`). Today only Gemini
|
|
67
|
+
* needs this — its tools are snake_case (`run_shell_command`, `read_file`,
|
|
68
|
+
* `write_file`, `replace`, …). Other CLIs pass through unchanged: Claude /
|
|
69
|
+
* Codex / Copilot already use PascalCase, and Cursor / Pi pre-canonicalize
|
|
70
|
+
* inside their own plugin shims before the payload reaches this binary.
|
|
71
|
+
*
|
|
72
|
+
* Unknown tool names (MCP `mcp_*`, third-party extensions, Skills) pass
|
|
73
|
+
* through unchanged so non-builtin tooling isn't lost.
|
|
74
|
+
*/
|
|
75
|
+
function canonicalizeToolName(raw: string | undefined, cli: IntegrationType): string | undefined {
|
|
76
|
+
if (!raw) return raw;
|
|
77
|
+
if (cli === "gemini") {
|
|
78
|
+
return GEMINI_TOOL_MAP[raw] ?? raw;
|
|
79
|
+
}
|
|
80
|
+
return raw;
|
|
81
|
+
}
|
|
82
|
+
|
|
37
83
|
export async function handleHookEvent(
|
|
38
84
|
eventType: string,
|
|
39
85
|
cli: IntegrationType = "claude",
|
|
@@ -79,6 +125,17 @@ export async function handleHookEvent(
|
|
|
79
125
|
// Canonicalize event name (Codex sends snake_case; internals expect PascalCase)
|
|
80
126
|
const canonicalEventType = canonicalizeEventType(eventType, cli);
|
|
81
127
|
|
|
128
|
+
// Canonicalize tool name in place so both the policy-registry tool-name
|
|
129
|
+
// filter and policy bodies (`ctx.toolName === "Bash"`) see the canonical
|
|
130
|
+
// form. Today only Gemini's snake_case names need translation; other CLIs
|
|
131
|
+
// are no-ops here. Mutating `parsed.tool_name` keeps the activity store +
|
|
132
|
+
// telemetry tagging consistent (they read from `parsed.tool_name`).
|
|
133
|
+
const rawToolName = parsed.tool_name as string | undefined;
|
|
134
|
+
const canonicalToolName = canonicalizeToolName(rawToolName, cli);
|
|
135
|
+
if (canonicalToolName !== rawToolName) {
|
|
136
|
+
parsed.tool_name = canonicalToolName;
|
|
137
|
+
}
|
|
138
|
+
|
|
82
139
|
// Extract session metadata from payload
|
|
83
140
|
const sessionId = parsed.session_id as string | undefined;
|
|
84
141
|
const session: SessionMetadata = {
|
|
@@ -87,6 +144,11 @@ export async function handleHookEvent(
|
|
|
87
144
|
cwd: parsed.cwd as string | undefined,
|
|
88
145
|
permissionMode: resolvePermissionMode(cli, parsed, sessionId),
|
|
89
146
|
hookEventName: parsed.hook_event_name as string | undefined,
|
|
147
|
+
// Preserve the raw CLI-side event name (eventType arg) before
|
|
148
|
+
// canonicalization. Response shapes that round-trip the agent-emitted
|
|
149
|
+
// event name (e.g. Gemini's `hookSpecificOutput.hookEventName`) prefer
|
|
150
|
+
// this over the canonicalized form when stdin omits hook_event_name.
|
|
151
|
+
rawHookEventName: eventType,
|
|
90
152
|
cli,
|
|
91
153
|
};
|
|
92
154
|
|
|
@@ -30,8 +30,13 @@ export interface PromptOptions {
|
|
|
30
30
|
includeBeta?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Whether the prompt is being shown for an install or an uninstall flow.
|
|
34
|
+
* Drives heading + hint text so `policies --uninstall` no longer says
|
|
35
|
+
* "Install Hooks". */
|
|
36
|
+
export type CliPromptAction = "install" | "uninstall";
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
|
-
* Resolve which agent CLIs to install hooks for.
|
|
39
|
+
* Resolve which agent CLIs to install/uninstall hooks for.
|
|
35
40
|
*
|
|
36
41
|
* Rules:
|
|
37
42
|
* • If `explicit` is provided (from `--cli`), use it as-is.
|
|
@@ -42,14 +47,26 @@ export interface PromptOptions {
|
|
|
42
47
|
*
|
|
43
48
|
* Returns the selected IntegrationType[] (always non-empty).
|
|
44
49
|
*/
|
|
45
|
-
export async function resolveTargetClis(
|
|
50
|
+
export async function resolveTargetClis(
|
|
51
|
+
explicit?: IntegrationType[],
|
|
52
|
+
action: CliPromptAction = "install",
|
|
53
|
+
): Promise<IntegrationType[]> {
|
|
46
54
|
if (explicit && explicit.length > 0) return [...new Set(explicit)];
|
|
47
55
|
|
|
48
56
|
const detected = detectInstalledClis();
|
|
49
57
|
|
|
50
58
|
if (detected.length === 0) {
|
|
59
|
+
if (action === "uninstall") {
|
|
60
|
+
// Uninstall flow: no agent CLIs detected — nothing to remove from. Default to
|
|
61
|
+
// claude so removeHooks operates over Claude's scopes (no-op if no settings file).
|
|
62
|
+
console.log(
|
|
63
|
+
"\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " +
|
|
64
|
+
"Defaulting to Claude Code; nothing will be removed if no settings file exists.\x1B[0m",
|
|
65
|
+
);
|
|
66
|
+
return ["claude"];
|
|
67
|
+
}
|
|
51
68
|
console.log(
|
|
52
|
-
"\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex). " +
|
|
69
|
+
"\x1B[33mWarning: no agent CLI binary found in PATH (claude, codex, copilot, cursor-agent, opencode, pi, gemini). " +
|
|
53
70
|
"Defaulting to Claude Code; hooks will activate when an agent is installed.\x1B[0m",
|
|
54
71
|
);
|
|
55
72
|
return ["claude"];
|
|
@@ -57,26 +74,29 @@ export async function resolveTargetClis(explicit?: IntegrationType[]): Promise<I
|
|
|
57
74
|
|
|
58
75
|
if (detected.length === 1) {
|
|
59
76
|
const integration = getIntegration(detected[0]);
|
|
60
|
-
|
|
77
|
+
const verb = action === "uninstall" ? "removing hooks from" : "installing hooks for";
|
|
78
|
+
console.log(`Detected ${integration.displayName}; ${verb} it.`);
|
|
61
79
|
return detected;
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
// Multiple detected. Prompt or default.
|
|
65
|
-
if (!process.stdin.isTTY) return detected; // non-interactive: install for all detected
|
|
83
|
+
if (!process.stdin.isTTY) return detected; // non-interactive: install/remove for all detected
|
|
66
84
|
|
|
67
|
-
return promptCliTargetSelection(detected);
|
|
85
|
+
return promptCliTargetSelection(detected, action);
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
/**
|
|
71
|
-
* Interactive arrow-key single-select for "install for which CLI?" when
|
|
89
|
+
* Interactive arrow-key single-select for "install/remove for which CLI?" when
|
|
72
90
|
* multiple agent CLIs are detected. Visual style mirrors promptPolicySelection.
|
|
73
91
|
*/
|
|
74
92
|
async function promptCliTargetSelection(
|
|
75
93
|
detected: IntegrationType[],
|
|
94
|
+
action: CliPromptAction = "install",
|
|
76
95
|
): Promise<IntegrationType[]> {
|
|
77
96
|
const labels = detected.map((id) => getIntegration(id).displayName).join(" + ");
|
|
97
|
+
const allLabel = detected.length > 2 ? "All" : "Both";
|
|
78
98
|
const options: Array<{ label: string; description: string; value: IntegrationType[] }> = [
|
|
79
|
-
{ label:
|
|
99
|
+
{ label: allLabel, description: labels, value: detected },
|
|
80
100
|
...detected.map((id) => ({
|
|
81
101
|
label: `${getIntegration(id).displayName} only`,
|
|
82
102
|
description: "",
|
|
@@ -122,14 +142,17 @@ async function promptCliTargetSelection(
|
|
|
122
142
|
return result;
|
|
123
143
|
}
|
|
124
144
|
|
|
145
|
+
const heading = action === "uninstall" ? "Remove Hooks" : "Install Hooks";
|
|
146
|
+
const verb = action === "uninstall" ? "remove from" : "install";
|
|
147
|
+
|
|
125
148
|
function render(): void {
|
|
126
149
|
const cols = process.stdout.columns || 120;
|
|
127
150
|
hideCursor();
|
|
128
151
|
|
|
129
152
|
const lines: string[] = [];
|
|
130
|
-
lines.push(
|
|
153
|
+
lines.push(` Failproof AI — ${heading}`);
|
|
131
154
|
lines.push("");
|
|
132
|
-
lines.push(` \x1B[2mDetected ${labels}. Choose where to
|
|
155
|
+
lines.push(` \x1B[2mDetected ${labels}. Choose where to ${verb}:\x1B[0m`);
|
|
133
156
|
lines.push("");
|
|
134
157
|
|
|
135
158
|
for (let i = 0; i < options.length; i++) {
|