failproofai 0.0.14-beta.1 → 0.0.14

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 (174) 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.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/index.html +1 -1
  38. package/.next/standalone/.next/server/app/index.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  40. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  42. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  43. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  48. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
  83. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0brjczq._.js → [root-of-the-server]__0zf8xlq._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wp9w6i._.js → [root-of-the-server]__1s2b6h0._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_12rjfea.js → node_modules_html-to-image_es_index_0gkzjaa.js} +1 -1
  110. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  113. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  114. package/.next/standalone/.next/server/pages/404.html +1 -1
  115. package/.next/standalone/.next/server/pages/500.html +1 -1
  116. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  117. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  118. package/.next/standalone/.next/static/chunks/06z_ex1ts3n4s.js +1 -0
  119. package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +1 -0
  120. package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +1 -0
  121. package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +1 -0
  122. package/.next/standalone/.next/static/chunks/{0emg30_6svycu.js → 29o4j32z643_a.js} +1 -1
  123. package/.next/standalone/.next/static/chunks/{1359o5785rmve.js → 2uh_4k4m296z5.js} +2 -2
  124. package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +1 -0
  125. package/.next/standalone/.next/static/chunks/{0d5lypu1zs21r.js → 3s8aaig91937v.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{0o9wpnze1ey8y.js → 42oeva1-w0z2d.js} +1 -1
  128. package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
  129. package/.next/standalone/app/components/copy-button.tsx +22 -6
  130. package/.next/standalone/app/components/log-viewer/entry-row.tsx +8 -3
  131. package/.next/standalone/app/components/raw-log-viewer.tsx +33 -15
  132. package/.next/standalone/app/policies/hooks-client.tsx +1 -1
  133. package/.next/standalone/{ci/cli-integration → integration-suite}/README.md +25 -7
  134. package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
  135. package/.next/standalone/{ci/cli-integration → integration-suite}/run.sh +4 -4
  136. package/.next/standalone/lib/entry-keys.ts +36 -0
  137. package/.next/standalone/lib/format-duration.ts +23 -9
  138. package/.next/standalone/package.json +5 -4
  139. package/.next/standalone/server.js +1 -1
  140. package/dist/cli.mjs +342 -106
  141. package/lib/entry-keys.ts +36 -0
  142. package/lib/format-duration.ts +23 -9
  143. package/package.json +5 -4
  144. package/scripts/translate-docs/cli.ts +6 -0
  145. package/scripts/translate-docs/mdx-translator.ts +30 -19
  146. package/scripts/translate-docs/readme-translator.ts +31 -21
  147. package/scripts/translate-docs/translator.ts +142 -7
  148. package/scripts/translate-docs/types.ts +6 -0
  149. package/scripts/translate-docs/validate-translation.ts +132 -0
  150. package/scripts/validate-mdx.ts +80 -11
  151. package/src/audit/cli.ts +12 -6
  152. package/src/auth/cli.ts +14 -5
  153. package/src/hooks/builtin-policies.ts +168 -50
  154. package/src/hooks/configure-wizard.ts +238 -22
  155. package/src/hooks/custom-hooks-loader.ts +70 -4
  156. package/src/hooks/handler.ts +4 -1
  157. package/src/hooks/policy-types.ts +8 -0
  158. package/src/hooks/tui.ts +95 -28
  159. package/.next/standalone/.next/static/chunks/20tuctw38jbgw.js +0 -1
  160. package/.next/standalone/.next/static/chunks/2ldhnaweu9ew3.js +0 -1
  161. package/.next/standalone/.next/static/chunks/2vav_x0_336ds.js +0 -1
  162. package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +0 -1
  163. package/.next/standalone/.next/static/chunks/3k87p-tijud-4.js +0 -1
  164. package/.next/standalone/.next/static/chunks/3kmaqcp7rldad.js +0 -1
  165. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_buildManifest.js +0 -0
  166. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_clientMiddlewareManifest.js +0 -0
  167. /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → WQRKgSfFekLcC_pvyzp4S}/_ssgManifest.js +0 -0
  168. /package/.next/standalone/{ci/cli-integration → integration-suite}/Dockerfile +0 -0
  169. /package/.next/standalone/{ci/cli-integration → integration-suite}/canary-policies.mjs +0 -0
  170. /package/.next/standalone/{ci/cli-integration → integration-suite}/capture-tokens.sh +0 -0
  171. /package/.next/standalone/{ci/cli-integration → integration-suite}/inject-tokens.sh +0 -0
  172. /package/.next/standalone/{ci/cli-integration → integration-suite}/install-clis.sh +0 -0
  173. /package/.next/standalone/{ci/cli-integration → integration-suite}/probe-cli.sh +0 -0
  174. /package/.next/standalone/{ci/cli-integration → integration-suite}/report.js +0 -0
@@ -2,12 +2,12 @@
2
2
  * Validate that every docs page (`.mdx` and `.md` — Mintlify parses both as
3
3
  * MDX) parses with the same MDX engine Mintlify runs at deploy time.
4
4
  *
5
- * Why this exists: `mintlify validate` (the existing `docs` CI job) only checks
6
- * `docs.json` structure and nav-link resolution it does NOT parse page
7
- * content. A page that is structurally referenced but contains an MDX syntax
8
- * error (e.g. a `<slug>` that escaped its surrounding backticks because a
9
- * translation dropped a closing `` ` ``) passes `mintlify validate` but fails
10
- * the Mintlify deploy with:
5
+ * Why this exists: `mintlify validate` (the existing `docs` CI job) checks
6
+ * `docs.json` structure, nav-link resolution, and frontmatter YAML but it
7
+ * does NOT report MDX *body* syntax errors. A page whose frontmatter is valid
8
+ * but whose body contains an MDX syntax error (e.g. a `<slug>` that escaped its
9
+ * surrounding backticks because a translation dropped a closing `` ` ``) passes
10
+ * `mintlify validate` but fails the Mintlify deploy with:
11
11
  *
12
12
  * Failed to parse page content at path tr/cli/audit.mdx:
13
13
  * Expected a closing tag for `<slug>` (61:127-61:133) before the end of `paragraph`
@@ -21,12 +21,16 @@
21
21
  *
22
22
  * The error string above is emitted by `@mdx-js/mdx`'s micromark MDX layer,
23
23
  * which is the same engine Mintlify uses, so compiling here reproduces the
24
- * deploy-time parse faithfully.
24
+ * deploy-time parse faithfully. `main()` validates the frontmatter too (via
25
+ * `findPageError`), so this net is a strict superset of `mintlify validate`:
26
+ * it catches both the frontmatter YAML class that fails `mintlify validate`
27
+ * and the body-MDX class that `mintlify validate` lets through to deploy.
25
28
  */
26
29
  import { readdirSync, statSync, readFileSync } from "node:fs";
27
30
  import { dirname, join, relative } from "node:path";
28
31
  import { fileURLToPath } from "node:url";
29
32
  import { compile } from "@mdx-js/mdx";
33
+ import YAML from "yaml";
30
34
 
31
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
32
36
  const DOCS_DIR = join(__dirname, "..", "docs");
@@ -37,21 +41,86 @@ export interface MdxParseError {
37
41
  column?: number;
38
42
  }
39
43
 
44
+ /**
45
+ * Shared frontmatter matcher. The capture group is the YAML block *body* (the
46
+ * text between the fences); `match[0]` is still the whole block including the
47
+ * fences and trailing newline, which is what `stripFrontmatter` blanks.
48
+ */
49
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/;
50
+
40
51
  /**
41
52
  * Replace a leading YAML frontmatter block (`--- … ---`) with blank lines.
42
53
  *
43
54
  * Mintlify parses frontmatter as YAML, not MDX, so it never causes an MDX parse
44
- * error. We blank it rather than delete it so the remaining content keeps its
45
- * original line numbers error positions then match the real file.
55
+ * error but it CAN contain a YAML syntax error, which blanking here hides
56
+ * from `findMdxParseError`. That gap is covered separately by
57
+ * `findFrontmatterError`; `findPageError` runs both. We blank rather than delete
58
+ * so the remaining body keeps its original line numbers — body error positions
59
+ * then match the real file.
46
60
  */
47
61
  export function stripFrontmatter(source: string): string {
48
- const match = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/.exec(source);
62
+ const match = FRONTMATTER_RE.exec(source);
49
63
  if (!match) return source;
50
64
  // Keep newlines, drop every other character, so line numbers stay aligned.
51
65
  const blanked = match[0].replace(/[^\n]/g, "");
52
66
  return blanked + source.slice(match[0].length);
53
67
  }
54
68
 
69
+ /**
70
+ * Parse the leading YAML frontmatter block and return its parse error, or
71
+ * `null` when the block is absent (legal — some pages and every i18n README
72
+ * have none) or valid.
73
+ *
74
+ * This is the half of page validation that `findMdxParseError` structurally
75
+ * cannot do: it compiles only the *body* (frontmatter blanked), so a YAML
76
+ * syntax error in `title:`/`description:` — the exact class that failed the
77
+ * `consolidate` job's `mintlify validate` step, and that this repo's own
78
+ * `validate:mdx` net could not see — sailed straight through. `mintlify`
79
+ * parses the frontmatter as YAML, so parsing it here reproduces that check.
80
+ *
81
+ * The reported `line` is FILE-relative (the opening `---` is file line 1), so
82
+ * it matches the convention `findMdxParseError` already uses for body errors.
83
+ * That is deliberately one greater than the block-relative number `mintlify`
84
+ * prints — do NOT "fix" it to match mintlify; file-relative is what points a
85
+ * reader (or a model asked to repair the page) at the right line.
86
+ */
87
+ export function findFrontmatterError(source: string): MdxParseError | null {
88
+ const match = FRONTMATTER_RE.exec(source);
89
+ if (!match) return null;
90
+ try {
91
+ YAML.parse(match[1]);
92
+ return null;
93
+ } catch (err) {
94
+ const e = err as {
95
+ message?: string;
96
+ linePos?: Array<{ line: number; col: number }>;
97
+ };
98
+ const raw = e.message ?? String(err);
99
+ return {
100
+ // Drop yaml's block-relative "at line N, column N:" phrase (it would
101
+ // contradict the file-relative line we report) but keep the caret-
102
+ // underlined excerpt after it — the single most useful signal for a
103
+ // model asked to repair the offending line.
104
+ message: raw.replace(/ at line \d+, column \d+:/, ":"),
105
+ line: e.linePos?.[0] ? e.linePos[0].line + 1 : undefined,
106
+ column: e.linePos?.[0]?.col,
107
+ };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Validate a full page the way the Mintlify deploy does: frontmatter YAML
113
+ * first, then the MDX body. Returns the first error found, or `null`. This is
114
+ * the single entry point shared by `validate:mdx` (`main()` below) and the
115
+ * translation pipeline's generation-time gate, so the two can never disagree
116
+ * about what is publishable.
117
+ */
118
+ export async function findPageError(
119
+ source: string,
120
+ ): Promise<MdxParseError | null> {
121
+ return findFrontmatterError(source) ?? (await findMdxParseError(source));
122
+ }
123
+
55
124
  /**
56
125
  * Compile one MDX source string with the deploy-time parser. Returns `null`
57
126
  * when it parses cleanly, or the parse error (with position) otherwise.
@@ -114,7 +183,7 @@ async function main(): Promise<void> {
114
183
  const failures: Array<{ file: string; error: MdxParseError }> = [];
115
184
 
116
185
  for (const file of files) {
117
- const error = await findMdxParseError(readFileSync(file, "utf-8"));
186
+ const error = await findPageError(readFileSync(file, "utf-8"));
118
187
  if (error) failures.push({ file: relative(process.cwd(), file), error });
119
188
  }
120
189
 
package/src/audit/cli.ts CHANGED
@@ -22,6 +22,7 @@ import { trackHookEvent } from "../hooks/hook-telemetry";
22
22
  import { getInstanceId } from "../../lib/telemetry-id";
23
23
  import { sanitizeErrorMessage } from "../../lib/telemetry-sanitize";
24
24
  import { openWhenReady } from "./open-browser";
25
+ import { brandAnsi, ANSI_RESET, ANSI_BOLD, ANSI_DIM } from "../hooks/tui";
25
26
 
26
27
  /** Port the bundled dashboard binds to. Matches `scripts/launch.ts`'s default
27
28
  * for `start` mode, which `failproofai` (bare) already uses. */
@@ -59,12 +60,17 @@ WHAT IT DOES
59
60
  `.trimStart();
60
61
 
61
62
  // ── ANSI helpers ────────────────────────────────────────────────────────────
62
- const RESET = "\x1b[0m";
63
- const DIM = "\x1b[2m";
64
- const BOLD = "\x1b[1m";
65
- const PINK = "\x1b[38;5;204m";
66
- const GREEN = "\x1b[38;5;120m";
67
- const CYAN = "\x1b[38;5;81m";
63
+ // Colours come from the shared brand palette in hooks/tui.ts, so `audit` reads
64
+ // as the same product as `config`. This file used to define its own 256-colour
65
+ // set (a green and a blue that appear nowhere in the brand). The palette has
66
+ // two accents rather than three, so success and command/URL text share the
67
+ // teal the glyph (✓) and the surrounding copy already distinguish them.
68
+ const RESET = ANSI_RESET;
69
+ const DIM = ANSI_DIM;
70
+ const BOLD = ANSI_BOLD;
71
+ const PINK = brandAnsi("pink");
72
+ const GREEN = brandAnsi("guide"); // success — brand teal
73
+ const CYAN = brandAnsi("guide"); // commands & URLs — brand teal
68
74
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
69
75
 
70
76
  function colorOn(): boolean {
package/src/auth/cli.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  writeAuth,
29
29
  } from "../../lib/auth/auth-store";
30
30
  import { CliError } from "../cli-error";
31
+ import { brandAnsi, colorsEnabled, ANSI_RESET, ANSI_DIM } from "../hooks/tui";
31
32
  import { trackHookEvent } from "../hooks/hook-telemetry";
32
33
  import { getInstanceId } from "../../lib/telemetry-id";
33
34
 
@@ -123,11 +124,19 @@ function prompt(question: string, opts: { hidden?: boolean } = {}): Promise<stri
123
124
  });
124
125
  }
125
126
 
126
- const DIM = "\x1b[2m";
127
- const RESET = "\x1b[0m";
128
- const PINK = "\x1b[38;5;204m";
129
- const GREEN = "\x1b[38;5;120m";
130
- const RED = "\x1b[38;5;197m";
127
+ // Brand palette (see hooks/tui.ts) rather than this file's former private
128
+ // 256-colour set, so `auth` matches `config` and `audit`. Resolved once here
129
+ // and blanked when colour is off — these are interpolated directly into
130
+ // template literals at ~15 call sites, which previously meant `auth` emitted
131
+ // escape codes even under NO_COLOR. Blanking the constants fixes every call
132
+ // site at once. The palette has no red, and both uses are recoverable input
133
+ // errors ("try again") rather than failures, so they take the amber warn hue.
134
+ const COLOR = colorsEnabled(process.stdout);
135
+ const DIM = COLOR ? ANSI_DIM : "";
136
+ const RESET = COLOR ? ANSI_RESET : "";
137
+ const PINK = COLOR ? brandAnsi("pink") : "";
138
+ const GREEN = COLOR ? brandAnsi("guide") : "";
139
+ const RED = COLOR ? brandAnsi("warn") : "";
131
140
 
132
141
  function emailLooksValid(email: string): boolean {
133
142
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
@@ -172,7 +172,8 @@ const CURL_PIPE_SH_RE = /(?:curl|wget)\s.*\|\s*(?:sh|bash|zsh|dash|ksh|csh|tcsh|
172
172
  const PS_WEB_PIPE_RE = /(?:Invoke-WebRequest|iwr|Invoke-RestMethod|irm)\s+.*\|\s*(?:Invoke-Expression|iex)/i;
173
173
 
174
174
  // blockForcePush
175
- const FORCE_PUSH_RE = /(?:--force|-f\b)/;
175
+ const SHORT_FLAG_BUNDLE_RE = /^-[a-zA-Z]*f[a-zA-Z]*$/;
176
+ const SAFE_FORCE_PREFIXES = ["--force-with-lease", "--force-if-includes"] as const;
176
177
 
177
178
  // blockSecretsWrite
178
179
  const SECRET_FILE_RE = /\.(?:pem|key)$/;
@@ -587,36 +588,154 @@ function blockPushMaster(ctx: PolicyContext): PolicyResult {
587
588
  return allow();
588
589
  }
589
590
 
591
+ // -- block-rm-rf: deletion-target resolution --
592
+
593
+ /**
594
+ * Leading shell forms that resolve to a home directory: `~`, `~user`, `$HOME`,
595
+ * `${HOME}`. The lookahead keeps `$HOMEBREW_PREFIX` (a variable this policy
596
+ * cannot resolve) from being mistaken for `$HOME`.
597
+ */
598
+ const HOME_PREFIX_RE = /^(?:~[A-Za-z0-9_.-]*|\$HOME|\$\{HOME\})(?=$|\/)/;
599
+
600
+ /** `rm`, `/bin/rm`, `/usr/bin/rm`, … — the command word of a delete. */
601
+ const RM_CMD_RE = /^(?:\/\S*\/)?rm$/;
602
+
603
+ /** `find` expression that deletes: `-delete`, `-exec rm`, `-execdir rm`, `-ok rm`. */
604
+ // Same shape as RM_CMD_RE: `find` is just as dangerous when invoked by
605
+ // absolute path (`/usr/bin/find / -delete`), so match both forms.
606
+ const FIND_CMD_RE = /^(?:\/\S*\/)?find$/;
607
+ const FIND_EXEC_RE = /^-(?:exec|execdir|ok|okdir)$/;
608
+
609
+ /** find's global options, which precede its path operands (`find -L / -delete`). */
610
+ const FIND_GLOBAL_OPT_RE = /^-(?:[HLP]|D|O\d*)$/;
611
+
612
+ /** First token of find's expression — everything before it is a path operand. */
613
+ const FIND_EXPR_START_RE = /^(?:-|\\?[(!])/;
614
+
615
+ /**
616
+ * Roots that exist to hold throwaway data. A delete of the root itself is still
617
+ * catastrophic (`rm -rf /tmp` wipes every process's scratch space); a delete of
618
+ * something *inside* one is ordinary work.
619
+ */
620
+ const SCRATCH_ROOTS = ["/tmp", "/var/tmp"];
621
+
622
+ /**
623
+ * How many path segments below a root (`/` or a home directory) a delete has to
624
+ * reach before it stops being catastrophic. `/etc` (1) and `/home/chetan` (2)
625
+ * are system- and user-level directories; `/home/chetan/project` (3) is a
626
+ * specific thing the caller meant to delete.
627
+ */
628
+ const CATASTROPHIC_DEPTH = 2;
629
+
630
+ /** Expand the leading `~` / `$HOME` / `${HOME}` of a path to the real home directory. */
631
+ function expandHomePrefix(path: string): string {
632
+ const m = path.match(/^(?:~|\$HOME|\$\{HOME\})(?=$|\/)/);
633
+ return m ? homedir() + path.slice(m[0].length) : path;
634
+ }
635
+
636
+ /** Drop a trailing `/*` glob and any trailing slashes: `/tmp/foo/*` → `/tmp/foo`. */
637
+ function stripTrailingGlob(path: string): string {
638
+ return path.replace(/\/\*$/, "").replace(/\/+$/, "");
639
+ }
640
+
641
+ /**
642
+ * Would deleting this target be catastrophic?
643
+ *
644
+ * Rather than pattern-matching the raw command text, this resolves the token as
645
+ * far as the shell would: quotes are stripped, `~` / `$HOME` are recognised as a
646
+ * root in their own right, and the remaining path segments are counted. A target
647
+ * within {@link CATASTROPHIC_DEPTH} segments of either root (`/` or home) takes
648
+ * out the machine or the user's data.
649
+ *
650
+ * Fails safe: a token whose head is an expansion this policy cannot evaluate —
651
+ * command substitution (`$(…)`, backticks) or any variable other than `$HOME` —
652
+ * could expand to `/`, so it counts as catastrophic. Relative targets are not
653
+ * flagged: they resolve under the working directory, not under a root.
654
+ */
655
+ function isCatastrophicTarget(token: string): boolean {
656
+ const raw = token.replace(/^['"]|['"]$/g, "");
657
+ if (raw === "") return false;
658
+
659
+ const homePrefix = raw.match(HOME_PREFIX_RE);
660
+ // Unresolvable head: `$(echo /)`, `` `pwd` ``, `$TARGET_DIR`, …
661
+ if (!homePrefix && /^[$`]/.test(raw)) return true;
662
+
663
+ const belowRoot = homePrefix ? raw.slice(homePrefix[0].length) : raw.startsWith("/") ? raw : null;
664
+ if (belowRoot === null) return false;
665
+
666
+ const segments = stripTrailingGlob(belowRoot).split("/").filter(Boolean);
667
+ if (!homePrefix && SCRATCH_ROOTS.some((r) => `/${segments.join("/")}`.startsWith(`${r}/`))) return false;
668
+ return segments.length <= CATASTROPHIC_DEPTH;
669
+ }
670
+
590
671
  /**
591
- * Check whether all *recursive* rm path targets in a command are under an allowlisted path.
672
+ * The paths a single command segment would recursively delete, or `null` when the
673
+ * segment is not a recursive delete at all.
674
+ *
675
+ * Understands the two shapes that reach every file under a target: `rm` with both
676
+ * `-r` and `-f` (in any spelling or order), and `find`, which recurses by design,
677
+ * paired with `-delete` or an `-exec rm`.
678
+ */
679
+ function recursiveDeletionTargets(seg: string): string[] | null {
680
+ const tokens = parseArgvTokens(seg);
681
+
682
+ const findIdx = tokens.findIndex((t) => FIND_CMD_RE.test(t));
683
+ if (findIdx >= 0) {
684
+ const expr = tokens.slice(findIdx + 1);
685
+ const execIdx = expr.findIndex((t) => FIND_EXEC_RE.test(t));
686
+ const deletes = expr.includes("-delete") || (execIdx >= 0 && RM_CMD_RE.test(expr[execIdx + 1] ?? ""));
687
+ if (deletes) {
688
+ // find's path operands sit between the leading global options and the
689
+ // first expression token: `find -L /home/chetan -name '*.log' -delete`
690
+ let start = 0;
691
+ while (start < expr.length && FIND_GLOBAL_OPT_RE.test(expr[start])) {
692
+ start += expr[start] === "-D" ? 2 : 1;
693
+ }
694
+ const rest = expr.slice(start);
695
+ const end = rest.findIndex((t) => FIND_EXPR_START_RE.test(t));
696
+ return end < 0 ? rest : rest.slice(0, end);
697
+ }
698
+ }
699
+
700
+ const rmIdx = tokens.findIndex((t) => RM_CMD_RE.test(t));
701
+ if (rmIdx >= 0) {
702
+ const args = tokens.slice(rmIdx + 1);
703
+ const shortFlags = args.filter((t) => /^-[^-]/.test(t)).join("");
704
+ const longFlags = args.filter((t) => /^--/.test(t));
705
+ const recursive = /r/i.test(shortFlags) || longFlags.some((f) => /^--recursive$/i.test(f));
706
+ const force = /f/.test(shortFlags) || longFlags.some((f) => /^--force$/i.test(f));
707
+ if (recursive && force) return args.filter((t) => !t.startsWith("-"));
708
+ }
709
+
710
+ return null;
711
+ }
712
+
713
+ /** Split a command into the segments the shell would run as separate commands. */
714
+ function shellSegments(cmd: string): string[] {
715
+ return cmd.split(/&&|\|\||[|;\n]/).map((s) => s.trim()).filter((s) => s !== "");
716
+ }
717
+
718
+ /**
719
+ * Check whether all recursive-delete targets in a command are under an allowlisted path.
592
720
  * Splits on shell operators first so that `/tmp` appearing in an unrelated
593
721
  * sub-command (e.g. `echo /tmp && rm -rf /`) does not trigger a false allow.
594
722
  * Uses path-boundary comparison so `/tmp` does not cover `/tmp2`.
595
723
  * Non-recursive rm segments (no -r/-R flag) are skipped — they pose no catastrophic risk.
596
724
  * Quoted paths with spaces are handled via a segment-level regex fallback.
725
+ * Home-relative targets and allowPaths entries are expanded, so `~/scratch` is
726
+ * covered by an allowPaths entry of either `~/scratch` or the absolute home path.
597
727
  */
598
- function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
728
+ function deletionTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
599
729
  if (allowPaths.length === 0) return false;
600
- const segments = cmd
601
- .split(/&&|\|\||[|;\n]/)
602
- .map((s) => s.trim())
603
- .filter((s) => /\brm\b/.test(s));
604
- if (segments.length === 0) return false;
605
- for (const seg of segments) {
606
- const tokens = parseArgvTokens(seg);
607
- const rmIdx = tokens.findIndex((t) => t === "rm");
608
- if (rmIdx < 0) continue;
609
- // Only validate recursive rm segments — non-recursive rm has no catastrophic-deletion risk
610
- const flagTokens = tokens.slice(rmIdx + 1).filter((t) => /^-[^-]/.test(t));
611
- const longFlagsInSeg = tokens.slice(rmIdx + 1).filter((t) => /^--/.test(t));
612
- if (!/r/i.test(flagTokens.join("")) && !longFlagsInSeg.some(f => /^--recursive$/i.test(f))) continue;
613
- const pathArgs = tokens.slice(rmIdx + 1).filter((t) => !t.startsWith("-"));
614
- for (const target of pathArgs) {
615
- const normalized = target.replace(/\/\*$/, "").replace(/\/+$/, "") || "/";
616
- const covered = allowPaths.some((p) => {
617
- const np = p.replace(/\/+$/, "") || "/";
618
- return normalized === np || normalized.startsWith(np + "/");
619
- });
730
+ const normalizedAllowPaths = allowPaths.map((p) => stripTrailingGlob(expandHomePrefix(p)) || "/");
731
+ let sawRecursiveDelete = false;
732
+ for (const seg of shellSegments(cmd)) {
733
+ const targets = recursiveDeletionTargets(seg);
734
+ if (targets === null) continue;
735
+ sawRecursiveDelete = true;
736
+ for (const target of targets) {
737
+ const normalized = stripTrailingGlob(expandHomePrefix(target)) || "/";
738
+ const covered = normalizedAllowPaths.some((np) => normalized === np || normalized.startsWith(np + "/"));
620
739
  if (!covered) {
621
740
  // Fallback: check the raw segment for quoted paths that contain spaces
622
741
  // (parseArgvTokens splits on whitespace, so "/tmp/my dir" becomes two tokens)
@@ -628,40 +747,23 @@ function rmTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
628
747
  }
629
748
  }
630
749
  }
631
- return true;
750
+ return sawRecursiveDelete;
632
751
  }
633
752
 
634
753
  function blockRmRf(ctx: PolicyContext): PolicyResult {
635
754
  if (ctx.toolName !== "Bash") return allow();
636
755
  const cmd = getCommand(ctx);
637
- const hasDestructivePath = parseArgvTokens(cmd).some((token) => {
638
- const normalized = token.replace(/\/\*$/, "").replace(/\/+$/, "") || (token.startsWith("/") ? "/" : "");
639
- return normalized === "/" || normalized === "~" || /^\/[A-Za-z_][\w.-]*$/.test(normalized);
640
- });
641
756
 
642
- // Combined flags in one token: rm -rf /, rm -fr /
643
- if (hasDestructivePath && (
644
- /rm\s+-[^\s]*r[^\s]*f[^\s]*/.test(cmd) ||
645
- /rm\s+-[^\s]*f[^\s]*r[^\s]*/.test(cmd)
646
- )) {
757
+ const hasCatastrophicTarget = shellSegments(cmd).some((seg) => {
758
+ const targets = recursiveDeletionTargets(seg);
759
+ return targets !== null && targets.some(isCatastrophicTarget);
760
+ });
761
+ if (hasCatastrophicTarget) {
647
762
  const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
648
- if (rmTargetIsAllowed(cmd, allowPaths)) return allow();
763
+ if (deletionTargetIsAllowed(cmd, allowPaths)) return allow();
649
764
  return deny("Catastrophic deletion blocked");
650
765
  }
651
766
 
652
- // Separated flags: rm -r -f /, rm -f -r /, rm -r -v -f /, etc.
653
- if (hasDestructivePath && /\brm\b/.test(cmd)) {
654
- const tokens = parseArgvTokens(cmd);
655
- const shortFlags = tokens.filter((t) => /^-[^-]/.test(t)).join("");
656
- const longFlags = tokens.filter((t) => /^--/.test(t));
657
- const hasRecursive = /r/i.test(shortFlags) || longFlags.some(f => /^--recursive$/i.test(f));
658
- const hasForce = /f/.test(shortFlags) || longFlags.some(f => /^--force$/i.test(f));
659
- if (hasRecursive && hasForce) {
660
- const allowPaths = ((ctx.params?.allowPaths ?? []) as string[]);
661
- if (rmTargetIsAllowed(cmd, allowPaths)) return allow();
662
- return deny("Catastrophic deletion blocked");
663
- }
664
- }
665
767
  // PowerShell: Remove-Item -Recurse -Force on root/drive
666
768
  if (/Remove-Item\s+.*-Recurse.*-Force.*(?:[A-Z]:\\(?:\s|$)|\\\*)/i.test(cmd)) {
667
769
  return deny("Catastrophic deletion blocked");
@@ -675,13 +777,29 @@ function blockRmRf(ctx: PolicyContext): PolicyResult {
675
777
 
676
778
  function blockForcePush(ctx: PolicyContext): PolicyResult {
677
779
  if (ctx.toolName !== "Bash") return allow();
678
- const args = extractGitPushArgs(getCommand(ctx));
679
- if (args.some((a) => FORCE_PUSH_RE.test(a))) {
680
- return deny("Force-pushing is blocked");
780
+ for (const segment of extractGitPushArgs(getCommand(ctx))) {
781
+ let sawEndOfOptions = false;
782
+ for (const token of segment.split(/\s+/)) {
783
+ if (token === "--") {
784
+ sawEndOfOptions = true;
785
+ continue;
786
+ }
787
+ if (sawEndOfOptions) continue;
788
+ if (isForcePushFlag(token)) {
789
+ return deny("Force-pushing is blocked");
790
+ }
791
+ }
681
792
  }
682
793
  return allow();
683
794
  }
684
795
 
796
+ function isForcePushFlag(token: string): boolean {
797
+ if (token === "--force") return true;
798
+ if (SAFE_FORCE_PREFIXES.some((prefix) => token.startsWith(prefix))) return false;
799
+ if (token.startsWith("--force")) return true;
800
+ return SHORT_FLAG_BUNDLE_RE.test(token);
801
+ }
802
+
685
803
  function blockSecretsWrite(ctx: PolicyContext): PolicyResult {
686
804
  if (ctx.toolName !== "Write") return allow();
687
805
  const filePath = getFilePath(ctx);