dsh-home-hosted 0.2.2 → 0.3.2
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 +106 -17
- package/docs/DESIGN.md +54 -0
- package/lib/client.js +6 -6
- package/lib/index.js +721 -189
- 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/resolve.d.ts +1 -1
- package/lib/types/home-hosted/token.d.ts +20 -0
- package/lib/types/service.d.ts +46 -0
- package/lib/types/shared/contracts.d.ts +22 -3
- package/package.json +2 -2
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";
|
|
@@ -2032,8 +2033,9 @@ function removeEntry(raw, id) {
|
|
|
2032
2033
|
}
|
|
2033
2034
|
|
|
2034
2035
|
// src/home-hosted/dsh-entry.ts
|
|
2035
|
-
import
|
|
2036
|
-
import
|
|
2036
|
+
import fs11 from "node:fs";
|
|
2037
|
+
import path10 from "node:path";
|
|
2038
|
+
import process7 from "node:process";
|
|
2037
2039
|
|
|
2038
2040
|
// src/home-hosted/launch.ts
|
|
2039
2041
|
import fs8 from "node:fs";
|
|
@@ -2101,144 +2103,12 @@ function buildHomeHostedBootSpec(facts, launch, options) {
|
|
|
2101
2103
|
};
|
|
2102
2104
|
}
|
|
2103
2105
|
|
|
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
2106
|
// src/home-hosted/resolve.ts
|
|
2237
2107
|
import { createRequire } from "node:module";
|
|
2238
2108
|
import fs9 from "node:fs";
|
|
2239
|
-
import
|
|
2240
|
-
import
|
|
2241
|
-
var EXPECTED_RANGE = "^0.6.
|
|
2109
|
+
import path8 from "node:path";
|
|
2110
|
+
import process5 from "node:process";
|
|
2111
|
+
var EXPECTED_RANGE = "^0.6.6";
|
|
2242
2112
|
var MIN_SUPPORTED_VERSION = "0.4.1";
|
|
2243
2113
|
var MIN_KILL_VERSION = "0.6.0";
|
|
2244
2114
|
var MIN_PERSISTENT_VERSION = "0.6.3";
|
|
@@ -2253,7 +2123,7 @@ function binEntryFromManifest(manifestPath) {
|
|
|
2253
2123
|
try {
|
|
2254
2124
|
const manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
|
|
2255
2125
|
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 ?
|
|
2126
|
+
return typeof declared === "string" && declared.length > 0 ? path8.resolve(path8.dirname(manifestPath), declared) : null;
|
|
2257
2127
|
} catch {
|
|
2258
2128
|
return null;
|
|
2259
2129
|
}
|
|
@@ -2309,7 +2179,7 @@ async function resolveCli(options = {}) {
|
|
|
2309
2179
|
const override = options.override?.trim();
|
|
2310
2180
|
let unusableOverride = null;
|
|
2311
2181
|
if (override !== void 0 && override.length > 0) {
|
|
2312
|
-
const launch = resolveShimmedCli(
|
|
2182
|
+
const launch = resolveShimmedCli(path8.resolve(override));
|
|
2313
2183
|
if (launch === null) {
|
|
2314
2184
|
unusableOverride = override;
|
|
2315
2185
|
} else {
|
|
@@ -2343,13 +2213,13 @@ async function resolveCli(options = {}) {
|
|
|
2343
2213
|
}
|
|
2344
2214
|
dependency = {
|
|
2345
2215
|
candidate: { source: "dependency", path: dependencyEntry, version: version2 },
|
|
2346
|
-
launch: { program:
|
|
2216
|
+
launch: { program: process5.execPath, args: [dependencyEntry], cliEntry: dependencyEntry, shimPath: null, source: "entry" }
|
|
2347
2217
|
};
|
|
2348
2218
|
}
|
|
2349
2219
|
let global = null;
|
|
2350
2220
|
const found = await locate("home-hosted") ?? await locate("hh");
|
|
2351
2221
|
if (found !== null) {
|
|
2352
|
-
const launch = resolveShimmedCli(
|
|
2222
|
+
const launch = resolveShimmedCli(path8.resolve(found));
|
|
2353
2223
|
if (launch !== null) {
|
|
2354
2224
|
global = {
|
|
2355
2225
|
candidate: { source: "path", path: launch.cliEntry ?? launch.shimPath ?? launch.program, version: await readVersion(launch) },
|
|
@@ -2413,16 +2283,16 @@ async function resolveCli(options = {}) {
|
|
|
2413
2283
|
|
|
2414
2284
|
// src/home-hosted/launcher.ts
|
|
2415
2285
|
import fs10 from "node:fs";
|
|
2416
|
-
import
|
|
2417
|
-
import
|
|
2286
|
+
import path9 from "node:path";
|
|
2287
|
+
import process6 from "node:process";
|
|
2418
2288
|
function launcherDir(stateDir) {
|
|
2419
|
-
return
|
|
2289
|
+
return path9.join(stateDir, "bin");
|
|
2420
2290
|
}
|
|
2421
2291
|
function launcherPath(stateDir) {
|
|
2422
|
-
return
|
|
2292
|
+
return path9.join(launcherDir(stateDir), "home-hosted.mjs");
|
|
2423
2293
|
}
|
|
2424
2294
|
function launcherRecordPath(stateDir) {
|
|
2425
|
-
return
|
|
2295
|
+
return path9.join(launcherDir(stateDir), "resolved.json");
|
|
2426
2296
|
}
|
|
2427
2297
|
function buildLauncherSource(options) {
|
|
2428
2298
|
const marker = options.marker ?? "managed by dsh-home-hosted";
|
|
@@ -2547,13 +2417,444 @@ async function preflightLauncher(stateDir, timeoutMs = 1e4) {
|
|
|
2547
2417
|
const file = launcherPath(stateDir);
|
|
2548
2418
|
if (!fs10.existsSync(file))
|
|
2549
2419
|
return null;
|
|
2550
|
-
const result =
|
|
2420
|
+
const result = process6.platform === "win32" ? await run(process6.execPath, [file, "--version"], { timeoutMs }) : await run(file, ["--version"], { timeoutMs });
|
|
2551
2421
|
return parseVersion(result.stdout) ?? parseVersion(result.stderr);
|
|
2552
2422
|
}
|
|
2423
|
+
function dshLauncherRecordPath(stateDir) {
|
|
2424
|
+
return path9.join(launcherDir(stateDir), "dsh-resolved.json");
|
|
2425
|
+
}
|
|
2426
|
+
function dshLauncherPath(stateDir, entryExtension) {
|
|
2427
|
+
const extension = (entryExtension ?? "").toLowerCase() === ".cjs" ? ".cjs" : ".mjs";
|
|
2428
|
+
return path9.join(launcherDir(stateDir), `dsh${extension}`);
|
|
2429
|
+
}
|
|
2430
|
+
function readDshLauncherRecord(stateDir) {
|
|
2431
|
+
return readJson(dshLauncherRecordPath(stateDir));
|
|
2432
|
+
}
|
|
2433
|
+
function buildDshLauncherSource(options) {
|
|
2434
|
+
const esm = (options.entryExtension ?? "").toLowerCase() !== ".cjs";
|
|
2435
|
+
const prelude = esm ? `import fs from 'node:fs'
|
|
2436
|
+
import path from 'node:path'
|
|
2437
|
+
import process from 'node:process'
|
|
2438
|
+
import { spawnSync } from 'node:child_process'` : `const fs = require('node:fs')
|
|
2439
|
+
const path = require('node:path')
|
|
2440
|
+
const process = require('node:process')
|
|
2441
|
+
const { spawnSync } = require('node:child_process')`;
|
|
2442
|
+
const roots = [options.resolvedEntry === null ? null : dshSearchRoot(options.resolvedEntry), ...options.searchRoots ?? []].filter((root) => typeof root === "string" && root.length > 0);
|
|
2443
|
+
return `#!/usr/bin/env node
|
|
2444
|
+
// managed by dsh-home-hosted \u2014 rewritten on every plugin start; do not edit.
|
|
2445
|
+
${prelude}
|
|
2446
|
+
|
|
2447
|
+
const DSH_HOME = ${JSON.stringify(options.dshHome)}
|
|
2448
|
+
const RECORD = ${JSON.stringify(dshLauncherRecordPath(options.stateDir))}
|
|
2449
|
+
const SEARCH_ROOTS = ${JSON.stringify([...new Set(roots)])}
|
|
2450
|
+
|
|
2451
|
+
function readRecord() {
|
|
2452
|
+
try { return JSON.parse(fs.readFileSync(RECORD, 'utf8')) } catch { return null }
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function isEntry(file) {
|
|
2456
|
+
let stat = null
|
|
2457
|
+
try { stat = fs.statSync(file) } catch { return false }
|
|
2458
|
+
return stat.isFile() && /\\.(?:mjs|cjs|js)$/.test(file)
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function collect(out, entry) {
|
|
2462
|
+
if (typeof entry === 'string' && isEntry(entry) && !out.includes(entry)) out.push(entry)
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function collectStore(root, out) {
|
|
2466
|
+
const store = path.join(root, 'node_modules', '.pnpm')
|
|
2467
|
+
let names = []
|
|
2468
|
+
try { names = fs.readdirSync(store) } catch { return }
|
|
2469
|
+
for (const name of names) {
|
|
2470
|
+
if (!name.startsWith('dsh@') && !name.startsWith('@deepseek-ai+dsh@')) continue
|
|
2471
|
+
collect(out, path.join(store, name, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'))
|
|
2472
|
+
collect(out, path.join(store, name, 'node_modules', 'dsh', 'bin', 'dsh.js'))
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
function collectRoot(root, out) {
|
|
2477
|
+
collect(out, path.join(root, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'))
|
|
2478
|
+
collect(out, path.join(root, 'node_modules', 'dsh', 'bin', 'dsh.js'))
|
|
2479
|
+
collectStore(root, out)
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
function candidates() {
|
|
2483
|
+
const out = []
|
|
2484
|
+
const record = readRecord()
|
|
2485
|
+
if (record && typeof record.entry === 'string') collect(out, record.entry)
|
|
2486
|
+
for (const root of [...(Array.isArray(record?.roots) ? record.roots : []), ...SEARCH_ROOTS]) collectRoot(root, out)
|
|
2487
|
+
collectRoot(DSH_HOME, out)
|
|
2488
|
+
let profiles = []
|
|
2489
|
+
try { profiles = fs.readdirSync(path.join(DSH_HOME, 'profiles')) } catch {}
|
|
2490
|
+
for (const name of profiles) collectRoot(path.join(DSH_HOME, 'profiles', name), out)
|
|
2491
|
+
// Last resort: a dsh on the entry's own PATH.
|
|
2492
|
+
for (const dir of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
2493
|
+
collect(out, path.join(dir, 'dsh'))
|
|
2494
|
+
collect(out, path.join(dir, 'dsh.cmd'))
|
|
2495
|
+
}
|
|
2496
|
+
return out
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
function versionOf(entry) {
|
|
2500
|
+
let dir = path.dirname(entry)
|
|
2501
|
+
for (let hop = 0; hop < 4; hop += 1) {
|
|
2502
|
+
try {
|
|
2503
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
2504
|
+
if (typeof manifest.version === 'string') return manifest.version
|
|
2505
|
+
} catch {}
|
|
2506
|
+
dir = path.dirname(dir)
|
|
2507
|
+
}
|
|
2508
|
+
return null
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function compare(a, b) {
|
|
2512
|
+
const split = value => {
|
|
2513
|
+
const [core, pre = null] = String(value).split('-', 2)
|
|
2514
|
+
return { parts: core.split('.').map(part => Number.parseInt(part, 10) || 0), pre }
|
|
2515
|
+
}
|
|
2516
|
+
const left = split(a); const right = split(b)
|
|
2517
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2518
|
+
const diff = (left.parts[i] ?? 0) - (right.parts[i] ?? 0)
|
|
2519
|
+
if (diff !== 0) return diff < 0 ? -1 : 1
|
|
2520
|
+
}
|
|
2521
|
+
if (left.pre === right.pre) return 0
|
|
2522
|
+
if (left.pre === null) return 1
|
|
2523
|
+
if (right.pre === null) return -1
|
|
2524
|
+
return left.pre < right.pre ? -1 : 1
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// A recorded path that still exists is usually the copy to boot, but never the
|
|
2528
|
+
// older one: a rebuild or upgrade that landed elsewhere must win on version.
|
|
2529
|
+
function choose() {
|
|
2530
|
+
const recorded = readRecord()?.entry ?? null
|
|
2531
|
+
const found = candidates().map(entry => ({ entry, recorded: entry === recorded, version: versionOf(entry) }))
|
|
2532
|
+
const known = found.filter(item => item.version !== null)
|
|
2533
|
+
if (known.length > 0) {
|
|
2534
|
+
known.sort((a, b) => (compare(b.version, a.version) || (Number(b.recorded) - Number(a.recorded))))
|
|
2535
|
+
return known[0].entry
|
|
2536
|
+
}
|
|
2537
|
+
return (found.find(item => item.recorded) ?? found[0])?.entry ?? null
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
const entry = choose()
|
|
2541
|
+
if (entry === null) {
|
|
2542
|
+
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')
|
|
2543
|
+
process.exit(1)
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
const args = process.argv.slice(2)
|
|
2547
|
+
const result = /\\.(mjs|cjs|js)$/.test(entry)
|
|
2548
|
+
? spawnSync(process.execPath, [entry, ...args], { stdio: 'inherit' })
|
|
2549
|
+
: spawnSync(entry, args, { stdio: 'inherit', shell: process.platform === 'win32' && /\\.(cmd|bat)$/i.test(entry) })
|
|
2550
|
+
process.exit(typeof result.status === 'number' ? result.status : 1)
|
|
2551
|
+
`;
|
|
2552
|
+
}
|
|
2553
|
+
function writeDshLauncher(options) {
|
|
2554
|
+
const file = dshLauncherPath(options.stateDir, options.entryExtension);
|
|
2555
|
+
const source = buildDshLauncherSource(options);
|
|
2556
|
+
const previous = fs10.existsSync(file) ? fs10.readFileSync(file, "utf8") : null;
|
|
2557
|
+
const changed = previous !== source;
|
|
2558
|
+
if (changed) {
|
|
2559
|
+
writeFileAtomic(file, source, 493);
|
|
2560
|
+
fs10.chmodSync(file, 493);
|
|
2561
|
+
}
|
|
2562
|
+
const raw = [dshSearchRoot(options.resolvedEntry), ...options.searchRoots ?? [], ...readDshLauncherRecord(options.stateDir)?.roots ?? []];
|
|
2563
|
+
const known = raw.filter((root) => typeof root === "string" && root.length > 0 && dshRootUsable(root));
|
|
2564
|
+
const roots = [...new Set(known)];
|
|
2565
|
+
writeJsonAtomic(dshLauncherRecordPath(options.stateDir), {
|
|
2566
|
+
entry: options.resolvedEntry,
|
|
2567
|
+
roots,
|
|
2568
|
+
writtenAt: Date.now()
|
|
2569
|
+
}, 384);
|
|
2570
|
+
return { path: file, changed };
|
|
2571
|
+
}
|
|
2572
|
+
function dshCandidatesUnder(root) {
|
|
2573
|
+
const out = [];
|
|
2574
|
+
for (const entry of [
|
|
2575
|
+
path9.join(root, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
2576
|
+
path9.join(root, "node_modules", "dsh", "bin", "dsh.js")
|
|
2577
|
+
]) {
|
|
2578
|
+
if (fs10.existsSync(entry))
|
|
2579
|
+
out.push(entry);
|
|
2580
|
+
}
|
|
2581
|
+
const store = path9.join(root, "node_modules", ".pnpm");
|
|
2582
|
+
if (fs10.existsSync(store)) {
|
|
2583
|
+
for (const name2 of fs10.readdirSync(store)) {
|
|
2584
|
+
if (!name2.startsWith("dsh@") && !name2.startsWith("@deepseek-ai+dsh@"))
|
|
2585
|
+
continue;
|
|
2586
|
+
for (const entry of [
|
|
2587
|
+
path9.join(store, name2, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
|
|
2588
|
+
path9.join(store, name2, "node_modules", "dsh", "bin", "dsh.js")
|
|
2589
|
+
]) {
|
|
2590
|
+
if (fs10.existsSync(entry))
|
|
2591
|
+
out.push(entry);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return out;
|
|
2596
|
+
}
|
|
2597
|
+
function dshSearchRoot(entry) {
|
|
2598
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
2599
|
+
return null;
|
|
2600
|
+
const parts = entry.split(path9.sep);
|
|
2601
|
+
const store = parts.indexOf(".pnpm");
|
|
2602
|
+
const modules = store > 0 ? store - 1 : parts.lastIndexOf("node_modules");
|
|
2603
|
+
return modules > 0 ? parts.slice(0, modules).join(path9.sep) : null;
|
|
2604
|
+
}
|
|
2605
|
+
function dshRootUsable(root) {
|
|
2606
|
+
return dshCandidatesUnder(root).length > 0 || fs10.existsSync(path9.join(root, "node_modules", ".pnpm"));
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/home-hosted/dsh-entry.ts
|
|
2610
|
+
function detectProfile(argv, env = {}) {
|
|
2611
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2612
|
+
const arg = argv[index];
|
|
2613
|
+
if (arg === "--profile" && typeof argv[index + 1] === "string" && !argv[index + 1].startsWith("-"))
|
|
2614
|
+
return argv[index + 1];
|
|
2615
|
+
if (arg?.startsWith("--profile="))
|
|
2616
|
+
return arg.slice("--profile=".length);
|
|
2617
|
+
}
|
|
2618
|
+
const dir = env.DSH_PROFILE_DIR;
|
|
2619
|
+
if (typeof dir === "string" && dir.length > 0) {
|
|
2620
|
+
const base = path10.basename(dir);
|
|
2621
|
+
if (base.length > 0 && base !== "." && base !== path10.sep)
|
|
2622
|
+
return base;
|
|
2623
|
+
}
|
|
2624
|
+
const app = argv.slice(2).find((candidate) => candidate !== void 0 && !candidate.startsWith("-"));
|
|
2625
|
+
if (app !== void 0 && app.length > 0)
|
|
2626
|
+
return app;
|
|
2627
|
+
const fromEnv = env.DSH_PROFILE;
|
|
2628
|
+
return typeof fromEnv === "string" && fromEnv.length > 0 ? fromEnv : "web";
|
|
2629
|
+
}
|
|
2630
|
+
function versionOf(entry) {
|
|
2631
|
+
let dir = path10.dirname(entry);
|
|
2632
|
+
for (let hop = 0; hop < 4; hop += 1) {
|
|
2633
|
+
const manifest = readJson(path10.join(dir, "package.json"));
|
|
2634
|
+
if (typeof manifest?.version === "string")
|
|
2635
|
+
return manifest.version;
|
|
2636
|
+
dir = path10.dirname(dir);
|
|
2637
|
+
}
|
|
2638
|
+
return null;
|
|
2639
|
+
}
|
|
2640
|
+
function isEntry(file) {
|
|
2641
|
+
return /\.[cm]?js$/.test(file) && fs11.existsSync(file) && !fs11.statSync(file).isDirectory();
|
|
2642
|
+
}
|
|
2643
|
+
function realPath(file) {
|
|
2644
|
+
try {
|
|
2645
|
+
return fs11.realpathSync(file);
|
|
2646
|
+
} catch {
|
|
2647
|
+
return file;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function cloneEntryFrom(argv1) {
|
|
2651
|
+
if (!isEntry(argv1))
|
|
2652
|
+
return null;
|
|
2653
|
+
const root = path10.dirname(path10.dirname(argv1));
|
|
2654
|
+
for (const candidate of [
|
|
2655
|
+
path10.join(root, "lib", "bin.js"),
|
|
2656
|
+
path10.join(root, "lib", "bin.mjs"),
|
|
2657
|
+
path10.join(root, "lib", "bin.cjs"),
|
|
2658
|
+
path10.join(root, "dist", "bin.js"),
|
|
2659
|
+
path10.join(root, "bin", "dsh.js"),
|
|
2660
|
+
path10.join(root, "bin", "dsh.mjs")
|
|
2661
|
+
]) {
|
|
2662
|
+
if (isEntry(candidate))
|
|
2663
|
+
return candidate;
|
|
2664
|
+
}
|
|
2665
|
+
return argv1;
|
|
2666
|
+
}
|
|
2667
|
+
function launchedEntry(argv1) {
|
|
2668
|
+
const real = realPath(argv1);
|
|
2669
|
+
const direct = cloneEntryFrom(real);
|
|
2670
|
+
if (direct !== null)
|
|
2671
|
+
return direct;
|
|
2672
|
+
const shimmed = resolveShimmedCli(real, process7.execPath);
|
|
2673
|
+
return shimmed?.cliEntry == null ? null : cloneEntryFrom(shimmed.cliEntry);
|
|
2674
|
+
}
|
|
2675
|
+
function findDshEntry(dshHome2) {
|
|
2676
|
+
const found = [];
|
|
2677
|
+
found.push(...dshCandidatesUnder(dshHome2));
|
|
2678
|
+
for (const name2 of readdirNames(path10.join(dshHome2, "profiles")))
|
|
2679
|
+
found.push(...dshCandidatesUnder(path10.join(dshHome2, "profiles", name2)));
|
|
2680
|
+
const withVersion = found.map((entry) => ({ entry, version: versionOf(entry) }));
|
|
2681
|
+
withVersion.sort((a, b) => compareVersions(b.version ?? "0.0.0", a.version ?? "0.0.0"));
|
|
2682
|
+
return withVersion[0]?.entry ?? null;
|
|
2683
|
+
}
|
|
2684
|
+
function readdirNames(dir) {
|
|
2685
|
+
try {
|
|
2686
|
+
return fs11.readdirSync(dir);
|
|
2687
|
+
} catch {
|
|
2688
|
+
return [];
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
async function resolveDshLaunch(options = {}) {
|
|
2692
|
+
const argv1 = options.argv1 === void 0 ? process7.argv[1] : options.argv1;
|
|
2693
|
+
const dshHome2 = options.dshHome ?? dshHome();
|
|
2694
|
+
const stateDir = options.stateDir ?? pluginStateDir();
|
|
2695
|
+
const local = typeof argv1 === "string" && argv1.length > 0 ? launchedEntry(argv1) : null;
|
|
2696
|
+
if (local !== null && realPath(local) !== realPath(dshLauncherPath(stateDir, path10.extname(local)))) {
|
|
2697
|
+
const launcher = writeDshLauncher({
|
|
2698
|
+
stateDir,
|
|
2699
|
+
dshHome: dshHome2,
|
|
2700
|
+
resolvedEntry: local,
|
|
2701
|
+
entryExtension: path10.extname(local),
|
|
2702
|
+
// Where this install sits, so a rebuilt/moved copy is found next boot.
|
|
2703
|
+
searchRoots: [dshSearchRoot(local)].filter((root) => root !== null)
|
|
2704
|
+
});
|
|
2705
|
+
return { program: process7.execPath, args: [local], cliEntry: local, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2706
|
+
}
|
|
2707
|
+
const installed = findDshEntry(dshHome2);
|
|
2708
|
+
if (installed !== null) {
|
|
2709
|
+
const launcher = writeDshLauncher({ stateDir, dshHome: dshHome2, resolvedEntry: installed, entryExtension: path10.extname(installed) });
|
|
2710
|
+
return { program: process7.execPath, args: [installed], cliEntry: installed, shimPath: null, source: "entry", launcherPath: launcher.path };
|
|
2711
|
+
}
|
|
2712
|
+
const found = await (options.findOnPath ?? which)("dsh");
|
|
2713
|
+
if (found === null)
|
|
2714
|
+
return null;
|
|
2715
|
+
const launch = resolveShimmedCli(found, process7.execPath);
|
|
2716
|
+
return launch === null ? null : { ...launch, launcherPath: null };
|
|
2717
|
+
}
|
|
2718
|
+
function buildCommand(launch, launcherPath2) {
|
|
2719
|
+
if (launcherPath2 !== null && launcherPath2.length > 0)
|
|
2720
|
+
return { command: process7.execPath, entryArgs: [launcherPath2] };
|
|
2721
|
+
return { command: launch.program, entryArgs: launch.args };
|
|
2722
|
+
}
|
|
2723
|
+
function entryScript(config) {
|
|
2724
|
+
const program = config.command;
|
|
2725
|
+
if (typeof program !== "string" || program.length === 0)
|
|
2726
|
+
return null;
|
|
2727
|
+
if (!path10.isAbsolute(program))
|
|
2728
|
+
return program;
|
|
2729
|
+
const rest = config.args ?? [];
|
|
2730
|
+
return rest.find((arg) => path10.isAbsolute(arg)) ?? program;
|
|
2731
|
+
}
|
|
2732
|
+
function needsLauncherRepair(config) {
|
|
2733
|
+
const program = config.command;
|
|
2734
|
+
if (typeof program !== "string" || program.length === 0)
|
|
2735
|
+
return false;
|
|
2736
|
+
if (path10.isAbsolute(program))
|
|
2737
|
+
return true;
|
|
2738
|
+
return (config.args ?? []).some((arg, index) => index === 0 && path10.isAbsolute(arg));
|
|
2739
|
+
}
|
|
2740
|
+
function launcherRepair(config, launch) {
|
|
2741
|
+
if (launch?.launcherPath == null || launch.launcherPath.length === 0)
|
|
2742
|
+
return null;
|
|
2743
|
+
const launcher = launch.launcherPath;
|
|
2744
|
+
const stored = entryScript(config);
|
|
2745
|
+
if (stored === null || stored === launcher)
|
|
2746
|
+
return null;
|
|
2747
|
+
if (stored !== launch.cliEntry)
|
|
2748
|
+
return null;
|
|
2749
|
+
const rest = (config.args ?? []).filter((arg, index) => !(index === 0 && path10.isAbsolute(arg)));
|
|
2750
|
+
return { command: process7.execPath, args: [launcher, ...rest] };
|
|
2751
|
+
}
|
|
2752
|
+
function buildDshEntry(facts) {
|
|
2753
|
+
const launch = facts.launch;
|
|
2754
|
+
const profile = facts.profile === null || facts.profile === void 0 ? "web" : facts.profile;
|
|
2755
|
+
const appArgs = [];
|
|
2756
|
+
if (facts.port !== null) {
|
|
2757
|
+
appArgs.push(
|
|
2758
|
+
"--port",
|
|
2759
|
+
"{port}",
|
|
2760
|
+
"--host",
|
|
2761
|
+
"{host}",
|
|
2762
|
+
"--no-open",
|
|
2763
|
+
"--trusted-host",
|
|
2764
|
+
`localhost:{port}`
|
|
2765
|
+
);
|
|
2766
|
+
if (facts.host === "0.0.0.0")
|
|
2767
|
+
appArgs.push("--trusted-host", "{lanIp}:{port}");
|
|
2768
|
+
}
|
|
2769
|
+
const launcherArgs = profile === "web" ? ["web"] : ["--profile", profile];
|
|
2770
|
+
const generated = launch === null ? { command: "dsh", entryArgs: [] } : buildCommand(launch, facts.launcherPath ?? null);
|
|
2771
|
+
return {
|
|
2772
|
+
id: facts.id,
|
|
2773
|
+
label: "DSH web",
|
|
2774
|
+
enabled: true,
|
|
2775
|
+
autostart: true,
|
|
2776
|
+
command: generated.command,
|
|
2777
|
+
args: [...generated.entryArgs, ...launcherArgs, ...appArgs],
|
|
2778
|
+
bind: facts.host === "0.0.0.0" ? "lan" : "local",
|
|
2779
|
+
port: facts.port,
|
|
2780
|
+
cwd: process7.cwd(),
|
|
2781
|
+
env: {},
|
|
2782
|
+
dataEnvs: { DSH_HOME: facts.dshHome },
|
|
2783
|
+
onPortConflict: "kill",
|
|
2784
|
+
stop: { killPortHolders: true },
|
|
2785
|
+
health: {
|
|
2786
|
+
enabled: true,
|
|
2787
|
+
mode: "http",
|
|
2788
|
+
// 401 is normal here: the panel answers the login route when auth is on.
|
|
2789
|
+
http: { path: "/", method: "GET", expectStatusBelow: 500, expectBody: "" }
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// src/home-hosted/entries.ts
|
|
2795
|
+
function defaultOnPortConflict(platform = process.platform) {
|
|
2796
|
+
return platform === "win32" ? "kill" : "follow";
|
|
2797
|
+
}
|
|
2798
|
+
function defaultIntent(id, platform = process.platform) {
|
|
2799
|
+
return {
|
|
2800
|
+
id,
|
|
2801
|
+
autostart: true,
|
|
2802
|
+
onPortConflict: defaultOnPortConflict(platform),
|
|
2803
|
+
stopKillPortHolders: true,
|
|
2804
|
+
persistent: true
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
function ownedPatch(intent, live, options = {}) {
|
|
2808
|
+
const stop = live?.stop ?? {};
|
|
2809
|
+
return {
|
|
2810
|
+
autostart: intent.autostart,
|
|
2811
|
+
onPortConflict: intent.onPortConflict,
|
|
2812
|
+
...options.persistent === false ? {} : { persistent: intent.persistent },
|
|
2813
|
+
stop: { ...stop, killPortHolders: intent.stopKillPortHolders }
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
function ownedDrift(live, intent, options = {}) {
|
|
2817
|
+
if (live === null)
|
|
2818
|
+
return ["missing entry"];
|
|
2819
|
+
const drift = [];
|
|
2820
|
+
if (live.autostart !== intent.autostart)
|
|
2821
|
+
drift.push("autostart");
|
|
2822
|
+
if (live.onPortConflict !== intent.onPortConflict)
|
|
2823
|
+
drift.push("onPortConflict");
|
|
2824
|
+
if (options.persistent !== false && (live.persistent ?? false) !== intent.persistent)
|
|
2825
|
+
drift.push("persistent");
|
|
2826
|
+
const killPortHolders = live.stop?.killPortHolders;
|
|
2827
|
+
if (killPortHolders !== intent.stopKillPortHolders)
|
|
2828
|
+
drift.push("stop.killPortHolders");
|
|
2829
|
+
return drift;
|
|
2830
|
+
}
|
|
2831
|
+
function snapshotOwned(live) {
|
|
2832
|
+
const killPortHolders = live.stop?.killPortHolders;
|
|
2833
|
+
return {
|
|
2834
|
+
id: live.id,
|
|
2835
|
+
autostart: typeof live.autostart === "boolean" ? live.autostart : false,
|
|
2836
|
+
onPortConflict: isOnPortConflict(live.onPortConflict) ? live.onPortConflict : "block",
|
|
2837
|
+
persistent: live.persistent === true,
|
|
2838
|
+
stop: { killPortHolders: typeof killPortHolders === "boolean" ? killPortHolders : false }
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
function restorePatch(live, snapshot) {
|
|
2842
|
+
const stop = live.stop ?? {};
|
|
2843
|
+
const previous = snapshot?.stop ?? {};
|
|
2844
|
+
return {
|
|
2845
|
+
autostart: snapshot?.autostart ?? false,
|
|
2846
|
+
onPortConflict: snapshot?.onPortConflict ?? "block",
|
|
2847
|
+
persistent: snapshot?.persistent === true,
|
|
2848
|
+
stop: {
|
|
2849
|
+
...stop,
|
|
2850
|
+
killPortHolders: typeof previous.killPortHolders === "boolean" ? previous.killPortHolders : false
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2553
2854
|
|
|
2554
2855
|
// src/home-hosted/panel-control.ts
|
|
2555
2856
|
import { spawn as spawn2 } from "node:child_process";
|
|
2556
|
-
import
|
|
2857
|
+
import fs12 from "node:fs";
|
|
2557
2858
|
import path11 from "node:path";
|
|
2558
2859
|
import process8 from "node:process";
|
|
2559
2860
|
function cliArgs(deps, command) {
|
|
@@ -2642,7 +2943,7 @@ log('started with exit ' + String(result.status))
|
|
|
2642
2943
|
function writeTakeoverHelper(deps, oldPid) {
|
|
2643
2944
|
const file = takeoverHelperPath(deps.stateDir);
|
|
2644
2945
|
writeFileAtomic(file, buildTakeoverSource(deps, oldPid), 493);
|
|
2645
|
-
|
|
2946
|
+
fs12.chmodSync(file, 493);
|
|
2646
2947
|
return file;
|
|
2647
2948
|
}
|
|
2648
2949
|
function spawnDetached(program, args) {
|
|
@@ -2760,14 +3061,19 @@ var PanelClient = class {
|
|
|
2760
3061
|
return await this.request("POST", `/api/servers/${encodeURIComponent(id)}/free-port`);
|
|
2761
3062
|
}
|
|
2762
3063
|
};
|
|
2763
|
-
async function
|
|
3064
|
+
async function probeToken(baseUrl, token, timeoutMs = 5e3) {
|
|
2764
3065
|
try {
|
|
2765
3066
|
await new PanelClient({ baseUrl, token, timeoutMs }).listServers();
|
|
2766
|
-
return
|
|
2767
|
-
} catch {
|
|
2768
|
-
|
|
3067
|
+
return "ok";
|
|
3068
|
+
} catch (error) {
|
|
3069
|
+
if (error instanceof PanelError && (error.code === "AUTH_REQUIRED" || error.status === 401 || error.status === 403))
|
|
3070
|
+
return "refused";
|
|
3071
|
+
return "unreachable";
|
|
2769
3072
|
}
|
|
2770
3073
|
}
|
|
3074
|
+
async function verifyToken(baseUrl, token, timeoutMs = 5e3) {
|
|
3075
|
+
return await probeToken(baseUrl, token, timeoutMs) === "ok";
|
|
3076
|
+
}
|
|
2771
3077
|
|
|
2772
3078
|
// src/home-hosted/runtime.ts
|
|
2773
3079
|
import http from "node:http";
|
|
@@ -2863,18 +3169,35 @@ function apiTokenEnrolled(home) {
|
|
|
2863
3169
|
const secrets = readJson(secretsFile(home));
|
|
2864
3170
|
return secrets !== null && secrets.apiToken !== null && secrets.apiToken !== void 0;
|
|
2865
3171
|
}
|
|
2866
|
-
var
|
|
3172
|
+
var queue = /* @__PURE__ */ new Map();
|
|
3173
|
+
function serialised(stateDir, work) {
|
|
3174
|
+
const key = path12.resolve(stateDir);
|
|
3175
|
+
const previous = queue.get(key) ?? Promise.resolve();
|
|
3176
|
+
const next = previous.then(work, work);
|
|
3177
|
+
const settled = next.then(() => void 0, () => void 0);
|
|
3178
|
+
queue.set(key, settled);
|
|
3179
|
+
void settled.then(() => {
|
|
3180
|
+
if (queue.get(key) === settled) queue.delete(key);
|
|
3181
|
+
});
|
|
3182
|
+
return next;
|
|
3183
|
+
}
|
|
2867
3184
|
async function ensureToken(options) {
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3185
|
+
return await serialised(options.stateDir, async () => await enrollToken(options));
|
|
3186
|
+
}
|
|
3187
|
+
async function reclaimToken(options) {
|
|
3188
|
+
return await serialised(options.stateDir, async () => await enrollFreshToken(options));
|
|
3189
|
+
}
|
|
3190
|
+
async function runCli2(exec, args, env) {
|
|
2874
3191
|
try {
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
3192
|
+
const result = await exec(args, env);
|
|
3193
|
+
if (result.code !== 0) {
|
|
3194
|
+
return {
|
|
3195
|
+
failure: result.error?.trim() || result.stderr.trim() || result.stdout.trim() || `exit ${String(result.code)}`
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
return { result };
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
return { failure: error instanceof Error ? error.message : String(error) };
|
|
2878
3201
|
}
|
|
2879
3202
|
}
|
|
2880
3203
|
async function enrollToken(options) {
|
|
@@ -2889,20 +3212,34 @@ async function enrollToken(options) {
|
|
|
2889
3212
|
};
|
|
2890
3213
|
}
|
|
2891
3214
|
const token = generateToken();
|
|
2892
|
-
const
|
|
2893
|
-
if (result
|
|
3215
|
+
const enrolled = await runCli2(options.exec, ["--home", options.home, "set-token"], { HHOSTED_TOKEN: token });
|
|
3216
|
+
if (enrolled.result === void 0) {
|
|
2894
3217
|
return {
|
|
2895
3218
|
token: null,
|
|
2896
3219
|
enrolled: false,
|
|
2897
|
-
detail: `could not enrol an API token: ${
|
|
3220
|
+
detail: `could not enrol an API token: ${enrolled.failure ?? "the CLI did not answer"}`
|
|
2898
3221
|
};
|
|
2899
3222
|
}
|
|
2900
3223
|
storeToken(options.stateDir, token);
|
|
2901
3224
|
return { token, enrolled: true, detail: "enrolled a new panel API token" };
|
|
2902
3225
|
}
|
|
3226
|
+
async function enrollFreshToken(options) {
|
|
3227
|
+
const token = generateToken();
|
|
3228
|
+
const enrolled = await runCli2(options.exec, ["--home", options.home, "set-token"], { HHOSTED_TOKEN: token });
|
|
3229
|
+
if (enrolled.result === void 0) {
|
|
3230
|
+
return {
|
|
3231
|
+
token: null,
|
|
3232
|
+
detail: `could not enrol a fresh API token: ${enrolled.failure ?? "the CLI did not answer"}; the previously stored token was kept`
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
storeToken(options.stateDir, token);
|
|
3236
|
+
return { token, detail: "enrolled a fresh panel API token" };
|
|
3237
|
+
}
|
|
2903
3238
|
|
|
2904
3239
|
// src/service.ts
|
|
2905
3240
|
var ENTRY_ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3241
|
+
var TOKEN_PROBE_TIMEOUT_MS = 3e3;
|
|
3242
|
+
var ENTRY_RECOVERY_INTERVAL_MS = 3e4;
|
|
2906
3243
|
function pluginRoot() {
|
|
2907
3244
|
try {
|
|
2908
3245
|
return path13.dirname(path13.dirname(fileURLToPath(import.meta.url)));
|
|
@@ -2924,8 +3261,18 @@ var HomeHostedService = class extends Service {
|
|
|
2924
3261
|
this.snapshotsFile = path13.join(options.stateDir, "snapshots.json");
|
|
2925
3262
|
}
|
|
2926
3263
|
clientCache = null;
|
|
3264
|
+
/** The last token this service proved against a panel, so a poll does not re-probe it. */
|
|
3265
|
+
tokenProof = null;
|
|
2927
3266
|
tokenDetail = "";
|
|
3267
|
+
/** When a missing managed entry was last put back, so a poll cannot become a write loop. */
|
|
3268
|
+
entryRecoveryAt = 0;
|
|
2928
3269
|
snapshotsFile;
|
|
3270
|
+
async resolveDsh(dshHome2) {
|
|
3271
|
+
return await (this.options.resolveDsh ?? ((options) => resolveDshLaunch(options)))({
|
|
3272
|
+
dshHome: dshHome2,
|
|
3273
|
+
stateDir: this.options.stateDir
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
2929
3276
|
// -------------------------------------------------------------------------
|
|
2930
3277
|
// Facts
|
|
2931
3278
|
// -------------------------------------------------------------------------
|
|
@@ -3019,14 +3366,40 @@ var HomeHostedService = class extends Service {
|
|
|
3019
3366
|
throw new HomeHostedError(read.error, "CONFIG_UNREADABLE");
|
|
3020
3367
|
writeConfig(this.options.home, patchControl(read.raw ?? {}, { port }), this.writtenBy());
|
|
3021
3368
|
}
|
|
3369
|
+
/** Whether this exact token was already proved against this exact panel. */
|
|
3370
|
+
tokenProven(url, token) {
|
|
3371
|
+
return this.tokenProof !== null && this.tokenProof.until > Date.now() && this.tokenProof.url === url && this.tokenProof.token === token;
|
|
3372
|
+
}
|
|
3373
|
+
proveToken(url, token) {
|
|
3374
|
+
this.tokenProof = { url, token, until: Date.now() + 3e4 };
|
|
3375
|
+
}
|
|
3022
3376
|
async panelStatus() {
|
|
3023
3377
|
const runtime = this.runtime();
|
|
3024
3378
|
const stored = readStoredToken(this.options.stateDir);
|
|
3025
3379
|
const enrolledOnDisk = apiTokenEnrolled(this.options.home);
|
|
3026
3380
|
const reachable = runtime !== null && (pidAlive(runtime.pid) || await probePanel(runtime.url));
|
|
3027
3381
|
const answered = runtime !== null && await probePanel(runtime.url);
|
|
3028
|
-
|
|
3029
|
-
|
|
3382
|
+
let token = "unknown";
|
|
3383
|
+
let tokenVerified = false;
|
|
3384
|
+
if (answered) {
|
|
3385
|
+
if (stored === null) {
|
|
3386
|
+
token = enrolledOnDisk ? "present" : "absent";
|
|
3387
|
+
} else if (this.tokenProven(runtime.url, stored)) {
|
|
3388
|
+
token = "enrolled";
|
|
3389
|
+
tokenVerified = true;
|
|
3390
|
+
} else {
|
|
3391
|
+
const probe = await probeToken(runtime.url, stored, TOKEN_PROBE_TIMEOUT_MS);
|
|
3392
|
+
if (probe === "ok") {
|
|
3393
|
+
token = "enrolled";
|
|
3394
|
+
tokenVerified = true;
|
|
3395
|
+
this.proveToken(runtime.url, stored);
|
|
3396
|
+
} else if (probe === "refused") {
|
|
3397
|
+
token = "stale";
|
|
3398
|
+
tokenVerified = true;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
const writeVia = token === "enrolled" ? "api" : "file";
|
|
3030
3403
|
return {
|
|
3031
3404
|
home: this.options.home,
|
|
3032
3405
|
reachable: answered,
|
|
@@ -3036,9 +3409,55 @@ var HomeHostedService = class extends Service {
|
|
|
3036
3409
|
pid: runtime?.pid ?? null,
|
|
3037
3410
|
writeVia,
|
|
3038
3411
|
token,
|
|
3039
|
-
|
|
3412
|
+
tokenVerified,
|
|
3413
|
+
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 but no API token is enrolled for it yet; writes go to servers.config.json until one is" : "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" : token === "present" || token === "stale" ? "the panel process is up but does not answer with this plugin's API token, so this page cannot reach it" : "the panel process is alive but is not answering"
|
|
3040
3414
|
};
|
|
3041
3415
|
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Replace the panel's API token with a fresh one and prove it.
|
|
3418
|
+
*
|
|
3419
|
+
* home-hosted keeps only a hash, so a token this plugin does not hold cannot
|
|
3420
|
+
* be recovered: a new token replaces the old hash in one CLI write, and it is
|
|
3421
|
+
* proved against the answering panel before being reported.
|
|
3422
|
+
*
|
|
3423
|
+
* The panel is not required to be *answering*: a missing or refused token is a
|
|
3424
|
+
* common reason it cannot be reached at all, and refusing the repair for that
|
|
3425
|
+
* reason would leave no way out from the page. Only a panel that was never
|
|
3426
|
+
* started is refused — there is nothing to enrol a token against yet.
|
|
3427
|
+
*/
|
|
3428
|
+
async reclaimPanelToken() {
|
|
3429
|
+
const runtime = this.runtime();
|
|
3430
|
+
if (runtime === null) {
|
|
3431
|
+
throw new HomeHostedError(
|
|
3432
|
+
"the panel has not been started, so there is nothing to enrol a token against; start it first",
|
|
3433
|
+
"PANEL_UNAVAILABLE"
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
3436
|
+
const answering = await probePanel(runtime.url);
|
|
3437
|
+
const result = await reclaimToken({
|
|
3438
|
+
home: this.options.home,
|
|
3439
|
+
stateDir: this.options.stateDir,
|
|
3440
|
+
exec: (args, env) => this.cliExec(args, env)
|
|
3441
|
+
});
|
|
3442
|
+
this.clientCache = null;
|
|
3443
|
+
this.tokenProof = null;
|
|
3444
|
+
this.tokenDetail = result.detail;
|
|
3445
|
+
if (result.token === null)
|
|
3446
|
+
throw new HomeHostedError(result.detail, "TOKEN_RECLAIM_FAILED");
|
|
3447
|
+
if (!answering) {
|
|
3448
|
+
this.tokenDetail = `${result.detail}; the panel is not answering, so it could not be proved yet`;
|
|
3449
|
+
return await this.status();
|
|
3450
|
+
}
|
|
3451
|
+
const probe = await probeToken(runtime.url, result.token, TOKEN_PROBE_TIMEOUT_MS);
|
|
3452
|
+
if (probe !== "ok") {
|
|
3453
|
+
throw new HomeHostedError(
|
|
3454
|
+
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",
|
|
3455
|
+
"TOKEN_RECLAIM_UNVERIFIED"
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
this.proveToken(runtime.url, result.token);
|
|
3459
|
+
return await this.status();
|
|
3460
|
+
}
|
|
3042
3461
|
async tryClient() {
|
|
3043
3462
|
if (this.clientCache !== null && this.clientCache.until > Date.now())
|
|
3044
3463
|
return this.clientCache.client;
|
|
@@ -3057,6 +3476,7 @@ var HomeHostedService = class extends Service {
|
|
|
3057
3476
|
return null;
|
|
3058
3477
|
const client = new PanelClient({ baseUrl: runtime.url, token: ensured.token });
|
|
3059
3478
|
this.clientCache = { client, until: Date.now() + 3e4 };
|
|
3479
|
+
this.proveToken(runtime.url, ensured.token);
|
|
3060
3480
|
return client;
|
|
3061
3481
|
}
|
|
3062
3482
|
async requireClient() {
|
|
@@ -3095,20 +3515,35 @@ var HomeHostedService = class extends Service {
|
|
|
3095
3515
|
map.set(entry.id, { view: { id: entry.id, status: "unknown", pid: null, url: null, config: entry }, config: entry });
|
|
3096
3516
|
return map;
|
|
3097
3517
|
}
|
|
3098
|
-
async createEntry(intent, patch
|
|
3518
|
+
async createEntry(intent, patch) {
|
|
3099
3519
|
if (intent.id !== this.options.defaultEntryId)
|
|
3100
3520
|
return null;
|
|
3101
|
-
const
|
|
3521
|
+
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
3522
|
+
const dsh = await this.resolveDsh(harnessHome);
|
|
3102
3523
|
const { port, host } = this.webServer();
|
|
3103
3524
|
const generated = buildDshEntry({
|
|
3104
3525
|
id: intent.id,
|
|
3105
3526
|
port,
|
|
3106
3527
|
host,
|
|
3107
3528
|
profile: detectProfile(process10.argv, process10.env),
|
|
3108
|
-
dshHome:
|
|
3109
|
-
launch: dsh
|
|
3529
|
+
dshHome: harnessHome,
|
|
3530
|
+
launch: dsh,
|
|
3531
|
+
launcherPath: dsh?.launcherPath ?? null
|
|
3110
3532
|
});
|
|
3111
|
-
return { ...
|
|
3533
|
+
return { ...generated, ...patch };
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* An entry written before the launcher existed still runs a clone's build
|
|
3537
|
+
* output directly. Point it at the stable launcher, or the fix would only
|
|
3538
|
+
* ever apply to freshly created entries — the plugin never rewrites an
|
|
3539
|
+
* existing entry's command.
|
|
3540
|
+
*/
|
|
3541
|
+
async dshCommandRepair(live) {
|
|
3542
|
+
if (live === null || !needsLauncherRepair(live))
|
|
3543
|
+
return null;
|
|
3544
|
+
const harnessHome = process10.env.DSH_HOME ?? dshHome();
|
|
3545
|
+
const dsh = await this.resolveDsh(harnessHome);
|
|
3546
|
+
return launcherRepair(live, dsh);
|
|
3112
3547
|
}
|
|
3113
3548
|
async writeOwned(intent) {
|
|
3114
3549
|
const live = (await this.liveEntries()).get(intent.id) ?? null;
|
|
@@ -3118,13 +3553,14 @@ var HomeHostedService = class extends Service {
|
|
|
3118
3553
|
this.saveSnapshots(snapshots);
|
|
3119
3554
|
}
|
|
3120
3555
|
const patch = ownedPatch(intent, live?.config ?? null, { persistent: await this.supportsPersistent() });
|
|
3556
|
+
const repair = intent.id === this.options.defaultEntryId ? await this.dshCommandRepair(live?.config ?? null) : null;
|
|
3121
3557
|
const client = await this.tryClient();
|
|
3122
3558
|
if (client !== null) {
|
|
3123
3559
|
if (live !== null) {
|
|
3124
|
-
await client.updateServer(intent.id, patch);
|
|
3560
|
+
await client.updateServer(intent.id, { ...patch, ...repair ?? {} });
|
|
3125
3561
|
return;
|
|
3126
3562
|
}
|
|
3127
|
-
const entry2 = await this.createEntry(intent, patch
|
|
3563
|
+
const entry2 = await this.createEntry(intent, patch);
|
|
3128
3564
|
if (entry2 === null) {
|
|
3129
3565
|
throw new HomeHostedError(
|
|
3130
3566
|
`no server "${intent.id}" exists; create it first, then this plugin can adopt its autostart and conflict policy`,
|
|
@@ -3139,10 +3575,10 @@ var HomeHostedService = class extends Service {
|
|
|
3139
3575
|
throw new HomeHostedError(read.error, "CONFIG_UNREADABLE");
|
|
3140
3576
|
let raw = read.raw ?? {};
|
|
3141
3577
|
if (findEntry(raw, intent.id) !== null) {
|
|
3142
|
-
writeConfig(this.options.home, patchEntry(raw, intent.id, patch), this.writtenBy());
|
|
3578
|
+
writeConfig(this.options.home, patchEntry(raw, intent.id, { ...patch, ...repair ?? {} }), this.writtenBy());
|
|
3143
3579
|
return;
|
|
3144
3580
|
}
|
|
3145
|
-
const entry = await this.createEntry(intent, patch
|
|
3581
|
+
const entry = await this.createEntry(intent, patch);
|
|
3146
3582
|
if (entry === null)
|
|
3147
3583
|
throw new HomeHostedError(`no server "${intent.id}" exists in ${this.options.home}/servers.config.json`, "ENTRY_MISSING");
|
|
3148
3584
|
if (await this.panelRunning()) {
|
|
@@ -3211,8 +3647,9 @@ var HomeHostedService = class extends Service {
|
|
|
3211
3647
|
entries.push(intent);
|
|
3212
3648
|
this.options.settings.update({
|
|
3213
3649
|
// The page's toggle reads this flag, so managing the harness has to record
|
|
3214
|
-
// itself; restoring or removing that entry clears it again.
|
|
3215
|
-
|
|
3650
|
+
// itself; restoring or removing that entry clears it again. A paused intent
|
|
3651
|
+
// (`autostart: false`) is not management, or the toggle could never go off.
|
|
3652
|
+
...intent.id === this.options.defaultEntryId ? { manageDsh: intent.autostart } : {},
|
|
3216
3653
|
entries
|
|
3217
3654
|
});
|
|
3218
3655
|
}
|
|
@@ -3301,7 +3738,7 @@ var HomeHostedService = class extends Service {
|
|
|
3301
3738
|
case "switch": {
|
|
3302
3739
|
if (file === void 0 || file.trim().length === 0)
|
|
3303
3740
|
throw new HomeHostedError("switching the UI needs the zip to install", "UI_FILE_REQUIRED");
|
|
3304
|
-
if (!
|
|
3741
|
+
if (!fs13.existsSync(file))
|
|
3305
3742
|
throw new HomeHostedError(`no file at ${file}`, "UI_FILE_MISSING");
|
|
3306
3743
|
const result = await runUi(["ui-switch", "--file", file, "--yes"]);
|
|
3307
3744
|
return { ok: result.code === 0, detail: result.code === 0 ? "the UI was replaced" : "the switch did not run", ui: active(), output: result.output };
|
|
@@ -3519,13 +3956,43 @@ var HomeHostedService = class extends Service {
|
|
|
3519
3956
|
* an explicit click or an approved tool call, not as a side effect of startup.
|
|
3520
3957
|
*/
|
|
3521
3958
|
async reconcile() {
|
|
3959
|
+
await this.ensureManagedEntry();
|
|
3960
|
+
await this.repairBootEntry();
|
|
3961
|
+
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Put back a managed entry that no longer exists, while its intent still wants
|
|
3964
|
+
* autostart. A paused intent is a deliberate stop and is left alone.
|
|
3965
|
+
*
|
|
3966
|
+
* Called from startup `reconcile()` as well as the status read, because a person
|
|
3967
|
+
* who deletes the entry from the panel is looking at the page right then — not
|
|
3968
|
+
* at the next plugin start. A failed attempt is not repeated for a while, so a
|
|
3969
|
+
* config that cannot be written does not turn every poll into a write.
|
|
3970
|
+
*/
|
|
3971
|
+
async ensureManagedEntry() {
|
|
3972
|
+
const { settings } = this.options;
|
|
3973
|
+
const id = this.options.defaultEntryId;
|
|
3974
|
+
if (!settings.get().manageDsh || !settings.intentFor(id).autostart)
|
|
3975
|
+
return;
|
|
3976
|
+
const now = Date.now();
|
|
3977
|
+
if (now - this.entryRecoveryAt < ENTRY_RECOVERY_INTERVAL_MS)
|
|
3978
|
+
return;
|
|
3979
|
+
this.entryRecoveryAt = now;
|
|
3980
|
+
if ((await this.entriesStatus()).every((entry) => entry.intent.id !== id || entry.exists))
|
|
3981
|
+
return;
|
|
3982
|
+
try {
|
|
3983
|
+
await this.writeOwned({ ...settings.intentFor(id), autostart: true });
|
|
3984
|
+
} catch {
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
/** Re-assert the boot entry when the OS still has it but it stopped working. */
|
|
3988
|
+
async repairBootEntry() {
|
|
3522
3989
|
if (!this.options.settings.get().autostart.enabled)
|
|
3523
3990
|
return;
|
|
3524
3991
|
const status = await this.bootStatus();
|
|
3525
3992
|
if (status.state !== "enabled-failing" && status.state !== "installed-disabled")
|
|
3526
3993
|
return;
|
|
3527
3994
|
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 || !
|
|
3995
|
+
if (fileBacked && (status.unitPath === null || !fs13.existsSync(status.unitPath)))
|
|
3529
3996
|
return;
|
|
3530
3997
|
try {
|
|
3531
3998
|
await this.installBoot();
|
|
@@ -3564,7 +4031,9 @@ var HomeHostedService = class extends Service {
|
|
|
3564
4031
|
defaultEntryId: this.options.defaultEntryId,
|
|
3565
4032
|
panel,
|
|
3566
4033
|
boot: await this.bootStatus(),
|
|
3567
|
-
|
|
4034
|
+
// A deleted managed entry is put back here as well as at startup: whoever
|
|
4035
|
+
// deleted it is looking at the page that reports it missing.
|
|
4036
|
+
entries: await this.ensureManagedEntry().then(async () => await this.entriesStatus()),
|
|
3568
4037
|
servers,
|
|
3569
4038
|
settings: this.options.settings.get(),
|
|
3570
4039
|
cli,
|
|
@@ -3638,6 +4107,8 @@ var HomeHostedService = class extends Service {
|
|
|
3638
4107
|
return await this.startPanelNow();
|
|
3639
4108
|
case "panel.takeover":
|
|
3640
4109
|
return await this.takeoverPanel(input.force === true);
|
|
4110
|
+
case "panel.reclaimToken":
|
|
4111
|
+
return await this.reclaimPanelToken();
|
|
3641
4112
|
case "cli.installGlobal":
|
|
3642
4113
|
return await this.installGlobalCli();
|
|
3643
4114
|
default:
|
|
@@ -3831,6 +4302,8 @@ function normalize(raw, fallbackEntryId) {
|
|
|
3831
4302
|
port: typeof raw?.panel?.port === "number" && Number.isInteger(raw.panel.port) && raw.panel.port >= 1 && raw.panel.port <= 65535 ? raw.panel.port : null
|
|
3832
4303
|
},
|
|
3833
4304
|
authNotice: raw?.authNotice === void 0 ? DEFAULT_SETTINGS.authNotice : raw.authNotice === true,
|
|
4305
|
+
// Absent means on: a stale token is a fault to repair, not a setting to opt into.
|
|
4306
|
+
reclaimToken: raw?.reclaimToken === void 0 ? DEFAULT_SETTINGS.reclaimToken : raw.reclaimToken === true,
|
|
3834
4307
|
uiStyle: raw?.uiStyle === "compact" ? "compact" : "detailed",
|
|
3835
4308
|
agentTools: {
|
|
3836
4309
|
// Absent means the default (on), and so does the pair the previous
|
|
@@ -3868,6 +4341,7 @@ var SettingsStore = class {
|
|
|
3868
4341
|
entries: patch.entries ?? this.current.entries,
|
|
3869
4342
|
panel: { ...this.current.panel, ...patch.panel ?? {} },
|
|
3870
4343
|
authNotice: patch.authNotice ?? this.current.authNotice,
|
|
4344
|
+
reclaimToken: patch.reclaimToken ?? this.current.reclaimToken,
|
|
3871
4345
|
uiStyle: patch.uiStyle ?? this.current.uiStyle,
|
|
3872
4346
|
agentTools: { ...this.current.agentTools, ...patch.agentTools ?? {} },
|
|
3873
4347
|
cli: { ...this.current.cli, ...patch.cli ?? {} }
|
|
@@ -4001,7 +4475,49 @@ function toolNameFor(name2) {
|
|
|
4001
4475
|
var READ_ONLY_ACTIONS = {
|
|
4002
4476
|
ui_manage: (input) => stringArg(input, "action") === "status"
|
|
4003
4477
|
};
|
|
4004
|
-
|
|
4478
|
+
var TOKEN_REFUSAL_CODES = /* @__PURE__ */ new Set([
|
|
4479
|
+
"AUTH_REQUIRED",
|
|
4480
|
+
"AUTH_UNARMED",
|
|
4481
|
+
"UNAUTHORIZED",
|
|
4482
|
+
"FORBIDDEN",
|
|
4483
|
+
"INVALID_TOKEN",
|
|
4484
|
+
"TOKEN_STALE",
|
|
4485
|
+
"TOKEN_REFUSED"
|
|
4486
|
+
]);
|
|
4487
|
+
function errorCode(error) {
|
|
4488
|
+
const code = error?.code;
|
|
4489
|
+
return typeof code === "string" ? code : null;
|
|
4490
|
+
}
|
|
4491
|
+
function errorStatus(error) {
|
|
4492
|
+
const status = error?.status;
|
|
4493
|
+
return typeof status === "number" ? status : null;
|
|
4494
|
+
}
|
|
4495
|
+
function refusedByPanel(error) {
|
|
4496
|
+
const status = errorStatus(error);
|
|
4497
|
+
if (status === 401 || status === 403)
|
|
4498
|
+
return true;
|
|
4499
|
+
const code = errorCode(error);
|
|
4500
|
+
return code !== null && TOKEN_REFUSAL_CODES.has(code);
|
|
4501
|
+
}
|
|
4502
|
+
async function tokenRefused(service, error) {
|
|
4503
|
+
if (refusedByPanel(error))
|
|
4504
|
+
return true;
|
|
4505
|
+
if (errorCode(error) !== "PANEL_UNAVAILABLE")
|
|
4506
|
+
return false;
|
|
4507
|
+
try {
|
|
4508
|
+
const status = await service.call("status", {});
|
|
4509
|
+
return status?.panel.token === "stale";
|
|
4510
|
+
} catch {
|
|
4511
|
+
return false;
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
function messageOf(error) {
|
|
4515
|
+
return error instanceof Error ? error.message : String(error);
|
|
4516
|
+
}
|
|
4517
|
+
function failureText(error) {
|
|
4518
|
+
return `failed: ${messageOf(error)}`;
|
|
4519
|
+
}
|
|
4520
|
+
function registerOne(ctx, service, settings, name2) {
|
|
4005
4521
|
const spec = TOOL_SPECS[name2];
|
|
4006
4522
|
const toolName = toolNameFor(name2);
|
|
4007
4523
|
const mutatingTool = MUTATING_AGENT_TOOLS.includes(name2);
|
|
@@ -4030,11 +4546,27 @@ function registerOne(ctx, service, name2) {
|
|
|
4030
4546
|
if (outcome !== "allowed-once")
|
|
4031
4547
|
return `refused: approval answered "${outcome}" (session sandbox: ${mode ?? "unknown"}). ${remedy}`;
|
|
4032
4548
|
}
|
|
4549
|
+
let request;
|
|
4033
4550
|
try {
|
|
4034
|
-
|
|
4035
|
-
return JSON.stringify(await service.call(endpoint, payload), null, 2);
|
|
4551
|
+
request = spec.run(input);
|
|
4036
4552
|
} catch (error) {
|
|
4037
|
-
return
|
|
4553
|
+
return failureText(error);
|
|
4554
|
+
}
|
|
4555
|
+
try {
|
|
4556
|
+
return JSON.stringify(await service.call(request.endpoint, request.payload), null, 2);
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
if (!settings.get().reclaimToken || !await tokenRefused(service, error))
|
|
4559
|
+
return failureText(error);
|
|
4560
|
+
try {
|
|
4561
|
+
await service.call("panel.reclaimToken", {});
|
|
4562
|
+
} catch (reclaimError) {
|
|
4563
|
+
return `${failureText(error)} (token reclaim failed: ${messageOf(reclaimError)})`;
|
|
4564
|
+
}
|
|
4565
|
+
try {
|
|
4566
|
+
return JSON.stringify(await service.call(request.endpoint, request.payload), null, 2);
|
|
4567
|
+
} catch (retryError) {
|
|
4568
|
+
return failureText(retryError);
|
|
4569
|
+
}
|
|
4038
4570
|
}
|
|
4039
4571
|
}
|
|
4040
4572
|
}));
|
|
@@ -4050,7 +4582,7 @@ function registerAgentTools(ctx, service, settings) {
|
|
|
4050
4582
|
return;
|
|
4051
4583
|
for (const name2 of current.agentTools.allow) {
|
|
4052
4584
|
try {
|
|
4053
|
-
disposers.push(registerOne(scoped, service, name2));
|
|
4585
|
+
disposers.push(registerOne(scoped, service, settings, name2));
|
|
4054
4586
|
} catch {
|
|
4055
4587
|
}
|
|
4056
4588
|
}
|