@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
package/dist/packs.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
1
3
|
/**
|
|
2
4
|
* Loads pack manifests from `templates/packs/<name>/pack.json` and installs
|
|
3
5
|
* a pack's files + JSON wiring into a freshly-bootstrapped project. Adopt
|
|
4
6
|
* mode never calls `installPack` -- it surveys packs into the report
|
|
5
|
-
* (`observeWiring
|
|
6
|
-
* `/customize`, which reads a project's real gate
|
|
7
|
-
* a pack's wiring (see inventory.ts).
|
|
7
|
+
* (`observeWiring`; `pack-stage.ts`'s `stagePacks` stages them, inert) and
|
|
8
|
+
* defers installation to `/customize`, which reads a project's real gate
|
|
9
|
+
* runner before translating a pack's wiring (see inventory.ts). Both modes call `loadPack` for every
|
|
10
|
+
* pack they handle (fresh mode: each `--pack`; adopt mode: every pack)
|
|
11
|
+
* before writing anything, and `loadPack` refuses a prototype-sensitive
|
|
12
|
+
* key in `wiring.settings`, `wiring.settingsTopLevel` or
|
|
13
|
+
* `wiring.packageScripts`, so such a pack is neither installed nor staged.
|
|
8
14
|
*/
|
|
9
15
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
10
16
|
import { dirname, join } from "node:path";
|
|
11
17
|
import { resolveAsset } from "./assets.js";
|
|
12
18
|
import { emitTemplate } from "./emit.js";
|
|
13
19
|
import { parseJsonc } from "./jsonc.js";
|
|
14
|
-
import {
|
|
20
|
+
import { blockedAbsentNote } from "./survey/internal/blocked-path.js";
|
|
21
|
+
import { probePath, readFailure, recordedReadCode, unreadableNote, } from "./survey/internal/read-guard.js";
|
|
22
|
+
import { isPrototypeSensitiveKey, isRecord, mergePackageScripts, mergeSettingsHooks, mergeSettingsTopLevel, mergeVerifySteps, } from "./merge-json.js";
|
|
15
23
|
/** `templates/packs`, resolved the same way `templatesCoreDir()` resolves `templates/core`. */
|
|
16
24
|
export function packsRootDir() {
|
|
17
25
|
return resolveAsset({ repo: "templates/packs", local: "templates/packs" });
|
|
@@ -26,7 +34,111 @@ export function listPackNames(root = packsRootDir()) {
|
|
|
26
34
|
.map((entry) => entry.name)
|
|
27
35
|
.sort();
|
|
28
36
|
}
|
|
29
|
-
/**
|
|
37
|
+
/** A pack's own `name` becomes a path segment under `.groundwork/packs/`, so it must be a bare lowercase identifier: no separators, no `..`. */
|
|
38
|
+
const PACK_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
39
|
+
const CAP_KEYS = [
|
|
40
|
+
"agents",
|
|
41
|
+
"skills",
|
|
42
|
+
"hooks",
|
|
43
|
+
"workflows",
|
|
44
|
+
"scripts",
|
|
45
|
+
];
|
|
46
|
+
/** True when `budget` has an own non-negative integer for every {@link CapCounts} key (one read per key). */
|
|
47
|
+
function isValidBudget(budget) {
|
|
48
|
+
if (!isRecord(budget))
|
|
49
|
+
return false;
|
|
50
|
+
return CAP_KEYS.every((key) => {
|
|
51
|
+
if (!Object.hasOwn(budget, key))
|
|
52
|
+
return false;
|
|
53
|
+
const value = budget[key];
|
|
54
|
+
return Number.isInteger(value) && value >= 0;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** The two CLI modes a pack's `modes` may name (see mode.ts). */
|
|
58
|
+
const PACK_MODES = ["fresh", "adopt"];
|
|
59
|
+
/** The `bin/lib/verify-steps.mjs` groups a pack's verify step may join. */
|
|
60
|
+
const VERIFY_GROUPS = [
|
|
61
|
+
"format",
|
|
62
|
+
"lint",
|
|
63
|
+
"typecheck",
|
|
64
|
+
"build",
|
|
65
|
+
"test",
|
|
66
|
+
];
|
|
67
|
+
/** Throws, naming the entry's 0-based `index` and the offending field, when one `wiring.verifySteps[]` entry isn't an object, lacks a string `id`/`name`, lacks a non-empty string-array `cmd`, or names an unknown `group`. Each field is read once into a local before it is checked. */
|
|
68
|
+
function assertValidVerifyStep(name, step, index) {
|
|
69
|
+
const where = `pack "${name}": pack.json's wiring.verifySteps[${String(index)}]`;
|
|
70
|
+
if (!isRecord(step)) {
|
|
71
|
+
throw new Error(`${where} must be an object, got ${String(JSON.stringify(step))}`);
|
|
72
|
+
}
|
|
73
|
+
for (const key of ["id", "name"]) {
|
|
74
|
+
const value = Object.hasOwn(step, key) ? step[key] : undefined;
|
|
75
|
+
if (typeof value !== "string") {
|
|
76
|
+
throw new Error(`${where}.${key} must be a string, got ${String(JSON.stringify(value))}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const cmd = Object.hasOwn(step, "cmd") ? step["cmd"] : undefined;
|
|
80
|
+
if (!Array.isArray(cmd) ||
|
|
81
|
+
cmd.length === 0 ||
|
|
82
|
+
!cmd.every((part) => typeof part === "string")) {
|
|
83
|
+
throw new Error(`${where}.cmd must be a non-empty array of strings, got ${String(JSON.stringify(cmd))}`);
|
|
84
|
+
}
|
|
85
|
+
const group = Object.hasOwn(step, "group")
|
|
86
|
+
? step["group"]
|
|
87
|
+
: undefined;
|
|
88
|
+
if (typeof group !== "string" || !VERIFY_GROUPS.includes(group)) {
|
|
89
|
+
throw new Error(`${where} group ${String(JSON.stringify(group))} must be one of ${VERIFY_GROUPS.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Throws, naming the pack, the field and the key, when one of `fragment`'s own
|
|
94
|
+
* enumerable keys is prototype-sensitive ({@link isPrototypeSensitiveKey}).
|
|
95
|
+
* `Object.keys` sees a `__proto__` key here because `parseJsonc`, like
|
|
96
|
+
* `JSON.parse`, creates it as an ordinary own data property. Only the
|
|
97
|
+
* fragment's own top-level keys are checked: they are the names a merge
|
|
98
|
+
* writes, while everything nested below them is an opaque value.
|
|
99
|
+
*/
|
|
100
|
+
function assertNoPrototypeSensitiveKeys(name, field, fragment) {
|
|
101
|
+
for (const key of Object.keys(fragment)) {
|
|
102
|
+
if (isPrototypeSensitiveKey(key)) {
|
|
103
|
+
throw new Error(`pack "${name}": pack.json's wiring.${field} must not use the prototype-sensitive key ${JSON.stringify(key)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Throws, naming the offending key or value, when `wiring.settings`/`packageScripts` is missing or isn't an object, `verifySteps` is missing or isn't an array, any `verifySteps[]` entry is malformed (see {@link assertValidVerifyStep}), or `settings`, `packageScripts` or (when it is an object) `settingsTopLevel` has a prototype-sensitive own key (see {@link assertNoPrototypeSensitiveKeys}).
|
|
109
|
+
* Those three are the only wiring fields whose keys a merge writes; `verifySteps` ids are values. An absent or non-object `settingsTopLevel` is not rejected here.
|
|
110
|
+
*/
|
|
111
|
+
function assertValidWiringShape(name, wiring) {
|
|
112
|
+
for (const key of ["settings", "packageScripts"]) {
|
|
113
|
+
const value = Object.hasOwn(wiring, key) ? wiring[key] : undefined;
|
|
114
|
+
if (!isRecord(value)) {
|
|
115
|
+
throw new Error(`pack "${name}": pack.json's wiring.${key} must be an object`);
|
|
116
|
+
}
|
|
117
|
+
assertNoPrototypeSensitiveKeys(name, key, value);
|
|
118
|
+
}
|
|
119
|
+
const topLevel = Object.hasOwn(wiring, "settingsTopLevel")
|
|
120
|
+
? wiring["settingsTopLevel"]
|
|
121
|
+
: undefined;
|
|
122
|
+
if (isRecord(topLevel)) {
|
|
123
|
+
assertNoPrototypeSensitiveKeys(name, "settingsTopLevel", topLevel);
|
|
124
|
+
}
|
|
125
|
+
const steps = Object.hasOwn(wiring, "verifySteps")
|
|
126
|
+
? wiring["verifySteps"]
|
|
127
|
+
: undefined;
|
|
128
|
+
if (!Array.isArray(steps)) {
|
|
129
|
+
throw new Error(`pack "${name}": pack.json's wiring.verifySteps must be an array`);
|
|
130
|
+
}
|
|
131
|
+
steps.forEach((step, index) => {
|
|
132
|
+
assertValidVerifyStep(name, step, index);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
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. */
|
|
30
142
|
export function loadPack(name, root = packsRootDir()) {
|
|
31
143
|
const packDir = join(root, name);
|
|
32
144
|
const manifestPath = join(packDir, "pack.json");
|
|
@@ -38,10 +150,47 @@ export function loadPack(name, root = packsRootDir()) {
|
|
|
38
150
|
if (!parsed.ok) {
|
|
39
151
|
throw new Error(`pack "${name}": pack.json failed to parse -- ${parsed.error}`);
|
|
40
152
|
}
|
|
41
|
-
const
|
|
153
|
+
const raw = parsed.value;
|
|
154
|
+
if (!isRecord(raw)) {
|
|
155
|
+
throw new Error(`pack "${name}": pack.json must be an object, got ${String(JSON.stringify(raw))}`);
|
|
156
|
+
}
|
|
157
|
+
const manifest = raw;
|
|
42
158
|
if (manifest.schemaVersion !== 1) {
|
|
43
159
|
throw new Error(`pack "${name}": unsupported pack.json schemaVersion ${JSON.stringify(manifest.schemaVersion)}`);
|
|
44
160
|
}
|
|
161
|
+
const manifestName = manifest.name;
|
|
162
|
+
if (typeof manifestName !== "string" ||
|
|
163
|
+
!PACK_NAME_PATTERN.test(manifestName)) {
|
|
164
|
+
throw new Error(`pack "${name}": pack.json's pack name ${JSON.stringify(manifestName)} must match ${String(PACK_NAME_PATTERN)} (a bare lowercase identifier, no path separators)`);
|
|
165
|
+
}
|
|
166
|
+
const modes = manifest.modes;
|
|
167
|
+
if (!Array.isArray(modes) ||
|
|
168
|
+
modes.length === 0 ||
|
|
169
|
+
!modes.every((mode) => typeof mode === "string")) {
|
|
170
|
+
throw new Error(`pack "${name}": pack.json's modes must be a non-empty array of strings`);
|
|
171
|
+
}
|
|
172
|
+
const badMode = modes.find((mode) => !PACK_MODES.includes(mode));
|
|
173
|
+
if (badMode !== undefined) {
|
|
174
|
+
throw new Error(`pack "${name}": pack.json's mode ${JSON.stringify(badMode)} must be one of ${PACK_MODES.join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
const wiring = manifest.wiring;
|
|
177
|
+
if (!isRecord(wiring)) {
|
|
178
|
+
throw new Error(`pack "${name}": pack.json's wiring must be an object`);
|
|
179
|
+
}
|
|
180
|
+
assertValidWiringShape(name, wiring);
|
|
181
|
+
const budget = manifest.budget;
|
|
182
|
+
if (!isValidBudget(budget)) {
|
|
183
|
+
throw new Error(`pack "${name}": pack.json's budget must set ${CAP_KEYS.join(", ")} to non-negative integers`);
|
|
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
|
+
}
|
|
189
|
+
// pack-stage.ts's stagePacks keys each pack's staging directory on
|
|
190
|
+
// manifest.name, so a mismatch would let two pack directories collide.
|
|
191
|
+
if (manifestName !== name) {
|
|
192
|
+
throw new Error(`pack "${name}": pack.json's name "${manifestName}" does not match its directory name "${name}"`);
|
|
193
|
+
}
|
|
45
194
|
return { manifest, filesDir: join(packDir, "files") };
|
|
46
195
|
}
|
|
47
196
|
function readJsonOrThrow(path) {
|
|
@@ -126,35 +275,96 @@ export function installPack(pack, targetDir, tokens) {
|
|
|
126
275
|
return { filesWritten, budget: manifest.budget };
|
|
127
276
|
}
|
|
128
277
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
278
|
+
* Reads `.claude/settings.json` for {@link observeWiring}. A failure that is
|
|
279
|
+
* a fact about the project's own tree -- a permission failure
|
|
280
|
+
* (`EACCES`/`EPERM`), a directory at the path (`EISDIR`), the file vanishing
|
|
281
|
+
* after the exists probe or a dangling symlink (`ENOENT`), a symlink loop
|
|
282
|
+
* (`ELOOP`) -- is recorded as an observation naming the path and errno, and
|
|
283
|
+
* once in `undetermined` (so the adoption report shows it), and `undefined`
|
|
284
|
+
* is returned; any other errno is about the machine and throws, naming the
|
|
285
|
+
* path with the original failure as `cause`.
|
|
135
286
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
287
|
+
function readSettingsOrObserve(settingsPath, observations, undetermined) {
|
|
288
|
+
try {
|
|
289
|
+
return readFileSync(settingsPath, "utf8");
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
const code = recordedReadCode(error);
|
|
293
|
+
if (code === undefined)
|
|
294
|
+
throw readFailure(settingsPath, error);
|
|
295
|
+
observations.push(`.claude/settings.json (${settingsPath}) exists but could not be read (${code})`);
|
|
296
|
+
// Called once per pack against the same file: record the note once.
|
|
297
|
+
const note = unreadableNote(settingsPath, code);
|
|
298
|
+
if (!undetermined.includes(note))
|
|
299
|
+
undetermined.push(note);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Whether `path` exists, for {@link observeWiring}. A real `stat`, never
|
|
305
|
+
* `existsSync`, so a file under a directory this process cannot search is
|
|
306
|
+
* not observed as missing: an `EACCES`/`EPERM`/`ELOOP` is recorded as an
|
|
307
|
+
* observation naming the path and errno, and `undefined` is returned so the
|
|
308
|
+
* caller states neither "found" nor "not found". `ENOENT`/`ENOTDIR` answers
|
|
309
|
+
* `false` only when nothing is really there: a dangling symlink at the path
|
|
310
|
+
* or an ancestor, or a regular file blocking an ancestor, is observed and
|
|
311
|
+
* recorded once in `undetermined` (the same note `conflicts.ts` records via
|
|
312
|
+
* `blockedAbsentNote`) and answers `undefined`. Any other errno throws (see
|
|
313
|
+
* `probePath`).
|
|
314
|
+
*/
|
|
315
|
+
function existsOrObserve(path, targetDir, observations, undetermined) {
|
|
316
|
+
const probe = probePath(path);
|
|
317
|
+
if (probe.kind === "unresolvable") {
|
|
318
|
+
observations.push(`could not check whether ${path} exists (${probe.code}) -- left undetermined, not reported missing`);
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
if (probe.kind === "present")
|
|
322
|
+
return true;
|
|
323
|
+
const note = blockedAbsentNote(path, targetDir);
|
|
324
|
+
if (note === undefined)
|
|
325
|
+
return false;
|
|
326
|
+
observations.push(`${note} -- left undetermined, not reported missing`);
|
|
327
|
+
// Called once per pack against the same path: record the note once.
|
|
328
|
+
if (!undetermined.includes(note))
|
|
329
|
+
undetermined.push(note);
|
|
330
|
+
return undefined;
|
|
141
331
|
}
|
|
142
332
|
/**
|
|
143
333
|
* Index-level, adopt-mode-only facts about how a pack's wiring would land
|
|
144
334
|
* against a real project's current `.claude/settings.json` and
|
|
145
335
|
* `bin/lib/verify-steps.packs.json` -- never a verdict on whether it will
|
|
146
|
-
* work
|
|
147
|
-
*
|
|
336
|
+
* work. That verdict is a judgment call for `/customize`'s Step 0 (the
|
|
337
|
+
* adopt-mode reconcile step in `/customize`) to make after reading the
|
|
338
|
+
* project's real gate runner and hook config. A path this process cannot
|
|
339
|
+
* reach (`EACCES`/`EPERM`/`ELOOP`) is observed with its errno, never as
|
|
340
|
+
* "not found"; nor is a dangling symlink or a file blocking an ancestor
|
|
341
|
+
* directory, which is also recorded once in `undetermined`. A
|
|
342
|
+
* `.claude/settings.json` that exists but cannot be read for a reason that
|
|
343
|
+
* is a property of the project's tree (`EACCES`/`EPERM`, `EISDIR`, `ENOENT`,
|
|
344
|
+
* `ELOOP`) is observed with its errno and also recorded once in
|
|
345
|
+
* `undetermined` -- adopt mode passes the survey's own list, so the report
|
|
346
|
+
* shows it; any other errno throws.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* const undetermined: string[] = [];
|
|
351
|
+
* const observations = observeWiring(targetDir, pack.manifest, undetermined);
|
|
352
|
+
* ```
|
|
148
353
|
*/
|
|
149
|
-
export function observeWiring(targetDir, manifest) {
|
|
354
|
+
export function observeWiring(targetDir, manifest, undetermined = []) {
|
|
150
355
|
const observations = [];
|
|
151
356
|
const settingsPath = join(targetDir, ".claude", "settings.json");
|
|
152
|
-
|
|
357
|
+
const settingsExists = existsOrObserve(settingsPath, targetDir, observations, undetermined);
|
|
358
|
+
if (settingsExists === false) {
|
|
153
359
|
observations.push("no .claude/settings.json found");
|
|
154
360
|
}
|
|
155
|
-
else {
|
|
156
|
-
const
|
|
157
|
-
|
|
361
|
+
else if (settingsExists) {
|
|
362
|
+
const content = readSettingsOrObserve(settingsPath, observations, undetermined);
|
|
363
|
+
const parsed = content === undefined ? undefined : parseJsonc(content);
|
|
364
|
+
if (parsed === undefined) {
|
|
365
|
+
// Unreadable -- already recorded as an observation.
|
|
366
|
+
}
|
|
367
|
+
else if (!parsed.ok || !isRecord(parsed.value)) {
|
|
158
368
|
observations.push(".claude/settings.json exists but could not be parsed");
|
|
159
369
|
}
|
|
160
370
|
else {
|
|
@@ -172,14 +382,17 @@ export function observeWiring(targetDir, manifest) {
|
|
|
172
382
|
}
|
|
173
383
|
}
|
|
174
384
|
}
|
|
175
|
-
if (
|
|
385
|
+
if (existsOrObserve(join(targetDir, ".claude", "settings.local.json"), targetDir, observations, undetermined) === true) {
|
|
176
386
|
observations.push(".claude/settings.local.json is present and may shadow a merged hook entry");
|
|
177
387
|
}
|
|
178
388
|
if (manifest.wiring.verifySteps.length > 0) {
|
|
179
389
|
const stepsPath = join(targetDir, "bin", "lib", "verify-steps.packs.json");
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
390
|
+
const stepsExist = existsOrObserve(stepsPath, targetDir, observations, undetermined);
|
|
391
|
+
if (stepsExist !== undefined) {
|
|
392
|
+
observations.push(stepsExist
|
|
393
|
+
? "bin/lib/verify-steps.packs.json exists"
|
|
394
|
+
: "no bin/lib/verify-steps.packs.json found -- no bin/verify.mjs-shaped gate runner detected");
|
|
395
|
+
}
|
|
183
396
|
}
|
|
184
397
|
return observations;
|
|
185
398
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE -- do not hand-edit. Run `node bin/build-design-tokens.mjs`
|
|
3
|
+
* to regenerate from design/source/dtcg (the vendored m3l-design tokens).
|
|
4
|
+
* `--check` (used by `pnpm verify`) fails instead of writing on drift.
|
|
5
|
+
* Source of truth: design/source/dtcg (DTCG 2025.10). See design/README.md.
|
|
6
|
+
*/
|
|
7
|
+
/** One role's resolved terminal color, per theme -- see term.ts's `paint()`. */
|
|
8
|
+
export interface PaletteRoleColors {
|
|
9
|
+
readonly success: string;
|
|
10
|
+
readonly info: string;
|
|
11
|
+
readonly warning: string;
|
|
12
|
+
readonly danger: string;
|
|
13
|
+
readonly accent: string;
|
|
14
|
+
readonly secondary: string;
|
|
15
|
+
}
|
|
16
|
+
/** {@link PaletteRoleColors} for each theme -- see term.ts's `resolveThemeId()`. */
|
|
17
|
+
export interface Palette {
|
|
18
|
+
readonly light: PaletteRoleColors;
|
|
19
|
+
readonly dark: PaletteRoleColors;
|
|
20
|
+
}
|
|
21
|
+
/** The resolved terminal palette, light and dark -- see term.ts's `paint()`. */
|
|
22
|
+
export declare const PALETTE: Palette;
|
|
23
|
+
//# sourceMappingURL=palette.d.ts.map
|
package/dist/palette.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: Copyright the m3l-groundwork contributors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/** The resolved terminal palette, light and dark -- see term.ts's `paint()`. */
|
|
4
|
+
export const PALETTE = {
|
|
5
|
+
light: {
|
|
6
|
+
success: "#086d31",
|
|
7
|
+
info: "#065da0",
|
|
8
|
+
warning: "#7d4f0a",
|
|
9
|
+
danger: "#963730",
|
|
10
|
+
accent: "#692746",
|
|
11
|
+
secondary: "#554d50",
|
|
12
|
+
},
|
|
13
|
+
dark: {
|
|
14
|
+
success: "#89d298",
|
|
15
|
+
info: "#8ac3fe",
|
|
16
|
+
warning: "#ebb16c",
|
|
17
|
+
danger: "#fda297",
|
|
18
|
+
accent: "#d1789e",
|
|
19
|
+
secondary: "#cdc5c8",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=palette.js.map
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,22 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an install wrote.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { installCustomizeSkill } from "./plugin.js";
|
|
7
|
+
*
|
|
8
|
+
* const { filesWritten } = installCustomizeSkill("/work/app");
|
|
9
|
+
* filesWritten.at(-1); // ".claude/skills/customize/SKILL.md"
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
1
12
|
export interface InstallPluginResult {
|
|
13
|
+
/** Paths relative to the project root, in write order (`SKILL.md` last); empty when the destination already held this exact payload. */
|
|
2
14
|
filesWritten: string[];
|
|
3
15
|
}
|
|
4
16
|
/**
|
|
5
17
|
* Installs the skill into `<targetDir>/.claude/skills/customize/`. Used by
|
|
6
|
-
* fresh-bootstrap mode, where the directory is
|
|
18
|
+
* fresh-bootstrap mode, where the directory is normally new (`--force` may
|
|
19
|
+
* point it at a non-empty one or an earlier install). An earlier install
|
|
20
|
+
* that is already byte-identical to this payload is left untouched
|
|
21
|
+
* (`filesWritten` is empty); otherwise any existing `SKILL.md` is removed
|
|
22
|
+
* first, then each payload file is removed and recreated. A symlinked
|
|
23
|
+
* `.claude`, `.claude/skills` or `.claude/skills/customize` is refused, not
|
|
24
|
+
* routed around.
|
|
25
|
+
*
|
|
26
|
+
* @throws `Error` ("could not install the /customize skill ...", raw error
|
|
27
|
+
* as `cause`) on a missing or unreadable source file, a directory at any
|
|
28
|
+
* payload name, a payload name or existing file that cannot be `lstat`ed,
|
|
29
|
+
* or an existing regular file whose read fails with anything other than
|
|
30
|
+
* `EACCES`/`EPERM` (all before anything is removed or written), a symlinked
|
|
31
|
+
* or non-directory directory component, any fs failure, or a failed write
|
|
32
|
+
* -- after removing every file this call wrote. An existing regular file
|
|
33
|
+
* whose read fails with `EACCES` or `EPERM` is not a failure: it is replaced
|
|
34
|
+
* like any stale copy. Entries it replaced before the failure are not
|
|
35
|
+
* restored; the error names them. A failure to locate the default
|
|
36
|
+
* `sourceDir` is thrown the same way. The message ends, once, by saying to
|
|
37
|
+
* fix the cause (for a symlink: remove it), then retry
|
|
38
|
+
* the same command with `--fresh --force` added -- a plain re-run would
|
|
39
|
+
* adopt the now-non-empty target -- and nowhere in the error chain gives
|
|
40
|
+
* adopt mode's bare "re-run the CLI" advice.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { installCustomizeSkill } from "./plugin.js";
|
|
45
|
+
*
|
|
46
|
+
* installCustomizeSkill("/work/app").filesWritten.length; // 5
|
|
47
|
+
* ```
|
|
7
48
|
*/
|
|
8
49
|
export declare function installCustomizeSkill(targetDir: string, sourceDir?: string): InstallPluginResult;
|
|
9
50
|
type InstallLocation = "claude" | "groundwork" | "already-present";
|
|
51
|
+
/**
|
|
52
|
+
* What the adopt-mode install did, and where.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { installCustomizeSkillGuarded } from "./plugin.js";
|
|
57
|
+
*
|
|
58
|
+
* const result = installCustomizeSkillGuarded("/work/app");
|
|
59
|
+
* if (result.fallbackReason !== undefined) console.log(result.fallbackReason);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
10
62
|
export interface GuardedInstallResult {
|
|
63
|
+
/** Paths relative to the project root, in write order (`SKILL.md` last); empty for `"already-present"`. */
|
|
11
64
|
filesWritten: string[];
|
|
12
65
|
location: InstallLocation;
|
|
66
|
+
/**
|
|
67
|
+
* Set on every `"groundwork"` result, and only then: why
|
|
68
|
+
* `.claude/skills/customize/` could not be used, naming the path
|
|
69
|
+
* responsible -- a symlinked or non-directory component, or the existing
|
|
70
|
+
* project entry under one of the skill's payload names.
|
|
71
|
+
*/
|
|
72
|
+
fallbackReason?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Set exactly when `fallbackReason` is: `"component"` when a
|
|
75
|
+
* `.claude`/`.claude/skills`/`.claude/skills/customize` component is a
|
|
76
|
+
* symlink or not a directory (no project-local copy of the skill exists),
|
|
77
|
+
* `"entry"` when a project-owned entry sits under one of the skill's
|
|
78
|
+
* payload names (a project copy exists, and is what a user would replace).
|
|
79
|
+
*/
|
|
80
|
+
fallbackCause?: "entry" | "component";
|
|
13
81
|
}
|
|
14
82
|
/**
|
|
15
|
-
* Adopt-mode install: purely additive, never overwrites
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
83
|
+
* Adopt-mode install: purely additive, never overwrites or removes a project
|
|
84
|
+
* entry under `.claude/skills/customize/`.
|
|
85
|
+
*
|
|
86
|
+
* - If `.claude`, `.claude/skills` or `.claude/skills/customize` is a
|
|
87
|
+
* symlink or not a directory, the skill is written to
|
|
88
|
+
* `.groundwork/customize/` instead, `fallbackReason` names that path and
|
|
89
|
+
* `fallbackCause` is `"component"` -- the entry (and any link target) is
|
|
90
|
+
* left untouched.
|
|
91
|
+
* - Otherwise, if every payload file already there is a regular file
|
|
92
|
+
* matching what this CLI ships byte-for-byte: with all five present the
|
|
93
|
+
* result is `"already-present"` (nothing written); with no `SKILL.md` file
|
|
94
|
+
* (none at all, or an install interrupted before writing it) the missing
|
|
95
|
+
* files are written into `.claude/skills/customize/`, `SKILL.md` last,
|
|
96
|
+
* never rewriting or removing the correct ones.
|
|
97
|
+
* - Anything else under a payload name (a differing file, a regular file
|
|
98
|
+
* that cannot be read, a symlink -- dangling or not -- a directory, a
|
|
99
|
+
* `SKILL.md` without its data; detected by `lstat`) is kept as the
|
|
100
|
+
* project's own: the skill is written to `.groundwork/customize/` instead,
|
|
101
|
+
* `fallbackReason` names that entry (saying so when it could not be read)
|
|
102
|
+
* and `fallbackCause` is `"entry"`. Adopt mode never guesses at a project
|
|
103
|
+
* entry it cannot compare, and the fallback does not guess either: it
|
|
104
|
+
* leaves that entry exactly as it was. Claude Code does not load a skill
|
|
105
|
+
* from there; the caller must say so.
|
|
106
|
+
*
|
|
107
|
+
* Writes into `.claude/skills/customize/` use `"wx"` only, so an entry that
|
|
108
|
+
* appears there mid-install fails the run (rolled back) rather than being
|
|
109
|
+
* replaced. Writes into the CLI-owned `.groundwork/customize/` replace that
|
|
110
|
+
* directory's payload files (any `SKILL.md` removed first, then each file
|
|
111
|
+
* removed and recreated with `"wx"`); a symlinked `.groundwork` or
|
|
112
|
+
* `.groundwork/customize`, or a directory at any payload name there, is
|
|
113
|
+
* refused, never routed around.
|
|
114
|
+
*
|
|
115
|
+
* @throws `Error` ("could not install the /customize skill ...", raw error
|
|
116
|
+
* as `cause`) on a missing or unreadable source file (before anything is
|
|
117
|
+
* written), a payload name under `.claude/skills/customize/` that cannot be
|
|
118
|
+
* `lstat`ed or read (a regular file there whose read is refused with
|
|
119
|
+
* `EACCES`/`EPERM` falls back instead; any other read failure throws before
|
|
120
|
+
* anything is written), any other fs failure while probing or writing, a
|
|
121
|
+
* symlinked `.groundwork`/`.groundwork/customize`, or a directory at a
|
|
122
|
+
* payload name there -- after removing every file this call wrote. A failed
|
|
123
|
+
* `.groundwork/customize/` install also names why the fallback was taken.
|
|
124
|
+
* A failure to locate the default `sourceDir` is thrown the same way. The
|
|
125
|
+
* message ends by
|
|
126
|
+
* saying to fix the cause (for a symlink: remove it) and re-run the CLI,
|
|
127
|
+
* once.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { installCustomizeSkillGuarded } from "./plugin.js";
|
|
132
|
+
*
|
|
133
|
+
* const { location } = installCustomizeSkillGuarded("/work/app");
|
|
134
|
+
* // "claude" | "groundwork" | "already-present"
|
|
135
|
+
* ```
|
|
20
136
|
*/
|
|
21
137
|
export declare function installCustomizeSkillGuarded(targetDir: string, sourceDir?: string): GuardedInstallResult;
|
|
22
138
|
export {};
|