create-cartwright 0.1.0 → 2.0.1

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 (52) hide show
  1. package/README.md +21 -2
  2. package/dist/approve.d.ts +2 -0
  3. package/dist/approve.js +16 -0
  4. package/dist/approve.js.map +1 -0
  5. package/dist/approve.test.d.ts +1 -0
  6. package/dist/approve.test.js +23 -0
  7. package/dist/approve.test.js.map +1 -0
  8. package/dist/brief.d.ts +26 -0
  9. package/dist/brief.js +25 -0
  10. package/dist/brief.js.map +1 -0
  11. package/dist/brief.test.d.ts +1 -0
  12. package/dist/brief.test.js +23 -0
  13. package/dist/brief.test.js.map +1 -0
  14. package/dist/generate/index.d.ts +4 -0
  15. package/dist/generate/index.js +42 -0
  16. package/dist/generate/index.js.map +1 -0
  17. package/dist/generate/index.test.d.ts +1 -0
  18. package/dist/generate/index.test.js +47 -0
  19. package/dist/generate/index.test.js.map +1 -0
  20. package/dist/index.js +122 -82
  21. package/dist/index.js.map +1 -1
  22. package/dist/inject.d.ts +2 -0
  23. package/dist/inject.js +21 -0
  24. package/dist/inject.js.map +1 -0
  25. package/dist/inject.test.d.ts +1 -0
  26. package/dist/inject.test.js +37 -0
  27. package/dist/inject.test.js.map +1 -0
  28. package/dist/interview.d.ts +8 -0
  29. package/dist/interview.js +73 -0
  30. package/dist/interview.js.map +1 -0
  31. package/dist/interview.test.d.ts +1 -0
  32. package/dist/interview.test.js +50 -0
  33. package/dist/interview.test.js.map +1 -0
  34. package/dist/key-step.d.ts +12 -0
  35. package/dist/key-step.js +22 -0
  36. package/dist/key-step.js.map +1 -0
  37. package/dist/key-step.test.d.ts +1 -0
  38. package/dist/key-step.test.js +56 -0
  39. package/dist/key-step.test.js.map +1 -0
  40. package/dist/llm.d.ts +2 -0
  41. package/dist/llm.js +51 -0
  42. package/dist/llm.js.map +1 -0
  43. package/dist/llm.test.d.ts +1 -0
  44. package/dist/llm.test.js +32 -0
  45. package/dist/llm.test.js.map +1 -0
  46. package/dist/scaffold.d.ts +48 -0
  47. package/dist/scaffold.js +171 -0
  48. package/dist/scaffold.js.map +1 -0
  49. package/dist/scaffold.test.d.ts +1 -0
  50. package/dist/scaffold.test.js +109 -0
  51. package/dist/scaffold.test.js.map +1 -0
  52. package/package.json +5 -3
package/README.md CHANGED
@@ -25,15 +25,34 @@ npx create-cartwright@latest my-shop --yes --db=turso --ai
25
25
  | `--yes`, `-y` | false | Skip prompts, use defaults |
26
26
  | `--db=<turso\|postgres\|sqlite>` | (prompt) | Database choice — drives next-steps guidance |
27
27
  | `--ai` / `--no-ai` | (prompt) | Enable / disable the AI commerce features hint |
28
- | `--ref=<tag\|branch>` | `v0.1.0-beta` | Template ref on `cartwright-template` mirror |
28
+ | `--ref=<stable\|next\|tag\|branch>` | `stable` | Template channel (see below) |
29
29
  | `--pm=<pnpm\|npm\|yarn\|bun>` | auto-detect | Package manager for install |
30
30
  | `--no-install` | false | Skip dependency install |
31
31
  | `--no-git` | false | Skip `git init` + initial commit |
32
32
 
