@monster-orange/den 0.4.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 +409 -0
- package/docs/distributions.md +204 -14
- package/package.json +3 -1
- package/src/shared/actor-token.ts +275 -0
- package/src/shared/agent-actor.ts +12 -2
- package/src/shared/agents/den-claude.ts +99 -1
- package/src/shared/agents/runtime.ts +7 -0
- package/src/shared/agents/runtimes.ts +9 -2
- package/src/shared/environment-manager/scope.ts +22 -0
- package/src/shared/environment-manager/service-runner.ts +94 -2
- package/src/shared/profile.ts +15 -4
- package/src/shared/sessions/client.ts +9 -2
- package/src/shared/sessions/env.ts +13 -3
- package/src/shared/sessions/host.ts +23 -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 +96 -614
- package/src/ui/cli/flag-parser.ts +21 -0
- package/src/ui/cli/index.ts +94 -10
- package/src/ui/cli/sessions-command.ts +32 -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/server.ts +74 -6
- package/src/ui/server/agent-routes.ts +323 -0
- package/src/ui/server/audit.ts +81 -27
- package/src/ui/server/http-errors.ts +21 -3
- package/src/ui/server/index.ts +814 -66
- 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/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 +13 -7
|
@@ -170,6 +170,27 @@ function validateFlagScopes(parsed: ParsedCliArgs): void {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
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
|
+
|
|
173
194
|
// Dash-flags each mutation subcommand accepts (global flags — -w/--workspace,
|
|
174
195
|
// -t/--tabs/--no-tabs, --skip-secrets — are consumed by parseCliArgs before
|
|
175
196
|
// dispatch, and -h/--help is handled by the dispatcher). Commands not listed
|
package/src/ui/cli/index.ts
CHANGED
|
@@ -215,10 +215,12 @@ import {
|
|
|
215
215
|
import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
|
|
216
216
|
import { cmdWeb, displayWebHelp } from "./web-command.js";
|
|
217
217
|
import { cmdGraph } from "./graph-command.js";
|
|
218
|
+
import { resolveTargetWorkspace } from "./target-workspace.js";
|
|
218
219
|
import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
|
|
219
220
|
import {
|
|
220
221
|
parseCliArgs,
|
|
221
222
|
takeWorkspaceFlags,
|
|
223
|
+
ownsSubcommandHelp,
|
|
222
224
|
unknownCommandFlags,
|
|
223
225
|
workspaceFlagRequiresActive,
|
|
224
226
|
type ParsedCliArgs,
|
|
@@ -311,19 +313,20 @@ const WORKSPACE_SUBCOMMANDS: {
|
|
|
311
313
|
|
|
312
314
|
const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
313
315
|
down: {
|
|
314
|
-
usage: "down [repo] [flags]",
|
|
316
|
+
usage: "down [repo] [-w <workspace>] [flags]",
|
|
315
317
|
lines: [
|
|
316
318
|
"Stop the cwd (or named) repo; --with-deps also stops what it sits on (its",
|
|
317
319
|
"derived closure, in reverse order), naming anything else that uses it.",
|
|
318
320
|
"-a/--all stops EVERY workspace system (reverse derived order, cwd-agnostic;",
|
|
319
321
|
"every down script runs — no already-stopped skip).",
|
|
320
|
-
"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)",
|
|
321
323
|
],
|
|
322
324
|
},
|
|
323
325
|
restart: {
|
|
324
|
-
usage: "restart [service...] [-A <app>] [-n|--dry-run]",
|
|
326
|
+
usage: "restart [service...] [-w <workspace>] [-A <app>] [-n|--dry-run]",
|
|
325
327
|
lines: [
|
|
326
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.",
|
|
327
330
|
],
|
|
328
331
|
},
|
|
329
332
|
status: {
|
|
@@ -368,7 +371,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
368
371
|
},
|
|
369
372
|
setup: {
|
|
370
373
|
usage:
|
|
371
|
-
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
374
|
+
"setup [service...] [-w <workspace>] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
372
375
|
lines: [
|
|
373
376
|
"Run 'make setup' once per system (clones missing managed repos first).",
|
|
374
377
|
"Default targets MANAGED clones only — setup's codegen/installs",
|
|
@@ -378,6 +381,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
378
381
|
"cloned, seeded or written (on a fresh machine: the blueprint's plan).",
|
|
379
382
|
"It plans the focused workspace; -w <workspace> (repeatable) plans those,",
|
|
380
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.",
|
|
381
385
|
],
|
|
382
386
|
},
|
|
383
387
|
org: {
|
|
@@ -794,10 +798,13 @@ class DenCLI {
|
|
|
794
798
|
|
|
795
799
|
// `<command> -h/--help` → that command's help (every command supports it).
|
|
796
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.
|
|
797
803
|
if (
|
|
798
804
|
command &&
|
|
799
805
|
!["-h", "--help", "-v", "--version"].includes(command) &&
|
|
800
|
-
(ownArgs.includes("-h") || ownArgs.includes("--help"))
|
|
806
|
+
(ownArgs.includes("-h") || ownArgs.includes("--help")) &&
|
|
807
|
+
!ownsSubcommandHelp(command, ownArgs[1])
|
|
801
808
|
) {
|
|
802
809
|
this.printCommandHelp(command, ownArgs[1]);
|
|
803
810
|
return;
|
|
@@ -2731,6 +2738,10 @@ class DenCLI {
|
|
|
2731
2738
|
"--manifest",
|
|
2732
2739
|
`use legacy ${product().manifestYml} depends_on instead of the derived graph`,
|
|
2733
2740
|
);
|
|
2741
|
+
row(
|
|
2742
|
+
"-w, --workspace <ws>",
|
|
2743
|
+
"act in that (active) workspace instead of the focused one; focus stays",
|
|
2744
|
+
);
|
|
2734
2745
|
row("--skip-secrets", "skip the secrets-backend sign-in preflight");
|
|
2735
2746
|
row("-h, --help", "show this help");
|
|
2736
2747
|
console.log();
|
|
@@ -2976,7 +2987,44 @@ class DenCLI {
|
|
|
2976
2987
|
}
|
|
2977
2988
|
}
|
|
2978
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
|
+
|
|
2979
3026
|
private async cmdServiceUp(args: string[]): Promise<void> {
|
|
3027
|
+
if (!this.pickTargetWorkspace("up", args)) return;
|
|
2980
3028
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
2981
3029
|
// never be read as up's own flags (`den up -- -a` is NOT `up -a`).
|
|
2982
3030
|
const terminator = args.indexOf("--");
|
|
@@ -3101,6 +3149,7 @@ class DenCLI {
|
|
|
3101
3149
|
}
|
|
3102
3150
|
|
|
3103
3151
|
private async cmdServiceDown(args: string[]): Promise<void> {
|
|
3152
|
+
if (!this.pickTargetWorkspace("down", args)) return;
|
|
3104
3153
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
3105
3154
|
// never be read as down's own flags (`den down -- -a` must NOT tear
|
|
3106
3155
|
// down the whole workspace).
|
|
@@ -3171,6 +3220,7 @@ class DenCLI {
|
|
|
3171
3220
|
}
|
|
3172
3221
|
|
|
3173
3222
|
private async cmdServiceRestart(argv: string[]): Promise<void> {
|
|
3223
|
+
if (!this.pickTargetWorkspace("restart", argv)) return;
|
|
3174
3224
|
const { app, rest: args } = takeAppFlag(argv);
|
|
3175
3225
|
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
3176
3226
|
if (!dryRun) preflightSecrets();
|
|
@@ -4068,15 +4118,33 @@ class DenCLI {
|
|
|
4068
4118
|
console.log();
|
|
4069
4119
|
return;
|
|
4070
4120
|
}
|
|
4071
|
-
if (planWs.length || allActive) {
|
|
4121
|
+
if (planWs.length > 1 || allActive) {
|
|
4072
4122
|
console.error(
|
|
4073
4123
|
chalk.red(
|
|
4074
|
-
`\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`,
|
|
4075
4125
|
),
|
|
4076
4126
|
);
|
|
4077
4127
|
process.exitCode = 1;
|
|
4078
4128
|
return;
|
|
4079
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;
|
|
4080
4148
|
|
|
4081
4149
|
// Clone any managed repos that aren't on disk yet before reading manifests.
|
|
4082
4150
|
this.cloneMissingManagedRepos();
|
|
@@ -4091,11 +4159,27 @@ class DenCLI {
|
|
|
4091
4159
|
return;
|
|
4092
4160
|
}
|
|
4093
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
|
+
}
|
|
4094
4180
|
let targets =
|
|
4095
4181
|
names.length > 0
|
|
4096
|
-
? services.filter(
|
|
4097
|
-
(s) => names.includes(s.address) || names.includes(s.repoName),
|
|
4098
|
-
)
|
|
4182
|
+
? services.filter((s) => names.some((n) => matches(n, s)))
|
|
4099
4183
|
: services;
|
|
4100
4184
|
|
|
4101
4185
|
// Workspace-wide default: MANAGED clones only. `make setup` runs
|
|
@@ -31,6 +31,10 @@ import {
|
|
|
31
31
|
noHost,
|
|
32
32
|
type HostClient,
|
|
33
33
|
} from "../../shared/sessions/client.js";
|
|
34
|
+
import {
|
|
35
|
+
ACTOR_TOKEN_ENV,
|
|
36
|
+
isPersonCredentialEnv,
|
|
37
|
+
} from "../../shared/actor-token.js";
|
|
34
38
|
import { DEN_PTY_ID } from "../../shared/sessions/den-pty.js";
|
|
35
39
|
import { AGENT_ACTOR_ENV, buildSessionEnv } from "../../shared/sessions/env.js";
|
|
36
40
|
import { runSessionHost } from "../../shared/sessions/host.js";
|
|
@@ -63,6 +67,7 @@ export const SESSIONS_HELP = {
|
|
|
63
67
|
" run argv in a PTY as <dist>-<ws>:<n> (cwd: here); capped by agents.max_concurrent",
|
|
64
68
|
" env: PATH, HOME, USER, LOGNAME, SHELL, TERM, COLORTERM, LANG, LC_*, TZ,",
|
|
65
69
|
" TMPDIR only; --keep-env NAME (exact, repeatable; audited) adds one",
|
|
70
|
+
" (never a person's credential: DEN_PERSON_TOKEN, a web.secret path)",
|
|
66
71
|
"{cli} sessions list [-w <ws>] [--json]",
|
|
67
72
|
"{cli} sessions attach <name|id>",
|
|
68
73
|
" raw terminal; ctrl-] detaches and leaves it running",
|
|
@@ -387,6 +392,33 @@ async function start(args: string[], d: Resolved): Promise<number> {
|
|
|
387
392
|
);
|
|
388
393
|
return 1;
|
|
389
394
|
}
|
|
395
|
+
// SEC-1 §5: a person's credential (DEN_PERSON_TOKEN, a person token, a
|
|
396
|
+
// path to web.secret) or an actor token is never forwarded. Refused, and
|
|
397
|
+
// audited by the session host (names only).
|
|
398
|
+
const forbidden = keep.filter(
|
|
399
|
+
(k) => k === ACTOR_TOKEN_ENV || isPersonCredentialEnv(k, d.env[k]),
|
|
400
|
+
);
|
|
401
|
+
if (forbidden.length) {
|
|
402
|
+
d.err(
|
|
403
|
+
`✗ --keep-env ${forbidden.join(", ")}: a person's credential (or an actor token) is never forwarded to a session`,
|
|
404
|
+
);
|
|
405
|
+
await withClient(d, (c) =>
|
|
406
|
+
c.start(
|
|
407
|
+
{
|
|
408
|
+
name: sessionName(d.dist, ws, name),
|
|
409
|
+
cwd,
|
|
410
|
+
argv: p.rest!,
|
|
411
|
+
env: {},
|
|
412
|
+
cols: 80,
|
|
413
|
+
rows: 24,
|
|
414
|
+
},
|
|
415
|
+
{ refusedEnv: forbidden, actor: `user ${cliUser(d.env)}` },
|
|
416
|
+
),
|
|
417
|
+
).catch((e: unknown) => {
|
|
418
|
+
if (!(e instanceof HostError)) throw e;
|
|
419
|
+
});
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
390
422
|
const env = plainSessionEnv(d.env, keep);
|
|
391
423
|
// Names only, never values: what was left out, so it can be asked for.
|
|
392
424
|
const dropped = Object.keys(d.env)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which workspace a lifecycle command (`setup`, `up`, `down`, `restart`)
|
|
3
|
+
* names its services in, when no `-w` says.
|
|
4
|
+
*
|
|
5
|
+
* The focused workspace is the default target (WS-6.1). A name it doesn't
|
|
6
|
+
* hold resolves across the other active workspaces, as the web API does
|
|
7
|
+
* with `&workspace=` (resolveService(address, workspace)):
|
|
8
|
+
*
|
|
9
|
+
* - in the focus: the focus (nothing changes);
|
|
10
|
+
* - in exactly one other active workspace: that one;
|
|
11
|
+
* - in several: an error that lists them (`-w <workspace>` picks one);
|
|
12
|
+
* - nowhere: an error (exit 1, never a silent exit 0).
|
|
13
|
+
*
|
|
14
|
+
* Every name must land in one workspace; names in different workspaces are
|
|
15
|
+
* an error (run them one workspace at a time, with `-w`).
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
resolveService,
|
|
19
|
+
serviceAliasCandidates,
|
|
20
|
+
} from "../../shared/environment-manager/service-runner.js";
|
|
21
|
+
import {
|
|
22
|
+
getActiveWorkspaces,
|
|
23
|
+
getFocusedWorkspaceName,
|
|
24
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
25
|
+
|
|
26
|
+
export type TargetWorkspace =
|
|
27
|
+
| { ok: true; workspace: string | null }
|
|
28
|
+
| { ok: false; error: string };
|
|
29
|
+
|
|
30
|
+
/** Whether `name` names a service (a repo, an address or an alias) in `ws`. */
|
|
31
|
+
export function nameResolvesIn(name: string, ws: string): boolean {
|
|
32
|
+
return (
|
|
33
|
+
resolveService(name, ws) !== null ||
|
|
34
|
+
serviceAliasCandidates(name, ws).length > 0
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The workspace `names` resolve in, given the focus and the active set:
|
|
40
|
+
* `workspace: null` when it is the focus (or there are no names). Pure
|
|
41
|
+
* apart from `resolves`.
|
|
42
|
+
*/
|
|
43
|
+
export function targetWorkspaceFor(
|
|
44
|
+
names: readonly string[],
|
|
45
|
+
o: {
|
|
46
|
+
cli: string;
|
|
47
|
+
command: string;
|
|
48
|
+
focus: string | null;
|
|
49
|
+
active: readonly string[];
|
|
50
|
+
resolves: (name: string, ws: string) => boolean;
|
|
51
|
+
},
|
|
52
|
+
): TargetWorkspace {
|
|
53
|
+
if (names.length === 0) return { ok: true, workspace: null };
|
|
54
|
+
const others = o.active.filter((w) => w !== o.focus);
|
|
55
|
+
const where = new Map<string, string>();
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
if (o.focus && o.resolves(name, o.focus)) {
|
|
58
|
+
where.set(name, o.focus);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const hits = others.filter((w) => o.resolves(name, w));
|
|
62
|
+
if (hits.length === 0) {
|
|
63
|
+
const looked = [o.focus, ...others].filter(Boolean).join(", ");
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
error: `"${name}" is not a service in any active workspace (${looked || "none active"}).`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (hits.length > 1)
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error:
|
|
73
|
+
`"${name}" is in more than one active workspace: ` +
|
|
74
|
+
`${hits.map((w) => `${w} (${o.cli} ${o.command} ${name} -w ${w})`).join(", ")}. ` +
|
|
75
|
+
"Name one with -w.",
|
|
76
|
+
};
|
|
77
|
+
where.set(name, hits[0]);
|
|
78
|
+
}
|
|
79
|
+
const workspaces = [...new Set(where.values())];
|
|
80
|
+
if (workspaces.length > 1)
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error:
|
|
84
|
+
"These names are in different workspaces: " +
|
|
85
|
+
`${[...where].map(([n, w]) => `${n} (${w})`).join(", ")}. ` +
|
|
86
|
+
`Run ${o.cli} ${o.command} once per workspace, with -w.`,
|
|
87
|
+
};
|
|
88
|
+
const ws = workspaces[0];
|
|
89
|
+
return { ok: true, workspace: ws === o.focus ? null : ws };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** targetWorkspaceFor over this distribution's workspaces. */
|
|
93
|
+
export function resolveTargetWorkspace(
|
|
94
|
+
names: readonly string[],
|
|
95
|
+
cli: string,
|
|
96
|
+
command: string,
|
|
97
|
+
): TargetWorkspace {
|
|
98
|
+
return targetWorkspaceFor(names, {
|
|
99
|
+
cli,
|
|
100
|
+
command,
|
|
101
|
+
focus: getFocusedWorkspaceName(),
|
|
102
|
+
active: getActiveWorkspaces(),
|
|
103
|
+
resolves: nameResolvesIn,
|
|
104
|
+
});
|
|
105
|
+
}
|