@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.
Files changed (170) hide show
  1. package/README.md +13 -5
  2. package/bin/m3l-groundwork.mjs +36 -2
  3. package/dist/assets.d.ts +10 -0
  4. package/dist/assets.js +14 -0
  5. package/dist/baseline-stage.d.ts +173 -0
  6. package/dist/baseline-stage.js +215 -0
  7. package/dist/caps.d.ts +4 -1
  8. package/dist/caps.js +17 -3
  9. package/dist/conflicts.d.ts +23 -2
  10. package/dist/conflicts.js +89 -11
  11. package/dist/customize-paths.d.ts +92 -0
  12. package/dist/customize-paths.js +115 -0
  13. package/dist/emit.d.ts +50 -1
  14. package/dist/emit.js +121 -13
  15. package/dist/fatal.d.ts +70 -0
  16. package/dist/fatal.js +132 -0
  17. package/dist/format-error.d.ts +113 -0
  18. package/dist/format-error.js +553 -0
  19. package/dist/fs-guard.d.ts +142 -0
  20. package/dist/fs-guard.js +222 -0
  21. package/dist/git.js +10 -1
  22. package/dist/harness/conformance.js +2 -0
  23. package/dist/harness/frontmatter.js +2 -13
  24. package/dist/harness/grade.js +37 -8
  25. package/dist/harness/rules.d.ts +20 -3
  26. package/dist/harness/rules.js +108 -6
  27. package/dist/harness/types.d.ts +13 -0
  28. package/dist/harness/types.js +3 -0
  29. package/dist/inventory.d.ts +77 -3
  30. package/dist/inventory.js +103 -12
  31. package/dist/jsonc.d.ts +49 -2
  32. package/dist/jsonc.js +140 -7
  33. package/dist/main.d.ts +62 -3
  34. package/dist/main.js +538 -67
  35. package/dist/merge-json.d.ts +47 -4
  36. package/dist/merge-json.js +120 -8
  37. package/dist/mode.js +12 -2
  38. package/dist/pack-stage.d.ts +210 -0
  39. package/dist/pack-stage.js +287 -0
  40. package/dist/packs.d.ts +19 -13
  41. package/dist/packs.js +231 -28
  42. package/dist/palette.d.ts +23 -0
  43. package/dist/palette.js +22 -0
  44. package/dist/plugin.d.ts +122 -6
  45. package/dist/plugin.js +687 -47
  46. package/dist/report.d.ts +21 -2
  47. package/dist/report.js +93 -5
  48. package/dist/staging.d.ts +176 -0
  49. package/dist/staging.js +375 -0
  50. package/dist/survey/fs-walk.d.ts +34 -2
  51. package/dist/survey/fs-walk.js +70 -5
  52. package/dist/survey/internal/blocked-path.d.ts +27 -0
  53. package/dist/survey/internal/blocked-path.js +129 -0
  54. package/dist/survey/internal/package-json.d.ts +13 -0
  55. package/dist/survey/internal/package-json.js +37 -0
  56. package/dist/survey/internal/read-guard.d.ts +178 -0
  57. package/dist/survey/internal/read-guard.js +276 -0
  58. package/dist/survey/survey-docs.d.ts +17 -2
  59. package/dist/survey/survey-docs.js +61 -21
  60. package/dist/survey/survey-harness.d.ts +20 -2
  61. package/dist/survey/survey-harness.js +87 -46
  62. package/dist/survey/survey-shape.d.ts +17 -2
  63. package/dist/survey/survey-shape.js +60 -46
  64. package/dist/survey/survey-toolchain.d.ts +16 -1
  65. package/dist/survey/survey-toolchain.js +69 -66
  66. package/dist/survey/survey.js +6 -4
  67. package/dist/survey/types.d.ts +79 -0
  68. package/dist/survey/types.js +2 -6
  69. package/dist/term.d.ts +80 -0
  70. package/dist/term.js +145 -0
  71. package/dist/tokens.js +2 -0
  72. package/dist/toolchain/conformance.js +2 -0
  73. package/dist/toolchain/grade.js +26 -8
  74. package/dist/toolchain/rules.d.ts +13 -4
  75. package/dist/toolchain/rules.js +16 -0
  76. package/dist/toolchain/tsconfig-chain.d.ts +2 -0
  77. package/dist/toolchain/tsconfig-chain.js +32 -8
  78. package/dist/toolchain/types.d.ts +16 -4
  79. package/dist/toolchain/types.js +3 -0
  80. package/package.json +4 -3
  81. package/plugin/skills/customize/SKILL.md +292 -18
  82. package/plugin/src/domain-map.ts +39 -14
  83. package/plugin/src/index.ts +5 -1
  84. package/plugin/src/kind-facet-map.ts +25 -8
  85. package/plugin/src/pack-map.ts +147 -25
  86. package/plugin/src/plugin-map.ts +236 -0
  87. package/templates/core/.claude/agents/Explore.md +0 -1
  88. package/templates/core/.claude/agents/code-implementer.md +4 -4
  89. package/templates/core/.claude/agents/code-reviewer.md +4 -4
  90. package/templates/core/.claude/agents/silent-failure-hunter.md +2 -2
  91. package/templates/core/.claude/agents/test-author.md +7 -5
  92. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +56 -10
  93. package/templates/core/.claude/hooks/guard-double-background.mjs +13 -8
  94. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +12 -9
  95. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +1890 -38
  96. package/templates/core/.claude/hooks/guard-js-extension.mjs +2 -1
  97. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +7 -5
  98. package/templates/core/.claude/hooks/guard-secret-writes.mjs +7 -5
  99. package/templates/core/.claude/hooks/inject-decision-gate.mjs +12 -9
  100. package/templates/core/.claude/hooks/post-edit-verify.mjs +276 -98
  101. package/templates/core/.claude/rules/agent-dispatch.md +7 -0
  102. package/templates/core/.claude/rules/tests.md +2 -2
  103. package/templates/core/.claude/settings.json +5 -0
  104. package/templates/core/.claude/skills/finishing-work/SKILL.md +58 -10
  105. package/templates/core/.claude/skills/harness-guidance/SKILL.md +16 -7
  106. package/templates/core/.claude/skills/starting-work/SKILL.md +5 -0
  107. package/templates/core/.claude/skills/triaging-ci/SKILL.md +9 -8
  108. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +137 -20
  109. package/templates/core/.claude/skills/typescript-guidance/references/area-catalog.md +135 -0
  110. package/templates/core/.claude/skills/typescript-guidance/references/tooling-sources.md +113 -0
  111. package/templates/core/.claude/skills/writing-commits/SKILL.md +2 -2
  112. package/templates/core/.github/dependabot.yml +18 -0
  113. package/templates/core/.github/workflows/ci.yml +15 -15
  114. package/templates/core/.github/workflows/dependency-review.yml +2 -2
  115. package/templates/core/.github/workflows/security-audit.yml +10 -4
  116. package/templates/core/.prettierignore +4 -0
  117. package/templates/core/CLAUDE.md +53 -3
  118. package/templates/core/README.md +30 -10
  119. package/templates/core/_gitignore +17 -0
  120. package/templates/core/bin/check-exports.mjs +11 -5
  121. package/templates/core/bin/lib/agent-roster.mjs +1 -1
  122. package/templates/core/bin/lib/frontmatter.mjs +4 -2
  123. package/templates/core/bin/lib/harness-rules.mjs +169 -20
  124. package/templates/core/bin/lib/protected-paths.mjs +93 -5
  125. package/templates/core/bin/lib/toolchain-rules.mjs +187 -26
  126. package/templates/core/bin/lib/verify-steps.mjs +29 -11
  127. package/templates/core/bin/verify.mjs +12 -0
  128. package/templates/core/eslint.config.js +5 -0
  129. package/templates/core/package.json +7 -7
  130. package/templates/core/vitest.config.ts +8 -1
  131. package/templates/packs/README.md +35 -24
  132. package/templates/packs/github/files/.claude/skills/reviewing-dependabot-prs/SKILL.md +133 -0
  133. package/templates/packs/github/files/.claude/skills/triaging-scan-alerts/SKILL.md +88 -0
  134. package/templates/packs/github/files/.claude/skills/watching-pr-checks/SKILL.md +94 -0
  135. package/templates/packs/github/files/.github/workflows/claude-pr-review.yml +267 -0
  136. package/templates/packs/github/files/.github/workflows/claude.yml +106 -0
  137. package/templates/packs/github/pack.json +19 -0
  138. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +1122 -65
  139. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +147 -13
  140. package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline.mjs +6 -2
  141. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +227 -44
  142. package/templates/packs/harness-extras/pack.json +18 -13
  143. package/templates/packs/publishing/files/.changeset/README.md +25 -0
  144. package/templates/packs/publishing/files/.changeset/config.json +7 -0
  145. package/templates/packs/publishing/files/.github/release-tools/package.json +9 -0
  146. package/templates/packs/publishing/files/.github/workflows/release.yml +293 -0
  147. package/templates/packs/publishing/files/REUSE.toml +28 -0
  148. package/templates/packs/publishing/files/bin/check-dts-deps.mjs +234 -0
  149. package/templates/packs/publishing/files/bin/check-license-headers.mjs +217 -0
  150. package/templates/packs/publishing/files/bin/check-publish-version.mjs +149 -0
  151. package/templates/packs/publishing/files/bin/lib/npm-publish-args.mjs +86 -0
  152. package/templates/packs/publishing/files/bin/pnpm-publish-shim.mjs +86 -0
  153. package/templates/packs/publishing/pack.json +35 -0
  154. package/templates/packs/{harness-extras → quality}/files/bin/check-file-budget.mjs +6 -4
  155. package/templates/packs/quality/pack.json +29 -0
  156. package/templates/packs/supply-chain/files/.github/workflows/gitleaks.yml +52 -0
  157. package/templates/packs/supply-chain/files/.github/workflows/scorecard.yml +48 -0
  158. package/templates/packs/supply-chain/files/.gitleaks.toml +2 -0
  159. package/templates/packs/supply-chain/pack.json +19 -0
  160. package/templates/packs/worktrees/files/.claude/hooks/ensure-worktree-deps.mjs +283 -0
  161. package/templates/packs/worktrees/files/.claude/hooks/guard-worktree-only.mjs +245 -0
  162. package/templates/packs/worktrees/files/.claude/hooks/repair-core-bare.mjs +335 -0
  163. package/templates/packs/worktrees/files/.claude/skills/working-in-worktrees/SKILL.md +139 -0
  164. package/templates/packs/worktrees/files/.worktreeinclude +11 -0
  165. package/templates/packs/worktrees/pack.json +64 -0
  166. package/templates/packs/statusline/pack.json +0 -31
  167. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/statusline-layout.mjs +0 -0
  168. /package/templates/packs/{statusline → harness-extras}/files/.claude/hooks/subagent-statusline.mjs +0 -0
  169. /package/templates/packs/{harness-extras → quality}/files/.claude/agents/type-design-analyzer.md +0 -0
  170. /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