@cimplify/cli 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{add-ZRSE2STJ.mjs → add-5ODVFWVR.mjs} +1 -1
  2. package/dist/{chunk-2ODNBO3D.mjs → chunk-PQUWRUVT.mjs} +2 -2
  3. package/dist/chunk-W3WLADJP.mjs +6455 -0
  4. package/dist/{chunk-QU6DN3WR.mjs → chunk-WF5HX2WB.mjs} +1 -1
  5. package/dist/dev-OF335HZZ.mjs +762 -0
  6. package/dist/dispatcher.mjs +10 -10
  7. package/dist/{doctor-6YCHMCZO.mjs → doctor-O2XLOCBB.mjs} +2 -2
  8. package/dist/{explain-CUVUG7O5.mjs → explain-327PXAY6.mjs} +1 -1
  9. package/dist/{introspect-J3REAIDU.mjs → introspect-P3BJ7WQL.mjs} +2 -2
  10. package/dist/{list-FCBWJ5DY.mjs → list-27TS2L47.mjs} +1 -1
  11. package/dist/{update-DI3MW7TW.mjs → update-TEHVCICJ.mjs} +1 -1
  12. package/package.json +2 -2
  13. package/templates/storefront-auto/app/layout.tsx +2 -0
  14. package/templates/storefront-auto/bun.lock +456 -8
  15. package/templates/storefront-auto/components/support-chat.tsx +34 -0
  16. package/templates/storefront-auto/package.json +1 -1
  17. package/templates/storefront-bakery/app/layout.tsx +2 -0
  18. package/templates/storefront-bakery/bun.lock +456 -8
  19. package/templates/storefront-bakery/components/support-chat.tsx +34 -0
  20. package/templates/storefront-bakery/package.json +1 -1
  21. package/templates/storefront-fashion/app/layout.tsx +2 -0
  22. package/templates/storefront-fashion/bun.lock +456 -8
  23. package/templates/storefront-fashion/components/support-chat.tsx +34 -0
  24. package/templates/storefront-fashion/package.json +1 -1
  25. package/templates/storefront-grocery/app/layout.tsx +2 -0
  26. package/templates/storefront-grocery/bun.lock +456 -8
  27. package/templates/storefront-grocery/components/support-chat.tsx +34 -0
  28. package/templates/storefront-grocery/package.json +1 -1
  29. package/templates/storefront-pharmacy/app/layout.tsx +2 -0
  30. package/templates/storefront-pharmacy/bun.lock +456 -8
  31. package/templates/storefront-pharmacy/components/support-chat.tsx +34 -0
  32. package/templates/storefront-pharmacy/package.json +1 -1
  33. package/templates/storefront-restaurant/app/layout.tsx +2 -0
  34. package/templates/storefront-restaurant/bun.lock +454 -8
  35. package/templates/storefront-restaurant/components/support-chat.tsx +34 -0
  36. package/templates/storefront-restaurant/package.json +1 -1
  37. package/templates/storefront-retail/app/layout.tsx +2 -0
  38. package/templates/storefront-retail/bun.lock +456 -8
  39. package/templates/storefront-retail/components/support-chat.tsx +34 -0
  40. package/templates/storefront-retail/package.json +1 -1
  41. package/templates/storefront-services/app/layout.tsx +2 -0
  42. package/templates/storefront-services/bun.lock +2 -2
  43. package/templates/storefront-services/components/support-chat.tsx +34 -0
  44. package/templates/storefront-services/package.json +1 -1
  45. package/dist/chunk-WBNI7PWA.mjs +0 -6455
  46. package/dist/dev-ABN3HOO2.mjs +0 -150
@@ -0,0 +1,762 @@
1
+ #!/usr/bin/env node
2
+ import { parseEnvFile, formatEnvFile } from './chunk-DBZ3UOQ2.mjs';
3
+ import { ENV_SCOPE, PUBLIC_ENV_PREFIX } from './chunk-MXYUAJEW.mjs';
4
+ import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
5
+ import { ApiClient } from './chunk-F4S5KETY.mjs';
6
+ import { readAuth, readProjectLink } from './chunk-UBAI443T.mjs';
7
+ import { info, CliError, CLI_ERROR_CODE, dim } from './chunk-E2T2SBP5.mjs';
8
+ import { spawn } from 'child_process';
9
+ import { promises } from 'fs';
10
+ import path2 from 'path';
11
+ import http from 'http';
12
+
13
+ // src/tunnel/protocol.ts
14
+ var MAX_PAYLOAD = 262144;
15
+ var HEADER_LEN = 9;
16
+ var SUBPROTOCOL = "cimplify-tunnel.v1";
17
+ var CONNECT_PATH = "/__cimplify/connect";
18
+ var FRAME_TYPE = {
19
+ OPEN_HTTP: 1,
20
+ OPEN_WS: 2,
21
+ RESP_HEAD: 3,
22
+ BODY: 4,
23
+ END: 5,
24
+ WS_TEXT: 6,
25
+ WS_BIN: 7,
26
+ CLOSE: 8
27
+ };
28
+ var KNOWN_FRAME_TYPES = new Set(Object.values(FRAME_TYPE));
29
+ var TunnelProtocolError = class extends Error {
30
+ };
31
+ function encodeFrame(frame) {
32
+ if (frame.payload.length > MAX_PAYLOAD) {
33
+ throw new TunnelProtocolError(`payload length ${frame.payload.length} exceeds ${MAX_PAYLOAD}`);
34
+ }
35
+ const bytes = new Uint8Array(HEADER_LEN + frame.payload.length);
36
+ const view = new DataView(bytes.buffer);
37
+ view.setUint32(0, frame.streamId);
38
+ view.setUint8(4, frame.frameType);
39
+ view.setUint32(5, frame.payload.length);
40
+ bytes.set(frame.payload, HEADER_LEN);
41
+ return bytes;
42
+ }
43
+ function decodeFrame(bytes) {
44
+ if (bytes.length < HEADER_LEN) {
45
+ throw new TunnelProtocolError(`frame truncated: ${bytes.length} bytes`);
46
+ }
47
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
48
+ const streamId = view.getUint32(0);
49
+ const rawType = view.getUint8(4);
50
+ if (!KNOWN_FRAME_TYPES.has(rawType)) {
51
+ throw new TunnelProtocolError(`unknown frame type 0x${rawType.toString(16).padStart(2, "0")}`);
52
+ }
53
+ const declared = view.getUint32(5);
54
+ if (declared > MAX_PAYLOAD) {
55
+ throw new TunnelProtocolError(`payload length ${declared} exceeds ${MAX_PAYLOAD}`);
56
+ }
57
+ const actual = bytes.length - HEADER_LEN;
58
+ if (declared !== actual) {
59
+ throw new TunnelProtocolError(
60
+ `payload length field ${declared} does not match remaining ${actual} bytes`
61
+ );
62
+ }
63
+ return { streamId, frameType: rawType, payload: bytes.subarray(HEADER_LEN) };
64
+ }
65
+ var textEncoder = new TextEncoder();
66
+ var textDecoder = new TextDecoder();
67
+ function encodeJsonPayload(value) {
68
+ return textEncoder.encode(JSON.stringify(value));
69
+ }
70
+ function decodeJsonPayload(payload) {
71
+ return JSON.parse(textDecoder.decode(payload));
72
+ }
73
+ function* chunkPayload(bytes) {
74
+ if (bytes.length === 0) {
75
+ return;
76
+ }
77
+ for (let offset = 0; offset < bytes.length; offset += MAX_PAYLOAD) {
78
+ yield bytes.subarray(offset, Math.min(offset + MAX_PAYLOAD, bytes.length));
79
+ }
80
+ }
81
+
82
+ // src/tunnel/client.ts
83
+ var LOCAL_HOST = "127.0.0.1";
84
+ var SEND_HIGH_WATER_BYTES = 4 * 1024 * 1024;
85
+ var BACKPRESSURE_POLL_MS = 25;
86
+ var EMPTY_PAYLOAD = new Uint8Array(0);
87
+ var HEADER_SEC_WS_PROTOCOL = "sec-websocket-protocol";
88
+ var HEADER_CONTENT_TYPE_TEXT = ["content-type", "text/plain; charset=utf-8"];
89
+ var WS_CLOSE_NORMAL = 1e3;
90
+ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
91
+ "connection",
92
+ "keep-alive",
93
+ "proxy-authenticate",
94
+ "proxy-authorization",
95
+ "te",
96
+ "trailer",
97
+ "transfer-encoding",
98
+ "upgrade"
99
+ ]);
100
+ var textEncoder2 = new TextEncoder();
101
+ var textDecoder2 = new TextDecoder();
102
+ function defaultSocketFactory(url, protocols) {
103
+ const ctor = globalThis.WebSocket;
104
+ if (!ctor) {
105
+ throw new Error("global WebSocket is unavailable; the preview tunnel requires Bun or Node 22+");
106
+ }
107
+ return new ctor(url, protocols);
108
+ }
109
+ function connectUrl(baseUrl, token) {
110
+ return `${baseUrl.replace(/\/+$/, "")}${CONNECT_PATH}?token=${encodeURIComponent(token)}`;
111
+ }
112
+ var TunnelClient = class {
113
+ constructor(options) {
114
+ this.options = options;
115
+ }
116
+ options;
117
+ streams = /* @__PURE__ */ new Map();
118
+ socket = null;
119
+ closed = false;
120
+ start() {
121
+ const factory = this.options.createSocket ?? defaultSocketFactory;
122
+ const socket = factory(connectUrl(this.options.url, this.options.token), [SUBPROTOCOL]);
123
+ socket.binaryType = "arraybuffer";
124
+ this.socket = socket;
125
+ socket.addEventListener("open", () => {
126
+ if (!this.closed) this.options.events?.onConnected?.();
127
+ });
128
+ socket.addEventListener("message", (event) => this.handleMessage(event.data));
129
+ socket.addEventListener(
130
+ "close",
131
+ (event) => this.teardown(`connection closed (${event.code ?? "unknown"})`)
132
+ );
133
+ socket.addEventListener("error", () => this.teardown("connection error"));
134
+ }
135
+ stop() {
136
+ if (this.closed) return;
137
+ try {
138
+ this.socket?.close(WS_CLOSE_NORMAL);
139
+ } catch {
140
+ }
141
+ this.teardown("stopped");
142
+ }
143
+ teardown(reason) {
144
+ if (this.closed) return;
145
+ this.closed = true;
146
+ for (const handler of this.streams.values()) {
147
+ handler.abort();
148
+ }
149
+ this.streams.clear();
150
+ this.socket = null;
151
+ this.options.events?.onClosed?.(reason);
152
+ }
153
+ handleMessage(data) {
154
+ if (this.closed) return;
155
+ let bytes;
156
+ if (data instanceof ArrayBuffer) {
157
+ bytes = new Uint8Array(data);
158
+ } else if (data instanceof Uint8Array) {
159
+ bytes = data;
160
+ } else {
161
+ this.failConnection("non-binary message violates the tunnel protocol");
162
+ return;
163
+ }
164
+ let frame;
165
+ try {
166
+ frame = decodeFrame(bytes);
167
+ } catch (err) {
168
+ this.failConnection(`undecodable frame: ${err.message}`);
169
+ return;
170
+ }
171
+ this.dispatch(frame);
172
+ }
173
+ dispatch(frame) {
174
+ const handler = this.streams.get(frame.streamId);
175
+ if (handler) {
176
+ if (frame.frameType === FRAME_TYPE.CLOSE) this.streams.delete(frame.streamId);
177
+ handler.handleFrame(frame);
178
+ return;
179
+ }
180
+ switch (frame.frameType) {
181
+ case FRAME_TYPE.OPEN_HTTP:
182
+ this.openHttp(frame);
183
+ break;
184
+ case FRAME_TYPE.OPEN_WS:
185
+ this.openWs(frame);
186
+ break;
187
+ case FRAME_TYPE.CLOSE:
188
+ break;
189
+ default:
190
+ this.sendClose(frame.streamId);
191
+ }
192
+ }
193
+ openHttp(frame) {
194
+ let head;
195
+ try {
196
+ head = decodeJsonPayload(frame.payload);
197
+ } catch {
198
+ this.sendClose(frame.streamId);
199
+ return;
200
+ }
201
+ this.streams.set(
202
+ frame.streamId,
203
+ new HttpProxyStream(this.contextFor(frame.streamId), head, this.options.localPort)
204
+ );
205
+ }
206
+ openWs(frame) {
207
+ let head;
208
+ try {
209
+ head = decodeJsonPayload(frame.payload);
210
+ } catch {
211
+ this.sendClose(frame.streamId);
212
+ return;
213
+ }
214
+ const factory = this.options.createLocalSocket ?? defaultSocketFactory;
215
+ this.streams.set(
216
+ frame.streamId,
217
+ new WsProxyStream(this.contextFor(frame.streamId), head, this.options.localPort, factory)
218
+ );
219
+ }
220
+ contextFor(streamId) {
221
+ return {
222
+ send: (frameType, payload) => this.sendFrame({ streamId, frameType, payload }),
223
+ sendJson: (frameType, value) => this.sendFrame({ streamId, frameType, payload: encodeJsonPayload(value) }),
224
+ finish: () => {
225
+ this.streams.delete(streamId);
226
+ },
227
+ fail: (info2) => {
228
+ this.streams.delete(streamId);
229
+ this.sendClose(streamId, info2);
230
+ },
231
+ bufferedAmount: () => this.socket?.bufferedAmount ?? 0
232
+ };
233
+ }
234
+ sendClose(streamId, info2) {
235
+ const payload = info2 && (info2.code !== void 0 || info2.reason !== void 0) ? encodeJsonPayload(info2) : EMPTY_PAYLOAD;
236
+ this.sendFrame({ streamId, frameType: FRAME_TYPE.CLOSE, payload });
237
+ }
238
+ sendFrame(frame) {
239
+ if (this.closed || !this.socket) return false;
240
+ try {
241
+ this.socket.send(encodeFrame(frame));
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+ failConnection(reason) {
248
+ try {
249
+ this.socket?.close(WS_CLOSE_NORMAL);
250
+ } catch {
251
+ }
252
+ this.teardown(reason);
253
+ }
254
+ };
255
+ var HttpProxyStream = class {
256
+ constructor(ctx, head, localPort) {
257
+ this.ctx = ctx;
258
+ this.request = http.request({
259
+ host: LOCAL_HOST,
260
+ port: localPort,
261
+ method: head.method,
262
+ path: head.path,
263
+ headers: toNodeHeaders(head.headers)
264
+ });
265
+ this.request.on("response", (response) => this.pumpResponse(response));
266
+ this.request.on("error", (err) => this.handleRequestError(err));
267
+ }
268
+ ctx;
269
+ request;
270
+ responded = false;
271
+ done = false;
272
+ handleFrame(frame) {
273
+ switch (frame.frameType) {
274
+ case FRAME_TYPE.BODY:
275
+ this.request.write(Buffer.from(frame.payload));
276
+ break;
277
+ case FRAME_TYPE.END:
278
+ this.request.end();
279
+ break;
280
+ case FRAME_TYPE.CLOSE:
281
+ this.abort();
282
+ break;
283
+ default:
284
+ this.ctx.fail();
285
+ this.abort();
286
+ }
287
+ }
288
+ abort() {
289
+ this.done = true;
290
+ this.request.destroy();
291
+ }
292
+ pumpResponse(response) {
293
+ this.responded = true;
294
+ const headers = [];
295
+ for (let i = 0; i + 1 < response.rawHeaders.length; i += 2) {
296
+ headers.push([response.rawHeaders[i].toLowerCase(), response.rawHeaders[i + 1]]);
297
+ }
298
+ const head = { status: response.statusCode ?? 502, headers };
299
+ if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, head)) {
300
+ this.abort();
301
+ return;
302
+ }
303
+ response.on("data", (chunk) => {
304
+ for (const piece of chunkPayload(chunk)) {
305
+ if (!this.ctx.send(FRAME_TYPE.BODY, piece)) {
306
+ this.abort();
307
+ return;
308
+ }
309
+ }
310
+ this.applyBackpressure(response);
311
+ });
312
+ response.on("end", () => {
313
+ if (this.done) return;
314
+ this.done = true;
315
+ this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
316
+ this.ctx.finish();
317
+ });
318
+ response.on("error", () => {
319
+ if (this.done) return;
320
+ this.done = true;
321
+ this.ctx.fail();
322
+ });
323
+ }
324
+ /** The mux socket has no stream-level flow control; pause the local read
325
+ * side when the shared connection's send buffer backs up. */
326
+ applyBackpressure(response) {
327
+ if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) return;
328
+ response.pause();
329
+ const poll = () => {
330
+ if (this.done) return;
331
+ if (this.ctx.bufferedAmount() <= SEND_HIGH_WATER_BYTES) {
332
+ response.resume();
333
+ } else {
334
+ setTimeout(poll, BACKPRESSURE_POLL_MS);
335
+ }
336
+ };
337
+ setTimeout(poll, BACKPRESSURE_POLL_MS);
338
+ }
339
+ handleRequestError(err) {
340
+ if (this.done) return;
341
+ this.done = true;
342
+ if (this.responded) {
343
+ this.ctx.fail();
344
+ return;
345
+ }
346
+ this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
347
+ status: 502,
348
+ headers: [HEADER_CONTENT_TYPE_TEXT]
349
+ });
350
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder2.encode(`dev server unreachable: ${err.message}`));
351
+ this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
352
+ this.ctx.finish();
353
+ }
354
+ };
355
+ var WsProxyStream = class {
356
+ constructor(ctx, head, localPort, factory) {
357
+ this.ctx = ctx;
358
+ const protocols = head.headers.filter(([name]) => name === HEADER_SEC_WS_PROTOCOL).flatMap(([, value]) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
359
+ this.local = factory(`ws://${LOCAL_HOST}:${localPort}${head.path}`, protocols);
360
+ this.local.binaryType = "arraybuffer";
361
+ this.local.addEventListener("open", () => {
362
+ this.opened = true;
363
+ const headers = this.local.protocol ? [[HEADER_SEC_WS_PROTOCOL, this.local.protocol]] : [];
364
+ if (!this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, { status: 101, headers })) {
365
+ this.abort();
366
+ }
367
+ });
368
+ this.local.addEventListener("message", (event) => this.forward(event.data));
369
+ this.local.addEventListener("close", (event) => {
370
+ if (this.done) return;
371
+ this.done = true;
372
+ this.ctx.fail({ code: event.code, reason: event.reason || void 0 });
373
+ });
374
+ this.local.addEventListener("error", () => {
375
+ if (this.done || this.opened) return;
376
+ this.done = true;
377
+ this.ctx.sendJson(FRAME_TYPE.RESP_HEAD, {
378
+ status: 502,
379
+ headers: [HEADER_CONTENT_TYPE_TEXT]
380
+ });
381
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder2.encode("dev server refused the websocket"));
382
+ this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
383
+ this.ctx.finish();
384
+ });
385
+ }
386
+ ctx;
387
+ local;
388
+ opened = false;
389
+ done = false;
390
+ handleFrame(frame) {
391
+ switch (frame.frameType) {
392
+ case FRAME_TYPE.WS_TEXT:
393
+ this.local.send(textDecoder2.decode(frame.payload));
394
+ break;
395
+ case FRAME_TYPE.WS_BIN:
396
+ this.local.send(frame.payload);
397
+ break;
398
+ case FRAME_TYPE.CLOSE: {
399
+ this.done = true;
400
+ const info2 = parseCloseInfo(frame.payload);
401
+ this.local.close(sanitizeCloseCode(info2.code), info2.reason);
402
+ break;
403
+ }
404
+ default:
405
+ this.ctx.fail();
406
+ this.abort();
407
+ }
408
+ }
409
+ abort() {
410
+ this.done = true;
411
+ try {
412
+ this.local.close();
413
+ } catch {
414
+ }
415
+ }
416
+ forward(data) {
417
+ if (this.done) return;
418
+ let payload;
419
+ let frameType;
420
+ if (typeof data === "string") {
421
+ payload = textEncoder2.encode(data);
422
+ frameType = FRAME_TYPE.WS_TEXT;
423
+ } else if (data instanceof ArrayBuffer) {
424
+ payload = new Uint8Array(data);
425
+ frameType = FRAME_TYPE.WS_BIN;
426
+ } else if (data instanceof Uint8Array) {
427
+ payload = data;
428
+ frameType = FRAME_TYPE.WS_BIN;
429
+ } else {
430
+ return;
431
+ }
432
+ if (payload.length > MAX_PAYLOAD) {
433
+ this.done = true;
434
+ this.ctx.fail({ reason: "message exceeds tunnel frame cap" });
435
+ this.abort();
436
+ return;
437
+ }
438
+ if (!this.ctx.send(frameType, payload)) this.abort();
439
+ }
440
+ };
441
+ function toNodeHeaders(pairs) {
442
+ const out = {};
443
+ for (const [name, value] of pairs) {
444
+ if (HOP_BY_HOP_HEADERS.has(name)) continue;
445
+ const existing = out[name];
446
+ if (existing === void 0) {
447
+ out[name] = value;
448
+ } else if (Array.isArray(existing)) {
449
+ existing.push(value);
450
+ } else {
451
+ out[name] = [existing, value];
452
+ }
453
+ }
454
+ return out;
455
+ }
456
+ function parseCloseInfo(payload) {
457
+ if (payload.length === 0) return {};
458
+ try {
459
+ return decodeJsonPayload(payload);
460
+ } catch {
461
+ return {};
462
+ }
463
+ }
464
+ function sanitizeCloseCode(code) {
465
+ if (code === void 0) return void 0;
466
+ return code === WS_CLOSE_NORMAL || code >= 3e3 && code <= 4999 ? code : void 0;
467
+ }
468
+ var TUNNEL_URL_ENV = "CIMPLIFY_TUNNEL_URL";
469
+ var TUNNEL_TOKEN_ENV = "CIMPLIFY_TUNNEL_TOKEN";
470
+ var TUNNEL_LOCAL_PORT_ENV = "CIMPLIFY_TUNNEL_LOCAL_PORT";
471
+ var TUNNEL_CONFIG_DIR = ".cimplify";
472
+ var TUNNEL_CONFIG_FILE = "tunnel.json";
473
+ var DEFAULT_DEV_SERVER_PORT = 3e3;
474
+ function tunnelConfigPath(cwd) {
475
+ return path2.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
476
+ }
477
+ async function readTunnelConfig(cwd, env = process.env) {
478
+ const url = env[TUNNEL_URL_ENV];
479
+ const token = env[TUNNEL_TOKEN_ENV];
480
+ if (url && token) {
481
+ return { url, token, localPort: envPort(env) ?? DEFAULT_DEV_SERVER_PORT };
482
+ }
483
+ return readTunnelConfigFile(cwd);
484
+ }
485
+ function envPort(env) {
486
+ const raw = env[TUNNEL_LOCAL_PORT_ENV];
487
+ if (!raw) return null;
488
+ const port = Number.parseInt(raw, 10);
489
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
490
+ }
491
+ async function readTunnelConfigFile(cwd) {
492
+ let raw;
493
+ try {
494
+ raw = await promises.readFile(tunnelConfigPath(cwd), "utf8");
495
+ } catch {
496
+ return null;
497
+ }
498
+ let parsed;
499
+ try {
500
+ parsed = JSON.parse(raw);
501
+ } catch {
502
+ return null;
503
+ }
504
+ if (typeof parsed !== "object" || parsed === null) return null;
505
+ const record = parsed;
506
+ if (typeof record.url !== "string" || record.url.length === 0) return null;
507
+ if (typeof record.token !== "string" || record.token.length === 0) return null;
508
+ const localPort = typeof record.port === "number" && Number.isInteger(record.port) && record.port > 0 ? record.port : DEFAULT_DEV_SERVER_PORT;
509
+ return { url: record.url, token: record.token, localPort };
510
+ }
511
+ function sameTunnelConfig(a, b) {
512
+ if (a === null || b === null) return a === b;
513
+ return a.url === b.url && a.token === b.token && a.localPort === b.localPort;
514
+ }
515
+
516
+ // src/tunnel/manager.ts
517
+ var CONFIG_POLL_MS = 2e3;
518
+ var RECONNECT_MIN_MS = 1e3;
519
+ var RECONNECT_MAX_MS = 3e4;
520
+ function defaultCreateClient(config, events) {
521
+ return new TunnelClient({
522
+ url: config.url,
523
+ token: config.token,
524
+ localPort: config.localPort,
525
+ events
526
+ });
527
+ }
528
+ function startTunnelRuntime(cwd, options = {}) {
529
+ const pollIntervalMs = options.pollIntervalMs ?? CONFIG_POLL_MS;
530
+ const createClient = options.createClient ?? defaultCreateClient;
531
+ const onStatus = options.onStatus ?? ((message) => info(dim(message)));
532
+ let stopped = false;
533
+ let generation = 0;
534
+ let activeConfig = null;
535
+ let activeClient = null;
536
+ let reconnectDelay = RECONNECT_MIN_MS;
537
+ let reconnectTimer = null;
538
+ let polling = false;
539
+ const clearReconnect = () => {
540
+ if (reconnectTimer !== null) {
541
+ clearTimeout(reconnectTimer);
542
+ reconnectTimer = null;
543
+ }
544
+ };
545
+ const startClient = (config) => {
546
+ generation += 1;
547
+ const myGeneration = generation;
548
+ const client = createClient(config, {
549
+ onConnected: () => {
550
+ if (stopped || myGeneration !== generation) return;
551
+ reconnectDelay = RECONNECT_MIN_MS;
552
+ onStatus("(preview tunnel: connected)");
553
+ },
554
+ onClosed: (reason) => {
555
+ if (stopped || myGeneration !== generation) return;
556
+ onStatus(`(preview tunnel: disconnected \u2014 ${reason})`);
557
+ scheduleReconnect();
558
+ }
559
+ });
560
+ activeConfig = config;
561
+ activeClient = client;
562
+ client.start();
563
+ };
564
+ const retireClient = () => {
565
+ generation += 1;
566
+ activeClient?.stop();
567
+ activeClient = null;
568
+ };
569
+ const scheduleReconnect = () => {
570
+ if (reconnectTimer !== null) return;
571
+ const delay = reconnectDelay + Math.floor(Math.random() * (reconnectDelay / 4));
572
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
573
+ reconnectTimer = setTimeout(() => {
574
+ reconnectTimer = null;
575
+ if (stopped || activeConfig === null) return;
576
+ retireClient();
577
+ startClient(activeConfig);
578
+ }, delay);
579
+ };
580
+ const stopClient = () => {
581
+ clearReconnect();
582
+ retireClient();
583
+ activeConfig = null;
584
+ };
585
+ const poll = async () => {
586
+ if (polling || stopped) return;
587
+ polling = true;
588
+ try {
589
+ const config = await readTunnelConfig(cwd, options.env);
590
+ if (stopped || sameTunnelConfig(config, activeConfig)) return;
591
+ stopClient();
592
+ if (config) {
593
+ onStatus("(preview tunnel: credentials found, connecting)");
594
+ reconnectDelay = RECONNECT_MIN_MS;
595
+ startClient(config);
596
+ } else {
597
+ onStatus("(preview tunnel: credentials removed, stopped)");
598
+ }
599
+ } finally {
600
+ polling = false;
601
+ }
602
+ };
603
+ const interval = setInterval(() => {
604
+ void poll();
605
+ }, pollIntervalMs);
606
+ void poll();
607
+ return () => {
608
+ if (stopped) return;
609
+ stopped = true;
610
+ clearInterval(interval);
611
+ stopClient();
612
+ };
613
+ }
614
+
615
+ // src/commands/dev.ts
616
+ var FLAG_REMOTE = "remote";
617
+ var PACKAGE_JSON = "package.json";
618
+ var ENV_LOCAL_FILE = ".env.local";
619
+ var PUBLIC_API_URL_KEY = "NEXT_PUBLIC_CIMPLIFY_API_URL";
620
+ var SCRIPT_DEV = "dev";
621
+ var PREVIEW_ORIGIN_ENV = "CIMPLIFY_PREVIEW_ORIGIN";
622
+ async function applyPreviewOrigin(cwd, origin) {
623
+ const { ensurePreviewOrigin } = await import('./preview-config-CHDYYWUY.mjs');
624
+ try {
625
+ const result = await ensurePreviewOrigin(cwd, origin);
626
+ if (result.status === "unrecognized") {
627
+ info(
628
+ dim(
629
+ `(preview: could not resolve ${result.file} config shape; hot-reload may be limited)`
630
+ )
631
+ );
632
+ }
633
+ } catch (err) {
634
+ info(dim(`(preview: dev-origin setup skipped: ${err.message})`));
635
+ }
636
+ }
637
+ var PM_BUN = "bun";
638
+ var PM_PNPM = "pnpm";
639
+ var PM_YARN = "yarn";
640
+ var PM_NPM = "npm";
641
+ var LOCKFILE_BUN = "bun.lock";
642
+ var LOCKFILE_BUN_BINARY = "bun.lockb";
643
+ var LOCKFILE_PNPM = "pnpm-lock.yaml";
644
+ var LOCKFILE_YARN = "yarn.lock";
645
+ var BUNFIG = "bunfig.toml";
646
+ function envEndpoint(businessId, projectId) {
647
+ return `/v1/businesses/${encodeURIComponent(businessId)}/projects/${encodeURIComponent(projectId)}/env-vars`;
648
+ }
649
+ async function detectPackageManager(cwd) {
650
+ if (await pathExists(path2.join(cwd, LOCKFILE_BUN))) return PM_BUN;
651
+ if (await pathExists(path2.join(cwd, LOCKFILE_BUN_BINARY))) return PM_BUN;
652
+ if (await pathExists(path2.join(cwd, BUNFIG))) return PM_BUN;
653
+ if (await pathExists(path2.join(cwd, LOCKFILE_PNPM))) return PM_PNPM;
654
+ if (await pathExists(path2.join(cwd, LOCKFILE_YARN))) return PM_YARN;
655
+ return PM_NPM;
656
+ }
657
+ async function pathExists(target) {
658
+ try {
659
+ await promises.stat(target);
660
+ return true;
661
+ } catch (err) {
662
+ if (err.code === "ENOENT") return false;
663
+ throw err;
664
+ }
665
+ }
666
+ async function readPackageJson(cwd) {
667
+ const pkgPath = path2.join(cwd, PACKAGE_JSON);
668
+ let raw;
669
+ try {
670
+ raw = await promises.readFile(pkgPath, "utf8");
671
+ } catch (err) {
672
+ if (err.code === "ENOENT") {
673
+ throw new CliError(
674
+ CLI_ERROR_CODE.INVALID_INPUT,
675
+ `No package.json in ${cwd}. \`cimplify dev\` runs your project's dev script.`
676
+ );
677
+ }
678
+ throw err;
679
+ }
680
+ return JSON.parse(raw);
681
+ }
682
+ function mergeEnvEntries(existing, incoming) {
683
+ const seen = new Set(existing.map((e) => e.key));
684
+ const merged = [...existing];
685
+ for (const entry of incoming) {
686
+ if (seen.has(entry.key)) continue;
687
+ merged.push(entry);
688
+ seen.add(entry.key);
689
+ }
690
+ return merged;
691
+ }
692
+ async function applyRemoteEnv(client, businessId, projectId, apiBaseUrl, cwd) {
693
+ const records = await client.get(envEndpoint(businessId, projectId), {
694
+ query: { scope: ENV_SCOPE.PRODUCTION }
695
+ });
696
+ const incoming = records.filter((r) => r.value !== null && !r.is_secret).map((r) => ({ key: r.key, value: r.value }));
697
+ incoming.push({ key: PUBLIC_API_URL_KEY, value: apiBaseUrl });
698
+ const envPath = path2.join(cwd, ENV_LOCAL_FILE);
699
+ let existing = [];
700
+ try {
701
+ const raw = await promises.readFile(envPath, "utf8");
702
+ existing = parseEnvFile(raw);
703
+ } catch (err) {
704
+ if (err.code !== "ENOENT") throw err;
705
+ }
706
+ const merged = mergeEnvEntries(existing, incoming);
707
+ await promises.writeFile(envPath, formatEnvFile(merged), "utf8");
708
+ const publicCount = incoming.filter((e) => e.key.startsWith(PUBLIC_ENV_PREFIX)).length;
709
+ return publicCount;
710
+ }
711
+ async function run(argv) {
712
+ const args = parseArgs(argv);
713
+ const remote = flagBool(args, FLAG_REMOTE);
714
+ const cwd = process.cwd();
715
+ const previewOrigin = process.env[PREVIEW_ORIGIN_ENV];
716
+ if (remote) {
717
+ const auth = await readAuth();
718
+ const link = await readProjectLink();
719
+ const client = ApiClient.fromAuth(auth);
720
+ const publicCount = await applyRemoteEnv(
721
+ client,
722
+ link.businessId,
723
+ link.projectId,
724
+ auth.apiBaseUrl,
725
+ cwd
726
+ );
727
+ const label = link.businessHandle ?? auth.businessHandle ?? auth.businessId;
728
+ info(`Dev mode: connected to ${label} production data (${publicCount} public var${publicCount === 1 ? "" : "s"} merged)`);
729
+ } else if (previewOrigin) {
730
+ info("Dev mode: hosted preview");
731
+ } else {
732
+ info("Dev mode: using local mock (`cimplify mock`)");
733
+ }
734
+ const pkg = await readPackageJson(cwd);
735
+ if (!pkg.scripts || !pkg.scripts[SCRIPT_DEV]) {
736
+ throw new CliError(
737
+ CLI_ERROR_CODE.INVALID_INPUT,
738
+ `No "${SCRIPT_DEV}" script in package.json. Add one (e.g. "next dev") and re-run.`
739
+ );
740
+ }
741
+ let stopTunnel = null;
742
+ if (previewOrigin) {
743
+ await applyPreviewOrigin(cwd, previewOrigin);
744
+ stopTunnel = startTunnelRuntime(cwd);
745
+ }
746
+ const pm = await detectPackageManager(cwd);
747
+ info(dim(`(running: ${pm} run ${SCRIPT_DEV})`));
748
+ const child = spawn(pm, ["run", SCRIPT_DEV], { cwd, stdio: "inherit" });
749
+ try {
750
+ await new Promise((resolve, reject) => {
751
+ child.on("error", (err) => reject(new CliError(CLI_ERROR_CODE.INVALID_INPUT, `Failed to spawn ${pm}: ${err.message}`)));
752
+ child.on("close", (code) => {
753
+ process.exitCode = code ?? 0;
754
+ resolve();
755
+ });
756
+ });
757
+ } finally {
758
+ stopTunnel?.();
759
+ }
760
+ }
761
+
762
+ export { run as default, detectPackageManager, mergeEnvEntries };