@pablopunk/tau 0.1.0 → 0.1.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,15 +1,16 @@
1
- import { $ as routes, B as COMMAND_NOUNS, G as isModelRefNode, H as commandOf, J as LIVE_PATH, K as routeFields, L as isErrorBody, M as decodeServerFrame, N as encodeFrame, U as commands, V as FLOW_COMMANDS, W as flagNameOf, Z as fillPath, a as readRunningInstance, c as lastAssistantText, it as RedeemPairingResponseSchema, l as viewToText, nt as nextViewState, z as AUTO_FIELDS } from "./wait-for-lock-D2V3vuye.js";
2
- import { a as lifecycleFlagsOf, c as createHubFileStore, i as commandWordOf, l as alreadyLinkedRefusal, n as serverLogOf, o as cliVersion, r as writeStoredPort, s as readOrCreateToken, t as serveInForeground, u as connectRefusal } from "./serve-92lMbcls.js";
3
- import { a as TemporaryEntryError, i as realContext, r as createServiceManager, t as runServiceCommand } from "./service-command-C7WDeeR2.js";
4
- import { t as cliEntry } from "./cli-entry-DcDpQ30L.js";
1
+ import { $ as routes, B as COMMAND_NOUNS, G as isModelRefNode, H as commandOf, J as LIVE_PATH, K as routeFields, M as decodeServerFrame, N as encodeFrame, U as commands, V as FLOW_COMMANDS, W as flagNameOf, a as lastAssistantText, it as RedeemPairingResponseSchema, nt as nextViewState, o as viewToText, s as compareVersions, z as AUTO_FIELDS } from "./src-DO9LUlhi.js";
2
+ import { a as lifecycleFlagsOf, c as connectRefusal, i as commandWordOf, n as serverLogOf, o as createHubFileStore, r as writeStoredPort, s as alreadyLinkedRefusal, t as serveInForeground } from "./serve-Uc4Nin5f.js";
3
+ import { a as realLifecycleDeps, c as renderExtras, d as discoverServer, f as CliExit, i as managerOf, l as processEnv, m as usageError, n as linkedMachinesLine, o as withToken, p as failure, r as NOT_RUNNING, s as NO_EXTRAS, u as callRoute } from "./machines-behind-BjGgmSs_.js";
4
+ import { a as cliVersion, r as npmOf, t as PACKAGE_NAME } from "./latest-version-di9hD7cY.js";
5
+ import { a as TemporaryEntryError, i as realContext } from "./manager-B_WHbb1U.js";
6
+ import { t as runServiceCommand } from "./service-command-C74K0saj.js";
5
7
  import { createReadStream, existsSync, readFileSync, statSync, watchFile } from "node:fs";
6
- import { join } from "node:path";
7
- import { homedir, hostname } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { hostname } from "node:os";
8
10
  import { fileURLToPath } from "node:url";
9
- import { promisify } from "node:util";
11
+ import { parseArgs } from "node:util";
10
12
  import { randomUUID } from "node:crypto";
11
13
  import { readFile, readdir } from "node:fs/promises";
12
- import { execFile } from "node:child_process";
13
14
  import { Value } from "typebox/value";
14
15
  import { createInterface } from "node:readline/promises";
15
16
  import QRCode from "qrcode";
@@ -65,18 +66,6 @@ function readLive(view) {
65
66
  queued: (inbox?.items ?? []).filter(isQueuedMessage).map(queuedOf)
66
67
  };
67
68
  }
68
- /** A failure that ends the command with a message on stderr and an exit code. */
69
- var CliExit = class extends Error {
70
- exitCode;
71
- constructor(exitCode, message) {
72
- super(message);
73
- this.name = "CliExit";
74
- this.exitCode = exitCode;
75
- }
76
- };
77
- const usageError = (message) => new CliExit(2, message);
78
- const failure = (message) => new CliExit(1, message);
79
- const NOT_RUNNING_MESSAGE = "tau is not running. Run `tau` to start it.";
80
69
  //#endregion
81
70
  //#region src/machines/connect-args.ts
82
71
  const USAGE = "Usage: tau connect <link | code --hub <url>> [--name <label>] [--yes]";
@@ -170,94 +159,6 @@ function parseConnectTarget(input, hub) {
170
159
  };
171
160
  }
172
161
  //#endregion