33
+ ## Template channels
34
+
35
+ | Channel | What it is | When to use |
36
+ |---|---|---|
37
+ | `stable` (default) | Latest tagged release of the template. Battle-tested across the maintainer's canary deploys before tagging. | Production scaffolds. New shops. |
38
+ | `next` | Bleeding-edge: the `next` branch on `cartwright-template`, updated on every push to the template's source repo. | Trying features that haven't been cut into a stable release yet. Not for production. |
39
+ | `vX.Y.Z` (any tag) | Pin to a specific historical release. | Reproducing a known-good scaffold. |
40
+ | `<branch>` (any branch) | Pin to a branch on the mirror. | Power-user experimentation. |
41
+
42
+ Examples:
43
+
44
+ ```bash
45
+ npx create-cartwright@latest my-shop # → stable (default)
46
+ npx create-cartwright@latest my-shop --ref next # → bleeding-edge
47
+ npx create-cartwright@latest my-shop --ref v0.1.0-beta # → pin to a tag
48
+ ```
49
+
50
+ The spinner shows the channel and the resolved ref so you can see exactly what you pulled — useful when reporting issues.
51
+
33
52
  ## What it does
34
53
 
35
54
  1. Three prompts (project name, database, AI features) — skippable with `--yes`.
36
- 2. Downloads a sanitised snapshot from [`cartwright-template`](https://github.com/Teloz1870/cartwright-template) at the pinned `--ref` (default `v0.1.0-beta`).
55
+ 2. Downloads a sanitised snapshot from [`cartwright-template`](https://github.com/Teloz1870/cartwright-template) at the resolved `--ref` channel (default `stable`).
37
56
  3. Generates a random 32-byte `AUTH_SECRET` and writes `.env.local`.
38
57
  4. Patches `brand.config.ts` — `storeName` (Title Case of project name) + `storeSlug` (kebab-case).
39
58
  5. Optional: `git init` + initial commit.
@@ -0,0 +1,2 @@
1
+ import { type ShopBrief } from "./brief";
2
+ export declare function summarizeBuild(brief: ShopBrief): string;
@@ -0,0 +1,16 @@
1
+ import pc from "picocolors";
2
+ export function summarizeBuild(brief) {
3
+ const lines = [
4
+ pc.bold(`${brief.storeName} (${brief.slug})`),
5
+ pc.dim(brief.tagline),
6
+ "",
7
+ `${pc.bold("Sælger:")} ${brief.sells}`,
8
+ `${pc.bold("Målgruppe:")} ${brief.audience}`,
9
+ `${pc.bold("Tone:")} ${brief.tone}`,
10
+ "",
11
+ `${pc.bold("Katalog:")} ${brief.categories.length} kategorier, ${brief.products.length} produkter`,
12
+ `${pc.bold("Palette:")} ${brief.palette.primary} (Primær) / ${brief.palette.background} (Baggrund)`,
13
+ ];
14
+ return lines.join("\n");
15
+ }
16
+ //# sourceMappingURL=approve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approve.js","sourceRoot":"","sources":["../src/approve.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAG5B,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC7C,MAAM,KAAK,GAAG;QACZ,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC;QAC7C,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;QACrB,EAAE;QACF,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE;QACtC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE;QAC5C,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE;QACnC,EAAE;QACF,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,gBAAgB,KAAK,CAAC,QAAQ,CAAC,MAAM,YAAY;QAClG,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,eAAe,KAAK,CAAC,OAAO,CAAC,UAAU,aAAa;KACpG,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { summarizeBuild } from "./approve";
3
+ describe("summarizeBuild", () => {
4
+ it("formaterer brief pænt til clack note", () => {
5
+ const brief = {
6
+ storeName: "Test Shop",
7
+ slug: "test-shop",
8
+ tagline: "Test",
9
+ sells: "Testing",
10
+ audience: "Testers",
11
+ tone: "Fun",
12
+ country: "DK",
13
+ currency: "DKK",
14
+ palette: { primary: "#000000", background: "#ffffff" },
15
+ categories: [{ name: "Cats", slug: "cats" }],
16
+ products: [{ name: "Cat 1", categorySlug: "cats", priceMinor: 1000, blurb: "A cat" }]
17
+ };
18
+ const lines = summarizeBuild(brief);
19
+ expect(lines).toContain("Test Shop (test-shop)");
20
+ expect(lines).toContain("1 kategorier, 1 produkter");
21
+ });
22
+ });
23
+ //# sourceMappingURL=approve.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approve.test.js","sourceRoot":"","sources":["../src/approve.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAc;YACvB,SAAS,EAAE,WAAW;YACtB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,MAAM;YACf,KAAK,EAAE,SAAS;YAChB,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE;YACtD,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;YAC5C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;SACtF,CAAC;QAEF,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QACjD,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ export declare const shopBriefSchema: z.ZodObject<{
3
+ storeName: z.ZodString;
4
+ slug: z.ZodString;
5
+ tagline: z.ZodString;
6
+ sells: z.ZodString;
7
+ audience: z.ZodString;
8
+ tone: z.ZodString;
9
+ country: z.ZodString;
10
+ currency: z.ZodString;
11
+ palette: z.ZodObject<{
12
+ primary: z.ZodString;
13
+ background: z.ZodString;
14
+ }, z.core.$strip>;
15
+ categories: z.ZodArray<z.ZodObject<{
16
+ name: z.ZodString;
17
+ slug: z.ZodString;
18
+ }, z.core.$strip>>;
19
+ products: z.ZodArray<z.ZodObject<{
20
+ name: z.ZodString;
21
+ categorySlug: z.ZodString;
22
+ priceMinor: z.ZodNumber;
23
+ blurb: z.ZodString;
24
+ }, z.core.$strip>>;
25
+ }, z.core.$strip>;
26
+ export type ShopBrief = z.infer<typeof shopBriefSchema>;
package/dist/brief.js ADDED
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ const slug = z.string().regex(/^[a-z][a-z0-9-]*$/);
3
+ const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/);
4
+ export const shopBriefSchema = z.object({
5
+ storeName: z.string().min(1),
6
+ slug,
7
+ tagline: z.string().min(1),
8
+ sells: z.string().min(1),
9
+ audience: z.string().min(1),
10
+ tone: z.string().min(1),
11
+ country: z.string().regex(/^[A-Z]{2}$/),
12
+ currency: z.string().regex(/^[A-Z]{3}$/),
13
+ palette: z.object({ primary: hex, background: hex }),
14
+ categories: z.array(z.object({ name: z.string().min(1), slug })).min(1).max(8),
15
+ products: z
16
+ .array(z.object({
17
+ name: z.string().min(1),
18
+ categorySlug: z.string().min(1),
19
+ priceMinor: z.number().int().positive(),
20
+ blurb: z.string().min(1),
21
+ }))
22
+ .min(1)
23
+ .max(40),
24
+ });
25
+ //# sourceMappingURL=brief.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brief.js","sourceRoot":"","sources":["../src/brief.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;AACnD,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAElD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,IAAI;IACJ,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC;IACvC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC;IACxC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;IACpD,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9E,QAAQ,EAAE,CAAC;SACR,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QACvC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KACzB,CAAC,CACH;SACA,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;CACX,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { shopBriefSchema } from "./brief";
3
+ const valid = {
4
+ storeName: "Nordkaffe", slug: "nordkaffe", tagline: "Specialkaffe fra Aarhus",
5
+ sells: "Ristet specialkaffe og bryggeudstyr", audience: "Hjemme-baristaer",
6
+ tone: "Afslappet og kyndig", country: "DK", currency: "DKK",
7
+ palette: { primary: "#3b2417", background: "#faf6f0" },
8
+ categories: [{ name: "Bønner", slug: "bonner" }],
9
+ products: [{ name: "Etiopien Yirgacheffe", categorySlug: "bonner", priceMinor: 9500, blurb: "Floral og lys." }],
10
+ };
11
+ describe("shopBriefSchema", () => {
12
+ it("accepterer et gyldigt brief", () => {
13
+ expect(shopBriefSchema.safeParse(valid).success).toBe(true);
14
+ });
15
+ it("afviser ugyldig hex-farve", () => {
16
+ const bad = { ...valid, palette: { ...valid.palette, primary: "brun" } };
17
+ expect(shopBriefSchema.safeParse(bad).success).toBe(false);
18
+ });
19
+ it("afviser tomt produkt-array", () => {
20
+ expect(shopBriefSchema.safeParse({ ...valid, products: [] }).success).toBe(false);
21
+ });
22
+ });
23
+ //# sourceMappingURL=brief.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brief.test.js","sourceRoot":"","sources":["../src/brief.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,KAAK,GAAG;IACZ,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,yBAAyB;IAC7E,KAAK,EAAE,qCAAqC,EAAE,QAAQ,EAAE,kBAAkB;IAC1E,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK;IAC3D,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE;IACtD,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAChD,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAChH,CAAC;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACzE,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type ShopBrief } from "../brief";
2
+ export declare function generateThemeCss(brief: ShopBrief): string;
3
+ export declare function generatePromptModule(brief: ShopBrief): string;
4
+ export declare function generateSeedData(brief: ShopBrief): string;
@@ -0,0 +1,42 @@
1
+ export function generateThemeCss(brief) {
2
+ return `/**
3
+ * Theme: ${brief.storeName}
4
+ * Generated by create-cartwright
5
+ */
6
+ :root {
7
+ --color-accent: ${brief.palette.primary};
8
+ --color-cream: ${brief.palette.background};
9
+ --color-sand: ${brief.palette.background}; /* Fallback until more colors requested */
10
+ --color-ink: #1a1a1a;
11
+ --color-muted: #726d62;
12
+ --color-success: ${brief.palette.primary};
13
+ }
14
+ `;
15
+ }
16
+ export function generatePromptModule(brief) {
17
+ return `/**
18
+ * AI System Prompt: ${brief.storeName}
19
+ * Generated by create-cartwright
20
+ */
21
+ export const systemPrompt = \`
22
+ Du er AI-assistent for ${brief.storeName}.
23
+ Tagline: ${brief.tagline}
24
+ Vi sælger: ${brief.sells}
25
+ Målgruppe: ${brief.audience}
26
+ Tone of voice: ${brief.tone}
27
+ \`;
28
+ `;
29
+ }
30
+ export function generateSeedData(brief) {
31
+ return `/**
32
+ * Seed data: ${brief.storeName}
33
+ * Generated by create-cartwright
34
+ */
35
+ export const seedData = {
36
+ categories: ${JSON.stringify(brief.categories, null, 2)},
37
+ products: ${JSON.stringify(brief.products, null, 2)},
38
+ pages: []
39
+ };
40
+ `;
41
+ }
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/generate/index.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAAC,KAAgB;IAC/C,OAAO;YACG,KAAK,CAAC,SAAS;;;;oBAIP,KAAK,CAAC,OAAO,CAAC,OAAO;mBACtB,KAAK,CAAC,OAAO,CAAC,UAAU;kBACzB,KAAK,CAAC,OAAO,CAAC,UAAU;;;qBAGrB,KAAK,CAAC,OAAO,CAAC,OAAO;;CAEzC,CAAC;AACF,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,KAAgB;IACnD,OAAO;uBACc,KAAK,CAAC,SAAS;;;;yBAIb,KAAK,CAAC,SAAS;WAC7B,KAAK,CAAC,OAAO;aACX,KAAK,CAAC,KAAK;aACX,KAAK,CAAC,QAAQ;iBACV,KAAK,CAAC,IAAI;;CAE1B,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAgB;IAC/C,OAAO;gBACO,KAAK,CAAC,SAAS;;;;gBAIf,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;cAC3C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;;;CAGpD,CAAC;AACF,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { generateThemeCss, generatePromptModule } from "./index";
3
+ const mockBrief = {
4
+ storeName: "Test",
5
+ slug: "test",
6
+ tagline: "Test tag",
7
+ sells: "Ting",
8
+ audience: "Folk",
9
+ tone: "Sjov",
10
+ country: "DK",
11
+ currency: "DKK",
12
+ palette: { primary: "#112233", background: "#ffeedd" },
13
+ categories: [],
14
+ products: []
15
+ };
16
+ describe("generateThemeCss", () => {
17
+ it("laver css fil med palette", () => {
18
+ const css = generateThemeCss(mockBrief);
19
+ expect(css).toContain("--color-accent: #112233;");
20
+ expect(css).toContain("--color-cream: #ffeedd;");
21
+ expect(css).toContain("--color-sand: #ffeedd;"); // For nu bare bg
22
+ });
23
+ });
24
+ describe("generatePromptModule", () => {
25
+ it("laver ts modul med system prompt", () => {
26
+ const ts = generatePromptModule(mockBrief);
27
+ expect(ts).toContain("export const systemPrompt");
28
+ expect(ts).toContain("Test tag");
29
+ expect(ts).toContain("Sjov");
30
+ });
31
+ });
32
+ import { generateSeedData } from "./index";
33
+ describe("generateSeedData", () => {
34
+ it("laver ts modul med categories og products", () => {
35
+ const brief = {
36
+ ...mockBrief,
37
+ categories: [{ name: "Kaffe", slug: "kaffe" }],
38
+ products: [{ name: "Bønne", categorySlug: "kaffe", priceMinor: 1000, blurb: "God" }]
39
+ };
40
+ const ts = generateSeedData(brief);
41
+ expect(ts).toContain("export const seedData = {");
42
+ expect(ts).toContain("Kaffe");
43
+ expect(ts).toContain("Bønne");
44
+ expect(ts).toContain("1000");
45
+ });
46
+ });
47
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/generate/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGjE,MAAM,SAAS,GAAc;IAC3B,SAAS,EAAE,MAAM;IACjB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,UAAU;IACnB,KAAK,EAAE,MAAM;IACb,QAAQ,EAAE,MAAM;IAChB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,KAAK;IACf,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE;IACtD,UAAU,EAAE,EAAE;IACd,QAAQ,EAAE,EAAE;CACb,CAAC;AAEF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC;QACjD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC,CAAC,iBAAiB;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,EAAE,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;QAC3C,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QAClD,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,KAAK,GAAc;YACvB,GAAG,SAAS;YACZ,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YAC9C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;SACrF,CAAC;QACF,MAAM,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QACnC,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QAClD,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/dist/index.js CHANGED
@@ -5,34 +5,52 @@
5
5
  * Usage:
