From 9e51a4092a29a1ffedefe0c86743477f392ed930 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Sat, 18 Jul 2026 18:57:39 -0400 Subject: [PATCH] make skeleton the only init template --- .changeset/skeleton-only-init-template.md | 5 + CLAUDE.md | 2 +- apps/docs/src/content/docs/cli-reference.mdx | 8 +- .../docs/src/content/docs/getting-started.mdx | 5 +- packages/ghost/src/scan/templates.ts | 283 ------------------ packages/ghost/src/skill-bundle/SKILL.md | 2 +- .../src/skill-bundle/references/capture.md | 3 +- packages/ghost/test/cli.test.ts | 146 +++------ 8 files changed, 56 insertions(+), 398 deletions(-) create mode 100644 .changeset/skeleton-only-init-template.md diff --git a/.changeset/skeleton-only-init-template.md b/.changeset/skeleton-only-init-template.md new file mode 100644 index 00000000..0b44103f --- /dev/null +++ b/.changeset/skeleton-only-init-template.md @@ -0,0 +1,5 @@ +--- +"@design-intelligence/ghost": major +--- + +Make `skeleton` the only `ghost init` template, removing `minimal`, `composition`, and the legacy `default` and `steering` aliases. diff --git a/CLAUDE.md b/CLAUDE.md index 4198dba7..34b2757b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,7 +100,7 @@ Core workflow: | Command | Description | | --- | --- | -| `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--template minimal` writes a smaller cover-led starter; `--body vessel-light` installs a full inhabited package instead. `--with checks` also adds the checks directory. | +| `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--body vessel-light` installs a full inhabited package instead. `--with checks` also adds the checks directory. | | `ghost checks init` | Scaffold `.ghost/checks/` with an example review assertion. | | `ghost validate` | Validate the package: manifest shape, node validity, material locators, check references, and glossary kind prefixes. | | `ghost gather [ask…]` | Emit the complete guidance menu so the agent can pull applicable nodes. | diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx index b403e254..c7707bb9 100644 --- a/apps/docs/src/content/docs/cli-reference.mdx +++ b/apps/docs/src/content/docs/cli-reference.mdx @@ -57,9 +57,9 @@ unexpected code. Scaffold a `.ghost/` package. The default starter includes a manifest, glossary, `brand.md` cover, foundation nodes with open questions, and context for the authoring conversation. These prompts tell the agent to ask rather than -invent brand values. Use `--template minimal` for a smaller starter. Add -guidance with files such as `principle.density.md`. Use `--package ` for an -exact directory, or set `GHOST_PACKAGE_DIR` when a host wrapper stores ghost +invent brand values. Add guidance with files such as `principle.density.md`. +Use `--package ` for an exact directory, or set `GHOST_PACKAGE_DIR` when a +host wrapper stores ghost files outside `.ghost`. Pass `--with checks` to add the checks directory. A template provides files and prompts for guidance you still need to author. A @@ -80,7 +80,7 @@ your own. `--body` and `--template` are mutually exclusive. ```bash ghost init -ghost init --template minimal +ghost init --template skeleton ghost init --with checks ghost init --package product-ghost GHOST_PACKAGE_DIR=.agents/ghost ghost init diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx index 06222b92..52a516a6 100644 --- a/apps/docs/src/content/docs/getting-started.mdx +++ b/apps/docs/src/content/docs/getting-started.mdx @@ -89,10 +89,9 @@ it as one node, then add guidance when another repeated decision appears. Your agent interviews you, works with the material you show it, drafts node prose, and asks you to curate the claims. `ghost init` handles the repeatable -setup by writing a starter you adapt: a glossary, a `brand.md` cover, and +setup by writing the skeleton starter: a glossary, a `brand.md` cover, and foundation nodes with open questions. Answer the questions with your brand's -decisions. Use `ghost init --template minimal` for a smaller starter. The -[CLI reference](/docs/cli) lists the generated files. +decisions. The [CLI reference](/docs/cli) lists the generated files. ```bash ghost init diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts index 2646a19a..fec7c7c2 100644 --- a/packages/ghost/src/scan/templates.ts +++ b/packages/ghost/src/scan/templates.ts @@ -42,283 +42,6 @@ function gitignoreFile(): TemplateFile { }; } -/** - * The minimal starter: a manifest, a package-level glossary declaring the - * starter kind vocabulary, and a package-root `index.md` cover. Additional - * truths are plain markdown nodes; optional material locators live on the node - * that explains them. Checks are opt-in via `ghost checks init`. - */ -const MINIMAL_TEMPLATE: GhostInitTemplate = { - name: "minimal", - description: - "Minimal node package: manifest + glossary + a starter cover node.", - async files() { - const medianFile = await medianTemplateFile(); - return [ - manifestFile("index"), - gitignoreFile(), - { - relativePath: "glossary.md", - content: `--- -kinds: - - name: principle - - name: condition - - name: exemplar - - name: anti-goal - - name: cliche - - name: asset - - name: pattern ---- - -# principle - -Durable stance: true across media unless a narrower condition explicitly limits it. - -# condition - -Situational truth: fires only when the stated situation holds. - -# exemplar - -Illustrative reference. Match its form and quality; treat its specifics as -evidence unless the node says the sample itself is normative. - -# anti-goal - -What this brand must never look, sound, or feel like — named generic patterns -and rejected neighbors. Always-on, like a principle, but stated as the thing -to steer away from. - -# cliche - -The defaults a generative model falls back on when no brand is steering it. -These are nobody's brand. Each entry pairs the tired move with the honest -replacement. Gather before styling anything greenfield; enforced by paired -checks at review. - -# asset - -Material truth about concrete brand assets such as logos, illustrations, motion, imagery, or files. - -# pattern - -Reusable composition or product pattern whose purpose is distinguishable from neighboring patterns. -`, - }, - { - relativePath: "index.md", - content: `--- -description: Always read first — the non-negotiables and how to read this package. ---- - -Replace this placeholder prose with two things: - -**The non-negotiables.** The handful of truths that apply to every task, every -medium, no matter what else is gathered — hard invariants, the anti-goals, the -one-sentence stance. Anything that must never be missed belongs here, stated -briefly; link out to the full node by id for depth. - -**How to read the rest.** What this package covers, how its kinds organize -the corpus, and where the package deliberately stays silent — including, if -you want one, a stricter silence posture ("when this package is silent on X, -ask a human") that overrides the default proceed-provisionally behavior. - -\`index\` is this package's manifest-declared cover: \`ghost gather\` inlines its -body before the menu and excludes it from selection. Its privilege comes from -\`cover: index\` in \`manifest.yml\`, not from the filename. You may rename it; -update the manifest's \`cover\` id at the same time. - -The glossary declares the kind vocabulary. A node's kind comes from its -filename prefix: \`principle.density.md\` has kind \`principle\` and slug -\`density\`. A bare filename like \`voice.md\` has no kind, which is fine. -When a truth is narrower, state the condition in the prose — the situation where -it applies — never a filing destination. -`, - }, - medianFile, - ]; - }, -}; - -/** - * The composition starter: everything in `minimal`, plus a worked composition - * ladder — an invariants floor (`principle.composition`), one bound/open - * pattern, and a cover that teaches the convention. For teams whose - * package must steer *what agents build*, not only what they say. - * - * The ladder is an authoring convention, not a schema: patterns state what is - * bound (do not redecide) and what is open (the generator's call), and every - * pattern names the principles it refines. Nothing here adds frontmatter, - * edges, or routing — the corpus stays flat. - */ -const COMPOSITION_TEMPLATE: GhostInitTemplate = { - name: "composition", - description: - "Composition starter: minimal files + an invariants floor and a worked bound/open pattern.", - async files() { - const medianFile = await medianTemplateFile(); - return [ - manifestFile("index"), - gitignoreFile(), - { - relativePath: "glossary.md", - content: `--- -kinds: - - name: principle - - name: condition - - name: exemplar - - name: anti-goal - - name: cliche - - name: asset - - name: pattern ---- - -# principle - -Durable stance: true across media unless a narrower condition explicitly limits it. -Composition principles are the floor — when no pattern matches an ask, they are -what the agent composes against. - -# condition - -Situational truth: fires only when the stated situation holds. - -# exemplar - -Illustrative reference. Match its form and quality; treat its specifics as -evidence unless the node says the sample itself is normative. Pair an -exemplar with a screenshot or implementation path in \`materials\` so the blessed -render travels with the prose. - -# anti-goal - -What this brand must never look, sound, or feel like — named generic patterns -and rejected neighbors. Always-on, like a principle, but stated as the thing -to steer away from. - -# cliche - -The defaults a generative model falls back on when no brand is steering it. -These are nobody's brand. Each entry pairs the tired move with the honest -replacement. Gather before styling anything greenfield; enforced by paired -checks at review. - -# asset - -Material truth about concrete brand assets such as logos, illustrations, motion, -imagery, or files. For components, state the slot contract in prose: what the -asset admits, how many, and where. - -# pattern - -Reusable composition whose purpose is distinguishable from neighboring patterns. -A pattern states what is **bound** (decided — do not redecide) and what is -**open** (the generator's call, within named limits), and names the principles -it refines. A pattern may only narrow what its principles allow — never loosen -them. -`, - }, - { - relativePath: "index.md", - content: `--- -description: Start here — how this package steers composition, from patterns down to principles. ---- - -This package steers what gets built, not only what gets said. It is -organized as a ladder of binding depth: - -1. **Patterns** (\`pattern.*\`) are mostly-decided compositions. When an ask - matches a pattern's stated purpose, use it: apply what it binds, decide only - what it leaves open. -2. **Principles** (\`principle.*\`) are the floor. When no pattern matches, do - not fall back to your own taste — compose freely *inside* the invariants in - \`principle.composition\` and its siblings. -3. **Exemplars and assets** carry the concrete vocabulary: blessed renders, - real components, real tokens, located via \`materials\`. - -Two laws keep the ladder honest: - -- **Narrowing only.** A pattern may bind or narrow what a principle leaves - open; it may never contradict one. If a pattern conflicts with a principle, - the principle wins and the pattern file is wrong. -- **Every "never" casts a shadow.** Each refusal-grade line in a principle - should be mirrored by a review check (\`ghost checks init\`) that - references the node it enforces, so the same opinion steers before building - and grades after. - -Replace this prose with your own front door once the corpus has shape. The -starter nodes below are teaching placeholders — edit them into your real -opinions before trusting them. -`, - }, - { - relativePath: "principle.composition.md", - content: `--- -description: The composition floor — hard layout invariants that hold for every screen, whether or not a pattern matches the ask. ---- - -These invariants apply to everything built against this package. Patterns -narrow them; nothing loosens them. Each is written as a refusal or a limit with -a number, because a stance an agent can check is a stance that steers. - -**The starter opinions below are placeholders. Keep the voice — refusals and -numbers, not vibes — and replace the values with yours.** - -- **One focal point.** Every screen has exactly one element at the largest - scale. If two elements compete for primacy, the second one is wrong — demote - or delete it. Never resolve the tie by making both big. -- **Hierarchy descends.** Type scale strictly decreases with nesting depth. - Never give a child element a larger scale than its parent context. -- **Density has a budget.** State the budget per surface (for example: at most - N data points per viewport on consumer surfaces; a higher N on operational - ones). When content exceeds the budget, cut or collapse — never shrink type - below the ramp to make room. -- **Actions land last.** Calls to action render at the end of reading order. - At most one primary action per screen; never two. -- **Separation escalates.** To separate content, reach for whitespace first, - then a divider, then a container — in that order, never skipping a step. - -Mirror each refusal here with a check under \`checks/\` that lists this -node in its \`references\`, so review enforces the same floor this prose sets. -`, - }, - { - relativePath: "pattern.status-with-next-step.md", - content: `--- -description: Status of an ongoing thing plus the single next step — reach for this when the ask is "where is X / how is X going." ---- - -**This is a worked example of the pattern convention — bound, open, refines. -Replace its content with a real pattern from your product, keep the structure.** - -**When this applies:** the ask is about the state of something in flight — an -order, a transfer, a build, a goal. - -**Bound (decided — do not redecide):** - -- The current status renders first and largest; it is the focal point. -- A short line of meaning follows the status: why it matters or what changed, - never a restatement of it. -- Exactly one primary action, rendered last: the single next step. - -**Open (your call, within these limits):** - -- Supporting evidence may be a timeline or a compact stat list — whichever - fits the data. Never a full chart in this pattern. -- Density may flex one step tighter on operational surfaces. - -**Refines:** \`principle.composition\`. If anything above conflicts with the -floor, the floor wins and this file is wrong. - -When a blessed render of this pattern exists, add an \`exemplar.*\` node with -\`materials\` pointing at the screenshot and the implementation path. -`, - }, - medianFile, - ]; - }, -}; - const SKELETON_FILE_ORDER = new Map( [ "glossary.md", @@ -363,8 +86,6 @@ async function medianTemplateFile(): Promise { } const TEMPLATES = new Map([ - [MINIMAL_TEMPLATE.name, MINIMAL_TEMPLATE], - [COMPOSITION_TEMPLATE.name, COMPOSITION_TEMPLATE], [SKELETON_TEMPLATE.name, SKELETON_TEMPLATE], ]); @@ -436,10 +157,6 @@ export const DEFAULT_TEMPLATE_NAME = SKELETON_TEMPLATE.name; /** Look up a registered init template by name. */ export function getInitTemplate(name: string): GhostInitTemplate | undefined { - // `default` and `steering` are silent aliases for the skeleton starter: - // `default` for callers that never named a template, `steering` for - // callers pinned to the pre-skeleton default's name. - if (name === "default" || name === "steering") return SKELETON_TEMPLATE; return TEMPLATES.get(name); } diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md index 5fd50987..28e18e3d 100644 --- a/packages/ghost/src/skill-bundle/SKILL.md +++ b/packages/ghost/src/skill-bundle/SKILL.md @@ -87,7 +87,7 @@ this same host-agent session. | Verb | Purpose | |---|---| -| `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--template minimal` writes a smaller cover-led starter. `--with checks` also adds the checks directory. | +| `ghost init` | Scaffold `.ghost/` with the skeleton starter: manifest, glossary, a `brand.md` cover, foundation chapters, context nodes, and the cliche floor. `--with checks` also adds the checks directory. | | `ghost checks init` | Scaffold `.ghost/checks/` with an example review assertion. | | `ghost validate [file-or-dir]` | Validate manifest, nodes, material locators, check references, and glossary kind prefixes. | | `ghost gather [ask…] [--format json]` | Emit the complete guidance menu plus coverage line; log exposed ids. | diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md index 0900596f..63539e8c 100644 --- a/packages/ghost/src/skill-bundle/references/capture.md +++ b/packages/ghost/src/skill-bundle/references/capture.md @@ -345,8 +345,7 @@ ghost validate `ghost init` seeds the skeleton starter: the manifest, a starter `glossary.md`, a `brand.md` cover, foundation chapters with open questions, and the model cliche floor. Replace open questions with real product guidance before -using it to steer generation. Use `ghost init --template minimal` for a -manifest, glossary, cover, and cliche floor. +using it to steer generation. Write the manifest-declared cover as the human-curated front door: non-negotiables that apply to every task, what the package covers, and any diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts index 4d090be5..12c0aca5 100644 --- a/packages/ghost/test/cli.test.ts +++ b/packages/ghost/test/cli.test.ts @@ -104,6 +104,29 @@ async function runCli( return { stdout, stderr, code: exitCode ?? 0 }; } +async function writeBareTestPackage(dir: string): Promise { + const packageDir = join(dir, ".ghost"); + await mkdir(packageDir, { recursive: true }); + await Promise.all([ + writeFile( + join(packageDir, "manifest.yml"), + "schema: ghost.package/v1\nid: local\ncover: index\n", + ), + writeFile( + join(packageDir, "glossary.md"), + "---\nkinds:\n - name: principle\n - name: condition\n - name: exemplar\n - name: anti-goal\n - name: cliche\n - name: asset\n - name: pattern\n---\n", + ), + writeFile( + join(packageDir, "index.md"), + "---\ndescription: Test package cover.\n---\n\nTest package.\n", + ), + writeFile( + join(packageDir, "cliche.median.md"), + "---\ndescription: Test cliche floor.\n---\n\nAvoid generic defaults.\n", + ), + ]); +} + describe("ghost CLI", () => { let dir: string; @@ -320,89 +343,6 @@ describe("ghost CLI", () => { await expectSkeletonPackage(initOutput.written); }); - it("initializes the minimal fingerprint package", async () => { - const init = await runCli( - ["init", "--template", "minimal", "--format", "json"], - dir, - ); - - expect(init.code).toBe(0); - const initOutput = JSON.parse(init.stdout); - expect(initOutput.written).toContain("manifest.yml"); - expect(initOutput.written).toContain("glossary.md"); - expect(initOutput.written).toContain("index.md"); - expect(initOutput.written).toContain("cliche.median.md"); - expect(initOutput.written).not.toContain("principle.stance.md"); - expect(initOutput.written).not.toContain("decision.tradeoff.md"); - - const validate = await runCli(["validate", "--format", "json"], dir); - expect(validate.code).toBe(0); - const report = JSON.parse(validate.stdout); - expect(report.errors).toBe(0); - expect(report.warnings).toBe(0); - }); - - it("keeps default and steering as aliases for the skeleton template", async () => { - for (const name of ["default", "steering"]) { - await rm(join(dir, ".ghost"), { recursive: true, force: true }); - const init = await runCli( - ["init", "--template", name, "--format", "json"], - dir, - ); - - expect(init.code).toBe(0); - const initOutput = JSON.parse(init.stdout); - expect([...initOutput.written].sort()).toEqual( - [...SKELETON_FILES].sort(), - ); - - const validate = await runCli(["validate"], dir); - expect(validate.code).toBe(0); - } - }); - - it("initializes the composition starter template", async () => { - const init = await runCli( - ["init", "--template", "composition", "--format", "json"], - dir, - ); - - expect(init.code).toBe(0); - const initOutput = JSON.parse(init.stdout); - expect(initOutput.written).toContain("manifest.yml"); - expect(initOutput.written).toContain("glossary.md"); - expect(initOutput.written).toContain("index.md"); - expect(initOutput.written).toContain("principle.composition.md"); - expect(initOutput.written).toContain("pattern.status-with-next-step.md"); - expect(initOutput.written).toContain("cliche.median.md"); - - // The scaffolded package is valid as written. - const validate = await runCli(["validate", "--format", "json"], dir); - expect(validate.code).toBe(0); - const report = JSON.parse(validate.stdout); - expect(report.errors).toBe(0); - expect(report.warnings).toBe(0); - - // The ladder nodes surface in the gather menu with their kinds. - const gather = await runCli(["gather", "--format", "json"], dir); - expect(gather.code).toBe(0); - const menu = JSON.parse(gather.stdout); - const ids = menu.nodes.map((node: { id: string }) => node.id); - expect(ids).toContain("principle.composition"); - expect(ids).toContain("pattern.status-with-next-step"); - const pattern = menu.nodes.find( - (node: { id: string }) => node.id === "pattern.status-with-next-step", - ); - expect(pattern.kind).toBe("pattern"); - - // The pattern body carries the bound/open/refines convention. - const pull = await runCli(["pull", "pattern.status-with-next-step"], dir); - expect(pull.code).toBe(0); - expect(pull.stdout).toContain("Bound (decided"); - expect(pull.stdout).toContain("Open (your call"); - expect(pull.stdout).toContain("principle.composition"); - }); - it("initializes the skeleton starter template by name", async () => { const init = await runCli( ["init", "--template", "skeleton", "--format", "json"], @@ -423,9 +363,7 @@ describe("ghost CLI", () => { expect(result.code).toBe(2); expect(result.stderr).toContain("Unknown init template 'nope'"); - expect(result.stderr).toContain("minimal"); - expect(result.stderr).toContain("composition"); - expect(result.stderr).toContain("skeleton"); + expect(result.stderr).toContain("Available: skeleton"); }); it("installs the vessel-light body: full corpus, materials, checks", async () => { @@ -490,7 +428,7 @@ describe("ghost CLI", () => { expect(unknown.stderr).toContain("vessel-light"); const both = await runCli( - ["init", "--body", "vessel-light", "--template", "minimal"], + ["init", "--body", "vessel-light", "--template", "skeleton"], dir, { allowNoExit: true }, ); @@ -604,9 +542,9 @@ describe("ghost CLI", () => { }); it("refuses to overwrite existing fingerprint files unless forced", async () => { - await runCli(["init", "--template", "minimal"], dir); + await runCli(["init"], dir); await writeFile( - join(dir, ".ghost", "index.md"), + join(dir, ".ghost", "brand.md"), "---\n---\n\nCurated Surface voice.\n", ); @@ -617,7 +555,7 @@ describe("ghost CLI", () => { "Refusing to overwrite existing ghost package file(s)", ); await expect( - readFile(join(dir, ".ghost", "index.md"), "utf-8"), + readFile(join(dir, ".ghost", "brand.md"), "utf-8"), ).resolves.toContain("Curated Surface"); const forced = await runCli(["init", "--force"], dir); @@ -875,7 +813,7 @@ describe("ghost CLI", () => { }); it("gather reports concrete coverage and pull uses steering order with given-order escape hatch", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await writeFile( join(dir, ".ghost", "glossary.md"), "---\nkinds:\n - name: anti-goal\n - name: asset\n - name: principle\n---\n\n# anti-goal\n\nReview-critical replacement.\n\n# asset\n\nConcrete material.\n\n# principle\n\nRule.\n", @@ -941,7 +879,7 @@ describe("ghost CLI", () => { }); it("gather names payload types without ranking them", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await writeFile( join(dir, ".ghost", "asset.tokens.md"), [ @@ -1022,7 +960,7 @@ describe("ghost CLI", () => { }); it("pull extracts Skeletons last and validate warns on malformed Skeleton sections", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await writeFile( join(dir, ".ghost", "pattern.card.md"), "---\ndescription: Card pattern.\n---\n\nPattern prose.\n\n## Skeleton\n\n```tsx\n
{children}
\n```\n\nAfter skeleton should be stripped.\n", @@ -1050,7 +988,7 @@ describe("ghost CLI", () => { }); it("pull uses fences longer than inlined material and Skeleton backtick runs", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, "brand"), { recursive: true }); await writeFile( join(dir, "brand", "example.md"), @@ -1099,7 +1037,7 @@ describe("ghost CLI", () => { }); it("pull emits binary materials as inspect-pointers in markdown and JSON", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, "brand"), { recursive: true }); await writeFile(join(dir, "brand", "mark.png"), Buffer.from([0, 1, 2])); await writeFile( @@ -1257,7 +1195,7 @@ describe("ghost CLI", () => { }); it("pull inlines material files and emits inspect-pointers for binary materials, oversize files, and URL locators", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, ".ghost", "materials"), { recursive: true }); await mkdir(join(dir, "brand"), { recursive: true }); await writeFile( @@ -1303,7 +1241,7 @@ describe("ghost CLI", () => { }); it("pull supports locator-only output with --no-materials", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, ".ghost", "materials"), { recursive: true }); await writeFile( join(dir, ".ghost", "materials", "tokens.css"), @@ -1332,7 +1270,7 @@ describe("ghost CLI", () => { }); it("pull emits transported material objects in JSON", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, ".ghost", "materials"), { recursive: true }); await mkdir(join(dir, "brand"), { recursive: true }); await writeFile( @@ -1373,7 +1311,7 @@ describe("ghost CLI", () => { }); it("pull expands globs with a cap and notes elision beyond it", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, "brand", "samples"), { recursive: true }); for (let i = 0; i < 13; i++) { await writeFile( @@ -1445,7 +1383,7 @@ describe("ghost CLI", () => { }); it("pulse reports local gather/pull metrics", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await writeFile( join(dir, ".ghost", "principle.trust.md"), "---\ndescription: Trust.\n---\n\nBody.\n", @@ -1952,7 +1890,7 @@ describe("ghost CLI", () => { }); it("export audits bundled, URL, and referenced local material locators", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, ".ghost", "materials"), { recursive: true }); await mkdir(join(dir, "brand"), { recursive: true }); await writeFile( @@ -1992,7 +1930,7 @@ describe("ghost CLI", () => { }); it("export --strict exits 2 when referenced local material locators are stranded", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await writeFile( join(dir, ".ghost", "asset.voice.md"), "---\ndescription: Voice.\nmaterials:\n - brand/voice.txt\n---\n\nVoice prose.\n", @@ -2006,7 +1944,7 @@ describe("ghost CLI", () => { }); it("commands work against an unpacked export directory outside a git repo", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await mkdir(join(dir, ".ghost", "materials"), { recursive: true }); await writeFile( join(dir, ".ghost", "materials", "tokens.css"), @@ -2088,7 +2026,7 @@ describe("ghost CLI", () => { }); it("checks init skips median tells when the median node is absent", async () => { - await runCli(["init", "--template", "minimal"], dir); + await writeBareTestPackage(dir); await rm(join(dir, ".ghost", "cliche.median.md")); const add = await runCli(["checks", "init"], dir);