@pushpalsdev/cli 1.0.2 → 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
@@ -32,7 +32,9 @@ Behavior:
32
32
 
33
33
  - If LocalBuddy is unavailable, CLI auto-start bootstraps embedded
34
34
  `server + localbuddy + remotebuddy + source_control_manager`.
35
- - Runtime clone path defaults to `~/.pushpals/runtime`.
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`.
36
38
  - Disable auto-start with `pushpals --no-auto-start`.
37
39
 
38
40
  ## No npm/Bun install
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,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // ../../scripts/pushpals-cli.ts
5
- import { copyFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5
+ import { chmodSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
6
6
  import { dirname, join as join2, resolve as resolve2 } from "path";
7
7
  import { createInterface } from "readline";
8
8
 
@@ -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;
@@ -745,7 +747,14 @@ var LOCALBUDDY_TIMEOUT_MS = 4000;
745
747
  var SSE_RECONNECT_MS = 1500;
746
748
  var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
747
749
  var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
748
- var DEFAULT_RUNTIME_REPO = "https://github.com/PushPalsDev/pushpals.git";
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
+ };
749
758
  var stateVersion = 1;
750
759
  function printUsage() {
751
760
  console.log("PushPals CLI");
@@ -759,7 +768,7 @@ function printUsage() {
759
768
  console.log(" --session-id <id> Override session ID");
760
769
  console.log(" --hub-url <url> Override monitoring hub URL");
761
770
  console.log(" --runtime-root <path> Override embedded runtime directory for auto-start");
762
- console.log(" --runtime-repo <url> Override embedded runtime git clone URL");
771
+ console.log(" --runtime-tag <tag> Override runtime release tag (e.g. v1.0.2)");
763
772
  console.log(" --no-auto-start Disable runtime auto-start when LocalBuddy is down");
764
773
  console.log(" --no-stream Disable live session event stream");
765
774
  console.log(" -h, --help Show this help");
@@ -811,8 +820,8 @@ function parseArgs(argv) {
811
820
  options.runtimeRoot = argv[++i];
812
821
  continue;
813
822
  }
814
- if (arg === "--runtime-repo") {
815
- options.runtimeRepo = argv[++i];
823
+ if (arg === "--runtime-tag") {
824
+ options.runtimeTag = argv[++i];
816
825
  continue;
817
826
  }
818
827
  console.error(`[pushpals] Unknown argument: ${arg}`);
@@ -860,69 +869,157 @@ async function resolveCurrentGitRepoRoot(cwd) {
860
869
  return null;
861
870
  return resolve2(root.stdout);
862
871
  }
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
872
  function resolveDefaultRuntimeRoot() {
891
873
  const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
892
874
  return resolve2(home, ".pushpals", "runtime");
893
875
  }
894
- function copyFileIfMissing(fromPath, toPath) {
895
- if (existsSync2(toPath) || !existsSync2(fromPath))
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))
896
918
  return;
897
- mkdirSync(dirname(toPath), { recursive: true });
898
- copyFileSync(fromPath, toPath);
919
+ mkdirSync(dirname(pathValue), { recursive: true });
920
+ writeFileSync(pathValue, text, "utf8");
899
921
  }
900
- async function ensureRuntimeCheckout(runtimeRoot, runtimeRepo) {
901
- if (existsSync2(join2(runtimeRoot, ".git")))
922
+ function copyBundledRuntimeAssets(runtimeRoot) {
923
+ const bundledRoot = resolve2(import.meta.dir, "..", "runtime");
924
+ if (!existsSync2(bundledRoot))
902
925
  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
- }
926
+ cpSync(bundledRoot, runtimeRoot, { recursive: true, force: true });
908
927
  return true;
909
928
  }
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"}`);
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);
917
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
+ `);
918
976
  }
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"}`);
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 {}
924
1020
  }
925
1021
  }
1022
+ return runtimeBinaries;
926
1023
  }
927
1024
  function spawnRuntimeService(name, command, cwd, env) {
928
1025
  const proc = Bun.spawn(command, {
@@ -997,44 +1094,35 @@ async function probeLocalBuddy(localAgentUrl, authToken) {
997
1094
  }
998
1095
  async function autoStartRuntimeServices(opts) {
999
1096
  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();
1097
+ const runtimeTag = await resolveRuntimeReleaseTag(opts.runtimeTag);
1098
+ const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
1099
+ await ensureRuntimeAssets(runtimeRoot, runtimeTag);
1001
1100
  console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1002
1101
  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);
1102
+ console.log(`[pushpals] runtimeTag=${runtimeTag}`);
1008
1103
  const runtimeEnv = {
1009
1104
  ...process.env,
1010
1105
  PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
1106
+ PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
1107
+ PUSHPALS_CONFIG_DIR_OVERRIDE: join2(runtimeRoot, "configs"),
1011
1108
  PUSHPALS_PROMPTS_ROOT_OVERRIDE: runtimeRoot
1012
1109
  };
1013
1110
  const services = [];
1014
1111
  const serverHealthy = await probeServer(opts.serverUrl);
1015
1112
  if (!serverHealthy) {
1016
1113
  console.log("[pushpals] Starting embedded server...");
1017
- services.push(spawnRuntimeService("server", ["bun", "--cwd", "apps/server", "--env-file", "../../.env", "dev"], runtimeRoot, runtimeEnv));
1114
+ services.push(spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv));
1018
1115
  } else {
1019
1116
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
1020
1117
  }
1021
1118
  console.log("[pushpals] Starting embedded LocalBuddy...");
1022
- services.push(spawnRuntimeService("localbuddy", ["bun", "--cwd", "apps/localbuddy", "--env-file", "../../.env", "dev"], runtimeRoot, runtimeEnv));
1119
+ services.push(spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv));
1023
1120
  console.log("[pushpals] Starting embedded RemoteBuddy...");
1024
- services.push(spawnRuntimeService("remotebuddy", ["bun", "--cwd", "apps/remotebuddy", "--env-file", "../../.env", "start"], runtimeRoot, runtimeEnv));
1121
+ services.push(spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv));
1025
1122
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
1026
1123
  if (!scmHealthy) {
1027
1124
  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));
1125
+ services.push(spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv));
1038
1126
  } else {
1039
1127
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
1040
1128
  }
@@ -1340,7 +1428,7 @@ async function main() {
1340
1428
  sourceControlManagerPort: config.sourceControlManager.port,
1341
1429
  authToken,
1342
1430
  runtimeRoot: parsed.runtimeRoot,
1343
- runtimeRepo: parsed.runtimeRepo
1431
+ runtimeTag: parsed.runtimeTag
1344
1432
  });
1345
1433
  health = await probeLocalBuddy(localAgentUrl, authToken);
1346
1434
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {