@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.
Files changed (151) hide show
  1. package/README.md +23 -0
  2. package/bin/m3l-groundwork.mjs +10 -0
  3. package/dist/assets.d.ts +20 -0
  4. package/dist/assets.js +79 -0
  5. package/dist/caps.d.ts +25 -0
  6. package/dist/caps.js +69 -0
  7. package/dist/conflicts.d.ts +12 -0
  8. package/dist/conflicts.js +77 -0
  9. package/dist/emit.d.ts +7 -0
  10. package/dist/emit.js +42 -0
  11. package/dist/git.d.ts +3 -0
  12. package/dist/git.js +9 -0
  13. package/dist/harness/conformance.d.ts +20 -0
  14. package/dist/harness/conformance.js +18 -0
  15. package/dist/harness/frontmatter.d.ts +38 -0
  16. package/dist/harness/frontmatter.js +204 -0
  17. package/dist/harness/grade.d.ts +4 -0
  18. package/dist/harness/grade.js +105 -0
  19. package/dist/harness/rules.d.ts +55 -0
  20. package/dist/harness/rules.js +580 -0
  21. package/dist/harness/types.d.ts +32 -0
  22. package/dist/harness/types.js +9 -0
  23. package/dist/inventory.d.ts +63 -0
  24. package/dist/inventory.js +66 -0
  25. package/dist/jsonc.d.ts +14 -0
  26. package/dist/jsonc.js +83 -0
  27. package/dist/main.d.ts +24 -0
  28. package/dist/main.js +297 -0
  29. package/dist/merge-json.d.ts +74 -0
  30. package/dist/merge-json.js +135 -0
  31. package/dist/mode.d.ts +19 -0
  32. package/dist/mode.js +53 -0
  33. package/dist/packs.d.ts +61 -0
  34. package/dist/packs.js +186 -0
  35. package/dist/plugin.d.ts +23 -0
  36. package/dist/plugin.js +79 -0
  37. package/dist/report.d.ts +4 -0
  38. package/dist/report.js +323 -0
  39. package/dist/survey/fs-walk.d.ts +14 -0
  40. package/dist/survey/fs-walk.js +60 -0
  41. package/dist/survey/survey-docs.d.ts +4 -0
  42. package/dist/survey/survey-docs.js +69 -0
  43. package/dist/survey/survey-harness.d.ts +4 -0
  44. package/dist/survey/survey-harness.js +121 -0
  45. package/dist/survey/survey-shape.d.ts +4 -0
  46. package/dist/survey/survey-shape.js +182 -0
  47. package/dist/survey/survey-toolchain.d.ts +4 -0
  48. package/dist/survey/survey-toolchain.js +217 -0
  49. package/dist/survey/survey.d.ts +5 -0
  50. package/dist/survey/survey.js +21 -0
  51. package/dist/survey/types.d.ts +117 -0
  52. package/dist/survey/types.js +8 -0
  53. package/dist/tokens.d.ts +13 -0
  54. package/dist/tokens.js +13 -0
  55. package/dist/toolchain/conformance.d.ts +20 -0
  56. package/dist/toolchain/conformance.js +30 -0
  57. package/dist/toolchain/grade.d.ts +4 -0
  58. package/dist/toolchain/grade.js +244 -0
  59. package/dist/toolchain/rules.d.ts +118 -0
  60. package/dist/toolchain/rules.js +706 -0
  61. package/dist/toolchain/tsconfig-chain.d.ts +36 -0
  62. package/dist/toolchain/tsconfig-chain.js +116 -0
  63. package/dist/toolchain/types.d.ts +27 -0
  64. package/dist/toolchain/types.js +9 -0
  65. package/package.json +59 -0
  66. package/plugin/skills/customize/SKILL.md +305 -0
  67. package/plugin/src/domain-map.ts +134 -0
  68. package/plugin/src/index.ts +4 -0
  69. package/plugin/src/kind-facet-map.ts +174 -0
  70. package/plugin/src/pack-map.ts +65 -0
  71. package/templates/core/.claude/agents/Explore.md +43 -0
  72. package/templates/core/.claude/agents/code-implementer.md +258 -0
  73. package/templates/core/.claude/agents/code-reviewer.md +163 -0
  74. package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
  75. package/templates/core/.claude/agents/test-author.md +211 -0
  76. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
  77. package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
  78. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
  79. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
  80. package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
  81. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
  82. package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
  83. package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
  84. package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
  85. package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
  86. package/templates/core/.claude/rules/agent-dispatch.md +121 -0
  87. package/templates/core/.claude/rules/refactoring.md +52 -0
  88. package/templates/core/.claude/rules/src.md +114 -0
  89. package/templates/core/.claude/rules/tests.md +129 -0
  90. package/templates/core/.claude/settings.json +111 -0
  91. package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
  92. package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
  93. package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
  94. package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
  95. package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
  96. package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
  97. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
  98. package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
  99. package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
  100. package/templates/core/.github/workflows/ci.yml +123 -0
  101. package/templates/core/.github/workflows/dependency-review.yml +26 -0
  102. package/templates/core/.github/workflows/security-audit.yml +54 -0
  103. package/templates/core/.node-version +1 -0
  104. package/templates/core/.prettierignore +5 -0
  105. package/templates/core/.prettierrc.json +4 -0
  106. package/templates/core/CLAUDE.md +127 -0
  107. package/templates/core/README.md +24 -0
  108. package/templates/core/_gitignore +19 -0
  109. package/templates/core/_npmrc +1 -0
  110. package/templates/core/bin/check-exports.mjs +92 -0
  111. package/templates/core/bin/check-harness.mjs +27 -0
  112. package/templates/core/bin/check-node-version.mjs +51 -0
  113. package/templates/core/bin/check-toolchain.mjs +20 -0
  114. package/templates/core/bin/lib/agent-roster.mjs +8 -0
  115. package/templates/core/bin/lib/frontmatter.mjs +210 -0
  116. package/templates/core/bin/lib/harness-rules.mjs +916 -0
  117. package/templates/core/bin/lib/protected-paths.mjs +23 -0
  118. package/templates/core/bin/lib/report.mjs +56 -0
  119. package/templates/core/bin/lib/signed-range.mjs +178 -0
  120. package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
  121. package/templates/core/bin/lib/verify-steps.mjs +131 -0
  122. package/templates/core/bin/lib/verify-steps.packs.json +1 -0
  123. package/templates/core/bin/lint-commit.mjs +50 -0
  124. package/templates/core/bin/strip-claude-trailers.mjs +25 -0
  125. package/templates/core/bin/verify.mjs +64 -0
  126. package/templates/core/commitlint.config.js +11 -0
  127. package/templates/core/docs/research/harness-refresh.md +27 -0
  128. package/templates/core/docs/research/typescript-refresh.md +32 -0
  129. package/templates/core/eslint.config.js +105 -0
  130. package/templates/core/knip.json +6 -0
  131. package/templates/core/lefthook.yml +39 -0
  132. package/templates/core/package.json +58 -0
  133. package/templates/core/pnpm-workspace.yaml +13 -0
  134. package/templates/core/src/index.ts +12 -0
  135. package/templates/core/tests/index.test.ts +8 -0
  136. package/templates/core/tsconfig.base.json +36 -0
  137. package/templates/core/tsconfig.build.json +10 -0
  138. package/templates/core/tsconfig.json +11 -0
  139. package/templates/core/vitest.config.ts +32 -0
  140. package/templates/packs/README.md +81 -0
  141. package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
  142. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
  143. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
  144. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
  145. package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
  146. package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
  147. package/templates/packs/harness-extras/pack.json +65 -0
  148. package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
  149. package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
  150. package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
  151. package/templates/packs/statusline/pack.json +31 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Serializes a survey + conflict plan + pack survey into
