@cimplify/cli 0.12.0 → 0.12.1

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.
@@ -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.1",
6
6
  description: "Cimplify CLI \u2014 deploy, manage env vars, link projects, and scaffold storefronts",
7
7
  keywords: [
8
8
  "cimplify",
@@ -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-FOJVWJ57.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';
@@ -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,408 @@ 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
+ buffer = new Uint8Array(0);
422
+ push(bytes) {
423
+ this.buffer = concat(this.buffer, bytes);
424
+ const messages = [];
425
+ for (; ; ) {
426
+ if (this.buffer.length < LENGTH_PREFIX_BYTES) break;
427
+ const length = new DataView(
428
+ this.buffer.buffer,
429
+ this.buffer.byteOffset,
430
+ LENGTH_PREFIX_BYTES
431
+ ).getUint32(0);
432
+ if (length > MAX_MESSAGE_BYTES) {
433
+ throw new ControlProtocolError(`framed length ${length} exceeds ${MAX_MESSAGE_BYTES}`);
434
+ }
435
+ if (this.buffer.length < LENGTH_PREFIX_BYTES + length) break;
436
+ const jsonBytes = this.buffer.subarray(LENGTH_PREFIX_BYTES, LENGTH_PREFIX_BYTES + length);
437
+ messages.push(JSON.parse(textDecoder2.decode(jsonBytes)));
438
+ this.buffer = this.buffer.subarray(LENGTH_PREFIX_BYTES + length);
439
+ }
440
+ if (this.buffer.byteOffset > 0) this.buffer = this.buffer.slice();
441
+ return messages;
442
+ }
443
+ };
444
+ function concat(a, b) {
445
+ if (a.length === 0) return b;
446
+ if (b.length === 0) return a;
447
+ const out = new Uint8Array(a.length + b.length);
448
+ out.set(a, 0);
449
+ out.set(b, a.length);
450
+ return out;
451
+ }
452
+
453
+ // src/tunnel/v2/control-agent.ts
454
+ var DEFAULT_IGNORED = /* @__PURE__ */ new Set([
455
+ "node_modules",
456
+ ".git",
457
+ ".next",
458
+ "dist",
459
+ "build",
460
+ ".turbo",
461
+ ".cache",
462
+ ".cimplify"
463
+ ]);
464
+ var ControlFsError = class extends Error {
465
+ constructor(code, message) {
466
+ super(message);
467
+ this.code = code;
468
+ }
469
+ code;
470
+ };
471
+ var ControlKind = class {
472
+ constructor(deps) {
473
+ this.deps = deps;
474
+ }
475
+ deps;
476
+ kind = CONTROL_KIND;
477
+ handle(incoming) {
478
+ void new ControlSession(incoming.stream, this.deps).run();
479
+ }
480
+ };
481
+ var ControlSession = class {
482
+ constructor(stream, deps) {
483
+ this.stream = stream;
484
+ this.deps = deps;
485
+ }
486
+ stream;
487
+ deps;
488
+ framer = new MessageFramer();
489
+ watch = null;
490
+ epoch = 0;
491
+ seq = 0;
492
+ // Serialize all writes so message bytes never interleave and event order
493
+ // (which carries seq) is preserved.
494
+ sending = Promise.resolve();
495
+ async run() {
496
+ try {
497
+ for (; ; ) {
498
+ const event = await this.stream.recv();
499
+ if (event === null) return;
500
+ if (event.type === "data") {
501
+ for (const message of this.framer.push(event.payload)) {
502
+ if (isRequest(message)) void this.dispatch(message);
503
+ }
504
+ } else if (event.type === "end") {
505
+ continue;
506
+ } else {
507
+ return;
508
+ }
509
+ }
510
+ } finally {
511
+ this.watch?.stop();
512
+ }
513
+ }
514
+ send(message) {
515
+ const bytes = encodeMessage(message);
516
+ this.sending = this.sending.then(() => this.stream.data(bytes).catch(() => {
517
+ }));
518
+ }
519
+ async dispatch(request) {
520
+ try {
521
+ const data = await this.execute(request);
522
+ this.send({ id: request.id, ok: true, data });
523
+ } catch (error) {
524
+ const code = error instanceof ControlFsError ? error.code : CONTROL_ERROR.IO;
525
+ const message = error instanceof Error ? error.message : String(error);
526
+ this.send({ id: request.id, ok: false, error: { code, message } });
527
+ }
528
+ }
529
+ async execute(request) {
530
+ const { fs: backend } = this.deps;
531
+ switch (request.op) {
532
+ case CONTROL_OP.READ:
533
+ return backend.read(str(request, FIELD.PATH));
534
+ case CONTROL_OP.WRITE:
535
+ await backend.write(str(request, FIELD.PATH), str(request, FIELD.CONTENT), enc(request));
536
+ return {};
537
+ case CONTROL_OP.DELETE:
538
+ await backend.delete(str(request, FIELD.PATH), request.recursive === true);
539
+ return {};
540
+ case CONTROL_OP.LIST:
541
+ return { entries: await backend.list(str(request, FIELD.PATH)) };
542
+ case CONTROL_OP.LIST_TREE:
543
+ return { paths: await backend.listTree() };
544
+ case CONTROL_OP.STAT:
545
+ return await backend.stat(str(request, FIELD.PATH));
546
+ case CONTROL_OP.MKDIR:
547
+ await backend.mkdir(str(request, FIELD.PATH));
548
+ return {};
549
+ case CONTROL_OP.RENAME:
550
+ await backend.rename(str(request, FIELD.FROM), str(request, FIELD.TO));
551
+ return {};
552
+ case CONTROL_OP.WATCH_START:
553
+ return this.startWatch(request);
554
+ case CONTROL_OP.WATCH_STOP:
555
+ this.watch?.stop();
556
+ this.watch = null;
557
+ return {};
558
+ case CONTROL_OP.EXEC:
559
+ return this.deps.exec.run(execCommand(request));
560
+ default:
561
+ throw new ControlFsError(CONTROL_ERROR.UNSUPPORTED, `unknown op ${request.op}`);
562
+ }
563
+ }
564
+ startWatch(request) {
565
+ this.watch?.stop();
566
+ this.epoch += 1;
567
+ this.seq = 0;
568
+ const epoch = this.epoch;
569
+ const paths = Array.isArray(request.paths) ? request.paths : [WORKSPACE_ROOT];
570
+ this.watch = this.deps.watcher.start(
571
+ paths,
572
+ (type, changed) => {
573
+ if (this.epoch !== epoch) return;
574
+ this.seq += 1;
575
+ this.send({ event: type, paths: changed, seq: this.seq, epoch });
576
+ },
577
+ () => {
578
+ if (this.epoch !== epoch) return;
579
+ this.send({ event: WATCH_EVENT.RESYNC, epoch });
580
+ }
581
+ );
582
+ return { epoch };
583
+ }
584
+ };
585
+ var WORKSPACE_ROOT = ".";
586
+ var FIELD = {
587
+ PATH: "path",
588
+ CONTENT: "content",
589
+ FROM: "from",
590
+ TO: "to"
591
+ };
592
+ function str(request, field) {
593
+ const value = request[field];
594
+ if (typeof value !== "string") {
595
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `missing string field '${field}'`);
596
+ }
597
+ return value;
598
+ }
599
+ function enc(request) {
600
+ return request.encoding === ENCODING.BASE64 ? ENCODING.BASE64 : ENCODING.UTF8;
601
+ }
602
+ function execCommand(request) {
603
+ const command = request.command;
604
+ if (typeof command === "string") return command;
605
+ if (Array.isArray(command) && command.every((part) => typeof part === "string")) {
606
+ return command;
607
+ }
608
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, "exec requires a command");
609
+ }
610
+ function resolveInRoot(root, rel) {
611
+ const resolved = path.resolve(root, rel);
612
+ const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
613
+ if (resolved !== root && !resolved.startsWith(rootWithSep)) {
614
+ throw new ControlFsError(CONTROL_ERROR.INVALID_PATH, `path escapes workspace: ${rel}`);
615
+ }
616
+ return resolved;
617
+ }
618
+ function isIgnored(rel) {
619
+ return rel.split(/[\\/]/).some((segment) => DEFAULT_IGNORED.has(segment));
620
+ }
621
+ var NodeControlFs = class {
622
+ constructor(root) {
623
+ this.root = root;
624
+ }
625
+ root;
626
+ async read(rel) {
627
+ const abs = resolveInRoot(this.root, rel);
628
+ const bytes = await promises.readFile(abs).catch(rethrow(rel));
629
+ const text = tryDecodeUtf8(bytes);
630
+ return text !== null ? { content: text, encoding: ENCODING.UTF8 } : { content: Buffer.from(bytes).toString("base64"), encoding: ENCODING.BASE64 };
631
+ }
632
+ async write(rel, content, encoding) {
633
+ const abs = resolveInRoot(this.root, rel);
634
+ await promises.mkdir(path.dirname(abs), { recursive: true });
635
+ const bytes = encoding === ENCODING.BASE64 ? Buffer.from(content, "base64") : Buffer.from(content, "utf8");
636
+ await promises.writeFile(abs, bytes).catch(rethrow(rel));
637
+ }
638
+ async delete(rel, recursive) {
639
+ const abs = resolveInRoot(this.root, rel);
640
+ await promises.rm(abs, { recursive, force: true });
641
+ }
642
+ async list(rel) {
643
+ const abs = resolveInRoot(this.root, rel);
644
+ const dirents = await promises.readdir(abs, { withFileTypes: true }).catch(rethrow(rel));
645
+ return dirents.filter((d) => !DEFAULT_IGNORED.has(d.name)).map((d) => ({ name: d.name, dir: d.isDirectory() }));
646
+ }
647
+ async listTree() {
648
+ const out = [];
649
+ const walk = async (dir) => {
650
+ const dirents = await promises.readdir(path.join(this.root, dir), { withFileTypes: true });
651
+ for (const dirent of dirents) {
652
+ if (DEFAULT_IGNORED.has(dirent.name)) continue;
653
+ const rel = dir ? `${dir}/${dirent.name}` : dirent.name;
654
+ if (dirent.isDirectory()) await walk(rel);
655
+ else out.push(rel);
656
+ }
657
+ };
658
+ await walk("");
659
+ return out;
660
+ }
661
+ async stat(rel) {
662
+ const abs = resolveInRoot(this.root, rel);
663
+ const stat = await promises.stat(abs).catch(rethrow(rel));
664
+ return {
665
+ kind: stat.isDirectory() ? "dir" : "file",
666
+ size: stat.size,
667
+ mtimeMs: Math.floor(stat.mtimeMs)
668
+ };
669
+ }
670
+ async mkdir(rel) {
671
+ await promises.mkdir(resolveInRoot(this.root, rel), { recursive: true });
672
+ }
673
+ async rename(from, to) {
674
+ const absFrom = resolveInRoot(this.root, from);
675
+ const absTo = resolveInRoot(this.root, to);
676
+ await promises.mkdir(path.dirname(absTo), { recursive: true });
677
+ await promises.rename(absFrom, absTo).catch(rethrow(from));
678
+ }
679
+ };
680
+ var WATCH_DEBOUNCE_MS = 60;
681
+ var NodeControlWatcher = class {
682
+ constructor(root) {
683
+ this.root = root;
684
+ }
685
+ root;
686
+ start(_relPaths, onEvent, onResync) {
687
+ let watcher;
688
+ const pending = /* @__PURE__ */ new Map();
689
+ const flush = (rel) => {
690
+ pending.delete(rel);
691
+ promises.stat(path.join(this.root, rel)).then(
692
+ (stat) => onEvent(stat.isDirectory() ? WATCH_EVENT.ADD : WATCH_EVENT.CHANGE, [rel]),
693
+ () => onEvent(WATCH_EVENT.REMOVE, [rel])
694
+ );
695
+ };
696
+ try {
697
+ watcher = watch(this.root, { recursive: true }, (_type, filename) => {
698
+ if (filename === null) {
699
+ onResync();
700
+ return;
701
+ }
702
+ const rel = filename.toString().split(path.sep).join("/");
703
+ if (isIgnored(rel)) return;
704
+ const existing = pending.get(rel);
705
+ if (existing) clearTimeout(existing);
706
+ pending.set(
707
+ rel,
708
+ setTimeout(() => flush(rel), WATCH_DEBOUNCE_MS)
709
+ );
710
+ });
711
+ } catch (error) {
712
+ onResync();
713
+ return { stop: () => {
714
+ } };
715
+ }
716
+ watcher.on("error", () => onResync());
717
+ return {
718
+ stop: () => {
719
+ for (const timer of pending.values()) clearTimeout(timer);
720
+ pending.clear();
721
+ watcher.close();
722
+ }
723
+ };
724
+ }
725
+ };
726
+ var EXEC_MAX_BUFFER = 8 * 1024 * 1024;
727
+ var NodeControlExec = class {
728
+ constructor(root) {
729
+ this.root = root;
730
+ }
731
+ root;
732
+ run(command) {
733
+ const [file, args, shell] = Array.isArray(command) ? [command[0], command.slice(1), false] : [command, [], true];
734
+ return new Promise((resolve) => {
735
+ const child = spawn(file, args, { cwd: this.root, shell, env: process.env });
736
+ let stdout = "";
737
+ let stderr = "";
738
+ let truncated = false;
739
+ const cap = (existing, chunk) => {
740
+ if (existing.length >= EXEC_MAX_BUFFER) {
741
+ truncated = true;
742
+ return existing;
743
+ }
744
+ return existing + chunk.toString("utf8");
745
+ };
746
+ child.stdout?.on("data", (chunk) => stdout = cap(stdout, chunk));
747
+ child.stderr?.on("data", (chunk) => stderr = cap(stderr, chunk));
748
+ child.on("error", (error) => resolve({ stdout, stderr: error.message, exitCode: 127 }));
749
+ child.on("close", (code) => {
750
+ if (truncated) stderr += "\n[output truncated]";
751
+ resolve({ stdout, stderr, exitCode: code ?? 0 });
752
+ });
753
+ });
754
+ }
755
+ };
756
+ function tryDecodeUtf8(bytes) {
757
+ try {
758
+ const text = new TextDecoder("utf8", { fatal: true }).decode(bytes);
759
+ return text;
760
+ } catch {
761
+ return null;
762
+ }
763
+ }
764
+ function rethrow(rel) {
765
+ return (error) => {
766
+ if (error.code === "ENOENT") throw new ControlFsError(CONTROL_ERROR.NOT_FOUND, `not found: ${rel}`);
767
+ if (error.code === "EACCES") throw new ControlFsError(CONTROL_ERROR.PERMISSION_DENIED, rel);
768
+ throw new ControlFsError(CONTROL_ERROR.IO, error.message);
769
+ };
770
+ }
771
+
370
772
  // src/tunnel/protocol.ts
