create-payload-stack 0.4.0 → 0.5.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 (2) hide show
  1. package/dist/index.js +219 -66
  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.2.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,31 +452,18 @@ 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
469
  var CONSENT_VERSION = "0.1.0";
@@ -364,7 +472,7 @@ var CONSENT_PACKAGES = {
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,32 +491,32 @@ 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
521
  var EMAILS_VERSION = "0.1.0";
414
522
  var REACT_EMAIL_COMPONENTS = ["@react-email/components", "^1.0.12"];
@@ -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
  }
@@ -696,7 +804,7 @@ function swapStoragePackage(packageJson, storage) {
696
804
  if (pkg !== storage?.packageName) delete deps[pkg];
697
805
  }
698
806
  if (storage) deps[storage.packageName] = payloadVersion;
699
- return { ...packageJson, dependencies: sortKeys3(deps) };
807
+ return { ...packageJson, dependencies: sortKeys4(deps) };
700
808
  }
701
809
  function storageChoice(key) {
702
810
  return key === "none" ? null : STORAGE_CHOICES[key];
@@ -708,7 +816,7 @@ function swapDatabasePackage(packageJson, db) {
708
816
  if (other.packageName !== db.packageName) delete deps[other.packageName];
709
817
  }
710
818
  deps[db.packageName] = payloadVersion;
711
- return { ...packageJson, dependencies: sortKeys3(deps) };
819
+ return { ...packageJson, dependencies: sortKeys4(deps) };
712
820
  }
713
821
  function stripWorkspaceDependencies(packageJson) {
714
822
  const next = { ...packageJson };
@@ -731,7 +839,10 @@ function renderEnv(example, o) {
731
839
  DATABASE_URL: o.connectionString,
732
840
  PAYLOAD_SECRET: generateSecret(24),
733
841
  BETTER_AUTH_SECRET: generateSecret(32),
734
- NEXT_PUBLIC_APP_URL: o.appUrl ?? "http://localhost:3000"
842
+ NEXT_PUBLIC_APP_URL: o.appUrl ?? "http://localhost:3000",
843
+ // A clock needs a secret to get past jobs.access.run; without one that endpoint is admin-only.
844
+ ...o.cronSecret ? { CRON_SECRET: generateSecret(32) } : {},
845
+ ...o.inProcessRunner ? { RUN_JOBS_IN_PROCESS: "true" } : {}
735
846
  };
736
847
  const lines = example.split("\n").map((line) => {
737
848
  const m = /^#?\s?([A-Z0-9_]+)=(.*)$/.exec(line);
@@ -742,7 +853,7 @@ function renderEnv(example, o) {
742
853
  });
743
854
  return lines.join("\n");
744
855
  }
745
- function sortKeys3(obj) {
856
+ function sortKeys4(obj) {
746
857
  return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
747
858
  }
748
859
  function escapeRegExp(s) {
@@ -750,8 +861,8 @@ function escapeRegExp(s) {
750
861
  }
751
862
  async function configureProject(o) {
752
863
  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);
864
+ const read = (rel) => readFile(path5.join(o.directory, rel), "utf8");
865
+ const write = (rel, content) => writeFile(path5.join(o.directory, rel), content);
755
866
  await write("src/stack.config.ts", renderStackConfig(o));
756
867
  const storage = storageChoice(o.storage);
757
868
  const payloadConfig = await read("src/payload.config.ts");
@@ -759,18 +870,33 @@ async function configureProject(o) {
759
870
  await write("src/payload.config.ts", o.emails ? addEmailsPlugin(withAdapters) : withAdapters);
760
871
  await applyEmailsChoice(o.directory, o.emails);
761
872
  await applyConsentChoice(o.directory, o.consent);
873
+ await applySchedulerChoice(o.directory, o.scheduler);
762
874
  await removeVariants(o.directory);
875
+ if (o.scheduler && o.runner === "vercel") {
876
+ await write("vercel.json", renderVercelJson());
877
+ }
763
878
  const pkg = JSON.parse(await read("package.json"));
764
- const configured = swapConsentPackages(
765
- swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
766
- o.consent
879
+ const configured = swapSchedulerPackages(
880
+ swapConsentPackages(
881
+ swapEmailsPackages(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.emails),
882
+ o.consent
883
+ ),
884
+ o.scheduler
767
885
  );
768
886
  await write(
769
887
  "package.json",
770
888
  JSON.stringify(renameProject(stripWorkspaceDependencies(configured), o.slug), null, 2) + "\n"
771
889
  );
890
+ const runner = RUNNER_CHOICES[o.runner];
772
891
  const example = await read(".env.example");
773
- await write(".env", renderEnv(example, { connectionString: o.connectionString }));
892
+ await write(
893
+ ".env",
894
+ renderEnv(example, {
895
+ connectionString: o.connectionString,
896
+ cronSecret: o.scheduler && runner.cronSecret,
897
+ inProcessRunner: o.scheduler && runner.inProcess
898
+ })
899
+ );
774
900
  }
775
901
 
776
902
  // src/options.ts
@@ -778,9 +904,9 @@ var AUTH_METHODS = ["email-password", "magic-link", "passkey"];
778
904
  var SOCIAL_PROVIDERS = ["google", "github"];
779
905
 
780
906
  // 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";
907
+ import { cp as cp4, mkdir, rm as rm4 } from "fs/promises";
908
+ import { existsSync as existsSync5 } from "fs";
909
+ import path6 from "path";
784
910
  import { Readable } from "stream";
785
911
  import { pipeline } from "stream/promises";
786
912
  import * as tar from "tar";
@@ -808,29 +934,29 @@ Check your network, or pass --local-template <path> to scaffold from a local che
808
934
  }
809
935
  })
810
936
  );
