@monte3l/groundwork 0.0.0
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 +23 -0
- package/bin/m3l-groundwork.mjs +10 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +79 -0
- package/dist/caps.d.ts +25 -0
- package/dist/caps.js +69 -0
- package/dist/conflicts.d.ts +12 -0
- package/dist/conflicts.js +77 -0
- package/dist/emit.d.ts +7 -0
- package/dist/emit.js +42 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +9 -0
- package/dist/harness/conformance.d.ts +20 -0
- package/dist/harness/conformance.js +18 -0
- package/dist/harness/frontmatter.d.ts +38 -0
- package/dist/harness/frontmatter.js +204 -0
- package/dist/harness/grade.d.ts +4 -0
- package/dist/harness/grade.js +105 -0
- package/dist/harness/rules.d.ts +55 -0
- package/dist/harness/rules.js +580 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.js +9 -0
- package/dist/inventory.d.ts +63 -0
- package/dist/inventory.js +66 -0
- package/dist/jsonc.d.ts +14 -0
- package/dist/jsonc.js +83 -0
- package/dist/main.d.ts +24 -0
- package/dist/main.js +297 -0
- package/dist/merge-json.d.ts +74 -0
- package/dist/merge-json.js +135 -0
- package/dist/mode.d.ts +19 -0
- package/dist/mode.js +53 -0
- package/dist/packs.d.ts +61 -0
- package/dist/packs.js +186 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +79 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +323 -0
- package/dist/survey/fs-walk.d.ts +14 -0
- package/dist/survey/fs-walk.js +60 -0
- package/dist/survey/survey-docs.d.ts +4 -0
- package/dist/survey/survey-docs.js +69 -0
- package/dist/survey/survey-harness.d.ts +4 -0
- package/dist/survey/survey-harness.js +121 -0
- package/dist/survey/survey-shape.d.ts +4 -0
- package/dist/survey/survey-shape.js +182 -0
- package/dist/survey/survey-toolchain.d.ts +4 -0
- package/dist/survey/survey-toolchain.js +217 -0
- package/dist/survey/survey.d.ts +5 -0
- package/dist/survey/survey.js +21 -0
- package/dist/survey/types.d.ts +117 -0
- package/dist/survey/types.js +8 -0
- package/dist/tokens.d.ts +13 -0
- package/dist/tokens.js +13 -0
- package/dist/toolchain/conformance.d.ts +20 -0
- package/dist/toolchain/conformance.js +30 -0
- package/dist/toolchain/grade.d.ts +4 -0
- package/dist/toolchain/grade.js +244 -0
- package/dist/toolchain/rules.d.ts +118 -0
- package/dist/toolchain/rules.js +706 -0
- package/dist/toolchain/tsconfig-chain.d.ts +36 -0
- package/dist/toolchain/tsconfig-chain.js +116 -0
- package/dist/toolchain/types.d.ts +27 -0
- package/dist/toolchain/types.js +9 -0
- package/package.json +59 -0
- package/plugin/skills/customize/SKILL.md +305 -0
- package/plugin/src/domain-map.ts +134 -0
- package/plugin/src/index.ts +4 -0
- package/plugin/src/kind-facet-map.ts +174 -0
- package/plugin/src/pack-map.ts +65 -0
- package/templates/core/.claude/agents/Explore.md +43 -0
- package/templates/core/.claude/agents/code-implementer.md +258 -0
- package/templates/core/.claude/agents/code-reviewer.md +163 -0
- package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
- package/templates/core/.claude/agents/test-author.md +211 -0
- package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
- package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
- package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
- package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
- package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
- package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
- package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
- package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
- package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
- package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
- package/templates/core/.claude/rules/agent-dispatch.md +121 -0
- package/templates/core/.claude/rules/refactoring.md +52 -0
- package/templates/core/.claude/rules/src.md +114 -0
- package/templates/core/.claude/rules/tests.md +129 -0
- package/templates/core/.claude/settings.json +111 -0
- package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
- package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
- package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
- package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
- package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
- package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
- package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
- package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
- package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
- package/templates/core/.github/workflows/ci.yml +123 -0
- package/templates/core/.github/workflows/dependency-review.yml +26 -0
- package/templates/core/.github/workflows/security-audit.yml +54 -0
- package/templates/core/.node-version +1 -0
- package/templates/core/.prettierignore +5 -0
- package/templates/core/.prettierrc.json +4 -0
- package/templates/core/CLAUDE.md +127 -0
- package/templates/core/README.md +24 -0
- package/templates/core/_gitignore +19 -0
- package/templates/core/_npmrc +1 -0
- package/templates/core/bin/check-exports.mjs +92 -0
- package/templates/core/bin/check-harness.mjs +27 -0
- package/templates/core/bin/check-node-version.mjs +51 -0
- package/templates/core/bin/check-toolchain.mjs +20 -0
- package/templates/core/bin/lib/agent-roster.mjs +8 -0
- package/templates/core/bin/lib/frontmatter.mjs +210 -0
- package/templates/core/bin/lib/harness-rules.mjs +916 -0
- package/templates/core/bin/lib/protected-paths.mjs +23 -0
- package/templates/core/bin/lib/report.mjs +56 -0
- package/templates/core/bin/lib/signed-range.mjs +178 -0
- package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
- package/templates/core/bin/lib/verify-steps.mjs +131 -0
- package/templates/core/bin/lib/verify-steps.packs.json +1 -0
- package/templates/core/bin/lint-commit.mjs +50 -0
- package/templates/core/bin/strip-claude-trailers.mjs +25 -0
- package/templates/core/bin/verify.mjs +64 -0
- package/templates/core/commitlint.config.js +11 -0
- package/templates/core/docs/research/harness-refresh.md +27 -0
- package/templates/core/docs/research/typescript-refresh.md +32 -0
- package/templates/core/eslint.config.js +105 -0
- package/templates/core/knip.json +6 -0
- package/templates/core/lefthook.yml +39 -0
- package/templates/core/package.json +58 -0
- package/templates/core/pnpm-workspace.yaml +13 -0
- package/templates/core/src/index.ts +12 -0
- package/templates/core/tests/index.test.ts +8 -0
- package/templates/core/tsconfig.base.json +36 -0
- package/templates/core/tsconfig.build.json +10 -0
- package/templates/core/tsconfig.json +11 -0
- package/templates/core/vitest.config.ts +32 -0
- package/templates/packs/README.md +81 -0
- package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
- package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
- package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
- package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
- package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
- package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
- package/templates/packs/harness-extras/pack.json +65 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
- package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
- package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
- package/templates/packs/statusline/pack.json +31 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, deterministic merges over parsed JSON -- the entire mechanism that
|
|
3
|
+
* lets a pack extend `.claude/settings.json` (its `hooks` block and a few
|
|
4
|
+
* top-level keys), `package.json`'s `scripts`, and
|
|
5
|
+
* `bin/lib/verify-steps.packs.json` without `packages/cli` ever parsing
|
|
6
|
+
* YAML or JavaScript. Every merge is append-only (it never rebuilds an
|
|
7
|
+
* object wholesale, which would risk reordering keys Prettier would
|
|
8
|
+
* otherwise preserve) and idempotent (merging the same fragment twice
|
|
9
|
+
* produces the same result as merging it once).
|
|
10
|
+
*/
|
|
11
|
+
export function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Merges a pack's `.claude/settings.json` hook fragment into an existing
|
|
16
|
+
* settings object. Per event, an entry is matched by `matcher` (both absent
|
|
17
|
+
* counts as a match -- e.g. `PreCompact`, which has none); within a matched
|
|
18
|
+
* entry, only `hooks` commands not already present (compared by exact
|
|
19
|
+
* `command` string) are appended. A command that matches on `command` but
|
|
20
|
+
* differs in its other fields (`if`, `timeout`) is a hard collision, never
|
|
21
|
+
* a silent overwrite.
|
|
22
|
+
*/
|
|
23
|
+
export function mergeSettingsHooks(existing, fragment) {
|
|
24
|
+
const settings = isRecord(existing)
|
|
25
|
+
? { ...existing }
|
|
26
|
+
: {};
|
|
27
|
+
const existingHooks = settings["hooks"];
|
|
28
|
+
const hooks = isRecord(existingHooks)
|
|
29
|
+
? { ...existingHooks }
|
|
30
|
+
: {};
|
|
31
|
+
for (const [event, entries] of Object.entries(fragment)) {
|
|
32
|
+
const existingEntries = Array.isArray(hooks[event])
|
|
33
|
+
? [...hooks[event]]
|
|
34
|
+
: [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const matchIndex = existingEntries.findIndex((candidate) => (candidate.matcher ?? undefined) === (entry.matcher ?? undefined));
|
|
37
|
+
if (matchIndex === -1) {
|
|
38
|
+
existingEntries.push(entry);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const matched = existingEntries[matchIndex];
|
|
42
|
+
if (matched === undefined) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const mergedHooks = [...matched.hooks];
|
|
46
|
+
for (const hookCmd of entry.hooks) {
|
|
47
|
+
const duplicate = mergedHooks.find((candidate) => candidate.command === hookCmd.command);
|
|
48
|
+
if (duplicate) {
|
|
49
|
+
if (JSON.stringify(duplicate) !== JSON.stringify(hookCmd)) {
|
|
50
|
+
throw new Error(`settings.json merge collision: "${event}" (matcher ${JSON.stringify(entry.matcher)}) already has a hook for "${hookCmd.command}" with different config`);
|
|
51
|
+
}
|
|
52
|
+
continue; // Identical entry already present -- idempotent no-op.
|
|
53
|
+
}
|
|
54
|
+
mergedHooks.push(hookCmd);
|
|
55
|
+
}
|
|
56
|
+
existingEntries[matchIndex] = { ...matched, hooks: mergedHooks };
|
|
57
|
+
}
|
|
58
|
+
hooks[event] = existingEntries;
|
|
59
|
+
}
|
|
60
|
+
return { ...settings, hooks };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Merges a pack's top-level `.claude/settings.json` keys (`statusLine`,
|
|
64
|
+
* `subagentStatusLine` -- settings that are not hook registrations) into an
|
|
65
|
+
* existing settings object. Disjoint from `mergeSettingsHooks`, which owns the
|
|
66
|
+
* `hooks` block: a fragment key of `hooks` is rejected outright rather than
|
|
67
|
+
* silently clobbering it. A key not yet present is appended; one already
|
|
68
|
+
* present with an identical value is a no-op; one present with a different
|
|
69
|
+
* value is a hard collision, never a silent overwrite -- an adopted project's
|
|
70
|
+
* own `statusLine` is the user's to replace deliberately.
|
|
71
|
+
*/
|
|
72
|
+
export function mergeSettingsTopLevel(existing, fragment) {
|
|
73
|
+
const settings = isRecord(existing)
|
|
74
|
+
? { ...existing }
|
|
75
|
+
: {};
|
|
76
|
+
for (const [key, value] of Object.entries(fragment)) {
|
|
77
|
+
if (key === "hooks") {
|
|
78
|
+
throw new Error('settings.json merge: "hooks" is owned by mergeSettingsHooks and cannot be set as a top-level key');
|
|
79
|
+
}
|
|
80
|
+
if (!Object.hasOwn(settings, key)) {
|
|
81
|
+
settings[key] = value;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (JSON.stringify(settings[key]) !== JSON.stringify(value)) {
|
|
85
|
+
throw new Error(`settings.json merge collision: "${key}" is already set with a different value`);
|
|
86
|
+
}
|
|
87
|
+
// Identical value already present -- idempotent no-op.
|
|
88
|
+
}
|
|
89
|
+
return settings;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Merges a pack's `package.json` script additions into the existing
|
|
93
|
+
* `scripts` block. Never overwrites a differing existing script -- the
|
|
94
|
+
* collision is returned for the caller to report, consistent with adopt
|
|
95
|
+
* mode's "report, then the user decides per conflict" policy.
|
|
96
|
+
*/
|
|
97
|
+
export function mergePackageScripts(existing, additions) {
|
|
98
|
+
const scripts = { ...(existing ?? {}) };
|
|
99
|
+
const collisions = [];
|
|
100
|
+
for (const [name, cmd] of Object.entries(additions)) {
|
|
101
|
+
const currentValue = scripts[name];
|
|
102
|
+
if (currentValue !== undefined) {
|
|
103
|
+
if (currentValue !== cmd) {
|
|
104
|
+
collisions.push({ name, existing: currentValue, incoming: cmd });
|
|
105
|
+
}
|
|
106
|
+
continue; // Either identical (no-op) or a collision already recorded.
|
|
107
|
+
}
|
|
108
|
+
scripts[name] = cmd;
|
|
109
|
+
}
|
|
110
|
+
return { scripts, collisions };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Merges a pack's verify-step additions into an existing step array
|
|
114
|
+
* (`bin/lib/verify-steps.packs.json`'s parsed contents). Append-by-`id`,
|
|
115
|
+
* idempotent; a same-`id`-different-content collision is a hard error.
|
|
116
|
+
*/
|
|
117
|
+
export function mergeVerifySteps(existing, additions) {
|
|
118
|
+
const current = Array.isArray(existing)
|
|
119
|
+
? [...existing]
|
|
120
|
+
: [];
|
|
121
|
+
for (const step of additions) {
|
|
122
|
+
const existingIndex = current.findIndex((candidate) => candidate.id === step.id);
|
|
123
|
+
if (existingIndex === -1) {
|
|
124
|
+
current.push(step);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const existingStep = current[existingIndex];
|
|
128
|
+
if (JSON.stringify(existingStep) !== JSON.stringify(step)) {
|
|
129
|
+
throw new Error(`verify-steps.packs.json merge collision: step "${step.id}" is already registered with different config`);
|
|
130
|
+
}
|
|
131
|
+
// Identical entry already present -- idempotent no-op.
|
|
132
|
+
}
|
|
133
|
+
return current;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=merge-json.js.map
|
package/dist/mode.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Mode = "fresh" | "adopt";
|
|
2
|
+
export interface ModeDetection {
|
|
3
|
+
mode: Mode;
|
|
4
|
+
signal: string;
|
|
5
|
+
}
|
|
6
|
+
/** Inspects `dir` and reports which mode it implies, and why. */
|
|
7
|
+
export declare function detectMode(dir: string): ModeDetection;
|
|
8
|
+
export interface ModeFlags {
|
|
9
|
+
adopt: boolean;
|
|
10
|
+
fresh: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Applies `--adopt`/`--fresh` overrides onto an auto-detected mode. Throws
|
|
14
|
+
* if both are passed -- an explicit contradiction should never resolve
|
|
15
|
+
* silently to one of the two.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveMode(detected: ModeDetection, flags: ModeFlags): ModeDetection;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=mode.d.ts.map
|
package/dist/mode.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether the CLI is writing a fresh project into an empty directory
|
|
3
|
+
* or adopting an already-established one. Adopt mode never overwrites
|
|
4
|
+
* project files -- see conflicts.ts and inventory.ts for what it does
|
|
5
|
+
* instead. Detection is a fact-check, never a guess it hides: the signal
|
|
6
|
+
* that chose the mode is always carried alongside it so an auto-detection
|
|
7
|
+
* is never a silent surprise.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { extname } from "node:path";
|
|
11
|
+
const LOOSE_SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js"]);
|
|
12
|
+
/** Inspects `dir` and reports which mode it implies, and why. */
|
|
13
|
+
export function detectMode(dir) {
|
|
14
|
+
if (!existsSync(dir)) {
|
|
15
|
+
return { mode: "fresh", signal: `${dir} does not exist yet` };
|
|
16
|
+
}
|
|
17
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
18
|
+
if (entries.length === 0) {
|
|
19
|
+
return { mode: "fresh", signal: `${dir} is empty` };
|
|
20
|
+
}
|
|
21
|
+
if (entries.some((entry) => entry.isFile() && entry.name === "package.json")) {
|
|
22
|
+
return { mode: "adopt", signal: "found package.json" };
|
|
23
|
+
}
|
|
24
|
+
if (entries.some((entry) => entry.isDirectory() && entry.name === ".git")) {
|
|
25
|
+
return { mode: "adopt", signal: "found a .git directory" };
|
|
26
|
+
}
|
|
27
|
+
const looseSource = entries.find((entry) => entry.isFile() && LOOSE_SOURCE_EXTENSIONS.has(extname(entry.name)));
|
|
28
|
+
if (looseSource !== undefined) {
|
|
29
|
+
return { mode: "adopt", signal: `found ${looseSource.name}` };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
mode: "fresh",
|
|
33
|
+
signal: `${dir} exists but has no recognizable project markers`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Applies `--adopt`/`--fresh` overrides onto an auto-detected mode. Throws
|
|
38
|
+
* if both are passed -- an explicit contradiction should never resolve
|
|
39
|
+
* silently to one of the two.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveMode(detected, flags) {
|
|
42
|
+
if (flags.adopt && flags.fresh) {
|
|
43
|
+
throw new Error("--adopt and --fresh are mutually exclusive");
|
|
44
|
+
}
|
|
45
|
+
if (flags.adopt) {
|
|
46
|
+
return { mode: "adopt", signal: "--adopt forced" };
|
|
47
|
+
}
|
|
48
|
+
if (flags.fresh) {
|
|
49
|
+
return { mode: "fresh", signal: "--fresh forced" };
|
|
50
|
+
}
|
|
51
|
+
return detected;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=mode.js.map
|
package/dist/packs.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CapCounts } from "./caps.js";
|
|
2
|
+
import type { SettingsHooksFragment, SettingsTopLevelFragment, VerifyStepAddition } from "./merge-json.js";
|
|
3
|
+
import type { TokenTable } from "./tokens.js";
|
|
4
|
+
export interface PackWiring {
|
|
5
|
+
settings: SettingsHooksFragment;
|
|
6
|
+
/** Top-level `.claude/settings.json` keys that aren't hook registrations (`statusLine`). Optional: packs that only register hooks omit it. */
|
|
7
|
+
settingsTopLevel?: SettingsTopLevelFragment;
|
|
8
|
+
packageScripts: Record<string, string>;
|
|
9
|
+
verifySteps: VerifyStepAddition[];
|
|
10
|
+
}
|
|
11
|
+
export interface PackManifest {
|
|
12
|
+
schemaVersion: number;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
modes: string[];
|
|
16
|
+
budget: CapCounts;
|
|
17
|
+
requires: {
|
|
18
|
+
paths: string[];
|
|
19
|
+
} | undefined;
|
|
20
|
+
wiring: PackWiring;
|
|
21
|
+
adoptNotes: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface Pack {
|
|
24
|
+
manifest: PackManifest;
|
|
25
|
+
filesDir: string;
|
|
26
|
+
}
|
|
27
|
+
/** `templates/packs`, resolved the same way `templatesCoreDir()` resolves `templates/core`. */
|
|
28
|
+
export declare function packsRootDir(): string;
|
|
29
|
+
/** 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
|
+
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. */
|
|
32
|
+
export declare function loadPack(name: string, root?: string): Pack;
|
|
33
|
+
export interface PackInstallResult {
|
|
34
|
+
filesWritten: string[];
|
|
35
|
+
budget: CapCounts;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Installs one pack into a target directory that already has the baseline
|
|
39
|
+
* emitted (fresh mode only). Copies `files/` via the existing `emitTemplate`
|
|
40
|
+
* unchanged, checks `requires.paths` against the just-emitted tree, then
|
|
41
|
+
* applies the pack's three JSON wiring merges.
|
|
42
|
+
*/
|
|
43
|
+
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
|
+
/**
|
|
54
|
+
* Index-level, adopt-mode-only facts about how a pack's wiring would land
|
|
55
|
+
* against a real project's current `.claude/settings.json` and
|
|
56
|
+
* `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.
|
|
59
|
+
*/
|
|
60
|
+
export declare function observeWiring(targetDir: string, manifest: PackManifest): string[];
|
|
61
|
+
//# sourceMappingURL=packs.d.ts.map
|
package/dist/packs.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads pack manifests from `templates/packs/<name>/pack.json` and installs
|
|
3
|
+
* a pack's files + JSON wiring into a freshly-bootstrapped project. Adopt
|
|
4
|
+
* mode never calls `installPack` -- it surveys packs into the report
|
|
5
|
+
* (`observeWiring`, `stagePackFiles`) and defers installation to
|
|
6
|
+
* `/customize`, which reads a project's real gate runner before translating
|
|
7
|
+
* a pack's wiring (see inventory.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { resolveAsset } from "./assets.js";
|
|
12
|
+
import { emitTemplate } from "./emit.js";
|
|
13
|
+
import { parseJsonc } from "./jsonc.js";
|
|
14
|
+
import { isRecord, mergePackageScripts, mergeSettingsHooks, mergeSettingsTopLevel, mergeVerifySteps, } from "./merge-json.js";
|
|
15
|
+
/** `templates/packs`, resolved the same way `templatesCoreDir()` resolves `templates/core`. */
|
|
16
|
+
export function packsRootDir() {
|
|
17
|
+
return resolveAsset({ repo: "templates/packs", local: "templates/packs" });
|
|
18
|
+
}
|
|
19
|
+
/** 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. */
|
|
20
|
+
export function listPackNames(root = packsRootDir()) {
|
|
21
|
+
if (!existsSync(root)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return readdirSync(root, { withFileTypes: true })
|
|
25
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "pack.json")))
|
|
26
|
+
.map((entry) => entry.name)
|
|
27
|
+
.sort();
|
|
28
|
+
}
|
|
29
|
+
/** 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. */
|
|
30
|
+
export function loadPack(name, root = packsRootDir()) {
|
|
31
|
+
const packDir = join(root, name);
|
|
32
|
+
const manifestPath = join(packDir, "pack.json");
|
|
33
|
+
if (!existsSync(manifestPath)) {
|
|
34
|
+
const available = listPackNames(root);
|
|
35
|
+
throw new Error(`unknown pack "${name}" -- available: ${available.length > 0 ? available.join(", ") : "(none)"}`);
|
|
36
|
+
}
|
|
37
|
+
const parsed = parseJsonc(readFileSync(manifestPath, "utf8"));
|
|
38
|
+
if (!parsed.ok) {
|
|
39
|
+
throw new Error(`pack "${name}": pack.json failed to parse -- ${parsed.error}`);
|
|
40
|
+
}
|
|
41
|
+
const manifest = parsed.value;
|
|
42
|
+
if (manifest.schemaVersion !== 1) {
|
|
43
|
+
throw new Error(`pack "${name}": unsupported pack.json schemaVersion ${JSON.stringify(manifest.schemaVersion)}`);
|
|
44
|
+
}
|
|
45
|
+
return { manifest, filesDir: join(packDir, "files") };
|
|
46
|
+
}
|
|
47
|
+
function readJsonOrThrow(path) {
|
|
48
|
+
const parsed = parseJsonc(readFileSync(path, "utf8"));
|
|
49
|
+
if (!parsed.ok) {
|
|
50
|
+
throw new Error(`${path} failed to parse -- ${parsed.error}`);
|
|
51
|
+
}
|
|
52
|
+
return parsed.value;
|
|
53
|
+
}
|
|
54
|
+
function writeJson(path, value) {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Serializes `bin/lib/verify-steps.packs.json` to match exactly what
|
|
60
|
+
* Prettier would produce for this shape: every field one per line except
|
|
61
|
+
* `cmd`, whose short array of strings Prettier collapses onto a single
|
|
62
|
+
* line when it fits within printWidth. `JSON.stringify(value, null, 2)`
|
|
63
|
+
* never collapses an array, so writing this file through the generic
|
|
64
|
+
* `writeJson` would fail `prettier --check` on every install. This printer
|
|
65
|
+
* is scoped to exactly the one shape `VerifyStepAddition[]` has -- it is
|
|
66
|
+
* not a general JSON formatter.
|
|
67
|
+
*/
|
|
68
|
+
function writeVerifyStepsPacksJson(path, steps) {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
+
if (steps.length === 0) {
|
|
71
|
+
writeFileSync(path, "[]\n");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const lines = ["["];
|
|
75
|
+
steps.forEach((step, index) => {
|
|
76
|
+
const comma = index < steps.length - 1 ? "," : "";
|
|
77
|
+
lines.push(" {", ` "id": ${JSON.stringify(step.id)},`, ` "group": ${JSON.stringify(step.group)},`, ` "name": ${JSON.stringify(step.name)},`, ` "cmd": [${step.cmd.map((arg) => JSON.stringify(arg)).join(", ")}]`, ` }${comma}`);
|
|
78
|
+
});
|
|
79
|
+
lines.push("]");
|
|
80
|
+
writeFileSync(path, lines.join("\n") + "\n");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Installs one pack into a target directory that already has the baseline
|
|
84
|
+
* emitted (fresh mode only). Copies `files/` via the existing `emitTemplate`
|
|
85
|
+
* unchanged, checks `requires.paths` against the just-emitted tree, then
|
|
86
|
+
* applies the pack's three JSON wiring merges.
|
|
87
|
+
*/
|
|
88
|
+
export function installPack(pack, targetDir, tokens) {
|
|
89
|
+
const { manifest, filesDir } = pack;
|
|
90
|
+
for (const relPath of manifest.requires?.paths ?? []) {
|
|
91
|
+
if (!existsSync(join(targetDir, relPath))) {
|
|
92
|
+
throw new Error(`pack "${manifest.name}" requires "${relPath}", which is missing from the target -- install the baseline first`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const { filesWritten } = emitTemplate(filesDir, targetDir, tokens);
|
|
96
|
+
const settingsPath = join(targetDir, ".claude", "settings.json");
|
|
97
|
+
const existingSettings = existsSync(settingsPath)
|
|
98
|
+
? readJsonOrThrow(settingsPath)
|
|
99
|
+
: {};
|
|
100
|
+
let settings = existingSettings;
|
|
101
|
+
// Only touch the hooks block when the pack registers hooks: merging an empty
|
|
102
|
+
// fragment would still write an empty `hooks: {}` into a settings file that
|
|
103
|
+
// had none.
|
|
104
|
+
if (Object.keys(manifest.wiring.settings).length > 0) {
|
|
105
|
+
settings = mergeSettingsHooks(settings, manifest.wiring.settings);
|
|
106
|
+
}
|
|
107
|
+
settings = mergeSettingsTopLevel(settings, manifest.wiring.settingsTopLevel ?? {});
|
|
108
|
+
writeJson(settingsPath, settings);
|
|
109
|
+
if (Object.keys(manifest.wiring.packageScripts).length > 0) {
|
|
110
|
+
const pkgPath = join(targetDir, "package.json");
|
|
111
|
+
const pkg = readJsonOrThrow(pkgPath);
|
|
112
|
+
const { scripts, collisions } = mergePackageScripts(pkg["scripts"], manifest.wiring.packageScripts);
|
|
113
|
+
if (collisions.length > 0) {
|
|
114
|
+
throw new Error(`pack "${manifest.name}": package.json script collision(s): ${collisions.map((c) => c.name).join(", ")}`);
|
|
115
|
+
}
|
|
116
|
+
pkg["scripts"] = scripts;
|
|
117
|
+
writeJson(pkgPath, pkg);
|
|
118
|
+
}
|
|
119
|
+
if (manifest.wiring.verifySteps.length > 0) {
|
|
120
|
+
const stepsPath = join(targetDir, "bin", "lib", "verify-steps.packs.json");
|
|
121
|
+
const existingSteps = existsSync(stepsPath)
|
|
122
|
+
? readJsonOrThrow(stepsPath)
|
|
123
|
+
: [];
|
|
124
|
+
writeVerifyStepsPacksJson(stepsPath, mergeVerifySteps(existingSteps, manifest.wiring.verifySteps));
|
|
125
|
+
}
|
|
126
|
+
return { filesWritten, budget: manifest.budget };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Copies a pack's `pack.json` + `files/` tree, unmodified, into
|
|
130
|
+
* `<groundworkDir>/packs/<name>/` -- adopt mode's staging area. `/customize`
|
|
131
|
+
* installs from this self-contained copy rather than from `templateRoot`
|
|
132
|
+
* (an absolute path that may not exist by the time it runs). Token
|
|
133
|
+
* substitution is a no-op here (`{}`): staging a project's real name into
|
|
134
|
+
* pack content is `/customize`'s job, not this offline copy's.
|
|
135
|
+
*/
|
|
136
|
+
export function stagePackFiles(pack, groundworkDir) {
|
|
137
|
+
const destDir = join(groundworkDir, "packs", pack.manifest.name);
|
|
138
|
+
const { filesWritten } = emitTemplate(pack.filesDir, join(destDir, "files"), {});
|
|
139
|
+
writeJson(join(destDir, "pack.json"), pack.manifest);
|
|
140
|
+
return [...filesWritten.map((f) => join("files", f)), "pack.json"];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Index-level, adopt-mode-only facts about how a pack's wiring would land
|
|
144
|
+
* against a real project's current `.claude/settings.json` and
|
|
145
|
+
* `bin/lib/verify-steps.packs.json` -- never a verdict on whether it will
|
|
146
|
+
* work, which is `/customize`'s Step 0 judgment call to make after reading
|
|
147
|
+
* the project's real gate runner and hook config.
|
|
148
|
+
*/
|
|
149
|
+
export function observeWiring(targetDir, manifest) {
|
|
150
|
+
const observations = [];
|
|
151
|
+
const settingsPath = join(targetDir, ".claude", "settings.json");
|
|
152
|
+
if (!existsSync(settingsPath)) {
|
|
153
|
+
observations.push("no .claude/settings.json found");
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const parsed = parseJsonc(readFileSync(settingsPath, "utf8"));
|
|
157
|
+
if (!parsed.ok || !isRecord(parsed.value)) {
|
|
158
|
+
observations.push(".claude/settings.json exists but could not be parsed");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const hooks = parsed.value["hooks"];
|
|
162
|
+
for (const event of Object.keys(manifest.wiring.settings)) {
|
|
163
|
+
const existingEntries = isRecord(hooks) ? hooks[event] : undefined;
|
|
164
|
+
observations.push(Array.isArray(existingEntries) && existingEntries.length > 0
|
|
165
|
+
? `.claude/settings.json already has a "${event}" entry (${existingEntries.length} registration(s))`
|
|
166
|
+
: `.claude/settings.json has no "${event}" entry yet`);
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(manifest.wiring.settingsTopLevel ?? {})) {
|
|
169
|
+
observations.push(key in parsed.value
|
|
170
|
+
? `.claude/settings.json already sets a top-level "${key}" -- installing this pack would collide with it, so replacing it is a decision for the user`
|
|
171
|
+
: `.claude/settings.json has no top-level "${key}" yet`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (existsSync(join(targetDir, ".claude", "settings.local.json"))) {
|
|
176
|
+
observations.push(".claude/settings.local.json is present and may shadow a merged hook entry");
|
|
177
|
+
}
|
|
178
|
+
if (manifest.wiring.verifySteps.length > 0) {
|
|
179
|
+
const stepsPath = join(targetDir, "bin", "lib", "verify-steps.packs.json");
|
|
180
|
+
observations.push(existsSync(stepsPath)
|
|
181
|
+
? "bin/lib/verify-steps.packs.json exists"
|
|
182
|
+
: "no bin/lib/verify-steps.packs.json found -- no bin/verify.mjs-shaped gate runner detected");
|
|
183
|
+
}
|
|
184
|
+
return observations;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=packs.js.map
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface InstallPluginResult {
|
|
2
|
+
filesWritten: string[];
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Installs the skill into `<targetDir>/.claude/skills/customize/`. Used by
|
|
6
|
+
* fresh-bootstrap mode, where the directory is always new.
|
|
7
|
+
*/
|
|
8
|
+
export declare function installCustomizeSkill(targetDir: string, sourceDir?: string): InstallPluginResult;
|
|
9
|
+
type InstallLocation = "claude" | "groundwork" | "already-present";
|
|
10
|
+
export interface GuardedInstallResult {
|
|
11
|
+
filesWritten: string[];
|
|
12
|
+
location: InstallLocation;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Adopt-mode install: purely additive, never overwrites. If the project
|
|
16
|
+
* already has its own `.claude/skills/customize/SKILL.md` and its content
|
|
17
|
+
* differs from what this CLI ships, the skill is written to
|
|
18
|
+
* `.groundwork/customize/` instead -- reported in the adoption report
|
|
19
|
+
* rather than silently overwriting whatever the project already had there.
|
|
20
|
+
*/
|
|
21
|
+
export declare function installCustomizeSkillGuarded(targetDir: string, sourceDir?: string): GuardedInstallResult;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs the `/customize` skill into a bootstrapped or adopted project.
|
|
3
|
+
* Claude Code's marketplace-based plugin installation is an interactive,
|
|
4
|
+
* network-involving flow this offline CLI can't drive; instead this copies
|
|
5
|
+
* the skill's SKILL.md plus its deterministic backing data directly into a
|
|
6
|
+
* destination directory, so `/customize` works immediately with no further
|
|
7
|
+
* setup step.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { resolveAsset } from "./assets.js";
|
|
12
|
+
/** Resolves the plugin payload for a source checkout (`packages/plugin`) or a published tarball (`plugin/`). */
|
|
13
|
+
function pluginDir() {
|
|
14
|
+
return resolveAsset({ repo: "packages/plugin", local: "plugin" });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Copies `skills/customize/SKILL.md` and its backing data
|
|
18
|
+
* (`src/kind-facet-map.ts`, `src/domain-map.ts`, `src/pack-map.ts`) from
|
|
19
|
+
* `sourceDir` into `destDir`. A missing source file is a broken install,
|
|
20
|
+
* not something to degrade past silently -- it throws.
|
|
21
|
+
*/
|
|
22
|
+
function copyCustomizeSkillFiles(destDir, sourceDir) {
|
|
23
|
+
const skillSourceDir = join(sourceDir, "skills", "customize");
|
|
24
|
+
const dataSourceDir = join(sourceDir, "src");
|
|
25
|
+
mkdirSync(destDir, { recursive: true });
|
|
26
|
+
const filesWritten = [];
|
|
27
|
+
const copyInto = (from, toName) => {
|
|
28
|
+
if (!existsSync(from)) {
|
|
29
|
+
throw new Error(`the /customize skill's source file is missing: ${from}`);
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(join(destDir, toName), readFileSync(from, "utf8"));
|
|
32
|
+
filesWritten.push(toName);
|
|
33
|
+
};
|
|
34
|
+
copyInto(join(skillSourceDir, "SKILL.md"), "SKILL.md");
|
|
35
|
+
copyInto(join(dataSourceDir, "kind-facet-map.ts"), "kind-facet-map.ts");
|
|
36
|
+
copyInto(join(dataSourceDir, "domain-map.ts"), "domain-map.ts");
|
|
37
|
+
copyInto(join(dataSourceDir, "pack-map.ts"), "pack-map.ts");
|
|
38
|
+
return { filesWritten };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Installs the skill into `<targetDir>/.claude/skills/customize/`. Used by
|
|
42
|
+
* fresh-bootstrap mode, where the directory is always new.
|
|
43
|
+
*/
|
|
44
|
+
export function installCustomizeSkill(targetDir, sourceDir = pluginDir()) {
|
|
45
|
+
const destDir = join(targetDir, ".claude", "skills", "customize");
|
|
46
|
+
const result = copyCustomizeSkillFiles(destDir, sourceDir);
|
|
47
|
+
return {
|
|
48
|
+
filesWritten: result.filesWritten.map((name) => join(".claude", "skills", "customize", name)),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Adopt-mode install: purely additive, never overwrites. If the project
|
|
53
|
+
* already has its own `.claude/skills/customize/SKILL.md` and its content
|
|
54
|
+
* differs from what this CLI ships, the skill is written to
|
|
55
|
+
* `.groundwork/customize/` instead -- reported in the adoption report
|
|
56
|
+
* rather than silently overwriting whatever the project already had there.
|
|
57
|
+
*/
|
|
58
|
+
export function installCustomizeSkillGuarded(targetDir, sourceDir = pluginDir()) {
|
|
59
|
+
const existingSkillMdPath = join(targetDir, ".claude", "skills", "customize", "SKILL.md");
|
|
60
|
+
const sourceSkillMdPath = join(sourceDir, "skills", "customize", "SKILL.md");
|
|
61
|
+
if (existsSync(existingSkillMdPath)) {
|
|
62
|
+
const existingContent = readFileSync(existingSkillMdPath, "utf8");
|
|
63
|
+
const sourceContent = existsSync(sourceSkillMdPath)
|
|
64
|
+
? readFileSync(sourceSkillMdPath, "utf8")
|
|
65
|
+
: undefined;
|
|
66
|
+
if (sourceContent !== undefined && existingContent === sourceContent) {
|
|
67
|
+
return { filesWritten: [], location: "already-present" };
|
|
68
|
+
}
|
|
69
|
+
const destDir = join(targetDir, ".groundwork", "customize");
|
|
70
|
+
const result = copyCustomizeSkillFiles(destDir, sourceDir);
|
|
71
|
+
return {
|
|
72
|
+
filesWritten: result.filesWritten.map((name) => join(".groundwork", "customize", name)),
|
|
73
|
+
location: "groundwork",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const result = installCustomizeSkill(targetDir, sourceDir);
|
|
77
|
+
return { filesWritten: result.filesWritten, location: "claude" };
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=plugin.js.map
|
package/dist/report.d.ts
ADDED