create-skit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +36 -0
  2. package/bin/create-skit.mjs +1064 -0
  3. package/lib/module-application.mjs +281 -0
  4. package/lib/module-resolver.mjs +179 -0
  5. package/modules/README.md +22 -0
  6. package/modules/ai-dx/files/AGENTS.md +116 -0
  7. package/modules/ai-dx/files/ARCHITECTURE.md +103 -0
  8. package/modules/ai-dx/module.json +14 -0
  9. package/modules/ai-dx-claude/files/CLAUDE.md +8 -0
  10. package/modules/ai-dx-claude/module.json +13 -0
  11. package/modules/ai-dx-cursor/files/.cursor/rules/auth.mdc +53 -0
  12. package/modules/ai-dx-cursor/files/.cursor/rules/database.mdc +48 -0
  13. package/modules/ai-dx-cursor/files/.cursor/rules/env.mdc +43 -0
  14. package/modules/ai-dx-cursor/files/.cursor/rules/nextjs.mdc +58 -0
  15. package/modules/ai-dx-cursor/files/.cursor/rules/project.mdc +33 -0
  16. package/modules/ai-dx-cursor/files/.cursor/rules/testing.mdc +55 -0
  17. package/modules/ai-dx-cursor/module.json +18 -0
  18. package/modules/ai-dx-gemini/files/.gemini/GEMINI.md +5 -0
  19. package/modules/ai-dx-gemini/module.json +13 -0
  20. package/modules/auth-core/module.json +8 -0
  21. package/modules/auth-github/module.json +20 -0
  22. package/modules/billing-polar/module.json +20 -0
  23. package/modules/billing-stripe/module.json +23 -0
  24. package/modules/dashboard-shell/files/src/app/globals.css +756 -0
  25. package/modules/dashboard-shell/files/src/app/settings/page.tsx +67 -0
  26. package/modules/dashboard-shell/module.json +11 -0
  27. package/modules/db-pg/module.json +21 -0
  28. package/modules/db-postgresjs/module.json +21 -0
  29. package/modules/deploy-docker/files/.dockerignore +19 -0
  30. package/modules/deploy-docker/files/Dockerfile +25 -0
  31. package/modules/deploy-docker/module.json +11 -0
  32. package/modules/email-resend/module.json +21 -0
  33. package/modules/quality-baseline/module.json +8 -0
  34. package/modules/testing-baseline/module.json +8 -0
  35. package/package.json +40 -0
  36. package/presets/README.md +12 -0
  37. package/presets/blank.json +67 -0
  38. package/presets/dashboard.json +67 -0
  39. package/templates/base-web/.env.example +17 -0
  40. package/templates/base-web/.github/workflows/ci.yml +34 -0
  41. package/templates/base-web/.husky/pre-commit +3 -0
  42. package/templates/base-web/.husky/pre-push +3 -0
  43. package/templates/base-web/.prettierignore +3 -0
  44. package/templates/base-web/README.md +42 -0
  45. package/templates/base-web/drizzle.config.ts +16 -0
  46. package/templates/base-web/eslint.config.mjs +127 -0
  47. package/templates/base-web/manifest.json +5 -0
  48. package/templates/base-web/next-env.d.ts +4 -0
  49. package/templates/base-web/next.config.ts +5 -0
  50. package/templates/base-web/package.json +75 -0
  51. package/templates/base-web/playwright.config.ts +21 -0
  52. package/templates/base-web/prettier.config.mjs +9 -0
  53. package/templates/base-web/proxy.ts +23 -0
  54. package/templates/base-web/src/app/api/auth/[...all]/route.ts +5 -0
  55. package/templates/base-web/src/app/api/billing/checkout/route.ts +26 -0
  56. package/templates/base-web/src/app/api/billing/portal/route.ts +25 -0
  57. package/templates/base-web/src/app/api/email/test/route.ts +28 -0
  58. package/templates/base-web/src/app/api/webhooks/polar/route.ts +5 -0
  59. package/templates/base-web/src/app/api/webhooks/stripe/route.ts +5 -0
  60. package/templates/base-web/src/app/billing/page.tsx +55 -0
  61. package/templates/base-web/src/app/dashboard/page.tsx +15 -0
  62. package/templates/base-web/src/app/email/page.tsx +46 -0
  63. package/templates/base-web/src/app/error.tsx +27 -0
  64. package/templates/base-web/src/app/globals.css +534 -0
  65. package/templates/base-web/src/app/layout.tsx +19 -0
  66. package/templates/base-web/src/app/llms-full.txt/route.ts +158 -0
  67. package/templates/base-web/src/app/llms.txt/route.ts +59 -0
  68. package/templates/base-web/src/app/loading.tsx +24 -0
  69. package/templates/base-web/src/app/not-found.tsx +16 -0
  70. package/templates/base-web/src/app/page.tsx +5 -0
  71. package/templates/base-web/src/app/sign-in/page.tsx +14 -0
  72. package/templates/base-web/src/app/sign-up/page.tsx +14 -0
  73. package/templates/base-web/src/components/auth/email-auth-form.test.tsx +40 -0
  74. package/templates/base-web/src/components/auth/email-auth-form.tsx +128 -0
  75. package/templates/base-web/src/components/auth/sign-out-button.tsx +29 -0
  76. package/templates/base-web/src/db/index.ts +16 -0
  77. package/templates/base-web/src/db/schema/auth.ts +4 -0
  78. package/templates/base-web/src/db/schema/index.ts +2 -0
  79. package/templates/base-web/src/db/schema/projects.ts +17 -0
  80. package/templates/base-web/src/db/seeds/index.ts +32 -0
  81. package/templates/base-web/src/lib/auth-client.ts +5 -0
  82. package/templates/base-web/src/lib/auth-session.ts +21 -0
  83. package/templates/base-web/src/lib/auth.ts +23 -0
  84. package/templates/base-web/src/lib/billing/index.ts +37 -0
  85. package/templates/base-web/src/lib/billing/providers/polar.ts +80 -0
  86. package/templates/base-web/src/lib/billing/providers/stripe.ts +77 -0
  87. package/templates/base-web/src/lib/billing/types.ts +25 -0
  88. package/templates/base-web/src/lib/email/index.ts +19 -0
  89. package/templates/base-web/src/lib/email/templates.test.ts +12 -0
  90. package/templates/base-web/src/lib/email/templates.ts +40 -0
  91. package/templates/base-web/src/lib/env.ts +83 -0
  92. package/templates/base-web/tests/e2e/home.spec.ts +8 -0
  93. package/templates/base-web/tsconfig.json +34 -0
  94. package/templates/base-web/vitest.config.ts +19 -0
  95. package/templates/blank/.env.example +16 -0
  96. package/templates/blank/.github/workflows/ci.yml +34 -0
  97. package/templates/blank/.husky/pre-commit +3 -0
  98. package/templates/blank/.husky/pre-push +3 -0
  99. package/templates/blank/.prettierignore +3 -0
  100. package/templates/blank/drizzle.config.ts +16 -0
  101. package/templates/blank/eslint.config.mjs +127 -0
  102. package/templates/blank/next-env.d.ts +4 -0
  103. package/templates/blank/next.config.ts +5 -0
  104. package/templates/blank/package.json +75 -0
  105. package/templates/blank/playwright.config.ts +21 -0
  106. package/templates/blank/prettier.config.mjs +9 -0
  107. package/templates/blank/proxy.ts +28 -0
  108. package/templates/blank/src/app/api/auth/[...all]/route.ts +5 -0
  109. package/templates/blank/src/app/api/billing/checkout/route.ts +26 -0
  110. package/templates/blank/src/app/api/billing/portal/route.ts +25 -0
  111. package/templates/blank/src/app/api/email/test/route.ts +28 -0
  112. package/templates/blank/src/app/api/webhooks/polar/route.ts +5 -0
  113. package/templates/blank/src/app/api/webhooks/stripe/route.ts +5 -0
  114. package/templates/blank/src/app/billing/page.tsx +70 -0
  115. package/templates/blank/src/app/email/page.tsx +46 -0
  116. package/templates/blank/src/app/globals.css +394 -0
  117. package/templates/blank/src/app/layout.tsx +19 -0
  118. package/templates/blank/src/app/page.tsx +23 -0
  119. package/templates/blank/src/app/sign-in/page.tsx +18 -0
  120. package/templates/blank/src/app/sign-up/page.tsx +18 -0
  121. package/templates/blank/src/components/auth/email-auth-form.test.tsx +39 -0
  122. package/templates/blank/src/components/auth/email-auth-form.tsx +109 -0
  123. package/templates/blank/src/components/auth/sign-out-button.tsx +29 -0
  124. package/templates/blank/src/db/index.ts +16 -0
  125. package/templates/blank/src/db/schema/auth.ts +4 -0
  126. package/templates/blank/src/db/schema/index.ts +2 -0
  127. package/templates/blank/src/db/schema/projects.ts +17 -0
  128. package/templates/blank/src/db/seeds/index.ts +28 -0
  129. package/templates/blank/src/lib/auth-client.ts +5 -0
  130. package/templates/blank/src/lib/auth-session.ts +11 -0
  131. package/templates/blank/src/lib/auth.ts +23 -0
  132. package/templates/blank/src/lib/billing/index.ts +37 -0
  133. package/templates/blank/src/lib/billing/providers/polar.ts +80 -0
  134. package/templates/blank/src/lib/billing/providers/stripe.ts +77 -0
  135. package/templates/blank/src/lib/billing/types.ts +25 -0
  136. package/templates/blank/src/lib/email/index.ts +19 -0
  137. package/templates/blank/src/lib/email/templates.test.ts +15 -0
  138. package/templates/blank/src/lib/email/templates.ts +40 -0
  139. package/templates/blank/src/lib/env.ts +80 -0
  140. package/templates/blank/tsconfig.json +34 -0
  141. package/templates/blank/vitest.config.ts +19 -0
  142. package/templates/dashboard/.env.example +16 -0
  143. package/templates/dashboard/.github/workflows/ci.yml +34 -0
  144. package/templates/dashboard/.husky/pre-commit +3 -0
  145. package/templates/dashboard/.husky/pre-push +3 -0
  146. package/templates/dashboard/.prettierignore +3 -0
  147. package/templates/dashboard/drizzle.config.ts +16 -0
  148. package/templates/dashboard/eslint.config.mjs +127 -0
  149. package/templates/dashboard/next-env.d.ts +4 -0
  150. package/templates/dashboard/next.config.ts +5 -0
  151. package/templates/dashboard/package.json +75 -0
  152. package/templates/dashboard/playwright.config.ts +21 -0
  153. package/templates/dashboard/prettier.config.mjs +9 -0
  154. package/templates/dashboard/proxy.ts +36 -0
  155. package/templates/dashboard/src/app/api/auth/[...all]/route.ts +5 -0
  156. package/templates/dashboard/src/app/api/billing/checkout/route.ts +26 -0
  157. package/templates/dashboard/src/app/api/billing/portal/route.ts +25 -0
  158. package/templates/dashboard/src/app/api/email/test/route.ts +28 -0
  159. package/templates/dashboard/src/app/api/webhooks/polar/route.ts +5 -0
  160. package/templates/dashboard/src/app/api/webhooks/stripe/route.ts +5 -0
  161. package/templates/dashboard/src/app/billing/layout.tsx +22 -0
  162. package/templates/dashboard/src/app/billing/page.tsx +73 -0
  163. package/templates/dashboard/src/app/dashboard/layout.tsx +22 -0
  164. package/templates/dashboard/src/app/dashboard/page.tsx +104 -0
  165. package/templates/dashboard/src/app/email/layout.tsx +22 -0
  166. package/templates/dashboard/src/app/email/page.tsx +54 -0
  167. package/templates/dashboard/src/app/globals.css +1357 -0
  168. package/templates/dashboard/src/app/layout.tsx +25 -0
  169. package/templates/dashboard/src/app/page.tsx +154 -0
  170. package/templates/dashboard/src/app/settings/layout.tsx +22 -0
  171. package/templates/dashboard/src/app/settings/page.tsx +85 -0
  172. package/templates/dashboard/src/app/sign-in/page.tsx +47 -0
  173. package/templates/dashboard/src/app/sign-up/page.tsx +47 -0
  174. package/templates/dashboard/src/components/auth/email-auth-form.test.tsx +39 -0
  175. package/templates/dashboard/src/components/auth/email-auth-form.tsx +160 -0
  176. package/templates/dashboard/src/components/auth/sign-out-button.tsx +29 -0
  177. package/templates/dashboard/src/components/dashboard/shell.tsx +158 -0
  178. package/templates/dashboard/src/db/index.ts +16 -0
  179. package/templates/dashboard/src/db/schema/auth.ts +4 -0
  180. package/templates/dashboard/src/db/schema/index.ts +2 -0
  181. package/templates/dashboard/src/db/schema/projects.ts +17 -0
  182. package/templates/dashboard/src/db/seeds/index.ts +28 -0
  183. package/templates/dashboard/src/lib/auth-client.ts +5 -0
  184. package/templates/dashboard/src/lib/auth-session.ts +11 -0
  185. package/templates/dashboard/src/lib/auth.ts +41 -0
  186. package/templates/dashboard/src/lib/billing/index.ts +37 -0
  187. package/templates/dashboard/src/lib/billing/providers/polar.ts +80 -0
  188. package/templates/dashboard/src/lib/billing/providers/stripe.ts +77 -0
  189. package/templates/dashboard/src/lib/billing/types.ts +25 -0
  190. package/templates/dashboard/src/lib/email/index.ts +19 -0
  191. package/templates/dashboard/src/lib/email/templates.test.ts +15 -0
  192. package/templates/dashboard/src/lib/email/templates.ts +40 -0
  193. package/templates/dashboard/src/lib/env.ts +88 -0
  194. package/templates/dashboard/tsconfig.json +34 -0
  195. package/templates/dashboard/vitest.config.ts +19 -0