811
- if (!existsSync4(path5.join(dest, "package.json"))) {
937
+ if (!existsSync5(path6.join(dest, "package.json"))) {
812
938
  throw new Error(`The downloaded archive did not contain ${TEMPLATE_PATH}/package.json (branch "${branch}").`);
813
939
  }
814
940
  }
815
941
  async function copyLocalTemplate({ from, dest }) {
816
- const source = path5.resolve(from);
817
- if (!existsSync4(path5.join(source, "package.json"))) {
942
+ const source = path6.resolve(from);
943
+ if (!existsSync5(path6.join(source, "package.json"))) {
818
944
  throw new Error(`--local-template must point at a directory containing package.json (got ${source})`);
819
945
  }
820
946
  await mkdir(dest, { recursive: true });
821
- await cp3(source, dest, {
947
+ await cp4(source, dest, {
822
948
  recursive: true,
823
949
  filter: (src) => {
824
- const rel = path5.relative(source, src);
825
- const top = rel.split(path5.sep)[0];
950
+ const rel = path6.relative(source, src);
951
+ const top = rel.split(path6.sep)[0];
826
952
  return !["node_modules", ".next", ".turbo", ".env", "test-results", "playwright-report", "next-env.d.ts", "tsconfig.tsbuildinfo", "CLAUDE.md"].includes(top ?? "");
827
953
  }
828
954
  });
829
- await rm4(path5.join(dest, "CLAUDE.md"), { force: true });
955
+ await rm4(path6.join(dest, "CLAUDE.md"), { force: true });
830
956
  }
831
957
 
832
958
  // src/cli.ts
833
- var DOCS = "https://payload.solutions/docs/payload-stack";
959
+ var DOCS2 = "https://payload.solutions/docs/payload-stack";
834
960
  var CONSENT_DOCS = "https://payload.solutions/docs/plugins/payload-consent/audit";
835
961
  function bail(message = "Cancelled.") {
836
962
  p.cancel(message);
@@ -865,7 +991,7 @@ async function run(flags, positionalName) {
865
991
  })
866
992
  );
867
993
  const slug = slugify(name);
868
- const directory = path6.resolve(process.cwd(), slug);
994
+ const directory = path7.resolve(process.cwd(), slug);
869
995
  if (!isDirectoryEmpty(directory)) {
870
996
  bail(`Directory ./${slug} already exists and is not empty.`);
871
997
  }
@@ -966,6 +1092,30 @@ async function run(flags, positionalName) {
966
1092
  inactive: "Plain legal pages, no banner"
967
1093
  })
968
1094
  ));
1095
+ const scheduler = flags.scheduler ?? (flags.defaults ? true : guard(
1096
+ await p.confirm({
1097
+ message: "Scheduled and recurring actions",
1098
+ initialValue: true,
1099
+ active: "Trial reminders, dunning, sweeps, and a ledger in the admin (Payload Action Scheduler)",
1100
+ inactive: "No scheduler"
1101
+ })
1102
+ ));
1103
+ const runnerFlag = flags.runner;
1104
+ if (runnerFlag && !RUNNER_KEYS.includes(runnerFlag)) {
1105
+ bail(`Unknown value "${flags.runner}" for --runner. Allowed: ${RUNNER_KEYS.join(", ")}`);
1106
+ }
1107
+ if (runnerFlag && !scheduler) bail("--runner needs --scheduler. Nothing runs a queue that does not exist.");
1108
+ const runner = !scheduler ? "later" : runnerFlag ?? (flags.defaults ? "clock" : guard(
1109
+ await p.select({
1110
+ message: "What runs the queue",
1111
+ initialValue: "clock",
1112
+ options: RUNNER_KEYS.map((key) => ({
1113
+ value: key,
1114
+ label: RUNNER_CHOICES[key].label,
1115
+ hint: RUNNER_CHOICES[key].hint
1116
+ }))
1117
+ })
1118
+ ));
969
1119
  const packageManager = flags.packageManager ?? await detectPackageManager();
970
1120
  const options = {
971
1121
  name,
@@ -980,6 +1130,8 @@ async function run(flags, positionalName) {
980
1130
  storage,
981
1131
  emails,
982
1132
  consent,
1133
+ scheduler,
1134
+ runner,
983
1135
  packageManager,
984
1136
  install: flags.install,
985
1137
  git: flags.git
@@ -1015,7 +1167,7 @@ async function run(flags, positionalName) {
1015
1167
  ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
1016
1168
  }
1017
1169
  }
1018
- if (options.git && !existsSync5(path6.join(directory, ".git"))) {
1170
+ if (options.git && !existsSync6(path7.join(directory, ".git"))) {
1019
1171
  try {
1020
1172
  await execa2("git", ["init", "-b", "main"], { cwd: directory });
1021
1173
  await execa2("git", ["add", "-A"], { cwd: directory });
@@ -1037,10 +1189,11 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
1037
1189
  ...consent ? [
1038
1190
  "# the seeded legal pages are templates, not policy: the homepage shows the six steps in development",
1039
1191
  `# ${CONSENT_DOCS}`
1040
- ] : []
1192
+ ] : [],
1193
+ ...scheduler ? ["# scheduled actions are under System -> Scheduled Actions in the admin", ...RUNNER_CHOICES[runner].next()] : []
1041
1194
  ];
1042
1195
  p.note(steps.join("\n"), "Done. Next steps:");
1043
- p.outro(`Docs: ${pc2.underline(DOCS)}`);
1196
+ p.outro(`Docs: ${pc2.underline(DOCS2)}`);
1044
1197
  }
1045
1198
 
1046
1199
  // 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.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",