@monte3l/groundwork 1.0.0-rc.3 → 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/dist/main.js +36 -0
- package/dist/packs.d.ts +3 -1
- package/dist/packs.js +11 -1
- package/package.json +1 -1
- package/templates/packs/README.md +9 -0
- package/templates/packs/publishing/files/.github/release-tools/package.json +1 -1
- package/templates/packs/publishing/files/.github/workflows/release.yml +8 -6
- package/templates/packs/publishing/pack.json +7 -1
package/dist/main.js
CHANGED
|
@@ -242,6 +242,27 @@ export function formatCapsSummary(baseline, installed) {
|
|
|
242
242
|
lines.push(`= ${formatCounts(total)}${overCap.length > 0 ? ` ⚠ over cap: ${overCap.join(", ")}` : ""}`);
|
|
243
243
|
return { text: lines.join("\n"), overCap: overCap.length > 0 };
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Fresh mode's closing "setup required" block: one heading, then each pack
|
|
247
|
+
* that declares `setupSteps` (in `packs` order) followed by its commands,
|
|
248
|
+
* each indented two spaces. `undefined` when no pack declares any, so the
|
|
249
|
+
* caller prints nothing extra.
|
|
250
|
+
*/
|
|
251
|
+
function formatSetupBlock(targetDir, packs) {
|
|
252
|
+
const lines = [];
|
|
253
|
+
for (const { manifest } of packs) {
|
|
254
|
+
const steps = manifest.setupSteps ?? [];
|
|
255
|
+
if (steps.length === 0)
|
|
256
|
+
continue;
|
|
257
|
+
lines.push(`pack "${manifest.name}":`, ...steps.map((step) => ` ${step}`));
|
|
258
|
+
}
|
|
259
|
+
if (lines.length === 0)
|
|
260
|
+
return undefined;
|
|
261
|
+
return [
|
|
262
|
+
`\nthe installed pack(s) need setup before the first \`pnpm verify\` -- run these in ${targetDir}:`,
|
|
263
|
+
...lines,
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
245
266
|
/**
|
|
246
267
|
* Fresh mode's pre-flight over the `/customize` skill's own destination
|
|
247
268
|
* (`.claude/skills/customize/`): each directory component must be missing
|
|
@@ -295,6 +316,18 @@ function runFresh(options, platform) {
|
|
|
295
316
|
: summary.text);
|
|
296
317
|
}
|
|
297
318
|
const { targetDir, skipInstall, projectName } = options;
|
|
319
|
+
// Built once, now that the project and its packs are on disk, and printed
|
|
320
|
+
// exactly once per run: at the very end on success, or just before the
|
|
321
|
+
// throw when git init / pnpm install fails -- a plain re-run of a
|
|
322
|
+
// non-empty target adopts it, and adopt mode never prints setupSteps. The
|
|
323
|
+
// /customize-skill failure below deliberately does not print it: its
|
|
324
|
+
// message says to re-run with --fresh --force, which prints it on success.
|
|
325
|
+
const setupBlock = formatSetupBlock(targetDir, packs);
|
|
326
|
+
const printSetupBlock = () => {
|
|
327
|
+
if (setupBlock !== undefined) {
|
|
328
|
+
console.log(paint(process.stdout, "warning", setupBlock));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
298
331
|
let pluginResult;
|
|
299
332
|
try {
|
|
300
333
|
pluginResult = installCustomizeSkill(targetDir);
|
|
@@ -314,6 +347,7 @@ function runFresh(options, platform) {
|
|
|
314
347
|
gitInit(targetDir);
|
|
315
348
|
}
|
|
316
349
|
catch (error) {
|
|
350
|
+
printSetupBlock();
|
|
317
351
|
throw new Error(`git init failed, but the project was written to ${targetDir}; run \`git init\`${skipInstall ? "" : " and `pnpm install`"} there yourself`, { cause: error });
|
|
318
352
|
}
|
|
319
353
|
console.log("initialized git repository");
|
|
@@ -323,11 +357,13 @@ function runFresh(options, platform) {
|
|
|
323
357
|
}
|
|
324
358
|
catch (error) {
|
|
325
359
|
console.log(paint(process.stdout, "warning", `\n${projectName} written to ${targetDir}, but dependencies are not installed`));
|
|
360
|
+
printSetupBlock();
|
|
326
361
|
throw new Error(`${describeInstallFailure(error)}; the project was written to ${targetDir} -- run \`pnpm install\` there yourself to finish`, { cause: error });
|
|
327
362
|
}
|
|
328
363
|
console.log("installed dependencies");
|
|
329
364
|
}
|
|
330
365
|
console.log(paint(process.stdout, "success", `\n✓ ${projectName} is ready at ${targetDir}`));
|
|
366
|
+
printSetupBlock();
|
|
331
367
|
}
|
|
332
368
|
/**
|
|
333
369
|
* Explains why the post-emission `pnpm install` failed: a missing binary
|
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; throws naming the pack and the problem if malformed, including a prototype-sensitive key in its wiring (see {@link assertValidWiringShape}). */
|
|
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[];
|
package/dist/packs.js
CHANGED
|
@@ -132,7 +132,13 @@ function assertValidWiringShape(name, wiring) {
|
|
|
132
132
|
assertValidVerifyStep(name, step, index);
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
-
/**
|
|
135
|
+
/** True when `steps` is a non-empty array of non-empty strings, none containing a line break -- each is printed as one indented line. */
|
|
136
|
+
function isValidSetupSteps(steps) {
|
|
137
|
+
return (Array.isArray(steps) &&
|
|
138
|
+
steps.length > 0 &&
|
|
139
|
+
steps.every((step) => typeof step === "string" && step !== "" && !/[\r\n]/.test(step)));
|
|
140
|
+
}
|
|
141
|
+
/** 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. */
|
|
136
142
|
export function loadPack(name, root = packsRootDir()) {
|
|
137
143
|
const packDir = join(root, name);
|
|
138
144
|
const manifestPath = join(packDir, "pack.json");
|
|
@@ -176,6 +182,10 @@ export function loadPack(name, root = packsRootDir()) {
|
|
|
176
182
|
if (!isValidBudget(budget)) {
|
|
177
183
|
throw new Error(`pack "${name}": pack.json's budget must set ${CAP_KEYS.join(", ")} to non-negative integers`);
|
|
178
184
|
}
|
|
185
|
+
const setupSteps = manifest.setupSteps;
|
|
186
|
+
if (setupSteps !== undefined && !isValidSetupSteps(setupSteps)) {
|
|
187
|
+
throw new Error(`pack "${name}": pack.json's setupSteps must be a non-empty array of single-line, non-empty strings`);
|
|
188
|
+
}
|
|
179
189
|
// pack-stage.ts's stagePacks keys each pack's staging directory on
|
|
180
190
|
// manifest.name, so a mismatch would let two pack directories collide.
|
|
181
191
|
if (manifestName !== name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monte3l/groundwork",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"description": "CLI that writes a TypeScript toolchain and Claude Code setup into an empty directory, or -- against an existing project -- read-only surveys it and reports the differences. No prompts, and no network access beyond the package install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -65,6 +65,15 @@ already enumerate groups rather than individual steps.
|
|
|
65
65
|
- `wiring.verifySteps` — entries appended to `bin/lib/verify-steps.packs.json`.
|
|
66
66
|
- `adoptNotes` — free text surfaced verbatim in `/customize`'s Step 0
|
|
67
67
|
confirmation round when the pack applies to an adopted project.
|
|
68
|
+
- `setupSteps` — the exact commands a user must run in the new project after
|
|
69
|
+
the pack is installed and before the first `pnpm verify`, or verify fails
|
|
70
|
+
(for example, `publishing` needs `@changesets/cli` added, since a pack can
|
|
71
|
+
never edit `dependencies`). A non-empty array of single-line, non-empty
|
|
72
|
+
strings; `loadPack` rejects anything else. Fresh mode prints every installed
|
|
73
|
+
pack's steps in one block after its `ready` line. Adopt mode neither prints
|
|
74
|
+
them nor adds them to the inventory (the staged `pack.json` still carries the
|
|
75
|
+
field verbatim): a pack that is adopt-capable and needs setup says so in
|
|
76
|
+
`adoptNotes`. Optional.
|
|
68
77
|
|
|
69
78
|
## Install path
|
|
70
79
|
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Pins the exact npm version the release workflow's publish job installs, so the pin is verified by a committed lockfile's integrity hash rather than a bare version string in YAML.",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"npm": "
|
|
7
|
+
"npm": "12.2.0"
|
|
8
8
|
}
|
|
9
9
|
}
|
|
@@ -185,12 +185,14 @@ jobs:
|
|
|
185
185
|
package-manager-cache: false
|
|
186
186
|
# Only the changesets CLI is needed here, not any package's lifecycle scripts.
|
|
187
187
|
- run: pnpm install --frozen-lockfile --ignore-scripts
|
|
188
|
-
# `npm stage publish` needs npm >= 11.15.0
|
|
189
|
-
#
|
|
190
|
-
# `
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
188
|
+
# `npm stage publish` needs npm >= 11.15.0. The pinned npm (12.2.0)
|
|
189
|
+
# declares engines Node ^22.22.2 || ^24.15.0 || >=26. .node-version's
|
|
190
|
+
# `24` meets that on GitHub-hosted runners, whose cached 24.x is recent;
|
|
191
|
+
# a self-hosted runner with an older cached 24.x does not. Pinned to an
|
|
192
|
+
# exact version, not a range or `latest`: this job holds the OIDC token,
|
|
193
|
+
# and an unpinned package-manager install is exactly what OpenSSF
|
|
194
|
+
# Scorecard's Pinned-Dependencies check flags. `.github/release-tools/`
|
|
195
|
+
# pins the exact tarball via its committed lockfile's integrity hash, and
|
|
194
196
|
# `npm ci` verifies the install against it -- bumping the pin is then
|
|
195
197
|
# a normal reviewed PR (a lockfile diff), not a hand-edited version
|
|
196
198
|
# string.
|
|
@@ -31,5 +31,11 @@
|
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
|
-
"
|
|
34
|
+
"setupSteps": [
|
|
35
|
+
"pnpm add -D @changesets/cli",
|
|
36
|
+
"git add -A",
|
|
37
|
+
"node bin/check-license-headers.mjs --fix",
|
|
38
|
+
"git add -A"
|
|
39
|
+
],
|
|
40
|
+
"adoptNotes": "REQUIRED right after install, before the first pnpm verify -- two one-time steps, or pnpm verify fails immediately, not from a real regression but from two setup gaps this pack cannot close itself: (1) `pnpm add -D @changesets/cli` -- the wiring contract only extends package.json's scripts, never its dependencies (see templates/packs/README.md), so the wired `changeset`/`version:packages` scripts reference a binary knip's own unlisted-binaries check correctly flags as missing until this is installed; (2) `git add -A`, then `node bin/check-license-headers.mjs --fix`, then `git add -A` again and commit the result (the gate only checks git-tracked files, so a fresh project with nothing staged passes it vacuously and fails on every file at its first push) -- templates/core has never had a license-header gate before this pack, so every one of its own files (hooks, bin/ scripts, CI workflows, the placeholder src/tests, and the workflows of any other installed pack such as `github` or `supply-chain`) is missing the SPDX header this pack's own license-headers gate now requires, the same one-time cost this repo itself paid when it adopted the same gate (see CLAUDE.md's Definition of Done). In a hand-copied install the CLI does not substitute tokens, so replace every literal `__PROJECT_NAME__` in the copied files (REUSE.toml, the SPDX headers in bin/, the workflows) with the project's real name first. Fresh mode only: a release pipeline encodes decisions (whether the package is actually public, which registry, whether a GitHub App backs the version PR) this pack cannot survey or guess at safely, so it is staged like every other pack (at .groundwork/packs/publishing/) but never auto-installed in adopt mode -- if an adopted project wants it, copy .groundwork/packs/publishing/files/ by hand, dropping the `.staged` suffix from every name (or copy templates/packs/publishing/files/ from this repo directly) and work through the one-time setup below. check-publish-version.mjs is deliberately NOT one of this pack's wired verify steps: with changesets, package.json's version on the release branch is always the one just published between releases, so a version-already-published check running on every ordinary push (via pnpm verify / pre-push) would fail every single time until the next version PR lands -- it is instead invoked directly as a step inside release.yml's pack job, the one place \"is this version about to collide with an already-published one\" is actually the right question to ask. The two remaining verify-group gates (dts-deps, license-headers) install anywhere a bin/verify.mjs-shaped gate runner exists; wire them into the project's real one if it differs. Both check-publish-version.mjs and check-dts-deps.mjs no-op (checked: 0, not a failure) on a package.json with `private: true` (the baseline's own default) or (check-dts-deps.mjs only) with no dist/**/*.d.ts yet -- flip `private` to `false` (and set a real `name`) once the package is actually meant to publish. The pack assumes a public package: `.changeset/config.json` ships `access: public`, npm refuses `restricted` for an unscoped name, and the publish shim always passes `--provenance`, which npm ties to a public source repository -- a private scoped package needs `access: restricted` and provenance removed from bin/lib/npm-publish-args.mjs. `@changesets/changelog-github` (and its `repo` option) is a drop-in swap for `.changeset/config.json`'s plain default changelog generator, once this project's GitHub repository is known, for changelog entries that link back to the originating PR/commit -- see `.changeset/README.md`. One-time setup before release.yml's first real run (distinct from the two pnpm-verify prerequisites above), distilled from this repo's own .claude/rules/releases.md: (1) npm cannot configure a trusted publisher for a package that doesn't exist yet -- publish once by hand with a temporary token first; (2) the trusted publisher is bound to the exact workflow filename `release.yml` -- renaming or moving it breaks publishing until reconfigured on npmjs.com; (3) set the trusted publisher's allowed actions to staged-only (`npm stage publish`, npm's own default and recommendation since 2026-09-03) -- a maintainer runs `npm stage approve <id>` (2FA, never automatable) before a version actually installs; (4) create an `npm-publish` GitHub environment (via `gh api`, not committed as JSON) with a required reviewer, restricted to the release branch, so the `publish` job itself pauses before the git tag/Release/stage-publish exist; (5) if branch protection requires status checks on every PR with no bypass actor, the version-PR job needs a GitHub App installation token (`APP_CLIENT_ID`/`APP_PRIVATE_KEY` repo secrets), not the default GITHUB_TOKEN, or those checks never trigger and the PR can never merge -- if this project has no such rule, `github-token: ${{ secrets.GITHUB_TOKEN }}` is enough and the app-token step can be dropped. release.yml pins `.github/release-tools/` (the npm CLI version) but the baseline's dependabot.yml only covers the github-actions ecosystem, so that pin goes stale unless you add an `npm` entry with `directory: \"/.github/release-tools\"` to the project's own .github/dependabot.yml. Security note, as of 2026-10-01: the npm pinned in the shipped `.github/release-tools/package-lock.json` (12.2.0) bundles undici 6.28.0, ip-address 10.5.0 and brace-expansion 5.0.9, which carry open advisories; every npm release found that day (11.20.0, 11.21.0, 12.2.0) bundles the same versions, and overrides cannot change bundled dependencies. Reading the npm tarballs and lockfile, no reach was found from what the shipped workflow runs (`npm ci --ignore-scripts`, `npm stage publish <tarball>` and `npm stage list`; the tarball is packed earlier by pnpm, not npm, and the shipped workflow configures no proxy; if your runner sets one, for example HTTPS_PROXY on a self-hosted runner behind an egress proxy, the ip-address advisories may apply), and the minimatch callers on those commands (Arborist, and tuf-js under sigstore) take their patterns from your own lockfile and from signed registry metadata, but that is analysis, not a run of the code. Re-check on the day of each release and bump the pinned npm as soon as a release bundling undici >= 6.28.1, ip-address >= 10.7.1 and brace-expansion >= 5.0.12 exists. The secret-scanning and Scorecard workflows that used to ship here are now the separate `supply-chain` pack."
|
|
35
41
|
}
|