@monster-orange/den 0.4.14 → 0.4.16
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 +194 -0
- package/README.md +7 -7
- package/docs/distributions.md +226 -19
- package/package.json +1 -1
- package/src/shared/actor-token.ts +12 -0
- package/src/shared/agents/conversations.ts +155 -8
- package/src/shared/agents/den-claude.ts +25 -3
- package/src/shared/agents/records.ts +37 -0
- package/src/shared/agents/runtime.ts +11 -1
- package/src/shared/claim-code.ts +163 -0
- package/src/shared/cli-command.ts +31 -0
- package/src/shared/den-config.ts +12 -0
- package/src/shared/environment-manager/workspaces.ts +20 -0
- package/src/shared/github-app.ts +7 -0
- package/src/shared/web-session.ts +64 -5
- package/src/ui/cli/agent-ops.ts +372 -23
- package/src/ui/cli/agents-command.ts +71 -4
- package/src/ui/cli/doctor-command.ts +29 -3
- package/src/ui/cli/doctor-onboard.ts +26 -0
- package/src/ui/cli/flag-parser.ts +9 -1
- package/src/ui/cli/index.ts +29 -3
- package/src/ui/cli/onboard-command.ts +180 -0
- package/src/ui/cli/onboard-reads.ts +613 -0
- package/src/ui/cli/repo-command.ts +156 -10
- package/src/ui/cli/web-command.ts +92 -1
- package/src/ui/mcp/api.ts +14 -2
- package/src/ui/mcp/server.ts +12 -2
- package/src/ui/mcp/tools.ts +368 -1
- package/src/ui/server/agent-routes.ts +91 -7
- package/src/ui/server/approval-routes.ts +390 -0
- package/src/ui/server/approvals.ts +416 -0
- package/src/ui/server/github-routes.ts +13 -5
- package/src/ui/server/held-actions.ts +203 -0
- package/src/ui/server/http-helpers.ts +35 -0
- package/src/ui/server/index.ts +308 -21
- package/src/ui/server/lifecycle-exec.ts +70 -0
- package/src/ui/server/onboard-routes.ts +132 -0
- package/src/ui/server/pair-routes.ts +7 -4
- package/src/ui/server/public/agents-page.js +378 -30
- package/src/ui/server/public/agents.html +1 -0
- package/src/ui/server/public/error-page.js +97 -18
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/header.js +1 -1
- package/src/ui/server/public/i18n/es.js +65 -5
- package/src/ui/server/public/i18n/ja.js +67 -5
- package/src/ui/server/public/index.html +1 -1
- package/src/ui/server/public/local-dev.html +5 -4
- package/src/ui/server/public/persona-ui.css +32 -0
- package/src/ui/server/public/release.html +0 -1
- package/src/ui/server/public/settings-registry.js +3 -1
- package/src/ui/server/public/settings.html +9 -1
- package/src/ui/server/public/shell.js +348 -5
- package/src/ui/server/request-guard.ts +2 -0
- package/src/ui/server/route-classes.ts +34 -1
- package/src/ui/server/session-routes.ts +14 -3
- package/src/ui/server/workspace-routes.ts +21 -10
- package/themes/den/den-theme.json +6 -6
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
*/
|
|
65
65
|
import * as fs from "fs";
|
|
66
66
|
import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
|
|
67
|
+
import { ACTOR_TOKEN_ENV } from "../../shared/actor-token.js";
|
|
67
68
|
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
68
69
|
import { HostError } from "../../shared/sessions/client.js";
|
|
69
70
|
import { checkHandback } from "../../shared/agents/handback.js";
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
import {
|
|
78
79
|
AgentError,
|
|
79
80
|
importAgent,
|
|
81
|
+
changePersona,
|
|
80
82
|
listAgents,
|
|
81
83
|
listConversations,
|
|
82
84
|
listPersonas as personaList,
|
|
@@ -116,14 +118,14 @@ export {
|
|
|
116
118
|
|
|
117
119
|
export const AGENTS_HELP = {
|
|
118
120
|
usage:
|
|
119
|
-
"agents <list|start|stop|resume|relaunch|import|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
121
|
+
"agents <list|start|stop|resume|relaunch|import|persona|rm|personas|doctor|handback-check|handoff-check> ...",
|
|
120
122
|
lines: [
|
|
121
123
|
"Agents: a persona and a workspace, run by a runtime in a session that",
|
|
122
124
|
"outlives the web server (see `{cli} sessions`).",
|
|
123
125
|
"",
|
|
124
126
|
"{cli} agents list -w <ws> [--stopped] [--json]",
|
|
125
127
|
" --stopped: only the stopped, interrupted and exited ones",
|
|
126
|
-
"{cli} agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]",
|
|
128
|
+
"{cli} agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--name <name>] [--id <id>]",
|
|
127
129
|
" runs in the workspace's dir as <dist>-<ws>:<id>; its MCP is pinned to <ws>",
|
|
128
130
|
" (plus agents.<id>.grants); capped by agents.max_concurrent",
|
|
129
131
|
"{cli} agents stop <id> [-w <ws>] [--no-wait]",
|
|
@@ -142,11 +144,17 @@ export const AGENTS_HELP = {
|
|
|
142
144
|
" passes the check, a fresh conversation (same persona, workspace, folder,",
|
|
143
145
|
" grants) whose first prompt is the handoff",
|
|
144
146
|
"{cli} agents import -w <ws> [--runtime <r>] [--json]",
|
|
145
|
-
"{cli} agents import -w <ws> --session <id> --persona <p> [--runtime <r>] [--id <id>]",
|
|
147
|
+
"{cli} agents import -w <ws> --session <id> --persona <p> [--runtime <r>] [--id <id>] [--model <m>]",
|
|
146
148
|
" bring in a conversation started outside Den: the first lists the runtime's",
|
|
147
149
|
" own (default den-claude) in the workspace's folder; the second continues",
|
|
148
150
|
" one as a Den agent (its resume, in the folder it ran in). Refused while it",
|
|
149
|
-
" is still open in another terminal: close it there first",
|
|
151
|
+
" is still open in another terminal: close it there first. It keeps the",
|
|
152
|
+
" conversation's own model unless --model gives one",
|
|
153
|
+
"{cli} agents persona <id> <persona> [-w <ws>] [--model <m>] [--now | --after-turn]",
|
|
154
|
+
" change an agent's persona: the same conversation resumes with the new",
|
|
155
|
+
" persona's role, permissions and commands; id, name, folder and model stay.",
|
|
156
|
+
" Mid-turn it asks: --after-turn waits for the turn to end, --now stops it.",
|
|
157
|
+
" A person's only (refused in an agent's session)",
|
|
150
158
|
"{cli} agents rm <id> [-w <ws>]",
|
|
151
159
|
"{cli} agents rm --stopped [-w <ws>]",
|
|
152
160
|
" forget a stopped, interrupted or exited agent (never a running one);",
|
|
@@ -273,12 +281,14 @@ async function start(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
273
281
|
"--persona": true,
|
|
274
282
|
"--runtime": true,
|
|
275
283
|
"--model": true,
|
|
284
|
+
"--name": true,
|
|
276
285
|
"--id": true,
|
|
277
286
|
});
|
|
278
287
|
noExtra(where, p);
|
|
279
288
|
const ws = workspaceFlag(where, p);
|
|
280
289
|
if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
|
|
281
290
|
const persona = str(p, "--persona");
|
|
291
|
+
const name = str(p, "--name");
|
|
282
292
|
if (!persona) throw new UsageError(`${where} needs --persona <p>`);
|
|
283
293
|
const s = await startAgent(d, {
|
|
284
294
|
workspace: ws,
|
|
@@ -286,6 +296,7 @@ async function start(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
286
296
|
runtime: str(p, "--runtime"),
|
|
287
297
|
model: str(p, "--model"),
|
|
288
298
|
id: str(p, "--id"),
|
|
299
|
+
...(name !== undefined ? { name } : {}),
|
|
289
300
|
});
|
|
290
301
|
return report(
|
|
291
302
|
d,
|
|
@@ -303,6 +314,8 @@ async function bringIn(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
303
314
|
"--session": true,
|
|
304
315
|
"--persona": true,
|
|
305
316
|
"--id": true,
|
|
317
|
+
"--model": true,
|
|
318
|
+
"--version": true,
|
|
306
319
|
"--json": false,
|
|
307
320
|
});
|
|
308
321
|
noExtra(where, p);
|
|
@@ -352,6 +365,11 @@ async function bringIn(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
352
365
|
persona,
|
|
353
366
|
runtime,
|
|
354
367
|
id: str(p, "--id"),
|
|
368
|
+
// AG-17.1c: the conversation's own model unless one is given.
|
|
369
|
+
...(str(p, "--model") !== undefined ? { model: str(p, "--model") } : {}),
|
|
370
|
+
...(str(p, "--version") !== undefined
|
|
371
|
+
? { version: str(p, "--version") }
|
|
372
|
+
: {}),
|
|
355
373
|
});
|
|
356
374
|
return report(
|
|
357
375
|
d,
|
|
@@ -360,6 +378,52 @@ async function bringIn(args: string[], d: AgentsResolved): Promise<number> {
|
|
|
360
378
|
);
|
|
361
379
|
}
|
|
362
380
|
|
|
381
|
+
/**
|
|
382
|
+
* `agents persona <id> <persona> [-w <ws>] [--model <m>] [--version <v>]
|
|
383
|
+
* [--now | --after-turn]` (AG-18). A person's only: refused inside an
|
|
384
|
+
* agent's session (its DEN_ACTOR_TOKEN), so an agent can't widen its own
|
|
385
|
+
* permissions.
|
|
386
|
+
*/
|
|
387
|
+
async function persona(args: string[], d: AgentsResolved): Promise<number> {
|
|
388
|
+
const where = "agents persona";
|
|
389
|
+
if (d.env[ACTOR_TOKEN_ENV] !== undefined)
|
|
390
|
+
throw new AgentError(
|
|
391
|
+
`${where}: only a person can change an agent's persona (this is an agent's session)`,
|
|
392
|
+
);
|
|
393
|
+
const p = parseArgs(where, args, {
|
|
394
|
+
"--workspace": true,
|
|
395
|
+
"--model": true,
|
|
396
|
+
"--version": true,
|
|
397
|
+
"--now": false,
|
|
398
|
+
"--after-turn": false,
|
|
399
|
+
});
|
|
400
|
+
noExtra(where, p, 2);
|
|
401
|
+
const [id, to] = p.positional;
|
|
402
|
+
if (!id || !to) throw new UsageError(`${where} needs <id> <persona>`);
|
|
403
|
+
if (p.flags["--now"] && p.flags["--after-turn"])
|
|
404
|
+
throw new UsageError(`${where}: --now or --after-turn, not both`);
|
|
405
|
+
const when = p.flags["--now"]
|
|
406
|
+
? "now"
|
|
407
|
+
: p.flags["--after-turn"]
|
|
408
|
+
? "after-turn"
|
|
409
|
+
: undefined;
|
|
410
|
+
if (when === "after-turn") d.out(`waiting for agent ${id}'s turn to end…`);
|
|
411
|
+
const s = await changePersona(d, id, workspaceFlag(where, p), {
|
|
412
|
+
persona: to,
|
|
413
|
+
...(str(p, "--model") !== undefined ? { model: str(p, "--model") } : {}),
|
|
414
|
+
...(str(p, "--version") !== undefined
|
|
415
|
+
? { version: str(p, "--version") }
|
|
416
|
+
: {}),
|
|
417
|
+
...(when ? { when } : {}),
|
|
418
|
+
by: "cli",
|
|
419
|
+
});
|
|
420
|
+
return report(
|
|
421
|
+
d,
|
|
422
|
+
s,
|
|
423
|
+
`agent ${s.rec.id} is now ${s.rec.persona} (was ${s.from}); same conversation, as ${s.rec.sessionName} in ${s.rec.cwd}`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
363
427
|
/** `<id> [-w <ws>]`. */
|
|
364
428
|
function idAndWorkspace(
|
|
365
429
|
where: string,
|
|
@@ -700,6 +764,9 @@ export async function cmdAgents(
|
|
|
700
764
|
case "import":
|
|
701
765
|
await loadAgentWorkspaces(deps);
|
|
702
766
|
return await bringIn(rest, d);
|
|
767
|
+
case "persona":
|
|
768
|
+
await loadAgentWorkspaces(deps);
|
|
769
|
+
return await persona(rest, d);
|
|
703
770
|
case "rm":
|
|
704
771
|
await loadAgentWorkspaces(deps);
|
|
705
772
|
return await rm(rest, d);
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* nothing is sent.
|
|
23
23
|
*/
|
|
24
24
|
import chalk from "chalk";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
readDenConfig,
|
|
27
|
+
readMcpConfig,
|
|
28
|
+
type DenConfig,
|
|
29
|
+
} from "../../shared/den-config.js";
|
|
30
|
+
import { approvalsState } from "../mcp/tools.js";
|
|
26
31
|
import { getStateDir } from "../../shared/paths.js";
|
|
27
32
|
import { product } from "../../shared/product.js";
|
|
28
33
|
import { listRecords } from "../../shared/agents/records.js";
|
|
@@ -216,6 +221,25 @@ export function agentsConfigured(
|
|
|
216
221
|
}
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The held writes' state (mcp.approvals), as doctor prints it. A bad value
|
|
226
|
+
* is a problem: the MCP server refuses to start on it.
|
|
227
|
+
*/
|
|
228
|
+
export function approvalsLines(read: () => boolean): {
|
|
229
|
+
lines: string[];
|
|
230
|
+
problem: boolean;
|
|
231
|
+
} {
|
|
232
|
+
try {
|
|
233
|
+
const state = approvalsState({ approvals: read() });
|
|
234
|
+
return { lines: ["", ` ℹ MCP ${state}`], problem: false };
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return {
|
|
237
|
+
lines: ["", chalk.red(` ✗ MCP ${(err as Error).message}`)],
|
|
238
|
+
problem: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
219
243
|
/** Informational lines after the rows: never a failure. */
|
|
220
244
|
export function doctorNotes(cli: string, configured: boolean): string[] {
|
|
221
245
|
return configured
|
|
@@ -312,6 +336,8 @@ export async function cmdDoctor(args: string[]): Promise<number> {
|
|
|
312
336
|
}
|
|
313
337
|
const rows = doctorRows();
|
|
314
338
|
for (const l of formatDoctor(rows, readBrand().name)) console.log(l);
|
|
339
|
+
const held = approvalsLines(() => readMcpConfig().approvals);
|
|
340
|
+
for (const l of held.lines) console.log(l);
|
|
315
341
|
for (const l of pollingLines(eventsFile(), readBrand().name)) console.log(l);
|
|
316
342
|
const cli = product().cli;
|
|
317
343
|
if (!probe) {
|
|
@@ -320,7 +346,7 @@ export async function cmdDoctor(args: string[]): Promise<number> {
|
|
|
320
346
|
agentsConfigured(readConfigOrNull(), getStateDir()),
|
|
321
347
|
))
|
|
322
348
|
console.log(l);
|
|
323
|
-
return rows.some((r) => r.problem) ? 1 : 0;
|
|
349
|
+
return rows.some((r) => r.problem) || held.problem ? 1 : 0;
|
|
324
350
|
}
|
|
325
351
|
const { runEnvProbe, formatProbe } = await import("./env-probe.js");
|
|
326
352
|
const { resolveDeps, plainSessionEnv } =
|
|
@@ -339,5 +365,5 @@ export async function cmdDoctor(args: string[]): Promise<number> {
|
|
|
339
365
|
console.log("");
|
|
340
366
|
for (const l of formatProbe(results)) console.log(l);
|
|
341
367
|
const found = results.some((r) => r.findings.length);
|
|
342
|
-
return rows.some((r) => r.problem) || found ? 1 : 0;
|
|
368
|
+
return rows.some((r) => r.problem) || held.problem || found ? 1 : 0;
|
|
343
369
|
}
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
import { readWebPort } from "../../shared/edge/config.js";
|
|
29
29
|
import { DEFAULT_PORT } from "../../shared/constants.js";
|
|
30
30
|
import { needsDocker } from "./repo-detect.js";
|
|
31
|
+
import { readMcpConfig } from "../../shared/den-config.js";
|
|
32
|
+
import { approvalsState } from "../mcp/tools.js";
|
|
31
33
|
import type { WebProbe } from "./mcp-connect.js";
|
|
32
34
|
|
|
33
35
|
export type CheckStatus = "ok" | "warn" | "fail";
|
|
@@ -59,6 +61,8 @@ export interface OnboardDeps {
|
|
|
59
61
|
probeWeb: (url: string) => Promise<WebProbe>;
|
|
60
62
|
/** The MCP one-shot against the web server: exit 0, or why not. */
|
|
61
63
|
mcpCall: () => { ok: boolean; detail?: string };
|
|
64
|
+
/** `mcp.approvals` (throws on a bad value): whether the held writes are on. */
|
|
65
|
+
approvals: () => boolean;
|
|
62
66
|
/** The command an agent runs, e.g. `./bin/acme`. */
|
|
63
67
|
bin: string;
|
|
64
68
|
}
|
|
@@ -193,9 +197,30 @@ export async function onboardingChecks(
|
|
|
193
197
|
},
|
|
194
198
|
);
|
|
195
199
|
}
|
|
200
|
+
out.push(approvalsCheck(d));
|
|
196
201
|
return out;
|
|
197
202
|
}
|
|
198
203
|
|
|
204
|
+
/** `mcp:approvals`: which state the held writes are in. Never needs the server. */
|
|
205
|
+
function approvalsCheck(d: OnboardDeps): OnboardCheck {
|
|
206
|
+
let on: boolean;
|
|
207
|
+
try {
|
|
208
|
+
on = d.approvals();
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
id: "mcp:approvals",
|
|
212
|
+
status: "fail",
|
|
213
|
+
detail: (err as Error).message,
|
|
214
|
+
ask: "Set mcp.approvals in the distribution config to true or false.",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
id: "mcp:approvals",
|
|
219
|
+
status: "ok",
|
|
220
|
+
detail: approvalsState({ approvals: on }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
function onPath(bin: string): boolean {
|
|
200
225
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
201
226
|
if (!dir) continue;
|
|
@@ -286,6 +311,7 @@ export function defaultOnboardDeps(
|
|
|
286
311
|
`exit ${r.status ?? "timeout"}`,
|
|
287
312
|
};
|
|
288
313
|
},
|
|
314
|
+
approvals: () => readMcpConfig().approvals,
|
|
289
315
|
bin: runnableCli(),
|
|
290
316
|
};
|
|
291
317
|
}
|
|
@@ -181,7 +181,15 @@ function validateFlagScopes(parsed: ParsedCliArgs): void {
|
|
|
181
181
|
*/
|
|
182
182
|
export const SUBCOMMAND_OWN_HELP: Readonly<Record<string, readonly string[]>> =
|
|
183
183
|
{
|
|
184
|
-
web: [
|
|
184
|
+
web: [
|
|
185
|
+
"token",
|
|
186
|
+
"login",
|
|
187
|
+
"sessions",
|
|
188
|
+
"logout",
|
|
189
|
+
"owner",
|
|
190
|
+
"approve",
|
|
191
|
+
"claim-link",
|
|
192
|
+
],
|
|
185
193
|
mcp: ["connect", "disconnect", "clients", "config"],
|
|
186
194
|
repo: ["add"],
|
|
187
195
|
};
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -366,12 +366,22 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
366
366
|
},
|
|
367
367
|
repo: {
|
|
368
368
|
usage:
|
|
369
|
-
'repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>] [--run "<cmd>"] [--health <url>] [--json]',
|
|
369
|
+
'repo add <path|git-url> [--sidecar] [-w <workspace>] [--name <repo>] [--run "<cmd>"] [--health <url>] [--dry-run] [--json] | repo detect <folder> [--json]',
|
|
370
370
|
lines: [
|
|
371
371
|
"Add a repo with no prompts, never writing into it. A repo with no",
|
|
372
372
|
"manifest (or --sidecar) gets a side-car manifest in manifests/, detected",
|
|
373
373
|
"from its Makefile, compose file or package.json scripts. Safe to re-run.",
|
|
374
|
-
"
|
|
374
|
+
"--dry-run prints the side-car it would write, validated, and writes nothing.",
|
|
375
|
+
"detect: the git repos in a folder in your home, how each runs, and",
|
|
376
|
+
"suggested workspaces. `{cli} repo add --help` says more.",
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
onboard: {
|
|
380
|
+
usage: "onboard status [--json]",
|
|
381
|
+
lines: [
|
|
382
|
+
"What's set up (a distribution, the web server, workspaces, repos, a",
|
|
383
|
+
"running service, Den's MCP) and the one next step, with its command.",
|
|
384
|
+
"Reads only; it never runs an agent's CLI.",
|
|
375
385
|
],
|
|
376
386
|
},
|
|
377
387
|
relocate: {
|
|
@@ -595,6 +605,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
595
605
|
"theme",
|
|
596
606
|
"plugin",
|
|
597
607
|
"repo",
|
|
608
|
+
"onboard",
|
|
598
609
|
]);
|
|
599
610
|
|
|
600
611
|
/**
|
|
@@ -975,6 +986,15 @@ class DenCLI {
|
|
|
975
986
|
break;
|
|
976
987
|
}
|
|
977
988
|
|
|
989
|
+
case "onboard": {
|
|
990
|
+
const { cmdOnboard } = await import("./onboard-command.js");
|
|
991
|
+
const { probeWebServer } = await import("./mcp-connect.js");
|
|
992
|
+
process.exitCode = await cmdOnboard(args.slice(1), (u) =>
|
|
993
|
+
probeWebServer(u),
|
|
994
|
+
);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
|
|
978
998
|
case "relocate":
|
|
979
999
|
await this.cmdRelocate(args.slice(1));
|
|
980
1000
|
break;
|
|
@@ -5271,7 +5291,13 @@ class DenCLI {
|
|
|
5271
5291
|
" register [path] Register a repo in the service catalog",
|
|
5272
5292
|
);
|
|
5273
5293
|
console.log(
|
|
5274
|
-
" repo add <path|url> Add a repo with no prompts, never changing it (--sidecar, --json)",
|
|
5294
|
+
" repo add <path|url> Add a repo with no prompts, never changing it (--sidecar, --dry-run, --json)",
|
|
5295
|
+
);
|
|
5296
|
+
console.log(
|
|
5297
|
+
" repo detect <folder> Find the repos in a folder and suggest workspaces (--json)",
|
|
5298
|
+
);
|
|
5299
|
+
console.log(
|
|
5300
|
+
" onboard status What's set up, and the next step (--json)",
|
|
5275
5301
|
);
|
|
5276
5302
|
console.log(
|
|
5277
5303
|
" relocate <repo> <path> Point a moved checkout's workspace entries at its new path",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> onboard status [--json]` (ON-3): what's set up and the one next
|
|
3
|
+
* step, the CLI twin of the MCP tool `onboard_status` and of
|
|
4
|
+
* GET /api/onboard/status. `doctor --json` has the machine checks (Node,
|
|
5
|
+
* git, Docker, the web server, Den's MCP); this adds what onboarding needs
|
|
6
|
+
* on top (a distribution, workspaces, repos, a running service) and picks
|
|
7
|
+
* the next step. Reads only. It never runs an agent's CLI: Den's MCP is
|
|
8
|
+
* checked in the agents' config files (onboard-reads.ts mcpRegistration).
|
|
9
|
+
* Exit 0 whenever it answers, whatever is left to do.
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "child_process";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { product } from "../../shared/product.js";
|
|
16
|
+
import { getDistRoot } from "../../shared/paths.js";
|
|
17
|
+
import {
|
|
18
|
+
DEN_SERVICES_DIR,
|
|
19
|
+
readWorkspacesFile,
|
|
20
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
21
|
+
import { listDaemons } from "../../shared/environment-manager/service-runner.js";
|
|
22
|
+
import { defaultOnboardDeps, runnableCli } from "./doctor-onboard.js";
|
|
23
|
+
import type { WebProbe } from "./mcp-connect.js";
|
|
24
|
+
import {
|
|
25
|
+
mcpRegistration,
|
|
26
|
+
onboardStatus,
|
|
27
|
+
type OnboardStatus,
|
|
28
|
+
type OnboardStatusDeps,
|
|
29
|
+
type RegisteredRepo,
|
|
30
|
+
} from "./onboard-reads.js";
|
|
31
|
+
import { printJson } from "./json-out.js";
|
|
32
|
+
|
|
33
|
+
/** Every repo in a workspace, at the folder Den runs it from. */
|
|
34
|
+
export function registeredRepos(): RegisteredRepo[] {
|
|
35
|
+
let file;
|
|
36
|
+
try {
|
|
37
|
+
file = readWorkspacesFile();
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const out: RegisteredRepo[] = [];
|
|
42
|
+
for (const [workspace, ws] of Object.entries(file.workspaces))
|
|
43
|
+
for (const [addr, entry] of Object.entries(ws?.services ?? {})) {
|
|
44
|
+
const repo = addr.split("/")[0];
|
|
45
|
+
const dir =
|
|
46
|
+
entry.mode === "dev-managed"
|
|
47
|
+
? entry.path
|
|
48
|
+
: path.join(DEN_SERVICES_DIR, repo, entry.branch ?? "main");
|
|
49
|
+
if (dir) out.push({ path: dir, workspace, repo });
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readText(file: string): string | null {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(file, "utf-8");
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether the Docker daemon answers, without blocking the event loop. */
|
|
63
|
+
function dockerUpAsync(): Promise<boolean> {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
execFile(
|
|
66
|
+
"docker",
|
|
67
|
+
["info", "--format", "{{.ServerVersion}}"],
|
|
68
|
+
{ timeout: 10_000 },
|
|
69
|
+
(err) => resolve(!err),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** This machine's deps for onboardStatus. */
|
|
75
|
+
export async function defaultStatusDeps(o: {
|
|
76
|
+
probeWeb: (url: string) => Promise<WebProbe>;
|
|
77
|
+
webUrl?: string;
|
|
78
|
+
viaMcp?: boolean;
|
|
79
|
+
}): Promise<OnboardStatusDeps> {
|
|
80
|
+
const doctor = defaultOnboardDeps(o.probeWeb);
|
|
81
|
+
if (o.webUrl) doctor.webUrl = o.webUrl;
|
|
82
|
+
// Docker is asked only when a registered repo needs it, and asynchronously:
|
|
83
|
+
// the web server answers this route too.
|
|
84
|
+
const dockerRepos = doctor.dockerRepos();
|
|
85
|
+
const dockerUp =
|
|
86
|
+
dockerRepos.length && doctor.which("docker")
|
|
87
|
+
? await dockerUpAsync()
|
|
88
|
+
: false;
|
|
89
|
+
const bin = runnableCli();
|
|
90
|
+
return {
|
|
91
|
+
bin,
|
|
92
|
+
distribution: fs.existsSync(path.join(getDistRoot(), "bin", product().cli)),
|
|
93
|
+
doctor: {
|
|
94
|
+
...doctor,
|
|
95
|
+
dockerRepos: () => dockerRepos,
|
|
96
|
+
dockerUp: () => dockerUp,
|
|
97
|
+
},
|
|
98
|
+
workspaces: () => {
|
|
99
|
+
let file;
|
|
100
|
+
try {
|
|
101
|
+
file = readWorkspacesFile();
|
|
102
|
+
} catch {
|
|
103
|
+
return { names: [], active: [], repos: [] };
|
|
104
|
+
}
|
|
105
|
+
const repos = new Set<string>();
|
|
106
|
+
for (const ws of Object.values(file.workspaces))
|
|
107
|
+
for (const addr of Object.keys(ws?.services ?? {}))
|
|
108
|
+
repos.add(addr.split("/")[0]);
|
|
109
|
+
return {
|
|
110
|
+
names: Object.keys(file.workspaces),
|
|
111
|
+
active: file.active.filter((n) => n in file.workspaces),
|
|
112
|
+
repos: [...repos],
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
running: () => {
|
|
116
|
+
try {
|
|
117
|
+
return listDaemons().map((d) => d.address);
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
mcp: () =>
|
|
123
|
+
mcpRegistration({
|
|
124
|
+
cli: bin,
|
|
125
|
+
home: os.homedir(),
|
|
126
|
+
platform: process.platform,
|
|
127
|
+
appData: process.env.APPDATA,
|
|
128
|
+
projectDir: getDistRoot(),
|
|
129
|
+
which: doctor.which,
|
|
130
|
+
read: readText,
|
|
131
|
+
viaMcp: o.viaMcp,
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const MARK: Record<string, string> = { ok: "✓", warn: "!", todo: "·" };
|
|
137
|
+
|
|
138
|
+
function printHuman(r: OnboardStatus): void {
|
|
139
|
+
for (const c of r.checks)
|
|
140
|
+
console.log(` ${MARK[c.status] ?? " "} ${c.id.padEnd(12)} ${c.detail}`);
|
|
141
|
+
console.log(`\n Next: ${r.next.step}`);
|
|
142
|
+
if (r.next.command) console.log(` Run: ${r.next.command}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const ONBOARD_USAGE = () => `${product().cli} onboard status [--json]`;
|
|
146
|
+
|
|
147
|
+
/** `onboard <sub> …`; returns the exit code. */
|
|
148
|
+
export async function cmdOnboard(
|
|
149
|
+
argv: string[],
|
|
150
|
+
probeWeb: (url: string) => Promise<WebProbe>,
|
|
151
|
+
): Promise<number> {
|
|
152
|
+
const [sub, ...rest] = argv;
|
|
153
|
+
const json = rest.includes("--json");
|
|
154
|
+
const unknown = rest.filter((a) => a !== "--json");
|
|
155
|
+
if (sub !== "status" || unknown.length) {
|
|
156
|
+
const error =
|
|
157
|
+
sub === undefined || sub === "--help" || sub === "-h"
|
|
158
|
+
? null
|
|
159
|
+
: sub !== "status"
|
|
160
|
+
? `unknown onboard subcommand "${sub}"`
|
|
161
|
+
: `unknown flag ${unknown[0]}`;
|
|
162
|
+
if (json && error)
|
|
163
|
+
printJson({
|
|
164
|
+
ok: false,
|
|
165
|
+
error,
|
|
166
|
+
fix: `${runnableCli()} onboard status --json`,
|
|
167
|
+
});
|
|
168
|
+
else {
|
|
169
|
+
if (error) console.error(`✗ ${error}`);
|
|
170
|
+
console.log(
|
|
171
|
+
`\n ${ONBOARD_USAGE()}\n\n What's set up (a distribution, the web server, workspaces, repos,\n a running service, Den's MCP) and the one next step, with its command.\n`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return error ? 2 : sub === undefined ? 1 : 0;
|
|
175
|
+
}
|
|
176
|
+
const r = await onboardStatus(await defaultStatusDeps({ probeWeb }));
|
|
177
|
+
if (json) printJson(r);
|
|
178
|
+
else printHuman(r);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|