@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,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
|
package/dist/jsonc.d.ts
ADDED
|
@@ -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
|