6
6
  * npx create-cartwright@latest [name] [--yes]
7
7
  * [--db=turso|postgres|sqlite] [--ai|--no-ai]
8
- * [--ref=<tag-or-branch>]
8
+ * [--ref=stable|next|<tag-or-branch>]
9
+ * [--template=website-corporate|coffee|sunglasses|agent-marketplace|generic]
9
10
  * [--pm=pnpm|npm|yarn|bun]
10
11
  * [--no-install] [--no-git]
11
12
  *
13
+ * Channels:
14
+ * --ref stable (default) → latest tagged template release
15
+ * --ref next → bleeding-edge main branch from cartwright-private
16
+ * --ref vX.Y.Z → pin to a specific historical tag
17
+ *
18
+ * Templates (sets brand.mode + brand.features defaults in brand.config.ts):
19
+ * --template generic (default) → webshop mode, no A2A
20
+ * --template website-corporate → website mode (no shop catalogue)
21
+ * --template coffee → webshop mode, coffee seed data
22
+ * --template sunglasses → webshop mode, legacy eyewear fields
23
+ * --template agent-marketplace → A2A mode, no shop GUI, A-JWT endpoints on
24
+ *
12
25
  * Pulls a sanitised snapshot of cartwright-template via giget, generates a
13
26
  * fresh AUTH_SECRET, injects the project name into brand.config.ts, and