3
+ * `.groundwork/inventory.json` -- the machine-readable handoff
4
+ * `/customize`'s Step 0 reads instead of re-deriving the survey itself.
5
+ * Schema-versioned so a future CLI release can tell an old inventory apart
6
+ * from a current one; `/customize` must tolerate an older inventory
7
+ * rather than crash on one -- `schemaVersion: 1` predates packs (no `packs`
8
+ * field), `schemaVersion` below 3 predates the harness grade (no
9
+ * `harnessGrade`/`harnessConformance`), and `schemaVersion` below 4 predates
10
+ * the toolchain grade (no `toolchainGrade`/`toolchainConformance`).
11
+ */
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { summarizeHarnessConformance } from "./harness/conformance.js";
16
+ import { summarizeToolchainConformance } from "./toolchain/conformance.js";
17
+ export const INVENTORY_SCHEMA_VERSION = 4;
18
+ /** Resolves this CLI package's own `package.json`, relative to this module's runtime location. */
19
+ function defaultPackageJsonPath() {
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ return join(here, "..", "package.json");
22
+ }
23
+ /**
24
+ * Resolves this CLI package's own declared version. `packageJsonPath`
25
+ * defaults to this module's own `package.json` -- overridable so callers
26
+ * (and tests) can exercise the missing-file/unparseable/non-string-version
27
+ * fallbacks without needing a broken real package.json on disk.
28
+ */
29
+ export function resolveCliVersion(packageJsonPath = defaultPackageJsonPath()) {
30
+ if (!existsSync(packageJsonPath)) {
31
+ return "unknown";
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
35
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
36
+ }
37
+ catch {
38
+ return "unknown";
39
+ }
40
+ }
41
+ /** Builds the inventory object. Does not write anything -- see `writeInventory`. */
42
+ export function buildInventory(params) {
43
+ return {
44
+ schemaVersion: INVENTORY_SCHEMA_VERSION,
45
+ cliVersion: resolveCliVersion(),
46
+ generatedAt: new Date().toISOString(),
47
+ modeSignal: params.detection.signal,
48
+ templateRoot: params.templateRoot,
49
+ targetDir: params.targetDir,
50
+ survey: params.survey,
51
+ conflicts: params.conflicts,
52
+ packs: params.packs,
53
+ harnessGrade: params.harnessGrade,
54
+ harnessConformance: summarizeHarnessConformance(params.conflicts),
55
+ toolchainGrade: params.toolchainGrade,
56
+ toolchainConformance: summarizeToolchainConformance(params.conflicts),
57
+ };
58
+ }
59
+ /** Writes `inventory.json` into `groundworkDir`, creating it if needed. */
60
+ export function writeInventory(inventory, groundworkDir) {
61
+ mkdirSync(groundworkDir, { recursive: true });
62
+ const path = join(groundworkDir, "inventory.json");
63
+ writeFileSync(path, `${JSON.stringify(inventory, null, 2)}\n`);
64
+ return path;
65
+ }
66
+ //# sourceMappingURL=inventory.js.map
@@ -0,0 +1,14 @@
1
+ export type JsoncReadResult = {
2
+ ok: true;
3
+ value: unknown;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ };
8
+ /** Strips `//` and block comments and trailing commas from JSONC source. */
9
+ export declare function stripJsoncNoise(content: string): string;
10
+ /** Parses JSONC source text, returning a structured result rather than throwing. */
11
+ export declare function parseJsonc(content: string): JsoncReadResult;
12
+ /** Reads and parses a JSONC file. A missing file is reported, not thrown. */
13
+ export declare function readJsoncFile(path: string): JsoncReadResult;
14
+ //# sourceMappingURL=jsonc.d.ts.map
package/dist/jsonc.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A tolerant reader for JSON-with-comments (JSONC) -- `tsconfig.json` and
3
+ * friends use `//`/`/* *\/` comments and trailing commas that `JSON.parse`
4
+ * rejects outright. This strips comments and trailing commas (respecting
5
+ * string literals, so a `//` inside a string is left alone) and delegates
6
+ * to `JSON.parse`. It is not a full JSON5 parser -- no unquoted keys, no
7
+ * single-quoted strings -- tsconfig-shaped input is the only intended use.
8
+ */
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ /** Strips `//` and block comments and trailing commas from JSONC source. */
11
+ export function stripJsoncNoise(content) {
12
+ let result = "";
13
+ let inString = false;
14
+ let inLineComment = false;
15
+ let inBlockComment = false;
16
+ for (let i = 0; i < content.length; i++) {
17
+ const ch = content[i];
18
+ const next = content[i + 1];
19
+ if (inLineComment) {
20
+ if (ch === "\n") {
21
+ inLineComment = false;
22
+ result += ch;
23
+ }
24
+ continue;
25
+ }
26
+ if (inBlockComment) {
27
+ if (ch === "*" && next === "/") {
28
+ inBlockComment = false;
29
+ i++;
30
+ }
31
+ continue;
32
+ }
33
+ if (inString) {
34
+ result += ch;
35
+ if (ch === "\\") {
36
+ result += next ?? "";
37
+ i++;
38
+ continue;
39
+ }
40
+ if (ch === '"') {
41
+ inString = false;
42
+ }
43
+ continue;
44
+ }
45
+ if (ch === '"') {
46
+ inString = true;
47
+ result += ch;
48
+ continue;
49
+ }
50
+ if (ch === "/" && next === "/") {
51
+ inLineComment = true;
52
+ i++;
53
+ continue;
54
+ }
55
+ if (ch === "/" && next === "*") {
56
+ inBlockComment = true;
57
+ i++;
58
+ continue;
59
+ }
60
+ result += ch;
61
+ }
62
+ return result.replace(/,(\s*[}\]])/g, "$1");
63
+ }
64
+ /** Parses JSONC source text, returning a structured result rather than throwing. */
65
+ export function parseJsonc(content) {
66
+ try {
67
+ return { ok: true, value: JSON.parse(stripJsoncNoise(content)) };
68
+ }
69
+ catch (error) {
70
+ return {
71
+ ok: false,
72
+ error: error instanceof Error ? error.message : String(error),
73
+ };
74
+ }
75
+ }
76
+ /** Reads and parses a JSONC file. A missing file is reported, not thrown. */
77
+ export function readJsoncFile(path) {
78
+ if (!existsSync(path)) {
79
+ return { ok: false, error: `${path} does not exist` };
80
+ }
81
+ return parseJsonc(readFileSync(path, "utf8"));
82
+ }
83
+ //# sourceMappingURL=jsonc.js.map
package/dist/main.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { CapCounts } from "./caps.js";
2
+ export interface CliOptions {
3
+ targetDir: string;
4
+ projectName: string;
5
+ skipInstall: boolean;
6
+ force: boolean;
7
+ adopt: boolean;
8
+ fresh: boolean;
9
+ help: boolean;
10
+ version: boolean;
11
+ listPacks: boolean;
12
+ packs: string[];
13
+ }
14
+ /** Parses argv into structured options. Throws with USAGE on a missing target dir, unless --help/--version/--list-packs is present. */
15
+ export declare function parseArgs(argv: string[]): CliOptions;
16
+ /** Resolves `templates/core` for a source checkout or a published tarball -- see `assets.ts`. */
17
+ export declare function templatesCoreDir(): string;
18
+ /** The post-`--pack`-install caps summary line(s) printed to fresh-mode's console output. */
19
+ export declare function formatCapsSummary(baseline: CapCounts, installed: {
20
+ name: string;
21
+ budget: CapCounts;
22
+ }[]): string;
23
+ export declare function main(argv: string[]): void;
24
+ //# sourceMappingURL=main.d.ts.map
package/dist/main.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Entry point: `m3l-groundwork <target-dir> [options]`. No prompts, no
3
+ * interactivity, no network call beyond the package install -- this must
4
+ * work on a plane against a warm pnpm store.
5
+ *
6
+ * Two modes, auto-detected from the target directory (`--adopt`/`--fresh`
7
+ * force either): **fresh** writes the baseline into an empty directory, as
8
+ * before, and installs any `--pack` requested. **adopt** surveys an
9
+ * already-established project and writes only a report -- see `mode.ts`,
10
+ * `survey/survey.ts`, `conflicts.ts`, and `report.ts`. Adopt mode never
11
+ * touches a project file, including a pack's: it surveys every pack under
12
+ * `templates/packs/` into the report and defers installation to
13
+ * `/customize`; see `runAdopt`.
14
+ */
15
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
16
+ import { join, relative, resolve, basename } from "node:path";
17
+ import { resolveAsset } from "./assets.js";
18
+ import { CAP_LIMITS, countBaselineCaps } from "./caps.js";
19
+ import { emitTemplate } from "./emit.js";
20
+ import { installCustomizeSkill, installCustomizeSkillGuarded, } from "./plugin.js";
21
+ import { gitInit, runInstall } from "./git.js";
22
+ import { gradeHarness } from "./harness/grade.js";
23
+ import { detectMode, resolveMode } from "./mode.js";
24
+ import { surveyProject } from "./survey/survey.js";
25
+ import { planConflicts } from "./conflicts.js";
26
+ import { buildInventory, resolveCliVersion, writeInventory, } from "./inventory.js";
27
+ import { listPackNames, loadPack, installPack, observeWiring, stagePackFiles, } from "./packs.js";
28
+ import { renderReport } from "./report.js";
29
+ import { gradeToolchain } from "./toolchain/grade.js";
30
+ const USAGE = [
31
+ "usage: m3l-groundwork <target-dir> [options]",
32
+ "",
33
+ " --name <project-name> Override the project name (default: the target directory's basename)",
34
+ " --skip-install Skip the final `pnpm install` (fresh mode only)",
35
+ " --force Overwrite a non-empty target directory (fresh mode only)",
36
+ " --adopt Force adopt mode, even if the target looks empty",
37
+ " --fresh Force fresh-bootstrap mode, even if the target looks pre-existing",
38
+ " --pack <name> Install an opt-in pack from templates/packs/ (fresh mode only; repeatable)",
39
+ " --list-packs Print every available pack and exit",
40
+ " --help Print this message",
41
+ " --version Print the CLI's version",
42
+ "",
43
+ "With no --adopt/--fresh override, the target directory is inspected: an",
44
+ "empty or missing directory bootstraps fresh; a directory that already",
45
+ "looks like a project (package.json, .git, or loose source files) is",
46
+ "surveyed and adopted instead -- see .groundwork/adoption-report.md.",
47
+ "",
48
+ "A pack requested in adopt mode is not installed -- adopt mode never",
49
+ "writes project files. Every available pack is surveyed into the report",
50
+ "regardless; run /customize to install one.",
51
+ ].join("\n");
52
+ // --name takes a single value; --pack is repeatable. Every other
53
+ // recognized flag is a bare boolean.
54
+ const VALUE_FLAGS = new Set(["--name"]);
55
+ const REPEATABLE_VALUE_FLAGS = new Set(["--pack"]);
56
+ const HELP_FLAGS = new Set(["--help", "-h"]);
57
+ const VERSION_FLAGS = new Set(["--version", "-v"]);
58
+ const LIST_PACKS_FLAGS = new Set(["--list-packs"]);
59
+ /** Splits argv into positionals, boolean flags, and value-flag pairs -- a value-flag's value is never mistaken for a positional. */
60
+ function tokenizeArgv(argv) {
61
+ const positional = [];
62
+ const flags = new Set();
63
+ const values = new Map();
64
+ const repeatableValues = new Map();
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const arg = argv[i];
67
+ if (arg === undefined)
68
+ continue;
69
+ if (!arg.startsWith("-")) {
70
+ positional.push(arg);
71
+ continue;
72
+ }
73
+ if (REPEATABLE_VALUE_FLAGS.has(arg)) {
74
+ const value = argv[i + 1];
75
+ if (value !== undefined) {
76
+ const existing = repeatableValues.get(arg) ?? [];
77
+ existing.push(value);
78
+ repeatableValues.set(arg, existing);
79
+ i++;
80
+ }
81
+ continue;
82
+ }
83
+ if (VALUE_FLAGS.has(arg)) {
84
+ const value = argv[i + 1];
85
+ if (value !== undefined) {
86
+ values.set(arg, value);
87
+ i++;
88
+ }
89
+ continue;
90
+ }
91
+ flags.add(arg);
92
+ }
93
+ return { positional, flags, values, repeatableValues };
94
+ }
95
+ /** Parses argv into structured options. Throws with USAGE on a missing target dir, unless --help/--version/--list-packs is present. */
96
+ export function parseArgs(argv) {
97
+ const { positional, flags, values, repeatableValues } = tokenizeArgv(argv);
98
+ const help = [...HELP_FLAGS].some((flag) => flags.has(flag));
99
+ const version = [...VERSION_FLAGS].some((flag) => flags.has(flag));
100
+ const listPacks = [...LIST_PACKS_FLAGS].some((flag) => flags.has(flag));
101
+ if (help || version || listPacks) {
102
+ return {
103
+ targetDir: "",
104
+ projectName: "",
105
+ skipInstall: false,
106
+ force: false,
107
+ adopt: false,
108
+ fresh: false,
109
+ help,
110
+ version,
111
+ listPacks,
112
+ packs: [],
113
+ };
114
+ }
115
+ const targetArg = positional[0];
116
+ if (targetArg === undefined) {
117
+ throw new Error(USAGE);
118
+ }
119
+ const targetDir = resolve(targetArg);
120
+ const explicitName = values.get("--name");
121
+ const packs = [...new Set(repeatableValues.get("--pack") ?? [])].sort();
122
+ return {
123
+ targetDir,
124
+ projectName: explicitName ?? basename(targetDir),
125
+ skipInstall: flags.has("--skip-install"),
126
+ force: flags.has("--force"),
127
+ adopt: flags.has("--adopt"),
128
+ fresh: flags.has("--fresh"),
129
+ help: false,
130
+ version: false,
131
+ listPacks: false,
132
+ packs,
133
+ };
134
+ }
135
+ /** Resolves `templates/core` for a source checkout or a published tarball -- see `assets.ts`. */
136
+ export function templatesCoreDir() {
137
+ return resolveAsset({ repo: "templates/core", local: "templates/core" });
138
+ }
139
+ function isEmptyOrMissing(dir) {
140
+ return !existsSync(dir) || readdirSync(dir).length === 0;
141
+ }
142
+ function buildTokens(projectName) {
143
+ return { PROJECT_NAME: projectName, YEAR: String(new Date().getFullYear()) };
144
+ }
145
+ function formatCounts(counts) {
146
+ return `${counts.agents} agents, ${counts.skills} skills, ${counts.hooks} hooks, ${counts.workflows} workflows, ${counts.scripts} scripts`;
147
+ }
148
+ /** The post-`--pack`-install caps summary line(s) printed to fresh-mode's console output. */
149
+ export function formatCapsSummary(baseline, installed) {
150
+ const total = { ...baseline };
151
+ const lines = [
152
+ `templates/core: ${formatCounts(baseline)} (caps: ${formatCounts(CAP_LIMITS)})`,
153
+ ];
154
+ for (const { name, budget } of installed) {
155
+ lines.push(`+ ${name}: ${formatCounts(budget)}`);
156
+ total.agents += budget.agents;
157
+ total.skills += budget.skills;
158
+ total.hooks += budget.hooks;
159
+ total.workflows += budget.workflows;
160
+ total.scripts += budget.scripts;
161
+ }
162
+ const overCap = ["agents", "skills", "hooks", "workflows", "scripts"].filter((key) => total[key] > CAP_LIMITS[key]);
163
+ lines.push(`= ${formatCounts(total)}${overCap.length > 0 ? ` ⚠ over cap: ${overCap.join(", ")}` : ""}`);
164
+ return lines.join("\n");
165
+ }
166
+ function runFresh(options) {
167
+ if (!isEmptyOrMissing(options.targetDir) && !options.force) {
168
+ throw new Error(`${options.targetDir} already exists and is not empty (pass --force to overwrite, or --adopt to survey it instead)`);
169
+ }
170
+ mkdirSync(options.targetDir, { recursive: true });
171
+ const tokens = buildTokens(options.projectName);
172
+ const result = emitTemplate(templatesCoreDir(), options.targetDir, tokens);
173
+ console.log(`wrote ${result.filesWritten.length} files to ${options.targetDir}`);
174
+ const installedPacks = [];
175
+ for (const name of options.packs) {
176
+ const pack = loadPack(name);
177
+ if (!pack.manifest.modes.includes("fresh")) {
178
+ throw new Error(`pack "${name}" does not support fresh mode (modes: ${pack.manifest.modes.join(", ")})`);
179
+ }
180
+ const packResult = installPack(pack, options.targetDir, tokens);
181
+ console.log(`installed pack "${name}" (${packResult.filesWritten.length} files)`);
182
+ installedPacks.push({ name, budget: packResult.budget });
183
+ }
184
+ if (installedPacks.length > 0) {
185
+ console.log(formatCapsSummary(countBaselineCaps(templatesCoreDir()), installedPacks));
186
+ }
187
+ const pluginResult = installCustomizeSkill(options.targetDir);
188
+ console.log(`installed the /customize skill (${pluginResult.filesWritten.length} files)`);
189
+ gitInit(options.targetDir);
190
+ console.log("initialized git repository");
191
+ if (!options.skipInstall) {
192
+ runInstall(options.targetDir);
193
+ console.log("installed dependencies");
194
+ }
195
+ console.log(`\n✓ ${options.projectName} is ready at ${options.targetDir}`);
196
+ }
197
+ /**
198
+ * Surveys an already-established project and writes `.groundwork/` --
199
+ * `inventory.json` and `adoption-report.md`. Never touches a project file:
200
+ * the one addition is a purely-additive, collision-guarded copy of the
201
+ * `/customize` skill (see `installCustomizeSkillGuarded`), so the report
202
+ * can point straight at a working next step. Every pack under
203
+ * `templates/packs/` is surveyed (not just those named by `--pack`, which
204
+ * this mode ignores) and staged, unapplied, at `.groundwork/packs/<name>/`.
205
+ */
206
+ function runAdopt(options, detection) {
207
+ if (options.force) {
208
+ throw new Error("--force has no effect in adopt mode -- adopt mode never writes project files; run /customize to reconcile instead.");
209
+ }
210
+ console.log(`adopt mode: ${detection.signal}`);
211
+ const survey = surveyProject(options.targetDir);
212
+ const templateRoot = templatesCoreDir();
213
+ const tokens = buildTokens(options.projectName);
214
+ const conflicts = planConflicts(templateRoot, options.targetDir, tokens);
215
+ const groundworkDir = join(options.targetDir, ".groundwork");
216
+ const packs = listPackNames().map((name) => {
217
+ const pack = loadPack(name);
218
+ const fileConflicts = planConflicts(pack.filesDir, options.targetDir, tokens);
219
+ const wiringObservations = observeWiring(options.targetDir, pack.manifest);
220
+ stagePackFiles(pack, groundworkDir);
221
+ return {
222
+ name: pack.manifest.name,
223
+ modes: pack.manifest.modes,
224
+ budget: pack.manifest.budget,
225
+ fileConflicts,
226
+ wiring: pack.manifest.wiring,
227
+ wiringObservations,
228
+ adoptNotes: pack.manifest.adoptNotes,
229
+ };
230
+ });
231
+ const inventory = buildInventory({
232
+ detection,
233
+ templateRoot,
234
+ targetDir: options.targetDir,
235
+ survey,
236
+ conflicts,
237
+ packs,
238
+ harnessGrade: gradeHarness(options.targetDir),
239
+ toolchainGrade: gradeToolchain(options.targetDir),
240
+ });
241
+ const inventoryPath = writeInventory(inventory, groundworkDir);
242
+ const reportPath = join(groundworkDir, "adoption-report.md");
243
+ writeFileSync(reportPath, renderReport(inventory));
244
+ console.log(`wrote ${relative(options.targetDir, inventoryPath)}`);
245
+ console.log(`wrote ${relative(options.targetDir, reportPath)}`);
246
+ if (packs.length > 0) {
247
+ console.log(`staged ${packs.length} pack(s) at .groundwork/packs/ for /customize`);
248
+ }
249
+ const pluginResult = installCustomizeSkillGuarded(options.targetDir);
250
+ if (pluginResult.location === "already-present") {
251
+ console.log("the /customize skill was already up to date");
252
+ }
253
+ else {
254
+ const where = pluginResult.location === "claude"
255
+ ? ".claude/skills/customize/"
256
+ : ".groundwork/customize/";
257
+ console.log(`installed the /customize skill into ${where} (${pluginResult.filesWritten.length} files)`);
258
+ }
259
+ console.log(`\n✓ adoption report ready at ${reportPath}`);
260
+ console.log("Next: open this project in Claude Code and run /customize.");
261
+ }
262
+ export function main(argv) {
263
+ const options = parseArgs(argv);
264
+ if (options.help) {
265
+ console.log(USAGE);
266
+ return;
267
+ }
268
+ if (options.version) {
269
+ console.log(resolveCliVersion());
270
+ return;
271
+ }
272
+ if (options.listPacks) {
273
+ const names = listPackNames();
274
+ if (names.length === 0) {
275
+ console.log("no packs available");
276
+ }
277
+ else {
278
+ for (const name of names) {
279
+ const pack = loadPack(name);
280
+ console.log(`${name} (modes: ${pack.manifest.modes.join(", ")}) -- ${pack.manifest.description}`);
281
+ }
282
+ }
283
+ return;
284
+ }
285
+ const detected = detectMode(options.targetDir);
286
+ const resolved = resolveMode(detected, {
287
+ adopt: options.adopt,
288
+ fresh: options.fresh,
289
+ });
290
+ if (resolved.mode === "adopt") {
291
+ runAdopt(options, resolved);
292
+ }
293
+ else {
294
+ runFresh(options);
295
+ }
296
+ }
297
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,74 @@
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 declare function isRecord(value: unknown): value is Record<string, unknown>;
12
+ interface SettingsHookCommand {
13
+ type: string;
14
+ command: string;
15
+ if?: string;
16
+ timeout?: number;
17
+ }
18
+ interface SettingsHookEntry {
19
+ matcher?: string;
20
+ hooks: SettingsHookCommand[];
21
+ }
22
+ export type SettingsHooksFragment = Record<string, SettingsHookEntry[]>;
23
+ /**
24
+ * Merges a pack's `.claude/settings.json` hook fragment into an existing
25
+ * settings object. Per event, an entry is matched by `matcher` (both absent
26
+ * counts as a match -- e.g. `PreCompact`, which has none); within a matched
27
+ * entry, only `hooks` commands not already present (compared by exact
28
+ * `command` string) are appended. A command that matches on `command` but
29
+ * differs in its other fields (`if`, `timeout`) is a hard collision, never
30
+ * a silent overwrite.
31
+ */
32
+ export declare function mergeSettingsHooks(existing: unknown, fragment: SettingsHooksFragment): Record<string, unknown>;
33
+ export type SettingsTopLevelFragment = Record<string, unknown>;
34
+ /**
35
+ * Merges a pack's top-level `.claude/settings.json` keys (`statusLine`,
36
+ * `subagentStatusLine` -- settings that are not hook registrations) into an
37
+ * existing settings object. Disjoint from `mergeSettingsHooks`, which owns the
38
+ * `hooks` block: a fragment key of `hooks` is rejected outright rather than
39
+ * silently clobbering it. A key not yet present is appended; one already
40
+ * present with an identical value is a no-op; one present with a different
41
+ * value is a hard collision, never a silent overwrite -- an adopted project's
42
+ * own `statusLine` is the user's to replace deliberately.
43
+ */
44
+ export declare function mergeSettingsTopLevel(existing: unknown, fragment: SettingsTopLevelFragment): Record<string, unknown>;
45
+ interface ScriptCollision {
46
+ name: string;
47
+ existing: string;
48
+ incoming: string;
49
+ }
50
+ export interface MergePackageScriptsResult {
51
+ scripts: Record<string, string>;
52
+ collisions: ScriptCollision[];
53
+ }
54
+ /**
55
+ * Merges a pack's `package.json` script additions into the existing
56
+ * `scripts` block. Never overwrites a differing existing script -- the
57
+ * collision is returned for the caller to report, consistent with adopt
58
+ * mode's "report, then the user decides per conflict" policy.
59
+ */
60
+ export declare function mergePackageScripts(existing: Record<string, string> | undefined, additions: Record<string, string>): MergePackageScriptsResult;
61
+ export interface VerifyStepAddition {
62
+ id: string;
63
+ group: string;
64
+ name: string;
65
+ cmd: string[];
66
+ }
67
+ /**
68
+ * Merges a pack's verify-step additions into an existing step array
69
+ * (`bin/lib/verify-steps.packs.json`'s parsed contents). Append-by-`id`,
70
+ * idempotent; a same-`id`-different-content collision is a hard error.
71
+ */
72
+ export declare function mergeVerifySteps(existing: unknown, additions: VerifyStepAddition[]): VerifyStepAddition[];
73
+ export {};
74
+ //# sourceMappingURL=merge-json.d.ts.map