371
773
  var MAX_PAYLOAD = 262144;
372
774
  var HEADER_LEN2 = 9;
@@ -419,13 +821,13 @@ function decodeFrame(bytes) {
419
821
  }
420
822
  return { streamId, frameType: rawType, payload: bytes.subarray(HEADER_LEN2) };
421
823
  }
422
- var textEncoder3 = new TextEncoder();
423
- var textDecoder2 = new TextDecoder();
824
+ var textEncoder4 = new TextEncoder();
825
+ var textDecoder3 = new TextDecoder();
424
826
  function encodeJsonPayload(value) {
425
- return textEncoder3.encode(JSON.stringify(value));
827
+ return textEncoder4.encode(JSON.stringify(value));
426
828
  }
427
829
  function decodeJsonPayload(payload) {
428
- return JSON.parse(textDecoder2.decode(payload));
830
+ return JSON.parse(textDecoder3.decode(payload));
429
831
  }
430
832
  function* chunkPayload(bytes) {
431
833
  if (bytes.length === 0) {
@@ -454,8 +856,8 @@ var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
454
856
  "transfer-encoding",
455
857
  "upgrade"
456
858
  ]);
457
- var textEncoder4 = new TextEncoder();
458
- var textDecoder3 = new TextDecoder();
859
+ var textEncoder5 = new TextEncoder();
860
+ var textDecoder4 = new TextDecoder();
459
861
  function defaultSocketFactory(url, protocols) {
460
862
  const ctor = globalThis.WebSocket;
461
863
  if (!ctor) {
@@ -705,7 +1107,7 @@ var HttpProxyStream = class {
705
1107
  status: 502,
706
1108
  headers: [HEADER_CONTENT_TYPE_TEXT]
707
1109
  });
708
- this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode(`dev server unreachable: ${err.message}`));
1110
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode(`dev server unreachable: ${err.message}`));
709
1111
  this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
710
1112
  this.ctx.finish();
711
1113
  }
@@ -736,7 +1138,7 @@ var WsProxyStream = class {
736
1138
  status: 502,
737
1139
  headers: [HEADER_CONTENT_TYPE_TEXT]
738
1140
  });
739
- this.ctx.send(FRAME_TYPE.BODY, textEncoder4.encode("dev server refused the websocket"));
1141
+ this.ctx.send(FRAME_TYPE.BODY, textEncoder5.encode("dev server refused the websocket"));
740
1142
  this.ctx.send(FRAME_TYPE.END, EMPTY_PAYLOAD);
741
1143
  this.ctx.finish();
742
1144
  });
@@ -748,7 +1150,7 @@ var WsProxyStream = class {
748
1150
  handleFrame(frame) {
749
1151
  switch (frame.frameType) {
750
1152
  case FRAME_TYPE.WS_TEXT:
751
- this.local.send(textDecoder3.decode(frame.payload));
1153
+ this.local.send(textDecoder4.decode(frame.payload));
752
1154
  break;
753
1155
  case FRAME_TYPE.WS_BIN:
754
1156
  this.local.send(frame.payload);
@@ -776,7 +1178,7 @@ var WsProxyStream = class {
776
1178
  let payload;
777
1179
  let frameType;
778
1180
  if (typeof data === "string") {
779
- payload = textEncoder4.encode(data);
1181
+ payload = textEncoder5.encode(data);
780
1182
  frameType = FRAME_TYPE.WS_TEXT;
781
1183
  } else if (data instanceof ArrayBuffer) {
782
1184
  payload = new Uint8Array(data);
@@ -902,8 +1304,8 @@ var StreamHandleV2 = class {
902
1304
  }
903
1305
  };
904
1306
  var CONNECTION_STREAM_ID = 0;
905
- var textEncoder5 = new TextEncoder();
906
- var textDecoder4 = new TextDecoder();
1307
+ var textEncoder6 = new TextEncoder();
1308
+ var textDecoder5 = new TextDecoder();
907
1309
  var SessionV2 = class {
908
1310
  constructor(transport, options) {
909
1311
  this.transport = transport;
@@ -927,7 +1329,7 @@ var SessionV2 = class {
927
1329
  sending = Promise.resolve();
928
1330
  /** Send our HELLO; the peer's must arrive as its first frame. */
929
1331
  async start() {
930
- const payload = textEncoder5.encode(JSON.stringify(this.options.local));
1332
+ const payload = textEncoder6.encode(JSON.stringify(this.options.local));
931
1333
  await this.transport.send(
932
1334
  encodeFrameV2({ streamId: CONNECTION_STREAM_ID, frameType: FRAME_TYPE_V2.HELLO, payload })
933
1335
  );
@@ -1153,7 +1555,7 @@ var SessionV2 = class {
1153
1555
  }
1154
1556
  }
1155
1557
  sendHead(streamId, frameType, value) {
1156
- const payload = textEncoder5.encode(JSON.stringify(value ?? null));
1558
+ const payload = textEncoder6.encode(JSON.stringify(value ?? null));
1157
1559
  if (payload.length > MAX_HEAD_BYTES) {
1158
1560
  return Promise.reject(new SessionV2Error("head exceeds MAX_HEAD_BYTES"));
1159
1561
  }
@@ -1277,7 +1679,7 @@ var SessionV2 = class {
1277
1679
  function parseJson(payload) {
1278
1680
  if (payload.length === 0) return null;
1279
1681
  try {
1280
- return JSON.parse(textDecoder4.decode(payload));
1682
+ return JSON.parse(textDecoder5.decode(payload));
1281
1683
  } catch {
1282
1684
  return null;
1283
1685
  }
@@ -1373,7 +1775,13 @@ var NegotiatingAgentClient = class {
1373
1775
  const hello = defaultHello();
1374
1776
  const transport = new WsTransportV2(socket);
1375
1777
  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)));
1778
+ const dispatcher = new AgentDispatcher().register(new HttpProxyKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(new WsSpliceKind(makeAgentContext(this.options.localPort, hello, createLocalSocket))).register(
1779
+ new ControlKind({
1780
+ fs: new NodeControlFs(this.options.workspaceRoot),
1781
+ watcher: new NodeControlWatcher(this.options.workspaceRoot),
1782
+ exec: new NodeControlExec(this.options.workspaceRoot)
1783
+ })
1784
+ );
1377
1785
  const session = new SessionV2(transport, {
1378
1786
  role: "agent",
1379
1787
  local: hello,
@@ -1412,7 +1820,7 @@ var TUNNEL_CONFIG_DIR = ".cimplify";
1412
1820
  var TUNNEL_CONFIG_FILE = "tunnel.json";
1413
1821
  var DEFAULT_DEV_SERVER_PORT = 3e3;
1414
1822
  function tunnelConfigPath(cwd) {
1415
- return path2.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
1823
+ return path.join(cwd, TUNNEL_CONFIG_DIR, TUNNEL_CONFIG_FILE);
1416
1824
  }
1417
1825
  async function readTunnelConfig(cwd, env = process.env) {
1418
1826
  const url = env[TUNNEL_URL_ENV];
@@ -1461,17 +1869,18 @@ var CONFIG_POLL_MS = 2e3;
1461
1869
  var RECONNECT_MIN_MS = 1e3;
1462
1870
  var RECONNECT_MAX_MS = 3e4;
1463
1871
  var CLOSE_CODE_REPLACED = 4e3;
1464
- function defaultCreateClient(config, events) {
1465
- return new NegotiatingAgentClient({
1872
+ function makeDefaultCreateClient(workspaceRoot) {
1873
+ return (config, events) => new NegotiatingAgentClient({
1466
1874
  url: config.url,
1467
1875
  token: config.token,
1468
1876
  localPort: config.localPort,
1877
+ workspaceRoot,
1469
1878
  events
1470
1879
  });
1471
1880
  }
1472
1881
  function startTunnelRuntime(cwd, options = {}) {
1473
1882
  const pollIntervalMs = options.pollIntervalMs ?? CONFIG_POLL_MS;
1474
- const createClient = options.createClient ?? defaultCreateClient;
1883
+ const createClient = options.createClient ?? makeDefaultCreateClient(cwd);
1475
1884
  const onStatus = options.onStatus ?? ((message) => info(dim(message)));
1476
1885
  let stopped = false;
1477
1886
  let generation = 0;
@@ -1609,11 +2018,11 @@ function envEndpoint(businessId, projectId) {
1609
2018
  return `/v1/businesses/${encodeURIComponent(businessId)}/projects/${encodeURIComponent(projectId)}/env-vars`;
1610
2019
  }
1611
2020
  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;
2021
+ if (await pathExists(path.join(cwd, LOCKFILE_BUN))) return PM_BUN;
2022
+ if (await pathExists(path.join(cwd, LOCKFILE_BUN_BINARY))) return PM_BUN;
2023
+ if (await pathExists(path.join(cwd, BUNFIG))) return PM_BUN;
2024
+ if (await pathExists(path.join(cwd, LOCKFILE_PNPM))) return PM_PNPM;
2025
+ if (await pathExists(path.join(cwd, LOCKFILE_YARN))) return PM_YARN;
1617
2026
  return PM_NPM;
1618
2027
  }
1619
2028
  async function pathExists(target) {
@@ -1626,7 +2035,7 @@ async function pathExists(target) {
1626
2035
  }
1627
2036
  }
1628
2037
  async function readPackageJson(cwd) {
1629
- const pkgPath = path2.join(cwd, PACKAGE_JSON);
2038
+ const pkgPath = path.join(cwd, PACKAGE_JSON);
1630
2039
  let raw;
1631
2040
  try {
1632
2041
  raw = await promises.readFile(pkgPath, "utf8");
@@ -1657,7 +2066,7 @@ async function applyRemoteEnv(client, businessId, projectId, apiBaseUrl, cwd) {
1657
2066
  });
1658
2067
  const incoming = records.filter((r) => r.value !== null && !r.is_secret).map((r) => ({ key: r.key, value: r.value }));
1659
2068
  incoming.push({ key: PUBLIC_API_URL_KEY, value: apiBaseUrl });
1660
- const envPath = path2.join(cwd, ENV_LOCAL_FILE);
2069
+ const envPath = path.join(cwd, ENV_LOCAL_FILE);
1661
2070
  let existing = [];
1662
2071
  try {
1663
2072
  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-FOJVWJ57.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-6OWQ2FZ7.mjs'),
146
146
  mock: () => import('./mock-TSUKCLWU.mjs'),
147
- introspect: () => import('./introspect-TB2QFQMF.mjs'),
147
+ introspect: () => import('./introspect-M2KNI7TW.mjs'),
148
148
  inspect: () => import('./inspect-CGYX4DDF.mjs'),
149
- doctor: () => import('./doctor-JQYXARYQ.mjs'),
150
- explain: () => import('./explain-2I4MWMPY.mjs'),
149
+ doctor: () => import('./doctor-UMR72TNV.mjs'),
150
+ explain: () => import('./explain-RD6TURI3.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-MCTGCQIY.mjs'),
156
+ upgrade: () => import('./update-MCTGCQIY.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-JPUGWNVY.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-W64GWXPF.mjs';
5
+ import './chunk-FOJVWJ57.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-FOJVWJ57.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-JPUGWNVY.mjs';
3
3
  import './chunk-K5464A3L.mjs';
4
4
  import './chunk-DBZ3UOQ2.mjs';
5
- import './chunk-W64GWXPF.mjs';
5
+ import './chunk-FOJVWJ57.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-FOJVWJ57.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.1",
4
4
  "description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
5
5
  "keywords": [
6
6
  "cimplify",