@puddle-code/cli 0.0.12 → 0.0.14

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +41 -77
  2. package/README.md +4 -4
  3. package/dist/index.js +446 -166
  4. package/dist/public/assets/Dashboard-Ct5exI8s.js +1 -0
  5. package/dist/public/assets/{EditorTabClose-C3OCj0Ia.js → EditorTabClose-BhFjRoCZ.js} +1 -1
  6. package/dist/public/assets/{LazyTerminal-BihPI9J1.js → LazyTerminal-B0xYs2ul.js} +2 -2
  7. package/dist/public/assets/{ModelRefcount-wkKpjkKn.js → ModelRefcount-BY5Ul0p3.js} +1 -1
  8. package/dist/public/assets/{PaneEditorBody-DRR16vdx.js → PaneEditorBody-DLMtDiov.js} +1 -1
  9. package/dist/public/assets/SettingsDialog-WhQXNCmS.js +1 -0
  10. package/dist/public/assets/{Terminal-Ci3-2jXt.js → Terminal-DA8zjtmU.js} +1 -1
  11. package/dist/public/assets/Workspace-B0FDl771.js +6 -0
  12. package/dist/public/assets/{buffer-store-mErJ-u_7.js → buffer-store-BHJ82Nh3.js} +2 -2
  13. package/dist/public/assets/context-menu--H5GoapF.js +1 -0
  14. package/dist/public/assets/{cssMode-BxSU83en.js → cssMode-Ctq00nXG.js} +1 -1
  15. package/dist/public/assets/{htmlMode-BI3Fg8wV.js → htmlMode-pYCLPiZ_.js} +1 -1
  16. package/dist/public/assets/index-CjgsCY-C.css +2 -0
  17. package/dist/public/assets/index-RvCLpi9M.js +10 -0
  18. package/dist/public/assets/{jsonMode-Bsh_R96t.js → jsonMode-Ch5UticT.js} +1 -1
  19. package/dist/public/assets/{session-seed-BbO3y4XI.js → session-seed-DBxHPYBK.js} +1 -1
  20. package/dist/public/assets/ssh-mode-eNTVum_C.js +1 -0
  21. package/dist/public/assets/{tsMode-CV6x0W8w.js → tsMode-CeXrzlPn.js} +1 -1
  22. package/dist/public/index.html +3 -3
  23. package/package.json +1 -1
  24. package/dist/public/assets/Dashboard-DOKOSPsG.js +0 -1
  25. package/dist/public/assets/SettingsDialog-CpnehH60.js +0 -1
  26. package/dist/public/assets/Workspace-D1XOZzcy.js +0 -6
  27. package/dist/public/assets/context-menu-DyvGbiNT.js +0 -1
  28. package/dist/public/assets/index-DIgA5eoW.js +0 -10
  29. package/dist/public/assets/index-WMJ9gBck.css +0 -2
  30. package/dist/public/assets/ssh-mode-BuFhqbL1.js +0 -1
