@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.
- 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 +574 -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 +21 -13
- package/dist/packs.js +241 -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 +44 -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 +295 -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 +41 -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,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
|
|
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
|
|
58
|
-
*
|
|
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
|