@monster-orange/den 0.4.2 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/README.md +9 -9
  3. package/docs/distributions.md +313 -50
  4. package/package.json +1 -1
  5. package/src/shared/agents/catalog-runtime.ts +178 -0
  6. package/src/shared/agents/catalog.ts +343 -0
  7. package/src/shared/agents/den-claude.ts +53 -12
  8. package/src/shared/agents/generic.ts +1 -0
  9. package/src/shared/agents/runtime-search.ts +543 -0
  10. package/src/shared/agents/runtime.ts +43 -1
  11. package/src/shared/agents/runtimes.ts +53 -4
  12. package/src/shared/auth/sudo-vault.ts +16 -0
  13. package/src/shared/den-config.ts +92 -2
  14. package/src/shared/edge/render.ts +31 -0
  15. package/src/shared/edge/status.ts +134 -0
  16. package/src/shared/environment-manager/health.ts +82 -8
  17. package/src/shared/environment-manager/workspaces.ts +54 -0
  18. package/src/shared/health/not-checked.ts +42 -0
  19. package/src/shared/health/rollup.ts +4 -0
  20. package/src/shared/process/spawn-streaming.ts +8 -3
  21. package/src/shared/sessions/client.ts +5 -1
  22. package/src/shared/sessions/host.ts +29 -1
  23. package/src/shared/sessions/protocol.ts +6 -1
  24. package/src/shared/sessions/types.ts +6 -0
  25. package/src/shared/stacks/status.ts +24 -0
  26. package/src/shared/strategies/registry.ts +5 -0
  27. package/src/shared/types/index.ts +6 -0
  28. package/src/ui/cli/agent-ops.ts +298 -19
  29. package/src/ui/cli/agents-command.ts +129 -4
  30. package/src/ui/cli/edge-command.ts +16 -3
  31. package/src/ui/cli/health-display.ts +4 -1
  32. package/src/ui/cli/sessions-command.ts +9 -4
  33. package/src/ui/cli/web-command.ts +53 -8
  34. package/src/ui/mcp/scope.ts +23 -3
  35. package/src/ui/mcp/server.ts +31 -7
  36. package/src/ui/mcp/tools.ts +327 -20
  37. package/src/ui/mcp/views.ts +450 -0
  38. package/src/ui/server/agent-routes.ts +123 -26
  39. package/src/ui/server/edge-exposure.ts +143 -0
  40. package/src/ui/server/index.ts +171 -25
  41. package/src/ui/server/public/agent-start.js +241 -0
  42. package/src/ui/server/public/agents-page.js +246 -45
  43. package/src/ui/server/public/agents-section.js +7 -21
  44. package/src/ui/server/public/agents.html +55 -1
  45. package/src/ui/server/public/graph-model.js +2 -0
  46. package/src/ui/server/public/header.js +2 -3
  47. package/src/ui/server/public/health-rollup.js +20 -8
  48. package/src/ui/server/public/i18n/es.js +26 -13
  49. package/src/ui/server/public/i18n/ja.js +25 -13
  50. package/src/ui/server/public/index.html +17 -4
  51. package/src/ui/server/public/local-dev.html +62 -22
  52. package/src/ui/server/public/services.html +21 -5
  53. package/src/ui/server/public/settings-page.js +3 -2
  54. package/src/ui/server/public/settings-registry.js +226 -1
  55. package/src/ui/server/public/settings.html +90 -1
  56. package/src/ui/server/public/shell.js +67 -22
  57. package/src/ui/server/public/topbar.js +217 -158
  58. package/src/ui/server/readonly-routes.ts +29 -5
  59. package/src/ui/server/request-guard.ts +123 -8
  60. package/src/ui/server/route-classes.ts +14 -2
  61. package/src/ui/server/session-routes.ts +9 -1
  62. package/src/ui/server/sudo-store.ts +170 -6
  63. package/src/ui/server/ui-assets.ts +2 -0
  64. package/src/ui/server/workspace-routes.ts +10 -3
@@ -7,15 +7,16 @@
7
7
  *
