@monster-orange/den 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +593 -0
- package/docs/distributions.md +429 -18
- package/package.json +6 -1
- package/src/shared/actor-token.ts +275 -0
- package/src/shared/agent-actor.ts +12 -2
- package/src/shared/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +271 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +199 -0
- package/src/shared/agents/runtimes.ts +54 -0
- package/src/shared/den-config.ts +116 -0
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/scope.ts +22 -0
- package/src/shared/environment-manager/service-runner.ts +94 -2
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/profile.ts +15 -4
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/sessions/client.ts +284 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +89 -0
- package/src/shared/sessions/host.ts +690 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- package/src/shared/web-secret.ts +181 -0
- package/src/shared/web-session.ts +491 -0
- package/src/ui/cli/agent-ops.ts +843 -0
- package/src/ui/cli/agents-command.ts +294 -0
- package/src/ui/cli/doctor-command.ts +138 -9
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +32 -3
- package/src/ui/cli/index.ts +128 -12
- package/src/ui/cli/sessions-command.ts +750 -0
- package/src/ui/cli/target-workspace.ts +105 -0
- package/src/ui/cli/web-command.ts +536 -14
- package/src/ui/mcp/api.ts +7 -0
- package/src/ui/mcp/scope.ts +123 -19
- package/src/ui/mcp/server.ts +100 -7
- package/src/ui/mcp/tools.ts +44 -5
- package/src/ui/server/agent-routes.ts +323 -0
- package/src/ui/server/audit.ts +111 -22
- package/src/ui/server/http-errors.ts +21 -3
- package/src/ui/server/index.ts +820 -65
- package/src/ui/server/public/agents-page.js +648 -0
- package/src/ui/server/public/agents-section.js +299 -0
- package/src/ui/server/public/agents.html +439 -0
- package/src/ui/server/public/appearance.js +3 -0
- package/src/ui/server/public/error-page.js +103 -0
- package/src/ui/server/public/header.js +45 -11
- package/src/ui/server/public/i18n/es.js +90 -1
- package/src/ui/server/public/i18n/ja.js +86 -1
- package/src/ui/server/public/local-dev.html +53 -0
- package/src/ui/server/public/logs.html +3 -0
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/release.html +8 -0
- package/src/ui/server/public/settings-page.js +5 -0
- package/src/ui/server/public/settings-registry.js +33 -0
- package/src/ui/server/public/shell.js +248 -5
- package/src/ui/server/public/topbar.js +51 -2
- package/src/ui/server/readonly-routes.ts +25 -2
- package/src/ui/server/request-guard.ts +670 -0
- package/src/ui/server/route-classes.ts +317 -0
- package/src/ui/server/session-routes.ts +445 -0
- package/src/ui/server/shutdown-guard.ts +91 -0
- package/src/ui/server/sudo-store.ts +29 -3
- package/src/ui/server/ui-assets.ts +10 -1
- package/src/ui/server/websocket.ts +326 -0
- package/src/ui/server/workspace-routes.ts +16 -22
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> doctor --probe` (AG spec §5a-env): check, through the real session
|
|
3
|
+
* host and backend, what environment a session actually gets. Opt-in: plain
|
|
4
|
+
* `doctor` never spawns anything.
|
|
5
|
+
*
|
|
6
|
+
* For a plain session (as `sessions start` builds it, no --keep-env) and
|
|
7
|
+
* for each available runtime's agent env (as `agents start` builds it, the
|
|
8
|
+
* runtime's launch env included, but running the probe instead of the
|
|
9
|
+
* runtime's CLI), it starts a short session running a tiny node script that
|
|
10
|
+
* prints each key name and one flag: whether the name or value is
|
|
11
|
+
* credential-shaped (secret-env.ts, with the redactor's patterns). Values
|
|
12
|
+
* never leave the probe process.
|
|
13
|
+
*
|
|
14
|
+
* A finding is any runtime's scrubEnv name, or any credential-shaped
|
|
15
|
+
* variable, in either env.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as os from "os";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import {
|
|
21
|
+
agentSessionEnv,
|
|
22
|
+
allScrubEnv,
|
|
23
|
+
getRuntime,
|
|
24
|
+
LaunchError,
|
|
25
|
+
runtimeIds,
|
|
26
|
+
type LaunchInput,
|
|
27
|
+
type Runtime,
|
|
28
|
+
} from "../../shared/agents/runtimes.js";
|
|
29
|
+
import { credentialPatternData } from "../../shared/secret-env.js";
|
|
30
|
+
import type { HostClient } from "../../shared/sessions/client.js";
|
|
31
|
+
import { matchesScrub } from "../../shared/sessions/env.js";
|
|
32
|
+
import { sessionName } from "../../shared/sessions/names.js";
|
|
33
|
+
import type { PtyLoad } from "../../shared/sessions/pty.js";
|
|
34
|
+
|
|
35
|
+
const MARK = "DENPROBE";
|
|
36
|
+
const PROBE_ID = "env-probe";
|
|
37
|
+
/** How long one probe session gets to print and exit. */
|
|
38
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
39
|
+
|
|
40
|
+
/** The probe: key names and a 0/1 flag per key, then an end mark. No values. */
|
|
41
|
+
export function probeScript(): string {
|
|
42
|
+
const p = JSON.stringify(credentialPatternData());
|
|
43
|
+
return [
|
|
44
|
+
`const P = ${p};`,
|
|
45
|
+
`const re = (l) => l.map((r) => new RegExp(r.source, r.flags));`,
|
|
46
|
+
`const N = re(P.name), V = re(P.value);`,
|
|
47
|
+
`for (const k of Object.keys(process.env).sort()) {`,
|
|
48
|
+
` const v = process.env[k] || "";`,
|
|
49
|
+
` const c = N.some((r) => r.test(k)) || (v !== "" && V.some((r) => r.test(v)));`,
|
|
50
|
+
` process.stdout.write("${MARK} " + k + " " + (c ? 1 : 0) + "\\n");`,
|
|
51
|
+
`}`,
|
|
52
|
+
`process.stdout.write("${MARK}-END\\n");`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ProbeKey {
|
|
57
|
+
name: string;
|
|
58
|
+
credentialShaped: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The probe's report from its output; null when it did not finish. */
|
|
62
|
+
export function parseProbe(output: string): ProbeKey[] | null {
|
|
63
|
+
const keys: ProbeKey[] = [];
|
|
64
|
+
let ended = false;
|
|
65
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
66
|
+
const line = raw.replace(/\r/g, "").trim();
|
|
67
|
+
if (line === `${MARK}-END`) ended = true;
|
|
68
|
+
const m = new RegExp(`^${MARK} ([A-Za-z_][A-Za-z0-9_]*) ([01])$`).exec(
|
|
69
|
+
line,
|
|
70
|
+
);
|
|
71
|
+
if (m) keys.push({ name: m[1], credentialShaped: m[2] === "1" });
|
|
72
|
+
}
|
|
73
|
+
return ended ? keys : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ProbeFinding {
|
|
77
|
+
key: string;
|
|
78
|
+
why: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ProbeResult {
|
|
82
|
+
/** "plain session", or "agent (<runtime>)". */
|
|
83
|
+
target: string;
|
|
84
|
+
/** False: not probed (the reason says why); never a failure. */
|
|
85
|
+
ran: boolean;
|
|
86
|
+
reason?: string;
|
|
87
|
+
keys?: number;
|
|
88
|
+
findings: ProbeFinding[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ProbeDeps {
|
|
92
|
+
dist: string;
|
|
93
|
+
cli: string;
|
|
94
|
+
env: NodeJS.ProcessEnv;
|
|
95
|
+
pty: () => PtyLoad;
|
|
96
|
+
connect: () => Promise<HostClient>;
|
|
97
|
+
/** A plain session's env, as `sessions start` builds it. */
|
|
98
|
+
plainEnv: (caller: NodeJS.ProcessEnv) => Record<string, string>;
|
|
99
|
+
/** An agent's env, as `agents start` builds it. */
|
|
100
|
+
agentEnv?: (
|
|
101
|
+
caller: NodeJS.ProcessEnv,
|
|
102
|
+
rt: Runtime,
|
|
103
|
+
launch: Record<string, string>,
|
|
104
|
+
agentId: string,
|
|
105
|
+
) => Record<string, string>;
|
|
106
|
+
/** Runtimes to probe (default: every registered one that is available). */
|
|
107
|
+
runtimes?: () => Runtime[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A launch input for the probe: nothing is written, nothing it names runs. */
|
|
111
|
+
function probeInput(d: ProbeDeps, dir: string): LaunchInput {
|
|
112
|
+
return {
|
|
113
|
+
persona: {
|
|
114
|
+
id: PROBE_ID,
|
|
115
|
+
role: "",
|
|
116
|
+
prompt: "",
|
|
117
|
+
standing: false,
|
|
118
|
+
tools: [],
|
|
119
|
+
context: [],
|
|
120
|
+
argv: [PROBE_ID],
|
|
121
|
+
source: "doctor --probe",
|
|
122
|
+
},
|
|
123
|
+
workspace: { name: "doctor", dir },
|
|
124
|
+
agentId: PROBE_ID,
|
|
125
|
+
worktree: dir,
|
|
126
|
+
mcp: { name: d.cli, command: [d.cli], env: {}, scope: "doctor" },
|
|
127
|
+
// Never written: the probe uses only the launch's env.
|
|
128
|
+
filesDir: path.join(dir, PROBE_ID),
|
|
129
|
+
env: d.env,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findings(keys: ProbeKey[]): ProbeFinding[] {
|
|
134
|
+
const scrub = allScrubEnv();
|
|
135
|
+
const out: ProbeFinding[] = [];
|
|
136
|
+
for (const k of keys) {
|
|
137
|
+
if (matchesScrub(k.name, scrub))
|
|
138
|
+
out.push({ key: k.name, why: "a runtime's scrubEnv name" });
|
|
139
|
+
else if (k.credentialShaped)
|
|
140
|
+
out.push({ key: k.name, why: "credential-shaped" });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Run the probe in one session with `env`; its report, or why not. */
|
|
146
|
+
async function probeOnce(
|
|
147
|
+
d: ProbeDeps,
|
|
148
|
+
c: HostClient,
|
|
149
|
+
target: string,
|
|
150
|
+
slug: string,
|
|
151
|
+
env: Record<string, string>,
|
|
152
|
+
cwd: string,
|
|
153
|
+
): Promise<ProbeResult> {
|
|
154
|
+
const info = await c.start({
|
|
155
|
+
name: sessionName(d.dist, "doctor", `${PROBE_ID}-${slug}`),
|
|
156
|
+
cwd,
|
|
157
|
+
argv: [process.execPath, "-e", probeScript()],
|
|
158
|
+
env,
|
|
159
|
+
cols: 200,
|
|
160
|
+
rows: 24,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
const deadline = Date.now() + PROBE_TIMEOUT_MS;
|
|
164
|
+
for (;;) {
|
|
165
|
+
const s = (await c.list()).find((x) => x.ref.id === info.ref.id);
|
|
166
|
+
if (!s || s.exited) break;
|
|
167
|
+
if (Date.now() > deadline) {
|
|
168
|
+
await c.kill(info.ref.id, "KILL").catch(() => undefined);
|
|
169
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
173
|
+
}
|
|
174
|
+
const keys = parseProbe(
|
|
175
|
+
(await c.scrollback(info.ref.id, 1024 * 1024)).toString("utf-8"),
|
|
176
|
+
);
|
|
177
|
+
if (!keys)
|
|
178
|
+
return {
|
|
179
|
+
target,
|
|
180
|
+
ran: true,
|
|
181
|
+
findings: [{ key: "-", why: "the probe did not finish" }],
|
|
182
|
+
};
|
|
183
|
+
return { target, ran: true, keys: keys.length, findings: findings(keys) };
|
|
184
|
+
} finally {
|
|
185
|
+
await c.forget(info.ref.id).catch(() => undefined);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Probe a plain session and each available runtime's agent env. */
|
|
190
|
+
export async function runEnvProbe(d: ProbeDeps): Promise<ProbeResult[]> {
|
|
191
|
+
const pty = d.pty();
|
|
192
|
+
if (!pty.available)
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
target: "sessions",
|
|
196
|
+
ran: false,
|
|
197
|
+
reason: `den-pty is unavailable: ${pty.reason}`,
|
|
198
|
+
findings: [],
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
const runtimes =
|
|
202
|
+
d.runtimes?.() ??
|
|
203
|
+
runtimeIds()
|
|
204
|
+
.map((id) => getRuntime(id))
|
|
205
|
+
.filter((rt): rt is Runtime => !!rt);
|
|
206
|
+
const agentEnv = d.agentEnv ?? agentSessionEnv;
|
|
207
|
+
// The probe writes nothing: its cwd is the temp dir, as it is.
|
|
208
|
+
const dir = fs.realpathSync(os.tmpdir());
|
|
209
|
+
const c = await d.connect();
|
|
210
|
+
try {
|
|
211
|
+
const out: ProbeResult[] = [
|
|
212
|
+
await probeOnce(d, c, "plain session", "plain", d.plainEnv(d.env), dir),
|
|
213
|
+
];
|
|
214
|
+
for (const rt of runtimes) {
|
|
215
|
+
const target = `agent (${rt.id})`;
|
|
216
|
+
const det = rt.detect(d.env);
|
|
217
|
+
if (!det.available) {
|
|
218
|
+
out.push({
|
|
219
|
+
target,
|
|
220
|
+
ran: false,
|
|
221
|
+
reason: `${rt.id} is unavailable: ${det.why ?? "unknown"}`,
|
|
222
|
+
findings: [],
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
let launch: Record<string, string>;
|
|
227
|
+
try {
|
|
228
|
+
launch = rt.launch(probeInput(d, dir)).env;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (!(e instanceof LaunchError)) throw e;
|
|
231
|
+
out.push({ target, ran: false, reason: e.message, findings: [] });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
out.push(
|
|
235
|
+
await probeOnce(
|
|
236
|
+
d,
|
|
237
|
+
c,
|
|
238
|
+
target,
|
|
239
|
+
rt.id,
|
|
240
|
+
agentEnv(d.env, rt, launch, PROBE_ID),
|
|
241
|
+
dir,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
} finally {
|
|
247
|
+
c.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** One line per result, one per finding; key names only. */
|
|
252
|
+
export function formatProbe(results: ProbeResult[]): string[] {
|
|
253
|
+
const lines: string[] = [];
|
|
254
|
+
const width = Math.max(...results.map((r) => r.target.length));
|
|
255
|
+
for (const r of results) {
|
|
256
|
+
const t = r.target.padEnd(width);
|
|
257
|
+
if (!r.ran)
|
|
258
|
+
lines.push(` ${t} - not probed: ${r.reason ?? "unavailable"}`);
|
|
259
|
+
else if (!r.findings.length)
|
|
260
|
+
lines.push(` ${t} ✓ clean (${r.keys ?? 0} variables)`);
|
|
261
|
+
else
|
|
262
|
+
for (const f of r.findings)
|
|
263
|
+
lines.push(` ${t} ✗ ${f.key} reaches it (${f.why})`);
|
|
264
|
+
}
|
|
265
|
+
return lines;
|
|
266
|
+
}
|
|
@@ -77,10 +77,18 @@ const WORKSPACE_COMMANDS = new Set([
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Commands that read `-w`/`--workspace` themselves, so it is left in their
|
|
80
|
-
* args: `setup --plan -w a -w b` takes several,
|
|
81
|
-
* scopes the MCP server
|
|
80
|
+
* args: `setup --plan -w a -w b` takes several, `mcp --workspace <ws>`
|
|
81
|
+
* scopes the MCP server, and `sessions`/`agents` name a session's
|
|
82
|
+
* workspace. None of them sets the CLI's workspace override.
|
|
82
83
|
*/
|
|
83
|
-
const OWN_WORKSPACE_FLAG_COMMANDS = new Set([
|
|
84
|
+
const OWN_WORKSPACE_FLAG_COMMANDS = new Set([
|
|
85
|
+
"setup",
|
|
86
|
+
"mcp",
|
|
87
|
+
// A session's workspace is part of its name (AG-3a); -w never sets the
|
|
88
|
+
// CLI's workspace override here.
|
|
89
|
+
"sessions",
|
|
90
|
+
"agents",
|
|
91
|
+
]);
|
|
84
92
|
|
|
85
93
|
/** Whether `-w <name>` on this command requires `<name>` to be active. */
|
|
86
94
|
export function workspaceFlagRequiresActive(command: string): boolean {
|
|
@@ -162,6 +170,27 @@ function validateFlagScopes(parsed: ParsedCliArgs): void {
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Subcommands that print their own `--help`: the dispatcher's
|
|
175
|
+
* `<command> --help` must not answer for them (`web sessions --help` printed
|
|
176
|
+
* the general web help). web-command.ts's WEB_SUBCOMMANDS_WITH_HELP and
|
|
177
|
+
* mcp-connect.ts's MCP_CONNECT_SUBCOMMANDS are the same lists (a test
|
|
178
|
+
* checks).
|
|
179
|
+
*/
|
|
180
|
+
export const SUBCOMMAND_OWN_HELP: Readonly<Record<string, readonly string[]>> =
|
|
181
|
+
{
|
|
182
|
+
web: ["token", "login", "sessions", "logout"],
|
|
183
|
+
mcp: ["connect", "disconnect", "clients", "config"],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Whether `<command> <sub> --help` is the subcommand's own to answer. */
|
|
187
|
+
export function ownsSubcommandHelp(
|
|
188
|
+
command: string,
|
|
189
|
+
sub: string | undefined,
|
|
190
|
+
): boolean {
|
|
191
|
+
return !!sub && (SUBCOMMAND_OWN_HELP[command] ?? []).includes(sub);
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
// Dash-flags each mutation subcommand accepts (global flags — -w/--workspace,
|
|
166
195
|
// -t/--tabs/--no-tabs, --skip-secrets — are consumed by parseCliArgs before
|
|
167
196
|
// dispatch, and -h/--help is handled by the dispatcher). Commands not listed
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -193,6 +193,8 @@ import { cleanupStaleTabs } from "../../shared/cli-tabs.js";
|
|
|
193
193
|
import { cmdConfig } from "./config-command.js";
|
|
194
194
|
import { cmdEdge } from "./edge-command.js";
|
|
195
195
|
import { cmdDoctor } from "./doctor-command.js";
|
|
196
|
+
import { cmdSessions, SESSIONS_HELP } from "./sessions-command.js";
|
|
197
|
+
import { AGENTS_HELP, cmdAgents } from "./agents-command.js";
|
|
196
198
|
import { cmdService } from "./service-command.js";
|
|
197
199
|
import { cmdStack } from "./stack-command.js";
|
|
198
200
|
import { cmdTheme } from "./theme-command.js";
|
|
@@ -213,10 +215,12 @@ import {
|
|
|
213
215
|
import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
|
|
214
216
|
import { cmdWeb, displayWebHelp } from "./web-command.js";
|
|
215
217
|
import { cmdGraph } from "./graph-command.js";
|
|
218
|
+
import { resolveTargetWorkspace } from "./target-workspace.js";
|
|
216
219
|
import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
|
|
217
220
|
import {
|
|
218
221
|
parseCliArgs,
|
|
219
222
|
takeWorkspaceFlags,
|
|
223
|
+
ownsSubcommandHelp,
|
|
220
224
|
unknownCommandFlags,
|
|
221
225
|
workspaceFlagRequiresActive,
|
|
222
226
|
type ParsedCliArgs,
|
|
@@ -309,19 +313,20 @@ const WORKSPACE_SUBCOMMANDS: {
|
|
|
309
313
|
|
|
310
314
|
const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
311
315
|
down: {
|
|
312
|
-
usage: "down [repo] [flags]",
|
|
316
|
+
usage: "down [repo] [-w <workspace>] [flags]",
|
|
313
317
|
lines: [
|
|
314
318
|
"Stop the cwd (or named) repo; --with-deps also stops what it sits on (its",
|
|
315
319
|
"derived closure, in reverse order), naming anything else that uses it.",
|
|
316
320
|
"-a/--all stops EVERY workspace system (reverse derived order, cwd-agnostic;",
|
|
317
321
|
"every down script runs — no already-stopped skip).",
|
|
318
|
-
"Flags: -a/--all · -A/--app <app> · --with-deps · -n/--dry-run (print order, stop nothing) · --manifest (legacy depends_on)",
|
|
322
|
+
"Flags: -w/--workspace <ws> (that active workspace, not the focused one; focus stays) · -a/--all · -A/--app <app> · --with-deps · -n/--dry-run (print order, stop nothing) · --manifest (legacy depends_on)",
|
|
319
323
|
],
|
|
320
324
|
},
|
|
321
325
|
restart: {
|
|
322
|
-
usage: "restart [service...] [-A <app>] [-n|--dry-run]",
|
|
326
|
+
usage: "restart [service...] [-w <workspace>] [-A <app>] [-n|--dry-run]",
|
|
323
327
|
lines: [
|
|
324
328
|
`Restart the given services, or every repo in an application (\`${product().cli} apps\`).`,
|
|
329
|
+
"-w/--workspace <ws> acts in that active workspace instead of the focused one; focus stays.",
|
|
325
330
|
],
|
|
326
331
|
},
|
|
327
332
|
status: {
|
|
@@ -366,7 +371,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
366
371
|
},
|
|
367
372
|
setup: {
|
|
368
373
|
usage:
|
|
369
|
-
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
374
|
+
"setup [service...] [-w <workspace>] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
370
375
|
lines: [
|
|
371
376
|
"Run 'make setup' once per system (clones missing managed repos first).",
|
|
372
377
|
"Default targets MANAGED clones only — setup's codegen/installs",
|
|
@@ -376,6 +381,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
376
381
|
"cloned, seeded or written (on a fresh machine: the blueprint's plan).",
|
|
377
382
|
"It plans the focused workspace; -w <workspace> (repeatable) plans those,",
|
|
378
383
|
"--all-active every active one. Repos only other workspaces hold are named.",
|
|
384
|
+
"Without --plan, -w <workspace> sets up that workspace's systems instead of the focused one's.",
|
|
379
385
|
],
|
|
380
386
|
},
|
|
381
387
|
org: {
|
|
@@ -459,10 +465,14 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
459
465
|
],
|
|
460
466
|
},
|
|
461
467
|
doctor: {
|
|
462
|
-
usage: "doctor",
|
|
468
|
+
usage: "doctor [--probe]",
|
|
463
469
|
lines: [
|
|
464
470
|
"Each strategy kind (secrets, tls): the one selected, whether it was implicit,",
|
|
465
471
|
"and whether it can work here (a PATH check; nothing is run or prompted).",
|
|
472
|
+
"--probe: start a short probe session (through the session host) for a",
|
|
473
|
+
"plain session and each available runtime's agent env; key names only.",
|
|
474
|
+
"Exit 1 when a runtime's scrubEnv name or a credential-shaped variable",
|
|
475
|
+
"reaches one. Without the PTY module it says unavailable, not failed.",
|
|
466
476
|
],
|
|
467
477
|
},
|
|
468
478
|
plugin: {
|
|
@@ -508,6 +518,8 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
508
518
|
usage: "menu",
|
|
509
519
|
lines: ["Open the interactive menu (pillar/module/env picker)."],
|
|
510
520
|
},
|
|
521
|
+
sessions: SESSIONS_HELP,
|
|
522
|
+
agents: AGENTS_HELP,
|
|
511
523
|
};
|
|
512
524
|
|
|
513
525
|
function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
@@ -558,6 +570,8 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
558
570
|
"apps",
|
|
559
571
|
"applications",
|
|
560
572
|
"mcp",
|
|
573
|
+
"sessions",
|
|
574
|
+
"agents",
|
|
561
575
|
"service",
|
|
562
576
|
"theme",
|
|
563
577
|
"plugin",
|
|
@@ -581,6 +595,10 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
|
581
595
|
"local-dev",
|
|
582
596
|
"view-results",
|
|
583
597
|
"down",
|
|
598
|
+
// The session host and its clients read no org data, and stopping or
|
|
599
|
+
// listing a session is never stranded by a config that fails (AG-3a).
|
|
600
|
+
"sessions",
|
|
601
|
+
"agents",
|
|
584
602
|
]);
|
|
585
603
|
|
|
586
604
|
function configCheckedCommand(command: string): boolean {
|
|
@@ -780,10 +798,13 @@ class DenCLI {
|
|
|
780
798
|
|
|
781
799
|
// `<command> -h/--help` → that command's help (every command supports it).
|
|
782
800
|
// (`-h`/`--help` AS the command falls through to the switch → global help.)
|
|
801
|
+
// A subcommand that prints its own help (`web sessions --help`, `mcp
|
|
802
|
+
// connect --help`) is dispatched instead: its help wins.
|
|
783
803
|
if (
|
|
784
804
|
command &&
|
|
785
805
|
!["-h", "--help", "-v", "--version"].includes(command) &&
|
|
786
|
-
(ownArgs.includes("-h") || ownArgs.includes("--help"))
|
|
806
|
+
(ownArgs.includes("-h") || ownArgs.includes("--help")) &&
|
|
807
|
+
!ownsSubcommandHelp(command, ownArgs[1])
|
|
787
808
|
) {
|
|
788
809
|
this.printCommandHelp(command, ownArgs[1]);
|
|
789
810
|
return;
|
|
@@ -964,7 +985,7 @@ class DenCLI {
|
|
|
964
985
|
break;
|
|
965
986
|
|
|
966
987
|
case "doctor":
|
|
967
|
-
process.exitCode = cmdDoctor(args.slice(1));
|
|
988
|
+
process.exitCode = await cmdDoctor(args.slice(1));
|
|
968
989
|
break;
|
|
969
990
|
|
|
970
991
|
case "service":
|
|
@@ -989,6 +1010,14 @@ class DenCLI {
|
|
|
989
1010
|
cmdApps(args.slice(1));
|
|
990
1011
|
break;
|
|
991
1012
|
|
|
1013
|
+
case "sessions":
|
|
1014
|
+
process.exitCode = await cmdSessions(args.slice(1));
|
|
1015
|
+
break;
|
|
1016
|
+
|
|
1017
|
+
case "agents":
|
|
1018
|
+
process.exitCode = await cmdAgents(args.slice(1));
|
|
1019
|
+
break;
|
|
1020
|
+
|
|
992
1021
|
case "mcp": {
|
|
993
1022
|
// Setup subcommands: register the server with agents.
|
|
994
1023
|
const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
|
|
@@ -2709,6 +2738,10 @@ class DenCLI {
|
|
|
2709
2738
|
"--manifest",
|
|
2710
2739
|
`use legacy ${product().manifestYml} depends_on instead of the derived graph`,
|
|
2711
2740
|
);
|
|
2741
|
+
row(
|
|
2742
|
+
"-w, --workspace <ws>",
|
|
2743
|
+
"act in that (active) workspace instead of the focused one; focus stays",
|
|
2744
|
+
);
|
|
2712
2745
|
row("--skip-secrets", "skip the secrets-backend sign-in preflight");
|
|
2713
2746
|
row("-h, --help", "show this help");
|
|
2714
2747
|
console.log();
|
|
@@ -2954,7 +2987,44 @@ class DenCLI {
|
|
|
2954
2987
|
}
|
|
2955
2988
|
}
|
|
2956
2989
|
|
|
2990
|
+
/**
|
|
2991
|
+
* With no `-w`: the workspace the named services are in, when it isn't
|
|
2992
|
+
* the focus (target-workspace.ts), set as this run's workspace. False
|
|
2993
|
+
* (exit 1, said why) when a name is in no active workspace, in several,
|
|
2994
|
+
* or the names are in different ones.
|
|
2995
|
+
*/
|
|
2996
|
+
private pickTargetWorkspace(command: string, args: string[]): boolean {
|
|
2997
|
+
if (process.env.DEN_WORKSPACE_OVERRIDE) return true;
|
|
2998
|
+
const terminator = args.indexOf("--");
|
|
2999
|
+
const own = terminator === -1 ? args : args.slice(0, terminator);
|
|
3000
|
+
let rest: string[];
|
|
3001
|
+
try {
|
|
3002
|
+
const taken = takeAppFlag(own);
|
|
3003
|
+
if (taken.app) return true; // -A names an application, not services
|
|
3004
|
+
rest = taken.rest;
|
|
3005
|
+
} catch {
|
|
3006
|
+
return true; // the command itself reports a bad -A
|
|
3007
|
+
}
|
|
3008
|
+
const names = rest.filter((a) => !a.startsWith("-"));
|
|
3009
|
+
const r = resolveTargetWorkspace(names, product().cli, command);
|
|
3010
|
+
if (!r.ok) {
|
|
3011
|
+
console.error(chalk.red(`\n ✗ ${r.error}\n`));
|
|
3012
|
+
process.exitCode = 1;
|
|
3013
|
+
return false;
|
|
3014
|
+
}
|
|
3015
|
+
if (r.workspace) {
|
|
3016
|
+
process.env.DEN_WORKSPACE_OVERRIDE = r.workspace;
|
|
3017
|
+
console.log(
|
|
3018
|
+
chalk.gray(
|
|
3019
|
+
`\n ${names.join(", ")}: in workspace ${r.workspace} (not the focus)`,
|
|
3020
|
+
),
|
|
3021
|
+
);
|
|
3022
|
+
}
|
|
3023
|
+
return true;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2957
3026
|
private async cmdServiceUp(args: string[]): Promise<void> {
|
|
3027
|
+
if (!this.pickTargetWorkspace("up", args)) return;
|
|
2958
3028
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
2959
3029
|
// never be read as up's own flags (`den up -- -a` is NOT `up -a`).
|
|
2960
3030
|
const terminator = args.indexOf("--");
|
|
@@ -3079,6 +3149,7 @@ class DenCLI {
|
|
|
3079
3149
|
}
|
|
3080
3150
|
|
|
3081
3151
|
private async cmdServiceDown(args: string[]): Promise<void> {
|
|
3152
|
+
if (!this.pickTargetWorkspace("down", args)) return;
|
|
3082
3153
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
3083
3154
|
// never be read as down's own flags (`den down -- -a` must NOT tear
|
|
3084
3155
|
// down the whole workspace).
|
|
@@ -3149,6 +3220,7 @@ class DenCLI {
|
|
|
3149
3220
|
}
|
|
3150
3221
|
|
|
3151
3222
|
private async cmdServiceRestart(argv: string[]): Promise<void> {
|
|
3223
|
+
if (!this.pickTargetWorkspace("restart", argv)) return;
|
|
3152
3224
|
const { app, rest: args } = takeAppFlag(argv);
|
|
3153
3225
|
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
3154
3226
|
if (!dryRun) preflightSecrets();
|
|
@@ -4046,15 +4118,33 @@ class DenCLI {
|
|
|
4046
4118
|
console.log();
|
|
4047
4119
|
return;
|
|
4048
4120
|
}
|
|
4049
|
-
if (planWs.length || allActive) {
|
|
4121
|
+
if (planWs.length > 1 || allActive) {
|
|
4050
4122
|
console.error(
|
|
4051
4123
|
chalk.red(
|
|
4052
|
-
`\n -w and --all-active go with --plan: setup itself works on the
|
|
4124
|
+
`\n More than one -w, and --all-active, go with --plan: setup itself works on one workspace (-w <workspace>, or the one the named repos are in, else the focus).\n`,
|
|
4053
4125
|
),
|
|
4054
4126
|
);
|
|
4055
4127
|
process.exitCode = 1;
|
|
4056
4128
|
return;
|
|
4057
4129
|
}
|
|
4130
|
+
if (planWs.length === 1) {
|
|
4131
|
+
const ws = planWs[0];
|
|
4132
|
+
if (!readWorkspacesFile().workspaces[ws]) {
|
|
4133
|
+
console.error(chalk.red(`\n Workspace "${ws}" does not exist\n`));
|
|
4134
|
+
process.exitCode = 1;
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
if (!isWorkspaceActive(ws)) {
|
|
4138
|
+
console.error(
|
|
4139
|
+
chalk.red(
|
|
4140
|
+
`\n Workspace "${ws}" is not active — run \`${product().cli} workspace activate ${ws}\` first\n`,
|
|
4141
|
+
),
|
|
4142
|
+
);
|
|
4143
|
+
process.exitCode = 1;
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
process.env.DEN_WORKSPACE_OVERRIDE = ws;
|
|
4147
|
+
} else if (!this.pickTargetWorkspace("setup", args)) return;
|
|
4058
4148
|
|
|
4059
4149
|
// Clone any managed repos that aren't on disk yet before reading manifests.
|
|
4060
4150
|
this.cloneMissingManagedRepos();
|
|
@@ -4069,11 +4159,27 @@ class DenCLI {
|
|
|
4069
4159
|
return;
|
|
4070
4160
|
}
|
|
4071
4161
|
|
|
4162
|
+
// A name the listing doesn't spell the same way (an alias, a bare repo
|
|
4163
|
+
// listed only by subsystem) is matched as resolveService resolves it.
|
|
4164
|
+
const matches = (name: string, s: (typeof services)[number]): boolean => {
|
|
4165
|
+
if (name === s.address || name === s.repoName) return true;
|
|
4166
|
+
const r = resolveService(name);
|
|
4167
|
+
return !!r && (r.address === s.address || r.repoName === s.address);
|
|
4168
|
+
};
|
|
4169
|
+
const unmatched = names.filter((n) => !services.some((s) => matches(n, s)));
|
|
4170
|
+
if (unmatched.length > 0) {
|
|
4171
|
+
const ws = getActiveWorkspace()?.name ?? "default";
|
|
4172
|
+
console.error(
|
|
4173
|
+
chalk.red(
|
|
4174
|
+
`\n ✗ Not in workspace ${ws}: ${unmatched.join(", ")}. Nothing was set up.\n`,
|
|
4175
|
+
),
|
|
4176
|
+
);
|
|
4177
|
+
process.exitCode = 1;
|
|
4178
|
+
return;
|
|
4179
|
+
}
|
|
4072
4180
|
let targets =
|
|
4073
4181
|
names.length > 0
|
|
4074
|
-
? services.filter(
|
|
4075
|
-
(s) => names.includes(s.address) || names.includes(s.repoName),
|
|
4076
|
-
)
|
|
4182
|
+
? services.filter((s) => names.some((n) => matches(n, s)))
|
|
4077
4183
|
: services;
|
|
4078
4184
|
|
|
4079
4185
|
// Workspace-wide default: MANAGED clones only. `make setup` runs
|
|
@@ -4805,6 +4911,16 @@ class DenCLI {
|
|
|
4805
4911
|
console.log(
|
|
4806
4912
|
" plugin list Local and added plugins, checked (validate, add, update, remove)",
|
|
4807
4913
|
);
|
|
4914
|
+
console.log(
|
|
4915
|
+
" sessions start -w <ws> --name <n> -- <argv…> A terminal that outlives the web server",
|
|
4916
|
+
);
|
|
4917
|
+
console.log(
|
|
4918
|
+
" sessions list|attach|kill List, attach to (ctrl-] detaches) or stop sessions",
|
|
4919
|
+
);
|
|
4920
|
+
console.log(" agents list -w <ws> The workspace's agents");
|
|
4921
|
+
console.log(
|
|
4922
|
+
" agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, personas)",
|
|
4923
|
+
);
|
|
4808
4924
|
console.log();
|
|
4809
4925
|
console.log(
|
|
4810
4926
|
" workspace list List workspaces (active set + focus)",
|