@forgeailab/create-spark 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude/skills/architecture-cutline/SKILL.md +96 -0
  2. package/.claude/skills/board-review/SKILL.md +77 -0
  3. package/.claude/skills/code-review/SKILL.md +76 -0
  4. package/.claude/skills/execute-task/SKILL.md +80 -0
  5. package/.claude/skills/idea-sharpen/SKILL.md +65 -0
  6. package/.claude/skills/implementation-brief/SKILL.md +87 -0
  7. package/.claude/skills/mvp-board/SKILL.md +95 -0
  8. package/.claude/skills/mvp-grill/SKILL.md +60 -0
  9. package/.claude/skills/mvp-spec/SKILL.md +78 -0
  10. package/.claude/skills/new-pack/SKILL.md +156 -0
  11. package/.claude/skills/next-task/SKILL.md +65 -0
  12. package/.claude/skills/pack-add/SKILL.md +64 -0
  13. package/.claude/skills/pack-resolve/SKILL.md +67 -0
  14. package/.claude/skills/parallel-execution/SKILL.md +68 -0
  15. package/.claude/skills/qa-verify/SKILL.md +77 -0
  16. package/.claude/skills/risk-check/SKILL.md +88 -0
  17. package/.claude/skills/sync-board/SKILL.md +76 -0
  18. package/.claude/skills/ux-theme/SKILL.md +93 -0
  19. package/.codex/skills/architecture-cutline/SKILL.md +94 -0
  20. package/.codex/skills/board-review/SKILL.md +75 -0
  21. package/.codex/skills/code-review/SKILL.md +73 -0
  22. package/.codex/skills/execute-task/SKILL.md +76 -0
  23. package/.codex/skills/idea-sharpen/SKILL.md +63 -0
  24. package/.codex/skills/implementation-brief/SKILL.md +85 -0
  25. package/.codex/skills/mvp-board/SKILL.md +93 -0
  26. package/.codex/skills/mvp-grill/SKILL.md +58 -0
  27. package/.codex/skills/mvp-spec/SKILL.md +76 -0
  28. package/.codex/skills/new-pack/SKILL.md +153 -0
  29. package/.codex/skills/next-task/SKILL.md +64 -0
  30. package/.codex/skills/pack-add/SKILL.md +62 -0
  31. package/.codex/skills/pack-resolve/SKILL.md +65 -0
  32. package/.codex/skills/parallel-execution/SKILL.md +66 -0
  33. package/.codex/skills/qa-verify/SKILL.md +74 -0
  34. package/.codex/skills/risk-check/SKILL.md +86 -0
  35. package/.codex/skills/sync-board/SKILL.md +72 -0
  36. package/.codex/skills/ux-theme/SKILL.md +91 -0
  37. package/package.json +8 -5
  38. package/scripts/sync-skills.ts +223 -0
  39. /package/templates/nextjs/{anvil.config.json → spark.config.json} +0 -0
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: qa-verify
3
+ description: Verify the app actually runs and the feature works end-to-end, not just that code compiles. Use after a feature batch lands, before a demo, when the user says "does this actually work?", "run it and check", or before flipping a task to `Validated`. Do NOT use as a substitute for `/code-review` — they cover different failure modes.
4
+ # Generated from .claude/skills/qa-verify/SKILL.md — DO NOT EDIT directly
5
+ ---
6
+
7
+ # Skill: qa-verify
8
+
9
+ ## Goal
10
+
11
+ Boot the app, click through the real user flow, and confirm the acceptance criteria hold when humans actually use the product. Type-checks and unit tests pass != feature works.
12
+
13
+ ## Recommended model
14
+
15
+ Sonnet 4.6. This is execution, not judgment.
16
+
17
+ ## Inputs
18
+
19
+ Read these (required):
20
+
21
+ - `.ai/board.md` — task(s) being verified
22
+ - `.ai/product-spec.md` — the core user journey
23
+
24
+ Read if useful:
25
+
26
+ - `.ai/ux-theme.md` for empty / loading / error patterns
27
+ - `README.md` / `package.json` for the run command
28
+
29
+ ## Rules
30
+
31
+ - Always run the app. If you cannot launch it, report that explicitly — do not claim verification from reading code.
32
+ - Walk the **core user journey from `product-spec.md`**, not just the changed feature. Regressions in adjacent flows count.
33
+ - Check empty / loading / error / mobile states for every screen touched. MVPs feel broken at the seams, not the happy path.
34
+ - Capture exact commands and outputs so the user can reproduce.
35
+ - Use a real browser or device when relevant (Playwright MCP if available). Curl-ing an API endpoint is not UI verification.
36
+
37
+ ## Workflow
38
+
39
+ 1. Find the run command (project README, `package.json` scripts, or ask).
40
+ 2. Boot the app. Note the URL.
41
+ 3. Walk the core journey step by step from the spec.
42
+ 4. Re-walk the specific feature(s) from the task(s).
43
+ 5. Probe empty / loading / error / mobile.
44
+ 6. Write the report.
45
+
46
+ ## Output format
47
+
48
+ ```md
49
+ ## QA verification — <TASK-ID(s)> / <feature name>
50
+
51
+ ### Boot
52
+ - Command: `<cmd>`
53
+ - Result: app running at <url> | failed (<reason>)
54
+
55
+ ### Core user journey (from spec)
56
+ - [x|✗] Step 1: <description> — <observation>
57
+ - [x|✗] Step 2: ...
58
+
59
+ ### Feature-specific checks
60
+ - [x|✗] <acceptance criterion> — <observation>
61
+
62
+ ### Edge states
63
+ - Empty state: <ok | broken | missing>
64
+ - Loading state: <ok | broken | missing>
65
+ - Error state: <ok | broken | missing>
66
+ - Mobile / narrow viewport: <ok | broken>
67
+
68
+ ### Broken flows discovered
69
+ - <one-line description> — <repro steps>
70
+
71
+ ### Recommended board update
72
+ - <TASK-ID>: review → Validated | review → In progress
73
+ - New tasks to add: <list or none>
74
+ ```
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: risk-check
3
+ description: Detect whether the project is drifting — scope creep, architecture creep, hidden dependencies, missing tests, unclear tasks, plus stale hybrid-pack helper versions (more than two minor versions behind the latest published). Use every few sessions, when the user says "are we on track?", "is this getting out of hand?", or before a demo. The anti-overthinking and anti-feature-creep skill.
4
+ # Generated from .claude/skills/risk-check/SKILL.md — DO NOT EDIT directly
5
+ ---
6
+
7
+ # Skill: risk-check
8
+
9
+ ## Goal
10
+
11
+ Be the brake. Compare the current state of the project to the spec and architecture, and call out where reality is drifting. Recommend concrete cuts.
12
+
13
+ ## Recommended model
14
+
15
+ Opus 4.7 or GPT-5.5.
16
+
17
+ ## Inputs
18
+
19
+ Read these (required):
20
+
21
+ - `.ai/product-spec.md`
22
+ - `.ai/architecture.md`
23
+ - `.ai/board.md`
24
+ - `.ai/decision-log.md` if it exists
25
+ - `.spark/state.json` if it exists
26
+ - `packs/*/pack.toml` if pack state exists and the registry is available
27
+
28
+ Sample reality:
29
+
30
+ - `git log --oneline -30`
31
+ - top-level directory listing
32
+ - list of dependencies in `package.json` / `pyproject.toml` / equivalent
33
+
34
+ ## Rules
35
+
36
+ - Compare **what is in the code now** to **what the spec said**. Highlight gaps in both directions: missing must-haves, plus things built that the spec did not ask for.
37
+ - Treat the spec's non-goals list as a checklist of things that should NOT be present in code. Violations are creep, not features.
38
+ - Recommend cuts, not additions. The default fix is "remove or defer," not "build more."
39
+ - Distinguish **drift** (planned scope grew quietly) from **discovery** (new task properly added to the board). Discovery is fine; silent drift is not.
40
+ - For pack-level drift, inspect `.spark/state.json` when present. For each installed pack, determine its provided capabilities from state or from `packs/<name>/pack.toml`; if none of those capabilities are referenced in `.ai/product-spec.md` or `.ai/architecture.md`, flag it as drift.
41
+ - The pack-level drift recommendation is exactly: **review or revert the pack-install commit via git**. Do not suggest a CLI removal command; v1 has no pack uninstall flow.
42
+ - For each installed pack whose manifest declares `[runtime_package]` (hybrid pack), inspect the consumer project's `package.json` (`dependencies` + `devDependencies`) for the named helper. Compare the installed version against the latest published version on the npm registry (use `bun pm view <pkg> version` or `npm view <pkg> version` via Bash). If the installed version is more than two minor versions behind the latest, flag it under "Stale helper". A `file:` specifier counts as "local dev link" and is NOT stale.
43
+
44
+ ## Checklist
45
+
46
+ - **Scope creep** — features in code that are not in `MVP feature list`, or are in `Non-goals`.
47
+ - **Architecture creep** — services / dependencies / abstractions beyond what `architecture.md` declared.
48
+ - **Pack-level drift** — installed packs whose provided capabilities are not justified by the spec or architecture.
49
+ - **Stale helper** — hybrid packs whose helper package is more than two minor versions behind the latest on npm.
50
+ - **Unclear tasks** — open board tasks without observable acceptance criteria.
51
+ - **Missing tests / verification** — tasks marked `Validated` with no run command or no review.
52
+ - **Hidden dependencies** — packages added not justified by a task or decision.
53
+ - **Stalled tasks** — tasks in `In progress` for more than ~2 sessions with no commits.
54
+
55
+ ## Output format
56
+
57
+ ```md
58
+ ## Pack-level drift
59
+ - no drift detected
60
+ - <pack-name>: provides <capability tag(s)>; none are referenced in `.ai/product-spec.md` or `.ai/architecture.md` — **recommend: review or revert the pack-install commit via git**
61
+
62
+ ## Stale helper
63
+ - no stale helpers
64
+ - <pack-name>: helper `<helper-package>` installed at <installed-version>, latest is <latest-version> — **recommend: `bun update <helper-package>`**
65
+
66
+ ## Risk check
67
+
68
+ ### Scope creep
69
+ - <thing built / in progress> — not in spec / in non-goals — **recommend: cut | defer | keep with decision log entry**
70
+
71
+ ### Architecture creep
72
+ - <new service / dep / abstraction> — **recommend: revert | document in architecture.md**
73
+
74
+ ### Unclear tasks
75
+ - <TASK-ID>: criteria are vague — **recommend: rewrite or send to `/board-review`**
76
+
77
+ ### Hidden dependencies
78
+ - <package> added in <commit> — justification: <none | <task>>
79
+
80
+ ### Stalled tasks
81
+ - <TASK-ID>: in progress since <when> — **recommend: split | unblock | drop**
82
+
83
+ ### Summary
84
+ - Drift severity: low | medium | high
85
+ - Suggested next action: <one line>
86
+ ```
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: sync-board
3
+ description: Update `.ai/board.md` to reflect actual code progress — apply status changes from execution reports, add discovered tasks, and recommend the next batch. Use after `/execute-task`, at the end of a working session, or when the user says "update the board", "sync progress", "what's next?". Do NOT use to create the initial board — that is `/mvp-board`.
4
+ # Generated from .claude/skills/sync-board/SKILL.md — DO NOT EDIT directly
5
+ ---
6
+
7
+ # Skill: sync-board
8
+
9
+ ## Goal
10
+
11
+ Keep `.ai/board.md` in sync with reality. The board is the source of truth — if it drifts from what is actually built, the whole system breaks.
12
+
13
+ ## Recommended model
14
+
15
+ Sonnet 4.6. This is mechanical reconciliation, not planning.
16
+
17
+ ## Inputs
18
+
19
+ Read these (required):
20
+
21
+ - `.ai/board.md`
22
+
23
+ Read if available:
24
+
25
+ - the most recent `/execute-task` report in the conversation
26
+ - `git status` and `git log` (one screenful) to confirm what actually changed
27
+ - `.ai/execution-log.md` if it exists
28
+
29
+ ## Rules
30
+
31
+ - **Trust git, not claims.** If a report says a file changed but git disagrees, flag it and do not advance the task.
32
+ - Never set status to `Validated` directly from execution. `Validated` requires a `/code-review` pass and, for user-facing changes, a `/qa-verify` pass. Update `Validation state` accordingly: `code-reviewed`, `qa-verified`, or `both`.
33
+ - Status flow is: `In progress` → `Needs review` → `Validated`. `Blocked` can come from any state.
34
+ - Never move a task to `Approved for execution`. That is `/board-review`'s job.
35
+ - New work discovered during execution becomes a new task in `Clarifying`, at the bottom of the relevant epic — not a silent edit to an existing task.
36
+ - Tasks the user explicitly cut go in the `Cut from MVP` section with a reason — never delete them.
37
+ - If a task is `Blocked`, record the specific blocker in a `Blocked by:` line.
38
+ - When a PR is opened, update `Linked PR:`. When a preview deploy exists, update `Demo URL:`.
39
+ - Append a one-line entry per state change to `.ai/execution-log.md` (create it if missing).
40
+
41
+ ## Workflow
42
+
43
+ 1. Read the board and the latest execution report.
44
+ 2. Run `git status` and a short `git log` to confirm actual changes.
45
+ 3. For each affected task: update status, append changed-files and verification result.
46
+ 4. Add any follow-up tasks discovered.
47
+ 5. Identify the next recommended task (or batch) based on dependencies.
48
+ 6. Write the updated board and append to the execution log.
49
+
50
+ ## Output format
51
+
52
+ After writing, return:
53
+
54
+ ```md
55
+ ## Board synced
56
+
57
+ ### Status changes
58
+ - <TASK-ID>: <old> → <new>
59
+
60
+ ### Tasks added
61
+ - <NEW-TASK-ID>: <title> (in <EPIC>)
62
+
63
+ ### Blockers
64
+ - <TASK-ID>: <reason>
65
+
66
+ ### Next recommended
67
+ - <TASK-ID> (or batch from `/parallel-execution`)
68
+ - Why now: <one line>
69
+
70
+ ### Drift detected
71
+ - <e.g. "claimed edit to foo.ts but git shows no change"> | none
72
+ ```
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: ux-theme
3
+ description: Define the visual and product direction (vibe, layout, color, typography, component style) before coding starts. Use when the user says "what should this look like?", "pick a theme", "make it feel like Linear/Notion/Vercel", or right before scaffolding UI. Do NOT use for fine-grained component styling — that belongs in execution.
4
+ # Generated from .claude/skills/ux-theme/SKILL.md — DO NOT EDIT directly
5
+ ---
6
+
7
+ # Skill: ux-theme
8
+
9
+ ## Goal
10
+
11
+ Produce `.ai/ux-theme.md` — a concrete visual direction that every executor task can consult. Lovable-style theme control: one clear vibe, one set of patterns, one reference product.
12
+
13
+ ## Recommended model
14
+
15
+ Opus 4.7 or GPT-5.5 for the direction. Sonnet 4.6 can implement against it later.
16
+
17
+ ## Inputs
18
+
19
+ Read these if they exist:
20
+
21
+ - `.ai/product-spec.md`
22
+ - `.ai/architecture.md`
23
+ - `.ai/decision-log.md`
24
+
25
+ ## Rules
26
+
27
+ - Pick **one** vibe. Not "minimal but playful but also enterprise."
28
+ - Name **one** reference product to imitate. "Linear-style productivity SaaS" beats "clean and modern."
29
+ - Give concrete tokens (color names, type scale, spacing) so executors do not invent their own.
30
+ - Define empty / loading / error patterns up front — these are where MVPs feel broken.
31
+ - Do not generate full design files. This is a brief, not Figma.
32
+
33
+ ## Reference directions (pick one or describe a new one)
34
+
35
+ - Linear-style productivity SaaS
36
+ - Notion-like workspace
37
+ - Vercel-like developer tool
38
+ - Stripe-like admin dashboard
39
+ - Arc-like playful consumer app
40
+
41
+ ## Output format
42
+
43
+ Write `.ai/ux-theme.md`:
44
+
45
+ ```md
46
+ # UX Theme — <name>
47
+
48
+ ## Vibe
49
+ <one sentence>
50
+
51
+ ## Reference product
52
+ <one product, with a link or short description of what to copy>
53
+
54
+ ## Layout style
55
+ <sidebar + content / centered single column / dashboard grid / etc.>
56
+
57
+ ## Color direction
58
+ - Background:
59
+ - Surface:
60
+ - Text primary:
61
+ - Text muted:
62
+ - Accent:
63
+ - Danger:
64
+ (prefer Tailwind palette names or hex)
65
+
66
+ ## Typography
67
+ - Display:
68
+ - Body:
69
+ - Mono:
70
+ - Scale: <e.g. 12 / 14 / 16 / 20 / 24 / 32>
71
+
72
+ ## Component style
73
+ - Corners: <radius>
74
+ - Borders: <weight, color>
75
+ - Shadows: <none / soft / layered>
76
+ - Buttons: <filled / outlined / ghost variants>
77
+ - Inputs: <style>
78
+
79
+ ## Patterns
80
+ - Empty state:
81
+ - Loading state:
82
+ - Error state:
83
+ - Table:
84
+ - Card:
85
+ - Dialog / modal:
86
+
87
+ ## Constraints
88
+ - <hard "no" rules, e.g. "no gradients", "no emoji in UI">
89
+ ```
90
+
91
+ After writing, recommend `/mvp-board` next.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeailab/create-spark",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Interactive scaffolder for spark projects with guided pack picker.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -9,16 +9,19 @@
9
9
  "files": [
10
10
  "src",
11
11
  "README.md",
12
- "templates",
13
12
  "packs",
14
- "presets"
13
+ "presets",
14
+ "templates",
15
+ ".claude",
16
+ ".codex",
17
+ "scripts"
15
18
  ],
16
19
  "bin": {
17
20
  "create-spark": "./src/cli.ts"
18
21
  },
19
22
  "dependencies": {
20
- "@forgeailab/spark": "^0.1.2",
21
- "@forgeailab/spark-schema": "^0.1.2",
23
+ "@forgeailab/spark": "^0.1.3",
24
+ "@forgeailab/spark-schema": "^0.1.3",
22
25
  "@clack/prompts": "latest",
23
26
  "citty": "latest",
24
27
  "picocolors": "latest"
@@ -0,0 +1,223 @@
1
+ import { mkdtemp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, relative, resolve, sep } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ parseSkillFrontmatter,
7
+ serializeSkillFrontmatter,
8
+ toCodexFrontmatter,
9
+ } from "@forgeailab/spark-skill-utils";
10
+
11
+ type SkillOutput = {
12
+ name: string;
13
+ content: string;
14
+ };
15
+
16
+ type Diff =
17
+ | { type: "missing"; path: string }
18
+ | { type: "extra"; path: string }
19
+ | { type: "changed"; path: string };
20
+
21
+ type SyncResult = {
22
+ ok: boolean;
23
+ count: number;
24
+ diffs: Diff[];
25
+ };
26
+
27
+ type TreeEntry = {
28
+ type: "dir" | "file";
29
+ content?: string;
30
+ };
31
+
32
+ export function transformSkillMarkdown(markdown: string, skillName: string): string {
33
+ const { frontmatter, body } = parseSkillFrontmatter(markdown);
34
+ const codexFrontmatter = toCodexFrontmatter(frontmatter);
35
+ const outputFrontmatter = serializeSkillFrontmatter(codexFrontmatter, {
36
+ trailingComments: [
37
+ `# Generated from .claude/skills/${skillName}/SKILL.md — DO NOT EDIT directly`,
38
+ ],
39
+ });
40
+
41
+ return `---\n${outputFrontmatter}\n---\n${body}`;
42
+ }
43
+
44
+ async function collectSkillOutputs(root: string): Promise<SkillOutput[]> {
45
+ const sourceRoot = join(root, ".claude", "skills");
46
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
47
+ const outputs: SkillOutput[] = [];
48
+
49
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
50
+ if (!entry.isDirectory()) {
51
+ continue;
52
+ }
53
+
54
+ const skillName = entry.name;
55
+ const sourceFile = join(sourceRoot, skillName, "SKILL.md");
56
+ if (!existsSync(sourceFile)) {
57
+ continue;
58
+ }
59
+
60
+ const source = await readFile(sourceFile, "utf8");
61
+ outputs.push({
62
+ name: skillName,
63
+ content: transformSkillMarkdown(source, skillName),
64
+ });
65
+ }
66
+
67
+ return outputs;
68
+ }
69
+
70
+ async function writeOutputs(targetRoot: string, outputs: SkillOutput[]): Promise<void> {
71
+ await rm(targetRoot, { force: true, recursive: true });
72
+ await mkdir(targetRoot, { recursive: true });
73
+
74
+ for (const output of outputs) {
75
+ const skillDir = join(targetRoot, output.name);
76
+ await mkdir(skillDir, { recursive: true });
77
+ await writeFile(join(skillDir, "SKILL.md"), output.content, "utf8");
78
+ }
79
+ }
80
+
81
+ async function collectTree(root: string): Promise<Map<string, TreeEntry>> {
82
+ const tree = new Map<string, TreeEntry>();
83
+
84
+ if (!existsSync(root)) {
85
+ return tree;
86
+ }
87
+
88
+ async function walk(dir: string): Promise<void> {
89
+ const entries = await readdir(dir, { withFileTypes: true });
90
+
91
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
92
+ const absolutePath = join(dir, entry.name);
93
+ const relativePath = relative(root, absolutePath).split(sep).join("/");
94
+
95
+ if (entry.isDirectory()) {
96
+ tree.set(relativePath, { type: "dir" });
97
+ await walk(absolutePath);
98
+ continue;
99
+ }
100
+
101
+ if (entry.isFile()) {
102
+ tree.set(relativePath, {
103
+ type: "file",
104
+ content: await readFile(absolutePath, "utf8"),
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ await walk(root);
111
+ return tree;
112
+ }
113
+
114
+ async function diffTrees(expectedRoot: string, actualRoot: string): Promise<Diff[]> {
115
+ const expected = await collectTree(expectedRoot);
116
+ const actual = await collectTree(actualRoot);
117
+ const paths = Array.from(new Set([...expected.keys(), ...actual.keys()])).sort();
118
+ const diffs: Diff[] = [];
119
+
120
+ for (const path of paths) {
121
+ const expectedEntry = expected.get(path);
122
+ const actualEntry = actual.get(path);
123
+
124
+ if (!expectedEntry) {
125
+ diffs.push({ type: "extra", path });
126
+ continue;
127
+ }
128
+
129
+ if (!actualEntry) {
130
+ diffs.push({ type: "missing", path });
131
+ continue;
132
+ }
133
+
134
+ if (
135
+ expectedEntry.type !== actualEntry.type ||
136
+ (expectedEntry.type === "file" && expectedEntry.content !== actualEntry.content)
137
+ ) {
138
+ diffs.push({ type: "changed", path });
139
+ }
140
+ }
141
+
142
+ return diffs;
143
+ }
144
+
145
+ export async function syncSkills(
146
+ targetRoot = process.cwd(),
147
+ options: { check?: boolean } = {},
148
+ ): Promise<SyncResult> {
149
+ const root = resolve(targetRoot);
150
+ const targetRootPath = join(root, ".codex", "skills");
151
+ const outputs = await collectSkillOutputs(root);
152
+
153
+ if (!options.check) {
154
+ await writeOutputs(targetRootPath, outputs);
155
+ return { ok: true, count: outputs.length, diffs: [] };
156
+ }
157
+
158
+ const tempRoot = await mkdtemp(join(tmpdir(), "sync-skills-"));
159
+ const expectedRoot = join(tempRoot, "skills");
160
+
161
+ try {
162
+ await writeOutputs(expectedRoot, outputs);
163
+ const diffs = await diffTrees(expectedRoot, targetRootPath);
164
+ return { ok: diffs.length === 0, count: outputs.length, diffs };
165
+ } finally {
166
+ await rm(tempRoot, { force: true, recursive: true });
167
+ }
168
+ }
169
+
170
+ function parseArgs(argv: string[]): { check: boolean; root: string } {
171
+ const positional: string[] = [];
172
+ let check = false;
173
+
174
+ for (const arg of argv) {
175
+ if (arg === "--check") {
176
+ check = true;
177
+ continue;
178
+ }
179
+
180
+ if (arg.startsWith("-")) {
181
+ throw new Error(`Unknown option: ${arg}`);
182
+ }
183
+
184
+ positional.push(arg);
185
+ }
186
+
187
+ if (positional.length > 1) {
188
+ throw new Error("Expected at most one positional argument: target project root");
189
+ }
190
+
191
+ return {
192
+ check,
193
+ root: positional[0] ?? process.cwd(),
194
+ };
195
+ }
196
+
197
+ function printDiffs(diffs: Diff[]): void {
198
+ for (const diff of diffs) {
199
+ console.error(`${diff.type}: ${diff.path}`);
200
+ }
201
+ }
202
+
203
+ if (import.meta.main) {
204
+ try {
205
+ const args = parseArgs(process.argv.slice(2));
206
+ const result = await syncSkills(args.root, { check: args.check });
207
+
208
+ if (!result.ok) {
209
+ console.error(".codex/skills is out of sync. Run bun run scripts/sync-skills.ts.");
210
+ printDiffs(result.diffs);
211
+ process.exit(1);
212
+ }
213
+
214
+ if (args.check) {
215
+ console.log(`.codex/skills is in sync (${result.count} skills).`);
216
+ } else {
217
+ console.log(`Synced ${result.count} skills into .codex/skills.`);
218
+ }
219
+ } catch (error) {
220
+ console.error(error instanceof Error ? error.message : String(error));
221
+ process.exit(1);
222
+ }
223
+ }