scale-stack 0.0.1-alpha.1 → 0.0.1

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 (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +97 -3
  3. package/dist/index.js +4218 -16
  4. package/package.json +22 -10
  5. package/templates/ai-chat/chat-panel.tsx.ejs +70 -0
  6. package/templates/ai-chat/layout.tsx.ejs +39 -0
  7. package/templates/ai-chat/page.tsx.ejs +31 -0
  8. package/templates/ai-chat/route.ts.ejs +68 -0
  9. package/templates/ai-chat/use-chat.ts.ejs +26 -0
  10. package/templates/analytics/analytics-provider.tsx.ejs +32 -0
  11. package/templates/analytics/analytics.ts.ejs +40 -0
  12. package/templates/auth/auth-client.ts.ejs +7 -0
  13. package/templates/auth/auth.ts.ejs +45 -0
  14. package/templates/auth/route.ts.ejs +4 -0
  15. package/templates/auth/sign-in-page.tsx.ejs +122 -0
  16. package/templates/auth/sign-up-page.tsx.ejs +137 -0
  17. package/templates/auth/unauthorized.tsx.ejs +28 -0
  18. package/templates/core/layout.tsx.ejs +36 -0
  19. package/templates/core/loading.tsx.ejs +7 -0
  20. package/templates/core/next.config.ts.ejs +33 -0
  21. package/templates/error-handling/catch-all-not-found-page.tsx.ejs +5 -0
  22. package/templates/error-handling/error.tsx.ejs +33 -0
  23. package/templates/error-handling/global-error.tsx.ejs +32 -0
  24. package/templates/error-handling/not-found.tsx.ejs +28 -0
  25. package/templates/eslint-prettier/.prettierignore.ejs +29 -0
  26. package/templates/eslint-prettier/eslint.config.mjs.ejs +31 -0
  27. package/templates/form-handling/dashboard-page.tsx.ejs +33 -0
  28. package/templates/form-handling/example-form-action.ts.ejs +50 -0
  29. package/templates/form-handling/example-form-schema.ts.ejs +87 -0
  30. package/templates/form-handling/example-form.tsx.ejs +428 -0
  31. package/templates/i18n/ar.json.ejs +77 -0
  32. package/templates/i18n/en.json.ejs +77 -0
  33. package/templates/i18n/locale-layout.tsx.ejs +45 -0
  34. package/templates/i18n/navigation.ts.ejs +5 -0
  35. package/templates/i18n/next-intl.d.ts.ejs +9 -0
  36. package/templates/i18n/request.ts.ejs +15 -0
  37. package/templates/i18n/routing.ts.ejs +7 -0
  38. package/templates/orm/prisma.config.ts.ejs +12 -0
  39. package/templates/orm/prisma.ts.ejs +17 -0
  40. package/templates/orm/schema.prisma.ejs +8 -0
  41. package/templates/pre-commit/prek.toml.ejs +35 -0
  42. package/templates/proxy/proxy.ts.ejs +81 -0
  43. package/templates/server-actions/safe-action.ts.ejs +51 -0
  44. package/templates/ui/client-side-wrappers.tsx.ejs +19 -0
  45. package/templates/ui/page.tsx.ejs +111 -0
package/dist/index.js CHANGED
@@ -2,23 +2,4225 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
+
6
+ // src/cli/commands/init.ts
7
+ import { exec as cpExec } from "child_process";
8
+ import { resolve as resolve3 } from "path";
9
+ import { InvalidArgumentError } from "commander";
10
+ import pc5 from "picocolors";
11
+
12
+ // src/cli/config.ts
13
+ var DEFAULT_CI_PROVIDER = "github";
14
+ var DEFAULT_ANALYTICS_PROVIDER = "plausible";
15
+ function deriveAuthStrategy(orm) {
16
+ return orm === "prisma" ? "stateful" : "stateless";
17
+ }
18
+ function resolveConfig(flags, answers) {
19
+ const projectName = flags.name ?? answers.projectName ?? "my-app";
20
+ const orm = flags.orm ? "prisma" : answers.orm ?? "none";
21
+ const authEnabled = flags.auth ?? answers.auth ?? false;
22
+ const optMods = new Set(answers.optionalModules ?? []);
23
+ const resolveCi = () => {
24
+ if (flags.ci !== void 0) {
25
+ return flags.ci === true ? DEFAULT_CI_PROVIDER : flags.ci;
26
+ }
27
+ return answers.ci ?? "none";
28
+ };
29
+ const resolveAnalytics = () => {
30
+ if (flags.analytics !== void 0) {
31
+ return flags.analytics === true ? DEFAULT_ANALYTICS_PROVIDER : flags.analytics;
32
+ }
33
+ return answers.analytics ?? "none";
34
+ };
35
+ return {
36
+ projectName,
37
+ orm,
38
+ authStrategy: authEnabled ? deriveAuthStrategy(orm) : "none",
39
+ aiChat: flags.chat ?? answers.aiChat ?? false,
40
+ i18n: flags.i18n ?? optMods.has("i18n"),
41
+ ci: resolveCi(),
42
+ analytics: resolveAnalytics(),
43
+ // Coming-soon modules are accepted by the CLI but must not affect the
44
+ // generator config until their DAG nodes are implemented.
45
+ jobs: false,
46
+ a11y: false,
47
+ apiClient: false,
48
+ preCommit: flags.preCommit ?? optMods.has("preCommit")
49
+ };
50
+ }
51
+
52
+ // src/cli/prompts/tech-picker.ts
53
+ import * as p from "@clack/prompts";
54
+ import pc2 from "picocolors";
55
+
56
+ // src/cli/ui/theme.ts
57
+ import gradient from "gradient-string";
58
+ import pc from "picocolors";
59
+ var ikeaGradient = gradient(["#FFDB00", "#0058A3"]);
60
+ var ikea = {
61
+ heading: (s) => pc.bold(pc.yellow(s)),
62
+ accent: pc.yellow,
63
+ muted: pc.dim,
64
+ success: pc.green,
65
+ bullet: pc.yellow("\u25B8"),
66
+ dimBullet: pc.dim("\u25B8")
67
+ };
68
+ function sectionDivider(title) {
69
+ const line = "\u2500".repeat(Math.max(1, 40 - title.length));
70
+ return pc.dim(` \u2500\u2500 ${pc.yellow(title)} ${line}`);
71
+ }
72
+
73
+ // src/cli/prompts/tech-picker.ts
74
+ function cancelled() {
75
+ p.cancel(pc2.yellow("Assembly cancelled \u2014 no parts were harmed."));
76
+ process.exit(0);
77
+ }
78
+ async function runTechPicker(flags) {
79
+ const answers = {};
80
+ p.intro(ikeaGradient(" S K A L S T \xC5 K K "));
81
+ console.log(sectionDivider("Core Stack"));
82
+ console.log();
83
+ if (!flags.name) {
84
+ const name = await p.text({
85
+ message: "Project name",
86
+ placeholder: "my-app",
87
+ validate: (v = "") => {
88
+ if (!v.trim()) return "Project name is required";
89
+ if (!/^[a-z0-9@][a-z0-9._-]*$/i.test(v.trim()))
90
+ return "Invalid package name";
91
+ }
92
+ });
93
+ if (p.isCancel(name)) cancelled();
94
+ answers.projectName = name;
95
+ }
96
+ if (flags.orm === void 0) {
97
+ const orm = await p.confirm({
98
+ message: "Add Prisma ORM? " + ikea.muted(
99
+ "\u2014 schema, migrations, seed script, singleton client \u2014 v7 + Driver Adapters"
100
+ ),
101
+ initialValue: false
102
+ });
103
+ if (p.isCancel(orm)) cancelled();
104
+ answers.orm = orm ? "prisma" : "none";
105
+ }
106
+ if (flags.auth === void 0) {
107
+ const resolvedOrm = flags.orm ? "prisma" : answers.orm ?? "none";
108
+ const hint = resolvedOrm === "prisma" ? "DB-backed sessions, login/signup pages, Prisma adapter" : "stateless JWT sessions, login/signup pages, no DB required";
109
+ const auth = await p.confirm({
110
+ message: "Add authentication? " + ikea.muted(`\u2014 Better Auth \xB7 ${hint}`),
111
+ initialValue: false
112
+ });
113
+ if (p.isCancel(auth)) cancelled();
114
+ answers.auth = auth;
115
+ }
116
+ if (flags.jobs === void 0) {
117
+ console.log(
118
+ ` ${pc2.yellow("\u25B8")} Background jobs ` + ikea.muted("\u2014 Inngest (not implemented; skipped)")
119
+ );
120
+ }
121
+ if (flags.chat === void 0) {
122
+ const chat = await p.confirm({
123
+ message: "Enable AI chat? " + ikea.muted("\u2014 Vercel AI SDK, streaming /api/chat, AI Elements UI"),
124
+ initialValue: false
125
+ });
126
+ if (p.isCancel(chat)) cancelled();
127
+ answers.aiChat = chat;
128
+ }
129
+ console.log();
130
+ console.log(sectionDivider("Features"));
131
+ console.log();
132
+ if (flags.ci === void 0) {
133
+ const ci = await p.select({
134
+ message: "CI pipeline",
135
+ options: [
136
+ {
137
+ value: "github",
138
+ label: pc2.bold("GitHub Actions"),
139
+ hint: ikea.muted(
140
+ "lint and Docker build now; test jobs as TODO no-ops"
141
+ )
142
+ },
143
+ {
144
+ value: "circleci",
145
+ label: "CircleCI (coming soon)",
146
+ hint: ikea.muted("placeholder option for a later release")
147
+ },
148
+ {
149
+ value: "none",
150
+ label: "None",
151
+ hint: ikea.muted("no CI pipeline \u2014 add your own later")
152
+ }
153
+ ]
154
+ });
155
+ if (p.isCancel(ci)) cancelled();
156
+ answers.ci = ci;
157
+ }
158
+ if (flags.analytics === void 0) {
159
+ const analytics = await p.select({
160
+ message: "Analytics",
161
+ options: [
162
+ {
163
+ value: "plausible",
164
+ label: pc2.bold("Plausible"),
165
+ hint: ikea.muted(
166
+ "privacy-friendly web analytics with local Compose support"
167
+ )
168
+ },
169
+ {
170
+ value: "posthog",
171
+ label: "PostHog (not implemented)",
172
+ hint: ikea.muted("placeholder option for a later release")
173
+ },
174
+ {
175
+ value: "none",
176
+ label: "None",
177
+ hint: ikea.muted("no analytics \u2014 add your own later")
178
+ }
179
+ ]
180
+ });
181
+ if (p.isCancel(analytics)) cancelled();
182
+ answers.analytics = analytics;
183
+ }
184
+ const alreadySelected = /* @__PURE__ */ new Set();
185
+ if (flags.a11y) alreadySelected.add("a11y");
186
+ if (flags.apiClient) alreadySelected.add("apiClient");
187
+ if (flags.i18n) alreadySelected.add("i18n");
188
+ if (flags.preCommit) alreadySelected.add("preCommit");
189
+ const availableModules = [
190
+ {
191
+ value: "a11y",
192
+ label: "Accessibility (coming soon)",
193
+ hint: ikea.muted("placeholder option for a later release")
194
+ },
195
+ {
196
+ value: "apiClient",
197
+ label: "API client (coming soon)",
198
+ hint: ikea.muted("placeholder option for a later release")
199
+ },
200
+ {
201
+ value: "i18n",
202
+ label: "Internationalization",
203
+ hint: ikea.muted("next-intl, locale routing, <LocaleSwitcher>")
204
+ },
205
+ {
206
+ value: "preCommit",
207
+ label: "Pre-commit hooks",
208
+ hint: ikea.muted("prek hooks for lint, format, and typecheck")
209
+ }
210
+ ].filter((m) => !alreadySelected.has(m.value));
211
+ if (availableModules.length > 0) {
212
+ console.log();
213
+ console.log(sectionDivider("Smart-Fittings"));
214
+ console.log();
215
+ const selected = await p.multiselect({
216
+ message: "Optional modules " + ikea.muted("\u2014 pick only what you need"),
217
+ options: availableModules,
218
+ required: false
219
+ });
220
+ if (p.isCancel(selected)) cancelled();
221
+ answers.optionalModules = selected;
222
+ }
223
+ p.outro(
224
+ pc2.green("\u2713") + " All parts selected. " + ikea.heading("Ready for assembly.")
225
+ );
226
+ return answers;
227
+ }
228
+
229
+ // src/cli/ui/config-display.ts
230
+ import pc3 from "picocolors";
231
+
232
+ // src/cli/ui/animate.ts
233
+ function sleep(ms) {
234
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
235
+ }
236
+ function cursorUp(n) {
237
+ process.stdout.write(`\x1B[${n}A`);
238
+ }
239
+ async function cascade(lines, delayMs) {
240
+ for (const line of lines) {
241
+ console.log(line);
242
+ await sleep(delayMs);
243
+ }
244
+ }
245
+ async function typewriter(text2, charDelayMs) {
246
+ for (const char of text2) {
247
+ process.stdout.write(char);
248
+ if (char !== " " && char !== "\n") {
249
+ await sleep(charDelayMs);
250
+ }
251
+ }
252
+ }
253
+
254
+ // src/cli/ui/config-display.ts
255
+ function row(content) {
256
+ return ` \u2502 ${content}`;
257
+ }
258
+ function labelRow(key, value, maxKey) {
259
+ const dots = "\xB7".repeat(Math.max(1, maxKey - key.length + 2));
260
+ return row(`${ikea.bullet} ${key} ${pc3.dim(dots)} ${value}`);
261
+ }
262
+ function formatValue(val) {
263
+ if (val === true || val === "yes") return pc3.green("yes");
264
+ if (val === false || val === "no" || val === "none") return ikea.muted("no");
265
+ return pc3.green(val);
266
+ }
267
+ function buildLines(config) {
268
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
269
+ const header = "S K A L S T \xC5 K K";
270
+ const artNo = `Art.${today.replace(/-/g, ".")}`;
271
+ const coreEntries = [
272
+ ["Project", config.projectName],
273
+ ["ORM", config.orm === "none" ? false : config.orm],
274
+ ["Auth", config.authStrategy === "none" ? false : config.authStrategy],
275
+ ["Jobs", config.jobs],
276
+ ["AI Chat", config.aiChat]
277
+ ];
278
+ const featureEntries = [
279
+ ["CI", config.ci === "none" ? false : config.ci],
280
+ ["Analytics", config.analytics === "none" ? false : config.analytics]
281
+ ];
282
+ const moduleEntries = [
283
+ ["a11y", config.a11y],
284
+ ["API Client", config.apiClient],
285
+ ["i18n", config.i18n],
286
+ ["Pre-commit", config.preCommit]
287
+ ];
288
+ const allEntries = [...coreEntries, ...featureEntries, ...moduleEntries];
289
+ const maxKey = Math.max(...allEntries.map(([k]) => k.length));
290
+ const lines = [];
291
+ lines.push(pc3.dim(" \u250C\u2500\u2500"));
292
+ lines.push(row(ikeaGradient(header) + " " + ikea.muted(artNo)));
293
+ lines.push(row(ikea.muted("Assembly summary")));
294
+ lines.push(pc3.dim(" \u2502"));
295
+ for (const [key, val] of coreEntries) {
296
+ lines.push(labelRow(key, formatValue(val), maxKey));
297
+ }
298
+ lines.push(pc3.dim(" \u2502"));
299
+ lines.push(row(pc3.yellow("Features:")));
300
+ lines.push(pc3.dim(" \u2502"));
301
+ for (const [key, val] of featureEntries) {
302
+ lines.push(labelRow(key, formatValue(val), maxKey));
303
+ }
304
+ lines.push(pc3.dim(" \u2502"));
305
+ lines.push(row(pc3.yellow("Smart-Fittings (modules):")));
306
+ lines.push(pc3.dim(" \u2502"));
307
+ for (const [key, val] of moduleEntries) {
308
+ lines.push(labelRow(key, formatValue(val), maxKey));
309
+ }
310
+ lines.push(pc3.dim(" \u2502"));
311
+ lines.push(row(ikea.muted(artNo)));
312
+ lines.push(pc3.dim(" \u2514\u2500\u2500"));
313
+ return lines;
314
+ }
315
+ async function animateConfigLabel(config) {
316
+ const lines = buildLines(config);
317
+ console.log();
318
+ await cascade(lines, 25);
319
+ console.log();
320
+ }
321
+ function printConfigLabel(config) {
322
+ const lines = buildLines(config);
323
+ console.log();
324
+ for (const line of lines) console.log(line);
325
+ console.log();
326
+ }
327
+
328
+ // src/engine/runner.ts
329
+ import { exec } from "child_process";
330
+ import { join as join4 } from "path";
331
+ import { promisify } from "util";
332
+
333
+ // src/engine/dag.ts
334
+ function getDependencies(generator, config) {
335
+ return [
336
+ ...generator.dependencies,
337
+ ...generator.resolveDependencies?.(config) ?? []
338
+ ];
339
+ }
340
+ function buildDAG(generators2, config) {
341
+ const active = generators2.filter((g) => !g.condition || g.condition(config));
342
+ const byName = /* @__PURE__ */ new Map();
343
+ for (const g of active) {
344
+ byName.set(g.name, g);
345
+ }
346
+ for (const g of active) {
347
+ for (const dep of getDependencies(g, config)) {
348
+ if (!byName.has(dep)) {
349
+ throw new Error(
350
+ `Generator "${g.name}" depends on "${dep}", which is not registered or was excluded by its condition`
351
+ );
352
+ }
353
+ }
354
+ }
355
+ const inDegree = /* @__PURE__ */ new Map();
356
+ const dependents = /* @__PURE__ */ new Map();
357
+ for (const g of active) {
358
+ inDegree.set(g.name, 0);
359
+ dependents.set(g.name, []);
360
+ }
361
+ for (const g of active) {
362
+ for (const dep of getDependencies(g, config)) {
363
+ inDegree.set(g.name, (inDegree.get(g.name) ?? 0) + 1);
364
+ dependents.get(dep).push(g.name);
365
+ }
366
+ }
367
+ const queue = [];
368
+ for (const [name, degree] of inDegree) {
369
+ if (degree === 0) queue.push(name);
370
+ }
371
+ queue.sort();
372
+ const sorted = [];
373
+ while (queue.length > 0) {
374
+ const name = queue.shift();
375
+ sorted.push(byName.get(name));
376
+ const neighbors = dependents.get(name) ?? [];
377
+ neighbors.sort();
378
+ for (const neighbor of neighbors) {
379
+ const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
380
+ inDegree.set(neighbor, newDeg);
381
+ if (newDeg === 0) queue.push(neighbor);
382
+ }
383
+ queue.sort();
384
+ }
385
+ if (sorted.length !== active.length) {
386
+ const remaining = active.filter((g) => !sorted.some((s) => s.name === g.name)).map((g) => g.name);
387
+ throw new Error(
388
+ `Dependency cycle detected among generators: ${remaining.join(", ")}`
389
+ );
390
+ }
391
+ return sorted;
392
+ }
393
+
394
+ // src/engine/fs.ts
395
+ import { readFile, writeFile, mkdir, rm, readdir } from "fs/promises";
396
+ import { existsSync } from "fs";
397
+ import { resolve, dirname, relative, join } from "path";
398
+ import { createPatch } from "diff";
399
+ var VirtualFS = class {
400
+ operations = [];
401
+ /** Maps absolute path → original content (string) or null if the file was new */
402
+ committedFiles = /* @__PURE__ */ new Map();
403
+ targetDir;
404
+ constructor(targetDir) {
405
+ this.targetDir = resolve(targetDir);
406
+ }
407
+ abs(p2) {
408
+ return resolve(this.targetDir, p2);
409
+ }
410
+ write(path, content) {
411
+ this.operations.push({ type: "write", path, content });
412
+ }
413
+ copy(src, dest) {
414
+ this.operations.push({ type: "copy", src, dest });
415
+ }
416
+ merge(path, mergeFn) {
417
+ this.operations.push({ type: "merge", path, mergeFn });
418
+ }
419
+ async commit() {
420
+ for (const op of this.operations) {
421
+ switch (op.type) {
422
+ case "write": {
423
+ const abs = this.abs(op.path);
424
+ await this.trackOriginal(abs);
425
+ await mkdir(dirname(abs), { recursive: true });
426
+ await writeFile(abs, op.content, "utf-8");
427
+ break;
428
+ }
429
+ case "copy": {
430
+ const abs = this.abs(op.dest);
431
+ await this.trackOriginal(abs);
432
+ const content = await readFile(resolve(op.src), "utf-8");
433
+ await mkdir(dirname(abs), { recursive: true });
434
+ await writeFile(abs, content, "utf-8");
435
+ break;
436
+ }
437
+ case "merge": {
438
+ const abs = this.abs(op.path);
439
+ const existing = existsSync(abs) ? await readFile(abs, "utf-8") : "";
440
+ await this.trackOriginal(abs);
441
+ const merged = op.mergeFn(existing);
442
+ await mkdir(dirname(abs), { recursive: true });
443
+ await writeFile(abs, merged, "utf-8");
444
+ break;
445
+ }
446
+ }
447
+ }
448
+ }
449
+ async rollback() {
450
+ for (const [abs, original] of this.committedFiles) {
451
+ if (original === null) {
452
+ if (existsSync(abs)) await rm(abs);
453
+ } else {
454
+ await writeFile(abs, original, "utf-8");
455
+ }
456
+ }
457
+ const dirs = /* @__PURE__ */ new Set();
458
+ for (const abs of this.committedFiles.keys()) {
459
+ let dir = dirname(abs);
460
+ while (dir.startsWith(this.targetDir) && dir !== this.targetDir) {
461
+ dirs.add(dir);
462
+ dir = dirname(dir);
463
+ }
464
+ }
465
+ const sortedDirs = [...dirs].sort((a, b) => b.length - a.length);
466
+ for (const dir of sortedDirs) {
467
+ try {
468
+ const entries = await readdir(dir);
469
+ if (entries.length === 0) await rm(dir, { recursive: true });
470
+ } catch {
471
+ }
472
+ }
473
+ this.committedFiles.clear();
474
+ }
475
+ async dryRun() {
476
+ const results = [];
477
+ for (const op of this.operations) {
478
+ switch (op.type) {
479
+ case "write": {
480
+ const abs = this.abs(op.path);
481
+ if (existsSync(abs)) {
482
+ const existing = await readFile(abs, "utf-8");
483
+ results.push({
484
+ path: op.path,
485
+ action: "modify",
486
+ diff: createPatch(op.path, existing, op.content)
487
+ });
488
+ } else {
489
+ results.push({
490
+ path: op.path,
491
+ action: "create",
492
+ content: op.content
493
+ });
494
+ }
495
+ break;
496
+ }
497
+ case "copy": {
498
+ const content = await readFile(resolve(op.src), "utf-8");
499
+ const abs = this.abs(op.dest);
500
+ if (existsSync(abs)) {
501
+ const existing = await readFile(abs, "utf-8");
502
+ results.push({
503
+ path: op.dest,
504
+ action: "modify",
505
+ diff: createPatch(op.dest, existing, content)
506
+ });
507
+ } else {
508
+ results.push({
509
+ path: op.dest,
510
+ action: "copy",
511
+ content
512
+ });
513
+ }
514
+ break;
515
+ }
516
+ case "merge": {
517
+ const abs = this.abs(op.path);
518
+ const existing = existsSync(abs) ? await readFile(abs, "utf-8") : "";
519
+ const merged = op.mergeFn(existing);
520
+ if (existing) {
521
+ results.push({
522
+ path: op.path,
523
+ action: "modify",
524
+ diff: createPatch(op.path, existing, merged)
525
+ });
526
+ } else {
527
+ results.push({
528
+ path: op.path,
529
+ action: "create",
530
+ content: merged
531
+ });
532
+ }
533
+ break;
534
+ }
535
+ }
536
+ }
537
+ return results;
538
+ }
539
+ // ── Adopt pattern for external tool output ──────────────
540
+ async snapshotDir() {
541
+ const snapshot = /* @__PURE__ */ new Map();
542
+ if (!existsSync(this.targetDir)) return snapshot;
543
+ await this.walkDir(this.targetDir, snapshot);
544
+ return snapshot;
545
+ }
546
+ async adoptExternalChanges(before) {
547
+ const after = await this.snapshotDir();
548
+ for (const [relPath, content] of after) {
549
+ const abs = this.abs(relPath);
550
+ if (this.committedFiles.has(abs)) continue;
551
+ const prev = before.get(relPath);
552
+ if (prev === void 0) {
553
+ this.committedFiles.set(abs, null);
554
+ } else if (prev !== content) {
555
+ this.committedFiles.set(abs, prev);
556
+ }
557
+ }
558
+ for (const [relPath] of before) {
559
+ if (!after.has(relPath)) {
560
+ const abs = this.abs(relPath);
561
+ if (!this.committedFiles.has(abs)) {
562
+ this.committedFiles.set(abs, before.get(relPath));
563
+ }
564
+ }
565
+ }
566
+ }
567
+ // ── Private helpers ────────────────────────────────────
568
+ async trackOriginal(abs) {
569
+ if (this.committedFiles.has(abs)) return;
570
+ if (existsSync(abs)) {
571
+ this.committedFiles.set(abs, await readFile(abs, "utf-8"));
572
+ } else {
573
+ this.committedFiles.set(abs, null);
574
+ }
575
+ }
576
+ async walkDir(dir, snapshot) {
577
+ const entries = await readdir(dir, { withFileTypes: true });
578
+ for (const entry of entries) {
579
+ const full = join(dir, entry.name);
580
+ if (entry.isDirectory()) {
581
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
582
+ await this.walkDir(full, snapshot);
583
+ } else if (entry.isFile()) {
584
+ try {
585
+ const content = await readFile(full, "utf-8");
586
+ snapshot.set(relative(this.targetDir, full), content);
587
+ } catch {
588
+ }
589
+ }
590
+ }
591
+ }
592
+ };
593
+
594
+ // src/engine/template.ts
595
+ import ejs from "ejs";
596
+ import { existsSync as existsSync2 } from "fs";
597
+ import { join as join2, resolve as resolve2 } from "path";
598
+ function resolveTemplatesDir() {
599
+ const here = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
600
+ for (const candidate of [
601
+ join2(here, "../../templates"),
602
+ join2(here, "../templates")
603
+ ]) {
604
+ if (existsSync2(candidate)) return candidate;
605
+ }
606
+ return join2(here, "../../templates");
607
+ }
608
+ var TemplateEngine = class {
609
+ templatesDir;
610
+ constructor(templatesDir) {
611
+ this.templatesDir = resolve2(templatesDir);
612
+ }
613
+ async renderFile(templatePath, data) {
614
+ const full = resolve2(this.templatesDir, templatePath);
615
+ return ejs.renderFile(full, data);
616
+ }
617
+ renderString(template, data) {
618
+ return ejs.render(template, data);
619
+ }
620
+ };
621
+ var DEFAULT_TEMPLATE_ENGINE = new TemplateEngine(
622
+ resolveTemplatesDir()
623
+ );
624
+ function renderTemplateFile(templatePath, data) {
625
+ return DEFAULT_TEMPLATE_ENGINE.renderFile(templatePath, data);
626
+ }
627
+
628
+ // src/engine/logger.ts
629
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
630
+ import { existsSync as existsSync3 } from "fs";
631
+ import { join as join3 } from "path";
632
+ import pc4 from "picocolors";
633
+ var Logger = class {
634
+ entries = [];
635
+ quiet;
636
+ constructor(quiet = false) {
637
+ this.quiet = quiet;
638
+ }
639
+ info(generator, message) {
640
+ this.log("info", generator, message);
641
+ }
642
+ success(generator, message) {
643
+ this.log("success", generator, message);
644
+ }
645
+ warn(generator, message) {
646
+ this.log("warn", generator, message);
647
+ }
648
+ error(generator, message, err) {
649
+ this.log("error", generator, message, void 0, err);
650
+ }
651
+ fileWritten(generator, path) {
652
+ this.log("info", generator, `wrote ${path}`, path);
653
+ }
654
+ async flush(logDir) {
655
+ if (this.entries.length === 0) return;
656
+ if (!existsSync3(logDir)) {
657
+ await mkdir2(logDir, { recursive: true });
658
+ }
659
+ const logPath = join3(logDir, "generate.log");
660
+ const ndjson = this.entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
661
+ await writeFile2(logPath, ndjson, { flag: "a", encoding: "utf-8" });
662
+ }
663
+ // ── Private ───────────────────────────────────────────
664
+ log(level, generator, message, file, err) {
665
+ const entry = {
666
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
667
+ level,
668
+ generator,
669
+ message,
670
+ ...file && { file },
671
+ ...err && { error: err.message }
672
+ };
673
+ this.entries.push(entry);
674
+ if (this.quiet) return;
675
+ const tag = pc4.dim(`[${generator}]`);
676
+ const formatted = CONSOLE_FORMATTERS[level](message);
677
+ console.log(` ${tag} ${formatted}`);
678
+ }
679
+ };
680
+ var CONSOLE_FORMATTERS = {
681
+ info: (msg) => msg,
682
+ success: (msg) => pc4.green(`\u2713 ${msg}`),
683
+ warn: (msg) => pc4.yellow(`\u26A0 ${msg}`),
684
+ error: (msg) => pc4.red(`\u2717 ${msg}`),
685
+ // The engine never emits "skip"; sync uses it to mark conflict-skipped keys.
686
+ // Included so CONSOLE_FORMATTERS covers the full LogLevel union.
687
+ skip: (msg) => pc4.dim(`\u21B7 ${msg}`)
688
+ };
689
+
690
+ // src/types/env-fragments.ts
691
+ function createEnvFragmentBag() {
692
+ const fragments = [];
693
+ return {
694
+ fragments,
695
+ push(fragment) {
696
+ fragments.push(fragment);
697
+ }
698
+ };
699
+ }
700
+
701
+ // src/engine/runner.ts
702
+ var execAsync = promisify(exec);
703
+ function isExecException(e) {
704
+ return e instanceof Error && ("stdout" in e || "stderr" in e || "code" in e || "signal" in e);
705
+ }
706
+ function asString(v) {
707
+ if (v === void 0) return "";
708
+ return typeof v === "string" ? v : v.toString("utf-8");
709
+ }
710
+ async function runCommand(options) {
711
+ const { cmd, cwd, env, maxBuffer, generator, logger } = options;
712
+ logger.info(generator, `exec: ${cmd}`);
713
+ try {
714
+ const { stdout, stderr } = await execAsync(cmd, { cwd, env, maxBuffer });
715
+ const stdoutStr = asString(stdout);
716
+ const stderrStr = asString(stderr);
717
+ if (stdoutStr) logger.info(generator, stdoutStr.trimEnd());
718
+ if (stderrStr) logger.warn(generator, stderrStr.trimEnd());
719
+ return { stdout: stdoutStr, stderr: stderrStr, exitCode: 0 };
720
+ } catch (e) {
721
+ if (!isExecException(e)) {
722
+ throw e instanceof Error ? e : new Error(String(e));
723
+ }
724
+ const stdoutStr = asString(e.stdout);
725
+ const stderrStr = asString(e.stderr);
726
+ if (stdoutStr) logger.info(generator, stdoutStr.trimEnd());
727
+ if (stderrStr) logger.warn(generator, stderrStr.trimEnd());
728
+ const exitCode = typeof e.code === "number" ? e.code : 1;
729
+ throw new Error(
730
+ `Command "${cmd}" exited with code ${exitCode}: ${stderrStr}`,
731
+ { cause: e }
732
+ );
733
+ }
734
+ }
735
+ async function run(options) {
736
+ const {
737
+ generators: generators2,
738
+ config,
739
+ targetDir,
740
+ dryRun = false,
741
+ quiet = false
742
+ } = options;
743
+ const logger = new Logger(quiet);
744
+ const template = DEFAULT_TEMPLATE_ENGINE;
745
+ const envBag = createEnvFragmentBag();
746
+ let order;
747
+ try {
748
+ order = buildDAG(generators2, config);
749
+ } catch (err) {
750
+ return {
751
+ succeeded: [],
752
+ failed: [
753
+ {
754
+ name: "dag",
755
+ error: err instanceof Error ? err : new Error(String(err))
756
+ }
757
+ ],
758
+ skipped: generators2.map((g) => g.name),
759
+ envBag
760
+ };
761
+ }
762
+ const succeeded = [];
763
+ const failed = [];
764
+ const skipped = [];
765
+ const allDryRunResults = [];
766
+ const dryRunVfsInstances = [];
767
+ for (let i = 0; i < order.length; i++) {
768
+ const generator = order[i];
769
+ const vfs = new VirtualFS(targetDir);
770
+ const execFn = async (cmd, execOpts) => {
771
+ if (dryRun) {
772
+ logger.info(generator.name, `[dry-run] would exec: ${cmd}`);
773
+ allDryRunResults.push({
774
+ path: cmd,
775
+ action: "exec",
776
+ content: `would exec: ${cmd} (file changes not previewable)`
777
+ });
778
+ return { stdout: "", stderr: "", exitCode: 0 };
779
+ }
780
+ const snapshot = await vfs.snapshotDir();
781
+ try {
782
+ return await runCommand({
783
+ cmd,
784
+ cwd: execOpts?.cwd ?? targetDir,
785
+ env: { ...process.env, ...execOpts?.env },
786
+ maxBuffer: 10 * 1024 * 1024,
787
+ generator: generator.name,
788
+ logger
789
+ });
790
+ } finally {
791
+ await vfs.adoptExternalChanges(snapshot);
792
+ }
793
+ };
794
+ const ctx = {
795
+ config,
796
+ envBag,
797
+ fs: vfs,
798
+ template,
799
+ logger,
800
+ targetDir,
801
+ exec: execFn
802
+ };
803
+ try {
804
+ await generator.execute(ctx);
805
+ if (dryRun) {
806
+ const results = await vfs.dryRun();
807
+ allDryRunResults.push(...results);
808
+ await vfs.commit();
809
+ dryRunVfsInstances.push(vfs);
810
+ } else {
811
+ await vfs.commit();
812
+ }
813
+ succeeded.push(generator.name);
814
+ logger.success(generator.name, "completed");
815
+ } catch (err) {
816
+ const error = err instanceof Error ? err : new Error(String(err));
817
+ await vfs.rollback();
818
+ if (generator.rollback) {
819
+ try {
820
+ await generator.rollback(ctx);
821
+ } catch (rollbackErr) {
822
+ logger.warn(
823
+ generator.name,
824
+ `rollback handler failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
825
+ );
826
+ }
827
+ }
828
+ logger.error(generator.name, `failed: ${error.message}`, error);
829
+ failed.push({ name: generator.name, error });
830
+ for (let j = i + 1; j < order.length; j++) {
831
+ skipped.push(order[j].name);
832
+ }
833
+ break;
834
+ }
835
+ }
836
+ if (dryRun) {
837
+ for (const vfs of dryRunVfsInstances.reverse()) {
838
+ await vfs.rollback();
839
+ }
840
+ }
841
+ const logDir = join4(targetDir, ".scale-stack");
842
+ if (!dryRun) {
843
+ await logger.flush(logDir);
844
+ }
845
+ return {
846
+ succeeded,
847
+ failed,
848
+ skipped,
849
+ envBag,
850
+ ...dryRun && { dryRunResults: allDryRunResults }
851
+ };
852
+ }
853
+ async function runAfterInstall(opts) {
854
+ const {
855
+ generators: generators2,
856
+ config,
857
+ envBag,
858
+ targetDir,
859
+ succeeded,
860
+ quiet = false
861
+ } = opts;
862
+ const logger = new Logger(quiet);
863
+ const template = DEFAULT_TEMPLATE_ENGINE;
864
+ let ran = false;
865
+ for (const name of succeeded) {
866
+ const generator = generators2.find((g) => g.name === name);
867
+ if (!generator?.afterInstall) continue;
868
+ ran = true;
869
+ const vfs = new VirtualFS(targetDir);
870
+ const execFn = async (cmd, execOpts) => runCommand({
871
+ cmd,
872
+ cwd: execOpts?.cwd ?? targetDir,
873
+ env: { ...process.env, ...execOpts?.env },
874
+ maxBuffer: 50 * 1024 * 1024,
875
+ generator: generator.name,
876
+ logger
877
+ });
878
+ const ctx = {
879
+ config,
880
+ envBag,
881
+ fs: vfs,
882
+ template,
883
+ logger,
884
+ targetDir,
885
+ exec: execFn
886
+ };
887
+ try {
888
+ await generator.afterInstall(ctx);
889
+ await vfs.commit();
890
+ } catch (err) {
891
+ logger.warn(
892
+ generator.name,
893
+ `afterInstall did not complete: ${err instanceof Error ? err.message : String(err)}`
894
+ );
895
+ }
896
+ }
897
+ if (ran) {
898
+ await logger.flush(join4(targetDir, ".scale-stack"));
899
+ }
900
+ }
901
+
902
+ // src/generators/registry.ts
903
+ var generators = /* @__PURE__ */ new Map();
904
+ function registerGenerator(gen) {
905
+ if (generators.has(gen.name)) {
906
+ throw new Error(`Generator "${gen.name}" is already registered`);
907
+ }
908
+ generators.set(gen.name, gen);
909
+ }
910
+ function getRegisteredGenerators() {
911
+ return [...generators.values()];
912
+ }
913
+
914
+ // src/cli/commands/init.ts
915
+ function parseCiProvider(value) {
916
+ if (value === "github" || value === "circleci" || value === "none") {
917
+ return value;
918
+ }
919
+ throw new InvalidArgumentError(
920
+ "CI provider must be one of: github, circleci, none"
921
+ );
922
+ }
923
+ function parseAnalyticsProvider(value) {
924
+ if (value === "plausible" || value === "posthog" || value === "none") {
925
+ return value;
926
+ }
927
+ throw new InvalidArgumentError(
928
+ "Analytics provider must be one of: plausible, posthog, none"
929
+ );
930
+ }
931
+ function registerInitCommand(program2) {
932
+ program2.command("init").description("Assemble a new SKALST\xC5KK project").argument("[name]", "Project name").option("--name <name>", "Project name (overrides positional argument)").option("--orm", "Use Prisma ORM").option("--auth", "Enable authentication (Better Auth)").option("--chat", "Enable AI chat").option("--i18n", "Enable internationalization").option(
933
+ "--ci [provider]",
934
+ "CI pipeline: github, circleci, or none (default: github)",
935
+ parseCiProvider
936
+ ).option(
937
+ "--analytics [provider]",
938
+ "Analytics provider: plausible, posthog, or none (default: plausible)",
939
+ parseAnalyticsProvider
940
+ ).option("--jobs", "Background jobs (Inngest; not implemented, ignored)").option("--a11y", "Accessibility tooling (coming soon, ignored)").option("--api-client", "API client generation (coming soon, ignored)").option("--pre-commit", "Enable prek pre-commit hooks").option("-y, --yes", "Skip prompts, accept defaults").option("--dry-run", "Show what would be generated without writing files").action(async (positionalName, opts) => {
941
+ const flags = {
942
+ ...opts,
943
+ name: opts.name ?? positionalName
944
+ };
945
+ const answers = flags.yes ? {} : await runTechPicker(flags);
946
+ const config = resolveConfig(flags, answers);
947
+ if (flags.jobs) {
948
+ console.log(
949
+ ikea.muted(
950
+ ` ${pc5.yellow("\u25B8")} Inngest jobs are not implemented yet; skipping --jobs.`
951
+ )
952
+ );
953
+ }
954
+ if (flags.a11y || answers.optionalModules?.includes("a11y")) {
955
+ console.log(
956
+ ikea.muted(
957
+ ` ${pc5.yellow("\u25B8")} Accessibility tooling is coming soon; skipping --a11y.`
958
+ )
959
+ );
960
+ }
961
+ if (flags.apiClient || answers.optionalModules?.includes("apiClient")) {
962
+ console.log(
963
+ ikea.muted(
964
+ ` ${pc5.yellow("\u25B8")} API client generation is coming soon; skipping --api-client.`
965
+ )
966
+ );
967
+ }
968
+ if (process.stdout.isTTY) {
969
+ await animateConfigLabel(config);
970
+ } else {
971
+ printConfigLabel(config);
972
+ }
973
+ const targetDir = resolve3(process.cwd(), config.projectName);
974
+ const generators2 = getRegisteredGenerators();
975
+ if (generators2.length === 0) {
976
+ console.log(
977
+ ikea.muted(
978
+ ` ${pc5.yellow("\u25B8")} No generators registered \u2014 config collected only.`
979
+ )
980
+ );
981
+ console.log();
982
+ return;
983
+ }
984
+ const result = await run({
985
+ generators: generators2,
986
+ config,
987
+ targetDir,
988
+ dryRun: flags.dryRun
989
+ });
990
+ if (flags.dryRun && result.dryRunResults) {
991
+ console.log();
992
+ console.log(ikea.heading(" Dry-run results:"));
993
+ console.log();
994
+ for (const dr of result.dryRunResults) {
995
+ const tag = dr.action === "exec" ? pc5.cyan("exec") : dr.action === "create" ? pc5.green("create") : dr.action === "copy" ? pc5.blue("copy") : pc5.yellow("modify");
996
+ console.log(` ${tag} ${dr.path}`);
997
+ if (dr.diff) {
998
+ for (const line of dr.diff.split("\n").slice(2)) {
999
+ if (line.startsWith("+")) console.log(` ${pc5.green(line)}`);
1000
+ else if (line.startsWith("-")) console.log(` ${pc5.red(line)}`);
1001
+ else console.log(` ${pc5.dim(line)}`);
1002
+ }
1003
+ }
1004
+ }
1005
+ console.log();
1006
+ return;
1007
+ }
1008
+ if (result.failed.length > 0) {
1009
+ console.log();
1010
+ console.log(
1011
+ pc5.red(
1012
+ ` \u2717 Generation failed at "${result.failed[0].name}": ${result.failed[0].error.message}`
1013
+ )
1014
+ );
1015
+ if (result.succeeded.length > 0) {
1016
+ console.log(
1017
+ ikea.muted(` Succeeded: ${result.succeeded.join(", ")}`)
1018
+ );
1019
+ }
1020
+ if (result.skipped.length > 0) {
1021
+ console.log(ikea.muted(` Skipped: ${result.skipped.join(", ")}`));
1022
+ }
1023
+ console.log();
1024
+ process.exitCode = 1;
1025
+ return;
1026
+ }
1027
+ if (!flags.dryRun) {
1028
+ console.log();
1029
+ console.log(ikea.muted(` ${pc5.cyan("\u25B8")} pnpm install\u2026`));
1030
+ try {
1031
+ await new Promise((resolvePromise, rejectPromise) => {
1032
+ cpExec(
1033
+ "pnpm install",
1034
+ { cwd: targetDir, maxBuffer: 50 * 1024 * 1024 },
1035
+ (err, stdout, stderr) => {
1036
+ if (stdout) process.stdout.write(stdout);
1037
+ if (stderr) process.stderr.write(stderr);
1038
+ if (err) rejectPromise(err);
1039
+ else resolvePromise();
1040
+ }
1041
+ );
1042
+ });
1043
+ } catch (err) {
1044
+ console.log();
1045
+ console.log(
1046
+ pc5.red(
1047
+ ` \u2717 pnpm install failed: ${err instanceof Error ? err.message : String(err)}`
1048
+ )
1049
+ );
1050
+ console.log();
1051
+ process.exitCode = 1;
1052
+ return;
1053
+ }
1054
+ await runAfterInstall({
1055
+ generators: generators2,
1056
+ config,
1057
+ envBag: result.envBag,
1058
+ targetDir,
1059
+ succeeded: result.succeeded
1060
+ });
1061
+ }
1062
+ console.log();
1063
+ console.log(pc5.green(` \u2713 Project assembled at ${pc5.bold(targetDir)}`));
1064
+ console.log();
1065
+ });
1066
+ }
1067
+
1068
+ // src/cli/commands/sync.ts
1069
+ import { resolve as resolve6 } from "path";
1070
+ import pc6 from "picocolors";
1071
+
1072
+ // src/sync/run-sync.ts
1073
+ import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
1074
+ import { existsSync as existsSync10 } from "fs";
1075
+ import { createPatch as createPatch2 } from "diff";
1076
+ import { exec as exec2 } from "child_process";
1077
+ import { join as join12 } from "path";
1078
+ import { promisify as promisify2 } from "util";
1079
+
1080
+ // src/sync/cli-version.ts
5
1081
  import { readFileSync } from "fs";
1082
+ import { dirname as dirname2, resolve as resolve4 } from "path";
6
1083
  import { fileURLToPath } from "url";
7
- import { dirname, resolve } from "path";
8
- var __dirname = dirname(fileURLToPath(import.meta.url));
9
- var pkgVersion = "0.0.0";
10
- try {
11
- const pkg = JSON.parse(
12
- readFileSync(resolve(__dirname, "../package.json"), "utf-8")
13
- );
14
- pkgVersion = pkg.version ?? pkgVersion;
15
- } catch {
16
- }
17
- var program = new Command().name("scale-stack").description("Opinionated scaffolding engine for Next.js projects").version(pkgVersion);
18
- program.command("init").description("Scaffold a new Scale Stack project").argument("[name]", "Project name").action((_name) => {
19
- console.log("init is not yet implemented");
20
- });
21
- program.command("sync").description("Update an existing Scale Stack project").action(() => {
22
- console.log("sync is not yet implemented");
1084
+ var cachedVersion;
1085
+ function getCliVersion() {
1086
+ if (cachedVersion !== void 0) return cachedVersion;
1087
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
1088
+ try {
1089
+ const pkg = JSON.parse(
1090
+ readFileSync(resolve4(__dirname, "../package.json"), "utf-8")
1091
+ );
1092
+ cachedVersion = pkg.version ?? "0.0.0";
1093
+ } catch {
1094
+ cachedVersion = "0.0.0";
1095
+ }
1096
+ return cachedVersion;
1097
+ }
1098
+
1099
+ // src/sync/targets.ts
1100
+ import { existsSync as existsSync5 } from "fs";
1101
+ import { join as join6 } from "path";
1102
+
1103
+ // src/config/eslint-config-contents.ts
1104
+ function shouldIgnorePrismaGeneratedOutput(config) {
1105
+ return config.orm === "prisma";
1106
+ }
1107
+ function shouldIgnoreAiElementsGeneratedOutput(config) {
1108
+ return config.aiChat === true;
1109
+ }
1110
+ function buildEslintConfigMjsContents(config) {
1111
+ return renderTemplateFile("eslint-prettier/eslint.config.mjs.ejs", {
1112
+ ignoreAiElementsGeneratedOutput: shouldIgnoreAiElementsGeneratedOutput(config),
1113
+ ignorePrismaGeneratedOutput: shouldIgnorePrismaGeneratedOutput(config)
1114
+ });
1115
+ }
1116
+
1117
+ // src/config/ai-chat-fragments.ts
1118
+ var AI_SDK_VERSION = "^6.0.175";
1119
+ var AI_SDK_REACT_VERSION = "^3.0.177";
1120
+ var AI_CHAT_DEPENDENCIES = {
1121
+ "@ai-sdk/react": AI_SDK_REACT_VERSION,
1122
+ ai: AI_SDK_VERSION
1123
+ };
1124
+
1125
+ // src/config/auth-fragments.ts
1126
+ var BETTER_AUTH_VERSION = "^1.6.9";
1127
+ var BETTER_AUTH_PRISMA_ADAPTER_VERSION = "^1.6.9";
1128
+ var AUTH_DEPENDENCIES = {
1129
+ "@better-auth/prisma-adapter": BETTER_AUTH_PRISMA_ADAPTER_VERSION,
1130
+ "better-auth": BETTER_AUTH_VERSION
1131
+ };
1132
+ function buildAuthEnvFragment() {
1133
+ return {
1134
+ variables: [
1135
+ {
1136
+ name: "BETTER_AUTH_SECRET",
1137
+ scope: "server",
1138
+ schema: "z.string().min(32)",
1139
+ example: "replace-with-at-least-32-character-secret"
1140
+ },
1141
+ {
1142
+ name: "BETTER_AUTH_URL",
1143
+ scope: "server",
1144
+ schema: "z.string().url()",
1145
+ example: "http://localhost:3000"
1146
+ },
1147
+ {
1148
+ name: "MICROSOFT_CLIENT_ID",
1149
+ scope: "server",
1150
+ schema: "z.string().min(1)",
1151
+ example: "replace-with-microsoft-client-id"
1152
+ },
1153
+ {
1154
+ name: "MICROSOFT_CLIENT_SECRET",
1155
+ scope: "server",
1156
+ schema: "z.string().min(1)",
1157
+ example: "replace-with-microsoft-client-secret"
1158
+ },
1159
+ {
1160
+ name: "MICROSOFT_TENANT_ID",
1161
+ scope: "server",
1162
+ schema: "z.string().min(1)",
1163
+ example: "common"
1164
+ }
1165
+ ]
1166
+ };
1167
+ }
1168
+
1169
+ // src/config/env-management-fragments.ts
1170
+ var T3_ENV_NEXTJS_VERSION = "^0.13.11";
1171
+ var ZOD_VERSION = "^4.3.6";
1172
+ var ENV_MANAGEMENT_DEPENDENCIES = {
1173
+ "@t3-oss/env-nextjs": T3_ENV_NEXTJS_VERSION,
1174
+ zod: ZOD_VERSION
1175
+ };
1176
+
1177
+ // src/config/form-handling-fragments.ts
1178
+ var HOOKFORM_RESOLVERS_VERSION = "^5.2.2";
1179
+ var NEXT_SAFE_ACTION_RHF_ADAPTER_VERSION = "^2.0.6";
1180
+ var REACT_HOOK_FORM_VERSION = "^7.74.0";
1181
+ var FORM_HANDLING_DEPENDENCIES = {
1182
+ "@hookform/resolvers": HOOKFORM_RESOLVERS_VERSION,
1183
+ "@next-safe-action/adapter-react-hook-form": NEXT_SAFE_ACTION_RHF_ADAPTER_VERSION,
1184
+ "react-hook-form": REACT_HOOK_FORM_VERSION
1185
+ };
1186
+
1187
+ // src/config/i18n-fragments.ts
1188
+ var NEXT_INTL_VERSION = "^4.6.0";
1189
+ var I18N_DEPENDENCIES = {
1190
+ "next-intl": NEXT_INTL_VERSION
1191
+ };
1192
+
1193
+ // src/config/orm-fragments.ts
1194
+ var PRISMA_VERSION = "^7.8.0";
1195
+ var PRISMA_CLIENT_VERSION = "^7.8.0";
1196
+ var PRISMA_ADAPTER_PG_VERSION = "^7.8.0";
1197
+ var PG_VERSION = "^8.20.0";
1198
+ var TYPES_PG_VERSION = "^8.20.0";
1199
+ var TSX_VERSION = "^4.21.0";
1200
+ var ORM_DEPENDENCIES = {
1201
+ "@prisma/adapter-pg": PRISMA_ADAPTER_PG_VERSION,
1202
+ "@prisma/client": PRISMA_CLIENT_VERSION,
1203
+ pg: PG_VERSION
1204
+ };
1205
+ var ORM_DEV_DEPENDENCIES = {
1206
+ "@types/pg": TYPES_PG_VERSION,
1207
+ prisma: PRISMA_VERSION,
1208
+ tsx: TSX_VERSION
1209
+ };
1210
+ var ORM_SCRIPT_DESCRIPTIONS = {
1211
+ "db:generate": "Regenerate Prisma Client from prisma/schema.prisma.",
1212
+ "db:validate": "Validate prisma/schema.prisma and prisma.config.ts.",
1213
+ "db:migrate": "Create and apply a local development migration.",
1214
+ "db:migrate:deploy": "Apply committed migrations in CI or production.",
1215
+ "db:migrate:status": "Show whether database migrations are in sync.",
1216
+ "db:push": "Push the Prisma schema to the database without a migration.",
1217
+ "db:pull": "Introspect the database and update the Prisma schema.",
1218
+ "db:studio": "Open Prisma Studio for local data browsing."
1219
+ };
1220
+ function buildOrmHelpScript() {
1221
+ const lines = [
1222
+ "Prisma database commands:",
1223
+ ...Object.entries(ORM_SCRIPT_DESCRIPTIONS).map(
1224
+ ([script, description]) => ` pnpm ${script.padEnd(18)} ${description}`
1225
+ )
1226
+ ];
1227
+ const source = `console.log(${JSON.stringify(lines.join("\n"))})`;
1228
+ return `node -e ${JSON.stringify(source)}`;
1229
+ }
1230
+ var ORM_SCRIPTS = {
1231
+ "db:help": buildOrmHelpScript(),
1232
+ "db:generate": "prisma generate",
1233
+ "db:validate": "prisma validate",
1234
+ "db:migrate": "prisma migrate dev",
1235
+ "db:migrate:deploy": "prisma migrate deploy",
1236
+ "db:migrate:status": "prisma migrate status",
1237
+ "db:push": "prisma db push",
1238
+ "db:pull": "prisma db pull",
1239
+ "db:studio": "prisma studio"
1240
+ };
1241
+ function buildPostgresDatabaseName(projectName) {
1242
+ const normalized = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
1243
+ return normalized || "app";
1244
+ }
1245
+ function buildDatabaseUrlExample(projectName) {
1246
+ return `postgresql://postgres:postgres@localhost:5432/${buildPostgresDatabaseName(projectName)}`;
1247
+ }
1248
+ function buildOrmEnvFragment(projectName) {
1249
+ return {
1250
+ variables: [
1251
+ {
1252
+ name: "DATABASE_URL",
1253
+ scope: "server",
1254
+ schema: "z.string().url()",
1255
+ example: buildDatabaseUrlExample(projectName)
1256
+ }
1257
+ ]
1258
+ };
1259
+ }
1260
+
1261
+ // src/config/server-actions-fragments.ts
1262
+ var NEXT_SAFE_ACTION_VERSION = "^8.5.2";
1263
+ var NEXT_SAFE_ACTION_BETTER_AUTH_ADAPTER_VERSION = "^0.1.6";
1264
+ var SERVER_ACTIONS_DEPENDENCIES = {
1265
+ "next-safe-action": NEXT_SAFE_ACTION_VERSION
1266
+ };
1267
+ var SERVER_ACTIONS_AUTH_DEPENDENCIES = {
1268
+ "@next-safe-action/adapter-better-auth": NEXT_SAFE_ACTION_BETTER_AUTH_ADAPTER_VERSION
1269
+ };
1270
+
1271
+ // src/config/state-fragments.ts
1272
+ var NUQS_VERSION = "^2.8.9";
1273
+ var ZUSTAND_VERSION = "^5.0.12";
1274
+ var STATE_DEPENDENCIES = {
1275
+ nuqs: NUQS_VERSION,
1276
+ zustand: ZUSTAND_VERSION
1277
+ };
1278
+
1279
+ // src/config/package-deps-baseline.ts
1280
+ var PACKAGE_DEPS_BASELINE = {
1281
+ ...AI_CHAT_DEPENDENCIES,
1282
+ ...AUTH_DEPENDENCIES,
1283
+ ...ENV_MANAGEMENT_DEPENDENCIES,
1284
+ ...FORM_HANDLING_DEPENDENCIES,
1285
+ ...I18N_DEPENDENCIES,
1286
+ ...ORM_DEPENDENCIES,
1287
+ ...SERVER_ACTIONS_DEPENDENCIES,
1288
+ ...STATE_DEPENDENCIES
1289
+ };
1290
+
1291
+ // src/config/eslint-prettier-fragments.ts
1292
+ var PRETTIER_VERSION = "^3.8.3";
1293
+ var ESLINT_CONFIG_PRETTIER_VERSION = "^10.1.8";
1294
+ var PRETTIER_RC_BASELINE = {
1295
+ semi: true,
1296
+ singleQuote: false,
1297
+ trailingComma: "all",
1298
+ printWidth: 80,
1299
+ tabWidth: 2,
1300
+ endOfLine: "lf"
1301
+ };
1302
+ var ESLINT_PRETTIER_DEV_DEPS = {
1303
+ prettier: PRETTIER_VERSION,
1304
+ "eslint-config-prettier": ESLINT_CONFIG_PRETTIER_VERSION
1305
+ };
1306
+ var ESLINT_PRETTIER_SCRIPTS = {
1307
+ lint: "eslint && prettier --check .",
1308
+ format: "prettier --write .",
1309
+ "format:check": "prettier --check ."
1310
+ };
1311
+
1312
+ // src/config/package-dev-deps-baseline.ts
1313
+ var PACKAGE_DEV_DEPS_BASELINE = {
1314
+ ...ESLINT_PRETTIER_DEV_DEPS,
1315
+ ...ORM_DEV_DEPENDENCIES
1316
+ };
1317
+
1318
+ // src/config/package-scripts-baseline.ts
1319
+ var PACKAGE_SCRIPTS_BASELINE = {
1320
+ dev: "next dev",
1321
+ build: "next build",
1322
+ start: "next start",
1323
+ ...ESLINT_PRETTIER_SCRIPTS,
1324
+ ...ORM_SCRIPTS
1325
+ };
1326
+
1327
+ // src/config/pre-commit-fragments.ts
1328
+ import { existsSync as existsSync4 } from "fs";
1329
+ import { dirname as dirname3, join as join5, resolve as resolve5 } from "path";
1330
+ var PRE_COMMIT_COMMON_SCRIPTS = {
1331
+ typecheck: "tsc --noEmit"
1332
+ };
1333
+ var PRE_COMMIT_STANDALONE_SCRIPTS = {
1334
+ prepare: "pnpm dlx @j178/prek install"
1335
+ };
1336
+ var PRE_COMMIT_MONOREPO_SCRIPTS = {
1337
+ "pre-commit": "pnpm dlx @j178/prek run --directory ."
1338
+ };
1339
+ function buildPrekTomlContents() {
1340
+ return renderTemplateFile("pre-commit/prek.toml.ejs", {});
1341
+ }
1342
+ function inferPreCommitMode(targetDir) {
1343
+ let current = dirname3(resolve5(targetDir));
1344
+ while (true) {
1345
+ if (existsSync4(join5(current, ".git"))) return "monorepo";
1346
+ const parent = dirname3(current);
1347
+ if (parent === current) return "standalone";
1348
+ current = parent;
1349
+ }
1350
+ }
1351
+ function buildPreCommitScripts(mode) {
1352
+ return {
1353
+ ...PRE_COMMIT_COMMON_SCRIPTS,
1354
+ ...mode === "standalone" ? PRE_COMMIT_STANDALONE_SCRIPTS : PRE_COMMIT_MONOREPO_SCRIPTS
1355
+ };
1356
+ }
1357
+
1358
+ // src/config/next-config-contents.ts
1359
+ function shouldEnableAuthInterrupts(context) {
1360
+ return context ? context.config.authStrategy !== "none" : false;
1361
+ }
1362
+ function shouldEnableNextIntl(context) {
1363
+ return context ? context.config.i18n === true : false;
1364
+ }
1365
+ function buildNextConfigTsContents(context) {
1366
+ return renderTemplateFile("core/next.config.ts.ejs", {
1367
+ authInterrupts: shouldEnableAuthInterrupts(context),
1368
+ nextIntl: shouldEnableNextIntl(context)
1369
+ });
1370
+ }
1371
+
1372
+ // src/config/tsconfig-managed-baseline.ts
1373
+ var BASE_EXCLUDE = [
1374
+ "node_modules",
1375
+ // shadcn/ui registry files are generated vendor code. Imported components
1376
+ // are still checked through their app imports.
1377
+ "src/components/ui/**/*.tsx",
1378
+ "src/hooks/use-mobile.ts"
1379
+ ];
1380
+ function buildTsconfigManagedBaseline(config) {
1381
+ const exclude = [
1382
+ ...BASE_EXCLUDE,
1383
+ ...config.aiChat ? ["src/components/ai-elements/**/*.tsx"] : []
1384
+ ];
1385
+ return {
1386
+ ...TSCONFIG_MANAGED_BASELINE,
1387
+ exclude
1388
+ };
1389
+ }
1390
+ var TSCONFIG_MANAGED_BASELINE = {
1391
+ compilerOptions: {
1392
+ target: "ES2017",
1393
+ lib: ["dom", "dom.iterable", "esnext"],
1394
+ allowJs: true,
1395
+ skipLibCheck: true,
1396
+ strict: true,
1397
+ noEmit: true,
1398
+ esModuleInterop: true,
1399
+ module: "esnext",
1400
+ moduleResolution: "bundler",
1401
+ resolveJsonModule: true,
1402
+ isolatedModules: true,
1403
+ jsx: "react-jsx",
1404
+ incremental: true,
1405
+ plugins: [{ name: "next" }],
1406
+ // "Super Duper" Strictness Additions
1407
+ noUncheckedIndexedAccess: true,
1408
+ exactOptionalPropertyTypes: true,
1409
+ noPropertyAccessFromIndexSignature: true,
1410
+ noImplicitOverride: true,
1411
+ // Code Cleanliness
1412
+ noUnusedLocals: true,
1413
+ noUnusedParameters: true,
1414
+ noImplicitReturns: true,
1415
+ noFallthroughCasesInSwitch: true,
1416
+ // Modern Environment
1417
+ forceConsistentCasingInFileNames: true,
1418
+ verbatimModuleSyntax: true
1419
+ },
1420
+ include: [
1421
+ "next-env.d.ts",
1422
+ "**/*.ts",
1423
+ "**/*.tsx",
1424
+ ".next/types/**/*.ts",
1425
+ ".next/dev/types/**/*.ts",
1426
+ "**/*.mts"
1427
+ ],
1428
+ exclude: BASE_EXCLUDE
1429
+ };
1430
+
1431
+ // src/sync/targets.ts
1432
+ function inferOrmProvider(context) {
1433
+ if (!context) return "none";
1434
+ return existsSync5(join6(context.projectRoot, "prisma/schema.prisma")) ? "prisma" : "none";
1435
+ }
1436
+ function inferAuthStrategy(context) {
1437
+ if (!context) return "none";
1438
+ return existsSync5(join6(context.projectRoot, "src/lib/auth.ts")) ? "stateless" : "none";
1439
+ }
1440
+ function inferAiChat(context) {
1441
+ if (!context) return false;
1442
+ return existsSync5(join6(context.projectRoot, "src/app/chat/_hooks/useChat.ts")) || existsSync5(join6(context.projectRoot, "src/app/api/chat/route.ts"));
1443
+ }
1444
+ function inferI18n(context) {
1445
+ if (!context) return false;
1446
+ return existsSync5(join6(context.projectRoot, "src/i18n/routing.ts")) || existsSync5(join6(context.projectRoot, "messages/en.json"));
1447
+ }
1448
+ function inferScaleStackConfig(context) {
1449
+ return {
1450
+ projectName: context ? "synced-project" : "project",
1451
+ orm: inferOrmProvider(context),
1452
+ authStrategy: inferAuthStrategy(context),
1453
+ aiChat: inferAiChat(context),
1454
+ i18n: inferI18n(context),
1455
+ ci: "none",
1456
+ analytics: "none",
1457
+ jobs: false,
1458
+ a11y: false,
1459
+ apiClient: false,
1460
+ preCommit: context ? hasPrekConfig(context) : false
1461
+ };
1462
+ }
1463
+ function hasPrekConfig(context) {
1464
+ return existsSync5(join6(context.projectRoot, "prek.toml"));
1465
+ }
1466
+ var SYNC_TARGETS = [
1467
+ {
1468
+ relativePath: "tsconfig.json",
1469
+ kind: "json",
1470
+ getDesiredFragment: (context) => structuredClone(
1471
+ buildTsconfigManagedBaseline(inferScaleStackConfig(context))
1472
+ )
1473
+ },
1474
+ {
1475
+ relativePath: "package.json",
1476
+ kind: "json",
1477
+ getDesiredFragment: (context) => ({
1478
+ scripts: {
1479
+ ...structuredClone(PACKAGE_SCRIPTS_BASELINE),
1480
+ ...context && hasPrekConfig(context) ? buildPreCommitScripts(inferPreCommitMode(context.projectRoot)) : {}
1481
+ },
1482
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE),
1483
+ devDependencies: structuredClone(PACKAGE_DEV_DEPS_BASELINE)
1484
+ })
1485
+ },
1486
+ {
1487
+ relativePath: "next.config.ts",
1488
+ kind: "text",
1489
+ getDesiredContent: (context) => buildNextConfigTsContents({
1490
+ config: {
1491
+ authStrategy: inferAuthStrategy(context),
1492
+ i18n: inferI18n(context)
1493
+ }
1494
+ })
1495
+ },
1496
+ {
1497
+ relativePath: "eslint.config.mjs",
1498
+ kind: "text",
1499
+ getDesiredContent: (context) => buildEslintConfigMjsContents(inferScaleStackConfig(context))
1500
+ },
1501
+ {
1502
+ relativePath: ".prettierrc",
1503
+ kind: "json",
1504
+ getDesiredFragment: () => structuredClone(PRETTIER_RC_BASELINE)
1505
+ },
1506
+ {
1507
+ relativePath: "prek.toml",
1508
+ kind: "text",
1509
+ condition: hasPrekConfig,
1510
+ getDesiredContent: () => buildPrekTomlContents()
1511
+ }
1512
+ ];
1513
+
1514
+ // src/sync/last-sync.ts
1515
+ import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1516
+ import { existsSync as existsSync6 } from "fs";
1517
+ import { dirname as dirname4, join as join7 } from "path";
1518
+
1519
+ // src/sync/constants.ts
1520
+ var MIGRATION_GUIDE_URL = "https://github.com/scaleapi/scale-stack/blob/main/IMPLEMENTATION_PLAN.md#phase-5--sync-command-early-validation";
1521
+ var SCALE_STACK_DIR = ".scale-stack";
1522
+ var LAST_SYNC_FILE = ".scale-stack/last-sync.json";
1523
+ var SYNC_LOG_FILE = ".scale-stack/sync.log";
1524
+ var PRE_SYNC_BACKUP_DIR = ".scale-stack/pre-sync-backup";
1525
+
1526
+ // src/sync/last-sync.ts
1527
+ import { cloneDeep } from "es-toolkit/compat";
1528
+ function isTextMarker(v) {
1529
+ return typeof v === "object" && v !== null && "__content" in v && typeof v.__content === "string";
1530
+ }
1531
+ async function readLastSync(projectRoot) {
1532
+ const abs = join7(projectRoot, LAST_SYNC_FILE);
1533
+ if (!existsSync6(abs)) return null;
1534
+ try {
1535
+ const raw = await readFile2(abs, "utf-8");
1536
+ const data = JSON.parse(raw);
1537
+ if (!data || typeof data.cliVersion !== "string" || !data.files) {
1538
+ return null;
1539
+ }
1540
+ return data;
1541
+ } catch {
1542
+ return null;
1543
+ }
1544
+ }
1545
+ async function writeLastSync(projectRoot, data) {
1546
+ const abs = join7(projectRoot, LAST_SYNC_FILE);
1547
+ await mkdir3(dirname4(abs), { recursive: true });
1548
+ const body = `${JSON.stringify(data, null, 2)}
1549
+ `;
1550
+ await writeFile3(abs, body, "utf-8");
1551
+ }
1552
+ function getJsonFragment(last, relativePath) {
1553
+ const entry = last?.files?.[relativePath];
1554
+ if (!entry || isTextMarker(entry)) return void 0;
1555
+ return cloneDeep(entry);
1556
+ }
1557
+ function getTextContent(last, relativePath) {
1558
+ const entry = last?.files?.[relativePath];
1559
+ if (!entry || !isTextMarker(entry)) return void 0;
1560
+ return entry.__content;
1561
+ }
1562
+
1563
+ // src/sync/version-guard.ts
1564
+ import semver from "semver";
1565
+ var SyncVersionAbortError = class extends Error {
1566
+ constructor(message, recorded, current) {
1567
+ super(message);
1568
+ this.recorded = recorded;
1569
+ this.current = current;
1570
+ this.name = "SyncVersionAbortError";
1571
+ }
1572
+ recorded;
1573
+ current;
1574
+ code = "SYNC_VERSION_ABORT";
1575
+ };
1576
+ function assertSyncVersionCompatible(recordedVersion, currentVersion) {
1577
+ if (!recordedVersion?.trim()) return;
1578
+ const rec = semver.coerce(recordedVersion);
1579
+ const cur = semver.coerce(currentVersion);
1580
+ if (!rec || !cur) return;
1581
+ if (cur.major > rec.major + 1) {
1582
+ throw new SyncVersionAbortError(
1583
+ `Scale Stack CLI (${currentVersion}) is more than one major version ahead of the project baseline (${recordedVersion}). See the migration guide: ${MIGRATION_GUIDE_URL}`,
1584
+ recordedVersion,
1585
+ currentVersion
1586
+ );
1587
+ }
1588
+ }
1589
+
1590
+ // src/sync/internal/log-entry.ts
1591
+ import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1592
+ import { join as join8 } from "path";
1593
+ function nowIso() {
1594
+ return (/* @__PURE__ */ new Date()).toISOString();
1595
+ }
1596
+ async function appendSyncLog(projectRoot, entries) {
1597
+ if (entries.length === 0) return;
1598
+ const dir = join8(projectRoot, SCALE_STACK_DIR);
1599
+ await mkdir4(dir, { recursive: true });
1600
+ const logPath = join8(projectRoot, SYNC_LOG_FILE);
1601
+ const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
1602
+ await writeFile4(logPath, lines, { flag: "a", encoding: "utf-8" });
1603
+ }
1604
+
1605
+ // src/sync/internal/backup.ts
1606
+ import { cp, mkdir as mkdir5, readdir as readdir2, rm as rm2 } from "fs/promises";
1607
+ import { existsSync as existsSync7 } from "fs";
1608
+ import { join as join9 } from "path";
1609
+ async function snapshotFile(projectRoot, backupDir, relativePath) {
1610
+ const src = join9(projectRoot, relativePath);
1611
+ if (!existsSync7(src)) return;
1612
+ const dest = join9(backupDir, relativePath);
1613
+ await mkdir5(join9(dest, ".."), { recursive: true });
1614
+ await cp(src, dest, { force: true });
1615
+ }
1616
+ async function removeDirIfExists(abs) {
1617
+ if (existsSync7(abs)) await rm2(abs, { recursive: true, force: true });
1618
+ }
1619
+ async function snapshotBackup(projectRoot, plannedPaths) {
1620
+ if (plannedPaths.length === 0) return void 0;
1621
+ const backupRoot = join9(projectRoot, PRE_SYNC_BACKUP_DIR);
1622
+ const sessionBackup = join9(backupRoot, `backup-${Date.now()}`);
1623
+ await mkdir5(sessionBackup, { recursive: true });
1624
+ const newlyCreated = /* @__PURE__ */ new Set();
1625
+ for (const rel of plannedPaths) {
1626
+ const abs = join9(projectRoot, rel);
1627
+ if (existsSync7(abs)) {
1628
+ await snapshotFile(projectRoot, sessionBackup, rel);
1629
+ } else {
1630
+ newlyCreated.add(rel);
1631
+ }
1632
+ }
1633
+ return { sessionBackup, newlyCreated };
1634
+ }
1635
+ async function cleanupBackup(projectRoot, session) {
1636
+ await removeDirIfExists(session.sessionBackup);
1637
+ const backupRoot = join9(projectRoot, PRE_SYNC_BACKUP_DIR);
1638
+ try {
1639
+ const entries = await readdir2(backupRoot);
1640
+ if (entries.length === 0) await removeDirIfExists(backupRoot);
1641
+ } catch {
1642
+ }
1643
+ }
1644
+ async function restoreFromBackup(projectRoot, session, plannedPaths) {
1645
+ for (const rel of plannedPaths) {
1646
+ const abs = join9(projectRoot, rel);
1647
+ if (session?.newlyCreated.has(rel)) {
1648
+ if (existsSync7(abs)) await rm2(abs, { force: true });
1649
+ continue;
1650
+ }
1651
+ if (session) {
1652
+ const snap = join9(session.sessionBackup, rel);
1653
+ if (existsSync7(snap)) {
1654
+ await cp(snap, abs, { force: true });
1655
+ }
1656
+ }
1657
+ }
1658
+ }
1659
+
1660
+ // src/sync/internal/apply-json-target.ts
1661
+ import { readFile as readFile3 } from "fs/promises";
1662
+ import { existsSync as existsSync8 } from "fs";
1663
+ import { join as join10 } from "path";
1664
+ import { isEqual as isEqual2 } from "es-toolkit/predicate";
1665
+
1666
+ // src/lib/merge/managed-json.ts
1667
+ import { isEqual } from "es-toolkit/predicate";
1668
+
1669
+ // src/lib/merge/json-path-utils.ts
1670
+ import { cloneDeep as cloneDeep2, get, set } from "es-toolkit/compat";
1671
+ function hasDottedPath(root, dotted) {
1672
+ const parts = dotted.split(".");
1673
+ let cur = root;
1674
+ for (const p2 of parts) {
1675
+ if (cur === null || typeof cur !== "object" || Array.isArray(cur))
1676
+ return false;
1677
+ if (!Object.prototype.hasOwnProperty.call(cur, p2)) return false;
1678
+ cur = cur[p2];
1679
+ }
1680
+ return true;
1681
+ }
1682
+ function getAt(obj, path) {
1683
+ if (path === "") return obj;
1684
+ return get(obj, path);
1685
+ }
1686
+ function setAt(root, path, value) {
1687
+ if (path === "") {
1688
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1689
+ return { ...value };
1690
+ }
1691
+ throw new Error(`setAt: cannot replace root with non-object at empty path`);
1692
+ }
1693
+ const draft = cloneDeep2(root);
1694
+ set(draft, path, value);
1695
+ return draft;
1696
+ }
1697
+ function deepCloneJson(v) {
1698
+ return cloneDeep2(v);
1699
+ }
1700
+
1701
+ // src/lib/merge/json-shape.ts
1702
+ function listLeafPathsFromBaseline(baseline, options) {
1703
+ const exclude = new Set(options?.excludeExactPaths ?? []);
1704
+ return leafPaths(baseline, "", exclude);
1705
+ }
1706
+ function leafPaths(v, prefix, exclude) {
1707
+ if (v === null || typeof v !== "object") {
1708
+ return prefix ? [prefix] : [];
1709
+ }
1710
+ if (Array.isArray(v)) {
1711
+ return prefix ? [prefix] : [];
1712
+ }
1713
+ const o = v;
1714
+ const keys = Object.keys(o);
1715
+ if (keys.length === 0) return prefix ? [prefix] : [];
1716
+ const out = [];
1717
+ for (const k of keys) {
1718
+ const p2 = prefix ? `${prefix}.${k}` : k;
1719
+ if (exclude.has(p2)) {
1720
+ continue;
1721
+ }
1722
+ const child = o[k];
1723
+ if (child !== null && typeof child === "object" && !Array.isArray(child)) {
1724
+ const nested = leafPaths(child, p2, exclude);
1725
+ out.push(...nested);
1726
+ } else {
1727
+ out.push(p2);
1728
+ }
1729
+ }
1730
+ return out;
1731
+ }
1732
+ function extractManagedFragment(disk, baseline) {
1733
+ const out = {};
1734
+ for (const k of Object.keys(baseline)) {
1735
+ const b = baseline[k];
1736
+ const d = disk[k];
1737
+ if (b === null || typeof b !== "object" || Array.isArray(b)) {
1738
+ out[k] = d !== void 0 ? deepCloneLeaf(d) : b;
1739
+ } else if (typeof d === "object" && d !== null && !Array.isArray(d)) {
1740
+ out[k] = extractManagedFragment(
1741
+ d,
1742
+ b
1743
+ );
1744
+ } else {
1745
+ out[k] = d !== void 0 ? deepCloneLeaf(d) : deepCloneLeaf(b);
1746
+ }
1747
+ }
1748
+ return out;
1749
+ }
1750
+ function deepCloneLeaf(v) {
1751
+ if (v === null || typeof v !== "object") return v;
1752
+ return JSON.parse(JSON.stringify(v));
1753
+ }
1754
+
1755
+ // src/lib/merge/managed-json.ts
1756
+ function mergeManagedJson(options) {
1757
+ const {
1758
+ fileLabel,
1759
+ disk,
1760
+ desiredFragment,
1761
+ lastFileFragment,
1762
+ hasGlobalLastSync,
1763
+ force,
1764
+ managedPaths
1765
+ } = options;
1766
+ const conflicts = [];
1767
+ const skippedPaths = [];
1768
+ const appliedPaths = [];
1769
+ let merged = deepCloneJson(disk);
1770
+ for (const path of managedPaths) {
1771
+ const desiredVal = getAt(desiredFragment, path);
1772
+ const diskVal = getAt(disk, path);
1773
+ let hasConflict = false;
1774
+ if (hasGlobalLastSync && lastFileFragment !== void 0 && hasDottedPath(lastFileFragment, path)) {
1775
+ const prevVal = getAt(lastFileFragment, path);
1776
+ if (!isEqual(diskVal, prevVal)) {
1777
+ hasConflict = true;
1778
+ }
1779
+ }
1780
+ if (hasConflict && !force) {
1781
+ conflicts.push({ relativePath: fileLabel, path });
1782
+ skippedPaths.push(path);
1783
+ continue;
1784
+ }
1785
+ if (hasConflict && force) {
1786
+ merged = setAt(merged, path, desiredVal);
1787
+ appliedPaths.push(path);
1788
+ continue;
1789
+ }
1790
+ if (!isEqual(diskVal, desiredVal)) {
1791
+ merged = setAt(merged, path, desiredVal);
1792
+ appliedPaths.push(path);
1793
+ }
1794
+ }
1795
+ return { merged, conflicts, appliedPaths, skippedPaths };
1796
+ }
1797
+ function mergeJsonFile(options) {
1798
+ const {
1799
+ relativePath,
1800
+ disk,
1801
+ desiredFragment,
1802
+ lastFileFragment,
1803
+ hasGlobalLastSync,
1804
+ force
1805
+ } = options;
1806
+ const managedPaths = listLeafPathsFromBaseline(desiredFragment);
1807
+ return mergeManagedJson({
1808
+ fileLabel: relativePath,
1809
+ disk,
1810
+ desiredFragment,
1811
+ lastFileFragment,
1812
+ hasGlobalLastSync,
1813
+ force,
1814
+ managedPaths
1815
+ });
1816
+ }
1817
+ function mergeManagedJsonString(options) {
1818
+ const { relativePath, existingContent, desiredFragment } = options;
1819
+ const disk = JSON.parse(existingContent);
1820
+ const { merged } = mergeJsonFile({
1821
+ relativePath,
1822
+ disk,
1823
+ desiredFragment,
1824
+ lastFileFragment: void 0,
1825
+ hasGlobalLastSync: false,
1826
+ force: false
1827
+ });
1828
+ return merged;
1829
+ }
1830
+ function formatMergedJson(merged) {
1831
+ return `${JSON.stringify(merged, null, 2)}
1832
+ `;
1833
+ }
1834
+
1835
+ // src/sync/internal/apply-json-target.ts
1836
+ async function applyJsonTarget(options) {
1837
+ const { projectRoot, target, lastSync, hasGlobalLastSync, force } = options;
1838
+ const rel = target.relativePath;
1839
+ const abs = join10(projectRoot, rel);
1840
+ const logEntries = [];
1841
+ let diskObj;
1842
+ if (!existsSync8(abs)) {
1843
+ diskObj = {};
1844
+ } else {
1845
+ try {
1846
+ diskObj = JSON.parse(await readFile3(abs, "utf-8"));
1847
+ } catch {
1848
+ const err = new Error(`Invalid JSON: ${rel}`);
1849
+ logEntries.push({
1850
+ timestamp: nowIso(),
1851
+ level: "error",
1852
+ message: err.message,
1853
+ file: rel
1854
+ });
1855
+ return { ok: false, error: err, logEntries };
1856
+ }
1857
+ }
1858
+ const desired = target.getDesiredFragment({ projectRoot });
1859
+ const lastFileFragment = getJsonFragment(lastSync, rel);
1860
+ const outcome = mergeJsonFile({
1861
+ relativePath: rel,
1862
+ disk: diskObj,
1863
+ desiredFragment: desired,
1864
+ lastFileFragment,
1865
+ hasGlobalLastSync,
1866
+ force
1867
+ });
1868
+ for (const c of outcome.conflicts) {
1869
+ logEntries.push({
1870
+ timestamp: nowIso(),
1871
+ level: "skip",
1872
+ message: "Conflict on managed key \u2014 skipped (use --force to overwrite)",
1873
+ file: c.relativePath,
1874
+ path: c.path
1875
+ });
1876
+ }
1877
+ for (const p2 of outcome.appliedPaths) {
1878
+ logEntries.push({
1879
+ timestamp: nowIso(),
1880
+ level: "info",
1881
+ message: "Applied managed key",
1882
+ file: rel,
1883
+ path: p2
1884
+ });
1885
+ }
1886
+ const changed = !isEqual2(outcome.merged, diskObj);
1887
+ let nextFragment = extractManagedFragment(outcome.merged, desired);
1888
+ if (!force && lastFileFragment !== void 0 && outcome.skippedPaths.length > 0) {
1889
+ for (const path of outcome.skippedPaths) {
1890
+ const prev = getAt(lastFileFragment, path);
1891
+ nextFragment = setAt(nextFragment, path, prev);
1892
+ }
1893
+ }
1894
+ let conflictForcedContent;
1895
+ if (!force && outcome.conflicts.length > 0) {
1896
+ const forced = mergeJsonFile({
1897
+ relativePath: rel,
1898
+ disk: diskObj,
1899
+ desiredFragment: desired,
1900
+ lastFileFragment,
1901
+ hasGlobalLastSync,
1902
+ force: true
1903
+ });
1904
+ if (!isEqual2(forced.merged, diskObj)) {
1905
+ conflictForcedContent = formatMergedJson(forced.merged);
1906
+ }
1907
+ }
1908
+ return {
1909
+ ok: true,
1910
+ plannedContent: changed ? formatMergedJson(outcome.merged) : void 0,
1911
+ conflictForcedContent,
1912
+ nextFragment,
1913
+ logEntries
1914
+ };
1915
+ }
1916
+
1917
+ // src/sync/internal/apply-text-target.ts
1918
+ import { readFile as readFile4 } from "fs/promises";
1919
+ import { existsSync as existsSync9 } from "fs";
1920
+ import { join as join11 } from "path";
1921
+
1922
+ // src/lib/merge/text.ts
1923
+ function mergeTextFile(options) {
1924
+ const { disk, desired, lastContent, hasGlobalLastSync, force } = options;
1925
+ let conflict = false;
1926
+ if (hasGlobalLastSync && lastContent !== void 0 && disk !== lastContent) {
1927
+ conflict = true;
1928
+ }
1929
+ if (conflict && !force) {
1930
+ return { merged: disk, conflict: true, applied: false };
1931
+ }
1932
+ if (disk === desired) {
1933
+ return { merged: disk, conflict: false, applied: false };
1934
+ }
1935
+ return { merged: desired, conflict, applied: true };
1936
+ }
1937
+
1938
+ // src/sync/internal/apply-text-target.ts
1939
+ async function applyTextTarget(options) {
1940
+ const { projectRoot, target, lastSync, hasGlobalLastSync, force } = options;
1941
+ const rel = target.relativePath;
1942
+ const abs = join11(projectRoot, rel);
1943
+ const logEntries = [];
1944
+ const desired = await target.getDesiredContent({ projectRoot });
1945
+ const diskText = existsSync9(abs) ? await readFile4(abs, "utf-8") : "";
1946
+ const lastContent = getTextContent(lastSync, rel);
1947
+ const out = mergeTextFile({
1948
+ disk: diskText,
1949
+ desired,
1950
+ lastContent,
1951
+ hasGlobalLastSync,
1952
+ force
1953
+ });
1954
+ if (out.conflict && !force) {
1955
+ logEntries.push({
1956
+ timestamp: nowIso(),
1957
+ level: "skip",
1958
+ message: "Conflict on managed file \u2014 skipped (use --force to overwrite)",
1959
+ file: rel
1960
+ });
1961
+ } else if (out.applied) {
1962
+ logEntries.push({
1963
+ timestamp: nowIso(),
1964
+ level: "info",
1965
+ message: "Applied managed file content",
1966
+ file: rel
1967
+ });
1968
+ }
1969
+ const nextContent = out.conflict && !force && lastContent !== void 0 ? lastContent : out.merged;
1970
+ const conflictForcedContent = out.conflict && !force && desired !== diskText ? desired : void 0;
1971
+ return {
1972
+ plannedContent: out.merged !== diskText ? out.merged : void 0,
1973
+ conflictForcedContent,
1974
+ nextMarker: { __content: nextContent },
1975
+ logEntries
1976
+ };
1977
+ }
1978
+
1979
+ // src/sync/run-sync.ts
1980
+ var execAsync2 = promisify2(exec2);
1981
+ async function runSync(opts) {
1982
+ const { projectRoot, dryRun, force, keepBackup } = opts;
1983
+ const logEntries = [];
1984
+ const guardResult = await guardProjectRoot(projectRoot);
1985
+ if (!guardResult.ok) {
1986
+ logEntries.push({
1987
+ timestamp: nowIso(),
1988
+ level: "error",
1989
+ message: guardResult.error.message
1990
+ });
1991
+ return { ok: false, error: guardResult.error, logEntries };
1992
+ }
1993
+ const currentCli = getCliVersion();
1994
+ const lastSync = await readLastSync(projectRoot);
1995
+ try {
1996
+ assertSyncVersionCompatible(lastSync?.cliVersion, currentCli);
1997
+ } catch (e) {
1998
+ if (e instanceof SyncVersionAbortError) {
1999
+ logEntries.push({
2000
+ timestamp: nowIso(),
2001
+ level: "error",
2002
+ message: e.message
2003
+ });
2004
+ return { ok: false, error: e, logEntries };
2005
+ }
2006
+ throw e;
2007
+ }
2008
+ const hasGlobalLastSync = lastSync !== null;
2009
+ const nextLast = {
2010
+ cliVersion: currentCli,
2011
+ files: { ...lastSync?.files ?? {} }
2012
+ };
2013
+ const plannedWrites = /* @__PURE__ */ new Map();
2014
+ const conflictForcedWrites = /* @__PURE__ */ new Map();
2015
+ const changedFiles = [];
2016
+ for (const target of SYNC_TARGETS) {
2017
+ const rel = target.relativePath;
2018
+ const context = { projectRoot };
2019
+ if (target.condition && !target.condition(context)) {
2020
+ continue;
2021
+ }
2022
+ if (target.kind === "json") {
2023
+ const outcome2 = await applyJsonTarget({
2024
+ projectRoot,
2025
+ target,
2026
+ lastSync,
2027
+ hasGlobalLastSync,
2028
+ force
2029
+ });
2030
+ logEntries.push(...outcome2.logEntries);
2031
+ if (!outcome2.ok) {
2032
+ return { ok: false, error: outcome2.error, logEntries };
2033
+ }
2034
+ if (outcome2.plannedContent !== void 0) {
2035
+ plannedWrites.set(rel, outcome2.plannedContent);
2036
+ changedFiles.push(rel);
2037
+ }
2038
+ if (outcome2.conflictForcedContent !== void 0) {
2039
+ conflictForcedWrites.set(rel, outcome2.conflictForcedContent);
2040
+ }
2041
+ nextLast.files[rel] = outcome2.nextFragment;
2042
+ continue;
2043
+ }
2044
+ const outcome = await applyTextTarget({
2045
+ projectRoot,
2046
+ target,
2047
+ lastSync,
2048
+ hasGlobalLastSync,
2049
+ force
2050
+ });
2051
+ logEntries.push(...outcome.logEntries);
2052
+ if (outcome.plannedContent !== void 0) {
2053
+ plannedWrites.set(rel, outcome.plannedContent);
2054
+ changedFiles.push(rel);
2055
+ }
2056
+ if (outcome.conflictForcedContent !== void 0) {
2057
+ conflictForcedWrites.set(rel, outcome.conflictForcedContent);
2058
+ }
2059
+ nextLast.files[rel] = outcome.nextMarker;
2060
+ }
2061
+ const conflictDiffs = await buildConflictDiffs(
2062
+ projectRoot,
2063
+ conflictForcedWrites
2064
+ );
2065
+ const agentPrompt = buildAgentPrompt(conflictDiffs);
2066
+ if (dryRun) {
2067
+ await emitDryRunDiffs(projectRoot, plannedWrites);
2068
+ logEntries.push({
2069
+ timestamp: nowIso(),
2070
+ level: "info",
2071
+ message: `Dry-run: ${plannedWrites.size} file(s) would change`
2072
+ });
2073
+ return {
2074
+ ok: true,
2075
+ dryRun: true,
2076
+ changedFiles: [...changedFiles],
2077
+ conflictDiffs,
2078
+ agentPrompt,
2079
+ logEntries
2080
+ };
2081
+ }
2082
+ const rollbackPaths = [.../* @__PURE__ */ new Set([...plannedWrites.keys(), LAST_SYNC_FILE])];
2083
+ const backup = await snapshotBackup(projectRoot, rollbackPaths);
2084
+ try {
2085
+ await commitPlannedWrites(projectRoot, plannedWrites);
2086
+ await writeLastSync(projectRoot, nextLast);
2087
+ await appendSyncLog(projectRoot, logEntries);
2088
+ if (backup && !keepBackup) {
2089
+ await cleanupBackup(projectRoot, backup);
2090
+ }
2091
+ const { skillsStdout, skillsError } = await runSkillsUpdate({
2092
+ projectRoot,
2093
+ enabled: plannedWrites.size > 0
2094
+ });
2095
+ if (skillsError) {
2096
+ await appendSyncLog(projectRoot, [
2097
+ {
2098
+ timestamp: nowIso(),
2099
+ level: "warn",
2100
+ message: skillsError.message
2101
+ }
2102
+ ]);
2103
+ }
2104
+ return {
2105
+ ok: true,
2106
+ dryRun: false,
2107
+ changedFiles: [...changedFiles],
2108
+ conflictDiffs,
2109
+ agentPrompt,
2110
+ logEntries,
2111
+ skillsStdout,
2112
+ ...skillsError && { skillsError }
2113
+ };
2114
+ } catch (e) {
2115
+ const err = e instanceof Error ? e : new Error(String(e));
2116
+ await restoreFromBackup(projectRoot, backup, rollbackPaths);
2117
+ logEntries.push({
2118
+ timestamp: nowIso(),
2119
+ level: "error",
2120
+ message: err.message
2121
+ });
2122
+ return { ok: false, error: err, logEntries };
2123
+ }
2124
+ }
2125
+ async function guardProjectRoot(projectRoot) {
2126
+ const pkgPath = join12(projectRoot, "package.json");
2127
+ if (!existsSync10(pkgPath)) {
2128
+ return {
2129
+ ok: false,
2130
+ error: new Error(
2131
+ `No package.json in ${projectRoot}. Run scale-stack sync from a project root.`
2132
+ )
2133
+ };
2134
+ }
2135
+ try {
2136
+ const pkgRaw = await readFile5(pkgPath, "utf-8");
2137
+ const pkg = JSON.parse(pkgRaw);
2138
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2139
+ if (!deps.next) {
2140
+ return {
2141
+ ok: false,
2142
+ error: new Error(
2143
+ "scale-stack sync applies to Next.js app projects (missing next dependency)."
2144
+ )
2145
+ };
2146
+ }
2147
+ return { ok: true };
2148
+ } catch (e) {
2149
+ return {
2150
+ ok: false,
2151
+ error: e instanceof Error ? e : new Error(`Invalid package.json: ${String(e)}`)
2152
+ };
2153
+ }
2154
+ }
2155
+ async function emitDryRunDiffs(projectRoot, plannedWrites) {
2156
+ for (const [rel, content] of plannedWrites) {
2157
+ const abs = join12(projectRoot, rel);
2158
+ const before = existsSync10(abs) ? await readFile5(abs, "utf-8") : "";
2159
+ const patch = createPatch2(rel, before, content, "", "");
2160
+ console.log(patch);
2161
+ }
2162
+ }
2163
+ async function buildConflictDiffs(projectRoot, conflictForcedWrites) {
2164
+ const out = [];
2165
+ for (const [rel, content] of conflictForcedWrites) {
2166
+ const abs = join12(projectRoot, rel);
2167
+ const before = existsSync10(abs) ? await readFile5(abs, "utf-8") : "";
2168
+ const diff = createPatch2(rel, before, content, "current", "canonical");
2169
+ out.push({ file: rel, diff });
2170
+ }
2171
+ return out;
2172
+ }
2173
+ function buildAgentPrompt(conflictDiffs) {
2174
+ if (conflictDiffs.length === 0) return void 0;
2175
+ const files = conflictDiffs.map((d) => d.file);
2176
+ const fileList = files.map((f) => ` - ${f}`).join("\n");
2177
+ const diffBlocks = conflictDiffs.map(
2178
+ ({ file, diff }) => [
2179
+ `--- BEGIN diff: ${file} ---`,
2180
+ diff.replace(/\n+$/, ""),
2181
+ `--- END diff: ${file} ---`
2182
+ ].join("\n")
2183
+ ).join("\n\n");
2184
+ return [
2185
+ "You are resolving sync conflicts reported by `scale-stack sync`.",
2186
+ "",
2187
+ "Each conflicted file is included below as a unified diff wrapped in",
2188
+ "`--- BEGIN diff: <file> --- ... --- END diff: <file> ---`. Within each block:",
2189
+ " * `-` lines are the user's current content on disk.",
2190
+ " * `+` lines are the Scale Stack canonical (what `--force` would write).",
2191
+ "",
2192
+ diffBlocks,
2193
+ "",
2194
+ "For every listed file, produce a merged version that:",
2195
+ " 1. preserves the user's intentional edits that don't contradict the canonical intent,",
2196
+ " 2. incorporates every canonical change (the `+` side),",
2197
+ " 3. is written back to disk at its given relative path.",
2198
+ "",
2199
+ "Conflicted files (relative to project root):",
2200
+ fileList,
2201
+ "",
2202
+ "Do NOT run `scale-stack sync --force` \u2014 it would discard the user's edits.",
2203
+ 'After merging, run `scale-stack sync` to verify: it must report "Already up to date".'
2204
+ ].join("\n");
2205
+ }
2206
+ async function commitPlannedWrites(projectRoot, plannedWrites) {
2207
+ for (const [rel, content] of plannedWrites) {
2208
+ const abs = join12(projectRoot, rel);
2209
+ await mkdir6(join12(abs, ".."), { recursive: true });
2210
+ await writeFile5(abs, content, "utf-8");
2211
+ }
2212
+ }
2213
+ async function runSkillsUpdate(options) {
2214
+ if (!options.enabled) return {};
2215
+ if (process.env.SCALE_STACK_SKIP_SKILLS_UPDATE === "1") return {};
2216
+ try {
2217
+ const { stdout, stderr } = await execAsync2("pnpm dlx skills update", {
2218
+ cwd: options.projectRoot,
2219
+ maxBuffer: 20 * 1024 * 1024,
2220
+ env: { ...process.env }
2221
+ });
2222
+ return { skillsStdout: (stdout || "") + (stderr || "") };
2223
+ } catch (e) {
2224
+ const msg = e instanceof Error ? e.message : `skills update failed: ${String(e)}`;
2225
+ return { skillsError: new Error(`pnpm dlx skills update failed: ${msg}`) };
2226
+ }
2227
+ }
2228
+
2229
+ // src/cli/commands/sync.ts
2230
+ function groupSkipsByFile(entries) {
2231
+ const byFile = /* @__PURE__ */ new Map();
2232
+ for (const e of entries) {
2233
+ if (e.level !== "skip" || !e.file) continue;
2234
+ const existing = byFile.get(e.file) ?? [];
2235
+ if (e.path) existing.push(e.path);
2236
+ byFile.set(e.file, existing);
2237
+ }
2238
+ return byFile;
2239
+ }
2240
+ function renderSkippedConflicts(skips) {
2241
+ const fileCount = skips.size;
2242
+ if (fileCount === 0) return;
2243
+ console.log(
2244
+ pc6.yellow(` \u26A0 ${fileCount} managed file(s) skipped due to local changes:`)
2245
+ );
2246
+ for (const [file, paths] of skips) {
2247
+ const detail = paths.length > 0 ? ` (${paths.join(", ")})` : "";
2248
+ console.log(pc6.yellow(` - ${file}${detail}`));
2249
+ }
2250
+ console.log(
2251
+ ikea.muted(
2252
+ ` Run \`scale-stack sync --force\` to overwrite with Scale Stack baselines.`
2253
+ )
2254
+ );
2255
+ }
2256
+ function colorizeDiffLine(line) {
2257
+ if (line.startsWith("+++") || line.startsWith("---")) return ikea.muted(line);
2258
+ if (line.startsWith("@@")) return pc6.cyan(line);
2259
+ if (line.startsWith("+")) return pc6.green(line);
2260
+ if (line.startsWith("-")) return pc6.red(line);
2261
+ return line;
2262
+ }
2263
+ function renderConflictDiffs(diffs) {
2264
+ if (diffs.length === 0) return;
2265
+ console.log();
2266
+ console.log(ikea.muted(" Proposed merge (what --force would write):"));
2267
+ for (const { file, diff } of diffs) {
2268
+ console.log();
2269
+ console.log(pc6.cyan(` \u2508 --- BEGIN diff: ${file} ---`));
2270
+ for (const line of diff.split("\n")) {
2271
+ console.log(colorizeDiffLine(line));
2272
+ }
2273
+ console.log(pc6.cyan(` \u2508 --- END diff: ${file} ---`));
2274
+ }
2275
+ }
2276
+ function renderAgentPrompt(prompt) {
2277
+ if (!prompt) return;
2278
+ console.log();
2279
+ console.log(
2280
+ ikea.muted(
2281
+ " Paste this prompt into an agent to reconcile the conflicts above:"
2282
+ )
2283
+ );
2284
+ console.log();
2285
+ console.log(pc6.cyan(" \u2508 --- BEGIN agent prompt ---"));
2286
+ for (const line of prompt.split("\n")) console.log(line);
2287
+ console.log(pc6.cyan(" \u2508 --- END agent prompt ---"));
2288
+ }
2289
+ function registerSyncCommand(program2) {
2290
+ program2.command("sync").description("Update an existing SKALST\xC5KK project to the latest fittings").option("--dry-run", "Print diffs without writing files").option(
2291
+ "--force",
2292
+ "Overwrite user-modified managed keys with Scale Stack baselines"
2293
+ ).option("--keep-backup", "Keep .scale-stack/pre-sync-backup/ after success").action(
2294
+ async (opts) => {
2295
+ const projectRoot = resolve6(process.cwd());
2296
+ const result = await runSync({
2297
+ projectRoot,
2298
+ dryRun: Boolean(opts.dryRun),
2299
+ force: Boolean(opts.force),
2300
+ keepBackup: Boolean(opts.keepBackup)
2301
+ });
2302
+ if (!result.ok) {
2303
+ console.log();
2304
+ console.log(
2305
+ pc6.red(
2306
+ ` \u2717 ${result.error instanceof Error ? result.error.message : String(result.error)}`
2307
+ )
2308
+ );
2309
+ console.log();
2310
+ process.exitCode = 1;
2311
+ return;
2312
+ }
2313
+ const skips = groupSkipsByFile(result.logEntries);
2314
+ if (result.dryRun) {
2315
+ console.log();
2316
+ console.log(
2317
+ ikea.muted(
2318
+ ` ${pc6.cyan("\u25B8")} Dry-run complete (${result.changedFiles.length} file(s) would change).`
2319
+ )
2320
+ );
2321
+ if (skips.size > 0) renderSkippedConflicts(skips);
2322
+ renderConflictDiffs(result.conflictDiffs);
2323
+ renderAgentPrompt(result.agentPrompt);
2324
+ console.log();
2325
+ return;
2326
+ }
2327
+ console.log();
2328
+ if (result.changedFiles.length > 0) {
2329
+ console.log(
2330
+ pc6.green(
2331
+ ` \u2713 Sync applied to ${result.changedFiles.length} file(s): ${result.changedFiles.join(", ")}`
2332
+ )
2333
+ );
2334
+ } else if (skips.size === 0) {
2335
+ console.log(
2336
+ ikea.muted(
2337
+ ` ${pc6.green("\u25B8")} Already up to date \u2014 no managed files changed.`
2338
+ )
2339
+ );
2340
+ }
2341
+ if (skips.size > 0) renderSkippedConflicts(skips);
2342
+ renderConflictDiffs(result.conflictDiffs);
2343
+ renderAgentPrompt(result.agentPrompt);
2344
+ if (result.skillsError) {
2345
+ console.log(
2346
+ pc6.yellow(
2347
+ ` \u26A0 skills update step did not complete: ${result.skillsError.message}`
2348
+ )
2349
+ );
2350
+ }
2351
+ console.log();
2352
+ }
2353
+ );
2354
+ }
2355
+
2356
+ // src/lib/merge/package-json.ts
2357
+ function sortPackageSectionKeys(pkg, sectionName) {
2358
+ const section = pkg[sectionName];
2359
+ if (section === null || typeof section !== "object" || Array.isArray(section)) {
2360
+ return pkg;
2361
+ }
2362
+ const sorted = {};
2363
+ for (const key of Object.keys(section).sort()) {
2364
+ sorted[key] = section[key];
2365
+ }
2366
+ return { ...pkg, [sectionName]: sorted };
2367
+ }
2368
+ function mergeManagedPackageJson(options) {
2369
+ const merged = mergeManagedJsonString({
2370
+ relativePath: "package.json",
2371
+ existingContent: options.existingContent,
2372
+ desiredFragment: options.desiredFragment
2373
+ });
2374
+ const sorted = options.sortSections.reduce(
2375
+ (pkg, sectionName) => sortPackageSectionKeys(pkg, sectionName),
2376
+ merged
2377
+ );
2378
+ return formatMergedJson(sorted);
2379
+ }
2380
+
2381
+ // src/generators/core/agent-skills-init.ts
2382
+ var PHASE_4_1_BASELINE_AGENT_SKILLS = [
2383
+ { repo: "vercel-labs/skills", skill: "find-skills" },
2384
+ { repo: "vercel-labs/next-skills", skill: "next-best-practices" },
2385
+ { repo: "vercel-labs/agent-skills", skill: "vercel-react-view-transitions" },
2386
+ { repo: "vercel-labs/agent-skills", skill: "vercel-react-best-practices" },
2387
+ { repo: "vercel-labs/agent-skills", skill: "vercel-composition-patterns" },
2388
+ { repo: "vercel-labs/agent-skills", skill: "web-design-guidelines" }
2389
+ ];
2390
+ function groupSkillsByRepo(specs) {
2391
+ const order = [];
2392
+ const byRepo = /* @__PURE__ */ new Map();
2393
+ for (const { repo, skill } of specs) {
2394
+ let skills = byRepo.get(repo);
2395
+ if (!skills) {
2396
+ skills = [];
2397
+ byRepo.set(repo, skills);
2398
+ order.push(repo);
2399
+ }
2400
+ skills.push(skill);
2401
+ }
2402
+ return order.map((repo) => ({ repo, skills: byRepo.get(repo) ?? [] }));
2403
+ }
2404
+ function buildSkillsAddCommand(repo, skills) {
2405
+ const skillFlags = skills.map((s) => `--skill ${s}`).join(" ");
2406
+ return ["pnpm dlx skills add", repo, skillFlags, "-y"].filter(Boolean).join(" ");
2407
+ }
2408
+ var AGENT_SKILLS_EXEC_ENV = {
2409
+ NPM_CONFIG_LOGLEVEL: "error"
2410
+ };
2411
+ var agentSkillsInitGenerator = {
2412
+ name: "agent-skills-init",
2413
+ dependencies: ["project-init"],
2414
+ async execute(ctx) {
2415
+ const groups = groupSkillsByRepo(PHASE_4_1_BASELINE_AGENT_SKILLS);
2416
+ for (const { repo, skills } of groups) {
2417
+ await ctx.exec(buildSkillsAddCommand(repo, skills), {
2418
+ cwd: ctx.targetDir,
2419
+ env: { ...AGENT_SKILLS_EXEC_ENV }
2420
+ });
2421
+ }
2422
+ }
2423
+ };
2424
+
2425
+ // src/generators/app-paths.ts
2426
+ function localizeAppRoutePath(outputPath, i18n) {
2427
+ if (!i18n) return outputPath;
2428
+ if (!outputPath.startsWith("src/app/")) return outputPath;
2429
+ if (outputPath.startsWith("src/app/api/")) return outputPath;
2430
+ return outputPath.replace("src/app/", "src/app/[locale]/");
2431
+ }
2432
+
2433
+ // src/generators/ai-chat/ai-chat.ts
2434
+ var PHASE_22_AI_SDK_AGENT_SKILL_COMMAND = buildSkillsAddCommand(
2435
+ "vercel/ai",
2436
+ ["ai-sdk"]
2437
+ );
2438
+ var PHASE_22_AI_ELEMENTS_AGENT_SKILL_COMMAND = buildSkillsAddCommand(
2439
+ "vercel/ai-elements",
2440
+ ["ai-elements"]
2441
+ );
2442
+ var PHASE_22_AI_ELEMENTS_COMMAND = "pnpm dlx ai-elements@latest";
2443
+ var AI_CHAT_FILES = [
2444
+ {
2445
+ templatePath: "ai-chat/layout.tsx.ejs",
2446
+ outputPath: "src/app/chat/layout.tsx"
2447
+ },
2448
+ {
2449
+ templatePath: "ai-chat/page.tsx.ejs",
2450
+ outputPath: "src/app/chat/page.tsx"
2451
+ },
2452
+ {
2453
+ templatePath: "ai-chat/chat-panel.tsx.ejs",
2454
+ outputPath: "src/app/chat/_components/ChatPanel.tsx"
2455
+ },
2456
+ {
2457
+ templatePath: "ai-chat/use-chat.ts.ejs",
2458
+ outputPath: "src/app/chat/_hooks/useChat.ts"
2459
+ },
2460
+ {
2461
+ templatePath: "ai-chat/route.ts.ejs",
2462
+ outputPath: "src/app/api/chat/route.ts"
2463
+ }
2464
+ ];
2465
+ function mergePackageJson(existing) {
2466
+ const desiredFragment = {
2467
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
2468
+ };
2469
+ return mergeManagedPackageJson({
2470
+ existingContent: existing,
2471
+ desiredFragment,
2472
+ sortSections: ["dependencies"]
2473
+ });
2474
+ }
2475
+ async function writeAiChatFiles(ctx) {
2476
+ for (const file of AI_CHAT_FILES) {
2477
+ const content = await ctx.template.renderFile(file.templatePath, {
2478
+ i18n: ctx.config.i18n
2479
+ });
2480
+ ctx.fs.write(
2481
+ localizeAppRoutePath(file.outputPath, ctx.config.i18n),
2482
+ content
2483
+ );
2484
+ }
2485
+ }
2486
+ var aiChatGenerator = {
2487
+ name: "ai-chat",
2488
+ dependencies: ["ui"],
2489
+ condition: (config) => config.aiChat === true,
2490
+ async execute(ctx) {
2491
+ ctx.fs.merge("package.json", mergePackageJson);
2492
+ await ctx.exec(PHASE_22_AI_SDK_AGENT_SKILL_COMMAND, {
2493
+ cwd: ctx.targetDir,
2494
+ env: { ...AGENT_SKILLS_EXEC_ENV }
2495
+ });
2496
+ await ctx.exec(PHASE_22_AI_ELEMENTS_AGENT_SKILL_COMMAND, {
2497
+ cwd: ctx.targetDir,
2498
+ env: { ...AGENT_SKILLS_EXEC_ENV }
2499
+ });
2500
+ await ctx.exec(PHASE_22_AI_ELEMENTS_COMMAND, { cwd: ctx.targetDir });
2501
+ await writeAiChatFiles(ctx);
2502
+ }
2503
+ };
2504
+
2505
+ // src/generators/ai-assistant/ai-assistant.ts
2506
+ var SCALE_STACK_AGENTS_START = "<!-- scale-stack:agents:start -->";
2507
+ var SCALE_STACK_AGENTS_END = "<!-- scale-stack:agents:end -->";
2508
+ var SCALE_STACK_CLAUDE_START = "<!-- scale-stack:claude:start -->";
2509
+ var SCALE_STACK_CLAUDE_END = "<!-- scale-stack:claude:end -->";
2510
+ var FALLBACK_AGENTS_MD = `# AGENTS.md
2511
+ `;
2512
+ var FALLBACK_CLAUDE_MD = `@AGENTS.md
2513
+ `;
2514
+ function hasAuth(config) {
2515
+ return config.authStrategy !== "none";
2516
+ }
2517
+ function buildTechnologyGuides(config) {
2518
+ const guides = [
2519
+ {
2520
+ name: "Next.js 16.2+ (App Router)",
2521
+ purpose: "application framework",
2522
+ selected: false
2523
+ },
2524
+ { name: "React 19", purpose: "UI runtime", selected: false },
2525
+ { name: "Node 22+", purpose: "runtime target", selected: false },
2526
+ { name: "pnpm", purpose: "exclusive package manager", selected: false },
2527
+ {
2528
+ name: "TypeScript",
2529
+ purpose: "strict application language",
2530
+ selected: false
2531
+ },
2532
+ { name: "Turbopack", purpose: "Next.js dev bundler", selected: false },
2533
+ {
2534
+ name: "React Compiler",
2535
+ purpose: "default compiler optimization",
2536
+ selected: false
2537
+ },
2538
+ { name: "Tailwind CSS 4", purpose: "styling system", selected: false },
2539
+ { name: "shadcn/ui", purpose: "UI component primitives", selected: false },
2540
+ { name: "Zustand", purpose: "client state management", selected: false },
2541
+ { name: "nuqs", purpose: "URL-synchronized state", selected: false },
2542
+ {
2543
+ name: "@t3-oss/env-nextjs",
2544
+ purpose: "environment schema validation",
2545
+ selected: false
2546
+ },
2547
+ { name: "Zod", purpose: "schema validation", selected: false },
2548
+ {
2549
+ name: "next-safe-action",
2550
+ purpose: "typed and validated Server Actions",
2551
+ selected: false
2552
+ },
2553
+ { name: "react-hook-form", purpose: "form state", selected: false },
2554
+ {
2555
+ name: "@hookform/resolvers",
2556
+ purpose: "Zod form integration",
2557
+ selected: false
2558
+ },
2559
+ { name: "Docker", purpose: "production container build", selected: false },
2560
+ {
2561
+ name: "Docker Compose",
2562
+ purpose: "local service orchestration",
2563
+ selected: false
2564
+ },
2565
+ {
2566
+ name: "OpenTelemetry",
2567
+ purpose: "provider-agnostic tracing",
2568
+ selected: false
2569
+ },
2570
+ {
2571
+ name: "Agent DevTools MCP",
2572
+ purpose: "local Next.js agent debugging",
2573
+ selected: false
2574
+ },
2575
+ {
2576
+ name: "Vercel Agent Skills",
2577
+ purpose: "packaged agent workflows",
2578
+ selected: false
2579
+ },
2580
+ { name: "ESLint", purpose: "linting", selected: false },
2581
+ { name: "Prettier", purpose: "formatting", selected: false }
2582
+ ];
2583
+ if (config.orm === "prisma") {
2584
+ guides.push(
2585
+ { name: "Prisma v7", purpose: "ORM", selected: true },
2586
+ { name: "PostgreSQL", purpose: "database", selected: true },
2587
+ {
2588
+ name: "@prisma/adapter-pg",
2589
+ purpose: "Prisma PostgreSQL driver adapter",
2590
+ selected: true
2591
+ }
2592
+ );
2593
+ }
2594
+ if (hasAuth(config)) {
2595
+ guides.push(
2596
+ { name: "Better Auth", purpose: "authentication", selected: true },
2597
+ {
2598
+ name: "Microsoft Entra OAuth",
2599
+ purpose: "SSO provider",
2600
+ selected: true
2601
+ }
2602
+ );
2603
+ }
2604
+ if (config.aiChat) {
2605
+ guides.push(
2606
+ {
2607
+ name: "AI SDK 6",
2608
+ purpose: "client chat transport and UI message stream contract",
2609
+ selected: true
2610
+ },
2611
+ { name: "AI Elements", purpose: "chat UI components", selected: true }
2612
+ );
2613
+ }
2614
+ if (config.i18n) {
2615
+ guides.push({
2616
+ name: "next-intl",
2617
+ purpose: "internationalization",
2618
+ selected: true
2619
+ });
2620
+ }
2621
+ if (config.ci === "github") {
2622
+ guides.push({
2623
+ name: "GitHub Actions",
2624
+ purpose: "CI/CD pipeline; pnpm lint and Docker build are active, while typecheck/test/e2e steps are TODO no-ops until scripts exist",
2625
+ selected: true
2626
+ });
2627
+ }
2628
+ if (config.analytics === "plausible") {
2629
+ guides.push(
2630
+ {
2631
+ name: "Plausible",
2632
+ purpose: "privacy-focused analytics; runtime script is consent-gated and uses env-driven domain/API host values",
2633
+ selected: true
2634
+ },
2635
+ {
2636
+ name: "ClickHouse",
2637
+ purpose: "Plausible event storage",
2638
+ selected: true
2639
+ }
2640
+ );
2641
+ }
2642
+ if (config.preCommit) {
2643
+ guides.push({
2644
+ name: "prek",
2645
+ purpose: "pre-commit hook runner",
2646
+ selected: true
2647
+ });
2648
+ }
2649
+ return guides;
2650
+ }
2651
+ function buildBaselineSkillGuides() {
2652
+ return PHASE_4_1_BASELINE_AGENT_SKILLS.map(({ skill }) => {
2653
+ switch (skill) {
2654
+ case "find-skills":
2655
+ return {
2656
+ name: skill,
2657
+ purpose: "discover additional project-relevant Agent Skills"
2658
+ };
2659
+ case "next-best-practices":
2660
+ return {
2661
+ name: skill,
2662
+ purpose: "follow current Next.js conventions"
2663
+ };
2664
+ case "vercel-react-view-transitions":
2665
+ return {
2666
+ name: skill,
2667
+ purpose: "work with React view transitions"
2668
+ };
2669
+ case "vercel-react-best-practices":
2670
+ return {
2671
+ name: skill,
2672
+ purpose: "keep React code idiomatic"
2673
+ };
2674
+ case "vercel-composition-patterns":
2675
+ return {
2676
+ name: skill,
2677
+ purpose: "compose React components consistently"
2678
+ };
2679
+ case "web-design-guidelines":
2680
+ return {
2681
+ name: skill,
2682
+ purpose: "follow web design guidance"
2683
+ };
2684
+ default:
2685
+ return {
2686
+ name: skill,
2687
+ purpose: "follow project-specific coding guidance"
2688
+ };
2689
+ }
2690
+ });
2691
+ }
2692
+ function buildInstalledSkillGuides(config) {
2693
+ const guides = [
2694
+ ...buildBaselineSkillGuides(),
2695
+ { name: "shadcn/ui", purpose: "use installed shadcn/ui primitives" },
2696
+ { name: "nuqs", purpose: "manage URL-synchronized state" },
2697
+ { name: "zustand-5", purpose: "manage colocated client state" },
2698
+ {
2699
+ name: "next-safe-action",
2700
+ purpose: "write validated, typed Server Actions"
2701
+ }
2702
+ ];
2703
+ if (config.orm === "prisma") {
2704
+ guides.push({
2705
+ name: "prisma/skills",
2706
+ purpose: "work with Prisma schema, migrations, and client code"
2707
+ });
2708
+ }
2709
+ if (hasAuth(config)) {
2710
+ guides.push({
2711
+ name: "better-auth-best-practices",
2712
+ purpose: "follow Better Auth integration patterns"
2713
+ });
2714
+ }
2715
+ if (config.aiChat) {
2716
+ guides.push(
2717
+ {
2718
+ name: "ai-sdk",
2719
+ purpose: "use AI SDK client transport and UI message contracts"
2720
+ },
2721
+ {
2722
+ name: "ai-elements",
2723
+ purpose: "compose generated chat UI with AI Elements components"
2724
+ }
2725
+ );
2726
+ }
2727
+ return guides;
2728
+ }
2729
+ function replaceManagedSection(existing, startMarker, endMarker, section) {
2730
+ const trimmedExisting = existing.trimEnd();
2731
+ const pattern = new RegExp(
2732
+ `${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}`
2733
+ );
2734
+ if (pattern.test(trimmedExisting)) {
2735
+ return `${trimmedExisting.replace(pattern, section)}
2736
+ `;
2737
+ }
2738
+ return `${trimmedExisting}
2739
+
2740
+ ${section}
2741
+ `;
2742
+ }
2743
+ function removeManagedSection(existing, startMarker, endMarker) {
2744
+ const pattern = new RegExp(
2745
+ `\\n*${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}`
2746
+ );
2747
+ return existing.replace(pattern, "");
2748
+ }
2749
+ function escapeRegExp(value) {
2750
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2751
+ }
2752
+ function buildAgentsScaleStackSection(config) {
2753
+ const technologyLines = buildTechnologyGuides(config).map(
2754
+ ({ name, purpose, selected }) => `- ${selected ? "Selected" : "Default"}: \`${name}\` - ${purpose}.`
2755
+ );
2756
+ const skillLines = buildInstalledSkillGuides(config).map(
2757
+ ({ name, purpose }) => `- \`${name}\`: ${purpose}.`
2758
+ );
2759
+ return [
2760
+ SCALE_STACK_AGENTS_START,
2761
+ "## Scale Stack Guidance",
2762
+ "",
2763
+ "- Keep route-specific UI, hooks, state, actions, and AI artifacts colocated under private `_folders` inside the route segment that consumes them.",
2764
+ "- Use the listed Technology Stack by default; do not add alternatives unless the task requires it.",
2765
+ "- When adding or changing environment variables, update `src/env.ts` and `env.example`; never bypass the schema.",
2766
+ "- Never commit `.env`, credentials, API keys, tokens, or other secrets.",
2767
+ "",
2768
+ "### Technology Stack",
2769
+ "",
2770
+ ...technologyLines,
2771
+ "",
2772
+ "### Installed Agent Skills",
2773
+ "",
2774
+ ...skillLines,
2775
+ SCALE_STACK_AGENTS_END
2776
+ ].join("\n");
2777
+ }
2778
+ function mergeAgentsMdContent(existing, config) {
2779
+ const base = existing.trim() ? existing : FALLBACK_AGENTS_MD;
2780
+ return replaceManagedSection(
2781
+ base,
2782
+ SCALE_STACK_AGENTS_START,
2783
+ SCALE_STACK_AGENTS_END,
2784
+ buildAgentsScaleStackSection(config)
2785
+ );
2786
+ }
2787
+ function mergeClaudeMdContent(existing) {
2788
+ const withImport = existing.includes("@AGENTS.md") ? existing : `${FALLBACK_CLAUDE_MD}
2789
+ ${existing}`;
2790
+ const withoutOldManagedSection = removeManagedSection(
2791
+ withImport,
2792
+ SCALE_STACK_CLAUDE_START,
2793
+ SCALE_STACK_CLAUDE_END
2794
+ );
2795
+ return `${withoutOldManagedSection.trimEnd()}
2796
+ `;
2797
+ }
2798
+ function buildMcpJson() {
2799
+ return `${JSON.stringify(
2800
+ {
2801
+ mcpServers: {
2802
+ "next-devtools": {
2803
+ command: "npx",
2804
+ args: ["-y", "next-devtools-mcp@latest"]
2805
+ }
2806
+ }
2807
+ },
2808
+ null,
2809
+ 2
2810
+ )}
2811
+ `;
2812
+ }
2813
+ var aiAssistantGenerator = {
2814
+ name: "ai-assistant",
2815
+ dependencies: [
2816
+ "docker",
2817
+ "form-handling",
2818
+ "error-handling",
2819
+ "state",
2820
+ "eslint-prettier"
2821
+ ],
2822
+ resolveDependencies: (config) => [
2823
+ ...config.preCommit ? ["pre-commit"] : [],
2824
+ ...config.aiChat ? ["ai-chat"] : [],
2825
+ ...config.ci !== "none" ? ["ci"] : [],
2826
+ ...config.analytics !== "none" ? ["analytics"] : []
2827
+ ],
2828
+ async execute(ctx) {
2829
+ ctx.fs.merge(
2830
+ "AGENTS.md",
2831
+ (existing) => mergeAgentsMdContent(existing, ctx.config)
2832
+ );
2833
+ ctx.fs.merge("CLAUDE.md", mergeClaudeMdContent);
2834
+ ctx.fs.write(".mcp.json", buildMcpJson());
2835
+ }
2836
+ };
2837
+
2838
+ // src/generators/analytics/analytics.ts
2839
+ var ANALYTICS_PROVIDER_PATH = "src/app/_providers/analytics-provider.tsx";
2840
+ var ANALYTICS_HELPER_PATH = "src/lib/analytics.ts";
2841
+ var POSTHOG_NOT_IMPLEMENTED_MESSAGE = "PostHog analytics is not implemented yet; no analytics runtime files or dependencies generated.";
2842
+ var PLAUSIBLE_ENV_FRAGMENT = {
2843
+ variables: [
2844
+ {
2845
+ name: "NEXT_PUBLIC_PLAUSIBLE_API_HOST",
2846
+ scope: "client",
2847
+ schema: "z.string().url()",
2848
+ example: "http://localhost:8000"
2849
+ },
2850
+ {
2851
+ name: "NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
2852
+ scope: "client",
2853
+ schema: "z.string().min(1)",
2854
+ example: "localhost"
2855
+ },
2856
+ {
2857
+ name: "NEXT_PUBLIC_PLAUSIBLE_CAPTURE_LOCALHOST",
2858
+ scope: "client",
2859
+ schema: 'z.enum(["true", "false"])',
2860
+ example: "true"
2861
+ },
2862
+ {
2863
+ name: "NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC",
2864
+ scope: "client",
2865
+ schema: "z.string().url()",
2866
+ example: "http://localhost:8000/js/script.js"
2867
+ }
2868
+ ]
2869
+ };
2870
+ async function writePlausibleFiles(ctx) {
2871
+ const [provider, helper] = await Promise.all([
2872
+ ctx.template.renderFile("analytics/analytics-provider.tsx.ejs", {}),
2873
+ ctx.template.renderFile("analytics/analytics.ts.ejs", {})
2874
+ ]);
2875
+ ctx.envBag.push(structuredClone(PLAUSIBLE_ENV_FRAGMENT));
2876
+ ctx.fs.write(ANALYTICS_PROVIDER_PATH, provider);
2877
+ ctx.fs.write(ANALYTICS_HELPER_PATH, helper);
2878
+ }
2879
+ var analyticsGenerator = {
2880
+ name: "analytics",
2881
+ dependencies: ["env-management", "ui"],
2882
+ condition: (config) => config.analytics !== "none",
2883
+ async execute(ctx) {
2884
+ if (ctx.config.analytics === "posthog") {
2885
+ ctx.logger.warn("analytics", POSTHOG_NOT_IMPLEMENTED_MESSAGE);
2886
+ return;
2887
+ }
2888
+ await writePlausibleFiles(ctx);
2889
+ }
2890
+ };
2891
+
2892
+ // src/generators/orm/orm.ts
2893
+ var PHASE_13_ORM_AGENT_SKILL_COMMAND = buildSkillsAddCommand(
2894
+ "prisma/skills",
2895
+ []
2896
+ );
2897
+ var PHASE_13_PRISMA_GENERATE_COMMAND = "pnpm db:generate";
2898
+ var ORM_FILES = [
2899
+ {
2900
+ templatePath: "orm/schema.prisma.ejs",
2901
+ outputPath: "prisma/schema.prisma"
2902
+ },
2903
+ {
2904
+ templatePath: "orm/prisma.config.ts.ejs",
2905
+ outputPath: "prisma.config.ts"
2906
+ },
2907
+ {
2908
+ templatePath: "orm/prisma.ts.ejs",
2909
+ outputPath: "src/lib/prisma.ts"
2910
+ }
2911
+ ];
2912
+ function mergePackageJson2(existing) {
2913
+ const desiredFragment = {
2914
+ scripts: structuredClone(PACKAGE_SCRIPTS_BASELINE),
2915
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE),
2916
+ devDependencies: structuredClone(PACKAGE_DEV_DEPS_BASELINE)
2917
+ };
2918
+ return mergeManagedPackageJson({
2919
+ existingContent: existing,
2920
+ desiredFragment,
2921
+ sortSections: ["dependencies", "devDependencies"]
2922
+ });
2923
+ }
2924
+ async function writeOrmFiles(ctx) {
2925
+ for (const file of ORM_FILES) {
2926
+ const content = await ctx.template.renderFile(file.templatePath, {});
2927
+ ctx.fs.write(file.outputPath, content);
2928
+ }
2929
+ }
2930
+ var ormGenerator = {
2931
+ name: "orm",
2932
+ dependencies: ["env-management"],
2933
+ condition: (config) => config.orm === "prisma",
2934
+ async execute(ctx) {
2935
+ ctx.fs.merge("package.json", mergePackageJson2);
2936
+ await writeOrmFiles(ctx);
2937
+ await ctx.exec(PHASE_13_ORM_AGENT_SKILL_COMMAND, {
2938
+ cwd: ctx.targetDir,
2939
+ env: { ...AGENT_SKILLS_EXEC_ENV }
2940
+ });
2941
+ ctx.envBag.push(buildOrmEnvFragment(ctx.config.projectName));
2942
+ },
2943
+ async afterInstall(ctx) {
2944
+ if (ctx.config.authStrategy === "stateful") return;
2945
+ await ctx.exec(PHASE_13_PRISMA_GENERATE_COMMAND, {
2946
+ cwd: ctx.targetDir,
2947
+ env: {
2948
+ DATABASE_URL: buildDatabaseUrlExample(ctx.config.projectName)
2949
+ }
2950
+ });
2951
+ }
2952
+ };
2953
+
2954
+ // src/generators/auth/auth.ts
2955
+ var PHASE_14_AUTH_AGENT_SKILL_COMMAND = buildSkillsAddCommand(
2956
+ "better-auth/skills",
2957
+ ["better-auth-best-practices"]
2958
+ );
2959
+ var PHASE_14_AUTH_GENERATE_COMMAND = "pnpm dlx auth@latest generate --config src/lib/auth.ts --yes";
2960
+ var AUTH_FILES = [
2961
+ {
2962
+ templatePath: "auth/auth.ts.ejs",
2963
+ outputPath: "src/lib/auth.ts"
2964
+ },
2965
+ {
2966
+ templatePath: "auth/auth-client.ts.ejs",
2967
+ outputPath: "src/lib/auth-client.ts"
2968
+ },
2969
+ {
2970
+ templatePath: "auth/route.ts.ejs",
2971
+ outputPath: "src/app/api/auth/[...all]/route.ts"
2972
+ },
2973
+ {
2974
+ templatePath: "auth/sign-in-page.tsx.ejs",
2975
+ outputPath: "src/app/(auth)/sign-in/page.tsx"
2976
+ },
2977
+ {
2978
+ templatePath: "auth/sign-up-page.tsx.ejs",
2979
+ outputPath: "src/app/(auth)/sign-up/page.tsx"
2980
+ },
2981
+ {
2982
+ templatePath: "auth/unauthorized.tsx.ejs",
2983
+ outputPath: "src/app/unauthorized.tsx"
2984
+ }
2985
+ ];
2986
+ function isStatefulAuth(strategy) {
2987
+ return strategy === "stateful";
2988
+ }
2989
+ function buildAuthAfterInstallEnv(strategy, projectName) {
2990
+ const env = {
2991
+ BETTER_AUTH_SECRET: "replace-with-at-least-32-character-secret",
2992
+ BETTER_AUTH_URL: "http://localhost:3000",
2993
+ MICROSOFT_CLIENT_ID: "replace-with-microsoft-client-id",
2994
+ MICROSOFT_CLIENT_SECRET: "replace-with-microsoft-client-secret",
2995
+ MICROSOFT_TENANT_ID: "common",
2996
+ NODE_ENV: "development",
2997
+ SKIP_ENV_VALIDATION: "1"
2998
+ };
2999
+ if (isStatefulAuth(strategy)) {
3000
+ env.DATABASE_URL = buildDatabaseUrlExample(projectName);
3001
+ }
3002
+ return env;
3003
+ }
3004
+ function mergePackageJson3(existing) {
3005
+ const desiredFragment = {
3006
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
3007
+ };
3008
+ return mergeManagedPackageJson({
3009
+ existingContent: existing,
3010
+ desiredFragment,
3011
+ sortSections: ["dependencies"]
3012
+ });
3013
+ }
3014
+ async function writeAuthFiles(ctx) {
3015
+ const isStateful = isStatefulAuth(ctx.config.authStrategy);
3016
+ for (const file of AUTH_FILES) {
3017
+ const content = await ctx.template.renderFile(file.templatePath, {
3018
+ isStateful,
3019
+ i18n: ctx.config.i18n
3020
+ });
3021
+ ctx.fs.write(
3022
+ localizeAppRoutePath(file.outputPath, ctx.config.i18n),
3023
+ content
3024
+ );
3025
+ }
3026
+ }
3027
+ var authGenerator = {
3028
+ name: "auth",
3029
+ dependencies: ["ui", "env-management"],
3030
+ resolveDependencies: (config) => isStatefulAuth(config.authStrategy) ? ["orm"] : [],
3031
+ condition: (config) => config.authStrategy !== "none",
3032
+ async execute(ctx) {
3033
+ ctx.fs.merge("package.json", mergePackageJson3);
3034
+ await writeAuthFiles(ctx);
3035
+ await ctx.exec(PHASE_14_AUTH_AGENT_SKILL_COMMAND, {
3036
+ cwd: ctx.targetDir,
3037
+ env: { ...AGENT_SKILLS_EXEC_ENV }
3038
+ });
3039
+ ctx.envBag.push(buildAuthEnvFragment());
3040
+ },
3041
+ async afterInstall(ctx) {
3042
+ if (!isStatefulAuth(ctx.config.authStrategy)) return;
3043
+ const env = buildAuthAfterInstallEnv(
3044
+ ctx.config.authStrategy,
3045
+ ctx.config.projectName
3046
+ );
3047
+ await ctx.exec(PHASE_13_PRISMA_GENERATE_COMMAND, {
3048
+ cwd: ctx.targetDir,
3049
+ env
3050
+ });
3051
+ await ctx.exec(PHASE_14_AUTH_GENERATE_COMMAND, {
3052
+ cwd: ctx.targetDir,
3053
+ env
3054
+ });
3055
+ await ctx.exec(PHASE_13_PRISMA_GENERATE_COMMAND, {
3056
+ cwd: ctx.targetDir,
3057
+ env
3058
+ });
3059
+ }
3060
+ };
3061
+
3062
+ // src/generators/ci/ci.ts
3063
+ var GITHUB_ACTIONS_WORKFLOW_PATH = ".github/workflows/ci.yml";
3064
+ var CIRCLECI_CONFIG_PATH = ".circleci/config.yml";
3065
+ function hasPrisma(config) {
3066
+ return config.orm === "prisma";
3067
+ }
3068
+ function buildPostgresServiceYaml() {
3069
+ return [
3070
+ " services:",
3071
+ " postgres:",
3072
+ " image: postgres:16-alpine",
3073
+ " env:",
3074
+ " POSTGRES_USER: postgres",
3075
+ " POSTGRES_DB: scale_stack_ci",
3076
+ " POSTGRES_HOST_AUTH_METHOD: trust",
3077
+ " ports:",
3078
+ " - 5432:5432",
3079
+ " options: >-",
3080
+ ' --health-cmd "pg_isready -U postgres -d scale_stack_ci"',
3081
+ " --health-interval 5s",
3082
+ " --health-timeout 5s",
3083
+ " --health-retries 10"
3084
+ ];
3085
+ }
3086
+ function buildPrismaMigrationCheckYaml(config) {
3087
+ if (!hasPrisma(config)) return [];
3088
+ return [
3089
+ " - name: Check Prisma migrations",
3090
+ " env:",
3091
+ " DATABASE_URL: postgresql://postgres@localhost:5432/scale_stack_ci",
3092
+ " run: |",
3093
+ " pnpm prisma migrate diff \\",
3094
+ " --from-migrations prisma/migrations \\",
3095
+ " --to-schema-datamodel prisma/schema.prisma \\",
3096
+ ' --shadow-database-url "$DATABASE_URL" \\',
3097
+ " --exit-code",
3098
+ ""
3099
+ ];
3100
+ }
3101
+ function buildGithubActionsWorkflow(config) {
3102
+ return [
3103
+ "name: CI",
3104
+ "",
3105
+ "on:",
3106
+ " push:",
3107
+ " branches: [main]",
3108
+ " pull_request:",
3109
+ "",
3110
+ "permissions:",
3111
+ " contents: read",
3112
+ "",
3113
+ "jobs:",
3114
+ " validate:",
3115
+ " name: Validate",
3116
+ " runs-on: ubuntu-latest",
3117
+ ...hasPrisma(config) ? buildPostgresServiceYaml() : [],
3118
+ " steps:",
3119
+ " - name: Checkout",
3120
+ " uses: actions/checkout@v4",
3121
+ "",
3122
+ " - name: Setup pnpm",
3123
+ " uses: pnpm/action-setup@v4",
3124
+ "",
3125
+ " - name: Setup Node.js",
3126
+ " uses: actions/setup-node@v4",
3127
+ " with:",
3128
+ " node-version: 24",
3129
+ " cache: pnpm",
3130
+ "",
3131
+ " - name: Install dependencies",
3132
+ " run: pnpm install --frozen-lockfile",
3133
+ "",
3134
+ " - name: Lint",
3135
+ " run: pnpm lint",
3136
+ "",
3137
+ " - name: TODO typecheck",
3138
+ ` run: 'echo "TODO: enable pnpm typecheck after Phase 18 adds the script"'`,
3139
+ "",
3140
+ " - name: TODO unit tests",
3141
+ ` run: 'echo "TODO: enable pnpm test after Phase 18 adds the script"'`,
3142
+ "",
3143
+ " - name: TODO end-to-end tests",
3144
+ ` run: 'echo "TODO: enable pnpm test:e2e after Phase 18 adds the script"'`,
3145
+ "",
3146
+ ...buildPrismaMigrationCheckYaml(config),
3147
+ " - name: Verify Docker build",
3148
+ " run: docker build .",
3149
+ ""
3150
+ ].join("\n");
3151
+ }
3152
+ var ciGenerator = {
3153
+ name: "ci",
3154
+ dependencies: ["docker"],
3155
+ resolveDependencies(config) {
3156
+ return hasPrisma(config) ? ["orm"] : [];
3157
+ },
3158
+ condition(config) {
3159
+ return config.ci !== "none";
3160
+ },
3161
+ async execute(ctx) {
3162
+ if (ctx.config.ci === "circleci") {
3163
+ ctx.logger.warn(
3164
+ "ci",
3165
+ `CircleCI coming soon; no ${CIRCLECI_CONFIG_PATH} generated yet.`
3166
+ );
3167
+ return;
3168
+ }
3169
+ ctx.fs.write(
3170
+ GITHUB_ACTIONS_WORKFLOW_PATH,
3171
+ buildGithubActionsWorkflow(ctx.config)
3172
+ );
3173
+ }
3174
+ };
3175
+
3176
+ // src/generators/core/eslint-prettier.ts
3177
+ function buildPrettierRcContents() {
3178
+ return `${JSON.stringify(PRETTIER_RC_BASELINE, null, 2)}
3179
+ `;
3180
+ }
3181
+ function buildPrettierIgnoreContents(config) {
3182
+ return renderTemplateFile("eslint-prettier/.prettierignore.ejs", {
3183
+ ignoreAiElementsGeneratedOutput: shouldIgnoreAiElementsGeneratedOutput(config),
3184
+ ignorePrismaGeneratedOutput: shouldIgnorePrismaGeneratedOutput(config)
3185
+ });
3186
+ }
3187
+ function mergePackageJson4(existing) {
3188
+ const desiredFragment = {
3189
+ scripts: structuredClone(PACKAGE_SCRIPTS_BASELINE),
3190
+ devDependencies: structuredClone(PACKAGE_DEV_DEPS_BASELINE)
3191
+ };
3192
+ return mergeManagedPackageJson({
3193
+ existingContent: existing,
3194
+ desiredFragment,
3195
+ sortSections: ["devDependencies"]
3196
+ });
3197
+ }
3198
+ var eslintPrettierGenerator = {
3199
+ name: "eslint-prettier",
3200
+ dependencies: ["project-init", "env-management"],
3201
+ async execute(ctx) {
3202
+ ctx.fs.write(
3203
+ "eslint.config.mjs",
3204
+ await buildEslintConfigMjsContents(ctx.config)
3205
+ );
3206
+ ctx.fs.write(".prettierrc", buildPrettierRcContents());
3207
+ ctx.fs.write(
3208
+ ".prettierignore",
3209
+ await buildPrettierIgnoreContents(ctx.config)
3210
+ );
3211
+ ctx.fs.merge("package.json", mergePackageJson4);
3212
+ },
3213
+ async afterInstall(ctx) {
3214
+ await ctx.exec("pnpm exec prettier --write .");
3215
+ }
3216
+ };
3217
+
3218
+ // src/generators/core/build-env-files-from-fragments.ts
3219
+ function collectVariables(bag) {
3220
+ const merged = /* @__PURE__ */ new Map();
3221
+ for (const fragment of bag.fragments) {
3222
+ for (const variable of fragment.variables) {
3223
+ merged.set(variable.name, variable);
3224
+ }
3225
+ }
3226
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
3227
+ }
3228
+ function pickScope(variables, scope) {
3229
+ return variables.filter((variable) => variable.scope === scope);
3230
+ }
3231
+ function buildSchemaSection(variables) {
3232
+ if (variables.length === 0) return "{}";
3233
+ return `{
3234
+ ${variables.map((variable) => ` ${variable.name}: ${variable.schema},`).join("\n")}
3235
+ }`;
3236
+ }
3237
+ function buildRuntimeEnvSection(variables) {
3238
+ if (variables.length === 0) return "{}";
3239
+ return `{
3240
+ ${variables.map((variable) => ` ${variable.name}: process.env["${variable.name}"],`).join("\n")}
3241
+ }`;
3242
+ }
3243
+ function buildEnvExample(variables) {
3244
+ const exampleLines = [
3245
+ "# Copy this file to `.env` and replace placeholder values before running the app.",
3246
+ "# `src/env.ts` is the source of truth for validation and types for the variables listed here.",
3247
+ "# Variables managed by Next.js/runtime (for example `NODE_ENV`) are intentionally omitted."
3248
+ ];
3249
+ const userSuppliedVariables = variables.filter(
3250
+ (variable) => variable.includeInExample !== false
3251
+ );
3252
+ if (userSuppliedVariables.length === 0) {
3253
+ return `${exampleLines.join("\n")}
3254
+ `;
3255
+ }
3256
+ return `${exampleLines.join("\n")}
3257
+ ${userSuppliedVariables.map((variable) => `${variable.name}=${variable.example}`).join("\n")}
3258
+ `;
3259
+ }
3260
+ function buildEnvFilesFromFragments(bag) {
3261
+ const variables = collectVariables(bag);
3262
+ const server = pickScope(variables, "server");
3263
+ const client = pickScope(variables, "client");
3264
+ const shared = pickScope(variables, "shared");
3265
+ const runtimeEnv = [...shared, ...client].sort(
3266
+ (a, b) => a.name.localeCompare(b.name)
3267
+ );
3268
+ const envTs = `import { createEnv } from "@t3-oss/env-nextjs";
3269
+ import { z } from "zod";
3270
+
3271
+ export const env = createEnv({
3272
+ server: ${buildSchemaSection(server)},
3273
+ client: ${buildSchemaSection(client)},
3274
+ shared: ${buildSchemaSection(shared)},
3275
+ experimental__runtimeEnv: ${buildRuntimeEnvSection(runtimeEnv)},
3276
+ skipValidation: process.env["SKIP_ENV_VALIDATION"] === "1",
23
3277
  });
3278
+ `;
3279
+ return { envTs, envExample: buildEnvExample(variables) };
3280
+ }
3281
+
3282
+ // src/generators/core/env-management.ts
3283
+ var BASELINE_ENV_FRAGMENT = {
3284
+ variables: [
3285
+ {
3286
+ name: "NODE_ENV",
3287
+ scope: "shared",
3288
+ schema: 'z.enum(["development", "production", "test"])',
3289
+ example: "development",
3290
+ includeInExample: false
3291
+ }
3292
+ ]
3293
+ };
3294
+ function mergePackageJson5(existing) {
3295
+ const desiredFragment = {
3296
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
3297
+ };
3298
+ return mergeManagedPackageJson({
3299
+ existingContent: existing,
3300
+ desiredFragment,
3301
+ sortSections: ["dependencies"]
3302
+ });
3303
+ }
3304
+ var envManagementGenerator = {
3305
+ name: "env-management",
3306
+ dependencies: ["project-init"],
3307
+ async execute(ctx) {
3308
+ ctx.fs.merge("package.json", mergePackageJson5);
3309
+ ctx.envBag.push(structuredClone(BASELINE_ENV_FRAGMENT));
3310
+ },
3311
+ async afterInstall(ctx) {
3312
+ const files = buildEnvFilesFromFragments(ctx.envBag);
3313
+ ctx.fs.write("src/env.ts", files.envTs);
3314
+ ctx.fs.write("env.example", files.envExample);
3315
+ }
3316
+ };
3317
+
3318
+ // src/generators/core/project-init.ts
3319
+ import { basename, dirname as dirname5 } from "path";
3320
+ import { rm as rm3 } from "fs/promises";
3321
+ var CREATE_NEXT_APP_VERSION = "16.2.6";
3322
+ var CNA_FLAGS = "--react-compiler --eslint --app --src-dir --use-pnpm --skip-install --disable-git";
3323
+ function buildCreateNextAppCommand(projectDirName) {
3324
+ const q = shellQuoteBasename(projectDirName);
3325
+ return `pnpm create next-app@${CREATE_NEXT_APP_VERSION} ${q} ${CNA_FLAGS}`;
3326
+ }
3327
+ function shellQuoteBasename(name) {
3328
+ if (/^[a-zA-Z0-9._-]+$/.test(name)) return name;
3329
+ return `'${name.replace(/'/g, `'\\''`)}'`;
3330
+ }
3331
+ function mergeTsconfigContent(existing, config) {
3332
+ const desiredFragment = structuredClone(
3333
+ buildTsconfigManagedBaseline(config)
3334
+ );
3335
+ const merged = mergeManagedJsonString({
3336
+ relativePath: "tsconfig.json",
3337
+ existingContent: existing,
3338
+ desiredFragment
3339
+ });
3340
+ return formatMergedJson(merged);
3341
+ }
3342
+ function mergeGitignoreContent(existing) {
3343
+ const needEnv = !/^\.env\*$/m.test(existing);
3344
+ const needScaleStack = !/^\.scale-stack\/?$/m.test(existing);
3345
+ const parts = [existing.replace(/\s+$/, "")];
3346
+ if (needEnv || needScaleStack) {
3347
+ parts.push("");
3348
+ parts.push("# Scale Stack");
3349
+ if (needEnv) parts.push(".env*");
3350
+ if (needScaleStack) parts.push(".scale-stack/");
3351
+ }
3352
+ return `${parts.join("\n")}
3353
+ `;
3354
+ }
3355
+ async function writeNextConfig(ctx) {
3356
+ ctx.fs.write(
3357
+ "next.config.ts",
3358
+ await buildNextConfigTsContents({
3359
+ config: {
3360
+ authStrategy: ctx.config.authStrategy,
3361
+ i18n: ctx.config.i18n
3362
+ }
3363
+ })
3364
+ );
3365
+ }
3366
+ async function writeAppOverlays(ctx) {
3367
+ const { projectName } = ctx.config;
3368
+ const [layout, loading] = await Promise.all([
3369
+ ctx.template.renderFile("core/layout.tsx.ejs", { projectName }),
3370
+ ctx.template.renderFile("core/loading.tsx.ejs", {})
3371
+ ]);
3372
+ ctx.fs.write("src/app/layout.tsx", layout);
3373
+ ctx.fs.write(
3374
+ localizeAppRoutePath("src/app/loading.tsx", ctx.config.i18n),
3375
+ loading
3376
+ );
3377
+ }
3378
+ var projectInitGenerator = {
3379
+ name: "project-init",
3380
+ dependencies: [],
3381
+ async execute(ctx) {
3382
+ const folderName = basename(ctx.targetDir);
3383
+ const parentDir = dirname5(ctx.targetDir);
3384
+ const cmd = buildCreateNextAppCommand(folderName);
3385
+ await ctx.exec(cmd, { cwd: parentDir });
3386
+ await writeNextConfig(ctx);
3387
+ await writeAppOverlays(ctx);
3388
+ ctx.fs.merge(
3389
+ "tsconfig.json",
3390
+ (existing) => mergeTsconfigContent(existing, ctx.config)
3391
+ );
3392
+ ctx.fs.merge(".gitignore", mergeGitignoreContent);
3393
+ },
3394
+ async rollback(ctx) {
3395
+ await rm3(ctx.targetDir, { recursive: true, force: true });
3396
+ }
3397
+ };
3398
+
3399
+ // src/generators/docker/docker.ts
3400
+ var DOCKER_NODE_VERSION = "24.13.0-slim";
3401
+ var POSTGRES_IMAGE = "postgres:16-alpine";
3402
+ var CLICKHOUSE_IMAGE = "clickhouse/clickhouse-server:24.12-alpine";
3403
+ var PLAUSIBLE_IMAGE = "ghcr.io/plausible/community-edition:v3.2.0";
3404
+ var PLAUSIBLE_LOCAL_SECRET_KEY_BASE = "local-only-change-me-local-only-change-me-local-only-change-me-local-only-change-me";
3405
+ function hasPrisma2(config) {
3406
+ return config.orm === "prisma";
3407
+ }
3408
+ function hasPlausible(config) {
3409
+ return config.analytics === "plausible";
3410
+ }
3411
+ function buildComposeDatabaseUrl(projectName) {
3412
+ return `postgresql://postgres:postgres@postgres:5432/${buildPostgresDatabaseName(projectName)}`;
3413
+ }
3414
+ function buildAppEnvironment(config) {
3415
+ const lines = [' PORT: "3000"', ' HOSTNAME: "0.0.0.0"'];
3416
+ if (hasPrisma2(config)) {
3417
+ lines.push(
3418
+ ` DATABASE_URL: ${buildComposeDatabaseUrl(config.projectName)}`
3419
+ );
3420
+ lines.push(' RUN_MIGRATIONS: "true"');
3421
+ }
3422
+ return lines;
3423
+ }
3424
+ function buildAppDependsOn(config) {
3425
+ const lines = [];
3426
+ if (hasPrisma2(config)) {
3427
+ lines.push(" postgres:");
3428
+ lines.push(" condition: service_healthy");
3429
+ }
3430
+ if (hasPlausible(config)) {
3431
+ lines.push(" plausible:");
3432
+ lines.push(" condition: service_healthy");
3433
+ }
3434
+ return lines;
3435
+ }
3436
+ function buildPostgresService(config) {
3437
+ if (!hasPrisma2(config)) return [];
3438
+ const databaseName = buildPostgresDatabaseName(config.projectName);
3439
+ return [
3440
+ " postgres:",
3441
+ ` image: ${POSTGRES_IMAGE}`,
3442
+ " environment:",
3443
+ " POSTGRES_USER: postgres",
3444
+ " POSTGRES_PASSWORD: postgres",
3445
+ ` POSTGRES_DB: ${databaseName}`,
3446
+ " volumes:",
3447
+ " - postgres-data:/var/lib/postgresql/data",
3448
+ " healthcheck:",
3449
+ ` test: ["CMD-SHELL", "pg_isready -U postgres -d ${databaseName}"]`,
3450
+ " interval: 5s",
3451
+ " timeout: 5s",
3452
+ " retries: 10",
3453
+ " start_period: 10s"
3454
+ ];
3455
+ }
3456
+ function buildPlausibleServices() {
3457
+ return [
3458
+ " plausible-db:",
3459
+ ` image: ${POSTGRES_IMAGE}`,
3460
+ " environment:",
3461
+ " POSTGRES_USER: postgres",
3462
+ " POSTGRES_PASSWORD: postgres",
3463
+ " POSTGRES_DB: plausible",
3464
+ " volumes:",
3465
+ " - plausible-db-data:/var/lib/postgresql/data",
3466
+ " healthcheck:",
3467
+ ' test: ["CMD-SHELL", "pg_isready -U postgres -d plausible"]',
3468
+ " interval: 5s",
3469
+ " timeout: 5s",
3470
+ " retries: 10",
3471
+ " start_period: 1m",
3472
+ "",
3473
+ " plausible-events-db:",
3474
+ ` image: ${CLICKHOUSE_IMAGE}`,
3475
+ " environment:",
3476
+ ' CLICKHOUSE_SKIP_USER_SETUP: "1"',
3477
+ " volumes:",
3478
+ " - plausible-event-data:/var/lib/clickhouse",
3479
+ " - plausible-event-logs:/var/log/clickhouse-server",
3480
+ " ulimits:",
3481
+ " nofile:",
3482
+ " soft: 262144",
3483
+ " hard: 262144",
3484
+ " healthcheck:",
3485
+ " test:",
3486
+ " [",
3487
+ ' "CMD-SHELL",',
3488
+ ' "wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1",',
3489
+ " ]",
3490
+ " interval: 5s",
3491
+ " timeout: 5s",
3492
+ " retries: 10",
3493
+ " start_period: 1m",
3494
+ "",
3495
+ " plausible:",
3496
+ ` image: ${PLAUSIBLE_IMAGE}`,
3497
+ ' command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"',
3498
+ " depends_on:",
3499
+ " plausible-db:",
3500
+ " condition: service_healthy",
3501
+ " plausible-events-db:",
3502
+ " condition: service_healthy",
3503
+ " ports:",
3504
+ ' - "8000:8000"',
3505
+ " volumes:",
3506
+ " - plausible-data:/var/lib/plausible",
3507
+ " ulimits:",
3508
+ " nofile:",
3509
+ " soft: 65535",
3510
+ " hard: 65535",
3511
+ " environment:",
3512
+ " TMPDIR: /var/lib/plausible/tmp",
3513
+ " BASE_URL: http://localhost:8000",
3514
+ ` SECRET_KEY_BASE: ${PLAUSIBLE_LOCAL_SECRET_KEY_BASE}`,
3515
+ " DATABASE_URL: postgres://postgres:postgres@plausible-db:5432/plausible",
3516
+ " CLICKHOUSE_DATABASE_URL: http://plausible-events-db:8123/plausible_events_db",
3517
+ " healthcheck:",
3518
+ " test:",
3519
+ " [",
3520
+ ' "CMD-SHELL",',
3521
+ ' "wget --no-verbose --tries=1 -O - http://127.0.0.1:8000/api/system/health/ready || exit 1",',
3522
+ " ]",
3523
+ " interval: 10s",
3524
+ " timeout: 5s",
3525
+ " retries: 12",
3526
+ " start_period: 1m"
3527
+ ];
3528
+ }
3529
+ function buildVolumes(config) {
3530
+ const lines = [];
3531
+ if (hasPrisma2(config)) {
3532
+ lines.push(" postgres-data:");
3533
+ }
3534
+ if (hasPlausible(config)) {
3535
+ lines.push(" plausible-db-data:");
3536
+ lines.push(" plausible-event-data:");
3537
+ lines.push(" plausible-event-logs:");
3538
+ lines.push(" plausible-data:");
3539
+ }
3540
+ return lines;
3541
+ }
3542
+ function buildDockerComposeYaml(config) {
3543
+ const appDependsOn = buildAppDependsOn(config);
3544
+ const services = [
3545
+ "services:",
3546
+ " app:",
3547
+ " build:",
3548
+ " context: .",
3549
+ " dockerfile: Dockerfile",
3550
+ " environment:",
3551
+ ...buildAppEnvironment(config),
3552
+ " ports:",
3553
+ ' - "3000:3000"',
3554
+ ...appDependsOn.length > 0 ? [" depends_on:", ...appDependsOn] : [],
3555
+ ...buildPostgresService(config),
3556
+ ...hasPlausible(config) ? buildPlausibleServices() : []
3557
+ ];
3558
+ const volumes = buildVolumes(config);
3559
+ return [
3560
+ ...services,
3561
+ ...volumes.length > 0 ? ["", "volumes:", ...volumes] : [],
3562
+ ""
3563
+ ].join("\n");
3564
+ }
3565
+ function buildPrismaRunnerAdditions(config) {
3566
+ if (!hasPrisma2(config)) return [];
3567
+ return [
3568
+ "RUN apt-get update \\",
3569
+ " && apt-get install -y --no-install-recommends openssl postgresql-client \\",
3570
+ " && rm -rf /var/lib/apt/lists/* \\",
3571
+ " && corepack enable pnpm",
3572
+ "",
3573
+ "COPY --from=deps --chown=node:node /app/node_modules ./node_modules",
3574
+ "COPY --from=builder --chown=node:node /app/package.json ./package.json",
3575
+ "COPY --from=builder --chown=node:node /app/pnpm-lock.yaml ./pnpm-lock.yaml",
3576
+ "COPY --from=builder --chown=node:node /app/prisma ./prisma",
3577
+ "COPY --from=builder --chown=node:node /app/prisma.config.ts ./prisma.config.ts",
3578
+ ""
3579
+ ];
3580
+ }
3581
+ function buildPrismaBuilderEnvironment(config) {
3582
+ if (!hasPrisma2(config)) return [];
3583
+ return [
3584
+ `ENV DATABASE_URL=${buildComposeDatabaseUrl(config.projectName)}`,
3585
+ ""
3586
+ ];
3587
+ }
3588
+ function buildDockerfile(config) {
3589
+ return [
3590
+ "# syntax=docker/dockerfile:1.7",
3591
+ `ARG NODE_VERSION=${DOCKER_NODE_VERSION}`,
3592
+ "",
3593
+ "FROM node:${NODE_VERSION} AS deps",
3594
+ "WORKDIR /app",
3595
+ "",
3596
+ "COPY package.json pnpm-lock.yaml* .npmrc* ./",
3597
+ "RUN --mount=type=cache,target=/root/.local/share/pnpm/store \\",
3598
+ " corepack enable pnpm \\",
3599
+ " && pnpm install --frozen-lockfile --dangerously-allow-all-builds",
3600
+ "",
3601
+ "FROM node:${NODE_VERSION} AS builder",
3602
+ "WORKDIR /app",
3603
+ "",
3604
+ "COPY --from=deps /app/node_modules ./node_modules",
3605
+ "COPY . .",
3606
+ "",
3607
+ "ENV NODE_ENV=production",
3608
+ "ENV NEXT_TELEMETRY_DISABLED=1",
3609
+ ...buildPrismaBuilderEnvironment(config),
3610
+ "",
3611
+ "RUN corepack enable pnpm && pnpm build",
3612
+ "",
3613
+ "FROM node:${NODE_VERSION} AS runner",
3614
+ "WORKDIR /app",
3615
+ "",
3616
+ "ENV NODE_ENV=production",
3617
+ "ENV PORT=3000",
3618
+ 'ENV HOSTNAME="0.0.0.0"',
3619
+ "ENV NEXT_TELEMETRY_DISABLED=1",
3620
+ "",
3621
+ ...buildPrismaRunnerAdditions(config),
3622
+ "COPY --from=builder --chown=node:node /app/public ./public",
3623
+ "",
3624
+ "RUN mkdir .next && chown node:node .next",
3625
+ "",
3626
+ "COPY --from=builder --chown=node:node /app/.next/standalone ./",
3627
+ "COPY --from=builder --chown=node:node /app/.next/static ./.next/static",
3628
+ "COPY --chown=node:node start.sh ./start.sh",
3629
+ "RUN chmod +x ./start.sh",
3630
+ "",
3631
+ "USER node",
3632
+ "",
3633
+ "EXPOSE 3000",
3634
+ "",
3635
+ 'CMD ["./start.sh"]',
3636
+ ""
3637
+ ].join("\n");
3638
+ }
3639
+ function buildStartSh(config) {
3640
+ const lines = [
3641
+ "#!/bin/sh",
3642
+ "set -eu",
3643
+ "",
3644
+ 'if [ "${RUN_MIGRATIONS:-false}" = "true" ]; then'
3645
+ ];
3646
+ if (hasPrisma2(config)) {
3647
+ lines.push(
3648
+ ' until pg_isready -d "${DATABASE_URL}"; do',
3649
+ ' echo "Waiting for database..."',
3650
+ " sleep 2",
3651
+ " done",
3652
+ "",
3653
+ " pnpm db:migrate:deploy"
3654
+ );
3655
+ } else {
3656
+ lines.push(
3657
+ ' echo "RUN_MIGRATIONS=true was set, but no ORM is configured."'
3658
+ );
3659
+ }
3660
+ lines.push("", "fi", "", "exec node server.js", "");
3661
+ return lines.join("\n");
3662
+ }
3663
+ function buildDockerignore() {
3664
+ return [
3665
+ ".git",
3666
+ ".scale-stack",
3667
+ ".mcp.json",
3668
+ ".next",
3669
+ "node_modules",
3670
+ "coverage",
3671
+ "dist",
3672
+ ".env*",
3673
+ "*.log",
3674
+ "npm-debug.log*",
3675
+ "yarn-debug.log*",
3676
+ "yarn-error.log*",
3677
+ ".pnpm-store",
3678
+ ".turbo",
3679
+ ".DS_Store",
3680
+ ""
3681
+ ].join("\n");
3682
+ }
3683
+ var dockerGenerator = {
3684
+ name: "docker",
3685
+ dependencies: ["project-init"],
3686
+ resolveDependencies(config) {
3687
+ return hasPrisma2(config) ? ["orm"] : [];
3688
+ },
3689
+ async execute(ctx) {
3690
+ ctx.fs.write("Dockerfile", buildDockerfile(ctx.config));
3691
+ ctx.fs.write("start.sh", buildStartSh(ctx.config));
3692
+ ctx.fs.write("docker-compose.yml", buildDockerComposeYaml(ctx.config));
3693
+ ctx.fs.write(".dockerignore", buildDockerignore());
3694
+ }
3695
+ };
3696
+
3697
+ // src/generators/error-handling/error-handling.ts
3698
+ var ERROR_HANDLING_FILES = [
3699
+ {
3700
+ templatePath: "error-handling/error.tsx.ejs",
3701
+ outputPath: "src/app/error.tsx"
3702
+ },
3703
+ {
3704
+ templatePath: "error-handling/global-error.tsx.ejs",
3705
+ outputPath: "src/app/global-error.tsx"
3706
+ },
3707
+ {
3708
+ templatePath: "error-handling/not-found.tsx.ejs",
3709
+ outputPath: "src/app/not-found.tsx"
3710
+ },
3711
+ {
3712
+ templatePath: "error-handling/catch-all-not-found-page.tsx.ejs",
3713
+ outputPath: "src/app/[...not-found]/page.tsx"
3714
+ }
3715
+ ];
3716
+ var errorHandlingGenerator = {
3717
+ name: "error-handling",
3718
+ dependencies: ["ui"],
3719
+ async execute(ctx) {
3720
+ for (const file of ERROR_HANDLING_FILES) {
3721
+ const content = await ctx.template.renderFile(file.templatePath, {
3722
+ i18n: ctx.config.i18n
3723
+ });
3724
+ const outputPath = file.outputPath === "src/app/global-error.tsx" || file.outputPath === "src/app/[...not-found]/page.tsx" ? file.outputPath : localizeAppRoutePath(file.outputPath, ctx.config.i18n);
3725
+ ctx.fs.write(outputPath, content);
3726
+ }
3727
+ }
3728
+ };
3729
+
3730
+ // src/generators/server-actions/server-actions.ts
3731
+ var PHASE_15_SERVER_ACTIONS_AGENT_SKILL_COMMAND = buildSkillsAddCommand("next-safe-action/skills", []);
3732
+ var SERVER_ACTIONS_FILES = [
3733
+ {
3734
+ templatePath: "server-actions/safe-action.ts.ejs",
3735
+ outputPath: "src/lib/safe-action.ts"
3736
+ }
3737
+ ];
3738
+ function hasAuth2(strategy) {
3739
+ return strategy !== "none";
3740
+ }
3741
+ function buildDependencies(strategy) {
3742
+ const dependencies = structuredClone(PACKAGE_DEPS_BASELINE);
3743
+ if (hasAuth2(strategy)) {
3744
+ Object.assign(dependencies, SERVER_ACTIONS_AUTH_DEPENDENCIES);
3745
+ }
3746
+ return dependencies;
3747
+ }
3748
+ function mergePackageJson6(existing, strategy = "none") {
3749
+ const desiredFragment = {
3750
+ dependencies: buildDependencies(strategy)
3751
+ };
3752
+ return mergeManagedPackageJson({
3753
+ existingContent: existing,
3754
+ desiredFragment,
3755
+ sortSections: ["dependencies"]
3756
+ });
3757
+ }
3758
+ async function writeServerActionFiles(ctx) {
3759
+ const hasAuthClient = hasAuth2(ctx.config.authStrategy);
3760
+ for (const file of SERVER_ACTIONS_FILES) {
3761
+ const content = await ctx.template.renderFile(file.templatePath, {
3762
+ hasAuth: hasAuthClient
3763
+ });
3764
+ ctx.fs.write(file.outputPath, content);
3765
+ }
3766
+ }
3767
+ var serverActionsGenerator = {
3768
+ name: "server-actions",
3769
+ dependencies: ["ui", "env-management"],
3770
+ resolveDependencies: (config) => hasAuth2(config.authStrategy) ? ["auth"] : [],
3771
+ async execute(ctx) {
3772
+ ctx.fs.merge(
3773
+ "package.json",
3774
+ (existing) => mergePackageJson6(existing, ctx.config.authStrategy)
3775
+ );
3776
+ await writeServerActionFiles(ctx);
3777
+ await ctx.exec(PHASE_15_SERVER_ACTIONS_AGENT_SKILL_COMMAND, {
3778
+ cwd: ctx.targetDir,
3779
+ env: { ...AGENT_SKILLS_EXEC_ENV }
3780
+ });
3781
+ }
3782
+ };
3783
+
3784
+ // src/generators/form-handling/form-handling.ts
3785
+ var FORM_HANDLING_FILES = [
3786
+ {
3787
+ templatePath: "form-handling/example-form-schema.ts.ejs",
3788
+ outputPath: "src/app/dashboard/_lib/example-form-schema.ts"
3789
+ },
3790
+ {
3791
+ templatePath: "form-handling/example-form.tsx.ejs",
3792
+ outputPath: "src/app/dashboard/_components/ExampleForm.tsx"
3793
+ },
3794
+ {
3795
+ templatePath: "form-handling/example-form-action.ts.ejs",
3796
+ outputPath: "src/app/dashboard/_actions/example-form.ts"
3797
+ },
3798
+ {
3799
+ templatePath: "form-handling/dashboard-page.tsx.ejs",
3800
+ outputPath: "src/app/dashboard/page.tsx"
3801
+ }
3802
+ ];
3803
+ function mergePackageJson7(existing) {
3804
+ const desiredFragment = {
3805
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
3806
+ };
3807
+ return mergeManagedPackageJson({
3808
+ existingContent: existing,
3809
+ desiredFragment,
3810
+ sortSections: ["dependencies"]
3811
+ });
3812
+ }
3813
+ async function writeFormHandlingFiles(ctx) {
3814
+ const hasAuthClient = hasAuth2(ctx.config.authStrategy);
3815
+ for (const file of FORM_HANDLING_FILES) {
3816
+ const content = await ctx.template.renderFile(file.templatePath, {
3817
+ hasAuth: hasAuthClient,
3818
+ i18n: ctx.config.i18n
3819
+ });
3820
+ ctx.fs.write(
3821
+ localizeAppRoutePath(file.outputPath, ctx.config.i18n),
3822
+ content
3823
+ );
3824
+ }
3825
+ }
3826
+ var formHandlingGenerator = {
3827
+ name: "form-handling",
3828
+ dependencies: ["ui", "server-actions"],
3829
+ async execute(ctx) {
3830
+ ctx.fs.merge("package.json", mergePackageJson7);
3831
+ await writeFormHandlingFiles(ctx);
3832
+ }
3833
+ };
3834
+
3835
+ // src/generators/i18n/i18n.ts
3836
+ var I18N_FILES = [
3837
+ {
3838
+ templatePath: "i18n/routing.ts.ejs",
3839
+ outputPath: "src/i18n/routing.ts"
3840
+ },
3841
+ {
3842
+ templatePath: "i18n/request.ts.ejs",
3843
+ outputPath: "src/i18n/request.ts"
3844
+ },
3845
+ {
3846
+ templatePath: "i18n/navigation.ts.ejs",
3847
+ outputPath: "src/i18n/navigation.ts"
3848
+ },
3849
+ {
3850
+ templatePath: "i18n/next-intl.d.ts.ejs",
3851
+ outputPath: "src/i18n/next-intl.d.ts"
3852
+ },
3853
+ {
3854
+ templatePath: "i18n/locale-layout.tsx.ejs",
3855
+ outputPath: "src/app/[locale]/layout.tsx"
3856
+ },
3857
+ {
3858
+ templatePath: "i18n/en.json.ejs",
3859
+ outputPath: "messages/en.json"
3860
+ },
3861
+ {
3862
+ templatePath: "i18n/ar.json.ejs",
3863
+ outputPath: "messages/ar.json"
3864
+ }
3865
+ ];
3866
+ function mergePackageJson8(existing) {
3867
+ const desiredFragment = {
3868
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
3869
+ };
3870
+ return mergeManagedPackageJson({
3871
+ existingContent: existing,
3872
+ desiredFragment,
3873
+ sortSections: ["dependencies"]
3874
+ });
3875
+ }
3876
+ async function writeI18nFiles(ctx) {
3877
+ for (const file of I18N_FILES) {
3878
+ const content = await ctx.template.renderFile(file.templatePath, {
3879
+ projectName: ctx.config.projectName
3880
+ });
3881
+ ctx.fs.write(file.outputPath, content);
3882
+ }
3883
+ }
3884
+ var i18nGenerator = {
3885
+ name: "i18n",
3886
+ dependencies: ["ui", "error-handling", "form-handling"],
3887
+ resolveDependencies: (config) => [
3888
+ ...config.authStrategy !== "none" ? ["auth"] : [],
3889
+ ...config.aiChat ? ["ai-chat"] : []
3890
+ ],
3891
+ condition: (config) => config.i18n === true,
3892
+ async execute(ctx) {
3893
+ ctx.fs.merge("package.json", mergePackageJson8);
3894
+ await writeI18nFiles(ctx);
3895
+ }
3896
+ };
3897
+
3898
+ // src/generators/pre-commit/pre-commit.ts
3899
+ var PHASE_19_GIT_INIT_COMMAND = "git init";
3900
+ function mergePackageJson9(existing, mode) {
3901
+ const desiredFragment = {
3902
+ scripts: buildPreCommitScripts(mode)
3903
+ };
3904
+ return mergeManagedPackageJson({
3905
+ existingContent: existing,
3906
+ desiredFragment,
3907
+ sortSections: []
3908
+ });
3909
+ }
3910
+ var preCommitGenerator = {
3911
+ name: "pre-commit",
3912
+ dependencies: ["eslint-prettier"],
3913
+ condition: (config) => config.preCommit,
3914
+ async execute(ctx) {
3915
+ const mode = inferPreCommitMode(ctx.targetDir);
3916
+ ctx.fs.write("prek.toml", await buildPrekTomlContents());
3917
+ ctx.fs.merge(
3918
+ "package.json",
3919
+ (existing) => mergePackageJson9(existing, mode)
3920
+ );
3921
+ if (mode === "standalone") {
3922
+ await ctx.exec(PHASE_19_GIT_INIT_COMMAND, { cwd: ctx.targetDir });
3923
+ }
3924
+ }
3925
+ };
3926
+
3927
+ // src/generators/proxy/proxy.ts
3928
+ var PROXY_OUTPUT_PATH = "src/proxy.ts";
3929
+ var proxyGenerator = {
3930
+ name: "proxy",
3931
+ dependencies: ["project-init"],
3932
+ resolveDependencies: (config) => [
3933
+ ...config.authStrategy !== "none" ? ["auth"] : [],
3934
+ ...config.i18n ? ["i18n"] : []
3935
+ ],
3936
+ async execute(ctx) {
3937
+ const content = await ctx.template.renderFile("proxy/proxy.ts.ejs", {
3938
+ hasAuth: ctx.config.authStrategy !== "none",
3939
+ hasI18n: ctx.config.i18n
3940
+ });
3941
+ ctx.fs.write(PROXY_OUTPUT_PATH, content);
3942
+ }
3943
+ };
3944
+
3945
+ // src/generators/state/state.ts
3946
+ var PHASE_9_STATE_AGENT_SKILL_COMMANDS = [
3947
+ buildSkillsAddCommand("https://github.com/pproenca/dot-skills", ["nuqs"]),
3948
+ buildSkillsAddCommand("https://github.com/prowler-cloud/prowler", [
3949
+ "zustand-5"
3950
+ ])
3951
+ ];
3952
+ function mergePackageJson10(existing) {
3953
+ const desiredFragment = {
3954
+ dependencies: structuredClone(PACKAGE_DEPS_BASELINE)
3955
+ };
3956
+ return mergeManagedPackageJson({
3957
+ existingContent: existing,
3958
+ desiredFragment,
3959
+ sortSections: ["dependencies"]
3960
+ });
3961
+ }
3962
+ var stateGenerator = {
3963
+ name: "state",
3964
+ dependencies: ["ui"],
3965
+ async execute(ctx) {
3966
+ ctx.fs.merge("package.json", mergePackageJson10);
3967
+ for (const command of PHASE_9_STATE_AGENT_SKILL_COMMANDS) {
3968
+ await ctx.exec(command, {
3969
+ cwd: ctx.targetDir,
3970
+ env: { ...AGENT_SKILLS_EXEC_ENV }
3971
+ });
3972
+ }
3973
+ }
3974
+ };
3975
+
3976
+ // src/generators/ui/ui.ts
3977
+ var SHADCN_PRESET = "nova";
3978
+ var REACT_SUSPENSE_IMPORT = 'import { Suspense } from "react";';
3979
+ var CLIENT_SIDE_WRAPPERS_IMPORT = 'import { ClientSideWrappers } from "./_providers/client-side-wrappers";';
3980
+ var SONNER_OPTIONAL_THEME_CAST = 'theme={theme as ToasterProps["theme"]}';
3981
+ var SONNER_NON_NULLABLE_THEME_CAST = 'theme={theme as NonNullable<ToasterProps["theme"]>}';
3982
+ var SELF_REFERENTIAL_FONT_SANS = "--font-sans: var(--font-sans);";
3983
+ var GEIST_FONT_SANS = "--font-sans: var(--font-geist-sans);";
3984
+ var FONT_HEADING_ALIAS = "--font-heading: var(--font-sans);";
3985
+ var GEIST_FONT_HEADING = "--font-heading: var(--font-geist-sans);";
3986
+ function buildShadcnInitCommand() {
3987
+ return `pnpm dlx shadcn@latest init --template next --preset ${SHADCN_PRESET} --yes`;
3988
+ }
3989
+ function buildShadcnAddAllCommand() {
3990
+ return "pnpm dlx shadcn@latest add --all --yes";
3991
+ }
3992
+ function buildShadcnSkillAddCommand() {
3993
+ return "pnpm dlx skills add shadcn/ui -y";
3994
+ }
3995
+ function mergeRootLayoutWithClientSideWrappers(existing) {
3996
+ if (existing.includes("<ClientSideWrappers>")) return existing;
3997
+ const importMarker = 'import "./globals.css";\n';
3998
+ if (!existing.includes(importMarker)) {
3999
+ throw new Error("Could not find globals.css import in root layout.");
4000
+ }
4001
+ const childrenMarker = [
4002
+ " {/* Scale Stack: wrap with NuqsAdapter and other root providers under src/app/_providers/ */}",
4003
+ " {children}"
4004
+ ].join("\n");
4005
+ if (!existing.includes(childrenMarker)) {
4006
+ throw new Error("Could not find root layout children slot.");
4007
+ }
4008
+ return existing.replace(
4009
+ importMarker,
4010
+ `${importMarker}${REACT_SUSPENSE_IMPORT}
4011
+ ${CLIENT_SIDE_WRAPPERS_IMPORT}
4012
+ `
4013
+ ).replace(
4014
+ childrenMarker,
4015
+ [
4016
+ " <Suspense>",
4017
+ " <ClientSideWrappers>{children}</ClientSideWrappers>",
4018
+ " </Suspense>"
4019
+ ].join("\n")
4020
+ );
4021
+ }
4022
+ function fixSonnerExactOptionalTheme(existing) {
4023
+ if (existing.includes(SONNER_NON_NULLABLE_THEME_CAST)) return existing;
4024
+ return existing.replace(
4025
+ SONNER_OPTIONAL_THEME_CAST,
4026
+ SONNER_NON_NULLABLE_THEME_CAST
4027
+ );
4028
+ }
4029
+ function fixGeneratedFontVariables(existing) {
4030
+ return existing.replace(SELF_REFERENTIAL_FONT_SANS, GEIST_FONT_SANS).replace(FONT_HEADING_ALIAS, GEIST_FONT_HEADING);
4031
+ }
4032
+ async function writeUiFiles(ctx) {
4033
+ const [page, clientSideWrappers] = await Promise.all([
4034
+ ctx.template.renderFile("ui/page.tsx.ejs", {
4035
+ projectName: ctx.config.projectName,
4036
+ hasAuth: ctx.config.authStrategy !== "none",
4037
+ hasAiChat: ctx.config.aiChat,
4038
+ isStatefulAuth: ctx.config.authStrategy === "stateful",
4039
+ i18n: ctx.config.i18n
4040
+ }),
4041
+ ctx.template.renderFile("ui/client-side-wrappers.tsx.ejs", {
4042
+ hasAnalyticsProvider: ctx.config.analytics === "plausible"
4043
+ })
4044
+ ]);
4045
+ ctx.fs.write(localizeAppRoutePath("src/app/page.tsx", ctx.config.i18n), page);
4046
+ ctx.fs.write(
4047
+ "src/app/_providers/client-side-wrappers.tsx",
4048
+ clientSideWrappers
4049
+ );
4050
+ }
4051
+ async function wireRootLayout(ctx) {
4052
+ ctx.fs.merge("src/app/layout.tsx", mergeRootLayoutWithClientSideWrappers);
4053
+ }
4054
+ async function patchGeneratedComponentFiles(ctx) {
4055
+ ctx.fs.merge("src/app/globals.css", fixGeneratedFontVariables);
4056
+ ctx.fs.merge("src/components/ui/sonner.tsx", fixSonnerExactOptionalTheme);
4057
+ }
4058
+ var uiGenerator = {
4059
+ name: "ui",
4060
+ dependencies: ["agent-skills-init"],
4061
+ async execute(ctx) {
4062
+ await ctx.exec(buildShadcnInitCommand(), { cwd: ctx.targetDir });
4063
+ await ctx.exec(buildShadcnAddAllCommand(), { cwd: ctx.targetDir });
4064
+ await ctx.exec(buildShadcnSkillAddCommand(), { cwd: ctx.targetDir });
4065
+ await patchGeneratedComponentFiles(ctx);
4066
+ await writeUiFiles(ctx);
4067
+ await wireRootLayout(ctx);
4068
+ }
4069
+ };
4070
+
4071
+ // src/generators/register.ts
4072
+ registerGenerator(projectInitGenerator);
4073
+ registerGenerator(proxyGenerator);
4074
+ registerGenerator(agentSkillsInitGenerator);
4075
+ registerGenerator(envManagementGenerator);
4076
+ registerGenerator(eslintPrettierGenerator);
4077
+ registerGenerator(uiGenerator);
4078
+ registerGenerator(stateGenerator);
4079
+ registerGenerator(errorHandlingGenerator);
4080
+ registerGenerator(ormGenerator);
4081
+ registerGenerator(authGenerator);
4082
+ registerGenerator(serverActionsGenerator);
4083
+ registerGenerator(formHandlingGenerator);
4084
+ registerGenerator(preCommitGenerator);
4085
+ registerGenerator(dockerGenerator);
4086
+ registerGenerator(ciGenerator);
4087
+ registerGenerator(analyticsGenerator);
4088
+ registerGenerator(aiChatGenerator);
4089
+ registerGenerator(i18nGenerator);
4090
+ registerGenerator(aiAssistantGenerator);
4091
+
4092
+ // src/cli/ui/banner.ts
4093
+ import gradient2 from "gradient-string";
4094
+ import pc7 from "picocolors";
4095
+ var LOGO = [
4096
+ " \xB0 ",
4097
+ "\u2554\u2550\u2557 \u2566\u2554\u2550 \u2554\u2550\u2557 \u2566 \u2554\u2550\u2557 \u2554\u2566\u2557 \u2554\u2550\u2557 \u2566\u2554\u2550 \u2566\u2554\u2550",
4098
+ "\u255A\u2550\u2557 \u2560\u2569\u2557 \u2560\u2550\u2563 \u2551 \u255A\u2550\u2557 \u2551 \u2560\u2550\u2563 \u2560\u2569\u2557 \u2560\u2569\u2557",
4099
+ "\u255A\u2550\u255D \u2569 \u2569 \u2569 \u2569 \u2569\u2550\u255D \u255A\u2550\u255D \u2569 \u2569 \u2569 \u2569 \u2569 \u2569 \u2569"
4100
+ ];
4101
+ var TAGLINE = "Project foundation w/ smart-fittings";
4102
+ var INNER = 43;
4103
+ var CONTENT_W = INNER - 6;
4104
+ var BORDER_TOP = ` \u256D${"\u2500".repeat(INNER)}\u256E`;
4105
+ var BORDER_BOT = ` \u2570${"\u2500".repeat(INNER)}\u256F`;
4106
+ var EMPTY_ROW = ` \u2502${" ".repeat(INNER)}\u2502`;
4107
+ function boxRow(content, rawLen) {
4108
+ const pad = CONTENT_W - rawLen;
4109
+ return ` \u2502 ${content}${" ".repeat(Math.max(0, pad))} \u2502`;
4110
+ }
4111
+ function logoBoxLine(colored, rawLen) {
4112
+ return pc7.dim(" \u2502") + " " + colored + " ".repeat(Math.max(0, CONTENT_W - rawLen)) + pc7.dim(" \u2502");
4113
+ }
4114
+ var SWEEP_GRADIENTS = [
4115
+ gradient2(["#6B6B2E", "#2E4A6B"]),
4116
+ gradient2(["#B8A31A", "#1A6B9E"]),
4117
+ ikeaGradient
4118
+ ];
4119
+ var PARTS = "\u2550\u2554\u2557\u2566\u2560\u2569\u2551\u255A\u255D\u2563\u256C\u2500\u2502\u250C\u2510\u2514\u2518\u252C\u2534\u251C\u2524\u253C";
4120
+ function scrambleLine(line, resolvedUpTo) {
4121
+ return Array.from(line).map((ch, i) => {
4122
+ if (i < resolvedUpTo) return ch;
4123
+ if (ch === " ") return " ";
4124
+ return PARTS[Math.floor(Math.random() * PARTS.length)];
4125
+ }).join("");
4126
+ }
4127
+ function progressBar(filled, total, width = 8) {
4128
+ const n = Math.round(filled / total * width);
4129
+ return "\u2588".repeat(n) + "\u2591".repeat(width - n);
4130
+ }
4131
+ var BOX_CONTENT_LINES = 8;
4132
+ async function animateBanner() {
4133
+ const logoText = LOGO.join("\n");
4134
+ const charCount = LOGO[0].length;
4135
+ const FRAMES = 10;
4136
+ const RATTLE = 2;
4137
+ console.log();
4138
+ console.log(pc7.dim(BORDER_TOP));
4139
+ console.log(pc7.dim(EMPTY_ROW));
4140
+ for (let f = 0; f <= FRAMES; f++) {
4141
+ if (f > 0) cursorUp(BOX_CONTENT_LINES);
4142
+ const progress = f <= RATTLE ? 0 : (f - RATTLE) / (FRAMES - RATTLE);
4143
+ const resolved = Math.floor(progress * charCount);
4144
+ const done = resolved >= charCount;
4145
+ for (const line of LOGO) {
4146
+ const text2 = done ? line : scrambleLine(line, resolved);
4147
+ console.log(logoBoxLine(pc7.dim(text2), line.length));
4148
+ }
4149
+ console.log(pc7.dim(EMPTY_ROW));
4150
+ if (done) {
4151
+ console.log(boxRow(ikea.heading(TAGLINE), TAGLINE.length));
4152
+ } else {
4153
+ const bar = progressBar(Math.max(0, f - RATTLE), FRAMES - RATTLE);
4154
+ const plain = `\u25B8 assembling ${bar}`;
4155
+ console.log(
4156
+ boxRow(`${ikea.bullet} assembling ${pc7.dim(bar)}`, plain.length)
4157
+ );
4158
+ }
4159
+ console.log(pc7.dim(EMPTY_ROW));
4160
+ console.log(pc7.dim(BORDER_BOT));
4161
+ await sleep(f <= RATTLE ? 120 : done ? 200 : 60);
4162
+ }
4163
+ for (const grad of SWEEP_GRADIENTS) {
4164
+ cursorUp(BOX_CONTENT_LINES);
4165
+ const colored = grad.multiline(logoText).split("\n");
4166
+ for (let i = 0; i < colored.length; i++) {
4167
+ console.log(logoBoxLine(colored[i], LOGO[i].length));
4168
+ }
4169
+ console.log(pc7.dim(EMPTY_ROW));
4170
+ console.log(boxRow(ikea.heading(TAGLINE), TAGLINE.length));
4171
+ console.log(pc7.dim(EMPTY_ROW));
4172
+ console.log(pc7.dim(BORDER_BOT));
4173
+ await sleep(70);
4174
+ }
4175
+ console.log();
4176
+ }
4177
+ async function animateDescription() {
4178
+ const lines = [
4179
+ "Stop the digital clutter. Flat-packed code efficiency",
4180
+ "delivered straight to your terminal."
4181
+ ];
4182
+ for (const line of lines) {
4183
+ process.stdout.write(" ");
4184
+ await typewriter(pc7.dim(line), 6);
4185
+ console.log();
4186
+ }
4187
+ console.log();
4188
+ }
4189
+ function printBanner() {
4190
+ const coloredLines = ikeaGradient.multiline(LOGO.join("\n")).split("\n");
4191
+ console.log();
4192
+ console.log(pc7.dim(BORDER_TOP));
4193
+ console.log(pc7.dim(EMPTY_ROW));
4194
+ for (let i = 0; i < coloredLines.length; i++) {
4195
+ console.log(logoBoxLine(coloredLines[i], LOGO[i].length));
4196
+ }
4197
+ console.log(pc7.dim(EMPTY_ROW));
4198
+ console.log(boxRow(ikea.heading(TAGLINE), TAGLINE.length));
4199
+ console.log(pc7.dim(EMPTY_ROW));
4200
+ console.log(pc7.dim(BORDER_BOT));
4201
+ console.log();
4202
+ }
4203
+ function printDescription() {
4204
+ console.log(
4205
+ ikea.muted(
4206
+ " Stop the digital clutter. Flat-packed code efficiency\n delivered straight to your terminal."
4207
+ )
4208
+ );
4209
+ console.log();
4210
+ }
4211
+
4212
+ // src/cli/index.ts
4213
+ var pkgVersion = getCliVersion();
4214
+ var argv = process.argv.slice(2);
4215
+ var wantsHelp = argv.includes("--help") || argv.includes("-h") || argv.includes("--version") || argv.includes("-V");
4216
+ if (process.stdout.isTTY && !wantsHelp) {
4217
+ await animateBanner();
4218
+ await animateDescription();
4219
+ } else if (process.stdout.isTTY) {
4220
+ printBanner();
4221
+ printDescription();
4222
+ }
4223
+ var program = new Command().name("scale-stack").version(pkgVersion);
4224
+ registerInitCommand(program);
4225
+ registerSyncCommand(program);
24
4226
  program.parse();