dsh-home-hosted 0.2.2 → 0.3.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/README.md +105 -17
- package/docs/DESIGN.md +16 -0
- package/lib/client.js +6 -6
- package/lib/index.js +687 -185
- package/lib/index.js.map +4 -4
- package/lib/types/home-hosted/dsh-entry.d.ts +65 -2
- package/lib/types/home-hosted/launcher.d.ts +48 -0
- package/lib/types/home-hosted/panel.d.ts +12 -0
- package/lib/types/home-hosted/token.d.ts +21 -0
- package/lib/types/service.d.ts +29 -0
- package/lib/types/shared/contracts.d.ts +21 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -40,13 +40,14 @@ var DEFAULT_SETTINGS = {
|
|
|
40
40
|
agentTools: { enabled: true, allow: [...AGENT_TOOL_NAMES] },
|
|
41
41
|
panel: { port: null },
|
|
42
42
|
authNotice: true,
|
|
43
|
+
reclaimToken: true,
|
|
43
44
|
uiStyle: "detailed",
|
|
44
45
|
cli: { prefer: "pinned" }
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
// src/service.ts
|
|
48
49
|
import { Service } from "@deepseek-ai/cordis";
|
|
49
|
-
import
|
|
50
|
+
import fs13 from "node:fs";
|
|
50
51
|
import path13 from "node:path";
|
|
51
52
|
import process10 from "node:process";
|
|
52
53
|
import { fileURLToPath } from "node:url";
|
|
@@ -169,6 +170,12 @@ function writeJsonAtomic(file, value, mode) {
|
|
|
169
170
|
writeFileAtomic(file, `${JSON.stringify(value, null, 2)}
|
|
170
171
|
`, mode);
|
|
171
172
|
}
|
|
173
|
+
function removeFile(file) {
|
|
174
|
+
try {
|
|
175
|
+
fs.rmSync(file, { force: true });
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
172
179
|
|
|
173
180
|
// src/boot/common.ts
|
|
174
181
|
function errorMessage(error) {
|
|
@@ -2032,8 +2039,9 @@ function removeEntry(raw, id) {
|
|
|
2032
2039
|
}
|
|
2033
2040
|
|
|
2034
2041
|
// src/home-hosted/dsh-entry.ts
|
|
2035
|
-
import
|
|
2036
|
-
import
|
|
2042
|
+
import fs11 from "node:fs";
|
|
2043
|
+
import path10 from "node:path";
|
|
2044
|
+
import process7 from "node:process";
|
|
2037
2045
|
|
|
2038
2046
|
// src/home-hosted/launch.ts
|
|
2039
2047
|
import fs8 from "node:fs";
|
|
@@ -2101,143 +2109,11 @@ function buildHomeHostedBootSpec(facts, launch, options) {
|
|
|
2101
2109
|
};
|
|
2102
2110
|
}
|
|
2103
2111
|
|
|
2104
|
-
// src/home-hosted/dsh-entry.ts
|
|
2105
|
-
function detectProfile(argv, env = {}) {
|
|
2106
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
2107
|
-
const arg = argv[index];
|
|
2108
|
-
if (arg === "--profile" && typeof argv[index + 1] === "string" && !argv[index + 1].startsWith("-"))
|
|
2109
|
-
return argv[index + 1];
|
|
2110
|
-
if (arg?.startsWith("--profile="))
|
|
2111
|
-
return arg.slice("--profile=".length);
|
|
2112
|
-
}
|
|
2113
|
-
const dir = env.DSH_PROFILE_DIR;
|
|
2114
|
-
if (typeof dir === "string" && dir.length > 0) {
|
|
2115
|
-
const base = path8.basename(dir);
|
|
2116
|
-
if (base.length > 0 && base !== "." && base !== path8.sep)
|
|
2117
|
-
return base;
|
|
2118
|
-
}
|
|
2119
|
-
const app = argv.slice(2).find((candidate) => candidate !== void 0 && !candidate.startsWith("-"));
|
|
2120
|
-
if (app !== void 0 && app.length > 0)
|
|
2121
|
-
return app;
|
|
2122
|
-
const fromEnv = env.DSH_PROFILE;
|
|
2123
|
-
return typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : "web";
|
|
2124
|
-
}
|
|
2125
|
-
async function resolveDshLaunch() {
|
|
2126
|
-
const fromArgv = process5.argv[1];
|
|
2127
|
-
if (typeof fromArgv === "string" && /\.[cm]?js$/.test(fromArgv) && path8.isAbsolute(fromArgv))
|
|
2128
|
-
return { program: process5.execPath, args: [fromArgv], cliEntry: fromArgv, shimPath: null, source: "entry" };
|
|
2129
|
-
const found = await which("dsh");
|
|
2130
|
-
return found === null ? null : resolveShimmedCli(found, process5.execPath);
|
|
2131
|
-
}
|
|
2132
|
-
function buildDshEntry(facts) {
|
|
2133
|
-
const launch = facts.launch;
|
|
2134
|
-
const entryArgs = launch?.args ?? [];
|
|
2135
|
-
const profile = facts.profile === null || facts.profile === void 0 ? "web" : facts.profile;
|
|
2136
|
-
const appArgs = [];
|
|
2137
|
-
if (facts.port !== null) {
|
|
2138
|
-
appArgs.push(
|
|
2139
|
-
"--port",
|
|
2140
|
-
"{port}",
|
|
2141
|
-
"--host",
|
|
2142
|
-
"{host}",
|
|
2143
|
-
"--no-open",
|
|
2144
|
-
"--trusted-host",
|
|
2145
|
-
`localhost:{port}`
|
|
2146
|
-
);
|
|
2147
|
-
if (facts.host === "0.0.0.0")
|
|
2148
|
-
appArgs.push("--trusted-host", "{lanIp}:{port}");
|
|
2149
|
-
}
|
|
2150
|
-
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2151
|
-
const args = [...entryArgs, ...launcherArgs, ...appArgs];
|
|
2152
|
-
return {
|
|
2153
|
-
id: facts.id,
|
|
2154
|
-
label: "DSH web",
|
|
2155
|
-
enabled: true,
|
|
2156
|
-
autostart: true,
|
|
2157
|
-
command: launch?.program ?? "dsh",
|
|
2158
|
-
args,
|
|
2159
|
-
bind: facts.host === "0.0.0.0" ? "lan" : "local",
|
|
2160
|
-
port: facts.port,
|
|
2161
|
-
cwd: process5.cwd(),
|
|
2162
|
-
env: {},
|
|
2163
|
-
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2164
|
-
onPortConflict: "kill",
|
|
2165
|
-
stop: { killPortHolders: true },
|
|
2166
|
-
health: {
|
|
2167
|
-
enabled: true,
|
|
2168
|
-
mode: "http",
|
|
2169
|
-
// 401 is normal here: the panel answers the login route when auth is on.
|
|
2170
|
-
http: { path: "/", method: "GET", expectStatusBelow: 500, expectBody: "" }
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// src/home-hosted/entries.ts
|
|
2176
|
-
function defaultOnPortConflict(platform = process.platform) {
|
|
2177
|
-
return platform === "win32" ? "kill" : "follow";
|
|
2178
|
-
}
|
|
2179
|
-
function defaultIntent(id, platform = process.platform) {
|
|
2180
|
-
return {
|
|
2181
|
-
id,
|
|
2182
|
-
autostart: true,
|
|
2183
|
-
onPortConflict: defaultOnPortConflict(platform),
|
|
2184
|
-
stopKillPortHolders: true,
|
|
2185
|
-
persistent: true
|
|
2186
|
-
};
|
|
2187
|
-
}
|
|
2188
|
-
function ownedPatch(intent, live, options = {}) {
|
|
2189
|
-
const stop = live?.stop ?? {};
|
|
2190
|
-
return {
|
|
2191
|
-
autostart: intent.autostart,
|
|
2192
|
-
onPortConflict: intent.onPortConflict,
|
|
2193
|
-
...options.persistent === false ? {} : { persistent: intent.persistent },
|
|
2194
|
-
stop: { ...stop, killPortHolders: intent.stopKillPortHolders }
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
function ownedDrift(live, intent, options = {}) {
|
|
2198
|
-
if (live === null)
|
|
2199
|
-
return ["missing entry"];
|
|
2200
|
-
const drift = [];
|
|
2201
|
-
if (live.autostart !== intent.autostart)
|
|
2202
|
-
drift.push("autostart");
|
|
2203
|
-
if (live.onPortConflict !== intent.onPortConflict)
|
|
2204
|
-
drift.push("onPortConflict");
|
|
2205
|
-
if (options.persistent !== false && (live.persistent ?? false) !== intent.persistent)
|
|
2206
|
-
drift.push("persistent");
|
|
2207
|
-
const killPortHolders = live.stop?.killPortHolders;
|
|
2208
|
-
if (killPortHolders !== intent.stopKillPortHolders)
|
|
2209
|
-
drift.push("stop.killPortHolders");
|
|
2210
|
-
return drift;
|
|
2211
|
-
}
|
|
2212
|
-
function snapshotOwned(live) {
|
|
2213
|
-
const killPortHolders = live.stop?.killPortHolders;
|
|
2214
|
-
return {
|
|
2215
|
-
id: live.id,
|
|
2216
|
-
autostart: typeof live.autostart === "boolean" ? live.autostart : false,
|
|
2217
|
-
onPortConflict: isOnPortConflict(live.onPortConflict) ? live.onPortConflict : "block",
|
|
2218
|
-
persistent: live.persistent === true,
|
|
2219
|
-
stop: { killPortHolders: typeof killPortHolders === "boolean" ? killPortHolders : false }
|
|
2220
|
-
};
|
|
2221
|
-
}
|
|
2222
|
-
function restorePatch(live, snapshot) {
|
|
2223
|
-
const stop = live.stop ?? {};
|
|
2224
|
-
const previous = snapshot?.stop ?? {};
|
|
2225
|
-
return {
|
|
2226
|
-
autostart: snapshot?.autostart ?? false,
|
|
2227
|
-
onPortConflict: snapshot?.onPortConflict ?? "block",
|
|
2228
|
-
persistent: snapshot?.persistent === true,
|
|
2229
|
-
stop: {
|
|
2230
|
-
...stop,
|
|
2231
|
-
killPortHolders: typeof previous.killPortHolders === "boolean" ? previous.killPortHolders : false
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
2112
|
// src/home-hosted/resolve.ts
|
|
2237
2113
|
import { createRequire } from "node:module";
|
|
2238
2114
|
import fs9 from "node:fs";
|
|
2239
|
-
import
|
|
2240
|
-
import
|
|
2115
|
+
import path8 from "node:path";
|
|
2116
|
+
import process5 from "node:process";
|
|
2241
2117
|
var EXPECTED_RANGE = "^0.6.4";
|
|
2242
2118
|
var MIN_SUPPORTED_VERSION = "0.4.1";
|
|
2243
2119
|
var MIN_KILL_VERSION = "0.6.0";
|
|
@@ -2253,7 +2129,7 @@ function binEntryFromManifest(manifestPath) {
|
|
|
2253
2129
|
try {
|
|
2254
2130
|
const manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
|
|
2255
2131
|
const declared = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["home-hosted"] ?? Object.values(manifest.bin ?? {})[0];
|
|
2256
|
-
return typeof declared === "string" && declared.length > 0 ?
|
|
2132
|
+
return typeof declared === "string" && declared.length > 0 ? path8.resolve(path8.dirname(manifestPath), declared) : null;
|
|
2257
2133
|
} catch {
|
|
2258
2134
|
return null;
|
|
2259
2135
|
}
|
|
@@ -2309,7 +2185,7 @@ async function resolveCli(options = {}) {
|
|
|
2309
2185
|
const override = options.override?.trim();
|
|
2310
2186
|
let unusableOverride = null;
|
|
2311
2187
|
if (override !== void 0 && override.length > 0) {
|
|
2312
|
-
const launch = resolveShimmedCli(
|
|
2188
|
+
const launch = resolveShimmedCli(path8.resolve(override));
|
|
2313
2189
|
if (launch === null) {
|
|
2314
2190
|
unusableOverride = override;
|
|
2315
2191
|
} else {
|
|
@@ -2343,13 +2219,13 @@ async function resolveCli(options = {}) {
|
|
|
2343
2219
|
}
|
|
2344
2220
|
dependency = {
|
|
2345
2221
|
candidate: { source: "dependency", path: dependencyEntry, version: version2 },
|
|
2346
|
-
launch: { program:
|
|
2222
|
+
launch: { program: process5.execPath, args: [dependencyEntry], cliEntry: dependencyEntry, shimPath: null, source: "entry" }
|
|
2347
2223
|
};
|
|
2348
2224
|
}
|
|
2349
2225
|
let global = null;
|
|
2350
2226
|
const found = await locate("home-hosted") ?? await locate("hh");
|
|
2351
2227
|
if (found !== null) {
|
|
2352
|
-
const launch = resolveShimmedCli(
|
|
2228
|
+
const launch = resolveShimmedCli(path8.resolve(found));
|
|
2353
2229
|
if (launch !== null) {
|
|
2354
2230
|
global = {
|
|
2355
2231
|
candidate: { source: "path", path: launch.cliEntry ?? launch.shimPath ?? launch.program, version: await readVersion(launch) },
|
|
@@ -2413,16 +2289,16 @@ async function resolveCli(options = {}) {
|
|
|
2413
2289
|
|
|
2414
2290
|
// src/home-hosted/launcher.ts
|
|
2415
2291
|
import fs10 from "node:fs";
|
|
2416
|
-
import
|
|
2417
|
-
import
|
|
2292
|
+
import path9 from "node:path";
|
|
2293
|
+
import process6 from "node:process";
|
|
2418
2294
|
function launcherDir(stateDir) {
|
|
2419
|
-
return
|
|
2295
|
+
return path9.join(stateDir, "bin");
|
|
2420
2296
|
}
|
|
2421
2297
|
function launcherPath(stateDir) {
|
|
2422
|
-
return
|
|
2298
|
+
return path9.join(launcherDir(stateDir), "home-hosted.mjs");
|
|
2423
2299
|
}
|
|
2424
2300
|
function launcherRecordPath(stateDir) {
|
|
2425
|
-
return
|
|
2301
|
+
return path9.join(launcherDir(stateDir), "resolved.json");
|
|
2426
2302
|
}
|
|
2427
2303
|
function buildLauncherSource(options) {
|
|
2428
2304
|
const marker = options.marker ?? "managed by dsh-home-hosted";
|
|
@@ -2547,13 +2423,444 @@ async function preflightLauncher(stateDir, timeoutMs = 1e4) {
|
|
|
2547
2423
|
const file = launcherPath(stateDir);
|
|
2548
2424
|
if (!fs10.existsSync(file))
|
|
2549
2425
|
return null;
|
|
2550
|
-
const result =
|
|
2426
|
+
const result = process6.platform === "win32" ? await run(process6.execPath, [file, "--version"], { timeoutMs }) : await run(file, ["--version"], { timeoutMs });
|
|
2551
2427
|
return parseVersion(result.stdout) ?? parseVersion(result.stderr);
|
|
2552
2428
|
}
|
|
2429
|
+
function dshLauncherRecordPath(stateDir) {
|
|
2430
|
+
return path9.join(launcherDir(stateDir), "dsh-resolved.json");
|
|
2431
|
+
}
|
|
2432
|
+
function dshLauncherPath(stateDir, entryExtension) {
|
|
2433
|
+
const extension = (entryExtension ?? "").toLowerCase() === ".cjs" ? ".cjs" : ".mjs";
|
|
2434
|
+
return path9.join(launcherDir(stateDir), `dsh${extension}`);
|
|
2435
|
+
}
|
|
2436
|
+
function readDshLauncherRecord(stateDir) {
|
|
2437
|
+
return readJson(dshLauncherRecordPath(stateDir));
|
|
2438
|
+
}
|
|
2439
|
+
function buildDshLauncherSource(options) {
|
|
2440
|
+
const esm = (options.entryExtension ?? "").toLowerCase() !== ".cjs";
|
|
2441
|
+
const prelude = esm ? `import fs from 'node:fs'
|
|
2442
|
+
import path from 'node:path'
|
|
2443
|
+
import process from 'node:process'
|
|
2444
|
+
import { spawnSync } from 'node:child_process'` : `const fs = require('node:fs')
|
|
2445
|
+
const path = require('node:path')
|
|
2446
|
+
const process = require('node:process')
|
|
2447
|
+
const { spawnSync } = require('node:child_process')`;
|
|
2448
|
+
const roots = [options.resolvedEntry === null ? null : dshSearchRoot(options.resolvedEntry), ...options.searchRoots ?? []].filter((root) => typeof root === "string" && root.length > 0);
|
|
2449
|
+
return `#!/usr/bin/env node
|
|
2450
|
+
// managed by dsh-home-hosted \u2014 rewritten on every plugin start; do not edit.
|
|
2451
|
+
${prelude}
|
|
2452
|
+
|
|
2453
|
+
const DSH_HOME = ${JSON.stringify(options.dshHome)}
|
|
2454
|
+
const RECORD = ${JSON.stringify(dshLauncherRecordPath(options.stateDir))}
|
|
2455
|
+
const SEARCH_ROOTS = ${JSON.stringify([...new Set(roots)])}
|
|
2456
|
+
|
|
2457
|
+
function readRecord() {
|
|
2458
|
+
try { return JSON.parse(fs.readFileSync(RECORD, 'utf8')) } catch { return null }
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function isEntry(file) {
|
|
2462
|
+
let stat = null
|
|
2463
|
+
try { stat = fs.statSync(file) } catch { return false }
|
|
2464
|
+
return stat.isFile() && /\\.(?:mjs|cjs|js)$/.test(file)
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function collect(out, entry) {
|
|
2468
|
+
if (typeof entry === 'string' && isEntry(entry) && !out.includes(entry)) out.push(entry)
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function collectStore(root, out) {
|
|
2472
|
+
const store = path.join(root, 'node_modules', '.pnpm')
|
|
2473
|
+
let names = []
|
|
2474
|
+
try { names = fs.readdirSync(store) } catch { return }
|
|
2475
|
+
for (const name of names) {
|
|
2476
|
+
if (!name.startsWith('dsh@') && !name.startsWith('@deepseek-ai+dsh@')) continue
|
|
2477
|
+
collect(out, path.join(store, name, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'))
|
|
2478
|
+
collect(out, path.join(store, name, 'node_modules', 'dsh', 'bin', 'dsh.js'))
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
function collectRoot(root, out) {
|
|
2483
|
+
collect(out, path.join(root, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'))
|
|
2484
|
+
collect(out, path.join(root, 'node_modules', 'dsh', 'bin', 'dsh.js'))
|
|
2485
|
+
collectStore(root, out)
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function candidates() {
|
|
2489
|
+
const out = []
|
|
2490
|
+
const record = readRecord()
|
|
2491
|
+
if (record && typeof record.entry === 'string') collect(out, record.entry)
|
|
2492
|
+
for (const root of [...(Array.isArray(record?.roots) ? record.roots : []), ...SEARCH_ROOTS]) collectRoot(root, out)
|
|
2493
|
+
collectRoot(DSH_HOME, out)
|
|
2494
|
+
let profiles = []
|
|
2495
|
+
try { profiles = fs.readdirSync(path.join(DSH_HOME, 'profiles')) } catch {}
|
|
2496
|
+
for (const name of profiles) collectRoot(path.join(DSH_HOME, 'profiles', name), out)
|
|
2497
|
+
// Last resort: a dsh on the entry's own PATH.
|
|
2498
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
2499
|
+
collect(out, path.join(dir, 'dsh'))
|
|
2500
|
+
collect(out, path.join(dir, 'dsh.cmd'))
|
|
2501
|
+
}
|
|
2502
|
+
return out
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function versionOf(entry) {
|
|
2506
|
+
let dir = path.dirname(entry)
|
|
2507
|
+
for (let hop = 0; hop < 4; hop += 1) {
|
|
2508
|
+
try {
|
|
2509
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
2510
|
+
if (typeof manifest.version === 'string') return manifest.version
|
|
2511
|
+
} catch {}
|
|
2512
|
+
dir = path.dirname(dir)
|
|
2513
|
+
}
|
|
2514
|
+
return null
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function compare(a, b) {
|
|
2518
|
+
const split = value => {
|
|
2519
|
+
const [core, pre = null] = String(value).split('-', 2)
|
|
2520
|
+
return { parts: core.split('.').map(part => Number.parseInt(part, 10) || 0), pre }
|
|
2521
|
+
}
|
|
2522
|
+
const left = split(a); const right = split(b)
|
|
2523
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2524
|
+
const diff = (left.parts[i] ?? 0) - (right.parts[i] ?? 0)
|
|
2525
|
+
if (diff !== 0) return diff < 0 ? -1 : 1
|
|
2526
|
+
}
|
|
2527
|
+
if (left.pre === right.pre) return 0
|
|
2528
|
+
if (left.pre === null) return 1
|
|
2529
|
+
if (right.pre === null) return -1
|
|
2530
|
+
return left.pre < right.pre ? -1 : 1
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// A recorded path that still exists is usually the copy to boot, but never the
|
|
2534
|
+
// older one: a rebuild or upgrade that landed elsewhere must win on version.
|
|
2535
|
+
function choose() {
|
|
2536
|
+
const recorded = readRecord()?.entry ?? null
|
|
2537
|
+
const found = candidates().map(entry => ({ entry, recorded: entry === recorded, version: versionOf(entry) }))
|
|
2538
|
+
const known = found.filter(item => item.version !== null)
|
|
2539
|
+
if (known.length > 0) {
|
|
2540
|
+
known.sort((a, b) => (compare(b.version, a.version) || (Number(b.recorded) - Number(a.recorded))))
|
|
2541
|
+
return known[0].entry
|
|
2542
|
+
}
|
|
2543
|
+
return (found.find(item => item.recorded) ?? found[0])?.entry ?? null
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
const entry = choose()
|
|
2547
|
+
if (entry === null) {
|
|
2548
|
+
console.error('[dsh-home-hosted] no dsh entry found for ' + DSH_HOME + ' (recorded path, $DSH_HOME node_modules, profiles/*, PATH). Reinstall dsh, or restart: dsh web')
|
|
2549
|
+
process.exit(1)
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
const args = process.argv.slice(2)
|
|
2553
|
+
const result = /\\.(mjs|cjs|js)$/.test(entry)
|
|
2554
|
+
? spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit' })
|
|
2555
|
+
: spawnSync(entry, args, { stdio: 'inherit', shell: process.platform === 'win32' && /\\.(cmd|bat)$/i.test(entry) })
|
|
2556
|
+
process.exit(typeof result.status === 'number' ? result.status : 1)
|
|
2557
|
+
`;
|
|
2558
|
+
}
|
|
2559
|
+
function writeDshLauncher(options) {
|
|
2560
|
+
const file = dshLauncherPath(options.stateDir, options.entryExtension);
|
|
2561
|
+
const source = buildDshLauncherSource(options);
|
|
2562
|
+
const previous = fs10.existsSync(file) ? fs10.readFileSync(file, "utf8") : null;
|
|
2563
|
+
const changed = previous !== source;
|
|
2564
|
+
if (changed) {
|
|
2565
|
+
writeFileAtomic(file, source, 493);
|
|
2566
|
+
fs10.chmodSync(file, 493);
|
|
2567
|
+
}
|
|
2568
|
+
const raw = [dshSearchRoot(options.resolvedEntry), ...options.searchRoots ?? [], ...readDshLauncherRecord(options.stateDir)?.roots ?? []];
|
|
2569
|
+
const known = raw.filter((root) => typeof root === "string" && root.length > 0 && dshRootUsable(root));
|
|
2570
|
+
const roots = [...new Set(known)];
|
|
2571
|
+
writeJsonAtomic(dshLauncherRecordPath(options.stateDir), {
|
|
2572
|
+
entry: options.resolvedEntry,
|
|
2573
|
+
roots,
|
|
2574
|
+
writtenAt: Date.now()
|
|
2575
|
+
}, 384);
|
|
2576
|
+
return { path: file, changed };
|
|
2577
|
+
}
|
|
2578
|
+
function dshCandidatesUnder(root) {
|
|
2579
|
+
const out = [];
|
|
2580
|
+
for (const entry of [
|
|
2581
|
+
path9.join(root, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
2582
|
+
path9.join(root, "node_modules", "dsh", "bin", "dsh.js")
|
|
2583
|
+
]) {
|
|
2584
|
+
if (fs10.existsSync(entry))
|
|
2585
|
+
out.push(entry);
|
|
2586
|
+
}
|
|
2587
|
+
const store = path9.join(root, "node_modules", ".pnpm");
|
|
2588
|
+
if (fs10.existsSync(store)) {
|
|
2589
|
+
for (const name2 of fs10.readdirSync(store)) {
|
|
2590
|
+
if (!name2.startsWith("dsh@") && !name2.startsWith("@deepseek-ai+dsh@"))
|
|
2591
|
+
continue;
|
|
2592
|
+
for (const entry of [
|
|
2593
|
+
path9.join(store, name2, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
2594
|
+
path9.join(store, name2, "node_modules", "dsh", "bin", "dsh.js")
|
|
2595
|
+
]) {
|
|
2596
|
+
if (fs10.existsSync(entry))
|
|
2597
|
+
out.push(entry);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return out;
|
|
2602
|
+
}
|
|
2603
|
+
function dshSearchRoot(entry) {
|
|
2604
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
2605
|
+
return null;
|
|
2606
|
+
const parts = entry.split(path9.sep);
|
|
2607
|
+
const store = parts.indexOf(".pnpm");
|
|
2608
|
+
const modules = store > 0 ? store - 1 : parts.lastIndexOf("node_modules");
|
|
2609
|
+
return modules > 0 ? parts.slice(0, modules).join(path9.sep) : null;
|
|
2610
|
+
}
|
|
2611
|
+
function dshRootUsable(root) {
|
|
2612
|
+
return dshCandidatesUnder(root).length > 0 || fs10.existsSync(path9.join(root, "node_modules", ".pnpm"));
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// src/home-hosted/dsh-entry.ts
|
|
2616
|
+
function detectProfile(argv, env = {}) {
|
|
2617
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2618
|
+
const arg = argv[index];
|
|
2619
|
+
if (arg === "--profile" && typeof argv[index + 1] === "string" && !argv[index + 1].startsWith("-"))
|
|
2620
|
+
return argv[index + 1];
|
|
2621
|
+
if (arg?.startsWith("--profile="))
|
|
2622
|
+
return arg.slice("--profile=".length);
|
|
2623
|
+
}
|
|
2624
|
+
const dir = env.DSH_PROFILE_DIR;
|
|
2625
|
+
if (typeof dir === "string" && dir.length > 0) {
|
|
2626
|
+
const base = path10.basename(dir);
|
|
2627
|
+
if (base.length > 0 && base !== "." && base !== path10.sep)
|
|
2628
|
+
return base;
|
|
2629
|
+
}
|
|
2630
|
+
const app = argv.slice(2).find((candidate) => candidate !== void 0 && !candidate.startsWith("-"));
|
|
2631
|
+
if (app !== void 0 && app.length > 0)
|
|
2632
|
+
return app;
|
|
2633
|
+
const fromEnv = env.DSH_PROFILE;
|
|
2634
|
+
return typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : "web";
|
|
2635
|
+
}
|
|
2636
|
+
function versionOf(entry) {
|
|
2637
|
+
let dir = path10.dirname(entry);
|
|
2638
|
+
for (let hop = 0; hop < 4; hop += 1) {
|
|
2639
|
+
const manifest = readJson(path10.join(dir, "package.json"));
|
|
2640
|
+
if (typeof manifest?.version === "string")
|
|
2641
|
+
return manifest.version;
|
|
2642
|
+
dir = path10.dirname(dir);
|
|
2643
|
+
}
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
function isEntry(file) {
|
|
2647
|
+
return /\.[cm]?js$/.test(file) && fs11.existsSync(file) && !fs11.statSync(file).isDirectory();
|
|
2648
|
+
}
|
|
2649
|
+
function realPath(file) {
|
|
2650
|
+
try {
|
|
2651
|
+
return fs11.realpathSync(file);
|
|
2652
|
+
} catch {
|
|
2653
|
+
return file;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
function cloneEntryFrom(argv1) {
|
|
2657
|
+
if (!isEntry(argv1))
|
|
2658
|
+
return null;
|
|
2659
|
+
const root = path10.dirname(path10.dirname(argv1));
|
|
2660
|
+
for (const candidate of [
|
|
2661
|
+
path10.join(root, "lib", "bin.js"),
|
|
2662
|
+
path10.join(root, "lib", "bin.mjs"),
|
|
2663
|
+
path10.join(root, "lib", "bin.cjs"),
|
|
2664
|
+
path10.join(root, "dist", "bin.js"),
|
|
2665
|
+
path10.join(root, "bin", "dsh.js"),
|
|
2666
|
+
path10.join(root, "bin", "dsh.mjs")
|
|
2667
|
+
]) {
|
|
2668
|
+
if (isEntry(candidate))
|
|
2669
|
+
return candidate;
|
|
2670
|
+
}
|
|
2671
|
+
return argv1;
|
|
2672
|
+
}
|
|
2673
|
+
function launchedEntry(argv1) {
|
|
2674
|
+
const real = realPath(argv1);
|
|
2675
|
+
const direct = cloneEntryFrom(real);
|
|
2676
|
+
if (direct !== null)
|
|
2677
|
+
return direct;
|
|
2678
|
+
const shimmed = resolveShimmedCli(real, process7.execPath);
|
|
2679
|
+
return shimmed?.cliEntry == null ? null : cloneEntryFrom(shimmed.cliEntry);
|
|
2680
|
+
}
|
|
2681
|
+
function findDshEntry(dshHome2) {
|
|
2682
|
+
const found = [];
|
|
2683
|
+
found.push(...dshCandidatesUnder(dshHome2));
|
|
2684
|
+
for (const name2 of readdirNames(path10.join(dshHome2, "profiles")))
|
|
2685
|
+
found.push(...dshCandidatesUnder(path10.join(dshHome2, "profiles", name2)));
|
|
2686
|
+
const withVersion = found.map((entry) => ({ entry, version: versionOf(entry) }));
|
|
2687
|
+
withVersion.sort((a, b) => compareVersions(b.version ?? "0.0.0", a.version ?? "0.0.0"));
|
|
2688
|
+
return withVersion[0]?.entry ?? null;
|
|
2689
|
+
}
|
|
2690
|
+
function readdirNames(dir) {
|
|
2691
|
+
try {
|
|
2692
|
+
return fs11.readdirSync(dir);
|
|
2693
|
+
} catch {
|
|
2694
|
+
return [];
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
async function resolveDshLaunch(options = {}) {
|
|
2698
|
+
const argv1 = options.argv1 === void 0 ? process7.argv[1] : options.argv1;
|
|
2699
|
+
const dshHome2 = options.dshHome ?? dshHome();
|
|
2700
|
+
const stateDir = options.stateDir ?? pluginStateDir();
|
|
2701
|
+
const local = typeof argv1 === "string" && argv1.length > 0 ? launchedEntry(argv1) : null;
|
|
2702
|
+
if (local !== null && realPath(local) !== realPath(dshLauncherPath(stateDir, path10.extname(local)))) {
|
|
2703
|
+
const launcher = writeDshLauncher({
|
|
2704
|
+
stateDir,
|
|
2705
|
+
dshHome: dshHome2,
|
|
2706
|
+
resolvedEntry: local,
|
|
2707
|
+
entryExtension: path10.extname(local),
|
|
2708
|
+
// Where this install sits, so a rebuilt/moved copy is found next boot.
|
|
2709
|
+
searchRoots: [dshSearchRoot(local)].filter((root) => root !== null)
|
|
2710
|
+
});
|
|
2711
|
+
return { program: process7.execPath, args: [local], cliEntry: local, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2712
|
+
}
|
|
2713
|
+
const installed = findDshEntry(dshHome2);
|
|
2714
|
+
if (installed !== null) {
|
|
2715
|
+
const launcher = writeDshLauncher({ stateDir, dshHome: dshHome2, resolvedEntry: installed, entryExtension: path10.extname(installed) });
|
|
2716
|
+
return { program: process7.execPath, args: [installed], cliEntry: installed, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2717
|
+
}
|
|
2718
|
+
const found = await (options.findOnPath ?? which)("dsh");
|
|
2719
|
+
if (found === null)
|
|
2720
|
+
return null;
|
|
2721
|
+
const launch = resolveShimmedCli(found, process7.execPath);
|
|
2722
|
+
return launch === null ? null : { ...launch, launcherPath: null };
|
|
2723
|
+
}
|
|
2724
|
+
function buildCommand(launch, launcherPath2) {
|
|
2725
|
+
if (launcherPath2 !== null && launcherPath2.length > 0)
|
|
2726
|
+
return { command: process7.execPath, entryArgs: [launcherPath2] };
|
|
2727
|
+
return { command: launch.program, entryArgs: launch.args };
|
|
2728
|
+
}
|
|
2729
|
+
function entryScript(config) {
|
|
2730
|
+
const program = config.command;
|
|
2731
|
+
if (typeof program !== "string" || program.length === 0)
|
|
2732
|
+
return null;
|
|
2733
|
+
if (!path10.isAbsolute(program))
|
|
2734
|
+
return program;
|
|
2735
|
+
const rest = config.args ?? [];
|
|
2736
|
+
return rest.find((arg) => path10.isAbsolute(arg)) ?? program;
|
|
2737
|
+
}
|
|
2738
|
+
function needsLauncherRepair(config) {
|
|
2739
|
+
const program = config.command;
|
|
2740
|
+
if (typeof program !== "string" || program.length === 0)
|
|
2741
|
+
return false;
|
|
2742
|
+
if (path10.isAbsolute(program))
|
|
2743
|
+
return true;
|
|
2744
|
+
return (config.args ?? []).some((arg, index) => index === 0 && path10.isAbsolute(arg));
|
|
2745
|
+
}
|
|
2746
|
+
function launcherRepair(config, launch) {
|
|
2747
|
+
if (launch?.launcherPath == null || launch.launcherPath.length === 0)
|
|
2748
|
+
return null;
|
|
2749
|
+
const launcher = launch.launcherPath;
|
|
2750
|
+
const stored = entryScript(config);
|
|
2751
|
+
if (stored === null || stored === launcher)
|
|
2752
|
+
return null;
|
|
2753
|
+
if (stored !== launch.cliEntry)
|
|
2754
|
+
return null;
|
|
2755
|
+
const rest = (config.args ?? []).filter((arg, index) => !(index === 0 && path10.isAbsolute(arg)));
|
|
2756
|
+
return { command: process7.execPath, args: [launcher, ...rest] };
|
|
2757
|
+
}
|
|
2758
|
+
function buildDshEntry(facts) {
|
|
2759
|
+
const launch = facts.launch;
|
|
2760
|
+
const profile = facts.profile === null || facts.profile === void 0 ? "web" : facts.profile;
|
|
2761
|
+
const appArgs = [];
|
|
2762
|
+
if (facts.port !== null) {
|
|
2763
|
+
appArgs.push(
|
|
2764
|
+
"--port",
|
|
2765
|
+
"{port}",
|
|
2766
|
+
"--host",
|
|
2767
|
+
"{host}",
|
|
2768
|
+
"--no-open",
|
|
2769
|
+
"--trusted-host",
|
|
2770
|
+
`localhost:{port}`
|
|
2771
|
+
);
|
|
2772
|
+
if (facts.host === "0.0.0.0")
|
|
2773
|
+
appArgs.push("--trusted-host", "{lanIp}:{port}");
|
|
2774
|
+
}
|
|
2775
|
+
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2776
|
+
const generated = launch === null ? { command: "dsh", entryArgs: [] } : buildCommand(launch, facts.launcherPath ?? null);
|
|
2777
|
+
return {
|
|
2778
|
+
id: facts.id,
|
|
2779
|
+
label: "DSH web",
|
|
2780
|
+
enabled: true,
|
|
2781
|
+
autostart: true,
|
|
2782
|
+
command: generated.command,
|
|
2783
|
+
args: [...generated.entryArgs, ...launcherArgs, ...appArgs],
|
|
2784
|
+
bind: facts.host === "0.0.0.0" ? "lan" : "local",
|
|
2785
|
+
port: facts.port,
|
|
2786
|
+
cwd: process7.cwd(),
|
|
2787
|
+
env: {},
|
|
2788
|
+
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2789
|
+
onPortConflict: "kill",
|
|
2790
|
+
stop: { killPortHolders: true },
|
|
2791
|
+
health: {
|
|
2792
|
+
enabled: true,
|
|
2793
|
+
mode: "http",
|
|
2794
|
+
// 401 is normal here: the panel answers the login route when auth is on.
|
|
2795
|
+
http: { path: "/", method: "GET", expectStatusBelow: 500, expectBody: "" }
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/home-hosted/entries.ts
|
|
2801
|
+
function defaultOnPortConflict(platform = process.platform) {
|
|
2802
|
+
return platform === "win32" ? "kill" : "follow";
|
|
2803
|
+
}
|
|
2804
|
+
function defaultIntent(id, platform = process.platform) {
|
|
2805
|
+
return {
|
|
2806
|
+
id,
|
|
2807
|
+
autostart: true,
|
|
2808
|
+
onPortConflict: defaultOnPortConflict(platform),
|
|
2809
|
+
stopKillPortHolders: true,
|
|
2810
|
+
persistent: true
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function ownedPatch(intent, live, options = {}) {
|
|
2814
|
+
const stop = live?.stop ?? {};
|
|
2815
|
+
return {
|
|
2816
|
+
autostart: intent.autostart,
|
|
2817
|
+
onPortConflict: intent.onPortConflict,
|
|
2818
|
+
...options.persistent === false ? {} : { persistent: intent.persistent },
|
|
2819
|
+
stop: { ...stop, killPortHolders: intent.stopKillPortHolders }
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
function ownedDrift(live, intent, options = {}) {
|
|
2823
|
+
if (live === null)
|
|
2824
|
+
return ["missing entry"];
|
|
2825
|
+
const drift = [];
|
|
2826
|
+
if (live.autostart !== intent.autostart)
|
|
2827
|
+
drift.push("autostart");
|
|
2828
|
+
if (live.onPortConflict !== intent.onPortConflict)
|
|
2829
|
+
drift.push("onPortConflict");
|
|
2830
|
+
if (options.persistent !== false && (live.persistent ?? false) !== intent.persistent)
|
|
2831
|
+
drift.push("persistent");
|
|
2832
|
+
const killPortHolders = live.stop?.killPortHolders;
|
|
2833
|
+
if (killPortHolders !== intent.stopKillPortHolders)
|
|
2834
|
+
drift.push("stop.killPortHolders");
|
|
2835
|
+
return drift;
|
|
2836
|
+
}
|
|
2837
|
+
function snapshotOwned(live) {
|
|
2838
|
+
const killPortHolders = live.stop?.killPortHolders;
|
|
2839
|
+
return {
|
|
2840
|
+
id: live.id,
|
|
2841
|
+
autostart: typeof live.autostart === "boolean" ? live.autostart : false,
|
|
2842
|
+
onPortConflict: isOnPortConflict(live.onPortConflict) ? live.onPortConflict : "block",
|
|
2843
|
+
persistent: live.persistent === true,
|
|
2844
|
+
stop: { killPortHolders: typeof killPortHolders === "boolean" ? killPortHolders : false }
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
function restorePatch(live, snapshot) {
|
|
2848
|
+
const stop = live.stop ?? {};
|
|
2849
|
+
const previous = snapshot?.stop ?? {};
|
|
2850
|
+
return {
|
|
2851
|
+
autostart: snapshot?.autostart ?? false,
|
|
2852
|
+
onPortConflict: snapshot?.onPortConflict ?? "block",
|
|
2853
|
+
persistent: snapshot?.persistent === true,
|
|
2854
|
+
stop: {
|
|
2855
|
+
...stop,
|
|
2856
|
+
killPortHolders: typeof previous.killPortHolders === "boolean" ? previous.killPortHolders : false
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2553
2860
|
|
|
2554
2861
|
// src/home-hosted/panel-control.ts
|
|
2555
2862
|
import { spawn as spawn2 } from "node:child_process";
|
|
2556
|
-
import
|
|
2863
|
+
import fs12 from "node:fs";
|
|
2557
2864
|
import path11 from "node:path";
|
|
2558
2865
|
import process8 from "node:process";
|
|
2559
2866
|
function cliArgs(deps, command) {
|
|
@@ -2642,7 +2949,7 @@ log('started with exit ' + String(result.status))
|
|
|
2642
2949
|
function writeTakeoverHelper(deps, oldPid) {
|
|
2643
2950
|
const file = takeoverHelperPath(deps.stateDir);
|
|
2644
2951
|
writeFileAtomic(file, buildTakeoverSource(deps, oldPid), 493);
|
|
2645
|
-
|
|
2952
|
+
fs12.chmodSync(file, 493);
|
|
2646
2953
|
return file;
|
|
2647
2954
|
}
|
|
2648
2955
|
function spawnDetached(program, args) {
|
|
@@ -2760,14 +3067,19 @@ var PanelClient = class {
|
|
|
2760
3067
|
return await this.request("POST", `/api/servers/${encodeURIComponent(id)}/free-port`);
|
|
2761
3068
|
}
|
|
2762
3069
|
};
|
|
2763
|
-
async function
|
|
3070
|
+
async function probeToken(baseUrl, token, timeoutMs = 5e3) {
|
|
2764
3071
|
try {
|
|
2765
3072
|
await new PanelClient({ baseUrl, token, timeoutMs }).listServers();
|
|
2766
|
-
return
|
|
2767
|
-
} catch {
|
|
2768
|
-
|
|
3073
|
+
return "ok";
|
|
3074
|
+
} catch (error) {
|
|
3075
|
+
if (error instanceof PanelError && (error.code === "AUTH_REQUIRED" || error.status === 401 || error.status === 403))
|
|
3076
|
+
return "refused";
|
|
3077
|
+
return "unreachable";
|
|
2769
3078
|
}
|
|
2770
3079
|
}
|
|
3080
|
+
async function verifyToken(baseUrl, token, timeoutMs = 5e3) {
|
|
3081
|
+
return await probeToken(baseUrl, token, timeoutMs) === "ok";
|
|
3082
|
+
}
|
|
2771
3083
|
|
|
2772
3084
|
// src/home-hosted/runtime.ts
|
|
2773
3085
|
import http from "node:http";
|
|
@@ -2863,18 +3175,35 @@ function apiTokenEnrolled(home) {
|
|
|
2863
3175
|
const secrets = readJson(secretsFile(home));
|
|
2864
3176
|
return secrets !== null && secrets.apiToken !== null && secrets.apiToken !== void 0;
|
|
2865
3177
|
}
|
|
2866
|
-
var
|
|
3178
|
+
var queue = /* @__PURE__ */ new Map();
|
|
3179
|
+
function serialised(stateDir, work) {
|
|
3180
|
+
const key = path12.resolve(stateDir);
|
|
3181
|
+
const previous = queue.get(key) ?? Promise.resolve();
|
|
3182
|
+
const next = previous.then(work, work);
|
|
3183
|
+
const settled = next.then(() => void 0, () => void 0);
|
|
3184
|
+
queue.set(key, settled);
|
|
3185
|
+
void settled.then(() => {
|
|
3186
|
+
if (queue.get(key) === settled) queue.delete(key);
|
|
3187
|
+
});
|
|
3188
|
+
return next;
|
|
3189
|
+
}
|
|
2867
3190
|
async function ensureToken(options) {
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3191
|
+
return await serialised(options.stateDir, async () => await enrollToken(options));
|
|
3192
|
+
}
|
|
3193
|
+
async function reclaimToken(options) {
|
|
3194
|
+
return await serialised(options.stateDir, async () => await enrollFreshToken(options));
|
|
3195
|
+
}
|
|
3196
|
+
async function runCli2(exec, args, env) {
|
|
2874
3197
|
try {
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3198
|
+
const result = await exec(args, env);
|
|
3199
|
+
if (result.code !== 0) {
|
|
3200
|
+
return {
|
|
3201
|
+
failure: result.error?.trim() || result.stderr.trim() || result.stdout.trim() || `exit ${String(result.code)}`
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
return { result };
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
return { failure: error instanceof Error ? error.message : String(error) };
|
|
2878
3207
|
}
|
|
2879
3208
|
}
|
|
2880
3209
|
async function enrollToken(options) {
|
|
@@ -2889,20 +3218,41 @@ async function enrollToken(options) {
|
|
|
2889
3218
|
};
|
|
2890
3219
|
}
|
|
2891
3220
|
const token = generateToken();
|
|
2892
|
-
const
|
|
2893
|
-
if (result
|
|
3221
|
+
const enrolled = await runCli2(options.exec, ["--home", options.home, "set-token"], { HHOSTED_TOKEN: token });
|
|
3222
|
+
if (enrolled.result === void 0) {
|
|
2894
3223
|
return {
|
|
2895
3224
|
token: null,
|
|
2896
3225
|
enrolled: false,
|
|
2897
|
-
detail: `could not enrol an API token: ${
|
|
3226
|
+
detail: `could not enrol an API token: ${enrolled.failure ?? "the CLI did not answer"}`
|
|
2898
3227
|
};
|
|
2899
3228
|
}
|
|
2900
3229
|
storeToken(options.stateDir, token);
|
|
2901
3230
|
return { token, enrolled: true, detail: "enrolled a new panel API token" };
|
|
2902
3231
|
}
|
|
3232
|
+
async function enrollFreshToken(options) {
|
|
3233
|
+
const cleared = await runCli2(options.exec, ["--home", options.home, "set-token", "--clear"], {});
|
|
3234
|
+
if (cleared.result === void 0) {
|
|
3235
|
+
return {
|
|
3236
|
+
token: null,
|
|
3237
|
+
detail: `could not clear the old API token: ${cleared.failure ?? "the CLI did not answer"}`
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
const token = generateToken();
|
|
3241
|
+
const enrolled = await runCli2(options.exec, ["--home", options.home, "set-token"], { HHOSTED_TOKEN: token });
|
|
3242
|
+
if (enrolled.result === void 0) {
|
|
3243
|
+
removeFile(storedTokenPath(options.stateDir));
|
|
3244
|
+
return {
|
|
3245
|
+
token: null,
|
|
3246
|
+
detail: `the old API token was cleared, but a new one could not be enrolled: ${enrolled.failure ?? "the CLI did not answer"}; retrying is safe`
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
storeToken(options.stateDir, token);
|
|
3250
|
+
return { token, detail: "enrolled a fresh panel API token" };
|
|
3251
|
+
}
|
|
2903
3252
|
|
|
2904
3253
|
// src/service.ts
|
|
2905
3254
|
var ENTRY_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3255
|
+
var TOKEN_PROBE_TIMEOUT_MS = 3e3;
|
|
2906
3256
|
function pluginRoot() {
|
|
2907
3257
|
try {
|
|
2908
3258
|
return path13.dirname(path13.dirname(fileURLToPath(import.meta.url)));
|
|
@@ -2924,8 +3274,16 @@ var HomeHostedService = class extends Service {
|
|
|
2924
3274
|
this.snapshotsFile = path13.join(options.stateDir, "snapshots.json");
|
|
2925
3275
|
}
|
|
2926
3276
|
clientCache = null;
|
|
3277
|
+
/** The last token this service proved against a panel, so a poll does not re-probe it. */
|
|
3278
|
+
tokenProof = null;
|
|
2927
3279
|
tokenDetail = "";
|
|
2928
3280
|
snapshotsFile;
|
|
3281
|
+
async resolveDsh(dshHome2) {
|
|
3282
|
+
return await (this.options.resolveDsh ?? ((options) => resolveDshLaunch(options)))({
|
|
3283
|
+
dshHome: dshHome2,
|
|
3284
|
+
stateDir: this.options.stateDir
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
2929
3287
|
// -------------------------------------------------------------------------
|
|
2930
3288
|
// Facts
|
|
2931
3289
|
// -------------------------------------------------------------------------
|
|
@@ -3019,14 +3377,40 @@ var HomeHostedService = class extends Service {
|
|
|
3019
3377
|
throw new HomeHostedError(read.error, "CONFIG_UNREADABLE");
|
|
3020
3378
|
writeConfig(this.options.home, patchControl(read.raw ?? {}, { port }), this.writtenBy());
|
|
3021
3379
|
}
|
|
3380
|
+
/** Whether this exact token was already proved against this exact panel. */
|
|
3381
|
+
tokenProven(url, token) {
|
|
3382
|
+
return this.tokenProof !== null && this.tokenProof.until > Date.now() && this.tokenProof.url === url && this.tokenProof.token === token;
|
|
3383
|
+
}
|
|
3384
|
+
proveToken(url, token) {
|
|
3385
|
+
this.tokenProof = { url, token, until: Date.now() + 3e4 };
|
|
3386
|
+
}
|
|
3022
3387
|
async panelStatus() {
|
|
3023
3388
|
const runtime = this.runtime();
|
|
3024
3389
|
const stored = readStoredToken(this.options.stateDir);
|
|
3025
3390
|
const enrolledOnDisk = apiTokenEnrolled(this.options.home);
|
|
3026
3391
|
const reachable = runtime !== null && (pidAlive(runtime.pid) || await probePanel(runtime.url));
|
|
3027
3392
|
const answered = runtime !== null && await probePanel(runtime.url);
|
|
3028
|
-
|
|
3029
|
-
|
|
3393
|
+
let token = "unknown";
|
|
3394
|
+
let tokenVerified = false;
|
|
3395
|
+
if (answered) {
|
|
3396
|
+
if (stored === null) {
|
|
3397
|
+
token = enrolledOnDisk ? "present" : "absent";
|
|
3398
|
+
} else if (this.tokenProven(runtime.url, stored)) {
|
|
3399
|
+
token = "enrolled";
|
|
3400
|
+
tokenVerified = true;
|
|
3401
|
+
} else {
|
|
3402
|
+
const probe = await probeToken(runtime.url, stored, TOKEN_PROBE_TIMEOUT_MS);
|
|
3403
|
+
if (probe === "ok") {
|
|
3404
|
+
token = "enrolled";
|
|
3405
|
+
tokenVerified = true;
|
|
3406
|
+
this.proveToken(runtime.url, stored);
|
|
3407
|
+
} else if (probe === "refused") {
|
|
3408
|
+
token = "stale";
|
|
3409
|
+
tokenVerified = true;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
const writeVia = token === "enrolled" ? "api" : "file";
|
|
3030
3414
|
return {
|
|
3031
3415
|
home: this.options.home,
|
|
3032
3416
|
reachable: answered,
|
|
@@ -3036,9 +3420,47 @@ var HomeHostedService = class extends Service {
|
|
|
3036
3420
|
pid: runtime?.pid ?? null,
|
|
3037
3421
|
writeVia,
|
|
3038
3422
|
token,
|
|
3039
|
-
|
|
3423
|
+
tokenVerified,
|
|
3424
|
+
detail: answered ? token === "enrolled" ? this.tokenDetail || "the panel is answering and this plugin holds a token" : token === "stale" ? "the panel refused this plugin's API token, so writes go straight to servers.config.json; regenerate the token" : token === "present" ? "the panel is answering, but home-hosted already holds an API token this plugin does not have" : token === "absent" ? "the panel is answering; a token will be enrolled on the first write" : "the panel answered, but the plugin's token could not be checked" : runtime === null ? "no run.json: the panel is not running, so entries are written straight to servers.config.json" : reachable ? "the panel process is alive but is not answering" : "the panel is not running"
|
|
3040
3425
|
};
|
|
3041
3426
|
}
|
|
3427
|
+
/**
|
|
3428
|
+
* Replace the panel's API token with a fresh one and prove it.
|
|
3429
|
+
*
|
|
3430
|
+
* home-hosted keeps only a hash, so a token this plugin does not hold cannot
|
|
3431
|
+
* be recovered: the old hash is cleared, a new token is enrolled through the
|
|
3432
|
+
* CLI, and it is proved against the answering panel before being reported.
|
|
3433
|
+
* Nothing is written when the CLI cannot run, and a panel that is not
|
|
3434
|
+
* answering is refused up front rather than left with an unverifiable token.
|
|
3435
|
+
*/
|
|
3436
|
+
async reclaimPanelToken() {
|
|
3437
|
+
const runtime = this.runtime();
|
|
3438
|
+
if (runtime === null || !await probePanel(runtime.url)) {
|
|
3439
|
+
throw new HomeHostedError(
|
|
3440
|
+
"the panel is not answering, so a regenerated token could not be verified; start the panel first",
|
|
3441
|
+
"PANEL_UNAVAILABLE"
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
const result = await reclaimToken({
|
|
3445
|
+
home: this.options.home,
|
|
3446
|
+
stateDir: this.options.stateDir,
|
|
3447
|
+
exec: (args, env) => this.cliExec(args, env)
|
|
3448
|
+
});
|
|
3449
|
+
this.clientCache = null;
|
|
3450
|
+
this.tokenProof = null;
|
|
3451
|
+
this.tokenDetail = result.detail;
|
|
3452
|
+
if (result.token === null)
|
|
3453
|
+
throw new HomeHostedError(result.detail, "TOKEN_RECLAIM_FAILED");
|
|
3454
|
+
const probe = await probeToken(runtime.url, result.token, TOKEN_PROBE_TIMEOUT_MS);
|
|
3455
|
+
if (probe !== "ok") {
|
|
3456
|
+
throw new HomeHostedError(
|
|
3457
|
+
probe === "refused" ? "a fresh token was enrolled, but the panel refused it; check the panel log before trying again" : "a fresh token was enrolled, but the panel stopped answering before it could be verified",
|
|
3458
|
+
"TOKEN_RECLAIM_UNVERIFIED"
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
this.proveToken(runtime.url, result.token);
|
|
3462
|
+
return await this.status();
|
|
3463
|
+
}
|
|
3042
3464
|
async tryClient() {
|
|
3043
3465
|
if (this.clientCache !== null && this.clientCache.until > Date.now())
|
|
3044
3466
|
return this.clientCache.client;
|
|
@@ -3057,6 +3479,7 @@ var HomeHostedService = class extends Service {
|
|
|
3057
3479
|
return null;
|
|
3058
3480
|
const client = new PanelClient({ baseUrl: runtime.url, token: ensured.token });
|
|
3059
3481
|
this.clientCache = { client, until: Date.now() + 3e4 };
|
|
3482
|
+
this.proveToken(runtime.url, ensured.token);
|
|
3060
3483
|
return client;
|
|
3061
3484
|
}
|
|
3062
3485
|
async requireClient() {
|
|
@@ -3095,20 +3518,35 @@ var HomeHostedService = class extends Service {
|
|
|
3095
3518
|
map.set(entry.id, { view: { id: entry.id, status: "unknown", pid: null, url: null, config: entry }, config: entry });
|
|
3096
3519
|
return map;
|
|
3097
3520
|
}
|
|
3098
|
-
async createEntry(intent, patch
|
|
3521
|
+
async createEntry(intent, patch) {
|
|
3099
3522
|
if (intent.id !== this.options.defaultEntryId)
|
|
3100
3523
|
return null;
|
|
3101
|
-
const
|
|
3524
|
+
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
3525
|
+
const dsh = await this.resolveDsh(harnessHome);
|
|
3102
3526
|
const { port, host } = this.webServer();
|
|
3103
3527
|
const generated = buildDshEntry({
|
|
3104
3528
|
id: intent.id,
|
|
3105
3529
|
port,
|
|
3106
3530
|
host,
|
|
3107
3531
|
profile: detectProfile(process10.argv, process10.env),
|
|
3108
|
-
dshHome:
|
|
3109
|
-
launch: dsh
|
|
3532
|
+
dshHome: harnessHome,
|
|
3533
|
+
launch: dsh,
|
|
3534
|
+
launcherPath: dsh?.launcherPath ?? null
|
|
3110
3535
|
});
|
|
3111
|
-
return { ...
|
|
3536
|
+
return { ...generated, ...patch };
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* An entry written before the launcher existed still runs a clone's build
|
|
3540
|
+
* output directly. Point it at the stable launcher, or the fix would only
|
|
3541
|
+
* ever apply to freshly created entries — the plugin never rewrites an
|
|
3542
|
+
* existing entry's command.
|
|
3543
|
+
*/
|
|
3544
|
+
async dshCommandRepair(live) {
|
|
3545
|
+
if (live === null || !needsLauncherRepair(live))
|
|
3546
|
+
return null;
|
|
3547
|
+
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
3548
|
+
const dsh = await this.resolveDsh(harnessHome);
|
|
3549
|
+
return launcherRepair(live, dsh);
|
|
3112
3550
|
}
|
|
3113
3551
|
async writeOwned(intent) {
|
|
3114
3552
|
const live = (await this.liveEntries()).get(intent.id) ?? null;
|
|
@@ -3118,13 +3556,14 @@ var HomeHostedService = class extends Service {
|
|
|
3118
3556
|
this.saveSnapshots(snapshots);
|
|
3119
3557
|
}
|
|
3120
3558
|
const patch = ownedPatch(intent, live?.config ?? null, { persistent: await this.supportsPersistent() });
|
|
3559
|
+
const repair = intent.id === this.options.defaultEntryId ? await this.dshCommandRepair(live?.config ?? null) : null;
|
|
3121
3560
|
const client = await this.tryClient();
|
|
3122
3561
|
if (client !== null) {
|
|
3123
3562
|
if (live !== null) {
|
|
3124
|
-
await client.updateServer(intent.id, patch);
|
|
3563
|
+
await client.updateServer(intent.id, { ...patch, ...repair ?? {} });
|
|
3125
3564
|
return;
|
|
3126
3565
|
}
|
|
3127
|
-
const entry2 = await this.createEntry(intent, patch
|
|
3566
|
+
const entry2 = await this.createEntry(intent, patch);
|
|
3128
3567
|
if (entry2 === null) {
|
|
3129
3568
|
throw new HomeHostedError(
|
|
3130
3569
|
`no server "${intent.id}" exists; create it first, then this plugin can adopt its autostart and conflict policy`,
|
|
@@ -3139,10 +3578,10 @@ var HomeHostedService = class extends Service {
|
|
|
3139
3578
|
throw new HomeHostedError(read.error, "CONFIG_UNREADABLE");
|
|
3140
3579
|
let raw = read.raw ?? {};
|
|
3141
3580
|
if (findEntry(raw, intent.id) !== null) {
|
|
3142
|
-
writeConfig(this.options.home, patchEntry(raw, intent.id, patch), this.writtenBy());
|
|
3581
|
+
writeConfig(this.options.home, patchEntry(raw, intent.id, { ...patch, ...repair ?? {} }), this.writtenBy());
|
|
3143
3582
|
return;
|
|
3144
3583
|
}
|
|
3145
|
-
const entry = await this.createEntry(intent, patch
|
|
3584
|
+
const entry = await this.createEntry(intent, patch);
|
|
3146
3585
|
if (entry === null)
|
|
3147
3586
|
throw new HomeHostedError(`no server "${intent.id}" exists in ${this.options.home}/servers.config.json`, "ENTRY_MISSING");
|
|
3148
3587
|
if (await this.panelRunning()) {
|
|
@@ -3301,7 +3740,7 @@ var HomeHostedService = class extends Service {
|
|
|
3301
3740
|
case "switch": {
|
|
3302
3741
|
if (file === void 0 || file.trim().length === 0)
|
|
3303
3742
|
throw new HomeHostedError("switching the UI needs the zip to install", "UI_FILE_REQUIRED");
|
|
3304
|
-
if (!
|
|
3743
|
+
if (!fs13.existsSync(file))
|
|
3305
3744
|
throw new HomeHostedError(`no file at ${file}`, "UI_FILE_MISSING");
|
|
3306
3745
|
const result = await runUi(["ui-switch", "--file", file, "--yes"]);
|
|
3307
3746
|
return { ok: result.code === 0, detail: result.code === 0 ? "the UI was replaced" : "the switch did not run", ui: active(), output: result.output };
|
|
@@ -3525,7 +3964,7 @@ var HomeHostedService = class extends Service {
|
|
|
3525
3964
|
if (status.state !== "enabled-failing" && status.state !== "installed-disabled")
|
|
3526
3965
|
return;
|
|
3527
3966
|
const fileBacked = status.mechanism === "systemd-user" || status.mechanism === "systemd-system" || status.mechanism === "xdg-autostart" || status.mechanism === "launchd-agent" || status.mechanism === "launchd-daemon";
|
|
3528
|
-
if (fileBacked && (status.unitPath === null || !
|
|
3967
|
+
if (fileBacked && (status.unitPath === null || !fs13.existsSync(status.unitPath)))
|
|
3529
3968
|
return;
|
|
3530
3969
|
try {
|
|
3531
3970
|
await this.installBoot();
|
|
@@ -3638,6 +4077,8 @@ var HomeHostedService = class extends Service {
|
|
|
3638
4077
|
return await this.startPanelNow();
|
|
3639
4078
|
case "panel.takeover":
|
|
3640
4079
|
return await this.takeoverPanel(input.force === true);
|
|
4080
|
+
case "panel.reclaimToken":
|
|
4081
|
+
return await this.reclaimPanelToken();
|
|
3641
4082
|
case "cli.installGlobal":
|
|
3642
4083
|
return await this.installGlobalCli();
|
|
3643
4084
|
default:
|
|
@@ -3831,6 +4272,8 @@ function normalize(raw, fallbackEntryId) {
|
|
|
3831
4272
|
port: typeof raw?.panel?.port === "number" && Number.isInteger(raw.panel.port) && raw.panel.port >= 1 && raw.panel.port <= 65535 ? raw.panel.port : null
|
|
3832
4273
|
},
|
|
3833
4274
|
authNotice: raw?.authNotice === void 0 ? DEFAULT_SETTINGS.authNotice : raw.authNotice === true,
|
|
4275
|
+
// Absent means on: a stale token is a fault to repair, not a setting to opt into.
|
|
4276
|
+
reclaimToken: raw?.reclaimToken === void 0 ? DEFAULT_SETTINGS.reclaimToken : raw.reclaimToken === true,
|
|
3834
4277
|
uiStyle: raw?.uiStyle === "compact" ? "compact" : "detailed",
|
|
3835
4278
|
agentTools: {
|
|
3836
4279
|
// Absent means the default (on), and so does the pair the previous
|
|
@@ -3868,6 +4311,7 @@ var SettingsStore = class {
|
|
|
3868
4311
|
entries: patch.entries ?? this.current.entries,
|
|
3869
4312
|
panel: { ...this.current.panel, ...patch.panel ?? {} },
|
|
3870
4313
|
authNotice: patch.authNotice ?? this.current.authNotice,
|
|
4314
|
+
reclaimToken: patch.reclaimToken ?? this.current.reclaimToken,
|
|
3871
4315
|
uiStyle: patch.uiStyle ?? this.current.uiStyle,
|
|
3872
4316
|
agentTools: { ...this.current.agentTools, ...patch.agentTools ?? {} },
|
|
3873
4317
|
cli: { ...this.current.cli, ...patch.cli ?? {} }
|
|
@@ -4001,7 +4445,49 @@ function toolNameFor(name2) {
|
|
|
4001
4445
|
var READ_ONLY_ACTIONS = {
|
|
4002
4446
|
ui_manage: (input) => stringArg(input, "action") === "status"
|
|
4003
4447
|
};
|
|
4004
|
-
|
|
4448
|
+
var TOKEN_REFUSAL_CODES = /* @__PURE__ */ new Set([
|
|
4449
|
+
"AUTH_REQUIRED",
|
|
4450
|
+
"AUTH_UNARMED",
|
|
4451
|
+
"UNAUTHORIZED",
|
|
4452
|
+
"FORBIDDEN",
|
|
4453
|
+
"INVALID_TOKEN",
|
|
4454
|
+
"TOKEN_STALE",
|
|
4455
|
+
"TOKEN_REFUSED"
|
|
4456
|
+
]);
|
|
4457
|
+
function errorCode(error) {
|
|
4458
|
+
const code = error?.code;
|
|
4459
|
+
return typeof code === "string" ? code : null;
|
|
4460
|
+
}
|
|
4461
|
+
function errorStatus(error) {
|
|
4462
|
+
const status = error?.status;
|
|
4463
|
+
return typeof status === "number" ? status : null;
|
|
4464
|
+
}
|
|
4465
|
+
function refusedByPanel(error) {
|
|
4466
|
+
const status = errorStatus(error);
|
|
4467
|
+
if (status === 401 || status === 403)
|
|
4468
|
+
return true;
|
|
4469
|
+
const code = errorCode(error);
|
|
4470
|
+
return code !== null && TOKEN_REFUSAL_CODES.has(code);
|
|
4471
|
+
}
|
|
4472
|
+
async function tokenRefused(service, error) {
|
|
4473
|
+
if (refusedByPanel(error))
|
|
4474
|
+
return true;
|
|
4475
|
+
if (errorCode(error) !== "PANEL_UNAVAILABLE")
|
|
4476
|
+
return false;
|
|
4477
|
+
try {
|
|
4478
|
+
const status = await service.call("status", {});
|
|
4479
|
+
return status?.panel.token === "stale";
|
|
4480
|
+
} catch {
|
|
4481
|
+
return false;
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
function messageOf(error) {
|
|
4485
|
+
return error instanceof Error ? error.message : String(error);
|
|
4486
|
+
}
|
|
4487
|
+
function failureText(error) {
|
|
4488
|
+
return `failed: ${messageOf(error)}`;
|
|
4489
|
+
}
|
|
4490
|
+
function registerOne(ctx, service, settings, name2) {
|
|
4005
4491
|
const spec = TOOL_SPECS[name2];
|
|
4006
4492
|
const toolName = toolNameFor(name2);
|
|
4007
4493
|
const mutatingTool = MUTATING_AGENT_TOOLS.includes(name2);
|
|
@@ -4030,11 +4516,27 @@ function registerOne(ctx, service, name2) {
|
|
|
4030
4516
|
if (outcome !== "allowed-once")
|
|
4031
4517
|
return `refused: approval answered "${outcome}" (session sandbox: ${mode ?? "unknown"}). ${remedy}`;
|
|
4032
4518
|
}
|
|
4519
|
+
let request;
|
|
4033
4520
|
try {
|
|
4034
|
-
|
|
4035
|
-
return JSON.stringify(await service.call(endpoint, payload), null, 2);
|
|
4521
|
+
request = spec.run(input);
|
|
4036
4522
|
} catch (error) {
|
|
4037
|
-
return
|
|
4523
|
+
return failureText(error);
|
|
4524
|
+
}
|
|
4525
|
+
try {
|
|
4526
|
+
return JSON.stringify(await service.call(request.endpoint, request.payload), null, 2);
|
|
4527
|
+
} catch (error) {
|
|
4528
|
+
if (!settings.get().reclaimToken || !await tokenRefused(service, error))
|
|
4529
|
+
return failureText(error);
|
|
4530
|
+
try {
|
|
4531
|
+
await service.call("panel.reclaimToken", {});
|
|
4532
|
+
} catch (reclaimError) {
|
|
4533
|
+
return `${failureText(error)} (token reclaim failed: ${messageOf(reclaimError)})`;
|
|
4534
|
+
}
|
|
4535
|
+
try {
|
|
4536
|
+
return JSON.stringify(await service.call(request.endpoint, request.payload), null, 2);
|
|
4537
|
+
} catch (retryError) {
|
|
4538
|
+
return failureText(retryError);
|
|
4539
|
+
}
|
|
4038
4540
|
}
|
|
4039
4541
|
}
|
|
4040
4542
|
}));
|
|
@@ -4050,7 +4552,7 @@ function registerAgentTools(ctx, service, settings) {
|
|
|
4050
4552
|
return;
|
|
4051
4553
|
for (const name2 of current.agentTools.allow) {
|
|
4052
4554
|
try {
|
|
4053
|
-
disposers.push(registerOne(scoped, service, name2));
|
|
4555
|
+
disposers.push(registerOne(scoped, service, settings, name2));
|
|
4054
4556
|
} catch {
|
|
4055
4557
|
}
|
|
4056
4558
|
}
|