@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.
Files changed (2) hide show
  1. package/dist/pushpals-cli.js +693 -70
  2. package/package.json +1 -1
@@ -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 remoteAutonomyDispatchByComponent = {
401
- ...remoteAutonomyDispatchByComponentCfg
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 normalizeAutonomyComponentKey = (value) => value.trim().toLowerCase().replace(/\\/g, "/").replace(/_+/g, "/").replace(/-+/g, "/").replace(/\/+/g, "/");
404
- const canonicalComponentByNormalized = new Map(Object.keys(remoteAutonomyDispatchByComponentCfg).map((key) => [normalizeAutonomyComponentKey(key), key]));
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 normalized = normalizeAutonomyComponentKey(rawKey);
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 = { noAutoStart: false, noStream: false, runtimeOnly: false };
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
- const proc = Bun.spawn(["git", ...args], {
1363
- cwd,
1364
- env: {
1365
- ...process.env,
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
- async function resolveRuntimeReleaseTag(explicitTag) {
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(process.env.PUSHPALS_RUNTIME_TAG ?? "").trim();
1615
+ const fromEnv = String(env.PUSHPALS_RUNTIME_TAG ?? "").trim();
1543
1616
  if (fromEnv)
1544
1617
  return fromEnv;
1545
- const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
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
- try {
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 repoHasRemote(repoRoot, remote) {
2023
- const normalizedRemote = remote.trim();
2024
- if (!normalizedRemote)
2202
+ async function canSpawnGitViaWindowsShell(commandArgs, cwd, env, platform = process.platform) {
2203
+ if (platform !== "win32")
2025
2204
  return false;
2026
- const result = await runGit(["remote", "get-url", normalizedRemote], repoRoot);
2027
- return result.ok && Boolean(result.stdout);
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 hasRemote = await repoHasRemote(repoRoot, normalizedRemote);
2036
- if (!hasRemote) {
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
- if (runtimeEnv.PUSHPALS_GIT_BIN) {
2291
- applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
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 scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
2381
- const gitForScm = typeof runtimeEnv.PUSHPALS_GIT_BIN === "string" && runtimeEnv.PUSHPALS_GIT_BIN.trim() ? runtimeEnv.PUSHPALS_GIT_BIN.trim() : typeof runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? runtimeEnv.PUSHPALS_GIT_BIN_ABSOLUTE.trim() : "git";
2382
- const gitProbeCommand = [gitForScm, "--version"];
2383
- const gitAvailableForScm = await canSpawnCommand(gitProbeCommand, opts.repoRoot, runtimeEnv);
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, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
2388
- } else {
2389
- console.log(`[pushpals] Embedded SourceControlManager git=${gitForScm}`);
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 statePath = resolveCliStatePath(repoRoot);
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
- applyResolvedGitBinaryToRuntimeEnv
3950
+ buildCliClearTargets,
3951
+ applyResolvedGitBinaryToRuntimeEnv,
3952
+ applyResolvedDockerBinaryToRuntimeEnv
3330
3953
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {