@pushpalsdev/cli 1.0.15 → 1.0.17

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 +444 -61
  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
@@ -1257,6 +1258,7 @@ function printUsage() {
1257
1258
  console.log(" --no-auto-start Disable runtime auto-start when the server is down");
1258
1259
  console.log(" --no-stream Disable live session event stream");
1259
1260
  console.log(" --runtime-only Start the local runtime and wait for shutdown without opening the interactive chat");
1261
+ console.log(" --clear Remove repo-local PushPals state and exit");
1260
1262
  console.log(" -h, --help Show this help");
1261
1263
  console.log("");
1262
1264
  console.log("Chat commands:");
@@ -1271,7 +1273,12 @@ function printUsage() {
1271
1273
  console.log(" - Interactive CLI talks directly to server sessions; LocalBuddy is optional.");
1272
1274
  }
1273
1275
  function parseArgs(argv) {
1274
- const options = { noAutoStart: false, noStream: false, runtimeOnly: false };
1276
+ const options = {
1277
+ noAutoStart: false,
1278
+ noStream: false,
1279
+ runtimeOnly: false,
1280
+ clear: false
1281
+ };
1275
1282
  for (let i = 0;i < argv.length; i++) {
1276
1283
  const arg = argv[i];
1277
1284
  if (arg === "-h" || arg === "--help") {
@@ -1290,6 +1297,10 @@ function parseArgs(argv) {
1290
1297
  options.runtimeOnly = true;
1291
1298
  continue;
1292
1299
  }
1300
+ if (arg === "--clear") {
1301
+ options.clear = true;
1302
+ continue;
1303
+ }
1293
1304
  if (arg === "--server-url") {
1294
1305
  options.serverUrl = argv[++i];
1295
1306
  continue;
@@ -1358,23 +1369,35 @@ function parsePositiveInt(value, fallback) {
1358
1369
  function jsonHtmlBootstrap(value) {
1359
1370
  return JSON.stringify(value).replace(/</g, "\\u003c");
1360
1371
  }
1372
+ async function runGitWithEnv(args, cwd, env) {
1373
+ try {
1374
+ const proc = Bun.spawn(["git", ...args], {
1375
+ cwd,
1376
+ env,
1377
+ stdout: "pipe",
1378
+ stderr: "pipe"
1379
+ });
1380
+ const [stdout, stderr, exitCode] = await Promise.all([
1381
+ new Response(proc.stdout).text(),
1382
+ new Response(proc.stderr).text(),
1383
+ proc.exited
1384
+ ]);
1385
+ return { ok: exitCode === 0, stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
1386
+ } catch (err) {
1387
+ return {
1388
+ ok: false,
1389
+ stdout: "",
1390
+ stderr: err instanceof Error ? err.message : String(err),
1391
+ exitCode: -1
1392
+ };
1393
+ }
1394
+ }
1361
1395
  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"
1396
+ return await runGitWithEnv(args, cwd, {
1397
+ ...process.env,
1398
+ GIT_TERMINAL_PROMPT: "0",
1399
+ GCM_INTERACTIVE: "Never"
1371
1400
  });
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
1401
  }
1379
1402
  async function resolveCurrentGitRepoRoot(cwd) {
1380
1403
  const inside = await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
@@ -1535,25 +1558,24 @@ async function fetchLatestReleaseTag() {
1535
1558
  throw new Error("Latest release payload did not include tag_name");
1536
1559
  return tagName;
1537
1560
  }
1538
- async function resolveRuntimeReleaseTag(explicitTag) {
1561
+ function resolvePreferredRuntimeReleaseTag(explicitTag, env = process.env) {
1539
1562
  const fromArg = String(explicitTag ?? "").trim();
1540
1563
  if (fromArg)
1541
1564
  return fromArg;
1542
- const fromEnv = String(process.env.PUSHPALS_RUNTIME_TAG ?? "").trim();
1565
+ const fromEnv = String(env.PUSHPALS_RUNTIME_TAG ?? "").trim();
1543
1566
  if (fromEnv)
1544
1567
  return fromEnv;
1545
- const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
1568
+ const packageVersion = parseSemverFromPackageVersion(env.PUSHPALS_CLI_PACKAGE_VERSION);
1569
+ if (packageVersion)
1570
+ return `v${packageVersion}`;
1571
+ return "";
1572
+ }
1573
+ async function resolveRuntimeReleaseTag(explicitTag) {
1574
+ const preferredTag = resolvePreferredRuntimeReleaseTag(explicitTag, process.env);
1575
+ if (preferredTag)
1576
+ return preferredTag;
1546
1577
  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
- }
1578
+ return await fetchLatestReleaseTag();
1557
1579
  }
1558
1580
  function writeTextFileIfMissing(pathValue, text) {
1559
1581
  if (existsSync4(pathValue))
@@ -1751,10 +1773,7 @@ function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
1751
1773
  return env;
1752
1774
  }
1753
1775
  async function resolveCommandPath(command, cwd, env) {
1754
- const lookupCommands = process.platform === "win32" ? [
1755
- ["where.exe", command],
1756
- ["where", command]
1757
- ] : [["which", command]];
1776
+ const lookupCommands = process.platform === "win32" ? resolveWindowsWhereExecutableCandidatesForEnv(env, process.platform).map((lookup) => [lookup, command]) : [["which", command]];
1758
1777
  for (const lookup of lookupCommands) {
1759
1778
  try {
1760
1779
  const proc = Bun.spawn(lookup, {
@@ -1848,12 +1867,14 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1848
1867
  server: join2(binDir, runtimeBinaryFilename("server", platformKey)),
1849
1868
  localbuddy: join2(binDir, runtimeBinaryFilename("localbuddy", platformKey)),
1850
1869
  remotebuddy: join2(binDir, runtimeBinaryFilename("remotebuddy", platformKey)),
1870
+ workerpals: join2(binDir, runtimeBinaryFilename("workerpals", platformKey)),
1851
1871
  sourceControlManager: join2(binDir, runtimeBinaryFilename("source_control_manager", platformKey))
1852
1872
  };
1853
1873
  const requiredAssets = [
1854
1874
  runtimeBinaries.server,
1855
1875
  runtimeBinaries.localbuddy,
1856
1876
  runtimeBinaries.remotebuddy,
1877
+ runtimeBinaries.workerpals,
1857
1878
  runtimeBinaries.sourceControlManager
1858
1879
  ];
1859
1880
  let downloadedCount = 0;
@@ -1999,6 +2020,83 @@ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = p
1999
2020
  }
2000
2021
  return env;
2001
2022
  }
2023
+ function resolveRuntimeGitExecutableCandidates(env, platform = process.platform) {
2024
+ const candidates = [];
2025
+ const seen = new Set;
2026
+ const pushCandidate = (value) => {
2027
+ const trimmed = String(value ?? "").trim();
2028
+ if (!trimmed)
2029
+ return;
2030
+ const key = platform === "win32" ? trimmed.toLowerCase() : trimmed;
2031
+ if (seen.has(key))
2032
+ return;
2033
+ seen.add(key);
2034
+ candidates.push(trimmed);
2035
+ };
2036
+ pushCandidate(env.PUSHPALS_GIT_BIN ?? "");
2037
+ pushCandidate(env.PUSHPALS_GIT_BIN_ABSOLUTE ?? "");
2038
+ pushCandidate(platform === "win32" ? "git.exe" : "git");
2039
+ pushCandidate("git");
2040
+ return candidates;
2041
+ }
2042
+ function resolveWindowsShellExecutableCandidatesForEnv(env, platform = process.platform) {
2043
+ if (platform !== "win32")
2044
+ return [];
2045
+ const candidates = [];
2046
+ const seen = new Set;
2047
+ const pushCandidate = (value) => {
2048
+ const trimmed = String(value ?? "").trim();
2049
+ if (!trimmed)
2050
+ return;
2051
+ const key = trimmed.toLowerCase();
2052
+ if (seen.has(key))
2053
+ return;
2054
+ seen.add(key);
2055
+ candidates.push(trimmed);
2056
+ };
2057
+ const comSpec = String(env.ComSpec ?? env.COMSPEC ?? process.env.ComSpec ?? process.env.COMSPEC ?? "").trim();
2058
+ const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "").trim();
2059
+ pushCandidate(comSpec);
2060
+ if (systemRoot) {
2061
+ pushCandidate(pathWin32.join(systemRoot, "System32", "cmd.exe"));
2062
+ pushCandidate(pathWin32.join(systemRoot, "Sysnative", "cmd.exe"));
2063
+ }
2064
+ pushCandidate("cmd.exe");
2065
+ return candidates;
2066
+ }
2067
+ function resolveWindowsWhereExecutableCandidatesForEnv(env, platform = process.platform) {
2068
+ if (platform !== "win32")
2069
+ return [];
2070
+ const candidates = [];
2071
+ const seen = new Set;
2072
+ const pushCandidate = (value) => {
2073
+ const trimmed = String(value ?? "").trim();
2074
+ if (!trimmed)
2075
+ return;
2076
+ const key = trimmed.toLowerCase();
2077
+ if (seen.has(key))
2078
+ return;
2079
+ seen.add(key);
2080
+ candidates.push(trimmed);
2081
+ };
2082
+ const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "").trim();
2083
+ if (systemRoot) {
2084
+ pushCandidate(pathWin32.join(systemRoot, "System32", "where.exe"));
2085
+ pushCandidate(pathWin32.join(systemRoot, "Sysnative", "where.exe"));
2086
+ }
2087
+ pushCandidate("where.exe");
2088
+ pushCandidate("where");
2089
+ return candidates;
2090
+ }
2091
+ function quoteWindowsCmdArg(value) {
2092
+ const text = String(value ?? "");
2093
+ if (!text.length)
2094
+ return '""';
2095
+ if (!/[ \t"]/.test(text))
2096
+ return text;
2097
+ const escaped = text.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1");
2098
+ return `"${escaped}"`;
2099
+ }
2002
2100
  function isOptionalEmbeddedService(name) {
2003
2101
  return name === "source_control_manager";
2004
2102
  }
@@ -2017,12 +2115,107 @@ async function canSpawnCommand(command, cwd, env) {
2017
2115
  return false;
2018
2116
  }
2019
2117
  }
2020
- async function repoHasRemote(repoRoot, remote) {
2021
- const normalizedRemote = remote.trim();
2022
- if (!normalizedRemote)
2118
+ async function canSpawnGitViaWindowsShell(commandArgs, cwd, env, platform = process.platform) {
2119
+ if (platform !== "win32")
2023
2120
  return false;
2024
- const result = await runGit(["remote", "get-url", normalizedRemote], repoRoot);
2025
- return result.ok && Boolean(result.stdout);
2121
+ const commandLine = commandArgs.map((arg) => quoteWindowsCmdArg(arg)).join(" ");
2122
+ for (const shellExecutable of resolveWindowsShellExecutableCandidatesForEnv(env, platform)) {
2123
+ try {
2124
+ const proc = Bun.spawn([shellExecutable, "/d", "/s", "/c", commandLine], {
2125
+ cwd,
2126
+ env,
2127
+ stdin: "ignore",
2128
+ stdout: "ignore",
2129
+ stderr: "ignore"
2130
+ });
2131
+ const exitCode = await proc.exited;
2132
+ return exitCode === 0;
2133
+ } catch {}
2134
+ }
2135
+ return false;
2136
+ }
2137
+ async function resolveSourceControlManagerGitProbe(cwd, env, platform = process.platform) {
2138
+ const candidates = resolveRuntimeGitExecutableCandidates(env, platform);
2139
+ for (const candidate of candidates) {
2140
+ if (await canSpawnCommand([candidate, "--version"], cwd, env)) {
2141
+ return { ok: true, detail: candidate };
2142
+ }
2143
+ }
2144
+ if (platform === "win32") {
2145
+ for (const candidate of candidates) {
2146
+ if (await canSpawnGitViaWindowsShell([candidate, "--version"], cwd, env, platform)) {
2147
+ return { ok: true, detail: `${candidate} via shell` };
2148
+ }
2149
+ }
2150
+ }
2151
+ return {
2152
+ ok: false,
2153
+ detail: candidates.join(", ") || "git"
2154
+ };
2155
+ }
2156
+ async function precheckSourceControlManagerGitAvailability(opts) {
2157
+ const platform = opts.platform ?? process.platform;
2158
+ const env = buildEmbeddedRuntimeEnv(opts.baseEnv ?? process.env, {
2159
+ repoRoot: opts.repoRoot,
2160
+ runtimeRoot: opts.runtimeRoot,
2161
+ useRuntimeConfig: opts.preflightUsesEmbeddedRuntime,
2162
+ sessionId: opts.sessionId
2163
+ });
2164
+ if (env.PUSHPALS_GIT_BIN) {
2165
+ applyResolvedGitBinaryToRuntimeEnv(env, env.PUSHPALS_GIT_BIN, platform);
2166
+ }
2167
+ 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);
2168
+ if (remoteStatus.status === "missing_remote") {
2169
+ return {
2170
+ status: "skipped",
2171
+ detail: `git remote "${opts.remote}" is not configured`,
2172
+ env
2173
+ };
2174
+ }
2175
+ if (remoteStatus.status === "error") {
2176
+ return {
2177
+ status: "failed",
2178
+ detail: `git remote "${opts.remote}" could not be inspected: ${remoteStatus.detail}`,
2179
+ env
2180
+ };
2181
+ }
2182
+ const gitLookupCommand = typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? env.PUSHPALS_GIT_BIN.trim() : platform === "win32" ? "git.exe" : "git";
2183
+ const resolvedGitBinary = await (opts.resolveCommandPathFn ?? resolveCommandPath)(gitLookupCommand, opts.repoRoot, env);
2184
+ if (resolvedGitBinary) {
2185
+ applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform);
2186
+ }
2187
+ const gitProbe = await (opts.gitProbeFn ?? resolveSourceControlManagerGitProbe)(opts.repoRoot, env, platform);
2188
+ if (!gitProbe.ok) {
2189
+ return {
2190
+ status: "failed",
2191
+ detail: gitProbe.detail,
2192
+ env
2193
+ };
2194
+ }
2195
+ return {
2196
+ status: "ok",
2197
+ detail: gitProbe.detail,
2198
+ env
2199
+ };
2200
+ }
2201
+ async function checkGitRemoteConfigured(repoRoot, remote, env) {
2202
+ const normalizedRemote = String(remote ?? "").trim();
2203
+ if (!normalizedRemote) {
2204
+ return { status: "missing_remote", remote: normalizedRemote };
2205
+ }
2206
+ const result = await runGitWithEnv(["remote", "get-url", normalizedRemote], repoRoot, env ?? {
2207
+ ...process.env,
2208
+ GIT_TERMINAL_PROMPT: "0",
2209
+ GCM_INTERACTIVE: "Never"
2210
+ });
2211
+ if (result.ok && result.stdout) {
2212
+ return { status: "ok", remote: normalizedRemote };
2213
+ }
2214
+ const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
2215
+ if (/no such remote/i.test(detail)) {
2216
+ return { status: "missing_remote", remote: normalizedRemote };
2217
+ }
2218
+ return { status: "error", remote: normalizedRemote, detail };
2026
2219
  }
2027
2220
  async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
2028
2221
  const normalizedRemote = String(remote ?? "").trim();
@@ -2030,10 +2223,18 @@ async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
2030
2223
  if (!normalizedRemote || !normalizedBranch) {
2031
2224
  return { status: "ok" };
2032
2225
  }
2033
- const hasRemote = await repoHasRemote(repoRoot, normalizedRemote);
2034
- if (!hasRemote) {
2226
+ const remoteStatus = await checkGitRemoteConfigured(repoRoot, normalizedRemote);
2227
+ if (remoteStatus.status === "missing_remote") {
2035
2228
  return { status: "missing_remote", remote: normalizedRemote };
2036
2229
  }
2230
+ if (remoteStatus.status === "error") {
2231
+ return {
2232
+ status: "error",
2233
+ remote: normalizedRemote,
2234
+ branch: normalizedBranch,
2235
+ detail: remoteStatus.detail
2236
+ };
2237
+ }
2037
2238
  const ref = `refs/heads/${normalizedBranch}`;
2038
2239
  const result = await runGit(["ls-remote", "--heads", normalizedRemote, ref], repoRoot);
2039
2240
  if (!result.ok) {
@@ -2070,6 +2271,151 @@ async function enforcePushpalsRemoteBranchPrecheck(repoRoot, remote, branch) {
2070
2271
  console.error(`[pushpals] Precheck failed: could not verify remote branch "${result.remote}/${result.branch}": ${result.detail}`);
2071
2272
  return false;
2072
2273
  }
2274
+ function isPathEqualOrWithin(parentPath, childPath) {
2275
+ const parent = normalizeRepoPathForComparison(parentPath);
2276
+ const child = normalizeRepoPathForComparison(childPath);
2277
+ return child === parent || child.startsWith(`${parent}/`);
2278
+ }
2279
+ function appendCliClearTarget(targets, label, pathValue) {
2280
+ const resolvedPath = String(pathValue ?? "").trim();
2281
+ if (!resolvedPath)
2282
+ return;
2283
+ const normalized = normalizeRepoPathForComparison(resolvedPath);
2284
+ if (targets.some((target) => normalizeRepoPathForComparison(target.path) === normalized))
2285
+ return;
2286
+ targets.push({ label, path: resolve4(resolvedPath) });
2287
+ }
2288
+ function buildCliClearTargets(opts) {
2289
+ const targets = [];
2290
+ const dataDir = resolve4(opts.config.paths.dataDir);
2291
+ appendCliClearTarget(targets, "runtime data", dataDir);
2292
+ const scmStateDir = resolve4(opts.config.sourceControlManager.stateDir);
2293
+ if (!isPathEqualOrWithin(dataDir, scmStateDir)) {
2294
+ appendCliClearTarget(targets, "SourceControlManager state", scmStateDir);
2295
+ }
2296
+ const scmRepoPath = resolve4(opts.config.sourceControlManager.repoPath);
2297
+ if (normalizeRepoPathForComparison(scmRepoPath) !== normalizeRepoPathForComparison(opts.repoRoot) && isPathEqualOrWithin(opts.repoRoot, scmRepoPath)) {
2298
+ appendCliClearTarget(targets, "SourceControlManager worktree", scmRepoPath);
2299
+ }
2300
+ appendCliClearTarget(targets, "CLI state file", opts.cliStatePath ?? null);
2301
+ appendCliClearTarget(targets, "client monitor state file", resolveGitStateFilePath(opts.repoRoot, "pushpals-client-state.json"));
2302
+ appendCliClearTarget(targets, "runtime bootstrap logs", join2(opts.runtimeRoot, "logs", "bootstrap"));
2303
+ return targets;
2304
+ }
2305
+ function removeCliClearTarget(target) {
2306
+ if (!existsSync4(target.path))
2307
+ return "missing";
2308
+ try {
2309
+ rmSync(target.path, { recursive: true, force: true });
2310
+ return "removed";
2311
+ } catch (err) {
2312
+ return {
2313
+ ...target,
2314
+ detail: err instanceof Error ? err.message : String(err)
2315
+ };
2316
+ }
2317
+ }
2318
+ async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2319
+ if (!await probeServer(serverUrl)) {
2320
+ return { attempted: false, accepted: false };
2321
+ }
2322
+ try {
2323
+ await ensureServerRepoAffinity(serverUrl, repoRoot);
2324
+ } catch (err) {
2325
+ return {
2326
+ attempted: false,
2327
+ accepted: false,
2328
+ detail: `skipping shutdown because ${String(err)}`
2329
+ };
2330
+ }
2331
+ try {
2332
+ const response = await fetchWithTimeout(`${serverUrl}/admin/shutdown`, {
2333
+ method: "POST",
2334
+ headers: { "Content-Type": "application/json" },
2335
+ body: JSON.stringify({ reason: "pushpals --clear" })
2336
+ }, 5000);
2337
+ if (!response.ok) {
2338
+ const detail = await response.text().catch(() => "");
2339
+ return {
2340
+ attempted: true,
2341
+ accepted: false,
2342
+ detail: `HTTP ${response.status}${detail ? ` ${detail}` : ""}`
2343
+ };
2344
+ }
2345
+ return { attempted: true, accepted: true };
2346
+ } catch (err) {
2347
+ return {
2348
+ attempted: true,
2349
+ accepted: false,
2350
+ detail: err instanceof Error ? err.message : String(err)
2351
+ };
2352
+ }
2353
+ }
2354
+ async function clearPushpalsState(opts) {
2355
+ console.log("[pushpals] Clear requested. Removing repo-local PushPals state.");
2356
+ const shutdown = await requestLocalRuntimeShutdownForClear(opts.serverUrl, opts.repoRoot);
2357
+ if (shutdown.attempted && shutdown.accepted) {
2358
+ console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
2359
+ await Bun.sleep(1500);
2360
+ } else if (shutdown.attempted) {
2361
+ console.warn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
2362
+ } else if (shutdown.detail) {
2363
+ console.warn(`[pushpals] ${shutdown.detail}`);
2364
+ }
2365
+ const targets = buildCliClearTargets({
2366
+ repoRoot: opts.repoRoot,
2367
+ runtimeRoot: opts.runtimeRoot,
2368
+ config: opts.config,
2369
+ cliStatePath: opts.cliStatePath
2370
+ });
2371
+ const removed = [];
2372
+ const missing = [];
2373
+ let failed = [];
2374
+ for (const target of targets) {
2375
+ const result = removeCliClearTarget(target);
2376
+ if (result === "removed") {
2377
+ removed.push(target);
2378
+ continue;
2379
+ }
2380
+ if (result === "missing") {
2381
+ missing.push(target);
2382
+ continue;
2383
+ }
2384
+ failed.push(result);
2385
+ }
2386
+ if (failed.length > 0 && shutdown.accepted) {
2387
+ await Bun.sleep(1000);
2388
+ const retryFailures = [];
2389
+ for (const failure of failed) {
2390
+ const retry = removeCliClearTarget(failure);
2391
+ if (retry === "removed") {
2392
+ removed.push({ label: failure.label, path: failure.path });
2393
+ continue;
2394
+ }
2395
+ if (retry === "missing") {
2396
+ missing.push({ label: failure.label, path: failure.path });
2397
+ continue;
2398
+ }
2399
+ retryFailures.push(retry);
2400
+ }
2401
+ failed = retryFailures;
2402
+ }
2403
+ for (const target of removed) {
2404
+ console.log(`[pushpals] Cleared ${target.label}: ${target.path}`);
2405
+ }
2406
+ for (const target of missing) {
2407
+ console.log(`[pushpals] Nothing to clear for ${target.label}: ${target.path}`);
2408
+ }
2409
+ for (const failure of failed) {
2410
+ console.error(`[pushpals] Failed to clear ${failure.label}: ${failure.path} (${failure.detail})`);
2411
+ }
2412
+ if (failed.length > 0) {
2413
+ console.error("[pushpals] Clear completed with errors.");
2414
+ return 1;
2415
+ }
2416
+ console.log("[pushpals] Clear completed.");
2417
+ return 0;
2418
+ }
2073
2419
  async function probeServer(serverUrl) {
2074
2420
  try {
2075
2421
  const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
@@ -2284,6 +2630,7 @@ async function autoStartRuntimeServices(opts) {
2284
2630
  useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime,
2285
2631
  sessionId: opts.sessionId
2286
2632
  });
2633
+ runtimeEnv.PUSHPALS_WORKERPALS_BIN = runtimeBinaries.workerpals;
2287
2634
  if (runtimeEnv.PUSHPALS_GIT_BIN) {
2288
2635
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, runtimeEnv.PUSHPALS_GIT_BIN);
2289
2636
  }
@@ -2374,27 +2721,25 @@ ${tail}` : ""}`);
2374
2721
  };
2375
2722
  reportRemoteBuddyAutonomousEngineState();
2376
2723
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
2377
- const scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
2378
- 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";
2379
- const gitProbeCommand = [gitForScm, "--version"];
2380
- const gitAvailableForScm = await canSpawnCommand(gitProbeCommand, opts.repoRoot, runtimeEnv);
2381
- if (!scmHealthy && scmRemoteAvailable) {
2382
- if (!gitAvailableForScm) {
2724
+ const scmGitProbe = await resolveSourceControlManagerGitProbe(opts.repoRoot, runtimeEnv, process.platform);
2725
+ const scmRemoteStatus = await checkGitRemoteConfigured(opts.repoRoot, opts.sourceControlManagerRemote, runtimeEnv);
2726
+ if (!scmHealthy) {
2727
+ if (!scmGitProbe.ok) {
2383
2728
  console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
2384
- appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
2385
- } else {
2386
- console.log(`[pushpals] Embedded SourceControlManager git=${gitForScm}`);
2729
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env (${scmGitProbe.detail}).`);
2730
+ } else if (scmRemoteStatus.status === "error") {
2731
+ console.warn(`[pushpals] Could not inspect SourceControlManager git remote "${opts.sourceControlManagerRemote}"; skipping SCM startup.`);
2732
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: remote "${opts.sourceControlManagerRemote}" could not be inspected (${scmRemoteStatus.detail}).`);
2733
+ } else if (scmRemoteStatus.status === "ok") {
2734
+ console.log(`[pushpals] Embedded SourceControlManager git=${scmGitProbe.detail}`);
2387
2735
  console.log("[pushpals] Starting embedded SourceControlManager...");
2388
2736
  const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, serviceLogPaths.source_control_manager, runtimeServicesLogPath);
2389
2737
  services.push(sourceControlManagerService);
2390
2738
  console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
2739
+ } else {
2740
+ console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
2741
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
2391
2742
  }
2392
- } else if (!scmRemoteAvailable) {
2393
- console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
2394
- appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
2395
- } else if (!gitAvailableForScm) {
2396
- console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
2397
- appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env.");
2398
2743
  } else {
2399
2744
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
2400
2745
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager already healthy; embedded start skipped.");
@@ -2461,7 +2806,10 @@ ${tail}` : ""}`);
2461
2806
  }
2462
2807
  console.log("[pushpals] Embedded runtime is ready.");
2463
2808
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded runtime is ready.");
2464
- return services;
2809
+ return {
2810
+ services,
2811
+ pushpalsLogPath: runtimeServicesLogPath
2812
+ };
2465
2813
  }
2466
2814
  await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
2467
2815
  }
@@ -2492,6 +2840,7 @@ function readCliState(pathValue) {
2492
2840
  localAgentUrl: typeof parsed.localAgentUrl === "string" ? parsed.localAgentUrl : undefined,
2493
2841
  sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : undefined,
2494
2842
  repoRoot: typeof parsed.repoRoot === "string" ? parsed.repoRoot : undefined,
2843
+ pushpalsLogPath: typeof parsed.pushpalsLogPath === "string" ? parsed.pushpalsLogPath : undefined,
2495
2844
  updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined
2496
2845
  };
2497
2846
  } catch {
@@ -3006,6 +3355,19 @@ async function main() {
3006
3355
  runtimeRoot: parsed.runtimeRoot,
3007
3356
  runtimeTag: parsed.runtimeTag
3008
3357
  });
3358
+ const config = preparedRuntime.runtimePreflight.config;
3359
+ const statePath = resolveCliStatePath(repoRoot);
3360
+ if (parsed.clear) {
3361
+ const serverUrl2 = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
3362
+ const exitCode = await clearPushpalsState({
3363
+ repoRoot,
3364
+ runtimeRoot: preparedRuntime.runtimeRoot,
3365
+ config,
3366
+ serverUrl: serverUrl2,
3367
+ cliStatePath: statePath
3368
+ });
3369
+ process.exit(exitCode);
3370
+ }
3009
3371
  console.log("[pushpals] Running runtime preflight...");
3010
3372
  console.log(`[pushpals] runtimeRoot=${preparedRuntime.runtimeRoot}`);
3011
3373
  if (preparedRuntime.runtimeTag) {
@@ -3019,12 +3381,21 @@ async function main() {
3019
3381
  if (!preparedRuntime.runtimePreflight.ok) {
3020
3382
  process.exit(1);
3021
3383
  }
3022
- const config = preparedRuntime.runtimePreflight.config;
3023
3384
  if (config.remotebuddy.autonomy.enabled) {
3024
3385
  console.log("[pushpals] RemoteBuddy autonomy is enabled for CLI.");
3025
3386
  } else {
3026
3387
  console.warn("[pushpals] RemoteBuddy autonomy is disabled in config (remotebuddy.autonomy.enabled=false); continuing.");
3027
3388
  }
3389
+ const scmGitPrecheck = await precheckSourceControlManagerGitAvailability({
3390
+ repoRoot,
3391
+ remote: config.sourceControlManager.remote,
3392
+ runtimeRoot: preparedRuntime.runtimeRoot,
3393
+ preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime
3394
+ });
3395
+ if (scmGitPrecheck.status === "failed") {
3396
+ console.error(`[pushpals] Precheck failed: embedded SourceControlManager git command is unavailable (${scmGitPrecheck.detail}).`);
3397
+ process.exit(1);
3398
+ }
3028
3399
  const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
3029
3400
  if (!precheckPassed) {
3030
3401
  process.exit(1);
@@ -3042,6 +3413,7 @@ async function main() {
3042
3413
  repoRoot
3043
3414
  };
3044
3415
  let autoStartedServices = [];
3416
+ let pushpalsLogPath;
3045
3417
  const stopAutoStartedServices = () => {
3046
3418
  if (autoStartedServices.length === 0)
3047
3419
  return;
@@ -3057,7 +3429,7 @@ async function main() {
3057
3429
  if (!serverHealthy) {
3058
3430
  if (!parsed.noAutoStart) {
3059
3431
  try {
3060
- autoStartedServices = await autoStartRuntimeServices({
3432
+ const startedRuntime = await autoStartRuntimeServices({
3061
3433
  repoRoot,
3062
3434
  serverUrl,
3063
3435
  localAgentUrl,
@@ -3068,6 +3440,8 @@ async function main() {
3068
3440
  requestedRuntimeTag: parsed.runtimeTag,
3069
3441
  startLocalBuddy: resolveCliLocalBuddyAutostart(parsed.runtimeOnly, Boolean(config.localbuddy.enabled))
3070
3442
  });
3443
+ autoStartedServices = startedRuntime.services;
3444
+ pushpalsLogPath = startedRuntime.pushpalsLogPath;
3071
3445
  serverHealthy = await probeServer(serverUrl);
3072
3446
  } catch (err) {
3073
3447
  console.error(`[pushpals] Auto-start failed: ${String(err)}`);
@@ -3119,8 +3493,8 @@ async function main() {
3119
3493
  }
3120
3494
  process.exit(1);
3121
3495
  }
3122
- const statePath = resolveCliStatePath(repoRoot);
3123
3496
  const saved = statePath ? readCliState(statePath) : {};
3497
+ pushpalsLogPath = pushpalsLogPath || (typeof saved.pushpalsLogPath === "string" ? saved.pushpalsLogPath : undefined);
3124
3498
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
3125
3499
  const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
3126
3500
  const monitoringHub = await resolveMonitoringHub({
@@ -3136,7 +3510,8 @@ async function main() {
3136
3510
  serverUrl,
3137
3511
  localAgentUrl,
3138
3512
  sessionId: activeSessionId,
3139
- repoRoot
3513
+ repoRoot,
3514
+ pushpalsLogPath
3140
3515
  });
3141
3516
  } else {
3142
3517
  console.warn("[pushpals] Could not resolve git metadata dir; skipping CLI state persistence.");
@@ -3153,6 +3528,7 @@ async function main() {
3153
3528
  console.log(`[pushpals] serverUrl=${serverUrl}`);
3154
3529
  console.log(`[pushpals] sessionId=${activeSessionId}`);
3155
3530
  console.log(`[pushpals] repoRoot=${repoRoot}`);
3531
+ console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
3156
3532
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
3157
3533
  if (parsed.runtimeOnly) {
3158
3534
  console.log("[pushpals] runtimeOnly=true");
@@ -3253,6 +3629,7 @@ ${line}
3253
3629
  console.log(`[pushpals] serverUrl=${serverUrl}`);
3254
3630
  console.log(`[pushpals] sessionId=${activeSessionId}`);
3255
3631
  console.log(`[pushpals] repoRoot=${repoRoot}`);
3632
+ console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
3256
3633
  console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
3257
3634
  rl.prompt();
3258
3635
  continue;
@@ -3291,12 +3668,17 @@ if (import.meta.main) {
3291
3668
  }
3292
3669
  export {
3293
3670
  startEmbeddedMonitoringHub,
3671
+ resolveWindowsWhereExecutableCandidatesForEnv,
3672
+ resolveWindowsShellExecutableCandidatesForEnv,
3673
+ resolveRuntimeGitExecutableCandidates,
3674
+ resolvePreferredRuntimeReleaseTag,
3294
3675
  resolveCommandPath,
3295
3676
  resolveCliStatePath,
3296
3677
  resolveCliLocalBuddyAutostart,
3297
3678
  resolveBundledRuntimeAssetSource,
3298
3679
  resolveBundledMonitoringHubRoot,
3299
3680
  prepareCliRuntime,
3681
+ precheckSourceControlManagerGitAvailability,
3300
3682
  normalizeRepoPathForComparison,
3301
3683
  normalizeCliInteractiveMessage,
3302
3684
  normalizeChildProcessEnv,
@@ -3312,5 +3694,6 @@ export {
3312
3694
  buildOpenMonitoringHubCommand,
3313
3695
  buildEmbeddedRuntimeEnv,
3314
3696
  buildEmbeddedMonitoringHubHtml,
3697
+ buildCliClearTargets,
3315
3698
  applyResolvedGitBinaryToRuntimeEnv
3316
3699
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {