@pushpalsdev/cli 1.0.16 → 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 +427 -58
  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, {
@@ -2001,6 +2020,83 @@ function applyResolvedGitBinaryToRuntimeEnv(env, resolvedGitBinary, platform = p
2001
2020
  }
2002
2021
  return env;
2003
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
+ }
2004
2100
  function isOptionalEmbeddedService(name) {
2005
2101
  return name === "source_control_manager";
2006
2102
  }
@@ -2019,12 +2115,107 @@ async function canSpawnCommand(command, cwd, env) {
2019
2115
  return false;
2020
2116
  }
2021
2117
  }
2022
- async function repoHasRemote(repoRoot, remote) {
2023
- const normalizedRemote = remote.trim();
2024
- if (!normalizedRemote)
2118
+ async function canSpawnGitViaWindowsShell(commandArgs, cwd, env, platform = process.platform) {
2119
+ if (platform !== "win32")
2025
2120
  return false;
2026
- const result = await runGit(["remote", "get-url", normalizedRemote], repoRoot);
2027
- 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 };
2028
2219
  }
2029
2220
  async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
2030
2221
  const normalizedRemote = String(remote ?? "").trim();
@@ -2032,10 +2223,18 @@ async function checkPushpalsBranchOnRemote(repoRoot, remote, branch) {
2032
2223
  if (!normalizedRemote || !normalizedBranch) {
2033
2224
  return { status: "ok" };
2034
2225
  }
2035
- const hasRemote = await repoHasRemote(repoRoot, normalizedRemote);
2036
- if (!hasRemote) {
2226
+ const remoteStatus = await checkGitRemoteConfigured(repoRoot, normalizedRemote);
2227
+ if (remoteStatus.status === "missing_remote") {
2037
2228
  return { status: "missing_remote", remote: normalizedRemote };
2038
2229
  }
2230
+ if (remoteStatus.status === "error") {
2231
+ return {
2232
+ status: "error",
2233
+ remote: normalizedRemote,
2234
+ branch: normalizedBranch,
2235
+ detail: remoteStatus.detail
2236
+ };
2237
+ }
2039
2238
  const ref = `refs/heads/${normalizedBranch}`;
2040
2239
  const result = await runGit(["ls-remote", "--heads", normalizedRemote, ref], repoRoot);
2041
2240
  if (!result.ok) {
@@ -2072,6 +2271,151 @@ async function enforcePushpalsRemoteBranchPrecheck(repoRoot, remote, branch) {
2072
2271
  console.error(`[pushpals] Precheck failed: could not verify remote branch "${result.remote}/${result.branch}": ${result.detail}`);
2073
2272
  return false;
2074
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
+ }
2075
2419
  async function probeServer(serverUrl) {
2076
2420
  try {
2077
2421
  const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
@@ -2377,27 +2721,25 @@ ${tail}` : ""}`);
2377
2721
  };
2378
2722
  reportRemoteBuddyAutonomousEngineState();
2379
2723
  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) {
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) {
2386
2728
  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}`);
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}`);
2390
2735
  console.log("[pushpals] Starting embedded SourceControlManager...");
2391
2736
  const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, serviceLogPaths.source_control_manager, runtimeServicesLogPath);
2392
2737
  services.push(sourceControlManagerService);
2393
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}".`);
2394
2742
  }
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
2743
  } else {
2402
2744
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
2403
2745
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager already healthy; embedded start skipped.");
@@ -3013,6 +3355,19 @@ async function main() {
3013
3355
  runtimeRoot: parsed.runtimeRoot,
3014
3356
  runtimeTag: parsed.runtimeTag
3015
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
+ }
3016
3371
  console.log("[pushpals] Running runtime preflight...");
3017
3372
  console.log(`[pushpals] runtimeRoot=${preparedRuntime.runtimeRoot}`);
3018
3373
  if (preparedRuntime.runtimeTag) {
@@ -3026,12 +3381,21 @@ async function main() {
3026
3381
  if (!preparedRuntime.runtimePreflight.ok) {
3027
3382
  process.exit(1);
3028
3383
  }
3029
- const config = preparedRuntime.runtimePreflight.config;
3030
3384
  if (config.remotebuddy.autonomy.enabled) {
3031
3385
  console.log("[pushpals] RemoteBuddy autonomy is enabled for CLI.");
3032
3386
  } else {
3033
3387
  console.warn("[pushpals] RemoteBuddy autonomy is disabled in config (remotebuddy.autonomy.enabled=false); continuing.");
3034
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
+ }
3035
3399
  const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
3036
3400
  if (!precheckPassed) {
3037
3401
  process.exit(1);
@@ -3129,7 +3493,6 @@ async function main() {
3129
3493
  }
3130
3494
  process.exit(1);
3131
3495
  }
3132
- const statePath = resolveCliStatePath(repoRoot);
3133
3496
  const saved = statePath ? readCliState(statePath) : {};
3134
3497
  pushpalsLogPath = pushpalsLogPath || (typeof saved.pushpalsLogPath === "string" ? saved.pushpalsLogPath : undefined);
3135
3498
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -3305,12 +3668,17 @@ if (import.meta.main) {
3305
3668
  }
3306
3669
  export {
3307
3670
  startEmbeddedMonitoringHub,
3671
+ resolveWindowsWhereExecutableCandidatesForEnv,
3672
+ resolveWindowsShellExecutableCandidatesForEnv,
3673
+ resolveRuntimeGitExecutableCandidates,
3674
+ resolvePreferredRuntimeReleaseTag,
3308
3675
  resolveCommandPath,
3309
3676
  resolveCliStatePath,
3310
3677
  resolveCliLocalBuddyAutostart,
3311
3678
  resolveBundledRuntimeAssetSource,
3312
3679
  resolveBundledMonitoringHubRoot,
3313
3680
  prepareCliRuntime,
3681
+ precheckSourceControlManagerGitAvailability,
3314
3682
  normalizeRepoPathForComparison,
3315
3683
  normalizeCliInteractiveMessage,
3316
3684
  normalizeChildProcessEnv,
@@ -3326,5 +3694,6 @@ export {
3326
3694
  buildOpenMonitoringHubCommand,
3327
3695
  buildEmbeddedRuntimeEnv,
3328
3696
  buildEmbeddedMonitoringHubHtml,
3697
+ buildCliClearTargets,
3329
3698
  applyResolvedGitBinaryToRuntimeEnv
3330
3699
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {