@cimplify/cli 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { gitDetectRoot, gitCurrentBranch, gitCurrentSha, gitStatusPorcelain } from './chunk-K5464A3L.mjs';
3
3
  import { parseEnvFile } from './chunk-DBZ3UOQ2.mjs';
4
- import { package_default } from './chunk-W64GWXPF.mjs';
4
+ import { package_default } from './chunk-PVHB2MHV.mjs';
5
5
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
6
6
  import { readAuthOrNull, readProjectLinkOrNull, readProjectState } from './chunk-UBAI443T.mjs';
7
7
  import { bold, dim, yellow, green, info, result, red } from './chunk-E2T2SBP5.mjs';
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@cimplify/cli",
5
- version: "0.12.0",
5
+ version: "0.12.2",
6
6
  description: "Cimplify CLI \u2014 deploy, manage env vars, link projects, and scaffold storefronts",
7
7
  keywords: [
8
8
  "cimplify",
@@ -6,8 +6,8 @@ import { ApiClient } from './chunk-F4S5KETY.mjs';
6
6
  import { readAuth, readProjectLink } from './chunk-UBAI443T.mjs';
7
7
  import { info, CliError, CLI_ERROR_CODE, dim } from './chunk-E2T2SBP5.mjs';
8
8
  import { spawn } from 'child_process';
9
- import { promises } from 'fs';
10
- import path2 from 'path';
9
+ import { promises, watch } from 'fs';
10
+ import path from 'path';
11
11
  import http from 'http';
12
12
 
13
13
  // src/tunnel/v2/codec.ts
@@ -367,6 +367,410 @@ function sendableCloseCode(code) {
367
367
  return legal ? code : void 0;
368
368
  }
369
369
 
370
+ // src/tunnel/v2/control-protocol.ts
371
+ var CONTROL_KIND = "control";
372
+ var LENGTH_PREFIX_BYTES = 4;
373
+ var MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
374
+ var CONTROL_OP = {
375
+ READ: "read",
376
+ WRITE: "write",
377
+ DELETE: "delete",
378
+ LIST: "list",
379
+ LIST_TREE: "list_tree",
380
+ STAT: "stat",
381
+ MKDIR: "mkdir",
382
+ RENAME: "rename",
383
+ WATCH_START: "watch_start",
384
+ WATCH_STOP: "watch_stop",
385
+ EXEC: "exec"
386
+ };
387
+ var CONTROL_ERROR = {
388
+ NOT_FOUND: "not_found",
389
+ INVALID_PATH: "invalid_path",
390
+ PERMISSION_DENIED: "permission_denied",
391
+ IO: "io",
392
+ UNSUPPORTED: "unsupported"};
393
+ var WATCH_EVENT = {
394
+ ADD: "add",
395
+ CHANGE: "change",
396
+ REMOVE: "remove",
397
+ RESYNC: "resync"
398
+ };
399
+ var ENCODING = {
400
+ UTF8: "utf8",
401
+ BASE64: "base64"
402
+ };
403
+ var ControlProtocolError = class extends Error {
404
+ };
405
+ var textEncoder3 = new TextEncoder();
406
+ var textDecoder2 = new TextDecoder();
407
+ function encodeMessage(message) {
408
+ const json = textEncoder3.encode(JSON.stringify(message));
409
+ if (json.length > MAX_MESSAGE_BYTES) {
410
+ throw new ControlProtocolError(`control message ${json.length} exceeds ${MAX_MESSAGE_BYTES}`);
411
+ }
412
+ const framed = new Uint8Array(LENGTH_PREFIX_BYTES + json.length);
413
+ new DataView(framed.buffer).setUint32(0, json.length);
414
+ framed.set(json, LENGTH_PREFIX_BYTES);
415
+ return framed;
416
+ }
417
+ function isRequest(message) {
418
+ return typeof message.op === "string";
419
+ }
420
+ var MessageFramer = class {
421
+ // Annotated Uint8Array (ArrayBufferLike) so concat/subarray/slice results
422
+ // assign back cleanly under TS 5.7's Uint8Array<ArrayBuffer> generic.
423
+ buffer = new Uint8Array(0);
424
+ push(bytes) {
425
+ this.buffer = concat(this.buffer, bytes);
426
+ const messages = [];
427
+ for (; ; ) {
428
+ if (this.buffer.length < LENGTH_PREFIX_BYTES) break;
429
+ const length = new DataView(
430
+ this.buffer.buffer,
431
+ this.buffer.byteOffset,
432
+ LENGTH_PREFIX_BYTES
433
+ ).getUint32(0);
434
+ if (length > MAX_MESSAGE_BYTES) {
435
+ throw new ControlProtocolError(`framed length ${length} exceeds ${MAX_MESSAGE_BYTES}`);
436
+ }
437
+ if (this.buffer.length < LENGTH_PREFIX_BYTES + length) break;
438
+ const jsonBytes = this.buffer.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + length);
439
+ messages.push(JSON.parse(textDecoder2.decode(jsonBytes)));
440
+ this.buffer = this.buffer.subarray(LENGTH_PREFIX_BYTES + length);
441
+ }
442
+ if (this.buffer.byteOffset > 0) this.buffer = this.buffer.slice();
443
+ return messages;
444
+ }
445
+ };
446
+ function concat(a, b) {
447
+ if (a.length === 0) return b;
448
+ if (b.length === 0) return a;
449
+ const out = new Uint8Array(a.length + b.length);
450
+ out.set(a, 0);
451
+ out.set(b, a.length);
452
+ return out;
453
+ }
454
+
455
+ // src/tunnel/v2/control-agent.ts
456
+ var DEFAULT_IGNORED = /* @__PURE__ */ new Set([
457
+ "node_modules",
458
+ ".git",
459
+ ".next",
460
+ "dist",
461
+ "build",
462
+ ".turbo",
463
+ ".cache",
464
+ ".cimplify"
465
+ ]);
466
+ var ControlFsError = class extends Error {
467
+ constructor(code, message) {
468
+ super(message);
469
+ this.code = code;
470
+ }
471
+ code;
472
+ };
473
+ var ControlKind = class {
474
+ constructor(deps) {
475
+ this.deps = deps;
476
+ }
477
+ deps;
478
+ kind = CONTROL_KIND;
479
+ handle(incoming) {
480
+ void new ControlSession(incoming.stream, this.deps).run();
481
+ }
482
+ };
483
+ var ControlSession = class {
484
+ constructor(stream, deps) {
485
+ this.stream = stream;
486
+ this.deps = deps;
487
+ }
488
+ stream;
489
+ deps;
490
+ framer = new MessageFramer();
491
+ watch = null;
492
+ epoch = 0;
493
+ seq = 0;
494
+ // Serialize all writes so message bytes never interleave and event order
495
+ // (which carries seq) is preserved.
496
+ sending = Promise.resolve();
497
+ async run() {
498
+ try {
499
+ for (; ; ) {
500
+ const event = await this.stream.recv();
501
+ if (event === null) return;
502
+ if (event.type === "data") {
503
+ for (const message of this.framer.push(event.payload)) {
504
+ if (isRequest(message)) void this.dispatch(message);
505
+ }
506
+ } else if (event.type === "end") {
507
+ continue;
508
+ } else {
509
+ return;
510
+ }
511
+ }
512
+ } finally {
513
+ this.watch?.stop();
514
+ }
515
+ }
516
+ send(message) {
517
+ const bytes = encodeMessage(message);
518
+ this.sending = this.sending.then(() => this.stream.data(bytes).catch(() => {
519
+ }));
520
+ }
521
+ async dispatch(request) {
522
+ try {
523
+ const data = await this.execute(request);
524
+ this.send({ id: request.id, ok: true, data });
525
+ } catch (error) {
526
+ const code = error instanceof ControlFsError ? error.code : CONTROL_ERROR.IO;
527
+ const message = error instanceof Error ? error.message : String(error);
528
+ this.send({ id: request.id, ok: false, error: { code, message } });
529
+ }
530
+ }
531
+ async execute(request) {
532
+ const { fs: backend } = this.deps;
533
+ switch (request.op) {
534
+ case CONTROL_OP.READ:
535
+ return backend.read(str(request, FIELD.PATH));
536
+ case CONTROL_OP.WRITE:
537
+ await backend.write(str(request, FIELD.PATH), str(request, FIELD.CONTENT), enc(request));
538
+ return {};
539
+ case CONTROL_OP.DELETE:
540
+ await backend.delete(str(request, FIELD.PATH), request.recursive === true);
541
+ return {};
542
+ case CONTROL_OP.LIST:
543
+ return { entries: await backend.list(str(request, FIELD.PATH)) };
544
+ case CONTROL_OP.LIST_TREE:
545
+ return { paths: await backend.listTree() };
546
+ case CONTROL_OP.STAT:
547
+ return await backend.stat(str(request, FIELD.PATH));
548
+ case CONTROL_OP.MKDIR:
549
+ await backend.mkdir(str(request, FIELD.PATH));
550
+ return {};
551
+ case CONTROL_OP.RENAME:
552
+ await backend.rename(str(request, FIELD.FROM), str(request, FIELD.TO));
553
+ return {};
554
+ case CONTROL_OP.WATCH_START:
555
+ return this.startWatch(request);
556
+ case CONTROL_OP.WATCH_STOP:
557
+ this.watch?.stop();
558
+ this.watch = null;
559
+ return {};
560
+ case CONTROL_OP.EXEC:
561
+ return this.deps.exec.run(execCommand(request));
562
+ default:
563
+ throw new ControlFsError(CONTROL_ERROR.UNSUPPORTED, `unknown op ${request.op}`);
564
+ }
565
+ }
566
+ startWatch(request) {
567
+ this.watch?.stop();
568
+ this.epoch += 1;
569
+ this.seq = 0;
570
+ const epoch = this.epoch;
571
+ const paths = Array.isArray(request.paths) ? request.paths : [WORKSPACE_ROOT];
572
+ this.watch = this.deps.watcher.start(
573
+ paths,
574
+ (type, changed) => {
575
+ if (this.epoch !== epoch) return;
576
+ this.seq += 1;
577
+ this.send({ event: type, paths: changed, seq: this.seq, epoch });
578
+ },
579
+ () => {
580
+ if (this.epoch !== epoch) return;
581
+ this.send({ event: WATCH_EVENT.RESYNC, epoch });
582
+ }
583
+ );
584
+ return { epoch };
585
+ }
586
+ };
587
+ var WORKSPACE_ROOT = ".";
588
+ var FIELD = {
589
+ PATH: "path",
590
+ CONTENT: "content",
591
+ FROM: "from",
592
+ TO: "to"
593
+ };
594
+ function str(request, field) {
595
+ const value = request[field];
596
+ if (typeof value !== "string") {
597
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `missing string field '${field}'`);
598
+ }
599
+ return value;
600
+ }
601
+ function enc(request) {
602
+ return request.encoding === ENCODING.BASE64 ? ENCODING.BASE64 : ENCODING.UTF8;
603
+ }
604
+ function execCommand(request) {
605
+ const command = request.command;
606
+ if (typeof command === "string") return command;
607
+ if (Array.isArray(command) && command.every((part) => typeof part === "string")) {
608
+ return command;
609
+ }
610
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, "exec requires a command");
611
+ }
612
+ function resolveInRoot(root, rel) {
613
+ const resolved = path.resolve(root, rel);
614
+ const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
615
+ if (resolved !== root && !resolved.startsWith(rootWithSep)) {
616
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `path escapes workspace: ${rel}`);
617
+ }
618
+ return resolved;
619
+ }
620
+ function isIgnored(rel) {
621
+ return rel.split(/[\\/]/).some((segment) => DEFAULT_IGNORED.has(segment));
622
+ }
623
+ var NodeControlFs = class {
624
+ constructor(root) {
625
+ this.root = root;
626
+ }
627
+ root;
628
+ async read(rel) {
629
+ const abs = resolveInRoot(this.root, rel);
630
+ const bytes = await promises.readFile(abs).catch(rethrow(rel));
631
+ const text = tryDecodeUtf8(bytes);
632
+ return text !== null ? { content: text, encoding: ENCODING.UTF8 } : { content: Buffer.from(bytes).toString("base64"), encoding: ENCODING.BASE64 };
633
+ }
634
+ async write(rel, content, encoding) {
635
+ const abs = resolveInRoot(this.root, rel);
636
+ await promises.mkdir(path.dirname(abs), { recursive: true });
637
+ const bytes = encoding === ENCODING.BASE64 ? Buffer.from(content, "base64") : Buffer.from(content, "utf8");
638
+ await promises.writeFile(abs, bytes).catch(rethrow(rel));
639
+ }
640
+ async delete(rel, recursive) {
641
+ const abs = resolveInRoot(this.root, rel);
642
+ await promises.rm(abs, { recursive, force: true });
643
+ }
644
+ async list(rel) {
645
+ const abs = resolveInRoot(this.root, rel);
646
+ const dirents = await promises.readdir(abs, { withFileTypes: true }).catch(rethrow(rel));
647
+ return dirents.filter((d) => !DEFAULT_IGNORED.has(d.name)).map((d) => ({ name: d.name, dir: d.isDirectory() }));
648
+ }
649
+ async listTree() {
650
+ const out = [];
651
+ const walk = async (dir) => {
652
+ const dirents = await promises.readdir(path.join(this.root, dir), { withFileTypes: true });
653
+ for (const dirent of dirents) {
654
+ if (DEFAULT_IGNORED.has(dirent.name)) continue;
655
+ const rel = dir ? `${dir}/${dirent.name}` : dirent.name;
656
+ if (dirent.isDirectory()) await walk(rel);
657
+ else out.push(rel);
658
+ }
659
+ };
660
+ await walk("");
661
+ return out;
662
+ }
663
+ async stat(rel) {
664
+ const abs = resolveInRoot(this.root, rel);
665
+ const stat = await promises.stat(abs).catch(rethrow(rel));
666
+ return {
667
+ kind: stat.isDirectory() ? "dir" : "file",
668
+ size: stat.size,
669
+ mtimeMs: Math.floor(stat.mtimeMs)
670
+ };
671
+ }
672
+ async mkdir(rel) {
673
+ await promises.mkdir(resolveInRoot(this.root, rel), { recursive: true });
674
+ }
675
+ async rename(from, to) {
676
+ const absFrom = resolveInRoot(this.root, from);
677
+ const absTo = resolveInRoot(this.root, to);
678
+ await promises.mkdir(path.dirname(absTo), { recursive: true });
679
+ await promises.rename(absFrom, absTo).catch(rethrow(from));
680
+ }
681
+ };
682
+ var WATCH_DEBOUNCE_MS = 60;
683
+ var NodeControlWatcher = class {
684
+ constructor(root) {
685
+ this.root = root;
686
+ }
687
+ root;
688
+ start(_relPaths, onEvent, onResync) {
689
+ let watcher;
690
+ const pending = /* @__PURE__ */ new Map();
691
+ const flush = (rel) => {
692
+ pending.delete(rel);
693
+ promises.stat(path.join(this.root, rel)).then(
694
+ (stat) => onEvent(stat.isDirectory() ? WATCH_EVENT.ADD : WATCH_EVENT.CHANGE, [rel]),
695
+ () => onEvent(WATCH_EVENT.REMOVE, [rel])
696
+ );
697
+ };
698
+ try {
699
+ watcher = watch(this.root, { recursive: true }, (_type, filename) => {
700
+ if (filename === null) {
701
+ onResync();
702
+ return;
703
+ }
704
+ const rel = filename.toString().split(path.sep).join("/");
705
+ if (isIgnored(rel)) return;
706
+ const existing = pending.get(rel);
707
+ if (existing) clearTimeout(existing);
708
+ pending.set(
709
+ rel,
710
+ setTimeout(() => flush(rel), WATCH_DEBOUNCE_MS)
711
+ );
712
+ });
713
+ } catch (error) {
714
+ onResync();
715
+ return { stop: () => {
716
+ } };
717
+ }
718
+ watcher.on("error", () => onResync());
719
+ return {
720
+ stop: () => {
721
+ for (const timer of pending.values()) clearTimeout(timer);
722
+ pending.clear();
723
+ watcher.close();
724
+ }
725
+ };
726
+ }
727
+ };
728
+ var EXEC_MAX_BUFFER = 8 * 1024 * 1024;
729
+ var NodeControlExec = class {
730
+ constructor(root) {
731
+ this.root = root;
732
+ }
733
+ root;
734
+ run(command) {
735
+ const [file, args, shell] = Array.isArray(command) ? [command[0], command.slice(1), false] : [command, [], true];
736
+ return new Promise((resolve) => {
737
+ const child = spawn(file, args, { cwd: this.root, shell, env: process.env });
738
+ let stdout = "";
739
+ let stderr = "";
740
+ let truncated = false;
741
+ const cap = (existing, chunk) => {
742
+ if (existing.length >= EXEC_MAX_BUFFER) {
743
+ truncated = true;
744
+ return existing;
745
+ }
746
+ return existing + chunk.toString("utf8");
747
+ };
748
+ child.stdout?.on("data", (chunk) => stdout = cap(stdout, chunk));
749
+ child.stderr?.on("data", (chunk) => stderr = cap(stderr, chunk));
750
+ child.on("error", (error) => resolve({ stdout, stderr: error.message, exitCode: 127 }));
751
+ child.on("close", (code) => {
752
+ if (truncated) stderr += "\n[output truncated]";
753
+ resolve({ stdout, stderr, exitCode: code ?? 0 });
754
+ });
755
+ });
756
+ }
757
+ };
758
+ function tryDecodeUtf8(bytes) {
759
+ try {
760
+ const text = new TextDecoder("utf8", { fatal: true }).decode(bytes);
761
+ return text;
762
+ } catch {
763
+ return null;
764
+ }
765
+ }
766
+ function rethrow(rel) {
767
+ return (error) => {
768
+ if (error.code === "ENOENT") throw new ControlFsError(CONTROL_ERROR.NOT_FOUND, `not found: ${rel}`);
769
+ if (error.code === "EACCES") throw new ControlFsError(CONTROL_ERROR.PERMISSION_DENIED, rel);
770
+ throw new ControlFsError(CONTROL_ERROR.IO, error.message);
771
+ };
772
+ }
773
+
370
774
  // src/tunnel/protocol.ts
371
775
  var MAX_PAYLOAD = 262144;
372
776
  var HEADER_LEN2 = 9;
@@ -419,13 +823,13 @@ function decodeFrame(bytes) {
419
823
  }
420
824
  return { streamId, frameType: rawType, payload: bytes.subarray(HEADER_LEN2) };
421
825
  }
422
- var textEncoder3 = new TextEncoder();
423
- var textDecoder2 = new TextDecoder();
826
+ var textEncoder4 = new TextEncoder();
827
+ var textDecoder3 = new TextDecoder();
424
828
  function encodeJsonPayload(value) {
425
- return textEncoder3.encode(JSON.stringify(value));
829
+ return textEncoder4.encode(JSON.stringify(value));
426
830
  }
427
831
  function decodeJsonPayload(payload) {
428
- return JSON.parse(textDecoder2.decode(payload));
832
+ return JSON.parse(textDecoder3.decode(payload));
429
833
  }
430
834
  function* chunkPayload(bytes) {
431
835
  if (bytes.length === 0) {
@@ -454,8 +858,8 @@ var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
454
858
  "transfer-encoding",
455
859
  "upgrade"
456
860
  ]);
457
- var textEncoder4 = new TextEncoder();
458
- var textDecoder3 = new TextDecoder();
861
+ var textEncoder5 = new TextEncoder();
862
+ var textDecoder4 = new TextDecoder();
459
863
  function defaultSocketFactory(url, protocols) {
460
864
  const ctor = globalThis.WebSocket;
461
865
  if (!ctor) {
@@ -705,7 +1109,7 @@ var HttpProxyStream = class {
705
1109
  status: 502,
706
1110
  headers: [HEADER_CONTENT_TYPE_TEXT]
707
1111
  });
708
- this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode(`dev server unreachable: ${err.message}`));
1112
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode(`dev server unreachable: ${err.message}`));
709
1113
  this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
710
1114
  this.ctx.finish();
711
1115
  }
@@ -736,7 +1140,7 @@ var WsProxyStream = class {
736
1140
  status: 502,
737
1141
  headers: [HEADER_CONTENT_TYPE_TEXT]
738
1142
  });
739
- this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode("dev server refused the websocket"));
1143
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode("dev server refused the websocket"));
740
1144
  this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
741
1145
  this.ctx.finish();
742
1146
  });
@@ -748,7 +1152,7 @@ var WsProxyStream = class {
748
1152
  handleFrame(frame) {
749
1153
  switch (frame.frameType) {
750
1154
  case FRAME_TYPE.WS_TEXT:
751
- this.local.send(textDecoder3.decode(frame.payload));
1155
+ this.local.send(textDecoder4.decode(frame.payload));
752
1156
  break;
753
1157
  case FRAME_TYPE.WS_BIN:
754
1158
  this.local.send(frame.payload);
@@ -776,7 +1180,7 @@ var WsProxyStream = class {
776
1180
  let payload;
777
1181
  let frameType;
778
1182
  if (typeof data === "string") {
779
- payload = textEncoder4.encode(data);
1183
+ payload = textEncoder5.encode(data);
780
1184
  frameType = FRAME_TYPE.WS_TEXT;
781
1185
  } else if (data instanceof ArrayBuffer) {
782
1186
  payload = new Uint8Array(data);
@@ -902,8 +1306,11 @@ var StreamHandleV2 = class {
902
1306
  }
903
1307
  };
904
1308
  var CONNECTION_STREAM_ID = 0;
905
- var textEncoder5 = new TextEncoder();
906
- var textDecoder4 = new TextDecoder();
1309
+ var LIVENESS_PING_INTERVAL_MS = 15e3;
1310
+ var LIVENESS_TIMEOUT_MS = 45e3;
1311
+ var FROZEN_TICK_GAP_MS = 6e4;
1312
+ var textEncoder6 = new TextEncoder();
1313
+ var textDecoder5 = new TextDecoder();
907
1314
  var SessionV2 = class {
908
1315
  constructor(transport, options) {
909
1316
  this.transport = transport;
@@ -911,6 +1318,10 @@ var SessionV2 = class {
911
1318
  this.nextStreamId = options.role === "service" ? 1 : 2;
912
1319
  this.connectionSend = new SendWindow(0);
913
1320
  this.connectionRecv = new RecvWindow(options.local.connection_window);
1321
+ this.lastInboundAt = Date.now();
1322
+ this.lastTickAt = Date.now();
1323
+ this.livenessTimer = setInterval(() => this.onLivenessTick(), LIVENESS_PING_INTERVAL_MS);
1324
+ this.livenessTimer.unref?.();
914
1325
  transport.onMessage((bytes) => this.onMessage(bytes));
915
1326
  transport.onClose((reason) => this.teardown(reason));
916
1327
  }
@@ -925,9 +1336,36 @@ var SessionV2 = class {
925
1336
  draining = false;
926
1337
  closed = false;
927
1338
  sending = Promise.resolve();
1339
+ lastInboundAt;
1340
+ lastTickAt;
1341
+ livenessTimer;
1342
+ onLivenessTick() {
1343
+ if (this.closed) return;
1344
+ const now = Date.now();
1345
+ const tickGap = now - this.lastTickAt;
1346
+ this.lastTickAt = now;
1347
+ if (tickGap > FROZEN_TICK_GAP_MS) {
1348
+ this.abort(`process froze ${Math.round(tickGap / 1e3)}s (suspend/resume); redialing`);
1349
+ return;
1350
+ }
1351
+ if (now - this.lastInboundAt > LIVENESS_TIMEOUT_MS) {
1352
+ this.abort(`no inbound frames for ${Math.round((now - this.lastInboundAt) / 1e3)}s`);
1353
+ return;
1354
+ }
1355
+ void this.enqueueSend({
1356
+ streamId: CONNECTION_STREAM_ID,
1357
+ frameType: FRAME_TYPE_V2.PING,
1358
+ payload: new Uint8Array(0)
1359
+ });
1360
+ }
1361
+ /** Presumed-dead teardown: no farewell frames — the socket is a zombie. */
1362
+ abort(reason) {
1363
+ this.teardown(reason);
1364
+ this.transport.close(1001, reason.slice(0, 100));
1365
+ }
928
1366
  /** Send our HELLO; the peer's must arrive as its first frame. */
929
1367
  async start() {
930
- const payload = textEncoder5.encode(JSON.stringify(this.options.local));
1368
+ const payload = textEncoder6.encode(JSON.stringify(this.options.local));
931
1369
  await this.transport.send(
932
1370
  encodeFrameV2({ streamId: CONNECTION_STREAM_ID, frameType: FRAME_TYPE_V2.HELLO, payload })
933
1371
  );
@@ -981,6 +1419,7 @@ var SessionV2 = class {
981
1419
  this.fail(`undecodable frame: ${error.message}`);
982
1420
  return;
983
1421
  }
1422
+ this.lastInboundAt = Date.now();
984
1423
  if (!this.peer) {
985
1424
  this.onHandshakeFrame(frame);
986
1425
  return;
@@ -1153,7 +1592,7 @@ var SessionV2 = class {
1153
1592
  }
1154
1593
  }
1155
1594
  sendHead(streamId, frameType, value) {
1156
- const payload = textEncoder5.encode(JSON.stringify(value ?? null));
1595
+ const payload = textEncoder6.encode(JSON.stringify(value ?? null));
1157
1596
  if (payload.length > MAX_HEAD_BYTES) {
1158
1597
  return Promise.reject(new SessionV2Error("head exceeds MAX_HEAD_BYTES"));
1159
1598
  }
@@ -1268,6 +1707,7 @@ var SessionV2 = class {
1268
1707
  teardown(reason) {
1269
1708
  if (this.closed) return;
1270
1709
  this.closed = true;
1710
+ clearInterval(this.livenessTimer);
1271
1711
  for (const streamId of this.streams.keys()) {
1272
1712
  this.dropStream(streamId);
1273
1713
  }
@@ -1277,7 +1717,7 @@ var SessionV2 = class {
1277
1717
  function parseJson(payload) {
1278
1718
  if (payload.length === 0) return null;
1279
1719
  try {
1280
- return JSON.parse(textDecoder4.decode(payload));
1720
+ return JSON.parse(textDecoder5.decode(payload));
1281
1721
  } catch {
1282
1722
  return null;
1283
1723
  }
@@ -1373,7 +1813,13 @@ var NegotiatingAgentClient = class {
1373
1813
  const hello = defaultHello();
1374
1814
  const transport = new WsTransportV2(socket);
1375
1815
  const createLocalSocket = this.options.createLocalSocket ?? defaultSocketFactory;
1376
- const dispatcher = new AgentDispatcher().register(new HttpProxyKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(new WsSpliceKind(makeAgentContext(this.options.localPort, hello, createLocalSocket)));
1816
+ const dispatcher = new AgentDispatcher().register(new HttpProxyKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(new WsSpliceKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(
1817
+ new ControlKind({
1818
+ fs: new NodeControlFs(this.options.workspaceRoot),
1819
+ watcher: new NodeControlWatcher(this.options.workspaceRoot),
1820
+ exec: new NodeControlExec(this.options.workspaceRoot)
1821
+ })
1822
+ );
1377
1823
  const session = new SessionV2(transport, {
1378
1824
  role: "agent",
1379
1825
  local: hello,
@@ -1412,7 +1858,7 @@ var TUNNEL_CONFIG_DIR = ".cimplify";
1412
1858
  var TUNNEL_CONFIG_FILE = "tunnel.json";
1413
1859
  var DEFAULT_DEV_SERVER_PORT = 3e3;
1414
1860
  function tunnelConfigPath(cwd) {
1415
- return path2.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
1861
+ return path.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
1416
1862
  }
1417
1863
  async function readTunnelConfig(cwd, env = process.env) {
1418
1864
  const url = env[TUNNEL_URL_ENV];
@@ -1461,17 +1907,18 @@ var CONFIG_POLL_MS = 2e3;
1461
1907
  var RECONNECT_MIN_MS = 1e3;
1462
1908
  var RECONNECT_MAX_MS = 3e4;
1463
1909
  var CLOSE_CODE_REPLACED = 4e3;
1464
- function defaultCreateClient(config, events) {
1465
- return new NegotiatingAgentClient({
1910
+ function makeDefaultCreateClient(workspaceRoot) {
1911
+ return (config, events) => new NegotiatingAgentClient({
1466
1912
  url: config.url,
1467
1913
  token: config.token,
1468
1914
  localPort: config.localPort,
1915
+ workspaceRoot,
1469
1916
  events
1470
1917
  });
1471
1918
  }
1472
1919
  function startTunnelRuntime(cwd, options = {}) {
1473
1920
  const pollIntervalMs = options.pollIntervalMs ?? CONFIG_POLL_MS;
1474
- const createClient = options.createClient ?? defaultCreateClient;
1921
+ const createClient = options.createClient ?? makeDefaultCreateClient(cwd);
1475
1922
  const onStatus = options.onStatus ?? ((message) => info(dim(message)));
1476
1923
  let stopped = false;
1477
1924
  let generation = 0;
@@ -1609,11 +2056,11 @@ function envEndpoint(businessId, projectId) {
1609
2056
  return `/v1/businesses/${encodeURIComponent(businessId)}/projects/${encodeURIComponent(projectId)}/env-vars`;
1610
2057
  }
1611
2058
  async function detectPackageManager(cwd) {
1612
- if (await pathExists(path2.join(cwd, LOCKFILE_BUN))) return PM_BUN;
1613
- if (await pathExists(path2.join(cwd, LOCKFILE_BUN_BINARY))) return PM_BUN;
1614
- if (await pathExists(path2.join(cwd, BUNFIG))) return PM_BUN;
1615
- if (await pathExists(path2.join(cwd, LOCKFILE_PNPM))) return PM_PNPM;
1616
- if (await pathExists(path2.join(cwd, LOCKFILE_YARN))) return PM_YARN;
2059
+ if (await pathExists(path.join(cwd, LOCKFILE_BUN))) return PM_BUN;
2060
+ if (await pathExists(path.join(cwd, LOCKFILE_BUN_BINARY))) return PM_BUN;
2061
+ if (await pathExists(path.join(cwd, BUNFIG))) return PM_BUN;
2062
+ if (await pathExists(path.join(cwd, LOCKFILE_PNPM))) return PM_PNPM;
2063
+ if (await pathExists(path.join(cwd, LOCKFILE_YARN))) return PM_YARN;
1617
2064
  return PM_NPM;
1618
2065
  }
1619
2066
  async function pathExists(target) {
@@ -1626,7 +2073,7 @@ async function pathExists(target) {
1626
2073
  }
1627
2074
  }
1628
2075
  async function readPackageJson(cwd) {
1629
- const pkgPath = path2.join(cwd, PACKAGE_JSON);
2076
+ const pkgPath = path.join(cwd, PACKAGE_JSON);
1630
2077
  let raw;
1631
2078
  try {
1632
2079
  raw = await promises.readFile(pkgPath, "utf8");
@@ -1657,7 +2104,7 @@ async function applyRemoteEnv(client, businessId, projectId, apiBaseUrl, cwd) {
1657
2104
  });
1658
2105
  const incoming = records.filter((r) => r.value !== null && !r.is_secret).map((r) => ({ key: r.key, value: r.value }));
1659
2106
  incoming.push({ key: PUBLIC_API_URL_KEY, value: apiBaseUrl });
1660
- const envPath = path2.join(cwd, ENV_LOCAL_FILE);
2107
+ const envPath = path.join(cwd, ENV_LOCAL_FILE);
1661
2108
  let existing = [];
1662
2109
  try {
1663
2110
  const raw = await promises.readFile(envPath, "utf8");
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-W64GWXPF.mjs';
2
+ import { package_default } from './chunk-PVHB2MHV.mjs';
3
3
  import { TEMPLATES } from './chunk-W3WLADJP.mjs';
4
4
 
5
5
  // src/dispatcher.ts
@@ -142,18 +142,18 @@ var COMMANDS = {
142
142
  domains: () => import('./domains-H3K2WW55.mjs'),
143
143
  logs: () => import('./logs-2ECJQ3MU.mjs'),
144
144
  status: () => import('./status-IA7PJ4TV.mjs'),
145
- dev: () => import('./dev-3TPDHU67.mjs'),
145
+ dev: () => import('./dev-5MB2BJ6L.mjs'),
146
146
  mock: () => import('./mock-TSUKCLWU.mjs'),
147
- introspect: () => import('./introspect-TB2QFQMF.mjs'),
147
+ introspect: () => import('./introspect-YBDCOO3I.mjs'),
148
148
  inspect: () => import('./inspect-CGYX4DDF.mjs'),
149
- doctor: () => import('./doctor-JQYXARYQ.mjs'),
150
- explain: () => import('./explain-2I4MWMPY.mjs'),
149
+ doctor: () => import('./doctor-6Z6YR37N.mjs'),
150
+ explain: () => import('./explain-2FLJFJP3.mjs'),
151
151
  assets: () => import('./assets-4D7VXRUE.mjs'),
152
152
  repo: () => import('./repo-WSUAHXWO.mjs'),
153
153
  list: () => import('./list-27TS2L47.mjs'),
154
154
  add: () => import('./add-5ODVFWVR.mjs'),
155
- update: () => import('./update-T5O6YCIC.mjs'),
156
- upgrade: () => import('./update-T5O6YCIC.mjs'),
155
+ update: () => import('./update-VIDP5OKO.mjs'),
156
+ upgrade: () => import('./update-VIDP5OKO.mjs'),
157
157
  "auth-step-up": () => import('./auth-step-up-NOSM4PUR.mjs')
158
158
  };
159
159
  var COMMAND_PREFIXES = {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { gatherIntrospection } from './chunk-EL34CORK.mjs';
2
+ import { gatherIntrospection } from './chunk-7P2OGMZ4.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-W64GWXPF.mjs';
5
+ import './chunk-PVHB2MHV.mjs';
6
6
  import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
7
7
  import { SDK_ENV_API_URL, DEFAULT_API_BASE_URL, ApiClient, ENV_API_BASE_URL } from './chunk-F4S5KETY.mjs';
8
8
  import { readAuthOrNull } from './chunk-UBAI443T.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-W64GWXPF.mjs';
2
+ import { package_default } from './chunk-PVHB2MHV.mjs';
3
3
  import { parseArgs } from './chunk-C4M3DXKC.mjs';
4
4
  import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
5
5
 
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-EL34CORK.mjs';
2
+ export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-7P2OGMZ4.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-W64GWXPF.mjs';
5
+ import './chunk-PVHB2MHV.mjs';
6
6
  import './chunk-C4M3DXKC.mjs';
7
7
  import './chunk-UBAI443T.mjs';
8
8
  import './chunk-E2T2SBP5.mjs';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { package_default } from './chunk-W64GWXPF.mjs';
2
+ import { package_default } from './chunk-PVHB2MHV.mjs';
3
3
  import { promptYesNo } from './chunk-ITAFAORS.mjs';
4
4
  import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
5
5
  import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
5
5
  "keywords": [
6
6
  "cimplify",