@puddle-code/cli 0.2.0 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -7,33 +7,11 @@ Past releases: see docs/changelogs/.
7
7
 
8
8
  # Changelog
9
9
 
10
- ## [0.2.0] — 2026-09-21
11
-
12
- ### Added
13
-
14
- - Add Settings → Remote access in local and SSH desktop cockpits for enablement, status, QR/link pairing, browser approval/revocation, disablement and host identity recovery; add authenticated cockpit controls in protocol 18.1.
15
- - Add self-hosted mobile access with Google/GitHub login, optional authenticator MFA, independently supervised host connectors and QR/link browser pairing.
16
- - Add a single-terminal phone view, native multiline prompt composer, visible terminal keys and read-only text/change review without rewriting desktop layouts.
17
- - Add remote administration and recovery commands, deployment images and isolated relay/browser acceptance suites.
18
- - Add lightweight built-process and loopback OpenSSH authentication suites, with separate manual browser and desktop acceptance.
19
-
20
- ### Changed
21
-
22
- - Open remote account registration to every verified Google/GitHub identity; remove the signup allowlist and registration switch while retaining account isolation and explicit host approval.
23
-
24
- ### Removed
25
-
26
- - Remove the TLS contact email setting from the Caddy deployment and environment example.
27
- - Remove email/password login, verification/reset email delivery, SMTP configuration and Nodemailer; require Google and/or GitHub OAuth, retain verified-email admission and optional authenticator MFA, and retire legacy passwords/sessions. Bump remote protocol to 2 and daemon/cockpit protocol to 19.0 for the changed pairing contract.
10
+ ## [0.2.1] — 2026-09-21
28
11
 
29
12
  ### Fixed
30
13
 
