@monte3l/groundwork 0.1.0-next.1 → 1.0.0-rc.3
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/README.md +16 -8
- package/bin/m3l-groundwork.mjs +36 -2
- package/dist/assets.d.ts +10 -0
- package/dist/assets.js +14 -0
- package/dist/baseline-stage.d.ts +173 -0
- package/dist/baseline-stage.js +215 -0
- package/dist/caps.d.ts +4 -1
- package/dist/caps.js +17 -3
- package/dist/conflicts.d.ts +23 -2
- package/dist/conflicts.js +89 -11
- package/dist/customize-paths.d.ts +92 -0
- package/dist/customize-paths.js +115 -0
- package/dist/emit.d.ts +50 -1
- package/dist/emit.js +121 -13
- package/dist/fatal.d.ts +70 -0
- package/dist/fatal.js +132 -0
- package/dist/format-error.d.ts +113 -0
- package/dist/format-error.js +553 -0
- package/dist/fs-guard.d.ts +142 -0
- package/dist/fs-guard.js +222 -0
- package/dist/git.js +10 -1
- package/dist/harness/conformance.js +2 -0
- package/dist/harness/frontmatter.js +2 -13
- package/dist/harness/grade.js +37 -8
- package/dist/harness/rules.d.ts +20 -3
- package/dist/harness/rules.js +108 -6
- package/dist/harness/types.d.ts +13 -0
- package/dist/harness/types.js +3 -0
- package/dist/inventory.d.ts +77 -3
- package/dist/inventory.js +103 -12
- package/dist/jsonc.d.ts +49 -2
- package/dist/jsonc.js +140 -7
- package/dist/main.d.ts +62 -3
- package/dist/main.js +538 -67
- package/dist/merge-json.d.ts +47 -4
- package/dist/merge-json.js +120 -8
- package/dist/mode.js +12 -2
- package/dist/pack-stage.d.ts +210 -0
- package/dist/pack-stage.js +287 -0
- package/dist/packs.d.ts +19 -13
- package/dist/packs.js +231 -28
- package/dist/palette.d.ts +23 -0
- package/dist/palette.js +22 -0
- package/dist/plugin.d.ts +122 -6
- package/dist/plugin.js +687 -47
- package/dist/report.d.ts +21 -2
- package/dist/report.js +93 -5
- package/dist/staging.d.ts +176 -0
- package/dist/staging.js +375 -0
- package/dist/survey/fs-walk.d.ts +34 -2
- package/dist/survey/fs-walk.js +70 -5
- package/dist/survey/internal/blocked-path.d.ts +27 -0
- package/dist/survey/internal/blocked-path.js +129 -0
- package/dist/survey/internal/package-json.d.ts +13 -0
- package/dist/survey/internal/package-json.js +37 -0
- package/dist/survey/internal/read-guard.d.ts +178 -0
- package/dist/survey/internal/read-guard.js +276 -0
- package/dist/survey/survey-docs.d.ts +17 -2
- package/dist/survey/survey-docs.js +61 -21
- package/dist/survey/survey-harness.d.ts +20 -2
- package/dist/survey/survey-harness.js +87 -46
- package/dist/survey/survey-shape.d.ts +17 -2
- package/dist/survey/survey-shape.js +60 -46
- package/dist/survey/survey-toolchain.d.ts +16 -1
- package/dist/survey/survey-toolchain.js +69 -66
- package/dist/survey/survey.js +6 -4
- package/dist/survey/types.d.ts +79 -0
- package/dist/survey/types.js +2 -6
- package/dist/term.d.ts +80 -0
- package/dist/term.js +145 -0
- package/dist/tokens.js +2 -0
- package/dist/toolchain/conformance.js +2 -0
- package/dist/toolchain/grade.js +26 -8
- package/dist/toolchain/rules.d.ts +13 -4
- package/dist/toolchain/rules.js +16 -0
- package/dist/toolchain/tsconfig-chain.d.ts +2 -0
- package/dist/toolchain/tsconfig-chain.js +32 -8
- package/dist/toolchain/types.d.ts +16 -4
- package/dist/toolchain/types.js +3 -0
- package/package.json +4 -3
- package/plugin/skills/customize/SKILL.md +292 -18
- package/plugin/src/domain-map.ts +39 -14
- package/plugin/src/index.ts +5 -1
- package/plugin/src/kind-facet-map.ts +25 -8
- package/plugin/src/pack-map.ts +147 -25
- package/plugin/src/plugin-map.ts +236 -0
- package/templates/core/.claude/agents/Explore.md +0 -1
- package/templates/core/.claude/agents/code-implementer.md +4 -4
- package/templates/core/.claude/agents/code-reviewer.md +4 -4
- package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
- package/templates/core/.claude/agents/test-author.md +7 -5
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
- package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
- package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
- package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
- package/templates/core/.claude/rules/agent-dispatch.md +7 -0
- package/templates/core/.claude/rules/tests.md +2 -2
- package/templates/core/.claude/settings.json +5 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
- package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
- package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
- package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
- package/templates/core/.github/dependabot.yml +18 -0
- package/templates/core/.github/workflows/ci.yml +15 -15
- package/templates/core/.github/workflows/dependency-review.yml +2 -2
- package/templates/core/.github/workflows/security-audit.yml +10 -4
- package/templates/core/.prettierignore +4 -0
- package/templates/core/CLAUDE.md +53 -3
- package/templates/core/README.md +30 -10
- package/templates/core/_gitignore +17 -0
- package/templates/core/bin/check-exports.mjs +11 -5
- package/templates/core/bin/lib/agent-roster.mjs +1 -1
- package/templates/core/bin/lib/frontmatter.mjs +4 -2
- package/templates/core/bin/lib/harness-rules.mjs +169 -20
- package/templates/core/bin/lib/protected-paths.mjs +93 -5
- package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
- package/templates/core/bin/lib/verify-steps.mjs +29 -11
- package/templates/core/bin/verify.mjs +12 -0
- package/templates/core/eslint.config.js +5 -0
- package/templates/core/package.json +7 -7
- package/templates/core/vitest.config.ts +8 -1
- package/templates/packs/README.md +35 -24
- package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
- package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
- package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
- package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
- package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
- package/templates/packs/github/pack.json +19 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
- package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
- package/templates/packs/harness-extras/pack.json +18 -13
- package/templates/packs/publishing/files/.changeset/README.md +25 -0
- package/templates/packs/publishing/files/.changeset/config.json +7 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
- package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
- package/templates/packs/publishing/files/REUSE.toml +28 -0
- package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
- package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
- package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
- package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
- package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
- package/templates/packs/publishing/pack.json +35 -0
- package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
- package/templates/packs/quality/pack.json +29 -0
- package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
- package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
- package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
- package/templates/packs/supply-chain/pack.json +19 -0
- package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
- package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
- package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
- package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
- package/templates/packs/worktrees/files/.worktreeinclude +11 -0
- package/templates/packs/worktrees/pack.json +64 -0
- package/templates/packs/statusline/pack.json +0 -31
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
- /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
- /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
- /package/templates/packs/{harness-extras → quality}/files/bin/file-budget-baseline.json +0 -0
package/dist/conflicts.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Compares `templates/core` (after token substitution) against a target
|
|
3
5
|
* directory, file by file, without writing anything. `package.json` and any
|
|
@@ -5,10 +7,13 @@
|
|
|
5
7
|
* a whole-file conflict on `package.json` is a useless finding, since the
|
|
6
8
|
* answer is always a merge, never "pick one file wholesale".
|
|
7
9
|
*/
|
|
8
|
-
import {
|
|
10
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
9
11
|
import { basename, join, relative } from "node:path";
|
|
10
12
|
import { restoreDotfilePath } from "./assets.js";
|
|
11
13
|
import { parseJsonc } from "./jsonc.js";
|
|
14
|
+
import { isRecord } from "./merge-json.js";
|
|
15
|
+
import { blockedAbsentNote } from "./survey/internal/blocked-path.js";
|
|
16
|
+
import { probePath, probeSubject, readFailure, recordedReadCode, unreadableNote, } from "./survey/internal/read-guard.js";
|
|
12
17
|
import { applyTokens } from "./tokens.js";
|
|
13
18
|
function isKeyLevelJsonFile(relPath) {
|
|
14
19
|
const name = basename(relPath);
|
|
@@ -20,8 +25,14 @@ function compareJsonKeys(baselineContent, targetContent) {
|
|
|
20
25
|
if (!baseline.ok || !target.ok) {
|
|
21
26
|
return undefined;
|
|
22
27
|
}
|
|
28
|
+
// Valid JSON need not be an object (`null`, a number, an array): a key-level
|
|
29
|
+
// compare is meaningless there, so take the same whole-file fallback as an
|
|
30
|
+
// unparseable file.
|
|
23
31
|
const baselineValue = baseline.value;
|
|
24
32
|
const targetValue = target.value;
|
|
33
|
+
if (!isRecord(baselineValue) || !isRecord(targetValue)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
25
36
|
const keys = new Set([
|
|
26
37
|
...Object.keys(baselineValue),
|
|
27
38
|
...Object.keys(targetValue),
|
|
@@ -34,11 +45,57 @@ function compareJsonKeys(baselineContent, targetContent) {
|
|
|
34
45
|
}
|
|
35
46
|
return diffs;
|
|
36
47
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Appends `note` unless already present: adopt mode passes the survey's own
|
|
50
|
+
* list, and a pack and the baseline can name the same path.
|
|
51
|
+
*/
|
|
52
|
+
function recordOnce(undetermined, note) {
|
|
53
|
+
if (!undetermined.includes(note))
|
|
54
|
+
undetermined.push(note);
|
|
55
|
+
}
|
|
56
|
+
function compareFile(relPath, baselineContent, targetPath, targetDir, undetermined) {
|
|
57
|
+
// A real `stat`, never `existsSync`: a file under a directory this process
|
|
58
|
+
// cannot search must not read as "absent" (a clean add `/customize` would
|
|
59
|
+
// then write straight over). Something there this process cannot reach is
|
|
60
|
+
// divergent -- it cannot be shown identical, and adopt mode never
|
|
61
|
+
// overwrites -- with the errno recorded. Any other errno throws.
|
|
62
|
+
const probe = probePath(targetPath);
|
|
63
|
+
if (probe.kind === "absent") {
|
|
64
|
+
// `absent` also covers a dangling symlink at the path or an ancestor
|
|
65
|
+
// (`ENOENT`) and a regular file where an ancestor directory should be
|
|
66
|
+
// (`ENOTDIR`): both are something in the project's tree a write would
|
|
67
|
+
// have to go through, so they are divergent, recorded once, never a
|
|
68
|
+
// silent clean add.
|
|
69
|
+
const note = blockedAbsentNote(targetPath, targetDir);
|
|
70
|
+
if (note === undefined) {
|
|
71
|
+
return { relPath, status: "absent", keyDiffs: undefined };
|
|
72
|
+
}
|
|
73
|
+
recordOnce(undetermined, note);
|
|
74
|
+
return { relPath, status: "divergent", keyDiffs: undefined };
|
|
75
|
+
}
|
|
76
|
+
if (probe.kind === "unresolvable") {
|
|
77
|
+
// A permission failure names the enclosing directory (the `stat` needed
|
|
78
|
+
// search permission there, not on the file), so every baseline file
|
|
79
|
+
// under one locked directory shares a single note; a symlink loop names
|
|
80
|
+
// the path itself.
|
|
81
|
+
recordOnce(undetermined, unreadableNote(probeSubject(targetPath, probe.code), probe.code));
|
|
82
|
+
return { relPath, status: "divergent", keyDiffs: undefined };
|
|
83
|
+
}
|
|
84
|
+
// A read failure that is a property of the project's tree (a permission
|
|
85
|
+
// failure, a directory where the baseline has a file) is divergent too --
|
|
86
|
+
// it cannot be shown identical -- and is recorded naming the path, so the
|
|
87
|
+
// report never shows "divergent" with no reason behind it.
|
|
88
|
+
let targetContent;
|
|
89
|
+
try {
|
|
90
|
+
targetContent = readFileSync(targetPath, "utf8");
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
const code = recordedReadCode(error);
|
|
94
|
+
if (code === undefined)
|
|
95
|
+
throw readFailure(targetPath, error);
|
|
96
|
+
recordOnce(undetermined, unreadableNote(targetPath, code));
|
|
97
|
+
return { relPath, status: "divergent", keyDiffs: undefined };
|
|
40
98
|
}
|
|
41
|
-
const targetContent = readFileSync(targetPath, "utf8");
|
|
42
99
|
if (isKeyLevelJsonFile(relPath)) {
|
|
43
100
|
const keyDiffs = compareJsonKeys(baselineContent, targetContent);
|
|
44
101
|
if (keyDiffs !== undefined) {
|
|
@@ -56,22 +113,43 @@ function compareFile(relPath, baselineContent, targetPath) {
|
|
|
56
113
|
keyDiffs: undefined,
|
|
57
114
|
};
|
|
58
115
|
}
|
|
59
|
-
function walkTemplate(root, currentDir, targetDir, tokens, results) {
|
|
116
|
+
function walkTemplate(root, currentDir, targetDir, tokens, results, undetermined) {
|
|
60
117
|
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
61
118
|
const sourcePath = join(currentDir, entry.name);
|
|
62
119
|
const relPath = restoreDotfilePath(applyTokens(relative(root, sourcePath), tokens));
|
|
63
120
|
if (entry.isDirectory()) {
|
|
64
|
-
walkTemplate(root, sourcePath, targetDir, tokens, results);
|
|
121
|
+
walkTemplate(root, sourcePath, targetDir, tokens, results, undetermined);
|
|
65
122
|
continue;
|
|
66
123
|
}
|
|
67
124
|
const baselineContent = applyTokens(readFileSync(sourcePath, "utf8"), tokens);
|
|
68
|
-
results.push(compareFile(relPath, baselineContent, join(targetDir, relPath)));
|
|
125
|
+
results.push(compareFile(relPath, baselineContent, join(targetDir, relPath), targetDir, undetermined));
|
|
69
126
|
}
|
|
70
127
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Compares every file `templates/core` (or a pack's `files/`) would emit
|
|
130
|
+
* against what `targetDir` already has. A target path this process cannot
|
|
131
|
+
* reach (`EACCES`/`EPERM`/`ELOOP`) is never reported `absent`: it is
|
|
132
|
+
* `divergent`, and a note naming the errno is appended to `undetermined`
|
|
133
|
+
* once (adopt mode passes the survey's own list, so the report shows it) --
|
|
134
|
+
* naming the enclosing directory for a permission failure on the `stat`, the
|
|
135
|
+
* path itself for a symlink loop, or for a permission failure or a
|
|
136
|
+
* directory-where-a-file-was-expected (`EISDIR`) on the read. A dangling
|
|
137
|
+
* symlink at the target path is `divergent` too, recorded naming the path;
|
|
138
|
+
* so is a dangling symlink at one of its ancestors below `targetDir`,
|
|
139
|
+
* recorded once naming that ancestor. A regular file where an enclosing
|
|
140
|
+
* directory should be (`ENOTDIR`) is `divergent`, recorded once naming that
|
|
141
|
+
* blocking ancestor. A genuinely missing path is `absent`; any other errno
|
|
142
|
+
* throws.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const undetermined: string[] = [];
|
|
147
|
+
* const conflicts = planConflicts(templateRoot, targetDir, tokens, undetermined);
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function planConflicts(templateRoot, targetDir, tokens, undetermined = []) {
|
|
73
151
|
const results = [];
|
|
74
|
-
walkTemplate(templateRoot, templateRoot, targetDir, tokens, results);
|
|
152
|
+
walkTemplate(templateRoot, templateRoot, targetDir, tokens, results, undetermined);
|
|
75
153
|
return results;
|
|
76
154
|
}
|
|
77
155
|
//# sourceMappingURL=conflicts.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skill's entry file: the one payload file that lives under the plugin
|
|
3
|
+
* source's `skills/customize/` (every other one lives under its `src/`), and
|
|
4
|
+
* the one whose presence makes Claude Code load the skill -- which is why
|
|
5
|
+
* {@link CUSTOMIZE_SKILL_WRITE_ORDER} writes it last.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { CUSTOMIZE_SKILL_ENTRY_FILE } from "./customize-paths.js";
|
|
10
|
+
*
|
|
11
|
+
* CUSTOMIZE_SKILL_ENTRY_FILE; // "SKILL.md"
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare const CUSTOMIZE_SKILL_ENTRY_FILE = "SKILL.md";
|
|
15
|
+
/**
|
|
16
|
+
* Every payload file's name inside an installed copy of the skill.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { CUSTOMIZE_SKILL_FILE_NAMES } from "./customize-paths.js";
|
|
21
|
+
*
|
|
22
|
+
* CUSTOMIZE_SKILL_FILE_NAMES.includes("SKILL.md"); // true
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const CUSTOMIZE_SKILL_FILE_NAMES: readonly ["SKILL.md", "kind-facet-map.ts", "domain-map.ts", "pack-map.ts", "plugin-map.ts"];
|
|
26
|
+
/**
|
|
27
|
+
* The order an install writes the payload in: every backing-data file
|
|
28
|
+
* first, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails part-way
|
|
29
|
+
* (and whose rollback cannot remove everything) therefore never leaves a
|
|
30
|
+
* loadable `SKILL.md` beside missing or stale data files -- given the
|
|
31
|
+
* precondition that no `SKILL.md` already sits at the destination. Wherever
|
|
32
|
+
* an install replaces existing entries -- fresh mode's
|
|
33
|
+
* `.claude/skills/customize/` (a `--force` re-run over an earlier install)
|
|
34
|
+
* and the CLI-owned `.groundwork/customize/` -- `plugin.ts` establishes that
|
|
35
|
+
* precondition by removing any existing `SKILL.md` entry before the first
|
|
36
|
+
* data file is rewritten. Adopt mode's additive `.claude/skills/customize/`
|
|
37
|
+
* install only ever writes there when no `SKILL.md` entry exists.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { CUSTOMIZE_SKILL_WRITE_ORDER } from "./customize-paths.js";
|
|
42
|
+
*
|
|
43
|
+
* CUSTOMIZE_SKILL_WRITE_ORDER.at(-1); // "SKILL.md"
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const CUSTOMIZE_SKILL_WRITE_ORDER: readonly ["kind-facet-map.ts", "domain-map.ts", "pack-map.ts", "plugin-map.ts", "SKILL.md"];
|
|
47
|
+
/**
|
|
48
|
+
* The fresh-mode destination -- and adopt mode's, when nothing is there yet
|
|
49
|
+
* or an earlier install was interrupted before its `SKILL.md` -- as path
|
|
50
|
+
* segments relative to the project root.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { join } from "node:path";
|
|
55
|
+
* import { CLAUDE_DEST_SEGMENTS } from "./customize-paths.js";
|
|
56
|
+
*
|
|
57
|
+
* join(...CLAUDE_DEST_SEGMENTS); // ".claude/skills/customize"
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare const CLAUDE_DEST_SEGMENTS: readonly [".claude", "skills", "customize"];
|
|
61
|
+
/**
|
|
62
|
+
* The adopt-mode fallback destination, as path segments relative to the
|
|
63
|
+
* project root. Used whenever {@link CLAUDE_DEST_SEGMENTS} cannot be written
|
|
64
|
+
* additively: a project-owned entry under any of the skill's payload names
|
|
65
|
+
* that is not this CLI's current copy (a differing file, a symlink, a
|
|
66
|
+
* directory, a `SKILL.md` without its data), or a component of
|
|
67
|
+
* `.claude/skills/customize` that is a symlink or not a directory.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { join } from "node:path";
|
|
72
|
+
* import { GROUNDWORK_DEST_SEGMENTS } from "./customize-paths.js";
|
|
73
|
+
*
|
|
74
|
+
* join(...GROUNDWORK_DEST_SEGMENTS); // ".groundwork/customize"
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare const GROUNDWORK_DEST_SEGMENTS: readonly [".groundwork", "customize"];
|
|
78
|
+
/**
|
|
79
|
+
* Every path, relative to the project root, that either install location
|
|
80
|
+
* could write -- so a caller can scope-check them before anything is
|
|
81
|
+
* written.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { plannedCustomizeSkillPaths } from "./customize-paths.js";
|
|
86
|
+
*
|
|
87
|
+
* plannedCustomizeSkillPaths();
|
|
88
|
+
* // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/plugin-map.ts"]
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function plannedCustomizeSkillPaths(): readonly string[];
|
|
92
|
+
//# sourceMappingURL=customize-paths.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Where the `/customize` skill install (`plugin.ts`) writes, owned in one
|
|
5
|
+
* place so `main.ts`'s adopt-mode write-scope check can validate the planned
|
|
6
|
+
* destinations before `plugin.ts` writes anything. Pure data, no I/O.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
/**
|
|
10
|
+
* The skill's entry file: the one payload file that lives under the plugin
|
|
11
|
+
* source's `skills/customize/` (every other one lives under its `src/`), and
|
|
12
|
+
* the one whose presence makes Claude Code load the skill -- which is why
|
|
13
|
+
* {@link CUSTOMIZE_SKILL_WRITE_ORDER} writes it last.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { CUSTOMIZE_SKILL_ENTRY_FILE } from "./customize-paths.js";
|
|
18
|
+
*
|
|
19
|
+
* CUSTOMIZE_SKILL_ENTRY_FILE; // "SKILL.md"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export const CUSTOMIZE_SKILL_ENTRY_FILE = "SKILL.md";
|
|
23
|
+
/** The backing-data payload files, all sourced from the plugin's `src/`. */
|
|
24
|
+
const CUSTOMIZE_SKILL_DATA_FILE_NAMES = [
|
|
25
|
+
"kind-facet-map.ts",
|
|
26
|
+
"domain-map.ts",
|
|
27
|
+
"pack-map.ts",
|
|
28
|
+
"plugin-map.ts",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Every payload file's name inside an installed copy of the skill.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { CUSTOMIZE_SKILL_FILE_NAMES } from "./customize-paths.js";
|
|
36
|
+
*
|
|
37
|
+
* CUSTOMIZE_SKILL_FILE_NAMES.includes("SKILL.md"); // true
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const CUSTOMIZE_SKILL_FILE_NAMES = [
|
|
41
|
+
CUSTOMIZE_SKILL_ENTRY_FILE,
|
|
42
|
+
...CUSTOMIZE_SKILL_DATA_FILE_NAMES,
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* The order an install writes the payload in: every backing-data file
|
|
46
|
+
* first, {@link CUSTOMIZE_SKILL_ENTRY_FILE} last. A run that fails part-way
|
|
47
|
+
* (and whose rollback cannot remove everything) therefore never leaves a
|
|
48
|
+
* loadable `SKILL.md` beside missing or stale data files -- given the
|
|
49
|
+
* precondition that no `SKILL.md` already sits at the destination. Wherever
|
|
50
|
+
* an install replaces existing entries -- fresh mode's
|
|
51
|
+
* `.claude/skills/customize/` (a `--force` re-run over an earlier install)
|
|
52
|
+
* and the CLI-owned `.groundwork/customize/` -- `plugin.ts` establishes that
|
|
53
|
+
* precondition by removing any existing `SKILL.md` entry before the first
|
|
54
|
+
* data file is rewritten. Adopt mode's additive `.claude/skills/customize/`
|
|
55
|
+
* install only ever writes there when no `SKILL.md` entry exists.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { CUSTOMIZE_SKILL_WRITE_ORDER } from "./customize-paths.js";
|
|
60
|
+
*
|
|
61
|
+
* CUSTOMIZE_SKILL_WRITE_ORDER.at(-1); // "SKILL.md"
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const CUSTOMIZE_SKILL_WRITE_ORDER = [
|
|
65
|
+
...CUSTOMIZE_SKILL_DATA_FILE_NAMES,
|
|
66
|
+
CUSTOMIZE_SKILL_ENTRY_FILE,
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* The fresh-mode destination -- and adopt mode's, when nothing is there yet
|
|
70
|
+
* or an earlier install was interrupted before its `SKILL.md` -- as path
|
|
71
|
+
* segments relative to the project root.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { join } from "node:path";
|
|
76
|
+
* import { CLAUDE_DEST_SEGMENTS } from "./customize-paths.js";
|
|
77
|
+
*
|
|
78
|
+
* join(...CLAUDE_DEST_SEGMENTS); // ".claude/skills/customize"
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const CLAUDE_DEST_SEGMENTS = [".claude", "skills", "customize"];
|
|
82
|
+
/**
|
|
83
|
+
* The adopt-mode fallback destination, as path segments relative to the
|
|
84
|
+
* project root. Used whenever {@link CLAUDE_DEST_SEGMENTS} cannot be written
|
|
85
|
+
* additively: a project-owned entry under any of the skill's payload names
|
|
86
|
+
* that is not this CLI's current copy (a differing file, a symlink, a
|
|
87
|
+
* directory, a `SKILL.md` without its data), or a component of
|
|
88
|
+
* `.claude/skills/customize` that is a symlink or not a directory.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { join } from "node:path";
|
|
93
|
+
* import { GROUNDWORK_DEST_SEGMENTS } from "./customize-paths.js";
|
|
94
|
+
*
|
|
95
|
+
* join(...GROUNDWORK_DEST_SEGMENTS); // ".groundwork/customize"
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export const GROUNDWORK_DEST_SEGMENTS = [".groundwork", "customize"];
|
|
99
|
+
/**
|
|
100
|
+
* Every path, relative to the project root, that either install location
|
|
101
|
+
* could write -- so a caller can scope-check them before anything is
|
|
102
|
+
* written.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { plannedCustomizeSkillPaths } from "./customize-paths.js";
|
|
107
|
+
*
|
|
108
|
+
* plannedCustomizeSkillPaths();
|
|
109
|
+
* // [".claude/skills/customize/SKILL.md", ..., ".groundwork/customize/plugin-map.ts"]
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function plannedCustomizeSkillPaths() {
|
|
113
|
+
return [CLAUDE_DEST_SEGMENTS, GROUNDWORK_DEST_SEGMENTS].flatMap((segments) => CUSTOMIZE_SKILL_FILE_NAMES.map((name) => join(...segments, name)));
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=customize-paths.js.map
|
package/dist/emit.d.ts
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
1
|
import type { TokenTable } from "./tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* True when `target`, once resolved, is `root` itself or lies inside it.
|
|
4
|
+
* Exact match or proper prefix plus a separator -- never a bare
|
|
5
|
+
* `startsWith`, which would wrongly accept a sibling path that merely shares
|
|
6
|
+
* `root`'s name as a prefix (e.g. `/foo/bar-evil` against `/foo/bar`).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* isPathContained("/foo/bar/x", "/foo/bar"); // true
|
|
11
|
+
* isPathContained("/foo/bar", "/foo/bar"); // true
|
|
12
|
+
* isPathContained("/foo/bar-evil/x", "/foo/bar"); // false
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function isPathContained(target: string, root: string): boolean;
|
|
2
16
|
export interface EmitResult {
|
|
3
17
|
filesWritten: string[];
|
|
4
18
|
}
|
|
5
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Refuses, before anything is written, when any destination that emitting
|
|
21
|
+
* each of `sourceDirs` into `targetDir` would touch is a symlink (dangling
|
|
22
|
+
* or not) or a non-directory where a directory is needed. `targetDir`
|
|
23
|
+
* itself and its ancestors are not checked -- only paths strictly below it.
|
|
24
|
+
* The error names the offending path and ends with fresh mode's single
|
|
25
|
+
* `--fresh --force` retry instruction; nothing is written either way.
|
|
26
|
+
*
|
|
27
|
+
* @throws `Error` naming the first offending destination path.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { assertSafeEmitDestinations, emitTemplate } from "./emit.js";
|
|
32
|
+
*
|
|
33
|
+
* // Validate the baseline AND a pack's payload before writing either.
|
|
34
|
+
* assertSafeEmitDestinations(["/tpl/core", "/tpl/packs/x/files"], "/work/app", {});
|
|
35
|
+
* emitTemplate("/tpl/core", "/work/app", {});
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function assertSafeEmitDestinations(sourceDirs: readonly string[], targetDir: string, tokens: TokenTable): void;
|
|
39
|
+
/**
|
|
40
|
+
* Recursively copies `sourceDir` into `targetDir`, applying `tokens`. Every
|
|
41
|
+
* destination is validated first (see {@link assertSafeEmitDestinations}),
|
|
42
|
+
* so a refusal writes nothing.
|
|
43
|
+
*
|
|
44
|
+
* @throws `Error` naming a symlinked or non-directory destination component.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { emitTemplate } from "./emit.js";
|
|
49
|
+
*
|
|
50
|
+
* const { filesWritten } = emitTemplate("/tpl/core", "/work/app", {
|
|
51
|
+
* PROJECT_NAME: "app",
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
6
55
|
export declare function emitTemplate(sourceDir: string, targetDir: string, tokens: TokenTable): EmitResult;
|
|
7
56
|
//# sourceMappingURL=emit.d.ts.map
|
package/dist/emit.js
CHANGED
|
@@ -1,35 +1,142 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Copies `templates/core` into a target directory, applying token
|
|
3
5
|
* substitution to both file contents and path segments (so a token in a
|
|
4
6
|
* directory or file NAME, not just its content, is honored). A published
|
|
5
7
|
* tarball stores `.gitignore`/`.npmrc` as `_gitignore`/`_npmrc` (npm strips
|
|
6
8
|
* the real names); they are restored here -- see `assets.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Every destination path is planned and `lstat`-checked before the first
|
|
11
|
+
* write ({@link assertSafeEmitDestinations}), so a symlink or a misplaced
|
|
12
|
+
* file below the target refuses the whole emit rather than redirecting a
|
|
13
|
+
* write outside it.
|
|
7
14
|
*/
|
|
8
15
|
import { readdirSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, } from "node:fs";
|
|
9
|
-
import
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { join, relative, dirname, extname, resolve, sep } from "node:path";
|
|
10
18
|
import { restoreDotfilePath } from "./assets.js";
|
|
19
|
+
import { assertDirectoryComponent, assertFileDestination, FRESH_SYMLINK_ADVICE, } from "./fs-guard.js";
|
|
11
20
|
import { applyTokens } from "./tokens.js";
|
|
12
21
|
// Extensions copied byte-for-byte, never text-decoded -- token substitution
|
|
13
22
|
// only makes sense for text content.
|
|
14
23
|
const BINARY_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".ico"]);
|
|
15
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* True when `target`, once resolved, is `root` itself or lies inside it.
|
|
26
|
+
* Exact match or proper prefix plus a separator -- never a bare
|
|
27
|
+
* `startsWith`, which would wrongly accept a sibling path that merely shares
|
|
28
|
+
* `root`'s name as a prefix (e.g. `/foo/bar-evil` against `/foo/bar`).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* isPathContained("/foo/bar/x", "/foo/bar"); // true
|
|
33
|
+
* isPathContained("/foo/bar", "/foo/bar"); // true
|
|
34
|
+
* isPathContained("/foo/bar-evil/x", "/foo/bar"); // false
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function isPathContained(target, root) {
|
|
38
|
+
const resolvedTarget = resolve(target);
|
|
39
|
+
const resolvedRoot = resolve(root);
|
|
40
|
+
return (resolvedTarget === resolvedRoot ||
|
|
41
|
+
resolvedTarget.startsWith(resolvedRoot + sep));
|
|
42
|
+
}
|
|
43
|
+
/** Every destination `sourceDir` would emit under `targetRoot`, in walk order (a directory before its contents). */
|
|
44
|
+
function planEmit(sourceDir, targetRoot, tokens) {
|
|
45
|
+
const entries = [];
|
|
46
|
+
const visit = (currentSourceDir) => {
|
|
47
|
+
for (const entry of readdirSync(currentSourceDir, {
|
|
48
|
+
withFileTypes: true,
|
|
49
|
+
})) {
|
|
50
|
+
const sourcePath = join(currentSourceDir, entry.name);
|
|
51
|
+
const relPath = restoreDotfilePath(applyTokens(relative(sourceDir, sourcePath), tokens));
|
|
52
|
+
const targetPath = join(targetRoot, relPath);
|
|
53
|
+
// CWE-22 invariant (docs/assurance-case.md): a token value substituted
|
|
54
|
+
// into a path segment must never walk the write out of `targetRoot`.
|
|
55
|
+
assert.ok(isPathContained(targetPath, targetRoot), `emitTemplate: target path ${resolve(targetPath)} escapes ${resolve(targetRoot)}`);
|
|
56
|
+
const isDirectory = entry.isDirectory();
|
|
57
|
+
entries.push({ sourcePath, relPath, isDirectory });
|
|
58
|
+
if (isDirectory)
|
|
59
|
+
visit(sourcePath);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
visit(sourceDir);
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `lstat`-checks every destination of `entries` under `targetRoot`: each
|
|
67
|
+
* directory component strictly below `targetRoot` must be missing or a real
|
|
68
|
+
* directory, and each destination file must not be a symlink. Ancestors are
|
|
69
|
+
* checked before descendants, so the topmost offending path is the one named.
|
|
70
|
+
*/
|
|
71
|
+
function assertPlanSafe(targetRoot, entries) {
|
|
72
|
+
const checked = new Set();
|
|
73
|
+
for (const { relPath, isDirectory } of entries) {
|
|
74
|
+
const segments = relPath.split(sep);
|
|
75
|
+
const dirCount = isDirectory ? segments.length : segments.length - 1;
|
|
76
|
+
for (let depth = 1; depth <= dirCount; depth++) {
|
|
77
|
+
const dirPath = join(targetRoot, ...segments.slice(0, depth));
|
|
78
|
+
if (checked.has(dirPath))
|
|
79
|
+
continue;
|
|
80
|
+
checked.add(dirPath);
|
|
81
|
+
assertDirectoryComponent(dirPath, FRESH_SYMLINK_ADVICE);
|
|
82
|
+
}
|
|
83
|
+
if (!isDirectory) {
|
|
84
|
+
assertFileDestination(join(targetRoot, relPath), FRESH_SYMLINK_ADVICE);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refuses, before anything is written, when any destination that emitting
|
|
90
|
+
* each of `sourceDirs` into `targetDir` would touch is a symlink (dangling
|
|
91
|
+
* or not) or a non-directory where a directory is needed. `targetDir`
|
|
92
|
+
* itself and its ancestors are not checked -- only paths strictly below it.
|
|
93
|
+
* The error names the offending path and ends with fresh mode's single
|
|
94
|
+
* `--fresh --force` retry instruction; nothing is written either way.
|
|
95
|
+
*
|
|
96
|
+
* @throws `Error` naming the first offending destination path.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { assertSafeEmitDestinations, emitTemplate } from "./emit.js";
|
|
101
|
+
*
|
|
102
|
+
* // Validate the baseline AND a pack's payload before writing either.
|
|
103
|
+
* assertSafeEmitDestinations(["/tpl/core", "/tpl/packs/x/files"], "/work/app", {});
|
|
104
|
+
* emitTemplate("/tpl/core", "/work/app", {});
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function assertSafeEmitDestinations(sourceDirs, targetDir, tokens) {
|
|
108
|
+
for (const sourceDir of sourceDirs) {
|
|
109
|
+
assertPlanSafe(targetDir, planEmit(sourceDir, targetDir, tokens));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Recursively copies `sourceDir` into `targetDir`, applying `tokens`. Every
|
|
114
|
+
* destination is validated first (see {@link assertSafeEmitDestinations}),
|
|
115
|
+
* so a refusal writes nothing.
|
|
116
|
+
*
|
|
117
|
+
* @throws `Error` naming a symlinked or non-directory destination component.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { emitTemplate } from "./emit.js";
|
|
122
|
+
*
|
|
123
|
+
* const { filesWritten } = emitTemplate("/tpl/core", "/work/app", {
|
|
124
|
+
* PROJECT_NAME: "app",
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
16
128
|
export function emitTemplate(sourceDir, targetDir, tokens) {
|
|
129
|
+
const entries = planEmit(sourceDir, targetDir, tokens);
|
|
130
|
+
assertPlanSafe(targetDir, entries);
|
|
17
131
|
const filesWritten = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function walk(root, currentSourceDir, targetRoot, tokens, filesWritten) {
|
|
22
|
-
for (const entry of readdirSync(currentSourceDir, { withFileTypes: true })) {
|
|
23
|
-
const sourcePath = join(currentSourceDir, entry.name);
|
|
24
|
-
const relPath = restoreDotfilePath(applyTokens(relative(root, sourcePath), tokens));
|
|
25
|
-
const targetPath = join(targetRoot, relPath);
|
|
26
|
-
if (entry.isDirectory()) {
|
|
132
|
+
for (const { sourcePath, relPath, isDirectory } of entries) {
|
|
133
|
+
const targetPath = join(targetDir, relPath);
|
|
134
|
+
if (isDirectory) {
|
|
27
135
|
mkdirSync(targetPath, { recursive: true });
|
|
28
|
-
walk(root, sourcePath, targetRoot, tokens, filesWritten);
|
|
29
136
|
continue;
|
|
30
137
|
}
|
|
31
138
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
32
|
-
if (BINARY_EXTENSIONS.has(extname(
|
|
139
|
+
if (BINARY_EXTENSIONS.has(extname(sourcePath))) {
|
|
33
140
|
copyFileSync(sourcePath, targetPath);
|
|
34
141
|
}
|
|
35
142
|
else {
|
|
@@ -38,5 +145,6 @@ function walk(root, currentSourceDir, targetRoot, tokens, filesWritten) {
|
|
|
38
145
|
}
|
|
39
146
|
filesWritten.push(relPath);
|
|
40
147
|
}
|
|
148
|
+
return { filesWritten };
|
|
41
149
|
}
|
|
42
150
|
//# sourceMappingURL=emit.js.map
|
package/dist/fatal.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports a fatal `error`: sets the exit code first -- 2 when
|
|
3
|
+
* `isUsageError(error)` says it is a usage error, 1 otherwise, including
|
|
4
|
+
* when `isUsageError` itself throws -- then prints the report through
|
|
5
|
+
* `io.print`. For a usage error the report is exactly
|
|
6
|
+
* `formatErrorChain(error)`. For any other failure it is
|
|
7
|
+
* `formatFatalError(error, { withName: true, withStack: debug })` (both in
|
|
8
|
+
* `./format-error.ts`): the same chain, its top line prefixed `<name>: `
|
|
9
|
+
* when `error` is an `Error` with a readable, non-blank `name` other than
|
|
10
|
+
* `Error` and a non-blank message (`TypeError: boom`), and, when `debug` is
|
|
11
|
+
* set, the stack frames appended as `formatFatalError` describes
|
|
12
|
+
* (control-escaped, header dropped, capped at its `MAX_STACK_LINES`). Only
|
|
13
|
+
* the top error's own stack is printed: a `cause`'s stack is deliberately
|
|
14
|
+
* omitted, since the chain already names every cause. If that print
|
|
15
|
+
* throws, the fallbacks never touch `io.print` again (it is the channel
|
|
16
|
+
* that just failed -- in the CLI, the one that paints colour), so `print`
|
|
17
|
+
* is called exactly once: `io.printRaw` prints the same report, so the
|
|
18
|
+
* cause chain (and any name prefix and stack) survives a failing `print`;
|
|
19
|
+
* if that throws, `io.printRaw` prints `String(error.stack ?? error)`,
|
|
20
|
+
* control-escaped the same way the chain is (`escapeControls` in
|
|
21
|
+
* `./format-error.ts`); if that throws, `io.printRaw` prints
|
|
22
|
+
* `[unprintable error]`; if even that throws, gives up silently, the exit
|
|
23
|
+
* code already set. Never throws. A `setExitCode` that throws is swallowed
|
|
24
|
+
* and not retried -- the report is still attempted -- but it is not
|
|
25
|
+
* otherwise handled: assigning Node's `process.exitCode` cannot throw, so
|
|
26
|
+
* the CLI never hits that case.
|
|
27
|
+
*
|
|
28
|
+
* @param error - The value the CLI's `main()` threw.
|
|
29
|
+
* @param io - Where the exit code and the report go: `print` for the
|
|
30
|
+
* formatted chain, `printRaw` -- a plain write with nothing in it that can
|
|
31
|
+
* fail the way `print` did -- for all three fallbacks.
|
|
32
|
+
* @param isUsageError - Whether `error` is a bad invocation rather than a
|
|
33
|
+
* runtime failure.
|
|
34
|
+
* @param debug - Append the top error's own stack (never a cause's) to a
|
|
35
|
+
* runtime failure's report (never to a usage error's); defaults to `false`.
|
|
36
|
+
* The CLI sets it when `M3L_DEBUG` is non-empty.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { CliUsageError, main } from "./main.js";
|
|
41
|
+
* import { handleFatal } from "./fatal.js";
|
|
42
|
+
*
|
|
43
|
+
* try {
|
|
44
|
+
* main(process.argv.slice(2));
|
|
45
|
+
* } catch (error) {
|
|
46
|
+
* handleFatal(
|
|
47
|
+
* error,
|
|
48
|
+
* {
|
|
49
|
+
* setExitCode: (code) => {
|
|
50
|
+
* process.exitCode = code;
|
|
51
|
+
* },
|
|
52
|
+
* print: (text) => {
|
|
53
|
+
* console.error(text);
|
|
54
|
+
* },
|
|
55
|
+
* printRaw: (text) => {
|
|
56
|
+
* console.error(text);
|
|
57
|
+
* },
|
|
58
|
+
* },
|
|
59
|
+
* (e) => e instanceof CliUsageError,
|
|
60
|
+
* (process.env["M3L_DEBUG"] ?? "") !== "",
|
|
61
|
+
* );
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function handleFatal(error: unknown, io: {
|
|
66
|
+
setExitCode(code: number): void;
|
|
67
|
+
print(text: string): void;
|
|
68
|
+
printRaw(text: string): void;
|
|
69
|
+
}, isUsageError: (error: unknown) => boolean, debug?: boolean): void;
|
|
70
|
+
//# sourceMappingURL=fatal.d.ts.map
|