scale-stack 0.0.1-alpha.2 → 0.0.2

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