@caddis/cli 0.4.57 → 0.4.59

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/cli.js CHANGED
@@ -1209,7 +1209,7 @@ var require_command = __commonJS({
1209
1209
  "use strict";
1210
1210
  var EventEmitter = __require("events").EventEmitter;
1211
1211
  var childProcess = __require("child_process");
1212
- var path6 = __require("path");
1212
+ var path7 = __require("path");
1213
1213
  var fs = __require("fs");
1214
1214
  var process3 = __require("process");
1215
1215
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -2222,9 +2222,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2222
2222
  let launchWithNode = false;
2223
2223
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2224
2224
  function findFile(baseDir, baseName) {
2225
- const localBin = path6.resolve(baseDir, baseName);
2225
+ const localBin = path7.resolve(baseDir, baseName);
2226
2226
  if (fs.existsSync(localBin)) return localBin;
2227
- if (sourceExt.includes(path6.extname(baseName))) return void 0;
2227
+ if (sourceExt.includes(path7.extname(baseName))) return void 0;
2228
2228
  const foundExt = sourceExt.find(
2229
2229
  (ext) => fs.existsSync(`${localBin}${ext}`)
2230
2230
  );
@@ -2242,17 +2242,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
2242
2242
  } catch {
2243
2243
  resolvedScriptPath = this._scriptPath;
2244
2244
  }
2245
- executableDir = path6.resolve(
2246
- path6.dirname(resolvedScriptPath),
2245
+ executableDir = path7.resolve(
2246
+ path7.dirname(resolvedScriptPath),
2247
2247
  executableDir
2248
2248
  );
2249
2249
  }
2250
2250
  if (executableDir) {
2251
2251
  let localFile = findFile(executableDir, executableFile);
2252
2252
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2253
- const legacyName = path6.basename(
2253
+ const legacyName = path7.basename(
2254
2254
  this._scriptPath,
2255
- path6.extname(this._scriptPath)
2255
+ path7.extname(this._scriptPath)
2256
2256
  );
2257
2257
  if (legacyName !== this._name) {
2258
2258
  localFile = findFile(
@@ -2263,7 +2263,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2263
2263
  }
2264
2264
  executableFile = localFile || executableFile;
2265
2265
  }
2266
- launchWithNode = sourceExt.includes(path6.extname(executableFile));
2266
+ launchWithNode = sourceExt.includes(path7.extname(executableFile));
2267
2267
  let proc;
2268
2268
  if (process3.platform !== "win32") {
2269
2269
  if (launchWithNode) {
@@ -3178,7 +3178,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3178
3178
  * @return {Command}
3179
3179
  */
3180
3180
  nameFromFilename(filename) {
3181
- this._name = path6.basename(filename, path6.extname(filename));
3181
+ this._name = path7.basename(filename, path7.extname(filename));
3182
3182
  return this;
3183
3183
  }
3184
3184
  /**
@@ -3192,9 +3192,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3192
3192
  * @param {string} [path]
3193
3193
  * @return {(string|null|Command)}
3194
3194
  */
3195
- executableDir(path7) {
3196
- if (path7 === void 0) return this._executableDir;
3197
- this._executableDir = path7;
3195
+ executableDir(path8) {
3196
+ if (path8 === void 0) return this._executableDir;
3197
+ this._executableDir = path8;
3198
3198
  return this;
3199
3199
  }
3200
3200
  /**
@@ -4644,6 +4644,7 @@ async function driveAgents(entries, options) {
4644
4644
  spinner?.stop(`${label} \u2014 ${color.green("done")}`, 0);
4645
4645
  if (!spinner) item("ok", `${label} \u2014 done`);
4646
4646
  for (const step of result.steps) detail(step.command);
4647
+ if (result.message) detail(result.message);
4647
4648
  continue;
4648
4649
  }
4649
4650
  outcome.failed += 1;
@@ -4873,10 +4874,10 @@ function keysFilePath(env) {
4873
4874
  const expanded = expandHome(configured);
4874
4875
  return isAbsolute(expanded) ? expanded : expanded;
4875
4876
  }
4876
- function parseKeysFile(path6) {
4877
+ function parseKeysFile(path7) {
4877
4878
  let text;
4878
4879
  try {
4879
- text = readFileSync3(path6, "utf8");
4880
+ text = readFileSync3(path7, "utf8");
4880
4881
  } catch {
4881
4882
  return {};
4882
4883
  }
@@ -4934,7 +4935,7 @@ __export(keys_exports, {
4934
4935
  probeKey: () => probeKey,
4935
4936
  upsertKeyLines: () => upsertKeyLines
4936
4937
  });
4937
- import { chmodSync, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4938
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
4938
4939
  import { dirname } from "path";
4939
4940
  async function probeKey(baseUrl, model, apiKey) {
4940
4941
  if (!baseUrl) return { ok: false, rejected: false, detail: "no endpoint for this provider" };
@@ -4988,18 +4989,18 @@ function upsertKeyLines(existing, updates) {
4988
4989
  if (!text.endsWith("\n")) text += "\n";
4989
4990
  return text;
4990
4991
  }
4991
- function restrictPermissions(path6) {
4992
+ function restrictPermissions(path7) {
4992
4993
  if (process.platform === "win32") return null;
4993
4994
  try {
4994
- chmodSync(path6, 384);
4995
+ chmodSync(path7, 384);
4995
4996
  return null;
4996
4997
  } catch (error) {
4997
4998
  return error instanceof Error ? error.message : String(error);
4998
4999
  }
4999
5000
  }
5000
5001
  async function keys(options = {}) {
5001
- const path6 = keysFilePath(process.env);
5002
- const fileKeys = parseKeysFile(path6);
5002
+ const path7 = keysFilePath(process.env);
5003
+ const fileKeys = parseKeysFile(path7);
5003
5004
  const names = Object.keys(PROVIDERS).sort();
5004
5005
  const current = names.map((name) => {
5005
5006
  const preset = PROVIDERS[name];
@@ -5015,7 +5016,7 @@ async function keys(options = {}) {
5015
5016
  if (options.check) {
5016
5017
  const configured = current.filter((entry) => entry.value);
5017
5018
  if (!configured.length) {
5018
- process.stdout.write(`no provider keys configured (looked in $ENV and ${path6})
5019
+ process.stdout.write(`no provider keys configured (looked in $ENV and ${path7})
5019
5020
  `);
5020
5021
  return 1;
5021
5022
  }
@@ -5030,7 +5031,7 @@ async function keys(options = {}) {
5030
5031
  return bad ? 1 : 0;
5031
5032
  }
5032
5033
  Ie("caddis keys");
5033
- process.stdout.write(` keys file: ${path6}
5034
+ process.stdout.write(` keys file: ${path7}
5034
5035
 
5035
5036
  `);
5036
5037
  for (const entry of current) {
@@ -5090,20 +5091,20 @@ async function keys(options = {}) {
5090
5091
  return 0;
5091
5092
  }
5092
5093
  if (options.dryRun) {
5093
- Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path6}`);
5094
+ Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path7}`);
5094
5095
  return 0;
5095
5096
  }
5096
5097
  let existing = "";
5097
5098
  try {
5098
- existing = readFileSync4(path6, "utf8");
5099
+ existing = readFileSync4(path7, "utf8");
5099
5100
  } catch {
5100
5101
  existing = "";
5101
5102
  }
5102
- mkdirSync(dirname(path6), { recursive: true });
5103
- writeFileSync(path6, upsertKeyLines(existing, collected), { encoding: "utf8" });
5104
- const permWarning = restrictPermissions(path6);
5103
+ mkdirSync2(dirname(path7), { recursive: true });
5104
+ writeFileSync(path7, upsertKeyLines(existing, collected), { encoding: "utf8" });
5105
+ const permWarning = restrictPermissions(path7);
5105
5106
  process.stdout.write(`
5106
- wrote ${Object.keys(collected).join(", ")} to ${path6}
5107
+ wrote ${Object.keys(collected).join(", ")} to ${path7}
5107
5108
  `);
5108
5109
  if (permWarning) process.stdout.write(color.yellow(` could not set 0600 on the keys file: ${permWarning}
5109
5110
  `));
@@ -5172,26 +5173,102 @@ var init_update = __esm({
5172
5173
  }
5173
5174
  });
5174
5175
 
5176
+ // src/util/sessions.ts
5177
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
5178
+ import os3 from "os";
5179
+ import path5 from "path";
5180
+ function caddisHome() {
5181
+ return process.env.CADDIS_HOME || os3.homedir();
5182
+ }
5183
+ function sessionsDir(home = caddisHome()) {
5184
+ return path5.join(home, ".caddis", "sessions");
5185
+ }
5186
+ function pidAlive(pid) {
5187
+ if (!Number.isInteger(pid) || pid <= 0) return false;
5188
+ try {
5189
+ process.kill(pid, 0);
5190
+ return true;
5191
+ } catch (error) {
5192
+ return error.code === "EPERM";
5193
+ }
5194
+ }
5195
+ function parse2(raw) {
5196
+ if (!raw || typeof raw !== "object") return null;
5197
+ const r2 = raw;
5198
+ const pid = Number(r2.pid);
5199
+ const lastSeen = Number(r2.last_seen);
5200
+ if (typeof r2.session_id !== "string" || !Number.isInteger(pid) || !Number.isFinite(lastSeen)) return null;
5201
+ return {
5202
+ sessionId: r2.session_id,
5203
+ harness: typeof r2.harness === "string" ? r2.harness : "claude",
5204
+ caddisVersion: typeof r2.caddis_version === "string" && r2.caddis_version ? r2.caddis_version : null,
5205
+ pid,
5206
+ started: (Number(r2.started) || lastSeen) * 1e3,
5207
+ lastSeen: lastSeen * 1e3
5208
+ };
5209
+ }
5210
+ function liveSessions(home = caddisHome(), now = Date.now(), alive = pidAlive) {
5211
+ const dir = sessionsDir(home);
5212
+ let names;
5213
+ try {
5214
+ names = readdirSync2(dir);
5215
+ } catch {
5216
+ return [];
5217
+ }
5218
+ const out = [];
5219
+ for (const name of names) {
5220
+ if (!name.endsWith(".json") || name.startsWith(".")) continue;
5221
+ let beat = null;
5222
+ try {
5223
+ beat = parse2(JSON.parse(readFileSync5(path5.join(dir, name), "utf8")));
5224
+ } catch {
5225
+ continue;
5226
+ }
5227
+ if (beat && now - beat.lastSeen <= STALE_AFTER_MS && alive(beat.pid)) out.push(beat);
5228
+ }
5229
+ return out.sort((a, b3) => a.started - b3.started);
5230
+ }
5231
+ function olderSessions(sessions, installed) {
5232
+ const out = [];
5233
+ for (const session of sessions) {
5234
+ const current = Object.hasOwn(installed, session.harness) ? installed[session.harness] : void 0;
5235
+ if (typeof current !== "string" || !current || !session.caddisVersion) continue;
5236
+ const order = compareVersions(session.caddisVersion, current);
5237
+ if (order !== null && order < 0) out.push({ ...session, installed: current });
5238
+ }
5239
+ return out;
5240
+ }
5241
+ var STALE_AFTER_MS;
5242
+ var init_sessions = __esm({
5243
+ "src/util/sessions.ts"() {
5244
+ "use strict";
5245
+ init_semver();
5246
+ STALE_AFTER_MS = 7 * 24 * 3600 * 1e3;
5247
+ }
5248
+ });
5249
+
5175
5250
  // src/commands/doctor.ts
5176
5251
  var doctor_exports = {};
5177
5252
  __export(doctor_exports, {
5178
5253
  doctor: () => doctor
5179
5254
  });
5180
5255
  import { existsSync as existsSync5 } from "fs";
5181
- import os3 from "os";
5256
+ import os4 from "os";
5182
5257
  import process2 from "process";
5183
- import path5 from "path";
5258
+ import path6 from "path";
5184
5259
  async function doctor(options) {
5185
5260
  const report = await gather(options.adapters, { checkCliUpdate: true });
5186
5261
  const findings = [];
5262
+ const older = olderSessions(liveSessions(options.home ?? caddisHome()), installedVersions(report));
5187
5263
  if (options.json) {
5188
5264
  const project = await renderProject(findings, { quiet: true });
5189
- line(JSON.stringify(toJson(report, project), null, 2));
5265
+ line(JSON.stringify({ ...toJson(report, project), olderSessions: older }, null, 2));
5190
5266
  return 0;
5191
5267
  }
5192
5268
  renderBanner({ cliVersion: report.cliVersion, poolVersion: report.poolVersion });
5193
5269
  renderEnvironment(report, findings);
5194
5270
  renderAgents(report, findings);
5271
+ renderSessions(older, findings);
5195
5272
  await renderProject(findings);
5196
5273
  renderSummary(report, findings);
5197
5274
  return options.strict && findings.some((f) => f.kind === "problem") ? 1 : 0;
@@ -5205,7 +5282,7 @@ function renderEnvironment(report, problems) {
5205
5282
  detail(`caddis requires node >= ${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}`);
5206
5283
  problems.push({ kind: "problem", text: `node ${process2.versions.node} is below the required ${REQUIRED_NODE_MAJOR}.${REQUIRED_NODE_MINOR}`, fix: "upgrade node" });
5207
5284
  }
5208
- item("info", `platform ${process2.platform} ${process2.arch} (${os3.release()})`);
5285
+ item("info", `platform ${process2.platform} ${process2.arch} (${os4.release()})`);
5209
5286
  if (report.cliUpdate) {
5210
5287
  item("warn", `@caddis/cli ${report.cliVersion} ${color.dim("\u2192")} ${color.bold(report.cliUpdate.latest)} available`);
5211
5288
  detail('so EVERY "current" below may be wrong \u2014 it only means "matches the pool this install carries"');
@@ -5257,6 +5334,35 @@ function renderAgents(report, problems) {
5257
5334
  }
5258
5335
  }
5259
5336
  }
5337
+ function installedVersions(report) {
5338
+ const out = {};
5339
+ for (const entry of report.agents) {
5340
+ if (entry.status.version) out[entry.adapter.id] = entry.status.version;
5341
+ }
5342
+ return out;
5343
+ }
5344
+ function renderSessions(older, problems) {
5345
+ heading("Sessions");
5346
+ if (older.length === 0) {
5347
+ item("ok", "no live session runs an older caddis");
5348
+ return;
5349
+ }
5350
+ for (const session of older) {
5351
+ const date = new Date(session.started);
5352
+ const started = Number.isNaN(date.getTime()) ? "unknown" : `${date.toISOString().replace("T", " ").slice(0, 16)} UTC`;
5353
+ item(
5354
+ "warn",
5355
+ `${session.harness} session ${color.bold(session.sessionId)} (pid ${session.pid}, started ${started}) runs caddis ${color.yellow(session.caddisVersion ?? "?")}, installed ${color.bold(session.installed)}`
5356
+ );
5357
+ }
5358
+ const fix = "restart these sessions (or run /reload-plugins in each) to load the installed caddis";
5359
+ hint(color.bold(fix));
5360
+ problems.push({
5361
+ kind: "problem",
5362
+ text: `${older.length} live session${older.length === 1 ? "" : "s"} still run${older.length === 1 ? "s" : ""} an older caddis`,
5363
+ fix
5364
+ });
5365
+ }
5260
5366
  async function renderProject(problems, options) {
5261
5367
  const quiet = options?.quiet ?? false;
5262
5368
  const python = await findBin("python");
@@ -5273,7 +5379,7 @@ async function renderProject(problems, options) {
5273
5379
  return { ran: false, exitCode: null };
5274
5380
  }
5275
5381
  const bundle = bundlePath("antigravity-plugin", { requireManifest: false });
5276
- const script = bundle ? path5.join(bundle, "scripts", "claudster_doctor.py") : null;
5382
+ const script = bundle ? path6.join(bundle, "scripts", "claudster_doctor.py") : null;
5277
5383
  if (!script || !existsSync5(script)) {
5278
5384
  if (!quiet) {
5279
5385
  heading("Project");
@@ -5473,6 +5579,7 @@ var init_doctor = __esm({
5473
5579
  init_exec();
5474
5580
  init_pkg();
5475
5581
  init_which();
5582
+ init_sessions();
5476
5583
  init_report();
5477
5584
  REQUIRED_NODE_MAJOR = 20;
5478
5585
  REQUIRED_NODE_MINOR = 19;
@@ -5655,14 +5762,14 @@ function* pluginBlocks(stdout) {
5655
5762
  if (current) yield current;
5656
5763
  }
5657
5764
  async function detect() {
5658
- const path6 = await findBin(BIN);
5659
- if (!path6) {
5765
+ const path7 = await findBin(BIN);
5766
+ if (!path7) {
5660
5767
  return { present: false, note: "no `claude` binary on PATH" };
5661
5768
  }
5662
5769
  const version = await run(BIN, ["--version"], { timeout: 2e4 });
5663
5770
  return {
5664
5771
  present: true,
5665
- path: path6,
5772
+ path: path7,
5666
5773
  agentVersion: version.ok ? version.stdout.trim().split(/\s+/)[0] || void 0 : void 0,
5667
5774
  note: version.ok ? void 0 : "binary found but `claude --version` failed"
5668
5775
  };
@@ -5761,7 +5868,7 @@ var claudeAdapter = {
5761
5868
  // src/agents/agy.ts
5762
5869
  init_exec();
5763
5870
  init_which();
5764
- import { readFileSync as readFileSync2, existsSync as existsSync2, mkdtempSync, cpSync, rmSync } from "fs";
5871
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, mkdtempSync, mkdirSync, cpSync, rmSync, renameSync } from "fs";
5765
5872
  import os from "os";
5766
5873
  import path2 from "path";
5767
5874
 
@@ -5832,6 +5939,133 @@ function backupInstalledPlugin(home) {
5832
5939
  }
5833
5940
  };
5834
5941
  }
5942
+ function agyStagingDir(home = os.homedir(), plugin = PLUGIN2) {
5943
+ return path2.join(home, ".caddis", "agy-staged", plugin);
5944
+ }
5945
+ function errorText(error) {
5946
+ if (!(error instanceof Error)) return String(error);
5947
+ const code = error.code;
5948
+ const kind = code ?? error.constructor.name;
5949
+ const message = code && error.message.startsWith(`${code}: `) ? error.message.slice(code.length + 2) : error.message;
5950
+ return `${kind}: ${message}`;
5951
+ }
5952
+ function stampMs(stamp) {
5953
+ const n = Number(stamp);
5954
+ if (!Number.isFinite(n)) return -1;
5955
+ return n > 1e15 ? (n - 621355968e9) / 1e4 : n;
5956
+ }
5957
+ function isWhole(dir, required) {
5958
+ return existsSync2(dir) && required.every((file) => existsSync2(path2.join(dir, file)));
5959
+ }
5960
+ function recoverInterruptedSwap(home, plugin, required) {
5961
+ const live = pluginDirFor(home, plugin);
5962
+ const staged = agyStagingDir(home, plugin);
5963
+ const parent = path2.dirname(staged);
5964
+ const prefix = `${path2.basename(staged)}.old-`;
5965
+ let olds;
5966
+ try {
5967
+ olds = readdirSync(parent).filter((name) => name.startsWith(prefix)).sort((x2, y3) => stampMs(y3.slice(prefix.length)) - stampMs(x2.slice(prefix.length))).map((name) => path2.join(parent, name));
5968
+ } catch {
5969
+ return null;
5970
+ }
5971
+ if (isWhole(live, required)) {
5972
+ for (const dir of olds) {
5973
+ try {
5974
+ rmSync(dir, { recursive: true, force: true });
5975
+ } catch {
5976
+ }
5977
+ }
5978
+ return null;
5979
+ }
5980
+ const source = olds.find((dir) => isWhole(dir, required));
5981
+ if (!source) return null;
5982
+ try {
5983
+ if (existsSync2(live)) renameSync(live, `${staged}.broken-${Date.now()}`);
5984
+ mkdirSync(path2.dirname(live), { recursive: true });
5985
+ renameSync(source, live);
5986
+ return `restored the previous plugin from ${source}`;
5987
+ } catch (error) {
5988
+ return `could not restore the previous plugin from ${source} (${errorText(error)}); it is kept there`;
5989
+ }
5990
+ }
5991
+ function stageAndSwap(bundleDir, home, plugin, required, rename = renameSync) {
5992
+ const live = pluginDirFor(home, plugin);
5993
+ const staged = agyStagingDir(home, plugin);
5994
+ const recovered = recoverInterruptedSwap(home, plugin, required);
5995
+ const note = (detail2) => recovered ? `${recovered}; ${detail2}` : detail2;
5996
+ try {
5997
+ rmSync(staged, { recursive: true, force: true });
5998
+ mkdirSync(path2.dirname(staged), { recursive: true });
5999
+ cpSync(bundleDir, staged, { recursive: true });
6000
+ } catch (error) {
6001
+ return { swapped: false, reason: "stage-failed", staged: null, detail: note(`staging failed (${errorText(error)})`) };
6002
+ }
6003
+ const missing = required.filter((file) => !existsSync2(path2.join(staged, file)));
6004
+ if (missing.length > 0) {
6005
+ return {
6006
+ swapped: false,
6007
+ reason: "stage-rejected",
6008
+ staged: null,
6009
+ kept: staged,
6010
+ detail: note(`the staged copy lacks ${missing.join(", ")}; the live plugin was not touched`)
6011
+ };
6012
+ }
6013
+ if (!existsSync2(live)) {
6014
+ return {
6015
+ swapped: false,
6016
+ reason: "first-install",
6017
+ staged,
6018
+ detail: note("first install: agy must register the plugin with `agy plugin install`")
6019
+ };
6020
+ }
6021
+ const old = `${staged}.old-${Date.now()}`;
6022
+ try {
6023
+ rename(live, old);
6024
+ } catch (error) {
6025
+ return {
6026
+ swapped: false,
6027
+ reason: "in-use",
6028
+ staged,
6029
+ detail: note(`could not move the live plugin aside (${errorText(error)}); agy may be using it`)
6030
+ };
6031
+ }
6032
+ try {
6033
+ rename(staged, live);
6034
+ } catch (error) {
6035
+ const why = `the swap failed (${errorText(error)})`;
6036
+ try {
6037
+ rename(old, live);
6038
+ return { swapped: false, reason: "swap-failed", staged: existsSync2(staged) ? staged : null, detail: `${why}; the old plugin is back` };
6039
+ } catch (backError) {
6040
+ return {
6041
+ swapped: false,
6042
+ reason: "swap-failed",
6043
+ staged: existsSync2(staged) ? staged : null,
6044
+ detail: `${why}; putting the old plugin back FAILED (${errorText(backError)}), it is kept at ${old}`
6045
+ };
6046
+ }
6047
+ }
6048
+ try {
6049
+ rmSync(old, { recursive: true, force: true });
6050
+ } catch {
6051
+ }
6052
+ return { swapped: true, reason: "swapped", staged: live, detail: note("swapped in by rename") };
6053
+ }
6054
+ function runningAgyMessage(pids, plugin, outcome, alreadySwapped = []) {
6055
+ let text = `agy is running (${pids}), so ${plugin} was not installed: ${outcome.detail}.`;
6056
+ if (alreadySwapped.length > 0) text += ` ${alreadySwapped.join(", ")} was already swapped in.`;
6057
+ if (outcome.kept) {
6058
+ text += ` Nothing was installed; the rejected copy is kept at ${outcome.kept}.`;
6059
+ } else if (outcome.staged && existsSync2(outcome.staged)) {
6060
+ text += ` The new version is staged at ${outcome.staged}. Stop agy and run \`caddis update\` again to put it in place.`;
6061
+ } else {
6062
+ text += " Nothing is staged. Stop agy and run `caddis update` again.";
6063
+ }
6064
+ if (outcome.reason === "first-install") {
6065
+ text += ` Or set ${ALLOW_RUNNING_ENV}=1 if you know no agy holds the plugin folder.`;
6066
+ }
6067
+ return text;
6068
+ }
5835
6069
  function pluginManifestPath(home = os.homedir(), plugin = PLUGIN2) {
5836
6070
  return path2.join(home, ".gemini", "config", "plugins", plugin, "plugin.json");
5837
6071
  }
@@ -5952,18 +6186,16 @@ async function drive2(_action, options) {
5952
6186
  message: "dry run \u2014 nothing executed"
5953
6187
  };
5954
6188
  }
6189
+ const home = options.home ?? os.homedir();
6190
+ const recovered = [
6191
+ recoverInterruptedSwap(home, PLUGIN2, REQUIRED_PLUGIN_FILES),
6192
+ recoverInterruptedSwap(home, EXTRAS_PLUGIN, ["plugin.json"])
6193
+ ].filter((note) => note !== null);
6194
+ const withRecovery = (result) => recovered.length === 0 ? result : { ...result, message: [...recovered, result.message].filter(Boolean).join("; ") };
5955
6195
  if (process.env[ALLOW_RUNNING_ENV] !== "1") {
5956
6196
  const running = await listAgyPids();
5957
- if (running.length > 0) {
5958
- return {
5959
- ok: false,
5960
- skipped: false,
5961
- steps: [],
5962
- message: `agy is running (PID ${running.join(", ")}). Installing now can empty the caddis plugin under it, and agy then refuses every edit. Stop agy and run this again, or set ${ALLOW_RUNNING_ENV}=1 if you know no agy holds the plugin folder.`
5963
- };
5964
- }
6197
+ if (running.length > 0) return withRecovery(swapUnderRunningAgy(planned, bundle, home, running));
5965
6198
  }
5966
- const home = options.home ?? os.homedir();
5967
6199
  const backup = backupInstalledPlugin(home);
5968
6200
  const cleanups = [];
5969
6201
  if (backup) cleanups.push(backup.cleanup);
@@ -6002,11 +6234,43 @@ async function drive2(_action, options) {
6002
6234
  return { ok: false, skipped: false, steps: steps3, message: `\`${command}\` failed` };
6003
6235
  }
6004
6236
  }
6005
- return { ok: true, skipped: false, steps: steps3 };
6237
+ for (const plugin of [PLUGIN2, EXTRAS_PLUGIN]) {
6238
+ try {
6239
+ rmSync(agyStagingDir(home, plugin), { recursive: true, force: true });
6240
+ } catch {
6241
+ }
6242
+ }
6243
+ return withRecovery({ ok: true, skipped: false, steps: steps3 });
6006
6244
  } finally {
6007
6245
  cleanups.forEach((fn) => fn());
6008
6246
  }
6009
6247
  }
6248
+ function swapUnderRunningAgy(planned, bundle, home, running) {
6249
+ const pids = `PID ${running.join(", ")}`;
6250
+ const steps3 = [];
6251
+ const swapped = [];
6252
+ for (const step of planned) {
6253
+ const core = step.bundleDir === bundle;
6254
+ const plugin = core ? PLUGIN2 : EXTRAS_PLUGIN;
6255
+ const outcome = stageAndSwap(step.bundleDir, home, plugin, core ? REQUIRED_PLUGIN_FILES : ["plugin.json"]);
6256
+ steps3.push({
6257
+ command: `stage ${step.bundleDir} and swap it into ${pluginDirFor(home, plugin)}`,
6258
+ ok: outcome.swapped,
6259
+ code: null,
6260
+ output: outcome.swapped ? void 0 : outcome.detail
6261
+ });
6262
+ if (!outcome.swapped) {
6263
+ return { ok: false, skipped: false, steps: steps3, message: runningAgyMessage(pids, plugin, outcome, swapped) };
6264
+ }
6265
+ swapped.push(plugin);
6266
+ }
6267
+ return {
6268
+ ok: true,
6269
+ skipped: false,
6270
+ steps: steps3,
6271
+ message: `agy is running (${pids}): the new plugin was staged and swapped in by rename; new agy sessions load it.`
6272
+ };
6273
+ }
6010
6274
  var agyAdapter = {
6011
6275
  id: "agy",
6012
6276
  name: "agy (Antigravity)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.57",
3
+ "version": "0.4.59",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",