@lastshotlabs/snapshot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,4529 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // node_modules/sisteransi/src/index.js
29
+ var require_src = __commonJS({
30
+ "node_modules/sisteransi/src/index.js"(exports, module) {
31
+ "use strict";
32
+ var ESC = "\x1B";
33
+ var CSI = `${ESC}[`;
34
+ var beep = "\x07";
35
+ var cursor = {
36
+ to(x3, y2) {
37
+ if (!y2) return `${CSI}${x3 + 1}G`;
38
+ return `${CSI}${y2 + 1};${x3 + 1}H`;
39
+ },
40
+ move(x3, y2) {
41
+ let ret = "";
42
+ if (x3 < 0) ret += `${CSI}${-x3}D`;
43
+ else if (x3 > 0) ret += `${CSI}${x3}C`;
44
+ if (y2 < 0) ret += `${CSI}${-y2}A`;
45
+ else if (y2 > 0) ret += `${CSI}${y2}B`;
46
+ return ret;
47
+ },
48
+ up: (count = 1) => `${CSI}${count}A`,
49
+ down: (count = 1) => `${CSI}${count}B`,
50
+ forward: (count = 1) => `${CSI}${count}C`,
51
+ backward: (count = 1) => `${CSI}${count}D`,
52
+ nextLine: (count = 1) => `${CSI}E`.repeat(count),
53
+ prevLine: (count = 1) => `${CSI}F`.repeat(count),
54
+ left: `${CSI}G`,
55
+ hide: `${CSI}?25l`,
56
+ show: `${CSI}?25h`,
57
+ save: `${ESC}7`,
58
+ restore: `${ESC}8`
59
+ };
60
+ var scroll = {
61
+ up: (count = 1) => `${CSI}S`.repeat(count),
62
+ down: (count = 1) => `${CSI}T`.repeat(count)
63
+ };
64
+ var erase = {
65
+ screen: `${CSI}2J`,
66
+ up: (count = 1) => `${CSI}1J`.repeat(count),
67
+ down: (count = 1) => `${CSI}J`.repeat(count),
68
+ line: `${CSI}2K`,
69
+ lineEnd: `${CSI}K`,
70
+ lineStart: `${CSI}1K`,
71
+ lines(count) {
72
+ let clear = "";
73
+ for (let i = 0; i < count; i++)
74
+ clear += this.line + (i < count - 1 ? cursor.up() : "");
75
+ if (count)
76
+ clear += cursor.left;
77
+ return clear;
78
+ }
79
+ };
80
+ module.exports = { cursor, scroll, erase, beep };
81
+ }
82
+ });
83
+
84
+ // src/cli/index.ts
85
+ import process2 from "process";
86
+ import path5 from "path";
87
+
88
+ // node_modules/@clack/core/dist/index.mjs
89
+ var import_sisteransi = __toESM(require_src(), 1);
90
+ import { styleText as D } from "util";
91
+ import { stdout as R, stdin as q } from "process";
92
+ import * as k from "readline";
93
+ import ot from "readline";
94
+ import { ReadStream as J } from "tty";
95
+ function x(t2, e, s) {
96
+ if (!s.some((u) => !u.disabled)) return t2;
97
+ const i = t2 + e, r = Math.max(s.length - 1, 0), n = i < 0 ? r : i > r ? 0 : i;
98
+ return s[n].disabled ? x(n, e < 0 ? -1 : 1, s) : n;
99
+ }
100
+ var at = (t2) => t2 === 161 || t2 === 164 || t2 === 167 || t2 === 168 || t2 === 170 || t2 === 173 || t2 === 174 || t2 >= 176 && t2 <= 180 || t2 >= 182 && t2 <= 186 || t2 >= 188 && t2 <= 191 || t2 === 198 || t2 === 208 || t2 === 215 || t2 === 216 || t2 >= 222 && t2 <= 225 || t2 === 230 || t2 >= 232 && t2 <= 234 || t2 === 236 || t2 === 237 || t2 === 240 || t2 === 242 || t2 === 243 || t2 >= 247 && t2 <= 250 || t2 === 252 || t2 === 254 || t2 === 257 || t2 === 273 || t2 === 275 || t2 === 283 || t2 === 294 || t2 === 295 || t2 === 299 || t2 >= 305 && t2 <= 307 || t2 === 312 || t2 >= 319 && t2 <= 322 || t2 === 324 || t2 >= 328 && t2 <= 331 || t2 === 333 || t2 === 338 || t2 === 339 || t2 === 358 || t2 === 359 || t2 === 363 || t2 === 462 || t2 === 464 || t2 === 466 || t2 === 468 || t2 === 470 || t2 === 472 || t2 === 474 || t2 === 476 || t2 === 593 || t2 === 609 || t2 === 708 || t2 === 711 || t2 >= 713 && t2 <= 715 || t2 === 717 || t2 === 720 || t2 >= 728 && t2 <= 731 || t2 === 733 || t2 === 735 || t2 >= 768 && t2 <= 879 || t2 >= 913 && t2 <= 929 || t2 >= 931 && t2 <= 937 || t2 >= 945 && t2 <= 961 || t2 >= 963 && t2 <= 969 || t2 === 1025 || t2 >= 1040 && t2 <= 1103 || t2 === 1105 || t2 === 8208 || t2 >= 8211 && t2 <= 8214 || t2 === 8216 || t2 === 8217 || t2 === 8220 || t2 === 8221 || t2 >= 8224 && t2 <= 8226 || t2 >= 8228 && t2 <= 8231 || t2 === 8240 || t2 === 8242 || t2 === 8243 || t2 === 8245 || t2 === 8251 || t2 === 8254 || t2 === 8308 || t2 === 8319 || t2 >= 8321 && t2 <= 8324 || t2 === 8364 || t2 === 8451 || t2 === 8453 || t2 === 8457 || t2 === 8467 || t2 === 8470 || t2 === 8481 || t2 === 8482 || t2 === 8486 || t2 === 8491 || t2 === 8531 || t2 === 8532 || t2 >= 8539 && t2 <= 8542 || t2 >= 8544 && t2 <= 8555 || t2 >= 8560 && t2 <= 8569 || t2 === 8585 || t2 >= 8592 && t2 <= 8601 || t2 === 8632 || t2 === 8633 || t2 === 8658 || t2 === 8660 || t2 === 8679 || t2 === 8704 || t2 === 8706 || t2 === 8707 || t2 === 8711 || t2 === 8712 || t2 === 8715 || t2 === 8719 || t2 === 8721 || t2 === 8725 || t2 === 8730 || t2 >= 8733 && t2 <= 8736 || t2 === 8739 || t2 === 8741 || t2 >= 8743 && t2 <= 8748 || t2 === 8750 || t2 >= 8756 && t2 <= 8759 || t2 === 8764 || t2 === 8765 || t2 === 8776 || t2 === 8780 || t2 === 8786 || t2 === 8800 || t2 === 8801 || t2 >= 8804 && t2 <= 8807 || t2 === 8810 || t2 === 8811 || t2 === 8814 || t2 === 8815 || t2 === 8834 || t2 === 8835 || t2 === 8838 || t2 === 8839 || t2 === 8853 || t2 === 8857 || t2 === 8869 || t2 === 8895 || t2 === 8978 || t2 >= 9312 && t2 <= 9449 || t2 >= 9451 && t2 <= 9547 || t2 >= 9552 && t2 <= 9587 || t2 >= 9600 && t2 <= 9615 || t2 >= 9618 && t2 <= 9621 || t2 === 9632 || t2 === 9633 || t2 >= 9635 && t2 <= 9641 || t2 === 9650 || t2 === 9651 || t2 === 9654 || t2 === 9655 || t2 === 9660 || t2 === 9661 || t2 === 9664 || t2 === 9665 || t2 >= 9670 && t2 <= 9672 || t2 === 9675 || t2 >= 9678 && t2 <= 9681 || t2 >= 9698 && t2 <= 9701 || t2 === 9711 || t2 === 9733 || t2 === 9734 || t2 === 9737 || t2 === 9742 || t2 === 9743 || t2 === 9756 || t2 === 9758 || t2 === 9792 || t2 === 9794 || t2 === 9824 || t2 === 9825 || t2 >= 9827 && t2 <= 9829 || t2 >= 9831 && t2 <= 9834 || t2 === 9836 || t2 === 9837 || t2 === 9839 || t2 === 9886 || t2 === 9887 || t2 === 9919 || t2 >= 9926 && t2 <= 9933 || t2 >= 9935 && t2 <= 9939 || t2 >= 9941 && t2 <= 9953 || t2 === 9955 || t2 === 9960 || t2 === 9961 || t2 >= 9963 && t2 <= 9969 || t2 === 9972 || t2 >= 9974 && t2 <= 9977 || t2 === 9979 || t2 === 9980 || t2 === 9982 || t2 === 9983 || t2 === 10045 || t2 >= 10102 && t2 <= 10111 || t2 >= 11094 && t2 <= 11097 || t2 >= 12872 && t2 <= 12879 || t2 >= 57344 && t2 <= 63743 || t2 >= 65024 && t2 <= 65039 || t2 === 65533 || t2 >= 127232 && t2 <= 127242 || t2 >= 127248 && t2 <= 127277 || t2 >= 127280 && t2 <= 127337 || t2 >= 127344 && t2 <= 127373 || t2 === 127375 || t2 === 127376 || t2 >= 127387 && t2 <= 127404 || t2 >= 917760 && t2 <= 917999 || t2 >= 983040 && t2 <= 1048573 || t2 >= 1048576 && t2 <= 1114109;
101
+ var lt = (t2) => t2 === 12288 || t2 >= 65281 && t2 <= 65376 || t2 >= 65504 && t2 <= 65510;
102
+ var ht = (t2) => t2 >= 4352 && t2 <= 4447 || t2 === 8986 || t2 === 8987 || t2 === 9001 || t2 === 9002 || t2 >= 9193 && t2 <= 9196 || t2 === 9200 || t2 === 9203 || t2 === 9725 || t2 === 9726 || t2 === 9748 || t2 === 9749 || t2 >= 9800 && t2 <= 9811 || t2 === 9855 || t2 === 9875 || t2 === 9889 || t2 === 9898 || t2 === 9899 || t2 === 9917 || t2 === 9918 || t2 === 9924 || t2 === 9925 || t2 === 9934 || t2 === 9940 || t2 === 9962 || t2 === 9970 || t2 === 9971 || t2 === 9973 || t2 === 9978 || t2 === 9981 || t2 === 9989 || t2 === 9994 || t2 === 9995 || t2 === 10024 || t2 === 10060 || t2 === 10062 || t2 >= 10067 && t2 <= 10069 || t2 === 10071 || t2 >= 10133 && t2 <= 10135 || t2 === 10160 || t2 === 10175 || t2 === 11035 || t2 === 11036 || t2 === 11088 || t2 === 11093 || t2 >= 11904 && t2 <= 11929 || t2 >= 11931 && t2 <= 12019 || t2 >= 12032 && t2 <= 12245 || t2 >= 12272 && t2 <= 12287 || t2 >= 12289 && t2 <= 12350 || t2 >= 12353 && t2 <= 12438 || t2 >= 12441 && t2 <= 12543 || t2 >= 12549 && t2 <= 12591 || t2 >= 12593 && t2 <= 12686 || t2 >= 12688 && t2 <= 12771 || t2 >= 12783 && t2 <= 12830 || t2 >= 12832 && t2 <= 12871 || t2 >= 12880 && t2 <= 19903 || t2 >= 19968 && t2 <= 42124 || t2 >= 42128 && t2 <= 42182 || t2 >= 43360 && t2 <= 43388 || t2 >= 44032 && t2 <= 55203 || t2 >= 63744 && t2 <= 64255 || t2 >= 65040 && t2 <= 65049 || t2 >= 65072 && t2 <= 65106 || t2 >= 65108 && t2 <= 65126 || t2 >= 65128 && t2 <= 65131 || t2 >= 94176 && t2 <= 94180 || t2 === 94192 || t2 === 94193 || t2 >= 94208 && t2 <= 100343 || t2 >= 100352 && t2 <= 101589 || t2 >= 101632 && t2 <= 101640 || t2 >= 110576 && t2 <= 110579 || t2 >= 110581 && t2 <= 110587 || t2 === 110589 || t2 === 110590 || t2 >= 110592 && t2 <= 110882 || t2 === 110898 || t2 >= 110928 && t2 <= 110930 || t2 === 110933 || t2 >= 110948 && t2 <= 110951 || t2 >= 110960 && t2 <= 111355 || t2 === 126980 || t2 === 127183 || t2 === 127374 || t2 >= 127377 && t2 <= 127386 || t2 >= 127488 && t2 <= 127490 || t2 >= 127504 && t2 <= 127547 || t2 >= 127552 && t2 <= 127560 || t2 === 127568 || t2 === 127569 || t2 >= 127584 && t2 <= 127589 || t2 >= 127744 && t2 <= 127776 || t2 >= 127789 && t2 <= 127797 || t2 >= 127799 && t2 <= 127868 || t2 >= 127870 && t2 <= 127891 || t2 >= 127904 && t2 <= 127946 || t2 >= 127951 && t2 <= 127955 || t2 >= 127968 && t2 <= 127984 || t2 === 127988 || t2 >= 127992 && t2 <= 128062 || t2 === 128064 || t2 >= 128066 && t2 <= 128252 || t2 >= 128255 && t2 <= 128317 || t2 >= 128331 && t2 <= 128334 || t2 >= 128336 && t2 <= 128359 || t2 === 128378 || t2 === 128405 || t2 === 128406 || t2 === 128420 || t2 >= 128507 && t2 <= 128591 || t2 >= 128640 && t2 <= 128709 || t2 === 128716 || t2 >= 128720 && t2 <= 128722 || t2 >= 128725 && t2 <= 128727 || t2 >= 128732 && t2 <= 128735 || t2 === 128747 || t2 === 128748 || t2 >= 128756 && t2 <= 128764 || t2 >= 128992 && t2 <= 129003 || t2 === 129008 || t2 >= 129292 && t2 <= 129338 || t2 >= 129340 && t2 <= 129349 || t2 >= 129351 && t2 <= 129535 || t2 >= 129648 && t2 <= 129660 || t2 >= 129664 && t2 <= 129672 || t2 >= 129680 && t2 <= 129725 || t2 >= 129727 && t2 <= 129733 || t2 >= 129742 && t2 <= 129755 || t2 >= 129760 && t2 <= 129768 || t2 >= 129776 && t2 <= 129784 || t2 >= 131072 && t2 <= 196605 || t2 >= 196608 && t2 <= 262141;
103
+ var O = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y;
104
+ var y = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
105
+ var L = /\t{1,1000}/y;
106
+ var P = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu");
107
+ var M = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
108
+ var ct = new RegExp("\\p{M}+", "gu");
109
+ var ft = { limit: 1 / 0, ellipsis: "" };
110
+ var X = (t2, e = {}, s = {}) => {
111
+ const i = e.limit ?? 1 / 0, r = e.ellipsis ?? "", n = e?.ellipsisWidth ?? (r ? X(r, ft, s).width : 0), u = s.ansiWidth ?? 0, a = s.controlWidth ?? 0, l = s.tabWidth ?? 8, E = s.ambiguousWidth ?? 1, g = s.emojiWidth ?? 2, m = s.fullWidthWidth ?? 2, A = s.regularWidth ?? 1, V2 = s.wideWidth ?? 2;
112
+ let h2 = 0, o = 0, p = t2.length, v = 0, F = false, d = p, b = Math.max(0, i - n), C = 0, w = 0, c = 0, f = 0;
113
+ t: for (; ; ) {
114
+ if (w > C || o >= p && o > h2) {
115
+ const ut = t2.slice(C, w) || t2.slice(h2, o);
116
+ v = 0;
117
+ for (const Y of ut.replaceAll(ct, "")) {
118
+ const $ = Y.codePointAt(0) || 0;
119
+ if (lt($) ? f = m : ht($) ? f = V2 : E !== A && at($) ? f = E : f = A, c + f > b && (d = Math.min(d, Math.max(C, h2) + v)), c + f > i) {
120
+ F = true;
121
+ break t;
122
+ }
123
+ v += Y.length, c += f;
124
+ }
125
+ C = w = 0;
126
+ }
127
+ if (o >= p) break;
128
+ if (M.lastIndex = o, M.test(t2)) {
129
+ if (v = M.lastIndex - o, f = v * A, c + f > b && (d = Math.min(d, o + Math.floor((b - c) / A))), c + f > i) {
130
+ F = true;
131
+ break;
132
+ }
133
+ c += f, C = h2, w = o, o = h2 = M.lastIndex;
134
+ continue;
135
+ }
136
+ if (O.lastIndex = o, O.test(t2)) {
137
+ if (c + u > b && (d = Math.min(d, o)), c + u > i) {
138
+ F = true;
139
+ break;
140
+ }
141
+ c += u, C = h2, w = o, o = h2 = O.lastIndex;
142
+ continue;
143
+ }
144
+ if (y.lastIndex = o, y.test(t2)) {
145
+ if (v = y.lastIndex - o, f = v * a, c + f > b && (d = Math.min(d, o + Math.floor((b - c) / a))), c + f > i) {
146
+ F = true;
147
+ break;
148
+ }
149
+ c += f, C = h2, w = o, o = h2 = y.lastIndex;
150
+ continue;
151
+ }
152
+ if (L.lastIndex = o, L.test(t2)) {
153
+ if (v = L.lastIndex - o, f = v * l, c + f > b && (d = Math.min(d, o + Math.floor((b - c) / l))), c + f > i) {
154
+ F = true;
155
+ break;
156
+ }
157
+ c += f, C = h2, w = o, o = h2 = L.lastIndex;
158
+ continue;
159
+ }
160
+ if (P.lastIndex = o, P.test(t2)) {
161
+ if (c + g > b && (d = Math.min(d, o)), c + g > i) {
162
+ F = true;
163
+ break;
164
+ }
165
+ c += g, C = h2, w = o, o = h2 = P.lastIndex;
166
+ continue;
167
+ }
168
+ o += 1;
169
+ }
170
+ return { width: F ? b : c, index: F ? d : p, truncated: F, ellipsed: F && i >= n };
171
+ };
172
+ var pt = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 };
173
+ var S = (t2, e = {}) => X(t2, pt, e).width;
174
+ var T = "\x1B";
175
+ var Z = "\x9B";
176
+ var Ft = 39;
177
+ var j = "\x07";
178
+ var Q = "[";
179
+ var dt = "]";
180
+ var tt = "m";
181
+ var U = `${dt}8;;`;
182
+ var et = new RegExp(`(?:\\${Q}(?<code>\\d+)m|\\${U}(?<uri>.*)${j})`, "y");
183
+ var mt = (t2) => {
184
+ if (t2 >= 30 && t2 <= 37 || t2 >= 90 && t2 <= 97) return 39;
185
+ if (t2 >= 40 && t2 <= 47 || t2 >= 100 && t2 <= 107) return 49;
186
+ if (t2 === 1 || t2 === 2) return 22;
187
+ if (t2 === 3) return 23;
188
+ if (t2 === 4) return 24;
189
+ if (t2 === 7) return 27;
190
+ if (t2 === 8) return 28;
191
+ if (t2 === 9) return 29;
192
+ if (t2 === 0) return 0;
193
+ };
194
+ var st = (t2) => `${T}${Q}${t2}${tt}`;
195
+ var it = (t2) => `${T}${U}${t2}${j}`;
196
+ var gt = (t2) => t2.map((e) => S(e));
197
+ var G = (t2, e, s) => {
198
+ const i = e[Symbol.iterator]();
199
+ let r = false, n = false, u = t2.at(-1), a = u === void 0 ? 0 : S(u), l = i.next(), E = i.next(), g = 0;
200
+ for (; !l.done; ) {
201
+ const m = l.value, A = S(m);
202
+ a + A <= s ? t2[t2.length - 1] += m : (t2.push(m), a = 0), (m === T || m === Z) && (r = true, n = e.startsWith(U, g + 1)), r ? n ? m === j && (r = false, n = false) : m === tt && (r = false) : (a += A, a === s && !E.done && (t2.push(""), a = 0)), l = E, E = i.next(), g += m.length;
203
+ }
204
+ u = t2.at(-1), !a && u !== void 0 && u.length > 0 && t2.length > 1 && (t2[t2.length - 2] += t2.pop());
205
+ };
206
+ var vt = (t2) => {
207
+ const e = t2.split(" ");
208
+ let s = e.length;
209
+ for (; s > 0 && !(S(e[s - 1]) > 0); ) s--;
210
+ return s === e.length ? t2 : e.slice(0, s).join(" ") + e.slice(s).join("");
211
+ };
212
+ var Et = (t2, e, s = {}) => {
213
+ if (s.trim !== false && t2.trim() === "") return "";
214
+ let i = "", r, n;
215
+ const u = t2.split(" "), a = gt(u);
216
+ let l = [""];
217
+ for (const [h2, o] of u.entries()) {
218
+ s.trim !== false && (l[l.length - 1] = (l.at(-1) ?? "").trimStart());
219
+ let p = S(l.at(-1) ?? "");
220
+ if (h2 !== 0 && (p >= e && (s.wordWrap === false || s.trim === false) && (l.push(""), p = 0), (p > 0 || s.trim === false) && (l[l.length - 1] += " ", p++)), s.hard && a[h2] > e) {
221
+ const v = e - p, F = 1 + Math.floor((a[h2] - v - 1) / e);
222
+ Math.floor((a[h2] - 1) / e) < F && l.push(""), G(l, o, e);
223
+ continue;
224
+ }
225
+ if (p + a[h2] > e && p > 0 && a[h2] > 0) {
226
+ if (s.wordWrap === false && p < e) {
227
+ G(l, o, e);
228
+ continue;
229
+ }
230
+ l.push("");
231
+ }
232
+ if (p + a[h2] > e && s.wordWrap === false) {
233
+ G(l, o, e);
234
+ continue;
235
+ }
236
+ l[l.length - 1] += o;
237
+ }
238
+ s.trim !== false && (l = l.map((h2) => vt(h2)));
239
+ const E = l.join(`
240
+ `), g = E[Symbol.iterator]();
241
+ let m = g.next(), A = g.next(), V2 = 0;
242
+ for (; !m.done; ) {
243
+ const h2 = m.value, o = A.value;
244
+ if (i += h2, h2 === T || h2 === Z) {
245
+ et.lastIndex = V2 + 1;
246
+ const F = et.exec(E)?.groups;
247
+ if (F?.code !== void 0) {
248
+ const d = Number.parseFloat(F.code);
249
+ r = d === Ft ? void 0 : d;
250
+ } else F?.uri !== void 0 && (n = F.uri.length === 0 ? void 0 : F.uri);
251
+ }
252
+ const p = r ? mt(r) : void 0;
253
+ o === `
254
+ ` ? (n && (i += it("")), r && p && (i += st(p))) : h2 === `
255
+ ` && (r && p && (i += st(r)), n && (i += it(n))), V2 += h2.length, m = A, A = g.next();
256
+ }
257
+ return i;
258
+ };
259
+ function K(t2, e, s) {
260
+ return String(t2).normalize().replaceAll(`\r
261
+ `, `
262
+ `).split(`
263
+ `).map((i) => Et(i, e, s)).join(`
264
+ `);
265
+ }
266
+ var At = ["up", "down", "left", "right", "space", "enter", "cancel"];
267
+ var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true };
268
+ function H(t2, e) {
269
+ if (typeof t2 == "string") return _.aliases.get(t2) === e;
270
+ for (const s of t2) if (s !== void 0 && H(s, e)) return true;
271
+ return false;
272
+ }
273
+ function _t(t2, e) {
274
+ if (t2 === e) return;
275
+ const s = t2.split(`
276
+ `), i = e.split(`
277
+ `), r = Math.max(s.length, i.length), n = [];
278
+ for (let u = 0; u < r; u++) s[u] !== i[u] && n.push(u);
279
+ return { lines: n, numLinesBefore: s.length, numLinesAfter: i.length, numLines: r };
280
+ }
281
+ var bt = globalThis.process.platform.startsWith("win");
282
+ var z = /* @__PURE__ */ Symbol("clack:cancel");
283
+ function Ct(t2) {
284
+ return t2 === z;
285
+ }
286
+ function W(t2, e) {
287
+ const s = t2;
288
+ s.isTTY && s.setRawMode(e);
289
+ }
290
+ function xt({ input: t2 = q, output: e = R, overwrite: s = true, hideCursor: i = true } = {}) {
291
+ const r = k.createInterface({ input: t2, output: e, prompt: "", tabSize: 1 });
292
+ k.emitKeypressEvents(t2, r), t2 instanceof J && t2.isTTY && t2.setRawMode(true);
293
+ const n = (u, { name: a, sequence: l }) => {
294
+ const E = String(u);
295
+ if (H([E, a, l], "cancel")) {
296
+ i && e.write(import_sisteransi.cursor.show), process.exit(0);
297
+ return;
298
+ }
299
+ if (!s) return;
300
+ const g = a === "return" ? 0 : -1, m = a === "return" ? -1 : 0;
301
+ k.moveCursor(e, g, m, () => {
302
+ k.clearLine(e, 1, () => {
303
+ t2.once("keypress", n);
304
+ });
305
+ });
306
+ };
307
+ return i && e.write(import_sisteransi.cursor.hide), t2.once("keypress", n), () => {
308
+ t2.off("keypress", n), i && e.write(import_sisteransi.cursor.show), t2 instanceof J && t2.isTTY && !bt && t2.setRawMode(false), r.terminal = false, r.close();
309
+ };
310
+ }
311
+ var rt = (t2) => "columns" in t2 && typeof t2.columns == "number" ? t2.columns : 80;
312
+ var nt = (t2) => "rows" in t2 && typeof t2.rows == "number" ? t2.rows : 20;
313
+ function Bt(t2, e, s, i = s) {
314
+ const r = rt(t2 ?? R);
315
+ return K(e, r - s.length, { hard: true, trim: false }).split(`
316
+ `).map((n, u) => `${u === 0 ? i : s}${n}`).join(`
317
+ `);
318
+ }
319
+ var B = class {
320
+ input;
321
+ output;
322
+ _abortSignal;
323
+ rl;
324
+ opts;
325
+ _render;
326
+ _track = false;
327
+ _prevFrame = "";
328
+ _subscribers = /* @__PURE__ */ new Map();
329
+ _cursor = 0;
330
+ state = "initial";
331
+ error = "";
332
+ value;
333
+ userInput = "";
334
+ constructor(e, s = true) {
335
+ const { input: i = q, output: r = R, render: n, signal: u, ...a } = e;
336
+ this.opts = a, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = n.bind(this), this._track = s, this._abortSignal = u, this.input = i, this.output = r;
337
+ }
338
+ unsubscribe() {
339
+ this._subscribers.clear();
340
+ }
341
+ setSubscriber(e, s) {
342
+ const i = this._subscribers.get(e) ?? [];
343
+ i.push(s), this._subscribers.set(e, i);
344
+ }
345
+ on(e, s) {
346
+ this.setSubscriber(e, { cb: s });
347
+ }
348
+ once(e, s) {
349
+ this.setSubscriber(e, { cb: s, once: true });
350
+ }
351
+ emit(e, ...s) {
352
+ const i = this._subscribers.get(e) ?? [], r = [];
353
+ for (const n of i) n.cb(...s), n.once && r.push(() => i.splice(i.indexOf(n), 1));
354
+ for (const n of r) n();
355
+ }
356
+ prompt() {
357
+ return new Promise((e) => {
358
+ if (this._abortSignal) {
359
+ if (this._abortSignal.aborted) return this.state = "cancel", this.close(), e(z);
360
+ this._abortSignal.addEventListener("abort", () => {
361
+ this.state = "cancel", this.close();
362
+ }, { once: true });
363
+ }
364
+ this.rl = ot.createInterface({ input: this.input, tabSize: 2, prompt: "", escapeCodeTimeout: 50, terminal: true }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), W(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => {
365
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), W(this.input, false), e(this.value);
366
+ }), this.once("cancel", () => {
367
+ this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), W(this.input, false), e(z);
368
+ });
369
+ });
370
+ }
371
+ _isActionKey(e, s) {
372
+ return e === " ";
373
+ }
374
+ _setValue(e) {
375
+ this.value = e, this.emit("value", this.value);
376
+ }
377
+ _setUserInput(e, s) {
378
+ this.userInput = e ?? "", this.emit("userInput", this.userInput), s && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor);
379
+ }
380
+ _clearUserInput() {
381
+ this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput("");
382
+ }
383
+ onKeypress(e, s) {
384
+ if (this._track && s.name !== "return" && (s.name && this._isActionKey(e, s) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), s?.name && (!this._track && _.aliases.has(s.name) && this.emit("cursor", _.aliases.get(s.name)), _.actions.has(s.name) && this.emit("cursor", s.name)), e && (e.toLowerCase() === "y" || e.toLowerCase() === "n") && this.emit("confirm", e.toLowerCase() === "y"), this.emit("key", e?.toLowerCase(), s), s?.name === "return") {
385
+ if (this.opts.validate) {
386
+ const i = this.opts.validate(this.value);
387
+ i && (this.error = i instanceof Error ? i.message : i, this.state = "error", this.rl?.write(this.userInput));
388
+ }
389
+ this.state !== "error" && (this.state = "submit");
390
+ }
391
+ H([e, s?.name, s?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close();
392
+ }
393
+ close() {
394
+ this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(`
395
+ `), W(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe();
396
+ }
397
+ restoreCursor() {
398
+ const e = K(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(`
399
+ `).length - 1;
400
+ this.output.write(import_sisteransi.cursor.move(-999, e * -1));
401
+ }
402
+ render() {
403
+ const e = K(this._render(this) ?? "", process.stdout.columns, { hard: true, trim: false });
404
+ if (e !== this._prevFrame) {
405
+ if (this.state === "initial") this.output.write(import_sisteransi.cursor.hide);
406
+ else {
407
+ const s = _t(this._prevFrame, e), i = nt(this.output);
408
+ if (this.restoreCursor(), s) {
409
+ const r = Math.max(0, s.numLinesAfter - i), n = Math.max(0, s.numLinesBefore - i);
410
+ let u = s.lines.find((a) => a >= r);
411
+ if (u === void 0) {
412
+ this._prevFrame = e;
413
+ return;
414
+ }
415
+ if (s.lines.length === 1) {
416
+ this.output.write(import_sisteransi.cursor.move(0, u - n)), this.output.write(import_sisteransi.erase.lines(1));
417
+ const a = e.split(`
418
+ `);
419
+ this.output.write(a[u]), this._prevFrame = e, this.output.write(import_sisteransi.cursor.move(0, a.length - u - 1));
420
+ return;
421
+ } else if (s.lines.length > 1) {
422
+ if (r < n) u = r;
423
+ else {
424
+ const l = u - n;
425
+ l > 0 && this.output.write(import_sisteransi.cursor.move(0, l));
426
+ }
427
+ this.output.write(import_sisteransi.erase.down());
428
+ const a = e.split(`
429
+ `).slice(u);
430
+ this.output.write(a.join(`
431
+ `)), this._prevFrame = e;
432
+ return;
433
+ }
434
+ }
435
+ this.output.write(import_sisteransi.erase.down());
436
+ }
437
+ this.output.write(e), this.state === "initial" && (this.state = "active"), this._prevFrame = e;
438
+ }
439
+ }
440
+ };
441
+ var kt = class extends B {
442
+ get cursor() {
443
+ return this.value ? 0 : 1;
444
+ }
445
+ get _value() {
446
+ return this.cursor === 0;
447
+ }
448
+ constructor(e) {
449
+ super(e, false), this.value = !!e.initialValue, this.on("userInput", () => {
450
+ this.value = this._value;
451
+ }), this.on("confirm", (s) => {
452
+ this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = s, this.state = "submit", this.close();
453
+ }), this.on("cursor", () => {
454
+ this.value = !this.value;
455
+ });
456
+ }
457
+ };
458
+ var Lt = class extends B {
459
+ options;
460
+ cursor = 0;
461
+ get _value() {
462
+ return this.options[this.cursor].value;
463
+ }
464
+ get _enabledOptions() {
465
+ return this.options.filter((e) => e.disabled !== true);
466
+ }
467
+ toggleAll() {
468
+ const e = this._enabledOptions, s = this.value !== void 0 && this.value.length === e.length;
469
+ this.value = s ? [] : e.map((i) => i.value);
470
+ }
471
+ toggleInvert() {
472
+ const e = this.value;
473
+ if (!e) return;
474
+ const s = this._enabledOptions.filter((i) => !e.includes(i.value));
475
+ this.value = s.map((i) => i.value);
476
+ }
477
+ toggleValue() {
478
+ this.value === void 0 && (this.value = []);
479
+ const e = this.value.includes(this._value);
480
+ this.value = e ? this.value.filter((s) => s !== this._value) : [...this.value, this._value];
481
+ }
482
+ constructor(e) {
483
+ super(e, false), this.options = e.options, this.value = [...e.initialValues ?? []];
484
+ const s = Math.max(this.options.findIndex(({ value: i }) => i === e.cursorAt), 0);
485
+ this.cursor = this.options[s].disabled ? x(s, 1, this.options) : s, this.on("key", (i) => {
486
+ i === "a" && this.toggleAll(), i === "i" && this.toggleInvert();
487
+ }), this.on("cursor", (i) => {
488
+ switch (i) {
489
+ case "left":
490
+ case "up":
491
+ this.cursor = x(this.cursor, -1, this.options);
492
+ break;
493
+ case "down":
494
+ case "right":
495
+ this.cursor = x(this.cursor, 1, this.options);
496
+ break;
497
+ case "space":
498
+ this.toggleValue();
499
+ break;
500
+ }
501
+ });
502
+ }
503
+ };
504
+ var Tt = class extends B {
505
+ options;
506
+ cursor = 0;
507
+ get _selectedValue() {
508
+ return this.options[this.cursor];
509
+ }
510
+ changeValue() {
511
+ this.value = this._selectedValue.value;
512
+ }
513
+ constructor(e) {
514
+ super(e, false), this.options = e.options;
515
+ const s = this.options.findIndex(({ value: r }) => r === e.initialValue), i = s === -1 ? 0 : s;
516
+ this.cursor = this.options[i].disabled ? x(i, 1, this.options) : i, this.changeValue(), this.on("cursor", (r) => {
517
+ switch (r) {
518
+ case "left":
519
+ case "up":
520
+ this.cursor = x(this.cursor, -1, this.options);
521
+ break;
522
+ case "down":
523
+ case "right":
524
+ this.cursor = x(this.cursor, 1, this.options);
525
+ break;
526
+ }
527
+ this.changeValue();
528
+ });
529
+ }
530
+ };
531
+ var $t = class extends B {
532
+ get userInputWithCursor() {
533
+ if (this.state === "submit") return this.userInput;
534
+ const e = this.userInput;
535
+ if (this.cursor >= e.length) return `${this.userInput}\u2588`;
536
+ const s = e.slice(0, this.cursor), [i, ...r] = e.slice(this.cursor);
537
+ return `${s}${D("inverse", i)}${r.join("")}`;
538
+ }
539
+ get cursor() {
540
+ return this._cursor;
541
+ }
542
+ constructor(e) {
543
+ super({ ...e, initialUserInput: e.initialUserInput ?? e.initialValue }), this.on("userInput", (s) => {
544
+ this._setValue(s);
545
+ }), this.on("finalize", () => {
546
+ this.value || (this.value = e.defaultValue), this.value === void 0 && (this.value = "");
547
+ });
548
+ }
549
+ };
550
+
551
+ // node_modules/@clack/prompts/dist/index.mjs
552
+ var import_sisteransi2 = __toESM(require_src(), 1);
553
+ import { styleText as t, stripVTControlCharacters as ue } from "util";
554
+ import N2 from "process";
555
+ import { readdirSync as $t2, existsSync as dt2, lstatSync as xe } from "fs";
556
+ import { dirname as _e, join as ht2 } from "path";
557
+ function pt2() {
558
+ return N2.platform !== "win32" ? N2.env.TERM !== "linux" : !!N2.env.CI || !!N2.env.WT_SESSION || !!N2.env.TERMINUS_SUBLIME || N2.env.ConEmuTask === "{cmd::Cmder}" || N2.env.TERM_PROGRAM === "Terminus-Sublime" || N2.env.TERM_PROGRAM === "vscode" || N2.env.TERM === "xterm-256color" || N2.env.TERM === "alacritty" || N2.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
559
+ }
560
+ var ee = pt2();
561
+ var ce = () => process.env.CI === "true";
562
+ var I2 = (e, r) => ee ? e : r;
563
+ var Re = I2("\u25C6", "*");
564
+ var $e = I2("\u25A0", "x");
565
+ var de = I2("\u25B2", "x");
566
+ var V = I2("\u25C7", "o");
567
+ var he = I2("\u250C", "T");
568
+ var h = I2("\u2502", "|");
569
+ var x2 = I2("\u2514", "\u2014");
570
+ var Oe = I2("\u2510", "T");
571
+ var Pe = I2("\u2518", "\u2014");
572
+ var z2 = I2("\u25CF", ">");
573
+ var H2 = I2("\u25CB", " ");
574
+ var te = I2("\u25FB", "[\u2022]");
575
+ var U2 = I2("\u25FC", "[+]");
576
+ var q2 = I2("\u25FB", "[ ]");
577
+ var Ne = I2("\u25AA", "\u2022");
578
+ var se = I2("\u2500", "-");
579
+ var pe = I2("\u256E", "+");
580
+ var We = I2("\u251C", "+");
581
+ var me = I2("\u256F", "+");
582
+ var ge = I2("\u2570", "+");
583
+ var Ge = I2("\u256D", "+");
584
+ var fe = I2("\u25CF", "\u2022");
585
+ var Fe = I2("\u25C6", "*");
586
+ var ye = I2("\u25B2", "!");
587
+ var Ee = I2("\u25A0", "x");
588
+ var W2 = (e) => {
589
+ switch (e) {
590
+ case "initial":
591
+ case "active":
592
+ return t("cyan", Re);
593
+ case "cancel":
594
+ return t("red", $e);
595
+ case "error":
596
+ return t("yellow", de);
597
+ case "submit":
598
+ return t("green", V);
599
+ }
600
+ };
601
+ var ve = (e) => {
602
+ switch (e) {
603
+ case "initial":
604
+ case "active":
605
+ return t("cyan", h);
606
+ case "cancel":
607
+ return t("red", h);
608
+ case "error":
609
+ return t("yellow", h);
610
+ case "submit":
611
+ return t("green", h);
612
+ }
613
+ };
614
+ var mt2 = (e) => e === 161 || e === 164 || e === 167 || e === 168 || e === 170 || e === 173 || e === 174 || e >= 176 && e <= 180 || e >= 182 && e <= 186 || e >= 188 && e <= 191 || e === 198 || e === 208 || e === 215 || e === 216 || e >= 222 && e <= 225 || e === 230 || e >= 232 && e <= 234 || e === 236 || e === 237 || e === 240 || e === 242 || e === 243 || e >= 247 && e <= 250 || e === 252 || e === 254 || e === 257 || e === 273 || e === 275 || e === 283 || e === 294 || e === 295 || e === 299 || e >= 305 && e <= 307 || e === 312 || e >= 319 && e <= 322 || e === 324 || e >= 328 && e <= 331 || e === 333 || e === 338 || e === 339 || e === 358 || e === 359 || e === 363 || e === 462 || e === 464 || e === 466 || e === 468 || e === 470 || e === 472 || e === 474 || e === 476 || e === 593 || e === 609 || e === 708 || e === 711 || e >= 713 && e <= 715 || e === 717 || e === 720 || e >= 728 && e <= 731 || e === 733 || e === 735 || e >= 768 && e <= 879 || e >= 913 && e <= 929 || e >= 931 && e <= 937 || e >= 945 && e <= 961 || e >= 963 && e <= 969 || e === 1025 || e >= 1040 && e <= 1103 || e === 1105 || e === 8208 || e >= 8211 && e <= 8214 || e === 8216 || e === 8217 || e === 8220 || e === 8221 || e >= 8224 && e <= 8226 || e >= 8228 && e <= 8231 || e === 8240 || e === 8242 || e === 8243 || e === 8245 || e === 8251 || e === 8254 || e === 8308 || e === 8319 || e >= 8321 && e <= 8324 || e === 8364 || e === 8451 || e === 8453 || e === 8457 || e === 8467 || e === 8470 || e === 8481 || e === 8482 || e === 8486 || e === 8491 || e === 8531 || e === 8532 || e >= 8539 && e <= 8542 || e >= 8544 && e <= 8555 || e >= 8560 && e <= 8569 || e === 8585 || e >= 8592 && e <= 8601 || e === 8632 || e === 8633 || e === 8658 || e === 8660 || e === 8679 || e === 8704 || e === 8706 || e === 8707 || e === 8711 || e === 8712 || e === 8715 || e === 8719 || e === 8721 || e === 8725 || e === 8730 || e >= 8733 && e <= 8736 || e === 8739 || e === 8741 || e >= 8743 && e <= 8748 || e === 8750 || e >= 8756 && e <= 8759 || e === 8764 || e === 8765 || e === 8776 || e === 8780 || e === 8786 || e === 8800 || e === 8801 || e >= 8804 && e <= 8807 || e === 8810 || e === 8811 || e === 8814 || e === 8815 || e === 8834 || e === 8835 || e === 8838 || e === 8839 || e === 8853 || e === 8857 || e === 8869 || e === 8895 || e === 8978 || e >= 9312 && e <= 9449 || e >= 9451 && e <= 9547 || e >= 9552 && e <= 9587 || e >= 9600 && e <= 9615 || e >= 9618 && e <= 9621 || e === 9632 || e === 9633 || e >= 9635 && e <= 9641 || e === 9650 || e === 9651 || e === 9654 || e === 9655 || e === 9660 || e === 9661 || e === 9664 || e === 9665 || e >= 9670 && e <= 9672 || e === 9675 || e >= 9678 && e <= 9681 || e >= 9698 && e <= 9701 || e === 9711 || e === 9733 || e === 9734 || e === 9737 || e === 9742 || e === 9743 || e === 9756 || e === 9758 || e === 9792 || e === 9794 || e === 9824 || e === 9825 || e >= 9827 && e <= 9829 || e >= 9831 && e <= 9834 || e === 9836 || e === 9837 || e === 9839 || e === 9886 || e === 9887 || e === 9919 || e >= 9926 && e <= 9933 || e >= 9935 && e <= 9939 || e >= 9941 && e <= 9953 || e === 9955 || e === 9960 || e === 9961 || e >= 9963 && e <= 9969 || e === 9972 || e >= 9974 && e <= 9977 || e === 9979 || e === 9980 || e === 9982 || e === 9983 || e === 10045 || e >= 10102 && e <= 10111 || e >= 11094 && e <= 11097 || e >= 12872 && e <= 12879 || e >= 57344 && e <= 63743 || e >= 65024 && e <= 65039 || e === 65533 || e >= 127232 && e <= 127242 || e >= 127248 && e <= 127277 || e >= 127280 && e <= 127337 || e >= 127344 && e <= 127373 || e === 127375 || e === 127376 || e >= 127387 && e <= 127404 || e >= 917760 && e <= 917999 || e >= 983040 && e <= 1048573 || e >= 1048576 && e <= 1114109;
615
+ var gt2 = (e) => e === 12288 || e >= 65281 && e <= 65376 || e >= 65504 && e <= 65510;
616
+ var ft2 = (e) => e >= 4352 && e <= 4447 || e === 8986 || e === 8987 || e === 9001 || e === 9002 || e >= 9193 && e <= 9196 || e === 9200 || e === 9203 || e === 9725 || e === 9726 || e === 9748 || e === 9749 || e >= 9800 && e <= 9811 || e === 9855 || e === 9875 || e === 9889 || e === 9898 || e === 9899 || e === 9917 || e === 9918 || e === 9924 || e === 9925 || e === 9934 || e === 9940 || e === 9962 || e === 9970 || e === 9971 || e === 9973 || e === 9978 || e === 9981 || e === 9989 || e === 9994 || e === 9995 || e === 10024 || e === 10060 || e === 10062 || e >= 10067 && e <= 10069 || e === 10071 || e >= 10133 && e <= 10135 || e === 10160 || e === 10175 || e === 11035 || e === 11036 || e === 11088 || e === 11093 || e >= 11904 && e <= 11929 || e >= 11931 && e <= 12019 || e >= 12032 && e <= 12245 || e >= 12272 && e <= 12287 || e >= 12289 && e <= 12350 || e >= 12353 && e <= 12438 || e >= 12441 && e <= 12543 || e >= 12549 && e <= 12591 || e >= 12593 && e <= 12686 || e >= 12688 && e <= 12771 || e >= 12783 && e <= 12830 || e >= 12832 && e <= 12871 || e >= 12880 && e <= 19903 || e >= 19968 && e <= 42124 || e >= 42128 && e <= 42182 || e >= 43360 && e <= 43388 || e >= 44032 && e <= 55203 || e >= 63744 && e <= 64255 || e >= 65040 && e <= 65049 || e >= 65072 && e <= 65106 || e >= 65108 && e <= 65126 || e >= 65128 && e <= 65131 || e >= 94176 && e <= 94180 || e === 94192 || e === 94193 || e >= 94208 && e <= 100343 || e >= 100352 && e <= 101589 || e >= 101632 && e <= 101640 || e >= 110576 && e <= 110579 || e >= 110581 && e <= 110587 || e === 110589 || e === 110590 || e >= 110592 && e <= 110882 || e === 110898 || e >= 110928 && e <= 110930 || e === 110933 || e >= 110948 && e <= 110951 || e >= 110960 && e <= 111355 || e === 126980 || e === 127183 || e === 127374 || e >= 127377 && e <= 127386 || e >= 127488 && e <= 127490 || e >= 127504 && e <= 127547 || e >= 127552 && e <= 127560 || e === 127568 || e === 127569 || e >= 127584 && e <= 127589 || e >= 127744 && e <= 127776 || e >= 127789 && e <= 127797 || e >= 127799 && e <= 127868 || e >= 127870 && e <= 127891 || e >= 127904 && e <= 127946 || e >= 127951 && e <= 127955 || e >= 127968 && e <= 127984 || e === 127988 || e >= 127992 && e <= 128062 || e === 128064 || e >= 128066 && e <= 128252 || e >= 128255 && e <= 128317 || e >= 128331 && e <= 128334 || e >= 128336 && e <= 128359 || e === 128378 || e === 128405 || e === 128406 || e === 128420 || e >= 128507 && e <= 128591 || e >= 128640 && e <= 128709 || e === 128716 || e >= 128720 && e <= 128722 || e >= 128725 && e <= 128727 || e >= 128732 && e <= 128735 || e === 128747 || e === 128748 || e >= 128756 && e <= 128764 || e >= 128992 && e <= 129003 || e === 129008 || e >= 129292 && e <= 129338 || e >= 129340 && e <= 129349 || e >= 129351 && e <= 129535 || e >= 129648 && e <= 129660 || e >= 129664 && e <= 129672 || e >= 129680 && e <= 129725 || e >= 129727 && e <= 129733 || e >= 129742 && e <= 129755 || e >= 129760 && e <= 129768 || e >= 129776 && e <= 129784 || e >= 131072 && e <= 196605 || e >= 196608 && e <= 262141;
617
+ var we = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/y;
618
+ var re = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
619
+ var ie = /\t{1,1000}/y;
620
+ var Ae = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu");
621
+ var ne = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y;
622
+ var Ft2 = new RegExp("\\p{M}+", "gu");
623
+ var yt2 = { limit: 1 / 0, ellipsis: "" };
624
+ var Le = (e, r = {}, s = {}) => {
625
+ const i = r.limit ?? 1 / 0, a = r.ellipsis ?? "", o = r?.ellipsisWidth ?? (a ? Le(a, yt2, s).width : 0), u = s.ansiWidth ?? 0, l = s.controlWidth ?? 0, n = s.tabWidth ?? 8, c = s.ambiguousWidth ?? 1, p = s.emojiWidth ?? 2, f = s.fullWidthWidth ?? 2, g = s.regularWidth ?? 1, E = s.wideWidth ?? 2;
626
+ let $ = 0, m = 0, d = e.length, F = 0, y2 = false, v = d, C = Math.max(0, i - o), A = 0, b = 0, w = 0, S2 = 0;
627
+ e: for (; ; ) {
628
+ if (b > A || m >= d && m > $) {
629
+ const T2 = e.slice(A, b) || e.slice($, m);
630
+ F = 0;
631
+ for (const M2 of T2.replaceAll(Ft2, "")) {
632
+ const O2 = M2.codePointAt(0) || 0;
633
+ if (gt2(O2) ? S2 = f : ft2(O2) ? S2 = E : c !== g && mt2(O2) ? S2 = c : S2 = g, w + S2 > C && (v = Math.min(v, Math.max(A, $) + F)), w + S2 > i) {
634
+ y2 = true;
635
+ break e;
636
+ }
637
+ F += M2.length, w += S2;
638
+ }
639
+ A = b = 0;
640
+ }
641
+ if (m >= d) break;
642
+ if (ne.lastIndex = m, ne.test(e)) {
643
+ if (F = ne.lastIndex - m, S2 = F * g, w + S2 > C && (v = Math.min(v, m + Math.floor((C - w) / g))), w + S2 > i) {
644
+ y2 = true;
645
+ break;
646
+ }
647
+ w += S2, A = $, b = m, m = $ = ne.lastIndex;
648
+ continue;
649
+ }
650
+ if (we.lastIndex = m, we.test(e)) {
651
+ if (w + u > C && (v = Math.min(v, m)), w + u > i) {
652
+ y2 = true;
653
+ break;
654
+ }
655
+ w += u, A = $, b = m, m = $ = we.lastIndex;
656
+ continue;
657
+ }
658
+ if (re.lastIndex = m, re.test(e)) {
659
+ if (F = re.lastIndex - m, S2 = F * l, w + S2 > C && (v = Math.min(v, m + Math.floor((C - w) / l))), w + S2 > i) {
660
+ y2 = true;
661
+ break;
662
+ }
663
+ w += S2, A = $, b = m, m = $ = re.lastIndex;
664
+ continue;
665
+ }
666
+ if (ie.lastIndex = m, ie.test(e)) {
667
+ if (F = ie.lastIndex - m, S2 = F * n, w + S2 > C && (v = Math.min(v, m + Math.floor((C - w) / n))), w + S2 > i) {
668
+ y2 = true;
669
+ break;
670
+ }
671
+ w += S2, A = $, b = m, m = $ = ie.lastIndex;
672
+ continue;
673
+ }
674
+ if (Ae.lastIndex = m, Ae.test(e)) {
675
+ if (w + p > C && (v = Math.min(v, m)), w + p > i) {
676
+ y2 = true;
677
+ break;
678
+ }
679
+ w += p, A = $, b = m, m = $ = Ae.lastIndex;
680
+ continue;
681
+ }
682
+ m += 1;
683
+ }
684
+ return { width: y2 ? C : w, index: y2 ? v : d, truncated: y2, ellipsed: y2 && i >= o };
685
+ };
686
+ var Et2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 };
687
+ var D2 = (e, r = {}) => Le(e, Et2, r).width;
688
+ var ae = "\x1B";
689
+ var je = "\x9B";
690
+ var vt2 = 39;
691
+ var Ce = "\x07";
692
+ var ke = "[";
693
+ var wt = "]";
694
+ var Ve = "m";
695
+ var Se = `${wt}8;;`;
696
+ var He = new RegExp(`(?:\\${ke}(?<code>\\d+)m|\\${Se}(?<uri>.*)${Ce})`, "y");
697
+ var At2 = (e) => {
698
+ if (e >= 30 && e <= 37 || e >= 90 && e <= 97) return 39;
699
+ if (e >= 40 && e <= 47 || e >= 100 && e <= 107) return 49;
700
+ if (e === 1 || e === 2) return 22;
701
+ if (e === 3) return 23;
702
+ if (e === 4) return 24;
703
+ if (e === 7) return 27;
704
+ if (e === 8) return 28;
705
+ if (e === 9) return 29;
706
+ if (e === 0) return 0;
707
+ };
708
+ var Ue = (e) => `${ae}${ke}${e}${Ve}`;
709
+ var Ke = (e) => `${ae}${Se}${e}${Ce}`;
710
+ var Ct2 = (e) => e.map((r) => D2(r));
711
+ var Ie = (e, r, s) => {
712
+ const i = r[Symbol.iterator]();
713
+ let a = false, o = false, u = e.at(-1), l = u === void 0 ? 0 : D2(u), n = i.next(), c = i.next(), p = 0;
714
+ for (; !n.done; ) {
715
+ const f = n.value, g = D2(f);
716
+ l + g <= s ? e[e.length - 1] += f : (e.push(f), l = 0), (f === ae || f === je) && (a = true, o = r.startsWith(Se, p + 1)), a ? o ? f === Ce && (a = false, o = false) : f === Ve && (a = false) : (l += g, l === s && !c.done && (e.push(""), l = 0)), n = c, c = i.next(), p += f.length;
717
+ }
718
+ u = e.at(-1), !l && u !== void 0 && u.length > 0 && e.length > 1 && (e[e.length - 2] += e.pop());
719
+ };
720
+ var St = (e) => {
721
+ const r = e.split(" ");
722
+ let s = r.length;
723
+ for (; s > 0 && !(D2(r[s - 1]) > 0); ) s--;
724
+ return s === r.length ? e : r.slice(0, s).join(" ") + r.slice(s).join("");
725
+ };
726
+ var It2 = (e, r, s = {}) => {
727
+ if (s.trim !== false && e.trim() === "") return "";
728
+ let i = "", a, o;
729
+ const u = e.split(" "), l = Ct2(u);
730
+ let n = [""];
731
+ for (const [$, m] of u.entries()) {
732
+ s.trim !== false && (n[n.length - 1] = (n.at(-1) ?? "").trimStart());
733
+ let d = D2(n.at(-1) ?? "");
734
+ if ($ !== 0 && (d >= r && (s.wordWrap === false || s.trim === false) && (n.push(""), d = 0), (d > 0 || s.trim === false) && (n[n.length - 1] += " ", d++)), s.hard && l[$] > r) {
735
+ const F = r - d, y2 = 1 + Math.floor((l[$] - F - 1) / r);
736
+ Math.floor((l[$] - 1) / r) < y2 && n.push(""), Ie(n, m, r);
737
+ continue;
738
+ }
739
+ if (d + l[$] > r && d > 0 && l[$] > 0) {
740
+ if (s.wordWrap === false && d < r) {
741
+ Ie(n, m, r);
742
+ continue;
743
+ }
744
+ n.push("");
745
+ }
746
+ if (d + l[$] > r && s.wordWrap === false) {
747
+ Ie(n, m, r);
748
+ continue;
749
+ }
750
+ n[n.length - 1] += m;
751
+ }
752
+ s.trim !== false && (n = n.map(($) => St($)));
753
+ const c = n.join(`
754
+ `), p = c[Symbol.iterator]();
755
+ let f = p.next(), g = p.next(), E = 0;
756
+ for (; !f.done; ) {
757
+ const $ = f.value, m = g.value;
758
+ if (i += $, $ === ae || $ === je) {
759
+ He.lastIndex = E + 1;
760
+ const y2 = He.exec(c)?.groups;
761
+ if (y2?.code !== void 0) {
762
+ const v = Number.parseFloat(y2.code);
763
+ a = v === vt2 ? void 0 : v;
764
+ } else y2?.uri !== void 0 && (o = y2.uri.length === 0 ? void 0 : y2.uri);
765
+ }
766
+ const d = a ? At2(a) : void 0;
767
+ m === `
768
+ ` ? (o && (i += Ke("")), a && d && (i += Ue(d))) : $ === `
769
+ ` && (a && d && (i += Ue(a)), o && (i += Ke(o))), E += $.length, f = g, g = p.next();
770
+ }
771
+ return i;
772
+ };
773
+ function J2(e, r, s) {
774
+ return String(e).normalize().replaceAll(`\r
775
+ `, `
776
+ `).split(`
777
+ `).map((i) => It2(i, r, s)).join(`
778
+ `);
779
+ }
780
+ var bt2 = (e, r, s, i, a) => {
781
+ let o = r, u = 0;
782
+ for (let l = s; l < i; l++) {
783
+ const n = e[l];
784
+ if (o = o - n.length, u++, o <= a) break;
785
+ }
786
+ return { lineCount: o, removals: u };
787
+ };
788
+ var X2 = ({ cursor: e, options: r, style: s, output: i = process.stdout, maxItems: a = Number.POSITIVE_INFINITY, columnPadding: o = 0, rowPadding: u = 4 }) => {
789
+ const l = rt(i) - o, n = nt(i), c = t("dim", "..."), p = Math.max(n - u, 0), f = Math.max(Math.min(a, p), 5);
790
+ let g = 0;
791
+ e >= f - 3 && (g = Math.max(Math.min(e - f + 3, r.length - f), 0));
792
+ let E = f < r.length && g > 0, $ = f < r.length && g + f < r.length;
793
+ const m = Math.min(g + f, r.length), d = [];
794
+ let F = 0;
795
+ E && F++, $ && F++;
796
+ const y2 = g + (E ? 1 : 0), v = m - ($ ? 1 : 0);
797
+ for (let A = y2; A < v; A++) {
798
+ const b = J2(s(r[A], A === e), l, { hard: true, trim: false }).split(`
799
+ `);
800
+ d.push(b), F += b.length;
801
+ }
802
+ if (F > p) {
803
+ let A = 0, b = 0, w = F;
804
+ const S2 = e - y2, T2 = (M2, O2) => bt2(d, w, M2, O2, p);
805
+ E ? ({ lineCount: w, removals: A } = T2(0, S2), w > p && ({ lineCount: w, removals: b } = T2(S2 + 1, d.length))) : ({ lineCount: w, removals: b } = T2(S2 + 1, d.length), w > p && ({ lineCount: w, removals: A } = T2(0, S2))), A > 0 && (E = true, d.splice(0, A)), b > 0 && ($ = true, d.splice(d.length - b, b));
806
+ }
807
+ const C = [];
808
+ E && C.push(c);
809
+ for (const A of d) for (const b of A) C.push(b);
810
+ return $ && C.push(c), C;
811
+ };
812
+ var Rt = (e) => {
813
+ const r = e.active ?? "Yes", s = e.inactive ?? "No";
814
+ return new kt({ active: r, inactive: s, signal: e.signal, input: e.input, output: e.output, initialValue: e.initialValue ?? true, render() {
815
+ const i = e.withGuide ?? _.withGuide, a = `${i ? `${t("gray", h)}
816
+ ` : ""}${W2(this.state)} ${e.message}
817
+ `, o = this.value ? r : s;
818
+ switch (this.state) {
819
+ case "submit": {
820
+ const u = i ? `${t("gray", h)} ` : "";
821
+ return `${a}${u}${t("dim", o)}`;
822
+ }
823
+ case "cancel": {
824
+ const u = i ? `${t("gray", h)} ` : "";
825
+ return `${a}${u}${t(["strikethrough", "dim"], o)}${i ? `
826
+ ${t("gray", h)}` : ""}`;
827
+ }
828
+ default: {
829
+ const u = i ? `${t("cyan", h)} ` : "", l = i ? t("cyan", x2) : "";
830
+ return `${a}${u}${this.value ? `${t("green", z2)} ${r}` : `${t("dim", H2)} ${t("dim", r)}`}${e.vertical ? i ? `
831
+ ${t("cyan", h)} ` : `
832
+ ` : ` ${t("dim", "/")} `}${this.value ? `${t("dim", H2)} ${t("dim", s)}` : `${t("green", z2)} ${s}`}
833
+ ${l}
834
+ `;
835
+ }
836
+ }
837
+ } }).prompt();
838
+ };
839
+ var R2 = { message: (e = [], { symbol: r = t("gray", h), secondarySymbol: s = t("gray", h), output: i = process.stdout, spacing: a = 1, withGuide: o } = {}) => {
840
+ const u = [], l = o ?? _.withGuide, n = l ? s : "", c = l ? `${r} ` : "", p = l ? `${s} ` : "";
841
+ for (let g = 0; g < a; g++) u.push(n);
842
+ const f = Array.isArray(e) ? e : e.split(`
843
+ `);
844
+ if (f.length > 0) {
845
+ const [g, ...E] = f;
846
+ g.length > 0 ? u.push(`${c}${g}`) : u.push(l ? r : "");
847
+ for (const $ of E) $.length > 0 ? u.push(`${p}${$}`) : u.push(l ? s : "");
848
+ }
849
+ i.write(`${u.join(`
850
+ `)}
851
+ `);
852
+ }, info: (e, r) => {
853
+ R2.message(e, { ...r, symbol: t("blue", fe) });
854
+ }, success: (e, r) => {
855
+ R2.message(e, { ...r, symbol: t("green", Fe) });
856
+ }, step: (e, r) => {
857
+ R2.message(e, { ...r, symbol: t("green", V) });
858
+ }, warn: (e, r) => {
859
+ R2.message(e, { ...r, symbol: t("yellow", ye) });
860
+ }, warning: (e, r) => {
861
+ R2.warn(e, r);
862
+ }, error: (e, r) => {
863
+ R2.message(e, { ...r, symbol: t("red", Ee) });
864
+ } };
865
+ var Nt = (e = "", r) => {
866
+ const s = r?.output ?? process.stdout, i = r?.withGuide ?? _.withGuide ? `${t("gray", x2)} ` : "";
867
+ s.write(`${i}${t("red", e)}
868
+
869
+ `);
870
+ };
871
+ var Wt2 = (e = "", r) => {
872
+ const s = r?.output ?? process.stdout, i = r?.withGuide ?? _.withGuide ? `${t("gray", he)} ` : "";
873
+ s.write(`${i}${e}
874
+ `);
875
+ };
876
+ var Gt = (e = "", r) => {
877
+ const s = r?.output ?? process.stdout, i = r?.withGuide ?? _.withGuide ? `${t("gray", h)}
878
+ ${t("gray", x2)} ` : "";
879
+ s.write(`${i}${e}
880
+
881
+ `);
882
+ };
883
+ var Q2 = (e, r) => e.split(`
884
+ `).map((s) => r(s)).join(`
885
+ `);
886
+ var Lt2 = (e) => {
887
+ const r = (i, a) => {
888
+ const o = i.label ?? String(i.value);
889
+ return a === "disabled" ? `${t("gray", q2)} ${Q2(o, (u) => t(["strikethrough", "gray"], u))}${i.hint ? ` ${t("dim", `(${i.hint ?? "disabled"})`)}` : ""}` : a === "active" ? `${t("cyan", te)} ${o}${i.hint ? ` ${t("dim", `(${i.hint})`)}` : ""}` : a === "selected" ? `${t("green", U2)} ${Q2(o, (u) => t("dim", u))}${i.hint ? ` ${t("dim", `(${i.hint})`)}` : ""}` : a === "cancelled" ? `${Q2(o, (u) => t(["strikethrough", "dim"], u))}` : a === "active-selected" ? `${t("green", U2)} ${o}${i.hint ? ` ${t("dim", `(${i.hint})`)}` : ""}` : a === "submitted" ? `${Q2(o, (u) => t("dim", u))}` : `${t("dim", q2)} ${Q2(o, (u) => t("dim", u))}`;
890
+ }, s = e.required ?? true;
891
+ return new Lt({ options: e.options, signal: e.signal, input: e.input, output: e.output, initialValues: e.initialValues, required: s, cursorAt: e.cursorAt, validate(i) {
892
+ if (s && (i === void 0 || i.length === 0)) return `Please select at least one option.
893
+ ${t("reset", t("dim", `Press ${t(["gray", "bgWhite", "inverse"], " space ")} to select, ${t("gray", t("bgWhite", t("inverse", " enter ")))} to submit`))}`;
894
+ }, render() {
895
+ const i = Bt(e.output, e.message, `${ve(this.state)} `, `${W2(this.state)} `), a = `${t("gray", h)}
896
+ ${i}
897
+ `, o = this.value ?? [], u = (l, n) => {
898
+ if (l.disabled) return r(l, "disabled");
899
+ const c = o.includes(l.value);
900
+ return n && c ? r(l, "active-selected") : c ? r(l, "selected") : r(l, n ? "active" : "inactive");
901
+ };
902
+ switch (this.state) {
903
+ case "submit": {
904
+ const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "submitted")).join(t("dim", ", ")) || t("dim", "none"), n = Bt(e.output, l, `${t("gray", h)} `);
905
+ return `${a}${n}`;
906
+ }
907
+ case "cancel": {
908
+ const l = this.options.filter(({ value: c }) => o.includes(c)).map((c) => r(c, "cancelled")).join(t("dim", ", "));
909
+ if (l.trim() === "") return `${a}${t("gray", h)}`;
910
+ const n = Bt(e.output, l, `${t("gray", h)} `);
911
+ return `${a}${n}
912
+ ${t("gray", h)}`;
913
+ }
914
+ case "error": {
915
+ const l = `${t("yellow", h)} `, n = this.error.split(`
916
+ `).map((f, g) => g === 0 ? `${t("yellow", x2)} ${t("yellow", f)}` : ` ${f}`).join(`
917
+ `), c = a.split(`
918
+ `).length, p = n.split(`
919
+ `).length + 1;
920
+ return `${a}${l}${X2({ output: e.output, options: this.options, cursor: this.cursor, maxItems: e.maxItems, columnPadding: l.length, rowPadding: c + p, style: u }).join(`
921
+ ${l}`)}
922
+ ${n}
923
+ `;
924
+ }
925
+ default: {
926
+ const l = `${t("cyan", h)} `, n = a.split(`
927
+ `).length;
928
+ return `${a}${l}${X2({ output: e.output, options: this.options, cursor: this.cursor, maxItems: e.maxItems, columnPadding: l.length, rowPadding: n + 2, style: u }).join(`
929
+ ${l}`)}
930
+ ${t("cyan", x2)}
931
+ `;
932
+ }
933
+ }
934
+ } }).prompt();
935
+ };
936
+ var Kt = (e) => t("magenta", e);
937
+ var be = ({ indicator: e = "dots", onCancel: r, output: s = process.stdout, cancelMessage: i, errorMessage: a, frames: o = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: u = ee ? 80 : 120, signal: l, ...n } = {}) => {
938
+ const c = ce();
939
+ let p, f, g = false, E = false, $ = "", m, d = performance.now();
940
+ const F = rt(s), y2 = n?.styleFrame ?? Kt, v = (B2) => {
941
+ const P2 = B2 > 1 ? a ?? _.messages.error : i ?? _.messages.cancel;
942
+ E = B2 === 1, g && (k2(P2, B2), E && typeof r == "function" && r());
943
+ }, C = () => v(2), A = () => v(1), b = () => {
944
+ process.on("uncaughtExceptionMonitor", C), process.on("unhandledRejection", C), process.on("SIGINT", A), process.on("SIGTERM", A), process.on("exit", v), l && l.addEventListener("abort", A);
945
+ }, w = () => {
946
+ process.removeListener("uncaughtExceptionMonitor", C), process.removeListener("unhandledRejection", C), process.removeListener("SIGINT", A), process.removeListener("SIGTERM", A), process.removeListener("exit", v), l && l.removeEventListener("abort", A);
947
+ }, S2 = () => {
948
+ if (m === void 0) return;
949
+ c && s.write(`
950
+ `);
951
+ const B2 = J2(m, F, { hard: true, trim: false }).split(`
952
+ `);
953
+ B2.length > 1 && s.write(import_sisteransi2.cursor.up(B2.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down());
954
+ }, T2 = (B2) => B2.replace(/\.+$/, ""), M2 = (B2) => {
955
+ const P2 = (performance.now() - B2) / 1e3, G2 = Math.floor(P2 / 60), L2 = Math.floor(P2 % 60);
956
+ return G2 > 0 ? `[${G2}m ${L2}s]` : `[${L2}s]`;
957
+ }, O2 = n.withGuide ?? _.withGuide, le = (B2 = "") => {
958
+ g = true, p = xt({ output: s }), $ = T2(B2), d = performance.now(), O2 && s.write(`${t("gray", h)}
959
+ `);
960
+ let P2 = 0, G2 = 0;
961
+ b(), f = setInterval(() => {
962
+ if (c && $ === m) return;
963
+ S2(), m = $;
964
+ const L2 = y2(o[P2]);
965
+ let Z2;
966
+ if (c) Z2 = `${L2} ${$}...`;
967
+ else if (e === "timer") Z2 = `${L2} ${$} ${M2(d)}`;
968
+ else {
969
+ const et2 = ".".repeat(Math.floor(G2)).slice(0, 3);
970
+ Z2 = `${L2} ${$}${et2}`;
971
+ }
972
+ const Ze = J2(Z2, F, { hard: true, trim: false });
973
+ s.write(Ze), P2 = P2 + 1 < o.length ? P2 + 1 : 0, G2 = G2 < 4 ? G2 + 0.125 : 0;
974
+ }, u);
975
+ }, k2 = (B2 = "", P2 = 0, G2 = false) => {
976
+ if (!g) return;
977
+ g = false, clearInterval(f), S2();
978
+ const L2 = P2 === 0 ? t("green", V) : P2 === 1 ? t("red", $e) : t("red", de);
979
+ $ = B2 ?? $, G2 || (e === "timer" ? s.write(`${L2} ${$} ${M2(d)}
980
+ `) : s.write(`${L2} ${$}
981
+ `)), w(), p();
982
+ };
983
+ return { start: le, stop: (B2 = "") => k2(B2, 0), message: (B2 = "") => {
984
+ $ = T2(B2 ?? $);
985
+ }, cancel: (B2 = "") => k2(B2, 1), error: (B2 = "") => k2(B2, 2), clear: () => k2("", 0, true), get isCancelled() {
986
+ return E;
987
+ } };
988
+ };
989
+ var ze = { light: I2("\u2500", "-"), heavy: I2("\u2501", "="), block: I2("\u2588", "#") };
990
+ var oe = (e, r) => e.includes(`
991
+ `) ? e.split(`
992
+ `).map((s) => r(s)).join(`
993
+ `) : r(e);
994
+ var Jt = (e) => {
995
+ const r = (s, i) => {
996
+ const a = s.label ?? String(s.value);
997
+ switch (i) {
998
+ case "disabled":
999
+ return `${t("gray", H2)} ${oe(a, (o) => t("gray", o))}${s.hint ? ` ${t("dim", `(${s.hint ?? "disabled"})`)}` : ""}`;
1000
+ case "selected":
1001
+ return `${oe(a, (o) => t("dim", o))}`;
1002
+ case "active":
1003
+ return `${t("green", z2)} ${a}${s.hint ? ` ${t("dim", `(${s.hint})`)}` : ""}`;
1004
+ case "cancelled":
1005
+ return `${oe(a, (o) => t(["strikethrough", "dim"], o))}`;
1006
+ default:
1007
+ return `${t("dim", H2)} ${oe(a, (o) => t("dim", o))}`;
1008
+ }
1009
+ };
1010
+ return new Tt({ options: e.options, signal: e.signal, input: e.input, output: e.output, initialValue: e.initialValue, render() {
1011
+ const s = e.withGuide ?? _.withGuide, i = `${W2(this.state)} `, a = `${ve(this.state)} `, o = Bt(e.output, e.message, a, i), u = `${s ? `${t("gray", h)}
1012
+ ` : ""}${o}
1013
+ `;
1014
+ switch (this.state) {
1015
+ case "submit": {
1016
+ const l = s ? `${t("gray", h)} ` : "", n = Bt(e.output, r(this.options[this.cursor], "selected"), l);
1017
+ return `${u}${n}`;
1018
+ }
1019
+ case "cancel": {
1020
+ const l = s ? `${t("gray", h)} ` : "", n = Bt(e.output, r(this.options[this.cursor], "cancelled"), l);
1021
+ return `${u}${n}${s ? `
1022
+ ${t("gray", h)}` : ""}`;
1023
+ }
1024
+ default: {
1025
+ const l = s ? `${t("cyan", h)} ` : "", n = s ? t("cyan", x2) : "", c = u.split(`
1026
+ `).length, p = s ? 2 : 1;
1027
+ return `${u}${l}${X2({ output: e.output, cursor: this.cursor, options: this.options, maxItems: e.maxItems, columnPadding: l.length, rowPadding: c + p, style: (f, g) => r(f, f.disabled ? "disabled" : g ? "active" : "inactive") }).join(`
1028
+ ${l}`)}
1029
+ ${n}
1030
+ `;
1031
+ }
1032
+ }
1033
+ } }).prompt();
1034
+ };
1035
+ var Qe = `${t("gray", h)} `;
1036
+ var Zt = (e) => new $t({ validate: e.validate, placeholder: e.placeholder, defaultValue: e.defaultValue, initialValue: e.initialValue, output: e.output, signal: e.signal, input: e.input, render() {
1037
+ const r = e?.withGuide ?? _.withGuide, s = `${`${r ? `${t("gray", h)}
1038
+ ` : ""}${W2(this.state)} `}${e.message}
1039
+ `, i = e.placeholder ? t("inverse", e.placeholder[0]) + t("dim", e.placeholder.slice(1)) : t(["inverse", "hidden"], "_"), a = this.userInput ? this.userInputWithCursor : i, o = this.value ?? "";
1040
+ switch (this.state) {
1041
+ case "error": {
1042
+ const u = this.error ? ` ${t("yellow", this.error)}` : "", l = r ? `${t("yellow", h)} ` : "", n = r ? t("yellow", x2) : "";
1043
+ return `${s.trim()}
1044
+ ${l}${a}
1045
+ ${n}${u}
1046
+ `;
1047
+ }
1048
+ case "submit": {
1049
+ const u = o ? ` ${t("dim", o)}` : "", l = r ? t("gray", h) : "";
1050
+ return `${s}${l}${u}`;
1051
+ }
1052
+ case "cancel": {
1053
+ const u = o ? ` ${t(["strikethrough", "dim"], o)}` : "", l = r ? t("gray", h) : "";
1054
+ return `${s}${l}${u}${o.trim() ? `
1055
+ ${l}` : ""}`;
1056
+ }
1057
+ default: {
1058
+ const u = r ? `${t("cyan", h)} ` : "", l = r ? t("cyan", x2) : "";
1059
+ return `${s}${u}${a}
1060
+ ${l}
1061
+ `;
1062
+ }
1063
+ }
1064
+ } }).prompt();
1065
+
1066
+ // src/cli/prompts.ts
1067
+ import path2 from "path";
1068
+
1069
+ // src/cli/utils.ts
1070
+ import { execSync } from "child_process";
1071
+ import os from "os";
1072
+ import path from "path";
1073
+ function slugify(name) {
1074
+ return name.toLowerCase().trim().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
1075
+ }
1076
+ function getEnvWithBun() {
1077
+ if (os.platform() !== "win32") return process.env;
1078
+ const bunDir = path.join(os.homedir(), ".bun", "bin");
1079
+ const current = process.env.PATH ?? "";
1080
+ if (current.includes(bunDir)) return process.env;
1081
+ return { ...process.env, PATH: `${bunDir};${current}` };
1082
+ }
1083
+ function exec(cmd, cwd, silent = false) {
1084
+ execSync(cmd, { cwd, stdio: silent ? "pipe" : "inherit", env: getEnvWithBun() });
1085
+ }
1086
+
1087
+ // src/cli/prompts.ts
1088
+ var RECOMMENDED_COMPONENTS = [
1089
+ "button",
1090
+ "input",
1091
+ "card",
1092
+ "dialog",
1093
+ "form",
1094
+ "label",
1095
+ "badge",
1096
+ "avatar",
1097
+ "skeleton",
1098
+ "separator",
1099
+ "sonner",
1100
+ "dropdown-menu",
1101
+ "select",
1102
+ "sheet",
1103
+ "tabs"
1104
+ ];
1105
+ var ALL_COMPONENTS = [
1106
+ ...RECOMMENDED_COMPONENTS,
1107
+ "table",
1108
+ "popover",
1109
+ "tooltip",
1110
+ "accordion",
1111
+ "alert",
1112
+ "calendar",
1113
+ "checkbox",
1114
+ "radio-group",
1115
+ "switch",
1116
+ "textarea",
1117
+ "progress",
1118
+ "slider"
1119
+ ];
1120
+ async function runPrompts(opts) {
1121
+ const { projectNameArg, outputDirArg, skipPrompts } = opts;
1122
+ let projectName;
1123
+ if (projectNameArg) {
1124
+ projectName = projectNameArg;
1125
+ } else {
1126
+ const val = await Zt({
1127
+ message: "Project name",
1128
+ placeholder: "My App",
1129
+ validate: (v) => {
1130
+ if (!v?.trim()) return "Project name is required";
1131
+ }
1132
+ });
1133
+ if (Ct(val)) return null;
1134
+ projectName = val;
1135
+ }
1136
+ if (skipPrompts) {
1137
+ const packageName2 = slugify(projectName);
1138
+ const dir2 = outputDirArg ? path2.resolve(process.cwd(), outputDirArg) : path2.join(process.cwd(), packageName2);
1139
+ return {
1140
+ projectName,
1141
+ packageName: packageName2,
1142
+ dir: dir2,
1143
+ securityProfile: "hardened",
1144
+ layout: "top-nav",
1145
+ theme: "default",
1146
+ authPages: true,
1147
+ mfaPages: true,
1148
+ passkeyPages: true,
1149
+ components: RECOMMENDED_COMPONENTS,
1150
+ webSocket: true,
1151
+ gitInit: true
1152
+ };
1153
+ }
1154
+ const pkgVal = await Zt({
1155
+ message: "Package name",
1156
+ initialValue: slugify(projectName),
1157
+ validate: (v) => {
1158
+ if (!v?.trim()) return "Package name is required";
1159
+ }
1160
+ });
1161
+ if (Ct(pkgVal)) return null;
1162
+ const packageName = pkgVal;
1163
+ const dir = outputDirArg ? path2.resolve(process.cwd(), outputDirArg) : path2.join(process.cwd(), packageName);
1164
+ const securityVal = await Jt({
1165
+ message: "Security profile",
1166
+ options: [
1167
+ { value: "hardened", label: "Hardened (recommended)" },
1168
+ { value: "prototype", label: "Prototype (local dev only)" }
1169
+ ],
1170
+ initialValue: "hardened"
1171
+ });
1172
+ if (Ct(securityVal)) return null;
1173
+ const securityProfile = securityVal;
1174
+ const layoutVal = await Jt({
1175
+ message: "Layout",
1176
+ options: [
1177
+ { value: "minimal", label: "Minimal", hint: "no nav \u2014 just a shell" },
1178
+ { value: "top-nav", label: "Top nav", hint: "header with nav links" },
1179
+ { value: "sidebar", label: "Sidebar", hint: "collapsible sidebar + header" }
1180
+ ],
1181
+ initialValue: "top-nav"
1182
+ });
1183
+ if (Ct(layoutVal)) return null;
1184
+ const layout = layoutVal;
1185
+ const themeVal = await Jt({
1186
+ message: "Theme",
1187
+ options: [
1188
+ { value: "default", label: "Default", hint: "shadcn neutral" },
1189
+ { value: "dark", label: "Dark", hint: "dark mode default" },
1190
+ { value: "minimal", label: "Minimal", hint: "reduced radius, muted palette" },
1191
+ { value: "vibrant", label: "Vibrant", hint: "saturated, high contrast" }
1192
+ ],
1193
+ initialValue: "default"
1194
+ });
1195
+ if (Ct(themeVal)) return null;
1196
+ const theme = themeVal;
1197
+ const authVal = await Rt({
1198
+ message: "Include auth pages?",
1199
+ initialValue: true
1200
+ });
1201
+ if (Ct(authVal)) return null;
1202
+ const authPages = authVal;
1203
+ let mfaPages = false;
1204
+ if (authPages) {
1205
+ const mfaVal = await Rt({
1206
+ message: "Include MFA pages?",
1207
+ initialValue: false
1208
+ });
1209
+ if (Ct(mfaVal)) return null;
1210
+ mfaPages = mfaVal;
1211
+ }
1212
+ let passkeyPages = false;
1213
+ if (authPages) {
1214
+ const passkeyVal = await Rt({
1215
+ message: "Include passkey login pages?",
1216
+ initialValue: false
1217
+ });
1218
+ if (Ct(passkeyVal)) return null;
1219
+ passkeyPages = passkeyVal;
1220
+ }
1221
+ const componentsVal = await Lt2({
1222
+ message: "shadcn components",
1223
+ options: ALL_COMPONENTS.map((c) => ({ value: c, label: c })),
1224
+ initialValues: RECOMMENDED_COMPONENTS,
1225
+ required: false
1226
+ });
1227
+ if (Ct(componentsVal)) return null;
1228
+ const components = componentsVal;
1229
+ const wsVal = await Rt({
1230
+ message: "WebSocket support?",
1231
+ initialValue: true
1232
+ });
1233
+ if (Ct(wsVal)) return null;
1234
+ const webSocket = wsVal;
1235
+ const gitVal = await Rt({
1236
+ message: "Git init?",
1237
+ initialValue: true
1238
+ });
1239
+ if (Ct(gitVal)) return null;
1240
+ const gitInit = gitVal;
1241
+ return {
1242
+ projectName,
1243
+ packageName,
1244
+ dir,
1245
+ securityProfile,
1246
+ layout,
1247
+ theme,
1248
+ authPages,
1249
+ mfaPages,
1250
+ passkeyPages,
1251
+ components,
1252
+ webSocket,
1253
+ gitInit
1254
+ };
1255
+ }
1256
+
1257
+ // src/cli/scaffold.ts
1258
+ import fs from "fs/promises";
1259
+ import path3 from "path";
1260
+
1261
+ // src/cli/templates/package-json.ts
1262
+ function generatePackageJson(config) {
1263
+ const dependencies = {
1264
+ "@lastshotlabs/snapshot": `^${"0.1.0"}`,
1265
+ "@tanstack/react-query": "^5.0.0",
1266
+ "@tanstack/react-router": "^1.0.0",
1267
+ "@tanstack/router-plugin": "^1.0.0",
1268
+ "@unhead/react": "^2.0.0",
1269
+ clsx: "^2.0.0",
1270
+ "class-variance-authority": "^0.7.0",
1271
+ jotai: "^2.0.0",
1272
+ zod: "^3.0.0",
1273
+ react: "^19.0.0",
1274
+ "react-dom": "^19.0.0",
1275
+ "tailwind-merge": "^3.0.0"
1276
+ };
1277
+ if (config.mfaPages) {
1278
+ dependencies["react-qr-code"] = "^2.0.0";
1279
+ }
1280
+ if (config.passkeyPages) {
1281
+ dependencies["@simplewebauthn/browser"] = "^13.0.0";
1282
+ }
1283
+ const pkg = {
1284
+ name: config.packageName,
1285
+ version: "0.1.0",
1286
+ type: "module",
1287
+ scripts: {
1288
+ dev: "vite",
1289
+ build: "tsc -b && vite build",
1290
+ preview: "vite preview",
1291
+ typecheck: "tsc -b",
1292
+ sync: "bunx snapshot sync"
1293
+ },
1294
+ dependencies,
1295
+ devDependencies: {
1296
+ "@tailwindcss/vite": "^4.0.0",
1297
+ "@types/react": "^19.0.0",
1298
+ "@types/react-dom": "^19.0.0",
1299
+ "@vitejs/plugin-react": "^4.0.0",
1300
+ tailwindcss: "^4.0.0",
1301
+ typescript: "^5.0.0",
1302
+ vite: "^6.0.0"
1303
+ }
1304
+ };
1305
+ return JSON.stringify(pkg, null, 2) + "\n";
1306
+ }
1307
+
1308
+ // src/cli/templates/aliases.ts
1309
+ var ALIASES = ["lib", "components", "pages", "hooks", "api", "store", "styles", "types"];
1310
+
1311
+ // src/cli/templates/tsconfig.ts
1312
+ function generateTsConfigRoot() {
1313
+ const paths = {
1314
+ "@/*": ["./src/*"]
1315
+ };
1316
+ for (const alias of ALIASES) {
1317
+ paths[`@${alias}/*`] = [`./src/${alias}/*`];
1318
+ }
1319
+ return JSON.stringify(
1320
+ {
1321
+ files: [],
1322
+ references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }],
1323
+ compilerOptions: { paths }
1324
+ },
1325
+ null,
1326
+ 2
1327
+ ) + "\n";
1328
+ }
1329
+ function generateTsConfigApp() {
1330
+ const paths = {
1331
+ "@/*": ["./src/*"]
1332
+ };
1333
+ for (const alias of ALIASES) {
1334
+ paths[`@${alias}/*`] = [`./src/${alias}/*`];
1335
+ }
1336
+ return JSON.stringify(
1337
+ {
1338
+ compilerOptions: {
1339
+ tsBuildInfoFile: "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
1340
+ target: "ES2020",
1341
+ useDefineForClassFields: true,
1342
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
1343
+ types: ["vite/client"],
1344
+ module: "ESNext",
1345
+ skipLibCheck: true,
1346
+ moduleResolution: "bundler",
1347
+ allowImportingTsExtensions: true,
1348
+ isolatedModules: true,
1349
+ moduleDetection: "force",
1350
+ noEmit: true,
1351
+ jsx: "react-jsx",
1352
+ strict: true,
1353
+ noUncheckedIndexedAccess: true,
1354
+ paths
1355
+ },
1356
+ include: ["src", "src/routeTree.gen.ts"]
1357
+ },
1358
+ null,
1359
+ 2
1360
+ ) + "\n";
1361
+ }
1362
+ function generateTsConfigNode() {
1363
+ return JSON.stringify(
1364
+ {
1365
+ compilerOptions: {
1366
+ tsBuildInfoFile: "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
1367
+ target: "ES2022",
1368
+ lib: ["ES2022"],
1369
+ module: "ESNext",
1370
+ moduleResolution: "bundler",
1371
+ allowImportingTsExtensions: true,
1372
+ isolatedModules: true,
1373
+ moduleDetection: "force",
1374
+ noEmit: true,
1375
+ strict: true,
1376
+ skipLibCheck: true,
1377
+ noUncheckedIndexedAccess: true
1378
+ },
1379
+ include: ["vite.config.ts"]
1380
+ },
1381
+ null,
1382
+ 2
1383
+ ) + "\n";
1384
+ }
1385
+
1386
+ // src/cli/templates/vite-config.ts
1387
+ function generateViteConfig() {
1388
+ const aliasLines = ALIASES.map(
1389
+ (a) => ` '@${a}': path.resolve(__dirname, './src/${a}'),`
1390
+ ).join("\n");
1391
+ return `import { defineConfig } from 'vite'
1392
+ import react from '@vitejs/plugin-react'
1393
+ import tailwindcss from '@tailwindcss/vite'
1394
+ import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
1395
+ import { snapshotSync } from '@lastshotlabs/snapshot/vite'
1396
+ import path from 'node:path'
1397
+
1398
+ export default defineConfig({
1399
+ plugins: [
1400
+ TanStackRouterVite({
1401
+ routesDirectory: './src/routes',
1402
+ generatedRouteTree: './src/routeTree.gen.ts',
1403
+ }),
1404
+ react(),
1405
+ tailwindcss(),
1406
+ snapshotSync({ file: 'schema.json' }),
1407
+ ],
1408
+ resolve: {
1409
+ dedupe: ['react', 'react-dom', '@tanstack/react-router', '@tanstack/react-query', 'jotai'],
1410
+ alias: {
1411
+ '@': path.resolve(__dirname, './src'),
1412
+ ${aliasLines}
1413
+ },
1414
+ },
1415
+ })
1416
+ `;
1417
+ }
1418
+
1419
+ // src/cli/templates/components-json.ts
1420
+ var BASE_COLOR = {
1421
+ default: "neutral",
1422
+ dark: "neutral",
1423
+ minimal: "slate",
1424
+ vibrant: "violet"
1425
+ };
1426
+ function generateComponentsJson(config) {
1427
+ const json = {
1428
+ $schema: "https://ui.shadcn.com/schema.json",
1429
+ style: "default",
1430
+ rsc: false,
1431
+ tsx: true,
1432
+ tailwind: {
1433
+ config: "",
1434
+ css: "src/styles/globals.css",
1435
+ baseColor: BASE_COLOR[config.theme],
1436
+ cssVariables: true,
1437
+ prefix: ""
1438
+ },
1439
+ aliases: {
1440
+ components: "@/components",
1441
+ utils: "@/lib/utils",
1442
+ ui: "@/components/ui",
1443
+ lib: "@/lib",
1444
+ hooks: "@/hooks"
1445
+ },
1446
+ iconLibrary: "lucide"
1447
+ };
1448
+ return JSON.stringify(json, null, 2) + "\n";
1449
+ }
1450
+
1451
+ // src/cli/templates/env.ts
1452
+ function generateEnv(config) {
1453
+ const wsLine = config.webSocket ? "\nVITE_WS_URL=ws://localhost:3000/ws" : "";
1454
+ if (config.securityProfile === "hardened") {
1455
+ return `VITE_API_URL=http://localhost:3000${wsLine}
1456
+ `;
1457
+ }
1458
+ return `VITE_API_URL=http://localhost:3000
1459
+ # WARNING: Static API credentials should not be deployed to production.
1460
+ VITE_BEARER_TOKEN=
1461
+ # Set to true to allow running this prototype on non-localhost origins (not recommended for production).
1462
+ VITE_ALLOW_PROTOTYPE_DEPLOYMENT=${wsLine}
1463
+ `;
1464
+ }
1465
+
1466
+ // src/cli/templates/index-html.ts
1467
+ var DARK_INIT_SCRIPT = `
1468
+ <script>if(!localStorage.getItem('snapshot-theme')){localStorage.setItem('snapshot-theme','dark');document.documentElement.classList.add('dark')}else if(localStorage.getItem('snapshot-theme')==='dark'){document.documentElement.classList.add('dark')}</script>`;
1469
+ function generateIndexHtml(config) {
1470
+ const darkScript = config.theme === "dark" ? DARK_INIT_SCRIPT : "";
1471
+ return `<!doctype html>
1472
+ <html lang="en">
1473
+ <head>
1474
+ <meta charset="UTF-8" />
1475
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
1476
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />${darkScript}
1477
+ <title>${config.projectName}</title>
1478
+ </head>
1479
+ <body>
1480
+ <div id="root"></div>
1481
+ <script type="module" src="/src/main.tsx"></script>
1482
+ </body>
1483
+ </html>
1484
+ `;
1485
+ }
1486
+
1487
+ // src/cli/templates/globals-css.ts
1488
+ var NEUTRAL_LIGHT = `
1489
+ --background: oklch(1 0 0);
1490
+ --foreground: oklch(0.145 0 0);
1491
+ --card: oklch(1 0 0);
1492
+ --card-foreground: oklch(0.145 0 0);
1493
+ --popover: oklch(1 0 0);
1494
+ --popover-foreground: oklch(0.145 0 0);
1495
+ --primary: oklch(0.205 0 0);
1496
+ --primary-foreground: oklch(0.985 0 0);
1497
+ --secondary: oklch(0.97 0 0);
1498
+ --secondary-foreground: oklch(0.205 0 0);
1499
+ --muted: oklch(0.97 0 0);
1500
+ --muted-foreground: oklch(0.556 0 0);
1501
+ --accent: oklch(0.97 0 0);
1502
+ --accent-foreground: oklch(0.205 0 0);
1503
+ --destructive: oklch(0.577 0.245 27.325);
1504
+ --border: oklch(0.922 0 0);
1505
+ --input: oklch(0.922 0 0);
1506
+ --ring: oklch(0.708 0 0);
1507
+ --radius: 0.625rem;
1508
+ --chart-1: oklch(0.646 0.222 41.116);
1509
+ --chart-2: oklch(0.6 0.118 184.704);
1510
+ --chart-3: oklch(0.398 0.07 227.392);
1511
+ --chart-4: oklch(0.828 0.189 84.429);
1512
+ --chart-5: oklch(0.769 0.188 70.08);`;
1513
+ var NEUTRAL_DARK = `
1514
+ --background: oklch(0.145 0 0);
1515
+ --foreground: oklch(0.985 0 0);
1516
+ --card: oklch(0.205 0 0);
1517
+ --card-foreground: oklch(0.985 0 0);
1518
+ --popover: oklch(0.205 0 0);
1519
+ --popover-foreground: oklch(0.985 0 0);
1520
+ --primary: oklch(0.922 0 0);
1521
+ --primary-foreground: oklch(0.205 0 0);
1522
+ --secondary: oklch(0.269 0 0);
1523
+ --secondary-foreground: oklch(0.985 0 0);
1524
+ --muted: oklch(0.269 0 0);
1525
+ --muted-foreground: oklch(0.708 0 0);
1526
+ --accent: oklch(0.269 0 0);
1527
+ --accent-foreground: oklch(0.985 0 0);
1528
+ --destructive: oklch(0.704 0.191 22.216);
1529
+ --border: oklch(1 0 0 / 10%);
1530
+ --input: oklch(1 0 0 / 15%);
1531
+ --ring: oklch(0.556 0 0);
1532
+ --chart-1: oklch(0.488 0.243 264.376);
1533
+ --chart-2: oklch(0.696 0.17 162.48);
1534
+ --chart-3: oklch(0.769 0.188 70.08);
1535
+ --chart-4: oklch(0.627 0.265 303.9);
1536
+ --chart-5: oklch(0.645 0.246 16.439);`;
1537
+ var MINIMAL_LIGHT = `
1538
+ --background: oklch(0.99 0 0);
1539
+ --foreground: oklch(0.2 0.01 264);
1540
+ --card: oklch(0.99 0 0);
1541
+ --card-foreground: oklch(0.2 0.01 264);
1542
+ --popover: oklch(0.99 0 0);
1543
+ --popover-foreground: oklch(0.2 0.01 264);
1544
+ --primary: oklch(0.3 0.02 264);
1545
+ --primary-foreground: oklch(0.97 0 0);
1546
+ --secondary: oklch(0.96 0.005 264);
1547
+ --secondary-foreground: oklch(0.3 0.02 264);
1548
+ --muted: oklch(0.96 0.005 264);
1549
+ --muted-foreground: oklch(0.58 0.01 264);
1550
+ --accent: oklch(0.94 0.008 264);
1551
+ --accent-foreground: oklch(0.3 0.02 264);
1552
+ --destructive: oklch(0.577 0.245 27.325);
1553
+ --border: oklch(0.9 0.005 264);
1554
+ --input: oklch(0.9 0.005 264);
1555
+ --ring: oklch(0.7 0.01 264);
1556
+ --radius: 0.25rem;
1557
+ --chart-1: oklch(0.55 0.12 240);
1558
+ --chart-2: oklch(0.6 0.1 180);
1559
+ --chart-3: oklch(0.5 0.08 220);
1560
+ --chart-4: oklch(0.65 0.09 200);
1561
+ --chart-5: oklch(0.45 0.1 260);`;
1562
+ var MINIMAL_DARK = `
1563
+ --background: oklch(0.15 0.01 264);
1564
+ --foreground: oklch(0.97 0 0);
1565
+ --card: oklch(0.19 0.01 264);
1566
+ --card-foreground: oklch(0.97 0 0);
1567
+ --popover: oklch(0.19 0.01 264);
1568
+ --popover-foreground: oklch(0.97 0 0);
1569
+ --primary: oklch(0.88 0.005 264);
1570
+ --primary-foreground: oklch(0.19 0.01 264);
1571
+ --secondary: oklch(0.25 0.01 264);
1572
+ --secondary-foreground: oklch(0.97 0 0);
1573
+ --muted: oklch(0.25 0.01 264);
1574
+ --muted-foreground: oklch(0.65 0.01 264);
1575
+ --accent: oklch(0.25 0.01 264);
1576
+ --accent-foreground: oklch(0.97 0 0);
1577
+ --destructive: oklch(0.704 0.191 22.216);
1578
+ --border: oklch(1 0 0 / 10%);
1579
+ --input: oklch(1 0 0 / 12%);
1580
+ --ring: oklch(0.5 0.01 264);
1581
+ --radius: 0.25rem;
1582
+ --chart-1: oklch(0.55 0.12 240);
1583
+ --chart-2: oklch(0.6 0.1 180);
1584
+ --chart-3: oklch(0.65 0.09 200);
1585
+ --chart-4: oklch(0.5 0.08 220);
1586
+ --chart-5: oklch(0.45 0.1 260);`;
1587
+ var VIBRANT_LIGHT = `
1588
+ --background: oklch(1 0 0);
1589
+ --foreground: oklch(0.16 0.04 285);
1590
+ --card: oklch(1 0 0);
1591
+ --card-foreground: oklch(0.16 0.04 285);
1592
+ --popover: oklch(1 0 0);
1593
+ --popover-foreground: oklch(0.16 0.04 285);
1594
+ --primary: oklch(0.52 0.24 285);
1595
+ --primary-foreground: oklch(0.98 0 0);
1596
+ --secondary: oklch(0.94 0.04 285);
1597
+ --secondary-foreground: oklch(0.3 0.12 285);
1598
+ --muted: oklch(0.96 0.02 285);
1599
+ --muted-foreground: oklch(0.52 0.06 285);
1600
+ --accent: oklch(0.92 0.06 285);
1601
+ --accent-foreground: oklch(0.3 0.12 285);
1602
+ --destructive: oklch(0.577 0.245 27.325);
1603
+ --border: oklch(0.88 0.04 285);
1604
+ --input: oklch(0.88 0.04 285);
1605
+ --ring: oklch(0.52 0.24 285);
1606
+ --radius: 0.625rem;
1607
+ --chart-1: oklch(0.52 0.24 285);
1608
+ --chart-2: oklch(0.6 0.2 200);
1609
+ --chart-3: oklch(0.55 0.22 310);
1610
+ --chart-4: oklch(0.65 0.18 160);
1611
+ --chart-5: oklch(0.58 0.2 40);`;
1612
+ var VIBRANT_DARK = `
1613
+ --background: oklch(0.13 0.03 285);
1614
+ --foreground: oklch(0.97 0.01 285);
1615
+ --card: oklch(0.18 0.04 285);
1616
+ --card-foreground: oklch(0.97 0.01 285);
1617
+ --popover: oklch(0.18 0.04 285);
1618
+ --popover-foreground: oklch(0.97 0.01 285);
1619
+ --primary: oklch(0.68 0.22 285);
1620
+ --primary-foreground: oklch(0.13 0.03 285);
1621
+ --secondary: oklch(0.25 0.06 285);
1622
+ --secondary-foreground: oklch(0.97 0.01 285);
1623
+ --muted: oklch(0.24 0.05 285);
1624
+ --muted-foreground: oklch(0.68 0.08 285);
1625
+ --accent: oklch(0.28 0.08 285);
1626
+ --accent-foreground: oklch(0.97 0.01 285);
1627
+ --destructive: oklch(0.704 0.191 22.216);
1628
+ --border: oklch(1 0 0 / 12%);
1629
+ --input: oklch(1 0 0 / 15%);
1630
+ --ring: oklch(0.68 0.22 285);
1631
+ --chart-1: oklch(0.68 0.22 285);
1632
+ --chart-2: oklch(0.65 0.18 200);
1633
+ --chart-3: oklch(0.62 0.2 310);
1634
+ --chart-4: oklch(0.68 0.16 160);
1635
+ --chart-5: oklch(0.65 0.18 40);`;
1636
+ var THEMES = {
1637
+ default: { light: NEUTRAL_LIGHT, dark: NEUTRAL_DARK },
1638
+ dark: { light: NEUTRAL_LIGHT, dark: NEUTRAL_DARK },
1639
+ minimal: { light: MINIMAL_LIGHT, dark: MINIMAL_DARK },
1640
+ vibrant: { light: VIBRANT_LIGHT, dark: VIBRANT_DARK }
1641
+ };
1642
+ function generateGlobalsCss(config) {
1643
+ const { light, dark } = THEMES[config.theme];
1644
+ return `@import "tailwindcss";
1645
+
1646
+ @layer base {
1647
+ :root {${light}
1648
+ }
1649
+
1650
+ .dark {${dark}
1651
+ }
1652
+ }
1653
+ `;
1654
+ }
1655
+
1656
+ // src/cli/templates/snapshot-lib.ts
1657
+ function generateSnapshotLib(config) {
1658
+ const wsConfig = config.webSocket ? `
1659
+ ws: {
1660
+ url: import.meta.env.VITE_WS_URL,
1661
+ reconnectOnLogin: true,
1662
+ reconnectOnFocus: true,
1663
+ },` : "";
1664
+ const mfaConfig = config.mfaPages ? `
1665
+ mfaPath: '/auth/mfa-verify',
1666
+ mfaSetupPath: '/mfa-setup',` : "";
1667
+ const wsExports = config.webSocket ? ` useSocket,
1668
+ useRoom,
1669
+ useRoomEvent,
1670
+ useWebSocketManager,` : "";
1671
+ const mfaExports = config.mfaPages ? ` useMfaVerify,
1672
+ useMfaSetup,
1673
+ useMfaVerifySetup,
1674
+ useMfaDisable,
1675
+ useMfaRecoveryCodes,
1676
+ useMfaResend,
1677
+ useMfaMethods,
1678
+ useMfaEmailOtpEnable,
1679
+ useMfaEmailOtpVerifySetup,
1680
+ useMfaEmailOtpDisable,
1681
+ usePendingMfaChallenge,
1682
+ isMfaChallenge,
1683
+ useWebAuthnRegisterOptions,
1684
+ useWebAuthnRegister,
1685
+ useWebAuthnCredentials,
1686
+ useWebAuthnRemoveCredential,
1687
+ useWebAuthnDisable,` : "";
1688
+ const authPageExports = config.authPages ? ` useResetPassword,
1689
+ useVerifyEmail,
1690
+ useResendVerification,` : "";
1691
+ const bearerTokenLine = config.securityProfile === "prototype" ? `
1692
+ // WARNING: Static API credentials are not supported in production browser deployments.
1693
+ bearerToken: import.meta.env.VITE_BEARER_TOKEN,` : "";
1694
+ const oauthExchangeExport = config.securityProfile === "prototype" ? ` useOAuthExchange,
1695
+ ` : "";
1696
+ const passkeyExports = config.passkeyPages ? ` usePasskeyLoginOptions,
1697
+ usePasskeyLogin,
1698
+ ` : "";
1699
+ return `import { createSnapshot } from '@lastshotlabs/snapshot'
1700
+
1701
+ export const snapshot = createSnapshot({
1702
+ apiUrl: import.meta.env.VITE_API_URL,${bearerTokenLine}
1703
+ loginPath: '/auth/login',
1704
+ homePath: '/',
1705
+ forbiddenPath: '/403',${wsConfig}${mfaConfig}
1706
+ })
1707
+
1708
+ export const {
1709
+ useUser,
1710
+ useLogin,
1711
+ useLogout,
1712
+ useRegister,
1713
+ useForgotPassword,
1714
+ ${passkeyExports}${wsExports}
1715
+ ${mfaExports}
1716
+ ${authPageExports}
1717
+ useSetPassword,
1718
+ useDeleteAccount,
1719
+ useCancelDeletion,
1720
+ useRefreshToken,
1721
+ useSessions,
1722
+ useRevokeSession,
1723
+ ${oauthExchangeExport} getOAuthUrl,
1724
+ getLinkUrl,
1725
+ useOAuthUnlink,
1726
+ useTheme,
1727
+ protectedBeforeLoad,
1728
+ guestBeforeLoad,
1729
+ QueryProvider,
1730
+ api,
1731
+ queryClient,
1732
+ tokenStorage,
1733
+ } = snapshot
1734
+ `;
1735
+ }
1736
+
1737
+ // src/cli/templates/router-lib.ts
1738
+ function generateRouterLib() {
1739
+ return `import { createRouter } from '@tanstack/react-router'
1740
+ import { routeTree } from '../routeTree.gen'
1741
+ import { snapshot } from './snapshot'
1742
+ import { PendingComponent } from '@components/layout/PendingComponent'
1743
+ import { ErrorComponent } from '@components/layout/ErrorComponent'
1744
+ import { NotFoundComponent } from '@components/layout/NotFoundComponent'
1745
+
1746
+ export const router = createRouter({
1747
+ routeTree,
1748
+ context: { queryClient: snapshot.queryClient },
1749
+ defaultPreload: 'intent',
1750
+ defaultPreloadStaleTime: 0,
1751
+ scrollRestoration: true,
1752
+ defaultPendingComponent: PendingComponent,
1753
+ defaultErrorComponent: ErrorComponent,
1754
+ defaultNotFoundComponent: NotFoundComponent,
1755
+ })
1756
+
1757
+ declare module '@tanstack/react-router' {
1758
+ interface Register {
1759
+ router: typeof router
1760
+ }
1761
+ }
1762
+ `;
1763
+ }
1764
+
1765
+ // src/cli/templates/main.ts
1766
+ function generateMain(config) {
1767
+ const hasTooltip = config.components.includes("tooltip");
1768
+ const tooltipImport = hasTooltip ? `import { TooltipProvider } from '@components/ui/tooltip'
1769
+ ` : "";
1770
+ const inner = hasTooltip ? ` <TooltipProvider>
1771
+ <RouterProvider router={router} />
1772
+ </TooltipProvider>` : ` <RouterProvider router={router} />`;
1773
+ return `import { StrictMode } from 'react'
1774
+ import { createRoot } from 'react-dom/client'
1775
+ import { RouterProvider } from '@tanstack/react-router'
1776
+ import { QueryProvider } from '@lib/snapshot'
1777
+ import { router } from '@lib/router'
1778
+ ${tooltipImport}import '@styles/globals.css'
1779
+
1780
+ // StrictMode double-invokes effects in dev.
1781
+ // WebSocket will connect, disconnect, reconnect on mount \u2014 expected, not a bug.
1782
+ createRoot(document.getElementById('root')!).render(
1783
+ <StrictMode>
1784
+ <QueryProvider>
1785
+ ${inner}
1786
+ </QueryProvider>
1787
+ </StrictMode>
1788
+ )
1789
+ `;
1790
+ }
1791
+
1792
+ // src/cli/templates/types-api.ts
1793
+ function generateTypesApi() {
1794
+ return `// Extend these types to match your API responses
1795
+
1796
+ export interface PaginatedResponse<T> {
1797
+ data: T[]
1798
+ total: number
1799
+ page: number
1800
+ perPage: number
1801
+ }
1802
+ `;
1803
+ }
1804
+
1805
+ // src/cli/templates/store-ui.ts
1806
+ function generateStoreUi(config) {
1807
+ const sidebarAtom = config.layout === "sidebar" ? `
1808
+ export const sidebarOpenAtom = atom(false)
1809
+ ` : "";
1810
+ const wsAtom = config.webSocket ? `
1811
+ export const wsConnectedAtom = atom(false)
1812
+ ` : "";
1813
+ const hasExports = config.layout === "sidebar" || config.webSocket;
1814
+ const fallback = !hasExports ? "\nexport {}\n" : "";
1815
+ return `import { atom } from 'jotai'
1816
+ ${sidebarAtom}${wsAtom}${fallback}`;
1817
+ }
1818
+
1819
+ // src/cli/templates/utils-ts.ts
1820
+ var UTILS_CONTENT = `import { type ClassValue, clsx } from 'clsx'
1821
+ import { twMerge } from 'tailwind-merge'
1822
+
1823
+ export function cn(...inputs: ClassValue[]) {
1824
+ return twMerge(clsx(inputs))
1825
+ }
1826
+ `;
1827
+
1828
+ // src/cli/templates/snapshot-config.ts
1829
+ function generateSnapshotConfig() {
1830
+ const config = {
1831
+ apiDir: "src/api",
1832
+ hooksDir: "src/hooks/api",
1833
+ typesPath: "src/types/api.ts",
1834
+ snapshotImport: "@lib/snapshot"
1835
+ };
1836
+ return JSON.stringify(config, null, 2) + "\n";
1837
+ }
1838
+
1839
+ // src/cli/templates/routes/root.ts
1840
+ function generateRootRoute(config) {
1841
+ return `import { createRootRouteWithContext } from '@tanstack/react-router'
1842
+ import { useHead } from '@unhead/react'
1843
+ import { UnheadProvider, createHead } from '@unhead/react/client'
1844
+ import { PageTransition } from '@components/layout/PageTransition'
1845
+ import type { QueryClient } from '@tanstack/react-query'
1846
+
1847
+ const head = createHead()
1848
+
1849
+ function RootDocument() {
1850
+ useHead({ titleTemplate: '%s | ${config.projectName}' })
1851
+ return <PageTransition />
1852
+ }
1853
+
1854
+ export const Route = createRootRouteWithContext<{
1855
+ queryClient: QueryClient
1856
+ }>()({
1857
+ component: () => (
1858
+ <UnheadProvider head={head}>
1859
+ <RootDocument />
1860
+ </UnheadProvider>
1861
+ ),
1862
+ })
1863
+ `;
1864
+ }
1865
+
1866
+ // src/cli/templates/routes/index.ts
1867
+ function generateIndexRoute() {
1868
+ return `import { createFileRoute } from '@tanstack/react-router'
1869
+ import { HomePage } from '@pages/HomePage'
1870
+
1871
+ export const Route = createFileRoute('/_authenticated/')({
1872
+ component: HomePage,
1873
+ })
1874
+ `;
1875
+ }
1876
+
1877
+ // src/cli/templates/routes/authenticated.ts
1878
+ function generateAuthenticatedRoute() {
1879
+ return `import { createFileRoute, Outlet } from '@tanstack/react-router'
1880
+ import { protectedBeforeLoad } from '@lib/snapshot'
1881
+ import { RootLayout } from '@components/layout/RootLayout'
1882
+
1883
+ export const Route = createFileRoute('/_authenticated')({
1884
+ beforeLoad: protectedBeforeLoad,
1885
+ component: () => (
1886
+ <RootLayout>
1887
+ <Outlet />
1888
+ </RootLayout>
1889
+ ),
1890
+ })
1891
+ `;
1892
+ }
1893
+
1894
+ // src/cli/templates/routes/guest.ts
1895
+ function generateGuestRoute() {
1896
+ return `import { createFileRoute, Outlet } from '@tanstack/react-router'
1897
+ import { guestBeforeLoad } from '@lib/snapshot'
1898
+ import { AuthLayout } from '@components/layout/AuthLayout'
1899
+
1900
+ export const Route = createFileRoute('/_guest')({
1901
+ beforeLoad: guestBeforeLoad,
1902
+ component: () => (
1903
+ <AuthLayout>
1904
+ <Outlet />
1905
+ </AuthLayout>
1906
+ ),
1907
+ })
1908
+ `;
1909
+ }
1910
+
1911
+ // src/cli/templates/routes/auth-login.ts
1912
+ function generateLoginPage(_config) {
1913
+ return `import { createFileRoute } from '@tanstack/react-router'
1914
+ import { LoginPage } from '@pages/auth/LoginPage'
1915
+
1916
+ export const Route = createFileRoute('/_guest/auth/login')({
1917
+ component: LoginPage,
1918
+ })
1919
+ `;
1920
+ }
1921
+
1922
+ // src/cli/templates/routes/auth-register.ts
1923
+ function generateRegisterPage(_config) {
1924
+ return `import { createFileRoute } from '@tanstack/react-router'
1925
+ import { RegisterPage } from '@pages/auth/RegisterPage'
1926
+
1927
+ export const Route = createFileRoute('/_guest/auth/register')({
1928
+ component: RegisterPage,
1929
+ })
1930
+ `;
1931
+ }
1932
+
1933
+ // src/cli/templates/routes/auth-forgot.ts
1934
+ function generateForgotPasswordPage() {
1935
+ return `import { createFileRoute } from '@tanstack/react-router'
1936
+ import { ForgotPasswordPage } from '@pages/auth/ForgotPasswordPage'
1937
+
1938
+ export const Route = createFileRoute('/_guest/auth/forgot-password')({
1939
+ component: ForgotPasswordPage,
1940
+ })
1941
+ `;
1942
+ }
1943
+
1944
+ // src/cli/templates/routes/auth-mfa-verify.ts
1945
+ function generateMfaVerifyPage(_config) {
1946
+ return `import { createFileRoute } from '@tanstack/react-router'
1947
+ import { MfaVerifyPage } from '@pages/auth/MfaVerifyPage'
1948
+
1949
+ export const Route = createFileRoute('/_guest/auth/mfa-verify')({
1950
+ component: MfaVerifyPage,
1951
+ })
1952
+ `;
1953
+ }
1954
+
1955
+ // src/cli/templates/routes/mfa-setup.ts
1956
+ function generateMfaSetupPage(_config) {
1957
+ return `import { createFileRoute } from '@tanstack/react-router'
1958
+ import { MfaSetupPage } from '@pages/auth/MfaSetupPage'
1959
+
1960
+ export const Route = createFileRoute('/_authenticated/mfa-setup')({
1961
+ component: MfaSetupPage,
1962
+ })
1963
+ `;
1964
+ }
1965
+
1966
+ // src/cli/templates/routes/auth-reset-password.ts
1967
+ function generateResetPasswordPage() {
1968
+ return `import { createFileRoute } from '@tanstack/react-router'
1969
+ import { z } from 'zod'
1970
+ import { ResetPasswordPage } from '@pages/auth/ResetPasswordPage'
1971
+
1972
+ export const Route = createFileRoute('/_guest/auth/reset-password')({
1973
+ validateSearch: z.object({
1974
+ token: z.string(),
1975
+ }),
1976
+ component: ResetPasswordPage,
1977
+ })
1978
+ `;
1979
+ }
1980
+
1981
+ // src/cli/templates/routes/auth-verify-email.ts
1982
+ function generateVerifyEmailPage() {
1983
+ return `import { createFileRoute } from '@tanstack/react-router'
1984
+ import { z } from 'zod'
1985
+ import { VerifyEmailPage } from '@pages/auth/VerifyEmailPage'
1986
+
1987
+ export const Route = createFileRoute('/_guest/auth/verify-email')({
1988
+ validateSearch: z.object({
1989
+ token: z.string().optional(),
1990
+ }),
1991
+ component: VerifyEmailPage,
1992
+ })
1993
+ `;
1994
+ }
1995
+
1996
+ // src/cli/templates/routes/auth-oauth-callback.ts
1997
+ function generateOAuthCallbackPage(config) {
1998
+ if (config.securityProfile === "prototype") {
1999
+ return `import { createFileRoute } from '@tanstack/react-router'
2000
+ import { z } from 'zod'
2001
+ import { OAuthCallbackPage } from '@pages/auth/OAuthCallbackPage'
2002
+
2003
+ export const Route = createFileRoute('/_guest/auth/oauth/callback')({
2004
+ validateSearch: z.object({
2005
+ code: z.string().optional(),
2006
+ error: z.string().optional(),
2007
+ }),
2008
+ component: OAuthCallbackPage,
2009
+ })
2010
+ `;
2011
+ }
2012
+ return `import { createFileRoute } from '@tanstack/react-router'
2013
+ import { z } from 'zod'
2014
+ import { OAuthCallbackPage } from '@pages/auth/OAuthCallbackPage'
2015
+
2016
+ export const Route = createFileRoute('/_guest/auth/oauth/callback')({
2017
+ validateSearch: z.object({
2018
+ success: z.boolean().optional(),
2019
+ error: z.string().optional(),
2020
+ }),
2021
+ component: OAuthCallbackPage,
2022
+ })
2023
+ `;
2024
+ }
2025
+
2026
+ // src/cli/templates/pages/auth-login.ts
2027
+ function generateLoginPageComponent(config) {
2028
+ if (config.passkeyPages) {
2029
+ return `import { useState } from 'react'
2030
+ import { Link } from '@tanstack/react-router'
2031
+ import { useHead } from '@unhead/react'
2032
+ import { startAuthentication } from '@simplewebauthn/browser'
2033
+ import { useLogin, usePasskeyLoginOptions, usePasskeyLogin } from '@lib/snapshot'
2034
+ import { Button } from '@components/ui/button'
2035
+ import { Input } from '@components/ui/input'
2036
+ import { Label } from '@components/ui/label'
2037
+ import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'
2038
+
2039
+ export function LoginPage() {
2040
+ useHead({ title: 'Sign in' })
2041
+ const login = useLogin()
2042
+ const loginOptions = usePasskeyLoginOptions()
2043
+ const passkeyLogin = usePasskeyLogin()
2044
+ const [email, setEmail] = useState('')
2045
+
2046
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2047
+ e.preventDefault()
2048
+ const data = new FormData(e.currentTarget)
2049
+ login.mutate({
2050
+ email: data.get('email') as string,
2051
+ password: data.get('password') as string,
2052
+ })
2053
+ }
2054
+
2055
+ async function handlePasskeyLogin() {
2056
+ try {
2057
+ const { options, passkeyToken } = await loginOptions.mutateAsync({ email: email || undefined })
2058
+ const assertionResponse = await startAuthentication(options as any)
2059
+ await passkeyLogin.mutateAsync({ passkeyToken, assertionResponse })
2060
+ } catch (e: any) {
2061
+ if (e?.name === 'NotAllowedError') return
2062
+ }
2063
+ }
2064
+
2065
+ return (
2066
+ <Card>
2067
+ <CardHeader>
2068
+ <CardTitle>Sign in to ${config.projectName}</CardTitle>
2069
+ </CardHeader>
2070
+ <CardContent>
2071
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2072
+ <div className="space-y-2">
2073
+ <Label htmlFor="email">Email</Label>
2074
+ <Input
2075
+ id="email"
2076
+ name="email"
2077
+ type="email"
2078
+ placeholder="you@example.com"
2079
+ required
2080
+ value={email}
2081
+ onChange={e => setEmail(e.target.value)}
2082
+ />
2083
+ </div>
2084
+ <div className="space-y-2">
2085
+ <Label htmlFor="password">Password</Label>
2086
+ <Input id="password" name="password" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2087
+ </div>
2088
+ {login.isError && (
2089
+ <p className="text-destructive text-sm">{login.error.message}</p>
2090
+ )}
2091
+ <Button type="submit" disabled={login.isPending}>
2092
+ {login.isPending ? 'Signing in...' : 'Sign in'}
2093
+ </Button>
2094
+ </form>
2095
+ {typeof window !== 'undefined' && !!window.PublicKeyCredential && (
2096
+ <div className="mt-4 flex flex-col gap-3">
2097
+ <div className="relative">
2098
+ <div className="absolute inset-0 flex items-center">
2099
+ <span className="w-full border-t" />
2100
+ </div>
2101
+ <div className="relative flex justify-center text-xs uppercase">
2102
+ <span className="bg-background px-2 text-muted-foreground">or</span>
2103
+ </div>
2104
+ </div>
2105
+ {passkeyLogin.isError && (
2106
+ <p className="text-destructive text-sm">{passkeyLogin.error.message}</p>
2107
+ )}
2108
+ <Button
2109
+ type="button"
2110
+ variant="outline"
2111
+ onClick={handlePasskeyLogin}
2112
+ disabled={loginOptions.isPending || passkeyLogin.isPending}
2113
+ >
2114
+ {loginOptions.isPending || passkeyLogin.isPending ? 'Signing in...' : 'Sign in with passkey'}
2115
+ </Button>
2116
+ </div>
2117
+ )}
2118
+ {/* To add OAuth: import { getOAuthUrl } from '@lib/snapshot' and link to getOAuthUrl('google') */}
2119
+ <div className="mt-4 flex flex-col gap-2 text-sm">
2120
+ <Link to="/auth/register" className="text-muted-foreground hover:text-foreground">
2121
+ Create account
2122
+ </Link>
2123
+ <Link to="/auth/forgot-password" className="text-muted-foreground hover:text-foreground">
2124
+ Forgot password?
2125
+ </Link>
2126
+ </div>
2127
+ </CardContent>
2128
+ </Card>
2129
+ )
2130
+ }
2131
+ `;
2132
+ }
2133
+ return `import { Link } from '@tanstack/react-router'
2134
+ import { useHead } from '@unhead/react'
2135
+ import { useLogin } from '@lib/snapshot'
2136
+ import { Button } from '@components/ui/button'
2137
+ import { Input } from '@components/ui/input'
2138
+ import { Label } from '@components/ui/label'
2139
+ import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'
2140
+
2141
+ export function LoginPage() {
2142
+ useHead({ title: 'Sign in' })
2143
+ const login = useLogin()
2144
+
2145
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2146
+ e.preventDefault()
2147
+ const data = new FormData(e.currentTarget)
2148
+ login.mutate({
2149
+ email: data.get('email') as string,
2150
+ password: data.get('password') as string,
2151
+ })
2152
+ }
2153
+
2154
+ return (
2155
+ <Card>
2156
+ <CardHeader>
2157
+ <CardTitle>Sign in to ${config.projectName}</CardTitle>
2158
+ </CardHeader>
2159
+ <CardContent>
2160
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2161
+ <div className="space-y-2">
2162
+ <Label htmlFor="email">Email</Label>
2163
+ <Input id="email" name="email" type="email" placeholder="you@example.com" required />
2164
+ </div>
2165
+ <div className="space-y-2">
2166
+ <Label htmlFor="password">Password</Label>
2167
+ <Input id="password" name="password" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2168
+ </div>
2169
+ {login.isError && (
2170
+ <p className="text-destructive text-sm">{login.error.message}</p>
2171
+ )}
2172
+ <Button type="submit" disabled={login.isPending}>
2173
+ {login.isPending ? 'Signing in...' : 'Sign in'}
2174
+ </Button>
2175
+ </form>
2176
+ {/* To add OAuth: import { getOAuthUrl } from '@lib/snapshot' and link to getOAuthUrl('google') */}
2177
+ <div className="mt-4 flex flex-col gap-2 text-sm">
2178
+ <Link to="/auth/register" className="text-muted-foreground hover:text-foreground">
2179
+ Create account
2180
+ </Link>
2181
+ <Link to="/auth/forgot-password" className="text-muted-foreground hover:text-foreground">
2182
+ Forgot password?
2183
+ </Link>
2184
+ </div>
2185
+ </CardContent>
2186
+ </Card>
2187
+ )
2188
+ }
2189
+ `;
2190
+ }
2191
+
2192
+ // src/cli/templates/pages/auth-passkey-manage.ts
2193
+ function generatePasskeyManagePageComponent() {
2194
+ return `import { useState } from 'react'
2195
+ import { useHead } from '@unhead/react'
2196
+ import { startRegistration } from '@simplewebauthn/browser'
2197
+ import {
2198
+ useWebAuthnRegisterOptions,
2199
+ useWebAuthnRegister,
2200
+ useWebAuthnCredentials,
2201
+ useWebAuthnRemoveCredential,
2202
+ } from '@lib/snapshot'
2203
+ import { Button } from '@components/ui/button'
2204
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2205
+
2206
+ export function PasskeyManagePage() {
2207
+ useHead({ title: 'Passkeys' })
2208
+ const credentials = useWebAuthnCredentials()
2209
+ const registerOptions = useWebAuthnRegisterOptions()
2210
+ const register = useWebAuthnRegister()
2211
+ const remove = useWebAuthnRemoveCredential()
2212
+ const [addError, setAddError] = useState<string | null>(null)
2213
+
2214
+ async function handleAddPasskey() {
2215
+ setAddError(null)
2216
+ try {
2217
+ const { options, registrationToken } = await registerOptions.mutateAsync()
2218
+ const attestationResponse = await startRegistration(options as any)
2219
+ await register.mutateAsync({ registrationToken, attestationResponse })
2220
+ } catch (e: any) {
2221
+ if (e?.name === 'NotAllowedError') return
2222
+ setAddError(e?.message ?? 'Registration failed')
2223
+ }
2224
+ }
2225
+
2226
+ return (
2227
+ <Card>
2228
+ <CardHeader>
2229
+ <CardTitle>Passkeys</CardTitle>
2230
+ <CardDescription>
2231
+ Sign in with Windows Hello, Face ID, Touch ID, or a hardware security key \u2014 no password needed.
2232
+ </CardDescription>
2233
+ </CardHeader>
2234
+ <CardContent className="flex flex-col gap-4">
2235
+ {credentials.isLoading && (
2236
+ <p className="text-sm text-muted-foreground">Loading...</p>
2237
+ )}
2238
+ {credentials.credentials?.length === 0 && (
2239
+ <p className="text-sm text-muted-foreground">No passkeys registered yet.</p>
2240
+ )}
2241
+ {credentials.credentials?.map((cred) => (
2242
+ <div
2243
+ key={cred.credentialId}
2244
+ className="flex items-center justify-between rounded-md border px-4 py-3"
2245
+ >
2246
+ <div>
2247
+ <p className="text-sm font-medium">{cred.name ?? 'Passkey'}</p>
2248
+ <p className="text-xs text-muted-foreground">
2249
+ Added {new Date(cred.createdAt).toLocaleDateString()}
2250
+ </p>
2251
+ </div>
2252
+ <Button
2253
+ variant="destructive"
2254
+ size="sm"
2255
+ onClick={() => remove.mutate(cred.credentialId)}
2256
+ disabled={remove.isPending}
2257
+ >
2258
+ Remove
2259
+ </Button>
2260
+ </div>
2261
+ ))}
2262
+ {addError && <p className="text-destructive text-sm">{addError}</p>}
2263
+ <Button
2264
+ onClick={handleAddPasskey}
2265
+ disabled={registerOptions.isPending || register.isPending}
2266
+ >
2267
+ {registerOptions.isPending || register.isPending ? 'Registering...' : 'Add passkey'}
2268
+ </Button>
2269
+ </CardContent>
2270
+ </Card>
2271
+ )
2272
+ }
2273
+ `;
2274
+ }
2275
+
2276
+ // src/cli/templates/routes/passkey-manage.ts
2277
+ function generatePasskeyManagePage() {
2278
+ return `import { createFileRoute } from '@tanstack/react-router'
2279
+ import { PasskeyManagePage } from '@pages/auth/PasskeyManagePage'
2280
+
2281
+ export const Route = createFileRoute('/_authenticated/passkey')({
2282
+ component: PasskeyManagePage,
2283
+ })
2284
+ `;
2285
+ }
2286
+
2287
+ // src/cli/templates/pages/auth-register.ts
2288
+ function generateRegisterPageComponent(config) {
2289
+ return `import { useState } from 'react'
2290
+ import { Link } from '@tanstack/react-router'
2291
+ import { useHead } from '@unhead/react'
2292
+ import { useRegister } from '@lib/snapshot'
2293
+ import { Button } from '@components/ui/button'
2294
+ import { Input } from '@components/ui/input'
2295
+ import { Label } from '@components/ui/label'
2296
+ import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'
2297
+
2298
+ export function RegisterPage() {
2299
+ useHead({ title: 'Create account' })
2300
+ const register = useRegister()
2301
+ const [passwordError, setPasswordError] = useState<string | null>(null)
2302
+
2303
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2304
+ e.preventDefault()
2305
+ const data = new FormData(e.currentTarget)
2306
+ const password = data.get('password') as string
2307
+ const confirmPassword = data.get('confirmPassword') as string
2308
+ if (password !== confirmPassword) {
2309
+ setPasswordError('Passwords do not match')
2310
+ return
2311
+ }
2312
+ setPasswordError(null)
2313
+ register.mutate({
2314
+ email: data.get('email') as string,
2315
+ password,
2316
+ })
2317
+ }
2318
+
2319
+ return (
2320
+ <Card>
2321
+ <CardHeader>
2322
+ <CardTitle>Create your ${config.projectName} account</CardTitle>
2323
+ </CardHeader>
2324
+ <CardContent>
2325
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2326
+ <div className="space-y-2">
2327
+ <Label htmlFor="email">Email</Label>
2328
+ <Input id="email" name="email" type="email" placeholder="you@example.com" required />
2329
+ </div>
2330
+ <div className="space-y-2">
2331
+ <Label htmlFor="password">Password</Label>
2332
+ <Input id="password" name="password" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2333
+ </div>
2334
+ <div className="space-y-2">
2335
+ <Label htmlFor="confirmPassword">Confirm password</Label>
2336
+ <Input id="confirmPassword" name="confirmPassword" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2337
+ </div>
2338
+ {passwordError && (
2339
+ <p className="text-destructive text-sm">{passwordError}</p>
2340
+ )}
2341
+ {register.isError && (
2342
+ <p className="text-destructive text-sm">{register.error.message}</p>
2343
+ )}
2344
+ <Button type="submit" disabled={register.isPending}>
2345
+ {register.isPending ? 'Creating account...' : 'Create account'}
2346
+ </Button>
2347
+ </form>
2348
+ <div className="mt-4 text-sm">
2349
+ <Link to="/auth/login" className="text-muted-foreground hover:text-foreground">
2350
+ Already have an account? Sign in
2351
+ </Link>
2352
+ </div>
2353
+ </CardContent>
2354
+ </Card>
2355
+ )
2356
+ }
2357
+ `;
2358
+ }
2359
+
2360
+ // src/cli/templates/pages/auth-forgot.ts
2361
+ function generateForgotPasswordPageComponent() {
2362
+ return `import { Link } from '@tanstack/react-router'
2363
+ import { useHead } from '@unhead/react'
2364
+ import { useForgotPassword } from '@lib/snapshot'
2365
+ import { Button } from '@components/ui/button'
2366
+ import { Input } from '@components/ui/input'
2367
+ import { Label } from '@components/ui/label'
2368
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2369
+
2370
+ export function ForgotPasswordPage() {
2371
+ useHead({ title: 'Reset password' })
2372
+ const forgotPassword = useForgotPassword()
2373
+
2374
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2375
+ e.preventDefault()
2376
+ const data = new FormData(e.currentTarget)
2377
+ forgotPassword.mutate({ email: data.get('email') as string })
2378
+ }
2379
+
2380
+ return (
2381
+ <Card>
2382
+ <CardHeader>
2383
+ <CardTitle>Reset your password</CardTitle>
2384
+ <CardDescription>
2385
+ Enter your email and we'll send you a reset link.
2386
+ </CardDescription>
2387
+ </CardHeader>
2388
+ <CardContent>
2389
+ {forgotPassword.isSuccess ? (
2390
+ <p className="text-sm text-muted-foreground">
2391
+ Check your email for a password reset link.
2392
+ </p>
2393
+ ) : (
2394
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2395
+ <div className="space-y-2">
2396
+ <Label htmlFor="email">Email</Label>
2397
+ <Input id="email" name="email" type="email" placeholder="you@example.com" required />
2398
+ </div>
2399
+ {forgotPassword.isError && (
2400
+ <p className="text-destructive text-sm">{forgotPassword.error.message}</p>
2401
+ )}
2402
+ <Button type="submit" disabled={forgotPassword.isPending}>
2403
+ {forgotPassword.isPending ? 'Sending...' : 'Send reset link'}
2404
+ </Button>
2405
+ </form>
2406
+ )}
2407
+ <div className="mt-4 text-sm">
2408
+ <Link to="/auth/login" className="text-muted-foreground hover:text-foreground">
2409
+ Back to sign in
2410
+ </Link>
2411
+ </div>
2412
+ </CardContent>
2413
+ </Card>
2414
+ )
2415
+ }
2416
+ `;
2417
+ }
2418
+
2419
+ // src/cli/templates/pages/auth-mfa-verify.ts
2420
+ function generateMfaVerifyPageComponent(config) {
2421
+ return `import { useState } from 'react'
2422
+ import { useHead } from '@unhead/react'
2423
+ import { Link } from '@tanstack/react-router'
2424
+ import { useMfaVerify, useMfaResend, usePendingMfaChallenge } from '@lib/snapshot'
2425
+ import { Button } from '@components/ui/button'
2426
+ import { Input } from '@components/ui/input'
2427
+ import { Label } from '@components/ui/label'
2428
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2429
+
2430
+ export function MfaVerifyPage() {
2431
+ useHead({ title: 'Two-factor authentication' })
2432
+ const pendingChallenge = usePendingMfaChallenge()
2433
+ const [useRecovery, setUseRecovery] = useState(false)
2434
+ const verify = useMfaVerify()
2435
+ const resend = useMfaResend()
2436
+
2437
+ if (!pendingChallenge) {
2438
+ return (
2439
+ <Card>
2440
+ <CardContent className="pt-6">
2441
+ <p className="text-sm text-muted-foreground">
2442
+ Your MFA challenge has expired or is no longer valid. Please{' '}
2443
+ <Link to="/auth/login" className="text-foreground underline underline-offset-4 hover:text-foreground/80">
2444
+ sign in
2445
+ </Link>{' '}
2446
+ again.
2447
+ </p>
2448
+ </CardContent>
2449
+ </Card>
2450
+ )
2451
+ }
2452
+
2453
+ const availableMethods = pendingChallenge.mfaMethods
2454
+
2455
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2456
+ e.preventDefault()
2457
+ const data = new FormData(e.currentTarget)
2458
+ const method = useRecovery ? 'recovery' : (availableMethods[0] ?? 'totp')
2459
+ verify.mutate({ code: data.get('code') as string, method })
2460
+ }
2461
+
2462
+ return (
2463
+ <Card>
2464
+ <CardHeader>
2465
+ <CardTitle>{useRecovery ? 'Recovery code' : 'Two-factor authentication'}</CardTitle>
2466
+ <CardDescription>
2467
+ {useRecovery
2468
+ ? 'Enter one of your recovery codes.'
2469
+ : 'Enter the 6-digit code from your authenticator app or email.'}
2470
+ </CardDescription>
2471
+ </CardHeader>
2472
+ <CardContent>
2473
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2474
+ <div className="space-y-2">
2475
+ <Label htmlFor="code">{useRecovery ? 'Recovery code' : 'Verification code'}</Label>
2476
+ <Input
2477
+ id="code"
2478
+ name="code"
2479
+ type="text"
2480
+ inputMode={useRecovery ? 'text' : 'numeric'}
2481
+ placeholder={useRecovery ? 'xxxxxxxx-xxxx' : '000000'}
2482
+ autoComplete="one-time-code"
2483
+ maxLength={useRecovery ? 20 : 6}
2484
+ required
2485
+ autoFocus
2486
+ />
2487
+ </div>
2488
+ {verify.isError && (
2489
+ <p className="text-destructive text-sm">{verify.error.message}</p>
2490
+ )}
2491
+ <Button type="submit" disabled={verify.isPending}>
2492
+ {verify.isPending ? 'Verifying...' : 'Verify'}
2493
+ </Button>
2494
+ </form>
2495
+ <div className="mt-4 flex flex-col gap-2 text-sm">
2496
+ <button
2497
+ type="button"
2498
+ onClick={() => setUseRecovery(!useRecovery)}
2499
+ className="text-muted-foreground hover:text-foreground text-left transition-colors"
2500
+ >
2501
+ {useRecovery ? 'Use authenticator code instead' : 'Use a recovery code instead'}
2502
+ </button>
2503
+ {!useRecovery && availableMethods.includes('emailOtp') && (
2504
+ <button
2505
+ type="button"
2506
+ onClick={() => resend.mutate({ mfaToken: pendingChallenge.mfaToken })}
2507
+ disabled={resend.isPending || resend.isSuccess}
2508
+ className="text-muted-foreground hover:text-foreground text-left transition-colors disabled:opacity-50"
2509
+ >
2510
+ {resend.isPending ? 'Sending...' : resend.isSuccess ? 'Code sent' : 'Resend email code'}
2511
+ </button>
2512
+ )}
2513
+ </div>
2514
+ </CardContent>
2515
+ </Card>
2516
+ )
2517
+ }
2518
+ `;
2519
+ }
2520
+
2521
+ // src/cli/templates/pages/auth-mfa-setup.ts
2522
+ function generateMfaSetupPageComponent(_config) {
2523
+ return `import { useState } from 'react'
2524
+ import { useHead } from '@unhead/react'
2525
+ import { Link } from '@tanstack/react-router'
2526
+ import QRCode from 'react-qr-code'
2527
+ import { useMfaSetup, useMfaVerifySetup } from '@lib/snapshot'
2528
+ import { Button } from '@components/ui/button'
2529
+ import { Input } from '@components/ui/input'
2530
+ import { Label } from '@components/ui/label'
2531
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2532
+
2533
+ export function MfaSetupPage() {
2534
+ useHead({ title: 'Set up authenticator' })
2535
+ const setup = useMfaSetup()
2536
+ const verifySetup = useMfaVerifySetup()
2537
+ const [copied, setCopied] = useState(false)
2538
+
2539
+ function handleVerify(e: React.FormEvent<HTMLFormElement>) {
2540
+ e.preventDefault()
2541
+ const data = new FormData(e.currentTarget)
2542
+ verifySetup.mutate({ code: data.get('code') as string })
2543
+ }
2544
+
2545
+ function handleCopy() {
2546
+ navigator.clipboard.writeText(verifySetup.data!.recoveryCodes.join('\\n'))
2547
+ setCopied(true)
2548
+ setTimeout(() => setCopied(false), 2000)
2549
+ }
2550
+
2551
+ // Step 3: Recovery codes
2552
+ if (verifySetup.isSuccess) {
2553
+ return (
2554
+ <Card>
2555
+ <CardHeader>
2556
+ <CardTitle>Save your recovery codes</CardTitle>
2557
+ <CardDescription>
2558
+ Store these codes in a safe place. Each code can only be used once.
2559
+ </CardDescription>
2560
+ </CardHeader>
2561
+ <CardContent className="flex flex-col gap-4">
2562
+ <div className="grid grid-cols-2 gap-2 rounded-md border bg-muted p-4 font-mono text-sm">
2563
+ {verifySetup.data.recoveryCodes.map((code) => (
2564
+ <span key={code}>{code}</span>
2565
+ ))}
2566
+ </div>
2567
+ <Button variant="outline" onClick={handleCopy}>
2568
+ {copied ? 'Copied!' : 'Copy codes'}
2569
+ </Button>
2570
+ <Link to="/" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
2571
+ Done \u2014 go to dashboard
2572
+ </Link>
2573
+ </CardContent>
2574
+ </Card>
2575
+ )
2576
+ }
2577
+
2578
+ // Step 2: QR code + verify
2579
+ if (setup.isSuccess) {
2580
+ return (
2581
+ <Card>
2582
+ <CardHeader>
2583
+ <CardTitle>Scan the QR code</CardTitle>
2584
+ <CardDescription>
2585
+ Scan this code with your authenticator app, then enter the 6-digit code below.
2586
+ </CardDescription>
2587
+ </CardHeader>
2588
+ <CardContent className="flex flex-col gap-4">
2589
+ <div className="flex justify-center rounded-md border bg-white p-4">
2590
+ <QRCode value={setup.data.uri} size={200} />
2591
+ </div>
2592
+ <details className="text-sm">
2593
+ <summary className="cursor-pointer text-muted-foreground">
2594
+ Can't scan? Enter this key manually
2595
+ </summary>
2596
+ <code className="mt-2 block break-all rounded bg-muted px-3 py-2 font-mono text-xs">
2597
+ {setup.data.secret}
2598
+ </code>
2599
+ </details>
2600
+ <form onSubmit={handleVerify} className="flex flex-col gap-4">
2601
+ <div className="space-y-2">
2602
+ <Label htmlFor="code">Verification code</Label>
2603
+ <Input
2604
+ id="code"
2605
+ name="code"
2606
+ type="text"
2607
+ inputMode="numeric"
2608
+ placeholder="000000"
2609
+ autoComplete="one-time-code"
2610
+ maxLength={6}
2611
+ required
2612
+ autoFocus
2613
+ />
2614
+ </div>
2615
+ {verifySetup.isError && (
2616
+ <p className="text-destructive text-sm">{verifySetup.error.message}</p>
2617
+ )}
2618
+ <Button type="submit" disabled={verifySetup.isPending}>
2619
+ {verifySetup.isPending ? 'Verifying...' : 'Verify & enable'}
2620
+ </Button>
2621
+ </form>
2622
+ </CardContent>
2623
+ </Card>
2624
+ )
2625
+ }
2626
+
2627
+ // Step 1: Initiate
2628
+ return (
2629
+ <Card>
2630
+ <CardHeader>
2631
+ <CardTitle>Two-factor authentication</CardTitle>
2632
+ <CardDescription>
2633
+ Add an extra layer of security to your account with a TOTP authenticator app.
2634
+ </CardDescription>
2635
+ </CardHeader>
2636
+ <CardContent className="flex flex-col gap-4">
2637
+ {setup.isError && (
2638
+ <p className="text-destructive text-sm">{setup.error.message}</p>
2639
+ )}
2640
+ <Button onClick={() => setup.mutate()} disabled={setup.isPending}>
2641
+ {setup.isPending ? 'Generating...' : 'Set up authenticator'}
2642
+ </Button>
2643
+ </CardContent>
2644
+ </Card>
2645
+ )
2646
+ }
2647
+ `;
2648
+ }
2649
+
2650
+ // src/cli/templates/pages/auth-reset-password.ts
2651
+ function generateResetPasswordPageComponent() {
2652
+ return `import { useState } from 'react'
2653
+ import { useHead } from '@unhead/react'
2654
+ import { Link } from '@tanstack/react-router'
2655
+ import { Route } from '../../routes/_guest/auth/reset-password'
2656
+ import { useResetPassword } from '@lib/snapshot'
2657
+ import { Button } from '@components/ui/button'
2658
+ import { Input } from '@components/ui/input'
2659
+ import { Label } from '@components/ui/label'
2660
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2661
+
2662
+ export function ResetPasswordPage() {
2663
+ useHead({ title: 'Set new password' })
2664
+ const { token } = Route.useSearch()
2665
+ const resetPassword = useResetPassword()
2666
+ const [passwordError, setPasswordError] = useState<string | null>(null)
2667
+
2668
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2669
+ e.preventDefault()
2670
+ const data = new FormData(e.currentTarget)
2671
+ const password = data.get('password') as string
2672
+ const confirmPassword = data.get('confirmPassword') as string
2673
+ if (password !== confirmPassword) {
2674
+ setPasswordError('Passwords do not match')
2675
+ return
2676
+ }
2677
+ setPasswordError(null)
2678
+ resetPassword.mutate({ token, password })
2679
+ }
2680
+
2681
+ return (
2682
+ <Card>
2683
+ <CardHeader>
2684
+ <CardTitle>Set new password</CardTitle>
2685
+ <CardDescription>Choose a strong password for your account.</CardDescription>
2686
+ </CardHeader>
2687
+ <CardContent>
2688
+ {resetPassword.isSuccess ? (
2689
+ <div className="flex flex-col gap-4">
2690
+ <p className="text-sm text-muted-foreground">Your password has been updated.</p>
2691
+ <Link to="/auth/login" className="text-sm text-foreground underline underline-offset-4 hover:text-foreground/80">
2692
+ Sign in \u2192
2693
+ </Link>
2694
+ </div>
2695
+ ) : (
2696
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
2697
+ <div className="space-y-2">
2698
+ <Label htmlFor="password">New password</Label>
2699
+ <Input id="password" name="password" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2700
+ </div>
2701
+ <div className="space-y-2">
2702
+ <Label htmlFor="confirmPassword">Confirm password</Label>
2703
+ <Input id="confirmPassword" name="confirmPassword" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" required />
2704
+ </div>
2705
+ {passwordError && (
2706
+ <p className="text-destructive text-sm">{passwordError}</p>
2707
+ )}
2708
+ {resetPassword.isError && (
2709
+ <p className="text-destructive text-sm">{resetPassword.error.message}</p>
2710
+ )}
2711
+ <Button type="submit" disabled={resetPassword.isPending}>
2712
+ {resetPassword.isPending ? 'Updating...' : 'Update password'}
2713
+ </Button>
2714
+ </form>
2715
+ )}
2716
+ </CardContent>
2717
+ </Card>
2718
+ )
2719
+ }
2720
+ `;
2721
+ }
2722
+
2723
+ // src/cli/templates/pages/auth-verify-email.ts
2724
+ function generateVerifyEmailPageComponent() {
2725
+ return `import { useEffect } from 'react'
2726
+ import { useHead } from '@unhead/react'
2727
+ import { Route } from '../../routes/_guest/auth/verify-email'
2728
+ import { useVerifyEmail, useResendVerification } from '@lib/snapshot'
2729
+ import { Button } from '@components/ui/button'
2730
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2731
+
2732
+ export function VerifyEmailPage() {
2733
+ useHead({ title: 'Verify email' })
2734
+ const { token } = Route.useSearch()
2735
+ const verifyEmail = useVerifyEmail()
2736
+ const resend = useResendVerification()
2737
+
2738
+ useEffect(() => {
2739
+ if (token) {
2740
+ verifyEmail.mutate({ token })
2741
+ }
2742
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2743
+ }, [token])
2744
+
2745
+ if (token) {
2746
+ return (
2747
+ <Card>
2748
+ <CardHeader>
2749
+ <CardTitle>Email verification</CardTitle>
2750
+ </CardHeader>
2751
+ <CardContent>
2752
+ {verifyEmail.isPending && (
2753
+ <p className="text-sm text-muted-foreground">Verifying your email...</p>
2754
+ )}
2755
+ {verifyEmail.isSuccess && (
2756
+ <p className="text-sm text-muted-foreground">
2757
+ Your email has been verified. You can now sign in.
2758
+ </p>
2759
+ )}
2760
+ {verifyEmail.isError && (
2761
+ <p className="text-destructive text-sm">{verifyEmail.error.message}</p>
2762
+ )}
2763
+ </CardContent>
2764
+ </Card>
2765
+ )
2766
+ }
2767
+
2768
+ // No token \u2014 show "check your email" + resend option
2769
+ return (
2770
+ <Card>
2771
+ <CardHeader>
2772
+ <CardTitle>Check your email</CardTitle>
2773
+ <CardDescription>
2774
+ We sent you a verification link. Click it to verify your email address.
2775
+ </CardDescription>
2776
+ </CardHeader>
2777
+ <CardContent className="flex flex-col gap-4">
2778
+ {resend.isSuccess ? (
2779
+ <p className="text-sm text-muted-foreground">Verification email resent. Check your inbox.</p>
2780
+ ) : (
2781
+ <Button
2782
+ variant="outline"
2783
+ disabled={resend.isPending}
2784
+ onClick={() => {
2785
+ const email = prompt('Enter your email address to resend the verification link:')
2786
+ if (email) resend.mutate({ email })
2787
+ }}
2788
+ >
2789
+ {resend.isPending ? 'Sending...' : 'Resend verification email'}
2790
+ </Button>
2791
+ )}
2792
+ {resend.isError && (
2793
+ <p className="text-destructive text-sm">{resend.error.message}</p>
2794
+ )}
2795
+ </CardContent>
2796
+ </Card>
2797
+ )
2798
+ }
2799
+ `;
2800
+ }
2801
+
2802
+ // src/cli/templates/pages/auth-oauth-callback.ts
2803
+ function generateOAuthCallbackPageComponent(config) {
2804
+ if (config.securityProfile === "prototype") {
2805
+ return `import { useEffect } from 'react'
2806
+ import { useHead } from '@unhead/react'
2807
+ import { Link } from '@tanstack/react-router'
2808
+ import { Route } from '../../routes/_guest/auth/oauth/callback'
2809
+ import { useOAuthExchange } from '@lib/snapshot'
2810
+ import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'
2811
+ import { Button } from '@components/ui/button'
2812
+
2813
+ export function OAuthCallbackPage() {
2814
+ // Legacy OAuth exchange \u2014 update to cookie mode for production
2815
+ useHead({ title: 'Signing in...' })
2816
+ const { code, error } = Route.useSearch()
2817
+ const exchange = useOAuthExchange()
2818
+
2819
+ useEffect(() => {
2820
+ if (code) {
2821
+ exchange.mutate({ code })
2822
+ }
2823
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2824
+ }, [code])
2825
+
2826
+ if (error) {
2827
+ return (
2828
+ <Card>
2829
+ <CardHeader>
2830
+ <CardTitle>Sign in failed</CardTitle>
2831
+ </CardHeader>
2832
+ <CardContent className="flex flex-col gap-4">
2833
+ <p className="text-destructive text-sm">{error}</p>
2834
+ <Button asChild variant="outline">
2835
+ <Link to="/auth/login">Back to sign in</Link>
2836
+ </Button>
2837
+ </CardContent>
2838
+ </Card>
2839
+ )
2840
+ }
2841
+
2842
+ if (exchange.isError) {
2843
+ return (
2844
+ <Card>
2845
+ <CardHeader>
2846
+ <CardTitle>Sign in failed</CardTitle>
2847
+ </CardHeader>
2848
+ <CardContent className="flex flex-col gap-4">
2849
+ <p className="text-destructive text-sm">{exchange.error.message}</p>
2850
+ <Button asChild variant="outline">
2851
+ <Link to="/auth/login">Back to sign in</Link>
2852
+ </Button>
2853
+ </CardContent>
2854
+ </Card>
2855
+ )
2856
+ }
2857
+
2858
+ return (
2859
+ <Card>
2860
+ <CardHeader>
2861
+ <CardTitle>Signing in...</CardTitle>
2862
+ </CardHeader>
2863
+ <CardContent>
2864
+ <p className="text-sm text-muted-foreground">Please wait while we complete your sign in.</p>
2865
+ </CardContent>
2866
+ </Card>
2867
+ )
2868
+ }
2869
+ `;
2870
+ }
2871
+ return `import { useEffect } from 'react'
2872
+ import { useHead } from '@unhead/react'
2873
+ import { useNavigate, Link } from '@tanstack/react-router'
2874
+ import { Route } from '../../routes/_guest/auth/oauth/callback'
2875
+ import { useUser, queryClient } from '@lib/snapshot'
2876
+ import { Card, CardContent, CardHeader, CardTitle } from '@components/ui/card'
2877
+ import { Button } from '@components/ui/button'
2878
+
2879
+ export function OAuthCallbackPage() {
2880
+ useHead({ title: 'Signing in...' })
2881
+ const { error } = Route.useSearch()
2882
+ const navigate = useNavigate()
2883
+ const { user } = useUser()
2884
+
2885
+ useEffect(() => {
2886
+ if (!error) {
2887
+ queryClient.invalidateQueries({ queryKey: ['auth', 'me'] })
2888
+ }
2889
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2890
+ }, [])
2891
+
2892
+ useEffect(() => {
2893
+ if (user) {
2894
+ navigate({ to: '/' })
2895
+ }
2896
+ }, [user, navigate])
2897
+
2898
+ if (error) {
2899
+ return (
2900
+ <Card>
2901
+ <CardHeader>
2902
+ <CardTitle>Sign in failed</CardTitle>
2903
+ </CardHeader>
2904
+ <CardContent className="flex flex-col gap-4">
2905
+ <p className="text-destructive text-sm">{error}</p>
2906
+ <Button asChild variant="outline">
2907
+ <Link to="/auth/login">Back to sign in</Link>
2908
+ </Button>
2909
+ </CardContent>
2910
+ </Card>
2911
+ )
2912
+ }
2913
+
2914
+ return (
2915
+ <Card>
2916
+ <CardHeader>
2917
+ <CardTitle>Signing in...</CardTitle>
2918
+ </CardHeader>
2919
+ <CardContent>
2920
+ <p className="text-sm text-muted-foreground">Please wait while we complete your sign in.</p>
2921
+ </CardContent>
2922
+ </Card>
2923
+ )
2924
+ }
2925
+ `;
2926
+ }
2927
+
2928
+ // src/cli/templates/pages/home.ts
2929
+ function generateHomePageComponent() {
2930
+ return `import { useHead } from '@unhead/react'
2931
+
2932
+ export function HomePage() {
2933
+ useHead({ title: 'Home' })
2934
+ return (
2935
+ <div>
2936
+ <h1 className="text-2xl font-bold">Welcome</h1>
2937
+ </div>
2938
+ )
2939
+ }
2940
+ `;
2941
+ }
2942
+
2943
+ // src/cli/templates/pages/settings.ts
2944
+ function generateSettingsPageComponent() {
2945
+ return `import { useHead } from '@unhead/react'
2946
+ import { Link } from '@tanstack/react-router'
2947
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2948
+
2949
+ export function SettingsPage() {
2950
+ useHead({ title: 'Settings' })
2951
+
2952
+ const links = [
2953
+ { to: '/settings/password', label: 'Password', description: 'Change your password' },
2954
+ { to: '/settings/sessions', label: 'Sessions', description: 'Manage active sessions' },
2955
+ { to: '/settings/delete-account', label: 'Delete account', description: 'Permanently delete your account' },
2956
+ ]
2957
+
2958
+ return (
2959
+ <Card>
2960
+ <CardHeader>
2961
+ <CardTitle>Settings</CardTitle>
2962
+ <CardDescription>Manage your account settings</CardDescription>
2963
+ </CardHeader>
2964
+ <CardContent className="flex flex-col gap-3">
2965
+ {links.map((link) => (
2966
+ <Link
2967
+ key={link.to}
2968
+ to={link.to}
2969
+ className="flex flex-col gap-0.5 rounded-md border px-4 py-3 hover:bg-muted transition-colors"
2970
+ >
2971
+ <span className="text-sm font-medium">{link.label}</span>
2972
+ <span className="text-xs text-muted-foreground">{link.description}</span>
2973
+ </Link>
2974
+ ))}
2975
+ </CardContent>
2976
+ </Card>
2977
+ )
2978
+ }
2979
+ `;
2980
+ }
2981
+
2982
+ // src/cli/templates/pages/settings-password.ts
2983
+ function generateSettingsPasswordPageComponent() {
2984
+ return `import { useHead } from '@unhead/react'
2985
+ import { useSetPassword } from '@lib/snapshot'
2986
+ import { Button } from '@components/ui/button'
2987
+ import { Input } from '@components/ui/input'
2988
+ import { Label } from '@components/ui/label'
2989
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
2990
+
2991
+ export function SettingsPasswordPage() {
2992
+ useHead({ title: 'Change password' })
2993
+ const setPassword = useSetPassword()
2994
+
2995
+ function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
2996
+ e.preventDefault()
2997
+ const data = new FormData(e.currentTarget)
2998
+ setPassword.mutate({
2999
+ currentPassword: data.get('currentPassword') as string,
3000
+ newPassword: data.get('newPassword') as string,
3001
+ })
3002
+ }
3003
+
3004
+ return (
3005
+ <Card>
3006
+ <CardHeader>
3007
+ <CardTitle>Change password</CardTitle>
3008
+ <CardDescription>Update your account password</CardDescription>
3009
+ </CardHeader>
3010
+ <CardContent>
3011
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
3012
+ <div className="space-y-2">
3013
+ <Label htmlFor="currentPassword">Current password</Label>
3014
+ <Input id="currentPassword" name="currentPassword" type="password" required autoComplete="current-password" />
3015
+ </div>
3016
+ <div className="space-y-2">
3017
+ <Label htmlFor="newPassword">New password</Label>
3018
+ <Input id="newPassword" name="newPassword" type="password" required autoComplete="new-password" minLength={8} />
3019
+ </div>
3020
+ {setPassword.isError && (
3021
+ <p className="text-destructive text-sm">{setPassword.error.message}</p>
3022
+ )}
3023
+ {setPassword.isSuccess && (
3024
+ <p className="text-sm text-green-600">Password updated.</p>
3025
+ )}
3026
+ <Button type="submit" disabled={setPassword.isPending}>
3027
+ {setPassword.isPending ? 'Saving...' : 'Update password'}
3028
+ </Button>
3029
+ </form>
3030
+ </CardContent>
3031
+ </Card>
3032
+ )
3033
+ }
3034
+ `;
3035
+ }
3036
+
3037
+ // src/cli/templates/pages/settings-sessions.ts
3038
+ function generateSettingsSessionsPageComponent() {
3039
+ return `import { useHead } from '@unhead/react'
3040
+ import { useSessions, useRevokeSession } from '@lib/snapshot'
3041
+ import { Button } from '@components/ui/button'
3042
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
3043
+
3044
+ export function SettingsSessionsPage() {
3045
+ useHead({ title: 'Sessions' })
3046
+ const { sessions, isLoading } = useSessions()
3047
+ const revoke = useRevokeSession()
3048
+
3049
+ return (
3050
+ <Card>
3051
+ <CardHeader>
3052
+ <CardTitle>Active sessions</CardTitle>
3053
+ <CardDescription>Manage devices that are signed in to your account</CardDescription>
3054
+ </CardHeader>
3055
+ <CardContent className="flex flex-col gap-3">
3056
+ {isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
3057
+ {sessions?.map((session) => (
3058
+ <div
3059
+ key={session.id}
3060
+ className="flex items-center justify-between rounded-md border px-4 py-3"
3061
+ >
3062
+ <div>
3063
+ <p className="text-sm font-medium">{session.userAgent ?? 'Unknown device'}</p>
3064
+ <p className="text-xs text-muted-foreground">
3065
+ Last active {new Date(session.lastActiveAt).toLocaleDateString()}
3066
+ </p>
3067
+ </div>
3068
+ <Button
3069
+ variant="outline"
3070
+ size="sm"
3071
+ onClick={() => revoke.mutate(session.id)}
3072
+ disabled={revoke.isPending}
3073
+ >
3074
+ Revoke
3075
+ </Button>
3076
+ </div>
3077
+ ))}
3078
+ {sessions?.length === 0 && (
3079
+ <p className="text-sm text-muted-foreground">No active sessions found.</p>
3080
+ )}
3081
+ </CardContent>
3082
+ </Card>
3083
+ )
3084
+ }
3085
+ `;
3086
+ }
3087
+
3088
+ // src/cli/templates/pages/settings-delete-account.ts
3089
+ function generateSettingsDeleteAccountPageComponent() {
3090
+ return `import { useState } from 'react'
3091
+ import { useHead } from '@unhead/react'
3092
+ import { useDeleteAccount, useCancelDeletion, useUser } from '@lib/snapshot'
3093
+ import { Button } from '@components/ui/button'
3094
+ import { Input } from '@components/ui/input'
3095
+ import { Label } from '@components/ui/label'
3096
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
3097
+
3098
+ export function SettingsDeleteAccountPage() {
3099
+ useHead({ title: 'Delete account' })
3100
+ const { user } = useUser()
3101
+ const deleteAccount = useDeleteAccount()
3102
+ const cancelDeletion = useCancelDeletion()
3103
+ const [confirm, setConfirm] = useState('')
3104
+
3105
+ const pendingDeletion = user?.deletionScheduledAt != null
3106
+
3107
+ return (
3108
+ <Card>
3109
+ <CardHeader>
3110
+ <CardTitle>Delete account</CardTitle>
3111
+ <CardDescription>
3112
+ {pendingDeletion
3113
+ ? 'Your account is scheduled for deletion.'
3114
+ : 'Permanently delete your account and all associated data.'}
3115
+ </CardDescription>
3116
+ </CardHeader>
3117
+ <CardContent className="flex flex-col gap-4">
3118
+ {pendingDeletion ? (
3119
+ <>
3120
+ <p className="text-sm text-muted-foreground">
3121
+ Scheduled for deletion on{' '}
3122
+ {new Date(user!.deletionScheduledAt!).toLocaleDateString()}.
3123
+ </p>
3124
+ {cancelDeletion.isError && (
3125
+ <p className="text-destructive text-sm">{cancelDeletion.error.message}</p>
3126
+ )}
3127
+ <Button
3128
+ variant="outline"
3129
+ onClick={() => cancelDeletion.mutate()}
3130
+ disabled={cancelDeletion.isPending}
3131
+ >
3132
+ {cancelDeletion.isPending ? 'Cancelling...' : 'Cancel deletion'}
3133
+ </Button>
3134
+ </>
3135
+ ) : (
3136
+ <>
3137
+ <div className="space-y-2">
3138
+ <Label htmlFor="confirm">Type your email to confirm</Label>
3139
+ <Input
3140
+ id="confirm"
3141
+ value={confirm}
3142
+ onChange={(e) => setConfirm(e.target.value)}
3143
+ placeholder={user?.email ?? 'your@email.com'}
3144
+ autoComplete="off"
3145
+ />
3146
+ </div>
3147
+ {deleteAccount.isError && (
3148
+ <p className="text-destructive text-sm">{deleteAccount.error.message}</p>
3149
+ )}
3150
+ <Button
3151
+ variant="destructive"
3152
+ onClick={() => deleteAccount.mutate()}
3153
+ disabled={deleteAccount.isPending || confirm !== user?.email}
3154
+ >
3155
+ {deleteAccount.isPending ? 'Deleting...' : 'Delete account'}
3156
+ </Button>
3157
+ </>
3158
+ )}
3159
+ </CardContent>
3160
+ </Card>
3161
+ )
3162
+ }
3163
+ `;
3164
+ }
3165
+
3166
+ // src/cli/templates/pages/settings-email-otp.ts
3167
+ function generateSettingsEmailOtpPageComponent() {
3168
+ return `import { useState } from 'react'
3169
+ import { useHead } from '@unhead/react'
3170
+ import { useMfaEmailOtpEnable, useMfaEmailOtpVerifySetup, useMfaEmailOtpDisable, useMfaMethods } from '@lib/snapshot'
3171
+ import { Button } from '@components/ui/button'
3172
+ import { Input } from '@components/ui/input'
3173
+ import { Label } from '@components/ui/label'
3174
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@components/ui/card'
3175
+
3176
+ export function SettingsEmailOtpPage() {
3177
+ useHead({ title: 'Email OTP' })
3178
+ const { methods } = useMfaMethods()
3179
+ const enable = useMfaEmailOtpEnable()
3180
+ const verifySetup = useMfaEmailOtpVerifySetup()
3181
+ const disable = useMfaEmailOtpDisable()
3182
+ const [step, setStep] = useState<'idle' | 'verify'>('idle')
3183
+ const [disablePassword, setDisablePassword] = useState('')
3184
+
3185
+ const isEnabled = methods?.includes('emailOtp') ?? false
3186
+
3187
+ async function handleEnable() {
3188
+ await enable.mutateAsync()
3189
+ setStep('verify')
3190
+ }
3191
+
3192
+ function handleVerify(e: React.FormEvent<HTMLFormElement>) {
3193
+ e.preventDefault()
3194
+ const data = new FormData(e.currentTarget)
3195
+ verifySetup.mutate({ code: data.get('code') as string })
3196
+ }
3197
+
3198
+ return (
3199
+ <Card>
3200
+ <CardHeader>
3201
+ <CardTitle>Email OTP</CardTitle>
3202
+ <CardDescription>
3203
+ {isEnabled
3204
+ ? 'Email one-time passwords are enabled for your account.'
3205
+ : 'Receive one-time codes by email as a second factor.'}
3206
+ </CardDescription>
3207
+ </CardHeader>
3208
+ <CardContent className="flex flex-col gap-4">
3209
+ {!isEnabled && step === 'idle' && (
3210
+ <>
3211
+ {enable.isError && <p className="text-destructive text-sm">{enable.error.message}</p>}
3212
+ <Button onClick={handleEnable} disabled={enable.isPending}>
3213
+ {enable.isPending ? 'Sending...' : 'Enable email OTP'}
3214
+ </Button>
3215
+ </>
3216
+ )}
3217
+ {step === 'verify' && (
3218
+ <form onSubmit={handleVerify} className="flex flex-col gap-4">
3219
+ <p className="text-sm text-muted-foreground">A verification code was sent to your email. Enter it below to confirm setup.</p>
3220
+ <div className="space-y-2">
3221
+ <Label htmlFor="code">Verification code</Label>
3222
+ <Input id="code" name="code" type="text" inputMode="numeric" placeholder="000000" maxLength={6} required autoFocus />
3223
+ </div>
3224
+ {verifySetup.isError && <p className="text-destructive text-sm">{verifySetup.error.message}</p>}
3225
+ {verifySetup.isSuccess && <p className="text-sm text-green-600">Email OTP enabled.</p>}
3226
+ <Button type="submit" disabled={verifySetup.isPending}>
3227
+ {verifySetup.isPending ? 'Verifying...' : 'Verify'}
3228
+ </Button>
3229
+ </form>
3230
+ )}
3231
+ {isEnabled && (
3232
+ <>
3233
+ <div className="space-y-2">
3234
+ <Label htmlFor="disablePassword">Password</Label>
3235
+ <Input
3236
+ id="disablePassword"
3237
+ type="password"
3238
+ value={disablePassword}
3239
+ onChange={(e) => setDisablePassword(e.target.value)}
3240
+ placeholder="Enter your password to disable"
3241
+ autoComplete="current-password"
3242
+ />
3243
+ </div>
3244
+ {disable.isError && <p className="text-destructive text-sm">{disable.error.message}</p>}
3245
+ <Button
3246
+ variant="destructive"
3247
+ onClick={() => disable.mutate({ password: disablePassword })}
3248
+ disabled={disable.isPending || !disablePassword}
3249
+ >
3250
+ {disable.isPending ? 'Disabling...' : 'Disable email OTP'}
3251
+ </Button>
3252
+ </>
3253
+ )}
3254
+ </CardContent>
3255
+ </Card>
3256
+ )
3257
+ }
3258
+ `;
3259
+ }
3260
+
3261
+ // src/cli/templates/routes/settings.ts
3262
+ function generateSettingsIndexRoute() {
3263
+ return `import { createFileRoute } from '@tanstack/react-router'
3264
+ import { SettingsPage } from '@pages/settings/SettingsPage'
3265
+
3266
+ export const Route = createFileRoute('/_authenticated/settings/')({
3267
+ component: SettingsPage,
3268
+ })
3269
+ `;
3270
+ }
3271
+ function generateSettingsPasswordRoute() {
3272
+ return `import { createFileRoute } from '@tanstack/react-router'
3273
+ import { SettingsPasswordPage } from '@pages/settings/SettingsPasswordPage'
3274
+
3275
+ export const Route = createFileRoute('/_authenticated/settings/password')({
3276
+ component: SettingsPasswordPage,
3277
+ })
3278
+ `;
3279
+ }
3280
+ function generateSettingsSessionsRoute() {
3281
+ return `import { createFileRoute } from '@tanstack/react-router'
3282
+ import { SettingsSessionsPage } from '@pages/settings/SettingsSessionsPage'
3283
+
3284
+ export const Route = createFileRoute('/_authenticated/settings/sessions')({
3285
+ component: SettingsSessionsPage,
3286
+ })
3287
+ `;
3288
+ }
3289
+ function generateSettingsDeleteAccountRoute() {
3290
+ return `import { createFileRoute } from '@tanstack/react-router'
3291
+ import { SettingsDeleteAccountPage } from '@pages/settings/SettingsDeleteAccountPage'
3292
+
3293
+ export const Route = createFileRoute('/_authenticated/settings/delete-account')({
3294
+ component: SettingsDeleteAccountPage,
3295
+ })
3296
+ `;
3297
+ }
3298
+ function generateSettingsEmailOtpRoute() {
3299
+ return `import { createFileRoute } from '@tanstack/react-router'
3300
+ import { SettingsEmailOtpPage } from '@pages/settings/SettingsEmailOtpPage'
3301
+
3302
+ export const Route = createFileRoute('/_authenticated/settings/email-otp')({
3303
+ component: SettingsEmailOtpPage,
3304
+ })
3305
+ `;
3306
+ }
3307
+
3308
+ // src/cli/templates/layouts/shared.ts
3309
+ function generateAuthLayout() {
3310
+ return `export function AuthLayout({ children }: { children: React.ReactNode }) {
3311
+ return (
3312
+ <div className="min-h-screen bg-background flex items-center justify-center p-4">
3313
+ <div className="w-full max-w-sm">
3314
+ {children}
3315
+ </div>
3316
+ </div>
3317
+ )
3318
+ }
3319
+ `;
3320
+ }
3321
+ function generatePendingComponent() {
3322
+ return `export function PendingComponent() {
3323
+ return (
3324
+ <div className="flex items-center justify-center min-h-[200px]">
3325
+ <div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
3326
+ </div>
3327
+ )
3328
+ }
3329
+ `;
3330
+ }
3331
+ function generateErrorComponent() {
3332
+ return `import { useRouter } from '@tanstack/react-router'
3333
+
3334
+ export function ErrorComponent({ error }: { error: Error }) {
3335
+ const router = useRouter()
3336
+ return (
3337
+ <div className="flex flex-col items-center justify-center min-h-[200px] gap-4">
3338
+ <p className="text-destructive">{error.message}</p>
3339
+ <button
3340
+ onClick={() => router.invalidate()}
3341
+ className="text-sm underline"
3342
+ >
3343
+ Try again
3344
+ </button>
3345
+ </div>
3346
+ )
3347
+ }
3348
+ `;
3349
+ }
3350
+ function generateNotFoundComponent() {
3351
+ return `import { Link } from '@tanstack/react-router'
3352
+
3353
+ export function NotFoundComponent() {
3354
+ return (
3355
+ <div className="flex flex-col items-center justify-center min-h-[200px] gap-4">
3356
+ <h1 className="text-2xl font-semibold">404</h1>
3357
+ <p className="text-muted-foreground">Page not found</p>
3358
+ <Link to="/" className="text-sm underline">Go home</Link>
3359
+ </div>
3360
+ )
3361
+ }
3362
+ `;
3363
+ }
3364
+ function generatePageTransition() {
3365
+ return `import { Outlet, useRouterState } from '@tanstack/react-router'
3366
+
3367
+ export function PageTransition() {
3368
+ const isPending = useRouterState({ select: (s) => s.status === 'pending' })
3369
+ return (
3370
+ <div className={\`transition-opacity duration-150 \${isPending ? 'opacity-0' : 'opacity-100'}\`}>
3371
+ <Outlet />
3372
+ </div>
3373
+ )
3374
+ }
3375
+ `;
3376
+ }
3377
+
3378
+ // src/cli/templates/layouts/minimal.ts
3379
+ function generateRootLayoutMinimal() {
3380
+ return `export function RootLayout({ children }: { children: React.ReactNode }) {
3381
+ return (
3382
+ <div className="min-h-screen bg-background text-foreground">
3383
+ {children}
3384
+ </div>
3385
+ )
3386
+ }
3387
+ `;
3388
+ }
3389
+
3390
+ // src/cli/templates/layouts/top-nav.ts
3391
+ function generateRootLayoutTopNav() {
3392
+ return `import { TopNav } from './TopNav'
3393
+
3394
+ export function RootLayout({ children }: { children: React.ReactNode }) {
3395
+ return (
3396
+ <div className="min-h-screen bg-background text-foreground flex flex-col">
3397
+ <TopNav />
3398
+ <main className="flex-1 container mx-auto px-4 py-6">
3399
+ {children}
3400
+ </main>
3401
+ </div>
3402
+ )
3403
+ }
3404
+ `;
3405
+ }
3406
+ function generateTopNav(config) {
3407
+ return `import { Link } from '@tanstack/react-router'
3408
+ import { useTheme, useUser, useLogout } from '@lib/snapshot'
3409
+
3410
+ export function TopNav() {
3411
+ const { user } = useUser()
3412
+ const logout = useLogout()
3413
+ const { theme, toggle } = useTheme()
3414
+
3415
+ return (
3416
+ <header className="border-b border-border bg-background">
3417
+ <div className="container mx-auto px-4 h-14 flex items-center justify-between">
3418
+ <Link to="/" className="font-semibold text-foreground">
3419
+ ${config.projectName}
3420
+ </Link>
3421
+ <nav className="flex items-center gap-4">
3422
+ <button
3423
+ onClick={toggle}
3424
+ className="text-sm text-muted-foreground hover:text-foreground"
3425
+ >
3426
+ {theme === 'dark' ? 'Light' : 'Dark'}
3427
+ </button>
3428
+ {user ? (
3429
+ <button
3430
+ onClick={() => logout.mutate()}
3431
+ className="text-sm text-muted-foreground hover:text-foreground"
3432
+ >
3433
+ Sign out
3434
+ </button>
3435
+ ) : (
3436
+ <Link
3437
+ to="/auth/login"
3438
+ className="text-sm text-muted-foreground hover:text-foreground"
3439
+ >
3440
+ Sign in
3441
+ </Link>
3442
+ )}
3443
+ </nav>
3444
+ </div>
3445
+ </header>
3446
+ )
3447
+ }
3448
+ `;
3449
+ }
3450
+
3451
+ // src/cli/templates/layouts/sidebar.ts
3452
+ function generateRootLayoutSidebar() {
3453
+ return `import { useAtom } from 'jotai'
3454
+ import { sidebarOpenAtom } from '@store/ui'
3455
+ import { Sidebar } from './Sidebar'
3456
+ import { TopBar } from './TopBar'
3457
+
3458
+ export function RootLayout({ children }: { children: React.ReactNode }) {
3459
+ const [sidebarOpen, setSidebarOpen] = useAtom(sidebarOpenAtom)
3460
+
3461
+ return (
3462
+ <div className="min-h-screen bg-background text-foreground">
3463
+ <Sidebar />
3464
+ {/* Click-outside overlay for mobile */}
3465
+ {sidebarOpen && (
3466
+ <div
3467
+ className="fixed inset-0 z-40 bg-black/50 md:hidden"
3468
+ onClick={() => setSidebarOpen(false)}
3469
+ aria-hidden="true"
3470
+ />
3471
+ )}
3472
+ <div className="md:pl-64 flex flex-col min-h-screen">
3473
+ <TopBar />
3474
+ <main className="flex-1 p-6">
3475
+ {children}
3476
+ </main>
3477
+ </div>
3478
+ </div>
3479
+ )
3480
+ }
3481
+ `;
3482
+ }
3483
+ function generateSidebar() {
3484
+ return `import { useAtom } from 'jotai'
3485
+ import { sidebarOpenAtom } from '@store/ui'
3486
+ import { SidebarHeader } from './SidebarHeader'
3487
+ import { SidebarNav } from './SidebarNav'
3488
+
3489
+ export function Sidebar() {
3490
+ const [open] = useAtom(sidebarOpenAtom)
3491
+
3492
+ return (
3493
+ <aside
3494
+ className={[
3495
+ 'fixed inset-y-0 left-0 z-50 w-64 bg-background border-r border-border',
3496
+ 'flex flex-col transform transition-transform duration-200',
3497
+ 'md:translate-x-0',
3498
+ open ? 'translate-x-0' : '-translate-x-full',
3499
+ ].join(' ')}
3500
+ >
3501
+ <SidebarHeader />
3502
+ <SidebarNav />
3503
+ </aside>
3504
+ )
3505
+ }
3506
+ `;
3507
+ }
3508
+ function generateSidebarHeader(config) {
3509
+ return `import { Link } from '@tanstack/react-router'
3510
+
3511
+ export function SidebarHeader() {
3512
+ return (
3513
+ <div className="h-14 flex items-center px-4 border-b border-border shrink-0">
3514
+ <Link to="/" className="font-semibold text-foreground">
3515
+ ${config.projectName}
3516
+ </Link>
3517
+ </div>
3518
+ )
3519
+ }
3520
+ `;
3521
+ }
3522
+ function generateSidebarNav() {
3523
+ return `import { Link } from '@tanstack/react-router'
3524
+
3525
+ const navItems = [
3526
+ { to: '/', label: 'Dashboard' },
3527
+ { to: '/settings', label: 'Settings' },
3528
+ ] as const
3529
+
3530
+ export function SidebarNav() {
3531
+ return (
3532
+ <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
3533
+ {navItems.map((item) => (
3534
+ <Link
3535
+ key={item.to}
3536
+ to={item.to}
3537
+ className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
3538
+ activeProps={{ className: 'text-foreground bg-muted' }}
3539
+ >
3540
+ {item.label}
3541
+ </Link>
3542
+ ))}
3543
+ </nav>
3544
+ )
3545
+ }
3546
+ `;
3547
+ }
3548
+ function generateTopBar() {
3549
+ return `import { useAtom } from 'jotai'
3550
+ import { sidebarOpenAtom } from '@store/ui'
3551
+ import { useTheme, useUser, useLogout } from '@lib/snapshot'
3552
+
3553
+ export function TopBar() {
3554
+ const [, setSidebarOpen] = useAtom(sidebarOpenAtom)
3555
+ const { user } = useUser()
3556
+ const logout = useLogout()
3557
+ const { theme, toggle } = useTheme()
3558
+
3559
+ return (
3560
+ <header className="h-14 border-b border-border bg-background flex items-center gap-4 px-4 shrink-0">
3561
+ <button
3562
+ onClick={() => setSidebarOpen((o) => !o)}
3563
+ className="md:hidden text-muted-foreground hover:text-foreground"
3564
+ aria-label="Toggle sidebar"
3565
+ >
3566
+ \u2630
3567
+ </button>
3568
+ <div className="flex-1" />
3569
+ <button
3570
+ onClick={toggle}
3571
+ className="text-sm text-muted-foreground hover:text-foreground"
3572
+ >
3573
+ {theme === 'dark' ? 'Light' : 'Dark'}
3574
+ </button>
3575
+ {user && (
3576
+ <button
3577
+ onClick={() => logout.mutate()}
3578
+ className="text-sm text-muted-foreground hover:text-foreground"
3579
+ >
3580
+ Sign out
3581
+ </button>
3582
+ )}
3583
+ </header>
3584
+ )
3585
+ }
3586
+ `;
3587
+ }
3588
+
3589
+ // src/cli/scaffold.ts
3590
+ var GITIGNORE_CONTENT = `node_modules/
3591
+ dist/
3592
+ .env
3593
+ .env.local
3594
+ *.local
3595
+ routeTree.gen.ts
3596
+ `;
3597
+ async function scaffold(config) {
3598
+ async function write(relPath, content) {
3599
+ const abs = path3.join(config.dir, relPath);
3600
+ await fs.mkdir(path3.dirname(abs), { recursive: true });
3601
+ await fs.writeFile(abs, content, "utf8");
3602
+ }
3603
+ await fs.mkdir(config.dir, { recursive: true });
3604
+ await write("package.json", generatePackageJson(config));
3605
+ await write("tsconfig.json", generateTsConfigRoot());
3606
+ await write("tsconfig.app.json", generateTsConfigApp());
3607
+ await write("tsconfig.node.json", generateTsConfigNode());
3608
+ await write("vite.config.ts", generateViteConfig());
3609
+ await write("components.json", generateComponentsJson(config));
3610
+ await write("snapshot.config.json", generateSnapshotConfig());
3611
+ await write(".env", generateEnv(config));
3612
+ await write(".gitignore", GITIGNORE_CONTENT);
3613
+ await write("index.html", generateIndexHtml(config));
3614
+ await write("public/vite.svg", '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#646cff"/><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="18" fill="white">S</text></svg>');
3615
+ await write("src/lib/snapshot.ts", generateSnapshotLib(config));
3616
+ await write("src/lib/router.ts", generateRouterLib());
3617
+ const mainContent = generateMain(config);
3618
+ if (config.securityProfile === "prototype") {
3619
+ const guard = `// Prototype mode deployment guard
3620
+ if (
3621
+ typeof window !== 'undefined' &&
3622
+ window.location.hostname !== 'localhost' &&
3623
+ window.location.hostname !== '127.0.0.1' &&
3624
+ !import.meta.env.VITE_ALLOW_PROTOTYPE_DEPLOYMENT
3625
+ ) {
3626
+ throw new Error(
3627
+ '[snapshot] This app was scaffolded in prototype mode and is running on a non-localhost origin. ' +
3628
+ 'Set VITE_ALLOW_PROTOTYPE_DEPLOYMENT=true to override (not recommended for production).'
3629
+ )
3630
+ }
3631
+
3632
+ `;
3633
+ await write("src/main.tsx", guard + mainContent);
3634
+ } else {
3635
+ await write("src/main.tsx", mainContent);
3636
+ }
3637
+ await write("src/routes/__root.tsx", generateRootRoute(config));
3638
+ await write("src/routes/_authenticated/index.tsx", generateIndexRoute());
3639
+ await write("src/pages/HomePage.tsx", generateHomePageComponent());
3640
+ await write("src/routes/_authenticated.tsx", generateAuthenticatedRoute());
3641
+ await write("src/routes/_guest.tsx", generateGuestRoute());
3642
+ if (config.authPages) {
3643
+ await write("src/routes/_guest/auth/login.tsx", generateLoginPage(config));
3644
+ await write("src/routes/_guest/auth/register.tsx", generateRegisterPage(config));
3645
+ await write("src/routes/_guest/auth/forgot-password.tsx", generateForgotPasswordPage());
3646
+ await write("src/routes/_guest/auth/reset-password.tsx", generateResetPasswordPage());
3647
+ await write("src/routes/_guest/auth/verify-email.tsx", generateVerifyEmailPage());
3648
+ await write("src/routes/_guest/auth/oauth/callback.tsx", generateOAuthCallbackPage(config));
3649
+ await write("src/pages/auth/LoginPage.tsx", generateLoginPageComponent(config));
3650
+ await write("src/pages/auth/RegisterPage.tsx", generateRegisterPageComponent(config));
3651
+ await write("src/pages/auth/ForgotPasswordPage.tsx", generateForgotPasswordPageComponent());
3652
+ await write("src/pages/auth/ResetPasswordPage.tsx", generateResetPasswordPageComponent());
3653
+ await write("src/pages/auth/VerifyEmailPage.tsx", generateVerifyEmailPageComponent());
3654
+ await write("src/pages/auth/OAuthCallbackPage.tsx", generateOAuthCallbackPageComponent(config));
3655
+ if (config.mfaPages) {
3656
+ await write("src/routes/_guest/auth/mfa-verify.tsx", generateMfaVerifyPage(config));
3657
+ await write("src/routes/_authenticated/mfa-setup.tsx", generateMfaSetupPage(config));
3658
+ await write("src/pages/auth/MfaVerifyPage.tsx", generateMfaVerifyPageComponent(config));
3659
+ await write("src/pages/auth/MfaSetupPage.tsx", generateMfaSetupPageComponent(config));
3660
+ await write("src/routes/_authenticated/settings/email-otp.tsx", generateSettingsEmailOtpRoute());
3661
+ await write("src/pages/settings/SettingsEmailOtpPage.tsx", generateSettingsEmailOtpPageComponent());
3662
+ }
3663
+ if (config.passkeyPages) {
3664
+ await write("src/routes/_authenticated/passkey.tsx", generatePasskeyManagePage());
3665
+ await write("src/pages/auth/PasskeyManagePage.tsx", generatePasskeyManagePageComponent());
3666
+ }
3667
+ await write("src/routes/_authenticated/settings/index.tsx", generateSettingsIndexRoute());
3668
+ await write("src/routes/_authenticated/settings/password.tsx", generateSettingsPasswordRoute());
3669
+ await write("src/routes/_authenticated/settings/sessions.tsx", generateSettingsSessionsRoute());
3670
+ await write("src/routes/_authenticated/settings/delete-account.tsx", generateSettingsDeleteAccountRoute());
3671
+ await write("src/pages/settings/SettingsPage.tsx", generateSettingsPageComponent());
3672
+ await write("src/pages/settings/SettingsPasswordPage.tsx", generateSettingsPasswordPageComponent());
3673
+ await write("src/pages/settings/SettingsSessionsPage.tsx", generateSettingsSessionsPageComponent());
3674
+ await write("src/pages/settings/SettingsDeleteAccountPage.tsx", generateSettingsDeleteAccountPageComponent());
3675
+ }
3676
+ if (config.layout === "minimal") {
3677
+ await write("src/components/layout/RootLayout.tsx", generateRootLayoutMinimal());
3678
+ } else if (config.layout === "top-nav") {
3679
+ await write("src/components/layout/RootLayout.tsx", generateRootLayoutTopNav());
3680
+ await write("src/components/layout/TopNav.tsx", generateTopNav(config));
3681
+ } else {
3682
+ await write("src/components/layout/RootLayout.tsx", generateRootLayoutSidebar());
3683
+ await write("src/components/layout/Sidebar.tsx", generateSidebar());
3684
+ await write("src/components/layout/SidebarHeader.tsx", generateSidebarHeader(config));
3685
+ await write("src/components/layout/SidebarNav.tsx", generateSidebarNav());
3686
+ await write("src/components/layout/TopBar.tsx", generateTopBar());
3687
+ }
3688
+ await write("src/components/layout/AuthLayout.tsx", generateAuthLayout());
3689
+ await write("src/components/layout/PendingComponent.tsx", generatePendingComponent());
3690
+ await write("src/components/layout/ErrorComponent.tsx", generateErrorComponent());
3691
+ await write("src/components/layout/NotFoundComponent.tsx", generateNotFoundComponent());
3692
+ await write("src/components/layout/PageTransition.tsx", generatePageTransition());
3693
+ await write("src/store/ui.ts", generateStoreUi(config));
3694
+ await write("src/types/api.ts", generateTypesApi());
3695
+ await write("src/lib/utils.ts", UTILS_CONTENT);
3696
+ await write("src/components/shared/.gitkeep", "");
3697
+ await write("src/hooks/.gitkeep", "");
3698
+ await write("src/hooks/api/.gitkeep", "");
3699
+ await write("src/api/.gitkeep", "");
3700
+ const s = be();
3701
+ s.start("Installing dependencies");
3702
+ exec("bun install", config.dir);
3703
+ s.stop("Dependencies installed");
3704
+ if (config.components.length > 0) {
3705
+ s.start("Running shadcn init");
3706
+ try {
3707
+ exec("bunx shadcn@latest init --yes", config.dir, true);
3708
+ s.stop("shadcn initialized");
3709
+ } catch {
3710
+ s.stop("shadcn init failed \u2014 continuing");
3711
+ R2.warn("shadcn init failed. You can run it manually: bunx shadcn@latest init");
3712
+ }
3713
+ s.start("Adding shadcn components");
3714
+ try {
3715
+ exec(
3716
+ `bunx shadcn@latest add ${config.components.join(" ")} --yes`,
3717
+ config.dir
3718
+ );
3719
+ s.stop("shadcn components added");
3720
+ const uiDir = path3.join(config.dir, "src/components/ui");
3721
+ const uiFiles = await fs.readdir(uiDir).catch(() => []);
3722
+ await Promise.all(
3723
+ uiFiles.filter((f) => f.endsWith(".tsx")).map(async (f) => {
3724
+ const filePath = path3.join(uiDir, f);
3725
+ const content = await fs.readFile(filePath, "utf8");
3726
+ if (content.includes("ElementRef")) {
3727
+ await fs.writeFile(filePath, content.replaceAll("ElementRef", "ComponentRef"), "utf8");
3728
+ }
3729
+ })
3730
+ );
3731
+ } catch {
3732
+ s.stop("Some components may not have been added");
3733
+ R2.warn(
3734
+ "shadcn add encountered an error. Run `bunx shadcn@latest add <component>` manually for any missing ones."
3735
+ );
3736
+ }
3737
+ }
3738
+ await write("src/styles/globals.css", generateGlobalsCss(config));
3739
+ if (config.gitInit) {
3740
+ s.start("Initialising git repository");
3741
+ try {
3742
+ exec(
3743
+ 'git init && git add -A && git commit -m "chore: initial scaffold from @lastshotlabs/snapshot"',
3744
+ config.dir,
3745
+ true
3746
+ );
3747
+ s.stop("Git repository initialised");
3748
+ } catch (err) {
3749
+ s.stop("Git init failed");
3750
+ const stderr = err?.stderr;
3751
+ R2.warn(`Git init failed: ${stderr?.toString() ?? String(err)}`);
3752
+ }
3753
+ }
3754
+ }
3755
+
3756
+ // src/cli/sync.ts
3757
+ import fs2 from "fs/promises";
3758
+ import path4 from "path";
3759
+ import { createHash } from "crypto";
3760
+ var clackLogger = {
3761
+ info: (msg) => R2.info(msg),
3762
+ success: (msg) => R2.success(msg),
3763
+ warn: (msg) => R2.warn(msg),
3764
+ error: (msg) => R2.error(msg)
3765
+ };
3766
+ async function readSyncConfig(cwd) {
3767
+ try {
3768
+ const content = await fs2.readFile(path4.join(cwd, "snapshot.config.json"), "utf8");
3769
+ return JSON.parse(content);
3770
+ } catch {
3771
+ }
3772
+ try {
3773
+ const content = await fs2.readFile(path4.join(cwd, "package.json"), "utf8");
3774
+ const pkg = JSON.parse(content);
3775
+ return pkg.snapshot ?? {};
3776
+ } catch {
3777
+ }
3778
+ return {};
3779
+ }
3780
+ function resolveBackendDirs(cwd, backend, globalOpts, isMulti) {
3781
+ let defaultApiDir;
3782
+ let defaultHooksDir;
3783
+ let defaultTypesPath;
3784
+ if (isMulti && backend.name) {
3785
+ const slug = backend.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
3786
+ defaultApiDir = `src/api/${slug}`;
3787
+ defaultHooksDir = `src/hooks/${slug}`;
3788
+ defaultTypesPath = `src/types/${slug}-api.ts`;
3789
+ } else {
3790
+ defaultApiDir = "src/api";
3791
+ defaultHooksDir = "src/hooks/api";
3792
+ defaultTypesPath = "src/types/api.ts";
3793
+ }
3794
+ return {
3795
+ apiDir: path4.resolve(cwd, backend.apiDir ?? globalOpts.apiDir ?? defaultApiDir),
3796
+ hooksDir: path4.resolve(cwd, backend.hooksDir ?? globalOpts.hooksDir ?? defaultHooksDir),
3797
+ typesFile: path4.resolve(cwd, backend.typesPath ?? globalOpts.typesPath ?? defaultTypesPath),
3798
+ snapshotImport: backend.snapshotImport ?? globalOpts.snapshotImport ?? "@lib/snapshot"
3799
+ };
3800
+ }
3801
+ function toRelativeImport(fromDir, toPath) {
3802
+ let rel = path4.relative(fromDir, toPath).replace(/\\/g, "/");
3803
+ if (!rel.startsWith(".")) rel = "./" + rel;
3804
+ return rel.replace(/\.ts$/, "");
3805
+ }
3806
+ function schemaHash(schema) {
3807
+ return createHash("sha1").update(JSON.stringify(schema)).digest("hex");
3808
+ }
3809
+ function refName(ref) {
3810
+ return ref.replace("#/components/schemas/", "");
3811
+ }
3812
+ function collectRefs(schema) {
3813
+ const refs = /* @__PURE__ */ new Set();
3814
+ if (schema.$ref) {
3815
+ refs.add(refName(schema.$ref));
3816
+ return refs;
3817
+ }
3818
+ if (schema.items) collectRefs(schema.items).forEach((r) => refs.add(r));
3819
+ if (schema.properties) {
3820
+ for (const prop of Object.values(schema.properties)) {
3821
+ collectRefs(prop).forEach((r) => refs.add(r));
3822
+ }
3823
+ }
3824
+ for (const arr of [schema.oneOf, schema.anyOf, schema.allOf]) {
3825
+ if (arr) for (const s of arr) collectRefs(s).forEach((r) => refs.add(r));
3826
+ }
3827
+ return refs;
3828
+ }
3829
+ function schemaToTs(schema, depth = 0) {
3830
+ if (schema.$ref) return refName(schema.$ref);
3831
+ if (schema.enum) {
3832
+ return schema.enum.map((v) => v === null ? "null" : typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
3833
+ }
3834
+ if (schema.allOf) {
3835
+ const parts = schema.allOf.map((s) => schemaToTs(s, depth));
3836
+ return parts.length === 1 ? parts[0] : parts.join(" & ");
3837
+ }
3838
+ if (schema.oneOf || schema.anyOf) {
3839
+ return (schema.oneOf ?? schema.anyOf).map((s) => schemaToTs(s, depth)).join(" | ");
3840
+ }
3841
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
3842
+ const isNullable = schema.nullable === true || types.includes("null");
3843
+ const baseTypes = types.filter((t2) => t2 !== "null");
3844
+ let base;
3845
+ if (baseTypes.includes("array") || schema.items) {
3846
+ const item = schema.items ? schemaToTs(schema.items, depth) : "unknown";
3847
+ base = item.includes(" | ") ? `(${item})[]` : `${item}[]`;
3848
+ } else if (baseTypes.includes("object") || schema.properties || schema.additionalProperties) {
3849
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
3850
+ const ind = " ".repeat(depth + 1);
3851
+ const close = " ".repeat(depth);
3852
+ const lines = Object.entries(schema.properties).map(([key, val]) => {
3853
+ const opt = schema.required?.includes(key) ? "" : "?";
3854
+ const valNull = val.nullable ? " | null" : "";
3855
+ const safeProp = /[^a-zA-Z0-9_$]/.test(key) ? `'${key}'` : key;
3856
+ return `${ind}${safeProp}${opt}: ${schemaToTs(val, depth + 1)}${valNull}`;
3857
+ });
3858
+ base = `{
3859
+ ${lines.join("\n")}
3860
+ ${close}}`;
3861
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
3862
+ base = `Record<string, ${schemaToTs(schema.additionalProperties, depth)}>`;
3863
+ } else {
3864
+ base = "Record<string, unknown>";
3865
+ }
3866
+ } else if (baseTypes.includes("string")) {
3867
+ base = "string";
3868
+ } else if (baseTypes.includes("integer") || baseTypes.includes("number")) {
3869
+ base = "number";
3870
+ } else if (baseTypes.includes("boolean")) {
3871
+ base = "boolean";
3872
+ } else {
3873
+ base = "unknown";
3874
+ }
3875
+ return isNullable ? `${base} | null` : base;
3876
+ }
3877
+ function schemaToZod(schema, componentSchemas, visiting = /* @__PURE__ */ new Set()) {
3878
+ if (schema.$ref) {
3879
+ const name = refName(schema.$ref);
3880
+ if (visiting.has(name)) return "z.unknown()";
3881
+ const resolved = componentSchemas[name];
3882
+ return resolved ? schemaToZod(resolved, componentSchemas, /* @__PURE__ */ new Set([...visiting, name])) : "z.unknown()";
3883
+ }
3884
+ if (schema.enum) {
3885
+ const allStrings = schema.enum.every((v) => typeof v === "string");
3886
+ if (allStrings && schema.enum.length > 0) {
3887
+ return `z.enum([${schema.enum.map((v) => `'${v}'`).join(", ")}])`;
3888
+ }
3889
+ const literals = schema.enum.map(
3890
+ (v) => v === null ? "z.null()" : `z.literal(${typeof v === "string" ? `'${v}'` : String(v)})`
3891
+ );
3892
+ return `z.union([${literals.join(", ")}])`;
3893
+ }
3894
+ if (schema.allOf) {
3895
+ if (schema.allOf.length === 1) return schemaToZod(schema.allOf[0], componentSchemas, visiting);
3896
+ const parts = schema.allOf.map((s) => schemaToZod(s, componentSchemas, visiting));
3897
+ return parts.reduce((acc, cur) => `z.intersection(${acc}, ${cur})`);
3898
+ }
3899
+ if (schema.oneOf || schema.anyOf) {
3900
+ const arr = schema.oneOf ?? schema.anyOf;
3901
+ return `z.union([${arr.map((s) => schemaToZod(s, componentSchemas, visiting)).join(", ")}])`;
3902
+ }
3903
+ const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
3904
+ const isNullable = schema.nullable === true || types.includes("null");
3905
+ const baseTypes = types.filter((t2) => t2 !== "null");
3906
+ let base;
3907
+ if (baseTypes.includes("array") || schema.items) {
3908
+ const item = schema.items ? schemaToZod(schema.items, componentSchemas, visiting) : "z.unknown()";
3909
+ base = `z.array(${item})`;
3910
+ } else if (baseTypes.includes("object") || schema.properties || schema.additionalProperties) {
3911
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
3912
+ const props = Object.entries(schema.properties).map(([key, val]) => {
3913
+ const zodVal = schemaToZod(val, componentSchemas, visiting);
3914
+ const nullable = val.nullable ? ".nullable()" : "";
3915
+ const opt = schema.required?.includes(key) ? "" : ".optional()";
3916
+ const safeProp = /[^a-zA-Z0-9_$]/.test(key) ? `'${key}'` : key;
3917
+ return ` ${safeProp}: ${zodVal}${nullable}${opt},`;
3918
+ });
3919
+ base = `z.object({
3920
+ ${props.join("\n")}
3921
+ })`;
3922
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
3923
+ base = `z.record(z.string(), ${schemaToZod(schema.additionalProperties, componentSchemas, visiting)})`;
3924
+ } else {
3925
+ base = "z.record(z.string(), z.unknown())";
3926
+ }
3927
+ } else if (baseTypes.includes("string")) {
3928
+ base = "z.string()";
3929
+ } else if (baseTypes.includes("integer") || baseTypes.includes("number")) {
3930
+ base = "z.number()";
3931
+ } else if (baseTypes.includes("boolean")) {
3932
+ base = "z.boolean()";
3933
+ } else {
3934
+ base = "z.unknown()";
3935
+ }
3936
+ return isNullable ? `${base}.nullable()` : base;
3937
+ }
3938
+ function generateTypesContent(schemas, hasPaginated) {
3939
+ const lines = [
3940
+ "// Generated by bunx snapshot sync. Do not edit manually.",
3941
+ ""
3942
+ ];
3943
+ if (hasPaginated) {
3944
+ lines.push(`export interface PaginatedResponse<T> {`);
3945
+ lines.push(` data: T[]`);
3946
+ lines.push(` total: number`);
3947
+ lines.push(` page: number`);
3948
+ lines.push(` perPage: number`);
3949
+ lines.push(`}`);
3950
+ lines.push("");
3951
+ }
3952
+ for (const [name, schema] of Object.entries(schemas)) {
3953
+ if (schema.description) lines.push(`/** ${schema.description} */`);
3954
+ const isObject = schema.type === "object" || !!schema.properties || Array.isArray(schema.type) && schema.type.includes("object");
3955
+ const isComplex = schema.enum || schema.oneOf || schema.anyOf || schema.allOf || schema.$ref;
3956
+ if (isObject && !isComplex) {
3957
+ const ind = " ";
3958
+ const props = schema.properties ? Object.entries(schema.properties).map(([key, val]) => {
3959
+ const opt = schema.required?.includes(key) ? "" : "?";
3960
+ const valNull = val.nullable ? " | null" : "";
3961
+ const comment = val.description ? ` /** ${val.description} */
3962
+ ` : "";
3963
+ const safeProp = /[^a-zA-Z0-9_$]/.test(key) ? `'${key}'` : key;
3964
+ return `${comment}${ind}${safeProp}${opt}: ${schemaToTs(val, 1)}${valNull}`;
3965
+ }) : [];
3966
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
3967
+ lines.push(`export interface ${name} extends Record<string, unknown> {`);
3968
+ } else {
3969
+ lines.push(`export interface ${name} {`);
3970
+ }
3971
+ lines.push(...props);
3972
+ lines.push("}");
3973
+ } else {
3974
+ lines.push(`export type ${name} = ${schemaToTs(schema)}`);
3975
+ }
3976
+ lines.push("");
3977
+ }
3978
+ return lines.join("\n");
3979
+ }
3980
+ function toCamelCase(str) {
3981
+ return str.replace(/[-_]([a-zA-Z0-9])/g, (_2, c) => c.toUpperCase());
3982
+ }
3983
+ function plainFnName(method, pathStr, operationId) {
3984
+ if (operationId) return toCamelCase(operationId);
3985
+ const segs = pathStr.split("/").filter(Boolean).map((seg) => {
3986
+ if (seg.startsWith("{") && seg.endsWith("}")) {
3987
+ const n = seg.slice(1, -1);
3988
+ return "By" + n.charAt(0).toUpperCase() + n.slice(1);
3989
+ }
3990
+ const camel = toCamelCase(seg);
3991
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
3992
+ });
3993
+ return method + segs.join("");
3994
+ }
3995
+ function hookName(method, fnName) {
3996
+ const isQuery = method === "get";
3997
+ const pascal = fnName.charAt(0).toUpperCase() + fnName.slice(1);
3998
+ if (pascal.endsWith("Query") || pascal.endsWith("Mutation")) return `use${pascal}`;
3999
+ return `use${pascal}${isQuery ? "Query" : "Mutation"}`;
4000
+ }
4001
+ function successSchema(responses) {
4002
+ for (const code of ["200", "201", "202"]) {
4003
+ const r = responses[code];
4004
+ if (r?.content?.["application/json"]?.schema) return r.content["application/json"].schema;
4005
+ }
4006
+ return null;
4007
+ }
4008
+ function requestBodySchema(rb) {
4009
+ if (!rb) return null;
4010
+ return rb.content?.["application/json"]?.schema ?? null;
4011
+ }
4012
+ function nonJsonBodyContentType(rb) {
4013
+ if (!rb?.content) return null;
4014
+ if ("application/json" in rb.content) return null;
4015
+ return Object.keys(rb.content)[0] ?? null;
4016
+ }
4017
+ function rawPathTemplate(pathStr, pathParams) {
4018
+ if (pathParams.length === 0) return `'${pathStr}'`;
4019
+ const tpl = pathStr.replace(/\{([^}]+)\}/g, (_2, n) => `\${${n}}`);
4020
+ return `\`${tpl}\``;
4021
+ }
4022
+ function paginatedUrlTemplate(pathStr, pathParams) {
4023
+ if (pathParams.length === 0) {
4024
+ return `\`${pathStr}?page=\${page}&perPage=\${perPage}\``;
4025
+ }
4026
+ const tpl = pathStr.replace(/\{([^}]+)\}/g, (_2, n) => `\${${n}}`);
4027
+ return `\`${tpl}?page=\${page}&perPage=\${perPage}\``;
4028
+ }
4029
+ function queryKey(pathStr, queryParams = []) {
4030
+ const parts = pathStr.split("/").filter(Boolean).map(
4031
+ (seg) => seg.startsWith("{") && seg.endsWith("}") ? `params.${seg.slice(1, -1)}` : `'${seg}'`
4032
+ );
4033
+ for (const p of queryParams) {
4034
+ parts.push(`params.${p.name}`);
4035
+ }
4036
+ return `[${parts.join(", ")}]`;
4037
+ }
4038
+ function paramType(p) {
4039
+ return schemaToTs(p.schema ?? { type: "string" });
4040
+ }
4041
+ function isPaginatedSchema(schema) {
4042
+ if (!schema.properties) return null;
4043
+ const { data, total } = schema.properties;
4044
+ if (!data || !data.items && data.type !== "array") return null;
4045
+ if (!total || !["integer", "number"].includes(total.type)) return null;
4046
+ const itemSchema = data.items ?? {};
4047
+ return { itemSchema };
4048
+ }
4049
+ function generateOperation(method, pathStr, op, pathLevelParams, componentSchemas, zod) {
4050
+ const isQuery = method === "get";
4051
+ const allParams = [
4052
+ ...pathLevelParams,
4053
+ ...(op.parameters ?? []).filter(
4054
+ (p) => !pathLevelParams.find((pp) => pp.name === p.name && pp.in === p.in)
4055
+ )
4056
+ ];
4057
+ const pathParams = allParams.filter((p) => p.in === "path");
4058
+ const queryParams = allParams.filter((p) => p.in === "query");
4059
+ const hasPathParams = pathParams.length > 0;
4060
+ const hasQueryParams = queryParams.length > 0;
4061
+ const fnName = plainFnName(method, pathStr, op.operationId);
4062
+ const name = hookName(method, fnName);
4063
+ const successSch = successSchema(op.responses);
4064
+ const paginatedResult = isQuery && successSch ? isPaginatedSchema(successSch) : null;
4065
+ const isPaginated = paginatedResult !== null;
4066
+ const respType = (() => {
4067
+ if (isPaginated && paginatedResult) {
4068
+ return `PaginatedResponse<${schemaToTs(paginatedResult.itemSchema)}>`;
4069
+ }
4070
+ return successSch ? schemaToTs(successSch) : "void";
4071
+ })();
4072
+ const bodyType = (() => {
4073
+ const s = requestBodySchema(op.requestBody);
4074
+ return s ? schemaToTs(s) : "void";
4075
+ })();
4076
+ const hasBody = bodyType !== "void";
4077
+ const nonJsonType = nonJsonBodyContentType(op.requestBody);
4078
+ const jsdocParts = [];
4079
+ if (op.deprecated) jsdocParts.push("@deprecated");
4080
+ if (op.summary) jsdocParts.push(op.summary);
4081
+ if (nonJsonType) jsdocParts.push(`NOTE: request body is ${nonJsonType} \u2014 body typed as void`);
4082
+ const jsdoc = jsdocParts.length > 0 ? `/** ${jsdocParts.join(" \u2014 ")} */
4083
+ ` : "";
4084
+ if (isPaginated) {
4085
+ const pathArgsStr = pathParams.map((p) => `${p.name}: ${paramType(p)}`).join(", ");
4086
+ const pathArgsWithComma = pathArgsStr ? `${pathArgsStr}, ` : "";
4087
+ const paginatedUrl = paginatedUrlTemplate(pathStr, pathParams);
4088
+ const plainFn2 = `${jsdoc}export const ${fnName} = (${pathArgsWithComma}page = 1, perPage = 20): Promise<${respType}> =>
4089
+ api.get<${respType}>(${paginatedUrl})`;
4090
+ const pathKeyParts = pathStr.split("/").filter(Boolean).map(
4091
+ (seg) => seg.startsWith("{") && seg.endsWith("}") ? `params.${seg.slice(1, -1)}` : `'${seg}'`
4092
+ );
4093
+ const paginatedQueryKey = `[${[...pathKeyParts, "params.page ?? 1", "params.perPage ?? 20"].join(", ")}]`;
4094
+ const paramsType = hasPathParams ? `{ ${pathParams.map((p) => `${p.name}: ${paramType(p)}`).join("; ")}; page?: number; perPage?: number }` : `{ page?: number; perPage?: number }`;
4095
+ const callPathArgs = pathParams.map((p) => `params.${p.name}`).join(", ");
4096
+ const callAllArgs = callPathArgs ? `${callPathArgs}, params.page ?? 1, params.perPage ?? 20` : `params.page ?? 1, params.perPage ?? 20`;
4097
+ const hookLines = [];
4098
+ if (jsdocParts.length > 0) hookLines.push(`/** ${jsdocParts.join(" \u2014 ")} */`);
4099
+ hookLines.push(`export function ${name}(`);
4100
+ hookLines.push(` params: ${paramsType} = {},`);
4101
+ hookLines.push(
4102
+ ` options?: Omit<UseQueryOptions<${respType}, ApiError>, 'queryKey' | 'queryFn'>`
4103
+ );
4104
+ hookLines.push(`) {`);
4105
+ hookLines.push(` return useQuery({`);
4106
+ hookLines.push(` queryKey: ${paginatedQueryKey},`);
4107
+ hookLines.push(` queryFn: () => ${fnName}(${callAllArgs}),`);
4108
+ hookLines.push(` ...options,`);
4109
+ hookLines.push(` })`);
4110
+ hookLines.push(`}`);
4111
+ return { apiCode: plainFn2, hookCode: hookLines.join("\n"), fnNames: [fnName], isPaginated: true };
4112
+ }
4113
+ const fnArgParts = [
4114
+ ...pathParams.map((p) => `${p.name}: ${paramType(p)}`),
4115
+ ...!isQuery && hasBody ? [`body: ${bodyType}`] : [],
4116
+ ...queryParams.map((p) => `${p.name}${p.required ? "" : "?"}: ${paramType(p)}`)
4117
+ ];
4118
+ const fnArgs = fnArgParts.join(", ");
4119
+ let plainFn;
4120
+ if (hasQueryParams) {
4121
+ const pathTpl = pathStr.replace(/\{([^}]+)\}/g, (_2, n) => `\${${n}}`);
4122
+ const qSetLines = queryParams.map(
4123
+ (p) => p.required ? ` _q.set('${p.name}', String(${p.name}))` : ` if (${p.name} != null) _q.set('${p.name}', String(${p.name}))`
4124
+ );
4125
+ const urlExpr = `\`${pathTpl}?\${_q}\``;
4126
+ const needsBody = method !== "get" && method !== "delete";
4127
+ const returnCall = hasBody ? `return api.${method}<${respType}>(${urlExpr}, body)` : needsBody ? `return api.${method}<${respType}>(${urlExpr}, undefined)` : `return api.${method}<${respType}>(${urlExpr})`;
4128
+ plainFn = `${jsdoc}export const ${fnName} = (${fnArgs}): Promise<${respType}> => {
4129
+ const _q = new URLSearchParams()
4130
+ ` + qSetLines.join("\n") + `
4131
+ ${returnCall}
4132
+ }`;
4133
+ } else {
4134
+ const rawUrl = rawPathTemplate(pathStr, pathParams);
4135
+ const needsBody = method !== "get" && method !== "delete";
4136
+ const apiCall = hasBody ? `api.${method}<${respType}>(${rawUrl}, body)` : needsBody ? `api.${method}<${respType}>(${rawUrl}, undefined)` : `api.${method}<${respType}>(${rawUrl})`;
4137
+ plainFn = `${jsdoc}export const ${fnName} = (${fnArgs}): Promise<${respType}> =>
4138
+ ${apiCall}`;
4139
+ }
4140
+ const lines = [];
4141
+ if (jsdocParts.length > 0) lines.push(`/** ${jsdocParts.join(" \u2014 ")} */`);
4142
+ if (isQuery) {
4143
+ const paramFields = [
4144
+ ...pathParams.map((p) => `${p.name}: ${paramType(p)}`),
4145
+ ...queryParams.map((p) => `${p.name}${p.required ? "" : "?"}: ${paramType(p)}`)
4146
+ ];
4147
+ const hasAnyParams = paramFields.length > 0;
4148
+ const allParamsOptional = pathParams.length === 0 && queryParams.every((p) => !p.required);
4149
+ const paramsDefault = hasAnyParams && allParamsOptional ? " = {}" : "";
4150
+ const paramsArg = hasAnyParams ? `params: { ${paramFields.join("; ")} }${paramsDefault}, ` : "";
4151
+ const allCallArgs = [
4152
+ ...pathParams.map((p) => `params.${p.name}`),
4153
+ ...queryParams.map((p) => `params.${p.name}`)
4154
+ ];
4155
+ lines.push(`export function ${name}(`);
4156
+ lines.push(
4157
+ ` ${paramsArg}options?: Omit<UseQueryOptions<${respType}, ApiError>, 'queryKey' | 'queryFn'>`
4158
+ );
4159
+ lines.push(`) {`);
4160
+ lines.push(` return useQuery({`);
4161
+ lines.push(` queryKey: ${queryKey(pathStr, queryParams)},`);
4162
+ if (allCallArgs.length > 0) {
4163
+ lines.push(` queryFn: () => ${fnName}(${allCallArgs.join(", ")}),`);
4164
+ } else {
4165
+ lines.push(` queryFn: ${fnName},`);
4166
+ }
4167
+ lines.push(` ...options,`);
4168
+ lines.push(` })`);
4169
+ lines.push(`}`);
4170
+ } else {
4171
+ let variablesType;
4172
+ let mutationFnExpr;
4173
+ const mutationQueryFields = queryParams.map(
4174
+ (p) => `${p.name}${p.required ? "" : "?"}: ${paramType(p)}`
4175
+ );
4176
+ const queryCallArgs = queryParams.map((p) => `vars.${p.name}`);
4177
+ if (!hasPathParams && !hasBody && !hasQueryParams) {
4178
+ variablesType = "void";
4179
+ mutationFnExpr = `() => ${fnName}()`;
4180
+ } else if (!hasPathParams && !hasQueryParams) {
4181
+ variablesType = bodyType;
4182
+ mutationFnExpr = fnName;
4183
+ } else {
4184
+ const fields = [
4185
+ ...pathParams.map((p) => `${p.name}: ${paramType(p)}`),
4186
+ ...hasBody ? [`body: ${bodyType}`] : [],
4187
+ ...mutationQueryFields
4188
+ ];
4189
+ variablesType = `{ ${fields.join("; ")} }`;
4190
+ const callArgs = [
4191
+ ...pathParams.map((p) => `vars.${p.name}`),
4192
+ ...hasBody ? [`vars.body`] : [],
4193
+ ...queryCallArgs
4194
+ ];
4195
+ mutationFnExpr = `(vars) => ${fnName}(${callArgs.join(", ")})`;
4196
+ }
4197
+ lines.push(`export function ${name}(`);
4198
+ lines.push(
4199
+ ` options?: UseMutationOptions<${respType}, ApiError, ${variablesType}> & { invalidateKeys?: QueryKey[] }`
4200
+ );
4201
+ lines.push(`) {`);
4202
+ lines.push(` const { invalidateKeys, ...mutationOptions } = options ?? {}`);
4203
+ lines.push(` const queryClient = useQueryClient()`);
4204
+ lines.push(` return useMutation({`);
4205
+ lines.push(` mutationFn: ${mutationFnExpr},`);
4206
+ lines.push(` ...mutationOptions,`);
4207
+ lines.push(` onSuccess: (...args) => {`);
4208
+ lines.push(` invalidateKeys?.forEach((key) => queryClient.invalidateQueries({ queryKey: key }))`);
4209
+ lines.push(` mutationOptions.onSuccess?.(...args)`);
4210
+ lines.push(` },`);
4211
+ lines.push(` })`);
4212
+ lines.push(`}`);
4213
+ }
4214
+ const apiParts = [plainFn];
4215
+ if (!isQuery && hasBody && zod) {
4216
+ const bodySchema = requestBodySchema(op.requestBody);
4217
+ if (bodySchema) {
4218
+ const zodSchemaName = `${fnName}Schema`;
4219
+ const zodTypeName = fnName.charAt(0).toUpperCase() + fnName.slice(1) + "Input";
4220
+ const zodSchemaStr = schemaToZod(bodySchema, componentSchemas);
4221
+ apiParts.push(
4222
+ `/** Zod schema for ${name} form validation */
4223
+ export const ${zodSchemaName} = ${zodSchemaStr}
4224
+ export type ${zodTypeName} = z.infer<typeof ${zodSchemaName}>`
4225
+ );
4226
+ }
4227
+ }
4228
+ return { apiCode: apiParts.join("\n\n"), hookCode: lines.join("\n"), fnNames: [fnName], isPaginated: false };
4229
+ }
4230
+ function collectTagRefs(ops) {
4231
+ const typeRefs = /* @__PURE__ */ new Set();
4232
+ for (const { operation, pathLevelParams } of ops) {
4233
+ const s = successSchema(operation.responses);
4234
+ if (s) collectRefs(s).forEach((r) => typeRefs.add(r));
4235
+ const b = requestBodySchema(operation.requestBody);
4236
+ if (b) collectRefs(b).forEach((r) => typeRefs.add(r));
4237
+ for (const p of [...operation.parameters ?? [], ...pathLevelParams]) {
4238
+ if (p.schema) collectRefs(p.schema).forEach((r) => typeRefs.add(r));
4239
+ }
4240
+ }
4241
+ for (const { method, operation } of ops) {
4242
+ if (method !== "get") continue;
4243
+ const s = successSchema(operation.responses);
4244
+ if (!s) continue;
4245
+ const pagResult = isPaginatedSchema(s);
4246
+ if (pagResult) collectRefs(pagResult.itemSchema).forEach((r) => typeRefs.add(r));
4247
+ }
4248
+ return typeRefs;
4249
+ }
4250
+ function generateTagFiles(ops, slug, componentSchemas, zod, importPaths) {
4251
+ const ip = importPaths ?? {
4252
+ snapshotImport: "@lib/snapshot",
4253
+ typesRelForApi: "../types/api",
4254
+ typesRelForHooks: "../../types/api",
4255
+ apiRelBase: "../../api"
4256
+ };
4257
+ const typeRefs = collectTagRefs(ops);
4258
+ const generated = ops.map(
4259
+ ({ method, pathStr, operation, pathLevelParams }) => generateOperation(method, pathStr, operation, pathLevelParams, componentSchemas, zod)
4260
+ );
4261
+ const hasPaginated = generated.some((g) => g.isPaginated);
4262
+ const hasZod = zod && generated.some((g) => g.apiCode.includes("z.infer"));
4263
+ const allFnNames = generated.flatMap((g) => g.fnNames);
4264
+ const localTypeImports = [...typeRefs].sort();
4265
+ if (hasPaginated) localTypeImports.unshift("PaginatedResponse");
4266
+ const apiLines = [
4267
+ "// Generated by bunx snapshot sync. Do not edit manually.",
4268
+ "",
4269
+ `import { api } from '${ip.snapshotImport}'`
4270
+ ];
4271
+ if (hasZod) apiLines.push("import { z } from 'zod'");
4272
+ if (localTypeImports.length > 0) {
4273
+ apiLines.push(`import type { ${localTypeImports.join(", ")} } from '${ip.typesRelForApi}'`);
4274
+ }
4275
+ apiLines.push("");
4276
+ for (const { apiCode } of generated) {
4277
+ apiLines.push(apiCode);
4278
+ apiLines.push("");
4279
+ }
4280
+ const hasQueries = ops.some(({ method }) => method === "get");
4281
+ const hasMutations = ops.some(({ method }) => method !== "get");
4282
+ const rqImports = [];
4283
+ if (hasQueries) rqImports.push("useQuery", "type UseQueryOptions");
4284
+ if (hasMutations) rqImports.push("useMutation", "useQueryClient", "type UseMutationOptions", "type QueryKey");
4285
+ const hooksLines = [
4286
+ "// Generated by bunx snapshot sync. Do not edit manually.",
4287
+ "",
4288
+ `import { ${rqImports.join(", ")} } from '@tanstack/react-query'`,
4289
+ "import { ApiError } from '@lastshotlabs/snapshot'",
4290
+ `import { ${allFnNames.join(", ")} } from '${ip.apiRelBase}/${slug}'`
4291
+ ];
4292
+ if (localTypeImports.length > 0) {
4293
+ hooksLines.push(`import type { ${localTypeImports.join(", ")} } from '${ip.typesRelForHooks}'`);
4294
+ }
4295
+ hooksLines.push("");
4296
+ for (const { hookCode } of generated) {
4297
+ hooksLines.push(hookCode);
4298
+ hooksLines.push("");
4299
+ }
4300
+ return {
4301
+ apiContent: apiLines.join("\n"),
4302
+ hooksContent: hooksLines.join("\n"),
4303
+ hasPaginated
4304
+ };
4305
+ }
4306
+ async function readEnvUrl(cwd) {
4307
+ try {
4308
+ const content = await fs2.readFile(path4.join(cwd, ".env"), "utf8");
4309
+ for (const line of content.split("\n")) {
4310
+ const m = line.match(/^VITE_API_URL\s*=\s*(.+)$/);
4311
+ if (m) return m[1].trim().replace(/^["']|["']$/g, "");
4312
+ }
4313
+ } catch {
4314
+ }
4315
+ return void 0;
4316
+ }
4317
+ async function loadSchema(opts) {
4318
+ if (opts.filePath) {
4319
+ const abs = path4.resolve(opts.cwd, opts.filePath);
4320
+ const content = await fs2.readFile(abs, "utf8");
4321
+ return JSON.parse(content);
4322
+ }
4323
+ let apiUrl = opts.apiUrl ?? process.env["VITE_API_URL"] ?? await readEnvUrl(opts.cwd);
4324
+ if (!apiUrl) {
4325
+ throw new Error(
4326
+ "No API URL found. Pass --api <url>, --file <path>, or set VITE_API_URL in .env"
4327
+ );
4328
+ }
4329
+ apiUrl = apiUrl.replace(/\/$/, "");
4330
+ const res = await fetch(`${apiUrl}/openapi.json`);
4331
+ if (!res.ok) {
4332
+ const err = new Error(`Server returned ${res.status} for ${apiUrl}/openapi.json`);
4333
+ err.status = res.status;
4334
+ throw err;
4335
+ }
4336
+ return await res.json();
4337
+ }
4338
+ async function runSyncOnce(opts, logger, backend, isMulti) {
4339
+ const { cwd } = opts;
4340
+ const sp = be();
4341
+ let schema;
4342
+ const effectiveFilePath = backend.filePath ?? opts.filePath;
4343
+ if (effectiveFilePath) {
4344
+ sp.start(`Reading schema from ${effectiveFilePath}`);
4345
+ } else {
4346
+ sp.start("Fetching schema...");
4347
+ }
4348
+ try {
4349
+ schema = await loadSchema({ ...opts, apiUrl: backend.apiUrl ?? opts.apiUrl, filePath: backend.filePath ?? opts.filePath });
4350
+ } catch (err) {
4351
+ sp.stop("Failed");
4352
+ const msg = err instanceof Error ? err.message : String(err);
4353
+ logger.error(msg);
4354
+ if (err instanceof Error && "status" in err) {
4355
+ const status = err.status;
4356
+ if (status === 401 || status === 403) {
4357
+ logger.info("Tip: check your bunshot config \u2014 /openapi.json may require authentication");
4358
+ }
4359
+ }
4360
+ throw err;
4361
+ }
4362
+ const title = schema.info?.title ?? "API";
4363
+ const version = schema.info?.version ?? "";
4364
+ sp.stop(`Schema: ${title}${version ? ` ${version}` : ""}`);
4365
+ const componentSchemas = schema.components?.schemas ?? {};
4366
+ const byTag = /* @__PURE__ */ new Map();
4367
+ for (const [pathStr, pathItem] of Object.entries(schema.paths ?? {})) {
4368
+ const pathLevelParams = pathItem.parameters ?? [];
4369
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
4370
+ const operation = pathItem[method];
4371
+ if (!operation) continue;
4372
+ const tag = operation.tags?.[0] ?? "index";
4373
+ if (!byTag.has(tag)) byTag.set(tag, []);
4374
+ byTag.get(tag).push({ method, pathStr, operation, pathLevelParams });
4375
+ }
4376
+ }
4377
+ if (byTag.size === 0) {
4378
+ logger.warn("No operations found in schema \u2014 no hook files generated");
4379
+ }
4380
+ const { apiDir, hooksDir, typesFile, snapshotImport } = resolveBackendDirs(cwd, backend, opts, isMulti);
4381
+ const importPaths = {
4382
+ snapshotImport,
4383
+ typesRelForApi: toRelativeImport(apiDir, typesFile),
4384
+ typesRelForHooks: toRelativeImport(hooksDir, typesFile),
4385
+ apiRelBase: toRelativeImport(hooksDir, apiDir)
4386
+ };
4387
+ await fs2.mkdir(apiDir, { recursive: true });
4388
+ await fs2.mkdir(hooksDir, { recursive: true });
4389
+ await fs2.mkdir(path4.dirname(typesFile), { recursive: true });
4390
+ let globalHasPaginated = false;
4391
+ for (const [tag, ops] of byTag.entries()) {
4392
+ const slug = tag.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4393
+ const fileName = `${slug}.ts`;
4394
+ const { apiContent, hooksContent, hasPaginated } = generateTagFiles(ops, slug, componentSchemas, backend.zod ?? opts.zod, importPaths);
4395
+ if (hasPaginated) globalHasPaginated = true;
4396
+ await fs2.writeFile(path4.join(apiDir, fileName), apiContent, "utf8");
4397
+ await fs2.writeFile(path4.join(hooksDir, fileName), hooksContent, "utf8");
4398
+ const apiRel = path4.relative(cwd, path4.join(apiDir, fileName)).replace(/\\/g, "/");
4399
+ const hooksRel = path4.relative(cwd, path4.join(hooksDir, fileName)).replace(/\\/g, "/");
4400
+ logger.success(`${apiRel} + ${hooksRel} \u2014 ${ops.length} operation(s)`);
4401
+ }
4402
+ const schemaCount = Object.keys(componentSchemas).length;
4403
+ if (schemaCount === 0) {
4404
+ const exists = await fs2.access(typesFile).then(() => true).catch(() => false);
4405
+ if (exists) {
4406
+ const typesRel = path4.relative(cwd, typesFile).replace(/\\/g, "/");
4407
+ logger.warn(`${typesRel} \u2014 0 schemas in spec, keeping existing file`);
4408
+ }
4409
+ } else {
4410
+ const typesContent = generateTypesContent(componentSchemas, globalHasPaginated);
4411
+ await fs2.writeFile(typesFile, typesContent, "utf8");
4412
+ const typesRel = path4.relative(cwd, typesFile).replace(/\\/g, "/");
4413
+ logger.success(`${typesRel} \u2014 ${schemaCount} type(s)`);
4414
+ }
4415
+ }
4416
+ async function runSync(opts) {
4417
+ const logger = opts.logger ?? clackLogger;
4418
+ const fileConfig = await readSyncConfig(opts.cwd);
4419
+ const backends = fileConfig.backends && fileConfig.backends.length > 0 ? fileConfig.backends : [fileConfig];
4420
+ const isMulti = !!(fileConfig.backends && fileConfig.backends.length > 0);
4421
+ const runAll = async () => {
4422
+ for (const backend of backends) {
4423
+ if (isMulti && backend.name) logger.info(`\u2500\u2500 ${backend.name} \u2500\u2500`);
4424
+ await runSyncOnce(opts, logger, backend, isMulti);
4425
+ }
4426
+ };
4427
+ await runAll();
4428
+ if (!opts.watch) return;
4429
+ logger.info("Watching for schema changes... (Ctrl+C to stop)");
4430
+ const lastHashes = /* @__PURE__ */ new Map();
4431
+ for (let i = 0; i < backends.length; i++) {
4432
+ const b = backends[i];
4433
+ try {
4434
+ const schema = await loadSchema({ ...opts, apiUrl: b.apiUrl ?? opts.apiUrl, filePath: b.filePath ?? opts.filePath });
4435
+ lastHashes.set(i, schemaHash(schema));
4436
+ } catch {
4437
+ lastHashes.set(i, "");
4438
+ }
4439
+ }
4440
+ const pollMs = backends.some((b) => b.filePath ?? opts.filePath) ? 1e3 : 3e3;
4441
+ const interval = setInterval(async () => {
4442
+ for (let i = 0; i < backends.length; i++) {
4443
+ const b = backends[i];
4444
+ try {
4445
+ const schema = await loadSchema({ ...opts, apiUrl: b.apiUrl ?? opts.apiUrl, filePath: b.filePath ?? opts.filePath });
4446
+ const h2 = schemaHash(schema);
4447
+ if (h2 === lastHashes.get(i)) continue;
4448
+ lastHashes.set(i, h2);
4449
+ if (isMulti && b.name) logger.info(`\u2500\u2500 ${b.name} (changed) \u2500\u2500`);
4450
+ await runSyncOnce(opts, logger, b, isMulti);
4451
+ } catch (e) {
4452
+ logger.error(e instanceof Error ? e.message : String(e));
4453
+ }
4454
+ }
4455
+ }, pollMs);
4456
+ process.on("SIGINT", () => {
4457
+ clearInterval(interval);
4458
+ process.exit(0);
4459
+ });
4460
+ }
4461
+
4462
+ // src/cli/index.ts
4463
+ async function main() {
4464
+ const args = process2.argv.slice(2);
4465
+ const positionals = args.filter((a) => !a.startsWith("--"));
4466
+ if (positionals[0] === "sync") {
4467
+ Wt2("@lastshotlabs/snapshot sync");
4468
+ const apiIdx = args.indexOf("--api");
4469
+ const apiUrl = apiIdx !== -1 ? args[apiIdx + 1] : args.find((a) => a.startsWith("--api="))?.slice(6);
4470
+ const fileIdx = args.indexOf("--file");
4471
+ const filePath = fileIdx !== -1 ? args[fileIdx + 1] : args.find((a) => a.startsWith("--file="))?.slice(7);
4472
+ const watch = args.includes("--watch") || args.includes("-w");
4473
+ const zod = args.includes("--zod");
4474
+ const apiDirIdx = args.indexOf("--api-dir");
4475
+ const apiDirArg = apiDirIdx !== -1 ? args[apiDirIdx + 1] : args.find((a) => a.startsWith("--api-dir="))?.slice(10);
4476
+ const hooksDirIdx = args.indexOf("--hooks-dir");
4477
+ const hooksDirArg = hooksDirIdx !== -1 ? args[hooksDirIdx + 1] : args.find((a) => a.startsWith("--hooks-dir="))?.slice(13);
4478
+ const typesPathIdx = args.indexOf("--types-path");
4479
+ const typesPathArg = typesPathIdx !== -1 ? args[typesPathIdx + 1] : args.find((a) => a.startsWith("--types-path="))?.slice(13);
4480
+ const snapshotImportIdx = args.indexOf("--snapshot-import");
4481
+ const snapshotImportArg = snapshotImportIdx !== -1 ? args[snapshotImportIdx + 1] : args.find((a) => a.startsWith("--snapshot-import="))?.slice(18);
4482
+ await runSync({ apiUrl, filePath, cwd: process2.cwd(), watch, zod, apiDir: apiDirArg, hooksDir: hooksDirArg, typesPath: typesPathArg, snapshotImport: snapshotImportArg });
4483
+ return;
4484
+ }
4485
+ if (positionals[0] && positionals[0] !== "init") {
4486
+ R2.error(`Unknown command: ${positionals[0]}`);
4487
+ R2.info("Usage: snapshot init [project-name] [output-dir]");
4488
+ R2.info(" snapshot sync [--api <url>] [--file <path>] [--watch]");
4489
+ process2.exit(1);
4490
+ }
4491
+ const projectNameArg = positionals[0] === "init" ? positionals[1] : void 0;
4492
+ const outputDirArg = positionals[0] === "init" ? positionals[2] : void 0;
4493
+ const skipPrompts = args.includes("--yes") || args.includes("-y");
4494
+ Wt2("@lastshotlabs/snapshot init");
4495
+ const config = await runPrompts({ projectNameArg, outputDirArg, skipPrompts });
4496
+ if (!config) {
4497
+ Nt("Scaffold cancelled.");
4498
+ process2.exit(0);
4499
+ }
4500
+ R2.info(`Initialising ${config.projectName} in ${path5.relative(process2.cwd(), config.dir) || config.dir}`);
4501
+ await scaffold(config);
4502
+ const relDir = path5.relative(process2.cwd(), config.dir);
4503
+ const wsLine = config.webSocket ? ` VITE_WS_URL \u2014 your WebSocket URL
4504
+ ` : "";
4505
+ const bearerLine = config.securityProfile === "prototype" ? ` VITE_BEARER_TOKEN \u2014 your API bearer token (prototype mode)
4506
+ ` : "";
4507
+ Gt(
4508
+ `${config.projectName} initialised successfully
4509
+
4510
+ Next steps:
4511
+
4512
+ cd ${relDir}
4513
+
4514
+ Fill in your .env:
4515
+ VITE_API_URL \u2014 your bunshot backend URL
4516
+ ${bearerLine}${wsLine}
4517
+ bun dev \u2014 start the dev server
4518
+ bun run sync \u2014 generate typed API hooks (auto-runs on bun dev when schema.json exists)
4519
+
4520
+ Note: TypeScript will show an error for routeTree.gen.ts until you run bun dev once.
4521
+
4522
+ Docs: github.com/lastshotlabs/snapshot`
4523
+ );
4524
+ }
4525
+ main().catch((err) => {
4526
+ const msg = err instanceof Error ? err.message : String(err);
4527
+ R2.error(`Init failed: ${msg}`);
4528
+ process2.exit(1);
4529
+ });