@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/bundles/antigravity-plugin/caddis_statusline.py +45 -11
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +69 -23
- package/bundles/antigravity-plugin/scripts/caddis_docs_pr.py +9 -8
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +4 -2
- package/bundles/antigravity-plugin/scripts/caddis_review.py +22 -2
- package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +14 -8
- package/bundles/antigravity-plugin/scripts/caddis_roadmap_run.py +26 -9
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +24 -4
- package/bundles/antigravity-plugin/scripts/caddis_spend.py +120 -10
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +45 -11
- package/bundles/antigravity-plugin/scripts/caddis_worktree_close.py +14 -9
- package/bundles/antigravity-plugin/scripts/caddis_worktree_safety.py +12 -0
- package/bundles/antigravity-plugin/scripts/hook_log.py +13 -2
- package/bundles/antigravity-plugin/scripts/secret_filter.py +6 -2
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +16 -6
- package/bundles/antigravity-plugin/skills/mmr/SKILL.md +3 -0
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +3 -2
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +52 -175
- package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +52 -175
- package/bundles/manifest.json +2 -2
- package/dist/cli.js +310 -46
- package/package.json +1 -1
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
|
|
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 =
|
|
2225
|
+
const localBin = path7.resolve(baseDir, baseName);
|
|
2226
2226
|
if (fs.existsSync(localBin)) return localBin;
|
|
2227
|
-
if (sourceExt.includes(
|
|
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 =
|
|
2246
|
-
|
|
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 =
|
|
2253
|
+
const legacyName = path7.basename(
|
|
2254
2254
|
this._scriptPath,
|
|
2255
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
3196
|
-
if (
|
|
3197
|
-
this._executableDir =
|
|
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(
|
|
4877
|
+
function parseKeysFile(path7) {
|
|
4877
4878
|
let text;
|
|
4878
4879
|
try {
|
|
4879
|
-
text = readFileSync3(
|
|
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(
|
|
4992
|
+
function restrictPermissions(path7) {
|
|
4992
4993
|
if (process.platform === "win32") return null;
|
|
4993
4994
|
try {
|
|
4994
|
-
chmodSync(
|
|
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
|
|
5002
|
-
const fileKeys = parseKeysFile(
|
|
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 ${
|
|
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: ${
|
|
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 ${
|
|
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(
|
|
5099
|
+
existing = readFileSync4(path7, "utf8");
|
|
5099
5100
|
} catch {
|
|
5100
5101
|
existing = "";
|
|
5101
5102
|
}
|
|
5102
|
-
|
|
5103
|
-
writeFileSync(
|
|
5104
|
-
const permWarning = restrictPermissions(
|
|
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 ${
|
|
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
|
|
5256
|
+
import os4 from "os";
|
|
5182
5257
|
import process2 from "process";
|
|
5183
|
-
import
|
|
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} (${
|
|
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 ?
|
|
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
|
|
5659
|
-
if (!
|
|
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:
|
|
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
|
-
|
|
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