@standardagents/code 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { stdout, stdin } from 'process';
8
8
  import fsp from 'fs/promises';
9
9
  import crypto from 'crypto';
10
10
  import readline from 'readline';
11
+ import { fileURLToPath } from 'url';
11
12
 
12
13
  // src/api.ts
13
14
  function classifyConnectError(err, endpoint) {
@@ -4096,8 +4097,10 @@ var PKG_NAME = "@standardagents/code";
4096
4097
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
4097
4098
  var CACHE_REL_DIR = ".config/standardagents-cli";
4098
4099
  var CACHE_FILE = "update-check.json";
4100
+ var STATE_FILE = "auto-update.json";
4099
4101
  var CACHE_TTL_MS = 1e3 * 60 * 60 * 24;
4100
4102
  var CHECK_TIMEOUT_MS = 4e3;
4103
+ var IN_FLIGHT_TTL_MS = 15 * 60 * 1e3;
4101
4104
  function cacheDir() {
4102
4105
  return path3.join(homedir(), CACHE_REL_DIR);
4103
4106
  }
@@ -4120,16 +4123,86 @@ function writeCache(latest) {
4120
4123
  } catch {
4121
4124
  }
4122
4125
  }
4123
- async function checkForUpdate(currentVersion) {
4124
- if (!currentVersion) return null;
4125
- if (process.env.NO_UPDATE_NOTIFIER || process.env.CI) return null;
4126
- const cached = readCache();
4127
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
4128
- if (cached.latest !== currentVersion) {
4129
- return { current: currentVersion, latest: cached.latest };
4130
- }
4126
+ function readAutoUpdateState(dir = cacheDir()) {
4127
+ try {
4128
+ const raw = fs4.readFileSync(path3.join(dir, STATE_FILE), "utf-8");
4129
+ const state = JSON.parse(raw);
4130
+ return typeof state?.version === "string" ? state : null;
4131
+ } catch {
4131
4132
  return null;
4132
4133
  }
4134
+ }
4135
+ function writeAutoUpdateState(state, dir = cacheDir()) {
4136
+ try {
4137
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
4138
+ fs4.writeFileSync(path3.join(dir, STATE_FILE), JSON.stringify(state));
4139
+ } catch {
4140
+ }
4141
+ }
4142
+ function clearAutoUpdateState(dir = cacheDir()) {
4143
+ try {
4144
+ fs4.unlinkSync(path3.join(dir, STATE_FILE));
4145
+ } catch {
4146
+ }
4147
+ }
4148
+ function detectPackageManager(selfPath = fileURLToPath(import.meta.url), env = process.env) {
4149
+ const norm = selfPath.split(/[\\/]/).join("/");
4150
+ if (!norm.includes("node_modules/@standardagents/code")) return null;
4151
+ const pnpmHome = (env.PNPM_HOME || "").split(/[\\/]/).join("/");
4152
+ if (pnpmHome && norm.startsWith(pnpmHome)) return "pnpm";
4153
+ if (norm.includes("/.pnpm/") || /\/pnpm\/global\//.test(norm)) return "pnpm";
4154
+ if (norm.includes("/.bun/")) return "bun";
4155
+ if (norm.includes("/.config/yarn/") || norm.includes("/.yarn/")) return "yarn";
4156
+ return "npm";
4157
+ }
4158
+ function updateCommand(pm) {
4159
+ switch (pm) {
4160
+ case "pnpm":
4161
+ return { cmd: "pnpm", args: ["add", "-g", `${PKG_NAME}@latest`], display: `pnpm add -g ${PKG_NAME}@latest` };
4162
+ case "yarn":
4163
+ return { cmd: "yarn", args: ["global", "add", `${PKG_NAME}@latest`], display: `yarn global add ${PKG_NAME}@latest` };
4164
+ case "bun":
4165
+ return { cmd: "bun", args: ["add", "-g", `${PKG_NAME}@latest`], display: `bun add -g ${PKG_NAME}@latest` };
4166
+ default:
4167
+ return { cmd: "npm", args: ["i", "-g", `${PKG_NAME}@latest`], display: `npm i -g ${PKG_NAME}@latest` };
4168
+ }
4169
+ }
4170
+ function decideAutoUpdate(info, opts) {
4171
+ const env = opts.env ?? process.env;
4172
+ if (env.STANDARD_CODE_NO_AUTO_UPDATE) return "disabled";
4173
+ if (!opts.pm) return "dev_checkout";
4174
+ const state = opts.state;
4175
+ if (state && state.version === info.latest) {
4176
+ if (state.exitCode === null) {
4177
+ const now = opts.now ?? Date.now();
4178
+ return now - state.startedAt < IN_FLIGHT_TTL_MS ? "in_flight" : "start";
4179
+ }
4180
+ return state.exitCode === 0 ? "path_shadowed" : "already_failed";
4181
+ }
4182
+ return "start";
4183
+ }
4184
+ function startBackgroundUpdate(latest, pm, dir = cacheDir()) {
4185
+ const startedAt = Date.now();
4186
+ writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir);
4187
+ const stateFile = path3.join(dir, STATE_FILE);
4188
+ const { cmd, args } = updateCommand(pm);
4189
+ const script = `const cp=require('child_process');const fs=require('fs');const r=cp.spawnSync(${JSON.stringify(cmd)},${JSON.stringify(args)},{shell:process.platform==='win32',encoding:'utf8'});const out=((r.stdout||'')+(r.stderr||'')).slice(-2000);fs.writeFileSync(${JSON.stringify(stateFile)},JSON.stringify({version:${JSON.stringify(latest)},startedAt:${startedAt},exitCode:r.status==null?-1:r.status,finishedAt:Date.now(),output:out}));`;
4190
+ try {
4191
+ const child = spawn(process.execPath, ["-e", script], { detached: true, stdio: "ignore" });
4192
+ child.unref();
4193
+ return true;
4194
+ } catch {
4195
+ writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir);
4196
+ return false;
4197
+ }
4198
+ }
4199
+ function consumeAppliedUpdate(currentVersion, dir = cacheDir()) {
4200
+ const state = readAutoUpdateState(dir);
4201
+ if (!state || state.version !== currentVersion) return null;
4202
+ clearAutoUpdateState(dir);
4203
+ return state.exitCode === 0 ? state : null;
4204
+ }
4205
+ async function fetchLatest(currentVersion) {
4133
4206
  const controller = new AbortController();
4134
4207
  const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
4135
4208
  try {
@@ -4142,55 +4215,48 @@ async function checkForUpdate(currentVersion) {
4142
4215
  });
4143
4216
  if (!res.ok) return null;
4144
4217
  const data = await res.json();
4145
- const latest = data["dist-tags"]?.latest;
4146
- if (!latest) return null;
4147
- writeCache(latest);
4148
- if (latest !== currentVersion) {
4149
- return { current: currentVersion, latest };
4150
- }
4151
- return null;
4218
+ return data["dist-tags"]?.latest ?? null;
4152
4219
  } catch {
4153
4220
  return null;
4154
4221
  } finally {
4155
4222
  clearTimeout(timeout);
4156
4223
  }
