@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.
- package/dist/pushpals-cli.js +427 -58
- package/package.json +1 -1
package/dist/pushpals-cli.js
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
mkdirSync,
|
|
12
12
|
readdirSync,
|
|
13
13
|
readFileSync as readFileSync4,
|
|
14
|
+
rmSync,
|
|
14
15
|
writeFileSync
|
|
15
16
|
} from "fs";
|
|
16
|
-
import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4 } from "path";
|
|
17
|
+
import { basename, delimiter, dirname, extname, join as join2, resolve as resolve4, win32 as pathWin32 } from "path";
|
|
17
18
|
import { createInterface } from "readline";
|
|
18
19
|
|
|
19
20
|
// ../shared/src/client_preflight.ts
|
|
@@ -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 = {
|
|
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
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
-
|
|
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(
|
|
1565
|
+
const fromEnv = String(env.PUSHPALS_RUNTIME_TAG ?? "").trim();
|
|
1543
1566
|
if (fromEnv)
|
|
1544
1567
|
return fromEnv;
|
|
1545
|
-
const packageVersion = parseSemverFromPackageVersion(
|
|
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
|
-
|
|
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
|
|
2023
|
-
|
|
2024
|
-
if (!normalizedRemote)
|
|
2118
|
+
async function canSpawnGitViaWindowsShell(commandArgs, cwd, env, platform = process.platform) {
|
|
2119
|
+
if (platform !== "win32")
|
|
2025
2120
|
return false;
|
|
2026
|
-
const
|
|
2027
|
-
|
|
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
|
|
2036
|
-
if (
|
|
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
|
|
2381
|
-
const
|
|
2382
|
-
|
|
2383
|
-
|
|
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,
|
|
2388
|
-
} else {
|
|
2389
|
-
console.
|
|
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
|
};
|