@@ -0,0 +1,1064 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "node:fs";
4
+ import { access, cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { emitKeypressEvents } from "node:readline";
8
+ import readline from "node:readline/promises";
9
+ import { stdin as input, stdout as output } from "node:process";
10
+
11
+ import {
12
+ buildModuleFileOverlays,
13
+ buildModulePruneList,
14
+ buildModuleTokenReplacements
15
+ } from "../lib/module-application.mjs";
16
+ import { resolveScaffoldPlan } from "../lib/module-resolver.mjs";
17
+
18
+ const TEMPLATE_OPTIONS = [
19
+ {
20
+ value: "blank",
21
+ label: "Blank",
22
+ description: "Minimal SaaS shell with auth, billing, email, and protected routes."
23
+ },
24
+ {
25
+ value: "dashboard",
26
+ label: "Dashboard",
27
+ description: "Starter dashboard with app shell, overview UI, settings, and protected flows."
28
+ }
29
+ ];
30
+ const ARCHITECTURE_OPTIONS = [
31
+ {
32
+ value: "route-colocated",
33
+ label: "Route Colocated",
34
+ description: "Default App Router structure with route-local pieces near the route."
35
+ },
36
+ {
37
+ value: "feature-first",
38
+ label: "Feature First",
39
+ description: "Domain-oriented structure for larger products."
40
+ },
41
+ {
42
+ value: "monorepo-ready",
43
+ label: "Monorepo Ready",
44
+ description: "Future-friendly layout for shared packages and multiple apps."
45
+ }
46
+ ];
47
+ const PACKAGE_MANAGER_OPTIONS = [
48
+ {
49
+ value: "pnpm",
50
+ label: "pnpm",
51
+ description: "Recommended default. Fast and monorepo-friendly."
52
+ },
53
+ {
54
+ value: "npm",
55
+ label: "npm",
56
+ description: "Mainstream fallback with the broadest default availability."
57
+ },
58
+ {
59
+ value: "bun",
60
+ label: "bun",
61
+ description: "Experimental option. Fast, but ecosystem parity still needs validation."
62
+ }
63
+ ];
64
+ const DATABASE_DRIVER_OPTIONS = [
65
+ {
66
+ value: "pg",
67
+ label: "pg",
68
+ description: "Recommended default PostgreSQL driver."
69
+ },
70
+ {
71
+ value: "postgres.js",
72
+ label: "postgres.js",
73
+ description: "Modern alternative driver, planned as follow-up support."
74
+ }
75
+ ];
76
+ const BILLING_OPTIONS = [
77
+ {
78
+ value: "stripe",
79
+ label: "Stripe",
80
+ description: "Recommended default billing provider."
81
+ },
82
+ {
83
+ value: "polar",
84
+ label: "Polar",
85
+ description: "Alternative billing provider."
86
+ },
87
+ {
88
+ value: "both",
89
+ label: "Both",
90
+ description: "Keep both provider entrypoints available."
91
+ },
92
+ {
93
+ value: "none",
94
+ label: "None",
95
+ description: "Generate the app without a billing provider selection."
96
+ }
97
+ ];
98
+ const AUTH_OPTIONS = [
99
+ {
100
+ value: "email-password",
101
+ label: "Email + Password",
102
+ description: "Recommended default auth path."
103
+ },
104
+ {
105
+ value: "email-password+github",
106
+ label: "Email + Password + GitHub",
107
+ description: "Add GitHub OAuth alongside email/password."
108
+ },
109
+ {
110
+ value: "email-password+google",
111
+ label: "Email + Password + Google",
112
+ description: "Add Google OAuth alongside email/password."
113
+ },
114
+ {
115
+ value: "email-password+github+google",
116
+ label: "Email + Password + GitHub + Google",
117
+ description: "Both GitHub and Google OAuth alongside email/password."
118
+ }
119
+ ];
120
+ const EMAIL_PROVIDER_OPTIONS = [
121
+ {
122
+ value: "resend",
123
+ label: "Resend",
124
+ description: "Recommended transactional email provider."
125
+ },
126
+ {
127
+ value: "none",
128
+ label: "None",
129
+ description: "Keep the email layer scaffold but treat it as disabled."
130
+ }
131
+ ];
132
+ const DEPLOY_TARGET_OPTIONS = [
133
+ {
134
+ value: "vercel",
135
+ label: "Vercel",
136
+ description: "Recommended default for Next.js deployment."
137
+ },
138
+ {
139
+ value: "docker",
140
+ label: "Docker",
141
+ description: "Use the starter as a self-hosted baseline."
142
+ }
143
+ ];
144
+ const AI_TOOLS_OPTIONS = [
145
+ {
146
+ value: "cursor",
147
+ label: "Cursor",
148
+ description: ".cursor/rules/*.mdc — modular auto-activated rules by file pattern."
149
+ },
150
+ {
151
+ value: "claude",
152
+ label: "Claude Code",
153
+ description: "CLAUDE.md — Claude-specific workflow and conventions."
154
+ },
155
+ {
156
+ value: "gemini",
157
+ label: "Gemini",
158
+ description: ".gemini/GEMINI.md — Gemini Code Assist pointer to AGENTS.md."
159
+ },
160
+ {
161
+ value: "copilot",
162
+ label: "Copilot / Codex",
163
+ description: "Uses shared AGENTS.md (no extra files needed)."
164
+ }
165
+ ];
166
+ const SEED_OPTIONS = [
167
+ {
168
+ value: "yes",
169
+ label: "Yes",
170
+ description: "Keep demo seed data enabled."
171
+ },
172
+ {
173
+ value: "no",
174
+ label: "No",
175
+ description: "Generate without expecting demo seed data usage."
176
+ }
177
+ ];
178
+ const TEXT_FILE_EXTENSIONS = new Set([
179
+ ".cjs",
180
+ ".css",
181
+ ".example",
182
+ ".html",
183
+ ".js",
184
+ ".json",
185
+ ".jsx",
186
+ ".md",
187
+ ".mjs",
188
+ ".svg",
189
+ ".ts",
190
+ ".tsx",
191
+ ".txt",
192
+ ".yaml",
193
+ ".yml"
194
+ ]);
195
+ const TEXT_FILE_NAMES = new Set(["Dockerfile", ".dockerignore"]);
196
+
197
+ async function main() {
198
+ const args = process.argv.slice(2);
199
+ const cliOptions = parseArgs(args);
200
+ const prompts = readline.createInterface({ input, output });
201
+
202
+ try {
203
+ const rawProjectName =
204
+ cliOptions.projectName ?? (await prompt(prompts, "Project name", "my-panda-app"));
205
+ const projectName = normalizeProjectName(rawProjectName);
206
+ const template =
207
+ cliOptions.template ??
208
+ (await promptSelect(prompts, "Template", TEMPLATE_OPTIONS, "blank"));
209
+ const architecture =
210
+ cliOptions.architecture ??
211
+ (await promptSelect(
212
+ prompts,
213
+ "Architecture",
214
+ ARCHITECTURE_OPTIONS,
215
+ "route-colocated"
216
+ ));
217
+ const packageManager =
218
+ cliOptions.packageManager ??
219
+ (await promptSelect(
220
+ prompts,
221
+ "Package manager",
222
+ PACKAGE_MANAGER_OPTIONS,
223
+ "pnpm"
224
+ ));
225
+ const databaseDriver =
226
+ cliOptions.databaseDriver ??
227
+ (await promptSelect(
228
+ prompts,
229
+ "Postgres driver",
230
+ DATABASE_DRIVER_OPTIONS,
231
+ "pg"
232
+ ));
233
+ const billingProvider =
234
+ cliOptions.billingProvider ??
235
+ (await promptSelect(prompts, "Billing", BILLING_OPTIONS, "stripe"));
236
+ const authMode =
237
+ cliOptions.authMode ??
238
+ (await promptSelect(prompts, "Auth", AUTH_OPTIONS, "email-password"));
239
+ const emailProvider =
240
+ cliOptions.emailProvider ??
241
+ (await promptSelect(prompts, "Email provider", EMAIL_PROVIDER_OPTIONS, "resend"));
242
+ const deployTarget =
243
+ cliOptions.deployTarget ??
244
+ (await promptSelect(prompts, "Deploy target", DEPLOY_TARGET_OPTIONS, "vercel"));
245
+ const seedDemoData =
246
+ cliOptions.seedDemoData ??
247
+ (await promptSelect(prompts, "Seed demo data", SEED_OPTIONS, "yes"));
248
+ const aiTools =
249
+ cliOptions.aiTools ??
250
+ (await promptMultiSelect(
251
+ prompts,
252
+ "AI tools (AGENTS.md + ARCHITECTURE.md always included)",
253
+ AI_TOOLS_OPTIONS,
254
+ AI_TOOLS_OPTIONS.map((o) => o.value)
255
+ ));
256
+
257
+ const destination = path.resolve(process.cwd(), projectName);
258
+ await ensureTargetDoesNotExist(destination);
259
+ const isNestedInsidePnpmWorkspace =
260
+ packageManager === "pnpm" && (await hasParentWorkspace(destination));
261
+
262
+ if (rawProjectName !== projectName) {
263
+ output.write(`\nUsing directory name: ${projectName}\n`);
264
+ }
265
+
266
+ output.write(`\nScaffolding ${projectName} from the ${template} template...\n`);
267
+
268
+ const repoRoot = getRepoRoot();
269
+ const scaffoldPlan = await resolveScaffoldPlan(repoRoot, {
270
+ template,
271
+ databaseDriver,
272
+ authMode,
273
+ billingProvider,
274
+ emailProvider,
275
+ deployTarget,
276
+ aiTools
277
+ });
278
+ const baseTemplateDir = path.resolve(repoRoot, "templates", scaffoldPlan.base);
279
+ const presetTemplateDir = path.resolve(repoRoot, "templates", scaffoldPlan.template);
280
+ const installCommand = getInstallCommand(packageManager, isNestedInsidePnpmWorkspace);
281
+ const runPrefix = getRunPrefix(packageManager);
282
+ const moduleTokenReplacements = await buildModuleTokenReplacements(repoRoot, scaffoldPlan);
283
+ const moduleFileOverlays = await buildModuleFileOverlays(repoRoot, scaffoldPlan);
284
+ await assembleTemplate(baseTemplateDir, presetTemplateDir, destination, scaffoldPlan.overrideFiles);
285
+ await applyModuleFileOverlays(destination, moduleFileOverlays);
286
+ await replaceTokensRecursively(destination, {
287
+ __PROJECT_NAME__: projectName,
288
+ __PACKAGE_MANAGER__: getPackageManagerSpec(packageManager),
289
+ __PACKAGE_MANAGER_NAME__: packageManager,
290
+ __INSTALL_COMMAND__: installCommand,
291
+ __RUN_DEV_COMMAND__: getDevCommand(packageManager),
292
+ __CHECK_COMMAND__: getCheckCommand(packageManager),
293
+ __CI_CACHE__: getCiCache(packageManager),
294
+ __CI_INSTALL_COMMAND__: getCiInstallCommand(packageManager),
295
+ __CI_RUN_LINT__: `${runPrefix} lint`,
296
+ __CI_RUN_TYPECHECK__: `${runPrefix} typecheck`,
297
+ __CI_RUN_TEST__: `${runPrefix} test`,
298
+ __CI_RUN_BUILD__: `${runPrefix} build`,
299
+ __DEPLOY_TARGET__: deployTarget,
300
+ __DOCKER_BASE_IMAGE__: getDockerBaseImage(packageManager),
301
+ __DOCKER_SETUP__: getDockerSetup(packageManager),
302
+ __DOCKER_INSTALL_COMMAND__: getDockerInstallCommand(packageManager),
303
+ __DOCKER_BUILD_COMMAND__: getDockerBuildCommand(packageManager),
304
+ __DOCKER_START_COMMAND__: getDockerStartCommand(packageManager),
305
+ __SEED_DEMO_DATA__: seedDemoData,
306
+ ...scaffoldPlan.tokenReplacements,
307
+ ...moduleTokenReplacements
308
+ });
309
+ await pruneGeneratedFiles(destination, buildModulePruneList(scaffoldPlan));
310
+ await writeStarterConfig(destination, {
311
+ base: scaffoldPlan.base,
312
+ preset: scaffoldPlan.preset,
313
+ modules: scaffoldPlan.modules,
314
+ template,
315
+ architecture,
316
+ packageManager,
317
+ databaseDriver,
318
+ billingProvider,
319
+ authMode,
320
+ emailProvider,
321
+ deployTarget,
322
+ seedDemoData,
323
+ aiTools
324
+ });
325
+
326
+ output.write("\nDone.\n");
327
+ output.write(`\nNext steps:\n`);
328
+ output.write(` cd ${projectName}\n`);
329
+ output.write(` ${installCommand}\n`);
330
+ output.write(` ${getDevCommand(packageManager)}\n\n`);
331
+ if (isNestedInsidePnpmWorkspace) {
332
+ output.write(
333
+ "Note: this project was created inside another pnpm workspace, so install uses --ignore-workspace.\n\n"
334
+ );
335
+ }
336
+ } catch (error) {
337
+ output.write(`\nError: ${formatError(error)}\n`);
338
+ process.exitCode = 1;
339
+ } finally {
340
+ prompts.close();
341
+ }
342
+ }
343
+
344
+ function parseArgs(args) {
345
+ const options = {};
346
+
347
+ for (let index = 0; index < args.length; index += 1) {
348
+ const arg = args[index];
349
+
350
+ if (!arg.startsWith("--") && !options.projectName) {
351
+ options.projectName = arg;
352
+ continue;
353
+ }
354
+
355
+ if (arg === "--template") {
356
+ const nextValue = args[index + 1];
357
+ if (nextValue && TEMPLATE_OPTIONS.some((option) => option.value === nextValue)) {
358
+ options.template = nextValue;
359
+ index += 1;
360
+ continue;
361
+ }
362
+
363
+ throw new Error(
364
+ `Invalid template. Expected one of: ${TEMPLATE_OPTIONS.map((option) => option.value).join(", ")}`
365
+ );
366
+ }
367
+
368
+ if (arg === "--package-manager") {
369
+ const nextValue = args[index + 1];
370
+ if (nextValue && PACKAGE_MANAGER_OPTIONS.some((option) => option.value === nextValue)) {
371
+ options.packageManager = nextValue;
372
+ index += 1;
373
+ continue;
374
+ }
375
+
376
+ throw new Error(
377
+ `Invalid package manager. Expected one of: ${PACKAGE_MANAGER_OPTIONS.map((option) => option.value).join(", ")}`
378
+ );
379
+ }
380
+
381
+ if (arg === "--architecture") {
382
+ const nextValue = args[index + 1];
383
+ if (nextValue && ARCHITECTURE_OPTIONS.some((option) => option.value === nextValue)) {
384
+ options.architecture = nextValue;
385
+ index += 1;
386
+ continue;
387
+ }
388
+
389
+ throw new Error(
390
+ `Invalid architecture. Expected one of: ${ARCHITECTURE_OPTIONS.map((option) => option.value).join(", ")}`
391
+ );
392
+ }
393
+
394
+ if (arg === "--database-driver") {
395
+ const nextValue = args[index + 1];
396
+ if (
397
+ nextValue &&
398
+ DATABASE_DRIVER_OPTIONS.some((option) => option.value === nextValue)
399
+ ) {
400
+ options.databaseDriver = nextValue;
401
+ index += 1;
402
+ continue;
403
+ }
404
+
405
+ throw new Error(
406
+ `Invalid database driver. Expected one of: ${DATABASE_DRIVER_OPTIONS.map((option) => option.value).join(", ")}`
407
+ );
408
+ }
409
+
410
+ if (arg === "--billing") {
411
+ const nextValue = args[index + 1];
412
+ if (nextValue && BILLING_OPTIONS.some((option) => option.value === nextValue)) {
413
+ options.billingProvider = nextValue;
414
+ index += 1;
415
+ continue;
416
+ }
417
+
418
+ throw new Error(
419
+ `Invalid billing provider. Expected one of: ${BILLING_OPTIONS.map((option) => option.value).join(", ")}`
420
+ );
421
+ }
422
+
423
+ if (arg === "--auth") {
424
+ const nextValue = args[index + 1];
425
+ if (nextValue && AUTH_OPTIONS.some((option) => option.value === nextValue)) {
426
+ options.authMode = nextValue;
427
+ index += 1;
428
+ continue;
429
+ }
430
+
431
+ throw new Error(
432
+ `Invalid auth mode. Expected one of: ${AUTH_OPTIONS.map((option) => option.value).join(", ")}`
433
+ );
434
+ }
435
+
436
+ if (arg === "--email-provider") {
437
+ const nextValue = args[index + 1];
438
+ if (nextValue && EMAIL_PROVIDER_OPTIONS.some((option) => option.value === nextValue)) {
439
+ options.emailProvider = nextValue;
440
+ index += 1;
441
+ continue;
442
+ }
443
+
444
+ throw new Error(
445
+ `Invalid email provider. Expected one of: ${EMAIL_PROVIDER_OPTIONS.map((option) => option.value).join(", ")}`
446
+ );
447
+ }
448
+
449
+ if (arg === "--deploy-target") {
450
+ const nextValue = args[index + 1];
451
+ if (nextValue && DEPLOY_TARGET_OPTIONS.some((option) => option.value === nextValue)) {
452
+ options.deployTarget = nextValue;
453
+ index += 1;
454
+ continue;
455
+ }
456
+
457
+ throw new Error(
458
+ `Invalid deploy target. Expected one of: ${DEPLOY_TARGET_OPTIONS.map((option) => option.value).join(", ")}`
459
+ );
460
+ }
461
+
462
+ if (arg === "--seed-demo-data") {
463
+ const nextValue = args[index + 1];
464
+ if (nextValue && SEED_OPTIONS.some((option) => option.value === nextValue)) {
465
+ options.seedDemoData = nextValue;
466
+ index += 1;
467
+ continue;
468
+ }
469
+
470
+ throw new Error(
471
+ `Invalid seed option. Expected one of: ${SEED_OPTIONS.map((option) => option.value).join(", ")}`
472
+ );
473
+ }
474
+
475
+ if (arg === "--ai-tools") {
476
+ const nextValue = args[index + 1];
477
+ if (!nextValue) {
478
+ throw new Error(`--ai-tools requires a value: all, none, or comma-separated list (cursor,claude,gemini,copilot).`);
479
+ }
480
+
481
+ index += 1;
482
+
483
+ if (nextValue === "all") {
484
+ options.aiTools = AI_TOOLS_OPTIONS.map((o) => o.value);
485
+ continue;
486
+ }
487
+
488
+ if (nextValue === "none") {
489
+ options.aiTools = [];
490
+ continue;
491
+ }
492
+
493
+ const values = nextValue.split(",").map((s) => s.trim());
494
+ const valid = AI_TOOLS_OPTIONS.map((o) => o.value);
495
+
496
+ for (const v of values) {
497
+ if (!valid.includes(v)) {
498
+ throw new Error(`Invalid AI tool: ${v}. Expected: ${valid.join(", ")}, all, or none.`);
499
+ }
500
+ }
501
+
502
+ options.aiTools = values;
503
+ continue;
504
+ }
505
+
506
+ throw new Error(`Unknown argument: ${arg}`);
507
+ }
508
+
509
+ return options;
510
+ }
511
+
512
+ async function prompt(prompts, label, fallback) {
513
+ const answer = await prompts.question(`${label} (${fallback}): `);
514
+ return answer.trim() || fallback;
515
+ }
516
+
517
+ async function promptSelect(prompts, label, options, fallback) {
518
+ if (input.isTTY && output.isTTY) {
519
+ return promptSelectWithArrows(label, options, fallback);
520
+ }
521
+
522
+ return promptSelectWithTextInput(prompts, label, options, fallback);
523
+ }
524
+
525
+ async function promptSelectWithTextInput(prompts, label, options, fallback) {
526
+ output.write(`\n${label}\n`);
527
+
528
+ for (const [index, option] of options.entries()) {
529
+ output.write(` ${index + 1}. ${option.label} (${option.value})\n`);
530
+ output.write(` ${option.description}\n`);
531
+ }
532
+
533
+ const fallbackIndex = options.findIndex((option) => option.value === fallback) + 1;
534
+ const answer = await prompts.question(`Choose ${label.toLowerCase()} (${fallbackIndex}): `);
535
+ const normalized = answer.trim();
536
+
537
+ if (!normalized) {
538
+ return fallback;
539
+ }
540
+
541
+ const numericIndex = Number(normalized);
542
+ if (!Number.isNaN(numericIndex)) {
543
+ const selected = options[numericIndex - 1];
544
+ if (!selected) {
545
+ throw new Error(`Invalid ${label.toLowerCase()} selection.`);
546
+ }
547
+
548
+ return selected.value;
549
+ }
550
+
551
+ const directMatch = options.find((option) => option.value === normalized);
552
+ if (directMatch) {
553
+ return directMatch.value;
554
+ }
555
+
556
+ throw new Error(`Invalid ${label.toLowerCase()} selection.`);
557
+ }
558
+
559
+ async function promptSelectWithArrows(label, options, fallback) {
560
+ const fallbackIndex = Math.max(
561
+ 0,
562
+ options.findIndex((option) => option.value === fallback)
563
+ );
564
+ let selectedIndex = fallbackIndex;
565
+
566
+ emitKeypressEvents(input);
567
+ input.setRawMode?.(true);
568
+
569
+ const cleanup = () => {
570
+ input.setRawMode?.(false);
571
+ input.pause();
572
+ input.removeListener("keypress", onKeypress);
573
+ };
574
+
575
+ const render = () => {
576
+ output.write(`\x1b[2J\x1b[0f`);
577
+ output.write(`${label}\n`);
578
+ output.write("Use ↑/↓ to move, Enter to select.\n\n");
579
+
580
+ for (const [index, option] of options.entries()) {
581
+ const isSelected = index === selectedIndex;
582
+ const marker = isSelected ? "❯" : " ";
583
+ const line = `${marker} ${option.label} (${option.value})`;
584
+ output.write(`${isSelected ? "\x1b[36m" : ""}${line}\x1b[0m\n`);
585
+ output.write(` ${option.description}\n`);
586
+ }
587
+ };
588
+
589
+ const selection = await new Promise((resolve, reject) => {
590
+ onKeypress = (_str, key) => {
591
+ if (key?.name === "up") {
592
+ selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1;
593
+ render();
594
+ return;
595
+ }
596
+
597
+ if (key?.name === "down") {
598
+ selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1;
599
+ render();
600
+ return;
601
+ }
602
+
603
+ if (key?.name === "return") {
604
+ output.write("\n");
605
+ resolve(options[selectedIndex].value);
606
+ return;
607
+ }
608
+
609
+ if (key?.ctrl && key.name === "c") {
610
+ reject(new Error("Prompt canceled."));
611
+ }
612
+ };
613
+
614
+ input.on("keypress", onKeypress);
615
+ input.resume();
616
+ render();
617
+ });
618
+
619
+ cleanup();
620
+ return selection;
621
+ }
622
+
623
+ let onKeypress = null;
624
+
625
+ async function promptMultiSelect(prompts, label, options, fallbackValues) {
626
+ if (input.isTTY && output.isTTY) {
627
+ return promptMultiSelectWithArrows(label, options, fallbackValues);
628
+ }
629
+
630
+ return promptMultiSelectWithTextInput(prompts, label, options, fallbackValues);
631
+ }
632
+
633
+ async function promptMultiSelectWithTextInput(prompts, label, options, fallbackValues) {
634
+ output.write(`\n${label}\n`);
635
+
636
+ for (const [index, option] of options.entries()) {
637
+ const checked = fallbackValues.includes(option.value) ? "[x]" : "[ ]";
638
+ output.write(` ${index + 1}. ${checked} ${option.label} (${option.value})\n`);
639
+ output.write(` ${option.description}\n`);
640
+ }
641
+
642
+ const fallbackDisplay = fallbackValues.join(",");
643
+ const answer = await prompts.question(
644
+ `Toggle by number (comma-separated), or "all"/"none" (${fallbackDisplay}): `
645
+ );
646
+ const normalized = answer.trim();
647
+
648
+ if (!normalized) {
649
+ return fallbackValues;
650
+ }
651
+
652
+ if (normalized === "all") {
653
+ return options.map((o) => o.value);
654
+ }
655
+
656
+ if (normalized === "none") {
657
+ return [];
658
+ }
659
+
660
+ const indices = normalized.split(",").map((s) => Number(s.trim()));
661
+ const selected = [];
662
+
663
+ for (const idx of indices) {
664
+ const option = options[idx - 1];
665
+ if (!option) {
666
+ throw new Error(`Invalid selection: ${idx}`);
667
+ }
668
+ selected.push(option.value);
669
+ }
670
+
671
+ return selected;
672
+ }
673
+
674
+ async function promptMultiSelectWithArrows(label, options, fallbackValues) {
675
+ const checked = new Set(fallbackValues);
676
+ let cursorIndex = 0;
677
+
678
+ emitKeypressEvents(input);
679
+ input.setRawMode?.(true);
680
+
681
+ const cleanup = () => {
682
+ input.setRawMode?.(false);
683
+ input.pause();
684
+ input.removeListener("keypress", onKeypress);
685
+ };
686
+
687
+ const render = () => {
688
+ output.write(`\x1b[2J\x1b[0f`);
689
+ output.write(`${label}\n`);
690
+ output.write("Use ↑/↓ to move, Space to toggle, Enter to confirm.\n\n");
691
+
692
+ for (const [index, option] of options.entries()) {
693
+ const isCursor = index === cursorIndex;
694
+ const box = checked.has(option.value) ? "[x]" : "[ ]";
695
+ const marker = isCursor ? "❯" : " ";
696
+ const line = `${marker} ${box} ${option.label}`;
697
+ output.write(`${isCursor ? "\x1b[36m" : ""}${line}\x1b[0m\n`);
698
+ output.write(` ${option.description}\n`);
699
+ }
700
+
701
+ output.write(`\n a = all, n = none\n`);
702
+ };
703
+
704
+ const selection = await new Promise((resolve, reject) => {
705
+ onKeypress = (_str, key) => {
706
+ if (key?.name === "up") {
707
+ cursorIndex = cursorIndex === 0 ? options.length - 1 : cursorIndex - 1;
708
+ render();
709
+ return;
710
+ }
711
+
712
+ if (key?.name === "down") {
713
+ cursorIndex = cursorIndex === options.length - 1 ? 0 : cursorIndex + 1;
714
+ render();
715
+ return;
716
+ }
717
+
718
+ if (key?.name === "space") {
719
+ const val = options[cursorIndex].value;
720
+ if (checked.has(val)) {
721
+ checked.delete(val);
722
+ } else {
723
+ checked.add(val);
724
+ }
725
+ render();
726
+ return;
727
+ }
728
+
729
+ if (_str === "a") {
730
+ for (const o of options) checked.add(o.value);
731
+ render();
732
+ return;
733
+ }
734
+
735
+ if (_str === "n") {
736
+ checked.clear();
737
+ render();
738
+ return;
739
+ }
740
+
741
+ if (key?.name === "return") {
742
+ output.write("\n");
743
+ resolve([...checked]);
744
+ return;
745
+ }
746
+
747
+ if (key?.ctrl && key.name === "c") {
748
+ reject(new Error("Prompt canceled."));
749
+ }
750
+ };
751
+
752
+ input.on("keypress", onKeypress);
753
+ input.resume();
754
+ render();
755
+ });
756
+
757
+ cleanup();
758
+ return selection;
759
+ }
760
+
761
+ function slugifyProjectSegment(value) {
762
+ const normalized = value
763
+ .trim()
764
+ .toLowerCase()
765
+ .replace(/[^a-z0-9]+/g, "-")
766
+ .replace(/^-+|-+$/g, "");
767
+
768
+ if (!normalized) {
769
+ throw new Error("Project name must contain at least one letter or number.");
770
+ }
771
+
772
+ return normalized;
773
+ }
774
+
775
+ function normalizeProjectName(value) {
776
+ const trimmed = value.trim();
777
+
778
+ if (!trimmed) {
779
+ throw new Error("Project name must contain at least one letter or number.");
780
+ }
781
+
782
+ const normalizedPath = path.normalize(trimmed);
783
+ const dirname = path.dirname(normalizedPath);
784
+ const basename = path.basename(normalizedPath);
785
+ const normalizedBase = slugifyProjectSegment(basename);
786
+
787
+ if (dirname === "." || dirname === "") {
788
+ return normalizedBase;
789
+ }
790
+
791
+ return path.join(dirname, normalizedBase);
792
+ }
793
+
794
+ function getInstallCommand(packageManager, isNestedInsidePnpmWorkspace = false) {
795
+ if (packageManager === "npm") {
796
+ return "npm install";
797
+ }
798
+
799
+ if (packageManager === "bun") {
800
+ return "bun install";
801
+ }
802
+
803
+ if (isNestedInsidePnpmWorkspace) {
804
+ return "pnpm install --ignore-workspace";
805
+ }
806
+
807
+ return "pnpm install";
808
+ }
809
+
810
+ function getDevCommand(packageManager) {
811
+ if (packageManager === "npm") {
812
+ return "npm run dev";
813
+ }
814
+
815
+ if (packageManager === "bun") {
816
+ return "bun run dev";
817
+ }
818
+
819
+ return "pnpm dev";
820
+ }
821
+
822
+ function getRunPrefix(packageManager) {
823
+ if (packageManager === "npm") {
824
+ return "npm run";
825
+ }
826
+
827
+ if (packageManager === "bun") {
828
+ return "bun run";
829
+ }
830
+
831
+ return "pnpm";
832
+ }
833
+
834
+ function getCheckCommand(packageManager) {
835
+ const runPrefix = getRunPrefix(packageManager);
836
+ return `${runPrefix} lint && ${runPrefix} typecheck && ${runPrefix} format:check`;
837
+ }
838
+
839
+ function getDockerBaseImage(packageManager) {
840
+ if (packageManager === "bun") {
841
+ return "oven/bun:1.2.22-alpine";
842
+ }
843
+
844
+ return "node:20-alpine";
845
+ }
846
+
847
+ function getDockerSetup(packageManager) {
848
+ if (packageManager === "pnpm") {
849
+ return "RUN corepack enable";
850
+ }
851
+
852
+ return "";
853
+ }
854
+
855
+ function getDockerInstallCommand(packageManager) {
856
+ if (packageManager === "pnpm") {
857
+ return "pnpm install --no-frozen-lockfile";
858
+ }
859
+
860
+ if (packageManager === "bun") {
861
+ return "bun install";
862
+ }
863
+
864
+ return "npm install";
865
+ }
866
+
867
+ function getDockerBuildCommand(packageManager) {
868
+ if (packageManager === "pnpm") {
869
+ return "pnpm build";
870
+ }
871
+
872
+ if (packageManager === "bun") {
873
+ return "bun run build";
874
+ }
875
+
876
+ return "npm run build";
877
+ }
878
+
879
+ function getDockerStartCommand(packageManager) {
880
+ if (packageManager === "pnpm") {
881
+ return "pnpm start";
882
+ }
883
+
884
+ if (packageManager === "bun") {
885
+ return "bun run start";
886
+ }
887
+
888
+ return "npm run start";
889
+ }
890
+
891
+ function getPackageManagerSpec(packageManager) {
892
+ if (packageManager === "npm") {
893
+ return "npm@11";
894
+ }
895
+
896
+ if (packageManager === "bun") {
897
+ return "bun@1";
898
+ }
899
+
900
+ return "pnpm@10.18.0";
901
+ }
902
+
903
+ function getCiCache(packageManager) {
904
+ if (packageManager === "pnpm") {
905
+ return "pnpm";
906
+ }
907
+
908
+ if (packageManager === "npm") {
909
+ return "npm";
910
+ }
911
+
912
+ return "npm";
913
+ }
914
+
915
+ function getCiInstallCommand(packageManager) {
916
+ if (packageManager === "npm") {
917
+ return "npm ci";
918
+ }
919
+
920
+ if (packageManager === "bun") {
921
+ return "bun install --frozen-lockfile";
922
+ }
923
+
924
+ return "pnpm install --frozen-lockfile";
925
+ }
926
+
927
+ async function writeStarterConfig(destination, selections) {
928
+ await writeFile(
929
+ path.join(destination, "skit.json"),
930
+ `${JSON.stringify(
931
+ {
932
+ starter: "skit",
933
+ version: 1,
934
+ ...selections
935
+ },
936
+ null,
937
+ 2
938
+ )}\n`
939
+ );
940
+ }
941
+
942
+ async function hasParentWorkspace(destination) {
943
+ let current = path.dirname(destination);
944
+
945
+ while (true) {
946
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
947
+
948
+ try {
949
+ await access(workspaceFile);
950
+ return true;
951
+ } catch (error) {
952
+ if (!error || error.code !== "ENOENT") {
953
+ throw error;
954
+ }
955
+ }
956
+
957
+ const parent = path.dirname(current);
958
+ if (parent === current) {
959
+ return false;
960
+ }
961
+
962
+ current = parent;
963
+ }
964
+ }
965
+
966
+ function getRepoRoot() {
967
+ const packageRoot = path.resolve(import.meta.dirname, "..");
968
+
969
+ if (hasScaffoldAssets(packageRoot)) {
970
+ return packageRoot;
971
+ }
972
+
973
+ return path.resolve(import.meta.dirname, "..", "..", "..");
974
+ }
975
+
976
+ function hasScaffoldAssets(root) {
977
+ return ["modules", "presets", "templates"].every((directory) =>
978
+ existsSync(path.join(root, directory))
979
+ );
980
+ }
981
+
982
+ async function ensureTargetDoesNotExist(destination) {
983
+ try {
984
+ await access(destination);
985
+ throw new Error(`Target directory already exists: ${destination}`);
986
+ } catch (error) {
987
+ if (error && error.code === "ENOENT") {
988
+ return;
989
+ }
990
+
991
+ throw error;
992
+ }
993
+ }
994
+
995
+ async function copyTemplate(sourceDir, destinationDir, replacements) {
996
+ await cp(sourceDir, destinationDir, { recursive: true });
997
+ await replaceTokensRecursively(destinationDir, replacements);
998
+ }
999
+
1000
+ async function assembleTemplate(baseDir, presetDir, destinationDir, overrideFiles) {
1001
+ await cp(baseDir, destinationDir, { recursive: true });
1002
+
1003
+ for (const relativeFilePath of overrideFiles) {
1004
+ const sourcePath = path.join(presetDir, relativeFilePath);
1005
+ const destinationPath = path.join(destinationDir, relativeFilePath);
1006
+
1007
+ await mkdir(path.dirname(destinationPath), { recursive: true });
1008
+ await cp(sourcePath, destinationPath);
1009
+ }
1010
+ }
1011
+
1012
+ async function applyModuleFileOverlays(destinationDir, fileOverlays) {
1013
+ for (const fileOverlay of fileOverlays) {
1014
+ const destinationPath = path.join(destinationDir, fileOverlay.destination);
1015
+ await mkdir(path.dirname(destinationPath), { recursive: true });
1016
+ await cp(fileOverlay.source, destinationPath);
1017
+ }
1018
+ }
1019
+
1020
+ async function replaceTokensRecursively(currentDir, replacements) {
1021
+ const entries = await readdir(currentDir, { withFileTypes: true });
1022
+
1023
+ for (const entry of entries) {
1024
+ const targetPath = path.join(currentDir, entry.name);
1025
+
1026
+ if (entry.isDirectory()) {
1027
+ await replaceTokensRecursively(targetPath, replacements);
1028
+ continue;
1029
+ }
1030
+
1031
+ if (!entry.isFile() || !shouldReplaceTokens(targetPath)) {
1032
+ continue;
1033
+ }
1034
+
1035
+ let content = await readFile(targetPath, "utf8");
1036
+
1037
+ for (const [token, value] of Object.entries(replacements)) {
1038
+ content = content.split(token).join(value);
1039
+ }
1040
+
1041
+ await writeFile(targetPath, content);
1042
+ }
1043
+ }
1044
+
1045
+ async function pruneGeneratedFiles(destinationDir, relativePaths) {
1046
+ for (const relativePath of relativePaths) {
1047
+ await rm(path.join(destinationDir, relativePath), {
1048
+ force: true
1049
+ });
1050
+ }
1051
+ }
1052
+
1053
+ function shouldReplaceTokens(filePath) {
1054
+ return (
1055
+ TEXT_FILE_EXTENSIONS.has(path.extname(filePath)) ||
1056
+ TEXT_FILE_NAMES.has(path.basename(filePath))
1057
+ );
1058
+ }
1059
+
1060
+ function formatError(error) {
1061
+ return error instanceof Error ? error.message : String(error);
1062
+ }
1063
+
1064
+ await main();