@monte3l/groundwork 1.0.0-rc.2 → 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 +13 -5
- 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
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Renders a thrown value and its full `cause` chain for the terminal --
|
|
5
|
+
* what `bin/m3l-groundwork.mjs` prints instead of a bare `error.message`,
|
|
6
|
+
* which would silently drop every chained cause.
|
|
7
|
+
*/
|
|
8
|
+
/** Rendered in place of a value that cannot be read or stringified (a throwing getter, a null-prototype object, a `Symbol` message, a revoked Proxy). */
|
|
9
|
+
const UNPRINTABLE = "[unprintable value]";
|
|
10
|
+
/** Rendered in place of an object already printed elsewhere in the output (not an ancestor, so not a cycle). */
|
|
11
|
+
const SEE_ABOVE = "(see above)";
|
|
12
|
+
/** Rendered as the top line in place of a blank message when there is no `Error` name to show instead. */
|
|
13
|
+
const EMPTY_MESSAGE = "(empty message)";
|
|
14
|
+
/**
|
|
15
|
+
* Most links followed along any one branch -- printed and suppressed links
|
|
16
|
+
* alike -- before that branch ends with a `...` line.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_DEPTH = 32;
|
|
19
|
+
/** Most `errors` members printed per parent before the rest collapse into one `... and N more` line; the `cause` sits outside this cap. */
|
|
20
|
+
const MAX_CHILDREN = 32;
|
|
21
|
+
/** Shortest cause message that is suppressed merely for appearing inside its parent's message. */
|
|
22
|
+
const MIN_SUBSTRING_LENGTH = 8;
|
|
23
|
+
/**
|
|
24
|
+
* Stands in for a child whose `cause`/`errors` accessor threw. A fresh
|
|
25
|
+
* instance per occurrence, so two unreadable children are never mistaken
|
|
26
|
+
* for one repeated value.
|
|
27
|
+
*/
|
|
28
|
+
class Unreadable {
|
|
29
|
+
}
|
|
30
|
+
/** Classifies `value`; an `instanceof` check that throws (a Proxy's `getPrototypeOf` trap, a revoked Proxy) makes it unreadable. */
|
|
31
|
+
function inspect(value) {
|
|
32
|
+
try {
|
|
33
|
+
if (value instanceof Unreadable) {
|
|
34
|
+
return { kind: "unreadable" };
|
|
35
|
+
}
|
|
36
|
+
if (value instanceof AggregateError) {
|
|
37
|
+
return { kind: "aggregate", value };
|
|
38
|
+
}
|
|
39
|
+
if (value instanceof Error) {
|
|
40
|
+
return { kind: "error", value };
|
|
41
|
+
}
|
|
42
|
+
return { kind: "other", value };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// The formatter runs while reporting another failure; a value that
|
|
46
|
+
// cannot even be classified must not replace that report with its own.
|
|
47
|
+
return { kind: "unreadable" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The value's own message (unindented, possibly multi-line), or {@link UNPRINTABLE} when reading or stringifying it fails. */
|
|
51
|
+
function messageOf(node) {
|
|
52
|
+
try {
|
|
53
|
+
let raw;
|
|
54
|
+
switch (node.kind) {
|
|
55
|
+
case "unreadable":
|
|
56
|
+
return UNPRINTABLE;
|
|
57
|
+
case "aggregate":
|
|
58
|
+
case "error":
|
|
59
|
+
// Read once: an accessor may answer differently on every read.
|
|
60
|
+
raw = node.value.message;
|
|
61
|
+
break;
|
|
62
|
+
case "other":
|
|
63
|
+
raw = node.value;
|
|
64
|
+
break;
|
|
65
|
+
default: {
|
|
66
|
+
const exhaustive = node;
|
|
67
|
+
return String(exhaustive);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// String(symbol) would succeed, but a Symbol is not a message.
|
|
71
|
+
return typeof raw === "symbol" ? UNPRINTABLE : String(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Same rationale as inspect(): never let the report itself throw.
|
|
75
|
+
return UNPRINTABLE;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** `Array.isArray`, narrowing to `unknown[]` rather than `any[]`. */
|
|
79
|
+
function isArray(value) {
|
|
80
|
+
return Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
/** Whether `value` has an identity worth tracking: only an object or a function can repeat or form a cycle. */
|
|
83
|
+
function isObjectLike(value) {
|
|
84
|
+
return ((typeof value === "object" && value !== null) || typeof value === "function");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The values a node chains to: its `cause` (when set), then an
|
|
88
|
+
* `AggregateError`'s `errors` (when really an array), capped at
|
|
89
|
+
* {@link MAX_CHILDREN} members. The cause is reserved outside that cap, so
|
|
90
|
+
* no number of members can hide it. An accessor that throws yields an
|
|
91
|
+
* {@link Unreadable} child in its place.
|
|
92
|
+
*/
|
|
93
|
+
function childrenOf(node) {
|
|
94
|
+
if (node.kind !== "aggregate" && node.kind !== "error") {
|
|
95
|
+
return { kept: [], omitted: 0 };
|
|
96
|
+
}
|
|
97
|
+
const kept = [];
|
|
98
|
+
let omitted = 0;
|
|
99
|
+
try {
|
|
100
|
+
const cause = node.value.cause;
|
|
101
|
+
if (cause !== undefined) {
|
|
102
|
+
kept.push(cause);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
kept.push(new Unreadable());
|
|
107
|
+
}
|
|
108
|
+
if (node.kind === "aggregate") {
|
|
109
|
+
try {
|
|
110
|
+
const errors = node.value.errors;
|
|
111
|
+
if (isArray(errors)) {
|
|
112
|
+
// Read only the entries that can be printed; a huge array is
|
|
113
|
+
// counted by its length, never walked.
|
|
114
|
+
const length = errors.length;
|
|
115
|
+
const take = Math.min(length, MAX_CHILDREN);
|
|
116
|
+
for (let i = 0; i < take; i++) {
|
|
117
|
+
kept.push(errors[i]);
|
|
118
|
+
}
|
|
119
|
+
omitted = length - take;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
kept.push(new Unreadable());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { kept, omitted };
|
|
127
|
+
}
|
|
128
|
+
/** Whether `message` adds nothing to `parentMessage` and need not be printed again. */
|
|
129
|
+
function isRedundant(message, parentMessage) {
|
|
130
|
+
if (message.trim() === "") {
|
|
131
|
+
// Every string "includes" the empty string, and an all-whitespace
|
|
132
|
+
// message would "equal" any other one after trim().
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return (message.trim() === parentMessage.trim() ||
|
|
136
|
+
(message.length >= MIN_SUBSTRING_LENGTH && parentMessage.includes(message)));
|
|
137
|
+
}
|
|
138
|
+
/** Whether `value` already appears on the `ancestors` path -- a genuine cycle. The path is at most {@link MAX_DEPTH} + 1 long. */
|
|
139
|
+
function isAncestor(value, ancestors) {
|
|
140
|
+
for (let link = ancestors; link !== undefined; link = link.parent) {
|
|
141
|
+
if (link.value === value) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/** Pushes `node`'s child visits onto `stack`, reversed so they pop in their original order, the `... and N more` marker last. */
|
|
148
|
+
function pushVisits(stack, node, parentMessage, depth, steps, ancestors) {
|
|
149
|
+
const cut = { done: false };
|
|
150
|
+
const { kept, omitted } = childrenOf(node);
|
|
151
|
+
if (omitted > 0) {
|
|
152
|
+
stack.push({ kind: "more", omitted, depth, steps, cut });
|
|
153
|
+
}
|
|
154
|
+
for (let i = kept.length - 1; i >= 0; i--) {
|
|
155
|
+
stack.push({
|
|
156
|
+
kind: "child",
|
|
157
|
+
value: kept[i],
|
|
158
|
+
parentMessage,
|
|
159
|
+
depth,
|
|
160
|
+
steps,
|
|
161
|
+
cut,
|
|
162
|
+
ancestors,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Code points that end a line: LF, VT, FF, CR (a CR immediately followed by LF is one break), NEL, LS, PS. */
|
|
167
|
+
const LINE_BREAKS = new Set([
|
|
168
|
+
0x0a, 0x0b, 0x0c, 0x0d, 0x85, 0x2028, 0x2029,
|
|
169
|
+
]);
|
|
170
|
+
/** `text` split on every {@link LINE_BREAKS} member, `\r\n` counting as one break; empty lines are kept. */
|
|
171
|
+
function splitLines(text) {
|
|
172
|
+
const lines = [];
|
|
173
|
+
let start = 0;
|
|
174
|
+
for (let i = 0; i < text.length; i++) {
|
|
175
|
+
const code = text.charCodeAt(i);
|
|
176
|
+
if (!LINE_BREAKS.has(code)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
lines.push(text.slice(start, i));
|
|
180
|
+
if (code === 0x0d && text.charCodeAt(i + 1) === 0x0a) {
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
start = i + 1;
|
|
184
|
+
}
|
|
185
|
+
lines.push(text.slice(start));
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Whether code unit `code` is a bidi embedding/override (U+202A-U+202E: LRE,
|
|
190
|
+
* RLE, PDF, LRO, RLO) or isolate (U+2066-U+2069: LRI, RLI, FSI, PDI) control
|
|
191
|
+
* -- the Trojan Source class (CVE-2021-42574), which can make a terminal
|
|
192
|
+
* display text in an order other than its bytes.
|
|
193
|
+
*/
|
|
194
|
+
function isBidiControl(code) {
|
|
195
|
+
return ((code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether code unit `code` is escaped: a C0 control other than TAB
|
|
199
|
+
* (U+0000-U+001F except U+0009), DEL (U+007F), a C1 control other than
|
|
200
|
+
* NEL (U+0080-U+009F except U+0085), or an {@link isBidiControl} code unit.
|
|
201
|
+
* Every one of these is a single UTF-16 code unit, so a surrogate half never
|
|
202
|
+
* matches.
|
|
203
|
+
*/
|
|
204
|
+
function isEscapedControl(code) {
|
|
205
|
+
return ((code <= 0x1f && code !== 0x09) ||
|
|
206
|
+
code === 0x7f ||
|
|
207
|
+
(code >= 0x80 && code <= 0x9f && code !== 0x85) ||
|
|
208
|
+
isBidiControl(code));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* `line` with every {@link isEscapedControl} code unit replaced by a literal
|
|
212
|
+
* escape in lowercase hex: `\uNNNN` (four digits) for a bidi control,
|
|
213
|
+
* `\xNN` (two digits) for every other one.
|
|
214
|
+
*/
|
|
215
|
+
function escapeLine(line) {
|
|
216
|
+
let out = "";
|
|
217
|
+
let start = 0;
|
|
218
|
+
for (let i = 0; i < line.length; i++) {
|
|
219
|
+
const code = line.charCodeAt(i);
|
|
220
|
+
if (isEscapedControl(code)) {
|
|
221
|
+
const hex = code.toString(16);
|
|
222
|
+
const escape = isBidiControl(code)
|
|
223
|
+
? `\\u${hex.padStart(4, "0")}`
|
|
224
|
+
: `\\x${hex.padStart(2, "0")}`;
|
|
225
|
+
out += `${line.slice(start, i)}${escape}`;
|
|
226
|
+
start = i + 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out + line.slice(start);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Makes `text` safe to write to a terminal: every line break it contains
|
|
233
|
+
* (`\r\n`, `\n`, a lone `\r`, `\v`, `\f`, U+0085, U+2028, U+2029) becomes a
|
|
234
|
+
* plain `\n`, and every other C0 control except TAB (U+0000-U+001F except
|
|
235
|
+
* U+0009), DEL (U+007F) and every C1 control except NEL (U+0080-U+009F
|
|
236
|
+
* except U+0085) is replaced by the literal text `\xNN`, lowercase two-digit
|
|
237
|
+
* hex -- so an ESC renders as `\x1b` and cannot start an escape sequence.
|
|
238
|
+
* Every bidi embedding/override and isolate control (U+202A-U+202E,
|
|
239
|
+
* U+2066-U+2069) is replaced by the literal text `\uNNNN`, lowercase
|
|
240
|
+
* four-digit hex -- so an RLO renders as `\u202e` and cannot reorder what
|
|
241
|
+
* the terminal displays. TAB and every other character pass through
|
|
242
|
+
* unchanged.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* import { escapeControls } from "./format-error.js";
|
|
247
|
+
*
|
|
248
|
+
* escapeControls("bad\u001b[2J\rline two"); // "bad\\x1b[2J\nline two"
|
|
249
|
+
* escapeControls("bad\u202eexe.txt"); // "bad\\u202eexe.txt"
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function escapeControls(text) {
|
|
253
|
+
return splitLines(text).map(escapeLine).join("\n");
|
|
254
|
+
}
|
|
255
|
+
/** Whether `message` would render as nothing but blank lines. */
|
|
256
|
+
function isBlank(message) {
|
|
257
|
+
return splitLines(message).every((line) => line.trim() === "");
|
|
258
|
+
}
|
|
259
|
+
/** An `Error`'s `name` when it is a readable, non-blank string (read once, safely), otherwise `undefined`. */
|
|
260
|
+
function readableName(node) {
|
|
261
|
+
if (node.kind === "aggregate" || node.kind === "error") {
|
|
262
|
+
try {
|
|
263
|
+
const name = node.value.name;
|
|
264
|
+
if (typeof name === "string" && !isBlank(name)) {
|
|
265
|
+
return name;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Same rationale as inspect(): never let the report itself throw.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/** What the top line shows in place of a blank message: an `Error`'s {@link readableName}, otherwise `(empty message)`. */
|
|
275
|
+
function blankLabel(node) {
|
|
276
|
+
return readableName(node) ?? EMPTY_MESSAGE;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The `Name: ` prefix {@link formatFatalError} gives the top line: an
|
|
280
|
+
* `Error`'s {@link readableName} other than the generic `Error` (which adds
|
|
281
|
+
* nothing), every line break in it rendered as a literal `\n` and every
|
|
282
|
+
* other control escaped as {@link escapeControls} describes, so the prefix
|
|
283
|
+
* stays on one line; `""` when there is no such name.
|
|
284
|
+
*/
|
|
285
|
+
function namePrefix(node) {
|
|
286
|
+
const name = readableName(node);
|
|
287
|
+
if (name === undefined || name === "Error") {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
return `${splitLines(name).map(escapeLine).join("\\n")}: `;
|
|
291
|
+
}
|
|
292
|
+
/** Most `stack` lines {@link formatFatalError} prints before the rest collapse into one `... and N more` line. */
|
|
293
|
+
const MAX_STACK_LINES = 50;
|
|
294
|
+
/** Whether `line` looks like a V8 stack frame (` at ...`). */
|
|
295
|
+
function isFrameLine(line) {
|
|
296
|
+
return /^\s*at /.test(line);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The header V8 puts on an `Error`'s `stack`, built the way
|
|
300
|
+
* `Error.prototype.toString` builds it (`Name: message`, just the name when
|
|
301
|
+
* the message is empty, just the message when the name is), or `undefined`
|
|
302
|
+
* when `name` or `message` cannot be read or is neither a string nor
|
|
303
|
+
* `undefined`.
|
|
304
|
+
*/
|
|
305
|
+
function stackHeader(error) {
|
|
306
|
+
let rawName;
|
|
307
|
+
let rawMessage;
|
|
308
|
+
try {
|
|
309
|
+
rawName = error.name;
|
|
310
|
+
rawMessage = error.message;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Same rationale as inspect(): never let the report itself throw.
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
const name = rawName === undefined ? "Error" : rawName;
|
|
317
|
+
const message = rawMessage === undefined ? "" : rawMessage;
|
|
318
|
+
if (typeof name !== "string" || typeof message !== "string") {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
if (name === "") {
|
|
322
|
+
return message;
|
|
323
|
+
}
|
|
324
|
+
return message === "" ? name : `${name}: ${message}`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* `raw` (a stack's lines) without its header: as many leading lines as
|
|
328
|
+
* {@link stackHeader} occupies when the stack starts with exactly those
|
|
329
|
+
* lines -- so a message line shaped like a frame is still dropped --
|
|
330
|
+
* otherwise every line before the first {@link isFrameLine}, or none when
|
|
331
|
+
* there is no frame line.
|
|
332
|
+
*/
|
|
333
|
+
function withoutHeader(raw, error) {
|
|
334
|
+
const header = stackHeader(error);
|
|
335
|
+
if (header !== undefined) {
|
|
336
|
+
const headerLines = splitLines(header);
|
|
337
|
+
if (headerLines.every((line, i) => raw[i] === line)) {
|
|
338
|
+
return raw.slice(headerLines.length);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const firstFrame = raw.findIndex(isFrameLine);
|
|
342
|
+
return firstFrame === -1 ? [...raw] : raw.slice(firstFrame);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The top value's `stack` as printable lines: read once, only off an
|
|
346
|
+
* `Error`, and only when it is a string (a throwing getter or a non-string
|
|
347
|
+
* yields none). V8's `Name: message` header, already printed as the chain's
|
|
348
|
+
* top line, is dropped by position (see {@link withoutHeader}), falling
|
|
349
|
+
* back to dropping the lines before the first `at ` frame when the stack
|
|
350
|
+
* does not start with that header; a stack with neither is kept whole.
|
|
351
|
+
* Trailing empty lines are dropped, every line is control-escaped like a
|
|
352
|
+
* message, and at most {@link MAX_STACK_LINES} are kept, the rest
|
|
353
|
+
* collapsing into one ` ... and N more` line.
|
|
354
|
+
*/
|
|
355
|
+
function stackLines(node) {
|
|
356
|
+
if (node.kind !== "aggregate" && node.kind !== "error") {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
let stack;
|
|
360
|
+
try {
|
|
361
|
+
stack = node.value.stack;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Same rationale as inspect(): never let the report itself throw.
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
if (typeof stack !== "string") {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
const lines = withoutHeader(splitLines(stack), node.value).map(escapeLine);
|
|
371
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
372
|
+
lines.pop();
|
|
373
|
+
}
|
|
374
|
+
if (lines.length <= MAX_STACK_LINES) {
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
377
|
+
const omitted = lines.length - MAX_STACK_LINES;
|
|
378
|
+
return [
|
|
379
|
+
...lines.slice(0, MAX_STACK_LINES),
|
|
380
|
+
` ... and ${String(omitted)} more`,
|
|
381
|
+
];
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* `message` as lines (split on every line break {@link escapeControls}
|
|
385
|
+
* recognizes, trailing empty lines dropped, each line control-escaped), the
|
|
386
|
+
* first prefixed with `head`, every further line indented one level past
|
|
387
|
+
* `depth` and marked `| ` -- a bare `|` when the line is empty -- so no
|
|
388
|
+
* continuation line can ever equal a real `caused by:` line.
|
|
389
|
+
*/
|
|
390
|
+
function messageLines(message, depth, head) {
|
|
391
|
+
const [first = "", ...rest] = splitLines(message).map(escapeLine);
|
|
392
|
+
while (rest.length > 0 && rest[rest.length - 1] === "") {
|
|
393
|
+
rest.pop();
|
|
394
|
+
}
|
|
395
|
+
const continuation = `${" ".repeat(depth + 1)}|`;
|
|
396
|
+
return [
|
|
397
|
+
`${head}${first}`,
|
|
398
|
+
...rest.map((line) => line === "" ? continuation : `${continuation} ${line}`),
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
/** `message` as a `caused by:` line at `depth`, every further line indented one level deeper and marked `| `. */
|
|
402
|
+
function causedByLines(message, depth) {
|
|
403
|
+
return messageLines(message, depth, `${" ".repeat(depth)}caused by: `);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Formats `error` as a multi-line string: its own message on the first line
|
|
407
|
+
* (any further line of it indented two spaces), then one
|
|
408
|
+
* `caused by: <message>` line per chained cause, indented two more spaces
|
|
409
|
+
* per depth; any further line of a cause's message is indented two spaces
|
|
410
|
+
* past its own `caused by:` line. Every such continuation line is marked
|
|
411
|
+
* `| ` after its indent (an empty one prints a bare `|`), so no continuation
|
|
412
|
+
* line can pass for a real `caused by:` line; the top message's first line
|
|
413
|
+
* is printed unmarked (but control-escaped, below). Messages split on
|
|
414
|
+
* `\r\n`, `\n`, a lone `\r`, `\v`, `\f`, U+0085, U+2028 or U+2029; trailing
|
|
415
|
+
* empty lines are dropped. Every line of every message -- the top message's
|
|
416
|
+
* first line, any cause at any depth, a non-`Error`'s `String(value)` -- is
|
|
417
|
+
* then control-escaped exactly as {@link escapeControls} describes (every C0
|
|
418
|
+
* control but TAB, DEL, every C1 control but NEL become `\xNN`; every bidi
|
|
419
|
+
* control in U+202A-U+202E and U+2066-U+2069 becomes `\uNNNN`), so no
|
|
420
|
+
* message can emit a terminal escape sequence. A top message that is empty
|
|
421
|
+
* or whitespace-only renders as the `Error`'s `name` instead (when that is
|
|
422
|
+
* a readable, non-blank string), otherwise as `(empty message)`, so the
|
|
423
|
+
* first line is never blank. An `AggregateError`'s `errors` are each listed
|
|
424
|
+
* as a `caused by:` line at the next depth (after its own `cause`, if any);
|
|
425
|
+
* an `errors` property that is not an array is ignored. At most 32 `errors`
|
|
426
|
+
* members are printed per parent; the rest collapse into one
|
|
427
|
+
* `... and N more` line at the children's indent. A `cause` never counts
|
|
428
|
+
* against that cap, so it is always printed.
|
|
429
|
+
*
|
|
430
|
+
* A message is an `Error`'s `message`, otherwise `String(value)`; it renders
|
|
431
|
+
* as `[unprintable value]` when stringifying throws, when it is a `Symbol`,
|
|
432
|
+
* when the `message`, `cause` or `errors` accessor itself throws, or when the
|
|
433
|
+
* value cannot even be classified (a Proxy whose `getPrototypeOf` trap
|
|
434
|
+
* throws, a revoked Proxy) -- such a value has no children.
|
|
435
|
+
*
|
|
436
|
+
* A cause is not printed again (its own causes still are, at the same
|
|
437
|
+
* indent) when its message is not empty or whitespace-only and either equals
|
|
438
|
+
* the nearest printed ancestor's message after `trim()` or is at least 8
|
|
439
|
+
* characters long and contained in it.
|
|
440
|
+
*
|
|
441
|
+
* An object (or function) that is its own ancestor -- a cause cycle -- is
|
|
442
|
+
* cut silently at the repeat; one already printed on another branch renders
|
|
443
|
+
* as `caused by: (see above)`. Primitives are never deduplicated. Every
|
|
444
|
+
* branch follows at most 32 links -- suppressed links count too -- and a
|
|
445
|
+
* parent whose children are cut there gets exactly one `...` line, at the
|
|
446
|
+
* cut point, with any sibling branches still printed after it. The walk is
|
|
447
|
+
* iterative and bounded, so the function always terminates and never
|
|
448
|
+
* throws, however long the chain.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* import { formatErrorChain } from "./format-error.js";
|
|
453
|
+
*
|
|
454
|
+
* const error = new Error("staging failed", { cause: new Error("ENOSPC") });
|
|
455
|
+
* formatErrorChain(error);
|
|
456
|
+
* // "staging failed\n caused by: ENOSPC"
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
export function formatErrorChain(error) {
|
|
460
|
+
return renderChain(error, false);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Formats `error` for the CLI's fatal-error report: exactly
|
|
464
|
+
* {@link formatErrorChain}'s text, with two optional additions.
|
|
465
|
+
*
|
|
466
|
+
* - `withName`: when `error` is an `Error` whose `name` is a readable,
|
|
467
|
+
* non-blank string other than the generic `Error`, and its message is not
|
|
468
|
+
* blank, the top line is prefixed `<name>: ` (`TypeError: boom`), the
|
|
469
|
+
* name control-escaped like a message and any line break in it rendered
|
|
470
|
+
* as a literal `\n`. A blank message already renders as the name alone,
|
|
471
|
+
* so it gets no prefix; `caused by:` lines never do.
|
|
472
|
+
* - `withStack`: when `error` is an `Error` whose `stack` (read once) is a
|
|
473
|
+
* string, its lines are appended after the chain -- minus V8's
|
|
474
|
+
* `Name: message` header, which repeats the top line. The header is
|
|
475
|
+
* dropped by position: as many leading lines as `Name: message` spans
|
|
476
|
+
* when the stack starts with exactly those lines (so a message line
|
|
477
|
+
* shaped like an `at ` frame never reappears), otherwise every line
|
|
478
|
+
* before the first `at ` frame. Each line is control-escaped like a
|
|
479
|
+
* message, at most {@link MAX_STACK_LINES} of them, the rest collapsing
|
|
480
|
+
* into one `... and N more` line. A missing, non-string or throwing
|
|
481
|
+
* `stack` appends nothing.
|
|
482
|
+
*
|
|
483
|
+
* Never throws, like {@link formatErrorChain}.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* import { formatFatalError } from "./format-error.js";
|
|
488
|
+
*
|
|
489
|
+
* formatFatalError(new TypeError("bad input"), { withName: true, withStack: false });
|
|
490
|
+
* // "TypeError: bad input"
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
export function formatFatalError(error, options) {
|
|
494
|
+
const chain = renderChain(error, options.withName);
|
|
495
|
+
const stack = options.withStack ? stackLines(inspect(error)) : [];
|
|
496
|
+
return stack.length === 0 ? chain : `${chain}\n${stack.join("\n")}`;
|
|
497
|
+
}
|
|
498
|
+
/** {@link formatErrorChain}'s rendering, its top line prefixed with {@link namePrefix} when `withName` is set and the top message is not blank. */
|
|
499
|
+
function renderChain(error, withName) {
|
|
500
|
+
const top = inspect(error);
|
|
501
|
+
const topMessage = messageOf(top);
|
|
502
|
+
const blank = isBlank(topMessage);
|
|
503
|
+
// Display only: suppression below still compares against the real message.
|
|
504
|
+
const lines = messageLines(blank ? blankLabel(top) : topMessage, 0, withName && !blank ? namePrefix(top) : "");
|
|
505
|
+
const seen = new Set();
|
|
506
|
+
if (isObjectLike(error)) {
|
|
507
|
+
seen.add(error);
|
|
508
|
+
}
|
|
509
|
+
const stack = [];
|
|
510
|
+
pushVisits(stack, top, topMessage, 1, 1, {
|
|
511
|
+
value: error,
|
|
512
|
+
parent: undefined,
|
|
513
|
+
});
|
|
514
|
+
for (let visit = stack.pop(); visit !== undefined; visit = stack.pop()) {
|
|
515
|
+
const { depth, steps, cut } = visit;
|
|
516
|
+
const indent = " ".repeat(depth);
|
|
517
|
+
if (visit.kind === "child" && isAncestor(visit.value, visit.ancestors)) {
|
|
518
|
+
// A cycle: everything from here on is already being printed above.
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (steps > MAX_DEPTH) {
|
|
522
|
+
if (!cut.done) {
|
|
523
|
+
cut.done = true;
|
|
524
|
+
lines.push(`${indent}...`);
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (visit.kind === "more") {
|
|
529
|
+
lines.push(`${indent}... and ${String(visit.omitted)} more`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const { value, parentMessage, ancestors } = visit;
|
|
533
|
+
if (isObjectLike(value)) {
|
|
534
|
+
if (seen.has(value)) {
|
|
535
|
+
lines.push(`${indent}caused by: ${SEE_ABOVE}`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
seen.add(value);
|
|
539
|
+
}
|
|
540
|
+
const node = inspect(value);
|
|
541
|
+
const message = messageOf(node);
|
|
542
|
+
const path = { value, parent: ancestors };
|
|
543
|
+
if (isRedundant(message, parentMessage)) {
|
|
544
|
+
// Already printed as part of the parent's message.
|
|
545
|
+
pushVisits(stack, node, parentMessage, depth, steps + 1, path);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
lines.push(...causedByLines(message, depth));
|
|
549
|
+
pushVisits(stack, node, message, depth + 1, steps + 1, path);
|
|
550
|
+
}
|
|
551
|
+
return lines.join("\n");
|
|
552
|
+
}
|
|
553
|
+
//# sourceMappingURL=format-error.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopt mode's generic re-run instruction, stated once so every adopt-mode
|
|
3
|
+
* failure that appends it (`main.ts`'s post-point-of-no-return error, the
|
|
4
|
+
* guarded `/customize` install) words it identically. Ends with the tail
|
|
5
|
+
* {@link endsWithRerunAdvice} recognises.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { FIX_AND_RERUN_ADVICE, endsWithRerunAdvice } from "./fs-guard.js";
|
|
10
|
+
*
|
|
11
|
+
* const message = `could not write x: EACCES; ${FIX_AND_RERUN_ADVICE}`;
|
|
12
|
+
* endsWithRerunAdvice(message); // true
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const FIX_AND_RERUN_ADVICE = "fix the cause and re-run the CLI";
|
|
16
|
+
/**
|
|
17
|
+
* Whether `message` already ENDS with "re-run the CLI" advice, in any
|
|
18
|
+
* wording that ends that way (e.g. {@link assertNotSymlink}'s "remove it and
|
|
19
|
+
* re-run the CLI", or "fix the cause and re-run the CLI"), so a caller about
|
|
20
|
+
* to append its own re-run advice can skip it rather than state it twice.
|
|
21
|
+
* End-anchored: the phrase appearing earlier in the message -- e.g. inside
|
|
22
|
+
* an embedded path -- does not count.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { endsWithRerunAdvice } from "./fs-guard.js";
|
|
27
|
+
*
|
|
28
|
+
* endsWithRerunAdvice("x is a symlink -- remove it and re-run the CLI"); // true
|
|
29
|
+
* endsWithRerunAdvice("could not write /tmp/re-run the CLI/a: EACCES"); // false
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function endsWithRerunAdvice(message: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Fresh mode's retry instruction. Its target is no longer empty after a
|
|
35
|
+
* failed write, so a plain re-run would adopt it; only `--fresh --force`
|
|
36
|
+
* repeats that run. Never contains adopt mode's bare "re-run the CLI".
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { FRESH_RETRY } from "./fs-guard.js";
|
|
41
|
+
*
|
|
42
|
+
* const advice = `fix the cause, then ${FRESH_RETRY}`;
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare const FRESH_RETRY = "retry the same command with --fresh --force added";
|
|
46
|
+
/**
|
|
47
|
+
* Throws when `path` exists and is a symbolic link; a missing path passes.
|
|
48
|
+
* Uses `lstat`, so the link itself is inspected, never its target. Call it
|
|
49
|
+
* on every staging directory before the first `rm` or write under it.
|
|
50
|
+
*
|
|
51
|
+
* @param advice - What the message ends with after `--`; defaults to
|
|
52
|
+
* "remove it and re-run the CLI". A caller whose run needs a different
|
|
53
|
+
* retry (fresh mode's `--fresh --force`) passes its own, so the error
|
|
54
|
+
* never carries a second, contradicting instruction.
|
|
55
|
+
* @throws `Error` naming `path` when it is a symlink, or when it cannot be
|
|
56
|
+
* inspected at all (any `lstat` failure but `ENOENT`/`ENOTDIR`, chained
|
|
57
|
+
* as `cause`).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { rmSync } from "node:fs";
|
|
62
|
+
* import { assertNotSymlink } from "./fs-guard.js";
|
|
63
|
+
*
|
|
64
|
+
* assertNotSymlink("/work/app/.groundwork"); // throws if it's a symlink
|
|
65
|
+
* rmSync("/work/app/.groundwork/inventory.json", { force: true });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function assertNotSymlink(path: string, advice?: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Throws when `path` exists and is not a real directory -- a symlink
|
|
71
|
+
* (dangling or not) or a file where a writer needs a directory. A missing
|
|
72
|
+
* path passes. Uses `lstat`, so a symlinked directory is refused rather
|
|
73
|
+
* than followed out of the tree being written.
|
|
74
|
+
*
|
|
75
|
+
* @param advice - What the message ends with after `--`, same convention as
|
|
76
|
+
* {@link assertNotSymlink}'s.
|
|
77
|
+
* @throws `Error` naming `path` when it is a symlink or a non-directory, or
|
|
78
|
+
* when it cannot be inspected (the original chained as `cause`).
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { assertDirectoryComponent, FRESH_SYMLINK_ADVICE } from "./fs-guard.js";
|
|
83
|
+
*
|
|
84
|
+
* assertDirectoryComponent("/work/app/.claude", FRESH_SYMLINK_ADVICE);
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function assertDirectoryComponent(path: string, advice: string): void;
|
|
88
|
+
/**
|
|
89
|
+
* Throws when `path` exists and cannot be written as a plain file -- a
|
|
90
|
+
* symlink (dangling or not), which a write would follow, or a directory,
|
|
91
|
+
* which a write would fail on only after earlier writes had landed. A
|
|
92
|
+
* missing path or an existing regular file passes. Uses `lstat`, so the
|
|
93
|
+
* entry itself is inspected, never a link's target.
|
|
94
|
+
*
|
|
95
|
+
* @param advice - What the message ends with after `--`, same convention as
|
|
96
|
+
* {@link assertNotSymlink}'s.
|
|
97
|
+
* @throws `Error` naming `path` when it is a symlink or a directory, or
|
|
98
|
+
* when it cannot be inspected (the original chained as `cause`).
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { assertFileDestination, FRESH_SYMLINK_ADVICE } from "./fs-guard.js";
|
|
103
|
+
*
|
|
104
|
+
* assertFileDestination("/work/app/tsconfig.json", FRESH_SYMLINK_ADVICE);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function assertFileDestination(path: string, advice: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* Throws when `path` exists and is a real directory, which a file write
|
|
110
|
+
* would fail on only after earlier writes had landed. A missing path, a
|
|
111
|
+
* file, or a symlink passes -- for a destination whose writer replaces a
|
|
112
|
+
* symlink rather than following it (fresh mode's `/customize` install),
|
|
113
|
+
* this is the one shape left to refuse up front. Uses `lstat`.
|
|
114
|
+
*
|
|
115
|
+
* @param advice - What the message ends with after `--`, same convention as
|
|
116
|
+
* {@link assertNotSymlink}'s.
|
|
117
|
+
* @throws `Error` naming `path` when it is a directory, or when it cannot be
|
|
118
|
+
* inspected (the original chained as `cause`).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { assertNotDirectory, FRESH_SYMLINK_ADVICE } from "./fs-guard.js";
|
|
123
|
+
*
|
|
124
|
+
* assertNotDirectory("/work/app/.claude/skills/customize/SKILL.md", FRESH_SYMLINK_ADVICE);
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare function assertNotDirectory(path: string, advice: string): void;
|
|
128
|
+
/**
|
|
129
|
+
* Fresh mode's advice for a refused destination path (a symlink, or a
|
|
130
|
+
* non-directory where a directory is needed), replacing
|
|
131
|
+
* {@link assertNotSymlink}'s adopt-mode default. Carries {@link FRESH_RETRY}
|
|
132
|
+
* exactly once.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { assertNotSymlink, FRESH_SYMLINK_ADVICE } from "./fs-guard.js";
|
|
137
|
+
*
|
|
138
|
+
* assertNotSymlink("/work/app/package.json", FRESH_SYMLINK_ADVICE);
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export declare const FRESH_SYMLINK_ADVICE = "remove it, then retry the same command with --fresh --force added";
|
|
142
|
+
//# sourceMappingURL=fs-guard.d.ts.map
|