@todos-dev/cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +207 -143
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1367,6 +1367,18 @@ function clearConfig() {
1367
1367
  }
1368
1368
  }
1369
1369
 
1370
+ // src/lib/log.ts
1371
+ var installed = false;
1372
+ function installTimestampedLogging() {
1373
+ if (installed) return;
1374
+ installed = true;
1375
+ const stamp = () => `[${(/* @__PURE__ */ new Date()).toISOString()}]`;
1376
+ for (const method of ["log", "info", "warn", "error", "debug"]) {
1377
+ const orig = console[method].bind(console);
1378
+ console[method] = (...args2) => orig(stamp(), ...args2);
1379
+ }
1380
+ }
1381
+
1370
1382
  // src/lib/daemon.ts
1371
1383
  var LOG_CAP = 10 * 1024 * 1024;
1372
1384
  var EXIT_TERMINAL = 78;
@@ -1431,6 +1443,7 @@ function spawnSupervisor(userArgs, logFd) {
1431
1443
  var MODE_FLAGS = /* @__PURE__ */ new Set(["--supervisor", "--foreground", "-f"]);
1432
1444
  var userFlagsOnly = (args2) => args2.filter((a) => !MODE_FLAGS.has(a));
1433
1445
  async function runSupervisor(args2) {
1446
+ installTimestampedLogging();
1434
1447
  const userArgs = userFlagsOnly(args2);
1435
1448
  const MAX_BACKOFF = 3e4;
1436
1449
  const STABLE_MS = 6e4;
@@ -14173,14 +14186,14 @@ var safeDecodeAsync2 = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
14173
14186
  var _installedGroups = /* @__PURE__ */ new WeakMap();
14174
14187
  function _installLazyMethods(inst, group, methods) {
14175
14188
  const proto = Object.getPrototypeOf(inst);
14176
- let installed = _installedGroups.get(proto);
14177
- if (!installed) {
14178
- installed = /* @__PURE__ */ new Set();
14179
- _installedGroups.set(proto, installed);
14189
+ let installed2 = _installedGroups.get(proto);
14190
+ if (!installed2) {
14191
+ installed2 = /* @__PURE__ */ new Set();
14192
+ _installedGroups.set(proto, installed2);
14180
14193
  }
14181
- if (installed.has(group))
14194
+ if (installed2.has(group))
14182
14195
  return;
14183
- installed.add(group);
14196
+ installed2.add(group);
14184
14197
  for (const key in methods) {
14185
14198
  const fn = methods[key];
14186
14199
  Object.defineProperty(proto, key, {
@@ -16183,6 +16196,7 @@ var PollClient = class {
16183
16196
  runningTasks = /* @__PURE__ */ new Map();
16184
16197
  presenceTimer = null;
16185
16198
  gcTimer = null;
16199
+ pokeTimer = null;
16186
16200
  lastSentTunnelKey;
16187
16201
  revoked = false;
16188
16202
  maxConcurrentTasks = null;
@@ -16223,6 +16237,16 @@ var PollClient = class {
16223
16237
  tunnel?.setNodeChangeHandler(() => void this.reportPresence());
16224
16238
  void this.reportPresence();
16225
16239
  }
16240
+ // Out-of-cycle beat for mirror-health transitions, so the web flips within ~a second
16241
+ // instead of waiting out the ≤20s presence tail. Debounced: a burst of transitions (daemon
16242
+ // restart syncing every mount at once) collapses into one POST carrying the final state.
16243
+ pokePresence() {
16244
+ if (this.pokeTimer) return;
16245
+ this.pokeTimer = setTimeout(() => {
16246
+ this.pokeTimer = null;
16247
+ void this.reportPresence();
16248
+ }, 250);
16249
+ }
16226
16250
  maybeStartClaim() {
16227
16251
  if (this.running && this.canClaim && !this.loopActive) {
16228
16252
  this.loopActive = true;
@@ -16257,6 +16281,10 @@ var PollClient = class {
16257
16281
  clearInterval(this.gcTimer);
16258
16282
  this.gcTimer = null;
16259
16283
  }
16284
+ if (this.pokeTimer) {
16285
+ clearTimeout(this.pokeTimer);
16286
+ this.pokeTimer = null;
16287
+ }
16260
16288
  }
16261
16289
  // The machine was removed from its team (or its token revoked): every /api/machine/* call now
16262
16290
  // 401/403s. Fail fast instead of spinning forever — stop and exit so the operator notices.
@@ -16413,10 +16441,12 @@ function sleep(ms, signal) {
16413
16441
  }
16414
16442
 
16415
16443
  // src/SyncManager.ts
16416
- var import_node_child_process2 = require("node:child_process");
16417
16444
  var import_node_os4 = require("node:os");
16418
16445
  var import_node_path6 = require("node:path");
16419
16446
 
16447
+ // src/lib/sidecar.ts
16448
+ var import_node_child_process2 = require("node:child_process");
16449
+
16420
16450
  // src/lib/bin.ts
16421
16451
  var import_node_fs5 = require("node:fs");
16422
16452
  var import_node_path5 = require("node:path");
@@ -16482,31 +16512,24 @@ function createNdjsonParser(onEvent) {
16482
16512
  };
16483
16513
  }
16484
16514
 
16485
- // src/SyncManager.ts
16515
+ // src/lib/sidecar.ts
16486
16516
  var RESTART_MIN_MS = 1e3;
16487
16517
  var RESTART_MAX_MS = 6e4;
16488
- function expandHome(p) {
16489
- if (p === "~") return (0, import_node_os4.homedir)();
16490
- if (p.startsWith("~/") || p.startsWith("~\\")) return (0, import_node_path6.join)((0, import_node_os4.homedir)(), p.slice(2));
16491
- return p;
16492
- }
16493
- var SyncManager = class {
16494
- // by mountId
16495
- constructor(serverUrl, token, machineId) {
16496
- this.serverUrl = serverUrl;
16497
- this.token = token;
16498
- this.machineId = machineId;
16518
+ var Sidecar = class {
16519
+ constructor(args2, label, onEvent, onExit) {
16520
+ this.args = args2;
16521
+ this.label = label;
16522
+ this.onExit = onExit;
16523
+ this.parse = createNdjsonParser(onEvent);
16499
16524
  }
16500
16525
  proc = null;
16501
16526
  stopped = false;
16502
16527
  restartTimer = null;
16503
16528
  restartDelayMs = RESTART_MIN_MS;
16504
- desired = [];
16505
- health = /* @__PURE__ */ new Map();
16529
+ parse;
16506
16530
  start() {
16507
16531
  this.stopped = false;
16508
- console.log(`[machine] runner: managing worktree mirrors (machineId=${this.machineId})`);
16509
- this.spawnSidecar();
16532
+ this.spawn();
16510
16533
  }
16511
16534
  stop() {
16512
16535
  this.stopped = true;
@@ -16517,6 +16540,88 @@ var SyncManager = class {
16517
16540
  this.proc?.kill();
16518
16541
  this.proc = null;
16519
16542
  }
16543
+ // Owner saw the sidecar's healthy signal — future crashes retry quickly again.
16544
+ resetBackoff() {
16545
+ this.restartDelayMs = RESTART_MIN_MS;
16546
+ }
16547
+ // Whether control messages can be sent at all (process up with a writable stdin).
16548
+ alive() {
16549
+ return Boolean(this.proc && !this.proc.killed && this.proc.stdin && !this.proc.stdin.destroyed);
16550
+ }
16551
+ // Write one NDJSON control message to the sidecar's stdin; false when it can't be delivered
16552
+ // (the exit/respawn path is the recovery, so callers just skip).
16553
+ write(msg) {
16554
+ const stdin = this.proc?.stdin;
16555
+ if (!stdin || stdin.destroyed) return false;
16556
+ try {
16557
+ stdin.write(JSON.stringify(msg) + "\n");
16558
+ return true;
16559
+ } catch {
16560
+ return false;
16561
+ }
16562
+ }
16563
+ spawn() {
16564
+ try {
16565
+ this.proc = (0, import_node_child_process2.spawn)(TUNNEL_BIN, this.args, { stdio: ["pipe", "pipe", "inherit"] });
16566
+ } catch {
16567
+ console.warn(`[${this.label}] tds-tunnel not available \u2014 disabled`);
16568
+ return;
16569
+ }
16570
+ this.proc.on("error", () => {
16571
+ console.warn(`[${this.label}] sidecar failed to start \u2014 disabled`);
16572
+ this.proc = null;
16573
+ });
16574
+ this.proc.on("exit", () => {
16575
+ this.proc = null;
16576
+ this.onExit?.();
16577
+ this.scheduleRestart();
16578
+ });
16579
+ this.proc.stdout?.on("data", (d) => this.parse(String(d)));
16580
+ this.proc.stdin?.on("error", () => {
16581
+ });
16582
+ }
16583
+ scheduleRestart() {
16584
+ if (this.stopped || this.restartTimer) return;
16585
+ const delay2 = this.restartDelayMs;
16586
+ this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS);
16587
+ console.warn(`[${this.label}] sidecar exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
16588
+ this.restartTimer = setTimeout(() => {
16589
+ this.restartTimer = null;
16590
+ this.spawn();
16591
+ }, delay2);
16592
+ }
16593
+ };
16594
+
16595
+ // src/SyncManager.ts
16596
+ function expandHome(p) {
16597
+ if (p === "~") return (0, import_node_os4.homedir)();
16598
+ if (p.startsWith("~/") || p.startsWith("~\\")) return (0, import_node_path6.join)((0, import_node_os4.homedir)(), p.slice(2));
16599
+ return p;
16600
+ }
16601
+ var WORKTREE_REBUILD_PATIENCE_MS = 18e4;
16602
+ var SyncManager = class {
16603
+ constructor(serverUrl, token, machineId) {
16604
+ this.machineId = machineId;
16605
+ this.sidecar = new Sidecar(
16606
+ ["mirror", "--server", serverUrl, "--token", token, "--machine", machineId],
16607
+ "runner",
16608
+ (e) => this.onEvent(e)
16609
+ );
16610
+ }
16611
+ sidecar;
16612
+ desired = [];
16613
+ health = /* @__PURE__ */ new Map();
16614
+ // by mountId
16615
+ // Fired on coarse health transitions (first cycle, error set/cleared) so the presence beat
16616
+ // carrying the change goes out immediately instead of on the next 20s tick.
16617
+ onHealthChanged;
16618
+ start() {
16619
+ console.log(`[machine] runner: managing worktree mirrors (machineId=${this.machineId})`);
16620
+ this.sidecar.start();
16621
+ }
16622
+ stop() {
16623
+ this.sidecar.stop();
16624
+ }
16520
16625
  // Desired mounts, straight off the presence response. Declarative: resolve dests, remember the
16521
16626
  // set (a respawned daemon gets it replayed on its 'up' event), and forward it.
16522
16627
  setMounts(resolved) {
@@ -16545,64 +16650,36 @@ var SyncManager = class {
16545
16650
  };
16546
16651
  });
16547
16652
  }
16548
- spawnSidecar() {
16549
- try {
16550
- this.proc = (0, import_node_child_process2.spawn)(
16551
- TUNNEL_BIN,
16552
- ["mirror", "--server", this.serverUrl, "--token", this.token, "--machine", this.machineId],
16553
- // stdin is piped: the desired mount set goes down as NDJSON control messages.
16554
- { stdio: ["pipe", "pipe", "inherit"] }
16555
- );
16556
- } catch {
16557
- console.warn("[machine] runner: tds-tunnel not available \u2014 mirroring disabled");
16558
- return;
16559
- }
16560
- this.proc.on("error", () => {
16561
- console.warn("[machine] runner: mirror daemon failed to start \u2014 mirroring disabled");
16562
- this.proc = null;
16563
- });
16564
- this.proc.on("exit", () => {
16565
- this.proc = null;
16566
- this.scheduleRestart();
16567
- });
16568
- this.proc.stdout?.on("data", (d) => this.onStdout(String(d)));
16569
- this.proc.stdin?.on("error", () => {
16570
- });
16571
- }
16572
- scheduleRestart() {
16573
- if (this.stopped || this.restartTimer) return;
16574
- const delay2 = this.restartDelayMs;
16575
- this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS);
16576
- console.warn(`[machine] runner: mirror daemon exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
16577
- this.restartTimer = setTimeout(() => {
16578
- this.restartTimer = null;
16579
- this.spawnSidecar();
16580
- }, delay2);
16581
- }
16582
- onStdout = createNdjsonParser((e) => {
16653
+ onEvent(e) {
16583
16654
  if (e.event === "up") {
16584
- this.restartDelayMs = RESTART_MIN_MS;
16655
+ this.sidecar.resetBackoff();
16585
16656
  this.sendMounts();
16586
16657
  } else if (e.event === "synced" && e.mount) {
16658
+ const prev = this.health.get(e.mount);
16587
16659
  this.health.set(e.mount, { syncedAt: Date.now(), lastError: null });
16660
+ if (!prev?.syncedAt || prev.lastError) this.onHealthChanged?.();
16588
16661
  if (e.changed || e.deleted) console.log(`[machine] runner: mount ${e.mount} \u2193 ${e.changed ?? 0} changed, ${e.deleted ?? 0} deleted`);
16662
+ } else if (e.event === "worktree-missing" && e.mount) {
16663
+ const prev = this.health.get(e.mount);
16664
+ const missingSince = prev?.missingSince ?? Date.now();
16665
+ const lastError = Date.now() - missingSince > WORKTREE_REBUILD_PATIENCE_MS ? e.message ?? "worktree missing" : null;
16666
+ this.health.set(e.mount, { syncedAt: 0, lastError, missingSince });
16667
+ if (!!prev?.syncedAt || (prev?.lastError ?? null) !== lastError) this.onHealthChanged?.();
16668
+ console.log(`[machine] runner: mount ${e.mount}: worktree missing \u2014 rebuild requested`);
16589
16669
  } else if (e.event === "error") {
16590
16670
  if (e.mount) {
16591
- const h = this.health.get(e.mount);
16592
- this.health.set(e.mount, { syncedAt: h?.syncedAt ?? 0, lastError: e.message ?? "error" });
16593
- console.warn(`[machine] runner: mount ${e.mount}: ${e.message ?? "error"}`);
16671
+ const prev = this.health.get(e.mount);
16672
+ const message = e.message ?? "error";
16673
+ this.health.set(e.mount, { syncedAt: prev?.syncedAt ?? 0, lastError: message });
16674
+ if (prev?.lastError !== message) this.onHealthChanged?.();
16675
+ console.warn(`[machine] runner: mount ${e.mount}: ${message}`);
16594
16676
  } else {
16595
16677
  console.warn(`[machine] runner: ${e.message ?? "error"}`);
16596
16678
  }
16597
16679
  }
16598
- });
16680
+ }
16599
16681
  sendMounts() {
16600
- const stdin = this.proc?.stdin;
16601
- if (!stdin || stdin.destroyed) return;
16602
- try {
16603
- stdin.write(JSON.stringify({ cmd: "mounts", mounts: this.desired }) + "\n");
16604
- } catch {
16605
- }
16682
+ this.sidecar.write({ cmd: "mounts", mounts: this.desired });
16606
16683
  }
16607
16684
  };
16608
16685
 
@@ -16714,26 +16791,30 @@ async function syncModelsToServer(serverUrl, token, runtime) {
16714
16791
  }
16715
16792
 
16716
16793
  // src/lib/tunnel.ts
16717
- var import_node_child_process3 = require("node:child_process");
16718
16794
  var SHELL_BUFFER_CAP = 256 * 1024;
16719
- var RESTART_MIN_MS2 = 1e3;
16720
- var RESTART_MAX_MS2 = 6e4;
16721
16795
  var EMPTY_BUFFER = Buffer.alloc(0);
16722
16796
  var TunnelServer = class _TunnelServer {
16723
- constructor(serverUrl, token) {
16724
- this.serverUrl = serverUrl;
16725
- this.token = token;
16726
- }
16727
- proc = null;
16728
16797
  node = null;
16729
16798
  onChange = null;
16730
- stopped = false;
16731
- restartTimer = null;
16732
- restartDelayMs = RESTART_MIN_MS2;
16799
+ // Process lifecycle (spawn / respawn-with-backoff / stdin control) is the shared Sidecar's job;
16800
+ // this class keeps only the domain state that dies with the process: node + shells.
16801
+ sidecar;
16733
16802
  // Reverse shells, keyed by (build, machine) — a build mirrored onto several runners has one shell
16734
16803
  // per machine. Output is buffered even with no live subscriber so output a backgrounded command
16735
16804
  // emits between tool calls survives until the next drain.
16736
16805
  shells = /* @__PURE__ */ new Map();
16806
+ constructor(serverUrl, token) {
16807
+ this.sidecar = new Sidecar(
16808
+ ["serve", "--server", serverUrl, "--token", token],
16809
+ "tunnel",
16810
+ (evt) => this.onEvent(evt),
16811
+ () => {
16812
+ this.node = null;
16813
+ this.closeAllShells();
16814
+ this.onChange?.();
16815
+ }
16816
+ );
16817
+ }
16737
16818
  // Set the single handler fired whenever the tunnel node appears/changes/clears, so presence can
16738
16819
  // be pushed immediately instead of waiting for the next heartbeat tick. Fires now if already ready.
16739
16820
  setNodeChangeHandler(cb) {
@@ -16741,56 +16822,16 @@ var TunnelServer = class _TunnelServer {
16741
16822
  if (this.node) cb();
16742
16823
  }
16743
16824
  start() {
16744
- this.stopped = false;
16745
- this.spawnSidecar();
16746
- }
16747
- spawnSidecar() {
16748
- try {
16749
- this.proc = (0, import_node_child_process3.spawn)(
16750
- TUNNEL_BIN,
16751
- ["serve", "--server", this.serverUrl, "--token", this.token],
16752
- // stdin is piped (not ignored) so we can write reverse-shell control messages to the sidecar.
16753
- { stdio: ["pipe", "pipe", "inherit"] }
16754
- );
16755
- } catch {
16756
- console.warn("[tunnel] sidecar not available \u2014 runner mirroring disabled");
16757
- return;
16758
- }
16759
- this.proc.on("error", () => {
16760
- console.warn("[tunnel] sidecar failed to start \u2014 runner mirroring disabled");
16761
- this.proc = null;
16762
- });
16763
- this.proc.on("exit", () => {
16764
- this.proc = null;
16765
- this.node = null;
16766
- this.closeAllShells();
16767
- this.onChange?.();
16768
- this.scheduleRestart();
16769
- });
16770
- this.proc.stdout?.on("data", (d) => this.onStdout(String(d)));
16771
- this.proc.stdin?.on("error", () => {
16772
- });
16773
- }
16774
- scheduleRestart() {
16775
- if (this.stopped || this.restartTimer) return;
16776
- const delay2 = this.restartDelayMs;
16777
- this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS2);
16778
- console.warn(`[tunnel] sidecar exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
16779
- this.restartTimer = setTimeout(() => {
16780
- this.restartTimer = null;
16781
- this.spawnSidecar();
16782
- }, delay2);
16825
+ this.sidecar.start();
16783
16826
  }
16784
16827
  // Reverse-shell map key: a build mirrored onto several runners has one shell per machine.
16785
16828
  static shellKey(build, machine) {
16786
16829
  return `${build}\0${machine}`;
16787
16830
  }
16788
- // Sidecar emits newline-delimited JSON events on stdout; `ready` carries the tunnel node, and
16789
- // shell-opened/shell-output/shell-closed carry reverse-shell traffic for a (build, machine).
16790
- onStdout = createNdjsonParser((evt) => {
16831
+ onEvent(evt) {
16791
16832
  if (evt.event === "ready" && evt.tailnetAddr && evt.sshHostKey) {
16792
16833
  this.node = { tailnetAddr: evt.tailnetAddr, sshHostKey: evt.sshHostKey };
16793
- this.restartDelayMs = RESTART_MIN_MS2;
16834
+ this.sidecar.resetBackoff();
16794
16835
  console.log(`[tunnel] ready (tailnet ${evt.tailnetAddr})`);
16795
16836
  this.onChange?.();
16796
16837
  } else if ((evt.event === "shell-opened" || evt.event === "shell-output" || evt.event === "shell-closed") && evt.build) {
@@ -16814,27 +16855,17 @@ var TunnelServer = class _TunnelServer {
16814
16855
  } else if (evt.event === "error") {
16815
16856
  console.warn(`[tunnel] ${evt.message ?? "error"}`);
16816
16857
  }
16817
- });
16858
+ }
16818
16859
  // --- Reverse shell (agent → user's machine via the sync client) -----------------------------
16819
16860
  // Whether reverse-shell control can be sent at all (sidecar up with a writable stdin). The tool
16820
16861
  // checks this to fail fast instead of writing into the void.
16821
16862
  shellAvailable() {
16822
- return Boolean(this.proc && !this.proc.killed && this.proc.stdin && !this.proc.stdin.destroyed);
16823
- }
16824
- writeControl(msg) {
16825
- const stdin = this.proc?.stdin;
16826
- if (!stdin || stdin.destroyed) return false;
16827
- try {
16828
- stdin.write(JSON.stringify(msg) + "\n");
16829
- return true;
16830
- } catch {
16831
- return false;
16832
- }
16863
+ return this.sidecar.alive();
16833
16864
  }
16834
16865
  // Send keystrokes/command text to a (build, machine) shell. data is UTF-8 text (may include control
16835
16866
  // bytes like \x03 for Ctrl-C); it's base64-framed on the wire.
16836
16867
  shellSendInput(build, machine, data) {
16837
- return this.writeControl({ cmd: "shell-input", build, machine, data: Buffer.from(data, "utf8").toString("base64") });
16868
+ return this.sidecar.write({ cmd: "shell-input", build, machine, data: Buffer.from(data, "utf8").toString("base64") });
16838
16869
  }
16839
16870
  // Whether the client's shell channel is currently registered sidecar-side. undefined = no event
16840
16871
  // seen yet for this key (a client may still be connected — input discovers it); false = it was
@@ -16891,13 +16922,7 @@ var TunnelServer = class _TunnelServer {
16891
16922
  return this.node;
16892
16923
  }
16893
16924
  stop() {
16894
- this.stopped = true;
16895
- if (this.restartTimer) {
16896
- clearTimeout(this.restartTimer);
16897
- this.restartTimer = null;
16898
- }
16899
- this.proc?.kill();
16900
- this.proc = null;
16925
+ this.sidecar.stop();
16901
16926
  this.node = null;
16902
16927
  this.closeAllShells();
16903
16928
  }
@@ -16957,6 +16982,7 @@ async function startCommand(args2) {
16957
16982
  const { flags, bools } = parseFlags(args2, { f: "foreground" });
16958
16983
  if (bools.supervisor) return runSupervisor(args2);
16959
16984
  if (bools.foreground) {
16985
+ installTimestampedLogging();
16960
16986
  const { config: config2, serverUrl } = await ensureEnrolled(flags);
16961
16987
  return runWorker(config2, serverUrl);
16962
16988
  }
@@ -17089,6 +17115,7 @@ async function runWorker(config2, serverUrl) {
17089
17115
  syncManager.start();
17090
17116
  syncManager.setMounts(lastMounts);
17091
17117
  client.getMountSync = () => syncManager?.status() ?? null;
17118
+ syncManager.onHealthChanged = () => client.pokePresence();
17092
17119
  };
17093
17120
  const disableRunner = () => {
17094
17121
  if (!syncManager) return;
@@ -17788,6 +17815,32 @@ async function logoutCommand() {
17788
17815
  console.log("[tds] Run 'tds start' to enroll again.");
17789
17816
  }
17790
17817
 
17818
+ // src/lib/version.ts
17819
+ var import_fs6 = require("fs");
17820
+ var import_path2 = require("path");
17821
+ function getVersion() {
17822
+ let dir = __dirname;
17823
+ for (let i = 0; i < 6; i++) {
17824
+ const p = (0, import_path2.join)(dir, "package.json");
17825
+ if ((0, import_fs6.existsSync)(p)) {
17826
+ try {
17827
+ const pkg = JSON.parse((0, import_fs6.readFileSync)(p, "utf-8"));
17828
+ if (pkg.version) return pkg.version;
17829
+ } catch {
17830
+ }
17831
+ }
17832
+ const parent = (0, import_path2.dirname)(dir);
17833
+ if (parent === dir) break;
17834
+ dir = parent;
17835
+ }
17836
+ return "unknown";
17837
+ }
17838
+
17839
+ // src/commands/version.ts
17840
+ async function versionCommand() {
17841
+ console.log(`tds ${getVersion()}`);
17842
+ }
17843
+
17791
17844
  // src/index.ts
17792
17845
  var COMMANDS = {
17793
17846
  start: {
@@ -17857,6 +17910,13 @@ Stop the background daemon (if running), then clear ~/.tds/machine.json so the n
17857
17910
  help: `Usage: tds status
17858
17911
 
17859
17912
  Show this machine's registration and current status.`
17913
+ },
17914
+ version: {
17915
+ run: () => versionCommand(),
17916
+ summary: "Show the tds CLI version",
17917
+ help: `Usage: tds version
17918
+
17919
+ Print the installed tds CLI version.`
17860
17920
  },
17861
17921
  provider: {
17862
17922
  run: providerCommand,
@@ -17875,6 +17935,10 @@ Subcommands:
17875
17935
  var args = process.argv.slice(2);
17876
17936
  var cmd = args[0];
17877
17937
  var rest = args.slice(1);
17938
+ if (cmd === "--version" || cmd === "-v") {
17939
+ console.log(`tds ${getVersion()}`);
17940
+ process.exit(0);
17941
+ }
17878
17942
  if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
17879
17943
  const topic = cmd === "help" ? rest[0] : void 0;
17880
17944
  if (topic && COMMANDS[topic]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todos-dev/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "bin": {
5
5
  "tds": "dist/index.js"
6
6
  },
@@ -27,12 +27,12 @@
27
27
  "@tds/types": "0.1.0"
28
28
  },
29
29
  "optionalDependencies": {
30
- "@todos-dev/cli-darwin-arm64": "0.1.3",
31
- "@todos-dev/cli-darwin-x64": "0.1.3",
32
- "@todos-dev/cli-linux-x64": "0.1.3",
33
- "@todos-dev/cli-linux-arm64": "0.1.3",
34
- "@todos-dev/cli-win32-x64": "0.1.3",
35
- "@todos-dev/cli-win32-arm64": "0.1.3"
30
+ "@todos-dev/cli-darwin-arm64": "0.1.4",
31
+ "@todos-dev/cli-darwin-x64": "0.1.4",
32
+ "@todos-dev/cli-linux-x64": "0.1.4",
33
+ "@todos-dev/cli-linux-arm64": "0.1.4",
34
+ "@todos-dev/cli-win32-x64": "0.1.4",
35
+ "@todos-dev/cli-win32-arm64": "0.1.4"
36
36
  },
37
37
  "scripts": {
38
38
  "dev": "tsx watch src/index.ts",