@pushpalsdev/cli 1.0.1 → 1.0.3
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/README.md +9 -1
- package/bin/pushpals.cjs +18 -3
- package/dist/pushpals-cli.js +322 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,10 +25,18 @@ pushpals
|
|
|
25
25
|
The CLI hard-fails if:
|
|
26
26
|
|
|
27
27
|
- current directory is not a git repository
|
|
28
|
-
- LocalBuddy is unavailable
|
|
29
28
|
- LocalBuddy is attached to a different repo root
|
|
30
29
|
- Bun runtime is not installed (for npm-installed entrypoint execution)
|
|
31
30
|
|
|
31
|
+
Behavior:
|
|
32
|
+
|
|
33
|
+
- If LocalBuddy is unavailable, CLI auto-start bootstraps embedded
|
|
34
|
+
`server + localbuddy + remotebuddy + source_control_manager`.
|
|
35
|
+
- Auto-start does not clone the PushPals repo; it downloads release-tagged runtime binaries
|
|
36
|
+
and prompt/config assets into `~/.pushpals/runtime`.
|
|
37
|
+
- Override runtime tag with `pushpals --runtime-tag vX.Y.Z`.
|
|
38
|
+
- Disable auto-start with `pushpals --no-auto-start`.
|
|
39
|
+
|
|
32
40
|
## No npm/Bun install
|
|
33
41
|
|
|
34
42
|
Download native binaries from GitHub Releases:
|
package/bin/pushpals.cjs
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { spawn, spawnSync } = require("node:child_process");
|
|
5
|
-
const { existsSync } = require("node:fs");
|
|
5
|
+
const { existsSync, readFileSync } = require("node:fs");
|
|
6
6
|
const { resolve } = require("node:path");
|
|
7
7
|
|
|
8
8
|
const bundledCliPath = resolve(__dirname, "..", "dist", "pushpals-cli.js");
|
|
9
|
+
const packageJsonPath = resolve(__dirname, "..", "package.json");
|
|
9
10
|
const releaseUrl = "https://github.com/PushPalsDev/pushpals/releases";
|
|
11
|
+
let packageVersion = "";
|
|
12
|
+
if (existsSync(packageJsonPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
15
|
+
packageVersion = String(parsed?.version ?? "").trim();
|
|
16
|
+
} catch {
|
|
17
|
+
packageVersion = "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
function fail(lines) {
|
|
12
22
|
for (const line of lines) {
|
|
@@ -41,10 +51,15 @@ if (!hasBunRuntime()) {
|
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
function spawnBunCli() {
|
|
54
|
+
const childEnv = {
|
|
55
|
+
...process.env,
|
|
56
|
+
PUSHPALS_CLI_PACKAGE_VERSION: packageVersion || process.env.PUSHPALS_CLI_PACKAGE_VERSION || "",
|
|
57
|
+
};
|
|
58
|
+
|
|
44
59
|
if (process.platform !== "win32") {
|
|
45
60
|
return spawn("bun", [bundledCliPath, ...process.argv.slice(2)], {
|
|
46
61
|
stdio: "inherit",
|
|
47
|
-
env:
|
|
62
|
+
env: childEnv,
|
|
48
63
|
});
|
|
49
64
|
}
|
|
50
65
|
|
|
@@ -57,7 +72,7 @@ function spawnBunCli() {
|
|
|
57
72
|
return spawn(commandLine, {
|
|
58
73
|
shell: true,
|
|
59
74
|
stdio: "inherit",
|
|
60
|
-
env:
|
|
75
|
+
env: childEnv,
|
|
61
76
|
});
|
|
62
77
|
}
|
|
63
78
|
|
package/dist/pushpals-cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// ../../scripts/pushpals-cli.ts
|
|
5
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
6
|
-
import { dirname, resolve as resolve2 } from "path";
|
|
5
|
+
import { chmodSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
8
|
|
|
9
9
|
// ../shared/src/config.ts
|
|
@@ -250,10 +250,12 @@ function resolveLlmConfig(serviceNode, envPrefix, defaults, globalSessionId) {
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
function loadPushPalsConfig(options = {}) {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
253
|
+
const projectRootOverride = firstNonEmpty(options.projectRoot, process.env.PUSHPALS_PROJECT_ROOT_OVERRIDE, PROJECT_ROOT);
|
|
254
|
+
const projectRoot = resolve(projectRootOverride);
|
|
255
|
+
const configDirOverride = firstNonEmpty(options.configDir, process.env.PUSHPALS_CONFIG_DIR_OVERRIDE, "");
|
|
256
|
+
const configDir = resolveRuntimeConfigDir(projectRoot, configDirOverride);
|
|
255
257
|
const legacyConfigDir = resolvePathFromRoot(projectRoot, LEGACY_CONFIG_DIR);
|
|
256
|
-
const fallbackConfigDir = !
|
|
258
|
+
const fallbackConfigDir = !configDirOverride && configDir !== legacyConfigDir ? legacyConfigDir : "";
|
|
257
259
|
const cacheKey = `${projectRoot}::${configDir}::${process.env.PUSHPALS_PROFILE ?? ""}`;
|
|
258
260
|
if (!options.reload && cachedConfig && cachedConfigKey === cacheKey) {
|
|
259
261
|
return cachedConfig;
|
|
@@ -743,6 +745,16 @@ var MONITOR_SCAN_PORTS = 32;
|
|
|
743
745
|
var HTTP_TIMEOUT_MS = 2500;
|
|
744
746
|
var LOCALBUDDY_TIMEOUT_MS = 4000;
|
|
745
747
|
var SSE_RECONNECT_MS = 1500;
|
|
748
|
+
var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
749
|
+
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
750
|
+
var GITHUB_OWNER = "PushPalsDev";
|
|
751
|
+
var GITHUB_REPO = "pushpals";
|
|
752
|
+
var GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`;
|
|
753
|
+
var GITHUB_RELEASE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download`;
|
|
754
|
+
var GITHUB_HEADERS = {
|
|
755
|
+
Accept: "application/vnd.github+json",
|
|
756
|
+
"User-Agent": "pushpals-cli"
|
|
757
|
+
};
|
|
746
758
|
var stateVersion = 1;
|
|
747
759
|
function printUsage() {
|
|
748
760
|
console.log("PushPals CLI");
|
|
@@ -755,6 +767,9 @@ function printUsage() {
|
|
|
755
767
|
console.log(" --local-agent-url <url> Override LocalBuddy URL");
|
|
756
768
|
console.log(" --session-id <id> Override session ID");
|
|
757
769
|
console.log(" --hub-url <url> Override monitoring hub URL");
|
|
770
|
+
console.log(" --runtime-root <path> Override embedded runtime directory for auto-start");
|
|
771
|
+
console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
|
|
772
|
+
console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
|
|
758
773
|
console.log(" --no-stream Disable live session event stream");
|
|
759
774
|
console.log(" -h, --help Show this help");
|
|
760
775
|
console.log("");
|
|
@@ -766,10 +781,11 @@ function printUsage() {
|
|
|
766
781
|
console.log("");
|
|
767
782
|
console.log("Notes:");
|
|
768
783
|
console.log(" - Must be run from inside a git repository.");
|
|
769
|
-
console.log(" -
|
|
784
|
+
console.log(" - Auto-start can bootstrap server/localbuddy/remotebuddy/source_control_manager.");
|
|
785
|
+
console.log(" - LocalBuddy must be attached to the same repo root.");
|
|
770
786
|
}
|
|
771
787
|
function parseArgs(argv) {
|
|
772
|
-
const options = { noStream: false };
|
|
788
|
+
const options = { noAutoStart: false, noStream: false };
|
|
773
789
|
for (let i = 0;i < argv.length; i++) {
|
|
774
790
|
const arg = argv[i];
|
|
775
791
|
if (arg === "-h" || arg === "--help") {
|
|
@@ -780,6 +796,10 @@ function parseArgs(argv) {
|
|
|
780
796
|
options.noStream = true;
|
|
781
797
|
continue;
|
|
782
798
|
}
|
|
799
|
+
if (arg === "--no-auto-start") {
|
|
800
|
+
options.noAutoStart = true;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
783
803
|
if (arg === "--server-url") {
|
|
784
804
|
options.serverUrl = argv[++i];
|
|
785
805
|
continue;
|
|
@@ -796,6 +816,14 @@ function parseArgs(argv) {
|
|
|
796
816
|
options.monitoringHubUrl = argv[++i];
|
|
797
817
|
continue;
|
|
798
818
|
}
|
|
819
|
+
if (arg === "--runtime-root") {
|
|
820
|
+
options.runtimeRoot = argv[++i];
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (arg === "--runtime-tag") {
|
|
824
|
+
options.runtimeTag = argv[++i];
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
799
827
|
console.error(`[pushpals] Unknown argument: ${arg}`);
|
|
800
828
|
printUsage();
|
|
801
829
|
process.exit(2);
|
|
@@ -841,6 +869,202 @@ async function resolveCurrentGitRepoRoot(cwd) {
|
|
|
841
869
|
return null;
|
|
842
870
|
return resolve2(root.stdout);
|
|
843
871
|
}
|
|
872
|
+
function resolveDefaultRuntimeRoot() {
|
|
873
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
|
|
874
|
+
return resolve2(home, ".pushpals", "runtime");
|
|
875
|
+
}
|
|
876
|
+
function parseSemverFromPackageVersion(value) {
|
|
877
|
+
const raw = String(value ?? "").trim();
|
|
878
|
+
if (!raw)
|
|
879
|
+
return "";
|
|
880
|
+
const match = raw.match(/^\d+\.\d+\.\d+(?:[-.][0-9A-Za-z.-]+)?$/);
|
|
881
|
+
return match ? raw : "";
|
|
882
|
+
}
|
|
883
|
+
function resolveRuntimePlatformKey() {
|
|
884
|
+
if (process.platform === "win32")
|
|
885
|
+
return "windows-x64";
|
|
886
|
+
if (process.platform === "linux")
|
|
887
|
+
return "linux-x64";
|
|
888
|
+
if (process.platform === "darwin") {
|
|
889
|
+
return process.arch === "arm64" ? "macos-arm64" : "macos-x64";
|
|
890
|
+
}
|
|
891
|
+
throw new Error(`Unsupported platform for embedded runtime binaries: ${process.platform}/${process.arch}`);
|
|
892
|
+
}
|
|
893
|
+
async function fetchLatestReleaseTag() {
|
|
894
|
+
const response = await fetchWithTimeout(`${GITHUB_API_URL}/releases/latest`, { headers: GITHUB_HEADERS }, 20000);
|
|
895
|
+
if (!response.ok) {
|
|
896
|
+
throw new Error(`Failed to resolve latest release tag (HTTP ${response.status})`);
|
|
897
|
+
}
|
|
898
|
+
const payload = await response.json();
|
|
899
|
+
const tagName = String(payload.tag_name ?? "").trim();
|
|
900
|
+
if (!tagName)
|
|
901
|
+
throw new Error("Latest release payload did not include tag_name");
|
|
902
|
+
return tagName;
|
|
903
|
+
}
|
|
904
|
+
async function resolveRuntimeReleaseTag(explicitTag) {
|
|
905
|
+
const fromArg = String(explicitTag ?? "").trim();
|
|
906
|
+
if (fromArg)
|
|
907
|
+
return fromArg;
|
|
908
|
+
const fromEnv = String(process.env.PUSHPALS_RUNTIME_TAG ?? "").trim();
|
|
909
|
+
if (fromEnv)
|
|
910
|
+
return fromEnv;
|
|
911
|
+
const packageVersion = parseSemverFromPackageVersion(process.env.PUSHPALS_CLI_PACKAGE_VERSION);
|
|
912
|
+
if (packageVersion)
|
|
913
|
+
return `v${packageVersion}`;
|
|
914
|
+
return await fetchLatestReleaseTag();
|
|
915
|
+
}
|
|
916
|
+
function writeTextFileIfMissing(pathValue, text) {
|
|
917
|
+
if (existsSync2(pathValue))
|
|
918
|
+
return;
|
|
919
|
+
mkdirSync(dirname(pathValue), { recursive: true });
|
|
920
|
+
writeFileSync(pathValue, text, "utf8");
|
|
921
|
+
}
|
|
922
|
+
function copyBundledRuntimeAssets(runtimeRoot) {
|
|
923
|
+
const bundledRoot = resolve2(import.meta.dir, "..", "runtime");
|
|
924
|
+
if (!existsSync2(bundledRoot))
|
|
925
|
+
return false;
|
|
926
|
+
cpSync(bundledRoot, runtimeRoot, { recursive: true, force: true });
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
async function fetchTextFromUrl(url, timeoutMs = 20000) {
|
|
930
|
+
const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, timeoutMs);
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
throw new Error(`HTTP ${response.status} while fetching ${url}`);
|
|
933
|
+
}
|
|
934
|
+
return await response.text();
|
|
935
|
+
}
|
|
936
|
+
async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
|
|
937
|
+
const treeUrl = `${GITHUB_API_URL}/git/trees/${encodeURIComponent(tag)}?recursive=1`;
|
|
938
|
+
const treeResponse = await fetchWithTimeout(treeUrl, { headers: GITHUB_HEADERS }, 30000);
|
|
939
|
+
if (!treeResponse.ok) {
|
|
940
|
+
throw new Error(`Failed to fetch runtime source tree for ${tag} (HTTP ${treeResponse.status})`);
|
|
941
|
+
}
|
|
942
|
+
const treePayload = await treeResponse.json();
|
|
943
|
+
const paths = (treePayload.tree ?? []).filter((entry) => entry.type === "blob" && typeof entry.path === "string").map((entry) => String(entry.path)).filter((pathValue) => pathValue === ".env.example" || pathValue.startsWith("configs/") || pathValue.startsWith("prompts/"));
|
|
944
|
+
if (paths.length === 0) {
|
|
945
|
+
throw new Error(`Runtime source tree for ${tag} did not include prompts/config assets`);
|
|
946
|
+
}
|
|
947
|
+
const sorted = [...paths].sort((a, b) => a.localeCompare(b));
|
|
948
|
+
for (const pathValue of sorted) {
|
|
949
|
+
const rawUrl = `https://raw.githubusercontent.com/${GITHUB_OWNER}/${GITHUB_REPO}/${encodeURIComponent(tag)}/${pathValue}`;
|
|
950
|
+
const body = await fetchTextFromUrl(rawUrl, 20000);
|
|
951
|
+
const outPath = join2(runtimeRoot, pathValue);
|
|
952
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
953
|
+
writeFileSync(outPath, body, "utf8");
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
|
|
957
|
+
const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
|
|
958
|
+
const currentTag = existsSync2(markerPath) ? readFileSync2(markerPath, "utf8").trim() : "";
|
|
959
|
+
const hasAssets = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts"));
|
|
960
|
+
if (!hasAssets || currentTag !== runtimeTag) {
|
|
961
|
+
const copied = copyBundledRuntimeAssets(runtimeRoot);
|
|
962
|
+
if (!copied) {
|
|
963
|
+
await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
|
|
964
|
+
}
|
|
965
|
+
writeFileSync(markerPath, `${runtimeTag}
|
|
966
|
+
`, "utf8");
|
|
967
|
+
}
|
|
968
|
+
writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
|
|
969
|
+
`);
|
|
970
|
+
const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
|
|
971
|
+
if (existsSync2(localExamplePath)) {
|
|
972
|
+
writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync2(localExamplePath, "utf8"));
|
|
973
|
+
} else {
|
|
974
|
+
writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
|
|
975
|
+
`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function runtimeBinaryFilename(serviceName, platformKey) {
|
|
979
|
+
const serviceToken = serviceName === "source_control_manager" ? "source-control-manager" : serviceName;
|
|
980
|
+
const extension = platformKey.startsWith("windows-") ? ".exe" : "";
|
|
981
|
+
return `pushpals-runtime-${serviceToken}-${platformKey}${extension}`;
|
|
982
|
+
}
|
|
983
|
+
async function downloadBinaryAsset(tag, assetName, outPath) {
|
|
984
|
+
const url = `${GITHUB_RELEASE_URL}/${encodeURIComponent(tag)}/${assetName}`;
|
|
985
|
+
const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, 60000);
|
|
986
|
+
if (!response.ok) {
|
|
987
|
+
throw new Error(`Failed to download ${assetName} from ${tag} (HTTP ${response.status})`);
|
|
988
|
+
}
|
|
989
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
990
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
991
|
+
await Bun.write(outPath, bytes);
|
|
992
|
+
}
|
|
993
|
+
async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
|
|
994
|
+
const platformKey = resolveRuntimePlatformKey();
|
|
995
|
+
const binDir = join2(runtimeRoot, "bin", `${runtimeTag}-${platformKey}`);
|
|
996
|
+
mkdirSync(binDir, { recursive: true });
|
|
997
|
+
const runtimeBinaries = {
|
|
998
|
+
server: join2(binDir, runtimeBinaryFilename("server", platformKey)),
|
|
999
|
+
localbuddy: join2(binDir, runtimeBinaryFilename("localbuddy", platformKey)),
|
|
1000
|
+
remotebuddy: join2(binDir, runtimeBinaryFilename("remotebuddy", platformKey)),
|
|
1001
|
+
sourceControlManager: join2(binDir, runtimeBinaryFilename("source_control_manager", platformKey))
|
|
1002
|
+
};
|
|
1003
|
+
const requiredAssets = [
|
|
1004
|
+
runtimeBinaries.server,
|
|
1005
|
+
runtimeBinaries.localbuddy,
|
|
1006
|
+
runtimeBinaries.remotebuddy,
|
|
1007
|
+
runtimeBinaries.sourceControlManager
|
|
1008
|
+
];
|
|
1009
|
+
for (const binaryPath of requiredAssets) {
|
|
1010
|
+
if (existsSync2(binaryPath))
|
|
1011
|
+
continue;
|
|
1012
|
+
const assetName = binaryPath.split(/[\\/]/).pop() || "";
|
|
1013
|
+
await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
|
|
1014
|
+
}
|
|
1015
|
+
if (process.platform !== "win32") {
|
|
1016
|
+
for (const binaryPath of requiredAssets) {
|
|
1017
|
+
try {
|
|
1018
|
+
chmodSync(binaryPath, 493);
|
|
1019
|
+
} catch {}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return runtimeBinaries;
|
|
1023
|
+
}
|
|
1024
|
+
function spawnRuntimeService(name, command, cwd, env) {
|
|
1025
|
+
const proc = Bun.spawn(command, {
|
|
1026
|
+
cwd,
|
|
1027
|
+
env,
|
|
1028
|
+
stdout: "ignore",
|
|
1029
|
+
stderr: "ignore"
|
|
1030
|
+
});
|
|
1031
|
+
const service = {
|
|
1032
|
+
name,
|
|
1033
|
+
proc,
|
|
1034
|
+
exited: false,
|
|
1035
|
+
exitCode: null
|
|
1036
|
+
};
|
|
1037
|
+
proc.exited.then((code) => {
|
|
1038
|
+
service.exited = true;
|
|
1039
|
+
service.exitCode = code;
|
|
1040
|
+
});
|
|
1041
|
+
return service;
|
|
1042
|
+
}
|
|
1043
|
+
function stopRuntimeServices(services) {
|
|
1044
|
+
for (const service of services) {
|
|
1045
|
+
try {
|
|
1046
|
+
service.proc.kill();
|
|
1047
|
+
} catch {}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function probeServer(serverUrl) {
|
|
1051
|
+
try {
|
|
1052
|
+
const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
|
|
1053
|
+
return response.ok;
|
|
1054
|
+
} catch {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function probeSourceControlManager(port) {
|
|
1059
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
1060
|
+
return false;
|
|
1061
|
+
try {
|
|
1062
|
+
const response = await fetchWithTimeout(`http://127.0.0.1:${Math.floor(port)}/health`, {}, HTTP_TIMEOUT_MS);
|
|
1063
|
+
return response.ok;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
844
1068
|
async function fetchWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
845
1069
|
const controller = new AbortController;
|
|
846
1070
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -868,6 +1092,64 @@ function authHeaders(authToken) {
|
|
|
868
1092
|
async function probeLocalBuddy(localAgentUrl, authToken) {
|
|
869
1093
|
return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, { headers: authHeaders(authToken) }, LOCALBUDDY_TIMEOUT_MS);
|
|
870
1094
|
}
|
|
1095
|
+
async function autoStartRuntimeServices(opts) {
|
|
1096
|
+
const runtimeRoot = resolve2(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
|
|
1097
|
+
const runtimeTag = await resolveRuntimeReleaseTag(opts.runtimeTag);
|
|
1098
|
+
const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
|
|
1099
|
+
await ensureRuntimeAssets(runtimeRoot, runtimeTag);
|
|
1100
|
+
console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
|
|
1101
|
+
console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
|
|
1102
|
+
console.log(`[pushpals] runtimeTag=${runtimeTag}`);
|
|
1103
|
+
const runtimeEnv = {
|
|
1104
|
+
...process.env,
|
|
1105
|
+
PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
|
|
1106
|
+
PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
|
|
1107
|
+
PUSHPALS_CONFIG_DIR_OVERRIDE: join2(runtimeRoot, "configs"),
|
|
1108
|
+
PUSHPALS_PROMPTS_ROOT_OVERRIDE: runtimeRoot
|
|
1109
|
+
};
|
|
1110
|
+
const services = [];
|
|
1111
|
+
const serverHealthy = await probeServer(opts.serverUrl);
|
|
1112
|
+
if (!serverHealthy) {
|
|
1113
|
+
console.log("[pushpals] Starting embedded server...");
|
|
1114
|
+
services.push(spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv));
|
|
1115
|
+
} else {
|
|
1116
|
+
console.log("[pushpals] Server already healthy; skipping embedded server start.");
|
|
1117
|
+
}
|
|
1118
|
+
console.log("[pushpals] Starting embedded LocalBuddy...");
|
|
1119
|
+
services.push(spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv));
|
|
1120
|
+
console.log("[pushpals] Starting embedded RemoteBuddy...");
|
|
1121
|
+
services.push(spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv));
|
|
1122
|
+
const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
|
|
1123
|
+
if (!scmHealthy) {
|
|
1124
|
+
console.log("[pushpals] Starting embedded SourceControlManager...");
|
|
1125
|
+
services.push(spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv));
|
|
1126
|
+
} else {
|
|
1127
|
+
console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
|
|
1128
|
+
}
|
|
1129
|
+
const deadline = Date.now() + DEFAULT_RUNTIME_BOOT_TIMEOUT_MS;
|
|
1130
|
+
while (Date.now() < deadline) {
|
|
1131
|
+
for (let i = services.length - 1;i >= 0; i--) {
|
|
1132
|
+
const service = services[i];
|
|
1133
|
+
if (service.exited) {
|
|
1134
|
+
if (service.name === "source_control_manager") {
|
|
1135
|
+
console.warn(`[pushpals] Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); continuing without SCM.`);
|
|
1136
|
+
services.splice(i, 1);
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
stopRuntimeServices(services);
|
|
1140
|
+
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"})`);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
|
|
1144
|
+
if (health?.ok) {
|
|
1145
|
+
console.log("[pushpals] Embedded runtime is ready.");
|
|
1146
|
+
return services;
|
|
1147
|
+
}
|
|
1148
|
+
await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
1149
|
+
}
|
|
1150
|
+
stopRuntimeServices(services);
|
|
1151
|
+
throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
|
|
1152
|
+
}
|
|
871
1153
|
function readCliState(pathValue) {
|
|
872
1154
|
if (!existsSync2(pathValue))
|
|
873
1155
|
return {};
|
|
@@ -1129,18 +1411,48 @@ async function main() {
|
|
|
1129
1411
|
const localAgentUrl = normalizeUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
|
|
1130
1412
|
const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
|
|
1131
1413
|
const authToken = config.authToken;
|
|
1132
|
-
|
|
1414
|
+
let autoStartedServices = [];
|
|
1415
|
+
const stopAutoStartedServices = () => {
|
|
1416
|
+
if (autoStartedServices.length === 0)
|
|
1417
|
+
return;
|
|
1418
|
+
stopRuntimeServices(autoStartedServices);
|
|
1419
|
+
autoStartedServices = [];
|
|
1420
|
+
};
|
|
1421
|
+
let health = await probeLocalBuddy(localAgentUrl, authToken);
|
|
1422
|
+
if (!health?.ok && !parsed.noAutoStart) {
|
|
1423
|
+
try {
|
|
1424
|
+
autoStartedServices = await autoStartRuntimeServices({
|
|
1425
|
+
repoRoot,
|
|
1426
|
+
serverUrl,
|
|
1427
|
+
localAgentUrl,
|
|
1428
|
+
sourceControlManagerPort: config.sourceControlManager.port,
|
|
1429
|
+
authToken,
|
|
1430
|
+
runtimeRoot: parsed.runtimeRoot,
|
|
1431
|
+
runtimeTag: parsed.runtimeTag
|
|
1432
|
+
});
|
|
1433
|
+
health = await probeLocalBuddy(localAgentUrl, authToken);
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
console.error(`[pushpals] Auto-start failed: ${String(err)}`);
|
|
1436
|
+
stopAutoStartedServices();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1133
1439
|
if (!health?.ok) {
|
|
1134
1440
|
console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
|
|
1135
|
-
|
|
1441
|
+
if (parsed.noAutoStart) {
|
|
1442
|
+
console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
|
|
1443
|
+
} else {
|
|
1444
|
+
console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
|
|
1445
|
+
}
|
|
1136
1446
|
process.exit(1);
|
|
1137
1447
|
}
|
|
1138
1448
|
const localBuddyRepo = health.repo ? resolve2(health.repo) : "";
|
|
1139
1449
|
if (!localBuddyRepo) {
|
|
1450
|
+
stopAutoStartedServices();
|
|
1140
1451
|
console.error("[pushpals] LocalBuddy health response did not include repo path.");
|
|
1141
1452
|
process.exit(1);
|
|
1142
1453
|
}
|
|
1143
1454
|
if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
|
|
1455
|
+
stopAutoStartedServices();
|
|
1144
1456
|
console.error("[pushpals] Repo mismatch detected.");
|
|
1145
1457
|
console.error(`[pushpals] currentRepo=${repoRoot}`);
|
|
1146
1458
|
console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
|
|
@@ -1194,6 +1506,7 @@ ${line}
|
|
|
1194
1506
|
streamAbort.abort();
|
|
1195
1507
|
if (rl)
|
|
1196
1508
|
rl.close();
|
|
1509
|
+
stopAutoStartedServices();
|
|
1197
1510
|
};
|
|
1198
1511
|
process.once("SIGINT", requestStop);
|
|
1199
1512
|
process.once("SIGTERM", requestStop);
|