14
27
  * optionally installs deps + inits git.
15
28
  */
16
- import { intro, outro, text, select, confirm, cancel, isCancel, spinner, note, } from "@clack/prompts";
29
+ import { intro, outro, text, select, confirm, cancel, isCancel, spinner, note, password, } from "@clack/prompts";
17
30
  import { downloadTemplate } from "giget";
18
31
  import pc from "picocolors";
19
32
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
33
  import { join, resolve } from "node:path";
21
- import { execSync } from "node:child_process";
22
- import { randomBytes } from "node:crypto";
23
34
  import { parseArgs } from "node:util";
24
35
  const TEMPLATE_REPO = "github:Teloz1870/cartwright-template";
25
- const DEFAULT_REF = "v0.1.0-beta";
26
- function detectPackageManager() {
27
- const ua = process.env.npm_config_user_agent ?? "";
28
- if (ua.startsWith("pnpm"))
29
- return "pnpm";
30
- if (ua.startsWith("yarn"))
31
- return "yarn";
32
- if (ua.startsWith("bun"))
33
- return "bun";
34
- return "npm";
35
- }
36
+ // Default channel resolves to the latest tag mirrored from cartwright-private.
37
+ // Bump together with a Changeset whenever a new template tag goes out —
38
+ // .github/workflows/bump-template-ref.yml does this automatically by opening
39
+ // a PR when it sees a newer tag on the public mirror.
40
+ const DEFAULT_REF = "v0.2.0";
41
+ // Channel aliases the user can pass via --ref.
42
+ // stable → DEFAULT_REF (latest tag — what default `npx create-cartwright` uses)
43
+ // next → bleeding-edge branch on the mirror, updated on every push to
44
+ // cartwright-private/main. Not recommended for production scaffolds.
45
+ const REF_ALIASES = {
46
+ stable: DEFAULT_REF,
47
+ next: "next",
48
+ };
49
+ import { TEMPLATE_SLUGS, detectPackageManager, generateAuthSecret, patchEnvLocal, patchBrandConfigContent, patchBrandConfigForTemplate, isTemplateSlug, tryGitInit, tryInstall, databaseNote } from "./scaffold";
50
+ import { resolveKeyMode } from "./key-step";
51
+ import { runInterview } from "./interview";
52
+ import { summarizeBuild } from "./approve";
53
+ import { injectBriefFiles } from "./inject";
36
54
  function exitOnCancel(value) {
37
55
  if (isCancel(value)) {
38
56
  cancel("Cancelled.");
@@ -40,72 +58,28 @@ function exitOnCancel(value) {
40
58
  }
41
59
  return value;
42
60
  }
43
- function generateAuthSecret() {
44
- return randomBytes(32).toString("hex");
45
- }
46
- function patchEnvLocal(targetDir, authSecret) {
47
- const envExamplePath = join(targetDir, ".env.example");
48
- if (!existsSync(envExamplePath))
49
- return;
50
- const example = readFileSync(envExamplePath, "utf8");
51
- const patched = example.replace(/^AUTH_SECRET=.*/m, `AUTH_SECRET="${authSecret}"`);
52
- writeFileSync(join(targetDir, ".env.local"), patched);
53
- }
54
61
  function patchBrandConfig(targetDir, projectName) {
55
62
  const path = join(targetDir, "brand.config.ts");
56
63
  if (!existsSync(path))
57
64
  return;
58
65
  const original = readFileSync(path, "utf8");
59
- const titled = projectName
60
- .split("-")
61
- .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
62
- .join(" ");
63
- // Replace the first storeName + storeSlug occurrences in the brand object.
64
- const patched = original
65
- .replace(/storeName:\s*"[^"]*"/, `storeName: "${titled}"`)
66
- .replace(/storeSlug:\s*"[^"]*"/, `storeSlug: "${projectName}"`);
66
+ const patched = patchBrandConfigContent(original, projectName);
67
67
  if (patched !== original)
68
68
  writeFileSync(path, patched);
69
69
  }
70
- function tryGitInit(targetDir) {
71
- try {
72
- execSync("git init -q && git add -A && git commit -q -m 'feat: initial commit from create-cartwright'", { cwd: targetDir, stdio: "ignore" });
73
- return true;
74
- }
75
- catch {
76
- return false;
77
- }
78
- }
79
- function tryInstall(targetDir, pm) {
80
- try {
81
- execSync(`${pm} install`, { cwd: targetDir, stdio: "ignore" });
82
- return true;
83
- }
84
- catch {
85
- return false;
86
- }
87
- }
88
- function databaseNote(db) {
89
- switch (db) {
90
- case "turso":
91
- return [
92
- pc.bold("Turso setup (production):"),
93
- " turso db create my-shop-db",
94
- " turso db tokens create my-shop-db",
95
- pc.dim(" Set TURSO_DATABASE_URL + TURSO_AUTH_TOKEN in Vercel."),
96
- ].join("\n");
97
- case "postgres":
98
- return [
99
- pc.bold("Postgres setup:"),
100
- " Update DATABASE_URL in .env.local to your Postgres URL.",
101
- pc.dim(" Prisma schema currently uses 'sqlite' provider — switch to 'postgresql' in prisma/schema.prisma and run a fresh migration."),
102
- ].join("\n");
103
- case "sqlite":
104
- return [
105
- pc.bold("SQLite (local only):"),
106
- " No extra setup. dev.db will be created on first migration.",
107
- ].join("\n");
108
- }
70
+ /**
71
+ * Apply per-template defaults to brand.config.ts. Called after the basic
72
+ * name/slug patch. Idempotent safe to call even if the template fields
73
+ * already match (the regex replacements no-op).
74
+ */
75
+ function applyTemplateDefaults(targetDir, template) {
76
+ const path = join(targetDir, "brand.config.ts");
77
+ if (!existsSync(path))
78
+ return;
79
+ const original = readFileSync(path, "utf8");
80
+ const patched = patchBrandConfigForTemplate(original, template);
81
+ if (patched !== original)
82
+ writeFileSync(path, patched);
109
83
  }
110
84
  async function run() {
111
85
  const { values, positionals } = parseArgs({
@@ -115,13 +89,25 @@ async function run() {
115
89
  db: { type: "string" },
116
90
  ai: { type: "boolean" },
117
91
  "no-ai": { type: "boolean" },
92
+ "ai-gen": { type: "boolean" },
118
93
  ref: { type: "string" },
119
94
  pm: { type: "string" },
95
+ template: { type: "string" },
120
96
  "no-install": { type: "boolean" },
121
97
  "no-git": { type: "boolean" },
122
98
  },
123
99
  });
124
- intro(`${pc.bgYellow(pc.black(" create-cartwright "))} ${pc.dim("v0.1.0-beta")}`);
100
+ // Validate --template if provided. Default is "generic" (full webshop
101
+ // scaffold matching pre-Phase-2 behaviour).
102
+ let templateSlug = "generic";
103
+ if (values.template !== undefined) {
104
+ if (!isTemplateSlug(values.template)) {
105
+ console.error(pc.red(`Invalid --template "${values.template}". Choose one of: ${TEMPLATE_SLUGS.join(", ")}`));
106
+ process.exit(1);
107
+ }
108
+ templateSlug = values.template;
109
+ }
110
+ intro(`${pc.bgYellow(pc.black(" create-cartwright "))} ${pc.dim("v2.0.0-beta")}`);
125
111
  // ── Project name ────────────────────────────────────────────────────────
126
112
  const defaultName = positionals[0] ?? "my-cartwright-shop";
127
113
  const projectName = values.yes
@@ -166,27 +152,65 @@ async function run() {
166
152
  message: "Include AI commerce features? (requires Anthropic + Gemini API keys)",
167
153
  initialValue: true,
168
154
  })));
155
+ // ── AI Generation (V2) ───────────────────────────────────────────────────
156
+ let generatedBrief = undefined;
157
+ let storeSlugOverride = undefined;
158
+ let storeNameOverride = undefined;
159
+ const useAiGen = values["ai-gen"] ?? (values.yes ? false : exitOnCancel(await confirm({
160
+ message: "Vil du prøve den nye AI-scaffolder (v2)? (Kræver Gemini API Key)",
161
+ initialValue: true,
162
+ })));
163
+ if (useAiGen) {
164
+ const keyMode = await resolveKeyMode({
165
+ getEnvKey: () => process.env.GEMINI_API_KEY,
166
+ promptKey: async () => exitOnCancel(await password({ message: "Indtast Gemini API Key:" })),
167
+ confirmManual: async () => exitOnCancel(await confirm({ message: "Key fejlede. Fortsæt med manuel scaffold (v1)?", initialValue: true }))
168
+ });
169
+ if (keyMode.type === "key") {
170
+ const initialPrompt = exitOnCancel(await text({ message: "Hvad slags butik vil du bygge?" }));
171
+ generatedBrief = await runInterview({
172
+ apiKey: keyMode.key,
173
+ initialPrompt,
174
+ askUser: async (q) => exitOnCancel(await text({ message: pc.cyan("AI:") + " " + q })),
175
+ logMsg: (msg) => console.log(pc.dim(msg))
176
+ });
177
+ console.log("\n" + summarizeBuild(generatedBrief) + "\n");
178
+ const ok = exitOnCancel(await confirm({ message: "Ser dette rigtigt ud? (Klar til at bygge)", initialValue: true }));
179
+ if (!ok) {
180
+ cancel("Afbrudt af bruger.");
181
+ process.exit(0);
182
+ }
183
+ storeSlugOverride = generatedBrief.slug;
184
+ storeNameOverride = generatedBrief.storeName;
185
+ }
186
+ }
169
187
  // ── Tooling defaults ────────────────────────────────────────────────────
170
188
  const detected = detectPackageManager();
171
189
  const packageManager = values.pm ?? detected;
172
190
  const installDeps = !values["no-install"];
173
191
  const initGit = !values["no-git"];
174
- const templateRef = values.ref ?? DEFAULT_REF;
192
+ const requestedRef = values.ref ?? "stable";
193
+ const templateRef = REF_ALIASES[requestedRef] ?? requestedRef;
175
194
  // ── Pre-flight ──────────────────────────────────────────────────────────
176
- const targetDir = resolve(process.cwd(), projectName);
195
+ const finalSlug = storeSlugOverride ?? projectName;
196
+ const finalProjectName = storeNameOverride ?? projectName;
197
+ const targetDir = resolve(process.cwd(), finalSlug);
177
198
  if (existsSync(targetDir)) {
178
- cancel(`Directory "${projectName}" already exists.`);
199
+ cancel(`Directory "${finalSlug}" already exists.`);
179
200
  process.exit(1);
180
201
  }
181
202
  // ── Template fetch ──────────────────────────────────────────────────────
182
203
  const fetchSpinner = spinner();
183
- fetchSpinner.start(`Fetching cartwright template (${templateRef})…`);
204
+ const refDisplay = requestedRef === templateRef
205
+ ? templateRef
206
+ : `${requestedRef} → ${templateRef}`;
207
+ fetchSpinner.start(`Fetching cartwright template (${refDisplay})…`);
184
208
  try {
185
209
  await downloadTemplate(`${TEMPLATE_REPO}#${templateRef}`, {
186
210
  dir: targetDir,
187
211
  force: false,
188
212
  });
189
- fetchSpinner.stop(pc.green(`Template downloaded (${templateRef}).`));
213
+ fetchSpinner.stop(pc.green(`Template downloaded (${refDisplay}).`));
190
214
  }
