@monte3l/groundwork 1.0.0-rc.2 → 1.0.0-rc.4

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 +574 -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 +21 -13
  41. package/dist/packs.js +241 -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 +44 -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 +295 -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 +41 -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,287 @@
1
+ // SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * Adopt mode's staging of every pack under `templates/packs/`: each pack's
5
+ * manifest and `files/` tree are copied into `.groundwork/packs/<name>/`,
6
+ * every file under an inert `<path>.staged` name, so `/customize`'s Step 0
7
+ * can install a pack from a self-contained copy -- after confirmation --
8
+ * without any toolchain globbing the project ever picking a staged file up.
9
+ * All packs are written together to a temporary sibling directory and
10
+ * swapped in by rename, so `packs/` is never half-written -- the same
11
+ * lifecycle `baseline-stage.ts` uses (both build on `staging.ts`).
12
+ */
13
+ import { readFileSync, statSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { toPosixPath } from "./assets.js";
16
+ import { isPathContained } from "./emit.js";
17
+ import { STAGED_SUFFIX, assertPlanBuiltFor, clearStaging, collectTemplateFiles, findStagedPathCollision, prepareStaging, stageAtomically, stagedNameFor, writeStagedBytes, } from "./staging.js";
18
+ /** The staging directory's name under `.groundwork/`. */
19
+ const PACKS_DIR_NAME = "packs";
20
+ /** Each staged pack's subdirectory holding its `files/` tree. */
21
+ const PACK_FILES_DIR = "files";
22
+ /**
23
+ * The project-relative directory every pack is staged under; a pack's own
24
+ * staging directory is `${STAGED_PACKS_DIR}/<name>`.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const dir = `${STAGED_PACKS_DIR}/quality`; // ".groundwork/packs/quality"
29
+ * ```
30
+ */
31
+ export const STAGED_PACKS_DIR = `.groundwork/${PACKS_DIR_NAME}`;
32
+ /**
33
+ * The install name of a pack's manifest; it is staged as
34
+ * `pack.json` + `.staged` beside the pack's `files/` directory.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const staged = `${STAGED_PACK_MANIFEST}.staged`; // "pack.json.staged"
39
+ * ```
40
+ */
41
+ export const STAGED_PACK_MANIFEST = "pack.json";
42
+ function assertSingleSegmentName(name) {
43
+ if (name === "" || name === "." || name === ".." || /[\\/]/.test(name)) {
44
+ throw new Error(`stagePacks: pack name ${JSON.stringify(name)} is not a single directory name`);
45
+ }
46
+ if (name.includes(":")) {
47
+ throw new Error(`stagePacks: pack name ${JSON.stringify(name)} contains ":", which Windows reads as a drive letter or an alternate data stream`);
48
+ }
49
+ }
50
+ /**
51
+ * Whether `filesDir` is a directory. A missing path answers `false`; any
52
+ * other `statSync` failure (a permission error, say) becomes a plan-time
53
+ * `Error` naming the pack and the path, with the failure as `cause` -- seen
54
+ * before anything is written, it is a template defect or a permission
55
+ * problem to fix, not an "incomplete, re-run" staging failure.
56
+ */
57
+ function isFilesDir(name, filesDir) {
58
+ try {
59
+ return (statSync(filesDir, { throwIfNoEntry: false })?.isDirectory() === true);
60
+ }
61
+ catch (cause) {
62
+ throw new Error(`stagePacks: could not inspect pack "${name}"'s files directory ${filesDir}`, { cause });
63
+ }
64
+ }
65
+ function serializeManifest(name, manifest) {
66
+ // JSON.stringify is typed string but yields undefined for some inputs
67
+ // (a toJSON returning undefined); never write or hash that as text.
68
+ const text = JSON.stringify(manifest, null, 2);
69
+ if (typeof text !== "string") {
70
+ throw new Error(`stagePacks: pack "${name}"'s manifest is not JSON`);
71
+ }
72
+ return Buffer.from(`${text}\n`);
73
+ }
74
+ /**
75
+ * Validates and projects every pack before anything is touched. These
76
+ * defects are refused, each with its own `Error` (no `cause`): a pack name
77
+ * that is not a single directory name, contains `:`, or is shared by two
78
+ * packs (also when the two differ only by letter case or Unicode
79
+ * normalization); a missing
80
+ * `filesDir`; two files in one pack installing to the same path; an install
81
+ * path containing `:` (a drive letter or alternate data stream on Windows);
82
+ * a tokenized install path whose staged name would land outside the pack's
83
+ * `files/` staging directory (CWE-22, docs/assurance-case.md); and two
84
+ * staged names in one pack that would land on the same file
85
+ * ({@link findStagedPathCollision}). A `filesDir` that cannot be inspected
86
+ * at all is refused with its failure as `cause` (see `isFilesDir`).
87
+ */
88
+ function planPacks(packs, packsDir, tokens) {
89
+ const seen = new Set();
90
+ const plan = packs.map(({ manifest, filesDir }) => {
91
+ const name = manifest.name;
92
+ assertSingleSegmentName(name);
93
+ if (seen.has(name)) {
94
+ throw new Error(`stagePacks: two packs are named "${name}"; pack names must be unique`);
95
+ }
96
+ seen.add(name);
97
+ if (!isFilesDir(name, filesDir)) {
98
+ throw new Error(`stagePacks: pack "${name}"'s files directory ${filesDir} does not exist`);
99
+ }
100
+ const stagedFilesDir = join(packsDir, name, PACK_FILES_DIR);
101
+ const files = [...collectTemplateFiles(filesDir, tokens)]
102
+ .sort(([a], [b]) => (a < b ? -1 : 1))
103
+ .map(([path, sourcePath]) => {
104
+ if (path.includes(":")) {
105
+ throw new Error(`stagePacks: pack "${name}"'s file ${toPosixPath(path)} contains ":", which Windows reads as a drive letter or an alternate data stream`);
106
+ }
107
+ const destPath = join(stagedFilesDir, stagedNameFor(path));
108
+ if (!isPathContained(destPath, stagedFilesDir)) {
109
+ throw new Error(`stagePacks: staged path ${resolve(destPath)} for pack "${name}"'s ${path} escapes ${resolve(stagedFilesDir)}`);
110
+ }
111
+ return { path, sourcePath };
112
+ });
113
+ const collision = findStagedPathCollision(files.map(({ path }) => toPosixPath(stagedNameFor(path))));
114
+ if (collision !== undefined) {
115
+ throw new Error(`stagePacks: two of pack "${name}"'s staged files collide: ${collision}; rename one of them under ${filesDir}`);
116
+ }
117
+ return { name, manifestBytes: serializeManifest(name, manifest), files };
118
+ });
119
+ const nameCollision = findStagedPathCollision(plan.map(({ name }) => name));
120
+ if (nameCollision !== undefined) {
121
+ throw new Error(`stagePacks: two packs' staging directories collide: ${nameCollision}; pack names must be unique ignoring case and Unicode normalization`);
122
+ }
123
+ return plan;
124
+ }
125
+ /**
126
+ * Validates `packs` and computes the plan {@link stagePacks} writes from:
127
+ * every path under `<groundworkDir>/packs/` -- each pack's
128
+ * `pack.json.staged` and every `files/<path>.staged` -- and each file's
129
+ * source, so adopt mode can scope-check `paths` before any pack is written
130
+ * and then hand the same plan to {@link stagePacks}. Reads the packs'
131
+ * source trees; writes nothing.
132
+ *
133
+ * @throws The same plan `Error`s as {@link stagePacks}.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { assertAdoptWriteScope } from "./main.js";
138
+ * const plan = planPackStaging(packs, groundworkDir, tokens);
139
+ * assertAdoptWriteScope(targetDir, plan.paths);
140
+ * stagePacks(packs, groundworkDir, tokens, plan);
141
+ * ```
142
+ */
143
+ export function planPackStaging(packs, groundworkDir, tokens) {
144
+ const packsDir = join(groundworkDir, PACKS_DIR_NAME);
145
+ const planned = planPacks(packs, packsDir, tokens);
146
+ const paths = planned.flatMap(({ name, files }) => [
147
+ join(packsDir, name, stagedNameFor(STAGED_PACK_MANIFEST)),
148
+ ...files.map(({ path }) => join(packsDir, name, PACK_FILES_DIR, stagedNameFor(path))),
149
+ ]);
150
+ return { groundworkDir, paths, packs: planned };
151
+ }
152
+ /**
153
+ * Every path {@link stagePacks} would write for `packs` -- a thin wrapper
154
+ * returning {@link planPackStaging}'s `paths`. Reads the packs' source
155
+ * trees; writes nothing.
156
+ *
157
+ * @throws The same plan `Error`s as {@link stagePacks}.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * import { assertAdoptWriteScope } from "./main.js";
162
+ * assertAdoptWriteScope(targetDir, plannedPackStagingPaths(packs, groundworkDir, tokens));
163
+ * ```
164
+ */
165
+ export function plannedPackStagingPaths(packs, groundworkDir, tokens) {
166
+ return [...planPackStaging(packs, groundworkDir, tokens).paths];
167
+ }
168
+ function writePack(plan, newDir) {
169
+ const manifestStaged = stagedNameFor(STAGED_PACK_MANIFEST);
170
+ // Relative to newDir, so the containment assertion also covers the name.
171
+ const manifestSha256 = writeStagedBytes("stagePacks", plan.manifestBytes, newDir, join(plan.name, manifestStaged));
172
+ const filesDir = join(newDir, plan.name, PACK_FILES_DIR);
173
+ const files = plan.files.map(({ path, sourcePath }) => {
174
+ const stagedName = stagedNameFor(path);
175
+ const sha256 = writeStagedBytes("stagePacks", readFileSync(sourcePath), filesDir, stagedName);
176
+ return {
177
+ path: toPosixPath(path),
178
+ staged: toPosixPath(stagedName),
179
+ sha256,
180
+ };
181
+ });
182
+ return {
183
+ name: plan.name,
184
+ dir: `${STAGED_PACKS_DIR}/${plan.name}`,
185
+ suffix: STAGED_SUFFIX,
186
+ manifest: {
187
+ path: STAGED_PACK_MANIFEST,
188
+ staged: manifestStaged,
189
+ sha256: manifestSha256,
190
+ },
191
+ files,
192
+ };
193
+ }
194
+ /**
195
+ * Stages every pack in `packs` into `<groundworkDir>/packs/`: for each, its
196
+ * manifest as `<name>/pack.json.staged` (`JSON.stringify(manifest, null, 2)`
197
+ * plus a newline) and every file of its `filesDir` tree as
198
+ * `<name>/files/<path>.staged`, copied byte-for-byte -- no token
199
+ * substitution into content, which is `/customize`'s job at install time.
200
+ * `tokens` only substitutes into install paths (and dotfile names are
201
+ * restored), the same derivation `planConflicts` uses. Recorded `path`/
202
+ * `staged` values use forward slashes; `dir` is the literal
203
+ * `.groundwork/packs/<name>`, whatever `groundworkDir` was passed.
204
+ *
205
+ * What is guaranteed:
206
+ * - The plan is validated first, before anything is deleted or written (or,
207
+ * when `plan` is passed, was already validated by {@link planPackStaging}
208
+ * and the packs' trees are not walked again): a pack name that is not a
209
+ * single directory name, contains `:`, or is used by two packs (ignoring
210
+ * case and Unicode normalization), a missing `filesDir`, two files in one
211
+ * pack installing to the same path, an install path containing `:`, a
212
+ * staged name that would escape its pack's staging directory, or two
213
+ * staged names in one pack landing on the same file (equal once
214
+ * NFC-normalized and case-folded, or one a directory prefix of the other)
215
+ * throws its own
216
+ * `Error`, leaving `.groundwork/` exactly as it was. A `filesDir` that
217
+ * cannot be inspected (a permission error) throws a plan `Error` naming
218
+ * the pack and path, with the failure as `cause`.
219
+ * - Then, still before anything is deleted or written, `groundworkDir` and
220
+ * `<groundworkDir>/packs` are checked not to be symlinks, and every
221
+ * `.packs-*` entry of the CLI-owned `groundworkDir` -- meant for work
222
+ * directories a crashed earlier run left, but removed whatever created
223
+ * it, so a concurrent run against the same directory can lose its
224
+ * in-progress work directory and fail with the incomplete/re-run error
225
+ * -- is removed (best effort; a failure to remove one only warns, a
226
+ * failure to list `groundworkDir` throws). `.baseline-*`
227
+ * entries are left alone.
228
+ * - When `packs` is empty, any previous `packs/` is removed and nothing is
229
+ * created.
230
+ * - Otherwise every pack is written into one temporary `.packs-*` sibling
231
+ * directory (each file created exclusively, `wx`) and swapped in by rename
232
+ * only after every copy succeeded, so `packs/` is never half-written and a
233
+ * failure leaves any previous `packs/` intact; a previous `packs/` is
234
+ * replaced wholesale (a pack no longer passed, or a file a pack dropped,
235
+ * does not linger).
236
+ * - With a previous `packs/`, the swap is two renames: the previous one is
237
+ * parked inside the temporary directory, then the new one moved into
238
+ * place. A process killed between the two leaves `packs/` absent and the
239
+ * previous copy at `.packs-XXXXXX/previous`; the next run's sweep removes
240
+ * it and regenerates the staging.
241
+ * - If the final swap rename fails, the previous `packs/` is renamed back.
242
+ * Only if that restore also fails is `packs/` left absent: the previous
243
+ * staging then survives, parked inside the temporary directory, which is
244
+ * deliberately not removed (a later run's stale-dir sweep does).
245
+ * - The temporary directory is otherwise always removed; a failure to remove
246
+ * it only warns, naming its path.
247
+ *
248
+ * @throws `Error` (no re-run advice; no `cause` except for an uninspectable
249
+ * `filesDir`) for an invalid plan, as above; `Error` before any delete or
250
+ * write when `groundworkDir` or
251
+ * `<groundworkDir>/packs` is a symlink; `AggregateError` of the swap and
252
+ * restore failures, naming where the previous `packs/` is parked, when both
253
+ * renames fail; the `AssertionError` itself, unwrapped, if the staged-path
254
+ * containment invariant ever fails while writing; otherwise an `Error` with
255
+ * `cause`, including the cause's message, saying `.groundwork/` is
256
+ * incomplete and the CLI should be re-run.
257
+ *
258
+ * @param plan - The plan {@link planPackStaging} computed for these same
259
+ * `packs`, `groundworkDir` and `tokens`; computed here when omitted. A plan
260
+ * whose `groundworkDir` resolves to a different directory throws a plain
261
+ * `Error` naming both ("the plan was built for …") before anything is
262
+ * deleted or written.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * import { listPackNames, loadPack } from "./packs.js";
267
+ * const packs = listPackNames().map((name) => loadPack(name));
268
+ * const staged = stagePacks(packs, "/work/app/.groundwork", { PROJECT_NAME: "app" });
269
+ * // staged[0]: { name: "github", dir: ".groundwork/packs/github", suffix: ".staged", … }
270
+ * ```
271
+ */
272
+ export function stagePacks(packs, groundworkDir, tokens, plan = planPackStaging(packs, groundworkDir, tokens)) {
273
+ assertPlanBuiltFor("stagePacks", plan.groundworkDir, groundworkDir);
274
+ const target = {
275
+ groundworkDir,
276
+ dirName: PACKS_DIR_NAME,
277
+ noun: "packs",
278
+ plural: true,
279
+ };
280
+ prepareStaging(target);
281
+ if (plan.packs.length === 0) {
282
+ clearStaging(target);
283
+ return [];
284
+ }
285
+ return stageAtomically(target, (newDir) => plan.packs.map((planned) => writePack(planned, newDir)));
286
+ }
287
+ //# sourceMappingURL=pack-stage.js.map
package/dist/packs.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface PackManifest {
19
19
  } | undefined;
20
20
  wiring: PackWiring;
21
21
  adoptNotes: string | undefined;
22
+ /** Shell commands, run in the target directory, that a fresh install needs before its first `pnpm verify` (fresh mode prints them; see main.ts). Optional: absent means no setup. */
23
+ setupSteps?: string[] | undefined;
22
24
  }
23
25
  export interface Pack {
24
26
  manifest: PackManifest;
@@ -28,7 +30,7 @@ export interface Pack {
28
30
  export declare function packsRootDir(): string;
29
31
  /** Names of every pack directory that has a `pack.json` under `root`, sorted for deterministic install order. `root` defaults to `templates/packs`, overridable for tests. */
30
32
  export declare function listPackNames(root?: string): string[];
31
- /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown or malformed. */
33
+ /** Loads and validates one pack's manifest from under `root` (default `templates/packs`, overridable for tests). Throws, naming the available packs, if unknown; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}) or a `setupSteps` that is present but not a non-empty array of single-line, non-empty strings. */
32
34
  export declare function loadPack(name: string, root?: string): Pack;
33
35
  export interface PackInstallResult {
34
36
  filesWritten: string[];
@@ -41,21 +43,27 @@ export interface PackInstallResult {
41
43
  * applies the pack's three JSON wiring merges.
42
44
  */
43
45
  export declare function installPack(pack: Pack, targetDir: string, tokens: TokenTable): PackInstallResult;
44
- /**
45
- * Copies a pack's `pack.json` + `files/` tree, unmodified, into
46
- * `<groundworkDir>/packs/<name>/` -- adopt mode's staging area. `/customize`
47
- * installs from this self-contained copy rather than from `templateRoot`
48
- * (an absolute path that may not exist by the time it runs). Token
49
- * substitution is a no-op here (`{}`): staging a project's real name into
50
- * pack content is `/customize`'s job, not this offline copy's.
51
- */
52
- export declare function stagePackFiles(pack: Pack, groundworkDir: string): string[];
53
46
  /**
54
47
  * Index-level, adopt-mode-only facts about how a pack's wiring would land
55
48
  * against a real project's current `.claude/settings.json` and
56
49
  * `bin/lib/verify-steps.packs.json` -- never a verdict on whether it will
57
- * work, which is `/customize`'s Step 0 judgment call to make after reading
58
- * the project's real gate runner and hook config.
50
+ * work. That verdict is a judgment call for `/customize`'s Step 0 (the
51
+ * adopt-mode reconcile step in `/customize`) to make after reading the
52
+ * project's real gate runner and hook config. A path this process cannot
53
+ * reach (`EACCES`/`EPERM`/`ELOOP`) is observed with its errno, never as
54
+ * "not found"; nor is a dangling symlink or a file blocking an ancestor
55
+ * directory, which is also recorded once in `undetermined`. A
56
+ * `.claude/settings.json` that exists but cannot be read for a reason that
57
+ * is a property of the project's tree (`EACCES`/`EPERM`, `EISDIR`, `ENOENT`,
58
+ * `ELOOP`) is observed with its errno and also recorded once in
59
+ * `undetermined` -- adopt mode passes the survey's own list, so the report
60
+ * shows it; any other errno throws.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const undetermined: string[] = [];
65
+ * const observations = observeWiring(targetDir, pack.manifest, undetermined);
66
+ * ```
59
67
  */
60
- export declare function observeWiring(targetDir: string, manifest: PackManifest): string[];
68
+ export declare function observeWiring(targetDir: string, manifest: PackManifest, undetermined?: string[]): string[];
61
69
  //# sourceMappingURL=packs.d.ts.map