4157
4224
  }
4158
- async function forceCheckForUpdate(currentVersion) {
4225
+ async function checkForUpdate(currentVersion) {
4159
4226
  if (!currentVersion) return null;
4160
4227
  if (process.env.NO_UPDATE_NOTIFIER || process.env.CI) return null;
4161
- const controller = new AbortController();
4162
- const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
4163
- try {
4164
- const res = await fetch(REGISTRY_URL, {
4165
- signal: controller.signal,
4166
- headers: {
4167
- Accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
4168
- "User-Agent": `${PKG_NAME}/${currentVersion}`
4169
- }
4170
- });
4171
- if (!res.ok) return null;
4172
- const data = await res.json();
4173
- const latest = data["dist-tags"]?.latest;
4174
- if (!latest) return null;
4175
- writeCache(latest);
4176
- if (latest !== currentVersion) {
4177
- return { current: currentVersion, latest };
4228
+ const cached = readCache();
4229
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
4230
+ if (cached.latest !== currentVersion) {
4231
+ return { current: currentVersion, latest: cached.latest };
4178
4232
  }
4179
4233
  return null;
4180
- } catch {
4181
- return null;
4182
- } finally {
4183
- clearTimeout(timeout);
4184
4234
  }
4235
+ const latest = await fetchLatest(currentVersion);
4236
+ if (!latest) return null;
4237
+ writeCache(latest);
4238
+ return latest !== currentVersion ? { current: currentVersion, latest } : null;
4239
+ }
4240
+ async function forceCheckForUpdate(currentVersion) {
4241
+ if (!currentVersion) return null;
4242
+ if (process.env.NO_UPDATE_NOTIFIER || process.env.CI) return null;
4243
+ const latest = await fetchLatest(currentVersion);
4244
+ if (!latest) return null;
4245
+ writeCache(latest);
4246
+ return latest !== currentVersion ? { current: currentVersion, latest } : null;
4185
4247
  }
4186
- function runNpmUpdate() {
4248
+ function runUpdate(pm) {
4187
4249
  return new Promise((resolve) => {
4188
- const child = spawn("npm", ["i", "-g", `${PKG_NAME}@latest`], {
4189
- stdio: "inherit",
4190
- shell: true
4250
+ const { cmd, args } = updateCommand(pm);
4251
+ const child = spawn(cmd, args, {
4252
+ shell: process.platform === "win32",
4253
+ stdio: ["ignore", "pipe", "pipe"]
4191
4254
  });
4192
- child.on("close", (code) => resolve(code === 0));
4193
- child.on("error", () => resolve(false));
4255
+ let out = "";
4256
+ child.stdout?.on("data", (d) => out += d);
4257
+ child.stderr?.on("data", (d) => out += d);
4258
+ child.on("close", (code) => resolve({ ok: code === 0, output: out }));
4259
+ child.on("error", (err) => resolve({ ok: false, output: String(err) }));
4194
4260
  });
4195
4261
  }
4196
4262
 
@@ -4483,13 +4549,36 @@ ${c.dim}Press Control-C again to exit${c.reset}
4483
4549
  const loading = startLoader("Checking for updates");
4484
4550
  updateAvailable = await checkForUpdate(version);
4485
4551
  loading.stop();
4552
+ const applied = consumeAppliedUpdate(version);
4553
+ if (applied) {
4554
+ stdout.write(` ${c.green}\u2713${c.reset} ${c.dim}Standard Code updated to v${version}.${c.reset}
4555
+
4556
+ `);
4557
+ }
4486
4558
  if (updateAvailable) {
4487
- stdout.write(
4488
- ` ${c.teal}\u25C7${c.reset} ${c.dim}Update available:${c.reset} ${c.dim}v${updateAvailable.current}${c.reset} \u2192 ${c.bold}v${updateAvailable.latest}${c.reset}
4489
- ${c.dim}Run ${c.reset}${c.bold}npm i -g @standardagents/code@latest${c.reset}${c.dim} to update${c.reset}
4559
+ const pm = detectPackageManager();
4560
+ const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
4561
+ if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
4562
+ stdout.write(
4563
+ ` ${c.teal}\u27F3${c.reset} ${c.dim}Standard Code ${c.reset}${c.bold}v${updateAvailable.latest}${c.reset}${c.dim} is installing in the background \u2014 it applies on your next launch.${c.reset}
4490
4564
 
4491
4565
  `
4492
- );
4566
+ );
4567
+ } else if (decision === "in_flight") {
4568
+ stdout.write(
4569
+ ` ${c.teal}\u27F3${c.reset} ${c.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c.reset}
4570
+
4571
+ `
4572
+ );
4573
+ } else {
4574
+ const display = updateCommand(pm ?? "npm").display;
4575
+ stdout.write(
4576
+ ` ${c.teal}\u25C7${c.reset} ${c.dim}Update available:${c.reset} ${c.dim}v${updateAvailable.current}${c.reset} \u2192 ${c.bold}v${updateAvailable.latest}${c.reset}
4577
+ ${c.dim}Run ${c.reset}${c.bold}${display}${c.reset}${c.dim} to update${c.reset}
4578
+
4579
+ `
4580
+ );
4581
+ }
4493
4582
  }
4494
4583
  }
4495
4584
  const stored = getCredential(endpoint);
@@ -5048,7 +5137,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5048
5137
  },
5049
5138
  { name: "background", label: "Background processes", hint: "list / stop", run: () => runProcessMenu(tui, bgMgr) },
5050
5139
  { name: "keybindings", label: "Keyboard shortcuts", run: () => showKeybindings(tui) },
5051
- { name: "update", label: "Check for updates", hint: "check npm for a newer version", run: () => runUpdateCommand(tui) },
5140
+ { name: "update", label: "Check for updates", hint: "check for a newer version", run: () => runUpdateCommand(tui) },
5052
5141
  { name: "logout", label: "Sign out", hint: "delete the saved token & quit", run: () => logout() },
5053
5142
  { name: "quit", label: "Quit", run: () => quit() }
5054
5143
  ]);
@@ -5315,21 +5404,26 @@ async function runUpdateCommand(tui) {
5315
5404
  const { latest } = result;
5316
5405
  tui.print(`
5317
5406
  ${c.yellow}\u27F3${c.reset} Update available: ${c.gray}v${version}${c.reset} \u2192 ${c.green}v${latest}${c.reset}`);
5318
- const choice = await tui.select(`Update now with \`npm i -g @standardagents/code@latest\`?`, [
5407
+ const pm = detectPackageManager();
5408
+ if (!pm) {
5409
+ tui.print(` ${c.gray}This is a source checkout \u2014 pull the repo to update.${c.reset}`);
5410
+ return;
5411
+ }
5412
+ const { display } = updateCommand(pm);
5413
+ const choice = await tui.select(`Update now with \`${display}\`?`, [
5319
5414
  { label: "Yes, update now", value: "yes" },
5320
5415
  { label: "No, skip", value: "no" }
5321
5416
  ]);
5322
5417
  if (choice === "yes") {
5323
- tui.print(` ${c.gray}Running npm i -g @standardagents/code@latest\u2026${c.reset}`);
5324
- try {
5325
- const ok = await runNpmUpdate();
5326
- if (ok) {
5327
- tui.print(` ${c.green}\u2713${c.reset} Updated to v${latest}. Restart to use the new version.`);
5328
- } else {
5329
- tui.print(` ${c.red}\u2717${c.reset} Update failed.`);
5418
+ tui.print(` ${c.gray}Running ${display}\u2026${c.reset}`);
5419
+ const { ok, output: pmOutput } = await runUpdate(pm);
5420
+ if (ok) {
5421
+ tui.print(` ${c.green}\u2713${c.reset} Updated to v${latest}. Restart to use the new version.`);
5422
+ } else {
5423
+ tui.print(` ${c.red}\u2717${c.reset} Update failed:`);
5424
+ for (const line of pmOutput.trim().split("\n").slice(-6)) {
5425
+ tui.print(` ${c.dim}${line}${c.reset}`);
5330
5426
  }
5331
- } catch (e) {
5332
- tui.print(` ${c.red}\u2717${c.reset} Update failed: ${e}`);
5333
5427
  }
5334
5428
  } else {
5335
5429
  tui.print(` ${c.gray}Skipped. Run /update later.${c.reset}`);