173
- //#region src/commands/client.ts
174
- const URL_POLL_MS = 100;
175
- async function waitForLockUrl(home, env) {
176
- const deadline = env.now() + env.startupWaitMs;
177
- for (;;) {
178
- const info = await readRunningInstance(home);
179
- if (!info) throw failure(NOT_RUNNING_MESSAGE);
180
- if (info.url) return info.url;
181
- if (env.now() >= deadline) throw failure("tau is starting but has not published its URL yet.");
182
- await env.sleep(URL_POLL_MS);
183
- }
184
- }
185
- async function readToken(home) {
186
- try {
187
- return (await readFile(join(home, "token"), "utf8")).trim();
188
- } catch {
189
- throw failure(NOT_RUNNING_MESSAGE);
190
- }
191
- }
192
- /** The lock gives the URL and `<home>/token` the secret. Retries while a live server has no URL yet. */
193
- async function discoverServer(env) {
194
- return {
195
- url: await waitForLockUrl(env.home, env),
196
- token: await readToken(env.home)
197
- };
198
- }
199
- const queryString = (query) => {
200
- const entries = Object.entries(query ?? {});
201
- return entries.length === 0 ? "" : `?${new URLSearchParams(entries).toString()}`;
202
- };
203
- async function errorMessageOf$1(response) {
204
- const text = await response.text();
205
- try {
206
- const parsed = JSON.parse(text);
207
- if (isErrorBody(parsed)) return `${parsed.error.code}: ${parsed.error.message}`;
208
- } catch {}
209
- return `HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`;
210
- }
211
- async function send(server, name, request, env) {
212
- const route = routes[name];
213
- const target = `${server.url}${fillPath(route.path, request.params)}${queryString(request.query)}`;
214
- const hasBody = request.body !== void 0;
215
- try {
216
- return await env.fetch(target, {
217
- method: route.method,
218
- headers: {
219
- authorization: `Bearer ${server.token}`,
220
- ...hasBody ? { "content-type": "application/json" } : {}
221
- },
222
- body: hasBody ? JSON.stringify(request.body) : void 0
223
- });
224
- } catch {
225
- throw failure(NOT_RUNNING_MESSAGE);
226
- }
227
- }
228
- /** Calls one route. Returns the parsed JSON, or `undefined` for an empty answer. An `AppError` becomes a failure. */
229
- async function callRoute(server, name, request, env) {
230
- const response = await send(server, name, request, env);
231
- if (!response.ok) throw failure(await errorMessageOf$1(response));
232
- const text = await response.text();
233
- return text ? JSON.parse(text) : void 0;
234
- }
235
- //#endregion
236
- //#region src/commands/command-env.ts
237
- const defaultOpenSocket = (url, headers) => {
238
- return new WebSocket(url, { headers });
239
- };
240
- const homeOf = (env) => env.TAU_HOME ?? join(homedir(), ".tau");
241
- function processEnv() {
242
- return {
243
- home: homeOf(process.env),
244
- env: process.env,
245
- stdout: (text) => void process.stdout.write(`${text}\n`),
246
- stderr: (text) => void process.stderr.write(`${text}\n`),
247
- fetch: (input, init) => fetch(input, init),
248
- openSocket: defaultOpenSocket,
249
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
250
- after: (ms, fn) => {
251
- const timer = setTimeout(fn, ms);
252
- return () => clearTimeout(timer);
253
- },
254
- now: Date.now,
255
- startupWaitMs: 5e3,
256
- pollMs: 2e3,
257
- skillPaths: []
258
- };
259
- }
260
- //#endregion
261
162
  //#region src/commands/command-flags.ts
262
163
  const WAIT_COMMANDS = ["threads start", "threads send"];
263
164
  const COMMON_FLAGS = [
@@ -526,8 +427,18 @@ const THREAD_EXTRAS = [" wait <id> Wait until the thread is done, then pr
526
427
  const LIFECYCLE = [
527
428
  " start, stop, restart, status, open, logs, service Run and manage the server",
528
429
  " serve Run the server in the foreground",
430
+ " update Install the latest tau and restart the server (--check, --yes, --force, --no-restart)",
529
431
  " skill Print the tau skill for agents"
530
432
  ];
433
+ const UPDATE_FLAGS = [
434
+ "",
435
+ "tau update flags:",
436
+ " --check Print current -> latest; exit 0 current, 10 update available, 1 error",
437
+ " --yes Skip only the \"Install X -> Y and restart?\" question",
438
+ " --force Restart even when threads are running",
439
+ " --no-restart Install and verify, then print `tau restart`",
440
+ " --home <dir>, --port <n> Forwarded to `tau restart`"
441
+ ];
531
442
  const pad = (text) => text.padEnd(26);
532
443
  function rootHelp() {
533
444
  return [
@@ -542,7 +453,8 @@ function rootHelp() {
542
453
  ...COMMAND_NOUNS.map((noun) => ` ${noun}`),
543
454
  ...FLOW_COMMANDS.map((word) => ` ${word}`),
544
455
  "",
545
- "Run `tau <noun> --help` for the commands of a noun, `tau <noun> <verb> --help` for one command."
456
+ "Run `tau <noun> --help` for the commands of a noun, `tau <noun> <verb> --help` for one command.",
457
+ ...UPDATE_FLAGS
546
458
  ].join("\n");
547
459
  }
548
460
  function nounHelp(noun) {
@@ -931,7 +843,7 @@ async function redeem(hubUrl, code, deviceName, env) {
931
843
  }
932
844
  async function startAfterLink(choice, env) {
933
845
  if (choice === "serve") {
934
- const { serveInForeground } = await import("./serve-BcVZj5e2.js");
846
+ const { serveInForeground } = await import("./serve-CuMzEud7.js");
935
847
  env.stdout("Starting tau for this session. Press Ctrl+C to stop.");
936
848
  await serveInForeground([
937
849
  "--no-open",
@@ -939,7 +851,7 @@ async function startAfterLink(choice, env) {
939
851
  env.home
940
852
  ]);
941
853
  } else if (choice === "service") {
942
- const { runServiceCommand } = await import("./service-command-BVr33vlP.js");
854
+ const { runServiceCommand } = await import("./service-command-DqcstJ8e.js");
943
855
  await runServiceCommand([
944
856
  "install",
945
857
  "--home",
@@ -1010,7 +922,7 @@ const runDisconnect = runner(executeDisconnect);
1010
922
  const REMOTE_WAIT_MS = 72e4;
1011
923
  const LINK_POLL_MS = 2e3;
1012
924
  const REMOTE_POLL_MS = 3e3;
1013
- const PROGRESS_EVERY_MS = 15e3;
925
+ const PROGRESS_EVERY_MS = 1e4;
1014
926
  const FULL_POWER_WARNING = "A linked device gets full power over this computer: agents, terminals and your files. Share this only with yourself.";
1015
927
  function parsePairArgs(argv) {
1016
928
  let name;
@@ -1024,6 +936,10 @@ function parsePairArgs(argv) {
1024
936
  }
1025
937
  const groupShortCode = (code) => code.match(/.{1,4}/g)?.join(" ") ?? code;
1026
938
  const remoteOf = (server, env) => callRoute(server, "getRemoteStatus", {}, env);
939
+ function elapsedLine(status, now) {
940
+ const seconds = status.since === null ? 0 : Math.max(0, Math.floor((now - status.since) / 1e3));
941
+ return `Setting up remote access, ${seconds < 60 ? `${seconds} s` : `${Math.floor(seconds / 60)} min`} so far (usually 3-6 minutes)`;
942
+ }
1027
943
  async function waitUntilOnline(server, env, first) {
1028
944
  const deadline = env.now() + REMOTE_WAIT_MS;
1029
945
  let status = first;
@@ -1037,7 +953,7 @@ async function waitUntilOnline(server, env, first) {
1037
953
  env.stdout(status.message);
1038
954
  } else if (env.now() - lastProgress >= PROGRESS_EVERY_MS) {
1039
955
  lastProgress = env.now();
1040
- env.stdout(`Still ${status.state}… (this takes a few minutes)`);
956
+ env.stdout(elapsedLine(status, env.now()));
1041
957
  }
1042
958
  await env.sleep(REMOTE_POLL_MS);
1043
959
  status = await remoteOf(server, env);
@@ -1049,7 +965,7 @@ async function ensureRemoteOnline(server, env) {
1049
965
  let status = await remoteOf(server, env);
1050
966
  if (status.state === "online") return;
1051
967
  if (status.state === "off") {
1052
- env.stdout("Remote access is off. Turning it on: the first time takes a few minutes, please wait.");
968
+ env.stdout("Remote access is off. Turning it on: a new URL takes 3-6 minutes, please wait.");
1053
969
  status = await callRoute(server, "enableRemote", {}, env);
1054
970
  } else env.stdout("Remote access is starting: this takes a few minutes.");
1055
971
  await waitUntilOnline(server, env, status);
@@ -1136,121 +1052,6 @@ async function runSkill(_argv) {
1136
1052
  await printSkill(processEnv());
1137
1053
  }
1138
1054
  //#endregion
1139
- //#region src/lifecycle/status-extras.ts
1140
- const NO_EXTRAS = {
1141
- uptimeSeconds: null,
1142
- lastExit: null,
1143
- tunnel: null
1144
- };
1145
- const execFileAsync = promisify(execFile);
1146
- /** `[[dd-]hh:]mm:ss` as printed by `ps -o etime=`. */
1147
- function parseElapsed(text) {
1148
- const match = /^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/.exec(text.trim());
1149
- if (!match) return null;
1150
- const [, days, hours, minutes, seconds] = match.map((part) => Number(part ?? 0));
1151
- return ((days ?? 0) * 24 + (hours ?? 0)) * 3600 + (minutes ?? 0) * 60 + (seconds ?? 0);
1152
- }
1153
- async function uptimeOf(pid) {
1154
- try {
1155
- const { stdout } = await execFileAsync("ps", [
1156
- "-o",
1157
- "etime=",
1158
- "-p",
1159
- String(pid)
1160
- ]);
1161
- return parseElapsed(stdout);
1162
- } catch {
1163
- return null;
1164
- }
1165
- }
1166
- /** Missing or corrupt files give `null`. */
1167
- async function readLastExit(home) {
1168
- try {
1169
- const value = JSON.parse(await readFile(join(home, "last-exit.json"), "utf8"));
1170
- if (typeof value.reason !== "string" || typeof value.at !== "number") return null;
1171
- return {
1172
- reason: value.reason,
1173
- at: value.at,
1174
- ...value.detail ? { detail: String(value.detail) } : {}
1175
- };
1176
- } catch {
1177
- return null;
1178
- }
1179
- }
1180
- async function readTunnel(home, instance) {
1181
- if (!instance?.url) return null;
1182
- try {
1183
- const token = (await readFile(join(home, "token"), "utf8")).trim();
1184
- const response = await fetch(`${instance.url}/api/remote`, {
1185
- headers: { authorization: `Bearer ${token}` },
1186
- signal: AbortSignal.timeout(2e3)
1187
- });
1188
- if (!response.ok) return null;
1189
- const { state, url, message } = await response.json();
1190
- return {
1191
- state,
1192
- url,
1193
- message
1194
- };
1195
- } catch {
1196
- return null;
1197
- }
1198
- }
1199
- async function realExtras(home, instance) {
1200
- const [uptimeSeconds, lastExit, tunnel] = await Promise.all([
1201
- instance ? uptimeOf(instance.pid) : null,
1202
- readLastExit(home),
1203
- readTunnel(home, instance)
1204
- ]);
1205
- return {
1206
- uptimeSeconds,
1207
- lastExit,
1208
- tunnel
1209
- };
1210
- }
1211
- function formatDuration(totalSeconds) {
1212
- const days = Math.floor(totalSeconds / 86400);
1213
- const hours = Math.floor(totalSeconds % 86400 / 3600);
1214
- const minutes = Math.floor(totalSeconds % 3600 / 60);
1215
- if (days > 0) return `${days}d ${hours}h`;
1216
- if (hours > 0) return `${hours}h ${minutes}m`;
1217
- return minutes > 0 ? `${minutes}m ${totalSeconds % 60}s` : `${totalSeconds}s`;
1218
- }
1219
- function renderExtras(extras) {
1220
- const { uptimeSeconds, lastExit, tunnel } = extras;
1221
- const exit = lastExit ? `${lastExit.reason}${lastExit.detail ? ` (${lastExit.detail})` : ""} at ${new Date(lastExit.at).toISOString()}` : "-";
1222
- const tunnelText = tunnel ? `${tunnel.state}${tunnel.url ? ` ${tunnel.url}` : ""}` : "-";
1223
- return [
1224
- `uptime: ${uptimeSeconds === null ? "-" : formatDuration(uptimeSeconds)}`,
1225
- `last exit: ${exit}`,
1226
- `tunnel: ${tunnelText}`
1227
- ];
1228
- }
1229
- //#endregion
1230
- //#region src/lifecycle/deps.ts
1231
- const withToken = (origin, token) => `${origin}/?token=${encodeURIComponent(token)}`;
1232
- const managerOf = (deps, home) => createServiceManager(deps.context, deps.cli(), home);
1233
- const NOT_RUNNING = "tau is not running. Run `tau` to start it.";
1234
- async function realLifecycleDeps() {
1235
- const { waitForUrl } = await import("./wait-for-lock-BMk6v3J5.js");
1236
- const { existsSync } = await import("node:fs");
1237
- return {
1238
- context: realContext(),
1239
- cli: () => cliEntry(),
1240
- version: cliVersion,
1241
- readInstance: readRunningInstance,
1242
- waitForInstanceUrl: (home) => waitForUrl(home),
1243
- readToken: readOrCreateToken,
1244
- openBrowser: async (url) => {
1245
- const { default: open } = await import("open");
1246
- await open(url);
1247
- },
1248
- out: (message) => console.log(message),
1249
- fileExists: existsSync,
1250
- extras: realExtras
1251
- };
1252
- }
1253
- //#endregion
1254
1055
  //#region src/lifecycle/open.ts
1255
1056
  /** Opens the browser on a running server; returns the URL it opened. */
1256
1057
  async function openRunning(deps, home) {
@@ -1433,6 +1234,324 @@ async function runStop(argv) {
1433
1234
  await runStopWith(await realLifecycleDeps(), argv);
1434
1235
  }
1435
1236
  //#endregion
1237
+ //#region src/update/install-method.ts
1238
+ const NPM_LAYOUT = /\/lib\/node_modules\/@pablopunk\/tau\/dist\/bin\.js$/;
1239
+ const SHIM_MANAGERS = /\/(mise|asdf)\//;
1240
+ const manual = (reason, manualCommand) => ({
1241
+ kind: "manual",
1242
+ reason,
1243
+ manualCommand
1244
+ });
1245
+ function refusalOf(entry, target) {
1246
+ const spec = `${PACKAGE_NAME}@${target}`;
1247
+ if (entry.includes("/.bun/")) return `Installed with bun. Run: bun add -g ${spec}`;
1248
+ if (entry.includes("/.volta/")) return `Installed with volta. Run: volta install ${spec}`;
1249
+ if (/\/_npx\/|\/dlx\/|\/\.bun\/install\/cache\//.test(entry) || entry.includes("/_npx/")) return "Running from a temporary npx/dlx cache: nothing to update. Run `npx @pablopunk/tau@latest` again.";
1250
+ if (/\/packages\/cli\/dist\/bin\.js$/.test(entry)) return "Running from a development checkout: update it with git.";
1251
+ }
1252
+ /** Which package manager owns this tau, from where the running entry file lives. Never guesses. */
1253
+ function installMethodOf(probe) {
1254
+ const { entry, target } = probe;
1255
+ const spec = `${PACKAGE_NAME}@${target}`;
1256
+ if (probe.platform === "win32") return manual("Automatic update is not supported on Windows.", `npm i -g ${spec}`);
1257
+ const refusal = refusalOf(entry, target);
1258
+ if (refusal) return manual(refusal);
1259
+ if (entry.includes("/.pnpm/") || /\/pnpm\/global\//.test(entry)) return {
1260
+ kind: "pnpm",
1261
+ install: {
1262
+ command: "pnpm",
1263
+ args: [
1264
+ "add",
1265
+ "-g",
1266
+ spec
1267
+ ]
1268
+ },
1269
+ manualCommand: `pnpm add -g ${spec}`
1270
+ };
1271
+ if (!NPM_LAYOUT.test(entry)) return manual(`Unknown install layout: ${entry}`, `npm i -g ${spec}`);
1272
+ return {
1273
+ kind: "npm",
1274
+ packageDir: dirname(dirname(entry)),
1275
+ install: {
1276
+ command: npmOf(probe.execPath),
1277
+ args: [
1278
+ "i",
1279
+ "-g",
1280
+ spec
1281
+ ]
1282
+ },
1283
+ manualCommand: `npm i -g ${spec}`,
1284
+ hint: SHIM_MANAGERS.test(entry) ? "If `tau` is missing afterwards, run `mise reshim`." : void 0
1285
+ };
1286
+ }
1287
+ const entryOfPackage = (packageDir) => join(packageDir, "dist", "bin.js");
1288
+ //#endregion
1289
+ //#region src/update/update-text.ts
1290
+ const LAG_NOTE = "A release can take 1-3 minutes to show up in the npm registry.";
1291
+ const LINKED_NOTE = "Linked machines must run the same tau version: run `tau update` on each.";
1292
+ const LINKED_HUB_NOTE = "This tau is linked to a hub: update the hub and every machine to the same version.";
1293
+ const INSIDE_TAU = "You are running inside tau; restarting would kill this command. Run `tau restart` from a normal terminal, or pass `--no-restart`.";
1294
+ const NON_TTY_NEEDS_YES = "Not a terminal. Add --yes to skip the question.";
1295
+ const FOREGROUND_NOTE = "tau runs in the foreground: restart it yourself (Ctrl-C, then `tau`).";
1296
+ const NOT_RUNNING_NOTE = "tau is not running: start it with `tau start`.";
1297
+ const names = (items) => items.map((item) => ` - ${item.title}`);
1298
+ function runningWorkText(work) {
1299
+ const lines = [`Running threads: ${work.threads.length}, subagents: ${work.subagents.length}.`];
1300
+ lines.push(...names(work.threads), ...names(work.subagents));
1301
+ lines.push("A restart resumes or aborts turns on startup, ends terminals and background processes, and the page reconnects in seconds.", "Add --force to restart anyway.");
1302
+ return lines.join("\n");
1303
+ }
1304
+ const resumingText = (work) => work.resuming.length > 0 ? `${work.resuming.length} thread(s) with timers or a goal will resume after the restart.` : void 0;
1305
+ const failedInstallText = (manual, rollback) => `The install failed. Run it yourself: ${manual}\nTo go back: ${rollback}`;
1306
+ const permissionText = (manual) => `Permission denied. Fix the permissions of the npm prefix (or use a node version manager), then run: ${manual}`;
1307
+ const verifyFailedText = (problem, rollback) => `The update did not verify: ${problem}\nNothing was restarted: the old server still runs on old in-memory code and files on disk may be inconsistent. Re-run \`tau update\`, or go back with: ${rollback}`;
1308
+ const skewText = (running, target) => `The server still reports version ${running ?? "none"}, expected ${target}. Check \`tau status\` and \`tau logs\`.`;
1309
+ //#endregion
1310
+ //#region src/update/restart-step.ts
1311
+ async function isInsideServer(deps, home) {
1312
+ if (deps.env.TAU_THREAD_ID) return true;
1313
+ const [instance, ancestors] = [await deps.readInstance(home), await (deps.ancestorPids?.() ?? Promise.resolve([]))];
1314
+ return instance !== void 0 && ancestors.includes(instance.pid);
1315
+ }
1316
+ /** Restarts the managed server with the NEW binary and waits until the lock reports the target version. */
1317
+ async function restartInNewBinary(deps, request) {
1318
+ const running = (await gatherStatus(deps, request.home)).running;
1319
+ if (running === "foreground") return note(deps, FOREGROUND_NOTE);
1320
+ if (running === false) return note(deps, NOT_RUNNING_NOTE);
1321
+ if (await isInsideServer(deps, request.home)) {
1322
+ deps.err(INSIDE_TAU);
1323
+ return 1;
1324
+ }
1325
+ const args = [
1326
+ entryOfPackage(request.packageDir),
1327
+ "restart",
1328
+ ...request.forwarded
1329
+ ];
1330
+ if ((await deps.runCommand(deps.execPath, args, { stdio: "inherit" })).code !== 0) {
1331
+ deps.err("`tau restart` failed. Check `tau status` and `tau logs`.");
1332
+ return 1;
1333
+ }
1334
+ const instance = await deps.pollInstance(request.home, request.target);
1335
+ if (instance?.version !== request.target) {
1336
+ deps.err(skewText(instance?.version, request.target));
1337
+ return 1;
1338
+ }
1339
+ deps.out(`tau ${request.target} is running.`);
1340
+ return 0;
1341
+ }
1342
+ function note(deps, message) {
1343
+ deps.out(`Updated. ${message}`);
1344
+ return 0;
1345
+ }
1346
+ //#endregion
1347
+ //#region src/update/running-work.ts
1348
+ /** What a restart would interrupt. Each thread is counted once; the caller's own lineage is left out. */
1349
+ function runningWorkOf(all, ownId) {
1350
+ const own = ownId ? lineageOf(all, ownId) : /* @__PURE__ */ new Set();
1351
+ const live = all.filter((item) => item.archivedAt === null && !own.has(item.id));
1352
+ const subagents = live.filter((item) => item.parentId !== null && item.agentStatus === "running");
1353
+ return {
1354
+ threads: live.filter((item) => item.parentId === null && item.status === "running"),
1355
+ subagents,
1356
+ resuming: live.filter((item) => item.timers > 0 || item.goal?.state === "active")
1357
+ };
1358
+ }
1359
+ const hasRunningWork = (work) => work.threads.length + work.subagents.length > 0;
1360
+ //#endregion
1361
+ //#region src/update/update-flags.ts
1362
+ function updateFlagsOf(argv) {
1363
+ const { values } = parseArgs({
1364
+ args: [...argv],
1365
+ strict: false,
1366
+ options: {
1367
+ check: { type: "boolean" },
1368
+ yes: {
1369
+ type: "boolean",
1370
+ short: "y"
1371
+ },
1372
+ force: { type: "boolean" },
1373
+ "no-restart": { type: "boolean" },
1374
+ home: { type: "string" },
1375
+ port: { type: "string" }
1376
+ }
1377
+ });
1378
+ const forwarded = [...typeof values.home === "string" ? ["--home", values.home] : [], ...typeof values.port === "string" ? ["--port", values.port] : []];
1379
+ return {
1380
+ check: values.check === true,
1381
+ yes: values.yes === true,
1382
+ force: values.force === true,
1383
+ noRestart: values["no-restart"] === true,
1384
+ forwarded
1385
+ };
1386
+ }
1387
+ //#endregion
1388
+ //#region src/update/verify-install.ts
1389
+ const STATUS_TIMEOUT_MS = 3e4;
1390
+ function reportedVersion(stdout) {
1391
+ try {
1392
+ return JSON.parse(stdout.trim()).cliVersion;
1393
+ } catch {
1394
+ return;
1395
+ }
1396
+ }
1397
+ /** The new files must say the target version and the new binary must load; returns the problem, if any. */
1398
+ async function verifyInstall(deps, packageDir, target, home) {
1399
+ const onDisk = deps.readPackageVersion(packageDir);
1400
+ if (onDisk !== target) return `${packageDir}/package.json says ${onDisk ?? "nothing"}, expected ${target}.`;
1401
+ const args = [
1402
+ entryOfPackage(packageDir),
1403
+ "status",
1404
+ "--home",
1405
+ home,
1406
+ "--json"
1407
+ ];
1408
+ const result = await deps.runCommand(deps.execPath, args, {
1409
+ stdio: "capture",
1410
+ timeoutMs: STATUS_TIMEOUT_MS
1411
+ });
1412
+ if (result.code !== 0) return `The new tau does not start (exit ${result.code}): ${result.stderr.trim().slice(0, 300)}`;
1413
+ const reported = reportedVersion(result.stdout);
1414
+ return reported === target ? void 0 : `The new tau reports version ${reported ?? "unknown"}, expected ${target}.`;
1415
+ }
1416
+ async function planOf(deps, target) {
1417
+ const method = installMethodOf({
1418
+ entry: deps.entry,
1419
+ execPath: deps.execPath,
1420
+ platform: deps.context.platform,
1421
+ target
1422
+ });
1423
+ if (method.kind === "manual") return [method.reason, method.manualCommand ? `Run: ${method.manualCommand}` : ""].filter(Boolean).join("\n");
1424
+ const install = {
1425
+ ...method.install,
1426
+ manual: method.manualCommand
1427
+ };
1428
+ if (method.kind === "npm") return {
1429
+ packageDir: method.packageDir,
1430
+ install,
1431
+ hint: method.hint
1432
+ };
1433
+ const root = await deps.runCommand("pnpm", ["root", "-g"], {
1434
+ stdio: "capture",
1435
+ timeoutMs: 15e3
1436
+ });
1437
+ const dir = root.stdout.trim();
1438
+ if (root.code !== 0 || !dir) return `Could not find the pnpm global directory.\nRun: ${method.manualCommand}`;
1439
+ try {
1440
+ return {
1441
+ packageDir: deps.realpath(`${dir}/@pablopunk/tau`),
1442
+ install
1443
+ };
1444
+ } catch {
1445
+ return `Could not find the pnpm global package.\nRun: ${method.manualCommand}`;
1446
+ }
1447
+ }
1448
+ async function refusalForRunningWork(deps, home) {
1449
+ let all;
1450
+ try {
1451
+ all = await deps.listConversations(home);
1452
+ } catch (error) {
1453
+ return `Could not list the running threads (${error instanceof Error ? error.message : error}). Add --force to restart anyway.`;
1454
+ }
1455
+ const work = runningWorkOf(all, deps.env.TAU_THREAD_ID);
1456
+ const resuming = resumingText(work);
1457
+ if (resuming) deps.out(resuming);
1458
+ return hasRunningWork(work) ? runningWorkText(work) : void 0;
1459
+ }
1460
+ async function installStep(deps, plan, current) {
1461
+ const { command, args, manual } = plan.install;
1462
+ const result = await deps.runCommand(command, args, { stdio: "tee" });
1463
+ if (result.code === 0) return void 0;
1464
+ if (/EACCES|EPERM/.test(result.stderr)) return permissionText(manual);
1465
+ return failedInstallText(manual, manual.replace(/@[^@]+$/, `@${current}`));
1466
+ }
1467
+ async function finish(deps, flags, plan, target, home) {
1468
+ const rollback = plan.install.manual.replace(/@[^@]+$/, `@${deps.version()}`);
1469
+ const problem = await verifyInstall(deps, plan.packageDir, target, home);
1470
+ if (problem) {
1471
+ deps.err(verifyFailedText(problem, rollback));
1472
+ return 1;
1473
+ }
1474
+ if (plan.hint) deps.out(plan.hint);
1475
+ const code = flags.noRestart ? noRestartNote(deps) : await restartInNewBinary(deps, {
1476
+ ...flags,
1477
+ packageDir: plan.packageDir,
1478
+ target,
1479
+ home
1480
+ });
1481
+ if (code === 0) await machinesBehindNote(deps, home, target);
1482
+ if (code === 0) deps.out(deps.hasHubFile(home) ? `${LINKED_NOTE}\n${LINKED_HUB_NOTE}` : LINKED_NOTE);
1483
+ return code;
1484
+ }
1485
+ function noRestartNote(deps) {
1486
+ deps.out("Updated. Run `tau restart` to use the new version.");
1487
+ return 0;
1488
+ }
1489
+ async function machinesBehindNote(deps, home, latest) {
1490
+ try {
1491
+ const line = linkedMachinesLine(await deps.linkedMachines?.(home), latest);
1492
+ if (line) deps.out(line);
1493
+ } catch {}
1494
+ }
1495
+ async function checkOnly(deps, home, current, latest) {
1496
+ deps.out(`${current} -> ${latest}`);
1497
+ await machinesBehindNote(deps, home, latest);
1498
+ return compareVersions(current, latest) < 0 ? 10 : 0;
1499
+ }
1500
+ async function latestOrError(deps) {
1501
+ try {
1502
+ return await deps.latestVersion();
1503
+ } catch (error) {
1504
+ deps.err(error instanceof Error ? error.message : String(error));
1505
+ return;
1506
+ }
1507
+ }
1508
+ /** `tau update`; resolves to the exit code. */
1509
+ async function runUpdateWith(deps, argv) {
1510
+ const flags = updateFlagsOf(argv);
1511
+ const home = lifecycleFlagsOf(argv, deps.context).home;
1512
+ const current = deps.version();
1513
+ const latest = await latestOrError(deps);
1514
+ if (latest === void 0) return 1;
1515
+ if (flags.check) return checkOnly(deps, home, current, latest);
1516
+ if (compareVersions(current, latest) >= 0) {
1517
+ deps.out(`tau ${current} is up to date. ${LAG_NOTE}`);
1518
+ return 0;
1519
+ }
1520
+ const plan = await planOf(deps, latest);
1521
+ if (typeof plan === "string") {
1522
+ deps.err(plan);
1523
+ return 1;
1524
+ }
1525
+ if ((await gatherStatus(deps, home)).running === "service" && !flags.noRestart && !flags.force) {
1526
+ const refusal = await refusalForRunningWork(deps, home);
1527
+ if (refusal) {
1528
+ deps.err(refusal);
1529
+ return 1;
1530
+ }
1531
+ }
1532
+ const question = `Install ${current} -> ${latest}${flags.noRestart ? "" : " and restart"}?`;
1533
+ if (!flags.yes) {
1534
+ if (!deps.isTty) {
1535
+ deps.err(NON_TTY_NEEDS_YES);
1536
+ return 1;
1537
+ }
1538
+ if (!await deps.confirm(question)) {
1539
+ deps.out("Cancelled.");
1540
+ return 0;
1541
+ }
1542
+ }
1543
+ const failed = await installStep(deps, plan, current);
1544
+ if (failed) {
1545
+ deps.err(failed);
1546
+ return 1;
1547
+ }
1548
+ return finish(deps, flags, plan, latest, home);
1549
+ }
1550
+ async function runUpdate(argv) {
1551
+ const { realUpdateDeps } = await import("./real-deps-f96UmLBz.js");
1552
+ process.exitCode = await runUpdateWith(await realUpdateDeps(), argv);
1553
+ }
1554
+ //#endregion
1436
1555
  //#region src/main.ts
1437
1556
  const apiCommandHandlers = Object.fromEntries(COMMAND_NOUNS.map((noun) => [noun, (argv) => runApiCommand(noun, argv)]));
1438
1557
  /** The one dispatch table: command word to handler. */
@@ -1444,6 +1563,7 @@ const commandTable = {
1444
1563
  status: runStatus,
1445
1564
  open: runOpen,
1446
1565
  logs: runLogs,
1566
+ update: runUpdate,
1447
1567
  service: runServiceCommand,
1448
1568
  skill: runSkill,
1449
1569
  help: runHelp,