8
8
  * Every call is the server's agent routes (agent-routes.ts), which are the
9
9
  * CLI's own operations: nothing here keeps an agent's state. Each action
10
- * goes through denShell.api() (a refusal is its error toast) after the
11
- * lock rule (requireUnlock, R8), as a service action does. A row with a
10
+ * goes through denShell.api() (a refusal is its error toast). None asks
11
+ * for the admin password: it is for sudo, and no agent action runs sudo
12
+ * (LK-0). A row with a
12
13
  * session has Open (its terminal on the Agents page, AG-4a-term), and the
13
14
  * header links to the Agents page for the workspace.
14
15
  *
15
16
  * The page's table styles (table.svc, .tablewrap, .tbl-*) and the shell's
16
17
  * (.den-sec, .den-btn, .den-dot) are reused as they are.
17
18
  */
18
- import { requireUnlock } from "/topbar.js";
19
+ import { fillStartForm } from "/agent-start.js";
19
20
 
20
21
  const t = (key, vars) => window.denShell?.t?.(key, vars) ?? key;
21
22
  const esc = (s) =>
@@ -62,6 +63,8 @@ export function agentsSectionHtml(ws) {
62
63
  <form class="agent-form" id="agent-start-form" hidden>
63
64
  <label>${esc(t("ag.persona"))} <select class="form-sel" name="persona" required></select></label>
64
65
  <label>${esc(t("ag.runtime"))} <select class="form-sel" name="runtime"></select></label>
66
+ <p class="agent-form-help" data-persona-help hidden></p>
67
+ <p class="agent-form-ready" data-runtime-ready role="status" hidden></p>
65
68
  <button type="submit" class="den-btn sm primary" id="agent-start-go">${esc(t("ag.go"))}</button>
66
69
  <button type="button" class="den-btn sm" data-agent-cancel>${esc(t("ag.cancel"))}</button>
67
70
  </form>
@@ -185,22 +188,7 @@ async function refresh(ws) {
185
188
  }
186
189
 
187
190
  function fillForm(form, data) {
188
- const ps = form.querySelector('select[name="persona"]');
189
- ps.innerHTML = (data.personas ?? [])
190
- .map(
191
- (p) =>
192
- `<option value="${esc(p.id)}">${esc(p.id)}${p.role ? ` — ${esc(p.role)}` : ""}</option>`,
193
- )
194
- .join("");
195
- const rs = form.querySelector('select[name="runtime"]');
196
- rs.innerHTML =
197
- `<option value="">${esc(t("ag.runtime.auto"))}</option>` +
198
- (data.runtimes ?? [])
199
- .map(
200
- (r) =>
201
- `<option value="${esc(r.id)}"${r.available ? "" : " disabled"}>${esc(r.id)}${r.available ? "" : ` — ${esc(t("ag.unavailable"))}`}</option>`,
202
- )
203
- .join("");
191
+ fillStartForm(form, data, t);
204
192
  }
205
193
 
206
194
  async function openForm(sec) {
@@ -229,7 +217,6 @@ async function openForm(sec) {
229
217
  }
230
218
 
231
219
  async function act(ws, verb, id, button) {
232
- if (!requireUnlock()) return;
233
220
  button.disabled = true;
234
221
  try {
235
222
  await api("POST", `${base(ws)}/${encodeURIComponent(id)}/${verb}`);
@@ -243,7 +230,6 @@ async function act(ws, verb, id, button) {
243
230
  }
244
231
 
245
232
  async function start(ws, form) {
246
- if (!requireUnlock()) return;
247
233
  const go = form.querySelector("#agent-start-go");
248
234
  const persona = form.querySelector('select[name="persona"]').value;
249
235
  const runtime = form.querySelector('select[name="runtime"]').value;
@@ -298,7 +298,17 @@
298
298
  font-size: 12.5px;
299
299
  }
300
300
  dialog.ag-dlg {
301
- width: min(440px, calc(100vw - 32px));
301
+ /* The approved mock's Start agent dialog (AG-0.5), measured on
302
+ den-shell.html: .dlg.wide, min(600px, 100%) of content in a
303
+ content-box, so 600 + 2 x 18 padding + 2 x 1 border = 638
304
+ outside at 1440. Here the box is border-box: 638, and never
305
+ wider than the viewport less 16px a side (at 375 the mock's
306
+ box is 381 and overflows; this one is 343). */
307
+ box-sizing: border-box;
308
+ width: min(638px, calc(100vw - 32px));
309
+ max-width: none;
310
+ max-height: calc(100vh - 32px);
311
+ overflow-y: auto;
302
312
  background: var(--raised);
303
313
  color: var(--text);
304
314
  border: 1px solid var(--border-strong);
@@ -311,8 +321,41 @@
311
321
  }
312
322
  .ag-dlg form {
313
323
  display: grid;
324
+ /* AG-10.0: minmax(0, 1fr), so a long option can't widen the form
325
+ past the dialog and push Cancel and Start out of sight. */
326
+ grid-template-columns: minmax(0, 1fr);
314
327
  gap: 12px;
315
328
  }
329
+ .ag-dlg .help {
330
+ margin: -6px 0 0;
331
+ font: 12.5px/1.45 var(--sans);
332
+ color: var(--muted);
333
+ overflow-wrap: anywhere;
334
+ }
335
+ .ag-dlg .help[hidden],
336
+ .ag-dlg .ready[hidden],
337
+ .ag-dlg .err[hidden] {
338
+ display: none;
339
+ }
340
+ /* AG-0.2: the runtime it will run, or why it can't, before Start
341
+ (the mock's .helpl: 12px, faint, 1.4). */
342
+ .ag-dlg .ready {
343
+ margin: -6px 0 0;
344
+ font: 12px/1.4 var(--sans);
345
+ color: var(--faint);
346
+ overflow-wrap: anywhere;
347
+ }
348
+ .ag-dlg .ready[data-tone="warn"] {
349
+ color: var(--warn);
350
+ }
351
+ .ag-dlg .ready .mono {
352
+ font-family: var(--mono);
353
+ }
354
+ .ag-dlg .err {
355
+ margin: 0;
356
+ font: 12.5px/1.45 var(--sans);
357
+ color: var(--bad);
358
+ }
316
359
  .ag-dlg h2 {
317
360
  margin: 0;
318
361
  font: 600 17px var(--display);
@@ -325,7 +368,10 @@
325
368
  text-transform: uppercase;
326
369
  letter-spacing: 0.07em;
327
370
  }
371
+ .ag-dlg input[type="password"],
328
372
  .ag-dlg select {
373
+ width: 100%;
374
+ min-width: 0;
329
375
  font: 13px var(--sans);
330
376
  text-transform: none;
331
377
  letter-spacing: 0;
@@ -404,10 +450,18 @@
404
450
  ><span id="ag-p-t">Persona</span>
405
451
  <select name="persona" id="ag-persona" required></select
406
452
  ></label>
453
+ <p class="help" id="ag-p-help" data-persona-help hidden></p>
407
454
  <label
408
455
  ><span id="ag-r-t">Runtime</span>
409
456
  <select name="runtime" id="ag-runtime"></select
410
457
  ></label>
458
+ <p
459
+ class="ready"
460
+ id="ag-r-ready"
461
+ data-runtime-ready
462
+ role="status"
463
+ hidden
464
+ ></p>
411
465
  <div class="btns">
412
466
  <button type="button" class="den-btn" id="ag-cancel">Cancel</button>
413
467
  <button type="submit" class="den-btn primary" id="ag-go">
@@ -156,6 +156,7 @@ export function applyHealth(m, results) {
156
156
  check: h.check ?? null,
157
157
  error: h.error ?? null,
158
158
  status: h.status ?? null,
159
+ redirect: h.redirect ?? null,
159
160
  ms:
160
161
  typeof h.ms === "number"
161
162
  ? h.ms
@@ -232,6 +233,7 @@ export function repoRollup(m, repo) {
232
233
  service: s.name,
233
234
  ok: s.health.ok,
234
235
  isEntrypoint: s.entry,
236
+ redirect: !!s.health.redirect,
235
237
  })),
236
238
  );
237
239
  }
@@ -30,12 +30,11 @@ const escT = (key, vars) =>
30
30
  );
31
31
 
32
32
  export {
33
- isUnlocked,
34
- onLockChange,
33
+ askSudo,
35
34
  problemCards,
36
35
  problemFix,
37
- requireUnlock,
38
36
  setServiceRunner,
37
+ sudoRun,
39
38
  takePendingAction,
40
39
  } from "./topbar.js";
41
40
  export { buildRow } from "./health-fit.js";
@@ -27,9 +27,9 @@
27
27
  export const STATE_RANK = { down: 0, degraded: 1, ok: 2, unknown: 3 };
28
28
 
29
29
  /**
30
- * @typedef {{ service: string, ok: boolean | null | undefined, isEntrypoint?: boolean, awaiting?: boolean }} HealthSignal
30
+ * @typedef {{ service: string, ok: boolean | null | undefined, isEntrypoint?: boolean, awaiting?: boolean, redirect?: boolean }} HealthSignal
31
31
  * @typedef {"down" | "degraded" | "ok" | "unknown"} RepoState
32
- * @typedef {{ state: RepoState, entry: string | null, down: string[], checked: number, unchecked: number, awaiting: number }} RepoRollup
32
+ * @typedef {{ state: RepoState, entry: string | null, down: string[], checked: number, unchecked: number, awaiting: number, redirected?: number }} RepoRollup
33
33
  */
34
34
 
35
35
  /**
@@ -60,6 +60,11 @@ export function rollupRepo(repo, signals) {
60
60
  const answered = signals.filter((s) => !s.awaiting);
61
61
  const entry = entrySignal(repo, answered);
62
62
  const checked = answered.filter((s) => s.ok === true || s.ok === false);
63
+ // A redirect (ok null, `redirect`) answered: not down, not up, and not
64
+ // "not checked" either; counted on its own.
65
+ const redirected = answered.filter(
66
+ (s) => s.redirect && s.ok !== true && s.ok !== false,
67
+ ).length;
63
68
  const down = awaiting
64
69
  ? []
65
70
  : checked.filter((s) => s.ok === false).map((s) => s.service);
@@ -77,8 +82,9 @@ export function rollupRepo(repo, signals) {
77
82
  entry: entry ? entry.service : null,
78
83
  down,
79
84
  checked: checked.length,
80
- unchecked: answered.length - checked.length,
85
+ unchecked: answered.length - checked.length - redirected,
81
86
  awaiting,
87
+ ...(redirected ? { redirected } : {}),
82
88
  };
83
89
  }
84
90
 
@@ -99,16 +105,22 @@ export function rollupRows(rows) {
99
105
 
100
106
  /**
101
107
  * "down (web down)", "degraded (mailpit down)", "healthy",
102
- * "healthy, 25 not checked", "unknown (not checked)".
108
+ * "healthy, 25 not checked", "healthy, 1 redirect", "unknown (not
109
+ * checked)", "unknown (1 redirect)".
103
110
  * @param {RepoRollup} r
104
111
  * @returns {string}
105
112
  */
106
113
  export function describeRollup(r) {
107
- const notChecked = r.unchecked > 0 ? `${r.unchecked} not checked` : "";
114
+ const extra = [
115
+ r.redirected ? `${r.redirected} redirect` : "",
116
+ r.unchecked > 0 ? `${r.unchecked} not checked` : "",
117
+ ]
118
+ .filter(Boolean)
119
+ .join(", ");
108
120
  if (r.awaiting) return "unknown (still checking)";
109
- if (r.state === "unknown") return "unknown (not checked)";
110
- if (r.state === "ok")
111
- return notChecked ? `healthy, ${notChecked}` : "healthy";
121
+ if (r.state === "unknown")
122
+ return r.redirected ? `unknown (${extra})` : "unknown (not checked)";
123
+ if (r.state === "ok") return extra ? `healthy, ${extra}` : "healthy";
112
124
  return `${r.state} (${r.down.join(", ")} down)`;
113
125
  }
114
126
 
@@ -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.locked": "Bloqueado",
146
- "lock.unlocked": "Desbloqueado",
147
- "lock.title":
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": "Desbloquear",
153
+ "lock.go": "Continuar",
160
154
  "lock.checking": "Comprobando…",
161
155
  "lock.note":
162
- "Den la pide una vez por sesión. Los agentes no pueden desbloquear.",
163
- "lock.again": "Escribir la contraseña de nuevo…",
164
- "lock.saveFailed": "Desbloqueado, pero no se pudo guardar en {s}: {err}",
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",
@@ -330,6 +327,7 @@
330
327
  "ag.persona": "Persona",
331
328
  "ag.runtime": "Runtime",
332
329
  "ag.runtime.auto": "La de la persona",
330
+ "ag.runtime.default": "{name} (la de la persona)",
333
331
  "ag.unavailable": "no disponible",
334
332
  "ag.go": "Iniciar",
335
333
  "ag.starting": "Iniciando…",
@@ -372,5 +370,20 @@
372
370
  "ag.palStart": "Iniciar agente…",
373
371
  "ag.cap":
374
372
  "Hasta 4 paneles: se cerró el de {id} para hacer sitio (sigue en marcha)",
373
+ "ag.rt.ready": "Inicia {name} {version} desde",
374
+ "ag.rt.readyNoVer": "Inicia {name} desde",
375
+ "ag.rt.notFound": "No se encontró {name}.",
376
+ "ag.rt.searched": "Se buscó en:",
377
+ "ag.rt.more": "y {n} más",
378
+ "ag.rt.nowhere": "ningún directorio",
379
+ "ag.rt.refused": "{name} no puede iniciarse: {why}.",
380
+ "ag.rt.fix":
381
+ "Instálalo, o pon su ruta en {key} en {file}, o ejecuta {cmd}.",
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",
375
388
  };
376
389
  })((window.denMessages = window.denMessages || {}));
@@ -131,27 +131,21 @@
131
131
  "dock.err": "失敗",
132
132
  "dock.last": "前回の実行: {s} ↗",
133
133
  "dock.resize": "ドラッグしてサイズを変更",
134
- "lock.locked": "ロック中",
135
- "lock.unlocked": "ロック解除",
136
- "lock.title":
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.again": "パスワードをもう一度入力…",
145
+ "Den はこれを sudo にだけ使います。エージェントは入力できません。",
146
+ "lock.saved": "このセッションの間、管理者パスワードを保持します",
153
147
  "lock.saveFailed":
154
- "ロックを解除しましたが、{s} への保存に失敗しました: {err}",
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": "組織のラベル",
@@ -318,6 +314,7 @@
318
314
  "ag.persona": "ペルソナ",
319
315
  "ag.runtime": "ランタイム",
320
316
  "ag.runtime.auto": "ペルソナの既定",
317
+ "ag.runtime.default": "{name}(ペルソナの既定)",
321
318
  "ag.unavailable": "利用不可",
322
319
  "ag.go": "開始",
323
320
  "ag.starting": "開始中…",
@@ -357,5 +354,20 @@
357
354
  "ag.palStart": "エージェントを開始…",
358
355
  "ag.cap":
359
356
  "ペインは最大 4 つです。{id} のペインを閉じました(実行は続きます)",
357
+ "ag.rt.ready": "{name} {version} を起動:",
358
+ "ag.rt.readyNoVer": "{name} を起動:",
359
+ "ag.rt.notFound": "{name} が見つかりません。",
360
+ "ag.rt.searched": "検索した場所:",
361
+ "ag.rt.more": "ほか {n} 件",
362
+ "ag.rt.nowhere": "なし",
363
+ "ag.rt.refused": "{name} を起動できません: {why}。",
364
+ "ag.rt.fix":
365
+ "インストールするか、{file} の {key} にパスを設定するか、{cmd} を実行してください。",
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 テンプレートがありません",
360
372
  };
361
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 = () => {