From c3418afe5517f6ee1b7baf8f8df02faecb0ecd03 Mon Sep 17 00:00:00 2001 From: cavewebs Date: Mon, 20 Apr 2026 17:38:22 +0100 Subject: [PATCH 1/2] feat: add @dashcommerce/create scaffolding CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'npm create @dashcommerce@latest' onboarding story. New package at packages/create/ that downloads a template from a flat repo on GitHub and walks the user through install + git init. Current template registry points to emdashCommerce/starter (the flat repo the sync workflow mirrors to). Adding more templates later means appending to the TEMPLATES map in src/index.ts and publishing a new version — no other CLI changes needed. - bin: create-dashcommerce (npm-convention — 'npm create @dashcommerce' resolves to '@dashcommerce/create' package's bin) - deps kept minimal: giget (template download), prompts (interactive UI), picocolors (matching emdash's terminal colors) - tsdown emits an esm bundle with a node shebang and chmod'd entry - Detects the invoking pm from npm_config_user_agent; falls back to npm - Non-interactive mode via --template flag + positional dir arg; --help prints usage + template registry - Refuses to clobber a non-empty target directory - prepublishOnly hook ensures dist/ is built before publish Included a changeset (.changeset/first-create-cli.md) marking this as a minor release. When this merges to main, the Version Packages PR will open with @dashcommerce/create@0.1.0; merging that publishes the package. --- .changeset/first-create-cli.md | 11 ++ bun.lock | 36 ++++- packages/create/README.md | 33 +++++ packages/create/package.json | 63 ++++++++ packages/create/src/index.ts | 243 +++++++++++++++++++++++++++++++ packages/create/tsconfig.json | 11 ++ packages/create/tsdown.config.ts | 23 +++ 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 .changeset/first-create-cli.md create mode 100644 packages/create/README.md create mode 100644 packages/create/package.json create mode 100644 packages/create/src/index.ts create mode 100644 packages/create/tsconfig.json create mode 100644 packages/create/tsdown.config.ts diff --git a/.changeset/first-create-cli.md b/.changeset/first-create-cli.md new file mode 100644 index 0000000..2bc89f5 --- /dev/null +++ b/.changeset/first-create-cli.md @@ -0,0 +1,11 @@ +--- +"@dashcommerce/create": minor +--- + +Initial release — scaffold a new DashCommerce project with `npm create @dashcommerce@latest`. + +- Interactive prompts for project directory, template, dependency install, and git init +- `--template ` flag for non-interactive overrides (default: `starter`) +- Template downloads via `giget` from the `emdashCommerce/starter` flat repo +- Detects the invoking package manager (bun/pnpm/yarn/npm) and runs install with it +- Prints Stripe setup + `bun run dev` next steps on success diff --git a/bun.lock b/bun.lock index a65155b..9f7e6f9 100644 --- a/bun.lock +++ b/bun.lock @@ -39,6 +39,24 @@ "react-dom", ], }, + "packages/create": { + "name": "@dashcommerce/create", + "version": "0.1.0", + "bin": { + "create-dashcommerce": "./dist/index.mjs", + }, + "dependencies": { + "giget": "^3.2.0", + "picocolors": "^1.1.1", + "prompts": "^2.4.2", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/prompts": "^2.4.9", + "tsdown": "^0.20.0", + "typescript": "^5.9.0", + }, + }, "packages/starter": { "name": "@dashcommerce/starter", "version": "0.2.0", @@ -227,6 +245,8 @@ "@dashcommerce/core": ["@dashcommerce/core@workspace:packages/core"], + "@dashcommerce/create": ["@dashcommerce/create@workspace:packages/create"], + "@dashcommerce/starter": ["@dashcommerce/starter@workspace:packages/starter"], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], @@ -705,6 +725,8 @@ "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + "@types/prompts": ["@types/prompts@2.4.9", "", { "dependencies": { "@types/node": "*", "kleur": "^3.0.3" } }, "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA=="], + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -1067,6 +1089,8 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "giget": ["giget@3.2.0", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -1195,7 +1219,7 @@ "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="], @@ -1483,6 +1507,8 @@ "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], @@ -1897,6 +1923,8 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@astrojs/check/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -1955,6 +1983,8 @@ "@oslojs/webauthn/@oslojs/encoding": ["@oslojs/encoding@1.0.0", "", {}, "sha512-dyIB0SdZgMm5BhGwdSp8rMxEFIopLKxDG1vxIBaiogyom6ZqH2aXPb6DEC2WzOOWKdPSq1cxdNeRx2wAn1Z+ZQ=="], + "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], "@types/babel__core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], @@ -1969,6 +1999,8 @@ "@types/babel__traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@types/prompts/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/sax/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/ws/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], @@ -2073,6 +2105,8 @@ "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@types/prompts/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@types/sax/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], diff --git a/packages/create/README.md b/packages/create/README.md new file mode 100644 index 0000000..7894c49 --- /dev/null +++ b/packages/create/README.md @@ -0,0 +1,33 @@ +# @dashcommerce/create + +Scaffold a new [DashCommerce](https://dashcommerce.dev) project. + +```sh +npm create @dashcommerce@latest +# or +npm create @dashcommerce@latest my-shop +# or +npm create @dashcommerce@latest my-shop -- --template starter +``` + +Works with `bun create`, `pnpm create`, and `yarn create` too — whichever package manager you invoke with is the one used to install the scaffolded project's dependencies. + +## Templates + +| Name | What you get | +|---|---| +| `starter` | Full Astro commerce site — 6 demo products, Stripe checkout, blog, subscriptions, admin. Mirrored from [`emdashCommerce/starter`](https://github.com/emdashCommerce/starter). | + +More templates will be added as standalone flat repos ship; the CLI picks them up automatically. + +## What it does + +1. Prompts for a project directory + template +2. Downloads the template via [`giget`](https://github.com/unjs/giget) (fast, no git clone) +3. Runs `install` with your package manager +4. `git init` + initial commit +5. Prints the Stripe + dev-server next steps + +## License + +MIT — see the monorepo [LICENSE](https://github.com/emdashCommerce/dashcommerce/blob/main/LICENSE). diff --git a/packages/create/package.json b/packages/create/package.json new file mode 100644 index 0000000..cbeadb0 --- /dev/null +++ b/packages/create/package.json @@ -0,0 +1,63 @@ +{ + "name": "@dashcommerce/create", + "version": "0.1.0", + "description": "Scaffold a new DashCommerce project. Invoke with `npm create @dashcommerce@latest`.", + "type": "module", + "main": "./dist/index.mjs", + "bin": { + "create-dashcommerce": "./dist/index.mjs" + }, + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "giget": "^3.2.0", + "picocolors": "^1.1.1", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/prompts": "^2.4.9", + "tsdown": "^0.20.0", + "typescript": "^5.9.0" + }, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "dashcommerce", + "emdash", + "starter", + "cli", + "scaffold", + "ecommerce", + "commerce" + ], + "author": "DashCommerce contributors", + "license": "MIT", + "homepage": "https://dashcommerce.dev", + "repository": { + "type": "git", + "url": "git+https://github.com/emdashCommerce/dashcommerce.git", + "directory": "packages/create" + }, + "bugs": { + "url": "https://github.com/emdashCommerce/dashcommerce/issues" + } +} diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts new file mode 100644 index 0000000..e5537aa --- /dev/null +++ b/packages/create/src/index.ts @@ -0,0 +1,243 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { downloadTemplate } from "giget"; +import pc from "picocolors"; +import prompts from "prompts"; + +interface TemplateDef { + label: string; + description: string; + /** giget source — any format the `giget` docs support. */ + source: string; +} + +/** + * Registry of available templates. Add new entries when new flat template + * repos ship — the CLI gets a menu option for free. + */ +const TEMPLATES: Record = { + starter: { + label: "DashCommerce Starter", + description: + "Full Astro commerce site. 6 demo products, Stripe checkout, blog, subscriptions.", + source: "github:emdashCommerce/starter", + }, +}; + +interface ParsedArgs { + directory?: string; + template?: string; + help?: boolean; +} + +function parseArgs(argv: string[]): ParsedArgs { + const args: ParsedArgs = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "-h" || a === "--help") { + args.help = true; + } else if (a === "-t" || a === "--template") { + args.template = argv[++i]; + } else if (!a.startsWith("-") && !args.directory) { + args.directory = a; + } + } + return args; +} + +function printHelp(): void { + const lines = [ + "", + ` ${pc.bold(pc.magenta("create-dashcommerce"))} · scaffold a new DashCommerce project`, + "", + ` ${pc.dim("Usage")}`, + ` npm create @dashcommerce@latest ${pc.dim("[directory]")} ${pc.dim("[--template name]")}`, + "", + ` ${pc.dim("Examples")}`, + ` npm create @dashcommerce@latest ${pc.cyan("my-shop")}`, + ` npm create @dashcommerce@latest my-shop ${pc.cyan("-- --template starter")}`, + "", + ` ${pc.dim("Templates")}`, + ...Object.entries(TEMPLATES).map( + ([k, v]) => ` ${pc.cyan(k.padEnd(10))} ${v.description}`, + ), + "", + ]; + console.log(lines.join("\n")); +} + +/** + * Detect the package manager that invoked us. `npm_config_user_agent` is set + * by every modern Node package manager and looks like + * "pnpm/9.4.0 npm/? node/v22.0.0 ...". Falls back to npm. + */ +function detectPackageManager(): "bun" | "pnpm" | "yarn" | "npm" { + const ua = process.env.npm_config_user_agent ?? ""; + if (ua.startsWith("bun")) return "bun"; + if (ua.startsWith("pnpm")) return "pnpm"; + if (ua.startsWith("yarn")) return "yarn"; + return "npm"; +} + +function run( + cmd: string, + args: string[], + cwd: string, + stdio: "inherit" | "ignore" = "inherit", +): boolean { + const result = spawnSync(cmd, args, { cwd, stdio, shell: false }); + return result.status === 0; +} + +async function main(): Promise { + const parsed = parseArgs(process.argv.slice(2)); + + if (parsed.help) { + printHelp(); + return; + } + + console.log( + `\n ${pc.bold(pc.magenta("DashCommerce"))} ${pc.dim("· create a new project")}\n`, + ); + + // Validate --template up front so we can fail before prompting. + if (parsed.template && !TEMPLATES[parsed.template]) { + console.error( + pc.red( + `Unknown template: ${parsed.template}. Available: ${Object.keys(TEMPLATES).join(", ")}`, + ), + ); + process.exit(1); + } + + const onCancel = () => { + console.log(pc.yellow("\n Cancelled.\n")); + process.exit(0); + }; + + const response = await prompts( + [ + { + type: parsed.directory ? null : "text", + name: "directory", + message: "Project directory", + initial: "my-dashcommerce-shop", + validate: (v: string) => + v.trim().length > 0 || "Directory name required", + }, + { + type: parsed.template ? null : "select", + name: "template", + message: "Template", + choices: Object.entries(TEMPLATES).map(([value, def]) => ({ + title: def.label, + description: def.description, + value, + })), + initial: 0, + }, + { + type: "confirm", + name: "install", + message: "Install dependencies now?", + initial: true, + }, + { + type: "confirm", + name: "git", + message: "Initialize a git repository?", + initial: true, + }, + ], + { onCancel }, + ); + + const directory = parsed.directory ?? (response.directory as string); + const templateKey = parsed.template ?? (response.template as string); + const template = TEMPLATES[templateKey]; + const targetDir = resolve(process.cwd(), directory); + + // Refuse to overwrite a non-empty target. Empty dirs are fine — users + // sometimes `mkdir` first. + if (existsSync(targetDir) && readdirSync(targetDir).length > 0) { + console.error( + pc.red( + `\n Target directory "${directory}" exists and is not empty. Aborting.\n`, + ), + ); + process.exit(1); + } + + console.log(`\n ${pc.cyan("↳")} Downloading ${pc.bold(template.label)}...`); + try { + await downloadTemplate(template.source, { + dir: targetDir, + force: false, + }); + } catch (err) { + console.error( + pc.red(`\n Failed to download template: ${(err as Error).message}\n`), + ); + process.exit(1); + } + + const pm = detectPackageManager(); + + if (response.install) { + console.log(` ${pc.cyan("↳")} Installing dependencies (${pm})...`); + if (!run(pm, ["install"], targetDir)) { + console.error( + pc.red( + `\n Install failed. You can retry manually: cd ${directory} && ${pm} install\n`, + ), + ); + process.exit(1); + } + } + + if (response.git) { + console.log(` ${pc.cyan("↳")} Initializing git...`); + run("git", ["init", "-q"], targetDir, "ignore"); + run("git", ["add", "-A"], targetDir, "ignore"); + run( + "git", + [ + "commit", + "-q", + "--no-gpg-sign", + "-m", + `init: scaffold from @dashcommerce/create (${templateKey})`, + ], + targetDir, + "ignore", + ); + } + + const runCmd = pm === "npm" ? "npm run" : pm; + + console.log(`\n ${pc.green("✓")} Done. Next steps:\n`); + console.log(` ${pc.dim("cd")} ${directory}`); + if (!response.install) { + console.log(` ${pc.dim(`${pm} install`)}`); + } + console.log( + ` ${pc.dim(`${runCmd} bootstrap`)} ${pc.dim("# seed DB + demo catalog")}`, + ); + console.log( + ` ${pc.dim(`${runCmd} dev`)} ${pc.dim("# dev server at :4321")}`, + ); + console.log(""); + console.log( + ` Stripe test keys: ${pc.cyan("https://dashboard.stripe.com/test/apikeys")}`, + ); + console.log( + ` Then paste them into ${pc.cyan("http://localhost:4321/_emdash/admin/plugins/dashcommerce/settings")}\n`, + ); +} + +main().catch((err) => { + console.error(pc.red(`\n Unexpected error: ${(err as Error).message}\n`)); + process.exit(1); +}); diff --git a/packages/create/tsconfig.json b/packages/create/tsconfig.json new file mode 100644 index 0000000..606092b --- /dev/null +++ b/packages/create/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/create/tsdown.config.ts b/packages/create/tsdown.config.ts new file mode 100644 index 0000000..d07234e --- /dev/null +++ b/packages/create/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: "esm", + dts: true, + clean: true, + sourcemap: true, + platform: "node", + // Prepend a node shebang so the built file is directly executable when npm + // links it as a bin script. + banner: { + js: "#!/usr/bin/env node", + }, + external: [ + // Keep dependencies external — this is a CLI, not a bundle. The user's + // runtime resolver will pull them in at install time. + "giget", + "prompts", + "picocolors", + /^node:/, + ], +}); From 2483c8673bd4ef5a4582dd173b9ab2ea6725e839 Mon Sep 17 00:00:00 2001 From: cavewebs Date: Mon, 20 Apr 2026 17:50:43 +0100 Subject: [PATCH 2/2] docs: reframe starter README around npm create @dashcommerce CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI is now the primary onboarding path — 'npm create @dashcommerce@latest' scaffolds a ready-to-run project in 30 seconds, zero cloud accounts required. One-click Deploy buttons are real but require real post-click setup (D1/R2/KV provisioning, secrets, seed); framing them as instant onboarding oversells. Rework: - New top-section 'Quick start' leads with the CLI, shows bootstrap+dev, and offers git clone as a fallback. The storefront + Stripe + checkout walkthrough flows naturally from a running local site. - Deploy section moved BELOW the feature walkthrough with honest framing ('expect to do some post-click configuration'). Each platform's setup shrunk to the essentials — full detail lives in dashcommerce.dev/docs once that's updated to match. - Railway section updated with the ephemeral-filesystem caveat and 'railway run bun run bootstrap' as the seed step. --- packages/starter/README.md | 172 +++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 84 deletions(-) diff --git a/packages/starter/README.md b/packages/starter/README.md index 95b955f..8f16646 100644 --- a/packages/starter/README.md +++ b/packages/starter/README.md @@ -1,76 +1,32 @@ # @dashcommerce/starter -**v0.2.0** — a ready-to-run Astro commerce site built on [EmDash CMS](https://github.com/emdash-cms/emdash) 0.5 and **`@dashcommerce/core@0.1.3`**. Clone, paste your Stripe test keys, run — every feature category the core plugin ships is exercised by a real page. Ships three deploy targets (Node, Cloudflare Workers, Docker) from one codebase. +**v0.2.0** — a ready-to-run Astro commerce site built on [EmDash CMS](https://github.com/emdash-cms/emdash) 0.5 and **`@dashcommerce/core@0.1.3`**. Every feature category the core plugin ships is exercised by a real page. **Live demo:** [demo.dashcommerce.dev](https://demo.dashcommerce.dev) · **Templates:** [dashcommerce.dev/templates](https://dashcommerce.dev/templates) -## Deploy - -[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/emdashCommerce/starter) -[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/emdashCommerce/starter) -[![Run with Docker](https://img.shields.io/badge/Run%20with-Docker-2496ED?logo=docker&logoColor=white)](#docker) - -Three production paths, one codebase. `astro.config.mjs` branches on env vars, so local dev, Docker, Railway (Node+Postgres) and Cloudflare Workers (D1+R2) all build from the same source. - -### Cloudflare Workers (D1 + R2) - -Uses `@astrojs/cloudflare` + `@emdash-cms/cloudflare` (D1 SQLite + R2 storage). One-time setup after clicking the button — or run locally: +## Quick start ```sh -wrangler d1 create dashcommerce-demo # paste database_id into wrangler.jsonc -wrangler r2 bucket create dashcommerce-demo-media -wrangler kv:namespace create SESSION # paste id into wrangler.jsonc -openssl rand -hex 32 | wrangler secret put EMDASH_AUTH_SECRET -openssl rand -hex 32 | wrangler secret put EMDASH_PREVIEW_SECRET -bun run cf:d1:seed # dumps local SQLite → applies to D1 -bun run cf:deploy -``` - -The `DEPLOY_TARGET=cloudflare` env (set by `cf:deploy`) flips `astro.config.mjs` to the Cloudflare adapter + D1 + R2 bindings. Monorepo note: if the button drops you into the dashboard for manual config, set **Root Directory** to `packages/starter`. - -### Railway (Node + Postgres + S3/R2) - -Uses `@astrojs/node` + Neon Postgres (or any Postgres) + S3-compatible storage (Cloudflare R2 works via its S3 API). Env vars on the service: - -``` -DATABASE_URL=postgres://… SITE_URL=https://your-domain -S3_BUCKET=… S3_ENDPOINT=… S3_ACCESS_KEY_ID=… S3_SECRET_ACCESS_KEY=… -S3_REGION=auto S3_PUBLIC_URL=https://pub-… +npm create @dashcommerce@latest ``` -One-time bootstrap against the remote DB (from your laptop): +Prompts for a project directory + template, downloads the starter, installs, commits. Then: ```sh -DATABASE_URL=postgres://… bun run --filter '@dashcommerce/starter' bootstrap +cd +bun run bootstrap # emdash init + merge-seed + seed (DB + 6 demo products) +bun run dev # Astro at :4321 ``` -`railway.json` at the repo root pins build + start commands, so "+ New Service → GitHub repo" picks them up with no extra clicking. - -### Docker +Open [http://localhost:4321](http://localhost:4321) — hero with "Enamel Mug" and a product grid. Paste your Stripe test keys at `/_emdash/admin/plugins/dashcommerce/settings` and you're exercising a real checkout in under a minute. -Zero-config local run — `docker compose up` from the repo root gives you the storefront at [localhost:4321](http://localhost:4321) with SQLite + uploads persisted in named volumes: +Prefer to clone directly? Works too: ```sh -docker compose up # builds once, then runs -docker compose exec app bun run bootstrap # seed DB + demo catalog -``` - -The same `Dockerfile` is your "deploy anywhere" image. Push it to any registry and run it on Fly/Render/ECS/Kubernetes/your-metal: - -```sh -docker build -t ghcr.io/you/dashcommerce . -docker push ghcr.io/you/dashcommerce - -docker run -p 4321:4321 \ - -e SITE_URL=https://your-domain \ - -e DATABASE_URL=postgres://… # optional; defaults to SQLite in /data - -v dashcommerce_data:/data \ - -v dashcommerce_uploads:/app/packages/starter/uploads \ - ghcr.io/you/dashcommerce +git clone https://github.com/emdashCommerce/starter +cd starter && bun install && bun run bootstrap && bun run dev ``` -To swap SQLite for Postgres, uncomment the `db` service in `docker-compose.yml` and set `DATABASE_URL=postgres://user:pass@db:5432/dashcommerce`. - ## What you get **Storefront routes** @@ -95,18 +51,7 @@ To swap SQLite for Postgres, uncomment the `db` service in `docker-compose.yml` Mounts alongside at `/_emdash/admin` with the full EmDash surface plus DashCommerce pages: Orders, Customers, Coupons, Shipping, Tax, Subscriptions, Reviews, Vendors, Menus, Reports, Settings — and the five dashboard widgets (Revenue, Low Stock, Recent Orders, Pending Reviews, Failed Renewals). -## Quickstart - -```sh -cd packages/starter -bun install -bun run bootstrap # emdash init + dashcommerce-merge-seed + seed (DB + catalog) -bun run dev # Astro at :4321 -``` - -Open `http://localhost:4321/` — you should see the hero with "Enamel Mug" and a product grid. - -### Configure Stripe +## Configure Stripe 1. Grab test keys from [dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys). 2. Open `http://localhost:4321/_emdash/admin/plugins/dashcommerce/settings`. @@ -119,21 +64,18 @@ Open `http://localhost:4321/` — you should see the hero with "Enamel Mug" and ### Exercise the checkout -1. Open any product from `/shop`, add to cart. -2. Click the drawer cart → **Checkout**. -3. Fill the contact form → **Continue to payment**. -4. Stripe test card: `4242 4242 4242 4242`, any future expiry, any CVC. -5. Redirect lands on `/thank-you/…`. The page polls `/orders/by-draft?id=…` every 800ms until the webhook fires. -6. Check `/_emdash/admin/plugins/dashcommerce/orders` — your order is listed with a green **Paid** badge. -7. Check your terminal (or an email inbox if SMTP is wired) — the receipt email has fired. +1. Open any product from `/shop`, add to cart → drawer cart → **Checkout**. +2. Fill contact form → **Continue to payment**. +3. Stripe test card: `4242 4242 4242 4242`, any future expiry, any CVC. +4. Redirect lands on `/thank-you/…`. The page polls `/orders/by-draft?id=…` every 800ms until the webhook fires. +5. Check `/_emdash/admin/plugins/dashcommerce/orders` — order is listed with a green **Paid** badge. +6. Receipt email fires (check terminal for the console transport, or an inbox if SMTP is wired). -### Exercise the refund path +### Refund path -1. Open the order detail in admin. -2. Click **Refund** → pick full or partial → confirm. -3. The order flips to **Refunded** / **Partially refunded** and a refund email is sent. +Open the order in admin → **Refund** → full or partial → confirm. Order flips to **Refunded** / **Partially refunded** and a refund email is sent. -### Exercise subscriptions +### Subscriptions Add `SUB-001` (Monthly Box) to cart → checkout. Stripe creates a Subscription with a 7-day trial. The `/subscriptions/[token]` page gives the customer self-service controls. `invoice.payment_succeeded` on cycle invoices triggers the renewal email; `invoice.payment_failed` starts the dunning flow. @@ -150,22 +92,84 @@ Six products spanning every DashCommerce type: | `SUB-001` | Monthly Box | subscription | $29/mo, 7-day trial | | `DIG-001` | Design Templates | simple + downloadable | Signed-URL token delivery | -Rebuild the seed from its TypeScript source with: +Rebuild the seed from its TypeScript source with `bun .emdash/build-seed.ts`. + +## Deploy + +When you're ready to ship, the starter builds for three targets from one codebase. `astro.config.mjs` branches on env vars. Expect to do some post-click configuration on the hosted options — these buttons get you into the provider's dashboard with sensible defaults, not an instant production site. + +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/emdashCommerce/starter) +[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/new/template?template=https://github.com/emdashCommerce/starter) +[![Run with Docker](https://img.shields.io/badge/Run%20with-Docker-2496ED?logo=docker&logoColor=white)](#docker) + +### Cloudflare Workers (D1 + R2) + +Runs on `@astrojs/cloudflare` + `@emdash-cms/cloudflare`. After clicking the button you'll still need to provision resources, wire secrets, and seed the DB. Locally: + +```sh +wrangler d1 create dashcommerce-demo # paste database_id into wrangler.jsonc +wrangler r2 bucket create dashcommerce-demo-media +wrangler kv namespace create SESSION # paste id into wrangler.jsonc +openssl rand -hex 32 | wrangler secret put EMDASH_AUTH_SECRET +openssl rand -hex 32 | wrangler secret put EMDASH_PREVIEW_SECRET +bun run cf:d1:seed # dumps local SQLite → applies to D1 +bun run cf:deploy +``` + +D1 migrations must run via wrangler before deploy (no runtime DDL on Workers). + +### Railway (Node + Postgres + S3/R2) + +Runs on `@astrojs/node` + any Postgres (Neon free tier works) + S3-compat storage (R2 or AWS). Env vars on the service: + +``` +DATABASE_URL=postgres://… +SITE_URL=https://your-domain +S3_BUCKET=… S3_ENDPOINT=… S3_ACCESS_KEY_ID=… S3_SECRET_ACCESS_KEY=… +S3_REGION=auto S3_PUBLIC_URL=https://pub-… +``` + +One-time seed against the remote DB: + +```sh +railway run bun run bootstrap +``` + +Railway's filesystem is ephemeral — use Postgres, or mount a volume at `/data` and set `SQLITE_URL=file:/data/data.db`. + +### Docker + +Local run with `docker compose up` from the repo root — storefront at [localhost:4321](http://localhost:4321), SQLite + uploads on named volumes: ```sh -bun .emdash/build-seed.ts +docker compose up +docker compose exec app bun run bootstrap ``` +The same image deploys anywhere (Fly, Render, ECS, Kubernetes, bare metal): + +```sh +docker build -t ghcr.io/you/dashcommerce . +docker run -p 4321:4321 \ + -e SITE_URL=https://your-domain \ + -e DATABASE_URL=postgres://… # optional; defaults to SQLite in /data + -v dashcommerce_data:/data \ + -v dashcommerce_uploads:/app/packages/starter/uploads \ + ghcr.io/you/dashcommerce +``` + +Swap SQLite for Postgres by uncommenting the `db` service in `docker-compose.yml` and setting `DATABASE_URL`. + ## Customizing -This is a *starting point*, not a framework. Everything is standard Astro — clone, rewrite. +This is a *starting point*, not a framework. Everything is standard Astro — edit freely. - **Layout + brand:** `src/layouts/Shop.astro`, `src/components/Header.astro`, `src/styles/global.css` -- **Homepage sections:** all editable in the admin under **Pages → Home** (hero, featured grid, blog teaser, value props) +- **Homepage sections:** admin under **Pages → Home** (hero, featured grid, blog teaser, value props) - **Nav / footer:** admin under **DashCommerce → Menus** (nested up to 4 levels, with mega-menu columns) - **Product tile + detail:** the starter copies components out of `@dashcommerce/core/astro/components/*`; edit the local copies freely, or drop in your own -The plugin itself lives in `../core` (or `@dashcommerce/core` on npm). Don't fork it — customize at the site level and file an issue if the core needs to change. +The plugin itself is `@dashcommerce/core` on npm. Don't fork it — customize at the site level and file an issue if the core needs to change. ## License