@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 +5 -27
- package/dist/host-control.mjs +28 -4
- package/dist/index.js +102 -77
- package/dist/install.sh +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,33 +7,11 @@ Past releases: see docs/changelogs/.
|
|
|
7
7
|
|
|
8
8
|
# Changelog
|
|
9
9
|
|
|
10
|
-
## [0.2.
|
|
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
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
package/dist/host-control.mjs
CHANGED
|
@@ -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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
20007
|
+
resolve5({ code: -1, stdout, stderr: stderr + String(err) });
|
|
20008
20008
|
});
|
|
20009
20009
|
child.on("close", (code) => {
|
|
20010
20010
|
if (timer) clearTimeout(timer);
|
|
20011
|
-
|
|
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((
|
|
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)
|
|
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((
|
|
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", () =>
|
|
20139
|
-
child.on("close", (code) =>
|
|
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((
|
|
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", () =>
|
|
20159
|
-
child.on("close", () =>
|
|
20158
|
+
child.on("error", () => resolve5());
|
|
20159
|
+
child.on("close", () => resolve5());
|
|
20160
20160
|
});
|
|
20161
20161
|
}
|
|
20162
20162
|
exec(command, opts = {}) {
|
|
20163
|
-
return new Promise((
|
|
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
|
-
|
|
20183
|
+
resolve5({ code: -1, stdout, stderr: stderr + String(err) });
|
|
20184
20184
|
});
|
|
20185
20185
|
child.on("close", (code) => {
|
|
20186
20186
|
if (timer) clearTimeout(timer);
|
|
20187
|
-
|
|
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((
|
|
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) =>
|
|
20211
|
-
child.on("close", (code) =>
|
|
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((
|
|
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)
|
|
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((
|
|
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
|
-
|
|
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((
|
|
20818
|
+
const stale = await new Promise((resolve5, reject) => {
|
|
20795
20819
|
const probe = connect(path);
|
|
20796
20820
|
probe.once("connect", () => {
|
|
20797
20821
|
probe.destroy();
|
|
20798
|
-
|
|
20822
|
+
resolve5(false);
|
|
20799
20823
|
});
|
|
20800
20824
|
probe.once("error", (err) => {
|
|
20801
|
-
if (err.code === "ECONNREFUSED" || err.code === "ENOENT")
|
|
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((
|
|
20838
|
+
await new Promise((resolve5, reject) => {
|
|
20815
20839
|
server.once("error", reject);
|
|
20816
20840
|
server.listen(path, () => {
|
|
20817
20841
|
server.off("error", reject);
|
|
20818
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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((
|
|
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", () =>
|
|
21559
|
-
child.on("exit", (code) =>
|
|
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((
|
|
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 ?
|
|
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 =
|
|
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
|
|
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",
|
|
21808
|
+
args.push("--tarball", resolve3(opts.tarball));
|
|
21784
21809
|
const localSums = `${opts.tarball}.sha256`;
|
|
21785
|
-
if (existsSync3(localSums)) args.push("--sums",
|
|
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((
|
|
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(() =>
|
|
21881
|
+
srv.close(() => resolve5(port));
|
|
21857
21882
|
});
|
|
21858
21883
|
});
|
|
21859
21884
|
}
|
|
21860
21885
|
function tcpListening(port, timeoutMs = 500) {
|
|
21861
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
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
|
|
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 =
|
|
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((
|
|
23250
|
-
server.close(() =>
|
|
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((
|
|
23285
|
+
const ok = await new Promise((resolve5, rejectListen) => {
|
|
23261
23286
|
const onError = (err) => {
|
|
23262
23287
|
server.off("listening", onListening);
|
|
23263
|
-
if (err.code === "EADDRINUSE")
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
24464
|
-
child.on("exit", (code) =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
24709
|
+
void stop().then(resolve5);
|
|
24685
24710
|
});
|
|
24686
24711
|
process.on("SIGTERM", () => {
|
|
24687
|
-
void stop().then(
|
|
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.
|
|
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",
|