superslate 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +797 -531
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,102 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
- var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
- target = mod != null ? __create(__getProtoOf(mod)) : {};
22
- const to = isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
- if (mod && typeof mod === "object" || typeof mod === "function") {
24
- for (let key of __getOwnPropNames(mod))
25
- if (!__hasOwnProp.call(to, key))
26
- __defProp(to, key, {
27
- get: __accessProp.bind(mod, key),
28
- enumerable: true
29
- });
30
- }
31
- if (canCache)
32
- cache.set(mod, to);
33
- return to;
34
- };
35
- var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
36
-
37
- // node_modules/sisteransi/src/index.js
38
- var require_src = __commonJS(function(exports, module) {
39
- var ESC = "\x1B";
40
- var CSI = `${ESC}[`;
41
- var beep = "\x07";
42
- var cursor = {
43
- to(x, y) {
44
- if (!y)
45
- return `${CSI}${x + 1}G`;
46
- return `${CSI}${y + 1};${x + 1}H`;
47
- },
48
- move(x, y) {
49
- let ret = "";
50
- if (x < 0)
51
- ret += `${CSI}${-x}D`;
52
- else if (x > 0)
53
- ret += `${CSI}${x}C`;
54
- if (y < 0)
55
- ret += `${CSI}${-y}A`;
56
- else if (y > 0)
57
- ret += `${CSI}${y}B`;
58
- return ret;
59
- },
60
- up: (count = 1) => `${CSI}${count}A`,
61
- down: (count = 1) => `${CSI}${count}B`,
62
- forward: (count = 1) => `${CSI}${count}C`,
63
- backward: (count = 1) => `${CSI}${count}D`,
64
- nextLine: (count = 1) => `${CSI}E`.repeat(count),
65
- prevLine: (count = 1) => `${CSI}F`.repeat(count),
66
- left: `${CSI}G`,
67
- hide: `${CSI}?25l`,
68
- show: `${CSI}?25h`,
69
- save: `${ESC}7`,
70
- restore: `${ESC}8`
71
- };
72
- var scroll = {
73
- up: (count = 1) => `${CSI}S`.repeat(count),
74
- down: (count = 1) => `${CSI}T`.repeat(count)
75
- };
76
- var erase = {
77
- screen: `${CSI}2J`,
78
- up: (count = 1) => `${CSI}1J`.repeat(count),
79
- down: (count = 1) => `${CSI}J`.repeat(count),
80
- line: `${CSI}2K`,
81
- lineEnd: `${CSI}K`,
82
- lineStart: `${CSI}1K`,
83
- lines(count) {
84
- let clear = "";
85
- for (let i = 0;i < count; i++)
86
- clear += this.line + (i < count - 1 ? cursor.up() : "");
87
- if (count)
88
- clear += cursor.left;
89
- return clear;
90
- }
91
- };
92
- module.exports = { cursor, scroll, erase, beep };
93
- });
94
3
 
95
4
  // node_modules/@clack/core/dist/index.mjs
96
5
  import { styleText } from "node:util";
97
6
  import { stdout, stdin } from "node:process";
98
- import * as l from "node:readline";
99
- import l__default from "node:readline";
7
+ import * as f$1 from "node:readline";
8
+ import f__default from "node:readline";
100
9
 
101
10
  // node_modules/fast-string-truncated-width/dist/utils.js
102
11
  var getCodePointsLength = (() => {
@@ -439,14 +348,66 @@ function wrapAnsi(string, columns, options) {
439
348
  `);
440
349
  }
441
350
 
351
+ // node_modules/sisteransi/dist/index.js
352
+ var ESC2 = "\x1B";
353
+ var CSI2 = `${ESC2}[`;
354
+ var cursor = {
355
+ to(x, y) {
356
+ if (!y)
357
+ return `${CSI2}${x + 1}G`;
358
+ return `${CSI2}${y + 1};${x + 1}H`;
359
+ },
360
+ move(x, y) {
361
+ let ret = "";
362
+ if (x < 0)
363
+ ret += `${CSI2}${-x}D`;
364
+ else if (x > 0)
365
+ ret += `${CSI2}${x}C`;
366
+ if (y < 0)
367
+ ret += `${CSI2}${-y}A`;
368
+ else if (y > 0)
369
+ ret += `${CSI2}${y}B`;
370
+ return ret;
371
+ },
372
+ up: (count = 1) => `${CSI2}${count}A`,
373
+ down: (count = 1) => `${CSI2}${count}B`,
374
+ forward: (count = 1) => `${CSI2}${count}C`,
375
+ backward: (count = 1) => `${CSI2}${count}D`,
376
+ nextLine: (count = 1) => `${CSI2}E`.repeat(count),
377
+ prevLine: (count = 1) => `${CSI2}F`.repeat(count),
378
+ left: `${CSI2}G`,
379
+ hide: `${CSI2}?25l`,
380
+ show: `${CSI2}?25h`,
381
+ save: `${ESC2}7`,
382
+ restore: `${ESC2}8`
383
+ };
384
+ var erase = {
385
+ screen: `${CSI2}2J`,
386
+ up: (count = 1) => `${CSI2}1J`.repeat(count),
387
+ down: (count = 1) => `${CSI2}J`.repeat(count),
388
+ line: `${CSI2}2K`,
389
+ lineEnd: `${CSI2}K`,
390
+ lineStart: `${CSI2}1K`,
391
+ lines(count) {
392
+ let clear = "";
393
+ for (let i = 0;i < count; i++)
394
+ clear += this.line + (i < count - 1 ? cursor.up() : "");
395
+ if (count)
396
+ clear += cursor.left;
397
+ return clear;
398
+ }
399
+ };
400
+ var clear = {
401
+ screen: `${ESC2}c`
402
+ };
403
+
442
404
  // node_modules/@clack/core/dist/index.mjs
443
- var import_sisteransi = __toESM(require_src(), 1);
444
405
  import { ReadStream } from "node:tty";
445
- function findCursor(s, o, l) {
406
+ function findCursor(o, s, l) {
446
407
  if (!l.some((r) => !r.disabled))
447
- return s;
448
- const t = s + o, n = Math.max(l.length - 1, 0), e = t < 0 ? n : t > n ? 0 : t;
449
- return l[e]?.disabled ? findCursor(e, o < 0 ? -1 : 1, l) : e;
408
+ return o;
409
+ const t = o + s, n = Math.max(l.length - 1, 0), e = t < 0 ? n : t > n ? 0 : t;
410
+ return l[e]?.disabled ? findCursor(e, s < 0 ? -1 : 1, l) : e;
450
411
  }
451
412
  var a$1 = ["up", "down", "left", "right", "space", "enter", "cancel"];
452
413
  var t = [
@@ -536,43 +497,43 @@ function block({
536
497
  overwrite: o = true,
537
498
  hideCursor: n = true
538
499
  } = {}) {
539
- const s = l.createInterface({
500
+ const s = f$1.createInterface({
540
501
  input: e,
541
502
  output: r,
542
503
  prompt: "",
543
504
  tabSize: 1
544
505
  });
545
- l.emitKeypressEvents(e, s), e instanceof ReadStream && e.isTTY && e.setRawMode(true);
546
- const t = (f, { name: a, sequence: w }) => {
547
- const c = String(f);
548
- if (isActionKey([c, a, w], "cancel")) {
549
- n && r.write(import_sisteransi.cursor.show), process.exit(0);
506
+ f$1.emitKeypressEvents(e, s), e instanceof ReadStream && e.isTTY && e.setRawMode(true);
507
+ const t = (l, { name: i, sequence: p }) => {
508
+ const w = String(l);
509
+ if (isActionKey([w, i, p], "cancel")) {
510
+ n && r.write(cursor.show), process.exit(0);
550
511
  return;
551
512
  }
552
513
  if (!o)
553
514
  return;
554
- const i = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0;
555
- l.moveCursor(r, i, m, () => {
556
- l.clearLine(r, 1, () => {
515
+ const c = i === "return" ? 0 : -1, a = i === "return" ? -1 : 0;
516
+ f$1.moveCursor(r, c, a, () => {
517
+ f$1.clearLine(r, 1, () => {
557
518
  e.once("keypress", t);
558
519
  });
559
520
  });
560
521
  };
561
- return n && r.write(import_sisteransi.cursor.hide), e.once("keypress", t), () => {
562
- e.off("keypress", t), n && r.write(import_sisteransi.cursor.show), e instanceof ReadStream && e.isTTY && !R && e.setRawMode(false), s.terminal = false, s.close();
522
+ return n && r.write(cursor.hide), e.once("keypress", t), () => {
523
+ e.off("keypress", t), n && r.write(cursor.show), e instanceof ReadStream && e.isTTY && !R && e.setRawMode(false), s.terminal = false, s.close();
563
524
  };
564
525
  }
565
526
  var getColumns = (e) => ("columns" in e) && typeof e.columns == "number" ? e.columns : 80;
566
527
  var getRows = (e) => ("rows" in e) && typeof e.rows == "number" ? e.rows : 20;
567
528
  function wrapTextWithPrefix(e, r, o, n = o, s = o, t) {
568
- const f = getColumns(e ?? stdout);
569
- return wrapAnsi(r, f - o.length, {
529
+ const l = getColumns(e ?? stdout), i = dist_default2(o);
530
+ return wrapAnsi(r, l - i, {
570
531
  hard: true,
571
532
  trim: false
572
533
  }).split(`
573
- `).map((c, i, m) => {
574
- const d = t ? t(c, i) : c;
575
- return i === 0 ? `${n}${d}` : i === m.length - 1 ? `${s}${d}` : `${o}${d}`;
534
+ `).map((c, a, g) => {
535
+ const m = t ? t(c, a) : c;
536
+ return a === 0 ? `${n}${m}` : a === g.length - 1 ? `${s}${m}` : `${o}${m}`;
576
537
  }).join(`
577
538
  `);
578
539
  }
@@ -583,8 +544,7 @@ function runValidation(e, a) {
583
544
  }
584
545
  return e(a);
585
546
  }
586
-
587
- class y {
547
+ var C$1 = class C {
588
548
  input;
589
549
  output;
590
550
  _abortSignal;
@@ -635,16 +595,16 @@ class y {
635
595
  this.state = "cancel", this.close();
636
596
  }, { once: true });
637
597
  }
638
- this.rl = l__default.createInterface({
598
+ this.rl = f__default.createInterface({
639
599
  input: this.input,
640
600
  tabSize: 2,
641
601
  prompt: "",
642
602
  escapeCodeTimeout: 50,
643
603
  terminal: true
644
604
  }), this.rl.prompt(), this.opts.initialUserInput !== undefined && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
645
- this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
605
+ this.output.write(cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(this.value);
646
606
  }), this.once("cancel", () => {
647
- this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
607
+ this.output.write(cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t(CANCEL_SYMBOL);
648
608
  });
649
609
  });
650
610
  }
@@ -681,18 +641,18 @@ class y {
681
641
  `), setRawMode(this.input, false), this.rl?.close(), this.rl = undefined, this.emit(`${this.state}`, this.value), this.unsubscribe();
682
642
  }
683
643
  restoreCursor() {
684
- const t = wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(`
644
+ const t = wrapAnsi(this._prevFrame, getColumns(this.output), { hard: true, trim: false }).split(`
685
645
  `).length - 1;
686
- this.output.write(import_sisteransi.cursor.move(-999, t * -1));
646
+ this.output.write(cursor.move(-999, t * -1));
687
647
  }
