@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 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: process.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: process.env,
75
+ env: childEnv,
61
76
  });
62
77
  }
63
78
 
@@ -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 projectRoot = resolve(options.projectRoot ?? PROJECT_ROOT);
254
- const configDir = resolveRuntimeConfigDir(projectRoot, options.configDir);
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 = !options.configDir && configDir !== legacyConfigDir ? legacyConfigDir : "";
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(" - LocalBuddy must be running and attached to the same repo root.");
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
- const health = await probeLocalBuddy(localAgentUrl, authToken);
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
- console.error("[pushpals] Start the stack first, then rerun `pushpals`.");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {