create-payload-stack 0.4.0 → 0.5.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 (2) hide show
  1. package/dist/index.js +274 -69
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -68,6 +68,120 @@ function defaultConnectionString(db, projectSlug) {
68
68
  return `${db.connectionPrefix}${projectSlug}${db.connectionSuffix}`;
69
69
  }
70
70
 
71
+ // src/scheduler.ts
72
+ import { cp } from "fs/promises";
73
+ import { existsSync } from "fs";
74
+ import path2 from "path";
75
+
76
+ // src/variants.ts
77
+ import { rm } from "fs/promises";
78
+ import path from "path";
79
+ var VARIANT_DIR = "variants";
80
+ function variantPath(step) {
81
+ return path.join(VARIANT_DIR, step);
82
+ }
83
+ async function removeVariants(directory) {
84
+ await rm(path.join(directory, VARIANT_DIR), { force: true, recursive: true });
85
+ }
86
+
87
+ // src/scheduler.ts
88
+ var SCHEDULER_PACKAGE = "@payload-solutions/plugin-action-scheduler";
89
+ var SCHEDULER_VERSION = "0.3.0";
90
+ var VARIANT_SCHEDULER = variantPath("scheduler-plugin");
91
+ var SEAM_FILES = ["plugin.ts", "jobs.ts", "hooks.ts"];
92
+ var ACTION_DIR = "actions";
93
+ var RUNNER_KEYS = ["clock", "vercel", "server", "later"];
94
+ var DOCS = "https://payload.solutions/docs/payload-stack/scheduling";
95
+ var ALTERNATIVES = [
96
+ "# - Payload Clock: https://payloadclock.com (free; retries and alerts, works on any host)",
97
+ '# - Vercel cron: a "crons" entry in vercel.json hitting the same path',
98
+ "# - Long-lived server: RUN_JOBS_IN_PROCESS=true (never on serverless)",
99
+ "# - Any cron you already have: curl the endpoint with the CRON_SECRET bearer token",
100
+ "# - By hand: Scheduled Actions -> Run queue, in the admin",
101
+ `# ${DOCS}`
102
+ ];
103
+ var RUNNER_CHOICES = {
104
+ clock: {
105
+ label: "Payload Clock",
106
+ hint: "free hosted clock, retries and alerts, any host",
107
+ cronSecret: true,
108
+ inProcess: false,
109
+ next: () => [
110
+ "# scheduled actions need a clock. Sign up at https://payloadclock.com (free), point it at",
111
+ "# POST/GET /api/payload-jobs/run?allQueues=true with the CRON_SECRET in .env",
112
+ "# or use one of the others:",
113
+ ...ALTERNATIVES.slice(1)
114
+ ]
115
+ },
116
+ vercel: {
117
+ label: "Vercel cron",
118
+ hint: "vercel.json; once a minute on Pro, once a day on Hobby",
119
+ cronSecret: true,
120
+ inProcess: false,
121
+ next: () => [
122
+ "# vercel.json runs the queue every minute (Hobby plans get one run a day, which is coarse",
123
+ "# for a scheduler: https://payloadclock.com is free and runs every minute on any plan)",
124
+ "# add CRON_SECRET from .env to the Vercel project: Vercel sends it as the bearer token",
125
+ ...ALTERNATIVES
126
+ ]
127
+ },
128
+ server: {
129
+ label: "This server",
130
+ hint: "RUN_JOBS_IN_PROCESS=true; docker, Fly, Railway, a VM",
131
+ cronSecret: true,
132
+ inProcess: true,
133
+ next: () => [
134
+ "# RUN_JOBS_IN_PROCESS=true in .env runs the queue in this process, every minute.",
135
+ "# Never set it on Vercel or another serverless host, and run one instance with it.",
136
+ ...ALTERNATIVES
137
+ ]
138
+ },
139
+ later: {
140
+ label: "Decide later",
141
+ hint: "nothing runs the queue; the admin Run queue button still does",
142
+ cronSecret: false,
143
+ inProcess: false,
144
+ next: () => [
145
+ "# nothing runs the queue yet, so scheduled actions wait. Pick one when you deploy:",
146
+ ...ALTERNATIVES
147
+ ]
148
+ }
149
+ };
150
+ function swapSchedulerPackages(packageJson, enabled) {
151
+ const deps = { ...packageJson.dependencies };
152
+ const devDeps = { ...packageJson.devDependencies };
153
+ delete devDeps[SCHEDULER_PACKAGE];
154
+ if (enabled) deps[SCHEDULER_PACKAGE] = SCHEDULER_VERSION;
155
+ else delete deps[SCHEDULER_PACKAGE];
156
+ return { ...packageJson, dependencies: sortKeys(deps), devDependencies: sortKeys(devDeps) };
157
+ }
158
+ function sortKeys(obj) {
159
+ return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
160
+ }
161
+ async function applySchedulerChoice(directory, enabled) {
162
+ if (!enabled) return;
163
+ const variant = path2.join(directory, VARIANT_SCHEDULER);
164
+ if (!existsSync(variant)) {
165
+ throw new Error(`The template is missing ${VARIANT_SCHEDULER}; it cannot scaffold with the scheduler.`);
166
+ }
167
+ for (const name of SEAM_FILES) {
168
+ await cp(path2.join(variant, name), path2.join(directory, "src/scheduler", name));
169
+ }
170
+ await cp(path2.join(variant, ACTION_DIR), path2.join(directory, "src/scheduler", ACTION_DIR), { recursive: true });
171
+ await cp(path2.join(variant, "tests/scheduler.spec.ts"), path2.join(directory, "tests/unit/scheduler.spec.ts"));
172
+ }
173
+ function renderVercelJson() {
174
+ return `${JSON.stringify(
175
+ {
176
+ $schema: "https://openapi.vercel.sh/vercel.json",
177
+ crons: [{ path: "/api/payload-jobs/run?allQueues=true", schedule: "* * * * *" }]
178
+ },
179
+ null,
180
+ 2
181
+ )}
182
+ `;
183
+ }
184
+
71
185
  // src/storage.ts
72
186
  var LOCAL_STORAGE_CONFIG = `// Local disk (./media): fine for development, lost on redeploy on Vercel and other ephemeral hosts.
73
187
  // Move uploads to Vercel Blob, S3, R2, Azure, GCS or Uploadthing: https://payload.solutions/docs/payload-stack/storage`;
@@ -253,6 +367,9 @@ function helpText(version2) {
253
367
  --no-emails Keep emails as React Email components in code
254
368
  --consent Cookie banner and audited legal pages (Payload Consent)
255
369
  --no-consent Plain legal pages, no banner
370
+ --scheduler Scheduled and recurring actions (Payload Action Scheduler)
371
+ --no-scheduler No scheduler
372
+ --runner <kind> ${RUNNER_KEYS.join(" | ")} (what runs the queue; default: clock)
256
373
  --use-pnpm | --use-npm | --use-yarn | --use-bun
257
374
  --no-install Skip dependency installation
258
375
  --no-git Skip git init
@@ -287,6 +404,8 @@ function parse(argv) {
287
404
  storage: { type: "string" },
288
405
  emails: { type: "boolean" },
289
406
  consent: { type: "boolean" },
407
+ scheduler: { type: "boolean" },
408
+ runner: { type: "string" },
290
409
  "use-pnpm": { type: "boolean" },
291
410
  "use-npm": { type: "boolean" },
292
411
  "use-yarn": { type: "boolean" },
@@ -314,6 +433,8 @@ function parse(argv) {
314
433
  storage: values.storage,
315
434
  emails: values.emails,
316
435
  consent: values.consent,
436
+ scheduler: values.scheduler,
437
+ runner: values.runner,
317
438
  packageManager,
318
439
  install: values.install ?? true,
319
440
  git: values.git ?? true,
@@ -331,40 +452,27 @@ function parse(argv) {
331
452
  // src/cli.ts
332
453
  import * as p from "@clack/prompts";
333
454
  import { execa as execa2 } from "execa";
334
- import { existsSync as existsSync5 } from "fs";
335
- import path6 from "path";
455
+ import { existsSync as existsSync6 } from "fs";
456
+ import path7 from "path";
336
457
  import pc2 from "picocolors";
337
458
 
338
459
  // src/configure.ts
339
460
  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";
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
- }
461
+ import path5 from "path";
357
462
 
358
463
  // src/consent.ts
464
+ import { cp as cp2, rm as rm2, rmdir } from "fs/promises";
465
+ import { existsSync as existsSync2 } from "fs";
466
+ import path3 from "path";
359
467
  var CONSENT_PACKAGE = "@payload-solutions/plugin-consent";
360
468
  var CONSENT_REACT_PACKAGE = "@payload-solutions/consent-react";
361
- var CONSENT_VERSION = "0.1.0";
469
+ var CONSENT_VERSION = "0.1.1";
362
470
  var CONSENT_PACKAGES = {
363
471
  [CONSENT_PACKAGE]: CONSENT_VERSION,
364
472
  [CONSENT_REACT_PACKAGE]: CONSENT_VERSION
365
473
  };
366
474
  var VARIANT_CONSENT = variantPath("consent-plugin");
367
- var SEAM_FILES = [
475
+ var SEAM_FILES2 = [
368
476
  "plugin.ts",
369
477
  "collections.ts",
370
478
  "seed.ts",
@@ -383,34 +491,34 @@ function swapConsentPackages(packageJson, enabled) {
383
491
  if (enabled) deps[pkg] = CONSENT_PACKAGES[pkg];
384
492
  else delete deps[pkg];
385
493
  }
386
- return { ...packageJson, dependencies: sortKeys(deps), devDependencies: sortKeys(devDeps) };
494
+ return { ...packageJson, dependencies: sortKeys2(deps), devDependencies: sortKeys2(devDeps) };
387
495
  }
388
- function sortKeys(obj) {
496
+ function sortKeys2(obj) {
389
497
  return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
390
498
  }
391
499
  async function applyConsentChoice(directory, enabled) {
392
500
  if (!enabled) return;
393
- const variant = path2.join(directory, VARIANT_CONSENT);
394
- if (!existsSync(variant)) {
501
+ const variant = path3.join(directory, VARIANT_CONSENT);
502
+ if (!existsSync2(variant)) {
395
503
  throw new Error(`The template is missing ${VARIANT_CONSENT}; it cannot scaffold with consent.`);
396
504
  }
397
- for (const name of [...SEAM_FILES, ...BANNER_FILES]) {
398
- await cp(path2.join(variant, name), path2.join(directory, "src/consent", name), { recursive: true });
505
+ for (const name of [...SEAM_FILES2, ...BANNER_FILES]) {
506
+ await cp2(path3.join(variant, name), path3.join(directory, "src/consent", name), { recursive: true });
399
507
  }
400
- await cp(path2.join(variant, "tests/consent.spec.ts"), path2.join(directory, "tests/unit/consent.spec.ts"));
508
+ await cp2(path3.join(variant, "tests/consent.spec.ts"), path3.join(directory, "tests/unit/consent.spec.ts"));
401
509
  for (const name of TEMPLATE_LEGAL_FILES) {
402
- await rm2(path2.join(directory, name), { force: true, recursive: true });
510
+ await rm2(path3.join(directory, name), { force: true, recursive: true });
403
511
  }
404
- await rmdir(path2.join(directory, "src/seed")).catch(() => {
512
+ await rmdir(path3.join(directory, "src/seed")).catch(() => {
405
513
  });
406
514
  }
407
515
 
408
516
  // 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";
517
+ import { cp as cp3, rm as rm3 } from "fs/promises";
518
+ import { existsSync as existsSync3 } from "fs";
519
+ import path4 from "path";
412
520
  var EMAILS_PACKAGE = "@payload-solutions/plugin-emails";
413
- var EMAILS_VERSION = "0.1.0";
521
+ var EMAILS_VERSION = "0.1.1";
414
522
  var REACT_EMAIL_COMPONENTS = ["@react-email/components", "^1.0.12"];
415
523
  var REACT_EMAIL_ONLY_PACKAGES = {
416
524
  "@react-email/render": "^2.1.0",
@@ -473,24 +581,24 @@ function swapEmailsPackages(packageJson, enabled) {
473
581
  Object.assign(deps, REACT_EMAIL_ONLY_PACKAGES);
474
582
  scripts[EMAIL_DEV_SCRIPT[0]] = EMAIL_DEV_SCRIPT[1];
475
583
  }
476
- return { ...packageJson, dependencies: sortKeys2(deps), devDependencies: sortKeys2(devDeps), scripts: sortKeys2(scripts) };
584
+ return { ...packageJson, dependencies: sortKeys3(deps), devDependencies: sortKeys3(devDeps), scripts: sortKeys3(scripts) };
477
585
  }
478
- function sortKeys2(obj) {
586
+ function sortKeys3(obj) {
479
587
  return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
480
588
  }
481
589
  var REACT_EMAIL_FILES = ["src/emails/send.ts", "src/components/auth/email"];
482
590
  async function applyEmailsChoice(directory, enabled) {
483
- const variant = path3.join(directory, VARIANT_EMAILS);
591
+ const variant = path4.join(directory, VARIANT_EMAILS);
484
592
  if (enabled) {
485
- if (!existsSync2(variant)) {
593
+ if (!existsSync3(variant)) {
486
594
  throw new Error(`The template is missing ${VARIANT_EMAILS}; it cannot scaffold with emails.`);
487
595
  }
488
596
  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 });
597
+ await cp3(path4.join(variant, name), path4.join(directory, "src/emails", name), { recursive: true });
490
598
  }
491
- await cp2(path3.join(variant, "tests/emails.spec.ts"), path3.join(directory, "tests/unit/emails.spec.ts"));
599
+ await cp3(path4.join(variant, "tests/emails.spec.ts"), path4.join(directory, "tests/unit/emails.spec.ts"));
492
600
  for (const name of REACT_EMAIL_FILES) {
493
- await rm3(path3.join(directory, name), { force: true, recursive: true });
601
+ await rm3(path4.join(directory, name), { force: true, recursive: true });
494
602
  }
495
603
  }
496
604
  }
@@ -498,7 +606,7 @@ async function applyEmailsChoice(directory, enabled) {
498
606
  // src/utils.ts
499
607
  import slugifyBase from "@sindresorhus/slugify";
500
608
  import { randomBytes } from "crypto";
501
- import { existsSync as existsSync3, readdirSync } from "fs";
609
+ import { existsSync as existsSync4, readdirSync } from "fs";
502
610
  import { execa } from "execa";
503
611
  function slugify(input) {
504
612
  return slugifyBase(input.trim()).replace(/^-+|-+$/g, "") || "my-saas";
@@ -507,7 +615,7 @@ function generateSecret(bytes = 32) {
507
615
  return randomBytes(bytes).toString("base64url");
508
616
  }
509
617
  function isDirectoryEmpty(dir) {
510
- if (!existsSync3(dir)) return true;
618
+ if (!existsSync4(dir)) return true;
511
619
  const entries = readdirSync(dir).filter((e) => ![".git", ".DS_Store"].includes(e));
512
620
  return entries.length === 0;
513
621
  }
@@ -542,6 +650,8 @@ function runCommand(pm, script) {
542
650
  // src/configure.ts
543
651
  function renderStackConfig(o) {
544
652
  const list = (items) => `[${items.map((i) => `'${i}'`).join(", ")}]`;
653
+ const parts = ["Authentication", ...o.organizations ? ["organizations"] : [], ...o.billing === "none" ? [] : ["subscriptions"], "an admin panel"];
654
+ const description = `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}, wired together on Payload CMS and Next.js.`;
545
655
  const billingBlock = o.billing === "none" ? ` billing: { provider: 'none' },` : ` billing: {
546
656
  provider: 'stripe',
547
657
  attachedTo: '${o.billing}', // or '${o.billing === "organization" ? "user" : "organization"}'
@@ -600,7 +710,7 @@ export default defineStack({
600
710
  name: '${o.name.replace(/'/g, "\\'")}',
601
711
  tagline: 'A new SaaS, built on Payload CMS.',
602
712
  description:
603
- 'Authentication, organizations, subscriptions and an admin panel, wired together on Payload CMS and Next.js.',
713
+ '${description}',
604
714
  url: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
605
715
  support: { email: 'support@example.com' },
606
716
 
@@ -696,7 +806,7 @@ function swapStoragePackage(packageJson, storage) {
696
806
  if (pkg !== storage?.packageName) delete deps[pkg];
697
807
  }
698
808
  if (storage) deps[storage.packageName] = payloadVersion;
699
- return { ...packageJson, dependencies: sortKeys3(deps) };
809
+ return { ...packageJson, dependencies: sortKeys4(deps) };
700
810
  }
701
811
  function storageChoice(key) {
702
812
  return key === "none" ? null : STORAGE_CHOICES[key];
@@ -708,7 +818,7 @@ function swapDatabasePackage(packageJson, db) {
708
818
  if (other.packageName !== db.packageName) delete deps[other.packageName];
709
819
  }
710
820
  deps[db.packageName] = payloadVersion;
711
- return { ...packageJson, dependencies: sortKeys3(deps) };
821
+ return { ...packageJson, dependencies: sortKeys4(deps) };
712
822
  }
713
823
  function stripWorkspaceDependencies(packageJson) {
714
824
  const next = { ...packageJson };
@@ -731,7 +841,10 @@ function renderEnv(example, o) {
731
841
  DATABASE_URL: o.connectionString,
732
842
  PAYLOAD_SECRET: generateSecret(24),
733
843
  BETTER_AUTH_SECRET: generateSecret(32),
734
- NEXT_PUBLIC_APP_URL: o.appUrl ?? "http://localhost:3000"
844
+ NEXT_PUBLIC_APP_URL: o.appUrl ?? "http://localhost:3000",
845
+ // A clock needs a secret to get past jobs.access.run; without one that endpoint is admin-only.
846
+ ...o.cronSecret ? { CRON_SECRET: generateSecret(32) } : {},
847
+ ...o.inProcessRunner ? { RUN_JOBS_IN_PROCESS: "true" } : {}
735
848
  };
736
849
  const lines = example.split("\n").map((line) => {
737
850
  const m = /^#?\s?([A-Z0-9_]+)=(.*)$/.exec(line);
@@ -742,16 +855,34 @@ function renderEnv(example, o) {
742
855
  });
743
856
  return lines.join("\n");
744
857
  }
745
- function sortKeys3(obj) {
858
+ function sortKeys4(obj) {
746
859
  return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
747
860
  }
861
+ function renderTestEnv(source, db, projectSlug) {
862
+ const urls = {
863
+ sqlite: {
864
+ int: "file:./test.db",
865
+ e2e: "file:./e2e.db",
866
+ note: "# SQLite: two local files, created on first run. Delete them to start the suites from empty."
867
+ },
868
+ mongodb: {
869
+ int: `mongodb://127.0.0.1/${projectSlug}-test`,
870
+ e2e: `mongodb://127.0.0.1/${projectSlug}-e2e`,
871
+ note: "# MongoDB: two databases on a local server (mongodb://127.0.0.1:27017). docker-compose.yml does not start one."
872
+ }
873
+ };
874
+ const target = urls[db.key];
875
+ if (!target) return source;
876
+ return source.replace(/^DATABASE_URL=.*$/m, `${target.note}
877
+ DATABASE_URL=${target.int}`).replace(/^E2E_DATABASE_URL=.*$/m, `E2E_DATABASE_URL=${target.e2e}`);
878
+ }
748
879
  function escapeRegExp(s) {
749
880
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
750
881
  }
751
882
  async function configureProject(o) {
752
883
  const db = DB_CHOICES[o.db];
753
- const read = (rel) => readFile(path4.join(o.directory, rel), "utf8");
754
- const write = (rel, content) => writeFile(path4.join(o.directory, rel), content);
884
+ const read = (rel) => readFile(path5.join(o.directory, rel), "utf8");
885
+ const write = (rel, content) => writeFile(path5.join(o.directory, rel), content);
755
886
  await write("src/stack.config.ts", renderStackConfig(o));
756
887
  const storage = storageChoice(o.storage);
757
888
  const payloadConfig = await read("src/payload.config.ts");
@@ -759,18 +890,45 @@ async function configureProject(o) {
759
890
  await write("src/payload.config.ts", o.emails ? addEmailsPlugin(withAdapters) : withAdapters);
760
891
  await applyEmailsChoice(o.directory, o.emails);
761
892
  await applyConsentChoice(o.directory, o.consent);
893
+ await applySchedulerChoice(o.directory, o.scheduler);
762
894
  await removeVariants(o.directory);
895
+ if (o.scheduler && o.runner === "vercel") {
896
+ await write("vercel.json", renderVercelJson());
897
+ }
763
898
  const pkg = JSON.parse(await read("package.json"));
764
- const configured = swapConsentPackages(
765
- swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
766
- o.consent
899
+ const configured = swapSchedulerPackages(
900
+ swapConsentPackages(
901
+ swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
902
+ o.consent
903
+ ),
904
+ o.scheduler
767
905
  );
768
906
  await write(
769
907
  "package.json",
770
908
  JSON.stringify(renameProject(stripWorkspaceDependencies(configured), o.slug), null, 2) + "\n"
771
909
  );
910
+ await write("test.env", renderTestEnv(await read("test.env"), db, o.slug));
911
+ if (db.key === "sqlite") {
912
+ const ignore = await read(".gitignore");
913
+ if (!/^\*\.db$/m.test(ignore)) await write(".gitignore", `${ignore.trimEnd()}
914
+
915
+ # SQLite databases
916
+ *.db
917
+ *.db-journal
918
+ *.db-wal
919
+ *.db-shm
920
+ `);
921
+ }
922
+ const runner = RUNNER_CHOICES[o.runner];
772
923
  const example = await read(".env.example");
773
- await write(".env", renderEnv(example, { connectionString: o.connectionString }));
924
+ await write(
925
+ ".env",
926
+ renderEnv(example, {
927
+ connectionString: o.connectionString,
928
+ cronSecret: o.scheduler && runner.cronSecret,
929
+ inProcessRunner: o.scheduler && runner.inProcess
930
+ })
931
+ );
774
932
  }
775
933
 
776
934
  // src/options.ts
@@ -778,9 +936,9 @@ var AUTH_METHODS = ["email-password", "magic-link", "passkey"];
778
936
  var SOCIAL_PROVIDERS = ["google", "github"];
779
937
 
780
938
  // src/template.ts
781
- import { cp as cp3, mkdir, rm as rm4 } from "fs/promises";
782
- import { existsSync as existsSync4 } from "fs";
783
- import path5 from "path";
939
+ import { cp as cp4, mkdir, rm as rm4 } from "fs/promises";
940
+ import { existsSync as existsSync5 } from "fs";
941
+ import path6 from "path";
784
942
  import { Readable } from "stream";
785
943
  import { pipeline } from "stream/promises";
786
944
  import * as tar from "tar";
@@ -808,29 +966,29 @@ Check your network, or pass --local-template <path> to scaffold from a local che
808
966
  }
809
967
  })
810
968
  );
811
- if (!existsSync4(path5.join(dest, "package.json"))) {
969
+ if (!existsSync5(path6.join(dest, "package.json"))) {
812
970
  throw new Error(`The downloaded archive did not contain ${TEMPLATE_PATH}/package.json (branch "${branch}").`);
813
971
  }
814
972
  }
815
973
  async function copyLocalTemplate({ from, dest }) {
816
- const source = path5.resolve(from);
817
- if (!existsSync4(path5.join(source, "package.json"))) {
974
+ const source = path6.resolve(from);
975
+ if (!existsSync5(path6.join(source, "package.json"))) {
818
976
  throw new Error(`--local-template must point at a directory containing package.json (got ${source})`);
819
977
  }
820
978
  await mkdir(dest, { recursive: true });
821
- await cp3(source, dest, {
979
+ await cp4(source, dest, {
822
980
  recursive: true,
823
981
  filter: (src) => {
824
- const rel = path5.relative(source, src);
825
- const top = rel.split(path5.sep)[0];
982
+ const rel = path6.relative(source, src);
983
+ const top = rel.split(path6.sep)[0];
826
984
  return !["node_modules", ".next", ".turbo", ".env", "test-results", "playwright-report", "next-env.d.ts", "tsconfig.tsbuildinfo", "CLAUDE.md"].includes(top ?? "");
827
985
  }
828
986
  });
829
- await rm4(path5.join(dest, "CLAUDE.md"), { force: true });
987
+ await rm4(path6.join(dest, "CLAUDE.md"), { force: true });
830
988
  }
831
989
 
832
990
  // src/cli.ts
833
- var DOCS = "https://payload.solutions/docs/payload-stack";
991
+ var DOCS2 = "https://payload.solutions/docs/payload-stack";
834
992
  var CONSENT_DOCS = "https://payload.solutions/docs/plugins/payload-consent/audit";
835
993
  function bail(message = "Cancelled.") {
836
994
  p.cancel(message);
@@ -865,7 +1023,7 @@ async function run(flags, positionalName) {
865
1023
  })
866
1024
  );
867
1025
  const slug = slugify(name);
868
- const directory = path6.resolve(process.cwd(), slug);
1026
+ const directory = path7.resolve(process.cwd(), slug);
869
1027
  if (!isDirectoryEmpty(directory)) {
870
1028
  bail(`Directory ./${slug} already exists and is not empty.`);
871
1029
  }
@@ -966,6 +1124,30 @@ async function run(flags, positionalName) {
966
1124
  inactive: "Plain legal pages, no banner"
967
1125
  })
968
1126
  ));
1127
+ const scheduler = flags.scheduler ?? (flags.defaults ? true : guard(
1128
+ await p.confirm({
1129
+ message: "Scheduled and recurring actions",
1130
+ initialValue: true,
1131
+ active: "Trial reminders, dunning, sweeps, and a ledger in the admin (Payload Action Scheduler)",
1132
+ inactive: "No scheduler"
1133
+ })
1134
+ ));
1135
+ const runnerFlag = flags.runner;
1136
+ if (runnerFlag && !RUNNER_KEYS.includes(runnerFlag)) {
1137
+ bail(`Unknown value "${flags.runner}" for --runner. Allowed: ${RUNNER_KEYS.join(", ")}`);
1138
+ }
1139
+ if (runnerFlag && !scheduler) bail("--runner needs --scheduler. Nothing runs a queue that does not exist.");
1140
+ const runner = !scheduler ? "later" : runnerFlag ?? (flags.defaults ? "clock" : guard(
1141
+ await p.select({
1142
+ message: "What runs the queue",
1143
+ initialValue: "clock",
1144
+ options: RUNNER_KEYS.map((key) => ({
1145
+ value: key,
1146
+ label: RUNNER_CHOICES[key].label,
1147
+ hint: RUNNER_CHOICES[key].hint
1148
+ }))
1149
+ })
1150
+ ));
969
1151
  const packageManager = flags.packageManager ?? await detectPackageManager();
970
1152
  const options = {
971
1153
  name,
@@ -980,6 +1162,8 @@ async function run(flags, positionalName) {
980
1162
  storage,
981
1163
  emails,
982
1164
  consent,
1165
+ scheduler,
1166
+ runner,
983
1167
  packageManager,
984
1168
  install: flags.install,
985
1169
  git: flags.git
@@ -1009,13 +1193,14 @@ async function run(flags, positionalName) {
1009
1193
  env: { ...process.env, CI: "true" }
1010
1194
  });
1011
1195
  s2.stop(`Installed dependencies with ${packageManager}`);
1196
+ await generateArtifacts(directory, packageManager, slug);
1012
1197
  } catch (error) {
1013
1198
  s2.stop("Dependency installation failed");
1014
1199
  p.log.warn(`Run ${pc2.cyan(installCommand(packageManager))} inside ./${slug} to retry.
1015
1200
  ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
1016
1201
  }
1017
1202
  }
1018
- if (options.git && !existsSync5(path6.join(directory, ".git"))) {
1203
+ if (options.git && !existsSync6(path7.join(directory, ".git"))) {
1019
1204
  try {
1020
1205
  await execa2("git", ["init", "-b", "main"], { cwd: directory });
1021
1206
  await execa2("git", ["add", "-A"], { cwd: directory });
@@ -1037,10 +1222,30 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
1037
1222
  ...consent ? [
1038
1223
  "# the seeded legal pages are templates, not policy: the homepage shows the six steps in development",
1039
1224
  `# ${CONSENT_DOCS}`
1040
- ] : []
1225
+ ] : [],
1226
+ ...scheduler ? ["# scheduled actions are under System -> Scheduled Actions in the admin", ...RUNNER_CHOICES[runner].next()] : []
1041
1227
  ];
1042
1228
  p.note(steps.join("\n"), "Done. Next steps:");
1043
- p.outro(`Docs: ${pc2.underline(DOCS)}`);
1229
+ p.outro(`Docs: ${pc2.underline(DOCS2)}`);
1230
+ }
1231
+ async function generateArtifacts(directory, packageManager, slug) {
1232
+ const s = p.spinner();
1233
+ s.start("Generating the admin import map and Payload types");
1234
+ const failed = [];
1235
+ for (const script of ["generate:importmap", "generate:types"]) {
1236
+ const [bin, ...args] = runCommand(packageManager, script).split(" ");
1237
+ try {
1238
+ await execa2(bin, args, { cwd: directory, env: { ...process.env, CI: "true" } });
1239
+ } catch {
1240
+ failed.push(runCommand(packageManager, script));
1241
+ }
1242
+ }
1243
+ if (failed.length === 0) {
1244
+ s.stop("Generated the admin import map and Payload types");
1245
+ return;
1246
+ }
1247
+ s.stop("Could not generate every file");
1248
+ p.log.warn(`Run ${failed.map((c) => pc2.cyan(c)).join(" and ")} inside ./${slug} before the first build.`);
1044
1249
  }
1045
1250
 
1046
1251
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-payload-stack",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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",