@monster-orange/den 0.2.0 → 0.2.2
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 +156 -0
- package/README.md +1 -1
- package/bin/den +3 -2
- package/docs/distributions.md +101 -9
- package/package.json +1 -1
- package/service/install.sh +14 -2
- package/src/shared/den-config.ts +69 -0
- package/src/shared/environment-manager/service-runner.ts +218 -4
- package/src/shared/manifest/placement.ts +15 -3
- package/src/shared/manifest/validate-layers.ts +40 -1
- package/src/shared/stacks/manifest-commands.ts +11 -1
- package/src/shared/stacks/runtime.ts +20 -0
- package/src/ui/cli/flag-parser.ts +2 -1
- package/src/ui/cli/index.ts +238 -45
- package/src/ui/cli/mcp-connect.ts +350 -0
- package/src/ui/cli/named-target.ts +11 -0
- package/src/ui/cli/service-command.ts +131 -0
- package/src/ui/cli/stack-command.ts +9 -0
- package/src/ui/mcp/api.ts +2 -0
- package/src/ui/mcp/connect.ts +304 -0
- package/src/ui/mcp/guards.ts +6 -2
- package/src/ui/mcp/server.ts +84 -1
- package/src/ui/mcp/tools.ts +89 -10
- package/src/ui/server/audit.ts +69 -3
- package/src/ui/server/index.ts +83 -2
- package/src/ui/server/lifecycle-exec.ts +188 -0
- package/src/ui/server/public/graph.css +4 -0
- package/src/ui/server/public/panel.js +6 -3
- package/src/ui/server/readonly-routes.ts +11 -2
- package/src/ui/server/workspace-routes.ts +1 -1
package/src/ui/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Main entry point for the interactive CLI
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isNamedTarget } from "./named-target.js";
|
|
8
9
|
import chalk from "chalk";
|
|
9
10
|
import oraSpinner from "ora";
|
|
10
11
|
|
|
@@ -95,7 +96,12 @@ import {
|
|
|
95
96
|
getWaitDeps,
|
|
96
97
|
} from "../../shared/environment-manager/dag.js";
|
|
97
98
|
import { waitForReady } from "../../shared/environment-manager/readiness.js";
|
|
98
|
-
import {
|
|
99
|
+
import {
|
|
100
|
+
canonicalServiceAddress,
|
|
101
|
+
previewServiceCommand,
|
|
102
|
+
resolveService,
|
|
103
|
+
type ServiceCommandPreview,
|
|
104
|
+
} from "../../shared/environment-manager/service-runner.js";
|
|
99
105
|
import { getGitHubCoords } from "../../shared/smoke/git-remote.js";
|
|
100
106
|
import {
|
|
101
107
|
getReposRoot,
|
|
@@ -156,6 +162,7 @@ import {
|
|
|
156
162
|
import { cleanupStaleTabs } from "../../shared/cli-tabs.js";
|
|
157
163
|
import { cmdConfig } from "./config-command.js";
|
|
158
164
|
import { cmdEdge } from "./edge-command.js";
|
|
165
|
+
import { cmdService } from "./service-command.js";
|
|
159
166
|
import { cmdStack } from "./stack-command.js";
|
|
160
167
|
import { cmdWeb, displayWebHelp } from "./web-command.js";
|
|
161
168
|
import { cmdGraph } from "./graph-command.js";
|
|
@@ -190,14 +197,15 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
190
197
|
down: {
|
|
191
198
|
usage: "down [repo] [flags]",
|
|
192
199
|
lines: [
|
|
193
|
-
"Stop the cwd (or named) repo
|
|
200
|
+
"Stop the cwd (or named) repo; --with-deps also stops what it sits on (its",
|
|
201
|
+
"derived closure, in reverse order), naming anything else that uses it.",
|
|
194
202
|
"-a/--all stops EVERY workspace system (reverse derived order, cwd-agnostic;",
|
|
195
203
|
"every down script runs — no already-stopped skip).",
|
|
196
|
-
"Flags: -a/--all · -A/--app <app> · -n/--dry-run (print order, stop nothing) · --manifest (legacy depends_on)",
|
|
204
|
+
"Flags: -a/--all · -A/--app <app> · --with-deps · -n/--dry-run (print order, stop nothing) · --manifest (legacy depends_on)",
|
|
197
205
|
],
|
|
198
206
|
},
|
|
199
207
|
restart: {
|
|
200
|
-
usage: "restart [service...] [-A <app>]",
|
|
208
|
+
usage: "restart [service...] [-A <app>] [-n|--dry-run]",
|
|
201
209
|
lines: [
|
|
202
210
|
`Restart the given services, or every repo in an application (\`${product().cli} apps\`).`,
|
|
203
211
|
],
|
|
@@ -300,10 +308,18 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
300
308
|
],
|
|
301
309
|
},
|
|
302
310
|
mcp: {
|
|
303
|
-
usage: "mcp [--port <web port>]",
|
|
311
|
+
usage: "mcp [--port <web port>] [--list | --call <tool> ['<json args>']]",
|
|
304
312
|
lines: [
|
|
305
313
|
"An MCP server over stdio for agents: a client of the running web server",
|
|
306
314
|
`(\`${product().cli} web up\`). Read tools always; actions only as named in mcp.actions.`,
|
|
315
|
+
"--list prints the tool names; --call runs one tool and prints its result.",
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
service: {
|
|
319
|
+
usage: "service [install|status|print|uninstall]",
|
|
320
|
+
lines: [
|
|
321
|
+
"The web UI as a macOS launchd service, started at login. Label and logs:",
|
|
322
|
+
"config web.service { label, logs }.",
|
|
307
323
|
],
|
|
308
324
|
},
|
|
309
325
|
"local-dev": {
|
|
@@ -362,6 +378,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
362
378
|
"apps",
|
|
363
379
|
"applications",
|
|
364
380
|
"mcp",
|
|
381
|
+
"service",
|
|
365
382
|
]);
|
|
366
383
|
|
|
367
384
|
/** Map a raw command token to a PII-safe, registered telemetry command name. */
|
|
@@ -379,6 +396,20 @@ function telemetryReason(error: unknown): string {
|
|
|
379
396
|
return name && name !== "Object" ? name : "Error";
|
|
380
397
|
}
|
|
381
398
|
|
|
399
|
+
/**
|
|
400
|
+
* What was typed, with each service alias (a manifest service key or a
|
|
401
|
+
* compose stack name) mapped to its service's address, so the derived graph,
|
|
402
|
+
* ownership and the daemon registry all see one name. Flags pass through; an
|
|
403
|
+
* ambiguous alias throws, naming the candidates. `limit` maps only the first
|
|
404
|
+
* n positional args.
|
|
405
|
+
*/
|
|
406
|
+
function canonicalTargets(args: string[], limit = Infinity): string[] {
|
|
407
|
+
let seen = 0;
|
|
408
|
+
return args.map((a) =>
|
|
409
|
+
a.startsWith("-") || seen++ >= limit ? a : canonicalServiceAddress(a),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
382
413
|
/** Print one manifest's validation: a status line, then each issue. */
|
|
383
414
|
function printValidation(
|
|
384
415
|
label: string,
|
|
@@ -684,6 +715,10 @@ class DenCLI {
|
|
|
684
715
|
await cmdEdge(args.slice(1));
|
|
685
716
|
break;
|
|
686
717
|
|
|
718
|
+
case "service":
|
|
719
|
+
process.exitCode = await cmdService(args.slice(1));
|
|
720
|
+
break;
|
|
721
|
+
|
|
687
722
|
case "stack":
|
|
688
723
|
await cmdStack(args.slice(1));
|
|
689
724
|
break;
|
|
@@ -695,6 +730,17 @@ class DenCLI {
|
|
|
695
730
|
break;
|
|
696
731
|
|
|
697
732
|
case "mcp": {
|
|
733
|
+
// Setup subcommands: register the server with agents.
|
|
734
|
+
const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
|
|
735
|
+
await import("./mcp-connect.js");
|
|
736
|
+
if (
|
|
737
|
+
(MCP_CONNECT_SUBCOMMANDS as readonly string[]).includes(
|
|
738
|
+
args[1] ?? "",
|
|
739
|
+
)
|
|
740
|
+
) {
|
|
741
|
+
process.exitCode = await runMcpConnect(args.slice(1));
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
698
744
|
// bin/den routes `mcp` to its own entry (stdout is the protocol);
|
|
699
745
|
// this covers the CLI run directly from a checkout.
|
|
700
746
|
await import("./mcp-stdout-guard.js");
|
|
@@ -1298,7 +1344,20 @@ class DenCLI {
|
|
|
1298
1344
|
/** Skip ANY already-healthy node (derived `up` starts only what's down). */
|
|
1299
1345
|
skipHealthyAll?: boolean;
|
|
1300
1346
|
},
|
|
1301
|
-
opts?: {
|
|
1347
|
+
opts?: {
|
|
1348
|
+
force?: boolean;
|
|
1349
|
+
/**
|
|
1350
|
+
* What the user named on the command line (repo or address). A remote
|
|
1351
|
+
* service among them is refused with a non-zero exit; one reached only
|
|
1352
|
+
* as part of a workspace or app run is said and skipped.
|
|
1353
|
+
*/
|
|
1354
|
+
named?: readonly string[];
|
|
1355
|
+
/**
|
|
1356
|
+
* `-n`/`--dry-run` (and `--plan` on this path): print what would run,
|
|
1357
|
+
* in order, and run nothing. Read-only on every path.
|
|
1358
|
+
*/
|
|
1359
|
+
dryRun?: boolean;
|
|
1360
|
+
},
|
|
1302
1361
|
): Promise<void> {
|
|
1303
1362
|
// How long to show startup output before handing control back to the menu
|
|
1304
1363
|
// for detachable (long-running) commands like `up`.
|
|
@@ -1308,6 +1367,22 @@ class DenCLI {
|
|
|
1308
1367
|
getFocusedWorkspaceName() ??
|
|
1309
1368
|
undefined;
|
|
1310
1369
|
const force = opts?.force ?? false;
|
|
1370
|
+
// R13: why a remote service is not run here, or null.
|
|
1371
|
+
const remoteRefusal = (address: string): string | null => {
|
|
1372
|
+
const placed = resolveService(address, workspace);
|
|
1373
|
+
return placed
|
|
1374
|
+
? placementRefusal(
|
|
1375
|
+
address,
|
|
1376
|
+
servicePlacement(
|
|
1377
|
+
readManifest(placed.repoPath),
|
|
1378
|
+
placed.subsystem,
|
|
1379
|
+
"local",
|
|
1380
|
+
),
|
|
1381
|
+
(e) => isProductionEnv(e),
|
|
1382
|
+
command,
|
|
1383
|
+
)
|
|
1384
|
+
: null;
|
|
1385
|
+
};
|
|
1311
1386
|
|
|
1312
1387
|
// Resolve dependency order. Derived mode supplies the order directly;
|
|
1313
1388
|
// otherwise topo-sort manifest depends_on (`down` → reverse).
|
|
@@ -1321,8 +1396,17 @@ class DenCLI {
|
|
|
1321
1396
|
const startupOrder = ws
|
|
1322
1397
|
? resolveStartupOrder(addresses, catalog, ws.config, ws.name)
|
|
1323
1398
|
: addresses;
|
|
1399
|
+
// Only up pulls in dependencies (a service needs them to start).
|
|
1400
|
+
// Down and restart act on what was named: its dependencies may be
|
|
1401
|
+
// serving other running services.
|
|
1402
|
+
const namedOnly = (): string[] => {
|
|
1403
|
+
const kept = startupOrder.filter((a) => addresses.includes(a));
|
|
1404
|
+
return [...kept, ...addresses.filter((a) => !kept.includes(a))];
|
|
1405
|
+
};
|
|
1324
1406
|
if (command === "down") {
|
|
1325
|
-
ordered =
|
|
1407
|
+
ordered = namedOnly().reverse();
|
|
1408
|
+
} else if (command !== "up") {
|
|
1409
|
+
ordered = namedOnly();
|
|
1326
1410
|
} else {
|
|
1327
1411
|
ordered = startupOrder;
|
|
1328
1412
|
if (ordered.length > addresses.length) {
|
|
@@ -1338,6 +1422,42 @@ class DenCLI {
|
|
|
1338
1422
|
}
|
|
1339
1423
|
}
|
|
1340
1424
|
|
|
1425
|
+
if (opts?.dryRun) {
|
|
1426
|
+
// The same lookup a real run makes, so the dry run says what it does.
|
|
1427
|
+
const previews = ordered.map((a): ServiceCommandPreview => {
|
|
1428
|
+
const remote = remoteRefusal(a);
|
|
1429
|
+
if (!remote) return previewServiceCommand(a, command, workspace);
|
|
1430
|
+
// As the loop below: a named remote target fails, any other is
|
|
1431
|
+
// said and skipped.
|
|
1432
|
+
return isNamedTarget(a, opts?.named ?? [])
|
|
1433
|
+
? { kind: "error", address: a, note: remote }
|
|
1434
|
+
: {
|
|
1435
|
+
kind: "nothing",
|
|
1436
|
+
address: a,
|
|
1437
|
+
note: `${a} (remote, skipped): ${remote}`,
|
|
1438
|
+
};
|
|
1439
|
+
});
|
|
1440
|
+
const doing =
|
|
1441
|
+
{ up: "starting", down: "stopping", restart: "restarting" }[command] ??
|
|
1442
|
+
`running "${command}" on`;
|
|
1443
|
+
const runs = previews
|
|
1444
|
+
.filter((p) => p.kind === "run" || p.kind === "via-repo")
|
|
1445
|
+
.map((p) => p.address);
|
|
1446
|
+
console.log(
|
|
1447
|
+
chalk.cyan(
|
|
1448
|
+
`\n Would ${command}: ${[...new Set(runs)].join(" → ") || "(nothing)"}`,
|
|
1449
|
+
),
|
|
1450
|
+
);
|
|
1451
|
+
for (const p of previews) {
|
|
1452
|
+
if (p.kind === "via-repo" || p.kind === "nothing")
|
|
1453
|
+
console.log(chalk.gray(` ${p.note}`));
|
|
1454
|
+
if (p.kind === "error") console.log(chalk.red(` ✗ ${p.note}`));
|
|
1455
|
+
}
|
|
1456
|
+
if (previews.some((p) => p.kind === "error")) process.exitCode = 1;
|
|
1457
|
+
console.log(chalk.gray(` (--dry-run: not ${doing} anything)\n`));
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1341
1461
|
// Build a catalog + workspace reference once per run for readiness lookups.
|
|
1342
1462
|
const catalog = readCatalog();
|
|
1343
1463
|
const ws = getActiveWorkspace();
|
|
@@ -1378,21 +1498,14 @@ class DenCLI {
|
|
|
1378
1498
|
}
|
|
1379
1499
|
// R13: a remote service is never started or stopped from here. Said,
|
|
1380
1500
|
// not failed, so a workspace or app up/down carries on past it.
|
|
1381
|
-
const
|
|
1382
|
-
const remote = placed
|
|
1383
|
-
? placementRefusal(
|
|
1384
|
-
address,
|
|
1385
|
-
servicePlacement(
|
|
1386
|
-
readManifest(placed.repoPath),
|
|
1387
|
-
placed.subsystem,
|
|
1388
|
-
"local",
|
|
1389
|
-
),
|
|
1390
|
-
(e) => isProductionEnv(e),
|
|
1391
|
-
command === "down" ? "stop" : "start",
|
|
1392
|
-
)
|
|
1393
|
-
: null;
|
|
1501
|
+
const remote = remoteRefusal(address);
|
|
1394
1502
|
if (remote) {
|
|
1395
|
-
|
|
1503
|
+
if (isNamedTarget(address, opts?.named ?? [])) {
|
|
1504
|
+
console.log(chalk.red(` ✗ ${remote}`));
|
|
1505
|
+
process.exitCode = 1;
|
|
1506
|
+
} else {
|
|
1507
|
+
console.log(chalk.gray(` • ${remote}`));
|
|
1508
|
+
}
|
|
1396
1509
|
continue;
|
|
1397
1510
|
}
|
|
1398
1511
|
if (command === "up" && !seedAddresses.has(address)) {
|
|
@@ -1604,6 +1717,9 @@ class DenCLI {
|
|
|
1604
1717
|
}
|
|
1605
1718
|
});
|
|
1606
1719
|
|
|
1720
|
+
// Any target that failed makes the whole command fail (scripts and
|
|
1721
|
+
// agents read the exit code).
|
|
1722
|
+
if (!started) process.exitCode = 1;
|
|
1607
1723
|
if (plan?.failFast && !started) {
|
|
1608
1724
|
console.log(
|
|
1609
1725
|
chalk.red(
|
|
@@ -1732,6 +1848,7 @@ class DenCLI {
|
|
|
1732
1848
|
plan = false,
|
|
1733
1849
|
pull = false,
|
|
1734
1850
|
): Promise<void> {
|
|
1851
|
+
const named = [seed];
|
|
1735
1852
|
const full = deriveGraph({ scope: "all" });
|
|
1736
1853
|
// Same positional closure the web "up with deps" uses: everything the seed
|
|
1737
1854
|
// sits ON TOP OF, never anything above it. Keeps the two entry points from
|
|
@@ -1791,7 +1908,12 @@ class DenCLI {
|
|
|
1791
1908
|
return;
|
|
1792
1909
|
}
|
|
1793
1910
|
if (dryRun) {
|
|
1794
|
-
|
|
1911
|
+
await this.runServicesCommand(
|
|
1912
|
+
order,
|
|
1913
|
+
"up",
|
|
1914
|
+
{ order },
|
|
1915
|
+
{ named, dryRun: true },
|
|
1916
|
+
);
|
|
1795
1917
|
return;
|
|
1796
1918
|
}
|
|
1797
1919
|
// Check the secrets backend first so any first-clone `make setup` (and the up
|
|
@@ -1813,12 +1935,17 @@ class DenCLI {
|
|
|
1813
1935
|
);
|
|
1814
1936
|
return;
|
|
1815
1937
|
}
|
|
1816
|
-
await this.runServicesCommand(
|
|
1938
|
+
await this.runServicesCommand(
|
|
1817
1939
|
order,
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1940
|
+
"up",
|
|
1941
|
+
{
|
|
1942
|
+
order,
|
|
1943
|
+
waitDepsFor: (address) => hardDepsOf(graph, address),
|
|
1944
|
+
failFast: true,
|
|
1945
|
+
skipHealthyAll: true,
|
|
1946
|
+
},
|
|
1947
|
+
{ named },
|
|
1948
|
+
);
|
|
1822
1949
|
}
|
|
1823
1950
|
|
|
1824
1951
|
/**
|
|
@@ -1831,10 +1958,13 @@ class DenCLI {
|
|
|
1831
1958
|
seed: string,
|
|
1832
1959
|
dryRun: boolean,
|
|
1833
1960
|
force = false,
|
|
1961
|
+
withDeps = false,
|
|
1834
1962
|
): Promise<void> {
|
|
1835
1963
|
const full = deriveGraph({ scope: "all" });
|
|
1964
|
+
// Only the named repo, unless --with-deps: what it sits on may be
|
|
1965
|
+
// serving other running systems (the same rule as a named service).
|
|
1836
1966
|
const upOrder = this.runtimeOnly(
|
|
1837
|
-
deriveStartTargets(full, seed).order,
|
|
1967
|
+
withDeps ? deriveStartTargets(full, seed).order : [seed],
|
|
1838
1968
|
"stopped",
|
|
1839
1969
|
);
|
|
1840
1970
|
if (upOrder.length === 0) return;
|
|
@@ -1845,6 +1975,9 @@ class DenCLI {
|
|
|
1845
1975
|
// still running on them. Name them rather than letting them fall over
|
|
1846
1976
|
// silently.
|
|
1847
1977
|
const inClosure = new Set(order);
|
|
1978
|
+
// Only what really stops here puts dependents at risk: a remote service
|
|
1979
|
+
// is refused (R13), so it stops nothing.
|
|
1980
|
+
const stopsHere = new Set(order.filter((a) => !this.isRemote(a, "down")));
|
|
1848
1981
|
const atRisk = [
|
|
1849
1982
|
...new Set(
|
|
1850
1983
|
full.edges
|
|
@@ -1852,14 +1985,16 @@ class DenCLI {
|
|
|
1852
1985
|
(e) =>
|
|
1853
1986
|
e.kind !== "jwks" &&
|
|
1854
1987
|
e.ordering !== false &&
|
|
1855
|
-
|
|
1988
|
+
stopsHere.has(e.to) &&
|
|
1856
1989
|
!inClosure.has(e.from),
|
|
1857
1990
|
)
|
|
1858
1991
|
.map((e) => e.from),
|
|
1859
1992
|
),
|
|
1860
1993
|
].sort();
|
|
1861
1994
|
console.log(
|
|
1862
|
-
chalk.cyan(
|
|
1995
|
+
chalk.cyan(
|
|
1996
|
+
`\n Derived down for ${chalk.bold(seed)}${withDeps ? " (+ dependencies)" : ""}`,
|
|
1997
|
+
),
|
|
1863
1998
|
);
|
|
1864
1999
|
console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
|
|
1865
2000
|
if (atRisk.length > 0) {
|
|
@@ -1871,10 +2006,38 @@ class DenCLI {
|
|
|
1871
2006
|
);
|
|
1872
2007
|
}
|
|
1873
2008
|
if (dryRun) {
|
|
1874
|
-
|
|
2009
|
+
await this.runServicesCommand(
|
|
2010
|
+
order,
|
|
2011
|
+
"down",
|
|
2012
|
+
{ order },
|
|
2013
|
+
{ force, named: [seed], dryRun: true },
|
|
2014
|
+
);
|
|
1875
2015
|
return;
|
|
1876
2016
|
}
|
|
1877
|
-
await this.runServicesCommand(
|
|
2017
|
+
await this.runServicesCommand(
|
|
2018
|
+
order,
|
|
2019
|
+
"down",
|
|
2020
|
+
{ order },
|
|
2021
|
+
{ force, named: [seed] },
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** Whether `address` runs remotely, so `command` is refused here (R13). */
|
|
2026
|
+
private isRemote(address: string, command: string): boolean {
|
|
2027
|
+
const placed = resolveService(address);
|
|
2028
|
+
return (
|
|
2029
|
+
!!placed &&
|
|
2030
|
+
placementRefusal(
|
|
2031
|
+
address,
|
|
2032
|
+
servicePlacement(
|
|
2033
|
+
readManifest(placed.repoPath),
|
|
2034
|
+
placed.subsystem,
|
|
2035
|
+
"local",
|
|
2036
|
+
),
|
|
2037
|
+
(e) => isProductionEnv(e),
|
|
2038
|
+
command,
|
|
2039
|
+
) !== null
|
|
2040
|
+
);
|
|
1878
2041
|
}
|
|
1879
2042
|
|
|
1880
2043
|
/** Workspace repos with no runtime (modules, libraries) → their kind. */
|
|
@@ -2301,9 +2464,10 @@ class DenCLI {
|
|
|
2301
2464
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
2302
2465
|
// never be read as up's own flags (`den up -- -a` is NOT `up -a`).
|
|
2303
2466
|
const terminator = args.indexOf("--");
|
|
2304
|
-
const { app, rest
|
|
2467
|
+
const { app, rest } = takeAppFlag(
|
|
2305
2468
|
terminator === -1 ? args : args.slice(0, terminator),
|
|
2306
2469
|
);
|
|
2470
|
+
const ownArgs = canonicalTargets(rest);
|
|
2307
2471
|
const dryRun = ownArgs.includes("--dry-run") || ownArgs.includes("-n");
|
|
2308
2472
|
const plan = ownArgs.includes("--plan");
|
|
2309
2473
|
const pull = ownArgs.includes("--pull");
|
|
@@ -2360,11 +2524,17 @@ class DenCLI {
|
|
|
2360
2524
|
await this.runDerivedUp(seed, include, dryRun, plan, pull);
|
|
2361
2525
|
return;
|
|
2362
2526
|
}
|
|
2363
|
-
await this.cmdServiceUpManifest(
|
|
2527
|
+
await this.cmdServiceUpManifest(
|
|
2528
|
+
ownArgs.filter((a) => !a.startsWith("-")),
|
|
2529
|
+
dryRun || plan,
|
|
2530
|
+
);
|
|
2364
2531
|
}
|
|
2365
2532
|
|
|
2366
2533
|
/** Legacy: start the given services via manifest depends_on. */
|
|
2367
|
-
private async cmdServiceUpManifest(
|
|
2534
|
+
private async cmdServiceUpManifest(
|
|
2535
|
+
addresses: string[],
|
|
2536
|
+
dryRun = false,
|
|
2537
|
+
): Promise<void> {
|
|
2368
2538
|
// Resolve targets BEFORE the secrets preflight — otherwise running
|
|
2369
2539
|
// `den up` from a non-service dir (e.g. the den repo root) triggers
|
|
2370
2540
|
// an interactive sign-in just to print "nothing to do".
|
|
@@ -2391,8 +2561,11 @@ class DenCLI {
|
|
|
2391
2561
|
}
|
|
2392
2562
|
return;
|
|
2393
2563
|
}
|
|
2394
|
-
preflightSecrets();
|
|
2395
|
-
await this.runServicesCommand(targets, "up"
|
|
2564
|
+
if (!dryRun) preflightSecrets();
|
|
2565
|
+
await this.runServicesCommand(targets, "up", undefined, {
|
|
2566
|
+
named: addresses,
|
|
2567
|
+
dryRun,
|
|
2568
|
+
});
|
|
2396
2569
|
}
|
|
2397
2570
|
|
|
2398
2571
|
private async cmdServiceDown(args: string[]): Promise<void> {
|
|
@@ -2400,9 +2573,10 @@ class DenCLI {
|
|
|
2400
2573
|
// never be read as down's own flags (`den down -- -a` must NOT tear
|
|
2401
2574
|
// down the whole workspace).
|
|
2402
2575
|
const terminator = args.indexOf("--");
|
|
2403
|
-
const { app, rest
|
|
2576
|
+
const { app, rest } = takeAppFlag(
|
|
2404
2577
|
terminator === -1 ? args : args.slice(0, terminator),
|
|
2405
2578
|
);
|
|
2579
|
+
const ownArgs = canonicalTargets(rest);
|
|
2406
2580
|
const dryRun = ownArgs.includes("--dry-run") || ownArgs.includes("-n");
|
|
2407
2581
|
const force = ownArgs.includes("--force") || ownArgs.includes("-f");
|
|
2408
2582
|
if (app) {
|
|
@@ -2444,7 +2618,12 @@ class DenCLI {
|
|
|
2444
2618
|
const seed = forceManifest ? undefined : this.derivedSeed(seedArg);
|
|
2445
2619
|
|
|
2446
2620
|
if (seed) {
|
|
2447
|
-
await this.runDerivedDown(
|
|
2621
|
+
await this.runDerivedDown(
|
|
2622
|
+
seed,
|
|
2623
|
+
dryRun,
|
|
2624
|
+
force,
|
|
2625
|
+
ownArgs.includes("--with-deps"),
|
|
2626
|
+
);
|
|
2448
2627
|
return;
|
|
2449
2628
|
}
|
|
2450
2629
|
const addresses = ownArgs.filter((a) => !a.startsWith("-"));
|
|
@@ -2452,26 +2631,34 @@ class DenCLI {
|
|
|
2452
2631
|
addresses.length > 0
|
|
2453
2632
|
? addresses
|
|
2454
2633
|
: this.resolveNoArgServiceTargets("down");
|
|
2455
|
-
await this.runServicesCommand(targets, "down", undefined, {
|
|
2634
|
+
await this.runServicesCommand(targets, "down", undefined, {
|
|
2635
|
+
force,
|
|
2636
|
+
named: addresses,
|
|
2637
|
+
dryRun,
|
|
2638
|
+
});
|
|
2456
2639
|
}
|
|
2457
2640
|
|
|
2458
2641
|
private async cmdServiceRestart(argv: string[]): Promise<void> {
|
|
2459
2642
|
const { app, rest: args } = takeAppFlag(argv);
|
|
2460
|
-
|
|
2643
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
2644
|
+
if (!dryRun) preflightSecrets();
|
|
2461
2645
|
if (app) {
|
|
2462
2646
|
const order =
|
|
2463
2647
|
this.allWorkspaceOrder(focusedAppRepos(app))?.rawOrder ?? [];
|
|
2464
|
-
await this.runServicesCommand(order, "restart");
|
|
2648
|
+
await this.runServicesCommand(order, "restart", undefined, { dryRun });
|
|
2465
2649
|
return;
|
|
2466
2650
|
}
|
|
2467
2651
|
// Drop flag tokens so a bare `den restart --foo` doesn't treat the flag
|
|
2468
2652
|
// as a service address (matches cmdServiceUp/cmdServiceDown).
|
|
2469
|
-
const filtered = args.filter((a) => !a.startsWith("-"));
|
|
2653
|
+
const filtered = canonicalTargets(args.filter((a) => !a.startsWith("-")));
|
|
2470
2654
|
const addresses =
|
|
2471
2655
|
filtered.length > 0
|
|
2472
2656
|
? filtered
|
|
2473
2657
|
: this.resolveNoArgServiceTargets("restart");
|
|
2474
|
-
await this.runServicesCommand(addresses, "restart"
|
|
2658
|
+
await this.runServicesCommand(addresses, "restart", undefined, {
|
|
2659
|
+
named: filtered,
|
|
2660
|
+
dryRun,
|
|
2661
|
+
});
|
|
2475
2662
|
}
|
|
2476
2663
|
|
|
2477
2664
|
/** Service address with a ⏸ marker when the repo is auto-start off. */
|
|
@@ -2579,7 +2766,7 @@ class DenCLI {
|
|
|
2579
2766
|
private async cmdLogs(args: string[]): Promise<void> {
|
|
2580
2767
|
await this.ensureWorkspaceSeeded();
|
|
2581
2768
|
const alias = loadAliasResolver();
|
|
2582
|
-
const target = resolveLogsTarget(args, alias);
|
|
2769
|
+
const target = resolveLogsTarget(canonicalTargets(args, 1), alias);
|
|
2583
2770
|
if (target.kind === "unknown-repo") {
|
|
2584
2771
|
console.log(chalk.red(`\n Unknown repo: ${target.arg}\n`));
|
|
2585
2772
|
process.exit(1);
|
|
@@ -3645,6 +3832,12 @@ class DenCLI {
|
|
|
3645
3832
|
console.log(
|
|
3646
3833
|
" mcp [--port <n>] MCP server (stdio) for agents, over the running web server",
|
|
3647
3834
|
);
|
|
3835
|
+
console.log(
|
|
3836
|
+
" mcp --call <tool> Call one MCP tool and print its result (--list: tool names)",
|
|
3837
|
+
);
|
|
3838
|
+
console.log(
|
|
3839
|
+
" service install Run the web UI as a launchd service (status, print, uninstall)",
|
|
3840
|
+
);
|
|
3648
3841
|
console.log();
|
|
3649
3842
|
console.log(" workspace list List workspaces");
|
|
3650
3843
|
console.log(
|