688
648
  render() {
689
- const t = wrapAnsi(this._render(this) ?? "", process.stdout.columns, {
649
+ const t = wrapAnsi(this._render(this) ?? "", getColumns(this.output), {
690
650
  hard: true,
691
651
  trim: false
692
652
  });
693
653
  if (t !== this._prevFrame) {
694
654
  if (this.state === "initial")
695
- this.output.write(import_sisteransi.cursor.hide);
655
+ this.output.write(cursor.hide);
696
656
  else {
697
657
  const e = diffLines(this._prevFrame, t), i = getRows(this.output);
698
658
  if (this.restoreCursor(), e) {
@@ -703,33 +663,33 @@ class y {
703
663
  return;
704
664
  }
705
665
  if (e.lines.length === 1) {
706
- this.output.write(import_sisteransi.cursor.move(0, n - r)), this.output.write(import_sisteransi.erase.lines(1));
666
+ this.output.write(cursor.move(0, n - r)), this.output.write(erase.lines(1));
707
667
  const o = t.split(`
708
668
  `);
709
- this.output.write(o[n]), this._prevFrame = t, this.output.write(import_sisteransi.cursor.move(0, o.length - n - 1));
669
+ this.output.write(o[n]), this._prevFrame = t, this.output.write(cursor.move(0, o.length - n - 1));
710
670
  return;
711
671
  } else if (e.lines.length > 1) {
712
672
  if (s < r)
713
673
  n = s;
714
674
  else {
715
675
  const h = n - r;
716
- h > 0 && this.output.write(import_sisteransi.cursor.move(0, h));
676
+ h > 0 && this.output.write(cursor.move(0, h));
717
677
  }
718
- this.output.write(import_sisteransi.erase.down());
719
- const f = t.split(`
678
+ this.output.write(erase.down());
679
+ const m = t.split(`
720
680
  `).slice(n);
721
- this.output.write(f.join(`
681
+ this.output.write(m.join(`
722
682
  `)), this._prevFrame = t;
723
683
  return;
724
684
  }
725
685
  }
726
- this.output.write(import_sisteransi.erase.down());
686
+ this.output.write(erase.down());
727
687
  }
728
688
  this.output.write(t), this.state === "initial" && (this.state = "active"), this._prevFrame = t;
729
689
  }
730
690
  }
731
- }
732
- class r extends y {
691
+ };
692
+ class r extends C$1 {
733
693
  get cursor() {
734
694
  return this.value ? 0 : 1;
735
695
  }
@@ -740,13 +700,13 @@ class r extends y {
740
700
  super(t, false), this.value = !!t.initialValue, this.on("userInput", () => {
741
701
  this.value = this._value;
742
702
  }), this.on("confirm", (i) => {
743
- this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
703
+ this.output.write(cursor.move(0, -1)), this.value = i, this.state = "submit", this.close();
744
704
  }), this.on("cursor", () => {
745
705
  this.value = !this.value;
746
706
  });
747
707
  }
748
708
  }
749
- class a extends y {
709
+ class a extends C$1 {
750
710
  options;
751
711
  cursor = 0;
752
712
  get _value() {
@@ -774,8 +734,8 @@ class a extends y {
774
734
  constructor(e) {
775
735
  super(e, false), this.options = e.options, this.value = [...e.initialValues ?? []];
776
736
  const i = Math.max(this.options.findIndex(({ value: t }) => t === e.cursorAt), 0);
777
- this.cursor = this.options[i]?.disabled ? findCursor(i, 1, this.options) : i, this.on("key", (t, l) => {
778
- l.name === "a" && this.toggleAll(), l.name === "i" && this.toggleInvert();
737
+ this.cursor = this.options[i]?.disabled ? findCursor(i, 1, this.options) : i, this.on("key", (t, s) => {
738
+ s.name === "a" && this.toggleAll(), s.name === "i" && this.toggleInvert(), s.name === undefined && t === " " && this.toggleValue();
779
739
  }), this.on("cursor", (t) => {
780
740
  switch (t) {
781
741
  case "left":
@@ -793,7 +753,8 @@ class a extends y {
793
753
  });
794
754
  }
795
755
  }
796
- var u$1 = class u extends y {
756
+
757
+ class u extends C$1 {
797
758
  _mask = "•";
798
759
  get cursor() {
799
760
  return this._cursor;
@@ -820,8 +781,8 @@ var u$1 = class u extends y {
820
781
  this.value === undefined && (this.value = "");
821
782
  });
822
783
  }
823
- };
824
- var n$1 = class n extends y {
784
+ }
785
+ var n$1 = class n extends C$1 {
825
786
  options;
826
787
  cursor = 0;
827
788
  get _selectedValue() {
@@ -849,7 +810,7 @@ var n$1 = class n extends y {
849
810
  });
850
811
  }
851
812
  };
852
- class n extends y {
813
+ class n extends C$1 {
853
814
  get userInputWithCursor() {
854
815
  if (this.state === "submit")
855
816
  return this.userInput;
@@ -877,7 +838,6 @@ class n extends y {
877
838
  // node_modules/@clack/prompts/dist/index.mjs
878
839
  import { styleText as styleText2, stripVTControlCharacters } from "node:util";
879
840
  import process$1 from "node:process";
880
- var import_sisteransi2 = __toESM(require_src(), 1);
881
841
  function isUnicodeSupported() {
882
842
  if (process$1.platform !== "win32") {
883
843
  return process$1.env.TERM !== "linux";
@@ -886,8 +846,8 @@ function isUnicodeSupported() {
886
846
  }
887
847
  var unicode = isUnicodeSupported();
888
848
  var isCI = () => process.env.CI === "true";
889
- var isTTY = (o) => o.isTTY === true;
890
- var unicodeOr = (o, e) => unicode ? o : e;
849
+ var isTTY = (e) => e.isTTY === true;
850
+ var unicodeOr = (e, o) => unicode ? e : o;
891
851
  var S_STEP_ACTIVE = unicodeOr("◆", "*");
892
852
  var S_STEP_CANCEL = unicodeOr("■", "x");
893
853
  var S_STEP_ERROR = unicodeOr("▲", "x");
@@ -913,8 +873,8 @@ var S_INFO = unicodeOr("●", "•");
913
873
  var S_SUCCESS = unicodeOr("◆", "*");
914
874
  var S_WARN = unicodeOr("▲", "!");
915
875
  var S_ERROR = unicodeOr("■", "x");
916
- var symbol = (o) => {
917
- switch (o) {
876
+ var symbol = (e) => {
877
+ switch (e) {
918
878
  case "initial":
919
879
  case "active":
920
880
  return styleText2("cyan", S_STEP_ACTIVE);
@@ -1028,7 +988,7 @@ ${styleText2("gray", S_BAR_END)} ` : "";
1028
988
 
1029
989
  `);
1030
990
  };
1031
- var password = (e) => new u$1({
991
+ var password = (e) => new u({
1032
992
  validate: e.validate,
1033
993
  mask: e.mask ?? S_PASSWORD_MASK,
1034
994
  signal: e.signal,
@@ -1064,85 +1024,103 @@ ${n}
1064
1024
  }
1065
1025
  }
1066
1026
  }).prompt();
1067
- var W = (l) => styleText2("magenta", l);
1027
+ var Y = 30000;
1028
+ var Z = (c) => styleText2("magenta", c);
1068
1029
  var spinner = ({
1069
- indicator: l = "dots",
1070
- onCancel: h,
1030
+ indicator: c = "dots",
1031
+ onCancel: b,
1071
1032
  output: n = process.stdout,
1072
- cancelMessage: G,
1073
- errorMessage: O,
1074
- frames: E = unicode ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
1075
- delay: F = unicode ? 80 : 120,
1076
- signal: m,
1077
- ...I
1033
+ cancelMessage: _,
1034
+ errorMessage: C,
1035
+ frames: M = unicode ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
1036
+ delay: O = unicode ? 80 : 120,
1037
+ signal: g,
1038
+ ...E
1078
1039
  } = {}) => {
1079
- const u = isCI();
1080
- let M, T, d = false, S = false, s = "", p, w = performance.now();
1081
- const x = getColumns(n), k = I?.styleFrame ?? W, g = (e) => {
1082
- const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
1083
- S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
1084
- }, f = () => g(2), i = () => g(1), A = () => {
1085
- process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
1086
- }, H = () => {
1087
- process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
1088
- }, y = () => {
1089
- if (p === undefined)
1040
+ const u = isCI(), h = isAccessible(E.accessible);
1041
+ let x, I, p = false, l = false, r = "", v, w = performance.now();
1042
+ const y = getColumns(n), F = E?.styleFrame ?? Z, S = (e) => {
1043
+ const s = e > 1 ? C ?? settings.messages.error : _ ?? settings.messages.cancel;
1044
+ l = e === 1, p && (m(s, e), l && typeof b == "function" && b());
1045
+ }, $ = () => S(2), a = () => S(1), H = () => {
1046
+ process.on("uncaughtExceptionMonitor", $), process.on("unhandledRejection", $), process.on("SIGINT", a), process.on("SIGTERM", a), process.on("exit", S), g && g.addEventListener("abort", a);
1047
+ }, B = () => {
1048
+ process.removeListener("uncaughtExceptionMonitor", $), process.removeListener("unhandledRejection", $), process.removeListener("SIGINT", a), process.removeListener("SIGTERM", a), process.removeListener("exit", S), g && g.removeEventListener("abort", a);
1049
+ }, R = () => {
1050
+ if (v === undefined)
1090
1051
  return;
1091
1052
  u && n.write(`
1092
1053
  `);
1093
- const r = wrapAnsi(p, x, {
1054
+ const s = wrapAnsi(v, y, {
1094
1055
  hard: true,
1095
1056
  trim: false
1096
1057
  }).split(`
1097
1058
  `);
1098
- r.length > 1 && n.write(import_sisteransi2.cursor.up(r.length - 1)), n.write(import_sisteransi2.cursor.to(0)), n.write(import_sisteransi2.erase.down());
1099
- }, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
1100
- const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
1101
- return t > 0 ? `[${t}m ${o}s]` : `[${o}s]`;
1102
- }, N = I.withGuide ?? settings.withGuide, P = (e = "") => {
1103
- d = true, M = block({ output: n }), s = C(e), w = performance.now(), N && n.write(`${styleText2("gray", S_BAR)}
1059
+ s.length > 1 && n.write(cursor.up(s.length - 1)), n.write(cursor.to(0)), n.write(erase.down());
1060
+ }, L = (e) => e.replace(/\.+$/, ""), T = (e) => {
1061
+ const s = (performance.now() - e) / 1000, i = Math.floor(s / 60), t = Math.floor(s % 60);
1062
+ return i > 0 ? `[${i}m ${t}s]` : `[${t}s]`;
1063
+ }, N = E.withGuide ?? settings.withGuide, P = (e = "") => {
1064
+ if (p = true, l = false, r = L(e), w = performance.now(), H(), h) {
1065
+ r !== "" && n.write(`${r}
1066
+ `), I = setInterval(() => {
1067
+ n.write(r === "" ? `still working
1068
+ ` : `still working: ${r}
1069
+ `);
1070
+ }, Y);
1071
+ return;
1072
+ }
1073
+ x = block({ output: n }), N && n.write(`${styleText2("gray", S_BAR)}
1104
1074
  `);
1105
- let r = 0, t = 0;
1106
- A(), T = setInterval(() => {
1107
- if (u && s === p)
1075
+ let s = 0, i = 0;
1076
+ I = setInterval(() => {
1077
+ if (u && r === v)
1108
1078
  return;
1109
- y(), p = s;
1110
- const o = k(E[r]);
1111
- let v;
1079
+ R(), v = r;
1080
+ const t = F(M[s]);
1081
+ let o;
1112
1082
  if (u)
1113
- v = `${o} ${s}...`;
1114
- else if (l === "timer")
1115
- v = `${o} ${s} ${_(w)}`;
1083
+ o = `${t} ${r}...`;
1084
+ else if (c === "timer")
1085
+ o = `${t} ${r} ${T(w)}`;
1116
1086
  else {
1117
- const B = ".".repeat(Math.floor(t)).slice(0, 3);
1118
- v = `${o} ${s}${B}`;
1087
+ const j = ".".repeat(Math.floor(i)).slice(0, 3);
1088
+ o = `${t} ${r}${j}`;
1119
1089
  }
1120
- const j = wrapAnsi(v, x, {
1090
+ const D = wrapAnsi(o, y, {
1121
1091
  hard: true,
1122
1092
  trim: false
1123
1093
  });
1124
- n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
1125
- }, F);
1126
- }, a = (e = "", r = 0, t = false) => {
1127
- if (!d)
1128
- return;
1129
- d = false, clearInterval(T), y();
1130
- const o = r === 0 ? styleText2("green", S_STEP_SUBMIT) : r === 1 ? styleText2("red", S_STEP_CANCEL) : styleText2("red", S_STEP_ERROR);
1131
- s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
1132
- `) : n.write(`${o} ${s}
1133
- `)), H(), M();
1094
+ n.write(D), s = s + 1 < M.length ? s + 1 : 0, i = i < 4 ? i + 0.125 : 0;
1095
+ }, O);
1096
+ }, m = (e = "", s = 0, i = false) => {
1097
+ if (p) {
1098
+ if (p = false, l = s === 1, clearInterval(I), h || R(), r = e ?? r, !i)
1099
+ if (h) {
1100
+ const t = s === 1 ? _ ?? settings.messages.cancel : s === 2 ? C ?? settings.messages.error : "Done", o = r || t;
1101
+ c === "timer" ? n.write(`${o} ${T(w)}
1102
+ `) : n.write(`${o}
1103
+ `);
1104
+ } else {
1105
+ const t = s === 0 ? styleText2("green", S_STEP_SUBMIT) : s === 1 ? styleText2("red", S_STEP_CANCEL) : styleText2("red", S_STEP_ERROR);
1106
+ c === "timer" ? n.write(`${t} ${r} ${T(w)}
1107
+ `) : n.write(`${t} ${r}
1108
+ `);
1109
+ }
1110
+ B(), x?.();
1111
+ }
1134
1112
  };
1135
1113
  return {
1136
1114
  start: P,
1137
- stop: (e = "") => a(e, 0),
1115
+ stop: (e = "") => m(e, 0),
1138
1116
  message: (e = "") => {
1139
- s = C(e ?? s);
1117
+ r = L(e ?? r);
1140
1118
  },
1141
- cancel: (e = "") => a(e, 1),
1142
- error: (e = "") => a(e, 2),
1143
- clear: () => a("", 0, true),
1119
+ cancel: (e = "") => m(e, 1),
1120
+ error: (e = "") => m(e, 2),
1121
+ clear: () => m("", 0, true),
1144
1122
  get isCancelled() {
1145
- return S;
1123
+ return l;
1146
1124
  }
1147
1125
  };
1148
1126
  };
@@ -1187,7 +1165,9 @@ function progress({
1187
1165
  error: r.error,
1188
1166
  clear: r.clear,
1189
1167
  advance: g,
1190
- isCancelled: r.isCancelled,
1168
+ get isCancelled() {
1169
+ return r.isCancelled;
1170
+ },
1191
1171
  message: (t) => g(0, t)
1192
1172
  };
1193
1173
  }
@@ -1266,20 +1246,31 @@ var LICENSE_KEY_VARIABLE2 = "SUPERSLATE_LICENSE_KEY";
1266
1246
  var LICENSE_KEY_PREFIX = "SUPERSLATE-";
1267
1247
 
1268
1248
  // src/core.ts
1249
+ var KEBAB_CASE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1250
+ var WHITESPACE_CHAR_RE = /\s/;
1251
+ var NPM_SCOPE_RE = /^@[a-z0-9][a-z0-9._-]*$/;
1252
+ var NEWLINE_CHAR_RE = /\r|\n/;
1253
+ var BRAND_NAME_RE = /^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u;
1254
+ var REGEX_SPECIAL_CHARS_RE = /[.*+?^${}()|[\]\\]/g;
1255
+ var ESCAPED_SOURCE_BRAND = SOURCE_BRAND_NAME.replace(REGEX_SPECIAL_CHARS_RE, "\\$&");
1256
+ var SOURCE_BRAND_RE = new RegExp(`\\b${ESCAPED_SOURCE_BRAND}\\b`, "gi");
1257
+ var ESCAPED_CLI_NAME = CLI_NAME2.replace(REGEX_SPECIAL_CHARS_RE, "\\$&");
1258
+ var SOURCE_DELIVERY_GUIDANCE_RE = new RegExp(`pnpm dlx ${ESCAPED_CLI_NAME}|bunx ${ESCAPED_CLI_NAME}|${ESCAPED_CLI_NAME} repository`, "i");
1259
+ var VENDOR_BOOTSTRAPPER_GUIDANCE_RE = new RegExp(`${ESCAPED_CLI_NAME}@\\d|public \`${ESCAPED_CLI_NAME}\` npm package`, "i");
1269
1260
  function validateAppName(value) {
1270
1261
  if (!value)
1271
1262
  return "App name is required.";
1272
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
1263
+ if (!KEBAB_CASE_RE.test(value)) {
1273
1264
  return "App name must be kebab-case (lowercase letters, numbers, and single hyphens).";
1274
1265
  }
1275
1266
  return;
1276
1267
  }
1277
1268
  function normalizeScope(value) {
1278
1269
  const scope = value.startsWith("@") ? value : `@${value}`;
1279
- if (/\s/.test(scope)) {
1270
+ if (WHITESPACE_CHAR_RE.test(scope)) {
1280
1271
  throw new Error("Use hyphens instead of spaces, for example test-package.");
1281
1272
  }
1282
- if (!/^@[a-z0-9][a-z0-9._-]*$/.test(scope)) {
1273
+ if (!NPM_SCOPE_RE.test(scope)) {
1283
1274
  throw new Error("Use lowercase letters and numbers, with optional periods, underscores, or hyphens.");
1284
1275
  }
1285
1276
  return scope;
@@ -1287,13 +1278,13 @@ function normalizeScope(value) {
1287
1278
  function validateBrandName(value) {
1288
1279
  if (!value.trim())
1289
1280
  return "Brand name cannot be empty.";
1290
- if (/\r|\n/.test(value))
1281
+ if (NEWLINE_CHAR_RE.test(value))
1291
1282
  return "Brand name must fit on one line.";
1292
1283
  if (value !== value.trim())
1293
1284
  return "Brand name cannot start or end with whitespace.";
1294
1285
  if (value.length > 48)
1295
1286
  return "Brand name must be 48 characters or less.";
1296
- if (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
1287
+ if (!BRAND_NAME_RE.test(value)) {
1297
1288
  return "Brand name may contain letters, numbers, spaces, periods, underscores, and hyphens.";
1298
1289
  }
1299
1290
  return;
@@ -1303,12 +1294,43 @@ var SCOPE_EXTENSIONS = new Set([".json", ".jsonc", ".md", ".ts", ".tsx", ".yaml"
1303
1294
  // src/core.ts
1304
1295
  import { randomBytes } from "node:crypto";
1305
1296
  import { extname } from "node:path";
1297
+ var KEBAB_CASE_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1298
+ var WHITESPACE_CHAR_RE2 = /\s/;
1299
+ var NPM_SCOPE_RE2 = /^@[a-z0-9][a-z0-9._-]*$/;
1300
+ var NEWLINE_CHAR_RE2 = /\r|\n/;
1301
+ var BRAND_NAME_RE2 = /^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u;
1302
+ var REGEX_SPECIAL_CHARS_RE2 = /[.*+?^${}()|[\]\\]/g;
1303
+ var NON_ALPHANUMERIC_CHAR_RE = /[^a-z0-9]/g;
1304
+ var LEADING_DIGIT_RE = /^[0-9]/;
1305
+ var HTTP_PROTOCOL_RE = /^https?:\/\//;
1306
+ var APP_DOMAIN_RE = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;
1307
+ var BUNDLE_IDENTIFIER_RE = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}$/;
1308
+ var MOBILE_APP_SCHEME_LINE_RE = /^MOBILE_APP_SCHEME=.*$/m;
1309
+ var MOBILE_DOCS_SOURCE_LINE_RE = /^\s*'mobile\.md',\n/m;
1310
+ var MOBILE_EDITION_SECTION_RE = /^## Mobile edition\n[\s\S]*?(?=^## |(?![\s\S]))/m;
1311
+ var MOBILE_TABLE_ROW_RE = /^\| `apps\/mobile` .*\n/gm;
1312
+ var MOBILE_PARAGRAPH_RE = /^`apps\/mobile`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm;
1313
+ var MOBILE_DOCS_LINK_RE = /\[([^\]]+)\]\(\.\/mobile\.md(?:#[^)]*)?\)/g;
1314
+ var NAMED_IMPORT_LINE_RE = /^(\s*)(import(?:\s+type)?\s+)\{([^}]+)\}(\s+from\s+.+)$/;
1315
+ var WHITESPACE_RE = /\s+/;
1316
+ var INTERNAL_AGENT_REFERENCE_RE = /internal\/|landing\/|seller-surface|studio-owned/i;
1317
+ var INTERNAL_CLAUDE_REFERENCE_RE = /internal\/|landing\/|studio-owned/i;
1318
+ var BETTER_AUTH_SECRET_LINE_RE = /^BETTER_AUTH_SECRET=.*$/m;
1319
+ var HEX_64_RE = /^[a-f0-9]{64}$/;
1320
+ var DATABASE_URL_LINE_RE = /^DATABASE_URL=.*$/m;
1321
+ var GHA_CREDS_FILE_RE = /^gha-creds-.*\.json$/;
1322
+ var ESCAPED_SOURCE_BRAND2 = SOURCE_BRAND_NAME.replace(REGEX_SPECIAL_CHARS_RE2, "\\$&");
1323
+ var SOURCE_BRAND_RE2 = new RegExp(`\\b${ESCAPED_SOURCE_BRAND2}\\b`, "gi");
1324
+ var ESCAPED_CLI_NAME2 = CLI_NAME2.replace(REGEX_SPECIAL_CHARS_RE2, "\\$&");
1325
+ var SOURCE_DELIVERY_GUIDANCE_RE2 = new RegExp(`pnpm dlx ${ESCAPED_CLI_NAME2}|bunx ${ESCAPED_CLI_NAME2}|${ESCAPED_CLI_NAME2} repository`, "i");
1326
+ var VENDOR_BOOTSTRAPPER_GUIDANCE_RE2 = new RegExp(`${ESCAPED_CLI_NAME2}@\\d|public \`${ESCAPED_CLI_NAME2}\` npm package`, "i");
1306
1327
  var BRAND_MANIFEST = [
1307
1328
  { path: "apps/web/src/configs/config.ts", occurrences: 2 },
1308
1329
  { path: "apps/web/index.html", occurrences: 4 },
1309
1330
  { path: "apps/web/public/favicon/site.webmanifest", occurrences: 2 },
1310
1331
  { path: "apps/web/public/offline.html", occurrences: 6 },
1311
1332
  { path: "apps/docs/lib/shared.ts", occurrences: 1 },
1333
+ { path: "apps/site/src/site.ts", occurrences: 1 },
1312
1334
  { path: "apps/docs/README.md", occurrences: 1 },
1313
1335
  { path: "README.md", occurrences: 1 },
1314
1336
  { path: "docs/meta.json", occurrences: 1 },
@@ -1351,17 +1373,17 @@ var UNIVERSAL_LINK_PATH_PREFIXES = ["/auth", "/invitations"];
1351
1373
  function validateAppName2(value) {
1352
1374
  if (!value)
1353
1375
  return "App name is required.";
1354
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) {
1376
+ if (!KEBAB_CASE_RE2.test(value)) {
1355
1377
  return "App name must be kebab-case (lowercase letters, numbers, and single hyphens).";
1356
1378
  }
1357
1379
  return;
1358
1380
  }
1359
1381
  function normalizeScope2(value) {
1360
1382
  const scope = value.startsWith("@") ? value : `@${value}`;
1361
- if (/\s/.test(scope)) {
1383
+ if (WHITESPACE_CHAR_RE2.test(scope)) {
1362
1384
  throw new Error("Use hyphens instead of spaces, for example test-package.");
1363
1385
  }
1364
- if (!/^@[a-z0-9][a-z0-9._-]*$/.test(scope)) {
1386
+ if (!NPM_SCOPE_RE2.test(scope)) {
1365
1387
  throw new Error("Use lowercase letters and numbers, with optional periods, underscores, or hyphens.");
1366
1388
  }
1367
1389
  return scope;
@@ -1369,13 +1391,13 @@ function normalizeScope2(value) {
1369
1391
  function validateBrandName2(value) {
1370
1392
  if (!value.trim())
1371
1393
  return "Brand name cannot be empty.";
1372
- if (/\r|\n/.test(value))
1394
+ if (NEWLINE_CHAR_RE2.test(value))
1373
1395
  return "Brand name must fit on one line.";
1374
1396
  if (value !== value.trim())
1375
1397
  return "Brand name cannot start or end with whitespace.";
1376
1398
  if (value.length > 48)
1377
1399
  return "Brand name must be 48 characters or less.";
1378
- if (!/^[\p{L}\p{N}](?:[\p{L}\p{N} ._-]*[\p{L}\p{N}])?$/u.test(value)) {
1400
+ if (!BRAND_NAME_RE2.test(value)) {
1379
1401
  return "Brand name may contain letters, numbers, spaces, periods, underscores, and hyphens.";
1380
1402
  }
1381
1403
  return;
@@ -1393,13 +1415,11 @@ function rewriteBrandContent(content, brandName) {
1393
1415
  const error = validateBrandName2(brandName);
1394
1416
  if (error)
1395
1417
  throw new Error(error);
1396
- const escapedSourceBrand = SOURCE_BRAND_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1397
- const sourceBrandPattern = new RegExp(`\\b${escapedSourceBrand}\\b`, "gi");
1398
- const matches = content.match(sourceBrandPattern);
1418
+ const matches = content.match(SOURCE_BRAND_RE2);
1399
1419
  if (brandName === SOURCE_BRAND_NAME || !matches)
1400
1420
  return { content, replacements: 0 };
1401
1421
  return {
1402
- content: content.replace(sourceBrandPattern, () => brandName),
1422
+ content: content.replace(SOURCE_BRAND_RE2, () => brandName),
1403
1423
  replacements: matches.length
1404
1424
  };
1405
1425
  }
@@ -1410,17 +1430,17 @@ function mobileSchemeFor(appName) {
1410
1430
  return appName;
1411
1431
  }
1412
1432
  function androidPackageSegment(appName) {
1413
- const segment = appName.replace(/[^a-z0-9]/g, "");
1414
- return /^[0-9]/.test(segment) || !segment ? `app${segment}` : segment;
1433
+ const segment = appName.replace(NON_ALPHANUMERIC_CHAR_RE, "");
1434
+ return LEADING_DIGIT_RE.test(segment) || !segment ? `app${segment}` : segment;
1415
1435
  }
1416
1436
  function validateAppDomain(value) {
1417
1437
  if (!value.trim())
1418
1438
  return "Application domain cannot be empty.";
1419
- if (/^https?:\/\//.test(value))
1439
+ if (HTTP_PROTOCOL_RE.test(value))
1420
1440
  return "Use a bare host such as app.example.com, not a URL.";
1421
1441
  if (value.includes("/"))
1422
1442
  return "Use a bare host such as app.example.com, with no path.";
1423
- if (!/^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(value)) {
1443
+ if (!APP_DOMAIN_RE.test(value)) {
1424
1444
  return "Use a lowercase hostname with at least two labels, for example app.example.com.";
1425
1445
  }
1426
1446
  return;
@@ -1428,7 +1448,7 @@ function validateAppDomain(value) {
1428
1448
  function validateBundleIdentifier(value) {
1429
1449
  if (!value.trim())
1430
1450
  return "Bundle identifier cannot be empty.";
1431
- if (!/^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*){2,}$/.test(value)) {
1451
+ if (!BUNDLE_IDENTIFIER_RE.test(value)) {
1432
1452
  return "Use at least three lowercase reverse-DNS segments, for example com.example.app.";
1433
1453
  }
1434
1454
  return;
@@ -1505,11 +1525,11 @@ function rewriteMobileAppConfig(content, identity) {
1505
1525
  `;
1506
1526
  }
1507
1527
  function rewriteMobileAppScheme(content, scheme) {
1508
- if (!/^MOBILE_APP_SCHEME=.*$/m.test(content)) {
1528
+ if (!MOBILE_APP_SCHEME_LINE_RE.test(content)) {
1509
1529
  throw new Error("Server .env.example must contain a MOBILE_APP_SCHEME entry.");
1510
1530
  }
1511
1531
  return {
1512
- content: content.replace(/^MOBILE_APP_SCHEME=.*$/m, `MOBILE_APP_SCHEME=${scheme}`),
1532
+ content: content.replace(MOBILE_APP_SCHEME_LINE_RE, `MOBILE_APP_SCHEME=${scheme}`),
1513
1533
  replacements: 1
1514
1534
  };
1515
1535
  }
@@ -1574,10 +1594,10 @@ function stripMobileDocsPage(content) {
1574
1594
  `;
1575
1595
  }
1576
1596
  function stripMobileDocsSource(content) {
1577
- return content.replace(/^\s*'mobile\.md',\n/m, "");
1597
+ return content.replace(MOBILE_DOCS_SOURCE_LINE_RE, "");
1578
1598
  }
1579
1599
  function stripMobileDocsReferences(content) {
1580
- return content.replace(/^## Mobile edition\n[\s\S]*?(?=^## |(?![\s\S]))/m, "").replace(/^\| `apps\/mobile` .*\n/gm, "").replace(/^`apps\/mobile`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm, "").replace(/\[([^\]]+)\]\(\.\/mobile\.md(?:#[^)]*)?\)/g, "$1");
1600
+ return content.replace(MOBILE_EDITION_SECTION_RE, "").replace(MOBILE_TABLE_ROW_RE, "").replace(MOBILE_PARAGRAPH_RE, "").replace(MOBILE_DOCS_LINK_RE, "$1");
1581
1601
  }
1582
1602
  function formatRewrittenPackageJson(content) {
1583
1603
  const parsed = JSON.parse(content);
@@ -1600,7 +1620,7 @@ function wrapLongNamedImports(content, width = 100) {
1600
1620
  `).map((line) => {
1601
1621
  if (line.length <= width)
1602
1622
  return line;
1603
- const match = line.match(/^(\s*)(import(?:\s+type)?\s+)\{([^}]+)\}(\s+from\s+.+)$/);
1623
+ const match = line.match(NAMED_IMPORT_LINE_RE);
1604
1624
  if (!match)
1605
1625
  return line;
1606
1626
  const [, indentation = "", prefix = "", names = "", suffix = ""] = match;
@@ -1644,7 +1664,7 @@ function wrapBrandedJsxText(content, brandName, width = 100) {
1644
1664
  continue;
1645
1665
  }
1646
1666
  let current = indentation;
1647
- for (const word of joined.split(/\s+/)) {
1667
+ for (const word of joined.split(WHITESPACE_RE)) {
1648
1668
  const candidate = current === indentation ? `${indentation}${word}` : `${current} ${word}`;
1649
1669
  if (candidate.length > width && current !== indentation) {
1650
1670
  output.push(current);
@@ -1682,6 +1702,16 @@ function rewriteWorkerName(content, appName) {
1682
1702
  return { content, replacements: 0 };
1683
1703
  return { content: content.replace(WORKER_NAME_DECLARATION, replacement), replacements: 1 };
1684
1704
  }
1705
+ var JSONC_WORKER_NAME_FIELD = /("name":\s*")[^"]*(")/;
1706
+ function rewriteSiteWorkerName(content, appName) {
1707
+ const error = validateAppName2(appName);
1708
+ if (error)
1709
+ throw new Error(error);
1710
+ if (!JSONC_WORKER_NAME_FIELD.test(content)) {
1711
+ throw new Error("The site Worker configuration is missing a name field.");
1712
+ }
1713
+ return content.replace(JSONC_WORKER_NAME_FIELD, `$1${appName}-site$2`);
1714
+ }
1685
1715
  function replaceMarkdownSection(content, heading, replacement) {
1686
1716
  const marker = `## ${heading}
1687
1717
  `;
@@ -1711,7 +1741,7 @@ function removeParagraphContaining(content, fragment) {
1711
1741
  `);
1712
1742
  }
1713
1743
  function rewriteBuyerAgentInstructions(content) {
1714
- if (content.includes("Audit and remediate the complete buyer-owned SaaS application") && !/internal\/|landing\/|seller-surface|studio-owned/i.test(content)) {
1744
+ if (content.includes("Audit and remediate the complete buyer-owned SaaS application") && !INTERNAL_AGENT_REFERENCE_RE.test(content)) {
1715
1745
  return content;
1716
1746
  }
1717
1747
  let rewritten = removeParagraphContaining(content, "`internal/` holds every seller-only surface");
@@ -1729,7 +1759,7 @@ into this repository.
1729
1759
  rewritten = rewritten.split(`
1730
1760
  `).filter((line) => !line.includes("| `internal`")).join(`
1731
1761
  `).replace("real env files, `internal/`, or\n `bun.lock`", "real env files, or `bun.lock`");
1732
- if (/internal\/|landing\/|seller-surface|studio-owned/i.test(rewritten)) {
1762
+ if (INTERNAL_AGENT_REFERENCE_RE.test(rewritten)) {
1733
1763
  throw new Error("Buyer agent instructions still contain seller-only guidance.");
1734
1764
  }
1735
1765
  return rewritten;
@@ -1793,10 +1823,10 @@ function stripSellerOnlyRootScripts(content) {
1793
1823
  `;
1794
1824
  }
1795
1825
  function rewriteBuyerClaudeInstructions(content) {
1796
- if (!/internal\/|landing\/|studio-owned/i.test(content))
1826
+ if (!INTERNAL_CLAUDE_REFERENCE_RE.test(content))
1797
1827
  return content;
1798
1828
  const rewritten = removeParagraphContaining(content, "`internal/landing/AGENTS.md`");
1799
- if (/internal\/|landing\/|studio-owned/i.test(rewritten)) {
1829
+ if (INTERNAL_CLAUDE_REFERENCE_RE.test(rewritten)) {
1800
1830
  throw new Error("Buyer Claude instructions still contain seller-only guidance.");
1801
1831
  }
1802
1832
  return rewritten;
@@ -1805,8 +1835,7 @@ function rewriteBuyerSetupInstructions(content) {
1805
1835
  if (!content.includes("## Create a product repository"))
1806
1836
  return content;
1807
1837
  const rewritten = replaceMarkdownSection(content, "Create a product repository", "");
1808
- const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1809
- if (new RegExp(`pnpm dlx ${escapedCliName}|bunx ${escapedCliName}|${escapedCliName} repository`, "i").test(rewritten)) {
1838
+ if (SOURCE_DELIVERY_GUIDANCE_RE2.test(rewritten)) {
1810
1839
  throw new Error("Buyer setup instructions still contain source-delivery guidance.");
1811
1840
  }
1812
1841
  return rewritten;
@@ -1815,30 +1844,29 @@ function rewriteBuyerReleaseInstructions(content) {
1815
1844
  if (!content.includes("## npm bootstrapper"))
1816
1845
  return content;
1817
1846
  const rewritten = replaceMarkdownSection(content, "npm bootstrapper", "");
1818
- const escapedCliName = CLI_NAME2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1819
- if (new RegExp(`${escapedCliName}@\\d|public \`${escapedCliName}\` npm package`, "i").test(rewritten)) {
1847
+ if (VENDOR_BOOTSTRAPPER_GUIDANCE_RE2.test(rewritten)) {
1820
1848
  throw new Error("Buyer release instructions still contain vendor bootstrapper guidance.");
1821
1849
  }
1822
1850
  return rewritten;
1823
1851
  }
1824
1852
  function generateStartDevVars(example, secret = randomBytes(32).toString("base64")) {
1825
- if (!/^BETTER_AUTH_SECRET=.*$/m.test(example)) {
1853
+ if (!BETTER_AUTH_SECRET_LINE_RE.test(example)) {
1826
1854
  throw new Error("apps/start/.dev.vars.example must contain a BETTER_AUTH_SECRET entry.");
1827
1855
  }
1828
- return example.replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${secret}`);
1856
+ return example.replace(BETTER_AUTH_SECRET_LINE_RE, `BETTER_AUTH_SECRET=${secret}`);
1829
1857
  }
1830
1858
  function generateServerEnv(example, secret = randomBytes(32).toString("hex"), databasePort = 5432) {
1831
- if (!/^[a-f0-9]{64}$/.test(secret)) {
1859
+ if (!HEX_64_RE.test(secret)) {
1832
1860
  throw new Error("Better Auth secret must be 32 bytes encoded as hex.");
1833
1861
  }
1834
- if (!/^DATABASE_URL=.*$/m.test(example) || !/^BETTER_AUTH_SECRET=.*$/m.test(example)) {
1862
+ if (!DATABASE_URL_LINE_RE.test(example) || !BETTER_AUTH_SECRET_LINE_RE.test(example)) {
1835
1863
  throw new Error("Server .env.example must contain DATABASE_URL and BETTER_AUTH_SECRET entries.");
1836
1864
  }
1837
1865
  if (!Number.isInteger(databasePort) || databasePort < 1 || databasePort > 65535) {
1838
1866
  throw new Error("Database port must be an integer between 1 and 65535.");
1839
1867
  }
1840
1868
  const databaseUrl = `postgresql://app:app@localhost:${databasePort}/app?sslmode=disable`;
1841
- return example.replace(/^DATABASE_URL=.*$/m, `DATABASE_URL=${databaseUrl}`).replace(/^BETTER_AUTH_SECRET=.*$/m, `BETTER_AUTH_SECRET=${secret}`);
1869
+ return example.replace(DATABASE_URL_LINE_RE, `DATABASE_URL=${databaseUrl}`).replace(BETTER_AUTH_SECRET_LINE_RE, `BETTER_AUTH_SECRET=${secret}`);
1842
1870
  }
1843
1871
  var SCOPE_EXTENSIONS2 = new Set([".json", ".jsonc", ".md", ".ts", ".tsx", ".yaml", ".yml"]);
1844
1872
  function isScopeRewriteFile(path) {
@@ -1871,7 +1899,7 @@ function isTemplatePathExcluded(relativePath) {
1871
1899
  if ([".expo", "android", "ios", "credentials", "dist-verify"].includes(mobileEntry))
1872
1900
  return true;
1873
1901
  }
1874
- if (parts.includes(".wrangler"))
1902
+ if (parts.includes(".wrangler") || parts.includes(".astro"))
1875
1903
  return true;
1876
1904
  if (parts.includes("dist") || parts.includes("out"))
1877
1905
  return true;
@@ -1883,7 +1911,7 @@ function isTemplatePathExcluded(relativePath) {
1883
1911
  return true;
1884
1912
  if (basename.endsWith(".tfvars") || basename === "backend.hcl")
1885
1913
  return true;
1886
- if (basename.includes(".tfstate") || /^gha-creds-.*\.json$/.test(basename))
1914
+ if (basename.includes(".tfstate") || GHA_CREDS_FILE_RE.test(basename))
1887
1915
  return true;
1888
1916
  if (normalized === "PLAN.md" || normalized === "comments.md")
1889
1917
  return true;
@@ -1907,6 +1935,13 @@ function isTemplatePathExcluded(relativePath) {
1907
1935
  }
1908
1936
 
1909
1937
  // src/editions.ts
1938
+ var NON_INDENTED_LINE_RE = /^\S/;
1939
+ var IMPORTER_KEY_LINE_RE = /^ {2}(\S[^:]*):\s*$/;
1940
+ var EXTRA_BLANK_LINES_RE = /\n{3,}/g;
1941
+ var START_TABLE_ROW_RE = /^\| `apps\/start` .*\n/gm;
1942
+ var START_PARAGRAPH_RE = /^`apps\/start`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm;
1943
+ var WORKER_NAME_FIELD_RE = /("name":\s*")[^"]+(")/;
1944
+ var DATABASE_NAME_FIELD_RE = /("database_name":\s*")[^"]+(")/;
1910
1945
  var EDITION_NAMES = ["web", "mobile", "start"];
1911
1946
  var START_EDITION_PATHS = ["apps/start", ".github/workflows/deploy-start.yml"];
1912
1947
  var START_EDITION_OVERLAY = "apps/start/edition";
@@ -1916,6 +1951,7 @@ var START_ROOT_CONTRACTS = {
1916
1951
  };
1917
1952
  var START_ONLY_KEPT_PATHS = [
1918
1953
  "apps/start",
1954
+ "apps/site",
1919
1955
  "packages/billing",
1920
1956
  "packages/tsconfig",
1921
1957
  ".github/workflows/deploy-start.yml",
@@ -1953,14 +1989,14 @@ function pruneLockfileImporters(content, removedImporters) {
1953
1989
  let inImporters = false;
1954
1990
  let skipping = false;
1955
1991
  for (const line of lines) {
1956
- if (/^\S/.test(line)) {
1992
+ if (NON_INDENTED_LINE_RE.test(line)) {
1957
1993
  inImporters = line === "importers:";
1958
1994
  skipping = false;
1959
1995
  output.push(line);
1960
1996
  continue;
1961
1997
  }
1962
1998
  if (inImporters) {
1963
- const importer = line.match(/^ {2}(\S[^:]*):\s*$/)?.[1];
1999
+ const importer = line.match(IMPORTER_KEY_LINE_RE)?.[1];
1964
2000
  if (importer !== undefined)
1965
2001
  skipping = removed.has(importer);
1966
2002
  if (skipping)
@@ -1969,7 +2005,7 @@ function pruneLockfileImporters(content, removedImporters) {
1969
2005
  output.push(line);
1970
2006
  }
1971
2007
  return output.join(`
1972
- `).replace(/\n{3,}/g, `
2008
+ `).replace(EXTRA_BLANK_LINES_RE, `
1973
2009
 
1974
2010
  `);
1975
2011
  }
@@ -1984,10 +2020,10 @@ function stripKnipWorkspaces(content, removedWorkspaces) {
1984
2020
  `;
1985
2021
  }
1986
2022
  function stripStartReferences(content) {
1987
- return content.replace(/^\| `apps\/start` .*\n/gm, "").replace(/^`apps\/start`[\s\S]*?(?:\n\n|\n?(?![\s\S]))/gm, "");
2023
+ return content.replace(START_TABLE_ROW_RE, "").replace(START_PARAGRAPH_RE, "");
1988
2024
  }
1989
2025
  function rewriteStartWorkerIdentifiers(content, appName) {
1990
- return content.replace(/("name":\s*")[^"]+(")/, `$1${appName}$2`).replace(/("database_name":\s*")[^"]+(")/, `$1${appName}$2`);
2026
+ return content.replace(WORKER_NAME_FIELD_RE, `$1${appName}$2`).replace(DATABASE_NAME_FIELD_RE, `$1${appName}$2`);
1991
2027
  }
1992
2028
  function startOnlyRootPackage(rootPackage, overlayPackage) {
1993
2029
  const root = JSON.parse(rootPackage);
@@ -1997,12 +2033,13 @@ function startOnlyRootPackage(rootPackage, overlayPackage) {
1997
2033
  }
1998
2034
 
1999
2035
  // src/delivery.ts
2036
+ var WHITESPACE_CHAR_RE3 = /\s/;
2000
2037
  var RELEASE_TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
2001
2038
  function validateLicenseKey(value) {
2002
2039
  const key = value.trim();
2003
2040
  if (!key)
2004
2041
  return "Enter the licence key from your purchase receipt.";
2005
- if (/\s/.test(key))
2042
+ if (WHITESPACE_CHAR_RE3.test(key))
2006
2043
  return "A licence key does not contain spaces.";
2007
2044
  if (!key.startsWith(LICENSE_KEY_PREFIX) || key.length === LICENSE_KEY_PREFIX.length) {
2008
2045
  return `A ${CLI_NAME2} licence key starts with ${LICENSE_KEY_PREFIX}. Copy it from your purchase receipt or the customer portal.`;
@@ -2024,7 +2061,6 @@ function normalizeSourceVersion(value) {
2024
2061
  // node_modules/@clack/prompts/dist/index.mjs
2025
2062
  import { styleText as styleText3, stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
2026
2063
  import process$12 from "node:process";
2027
- var import_sisteransi3 = __toESM(require_src(), 1);
2028
2064
  function isUnicodeSupported2() {
2029
2065
  if (process$12.platform !== "win32") {
2030
2066
  return process$12.env.TERM !== "linux";
@@ -2033,7 +2069,7 @@ function isUnicodeSupported2() {
2033
2069
  }
2034
2070
  var unicode2 = isUnicodeSupported2();
2035
2071
  var isCI2 = () => process.env.CI === "true";
2036
- var unicodeOr2 = (o, e) => unicode2 ? o : e;
2072
+ var unicodeOr2 = (e, o) => unicode2 ? e : o;
2037
2073
  var S_STEP_ACTIVE2 = unicodeOr2("◆", "*");
2038
2074
  var S_STEP_CANCEL2 = unicodeOr2("■", "x");
2039
2075
  var S_STEP_ERROR2 = unicodeOr2("▲", "x");
@@ -2059,8 +2095,8 @@ var S_INFO2 = unicodeOr2("●", "•");
2059
2095
  var S_SUCCESS2 = unicodeOr2("◆", "*");
2060
2096
  var S_WARN2 = unicodeOr2("▲", "!");
2061
2097
  var S_ERROR2 = unicodeOr2("■", "x");
2062
- var symbol2 = (o) => {
2063
- switch (o) {
2098
+ var symbol2 = (e) => {
2099
+ switch (e) {
2064
2100
  case "initial":
2065
2101
  case "active":
2066
2102
  return styleText3("cyan", S_STEP_ACTIVE2);
@@ -2074,8 +2110,8 @@ var symbol2 = (o) => {
2074
2110
  return styleText3("dim", S_STEP_ACTIVE2);
2075
2111
  }
2076
2112
  };
2077
- var symbolBar = (o) => {
2078
- switch (o) {
2113
+ var symbolBar = (e) => {
2114
+ switch (e) {
2079
2115
  case "initial":
2080
2116
  case "active":
2081
2117
  return styleText3("cyan", S_BAR2);
@@ -2087,11 +2123,12 @@ var symbolBar = (o) => {
2087
2123
  return styleText3("green", S_BAR2);
2088
2124
  }
2089
2125
  };
2090
- function formatInstructionFooter(o, e) {
2091
- const r = [`${e ? `${styleText3("cyan", S_BAR2)} ` : ""}${o.join(" • ")}`];
2092
- return e && r.push(styleText3("cyan", S_BAR_END2)), r;
2126
+ function formatInstructionFooter(e, o, r) {
2127
+ const s = r ? `${styleText3("cyan", S_BAR2)} ` : "", n = wrapTextWithPrefix(e, o.join(" • "), s).split(`
2128
+ `);
2129
+ return r && n.push(styleText3("cyan", S_BAR_END2)), n;
2093
2130
  }
2094
- var I = (l, e, w, p, b, C = false) => {
2131
+ var E$1 = (l, e, w, p, b, C = false) => {
2095
2132
  let r = e, O = 0;
2096
2133
  if (C)
2097
2134
  for (let i = p - 1;i >= w; i--) {
@@ -2120,12 +2157,12 @@ var limitOptions = ({
2120
2157
  let f = 0;
2121
2158
  l >= a - 3 && (f = Math.max(Math.min(l - a + 3, e.length - a), 0));
2122
2159
  let d = a < e.length && f > 0, c = a < e.length && f + a < e.length;
2123
- const W = Math.min(f + a, e.length), s = [];
2160
+ const I = Math.min(f + a, e.length), s = [];
2124
2161
  let g = 0;
2125
2162
  d && g++, c && g++;
2126
- const T = f + (d ? 1 : 0), y = W - (c ? 1 : 0);
2127
- for (let t = T;t < y; t++) {
2128
- const n = e[t], o = n ? w(n, t === l) : "", h = wrapAnsi(o, i, {
2163
+ const T = f + (d ? 1 : 0), W = I - (c ? 1 : 0);
2164
+ for (let t = T;t < W; t++) {
2165
+ const n = e[t], o = n !== undefined ? w(n, t === l) : "", h = wrapAnsi(o, i, {
2129
2166
  hard: true,
2130
2167
  trim: false
2131
2168
  }).split(`
@@ -2136,8 +2173,8 @@ var limitOptions = ({
2136
2173
  let t = 0, n = 0, o = g;
2137
2174
  const h = l - T;
2138
2175
  let u = v;
2139
- const L = () => I(s, o, 0, h, u), E = () => I(s, o, h + 1, s.length, u, true);
2140
- d ? ({ lineCount: o, removals: t } = L(), o > u && (c || (u -= 1), { lineCount: o, removals: n } = E())) : (c || (u -= 1), { lineCount: o, removals: n } = E(), o > u && (u -= 1, { lineCount: o, removals: t } = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
2176
+ const L = () => E$1(s, o, 0, h, u), y = () => E$1(s, o, h + 1, s.length, u, true);
2177
+ d ? ({ lineCount: o, removals: t } = L(), o > u && (c || (u -= 1), { lineCount: o, removals: n } = y())) : (c || (u -= 1), { lineCount: o, removals: n } = y(), o > u && (u -= 1, { lineCount: o, removals: t } = L())), t > 0 && (d = true, s.splice(0, t)), n > 0 && (c = true, s.splice(s.length - n, n));
2141
2178
  }
2142
2179
  const x = [];
2143
2180
  d && x.push(M);
@@ -2186,82 +2223,82 @@ var MULTISELECT_INSTRUCTIONS2 = [
2186
2223
  `${styleText3("dim", "Space:")} select`,
2187
2224
  `${styleText3("dim", "Enter:")} confirm`
2188
2225
  ];
2189
- var m = (i, u) => i.split(`
2226
+ var m = (n, u) => n.split(`
2190
2227
  `).map((d) => u(d)).join(`
2191
2228
  `);
2192
- var multiselect = (i) => {
2229
+ var multiselect = (n) => {
2193
2230
  const u = (t, a) => {
2194
2231
  const r = t.label ?? String(t.value);
2195
2232
  return a === "disabled" ? `${styleText3("gray", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3(["strikethrough", "gray"], o))}${t.hint ? ` ${styleText3("dim", `(${t.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${styleText3("cyan", S_CHECKBOX_ACTIVE2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${m(r, (o) => styleText3("dim", o))}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "cancelled" ? `${m(r, (o) => styleText3(["strikethrough", "dim"], o))}` : a === "active-selected" ? `${styleText3("green", S_CHECKBOX_SELECTED2)} ${r}${t.hint ? ` ${styleText3("dim", `(${t.hint})`)}` : ""}` : a === "submitted" ? `${m(r, (o) => styleText3("dim", o))}` : `${styleText3("dim", S_CHECKBOX_INACTIVE2)} ${m(r, (o) => styleText3("dim", o))}`;
2196
- }, d = i.required ?? true, x = i.showInstructions ?? true;
2233
+ }, d = n.required ?? true, x = n.showInstructions ?? true;
2197
2234
  return new a({
2198
- options: i.options,
2199
- signal: i.signal,
2200
- input: i.input,
2201
- output: i.output,
2202
- initialValues: i.initialValues,
2235
+ options: n.options,
2236
+ signal: n.signal,
2237
+ input: n.input,
2238
+ output: n.output,
2239
+ initialValues: n.initialValues,
2203
2240
  required: d,
2204
- cursorAt: i.cursorAt,
2241
+ cursorAt: n.cursorAt,
2205
2242
  validate(t) {
2206
2243
  if (d && (t === undefined || t.length === 0))
2207
2244
  return `Please select at least one option.
2208
2245
  ${styleText3("reset", styleText3("dim", `Press ${styleText3(["gray", "bgWhite", "inverse"], " space ")} to select, ${styleText3("gray", styleText3("bgWhite", styleText3("inverse", " enter ")))} to submit`))}`;
2209
2246
  },
2210
2247
  render() {
2211
- const t = i.withGuide ?? settings.withGuide, a = wrapTextWithPrefix(i.output, i.message, t ? `${symbolBar(this.state)} ` : "", `${symbol2(this.state)} `), r = `${t ? `${styleText3("gray", S_BAR2)}
2248
+ const t = n.withGuide ?? settings.withGuide, a = wrapTextWithPrefix(n.output, n.message, t ? `${symbolBar(this.state)} ` : "", `${symbol2(this.state)} `), r = `${t ? `${styleText3("gray", S_BAR2)}
2212
2249
  ` : ""}${a}
2213
- `, o = this.value ?? [], g = (n, l) => {
2214
- if (n.disabled)
2215
- return u(n, "disabled");
2216
- const s = o.includes(n.value);
2217
- return l && s ? u(n, "active-selected") : s ? u(n, "selected") : u(n, l ? "active" : "inactive");
2250
+ `, o = this.value ?? [], g = (i, l) => {
2251
+ if (i.disabled)
2252
+ return u(i, "disabled");
2253
+ const s = o.includes(i.value);
2254
+ return l && s ? u(i, "active-selected") : s ? u(i, "selected") : u(i, l ? "active" : "inactive");
2218
2255
  };
2219
2256
  switch (this.state) {
2220
2257
  case "submit": {
2221
- const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "submitted")).join(styleText3("dim", ", ")) || styleText3("dim", "none"), l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
2258
+ const i = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "submitted")).join(styleText3("dim", ", ")) || styleText3("dim", "none"), l = wrapTextWithPrefix(n.output, i, t ? `${styleText3("gray", S_BAR2)} ` : "");
2222
2259
  return `${r}${l}`;
2223
2260
  }
2224
2261
  case "cancel": {
2225
- const n = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "cancelled")).join(styleText3("dim", ", "));
2226
- if (n.trim() === "")
2262
+ const i = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u(s, "cancelled")).join(styleText3("dim", ", "));
2263
+ if (i.trim() === "")
2227
2264
  return `${r}${styleText3("gray", S_BAR2)}`;
2228
- const l = wrapTextWithPrefix(i.output, n, t ? `${styleText3("gray", S_BAR2)} ` : "");
2265
+ const l = wrapTextWithPrefix(n.output, i, t ? `${styleText3("gray", S_BAR2)} ` : "");
2229
2266
  return `${r}${l}${t ? `
2230
2267
  ${styleText3("gray", S_BAR2)}` : ""}`;
2231
2268
  }
2232
2269
  case "error": {
2233
- const n = t ? `${styleText3("yellow", S_BAR2)} ` : "", l = this.error.split(`
2270
+ const i = t ? `${styleText3("yellow", S_BAR2)} ` : "", l = this.error.split(`
2234
2271
  `).map(($, v) => v === 0 ? `${t ? `${styleText3("yellow", S_BAR_END2)} ` : ""}${styleText3("yellow", $)}` : ` ${$}`).join(`
2235
2272
  `), s = r.split(`
2236
2273
  `).length, h = l.split(`
2237
2274
  `).length + 1;
2238
- return `${r}${n}${limitOptions({
2239
- output: i.output,
2275
+ return `${r}${i}${limitOptions({
2276
+ output: n.output,
2240
2277
  options: this.options,
2241
2278
  cursor: this.cursor,
2242
- maxItems: i.maxItems,
2243
- columnPadding: n.length,
2279
+ maxItems: n.maxItems,
2280
+ columnPadding: i.length,
2244
2281
  rowPadding: s + h,
2245
2282
  style: g
2246
2283
  }).join(`
2247
- ${n}`)}
2284
+ ${i}`)}
2248
2285
  ${l}
2249
2286
  `;
2250
2287
  }
2251
2288
  default: {
2252
- const n = t ? `${styleText3("cyan", S_BAR2)} ` : "", l = r.split(`
2253
- `).length, s = x ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS2, t) : t ? [styleText3("cyan", S_BAR_END2)] : [], h = s.join(`
2289
+ const i = t ? `${styleText3("cyan", S_BAR2)} ` : "", l = r.split(`
2290
+ `).length, s = x ? formatInstructionFooter(n.output, MULTISELECT_INSTRUCTIONS2, t) : t ? [styleText3("cyan", S_BAR_END2)] : [], h = s.join(`
2254
2291
  `), $ = s.length + 1;
2255
- return `${r}${n}${limitOptions({
2256
- output: i.output,
2292
+ return `${r}${i}${limitOptions({
2293
+ output: n.output,
2257
2294
  options: this.options,
2258
2295
  cursor: this.cursor,
2259
- maxItems: i.maxItems,
2260
- columnPadding: n.length,
2296
+ maxItems: n.maxItems,
2297
+ columnPadding: i.length,
2261
2298
  rowPadding: l + $,
2262
2299
  style: g
2263
2300
  }).join(`
2264
- ${n}`)}
2301
+ ${i}`)}
2265
2302
  ${h}
2266
2303
  `;
2267
2304
  }
@@ -2329,85 +2366,103 @@ ${styleText3("gray", S_BAR_END2)} ` : "";
2329
2366
 
2330
2367
  `);
2331
2368
  };
2332
- var W2 = (l) => styleText3("magenta", l);
2369
+ var Y2 = 30000;
2370
+ var Z2 = (c) => styleText3("magenta", c);
2333
2371
  var spinner2 = ({
2334
- indicator: l = "dots",
2335
- onCancel: h,
2372
+ indicator: c = "dots",
2373
+ onCancel: b,
2336
2374
  output: n = process.stdout,
2337
- cancelMessage: G,
2338
- errorMessage: O,
2339
- frames: E = unicode2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
2340
- delay: F = unicode2 ? 80 : 120,
2341
- signal: m,
2342
- ...I
2375
+ cancelMessage: _,
2376
+ errorMessage: C,
2377
+ frames: M = unicode2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"],
2378
+ delay: O = unicode2 ? 80 : 120,
2379
+ signal: g,
2380
+ ...E
2343
2381
  } = {}) => {
2344
- const u = isCI2();
2345
- let M, T, d = false, S = false, s = "", p, w = performance.now();
2346
- const x = getColumns(n), k = I?.styleFrame ?? W2, g = (e) => {
2347
- const r = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel;
2348
- S = e === 1, d && (a(r, e), S && typeof h == "function" && h());
2349
- }, f = () => g(2), i = () => g(1), A = () => {
2350
- process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i), process.on("SIGTERM", i), process.on("exit", g), m && m.addEventListener("abort", i);
2351
- }, H = () => {
2352
- process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i), process.removeListener("SIGTERM", i), process.removeListener("exit", g), m && m.removeEventListener("abort", i);
2353
- }, y = () => {
2354
- if (p === undefined)
2382
+ const u = isCI2(), h = isAccessible(E.accessible);
2383
+ let x, I, p = false, l = false, r = "", v, w = performance.now();
2384
+ const y = getColumns(n), F = E?.styleFrame ?? Z2, S = (e) => {
2385
+ const s = e > 1 ? C ?? settings.messages.error : _ ?? settings.messages.cancel;
2386
+ l = e === 1, p && (m(s, e), l && typeof b == "function" && b());
2387
+ }, $ = () => S(2), a = () => S(1), H = () => {
2388
+ process.on("uncaughtExceptionMonitor", $), process.on("unhandledRejection", $), process.on("SIGINT", a), process.on("SIGTERM", a), process.on("exit", S), g && g.addEventListener("abort", a);
2389
+ }, B = () => {
2390
+ process.removeListener("uncaughtExceptionMonitor", $), process.removeListener("unhandledRejection", $), process.removeListener("SIGINT", a), process.removeListener("SIGTERM", a), process.removeListener("exit", S), g && g.removeEventListener("abort", a);
2391
+ }, R = () => {
2392
+ if (v === undefined)
2355
2393
  return;
2356
2394
  u && n.write(`
2357
2395
  `);
2358
- const r = wrapAnsi(p, x, {
2396
+ const s = wrapAnsi(v, y, {
2359
2397
  hard: true,
2360
2398
  trim: false
2361
2399
  }).split(`
2362
2400
  `);
2363
- r.length > 1 && n.write(import_sisteransi3.cursor.up(r.length - 1)), n.write(import_sisteransi3.cursor.to(0)), n.write(import_sisteransi3.erase.down());
2364
- }, C = (e) => e.replace(/\.+$/, ""), _ = (e) => {
2365
- const r = (performance.now() - e) / 1000, t = Math.floor(r / 60), o = Math.floor(r % 60);
2366
- return t > 0 ? `[${t}m ${o}s]` : `[${o}s]`;
2367
- }, N = I.withGuide ?? settings.withGuide, P = (e = "") => {
2368
- d = true, M = block({ output: n }), s = C(e), w = performance.now(), N && n.write(`${styleText3("gray", S_BAR2)}
2401
+ s.length > 1 && n.write(cursor.up(s.length - 1)), n.write(cursor.to(0)), n.write(erase.down());
2402
+ }, L = (e) => e.replace(/\.+$/, ""), T = (e) => {
2403
+ const s = (performance.now() - e) / 1000, i = Math.floor(s / 60), t = Math.floor(s % 60);
2404
+ return i > 0 ? `[${i}m ${t}s]` : `[${t}s]`;
2405
+ }, N = E.withGuide ?? settings.withGuide, P = (e = "") => {
2406
+ if (p = true, l = false, r = L(e), w = performance.now(), H(), h) {
2407
+ r !== "" && n.write(`${r}
2408
+ `), I = setInterval(() => {
2409
+ n.write(r === "" ? `still working
2410
+ ` : `still working: ${r}
2369
2411
  `);
2370
- let r = 0, t = 0;
2371
- A(), T = setInterval(() => {
2372
- if (u && s === p)
2412
+ }, Y2);
2413
+ return;
2414
+ }
2415
+ x = block({ output: n }), N && n.write(`${styleText3("gray", S_BAR2)}
2416
+ `);
2417
+ let s = 0, i = 0;
2418
+ I = setInterval(() => {
2419
+ if (u && r === v)
2373
2420
  return;
2374
- y(), p = s;
2375
- const o = k(E[r]);
2376
- let v;
2421
+ R(), v = r;
2422
+ const t = F(M[s]);
2423
+ let o;
2377
2424
  if (u)
2378
- v = `${o} ${s}...`;
2379
- else if (l === "timer")
2380
- v = `${o} ${s} ${_(w)}`;
2425
+ o = `${t} ${r}...`;
2426
+ else if (c === "timer")
2427
+ o = `${t} ${r} ${T(w)}`;
2381
2428
  else {
2382
- const B = ".".repeat(Math.floor(t)).slice(0, 3);
2383
- v = `${o} ${s}${B}`;
2429
+ const j = ".".repeat(Math.floor(i)).slice(0, 3);
2430
+ o = `${t} ${r}${j}`;
2384
2431
  }
2385
- const j = wrapAnsi(v, x, {
2432
+ const D = wrapAnsi(o, y, {
2386
2433
  hard: true,
2387
2434
  trim: false
2388
2435
  });
2389
- n.write(j), r = r + 1 < E.length ? r + 1 : 0, t = t < 4 ? t + 0.125 : 0;
2390
- }, F);
2391
- }, a = (e = "", r = 0, t = false) => {
2392
- if (!d)
2393
- return;
2394
- d = false, clearInterval(T), y();
2395
- const o = r === 0 ? styleText3("green", S_STEP_SUBMIT2) : r === 1 ? styleText3("red", S_STEP_CANCEL2) : styleText3("red", S_STEP_ERROR2);
2396
- s = e ?? s, t || (l === "timer" ? n.write(`${o} ${s} ${_(w)}
2397
- `) : n.write(`${o} ${s}
2398
- `)), H(), M();
2436
+ n.write(D), s = s + 1 < M.length ? s + 1 : 0, i = i < 4 ? i + 0.125 : 0;
2437
+ }, O);
2438
+ }, m = (e = "", s = 0, i = false) => {
2439
+ if (p) {
2440
+ if (p = false, l = s === 1, clearInterval(I), h || R(), r = e ?? r, !i)
2441
+ if (h) {
2442
+ const t = s === 1 ? _ ?? settings.messages.cancel : s === 2 ? C ?? settings.messages.error : "Done", o = r || t;
2443
+ c === "timer" ? n.write(`${o} ${T(w)}
2444
+ `) : n.write(`${o}
2445
+ `);
2446
+ } else {
2447
+ const t = s === 0 ? styleText3("green", S_STEP_SUBMIT2) : s === 1 ? styleText3("red", S_STEP_CANCEL2) : styleText3("red", S_STEP_ERROR2);
2448
+ c === "timer" ? n.write(`${t} ${r} ${T(w)}
2449
+ `) : n.write(`${t} ${r}
2450
+ `);
2451
+ }
2452
+ B(), x?.();
2453
+ }
2399
2454
  };
2400
2455
  return {
2401
2456
  start: P,
2402
- stop: (e = "") => a(e, 0),
2457
+ stop: (e = "") => m(e, 0),
2403
2458
  message: (e = "") => {
2404
- s = C(e ?? s);
2459
+ r = L(e ?? r);
2405
2460
  },
2406
- cancel: (e = "") => a(e, 1),
2407
- error: (e = "") => a(e, 2),
2408
- clear: () => a("", 0, true),
2461
+ cancel: (e = "") => m(e, 1),
2462
+ error: (e = "") => m(e, 2),
2463
+ clear: () => m("", 0, true),
2409
2464
  get isCancelled() {
2410
- return S;
2465
+ return l;
2411
2466
  }
2412
2467
  };
2413
2468
  };
@@ -2420,7 +2475,7 @@ var SELECT_INSTRUCTIONS2 = [
2420
2475
  `${styleText3("dim", "↑/↓")} to navigate`,
2421
2476
  `${styleText3("dim", "Enter:")} confirm`
2422
2477
  ];
2423
- var c = (t, o) => t.includes(`
2478
+ var c2 = (t, o) => t.includes(`
2424
2479
  `) ? t.split(`
2425
2480
  `).map((d) => o(d)).join(`
2426
2481
  `) : o(t);
@@ -2431,15 +2486,15 @@ var select = (t) => {
2431
2486
  const s = n.label ?? String(n.value);
2432
2487
  switch (m) {
2433
2488
  case "disabled":
2434
- return `${styleText3("gray", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("gray", i))}${n.hint ? ` ${styleText3("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
2489
+ return `${styleText3("gray", S_RADIO_INACTIVE2)} ${c2(s, (i) => styleText3("gray", i))}${n.hint ? ` ${styleText3("dim", `(${n.hint ?? "disabled"})`)}` : ""}`;
2435
2490
  case "selected":
2436
- return `${c(s, (i) => styleText3("dim", i))}`;
2491
+ return `${c2(s, (i) => styleText3("dim", i))}`;
2437
2492
  case "active":
2438
2493
  return `${styleText3("green", S_RADIO_ACTIVE2)} ${s}${n.hint ? ` ${styleText3("dim", `(${n.hint})`)}` : ""}`;
2439
2494
  case "cancelled":
2440
- return `${c(s, (i) => styleText3(["strikethrough", "dim"], i))}`;
2495
+ return `${c2(s, (i) => styleText3(["strikethrough", "dim"], i))}`;
2441
2496
  default:
2442
- return `${styleText3("dim", S_RADIO_INACTIVE2)} ${c(s, (i) => styleText3("dim", i))}`;
2497
+ return `${styleText3("dim", S_RADIO_INACTIVE2)} ${c2(s, (i) => styleText3("dim", i))}`;
2443
2498
  }
2444
2499
  }, d = t.showInstructions ?? true;
2445
2500
  return new n$1({
@@ -2464,7 +2519,7 @@ ${styleText3("gray", S_BAR2)}` : ""}`;
2464
2519
  }
2465
2520
  default: {
2466
2521
  const r = n ? `${styleText3("cyan", S_BAR2)} ` : "", a = u.split(`
2467
- `).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS2, n) : n ? [styleText3("cyan", S_BAR_END2)] : [], f = p.join(`
2522
+ `).length, p = d ? formatInstructionFooter(t.output, SELECT_INSTRUCTIONS2, n) : n ? [styleText3("cyan", S_BAR_END2)] : [], f = p.join(`
2468
2523
  `), b = p.length + 1;
2469
2524
  return `${u}${r}${limitOptions({
2470
2525
  output: t.output,
@@ -2588,6 +2643,8 @@ function isExecutable(path) {
2588
2643
  }
2589
2644
 
2590
2645
  // src/agent-setup.ts
2646
+ var CODEX_AGENT_DOCS_TABLE_RE = /^\[mcp_servers\.(?:agent-docs|agent_docs|"agent-docs")\]$/m;
2647
+ var LINE_BREAK_RE = /\r?\n/;
2591
2648
  var CLIENTS = ["codex", "claude", "cursor"];
2592
2649
  var SERVER_NAME = "agent-docs";
2593
2650
  var CODEX_START = "# agent-context:start";
@@ -2681,7 +2738,7 @@ function mergeCodexConfig(existing, root) {
2681
2738
 
2682
2739
  ${managed}${existing.slice(after)}`.trimStart();
2683
2740
  }
2684
- if (/^\[mcp_servers\.(?:agent-docs|agent_docs|"agent-docs")\]$/m.test(existing)) {
2741
+ if (CODEX_AGENT_DOCS_TABLE_RE.test(existing)) {
2685
2742
  throw new Error("Codex already has an unmanaged agent-docs server; remove or rename it first.");
2686
2743
  }
2687
2744
  return `${existing.trimEnd()}${existing.trim() ? `
@@ -2732,14 +2789,14 @@ async function addGitInfoExcludes(root, paths) {
2732
2789
  return false;
2733
2790
  const excludePath = resolve(root, rawPath);
2734
2791
  const existing = await readFile(excludePath, "utf8").catch(() => "");
2735
- const missing = paths.filter((path) => !existing.split(/\r?\n/).includes(path));
2792
+ const missing = paths.filter((path) => !existing.split(LINE_BREAK_RE).includes(path));
2736
2793
  if (missing.length === 0)
2737
2794
  return true;
2738
2795
  const prefix = existing && !existing.endsWith(`
2739
2796
  `) ? `
2740
2797
  ` : "";
2741
2798
  const marker = "# Local agent configuration";
2742
- const heading = existing.split(/\r?\n/).includes(marker) ? "" : `${marker}
2799
+ const heading = existing.split(LINE_BREAK_RE).includes(marker) ? "" : `${marker}
2743
2800
  `;
2744
2801
  await atomicWrite(excludePath, `${existing}${prefix}${heading}${missing.join(`
2745
2802
  `)}
@@ -2889,24 +2946,40 @@ import { resolve as resolve2 } from "node:path";
2889
2946
  // src/deployment.ts
2890
2947
  import { mkdir as mkdir2, readFile as readFile2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
2891
2948
  import { join as join3 } from "node:path";
2892
- var deploymentClouds = ["aws", "gcp", "azure"];
2893
- var frontendTargets = ["native", "vercel", "cloudflare"];
2949
+ var DEPLOYMENT_ENVIRONMENT_RE = /^[a-z][a-z0-9-]{1,30}$/;
2950
+ var REGION_RE = /^[a-z0-9][a-z0-9-]{1,31}$/;
2951
+ var DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
2952
+ var deploymentClouds = ["aws", "gcp", "railway"];
2953
+ var openTofuClouds = ["aws", "gcp"];
2954
+ var frontendTargets = ["native", "cloudflare"];
2894
2955
  var storageProviders = ["r2", "s3", "gcs"];
2895
2956
  var defaultRegions = {
2896
2957
  aws: "us-east-1",
2897
- gcp: "us-central1",
2898
- azure: "eastus2"
2958
+ gcp: "us-central1"
2899
2959
  };
2900
2960
  var defaultStorageProviders = {
2901
2961
  aws: "s3",
2902
2962
  gcp: "gcs",
2903
- azure: "r2"
2963
+ railway: "r2"
2904
2964
  };
2905
2965
  var storageProvidersByCloud = {
2906
2966
  aws: ["s3", "r2"],
2907
2967
  gcp: ["gcs", "r2"],
2908
- azure: ["r2"]
2968
+ railway: ["r2"]
2969
+ };
2970
+ var frontendTargetsByCloud = {
2971
+ aws: ["native", "cloudflare"],
2972
+ gcp: ["native", "cloudflare"],
2973
+ railway: ["cloudflare"]
2974
+ };
2975
+ var cloudLabels = {
2976
+ aws: "AWS",
2977
+ gcp: "GCP",
2978
+ railway: "Railway"
2909
2979
  };
2980
+ function isOpenTofuCloud(cloud) {
2981
+ return openTofuClouds.some((candidate) => candidate === cloud);
2982
+ }
2910
2983
  function isDeploymentCloud(value) {
2911
2984
  return deploymentClouds.some((cloud) => cloud === value);
2912
2985
  }
@@ -2918,12 +2991,12 @@ function isStorageProvider(value) {
2918
2991
  }
2919
2992
  function parseDeploymentCloud(value) {
2920
2993
  if (!isDeploymentCloud(value))
2921
- throw new Error("Cloud must be aws, gcp, or azure.");
2994
+ throw new Error("Cloud must be aws, gcp, or railway.");
2922
2995
  return value;
2923
2996
  }
2924
2997
  function parseFrontendTarget(value) {
2925
2998
  if (!isFrontendTarget(value)) {
2926
- throw new Error("Frontend target must be native, vercel, or cloudflare.");
2999
+ throw new Error("Frontend target must be native or cloudflare.");
2927
3000
  }
2928
3001
  return value;
2929
3002
  }
@@ -2933,19 +3006,19 @@ function parseStorageProvider(value) {
2933
3006
  return value;
2934
3007
  }
2935
3008
  function validateDeploymentEnvironment(value) {
2936
- if (!/^[a-z][a-z0-9-]{1,30}$/.test(value)) {
3009
+ if (!DEPLOYMENT_ENVIRONMENT_RE.test(value)) {
2937
3010
  return "Environment must use 2-31 lowercase letters, numbers, or hyphens.";
2938
3011
  }
2939
3012
  return;
2940
3013
  }
2941
3014
  function validateRegion(value) {
2942
- if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
3015
+ if (!REGION_RE.test(value)) {
2943
3016
  return "Region must use 2-32 lowercase letters, numbers, or hyphens.";
2944
3017
  }
2945
3018
  return;
2946
3019
  }
2947
3020
  function validateDomain(value) {
2948
- if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
3021
+ if (value.length > 80 || !DOMAIN_RE.test(value)) {
2949
3022
  return "Enter a lowercase hostname such as api.example.com, without a protocol or path.";
2950
3023
  }
2951
3024
  return;
@@ -2954,12 +3027,24 @@ function validateDeploymentSelection(selection) {
2954
3027
  if (!storageProvidersByCloud[selection.cloud].includes(selection.storageProvider)) {
2955
3028
  throw new Error(`${selection.storageProvider} storage is not supported for ${selection.cloud} deployments.`);
2956
3029
  }
3030
+ if (!frontendTargetsByCloud[selection.cloud].includes(selection.frontendTarget)) {
3031
+ throw new Error(`The ${selection.frontendTarget} frontend is not supported for ${selection.cloud} deployments.`);
3032
+ }
3033
+ if (isOpenTofuCloud(selection.cloud)) {
3034
+ const regionError = validateRegion(selection.region ?? "");
3035
+ if (regionError)
3036
+ throw new Error(regionError);
3037
+ } else {
3038
+ if (selection.region !== null) {
3039
+ throw new Error("Railway selects its region per service; omit the region.");
3040
+ }
3041
+ if (selection.haEnabled) {
3042
+ throw new Error("The higher-availability profile applies to AWS and GCP only.");
3043
+ }
3044
+ }
2957
3045
  const environmentError = validateDeploymentEnvironment(selection.environment);
2958
3046
  if (environmentError)
2959
3047
  throw new Error(environmentError);
2960
- const regionError = validateRegion(selection.region);
2961
- if (regionError)
2962
- throw new Error(regionError);
2963
3048
  const apiError = validateDomain(selection.apiDomain);
2964
3049
  if (apiError)
2965
3050
  throw new Error(`API domain: ${apiError}`);
@@ -2991,20 +3076,86 @@ function createDeploymentConfig(projectName, selection) {
2991
3076
  ha_enabled: selection.haEnabled
2992
3077
  };
2993
3078
  }
2994
- function deploymentReadme(config) {
2995
- const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : config.cloud === "gcp" ? "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets" : "`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` secrets";
2996
- const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : config.cloud === "gcp" ? "GCP project, Cloud DNS zone, and runtime Secret Manager names" : "Azure DNS zone ID and runtime Key Vault secret identifiers";
3079
+ function lockSummary(config) {
3080
+ return `The deployment target is locked:
3081
+
3082
+ - backend: **${cloudLabels[config.cloud]}**;
3083
+ - storage: **${config.storage_provider}**;
3084
+ - frontend: **${config.frontend_target}**;
3085
+ - environment: **${config.environment}**.`;
3086
+ }
3087
+ function railwayReadme(config) {
3088
+ return `# Deployment setup
3089
+
3090
+ ${lockSummary(config)}
3091
+
3092
+ The production hosts are:
3093
+
3094
+ - API: \`${config.api_domain}\`;
3095
+ - frontend: \`${config.frontend_domain}\`.
3096
+
3097
+ ## Railway API and PostgreSQL
3098
+
3099
+ 1. Create a Railway project with a PostgreSQL service and an API service connected to this
3100
+ repository. Build from the repository root, because the image copies the whole workspace.
3101
+ 2. Set the API service's config-as-code path to \`apps/server/railway.toml\`. It builds
3102
+ \`apps/server/Dockerfile\`, runs \`./server migrate\` before each deploy, starts \`./server serve\`,
3103
+ and admits traffic only after \`/ready\` succeeds.
3104
+ 3. Set these API service variables in the \`${config.environment}\` Railway environment:
3105
+
3106
+ \`\`\`text
3107
+ DATABASE_URL=\${{Postgres.DATABASE_URL}}
3108
+ ENVIRONMENT=production
3109
+ TRUSTED_PROXY_PROFILE=railway
3110
+ DEPLOYMENT_CLOUD=railway
3111
+ FRONTEND_URL=https://${config.frontend_domain}
3112
+ SERVER_URL=https://${config.api_domain}
3113
+ \`\`\`
3114
+
3115
+ Add \`BETTER_AUTH_SECRET\`, \`RESEND_API_KEY\`, \`EMAIL_FROM\`, \`APP_DB_POOL_MAX\`, and
3116
+ \`AUTH_DB_POOL_MAX\`, then the Cloudflare R2 storage variables and every enabled billing,
3117
+ monitoring, and OAuth value. Railway injects \`PORT\`; leave it unset.
3118
+
3119
+ 4. Add the API host as the service's custom domain and publish the DNS records Railway reports. A
3120
+ Cloudflare-managed zone keeps them \`DNS only\`.
3121
+
3122
+ ## Cloudflare frontend
3123
+
3124
+ 1. Create a Cloudflare Workers project rooted at \`apps/web\`. It uses \`apps/web/wrangler.toml\`, which
3125
+ serves \`dist\` as static assets with single-page-application fallback.
3126
+ 2. Build with \`pnpm run build:prod\` and these variables:
3127
+
3128
+ \`\`\`text
3129
+ VITE_APP_ENV=production
3130
+ VITE_API_URL=https://${config.api_domain}
3131
+ \`\`\`
3132
+
3133
+ Add \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
3134
+ public marketing site.
3135
+
3136
+ 3. Attach the frontend host as the Worker's custom domain.
3137
+
3138
+ ## Verify
3139
+
3140
+ \`\`\`sh
3141
+ API_ORIGIN=https://${config.api_domain} \\
3142
+ WEB_ORIGIN=https://${config.frontend_domain} \\
3143
+ bash scripts/verify-public-deployment.sh
3144
+ \`\`\`
3145
+
3146
+ The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
3147
+ before changing providers.
3148
+ `;
3149
+ }
3150
+ function openTofuReadme(config) {
3151
+ const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets";
3152
+ const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : "GCP project, Cloud DNS zone, and runtime Secret Manager names";
2997
3153
  const storageSetup = config.storage_provider === "r2" ? `Create private staging and public Cloudflare R2 buckets, then store the required storage
2998
3154
  variables in the runtime secret container.` : `${config.storage_provider.toUpperCase()} application buckets, lifecycle policy, CORS, public access, and workload permissions are
2999
3155
  managed by the selected OpenTofu root.`;
3000
3156
  return `# Deployment setup
3001
3157
 
3002
- The deployment target is locked:
3003
-
3004
- - backend: **${config.cloud.toUpperCase()}**;
3005
- - storage: **${config.storage_provider}**;
3006
- - frontend: **${config.frontend_target}**;
3007
- - environment: **${config.environment}**.
3158
+ ${lockSummary(config)}
3008
3159
 
3009
3160
  ## Protected GitHub environment
3010
3161
 
@@ -3014,8 +3165,8 @@ Create a GitHub environment named \`${config.environment}\` and configure:
3014
3165
  - a \`BACKEND_HCL\` secret containing the selected remote-state backend configuration;
3015
3166
  - a \`TFVARS_JSON\` secret containing only:
3016
3167
  - ${providerInputs};
3017
- - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
3018
- public marketing site.
3168
+ - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a public
3169
+ marketing site.
3019
3170
 
3020
3171
  ## Cloud preparation
3021
3172
 
@@ -3036,9 +3187,33 @@ The workflow creates the registry first, pushes an immutable image, provisions t
3036
3187
  traffic disabled, runs migrations, and admits traffic only after migration succeeds.
3037
3188
 
3038
3189
  The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
3039
- before changing cloud providers.
3190
+ before changing providers.
3040
3191
  `;
3041
3192
  }
3193
+ function deploymentReadme(config) {
3194
+ return config.cloud === "railway" ? railwayReadme(config) : openTofuReadme(config);
3195
+ }
3196
+ function openTofuPaths(cloud) {
3197
+ return [
3198
+ `infra/bootstrap/${cloud}`,
3199
+ `infra/modules/${cloud}`,
3200
+ `infra/roots/${cloud}`,
3201
+ `scripts/deploy/release-${cloud}.sh`,
3202
+ `scripts/deploy/frontend-${cloud}.sh`
3203
+ ];
3204
+ }
3205
+ function unusedDeploymentPaths(selection) {
3206
+ const paths = selection.cloud === "railway" ? ["infra", "scripts/deploy", ".github/workflows/deploy-cloud.yml"] : [
3207
+ ...openTofuClouds.filter((cloud) => cloud !== selection.cloud).flatMap(openTofuPaths),
3208
+ "apps/server/railway.toml"
3209
+ ];
3210
+ if (selection.frontendTarget === "native")
3211
+ paths.push("apps/web/wrangler.toml");
3212
+ else if (selection.cloud !== "railway") {
3213
+ paths.push(`scripts/deploy/frontend-${selection.cloud}.sh`);
3214
+ }
3215
+ return paths;
3216
+ }
3042
3217
  async function removeIfPresent(target) {
3043
3218
  if (!await stat2(target).catch(() => {
3044
3219
  return;
@@ -3047,6 +3222,20 @@ async function removeIfPresent(target) {
3047
3222
  await rm(target, { recursive: true, force: true });
3048
3223
  return true;
3049
3224
  }
3225
+ async function removeInfrastructureScript(repositoryRoot) {
3226
+ const packagePath = join3(repositoryRoot, "package.json");
3227
+ const content = await readFile2(packagePath, "utf8").catch(() => {
3228
+ return;
3229
+ });
3230
+ if (!content)
3231
+ return;
3232
+ const rootPackage = JSON.parse(content);
3233
+ if (!rootPackage.scripts?.["verify:infra"])
3234
+ return;
3235
+ delete rootPackage.scripts["verify:infra"];
3236
+ await writeFile2(packagePath, `${JSON.stringify(rootPackage, null, 2)}
3237
+ `);
3238
+ }
3050
3239
  async function applyDeploymentSelection(repositoryRoot, projectName, selection) {
3051
3240
  const config = createDeploymentConfig(projectName, selection);
3052
3241
  const configPath = join3(repositoryRoot, "deployment/config.json");
@@ -3060,40 +3249,12 @@ async function applyDeploymentSelection(repositoryRoot, projectName, selection)
3060
3249
  `);
3061
3250
  await writeFile2(join3(repositoryRoot, "deployment/README.md"), deploymentReadme(config));
3062
3251
  const removed = [];
3063
- for (const cloud of deploymentClouds) {
3064
- if (cloud === selection.cloud)
3065
- continue;
3066
- for (const relativePath of [
3067
- `infra/bootstrap/${cloud}`,
3068
- `infra/modules/${cloud}`,
3069
- `infra/roots/${cloud}`,
3070
- `scripts/deploy/release-${cloud}.sh`,
3071
- `scripts/deploy/frontend-${cloud}.sh`
3072
- ]) {
3073
- if (await removeIfPresent(join3(repositoryRoot, relativePath)))
3074
- removed.push(relativePath);
3075
- }
3076
- }
3077
- const adapters = [
3078
- { target: "vercel", path: "apps/web/vercel.json" },
3079
- { target: "cloudflare", path: "apps/web/wrangler.toml" },
3080
- {
3081
- target: "azure-native",
3082
- path: "apps/web/public/staticwebapp.config.json"
3083
- }
3084
- ];
3085
- const selectedAdapter = selection.frontendTarget === "native" ? selection.cloud === "azure" ? "azure-native" : undefined : selection.frontendTarget;
3086
- for (const adapter of adapters) {
3087
- if (adapter.target === selectedAdapter)
3088
- continue;
3089
- if (await removeIfPresent(join3(repositoryRoot, adapter.path)))
3090
- removed.push(adapter.path);
3091
- }
3092
- if (selection.frontendTarget !== "native") {
3093
- const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
3094
- if (await removeIfPresent(join3(repositoryRoot, nativeScript)))
3095
- removed.push(nativeScript);
3252
+ for (const relativePath of unusedDeploymentPaths(selection)) {
3253
+ if (await removeIfPresent(join3(repositoryRoot, relativePath)))
3254
+ removed.push(relativePath);
3096
3255
  }
3256
+ if (removed.includes("infra"))
3257
+ await removeInfrastructureScript(repositoryRoot);
3097
3258
  return removed;
3098
3259
  }
3099
3260
 
@@ -3112,37 +3273,31 @@ async function collectDeploymentSelection(defaults = {}, nonInteractive = false)
3112
3273
  if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
3113
3274
  throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
3114
3275
  }
3276
+ const cloud = defaults.cloud;
3115
3277
  return {
3116
- cloud: defaults.cloud,
3117
- storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
3118
- frontendTarget: defaults.frontendTarget ?? "native",
3278
+ cloud,
3279
+ storageProvider: defaults.storageProvider ?? defaultStorageProviders[cloud],
3280
+ frontendTarget: defaults.frontendTarget ?? frontendTargetsByCloud[cloud][0] ?? "cloudflare",
3119
3281
  environment: defaults.environment ?? "production",
3120
- region: defaults.region ?? defaultRegions[defaults.cloud],
3282
+ region: isOpenTofuCloud(cloud) ? defaults.region ?? defaultRegions[cloud] : defaults.region ?? null,
3121
3283
  apiDomain: defaults.apiDomain,
3122
3284
  frontendDomain: defaults.frontendDomain,
3123
3285
  haEnabled: defaults.haEnabled ?? false
3124
3286
  };
3125
3287
  }
3126
3288
  const cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap(await select({
3127
- message: "Backend cloud",
3289
+ message: "Backend host",
3128
3290
  options: [
3129
- { value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
3130
- { value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
3131
- { value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
3291
+ { value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL, OpenTofu" },
3292
+ { value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL, OpenTofu" },
3293
+ { value: "railway", label: "Railway", hint: "Managed container + PostgreSQL" }
3132
3294
  ]
3133
3295
  })));
3134
- const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap(await select({
3296
+ const frontendOptions = frontendTargetsByCloud[cloud];
3297
+ const frontendTarget = defaults.frontendTarget ?? (frontendOptions.length === 1 ? frontendOptions[0] ?? "cloudflare" : parseFrontendTarget(await unwrap(await select({
3135
3298
  message: "Frontend host",
3136
- options: [
3137
- { value: "native", label: "Cloud native", hint: "Managed with the backend" },
3138
- { value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
3139
- {
3140
- value: "cloudflare",
3141
- label: "Cloudflare static assets",
3142
- hint: "No API Worker"
3143
- }
3144
- ]
3145
- })));
3299
+ options: frontendOptions.map((target) => target === "native" ? { value: target, label: "Cloud native", hint: "Managed with the backend" } : { value: target, label: "Cloudflare static assets", hint: "No API Worker" })
3300
+ }))));
3146
3301
  const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap(await select({
3147
3302
  message: "Object storage",
3148
3303
  options: storageProvidersByCloud[cloud].map((provider) => ({
@@ -3157,8 +3312,8 @@ async function collectDeploymentSelection(defaults = {}, nonInteractive = false)
3157
3312
  defaultValue: "production",
3158
3313
  validate: (value) => validateDeploymentEnvironment(value || "production")
3159
3314
  }));
3160
- const region = defaults.region ?? await unwrap(await text2({
3161
- message: `${cloud.toUpperCase()} region`,
3315
+ const region = !isOpenTofuCloud(cloud) ? defaults.region ?? null : defaults.region ?? await unwrap(await text2({
3316
+ message: `${cloudLabels[cloud]} region`,
3162
3317
  placeholder: defaultRegions[cloud],
3163
3318
  defaultValue: defaultRegions[cloud],
3164
3319
  validate: (value) => validateRegion(value || defaultRegions[cloud])
@@ -3173,7 +3328,7 @@ async function collectDeploymentSelection(defaults = {}, nonInteractive = false)
3173
3328
  placeholder: "app.example.com",
3174
3329
  validate: (value) => validateDomain(value ?? "")
3175
3330
  }));
3176
- const haEnabled = defaults.haEnabled ?? await unwrap(await confirm2({
3331
+ const haEnabled = !isOpenTofuCloud(cloud) ? defaults.haEnabled ?? false : defaults.haEnabled ?? await unwrap(await confirm2({
3177
3332
  message: "Enable the higher-availability profile?",
3178
3333
  initialValue: false
3179
3334
  }));
@@ -3235,10 +3390,11 @@ async function runDeploymentConfigure(argv, productName, repositoryRoot = proces
3235
3390
  try {
3236
3391
  const selection = await collectDeploymentSelection(parsed, parsed.yes);
3237
3392
  const removed = await applyDeploymentSelection(root, rootPackage.name, selection);
3238
- log2.success(`${selection.cloud.toUpperCase()} + ${selection.storageProvider} + ${selection.frontendTarget} is locked for ${selection.environment}.`);
3393
+ log2.success(`${cloudLabels[selection.cloud]} + ${selection.storageProvider} + ${selection.frontendTarget} is locked for ${selection.environment}.`);
3239
3394
  log2.info(`${removed.length} unused deployment paths removed.`);
3240
3395
  log2.step("Next");
3241
- log2.message(`Review deployment/config.json
3396
+ log2.message(selection.cloud === "railway" ? `Review deployment/config.json
3397
+ Follow deployment/README.md for the Railway services and Cloudflare frontend` : `Review deployment/config.json
3242
3398
  Configure the protected GitHub environment
3243
3399
  Dispatch .github/workflows/deploy-cloud.yml`);
3244
3400
  outro2("Deployment configuration is ready to commit.");
@@ -3329,37 +3485,31 @@ async function collectDeploymentSelection2(defaults = {}, nonInteractive = false
3329
3485
  if (!defaults.cloud || !defaults.apiDomain || !defaults.frontendDomain) {
3330
3486
  throw new Error("Non-interactive deployment setup requires --cloud, --api-domain, and --frontend-domain.");
3331
3487
  }
3488
+ const cloud = defaults.cloud;
3332
3489
  return {
3333
- cloud: defaults.cloud,
3334
- storageProvider: defaults.storageProvider ?? defaultStorageProviders[defaults.cloud],
3335
- frontendTarget: defaults.frontendTarget ?? "native",
3490
+ cloud,
3491
+ storageProvider: defaults.storageProvider ?? defaultStorageProviders[cloud],
3492
+ frontendTarget: defaults.frontendTarget ?? frontendTargetsByCloud[cloud][0] ?? "cloudflare",
3336
3493
  environment: defaults.environment ?? "production",
3337
- region: defaults.region ?? defaultRegions[defaults.cloud],
3494
+ region: isOpenTofuCloud(cloud) ? defaults.region ?? defaultRegions[cloud] : defaults.region ?? null,
3338
3495
  apiDomain: defaults.apiDomain,
3339
3496
  frontendDomain: defaults.frontendDomain,
3340
3497
  haEnabled: defaults.haEnabled ?? false
3341
3498
  };
3342
3499
  }
3343
3500
  const cloud = defaults.cloud ?? parseDeploymentCloud(await unwrap2(await select({
3344
- message: "Backend cloud",
3501
+ message: "Backend host",
3345
3502
  options: [
3346
- { value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL" },
3347
- { value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL" },
3348
- { value: "azure", label: "Azure", hint: "Container Apps + Flexible Server" }
3503
+ { value: "aws", label: "AWS", hint: "ECS Fargate + RDS PostgreSQL, OpenTofu" },
3504
+ { value: "gcp", label: "Google Cloud", hint: "Cloud Run + Cloud SQL, OpenTofu" },
3505
+ { value: "railway", label: "Railway", hint: "Managed container + PostgreSQL" }
3349
3506
  ]
3350
3507
  })));
3351
- const frontendTarget = defaults.frontendTarget ?? parseFrontendTarget(await unwrap2(await select({
3508
+ const frontendOptions = frontendTargetsByCloud[cloud];
3509
+ const frontendTarget = defaults.frontendTarget ?? (frontendOptions.length === 1 ? frontendOptions[0] ?? "cloudflare" : parseFrontendTarget(await unwrap2(await select({
3352
3510
  message: "Frontend host",
3353
- options: [
3354
- { value: "native", label: "Cloud native", hint: "Managed with the backend" },
3355
- { value: "vercel", label: "Vercel SPA", hint: "Static frontend only" },
3356
- {
3357
- value: "cloudflare",
3358
- label: "Cloudflare static assets",
3359
- hint: "No API Worker"
3360
- }
3361
- ]
3362
- })));
3511
+ options: frontendOptions.map((target) => target === "native" ? { value: target, label: "Cloud native", hint: "Managed with the backend" } : { value: target, label: "Cloudflare static assets", hint: "No API Worker" })
3512
+ }))));
3363
3513
  const storageProvider = defaults.storageProvider ?? (storageProvidersByCloud[cloud].length === 1 ? storageProvidersByCloud[cloud][0] ?? defaultStorageProviders[cloud] : parseStorageProvider(await unwrap2(await select({
3364
3514
  message: "Object storage",
3365
3515
  options: storageProvidersByCloud[cloud].map((provider) => ({
@@ -3374,8 +3524,8 @@ async function collectDeploymentSelection2(defaults = {}, nonInteractive = false
3374
3524
  defaultValue: "production",
3375
3525
  validate: (value) => validateDeploymentEnvironment(value || "production")
3376
3526
  }));
3377
- const region = defaults.region ?? await unwrap2(await text2({
3378
- message: `${cloud.toUpperCase()} region`,
3527
+ const region = !isOpenTofuCloud(cloud) ? defaults.region ?? null : defaults.region ?? await unwrap2(await text2({
3528
+ message: `${cloudLabels[cloud]} region`,
3379
3529
  placeholder: defaultRegions[cloud],
3380
3530
  defaultValue: defaultRegions[cloud],
3381
3531
  validate: (value) => validateRegion(value || defaultRegions[cloud])
@@ -3390,7 +3540,7 @@ async function collectDeploymentSelection2(defaults = {}, nonInteractive = false
3390
3540
  placeholder: "app.example.com",
3391
3541
  validate: (value) => validateDomain(value ?? "")
3392
3542
  }));
3393
- const haEnabled = defaults.haEnabled ?? await unwrap2(await confirm2({
3543
+ const haEnabled = !isOpenTofuCloud(cloud) ? defaults.haEnabled ?? false : defaults.haEnabled ?? await unwrap2(await confirm2({
3394
3544
  message: "Enable the higher-availability profile?",
3395
3545
  initialValue: false
3396
3546
  }));
@@ -3409,14 +3559,31 @@ async function collectDeploymentSelection2(defaults = {}, nonInteractive = false
3409
3559
  // src/deployment.ts
3410
3560
  import { mkdir as mkdir3, readFile as readFile4, rm as rm2, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
3411
3561
  import { join as join5 } from "node:path";
3412
- var deploymentClouds2 = ["aws", "gcp", "azure"];
3413
- var frontendTargets2 = ["native", "vercel", "cloudflare"];
3562
+ var DEPLOYMENT_ENVIRONMENT_RE2 = /^[a-z][a-z0-9-]{1,30}$/;
3563
+ var REGION_RE2 = /^[a-z0-9][a-z0-9-]{1,31}$/;
3564
+ var DOMAIN_RE2 = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
3565
+ var deploymentClouds2 = ["aws", "gcp", "railway"];
3566
+ var openTofuClouds2 = ["aws", "gcp"];
3567
+ var frontendTargets2 = ["native", "cloudflare"];
3414
3568
  var storageProviders2 = ["r2", "s3", "gcs"];
3415
3569
  var storageProvidersByCloud2 = {
3416
3570
  aws: ["s3", "r2"],
3417
3571
  gcp: ["gcs", "r2"],
3418
- azure: ["r2"]
3572
+ railway: ["r2"]
3573
+ };
3574
+ var frontendTargetsByCloud2 = {
3575
+ aws: ["native", "cloudflare"],
3576
+ gcp: ["native", "cloudflare"],
3577
+ railway: ["cloudflare"]
3419
3578
  };
3579
+ var cloudLabels2 = {
3580
+ aws: "AWS",
3581
+ gcp: "GCP",
3582
+ railway: "Railway"
3583
+ };
3584
+ function isOpenTofuCloud2(cloud) {
3585
+ return openTofuClouds2.some((candidate) => candidate === cloud);
3586
+ }
3420
3587
  function isDeploymentCloud2(value) {
3421
3588
  return deploymentClouds2.some((cloud) => cloud === value);
3422
3589
  }
@@ -3428,12 +3595,12 @@ function isStorageProvider2(value) {
3428
3595
  }
3429
3596
  function parseDeploymentCloud2(value) {
3430
3597
  if (!isDeploymentCloud2(value))
3431
- throw new Error("Cloud must be aws, gcp, or azure.");
3598
+ throw new Error("Cloud must be aws, gcp, or railway.");
3432
3599
  return value;
3433
3600
  }
3434
3601
  function parseFrontendTarget2(value) {
3435
3602
  if (!isFrontendTarget2(value)) {
3436
- throw new Error("Frontend target must be native, vercel, or cloudflare.");
3603
+ throw new Error("Frontend target must be native or cloudflare.");
3437
3604
  }
3438
3605
  return value;
3439
3606
  }
@@ -3443,19 +3610,19 @@ function parseStorageProvider2(value) {
3443
3610
  return value;
3444
3611
  }
3445
3612
  function validateDeploymentEnvironment2(value) {
3446
- if (!/^[a-z][a-z0-9-]{1,30}$/.test(value)) {
3613
+ if (!DEPLOYMENT_ENVIRONMENT_RE2.test(value)) {
3447
3614
  return "Environment must use 2-31 lowercase letters, numbers, or hyphens.";
3448
3615
  }
3449
3616
  return;
3450
3617
  }
3451
3618
  function validateRegion2(value) {
3452
- if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(value)) {
3619
+ if (!REGION_RE2.test(value)) {
3453
3620
  return "Region must use 2-32 lowercase letters, numbers, or hyphens.";
3454
3621
  }
3455
3622
  return;
3456
3623
  }
3457
3624
  function validateDomain2(value) {
3458
- if (value.length > 80 || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(value)) {
3625
+ if (value.length > 80 || !DOMAIN_RE2.test(value)) {
3459
3626
  return "Enter a lowercase hostname such as api.example.com, without a protocol or path.";
3460
3627
  }
3461
3628
  return;
@@ -3464,12 +3631,24 @@ function validateDeploymentSelection2(selection) {
3464
3631
  if (!storageProvidersByCloud2[selection.cloud].includes(selection.storageProvider)) {
3465
3632
  throw new Error(`${selection.storageProvider} storage is not supported for ${selection.cloud} deployments.`);
3466
3633
  }
3634
+ if (!frontendTargetsByCloud2[selection.cloud].includes(selection.frontendTarget)) {
3635
+ throw new Error(`The ${selection.frontendTarget} frontend is not supported for ${selection.cloud} deployments.`);
3636
+ }
3637
+ if (isOpenTofuCloud2(selection.cloud)) {
3638
+ const regionError = validateRegion2(selection.region ?? "");
3639
+ if (regionError)
3640
+ throw new Error(regionError);
3641
+ } else {
3642
+ if (selection.region !== null) {
3643
+ throw new Error("Railway selects its region per service; omit the region.");
3644
+ }
3645
+ if (selection.haEnabled) {
3646
+ throw new Error("The higher-availability profile applies to AWS and GCP only.");
3647
+ }
3648
+ }
3467
3649
  const environmentError = validateDeploymentEnvironment2(selection.environment);
3468
3650
  if (environmentError)
3469
3651
  throw new Error(environmentError);
3470
- const regionError = validateRegion2(selection.region);
3471
- if (regionError)
3472
- throw new Error(regionError);
3473
3652
  const apiError = validateDomain2(selection.apiDomain);
3474
3653
  if (apiError)
3475
3654
  throw new Error(`API domain: ${apiError}`);
@@ -3501,20 +3680,86 @@ function createDeploymentConfig2(projectName, selection) {
3501
3680
  ha_enabled: selection.haEnabled
3502
3681
  };
3503
3682
  }
3504
- function deploymentReadme2(config) {
3505
- const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : config.cloud === "gcp" ? "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets" : "`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID` secrets";
3506
- const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : config.cloud === "gcp" ? "GCP project, Cloud DNS zone, and runtime Secret Manager names" : "Azure DNS zone ID and runtime Key Vault secret identifiers";
3683
+ function lockSummary2(config) {
3684
+ return `The deployment target is locked:
3685
+
3686
+ - backend: **${cloudLabels2[config.cloud]}**;
3687
+ - storage: **${config.storage_provider}**;
3688
+ - frontend: **${config.frontend_target}**;
3689
+ - environment: **${config.environment}**.`;
3690
+ }
3691
+ function railwayReadme2(config) {
3692
+ return `# Deployment setup
3693
+
3694
+ ${lockSummary2(config)}
3695
+
3696
+ The production hosts are:
3697
+
3698
+ - API: \`${config.api_domain}\`;
3699
+ - frontend: \`${config.frontend_domain}\`.
3700
+
3701
+ ## Railway API and PostgreSQL
3702
+
3703
+ 1. Create a Railway project with a PostgreSQL service and an API service connected to this
3704
+ repository. Build from the repository root, because the image copies the whole workspace.
3705
+ 2. Set the API service's config-as-code path to \`apps/server/railway.toml\`. It builds
3706
+ \`apps/server/Dockerfile\`, runs \`./server migrate\` before each deploy, starts \`./server serve\`,
3707
+ and admits traffic only after \`/ready\` succeeds.
3708
+ 3. Set these API service variables in the \`${config.environment}\` Railway environment:
3709
+
3710
+ \`\`\`text
3711
+ DATABASE_URL=\${{Postgres.DATABASE_URL}}
3712
+ ENVIRONMENT=production
3713
+ TRUSTED_PROXY_PROFILE=railway
3714
+ DEPLOYMENT_CLOUD=railway
3715
+ FRONTEND_URL=https://${config.frontend_domain}
3716
+ SERVER_URL=https://${config.api_domain}
3717
+ \`\`\`
3718
+
3719
+ Add \`BETTER_AUTH_SECRET\`, \`RESEND_API_KEY\`, \`EMAIL_FROM\`, \`APP_DB_POOL_MAX\`, and
3720
+ \`AUTH_DB_POOL_MAX\`, then the Cloudflare R2 storage variables and every enabled billing,
3721
+ monitoring, and OAuth value. Railway injects \`PORT\`; leave it unset.
3722
+
3723
+ 4. Add the API host as the service's custom domain and publish the DNS records Railway reports. A
3724
+ Cloudflare-managed zone keeps them \`DNS only\`.
3725
+
3726
+ ## Cloudflare frontend
3727
+
3728
+ 1. Create a Cloudflare Workers project rooted at \`apps/web\`. It uses \`apps/web/wrangler.toml\`, which
3729
+ serves \`dist\` as static assets with single-page-application fallback.
3730
+ 2. Build with \`pnpm run build:prod\` and these variables:
3731
+
3732
+ \`\`\`text
3733
+ VITE_APP_ENV=production
3734
+ VITE_API_URL=https://${config.api_domain}
3735
+ \`\`\`
3736
+
3737
+ Add \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
3738
+ public marketing site.
3739
+
3740
+ 3. Attach the frontend host as the Worker's custom domain.
3741
+
3742
+ ## Verify
3743
+
3744
+ \`\`\`sh
3745
+ API_ORIGIN=https://${config.api_domain} \\
3746
+ WEB_ORIGIN=https://${config.frontend_domain} \\
3747
+ bash scripts/verify-public-deployment.sh
3748
+ \`\`\`
3749
+
3750
+ The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
3751
+ before changing providers.
3752
+ `;
3753
+ }
3754
+ function openTofuReadme2(config) {
3755
+ const identity = config.cloud === "aws" ? "`AWS_ROLE_ARN` secret" : "`GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT` secrets";
3756
+ const providerInputs = config.cloud === "aws" ? "Route 53 zone or certificate identifiers and runtime Secrets Manager ARNs" : "GCP project, Cloud DNS zone, and runtime Secret Manager names";
3507
3757
  const storageSetup = config.storage_provider === "r2" ? `Create private staging and public Cloudflare R2 buckets, then store the required storage
3508
3758
  variables in the runtime secret container.` : `${config.storage_provider.toUpperCase()} application buckets, lifecycle policy, CORS, public access, and workload permissions are
3509
3759
  managed by the selected OpenTofu root.`;
3510
3760
  return `# Deployment setup
3511
3761
 
3512
- The deployment target is locked:
3513
-
3514
- - backend: **${config.cloud.toUpperCase()}**;
3515
- - storage: **${config.storage_provider}**;
3516
- - frontend: **${config.frontend_target}**;
3517
- - environment: **${config.environment}**.
3762
+ ${lockSummary2(config)}
3518
3763
 
3519
3764
  ## Protected GitHub environment
3520
3765
 
@@ -3524,8 +3769,8 @@ Create a GitHub environment named \`${config.environment}\` and configure:
3524
3769
  - a \`BACKEND_HCL\` secret containing the selected remote-state backend configuration;
3525
3770
  - a \`TFVARS_JSON\` secret containing only:
3526
3771
  - ${providerInputs};
3527
- - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a
3528
- public marketing site.
3772
+ - \`VITE_TERMS_URL\` and \`VITE_PRIVACY_URL\`, plus \`VITE_MARKETING_URL\` when the product has a public
3773
+ marketing site.
3529
3774
 
3530
3775
  ## Cloud preparation
3531
3776
 
@@ -3546,9 +3791,33 @@ The workflow creates the registry first, pushes an immutable image, provisions t
3546
3791
  traffic disabled, runs migrations, and admits traffic only after migration succeeds.
3547
3792
 
3548
3793
  The provider lock is deliberate. Restore the deployment directories from a fresh licensed release
3549
- before changing cloud providers.
3794
+ before changing providers.
3550
3795
  `;
3551
3796
  }
3797
+ function deploymentReadme2(config) {
3798
+ return config.cloud === "railway" ? railwayReadme2(config) : openTofuReadme2(config);
3799
+ }
3800
+ function openTofuPaths2(cloud) {
3801
+ return [
3802
+ `infra/bootstrap/${cloud}`,
3803
+ `infra/modules/${cloud}`,
3804
+ `infra/roots/${cloud}`,
3805
+ `scripts/deploy/release-${cloud}.sh`,
3806
+ `scripts/deploy/frontend-${cloud}.sh`
3807
+ ];
3808
+ }
3809
+ function unusedDeploymentPaths2(selection) {
3810
+ const paths = selection.cloud === "railway" ? ["infra", "scripts/deploy", ".github/workflows/deploy-cloud.yml"] : [
3811
+ ...openTofuClouds2.filter((cloud) => cloud !== selection.cloud).flatMap(openTofuPaths2),
3812
+ "apps/server/railway.toml"
3813
+ ];
3814
+ if (selection.frontendTarget === "native")
3815
+ paths.push("apps/web/wrangler.toml");
3816
+ else if (selection.cloud !== "railway") {
3817
+ paths.push(`scripts/deploy/frontend-${selection.cloud}.sh`);
3818
+ }
3819
+ return paths;
3820
+ }
3552
3821
  async function removeIfPresent2(target) {
3553
3822
  if (!await stat3(target).catch(() => {
3554
3823
  return;
@@ -3557,6 +3826,20 @@ async function removeIfPresent2(target) {
3557
3826
  await rm2(target, { recursive: true, force: true });
3558
3827
  return true;
3559
3828
  }
3829
+ async function removeInfrastructureScript2(repositoryRoot) {
3830
+ const packagePath = join5(repositoryRoot, "package.json");
3831
+ const content = await readFile4(packagePath, "utf8").catch(() => {
3832
+ return;
3833
+ });
3834
+ if (!content)
3835
+ return;
3836
+ const rootPackage = JSON.parse(content);
3837
+ if (!rootPackage.scripts?.["verify:infra"])
3838
+ return;
3839
+ delete rootPackage.scripts["verify:infra"];
3840
+ await writeFile3(packagePath, `${JSON.stringify(rootPackage, null, 2)}
3841
+ `);
3842
+ }
3560
3843
  async function applyDeploymentSelection2(repositoryRoot, projectName, selection) {
3561
3844
  const config = createDeploymentConfig2(projectName, selection);
3562
3845
  const configPath = join5(repositoryRoot, "deployment/config.json");
@@ -3570,40 +3853,12 @@ async function applyDeploymentSelection2(repositoryRoot, projectName, selection)
3570
3853
  `);
3571
3854
  await writeFile3(join5(repositoryRoot, "deployment/README.md"), deploymentReadme2(config));
3572
3855
  const removed = [];
3573
- for (const cloud of deploymentClouds2) {
3574
- if (cloud === selection.cloud)
3575
- continue;
3576
- for (const relativePath of [
3577
- `infra/bootstrap/${cloud}`,
3578
- `infra/modules/${cloud}`,
3579
- `infra/roots/${cloud}`,
3580
- `scripts/deploy/release-${cloud}.sh`,
3581
- `scripts/deploy/frontend-${cloud}.sh`
3582
- ]) {
3583
- if (await removeIfPresent2(join5(repositoryRoot, relativePath)))
3584
- removed.push(relativePath);
3585
- }
3586
- }
3587
- const adapters = [
3588
- { target: "vercel", path: "apps/web/vercel.json" },
3589
- { target: "cloudflare", path: "apps/web/wrangler.toml" },
3590
- {
3591
- target: "azure-native",
3592
- path: "apps/web/public/staticwebapp.config.json"
3593
- }
3594
- ];
3595
- const selectedAdapter = selection.frontendTarget === "native" ? selection.cloud === "azure" ? "azure-native" : undefined : selection.frontendTarget;
3596
- for (const adapter of adapters) {
3597
- if (adapter.target === selectedAdapter)
3598
- continue;
3599
- if (await removeIfPresent2(join5(repositoryRoot, adapter.path)))
3600
- removed.push(adapter.path);
3601
- }
3602
- if (selection.frontendTarget !== "native") {
3603
- const nativeScript = `scripts/deploy/frontend-${selection.cloud}.sh`;
3604
- if (await removeIfPresent2(join5(repositoryRoot, nativeScript)))
3605
- removed.push(nativeScript);
3856
+ for (const relativePath of unusedDeploymentPaths2(selection)) {
3857
+ if (await removeIfPresent2(join5(repositoryRoot, relativePath)))
3858
+ removed.push(relativePath);
3606
3859
  }
3860
+ if (removed.includes("infra"))
3861
+ await removeInfrastructureScript2(repositoryRoot);
3607
3862
  return removed;
3608
3863
  }
3609
3864
 
@@ -3632,12 +3887,13 @@ import { rm as rm3 } from "node:fs/promises";
3632
3887
  import { join as join6 } from "node:path";
3633
3888
  import { Readable, Transform } from "node:stream";
3634
3889
  import { pipeline } from "node:stream/promises";
3890
+ var TRAILING_SLASHES_RE = /\/+$/;
3635
3891
  var RELEASE_TAG_PATTERN2 = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/;
3636
3892
  var INDEX_TIMEOUT_MS = 15000;
3637
3893
  var DOWNLOAD_TIMEOUT_MS = 300000;
3638
3894
  function deliveryBaseUrl(environment = process.env) {
3639
3895
  const configured = environment[DELIVERY_URL_VARIABLE]?.trim();
3640
- return (configured || DELIVERY_BASE_URL).replace(/\/+$/, "");
3896
+ return (configured || DELIVERY_BASE_URL).replace(TRAILING_SLASHES_RE, "");
3641
3897
  }
3642
3898
  function entitledEditions(index) {
3643
3899
  if (!Array.isArray(index.editions))
@@ -3756,6 +4012,7 @@ async function downloadAsset(licenseKey, releaseTag, assetName, directory, baseU
3756
4012
  }
3757
4013
 
3758
4014
  // src/scaffold.ts
4015
+ var SHA256_CHECKSUM_LINE_RE = /^([a-f0-9]{64})\s+\*?([^\s]+)$/i;
3759
4016
  var execFileAsync = promisify(execFile);
3760
4017
  async function assertEmptyTarget(targetDirectory) {
3761
4018
  try {
@@ -3825,7 +4082,7 @@ async function assertNoSymbolicLinks(directory, root = directory, isIncluded) {
3825
4082
  }
3826
4083
  }
3827
4084
  function verifyReleaseMetadata(checksum, manifest, artifactName, digest, templateRef) {
3828
- const checksumMatch = checksum.trim().match(/^([a-f0-9]{64})\s+\*?([^\s]+)$/i);
4085
+ const checksumMatch = checksum.trim().match(SHA256_CHECKSUM_LINE_RE);
3829
4086
  if (!checksumMatch || checksumMatch[2] !== artifactName) {
3830
4087
  throw new Error(`${CLI_NAME2} release checksum metadata is malformed.`);
3831
4088
  }
@@ -4041,6 +4298,9 @@ async function assembleEditions(target, selection, { keepStartOverlay = false }
4041
4298
  async function applyStartIdentity(target, appName) {
4042
4299
  await rewriteIfPresent(join7(target, "apps/start/wrangler.jsonc"), (content) => rewriteStartWorkerIdentifiers(content, appName));
4043
4300
  }
4301
+ async function renameSiteWorker(target, appName) {
4302
+ await rewriteIfPresent(join7(target, "apps/site/wrangler.jsonc"), (content) => rewriteSiteWorkerName(content, appName));
4303
+ }
4044
4304
  async function renameFrontendWorker(target, appName) {
4045
4305
  const workerPath = join7(target, "apps/web/wrangler.toml");
4046
4306
  if (!await stat4(workerPath).catch(() => {
@@ -4152,6 +4412,7 @@ async function scaffold(options) {
4152
4412
  const packagePath = join7(target, "package.json");
4153
4413
  const packageReplacements = await rewriteFile(packagePath, (content) => rewriteRootPackageName(content, options.appName));
4154
4414
  await renameFrontendWorker(target, options.appName);
4415
+ await renameSiteWorker(target, options.appName);
4155
4416
  const scope = await applyScopeRewrite(target, options.scope);
4156
4417
  if (selection.web) {
4157
4418
  await neutralizeAgentInstructions(target);
@@ -4185,6 +4446,10 @@ async function scaffold(options) {
4185
4446
  }
4186
4447
 
4187
4448
  // src/cli.ts
4449
+ var COMBINING_MARK_RE = /\p{Mark}/gu;
4450
+ var NON_ALPHANUMERIC_RE = /[^a-z0-9]+/g;
4451
+ var OUTER_DASHES_RE = /^-+|-+$/g;
4452
+
4188
4453
  class CliCancelled extends Error {
4189
4454
  }
4190
4455
  function usage() {
@@ -4215,15 +4480,15 @@ Options:
4215
4480
  --no-install Skip dependency installation
4216
4481
  --start Start Postgres and development servers after creation
4217
4482
  --no-start Create the application without starting it
4218
- --cloud <provider> Configure AWS, GCP, or Azure deployment during setup
4483
+ --cloud <provider> Configure AWS, GCP, or Railway deployment during setup
4219
4484
  --storage <provider> Select r2, s3, or gcs object storage
4220
- --no-deploy Keep every cloud option for later deployment setup
4221
- --frontend <target> Select native, vercel, or cloudflare frontend hosting
4485
+ --no-deploy Keep every deployment option for later setup
4486
+ --frontend <target> Select native or cloudflare frontend hosting (Railway: cloudflare)
4222
4487
  --deploy-environment <name> Deployment environment (default: production)
4223
- --region <name> Cloud region
4488
+ --region <name> AWS or GCP region
4224
4489
  --api-domain <host> Stable production API hostname
4225
4490
  --frontend-domain <host> Stable production frontend hostname
4226
- --ha / --no-ha Enable or disable the higher-availability profile
4491
+ --ha / --no-ha Enable or disable the AWS or GCP higher-availability profile
4227
4492
  -v, --version Print the CLI version
4228
4493
  -h, --help Show this help
4229
4494
 
@@ -4231,7 +4496,8 @@ Agent tooling:
4231
4496
  ${agentSetupUsage(CLI_NAME)}
4232
4497
 
4233
4498
  Deployment:
4234
- Run pnpm deploy:configure inside a generated app to make a deferred cloud selection.`;
4499
+ Run pnpm deploy:configure inside a generated app to make a deferred deployment selection.
4500
+ The TanStack Start edition always deploys to Cloudflare Workers and D1.`;
4235
4501
  }
4236
4502
  function takeValue3(args, index, flag) {
4237
4503
  const value = args[index + 1];
@@ -4334,7 +4600,7 @@ function suggestBrandName(appName) {
4334
4600
  return appName.split("-").map((segment) => productAcronyms[segment] ?? `${segment.charAt(0).toUpperCase()}${segment.slice(1)}`).join(" ");
4335
4601
  }
4336
4602
  function suggestScope(brandName, fallbackAppName) {
4337
- const namespace = brandName.normalize("NFKD").replace(/\p{Mark}/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4603
+ const namespace = brandName.normalize("NFKD").replace(COMBINING_MARK_RE, "").toLowerCase().replace(NON_ALPHANUMERIC_RE, "-").replace(OUTER_DASHES_RE, "");
4338
4604
  return namespace || fallbackAppName;
4339
4605
  }
4340
4606
  async function unwrapPrompt(value) {
@@ -4700,7 +4966,7 @@ ${completion.database}`);
4700
4966
  log.info("Mobile pnpm dev:mobile \u2014 run `npx eas init` inside apps/mobile to claim your own\n Expo project before the first build. See docs/mobile.md.");
4701
4967
  }
4702
4968
  if (options.deployment) {
4703
- log.info(`Deployment ${options.deployment.cloud.toUpperCase()} + ${options.deployment.storageProvider} + ${options.deployment.frontendTarget} locked. Review deployment/README.md.`);
4969
+ log.info(`Deployment ${cloudLabels2[options.deployment.cloud]} + ${options.deployment.storageProvider} + ${options.deployment.frontendTarget} locked. Review deployment/README.md.`);
4704
4970
  } else {
4705
4971
  log.info("Deployment deferred. Run `pnpm deploy:configure` when you are ready.");
4706
4972
  }