runcastle 1.6.1 → 1.6.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.
- package/bin/runcastle.js +2166 -18
- package/index.js +28 -2
- package/package.json +1 -1
package/bin/runcastle.js
CHANGED
|
@@ -2061,11 +2061,35 @@ var init_schema = __esm(() => {
|
|
|
2061
2061
|
// src/db/client.ts
|
|
2062
2062
|
var exports_client = {};
|
|
2063
2063
|
__export(exports_client, {
|
|
2064
|
+
openRawSqlite: () => openRawSqlite,
|
|
2064
2065
|
createDb: () => createDb,
|
|
2065
2066
|
BUSY_TIMEOUT_MS: () => BUSY_TIMEOUT_MS
|
|
2066
2067
|
});
|
|
2067
2068
|
import { Database } from "bun:sqlite";
|
|
2068
2069
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
2070
|
+
function openRawSqlite(path, opts) {
|
|
2071
|
+
const sqlite = new Database(path, opts.readonly ? { readonly: true } : { readwrite: true });
|
|
2072
|
+
sqlite.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS};`);
|
|
2073
|
+
return {
|
|
2074
|
+
all: (sql, ...params) => {
|
|
2075
|
+
const statement = sqlite.prepare(sql);
|
|
2076
|
+
try {
|
|
2077
|
+
return statement.all(...params);
|
|
2078
|
+
} finally {
|
|
2079
|
+
statement.finalize();
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
run: (sql, ...params) => {
|
|
2083
|
+
const statement = sqlite.prepare(sql);
|
|
2084
|
+
try {
|
|
2085
|
+
statement.run(...params);
|
|
2086
|
+
} finally {
|
|
2087
|
+
statement.finalize();
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
close: () => sqlite.close()
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2069
2093
|
function createDb(path) {
|
|
2070
2094
|
const sqlite = new Database(path, { create: true });
|
|
2071
2095
|
sqlite.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS};`);
|
|
@@ -2133,6 +2157,9 @@ var init_asset_paths = __esm(() => {
|
|
|
2133
2157
|
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
2134
2158
|
import { join as join4 } from "path";
|
|
2135
2159
|
import { sql } from "drizzle-orm";
|
|
2160
|
+
function bundledMigrations(dir = MIGRATIONS_DIR) {
|
|
2161
|
+
return readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
2162
|
+
}
|
|
2136
2163
|
function runMigrations(db, dir = MIGRATIONS_DIR) {
|
|
2137
2164
|
db.run(sql.raw(`CREATE TABLE IF NOT EXISTS __migrations (
|
|
2138
2165
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -2141,8 +2168,7 @@ function runMigrations(db, dir = MIGRATIONS_DIR) {
|
|
|
2141
2168
|
)`));
|
|
2142
2169
|
const appliedRows = db.all(sql.raw("SELECT name FROM __migrations"));
|
|
2143
2170
|
const applied = new Set(appliedRows.map((r) => r.name));
|
|
2144
|
-
const
|
|
2145
|
-
for (const file of files) {
|
|
2171
|
+
for (const file of bundledMigrations(dir)) {
|
|
2146
2172
|
if (applied.has(file))
|
|
2147
2173
|
continue;
|
|
2148
2174
|
const contents = readFileSync2(join4(dir, file), "utf8");
|
|
@@ -75440,6 +75466,2110 @@ var init_setup3 = __esm(() => {
|
|
|
75440
75466
|
AGENT_KEYS = { "2": "codex", "3": "both" };
|
|
75441
75467
|
});
|
|
75442
75468
|
|
|
75469
|
+
// src/move/shell.ts
|
|
75470
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
75471
|
+
import { lstatSync, readdirSync as readdirSync11, realpathSync as realpathSync3, statfsSync, statSync as statSync12 } from "fs";
|
|
75472
|
+
import { join as join48 } from "path";
|
|
75473
|
+
function run8(command, args2, opts = {}) {
|
|
75474
|
+
const result = spawnSync3(command, args2, {
|
|
75475
|
+
cwd: opts.cwd,
|
|
75476
|
+
input: opts.input,
|
|
75477
|
+
stdio: opts.stdio,
|
|
75478
|
+
env: opts.env,
|
|
75479
|
+
encoding: "utf8",
|
|
75480
|
+
maxBuffer: HUGE_OUTPUT,
|
|
75481
|
+
windowsHide: true
|
|
75482
|
+
});
|
|
75483
|
+
return {
|
|
75484
|
+
ok: result.status === 0,
|
|
75485
|
+
stdout: result.stdout ?? "",
|
|
75486
|
+
stderr: result.stderr ?? (result.error?.message ?? "")
|
|
75487
|
+
};
|
|
75488
|
+
}
|
|
75489
|
+
function git3(cwd, args2) {
|
|
75490
|
+
return run8("git", ["-c", "core.quotepath=off", ...args2], { cwd, env: READ_ONLY_GIT });
|
|
75491
|
+
}
|
|
75492
|
+
function gitOrThrow(cwd, args2) {
|
|
75493
|
+
const result = git3(cwd, args2);
|
|
75494
|
+
if (!result.ok)
|
|
75495
|
+
throw new Error(`git ${args2.join(" ")} failed in ${cwd}: ${result.stderr.trim()}`);
|
|
75496
|
+
return result.stdout;
|
|
75497
|
+
}
|
|
75498
|
+
function nulList(output) {
|
|
75499
|
+
return output.split("\x00").filter((entry) => entry.length > 0);
|
|
75500
|
+
}
|
|
75501
|
+
function systemTar() {
|
|
75502
|
+
if (process.platform !== "win32")
|
|
75503
|
+
return "tar";
|
|
75504
|
+
return join48(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe");
|
|
75505
|
+
}
|
|
75506
|
+
function walkFiles(dir, base = "", strict2 = false) {
|
|
75507
|
+
let entries2;
|
|
75508
|
+
try {
|
|
75509
|
+
entries2 = readdirSync11(dir, { withFileTypes: true });
|
|
75510
|
+
} catch (err) {
|
|
75511
|
+
if (strict2)
|
|
75512
|
+
throw err;
|
|
75513
|
+
return [];
|
|
75514
|
+
}
|
|
75515
|
+
const files = [];
|
|
75516
|
+
for (const entry of entries2) {
|
|
75517
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
75518
|
+
if (entry.isDirectory())
|
|
75519
|
+
files.push(...walkFiles(join48(dir, entry.name), rel, strict2));
|
|
75520
|
+
else if (entry.isFile())
|
|
75521
|
+
files.push(rel);
|
|
75522
|
+
}
|
|
75523
|
+
return files;
|
|
75524
|
+
}
|
|
75525
|
+
function walkFollowing(path3, rel, broken, parents = []) {
|
|
75526
|
+
let real2;
|
|
75527
|
+
let isDirectory;
|
|
75528
|
+
try {
|
|
75529
|
+
real2 = realpathSync3(path3);
|
|
75530
|
+
isDirectory = statSync12(path3).isDirectory();
|
|
75531
|
+
} catch {
|
|
75532
|
+
broken.push(rel);
|
|
75533
|
+
return [];
|
|
75534
|
+
}
|
|
75535
|
+
if (!isDirectory)
|
|
75536
|
+
return [rel];
|
|
75537
|
+
if (parents.includes(real2)) {
|
|
75538
|
+
broken.push(rel);
|
|
75539
|
+
return [];
|
|
75540
|
+
}
|
|
75541
|
+
return readdirSync11(path3).filter((name) => name !== "node_modules" && name !== ".git").flatMap((name) => walkFollowing(join48(path3, name), `${rel}/${name}`, broken, [...parents, real2]));
|
|
75542
|
+
}
|
|
75543
|
+
function walkSymlinks(dir, base = "") {
|
|
75544
|
+
let entries2;
|
|
75545
|
+
try {
|
|
75546
|
+
entries2 = readdirSync11(dir, { withFileTypes: true });
|
|
75547
|
+
} catch {
|
|
75548
|
+
return [];
|
|
75549
|
+
}
|
|
75550
|
+
const links = [];
|
|
75551
|
+
for (const entry of entries2) {
|
|
75552
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
75553
|
+
if (entry.isSymbolicLink())
|
|
75554
|
+
links.push(rel);
|
|
75555
|
+
else if (entry.isDirectory())
|
|
75556
|
+
links.push(...walkSymlinks(join48(dir, entry.name), rel));
|
|
75557
|
+
}
|
|
75558
|
+
return links;
|
|
75559
|
+
}
|
|
75560
|
+
function isSymlink(path3) {
|
|
75561
|
+
try {
|
|
75562
|
+
return lstatSync(path3).isSymbolicLink();
|
|
75563
|
+
} catch {
|
|
75564
|
+
return false;
|
|
75565
|
+
}
|
|
75566
|
+
}
|
|
75567
|
+
function linkInPath(root, rel) {
|
|
75568
|
+
const segments = rel.split("/");
|
|
75569
|
+
for (let i = 1;i <= segments.length; i++) {
|
|
75570
|
+
const prefix = segments.slice(0, i).join("/");
|
|
75571
|
+
if (isSymlink(join48(root, prefix)))
|
|
75572
|
+
return prefix;
|
|
75573
|
+
}
|
|
75574
|
+
return null;
|
|
75575
|
+
}
|
|
75576
|
+
function pathBytes(path3) {
|
|
75577
|
+
try {
|
|
75578
|
+
const stat3 = lstatSync(path3);
|
|
75579
|
+
if (!stat3.isDirectory())
|
|
75580
|
+
return stat3.isFile() ? stat3.size : 0;
|
|
75581
|
+
} catch {
|
|
75582
|
+
return 0;
|
|
75583
|
+
}
|
|
75584
|
+
return walkFiles(path3).reduce((total, rel) => total + pathBytes(join48(path3, rel)), 0);
|
|
75585
|
+
}
|
|
75586
|
+
function freeBytes(path3) {
|
|
75587
|
+
try {
|
|
75588
|
+
const stats = statfsSync(path3);
|
|
75589
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
75590
|
+
} catch {
|
|
75591
|
+
return null;
|
|
75592
|
+
}
|
|
75593
|
+
}
|
|
75594
|
+
function formatBytes(bytes) {
|
|
75595
|
+
if (bytes >= 1024 ** 3)
|
|
75596
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
75597
|
+
if (bytes >= 1024 ** 2)
|
|
75598
|
+
return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
75599
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
75600
|
+
}
|
|
75601
|
+
function stamp(now2 = new Date) {
|
|
75602
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
75603
|
+
return `${now2.getFullYear()}${pad(now2.getMonth() + 1)}${pad(now2.getDate())}-` + `${pad(now2.getHours())}${pad(now2.getMinutes())}${pad(now2.getSeconds())}`;
|
|
75604
|
+
}
|
|
75605
|
+
var HUGE_OUTPUT, READ_ONLY_GIT;
|
|
75606
|
+
var init_shell = __esm(() => {
|
|
75607
|
+
HUGE_OUTPUT = 512 * 1024 * 1024;
|
|
75608
|
+
READ_ONLY_GIT = { ...process.env, GIT_OPTIONAL_LOCKS: "0" };
|
|
75609
|
+
});
|
|
75610
|
+
|
|
75611
|
+
// src/move/destination.ts
|
|
75612
|
+
import { existsSync as existsSync41, readdirSync as readdirSync12, readFileSync as readFileSync28 } from "fs";
|
|
75613
|
+
import { isAbsolute as isAbsolute7, join as join49 } from "path";
|
|
75614
|
+
function reposDirOf(env, override) {
|
|
75615
|
+
return override ?? join49(env.home, DEFAULT_REPOS_FOLDER);
|
|
75616
|
+
}
|
|
75617
|
+
function serverPort(env) {
|
|
75618
|
+
const fromEnv2 = Number(process.env.RUNCASTLE_SERVER_PORT);
|
|
75619
|
+
if (Number.isInteger(fromEnv2) && fromEnv2 > 0)
|
|
75620
|
+
return fromEnv2;
|
|
75621
|
+
try {
|
|
75622
|
+
const config2 = JSON.parse(readFileSync28(join49(env.dataDir, "config.json"), "utf8"));
|
|
75623
|
+
return config2.serverPort ?? 4512;
|
|
75624
|
+
} catch {
|
|
75625
|
+
return 4512;
|
|
75626
|
+
}
|
|
75627
|
+
}
|
|
75628
|
+
async function serverRunning(env) {
|
|
75629
|
+
try {
|
|
75630
|
+
const reply = await fetch(`http://127.0.0.1:${serverPort(env)}/health`, {
|
|
75631
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
75632
|
+
});
|
|
75633
|
+
const body = await reply.json();
|
|
75634
|
+
return !body.dataDir || sameDataDir(body.dataDir, env.dataDir);
|
|
75635
|
+
} catch (err) {
|
|
75636
|
+
return err instanceof SyntaxError || err.name === "TimeoutError";
|
|
75637
|
+
}
|
|
75638
|
+
}
|
|
75639
|
+
function existingProjects(env, open4) {
|
|
75640
|
+
const path3 = join49(env.dataDir, "runcastle.db");
|
|
75641
|
+
if (!existsSync41(path3))
|
|
75642
|
+
return [];
|
|
75643
|
+
const sql5 = open4(path3, { readonly: true });
|
|
75644
|
+
try {
|
|
75645
|
+
return sql5.all("SELECT name FROM projects").map((row2) => row2.name);
|
|
75646
|
+
} catch {
|
|
75647
|
+
return [];
|
|
75648
|
+
} finally {
|
|
75649
|
+
sql5.close();
|
|
75650
|
+
}
|
|
75651
|
+
}
|
|
75652
|
+
function bytesNeeded(question) {
|
|
75653
|
+
return question.dataBytes * 2 + question.repoBytes * 4;
|
|
75654
|
+
}
|
|
75655
|
+
async function checkDestination(question, env, opts) {
|
|
75656
|
+
const blockers = [];
|
|
75657
|
+
const notes = [];
|
|
75658
|
+
const reposDir = reposDirOf(env, opts.reposDir);
|
|
75659
|
+
if (env.platform === "win32")
|
|
75660
|
+
blockers.push("a Windows machine cannot be the destination of a move");
|
|
75661
|
+
if (!isAbsolute7(reposDir))
|
|
75662
|
+
blockers.push(`--repos-dir must be an absolute path on this machine, not ${reposDir}`);
|
|
75663
|
+
if (await serverRunning(env))
|
|
75664
|
+
blockers.push("this machine's runcastle server is running: stop it first");
|
|
75665
|
+
const known = new Set(bundledMigrations());
|
|
75666
|
+
const unknown2 = question.migrations.filter((name) => !known.has(name));
|
|
75667
|
+
if (unknown2.length > 0) {
|
|
75668
|
+
blockers.push(`the pack comes from a newer runcastle (${question.runcastleVersion}) than this one: update this machine first`);
|
|
75669
|
+
}
|
|
75670
|
+
const projects2 = existingProjects(env, opts.open);
|
|
75671
|
+
if (projects2.length > 0) {
|
|
75672
|
+
const named = `${projects2.length} ${projects2.length === 1 ? "project" : "projects"} (${projects2.join(", ")})`;
|
|
75673
|
+
if (opts.replace)
|
|
75674
|
+
notes.push(`${named} will be set aside, not merged`);
|
|
75675
|
+
else
|
|
75676
|
+
blockers.push(`this machine holds ${named}: add --replace to set them aside`);
|
|
75677
|
+
}
|
|
75678
|
+
for (const folder of question.targetFolders) {
|
|
75679
|
+
const path3 = join49(reposDir, folder);
|
|
75680
|
+
if (!folderHasContent(path3))
|
|
75681
|
+
continue;
|
|
75682
|
+
if (opts.replace)
|
|
75683
|
+
notes.push(`${path3} will be set aside`);
|
|
75684
|
+
else
|
|
75685
|
+
blockers.push(`${path3} already exists: add --replace to set it aside, or choose --repos-dir`);
|
|
75686
|
+
}
|
|
75687
|
+
const free = freeBytes(existsSync41(env.home) ? env.home : "/");
|
|
75688
|
+
const needed = bytesNeeded(question);
|
|
75689
|
+
if (free !== null && free < needed) {
|
|
75690
|
+
blockers.push(`not enough disk: about ${formatBytes(needed)} needed, ${formatBytes(free)} free`);
|
|
75691
|
+
}
|
|
75692
|
+
return { platform: env.platform, blockers, notes, freeBytes: free, reposDir };
|
|
75693
|
+
}
|
|
75694
|
+
function folderHasContent(path3) {
|
|
75695
|
+
try {
|
|
75696
|
+
return readdirSync12(path3).length > 0;
|
|
75697
|
+
} catch {
|
|
75698
|
+
return false;
|
|
75699
|
+
}
|
|
75700
|
+
}
|
|
75701
|
+
var DEFAULT_REPOS_FOLDER = "projects", HEALTH_TIMEOUT_MS = 3000;
|
|
75702
|
+
var init_destination = __esm(() => {
|
|
75703
|
+
init_paths();
|
|
75704
|
+
init_migrate();
|
|
75705
|
+
init_shell();
|
|
75706
|
+
});
|
|
75707
|
+
|
|
75708
|
+
// src/move/paths.ts
|
|
75709
|
+
function canonicalSlashes(path3, sourcePlatform) {
|
|
75710
|
+
const slashed = sourcePlatform === "win32" ? path3.replace(/\\/g, "/") : path3;
|
|
75711
|
+
return slashed.length > 1 ? slashed.replace(/\/+$/, "") : slashed;
|
|
75712
|
+
}
|
|
75713
|
+
function foldForCompare(path3, sourcePlatform) {
|
|
75714
|
+
const canonical = canonicalSlashes(path3, sourcePlatform);
|
|
75715
|
+
return sourcePlatform === "win32" ? canonical.toLowerCase() : canonical;
|
|
75716
|
+
}
|
|
75717
|
+
function samePath2(a, b, sourcePlatform) {
|
|
75718
|
+
return foldForCompare(a, sourcePlatform) === foldForCompare(b, sourcePlatform);
|
|
75719
|
+
}
|
|
75720
|
+
function mapSourcePath(path3, rules, sourcePlatform) {
|
|
75721
|
+
const folded = foldForCompare(path3, sourcePlatform);
|
|
75722
|
+
const canonical = canonicalSlashes(path3, sourcePlatform);
|
|
75723
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
75724
|
+
for (const rule of ordered) {
|
|
75725
|
+
const from = foldForCompare(rule.from, sourcePlatform);
|
|
75726
|
+
if (folded !== from && !folded.startsWith(`${from}/`))
|
|
75727
|
+
continue;
|
|
75728
|
+
const sep5 = rule.to.includes("\\") ? "\\" : "/";
|
|
75729
|
+
const rest = canonical.slice(from.length).split("/").join(sep5);
|
|
75730
|
+
return rule.to + rest;
|
|
75731
|
+
}
|
|
75732
|
+
return null;
|
|
75733
|
+
}
|
|
75734
|
+
function escapeRegExp(text3) {
|
|
75735
|
+
return text3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
75736
|
+
}
|
|
75737
|
+
function rewritePathsInText(text3, rules, sourcePlatform) {
|
|
75738
|
+
const windows = sourcePlatform === "win32";
|
|
75739
|
+
const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
|
|
75740
|
+
if (ordered.length === 0)
|
|
75741
|
+
return text3;
|
|
75742
|
+
const places = ordered.map((rule) => canonicalSlashes(rule.from, sourcePlatform).split("/").map(escapeRegExp).join(windows ? "[\\\\/]+" : "/"));
|
|
75743
|
+
const pattern = new RegExp(`(?<![\\w.-])(${places.join("|")})(?![\\w.-])((?:[\\\\/][^\\s"'\`<>|&;,()]*)?)`, windows ? "gi" : "g");
|
|
75744
|
+
return text3.replace(pattern, (whole, place, rest) => {
|
|
75745
|
+
const rule = ordered.find((candidate) => samePath2(place, candidate.from, sourcePlatform));
|
|
75746
|
+
if (!rule)
|
|
75747
|
+
return whole;
|
|
75748
|
+
return rule.to + (windows ? rest.replace(/\\+/g, "/") : rest);
|
|
75749
|
+
});
|
|
75750
|
+
}
|
|
75751
|
+
function looksWindowsOnly(text3) {
|
|
75752
|
+
if (/(?<![\w])[A-Za-z]:[\\/]/.test(text3))
|
|
75753
|
+
return true;
|
|
75754
|
+
if (/%[A-Za-z_][A-Za-z0-9_]*%/.test(text3))
|
|
75755
|
+
return true;
|
|
75756
|
+
return /\b(if\s+(not\s+)?exist|del\s+\/|rd\s+\/|rmdir\s+\/|cmd(\.exe)?\s+\/|powershell|pwsh|taskkill)\b|\.(bat|cmd|exe|ps1)\b/i.test(text3);
|
|
75757
|
+
}
|
|
75758
|
+
function sourceBasename(path3, sourcePlatform) {
|
|
75759
|
+
const canonical = canonicalSlashes(path3, sourcePlatform);
|
|
75760
|
+
return canonical.slice(canonical.lastIndexOf("/") + 1);
|
|
75761
|
+
}
|
|
75762
|
+
function claudeHash(text3) {
|
|
75763
|
+
let hash2 = 0;
|
|
75764
|
+
for (let i = 0;i < text3.length; i++)
|
|
75765
|
+
hash2 = (hash2 << 5) - hash2 + text3.charCodeAt(i) | 0;
|
|
75766
|
+
return Math.abs(hash2).toString(36);
|
|
75767
|
+
}
|
|
75768
|
+
function encodeClaudeFolder(path3) {
|
|
75769
|
+
const encoded = path3.replace(/[^a-zA-Z0-9]/g, "-");
|
|
75770
|
+
if (encoded.length <= CLAUDE_FOLDER_MAX)
|
|
75771
|
+
return encoded;
|
|
75772
|
+
return `${encoded.slice(0, CLAUDE_FOLDER_MAX)}-${claudeHash(path3)}`;
|
|
75773
|
+
}
|
|
75774
|
+
function assignTargetFolders(projects2) {
|
|
75775
|
+
const movers = new Map;
|
|
75776
|
+
for (const project3 of projects2) {
|
|
75777
|
+
if (project3.repoMoves)
|
|
75778
|
+
movers.set(project3.folder, [...movers.get(project3.folder) ?? [], project3.id]);
|
|
75779
|
+
}
|
|
75780
|
+
const targets = new Map;
|
|
75781
|
+
for (const project3 of projects2) {
|
|
75782
|
+
const shared = projects2.filter((p) => p.folder === project3.folder).length > 1;
|
|
75783
|
+
const taken = shared && !project3.repoMoves;
|
|
75784
|
+
targets.set(project3.id, taken ? `${project3.folder}-${project3.id}` : project3.folder);
|
|
75785
|
+
}
|
|
75786
|
+
const clashes = [...movers].filter(([, ids3]) => ids3.length > 1).map(([folder]) => folder);
|
|
75787
|
+
return { targets, clashes };
|
|
75788
|
+
}
|
|
75789
|
+
var CLAUDE_FOLDER_MAX = 200;
|
|
75790
|
+
|
|
75791
|
+
// src/move/scan.ts
|
|
75792
|
+
import { existsSync as existsSync42, readdirSync as readdirSync13, readFileSync as readFileSync29, statSync as statSync13 } from "fs";
|
|
75793
|
+
import { join as join50 } from "path";
|
|
75794
|
+
function staysBehind(options) {
|
|
75795
|
+
return [
|
|
75796
|
+
options.withEnv ? "logins" : ".env files and logins",
|
|
75797
|
+
"Codex auth copies",
|
|
75798
|
+
"logs and caches",
|
|
75799
|
+
"the install id",
|
|
75800
|
+
"Claude's file undo checkpoints",
|
|
75801
|
+
...options.withClaudeSetup ? [] : ["global Claude rules, skills and settings"],
|
|
75802
|
+
"Claude chats outside these repos"
|
|
75803
|
+
];
|
|
75804
|
+
}
|
|
75805
|
+
function count5(n, noun) {
|
|
75806
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
75807
|
+
}
|
|
75808
|
+
function repoFacts(repoPath) {
|
|
75809
|
+
const refs = {};
|
|
75810
|
+
const listing = git3(repoPath, ["for-each-ref", "--format=%(objectname) %(refname)"]);
|
|
75811
|
+
for (const line2 of listing.stdout.split(`
|
|
75812
|
+
`)) {
|
|
75813
|
+
const space = line2.indexOf(" ");
|
|
75814
|
+
if (space > 0)
|
|
75815
|
+
refs[line2.slice(space + 1).trim()] = line2.slice(0, space);
|
|
75816
|
+
}
|
|
75817
|
+
const remotes = [];
|
|
75818
|
+
for (const name of git3(repoPath, ["remote"]).stdout.split(`
|
|
75819
|
+
`).map((n) => n.trim()).filter(Boolean)) {
|
|
75820
|
+
const url = git3(repoPath, ["remote", "get-url", name]);
|
|
75821
|
+
if (url.ok)
|
|
75822
|
+
remotes.push({ name, url: url.stdout.trim() });
|
|
75823
|
+
}
|
|
75824
|
+
const settings = git3(repoPath, ["config", "--local", "-z", "--get-regexp", "^(branch\\.|user\\.|core\\.hookspath)"]);
|
|
75825
|
+
const config2 = nulList(settings.stdout).map((entry) => {
|
|
75826
|
+
const newline = entry.indexOf(`
|
|
75827
|
+
`);
|
|
75828
|
+
return newline < 0 ? { key: entry, value: "" } : { key: entry.slice(0, newline), value: entry.slice(newline + 1) };
|
|
75829
|
+
});
|
|
75830
|
+
return { refs, remotes, config: config2, ...headOf2(repoPath) };
|
|
75831
|
+
}
|
|
75832
|
+
function headOf2(path3) {
|
|
75833
|
+
const branch = git3(path3, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
75834
|
+
const sha = git3(path3, ["rev-parse", "--verify", "--quiet", "HEAD"]);
|
|
75835
|
+
return {
|
|
75836
|
+
...branch.ok ? { headBranch: branch.stdout.trim() } : {},
|
|
75837
|
+
...sha.ok ? { headSha: sha.stdout.trim() } : {}
|
|
75838
|
+
};
|
|
75839
|
+
}
|
|
75840
|
+
function midOperation(path3) {
|
|
75841
|
+
const gitDirResult = git3(path3, ["rev-parse", "--absolute-git-dir"]);
|
|
75842
|
+
if (!gitDirResult.ok)
|
|
75843
|
+
return null;
|
|
75844
|
+
const gitDir = gitDirResult.stdout.trim();
|
|
75845
|
+
const marks = [
|
|
75846
|
+
["MERGE_HEAD", "a merge"],
|
|
75847
|
+
["rebase-merge", "a rebase"],
|
|
75848
|
+
["rebase-apply", "a rebase"],
|
|
75849
|
+
["CHERRY_PICK_HEAD", "a cherry-pick"],
|
|
75850
|
+
["REVERT_HEAD", "a revert"]
|
|
75851
|
+
];
|
|
75852
|
+
return marks.find(([file]) => existsSync42(join50(gitDir, file)))?.[1] ?? null;
|
|
75853
|
+
}
|
|
75854
|
+
function excludeRules(repoPath) {
|
|
75855
|
+
try {
|
|
75856
|
+
const rules = readFileSync29(join50(repoPath, ".git", "info", "exclude"), "utf8").replace(/\r\n/g, `
|
|
75857
|
+
`);
|
|
75858
|
+
return rules.split(`
|
|
75859
|
+
`).some((line2) => line2.trim() && !line2.startsWith("#")) ? rules : null;
|
|
75860
|
+
} catch {
|
|
75861
|
+
return null;
|
|
75862
|
+
}
|
|
75863
|
+
}
|
|
75864
|
+
function isEnvFile(rel) {
|
|
75865
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
75866
|
+
return /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template)$/.test(name);
|
|
75867
|
+
}
|
|
75868
|
+
function checkoutWip(key, path3) {
|
|
75869
|
+
const { headBranch, headSha: headSha2 } = headOf2(path3);
|
|
75870
|
+
const status3 = gitOrThrow(path3, ["status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=no"]);
|
|
75871
|
+
const changed = nulList(status3).map((entry) => entry.slice(3));
|
|
75872
|
+
const nested = [];
|
|
75873
|
+
const loose = [];
|
|
75874
|
+
for (const rel of nulList(gitOrThrow(path3, ["ls-files", "-o", "--exclude-standard", "-z"]))) {
|
|
75875
|
+
if (isBurnerPath(rel))
|
|
75876
|
+
continue;
|
|
75877
|
+
const bare = rel.replace(/\/$/, "");
|
|
75878
|
+
if (!rel.endsWith("/") || linkInPath(path3, bare)) {
|
|
75879
|
+
loose.push(bare);
|
|
75880
|
+
continue;
|
|
75881
|
+
}
|
|
75882
|
+
nested.push(bare);
|
|
75883
|
+
loose.push(...nulList(gitOrThrow(join50(path3, rel), ["ls-files", "-co", "--exclude-standard", "-z"])).map((inner) => `${rel}${inner}`));
|
|
75884
|
+
}
|
|
75885
|
+
const candidates = headSha2 ? loose : [...changed, ...loose];
|
|
75886
|
+
const links = candidates.map((rel) => linkInPath(path3, rel));
|
|
75887
|
+
const untracked = candidates.filter((_, i) => links[i] === null);
|
|
75888
|
+
return {
|
|
75889
|
+
key,
|
|
75890
|
+
path: path3,
|
|
75891
|
+
...headBranch ? { branch: headBranch } : {},
|
|
75892
|
+
...headSha2 ? { sha: headSha2 } : {},
|
|
75893
|
+
tracked: headSha2 ? changed : [],
|
|
75894
|
+
untracked: untracked.filter((rel) => !isEnvFile(rel)),
|
|
75895
|
+
nested,
|
|
75896
|
+
heldBack: untracked.filter(isEnvFile),
|
|
75897
|
+
symlinks: [...new Set(links.filter((link3) => link3 !== null))]
|
|
75898
|
+
};
|
|
75899
|
+
}
|
|
75900
|
+
function wipAttention(project3, wip) {
|
|
75901
|
+
const where = wip.key === "main" ? "" : ` (${wip.key})`;
|
|
75902
|
+
const lines2 = wip.nested.map((rel) => `${project3}${where}: ${rel} is a git repo of its own; its files travel, its history does not`);
|
|
75903
|
+
if (wip.heldBack.length > 0)
|
|
75904
|
+
lines2.push(`${project3}${where}: ${wip.heldBack.join(", ")} stays behind, though git does not ignore it`);
|
|
75905
|
+
if (wip.symlinks.length > 0)
|
|
75906
|
+
lines2.push(`${project3}${where}: untracked symlinks stay behind: ${wip.symlinks.join(", ")}`);
|
|
75907
|
+
if (wip.untracked.length > 0) {
|
|
75908
|
+
const shown2 = wip.untracked.slice(0, 3).join(", ");
|
|
75909
|
+
const more = wip.untracked.length > 3 ? `, and ${wip.untracked.length - 3} more` : "";
|
|
75910
|
+
lines2.push(`${project3}${where}: untracked files travel as they are: ${shown2}${more}`);
|
|
75911
|
+
}
|
|
75912
|
+
return lines2;
|
|
75913
|
+
}
|
|
75914
|
+
function hasWork(wip) {
|
|
75915
|
+
return wip.tracked.length > 0 || wip.untracked.length > 0;
|
|
75916
|
+
}
|
|
75917
|
+
function ignoredEntries(repoPath, carried) {
|
|
75918
|
+
const listing = git3(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "--directory", "-z"]);
|
|
75919
|
+
return nulList(listing.stdout).map((rel) => rel.replace(/\/$/, "")).filter((rel) => !rel.split("/").includes("node_modules") && !isBurnerPath(rel) && !carried(rel)).sort().map((rel) => {
|
|
75920
|
+
const bytes = pathBytes(join50(repoPath, rel));
|
|
75921
|
+
return bytes >= 1024 * 1024 ? `${rel} (${formatBytes(bytes)})` : rel;
|
|
75922
|
+
});
|
|
75923
|
+
}
|
|
75924
|
+
function ignoredEnvFiles(repoPath) {
|
|
75925
|
+
const listing = git3(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z", "--", ":(glob)**/.env", ":(glob)**/.env.*"]);
|
|
75926
|
+
return nulList(listing.stdout).filter((rel) => {
|
|
75927
|
+
const folders = rel.split("/").slice(0, -1);
|
|
75928
|
+
return isEnvFile(rel) && !folders.some((folder) => folder.startsWith(".") || folder === "node_modules");
|
|
75929
|
+
});
|
|
75930
|
+
}
|
|
75931
|
+
function parseAlso(value) {
|
|
75932
|
+
const colon = value.indexOf(":");
|
|
75933
|
+
if (colon < 1)
|
|
75934
|
+
return null;
|
|
75935
|
+
const path3 = value.slice(colon + 1).replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
75936
|
+
if (!path3 || path3.startsWith("/") || /^[A-Za-z]:/.test(path3) || path3.split("/").includes(".."))
|
|
75937
|
+
return null;
|
|
75938
|
+
return { project: value.slice(0, colon), path: path3 };
|
|
75939
|
+
}
|
|
75940
|
+
function isBurnerPath(rel) {
|
|
75941
|
+
return rel === ".sandcastle" || rel.startsWith(".sandcastle/");
|
|
75942
|
+
}
|
|
75943
|
+
function commitsOnNoRemote(repoPath) {
|
|
75944
|
+
const count6 = git3(repoPath, ["rev-list", "--count", "--branches", "--not", "--remotes"]);
|
|
75945
|
+
return count6.ok ? Number(count6.stdout.trim()) : 0;
|
|
75946
|
+
}
|
|
75947
|
+
function stashCount(repoPath) {
|
|
75948
|
+
return git3(repoPath, ["stash", "list"]).stdout.split(`
|
|
75949
|
+
`).filter(Boolean).length;
|
|
75950
|
+
}
|
|
75951
|
+
function leftoverWorktrees(repoPath, talkRoot) {
|
|
75952
|
+
const listing = git3(repoPath, ["worktree", "list", "--porcelain"]).stdout;
|
|
75953
|
+
const paths = listing.split(`
|
|
75954
|
+
`).filter((line2) => line2.startsWith("worktree ")).map((line2) => line2.slice("worktree ".length).trim()).slice(1);
|
|
75955
|
+
const talk = talkRoot.replace(/\\/g, "/").toLowerCase();
|
|
75956
|
+
return paths.filter((path3) => {
|
|
75957
|
+
if (path3.replace(/\\/g, "/").toLowerCase().startsWith(talk))
|
|
75958
|
+
return false;
|
|
75959
|
+
return existsSync42(path3) && git3(path3, ["status", "--porcelain"]).stdout.trim().length > 0;
|
|
75960
|
+
});
|
|
75961
|
+
}
|
|
75962
|
+
function localHooks(project3, repoPath) {
|
|
75963
|
+
try {
|
|
75964
|
+
const hooks2 = readdirSync13(join50(repoPath, ".git", "hooks")).filter((name) => !name.endsWith(".sample"));
|
|
75965
|
+
return hooks2.length > 0 ? [`${project3}: local git hooks stay behind: ${hooks2.join(", ")}`] : [];
|
|
75966
|
+
} catch {
|
|
75967
|
+
return [];
|
|
75968
|
+
}
|
|
75969
|
+
}
|
|
75970
|
+
function usesLfsOrSubmodules(repoPath) {
|
|
75971
|
+
const holds = (file, text3) => {
|
|
75972
|
+
try {
|
|
75973
|
+
return readFileSync29(join50(repoPath, file), "utf8").includes(text3);
|
|
75974
|
+
} catch {
|
|
75975
|
+
return false;
|
|
75976
|
+
}
|
|
75977
|
+
};
|
|
75978
|
+
return holds(".gitmodules", "[submodule") || holds(".gitattributes", "filter=lfs");
|
|
75979
|
+
}
|
|
75980
|
+
function repoBytes(repoPath) {
|
|
75981
|
+
return pathBytes(join50(repoPath, ".git", "objects"));
|
|
75982
|
+
}
|
|
75983
|
+
function isRepo(path3) {
|
|
75984
|
+
return existsSync42(join50(path3, ".git")) && git3(path3, ["rev-parse", "--git-dir"]).ok;
|
|
75985
|
+
}
|
|
75986
|
+
function conversationFolders(projectsDir) {
|
|
75987
|
+
const found = new Map;
|
|
75988
|
+
let folders;
|
|
75989
|
+
try {
|
|
75990
|
+
folders = readdirSync13(projectsDir);
|
|
75991
|
+
} catch {
|
|
75992
|
+
return found;
|
|
75993
|
+
}
|
|
75994
|
+
for (const folder of folders) {
|
|
75995
|
+
let names;
|
|
75996
|
+
try {
|
|
75997
|
+
names = readdirSync13(join50(projectsDir, folder));
|
|
75998
|
+
} catch {
|
|
75999
|
+
continue;
|
|
76000
|
+
}
|
|
76001
|
+
for (const name of names) {
|
|
76002
|
+
if (name.endsWith(".jsonl"))
|
|
76003
|
+
found.set(name.slice(0, -".jsonl".length), folder);
|
|
76004
|
+
}
|
|
76005
|
+
}
|
|
76006
|
+
return found;
|
|
76007
|
+
}
|
|
76008
|
+
function findClaudeFolders(env, repoPaths, sessions2) {
|
|
76009
|
+
const projectsDir = join50(env.claudeDir, "projects");
|
|
76010
|
+
const owned = new Map;
|
|
76011
|
+
const claim = (name, cwd) => {
|
|
76012
|
+
if (!owned.has(name))
|
|
76013
|
+
owned.set(name, cwd);
|
|
76014
|
+
};
|
|
76015
|
+
const cwds = new Set([...repoPaths, ...sessions2.map((s) => s.worktree_path)]);
|
|
76016
|
+
const onDisk = new Map;
|
|
76017
|
+
try {
|
|
76018
|
+
for (const name of readdirSync13(projectsDir))
|
|
76019
|
+
onDisk.set(fold(env, name), name);
|
|
76020
|
+
} catch {
|
|
76021
|
+
return { folders: [], conversations: [], missing: 0 };
|
|
76022
|
+
}
|
|
76023
|
+
for (const cwd of cwds) {
|
|
76024
|
+
const name = onDisk.get(fold(env, encodeClaudeFolder(cwd)));
|
|
76025
|
+
if (name)
|
|
76026
|
+
claim(name, cwd);
|
|
76027
|
+
}
|
|
76028
|
+
const onDiskConversations = conversationFolders(projectsDir);
|
|
76029
|
+
const conversations = new Map;
|
|
76030
|
+
const missing = new Set;
|
|
76031
|
+
for (const session of sessions2) {
|
|
76032
|
+
const id3 = session.cc_session_id;
|
|
76033
|
+
if (!id3 || (session.runtime ?? "claude-code") !== "claude-code")
|
|
76034
|
+
continue;
|
|
76035
|
+
const folder = onDiskConversations.get(id3);
|
|
76036
|
+
if (!folder) {
|
|
76037
|
+
missing.add(id3);
|
|
76038
|
+
continue;
|
|
76039
|
+
}
|
|
76040
|
+
claim(folder, session.worktree_path);
|
|
76041
|
+
conversations.set(id3, folder);
|
|
76042
|
+
}
|
|
76043
|
+
return {
|
|
76044
|
+
folders: [...owned].map(([name, cwd]) => ({ name, cwd })),
|
|
76045
|
+
conversations: [...conversations].map(([id3, folder]) => ({ id: id3, folder })),
|
|
76046
|
+
missing: missing.size
|
|
76047
|
+
};
|
|
76048
|
+
}
|
|
76049
|
+
function claudeJsonPath(env) {
|
|
76050
|
+
return env.claudeDir === join50(env.home, ".claude") ? join50(env.home, ".claude.json") : join50(env.claudeDir, ".claude.json");
|
|
76051
|
+
}
|
|
76052
|
+
function readJsonObject(path3) {
|
|
76053
|
+
try {
|
|
76054
|
+
const parsed = JSON.parse(readFileSync29(path3, "utf8"));
|
|
76055
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
76056
|
+
} catch {
|
|
76057
|
+
return {};
|
|
76058
|
+
}
|
|
76059
|
+
}
|
|
76060
|
+
function findClaudeSetup(env) {
|
|
76061
|
+
const broken = [];
|
|
76062
|
+
const files = CLAUDE_SETUP_ENTRIES.filter((entry) => existsSync42(join50(env.claudeDir, entry))).flatMap((entry) => walkFollowing(join50(env.claudeDir, entry), entry, broken));
|
|
76063
|
+
const settings = readJsonObject(join50(env.claudeDir, "settings.json"));
|
|
76064
|
+
const enabled2 = settings.enabledPlugins ?? {};
|
|
76065
|
+
const plugins = Object.keys(enabled2).filter((id3) => enabled2[id3] === true);
|
|
76066
|
+
const known = readJsonObject(join50(env.claudeDir, "plugins", "known_marketplaces.json"));
|
|
76067
|
+
const needed = new Set(plugins.map((id3) => id3.slice(id3.lastIndexOf("@") + 1)));
|
|
76068
|
+
const marketplaces = Object.keys(known).filter((name) => needed.has(name)).flatMap((name) => known[name].source?.repo ?? known[name].source?.url ?? []);
|
|
76069
|
+
const mcpServers = readJsonObject(claudeJsonPath(env)).mcpServers ?? {};
|
|
76070
|
+
return { setup: { files, marketplaces, plugins, mcpServers }, broken };
|
|
76071
|
+
}
|
|
76072
|
+
function fold(env, name) {
|
|
76073
|
+
return env.platform === "win32" ? name.toLowerCase() : name;
|
|
76074
|
+
}
|
|
76075
|
+
function dataSymlinks(env, folders) {
|
|
76076
|
+
const inData = DATA_ENTRIES.flatMap((entry) => {
|
|
76077
|
+
const path3 = join50(env.dataDir, entry);
|
|
76078
|
+
if (isSymlink(path3))
|
|
76079
|
+
return [entry];
|
|
76080
|
+
try {
|
|
76081
|
+
if (!statSync13(path3).isDirectory())
|
|
76082
|
+
return [];
|
|
76083
|
+
} catch {
|
|
76084
|
+
return [];
|
|
76085
|
+
}
|
|
76086
|
+
return walkSymlinks(path3).map((rel) => `${entry}/${rel}`);
|
|
76087
|
+
});
|
|
76088
|
+
const inClaude = folders.flatMap((folder) => walkSymlinks(join50(env.claudeDir, "projects", folder.name)).map((rel) => `claude/${folder.name}/${rel}`));
|
|
76089
|
+
return [...inData, ...inClaude];
|
|
76090
|
+
}
|
|
76091
|
+
function wipBytes(projects2) {
|
|
76092
|
+
let total = 0;
|
|
76093
|
+
for (const project3 of projects2) {
|
|
76094
|
+
for (const wip of project3.wip) {
|
|
76095
|
+
for (const rel of [...wip.tracked, ...wip.untracked])
|
|
76096
|
+
total += pathBytes(join50(wip.path, rel));
|
|
76097
|
+
}
|
|
76098
|
+
for (const rel of project3.carried)
|
|
76099
|
+
total += pathBytes(join50(project3.repoPath, rel));
|
|
76100
|
+
}
|
|
76101
|
+
return total;
|
|
76102
|
+
}
|
|
76103
|
+
function dataBytes(env, folders) {
|
|
76104
|
+
const data = DATA_ENTRIES.reduce((total, entry) => total + pathBytes(join50(env.dataDir, entry)), 0);
|
|
76105
|
+
const claude = folders.reduce((total, folder) => total + pathBytes(join50(env.claudeDir, "projects", folder.name)), 0);
|
|
76106
|
+
return data + claude + pathBytes(join50(env.dataDir, "runcastle.db"));
|
|
76107
|
+
}
|
|
76108
|
+
function scanTalkWorktrees(env, project3, features2) {
|
|
76109
|
+
const root = join50(env.dataDir, "worktrees", project3.id);
|
|
76110
|
+
const wips = [];
|
|
76111
|
+
const stopped = [];
|
|
76112
|
+
let names;
|
|
76113
|
+
try {
|
|
76114
|
+
names = readdirSync13(root);
|
|
76115
|
+
} catch {
|
|
76116
|
+
return { wips, stopped };
|
|
76117
|
+
}
|
|
76118
|
+
for (const name of names) {
|
|
76119
|
+
const status3 = features2.find((f) => f.project_id === project3.id && f.slug === name)?.status;
|
|
76120
|
+
if (status3 === "shipped")
|
|
76121
|
+
continue;
|
|
76122
|
+
const path3 = join50(root, name);
|
|
76123
|
+
if (!git3(path3, ["rev-parse", "--git-dir"]).ok)
|
|
76124
|
+
continue;
|
|
76125
|
+
const operation = midOperation(path3);
|
|
76126
|
+
if (operation)
|
|
76127
|
+
stopped.push(`${project3.name}: the talk worktree ${name} is in the middle of ${operation}. Finish or abort it`);
|
|
76128
|
+
const wip = checkoutWip(name, path3);
|
|
76129
|
+
if (hasWork(wip))
|
|
76130
|
+
wips.push(wip);
|
|
76131
|
+
}
|
|
76132
|
+
return { wips, stopped };
|
|
76133
|
+
}
|
|
76134
|
+
function scanMove(env, sql5, options) {
|
|
76135
|
+
const projectRows = sql5.all("SELECT * FROM projects");
|
|
76136
|
+
const sessionRows = sql5.all("SELECT project_id, feature_id, status, worktree_path, cc_session_id, runtime FROM sessions");
|
|
76137
|
+
const featureRows = sql5.all("SELECT id, project_id, slug, title, branch, status FROM features");
|
|
76138
|
+
const running3 = sql5.all("SELECT feature_id FROM runs WHERE status = 'running'");
|
|
76139
|
+
const blockers = [];
|
|
76140
|
+
const attention = [];
|
|
76141
|
+
const also = (options.also ?? []).flatMap((value) => {
|
|
76142
|
+
const request2 = parseAlso(value);
|
|
76143
|
+
if (request2)
|
|
76144
|
+
return [{ ...request2, used: false }];
|
|
76145
|
+
blockers.push(`--also ${value}: write it as <project>:<path inside its repo>`);
|
|
76146
|
+
return [];
|
|
76147
|
+
});
|
|
76148
|
+
for (const run7 of running3) {
|
|
76149
|
+
const feature = featureRows.find((f) => f.id === run7.feature_id);
|
|
76150
|
+
blockers.push(`a round of ${feature?.slug ?? run7.feature_id} is marked running: let it finish or press Stop, and if the server is already stopped, start it once so it can clean up`);
|
|
76151
|
+
}
|
|
76152
|
+
const placed = projectRows.map((row2) => {
|
|
76153
|
+
const folder = sourceBasename(row2.repo_path, env.platform);
|
|
76154
|
+
const skipped = options.noRepo.includes(row2.name) || options.noRepo.includes(folder);
|
|
76155
|
+
const present = isRepo(row2.repo_path);
|
|
76156
|
+
return { id: row2.id, row: row2, folder, skipped, present, repoMoves: present && !skipped };
|
|
76157
|
+
});
|
|
76158
|
+
const { targets, clashes } = assignTargetFolders(placed);
|
|
76159
|
+
for (const folder of clashes) {
|
|
76160
|
+
const names = placed.filter((p) => p.folder === folder && p.repoMoves).map((p) => p.row.repo_path);
|
|
76161
|
+
blockers.push(`two projects want the folder ${folder}: ${names.join(" and ")}. Leave one out with --no-repo`);
|
|
76162
|
+
}
|
|
76163
|
+
const projects2 = [];
|
|
76164
|
+
const lines2 = [];
|
|
76165
|
+
let totalRepoBytes = 0;
|
|
76166
|
+
for (const { row: row2, folder, skipped, repoMoves } of placed) {
|
|
76167
|
+
const features2 = featureRows.filter((f) => f.project_id === row2.id);
|
|
76168
|
+
const featureIds = new Set(features2.map((f) => f.id));
|
|
76169
|
+
const chats = sessionRows.filter((s) => s.project_id === row2.id || s.feature_id !== null && featureIds.has(s.feature_id));
|
|
76170
|
+
const targetFolder = targets.get(row2.id) ?? folder;
|
|
76171
|
+
const project3 = {
|
|
76172
|
+
id: row2.id,
|
|
76173
|
+
name: row2.name,
|
|
76174
|
+
repoPath: row2.repo_path,
|
|
76175
|
+
targetFolder,
|
|
76176
|
+
repoMoves,
|
|
76177
|
+
refs: {},
|
|
76178
|
+
remotes: [],
|
|
76179
|
+
config: [],
|
|
76180
|
+
wip: [],
|
|
76181
|
+
carried: []
|
|
76182
|
+
};
|
|
76183
|
+
let bytes = 0;
|
|
76184
|
+
if (!repoMoves) {
|
|
76185
|
+
project3.repoLeftReason = skipped ? "--no-repo" : "its repo folder is gone";
|
|
76186
|
+
attention.push(`${row2.name}: repo stays behind (${project3.repoLeftReason}); put it in the repos folder as ${targetFolder} to use the project`);
|
|
76187
|
+
} else {
|
|
76188
|
+
Object.assign(project3, repoFacts(row2.repo_path));
|
|
76189
|
+
const main = checkoutWip("main", row2.repo_path);
|
|
76190
|
+
const talk = scanTalkWorktrees(env, row2, featureRows);
|
|
76191
|
+
project3.wip = [...hasWork(main) ? [main] : [], ...talk.wips];
|
|
76192
|
+
blockers.push(...talk.stopped);
|
|
76193
|
+
const envFiles = options.withEnv ? [...new Set([...ignoredEnvFiles(row2.repo_path), ...main.heldBack])] : [];
|
|
76194
|
+
const requested = also.filter((request2) => request2.project === row2.name || request2.project === folder);
|
|
76195
|
+
const asked = requested.flatMap((request2) => {
|
|
76196
|
+
request2.used = true;
|
|
76197
|
+
const path3 = join50(row2.repo_path, request2.path);
|
|
76198
|
+
if (existsSync42(path3))
|
|
76199
|
+
return statSync13(path3).isDirectory() ? walkFiles(path3, request2.path) : [request2.path];
|
|
76200
|
+
blockers.push(`${row2.name}: --also ${request2.path} is not in the repo folder`);
|
|
76201
|
+
return [];
|
|
76202
|
+
});
|
|
76203
|
+
project3.carried = [...new Set([...envFiles, ...asked])];
|
|
76204
|
+
if (envFiles.length > 0)
|
|
76205
|
+
attention.push(`${row2.name}: .env files travel: ${envFiles.join(", ")}`);
|
|
76206
|
+
if (requested.length > 0) {
|
|
76207
|
+
const paths = requested.map((request2) => request2.path).join(", ");
|
|
76208
|
+
attention.push(`${row2.name}: travels at your request: ${paths}`);
|
|
76209
|
+
}
|
|
76210
|
+
const shownMain = options.withEnv ? { ...main, heldBack: [] } : main;
|
|
76211
|
+
attention.push(...[shownMain, ...talk.wips].flatMap((wip) => wipAttention(row2.name, wip)));
|
|
76212
|
+
const rules = excludeRules(row2.repo_path);
|
|
76213
|
+
if (rules)
|
|
76214
|
+
project3.excludeRules = rules;
|
|
76215
|
+
if (git3(row2.repo_path, ["rev-parse", "--is-shallow-repository"]).stdout.trim() === "true") {
|
|
76216
|
+
blockers.push(`${row2.name}: a shallow clone cannot be packed. Run git fetch --unshallow, or leave it out with --no-repo`);
|
|
76217
|
+
}
|
|
76218
|
+
if (project3.remotes.some((remote) => /^https?:\/\/[^/@]+@/.test(remote.url))) {
|
|
76219
|
+
attention.push(`${row2.name}: a remote URL holds a credential, and it travels with the repo`);
|
|
76220
|
+
}
|
|
76221
|
+
bytes = repoBytes(row2.repo_path);
|
|
76222
|
+
const mid = midOperation(row2.repo_path);
|
|
76223
|
+
if (mid)
|
|
76224
|
+
blockers.push(`${row2.name}: the main checkout is in the middle of ${mid}. Finish or abort it`);
|
|
76225
|
+
const driving = features2.find((f) => f.status === "active" && f.branch === project3.headBranch);
|
|
76226
|
+
if (driving) {
|
|
76227
|
+
blockers.push(`${row2.name}: the main checkout is on ${driving.branch}, which a test drive leaves behind. Stop the drive or check out the base branch`);
|
|
76228
|
+
}
|
|
76229
|
+
if (project3.remotes.length === 0) {
|
|
76230
|
+
attention.push(`${row2.name}: no remote, so the new machine and this one will be its only copies`);
|
|
76231
|
+
} else {
|
|
76232
|
+
const lonely = commitsOnNoRemote(row2.repo_path);
|
|
76233
|
+
if (lonely > 0)
|
|
76234
|
+
attention.push(`${row2.name}: ${count5(lonely, "commit")} on no remote; they travel in the pack, nothing is pushed`);
|
|
76235
|
+
}
|
|
76236
|
+
const carried = new Set(project3.carried);
|
|
76237
|
+
const roots3 = requested.map((request2) => request2.path);
|
|
76238
|
+
const ignored = ignoredEntries(row2.repo_path, (rel) => carried.has(rel) || roots3.some((root) => rel === root || rel.startsWith(`${root}/`)));
|
|
76239
|
+
if (ignored.length > 0)
|
|
76240
|
+
attention.push(`${row2.name}: ignored files stay behind: ${ignored.join(", ")}`);
|
|
76241
|
+
const stashes = stashCount(row2.repo_path);
|
|
76242
|
+
if (stashes > 1)
|
|
76243
|
+
attention.push(`${row2.name}: ${stashes} stashes, only the newest travels`);
|
|
76244
|
+
if (Object.keys(project3.refs).length > 0) {
|
|
76245
|
+
attention.push(...localHooks(row2.name, row2.repo_path));
|
|
76246
|
+
}
|
|
76247
|
+
const leftovers = leftoverWorktrees(row2.repo_path, join50(env.dataDir, "worktrees"));
|
|
76248
|
+
if (leftovers.length > 0)
|
|
76249
|
+
attention.push(`${row2.name}: uncommitted changes in other worktrees stay behind: ${leftovers.join(", ")}`);
|
|
76250
|
+
if (usesLfsOrSubmodules(row2.repo_path)) {
|
|
76251
|
+
attention.push(`${row2.name}: uses LFS or submodules, which the pack does not carry; fetch them on the new machine`);
|
|
76252
|
+
}
|
|
76253
|
+
}
|
|
76254
|
+
projects2.push(project3);
|
|
76255
|
+
totalRepoBytes += bytes;
|
|
76256
|
+
lines2.push({
|
|
76257
|
+
name: row2.name,
|
|
76258
|
+
branches: Object.keys(project3.refs).filter((ref) => ref.startsWith("refs/heads/")).length,
|
|
76259
|
+
uncommitted: project3.wip.reduce((total, wip) => total + wip.tracked.length + wip.untracked.length, 0),
|
|
76260
|
+
features: features2.length,
|
|
76261
|
+
chats: chats.length,
|
|
76262
|
+
repoBytes: bytes,
|
|
76263
|
+
repoMoves,
|
|
76264
|
+
repoPath: row2.repo_path,
|
|
76265
|
+
targetFolder
|
|
76266
|
+
});
|
|
76267
|
+
}
|
|
76268
|
+
for (const request2 of also.filter((item) => !item.used)) {
|
|
76269
|
+
blockers.push(`--also ${request2.project}:${request2.path}: no moving repo is called ${request2.project}`);
|
|
76270
|
+
}
|
|
76271
|
+
const openChats = sessionRows.filter((s) => s.status === "live" || s.status === "launching").length;
|
|
76272
|
+
if (openChats > 0)
|
|
76273
|
+
attention.push(`${count5(openChats, "open chat")} will end at the new machine's first boot, and can be resumed`);
|
|
76274
|
+
const claude = findClaudeFolders(env, projectRows.map((p) => p.repo_path), sessionRows);
|
|
76275
|
+
if (claude.missing > 0) {
|
|
76276
|
+
attention.push(`${count5(claude.missing, "chat")} already gone from this machine: Claude Code removes transcripts after cleanupPeriodDays, 30 by default`);
|
|
76277
|
+
}
|
|
76278
|
+
const links = dataSymlinks(env, claude.folders);
|
|
76279
|
+
if (links.length > 0)
|
|
76280
|
+
attention.push(`symlinks in runcastle's folders stay behind: ${links.join(", ")}`);
|
|
76281
|
+
let claudeSetup;
|
|
76282
|
+
let setupBytes = 0;
|
|
76283
|
+
if (options.withClaudeSetup) {
|
|
76284
|
+
const found = findClaudeSetup(env);
|
|
76285
|
+
claudeSetup = found.setup;
|
|
76286
|
+
setupBytes = found.setup.files.reduce((total, rel) => total + pathBytes(join50(env.claudeDir, rel)), 0);
|
|
76287
|
+
attention.push(`Claude setup travels: ${count5(found.setup.files.length, "file")}, ${count5(found.setup.plugins.length, "plugin")} to install again, ${count5(Object.keys(found.setup.mcpServers).length, "MCP server")}. What the new machine has is set aside, and its login stays`);
|
|
76288
|
+
if (found.broken.length > 0)
|
|
76289
|
+
attention.push(`Claude setup links that lead nowhere stay behind: ${found.broken.join(", ")}`);
|
|
76290
|
+
} else if (readJsonObject(join50(env.claudeDir, "settings.json")).cleanupPeriodDays === undefined) {
|
|
76291
|
+
attention.push("Claude Code on the new machine deletes moved chats older than 30 days unless cleanupPeriodDays is raised; --with-claude-setup does that");
|
|
76292
|
+
}
|
|
76293
|
+
const report = {
|
|
76294
|
+
blockers,
|
|
76295
|
+
projects: lines2,
|
|
76296
|
+
attention,
|
|
76297
|
+
staysBehind: staysBehind(options),
|
|
76298
|
+
dataBytes: dataBytes(env, claude.folders) + wipBytes(projects2) + setupBytes,
|
|
76299
|
+
repoBytes: totalRepoBytes,
|
|
76300
|
+
openChats
|
|
76301
|
+
};
|
|
76302
|
+
return {
|
|
76303
|
+
report,
|
|
76304
|
+
projects: projects2,
|
|
76305
|
+
claudeFolders: claude.folders,
|
|
76306
|
+
conversations: claude.conversations,
|
|
76307
|
+
envCarried: options.withEnv === true,
|
|
76308
|
+
...claudeSetup ? { claudeSetup } : {}
|
|
76309
|
+
};
|
|
76310
|
+
}
|
|
76311
|
+
function windowsCommandLines(sql5, projects2, env) {
|
|
76312
|
+
const rules = [
|
|
76313
|
+
...projects2.map((project3) => ({ from: project3.repoPath, to: `/repos/${project3.targetFolder}` })),
|
|
76314
|
+
{ from: env.dataDir, to: "/data" },
|
|
76315
|
+
{ from: env.home, to: "/home" }
|
|
76316
|
+
];
|
|
76317
|
+
return sql5.all("SELECT * FROM projects").flatMap((row2) => HOST_COMMANDS.flatMap(({ column, label }) => {
|
|
76318
|
+
const value = row2[column];
|
|
76319
|
+
if (!value || !looksWindowsOnly(rewritePathsInText(value, rules, env.platform)))
|
|
76320
|
+
return [];
|
|
76321
|
+
return [`${row2.name}, ${label}: only works on Windows; change it before the move or it will fail on the new machine`];
|
|
76322
|
+
}));
|
|
76323
|
+
}
|
|
76324
|
+
var DATA_ENTRIES, HOST_COMMANDS, CLAUDE_SETUP_ENTRIES;
|
|
76325
|
+
var init_scan = __esm(() => {
|
|
76326
|
+
init_shell();
|
|
76327
|
+
DATA_ENTRIES = [
|
|
76328
|
+
"config.json",
|
|
76329
|
+
"sessions",
|
|
76330
|
+
"reviews",
|
|
76331
|
+
"annotations",
|
|
76332
|
+
"project-notes",
|
|
76333
|
+
"design-crops",
|
|
76334
|
+
"stories"
|
|
76335
|
+
];
|
|
76336
|
+
HOST_COMMANDS = [
|
|
76337
|
+
{ column: "dev_command", label: "dev command" },
|
|
76338
|
+
{ column: "setup_command", label: "setup command" },
|
|
76339
|
+
{ column: "verify_commands", label: "verify commands" },
|
|
76340
|
+
{ column: "drive_setup_command", label: "drive setup command" },
|
|
76341
|
+
{ column: "drive_stop_command", label: "drive stop command" },
|
|
76342
|
+
{ column: "db_reset_command", label: "database reset command" },
|
|
76343
|
+
{ column: "drive_instructions", label: "drive instructions" }
|
|
76344
|
+
];
|
|
76345
|
+
CLAUDE_SETUP_ENTRIES = ["CLAUDE.md", "rules", "skills", "agents", "commands", "settings.json"];
|
|
76346
|
+
});
|
|
76347
|
+
|
|
76348
|
+
// src/move/tar.ts
|
|
76349
|
+
import { createHash as createHash5 } from "crypto";
|
|
76350
|
+
import { createReadStream as createReadStream2, createWriteStream, statSync as statSync14 } from "fs";
|
|
76351
|
+
import { once as once5 } from "events";
|
|
76352
|
+
import { createGzip } from "zlib";
|
|
76353
|
+
function writeField(block, offset, length3, value) {
|
|
76354
|
+
block.write(value.slice(0, length3), offset, length3, "utf8");
|
|
76355
|
+
}
|
|
76356
|
+
function octal(value, digits) {
|
|
76357
|
+
return `${Math.trunc(value).toString(8).padStart(digits, "0")}\x00`;
|
|
76358
|
+
}
|
|
76359
|
+
function header(name, size6, mtimeSeconds, type) {
|
|
76360
|
+
const block = Buffer.alloc(BLOCK);
|
|
76361
|
+
writeField(block, 0, 100, name);
|
|
76362
|
+
writeField(block, 100, 8, octal(type === "x" ? 420 : 384, 7));
|
|
76363
|
+
writeField(block, 108, 8, octal(0, 7));
|
|
76364
|
+
writeField(block, 116, 8, octal(0, 7));
|
|
76365
|
+
writeField(block, 124, 12, octal(Math.min(size6, USTAR_MAX_SIZE), 11));
|
|
76366
|
+
writeField(block, 136, 12, octal(mtimeSeconds, 11));
|
|
76367
|
+
block.fill(" ", 148, 156);
|
|
76368
|
+
writeField(block, 156, 1, type);
|
|
76369
|
+
writeField(block, 257, 8, "ustar\x0000");
|
|
76370
|
+
const sum2 = block.reduce((total, byte) => total + byte, 0);
|
|
76371
|
+
writeField(block, 148, 8, `${sum2.toString(8).padStart(6, "0")}\x00 `);
|
|
76372
|
+
return block;
|
|
76373
|
+
}
|
|
76374
|
+
function paxRecord(key, value) {
|
|
76375
|
+
const body = ` ${key}=${value}
|
|
76376
|
+
`;
|
|
76377
|
+
const bodyBytes = Buffer.byteLength(body);
|
|
76378
|
+
let length3 = bodyBytes + 1;
|
|
76379
|
+
while (String(length3).length + bodyBytes !== length3)
|
|
76380
|
+
length3 = String(length3).length + bodyBytes;
|
|
76381
|
+
return `${length3}${body}`;
|
|
76382
|
+
}
|
|
76383
|
+
function needsPaxName(name) {
|
|
76384
|
+
return name.length > 100 || Buffer.byteLength(name) !== name.length;
|
|
76385
|
+
}
|
|
76386
|
+
|
|
76387
|
+
class TarWriter {
|
|
76388
|
+
gzip;
|
|
76389
|
+
out;
|
|
76390
|
+
failure;
|
|
76391
|
+
paxCount = 0;
|
|
76392
|
+
files = [];
|
|
76393
|
+
constructor(path3) {
|
|
76394
|
+
this.gzip = createGzip({ level: 6 });
|
|
76395
|
+
this.out = createWriteStream(path3, { mode: 384 });
|
|
76396
|
+
const fail9 = (err) => {
|
|
76397
|
+
this.failure ??= err;
|
|
76398
|
+
this.gzip.destroy(err);
|
|
76399
|
+
};
|
|
76400
|
+
this.gzip.on("error", fail9);
|
|
76401
|
+
this.out.on("error", fail9);
|
|
76402
|
+
this.gzip.pipe(this.out);
|
|
76403
|
+
}
|
|
76404
|
+
async write(chunk) {
|
|
76405
|
+
if (this.failure)
|
|
76406
|
+
throw this.failure;
|
|
76407
|
+
if (!this.gzip.write(chunk))
|
|
76408
|
+
await once5(this.gzip, "drain");
|
|
76409
|
+
}
|
|
76410
|
+
async writeHeaders(name, size6, mtimeSeconds) {
|
|
76411
|
+
const records = (needsPaxName(name) ? paxRecord("path", name) : "") + (size6 > USTAR_MAX_SIZE ? paxRecord("size", String(size6)) : "");
|
|
76412
|
+
if (records) {
|
|
76413
|
+
const body = Buffer.from(records, "utf8");
|
|
76414
|
+
await this.write(header(`PaxHeader/${this.paxCount++}`, body.length, mtimeSeconds, "x"));
|
|
76415
|
+
await this.write(body);
|
|
76416
|
+
await this.pad(body.length);
|
|
76417
|
+
}
|
|
76418
|
+
await this.write(header(needsPaxName(name) ? `long/${this.paxCount}` : name, size6, mtimeSeconds, "0"));
|
|
76419
|
+
}
|
|
76420
|
+
async pad(size6) {
|
|
76421
|
+
const over = size6 % BLOCK;
|
|
76422
|
+
if (over)
|
|
76423
|
+
await this.write(Buffer.alloc(BLOCK - over));
|
|
76424
|
+
}
|
|
76425
|
+
async addFile(name, path3) {
|
|
76426
|
+
const stat3 = statSync14(path3);
|
|
76427
|
+
await this.writeHeaders(name, stat3.size, stat3.mtimeMs / 1000);
|
|
76428
|
+
const hash2 = createHash5("sha256");
|
|
76429
|
+
let read2 = 0;
|
|
76430
|
+
for await (const chunk of createReadStream2(path3)) {
|
|
76431
|
+
const buffer = chunk;
|
|
76432
|
+
read2 += buffer.length;
|
|
76433
|
+
hash2.update(buffer);
|
|
76434
|
+
await this.write(buffer);
|
|
76435
|
+
}
|
|
76436
|
+
if (read2 !== stat3.size)
|
|
76437
|
+
throw new Error(`${path3} changed while it was being packed`);
|
|
76438
|
+
await this.pad(read2);
|
|
76439
|
+
this.files.push({ path: name, size: read2, sha256: hash2.digest("hex") });
|
|
76440
|
+
}
|
|
76441
|
+
async addBuffer(name, body) {
|
|
76442
|
+
await this.writeHeaders(name, body.length, Date.now() / 1000);
|
|
76443
|
+
await this.write(body);
|
|
76444
|
+
await this.pad(body.length);
|
|
76445
|
+
}
|
|
76446
|
+
abort() {
|
|
76447
|
+
this.gzip.destroy();
|
|
76448
|
+
this.out.destroy();
|
|
76449
|
+
}
|
|
76450
|
+
async finish() {
|
|
76451
|
+
await this.write(Buffer.alloc(BLOCK * 2));
|
|
76452
|
+
this.gzip.end();
|
|
76453
|
+
if (!this.out.closed)
|
|
76454
|
+
await once5(this.out, "close");
|
|
76455
|
+
if (this.failure)
|
|
76456
|
+
throw this.failure;
|
|
76457
|
+
}
|
|
76458
|
+
}
|
|
76459
|
+
async function hashFile(path3) {
|
|
76460
|
+
const hash2 = createHash5("sha256");
|
|
76461
|
+
for await (const chunk of createReadStream2(path3))
|
|
76462
|
+
hash2.update(chunk);
|
|
76463
|
+
return hash2.digest("hex");
|
|
76464
|
+
}
|
|
76465
|
+
var BLOCK = 512, USTAR_MAX_SIZE = 8589934591;
|
|
76466
|
+
var init_tar = () => {};
|
|
76467
|
+
|
|
76468
|
+
// src/move/pack.ts
|
|
76469
|
+
import { closeSync as closeSync6, copyFileSync as copyFileSync4, existsSync as existsSync43, mkdtempSync as mkdtempSync2, openSync as openSync6, rmSync as rmSync11, statSync as statSync15 } from "fs";
|
|
76470
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
76471
|
+
import { dirname as dirname23, join as join51, resolve as resolve16 } from "path";
|
|
76472
|
+
function isLeftOut(entry) {
|
|
76473
|
+
return /^sessions\/[^/]+\/(system-prompt\.md|settings\.json|mcp\.json|agent-stderr\.log)$/.test(entry) || /^sessions\/[^/]+\/codex-home\/(?!sessions\/)/.test(entry);
|
|
76474
|
+
}
|
|
76475
|
+
async function addTree(tar, dir, prefix, skip) {
|
|
76476
|
+
for (const rel of walkFiles(dir, "", true)) {
|
|
76477
|
+
if (skip?.(rel))
|
|
76478
|
+
continue;
|
|
76479
|
+
await tar.addFile(`${prefix}/${rel}`, join51(dir, rel));
|
|
76480
|
+
}
|
|
76481
|
+
}
|
|
76482
|
+
async function addDataEntries(tar, env) {
|
|
76483
|
+
for (const entry of DATA_ENTRIES) {
|
|
76484
|
+
const path3 = join51(env.dataDir, entry);
|
|
76485
|
+
let isFile3 = false;
|
|
76486
|
+
try {
|
|
76487
|
+
isFile3 = statSync15(path3).isFile();
|
|
76488
|
+
} catch {
|
|
76489
|
+
continue;
|
|
76490
|
+
}
|
|
76491
|
+
if (isFile3)
|
|
76492
|
+
await tar.addFile(`data/${entry}`, path3);
|
|
76493
|
+
else
|
|
76494
|
+
await addTree(tar, path3, `data/${entry}`, (rel) => isLeftOut(`${entry}/${rel}`));
|
|
76495
|
+
}
|
|
76496
|
+
}
|
|
76497
|
+
function snapshotDatabase(env, open4, target) {
|
|
76498
|
+
const source = open4(join51(env.dataDir, "runcastle.db"), { readonly: true });
|
|
76499
|
+
try {
|
|
76500
|
+
source.run(`VACUUM INTO '${target.replace(/'/g, "''")}'`);
|
|
76501
|
+
} finally {
|
|
76502
|
+
source.close();
|
|
76503
|
+
}
|
|
76504
|
+
const copy4 = open4(target, { readonly: true });
|
|
76505
|
+
try {
|
|
76506
|
+
return { rowCounts: countRows(copy4), migrations: copy4.all("SELECT name FROM __migrations ORDER BY name").map((r) => r.name) };
|
|
76507
|
+
} finally {
|
|
76508
|
+
copy4.close();
|
|
76509
|
+
}
|
|
76510
|
+
}
|
|
76511
|
+
function countRows(sql5) {
|
|
76512
|
+
const tables = sql5.all("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'");
|
|
76513
|
+
const counts = {};
|
|
76514
|
+
for (const { name } of tables) {
|
|
76515
|
+
counts[name] = sql5.all(`SELECT COUNT(*) AS n FROM "${name}"`)[0].n;
|
|
76516
|
+
}
|
|
76517
|
+
return counts;
|
|
76518
|
+
}
|
|
76519
|
+
async function addRepo(tar, id3, repoPath, scratch) {
|
|
76520
|
+
const bundle = join51(scratch, `${id3}.bundle`);
|
|
76521
|
+
gitOrThrow(repoPath, ["bundle", "create", "-q", bundle, "--all"]);
|
|
76522
|
+
await tar.addFile(`repos/${id3}.bundle`, bundle);
|
|
76523
|
+
rmSync11(bundle, { force: true });
|
|
76524
|
+
}
|
|
76525
|
+
async function addWip(tar, projectId, wip, scratch) {
|
|
76526
|
+
const prefix = `wip/${projectId}/${wip.key}`;
|
|
76527
|
+
if (wip.sha && wip.tracked.length > 0) {
|
|
76528
|
+
const patch9 = join51(scratch, "changes.patch");
|
|
76529
|
+
const index2 = resolve16(wip.path, gitOrThrow(wip.path, ["rev-parse", "--git-path", "index"]).trim());
|
|
76530
|
+
const indexCopy = join51(scratch, "index");
|
|
76531
|
+
if (existsSync43(index2))
|
|
76532
|
+
copyFileSync4(index2, indexCopy);
|
|
76533
|
+
const fd = openSync6(patch9, "w");
|
|
76534
|
+
try {
|
|
76535
|
+
const diff8 = spawnSync4("git", ["-c", "core.quotepath=off", "diff", "--binary", "HEAD"], {
|
|
76536
|
+
cwd: wip.path,
|
|
76537
|
+
stdio: ["ignore", fd, "pipe"],
|
|
76538
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_INDEX_FILE: indexCopy },
|
|
76539
|
+
windowsHide: true
|
|
76540
|
+
});
|
|
76541
|
+
if (diff8.status !== 0)
|
|
76542
|
+
throw new Error(`git diff failed in ${wip.path}: ${String(diff8.stderr)}`);
|
|
76543
|
+
} finally {
|
|
76544
|
+
closeSync6(fd);
|
|
76545
|
+
}
|
|
76546
|
+
await tar.addFile(`${prefix}/changes.patch`, patch9);
|
|
76547
|
+
rmSync11(patch9, { force: true });
|
|
76548
|
+
rmSync11(indexCopy, { force: true });
|
|
76549
|
+
}
|
|
76550
|
+
for (const rel of [...wip.tracked, ...wip.untracked]) {
|
|
76551
|
+
const path3 = join51(wip.path, rel);
|
|
76552
|
+
try {
|
|
76553
|
+
if (isSymlink(path3) || !statSync15(path3).isFile())
|
|
76554
|
+
continue;
|
|
76555
|
+
} catch {
|
|
76556
|
+
continue;
|
|
76557
|
+
}
|
|
76558
|
+
await tar.addFile(`${prefix}/files/${rel}`, path3);
|
|
76559
|
+
}
|
|
76560
|
+
}
|
|
76561
|
+
async function packMove(input) {
|
|
76562
|
+
const { env, scan, open: open4, out } = input;
|
|
76563
|
+
const log6 = input.log ?? (() => {});
|
|
76564
|
+
const scratch = mkdtempSync2(join51(dirname23(out), "runcastle-move-"));
|
|
76565
|
+
const tar = new TarWriter(out);
|
|
76566
|
+
try {
|
|
76567
|
+
log6("Copying the database");
|
|
76568
|
+
const dbCopy = join51(scratch, "runcastle.db");
|
|
76569
|
+
const { rowCounts, migrations } = snapshotDatabase(env, open4, dbCopy);
|
|
76570
|
+
await tar.addFile("db/runcastle.db", dbCopy);
|
|
76571
|
+
rmSync11(dbCopy, { force: true });
|
|
76572
|
+
log6("Packing chats, reviews and notes");
|
|
76573
|
+
await addDataEntries(tar, env);
|
|
76574
|
+
for (const folder of scan.claudeFolders) {
|
|
76575
|
+
await addTree(tar, join51(env.claudeDir, "projects", folder.name), `claude/${folder.name}`);
|
|
76576
|
+
}
|
|
76577
|
+
for (const project3 of scan.projects) {
|
|
76578
|
+
if (!project3.repoMoves)
|
|
76579
|
+
continue;
|
|
76580
|
+
log6(`Packing ${project3.name}`);
|
|
76581
|
+
if (Object.keys(project3.refs).length > 0)
|
|
76582
|
+
await addRepo(tar, project3.id, project3.repoPath, scratch);
|
|
76583
|
+
for (const wip of project3.wip)
|
|
76584
|
+
await addWip(tar, project3.id, wip, scratch);
|
|
76585
|
+
for (const rel of project3.carried)
|
|
76586
|
+
await tar.addFile(`carried/${project3.id}/${rel}`, join51(project3.repoPath, rel));
|
|
76587
|
+
}
|
|
76588
|
+
if (scan.claudeSetup) {
|
|
76589
|
+
log6("Packing your Claude setup");
|
|
76590
|
+
for (const rel of scan.claudeSetup.files)
|
|
76591
|
+
await tar.addFile(`claude-setup/${rel}`, join51(env.claudeDir, rel));
|
|
76592
|
+
}
|
|
76593
|
+
const manifest = {
|
|
76594
|
+
formatVersion: 1,
|
|
76595
|
+
runcastleVersion: runcastleVersion(),
|
|
76596
|
+
createdAt: Date.now(),
|
|
76597
|
+
source: env,
|
|
76598
|
+
migrations,
|
|
76599
|
+
projects: scan.projects,
|
|
76600
|
+
claudeFolders: scan.claudeFolders,
|
|
76601
|
+
conversations: scan.conversations,
|
|
76602
|
+
envCarried: scan.envCarried,
|
|
76603
|
+
...scan.claudeSetup ? { claudeSetup: scan.claudeSetup } : {},
|
|
76604
|
+
reportText: input.reportText ?? "",
|
|
76605
|
+
rowCounts,
|
|
76606
|
+
files: tar.files
|
|
76607
|
+
};
|
|
76608
|
+
await tar.addBuffer("manifest.json", Buffer.from(JSON.stringify(manifest), "utf8"));
|
|
76609
|
+
await tar.finish();
|
|
76610
|
+
return manifest;
|
|
76611
|
+
} catch (err) {
|
|
76612
|
+
tar.abort();
|
|
76613
|
+
rmSync11(out, { force: true });
|
|
76614
|
+
throw err;
|
|
76615
|
+
} finally {
|
|
76616
|
+
rmSync11(scratch, { recursive: true, force: true });
|
|
76617
|
+
}
|
|
76618
|
+
}
|
|
76619
|
+
var init_pack = __esm(() => {
|
|
76620
|
+
init_version();
|
|
76621
|
+
init_scan();
|
|
76622
|
+
init_shell();
|
|
76623
|
+
init_tar();
|
|
76624
|
+
});
|
|
76625
|
+
|
|
76626
|
+
// src/move/remote.ts
|
|
76627
|
+
import { basename as basename8, dirname as dirname24 } from "path";
|
|
76628
|
+
function remoteEnv() {
|
|
76629
|
+
const extra = process.env.RUNCASTLE_MOVE_REMOTE_ENV?.trim();
|
|
76630
|
+
return extra ? `${REMOTE_PATH} ${extra}` : REMOTE_PATH;
|
|
76631
|
+
}
|
|
76632
|
+
function flagWords(flags) {
|
|
76633
|
+
return [
|
|
76634
|
+
...flags.replace ? ["--replace"] : [],
|
|
76635
|
+
...flags.reposDir ? ["--repos-dir", `"${flags.reposDir}"`] : []
|
|
76636
|
+
].join(" ");
|
|
76637
|
+
}
|
|
76638
|
+
function remoteMoveInCommand(packName, flags) {
|
|
76639
|
+
return [remoteEnv(), `runcastle move-in "$HOME/${packName}"`, flagWords(flags)].filter(Boolean).join(" ");
|
|
76640
|
+
}
|
|
76641
|
+
function remoteCheckCommand(flags) {
|
|
76642
|
+
return [remoteEnv(), "runcastle move-in --check", flagWords(flags)].filter(Boolean).join(" ");
|
|
76643
|
+
}
|
|
76644
|
+
function manualSteps(pack2, host, flags) {
|
|
76645
|
+
const target = host ?? "<user@host>";
|
|
76646
|
+
return [
|
|
76647
|
+
`cd "${dirname24(pack2)}"`,
|
|
76648
|
+
`scp "${basename8(pack2)}" ${target}:`,
|
|
76649
|
+
`ssh ${target} '${remoteMoveInCommand(basename8(pack2), flags)}'`
|
|
76650
|
+
];
|
|
76651
|
+
}
|
|
76652
|
+
function failureMessage2(step4, detail, pack2, host, flags) {
|
|
76653
|
+
return [
|
|
76654
|
+
`${step4} failed: ${detail}`,
|
|
76655
|
+
"",
|
|
76656
|
+
`Nothing was lost. The pack is kept at ${pack2}, and this machine is unchanged.`,
|
|
76657
|
+
"Finish by hand with:",
|
|
76658
|
+
...manualSteps(pack2, host, flags).map((line2) => ` ${line2}`)
|
|
76659
|
+
].join(`
|
|
76660
|
+
`);
|
|
76661
|
+
}
|
|
76662
|
+
function askDestination(host, question, flags) {
|
|
76663
|
+
const result = run8("ssh", [host, remoteCheckCommand(flags)], {
|
|
76664
|
+
input: JSON.stringify(question),
|
|
76665
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
76666
|
+
});
|
|
76667
|
+
const last2 = result.stdout.trim().split(`
|
|
76668
|
+
`).pop() ?? "";
|
|
76669
|
+
try {
|
|
76670
|
+
const parsed = JSON.parse(last2);
|
|
76671
|
+
if (Array.isArray(parsed.blockers))
|
|
76672
|
+
return { answer: parsed, detail: "" };
|
|
76673
|
+
} catch {}
|
|
76674
|
+
const detail = result.stderr.trim() || "it does not have a runcastle with move-in; install this version there";
|
|
76675
|
+
return { answer: null, detail };
|
|
76676
|
+
}
|
|
76677
|
+
function copyPack(pack2, host) {
|
|
76678
|
+
const result = run8("scp", [basename8(pack2), `${host}:`], { cwd: dirname24(pack2), stdio: "inherit" });
|
|
76679
|
+
return { ok: result.ok, detail: result.ok ? "" : `scp to ${host} did not finish` };
|
|
76680
|
+
}
|
|
76681
|
+
function runRemoteMoveIn(pack2, host, flags) {
|
|
76682
|
+
const result = run8("ssh", [host, remoteMoveInCommand(basename8(pack2), flags)], { stdio: "inherit" });
|
|
76683
|
+
return { ok: result.ok, detail: result.ok ? "" : `move-in on ${host} did not finish` };
|
|
76684
|
+
}
|
|
76685
|
+
var REMOTE_PATH = 'PATH="$HOME/.bun/bin:$PATH"';
|
|
76686
|
+
var init_remote = __esm(() => {
|
|
76687
|
+
init_shell();
|
|
76688
|
+
});
|
|
76689
|
+
|
|
76690
|
+
// src/move/report.ts
|
|
76691
|
+
function formatReport2(report, destination, windowsCommands) {
|
|
76692
|
+
const blockers = [
|
|
76693
|
+
...report.blockers,
|
|
76694
|
+
...destination?.answer?.blockers ?? [],
|
|
76695
|
+
...destination && !destination.answer ? [`the destination could not be checked: ${destination.detail}`] : []
|
|
76696
|
+
];
|
|
76697
|
+
const out = [];
|
|
76698
|
+
if (blockers.length > 0) {
|
|
76699
|
+
out.push("Blocks", ...blockers.map((line2) => ` x ${line2}`), "");
|
|
76700
|
+
}
|
|
76701
|
+
const width = Math.max(10, ...report.projects.map((p) => p.name.length));
|
|
76702
|
+
const reposDir = destination?.answer?.reposDir ?? "<repos folder>";
|
|
76703
|
+
out.push(`Moves${" ".repeat(width - 2)}branches uncommitted features chats repo size`);
|
|
76704
|
+
for (const p of report.projects) {
|
|
76705
|
+
const row2 = `${String(p.branches).padStart(8)} ${String(p.uncommitted).padStart(11)} ${String(p.features).padStart(8)} ${String(p.chats).padStart(5)} ${formatBytes(p.repoBytes).padStart(9)}`;
|
|
76706
|
+
out.push(` + ${p.name.padEnd(width)} ${row2}${p.repoMoves ? "" : " (repo stays)"}`);
|
|
76707
|
+
out.push(` ${p.repoPath} -> ${reposDir}/${p.targetFolder}`);
|
|
76708
|
+
}
|
|
76709
|
+
out.push("");
|
|
76710
|
+
const attention = [...windowsCommands, ...report.attention, ...destination?.answer?.notes ?? []];
|
|
76711
|
+
if (attention.length > 0) {
|
|
76712
|
+
out.push("Worth a look", ...attention.map((line2) => ` ! ${line2}`), "");
|
|
76713
|
+
}
|
|
76714
|
+
out.push(`Stays behind ${report.staysBehind.join(", ")}`, "");
|
|
76715
|
+
const size6 = `Pack holds about ${formatBytes(report.dataBytes + report.repoBytes)} before compression.`;
|
|
76716
|
+
const free = destination?.answer?.freeBytes;
|
|
76717
|
+
const where = destination?.answer ? ` Repos land in ${destination.answer.reposDir}.` : "";
|
|
76718
|
+
const room = free != null ? ` The destination has ${formatBytes(free)} free.` : "";
|
|
76719
|
+
const unchecked = destination ? "" : " The destination was not checked; give a host to check it.";
|
|
76720
|
+
out.push(`${size6}${where}${room}${unchecked}`);
|
|
76721
|
+
return out.join(`
|
|
76722
|
+
`);
|
|
76723
|
+
}
|
|
76724
|
+
function isBlocked(report, destination) {
|
|
76725
|
+
if (report.blockers.length > 0)
|
|
76726
|
+
return true;
|
|
76727
|
+
if (!destination)
|
|
76728
|
+
return false;
|
|
76729
|
+
return !destination.answer || destination.answer.blockers.length > 0;
|
|
76730
|
+
}
|
|
76731
|
+
function formatMoveInResult(result) {
|
|
76732
|
+
const out = ["Moved in.", "", "Repos", ...result.repos.map((r) => ` ${r.name}: ${r.path}`)];
|
|
76733
|
+
if (result.wipApplied.length > 0) {
|
|
76734
|
+
out.push("", "Uncommitted work put back", ...result.wipApplied.map((w) => ` ${w}`));
|
|
76735
|
+
}
|
|
76736
|
+
if (result.wipKept.length > 0) {
|
|
76737
|
+
out.push("", "Uncommitted work that would not apply, kept here", ...result.wipKept.map((w) => ` ${w.project} (${w.key}): ${w.folder}`));
|
|
76738
|
+
}
|
|
76739
|
+
if (result.wipStuck.length > 0) {
|
|
76740
|
+
out.push("", `Uncommitted work that could not be placed. Copy it out of ${result.stagingKept}/wip before anything else`, ...result.wipStuck.map((w) => ` ${w}`));
|
|
76741
|
+
}
|
|
76742
|
+
if (result.commandsWindows.length > 0) {
|
|
76743
|
+
out.push("", "Commands that only work on Windows; change these in the project settings");
|
|
76744
|
+
out.push(...result.commandsWindows.map((c) => ` ${c.project}, ${c.field}: ${c.value}`));
|
|
76745
|
+
}
|
|
76746
|
+
if (result.commandsChanged.length > 0) {
|
|
76747
|
+
out.push("", "Commands with their paths rewritten");
|
|
76748
|
+
out.push(...result.commandsChanged.map((c) => ` ${c.project}, ${c.field}`));
|
|
76749
|
+
}
|
|
76750
|
+
if (result.claudeSetup) {
|
|
76751
|
+
if (result.claudeSetup.chatsKept)
|
|
76752
|
+
out.push("", "Claude Code now keeps chats for 10 years, so it will not delete moved ones.");
|
|
76753
|
+
if (result.claudeSetup.failed.length > 0) {
|
|
76754
|
+
out.push("", "Claude setup that did not go in", ...result.claudeSetup.failed.map((item) => ` ${item}`));
|
|
76755
|
+
}
|
|
76756
|
+
}
|
|
76757
|
+
if (result.setAside.length > 0) {
|
|
76758
|
+
out.push("", "Set aside", ...result.setAside.map((s) => ` ${s.to}`));
|
|
76759
|
+
}
|
|
76760
|
+
if (result.unmappedPaths > 0) {
|
|
76761
|
+
out.push("", `${result.unmappedPaths} stored paths matched no known folder and were left as they were.`);
|
|
76762
|
+
}
|
|
76763
|
+
out.push("", `Full report: ${result.reportPath}`, "", result.envCarried ? "Next: start runcastle here." : "Next: start runcastle here, and recreate the .env files.", "This machine is now the only home. Do not work on the old one.");
|
|
76764
|
+
return out.join(`
|
|
76765
|
+
`);
|
|
76766
|
+
}
|
|
76767
|
+
var init_report2 = __esm(() => {
|
|
76768
|
+
init_shell();
|
|
76769
|
+
});
|
|
76770
|
+
|
|
76771
|
+
// src/move/unpack.ts
|
|
76772
|
+
import {
|
|
76773
|
+
chmodSync,
|
|
76774
|
+
closeSync as closeSync7,
|
|
76775
|
+
copyFileSync as copyFileSync5,
|
|
76776
|
+
cpSync as cpSync3,
|
|
76777
|
+
existsSync as existsSync44,
|
|
76778
|
+
mkdirSync as mkdirSync28,
|
|
76779
|
+
openSync as openSync7,
|
|
76780
|
+
readdirSync as readdirSync14,
|
|
76781
|
+
readFileSync as readFileSync30,
|
|
76782
|
+
readSync as readSync4,
|
|
76783
|
+
renameSync as renameSync2,
|
|
76784
|
+
rmSync as rmSync12,
|
|
76785
|
+
statSync as statSync16,
|
|
76786
|
+
writeFileSync as writeFileSync22
|
|
76787
|
+
} from "fs";
|
|
76788
|
+
import { basename as basename9, dirname as dirname25, join as join52, resolve as resolve17 } from "path";
|
|
76789
|
+
function extract(pack2, into) {
|
|
76790
|
+
const result = run8(systemTar(), ["-xzf", basename9(pack2), "-C", into], { cwd: dirname25(resolve17(pack2)) });
|
|
76791
|
+
if (!result.ok)
|
|
76792
|
+
throw new Error(`could not unpack ${pack2}: ${result.stderr.trim()}`);
|
|
76793
|
+
}
|
|
76794
|
+
async function verifyFiles(staging, manifest) {
|
|
76795
|
+
for (const file of manifest.files) {
|
|
76796
|
+
const path3 = join52(staging, file.path);
|
|
76797
|
+
if (!existsSync44(path3))
|
|
76798
|
+
throw new Error(`the pack is missing ${file.path}`);
|
|
76799
|
+
if (await hashFile(path3) !== file.sha256)
|
|
76800
|
+
throw new Error(`${file.path} did not arrive intact`);
|
|
76801
|
+
}
|
|
76802
|
+
}
|
|
76803
|
+
function pathRules(manifest, env, reposDir) {
|
|
76804
|
+
return [
|
|
76805
|
+
...manifest.projects.map((p) => ({ from: p.repoPath, to: join52(reposDir, p.targetFolder) })),
|
|
76806
|
+
{ from: manifest.source.dataDir, to: env.dataDir }
|
|
76807
|
+
];
|
|
76808
|
+
}
|
|
76809
|
+
function claudeRenames(manifest, rules) {
|
|
76810
|
+
const platform = manifest.source.platform;
|
|
76811
|
+
const renames = new Map;
|
|
76812
|
+
for (const folder of manifest.claudeFolders) {
|
|
76813
|
+
const mapped = mapSourcePath(folder.cwd, rules, platform);
|
|
76814
|
+
renames.set(folder.name, mapped ? encodeClaudeFolder(mapped) : folder.name);
|
|
76815
|
+
}
|
|
76816
|
+
return renames;
|
|
76817
|
+
}
|
|
76818
|
+
function mapTranscript(path3, manifest, rules, renames, env) {
|
|
76819
|
+
const platform = manifest.source.platform;
|
|
76820
|
+
const projects2 = `${manifest.source.claudeDir}${platform === "win32" ? "\\" : "/"}projects`;
|
|
76821
|
+
const rest = mapSourcePath(path3, [{ from: projects2, to: "" }], platform);
|
|
76822
|
+
if (rest === null)
|
|
76823
|
+
return mapSourcePath(path3, rules, platform);
|
|
76824
|
+
const [, folder, ...tail] = rest.split("/");
|
|
76825
|
+
const key = platform === "win32" ? folder.toLowerCase() : folder;
|
|
76826
|
+
for (const [oldName, newName] of renames) {
|
|
76827
|
+
if ((platform === "win32" ? oldName.toLowerCase() : oldName) === key) {
|
|
76828
|
+
return join52(env.claudeDir, "projects", newName, ...tail);
|
|
76829
|
+
}
|
|
76830
|
+
}
|
|
76831
|
+
return null;
|
|
76832
|
+
}
|
|
76833
|
+
function rewriteCommands(sql5, rules, platform) {
|
|
76834
|
+
const result = { changed: [], windows: [] };
|
|
76835
|
+
const projects2 = sql5.all("SELECT * FROM projects");
|
|
76836
|
+
for (const project3 of projects2) {
|
|
76837
|
+
for (const { column, label } of HOST_COMMANDS) {
|
|
76838
|
+
const value = project3[column];
|
|
76839
|
+
if (!value)
|
|
76840
|
+
continue;
|
|
76841
|
+
const next4 = rewritePathsInText(value, rules, platform);
|
|
76842
|
+
if (next4 !== value) {
|
|
76843
|
+
sql5.run(`UPDATE projects SET ${column} = ? WHERE id = ?`, next4, project3.id);
|
|
76844
|
+
result.changed.push({ project: project3.name, field: label, from: value, to: next4 });
|
|
76845
|
+
}
|
|
76846
|
+
if (looksWindowsOnly(next4))
|
|
76847
|
+
result.windows.push({ project: project3.name, field: label, value: next4 });
|
|
76848
|
+
}
|
|
76849
|
+
}
|
|
76850
|
+
return result;
|
|
76851
|
+
}
|
|
76852
|
+
function rewriteDatabase(sql5, manifest, rules, renames, env) {
|
|
76853
|
+
const platform = manifest.source.platform;
|
|
76854
|
+
let unmapped = 0;
|
|
76855
|
+
let commands;
|
|
76856
|
+
sql5.run("BEGIN");
|
|
76857
|
+
try {
|
|
76858
|
+
for (const row2 of sql5.all("SELECT id, repo_path FROM projects")) {
|
|
76859
|
+
const mapped = mapSourcePath(row2.repo_path, rules, platform);
|
|
76860
|
+
if (mapped)
|
|
76861
|
+
sql5.run("UPDATE projects SET repo_path = ? WHERE id = ?", mapped, row2.id);
|
|
76862
|
+
else
|
|
76863
|
+
unmapped++;
|
|
76864
|
+
}
|
|
76865
|
+
const sessions2 = sql5.all("SELECT id, worktree_path, transcript_path FROM sessions");
|
|
76866
|
+
for (const row2 of sessions2) {
|
|
76867
|
+
const worktree = mapSourcePath(row2.worktree_path, rules, platform);
|
|
76868
|
+
if (worktree)
|
|
76869
|
+
sql5.run("UPDATE sessions SET worktree_path = ? WHERE id = ?", worktree, row2.id);
|
|
76870
|
+
else
|
|
76871
|
+
unmapped++;
|
|
76872
|
+
if (!row2.transcript_path)
|
|
76873
|
+
continue;
|
|
76874
|
+
const transcript = mapTranscript(row2.transcript_path, manifest, rules, renames, env);
|
|
76875
|
+
if (transcript)
|
|
76876
|
+
sql5.run("UPDATE sessions SET transcript_path = ? WHERE id = ?", transcript, row2.id);
|
|
76877
|
+
else
|
|
76878
|
+
unmapped++;
|
|
76879
|
+
}
|
|
76880
|
+
commands = rewriteCommands(sql5, [...rules, { from: manifest.source.home, to: env.home }], platform);
|
|
76881
|
+
if (platform !== env.platform)
|
|
76882
|
+
sql5.run("UPDATE project_findings SET verified_at = NULL, verified_sha = NULL");
|
|
76883
|
+
sql5.run("COMMIT");
|
|
76884
|
+
} catch (err) {
|
|
76885
|
+
sql5.run("ROLLBACK");
|
|
76886
|
+
throw err;
|
|
76887
|
+
}
|
|
76888
|
+
return { unmapped, commands };
|
|
76889
|
+
}
|
|
76890
|
+
function rebuildRepo(project3, staging, path3) {
|
|
76891
|
+
mkdirSync28(path3, { recursive: true });
|
|
76892
|
+
gitOrThrow(path3, ["init", "-q"]);
|
|
76893
|
+
if (Object.keys(project3.refs).length > 0) {
|
|
76894
|
+
const bundle = join52(staging, "repos", `${project3.id}.bundle`);
|
|
76895
|
+
gitOrThrow(path3, ["fetch", "-q", "--no-tags", "--update-head-ok", bundle, "+refs/*:refs/*"]);
|
|
76896
|
+
}
|
|
76897
|
+
for (const remote of project3.remotes)
|
|
76898
|
+
gitOrThrow(path3, ["remote", "add", remote.name, remote.url]);
|
|
76899
|
+
for (const { key, value } of project3.config)
|
|
76900
|
+
gitOrThrow(path3, ["config", "--local", "--add", key, value]);
|
|
76901
|
+
if (project3.headBranch) {
|
|
76902
|
+
gitOrThrow(path3, ["symbolic-ref", "HEAD", `refs/heads/${project3.headBranch}`]);
|
|
76903
|
+
if (project3.headSha)
|
|
76904
|
+
gitOrThrow(path3, ["reset", "-q", "--hard"]);
|
|
76905
|
+
} else if (project3.headSha) {
|
|
76906
|
+
gitOrThrow(path3, ["checkout", "-q", "-f", "--detach", project3.headSha]);
|
|
76907
|
+
}
|
|
76908
|
+
restoreStash(project3, path3);
|
|
76909
|
+
if (project3.excludeRules) {
|
|
76910
|
+
mkdirSync28(join52(path3, ".git", "info"), { recursive: true });
|
|
76911
|
+
writeFileSync22(join52(path3, ".git", "info", "exclude"), project3.excludeRules);
|
|
76912
|
+
}
|
|
76913
|
+
for (const rel of project3.carried) {
|
|
76914
|
+
mkdirSync28(dirname25(join52(path3, rel)), { recursive: true });
|
|
76915
|
+
copyFileSync5(join52(staging, "carried", project3.id, rel), join52(path3, rel));
|
|
76916
|
+
}
|
|
76917
|
+
}
|
|
76918
|
+
function restoreStash(project3, path3) {
|
|
76919
|
+
const sha = project3.refs["refs/stash"];
|
|
76920
|
+
if (!sha)
|
|
76921
|
+
return;
|
|
76922
|
+
gitOrThrow(path3, ["update-ref", "-d", "refs/stash"]);
|
|
76923
|
+
gitOrThrow(path3, ["-c", "user.name=runcastle", "-c", "user.email=runcastle@localhost", "stash", "store", "-m", "moved from the old machine", sha]);
|
|
76924
|
+
}
|
|
76925
|
+
function verifyRefs(project3, path3) {
|
|
76926
|
+
const listing = gitOrThrow(path3, ["for-each-ref", "--format=%(objectname) %(refname)"]);
|
|
76927
|
+
const found = new Map(listing.split(`
|
|
76928
|
+
`).filter(Boolean).map((line2) => {
|
|
76929
|
+
const space = line2.indexOf(" ");
|
|
76930
|
+
return [line2.slice(space + 1).trim(), line2.slice(0, space)];
|
|
76931
|
+
}));
|
|
76932
|
+
for (const [ref, sha] of Object.entries(project3.refs)) {
|
|
76933
|
+
if (found.get(ref) !== sha)
|
|
76934
|
+
throw new Error(`${project3.name}: ${ref} did not arrive at ${sha}`);
|
|
76935
|
+
}
|
|
76936
|
+
}
|
|
76937
|
+
function verifyConversations(staging, manifest) {
|
|
76938
|
+
for (const { id: id3, folder } of manifest.conversations) {
|
|
76939
|
+
if (!existsSync44(join52(staging, "claude", folder, `${id3}.jsonl`))) {
|
|
76940
|
+
throw new Error(`the pack is missing the transcript of conversation ${id3}`);
|
|
76941
|
+
}
|
|
76942
|
+
}
|
|
76943
|
+
}
|
|
76944
|
+
function verifyRowCounts(sql5, manifest) {
|
|
76945
|
+
const counts = countRows(sql5);
|
|
76946
|
+
for (const [table, count6] of Object.entries(manifest.rowCounts)) {
|
|
76947
|
+
if (counts[table] !== count6) {
|
|
76948
|
+
throw new Error(`the database table ${table} has ${counts[table]} rows, the pack says ${count6}`);
|
|
76949
|
+
}
|
|
76950
|
+
}
|
|
76951
|
+
}
|
|
76952
|
+
function mergeSettings(staged, current2) {
|
|
76953
|
+
if (!existsSync44(staged))
|
|
76954
|
+
return;
|
|
76955
|
+
const incoming = JSON.parse(readFileSync30(staged, "utf8"));
|
|
76956
|
+
let own = {};
|
|
76957
|
+
try {
|
|
76958
|
+
own = JSON.parse(readFileSync30(current2, "utf8"));
|
|
76959
|
+
} catch {}
|
|
76960
|
+
for (const key of MACHINE_SETTINGS) {
|
|
76961
|
+
if (key in own)
|
|
76962
|
+
incoming[key] = own[key];
|
|
76963
|
+
else
|
|
76964
|
+
delete incoming[key];
|
|
76965
|
+
}
|
|
76966
|
+
writeFileSync22(staged, `${JSON.stringify(incoming, null, 2)}
|
|
76967
|
+
`);
|
|
76968
|
+
}
|
|
76969
|
+
|
|
76970
|
+
class Swap {
|
|
76971
|
+
made = [];
|
|
76972
|
+
rename(from, to) {
|
|
76973
|
+
mkdirSync28(dirname25(to), { recursive: true });
|
|
76974
|
+
renameSync2(from, to);
|
|
76975
|
+
this.made.push({ from, to });
|
|
76976
|
+
}
|
|
76977
|
+
undo() {
|
|
76978
|
+
for (const { from, to } of this.made.reverse()) {
|
|
76979
|
+
try {
|
|
76980
|
+
renameSync2(to, from);
|
|
76981
|
+
} catch {}
|
|
76982
|
+
}
|
|
76983
|
+
}
|
|
76984
|
+
}
|
|
76985
|
+
function placeClaudeFolder(from, to, swap2, when3) {
|
|
76986
|
+
if (!existsSync44(to)) {
|
|
76987
|
+
swap2.rename(from, to);
|
|
76988
|
+
return;
|
|
76989
|
+
}
|
|
76990
|
+
if (statSync16(from).isDirectory() && statSync16(to).isDirectory()) {
|
|
76991
|
+
for (const entry of readdirSync14(from))
|
|
76992
|
+
placeClaudeFolder(join52(from, entry), join52(to, entry), swap2, when3);
|
|
76993
|
+
return;
|
|
76994
|
+
}
|
|
76995
|
+
if (readFileSync30(from).equals(readFileSync30(to)))
|
|
76996
|
+
return;
|
|
76997
|
+
let aside = `${to}.moved-${when3}`;
|
|
76998
|
+
for (let n = 2;existsSync44(aside); n++)
|
|
76999
|
+
aside = `${to}.moved-${when3}-${n}`;
|
|
77000
|
+
swap2.rename(from, aside);
|
|
77001
|
+
}
|
|
77002
|
+
function restoreScriptBits(dir) {
|
|
77003
|
+
for (const rel of walkFiles(dir)) {
|
|
77004
|
+
const path3 = join52(dir, rel);
|
|
77005
|
+
const start3 = Buffer.alloc(2);
|
|
77006
|
+
const fd = openSync7(path3, "r");
|
|
77007
|
+
try {
|
|
77008
|
+
readSync4(fd, start3, 0, 2, 0);
|
|
77009
|
+
} finally {
|
|
77010
|
+
closeSync7(fd);
|
|
77011
|
+
}
|
|
77012
|
+
if (start3.toString() === "#!")
|
|
77013
|
+
chmodSync(path3, 448);
|
|
77014
|
+
}
|
|
77015
|
+
}
|
|
77016
|
+
function settleClaudeSettings(staged, current2) {
|
|
77017
|
+
if (!existsSync44(staged))
|
|
77018
|
+
return false;
|
|
77019
|
+
const incoming = JSON.parse(readFileSync30(staged, "utf8"));
|
|
77020
|
+
let own = {};
|
|
77021
|
+
try {
|
|
77022
|
+
own = JSON.parse(readFileSync30(current2, "utf8"));
|
|
77023
|
+
} catch {}
|
|
77024
|
+
const keepChats = incoming.cleanupPeriodDays === undefined;
|
|
77025
|
+
const settled = { ...own, ...incoming, ...keepChats ? { cleanupPeriodDays: CHAT_KEEP_DAYS } : {} };
|
|
77026
|
+
writeFileSync22(staged, `${JSON.stringify(settled, null, 2)}
|
|
77027
|
+
`);
|
|
77028
|
+
return keepChats;
|
|
77029
|
+
}
|
|
77030
|
+
function addMcpServers(env, servers2, aside) {
|
|
77031
|
+
if (Object.keys(servers2).length === 0)
|
|
77032
|
+
return true;
|
|
77033
|
+
const path3 = claudeJsonPath(env);
|
|
77034
|
+
let own = {};
|
|
77035
|
+
if (existsSync44(path3)) {
|
|
77036
|
+
try {
|
|
77037
|
+
own = JSON.parse(readFileSync30(path3, "utf8"));
|
|
77038
|
+
} catch {
|
|
77039
|
+
return false;
|
|
77040
|
+
}
|
|
77041
|
+
copyFileSync5(path3, aside);
|
|
77042
|
+
}
|
|
77043
|
+
const mcpServers = { ...own.mcpServers, ...servers2 };
|
|
77044
|
+
writeFileSync22(path3, `${JSON.stringify({ ...own, mcpServers }, null, 2)}
|
|
77045
|
+
`, { mode: 384 });
|
|
77046
|
+
return true;
|
|
77047
|
+
}
|
|
77048
|
+
function installPlugins(setup, runClaude) {
|
|
77049
|
+
const failed = [];
|
|
77050
|
+
for (const marketplace of setup.marketplaces) {
|
|
77051
|
+
if (!runClaude(["plugin", "marketplace", "add", marketplace]))
|
|
77052
|
+
failed.push(`plugin marketplace ${marketplace}`);
|
|
77053
|
+
}
|
|
77054
|
+
for (const plugin of setup.plugins) {
|
|
77055
|
+
if (!runClaude(["plugin", "install", plugin]))
|
|
77056
|
+
failed.push(`plugin ${plugin}`);
|
|
77057
|
+
}
|
|
77058
|
+
return failed;
|
|
77059
|
+
}
|
|
77060
|
+
function claudeOn(env) {
|
|
77061
|
+
const PATH = [join52(env.home, ".local", "bin"), process.env.PATH].filter(Boolean).join(":");
|
|
77062
|
+
return (args2) => run8("claude", args2, { env: { ...process.env, PATH } }).ok;
|
|
77063
|
+
}
|
|
77064
|
+
function applyWip(project3, wip, repoPath, checkout, staging) {
|
|
77065
|
+
const source = join52(staging, "wip", project3.id, wip.key);
|
|
77066
|
+
const patch9 = join52(source, "changes.patch");
|
|
77067
|
+
const applied = !existsSync44(patch9) || run8("git", ["apply", "--whitespace=nowarn", patch9], { cwd: checkout }).ok;
|
|
77068
|
+
if (!applied) {
|
|
77069
|
+
const kept = join52(`${repoPath}.uncommitted`, wip.key);
|
|
77070
|
+
mkdirSync28(dirname25(kept), { recursive: true });
|
|
77071
|
+
cpSync3(source, kept, { recursive: true });
|
|
77072
|
+
return kept;
|
|
77073
|
+
}
|
|
77074
|
+
for (const rel of wip.untracked) {
|
|
77075
|
+
const from = join52(source, "files", rel);
|
|
77076
|
+
if (!existsSync44(from))
|
|
77077
|
+
continue;
|
|
77078
|
+
mkdirSync28(dirname25(join52(checkout, rel)), { recursive: true });
|
|
77079
|
+
copyFileSync5(from, join52(checkout, rel));
|
|
77080
|
+
}
|
|
77081
|
+
return null;
|
|
77082
|
+
}
|
|
77083
|
+
function addTalkWorktree(repoPath, path3, wip) {
|
|
77084
|
+
mkdirSync28(dirname25(path3), { recursive: true });
|
|
77085
|
+
if (wip.branch && run8("git", ["worktree", "add", "-q", path3, wip.branch], { cwd: repoPath }).ok)
|
|
77086
|
+
return;
|
|
77087
|
+
gitOrThrow(repoPath, ["worktree", "add", "-q", "--detach", path3, wip.sha ?? "HEAD"]);
|
|
77088
|
+
}
|
|
77089
|
+
function renderReport(manifest, result) {
|
|
77090
|
+
const lines2 = [
|
|
77091
|
+
"# runcastle move report",
|
|
77092
|
+
"",
|
|
77093
|
+
`Moved ${new Date(manifest.createdAt).toISOString()} from ${manifest.source.platform} (${manifest.source.home}).`,
|
|
77094
|
+
"",
|
|
77095
|
+
"## Repos",
|
|
77096
|
+
...result.repos.map((r) => `- ${r.name}: ${r.path}`)
|
|
77097
|
+
];
|
|
77098
|
+
if (result.wipStuck.length > 0) {
|
|
77099
|
+
lines2.push("", `## Uncommitted work still in ${result.stagingKept}/wip`, "");
|
|
77100
|
+
for (const w of result.wipStuck)
|
|
77101
|
+
lines2.push(`- ${w}`);
|
|
77102
|
+
}
|
|
77103
|
+
if (result.commandsWindows.length > 0) {
|
|
77104
|
+
lines2.push("", "## Commands that only work on Windows", "");
|
|
77105
|
+
for (const c of result.commandsWindows)
|
|
77106
|
+
lines2.push(`- ${c.project}, ${c.field}: \`${c.value}\``);
|
|
77107
|
+
}
|
|
77108
|
+
if (result.commandsChanged.length > 0) {
|
|
77109
|
+
lines2.push("", "## Commands with their paths rewritten", "");
|
|
77110
|
+
for (const c of result.commandsChanged)
|
|
77111
|
+
lines2.push(`- ${c.project}, ${c.field}: \`${c.from}\` became \`${c.to}\``);
|
|
77112
|
+
}
|
|
77113
|
+
if (result.claudeSetup && result.claudeSetup.failed.length > 0) {
|
|
77114
|
+
lines2.push("", "## Claude setup that did not go in", "");
|
|
77115
|
+
for (const item of result.claudeSetup.failed)
|
|
77116
|
+
lines2.push(`- ${item}`);
|
|
77117
|
+
}
|
|
77118
|
+
if (result.wipKept.length > 0) {
|
|
77119
|
+
lines2.push("", "## Uncommitted work kept beside the repo", "");
|
|
77120
|
+
for (const w of result.wipKept)
|
|
77121
|
+
lines2.push(`- ${w.project} (${w.key}): ${w.folder}`);
|
|
77122
|
+
}
|
|
77123
|
+
if (result.setAside.length > 0) {
|
|
77124
|
+
lines2.push("", "## Set aside", "");
|
|
77125
|
+
for (const s of result.setAside)
|
|
77126
|
+
lines2.push(`- ${s.from} is now ${s.to}`);
|
|
77127
|
+
}
|
|
77128
|
+
if (result.unmappedPaths > 0) {
|
|
77129
|
+
lines2.push("", `${result.unmappedPaths} stored paths matched no known folder and were left as they were.`);
|
|
77130
|
+
}
|
|
77131
|
+
if (manifest.reportText)
|
|
77132
|
+
lines2.push("", "## The report before the move", "", "```", manifest.reportText, "```");
|
|
77133
|
+
return `${lines2.join(`
|
|
77134
|
+
`)}
|
|
77135
|
+
`;
|
|
77136
|
+
}
|
|
77137
|
+
async function moveIn(input) {
|
|
77138
|
+
const { pack: pack2, env, open: open4, replace } = input;
|
|
77139
|
+
const log6 = input.log ?? (() => {});
|
|
77140
|
+
const reposDir = reposDirOf(env, input.reposDir);
|
|
77141
|
+
const when3 = stamp();
|
|
77142
|
+
const staging = `${env.dataDir}.incoming-${when3}`;
|
|
77143
|
+
const stagedRepos = [];
|
|
77144
|
+
let keepStaging = false;
|
|
77145
|
+
try {
|
|
77146
|
+
if (!existsSync44(pack2))
|
|
77147
|
+
throw new Error(`no pack at ${pack2}`);
|
|
77148
|
+
const free = freeBytes(dirname25(env.dataDir));
|
|
77149
|
+
const unpackBytes = statSync16(pack2).size * 3;
|
|
77150
|
+
if (free !== null && free < unpackBytes) {
|
|
77151
|
+
throw new Error(`not enough disk to unpack: about ${formatBytes(unpackBytes)} needed, ${formatBytes(free)} free`);
|
|
77152
|
+
}
|
|
77153
|
+
mkdirSync28(staging, { recursive: true });
|
|
77154
|
+
log6("Unpacking");
|
|
77155
|
+
extract(pack2, staging);
|
|
77156
|
+
const manifest = JSON.parse(readFileSync30(join52(staging, "manifest.json"), "utf8"));
|
|
77157
|
+
if (manifest.formatVersion !== 1)
|
|
77158
|
+
throw new Error("this pack was made by a newer runcastle");
|
|
77159
|
+
const question = {
|
|
77160
|
+
runcastleVersion: manifest.runcastleVersion,
|
|
77161
|
+
sourcePlatform: manifest.source.platform,
|
|
77162
|
+
migrations: manifest.migrations,
|
|
77163
|
+
targetFolders: manifest.projects.filter((p) => p.repoMoves).map((p) => p.targetFolder),
|
|
77164
|
+
dataBytes: 0,
|
|
77165
|
+
repoBytes: manifest.files.filter((f) => f.path.startsWith("repos/")).reduce((total, f) => total + f.size, 0)
|
|
77166
|
+
};
|
|
77167
|
+
const answer = await checkDestination(question, env, { replace, reposDir, open: open4 });
|
|
77168
|
+
if (answer.blockers.length > 0)
|
|
77169
|
+
throw new Error(answer.blockers.join(`
|
|
77170
|
+
`));
|
|
77171
|
+
log6("Checking every file");
|
|
77172
|
+
await verifyFiles(staging, manifest);
|
|
77173
|
+
verifyConversations(staging, manifest);
|
|
77174
|
+
const rules = pathRules(manifest, env, reposDir);
|
|
77175
|
+
const renames = claudeRenames(manifest, rules);
|
|
77176
|
+
log6("Rewriting paths in the database");
|
|
77177
|
+
const sql5 = open4(join52(staging, "db", "runcastle.db"), { readonly: false });
|
|
77178
|
+
let rewritten;
|
|
77179
|
+
try {
|
|
77180
|
+
verifyRowCounts(sql5, manifest);
|
|
77181
|
+
rewritten = rewriteDatabase(sql5, manifest, rules, renames, env);
|
|
77182
|
+
} finally {
|
|
77183
|
+
sql5.close();
|
|
77184
|
+
}
|
|
77185
|
+
const repoStages = new Map;
|
|
77186
|
+
for (const project3 of manifest.projects) {
|
|
77187
|
+
if (!project3.repoMoves)
|
|
77188
|
+
continue;
|
|
77189
|
+
log6(`Rebuilding ${project3.name}`);
|
|
77190
|
+
const stage = join52(reposDir, `${project3.targetFolder}.incoming-${when3}`);
|
|
77191
|
+
stagedRepos.push(stage);
|
|
77192
|
+
rebuildRepo(project3, staging, stage);
|
|
77193
|
+
verifyRefs(project3, stage);
|
|
77194
|
+
repoStages.set(project3.id, stage);
|
|
77195
|
+
}
|
|
77196
|
+
const data = join52(staging, "data");
|
|
77197
|
+
mkdirSync28(data, { recursive: true });
|
|
77198
|
+
renameSync2(join52(staging, "db", "runcastle.db"), join52(data, "runcastle.db"));
|
|
77199
|
+
mergeSettings(join52(data, "config.json"), join52(env.dataDir, "config.json"));
|
|
77200
|
+
for (const file of KEPT_FILES) {
|
|
77201
|
+
if (existsSync44(join52(env.dataDir, file)))
|
|
77202
|
+
copyFileSync5(join52(env.dataDir, file), join52(data, file));
|
|
77203
|
+
}
|
|
77204
|
+
const stagedSetup = join52(staging, "claude-setup");
|
|
77205
|
+
let chatsKept = false;
|
|
77206
|
+
if (manifest.claudeSetup && existsSync44(stagedSetup)) {
|
|
77207
|
+
restoreScriptBits(stagedSetup);
|
|
77208
|
+
chatsKept = settleClaudeSettings(join52(stagedSetup, "settings.json"), join52(env.claudeDir, "settings.json"));
|
|
77209
|
+
}
|
|
77210
|
+
log6("Swapping into place");
|
|
77211
|
+
const swap2 = new Swap;
|
|
77212
|
+
const setAside = [];
|
|
77213
|
+
const asideOf = (path3) => `${path3}.before-move-${when3}`;
|
|
77214
|
+
try {
|
|
77215
|
+
for (const project3 of manifest.projects) {
|
|
77216
|
+
const stage = repoStages.get(project3.id);
|
|
77217
|
+
if (!stage)
|
|
77218
|
+
continue;
|
|
77219
|
+
const target = join52(reposDir, project3.targetFolder);
|
|
77220
|
+
if (existsSync44(target)) {
|
|
77221
|
+
swap2.rename(target, asideOf(target));
|
|
77222
|
+
setAside.push({ from: target, to: asideOf(target) });
|
|
77223
|
+
}
|
|
77224
|
+
swap2.rename(stage, target);
|
|
77225
|
+
}
|
|
77226
|
+
const claudeProjects = join52(env.claudeDir, "projects");
|
|
77227
|
+
mkdirSync28(claudeProjects, { recursive: true });
|
|
77228
|
+
const asideNames = new Set;
|
|
77229
|
+
for (const folder of manifest.claudeFolders) {
|
|
77230
|
+
const newName = renames.get(folder.name) ?? folder.name;
|
|
77231
|
+
const target = join52(claudeProjects, newName);
|
|
77232
|
+
if (existsSync44(target) && !asideNames.has(newName) && replace) {
|
|
77233
|
+
swap2.rename(target, asideOf(target));
|
|
77234
|
+
setAside.push({ from: target, to: asideOf(target) });
|
|
77235
|
+
}
|
|
77236
|
+
asideNames.add(newName);
|
|
77237
|
+
const staged = join52(staging, "claude", folder.name);
|
|
77238
|
+
if (existsSync44(staged))
|
|
77239
|
+
placeClaudeFolder(staged, target, swap2, when3);
|
|
77240
|
+
}
|
|
77241
|
+
if (manifest.claudeSetup && existsSync44(stagedSetup)) {
|
|
77242
|
+
for (const entry of readdirSync14(stagedSetup)) {
|
|
77243
|
+
const target = join52(env.claudeDir, entry);
|
|
77244
|
+
if (existsSync44(target)) {
|
|
77245
|
+
swap2.rename(target, asideOf(target));
|
|
77246
|
+
setAside.push({ from: target, to: asideOf(target) });
|
|
77247
|
+
}
|
|
77248
|
+
swap2.rename(join52(stagedSetup, entry), target);
|
|
77249
|
+
}
|
|
77250
|
+
}
|
|
77251
|
+
if (existsSync44(env.dataDir)) {
|
|
77252
|
+
swap2.rename(env.dataDir, asideOf(env.dataDir));
|
|
77253
|
+
setAside.push({ from: env.dataDir, to: asideOf(env.dataDir) });
|
|
77254
|
+
}
|
|
77255
|
+
swap2.rename(data, env.dataDir);
|
|
77256
|
+
} catch (err) {
|
|
77257
|
+
swap2.undo();
|
|
77258
|
+
throw err;
|
|
77259
|
+
}
|
|
77260
|
+
log6("Putting uncommitted work back");
|
|
77261
|
+
const wipApplied = [];
|
|
77262
|
+
const wipKept = [];
|
|
77263
|
+
const wipStuck = [];
|
|
77264
|
+
for (const project3 of manifest.projects) {
|
|
77265
|
+
if (!project3.repoMoves)
|
|
77266
|
+
continue;
|
|
77267
|
+
const repoPath = join52(reposDir, project3.targetFolder);
|
|
77268
|
+
for (const wip of project3.wip) {
|
|
77269
|
+
const checkout = wip.key === "main" ? repoPath : join52(env.dataDir, "worktrees", project3.id, wip.key);
|
|
77270
|
+
let kept;
|
|
77271
|
+
try {
|
|
77272
|
+
if (wip.key !== "main")
|
|
77273
|
+
addTalkWorktree(repoPath, checkout, wip);
|
|
77274
|
+
kept = applyWip(project3, wip, repoPath, checkout, staging);
|
|
77275
|
+
} catch {
|
|
77276
|
+
try {
|
|
77277
|
+
kept = join52(`${repoPath}.uncommitted`, wip.key);
|
|
77278
|
+
cpSync3(join52(staging, "wip", project3.id, wip.key), kept, { recursive: true });
|
|
77279
|
+
} catch {
|
|
77280
|
+
keepStaging = true;
|
|
77281
|
+
wipStuck.push(`${project3.name} (${wip.key})`);
|
|
77282
|
+
continue;
|
|
77283
|
+
}
|
|
77284
|
+
}
|
|
77285
|
+
if (kept)
|
|
77286
|
+
wipKept.push({ project: project3.name, key: wip.key, folder: kept });
|
|
77287
|
+
else
|
|
77288
|
+
wipApplied.push(`${project3.name} (${wip.key})`);
|
|
77289
|
+
}
|
|
77290
|
+
}
|
|
77291
|
+
let claudeSetup;
|
|
77292
|
+
if (manifest.claudeSetup) {
|
|
77293
|
+
const failed = [];
|
|
77294
|
+
if (!addMcpServers(env, manifest.claudeSetup.mcpServers, asideOf(claudeJsonPath(env)))) {
|
|
77295
|
+
failed.push(`MCP servers: could not read ${claudeJsonPath(env)}`);
|
|
77296
|
+
}
|
|
77297
|
+
log6("Installing your Claude plugins again");
|
|
77298
|
+
failed.push(...installPlugins(manifest.claudeSetup, input.runClaude ?? claudeOn(env)));
|
|
77299
|
+
claudeSetup = { failed, chatsKept };
|
|
77300
|
+
}
|
|
77301
|
+
const partial = {
|
|
77302
|
+
repos: manifest.projects.filter((p) => p.repoMoves).map((p) => ({ name: p.name, path: join52(reposDir, p.targetFolder) })),
|
|
77303
|
+
commandsChanged: rewritten.commands.changed,
|
|
77304
|
+
commandsWindows: rewritten.commands.windows,
|
|
77305
|
+
envCarried: manifest.envCarried,
|
|
77306
|
+
...claudeSetup ? { claudeSetup } : {},
|
|
77307
|
+
wipApplied,
|
|
77308
|
+
wipKept,
|
|
77309
|
+
wipStuck,
|
|
77310
|
+
...keepStaging ? { stagingKept: staging } : {},
|
|
77311
|
+
unmappedPaths: rewritten.unmapped,
|
|
77312
|
+
setAside
|
|
77313
|
+
};
|
|
77314
|
+
const reportPath = join52(env.dataDir, "move-report.md");
|
|
77315
|
+
writeFileSync22(reportPath, renderReport(manifest, partial));
|
|
77316
|
+
if (!keepStaging)
|
|
77317
|
+
rmSync12(pack2, { force: true });
|
|
77318
|
+
return { ...partial, reportPath };
|
|
77319
|
+
} finally {
|
|
77320
|
+
if (!keepStaging)
|
|
77321
|
+
rmSync12(staging, { recursive: true, force: true });
|
|
77322
|
+
for (const stage of stagedRepos)
|
|
77323
|
+
rmSync12(stage, { recursive: true, force: true });
|
|
77324
|
+
}
|
|
77325
|
+
}
|
|
77326
|
+
var MACHINE_SETTINGS, KEPT_FILES, CHAT_KEEP_DAYS = 3650;
|
|
77327
|
+
var init_unpack = __esm(() => {
|
|
77328
|
+
init_destination();
|
|
77329
|
+
init_pack();
|
|
77330
|
+
init_scan();
|
|
77331
|
+
init_shell();
|
|
77332
|
+
init_tar();
|
|
77333
|
+
MACHINE_SETTINGS = ["burnConcurrency", "burnCpus"];
|
|
77334
|
+
KEPT_FILES = [".env", "install-id"];
|
|
77335
|
+
});
|
|
77336
|
+
|
|
77337
|
+
// src/move/cli.ts
|
|
77338
|
+
var exports_cli2 = {};
|
|
77339
|
+
__export(exports_cli2, {
|
|
77340
|
+
runMoveInCli: () => runMoveInCli,
|
|
77341
|
+
runMoveCli: () => runMoveCli,
|
|
77342
|
+
parseMoveInArgs: () => parseMoveInArgs,
|
|
77343
|
+
parseMoveArgs: () => parseMoveArgs,
|
|
77344
|
+
currentEnv: () => currentEnv
|
|
77345
|
+
});
|
|
77346
|
+
import { rmSync as rmSync13 } from "fs";
|
|
77347
|
+
import { homedir as homedir7, tmpdir as tmpdir7 } from "os";
|
|
77348
|
+
import { dirname as dirname26, join as join53 } from "path";
|
|
77349
|
+
import { createInterface as createInterface6 } from "readline/promises";
|
|
77350
|
+
function takeValue(args2, i, flag) {
|
|
77351
|
+
const value = args2[i + 1];
|
|
77352
|
+
if (value === undefined || value.startsWith("--"))
|
|
77353
|
+
throw new Error(`${flag} needs a value`);
|
|
77354
|
+
return value;
|
|
77355
|
+
}
|
|
77356
|
+
function parseMoveArgs(args2) {
|
|
77357
|
+
const parsed = {
|
|
77358
|
+
dryRun: false,
|
|
77359
|
+
yes: false,
|
|
77360
|
+
replace: false,
|
|
77361
|
+
noRepo: [],
|
|
77362
|
+
withEnv: false,
|
|
77363
|
+
withClaudeSetup: false,
|
|
77364
|
+
also: []
|
|
77365
|
+
};
|
|
77366
|
+
for (let i = 0;i < args2.length; i++) {
|
|
77367
|
+
const arg = args2[i];
|
|
77368
|
+
if (arg === "--dry-run")
|
|
77369
|
+
parsed.dryRun = true;
|
|
77370
|
+
else if (arg === "--yes")
|
|
77371
|
+
parsed.yes = true;
|
|
77372
|
+
else if (arg === "--replace")
|
|
77373
|
+
parsed.replace = true;
|
|
77374
|
+
else if (arg === "--with-env")
|
|
77375
|
+
parsed.withEnv = true;
|
|
77376
|
+
else if (arg === "--with-claude-setup")
|
|
77377
|
+
parsed.withClaudeSetup = true;
|
|
77378
|
+
else if (arg === "--also")
|
|
77379
|
+
parsed.also.push(takeValue(args2, i++, arg));
|
|
77380
|
+
else if (arg === "--repos-dir")
|
|
77381
|
+
parsed.reposDir = takeValue(args2, i++, arg);
|
|
77382
|
+
else if (arg === "--no-repo")
|
|
77383
|
+
parsed.noRepo.push(takeValue(args2, i++, arg));
|
|
77384
|
+
else if (arg === "--out")
|
|
77385
|
+
parsed.out = takeValue(args2, i++, arg);
|
|
77386
|
+
else if (arg.startsWith("--"))
|
|
77387
|
+
throw new Error(`unknown flag ${arg}`);
|
|
77388
|
+
else if (parsed.host)
|
|
77389
|
+
throw new Error(`unexpected argument ${arg}`);
|
|
77390
|
+
else
|
|
77391
|
+
parsed.host = arg;
|
|
77392
|
+
}
|
|
77393
|
+
return parsed;
|
|
77394
|
+
}
|
|
77395
|
+
function parseMoveInArgs(args2) {
|
|
77396
|
+
const parsed = { check: false, replace: false };
|
|
77397
|
+
for (let i = 0;i < args2.length; i++) {
|
|
77398
|
+
const arg = args2[i];
|
|
77399
|
+
if (arg === "--check")
|
|
77400
|
+
parsed.check = true;
|
|
77401
|
+
else if (arg === "--replace")
|
|
77402
|
+
parsed.replace = true;
|
|
77403
|
+
else if (arg === "--repos-dir")
|
|
77404
|
+
parsed.reposDir = takeValue(args2, i++, arg);
|
|
77405
|
+
else if (arg.startsWith("--"))
|
|
77406
|
+
throw new Error(`unknown flag ${arg}`);
|
|
77407
|
+
else if (parsed.pack)
|
|
77408
|
+
throw new Error(`unexpected argument ${arg}`);
|
|
77409
|
+
else
|
|
77410
|
+
parsed.pack = arg;
|
|
77411
|
+
}
|
|
77412
|
+
return parsed;
|
|
77413
|
+
}
|
|
77414
|
+
function currentEnv() {
|
|
77415
|
+
return {
|
|
77416
|
+
platform: process.platform,
|
|
77417
|
+
home: homedir7(),
|
|
77418
|
+
dataDir: dataDir(),
|
|
77419
|
+
claudeDir: process.env.CLAUDE_CONFIG_DIR ?? join53(homedir7(), ".claude")
|
|
77420
|
+
};
|
|
77421
|
+
}
|
|
77422
|
+
async function openSql() {
|
|
77423
|
+
const { openRawSqlite: openRawSqlite2 } = await Promise.resolve().then(() => (init_client(), exports_client));
|
|
77424
|
+
return openRawSqlite2;
|
|
77425
|
+
}
|
|
77426
|
+
function questionFor(scan, migrations, env) {
|
|
77427
|
+
return {
|
|
77428
|
+
runcastleVersion: runcastleVersion(),
|
|
77429
|
+
sourcePlatform: env.platform,
|
|
77430
|
+
migrations,
|
|
77431
|
+
targetFolders: scan.projects.filter((p) => p.repoMoves).map((p) => p.targetFolder),
|
|
77432
|
+
dataBytes: scan.report.dataBytes,
|
|
77433
|
+
repoBytes: scan.report.repoBytes
|
|
77434
|
+
};
|
|
77435
|
+
}
|
|
77436
|
+
async function confirmed() {
|
|
77437
|
+
if (!process.stdin.isTTY) {
|
|
77438
|
+
console.error("Run with --yes to go ahead without a terminal to answer in.");
|
|
77439
|
+
return false;
|
|
77440
|
+
}
|
|
77441
|
+
const prompt = createInterface6({ input: process.stdin, output: process.stdout });
|
|
77442
|
+
const answer = await prompt.question(`
|
|
77443
|
+
Go ahead? (y/N) `);
|
|
77444
|
+
prompt.close();
|
|
77445
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
77446
|
+
}
|
|
77447
|
+
async function runMoveCli(argv) {
|
|
77448
|
+
const args2 = parseMoveArgs(argv);
|
|
77449
|
+
const env = currentEnv();
|
|
77450
|
+
const open4 = await openSql();
|
|
77451
|
+
const flags = { replace: args2.replace, ...args2.reposDir ? { reposDir: args2.reposDir } : {} };
|
|
77452
|
+
let sql5;
|
|
77453
|
+
try {
|
|
77454
|
+
sql5 = open4(join53(env.dataDir, "runcastle.db"), { readonly: true });
|
|
77455
|
+
} catch {
|
|
77456
|
+
console.error(`No runcastle database at ${env.dataDir}.`);
|
|
77457
|
+
return 1;
|
|
77458
|
+
}
|
|
77459
|
+
let scan;
|
|
77460
|
+
let migrations;
|
|
77461
|
+
let windowsCommands;
|
|
77462
|
+
try {
|
|
77463
|
+
scan = scanMove(env, sql5, {
|
|
77464
|
+
noRepo: args2.noRepo,
|
|
77465
|
+
withEnv: args2.withEnv,
|
|
77466
|
+
withClaudeSetup: args2.withClaudeSetup,
|
|
77467
|
+
also: args2.also
|
|
77468
|
+
});
|
|
77469
|
+
migrations = sql5.all("SELECT name FROM __migrations").map((r) => r.name);
|
|
77470
|
+
windowsCommands = windowsCommandLines(sql5, scan.projects, env);
|
|
77471
|
+
} finally {
|
|
77472
|
+
sql5.close();
|
|
77473
|
+
}
|
|
77474
|
+
const running3 = await serverRunning(env);
|
|
77475
|
+
if (running3)
|
|
77476
|
+
scan.report.blockers.unshift("this machine's runcastle server is running: stop it for a real move");
|
|
77477
|
+
const destination = args2.host ? askDestination(args2.host, questionFor(scan, migrations, env), flags) : undefined;
|
|
77478
|
+
const pack2 = args2.out ?? join53(tmpdir7(), `runcastle-move-${stamp()}.tar.gz`);
|
|
77479
|
+
const room = freeBytes(dirname26(pack2));
|
|
77480
|
+
const packMax = scan.report.dataBytes + scan.report.repoBytes;
|
|
77481
|
+
if (room !== null && room < packMax) {
|
|
77482
|
+
scan.report.blockers.push(`not enough disk beside the pack: up to ${formatBytes(packMax)} needed, ${formatBytes(room)} free. Choose another place with --out`);
|
|
77483
|
+
}
|
|
77484
|
+
const reportText = formatReport2(scan.report, destination, windowsCommands);
|
|
77485
|
+
console.log(reportText);
|
|
77486
|
+
if (args2.dryRun) {
|
|
77487
|
+
console.log(`
|
|
77488
|
+
Nothing was changed.`);
|
|
77489
|
+
return 0;
|
|
77490
|
+
}
|
|
77491
|
+
if (isBlocked(scan.report, destination)) {
|
|
77492
|
+
console.error(`
|
|
77493
|
+
Nothing was changed. Clear the blocks above, then run this again.`);
|
|
77494
|
+
return 1;
|
|
77495
|
+
}
|
|
77496
|
+
if (!args2.yes && !await confirmed()) {
|
|
77497
|
+
console.log("Nothing was changed.");
|
|
77498
|
+
return 0;
|
|
77499
|
+
}
|
|
77500
|
+
await packMove({ env, scan, open: open4, out: pack2, reportText, log: (line2) => console.log(line2) });
|
|
77501
|
+
console.log(`
|
|
77502
|
+
Packed: ${pack2}`);
|
|
77503
|
+
if (!args2.host) {
|
|
77504
|
+
console.log(`
|
|
77505
|
+
Finish on the new machine with:`);
|
|
77506
|
+
for (const line2 of manualSteps(pack2, undefined, flags))
|
|
77507
|
+
console.log(` ${line2}`);
|
|
77508
|
+
return 0;
|
|
77509
|
+
}
|
|
77510
|
+
const copy4 = copyPack(pack2, args2.host);
|
|
77511
|
+
if (!copy4.ok) {
|
|
77512
|
+
console.error(failureMessage2("The copy", copy4.detail, pack2, args2.host, flags));
|
|
77513
|
+
return 1;
|
|
77514
|
+
}
|
|
77515
|
+
const remote = runRemoteMoveIn(pack2, args2.host, flags);
|
|
77516
|
+
if (!remote.ok) {
|
|
77517
|
+
console.error(failureMessage2("The move on the other machine", remote.detail, pack2, args2.host, flags));
|
|
77518
|
+
return 1;
|
|
77519
|
+
}
|
|
77520
|
+
rmSync13(pack2, { force: true });
|
|
77521
|
+
console.log(`
|
|
77522
|
+
The move is finished. This machine is unchanged and must not be used for work now.`);
|
|
77523
|
+
return 0;
|
|
77524
|
+
}
|
|
77525
|
+
async function readStdin() {
|
|
77526
|
+
let text3 = "";
|
|
77527
|
+
for await (const chunk of process.stdin)
|
|
77528
|
+
text3 += String(chunk);
|
|
77529
|
+
return text3;
|
|
77530
|
+
}
|
|
77531
|
+
async function runMoveInCli(argv) {
|
|
77532
|
+
const args2 = parseMoveInArgs(argv);
|
|
77533
|
+
const env = currentEnv();
|
|
77534
|
+
const open4 = await openSql();
|
|
77535
|
+
if (args2.check) {
|
|
77536
|
+
const question = JSON.parse(await readStdin());
|
|
77537
|
+
const answer = await checkDestination(question, env, {
|
|
77538
|
+
replace: args2.replace,
|
|
77539
|
+
open: open4,
|
|
77540
|
+
...args2.reposDir ? { reposDir: args2.reposDir } : {}
|
|
77541
|
+
});
|
|
77542
|
+
console.log(JSON.stringify(answer));
|
|
77543
|
+
return 0;
|
|
77544
|
+
}
|
|
77545
|
+
if (!args2.pack) {
|
|
77546
|
+
console.error("Usage: runcastle move-in <pack> [--replace] [--repos-dir <dir>]");
|
|
77547
|
+
return 1;
|
|
77548
|
+
}
|
|
77549
|
+
const result = await moveIn({
|
|
77550
|
+
pack: args2.pack,
|
|
77551
|
+
env,
|
|
77552
|
+
open: open4,
|
|
77553
|
+
replace: args2.replace,
|
|
77554
|
+
log: (line2) => console.log(line2),
|
|
77555
|
+
...args2.reposDir ? { reposDir: args2.reposDir } : {}
|
|
77556
|
+
});
|
|
77557
|
+
console.log(`
|
|
77558
|
+
${formatMoveInResult(result)}`);
|
|
77559
|
+
return 0;
|
|
77560
|
+
}
|
|
77561
|
+
var init_cli2 = __esm(() => {
|
|
77562
|
+
init_paths();
|
|
77563
|
+
init_version();
|
|
77564
|
+
init_destination();
|
|
77565
|
+
init_pack();
|
|
77566
|
+
init_remote();
|
|
77567
|
+
init_report2();
|
|
77568
|
+
init_scan();
|
|
77569
|
+
init_shell();
|
|
77570
|
+
init_unpack();
|
|
77571
|
+
});
|
|
77572
|
+
|
|
75443
77573
|
// src/bin/token.ts
|
|
75444
77574
|
var exports_token = {};
|
|
75445
77575
|
__export(exports_token, {
|
|
@@ -75461,7 +77591,7 @@ async function readAllStdin() {
|
|
|
75461
77591
|
return Buffer.concat(chunks).toString("utf8");
|
|
75462
77592
|
}
|
|
75463
77593
|
function readHiddenFromTerminal() {
|
|
75464
|
-
return new Promise((
|
|
77594
|
+
return new Promise((resolve18, reject) => {
|
|
75465
77595
|
const stdin3 = process.stdin;
|
|
75466
77596
|
let value = "";
|
|
75467
77597
|
let timer;
|
|
@@ -75487,7 +77617,7 @@ function readHiddenFromTerminal() {
|
|
|
75487
77617
|
value += ch;
|
|
75488
77618
|
}
|
|
75489
77619
|
if (/[\r\n]/.test(value))
|
|
75490
|
-
timer = setTimeout(() => finish(() =>
|
|
77620
|
+
timer = setTimeout(() => finish(() => resolve18(value)), PASTE_QUIET_MS);
|
|
75491
77621
|
};
|
|
75492
77622
|
stdin3.setRawMode(true);
|
|
75493
77623
|
stdin3.resume();
|
|
@@ -75559,14 +77689,14 @@ __export(exports_service, {
|
|
|
75559
77689
|
runService: () => runService,
|
|
75560
77690
|
SERVICE_USAGE: () => SERVICE_USAGE
|
|
75561
77691
|
});
|
|
75562
|
-
import { spawnSync as
|
|
75563
|
-
import { existsSync as
|
|
75564
|
-
import { homedir as
|
|
75565
|
-
import { join as
|
|
77692
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
77693
|
+
import { existsSync as existsSync45, mkdtempSync as mkdtempSync3, rmSync as rmSync14, writeFileSync as writeFileSync23 } from "fs";
|
|
77694
|
+
import { homedir as homedir8, hostname, tmpdir as tmpdir8, userInfo as userInfo3 } from "os";
|
|
77695
|
+
import { join as join54 } from "path";
|
|
75566
77696
|
function writeTempFile(text3) {
|
|
75567
|
-
const dir =
|
|
75568
|
-
const file =
|
|
75569
|
-
|
|
77697
|
+
const dir = mkdtempSync3(join54(tmpdir8(), "runcastle-service-"));
|
|
77698
|
+
const file = join54(dir, "runcastle.service");
|
|
77699
|
+
writeFileSync23(file, text3, "utf8");
|
|
75570
77700
|
return file;
|
|
75571
77701
|
}
|
|
75572
77702
|
function runSteps(steps, run7, log6) {
|
|
@@ -75586,7 +77716,7 @@ async function runService(argv, deps2 = {}) {
|
|
|
75586
77716
|
return 1;
|
|
75587
77717
|
}
|
|
75588
77718
|
const platform = deps2.platform ?? process.platform;
|
|
75589
|
-
const systemdPresent = deps2.systemdPresent ??
|
|
77719
|
+
const systemdPresent = deps2.systemdPresent ?? existsSync45("/run/systemd/system");
|
|
75590
77720
|
if (platform !== "linux" || !systemdPresent) {
|
|
75591
77721
|
log6("A background service is not supported here: it needs Linux with systemd.");
|
|
75592
77722
|
return 1;
|
|
@@ -75596,7 +77726,7 @@ async function runService(argv, deps2 = {}) {
|
|
|
75596
77726
|
return 1;
|
|
75597
77727
|
}
|
|
75598
77728
|
const run7 = deps2.runStep ?? inheritStdio;
|
|
75599
|
-
const existed = deps2.serviceFileExists ??
|
|
77729
|
+
const existed = deps2.serviceFileExists ?? existsSync45(SERVICE_FILE_PATH);
|
|
75600
77730
|
if (action === "remove") {
|
|
75601
77731
|
if (!existed) {
|
|
75602
77732
|
log6("No runcastle service is installed. Nothing to remove.");
|
|
@@ -75612,7 +77742,7 @@ async function runService(argv, deps2 = {}) {
|
|
|
75612
77742
|
return ok3 ? 0 : 1;
|
|
75613
77743
|
}
|
|
75614
77744
|
const user = deps2.user ?? userInfo3().username;
|
|
75615
|
-
const home = deps2.home ??
|
|
77745
|
+
const home = deps2.home ?? homedir8();
|
|
75616
77746
|
const text3 = renderServiceFile({
|
|
75617
77747
|
user,
|
|
75618
77748
|
home,
|
|
@@ -75631,14 +77761,14 @@ async function runService(argv, deps2 = {}) {
|
|
|
75631
77761
|
return 1;
|
|
75632
77762
|
} finally {
|
|
75633
77763
|
if (!deps2.writeTemp)
|
|
75634
|
-
|
|
77764
|
+
rmSync14(join54(tmp, ".."), { recursive: true, force: true });
|
|
75635
77765
|
}
|
|
75636
77766
|
log6(existed ? "Replaced the existing runcastle service." : "Installed the runcastle service.");
|
|
75637
77767
|
log6("Check it with: systemctl status runcastle");
|
|
75638
77768
|
log6(`Reach the app from your own machine with: ssh -L 4512:localhost:4512 ${user}@${deps2.host ?? hostname()}`);
|
|
75639
77769
|
return 0;
|
|
75640
77770
|
}
|
|
75641
|
-
var SERVICE_USAGE, inheritStdio = (command, args2) =>
|
|
77771
|
+
var SERVICE_USAGE, inheritStdio = (command, args2) => spawnSync5(command, args2, { stdio: "inherit" }).status ?? 1;
|
|
75642
77772
|
var init_service = __esm(() => {
|
|
75643
77773
|
init_service_file();
|
|
75644
77774
|
init_resolve_executable();
|
|
@@ -75693,7 +77823,7 @@ var init_build_image = __esm(() => {
|
|
|
75693
77823
|
// src/bin/runcastle.ts
|
|
75694
77824
|
init_asset_paths();
|
|
75695
77825
|
init_version();
|
|
75696
|
-
import { dirname as
|
|
77826
|
+
import { dirname as dirname27, resolve as resolve18 } from "path";
|
|
75697
77827
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
75698
77828
|
function parseCommand(argv) {
|
|
75699
77829
|
const [first, ...rest] = argv;
|
|
@@ -75707,6 +77837,10 @@ function parseCommand(argv) {
|
|
|
75707
77837
|
return { command: "setup", args: rest };
|
|
75708
77838
|
if (first === "doctor")
|
|
75709
77839
|
return { command: "doctor", args: rest };
|
|
77840
|
+
if (first === "move")
|
|
77841
|
+
return { command: "move", args: rest };
|
|
77842
|
+
if (first === "move-in")
|
|
77843
|
+
return { command: "move-in", args: rest };
|
|
75710
77844
|
if (first === "token")
|
|
75711
77845
|
return { command: "token", args: rest };
|
|
75712
77846
|
if (first === "service")
|
|
@@ -75724,6 +77858,12 @@ Usage:
|
|
|
75724
77858
|
runcastle serve The same, spelled out for scripts and services
|
|
75725
77859
|
runcastle setup List what this machine is missing and install it (--yes, --agent claude|codex|both)
|
|
75726
77860
|
runcastle doctor Check prerequisites (add --gate for the pre-boot gate)
|
|
77861
|
+
runcastle move [user@host] [--dry-run] [--yes] [--replace] [--with-env]
|
|
77862
|
+
[--with-claude-setup] [--also <project>:<path>]
|
|
77863
|
+
[--repos-dir <dir>] [--no-repo <project>] [--out <file>]
|
|
77864
|
+
Move runcastle to another machine; only reads this one
|
|
77865
|
+
runcastle move-in <pack> [--replace] [--repos-dir <dir>]
|
|
77866
|
+
Bring a move in on this machine
|
|
75727
77867
|
runcastle token Save the unattended Claude Code token (--stdin to read it from a pipe)
|
|
75728
77868
|
runcastle service install
|
|
75729
77869
|
Keep runcastle running on this Linux machine (service remove undoes it)
|
|
@@ -75733,7 +77873,7 @@ Usage:
|
|
|
75733
77873
|
runcastle --help Show this help
|
|
75734
77874
|
`;
|
|
75735
77875
|
async function main(argv) {
|
|
75736
|
-
applyInstalledAssetEnv(
|
|
77876
|
+
applyInstalledAssetEnv(resolve18(dirname27(fileURLToPath7(import.meta.url)), ".."));
|
|
75737
77877
|
const { command, args: args2 } = parseCommand(argv);
|
|
75738
77878
|
switch (command) {
|
|
75739
77879
|
case "version":
|
|
@@ -75750,6 +77890,14 @@ async function main(argv) {
|
|
|
75750
77890
|
const { runCli: runCli2 } = await Promise.resolve().then(() => (init_cli(), exports_cli));
|
|
75751
77891
|
return runCli2(args2);
|
|
75752
77892
|
}
|
|
77893
|
+
case "move": {
|
|
77894
|
+
const { runMoveCli: runMoveCli2 } = await Promise.resolve().then(() => (init_cli2(), exports_cli2));
|
|
77895
|
+
return runMoveCli2(args2);
|
|
77896
|
+
}
|
|
77897
|
+
case "move-in": {
|
|
77898
|
+
const { runMoveInCli: runMoveInCli2 } = await Promise.resolve().then(() => (init_cli2(), exports_cli2));
|
|
77899
|
+
return runMoveInCli2(args2);
|
|
77900
|
+
}
|
|
75753
77901
|
case "token": {
|
|
75754
77902
|
const { runToken: runToken2 } = await Promise.resolve().then(() => (init_token(), exports_token));
|
|
75755
77903
|
return runToken2(args2);
|