@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
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @monte3l/groundwork
|
|
2
|
+
|
|
3
|
+
The offline bootstrapper behind [m3l-groundwork](https://github.com/monte3l/m3l-groundwork):
|
|
4
|
+
one deterministic CLI that either writes a baseline TypeScript toolchain and
|
|
5
|
+
Claude Code harness into an empty directory (**fresh** mode) or surveys an
|
|
6
|
+
existing project and writes only a report (**adopt** mode, which never touches a
|
|
7
|
+
project file).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# 0.x is a prerelease and ships on the `next` dist-tag
|
|
11
|
+
npx @monte3l/groundwork@next my-new-project
|
|
12
|
+
npx @monte3l/groundwork@next my-new-project --pack statusline
|
|
13
|
+
npx @monte3l/groundwork@next ../existing-project
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node 24+. No prompts, and no network call beyond the `pnpm install` a
|
|
17
|
+
fresh bootstrap ends with (`--skip-install` to skip it). Run with `--help` for
|
|
18
|
+
every flag, or `--list-packs` for the optional packs.
|
|
19
|
+
|
|
20
|
+
The `/customize` skill it installs, the packs, and the design are documented in
|
|
21
|
+
the [repository README](https://github.com/monte3l/m3l-groundwork#readme).
|
|
22
|
+
|
|
23
|
+
MIT licensed.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { main } from "../dist/main.js";
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
main(process.argv.slice(2));
|
|
7
|
+
} catch (error) {
|
|
8
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Where an asset lives in each layout, relative to that layout's root. */
|
|
2
|
+
export interface AssetPaths {
|
|
3
|
+
/** Path from the repo root, in a source checkout. */
|
|
4
|
+
repo: string;
|
|
5
|
+
/** Path from the package root, in a published tarball. */
|
|
6
|
+
local: string;
|
|
7
|
+
}
|
|
8
|
+
/** The vendored spelling of a stripped dotfile: `.gitignore` becomes `_gitignore`. */
|
|
9
|
+
export declare function escapeDotfileName(name: string): string;
|
|
10
|
+
/** Inverse of `escapeDotfileName`: `_gitignore` becomes `.gitignore`; every other name is unchanged. */
|
|
11
|
+
export declare function restoreDotfileName(name: string): string;
|
|
12
|
+
/** `restoreDotfileName` applied to the final segment of a `/`- or `\`-separated relative path. */
|
|
13
|
+
export declare function restoreDotfilePath(relPath: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves an asset for whichever layout is running. `fromDir` is the
|
|
16
|
+
* directory of a module at `src/` or `dist/` depth; it defaults to this
|
|
17
|
+
* file's own, and is injectable so both layouts can be tested from one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveAsset(paths: AssetPaths, fromDir?: string): string;
|
|
20
|
+
//# sourceMappingURL=assets.d.ts.map
|
package/dist/assets.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that decides where the CLI's data trees live: `templates/`
|
|
3
|
+
* (the baseline and its packs) and the `/customize` plugin payload.
|
|
4
|
+
*
|
|
5
|
+
* Two layouts exist. In a **source checkout** they sit at the repo root, three
|
|
6
|
+
* directories above `src/` or `dist/`. In a **published tarball** they are
|
|
7
|
+
* vendored beside `dist/` (`scripts/vendor-assets.mjs` copies them in at
|
|
8
|
+
* `prepack`), because a tarball cannot reach outside its own package.
|
|
9
|
+
*
|
|
10
|
+
* The checkout is probed first, and only by a positive marker. A vendored copy
|
|
11
|
+
* can be left behind by a crashed `pnpm pack`; preferring it would make every
|
|
12
|
+
* later dev run read a stale tree. And an unmarked three-hop walk from an
|
|
13
|
+
* installed package lands in the consumer's `node_modules` (scoped) or, for an
|
|
14
|
+
* unscoped name, on the consumer's own project root -- where an unrelated
|
|
15
|
+
* `templates/` directory could be mistaken for ours.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
/**
|
|
21
|
+
* Files npm-family tooling strips from a tarball no matter what `files` says
|
|
22
|
+
* (`pnpm pack` keeps `.gitignore`, `npm pack` drops it; both drop `.npmrc`).
|
|
23
|
+
* The vendored copy stores each under `_<name-without-dot>` and the walkers
|
|
24
|
+
* (`emit.ts`, `conflicts.ts`) restore the real name on the way out.
|
|
25
|
+
*/
|
|
26
|
+
const ESCAPED_DOTFILES = [".gitignore", ".npmrc", ".npmignore"];
|
|
27
|
+
/** The vendored spelling of a stripped dotfile: `.gitignore` becomes `_gitignore`. */
|
|
28
|
+
export function escapeDotfileName(name) {
|
|
29
|
+
return ESCAPED_DOTFILES.includes(name)
|
|
30
|
+
? `_${name.slice(1)}`
|
|
31
|
+
: name;
|
|
32
|
+
}
|
|
33
|
+
/** Inverse of `escapeDotfileName`: `_gitignore` becomes `.gitignore`; every other name is unchanged. */
|
|
34
|
+
export function restoreDotfileName(name) {
|
|
35
|
+
const restored = `.${name.slice(1)}`;
|
|
36
|
+
return name.startsWith("_") &&
|
|
37
|
+
ESCAPED_DOTFILES.includes(restored)
|
|
38
|
+
? restored
|
|
39
|
+
: name;
|
|
40
|
+
}
|
|
41
|
+
/** `restoreDotfileName` applied to the final segment of a `/`- or `\`-separated relative path. */
|
|
42
|
+
export function restoreDotfilePath(relPath) {
|
|
43
|
+
const cut = Math.max(relPath.lastIndexOf("/"), relPath.lastIndexOf("\\"));
|
|
44
|
+
return relPath.slice(0, cut + 1) + restoreDotfileName(relPath.slice(cut + 1));
|
|
45
|
+
}
|
|
46
|
+
/** True when `dir` is this project's own source checkout, by two independent markers. */
|
|
47
|
+
function isSourceCheckout(dir) {
|
|
48
|
+
if (!existsSync(join(dir, "pnpm-workspace.yaml"))) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
53
|
+
return (typeof pkg === "object" &&
|
|
54
|
+
pkg !== null &&
|
|
55
|
+
"name" in pkg &&
|
|
56
|
+
pkg.name === "m3l-groundwork");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolves an asset for whichever layout is running. `fromDir` is the
|
|
64
|
+
* directory of a module at `src/` or `dist/` depth; it defaults to this
|
|
65
|
+
* file's own, and is injectable so both layouts can be tested from one.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveAsset(paths, fromDir = dirname(fileURLToPath(import.meta.url))) {
|
|
68
|
+
const repoRoot = join(fromDir, "..", "..", "..");
|
|
69
|
+
if (isSourceCheckout(repoRoot)) {
|
|
70
|
+
return join(repoRoot, paths.repo);
|
|
71
|
+
}
|
|
72
|
+
const vendored = join(fromDir, "..", paths.local);
|
|
73
|
+
if (existsSync(vendored)) {
|
|
74
|
+
return vendored;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`cannot locate "${paths.local}": not inside the m3l-groundwork checkout, and no vendored copy at ${vendored} -- ` +
|
|
77
|
+
"a published package should ship one (see scripts/vendor-assets.mjs)");
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=assets.js.map
|
package/dist/caps.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface CapCounts {
|
|
2
|
+
agents: number;
|
|
3
|
+
skills: number;
|
|
4
|
+
hooks: number;
|
|
5
|
+
workflows: number;
|
|
6
|
+
scripts: number;
|
|
7
|
+
}
|
|
8
|
+
/** The baseline's own hard caps -- `templates/core`'s `CLAUDE.md` is the prose statement of these same numbers. */
|
|
9
|
+
export declare const CAP_LIMITS: CapCounts;
|
|
10
|
+
export declare function countDirEntries(dir: string, filter?: (name: string) => boolean): number;
|
|
11
|
+
/**
|
|
12
|
+
* Counts every capped artifact category at `templateRoot`
|
|
13
|
+
* (`templates/core`). The `skills` count adds 1 for the `/customize` skill,
|
|
14
|
+
* which ships via the plugin copy rather than as a `templates/core` file --
|
|
15
|
+
* see `plugin.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function countBaselineCaps(templateRoot: string): CapCounts;
|
|
18
|
+
/**
|
|
19
|
+
* Counts every capped artifact category under a pack's `files/` root --
|
|
20
|
+
* the same shape as {@link countBaselineCaps} but without the `/customize`
|
|
21
|
+
* adjustment, since a pack never ships that skill. Used to verify a pack's
|
|
22
|
+
* declared `budget` matches what its own file tree actually contains.
|
|
23
|
+
*/
|
|
24
|
+
export declare function countPackBudget(packFilesDir: string): CapCounts;
|
|
25
|
+
//# sourceMappingURL=caps.d.ts.map
|
package/dist/caps.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts the baseline's own `.claude/` + workflow + script artifacts
|
|
3
|
+
* against the five hard caps `templates/core`'s own `CLAUDE.md` states
|
|
4
|
+
* (≤5 agents, ≤8 skills, ≤10 hooks, ≤3 CI workflows, ≤12 root scripts).
|
|
5
|
+
* The counting logic and the cap numbers both live here, once, so
|
|
6
|
+
* `report.ts` (the adoption-report table) and `main.ts` (the fresh-mode
|
|
7
|
+
* post-install summary) can't state a different number for the same cap.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { parseJsonc } from "./jsonc.js";
|
|
12
|
+
/** The baseline's own hard caps -- `templates/core`'s `CLAUDE.md` is the prose statement of these same numbers. */
|
|
13
|
+
export const CAP_LIMITS = {
|
|
14
|
+
agents: 5,
|
|
15
|
+
skills: 8,
|
|
16
|
+
hooks: 10,
|
|
17
|
+
workflows: 3,
|
|
18
|
+
scripts: 12,
|
|
19
|
+
};
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
export function countDirEntries(dir, filter) {
|
|
24
|
+
if (!existsSync(dir))
|
|
25
|
+
return 0;
|
|
26
|
+
const names = readdirSync(dir);
|
|
27
|
+
return filter === undefined ? names.length : names.filter(filter).length;
|
|
28
|
+
}
|
|
29
|
+
function countPackageScripts(root) {
|
|
30
|
+
const pkgPath = join(root, "package.json");
|
|
31
|
+
if (!existsSync(pkgPath))
|
|
32
|
+
return 0;
|
|
33
|
+
const parsed = parseJsonc(readFileSync(pkgPath, "utf8"));
|
|
34
|
+
if (!parsed.ok || !isRecord(parsed.value))
|
|
35
|
+
return 0;
|
|
36
|
+
const scripts = parsed.value["scripts"];
|
|
37
|
+
return isRecord(scripts) ? Object.keys(scripts).length : 0;
|
|
38
|
+
}
|
|
39
|
+
/** Raw artifact counts at `root`, with no baseline-specific adjustment. */
|
|
40
|
+
function countArtifacts(root) {
|
|
41
|
+
const claudeDir = join(root, ".claude");
|
|
42
|
+
return {
|
|
43
|
+
agents: countDirEntries(join(claudeDir, "agents"), (n) => n.endsWith(".md")),
|
|
44
|
+
skills: countDirEntries(join(claudeDir, "skills")),
|
|
45
|
+
hooks: countDirEntries(join(claudeDir, "hooks"), (n) => n.endsWith(".mjs") || n.endsWith(".js")),
|
|
46
|
+
workflows: countDirEntries(join(root, ".github", "workflows"), (n) => n.endsWith(".yml") || n.endsWith(".yaml")),
|
|
47
|
+
scripts: countPackageScripts(root),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Counts every capped artifact category at `templateRoot`
|
|
52
|
+
* (`templates/core`). The `skills` count adds 1 for the `/customize` skill,
|
|
53
|
+
* which ships via the plugin copy rather than as a `templates/core` file --
|
|
54
|
+
* see `plugin.ts`.
|
|
55
|
+
*/
|
|
56
|
+
export function countBaselineCaps(templateRoot) {
|
|
57
|
+
const raw = countArtifacts(templateRoot);
|
|
58
|
+
return { ...raw, skills: raw.skills + 1 };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Counts every capped artifact category under a pack's `files/` root --
|
|
62
|
+
* the same shape as {@link countBaselineCaps} but without the `/customize`
|
|
63
|
+
* adjustment, since a pack never ships that skill. Used to verify a pack's
|
|
64
|
+
* declared `budget` matches what its own file tree actually contains.
|
|
65
|
+
*/
|
|
66
|
+
export function countPackBudget(packFilesDir) {
|
|
67
|
+
return countArtifacts(packFilesDir);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=caps.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TokenTable } from "./tokens.js";
|
|
2
|
+
type ConflictStatus = "absent" | "identical" | "divergent";
|
|
3
|
+
export interface FileConflict {
|
|
4
|
+
relPath: string;
|
|
5
|
+
status: ConflictStatus;
|
|
6
|
+
/** Present only for a key-level comparison (package.json / tsconfig*.json): the top-level keys that differ. */
|
|
7
|
+
keyDiffs: string[] | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Compares every file `templates/core` would emit against what `targetDir` already has. */
|
|
10
|
+
export declare function planConflicts(templateRoot: string, targetDir: string, tokens: TokenTable): FileConflict[];
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=conflicts.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares `templates/core` (after token substitution) against a target
|
|
3
|
+
* directory, file by file, without writing anything. `package.json` and any
|
|
4
|
+
* `tsconfig*.json` get a key-level comparison rather than a whole-file one --
|
|
5
|
+
* a whole-file conflict on `package.json` is a useless finding, since the
|
|
6
|
+
* answer is always a merge, never "pick one file wholesale".
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
9
|
+
import { basename, join, relative } from "node:path";
|
|
10
|
+
import { restoreDotfilePath } from "./assets.js";
|
|
11
|
+
import { parseJsonc } from "./jsonc.js";
|
|
12
|
+
import { applyTokens } from "./tokens.js";
|
|
13
|
+
function isKeyLevelJsonFile(relPath) {
|
|
14
|
+
const name = basename(relPath);
|
|
15
|
+
return name === "package.json" || /^tsconfig(\..+)?\.json$/.test(name);
|
|
16
|
+
}
|
|
17
|
+
function compareJsonKeys(baselineContent, targetContent) {
|
|
18
|
+
const baseline = parseJsonc(baselineContent);
|
|
19
|
+
const target = parseJsonc(targetContent);
|
|
20
|
+
if (!baseline.ok || !target.ok) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const baselineValue = baseline.value;
|
|
24
|
+
const targetValue = target.value;
|
|
25
|
+
const keys = new Set([
|
|
26
|
+
...Object.keys(baselineValue),
|
|
27
|
+
...Object.keys(targetValue),
|
|
28
|
+
]);
|
|
29
|
+
const diffs = [];
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
if (JSON.stringify(baselineValue[key]) !== JSON.stringify(targetValue[key])) {
|
|
32
|
+
diffs.push(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return diffs;
|
|
36
|
+
}
|
|
37
|
+
function compareFile(relPath, baselineContent, targetPath) {
|
|
38
|
+
if (!existsSync(targetPath)) {
|
|
39
|
+
return { relPath, status: "absent", keyDiffs: undefined };
|
|
40
|
+
}
|
|
41
|
+
const targetContent = readFileSync(targetPath, "utf8");
|
|
42
|
+
if (isKeyLevelJsonFile(relPath)) {
|
|
43
|
+
const keyDiffs = compareJsonKeys(baselineContent, targetContent);
|
|
44
|
+
if (keyDiffs !== undefined) {
|
|
45
|
+
return {
|
|
46
|
+
relPath,
|
|
47
|
+
status: keyDiffs.length === 0 ? "identical" : "divergent",
|
|
48
|
+
keyDiffs,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Fall through to a whole-file compare if either side failed to parse.
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
relPath,
|
|
55
|
+
status: baselineContent === targetContent ? "identical" : "divergent",
|
|
56
|
+
keyDiffs: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function walkTemplate(root, currentDir, targetDir, tokens, results) {
|
|
60
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
61
|
+
const sourcePath = join(currentDir, entry.name);
|
|
62
|
+
const relPath = restoreDotfilePath(applyTokens(relative(root, sourcePath), tokens));
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
walkTemplate(root, sourcePath, targetDir, tokens, results);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const baselineContent = applyTokens(readFileSync(sourcePath, "utf8"), tokens);
|
|
68
|
+
results.push(compareFile(relPath, baselineContent, join(targetDir, relPath)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Compares every file `templates/core` would emit against what `targetDir` already has. */
|
|
72
|
+
export function planConflicts(templateRoot, targetDir, tokens) {
|
|
73
|
+
const results = [];
|
|
74
|
+
walkTemplate(templateRoot, templateRoot, targetDir, tokens, results);
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=conflicts.js.map
|
package/dist/emit.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TokenTable } from "./tokens.js";
|
|
2
|
+
export interface EmitResult {
|
|
3
|
+
filesWritten: string[];
|
|
4
|
+
}
|
|
5
|
+
/** Recursively copies `sourceDir` into `targetDir`, applying `tokens`. */
|
|
6
|
+
export declare function emitTemplate(sourceDir: string, targetDir: string, tokens: TokenTable): EmitResult;
|
|
7
|
+
//# sourceMappingURL=emit.d.ts.map
|
package/dist/emit.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies `templates/core` into a target directory, applying token
|
|
3
|
+
* substitution to both file contents and path segments (so a token in a
|
|
4
|
+
* directory or file NAME, not just its content, is honored). A published
|
|
5
|
+
* tarball stores `.gitignore`/`.npmrc` as `_gitignore`/`_npmrc` (npm strips
|
|
6
|
+
* the real names); they are restored here -- see `assets.ts`.
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, } from "node:fs";
|
|
9
|
+
import { join, relative, dirname, extname } from "node:path";
|
|
10
|
+
import { restoreDotfilePath } from "./assets.js";
|
|
11
|
+
import { applyTokens } from "./tokens.js";
|
|
12
|
+
// Extensions copied byte-for-byte, never text-decoded -- token substitution
|
|
13
|
+
// only makes sense for text content.
|
|
14
|
+
const BINARY_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".ico"]);
|
|
15
|
+
/** Recursively copies `sourceDir` into `targetDir`, applying `tokens`. */
|
|
16
|
+
export function emitTemplate(sourceDir, targetDir, tokens) {
|
|
17
|
+
const filesWritten = [];
|
|
18
|
+
walk(sourceDir, sourceDir, targetDir, tokens, filesWritten);
|
|
19
|
+
return { filesWritten };
|
|
20
|
+
}
|
|
21
|
+
function walk(root, currentSourceDir, targetRoot, tokens, filesWritten) {
|
|
22
|
+
for (const entry of readdirSync(currentSourceDir, { withFileTypes: true })) {
|
|
23
|
+
const sourcePath = join(currentSourceDir, entry.name);
|
|
24
|
+
const relPath = restoreDotfilePath(applyTokens(relative(root, sourcePath), tokens));
|
|
25
|
+
const targetPath = join(targetRoot, relPath);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
mkdirSync(targetPath, { recursive: true });
|
|
28
|
+
walk(root, sourcePath, targetRoot, tokens, filesWritten);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
32
|
+
if (BINARY_EXTENSIONS.has(extname(entry.name))) {
|
|
33
|
+
copyFileSync(sourcePath, targetPath);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const content = readFileSync(sourcePath, "utf8");
|
|
37
|
+
writeFileSync(targetPath, applyTokens(content, tokens));
|
|
38
|
+
}
|
|
39
|
+
filesWritten.push(relPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=emit.js.map
|
package/dist/git.d.ts
ADDED
package/dist/git.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The two mechanical steps after emission: `git init`, then the first install. */
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
export function gitInit(cwd) {
|
|
4
|
+
execFileSync("git", ["init", "-q"], { cwd, stdio: "inherit" });
|
|
5
|
+
}
|
|
6
|
+
export function runInstall(cwd) {
|
|
7
|
+
execFileSync("pnpm", ["install"], { cwd, stdio: "inherit" });
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far a project's harness has drifted from `templates/core`'s -- the
|
|
3
|
+
* second number the grader reports, kept separate from rubric quality
|
|
4
|
+
* because `/customize` deliberately rewrites the baseline. "Divergent" is
|
|
5
|
+
* therefore information, never a defect. Derived from the conflict plan
|
|
6
|
+
* `planConflicts` already computed, so no second diff is ever run.
|
|
7
|
+
*/
|
|
8
|
+
import type { FileConflict } from "../conflicts.js";
|
|
9
|
+
export interface HarnessConformance {
|
|
10
|
+
identical: number;
|
|
11
|
+
divergent: number;
|
|
12
|
+
absent: number;
|
|
13
|
+
/** Baseline harness files the project has edited. */
|
|
14
|
+
divergentFiles: string[];
|
|
15
|
+
/** Baseline harness files the project lacks. */
|
|
16
|
+
absentFiles: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Counts the harness-scoped subset of a baseline conflict plan. */
|
|
19
|
+
export declare function summarizeHarnessConformance(conflicts: FileConflict[]): HarnessConformance;
|
|
20
|
+
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function isHarnessPath(relPath) {
|
|
2
|
+
return relPath.startsWith(".claude/") || relPath === "CLAUDE.md";
|
|
3
|
+
}
|
|
4
|
+
/** Counts the harness-scoped subset of a baseline conflict plan. */
|
|
5
|
+
export function summarizeHarnessConformance(conflicts) {
|
|
6
|
+
const scoped = conflicts.filter((conflict) => isHarnessPath(conflict.relPath));
|
|
7
|
+
const files = (status) => scoped
|
|
8
|
+
.filter((conflict) => conflict.status === status)
|
|
9
|
+
.map((conflict) => conflict.relPath);
|
|
10
|
+
return {
|
|
11
|
+
identical: files("identical").length,
|
|
12
|
+
divergent: files("divergent").length,
|
|
13
|
+
absent: files("absent").length,
|
|
14
|
+
divergentFiles: files("divergent"),
|
|
15
|
+
absentFiles: files("absent"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reader for the YAML subset Claude Code frontmatter actually uses --
|
|
3
|
+
* `SKILL.md`, agent, and rule files. Hand-rolled because this package has no
|
|
4
|
+
* runtime dependencies (see `jsonc.ts` for the same trade-off). It handles
|
|
5
|
+
* the scalar forms real harness files contain: plain, single/double quoted,
|
|
6
|
+
* block scalars (`>-`, `>`, `|`, `|-`), block lists, and flow lists.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT supported: nested mappings (a key whose value is an
|
|
9
|
+
* indented map, e.g. `mcpServers:` with inline server definitions) -- such a
|
|
10
|
+
* key is recorded with an empty string value rather than misparsed -- plus
|
|
11
|
+
* anchors, tags, and multi-document streams. Every string result is
|
|
12
|
+
* trimmed; a block scalar's trailing newline is not preserved.
|
|
13
|
+
*/
|
|
14
|
+
/** A parsed frontmatter value: a scalar, or a list of scalars. */
|
|
15
|
+
export type FrontmatterValue = string | string[];
|
|
16
|
+
export type FrontmatterResult = {
|
|
17
|
+
ok: true;
|
|
18
|
+
fields: Map<string, FrontmatterValue>;
|
|
19
|
+
/** Everything after the closing `---` line. */
|
|
20
|
+
body: string;
|
|
21
|
+
/** Lines inside the block the reader could not interpret -- never silently dropped. */
|
|
22
|
+
problems: string[];
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parses the `---`-delimited frontmatter at the top of `content`. Returns a
|
|
29
|
+
* structured failure (never throws) when there is no block or it is never
|
|
30
|
+
* closed; lines it cannot interpret inside an otherwise valid block are
|
|
31
|
+
* reported in `problems`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseFrontmatter(content: string): FrontmatterResult;
|
|
34
|
+
/** A field as one string -- a list is joined with `, `. `undefined` when absent. */
|
|
35
|
+
export declare function fieldText(fields: Map<string, FrontmatterValue>, key: string): string | undefined;
|
|
36
|
+
/** A field as a list -- a non-empty scalar becomes a one-item list. `undefined` when absent. */
|
|
37
|
+
export declare function fieldList(fields: Map<string, FrontmatterValue>, key: string): string[] | undefined;
|
|
38
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|