@shipers-dev/multi 0.17.2 → 0.18.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 +184 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16073,7 +16073,7 @@ var StreamEventInputSchema = exports_external.object({
|
|
|
16073
16073
|
});
|
|
16074
16074
|
// src/worktree.ts
|
|
16075
16075
|
import { spawn } from "child_process";
|
|
16076
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, symlinkSync } from "fs";
|
|
16076
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, symlinkSync, rmSync } from "fs";
|
|
16077
16077
|
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
16078
16078
|
async function run(cwd, cmd, args) {
|
|
16079
16079
|
return await new Promise((resolve2) => {
|
|
@@ -16100,6 +16100,14 @@ async function branchExists(dir, branch) {
|
|
|
16100
16100
|
const r = await run(dir, "git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
16101
16101
|
return r.code === 0;
|
|
16102
16102
|
}
|
|
16103
|
+
async function hasOriginRemote(dir) {
|
|
16104
|
+
const r = await run(dir, "git", ["remote", "get-url", "origin"]);
|
|
16105
|
+
return r.code === 0 && !!r.stdout;
|
|
16106
|
+
}
|
|
16107
|
+
async function isDirty(dir) {
|
|
16108
|
+
const r = await run(dir, "git", ["status", "--porcelain"]);
|
|
16109
|
+
return r.code === 0 && r.stdout.length > 0;
|
|
16110
|
+
}
|
|
16103
16111
|
function ensureGitignoreEntry(workingDir, entry) {
|
|
16104
16112
|
const gip = join3(workingDir, ".gitignore");
|
|
16105
16113
|
let body = "";
|
|
@@ -16123,7 +16131,45 @@ function ensureGitignoreEntry(workingDir, entry) {
|
|
|
16123
16131
|
function normalizeKey(issueKey) {
|
|
16124
16132
|
return issueKey.toLowerCase().replace(/[^a-z0-9\-_\/]/g, "-");
|
|
16125
16133
|
}
|
|
16126
|
-
|
|
16134
|
+
function indexPath(workingDir) {
|
|
16135
|
+
return join3(workingDir, ".multi", "worktrees", "_index.json");
|
|
16136
|
+
}
|
|
16137
|
+
function readIndex(workingDir) {
|
|
16138
|
+
try {
|
|
16139
|
+
const p = indexPath(workingDir);
|
|
16140
|
+
if (!existsSync2(p))
|
|
16141
|
+
return {};
|
|
16142
|
+
return JSON.parse(readFileSync2(p, "utf8"));
|
|
16143
|
+
} catch {
|
|
16144
|
+
return {};
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
function writeIndex(workingDir, idx) {
|
|
16148
|
+
try {
|
|
16149
|
+
const p = indexPath(workingDir);
|
|
16150
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
16151
|
+
writeFileSync2(p, JSON.stringify(idx, null, 2), "utf8");
|
|
16152
|
+
} catch {}
|
|
16153
|
+
}
|
|
16154
|
+
function listWorktreeIndex(workingDir) {
|
|
16155
|
+
const idx = readIndex(workingDir);
|
|
16156
|
+
return Object.entries(idx).map(([key, issueId]) => ({ key, issueId }));
|
|
16157
|
+
}
|
|
16158
|
+
function removeWorktreeIndexEntry(workingDir, key) {
|
|
16159
|
+
const idx = readIndex(workingDir);
|
|
16160
|
+
if (key in idx) {
|
|
16161
|
+
delete idx[key];
|
|
16162
|
+
writeIndex(workingDir, idx);
|
|
16163
|
+
}
|
|
16164
|
+
}
|
|
16165
|
+
function setWorktreeIndexEntry(workingDir, key, issueId) {
|
|
16166
|
+
const idx = readIndex(workingDir);
|
|
16167
|
+
if (idx[key] === issueId)
|
|
16168
|
+
return;
|
|
16169
|
+
idx[key] = issueId;
|
|
16170
|
+
writeIndex(workingDir, idx);
|
|
16171
|
+
}
|
|
16172
|
+
async function ensureWorktree(workingDir, issueKey, issueId) {
|
|
16127
16173
|
if (!await isGitRepo(workingDir)) {
|
|
16128
16174
|
return { path: workingDir, branch: "", created: false };
|
|
16129
16175
|
}
|
|
@@ -16133,6 +16179,8 @@ async function ensureWorktree(workingDir, issueKey) {
|
|
|
16133
16179
|
const wtDir = join3(workingDir, ".multi", "worktrees");
|
|
16134
16180
|
const wtPath = join3(wtDir, key);
|
|
16135
16181
|
if (existsSync2(wtPath)) {
|
|
16182
|
+
if (issueId)
|
|
16183
|
+
setWorktreeIndexEntry(workingDir, key, issueId);
|
|
16136
16184
|
return { path: wtPath, branch, created: false };
|
|
16137
16185
|
}
|
|
16138
16186
|
try {
|
|
@@ -16145,6 +16193,8 @@ async function ensureWorktree(workingDir, issueKey) {
|
|
|
16145
16193
|
throw new Error(`git worktree add failed: ${r.stderr || r.stdout}`);
|
|
16146
16194
|
}
|
|
16147
16195
|
await linkIgnoredFiles(workingDir, wtPath);
|
|
16196
|
+
if (issueId)
|
|
16197
|
+
setWorktreeIndexEntry(workingDir, key, issueId);
|
|
16148
16198
|
return { path: wtPath, branch, created: true };
|
|
16149
16199
|
}
|
|
16150
16200
|
async function linkIgnoredFiles(workingDir, wtPath) {
|
|
@@ -16177,9 +16227,61 @@ async function linkIgnoredFiles(workingDir, wtPath) {
|
|
|
16177
16227
|
} catch {}
|
|
16178
16228
|
}
|
|
16179
16229
|
}
|
|
16230
|
+
async function removeWorktree(workingDir, issueKey, opts = {}) {
|
|
16231
|
+
const result = { removedPath: false, pushed: false, skipped: null, errors: [] };
|
|
16232
|
+
if (!await isGitRepo(workingDir)) {
|
|
16233
|
+
result.skipped = "not_git";
|
|
16234
|
+
return result;
|
|
16235
|
+
}
|
|
16236
|
+
const key = normalizeKey(issueKey);
|
|
16237
|
+
const branch = `multi/${key}`;
|
|
16238
|
+
const wtPath = join3(workingDir, ".multi", "worktrees", key);
|
|
16239
|
+
if (existsSync2(wtPath)) {
|
|
16240
|
+
const dirty = await isDirty(wtPath);
|
|
16241
|
+
if (dirty && !opts.force) {
|
|
16242
|
+
result.skipped = "dirty";
|
|
16243
|
+
return result;
|
|
16244
|
+
}
|
|
16245
|
+
}
|
|
16246
|
+
const noPush = opts.noPush || process.env.MULTI_GC_NO_PUSH === "1";
|
|
16247
|
+
if (!noPush) {
|
|
16248
|
+
if (await branchExists(workingDir, branch)) {
|
|
16249
|
+
if (await hasOriginRemote(workingDir)) {
|
|
16250
|
+
const push = await run(workingDir, "git", ["push", "--force-with-lease", "origin", `${branch}:${branch}`]);
|
|
16251
|
+
if (push.code === 0) {
|
|
16252
|
+
result.pushed = true;
|
|
16253
|
+
} else {
|
|
16254
|
+
result.errors.push(`push failed: ${push.stderr || push.stdout}`);
|
|
16255
|
+
}
|
|
16256
|
+
} else {
|
|
16257
|
+
result.errors.push("no origin remote, skipped push");
|
|
16258
|
+
}
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
await run(workingDir, "git", ["worktree", "prune"]);
|
|
16262
|
+
if (existsSync2(wtPath)) {
|
|
16263
|
+
const rm = await run(workingDir, "git", ["worktree", "remove", "--force", wtPath]);
|
|
16264
|
+
if (rm.code === 0) {
|
|
16265
|
+
result.removedPath = true;
|
|
16266
|
+
} else {
|
|
16267
|
+
result.errors.push(`worktree remove failed: ${rm.stderr || rm.stdout}`);
|
|
16268
|
+
try {
|
|
16269
|
+
rmSync(wtPath, { recursive: true, force: true });
|
|
16270
|
+
result.removedPath = true;
|
|
16271
|
+
} catch (e) {
|
|
16272
|
+
result.errors.push(`fs rm failed: ${String(e)}`);
|
|
16273
|
+
}
|
|
16274
|
+
}
|
|
16275
|
+
await run(workingDir, "git", ["worktree", "prune"]);
|
|
16276
|
+
} else {
|
|
16277
|
+
result.removedPath = true;
|
|
16278
|
+
}
|
|
16279
|
+
removeWorktreeIndexEntry(workingDir, key);
|
|
16280
|
+
return result;
|
|
16281
|
+
}
|
|
16180
16282
|
|
|
16181
16283
|
// src/materializer.ts
|
|
16182
|
-
import { mkdirSync as mkdirSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, rmSync, symlinkSync as symlinkSync2, lstatSync } from "fs";
|
|
16284
|
+
import { mkdirSync as mkdirSync3, existsSync as existsSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync3, rmSync as rmSync2, symlinkSync as symlinkSync2, lstatSync } from "fs";
|
|
16183
16285
|
import { join as join4, dirname as dirname3 } from "path";
|
|
16184
16286
|
var HOME2 = process.env.HOME || process.env.USERPROFILE || ".";
|
|
16185
16287
|
var MULTI_DIR = join4(HOME2, ".multi");
|
|
@@ -16210,17 +16312,17 @@ function safeRmManaged(path) {
|
|
|
16210
16312
|
try {
|
|
16211
16313
|
const st = lstatSync(path);
|
|
16212
16314
|
if (st.isSymbolicLink()) {
|
|
16213
|
-
|
|
16315
|
+
rmSync2(path);
|
|
16214
16316
|
return;
|
|
16215
16317
|
}
|
|
16216
16318
|
if (st.isDirectory() && existsSync3(join4(path, MARKER))) {
|
|
16217
|
-
|
|
16319
|
+
rmSync2(path, { recursive: true, force: true });
|
|
16218
16320
|
return;
|
|
16219
16321
|
}
|
|
16220
16322
|
if (st.isFile() && path.endsWith(".md")) {
|
|
16221
16323
|
const head = readFileSync3(path, "utf8").slice(0, 200);
|
|
16222
16324
|
if (head.includes("multi-managed: true"))
|
|
16223
|
-
|
|
16325
|
+
rmSync2(path);
|
|
16224
16326
|
}
|
|
16225
16327
|
} catch {}
|
|
16226
16328
|
}
|
|
@@ -16324,7 +16426,7 @@ import { join as join5, dirname as dirname4 } from "path";
|
|
|
16324
16426
|
// package.json
|
|
16325
16427
|
var package_default = {
|
|
16326
16428
|
name: "@shipers-dev/multi",
|
|
16327
|
-
version: "0.
|
|
16429
|
+
version: "0.18.0",
|
|
16328
16430
|
type: "module",
|
|
16329
16431
|
bin: {
|
|
16330
16432
|
"multi-agent": "./dist/index.js"
|
|
@@ -16365,7 +16467,8 @@ var COMMANDS = {
|
|
|
16365
16467
|
stop: "Stop the running daemon",
|
|
16366
16468
|
restart: "Stop and relaunch the daemon in background",
|
|
16367
16469
|
logs: "View execution logs",
|
|
16368
|
-
reset: "Reset acpx session for an issue (--issue <id>)"
|
|
16470
|
+
reset: "Reset acpx session for an issue (--issue <id>)",
|
|
16471
|
+
worktree: "Worktree maintenance (subcommands: gc)"
|
|
16369
16472
|
};
|
|
16370
16473
|
function ensureDirs() {
|
|
16371
16474
|
for (const d of [MULTI_DIR2, join5(MULTI_DIR2, "logs"), SKILLS_DIR]) {
|
|
@@ -16396,7 +16499,10 @@ async function main() {
|
|
|
16396
16499
|
workspace: { type: "string" },
|
|
16397
16500
|
agent: { type: "string" },
|
|
16398
16501
|
api: { type: "string" },
|
|
16399
|
-
issue: { type: "string" }
|
|
16502
|
+
issue: { type: "string" },
|
|
16503
|
+
key: { type: "string" },
|
|
16504
|
+
force: { type: "boolean", default: false },
|
|
16505
|
+
"no-push": { type: "boolean", default: false }
|
|
16400
16506
|
},
|
|
16401
16507
|
allowPositionals: true,
|
|
16402
16508
|
strict: false
|
|
@@ -16440,6 +16546,11 @@ async function main() {
|
|
|
16440
16546
|
case "reset":
|
|
16441
16547
|
await cmdReset(args.values.issue);
|
|
16442
16548
|
break;
|
|
16549
|
+
case "worktree": {
|
|
16550
|
+
const sub = args.positionals.slice(3)[0] || "help";
|
|
16551
|
+
await cmdWorktree(sub, { force: !!args.values.force, noPush: !!args.values["no-push"], key: args.values.key });
|
|
16552
|
+
break;
|
|
16553
|
+
}
|
|
16443
16554
|
default:
|
|
16444
16555
|
console.error(`Unknown command: ${command}`);
|
|
16445
16556
|
printHelp();
|
|
@@ -16461,6 +16572,7 @@ Commands:
|
|
|
16461
16572
|
restart ${COMMANDS.restart}
|
|
16462
16573
|
logs ${COMMANDS.logs}
|
|
16463
16574
|
reset ${COMMANDS.reset}
|
|
16575
|
+
worktree ${COMMANDS.worktree}
|
|
16464
16576
|
|
|
16465
16577
|
Options:
|
|
16466
16578
|
--name <name> Device name
|
|
@@ -17066,6 +17178,25 @@ async function markStopped(apiUrl, issueId, reason) {
|
|
|
17066
17178
|
} catch {}
|
|
17067
17179
|
await postStream(apiUrl, issueId, "stopped", { reason });
|
|
17068
17180
|
}
|
|
17181
|
+
async function gcWorktreeAfterTerminal(baseWorkingDir, issueKey) {
|
|
17182
|
+
try {
|
|
17183
|
+
const res = await removeWorktree(baseWorkingDir, issueKey);
|
|
17184
|
+
if (res.skipped === "dirty") {
|
|
17185
|
+
log(` \uD83D\uDEB1 worktree gc ${issueKey}: skipped (dirty \u2014 uncommitted changes preserved)`);
|
|
17186
|
+
return;
|
|
17187
|
+
}
|
|
17188
|
+
if (res.skipped === "not_git")
|
|
17189
|
+
return;
|
|
17190
|
+
const errs = res.errors.length ? ` (warn: ${res.errors.join("; ")})` : "";
|
|
17191
|
+
if (res.removedPath) {
|
|
17192
|
+
log(` \uD83E\uDDF9 worktree gc ${issueKey}: pushed=${res.pushed} removed=true (branch kept)${errs}`);
|
|
17193
|
+
} else if (res.errors.length) {
|
|
17194
|
+
log(` \u26A0 worktree gc ${issueKey}: ${res.errors.join("; ")}`);
|
|
17195
|
+
}
|
|
17196
|
+
} catch (e) {
|
|
17197
|
+
log(` \u26A0 worktree gc ${issueKey} threw: ${fmtError(e)}`);
|
|
17198
|
+
}
|
|
17199
|
+
}
|
|
17069
17200
|
async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
17070
17201
|
const issueId = task.issue_id;
|
|
17071
17202
|
const isFollowup = !!task.followup;
|
|
@@ -17074,7 +17205,7 @@ async function handleRunTask(apiUrl, deviceId, task, detected, ctx) {
|
|
|
17074
17205
|
let worktreeBranch = "";
|
|
17075
17206
|
if (baseWorkingDir) {
|
|
17076
17207
|
try {
|
|
17077
|
-
const wt = await ensureWorktree(baseWorkingDir, task.key || issueId);
|
|
17208
|
+
const wt = await ensureWorktree(baseWorkingDir, task.key || issueId, issueId);
|
|
17078
17209
|
workingDir = wt.path;
|
|
17079
17210
|
worktreeBranch = wt.branch;
|
|
17080
17211
|
await postStream(apiUrl, issueId, "worktree_created", { path: wt.path, branch: wt.branch, reused: !wt.created });
|
|
@@ -17540,9 +17671,13 @@ ${userPart}` : userPart;
|
|
|
17540
17671
|
} else if (hadError) {
|
|
17541
17672
|
await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
|
|
17542
17673
|
log(` \u2717 ${task.key} failed`);
|
|
17674
|
+
if (baseWorkingDir)
|
|
17675
|
+
await gcWorktreeAfterTerminal(baseWorkingDir, task.key || issueId);
|
|
17543
17676
|
} else {
|
|
17544
17677
|
await apiClient.post(`${apiUrl}/api/issues/${issueId}/complete`, {});
|
|
17545
17678
|
log(` \u2713 ${task.key} complete`);
|
|
17679
|
+
if (baseWorkingDir)
|
|
17680
|
+
await gcWorktreeAfterTerminal(baseWorkingDir, task.key || issueId);
|
|
17546
17681
|
}
|
|
17547
17682
|
} catch (e) {
|
|
17548
17683
|
const msg = fmtError(e);
|
|
@@ -17714,6 +17849,11 @@ async function executePlanActions(apiUrl, parentTask, actions, ctx) {
|
|
|
17714
17849
|
continue;
|
|
17715
17850
|
}
|
|
17716
17851
|
lines.push(`- \u2713 updated ${res.data.key}`);
|
|
17852
|
+
if ((a.status === "done" || a.status === "cancelled") && parentTask.working_dir && existsSync4(parentTask.working_dir)) {
|
|
17853
|
+
const targetKey = res.data?.key;
|
|
17854
|
+
if (targetKey)
|
|
17855
|
+
await gcWorktreeAfterTerminal(parentTask.working_dir, targetKey);
|
|
17856
|
+
}
|
|
17717
17857
|
} else if (a.type === "delegate") {
|
|
17718
17858
|
const res = await apiClient.post(`${apiUrl}/api/issues/agent/mutate`, { action: "update", id: a.id, assignee_type: "agent", assignee_id: a.assignee_id, status: "todo" }, { headers });
|
|
17719
17859
|
if (!res.success) {
|
|
@@ -18228,6 +18368,40 @@ async function cmdReset(issueId) {
|
|
|
18228
18368
|
}
|
|
18229
18369
|
console.log(body);
|
|
18230
18370
|
}
|
|
18371
|
+
async function cmdWorktree(sub, opts) {
|
|
18372
|
+
const config2 = loadConfig();
|
|
18373
|
+
const baseDir = process.cwd();
|
|
18374
|
+
if (sub === "gc") {
|
|
18375
|
+
const targets = opts.key ? [opts.key] : listWorktreeIndex(baseDir).map((e) => e.key);
|
|
18376
|
+
if (!targets.length) {
|
|
18377
|
+
console.log("No worktrees in index.");
|
|
18378
|
+
return;
|
|
18379
|
+
}
|
|
18380
|
+
let kept = 0, gone = 0, dirty = 0;
|
|
18381
|
+
for (const k of targets) {
|
|
18382
|
+
const r = await removeWorktree(baseDir, k, { force: opts.force, noPush: opts.noPush });
|
|
18383
|
+
if (r.skipped === "dirty") {
|
|
18384
|
+
dirty++;
|
|
18385
|
+
console.log(` \uD83D\uDEB1 ${k}: skipped (dirty)`);
|
|
18386
|
+
continue;
|
|
18387
|
+
}
|
|
18388
|
+
if (r.skipped === "not_git") {
|
|
18389
|
+
console.log(` \u26A0 ${k}: not a git repo`);
|
|
18390
|
+
continue;
|
|
18391
|
+
}
|
|
18392
|
+
if (r.removedPath) {
|
|
18393
|
+
gone++;
|
|
18394
|
+
console.log(` \uD83E\uDDF9 ${k}: pushed=${r.pushed} removed=true (branch kept)${r.errors.length ? " warn=" + r.errors.join("; ") : ""}`);
|
|
18395
|
+
} else {
|
|
18396
|
+
kept++;
|
|
18397
|
+
console.log(` \u26A0 ${k}: ${r.errors.join("; ") || "unchanged"}`);
|
|
18398
|
+
}
|
|
18399
|
+
}
|
|
18400
|
+
console.log(`Done. removed=${gone} dirty=${dirty} kept=${kept}`);
|
|
18401
|
+
return;
|
|
18402
|
+
}
|
|
18403
|
+
console.log("Usage: multi-agent worktree gc [--key <key>] [--force] [--no-push]");
|
|
18404
|
+
}
|
|
18231
18405
|
async function cmdRestart(apiUrl) {
|
|
18232
18406
|
if (existsSync4(PID_PATH)) {
|
|
18233
18407
|
const pid = Number(readFileSync4(PID_PATH, "utf8").trim());
|