create-headless-store 0.1.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.
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk from "chalk";
6
+ import prompts from "prompts";
7
+ import ora from "ora";
8
+ import { execa } from "execa";
9
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
10
+ import { join, resolve } from "path";
11
+ import crypto from "crypto";
12
+ var VERSION = "0.1.0";
13
+ var BANNER = `
14
+ ${chalk.bold.blue("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
15
+ ${chalk.bold.blue("\u2551")} ${chalk.bold("\u26A1 Headless Commerce")} ${chalk.bold.blue("\u2551")}
16
+ ${chalk.bold.blue("\u2551")} ${chalk.dim("AI-native commerce in 5 minutes")} ${chalk.bold.blue("\u2551")}
17
+ ${chalk.bold.blue("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
18
+ `;
19
+ var program = new Command();
20
+ program.name("create-headless-store").description("Create a new headless commerce store").version(VERSION).argument("[directory]", "Project directory name").option("--no-demo", "Skip demo seed data").option("--port <port>", "API port", "3010").action(async (directory) => {
21
+ console.log(BANNER);
22
+ const opts = program.opts();
23
+ const answers = await prompts([
24
+ {
25
+ type: directory ? null : "text",
26
+ name: "directory",
27
+ message: "Project directory:",
28
+ initial: "my-store"
29
+ },
30
+ {
31
+ type: "text",
32
+ name: "name",
33
+ message: "Store name:",
34
+ initial: "My Store"
35
+ },
36
+ {
37
+ type: "text",
38
+ name: "stripeSecretKey",
39
+ message: `Stripe Secret Key ${chalk.dim("(optional, press Enter to skip)")}:`,
40
+ initial: ""
41
+ },
42
+ {
43
+ type: (prev) => prev ? "text" : null,
44
+ name: "stripePublishableKey",
45
+ message: "Stripe Publishable Key:",
46
+ initial: ""
47
+ }
48
+ ], {
49
+ onCancel: () => {
50
+ console.log(chalk.yellow("\nSetup cancelled."));
51
+ process.exit(0);
52
+ }
53
+ });
54
+ const config = {
55
+ name: answers.name || "My Store",
56
+ directory: directory || answers.directory || "my-store",
57
+ stripeSecretKey: answers.stripeSecretKey || void 0,
58
+ stripePublishableKey: answers.stripePublishableKey || void 0,
59
+ withDemo: opts.demo !== false,
60
+ port: parseInt(opts.port, 10) || 3010
61
+ };
62
+ await setup(config);
63
+ });
64
+ async function setup(config) {
65
+ const projectDir = resolve(process.cwd(), config.directory);
66
+ if (existsSync(projectDir)) {
67
+ console.log(chalk.red(`
68
+ Directory "${config.directory}" already exists.`));
69
+ process.exit(1);
70
+ }
71
+ console.log(`
72
+ ${chalk.bold("Setting up:")} ${config.name} in ${chalk.cyan(config.directory)}
73
+ `);
74
+ const depSpinner = ora("Checking dependencies...").start();
75
+ await checkDependencies(depSpinner);
76
+ depSpinner.succeed("Dependencies verified (Docker, Node.js, pnpm)");
77
+ const scaffoldSpinner = ora("Scaffolding project...").start();
78
+ scaffoldProject(projectDir, config);
79
+ scaffoldSpinner.succeed("Project scaffolded");
80
+ const dockerSpinner = ora("Starting Docker containers (PostgreSQL + Redis + Inngest)...").start();
81
+ try {
82
+ await execa("docker", ["compose", "up", "-d"], { cwd: projectDir });
83
+ dockerSpinner.succeed("Docker containers running");
84
+ } catch (err) {
85
+ dockerSpinner.fail("Failed to start Docker containers");
86
+ console.log(chalk.yellow(" Make sure Docker is running and try: docker compose up -d"));
87
+ process.exit(1);
88
+ }
89
+ const pgSpinner = ora("Waiting for PostgreSQL to be ready...").start();
90
+ await waitForPostgres(projectDir, pgSpinner);
91
+ pgSpinner.succeed("PostgreSQL is ready");
92
+ const installSpinner = ora("Installing dependencies (this may take a minute)...").start();
93
+ try {
94
+ await execa("pnpm", ["install"], { cwd: projectDir });
95
+ installSpinner.succeed("Dependencies installed");
96
+ } catch {
97
+ installSpinner.fail("Failed to install dependencies");
98
+ process.exit(1);
99
+ }
100
+ const buildSpinner = ora("Building packages...").start();
101
+ try {
102
+ await execa("pnpm", ["build"], { cwd: projectDir });
103
+ buildSpinner.succeed("Packages built");
104
+ } catch {
105
+ buildSpinner.fail("Build failed");
106
+ process.exit(1);
107
+ }
108
+ const schemaSpinner = ora("Pushing database schema...").start();
109
+ try {
110
+ await execa("pnpm", ["db:push"], { cwd: projectDir });
111
+ schemaSpinner.succeed("Database schema applied");
112
+ } catch {
113
+ schemaSpinner.fail("Schema push failed");
114
+ process.exit(1);
115
+ }
116
+ if (config.withDemo) {
117
+ const seedSpinner = ora("Seeding demo data...").start();
118
+ try {
119
+ await execa("pnpm", ["db:seed"], { cwd: projectDir });
120
+ seedSpinner.succeed("Demo data seeded");
121
+ } catch {
122
+ seedSpinner.fail("Seed failed (you can run pnpm db:seed manually)");
123
+ }
124
+ }
125
+ printSuccess(config);
126
+ }
127
+ async function checkDependencies(spinner) {
128
+ try {
129
+ await execa("docker", ["--version"]);
130
+ } catch {
131
+ spinner.fail("Docker not found");
132
+ console.log(chalk.yellow(" Install Docker: https://docs.docker.com/get-docker/"));
133
+ process.exit(1);
134
+ }
135
+ try {
136
+ await execa("docker", ["compose", "version"]);
137
+ } catch {
138
+ spinner.fail("Docker Compose not found");
139
+ console.log(chalk.yellow(" Docker Compose is included with Docker Desktop"));
140
+ process.exit(1);
141
+ }
142
+ try {
143
+ const { stdout } = await execa("node", ["--version"]);
144
+ const major = parseInt(stdout.replace("v", "").split(".")[0], 10);
145
+ if (major < 20) {
146
+ spinner.fail(`Node.js 20+ required (found ${stdout.trim()})`);
147
+ process.exit(1);
148
+ }
149
+ } catch {
150
+ spinner.fail("Node.js not found");
151
+ process.exit(1);
152
+ }
153
+ try {
154
+ await execa("pnpm", ["--version"]);
155
+ } catch {
156
+ spinner.warn("pnpm not found, installing...");
157
+ await execa("npm", ["install", "-g", "pnpm@9"]);
158
+ }
159
+ }
160
+ function scaffoldProject(projectDir, config) {
161
+ mkdirSync(projectDir, { recursive: true });
162
+ const customerTokenSecret = crypto.randomBytes(32).toString("hex");
163
+ const authSecret = crypto.randomBytes(32).toString("hex");
164
+ writeFileSync(join(projectDir, "docker-compose.yml"), `services:
165
+ postgres:
166
+ image: postgres:16-alpine
167
+ container_name: ${config.directory}-postgres
168
+ ports:
169
+ - "5432:5432"
170
+ environment:
171
+ POSTGRES_DB: headless_commerce
172
+ POSTGRES_USER: postgres
173
+ POSTGRES_PASSWORD: postgres
174
+ volumes:
175
+ - postgres_data:/var/lib/postgresql/data
176
+ healthcheck:
177
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
178
+ interval: 5s
179
+ timeout: 5s
180
+ retries: 5
181
+
182
+ redis:
183
+ image: redis:7-alpine
184
+ container_name: ${config.directory}-redis
185
+ ports:
186
+ - "6379:6379"
187
+ volumes:
188
+ - redis_data:/data
189
+ healthcheck:
190
+ test: ["CMD", "redis-cli", "ping"]
191
+ interval: 5s
192
+ timeout: 5s
193
+ retries: 5
194
+
195
+ inngest:
196
+ image: inngest/inngest:latest
197
+ container_name: ${config.directory}-inngest
198
+ ports:
199
+ - "8288:8288"
200
+ environment:
201
+ INNGEST_EVENT_KEY: local-dev-key
202
+ INNGEST_SIGNING_KEY: local-dev-signing-key
203
+ command: inngest dev -u http://host.docker.internal:${config.port}/api/inngest
204
+
205
+ volumes:
206
+ postgres_data:
207
+ redis_data:
208
+ `);
209
+ writeFileSync(join(projectDir, "package.json"), JSON.stringify({
210
+ name: slugify(config.name),
211
+ private: true,
212
+ type: "module",
213
+ packageManager: "pnpm@9.15.0",
214
+ engines: { node: ">=20" },
215
+ scripts: {
216
+ dev: "turbo run dev",
217
+ build: "turbo run build",
218
+ typecheck: "turbo run typecheck",
219
+ "db:push": "pnpm --filter @headless-commerce/db push",
220
+ "db:seed": "pnpm --filter @headless-commerce/db seed",
221
+ "stripe:setup": "tsx apps/api/scripts/stripe-webhook-setup.ts"
222
+ },
223
+ devDependencies: {
224
+ "@types/node": "^20.14.0",
225
+ turbo: "^2.3.0",
226
+ typescript: "^5.5.0"
227
+ }
228
+ }, null, 2) + "\n");
229
+ writeFileSync(join(projectDir, "pnpm-workspace.yaml"), `packages:
230
+ - "apps/*"
231
+ - "packages/*"
232
+ `);
233
+ writeFileSync(join(projectDir, "turbo.json"), JSON.stringify({
234
+ $schema: "https://turbo.build/schema.json",
235
+ globalDependencies: [".env*"],
236
+ pipeline: {
237
+ build: {
238
+ dependsOn: ["^build"],
239
+ outputs: ["dist/**", ".next/**"],
240
+ env: [
241
+ "DATABASE_URL",
242
+ "REDIS_URL",
243
+ "NODE_ENV",
244
+ "PORT",
245
+ "STRIPE_SECRET_KEY",
246
+ "STRIPE_WEBHOOK_SECRET",
247
+ "NEXT_PUBLIC_API_URL",
248
+ "NEXT_PUBLIC_STOREFRONT_API_KEY",
249
+ "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY"
250
+ ]
251
+ },
252
+ dev: { cache: false, persistent: true },
253
+ typecheck: { dependsOn: ["^build"] }
254
+ }
255
+ }, null, 2) + "\n");
256
+ writeFileSync(join(projectDir, ".gitignore"), `node_modules
257
+ dist
258
+ .next
259
+ .env
260
+ .env.local
261
+ *.log
262
+ .turbo
263
+ `);
264
+ const apiEnvDir = join(projectDir, "apps", "api");
265
+ mkdirSync(apiEnvDir, { recursive: true });
266
+ writeFileSync(join(apiEnvDir, ".env"), `# Database
267
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/headless_commerce
268
+
269
+ # Redis
270
+ REDIS_URL=redis://localhost:6379
271
+
272
+ # Inngest
273
+ INNGEST_EVENT_KEY=local-dev-key
274
+ INNGEST_SIGNING_KEY=local-dev-signing-key
275
+
276
+ # Stripe
277
+ STRIPE_SECRET_KEY=${config.stripeSecretKey || "sk_test_xxxxx"}
278
+ STRIPE_WEBHOOK_SECRET=whsec_xxxxx
279
+
280
+ # JWT
281
+ CUSTOMER_TOKEN_SECRET=${customerTokenSecret}
282
+
283
+ # Server
284
+ PORT=${config.port}
285
+ NODE_ENV=development
286
+ `);
287
+ const sfEnvDir = join(projectDir, "apps", "storefront");
288
+ mkdirSync(sfEnvDir, { recursive: true });
289
+ writeFileSync(join(sfEnvDir, ".env.local"), `NEXT_PUBLIC_API_URL=http://localhost:${config.port}/v1
290
+ NEXT_PUBLIC_STOREFRONT_API_KEY=pk_test_your_key_here
291
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${config.stripePublishableKey || "pk_test_xxxxx"}
292
+ CUSTOMER_AUTH_SECRET=${authSecret}
293
+ `);
294
+ const dashEnvDir = join(projectDir, "apps", "dashboard");
295
+ mkdirSync(dashEnvDir, { recursive: true });
296
+ writeFileSync(join(dashEnvDir, ".env.local"), `NEXT_PUBLIC_API_BASE_URL=http://localhost:${config.port}
297
+ NEXTAUTH_URL=http://localhost:3011
298
+ NEXTAUTH_SECRET=${authSecret}
299
+ AUTH_SECRET=${authSecret}
300
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/headless_commerce
301
+ `);
302
+ writeFileSync(join(projectDir, "README.md"), `# ${config.name}
303
+
304
+ Powered by [Headless Commerce](https://github.com/your-repo/headless-commerce).
305
+
306
+ ## Quick Start
307
+
308
+ \`\`\`bash
309
+ # Start infrastructure
310
+ docker compose up -d
311
+
312
+ # Install & build
313
+ pnpm install && pnpm build
314
+
315
+ # Push schema & seed data
316
+ pnpm db:push && pnpm db:seed
317
+
318
+ # Start development servers
319
+ pnpm dev
320
+ \`\`\`
321
+
322
+ ## Ports
323
+
324
+ | Service | URL |
325
+ |---------|-----|
326
+ | API | http://localhost:${config.port} |
327
+ | Dashboard | http://localhost:3011 |
328
+ | Storefront | http://localhost:3012 |
329
+ | Inngest | http://localhost:8288 |
330
+
331
+ ## API Keys
332
+
333
+ After seeding, check the console output for your API keys.
334
+ Use the Dashboard to manage products, orders, and settings.
335
+ `);
336
+ writeFileSync(join(projectDir, "setup.sh"), `#!/bin/bash
337
+ # This script sets up the headless-commerce source code.
338
+ # Run this if you haven't already cloned the source.
339
+
340
+ set -e
341
+
342
+ REPO_URL="https://github.com/your-repo/headless-commerce.git"
343
+
344
+ echo "Cloning headless-commerce..."
345
+ git clone --depth 1 "$REPO_URL" _source
346
+
347
+ echo "Linking packages..."
348
+ cp -r _source/packages ./
349
+ cp -r _source/apps ./
350
+
351
+ echo "Cleaning up..."
352
+ rm -rf _source
353
+
354
+ echo "Done! Run: pnpm install && pnpm build"
355
+ `);
356
+ }
357
+ async function waitForPostgres(projectDir, spinner) {
358
+ for (let i = 0; i < 30; i++) {
359
+ try {
360
+ await execa("docker", ["compose", "exec", "-T", "postgres", "pg_isready", "-U", "postgres"], {
361
+ cwd: projectDir
362
+ });
363
+ return;
364
+ } catch {
365
+ spinner.text = `Waiting for PostgreSQL to be ready... (${i + 1}s)`;
366
+ await new Promise((r) => setTimeout(r, 1e3));
367
+ }
368
+ }
369
+ spinner.fail("PostgreSQL did not become ready in time");
370
+ process.exit(1);
371
+ }
372
+ function printSuccess(config) {
373
+ console.log("");
374
+ console.log(chalk.green.bold(" \u2713 Setup complete!"));
375
+ console.log("");
376
+ console.log(` ${chalk.bold("Next steps:")}`);
377
+ console.log("");
378
+ console.log(` ${chalk.cyan("cd")} ${config.directory}`);
379
+ console.log(` ${chalk.cyan("pnpm dev")}`);
380
+ console.log("");
381
+ console.log(` ${chalk.bold("Services:")}`);
382
+ console.log(` API ${chalk.cyan(`http://localhost:${config.port}`)}`);
383
+ console.log(` Dashboard ${chalk.cyan("http://localhost:3011")}`);
384
+ console.log(` Storefront ${chalk.cyan("http://localhost:3012")}`);
385
+ console.log(` API Docs ${chalk.cyan(`http://localhost:${config.port}/reference`)}`);
386
+ console.log(` Inngest ${chalk.cyan("http://localhost:8288")}`);
387
+ console.log("");
388
+ if (!config.stripeSecretKey) {
389
+ console.log(` ${chalk.yellow("Note:")} Stripe keys not configured.`);
390
+ console.log(` Edit ${chalk.dim(`${config.directory}/apps/api/.env`)} to add your Stripe keys.`);
391
+ console.log("");
392
+ }
393
+ console.log(` ${chalk.dim("Docs:")} https://docs.headlesscommerce.io`);
394
+ console.log("");
395
+ }
396
+ function slugify(text) {
397
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
398
+ }
399
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "create-headless-store",
3
+ "version": "0.1.0",
4
+ "description": "Create a headless commerce store in minutes",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-headless-store": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "typecheck": "tsc --noEmit",
16
+ "dev": "tsx src/index.ts"
17
+ },
18
+ "dependencies": {
19
+ "chalk": "^5.3.0",
20
+ "commander": "^12.1.0",
21
+ "execa": "^9.3.0",
22
+ "ora": "^8.0.1",
23
+ "prompts": "^2.4.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/prompts": "^2.4.9",
27
+ "tsup": "^8.5.0",
28
+ "tsx": "^4.19.0",
29
+ "typescript": "^5.5.0"
30
+ },
31
+ "keywords": [
32
+ "headless-commerce",
33
+ "ecommerce",
34
+ "cli",
35
+ "scaffolding",
36
+ "create"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/Supia7/headless-commerce.git",
42
+ "directory": "packages/create-headless-store"
43
+ }
44
+ }