@monster-orange/den 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +179 -0
- package/README.md +9 -9
- package/docs/distributions.md +215 -65
- package/package.json +1 -1
- package/src/shared/agents/catalog-runtime.ts +178 -0
- package/src/shared/agents/catalog.ts +343 -0
- package/src/shared/agents/den-claude.ts +14 -18
- package/src/shared/agents/runtime-search.ts +142 -15
- package/src/shared/agents/runtime.ts +18 -1
- package/src/shared/agents/runtimes.ts +23 -1
- package/src/shared/auth/sudo-vault.ts +16 -0
- package/src/shared/den-config.ts +57 -3
- package/src/shared/edge/status.ts +134 -0
- package/src/shared/environment-manager/health.ts +82 -8
- package/src/shared/environment-manager/workspaces.ts +54 -0
- package/src/shared/health/not-checked.ts +42 -0
- package/src/shared/health/rollup.ts +4 -0
- package/src/shared/process/spawn-streaming.ts +8 -3
- package/src/shared/sessions/client.ts +5 -1
- package/src/shared/sessions/host.ts +29 -1
- package/src/shared/sessions/protocol.ts +6 -1
- package/src/shared/sessions/types.ts +6 -0
- package/src/shared/stacks/status.ts +24 -0
- package/src/shared/strategies/registry.ts +5 -0
- package/src/shared/types/index.ts +6 -0
- package/src/ui/cli/agent-ops.ts +210 -19
- package/src/ui/cli/agents-command.ts +50 -21
- package/src/ui/cli/edge-command.ts +16 -3
- package/src/ui/cli/health-display.ts +4 -1
- package/src/ui/mcp/scope.ts +23 -3
- package/src/ui/mcp/server.ts +31 -7
- package/src/ui/mcp/tools.ts +327 -20
- package/src/ui/mcp/views.ts +450 -0
- package/src/ui/server/agent-routes.ts +118 -28
- package/src/ui/server/index.ts +77 -6
- package/src/ui/server/public/agent-start.js +46 -7
- package/src/ui/server/public/agents-page.js +232 -97
- package/src/ui/server/public/agents-section.js +3 -5
- package/src/ui/server/public/agents.html +12 -28
- package/src/ui/server/public/graph-model.js +2 -0
- package/src/ui/server/public/header.js +2 -3
- package/src/ui/server/public/health-rollup.js +20 -8
- package/src/ui/server/public/i18n/es.js +15 -13
- package/src/ui/server/public/i18n/ja.js +14 -13
- package/src/ui/server/public/index.html +17 -4
- package/src/ui/server/public/local-dev.html +28 -17
- package/src/ui/server/public/services.html +21 -5
- package/src/ui/server/public/settings-page.js +3 -2
- package/src/ui/server/public/settings-registry.js +225 -0
- package/src/ui/server/public/settings.html +90 -1
- package/src/ui/server/public/shell.js +28 -23
- package/src/ui/server/public/topbar.js +166 -206
- package/src/ui/server/readonly-routes.ts +29 -5
- package/src/ui/server/request-guard.ts +1 -1
- package/src/ui/server/route-classes.ts +14 -2
- package/src/ui/server/session-routes.ts +9 -1
- package/src/ui/server/sudo-store.ts +170 -6
- package/src/ui/server/workspace-routes.ts +10 -3
|
@@ -142,26 +142,21 @@
|
|
|
142
142
|
"dock.err": "falló",
|
|
143
143
|
"dock.last": "Última ejecución: {s} ↗",
|
|
144
144
|
"dock.resize": "Arrastra para cambiar el tamaño",
|
|
145
|
-
"lock.
|
|
146
|
-
"lock.
|
|
147
|
-
"lock.
|
|
148
|
-
"Desbloquea para iniciar, detener y reiniciar servicios y agentes",
|
|
149
|
-
"lock.nopw":
|
|
150
|
-
"sudo sin contraseña (NOPASSWD): no hace falta la contraseña de administrador",
|
|
151
|
-
"lock.session": "Desbloqueado durante esta sesión",
|
|
152
|
-
"lock.sessionDesc":
|
|
153
|
-
"Desbloqueado durante esta sesión: iniciar, detener y reiniciar funcionan en todas las páginas.",
|
|
145
|
+
"lock.waiting": "Se necesita la contraseña de administrador",
|
|
146
|
+
"lock.needs": "{cmd} necesita tu contraseña de administrador",
|
|
147
|
+
"lock.cmd": "Un comando",
|
|
154
148
|
"lock.cfg":
|
|
155
149
|
"Guardar la contraseña está desactivado: define secrets.provider y secrets.refs.sudo en {file}.",
|
|
156
150
|
"lock.update": "Actualizar la contraseña guardada",
|
|
157
151
|
"lock.saveTo": "Guardar en {s}",
|
|
158
152
|
"lock.pw": "Contraseña de administrador",
|
|
159
|
-
"lock.go": "
|
|
153
|
+
"lock.go": "Continuar",
|
|
160
154
|
"lock.checking": "Comprobando…",
|
|
161
155
|
"lock.note":
|
|
162
|
-
"Den la
|
|
163
|
-
"lock.
|
|
164
|
-
"lock.saveFailed":
|
|
156
|
+
"Den la usa solo para sudo. Los agentes no pueden introducirla.",
|
|
157
|
+
"lock.saved": "Contraseña de administrador guardada para esta sesión",
|
|
158
|
+
"lock.saveFailed":
|
|
159
|
+
"Guardada para esta sesión, pero no se pudo guardar en {s}: {err}",
|
|
165
160
|
"lock.wrong": "Contraseña incorrecta; inténtalo de nuevo",
|
|
166
161
|
"lock.reqFailed": "La solicitud falló: ¿está en marcha el servidor?",
|
|
167
162
|
"v.title": "Ver el registro de cambios",
|
|
@@ -289,11 +284,13 @@
|
|
|
289
284
|
"set.graph.density": "Tarjetas",
|
|
290
285
|
"set.graph.panelWidth": "Panel de detalles",
|
|
291
286
|
"set.workspace.editor": "IDE de este espacio de trabajo",
|
|
287
|
+
"set.agents.runtimes": "Runtimes",
|
|
292
288
|
"set.cli.local_dev": "Entorno local",
|
|
293
289
|
"set.cli.cli_tabs": "Pestañas de terminal",
|
|
294
290
|
"set.cli.log_window_lines": "Líneas del panel de registros",
|
|
295
291
|
"set.cli.web_port": "Tu puerto web",
|
|
296
292
|
"set.cli.signin": "Inicio de sesión del navegador",
|
|
293
|
+
"set.cli.sudo": "Contraseña de administrador",
|
|
297
294
|
"set.distribution.brand": "Nombre del producto",
|
|
298
295
|
"set.distribution.github_org": "Org de GitHub",
|
|
299
296
|
"set.distribution.orgs": "Etiquetas de org",
|
|
@@ -383,5 +380,10 @@
|
|
|
383
380
|
"ag.rt.fix":
|
|
384
381
|
"Instálalo, o pon su ruta en {key} en {file}, o ejecuta {cmd}.",
|
|
385
382
|
"ag.rt.unknown": "Aquí no hay ningún runtime {id}.",
|
|
383
|
+
"ag.rt.denTools": "Herramientas de Den: próximamente",
|
|
384
|
+
"ag.rt.ownCli":
|
|
385
|
+
"Se ejecuta como su propia CLI en el workspace: las herramientas y los modos de permiso de Den llegarán más adelante.",
|
|
386
|
+
"ag.rt.notInstalled": "no instalado",
|
|
387
|
+
"ag.rt.needsArgv": "la persona no tiene plantilla argv",
|
|
386
388
|
};
|
|
387
389
|
})((window.denMessages = window.denMessages || {}));
|
|
@@ -131,27 +131,21 @@
|
|
|
131
131
|
"dock.err": "失敗",
|
|
132
132
|
"dock.last": "前回の実行: {s} ↗",
|
|
133
133
|
"dock.resize": "ドラッグしてサイズを変更",
|
|
134
|
-
"lock.
|
|
135
|
-
"lock.
|
|
136
|
-
"lock.
|
|
137
|
-
"ロックを解除するとサービスとエージェントの起動・停止・再起動ができます",
|
|
138
|
-
"lock.nopw":
|
|
139
|
-
"パスワード不要の sudo (NOPASSWD): 管理者パスワードは必要ありません",
|
|
140
|
-
"lock.session": "このセッションの間ロックを解除しました",
|
|
141
|
-
"lock.sessionDesc":
|
|
142
|
-
"このセッションの間ロック解除中: どのページでも起動・停止・再起動ができます。",
|
|
134
|
+
"lock.waiting": "管理者パスワードが必要です",
|
|
135
|
+
"lock.needs": "{cmd} に管理者パスワードが必要です",
|
|
136
|
+
"lock.cmd": "コマンド",
|
|
143
137
|
"lock.cfg":
|
|
144
138
|
"パスワードの保存はオフです: {file} で secrets.provider と secrets.refs.sudo を設定してください。",
|
|
145
139
|
"lock.update": "保存したパスワードを更新",
|
|
146
140
|
"lock.saveTo": "{s} に保存",
|
|
147
141
|
"lock.pw": "管理者パスワード",
|
|
148
|
-
"lock.go": "
|
|
142
|
+
"lock.go": "続行",
|
|
149
143
|
"lock.checking": "確認中…",
|
|
150
144
|
"lock.note":
|
|
151
|
-
"Den
|
|
152
|
-
"lock.
|
|
145
|
+
"Den はこれを sudo にだけ使います。エージェントは入力できません。",
|
|
146
|
+
"lock.saved": "このセッションの間、管理者パスワードを保持します",
|
|
153
147
|
"lock.saveFailed":
|
|
154
|
-
"
|
|
148
|
+
"このセッションの間は保持しますが、{s} への保存に失敗しました: {err}",
|
|
155
149
|
"lock.wrong": "パスワードが違います。もう一度お試しください",
|
|
156
150
|
"lock.reqFailed": "リクエストに失敗しました。サーバーは動いていますか?",
|
|
157
151
|
"v.title": "変更履歴を見る",
|
|
@@ -278,11 +272,13 @@
|
|
|
278
272
|
"set.graph.density": "カード",
|
|
279
273
|
"set.graph.panelWidth": "詳細パネル",
|
|
280
274
|
"set.workspace.editor": "このワークスペースの IDE",
|
|
275
|
+
"set.agents.runtimes": "ランタイム",
|
|
281
276
|
"set.cli.local_dev": "ローカル環境",
|
|
282
277
|
"set.cli.cli_tabs": "ターミナルのタブ",
|
|
283
278
|
"set.cli.log_window_lines": "ログ欄の行数",
|
|
284
279
|
"set.cli.web_port": "あなたの web ポート",
|
|
285
280
|
"set.cli.signin": "ブラウザのサインイン",
|
|
281
|
+
"set.cli.sudo": "管理者パスワード",
|
|
286
282
|
"set.distribution.brand": "製品名",
|
|
287
283
|
"set.distribution.github_org": "GitHub の組織",
|
|
288
284
|
"set.distribution.orgs": "組織のラベル",
|
|
@@ -368,5 +364,10 @@
|
|
|
368
364
|
"ag.rt.fix":
|
|
369
365
|
"インストールするか、{file} の {key} にパスを設定するか、{cmd} を実行してください。",
|
|
370
366
|
"ag.rt.unknown": "ランタイム {id} はここにありません。",
|
|
367
|
+
"ag.rt.denTools": "Den ツール: 近日対応",
|
|
368
|
+
"ag.rt.ownCli":
|
|
369
|
+
"ワークスペースで独自の CLI として動きます。Den のツールと権限モードは今後対応します。",
|
|
370
|
+
"ag.rt.notInstalled": "未インストール",
|
|
371
|
+
"ag.rt.needsArgv": "ペルソナに argv テンプレートがありません",
|
|
371
372
|
};
|
|
372
373
|
})((window.denMessages = window.denMessages || {}));
|
|
@@ -1160,6 +1160,7 @@
|
|
|
1160
1160
|
storeOpenLog,
|
|
1161
1161
|
initChangelog,
|
|
1162
1162
|
envReady,
|
|
1163
|
+
sudoRun,
|
|
1163
1164
|
} from "/header.js";
|
|
1164
1165
|
import { ownersLabel } from "/workspace-picker.js";
|
|
1165
1166
|
import {
|
|
@@ -2128,10 +2129,16 @@
|
|
|
2128
2129
|
|
|
2129
2130
|
pane.innerHTML = services
|
|
2130
2131
|
.map((s) => {
|
|
2132
|
+
// The row's own workspace (the first in view that defines it):
|
|
2133
|
+
// its command runs there, not in the focused one.
|
|
2134
|
+
const ws = s.inWorkspaces?.[0];
|
|
2135
|
+
const wsAttr = ws
|
|
2136
|
+
? ` data-workspace="${String(ws).replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`)}"`
|
|
2137
|
+
: "";
|
|
2131
2138
|
const cmdBtns = s.commands
|
|
2132
2139
|
.map(
|
|
2133
2140
|
(cmd) =>
|
|
2134
|
-
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
|
|
2141
|
+
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec"${wsAttr}>${cmd}</button>`,
|
|
2135
2142
|
)
|
|
2136
2143
|
.join("");
|
|
2137
2144
|
const logsBtn = s.logs
|
|
@@ -2156,8 +2163,8 @@
|
|
|
2156
2163
|
|
|
2157
2164
|
pane.querySelectorAll(".cmd-btn").forEach((btn) => {
|
|
2158
2165
|
btn.addEventListener("click", () => {
|
|
2159
|
-
const { repo, service, cmd, type } = btn.dataset;
|
|
2160
|
-
if (type === "exec") runExec(repo, service, cmd);
|
|
2166
|
+
const { repo, service, cmd, type, workspace } = btn.dataset;
|
|
2167
|
+
if (type === "exec") runExec(repo, service, cmd, workspace);
|
|
2161
2168
|
else {
|
|
2162
2169
|
const env = document.getElementById("env-sel").value;
|
|
2163
2170
|
storeOpenLog(repo, service, env);
|
|
@@ -2201,7 +2208,7 @@
|
|
|
2201
2208
|
}
|
|
2202
2209
|
|
|
2203
2210
|
// ── Service exec ──────────────────────────────────────────────────────────
|
|
2204
|
-
function runExec(repo, service, cmd) {
|
|
2211
|
+
function runExec(repo, service, cmd, workspace) {
|
|
2205
2212
|
if (execEs) {
|
|
2206
2213
|
execEs.close();
|
|
2207
2214
|
execEs = null;
|
|
@@ -2234,9 +2241,14 @@
|
|
|
2234
2241
|
);
|
|
2235
2242
|
|
|
2236
2243
|
const params = new URLSearchParams({ repo, service, cmd, env });
|
|
2244
|
+
if (workspace) params.set("workspace", workspace);
|
|
2237
2245
|
execEs = new EventSource(`/api/service/exec?${params}`);
|
|
2246
|
+
// LK-0: a command that needs sudo without the password asks, then
|
|
2247
|
+
// runs again.
|
|
2248
|
+
const sudo = sudoRun();
|
|
2238
2249
|
execEs.onmessage = (e) => {
|
|
2239
2250
|
const msg = JSON.parse(e.data);
|
|
2251
|
+
sudo.see(msg);
|
|
2240
2252
|
if (msg.type === "cmd") {
|
|
2241
2253
|
cmdAppendLine(msg.text, "cmd");
|
|
2242
2254
|
execDetachable = !!msg.detached;
|
|
@@ -2261,6 +2273,7 @@
|
|
|
2261
2273
|
.forEach((b) => (b.disabled = false));
|
|
2262
2274
|
execEs?.close();
|
|
2263
2275
|
execEs = null;
|
|
2276
|
+
void sudo.done(() => runExec(repo, service, cmd, workspace));
|
|
2264
2277
|
}
|
|
2265
2278
|
};
|
|
2266
2279
|
execEs.onerror = () => {
|
|
@@ -1487,8 +1487,8 @@
|
|
|
1487
1487
|
initChangelog,
|
|
1488
1488
|
copyToClipboard,
|
|
1489
1489
|
computePrimaryByRepo,
|
|
1490
|
-
requireUnlock,
|
|
1491
1490
|
setServiceRunner,
|
|
1491
|
+
sudoRun,
|
|
1492
1492
|
takePendingAction,
|
|
1493
1493
|
problemCards,
|
|
1494
1494
|
problemFix,
|
|
@@ -1545,14 +1545,12 @@
|
|
|
1545
1545
|
.replace(/>/g, ">")
|
|
1546
1546
|
.replace(/"/g, """);
|
|
1547
1547
|
|
|
1548
|
-
// ── The
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
|
|
1554
|
-
return requireUnlock();
|
|
1555
|
-
}
|
|
1548
|
+
// ── The admin password (R8, LK-0) ─────────────────────────────────────────
|
|
1549
|
+
// It is for sudo, and nothing else: no action here asks for it up
|
|
1550
|
+
// front. A run whose command is about to run sudo without it gets a
|
|
1551
|
+
// needs-sudo event from the server (sudoRun().see); the shell's
|
|
1552
|
+
// prompt then says what needs it, and after the password the action
|
|
1553
|
+
// runs again (sudoRun().done).
|
|
1556
1554
|
|
|
1557
1555
|
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
|
1558
1556
|
// Systems with auto-start toggled OFF in the viewed workspace (`autostart:
|
|
@@ -1572,7 +1570,7 @@
|
|
|
1572
1570
|
]);
|
|
1573
1571
|
initLogBadge();
|
|
1574
1572
|
// Start and Restart from the status drawer and the ⌘K palette run
|
|
1575
|
-
// here, with this page's
|
|
1573
|
+
// here, with this page's output (R15, R17).
|
|
1576
1574
|
setServiceRunner((cmd, s) =>
|
|
1577
1575
|
window.runCmd(s.address, s.repo, s.service, cmd),
|
|
1578
1576
|
);
|
|
@@ -3051,9 +3049,9 @@
|
|
|
3051
3049
|
// ── Workspace-level command (SSE) ─────────────────────────────────────────
|
|
3052
3050
|
// Routes output to each service's own tab as `label` events arrive.
|
|
3053
3051
|
window.runWorkspaceSync = async function () {
|
|
3054
|
-
if (!(await requireSudo())) return;
|
|
3055
3052
|
setAllButtons(true);
|
|
3056
3053
|
setWsStatus("syncing…");
|
|
3054
|
+
const sudo = sudoRun();
|
|
3057
3055
|
const es = new EventSource("/api/workspace/sync");
|
|
3058
3056
|
// Clear previous sync output then open/activate the tab (explicit action).
|
|
3059
3057
|
clearDismissed("⟳ sync");
|
|
@@ -3062,6 +3060,7 @@
|
|
|
3062
3060
|
setBusy("⟳ sync", true);
|
|
3063
3061
|
es.onmessage = (e) => {
|
|
3064
3062
|
const msg = JSON.parse(e.data);
|
|
3063
|
+
sudo.see(msg);
|
|
3065
3064
|
if (msg.type === "line") appendToTab("⟳ sync", msg.text);
|
|
3066
3065
|
if (msg.type === "error")
|
|
3067
3066
|
appendToTab("⟳ sync", "Error: " + msg.text, "err");
|
|
@@ -3073,6 +3072,7 @@
|
|
|
3073
3072
|
setTimeout(() => setWsStatus(""), 3000);
|
|
3074
3073
|
// See refreshAfterSync — a sync that clones brings new commands with it.
|
|
3075
3074
|
refreshAfterSync();
|
|
3075
|
+
void sudo.done(() => window.runWorkspaceSync());
|
|
3076
3076
|
}
|
|
3077
3077
|
};
|
|
3078
3078
|
es.onerror = () => {
|
|
@@ -3103,9 +3103,9 @@
|
|
|
3103
3103
|
// system. Buttons are not globally disabled: it is one repo.
|
|
3104
3104
|
const _syncing = new Set();
|
|
3105
3105
|
window.runRepoSync = async function (repo) {
|
|
3106
|
-
if (!(await requireSudo())) return;
|
|
3107
3106
|
if (_syncing.has(repo)) return; // already in flight — ignore re-entry
|
|
3108
3107
|
_syncing.add(repo);
|
|
3108
|
+
const sudo = sudoRun();
|
|
3109
3109
|
|
|
3110
3110
|
const route = tabRouteFor(repo);
|
|
3111
3111
|
clearDismissed(route); // explicit action — undo any prior dismissal
|
|
@@ -3130,12 +3130,14 @@
|
|
|
3130
3130
|
);
|
|
3131
3131
|
es.onmessage = (e) => {
|
|
3132
3132
|
const msg = JSON.parse(e.data);
|
|
3133
|
+
sudo.see(msg);
|
|
3133
3134
|
if (msg.type === "line") appendToTab(route, msg.text);
|
|
3134
3135
|
if (msg.type === "error")
|
|
3135
3136
|
appendToTab(route, "Error: " + msg.text, "err");
|
|
3136
3137
|
if (msg.type === "done") {
|
|
3137
3138
|
es.close();
|
|
3138
3139
|
finish("sync complete");
|
|
3140
|
+
void sudo.done(() => window.runRepoSync(repo));
|
|
3139
3141
|
// A pull can replace this repo's health probes on disk. The health
|
|
3140
3142
|
// response is cached ~8s, so force a fresh poll rather than leaving
|
|
3141
3143
|
// dots that reflect the code we just replaced.
|
|
@@ -3156,7 +3158,7 @@
|
|
|
3156
3158
|
};
|
|
3157
3159
|
|
|
3158
3160
|
window.runWorkspaceCmd = async function (cmd, addresses) {
|
|
3159
|
-
|
|
3161
|
+
const sudo = sudoRun();
|
|
3160
3162
|
const only =
|
|
3161
3163
|
Array.isArray(addresses) && addresses.length
|
|
3162
3164
|
? `&addresses=${encodeURIComponent(addresses.join(","))}`
|
|
@@ -3174,6 +3176,7 @@
|
|
|
3174
3176
|
|
|
3175
3177
|
esWs.onmessage = (e) => {
|
|
3176
3178
|
const msg = JSON.parse(e.data);
|
|
3179
|
+
sudo.see(msg);
|
|
3177
3180
|
if (msg.type === "label") {
|
|
3178
3181
|
// New service section — route to the SYSTEM's primary tab (one tab
|
|
3179
3182
|
// per system; sidecar output lands in its repo's tab with a header).
|
|
@@ -3239,6 +3242,7 @@
|
|
|
3239
3242
|
idle();
|
|
3240
3243
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3241
3244
|
syncHealthNow();
|
|
3245
|
+
void sudo.done(() => window.runWorkspaceCmd(cmd, addresses));
|
|
3242
3246
|
}
|
|
3243
3247
|
};
|
|
3244
3248
|
esWs.onerror = () => {
|
|
@@ -3270,7 +3274,6 @@
|
|
|
3270
3274
|
// its rules are load-bearing, and the already-healthy skip it relies on is
|
|
3271
3275
|
// suppressed whenever an explicit address list is sent.
|
|
3272
3276
|
window.runUpWithDeps = async function (repo) {
|
|
3273
|
-
if (!(await requireSudo())) return;
|
|
3274
3277
|
void runWorkspaceCmdAsync("up", null, {
|
|
3275
3278
|
seed: repo,
|
|
3276
3279
|
undismiss: true,
|
|
@@ -3282,7 +3285,6 @@
|
|
|
3282
3285
|
// same reason as its counterpart: the already-stopped skip is suppressed
|
|
3283
3286
|
// when an explicit address list is sent.
|
|
3284
3287
|
window.runDownWithDeps = async function (repo) {
|
|
3285
|
-
if (!(await requireSudo())) return;
|
|
3286
3288
|
void runWorkspaceCmdAsync("down", null, {
|
|
3287
3289
|
seed: repo,
|
|
3288
3290
|
undismiss: true,
|
|
@@ -3290,7 +3292,6 @@
|
|
|
3290
3292
|
};
|
|
3291
3293
|
|
|
3292
3294
|
window.runCmd = async function (address, repo, service, cmd) {
|
|
3293
|
-
if (!(await requireSudo())) return;
|
|
3294
3295
|
// Explicit single-service action — undo any prior dismissal so its pane returns.
|
|
3295
3296
|
clearDismissed(address);
|
|
3296
3297
|
// up/restart/down go through the workspace-exec runner: it uses the full
|
|
@@ -3319,9 +3320,12 @@
|
|
|
3319
3320
|
setTabStatus(address, { es: esCmd });
|
|
3320
3321
|
const _cmdTab = _tabs.get(address);
|
|
3321
3322
|
if (_cmdTab) _cmdTab.esKind = "exec";
|
|
3323
|
+
const sudo = sudoRun();
|
|
3324
|
+
const again = () => window.runCmd(address, repo, service, cmd);
|
|
3322
3325
|
|
|
3323
3326
|
esCmd.onmessage = (e) => {
|
|
3324
3327
|
const msg = JSON.parse(e.data);
|
|
3328
|
+
sudo.see(msg);
|
|
3325
3329
|
if (msg.type === "cmd") {
|
|
3326
3330
|
appendToTab(address, msg.text, "cmd");
|
|
3327
3331
|
} else if (msg.type === "started") {
|
|
@@ -3349,6 +3353,7 @@
|
|
|
3349
3353
|
"done-fail",
|
|
3350
3354
|
);
|
|
3351
3355
|
setTabStatus(address, { live: false, errored: true, es: null });
|
|
3356
|
+
void sudo.done(again);
|
|
3352
3357
|
}
|
|
3353
3358
|
} else if (msg.type === "line") {
|
|
3354
3359
|
appendToTab(address, msg.text, "");
|
|
@@ -3369,6 +3374,7 @@
|
|
|
3369
3374
|
);
|
|
3370
3375
|
_tabs.get(address) && (_tabs.get(address).busy = false);
|
|
3371
3376
|
setTabStatus(address, { live: false, errored: !ok, es: null });
|
|
3377
|
+
void sudo.done(again);
|
|
3372
3378
|
}
|
|
3373
3379
|
};
|
|
3374
3380
|
esCmd.onerror = () => {
|
|
@@ -3853,9 +3859,11 @@
|
|
|
3853
3859
|
// address filter — the filter suppresses the already-healthy skip and
|
|
3854
3860
|
// the auto-start-off handling that make "with deps" behave.
|
|
3855
3861
|
if (opts.seed) params.set("seed", opts.seed);
|
|
3862
|
+
const sudo = sudoRun();
|
|
3856
3863
|
const es = new EventSource(`/api/workspace/exec?${params}`);
|
|
3857
3864
|
es.onmessage = (e) => {
|
|
3858
3865
|
const msg = JSON.parse(e.data);
|
|
3866
|
+
sudo.see(msg);
|
|
3859
3867
|
if (msg.type === "label") {
|
|
3860
3868
|
currentAddress = msg.text;
|
|
3861
3869
|
// Route to the system's primary tab; skip dismissed panes on
|
|
@@ -3924,7 +3932,10 @@
|
|
|
3924
3932
|
idle();
|
|
3925
3933
|
// Lifecycle changed run-state — sync dots now, don't wait for the poll.
|
|
3926
3934
|
syncHealthNow();
|
|
3927
|
-
|
|
3935
|
+
// LK-0: after the admin password, the same run again.
|
|
3936
|
+
void sudo
|
|
3937
|
+
.done(() => runWorkspaceCmdAsync(cmd, addresses, opts))
|
|
3938
|
+
.then((again) => resolve(again ?? ok));
|
|
3928
3939
|
}
|
|
3929
3940
|
};
|
|
3930
3941
|
es.onerror = () => {
|
|
@@ -333,6 +333,7 @@
|
|
|
333
333
|
initChangelog,
|
|
334
334
|
computePrimaryByRepo,
|
|
335
335
|
envReady,
|
|
336
|
+
sudoRun,
|
|
336
337
|
} from "/header.js";
|
|
337
338
|
import { ownersLabel } from "/workspace-picker.js";
|
|
338
339
|
|
|
@@ -613,10 +614,16 @@
|
|
|
613
614
|
const usableCmds = isSub
|
|
614
615
|
? s.commands.filter((cmd) => !LIFECYCLE_CMDS.has(cmd))
|
|
615
616
|
: s.commands;
|
|
617
|
+
// The workspace the row was built from (the first in view that
|
|
618
|
+
// defines it, focus first): its command runs there, not in
|
|
619
|
+
// whichever workspace is focused.
|
|
620
|
+
const wsAttr = s.inWorkspaces?.[0]
|
|
621
|
+
? ` data-workspace="${esc(s.inWorkspaces[0])}"`
|
|
622
|
+
: "";
|
|
616
623
|
const cmdButtons = usableCmds
|
|
617
624
|
.map(
|
|
618
625
|
(cmd) =>
|
|
619
|
-
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec">${cmd}</button>`,
|
|
626
|
+
`<button class="cmd-btn" data-repo="${s.repo}" data-service="${s.service}" data-cmd="${cmd}" data-type="exec"${wsAttr}>${cmd}</button>`,
|
|
620
627
|
)
|
|
621
628
|
.join("");
|
|
622
629
|
const logsButton = s.logs
|
|
@@ -743,8 +750,9 @@
|
|
|
743
750
|
// Attach click handlers
|
|
744
751
|
pane.querySelectorAll(".cmd-btn").forEach((btn) => {
|
|
745
752
|
btn.addEventListener("click", () => {
|
|
746
|
-
const { repo, service, cmd, type } = btn.dataset;
|
|
747
|
-
if (type === "exec")
|
|
753
|
+
const { repo, service, cmd, type, workspace } = btn.dataset;
|
|
754
|
+
if (type === "exec")
|
|
755
|
+
runExec(repo, service, cmd, btn, workspace);
|
|
748
756
|
else runLogs(repo, service, btn);
|
|
749
757
|
});
|
|
750
758
|
});
|
|
@@ -765,7 +773,7 @@
|
|
|
765
773
|
}
|
|
766
774
|
}
|
|
767
775
|
|
|
768
|
-
function runExec(repo, service, cmd, btn) {
|
|
776
|
+
function runExec(repo, service, cmd, btn, workspace) {
|
|
769
777
|
endStream();
|
|
770
778
|
const env = document.getElementById("env-sel").value;
|
|
771
779
|
|
|
@@ -781,8 +789,13 @@
|
|
|
781
789
|
appendLine(`\n▶ ${repo}/${service} · ${cmd} · ${env}\n`, "cmd");
|
|
782
790
|
|
|
783
791
|
const params = new URLSearchParams({ repo, service, cmd, env });
|
|
792
|
+
if (workspace) params.set("workspace", workspace);
|
|
784
793
|
es = new EventSource(`/api/service/exec?${params}`);
|
|
785
|
-
|
|
794
|
+
// LK-0: a command that needs sudo without the password asks,
|
|
795
|
+
// then runs again.
|
|
796
|
+
handleSSE(es, repo, service, {
|
|
797
|
+
again: () => runExec(repo, service, cmd, btn, workspace),
|
|
798
|
+
});
|
|
786
799
|
}
|
|
787
800
|
|
|
788
801
|
function runLogs(repo, service, btn) {
|
|
@@ -819,8 +832,10 @@
|
|
|
819
832
|
const isStream = opts.stream === true;
|
|
820
833
|
const id = runId;
|
|
821
834
|
const appendLine = (text, cls) => runs().append(id, text, cls);
|
|
835
|
+
const sudo = sudoRun();
|
|
822
836
|
evtSrc.onmessage = (e) => {
|
|
823
837
|
const msg = JSON.parse(e.data);
|
|
838
|
+
sudo.see(msg);
|
|
824
839
|
if (msg.type === "cmd") {
|
|
825
840
|
appendLine(msg.text, "cmd");
|
|
826
841
|
} else if (msg.type === "line") {
|
|
@@ -842,6 +857,7 @@
|
|
|
842
857
|
} else runs().set(id, { running: false });
|
|
843
858
|
evtSrc.close();
|
|
844
859
|
es = null;
|
|
860
|
+
if (opts.again) void sudo.done(opts.again);
|
|
845
861
|
}
|
|
846
862
|
};
|
|
847
863
|
evtSrc.onerror = () => {
|
|
@@ -300,7 +300,8 @@ export async function mountSettings(deps) {
|
|
|
300
300
|
const did = `d-${id}`;
|
|
301
301
|
const v = value(r);
|
|
302
302
|
const t = r.control?.type || "readonly";
|
|
303
|
-
|
|
303
|
+
// A custom control draws (and saves) itself: no write to read it by.
|
|
304
|
+
const ro = !r.write && t !== "custom";
|
|
304
305
|
const single = ["number", "select", "toggle", "text"].includes(t);
|
|
305
306
|
const label = single
|
|
306
307
|
? `<label class="l" id="${lid}" for="${cid}">${esc(r.label)}</label>`
|
|
@@ -499,7 +500,7 @@ export async function mountSettings(deps) {
|
|
|
499
500
|
}, 2000);
|
|
500
501
|
}
|
|
501
502
|
function wireRow(r) {
|
|
502
|
-
if (!r.write) return;
|
|
503
|
+
if (!r.write && r.control?.type !== "custom") return;
|
|
503
504
|
const row = $(`setting-${slug(r.id)}`);
|
|
504
505
|
if (!row) return;
|
|
505
506
|
const t = r.control?.type;
|