@pushpalsdev/cli 1.0.1 → 1.0.2
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 +7 -1
- package/dist/pushpals-cli.js +231 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,10 +25,16 @@ 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
|
+
- Runtime clone path defaults to `~/.pushpals/runtime`.
|
|
36
|
+
- Disable auto-start with `pushpals --no-auto-start`.
|
|
37
|
+
|
|
32
38
|
## No npm/Bun install
|
|
33
39
|
|
|
34
40
|
Download native binaries from GitHub Releases:
|
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 { copyFileSync, 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
|
|
@@ -743,6 +743,9 @@ var MONITOR_SCAN_PORTS = 32;
|
|
|
743
743
|
var HTTP_TIMEOUT_MS = 2500;
|
|
744
744
|
var LOCALBUDDY_TIMEOUT_MS = 4000;
|
|
745
745
|
var SSE_RECONNECT_MS = 1500;
|
|
746
|
+
var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
|
|
747
|
+
var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
|
|
748
|
+
var DEFAULT_RUNTIME_REPO = "https://github.com/PushPalsDev/pushpals.git";
|
|
746
749
|
var stateVersion = 1;
|
|
747
750
|
function printUsage() {
|
|
748
751
|
console.log("PushPals CLI");
|
|
@@ -755,6 +758,9 @@ function printUsage() {
|
|
|
755
758
|
console.log(" --local-agent-url <url> Override LocalBuddy URL");
|
|
756
759
|
console.log(" --session-id <id> Override session ID");
|
|
757
760
|
console.log(" --hub-url <url> Override monitoring hub URL");
|
|
761
|
+
console.log(" --runtime-root <path> Override embedded runtime directory for auto-start");
|
|
762
|
+
console.log(" --runtime-repo <url> Override embedded runtime git clone URL");
|
|
763
|
+
console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
|
|
758
764
|
console.log(" --no-stream Disable live session event stream");
|
|
759
765
|
console.log(" -h, --help Show this help");
|
|
760
766
|
console.log("");
|
|
@@ -766,10 +772,11 @@ function printUsage() {
|
|
|
766
772
|
console.log("");
|
|
767
773
|
console.log("Notes:");
|
|
768
774
|
console.log(" - Must be run from inside a git repository.");
|
|
769
|
-
console.log(" -
|
|
775
|
+
console.log(" - Auto-start can bootstrap server/localbuddy/remotebuddy/source_control_manager.");
|
|
776
|
+
console.log(" - LocalBuddy must be attached to the same repo root.");
|
|
770
777
|
}
|
|
771
778
|
function parseArgs(argv) {
|
|
772
|
-
const options = { noStream: false };
|
|
779
|
+
const options = { noAutoStart: false, noStream: false };
|
|
773
780
|
for (let i = 0;i < argv.length; i++) {
|
|
774
781
|
const arg = argv[i];
|
|
775
782
|
if (arg === "-h" || arg === "--help") {
|
|
@@ -780,6 +787,10 @@ function parseArgs(argv) {
|
|
|
780
787
|
options.noStream = true;
|
|
781
788
|
continue;
|
|
782
789
|
}
|
|
790
|
+
if (arg === "--no-auto-start") {
|
|
791
|
+
options.noAutoStart = true;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
783
794
|
if (arg === "--server-url") {
|
|
784
795
|
options.serverUrl = argv[++i];
|
|
785
796
|
continue;
|
|
@@ -796,6 +807,14 @@ function parseArgs(argv) {
|
|
|
796
807
|
options.monitoringHubUrl = argv[++i];
|
|
797
808
|
continue;
|
|
798
809
|
}
|
|
810
|
+
if (arg === "--runtime-root") {
|
|
811
|
+
options.runtimeRoot = argv[++i];
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (arg === "--runtime-repo") {
|
|
815
|
+
options.runtimeRepo = argv[++i];
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
799
818
|
console.error(`[pushpals] Unknown argument: ${arg}`);
|
|
800
819
|
printUsage();
|
|
801
820
|
process.exit(2);
|
|
@@ -841,6 +860,114 @@ async function resolveCurrentGitRepoRoot(cwd) {
|
|
|
841
860
|
return null;
|
|
842
861
|
return resolve2(root.stdout);
|
|
843
862
|
}
|
|
863
|
+
async function runCommand(command, cwd, timeoutMs = 120000) {
|
|
864
|
+
const proc = Bun.spawn(command, {
|
|
865
|
+
cwd,
|
|
866
|
+
stdout: "pipe",
|
|
867
|
+
stderr: "pipe"
|
|
868
|
+
});
|
|
869
|
+
const killTimer = setTimeout(() => {
|
|
870
|
+
try {
|
|
871
|
+
proc.kill();
|
|
872
|
+
} catch {}
|
|
873
|
+
}, timeoutMs);
|
|
874
|
+
try {
|
|
875
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
876
|
+
new Response(proc.stdout).text(),
|
|
877
|
+
new Response(proc.stderr).text(),
|
|
878
|
+
proc.exited
|
|
879
|
+
]);
|
|
880
|
+
return {
|
|
881
|
+
ok: exitCode === 0,
|
|
882
|
+
stdout: stdout.trim(),
|
|
883
|
+
stderr: stderr.trim(),
|
|
884
|
+
exitCode
|
|
885
|
+
};
|
|
886
|
+
} finally {
|
|
887
|
+
clearTimeout(killTimer);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
function resolveDefaultRuntimeRoot() {
|
|
891
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
|
|
892
|
+
return resolve2(home, ".pushpals", "runtime");
|
|
893
|
+
}
|
|
894
|
+
function copyFileIfMissing(fromPath, toPath) {
|
|
895
|
+
if (existsSync2(toPath) || !existsSync2(fromPath))
|
|
896
|
+
return;
|
|
897
|
+
mkdirSync(dirname(toPath), { recursive: true });
|
|
898
|
+
copyFileSync(fromPath, toPath);
|
|
899
|
+
}
|
|
900
|
+
async function ensureRuntimeCheckout(runtimeRoot, runtimeRepo) {
|
|
901
|
+
if (existsSync2(join2(runtimeRoot, ".git")))
|
|
902
|
+
return false;
|
|
903
|
+
mkdirSync(dirname(runtimeRoot), { recursive: true });
|
|
904
|
+
const clone = await runCommand(["git", "clone", "--depth", "1", runtimeRepo, runtimeRoot], dirname(runtimeRoot), 300000);
|
|
905
|
+
if (!clone.ok) {
|
|
906
|
+
throw new Error(`Failed to clone PushPals runtime from ${runtimeRepo} (exit ${clone.exitCode}): ${clone.stderr || clone.stdout || "unknown error"}`);
|
|
907
|
+
}
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
async function ensureRuntimePrepared(runtimeRoot) {
|
|
911
|
+
copyFileIfMissing(join2(runtimeRoot, ".env.example"), join2(runtimeRoot, ".env"));
|
|
912
|
+
copyFileIfMissing(join2(runtimeRoot, "configs", "local.example.toml"), join2(runtimeRoot, "configs", "local.toml"));
|
|
913
|
+
if (!existsSync2(join2(runtimeRoot, "node_modules"))) {
|
|
914
|
+
const install = await runCommand(["bun", "install"], runtimeRoot, 600000);
|
|
915
|
+
if (!install.ok) {
|
|
916
|
+
throw new Error(`Failed to install runtime dependencies (exit ${install.exitCode}): ${install.stderr || install.stdout || "unknown error"}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const protocolDist = join2(runtimeRoot, "packages", "protocol", "dist", "index.js");
|
|
920
|
+
if (!existsSync2(protocolDist)) {
|
|
921
|
+
const buildProtocol = await runCommand(["bun", "run", "--cwd", "packages/protocol", "build"], runtimeRoot, 300000);
|
|
922
|
+
if (!buildProtocol.ok) {
|
|
923
|
+
throw new Error(`Failed to build protocol package (exit ${buildProtocol.exitCode}): ${buildProtocol.stderr || buildProtocol.stdout || "unknown error"}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function spawnRuntimeService(name, command, cwd, env) {
|
|
928
|
+
const proc = Bun.spawn(command, {
|
|
929
|
+
cwd,
|
|
930
|
+
env,
|
|
931
|
+
stdout: "ignore",
|
|
932
|
+
stderr: "ignore"
|
|
933
|
+
});
|
|
934
|
+
const service = {
|
|
935
|
+
name,
|
|
936
|
+
proc,
|
|
937
|
+
exited: false,
|
|
938
|
+
exitCode: null
|
|
939
|
+
};
|
|
940
|
+
proc.exited.then((code) => {
|
|
941
|
+
service.exited = true;
|
|
942
|
+
service.exitCode = code;
|
|
943
|
+
});
|
|
944
|
+
return service;
|
|
945
|
+
}
|
|
946
|
+
function stopRuntimeServices(services) {
|
|
947
|
+
for (const service of services) {
|
|
948
|
+
try {
|
|
949
|
+
service.proc.kill();
|
|
950
|
+
} catch {}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function probeServer(serverUrl) {
|
|
954
|
+
try {
|
|
955
|
+
const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
|
|
956
|
+
return response.ok;
|
|
957
|
+
} catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function probeSourceControlManager(port) {
|
|
962
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
963
|
+
return false;
|
|
964
|
+
try {
|
|
965
|
+
const response = await fetchWithTimeout(`http://127.0.0.1:${Math.floor(port)}/health`, {}, HTTP_TIMEOUT_MS);
|
|
966
|
+
return response.ok;
|
|
967
|
+
} catch {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
844
971
|
async function fetchWithTimeout(url, init = {}, timeoutMs = HTTP_TIMEOUT_MS) {
|
|
845
972
|
const controller = new AbortController;
|
|
846
973
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -868,6 +995,73 @@ function authHeaders(authToken) {
|
|
|
868
995
|
async function probeLocalBuddy(localAgentUrl, authToken) {
|
|
869
996
|
return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, { headers: authHeaders(authToken) }, LOCALBUDDY_TIMEOUT_MS);
|
|
870
997
|
}
|
|
998
|
+
async function autoStartRuntimeServices(opts) {
|
|
999
|
+
const runtimeRoot = resolve2(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
|
|
1000
|
+
const runtimeRepo = String(opts.runtimeRepo || process.env.PUSHPALS_RUNTIME_REPO || DEFAULT_RUNTIME_REPO).trim();
|
|
1001
|
+
console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
|
|
1002
|
+
console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
|
|
1003
|
+
const cloned = await ensureRuntimeCheckout(runtimeRoot, runtimeRepo);
|
|
1004
|
+
if (cloned) {
|
|
1005
|
+
console.log(`[pushpals] Cloned runtime from ${runtimeRepo}`);
|
|
1006
|
+
}
|
|
1007
|
+
await ensureRuntimePrepared(runtimeRoot);
|
|
1008
|
+
const runtimeEnv = {
|
|
1009
|
+
...process.env,
|
|
1010
|
+
PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
|
|
1011
|
+
PUSHPALS_PROMPTS_ROOT_OVERRIDE: runtimeRoot
|
|
1012
|
+
};
|
|
1013
|
+
const services = [];
|
|
1014
|
+
const serverHealthy = await probeServer(opts.serverUrl);
|
|
1015
|
+
if (!serverHealthy) {
|
|
1016
|
+
console.log("[pushpals] Starting embedded server...");
|
|
1017
|
+
services.push(spawnRuntimeService("server", ["bun", "--cwd", "apps/server", "--env-file", "../../.env", "dev"], runtimeRoot, runtimeEnv));
|
|
1018
|
+
} else {
|
|
1019
|
+
console.log("[pushpals] Server already healthy; skipping embedded server start.");
|
|
1020
|
+
}
|
|
1021
|
+
console.log("[pushpals] Starting embedded LocalBuddy...");
|
|
1022
|
+
services.push(spawnRuntimeService("localbuddy", ["bun", "--cwd", "apps/localbuddy", "--env-file", "../../.env", "dev"], runtimeRoot, runtimeEnv));
|
|
1023
|
+
console.log("[pushpals] Starting embedded RemoteBuddy...");
|
|
1024
|
+
services.push(spawnRuntimeService("remotebuddy", ["bun", "--cwd", "apps/remotebuddy", "--env-file", "../../.env", "start"], runtimeRoot, runtimeEnv));
|
|
1025
|
+
const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
|
|
1026
|
+
if (!scmHealthy) {
|
|
1027
|
+
console.log("[pushpals] Starting embedded SourceControlManager...");
|
|
1028
|
+
services.push(spawnRuntimeService("source_control_manager", [
|
|
1029
|
+
"bun",
|
|
1030
|
+
"--cwd",
|
|
1031
|
+
"apps/source_control_manager",
|
|
1032
|
+
"--env-file",
|
|
1033
|
+
"../../.env",
|
|
1034
|
+
"start",
|
|
1035
|
+
"--",
|
|
1036
|
+
"--skip-clean-check"
|
|
1037
|
+
], runtimeRoot, runtimeEnv));
|
|
1038
|
+
} else {
|
|
1039
|
+
console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
|
|
1040
|
+
}
|
|
1041
|
+
const deadline = Date.now() + DEFAULT_RUNTIME_BOOT_TIMEOUT_MS;
|
|
1042
|
+
while (Date.now() < deadline) {
|
|
1043
|
+
for (let i = services.length - 1;i >= 0; i--) {
|
|
1044
|
+
const service = services[i];
|
|
1045
|
+
if (service.exited) {
|
|
1046
|
+
if (service.name === "source_control_manager") {
|
|
1047
|
+
console.warn(`[pushpals] Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}); continuing without SCM.`);
|
|
1048
|
+
services.splice(i, 1);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
stopRuntimeServices(services);
|
|
1052
|
+
throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"})`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
|
|
1056
|
+
if (health?.ok) {
|
|
1057
|
+
console.log("[pushpals] Embedded runtime is ready.");
|
|
1058
|
+
return services;
|
|
1059
|
+
}
|
|
1060
|
+
await Bun.sleep(DEFAULT_RUNTIME_BOOT_POLL_MS);
|
|
1061
|
+
}
|
|
1062
|
+
stopRuntimeServices(services);
|
|
1063
|
+
throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
|
|
1064
|
+
}
|
|
871
1065
|
function readCliState(pathValue) {
|
|
872
1066
|
if (!existsSync2(pathValue))
|
|
873
1067
|
return {};
|
|
@@ -1129,18 +1323,48 @@ async function main() {
|
|
|
1129
1323
|
const localAgentUrl = normalizeUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
|
|
1130
1324
|
const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
|
|
1131
1325
|
const authToken = config.authToken;
|
|
1132
|
-
|
|
1326
|
+
let autoStartedServices = [];
|
|
1327
|
+
const stopAutoStartedServices = () => {
|
|
1328
|
+
if (autoStartedServices.length === 0)
|
|
1329
|
+
return;
|
|
1330
|
+
stopRuntimeServices(autoStartedServices);
|
|
1331
|
+
autoStartedServices = [];
|
|
1332
|
+
};
|
|
1333
|
+
let health = await probeLocalBuddy(localAgentUrl, authToken);
|
|
1334
|
+
if (!health?.ok && !parsed.noAutoStart) {
|
|
1335
|
+
try {
|
|
1336
|
+
autoStartedServices = await autoStartRuntimeServices({
|
|
1337
|
+
repoRoot,
|
|
1338
|
+
serverUrl,
|
|
1339
|
+
localAgentUrl,
|
|
1340
|
+
sourceControlManagerPort: config.sourceControlManager.port,
|
|
1341
|
+
authToken,
|
|
1342
|
+
runtimeRoot: parsed.runtimeRoot,
|
|
1343
|
+
runtimeRepo: parsed.runtimeRepo
|
|
1344
|
+
});
|
|
1345
|
+
health = await probeLocalBuddy(localAgentUrl, authToken);
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
console.error(`[pushpals] Auto-start failed: ${String(err)}`);
|
|
1348
|
+
stopAutoStartedServices();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1133
1351
|
if (!health?.ok) {
|
|
1134
1352
|
console.error(`[pushpals] LocalBuddy is unavailable at ${localAgentUrl}.`);
|
|
1135
|
-
|
|
1353
|
+
if (parsed.noAutoStart) {
|
|
1354
|
+
console.error("[pushpals] Auto-start is disabled (--no-auto-start).");
|
|
1355
|
+
} else {
|
|
1356
|
+
console.error("[pushpals] Auto-start could not bring LocalBuddy online.");
|
|
1357
|
+
}
|
|
1136
1358
|
process.exit(1);
|
|
1137
1359
|
}
|
|
1138
1360
|
const localBuddyRepo = health.repo ? resolve2(health.repo) : "";
|
|
1139
1361
|
if (!localBuddyRepo) {
|
|
1362
|
+
stopAutoStartedServices();
|
|
1140
1363
|
console.error("[pushpals] LocalBuddy health response did not include repo path.");
|
|
1141
1364
|
process.exit(1);
|
|
1142
1365
|
}
|
|
1143
1366
|
if (normalizePath(localBuddyRepo) !== normalizePath(repoRoot)) {
|
|
1367
|
+
stopAutoStartedServices();
|
|
1144
1368
|
console.error("[pushpals] Repo mismatch detected.");
|
|
1145
1369
|
console.error(`[pushpals] currentRepo=${repoRoot}`);
|
|
1146
1370
|
console.error(`[pushpals] localBuddyRepo=${localBuddyRepo}`);
|
|
@@ -1194,6 +1418,7 @@ ${line}
|
|
|
1194
1418
|
streamAbort.abort();
|
|
1195
1419
|
if (rl)
|
|
1196
1420
|
rl.close();
|
|
1421
|
+
stopAutoStartedServices();
|
|
1197
1422
|
};
|
|
1198
1423
|
process.once("SIGINT", requestStop);
|
|
1199
1424
|
process.once("SIGTERM", requestStop);
|