191
215
  catch (err) {
192
216
  fetchSpinner.stop(pc.red("Template fetch failed."));
@@ -196,7 +220,22 @@ async function run() {
196
220
  // ── Customise the scaffold ──────────────────────────────────────────────
197
221
  const authSecret = generateAuthSecret();
198
222
  patchEnvLocal(targetDir, authSecret);
199
- patchBrandConfig(targetDir, projectName);
223
+ if (generatedBrief) {
224
+ injectBriefFiles(targetDir, generatedBrief);
225
+ // patchBrandConfig vil nu bruge briefets værdier (som vi gav videre via finalSlug)
226
+ patchBrandConfig(targetDir, finalSlug);
227
+ }
228
+ else {
229
+ patchBrandConfig(targetDir, finalProjectName);
230
+ }
231
+ // Apply per-template defaults (mode, features, industryTemplate) AFTER
232
+ // the basic name/slug patch so the regex-based replacements act on a
233
+ // known shape. Always run — for `--template generic` (the default) the
234
+ // patches are no-ops on a generic-defaulted brand.config.
235
+ applyTemplateDefaults(targetDir, templateSlug);
236
+ if (templateSlug !== "generic") {
237
+ note(`Template: ${pc.bold(templateSlug)} — applied mode + features defaults to brand.config.ts`, "info");
238
+ }
200
239
  // ── Git init ────────────────────────────────────────────────────────────
201
240
  if (initGit) {
202
241
  const gitOk = tryGitInit(targetDir);
@@ -223,12 +262,13 @@ async function run() {
223
262
  : "";
224
263
  const lines = [
225
264
  pc.green("✓") +
226
- ` Created ${pc.bold(projectName)} at ${pc.dim(targetDir)}`,
265
+ ` Created ${pc.bold(finalProjectName)} at ${pc.dim(targetDir)}`,
227
266
  pc.green("✓") + ` AUTH_SECRET generated and written to .env.local`,
228
267
  pc.green("✓") + ` brand.config.ts patched (storeName + storeSlug)`,
268
+ generatedBrief ? pc.green("✓") + ` AI brief injected` : "",
229
269
  "",
230
270
  pc.bold("Next steps:"),
231
- ` cd ${projectName}`,
271
+ ` cd ${finalSlug}`,
232
272
  ` npx prisma migrate deploy ${pc.dim("# create / sync the DB schema")}`,
233
273
  ` npx prisma db seed ${pc.dim("# seed demo products + categories")}`,
234
274
  ` ${runCmd} dev ${pc.dim("# http://localhost:3000")}`,