31
- - Reject incomplete remote-origin input without throwing during registration form rendering.
32
- - Preserve browser login, terminal snapshots and refresh correlation across cockpit replacement and upstream recovery; retain desktop routes when the origin moves.
33
-
34
- ### Security
35
-
36
- - Require exact browser approval at the host, pinned Noise XX transport, bounded forwarding and fresh browser participation in the existing host leases; keep remote protocol 2 independent of the protocol-18 host lease foundation.
37
- - Persist device revocation and offline remote disable, isolate application delivery from the relay origin, and deny unreviewed remote routes and executable previews.
38
- - Replace distributed master credentials with private host control, short-lived renewable connection leases and single-use browser invitations; migrate to protocol 18.0 and require existing tabs to run `puddle launch` once.
39
- - Isolate forwarded applications on a separate loopback origin, bind proxy grants to browser authorisation and revoke active streams when their authority expires.
14
+ - Fix daemon and cockpit startup on legacy installations by migrating owned Puddle homes from `0755` to `0700`, and create fresh installer homes privately.
15
+ - Fix self-hosted application builds with filtered dependencies by separating the production Vite configuration from the development cockpit gateway.
16
+ - Fix remote container builds and static file serving from checkouts with private file permissions.
17
+ - Remove Caddy's unused privileged-port capability so the application container starts with all capabilities dropped.
@@ -16043,6 +16043,9 @@ import { createHash, randomBytes } from "node:crypto";
16043
16043
  import {
16044
16044
  chmodSync,
16045
16045
  closeSync,
16046
+ constants,
16047
+ fchmodSync,
16048
+ fstatSync,
16046
16049
  fsyncSync,
16047
16050
  lstatSync,
16048
16051
  mkdirSync,
@@ -16053,7 +16056,7 @@ import {
16053
16056
  rmSync,
16054
16057
  writeFileSync
16055
16058
  } from "node:fs";
16056
- import { dirname, join } from "node:path";
16059
+ import { dirname, join, resolve } from "node:path";
16057
16060
  var digest = (value) => createHash("sha256").update(value).digest("hex");
16058
16061
  function privatePath(path, directory = false) {
16059
16062
  const st = lstatSync(path);
@@ -16065,8 +16068,29 @@ function privateDirectory(path) {
16065
16068
  mkdirSync(path, { recursive: true, mode: 448 });
16066
16069
  privatePath(path, true);
16067
16070
  }
16071
+ function initialisePrivateHome(home) {
16072
+ const path = resolve(home);
16073
+ mkdirSync(path, { recursive: true, mode: 448 });
16074
+ if (process.platform === "win32") {
16075
+ privatePath(path, true);
16076
+ return;
16077
+ }
16078
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
16079
+ try {
16080
+ const st = fstatSync(fd);
16081
+ if (!st.isDirectory() || st.uid !== process.getuid() || (st.mode & 18) !== 0) {
16082
+ throw new Error(
16083
+ `Puddle home must be owned by the current user and not writable by other users: ${path}`
16084
+ );
16085
+ }
16086
+ if ((st.mode & 63) !== 0) fchmodSync(fd, 448);
16087
+ } finally {
16088
+ closeSync(fd);
16089
+ }
16090
+ privatePath(path, true);
16091
+ }
16068
16092
  function ipcPath(home, scope) {
16069
- privateDirectory(home);
16093
+ initialisePrivateHome(home);
16070
16094
  const key = digest(`${realpathSync(home)}\0${scope}`).slice(0, 24);
16071
16095
  if (process.platform === "win32") return `\\\\.\\pipe\\puddle-${key}`;
16072
16096
  const dir = join("/tmp", `puddle-${process.getuid()}-${key}`);
@@ -16189,7 +16213,7 @@ async function inspectHostLocally(home) {
16189
16213
  }
16190
16214
  const socket = connect(path);
16191
16215
  try {
16192
- const grant = await new Promise((resolve, reject) => {
16216
+ const grant = await new Promise((resolve2, reject) => {
16193
16217
  socket.setTimeout(5e3, () => socket.destroy(new Error("Host inspection timed out")));
16194
16218
  socket.once("error", reject);
16195
16219
  socket.once("close", () => reject(new Error("Host control closed")));
@@ -16199,7 +16223,7 @@ async function inspectHostLocally(home) {
16199
16223
  const message = controlResponseSchema.parse(value);
16200
16224
  if (message.t !== "authority" || !message.token)
16201
16225
  throw new Error("Host authority unavailable");
16202
- resolve(message);
16226
+ resolve2(message);
16203
16227
  },
16204
16228
  () => socket.destroy(new Error("Invalid host control response"))
16205
16229
  );
package/dist/index.js CHANGED
@@ -3756,7 +3756,7 @@ var init_wrapper = __esm({
3756
3756
 
3757
3757
  // src/lib/ws-client.ts
3758
3758
  function connectGateway(port, authority) {
3759
- return new Promise((resolve4, reject) => {
3759
+ return new Promise((resolve5, reject) => {
3760
3760
  const ws = new wrapper_default(`ws://127.0.0.1:${port}/ws`, {
3761
3761
  headers: { host: `localhost:${port}` }
3762
3762
  });
@@ -3781,7 +3781,7 @@ function connectGateway(port, authority) {
3781
3781
  }
3782
3782
  if (parsed.t === "authenticated") {
3783
3783
  clearTimeout(timer);
3784
- resolve4({
3784
+ resolve5({
3785
3785
  send(message) {
3786
3786
  if (resource.valid() && ws.readyState === wrapper_default.OPEN)
3787
3787
  ws.send(JSON.stringify(message));
@@ -3855,7 +3855,7 @@ async function attachSession(opts) {
3855
3855
  rows: streams.stdout.rows ?? 24
3856
3856
  });
3857
3857
  gateway.send({ t: "attach", session: session.id, term, ...dims2() });
3858
- return new Promise((resolve4) => {
3858
+ return new Promise((resolve5) => {
3859
3859
  let settled = false;
3860
3860
  const finish = (outcome) => {
3861
3861
  if (settled) return;
@@ -3864,7 +3864,7 @@ async function attachSession(opts) {
3864
3864
  stdin.removeListener("data", onStdin);
3865
3865
  unResize?.();
3866
3866
  gateway.close();
3867
- resolve4(outcome);
3867
+ resolve5(outcome);
3868
3868
  };
3869
3869
  gateway.onMessage((message) => {
3870
3870
  switch (message.t) {
@@ -19987,7 +19987,7 @@ var LocalTransport = class {
19987
19987
  kind = "local";
19988
19988
  label = "this machine";
19989
19989
  exec(command, opts = {}) {
19990
- return new Promise((resolve4) => {
19990
+ return new Promise((resolve5) => {
19991
19991
  const child = spawn("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
19992
19992
  let stdout = "";
19993
19993
  let stderr = "";
@@ -20004,11 +20004,11 @@ var LocalTransport = class {
20004
20004
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20005
20005
  child.on("error", (err) => {
20006
20006
  if (timer) clearTimeout(timer);
20007
- resolve4({ code: -1, stdout, stderr: stderr + String(err) });
20007
+ resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20008
20008
  });
20009
20009
  child.on("close", (code) => {
20010
20010
  if (timer) clearTimeout(timer);
20011
- resolve4({ code: code ?? -1, stdout, stderr });
20011
+ resolve5({ code: code ?? -1, stdout, stderr });
20012
20012
  });
20013
20013
  child.stdin.on("error", () => {
20014
20014
  });
@@ -20102,7 +20102,7 @@ var SshTransport = class {
20102
20102
  * live control socket behind.
20103
20103
  */
20104
20104
  open() {
20105
- return new Promise((resolve4, reject) => {
20105
+ return new Promise((resolve5, reject) => {
20106
20106
  const child = spawn2(this.ssh, this.args(this.host, "true"), {
20107
20107
  // A GUI has no useful inherited terminal; OpenSSH calls its askpass
20108
20108
  // helper instead. The terminal CLI keeps byte-for-byte inheritance.
@@ -20114,7 +20114,7 @@ var SshTransport = class {
20114
20114
  (err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
20115
20115
  );
20116
20116
  child.on("close", (code) => {
20117
- if (code === 0) resolve4();
20117
+ if (code === 0) resolve5();
20118
20118
  else {
20119
20119
  reject(
20120
20120
  new CliError(
@@ -20130,13 +20130,13 @@ var SshTransport = class {
20130
20130
  /** Whether the master connection is still alive (-O check). */
20131
20131
  isAlive() {
20132
20132
  if (!this.hasControlMaster) return Promise.resolve(true);
20133
- return new Promise((resolve4) => {
20133
+ return new Promise((resolve5) => {
20134
20134
  const child = spawn2(this.ssh, this.args("-O", "check", this.host), {
20135
20135
  stdio: "ignore",
20136
20136
  env: this.spawnEnv()
20137
20137
  });
20138
- child.on("error", () => resolve4(false));
20139
- child.on("close", (code) => resolve4(code === 0));
20138
+ child.on("error", () => resolve5(false));
20139
+ child.on("close", (code) => resolve5(code === 0));
20140
20140
  });
20141
20141
  }
20142
20142
  /**
@@ -20149,18 +20149,18 @@ var SshTransport = class {
20149
20149
  */
20150
20150
  cancelForward(localPort, remotePort) {
20151
20151
  if (!this.hasControlMaster) return Promise.resolve();
20152
- return new Promise((resolve4) => {
20152
+ return new Promise((resolve5) => {
20153
20153
  const spec = `${localPort}:127.0.0.1:${remotePort}`;
20154
20154
  const child = spawn2(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
20155
20155
  stdio: "ignore",
20156
20156
  env: this.spawnEnv()
20157
20157
  });
20158
- child.on("error", () => resolve4());
20159
- child.on("close", () => resolve4());
20158
+ child.on("error", () => resolve5());
20159
+ child.on("close", () => resolve5());
20160
20160
  });
20161
20161
  }
20162
20162
  exec(command, opts = {}) {
20163
- return new Promise((resolve4) => {
20163
+ return new Promise((resolve5) => {
20164
20164
  const child = spawn2(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
20165
20165
  stdio: ["pipe", "pipe", "pipe"],
20166
20166
  env: this.spawnEnv()
@@ -20180,11 +20180,11 @@ var SshTransport = class {
20180
20180
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20181
20181
  child.on("error", (err) => {
20182
20182
  if (timer) clearTimeout(timer);
20183
- resolve4({ code: -1, stdout, stderr: stderr + String(err) });
20183
+ resolve5({ code: -1, stdout, stderr: stderr + String(err) });
20184
20184
  });
20185
20185
  child.on("close", (code) => {
20186
20186
  if (timer) clearTimeout(timer);
20187
- resolve4({ code: code ?? -1, stdout, stderr });
20187
+ resolve5({ code: code ?? -1, stdout, stderr });
20188
20188
  });
20189
20189
  child.stdin.on("error", () => {
20190
20190
  });
@@ -20204,11 +20204,11 @@ var SshTransport = class {
20204
20204
  });
20205
20205
  let stdout = "";
20206
20206
  let stderr = "";
20207
- const result = new Promise((resolve4) => {
20207
+ const result = new Promise((resolve5) => {
20208
20208
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
20209
20209
  child.stderr.on("data", (chunk) => stderr += chunk.toString());
20210
- child.on("error", (err) => resolve4({ code: -1, stdout, stderr: stderr + String(err) }));
20211
- child.on("close", (code) => resolve4({ code: code ?? -1, stdout, stderr }));
20210
+ child.on("error", (err) => resolve5({ code: -1, stdout, stderr: stderr + String(err) }));
20211
+ child.on("close", (code) => resolve5({ code: code ?? -1, stdout, stderr }));
20212
20212
  });
20213
20213
  return {
20214
20214
  result,
@@ -20223,14 +20223,14 @@ var SshTransport = class {
20223
20223
  }
20224
20224
  async copyTo(localPath, destPath) {
20225
20225
  await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
20226
- await new Promise((resolve4, reject) => {
20226
+ await new Promise((resolve5, reject) => {
20227
20227
  const child = spawn2(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
20228
20228
  stdio: ["ignore", "ignore", "inherit"],
20229
20229
  env: this.spawnEnv()
20230
20230
  });
20231
20231
  child.on("error", reject);
20232
20232
  child.on("close", (code) => {
20233
- if (code === 0) resolve4();
20233
+ if (code === 0) resolve5();
20234
20234
  else reject(new CliError("ssh_unreachable", `scp to ${this.host} failed (exit ${code})`));
20235
20235
  });
20236
20236
  });
@@ -20331,7 +20331,7 @@ async function registrationCode() {
20331
20331
  const readline = createInterface({ input: process.stdin, output: silent, terminal: true });
20332
20332
  try {
20333
20333
  return remoteSecretSchema.parse(
20334
- await new Promise((resolve4) => readline.question("", resolve4))
20334
+ await new Promise((resolve5) => readline.question("", resolve5))
20335
20335
  );
20336
20336
  } finally {
20337
20337
  readline.close();
@@ -20733,6 +20733,9 @@ import { createHash, randomBytes } from "node:crypto";
20733
20733
  import {
20734
20734
  chmodSync,
20735
20735
  closeSync,
20736
+ constants,
20737
+ fchmodSync,
20738
+ fstatSync,
20736
20739
  fsyncSync,
20737
20740
  lstatSync,
20738
20741
  mkdirSync as mkdirSync3,
@@ -20744,7 +20747,7 @@ import {
20744
20747
  writeFileSync
20745
20748
  } from "node:fs";
20746
20749
  import { connect, createServer } from "node:net";
20747
- import { dirname as dirname2, join as join2 } from "node:path";
20750
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
20748
20751
  var digest = (value) => createHash("sha256").update(value).digest("hex");
20749
20752
  var secret = (prefix = "") => prefix + randomBytes(32).toString("hex");
20750
20753
  function privatePath(path, directory = false) {
@@ -20757,6 +20760,27 @@ function privateDirectory(path) {
20757
20760
  mkdirSync3(path, { recursive: true, mode: 448 });
20758
20761
  privatePath(path, true);
20759
20762
  }
20763
+ function initialisePrivateHome(home) {
20764
+ const path = resolve(home);
20765
+ mkdirSync3(path, { recursive: true, mode: 448 });
20766
+ if (process.platform === "win32") {
20767
+ privatePath(path, true);
20768
+ return;
20769
+ }
20770
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
20771
+ try {
20772
+ const st = fstatSync(fd);
20773
+ if (!st.isDirectory() || st.uid !== process.getuid() || (st.mode & 18) !== 0) {
20774
+ throw new Error(
20775
+ `Puddle home must be owned by the current user and not writable by other users: ${path}`
20776
+ );
20777
+ }
20778
+ if ((st.mode & 63) !== 0) fchmodSync(fd, 448);
20779
+ } finally {
20780
+ closeSync(fd);
20781
+ }
20782
+ privatePath(path, true);
20783
+ }
20760
20784
  function atomicPrivateJson(path, value) {
20761
20785
  privateDirectory(dirname2(path));
20762
20786
  const tmp = `${path}.${secret()}.tmp`;
@@ -20778,7 +20802,7 @@ function readPrivateJson(path) {
20778
20802
  return JSON.parse(readFileSync2(path, "utf8"));
20779
20803
  }
20780
20804
  function ipcPath(home, scope) {
20781
- privateDirectory(home);
20805
+ initialisePrivateHome(home);
20782
20806
  const key = digest(`${realpathSync(home)}\0${scope}`).slice(0, 24);
20783
20807
  if (process.platform === "win32") return `\\\\.\\pipe\\puddle-${key}`;
20784
20808
  const dir = join2("/tmp", `puddle-${process.getuid()}-${key}`);
@@ -20791,14 +20815,14 @@ async function listenPrivate(path, onConnection) {
20791
20815
  privatePath(path);
20792
20816
  const before = lstatSync(path);
20793
20817
  if (!before.isSocket()) throw new Error("Puddle control path is not a socket");
20794
- const stale = await new Promise((resolve4, reject) => {
20818
+ const stale = await new Promise((resolve5, reject) => {
20795
20819
  const probe = connect(path);
20796
20820
  probe.once("connect", () => {
20797
20821
  probe.destroy();
20798
- resolve4(false);
20822
+ resolve5(false);
20799
20823
  });
20800
20824
  probe.once("error", (err) => {
20801
- if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve4(true);
20825
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") resolve5(true);
20802
20826
  else reject(err);
20803
20827
  });
20804
20828
  });
@@ -20811,11 +20835,11 @@ async function listenPrivate(path, onConnection) {
20811
20835
  }
20812
20836
  }
20813
20837
  const server = createServer(onConnection);
20814
- await new Promise((resolve4, reject) => {
20838
+ await new Promise((resolve5, reject) => {
20815
20839
  server.once("error", reject);
20816
20840
  server.listen(path, () => {
20817
20841
  server.off("error", reject);
20818
- resolve4();
20842
+ resolve5();
20819
20843
  });
20820
20844
  });
20821
20845
  if (process.platform !== "win32") chmodSync(path, 384);
@@ -20929,7 +20953,7 @@ var HostControlClient = class {
20929
20953
  }
20930
20954
  this.channel = channel;
20931
20955
  this.until = performance.now() + CONNECTION_POLICY.leaseMs;
20932
- await new Promise((resolve4, reject) => {
20956
+ await new Promise((resolve5, reject) => {
20933
20957
  let pendingAt = null;
20934
20958
  let initial = true;
20935
20959
  const send = (message, sentAt = performance.now()) => {
@@ -20982,7 +21006,7 @@ var HostControlClient = class {
20982
21006
  if (this.channel !== channel) return;
20983
21007
  this.status = "ready";
20984
21008
  this.emit();
20985
- resolve4();
21009
+ resolve5();
20986
21010
  }).catch(() => channel.destroy());
20987
21011
  }
20988
21012
  },
@@ -21103,7 +21127,7 @@ async function inspectHostLocally(home) {
21103
21127
  }
21104
21128
  const socket = connect2(path);
21105
21129
  try {
21106
- const grant = await new Promise((resolve4, reject) => {
21130
+ const grant = await new Promise((resolve5, reject) => {
21107
21131
  socket.setTimeout(5e3, () => socket.destroy(new Error("Host inspection timed out")));
21108
21132
  socket.once("error", reject);
21109
21133
  socket.once("close", () => reject(new Error("Host control closed")));
@@ -21113,7 +21137,7 @@ async function inspectHostLocally(home) {
21113
21137
  const message = controlResponseSchema.parse(value);
21114
21138
  if (message.t !== "authority" || !message.token)
21115
21139
  throw new Error("Host authority unavailable");
21116
- resolve4(message);
21140
+ resolve5(message);
21117
21141
  },
21118
21142
  () => socket.destroy(new Error("Invalid host control response"))
21119
21143
  );
@@ -21254,12 +21278,12 @@ import {
21254
21278
  renameSync as renameSync2,
21255
21279
  writeFileSync as writeFileSync2
21256
21280
  } from "node:fs";
21257
- import { dirname as dirname6, join as join7, resolve } from "node:path";
21281
+ import { dirname as dirname6, join as join7, resolve as resolve2 } from "node:path";
21258
21282
 
21259
21283
  // src/lib/desktop-update.ts
21260
21284
  import { createHash as createHash2 } from "node:crypto";
21261
21285
  import { spawn as spawn4 } from "node:child_process";
21262
- import { constants, createReadStream, createWriteStream } from "node:fs";
21286
+ import { constants as constants2, createReadStream, createWriteStream } from "node:fs";
21263
21287
  import { access, chmod, mkdir, readdir, rm, writeFile } from "node:fs/promises";
21264
21288
  import { dirname as dirname5, join as join6 } from "node:path";
21265
21289
  import { Readable } from "node:stream";
@@ -21271,7 +21295,7 @@ import { readFileSync as readFileSync4 } from "node:fs";
21271
21295
  import { dirname as dirname4, join as join5 } from "node:path";
21272
21296
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21273
21297
  function cliVersion() {
21274
- if (true) return "0.2.0";
21298
+ if (true) return "0.2.1";
21275
21299
  const here = dirname4(fileURLToPath2(import.meta.url));
21276
21300
  for (const candidate of [
21277
21301
  join5(here, "..", "..", "package.json"),
@@ -21539,7 +21563,7 @@ async function desktopAppInstallPath(opts = {}) {
21539
21563
  const systemApplications = opts.systemApplicationsDir ?? "/Applications";
21540
21564
  const canWrite = opts.canWrite ?? (async (path) => {
21541
21565
  try {
21542
- await access(path, constants.W_OK);
21566
+ await access(path, constants2.W_OK);
21543
21567
  return true;
21544
21568
  } catch {
21545
21569
  return false;
@@ -21551,12 +21575,12 @@ async function desktopAppInstallPath(opts = {}) {
21551
21575
  return join6(userApplications, "Puddle.app");
21552
21576
  }
21553
21577
  function isDesktopAppRunning(appPath) {
21554
- return new Promise((resolve4) => {
21578
+ return new Promise((resolve5) => {
21555
21579
  const child = spawn4("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
21556
21580
  stdio: "ignore"
21557
21581
  });
21558
- child.on("error", () => resolve4(false));
21559
- child.on("exit", (code) => resolve4(code === 0));
21582
+ child.on("error", () => resolve5(false));
21583
+ child.on("exit", (code) => resolve5(code === 0));
21560
21584
  });
21561
21585
  }
21562
21586
  function fileSha256(path) {
@@ -21564,12 +21588,12 @@ function fileSha256(path) {
21564
21588
  return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
21565
21589
  }
21566
21590
  function run(command, args) {
21567
- return new Promise((resolve4, reject) => {
21591
+ return new Promise((resolve5, reject) => {
21568
21592
  const child = spawn4(command, args, { stdio: "ignore" });
21569
21593
  child.on("error", reject);
21570
21594
  child.on(
21571
21595
  "exit",
21572
- (code) => code === 0 ? resolve4() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21596
+ (code) => code === 0 ? resolve5() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
21573
21597
  );
21574
21598
  });
21575
21599
  }
@@ -21644,7 +21668,8 @@ var RELEASE_PROTOCOLS = {
21644
21668
  "0.1.11": { major: 17, minor: 3 },
21645
21669
  "0.1.12": { major: 17, minor: 3 },
21646
21670
  "0.1.13": { major: 17, minor: 3 },
21647
- "0.2.0": { major: 19, minor: 0 }
21671
+ "0.2.0": { major: 19, minor: 0 },
21672
+ "0.2.1": { major: 19, minor: 0 }
21648
21673
  };
21649
21674
  function validProtocol(value) {
21650
21675
  if (typeof value !== "object" || value === null) return false;
@@ -21732,7 +21757,7 @@ async function installedComponentVersions(opts = {}) {
21732
21757
  };
21733
21758
  }
21734
21759
  }
21735
- const legacyLinuxPath = resolve(home, "..", "puddle", "Puddle.AppImage");
21760
+ const legacyLinuxPath = resolve2(home, "..", "puddle", "Puddle.AppImage");
21736
21761
  const legacyLinuxDesktop = desktopInstallation === null && (opts.platform ?? process.platform) === "linux" && existsSync2(legacyLinuxPath);
21737
21762
  const desktopProtocol = desktopInstallation?.protocol ?? (desktopInstallation ? componentProtocolForVersion(desktopInstallation.version) : void 0);
21738
21763
  const desktop = desktopInstallation !== null ? {
@@ -21761,7 +21786,7 @@ import { randomUUID } from "node:crypto";
21761
21786
 
21762
21787
  // src/lib/bootstrap.ts
21763
21788
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
21764
- import { dirname as dirname7, join as join8, resolve as resolve2 } from "node:path";
21789
+ import { dirname as dirname7, join as join8, resolve as resolve3 } from "node:path";
21765
21790
  import { fileURLToPath as fileURLToPath3 } from "node:url";
21766
21791
  init_types();
21767
21792
  async function installedVersion(transport) {
@@ -21780,9 +21805,9 @@ async function installDaemon(transport, opts = {}) {
21780
21805
  throw new CliError("not_installed", `tarball not found: ${opts.tarball}`);
21781
21806
  }
21782
21807
  if (transport.kind === "local") {
21783
- args.push("--tarball", resolve2(opts.tarball));
21808
+ args.push("--tarball", resolve3(opts.tarball));
21784
21809
  const localSums = `${opts.tarball}.sha256`;
21785
- if (existsSync3(localSums)) args.push("--sums", resolve2(localSums));
21810
+ if (existsSync3(localSums)) args.push("--sums", resolve3(localSums));
21786
21811
  } else {
21787
21812
  const base = opts.tarball.split("/").pop() ?? "puddled.tar.gz";
21788
21813
  const destTarball = `.puddle/cache/${base}`;
@@ -21847,22 +21872,22 @@ function readInstallScript() {
21847
21872
  // src/lib/net.ts
21848
21873
  import { connect as connect4, createServer as createServer2 } from "node:net";
21849
21874
  function findFreePort() {
21850
- return new Promise((resolve4, reject) => {
21875
+ return new Promise((resolve5, reject) => {
21851
21876
  const srv = createServer2();
21852
21877
  srv.once("error", reject);
21853
21878
  srv.listen(0, "127.0.0.1", () => {
21854
21879
  const address = srv.address();
21855
21880
  const port = typeof address === "object" && address !== null ? address.port : 0;
21856
- srv.close(() => resolve4(port));
21881
+ srv.close(() => resolve5(port));
21857
21882
  });
21858
21883
  });
21859
21884
  }
21860
21885
  function tcpListening(port, timeoutMs = 500) {
21861
- return new Promise((resolve4) => {
21886
+ return new Promise((resolve5) => {
21862
21887
  const sock = connect4({ host: "127.0.0.1", port });
21863
21888
  const done = (ok) => {
21864
21889
  sock.destroy();
21865
- resolve4(ok);
21890
+ resolve5(ok);
21866
21891
  };
21867
21892
  sock.setTimeout(timeoutMs, () => done(false));
21868
21893
  sock.once("connect", () => done(true));
@@ -22311,7 +22336,7 @@ async function startLauncher(home, identity, invite) {
22311
22336
  path,
22312
22337
  close: async () => {
22313
22338
  for (const socket of sockets) socket.destroy();
22314
- await new Promise((resolve4) => server.close(() => resolve4()));
22339
+ await new Promise((resolve5) => server.close(() => resolve5()));
22315
22340
  }
22316
22341
  };
22317
22342
  }
@@ -22321,14 +22346,14 @@ async function requestInvitation(home, path) {
22321
22346
  const stored = readPrivateJson(secretFile(home, path));
22322
22347
  pipeSecret = stored.secret;
22323
22348
  } else privatePath(path);
22324
- return new Promise((resolve4, reject) => {
22349
+ return new Promise((resolve5, reject) => {
22325
22350
  const socket = connect5(path);
22326
22351
  socket.setTimeout(5e3, () => socket.destroy(new Error("Launcher did not answer")));
22327
22352
  jsonLines(
22328
22353
  socket,
22329
22354
  (value) => {
22330
22355
  const message = launcherResponseSchema.parse(value);
22331
- resolve4(message.url);
22356
+ resolve5(message.url);
22332
22357
  socket.end();
22333
22358
  },
22334
22359
  () => socket.destroy(new Error("Invalid launcher reply"))
@@ -22548,7 +22573,7 @@ function writeStore(file2, store) {
22548
22573
  renameSync3(tmp, file2);
22549
22574
  }
22550
22575
  function readBody(req) {
22551
- return new Promise((resolve4, reject) => {
22576
+ return new Promise((resolve5, reject) => {
22552
22577
  let size = 0;
22553
22578
  const chunks = [];
22554
22579
  req.on("data", (chunk) => {
@@ -22560,7 +22585,7 @@ function readBody(req) {
22560
22585
  }
22561
22586
  chunks.push(chunk);
22562
22587
  });
22563
- req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
22588
+ req.on("end", () => resolve5(Buffer.concat(chunks).toString("utf8")));
22564
22589
  req.on("error", reject);
22565
22590
  });
22566
22591
  }
@@ -22598,7 +22623,7 @@ function handleLocalSync(req, res, opts, authorised) {
22598
22623
 
22599
22624
  // src/lib/serve/static.ts
22600
22625
  import { createReadStream as createReadStream2, existsSync as existsSync4, statSync } from "node:fs";
22601
- import { extname, join as join11, normalize, resolve as resolve3, sep } from "node:path";
22626
+ import { extname, join as join11, normalize, resolve as resolve4, sep } from "node:path";
22602
22627
  var MIME = {
22603
22628
  ".html": "text/html; charset=utf-8",
22604
22629
  ".js": "text/javascript; charset=utf-8",
@@ -22613,7 +22638,7 @@ var MIME = {
22613
22638
  ".map": "application/json"
22614
22639
  };
22615
22640
  function createStaticHandler(rootDir) {
22616
- const root = resolve3(rootDir);
22641
+ const root = resolve4(rootDir);
22617
22642
  return (req, res) => {
22618
22643
  if (req.method !== "GET" && req.method !== "HEAD") {
22619
22644
  res.writeHead(405, { allow: "GET, HEAD" }).end();
@@ -23246,8 +23271,8 @@ async function startUiServer(opts) {
23246
23271
  browsers.close();
23247
23272
  bridge.close();
23248
23273
  tracker.destroyAll();
23249
- await new Promise((resolve4) => {
23250
- server.close(() => resolve4());
23274
+ await new Promise((resolve5) => {
23275
+ server.close(() => resolve5());
23251
23276
  server.closeAllConnections();
23252
23277
  });
23253
23278
  }
@@ -23257,15 +23282,15 @@ async function listen(server, startPort, strict, avoidPort) {
23257
23282
  for (let probe = 0; probe < MAX_PORT_PROBES; probe += 1) {
23258
23283
  const port = startPort + probe;
23259
23284
  if (!strict && port === avoidPort) continue;
23260
- const ok = await new Promise((resolve4, rejectListen) => {
23285
+ const ok = await new Promise((resolve5, rejectListen) => {
23261
23286
  const onError = (err) => {
23262
23287
  server.off("listening", onListening);
23263
- if (err.code === "EADDRINUSE") resolve4(false);
23288
+ if (err.code === "EADDRINUSE") resolve5(false);
23264
23289
  else rejectListen(err);
23265
23290
  };
23266
23291
  const onListening = () => {
23267
23292
  server.off("error", onError);
23268
- resolve4(true);
23293
+ resolve5(true);
23269
23294
  };
23270
23295
  server.once("error", onError);
23271
23296
  server.once("listening", onListening);
@@ -24182,7 +24207,7 @@ async function ask(question, def) {
24182
24207
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
24183
24208
  try {
24184
24209
  const answer = await new Promise(
24185
- (resolve4) => rl.question(`${question} [${def}] `, resolve4)
24210
+ (resolve5) => rl.question(`${question} [${def}] `, resolve5)
24186
24211
  );
24187
24212
  return answer.trim() === "" ? def : answer.trim();
24188
24213
  } finally {
@@ -24201,7 +24226,7 @@ async function confirm(question, opts = {}) {
24201
24226
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
24202
24227
  try {
24203
24228
  const answer = await new Promise(
24204
- (resolve4) => rl.question(`${question} [y/N] `, resolve4)
24229
+ (resolve5) => rl.question(`${question} [y/N] `, resolve5)
24205
24230
  );
24206
24231
  return /^y(es)?$/i.test(answer.trim());
24207
24232
  } finally {
@@ -24320,7 +24345,7 @@ async function runUpgrade(cmd, logger) {
24320
24345
  function upgradeCli(version2, logger) {
24321
24346
  const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
24322
24347
  logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
24323
- return new Promise((resolve4, reject) => {
24348
+ return new Promise((resolve5, reject) => {
24324
24349
  const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
24325
24350
  child.on(
24326
24351
  "error",
@@ -24335,7 +24360,7 @@ function upgradeCli(version2, logger) {
24335
24360
  child.on("exit", (code) => {
24336
24361
  if (code === 0) {
24337
24362
  logger.info("done \u2014 `puddle --version` shows the installed version");
24338
- resolve4(0);
24363
+ resolve5(0);
24339
24364
  } else {
24340
24365
  reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
24341
24366
  }
@@ -24458,10 +24483,10 @@ async function runRemove(cmd, logger) {
24458
24483
  }
24459
24484
  }
24460
24485
  async function removeCli(yes, logger) {
24461
- const managed = await new Promise((resolve4) => {
24486
+ const managed = await new Promise((resolve5) => {
24462
24487
  const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
24463
- child.on("error", () => resolve4(false));
24464
- child.on("exit", (code) => resolve4(code === 0));
24488
+ child.on("error", () => resolve5(false));
24489
+ child.on("exit", (code) => resolve5(code === 0));
24465
24490
  });
24466
24491
  if (!managed) {
24467
24492
  throw new CliError(
@@ -24478,13 +24503,13 @@ async function removeCli(yes, logger) {
24478
24503
  logger.info("nothing removed");
24479
24504
  return 0;
24480
24505
  }
24481
- return new Promise((resolve4, reject) => {
24506
+ return new Promise((resolve5, reject) => {
24482
24507
  const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
24483
24508
  child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
24484
24509
  child.on("exit", (code) => {
24485
24510
  if (code === 0) {
24486
24511
  logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
24487
- resolve4(0);
24512
+ resolve5(0);
24488
24513
  } else {
24489
24514
  reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
24490
24515
  }
@@ -24673,7 +24698,7 @@ async function openTarget(host) {
24673
24698
  };
24674
24699
  }
24675
24700
  function runUntilInterrupted(stop, farewell) {
24676
- return new Promise((resolve4) => {
24701
+ return new Promise((resolve5) => {
24677
24702
  let stopping = false;
24678
24703
  process.on("SIGINT", () => {
24679
24704
  if (stopping) process.exit(130);
@@ -24681,10 +24706,10 @@ function runUntilInterrupted(stop, farewell) {
24681
24706
  process.stderr.write(`
24682
24707
  ${farewell}
24683
24708
  `);
24684
- void stop().then(resolve4);
24709
+ void stop().then(resolve5);
24685
24710
  });
24686
24711
  process.on("SIGTERM", () => {
24687
- void stop().then(resolve4);
24712
+ void stop().then(resolve5);
24688
24713
  });
24689
24714
  });
24690
24715
  }
package/dist/install.sh CHANGED
@@ -76,6 +76,19 @@ if [ "$OS" = linux ] && [ -f /etc/alpine-release ]; then
76
76
  fi
77
77
 
78
78
  HOME_DIR="${PUDDLE_HOME:-$HOME/.puddle}"
79
+ while [ "$HOME_DIR" != / ] && [ "${HOME_DIR%/}" != "$HOME_DIR" ]; do HOME_DIR=${HOME_DIR%/}; done
80
+ # New homes must be private even with umask 022. Existing releases used 0755;
81
+ # tighten only an owned directory that other users could not have modified.
82
+ # Keep this migration aligned with shared/node's initialisePrivateHome.
83
+ [ ! -L "$HOME_DIR" ] || die "Puddle home must not be a symlink: $HOME_DIR"
84
+ (umask 077; mkdir -p "$HOME_DIR")
85
+ case "$OS" in
86
+ darwin) DIR_OWNER=$(stat -f '%u' "$HOME_DIR"); DIR_MODE=$(stat -f '%Lp' "$HOME_DIR") ;;
87
+ linux) DIR_OWNER=$(stat -c '%u' "$HOME_DIR"); DIR_MODE=$(stat -c '%a' "$HOME_DIR") ;;
88
+ esac
89
+ [ "$DIR_OWNER" = "$(id -u)" ] || die "Puddle home must be owned by the current user: $HOME_DIR"
90
+ [ "$((0$DIR_MODE & 0022))" -eq 0 ] || die "Puddle home must not be writable by other users: $HOME_DIR"
91
+ chmod 700 "$HOME_DIR"
79
92
  BIN_DIR="$HOME_DIR/bin"
80
93
  CACHE_DIR="$HOME_DIR/cache"
81
94
  mkdir -p "$BIN_DIR/versions" "$CACHE_DIR" "$HOME_DIR/logs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puddle-code/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "puddle — self-hosted orchestrator for CLI coding agents: serves the cockpit UI and connects it to local or SSH hosts.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/PerceptronV/puddle-code#readme",