failproofai 0.0.14-beta.1 → 0.0.14-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.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.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 +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- 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 +9 -9
- 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 +1 -1
- 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/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 +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- 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/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/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/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/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]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.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]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.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/[root-of-the-server]__1qb590j._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- 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]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
- 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]__05r_17v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0wp9w6i._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.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]__0p84xee._.js +1 -1
- 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]__0brjczq._.js → [root-of-the-server]__0twi4fj._.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]__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]__1nq7ivq._.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/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- 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/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_12rjfea.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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/{0o9wpnze1ey8y.js → 0h-6ymo9v86b0.js} +2 -2
- package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
- package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
- package/.next/standalone/.next/static/chunks/{2vav_x0_336ds.js → 1bg_g3g359c_d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1359o5785rmve.js → 1gg-8e9q_m__9.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0emg30_6svycu.js → 29zkom4s9de2d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0d5lypu1zs21r.js → 2llkdafnaejw_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{20tuctw38jbgw.js → 388hgfqye67xa.js} +1 -1
- package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
- package/.next/standalone/app/components/copy-button.tsx +22 -6
- package/.next/standalone/{ci/cli-integration → integration-suite}/README.md +25 -7
- package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
- package/.next/standalone/{ci/cli-integration → integration-suite}/run.sh +4 -4
- package/.next/standalone/lib/format-duration.ts +23 -9
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +342 -106
- package/lib/format-duration.ts +23 -9
- package/package.json +1 -1
- package/scripts/translate-docs/cli.ts +6 -0
- package/scripts/translate-docs/mdx-translator.ts +30 -19
- package/scripts/translate-docs/readme-translator.ts +31 -21
- package/scripts/translate-docs/translator.ts +142 -7
- package/scripts/translate-docs/types.ts +6 -0
- package/scripts/translate-docs/validate-translation.ts +132 -0
- package/scripts/validate-mdx.ts +80 -11
- package/src/audit/cli.ts +12 -6
- package/src/auth/cli.ts +14 -5
- package/src/hooks/builtin-policies.ts +168 -50
- package/src/hooks/configure-wizard.ts +238 -22
- package/src/hooks/custom-hooks-loader.ts +70 -4
- package/src/hooks/handler.ts +4 -1
- package/src/hooks/policy-types.ts +8 -0
- package/src/hooks/tui.ts +95 -28
- package/.next/standalone/.next/static/chunks/2ldhnaweu9ew3.js +0 -1
- package/.next/standalone/.next/static/chunks/3cg7x0joi2b98.js +0 -1
- package/.next/standalone/.next/static/chunks/3k87p-tijud-4.js +0 -1
- package/.next/standalone/.next/static/chunks/3kmaqcp7rldad.js +0 -1
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{LkN-JjmhKGmL-WklH7WVi → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/Dockerfile +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/canary-policies.mjs +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/capture-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/inject-tokens.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/install-clis.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/probe-cli.sh +0 -0
- /package/.next/standalone/{ci/cli-integration → integration-suite}/report.js +0 -0
package/scripts/validate-mdx.ts
CHANGED
|
@@ -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)
|
|
6
|
-
* `docs.json` structure
|
|
7
|
-
*
|
|
8
|
-
* error (e.g. a `<slug>` that escaped its
|
|
9
|
-
* translation dropped a closing `` ` ``) passes
|
|
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
|
|
45
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
728
|
+
function deletionTargetIsAllowed(cmd: string, allowPaths: string[]): boolean {
|
|
599
729
|
if (allowPaths.length === 0) return false;
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
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
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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 (
|
|
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
|
|
679
|
-
|
|
680
|
-
|
|
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);
|