create-payload-stack 0.1.1 → 0.3.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/index.js +348 -102
  4. package/package.json +10 -10
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Presecan / Fortbit d.o.o. and Payload Solutions contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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
- Six prompts: project name, database (PostgreSQL, MongoDB, SQLite, Vercel Postgres), connection string, sign-in methods, organizations, billing. The CLI downloads [Payload Stack](https://www.payloadstack.com), writes your answers into `src/stack.config.ts`, swaps the Payload database adapter, generates `.env` with fresh secrets, installs dependencies and initializes git.
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.
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 -y
13
+ npx create-payload-stack@latest ridgeline -d postgres --auth email-password,magic-link,passkey --social github --billing organization --storage vercel-blob -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
@@ -1,21 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { parseArgs } from "util";
5
4
  import { createRequire } from "module";
6
- import pc2 from "picocolors";
5
+ import pc3 from "picocolors";
7
6
 
8
- // src/cli.ts
9
- import * as p from "@clack/prompts";
10
- import { execa as execa2 } from "execa";
11
- import { existsSync as existsSync3 } from "fs";
12
- import path3 from "path";
7
+ // src/args.ts
8
+ import { parseArgs } from "util";
13
9
  import pc from "picocolors";
14
10
 
15
- // src/configure.ts
16
- import { readFile, writeFile } from "fs/promises";
17
- import path from "path";
18
-
19
11
  // src/databases.ts
20
12
  var DB_CHOICES = {
21
13
  postgres: {
@@ -76,6 +68,269 @@ function defaultConnectionString(db, projectSlug) {
76
68
  return `${db.connectionPrefix}${projectSlug}${db.connectionSuffix}`;
77
69
  }
78
70
 
71
+ // src/storage.ts
72
+ var LOCAL_STORAGE_CONFIG = `// Local disk (./media): fine for development, lost on redeploy on Vercel and other ephemeral hosts.
73
+ // Move uploads to Vercel Blob, S3, R2, Azure, GCS or Uploadthing: https://payload.solutions/docs/payload-stack/storage`;
74
+ var STORAGE_CHOICES = {
75
+ "vercel-blob": {
76
+ key: "vercel-blob",
77
+ label: "Vercel Blob",
78
+ hint: "needs BLOB_READ_WRITE_TOKEN",
79
+ packageName: "@payloadcms/storage-vercel-blob",
80
+ importName: "vercelBlobStorage",
81
+ usesRequireEnv: false,
82
+ envVars: ["BLOB_READ_WRITE_TOKEN"],
83
+ config: `// Vercel Blob. Without BLOB_READ_WRITE_TOKEN uploads stay on local disk (./media).
84
+ if (env.BLOB_READ_WRITE_TOKEN) {
85
+ plugins.push(
86
+ vercelBlobStorage({
87
+ collections: { media: true },
88
+ token: env.BLOB_READ_WRITE_TOKEN,
89
+ // Vercel functions accept request bodies up to 4.5 MB. For larger files upload from the
90
+ // browser instead: set clientUploads: true and run pnpm generate:importmap.
91
+ // clientUploads: true,
92
+ }),
93
+ )
94
+ }`
95
+ },
96
+ s3: {
97
+ key: "s3",
98
+ label: "AWS S3",
99
+ hint: "or S3-compatible: MinIO, DigitalOcean Spaces, Backblaze B2",
100
+ packageName: "@payloadcms/storage-s3",
101
+ importName: "s3Storage",
102
+ usesRequireEnv: false,
103
+ envVars: ["S3_BUCKET", "S3_REGION", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"],
104
+ config: `// AWS S3, or any S3-compatible service. Without S3_BUCKET uploads stay on local disk (./media).
105
+ if (env.S3_BUCKET) {
106
+ plugins.push(
107
+ s3Storage({
108
+ collections: { media: true },
109
+ bucket: env.S3_BUCKET,
110
+ config: {
111
+ region: env.S3_REGION,
112
+ // Leave the keys unset to use the AWS default credential chain (IAM roles, profiles).
113
+ credentials:
114
+ env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY
115
+ ? { accessKeyId: env.S3_ACCESS_KEY_ID, secretAccessKey: env.S3_SECRET_ACCESS_KEY }
116
+ : undefined,
117
+ // MinIO, DigitalOcean Spaces, Backblaze B2 and other S3-compatible services: set S3_ENDPOINT.
118
+ endpoint: env.S3_ENDPOINT,
119
+ forcePathStyle: Boolean(env.S3_ENDPOINT),
120
+ },
121
+ }),
122
+ )
123
+ }`
124
+ },
125
+ r2: {
126
+ key: "r2",
127
+ label: "Cloudflare R2",
128
+ hint: "through the S3 API; needs R2_BUCKET, R2_ENDPOINT and keys",
129
+ packageName: "@payloadcms/storage-s3",
130
+ importName: "s3Storage",
131
+ usesRequireEnv: true,
132
+ envVars: ["R2_BUCKET", "R2_ENDPOINT", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"],
133
+ config: `// Cloudflare R2 through its S3-compatible API. Without R2_BUCKET uploads stay on local disk (./media).
134
+ if (env.R2_BUCKET) {
135
+ plugins.push(
136
+ s3Storage({
137
+ collections: {
138
+ media: env.R2_PUBLIC_URL
139
+ ? {
140
+ // Serve files from the bucket's public URL or custom domain instead of through Payload.
141
+ disablePayloadAccessControl: true,
142
+ generateFileURL: ({ filename, prefix }) =>
143
+ \`\${env.R2_PUBLIC_URL}/\${prefix ? \`\${prefix}/\${filename}\` : filename}\`,
144
+ }
145
+ : true,
146
+ },
147
+ bucket: env.R2_BUCKET,
148
+ config: {
149
+ credentials: {
150
+ accessKeyId: requireEnv('R2_ACCESS_KEY_ID', 'for Cloudflare R2'),
151
+ secretAccessKey: requireEnv('R2_SECRET_ACCESS_KEY', 'for Cloudflare R2'),
152
+ },
153
+ // R2 requires region "auto" and path-style addressing.
154
+ region: 'auto',
155
+ endpoint: requireEnv('R2_ENDPOINT', 'for Cloudflare R2'),
156
+ forcePathStyle: true,
157
+ },
158
+ }),
159
+ )
160
+ }`
161
+ },
162
+ azure: {
163
+ key: "azure",
164
+ label: "Azure Blob Storage",
165
+ hint: "needs AZURE_STORAGE_CONNECTION_STRING, container name and base URL",
166
+ packageName: "@payloadcms/storage-azure",
167
+ importName: "azureStorage",
168
+ usesRequireEnv: true,
169
+ envVars: ["AZURE_STORAGE_CONNECTION_STRING", "AZURE_STORAGE_CONTAINER_NAME", "AZURE_STORAGE_ACCOUNT_BASEURL"],
170
+ config: `// Azure Blob Storage. Without AZURE_STORAGE_CONNECTION_STRING uploads stay on local disk (./media).
171
+ if (env.AZURE_STORAGE_CONNECTION_STRING) {
172
+ plugins.push(
173
+ azureStorage({
174
+ collections: { media: true },
175
+ connectionString: env.AZURE_STORAGE_CONNECTION_STRING,
176
+ containerName: requireEnv('AZURE_STORAGE_CONTAINER_NAME', 'for Azure Blob Storage'),
177
+ baseURL: requireEnv('AZURE_STORAGE_ACCOUNT_BASEURL', 'for Azure Blob Storage'),
178
+ allowContainerCreate: env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
179
+ }),
180
+ )
181
+ }`
182
+ },
183
+ gcs: {
184
+ key: "gcs",
185
+ label: "Google Cloud Storage",
186
+ hint: "needs GCS_BUCKET and service-account credentials",
187
+ packageName: "@payloadcms/storage-gcs",
188
+ importName: "gcsStorage",
189
+ usesRequireEnv: false,
190
+ envVars: ["GCS_BUCKET", "GCS_PROJECT_ID", "GOOGLE_APPLICATION_CREDENTIALS or GCS_SERVICE_ACCOUNT_KEY"],
191
+ config: `// Google Cloud Storage. Without GCS_BUCKET uploads stay on local disk (./media).
192
+ if (env.GCS_BUCKET) {
193
+ plugins.push(
194
+ gcsStorage({
195
+ collections: { media: true },
196
+ bucket: env.GCS_BUCKET,
197
+ options: {
198
+ projectId: env.GCS_PROJECT_ID,
199
+ // Locally, point GOOGLE_APPLICATION_CREDENTIALS at a service-account key file. On hosts
200
+ // without a filesystem, paste the key's JSON into GCS_SERVICE_ACCOUNT_KEY instead.
201
+ credentials: env.GCS_SERVICE_ACCOUNT_KEY ? JSON.parse(env.GCS_SERVICE_ACCOUNT_KEY) : undefined,
202
+ },
203
+ }),
204
+ )
205
+ }`
206
+ },
207
+ uploadthing: {
208
+ key: "uploadthing",
209
+ label: "Uploadthing",
210
+ hint: "needs UPLOADTHING_TOKEN",
211
+ packageName: "@payloadcms/storage-uploadthing",
212
+ importName: "uploadthingStorage",
213
+ usesRequireEnv: false,
214
+ envVars: ["UPLOADTHING_TOKEN"],
215
+ config: `// Uploadthing. Without UPLOADTHING_TOKEN uploads stay on local disk (./media).
216
+ if (env.UPLOADTHING_TOKEN) {
217
+ plugins.push(
218
+ uploadthingStorage({
219
+ collections: { media: true },
220
+ options: {
221
+ token: env.UPLOADTHING_TOKEN,
222
+ acl: 'public-read',
223
+ },
224
+ }),
225
+ )
226
+ }`
227
+ }
228
+ };
229
+ var STORAGE_ADAPTER_KEYS = Object.keys(STORAGE_CHOICES);
230
+ var STORAGE_KEYS = ["none", ...STORAGE_ADAPTER_KEYS];
231
+ var STORAGE_PACKAGES = [...new Set(Object.values(STORAGE_CHOICES).map((c) => c.packageName))];
232
+
233
+ // src/args.ts
234
+ function helpText(version2) {
235
+ return `
236
+ ${pc.bold("create-payload-stack")} ${pc.dim(`v${version2}`)}
237
+ Scaffold a SaaS on Payload CMS: Better Auth, organizations, Stripe and a shadcn dashboard.
238
+
239
+ ${pc.bold("Usage")}
240
+ $ npx create-payload-stack@latest [name] [options]
241
+
242
+ ${pc.bold("Options")}
243
+ -n, --name <name> Project name (also the directory)
244
+ -d, --db <db> ${DB_KEYS.join(" | ")}
245
+ --db-connection-string Connection string written to .env
246
+ --auth <list> Comma-separated: email-password, magic-link, passkey
247
+ --social <list> Comma-separated: google, github
248
+ --organizations Enable organizations (teams)
249
+ --no-organizations Disable organizations
250
+ --billing <mode> organization | user | none
251
+ --storage <adapter> ${STORAGE_KEYS.join(" | ")} (none = local disk)
252
+ --use-pnpm | --use-npm | --use-yarn | --use-bun
253
+ --no-install Skip dependency installation
254
+ --no-git Skip git init
255
+ -y, --defaults Accept defaults for every unanswered prompt
256
+ --branch <ref> Git branch or tag of the template repo (default: main)
257
+ --local-template <path> Scaffold from a local checkout of templates/payload-stack
258
+ --dry-run Print the resolved options and exit
259
+ -h, --help Show this help
260
+ -v, --version Show the version
261
+
262
+ ${pc.bold("Examples")}
263
+ $ npx create-payload-stack@latest ridgeline
264
+ $ npx create-payload-stack@latest ridgeline -d sqlite --auth email-password,passkey --billing none -y
265
+ $ npx create-payload-stack@latest ridgeline --storage vercel-blob -y
266
+
267
+ Docs: https://payload.solutions/docs/payload-stack
268
+ `;
269
+ }
270
+ function parse(argv) {
271
+ const { values, positionals } = parseArgs({
272
+ args: argv,
273
+ allowPositionals: true,
274
+ allowNegative: true,
275
+ options: {
276
+ name: { type: "string", short: "n" },
277
+ db: { type: "string", short: "d" },
278
+ "db-connection-string": { type: "string" },
279
+ auth: { type: "string" },
280
+ social: { type: "string" },
281
+ organizations: { type: "boolean" },
282
+ billing: { type: "string" },
283
+ storage: { type: "string" },
284
+ "use-pnpm": { type: "boolean" },
285
+ "use-npm": { type: "boolean" },
286
+ "use-yarn": { type: "boolean" },
287
+ "use-bun": { type: "boolean" },
288
+ install: { type: "boolean", default: true },
289
+ git: { type: "boolean", default: true },
290
+ defaults: { type: "boolean", short: "y", default: false },
291
+ branch: { type: "string", default: "main" },
292
+ "local-template": { type: "string" },
293
+ "dry-run": { type: "boolean", default: false },
294
+ help: { type: "boolean", short: "h", default: false },
295
+ version: { type: "boolean", short: "v", default: false }
296
+ }
297
+ });
298
+ const packageManager = values["use-pnpm"] ? "pnpm" : values["use-npm"] ? "npm" : values["use-yarn"] ? "yarn" : values["use-bun"] ? "bun" : void 0;
299
+ return {
300
+ flags: {
301
+ name: values.name,
302
+ db: values.db,
303
+ connectionString: values["db-connection-string"],
304
+ auth: values.auth,
305
+ social: values.social,
306
+ organizations: values.organizations,
307
+ billing: values.billing,
308
+ storage: values.storage,
309
+ packageManager,
310
+ install: values.install ?? true,
311
+ git: values.git ?? true,
312
+ defaults: values.defaults ?? false,
313
+ branch: values.branch ?? "main",
314
+ localTemplate: values["local-template"],
315
+ dryRun: values["dry-run"] ?? false,
316
+ help: values.help ?? false,
317
+ version: values.version ?? false
318
+ },
319
+ positional: positionals[0]
320
+ };
321
+ }
322
+
323
+ // src/cli.ts
324
+ import * as p from "@clack/prompts";
325
+ import { execa as execa2 } from "execa";
326
+ import { existsSync as existsSync3 } from "fs";
327
+ import path3 from "path";
328
+ import pc2 from "picocolors";
329
+
330
+ // src/configure.ts
331
+ import { readFile, writeFile } from "fs/promises";
332
+ import path from "path";
333
+
79
334
  // src/utils.ts
80
335
  import slugifyBase from "@sindresorhus/slugify";
81
336
  import { randomBytes } from "crypto";
@@ -190,6 +445,11 @@ export default defineStack({
190
445
 
191
446
  ${billingBlock}
192
447
 
448
+ observability: {
449
+ tracesSampleRate: 0.1,
450
+ sendPII: false,
451
+ },
452
+
193
453
  legal: {
194
454
  company: '${o.name.replace(/'/g, "\\'")} Ltd',
195
455
  jurisdiction: 'Ireland',
@@ -222,6 +482,52 @@ import { ${db.importName} } from '${db.packageName}'`;
222
482
  ${db.config}
223
483
  ${withImport.slice(end)}`;
224
484
  }
485
+ var STORAGE_IMPORT_MARKER = "// storage-adapter-import";
486
+ var STORAGE_CONFIG_START = "// storage-adapter-config-start";
487
+ var STORAGE_CONFIG_END = "// storage-adapter-config-end";
488
+ var ENV_IMPORT = /^import \{ ([^}]*) \} from '@\/lib\/env'$/m;
489
+ function swapStorageAdapter(payloadConfigSource, storage) {
490
+ const importPattern = new RegExp(
491
+ `${escapeRegExp(STORAGE_IMPORT_MARKER)}
492
+ (?:import \\{ \\w+ \\} from '@payloadcms\\/storage-[a-z0-9-]+'
493
+ )?`
494
+ );
495
+ if (!importPattern.test(payloadConfigSource)) {
496
+ throw new Error(`payload.config.ts is missing the "${STORAGE_IMPORT_MARKER}" marker`);
497
+ }
498
+ const importLines = storage ? `${STORAGE_IMPORT_MARKER}
499
+ import { ${storage.importName} } from '${storage.packageName}'
500
+ ` : `${STORAGE_IMPORT_MARKER}
501
+ `;
502
+ let out = payloadConfigSource.replace(importPattern, importLines);
503
+ const envImport = ENV_IMPORT.exec(out);
504
+ if (!envImport) throw new Error(`payload.config.ts is missing the "@/lib/env" import`);
505
+ const names = envImport[1].split(",").map((name) => name.trim()).filter((name) => name && name !== "requireEnv");
506
+ if (storage?.usesRequireEnv) {
507
+ const at = names.findIndex((name) => name > "requireEnv");
508
+ names.splice(at === -1 ? names.length : at, 0, "requireEnv");
509
+ }
510
+ out = out.replace(ENV_IMPORT, `import { ${names.join(", ")} } from '@/lib/env'`);
511
+ const start = out.indexOf(STORAGE_CONFIG_START);
512
+ const end = out.indexOf(STORAGE_CONFIG_END);
513
+ if (start === -1 || end === -1 || end < start) throw new Error(`payload.config.ts is missing the storage adapter markers`);
514
+ const body = storage ? storage.config : LOCAL_STORAGE_CONFIG;
515
+ return `${out.slice(0, start)}${STORAGE_CONFIG_START}
516
+ ${body}
517
+ ${out.slice(end)}`;
518
+ }
519
+ function swapStoragePackage(packageJson, storage) {
520
+ const deps = { ...packageJson.dependencies };
521
+ const payloadVersion = deps.payload ?? "latest";
522
+ for (const pkg of STORAGE_PACKAGES) {
523
+ if (pkg !== storage?.packageName) delete deps[pkg];
524
+ }
525
+ if (storage) deps[storage.packageName] = payloadVersion;
526
+ return { ...packageJson, dependencies: sortKeys(deps) };
527
+ }
528
+ function storageChoice(key) {
529
+ return key === "none" ? null : STORAGE_CHOICES[key];
530
+ }
225
531
  function swapDatabasePackage(packageJson, db) {
226
532
  const deps = { ...packageJson.dependencies };
227
533
  const payloadVersion = deps.payload ?? "latest";
@@ -265,10 +571,14 @@ async function configureProject(o) {
265
571
  const read = (rel) => readFile(path.join(o.directory, rel), "utf8");
266
572
  const write = (rel, content) => writeFile(path.join(o.directory, rel), content);
267
573
  await write("src/stack.config.ts", renderStackConfig(o));
574
+ const storage = storageChoice(o.storage);
268
575
  const payloadConfig = await read("src/payload.config.ts");
269
- await write("src/payload.config.ts", swapDatabaseAdapter(payloadConfig, db));
576
+ await write("src/payload.config.ts", swapStorageAdapter(swapDatabaseAdapter(payloadConfig, db), storage));
270
577
  const pkg = JSON.parse(await read("package.json"));
271
- await write("package.json", JSON.stringify(renameProject(swapDatabasePackage(pkg, db), o.slug), null, 2) + "\n");
578
+ await write(
579
+ "package.json",
580
+ JSON.stringify(renameProject(swapStoragePackage(swapDatabasePackage(pkg, db), storage), o.slug), null, 2) + "\n"
581
+ );
272
582
  const example = await read(".env.example");
273
583
  await write(".env", renderEnv(example, { connectionString: o.connectionString }));
274
584
  }
@@ -347,9 +657,13 @@ function parseList(raw, allowed, flag) {
347
657
  }
348
658
  return items;
349
659
  }
660
+ function listWords(items) {
661
+ if (items.length <= 1) return items.join("");
662
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
663
+ }
350
664
  async function run(flags, positionalName) {
351
665
  console.log();
352
- p.intro(pc.bgWhite(pc.black(" create-payload-stack ")));
666
+ p.intro(pc2.bgWhite(pc2.black(" create-payload-stack ")));
353
667
  const providedName = flags.name ?? positionalName;
354
668
  const name = flags.defaults && !providedName ? "my-saas" : providedName ?? guard(
355
669
  await p.text({
@@ -433,6 +747,18 @@ async function run(flags, positionalName) {
433
747
  })
434
748
  );
435
749
  }
750
+ const storageFlag = flags.storage;
751
+ if (storageFlag && !STORAGE_KEYS.includes(storageFlag)) bail(`Unknown value "${storageFlag}" for --storage. Allowed: ${STORAGE_KEYS.join(", ")}`);
752
+ const storage = storageFlag ?? (flags.defaults ? "none" : guard(
753
+ await p.select({
754
+ message: "Media storage",
755
+ initialValue: "none",
756
+ options: [
757
+ { value: "none", label: "Skip for now", hint: "uploads go to ./media on local disk; add an adapter any time" },
758
+ ...STORAGE_ADAPTER_KEYS.map((key) => ({ value: key, label: STORAGE_CHOICES[key].label, hint: STORAGE_CHOICES[key].hint }))
759
+ ]
760
+ })
761
+ ));
436
762
  const packageManager = flags.packageManager ?? await detectPackageManager();
437
763
  const options = {
438
764
  name,
@@ -444,6 +770,7 @@ async function run(flags, positionalName) {
444
770
  social,
445
771
  organizations,
446
772
  billing,
773
+ storage,
447
774
  packageManager,
448
775
  install: flags.install,
449
776
  git: flags.git
@@ -475,7 +802,7 @@ async function run(flags, positionalName) {
475
802
  s2.stop(`Installed dependencies with ${packageManager}`);
476
803
  } catch (error) {
477
804
  s2.stop("Dependency installation failed");
478
- p.log.warn(`Run ${pc.cyan(installCommand(packageManager))} inside ./${slug} to retry.
805
+ p.log.warn(`Run ${pc2.cyan(installCommand(packageManager))} inside ./${slug} to retry.
479
806
  ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
480
807
  }
481
808
  }
@@ -494,109 +821,28 @@ ${error instanceof Error ? error.message.split("\n")[0] : ""}`);
494
821
  ...db === "sqlite" ? [] : ["# start your database, then check DATABASE_URL in .env"],
495
822
  ...social.length ? [`# add ${social.map((sp) => `${sp.toUpperCase()}_CLIENT_ID / _SECRET`).join(" and ")} to .env`] : [],
496
823
  ...billing !== "none" ? ["# add STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and NEXT_PUBLIC_STRIPE_PRICE_* to .env when you are ready for billing"] : [],
824
+ ...storage !== "none" ? [`# add ${listWords(STORAGE_CHOICES[storage].envVars)} to .env to store uploads in ${STORAGE_CHOICES[storage].label} (local disk until then)`] : [],
497
825
  runCommand(packageManager, "dev"),
498
826
  "open http://localhost:3000/admin"
499
827
  ];
500
828
  p.note(steps.join("\n"), "Done. Next steps:");
501
- p.outro(`Docs: ${pc.underline(DOCS)}`);
829
+ p.outro(`Docs: ${pc2.underline(DOCS)}`);
502
830
  }
503
831
 
504
832
  // src/index.ts
505
833
  var require2 = createRequire(import.meta.url);
506
834
  var { version } = require2("../package.json");
507
- var HELP = `
508
- ${pc2.bold("create-payload-stack")} ${pc2.dim(`v${version}`)}
509
- Scaffold a SaaS on Payload CMS: Better Auth, organizations, Stripe and a shadcn dashboard.
510
-
511
- ${pc2.bold("Usage")}
512
- $ npx create-payload-stack@latest [name] [options]
513
-
514
- ${pc2.bold("Options")}
515
- -n, --name <name> Project name (also the directory)
516
- -d, --db <db> ${DB_KEYS.join(" | ")}
517
- --db-connection-string Connection string written to .env
518
- --auth <list> Comma-separated: email-password, magic-link, passkey
519
- --social <list> Comma-separated: google, github
520
- --organizations Enable organizations (teams)
521
- --no-organizations Disable organizations
522
- --billing <mode> organization | user | none
523
- --use-pnpm | --use-npm | --use-yarn | --use-bun
524
- --no-install Skip dependency installation
525
- --no-git Skip git init
526
- -y, --defaults Accept defaults for every unanswered prompt
527
- --branch <ref> Git branch or tag of the template repo (default: main)
528
- --local-template <path> Scaffold from a local checkout of templates/payload-stack
529
- --dry-run Print the resolved options and exit
530
- -h, --help Show this help
531
- -v, --version Show the version
532
-
533
- ${pc2.bold("Examples")}
534
- $ npx create-payload-stack@latest ridgeline
535
- $ npx create-payload-stack@latest ridgeline -d sqlite --auth email-password,passkey --billing none -y
536
-
537
- Docs: https://payload.solutions/docs/payload-stack
538
- `;
539
- function parse(argv) {
540
- const { values, positionals } = parseArgs({
541
- args: argv,
542
- allowPositionals: true,
543
- allowNegative: true,
544
- options: {
545
- name: { type: "string", short: "n" },
546
- db: { type: "string", short: "d" },
547
- "db-connection-string": { type: "string" },
548
- auth: { type: "string" },
549
- social: { type: "string" },
550
- organizations: { type: "boolean" },
551
- billing: { type: "string" },
552
- "use-pnpm": { type: "boolean" },
553
- "use-npm": { type: "boolean" },
554
- "use-yarn": { type: "boolean" },
555
- "use-bun": { type: "boolean" },
556
- install: { type: "boolean", default: true },
557
- git: { type: "boolean", default: true },
558
- defaults: { type: "boolean", short: "y", default: false },
559
- branch: { type: "string", default: "main" },
560
- "local-template": { type: "string" },
561
- "dry-run": { type: "boolean", default: false },
562
- help: { type: "boolean", short: "h", default: false },
563
- version: { type: "boolean", short: "v", default: false }
564
- }
565
- });
566
- const packageManager = values["use-pnpm"] ? "pnpm" : values["use-npm"] ? "npm" : values["use-yarn"] ? "yarn" : values["use-bun"] ? "bun" : void 0;
567
- return {
568
- flags: {
569
- name: values.name,
570
- db: values.db,
571
- connectionString: values["db-connection-string"],
572
- auth: values.auth,
573
- social: values.social,
574
- organizations: values.organizations,
575
- billing: values.billing,
576
- packageManager,
577
- install: values.install ?? true,
578
- git: values.git ?? true,
579
- defaults: values.defaults ?? false,
580
- branch: values.branch ?? "main",
581
- localTemplate: values["local-template"],
582
- dryRun: values["dry-run"] ?? false,
583
- help: values.help ?? false,
584
- version: values.version ?? false
585
- },
586
- positional: positionals[0]
587
- };
588
- }
589
835
  async function main() {
590
836
  let parsed;
591
837
  try {
592
838
  parsed = parse(process.argv.slice(2));
593
839
  } catch (error) {
594
- console.error(pc2.red(error instanceof Error ? error.message : String(error)));
595
- console.log(HELP);
840
+ console.error(pc3.red(error instanceof Error ? error.message : String(error)));
841
+ console.log(helpText(version));
596
842
  process.exit(1);
597
843
  }
598
844
  if (parsed.flags.help) {
599
- console.log(HELP);
845
+ console.log(helpText(version));
600
846
  return;
601
847
  }
602
848
  if (parsed.flags.version) {
@@ -606,6 +852,6 @@ async function main() {
606
852
  await run(parsed.flags, parsed.positional);
607
853
  }
608
854
  main().catch((error) => {
609
- console.error(pc2.red(error instanceof Error ? error.stack ?? error.message : String(error)));
855
+ console.error(pc3.red(error instanceof Error ? error.stack ?? error.message : String(error)));
610
856
  process.exit(1);
611
857
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-payload-stack",
3
- "version": "0.1.1",
3
+ "version": "0.3.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",
@@ -33,14 +33,6 @@
33
33
  "engines": {
34
34
  "node": ">=20.9.0"
35
35
  },
36
- "scripts": {
37
- "build": "tsup",
38
- "dev": "tsup --watch",
39
- "typecheck": "tsc --noEmit",
40
- "test": "vitest run",
41
- "clean": "rm -rf dist .turbo",
42
- "prepublishOnly": "pnpm build"
43
- },
44
36
  "dependencies": {
45
37
  "@clack/prompts": "^1.7.0",
46
38
  "@sindresorhus/slugify": "^3.0.1",
@@ -53,5 +45,13 @@
53
45
  "tsup": "^8.5.1",
54
46
  "typescript": "^5.9.3",
55
47
  "vitest": "^4.0.18"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "pnpm build && vitest run",
54
+ "clean": "rm -rf dist .turbo",
55
+ "test:unit": "vitest run --exclude test/scaffold.test.ts"
56
56
  }
57
- }
57
+ }