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.
- package/README.md +21 -2
- package/dist/approve.d.ts +2 -0
- package/dist/approve.js +16 -0
- package/dist/approve.js.map +1 -0
- package/dist/approve.test.d.ts +1 -0
- package/dist/approve.test.js +23 -0
- package/dist/approve.test.js.map +1 -0
- package/dist/brief.d.ts +26 -0
- package/dist/brief.js +25 -0
- package/dist/brief.js.map +1 -0
- package/dist/brief.test.d.ts +1 -0
- package/dist/brief.test.js +23 -0
- package/dist/brief.test.js.map +1 -0
- package/dist/generate/index.d.ts +4 -0
- package/dist/generate/index.js +42 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/generate/index.test.d.ts +1 -0
- package/dist/generate/index.test.js +47 -0
- package/dist/generate/index.test.js.map +1 -0
- package/dist/index.js +122 -82
- package/dist/index.js.map +1 -1
- package/dist/inject.d.ts +2 -0
- package/dist/inject.js +21 -0
- package/dist/inject.js.map +1 -0
- package/dist/inject.test.d.ts +1 -0
- package/dist/inject.test.js +37 -0
- package/dist/inject.test.js.map +1 -0
- package/dist/interview.d.ts +8 -0
- package/dist/interview.js +73 -0
- package/dist/interview.js.map +1 -0
- package/dist/interview.test.d.ts +1 -0
- package/dist/interview.test.js +50 -0
- package/dist/interview.test.js.map +1 -0
- package/dist/key-step.d.ts +12 -0
- package/dist/key-step.js +22 -0
- package/dist/key-step.js.map +1 -0
- package/dist/key-step.test.d.ts +1 -0
- package/dist/key-step.test.js +56 -0
- package/dist/key-step.test.js.map +1 -0
- package/dist/llm.d.ts +2 -0
- package/dist/llm.js +51 -0
- package/dist/llm.js.map +1 -0
- package/dist/llm.test.d.ts +1 -0
- package/dist/llm.test.js +32 -0
- package/dist/llm.test.js.map +1 -0
- package/dist/scaffold.d.ts +48 -0
- package/dist/scaffold.js +171 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/scaffold.test.d.ts +1 -0
- package/dist/scaffold.test.js +109 -0
- package/dist/scaffold.test.js.map +1 -0
- 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>` | `
|
|
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
|
|
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.
|
package/dist/approve.js
ADDED
|
@@ -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"}
|
package/dist/brief.d.ts
ADDED
|
@@ -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,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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
192
|
+
const requestedRef = values.ref ?? "stable";
|
|
193
|
+
const templateRef = REF_ALIASES[requestedRef] ?? requestedRef;
|
|
175
194
|
// ── Pre-flight ──────────────────────────────────────────────────────────
|
|
176
|
-
const
|
|
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 "${
|
|
199
|
+
cancel(`Directory "${finalSlug}" already exists.`);
|
|
179
200
|
process.exit(1);
|
|
180
201
|
}
|
|
181
202
|
// ── Template fetch ──────────────────────────────────────────────────────
|
|
182
203
|
const fetchSpinner = spinner();
|
|
183
|
-
|
|
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 (${
|
|
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
|
-
|
|
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(
|
|
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 ${
|
|
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")}`,
|