create-payload-stack 0.5.0 → 0.6.0
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 +2 -2
- package/dist/index.js +197 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ Scaffold a SaaS on Payload CMS in one command.
|
|
|
6
6
|
npx create-payload-stack@latest
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Twelve prompts: project name, database (PostgreSQL, MongoDB, SQLite, Vercel Postgres), connection string, sign-in methods, organizations, billing, media storage (skip for local disk, or Vercel Blob, AWS S3, Cloudflare R2, Azure Blob Storage, Google Cloud Storage, Uploadthing), transactional emails (edit copy in the Payload admin, or keep React Email components in code) cookie consent (banner with audited legal pages, or plain legal pages), scheduled actions and what runs them, and the coding agents you use (Claude Code, Codex, Cursor: task skills and MCP servers on top of `AGENTS.md`). The CLI downloads [Payload Stack](https://www.payloadstack.com), writes your answers into `src/stack.config.ts`, swaps the Payload database adapter, wires the storage adapter into `payload.config.ts`, generates `.env` with fresh secrets, installs dependencies and initializes git.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
# non-interactive
|
|
13
|
-
npx create-payload-stack@latest ridgeline -d postgres --auth email-password,magic-link,passkey --social github --billing organization --storage vercel-blob --emails --consent -y
|
|
13
|
+
npx create-payload-stack@latest ridgeline -d postgres --auth email-password,magic-link,passkey --social github --billing organization --storage vercel-blob --emails --consent --agents claude,cursor -y
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Run `npx create-payload-stack@latest --help` for every flag. Documentation: https://payload.solutions/docs/payload-stack
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,74 @@ import pc3 from "picocolors";
|
|
|
8
8
|
import { parseArgs } from "util";
|
|
9
9
|
import pc from "picocolors";
|
|
10
10
|
|
|
11
|
+
// src/agents.ts
|
|
12
|
+
import { cp, mkdir, rm, writeFile } from "fs/promises";
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var AGENT_KEYS = ["claude", "codex", "cursor"];
|
|
16
|
+
var AGENT_CHOICES = {
|
|
17
|
+
claude: { label: "Claude Code", hint: "CLAUDE.md, skills, MCP servers in .mcp.json" },
|
|
18
|
+
codex: { label: "Codex", hint: "skills in .agents/skills" },
|
|
19
|
+
cursor: { label: "Cursor", hint: "skills, MCP servers in .cursor/mcp.json" }
|
|
20
|
+
};
|
|
21
|
+
var CLAUDE_SKILLS_DIR = ".claude/skills";
|
|
22
|
+
var AGENTS_SKILLS_DIR = ".agents/skills";
|
|
23
|
+
function mcpServers({ consent }) {
|
|
24
|
+
return {
|
|
25
|
+
"next-devtools": { command: "npx", args: ["-y", "next-devtools-mcp@latest"] },
|
|
26
|
+
playwright: { command: "npx", args: ["-y", "@playwright/mcp@latest"] },
|
|
27
|
+
shadcn: { command: "npx", args: ["-y", "shadcn@latest", "mcp"] },
|
|
28
|
+
...consent ? { "payload-consent": { command: "npx", args: ["payload-consent", "mcp"] } } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function renderMcpJson(servers) {
|
|
32
|
+
return `${JSON.stringify({ mcpServers: servers }, null, 2)}
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
function parseAgents(raw) {
|
|
36
|
+
if (raw === void 0) return {};
|
|
37
|
+
const items = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
38
|
+
if (items.length === 1 && items[0] === "none") return { agents: [] };
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (!AGENT_KEYS.includes(item)) {
|
|
41
|
+
return { error: `Unknown value "${item}" for --agents. Allowed: ${AGENT_KEYS.join(", ")}, none` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { agents: AGENT_KEYS.filter((key) => items.includes(key)) };
|
|
45
|
+
}
|
|
46
|
+
function agentFiles(agents) {
|
|
47
|
+
const has = (key) => agents.includes(key);
|
|
48
|
+
return [
|
|
49
|
+
"AGENTS.md",
|
|
50
|
+
...has("claude") ? ["CLAUDE.md", CLAUDE_SKILLS_DIR, ".claude/settings.json", ".mcp.json"] : [],
|
|
51
|
+
...has("codex") || has("cursor") && !has("claude") ? [AGENTS_SKILLS_DIR] : [],
|
|
52
|
+
...has("cursor") ? [".cursor/mcp.json"] : []
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
async function applyAgentsChoice(directory, agents, { consent }) {
|
|
56
|
+
const has = (key) => agents.includes(key);
|
|
57
|
+
const at = (rel) => path.join(directory, rel);
|
|
58
|
+
const servers = renderMcpJson(mcpServers({ consent }));
|
|
59
|
+
if (agentFiles(agents).includes(AGENTS_SKILLS_DIR)) {
|
|
60
|
+
if (!existsSync(at(CLAUDE_SKILLS_DIR))) {
|
|
61
|
+
throw new Error(`The template is missing ${CLAUDE_SKILLS_DIR}; it cannot scaffold agent skills.`);
|
|
62
|
+
}
|
|
63
|
+
await mkdir(path.dirname(at(AGENTS_SKILLS_DIR)), { recursive: true });
|
|
64
|
+
await cp(at(CLAUDE_SKILLS_DIR), at(AGENTS_SKILLS_DIR), { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
if (has("claude")) {
|
|
67
|
+
await writeFile(at(".mcp.json"), servers);
|
|
68
|
+
} else {
|
|
69
|
+
for (const rel of ["CLAUDE.md", ".claude", ".mcp.json"]) {
|
|
70
|
+
await rm(at(rel), { force: true, recursive: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (has("cursor")) {
|
|
74
|
+
await mkdir(at(".cursor"), { recursive: true });
|
|
75
|
+
await writeFile(at(".cursor/mcp.json"), servers);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
11
79
|
// src/databases.ts
|
|
12
80
|
var DB_CHOICES = {
|
|
13
81
|
postgres: {
|
|
@@ -69,24 +137,24 @@ function defaultConnectionString(db, projectSlug) {
|
|
|
69
137
|
}
|
|
70
138
|
|
|
71
139
|
// src/scheduler.ts
|
|
72
|
-
import { cp } from "fs/promises";
|
|
73
|
-
import { existsSync } from "fs";
|
|
74
|
-
import
|
|
140
|
+
import { cp as cp2 } from "fs/promises";
|
|
141
|
+
import { existsSync as existsSync2 } from "fs";
|
|
142
|
+
import path3 from "path";
|
|
75
143
|
|
|
76
144
|
// src/variants.ts
|
|
77
|
-
import { rm } from "fs/promises";
|
|
78
|
-
import
|
|
145
|
+
import { rm as rm2 } from "fs/promises";
|
|
146
|
+
import path2 from "path";
|
|
79
147
|
var VARIANT_DIR = "variants";
|
|
80
148
|
function variantPath(step) {
|
|
81
|
-
return
|
|
149
|
+
return path2.join(VARIANT_DIR, step);
|
|
82
150
|
}
|
|
83
151
|
async function removeVariants(directory) {
|
|
84
|
-
await
|
|
152
|
+
await rm2(path2.join(directory, VARIANT_DIR), { force: true, recursive: true });
|
|
85
153
|
}
|
|
86
154
|
|
|
87
155
|
// src/scheduler.ts
|
|
88
156
|
var SCHEDULER_PACKAGE = "@payload-solutions/plugin-action-scheduler";
|
|
89
|
-
var SCHEDULER_VERSION = "0.
|
|
157
|
+
var SCHEDULER_VERSION = "0.3.0";
|
|
90
158
|
var VARIANT_SCHEDULER = variantPath("scheduler-plugin");
|
|
91
159
|
var SEAM_FILES = ["plugin.ts", "jobs.ts", "hooks.ts"];
|
|
92
160
|
var ACTION_DIR = "actions";
|
|
@@ -160,15 +228,15 @@ function sortKeys(obj) {
|
|
|
160
228
|
}
|
|
161
229
|
async function applySchedulerChoice(directory, enabled) {
|
|
162
230
|
if (!enabled) return;
|
|
163
|
-
const variant =
|
|
164
|
-
if (!
|
|
231
|
+
const variant = path3.join(directory, VARIANT_SCHEDULER);
|
|
232
|
+
if (!existsSync2(variant)) {
|
|
165
233
|
throw new Error(`The template is missing ${VARIANT_SCHEDULER}; it cannot scaffold with the scheduler.`);
|
|
166
234
|
}
|
|
167
235
|
for (const name of SEAM_FILES) {
|
|
168
|
-
await
|
|
236
|
+
await cp2(path3.join(variant, name), path3.join(directory, "src/scheduler", name));
|
|
169
237
|
}
|
|
170
|
-
await
|
|
171
|
-
await
|
|
238
|
+
await cp2(path3.join(variant, ACTION_DIR), path3.join(directory, "src/scheduler", ACTION_DIR), { recursive: true });
|
|
239
|
+
await cp2(path3.join(variant, "tests/scheduler.spec.ts"), path3.join(directory, "tests/unit/scheduler.spec.ts"));
|
|
172
240
|
}
|
|
173
241
|
function renderVercelJson() {
|
|
174
242
|
return `${JSON.stringify(
|
|
@@ -370,6 +438,7 @@ function helpText(version2) {
|
|
|
370
438
|
--scheduler Scheduled and recurring actions (Payload Action Scheduler)
|
|
371
439
|
--no-scheduler No scheduler
|
|
372
440
|
--runner <kind> ${RUNNER_KEYS.join(" | ")} (what runs the queue; default: clock)
|
|
441
|
+
--agents <list> Comma-separated: ${AGENT_KEYS.join(", ")}, or none (skills and MCP servers)
|
|
373
442
|
--use-pnpm | --use-npm | --use-yarn | --use-bun
|
|
374
443
|
--no-install Skip dependency installation
|
|
375
444
|
--no-git Skip git init
|
|
@@ -384,6 +453,7 @@ function helpText(version2) {
|
|
|
384
453
|
$ npx create-payload-stack@latest ridgeline
|
|
385
454
|
$ npx create-payload-stack@latest ridgeline -d sqlite --auth email-password,passkey --billing none -y
|
|
386
455
|
$ npx create-payload-stack@latest ridgeline --storage vercel-blob -y
|
|
456
|
+
$ npx create-payload-stack@latest ridgeline --agents claude,cursor -y
|
|
387
457
|
|
|
388
458
|
Docs: https://payload.solutions/docs/payload-stack
|
|
389
459
|
`;
|
|
@@ -406,6 +476,7 @@ function parse(argv) {
|
|
|
406
476
|
consent: { type: "boolean" },
|
|
407
477
|
scheduler: { type: "boolean" },
|
|
408
478
|
runner: { type: "string" },
|
|
479
|
+
agents: { type: "string" },
|
|
409
480
|
"use-pnpm": { type: "boolean" },
|
|
410
481
|
"use-npm": { type: "boolean" },
|
|
411
482
|
"use-yarn": { type: "boolean" },
|
|
@@ -435,6 +506,7 @@ function parse(argv) {
|
|
|
435
506
|
consent: values.consent,
|
|
436
507
|
scheduler: values.scheduler,
|
|
437
508
|
runner: values.runner,
|
|
509
|
+
agents: values.agents,
|
|
438
510
|
packageManager,
|
|
439
511
|
install: values.install ?? true,
|
|
440
512
|
git: values.git ?? true,
|
|
@@ -452,21 +524,21 @@ function parse(argv) {
|
|
|
452
524
|
// src/cli.ts
|
|
453
525
|
import * as p from "@clack/prompts";
|
|
454
526
|
import { execa as execa2 } from "execa";
|
|
455
|
-
import { existsSync as
|
|
456
|
-
import
|
|
527
|
+
import { existsSync as existsSync7 } from "fs";
|
|
528
|
+
import path8 from "path";
|
|
457
529
|
import pc2 from "picocolors";
|
|
458
530
|
|
|
459
531
|
// src/configure.ts
|
|
460
|
-
import { readFile, writeFile } from "fs/promises";
|
|
461
|
-
import
|
|
532
|
+
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
533
|
+
import path6 from "path";
|
|
462
534
|
|
|
463
535
|
// src/consent.ts
|
|
464
|
-
import { cp as
|
|
465
|
-
import { existsSync as
|
|
466
|
-
import
|
|
536
|
+
import { cp as cp3, rm as rm3, rmdir } from "fs/promises";
|
|
537
|
+
import { existsSync as existsSync3 } from "fs";
|
|
538
|
+
import path4 from "path";
|
|
467
539
|
var CONSENT_PACKAGE = "@payload-solutions/plugin-consent";
|
|
468
540
|
var CONSENT_REACT_PACKAGE = "@payload-solutions/consent-react";
|
|
469
|
-
var CONSENT_VERSION = "0.1.
|
|
541
|
+
var CONSENT_VERSION = "0.1.1";
|
|
470
542
|
var CONSENT_PACKAGES = {
|
|
471
543
|
[CONSENT_PACKAGE]: CONSENT_VERSION,
|
|
472
544
|
[CONSENT_REACT_PACKAGE]: CONSENT_VERSION
|
|
@@ -498,27 +570,27 @@ function sortKeys2(obj) {
|
|
|
498
570
|
}
|
|
499
571
|
async function applyConsentChoice(directory, enabled) {
|
|
500
572
|
if (!enabled) return;
|
|
501
|
-
const variant =
|
|
502
|
-
if (!
|
|
573
|
+
const variant = path4.join(directory, VARIANT_CONSENT);
|
|
574
|
+
if (!existsSync3(variant)) {
|
|
503
575
|
throw new Error(`The template is missing ${VARIANT_CONSENT}; it cannot scaffold with consent.`);
|
|
504
576
|
}
|
|
505
577
|
for (const name of [...SEAM_FILES2, ...BANNER_FILES]) {
|
|
506
|
-
await
|
|
578
|
+
await cp3(path4.join(variant, name), path4.join(directory, "src/consent", name), { recursive: true });
|
|
507
579
|
}
|
|
508
|
-
await
|
|
580
|
+
await cp3(path4.join(variant, "tests/consent.spec.ts"), path4.join(directory, "tests/unit/consent.spec.ts"));
|
|
509
581
|
for (const name of TEMPLATE_LEGAL_FILES) {
|
|
510
|
-
await
|
|
582
|
+
await rm3(path4.join(directory, name), { force: true, recursive: true });
|
|
511
583
|
}
|
|
512
|
-
await rmdir(
|
|
584
|
+
await rmdir(path4.join(directory, "src/seed")).catch(() => {
|
|
513
585
|
});
|
|
514
586
|
}
|
|
515
587
|
|
|
516
588
|
// src/emails.ts
|
|
517
|
-
import { cp as
|
|
518
|
-
import { existsSync as
|
|
519
|
-
import
|
|
589
|
+
import { cp as cp4, rm as rm4 } from "fs/promises";
|
|
590
|
+
import { existsSync as existsSync4 } from "fs";
|
|
591
|
+
import path5 from "path";
|
|
520
592
|
var EMAILS_PACKAGE = "@payload-solutions/plugin-emails";
|
|
521
|
-
var EMAILS_VERSION = "0.1.
|
|
593
|
+
var EMAILS_VERSION = "0.1.1";
|
|
522
594
|
var REACT_EMAIL_COMPONENTS = ["@react-email/components", "^1.0.12"];
|
|
523
595
|
var REACT_EMAIL_ONLY_PACKAGES = {
|
|
524
596
|
"@react-email/render": "^2.1.0",
|
|
@@ -588,17 +660,17 @@ function sortKeys3(obj) {
|
|
|
588
660
|
}
|
|
589
661
|
var REACT_EMAIL_FILES = ["src/emails/send.ts", "src/components/auth/email"];
|
|
590
662
|
async function applyEmailsChoice(directory, enabled) {
|
|
591
|
-
const variant =
|
|
663
|
+
const variant = path5.join(directory, VARIANT_EMAILS);
|
|
592
664
|
if (enabled) {
|
|
593
|
-
if (!
|
|
665
|
+
if (!existsSync4(variant)) {
|
|
594
666
|
throw new Error(`The template is missing ${VARIANT_EMAILS}; it cannot scaffold with emails.`);
|
|
595
667
|
}
|
|
596
668
|
for (const name of ["index.ts", "hooks.ts", "template.tsx", "variables.ts", "definitions"]) {
|
|
597
|
-
await
|
|
669
|
+
await cp4(path5.join(variant, name), path5.join(directory, "src/emails", name), { recursive: true });
|
|
598
670
|
}
|
|
599
|
-
await
|
|
671
|
+
await cp4(path5.join(variant, "tests/emails.spec.ts"), path5.join(directory, "tests/unit/emails.spec.ts"));
|
|
600
672
|
for (const name of REACT_EMAIL_FILES) {
|
|
601
|
-
await
|
|
673
|
+
await rm4(path5.join(directory, name), { force: true, recursive: true });
|
|
602
674
|
}
|
|
603
675
|
}
|
|
604
676
|
}
|
|
@@ -606,7 +678,7 @@ async function applyEmailsChoice(directory, enabled) {
|
|
|
606
678
|
// src/utils.ts
|
|
607
679
|
import slugifyBase from "@sindresorhus/slugify";
|
|
608
680
|
import { randomBytes } from "crypto";
|
|
609
|
-
import { existsSync as
|
|
681
|
+
import { existsSync as existsSync5, readdirSync } from "fs";
|
|
610
682
|
import { execa } from "execa";
|
|
611
683
|
function slugify(input) {
|
|
612
684
|
return slugifyBase(input.trim()).replace(/^-+|-+$/g, "") || "my-saas";
|
|
@@ -615,7 +687,7 @@ function generateSecret(bytes = 32) {
|
|
|
615
687
|
return randomBytes(bytes).toString("base64url");
|
|
616
688
|
}
|
|
617
689
|
function isDirectoryEmpty(dir) {
|
|
618
|
-
if (!
|
|
690
|
+
if (!existsSync5(dir)) return true;
|
|
619
691
|
const entries = readdirSync(dir).filter((e) => ![".git", ".DS_Store"].includes(e));
|
|
620
692
|
return entries.length === 0;
|
|
621
693
|
}
|
|
@@ -650,6 +722,8 @@ function runCommand(pm, script) {
|
|
|
650
722
|
// src/configure.ts
|
|
651
723
|
function renderStackConfig(o) {
|
|
652
724
|
const list = (items) => `[${items.map((i) => `'${i}'`).join(", ")}]`;
|
|
725
|
+
const parts = ["Authentication", ...o.organizations ? ["organizations"] : [], ...o.billing === "none" ? [] : ["subscriptions"], "an admin panel"];
|
|
726
|
+
const description = `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}, wired together on Payload CMS and Next.js.`;
|
|
653
727
|
const billingBlock = o.billing === "none" ? ` billing: { provider: 'none' },` : ` billing: {
|
|
654
728
|
provider: 'stripe',
|
|
655
729
|
attachedTo: '${o.billing}', // or '${o.billing === "organization" ? "user" : "organization"}'
|
|
@@ -708,7 +782,7 @@ export default defineStack({
|
|
|
708
782
|
name: '${o.name.replace(/'/g, "\\'")}',
|
|
709
783
|
tagline: 'A new SaaS, built on Payload CMS.',
|
|
710
784
|
description:
|
|
711
|
-
'
|
|
785
|
+
'${description}',
|
|
712
786
|
url: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
|
|
713
787
|
support: { email: 'support@example.com' },
|
|
714
788
|
|
|
@@ -856,13 +930,31 @@ function renderEnv(example, o) {
|
|
|
856
930
|
function sortKeys4(obj) {
|
|
857
931
|
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
858
932
|
}
|
|
933
|
+
function renderTestEnv(source, db, projectSlug) {
|
|
934
|
+
const urls = {
|
|
935
|
+
sqlite: {
|
|
936
|
+
int: "file:./test.db",
|
|
937
|
+
e2e: "file:./e2e.db",
|
|
938
|
+
note: "# SQLite: two local files, created on first run. Delete them to start the suites from empty."
|
|
939
|
+
},
|
|
940
|
+
mongodb: {
|
|
941
|
+
int: `mongodb://127.0.0.1/${projectSlug}-test`,
|
|
942
|
+
e2e: `mongodb://127.0.0.1/${projectSlug}-e2e`,
|
|
943
|
+
note: "# MongoDB: two databases on a local server (mongodb://127.0.0.1:27017). docker-compose.yml does not start one."
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const target = urls[db.key];
|
|
947
|
+
if (!target) return source;
|
|
948
|
+
return source.replace(/^DATABASE_URL=.*$/m, `${target.note}
|
|
949
|
+
DATABASE_URL=${target.int}`).replace(/^E2E_DATABASE_URL=.*$/m, `E2E_DATABASE_URL=${target.e2e}`);
|
|
950
|
+
}
|
|
859
951
|
function escapeRegExp(s) {
|
|
860
952
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
861
953
|
}
|
|
862
954
|
async function configureProject(o) {
|
|
863
955
|
const db = DB_CHOICES[o.db];
|
|
864
|
-
const read = (rel) => readFile(
|
|
865
|
-
const write = (rel, content) =>
|
|
956
|
+
const read = (rel) => readFile(path6.join(o.directory, rel), "utf8");
|
|
957
|
+
const write = (rel, content) => writeFile2(path6.join(o.directory, rel), content);
|
|
866
958
|
await write("src/stack.config.ts", renderStackConfig(o));
|
|
867
959
|
const storage = storageChoice(o.storage);
|
|
868
960
|
const payloadConfig = await read("src/payload.config.ts");
|
|
@@ -872,6 +964,7 @@ async function configureProject(o) {
|
|
|
872
964
|
await applyConsentChoice(o.directory, o.consent);
|
|
873
965
|
await applySchedulerChoice(o.directory, o.scheduler);
|
|
874
966
|
await removeVariants(o.directory);
|
|
967
|
+
await applyAgentsChoice(o.directory, o.agents, { consent: o.consent });
|
|
875
968
|
if (o.scheduler && o.runner === "vercel") {
|
|
876
969
|
await write("vercel.json", renderVercelJson());
|
|
877
970
|
}
|
|
@@ -887,6 +980,18 @@ async function configureProject(o) {
|
|
|
887
980
|
"package.json",
|
|
888
981
|
JSON.stringify(renameProject(stripWorkspaceDependencies(configured), o.slug), null, 2) + "\n"
|
|
889
982
|
);
|
|
983
|
+
await write("test.env", renderTestEnv(await read("test.env"), db, o.slug));
|
|
984
|
+
if (db.key === "sqlite") {
|
|
985
|
+
const ignore = await read(".gitignore");
|
|
986
|
+
if (!/^\*\.db$/m.test(ignore)) await write(".gitignore", `${ignore.trimEnd()}
|
|
987
|
+
|
|
988
|
+
# SQLite databases
|
|
989
|
+
*.db
|
|
990
|
+
*.db-journal
|
|
991
|
+
*.db-wal
|
|
992
|
+
*.db-shm
|
|
993
|
+
`);
|
|
994
|
+
}
|
|
890
995
|
const runner = RUNNER_CHOICES[o.runner];
|
|
891
996
|
const example = await read(".env.example");
|
|
892
997
|
await write(
|
|
@@ -904,9 +1009,9 @@ var AUTH_METHODS = ["email-password", "magic-link", "passkey"];
|
|
|
904
1009
|
var SOCIAL_PROVIDERS = ["google", "github"];
|
|
905
1010
|
|
|
906
1011
|
// src/template.ts
|
|
907
|
-
import { cp as
|
|
908
|
-
import { existsSync as
|
|
909
|
-
import
|
|
1012
|
+
import { cp as cp5, mkdir as mkdir2 } from "fs/promises";
|
|
1013
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1014
|
+
import path7 from "path";
|
|
910
1015
|
import { Readable } from "stream";
|
|
911
1016
|
import { pipeline } from "stream/promises";
|
|
912
1017
|
import * as tar from "tar";
|
|
@@ -921,7 +1026,7 @@ async function downloadTemplate({ dest, branch }) {
|
|
|
921
1026
|
Check your network, or pass --local-template <path> to scaffold from a local checkout.`
|
|
922
1027
|
);
|
|
923
1028
|
}
|
|
924
|
-
await
|
|
1029
|
+
await mkdir2(dest, { recursive: true });
|
|
925
1030
|
const prefixDepth = 1 + TEMPLATE_PATH.split("/").length;
|
|
926
1031
|
await pipeline(
|
|
927
1032
|
Readable.fromWeb(response.body),
|
|
@@ -934,25 +1039,25 @@ Check your network, or pass --local-template <path> to scaffold from a local che
|
|
|
934
1039
|
}
|
|
935
1040
|
})
|
|
936
1041
|
);
|
|
937
|
-
if (!
|
|
1042
|
+
if (!existsSync6(path7.join(dest, "package.json"))) {
|
|
938
1043
|
throw new Error(`The downloaded archive did not contain ${TEMPLATE_PATH}/package.json (branch "${branch}").`);
|
|
939
1044
|
}
|
|
940
1045
|
}
|
|
941
1046
|
async function copyLocalTemplate({ from, dest }) {
|
|
942
|
-
const source =
|
|
943
|
-
if (!
|
|
1047
|
+
const source = path7.resolve(from);
|
|
1048
|
+
if (!existsSync6(path7.join(source, "package.json"))) {
|
|
944
1049
|
throw new Error(`--local-template must point at a directory containing package.json (got ${source})`);
|
|
945
1050
|
}
|
|
946
|
-
await
|
|
947
|
-
await
|
|
1051
|
+
await mkdir2(dest, { recursive: true });
|
|
1052
|
+
await cp5(source, dest, {
|
|
948
1053
|
recursive: true,
|
|
949
1054
|
filter: (src) => {
|
|
950
|
-
const rel =
|
|
951
|
-
const top = rel.split(
|
|
952
|
-
|
|
1055
|
+
const rel = path7.relative(source, src);
|
|
1056
|
+
const top = rel.split(path7.sep)[0];
|
|
1057
|
+
if (rel === path7.join(".claude", "settings.local.json")) return false;
|
|
1058
|
+
return !["node_modules", ".next", ".turbo", ".env", "test-results", "playwright-report", "next-env.d.ts", "tsconfig.tsbuildinfo"].includes(top ?? "");
|
|
953
1059
|
}
|
|
954
1060
|
});
|
|
955
|
-
await rm4(path6.join(dest, "CLAUDE.md"), { force: true });
|
|
956
1061
|
}
|
|
957
1062
|
|
|
958
1063
|
// src/cli.ts
|
|
@@ -991,7 +1096,7 @@ async function run(flags, positionalName) {
|
|
|
991
1096
|
})
|
|
992
1097
|
);
|
|
993
1098
|
const slug = slugify(name);
|
|
994
|
-
const directory =
|
|
1099
|
+
const directory = path8.resolve(process.cwd(), slug);
|
|
995
1100
|
if (!isDirectoryEmpty(directory)) {
|
|
996
1101
|
bail(`Directory ./${slug} already exists and is not empty.`);
|
|
997
1102
|
}
|
|
@@ -1116,6 +1221,16 @@ async function run(flags, positionalName) {
|
|
|
1116
1221
|
}))
|
|
1117
1222
|
})
|
|
1118
1223
|
));
|
|
1224
|
+
const agentsFlag = parseAgents(flags.agents);
|
|
1225
|
+
if (agentsFlag.error) bail(agentsFlag.error);
|
|
1226
|
+
const agents = agentsFlag.agents ?? (flags.defaults ? [...AGENT_KEYS] : guard(
|
|
1227
|
+
await p.multiselect({
|
|
1228
|
+
message: "Coding agents",
|
|
1229
|
+
required: false,
|
|
1230
|
+
initialValues: [...AGENT_KEYS],
|
|
1231
|
+
options: AGENT_KEYS.map((key) => ({ value: key, label: AGENT_CHOICES[key].label, hint: AGENT_CHOICES[key].hint }))
|
|
1232
|
+
})
|
|
1233
|
+
));
|
|
1119
1234
|
const packageManager = flags.packageManager ?? await detectPackageManager();
|
|
1120
1235
|
const options = {
|
|
1121
1236
|
name,
|
|
@@ -1132,6 +1247,7 @@ async function run(flags, positionalName) {
|
|
|
1132
1247
|
consent,
|
|
1133
1248
|
scheduler,
|
|
1134
1249
|
runner,
|
|
1250
|
+
agents,
|
|
1135
1251
|
packageManager,
|
|
1136
1252
|
install: flags.install,
|
|
1137
1253
|
git: flags.git
|
|
@@ -1161,13 +1277,14 @@ async function run(flags, positionalName) {
|
|
|
1161
1277
|
env: { ...process.env, CI: "true" }
|
|
1162
1278
|
});
|
|
1163
1279
|
s2.stop(`Installed dependencies with ${packageManager}`);
|
|
1280
|
+
await generateArtifacts(directory, packageManager, slug);
|
|
1164
1281
|
} catch (error) {
|
|
1165
1282
|
s2.stop("Dependency installation failed");
|
|
1166
1283
|
p.log.warn(`Run ${pc2.cyan(installCommand(packageManager))} inside ./${slug} to retry.
|
|
1167
1284
|
${error instanceof Error ? error.message.split("\n")[0] : ""}`);
|
|
1168
1285
|
}
|
|
1169
1286
|
}
|
|
1170
|
-
if (options.git && !
|
|
1287
|
+
if (options.git && !existsSync7(path8.join(directory, ".git"))) {
|
|
1171
1288
|
try {
|
|
1172
1289
|
await execa2("git", ["init", "-b", "main"], { cwd: directory });
|
|
1173
1290
|
await execa2("git", ["add", "-A"], { cwd: directory });
|
|
@@ -1190,11 +1307,34 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
|
|
|
1190
1307
|
"# the seeded legal pages are templates, not policy: the homepage shows the six steps in development",
|
|
1191
1308
|
`# ${CONSENT_DOCS}`
|
|
1192
1309
|
] : [],
|
|
1193
|
-
...scheduler ? ["# scheduled actions are under System -> Scheduled Actions in the admin", ...RUNNER_CHOICES[runner].next()] : []
|
|
1310
|
+
...scheduler ? ["# scheduled actions are under System -> Scheduled Actions in the admin", ...RUNNER_CHOICES[runner].next()] : [],
|
|
1311
|
+
...agents.length ? [
|
|
1312
|
+
`# agents: AGENTS.md and the stack-* skills are set up for ${listWords(agents.map((key) => AGENT_CHOICES[key].label))}`,
|
|
1313
|
+
`# agents finish with ${runCommand(packageManager, "verify")} (typecheck, lint, unit tests)`
|
|
1314
|
+
] : []
|
|
1194
1315
|
];
|
|
1195
1316
|
p.note(steps.join("\n"), "Done. Next steps:");
|
|
1196
1317
|
p.outro(`Docs: ${pc2.underline(DOCS2)}`);
|
|
1197
1318
|
}
|
|
1319
|
+
async function generateArtifacts(directory, packageManager, slug) {
|
|
1320
|
+
const s = p.spinner();
|
|
1321
|
+
s.start("Generating the admin import map and Payload types");
|
|
1322
|
+
const failed = [];
|
|
1323
|
+
for (const script of ["generate:importmap", "generate:types"]) {
|
|
1324
|
+
const [bin, ...args] = runCommand(packageManager, script).split(" ");
|
|
1325
|
+
try {
|
|
1326
|
+
await execa2(bin, args, { cwd: directory, env: { ...process.env, CI: "true" } });
|
|
1327
|
+
} catch {
|
|
1328
|
+
failed.push(runCommand(packageManager, script));
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (failed.length === 0) {
|
|
1332
|
+
s.stop("Generated the admin import map and Payload types");
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
s.stop("Could not generate every file");
|
|
1336
|
+
p.log.warn(`Run ${failed.map((c) => pc2.cyan(c)).join(" and ")} inside ./${slug} before the first build.`);
|
|
1337
|
+
}
|
|
1198
1338
|
|
|
1199
1339
|
// src/index.ts
|
|
1200
1340
|
var require2 = createRequire(import.meta.url);
|
package/package.json
CHANGED