create-payload-stack 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +251 -37
  3. 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
- Seven 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). 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.
9
+ Nine 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) and cookie consent (banner with audited legal pages, or plain legal pages). 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 -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 -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
@@ -249,6 +249,10 @@ function helpText(version2) {
249
249
  --no-organizations Disable organizations
250
250
  --billing <mode> organization | user | none
251
251
  --storage <adapter> ${STORAGE_KEYS.join(" | ")} (none = local disk)
252
+ --emails Edit transactional email copy in the Payload admin
253
+ --no-emails Keep emails as React Email components in code
254
+ --consent Cookie banner and audited legal pages (Payload Consent)
255
+ --no-consent Plain legal pages, no banner
252
256
  --use-pnpm | --use-npm | --use-yarn | --use-bun
253
257
  --no-install Skip dependency installation
254
258
  --no-git Skip git init
@@ -281,6 +285,8 @@ function parse(argv) {
281
285
  organizations: { type: "boolean" },
282
286
  billing: { type: "string" },
283
287
  storage: { type: "string" },
288
+ emails: { type: "boolean" },
289
+ consent: { type: "boolean" },
284
290
  "use-pnpm": { type: "boolean" },
285
291
  "use-npm": { type: "boolean" },
286
292
  "use-yarn": { type: "boolean" },
@@ -306,6 +312,8 @@ function parse(argv) {
306
312
  organizations: values.organizations,
307
313
  billing: values.billing,
308
314
  storage: values.storage,
315
+ emails: values.emails,
316
+ consent: values.consent,
309
317
  packageManager,
310
318
  install: values.install ?? true,
311
319
  git: values.git ?? true,
@@ -323,18 +331,174 @@ function parse(argv) {
323
331
  // src/cli.ts
324
332
  import * as p from "@clack/prompts";
325
333
  import { execa as execa2 } from "execa";
326
- import { existsSync as existsSync3 } from "fs";
327
- import path3 from "path";
334
+ import { existsSync as existsSync5 } from "fs";
335
+ import path6 from "path";
328
336
  import pc2 from "picocolors";
329
337
 
330
338
  // src/configure.ts
331
339
  import { readFile, writeFile } from "fs/promises";
340
+ import path4 from "path";
341
+
342
+ // src/consent.ts
343
+ import { cp, rm as rm2, rmdir } from "fs/promises";
344
+ import { existsSync } from "fs";
345
+ import path2 from "path";
346
+
347
+ // src/variants.ts
348
+ import { rm } from "fs/promises";
332
349
  import path from "path";
350
+ var VARIANT_DIR = "variants";
351
+ function variantPath(step) {
352
+ return path.join(VARIANT_DIR, step);
353
+ }
354
+ async function removeVariants(directory) {
355
+ await rm(path.join(directory, VARIANT_DIR), { force: true, recursive: true });
356
+ }
357
+
358
+ // src/consent.ts
359
+ var CONSENT_PACKAGE = "@payload-solutions/plugin-consent";
360
+ var CONSENT_REACT_PACKAGE = "@payload-solutions/consent-react";
361
+ var CONSENT_VERSION = "0.1.0";
362
+ var CONSENT_PACKAGES = {
363
+ [CONSENT_PACKAGE]: CONSENT_VERSION,
364
+ [CONSENT_REACT_PACKAGE]: CONSENT_VERSION
365
+ };
366
+ var VARIANT_CONSENT = variantPath("consent-plugin");
367
+ var SEAM_FILES = [
368
+ "plugin.ts",
369
+ "collections.ts",
370
+ "seed.ts",
371
+ "consent-root.tsx",
372
+ "consent-settings-link.tsx",
373
+ "legal-page.tsx",
374
+ "legal-setup-notice.tsx"
375
+ ];
376
+ var BANNER_FILES = ["consent-banner.tsx", "consent-preferences-dialog.tsx"];
377
+ var TEMPLATE_LEGAL_FILES = ["src/collections/LegalPages.ts", "src/seed/legal.ts", "tests/unit/seed-legal.spec.ts"];
378
+ function swapConsentPackages(packageJson, enabled) {
379
+ const deps = { ...packageJson.dependencies };
380
+ const devDeps = { ...packageJson.devDependencies };
381
+ for (const pkg of Object.keys(CONSENT_PACKAGES)) {
382
+ delete devDeps[pkg];
383
+ if (enabled) deps[pkg] = CONSENT_PACKAGES[pkg];
384
+ else delete deps[pkg];
385
+ }
386
+ return { ...packageJson, dependencies: sortKeys(deps), devDependencies: sortKeys(devDeps) };
387
+ }
388
+ function sortKeys(obj) {
389
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
390
+ }
391
+ async function applyConsentChoice(directory, enabled) {
392
+ if (!enabled) return;
393
+ const variant = path2.join(directory, VARIANT_CONSENT);
394
+ if (!existsSync(variant)) {
395
+ throw new Error(`The template is missing ${VARIANT_CONSENT}; it cannot scaffold with consent.`);
396
+ }
397
+ for (const name of [...SEAM_FILES, ...BANNER_FILES]) {
398
+ await cp(path2.join(variant, name), path2.join(directory, "src/consent", name), { recursive: true });
399
+ }
400
+ await cp(path2.join(variant, "tests/consent.spec.ts"), path2.join(directory, "tests/unit/consent.spec.ts"));
401
+ for (const name of TEMPLATE_LEGAL_FILES) {
402
+ await rm2(path2.join(directory, name), { force: true, recursive: true });
403
+ }
404
+ await rmdir(path2.join(directory, "src/seed")).catch(() => {
405
+ });
406
+ }
407
+
408
+ // src/emails.ts
409
+ import { cp as cp2, rm as rm3 } from "fs/promises";
410
+ import { existsSync as existsSync2 } from "fs";
411
+ import path3 from "path";
412
+ var EMAILS_PACKAGE = "@payload-solutions/plugin-emails";
413
+ var EMAILS_VERSION = "0.1.0";
414
+ var REACT_EMAIL_COMPONENTS = ["@react-email/components", "^1.0.12"];
415
+ var REACT_EMAIL_ONLY_PACKAGES = {
416
+ "@react-email/render": "^2.1.0",
417
+ "react-email": "^6.9.3"
418
+ };
419
+ var EMAIL_DEV_SCRIPT = ["email:dev", "email dev --dir src/components/auth/email --port 3001"];
420
+ var VARIANT_EMAILS = variantPath("emails-plugin");
421
+ var IMPORT_MARKER = "// emails-plugin-import";
422
+ var CONFIG_START = "// emails-plugin-config-start";
423
+ var CONFIG_END = "// emails-plugin-config-end";
424
+ var IMPORTS = `${IMPORT_MARKER}
425
+ import { emailsPlugin } from '${EMAILS_PACKAGE}'
426
+ import { emailDefinitions } from '@/emails/definitions'
427
+ import { StackEmailTemplate } from '@/emails/template'
428
+ import { globalVariableManifest, globalVariables } from '@/emails/variables'`;
429
+ var REGISTRATION = `plugins.push(
430
+ emailsPlugin({
431
+ // The catalogue, filtered by the features in stack.config.ts (src/emails/definitions).
432
+ emails: emailDefinitions,
433
+ // Branding lives in code; editors own the words (src/emails/template.tsx).
434
+ templates: { default: StackEmailTemplate },
435
+ // {{site.name}}, {{support.email}}, {{url.dashboard}} \u2026 in every email (src/emails/variables.ts).
436
+ globalVariables,
437
+ globalVariableManifest,
438
+ // Every send recorded, for 90 days. Set enabled: false to keep no record at all.
439
+ log: { enabled: true, retentionDays: 90 },
440
+ settings: { mediaCollection: 'media' },
441
+ }),
442
+ )`;
443
+ function addEmailsPlugin(payloadConfigSource) {
444
+ if (!payloadConfigSource.includes(IMPORT_MARKER)) {
445
+ throw new Error(`payload.config.ts is missing the "${IMPORT_MARKER}" marker`);
446
+ }
447
+ const start = payloadConfigSource.indexOf(CONFIG_START);
448
+ const end = payloadConfigSource.indexOf(CONFIG_END);
449
+ if (start === -1 || end === -1 || end < start) {
450
+ throw new Error(`payload.config.ts is missing the emails plugin markers`);
451
+ }
452
+ const withImports = payloadConfigSource.replace(IMPORT_MARKER, IMPORTS);
453
+ const at = withImports.indexOf(CONFIG_START);
454
+ const to = withImports.indexOf(CONFIG_END);
455
+ return `${withImports.slice(0, at)}${CONFIG_START}
456
+ ${REGISTRATION}
457
+ ${withImports.slice(to)}`;
458
+ }
459
+ function swapEmailsPackages(packageJson, enabled) {
460
+ const deps = { ...packageJson.dependencies };
461
+ const devDeps = { ...packageJson.devDependencies };
462
+ const scripts = { ...packageJson.scripts };
463
+ delete devDeps[EMAILS_PACKAGE];
464
+ delete devDeps[REACT_EMAIL_COMPONENTS[0]];
465
+ if (enabled) {
466
+ deps[EMAILS_PACKAGE] = EMAILS_VERSION;
467
+ deps[REACT_EMAIL_COMPONENTS[0]] = REACT_EMAIL_COMPONENTS[1];
468
+ for (const pkg of Object.keys(REACT_EMAIL_ONLY_PACKAGES)) delete deps[pkg];
469
+ delete scripts[EMAIL_DEV_SCRIPT[0]];
470
+ } else {
471
+ delete deps[EMAILS_PACKAGE];
472
+ delete deps[REACT_EMAIL_COMPONENTS[0]];
473
+ Object.assign(deps, REACT_EMAIL_ONLY_PACKAGES);
474
+ scripts[EMAIL_DEV_SCRIPT[0]] = EMAIL_DEV_SCRIPT[1];
475
+ }
476
+ return { ...packageJson, dependencies: sortKeys2(deps), devDependencies: sortKeys2(devDeps), scripts: sortKeys2(scripts) };
477
+ }
478
+ function sortKeys2(obj) {
479
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
480
+ }
481
+ var REACT_EMAIL_FILES = ["src/emails/send.ts", "src/components/auth/email"];
482
+ async function applyEmailsChoice(directory, enabled) {
483
+ const variant = path3.join(directory, VARIANT_EMAILS);
484
+ if (enabled) {
485
+ if (!existsSync2(variant)) {
486
+ throw new Error(`The template is missing ${VARIANT_EMAILS}; it cannot scaffold with emails.`);
487
+ }
488
+ for (const name of ["index.ts", "hooks.ts", "template.tsx", "variables.ts", "definitions"]) {
489
+ await cp2(path3.join(variant, name), path3.join(directory, "src/emails", name), { recursive: true });
490
+ }
491
+ await cp2(path3.join(variant, "tests/emails.spec.ts"), path3.join(directory, "tests/unit/emails.spec.ts"));
492
+ for (const name of REACT_EMAIL_FILES) {
493
+ await rm3(path3.join(directory, name), { force: true, recursive: true });
494
+ }
495
+ }
496
+ }
333
497
 
334
498
  // src/utils.ts
335
499
  import slugifyBase from "@sindresorhus/slugify";
336
500
  import { randomBytes } from "crypto";
337
- import { existsSync, readdirSync } from "fs";
501
+ import { existsSync as existsSync3, readdirSync } from "fs";
338
502
  import { execa } from "execa";
339
503
  function slugify(input) {
340
504
  return slugifyBase(input.trim()).replace(/^-+|-+$/g, "") || "my-saas";
@@ -343,7 +507,7 @@ function generateSecret(bytes = 32) {
343
507
  return randomBytes(bytes).toString("base64url");
344
508
  }
345
509
  function isDirectoryEmpty(dir) {
346
- if (!existsSync(dir)) return true;
510
+ if (!existsSync3(dir)) return true;
347
511
  const entries = readdirSync(dir).filter((e) => ![".git", ".DS_Store"].includes(e));
348
512
  return entries.length === 0;
349
513
  }
@@ -408,6 +572,18 @@ function renderStackConfig(o) {
408
572
  },
409
573
  ],
410
574
  },`;
575
+ const legalBlock = o.consent ? ` legal: {
576
+ company: '${o.name.replace(/'/g, "\\'")} Ltd',
577
+ jurisdiction: 'Ireland',
578
+ // The seeded legal pages are written from here. Fill these in before you publish them:
579
+ // \`npx payload-consent scan\` lists every value still left as a placeholder.
580
+ // https://payload.solutions/docs/plugins/payload-consent/audit
581
+ legalName: '${o.name.replace(/'/g, "\\'")} Ltd',
582
+ address: '[REGISTERED ADDRESS]',
583
+ },` : ` legal: {
584
+ company: '${o.name.replace(/'/g, "\\'")} Ltd',
585
+ jurisdiction: 'Ireland',
586
+ },`;
411
587
  return `import { defineStack } from '@/lib/stack'
412
588
 
413
589
  /**
@@ -450,10 +626,7 @@ ${billingBlock}
450
626
  sendPII: false,
451
627
  },
452
628
 
453
- legal: {
454
- company: '${o.name.replace(/'/g, "\\'")} Ltd',
455
- jurisdiction: 'Ireland',
456
- },
629
+ ${legalBlock}
457
630
 
458
631
  nav: [${o.billing !== "none" ? "{ label: 'Pricing', href: '/pricing' }, " : ""}{ label: 'Docs', href: 'https://payload.solutions/docs/payload-stack' }],
459
632
 
@@ -461,24 +634,24 @@ ${billingBlock}
461
634
  })
462
635
  `;
463
636
  }
464
- var IMPORT_MARKER = "// database-adapter-import";
465
- var CONFIG_START = "// database-adapter-config-start";
466
- var CONFIG_END = "// database-adapter-config-end";
637
+ var IMPORT_MARKER2 = "// database-adapter-import";
638
+ var CONFIG_START2 = "// database-adapter-config-start";
639
+ var CONFIG_END2 = "// database-adapter-config-end";
467
640
  function swapDatabaseAdapter(payloadConfigSource, db) {
468
- const importLine = `${IMPORT_MARKER}
641
+ const importLine = `${IMPORT_MARKER2}
469
642
  import { ${db.importName} } from '${db.packageName}'`;
470
643
  const withImport = payloadConfigSource.replace(
471
- new RegExp(`${escapeRegExp(IMPORT_MARKER)}\\nimport [^\\n]+\\n`),
644
+ new RegExp(`${escapeRegExp(IMPORT_MARKER2)}\\nimport [^\\n]+\\n`),
472
645
  `${importLine}
473
646
  `
474
647
  );
475
648
  if (withImport === payloadConfigSource && !payloadConfigSource.includes(importLine)) {
476
- throw new Error(`payload.config.ts is missing the "${IMPORT_MARKER}" marker`);
649
+ throw new Error(`payload.config.ts is missing the "${IMPORT_MARKER2}" marker`);
477
650
  }
478
- const start = withImport.indexOf(CONFIG_START);
479
- const end = withImport.indexOf(CONFIG_END);
651
+ const start = withImport.indexOf(CONFIG_START2);
652
+ const end = withImport.indexOf(CONFIG_END2);
480
653
  if (start === -1 || end === -1) throw new Error(`payload.config.ts is missing the database adapter markers`);
481
- return `${withImport.slice(0, start)}${CONFIG_START}
654
+ return `${withImport.slice(0, start)}${CONFIG_START2}
482
655
  ${db.config}
483
656
  ${withImport.slice(end)}`;
484
657
  }
@@ -523,7 +696,7 @@ function swapStoragePackage(packageJson, storage) {
523
696
  if (pkg !== storage?.packageName) delete deps[pkg];
524
697
  }
525
698
  if (storage) deps[storage.packageName] = payloadVersion;
526
- return { ...packageJson, dependencies: sortKeys(deps) };
699
+ return { ...packageJson, dependencies: sortKeys3(deps) };
527
700
  }
528
701
  function storageChoice(key) {
529
702
  return key === "none" ? null : STORAGE_CHOICES[key];
@@ -535,7 +708,16 @@ function swapDatabasePackage(packageJson, db) {
535
708
  if (other.packageName !== db.packageName) delete deps[other.packageName];
536
709
  }
537
710
  deps[db.packageName] = payloadVersion;
538
- return { ...packageJson, dependencies: sortKeys(deps) };
711
+ return { ...packageJson, dependencies: sortKeys3(deps) };
712
+ }
713
+ function stripWorkspaceDependencies(packageJson) {
714
+ const next = { ...packageJson };
715
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
716
+ const deps = next[field];
717
+ if (!deps) continue;
718
+ next[field] = Object.fromEntries(Object.entries(deps).filter(([, version2]) => !version2.startsWith("workspace:")));
719
+ }
720
+ return next;
539
721
  }
540
722
  function renameProject(packageJson, slug) {
541
723
  const next = { ...packageJson, name: slug, version: "0.1.0", private: true };
@@ -560,7 +742,7 @@ function renderEnv(example, o) {
560
742
  });
561
743
  return lines.join("\n");
562
744
  }
563
- function sortKeys(obj) {
745
+ function sortKeys3(obj) {
564
746
  return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
565
747
  }
566
748
  function escapeRegExp(s) {
@@ -568,16 +750,24 @@ function escapeRegExp(s) {
568
750
  }
569
751
  async function configureProject(o) {
570
752
  const db = DB_CHOICES[o.db];
571
- const read = (rel) => readFile(path.join(o.directory, rel), "utf8");
572
- const write = (rel, content) => writeFile(path.join(o.directory, rel), content);
753
+ const read = (rel) => readFile(path4.join(o.directory, rel), "utf8");
754
+ const write = (rel, content) => writeFile(path4.join(o.directory, rel), content);
573
755
  await write("src/stack.config.ts", renderStackConfig(o));
574
756
  const storage = storageChoice(o.storage);
575
757
  const payloadConfig = await read("src/payload.config.ts");
576
- await write("src/payload.config.ts", swapStorageAdapter(swapDatabaseAdapter(payloadConfig, db), storage));
758
+ const withAdapters = swapStorageAdapter(swapDatabaseAdapter(payloadConfig, db), storage);
759
+ await write("src/payload.config.ts", o.emails ? addEmailsPlugin(withAdapters) : withAdapters);
760
+ await applyEmailsChoice(o.directory, o.emails);
761
+ await applyConsentChoice(o.directory, o.consent);
762
+ await removeVariants(o.directory);
577
763
  const pkg = JSON.parse(await read("package.json"));
764
+ const configured = swapConsentPackages(
765
+ swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
766
+ o.consent
767
+ );
578
768
  await write(
579
769
  "package.json",
580
- JSON.stringify(renameProject(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.slug), null, 2) + "\n"
770
+ JSON.stringify(renameProject(stripWorkspaceDependencies(configured), o.slug), null, 2) + "\n"
581
771
  );
582
772
  const example = await read(".env.example");
583
773
  await write(".env", renderEnv(example, { connectionString: o.connectionString }));
@@ -588,9 +778,9 @@ var AUTH_METHODS = ["email-password", "magic-link", "passkey"];
588
778
  var SOCIAL_PROVIDERS = ["google", "github"];
589
779
 
590
780
  // src/template.ts
591
- import { cp, mkdir, rm } from "fs/promises";
592
- import { existsSync as existsSync2 } from "fs";
593
- import path2 from "path";
781
+ import { cp as cp3, mkdir, rm as rm4 } from "fs/promises";
782
+ import { existsSync as existsSync4 } from "fs";
783
+ import path5 from "path";
594
784
  import { Readable } from "stream";
595
785
  import { pipeline } from "stream/promises";
596
786
  import * as tar from "tar";
@@ -618,29 +808,30 @@ Check your network, or pass --local-template <path> to scaffold from a local che
618
808
  }
619
809
  })
620
810
  );
621
- if (!existsSync2(path2.join(dest, "package.json"))) {
811
+ if (!existsSync4(path5.join(dest, "package.json"))) {
622
812
  throw new Error(`The downloaded archive did not contain ${TEMPLATE_PATH}/package.json (branch "${branch}").`);
623
813
  }
624
814
  }
625
815
  async function copyLocalTemplate({ from, dest }) {
626
- const source = path2.resolve(from);
627
- if (!existsSync2(path2.join(source, "package.json"))) {
816
+ const source = path5.resolve(from);
817
+ if (!existsSync4(path5.join(source, "package.json"))) {
628
818
  throw new Error(`--local-template must point at a directory containing package.json (got ${source})`);
629
819
  }
630
820
  await mkdir(dest, { recursive: true });
631
- await cp(source, dest, {
821
+ await cp3(source, dest, {
632
822
  recursive: true,
633
823
  filter: (src) => {
634
- const rel = path2.relative(source, src);
635
- const top = rel.split(path2.sep)[0];
824
+ const rel = path5.relative(source, src);
825
+ const top = rel.split(path5.sep)[0];
636
826
  return !["node_modules", ".next", ".turbo", ".env", "test-results", "playwright-report", "next-env.d.ts", "tsconfig.tsbuildinfo", "CLAUDE.md"].includes(top ?? "");
637
827
  }
638
828
  });
639
- await rm(path2.join(dest, "CLAUDE.md"), { force: true });
829
+ await rm4(path5.join(dest, "CLAUDE.md"), { force: true });
640
830
  }
641
831
 
642
832
  // src/cli.ts
643
833
  var DOCS = "https://payload.solutions/docs/payload-stack";
834
+ var CONSENT_DOCS = "https://payload.solutions/docs/plugins/payload-consent/audit";
644
835
  function bail(message = "Cancelled.") {
645
836
  p.cancel(message);
646
837
  process.exit(1);
@@ -674,7 +865,7 @@ async function run(flags, positionalName) {
674
865
  })
675
866
  );
676
867
  const slug = slugify(name);
677
- const directory = path3.resolve(process.cwd(), slug);
868
+ const directory = path6.resolve(process.cwd(), slug);
678
869
  if (!isDirectoryEmpty(directory)) {
679
870
  bail(`Directory ./${slug} already exists and is not empty.`);
680
871
  }
@@ -759,6 +950,22 @@ async function run(flags, positionalName) {
759
950
  ]
760
951
  })
761
952
  ));
953
+ const emails = flags.emails ?? (flags.defaults ? true : guard(
954
+ await p.confirm({
955
+ message: "Transactional emails",
956
+ initialValue: true,
957
+ active: "Editable in the admin (Payload Emails)",
958
+ inactive: "React Email components in code"
959
+ })
960
+ ));
961
+ const consent = flags.consent ?? (flags.defaults ? true : guard(
962
+ await p.confirm({
963
+ message: "Cookie consent and legal pages",
964
+ initialValue: true,
965
+ active: "Banner, consent records and audited legal pages (Payload Consent)",
966
+ inactive: "Plain legal pages, no banner"
967
+ })
968
+ ));
762
969
  const packageManager = flags.packageManager ?? await detectPackageManager();
763
970
  const options = {
764
971
  name,
@@ -771,6 +978,8 @@ async function run(flags, positionalName) {
771
978
  organizations,
772
979
  billing,
773
980
  storage,
981
+ emails,
982
+ consent,
774
983
  packageManager,
775
984
  install: flags.install,
776
985
  git: flags.git
@@ -806,7 +1015,7 @@ async function run(flags, positionalName) {
806
1015
  ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
807
1016
  }
808
1017
  }
809
- if (options.git && !existsSync3(path3.join(directory, ".git"))) {
1018
+ if (options.git && !existsSync5(path6.join(directory, ".git"))) {
810
1019
  try {
811
1020
  await execa2("git", ["init", "-b", "main"], { cwd: directory });
812
1021
  await execa2("git", ["add", "-A"], { cwd: directory });
@@ -823,7 +1032,12 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
823
1032
  ...billing !== "none" ? ["# add STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and NEXT_PUBLIC_STRIPE_PRICE_* to .env when you are ready for billing"] : [],
824
1033
  ...storage !== "none" ? [`# add ${listWords(STORAGE_CHOICES[storage].envVars)} to .env to store uploads in ${STORAGE_CHOICES[storage].label} (local disk until then)`] : [],
825
1034
  runCommand(packageManager, "dev"),
826
- "open http://localhost:3000/admin"
1035
+ "open http://localhost:3000/admin",
1036
+ ...emails ? ["# email copy is under Emails in the admin; the design is src/emails/template.tsx"] : [],
1037
+ ...consent ? [
1038
+ "# the seeded legal pages are templates, not policy: the homepage shows the six steps in development",
1039
+ `# ${CONSENT_DOCS}`
1040
+ ] : []
827
1041
  ];
828
1042
  p.note(steps.join("\n"), "Done. Next steps:");
829
1043
  p.outro(`Docs: ${pc2.underline(DOCS)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-payload-stack",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold a SaaS on Payload CMS: Better Auth, organizations, Stripe subscriptions and a shadcn dashboard, in one command.",
5
5
  "keywords": [
6
6
  "payload",