shipd 0.1.3 → 0.1.4

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 (115) hide show
  1. package/base-package/app/globals.css +126 -0
  2. package/base-package/app/layout.tsx +53 -0
  3. package/base-package/app/page.tsx +15 -0
  4. package/base-package/base.config.json +57 -0
  5. package/base-package/components/ui/avatar.tsx +53 -0
  6. package/base-package/components/ui/badge.tsx +46 -0
  7. package/base-package/components/ui/button.tsx +59 -0
  8. package/base-package/components/ui/card.tsx +92 -0
  9. package/base-package/components/ui/chart.tsx +353 -0
  10. package/base-package/components/ui/checkbox.tsx +32 -0
  11. package/base-package/components/ui/dialog.tsx +135 -0
  12. package/base-package/components/ui/dropdown-menu.tsx +257 -0
  13. package/base-package/components/ui/form.tsx +167 -0
  14. package/base-package/components/ui/input.tsx +21 -0
  15. package/base-package/components/ui/label.tsx +24 -0
  16. package/base-package/components/ui/progress.tsx +31 -0
  17. package/base-package/components/ui/resizable.tsx +56 -0
  18. package/base-package/components/ui/select.tsx +185 -0
  19. package/base-package/components/ui/separator.tsx +28 -0
  20. package/base-package/components/ui/sheet.tsx +139 -0
  21. package/base-package/components/ui/skeleton.tsx +13 -0
  22. package/base-package/components/ui/sonner.tsx +25 -0
  23. package/base-package/components/ui/switch.tsx +31 -0
  24. package/base-package/components/ui/tabs.tsx +66 -0
  25. package/base-package/components/ui/textarea.tsx +18 -0
  26. package/base-package/components/ui/toggle-group.tsx +73 -0
  27. package/base-package/components/ui/toggle.tsx +47 -0
  28. package/base-package/components/ui/tooltip.tsx +61 -0
  29. package/base-package/components.json +21 -0
  30. package/base-package/eslint.config.mjs +16 -0
  31. package/base-package/lib/utils.ts +6 -0
  32. package/base-package/middleware.ts +12 -0
  33. package/base-package/next.config.ts +27 -0
  34. package/base-package/package.json +49 -0
  35. package/base-package/postcss.config.mjs +5 -0
  36. package/base-package/public/favicon.svg +4 -0
  37. package/base-package/tailwind.config.ts +89 -0
  38. package/base-package/tsconfig.json +27 -0
  39. package/dist/index.js +1858 -956
  40. package/features/ai-chat/README.md +258 -0
  41. package/features/ai-chat/app/api/chat/route.ts +16 -0
  42. package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
  43. package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
  44. package/features/ai-chat/feature.config.json +22 -0
  45. package/features/analytics/README.md +308 -0
  46. package/features/analytics/feature.config.json +20 -0
  47. package/features/analytics/lib/posthog.ts +36 -0
  48. package/features/auth/README.md +336 -0
  49. package/features/auth/app/api/auth/[...all]/route.ts +4 -0
  50. package/features/auth/app/dashboard/layout.tsx +15 -0
  51. package/features/auth/app/dashboard/page.tsx +140 -0
  52. package/features/auth/app/sign-in/page.tsx +228 -0
  53. package/features/auth/app/sign-up/page.tsx +243 -0
  54. package/features/auth/auth-schema.ts +47 -0
  55. package/features/auth/components/auth/setup-instructions.tsx +123 -0
  56. package/features/auth/feature.config.json +33 -0
  57. package/features/auth/lib/auth-client.ts +8 -0
  58. package/features/auth/lib/auth.ts +295 -0
  59. package/features/auth/lib/email-stub.ts +55 -0
  60. package/features/auth/lib/email.ts +47 -0
  61. package/features/auth/middleware.patch.ts +43 -0
  62. package/features/database/README.md +256 -0
  63. package/features/database/db/drizzle.ts +48 -0
  64. package/features/database/db/schema.ts +21 -0
  65. package/features/database/drizzle.config.ts +13 -0
  66. package/features/database/feature.config.json +30 -0
  67. package/features/email/README.md +282 -0
  68. package/features/email/emails/components/layout.tsx +181 -0
  69. package/features/email/emails/password-reset.tsx +67 -0
  70. package/features/email/emails/payment-failed.tsx +167 -0
  71. package/features/email/emails/subscription-confirmation.tsx +129 -0
  72. package/features/email/emails/welcome.tsx +100 -0
  73. package/features/email/feature.config.json +22 -0
  74. package/features/email/lib/email.ts +118 -0
  75. package/features/file-upload/README.md +271 -0
  76. package/features/file-upload/app/api/upload-image/route.ts +64 -0
  77. package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
  78. package/features/file-upload/feature.config.json +23 -0
  79. package/features/file-upload/lib/upload-image.ts +28 -0
  80. package/features/marketing-landing/README.md +266 -0
  81. package/features/marketing-landing/app/page.tsx +25 -0
  82. package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
  83. package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
  84. package/features/marketing-landing/components/homepage/footer.tsx +53 -0
  85. package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
  86. package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
  87. package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
  88. package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
  89. package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
  90. package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
  91. package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
  92. package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
  93. package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
  94. package/features/marketing-landing/components/logos/Polar.tsx +7 -0
  95. package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
  96. package/features/marketing-landing/components/logos/index.ts +6 -0
  97. package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
  98. package/features/marketing-landing/feature.config.json +23 -0
  99. package/features/payments/README.md +306 -0
  100. package/features/payments/app/api/subscription/route.ts +25 -0
  101. package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  102. package/features/payments/app/dashboard/payment/page.tsx +126 -0
  103. package/features/payments/app/success/page.tsx +123 -0
  104. package/features/payments/feature.config.json +31 -0
  105. package/features/payments/lib/polar-products.ts +49 -0
  106. package/features/payments/lib/subscription.ts +148 -0
  107. package/features/payments/payments-schema.ts +30 -0
  108. package/features/seo/README.md +244 -0
  109. package/features/seo/app/blog/[slug]/page.tsx +314 -0
  110. package/features/seo/app/blog/page.tsx +107 -0
  111. package/features/seo/app/robots.txt +13 -0
  112. package/features/seo/app/sitemap.ts +70 -0
  113. package/features/seo/feature.config.json +19 -0
  114. package/features/seo/lib/seo-utils.ts +163 -0
  115. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -29,12 +29,27 @@ function validateProjectName(name) {
29
29
  }
30
30
 
31
31
  // src/prompts/index.ts
32
- async function runPrompts(projectName) {
32
+ async function runPrompts(projectName, options) {
33
33
  if (projectName) {
34
34
  console.log(`
35
35
  \u{1F4C1} Project name: ${projectName}`);
36
36
  }
37
- const answers = await inquirer.prompt([
37
+ let selectedFeatures = [];
38
+ if (options?.features) {
39
+ selectedFeatures = options.features.split(",").map((f) => f.trim());
40
+ console.log(`
41
+ \u{1F4E6} Selected features: ${selectedFeatures.join(", ")}`);
42
+ }
43
+ const skipPrompts = projectName && options?.features && options?.description && options?.packageManager;
44
+ if (skipPrompts) {
45
+ return {
46
+ projectName,
47
+ description: options.description,
48
+ features: selectedFeatures,
49
+ packageManager: options.packageManager
50
+ };
51
+ }
52
+ const prompts = [
38
53
  {
39
54
  type: "input",
40
55
  name: "projectName",
@@ -47,41 +62,52 @@ async function runPrompts(projectName) {
47
62
  type: "input",
48
63
  name: "description",
49
64
  message: "Project description:",
50
- default: "A modern SaaS application"
51
- },
52
- {
65
+ default: options?.description || "A modern SaaS application",
66
+ when: !options?.description
67
+ }
68
+ ];
69
+ if (!options?.features) {
70
+ prompts.push({
53
71
  type: "checkbox",
54
72
  name: "features",
55
73
  message: "Select optional features to include:",
56
74
  choices: [
57
- { name: "Polar.sh Payments & Subscriptions", value: "payments", checked: true },
58
- { name: "OpenAI Chat Integration", value: "ai-chat", checked: true },
59
- { name: "Cloudflare R2 File Uploads", value: "file-upload", checked: true },
60
- { name: "PostHog Analytics", value: "analytics", checked: true }
75
+ { name: "Marketing Landing Page", value: "marketing-landing", checked: true },
76
+ { name: "Documentation Pages", value: "docs", checked: false },
77
+ { name: "Database (PostgreSQL + Drizzle)", value: "database", checked: false },
78
+ { name: "Authentication (Better Auth)", value: "auth", checked: false },
79
+ { name: "Payments (Polar.sh)", value: "payments", checked: false },
80
+ { name: "Email (Resend)", value: "email", checked: false },
81
+ { name: "AI Chat (OpenAI)", value: "ai-chat", checked: false },
82
+ { name: "File Upload (Cloudflare R2)", value: "file-upload", checked: false },
83
+ { name: "Analytics (PostHog)", value: "analytics", checked: false },
84
+ { name: "SEO (Sitemap, Robots, Blog)", value: "seo", checked: false }
61
85
  ]
62
- },
63
- {
64
- type: "list",
65
- name: "packageManager",
66
- message: "Package manager:",
67
- choices: ["npm", "pnpm", "yarn"],
68
- default: "npm"
69
- }
70
- ]);
86
+ });
87
+ }
88
+ prompts.push({
89
+ type: "list",
90
+ name: "packageManager",
91
+ message: "Package manager:",
92
+ choices: ["npm", "pnpm", "yarn"],
93
+ default: options?.packageManager || "npm",
94
+ when: !options?.packageManager
95
+ });
96
+ const answers = await inquirer.prompt(prompts);
71
97
  return {
72
98
  projectName: projectName || answers.projectName,
73
- description: answers.description,
74
- features: ["auth", "database", ...answers.features],
75
- // Always include auth & database
76
- packageManager: answers.packageManager
99
+ description: options?.description || answers.description,
100
+ features: options?.features ? selectedFeatures : answers.features || [],
101
+ packageManager: options?.packageManager || answers.packageManager
77
102
  };
78
103
  }
79
104
 
80
105
  // src/generators/project.ts
81
- import { resolve as resolve3 } from "path";
82
- import ora from "ora";
83
- import { execa } from "execa";
84
- import fs from "fs-extra";
106
+ import { resolve as resolve4, dirname as dirname3 } from "path";
107
+ import { fileURLToPath as fileURLToPath3 } from "url";
108
+ import ora2 from "ora";
109
+ import { execa as execa2 } from "execa";
110
+ import fs2 from "fs-extra";
85
111
 
86
112
  // src/utils/template-config.ts
87
113
  import { readFile } from "fs/promises";
@@ -90,56 +116,25 @@ import { join, resolve as resolve2, dirname } from "path";
90
116
  import { fileURLToPath } from "url";
91
117
  var __filename = fileURLToPath(import.meta.url);
92
118
  var __dirname = dirname(__filename);
93
- function getTemplatePath() {
94
- const triedPaths = [];
95
- const bundledPath = resolve2(__dirname, "../template");
96
- triedPaths.push(bundledPath);
97
- if (existsSync2(bundledPath)) {
98
- return bundledPath;
99
- }
100
- const distPath = resolve2(__dirname, "../../template");
101
- triedPaths.push(distPath);
102
- if (existsSync2(distPath)) {
103
- return distPath;
104
- }
105
- const srcPath = resolve2(__dirname, "../../../template");
106
- triedPaths.push(srcPath);
107
- if (existsSync2(srcPath)) {
108
- return srcPath;
109
- }
110
- let currentDir = __dirname;
111
- for (let i = 0; i < 10; i++) {
112
- const pkgJsonPath = join(currentDir, "package.json");
113
- if (existsSync2(pkgJsonPath)) {
114
- const templatePath = join(currentDir, "template");
115
- triedPaths.push(templatePath);
116
- if (existsSync2(templatePath)) {
117
- return templatePath;
118
- }
119
- break;
120
- }
121
- const parentDir = resolve2(currentDir, "..");
122
- if (parentDir === currentDir) {
123
- break;
124
- }
125
- currentDir = parentDir;
126
- }
127
- const absolutePath = resolve2(__dirname, "../../../../packages/template");
128
- triedPaths.push(absolutePath);
129
- if (existsSync2(absolutePath)) {
130
- return absolutePath;
119
+ function getBasePath() {
120
+ const candidates = [
121
+ // bundled: dist/utils -> ../base-package
122
+ resolve2(__dirname, "../base-package"),
123
+ // bundled alt: dist/utils -> ../../base-package
124
+ resolve2(__dirname, "../../base-package"),
125
+ // source: src/utils -> ../../base-package
126
+ resolve2(__dirname, "../../../base-package"),
127
+ // monorepo absolute fallback
128
+ resolve2(__dirname, "../../../../packages/cli/base-package")
129
+ ];
130
+ const found = candidates.find((p) => existsSync2(p));
131
+ if (!found) {
132
+ throw new Error(
133
+ `Base package directory not found. Tried paths:
134
+ ${candidates.map((p) => ` - ${p}`).join("\n")}`
135
+ );
131
136
  }
132
- throw new Error(`Template directory not found. Tried paths:
133
- ${triedPaths.map((p) => ` - ${p}`).join("\n")}`);
134
- }
135
- async function loadTemplateConfig() {
136
- const templatePath = getTemplatePath();
137
- const configPath = join(templatePath, "template.config.json");
138
- if (!existsSync2(configPath)) {
139
- throw new Error(`template.config.json not found at ${configPath}`);
140
- }
141
- const content = await readFile(configPath, "utf-8");
142
- return JSON.parse(content);
137
+ return found;
143
138
  }
144
139
 
145
140
  // src/utils/file-system.ts
@@ -175,85 +170,6 @@ function shouldExclude(filePath, patterns) {
175
170
  return regex.test(filePath);
176
171
  });
177
172
  }
178
- async function removeFeatureFiles(targetPath, featureFiles) {
179
- for (const filePattern of featureFiles) {
180
- const files = await globby(filePattern, {
181
- cwd: targetPath,
182
- absolute: true,
183
- dot: true
184
- });
185
- for (const file of files) {
186
- await rm(file, { recursive: true, force: true });
187
- }
188
- }
189
- }
190
- async function removeEmptyDirectories(dirPath) {
191
- if (!existsSync3(dirPath)) {
192
- return;
193
- }
194
- const entries = await readdir(dirPath, { withFileTypes: true });
195
- for (const entry of entries) {
196
- if (entry.isDirectory()) {
197
- const fullPath = join2(dirPath, entry.name);
198
- await removeEmptyDirectories(fullPath);
199
- }
200
- }
201
- const remaining = await readdir(dirPath);
202
- if (remaining.length === 0) {
203
- await rm(dirPath, { recursive: true, force: true });
204
- }
205
- }
206
- async function removeConditionalBlocks(targetPath, unselectedFeatures) {
207
- const featureMarkers = {
208
- "ai-chat": "AI_CHAT",
209
- "payments": "PAYMENTS",
210
- "file-upload": "FILE_UPLOAD",
211
- "analytics": "ANALYTICS"
212
- };
213
- const filesToCheck = await globby("**/*.{ts,tsx,js,jsx}", {
214
- cwd: targetPath,
215
- absolute: true,
216
- ignore: ["**/node_modules/**", "**/.next/**"]
217
- });
218
- for (const filePath of filesToCheck) {
219
- let content = await readFile2(filePath, "utf-8");
220
- let modified = false;
221
- for (const feature of unselectedFeatures) {
222
- const marker = featureMarkers[feature];
223
- if (!marker) continue;
224
- const blockRegex = new RegExp(
225
- `\\/\\*${marker}_START\\*\\/[\\s\\S]*?\\/\\*${marker}_END\\*\\/\\n?`,
226
- "g"
227
- );
228
- const jsxBlockRegex = new RegExp(
229
- `\\{\\s*\\/\\*${marker}_START\\*\\/\\s*\\}[\\s\\S]*?\\{\\s*\\/\\*${marker}_END\\*\\/\\s*\\}\\n?`,
230
- "g"
231
- );
232
- if (blockRegex.test(content) || jsxBlockRegex.test(content)) {
233
- content = content.replace(blockRegex, "");
234
- content = content.replace(jsxBlockRegex, "");
235
- modified = true;
236
- }
237
- }
238
- if (modified) {
239
- await writeFile(filePath, content, "utf-8");
240
- }
241
- }
242
- }
243
- async function removeUnselectedFeatures(targetPath, selectedFeatures, templateConfig) {
244
- const allFeatures = Object.keys(templateConfig.features);
245
- const unselectedFeatures = allFeatures.filter(
246
- (f) => !selectedFeatures.includes(f) && templateConfig.features[f].optional
247
- );
248
- await removeConditionalBlocks(targetPath, unselectedFeatures);
249
- for (const feature of unselectedFeatures) {
250
- const featureConfig = templateConfig.features[feature];
251
- if (featureConfig.files.length > 0) {
252
- await removeFeatureFiles(targetPath, featureConfig.files);
253
- }
254
- }
255
- await removeEmptyDirectories(targetPath);
256
- }
257
173
 
258
174
  // src/transformers/template-vars.ts
259
175
  import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
@@ -291,6 +207,7 @@ async function replaceTemplateVariables(targetPath, config) {
291
207
  "package.json",
292
208
  "README.md",
293
209
  "app/layout.tsx",
210
+ "app/page.tsx",
294
211
  "app/sign-in/page.tsx",
295
212
  "app/sign-up/page.tsx",
296
213
  ".env.example"
@@ -301,238 +218,6 @@ async function replaceTemplateVariables(targetPath, config) {
301
218
  }
302
219
  }
303
220
 
304
- // src/generators/package-json.ts
305
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
306
- import { join as join4 } from "path";
307
- async function generatePackageJson(targetPath, selectedFeatures, templateConfig) {
308
- const packageJsonPath = join4(targetPath, "package.json");
309
- const packageJson = JSON.parse(await readFile4(packageJsonPath, "utf-8"));
310
- const templatePackageJsonPath = join4(getTemplatePath(), "package.json");
311
- const templatePackageJson = JSON.parse(await readFile4(templatePackageJsonPath, "utf-8"));
312
- const dependencies = {};
313
- const devDependencies = {};
314
- const coreDeps = [
315
- "next",
316
- "react",
317
- "react-dom",
318
- "typescript",
319
- "tailwindcss",
320
- "dotenv",
321
- "@radix-ui/react-avatar",
322
- "@radix-ui/react-slot",
323
- "@radix-ui/react-dialog",
324
- "@radix-ui/react-dropdown-menu",
325
- "@radix-ui/react-label",
326
- "@radix-ui/react-tabs",
327
- "@radix-ui/react-separator",
328
- "@radix-ui/react-checkbox",
329
- "@radix-ui/react-switch",
330
- "class-variance-authority",
331
- "clsx",
332
- "tailwind-merge",
333
- "lucide-react",
334
- "next-themes",
335
- "sonner",
336
- "tailwindcss-animate",
337
- "framer-motion",
338
- "motion",
339
- "@vercel/analytics",
340
- "zod",
341
- "react-hook-form",
342
- "@hookform/resolvers",
343
- "drizzle-orm",
344
- "postgres",
345
- "better-auth",
346
- "@polar-sh/better-auth",
347
- "@polar-sh/sdk",
348
- "@neondatabase/serverless",
349
- "resend",
350
- "@react-email/components",
351
- "@react-email/render"
352
- ];
353
- const coreDevDeps = [
354
- "@types/node",
355
- "@types/react",
356
- "@types/react-dom",
357
- "eslint",
358
- "eslint-config-next",
359
- "@eslint/eslintrc",
360
- "drizzle-kit",
361
- "@tailwindcss/postcss",
362
- "tw-animate-css"
363
- ];
364
- coreDeps.forEach((dep) => {
365
- if (templatePackageJson.dependencies?.[dep]) {
366
- dependencies[dep] = templatePackageJson.dependencies[dep];
367
- }
368
- });
369
- coreDevDeps.forEach((dep) => {
370
- if (templatePackageJson.devDependencies?.[dep]) {
371
- devDependencies[dep] = templatePackageJson.devDependencies[dep];
372
- }
373
- });
374
- for (const feature of selectedFeatures) {
375
- const featureConfig = templateConfig.features[feature];
376
- if (featureConfig) {
377
- for (const dep of featureConfig.dependencies || []) {
378
- if (templatePackageJson.dependencies?.[dep]) {
379
- dependencies[dep] = templatePackageJson.dependencies[dep];
380
- }
381
- }
382
- for (const dep of featureConfig.devDependencies || []) {
383
- if (templatePackageJson.devDependencies?.[dep]) {
384
- devDependencies[dep] = templatePackageJson.devDependencies[dep];
385
- }
386
- }
387
- }
388
- }
389
- const sortedDeps = Object.keys(dependencies).sort().reduce((acc, key) => {
390
- acc[key] = dependencies[key];
391
- return acc;
392
- }, {});
393
- const sortedDevDeps = Object.keys(devDependencies).sort().reduce((acc, key) => {
394
- acc[key] = devDependencies[key];
395
- return acc;
396
- }, {});
397
- packageJson.dependencies = sortedDeps;
398
- packageJson.devDependencies = sortedDevDeps;
399
- await writeFile3(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
400
- }
401
-
402
- // src/generators/env.ts
403
- import { writeFile as writeFile4 } from "fs/promises";
404
- import { join as join5 } from "path";
405
- var ENV_DEFAULTS = {
406
- // Database
407
- "DATABASE_URL": "postgresql://user:password@localhost:5432/mydb",
408
- // Auth
409
- "BETTER_AUTH_SECRET": "please-change-this-to-a-random-32-character-string-or-longer",
410
- "GOOGLE_CLIENT_ID": "your-google-client-id.apps.googleusercontent.com",
411
- "GOOGLE_CLIENT_SECRET": "your-google-client-secret",
412
- // Polar.sh Payments
413
- "POLAR_ACCESS_TOKEN": "your-polar-access-token",
414
- "POLAR_SUCCESS_URL": "http://localhost:3000/success",
415
- "POLAR_WEBHOOK_SECRET": "your-polar-webhook-secret",
416
- "NEXT_PUBLIC_STARTER_TIER": "your-starter-tier-id",
417
- "NEXT_PUBLIC_STARTER_SLUG": "your-organization-slug",
418
- // OpenAI
419
- "OPENAI_API_KEY": "sk-your-openai-api-key",
420
- // Cloudflare R2
421
- "R2_UPLOAD_IMAGE_ACCESS_KEY_ID": "your-r2-access-key-id",
422
- "R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY": "your-r2-secret-access-key",
423
- "CLOUDFLARE_ACCOUNT_ID": "your-cloudflare-account-id",
424
- "R2_UPLOAD_IMAGE_BUCKET_NAME": "your-bucket-name",
425
- // PostHog
426
- "NEXT_PUBLIC_POSTHOG_KEY": "phc_your-posthog-project-key",
427
- "NEXT_PUBLIC_POSTHOG_HOST": "https://app.posthog.com"
428
- };
429
- async function generateEnvExample(targetPath, selectedFeatures, templateConfig) {
430
- const lines = [];
431
- lines.push("# Environment Variables");
432
- lines.push("# Copy this file to .env.local and customize the values");
433
- lines.push("");
434
- lines.push("# \u26A0\uFE0F REQUIRED FOR BASIC FUNCTIONALITY:");
435
- lines.push("# - DATABASE_URL (for data storage)");
436
- lines.push("# - BETTER_AUTH_SECRET (for authentication)");
437
- lines.push("# - GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET (for Google login)");
438
- lines.push("");
439
- lines.push("# \u2139\uFE0F OPTIONAL SERVICES:");
440
- lines.push("# - POLAR_* variables (only needed for subscription payments)");
441
- lines.push("# - OPENAI_API_KEY (only needed for AI chat features)");
442
- lines.push("# - R2_* variables (only needed for file uploads)");
443
- lines.push("# - POSTHOG_* variables (only needed for analytics)");
444
- lines.push("");
445
- lines.push("# The app will launch with these placeholder values, but configure real services for production");
446
- lines.push("");
447
- lines.push("# Application");
448
- lines.push("NEXT_PUBLIC_APP_URL=http://localhost:3000");
449
- lines.push("");
450
- for (const feature of selectedFeatures) {
451
- const featureConfig = templateConfig.features[feature];
452
- if (featureConfig && featureConfig.envVars.length > 0) {
453
- const featureName = feature.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
454
- const isOptional = !featureConfig.required;
455
- const requirementNote = isOptional ? " (Optional - only needed if using this feature)" : " (Required)";
456
- lines.push(`# ${featureName}${requirementNote}`);
457
- featureConfig.envVars.forEach((envVar) => {
458
- const defaultValue = ENV_DEFAULTS[envVar] || "";
459
- lines.push(`${envVar}=${defaultValue}`);
460
- });
461
- lines.push("");
462
- }
463
- }
464
- await writeFile4(
465
- join5(targetPath, ".env.example"),
466
- lines.join("\n")
467
- );
468
- }
469
-
470
- // src/generators/readme.ts
471
- import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
472
- import { join as join6 } from "path";
473
- async function generateReadme(targetPath, config, templateConfig) {
474
- const readmePath = join6(targetPath, "README.md");
475
- let readme = await readFile5(readmePath, "utf-8");
476
- const featureSections = [];
477
- if (config.features.includes("payments")) {
478
- featureSections.push(`
479
- ### Polar.sh Setup (Payments)
480
- 1. Create account at [polar.sh](https://polar.sh)
481
- 2. Create organization and products/subscription tiers
482
- 3. Get your access token from Settings \u2192 API
483
- 4. Add to \`.env.local\`:
484
- - \`POLAR_ACCESS_TOKEN\`
485
- - \`POLAR_WEBHOOK_SECRET\`
486
- - \`NEXT_PUBLIC_STARTER_TIER\`
487
- - \`NEXT_PUBLIC_STARTER_SLUG\`
488
- `);
489
- }
490
- if (config.features.includes("ai-chat")) {
491
- featureSections.push(`
492
- ### OpenAI Setup (AI Chat)
493
- 1. Get API key from [platform.openai.com](https://platform.openai.com)
494
- 2. Add to \`.env.local\`:
495
- - \`OPENAI_API_KEY\`
496
- `);
497
- }
498
- if (config.features.includes("file-upload")) {
499
- featureSections.push(`
500
- ### Cloudflare R2 Setup (File Uploads)
501
- 1. Create R2 bucket in Cloudflare dashboard
502
- 2. Generate API tokens with R2 permissions
503
- 3. Add to \`.env.local\`:
504
- - \`R2_UPLOAD_IMAGE_ACCESS_KEY_ID\`
505
- - \`R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY\`
506
- - \`CLOUDFLARE_ACCOUNT_ID\`
507
- - \`R2_UPLOAD_IMAGE_BUCKET_NAME\`
508
- `);
509
- }
510
- if (config.features.includes("analytics")) {
511
- featureSections.push(`
512
- ### PostHog Setup (Analytics)
513
- 1. Create account at [posthog.com](https://posthog.com)
514
- 2. Create a new project
515
- 3. Add to \`.env.local\`:
516
- - \`NEXT_PUBLIC_POSTHOG_KEY\`
517
- - \`NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\`
518
- `);
519
- }
520
- if (featureSections.length > 0) {
521
- const setupSection = `
522
-
523
- ## \u{1F527} Service Configuration
524
- ${featureSections.join("\n")}`;
525
- readme += setupSection;
526
- }
527
- readme += `
528
-
529
- ---
530
-
531
- **Generated with** [Shipd](https://github.com/yourusername/shipd)
532
- `;
533
- await writeFile5(readmePath, readme);
534
- }
535
-
536
221
  // src/utils/logger.ts
537
222
  import chalk from "chalk";
538
223
  var SHIPD_ASCII = chalk.hex("#ff7043")(` # # ( )
@@ -573,162 +258,1707 @@ var logger = {
573
258
  }
574
259
  };
575
260
 
576
- // src/generators/project.ts
577
- async function generateProject(config) {
578
- const targetPath = resolve3(process.cwd(), config.projectName);
579
- if (await fs.pathExists(targetPath)) {
580
- const files = await fs.readdir(targetPath);
581
- if (files.length > 0) {
582
- const hasPkgJson = await fs.pathExists(resolve3(targetPath, "package.json"));
583
- const hasGit = await fs.pathExists(resolve3(targetPath, ".git"));
584
- const hasNodeModules = await fs.pathExists(resolve3(targetPath, "node_modules"));
585
- if (hasPkgJson || hasGit || hasNodeModules) {
586
- throw new Error(
587
- `Directory "${config.projectName}" appears to contain an existing project.
588
- - package.json: ${hasPkgJson ? "\u2713 found" : "\u2717 not found"}
589
- - .git: ${hasGit ? "\u2713 found" : "\u2717 not found"}
590
- - node_modules: ${hasNodeModules ? "\u2713 found" : "\u2717 not found"}
261
+ // src/commands/append.ts
262
+ import inquirer2 from "inquirer";
263
+ import { existsSync as existsSync6 } from "fs";
264
+ import { join as join5, resolve as resolve3, dirname as dirname2 } from "path";
265
+ import { fileURLToPath as fileURLToPath2 } from "url";
266
+ import ora from "ora";
267
+ import fs from "fs-extra";
268
+ import { execa } from "execa";
591
269
 
592
- To avoid data loss, shipd init will not overwrite existing projects.
593
- Please:
594
- 1. Choose a different project name, or
595
- 2. Delete the existing directory first, or
596
- 3. Use 'shipd append' to add features to the existing project`
597
- );
598
- }
599
- logger.warn(`Directory "${config.projectName}" is not empty but doesn't appear to be a project.`);
600
- logger.warn("Existing files may be overwritten. Press Ctrl+C to cancel or wait 3 seconds to continue...");
601
- await new Promise((resolve5) => setTimeout(resolve5, 3e3));
270
+ // src/utils/env-vars.ts
271
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
272
+ import { existsSync as existsSync5 } from "fs";
273
+ import { join as join4 } from "path";
274
+ var ENV_DEFAULTS = {
275
+ // Database
276
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/mydb",
277
+ // Auth
278
+ "BETTER_AUTH_SECRET": "please-change-this-to-a-random-32-character-string-or-longer",
279
+ "GOOGLE_CLIENT_ID": "your-google-client-id.apps.googleusercontent.com",
280
+ "GOOGLE_CLIENT_SECRET": "your-google-client-secret",
281
+ "NEXT_PUBLIC_APP_URL": "http://localhost:3000",
282
+ // Polar.sh Payments
283
+ "POLAR_ACCESS_TOKEN": "your-polar-access-token",
284
+ "POLAR_SUCCESS_URL": "success",
285
+ "POLAR_WEBHOOK_SECRET": "your-polar-webhook-secret",
286
+ "NEXT_PUBLIC_STARTER_TIER": "your-starter-tier-id",
287
+ "NEXT_PUBLIC_STARTER_SLUG": "your-organization-slug",
288
+ // Email (Resend)
289
+ "RESEND_API_KEY": "re_xxxxxxxxxxxxx",
290
+ "EMAIL_FROM": "Your App <noreply@yourdomain.com>",
291
+ // OpenAI
292
+ "OPENAI_API_KEY": "sk-your-openai-api-key",
293
+ // Cloudflare R2
294
+ "R2_UPLOAD_IMAGE_ACCESS_KEY_ID": "your-r2-access-key-id",
295
+ "R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY": "your-r2-secret-access-key",
296
+ "CLOUDFLARE_ACCOUNT_ID": "your-cloudflare-account-id",
297
+ "R2_UPLOAD_IMAGE_BUCKET_NAME": "your-bucket-name",
298
+ // PostHog
299
+ "NEXT_PUBLIC_POSTHOG_KEY": "phc_your-posthog-project-key",
300
+ "NEXT_PUBLIC_POSTHOG_HOST": "https://app.posthog.com"
301
+ };
302
+ var FEATURE_NAMES = {
303
+ "database": "Database",
304
+ "auth": "Authentication",
305
+ "payments": "Payments (Polar.sh)",
306
+ "email": "Email (Resend)",
307
+ "docs": "Documentation",
308
+ "marketing-landing": "Marketing Landing"
309
+ };
310
+ async function addEnvVarsToExample(targetPath, featureName, envVars) {
311
+ if (envVars.length === 0) {
312
+ return;
313
+ }
314
+ const envExamplePath = join4(targetPath, ".env.example");
315
+ let existingContent = "";
316
+ if (existsSync5(envExamplePath)) {
317
+ existingContent = await readFile4(envExamplePath, "utf-8");
318
+ } else {
319
+ existingContent = `# Environment Variables
320
+ # Copy this file to .env.local and customize the values
321
+
322
+ # Application
323
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
324
+
325
+ `;
326
+ }
327
+ const existingVars = /* @__PURE__ */ new Set();
328
+ const lines = existingContent.split("\n");
329
+ for (const line of lines) {
330
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
331
+ if (match) {
332
+ existingVars.add(match[1]);
602
333
  }
603
334
  }
604
- const templateConfig = await loadTemplateConfig();
605
- const templatePath = getTemplatePath();
606
- const copySpinner = ora("Copying template files...").start();
607
- try {
608
- await copyTemplate(templatePath, targetPath);
609
- copySpinner.succeed("Template files copied");
610
- } catch (error) {
611
- copySpinner.fail("Failed to copy template files");
612
- throw error;
335
+ const newVars = envVars.filter((v) => !existingVars.has(v));
336
+ if (newVars.length === 0) {
337
+ return;
613
338
  }
614
- const removeSpinner = ora("Removing unselected features...").start();
615
- try {
616
- await removeUnselectedFeatures(targetPath, config.features, templateConfig);
617
- removeSpinner.succeed("Features filtered");
618
- } catch (error) {
619
- removeSpinner.fail("Failed to remove feature files");
620
- throw error;
339
+ const displayName = FEATURE_NAMES[featureName] || featureName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
340
+ const newSection = [];
341
+ newSection.push(`# ${displayName}`);
342
+ for (const envVar of newVars) {
343
+ const defaultValue = ENV_DEFAULTS[envVar] || "";
344
+ newSection.push(`${envVar}=${defaultValue}`);
621
345
  }
622
- const varsSpinner = ora("Replacing template variables...").start();
623
- try {
624
- await replaceTemplateVariables(targetPath, config);
625
- varsSpinner.succeed("Variables replaced");
626
- } catch (error) {
627
- varsSpinner.fail("Failed to replace variables");
628
- throw error;
346
+ newSection.push("");
347
+ const updatedContent = existingContent.trimEnd() + "\n\n" + newSection.join("\n");
348
+ await writeFile3(envExamplePath, updatedContent, "utf-8");
349
+ }
350
+
351
+ // src/commands/append.ts
352
+ var __filename2 = fileURLToPath2(import.meta.url);
353
+ var __dirname2 = dirname2(__filename2);
354
+ async function detectProject(cwd) {
355
+ const pkgPath = join5(cwd, "package.json");
356
+ if (!existsSync6(pkgPath)) {
357
+ throw new Error("No package.json found. Make sure you're in a Next.js project directory.");
629
358
  }
630
- const pkgSpinner = ora("Generating package.json...").start();
631
- try {
632
- await generatePackageJson(targetPath, config.features, templateConfig);
633
- pkgSpinner.succeed("package.json generated");
634
- } catch (error) {
635
- pkgSpinner.fail("Failed to generate package.json");
636
- throw error;
359
+ const pkg = await fs.readJson(pkgPath);
360
+ const isNextJs = !!(pkg.dependencies?.next || pkg.devDependencies?.next);
361
+ if (!isNextJs) {
362
+ throw new Error("This doesn't appear to be a Next.js project. shipd append requires a Next.js project.");
637
363
  }
638
- const envSpinner = ora("Generating .env.example...").start();
639
- try {
640
- await generateEnvExample(targetPath, config.features, templateConfig);
641
- envSpinner.succeed(".env.example generated");
642
- } catch (error) {
643
- envSpinner.fail("Failed to generate .env.example");
644
- throw error;
364
+ const hasAppRouter = existsSync6(join5(cwd, "app"));
365
+ if (!hasAppRouter) {
366
+ throw new Error("App Router not detected. shipd append requires Next.js App Router (app directory).");
645
367
  }
646
- const readmeSpinner = ora("Updating README...").start();
647
- try {
648
- await generateReadme(targetPath, config, templateConfig);
649
- readmeSpinner.succeed("README updated");
650
- } catch (error) {
651
- readmeSpinner.fail("Failed to update README");
652
- throw error;
368
+ let packageManager = "npm";
369
+ if (existsSync6(join5(cwd, "pnpm-lock.yaml"))) {
370
+ packageManager = "pnpm";
371
+ } else if (existsSync6(join5(cwd, "yarn.lock"))) {
372
+ packageManager = "yarn";
653
373
  }
654
- const gitSpinner = ora("Initializing git repository...").start();
655
- try {
656
- await execa("git", ["init"], { cwd: targetPath });
657
- await execa("git", ["add", "."], { cwd: targetPath });
658
- await execa("git", ["commit", "-m", "Initial commit from Shipd"], { cwd: targetPath });
659
- gitSpinner.succeed("Git repository initialized");
660
- } catch (error) {
661
- gitSpinner.fail("Failed to initialize git");
662
- throw error;
374
+ const installedFeatures = [];
375
+ const shipdManifest = join5(cwd, ".shipd", "manifest.json");
376
+ if (existsSync6(shipdManifest)) {
377
+ const manifest = await fs.readJson(shipdManifest);
378
+ installedFeatures.push(...Object.keys(manifest.features || {}));
663
379
  }
664
- let installInterval;
665
- const installSpinner = ora("Preparing to install dependencies...").start();
380
+ return {
381
+ isNextJs,
382
+ hasAppRouter,
383
+ packageManager,
384
+ installedFeatures
385
+ };
386
+ }
387
+ async function appendDocsFeature(targetPath, dryRun = false) {
388
+ const docsSourceCandidates = [
389
+ resolve3(__dirname2, "../docs-template"),
390
+ resolve3(__dirname2, "../../docs-template"),
391
+ resolve3(__dirname2, "../../../docs-template")
392
+ ];
393
+ const docsSourcePath = docsSourceCandidates.find((p) => existsSync6(p));
394
+ if (!docsSourcePath) {
395
+ throw new Error(
396
+ `Docs template not found. Tried paths:
397
+ ${docsSourceCandidates.map((p) => ` - ${p}`).join("\n")}`
398
+ );
399
+ }
400
+ const spinner = ora("Copying docs feature (pages + components)...").start();
666
401
  try {
667
- const pkgJson = await fs.readJson(resolve3(targetPath, "package.json"));
668
- const deps = Object.keys(pkgJson.dependencies || {}).length;
669
- const devDeps = Object.keys(pkgJson.devDependencies || {}).length;
670
- const totalDeps = deps + devDeps;
671
- installSpinner.text = `Installing ${totalDeps} dependencies with ${config.packageManager}...
672
- This may take 2-5 minutes depending on your connection`;
673
- const startTime = Date.now();
674
- installInterval = setInterval(() => {
675
- const elapsed = Math.floor((Date.now() - startTime) / 1e3);
676
- installSpinner.text = `Installing ${totalDeps} dependencies (${elapsed}s elapsed)...
677
- ${config.packageManager} install is running in background`;
678
- }, 2e3);
679
- await execa(config.packageManager, ["install"], {
680
- cwd: targetPath,
681
- stdio: "pipe"
682
- });
683
- clearInterval(installInterval);
684
- const totalTime = Math.floor((Date.now() - startTime) / 1e3);
685
- installSpinner.succeed(`Dependencies installed (${totalDeps} packages in ${totalTime}s)`);
686
- } catch (error) {
687
- if (installInterval) {
688
- clearInterval(installInterval);
402
+ if (dryRun) {
403
+ spinner.info("DRY RUN: Would copy docs pages and components from template");
404
+ return;
689
405
  }
690
- installSpinner.fail("Failed to install dependencies");
691
- throw error;
692
- }
693
- logger.success(`
694
- Project "${config.projectName}" created successfully at:
695
- ${targetPath}
696
- `);
406
+ const docsTargetPath = join5(targetPath, "app", "docs");
407
+ await fs.ensureDir(docsTargetPath);
408
+ const pagesToCopy = [
409
+ "page.tsx",
410
+ "layout.tsx",
411
+ "[slug]",
412
+ "api",
413
+ "documentation"
414
+ ];
415
+ for (const item of pagesToCopy) {
416
+ const sourceItem = join5(docsSourcePath, item);
417
+ const targetItem = join5(docsTargetPath, item);
418
+ if (existsSync6(sourceItem)) {
419
+ const stats = await fs.stat(sourceItem);
420
+ if (stats.isDirectory()) {
421
+ await fs.ensureDir(targetItem);
422
+ await fs.copy(sourceItem, targetItem, {
423
+ overwrite: false,
424
+ errorOnExist: false
425
+ });
426
+ } else {
427
+ if (!existsSync6(targetItem)) {
428
+ await fs.copyFile(sourceItem, targetItem);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ const componentsSourcePath = join5(docsSourcePath, "components", "docs");
434
+ const componentsTargetPath = join5(targetPath, "components", "docs");
435
+ if (existsSync6(componentsSourcePath)) {
436
+ await fs.ensureDir(componentsTargetPath);
437
+ await fs.copy(componentsSourcePath, componentsTargetPath, {
438
+ overwrite: false,
439
+ errorOnExist: false
440
+ });
441
+ }
442
+ const uiComponentsSourcePath = join5(docsSourcePath, "components", "ui");
443
+ const uiComponentsTargetPath = join5(targetPath, "components", "ui");
444
+ if (existsSync6(uiComponentsSourcePath)) {
445
+ await fs.ensureDir(uiComponentsTargetPath);
446
+ await fs.copy(uiComponentsSourcePath, uiComponentsTargetPath, {
447
+ overwrite: false,
448
+ errorOnExist: false,
449
+ filter: (src) => {
450
+ const relativePath = src.replace(uiComponentsSourcePath, "");
451
+ const targetFile = join5(uiComponentsTargetPath, relativePath);
452
+ if (existsSync6(targetFile)) {
453
+ return false;
454
+ }
455
+ return true;
456
+ }
457
+ });
458
+ }
459
+ const utilsSourcePath = join5(docsSourcePath, "lib", "utils.ts");
460
+ const utilsTargetPath = join5(targetPath, "lib", "utils.ts");
461
+ if (existsSync6(utilsSourcePath) && !existsSync6(utilsTargetPath)) {
462
+ await fs.ensureDir(join5(targetPath, "lib"));
463
+ await fs.copy(utilsSourcePath, utilsTargetPath);
464
+ } else if (existsSync6(utilsTargetPath)) {
465
+ }
466
+ spinner.succeed("Docs feature copied (pages + components)");
467
+ const shipdDir = join5(targetPath, ".shipd");
468
+ await fs.ensureDir(shipdDir);
469
+ const manifestPath = join5(shipdDir, "manifest.json");
470
+ let manifest = { version: "1.0.0", features: {} };
471
+ if (existsSync6(manifestPath)) {
472
+ manifest = await fs.readJson(manifestPath);
473
+ }
474
+ manifest.features.docs = {
475
+ version: "1.0.0",
476
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
477
+ files: [
478
+ "app/docs/**/*",
479
+ "components/docs/**/*",
480
+ "components/ui/**/*",
481
+ "lib/utils.ts"
482
+ ]
483
+ };
484
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
485
+ const pkgPath = join5(targetPath, "package.json");
486
+ if (existsSync6(pkgPath)) {
487
+ const pkg = await fs.readJson(pkgPath);
488
+ let updated = false;
489
+ const requiredDeps = {
490
+ "@radix-ui/react-slot": "^1.1.0",
491
+ "@radix-ui/react-dialog": "^1.1.0",
492
+ "class-variance-authority": "^0.7.0",
493
+ "clsx": "^2.1.1",
494
+ "tailwind-merge": "^2.5.4",
495
+ "lucide-react": "^0.469.0",
496
+ "react-markdown": "^9.0.0",
497
+ "remark-gfm": "^4.0.0"
498
+ };
499
+ if (!pkg.dependencies) {
500
+ pkg.dependencies = {};
501
+ }
502
+ for (const [dep, version] of Object.entries(requiredDeps)) {
503
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
504
+ pkg.dependencies[dep] = version;
505
+ updated = true;
506
+ }
507
+ }
508
+ if (updated) {
509
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
510
+ logger.info("Added missing dependencies to package.json");
511
+ logger.info("Run npm install (or your package manager) to install them");
512
+ }
513
+ }
514
+ const featureDocs = join5(shipdDir, "features");
515
+ await fs.ensureDir(featureDocs);
516
+ const docsReadme = `# Docs Feature - Integration Guide
517
+
518
+ **Installed:** ${(/* @__PURE__ */ new Date()).toISOString()}
519
+ **Version:** 1.0.0
520
+
521
+ ## Overview
522
+
523
+ This feature adds a complete, standalone documentation section to your Next.js application. All required components are included - no external dependencies needed.
524
+
525
+ ## Files Added
526
+
527
+ ### Pages
528
+ - \`app/docs/page.tsx\` - Docs landing page
529
+ - \`app/docs/layout.tsx\` - Docs layout with header and sidebar
530
+ - \`app/docs/[slug]/page.tsx\` - Dynamic doc pages
531
+ - \`app/docs/[slug]/[subslug]/page.tsx\` - Nested doc pages
532
+ - \`app/docs/api/page.tsx\` - API documentation page
533
+ - \`app/docs/documentation/page.tsx\` - Documentation page
534
+
535
+ ### Components (Standalone Package)
536
+ All required components are included:
537
+ - \`components/docs/docs-header.tsx\` - Docs header with navigation
538
+ - \`components/docs/docs-sidebar.tsx\` - Sidebar navigation
539
+ - \`components/docs/docs-toc.tsx\` - Table of contents component
540
+ - \`components/docs/docs-category-page.tsx\` - Category page layout
541
+ - \`components/docs/docs-code-card.tsx\` - Code snippet display
542
+ - \`components/docs/docs-nav.ts\` - Navigation data structure
543
+
544
+ ## Dependencies
545
+
546
+ This feature is **completely standalone** and includes:
547
+ - \u2705 All required UI components (\`components/ui/*\`)
548
+ - \u2705 Utility functions (\`lib/utils.ts\`)
549
+ - \u2705 All docs-specific components (\`components/docs/*\`)
550
+
551
+ **Smart Deduplication:** If UI components already exist in your project, they won't be overwritten.
552
+
553
+ **Package Dependencies:** The following will be added to your \`package.json\` if missing:
554
+ - \`@radix-ui/react-slot\` - For Button component
555
+ - \`@radix-ui/react-dialog\` - For Sheet component
556
+ - \`class-variance-authority\` - For component variants
557
+ - \`clsx\` & \`tailwind-merge\` - For className utilities
558
+ - \`lucide-react\` - For icons
559
+
560
+ **Note:** Run \`npm install\` (or your package manager) after appending to install any new dependencies.
561
+
562
+ ## Setup Instructions
563
+
564
+ 1. **Access the docs**
565
+ - Visit \`/docs\` in your application
566
+ - The docs section is fully functional with navigation and layout
567
+
568
+ 2. **Customize content**
569
+ - Edit pages in \`app/docs/\` to customize content
570
+ - Update navigation in \`components/docs/docs-nav.ts\`
571
+ - Modify components in \`components/docs/\` to change styling/behavior
572
+
573
+ 3. **Add your own documentation**
574
+ - Create new pages in \`app/docs/\`
575
+ - Follow the existing page structure
576
+ - Add entries to \`docs-nav.ts\` to include in sidebar
577
+
578
+ ## Next Steps
579
+
580
+ - Customize the docs landing page in \`app/docs/page.tsx\`
581
+ - Update navigation structure in \`components/docs/docs-nav.ts\`
582
+ - Add your own documentation pages
583
+ - Customize styling in components if needed
584
+
585
+ ## Feature Status
586
+
587
+ \u2705 **Standalone Package** - All components included
588
+ \u2705 **No Missing Dependencies** - Everything needed is bundled
589
+ \u2705 **Ready to Use** - Works immediately after append
590
+ `;
591
+ await fs.writeFile(join5(featureDocs, "docs.md"), docsReadme);
592
+ } catch (error) {
593
+ spinner.fail("Failed to copy docs pages");
594
+ throw error;
595
+ }
596
+ }
597
+ async function appendMarketingFeature(targetPath, dryRun = false) {
598
+ const marketingSourceCandidates = [
599
+ resolve3(__dirname2, "../features/marketing-landing"),
600
+ resolve3(__dirname2, "../../features/marketing-landing"),
601
+ resolve3(__dirname2, "../../../features/marketing-landing")
602
+ ];
603
+ const marketingSourcePath = marketingSourceCandidates.find((p) => existsSync6(p));
604
+ if (!marketingSourcePath) {
605
+ throw new Error(
606
+ `Marketing landing template not found. Tried paths:
607
+ ${marketingSourceCandidates.map((p) => ` - ${p}`).join("\n")}`
608
+ );
609
+ }
610
+ const spinner = ora("Copying marketing landing feature...").start();
611
+ try {
612
+ if (dryRun) {
613
+ spinner.info("DRY RUN: Would copy marketing landing pages and components");
614
+ return;
615
+ }
616
+ const sourcePage = join5(marketingSourcePath, "app", "page.tsx");
617
+ const targetPage = join5(targetPath, "app", "page.tsx");
618
+ if (!existsSync6(targetPage)) {
619
+ await fs.ensureDir(join5(targetPath, "app"));
620
+ await fs.copyFile(sourcePage, targetPage);
621
+ } else {
622
+ const existingContent = await fs.readFile(targetPage, "utf-8");
623
+ const isBaseTemplate = existingContent.includes("Welcome to") && (existingContent.includes("{{PROJECT_NAME}}") || existingContent.includes("my-test-app"));
624
+ if (isBaseTemplate) {
625
+ await fs.copyFile(sourcePage, targetPage);
626
+ logger.info("Replaced base template page.tsx with marketing landing page");
627
+ } else {
628
+ logger.warn("app/page.tsx already exists with custom content. Skipping overwrite.");
629
+ logger.warn("To use the marketing landing page, manually merge or replace app/page.tsx");
630
+ }
631
+ }
632
+ const homepageSource = join5(marketingSourcePath, "components", "homepage");
633
+ const homepageTarget = join5(targetPath, "components", "homepage");
634
+ if (existsSync6(homepageSource)) {
635
+ await fs.ensureDir(homepageTarget);
636
+ await fs.copy(homepageSource, homepageTarget, {
637
+ overwrite: false,
638
+ errorOnExist: false
639
+ });
640
+ }
641
+ const logosSource = join5(marketingSourcePath, "components", "logos");
642
+ const logosTarget = join5(targetPath, "components", "logos");
643
+ if (existsSync6(logosSource)) {
644
+ await fs.ensureDir(logosTarget);
645
+ await fs.copy(logosSource, logosTarget, {
646
+ overwrite: false,
647
+ errorOnExist: false
648
+ });
649
+ }
650
+ const shipdDir = join5(targetPath, ".shipd");
651
+ const featureDocs = join5(shipdDir, "features");
652
+ await fs.ensureDir(featureDocs);
653
+ const featureNote = `# Marketing Landing Feature
654
+
655
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
656
+
657
+ Includes:
658
+ - app/page.tsx (skipped if already present)
659
+ - components/homepage/*
660
+ - components/logos/*
661
+
662
+ Notes:
663
+ - Pricing section is static and has no external dependencies.
664
+ - If app/page.tsx already existed, merge the content manually if you want this landing page.
665
+ `;
666
+ await fs.writeFile(join5(featureDocs, "marketing-landing.md"), featureNote);
667
+ spinner.succeed("Marketing landing feature copied");
668
+ } catch (error) {
669
+ spinner.fail("Failed to copy marketing landing feature");
670
+ throw error;
671
+ }
672
+ }
673
+ async function appendDatabaseFeature(targetPath, dryRun = false) {
674
+ const dbSourceCandidates = [
675
+ resolve3(__dirname2, "../features/database"),
676
+ resolve3(__dirname2, "../../features/database"),
677
+ resolve3(__dirname2, "../../../features/database")
678
+ ];
679
+ const dbSourcePath = dbSourceCandidates.find((p) => existsSync6(p));
680
+ if (!dbSourcePath) {
681
+ throw new Error(`Database module not found. Tried paths:
682
+ ${dbSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
683
+ }
684
+ const spinner = ora("Copying database feature...").start();
685
+ try {
686
+ if (dryRun) {
687
+ spinner.info("DRY RUN: Would copy database configuration and setup");
688
+ return;
689
+ }
690
+ const drizzleConfigSource = join5(dbSourcePath, "drizzle.config.ts");
691
+ const drizzleConfigTarget = join5(targetPath, "drizzle.config.ts");
692
+ if (!existsSync6(drizzleConfigTarget)) {
693
+ await fs.copyFile(drizzleConfigSource, drizzleConfigTarget);
694
+ }
695
+ const dbSource = join5(dbSourcePath, "db");
696
+ const dbTarget = join5(targetPath, "db");
697
+ if (existsSync6(dbSource)) {
698
+ await fs.ensureDir(dbTarget);
699
+ await fs.copy(dbSource, dbTarget, {
700
+ overwrite: false,
701
+ errorOnExist: false
702
+ });
703
+ }
704
+ const pkgPath = join5(targetPath, "package.json");
705
+ if (existsSync6(pkgPath)) {
706
+ const pkg = await fs.readJson(pkgPath);
707
+ if (!pkg.scripts) {
708
+ pkg.scripts = {};
709
+ }
710
+ const dbScripts = {
711
+ "db:push": "drizzle-kit push",
712
+ "db:generate": "drizzle-kit generate",
713
+ "db:migrate": "drizzle-kit migrate",
714
+ "db:studio": "drizzle-kit studio"
715
+ };
716
+ let scriptsUpdated = false;
717
+ for (const [script, command] of Object.entries(dbScripts)) {
718
+ if (!pkg.scripts[script]) {
719
+ pkg.scripts[script] = command;
720
+ scriptsUpdated = true;
721
+ }
722
+ }
723
+ if (scriptsUpdated) {
724
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
725
+ }
726
+ }
727
+ if (existsSync6(pkgPath)) {
728
+ const pkg = await fs.readJson(pkgPath);
729
+ let updated = false;
730
+ const requiredDeps = {
731
+ "postgres": "^3.4.4",
732
+ "drizzle-orm": "^0.41.0"
733
+ };
734
+ const requiredDevDeps = {
735
+ "drizzle-kit": "^0.31.0"
736
+ };
737
+ if (!pkg.dependencies) {
738
+ pkg.dependencies = {};
739
+ }
740
+ if (!pkg.devDependencies) {
741
+ pkg.devDependencies = {};
742
+ }
743
+ for (const [dep, version] of Object.entries(requiredDeps)) {
744
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
745
+ pkg.dependencies[dep] = version;
746
+ updated = true;
747
+ }
748
+ }
749
+ for (const [dep, version] of Object.entries(requiredDevDeps)) {
750
+ if (!pkg.devDependencies[dep]) {
751
+ pkg.devDependencies[dep] = version;
752
+ updated = true;
753
+ }
754
+ }
755
+ if (updated) {
756
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
757
+ }
758
+ }
759
+ const shipdDir = join5(targetPath, ".shipd");
760
+ const featureDocs = join5(shipdDir, "features");
761
+ await fs.ensureDir(featureDocs);
762
+ const featureNote = `# Database Feature
763
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
764
+
765
+ Includes:
766
+ - drizzle.config.ts
767
+ - db/drizzle.ts (connection)
768
+ - db/schema.ts (base schema)
769
+ - db/migrations/ (migration directory)
770
+ - Package scripts (db:push, db:generate, etc.)
771
+
772
+ Notes:
773
+ - Base schema is empty. Other modules (auth, payments) will add tables.
774
+ - Set DATABASE_URL in .env.local to connect to your database.
775
+ `;
776
+ await fs.writeFile(join5(featureDocs, "database.md"), featureNote);
777
+ const dbConfig = await fs.readJson(join5(dbSourcePath, "feature.config.json")).catch(() => null);
778
+ if (dbConfig?.envVars) {
779
+ await addEnvVarsToExample(targetPath, "database", dbConfig.envVars);
780
+ }
781
+ spinner.succeed("Database feature copied");
782
+ } catch (error) {
783
+ spinner.fail("Failed to copy database feature");
784
+ throw error;
785
+ }
786
+ }
787
+ async function appendAuthFeature(targetPath, dryRun = false) {
788
+ const authSourceCandidates = [
789
+ resolve3(__dirname2, "../features/auth"),
790
+ resolve3(__dirname2, "../../features/auth"),
791
+ resolve3(__dirname2, "../../../features/auth")
792
+ ];
793
+ const authSourcePath = authSourceCandidates.find((p) => existsSync6(p));
794
+ if (!authSourcePath) {
795
+ throw new Error(`Auth module not found. Tried paths:
796
+ ${authSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
797
+ }
798
+ const spinner = ora("Copying auth feature...").start();
799
+ try {
800
+ if (dryRun) {
801
+ spinner.info("DRY RUN: Would copy auth pages, API routes, and configuration");
802
+ return;
803
+ }
804
+ const dbSchemaPath = join5(targetPath, "db", "schema.ts");
805
+ if (!existsSync6(dbSchemaPath)) {
806
+ spinner.info("Database module not found. Installing database first...");
807
+ await appendDatabaseFeature(targetPath, dryRun);
808
+ }
809
+ const signInSource = join5(authSourcePath, "app", "sign-in", "page.tsx");
810
+ const signInTarget = join5(targetPath, "app", "sign-in", "page.tsx");
811
+ await fs.ensureDir(join5(targetPath, "app", "sign-in"));
812
+ await fs.copyFile(signInSource, signInTarget);
813
+ const signUpSource = join5(authSourcePath, "app", "sign-up", "page.tsx");
814
+ const signUpTarget = join5(targetPath, "app", "sign-up", "page.tsx");
815
+ await fs.ensureDir(join5(targetPath, "app", "sign-up"));
816
+ await fs.copyFile(signUpSource, signUpTarget);
817
+ const dashboardSource = join5(authSourcePath, "app", "dashboard");
818
+ const dashboardTarget = join5(targetPath, "app", "dashboard");
819
+ if (existsSync6(dashboardSource)) {
820
+ await fs.ensureDir(dashboardTarget);
821
+ await fs.copy(dashboardSource, dashboardTarget, {
822
+ overwrite: false,
823
+ errorOnExist: false
824
+ });
825
+ }
826
+ const authApiSource = join5(authSourcePath, "app", "api", "auth");
827
+ const authApiTarget = join5(targetPath, "app", "api", "auth");
828
+ if (existsSync6(authApiSource)) {
829
+ await fs.ensureDir(authApiTarget);
830
+ await fs.copy(authApiSource, authApiTarget, { overwrite: true });
831
+ }
832
+ const authLibSource = join5(authSourcePath, "lib");
833
+ const authLibTarget = join5(targetPath, "lib");
834
+ if (existsSync6(authLibSource)) {
835
+ await fs.ensureDir(authLibTarget);
836
+ const authFiles = ["auth.ts", "auth-client.ts"];
837
+ for (const file of authFiles) {
838
+ const sourceFile = join5(authLibSource, file);
839
+ const targetFile = join5(authLibTarget, file);
840
+ if (existsSync6(sourceFile)) {
841
+ await fs.copyFile(sourceFile, targetFile);
842
+ }
843
+ }
844
+ const emailStubSource = join5(authLibSource, "email.ts");
845
+ const emailTarget = join5(authLibTarget, "email.ts");
846
+ if (existsSync6(emailStubSource) && !existsSync6(emailTarget)) {
847
+ await fs.copyFile(emailStubSource, emailTarget);
848
+ logger.info("Created email.ts stub (install email module for production)");
849
+ }
850
+ }
851
+ const authSchemaSource = join5(authSourcePath, "auth-schema.ts");
852
+ const authSchemaTarget = join5(targetPath, "auth-schema.ts");
853
+ await fs.copyFile(authSchemaSource, authSchemaTarget);
854
+ const dbSchemaTarget = join5(targetPath, "db", "schema.ts");
855
+ if (existsSync6(dbSchemaTarget)) {
856
+ const dbSchemaContent = await fs.readFile(dbSchemaTarget, "utf-8");
857
+ const authSchemaContent = await fs.readFile(authSchemaSource, "utf-8");
858
+ const tableRegex = /export const \w+ = pgTable\([^}]+}\);/gs;
859
+ const tableExports = [];
860
+ let match;
861
+ while ((match = tableRegex.exec(authSchemaContent)) !== null) {
862
+ tableExports.push(match[0]);
863
+ }
864
+ if (tableExports.length === 0) {
865
+ const tableNames = ["user", "session", "account", "verification"];
866
+ for (const tableName of tableNames) {
867
+ const tableRegex2 = new RegExp(`export const ${tableName} = pgTable\\([\\s\\S]*?\\)};`, "g");
868
+ const match2 = authSchemaContent.match(tableRegex2);
869
+ if (match2) {
870
+ tableExports.push(match2[0]);
871
+ }
872
+ }
873
+ }
874
+ let updatedSchema = dbSchemaContent;
875
+ if (!updatedSchema.includes('from "drizzle-orm/pg-core"')) {
876
+ const importLine = 'import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";';
877
+ updatedSchema = importLine + "\n\n" + updatedSchema;
878
+ }
879
+ const tablesToAdd = [];
880
+ for (const tableExport of tableExports) {
881
+ const tableNameMatch = tableExport.match(/export const (\w+) = pgTable/);
882
+ if (tableNameMatch) {
883
+ const tableName = tableNameMatch[1];
884
+ if (!updatedSchema.includes(`export const ${tableName} = pgTable`)) {
885
+ tablesToAdd.push(tableExport);
886
+ }
887
+ }
888
+ }
889
+ if (tablesToAdd.length > 0) {
890
+ updatedSchema += "\n\n// Auth tables\n" + tablesToAdd.join("\n\n");
891
+ }
892
+ await fs.writeFile(dbSchemaTarget, updatedSchema, "utf-8");
893
+ }
894
+ const middlewarePath = join5(targetPath, "middleware.ts");
895
+ if (existsSync6(middlewarePath)) {
896
+ const middlewareContent = await fs.readFile(middlewarePath, "utf-8");
897
+ if (!middlewareContent.includes("getSessionCookie")) {
898
+ const authMiddlewarePatch = await fs.readFile(join5(authSourcePath, "middleware.patch.ts"), "utf-8");
899
+ const updatedMiddleware = middlewareContent.replace(
900
+ /export async function middleware\(request: NextRequest\) \{[\s\S]*?return NextResponse\.next\(\);?\s*\}/,
901
+ `export async function middleware(request: NextRequest) {
902
+ const sessionCookie = getSessionCookie(request);
903
+ const { pathname } = request.nextUrl;
904
+
905
+ // Don't redirect from sign-in/sign-up pages
906
+ if (["/sign-in", "/sign-up"].includes(pathname)) {
907
+ return NextResponse.next();
908
+ }
909
+
910
+ // Redirect unauthenticated users from protected routes
911
+ const protectedRoutes = ["/dashboard"];
912
+ const isProtectedRoute = protectedRoutes.some(route => pathname.startsWith(route));
913
+
914
+ if (isProtectedRoute && !sessionCookie) {
915
+ const returnTo = encodeURIComponent(pathname);
916
+ return NextResponse.redirect(new URL(\`/sign-in?returnTo=\${returnTo}\`, request.url));
917
+ }
918
+
919
+ return NextResponse.next();
920
+ }`
921
+ );
922
+ let finalMiddleware = updatedMiddleware;
923
+ if (!finalMiddleware.includes('from "better-auth/cookies"')) {
924
+ const importMatch = finalMiddleware.match(/^import .+ from .+;\n/gm);
925
+ if (importMatch) {
926
+ const lastImportIndex = finalMiddleware.lastIndexOf(importMatch[importMatch.length - 1]) + importMatch[importMatch.length - 1].length;
927
+ finalMiddleware = finalMiddleware.slice(0, lastImportIndex) + 'import { getSessionCookie } from "better-auth/cookies";\n' + finalMiddleware.slice(lastImportIndex);
928
+ } else {
929
+ finalMiddleware = 'import { getSessionCookie } from "better-auth/cookies";\n' + finalMiddleware;
930
+ }
931
+ }
932
+ if (finalMiddleware.includes("matcher: []")) {
933
+ finalMiddleware = finalMiddleware.replace(
934
+ /export const config = \{[\s\S]*?\};/,
935
+ 'export const config = {\n matcher: ["/dashboard/:path*", "/sign-in", "/sign-up"],\n};'
936
+ );
937
+ }
938
+ await fs.writeFile(middlewarePath, finalMiddleware, "utf-8");
939
+ }
940
+ }
941
+ const pkgPath = join5(targetPath, "package.json");
942
+ if (existsSync6(pkgPath)) {
943
+ const pkg = await fs.readJson(pkgPath);
944
+ let updated = false;
945
+ const requiredDeps = {
946
+ "better-auth": "^1.2.8",
947
+ "@polar-sh/better-auth": "^1.0.1",
948
+ "sonner": "^2.0.3"
949
+ // For toast notifications in sign-in/sign-up
950
+ };
951
+ if (!pkg.dependencies) {
952
+ pkg.dependencies = {};
953
+ }
954
+ for (const [dep, version] of Object.entries(requiredDeps)) {
955
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
956
+ pkg.dependencies[dep] = version;
957
+ updated = true;
958
+ }
959
+ }
960
+ if (updated) {
961
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
962
+ }
963
+ }
964
+ const shipdDir = join5(targetPath, ".shipd");
965
+ const featureDocs = join5(shipdDir, "features");
966
+ await fs.ensureDir(featureDocs);
967
+ const featureNote = `# Auth Feature
968
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
969
+
970
+ Includes:
971
+ - app/sign-in/page.tsx
972
+ - app/sign-up/page.tsx
973
+ - app/api/auth/[...all]/route.ts
974
+ - lib/auth.ts
975
+ - lib/auth-client.ts
976
+ - auth-schema.ts (merged into db/schema.ts)
977
+ - Middleware updates for route protection
978
+
979
+ Notes:
980
+ - Database module was automatically installed (required dependency)
981
+ - Auth tables added to db/schema.ts
982
+ - Set BETTER_AUTH_SECRET and Google OAuth credentials in .env.local
983
+ `;
984
+ await fs.writeFile(join5(featureDocs, "auth.md"), featureNote);
985
+ const authConfig = await fs.readJson(join5(authSourcePath, "feature.config.json")).catch(() => null);
986
+ if (authConfig?.envVars) {
987
+ await addEnvVarsToExample(targetPath, "auth", authConfig.envVars);
988
+ }
989
+ spinner.succeed("Auth feature copied");
990
+ } catch (error) {
991
+ spinner.fail("Failed to copy auth feature");
992
+ throw error;
993
+ }
994
+ }
995
+ async function appendPaymentsFeature(targetPath, dryRun = false) {
996
+ const paymentsSourceCandidates = [
997
+ resolve3(__dirname2, "../features/payments"),
998
+ resolve3(__dirname2, "../../features/payments"),
999
+ resolve3(__dirname2, "../../../features/payments")
1000
+ ];
1001
+ const paymentsSourcePath = paymentsSourceCandidates.find((p) => existsSync6(p));
1002
+ if (!paymentsSourcePath) {
1003
+ throw new Error(`Payments module not found. Tried paths:
1004
+ ${paymentsSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
1005
+ }
1006
+ const spinner = ora("Copying payments feature...").start();
1007
+ try {
1008
+ if (dryRun) {
1009
+ spinner.info("DRY RUN: Would copy payment pages, API routes, and utilities");
1010
+ return;
1011
+ }
1012
+ const dbSchemaPath = join5(targetPath, "db", "schema.ts");
1013
+ const authLibPath = join5(targetPath, "lib", "auth.ts");
1014
+ if (!existsSync6(dbSchemaPath)) {
1015
+ spinner.info("Database module not found. Installing database first...");
1016
+ await appendDatabaseFeature(targetPath, dryRun);
1017
+ }
1018
+ if (!existsSync6(authLibPath)) {
1019
+ spinner.info("Auth module not found. Installing auth first...");
1020
+ await appendAuthFeature(targetPath, dryRun);
1021
+ }
1022
+ const paymentDashboardSource = join5(paymentsSourcePath, "app", "dashboard", "payment");
1023
+ const paymentDashboardTarget = join5(targetPath, "app", "dashboard", "payment");
1024
+ if (existsSync6(paymentDashboardSource)) {
1025
+ await fs.ensureDir(paymentDashboardTarget);
1026
+ await fs.copy(paymentDashboardSource, paymentDashboardTarget, { overwrite: true });
1027
+ }
1028
+ const successSource = join5(paymentsSourcePath, "app", "success");
1029
+ const successTarget = join5(targetPath, "app", "success");
1030
+ if (existsSync6(successSource)) {
1031
+ await fs.ensureDir(successTarget);
1032
+ await fs.copy(successSource, successTarget, { overwrite: true });
1033
+ }
1034
+ const subscriptionApiSource = join5(paymentsSourcePath, "app", "api", "subscription");
1035
+ const subscriptionApiTarget = join5(targetPath, "app", "api", "subscription");
1036
+ if (existsSync6(subscriptionApiSource)) {
1037
+ await fs.ensureDir(subscriptionApiTarget);
1038
+ await fs.copy(subscriptionApiSource, subscriptionApiTarget, { overwrite: true });
1039
+ }
1040
+ const paymentsLibSource = join5(paymentsSourcePath, "lib");
1041
+ const paymentsLibTarget = join5(targetPath, "lib");
1042
+ if (existsSync6(paymentsLibSource)) {
1043
+ await fs.ensureDir(paymentsLibTarget);
1044
+ await fs.copy(paymentsLibSource, paymentsLibTarget, {
1045
+ overwrite: false,
1046
+ errorOnExist: false
1047
+ });
1048
+ }
1049
+ const paymentsSchemaSource = join5(paymentsSourcePath, "payments-schema.ts");
1050
+ const paymentsSchemaTarget = join5(targetPath, "payments-schema.ts");
1051
+ await fs.copyFile(paymentsSchemaSource, paymentsSchemaTarget);
1052
+ const dbSchemaTarget = join5(targetPath, "db", "schema.ts");
1053
+ if (existsSync6(dbSchemaTarget)) {
1054
+ const dbSchemaContent = await fs.readFile(dbSchemaTarget, "utf-8");
1055
+ const paymentsSchemaContent = await fs.readFile(paymentsSchemaSource, "utf-8");
1056
+ const subscriptionTableRegex = /export const subscription = pgTable\([\s\S]*?}\);?/;
1057
+ const subscriptionTableMatch = paymentsSchemaContent.match(subscriptionTableRegex);
1058
+ if (subscriptionTableMatch && !dbSchemaContent.includes("export const subscription = pgTable")) {
1059
+ let subscriptionTable = subscriptionTableMatch[0];
1060
+ if (!subscriptionTable.includes("references") && dbSchemaContent.includes("export const user = pgTable")) {
1061
+ subscriptionTable = subscriptionTable.replace(
1062
+ /userId: text\("userId"\),/,
1063
+ 'userId: text("userId").references(() => user.id),'
1064
+ );
1065
+ }
1066
+ const updatedSchema = dbSchemaContent + "\n\n// Subscription table (from payments module)\n" + subscriptionTable;
1067
+ await fs.writeFile(dbSchemaTarget, updatedSchema, "utf-8");
1068
+ }
1069
+ }
1070
+ const pkgPath = join5(targetPath, "package.json");
1071
+ if (existsSync6(pkgPath)) {
1072
+ const pkg = await fs.readJson(pkgPath);
1073
+ let updated = false;
1074
+ const requiredDeps = {
1075
+ "@polar-sh/sdk": "^0.42.1"
1076
+ };
1077
+ if (!pkg.dependencies) {
1078
+ pkg.dependencies = {};
1079
+ }
1080
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1081
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1082
+ pkg.dependencies[dep] = version;
1083
+ updated = true;
1084
+ }
1085
+ }
1086
+ if (updated) {
1087
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1088
+ }
1089
+ }
1090
+ const shipdDir = join5(targetPath, ".shipd");
1091
+ const featureDocs = join5(shipdDir, "features");
1092
+ await fs.ensureDir(featureDocs);
1093
+ const featureNote = `# Payments Feature
1094
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1095
+
1096
+ Includes:
1097
+ - app/dashboard/payment/page.tsx (subscription management)
1098
+ - app/success/page.tsx (payment success page)
1099
+ - app/api/subscription/route.ts (subscription API)
1100
+ - lib/subscription.ts (subscription utilities)
1101
+ - lib/polar-products.ts (Polar product fetching)
1102
+ - payments-schema.ts (merged into db/schema.ts)
1103
+
1104
+ Notes:
1105
+ - Auth and Database modules were automatically installed (required dependencies)
1106
+ - Subscription table added to db/schema.ts
1107
+ - Set Polar.sh credentials in .env.local
1108
+ - Webhook handling is in lib/auth.ts (from auth module)
1109
+ `;
1110
+ await fs.writeFile(join5(featureDocs, "payments.md"), featureNote);
1111
+ const paymentsConfig = await fs.readJson(join5(paymentsSourcePath, "feature.config.json")).catch(() => null);
1112
+ if (paymentsConfig?.envVars) {
1113
+ await addEnvVarsToExample(targetPath, "payments", paymentsConfig.envVars);
1114
+ }
1115
+ spinner.succeed("Payments feature copied");
1116
+ } catch (error) {
1117
+ spinner.fail("Failed to copy payments feature");
1118
+ throw error;
1119
+ }
1120
+ }
1121
+ async function appendEmailFeature(targetPath, dryRun = false) {
1122
+ const emailSourceCandidates = [
1123
+ resolve3(__dirname2, "../features/email"),
1124
+ resolve3(__dirname2, "../../features/email"),
1125
+ resolve3(__dirname2, "../../../features/email")
1126
+ ];
1127
+ const emailSourcePath = emailSourceCandidates.find((p) => existsSync6(p));
1128
+ if (!emailSourcePath) {
1129
+ throw new Error(`Email module not found. Tried paths:
1130
+ ${emailSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
1131
+ }
1132
+ const spinner = ora("Copying email feature...").start();
1133
+ try {
1134
+ if (dryRun) {
1135
+ spinner.info("DRY RUN: Would copy email utilities and templates");
1136
+ return;
1137
+ }
1138
+ const emailLibSource = join5(emailSourcePath, "lib", "email.ts");
1139
+ const emailLibTarget = join5(targetPath, "lib", "email.ts");
1140
+ await fs.ensureDir(join5(targetPath, "lib"));
1141
+ await fs.copyFile(emailLibSource, emailLibTarget);
1142
+ const existingContent = await fs.readFile(emailLibTarget, "utf-8").catch(() => "");
1143
+ if (existingContent.includes("Email Stub")) {
1144
+ logger.info("Replaced email stub with full email module");
1145
+ }
1146
+ const emailsSource = join5(emailSourcePath, "emails");
1147
+ const emailsTarget = join5(targetPath, "emails");
1148
+ if (existsSync6(emailsSource)) {
1149
+ await fs.ensureDir(emailsTarget);
1150
+ await fs.copy(emailsSource, emailsTarget, { overwrite: true });
1151
+ }
1152
+ const pkgPath = join5(targetPath, "package.json");
1153
+ if (existsSync6(pkgPath)) {
1154
+ const pkg = await fs.readJson(pkgPath);
1155
+ let updated = false;
1156
+ const requiredDeps = {
1157
+ "resend": "^6.6.0",
1158
+ "@react-email/components": "^1.0.2",
1159
+ "@react-email/render": "^2.0.0"
1160
+ };
1161
+ if (!pkg.dependencies) {
1162
+ pkg.dependencies = {};
1163
+ }
1164
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1165
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1166
+ pkg.dependencies[dep] = version;
1167
+ updated = true;
1168
+ }
1169
+ }
1170
+ if (updated) {
1171
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1172
+ }
1173
+ }
1174
+ const shipdDir = join5(targetPath, ".shipd");
1175
+ const featureDocs = join5(shipdDir, "features");
1176
+ await fs.ensureDir(featureDocs);
1177
+ const featureNote = `# Email Feature
1178
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1179
+
1180
+ Includes:
1181
+ - lib/email.ts (full email sending utilities)
1182
+ - emails/welcome.tsx (welcome email template)
1183
+ - emails/password-reset.tsx (password reset template)
1184
+ - emails/subscription-confirmation.tsx (subscription confirmation template)
1185
+ - emails/payment-failed.tsx (payment failed template)
1186
+ - emails/components/layout.tsx (shared email layout)
1187
+
1188
+ Notes:
1189
+ - Replaces email stub from auth module (if installed)
1190
+ - Set RESEND_API_KEY in .env.local to enable email sending
1191
+ - Works without configuration (logs to console in development)
1192
+ - Auth and Payments modules will automatically use real email sending
1193
+ `;
1194
+ await fs.writeFile(join5(featureDocs, "email.md"), featureNote);
1195
+ const emailConfig = await fs.readJson(join5(emailSourcePath, "feature.config.json")).catch(() => null);
1196
+ if (emailConfig?.envVars) {
1197
+ await addEnvVarsToExample(targetPath, "email", emailConfig.envVars);
1198
+ }
1199
+ spinner.succeed("Email feature copied");
1200
+ } catch (error) {
1201
+ spinner.fail("Failed to copy email feature");
1202
+ throw error;
1203
+ }
1204
+ }
1205
+ async function appendAiChatFeature(targetPath, dryRun = false) {
1206
+ const aiChatSourceCandidates = [
1207
+ resolve3(__dirname2, "../features/ai-chat"),
1208
+ resolve3(__dirname2, "../../features/ai-chat"),
1209
+ resolve3(__dirname2, "../../../features/ai-chat")
1210
+ ];
1211
+ const aiChatSourcePath = aiChatSourceCandidates.find((p) => existsSync6(p));
1212
+ if (!aiChatSourcePath) {
1213
+ throw new Error(`AI Chat module not found. Tried paths:
1214
+ ${aiChatSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
1215
+ }
1216
+ const spinner = ora("Copying AI Chat feature...").start();
1217
+ try {
1218
+ if (dryRun) {
1219
+ spinner.info("DRY RUN: Would copy chat API route, chat page, and chatbot component");
1220
+ return;
1221
+ }
1222
+ const chatApiSource = join5(aiChatSourcePath, "app", "api", "chat");
1223
+ const chatApiTarget = join5(targetPath, "app", "api", "chat");
1224
+ if (existsSync6(chatApiSource)) {
1225
+ await fs.ensureDir(chatApiTarget);
1226
+ await fs.copy(chatApiSource, chatApiTarget, { overwrite: true });
1227
+ }
1228
+ const chatPageSource = join5(aiChatSourcePath, "app", "dashboard", "chat");
1229
+ const chatPageTarget = join5(targetPath, "app", "dashboard", "chat");
1230
+ if (existsSync6(chatPageSource)) {
1231
+ await fs.ensureDir(chatPageTarget);
1232
+ await fs.copy(chatPageSource, chatPageTarget, { overwrite: true });
1233
+ }
1234
+ const chatbotSource = join5(aiChatSourcePath, "app", "dashboard", "_components", "chatbot.tsx");
1235
+ const chatbotTarget = join5(targetPath, "app", "dashboard", "_components", "chatbot.tsx");
1236
+ if (existsSync6(chatbotSource)) {
1237
+ await fs.ensureDir(join5(targetPath, "app", "dashboard", "_components"));
1238
+ await fs.copyFile(chatbotSource, chatbotTarget);
1239
+ }
1240
+ const pkgPath = join5(targetPath, "package.json");
1241
+ if (existsSync6(pkgPath)) {
1242
+ const pkg = await fs.readJson(pkgPath);
1243
+ let updated = false;
1244
+ const requiredDeps = {
1245
+ "@ai-sdk/openai": "^1.3.22",
1246
+ "ai": "^4.3.16",
1247
+ "@ai-sdk/react": "^1.0.0"
1248
+ };
1249
+ if (!pkg.dependencies?.["react-markdown"] && !pkg.devDependencies?.["react-markdown"]) {
1250
+ requiredDeps["react-markdown"] = "^10.1.0";
1251
+ updated = true;
1252
+ }
1253
+ if (!pkg.dependencies?.["remark-gfm"] && !pkg.devDependencies?.["remark-gfm"]) {
1254
+ requiredDeps["remark-gfm"] = "^4.0.1";
1255
+ updated = true;
1256
+ }
1257
+ if (!pkg.dependencies) {
1258
+ pkg.dependencies = {};
1259
+ }
1260
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1261
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1262
+ pkg.dependencies[dep] = version;
1263
+ updated = true;
1264
+ }
1265
+ }
1266
+ if (updated) {
1267
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1268
+ }
1269
+ }
1270
+ const shipdDir = join5(targetPath, ".shipd");
1271
+ const featureDocs = join5(shipdDir, "features");
1272
+ await fs.ensureDir(featureDocs);
1273
+ const featureNote = `# AI Chat Feature
1274
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1275
+
1276
+ Includes:
1277
+ - app/api/chat/route.ts (OpenAI streaming chat API)
1278
+ - app/dashboard/chat/page.tsx (full chat interface)
1279
+ - app/dashboard/_components/chatbot.tsx (chatbot widget component)
1280
+
1281
+ Notes:
1282
+ - Set OPENAI_API_KEY in .env.local to enable chat functionality
1283
+ - Uses GPT-4o model with web search preview tool
1284
+ - Streaming responses with markdown rendering
1285
+ - Chatbot component can be integrated into dashboard layout
1286
+ `;
1287
+ await fs.writeFile(join5(featureDocs, "ai-chat.md"), featureNote);
1288
+ const aiChatConfig = await fs.readJson(join5(aiChatSourcePath, "feature.config.json")).catch(() => null);
1289
+ if (aiChatConfig?.envVars) {
1290
+ await addEnvVarsToExample(targetPath, "ai-chat", aiChatConfig.envVars);
1291
+ }
1292
+ spinner.succeed("AI Chat feature copied");
1293
+ } catch (error) {
1294
+ spinner.fail("Failed to copy AI Chat feature");
1295
+ throw error;
1296
+ }
1297
+ }
1298
+ async function appendFileUploadFeature(targetPath, dryRun = false) {
1299
+ const fileUploadSourceCandidates = [
1300
+ resolve3(__dirname2, "../features/file-upload"),
1301
+ resolve3(__dirname2, "../../features/file-upload"),
1302
+ resolve3(__dirname2, "../../../features/file-upload")
1303
+ ];
1304
+ const fileUploadSourcePath = fileUploadSourceCandidates.find((p) => existsSync6(p));
1305
+ if (!fileUploadSourcePath) {
1306
+ throw new Error(`File Upload module not found. Tried paths:
1307
+ ${fileUploadSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
1308
+ }
1309
+ const spinner = ora("Copying File Upload feature...").start();
1310
+ try {
1311
+ if (dryRun) {
1312
+ spinner.info("DRY RUN: Would copy upload API route, upload page, and upload utilities");
1313
+ return;
1314
+ }
1315
+ const uploadApiSource = join5(fileUploadSourcePath, "app", "api", "upload-image");
1316
+ const uploadApiTarget = join5(targetPath, "app", "api", "upload-image");
1317
+ if (existsSync6(uploadApiSource)) {
1318
+ await fs.ensureDir(uploadApiTarget);
1319
+ await fs.copy(uploadApiSource, uploadApiTarget, { overwrite: true });
1320
+ }
1321
+ const uploadPageSource = join5(fileUploadSourcePath, "app", "dashboard", "upload");
1322
+ const uploadPageTarget = join5(targetPath, "app", "dashboard", "upload");
1323
+ if (existsSync6(uploadPageSource)) {
1324
+ await fs.ensureDir(uploadPageTarget);
1325
+ await fs.copy(uploadPageSource, uploadPageTarget, { overwrite: true });
1326
+ }
1327
+ const uploadLibSource = join5(fileUploadSourcePath, "lib", "upload-image.ts");
1328
+ const uploadLibTarget = join5(targetPath, "lib", "upload-image.ts");
1329
+ if (existsSync6(uploadLibSource)) {
1330
+ await fs.ensureDir(join5(targetPath, "lib"));
1331
+ await fs.copyFile(uploadLibSource, uploadLibTarget);
1332
+ }
1333
+ const pkgPath = join5(targetPath, "package.json");
1334
+ if (existsSync6(pkgPath)) {
1335
+ const pkg = await fs.readJson(pkgPath);
1336
+ let updated = false;
1337
+ const requiredDeps = {
1338
+ "@aws-sdk/client-s3": "^3.800.0"
1339
+ };
1340
+ if (!pkg.dependencies) {
1341
+ pkg.dependencies = {};
1342
+ }
1343
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1344
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1345
+ pkg.dependencies[dep] = version;
1346
+ updated = true;
1347
+ }
1348
+ }
1349
+ if (updated) {
1350
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1351
+ }
1352
+ }
1353
+ const shipdDir = join5(targetPath, ".shipd");
1354
+ const featureDocs = join5(shipdDir, "features");
1355
+ await fs.ensureDir(featureDocs);
1356
+ const featureNote = `# File Upload Feature
1357
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1358
+
1359
+ Includes:
1360
+ - app/api/upload-image/route.ts (R2 upload API)
1361
+ - app/dashboard/upload/page.tsx (upload interface with drag-and-drop)
1362
+ - lib/upload-image.ts (R2 upload utilities)
1363
+
1364
+ Notes:
1365
+ - Set Cloudflare R2 credentials in .env.local
1366
+ - Configure public domain for R2 bucket
1367
+ - Update public URL in lib/upload-image.ts
1368
+ - Supports image uploads up to 5MB (client) / 10MB (server)
1369
+ `;
1370
+ await fs.writeFile(join5(featureDocs, "file-upload.md"), featureNote);
1371
+ const fileUploadConfig = await fs.readJson(join5(fileUploadSourcePath, "feature.config.json")).catch(() => null);
1372
+ if (fileUploadConfig?.envVars) {
1373
+ await addEnvVarsToExample(targetPath, "file-upload", fileUploadConfig.envVars);
1374
+ }
1375
+ spinner.succeed("File Upload feature copied");
1376
+ } catch (error) {
1377
+ spinner.fail("Failed to copy File Upload feature");
1378
+ throw error;
1379
+ }
1380
+ }
1381
+ async function appendAnalyticsFeature(targetPath, dryRun = false) {
1382
+ const analyticsSourceCandidates = [
1383
+ resolve3(__dirname2, "../features/analytics"),
1384
+ resolve3(__dirname2, "../../features/analytics"),
1385
+ resolve3(__dirname2, "../../../features/analytics")
1386
+ ];
1387
+ const analyticsSourcePath = analyticsSourceCandidates.find((p) => existsSync6(p));
1388
+ if (!analyticsSourcePath) {
1389
+ throw new Error(`Analytics module not found. Tried paths:
1390
+ ${analyticsSourceCandidates.map((p) => ` - ${p}`).join("\n")}`);
1391
+ }
1392
+ const spinner = ora("Copying Analytics feature...").start();
1393
+ try {
1394
+ if (dryRun) {
1395
+ spinner.info("DRY RUN: Would copy PostHog provider and utilities");
1396
+ return;
1397
+ }
1398
+ const analyticsLibSource = join5(analyticsSourcePath, "lib", "posthog.ts");
1399
+ const analyticsLibTarget = join5(targetPath, "lib", "posthog.ts");
1400
+ if (existsSync6(analyticsLibSource)) {
1401
+ await fs.ensureDir(join5(targetPath, "lib"));
1402
+ await fs.copyFile(analyticsLibSource, analyticsLibTarget);
1403
+ }
1404
+ const pkgPath = join5(targetPath, "package.json");
1405
+ if (existsSync6(pkgPath)) {
1406
+ const pkg = await fs.readJson(pkgPath);
1407
+ let updated = false;
1408
+ const requiredDeps = {
1409
+ "posthog-js": "^1.248.1",
1410
+ "posthog-node": "^4.18.0"
1411
+ };
1412
+ if (!pkg.dependencies) {
1413
+ pkg.dependencies = {};
1414
+ }
1415
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1416
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1417
+ pkg.dependencies[dep] = version;
1418
+ updated = true;
1419
+ }
1420
+ }
1421
+ if (updated) {
1422
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1423
+ }
1424
+ }
1425
+ const shipdDir = join5(targetPath, ".shipd");
1426
+ const featureDocs = join5(shipdDir, "features");
1427
+ await fs.ensureDir(featureDocs);
1428
+ const featureNote = `# Analytics Feature
1429
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1430
+
1431
+ Includes:
1432
+ - lib/posthog.ts (PostHog provider and initialization)
1433
+
1434
+ Notes:
1435
+ - Set NEXT_PUBLIC_POSTHOG_KEY in .env.local to enable analytics
1436
+ - Add PostHogProviderWrapper to app/layout.tsx
1437
+ - Automatic pageview tracking enabled
1438
+ - Custom event tracking available via usePostHog hook
1439
+ `;
1440
+ await fs.writeFile(join5(featureDocs, "analytics.md"), featureNote);
1441
+ const analyticsConfig = await fs.readJson(join5(analyticsSourcePath, "feature.config.json")).catch(() => null);
1442
+ if (analyticsConfig?.envVars) {
1443
+ await addEnvVarsToExample(targetPath, "analytics", analyticsConfig.envVars);
1444
+ }
1445
+ spinner.succeed("Analytics feature copied");
1446
+ } catch (error) {
1447
+ spinner.fail("Failed to copy Analytics feature");
1448
+ throw error;
1449
+ }
1450
+ }
1451
+ async function appendSeoFeature(targetPath, dryRun = false) {
1452
+ const seoSourceCandidates = [
1453
+ resolve3(__dirname2, "../features/seo"),
1454
+ resolve3(__dirname2, "../../features/seo"),
1455
+ resolve3(__dirname2, "../../../features/seo")
1456
+ ];
1457
+ const seoSourcePath = seoSourceCandidates.find((p) => existsSync6(p));
1458
+ if (!seoSourcePath) {
1459
+ throw new Error(
1460
+ `SEO feature not found. Tried paths:
1461
+ ${seoSourceCandidates.map((p) => ` - ${p}`).join("\n")}`
1462
+ );
1463
+ }
1464
+ const spinner = ora("Copying SEO feature...").start();
1465
+ try {
1466
+ if (dryRun) {
1467
+ spinner.info("DRY RUN: Would copy SEO files");
1468
+ return;
1469
+ }
1470
+ const sitemapSource = join5(seoSourcePath, "app", "sitemap.ts");
1471
+ const sitemapTarget = join5(targetPath, "app", "sitemap.ts");
1472
+ if (existsSync6(sitemapSource)) {
1473
+ await fs.ensureDir(dirname2(sitemapTarget));
1474
+ await fs.copyFile(sitemapSource, sitemapTarget);
1475
+ }
1476
+ const robotsSource = join5(seoSourcePath, "app", "robots.txt");
1477
+ const robotsTarget = join5(targetPath, "app", "robots.txt");
1478
+ if (existsSync6(robotsSource)) {
1479
+ await fs.ensureDir(dirname2(robotsTarget));
1480
+ await fs.copyFile(robotsSource, robotsTarget);
1481
+ }
1482
+ const blogSource = join5(seoSourcePath, "app", "blog");
1483
+ const blogTarget = join5(targetPath, "app", "blog");
1484
+ if (existsSync6(blogSource)) {
1485
+ await fs.copy(blogSource, blogTarget, {
1486
+ overwrite: false,
1487
+ errorOnExist: false
1488
+ });
1489
+ }
1490
+ const seoUtilsSource = join5(seoSourcePath, "lib", "seo-utils.ts");
1491
+ const seoUtilsTarget = join5(targetPath, "lib", "seo-utils.ts");
1492
+ if (existsSync6(seoUtilsSource)) {
1493
+ await fs.ensureDir(dirname2(seoUtilsTarget));
1494
+ await fs.copyFile(seoUtilsSource, seoUtilsTarget);
1495
+ }
1496
+ const pkgPath = join5(targetPath, "package.json");
1497
+ if (existsSync6(pkgPath)) {
1498
+ const pkg = await fs.readJson(pkgPath);
1499
+ let updated = false;
1500
+ const requiredDeps = {
1501
+ "react-markdown": "^10.1.0",
1502
+ "remark-gfm": "^4.0.1"
1503
+ };
1504
+ if (!pkg.dependencies) {
1505
+ pkg.dependencies = {};
1506
+ }
1507
+ for (const [dep, version] of Object.entries(requiredDeps)) {
1508
+ if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1509
+ pkg.dependencies[dep] = version;
1510
+ updated = true;
1511
+ }
1512
+ }
1513
+ if (updated) {
1514
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
1515
+ }
1516
+ }
1517
+ const shipdDir = join5(targetPath, ".shipd");
1518
+ const featureDocs = join5(shipdDir, "features");
1519
+ await fs.ensureDir(featureDocs);
1520
+ const featureNote = `# SEO Feature
1521
+ Installed: ${(/* @__PURE__ */ new Date()).toISOString()}
1522
+
1523
+ Includes:
1524
+ - app/sitemap.ts (Automated sitemap generation)
1525
+ - app/robots.txt (Search engine crawler instructions)
1526
+ - app/blog/ (Complete blog structure)
1527
+ - lib/seo-utils.ts (Structured data helpers)
1528
+
1529
+ Notes:
1530
+ - Set NEXT_PUBLIC_APP_URL in .env.local
1531
+ - Update sitemap.ts with your custom routes
1532
+ - Customize blog posts in app/blog/
1533
+ - Add structured data to your pages using lib/seo-utils.ts
1534
+ `;
1535
+ await fs.writeFile(join5(featureDocs, "seo.md"), featureNote);
1536
+ const readmeSource = join5(seoSourcePath, "README.md");
1537
+ if (existsSync6(readmeSource)) {
1538
+ await fs.copyFile(readmeSource, join5(featureDocs, "seo.md"));
1539
+ }
1540
+ const seoConfig = await fs.readJson(join5(seoSourcePath, "feature.config.json")).catch(() => null);
1541
+ if (seoConfig?.envVars) {
1542
+ await addEnvVarsToExample(targetPath, "seo", seoConfig.envVars);
1543
+ }
1544
+ spinner.succeed("SEO feature copied");
1545
+ } catch (error) {
1546
+ spinner.fail("Failed to copy SEO feature");
1547
+ throw error;
1548
+ }
1549
+ }
1550
+ async function appendCommand(options) {
1551
+ try {
1552
+ const cwd = process.cwd();
1553
+ logger.intro("Adding Shipd features to your project");
1554
+ const detectSpinner = ora("Detecting project...").start();
1555
+ const projectInfo = await detectProject(cwd);
1556
+ detectSpinner.succeed("Project validated");
1557
+ logger.info(`\u2713 Next.js App Router project detected`);
1558
+ logger.info(`\u2713 Package manager: ${projectInfo.packageManager}`);
1559
+ if (projectInfo.installedFeatures.length > 0) {
1560
+ logger.info(`\u2713 Installed features: ${projectInfo.installedFeatures.join(", ")}`);
1561
+ }
1562
+ const availableFeatures = ["docs", "marketing-landing", "database", "auth", "payments", "email", "ai-chat", "file-upload", "analytics", "seo"];
1563
+ const alreadyInstalled = projectInfo.installedFeatures.filter((f) => availableFeatures.includes(f));
1564
+ if (alreadyInstalled.length > 0) {
1565
+ logger.warn(`Already installed: ${alreadyInstalled.join(", ")}`);
1566
+ }
1567
+ let selectedFeatures;
1568
+ if (options.features) {
1569
+ selectedFeatures = options.features.split(",").map((f) => f.trim());
1570
+ } else {
1571
+ const answers = await inquirer2.prompt([
1572
+ {
1573
+ type: "checkbox",
1574
+ name: "features",
1575
+ message: "Select features to add:",
1576
+ choices: [
1577
+ { name: "Documentation Pages", value: "docs", checked: false },
1578
+ { name: "Marketing Landing Page", value: "marketing-landing", checked: false },
1579
+ { name: "Database (PostgreSQL + Drizzle)", value: "database", checked: false },
1580
+ { name: "Authentication (Better Auth)", value: "auth", checked: false },
1581
+ { name: "Payments (Polar.sh)", value: "payments", checked: false },
1582
+ { name: "Email (Resend)", value: "email", checked: false },
1583
+ { name: "AI Chat (OpenAI)", value: "ai-chat", checked: false },
1584
+ { name: "File Upload (Cloudflare R2)", value: "file-upload", checked: false },
1585
+ { name: "Analytics (PostHog)", value: "analytics", checked: false },
1586
+ { name: "SEO (Sitemap, Robots, Blog)", value: "seo", checked: false }
1587
+ ]
1588
+ }
1589
+ ]);
1590
+ selectedFeatures = answers.features;
1591
+ if (selectedFeatures.includes("auth") && !selectedFeatures.includes("database")) {
1592
+ selectedFeatures.push("database");
1593
+ logger.info("\u2713 Database will be included automatically (required by auth)");
1594
+ }
1595
+ if (selectedFeatures.includes("payments")) {
1596
+ if (!selectedFeatures.includes("auth")) {
1597
+ selectedFeatures.push("auth");
1598
+ logger.info("\u2713 Auth will be included automatically (required by payments)");
1599
+ }
1600
+ if (!selectedFeatures.includes("database")) {
1601
+ selectedFeatures.push("database");
1602
+ logger.info("\u2713 Database will be included automatically (required by payments)");
1603
+ }
1604
+ }
1605
+ }
1606
+ if (selectedFeatures.length === 0) {
1607
+ logger.warn("No features selected. Exiting.");
1608
+ return;
1609
+ }
1610
+ console.log("\n\u{1F4CB} Changes Preview:");
1611
+ console.log(" Features to add:", selectedFeatures.join(", "));
1612
+ console.log(" Mode:", options.dryRun ? "DRY RUN" : "LIVE");
1613
+ if (!options.dryRun) {
1614
+ const confirm = await inquirer2.prompt([
1615
+ {
1616
+ type: "confirm",
1617
+ name: "proceed",
1618
+ message: "Proceed with installation?",
1619
+ default: true
1620
+ }
1621
+ ]);
1622
+ if (!confirm.proceed) {
1623
+ logger.warn("Installation cancelled");
1624
+ return;
1625
+ }
1626
+ }
1627
+ console.log();
1628
+ const featuresToInstall = [...selectedFeatures];
1629
+ featuresToInstall.sort((a, b) => {
1630
+ const order = ["database", "auth", "payments"];
1631
+ const aIndex = order.indexOf(a);
1632
+ const bIndex = order.indexOf(b);
1633
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1634
+ if (aIndex !== -1) return -1;
1635
+ if (bIndex !== -1) return 1;
1636
+ return 0;
1637
+ });
1638
+ for (const feature of featuresToInstall) {
1639
+ if (feature === "docs") {
1640
+ await appendDocsFeature(cwd, options.dryRun);
1641
+ } else if (feature === "marketing-landing") {
1642
+ await appendMarketingFeature(cwd, options.dryRun);
1643
+ } else if (feature === "database") {
1644
+ await appendDatabaseFeature(cwd, options.dryRun);
1645
+ } else if (feature === "auth") {
1646
+ await appendAuthFeature(cwd, options.dryRun);
1647
+ } else if (feature === "payments") {
1648
+ await appendPaymentsFeature(cwd, options.dryRun);
1649
+ } else if (feature === "email") {
1650
+ await appendEmailFeature(cwd, options.dryRun);
1651
+ } else if (feature === "ai-chat") {
1652
+ await appendAiChatFeature(cwd, options.dryRun);
1653
+ } else if (feature === "file-upload") {
1654
+ await appendFileUploadFeature(cwd, options.dryRun);
1655
+ } else if (feature === "analytics") {
1656
+ await appendAnalyticsFeature(cwd, options.dryRun);
1657
+ } else if (feature === "seo") {
1658
+ await appendSeoFeature(cwd, options.dryRun);
1659
+ }
1660
+ }
1661
+ if (!options.dryRun) {
1662
+ const pkgPath = join5(cwd, "package.json");
1663
+ if (existsSync6(pkgPath)) {
1664
+ const pkg = await fs.readJson(pkgPath);
1665
+ const hasNewDeps = selectedFeatures.some((f) => {
1666
+ return true;
1667
+ });
1668
+ if (hasNewDeps) {
1669
+ const installSpinner = ora("Installing dependencies...").start();
1670
+ try {
1671
+ await execa(projectInfo.packageManager, ["install"], {
1672
+ cwd,
1673
+ stdio: "pipe"
1674
+ });
1675
+ installSpinner.succeed("Dependencies installed");
1676
+ } catch (error) {
1677
+ installSpinner.fail("Failed to install dependencies");
1678
+ logger.warn("You may need to run npm install (or your package manager) manually");
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+ console.log();
1684
+ logger.success("\u2728 Features added successfully!");
1685
+ console.log();
1686
+ console.log("\u{1F4D6} Next steps:");
1687
+ if (selectedFeatures.includes("docs")) {
1688
+ console.log(" \u2713 Visit /docs in your application");
1689
+ console.log(" - Customize the docs in app/docs/");
1690
+ console.log(" - Check .shipd/features/docs.md for details");
1691
+ }
1692
+ if (selectedFeatures.includes("marketing-landing")) {
1693
+ console.log(" \u2713 Visit / (landing page) in your application");
1694
+ console.log(" - Check .shipd/features/marketing-landing.md for details");
1695
+ }
1696
+ if (selectedFeatures.includes("database")) {
1697
+ console.log(" \u2713 Database setup complete");
1698
+ console.log(" - Set DATABASE_URL in .env.local");
1699
+ console.log(" - Run npm run db:push to initialize database");
1700
+ console.log(" - Check .shipd/features/database.md for details");
1701
+ }
1702
+ if (selectedFeatures.includes("auth")) {
1703
+ console.log(" \u2713 Visit /sign-in and /sign-up in your application");
1704
+ console.log(" - Set BETTER_AUTH_SECRET and Google OAuth credentials");
1705
+ console.log(" - Check .shipd/features/auth.md for details");
1706
+ }
1707
+ if (selectedFeatures.includes("payments")) {
1708
+ console.log(" \u2713 Visit /dashboard/payment for subscription management");
1709
+ console.log(" - Set Polar.sh credentials in .env.local");
1710
+ console.log(" - Run npm run db:push to create subscription table");
1711
+ console.log(" - Check .shipd/features/payments.md for details");
1712
+ }
1713
+ if (selectedFeatures.includes("email")) {
1714
+ console.log(" \u2713 Email sending enabled");
1715
+ console.log(" - Set RESEND_API_KEY in .env.local");
1716
+ console.log(" - Auth and Payments modules will use real email sending");
1717
+ console.log(" - Check .shipd/features/email.md for details");
1718
+ }
1719
+ if (selectedFeatures.includes("ai-chat")) {
1720
+ console.log(" \u2713 AI Chat enabled");
1721
+ console.log(" - Visit /dashboard/chat to use the chat interface");
1722
+ console.log(" - Set OPENAI_API_KEY in .env.local");
1723
+ console.log(" - Check .shipd/features/ai-chat.md for details");
1724
+ }
1725
+ if (selectedFeatures.includes("file-upload")) {
1726
+ console.log(" \u2713 File Upload enabled");
1727
+ console.log(" - Visit /dashboard/upload to use the upload interface");
1728
+ console.log(" - Set Cloudflare R2 credentials in .env.local");
1729
+ console.log(" - Configure public domain for R2 bucket");
1730
+ console.log(" - Check .shipd/features/file-upload.md for details");
1731
+ }
1732
+ if (selectedFeatures.includes("analytics")) {
1733
+ console.log(" \u2713 Analytics enabled");
1734
+ console.log(" - Set NEXT_PUBLIC_POSTHOG_KEY in .env.local");
1735
+ console.log(" - Add PostHogProviderWrapper to app/layout.tsx");
1736
+ console.log(" - Check .shipd/features/analytics.md for details");
1737
+ }
1738
+ console.log();
1739
+ console.log("\u{1F680} Ready to build:");
1740
+ logger.step(`${projectInfo.packageManager} run build`);
1741
+ logger.step(`${projectInfo.packageManager} run dev`);
1742
+ console.log();
1743
+ } catch (error) {
1744
+ logger.error("Failed to append features");
1745
+ if (error instanceof Error) {
1746
+ console.error("\n" + error.message);
1747
+ }
1748
+ process.exit(1);
1749
+ }
1750
+ }
1751
+
1752
+ // src/generators/project.ts
1753
+ var __filename3 = fileURLToPath3(import.meta.url);
1754
+ var __dirname3 = dirname3(__filename3);
1755
+ async function generateProject(config) {
1756
+ const targetPath = resolve4(process.cwd(), config.projectName);
1757
+ if (await fs2.pathExists(targetPath)) {
1758
+ const files = await fs2.readdir(targetPath);
1759
+ if (files.length > 0) {
1760
+ const hasPkgJson = await fs2.pathExists(resolve4(targetPath, "package.json"));
1761
+ const hasGit = await fs2.pathExists(resolve4(targetPath, ".git"));
1762
+ const hasNodeModules = await fs2.pathExists(resolve4(targetPath, "node_modules"));
1763
+ if (hasPkgJson || hasGit || hasNodeModules) {
1764
+ throw new Error(
1765
+ `Directory "${config.projectName}" appears to contain an existing project.
1766
+ - package.json: ${hasPkgJson ? "\u2713 found" : "\u2717 not found"}
1767
+ - .git: ${hasGit ? "\u2713 found" : "\u2717 not found"}
1768
+ - node_modules: ${hasNodeModules ? "\u2713 found" : "\u2717 not found"}
1769
+
1770
+ To avoid data loss, shipd init will not overwrite existing projects.
1771
+ Please:
1772
+ 1. Choose a different project name, or
1773
+ 2. Delete the existing directory first, or
1774
+ 3. Use 'shipd append' to add features to the existing project`
1775
+ );
1776
+ }
1777
+ logger.warn(`Directory "${config.projectName}" is not empty but doesn't appear to be a project.`);
1778
+ logger.warn("Existing files may be overwritten. Press Ctrl+C to cancel or wait 3 seconds to continue...");
1779
+ await new Promise((resolve5) => setTimeout(resolve5, 3e3));
1780
+ }
1781
+ }
1782
+ const basePath = getBasePath();
1783
+ const copySpinner = ora2("Copying base package...").start();
1784
+ try {
1785
+ await copyTemplate(basePath, targetPath);
1786
+ copySpinner.succeed("Base package copied");
1787
+ } catch (error) {
1788
+ copySpinner.fail("Failed to copy base package");
1789
+ throw error;
1790
+ }
1791
+ if (config.features.length > 0) {
1792
+ const modulesSpinner = ora2("Adding feature modules...").start();
1793
+ try {
1794
+ const featuresToInstall = [...config.features];
1795
+ if (featuresToInstall.includes("auth") && !featuresToInstall.includes("database")) {
1796
+ featuresToInstall.push("database");
1797
+ logger.info("\u2713 Database will be included automatically (required by auth)");
1798
+ }
1799
+ if (featuresToInstall.includes("payments")) {
1800
+ if (!featuresToInstall.includes("auth")) {
1801
+ featuresToInstall.push("auth");
1802
+ logger.info("\u2713 Auth will be included automatically (required by payments)");
1803
+ }
1804
+ if (!featuresToInstall.includes("database")) {
1805
+ featuresToInstall.push("database");
1806
+ logger.info("\u2713 Database will be included automatically (required by payments)");
1807
+ }
1808
+ }
1809
+ featuresToInstall.sort((a, b) => {
1810
+ const order = ["database", "auth", "payments"];
1811
+ const aIndex = order.indexOf(a);
1812
+ const bIndex = order.indexOf(b);
1813
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1814
+ if (aIndex !== -1) return -1;
1815
+ if (bIndex !== -1) return 1;
1816
+ return 0;
1817
+ });
1818
+ for (const feature of featuresToInstall) {
1819
+ if (feature === "marketing-landing") {
1820
+ await appendMarketingFeature(targetPath, false);
1821
+ } else if (feature === "docs") {
1822
+ await appendDocsFeature(targetPath, false);
1823
+ } else if (feature === "database") {
1824
+ await appendDatabaseFeature(targetPath, false);
1825
+ } else if (feature === "auth") {
1826
+ await appendAuthFeature(targetPath, false);
1827
+ } else if (feature === "payments") {
1828
+ await appendPaymentsFeature(targetPath, false);
1829
+ } else if (feature === "email") {
1830
+ await appendEmailFeature(targetPath, false);
1831
+ } else if (feature === "ai-chat") {
1832
+ await appendAiChatFeature(targetPath, false);
1833
+ } else if (feature === "file-upload") {
1834
+ await appendFileUploadFeature(targetPath, false);
1835
+ } else if (feature === "analytics") {
1836
+ await appendAnalyticsFeature(targetPath, false);
1837
+ } else if (feature === "seo") {
1838
+ await appendSeoFeature(targetPath, false);
1839
+ }
1840
+ }
1841
+ modulesSpinner.succeed(`Added ${featuresToInstall.length} feature module(s)`);
1842
+ } catch (error) {
1843
+ modulesSpinner.fail("Failed to add feature modules");
1844
+ throw error;
1845
+ }
1846
+ }
1847
+ const varsSpinner = ora2("Replacing template variables...").start();
1848
+ try {
1849
+ await replaceTemplateVariables(targetPath, config);
1850
+ varsSpinner.succeed("Variables replaced");
1851
+ } catch (error) {
1852
+ varsSpinner.fail("Failed to replace variables");
1853
+ throw error;
1854
+ }
1855
+ const gitSpinner = ora2("Initializing git repository...").start();
1856
+ try {
1857
+ await execa2("git", ["init"], { cwd: targetPath });
1858
+ await execa2("git", ["add", "."], { cwd: targetPath });
1859
+ await execa2("git", ["commit", "-m", "Initial commit from Shipd"], { cwd: targetPath });
1860
+ gitSpinner.succeed("Git repository initialized");
1861
+ } catch (error) {
1862
+ gitSpinner.fail("Failed to initialize git");
1863
+ throw error;
1864
+ }
1865
+ let installInterval;
1866
+ const installSpinner = ora2("Preparing to install dependencies...").start();
1867
+ try {
1868
+ const pkgJson = await fs2.readJson(resolve4(targetPath, "package.json"));
1869
+ const deps = Object.keys(pkgJson.dependencies || {}).length;
1870
+ const devDeps = Object.keys(pkgJson.devDependencies || {}).length;
1871
+ const totalDeps = deps + devDeps;
1872
+ installSpinner.text = `Installing ${totalDeps} dependencies with ${config.packageManager}...
1873
+ This may take 2-5 minutes depending on your connection`;
1874
+ const startTime = Date.now();
1875
+ installInterval = setInterval(() => {
1876
+ const elapsed = Math.floor((Date.now() - startTime) / 1e3);
1877
+ installSpinner.text = `Installing ${totalDeps} dependencies (${elapsed}s elapsed)...
1878
+ ${config.packageManager} install is running in background`;
1879
+ }, 2e3);
1880
+ await execa2(config.packageManager, ["install"], {
1881
+ cwd: targetPath,
1882
+ stdio: "pipe"
1883
+ });
1884
+ clearInterval(installInterval);
1885
+ const totalTime = Math.floor((Date.now() - startTime) / 1e3);
1886
+ installSpinner.succeed(`Dependencies installed (${totalDeps} packages in ${totalTime}s)`);
1887
+ } catch (error) {
1888
+ if (installInterval) {
1889
+ clearInterval(installInterval);
1890
+ }
1891
+ installSpinner.fail("Failed to install dependencies");
1892
+ throw error;
1893
+ }
1894
+ logger.success(`
1895
+ Project "${config.projectName}" created successfully at:
1896
+ ${targetPath}
1897
+ `);
697
1898
  console.log("\nNext steps:");
698
1899
  logger.step(`cd "${targetPath}"`);
699
1900
  logger.step("Copy .env.example to .env.local and configure your services");
700
1901
  logger.step(`${config.packageManager} run dev`);
701
1902
  logger.info('Tip: If you see "Could not read package.json", you are not in the project directory yet.');
1903
+ if (config.features.includes("marketing-landing")) {
1904
+ console.log("\n\u{1F3A8} Marketing Landing Page:");
1905
+ logger.info("Visit / to see your landing page");
1906
+ }
1907
+ if (config.features.includes("docs")) {
1908
+ console.log("\n\u{1F4DA} Documentation Pages:");
1909
+ logger.info("Visit /docs to see your documentation");
1910
+ }
1911
+ if (config.features.includes("database")) {
1912
+ console.log("\n\u{1F4BE} Database:");
1913
+ logger.info("Set DATABASE_URL in .env.local and run npm run db:push");
1914
+ }
1915
+ if (config.features.includes("auth")) {
1916
+ console.log("\n\u{1F510} Authentication:");
1917
+ logger.info("Set BETTER_AUTH_SECRET and Google OAuth credentials in .env.local");
1918
+ logger.info("Visit /sign-in or /sign-up to test authentication");
1919
+ }
702
1920
  if (config.features.includes("payments")) {
703
- console.log("\n\u{1F4B3} Polar.sh Setup Required:");
704
- logger.info("Visit https://polar.sh to create your account and configure payment tiers");
1921
+ console.log("\n\u{1F4B3} Payments:");
1922
+ logger.info("Set Polar.sh credentials in .env.local");
1923
+ logger.info("Visit /dashboard/payment for subscription management");
1924
+ }
1925
+ if (config.features.includes("email")) {
1926
+ console.log("\n\u{1F4E7} Email:");
1927
+ logger.info("Set RESEND_API_KEY in .env.local to enable email sending");
705
1928
  }
706
1929
  if (config.features.includes("ai-chat")) {
707
- console.log("\n\u{1F916} OpenAI Setup Required:");
708
- logger.info("Get your API key from https://platform.openai.com");
1930
+ console.log("\n\u{1F916} AI Chat:");
1931
+ logger.info("Set OPENAI_API_KEY in .env.local");
1932
+ logger.info("Visit /dashboard/chat to use the chat interface");
709
1933
  }
710
1934
  if (config.features.includes("file-upload")) {
711
- console.log("\n\u{1F4C1} Cloudflare R2 Setup Required:");
712
- logger.info("Create an R2 bucket in your Cloudflare dashboard");
1935
+ console.log("\n\u{1F4E4} File Upload:");
1936
+ logger.info("Set Cloudflare R2 credentials in .env.local");
1937
+ logger.info("Visit /dashboard/upload to use the upload interface");
713
1938
  }
714
1939
  if (config.features.includes("analytics")) {
715
- console.log("\n\u{1F4CA} PostHog Setup Required:");
716
- logger.info("Create a project at https://posthog.com");
1940
+ console.log("\n\u{1F4CA} Analytics:");
1941
+ logger.info("Set NEXT_PUBLIC_POSTHOG_KEY in .env.local");
1942
+ logger.info("Add PostHogProviderWrapper to app/layout.tsx");
1943
+ }
1944
+ if (config.features.length === 0) {
1945
+ console.log("\n\u{1F4A1} Tip:");
1946
+ logger.info("You can add more features later using: shipd append");
717
1947
  }
718
1948
  console.log("\n");
719
1949
  }
720
1950
 
721
1951
  // src/auth/token-storage.ts
722
- import fs3 from "fs-extra";
1952
+ import fs4 from "fs-extra";
723
1953
 
724
1954
  // src/config/paths.ts
725
1955
  import os from "os";
726
1956
  import path from "path";
727
- import fs2 from "fs-extra";
1957
+ import fs3 from "fs-extra";
728
1958
  function getConfigDir() {
729
1959
  const homeDir = os.homedir();
730
1960
  const configDir = path.join(homeDir, ".shipd");
731
- fs2.ensureDirSync(configDir);
1961
+ fs3.ensureDirSync(configDir);
732
1962
  return configDir;
733
1963
  }
734
1964
  function getAuthPath() {
@@ -739,10 +1969,10 @@ function getAuthPath() {
739
1969
  function loadAuth() {
740
1970
  const authPath = getAuthPath();
741
1971
  try {
742
- if (!fs3.existsSync(authPath)) {
1972
+ if (!fs4.existsSync(authPath)) {
743
1973
  return null;
744
1974
  }
745
- const data = fs3.readJsonSync(authPath);
1975
+ const data = fs4.readJsonSync(authPath);
746
1976
  return data;
747
1977
  } catch (error) {
748
1978
  console.error("Failed to load auth data:", error);
@@ -752,7 +1982,7 @@ function loadAuth() {
752
1982
  function saveAuth(authData) {
753
1983
  const authPath = getAuthPath();
754
1984
  try {
755
- fs3.writeJsonSync(authPath, authData, { spaces: 2 });
1985
+ fs4.writeJsonSync(authPath, authData, { spaces: 2 });
756
1986
  } catch (error) {
757
1987
  console.error("Failed to save auth data:", error);
758
1988
  throw new Error("Failed to save authentication data");
@@ -761,8 +1991,8 @@ function saveAuth(authData) {
761
1991
  function clearAuth() {
762
1992
  const authPath = getAuthPath();
763
1993
  try {
764
- if (fs3.existsSync(authPath)) {
765
- fs3.removeSync(authPath);
1994
+ if (fs4.existsSync(authPath)) {
1995
+ fs4.removeSync(authPath);
766
1996
  }
767
1997
  } catch (error) {
768
1998
  console.error("Failed to clear auth data:", error);
@@ -783,128 +2013,18 @@ function isAuthenticated() {
783
2013
  return true;
784
2014
  }
785
2015
 
786
- // src/auth/token-validator.ts
787
- var API_BASE_URL = process.env.SAAS_SCAFFOLD_API_URL || "https://www.shipd.dev";
788
- async function validateToken(token) {
789
- const authToken = token || loadAuth()?.token;
790
- if (!authToken) {
791
- return { valid: false, error: "No authentication token found" };
792
- }
793
- try {
794
- const response = await fetch(`${API_BASE_URL}/api/cli/validate`, {
795
- method: "POST",
796
- headers: {
797
- "Content-Type": "application/json"
798
- },
799
- body: JSON.stringify({ token: authToken })
800
- });
801
- if (!response.ok) {
802
- clearAuth();
803
- return { valid: false, error: "Invalid or expired token" };
804
- }
805
- const data = await response.json();
806
- if (data.subscriptionStatus && data.subscriptionStatus !== "active") {
807
- clearAuth();
808
- return {
809
- valid: false,
810
- error: `Subscription ${data.subscriptionStatus}. Please renew at ${API_BASE_URL}/pricing`
811
- };
812
- }
813
- return data;
814
- } catch (error) {
815
- console.error("Token validation failed:", error);
816
- return {
817
- valid: false,
818
- error: "Unable to validate token. Please check your internet connection."
819
- };
820
- }
821
- }
822
- function isDevelopmentMode() {
823
- return process.env.SAAS_SCAFFOLD_DEV === "true";
824
- }
825
- async function ensureAuthenticated(mockMode = false) {
826
- if (mockMode || isDevelopmentMode()) {
827
- console.log("\x1B[33m\u26A0\x1B[0m Running in mock mode (authentication bypassed)");
828
- return;
829
- }
830
- const auth = loadAuth();
831
- if (!auth) {
832
- console.error("\n\u274C Not authenticated.");
833
- console.error("\nPlease run: \x1B[36mshipd login\x1B[0m\n");
834
- console.error("\x1B[2mTip: Use --mock flag to skip authentication during development\x1B[0m\n");
835
- process.exit(1);
836
- }
837
- const expiresAt = new Date(auth.expiresAt);
838
- const now = /* @__PURE__ */ new Date();
839
- if (now >= expiresAt) {
840
- clearAuth();
841
- console.error("\n\u274C Session expired.");
842
- console.error("\nPlease run: \x1B[36mshipd login\x1B[0m\n");
843
- process.exit(1);
844
- }
845
- const validation = await validateToken(auth.token);
846
- if (!validation.valid) {
847
- console.error(`
848
- \u274C Authentication failed: ${validation.error}`);
849
- console.error("\nPlease run: \x1B[36mshipd login\x1B[0m\n");
850
- process.exit(1);
851
- }
852
- console.log("\x1B[32m\u2713\x1B[0m Authenticated");
853
- }
854
-
855
- // src/commands/init.ts
856
- async function initCommand(projectName, options) {
857
- try {
858
- await ensureAuthenticated(options?.mock || false);
859
- console.log(SHIPD_ASCII);
860
- logger.intro("Welcome aboard! Let's get shipping");
861
- const config = await runPrompts(projectName);
862
- await generateProject(config);
863
- } catch (error) {
864
- logger.error("Project generation failed");
865
- if (error instanceof Error) {
866
- console.error("\n" + error.message);
867
- }
868
- process.exit(1);
869
- }
870
- }
871
-
872
- // src/commands/list.ts
873
- import chalk2 from "chalk";
874
- async function listCommand() {
875
- logger.intro("Available Features");
876
- console.log("\n" + chalk2.bold("Core (Always Included):"));
877
- console.log(" \u2022 Next.js 15 with App Router");
878
- console.log(" \u2022 Better Auth + Google OAuth");
879
- console.log(" \u2022 Neon PostgreSQL + Drizzle ORM");
880
- console.log(" \u2022 shadcn/ui components (24 components)");
881
- console.log(" \u2022 Dark/Light mode");
882
- console.log(" \u2022 User dashboard");
883
- console.log("\n" + chalk2.bold("Optional Features:"));
884
- console.log(" \u2022 Polar.sh payments & subscriptions");
885
- console.log(" \u2022 OpenAI chat integration");
886
- console.log(" \u2022 Cloudflare R2 file uploads");
887
- console.log(" \u2022 PostHog analytics");
888
- console.log("\n" + chalk2.bold("Usage:"));
889
- console.log(" npx shipd init <project-name>");
890
- console.log("");
891
- }
892
-
893
- // src/commands/login.ts
894
- import { Command } from "commander";
895
-
896
2016
  // src/auth/auth-manager.ts
897
2017
  import { nanoid } from "nanoid";
898
2018
  import open from "open";
899
- import ora2 from "ora";
900
- var API_BASE_URL2 = process.env.SAAS_SCAFFOLD_API_URL || "https://www.shipd.dev";
2019
+ import ora3 from "ora";
2020
+ var API_BASE_URL = process.env.SAAS_SCAFFOLD_API_URL || "https://www.shipd.dev";
901
2021
  var POLL_INTERVAL = 2e3;
902
2022
  var MAX_POLL_TIME = 3e5;
903
2023
  function generateDeviceCode() {
904
2024
  return nanoid(10).toUpperCase().match(/.{1,4}/g)?.join("-") || nanoid(10);
905
2025
  }
906
2026
  async function openAuthPage(deviceCode) {
907
- const authUrl = `${API_BASE_URL2}/cli/auth?code=${deviceCode}`;
2027
+ const authUrl = `${API_BASE_URL}/cli/auth?code=${deviceCode}`;
908
2028
  console.log("\n\x1B[36m\u2192\x1B[0m Opening browser for authentication...");
909
2029
  console.log(`\x1B[2m ${authUrl}\x1B[0m
910
2030
  `);
@@ -921,7 +2041,7 @@ async function pollForAuth(deviceCode) {
921
2041
  const startTime = Date.now();
922
2042
  while (Date.now() - startTime < MAX_POLL_TIME) {
923
2043
  try {
924
- const response = await fetch(`${API_BASE_URL2}/api/cli/poll?code=${deviceCode}`, {
2044
+ const response = await fetch(`${API_BASE_URL}/api/cli/poll?code=${deviceCode}`, {
925
2045
  method: "GET"
926
2046
  });
927
2047
  if (!response.ok) {
@@ -965,7 +2085,7 @@ async function login() {
965
2085
  const deviceCode = generateDeviceCode();
966
2086
  console.log(`\x1B[2mDevice code: ${deviceCode}\x1B[0m`);
967
2087
  await openAuthPage(deviceCode);
968
- const spinner = ora2({
2088
+ const spinner = ora3({
969
2089
  text: "Waiting for authentication...",
970
2090
  color: "cyan"
971
2091
  }).start();
@@ -987,7 +2107,118 @@ async function login() {
987
2107
  }
988
2108
  }
989
2109
 
2110
+ // src/auth/token-validator.ts
2111
+ var API_BASE_URL2 = process.env.SAAS_SCAFFOLD_API_URL || "https://www.shipd.dev";
2112
+ async function validateToken(token) {
2113
+ const authToken = token || loadAuth()?.token;
2114
+ if (!authToken) {
2115
+ return { valid: false, error: "No authentication token found" };
2116
+ }
2117
+ try {
2118
+ const response = await fetch(`${API_BASE_URL2}/api/cli/validate`, {
2119
+ method: "POST",
2120
+ headers: {
2121
+ "Content-Type": "application/json"
2122
+ },
2123
+ body: JSON.stringify({ token: authToken })
2124
+ });
2125
+ if (!response.ok) {
2126
+ clearAuth();
2127
+ return { valid: false, error: "Invalid or expired token" };
2128
+ }
2129
+ const data = await response.json();
2130
+ if (data.subscriptionStatus && data.subscriptionStatus !== "active") {
2131
+ clearAuth();
2132
+ return {
2133
+ valid: false,
2134
+ error: `Subscription ${data.subscriptionStatus}. Please renew at ${API_BASE_URL2}/pricing`
2135
+ };
2136
+ }
2137
+ return data;
2138
+ } catch (error) {
2139
+ console.error("Token validation failed:", error);
2140
+ return {
2141
+ valid: false,
2142
+ error: "Unable to validate token. Please check your internet connection."
2143
+ };
2144
+ }
2145
+ }
2146
+ function isDevelopmentMode() {
2147
+ return process.env.SAAS_SCAFFOLD_DEV === "true";
2148
+ }
2149
+ async function ensureAuthenticated(mockMode = false) {
2150
+ if (mockMode || isDevelopmentMode()) {
2151
+ console.log("\x1B[33m\u26A0\x1B[0m Running in mock mode (authentication bypassed)");
2152
+ return;
2153
+ }
2154
+ const auth = loadAuth();
2155
+ if (!auth) {
2156
+ console.log("\n\u{1F510} Authentication required");
2157
+ console.log("Starting login flow...\n");
2158
+ await login();
2159
+ return;
2160
+ }
2161
+ const expiresAt = new Date(auth.expiresAt);
2162
+ const now = /* @__PURE__ */ new Date();
2163
+ if (now >= expiresAt) {
2164
+ clearAuth();
2165
+ console.log("\n\u{1F510} Session expired");
2166
+ console.log("Starting login flow...\n");
2167
+ await login();
2168
+ return;
2169
+ }
2170
+ const validation = await validateToken(auth.token);
2171
+ if (!validation.valid) {
2172
+ clearAuth();
2173
+ console.log(`
2174
+ \u{1F510} ${validation.error || "Authentication failed"}`);
2175
+ console.log("Starting login flow...\n");
2176
+ await login();
2177
+ return;
2178
+ }
2179
+ console.log("\x1B[32m\u2713\x1B[0m Authenticated");
2180
+ }
2181
+
2182
+ // src/commands/init.ts
2183
+ async function initCommand(projectName, options) {
2184
+ try {
2185
+ await ensureAuthenticated(options?.mock || false);
2186
+ console.log(SHIPD_ASCII);
2187
+ logger.intro("Welcome aboard! Let's get shipping");
2188
+ const config = await runPrompts(projectName, options);
2189
+ await generateProject(config);
2190
+ } catch (error) {
2191
+ logger.error("Project generation failed");
2192
+ if (error instanceof Error) {
2193
+ console.error("\n" + error.message);
2194
+ }
2195
+ process.exit(1);
2196
+ }
2197
+ }
2198
+
2199
+ // src/commands/list.ts
2200
+ import chalk2 from "chalk";
2201
+ async function listCommand() {
2202
+ logger.intro("Available Features");
2203
+ console.log("\n" + chalk2.bold("Core (Always Included):"));
2204
+ console.log(" \u2022 Next.js 15 with App Router");
2205
+ console.log(" \u2022 Better Auth + Google OAuth");
2206
+ console.log(" \u2022 Neon PostgreSQL + Drizzle ORM");
2207
+ console.log(" \u2022 shadcn/ui components (24 components)");
2208
+ console.log(" \u2022 Dark/Light mode");
2209
+ console.log(" \u2022 User dashboard");
2210
+ console.log("\n" + chalk2.bold("Optional Features:"));
2211
+ console.log(" \u2022 Polar.sh payments & subscriptions");
2212
+ console.log(" \u2022 OpenAI chat integration");
2213
+ console.log(" \u2022 Cloudflare R2 file uploads");
2214
+ console.log(" \u2022 PostHog analytics");
2215
+ console.log("\n" + chalk2.bold("Usage:"));
2216
+ console.log(" npx shipd init <project-name>");
2217
+ console.log("");
2218
+ }
2219
+
990
2220
  // src/commands/login.ts
2221
+ import { Command } from "commander";
991
2222
  function createLoginCommand() {
992
2223
  const command = new Command("login");
993
2224
  command.description("Authenticate with shipd").action(async () => {
@@ -1023,341 +2254,12 @@ function createLogoutCommand() {
1023
2254
  return command;
1024
2255
  }
1025
2256
 
1026
- // src/commands/append.ts
1027
- import inquirer2 from "inquirer";
1028
- import { existsSync as existsSync5 } from "fs";
1029
- import { join as join7, resolve as resolve4, dirname as dirname2 } from "path";
1030
- import { fileURLToPath as fileURLToPath2 } from "url";
1031
- import ora3 from "ora";
1032
- import fs4 from "fs-extra";
1033
- var __filename2 = fileURLToPath2(import.meta.url);
1034
- var __dirname2 = dirname2(__filename2);
1035
- async function detectProject(cwd) {
1036
- const pkgPath = join7(cwd, "package.json");
1037
- if (!existsSync5(pkgPath)) {
1038
- throw new Error("No package.json found. Make sure you're in a Next.js project directory.");
1039
- }
1040
- const pkg = await fs4.readJson(pkgPath);
1041
- const isNextJs = !!(pkg.dependencies?.next || pkg.devDependencies?.next);
1042
- if (!isNextJs) {
1043
- throw new Error("This doesn't appear to be a Next.js project. shipd append requires a Next.js project.");
1044
- }
1045
- const hasAppRouter = existsSync5(join7(cwd, "app"));
1046
- if (!hasAppRouter) {
1047
- throw new Error("App Router not detected. shipd append requires Next.js App Router (app directory).");
1048
- }
1049
- let packageManager = "npm";
1050
- if (existsSync5(join7(cwd, "pnpm-lock.yaml"))) {
1051
- packageManager = "pnpm";
1052
- } else if (existsSync5(join7(cwd, "yarn.lock"))) {
1053
- packageManager = "yarn";
1054
- }
1055
- const installedFeatures = [];
1056
- const shipdManifest = join7(cwd, ".shipd", "manifest.json");
1057
- if (existsSync5(shipdManifest)) {
1058
- const manifest = await fs4.readJson(shipdManifest);
1059
- installedFeatures.push(...Object.keys(manifest.features || {}));
1060
- }
1061
- return {
1062
- isNextJs,
1063
- hasAppRouter,
1064
- packageManager,
1065
- installedFeatures
1066
- };
1067
- }
1068
- async function appendDocsFeature(targetPath, dryRun = false) {
1069
- let docsSourcePath = resolve4(__dirname2, "../docs-template");
1070
- if (!existsSync5(docsSourcePath)) {
1071
- docsSourcePath = resolve4(__dirname2, "../../docs-template");
1072
- }
1073
- if (!existsSync5(docsSourcePath)) {
1074
- docsSourcePath = resolve4(__dirname2, "../../../docs-template");
1075
- }
1076
- if (!existsSync5(docsSourcePath)) {
1077
- throw new Error(`Docs template not found. Tried paths:
1078
- - ${resolve4(__dirname2, "../docs-template")}
1079
- - ${resolve4(__dirname2, "../../docs-template")}
1080
- - ${resolve4(__dirname2, "../../../docs-template")}`);
1081
- }
1082
- const spinner = ora3("Copying docs feature (pages + components)...").start();
1083
- try {
1084
- if (dryRun) {
1085
- spinner.info("DRY RUN: Would copy docs pages and components from template");
1086
- return;
1087
- }
1088
- const docsTargetPath = join7(targetPath, "app", "docs");
1089
- await fs4.ensureDir(docsTargetPath);
1090
- await fs4.copy(docsSourcePath, docsTargetPath, {
1091
- overwrite: false,
1092
- errorOnExist: false,
1093
- filter: (src) => {
1094
- if (src.includes("/components/")) {
1095
- return false;
1096
- }
1097
- const relativePath = src.replace(docsSourcePath, "");
1098
- const targetFile = join7(docsTargetPath, relativePath);
1099
- if (existsSync5(targetFile)) {
1100
- logger.warn(`Skipping existing file: ${relativePath}`);
1101
- return false;
1102
- }
1103
- return true;
1104
- }
1105
- });
1106
- const componentsSourcePath = join7(docsSourcePath, "components", "docs");
1107
- const componentsTargetPath = join7(targetPath, "components", "docs");
1108
- if (existsSync5(componentsSourcePath)) {
1109
- await fs4.ensureDir(componentsTargetPath);
1110
- await fs4.copy(componentsSourcePath, componentsTargetPath, {
1111
- overwrite: false,
1112
- errorOnExist: false,
1113
- filter: (src) => {
1114
- const relativePath = src.replace(componentsSourcePath, "");
1115
- const targetFile = join7(componentsTargetPath, relativePath);
1116
- if (existsSync5(targetFile)) {
1117
- logger.warn(`Skipping existing component: ${relativePath}`);
1118
- return false;
1119
- }
1120
- return true;
1121
- }
1122
- });
1123
- }
1124
- const uiComponentsSourcePath = join7(docsSourcePath, "components", "ui");
1125
- const uiComponentsTargetPath = join7(targetPath, "components", "ui");
1126
- if (existsSync5(uiComponentsSourcePath)) {
1127
- await fs4.ensureDir(uiComponentsTargetPath);
1128
- await fs4.copy(uiComponentsSourcePath, uiComponentsTargetPath, {
1129
- overwrite: false,
1130
- errorOnExist: false,
1131
- filter: (src) => {
1132
- const relativePath = src.replace(uiComponentsSourcePath, "");
1133
- const targetFile = join7(uiComponentsTargetPath, relativePath);
1134
- if (existsSync5(targetFile)) {
1135
- return false;
1136
- }
1137
- return true;
1138
- }
1139
- });
1140
- }
1141
- const utilsSourcePath = join7(docsSourcePath, "lib", "utils.ts");
1142
- const utilsTargetPath = join7(targetPath, "lib", "utils.ts");
1143
- if (existsSync5(utilsSourcePath) && !existsSync5(utilsTargetPath)) {
1144
- await fs4.ensureDir(join7(targetPath, "lib"));
1145
- await fs4.copy(utilsSourcePath, utilsTargetPath);
1146
- } else if (existsSync5(utilsTargetPath)) {
1147
- }
1148
- spinner.succeed("Docs feature copied (pages + components)");
1149
- const shipdDir = join7(targetPath, ".shipd");
1150
- await fs4.ensureDir(shipdDir);
1151
- const manifestPath = join7(shipdDir, "manifest.json");
1152
- let manifest = { version: "1.0.0", features: {} };
1153
- if (existsSync5(manifestPath)) {
1154
- manifest = await fs4.readJson(manifestPath);
1155
- }
1156
- manifest.features.docs = {
1157
- version: "1.0.0",
1158
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1159
- files: [
1160
- "app/docs/**/*",
1161
- "components/docs/**/*",
1162
- "components/ui/**/*",
1163
- "lib/utils.ts"
1164
- ]
1165
- };
1166
- await fs4.writeJson(manifestPath, manifest, { spaces: 2 });
1167
- const pkgPath = join7(targetPath, "package.json");
1168
- if (existsSync5(pkgPath)) {
1169
- const pkg = await fs4.readJson(pkgPath);
1170
- let updated = false;
1171
- const requiredDeps = {
1172
- "@radix-ui/react-slot": "^1.1.0",
1173
- "@radix-ui/react-dialog": "^1.1.0",
1174
- "class-variance-authority": "^0.7.0",
1175
- "clsx": "^2.1.1",
1176
- "tailwind-merge": "^2.5.4",
1177
- "lucide-react": "^0.469.0"
1178
- };
1179
- if (!pkg.dependencies) {
1180
- pkg.dependencies = {};
1181
- }
1182
- for (const [dep, version] of Object.entries(requiredDeps)) {
1183
- if (!pkg.dependencies[dep] && !pkg.devDependencies?.[dep]) {
1184
- pkg.dependencies[dep] = version;
1185
- updated = true;
1186
- }
1187
- }
1188
- if (updated) {
1189
- await fs4.writeJson(pkgPath, pkg, { spaces: 2 });
1190
- logger.info("Added missing dependencies to package.json");
1191
- logger.info("Run npm install (or your package manager) to install them");
1192
- }
1193
- }
1194
- const featureDocs = join7(shipdDir, "features");
1195
- await fs4.ensureDir(featureDocs);
1196
- const docsReadme = `# Docs Feature - Integration Guide
1197
-
1198
- **Installed:** ${(/* @__PURE__ */ new Date()).toISOString()}
1199
- **Version:** 1.0.0
1200
-
1201
- ## Overview
1202
-
1203
- This feature adds a complete, standalone documentation section to your Next.js application. All required components are included - no external dependencies needed.
1204
-
1205
- ## Files Added
1206
-
1207
- ### Pages
1208
- - \`app/docs/page.tsx\` - Docs landing page
1209
- - \`app/docs/layout.tsx\` - Docs layout with header and sidebar
1210
- - \`app/docs/[slug]/page.tsx\` - Dynamic doc pages
1211
- - \`app/docs/[slug]/[subslug]/page.tsx\` - Nested doc pages
1212
- - \`app/docs/api/page.tsx\` - API documentation page
1213
- - \`app/docs/documentation/page.tsx\` - Documentation page
1214
-
1215
- ### Components (Standalone Package)
1216
- All required components are included:
1217
- - \`components/docs/docs-header.tsx\` - Docs header with navigation
1218
- - \`components/docs/docs-sidebar.tsx\` - Sidebar navigation
1219
- - \`components/docs/docs-toc.tsx\` - Table of contents component
1220
- - \`components/docs/docs-category-page.tsx\` - Category page layout
1221
- - \`components/docs/docs-code-card.tsx\` - Code snippet display
1222
- - \`components/docs/docs-nav.ts\` - Navigation data structure
1223
-
1224
- ## Dependencies
1225
-
1226
- This feature is **completely standalone** and includes:
1227
- - \u2705 All required UI components (\`components/ui/*\`)
1228
- - \u2705 Utility functions (\`lib/utils.ts\`)
1229
- - \u2705 All docs-specific components (\`components/docs/*\`)
1230
-
1231
- **Smart Deduplication:** If UI components already exist in your project, they won't be overwritten.
1232
-
1233
- **Package Dependencies:** The following will be added to your \`package.json\` if missing:
1234
- - \`@radix-ui/react-slot\` - For Button component
1235
- - \`@radix-ui/react-dialog\` - For Sheet component
1236
- - \`class-variance-authority\` - For component variants
1237
- - \`clsx\` & \`tailwind-merge\` - For className utilities
1238
- - \`lucide-react\` - For icons
1239
-
1240
- **Note:** Run \`npm install\` (or your package manager) after appending to install any new dependencies.
1241
-
1242
- ## Setup Instructions
1243
-
1244
- 1. **Access the docs**
1245
- - Visit \`/docs\` in your application
1246
- - The docs section is fully functional with navigation and layout
1247
-
1248
- 2. **Customize content**
1249
- - Edit pages in \`app/docs/\` to customize content
1250
- - Update navigation in \`components/docs/docs-nav.ts\`
1251
- - Modify components in \`components/docs/\` to change styling/behavior
1252
-
1253
- 3. **Add your own documentation**
1254
- - Create new pages in \`app/docs/\`
1255
- - Follow the existing page structure
1256
- - Add entries to \`docs-nav.ts\` to include in sidebar
1257
-
1258
- ## Next Steps
1259
-
1260
- - Customize the docs landing page in \`app/docs/page.tsx\`
1261
- - Update navigation structure in \`components/docs/docs-nav.ts\`
1262
- - Add your own documentation pages
1263
- - Customize styling in components if needed
1264
-
1265
- ## Feature Status
1266
-
1267
- \u2705 **Standalone Package** - All components included
1268
- \u2705 **No Missing Dependencies** - Everything needed is bundled
1269
- \u2705 **Ready to Use** - Works immediately after append
1270
- `;
1271
- await fs4.writeFile(join7(featureDocs, "docs.md"), docsReadme);
1272
- } catch (error) {
1273
- spinner.fail("Failed to copy docs pages");
1274
- throw error;
1275
- }
1276
- }
1277
- async function appendCommand(options) {
1278
- try {
1279
- const cwd = process.cwd();
1280
- logger.intro("Adding Shipd features to your project");
1281
- const detectSpinner = ora3("Detecting project...").start();
1282
- const projectInfo = await detectProject(cwd);
1283
- detectSpinner.succeed("Project validated");
1284
- logger.info(`\u2713 Next.js App Router project detected`);
1285
- logger.info(`\u2713 Package manager: ${projectInfo.packageManager}`);
1286
- if (projectInfo.installedFeatures.length > 0) {
1287
- logger.info(`\u2713 Installed features: ${projectInfo.installedFeatures.join(", ")}`);
1288
- }
1289
- const availableFeatures = ["docs"];
1290
- const alreadyInstalled = projectInfo.installedFeatures.filter((f) => availableFeatures.includes(f));
1291
- if (alreadyInstalled.includes("docs")) {
1292
- logger.warn("Docs feature is already installed!");
1293
- return;
1294
- }
1295
- let selectedFeatures;
1296
- if (options.features) {
1297
- selectedFeatures = options.features.split(",").map((f) => f.trim());
1298
- } else {
1299
- const answers = await inquirer2.prompt([
1300
- {
1301
- type: "checkbox",
1302
- name: "features",
1303
- message: "Select features to add:",
1304
- choices: [
1305
- { name: "Documentation Pages", value: "docs", checked: true }
1306
- ]
1307
- }
1308
- ]);
1309
- selectedFeatures = answers.features;
1310
- }
1311
- if (selectedFeatures.length === 0) {
1312
- logger.warn("No features selected. Exiting.");
1313
- return;
1314
- }
1315
- console.log("\n\u{1F4CB} Changes Preview:");
1316
- console.log(" Features to add:", selectedFeatures.join(", "));
1317
- console.log(" Mode:", options.dryRun ? "DRY RUN" : "LIVE");
1318
- if (!options.dryRun) {
1319
- const confirm = await inquirer2.prompt([
1320
- {
1321
- type: "confirm",
1322
- name: "proceed",
1323
- message: "Proceed with installation?",
1324
- default: true
1325
- }
1326
- ]);
1327
- if (!confirm.proceed) {
1328
- logger.warn("Installation cancelled");
1329
- return;
1330
- }
1331
- }
1332
- console.log();
1333
- for (const feature of selectedFeatures) {
1334
- if (feature === "docs") {
1335
- await appendDocsFeature(cwd, options.dryRun);
1336
- }
1337
- }
1338
- console.log();
1339
- logger.success("\u2728 Features added successfully!");
1340
- console.log();
1341
- console.log("\u{1F4D6} Next steps:");
1342
- console.log(" 1. Visit /docs in your application");
1343
- console.log(" 2. Customize the docs in app/docs/");
1344
- console.log(" 3. Check .shipd/features/docs.md for details");
1345
- console.log();
1346
- } catch (error) {
1347
- logger.error("Failed to append features");
1348
- if (error instanceof Error) {
1349
- console.error("\n" + error.message);
1350
- }
1351
- process.exit(1);
1352
- }
1353
- }
1354
-
1355
2257
  // src/index.ts
1356
2258
  var program = new Command3();
1357
2259
  program.name("shipd").description("Generate production-ready SaaS applications").version("0.1.0");
1358
2260
  program.addCommand(createLoginCommand());
1359
2261
  program.addCommand(createLogoutCommand());
1360
- program.command("init [project-name]").description("Initialize a new SaaS project").option("--mock", "Skip authentication (for development/testing)").action((projectName, options) => initCommand(projectName, options));
2262
+ program.command("init [project-name]").description("Initialize a new SaaS project").option("--mock", "Skip authentication (for development/testing)").option("--features <features>", "Comma-separated list of features to include (e.g., docs,auth,payments)").option("--description <description>", "Project description").option("--package-manager <manager>", "Package manager (npm, pnpm, yarn)").action((projectName, options) => initCommand(projectName, options));
1361
2263
  program.command("list").description("List available features").action(listCommand);
1362
2264
  program.command("append").description("Add Shipd features to an existing Next.js project").option("--features <features>", "Comma-separated list of features to add (e.g., docs)").option("--dry-run", "Preview changes without applying them").action((options) => appendCommand(options));
1363
2265
  async function interactiveMode() {