@puddle-code/cli 0.0.32 → 0.0.34
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 +4 -16
- package/README.md +18 -10
- package/dist/index.js +1047 -456
- package/dist/public/assets/Dashboard-D0B3Kact.js +1 -0
- package/dist/public/assets/{EditorTabClose-fAutR9L2.js → EditorTabClose-DH-LS9Zv.js} +1 -1
- package/dist/public/assets/LazyTerminal-SW0c7j8t.js +2 -0
- package/dist/public/assets/{ModelRefcount-MJz59mka.js → ModelRefcount-BO6Mp87O.js} +1 -1
- package/dist/public/assets/{PaneEditorBody-DGqg_4uR.js → PaneEditorBody-_zHVXavR.js} +2 -2
- package/dist/public/assets/SettingsDialog-DvUCRTKI.js +1 -0
- package/dist/public/assets/Terminal-MrMvzcuc.js +117 -0
- package/dist/public/assets/Workspace-CNchP0lE.js +7 -0
- package/dist/public/assets/{buffer-store-bK5Xpj6d.js → buffer-store-5mtYW1Xq.js} +2 -2
- package/dist/public/assets/context-menu-7ErbVngu.js +1 -0
- package/dist/public/assets/{cssMode-DfLbNQ92.js → cssMode-DgxyyatT.js} +1 -1
- package/dist/public/assets/editor-context-CJ-O2xj0.js +1 -0
- package/dist/public/assets/{editor-sync-Cc36ZLyi.js → editor-sync-CCVhpCCj.js} +1 -1
- package/dist/public/assets/hotkeys-BJ9IivRE.js +73 -0
- package/dist/public/assets/{htmlMode-Decw-wtn.js → htmlMode-CsnCJ5rN.js} +1 -1
- package/dist/public/assets/index-D9bGlq8g.js +10 -0
- package/dist/public/assets/{jsonMode-vNCk8wN_.js → jsonMode-Bm5_kMyH.js} +1 -1
- package/dist/public/assets/session-seed-CXqY3vWw.js +1 -0
- package/dist/public/assets/ssh-mode-nM399PS5.js +1 -0
- package/dist/public/assets/{tsMode-XcRZTbul.js → tsMode-D76dymM5.js} +1 -1
- package/dist/public/index.html +3 -4
- package/package.json +1 -1
- package/dist/public/assets/Dashboard-CaIw594e.js +0 -1
- package/dist/public/assets/LazyTerminal-Cv4MivyB.js +0 -2
- package/dist/public/assets/SettingsDialog-Ck6AFRjw.js +0 -1
- package/dist/public/assets/Terminal-BtsgoMqj.js +0 -117
- package/dist/public/assets/Workspace-BaIcmmwi.js +0 -7
- package/dist/public/assets/api-BJeOJuGe.js +0 -73
- package/dist/public/assets/context-menu-CLWmCX87.js +0 -1
- package/dist/public/assets/editor-context-Cz5G4UVr.js +0 -1
- package/dist/public/assets/hotkeys-BrRVXikm.js +0 -1
- package/dist/public/assets/index-IEQHp8h9.js +0 -10
- package/dist/public/assets/session-seed-mKf0bRzJ.js +0 -1
- package/dist/public/assets/ssh-mode-DFHbi7WV.js +0 -1
package/dist/index.js
CHANGED
|
@@ -3905,6 +3905,29 @@ var init_attach = __esm({
|
|
|
3905
3905
|
|
|
3906
3906
|
// src/cli/args.ts
|
|
3907
3907
|
init_types();
|
|
3908
|
+
var COMPONENTS = ["cli", "daemon", "desktop"];
|
|
3909
|
+
function parseComponentSpec(raw) {
|
|
3910
|
+
const at = raw.indexOf("@");
|
|
3911
|
+
const name = at === -1 ? raw : raw.slice(0, at);
|
|
3912
|
+
const versionRaw = at === -1 ? void 0 : raw.slice(at + 1);
|
|
3913
|
+
if (name !== "" && !COMPONENTS.includes(name)) return null;
|
|
3914
|
+
if (name === "" && versionRaw === void 0) return null;
|
|
3915
|
+
let version2;
|
|
3916
|
+
if (versionRaw !== void 0) {
|
|
3917
|
+
version2 = versionRaw.replace(/^v/, "");
|
|
3918
|
+
if (!/^\d+\.\d+\.\d+$/.test(version2)) {
|
|
3919
|
+
throw new CliError(
|
|
3920
|
+
"bad_arguments",
|
|
3921
|
+
`'${versionRaw}' is not a release version`,
|
|
3922
|
+
"versions look like @v0.0.32 (or @0.0.32)"
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
return {
|
|
3927
|
+
...name !== "" ? { what: name } : {},
|
|
3928
|
+
...version2 !== void 0 ? { version: version2 } : {}
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3908
3931
|
var USAGE = `Puddle \u2014 self-hosted orchestrator for CLI coding agents
|
|
3909
3932
|
|
|
3910
3933
|
usage:
|
|
@@ -3917,7 +3940,9 @@ usage:
|
|
|
3917
3940
|
puddle status [user@host]
|
|
3918
3941
|
puddle attach [user@host] <session> [--term <id>]
|
|
3919
3942
|
puddle logs [user@host] [session] [--term <id>] [-f|--follow]
|
|
3920
|
-
puddle
|
|
3943
|
+
puddle install <daemon|desktop>[@version] [user@host] [--tarball <path>]
|
|
3944
|
+
puddle upgrade [cli|daemon|desktop][@version] [user@host] [--tarball <path>]
|
|
3945
|
+
puddle remove <cli|daemon|desktop> [user@host] [--yes] [--purge]
|
|
3921
3946
|
puddle --version | --help
|
|
3922
3947
|
|
|
3923
3948
|
launch serves the cockpit at http://localhost:7433 against the daemon on this
|
|
@@ -3928,9 +3953,19 @@ to stay attached; Ctrl-C then stops the cockpit). refresh stops a target's
|
|
|
3928
3953
|
cockpit (even a wedged one) and runs the full launch flow again \u2014 tunnel,
|
|
3929
3954
|
daemon restart if needed \u2014 keeping the old UI port so open tabs survive.
|
|
3930
3955
|
list shows running cockpits; kill stops one \u2014 sessions keep running on the
|
|
3931
|
-
host either way.
|
|
3932
|
-
|
|
3933
|
-
|
|
3956
|
+
host either way.
|
|
3957
|
+
|
|
3958
|
+
install puts a component in place: 'daemon' under ~/.puddle on this machine
|
|
3959
|
+
or a user@host, 'desktop' as the macOS app bundle \u2014 or, on Linux, an
|
|
3960
|
+
AppImage placed where you say (default ~/puddle), then opened in the file
|
|
3961
|
+
manager. Already installed and no @version \u2192 nothing changes.
|
|
3962
|
+
upgrade moves components to the newest release (or the named @version),
|
|
3963
|
+
installing any that are missing; with no component it covers everything
|
|
3964
|
+
installed on the target \u2014 the CLI last, via npm. cli and desktop are
|
|
3965
|
+
client-machine artefacts: user@host targets the daemon only. remove
|
|
3966
|
+
uninstalls a component after confirmation; 'daemon' stops it (interrupting
|
|
3967
|
+
its sessions), unregisters the supervisor, and keeps ~/.puddle's data \u2014
|
|
3968
|
+
profiles, session history, worktrees \u2014 unless you also confirm --purge.`;
|
|
3934
3969
|
function parseArgs(argv) {
|
|
3935
3970
|
const [cmd, ...rest] = argv;
|
|
3936
3971
|
if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h")
|
|
@@ -4101,30 +4136,76 @@ function parseArgs(argv) {
|
|
|
4101
4136
|
return { cmd: "logs", follow, ...term !== void 0 ? { term } : {} };
|
|
4102
4137
|
return first.includes("@") ? { cmd: "logs", host: first, follow, ...term !== void 0 ? { term } : {} } : { cmd: "logs", session: first, follow, ...term !== void 0 ? { term } : {} };
|
|
4103
4138
|
}
|
|
4139
|
+
case "install": {
|
|
4140
|
+
const [specRaw, host, extra] = positionals;
|
|
4141
|
+
if (extra !== void 0)
|
|
4142
|
+
throw new CliError("bad_arguments", "install takes at most a component + host");
|
|
4143
|
+
const tarball = strFlag("--tarball");
|
|
4144
|
+
expect("--tarball");
|
|
4145
|
+
const spec = specRaw !== void 0 ? parseComponentSpec(specRaw) : null;
|
|
4146
|
+
if (spec?.what === void 0 || spec.what === "cli") {
|
|
4147
|
+
throw new CliError(
|
|
4148
|
+
"bad_arguments",
|
|
4149
|
+
"install takes one of: daemon | desktop (with an optional @version)",
|
|
4150
|
+
spec?.what === "cli" ? "the CLI installs itself via npm: npm install -g @puddle-code/cli" : "e.g. puddle install daemon@v0.0.32 user@host"
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
return {
|
|
4154
|
+
cmd: "install",
|
|
4155
|
+
what: spec.what,
|
|
4156
|
+
...spec.version !== void 0 ? { version: spec.version } : {},
|
|
4157
|
+
...host !== void 0 ? { host } : {},
|
|
4158
|
+
...tarball !== void 0 ? { tarball } : {}
|
|
4159
|
+
};
|
|
4160
|
+
}
|
|
4104
4161
|
case "upgrade": {
|
|
4162
|
+
const tarball = strFlag("--tarball");
|
|
4163
|
+
expect("--tarball");
|
|
4164
|
+
if (positionals.length > 2)
|
|
4165
|
+
throw new CliError("bad_arguments", "upgrade takes at most a component + host");
|
|
4166
|
+
const [first, second] = positionals;
|
|
4167
|
+
const spec = first !== void 0 ? parseComponentSpec(first) : null;
|
|
4168
|
+
const host = spec === null ? first : second;
|
|
4169
|
+
if (spec === null && second !== void 0)
|
|
4170
|
+
throw new CliError(
|
|
4171
|
+
"bad_arguments",
|
|
4172
|
+
`'${first}' is not a component`,
|
|
4173
|
+
"upgrade takes [cli|daemon|desktop][@version], then an optional user@host"
|
|
4174
|
+
);
|
|
4175
|
+
return {
|
|
4176
|
+
cmd: "upgrade",
|
|
4177
|
+
...spec?.what !== void 0 ? { what: spec.what } : {},
|
|
4178
|
+
...spec?.version !== void 0 ? { version: spec.version } : {},
|
|
4179
|
+
...host !== void 0 ? { host } : {},
|
|
4180
|
+
...tarball !== void 0 ? { tarball } : {}
|
|
4181
|
+
};
|
|
4182
|
+
}
|
|
4183
|
+
case "remove": {
|
|
4105
4184
|
const [what, host, extra] = positionals;
|
|
4106
|
-
if (
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
if (what === "cli") {
|
|
4185
|
+
if (extra !== void 0)
|
|
4186
|
+
throw new CliError("bad_arguments", "remove takes at most a component + host");
|
|
4187
|
+
expect("--yes", "--purge");
|
|
4188
|
+
if (what === void 0 || !["cli", "daemon", "desktop"].includes(what)) {
|
|
4111
4189
|
throw new CliError(
|
|
4112
4190
|
"bad_arguments",
|
|
4113
|
-
"
|
|
4114
|
-
"
|
|
4191
|
+
"remove needs a component: cli | daemon | desktop",
|
|
4192
|
+
what !== void 0 && /^(cli|daemon|desktop)@/.test(what) ? "remove takes no @version \u2014 a removal has no version to pick" : "e.g. puddle remove daemon user@host"
|
|
4115
4193
|
);
|
|
4116
4194
|
}
|
|
4117
|
-
|
|
4195
|
+
const purge = flags.has("--purge");
|
|
4196
|
+
if (purge && what !== "daemon") {
|
|
4118
4197
|
throw new CliError(
|
|
4119
4198
|
"bad_arguments",
|
|
4120
|
-
"
|
|
4121
|
-
what.includes("@") ? `to upgrade the daemon on ${what}: puddle upgrade daemon ${what}` : "see: puddle --help"
|
|
4199
|
+
"--purge only applies to the daemon (it deletes ~/.puddle)"
|
|
4122
4200
|
);
|
|
4123
4201
|
}
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4202
|
+
return {
|
|
4203
|
+
cmd: "remove",
|
|
4204
|
+
what,
|
|
4205
|
+
...host !== void 0 ? { host } : {},
|
|
4206
|
+
yes: flags.has("--yes"),
|
|
4207
|
+
purge
|
|
4208
|
+
};
|
|
4128
4209
|
}
|
|
4129
4210
|
default:
|
|
4130
4211
|
throw new CliError("bad_arguments", `unknown command '${cmd}'`, "see: puddle --help");
|
|
@@ -4145,331 +4226,30 @@ function argvFor(command) {
|
|
|
4145
4226
|
|
|
4146
4227
|
// src/cli/run.ts
|
|
4147
4228
|
init_attach();
|
|
4148
|
-
import {
|
|
4149
|
-
import {
|
|
4150
|
-
import { dirname as dirname7, join as join9 } from "node:path";
|
|
4229
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4230
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
4151
4231
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4152
4232
|
|
|
4153
|
-
// src/lib/
|
|
4154
|
-
import { createHash } from "node:crypto";
|
|
4233
|
+
// src/lib/browser.ts
|
|
4155
4234
|
import { spawn } from "node:child_process";
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
4159
|
-
import { Readable } from "node:stream";
|
|
4160
|
-
import { pipeline } from "node:stream/promises";
|
|
4161
|
-
|
|
4162
|
-
// src/lib/paths.ts
|
|
4163
|
-
import { homedir } from "node:os";
|
|
4164
|
-
import { join } from "node:path";
|
|
4165
|
-
function clientHome(env = process.env) {
|
|
4166
|
-
return env.PUDDLE_HOME ?? join(homedir(), ".puddle");
|
|
4167
|
-
}
|
|
4168
|
-
var HOST_HOME = '"${PUDDLE_HOME:-$HOME/.puddle}"';
|
|
4169
|
-
var hostPaths = {
|
|
4170
|
-
home: HOST_HOME,
|
|
4171
|
-
token: `${HOST_HOME}/token`,
|
|
4172
|
-
config: `${HOST_HOME}/config.json`,
|
|
4173
|
-
runtime: `${HOST_HOME}/runtime.json`,
|
|
4174
|
-
current: `${HOST_HOME}/bin/current`,
|
|
4175
|
-
cache: `${HOST_HOME}/cache`,
|
|
4176
|
-
logs: `${HOST_HOME}/logs`
|
|
4177
|
-
};
|
|
4178
|
-
|
|
4179
|
-
// src/lib/desktop-update.ts
|
|
4180
|
-
init_types();
|
|
4181
|
-
|
|
4182
|
-
// src/lib/version.ts
|
|
4183
|
-
import { readFileSync } from "node:fs";
|
|
4184
|
-
import { dirname, join as join2 } from "node:path";
|
|
4185
|
-
import { fileURLToPath } from "node:url";
|
|
4186
|
-
function cliVersion() {
|
|
4187
|
-
if (true) return "0.0.32";
|
|
4188
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
4189
|
-
for (const candidate of [
|
|
4190
|
-
join2(here, "..", "..", "package.json"),
|
|
4191
|
-
join2(here, "..", "package.json")
|
|
4192
|
-
]) {
|
|
4193
|
-
try {
|
|
4194
|
-
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
4195
|
-
if (pkg.version !== void 0) return pkg.version;
|
|
4196
|
-
} catch {
|
|
4197
|
-
}
|
|
4198
|
-
}
|
|
4199
|
-
return "0.0.0";
|
|
4200
|
-
}
|
|
4201
|
-
function pinnedDaemonVersion() {
|
|
4202
|
-
return cliVersion();
|
|
4203
|
-
}
|
|
4204
|
-
function repoSlug(env = process.env) {
|
|
4205
|
-
if (env.PUDDLE_REPO !== void 0 && env.PUDDLE_REPO !== "") return env.PUDDLE_REPO;
|
|
4206
|
-
if (true) return "PerceptronV/puddle-code";
|
|
4207
|
-
return void 0;
|
|
4208
|
-
}
|
|
4209
|
-
|
|
4210
|
-
// src/lib/desktop-update.ts
|
|
4211
|
-
function isNewerVersion(candidate, current) {
|
|
4212
|
-
const parse3 = (v) => {
|
|
4213
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
4214
|
-
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
4215
|
-
};
|
|
4216
|
-
const a = parse3(candidate);
|
|
4217
|
-
const b = parse3(current);
|
|
4218
|
-
if (a === null || b === null) return false;
|
|
4219
|
-
for (let i = 0; i < 3; i++) {
|
|
4220
|
-
if (a[i] !== b[i]) return a[i] > b[i];
|
|
4221
|
-
}
|
|
4222
|
-
return false;
|
|
4223
|
-
}
|
|
4224
|
-
function pickDesktopAsset(assets, platform, arch) {
|
|
4225
|
-
const suffix = platform === "darwin" ? "-mac.zip" : platform === "linux" ? ".AppImage" : null;
|
|
4226
|
-
if (suffix === null) return null;
|
|
4227
|
-
return assets.find(
|
|
4228
|
-
(a) => a.name.endsWith(suffix) && a.name.includes("-arm64") === (arch === "arm64")
|
|
4229
|
-
) ?? null;
|
|
4230
|
-
}
|
|
4231
|
-
function parseSums(text) {
|
|
4232
|
-
const sums = /* @__PURE__ */ new Map();
|
|
4233
|
-
for (const line of text.split("\n")) {
|
|
4234
|
-
const match = /^([0-9a-f]{64})\s+\*?(.+?)\s*$/.exec(line.trim());
|
|
4235
|
-
if (match) sums.set(match[2], match[1]);
|
|
4236
|
-
}
|
|
4237
|
-
return sums;
|
|
4238
|
-
}
|
|
4239
|
-
async function checkForDesktopUpdate(currentVersion, opts = {}) {
|
|
4240
|
-
const slug2 = repoSlug();
|
|
4241
|
-
if (slug2 === void 0) return null;
|
|
4242
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
4243
|
-
const response = await fetchFn(`https://api.github.com/repos/${slug2}/releases/latest`, {
|
|
4244
|
-
headers: { accept: "application/vnd.github+json", "user-agent": "puddle-desktop" }
|
|
4245
|
-
});
|
|
4246
|
-
if (!response.ok) {
|
|
4247
|
-
throw new CliError("not_installed", `release lookup failed (HTTP ${response.status})`);
|
|
4248
|
-
}
|
|
4249
|
-
const release = await response.json();
|
|
4250
|
-
const version2 = (release.tag_name ?? "").replace(/^v/, "");
|
|
4251
|
-
if (!isNewerVersion(version2, currentVersion)) return null;
|
|
4252
|
-
const assets = release.assets ?? [];
|
|
4253
|
-
const asset = pickDesktopAsset(
|
|
4254
|
-
assets,
|
|
4255
|
-
opts.platform ?? process.platform,
|
|
4256
|
-
opts.arch ?? process.arch
|
|
4257
|
-
);
|
|
4258
|
-
const sums = assets.find((a) => a.name === "SHA256SUMS");
|
|
4259
|
-
if (asset === null || sums === void 0) return null;
|
|
4260
|
-
return {
|
|
4261
|
-
version: version2,
|
|
4262
|
-
asset: { name: asset.name, url: asset.browser_download_url },
|
|
4263
|
-
sumsUrl: sums.browser_download_url
|
|
4264
|
-
};
|
|
4265
|
-
}
|
|
4266
|
-
async function stageDesktopUpdate(update, opts = {}) {
|
|
4267
|
-
const logger = opts.logger ?? silentLogger;
|
|
4268
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
4269
|
-
const dir = join3(opts.cacheDir ?? join3(clientHome(), "cache", "desktop"), update.version);
|
|
4270
|
-
await rm(dir, { recursive: true, force: true });
|
|
4271
|
-
await mkdir(dir, { recursive: true });
|
|
4235
|
+
function openBrowser(url2, platform = process.platform) {
|
|
4236
|
+
const command = platform === "darwin" ? { bin: "open", args: [url2] } : platform === "win32" ? { bin: "cmd", args: ["/c", "start", "", url2] } : { bin: "xdg-open", args: [url2] };
|
|
4272
4237
|
try {
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
const response = await fetchFn(update.asset.url, {
|
|
4276
|
-
headers: { "user-agent": "puddle-desktop" }
|
|
4277
|
-
});
|
|
4278
|
-
if (!response.ok || response.body === null) {
|
|
4279
|
-
throw new CliError("not_installed", `download failed (HTTP ${response.status})`);
|
|
4280
|
-
}
|
|
4281
|
-
await pipeline(
|
|
4282
|
-
Readable.fromWeb(response.body),
|
|
4283
|
-
createWriteStream(archive)
|
|
4284
|
-
);
|
|
4285
|
-
const sumsResponse = await fetchFn(update.sumsUrl, {
|
|
4286
|
-
headers: { "user-agent": "puddle-desktop" }
|
|
4238
|
+
const child = spawn(command.bin, command.args, { stdio: "ignore", detached: true });
|
|
4239
|
+
child.on("error", () => {
|
|
4287
4240
|
});
|
|
4288
|
-
if (!sumsResponse.ok) {
|
|
4289
|
-
throw new CliError("not_installed", `SHA256SUMS fetch failed (HTTP ${sumsResponse.status})`);
|
|
4290
|
-
}
|
|
4291
|
-
const expected = parseSums(await sumsResponse.text()).get(update.asset.name);
|
|
4292
|
-
if (expected === void 0) {
|
|
4293
|
-
throw new CliError("not_installed", `${update.asset.name} is not in SHA256SUMS`);
|
|
4294
|
-
}
|
|
4295
|
-
const actual = await fileSha256(archive);
|
|
4296
|
-
if (actual !== expected) {
|
|
4297
|
-
throw new CliError(
|
|
4298
|
-
"not_installed",
|
|
4299
|
-
`checksum mismatch for ${update.asset.name}`,
|
|
4300
|
-
`expected ${expected}, got ${actual}`
|
|
4301
|
-
);
|
|
4302
|
-
}
|
|
4303
|
-
if (update.asset.name.endsWith(".zip")) {
|
|
4304
|
-
const extractDir = join3(dir, "extract");
|
|
4305
|
-
await run("/usr/bin/ditto", ["-x", "-k", archive, extractDir]);
|
|
4306
|
-
const bundle = (await readdir(extractDir)).find((name) => name.endsWith(".app"));
|
|
4307
|
-
if (bundle === void 0) {
|
|
4308
|
-
throw new CliError("not_installed", "the update zip contains no .app bundle");
|
|
4309
|
-
}
|
|
4310
|
-
logger.info(`staged ${update.version} at ${join3(extractDir, bundle)}`);
|
|
4311
|
-
return {
|
|
4312
|
-
version: update.version,
|
|
4313
|
-
kind: "mac-app",
|
|
4314
|
-
stagedPath: join3(extractDir, bundle),
|
|
4315
|
-
dir
|
|
4316
|
-
};
|
|
4317
|
-
}
|
|
4318
|
-
await chmod(archive, 493);
|
|
4319
|
-
logger.info(`staged ${update.version} at ${archive}`);
|
|
4320
|
-
return { version: update.version, kind: "appimage", stagedPath: archive, dir };
|
|
4321
|
-
} catch (e) {
|
|
4322
|
-
await rm(dir, { recursive: true, force: true });
|
|
4323
|
-
throw e;
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
async function applyDesktopUpdate(staged, opts) {
|
|
4327
|
-
const logger = opts.logger ?? silentLogger;
|
|
4328
|
-
const script = swapScript(staged, opts);
|
|
4329
|
-
const scriptPath = join3(staged.dir, "apply.sh");
|
|
4330
|
-
await writeFile(scriptPath, script, { mode: 493 });
|
|
4331
|
-
logger.info(`applying ${staged.version} via ${scriptPath}`);
|
|
4332
|
-
if (opts.detach ?? true) {
|
|
4333
|
-
const child = spawn("/bin/sh", [scriptPath], { detached: true, stdio: "ignore" });
|
|
4334
4241
|
child.unref();
|
|
4335
|
-
return;
|
|
4336
|
-
}
|
|
4337
|
-
|
|
4338
|
-
}
|
|
4339
|
-
var q = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
4340
|
-
function swapScript(staged, opts) {
|
|
4341
|
-
const wait = opts.waitPid === void 0 ? [] : [
|
|
4342
|
-
`i=0`,
|
|
4343
|
-
`while kill -0 ${opts.waitPid} 2>/dev/null; do`,
|
|
4344
|
-
` i=$((i+1)); [ "$i" -gt 600 ] && exit 1`,
|
|
4345
|
-
` sleep 0.1`,
|
|
4346
|
-
`done`
|
|
4347
|
-
];
|
|
4348
|
-
if (staged.kind === "mac-app") {
|
|
4349
|
-
return [
|
|
4350
|
-
`#!/bin/sh`,
|
|
4351
|
-
`# Puddle desktop update ${staged.version} \u2014 swap once the app exits`,
|
|
4352
|
-
...wait,
|
|
4353
|
-
`target=${q(opts.targetPath)}`,
|
|
4354
|
-
`target_dir=${q(dirname2(opts.targetPath))}`,
|
|
4355
|
-
`staged=${q(staged.stagedPath)}`,
|
|
4356
|
-
`mkdir -p "$target_dir" || exit 1`,
|
|
4357
|
-
`rm -rf "$target.old"`,
|
|
4358
|
-
`had_old=0`,
|
|
4359
|
-
`if [ -e "$target" ]; then`,
|
|
4360
|
-
` mv "$target" "$target.old" || exit 1`,
|
|
4361
|
-
` had_old=1`,
|
|
4362
|
-
`fi`,
|
|
4363
|
-
`if mv "$staged" "$target" 2>/dev/null || /usr/bin/ditto "$staged" "$target"; then`,
|
|
4364
|
-
// Defence in depth: the normal path never quarantines (node's fetch is
|
|
4365
|
-
// not a quarantine-opted-in app, and ditto only propagates what the
|
|
4366
|
-
// zip already carries), but if the bundle ever acquires the attribute
|
|
4367
|
-
// the swap must not resurrect the Gatekeeper prompt.
|
|
4368
|
-
` /usr/bin/xattr -dr com.apple.quarantine "$target" 2>/dev/null`,
|
|
4369
|
-
` rm -rf "$target.old" ${q(staged.dir)}`,
|
|
4370
|
-
...opts.relaunch ? [` open "$target"`] : [],
|
|
4371
|
-
`else`,
|
|
4372
|
-
` rm -rf "$target"`,
|
|
4373
|
-
` [ "$had_old" -eq 0 ] || mv "$target.old" "$target"`,
|
|
4374
|
-
` exit 1`,
|
|
4375
|
-
`fi`,
|
|
4376
|
-
``
|
|
4377
|
-
].join("\n");
|
|
4378
|
-
}
|
|
4379
|
-
return [
|
|
4380
|
-
`#!/bin/sh`,
|
|
4381
|
-
`# Puddle desktop update ${staged.version} \u2014 swap once the app exits`,
|
|
4382
|
-
...wait,
|
|
4383
|
-
`target=${q(opts.targetPath)}`,
|
|
4384
|
-
`cp -f ${q(staged.stagedPath)} "$target" || exit 1`,
|
|
4385
|
-
`chmod +x "$target"`,
|
|
4386
|
-
`rm -rf ${q(staged.dir)}`,
|
|
4387
|
-
...opts.relaunch ? [`"$target" >/dev/null 2>&1 &`] : [],
|
|
4388
|
-
``
|
|
4389
|
-
].join("\n");
|
|
4390
|
-
}
|
|
4391
|
-
async function findInstalledDesktopApp(opts = {}) {
|
|
4392
|
-
if ((opts.platform ?? process.platform) !== "darwin") return null;
|
|
4393
|
-
const { readFile } = await import("node:fs/promises");
|
|
4394
|
-
const { homedir: homedir2 } = await import("node:os");
|
|
4395
|
-
const systemApplications = opts.systemApplicationsDir ?? "/Applications";
|
|
4396
|
-
const home = opts.homeDir ?? homedir2();
|
|
4397
|
-
for (const appPath of [
|
|
4398
|
-
join3(systemApplications, "Puddle.app"),
|
|
4399
|
-
join3(home, "Applications", "Puddle.app")
|
|
4400
|
-
]) {
|
|
4401
|
-
try {
|
|
4402
|
-
const plist = await readFile(join3(appPath, "Contents/Info.plist"), "utf8");
|
|
4403
|
-
const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
|
|
4404
|
-
plist
|
|
4405
|
-
);
|
|
4406
|
-
if (match) return { appPath, version: match[1] };
|
|
4407
|
-
} catch {
|
|
4408
|
-
}
|
|
4409
|
-
}
|
|
4410
|
-
return null;
|
|
4411
|
-
}
|
|
4412
|
-
async function desktopAppInstallPath(opts = {}) {
|
|
4413
|
-
if ((opts.platform ?? process.platform) !== "darwin") return null;
|
|
4414
|
-
const { homedir: homedir2 } = await import("node:os");
|
|
4415
|
-
const systemApplications = opts.systemApplicationsDir ?? "/Applications";
|
|
4416
|
-
const canWrite = opts.canWrite ?? (async (path) => {
|
|
4417
|
-
try {
|
|
4418
|
-
await access(path, constants.W_OK);
|
|
4419
|
-
return true;
|
|
4420
|
-
} catch {
|
|
4421
|
-
return false;
|
|
4422
|
-
}
|
|
4423
|
-
});
|
|
4424
|
-
if (await canWrite(systemApplications)) return join3(systemApplications, "Puddle.app");
|
|
4425
|
-
const userApplications = join3(opts.homeDir ?? homedir2(), "Applications");
|
|
4426
|
-
await mkdir(userApplications, { recursive: true });
|
|
4427
|
-
return join3(userApplications, "Puddle.app");
|
|
4428
|
-
}
|
|
4429
|
-
function isDesktopAppRunning(appPath) {
|
|
4430
|
-
return new Promise((resolve3) => {
|
|
4431
|
-
const child = spawn("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
|
|
4432
|
-
stdio: "ignore"
|
|
4433
|
-
});
|
|
4434
|
-
child.on("error", () => resolve3(false));
|
|
4435
|
-
child.on("exit", (code) => resolve3(code === 0));
|
|
4436
|
-
});
|
|
4437
|
-
}
|
|
4438
|
-
function fileSha256(path) {
|
|
4439
|
-
const hash2 = createHash("sha256");
|
|
4440
|
-
return pipeline(createReadStream(path), hash2).then(() => hash2.digest("hex"));
|
|
4441
|
-
}
|
|
4442
|
-
function run(command, args) {
|
|
4443
|
-
return new Promise((resolve3, reject) => {
|
|
4444
|
-
const child = spawn(command, args, { stdio: "ignore" });
|
|
4445
|
-
child.on("error", reject);
|
|
4446
|
-
child.on(
|
|
4447
|
-
"exit",
|
|
4448
|
-
(code) => code === 0 ? resolve3() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
|
|
4449
|
-
);
|
|
4450
|
-
});
|
|
4451
|
-
}
|
|
4452
|
-
|
|
4453
|
-
// src/lib/browser.ts
|
|
4454
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
4455
|
-
function openBrowser(url2, platform = process.platform) {
|
|
4456
|
-
const command = platform === "darwin" ? { bin: "open", args: [url2] } : platform === "win32" ? { bin: "cmd", args: ["/c", "start", "", url2] } : { bin: "xdg-open", args: [url2] };
|
|
4457
|
-
try {
|
|
4458
|
-
const child = spawn2(command.bin, command.args, { stdio: "ignore", detached: true });
|
|
4459
|
-
child.on("error", () => {
|
|
4460
|
-
});
|
|
4461
|
-
child.unref();
|
|
4462
|
-
return true;
|
|
4463
|
-
} catch {
|
|
4464
|
-
return false;
|
|
4242
|
+
return true;
|
|
4243
|
+
} catch {
|
|
4244
|
+
return false;
|
|
4465
4245
|
}
|
|
4466
4246
|
}
|
|
4467
4247
|
|
|
4468
4248
|
// src/lib/connect.ts
|
|
4469
|
-
import { join as
|
|
4249
|
+
import { join as join5 } from "node:path";
|
|
4470
4250
|
|
|
4471
4251
|
// ../shared/src/protocol.ts
|
|
4472
|
-
var PROTOCOL_VERSION = { major: 14, minor:
|
|
4252
|
+
var PROTOCOL_VERSION = { major: 14, minor: 2 };
|
|
4473
4253
|
|
|
4474
4254
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
4475
4255
|
var external_exports = {};
|
|
@@ -19124,6 +18904,14 @@ var profileSettingsSchema = external_exports.looseObject({
|
|
|
19124
18904
|
* PTY spawn. Off pauses the whole feature; stored maps are kept untouched.
|
|
19125
18905
|
*/
|
|
19126
18906
|
captureSessionEnv: external_exports.boolean().default(true),
|
|
18907
|
+
/**
|
|
18908
|
+
* App shortcuts win over a focused terminal (SPEC §11, 14.2): chords bound
|
|
18909
|
+
* to registered actions — ⌃⇥ tab cycling, ⌘K — reach the app instead of
|
|
18910
|
+
* being eaten by xterm and sent to the PTY. Off, the terminal receives
|
|
18911
|
+
* every key it handles. Per profile: which keys your muscle memory owns is
|
|
18912
|
+
* a fact about you, not about the browser you happen to be in.
|
|
18913
|
+
*/
|
|
18914
|
+
terminalAppShortcuts: external_exports.boolean().default(true),
|
|
19127
18915
|
/**
|
|
19128
18916
|
* Launch-text templates (SPEC §4). A key that is ABSENT falls back to the
|
|
19129
18917
|
* daemon's built-in default; an EMPTY string is an intentional empty preamble.
|
|
@@ -19901,7 +19689,20 @@ var wsClientMessageSchema = external_exports.discriminatedUnion("t", [
|
|
|
19901
19689
|
external_exports.object({ t: external_exports.literal("spawn-shell"), session: external_exports.string() }),
|
|
19902
19690
|
/** Terminate a shell PTY (never the agent term); viewers learn via `exit`. */
|
|
19903
19691
|
external_exports.object({ t: external_exports.literal("kill-shell"), session: external_exports.string(), term: termId }),
|
|
19904
|
-
external_exports.object({ t: external_exports.literal("subscribe-status") })
|
|
19692
|
+
external_exports.object({ t: external_exports.literal("subscribe-status") }),
|
|
19693
|
+
/**
|
|
19694
|
+
* The client's resolved terminal colours (14.1): the DAEMON answers agents'
|
|
19695
|
+
* OSC 10/11 dynamic-colour queries from the last pair any client reported —
|
|
19696
|
+
* an auto-theming agent (e.g. Claude Code) queries at spawn, usually before
|
|
19697
|
+
* a viewer has attached, so a viewer-side answer misses it and the agent
|
|
19698
|
+
* falls back to dark whatever the app's theme. Sent after auth on every
|
|
19699
|
+
* connect and again on theme switches.
|
|
19700
|
+
*/
|
|
19701
|
+
external_exports.object({
|
|
19702
|
+
t: external_exports.literal("theme"),
|
|
19703
|
+
fg: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
19704
|
+
bg: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/)
|
|
19705
|
+
})
|
|
19905
19706
|
]);
|
|
19906
19707
|
|
|
19907
19708
|
// src/lib/net.ts
|
|
@@ -19953,6 +19754,23 @@ function sleep(ms) {
|
|
|
19953
19754
|
return new Promise((r) => setTimeout(r, ms));
|
|
19954
19755
|
}
|
|
19955
19756
|
|
|
19757
|
+
// src/lib/paths.ts
|
|
19758
|
+
import { homedir } from "node:os";
|
|
19759
|
+
import { join } from "node:path";
|
|
19760
|
+
function clientHome(env = process.env) {
|
|
19761
|
+
return env.PUDDLE_HOME ?? join(homedir(), ".puddle");
|
|
19762
|
+
}
|
|
19763
|
+
var HOST_HOME = '"${PUDDLE_HOME:-$HOME/.puddle}"';
|
|
19764
|
+
var hostPaths = {
|
|
19765
|
+
home: HOST_HOME,
|
|
19766
|
+
token: `${HOST_HOME}/token`,
|
|
19767
|
+
config: `${HOST_HOME}/config.json`,
|
|
19768
|
+
runtime: `${HOST_HOME}/runtime.json`,
|
|
19769
|
+
current: `${HOST_HOME}/bin/current`,
|
|
19770
|
+
cache: `${HOST_HOME}/cache`,
|
|
19771
|
+
logs: `${HOST_HOME}/logs`
|
|
19772
|
+
};
|
|
19773
|
+
|
|
19956
19774
|
// src/lib/daemon-client.ts
|
|
19957
19775
|
init_types();
|
|
19958
19776
|
var LIVE = /* @__PURE__ */ new Set(["starting", "running", "waiting_input"]);
|
|
@@ -19995,6 +19813,13 @@ var DaemonClient = class {
|
|
|
19995
19813
|
sessions() {
|
|
19996
19814
|
return this.get("/api/sessions").then((body) => sessionSchema.array().parse(body));
|
|
19997
19815
|
}
|
|
19816
|
+
profiles() {
|
|
19817
|
+
return this.get("/api/profiles").then((body) => profileSchema.array().parse(body));
|
|
19818
|
+
}
|
|
19819
|
+
/** The live subset of sessions() — what an upgrade/removal interrupts. */
|
|
19820
|
+
async liveSessions() {
|
|
19821
|
+
return (await this.sessions()).filter((s) => LIVE.has(s.status));
|
|
19822
|
+
}
|
|
19998
19823
|
async liveSessionCount() {
|
|
19999
19824
|
return (await this.sessions()).filter((s) => LIVE.has(s.status)).length;
|
|
20000
19825
|
}
|
|
@@ -20051,9 +19876,39 @@ async function waitForToken(transport, timeoutMs) {
|
|
|
20051
19876
|
|
|
20052
19877
|
// src/lib/bootstrap.ts
|
|
20053
19878
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
20054
|
-
import { dirname as
|
|
19879
|
+
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
20055
19880
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
20056
19881
|
init_types();
|
|
19882
|
+
|
|
19883
|
+
// src/lib/version.ts
|
|
19884
|
+
import { readFileSync } from "node:fs";
|
|
19885
|
+
import { dirname, join as join2 } from "node:path";
|
|
19886
|
+
import { fileURLToPath } from "node:url";
|
|
19887
|
+
function cliVersion() {
|
|
19888
|
+
if (true) return "0.0.34";
|
|
19889
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19890
|
+
for (const candidate of [
|
|
19891
|
+
join2(here, "..", "..", "package.json"),
|
|
19892
|
+
join2(here, "..", "package.json")
|
|
19893
|
+
]) {
|
|
19894
|
+
try {
|
|
19895
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf8"));
|
|
19896
|
+
if (pkg.version !== void 0) return pkg.version;
|
|
19897
|
+
} catch {
|
|
19898
|
+
}
|
|
19899
|
+
}
|
|
19900
|
+
return "0.0.0";
|
|
19901
|
+
}
|
|
19902
|
+
function pinnedDaemonVersion() {
|
|
19903
|
+
return cliVersion();
|
|
19904
|
+
}
|
|
19905
|
+
function repoSlug(env = process.env) {
|
|
19906
|
+
if (env.PUDDLE_REPO !== void 0 && env.PUDDLE_REPO !== "") return env.PUDDLE_REPO;
|
|
19907
|
+
if (true) return "PerceptronV/puddle-code";
|
|
19908
|
+
return void 0;
|
|
19909
|
+
}
|
|
19910
|
+
|
|
19911
|
+
// src/lib/bootstrap.ts
|
|
20057
19912
|
async function installedVersion(transport) {
|
|
20058
19913
|
const result = await transport.exec(`readlink ${hostPaths.current}`, { timeoutMs: 15e3 });
|
|
20059
19914
|
if (result.code !== 0) return null;
|
|
@@ -20121,11 +19976,11 @@ function lastLines(text, count = 3) {
|
|
|
20121
19976
|
return text.trim().split("\n").slice(-count).join("\n");
|
|
20122
19977
|
}
|
|
20123
19978
|
function readInstallScript() {
|
|
20124
|
-
const here =
|
|
19979
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
20125
19980
|
const candidates = [
|
|
20126
|
-
|
|
19981
|
+
join3(here, "install.sh"),
|
|
20127
19982
|
// dist/index.js → dist/install.sh
|
|
20128
|
-
|
|
19983
|
+
join3(here, "..", "..", "..", "..", "scripts", "install.sh")
|
|
20129
19984
|
// src/lib/ → repo root
|
|
20130
19985
|
];
|
|
20131
19986
|
for (const candidate of candidates) {
|
|
@@ -20285,7 +20140,7 @@ function isLocalOrigin(origin) {
|
|
|
20285
20140
|
|
|
20286
20141
|
// src/lib/serve/local-sync.ts
|
|
20287
20142
|
import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
20288
|
-
import { dirname as
|
|
20143
|
+
import { dirname as dirname3 } from "node:path";
|
|
20289
20144
|
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
20290
20145
|
function readStore(file2) {
|
|
20291
20146
|
try {
|
|
@@ -20298,7 +20153,7 @@ function readStore(file2) {
|
|
|
20298
20153
|
return { version: 1, profiles: {} };
|
|
20299
20154
|
}
|
|
20300
20155
|
function writeStore(file2, store) {
|
|
20301
|
-
mkdirSync(
|
|
20156
|
+
mkdirSync(dirname3(file2), { recursive: true });
|
|
20302
20157
|
const tmp = `${file2}.tmp-${process.pid}`;
|
|
20303
20158
|
writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
|
|
20304
20159
|
renameSync(tmp, file2);
|
|
@@ -20373,8 +20228,8 @@ function recoverProxiedPath(referer, requestUrl) {
|
|
|
20373
20228
|
}
|
|
20374
20229
|
|
|
20375
20230
|
// src/lib/serve/static.ts
|
|
20376
|
-
import { createReadStream
|
|
20377
|
-
import { extname, join as
|
|
20231
|
+
import { createReadStream, existsSync as existsSync2, statSync } from "node:fs";
|
|
20232
|
+
import { extname, join as join4, normalize, resolve as resolve2, sep } from "node:path";
|
|
20378
20233
|
var MIME = {
|
|
20379
20234
|
".html": "text/html; charset=utf-8",
|
|
20380
20235
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -20402,14 +20257,14 @@ function createStaticHandler(rootDir) {
|
|
|
20402
20257
|
res.writeHead(404, { "content-type": "text/plain" }).end("not found");
|
|
20403
20258
|
return;
|
|
20404
20259
|
}
|
|
20405
|
-
const requested = normalize(
|
|
20260
|
+
const requested = normalize(join4(root, decoded));
|
|
20406
20261
|
const candidate = requested.startsWith(root + sep) || requested === root ? requested : root;
|
|
20407
20262
|
const isFile = existsSync2(candidate) && statSync(candidate).isFile();
|
|
20408
20263
|
if (!isFile && extname(pathname) !== "") {
|
|
20409
20264
|
res.writeHead(404, { "content-type": "text/plain" }).end("not found");
|
|
20410
20265
|
return;
|
|
20411
20266
|
}
|
|
20412
|
-
const file2 = isFile ? candidate :
|
|
20267
|
+
const file2 = isFile ? candidate : join4(root, "index.html");
|
|
20413
20268
|
if (!existsSync2(file2)) {
|
|
20414
20269
|
res.writeHead(404, { "content-type": "text/plain" }).end("not found");
|
|
20415
20270
|
return;
|
|
@@ -20422,7 +20277,7 @@ function createStaticHandler(rootDir) {
|
|
|
20422
20277
|
res.end();
|
|
20423
20278
|
return;
|
|
20424
20279
|
}
|
|
20425
|
-
|
|
20280
|
+
createReadStream(file2).pipe(res);
|
|
20426
20281
|
};
|
|
20427
20282
|
}
|
|
20428
20283
|
|
|
@@ -20687,7 +20542,7 @@ async function listen(server, startPort, strict, avoidPort) {
|
|
|
20687
20542
|
}
|
|
20688
20543
|
|
|
20689
20544
|
// src/lib/tunnel.ts
|
|
20690
|
-
import { spawn as
|
|
20545
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
20691
20546
|
init_types();
|
|
20692
20547
|
var RECONNECT_INITIAL_MS = 500;
|
|
20693
20548
|
var RECONNECT_MAX_MS = 1e4;
|
|
@@ -20703,7 +20558,7 @@ async function openCallbackForward(ssh, port, opts = {}) {
|
|
|
20703
20558
|
return null;
|
|
20704
20559
|
};
|
|
20705
20560
|
if (await tcpListening(port)) return skip("local port busy");
|
|
20706
|
-
const child =
|
|
20561
|
+
const child = spawn2(
|
|
20707
20562
|
opts.sshBinary ?? "ssh",
|
|
20708
20563
|
ssh.args("-N", "-L", `${port}:127.0.0.1:${port}`, ssh.host),
|
|
20709
20564
|
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
@@ -20735,7 +20590,7 @@ async function openTunnel(ssh, remotePort, opts = {}) {
|
|
|
20735
20590
|
let healthTimer = null;
|
|
20736
20591
|
const spawnForward = async (port) => {
|
|
20737
20592
|
if (stopping) return false;
|
|
20738
|
-
const child =
|
|
20593
|
+
const child = spawn2(
|
|
20739
20594
|
sshBinary,
|
|
20740
20595
|
ssh.args("-N", "-L", `${port}:127.0.0.1:${remotePort}`, ssh.host),
|
|
20741
20596
|
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
@@ -20858,15 +20713,15 @@ async function openTunnel(ssh, remotePort, opts = {}) {
|
|
|
20858
20713
|
}
|
|
20859
20714
|
|
|
20860
20715
|
// src/lib/transport/local.ts
|
|
20861
|
-
import { spawn as
|
|
20716
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
20862
20717
|
import { copyFileSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
20863
|
-
import { dirname as
|
|
20718
|
+
import { dirname as dirname4 } from "node:path";
|
|
20864
20719
|
var LocalTransport = class {
|
|
20865
20720
|
kind = "local";
|
|
20866
20721
|
label = "this machine";
|
|
20867
20722
|
exec(command, opts = {}) {
|
|
20868
20723
|
return new Promise((resolve3) => {
|
|
20869
|
-
const child =
|
|
20724
|
+
const child = spawn3("sh", ["-c", command], { stdio: ["pipe", "pipe", "pipe"] });
|
|
20870
20725
|
let stdout = "";
|
|
20871
20726
|
let stderr = "";
|
|
20872
20727
|
let timer;
|
|
@@ -20901,7 +20756,7 @@ var LocalTransport = class {
|
|
|
20901
20756
|
}
|
|
20902
20757
|
copyTo(localPath, destPath) {
|
|
20903
20758
|
const dest = expandHome(destPath);
|
|
20904
|
-
mkdirSync2(
|
|
20759
|
+
mkdirSync2(dirname4(dest), { recursive: true });
|
|
20905
20760
|
copyFileSync(localPath, dest);
|
|
20906
20761
|
return Promise.resolve();
|
|
20907
20762
|
}
|
|
@@ -20913,7 +20768,7 @@ function expandHome(path) {
|
|
|
20913
20768
|
}
|
|
20914
20769
|
|
|
20915
20770
|
// src/lib/transport/ssh.ts
|
|
20916
|
-
import { spawn as
|
|
20771
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
20917
20772
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
20918
20773
|
init_types();
|
|
20919
20774
|
var SshTransport = class {
|
|
@@ -20960,7 +20815,7 @@ var SshTransport = class {
|
|
|
20960
20815
|
*/
|
|
20961
20816
|
open() {
|
|
20962
20817
|
return new Promise((resolve3, reject) => {
|
|
20963
|
-
const child =
|
|
20818
|
+
const child = spawn4(this.ssh, this.args(this.host, "true"), { stdio: "inherit" });
|
|
20964
20819
|
child.on(
|
|
20965
20820
|
"error",
|
|
20966
20821
|
(err) => reject(new CliError("ssh_unreachable", `could not run ${this.ssh}: ${err.message}`))
|
|
@@ -20983,7 +20838,7 @@ var SshTransport = class {
|
|
|
20983
20838
|
isAlive() {
|
|
20984
20839
|
if (!this.hasControlMaster) return Promise.resolve(true);
|
|
20985
20840
|
return new Promise((resolve3) => {
|
|
20986
|
-
const child =
|
|
20841
|
+
const child = spawn4(this.ssh, this.args("-O", "check", this.host), { stdio: "ignore" });
|
|
20987
20842
|
child.on("error", () => resolve3(false));
|
|
20988
20843
|
child.on("close", (code) => resolve3(code === 0));
|
|
20989
20844
|
});
|
|
@@ -21000,7 +20855,7 @@ var SshTransport = class {
|
|
|
21000
20855
|
if (!this.hasControlMaster) return Promise.resolve();
|
|
21001
20856
|
return new Promise((resolve3) => {
|
|
21002
20857
|
const spec = `${localPort}:127.0.0.1:${remotePort}`;
|
|
21003
|
-
const child =
|
|
20858
|
+
const child = spawn4(this.ssh, this.args("-O", "cancel", "-L", spec, this.host), {
|
|
21004
20859
|
stdio: "ignore"
|
|
21005
20860
|
});
|
|
21006
20861
|
child.on("error", () => resolve3());
|
|
@@ -21009,7 +20864,7 @@ var SshTransport = class {
|
|
|
21009
20864
|
}
|
|
21010
20865
|
exec(command, opts = {}) {
|
|
21011
20866
|
return new Promise((resolve3) => {
|
|
21012
|
-
const child =
|
|
20867
|
+
const child = spawn4(this.ssh, this.args(this.host, "--", `sh -c ${shellQuote(command)}`), {
|
|
21013
20868
|
stdio: ["pipe", "pipe", "pipe"]
|
|
21014
20869
|
});
|
|
21015
20870
|
let stdout = "";
|
|
@@ -21044,7 +20899,7 @@ var SshTransport = class {
|
|
|
21044
20899
|
async copyTo(localPath, destPath) {
|
|
21045
20900
|
await this.exec(`mkdir -p $(dirname ${destPath})`, { timeoutMs: 15e3 });
|
|
21046
20901
|
await new Promise((resolve3, reject) => {
|
|
21047
|
-
const child =
|
|
20902
|
+
const child = spawn4(this.scp, [...this.controlArgs, localPath, `${this.host}:${destPath}`], {
|
|
21048
20903
|
stdio: ["ignore", "ignore", "inherit"]
|
|
21049
20904
|
});
|
|
21050
20905
|
child.on("error", reject);
|
|
@@ -21109,7 +20964,7 @@ async function connectRemote(opts) {
|
|
|
21109
20964
|
...opts.onRefreshRequest !== void 0 ? { control: { token: endpoint.token, onRefresh: opts.onRefreshRequest } } : {},
|
|
21110
20965
|
// The store lives on the CLIENT machine — every cockpit here shares it,
|
|
21111
20966
|
// whichever remote daemon each one drives.
|
|
21112
|
-
localSync: { token: endpoint.token, file:
|
|
20967
|
+
localSync: { token: endpoint.token, file: join5(clientHome(), "local-sync.json") }
|
|
21113
20968
|
});
|
|
21114
20969
|
tunnel.onPortChange((port) => ui.setTarget({ host: "127.0.0.1", port }));
|
|
21115
20970
|
const eventCbs = /* @__PURE__ */ new Set();
|
|
@@ -21161,18 +21016,18 @@ async function showLogs(transport, opts) {
|
|
|
21161
21016
|
|
|
21162
21017
|
// src/lib/registry.ts
|
|
21163
21018
|
import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
21164
|
-
import { join as
|
|
21019
|
+
import { join as join6 } from "node:path";
|
|
21165
21020
|
function cockpitsDir(env) {
|
|
21166
|
-
return
|
|
21021
|
+
return join6(clientHome(env), "cockpits");
|
|
21167
21022
|
}
|
|
21168
21023
|
function slug(target) {
|
|
21169
21024
|
return target.replace(/[^A-Za-z0-9@._-]/g, "_");
|
|
21170
21025
|
}
|
|
21171
21026
|
function cockpitRecordPath(target) {
|
|
21172
|
-
return
|
|
21027
|
+
return join6(cockpitsDir(), `${slug(target)}.json`);
|
|
21173
21028
|
}
|
|
21174
21029
|
function cockpitLogPath(target) {
|
|
21175
|
-
return
|
|
21030
|
+
return join6(clientHome(), "logs", `cockpit-${slug(target)}.log`);
|
|
21176
21031
|
}
|
|
21177
21032
|
function writeCockpitRecord(record2) {
|
|
21178
21033
|
mkdirSync4(cockpitsDir(), { recursive: true, mode: 448 });
|
|
@@ -21201,7 +21056,7 @@ function listCockpitRecords() {
|
|
|
21201
21056
|
for (const name of names) {
|
|
21202
21057
|
if (!name.endsWith(".json")) continue;
|
|
21203
21058
|
try {
|
|
21204
|
-
records.push(JSON.parse(readFileSync5(
|
|
21059
|
+
records.push(JSON.parse(readFileSync5(join6(cockpitsDir(), name), "utf8")));
|
|
21205
21060
|
} catch {
|
|
21206
21061
|
}
|
|
21207
21062
|
}
|
|
@@ -21233,7 +21088,7 @@ async function checkCockpit(record2) {
|
|
|
21233
21088
|
}
|
|
21234
21089
|
|
|
21235
21090
|
// src/lib/start.ts
|
|
21236
|
-
import { join as
|
|
21091
|
+
import { join as join7 } from "node:path";
|
|
21237
21092
|
init_types();
|
|
21238
21093
|
async function startLocal(opts) {
|
|
21239
21094
|
const logger = opts.logger ?? silentLogger;
|
|
@@ -21256,7 +21111,7 @@ async function startLocal(opts) {
|
|
|
21256
21111
|
// never squat the daemon's own port
|
|
21257
21112
|
target: { host: "127.0.0.1", port: endpoint.port },
|
|
21258
21113
|
...opts.onRefreshRequest !== void 0 ? { control: { token: endpoint.token, onRefresh: opts.onRefreshRequest } } : {},
|
|
21259
|
-
localSync: { token: endpoint.token, file:
|
|
21114
|
+
localSync: { token: endpoint.token, file: join7(clientHome(), "local-sync.json") }
|
|
21260
21115
|
});
|
|
21261
21116
|
const eventCbs = /* @__PURE__ */ new Set();
|
|
21262
21117
|
return {
|
|
@@ -21296,7 +21151,405 @@ async function statusReport(client) {
|
|
|
21296
21151
|
return { daemon, host, sessions: sessions.filter((s) => s.status !== "archived") };
|
|
21297
21152
|
}
|
|
21298
21153
|
|
|
21299
|
-
// src/cli/run.ts
|
|
21154
|
+
// src/cli/run.ts
|
|
21155
|
+
init_types();
|
|
21156
|
+
|
|
21157
|
+
// src/cli/manage.ts
|
|
21158
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
21159
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
21160
|
+
import { chmod as chmod2, copyFile, mkdir as mkdir2, rename, rm as rm2 } from "node:fs/promises";
|
|
21161
|
+
import { homedir as homedir2 } from "node:os";
|
|
21162
|
+
import { join as join9 } from "node:path";
|
|
21163
|
+
|
|
21164
|
+
// src/lib/desktop-update.ts
|
|
21165
|
+
import { createHash } from "node:crypto";
|
|
21166
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
21167
|
+
import { constants, createReadStream as createReadStream2, createWriteStream } from "node:fs";
|
|
21168
|
+
import { access, chmod, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
21169
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
21170
|
+
import { Readable } from "node:stream";
|
|
21171
|
+
import { pipeline } from "node:stream/promises";
|
|
21172
|
+
init_types();
|
|
21173
|
+
function isNewerVersion(candidate, current) {
|
|
21174
|
+
const parse3 = (v) => {
|
|
21175
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
21176
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
21177
|
+
};
|
|
21178
|
+
const a = parse3(candidate);
|
|
21179
|
+
const b = parse3(current);
|
|
21180
|
+
if (a === null || b === null) return false;
|
|
21181
|
+
for (let i = 0; i < 3; i++) {
|
|
21182
|
+
if (a[i] !== b[i]) return a[i] > b[i];
|
|
21183
|
+
}
|
|
21184
|
+
return false;
|
|
21185
|
+
}
|
|
21186
|
+
function pickDesktopAsset(assets, platform, arch) {
|
|
21187
|
+
const suffix = platform === "darwin" ? "-mac.zip" : platform === "linux" ? ".AppImage" : null;
|
|
21188
|
+
if (suffix === null) return null;
|
|
21189
|
+
return assets.find(
|
|
21190
|
+
(a) => a.name.endsWith(suffix) && a.name.includes("-arm64") === (arch === "arm64")
|
|
21191
|
+
) ?? null;
|
|
21192
|
+
}
|
|
21193
|
+
function parseSums(text) {
|
|
21194
|
+
const sums = /* @__PURE__ */ new Map();
|
|
21195
|
+
for (const line of text.split("\n")) {
|
|
21196
|
+
const match = /^([0-9a-f]{64})\s+\*?(.+?)\s*$/.exec(line.trim());
|
|
21197
|
+
if (match) sums.set(match[2], match[1]);
|
|
21198
|
+
}
|
|
21199
|
+
return sums;
|
|
21200
|
+
}
|
|
21201
|
+
async function checkForDesktopUpdate(currentVersion, opts = {}) {
|
|
21202
|
+
const slug2 = repoSlug();
|
|
21203
|
+
if (slug2 === void 0) return null;
|
|
21204
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
21205
|
+
const response = await fetchFn(`https://api.github.com/repos/${slug2}/releases/latest`, {
|
|
21206
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "puddle-desktop" }
|
|
21207
|
+
});
|
|
21208
|
+
if (!response.ok) {
|
|
21209
|
+
throw new CliError("not_installed", `release lookup failed (HTTP ${response.status})`);
|
|
21210
|
+
}
|
|
21211
|
+
const release = await response.json();
|
|
21212
|
+
const version2 = (release.tag_name ?? "").replace(/^v/, "");
|
|
21213
|
+
if (!isNewerVersion(version2, currentVersion)) return null;
|
|
21214
|
+
return updateFromRelease(version2, release.assets ?? [], opts);
|
|
21215
|
+
}
|
|
21216
|
+
async function desktopUpdateAt(version2, opts = {}) {
|
|
21217
|
+
const slug2 = repoSlug();
|
|
21218
|
+
if (slug2 === void 0) {
|
|
21219
|
+
throw new CliError(
|
|
21220
|
+
"not_installed",
|
|
21221
|
+
"no release source is configured for this build",
|
|
21222
|
+
"set PUDDLE_REPO=owner/repo"
|
|
21223
|
+
);
|
|
21224
|
+
}
|
|
21225
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
21226
|
+
const response = await fetchFn(`https://api.github.com/repos/${slug2}/releases/tags/v${version2}`, {
|
|
21227
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "puddle-desktop" }
|
|
21228
|
+
});
|
|
21229
|
+
if (response.status === 404) {
|
|
21230
|
+
throw new CliError("not_installed", `no release v${version2} exists in ${slug2}`);
|
|
21231
|
+
}
|
|
21232
|
+
if (!response.ok) {
|
|
21233
|
+
throw new CliError("not_installed", `release lookup failed (HTTP ${response.status})`);
|
|
21234
|
+
}
|
|
21235
|
+
const release = await response.json();
|
|
21236
|
+
return updateFromRelease(version2, release.assets ?? [], opts);
|
|
21237
|
+
}
|
|
21238
|
+
function updateFromRelease(version2, assets, opts) {
|
|
21239
|
+
const asset = pickDesktopAsset(
|
|
21240
|
+
assets,
|
|
21241
|
+
opts.platform ?? process.platform,
|
|
21242
|
+
opts.arch ?? process.arch
|
|
21243
|
+
);
|
|
21244
|
+
const sums = assets.find((a) => a.name === "SHA256SUMS");
|
|
21245
|
+
if (asset === null || sums === void 0) return null;
|
|
21246
|
+
return {
|
|
21247
|
+
version: version2,
|
|
21248
|
+
asset: { name: asset.name, url: asset.browser_download_url },
|
|
21249
|
+
sumsUrl: sums.browser_download_url
|
|
21250
|
+
};
|
|
21251
|
+
}
|
|
21252
|
+
async function stageDesktopUpdate(update, opts = {}) {
|
|
21253
|
+
const logger = opts.logger ?? silentLogger;
|
|
21254
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
21255
|
+
const dir = join8(opts.cacheDir ?? join8(clientHome(), "cache", "desktop"), update.version);
|
|
21256
|
+
await rm(dir, { recursive: true, force: true });
|
|
21257
|
+
await mkdir(dir, { recursive: true });
|
|
21258
|
+
try {
|
|
21259
|
+
logger.info(`downloading ${update.asset.name}`);
|
|
21260
|
+
const archive = join8(dir, update.asset.name);
|
|
21261
|
+
const response = await fetchFn(update.asset.url, {
|
|
21262
|
+
headers: { "user-agent": "puddle-desktop" }
|
|
21263
|
+
});
|
|
21264
|
+
if (!response.ok || response.body === null) {
|
|
21265
|
+
throw new CliError("not_installed", `download failed (HTTP ${response.status})`);
|
|
21266
|
+
}
|
|
21267
|
+
await pipeline(
|
|
21268
|
+
Readable.fromWeb(response.body),
|
|
21269
|
+
createWriteStream(archive)
|
|
21270
|
+
);
|
|
21271
|
+
const sumsResponse = await fetchFn(update.sumsUrl, {
|
|
21272
|
+
headers: { "user-agent": "puddle-desktop" }
|
|
21273
|
+
});
|
|
21274
|
+
if (!sumsResponse.ok) {
|
|
21275
|
+
throw new CliError("not_installed", `SHA256SUMS fetch failed (HTTP ${sumsResponse.status})`);
|
|
21276
|
+
}
|
|
21277
|
+
const expected = parseSums(await sumsResponse.text()).get(update.asset.name);
|
|
21278
|
+
if (expected === void 0) {
|
|
21279
|
+
throw new CliError("not_installed", `${update.asset.name} is not in SHA256SUMS`);
|
|
21280
|
+
}
|
|
21281
|
+
const actual = await fileSha256(archive);
|
|
21282
|
+
if (actual !== expected) {
|
|
21283
|
+
throw new CliError(
|
|
21284
|
+
"not_installed",
|
|
21285
|
+
`checksum mismatch for ${update.asset.name}`,
|
|
21286
|
+
`expected ${expected}, got ${actual}`
|
|
21287
|
+
);
|
|
21288
|
+
}
|
|
21289
|
+
if (update.asset.name.endsWith(".zip")) {
|
|
21290
|
+
const extractDir = join8(dir, "extract");
|
|
21291
|
+
await run("/usr/bin/ditto", ["-x", "-k", archive, extractDir]);
|
|
21292
|
+
const bundle = (await readdir(extractDir)).find((name) => name.endsWith(".app"));
|
|
21293
|
+
if (bundle === void 0) {
|
|
21294
|
+
throw new CliError("not_installed", "the update zip contains no .app bundle");
|
|
21295
|
+
}
|
|
21296
|
+
logger.info(`staged ${update.version} at ${join8(extractDir, bundle)}`);
|
|
21297
|
+
return {
|
|
21298
|
+
version: update.version,
|
|
21299
|
+
kind: "mac-app",
|
|
21300
|
+
stagedPath: join8(extractDir, bundle),
|
|
21301
|
+
dir
|
|
21302
|
+
};
|
|
21303
|
+
}
|
|
21304
|
+
await chmod(archive, 493);
|
|
21305
|
+
logger.info(`staged ${update.version} at ${archive}`);
|
|
21306
|
+
return { version: update.version, kind: "appimage", stagedPath: archive, dir };
|
|
21307
|
+
} catch (e) {
|
|
21308
|
+
await rm(dir, { recursive: true, force: true });
|
|
21309
|
+
throw e;
|
|
21310
|
+
}
|
|
21311
|
+
}
|
|
21312
|
+
async function pruneDesktopUpdateCache(keep = [], opts = {}) {
|
|
21313
|
+
const dir = opts.cacheDir ?? join8(clientHome(), "cache", "desktop");
|
|
21314
|
+
let entries;
|
|
21315
|
+
try {
|
|
21316
|
+
entries = await readdir(dir);
|
|
21317
|
+
} catch {
|
|
21318
|
+
return;
|
|
21319
|
+
}
|
|
21320
|
+
await Promise.all(
|
|
21321
|
+
entries.filter((name) => !keep.includes(name)).map((name) => rm(join8(dir, name), { recursive: true, force: true }).catch(() => {
|
|
21322
|
+
}))
|
|
21323
|
+
);
|
|
21324
|
+
}
|
|
21325
|
+
async function applyDesktopUpdate(staged, opts) {
|
|
21326
|
+
const logger = opts.logger ?? silentLogger;
|
|
21327
|
+
const script = swapScript(staged, opts);
|
|
21328
|
+
const scriptPath = join8(staged.dir, "apply.sh");
|
|
21329
|
+
await writeFile(scriptPath, script, { mode: 493 });
|
|
21330
|
+
logger.info(`applying ${staged.version} via ${scriptPath}`);
|
|
21331
|
+
if (opts.detach ?? true) {
|
|
21332
|
+
const child = spawn5("/bin/sh", [scriptPath], { detached: true, stdio: "ignore" });
|
|
21333
|
+
child.unref();
|
|
21334
|
+
return;
|
|
21335
|
+
}
|
|
21336
|
+
await run("/bin/sh", [scriptPath]);
|
|
21337
|
+
}
|
|
21338
|
+
var q = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
21339
|
+
function swapScript(staged, opts) {
|
|
21340
|
+
const wait = opts.waitPid === void 0 ? [] : [
|
|
21341
|
+
`i=0`,
|
|
21342
|
+
`while kill -0 ${opts.waitPid} 2>/dev/null; do`,
|
|
21343
|
+
` i=$((i+1)); [ "$i" -gt 600 ] && exit 1`,
|
|
21344
|
+
` sleep 0.1`,
|
|
21345
|
+
`done`
|
|
21346
|
+
];
|
|
21347
|
+
if (staged.kind === "mac-app") {
|
|
21348
|
+
return [
|
|
21349
|
+
`#!/bin/sh`,
|
|
21350
|
+
`# Puddle desktop update ${staged.version} \u2014 swap once the app exits`,
|
|
21351
|
+
...wait,
|
|
21352
|
+
`target=${q(opts.targetPath)}`,
|
|
21353
|
+
`target_dir=${q(dirname5(opts.targetPath))}`,
|
|
21354
|
+
`staged=${q(staged.stagedPath)}`,
|
|
21355
|
+
`mkdir -p "$target_dir" || exit 1`,
|
|
21356
|
+
`rm -rf "$target.old"`,
|
|
21357
|
+
`had_old=0`,
|
|
21358
|
+
`if [ -e "$target" ]; then`,
|
|
21359
|
+
` mv "$target" "$target.old" || exit 1`,
|
|
21360
|
+
` had_old=1`,
|
|
21361
|
+
`fi`,
|
|
21362
|
+
`if mv "$staged" "$target" 2>/dev/null || /usr/bin/ditto "$staged" "$target"; then`,
|
|
21363
|
+
// Defence in depth: the normal path never quarantines (node's fetch is
|
|
21364
|
+
// not a quarantine-opted-in app, and ditto only propagates what the
|
|
21365
|
+
// zip already carries), but if the bundle ever acquires the attribute
|
|
21366
|
+
// the swap must not resurrect the Gatekeeper prompt.
|
|
21367
|
+
` /usr/bin/xattr -dr com.apple.quarantine "$target" 2>/dev/null`,
|
|
21368
|
+
` rm -rf "$target.old" ${q(staged.dir)}`,
|
|
21369
|
+
...opts.relaunch ? [` open "$target"`] : [],
|
|
21370
|
+
`else`,
|
|
21371
|
+
` rm -rf "$target"`,
|
|
21372
|
+
` [ "$had_old" -eq 0 ] || mv "$target.old" "$target"`,
|
|
21373
|
+
` exit 1`,
|
|
21374
|
+
`fi`,
|
|
21375
|
+
``
|
|
21376
|
+
].join("\n");
|
|
21377
|
+
}
|
|
21378
|
+
return [
|
|
21379
|
+
`#!/bin/sh`,
|
|
21380
|
+
`# Puddle desktop update ${staged.version} \u2014 swap once the app exits`,
|
|
21381
|
+
...wait,
|
|
21382
|
+
`target=${q(opts.targetPath)}`,
|
|
21383
|
+
`cp -f ${q(staged.stagedPath)} "$target" || exit 1`,
|
|
21384
|
+
`chmod +x "$target"`,
|
|
21385
|
+
`rm -rf ${q(staged.dir)}`,
|
|
21386
|
+
...opts.relaunch ? [`"$target" >/dev/null 2>&1 &`] : [],
|
|
21387
|
+
``
|
|
21388
|
+
].join("\n");
|
|
21389
|
+
}
|
|
21390
|
+
async function findInstalledDesktopApp(opts = {}) {
|
|
21391
|
+
if ((opts.platform ?? process.platform) !== "darwin") return null;
|
|
21392
|
+
const { readFile } = await import("node:fs/promises");
|
|
21393
|
+
const { homedir: homedir3 } = await import("node:os");
|
|
21394
|
+
const systemApplications = opts.systemApplicationsDir ?? "/Applications";
|
|
21395
|
+
const home = opts.homeDir ?? homedir3();
|
|
21396
|
+
for (const appPath of [
|
|
21397
|
+
join8(systemApplications, "Puddle.app"),
|
|
21398
|
+
join8(home, "Applications", "Puddle.app")
|
|
21399
|
+
]) {
|
|
21400
|
+
try {
|
|
21401
|
+
const plist = await readFile(join8(appPath, "Contents/Info.plist"), "utf8");
|
|
21402
|
+
const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
|
|
21403
|
+
plist
|
|
21404
|
+
);
|
|
21405
|
+
if (match) return { appPath, version: match[1] };
|
|
21406
|
+
} catch {
|
|
21407
|
+
}
|
|
21408
|
+
}
|
|
21409
|
+
return null;
|
|
21410
|
+
}
|
|
21411
|
+
async function desktopAppInstallPath(opts = {}) {
|
|
21412
|
+
if ((opts.platform ?? process.platform) !== "darwin") return null;
|
|
21413
|
+
const { homedir: homedir3 } = await import("node:os");
|
|
21414
|
+
const systemApplications = opts.systemApplicationsDir ?? "/Applications";
|
|
21415
|
+
const canWrite = opts.canWrite ?? (async (path) => {
|
|
21416
|
+
try {
|
|
21417
|
+
await access(path, constants.W_OK);
|
|
21418
|
+
return true;
|
|
21419
|
+
} catch {
|
|
21420
|
+
return false;
|
|
21421
|
+
}
|
|
21422
|
+
});
|
|
21423
|
+
if (await canWrite(systemApplications)) return join8(systemApplications, "Puddle.app");
|
|
21424
|
+
const userApplications = join8(opts.homeDir ?? homedir3(), "Applications");
|
|
21425
|
+
await mkdir(userApplications, { recursive: true });
|
|
21426
|
+
return join8(userApplications, "Puddle.app");
|
|
21427
|
+
}
|
|
21428
|
+
function isDesktopAppRunning(appPath) {
|
|
21429
|
+
return new Promise((resolve3) => {
|
|
21430
|
+
const child = spawn5("/usr/bin/pgrep", ["-f", `${appPath}/Contents/MacOS/`], {
|
|
21431
|
+
stdio: "ignore"
|
|
21432
|
+
});
|
|
21433
|
+
child.on("error", () => resolve3(false));
|
|
21434
|
+
child.on("exit", (code) => resolve3(code === 0));
|
|
21435
|
+
});
|
|
21436
|
+
}
|
|
21437
|
+
function fileSha256(path) {
|
|
21438
|
+
const hash2 = createHash("sha256");
|
|
21439
|
+
return pipeline(createReadStream2(path), hash2).then(() => hash2.digest("hex"));
|
|
21440
|
+
}
|
|
21441
|
+
function run(command, args) {
|
|
21442
|
+
return new Promise((resolve3, reject) => {
|
|
21443
|
+
const child = spawn5(command, args, { stdio: "ignore" });
|
|
21444
|
+
child.on("error", reject);
|
|
21445
|
+
child.on(
|
|
21446
|
+
"exit",
|
|
21447
|
+
(code) => code === 0 ? resolve3() : reject(new CliError("not_installed", `${command} exited with ${code ?? "signal"}`))
|
|
21448
|
+
);
|
|
21449
|
+
});
|
|
21450
|
+
}
|
|
21451
|
+
|
|
21452
|
+
// src/lib/releases.ts
|
|
21453
|
+
init_types();
|
|
21454
|
+
async function latestReleaseVersion(opts = {}) {
|
|
21455
|
+
const slug2 = repoSlug();
|
|
21456
|
+
if (slug2 === void 0) {
|
|
21457
|
+
throw new CliError(
|
|
21458
|
+
"not_installed",
|
|
21459
|
+
"no release source is configured for this build",
|
|
21460
|
+
"name a version and pass --tarball, or set PUDDLE_REPO=owner/repo"
|
|
21461
|
+
);
|
|
21462
|
+
}
|
|
21463
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
21464
|
+
const res = await fetchFn(`https://api.github.com/repos/${slug2}/releases/latest`, {
|
|
21465
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "puddle-cli" }
|
|
21466
|
+
});
|
|
21467
|
+
if (!res.ok) {
|
|
21468
|
+
throw new CliError(
|
|
21469
|
+
"not_installed",
|
|
21470
|
+
`release lookup failed (HTTP ${res.status})`,
|
|
21471
|
+
"name a version explicitly, e.g. puddle install daemon@v0.0.32"
|
|
21472
|
+
);
|
|
21473
|
+
}
|
|
21474
|
+
const tag = (await res.json()).tag_name ?? "";
|
|
21475
|
+
const version2 = tag.replace(/^v/, "");
|
|
21476
|
+
if (version2 === "") throw new CliError("not_installed", `${slug2} has no published releases`);
|
|
21477
|
+
return version2;
|
|
21478
|
+
}
|
|
21479
|
+
|
|
21480
|
+
// src/lib/remove-daemon.ts
|
|
21481
|
+
init_types();
|
|
21482
|
+
async function sweepDirtyWorktrees(transport) {
|
|
21483
|
+
const script = `
|
|
21484
|
+
W="\${PUDDLE_HOME:-$HOME/.puddle}/worktrees"
|
|
21485
|
+
[ -d "$W" ] || exit 0
|
|
21486
|
+
for d in "$W"/*/*; do
|
|
21487
|
+
[ -e "$d/.git" ] || continue
|
|
21488
|
+
dirty=$(git -C "$d" status --porcelain 2>/dev/null | head -1)
|
|
21489
|
+
unpushed=$(git -C "$d" log --branches --not --remotes --oneline 2>/dev/null | head -1)
|
|
21490
|
+
if [ -n "$dirty" ] || [ -n "$unpushed" ]; then printf '%s\\n' "$d"; fi
|
|
21491
|
+
done
|
|
21492
|
+
exit 0`;
|
|
21493
|
+
const result = await transport.exec("sh -s", { stdin: script, timeoutMs: 6e4 });
|
|
21494
|
+
if (result.code !== 0) return [];
|
|
21495
|
+
return result.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
21496
|
+
}
|
|
21497
|
+
async function removeDaemon(transport, opts = { purge: false }) {
|
|
21498
|
+
const logger = opts.logger ?? silentLogger;
|
|
21499
|
+
const script = `
|
|
21500
|
+
set -u
|
|
21501
|
+
HOME_DIR="\${PUDDLE_HOME:-$HOME/.puddle}"
|
|
21502
|
+
say() { printf 'puddled remove: %s\\n' "$1"; }
|
|
21503
|
+
|
|
21504
|
+
# systemd user unit
|
|
21505
|
+
if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
|
|
21506
|
+
systemctl --user disable --now puddled >/dev/null 2>&1 || true
|
|
21507
|
+
if [ -f "$HOME/.config/systemd/user/puddled.service" ]; then
|
|
21508
|
+
rm -f "$HOME/.config/systemd/user/puddled.service"
|
|
21509
|
+
systemctl --user daemon-reload >/dev/null 2>&1 || true
|
|
21510
|
+
say "removed the systemd user unit"
|
|
21511
|
+
fi
|
|
21512
|
+
fi
|
|
21513
|
+
|
|
21514
|
+
# launchd agent
|
|
21515
|
+
PLIST="$HOME/Library/LaunchAgents/dev.puddle.puddled.plist"
|
|
21516
|
+
if [ -f "$PLIST" ]; then
|
|
21517
|
+
launchctl bootout "gui/$(id -u)/dev.puddle.puddled" 2>/dev/null \\
|
|
21518
|
+
|| launchctl unload "$PLIST" 2>/dev/null || true
|
|
21519
|
+
rm -f "$PLIST"
|
|
21520
|
+
say "removed the launchd agent"
|
|
21521
|
+
fi
|
|
21522
|
+
|
|
21523
|
+
# nohup fallback
|
|
21524
|
+
if [ -f "$HOME_DIR/puddled.pid" ]; then
|
|
21525
|
+
kill "$(cat "$HOME_DIR/puddled.pid")" 2>/dev/null || true
|
|
21526
|
+
rm -f "$HOME_DIR/puddled.pid"
|
|
21527
|
+
fi
|
|
21528
|
+
|
|
21529
|
+
if [ "\${PUDDLE_PURGE:-0}" = 1 ]; then
|
|
21530
|
+
rm -rf "$HOME_DIR"
|
|
21531
|
+
say "deleted $HOME_DIR"
|
|
21532
|
+
else
|
|
21533
|
+
rm -rf "$HOME_DIR/bin" "$HOME_DIR/cache" "$HOME_DIR/runtime.json"
|
|
21534
|
+
say "removed the install; data kept under $HOME_DIR"
|
|
21535
|
+
fi`;
|
|
21536
|
+
const result = await transport.exec(`PUDDLE_PURGE=${opts.purge ? 1 : 0} sh -s`, {
|
|
21537
|
+
stdin: script,
|
|
21538
|
+
timeoutMs: 2 * 6e4,
|
|
21539
|
+
onStdout: (chunk) => {
|
|
21540
|
+
for (const line of chunk.split("\n")) if (line.trim() !== "") logger.info(line.trimEnd());
|
|
21541
|
+
}
|
|
21542
|
+
});
|
|
21543
|
+
if (result.code !== 0) {
|
|
21544
|
+
throw new CliError(
|
|
21545
|
+
"not_installed",
|
|
21546
|
+
`the removal failed on ${transport.label} (exit ${result.code})`,
|
|
21547
|
+
result.stderr.trim().split("\n").slice(-3).join("\n")
|
|
21548
|
+
);
|
|
21549
|
+
}
|
|
21550
|
+
}
|
|
21551
|
+
|
|
21552
|
+
// src/cli/manage.ts
|
|
21300
21553
|
init_types();
|
|
21301
21554
|
|
|
21302
21555
|
// src/lib/upgrade.ts
|
|
@@ -21455,6 +21708,427 @@ async function terminateCockpit(record2) {
|
|
|
21455
21708
|
if (current === null || current.pid === record2.pid) removeCockpitRecord(record2.target);
|
|
21456
21709
|
}
|
|
21457
21710
|
|
|
21711
|
+
// src/cli/prompt.ts
|
|
21712
|
+
init_types();
|
|
21713
|
+
import { createInterface } from "node:readline";
|
|
21714
|
+
async function ask(question, def) {
|
|
21715
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return def;
|
|
21716
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21717
|
+
try {
|
|
21718
|
+
const answer = await new Promise(
|
|
21719
|
+
(resolve3) => rl.question(`${question} [${def}] `, resolve3)
|
|
21720
|
+
);
|
|
21721
|
+
return answer.trim() === "" ? def : answer.trim();
|
|
21722
|
+
} finally {
|
|
21723
|
+
rl.close();
|
|
21724
|
+
}
|
|
21725
|
+
}
|
|
21726
|
+
async function confirm(question, opts = {}) {
|
|
21727
|
+
if (opts.skip === true) return true;
|
|
21728
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
21729
|
+
throw new CliError(
|
|
21730
|
+
"bad_arguments",
|
|
21731
|
+
"refusing to confirm without a terminal",
|
|
21732
|
+
"pass --yes to proceed non-interactively"
|
|
21733
|
+
);
|
|
21734
|
+
}
|
|
21735
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21736
|
+
try {
|
|
21737
|
+
const answer = await new Promise(
|
|
21738
|
+
(resolve3) => rl.question(`${question} [y/N] `, resolve3)
|
|
21739
|
+
);
|
|
21740
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
21741
|
+
} finally {
|
|
21742
|
+
rl.close();
|
|
21743
|
+
}
|
|
21744
|
+
}
|
|
21745
|
+
|
|
21746
|
+
// src/cli/manage.ts
|
|
21747
|
+
function assertLocal(what, host, verb) {
|
|
21748
|
+
if (host === void 0 || host === "local") return;
|
|
21749
|
+
throw new CliError(
|
|
21750
|
+
"bad_arguments",
|
|
21751
|
+
`puddle ${verb} ${what} runs on the machine it ${verb}s`,
|
|
21752
|
+
`ssh into ${host} and run it there \u2014 user@host targets the daemon only`
|
|
21753
|
+
);
|
|
21754
|
+
}
|
|
21755
|
+
async function openTransport(host) {
|
|
21756
|
+
const transport = host === void 0 || host === "local" ? new LocalTransport() : new SshTransport(host);
|
|
21757
|
+
if (transport instanceof SshTransport) await transport.open();
|
|
21758
|
+
return transport;
|
|
21759
|
+
}
|
|
21760
|
+
async function resolveDaemonVersion(logger) {
|
|
21761
|
+
try {
|
|
21762
|
+
return await latestReleaseVersion();
|
|
21763
|
+
} catch (e) {
|
|
21764
|
+
const pinned = pinnedDaemonVersion();
|
|
21765
|
+
logger.warn(
|
|
21766
|
+
`cannot resolve the newest release (${e instanceof Error ? e.message : String(e)}) \u2014 using this CLI's version train v${pinned}`
|
|
21767
|
+
);
|
|
21768
|
+
return pinned;
|
|
21769
|
+
}
|
|
21770
|
+
}
|
|
21771
|
+
function pinWarning(logger, version2) {
|
|
21772
|
+
if (version2 === cliVersion()) return;
|
|
21773
|
+
logger.warn(
|
|
21774
|
+
`pinning the daemon to v${version2} while this CLI is v${cliVersion()} \u2014 if the two speak different protocol majors, the next \`puddle launch\` will upgrade the daemon back (or refuse). Pin the CLI too (puddle upgrade cli@v${version2}) or launch with --no-upgrade.`
|
|
21775
|
+
);
|
|
21776
|
+
}
|
|
21777
|
+
async function runInstall(cmd, logger) {
|
|
21778
|
+
if (cmd.what === "desktop") {
|
|
21779
|
+
assertLocal("desktop", cmd.host, "install");
|
|
21780
|
+
if (cmd.tarball !== void 0) {
|
|
21781
|
+
throw new CliError("bad_arguments", "--tarball applies to the daemon only");
|
|
21782
|
+
}
|
|
21783
|
+
return setDesktop({ version: cmd.version, verb: "install" }, logger);
|
|
21784
|
+
}
|
|
21785
|
+
const transport = await openTransport(cmd.host);
|
|
21786
|
+
try {
|
|
21787
|
+
const installed = await installedVersion(transport);
|
|
21788
|
+
if (installed !== null && cmd.version === void 0) {
|
|
21789
|
+
logger.info(
|
|
21790
|
+
`puddled v${installed} is already installed on ${transport.label} \u2014 \`puddle upgrade daemon\` moves it to the newest release`
|
|
21791
|
+
);
|
|
21792
|
+
return 0;
|
|
21793
|
+
}
|
|
21794
|
+
if (installed !== null && installed === cmd.version) {
|
|
21795
|
+
logger.info(`puddled v${installed} is already installed on ${transport.label}`);
|
|
21796
|
+
return 0;
|
|
21797
|
+
}
|
|
21798
|
+
return installDaemonAt(transport, cmd.version, cmd.tarball, logger);
|
|
21799
|
+
} finally {
|
|
21800
|
+
transport.dispose();
|
|
21801
|
+
}
|
|
21802
|
+
}
|
|
21803
|
+
async function installDaemonAt(transport, explicit, tarball, logger) {
|
|
21804
|
+
const version2 = explicit ?? (tarball !== void 0 ? void 0 : await resolveDaemonVersion(logger));
|
|
21805
|
+
if (explicit !== void 0) pinWarning(logger, explicit);
|
|
21806
|
+
const result = await upgradeDaemon(transport, {
|
|
21807
|
+
...version2 !== void 0 ? { version: version2 } : {},
|
|
21808
|
+
...tarball !== void 0 ? { tarball } : {},
|
|
21809
|
+
logger
|
|
21810
|
+
});
|
|
21811
|
+
logger.info(`puddled ${result.from ?? "(fresh install)"} \u2192 ${result.to} on ${transport.label}`);
|
|
21812
|
+
return 0;
|
|
21813
|
+
}
|
|
21814
|
+
async function runUpgrade(cmd, logger) {
|
|
21815
|
+
if (cmd.what !== void 0) {
|
|
21816
|
+
if (cmd.what !== "daemon" && cmd.tarball !== void 0) {
|
|
21817
|
+
throw new CliError("bad_arguments", "--tarball applies to the daemon only");
|
|
21818
|
+
}
|
|
21819
|
+
switch (cmd.what) {
|
|
21820
|
+
case "cli":
|
|
21821
|
+
assertLocal("cli", cmd.host, "upgrade");
|
|
21822
|
+
return upgradeCli(cmd.version, logger);
|
|
21823
|
+
case "desktop":
|
|
21824
|
+
assertLocal("desktop", cmd.host, "upgrade");
|
|
21825
|
+
return setDesktop({ version: cmd.version, verb: "upgrade" }, logger);
|
|
21826
|
+
case "daemon": {
|
|
21827
|
+
const transport2 = await openTransport(cmd.host);
|
|
21828
|
+
try {
|
|
21829
|
+
return await installDaemonAt(transport2, cmd.version, cmd.tarball, logger);
|
|
21830
|
+
} finally {
|
|
21831
|
+
transport2.dispose();
|
|
21832
|
+
}
|
|
21833
|
+
}
|
|
21834
|
+
}
|
|
21835
|
+
}
|
|
21836
|
+
if (cmd.host !== void 0 && cmd.host !== "local") {
|
|
21837
|
+
logger.info(`${cmd.host} hosts the daemon only \u2014 upgrading it`);
|
|
21838
|
+
const transport2 = await openTransport(cmd.host);
|
|
21839
|
+
try {
|
|
21840
|
+
return await installDaemonAt(transport2, cmd.version, cmd.tarball, logger);
|
|
21841
|
+
} finally {
|
|
21842
|
+
transport2.dispose();
|
|
21843
|
+
}
|
|
21844
|
+
}
|
|
21845
|
+
const transport = new LocalTransport();
|
|
21846
|
+
const daemonInstalled = await installedVersion(transport) !== null;
|
|
21847
|
+
const desktopInstalled = process.platform === "darwin" && await findInstalledDesktopApp() !== null;
|
|
21848
|
+
if (daemonInstalled) await installDaemonAt(transport, cmd.version, cmd.tarball, logger);
|
|
21849
|
+
else logger.info("no daemon installed on this machine \u2014 skipping (puddle install daemon)");
|
|
21850
|
+
if (desktopInstalled) await setDesktop({ version: cmd.version, verb: "upgrade" }, logger);
|
|
21851
|
+
else if (process.platform === "darwin") logger.info("no desktop app installed \u2014 skipping");
|
|
21852
|
+
return upgradeCli(cmd.version, logger);
|
|
21853
|
+
}
|
|
21854
|
+
function upgradeCli(version2, logger) {
|
|
21855
|
+
const spec = `@puddle-code/cli@${version2 ?? "latest"}`;
|
|
21856
|
+
logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for ${version2 ?? "the latest release"}`);
|
|
21857
|
+
return new Promise((resolve3, reject) => {
|
|
21858
|
+
const child = spawn7("npm", ["install", "-g", spec], { stdio: "inherit" });
|
|
21859
|
+
child.on(
|
|
21860
|
+
"error",
|
|
21861
|
+
() => reject(
|
|
21862
|
+
new CliError(
|
|
21863
|
+
"not_installed",
|
|
21864
|
+
"npm is not on PATH",
|
|
21865
|
+
`the CLI is installed and upgraded via npm: npm install -g ${spec}`
|
|
21866
|
+
)
|
|
21867
|
+
)
|
|
21868
|
+
);
|
|
21869
|
+
child.on("exit", (code) => {
|
|
21870
|
+
if (code === 0) {
|
|
21871
|
+
logger.info("done \u2014 `puddle --version` shows the installed version");
|
|
21872
|
+
resolve3(0);
|
|
21873
|
+
} else {
|
|
21874
|
+
reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
|
|
21875
|
+
}
|
|
21876
|
+
});
|
|
21877
|
+
});
|
|
21878
|
+
}
|
|
21879
|
+
async function setDesktop(opts, logger) {
|
|
21880
|
+
if (process.platform === "linux") {
|
|
21881
|
+
if (opts.verb !== "install") {
|
|
21882
|
+
throw new CliError(
|
|
21883
|
+
"not_installed",
|
|
21884
|
+
"AppImages upgrade from inside the app (its toast knows $APPIMAGE)",
|
|
21885
|
+
"to place a fresh copy: puddle install desktop"
|
|
21886
|
+
);
|
|
21887
|
+
}
|
|
21888
|
+
return installDesktopAppImage(opts.version, logger);
|
|
21889
|
+
}
|
|
21890
|
+
const installed = await findInstalledDesktopApp();
|
|
21891
|
+
const targetPath = installed?.appPath ?? await desktopAppInstallPath();
|
|
21892
|
+
if (targetPath === null) {
|
|
21893
|
+
throw new CliError("not_installed", "the desktop app ships for macOS and Linux only");
|
|
21894
|
+
}
|
|
21895
|
+
if (installed !== null && await isDesktopAppRunning(installed.appPath)) {
|
|
21896
|
+
throw new CliError(
|
|
21897
|
+
"already_running",
|
|
21898
|
+
"Puddle is running \u2014 use its update toast, or quit it and rerun"
|
|
21899
|
+
);
|
|
21900
|
+
}
|
|
21901
|
+
if (opts.verb === "install" && installed !== null && opts.version === void 0) {
|
|
21902
|
+
logger.info(
|
|
21903
|
+
`Puddle ${installed.version} is already installed at ${installed.appPath} \u2014 \`puddle upgrade desktop\` moves it to the newest release`
|
|
21904
|
+
);
|
|
21905
|
+
return 0;
|
|
21906
|
+
}
|
|
21907
|
+
if (installed !== null && installed.version === opts.version) {
|
|
21908
|
+
logger.info(`Puddle ${installed.version} is already installed at ${installed.appPath}`);
|
|
21909
|
+
return 0;
|
|
21910
|
+
}
|
|
21911
|
+
const update = opts.version !== void 0 ? await desktopUpdateAt(opts.version) : await checkForDesktopUpdate(installed?.version ?? "0.0.0");
|
|
21912
|
+
if (update === null) {
|
|
21913
|
+
if (opts.version !== void 0) {
|
|
21914
|
+
throw new CliError(
|
|
21915
|
+
"not_installed",
|
|
21916
|
+
`release v${opts.version} has no desktop build for this platform/architecture`
|
|
21917
|
+
);
|
|
21918
|
+
}
|
|
21919
|
+
if (installed === null) {
|
|
21920
|
+
throw new CliError(
|
|
21921
|
+
"not_installed",
|
|
21922
|
+
"no Puddle desktop release is available for this macOS architecture"
|
|
21923
|
+
);
|
|
21924
|
+
}
|
|
21925
|
+
logger.info(`Puddle ${installed.version} is already the latest release`);
|
|
21926
|
+
return 0;
|
|
21927
|
+
}
|
|
21928
|
+
const move = installed === null ? `installing Puddle ${update.version}` : `Puddle ${installed.version} \u2192 ${update.version}` + (isNewerVersion(update.version, installed.version) ? "" : " (a downgrade)");
|
|
21929
|
+
logger.info(move);
|
|
21930
|
+
const staged = await stageDesktopUpdate(update, { logger });
|
|
21931
|
+
await applyDesktopUpdate(staged, { targetPath, detach: false, relaunch: false, logger });
|
|
21932
|
+
logger.info(`Puddle ${update.version} installed at ${targetPath}`);
|
|
21933
|
+
return 0;
|
|
21934
|
+
}
|
|
21935
|
+
async function installDesktopAppImage(version2, logger) {
|
|
21936
|
+
const update = version2 !== void 0 ? await desktopUpdateAt(version2) : await checkForDesktopUpdate("0.0.0");
|
|
21937
|
+
if (update === null) {
|
|
21938
|
+
throw new CliError(
|
|
21939
|
+
"not_installed",
|
|
21940
|
+
version2 !== void 0 ? `release v${version2} has no AppImage for this architecture` : "no Puddle desktop release is available for this Linux architecture"
|
|
21941
|
+
);
|
|
21942
|
+
}
|
|
21943
|
+
const dir = expandHome2(await ask("Where should the AppImage be stored?", "~/puddle"));
|
|
21944
|
+
const target = join9(dir, "Puddle.AppImage");
|
|
21945
|
+
if (existsSync3(target) && version2 === void 0) {
|
|
21946
|
+
logger.info(`${target} already exists \u2014 the app updates itself in-app (its update toast)`);
|
|
21947
|
+
return 0;
|
|
21948
|
+
}
|
|
21949
|
+
logger.info(`installing Puddle ${update.version} to ${target}`);
|
|
21950
|
+
const staged = await stageDesktopUpdate(update, { logger });
|
|
21951
|
+
await mkdir2(dir, { recursive: true });
|
|
21952
|
+
await rm2(target, { force: true });
|
|
21953
|
+
try {
|
|
21954
|
+
await rename(staged.stagedPath, target);
|
|
21955
|
+
} catch {
|
|
21956
|
+
await copyFile(staged.stagedPath, target);
|
|
21957
|
+
await chmod2(target, 493);
|
|
21958
|
+
}
|
|
21959
|
+
await rm2(staged.dir, { recursive: true, force: true });
|
|
21960
|
+
logger.info(`installed \u2014 run it directly (${target}) or add it to your launcher`);
|
|
21961
|
+
const opener = spawn7("xdg-open", [dir], { stdio: "ignore", detached: true });
|
|
21962
|
+
opener.on("error", () => void 0);
|
|
21963
|
+
opener.unref();
|
|
21964
|
+
return 0;
|
|
21965
|
+
}
|
|
21966
|
+
function expandHome2(path) {
|
|
21967
|
+
if (path === "~") return homedir2();
|
|
21968
|
+
return path.startsWith("~/") ? join9(homedir2(), path.slice(2)) : path;
|
|
21969
|
+
}
|
|
21970
|
+
async function runRemove(cmd, logger) {
|
|
21971
|
+
switch (cmd.what) {
|
|
21972
|
+
case "cli":
|
|
21973
|
+
assertLocal("cli", cmd.host, "remove");
|
|
21974
|
+
return removeCli(cmd.yes, logger);
|
|
21975
|
+
case "desktop":
|
|
21976
|
+
assertLocal("desktop", cmd.host, "remove");
|
|
21977
|
+
return removeDesktop(cmd.yes, logger);
|
|
21978
|
+
case "daemon":
|
|
21979
|
+
return removeDaemonCmd(cmd, logger);
|
|
21980
|
+
}
|
|
21981
|
+
}
|
|
21982
|
+
async function removeCli(yes, logger) {
|
|
21983
|
+
const managed = await new Promise((resolve3) => {
|
|
21984
|
+
const child = spawn7("npm", ["ls", "-g", "@puddle-code/cli", "--depth=0"], { stdio: "ignore" });
|
|
21985
|
+
child.on("error", () => resolve3(false));
|
|
21986
|
+
child.on("exit", (code) => resolve3(code === 0));
|
|
21987
|
+
});
|
|
21988
|
+
if (!managed) {
|
|
21989
|
+
throw new CliError(
|
|
21990
|
+
"not_installed",
|
|
21991
|
+
"the puddle CLI is not an npm global install on this machine",
|
|
21992
|
+
"a repo checkout or linked dev build is removed the way it was installed"
|
|
21993
|
+
);
|
|
21994
|
+
}
|
|
21995
|
+
const proceed = await confirm(
|
|
21996
|
+
`Remove the puddle CLI (npm uninstall -g @puddle-code/cli)? Running cockpits keep running until killed; daemons and their sessions are untouched.`,
|
|
21997
|
+
{ skip: yes }
|
|
21998
|
+
);
|
|
21999
|
+
if (!proceed) {
|
|
22000
|
+
logger.info("nothing removed");
|
|
22001
|
+
return 0;
|
|
22002
|
+
}
|
|
22003
|
+
return new Promise((resolve3, reject) => {
|
|
22004
|
+
const child = spawn7("npm", ["uninstall", "-g", "@puddle-code/cli"], { stdio: "inherit" });
|
|
22005
|
+
child.on("error", () => reject(new CliError("not_installed", "npm is not on PATH")));
|
|
22006
|
+
child.on("exit", (code) => {
|
|
22007
|
+
if (code === 0) {
|
|
22008
|
+
logger.info("removed \u2014 daemons keep running (puddle remove daemon uninstalls one)");
|
|
22009
|
+
resolve3(0);
|
|
22010
|
+
} else {
|
|
22011
|
+
reject(new CliError("not_installed", `npm uninstall exited with ${code ?? "a signal"}`));
|
|
22012
|
+
}
|
|
22013
|
+
});
|
|
22014
|
+
});
|
|
22015
|
+
}
|
|
22016
|
+
async function removeDesktop(yes, logger) {
|
|
22017
|
+
if (process.platform !== "darwin") {
|
|
22018
|
+
throw new CliError(
|
|
22019
|
+
"not_installed",
|
|
22020
|
+
"desktop removal is macOS-only",
|
|
22021
|
+
"an AppImage carries no fixed path \u2014 delete the file where you put it (puddle install desktop defaults to ~/puddle/Puddle.AppImage)"
|
|
22022
|
+
);
|
|
22023
|
+
}
|
|
22024
|
+
const installed = await findInstalledDesktopApp();
|
|
22025
|
+
if (installed === null) {
|
|
22026
|
+
logger.info("no Puddle.app in /Applications or ~/Applications \u2014 nothing to remove");
|
|
22027
|
+
return 0;
|
|
22028
|
+
}
|
|
22029
|
+
if (await isDesktopAppRunning(installed.appPath)) {
|
|
22030
|
+
throw new CliError("already_running", "Puddle is running \u2014 quit it first");
|
|
22031
|
+
}
|
|
22032
|
+
const proceed = await confirm(
|
|
22033
|
+
`Delete ${installed.appPath} (Puddle ${installed.version})? Daemons and their sessions are untouched.`,
|
|
22034
|
+
{ skip: yes }
|
|
22035
|
+
);
|
|
22036
|
+
if (!proceed) {
|
|
22037
|
+
logger.info("nothing removed");
|
|
22038
|
+
return 0;
|
|
22039
|
+
}
|
|
22040
|
+
await rm2(installed.appPath, { recursive: true, force: true });
|
|
22041
|
+
await pruneDesktopUpdateCache([]);
|
|
22042
|
+
logger.info(`deleted ${installed.appPath} (recent hosts kept in ~/.puddle/recent-hosts.json)`);
|
|
22043
|
+
return 0;
|
|
22044
|
+
}
|
|
22045
|
+
async function removeDaemonCmd(cmd, logger) {
|
|
22046
|
+
const target = cmd.host ?? "local";
|
|
22047
|
+
const transport = await openTransport(cmd.host);
|
|
22048
|
+
let tunnel = null;
|
|
22049
|
+
try {
|
|
22050
|
+
const installed = await installedVersion(transport);
|
|
22051
|
+
if (installed === null) {
|
|
22052
|
+
logger.info(
|
|
22053
|
+
`no bootstrap-managed daemon on ${transport.label} \u2014 nothing to remove (its data, if any, stays under ~/.puddle)`
|
|
22054
|
+
);
|
|
22055
|
+
return 0;
|
|
22056
|
+
}
|
|
22057
|
+
let live = null;
|
|
22058
|
+
let profileNames = null;
|
|
22059
|
+
try {
|
|
22060
|
+
const token = await readToken(transport);
|
|
22061
|
+
if (token !== null) {
|
|
22062
|
+
let port = await readDaemonPort(transport);
|
|
22063
|
+
if (transport instanceof SshTransport) {
|
|
22064
|
+
tunnel = await openTunnel(transport, port, {
|
|
22065
|
+
ready: (localPort) => waitForHttp(`http://127.0.0.1:${localPort}/api/version`, 8e3)
|
|
22066
|
+
});
|
|
22067
|
+
port = tunnel.localPort;
|
|
22068
|
+
}
|
|
22069
|
+
const client = new DaemonClient(port, token);
|
|
22070
|
+
live = await client.liveSessions();
|
|
22071
|
+
profileNames = (await client.profiles()).map((p) => p.name);
|
|
22072
|
+
}
|
|
22073
|
+
} catch {
|
|
22074
|
+
}
|
|
22075
|
+
logger.info(`puddled v${installed} on ${transport.label}`);
|
|
22076
|
+
if (live !== null && live.length > 0) {
|
|
22077
|
+
logger.warn(`${live.length} running session(s) will be interrupted:`);
|
|
22078
|
+
for (const s of live) {
|
|
22079
|
+
logger.warn(` ${s.id.slice(0, 8)} ${s.status} ${s.title ?? "(untitled)"}`);
|
|
22080
|
+
}
|
|
22081
|
+
}
|
|
22082
|
+
if (profileNames !== null && profileNames.length > 0) {
|
|
22083
|
+
logger.info(`profiles on this daemon: ${profileNames.join(", ")}`);
|
|
22084
|
+
}
|
|
22085
|
+
const proceed = await confirm(
|
|
22086
|
+
`Stop puddled v${installed} on ${transport.label}, unregister its supervisor, and uninstall it?`,
|
|
22087
|
+
{ skip: cmd.yes }
|
|
22088
|
+
);
|
|
22089
|
+
if (!proceed) {
|
|
22090
|
+
logger.info("nothing removed");
|
|
22091
|
+
return 0;
|
|
22092
|
+
}
|
|
22093
|
+
let purge = cmd.purge;
|
|
22094
|
+
if (!purge && !cmd.yes) {
|
|
22095
|
+
purge = await confirm(
|
|
22096
|
+
`Also delete ~/.puddle on ${transport.label} \u2014 profiles, session history, agent credentials, and WORKTREES?`
|
|
22097
|
+
);
|
|
22098
|
+
}
|
|
22099
|
+
if (purge) {
|
|
22100
|
+
const dirty = await sweepDirtyWorktrees(transport);
|
|
22101
|
+
if (dirty.length > 0) {
|
|
22102
|
+
logger.warn("these worktrees carry uncommitted or unpushed work:");
|
|
22103
|
+
for (const path of dirty) logger.warn(` ${path}`);
|
|
22104
|
+
const anyway = cmd.yes ? true : await confirm("Delete them anyway?");
|
|
22105
|
+
if (!anyway) {
|
|
22106
|
+
purge = false;
|
|
22107
|
+
logger.info("keeping ~/.puddle \u2014 only the install is removed");
|
|
22108
|
+
}
|
|
22109
|
+
}
|
|
22110
|
+
}
|
|
22111
|
+
const record2 = readCockpitRecord(target);
|
|
22112
|
+
if (record2 !== null) {
|
|
22113
|
+
if (await checkCockpit(record2) === "dead") removeCockpitRecord(target);
|
|
22114
|
+
else {
|
|
22115
|
+
await terminateCockpit(record2);
|
|
22116
|
+
logger.info(`stopped the cockpit for ${target}`);
|
|
22117
|
+
}
|
|
22118
|
+
}
|
|
22119
|
+
await tunnel?.close();
|
|
22120
|
+
tunnel = null;
|
|
22121
|
+
await removeDaemon(transport, { purge, logger });
|
|
22122
|
+
logger.info(
|
|
22123
|
+
purge ? `puddled removed from ${transport.label}; ~/.puddle deleted` : `puddled removed from ${transport.label}; data kept \u2014 \`puddle launch\` reinstalls and resumes`
|
|
22124
|
+
);
|
|
22125
|
+
return 0;
|
|
22126
|
+
} finally {
|
|
22127
|
+
await tunnel?.close();
|
|
22128
|
+
transport.dispose();
|
|
22129
|
+
}
|
|
22130
|
+
}
|
|
22131
|
+
|
|
21458
22132
|
// src/cli/output.ts
|
|
21459
22133
|
function terminalLogger() {
|
|
21460
22134
|
return {
|
|
@@ -21484,8 +22158,8 @@ function timestampedLogger() {
|
|
|
21484
22158
|
// src/cli/run.ts
|
|
21485
22159
|
function assetsDir() {
|
|
21486
22160
|
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
21487
|
-
for (const candidate of [
|
|
21488
|
-
if (
|
|
22161
|
+
for (const candidate of [join10(here, "public"), join10(here, "..", "..", "dist", "public")]) {
|
|
22162
|
+
if (existsSync4(join10(candidate, "index.html"))) return candidate;
|
|
21489
22163
|
}
|
|
21490
22164
|
throw new CliError(
|
|
21491
22165
|
"not_installed",
|
|
@@ -21767,96 +22441,13 @@ async function run2(command) {
|
|
|
21767
22441
|
await target.close();
|
|
21768
22442
|
}
|
|
21769
22443
|
}
|
|
21770
|
-
case "
|
|
21771
|
-
|
|
21772
|
-
|
|
21773
|
-
|
|
21774
|
-
|
|
21775
|
-
|
|
21776
|
-
const result = await upgradeDaemon(transport, { logger });
|
|
21777
|
-
logger.info(
|
|
21778
|
-
`puddled ${result.from ?? "(unmanaged)"} \u2192 ${result.to} on ${transport.label}`
|
|
21779
|
-
);
|
|
21780
|
-
return 0;
|
|
21781
|
-
} finally {
|
|
21782
|
-
transport.dispose();
|
|
21783
|
-
}
|
|
21784
|
-
}
|
|
21785
|
-
if (!local) {
|
|
21786
|
-
throw new CliError(
|
|
21787
|
-
"bad_arguments",
|
|
21788
|
-
`puddle upgrade ${command.what} runs on the machine it upgrades`,
|
|
21789
|
-
`ssh into ${command.host} and run it there \u2014 user@host targets the daemon only`
|
|
21790
|
-
);
|
|
21791
|
-
}
|
|
21792
|
-
return command.what === "cli" ? upgradeCli(logger) : upgradeDesktop(logger);
|
|
21793
|
-
}
|
|
21794
|
-
}
|
|
21795
|
-
}
|
|
21796
|
-
function upgradeCli(logger) {
|
|
21797
|
-
logger.info(`puddle CLI ${cliVersion()} \u2014 asking npm for the latest release`);
|
|
21798
|
-
return new Promise((resolve3, reject) => {
|
|
21799
|
-
const child = spawn7("npm", ["install", "-g", "@puddle-code/cli@latest"], {
|
|
21800
|
-
stdio: "inherit"
|
|
21801
|
-
});
|
|
21802
|
-
child.on(
|
|
21803
|
-
"error",
|
|
21804
|
-
() => reject(
|
|
21805
|
-
new CliError(
|
|
21806
|
-
"not_installed",
|
|
21807
|
-
"npm is not on PATH",
|
|
21808
|
-
"the CLI is installed and upgraded via npm: npm install -g @puddle-code/cli@latest"
|
|
21809
|
-
)
|
|
21810
|
-
)
|
|
21811
|
-
);
|
|
21812
|
-
child.on("exit", (code) => {
|
|
21813
|
-
if (code === 0) {
|
|
21814
|
-
logger.info("done \u2014 `puddle --version` shows the installed version");
|
|
21815
|
-
resolve3(0);
|
|
21816
|
-
} else {
|
|
21817
|
-
reject(new CliError("not_installed", `npm install exited with ${code ?? "a signal"}`));
|
|
21818
|
-
}
|
|
21819
|
-
});
|
|
21820
|
-
});
|
|
21821
|
-
}
|
|
21822
|
-
async function upgradeDesktop(logger) {
|
|
21823
|
-
const installed = await findInstalledDesktopApp();
|
|
21824
|
-
const targetPath = installed?.appPath ?? await desktopAppInstallPath();
|
|
21825
|
-
if (targetPath === null) {
|
|
21826
|
-
throw new CliError(
|
|
21827
|
-
"not_installed",
|
|
21828
|
-
"no Puddle.app in /Applications or ~/Applications",
|
|
21829
|
-
process.platform === "linux" ? "AppImages carry no fixed path \u2014 update from the app itself (its banner knows $APPIMAGE)" : void 0
|
|
21830
|
-
);
|
|
21831
|
-
}
|
|
21832
|
-
if (installed !== null && await isDesktopAppRunning(installed.appPath)) {
|
|
21833
|
-
throw new CliError(
|
|
21834
|
-
"already_running",
|
|
21835
|
-
"Puddle is running \u2014 use its update banner, or quit it and rerun"
|
|
21836
|
-
);
|
|
21837
|
-
}
|
|
21838
|
-
const update = await checkForDesktopUpdate(installed?.version ?? "0.0.0");
|
|
21839
|
-
if (update === null) {
|
|
21840
|
-
if (installed === null) {
|
|
21841
|
-
throw new CliError(
|
|
21842
|
-
"not_installed",
|
|
21843
|
-
"no Puddle desktop release is available for this macOS architecture"
|
|
21844
|
-
);
|
|
21845
|
-
}
|
|
21846
|
-
logger.info(`Puddle ${installed.version} is already the latest release`);
|
|
21847
|
-
return 0;
|
|
22444
|
+
case "install":
|
|
22445
|
+
return runInstall(command, logger);
|
|
22446
|
+
case "upgrade":
|
|
22447
|
+
return runUpgrade(command, logger);
|
|
22448
|
+
case "remove":
|
|
22449
|
+
return runRemove(command, logger);
|
|
21848
22450
|
}
|
|
21849
|
-
if (installed === null) logger.info(`installing Puddle ${update.version}`);
|
|
21850
|
-
else logger.info(`Puddle ${installed.version} \u2192 ${update.version}`);
|
|
21851
|
-
const staged = await stageDesktopUpdate(update, { logger });
|
|
21852
|
-
await applyDesktopUpdate(staged, {
|
|
21853
|
-
targetPath,
|
|
21854
|
-
detach: false,
|
|
21855
|
-
relaunch: false,
|
|
21856
|
-
logger
|
|
21857
|
-
});
|
|
21858
|
-
logger.info(`Puddle ${update.version} installed at ${targetPath}`);
|
|
21859
|
-
return 0;
|
|
21860
22451
|
}
|
|
21861
22452
|
async function runCockpit(command, target, terminal) {
|
|
21862
22453
|
const detached = isCockpitChild();
|