@pushpalsdev/cli 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pushpals-cli.js +693 -70
- package/package.json +1 -1
package/dist/pushpals-cli.js
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
mkdirSync,
|
|
12
12
|
readdirSync,
|
|
13
13
|
readFileSync as readFileSync4,
|
|
14
|
+
rmSync,
|
|
14
15
|
writeFileSync
|
|
15
16
|
} from "fs";
|
|
16
|
-
import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4 } from "path";
|
|
17
|
+
import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4, win32 as pathWin32 } from "path";
|
|
17
18
|
import { createInterface } from "readline";
|
|
18
19
|
|
|
19
20
|
// ../shared/src/client_preflight.ts
|
|
@@ -24,6 +25,41 @@ import { relative, resolve as resolve2 } from "path";
|
|
|
24
25
|
import { existsSync, readFileSync } from "fs";
|
|
25
26
|
import { join, resolve, isAbsolute } from "path";
|
|
26
27
|
|
|
28
|
+
// ../shared/src/autonomy_policy.ts
|
|
29
|
+
var DRIVE_RE = /^[A-Za-z]:\//;
|
|
30
|
+
var SLASH_RE = /\/+/g;
|
|
31
|
+
function normalizeAutonomyComponentArea(value) {
|
|
32
|
+
const normalized = normalizeRepoRelativePath(value);
|
|
33
|
+
if (!normalized)
|
|
34
|
+
return null;
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
function normalizeRepoRelativePath(value) {
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
let path = value.trim();
|
|
41
|
+
if (!path)
|
|
42
|
+
return null;
|
|
43
|
+
path = path.normalize("NFC").replace(/\\/g, "/");
|
|
44
|
+
if (path.startsWith("/"))
|
|
45
|
+
return null;
|
|
46
|
+
if (DRIVE_RE.test(path))
|
|
47
|
+
return null;
|
|
48
|
+
path = path.replace(SLASH_RE, "/");
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const rawSegment of path.split("/")) {
|
|
51
|
+
const segment = rawSegment.trim();
|
|
52
|
+
if (!segment || segment === ".")
|
|
53
|
+
continue;
|
|
54
|
+
if (segment === "..")
|
|
55
|
+
return null;
|
|
56
|
+
out.push(segment);
|
|
57
|
+
}
|
|
58
|
+
if (out.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return out.join("/");
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
// ../shared/src/local_network.ts
|
|
28
64
|
var DEFAULT_LOCAL_LOOPBACK_HOST = "127.0.0.1";
|
|
29
65
|
function isLoopbackHost(hostname) {
|
|
@@ -397,14 +433,26 @@ function loadPushPalsConfig(options = {}) {
|
|
|
397
433
|
"tests/unit": 2
|
|
398
434
|
};
|
|
399
435
|
const remoteAutonomyDispatchByComponentRaw = asStringNumberRecord(remoteAutonomyNode.max_dispatch_per_hour_by_component);
|
|
400
|
-
const
|
|
401
|
-
|
|
436
|
+
const legacyAutonomyComponentAliasMap = new Map(Object.keys(remoteAutonomyDispatchByComponentCfg).flatMap((key) => {
|
|
437
|
+
const direct = normalizeAutonomyComponentArea(key);
|
|
438
|
+
const legacyUnderscore = normalizeAutonomyComponentArea(key.replace(/\//g, "_"));
|
|
439
|
+
const legacyHyphen = normalizeAutonomyComponentArea(key.replace(/\//g, "-"));
|
|
440
|
+
return [direct, legacyUnderscore, legacyHyphen].filter((value) => Boolean(value)).map((value) => [value, key]);
|
|
441
|
+
}));
|
|
442
|
+
const coerceAutonomyComponentConfigKey = (value) => {
|
|
443
|
+
const direct = normalizeAutonomyComponentArea(value);
|
|
444
|
+
const legacyAliasCandidate = normalizeAutonomyComponentArea(value.trim().toLowerCase().replace(/\\/g, "/").replace(/_+/g, "/").replace(/-+/g, "/").replace(/\/+/g, "/"));
|
|
445
|
+
if (legacyAliasCandidate && legacyAutonomyComponentAliasMap.has(legacyAliasCandidate)) {
|
|
446
|
+
return legacyAutonomyComponentAliasMap.get(legacyAliasCandidate) ?? legacyAliasCandidate;
|
|
447
|
+
}
|
|
448
|
+
return direct;
|
|
402
449
|
};
|
|
403
|
-
const
|
|
404
|
-
|
|
450
|
+
const remoteAutonomyDispatchByComponent = Object.fromEntries(Object.entries(remoteAutonomyDispatchByComponentCfg).map(([key, value]) => [
|
|
451
|
+
coerceAutonomyComponentConfigKey(key) ?? key,
|
|
452
|
+
value
|
|
453
|
+
]));
|
|
405
454
|
for (const [rawKey, rawValue] of Object.entries(remoteAutonomyDispatchByComponentRaw)) {
|
|
406
|
-
const
|
|
407
|
-
const canonical = canonicalComponentByNormalized.get(normalized);
|
|
455
|
+
const canonical = coerceAutonomyComponentConfigKey(rawKey);
|
|
408
456
|
if (!canonical)
|
|
409
457
|
continue;
|
|
410
458
|
const parsed = typeof rawValue === "number" ? rawValue : typeof rawValue === "string" ? Number.parseInt(rawValue.trim(), 10) : Number.NaN;
|
|
@@ -1257,6 +1305,7 @@ function printUsage() {
|
|
|
1257
1305
|
console.log(" --no-auto-start Disable runtime auto-start when the server is down");
|
|
1258
1306
|
console.log(" --no-stream Disable live session event stream");
|
|
1259
1307
|
console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
|
|
1308
|
+
console.log(" --clear Remove repo-local PushPals state and exit");
|
|
1260
1309
|
console.log(" -h, --help Show this help");
|
|
1261
1310
|
console.log("");
|
|
1262
1311
|
console.log("Chat commands:");
|
|
@@ -1271,7 +1320,12 @@ function printUsage() {
|
|
|
1271
1320
|
console.log(" - Interactive CLI talks directly to server sessions; LocalBuddy is optional.");
|
|
1272
1321
|
}
|
|
1273
1322
|
function parseArgs(argv) {
|
|
1274
|
-
const options = {
|
|
1323
|
+
const options = {
|
|
1324
|
+
noAutoStart: false,
|
|
1325
|
+
noStream: false,
|
|
1326
|
+
runtimeOnly: false,
|
|
1327
|
+
clear: false
|
|
1328
|
+
};
|
|
1275
1329
|
for (let i = 0;i < argv.length; i++) {
|
|
1276
1330
|
const arg = argv[i];
|
|
1277
1331
|
if (arg === "-h" || arg === "--help") {
|
|
@@ -1290,6 +1344,10 @@ function parseArgs(argv) {
|
|
|
1290
1344
|
options.runtimeOnly = true;
|
|
1291
1345
|
continue;
|
|
1292
1346
|
}
|
|
1347
|
+
if (arg === "--clear") {
|
|
1348
|
+
options.clear = true;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1293
1351
|
if (arg === "--server-url") {
|
|
1294
1352
|
options.serverUrl = argv[++i];
|
|
1295
1353
|
continue;
|
|
@@ -1358,23 +1416,38 @@ function parsePositiveInt(value, fallback) {
|
|
|
1358
1416
|
function jsonHtmlBootstrap(value) {
|
|
1359
1417
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1360
1418
|
}
|
|
1419
|
+
async function runCommandWithEnv(command, cwd, env) {
|
|
1420
|
+
try {
|
|
1421
|
+
const proc = Bun.spawn(command, {
|
|
1422
|
+
cwd,
|
|
1423
|
+
env,
|
|
1424
|
+
stdout: "pipe",
|
|
1425
|
+
stderr: "pipe"
|
|
1426
|
+
});
|
|
1427
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1428
|
+
new Response(proc.stdout).text(),
|
|
1429
|
+
new Response(proc.stderr).text(),
|
|
1430
|
+
proc.exited
|
|
1431
|
+
]);
|
|
1432
|
+
return { ok: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
return {
|
|
1435
|
+
ok: false,
|
|
1436
|
+
stdout: "",
|
|
1437
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
1438
|
+
exitCode: -1
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
async function runGitWithEnv(args, cwd, env) {
|
|
1443
|
+
return await runCommandWithEnv(["git", ...args], cwd, env);
|
|
1444
|
+
}
|
|
1361
1445
|
async function runGit(args, cwd) {
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
1367
|
-
GCM_INTERACTIVE: "Never"
|
|
1368
|
-
},
|
|
1369
|
-
stdout: "pipe",
|
|
1370
|
-
stderr: "pipe"
|
|
1446
|
+
return await runGitWithEnv(args, cwd, {
|
|
1447
|
+
...process.env,
|
|
1448
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1449
|
+
GCM_INTERACTIVE: "Never"
|
|
1371
1450
|
});
|
|
1372
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1373
|
-
new Response(proc.stdout).text(),
|
|
1374
|
-
new Response(proc.stderr).text(),
|
|
1375
|
-
proc.exited
|
|
1376
|
-
]);
|
|
1377
|
-
return { ok: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
|
|
1378
1451
|
}
|
|
1379
1452
|
async function resolveCurrentGitRepoRoot(cwd) {
|
|
1380
1453
|
const inside = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
@@ -1535,25 +1608,24 @@ async function fetchLatestReleaseTag() {
|
|
|
1535
1608
|
throw new Error("Latest release payload did not include tag_name");
|
|
1536
1609
|
return tagName;
|
|
1537
1610
|
}
|
|
1538
|
-
|
|
1611
|
+
function resolvePreferredRuntimeReleaseTag(explicitTag, env = process.env) {
|
|
1539
1612
|
const fromArg = String(explicitTag ?? "").trim();
|
|
1540
1613
|
if (fromArg)
|
|
1541
1614
|
return fromArg;
|
|
1542
|
-
const fromEnv = String(
|
|
1615
|
+
const fromEnv = String(env.PUSHPALS_RUNTIME_TAG ?? "").trim();
|
|
1543
1616
|
if (fromEnv)
|
|
1544
1617
|
return fromEnv;
|
|
1545
|
-
const packageVersion = parseSemverFromPackageVersion(
|
|
1618
|
+
const packageVersion = parseSemverFromPackageVersion(env.PUSHPALS_CLI_PACKAGE_VERSION);
|
|
1619
|
+
if (packageVersion)
|
|
1620
|
+
return `v${packageVersion}`;
|
|
1621
|
+
return "";
|
|
1622
|
+
}
|
|
1623
|
+
async function resolveRuntimeReleaseTag(explicitTag) {
|
|
1624
|
+
const preferredTag = resolvePreferredRuntimeReleaseTag(explicitTag, process.env);
|
|
1625
|
+
if (preferredTag)
|
|
1626
|
+
return preferredTag;
|
|
1546
1627
|
console.log("[pushpals] Resolving embedded runtime release tag from GitHub...");
|
|
1547
|
-
|
|
1548
|
-
return await fetchLatestReleaseTag();
|
|
1549
|
-
} catch (err) {
|
|
1550
|
-
if (packageVersion) {
|
|
1551
|
-
const fallbackTag = `v${packageVersion}`;
|
|
1552
|
-
console.warn(`[pushpals] Could not resolve latest runtime tag; falling back to package version tag ${fallbackTag}: ${String(err)}`);
|
|
1553
|
-
return fallbackTag;
|
|
1554
|
-
}
|
|
1555
|
-
throw err;
|
|
1556
|
-
}
|
|
1628
|
+
return await fetchLatestReleaseTag();
|
|
1557
1629
|
}
|
|
1558
1630
|
function writeTextFileIfMissing(pathValue, text) {
|
|
1559
1631
|
if (existsSync4(pathValue))
|
|
@@ -1722,7 +1794,9 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
|
|
|
1722
1794
|
PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
|
|
1723
1795
|
...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
|
|
1724
1796
|
...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {},
|
|
1725
|
-
...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {}
|
|
1797
|
+
...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {},
|
|
1798
|
+
...typeof env.PUSHPALS_DOCKER_BIN === "string" && env.PUSHPALS_DOCKER_BIN.trim() ? { PUSHPALS_DOCKER_BIN: env.PUSHPALS_DOCKER_BIN.trim() } : {},
|
|
1799
|
+
...typeof env.PUSHPALS_DOCKER_BIN_ABSOLUTE === "string" && env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() ? { PUSHPALS_DOCKER_BIN_ABSOLUTE: env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() } : {}
|
|
1726
1800
|
};
|
|
1727
1801
|
}
|
|
1728
1802
|
function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
|
|
@@ -1751,10 +1825,7 @@ function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
|
|
|
1751
1825
|
return env;
|
|
1752
1826
|
}
|
|
1753
1827
|
async function resolveCommandPath(command, cwd, env) {
|
|
1754
|
-
const lookupCommands = process.platform === "win32" ? [
|
|
1755
|
-
["where.exe", command],
|
|
1756
|
-
["where", command]
|
|
1757
|
-
] : [["which", command]];
|
|
1828
|
+
const lookupCommands = process.platform === "win32" ? resolveWindowsWhereExecutableCandidatesForEnv(env, process.platform).map((lookup) => [lookup, command]) : [["which", command]];
|
|
1758
1829
|
for (const lookup of lookupCommands) {
|
|
1759
1830
|
try {
|
|
1760
1831
|
const proc = Bun.spawn(lookup, {
|
|
@@ -2001,6 +2072,115 @@ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = p
|
|
|
2001
2072
|
}
|
|
2002
2073
|
return env;
|
|
2003
2074
|
}
|
|
2075
|
+
function applyResolvedDockerBinaryToRuntimeEnv(env, resolvedDockerBinary, platform = process.platform) {
|
|
2076
|
+
const resolvedPath = String(resolvedDockerBinary ?? "").trim();
|
|
2077
|
+
if (!resolvedPath)
|
|
2078
|
+
return env;
|
|
2079
|
+
prependExecutableDirToPath(env, resolvedPath, platform);
|
|
2080
|
+
env.PUSHPALS_DOCKER_BIN = basename(resolvedPath);
|
|
2081
|
+
if (resolvedPath.includes("/") || resolvedPath.includes("\\")) {
|
|
2082
|
+
env.PUSHPALS_DOCKER_BIN_ABSOLUTE = resolvedPath;
|
|
2083
|
+
} else {
|
|
2084
|
+
delete env.PUSHPALS_DOCKER_BIN_ABSOLUTE;
|
|
2085
|
+
}
|
|
2086
|
+
return env;
|
|
2087
|
+
}
|
|
2088
|
+
function resolveRuntimeGitExecutableCandidates(env, platform = process.platform) {
|
|
2089
|
+
const candidates = [];
|
|
2090
|
+
const seen = new Set;
|
|
2091
|
+
const pushCandidate = (value) => {
|
|
2092
|
+
const trimmed = String(value ?? "").trim();
|
|
2093
|
+
if (!trimmed)
|
|
2094
|
+
return;
|
|
2095
|
+
const key = platform === "win32" ? trimmed.toLowerCase() : trimmed;
|
|
2096
|
+
if (seen.has(key))
|
|
2097
|
+
return;
|
|
2098
|
+
seen.add(key);
|
|
2099
|
+
candidates.push(trimmed);
|
|
2100
|
+
};
|
|
2101
|
+
pushCandidate(env.PUSHPALS_GIT_BIN ?? "");
|
|
2102
|
+
pushCandidate(env.PUSHPALS_GIT_BIN_ABSOLUTE ?? "");
|
|
2103
|
+
pushCandidate(platform === "win32" ? "git.exe" : "git");
|
|
2104
|
+
pushCandidate("git");
|
|
2105
|
+
return candidates;
|
|
2106
|
+
}
|
|
2107
|
+
function resolveRuntimeDockerExecutableCandidates(env, platform = process.platform) {
|
|
2108
|
+
const candidates = [];
|
|
2109
|
+
const seen = new Set;
|
|
2110
|
+
const pushCandidate = (value) => {
|
|
2111
|
+
const trimmed = String(value ?? "").trim();
|
|
2112
|
+
if (!trimmed)
|
|
2113
|
+
return;
|
|
2114
|
+
const key = platform === "win32" ? trimmed.toLowerCase() : trimmed;
|
|
2115
|
+
if (seen.has(key))
|
|
2116
|
+
return;
|
|
2117
|
+
seen.add(key);
|
|
2118
|
+
candidates.push(trimmed);
|
|
2119
|
+
};
|
|
2120
|
+
pushCandidate(env.PUSHPALS_DOCKER_BIN ?? "");
|
|
2121
|
+
pushCandidate(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? "");
|
|
2122
|
+
pushCandidate(platform === "win32" ? "docker.exe" : "docker");
|
|
2123
|
+
pushCandidate("docker");
|
|
2124
|
+
return candidates;
|
|
2125
|
+
}
|
|
2126
|
+
function resolveWindowsShellExecutableCandidatesForEnv(env, platform = process.platform) {
|
|
2127
|
+
if (platform !== "win32")
|
|
2128
|
+
return [];
|
|
2129
|
+
const candidates = [];
|
|
2130
|
+
const seen = new Set;
|
|
2131
|
+
const pushCandidate = (value) => {
|
|
2132
|
+
const trimmed = String(value ?? "").trim();
|
|
2133
|
+
if (!trimmed)
|
|
2134
|
+
return;
|
|
2135
|
+
const key = trimmed.toLowerCase();
|
|
2136
|
+
if (seen.has(key))
|
|
2137
|
+
return;
|
|
2138
|
+
seen.add(key);
|
|
2139
|
+
candidates.push(trimmed);
|
|
2140
|
+
};
|
|
2141
|
+
const comSpec = String(env.ComSpec ?? env.COMSPEC ?? process.env.ComSpec ?? process.env.COMSPEC ?? "").trim();
|
|
2142
|
+
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "").trim();
|
|
2143
|
+
pushCandidate(comSpec);
|
|
2144
|
+
if (systemRoot) {
|
|
2145
|
+
pushCandidate(pathWin32.join(systemRoot, "System32", "cmd.exe"));
|
|
2146
|
+
pushCandidate(pathWin32.join(systemRoot, "Sysnative", "cmd.exe"));
|
|
2147
|
+
}
|
|
2148
|
+
pushCandidate("cmd.exe");
|
|
2149
|
+
return candidates;
|
|
2150
|
+
}
|
|
2151
|
+
function resolveWindowsWhereExecutableCandidatesForEnv(env, platform = process.platform) {
|
|
2152
|
+
if (platform !== "win32")
|
|
2153
|
+
return [];
|
|
2154
|
+
const candidates = [];
|
|
2155
|
+
const seen = new Set;
|
|
2156
|
+
const pushCandidate = (value) => {
|
|
2157
|
+
const trimmed = String(value ?? "").trim();
|
|
2158
|
+
if (!trimmed)
|
|
2159
|
+
return;
|
|
2160
|
+
const key = trimmed.toLowerCase();
|
|
2161
|
+
if (seen.has(key))
|
|
2162
|
+
return;
|
|
2163
|
+
seen.add(key);
|
|
2164
|
+
candidates.push(trimmed);
|
|
2165
|
+
};
|
|
2166
|
+
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "").trim();
|
|
2167
|
+
if (systemRoot) {
|
|
2168
|
+
pushCandidate(pathWin32.join(systemRoot, "System32", "where.exe"));
|
|
2169
|
+
pushCandidate(pathWin32.join(systemRoot, "Sysnative", "where.exe"));
|
|
2170
|
+
}
|
|
2171
|
+
pushCandidate("where.exe");
|
|
2172
|
+
pushCandidate("where");
|
|
2173
|
+
return candidates;
|
|
2174
|
+
}
|
|
2175
|
+
function quoteWindowsCmdArg(value) {
|
|
2176
|
+
const text = String(value ?? "");
|
|
2177
|
+
if (!text.length)
|
|
2178
|
+
return '""';
|
|
2179
|
+
if (!/[ \t"]/.test(text))
|
|
2180
|
+
return text;
|
|
2181
|
+
const escaped = text.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1");
|
|
2182
|
+
return `"${escaped}"`;
|
|
2183
|
+
}
|
|
2004
2184
|
function isOptionalEmbeddedService(name) {
|
|
2005
2185
|
return name === "source_control_manager";
|
|
2006
2186
|
}
|
|
@@ -2019,12 +2199,183 @@ async function canSpawnCommand(command, cwd, env) {
|
|
|
2019
2199
|
return false;
|
|
2020
2200
|
}
|
|
2021
2201
|
}
|
|
2022
|
-
async function
|
|
2023
|
-
|
|
2024
|
-
if (!normalizedRemote)
|
|
2202
|
+
async function canSpawnGitViaWindowsShell(commandArgs, cwd, env, platform = process.platform) {
|
|
2203
|
+
if (platform !== "win32")
|
|
2025
2204
|
return false;
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2205
|
+
const commandLine = commandArgs.map((arg) => quoteWindowsCmdArg(arg)).join(" ");
|
|
2206
|
+
for (const shellExecutable of resolveWindowsShellExecutableCandidatesForEnv(env, platform)) {
|
|
2207
|
+
try {
|
|
2208
|
+
const proc = Bun.spawn([shellExecutable, "/d", "/s", "/c", commandLine], {
|
|
2209
|
+
cwd,
|
|
2210
|
+
env,
|
|
2211
|
+
stdin: "ignore",
|
|
2212
|
+
stdout: "ignore",
|
|
2213
|
+
stderr: "ignore"
|
|
2214
|
+
});
|
|
2215
|
+
const exitCode = await proc.exited;
|
|
2216
|
+
return exitCode === 0;
|
|
2217
|
+
} catch {}
|
|
2218
|
+
}
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
async function resolveSourceControlManagerGitProbe(cwd, env, platform = process.platform) {
|
|
2222
|
+
const candidates = resolveRuntimeGitExecutableCandidates(env, platform);
|
|
2223
|
+
for (const candidate of candidates) {
|
|
2224
|
+
if (await canSpawnCommand([candidate, "--version"], cwd, env)) {
|
|
2225
|
+
return { ok: true, detail: candidate };
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (platform === "win32") {
|
|
2229
|
+
for (const candidate of candidates) {
|
|
2230
|
+
if (await canSpawnGitViaWindowsShell([candidate, "--version"], cwd, env, platform)) {
|
|
2231
|
+
return { ok: true, detail: `${candidate} via shell` };
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
ok: false,
|
|
2237
|
+
detail: candidates.join(", ") || "git"
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform) {
|
|
2241
|
+
const resolvedDockerBinary = await resolveCommandPath(platform === "win32" ? "docker.exe" : "docker", cwd, env);
|
|
2242
|
+
if (resolvedDockerBinary) {
|
|
2243
|
+
prependExecutableDirToPath(env, resolvedDockerBinary, platform);
|
|
2244
|
+
env.PUSHPALS_DOCKER_BIN = basename(resolvedDockerBinary);
|
|
2245
|
+
env.PUSHPALS_DOCKER_BIN_ABSOLUTE = resolvedDockerBinary;
|
|
2246
|
+
}
|
|
2247
|
+
const candidates = resolveRuntimeDockerExecutableCandidates(env, platform);
|
|
2248
|
+
const failures = [];
|
|
2249
|
+
for (const candidate of candidates) {
|
|
2250
|
+
const result = await runCommandWithEnv([candidate, "version", "--format", "{{.Server.Version}}"], cwd, env);
|
|
2251
|
+
if (result.ok) {
|
|
2252
|
+
const version = result.stdout.trim();
|
|
2253
|
+
return {
|
|
2254
|
+
ok: true,
|
|
2255
|
+
detail: version ? `${candidate} (${version})` : candidate
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
|
|
2259
|
+
failures.push(`${candidate}: ${detail}`);
|
|
2260
|
+
}
|
|
2261
|
+
return {
|
|
2262
|
+
ok: false,
|
|
2263
|
+
detail: failures.join(" | ") || "docker"
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async function precheckSourceControlManagerGitAvailability(opts) {
|
|
2267
|
+
const platform = opts.platform ?? process.platform;
|
|
2268
|
+
const env = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
|
|
2269
|
+
repoRoot: opts.repoRoot,
|
|
2270
|
+
runtimeRoot: opts.runtimeRoot,
|
|
2271
|
+
useRuntimeConfig: opts.preflightUsesEmbeddedRuntime,
|
|
2272
|
+
sessionId: opts.sessionId
|
|
2273
|
+
});
|
|
2274
|
+
const preconfiguredGitBinary = env.PUSHPALS_GIT_BIN_ABSOLUTE ?? env.PUSHPALS_GIT_BIN;
|
|
2275
|
+
if (preconfiguredGitBinary) {
|
|
2276
|
+
applyResolvedGitBinaryToRuntimeEnv(env, preconfiguredGitBinary, platform);
|
|
2277
|
+
}
|
|
2278
|
+
const remoteStatus = opts.gitRemoteCheckFn ? await opts.gitRemoteCheckFn(opts.repoRoot, opts.remote, env) : opts.repoHasRemoteFn ? await opts.repoHasRemoteFn(opts.repoRoot, opts.remote) ? { status: "ok", remote: opts.remote } : { status: "missing_remote", remote: opts.remote } : await checkGitRemoteConfigured(opts.repoRoot, opts.remote, env);
|
|
2279
|
+
if (remoteStatus.status === "missing_remote") {
|
|
2280
|
+
return {
|
|
2281
|
+
status: "skipped",
|
|
2282
|
+
detail: `git remote "${opts.remote}" is not configured`,
|
|
2283
|
+
env
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
if (remoteStatus.status === "error") {
|
|
2287
|
+
return {
|
|
2288
|
+
status: "failed",
|
|
2289
|
+
detail: `git remote "${opts.remote}" could not be inspected: ${remoteStatus.detail}`,
|
|
2290
|
+
env
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
const gitLookupCommand = typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? env.PUSHPALS_GIT_BIN.trim() : platform === "win32" ? "git.exe" : "git";
|
|
2294
|
+
const resolvedGitBinary = await (opts.resolveCommandPathFn ?? resolveCommandPath)(gitLookupCommand, opts.repoRoot, env);
|
|
2295
|
+
if (resolvedGitBinary) {
|
|
2296
|
+
applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform);
|
|
2297
|
+
}
|
|
2298
|
+
const gitProbe = await (opts.gitProbeFn ?? resolveSourceControlManagerGitProbe)(opts.repoRoot, env, platform);
|
|
2299
|
+
if (!gitProbe.ok) {
|
|
2300
|
+
return {
|
|
2301
|
+
status: "failed",
|
|
2302
|
+
detail: gitProbe.detail,
|
|
2303
|
+
env
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return {
|
|
2307
|
+
status: "ok",
|
|
2308
|
+
detail: gitProbe.detail,
|
|
2309
|
+
env
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
async function precheckWorkerpalDockerAvailability(opts) {
|
|
2313
|
+
const env = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
|
|
2314
|
+
repoRoot: opts.repoRoot,
|
|
2315
|
+
runtimeRoot: opts.runtimeRoot,
|
|
2316
|
+
useRuntimeConfig: opts.preflightUsesEmbeddedRuntime,
|
|
2317
|
+
sessionId: opts.sessionId
|
|
2318
|
+
});
|
|
2319
|
+
const preconfiguredDockerBinary = env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN;
|
|
2320
|
+
if (preconfiguredDockerBinary) {
|
|
2321
|
+
applyResolvedDockerBinaryToRuntimeEnv(env, preconfiguredDockerBinary, opts.platform ?? process.platform);
|
|
2322
|
+
}
|
|
2323
|
+
if (!opts.autoSpawnWorkerpals) {
|
|
2324
|
+
return {
|
|
2325
|
+
status: "skipped",
|
|
2326
|
+
detail: "WorkerPal auto-spawn is disabled",
|
|
2327
|
+
env
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
if (!opts.dockerEnabled) {
|
|
2331
|
+
return {
|
|
2332
|
+
status: "skipped",
|
|
2333
|
+
detail: "WorkerPal docker mode is disabled",
|
|
2334
|
+
env
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
if (!opts.requireDocker) {
|
|
2338
|
+
return {
|
|
2339
|
+
status: "skipped",
|
|
2340
|
+
detail: "WorkerPal docker mode is optional",
|
|
2341
|
+
env
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
const dockerProbe = await (opts.dockerProbeFn ?? resolveWorkerpalDockerProbe)(opts.repoRoot, env, opts.platform ?? process.platform);
|
|
2345
|
+
if (!dockerProbe.ok) {
|
|
2346
|
+
return {
|
|
2347
|
+
status: "failed",
|
|
2348
|
+
detail: dockerProbe.detail,
|
|
2349
|
+
env
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
status: "ok",
|
|
2354
|
+
detail: dockerProbe.detail,
|
|
2355
|
+
env
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function resolveWorkerpalCapacityTimeoutMs(config) {
|
|
2359
|
+
return Math.max(config.remotebuddy.waitForWorkerpalMs, config.remotebuddy.workerpalStartupTimeoutMs, config.remotebuddy.workerpalDocker ? config.workerpals.dockerAgentStartupTimeoutMs + 15000 : 0, 1e4);
|
|
2360
|
+
}
|
|
2361
|
+
async function checkGitRemoteConfigured(repoRoot, remote, env) {
|
|
2362
|
+
const normalizedRemote = String(remote ?? "").trim();
|
|
2363
|
+
if (!normalizedRemote) {
|
|
2364
|
+
return { status: "missing_remote", remote: normalizedRemote };
|
|
2365
|
+
}
|
|
2366
|
+
const result = await runGitWithEnv(["remote", "get-url", normalizedRemote], repoRoot, env ?? {
|
|
2367
|
+
...process.env,
|
|
2368
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2369
|
+
GCM_INTERACTIVE: "Never"
|
|
2370
|
+
});
|
|
2371
|
+
if (result.ok && result.stdout) {
|
|
2372
|
+
return { status: "ok", remote: normalizedRemote };
|
|
2373
|
+
}
|
|
2374
|
+
const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
|
|
2375
|
+
if (/no such remote/i.test(detail)) {
|
|
2376
|
+
return { status: "missing_remote", remote: normalizedRemote };
|
|
2377
|
+
}
|
|
2378
|
+
return { status: "error", remote: normalizedRemote, detail };
|
|
2028
2379
|
}
|
|
2029
2380
|
async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
|
|
2030
2381
|
const normalizedRemote = String(remote ?? "").trim();
|
|
@@ -2032,10 +2383,18 @@ async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
|
|
|
2032
2383
|
if (!normalizedRemote || !normalizedBranch) {
|
|
2033
2384
|
return { status: "ok" };
|
|
2034
2385
|
}
|
|
2035
|
-
const
|
|
2036
|
-
if (
|
|
2386
|
+
const remoteStatus = await checkGitRemoteConfigured(repoRoot, normalizedRemote);
|
|
2387
|
+
if (remoteStatus.status === "missing_remote") {
|
|
2037
2388
|
return { status: "missing_remote", remote: normalizedRemote };
|
|
2038
2389
|
}
|
|
2390
|
+
if (remoteStatus.status === "error") {
|
|
2391
|
+
return {
|
|
2392
|
+
status: "error",
|
|
2393
|
+
remote: normalizedRemote,
|
|
2394
|
+
branch: normalizedBranch,
|
|
2395
|
+
detail: remoteStatus.detail
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2039
2398
|
const ref = `refs/heads/${normalizedBranch}`;
|
|
2040
2399
|
const result = await runGit(["ls-remote", "--heads", normalizedRemote, ref], repoRoot);
|
|
2041
2400
|
if (!result.ok) {
|
|
@@ -2072,6 +2431,151 @@ async function enforcePushpalsRemoteBranchPrecheck(repoRoot, remote, branch) {
|
|
|
2072
2431
|
console.error(`[pushpals] Precheck failed: could not verify remote branch "${result.remote}/${result.branch}": ${result.detail}`);
|
|
2073
2432
|
return false;
|
|
2074
2433
|
}
|
|
2434
|
+
function isPathEqualOrWithin(parentPath, childPath) {
|
|
2435
|
+
const parent = normalizeRepoPathForComparison(parentPath);
|
|
2436
|
+
const child = normalizeRepoPathForComparison(childPath);
|
|
2437
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
2438
|
+
}
|
|
2439
|
+
function appendCliClearTarget(targets, label, pathValue) {
|
|
2440
|
+
const resolvedPath = String(pathValue ?? "").trim();
|
|
2441
|
+
if (!resolvedPath)
|
|
2442
|
+
return;
|
|
2443
|
+
const normalized = normalizeRepoPathForComparison(resolvedPath);
|
|
2444
|
+
if (targets.some((target) => normalizeRepoPathForComparison(target.path) === normalized))
|
|
2445
|
+
return;
|
|
2446
|
+
targets.push({ label, path: resolve4(resolvedPath) });
|
|
2447
|
+
}
|
|
2448
|
+
function buildCliClearTargets(opts) {
|
|
2449
|
+
const targets = [];
|
|
2450
|
+
const dataDir = resolve4(opts.config.paths.dataDir);
|
|
2451
|
+
appendCliClearTarget(targets, "runtime data", dataDir);
|
|
2452
|
+
const scmStateDir = resolve4(opts.config.sourceControlManager.stateDir);
|
|
2453
|
+
if (!isPathEqualOrWithin(dataDir, scmStateDir)) {
|
|
2454
|
+
appendCliClearTarget(targets, "SourceControlManager state", scmStateDir);
|
|
2455
|
+
}
|
|
2456
|
+
const scmRepoPath = resolve4(opts.config.sourceControlManager.repoPath);
|
|
2457
|
+
if (normalizeRepoPathForComparison(scmRepoPath) !== normalizeRepoPathForComparison(opts.repoRoot) && isPathEqualOrWithin(opts.repoRoot, scmRepoPath)) {
|
|
2458
|
+
appendCliClearTarget(targets, "SourceControlManager worktree", scmRepoPath);
|
|
2459
|
+
}
|
|
2460
|
+
appendCliClearTarget(targets, "CLI state file", opts.cliStatePath ?? null);
|
|
2461
|
+
appendCliClearTarget(targets, "client monitor state file", resolveGitStateFilePath(opts.repoRoot, "pushpals-client-state.json"));
|
|
2462
|
+
appendCliClearTarget(targets, "runtime bootstrap logs", join2(opts.runtimeRoot, "logs", "bootstrap"));
|
|
2463
|
+
return targets;
|
|
2464
|
+
}
|
|
2465
|
+
function removeCliClearTarget(target) {
|
|
2466
|
+
if (!existsSync4(target.path))
|
|
2467
|
+
return "missing";
|
|
2468
|
+
try {
|
|
2469
|
+
rmSync(target.path, { recursive: true, force: true });
|
|
2470
|
+
return "removed";
|
|
2471
|
+
} catch (err) {
|
|
2472
|
+
return {
|
|
2473
|
+
...target,
|
|
2474
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
|
|
2479
|
+
if (!await probeServer(serverUrl)) {
|
|
2480
|
+
return { attempted: false, accepted: false };
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
await ensureServerRepoAffinity(serverUrl, repoRoot);
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
return {
|
|
2486
|
+
attempted: false,
|
|
2487
|
+
accepted: false,
|
|
2488
|
+
detail: `skipping shutdown because ${String(err)}`
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const response = await fetchWithTimeout(`${serverUrl}/admin/shutdown`, {
|
|
2493
|
+
method: "POST",
|
|
2494
|
+
headers: { "Content-Type": "application/json" },
|
|
2495
|
+
body: JSON.stringify({ reason: "pushpals --clear" })
|
|
2496
|
+
}, 5000);
|
|
2497
|
+
if (!response.ok) {
|
|
2498
|
+
const detail = await response.text().catch(() => "");
|
|
2499
|
+
return {
|
|
2500
|
+
attempted: true,
|
|
2501
|
+
accepted: false,
|
|
2502
|
+
detail: `HTTP ${response.status}${detail ? ` ${detail}` : ""}`
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
return { attempted: true, accepted: true };
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
return {
|
|
2508
|
+
attempted: true,
|
|
2509
|
+
accepted: false,
|
|
2510
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
async function clearPushpalsState(opts) {
|
|
2515
|
+
console.log("[pushpals] Clear requested. Removing repo-local PushPals state.");
|
|
2516
|
+
const shutdown = await requestLocalRuntimeShutdownForClear(opts.serverUrl, opts.repoRoot);
|
|
2517
|
+
if (shutdown.attempted && shutdown.accepted) {
|
|
2518
|
+
console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
|
|
2519
|
+
await Bun.sleep(1500);
|
|
2520
|
+
} else if (shutdown.attempted) {
|
|
2521
|
+
console.warn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
|
|
2522
|
+
} else if (shutdown.detail) {
|
|
2523
|
+
console.warn(`[pushpals] ${shutdown.detail}`);
|
|
2524
|
+
}
|
|
2525
|
+
const targets = buildCliClearTargets({
|
|
2526
|
+
repoRoot: opts.repoRoot,
|
|
2527
|
+
runtimeRoot: opts.runtimeRoot,
|
|
2528
|
+
config: opts.config,
|
|
2529
|
+
cliStatePath: opts.cliStatePath
|
|
2530
|
+
});
|
|
2531
|
+
const removed = [];
|
|
2532
|
+
const missing = [];
|
|
2533
|
+
let failed = [];
|
|
2534
|
+
for (const target of targets) {
|
|
2535
|
+
const result = removeCliClearTarget(target);
|
|
2536
|
+
if (result === "removed") {
|
|
2537
|
+
removed.push(target);
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
if (result === "missing") {
|
|
2541
|
+
missing.push(target);
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
failed.push(result);
|
|
2545
|
+
}
|
|
2546
|
+
if (failed.length > 0 && shutdown.accepted) {
|
|
2547
|
+
await Bun.sleep(1000);
|
|
2548
|
+
const retryFailures = [];
|
|
2549
|
+
for (const failure of failed) {
|
|
2550
|
+
const retry = removeCliClearTarget(failure);
|
|
2551
|
+
if (retry === "removed") {
|
|
2552
|
+
removed.push({ label: failure.label, path: failure.path });
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
if (retry === "missing") {
|
|
2556
|
+
missing.push({ label: failure.label, path: failure.path });
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
retryFailures.push(retry);
|
|
2560
|
+
}
|
|
2561
|
+
failed = retryFailures;
|
|
2562
|
+
}
|
|
2563
|
+
for (const target of removed) {
|
|
2564
|
+
console.log(`[pushpals] Cleared ${target.label}: ${target.path}`);
|
|
2565
|
+
}
|
|
2566
|
+
for (const target of missing) {
|
|
2567
|
+
console.log(`[pushpals] Nothing to clear for ${target.label}: ${target.path}`);
|
|
2568
|
+
}
|
|
2569
|
+
for (const failure of failed) {
|
|
2570
|
+
console.error(`[pushpals] Failed to clear ${failure.label}: ${failure.path} (${failure.detail})`);
|
|
2571
|
+
}
|
|
2572
|
+
if (failed.length > 0) {
|
|
2573
|
+
console.error("[pushpals] Clear completed with errors.");
|
|
2574
|
+
return 1;
|
|
2575
|
+
}
|
|
2576
|
+
console.log("[pushpals] Clear completed.");
|
|
2577
|
+
return 0;
|
|
2578
|
+
}
|
|
2075
2579
|
async function probeServer(serverUrl) {
|
|
2076
2580
|
try {
|
|
2077
2581
|
const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
|
|
@@ -2195,6 +2699,42 @@ async function probeSourceControlManager(port) {
|
|
|
2195
2699
|
return false;
|
|
2196
2700
|
}
|
|
2197
2701
|
}
|
|
2702
|
+
async function fetchWorkerStatusRows(serverUrl, ttlMs) {
|
|
2703
|
+
const payload = await fetchJsonWithTimeout(`${serverUrl}/workers?ttlMs=${Math.max(1000, Math.floor(ttlMs))}`, {}, 1e4);
|
|
2704
|
+
if (!payload?.ok || !Array.isArray(payload.workers)) {
|
|
2705
|
+
return [];
|
|
2706
|
+
}
|
|
2707
|
+
return payload.workers;
|
|
2708
|
+
}
|
|
2709
|
+
async function waitForWorkerpalCapacity(opts) {
|
|
2710
|
+
const deadline = Date.now() + Math.max(1000, opts.timeoutMs);
|
|
2711
|
+
let lastObservedOnline = 0;
|
|
2712
|
+
while (Date.now() < deadline) {
|
|
2713
|
+
const workers = await (opts.fetchWorkersFn ?? fetchWorkerStatusRows)(opts.serverUrl, opts.ttlMs);
|
|
2714
|
+
const onlineWorkers = workers.filter((worker) => Boolean(worker?.isOnline) && String(worker?.status ?? "").trim().toLowerCase() !== "offline");
|
|
2715
|
+
const idleWorkers = onlineWorkers.filter((worker) => Number(worker?.activeJobCount ?? 0) <= 0);
|
|
2716
|
+
if (onlineWorkers.length > 0) {
|
|
2717
|
+
lastObservedOnline = Math.max(lastObservedOnline, onlineWorkers.length);
|
|
2718
|
+
}
|
|
2719
|
+
if (idleWorkers.length > 0) {
|
|
2720
|
+
return {
|
|
2721
|
+
ok: true,
|
|
2722
|
+
detail: `${idleWorkers.length} idle / ${onlineWorkers.length} online`
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
await (opts.sleepFn ?? Bun.sleep)(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
2726
|
+
}
|
|
2727
|
+
if (lastObservedOnline > 0) {
|
|
2728
|
+
return {
|
|
2729
|
+
ok: false,
|
|
2730
|
+
detail: `${lastObservedOnline} online WorkerPal(s) reported but none became idle within ${Math.max(1000, opts.timeoutMs)}ms`
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
return {
|
|
2734
|
+
ok: false,
|
|
2735
|
+
detail: `no online WorkerPal reported within ${Math.max(1000, opts.timeoutMs)}ms`
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2198
2738
|
async function fetchWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
2199
2739
|
const controller = new AbortController;
|
|
2200
2740
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -2280,15 +2820,20 @@ async function autoStartRuntimeServices(opts) {
|
|
|
2280
2820
|
}
|
|
2281
2821
|
await ensureRuntimeAssets(runtimeRoot, runtimeTag);
|
|
2282
2822
|
const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
|
|
2283
|
-
const runtimeEnv = buildEmbeddedRuntimeEnv(process.env, {
|
|
2823
|
+
const runtimeEnv = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
|
|
2284
2824
|
repoRoot: opts.repoRoot,
|
|
2285
2825
|
runtimeRoot,
|
|
2286
2826
|
useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
|
|
2287
2827
|
sessionId: opts.sessionId
|
|
2288
2828
|
});
|
|
2289
2829
|
runtimeEnv.PUSHPALS_WORKERPALS_BIN = runtimeBinaries.workerpals;
|
|
2290
|
-
|
|
2291
|
-
|
|
2830
|
+
const preconfiguredRuntimeGitBinary = runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE ?? runtimeEnv.PUSHPALS_GIT_BIN;
|
|
2831
|
+
if (preconfiguredRuntimeGitBinary) {
|
|
2832
|
+
applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, preconfiguredRuntimeGitBinary);
|
|
2833
|
+
}
|
|
2834
|
+
const preconfiguredRuntimeDockerBinary = runtimeEnv.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? runtimeEnv.PUSHPALS_DOCKER_BIN;
|
|
2835
|
+
if (preconfiguredRuntimeDockerBinary) {
|
|
2836
|
+
applyResolvedDockerBinaryToRuntimeEnv(runtimeEnv, preconfiguredRuntimeDockerBinary);
|
|
2292
2837
|
}
|
|
2293
2838
|
const gitLookupCommand = typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? runtimeEnv.PUSHPALS_GIT_BIN.trim() : "git";
|
|
2294
2839
|
const resolvedGitBinary = await resolveCommandPath(gitLookupCommand, opts.repoRoot, runtimeEnv);
|
|
@@ -2376,28 +2921,44 @@ ${tail}` : ""}`);
|
|
|
2376
2921
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded remotebuddy autonomous engine is disabled (remotebuddy.autonomy.enabled=false).");
|
|
2377
2922
|
};
|
|
2378
2923
|
reportRemoteBuddyAutonomousEngineState();
|
|
2924
|
+
if (runtimePreflight.config.remotebuddy.autoSpawnWorkerpals) {
|
|
2925
|
+
const workerpalReadyTimeoutMs = resolveWorkerpalCapacityTimeoutMs(runtimePreflight.config);
|
|
2926
|
+
const workerpalCapacity = await waitForWorkerpalCapacity({
|
|
2927
|
+
serverUrl: opts.serverUrl,
|
|
2928
|
+
timeoutMs: workerpalReadyTimeoutMs,
|
|
2929
|
+
ttlMs: runtimePreflight.config.remotebuddy.workerpalOnlineTtlMs
|
|
2930
|
+
});
|
|
2931
|
+
if (!workerpalCapacity.ok) {
|
|
2932
|
+
const tail = readLogTail(remotebuddyService.logPath);
|
|
2933
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity did not become available within ${workerpalReadyTimeoutMs}ms.`);
|
|
2934
|
+
stopRuntimeServices(services);
|
|
2935
|
+
throw new Error(`Embedded WorkerPal capacity did not become available within ${workerpalReadyTimeoutMs}ms (${workerpalCapacity.detail}). ` + `See ${remotebuddyService.logPath}${tail ? `
|
|
2936
|
+
--- remotebuddy log tail ---
|
|
2937
|
+
${tail}` : ""}`);
|
|
2938
|
+
}
|
|
2939
|
+
console.log(`[pushpals] Embedded WorkerPal capacity is ready (${workerpalCapacity.detail}).`);
|
|
2940
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity ready (${workerpalCapacity.detail}).`);
|
|
2941
|
+
}
|
|
2379
2942
|
const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
|
|
2380
|
-
const
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (!scmHealthy && scmRemoteAvailable) {
|
|
2385
|
-
if (!gitAvailableForScm) {
|
|
2943
|
+
const scmGitProbe = await resolveSourceControlManagerGitProbe(opts.repoRoot, runtimeEnv, process.platform);
|
|
2944
|
+
const scmRemoteStatus = await checkGitRemoteConfigured(opts.repoRoot, opts.sourceControlManagerRemote, runtimeEnv);
|
|
2945
|
+
if (!scmHealthy) {
|
|
2946
|
+
if (!scmGitProbe.ok) {
|
|
2386
2947
|
console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
|
|
2387
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath,
|
|
2388
|
-
} else {
|
|
2389
|
-
console.
|
|
2948
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env (${scmGitProbe.detail}).`);
|
|
2949
|
+
} else if (scmRemoteStatus.status === "error") {
|
|
2950
|
+
console.warn(`[pushpals] Could not inspect SourceControlManager git remote "${opts.sourceControlManagerRemote}"; skipping SCM startup.`);
|
|
2951
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: remote "${opts.sourceControlManagerRemote}" could not be inspected (${scmRemoteStatus.detail}).`);
|
|
2952
|
+
} else if (scmRemoteStatus.status === "ok") {
|
|
2953
|
+
console.log(`[pushpals] Embedded SourceControlManager git=${scmGitProbe.detail}`);
|
|
2390
2954
|
console.log("[pushpals] Starting embedded SourceControlManager...");
|
|
2391
2955
|
const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, serviceLogPaths.source_control_manager, runtimeServicesLogPath);
|
|
2392
2956
|
services.push(sourceControlManagerService);
|
|
2393
2957
|
console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
|
|
2958
|
+
} else {
|
|
2959
|
+
console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
|
|
2960
|
+
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
|
|
2394
2961
|
}
|
|
2395
|
-
} else if (!scmRemoteAvailable) {
|
|
2396
|
-
console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
|
|
2397
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
|
|
2398
|
-
} else if (!gitAvailableForScm) {
|
|
2399
|
-
console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
|
|
2400
|
-
appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
|
|
2401
2962
|
} else {
|
|
2402
2963
|
console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
|
|
2403
2964
|
appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager already healthy; embedded start skipped.");
|
|
@@ -3013,6 +3574,19 @@ async function main() {
|
|
|
3013
3574
|
runtimeRoot: parsed.runtimeRoot,
|
|
3014
3575
|
runtimeTag: parsed.runtimeTag
|
|
3015
3576
|
});
|
|
3577
|
+
const config = preparedRuntime.runtimePreflight.config;
|
|
3578
|
+
const statePath = resolveCliStatePath(repoRoot);
|
|
3579
|
+
if (parsed.clear) {
|
|
3580
|
+
const serverUrl2 = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
|
|
3581
|
+
const exitCode = await clearPushpalsState({
|
|
3582
|
+
repoRoot,
|
|
3583
|
+
runtimeRoot: preparedRuntime.runtimeRoot,
|
|
3584
|
+
config,
|
|
3585
|
+
serverUrl: serverUrl2,
|
|
3586
|
+
cliStatePath: statePath
|
|
3587
|
+
});
|
|
3588
|
+
process.exit(exitCode);
|
|
3589
|
+
}
|
|
3016
3590
|
console.log("[pushpals] Running runtime preflight...");
|
|
3017
3591
|
console.log(`[pushpals] runtimeRoot=${preparedRuntime.runtimeRoot}`);
|
|
3018
3592
|
if (preparedRuntime.runtimeTag) {
|
|
@@ -3026,12 +3600,30 @@ async function main() {
|
|
|
3026
3600
|
if (!preparedRuntime.runtimePreflight.ok) {
|
|
3027
3601
|
process.exit(1);
|
|
3028
3602
|
}
|
|
3029
|
-
const config = preparedRuntime.runtimePreflight.config;
|
|
3030
3603
|
if (config.remotebuddy.autonomy.enabled) {
|
|
3031
3604
|
console.log("[pushpals] RemoteBuddy autonomy is enabled for CLI.");
|
|
3032
3605
|
} else {
|
|
3033
3606
|
console.warn("[pushpals] RemoteBuddy autonomy is disabled in config (remotebuddy.autonomy.enabled=false); continuing.");
|
|
3034
3607
|
}
|
|
3608
|
+
const scmGitPrecheck = await precheckSourceControlManagerGitAvailability({
|
|
3609
|
+
repoRoot,
|
|
3610
|
+
remote: config.sourceControlManager.remote,
|
|
3611
|
+
runtimeRoot: preparedRuntime.runtimeRoot,
|
|
3612
|
+
preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime
|
|
3613
|
+
});
|
|
3614
|
+
if (scmGitPrecheck.status === "failed") {
|
|
3615
|
+
console.error(`[pushpals] Precheck failed: embedded SourceControlManager git command is unavailable (${scmGitPrecheck.detail}).`);
|
|
3616
|
+
process.exit(1);
|
|
3617
|
+
}
|
|
3618
|
+
const workerpalDockerPrecheck = await precheckWorkerpalDockerAvailability({
|
|
3619
|
+
repoRoot,
|
|
3620
|
+
runtimeRoot: preparedRuntime.runtimeRoot,
|
|
3621
|
+
preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
|
|
3622
|
+
autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
|
|
3623
|
+
dockerEnabled: Boolean(config.remotebuddy.workerpalDocker),
|
|
3624
|
+
requireDocker: Boolean(config.remotebuddy.workerpalRequireDocker),
|
|
3625
|
+
baseEnv: scmGitPrecheck.env
|
|
3626
|
+
});
|
|
3035
3627
|
const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
|
|
3036
3628
|
if (!precheckPassed) {
|
|
3037
3629
|
process.exit(1);
|
|
@@ -3064,6 +3656,11 @@ async function main() {
|
|
|
3064
3656
|
};
|
|
3065
3657
|
if (!serverHealthy) {
|
|
3066
3658
|
if (!parsed.noAutoStart) {
|
|
3659
|
+
if (workerpalDockerPrecheck.status === "failed") {
|
|
3660
|
+
console.error(`[pushpals] Precheck failed: Docker-backed WorkerPal auto-spawn is required but Docker is unavailable (${workerpalDockerPrecheck.detail}).`);
|
|
3661
|
+
console.error("[pushpals] Precheck failed: start Docker Desktop or the Docker daemon, then retry pushpals.");
|
|
3662
|
+
process.exit(1);
|
|
3663
|
+
}
|
|
3067
3664
|
try {
|
|
3068
3665
|
const startedRuntime = await autoStartRuntimeServices({
|
|
3069
3666
|
repoRoot,
|
|
@@ -3074,7 +3671,8 @@ async function main() {
|
|
|
3074
3671
|
sourceControlManagerRemote: config.sourceControlManager.remote,
|
|
3075
3672
|
preparedRuntime,
|
|
3076
3673
|
requestedRuntimeTag: parsed.runtimeTag,
|
|
3077
|
-
startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled))
|
|
3674
|
+
startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled)),
|
|
3675
|
+
baseEnv: workerpalDockerPrecheck.env
|
|
3078
3676
|
});
|
|
3079
3677
|
autoStartedServices = startedRuntime.services;
|
|
3080
3678
|
pushpalsLogPath = startedRuntime.pushpalsLogPath;
|
|
@@ -3129,7 +3727,22 @@ async function main() {
|
|
|
3129
3727
|
}
|
|
3130
3728
|
process.exit(1);
|
|
3131
3729
|
}
|
|
3132
|
-
const
|
|
3730
|
+
const workerpalCapacity = await waitForWorkerpalCapacity({
|
|
3731
|
+
serverUrl,
|
|
3732
|
+
timeoutMs: resolveWorkerpalCapacityTimeoutMs(config),
|
|
3733
|
+
ttlMs: config.remotebuddy.workerpalOnlineTtlMs
|
|
3734
|
+
});
|
|
3735
|
+
if (!workerpalCapacity.ok) {
|
|
3736
|
+
stopAutoStartedServices();
|
|
3737
|
+
console.error(`[pushpals] WorkerPal capacity is not ready for repo ${repoRoot}: ${workerpalCapacity.detail}.`);
|
|
3738
|
+
if (workerpalDockerPrecheck.status === "failed") {
|
|
3739
|
+
console.error(`[pushpals] Docker precheck detail: ${workerpalDockerPrecheck.detail}`);
|
|
3740
|
+
} else if (serverWasAlreadyHealthy) {
|
|
3741
|
+
console.error("[pushpals] A PushPals runtime is already serving this repo, but it does not currently have an idle WorkerPal available.");
|
|
3742
|
+
console.error("[pushpals] Wait for a worker to become idle or restart the runtime after fixing WorkerPal startup.");
|
|
3743
|
+
}
|
|
3744
|
+
process.exit(1);
|
|
3745
|
+
}
|
|
3133
3746
|
const saved = statePath ? readCliState(statePath) : {};
|
|
3134
3747
|
pushpalsLogPath = pushpalsLogPath || (typeof saved.pushpalsLogPath === "string" ? saved.pushpalsLogPath : undefined);
|
|
3135
3748
|
const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
|
|
@@ -3304,13 +3917,21 @@ if (import.meta.main) {
|
|
|
3304
3917
|
});
|
|
3305
3918
|
}
|
|
3306
3919
|
export {
|
|
3920
|
+
waitForWorkerpalCapacity,
|
|
3307
3921
|
startEmbeddedMonitoringHub,
|
|
3922
|
+
resolveWindowsWhereExecutableCandidatesForEnv,
|
|
3923
|
+
resolveWindowsShellExecutableCandidatesForEnv,
|
|
3924
|
+
resolveRuntimeGitExecutableCandidates,
|
|
3925
|
+
resolveRuntimeDockerExecutableCandidates,
|
|
3926
|
+
resolvePreferredRuntimeReleaseTag,
|
|
3308
3927
|
resolveCommandPath,
|
|
3309
3928
|
resolveCliStatePath,
|
|
3310
3929
|
resolveCliLocalBuddyAutostart,
|
|
3311
3930
|
resolveBundledRuntimeAssetSource,
|
|
3312
3931
|
resolveBundledMonitoringHubRoot,
|
|
3313
3932
|
prepareCliRuntime,
|
|
3933
|
+
precheckWorkerpalDockerAvailability,
|
|
3934
|
+
precheckSourceControlManagerGitAvailability,
|
|
3314
3935
|
normalizeRepoPathForComparison,
|
|
3315
3936
|
normalizeCliInteractiveMessage,
|
|
3316
3937
|
normalizeChildProcessEnv,
|
|
@@ -3326,5 +3947,7 @@ export {
|
|
|
3326
3947
|
buildOpenMonitoringHubCommand,
|
|
3327
3948
|
buildEmbeddedRuntimeEnv,
|
|
3328
3949
|
buildEmbeddedMonitoringHubHtml,
|
|
3329
|
-
|
|
3950
|
+
buildCliClearTargets,
|
|
3951
|
+
applyResolvedGitBinaryToRuntimeEnv,
|
|
3952
|
+
applyResolvedDockerBinaryToRuntimeEnv
|
|
3330
3953
|
};
|