package/dist/index.js CHANGED
@@ -2307,8 +2307,8 @@ var require_websocket = __commonJS({
2307
2307
  var http = __require("http");
2308
2308
  var net = __require("net");
2309
2309
  var tls = __require("tls");
2310
- var { randomBytes, createHash } = __require("crypto");
2311
- var { Duplex, Readable } = __require("stream");
2310
+ var { randomBytes, createHash: createHash2 } = __require("crypto");
2311
+ var { Duplex, Readable: Readable2 } = __require("stream");
2312
2312
  var { URL: URL2 } = __require("url");
2313
2313
  var PerMessageDeflate2 = require_permessage_deflate();
2314
2314
  var Receiver2 = require_receiver();
@@ -2975,7 +2975,7 @@ var require_websocket = __commonJS({
2975
2975
  abortHandshake(websocket, socket, "Invalid Upgrade header");
2976
2976
  return;
2977
2977
  }
2978
- const digest = createHash("sha1").update(key + GUID).digest("base64");
2978
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
2979
2979
  if (res.headers["sec-websocket-accept"] !== digest) {
2980
2980
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
2981
2981
  return;
@@ -3344,7 +3344,7 @@ var require_websocket_server = __commonJS({
3344
3344
  var EventEmitter = __require("events");
3345
3345
  var http = __require("http");
3346
3346
  var { Duplex } = __require("stream");
3347
- var { createHash } = __require("crypto");
3347
+ var { createHash: createHash2 } = __require("crypto");
3348
3348
  var extension2 = require_extension();
3349
3349
  var PerMessageDeflate2 = require_permessage_deflate();
3350
3350
  var subprotocol2 = require_subprotocol();
@@ -3651,7 +3651,7 @@ var require_websocket_server = __commonJS({
3651
3651
  );
3652
3652
  }
3653
3653
  if (this._state > RUNNING) return abortHandshake(socket, 503);
3654
- const digest = createHash("sha1").update(key + GUID).digest("base64");
3654
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
3655
3655
  const headers = [
3656
3656
  "HTTP/1.1 101 Switching Protocols",
3657
3657
  "Upgrade: websocket",
@@ -3908,10 +3908,8 @@ init_types();
3908
3908
  var USAGE = `puddle \u2014 self-hosted orchestrator for CLI coding agents
3909
3909
 
3910
3910
  usage:
3911
- puddle start [--port <p>] [--foreground] [--no-browser] [--no-upgrade]
3912
- [--tarball <path>]
3913
- puddle connect user@host [--port <local>] [--remote-port <p>] [--foreground]
3914
- [--no-browser] [--no-upgrade] [--tarball <path>]
3911
+ puddle launch [local | user@host] [--port <p>] [--remote-port <p>]
3912
+ [--foreground] [--no-browser] [--no-upgrade] [--tarball <path>]
3915
3913
  puddle refresh [local | user@host] [--port <p>] [--remote-port <p>]
3916
3914
  [--foreground] [--no-browser] [--no-upgrade] [--tarball <path>]
3917
3915
  puddle list
@@ -3919,17 +3917,18 @@ usage:
3919
3917
  puddle status [user@host]
3920
3918
  puddle attach [user@host] <session> [--term <id>]
3921
3919
  puddle logs [user@host] [session] [--term <id>] [-f|--follow]
3922
- puddle upgrade [user@host]
3920
+ puddle upgrade <cli | daemon | desktop> [user@host]
3923
3921
  puddle --version | --help
3924
3922
 
3925
- start serves the cockpit at http://localhost:7433 against the daemon on this
3926
- machine (installing it under ~/.puddle if needed); connect does the same for
3927
- an SSH host through one tunnel. Both keep running in the background once
3928
- ready, so the terminal may close (--foreground to stay attached; Ctrl-C then
3929
- stops the cockpit). refresh stops a target's cockpit (even a wedged one) and
3930
- runs the full start/connect flow again \u2014 tunnel, daemon restart if needed \u2014
3931
- keeping the old UI port so open tabs survive. list shows running cockpits;
3932
- kill stops one \u2014 sessions keep running on the host either way.`;
3923
+ launch serves the cockpit at http://localhost:7433 against the daemon on this
3924
+ machine when no host (or 'local') is given \u2014 installing it under ~/.puddle if
3925
+ needed \u2014 and does the same for an SSH host through one tunnel. It keeps
3926
+ running in the background once ready, so the terminal may close (--foreground
3927
+ to stay attached; Ctrl-C then stops the cockpit). refresh stops a target's
3928
+ cockpit (even a wedged one) and runs the full launch flow again \u2014 tunnel,
3929
+ daemon restart if needed \u2014 keeping the old UI port so open tabs survive.
3930
+ list shows running cockpits; kill stops one \u2014 sessions keep running on the
3931
+ host either way.`;
3933
3932
  function parseArgs(argv) {
3934
3933
  const [cmd, ...rest] = argv;
3935
3934
  if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h")
@@ -3977,40 +3976,10 @@ function parseArgs(argv) {
3977
3976
  }
3978
3977
  };
3979
3978
  switch (cmd) {
3980
- case "start": {
3981
- if (positionals.length > 0) {
3982
- throw new CliError(
3983
- "bad_arguments",
3984
- `start takes no positional arguments (got '${positionals[0]}')`
3985
- );
3986
- }
3987
- const port = intFlag("--port");
3988
- const preferPort = intFlag("--prefer-port");
3989
- const tarball = strFlag("--tarball");
3990
- expect(
3991
- "--port",
3992
- "--prefer-port",
3993
- "--no-browser",
3994
- "--no-upgrade",
3995
- "--tarball",
3996
- "--foreground"
3997
- );
3998
- return {
3999
- cmd: "start",
4000
- ...port !== void 0 ? { port } : {},
4001
- ...preferPort !== void 0 ? { preferPort } : {},
4002
- ...tarball !== void 0 ? { tarball } : {},
4003
- noBrowser: flags.has("--no-browser"),
4004
- noUpgrade: flags.has("--no-upgrade"),
4005
- foreground: flags.has("--foreground")
4006
- };
4007
- }
4008
- case "connect": {
4009
- const host = positionals[0];
4010
- if (host === void 0)
4011
- throw new CliError("bad_arguments", "connect needs a host: puddle connect user@host");
3979
+ case "launch": {
4012
3980
  if (positionals.length > 1)
4013
- throw new CliError("bad_arguments", "connect takes exactly one host");
3981
+ throw new CliError("bad_arguments", "launch takes at most one target");
3982
+ const target = positionals[0] ?? "local";
4014
3983
  const port = intFlag("--port");
4015
3984
  const preferPort = intFlag("--prefer-port");
4016
3985
  const remotePort = intFlag("--remote-port");
@@ -4024,9 +3993,12 @@ function parseArgs(argv) {
4024
3993
  "--tarball",
4025
3994
  "--foreground"
4026
3995
  );
3996
+ if (target === "local" && remotePort !== void 0) {
3997
+ throw new CliError("bad_arguments", "--remote-port only applies to an SSH target");
3998
+ }
4027
3999
  return {
4028
- cmd: "connect",
4029
- host,
4000
+ cmd: "launch",
4001
+ target,
4030
4002
  ...port !== void 0 ? { port } : {},
4031
4003
  ...preferPort !== void 0 ? { preferPort } : {},
4032
4004
  ...remotePort !== void 0 ? { remotePort } : {},
@@ -4036,6 +4008,14 @@ function parseArgs(argv) {
4036
4008
  foreground: flags.has("--foreground")
4037
4009
  };
4038
4010
  }
4011
+ // The pre-unification verbs, kept only to say what replaced them.
4012
+ case "start":
4013
+ throw new CliError("bad_arguments", `'start' is now: puddle launch`);
4014
+ case "connect":
4015
+ throw new CliError(
4016
+ "bad_arguments",
4017
+ `'connect' is now: puddle launch ${positionals[0] ?? "user@host"}`
4018
+ );
4039
4019
  case "refresh": {
4040
4020
  if (positionals.length > 1)
4041
4021
  throw new CliError("bad_arguments", "refresh takes at most one target");
@@ -4120,24 +4100,29 @@ function parseArgs(argv) {
4120
4100
  return first.includes("@") ? { cmd: "logs", host: first, follow, ...term !== void 0 ? { term } : {} } : { cmd: "logs", session: first, follow, ...term !== void 0 ? { term } : {} };
4121
4101
  }
4122
4102
  case "upgrade": {
4123
- if (positionals.length > 1)
4124
- throw new CliError("bad_arguments", "upgrade takes at most a host");
4103
+ const [what, host, extra] = positionals;
4104
+ if (what !== "cli" && what !== "daemon" && what !== "desktop") {
4105
+ throw new CliError(
4106
+ "bad_arguments",
4107
+ "upgrade needs a subject: puddle upgrade <cli | daemon | desktop> [user@host]",
4108
+ what !== void 0 && what.includes("@") ? `to upgrade the daemon on ${what}: puddle upgrade daemon ${what}` : void 0
4109
+ );
4110
+ }
4111
+ if (extra !== void 0)
4112
+ throw new CliError("bad_arguments", "upgrade takes at most a subject + host");
4125
4113
  expect();
4126
- const host = positionals[0];
4127
- return { cmd: "upgrade", ...host !== void 0 ? { host } : {} };
4114
+ return { cmd: "upgrade", what, ...host !== void 0 ? { host } : {} };
4128
4115
  }
4129
4116
  default:
4130
4117
  throw new CliError("bad_arguments", `unknown command '${cmd}'`, "see: puddle --help");
4131
4118
  }
4132
4119
  }
4133
4120
  function argvFor(command) {
4134
- const argv = [command.cmd];
4135
- if (command.cmd === "connect") argv.push(command.host);
4121
+ const argv = ["launch"];
4122
+ if (command.target !== "local") argv.push(command.target);
4136
4123
  if (command.port !== void 0) argv.push("--port", String(command.port));
4137
4124
  if (command.preferPort !== void 0) argv.push("--prefer-port", String(command.preferPort));
4138
- if (command.cmd === "connect" && command.remotePort !== void 0) {
4139
- argv.push("--remote-port", String(command.remotePort));
4140
- }
4125
+ if (command.remotePort !== void 0) argv.push("--remote-port", String(command.remotePort));
4141
4126
  if (command.tarball !== void 0) argv.push("--tarball", command.tarball);
4142
4127
  if (command.noBrowser) argv.push("--no-browser");
4143
4128
  if (command.noUpgrade) argv.push("--no-upgrade");
@@ -4147,16 +4132,288 @@ function argvFor(command) {
4147
4132
 
4148
4133
  // src/cli/run.ts
4149
4134
  init_attach();
4135
+ import { spawn as spawn7 } from "node:child_process";
4150
4136
  import { existsSync as existsSync3 } from "node:fs";
4151
- import { dirname as dirname6, join as join8 } from "node:path";
4137
+ import { dirname as dirname6, join as join9 } from "node:path";
4152
4138
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4153
4139
 
4154
- // src/lib/browser.ts
4140
+ // src/lib/desktop-update.ts
4141
+ import { createHash } from "node:crypto";
4155
4142
  import { spawn } from "node:child_process";
4143
+ import { createReadStream, createWriteStream } from "node:fs";
4144
+ import { chmod, mkdir, readdir, rm, writeFile } from "node:fs/promises";
4145
+ import { join as join3 } from "node:path";
4146
+ import { Readable } from "node:stream";
4147
+ import { pipeline } from "node:stream/promises";
4148
+
4149
+ // src/lib/paths.ts
4150
+ import { homedir } from "node:os";
4151
+ import { join } from "node:path";
4152
+ function clientHome(env = process.env) {
4153
+ return env.PUDDLE_HOME ?? join(homedir(), ".puddle");
4154
+ }
4155
+ var HOST_HOME = '"${PUDDLE_HOME:-$HOME/.puddle}"';
4156
+ var hostPaths = {
4157
+ home: HOST_HOME,
4158
+ token: `${HOST_HOME}/token`,
4159
+ config: `${HOST_HOME}/config.json`,
4160
+ runtime: `${HOST_HOME}/runtime.json`,
4161
+ current: `${HOST_HOME}/bin/current`,
4162
+ cache: `${HOST_HOME}/cache`,
4163
+ logs: `${HOST_HOME}/logs`
4164
+ };
4165
+
4166
+ // src/lib/desktop-update.ts
4167
+ init_types();
4168
+
4169
+ // src/lib/version.ts
4170
+ import { readFileSync } from "node:fs";
4171
+ import { dirname, join as join2 } from "node:path";
4172
+ import { fileURLToPath } from "node:url";
4173
+ function cliVersion() {
4174
+ if (true) return "0.0.14";
4175
+ const here = dirname(fileURLToPath(import.meta.url));
4176
+ for (const candidate of [
4177
+ join2(here, "..", "..", "package.json"),
4178
+ join2(here, "..", "package.json")
4179
+ ]) {
4180
+ try {
4181
+ const pkg = JSON.parse(readFileSync(candidate, "utf8"));
4182
+ if (pkg.version !== void 0) return pkg.version;
4183
+ } catch {
4184
+ }
4185
+ }
4186
+ return "0.0.0";
4187
+ }
4188
+ function pinnedDaemonVersion() {
4189
+ return cliVersion();
4190
+ }
4191
+ function repoSlug(env = process.env) {
4192
+ if (env.PUDDLE_REPO !== void 0 && env.PUDDLE_REPO !== "") return env.PUDDLE_REPO;
4193
+ if (true) return "PerceptronV/puddle-code";
4194
+ return void 0;
4195
+ }
4196
+
4197
+ // src/lib/desktop-update.ts
4198
+ function isNewerVersion(candidate, current) {
4199
+ const parse3 = (v) => {
4200
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
4201
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
4202
+ };
4203
+ const a = parse3(candidate);
4204
+ const b = parse3(current);
4205
+ if (a === null || b === null) return false;
4206
+ for (let i = 0; i < 3; i++) {
4207
+ if (a[i] !== b[i]) return a[i] > b[i];
4208
+ }
4209
+ return false;
4210
+ }
4211
+ function pickDesktopAsset(assets, platform, arch) {
4212
+ const suffix = platform === "darwin" ? "-mac.zip" : platform === "linux" ? ".AppImage" : null;
4213
+ if (suffix === null) return null;
4214
+ return assets.find(
4215
+ (a) => a.name.endsWith(suffix) && a.name.includes("-arm64") === (arch === "arm64")
4216
+ ) ?? null;
4217
+ }
4218
+ function parseSums(text) {
4219
+ const sums = /* @__PURE__ */ new Map();
4220
+ for (const line of text.split("\n")) {
4221
+ const match = /^([0-9a-f]{64})\s+\*?(.+?)\s*$/.exec(line.trim());
4222
+ if (match) sums.set(match[2], match[1]);
4223
+ }
4224
+ return sums;
4225
+ }
4226
+ async function checkForDesktopUpdate(currentVersion, opts = {}) {
4227
+ const slug2 = repoSlug();
4228
+ if (slug2 === void 0) return null;
4229
+ const fetchFn = opts.fetchFn ?? fetch;
4230
+ const response = await fetchFn(`https://api.github.com/repos/${slug2}/releases/latest`, {
4231
+ headers: { accept: "application/vnd.github+json", "user-agent": "puddle-desktop" }
4232
+ });
4233
+ if (!response.ok) {
4234
+ throw new CliError("not_installed", `release lookup failed (HTTP ${response.status})`);
4235
+ }
4236
+ const release = await response.json();
4237
+ const version2 = (release.tag_name ?? "").replace(/^v/, "");
4238
+ if (!isNewerVersion(version2, currentVersion)) return null;
4239
+ const assets = release.assets ?? [];
4240
+ const asset = pickDesktopAsset(
4241
+ assets,
4242
+ opts.platform ?? process.platform,
4243
+ opts.arch ?? process.arch
4244
+ );
4245
+ const sums = assets.find((a) => a.name === "SHA256SUMS");
4246
+ if (asset === null || sums === void 0) return null;
4247
+ return {
4248
+ version: version2,
4249
+ asset: { name: asset.name, url: asset.browser_download_url },
4250
+ sumsUrl: sums.browser_download_url
4251
+ };
4252
+ }
4253
+ async function stageDesktopUpdate(update, opts = {}) {
4254
+ const logger = opts.logger ?? silentLogger;
4255
+ const fetchFn = opts.fetchFn ?? fetch;
4256
+ const dir = join3(opts.cacheDir ?? join3(clientHome(), "cache", "desktop"), update.version);
4257
+ await rm(dir, { recursive: true, force: true });
4258
+ await mkdir(dir, { recursive: true });
4259
+ try {
4260
+ logger.info(`downloading ${update.asset.name}`);
4261
+ const archive = join3(dir, update.asset.name);
4262
+ const response = await fetchFn(update.asset.url, {
4263
+ headers: { "user-agent": "puddle-desktop" }
4264
+ });
4265
+ if (!response.ok || response.body === null) {
4266
+ throw new CliError("not_installed", `download failed (HTTP ${response.status})`);
4267
+ }
4268
+ await pipeline(
4269
+ Readable.fromWeb(response.body),
4270
+ createWriteStream(archive)
4271
+ );
4272
+ const sumsResponse = await fetchFn(update.sumsUrl, {
4273
+ headers: { "user-agent": "puddle-desktop" }
4274
+ });
4275
+ if (!sumsResponse.ok) {
4276
+ throw new CliError("not_installed", `SHA256SUMS fetch failed (HTTP ${sumsResponse.status})`);
4277
+ }
4278
+ const expected = parseSums(await sumsResponse.text()).get(update.asset.name);
4279
+ if (expected === void 0) {
4280
+ throw new CliError("not_installed", `${update.asset.name} is not in SHA256SUMS`);
4281
+ }
4282
+ const actual = await fileSha256(archive);
4283
+ if (actual !== expected) {
4284
+ throw new CliError(
4285
+ "not_installed",
4286
+ `checksum mismatch for ${update.asset.name}`,
4287
+ `expected ${expected}, got ${actual}`
4288
+ );
4289
+ }
4290
+ if (update.asset.name.endsWith(".zip")) {
4291
+ const extractDir = join3(dir, "extract");
4292
+ await run("/usr/bin/ditto", ["-x", "-k", archive, extractDir]);
4293
+ const bundle = (await readdir(extractDir)).find((name) => name.endsWith(".app"));
4294
+ if (bundle === void 0) {
4295
+ throw new CliError("not_installed", "the update zip contains no .app bundle");
4296
+ }
4297
+ logger.info(`staged ${update.version} at ${join3(extractDir, bundle)}`);
4298
+ return {
4299
+ version: update.version,
4300
+ kind: "mac-app",
4301
+ stagedPath: join3(extractDir, bundle),
4302
+ dir
4303
+ };
4304
+ }
4305
+ await chmod(archive, 493);
4306
+ logger.info(`staged ${update.version} at ${archive}`);
4307
+ return { version: update.version, kind: "appimage", stagedPath: archive, dir };
4308
+ } catch (e) {
4309
+ await rm(dir, { recursive: true, force: true });
4310
+ throw e;
4311
+ }
4312
+ }
4313
+ async function applyDesktopUpdate(staged, opts) {
4314
+ const logger = opts.logger ?? silentLogger;
4315
+ const script = swapScript(staged, opts);
4316
+ const scriptPath = join3(staged.dir, "apply.sh");
4317
+ await writeFile(scriptPath, script, { mode: 493 });
4318
+ logger.info(`applying ${staged.version} via ${scriptPath}`);
4319
+ if (opts.detach ?? true) {
4320
+ const child = spawn("/bin/sh", [scriptPath], { detached: true, stdio: "ignore" });
4321
+ child.unref();
4322
+ return;
4323
+ }
4324
+ await run("/bin/sh", [scriptPath]);
4325
+ }
4326
+ var q = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
4327
+ function swapScript(staged, opts) {
4328
+ const wait = opts.waitPid === void 0 ? [] : [
4329
+ `i=0`,
4330
+ `while kill -0 ${opts.waitPid} 2>/dev/null; do`,
4331
+ ` i=$((i+1)); [ "$i" -gt 600 ] && exit 1`,
4332
+ ` sleep 0.1`,
4333
+ `done`
4334
+ ];
4335
+ if (staged.kind === "mac-app") {
4336
+ return [
4337
+ `#!/bin/sh`,
4338
+ `# puddle desktop update ${staged.version} \u2014 swap once the app exits`,
4339
+ ...wait,
4340
+ `target=${q(opts.targetPath)}`,
4341
+ `staged=${q(staged.stagedPath)}`,
4342
+ `rm -rf "$target.old"`,
4343
+ `mv "$target" "$target.old" || exit 1`,
4344
+ `if mv "$staged" "$target" 2>/dev/null || /usr/bin/ditto "$staged" "$target"; then`,
4345
+ // Defence in depth: the normal path never quarantines (node's fetch is
4346
+ // not a quarantine-opted-in app, and ditto only propagates what the
4347
+ // zip already carries), but if the bundle ever acquires the attribute
4348
+ // the swap must not resurrect the Gatekeeper prompt.
4349
+ ` /usr/bin/xattr -dr com.apple.quarantine "$target" 2>/dev/null`,
4350
+ ` rm -rf "$target.old" ${q(staged.dir)}`,
4351
+ ...opts.relaunch ? [` open "$target"`] : [],
4352
+ `else`,
4353
+ ` mv "$target.old" "$target"`,
4354
+ ` exit 1`,
4355
+ `fi`,
4356
+ ``
4357
+ ].join("\n");
4358
+ }
4359
+ return [
4360
+ `#!/bin/sh`,
4361
+ `# puddle desktop update ${staged.version} \u2014 swap once the app exits`,
4362
+ ...wait,
4363
+ `target=${q(opts.targetPath)}`,
4364
+ `cp -f ${q(staged.stagedPath)} "$target" || exit 1`,
4365
+ `chmod +x "$target"`,
4366
+ `rm -rf ${q(staged.dir)}`,
4367
+ ...opts.relaunch ? [`"$target" >/dev/null 2>&1 &`] : [],
4368
+ ``
4369
+ ].join("\n");
4370
+ }
4371
+ async function findInstalledDesktopApp() {
4372
+ if (process.platform !== "darwin") return null;
4373
+ const { readFile } = await import("node:fs/promises");
4374
+ const { homedir: homedir2 } = await import("node:os");
4375
+ for (const appPath of ["/Applications/Puddle.app", join3(homedir2(), "Applications/Puddle.app")]) {
4376
+ try {
4377
+ const plist = await readFile(join3(appPath, "Contents/Info.plist"), "utf8");
4378
+ const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
4379
+ plist
4380
+ );
4381
+ if (match) return { appPath, version: match[1] };
4382
+ } catch {
4383
+ }
4384
+ }
4385
+ return null;
4386
+ }
4387
+ function isDesktopAppRunning(appPath) {
4388
+ return new Promise((resolve3) => {
4389
+ const child = spawn("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
4390
+ stdio: "ignore"
4391
+ });
4392
+ child.on("error", () => resolve3(false));
4393
+ child.on("exit", (code) => resolve3(code === 0));
4394
+ });
4395
+ }
4396
+ function fileSha256(path) {
4397
+ const hash2 = createHash("sha256");
4398
+ return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
4399
+ }
4400
+ function run(command, args) {
4401
+ return new Promise((resolve3, reject) => {
4402
+ const child = spawn(command, args, { stdio: "ignore" });
4403
+ child.on("error", reject);
4404
+ child.on(
4405
+ "exit",
4406
+ (code) => code === 0 ? resolve3() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
4407
+ );
4408
+ });
4409
+ }
4410
+
4411
+ // src/lib/browser.ts
4412
+ import { spawn as spawn2 } from "node:child_process";
4156
4413
  function openBrowser(url2, platform = process.platform) {
4157
4414
  const command = platform === "darwin" ? { bin: "open", args: [url2] } : platform === "win32" ? { bin: "cmd", args: ["/c", "start", "", url2] } : { bin: "xdg-open", args: [url2] };
4158
4415
  try {
4159
- const child = spawn(command.bin, command.args, { stdio: "ignore", detached: true });
4416
+ const child = spawn2(command.bin, command.args, { stdio: "ignore", detached: true });
4160
4417
  child.on("error", () => {
4161
4418
  });
4162
4419
  child.unref();
@@ -4167,7 +4424,7 @@ function openBrowser(url2, platform = process.platform) {
4167
4424
  }
4168
4425
 
4169
4426
  // src/lib/connect.ts
4170
- import { join as join5 } from "node:path";
4427
+ import { join as join6 } from "node:path";
4171
4428
 
4172
4429
  // ../shared/src/protocol.ts
4173
4430
  var PROTOCOL_VERSION = { major: 10, minor: 1 };
@@ -19537,23 +19794,6 @@ function sleep(ms) {
19537
19794
  return new Promise((r) => setTimeout(r, ms));
19538
19795
  }
19539
19796
 
19540
- // src/lib/paths.ts
19541
- import { homedir } from "node:os";
19542
- import { join } from "node:path";
19543
- function clientHome(env = process.env) {
19544
- return env.PUDDLE_HOME ?? join(homedir(), ".puddle");
19545
- }
19546
- var HOST_HOME = '"${PUDDLE_HOME:-$HOME/.puddle}"';
19547
- var hostPaths = {
19548
- home: HOST_HOME,
19549
- token: `${HOST_HOME}/token`,
19550
- config: `${HOST_HOME}/config.json`,
19551
- runtime: `${HOST_HOME}/runtime.json`,
19552
- current: `${HOST_HOME}/bin/current`,
19553
- cache: `${HOST_HOME}/cache`,
19554
- logs: `${HOST_HOME}/logs`
19555
- };
19556
-
19557
19797
  // src/lib/daemon-client.ts
19558
19798
  init_types();
19559
19799
  var LIVE = /* @__PURE__ */ new Set(["starting", "running", "waiting_input"]);
@@ -19652,39 +19892,9 @@ async function waitForToken(transport, timeoutMs) {
19652
19892
 
19653
19893
  // src/lib/bootstrap.ts
19654
19894
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
19655
- import { dirname as dirname2, join as join3, resolve } from "node:path";
19895
+ import { dirname as dirname2, join as join4, resolve } from "node:path";
19656
19896
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19657
19897
  init_types();
19658
-
19659
- // src/lib/version.ts
19660
- import { readFileSync } from "node:fs";
19661
- import { dirname, join as join2 } from "node:path";
19662
- import { fileURLToPath } from "node:url";
19663
- function cliVersion() {
19664
- if (true) return "0.0.12";
19665
- const here = dirname(fileURLToPath(import.meta.url));
19666
- for (const candidate of [
19667
- join2(here, "..", "..", "package.json"),
19668
- join2(here, "..", "package.json")
19669
- ]) {
19670
- try {
19671
- const pkg = JSON.parse(readFileSync(candidate, "utf8"));
19672
- if (pkg.version !== void 0) return pkg.version;
19673
- } catch {
19674
- }
19675
- }
19676
- return "0.0.0";
19677
- }
19678
- function pinnedDaemonVersion() {
19679
- return cliVersion();
19680
- }
19681
- function repoSlug(env = process.env) {
19682
- if (env.PUDDLE_REPO !== void 0 && env.PUDDLE_REPO !== "") return env.PUDDLE_REPO;
19683
- if (true) return "PerceptronV/puddle-code";
19684
- return void 0;
19685
- }
19686
-
19687
- // src/lib/bootstrap.ts
19688
19898
  async function installedVersion(transport) {
19689
19899
  const result = await transport.exec(`readlink ${hostPaths.current}`, { timeoutMs: 15e3 });
19690
19900
  if (result.code !== 0) return null;
@@ -19754,9 +19964,9 @@ function lastLines(text, count = 3) {
19754
19964
  function readInstallScript() {
19755
19965
  const here = dirname2(fileURLToPath2(import.meta.url));
19756
19966
  const candidates = [
19757
- join3(here, "install.sh"),
19967
+ join4(here, "install.sh"),
19758
19968
  // dist/index.js → dist/install.sh
19759
- join3(here, "..", "..", "..", "..", "scripts", "install.sh")
19969
+ join4(here, "..", "..", "..", "..", "scripts", "install.sh")
19760
19970
  // src/lib/ → repo root
19761
19971
  ];
19762
19972
  for (const candidate of candidates) {
@@ -20004,8 +20214,8 @@ function recoverProxiedPath(referer, requestUrl) {
20004
20214
  }
20005
20215
 
20006
20216
  // src/lib/serve/static.ts
20007
- import { createReadStream, existsSync as existsSync2, statSync } from "node:fs";
20008
- import { extname, join as join4, normalize, resolve as resolve2, sep } from "node:path";
20217
+ import { createReadStream as createReadStream2, existsSync as existsSync2, statSync } from "node:fs";
20218
+ import { extname, join as join5, normalize, resolve as resolve2, sep } from "node:path";
20009
20219
  var MIME = {
20010
20220
  ".html": "text/html; charset=utf-8",
20011
20221
  ".js": "text/javascript; charset=utf-8",
@@ -20033,14 +20243,14 @@ function createStaticHandler(rootDir) {
20033
20243
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
20034
20244
  return;
20035
20245
  }
20036
- const requested = normalize(join4(root, decoded));
20246
+ const requested = normalize(join5(root, decoded));
20037
20247
  const candidate = requested.startsWith(root + sep) || requested === root ? requested : root;
20038
20248
  const isFile = existsSync2(candidate) && statSync(candidate).isFile();
20039
20249
  if (!isFile && extname(pathname) !== "") {
20040
20250
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
20041
20251
  return;
20042
20252
  }
20043
- const file2 = isFile ? candidate : join4(root, "index.html");
20253
+ const file2 = isFile ? candidate : join5(root, "index.html");
20044
20254
  if (!existsSync2(file2)) {
20045
20255
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
20046
20256
  return;
@@ -20053,7 +20263,7 @@ function createStaticHandler(rootDir) {
20053
20263
  res.end();
20054
20264
  return;
20055
20265
  }
20056
- createReadStream(file2).pipe(res);
20266
+ createReadStream2(file2).pipe(res);
20057
20267
  };
20058
20268
  }
20059
20269
 
@@ -20318,7 +20528,7 @@ async function listen(server, startPort, strict, avoidPort) {
20318
20528
  }
20319
20529
 
20320
20530
  // src/lib/tunnel.ts
20321
- import { spawn as spawn2 } from "node:child_process";
20531
+ import { spawn as spawn3 } from "node:child_process";
20322
20532
  init_types();
20323
20533
  var RECONNECT_INITIAL_MS = 500;
20324
20534
  var RECONNECT_MAX_MS = 1e4;
@@ -20342,7 +20552,7 @@ async function openTunnel(ssh, remotePort, opts = {}) {
20342
20552
  let healthTimer = null;
20343
20553
  const spawnForward = async (port) => {
20344
20554
  if (stopping) return false;
20345
- const child = spawn2(
20555
+ const child = spawn3(
20346
20556
  sshBinary,
20347
20557
  ssh.args("-N", "-L", `${port}:127.0.0.1:${remotePort}`, ssh.host),
20348
20558
  { stdio: ["ignore", "ignore", "inherit"] }
@@ -20465,7 +20675,7 @@ async function openTunnel(ssh, remotePort, opts = {}) {
20465
20675
  }
20466
20676
 
20467
20677
  // src/lib/transport/local.ts
20468
- import { spawn as spawn3 } from "node:child_process";
20678
+ import { spawn as spawn4 } from "node:child_process";
20469
20679
  import { copyFileSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "node:fs";
20470
20680
  import { dirname as dirname4 } from "node:path";
20471
20681
  var LocalTransport = class {
@@ -20473,7 +20683,7 @@ var LocalTransport = class {
20473
20683
  label = "this machine";
20474
20684
  exec(command, opts = {}) {
20475
20685
  return new Promise((resolve3) => {
20476
- const child = spawn3("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
20686
+ const child = spawn4("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
20477
20687
  let stdout = "";
20478
20688
  let stderr = "";
20479
20689
  let timer;
@@ -20520,7 +20730,7 @@ function expandHome(path) {
20520
20730
  }
20521
20731
 
20522
20732
  // src/lib/transport/ssh.ts
20523
- import { spawn as spawn4 } from "node:child_process";
20733
+ import { spawn as spawn5 } from "node:child_process";
20524
20734
  import { mkdirSync as mkdirSync3 } from "node:fs";
20525
20735
  init_types();
20526
20736
  var SshTransport = class {
@@ -20567,7 +20777,7 @@ var SshTransport = class {
20567
20777
  */
20568
20778
  open() {
20569
20779
  return new Promise((resolve3, reject) => {
20570
- const child = spawn4(this.ssh, this.args(this.host, "true"), { stdio: "inherit" });
20780
+ const child = spawn5(this.ssh, this.args(this.host, "true"), { stdio: "inherit" });
20571
20781
  child.on(
20572
20782
  "error",
20573
20783
  (err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
@@ -20590,7 +20800,7 @@ var SshTransport = class {
20590
20800
  isAlive() {
20591
20801
  if (!this.hasControlMaster) return Promise.resolve(true);
20592
20802
  return new Promise((resolve3) => {
20593
- const child = spawn4(this.ssh, this.args("-O", "check", this.host), { stdio: "ignore" });
20803
+ const child = spawn5(this.ssh, this.args("-O", "check", this.host), { stdio: "ignore" });
20594
20804
  child.on("error", () => resolve3(false));
20595
20805
  child.on("close", (code) => resolve3(code === 0));
20596
20806
  });
@@ -20607,7 +20817,7 @@ var SshTransport = class {
20607
20817
  if (!this.hasControlMaster) return Promise.resolve();
20608
20818
  return new Promise((resolve3) => {
20609
20819
  const spec = `${localPort}:127.0.0.1:${remotePort}`;
20610
- const child = spawn4(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20820
+ const child = spawn5(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20611
20821
  stdio: "ignore"
20612
20822
  });
20613
20823
  child.on("error", () => resolve3());
@@ -20616,7 +20826,7 @@ var SshTransport = class {
20616
20826
  }
20617
20827
  exec(command, opts = {}) {
20618
20828
  return new Promise((resolve3) => {
20619
- const child = spawn4(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20829
+ const child = spawn5(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20620
20830
  stdio: ["pipe", "pipe", "pipe"]
20621
20831
  });
20622
20832
  let stdout = "";
@@ -20651,7 +20861,7 @@ var SshTransport = class {
20651
20861
  async copyTo(localPath, destPath) {
20652
20862
  await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
20653
20863
  await new Promise((resolve3, reject) => {
20654
- const child = spawn4(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20864
+ const child = spawn5(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20655
20865
  stdio: ["ignore", "ignore", "inherit"]
20656
20866
  });
20657
20867
  child.on("error", reject);
@@ -20711,7 +20921,7 @@ async function connectRemote(opts) {
20711
20921
  ...opts.onRefreshRequest !== void 0 ? { control: { token: endpoint.token, onRefresh: opts.onRefreshRequest } } : {},
20712
20922
  // The store lives on the CLIENT machine — every cockpit here shares it,
20713
20923
  // whichever remote daemon each one drives.
20714
- localSync: { token: endpoint.token, file: join5(clientHome(), "local-sync.json") }
20924
+ localSync: { token: endpoint.token, file: join6(clientHome(), "local-sync.json") }
20715
20925
  });
20716
20926
  tunnel.onPortChange((port) => ui.setTarget({ host: "127.0.0.1", port }));
20717
20927
  const eventCbs = /* @__PURE__ */ new Set();
@@ -20762,18 +20972,18 @@ async function showLogs(transport, opts) {
20762
20972
 
20763
20973
  // src/lib/registry.ts
20764
20974
  import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
20765
- import { join as join6 } from "node:path";
20975
+ import { join as join7 } from "node:path";
20766
20976
  function cockpitsDir(env) {
20767
- return join6(clientHome(env), "cockpits");
20977
+ return join7(clientHome(env), "cockpits");
20768
20978
  }
20769
20979
  function slug(target) {
20770
20980
  return target.replace(/[^A-Za-z0-9@._-]/g, "_");
20771
20981
  }
20772
20982
  function cockpitRecordPath(target) {
20773
- return join6(cockpitsDir(), `${slug(target)}.json`);
20983
+ return join7(cockpitsDir(), `${slug(target)}.json`);
20774
20984
  }
20775
20985
  function cockpitLogPath(target) {
20776
- return join6(clientHome(), "logs", `cockpit-${slug(target)}.log`);
20986
+ return join7(clientHome(), "logs", `cockpit-${slug(target)}.log`);
20777
20987
  }
20778
20988
  function writeCockpitRecord(record2) {
20779
20989
  mkdirSync4(cockpitsDir(), { recursive: true, mode: 448 });
@@ -20802,7 +21012,7 @@ function listCockpitRecords() {
20802
21012
  for (const name of names) {
20803
21013
  if (!name.endsWith(".json")) continue;
20804
21014
  try {
20805
- records.push(JSON.parse(readFileSync5(join6(cockpitsDir(), name), "utf8")));
21015
+ records.push(JSON.parse(readFileSync5(join7(cockpitsDir(), name), "utf8")));
20806
21016
  } catch {
20807
21017
  }
20808
21018
  }
@@ -20834,7 +21044,7 @@ async function checkCockpit(record2) {
20834
21044
  }
20835
21045
 
20836
21046
  // src/lib/start.ts
20837
- import { join as join7 } from "node:path";
21047
+ import { join as join8 } from "node:path";
20838
21048
  init_types();
20839
21049
  async function startLocal(opts) {
20840
21050
  const logger = opts.logger ?? silentLogger;
@@ -20857,7 +21067,7 @@ async function startLocal(opts) {
20857
21067
  // never squat the daemon's own port
20858
21068
  target: { host: "127.0.0.1", port: endpoint.port },
20859
21069
  ...opts.onRefreshRequest !== void 0 ? { control: { token: endpoint.token, onRefresh: opts.onRefreshRequest } } : {},
20860
- localSync: { token: endpoint.token, file: join7(clientHome(), "local-sync.json") }
21070
+ localSync: { token: endpoint.token, file: join8(clientHome(), "local-sync.json") }
20861
21071
  });
20862
21072
  const eventCbs = /* @__PURE__ */ new Set();
20863
21073
  return {
@@ -20919,7 +21129,7 @@ async function upgradeDaemon(transport, opts = {}) {
20919
21129
  }
20920
21130
 
20921
21131
  // src/cli/detach.ts
20922
- import { spawn as spawn5 } from "node:child_process";
21132
+ import { spawn as spawn6 } from "node:child_process";
20923
21133
  import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync6 } from "node:fs";
20924
21134
  import { dirname as dirname5 } from "node:path";
20925
21135
  init_types();
@@ -20965,7 +21175,7 @@ async function launchDetached(opts) {
20965
21175
  mkdirSync5(dirname5(logFile), { recursive: true });
20966
21176
  const fd = openSync(logFile, "w");
20967
21177
  const argv = opts.argv ?? process.argv.slice(2);
20968
- const child = spawn5(process.execPath, [process.argv[1] ?? "", ...argv], {
21178
+ const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
20969
21179
  detached: true,
20970
21180
  stdio: ["ignore", fd, fd],
20971
21181
  env: { ...process.env, [COCKPIT_CHILD_ENV]: "1" }
@@ -21031,7 +21241,7 @@ function spawnDetachedRefresh(target, argv) {
21031
21241
  const fd = openSync(logFile, "a");
21032
21242
  const env = { ...process.env };
21033
21243
  delete env[COCKPIT_CHILD_ENV];
21034
- const child = spawn5(process.execPath, [process.argv[1] ?? "", ...argv], {
21244
+ const child = spawn6(process.execPath, [process.argv[1] ?? "", ...argv], {
21035
21245
  detached: true,
21036
21246
  stdio: ["ignore", fd, fd],
21037
21247
  env
@@ -21085,8 +21295,8 @@ function timestampedLogger() {
21085
21295
  // src/cli/run.ts
21086
21296
  function assetsDir() {
21087
21297
  const here = dirname6(fileURLToPath3(import.meta.url));
21088
- for (const candidate of [join8(here, "public"), join8(here, "..", "..", "dist", "public")]) {
21089
- if (existsSync3(join8(candidate, "index.html"))) return candidate;
21298
+ for (const candidate of [join9(here, "public"), join9(here, "..", "..", "dist", "public")]) {
21299
+ if (existsSync3(join9(candidate, "index.html"))) return candidate;
21090
21300
  }
21091
21301
  throw new CliError(
21092
21302
  "not_installed",
@@ -21102,7 +21312,7 @@ async function openTarget(host) {
21102
21312
  throw new CliError(
21103
21313
  "not_installed",
21104
21314
  `no puddle daemon is set up on ${transport.label}`,
21105
- host === void 0 ? "run: puddle start" : `run: puddle connect ${host}`
21315
+ host === void 0 ? "run: puddle launch" : `run: puddle launch ${host}`
21106
21316
  );
21107
21317
  }
21108
21318
  const daemonPort = await readDaemonPort(transport);
@@ -21141,7 +21351,7 @@ ${farewell}
21141
21351
  });
21142
21352
  });
21143
21353
  }
21144
- async function run(command) {
21354
+ async function run2(command) {
21145
21355
  const logger = terminalLogger();
21146
21356
  switch (command.cmd) {
21147
21357
  case "help":
@@ -21151,9 +21361,8 @@ async function run(command) {
21151
21361
  process.stdout.write(`puddle ${cliVersion()}
21152
21362
  `);
21153
21363
  return 0;
21154
- case "start":
21155
- case "connect": {
21156
- const target = command.cmd === "connect" ? command.host : "local";
21364
+ case "launch": {
21365
+ const target = command.target;
21157
21366
  if (!command.foreground && !isCockpitChild()) {
21158
21367
  return launchDetached({ target, noBrowser: command.noBrowser, logger });
21159
21368
  }
@@ -21203,11 +21412,11 @@ async function run(command) {
21203
21412
  noUpgrade: command.noUpgrade,
21204
21413
  foreground: command.foreground
21205
21414
  };
21206
- const next = target === "local" ? { cmd: "start", ...shared } : {
21207
- cmd: "connect",
21208
- host: target,
21415
+ const next = {
21416
+ cmd: "launch",
21417
+ target,
21209
21418
  ...shared,
21210
- ...command.remotePort !== void 0 ? { remotePort: command.remotePort } : {}
21419
+ ...target !== "local" && command.remotePort !== void 0 ? { remotePort: command.remotePort } : {}
21211
21420
  };
21212
21421
  if (!command.foreground && !isCockpitChild()) {
21213
21422
  return launchDetached({
@@ -21370,18 +21579,88 @@ async function run(command) {
21370
21579
  }
21371
21580
  }
21372
21581
  case "upgrade": {
21373
- const transport = command.host === void 0 ? new LocalTransport() : new SshTransport(command.host);
21374
- if (transport instanceof SshTransport) await transport.open();
21375
- try {
21376
- const result = await upgradeDaemon(transport, { logger });
21377
- logger.info(`puddled ${result.from ?? "(unmanaged)"} \u2192 ${result.to} on ${transport.label}`);
21378
- return 0;
21379
- } finally {
21380
- transport.dispose();
21582
+ const local = command.host === void 0 || command.host === "local";
21583
+ if (command.what === "daemon") {
21584
+ const transport = local ? new LocalTransport() : new SshTransport(command.host);
21585
+ if (transport instanceof SshTransport) await transport.open();
21586
+ try {
21587
+ const result = await upgradeDaemon(transport, { logger });
21588
+ logger.info(
21589
+ `puddled ${result.from ?? "(unmanaged)"} \u2192 ${result.to} on ${transport.label}`
21590
+ );
21591
+ return 0;
21592
+ } finally {
21593
+ transport.dispose();
21594
+ }
21381
21595
  }
21596
+ if (!local) {
21597
+ throw new CliError(
21598
+ "bad_arguments",
21599
+ `puddle upgrade ${command.what} runs on the machine it upgrades`,
21600
+ `ssh into ${command.host} and run it there \u2014 user@host targets the daemon only`
21601
+ );
21602
+ }
21603
+ return command.what === "cli" ? upgradeCli(logger) : upgradeDesktop(logger);
21382
21604
  }
21383
21605
  }
21384
21606
  }
21607
+ function upgradeCli(logger) {
21608
+ logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for the latest release`);
21609
+ return new Promise((resolve3, reject) => {
21610
+ const child = spawn7("npm", ["install", "-g", "@puddle-code/cli@latest"], {
21611
+ stdio: "inherit"
21612
+ });
21613
+ child.on(
21614
+ "error",
21615
+ () => reject(
21616
+ new CliError(
21617
+ "not_installed",
21618
+ "npm is not on PATH",
21619
+ "the CLI is installed and upgraded via npm: npm install -g @puddle-code/cli@latest"
21620
+ )
21621
+ )
21622
+ );
21623
+ child.on("exit", (code) => {
21624
+ if (code === 0) {
21625
+ logger.info("done \u2014 `puddle --version` shows the installed version");
21626
+ resolve3(0);
21627
+ } else {
21628
+ reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
21629
+ }
21630
+ });
21631
+ });
21632
+ }
21633
+ async function upgradeDesktop(logger) {
21634
+ const installed = await findInstalledDesktopApp();
21635
+ if (installed === null) {
21636
+ throw new CliError(
21637
+ "not_installed",
21638
+ "no Puddle.app in /Applications or ~/Applications",
21639
+ process.platform === "linux" ? "AppImages carry no fixed path \u2014 update from the app itself (its banner knows $APPIMAGE)" : void 0
21640
+ );
21641
+ }
21642
+ if (await isDesktopAppRunning(installed.appPath)) {
21643
+ throw new CliError(
21644
+ "already_running",
21645
+ "Puddle is running \u2014 use its update banner, or quit it and rerun"
21646
+ );
21647
+ }
21648
+ const update = await checkForDesktopUpdate(installed.version);
21649
+ if (update === null) {
21650
+ logger.info(`Puddle ${installed.version} is already the latest release`);
21651
+ return 0;
21652
+ }
21653
+ logger.info(`Puddle ${installed.version} \u2192 ${update.version}`);
21654
+ const staged = await stageDesktopUpdate(update, { logger });
21655
+ await applyDesktopUpdate(staged, {
21656
+ targetPath: installed.appPath,
21657
+ detach: false,
21658
+ relaunch: false,
21659
+ logger
21660
+ });
21661
+ logger.info(`Puddle ${update.version} installed at ${installed.appPath}`);
21662
+ return 0;
21663
+ }
21385
21664
  async function runCockpit(command, target, terminal) {
21386
21665
  const detached = isCockpitChild();
21387
21666
  const logger = detached ? timestampedLogger() : terminal;
@@ -21413,14 +21692,15 @@ async function runCockpit(command, target, terminal) {
21413
21692
  "--no-browser",
21414
21693
  // the requesting tab reloads itself; no second tab
21415
21694
  ...command.port !== void 0 ? ["--port", String(command.port)] : [],
21416
- ...command.cmd === "connect" && command.remotePort !== void 0 ? ["--remote-port", String(command.remotePort)] : [],
21695
+ ...command.remotePort !== void 0 ? ["--remote-port", String(command.remotePort)] : [],
21417
21696
  ...command.tarball !== void 0 ? ["--tarball", command.tarball] : [],
21418
21697
  ...command.noUpgrade ? ["--no-upgrade"] : []
21419
21698
  ]);
21420
21699
  };
21421
21700
  let cockpit;
21422
21701
  try {
21423
- cockpit = command.cmd === "start" ? await startLocal({ ...command, assetsDir: assetsDir(), logger, onRefreshRequest }) : await connectRemote({ ...command, assetsDir: assetsDir(), logger, onRefreshRequest });
21702
+ const common = { ...command, assetsDir: assetsDir(), logger, onRefreshRequest };
21703
+ cockpit = command.target === "local" ? await startLocal(common) : await connectRemote({ ...common, host: command.target });
21424
21704
  } catch (err) {
21425
21705
  if (err instanceof CliError) {
21426
21706
  writeCockpitRecord({
@@ -21441,13 +21721,13 @@ async function runCockpit(command, target, terminal) {
21441
21721
  browserUrl: cockpit.browserUrl,
21442
21722
  nonce: cockpit.nonce
21443
21723
  });
21444
- const arrow = command.cmd === "connect" ? ` \u2192 ${command.host}` : "";
21724
+ const arrow = command.target === "local" ? "" : ` \u2192 ${command.target}`;
21445
21725
  logger.info(`puddle cockpit at ${cockpit.origin}${arrow} (daemon ${cockpit.daemon.version})`);
21446
21726
  if (!detached) {
21447
21727
  if (!command.noBrowser) openBrowser(cockpit.browserUrl);
21448
21728
  logger.info(`open: ${cockpit.browserUrl}`);
21449
21729
  }
21450
- const where = command.cmd === "start" ? "this machine" : command.host;
21730
+ const where = command.target === "local" ? "this machine" : command.target;
21451
21731
  await runUntilInterrupted(async () => {
21452
21732
  await cockpit.stop();
21453
21733
  removeCockpitRecord(target);
@@ -21462,7 +21742,7 @@ function pad(text, width) {
21462
21742
  init_types();
21463
21743
  try {
21464
21744
  const command = parseArgs(process.argv.slice(2));
21465
- process.exit(await run(command));
21745
+ process.exit(await run2(command));
21466
21746
  } catch (err) {
21467
21747
  if (err instanceof CliError) {
21468
21748
  process.stderr.write(`puddle: ${err.message}