@forgeailab/create-spark 0.1.1 → 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.
- package/.claude/skills/architecture-cutline/SKILL.md +96 -0
- package/.claude/skills/board-review/SKILL.md +77 -0
- package/.claude/skills/code-review/SKILL.md +76 -0
- package/.claude/skills/execute-task/SKILL.md +80 -0
- package/.claude/skills/idea-sharpen/SKILL.md +65 -0
- package/.claude/skills/implementation-brief/SKILL.md +87 -0
- package/.claude/skills/mvp-board/SKILL.md +95 -0
- package/.claude/skills/mvp-grill/SKILL.md +60 -0
- package/.claude/skills/mvp-spec/SKILL.md +78 -0
- package/.claude/skills/new-pack/SKILL.md +156 -0
- package/.claude/skills/next-task/SKILL.md +65 -0
- package/.claude/skills/pack-add/SKILL.md +64 -0
- package/.claude/skills/pack-resolve/SKILL.md +67 -0
- package/.claude/skills/parallel-execution/SKILL.md +68 -0
- package/.claude/skills/qa-verify/SKILL.md +77 -0
- package/.claude/skills/risk-check/SKILL.md +88 -0
- package/.claude/skills/sync-board/SKILL.md +76 -0
- package/.claude/skills/ux-theme/SKILL.md +93 -0
- package/.codex/skills/architecture-cutline/SKILL.md +94 -0
- package/.codex/skills/board-review/SKILL.md +75 -0
- package/.codex/skills/code-review/SKILL.md +73 -0
- package/.codex/skills/execute-task/SKILL.md +76 -0
- package/.codex/skills/idea-sharpen/SKILL.md +63 -0
- package/.codex/skills/implementation-brief/SKILL.md +85 -0
- package/.codex/skills/mvp-board/SKILL.md +93 -0
- package/.codex/skills/mvp-grill/SKILL.md +58 -0
- package/.codex/skills/mvp-spec/SKILL.md +76 -0
- package/.codex/skills/new-pack/SKILL.md +153 -0
- package/.codex/skills/next-task/SKILL.md +64 -0
- package/.codex/skills/pack-add/SKILL.md +62 -0
- package/.codex/skills/pack-resolve/SKILL.md +65 -0
- package/.codex/skills/parallel-execution/SKILL.md +66 -0
- package/.codex/skills/qa-verify/SKILL.md +74 -0
- package/.codex/skills/risk-check/SKILL.md +86 -0
- package/.codex/skills/sync-board/SKILL.md +72 -0
- package/.codex/skills/ux-theme/SKILL.md +91 -0
- package/package.json +10 -4
- package/packs/README.md +132 -0
- package/packs/ai-anthropic/files/app/api/ai/route.ts +57 -0
- package/packs/ai-anthropic/files/lib/anthropic.ts +15 -0
- package/packs/ai-anthropic/pack.toml +32 -0
- package/packs/ai-anthropic/skills/ai-feature-patterns/SKILL.md +87 -0
- package/packs/ai-anthropic/tasks.yaml +9 -0
- package/packs/ai-openai/files/app/api/ai-openai/route.ts +55 -0
- package/packs/ai-openai/files/lib/openai.ts +21 -0
- package/packs/ai-openai/pack.toml +30 -0
- package/packs/ai-openai/tasks.yaml +9 -0
- package/packs/analytics-posthog/files/components/PostHogProvider.tsx +19 -0
- package/packs/analytics-posthog/files/lib/posthog/client.ts +20 -0
- package/packs/analytics-posthog/files/lib/posthog/server.ts +24 -0
- package/packs/analytics-posthog/pack.toml +35 -0
- package/packs/analytics-posthog/tasks.yaml +15 -0
- package/packs/auth-better-auth/files/app/(auth)/login/page.tsx +58 -0
- package/packs/auth-better-auth/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth/files/lib/auth.ts +21 -0
- package/packs/auth-better-auth/pack.toml +32 -0
- package/packs/auth-better-auth/tasks.yaml +10 -0
- package/packs/auth-better-auth-pg/files/app/api/auth/[...all]/route.ts +4 -0
- package/packs/auth-better-auth-pg/files/lib/auth.ts +86 -0
- package/packs/auth-better-auth-pg/pack.toml +32 -0
- package/packs/auth-better-auth-pg/tasks.yaml +17 -0
- package/packs/auth-supabase/files/app/(auth)/login/page.tsx +64 -0
- package/packs/auth-supabase/files/app/auth/callback/route.ts +15 -0
- package/packs/auth-supabase/files/middleware.ts +41 -0
- package/packs/auth-supabase/pack.toml +34 -0
- package/packs/auth-supabase/tasks.yaml +10 -0
- package/packs/db-postgres/files/compose/postgres.yml +28 -0
- package/packs/db-postgres/files/docker-compose.include.yml +1 -0
- package/packs/db-postgres/files/docker-compose.yml +6 -0
- package/packs/db-postgres/files/drizzle.config.ts +10 -0
- package/packs/db-postgres/files/lib/db/index.ts +10 -0
- package/packs/db-postgres/files/lib/db/schema.ts +11 -0
- package/packs/db-postgres/pack.toml +53 -0
- package/packs/db-postgres/tasks.yaml +11 -0
- package/packs/db-sqlite/files/drizzle.config.ts +10 -0
- package/packs/db-sqlite/files/lib/db.ts +8 -0
- package/packs/db-sqlite/files/lib/schema.ts +13 -0
- package/packs/db-sqlite/pack.toml +34 -0
- package/packs/db-sqlite/tasks.yaml +6 -0
- package/packs/db-supabase/files/lib/supabase/client.ts +8 -0
- package/packs/db-supabase/files/lib/supabase/server.ts +27 -0
- package/packs/db-supabase/pack.toml +32 -0
- package/packs/db-supabase/skills/supabase-patterns/SKILL.md +82 -0
- package/packs/db-supabase/tasks.yaml +6 -0
- package/packs/deploy-vercel/files/docs/deploy.md +21 -0
- package/packs/deploy-vercel/files/vercel.json +4 -0
- package/packs/deploy-vercel/pack.toml +30 -0
- package/packs/deploy-vercel/tasks.yaml +14 -0
- package/packs/docker-compose-dev/files/.env.docker.example +2 -0
- package/packs/docker-compose-dev/files/compose/redis.yml +17 -0
- package/packs/docker-compose-dev/files/docker-compose.include.yml +1 -0
- package/packs/docker-compose-dev/files/docker-compose.yml +6 -0
- package/packs/docker-compose-dev/pack.toml +38 -0
- package/packs/docker-compose-dev/tasks.yaml +9 -0
- package/packs/email-resend/files/app/api/email/test/route.ts +38 -0
- package/packs/email-resend/files/emails/welcome.tsx +66 -0
- package/packs/email-resend/files/lib/email.ts +40 -0
- package/packs/email-resend/pack.toml +34 -0
- package/packs/email-resend/tasks.yaml +9 -0
- package/packs/example/pack.toml +69 -0
- package/packs/payments-stripe/files/app/api/billing-portal/route.ts +24 -0
- package/packs/payments-stripe/files/app/api/checkout/route.ts +58 -0
- package/packs/payments-stripe/files/app/api/webhooks/stripe/route.ts +84 -0
- package/packs/payments-stripe/files/lib/stripe.ts +60 -0
- package/packs/payments-stripe/pack.toml +49 -0
- package/packs/payments-stripe/skills/stripe-patterns/SKILL.md +93 -0
- package/packs/payments-stripe/tasks.yaml +16 -0
- package/packs/sync-zero/files/components/ZeroProvider.tsx +3 -0
- package/packs/sync-zero/files/compose/zero-cache.yml +26 -0
- package/packs/sync-zero/files/docker-compose.include.yml +1 -0
- package/packs/sync-zero/files/docker-compose.yml +6 -0
- package/packs/sync-zero/files/lib/zero/client.ts +18 -0
- package/packs/sync-zero/files/lib/zero/schema.ts +17 -0
- package/packs/sync-zero/files/zero.config.ts +26 -0
- package/packs/sync-zero/pack.toml +61 -0
- package/packs/sync-zero/skills/zero-patterns/SKILL.md +69 -0
- package/packs/sync-zero/tasks.yaml +16 -0
- package/packs/testing-playwright/files/e2e/example.spec.ts +7 -0
- package/packs/testing-playwright/files/playwright.config.ts +33 -0
- package/packs/testing-playwright/pack.toml +25 -0
- package/packs/testing-playwright/tasks.yaml +9 -0
- package/packs/ui-shadcn/files/app/globals.css +56 -0
- package/packs/ui-shadcn/files/components/ui/button.tsx +47 -0
- package/packs/ui-shadcn/files/components/ui/card.tsx +33 -0
- package/packs/ui-shadcn/files/lib/utils.ts +6 -0
- package/packs/ui-shadcn/files/postcss.config.mjs +7 -0
- package/packs/ui-shadcn/files/tailwind.config.ts +57 -0
- package/packs/ui-shadcn/pack.toml +44 -0
- package/packs/ui-shadcn/skills/shadcn-dashboard-patterns/SKILL.md +85 -0
- package/packs/ui-shadcn/tasks.yaml +6 -0
- package/presets/docs-site.toml +4 -0
- package/presets/internal-tool.toml +4 -0
- package/presets/lean-saas.toml +4 -0
- package/presets/local-ai-mvp.toml +4 -0
- package/presets/saas-classic.toml +4 -0
- package/scripts/sync-skills.ts +223 -0
- package/src/paths.ts +22 -4
- package/templates/README.md +43 -0
- package/templates/astro/README.md +3 -0
- package/templates/astro/template.toml +4 -0
- package/templates/astro-starlight/README.md +3 -0
- package/templates/astro-starlight/template.toml +4 -0
- package/templates/nextjs/.ai/architecture.md +13 -0
- package/templates/nextjs/.ai/board.md +7 -0
- package/templates/nextjs/.ai/product-spec.md +11 -0
- package/templates/nextjs/.claude/skills/.gitkeep +0 -0
- package/templates/nextjs/.codex/skills/.gitkeep +0 -0
- package/templates/nextjs/AGENTS.md +95 -0
- package/templates/nextjs/CLAUDE.md +3 -0
- package/templates/nextjs/README.md +20 -0
- package/templates/nextjs/app/(app)/home/page.tsx +43 -0
- package/templates/nextjs/app/(app)/home/posts-panel.tsx +83 -0
- package/templates/nextjs/app/(app)/layout.tsx +12 -0
- package/templates/nextjs/app/(auth)/login/page.tsx +97 -0
- package/templates/nextjs/app/globals.css +23 -0
- package/templates/nextjs/app/layout.tsx +20 -0
- package/templates/nextjs/app/page.tsx +39 -0
- package/templates/nextjs/lib/auth-placeholder.ts +21 -0
- package/templates/nextjs/lib/posts-placeholder.ts +30 -0
- package/templates/nextjs/next.config.ts +5 -0
- package/templates/nextjs/package.json +26 -0
- package/templates/nextjs/postcss.config.mjs +7 -0
- package/templates/nextjs/spark.config.json +4 -0
- package/templates/nextjs/template.toml +4 -0
- package/templates/nextjs/tsconfig.json +27 -0
- package/templates/nextjs/types/post.ts +13 -0
- package/templates/one/README.md +5 -0
- package/templates/one/template.toml +4 -0
- package/templates/vite-react/README.md +3 -0
- package/templates/vite-react/template.toml +4 -0
|
@@ -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
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
'
|
|
4
|
+
const catalogRootError =
|
|
5
|
+
'Catalog directories (templates/, packs/) not found. Set SPARK_ROOT to point at the spark monorepo, or install via the published @forgeailab/create-spark npm package.';
|
|
6
6
|
|
|
7
7
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -32,6 +32,15 @@ function hasWorkspacePackageJson(dir: string): boolean {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// A catalog root is any directory containing the `templates/` and `packs/`
|
|
36
|
+
// folders we need. Two cases produce one:
|
|
37
|
+
// 1) The monorepo root (dev). Also has a `package.json` with `workspaces`.
|
|
38
|
+
// 2) The published @forgeailab/create-spark package directory (npm install).
|
|
39
|
+
// The publish script bundles templates/ and packs/ into the tarball.
|
|
40
|
+
function isCatalogRoot(dir: string): boolean {
|
|
41
|
+
return isDirectory(join(dir, 'templates')) && isDirectory(join(dir, 'packs'));
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
function isMonorepoRoot(dir: string): boolean {
|
|
36
45
|
return hasWorkspacePackageJson(dir) && isDirectory(join(dir, 'templates'));
|
|
37
46
|
}
|
|
@@ -42,15 +51,24 @@ export function findMonorepoRoot(startDir: string = import.meta.dir): string {
|
|
|
42
51
|
return resolve(override);
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
// Published-package case: this file lives at <package>/src/paths.ts ->
|
|
55
|
+
// walk up one dir to <package>/ which the publish script populates with
|
|
56
|
+
// templates/ and packs/.
|
|
57
|
+
const packageRoot = resolve(import.meta.dir, '..');
|
|
58
|
+
if (isCatalogRoot(packageRoot)) {
|
|
59
|
+
return packageRoot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Dev / monorepo case: walk up from cwd or src looking for the monorepo.
|
|
45
63
|
let current = resolve(startDir);
|
|
46
64
|
while (true) {
|
|
47
|
-
if (isMonorepoRoot(current)) {
|
|
65
|
+
if (isMonorepoRoot(current) || isCatalogRoot(current)) {
|
|
48
66
|
return current;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
const parent = dirname(current);
|
|
52
70
|
if (parent === current) {
|
|
53
|
-
throw new Error(
|
|
71
|
+
throw new Error(catalogRootError);
|
|
54
72
|
}
|
|
55
73
|
current = parent;
|
|
56
74
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Template Authoring
|
|
2
|
+
|
|
3
|
+
Templates live under `templates/<name>/` and register a base scaffold for `create-spark`. Each template directory must contain a `template.toml` manifest and may contain scaffold files when the template is stable.
|
|
4
|
+
|
|
5
|
+
## `template.toml`
|
|
6
|
+
|
|
7
|
+
`template.toml` is parsed with `TemplateManifestSchema` from `packages/spark-schema/src/template.ts`.
|
|
8
|
+
|
|
9
|
+
Required fields:
|
|
10
|
+
|
|
11
|
+
- `name`: lowercase template id matching `^[a-z][a-z0-9-]*$`.
|
|
12
|
+
- `status`: `stable` or `planned`.
|
|
13
|
+
- `provides`: array of template capability tags.
|
|
14
|
+
- `description`: one-line human-readable summary.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
|
|
18
|
+
```toml
|
|
19
|
+
name = "nextjs"
|
|
20
|
+
status = "stable"
|
|
21
|
+
provides = ["server", "static", "spa"]
|
|
22
|
+
description = "Next.js App Router scaffold for TypeScript web apps."
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Template Capabilities
|
|
26
|
+
|
|
27
|
+
Template capabilities describe what the base scaffold runtime provides. They are separate from pack capabilities such as `db`, `auth`, or `payments`.
|
|
28
|
+
|
|
29
|
+
Current accepted template capabilities:
|
|
30
|
+
|
|
31
|
+
- `server`: can run server-side application code.
|
|
32
|
+
- `spa`: supports client-side single-page app behavior.
|
|
33
|
+
- `native`: supports native app targets.
|
|
34
|
+
- `edge`: supports edge runtime deployment.
|
|
35
|
+
- `library`: is intended as a reusable package or library scaffold.
|
|
36
|
+
- `monorepo`: is a multi-package workspace scaffold.
|
|
37
|
+
- `static`: can emit static assets or static pages.
|
|
38
|
+
|
|
39
|
+
Use only these exact values until the schema changes. Planned templates should still include a valid `template.toml` so packs and presets can declare compatibility before the full scaffold ships.
|
|
40
|
+
|
|
41
|
+
## Promoting Planned Templates
|
|
42
|
+
|
|
43
|
+
To promote a template from `planned` to `stable`, add the runnable scaffold files under `templates/<name>/`, update `status = "stable"`, and run the manifest parser against every template. Stable templates should be copyable into a new project and runnable without requiring auth, database, billing, email, UI libraries, or AI SDK setup by default.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Astro Template
|
|
2
|
+
|
|
3
|
+
This template is registered for pack compatibility planning, but it is not implemented yet. The planned scope is an Astro content-site scaffold with server rendering, static output support, MDX-oriented content workflows once the template capability schema supports that tag, and the same AI workflow artifacts as the stable Next.js template.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Astro Starlight Template
|
|
2
|
+
|
|
3
|
+
This template is registered for pack compatibility planning, but it is not implemented yet. The planned scope is a static documentation scaffold based on Astro Starlight, with content-focused defaults and the same AI workflow artifacts as the stable Next.js template.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Operating rules for any agent (Codex, Claude Code, or otherwise) working in this repo. Mirror of the workflow encoded in `.claude/skills/`, so the system stays portable.
|
|
4
|
+
|
|
5
|
+
<!-- SPEC:START -->
|
|
6
|
+
## Spec workflow
|
|
7
|
+
|
|
8
|
+
For proposals/specs/plans, new capabilities, breaking changes, architecture shifts, or behavior-changing perf/security work, keep the source of truth in this project's `.ai/` artifacts first. If the project later adopts formal spec deltas, place them under `docs/spec/changes/<id>-YYYY-MM-DD/`; truth specs after archive live under `docs/spec/specs/`.
|
|
9
|
+
<!-- SPEC:END -->
|
|
10
|
+
|
|
11
|
+
## Operating model
|
|
12
|
+
|
|
13
|
+
This project runs a **planner / implementer / evaluator** loop. The same agent should not plan, build, and grade its own work. Use a strong reasoning model (Opus 4.7 / GPT-5.5) for planning and review; use a faster executor (Sonnet 4.6 / GPT-5 family) for routine implementation.
|
|
14
|
+
|
|
15
|
+
## Source of truth
|
|
16
|
+
|
|
17
|
+
The truth is in repo artifacts, not in chat:
|
|
18
|
+
|
|
19
|
+
- `.ai/product-spec.md` — what the MVP is. Source of acceptance criteria and non-goals.
|
|
20
|
+
- `.ai/architecture.md` — the stack and the cutline (what we are NOT building yet).
|
|
21
|
+
- `.ai/ux-theme.md` — visual direction. Empty / loading / error patterns live here.
|
|
22
|
+
- `.ai/board.md` — every task, with status, dependencies, owners, validation state, linked PR.
|
|
23
|
+
- `.ai/decision-log.md` — locked-in decisions and the reasoning.
|
|
24
|
+
- `.ai/execution-log.md` — append-only history of state changes.
|
|
25
|
+
|
|
26
|
+
If an answer is not in these files, ask the user — do not invent it.
|
|
27
|
+
|
|
28
|
+
## Workflow phases
|
|
29
|
+
|
|
30
|
+
1. **Grill the idea** until it is buildable. Max 5 questions per round. Only questions that change scope or architecture.
|
|
31
|
+
2. **Write the spec.** One MVP slice. Non-goals are mandatory.
|
|
32
|
+
3. **Cut the architecture.** Boring stack > clever stack. Every choice has a "not building yet" sibling.
|
|
33
|
+
4. **Theme the UX.** One vibe, one reference product, concrete tokens.
|
|
34
|
+
5. **Build the board.** Tasks sized for one focused session. Declare `Depends on:` and `Parallel-safe:`.
|
|
35
|
+
6. **Review the board.** Approval gate between planning and execution. No task starts until it is `Approved for execution`.
|
|
36
|
+
7. **Brief each task** before execution. Self-contained, with files-to-inspect, acceptance criteria verbatim, and a verification command.
|
|
37
|
+
8. **Execute one task at a time.** Stay inside the declared file list. No bonus refactors.
|
|
38
|
+
9. **Review independently.** A separate pass checks the diff against acceptance criteria, security, and scope.
|
|
39
|
+
10. **QA-verify** by actually running the app and walking the core user journey.
|
|
40
|
+
11. **Sync the board** from git reality. Trust git, not claims.
|
|
41
|
+
|
|
42
|
+
## Board statuses
|
|
43
|
+
|
|
44
|
+
`Clarifying` → `Approved for planning` → `Approved for execution` → `In progress` → `Needs review` → `Validated`
|
|
45
|
+
|
|
46
|
+
Side states: `Blocked`, `Cut from MVP`.
|
|
47
|
+
|
|
48
|
+
`Validated` requires both a `/code-review` pass and a `/qa-verify` pass for user-facing changes. Execution never grades itself.
|
|
49
|
+
|
|
50
|
+
## Hard rules
|
|
51
|
+
|
|
52
|
+
- Do not edit files outside the current task's declared scope. New discoveries become new tasks in `Clarifying`, not silent edits.
|
|
53
|
+
- Do not pick a stack outside `.ai/architecture.md`. Propose a decision-log update first.
|
|
54
|
+
- Do not mark tasks `Validated` without independent review.
|
|
55
|
+
- Do not move a task to `Approved for execution` from execution skills. Only board-review can.
|
|
56
|
+
- Treat the `Non-goals` section of the spec and the `What we are NOT building yet` section of architecture as a `do-not-build` list. Violations are scope creep, not features.
|
|
57
|
+
- Verification commands must actually run. Type-check passing is not the same as feature working.
|
|
58
|
+
- When `git status` and a self-report disagree, trust git.
|
|
59
|
+
|
|
60
|
+
## Skill / command equivalents
|
|
61
|
+
|
|
62
|
+
Claude Code skills live in `.claude/skills/`. The same operations on Codex should be triggered through the matching workflow names:
|
|
63
|
+
|
|
64
|
+
| Stage | Skill |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| Grill | `mvp-grill`, `idea-sharpen` |
|
|
67
|
+
| Spec | `mvp-spec` |
|
|
68
|
+
| Architecture | `architecture-cutline` |
|
|
69
|
+
| Theme | `ux-theme` |
|
|
70
|
+
| Board | `mvp-board`, `board-review` |
|
|
71
|
+
| Schedule | `parallel-execution`, `next-task` |
|
|
72
|
+
| Execute | `implementation-brief`, `execute-task` |
|
|
73
|
+
| Evaluate | `code-review`, `qa-verify` |
|
|
74
|
+
| Sync | `sync-board` |
|
|
75
|
+
| Watch | `risk-check` |
|
|
76
|
+
|
|
77
|
+
## Model assignment defaults
|
|
78
|
+
|
|
79
|
+
- Planning / reviewing / scoping: **Opus 4.7** or **GPT-5.5**.
|
|
80
|
+
- Routine execution: **Sonnet 4.6** or a GPT-5 family executor.
|
|
81
|
+
- High-risk tasks (auth, payments, migrations, security): planning-quality model for both build and review.
|
|
82
|
+
|
|
83
|
+
## Conventions
|
|
84
|
+
|
|
85
|
+
- Stable task IDs (e.g. `AUTH-001`). Never renumber.
|
|
86
|
+
- Append to `.ai/decision-log.md` whenever a non-obvious choice is made, with: decision, context, alternatives considered, why, risk, revisit condition.
|
|
87
|
+
- Append to `.ai/execution-log.md` one line per state change in `.ai/board.md`.
|
|
88
|
+
- Do not delete board tasks. Move them to `Cut from MVP` with a reason.
|
|
89
|
+
|
|
90
|
+
## Packs
|
|
91
|
+
|
|
92
|
+
Feature packs are installed with the `spark` CLI.
|
|
93
|
+
Use `pack-resolve` to choose the scaffold and pack set, `pack-add` to dry-run
|
|
94
|
+
and install declarative pack changes, then `sync-board` to reconcile `.ai/board.md`
|
|
95
|
+
with the installed capabilities and current repository state.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Next.js Spark Template
|
|
2
|
+
|
|
3
|
+
This is the minimal Next.js 15 + TypeScript scaffold used by `create-spark`.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
Install dependencies, then start the dev server:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun install
|
|
11
|
+
bun dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Next Steps
|
|
15
|
+
|
|
16
|
+
Read `AGENTS.md`, fill in the `.ai/` planning files, and keep `.ai/board.md` current before implementing. Add capabilities only when the board calls for them:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
spark add <pack>
|
|
20
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { getSessionUser } from '@/lib/auth-placeholder';
|
|
3
|
+
import { PostsPanel } from './posts-panel';
|
|
4
|
+
|
|
5
|
+
export default async function AppHome() {
|
|
6
|
+
const user = await getSessionUser();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
10
|
+
<header className="flex items-center justify-between">
|
|
11
|
+
<div>
|
|
12
|
+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
|
13
|
+
Home
|
|
14
|
+
</p>
|
|
15
|
+
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-slate-900">
|
|
16
|
+
{user ? `Hello ${user.name}` : 'Hello, friend'}
|
|
17
|
+
</h1>
|
|
18
|
+
</div>
|
|
19
|
+
{!user && (
|
|
20
|
+
<Link
|
|
21
|
+
href="/login"
|
|
22
|
+
className="rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50"
|
|
23
|
+
>
|
|
24
|
+
Sign in
|
|
25
|
+
</Link>
|
|
26
|
+
)}
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
{!user && (
|
|
30
|
+
<p className="mt-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
|
31
|
+
No session detected. Install the{' '}
|
|
32
|
+
<code className="rounded bg-amber-100 px-1">auth-better-auth</code>{' '}
|
|
33
|
+
pack to wire real authentication.
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
<section className="mt-8">
|
|
38
|
+
<h2 className="text-sm font-semibold text-slate-700">Your posts</h2>
|
|
39
|
+
<PostsPanel />
|
|
40
|
+
</section>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, type FormEvent } from 'react';
|
|
4
|
+
import { usePosts } from '@/lib/posts-placeholder';
|
|
5
|
+
|
|
6
|
+
export function PostsPanel() {
|
|
7
|
+
const { posts, createPost } = usePosts();
|
|
8
|
+
const [title, setTitle] = useState('');
|
|
9
|
+
const [body, setBody] = useState('');
|
|
10
|
+
const [pending, setPending] = useState(false);
|
|
11
|
+
|
|
12
|
+
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
if (!title.trim()) return;
|
|
15
|
+
setPending(true);
|
|
16
|
+
try {
|
|
17
|
+
await createPost({ title: title.trim(), body: body.trim() });
|
|
18
|
+
setTitle('');
|
|
19
|
+
setBody('');
|
|
20
|
+
} finally {
|
|
21
|
+
setPending(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="mt-3 space-y-4">
|
|
27
|
+
<form
|
|
28
|
+
onSubmit={onSubmit}
|
|
29
|
+
className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
30
|
+
>
|
|
31
|
+
<input
|
|
32
|
+
value={title}
|
|
33
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
34
|
+
placeholder="Post title"
|
|
35
|
+
className="block w-full rounded-md border border-slate-300 px-3 py-2 text-sm placeholder:text-slate-400 focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900"
|
|
36
|
+
/>
|
|
37
|
+
<textarea
|
|
38
|
+
value={body}
|
|
39
|
+
onChange={(e) => setBody(e.target.value)}
|
|
40
|
+
placeholder="Write something…"
|
|
41
|
+
rows={2}
|
|
42
|
+
className="mt-2 block w-full rounded-md border border-slate-300 px-3 py-2 text-sm placeholder:text-slate-400 focus:border-slate-900 focus:outline-none focus:ring-1 focus:ring-slate-900"
|
|
43
|
+
/>
|
|
44
|
+
<div className="mt-3 flex items-center justify-end">
|
|
45
|
+
<button
|
|
46
|
+
type="submit"
|
|
47
|
+
disabled={pending || !title.trim()}
|
|
48
|
+
className="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-1.5 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
49
|
+
>
|
|
50
|
+
{pending ? 'Posting…' : 'Post'}
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</form>
|
|
54
|
+
|
|
55
|
+
{posts.length === 0 ? (
|
|
56
|
+
<div className="rounded-xl border border-dashed border-slate-300 bg-white/60 px-4 py-8 text-center text-sm text-slate-500">
|
|
57
|
+
No posts yet. Install the{' '}
|
|
58
|
+
<code className="rounded bg-slate-100 px-1">sync-zero</code> pack to
|
|
59
|
+
persist and sync posts in real time.
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<ul className="space-y-2">
|
|
63
|
+
{posts.map((post) => (
|
|
64
|
+
<li
|
|
65
|
+
key={post.id}
|
|
66
|
+
className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
67
|
+
>
|
|
68
|
+
<h3 className="text-sm font-semibold text-slate-900">
|
|
69
|
+
{post.title}
|
|
70
|
+
</h3>
|
|
71
|
+
{post.body && (
|
|
72
|
+
<p className="mt-1 text-sm text-slate-600">{post.body}</p>
|
|
73
|
+
)}
|
|
74
|
+
<p className="mt-2 text-xs text-slate-400">
|
|
75
|
+
{post.createdAt.toLocaleString()}
|
|
76
|
+
</p>
|
|
77
|
+
</li>
|
|
78
|
+
))}
|
|
79
|
+
</ul>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// Placeholder authed-route layout.
|
|
4
|
+
//
|
|
5
|
+
// The `auth-better-auth` pack will replace this with a server-side guard that
|
|
6
|
+
// calls `getSessionUser()` and `redirect('/login')` when there is no session.
|
|
7
|
+
// Until then this just renders children so the scaffold compiles and runs.
|
|
8
|
+
export default function AppLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen bg-slate-50 text-slate-900">{children}</div>
|
|
11
|
+
);
|
|
12
|
+
}
|