@monster-orange/den 0.2.8 → 0.4.0
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 +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +12 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -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 +173 -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 +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +257 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -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/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +539 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +37 -3
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +48 -18
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
package/src/ui/cli/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runningWorkspaceStacks,
|
|
12
12
|
} from "../../shared/stacks/status.js";
|
|
13
13
|
import { isNamedTarget } from "./named-target.js";
|
|
14
|
+
import { isNotice, printNotice } from "./notice.js";
|
|
14
15
|
import chalk from "chalk";
|
|
15
16
|
import { noun, plural } from "../../shared/plural.js";
|
|
16
17
|
// Spinners: progress on stderr (a terminal) or stdout (piped); plain lines
|
|
@@ -58,6 +59,13 @@ import {
|
|
|
58
59
|
DEN_SERVICES_DIR,
|
|
59
60
|
DEN_WORKSPACES_DIR,
|
|
60
61
|
} from "../../shared/environment-manager/workspaces.js";
|
|
62
|
+
import {
|
|
63
|
+
ownersSuffix,
|
|
64
|
+
parseScope,
|
|
65
|
+
scopeLine,
|
|
66
|
+
scopeView,
|
|
67
|
+
takeScopeFlag,
|
|
68
|
+
} from "../../shared/environment-manager/scope.js";
|
|
61
69
|
import {
|
|
62
70
|
deleteWorkspaceDirectory,
|
|
63
71
|
checkBehindCount,
|
|
@@ -70,9 +78,11 @@ import {
|
|
|
70
78
|
markSetupSucceeded,
|
|
71
79
|
fetchAndFastForward,
|
|
72
80
|
} from "../../shared/environment-manager/workspace-manager.js";
|
|
81
|
+
import { stackNamespaceRows } from "../../shared/stacks/namespace.js";
|
|
73
82
|
import {
|
|
74
83
|
runServiceCommand,
|
|
75
84
|
claimHealthyService,
|
|
85
|
+
daemonsOutsideWorkspaces,
|
|
76
86
|
getWorkspaceServices,
|
|
77
87
|
ensureDaemonRegistryLoaded,
|
|
78
88
|
hasRunningOpctlContainers,
|
|
@@ -104,14 +114,18 @@ import { waitForReady } from "../../shared/environment-manager/readiness.js";
|
|
|
104
114
|
import {
|
|
105
115
|
canonicalServiceAddress,
|
|
106
116
|
previewServiceCommand,
|
|
117
|
+
previewUpSettle,
|
|
107
118
|
previewServiceSecretEnv,
|
|
108
119
|
prepareServiceSecretEnv,
|
|
109
120
|
resolveService,
|
|
121
|
+
type DownOutcome,
|
|
110
122
|
type ServiceCommandPreview,
|
|
111
123
|
} from "../../shared/environment-manager/service-runner.js";
|
|
112
124
|
import { getGitHubCoords } from "../../shared/smoke/git-remote.js";
|
|
125
|
+
import { isConfigError } from "../../shared/config-error.js";
|
|
113
126
|
import {
|
|
114
127
|
assertDenConfigValid,
|
|
128
|
+
waiveInvalidOrgs,
|
|
115
129
|
getReposRoot,
|
|
116
130
|
mergeUserConfig,
|
|
117
131
|
readDenConfig,
|
|
@@ -126,6 +140,11 @@ import {
|
|
|
126
140
|
deriveStartTargets,
|
|
127
141
|
} from "../../shared/environment-manager/derivation/derived-up.js";
|
|
128
142
|
import type { DerivedGraph } from "../../shared/environment-manager/derivation/types.js";
|
|
143
|
+
import {
|
|
144
|
+
repoOfAddress,
|
|
145
|
+
resolveUpTargets,
|
|
146
|
+
type UpTargets,
|
|
147
|
+
} from "../../shared/environment-manager/up-targets.js";
|
|
129
148
|
import { loadAliasResolver } from "../../shared/environment-manager/derivation/load-alias.js";
|
|
130
149
|
import {
|
|
131
150
|
cmdApps,
|
|
@@ -173,8 +192,13 @@ import {
|
|
|
173
192
|
import { cleanupStaleTabs } from "../../shared/cli-tabs.js";
|
|
174
193
|
import { cmdConfig } from "./config-command.js";
|
|
175
194
|
import { cmdEdge } from "./edge-command.js";
|
|
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";
|
|
176
198
|
import { cmdService } from "./service-command.js";
|
|
177
199
|
import { cmdStack } from "./stack-command.js";
|
|
200
|
+
import { cmdTheme } from "./theme-command.js";
|
|
201
|
+
import { cmdPlugin } from "./plugin-command.js";
|
|
178
202
|
import {
|
|
179
203
|
cmdOrg,
|
|
180
204
|
orgFlag,
|
|
@@ -209,6 +233,9 @@ import {
|
|
|
209
233
|
formatHealthDot,
|
|
210
234
|
formatHealthLabel,
|
|
211
235
|
formatHealthReason,
|
|
236
|
+
formatRollupLabel,
|
|
237
|
+
healthReasonText,
|
|
238
|
+
rollupReasonText,
|
|
212
239
|
} from "./health-display.js";
|
|
213
240
|
import { DEN_BANNER_LINES, DEN_DIVIDER } from "../../shared/banner.js";
|
|
214
241
|
|
|
@@ -300,9 +327,12 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
300
327
|
],
|
|
301
328
|
},
|
|
302
329
|
status: {
|
|
303
|
-
usage: "status [-A <app>]",
|
|
330
|
+
usage: "status [-A <app>] [--scope <all|workspace>] [--verbose]",
|
|
304
331
|
lines: [
|
|
305
|
-
"Show service health for
|
|
332
|
+
"Show service health for what is in view: every active workspace by",
|
|
333
|
+
"default (the web UI's scope), or --scope <workspace> for one. A",
|
|
334
|
+
"service several workspaces share is listed once, with its owners.",
|
|
335
|
+
"--verbose (or --all) shows one row per service instead of per repo.",
|
|
306
336
|
],
|
|
307
337
|
},
|
|
308
338
|
logs: {
|
|
@@ -319,7 +349,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
319
349
|
lines: ["List all registered services in the catalog."],
|
|
320
350
|
},
|
|
321
351
|
register: {
|
|
322
|
-
usage: "register [path] [
|
|
352
|
+
usage: "register [path] [-w <workspace>] [--org <id>]",
|
|
323
353
|
lines: [
|
|
324
354
|
"With a path: add that checkout (dev-managed) under its manifest name.",
|
|
325
355
|
"Without one: pick the repo interactively. A managed clone is picked",
|
|
@@ -327,6 +357,15 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
327
357
|
"-w writes that workspace (active or not) and leaves focus alone.",
|
|
328
358
|
],
|
|
329
359
|
},
|
|
360
|
+
relocate: {
|
|
361
|
+
usage: "relocate <repo> <path> [--dry-run] [--force] [-w <workspace>]",
|
|
362
|
+
lines: [
|
|
363
|
+
"Point every workspace's dev-managed entries for <repo> at <path>.",
|
|
364
|
+
"Refuses a path that is missing, holds a different repo, or has",
|
|
365
|
+
"neither a manifest nor a remote naming this one (unless --force).",
|
|
366
|
+
"-w limits it to that workspace; focus never changes.",
|
|
367
|
+
],
|
|
368
|
+
},
|
|
330
369
|
setup: {
|
|
331
370
|
usage:
|
|
332
371
|
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
@@ -417,7 +456,37 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
417
456
|
edge: {
|
|
418
457
|
usage: "edge [up|down|status|trust|untrust|routes]",
|
|
419
458
|
lines: [
|
|
420
|
-
"This org's
|
|
459
|
+
"This org's edge: routes at <name>.<edge.domain>, TLS as edge.tls says",
|
|
460
|
+
"(none | mkcert | files). trust/untrust exist only under mkcert.",
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
doctor: {
|
|
464
|
+
usage: "doctor [--probe]",
|
|
465
|
+
lines: [
|
|
466
|
+
"Each strategy kind (secrets, tls): the one selected, whether it was implicit,",
|
|
467
|
+
"and whether it can work here (a PATH check; nothing is run or prompted).",
|
|
468
|
+
"--probe: start a short probe session (through the session host) for a",
|
|
469
|
+
"plain session and each available runtime's agent env; key names only.",
|
|
470
|
+
"Exit 1 when a runtime's scrubEnv name or a credential-shaped variable",
|
|
471
|
+
"reaches one. Without the PTY module it says unavailable, not failed.",
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
plugin: {
|
|
475
|
+
usage:
|
|
476
|
+
"plugin [list] | validate [dir] | add <repo>[/<subpath>]@<ref> [--track] [--yes] | update <id> @<ref> | remove <id>",
|
|
477
|
+
lines: [
|
|
478
|
+
"Plugins (den.plugin/v1) in plugins/<id>/: local (the distribution's own, no lock entry)",
|
|
479
|
+
"or added (vendored at an exact ref, pinned in plugins.lock.json). list checks each",
|
|
480
|
+
"against the lock; validate checks every manifest. add refuses file:// sources.",
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
theme: {
|
|
484
|
+
usage:
|
|
485
|
+
"theme [list] | validate [dir] | new <id> | add <repo>[/<subpath>]@<ref> | remove <id>",
|
|
486
|
+
lines: [
|
|
487
|
+
"Web UI themes (den.theme/v1). add fetches one at an exact ref, validates it",
|
|
488
|
+
"(schema, contrast in both modes, files) and vendors it into plugins/<id>/",
|
|
489
|
+
"with a plugins.lock.json entry; commit both. list checks them against the lock.",
|
|
421
490
|
],
|
|
422
491
|
},
|
|
423
492
|
mcp: {
|
|
@@ -438,13 +507,15 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
438
507
|
"local-dev": {
|
|
439
508
|
usage: "local-dev [on|off]",
|
|
440
509
|
lines: [
|
|
441
|
-
"Toggle the local-dev experience (web
|
|
510
|
+
"Toggle the local-dev experience (web Workspaces page + service mgmt).",
|
|
442
511
|
],
|
|
443
512
|
},
|
|
444
513
|
menu: {
|
|
445
514
|
usage: "menu",
|
|
446
515
|
lines: ["Open the interactive menu (pillar/module/env picker)."],
|
|
447
516
|
},
|
|
517
|
+
sessions: SESSIONS_HELP,
|
|
518
|
+
agents: AGENTS_HELP,
|
|
448
519
|
};
|
|
449
520
|
|
|
450
521
|
function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
@@ -466,6 +537,7 @@ function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
|
466
537
|
const TELEMETRY_COMMANDS = new Set<string>([
|
|
467
538
|
"run",
|
|
468
539
|
"view-results",
|
|
540
|
+
"down",
|
|
469
541
|
"menu",
|
|
470
542
|
"web",
|
|
471
543
|
"up",
|
|
@@ -476,6 +548,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
476
548
|
"services",
|
|
477
549
|
"graph",
|
|
478
550
|
"register",
|
|
551
|
+
"relocate",
|
|
479
552
|
"use-local",
|
|
480
553
|
`use-managed`,
|
|
481
554
|
"setup",
|
|
@@ -488,19 +561,26 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
488
561
|
"local-dev",
|
|
489
562
|
"config",
|
|
490
563
|
"edge",
|
|
564
|
+
"doctor",
|
|
491
565
|
"stack",
|
|
492
566
|
"apps",
|
|
493
567
|
"applications",
|
|
494
568
|
"mcp",
|
|
569
|
+
"sessions",
|
|
570
|
+
"agents",
|
|
495
571
|
"service",
|
|
572
|
+
"theme",
|
|
573
|
+
"plugin",
|
|
496
574
|
]);
|
|
497
575
|
|
|
498
576
|
/**
|
|
499
577
|
* Commands that run with a config that fails validation: the web and MCP
|
|
500
578
|
* servers (each route or tool reports it), and those that read neither orgs,
|
|
501
579
|
* the catalog nor clones (manifest validate, create, the per-engineer
|
|
502
|
-
* config and local-dev toggles, saved results)
|
|
503
|
-
*
|
|
580
|
+
* config and local-dev toggles, saved results), and `down` (0.3.0-dev.40:
|
|
581
|
+
* stopping is never stranded by a config that fails; it stops from the
|
|
582
|
+
* daemon records and compose projects, reading no org data). Every other
|
|
583
|
+
* registered command checks the config first.
|
|
504
584
|
*/
|
|
505
585
|
const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
506
586
|
"web",
|
|
@@ -510,6 +590,11 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
|
510
590
|
"config",
|
|
511
591
|
"local-dev",
|
|
512
592
|
"view-results",
|
|
593
|
+
"down",
|
|
594
|
+
// The session host and its clients read no org data, and stopping or
|
|
595
|
+
// listing a session is never stranded by a config that fails (AG-3a).
|
|
596
|
+
"sessions",
|
|
597
|
+
"agents",
|
|
513
598
|
]);
|
|
514
599
|
|
|
515
600
|
function configCheckedCommand(command: string): boolean {
|
|
@@ -743,6 +828,22 @@ class DenCLI {
|
|
|
743
828
|
// anything: some (sync over existing clones, register, workspace
|
|
744
829
|
// create) never read `orgs:` and used to carry on (ORG-1d).
|
|
745
830
|
if (command && configCheckedCommand(command)) assertDenConfigValid();
|
|
831
|
+
// `down` is never stranded by an invalid config (0.3.0-dev.40): it
|
|
832
|
+
// says so and stops from the daemon records and compose projects,
|
|
833
|
+
// reading the config without its org data.
|
|
834
|
+
if (command === "down") {
|
|
835
|
+
try {
|
|
836
|
+
assertDenConfigValid();
|
|
837
|
+
} catch (e) {
|
|
838
|
+
if (!isConfigError(e)) throw e;
|
|
839
|
+
console.log(
|
|
840
|
+
chalk.yellow(
|
|
841
|
+
`\n ⚠ ${e.message}\n Stopping anyway, without the config's org data. Fix it, then \`${product().cli} validate\`.`,
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
waiveInvalidOrgs();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
746
847
|
|
|
747
848
|
switch (command) {
|
|
748
849
|
case "run":
|
|
@@ -798,6 +899,10 @@ class DenCLI {
|
|
|
798
899
|
await this.cmdRegister(args.slice(1));
|
|
799
900
|
break;
|
|
800
901
|
|
|
902
|
+
case "relocate":
|
|
903
|
+
await this.cmdRelocate(args.slice(1));
|
|
904
|
+
break;
|
|
905
|
+
|
|
801
906
|
case "use-local":
|
|
802
907
|
await this.cmdUseMode(args.slice(1), "dev-managed");
|
|
803
908
|
break;
|
|
@@ -872,6 +977,10 @@ class DenCLI {
|
|
|
872
977
|
await cmdEdge(args.slice(1));
|
|
873
978
|
break;
|
|
874
979
|
|
|
980
|
+
case "doctor":
|
|
981
|
+
process.exitCode = await cmdDoctor(args.slice(1));
|
|
982
|
+
break;
|
|
983
|
+
|
|
875
984
|
case "service":
|
|
876
985
|
process.exitCode = await cmdService(args.slice(1));
|
|
877
986
|
break;
|
|
@@ -880,12 +989,28 @@ class DenCLI {
|
|
|
880
989
|
await cmdStack(args.slice(1));
|
|
881
990
|
break;
|
|
882
991
|
|
|
992
|
+
case "theme":
|
|
993
|
+
process.exitCode = await cmdTheme(args.slice(1));
|
|
994
|
+
break;
|
|
995
|
+
|
|
996
|
+
case "plugin":
|
|
997
|
+
process.exitCode = await cmdPlugin(args.slice(1));
|
|
998
|
+
break;
|
|
999
|
+
|
|
883
1000
|
case "apps":
|
|
884
1001
|
case "applications":
|
|
885
1002
|
await this.ensureWorkspaceSeeded();
|
|
886
1003
|
cmdApps(args.slice(1));
|
|
887
1004
|
break;
|
|
888
1005
|
|
|
1006
|
+
case "sessions":
|
|
1007
|
+
process.exitCode = await cmdSessions(args.slice(1));
|
|
1008
|
+
break;
|
|
1009
|
+
|
|
1010
|
+
case "agents":
|
|
1011
|
+
process.exitCode = await cmdAgents(args.slice(1));
|
|
1012
|
+
break;
|
|
1013
|
+
|
|
889
1014
|
case "mcp": {
|
|
890
1015
|
// Setup subcommands: register the server with agents.
|
|
891
1016
|
const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
|
|
@@ -1597,7 +1722,15 @@ class DenCLI {
|
|
|
1597
1722
|
// The same lookup a real run makes, so the dry run says what it does.
|
|
1598
1723
|
const previews = ordered.map((a): ServiceCommandPreview => {
|
|
1599
1724
|
const remote = remoteRefusal(a);
|
|
1600
|
-
if (!remote)
|
|
1725
|
+
if (!remote) {
|
|
1726
|
+
const p = previewServiceCommand(a, command, workspace);
|
|
1727
|
+
// What up does first about a pre-dev.37 record or a held port.
|
|
1728
|
+
return (
|
|
1729
|
+
(command === "up" && p.kind === "run"
|
|
1730
|
+
? previewUpSettle(a, workspace)
|
|
1731
|
+
: null) ?? p
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1601
1734
|
// As the loop below: a named remote target fails, any other is
|
|
1602
1735
|
// said and skipped.
|
|
1603
1736
|
return isNamedTarget(a, opts?.named ?? [])
|
|
@@ -1620,7 +1753,7 @@ class DenCLI {
|
|
|
1620
1753
|
),
|
|
1621
1754
|
);
|
|
1622
1755
|
for (const p of previews) {
|
|
1623
|
-
if (p.kind
|
|
1756
|
+
if (p.kind !== "error" && p.note)
|
|
1624
1757
|
console.log(chalk.gray(` ${p.note}`));
|
|
1625
1758
|
if (p.kind === "error") console.log(chalk.red(` ✗ ${p.note}`));
|
|
1626
1759
|
}
|
|
@@ -1698,12 +1831,27 @@ class DenCLI {
|
|
|
1698
1831
|
const resolved = resolveService(address, workspace);
|
|
1699
1832
|
if (resolved) {
|
|
1700
1833
|
const health = await checkServiceHealth(resolved, "local");
|
|
1701
|
-
|
|
1834
|
+
// `conflict`: a shared stack namespace runs another checkout's
|
|
1835
|
+
// project; the real up below refuses it, naming both.
|
|
1836
|
+
const claim =
|
|
1837
|
+
health.health === "ok" && workspace
|
|
1838
|
+
? claimHealthyService(resolved, workspace, printNotice)
|
|
1839
|
+
: "untracked";
|
|
1840
|
+
// Healthy, but another process answers on its port: said (above),
|
|
1841
|
+
// never claimed, and nothing is started on top of it.
|
|
1842
|
+
if (health.health === "ok" && claim === "busy") {
|
|
1843
|
+
console.log(
|
|
1844
|
+
chalk.red(
|
|
1845
|
+
` ✗ ${chalk.bold(address)} not started: its port is in use by a process ${product().cli} won't adopt`,
|
|
1846
|
+
),
|
|
1847
|
+
);
|
|
1848
|
+
// A dependency only warns; the service you named failed.
|
|
1849
|
+
if (isNamedTarget(address, opts?.named ?? [])) process.exitCode = 1;
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
if (health.health === "ok" && claim !== "conflict") {
|
|
1702
1853
|
// Skipping must not lose the refcount — this workspace is an owner
|
|
1703
1854
|
// of what's running, whether den tracked it or not.
|
|
1704
|
-
const claim = workspace
|
|
1705
|
-
? claimHealthyService(resolved, workspace)
|
|
1706
|
-
: "untracked";
|
|
1707
1855
|
const suffix =
|
|
1708
1856
|
claim === "adopted"
|
|
1709
1857
|
? " (adopted its running containers)"
|
|
@@ -1804,6 +1952,8 @@ class DenCLI {
|
|
|
1804
1952
|
let result: ReturnType<typeof runServiceCommand> | null = null;
|
|
1805
1953
|
let detachable = false;
|
|
1806
1954
|
let interactive = false;
|
|
1955
|
+
// A down that found nothing to stop is said as such, never as ✔.
|
|
1956
|
+
let downOutcome: DownOutcome | null = null;
|
|
1807
1957
|
|
|
1808
1958
|
const settleFromClose = (exitCode: number): void => {
|
|
1809
1959
|
if (settled) return;
|
|
@@ -1820,6 +1970,17 @@ class DenCLI {
|
|
|
1820
1970
|
),
|
|
1821
1971
|
);
|
|
1822
1972
|
}
|
|
1973
|
+
} else if (exitCode === 0 && downOutcome === "nothing-recorded") {
|
|
1974
|
+
spinner.info(
|
|
1975
|
+
`${command} ${chalk.bold(address)} ${chalk.gray("(nothing to stop)")}`,
|
|
1976
|
+
);
|
|
1977
|
+
} else if (exitCode === 0 && downOutcome === "unstoppable") {
|
|
1978
|
+
spinner.warn(
|
|
1979
|
+
`${command} ${chalk.bold(address)} ${chalk.yellow("(could not stop it)")}`,
|
|
1980
|
+
);
|
|
1981
|
+
for (const l of lastLines) {
|
|
1982
|
+
console.log(chalk.gray(` ${l}`));
|
|
1983
|
+
}
|
|
1823
1984
|
} else if (exitCode === 0) {
|
|
1824
1985
|
spinner.succeed(`${command} ${chalk.bold(address)}`);
|
|
1825
1986
|
} else {
|
|
@@ -1873,8 +2034,21 @@ class DenCLI {
|
|
|
1873
2034
|
runtime,
|
|
1874
2035
|
workspace,
|
|
1875
2036
|
force: command === "down" ? force : false,
|
|
2037
|
+
// Down fails when it cannot stop a service the user named; one
|
|
2038
|
+
// reached as part of a workspace-wide down warns instead.
|
|
2039
|
+
explicit: isNamedTarget(address, opts?.named ?? []),
|
|
2040
|
+
onDownOutcome: (outcome) => {
|
|
2041
|
+
downOutcome = outcome;
|
|
2042
|
+
},
|
|
1876
2043
|
secretEnv: secrets.secretEnv,
|
|
1877
2044
|
onLine: (line) => {
|
|
2045
|
+
// What Den decided about a record or a port is said, not
|
|
2046
|
+
// only flashed in the spinner (0.3.0-dev.39).
|
|
2047
|
+
if (isNotice(line)) {
|
|
2048
|
+
spinner.clear();
|
|
2049
|
+
printNotice(line);
|
|
2050
|
+
if (spinner.isEnabled) spinner.render();
|
|
2051
|
+
}
|
|
1878
2052
|
spinner.text = `${command} ${chalk.bold(address)}: ${chalk.gray(line.slice(0, 80))}`;
|
|
1879
2053
|
const trimmed = line.trim();
|
|
1880
2054
|
if (trimmed) {
|
|
@@ -2130,11 +2304,15 @@ class DenCLI {
|
|
|
2130
2304
|
this.renderUpClonePlan(rawOrder);
|
|
2131
2305
|
return;
|
|
2132
2306
|
}
|
|
2307
|
+
// A repo with no up of its own starts its services, one with no up at
|
|
2308
|
+
// all is skipped: the decision `up -a` makes (resolveUpTargets).
|
|
2309
|
+
const targets = new Set(include ? [seed] : []);
|
|
2133
2310
|
if (dryRun) {
|
|
2311
|
+
const up = this.upTargets(order, targets, graph);
|
|
2134
2312
|
await this.runServicesCommand(
|
|
2135
|
-
order,
|
|
2313
|
+
up.order,
|
|
2136
2314
|
"up",
|
|
2137
|
-
{ order },
|
|
2315
|
+
{ order: up.order },
|
|
2138
2316
|
{ named, dryRun: true },
|
|
2139
2317
|
);
|
|
2140
2318
|
return;
|
|
@@ -2158,12 +2336,18 @@ class DenCLI {
|
|
|
2158
2336
|
);
|
|
2159
2337
|
return;
|
|
2160
2338
|
}
|
|
2339
|
+
const up = this.upTargets(order, targets, graph);
|
|
2340
|
+
if (up.order.length === 0) {
|
|
2341
|
+
console.log(chalk.yellow(" Nothing to start.\n"));
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2161
2344
|
await this.runServicesCommand(
|
|
2162
|
-
order,
|
|
2345
|
+
up.order,
|
|
2163
2346
|
"up",
|
|
2164
2347
|
{
|
|
2165
|
-
order,
|
|
2166
|
-
waitDepsFor: (address) =>
|
|
2348
|
+
order: up.order,
|
|
2349
|
+
waitDepsFor: (address) =>
|
|
2350
|
+
up.depsAs(hardDepsOf(graph, repoOfAddress(address))),
|
|
2167
2351
|
failFast: true,
|
|
2168
2352
|
skipHealthyAll: true,
|
|
2169
2353
|
},
|
|
@@ -2171,6 +2355,37 @@ class DenCLI {
|
|
|
2171
2355
|
);
|
|
2172
2356
|
}
|
|
2173
2357
|
|
|
2358
|
+
/**
|
|
2359
|
+
* The start order `up` runs (`up <repo>` and `up -a` alike): a repo with
|
|
2360
|
+
* no up of its own replaced by its services' ups, one with no up at all
|
|
2361
|
+
* skipped (resolveUpTargets), said as it decides.
|
|
2362
|
+
*/
|
|
2363
|
+
private upTargets(
|
|
2364
|
+
order: string[],
|
|
2365
|
+
targets: ReadonlySet<string>,
|
|
2366
|
+
graph: DerivedGraph,
|
|
2367
|
+
): UpTargets {
|
|
2368
|
+
const up = resolveUpTargets(order, this.focusedWs(), { targets, graph });
|
|
2369
|
+
this.sayUpTargets(up);
|
|
2370
|
+
return up;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/** What resolveUpTargets decided, in `up`'s words. */
|
|
2374
|
+
private sayUpTargets(up: UpTargets): void {
|
|
2375
|
+
for (const [repo, addrs] of up.expanded)
|
|
2376
|
+
console.log(
|
|
2377
|
+
chalk.gray(
|
|
2378
|
+
` ${repo} has no up of its own: starting ${addrs.join(", ")}`,
|
|
2379
|
+
),
|
|
2380
|
+
);
|
|
2381
|
+
if (up.skippedNoUp.length > 0)
|
|
2382
|
+
console.log(
|
|
2383
|
+
chalk.gray(
|
|
2384
|
+
` Skipping (no local up command): ${up.skippedNoUp.join(", ")}\n`,
|
|
2385
|
+
),
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2174
2389
|
/**
|
|
2175
2390
|
* Stop the seed repo and the dependencies it was started with — the same
|
|
2176
2391
|
* closure `den up <seed>` brings up, torn down in reverse (seed first,
|
|
@@ -2388,19 +2603,18 @@ class DenCLI {
|
|
|
2388
2603
|
// is skipped, not fatal. `-a` starts independent peers — one unrunnable
|
|
2389
2604
|
// repo must not strand the rest (runServiceCommand exits 1 on a missing
|
|
2390
2605
|
// command, and a fail-fast would turn that into a full abort). Now that
|
|
2391
|
-
// clones exist, the manifest lookup is meaningful.
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
}
|
|
2606
|
+
// clones exist, the manifest lookup is meaningful. A repo with no up of
|
|
2607
|
+
// its own but per-service ups starts those services: the same decision
|
|
2608
|
+
// `up <repo>` makes for a dependency (upTargets).
|
|
2609
|
+
const up = resolveUpTargets(candidates, this.focusedWs(), {
|
|
2610
|
+
targets: new Set(candidates),
|
|
2611
|
+
graph,
|
|
2612
|
+
});
|
|
2613
|
+
const order = up.order;
|
|
2614
|
+
const systems = candidates.filter((r) => !up.skippedNoUp.includes(r));
|
|
2401
2615
|
console.log(
|
|
2402
2616
|
chalk.cyan(
|
|
2403
|
-
`\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(
|
|
2617
|
+
`\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(systems.length))} ${noun(systems.length, "system")}`,
|
|
2404
2618
|
),
|
|
2405
2619
|
);
|
|
2406
2620
|
console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
|
|
@@ -2412,13 +2626,7 @@ class DenCLI {
|
|
|
2412
2626
|
),
|
|
2413
2627
|
);
|
|
2414
2628
|
}
|
|
2415
|
-
|
|
2416
|
-
console.log(
|
|
2417
|
-
chalk.gray(
|
|
2418
|
-
` Skipping (no local up command): ${skippedNoUp.join(", ")}\n`,
|
|
2419
|
-
),
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2629
|
+
this.sayUpTargets(up);
|
|
2422
2630
|
if (order.length === 0) {
|
|
2423
2631
|
console.log(chalk.yellow(" Nothing to start.\n"));
|
|
2424
2632
|
return;
|
|
@@ -2428,13 +2636,17 @@ class DenCLI {
|
|
|
2428
2636
|
// healthy-skip. Unlike runDerivedUp there is NO failFast on the start
|
|
2429
2637
|
// loop: `-a`'s systems are independent peers — one failure must not
|
|
2430
2638
|
// strand the rest (the web master button continues too).
|
|
2431
|
-
const repoSet = new Set(
|
|
2639
|
+
const repoSet = new Set(systems);
|
|
2432
2640
|
await this.runServicesCommand(order, "up", {
|
|
2433
2641
|
order,
|
|
2434
2642
|
// Hard deps outside the workspace can't be waited on — filter to the set
|
|
2435
2643
|
// we are actually starting.
|
|
2436
2644
|
waitDepsFor: (address) =>
|
|
2437
|
-
|
|
2645
|
+
up.depsAs(
|
|
2646
|
+
hardDepsOf(graph, repoOfAddress(address)).filter((dep) =>
|
|
2647
|
+
repoSet.has(dep),
|
|
2648
|
+
),
|
|
2649
|
+
),
|
|
2438
2650
|
skipHealthyAll: true,
|
|
2439
2651
|
});
|
|
2440
2652
|
}
|
|
@@ -2470,6 +2682,14 @@ class DenCLI {
|
|
|
2470
2682
|
return;
|
|
2471
2683
|
}
|
|
2472
2684
|
await this.runServicesCommand(order, "down", { order }, { force });
|
|
2685
|
+
// Records no workspace reaches: -a stops every workspace's services,
|
|
2686
|
+
// not these. Said, and left alone (0.3.0-dev.39, a consumer's finding).
|
|
2687
|
+
if (!only)
|
|
2688
|
+
for (const d of daemonsOutsideWorkspaces())
|
|
2689
|
+
printNotice(
|
|
2690
|
+
`Warning: ${d.address} is recorded as running (${d.repoPath}) but is in no active workspace: ` +
|
|
2691
|
+
`down -a left it alone. Stop it yourself if it still runs.`,
|
|
2692
|
+
);
|
|
2473
2693
|
}
|
|
2474
2694
|
|
|
2475
2695
|
private printUpHelp(): void {
|
|
@@ -2993,15 +3213,49 @@ class DenCLI {
|
|
|
2993
3213
|
|
|
2994
3214
|
private async cmdServiceStatus(argv: string[] = []): Promise<void> {
|
|
2995
3215
|
await this.ensureWorkspaceSeeded();
|
|
2996
|
-
const { app, rest:
|
|
3216
|
+
const { app, rest: appRest } = takeAppFlag(argv);
|
|
3217
|
+
// The view scope (as the web UI's): every active workspace by default,
|
|
3218
|
+
// or `--scope <workspace>` for one. A view only: focus is unchanged.
|
|
3219
|
+
const scoped = takeScopeFlag(appRest);
|
|
3220
|
+
const args = scoped.rest;
|
|
3221
|
+
const scopeArg =
|
|
3222
|
+
scoped.scope === null
|
|
3223
|
+
? null
|
|
3224
|
+
: parseScope(scoped.scope, getActiveWorkspaces());
|
|
3225
|
+
if (scoped.error || (scopeArg && !scopeArg.ok)) {
|
|
3226
|
+
console.log(
|
|
3227
|
+
chalk.red(
|
|
3228
|
+
`\n ${scoped.error ?? (scopeArg && !scopeArg.ok ? scopeArg.error : "")}\n`,
|
|
3229
|
+
),
|
|
3230
|
+
);
|
|
3231
|
+
process.exit(1);
|
|
3232
|
+
}
|
|
3233
|
+
// `-w <workspace>` still means that workspace only (active or not).
|
|
3234
|
+
const named = process.env.DEN_WORKSPACE_OVERRIDE
|
|
3235
|
+
? getActiveWorkspace()
|
|
3236
|
+
: null;
|
|
3237
|
+
const view =
|
|
3238
|
+
named && !scopeArg
|
|
3239
|
+
? {
|
|
3240
|
+
scope: named.name,
|
|
3241
|
+
fellBack: false,
|
|
3242
|
+
workspaces: [named],
|
|
3243
|
+
focus: named.name,
|
|
3244
|
+
}
|
|
3245
|
+
: scopeView(scopeArg?.ok ? scopeArg.value : undefined);
|
|
2997
3246
|
const only = app ? focusedAppRepos(app) : null;
|
|
2998
3247
|
const verbose = args.includes("--verbose") || args.includes("--all");
|
|
2999
3248
|
const spinner = ora("Checking service health...").start();
|
|
3000
|
-
const raw = (
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3249
|
+
const raw = (
|
|
3250
|
+
await checkCatalogHealth(
|
|
3251
|
+
undefined,
|
|
3252
|
+
"local",
|
|
3253
|
+
view.workspaces.filter((w) => w.config),
|
|
3254
|
+
)
|
|
3255
|
+
).filter((h) => !only || only.has(h.repoName));
|
|
3256
|
+
// One row per repo, rolled up from every checked service by the rule the
|
|
3257
|
+
// web UI's header and graph use: entry down → down, another checked
|
|
3258
|
+
// service down → degraded; unchecked services never count.
|
|
3005
3259
|
const rolled = verbose ? raw : rollupRepoHealth(raw);
|
|
3006
3260
|
// Order top-down like the graph: entry repo first (shop-web) … foundation
|
|
3007
3261
|
// last (infra-stack), subsystems grouped under their repo.
|
|
@@ -3019,10 +3273,7 @@ class DenCLI {
|
|
|
3019
3273
|
return;
|
|
3020
3274
|
}
|
|
3021
3275
|
|
|
3022
|
-
|
|
3023
|
-
console.log(
|
|
3024
|
-
chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
|
|
3025
|
-
);
|
|
3276
|
+
console.log(chalk.cyan(`\n ${scopeLine(view)}\n`));
|
|
3026
3277
|
|
|
3027
3278
|
const table = newTable({
|
|
3028
3279
|
head: ["Service", "Mode", "Branch", "Health", "Reason"],
|
|
@@ -3030,8 +3281,20 @@ class DenCLI {
|
|
|
3030
3281
|
});
|
|
3031
3282
|
|
|
3032
3283
|
for (const r of results) {
|
|
3033
|
-
const healthStr =
|
|
3034
|
-
|
|
3284
|
+
const healthStr = r.rollup
|
|
3285
|
+
? formatRollupLabel(r.rollup)
|
|
3286
|
+
: formatHealthLabel(r.health);
|
|
3287
|
+
const rolledWhy = r.rollup ? rollupReasonText(r.rollup) : "";
|
|
3288
|
+
const reasonStr =
|
|
3289
|
+
r.rollup && r.rollup.state !== "unknown"
|
|
3290
|
+
? chalk.gray(
|
|
3291
|
+
[r.rollup.state === "down" ? healthReasonText(r) : "", rolledWhy]
|
|
3292
|
+
.filter(Boolean)
|
|
3293
|
+
.join("; ")
|
|
3294
|
+
.slice(0, 60),
|
|
3295
|
+
)
|
|
3296
|
+
: formatHealthReason(r) ||
|
|
3297
|
+
(rolledWhy ? chalk.gray(rolledWhy.slice(0, 60)) : "");
|
|
3035
3298
|
const placed = placementSuffix({
|
|
3036
3299
|
runs: r.runs ?? "none",
|
|
3037
3300
|
target: r.target ?? null,
|
|
@@ -3039,6 +3302,7 @@ class DenCLI {
|
|
|
3039
3302
|
|
|
3040
3303
|
table.push([
|
|
3041
3304
|
this.labelWithAutostart(r.address, noAutostart) +
|
|
3305
|
+
chalk.gray(ownersSuffix(r.workspaces)) +
|
|
3042
3306
|
(placed ? chalk.cyan(` ${placed}`) : ""),
|
|
3043
3307
|
r.mode === "dev-managed"
|
|
3044
3308
|
? chalk.blue("dev-managed")
|
|
@@ -3060,7 +3324,7 @@ class DenCLI {
|
|
|
3060
3324
|
if (!verbose && raw.length !== results.length) {
|
|
3061
3325
|
console.log(
|
|
3062
3326
|
chalk.gray(
|
|
3063
|
-
` ▪
|
|
3327
|
+
` ▪ one row per repo, rolled up from its checked services — \`${product().cli} status --verbose\` for each`,
|
|
3064
3328
|
),
|
|
3065
3329
|
);
|
|
3066
3330
|
}
|
|
@@ -3231,13 +3495,14 @@ class DenCLI {
|
|
|
3231
3495
|
private async cmdRegister(args: string[]): Promise<void> {
|
|
3232
3496
|
if (args.includes("-h") || args.includes("--help")) {
|
|
3233
3497
|
console.log(
|
|
3234
|
-
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>]\n\n` +
|
|
3498
|
+
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>] [--org <id>]\n\n` +
|
|
3235
3499
|
" With a path: add that checkout to the catalog and the focused\n" +
|
|
3236
3500
|
" workspace as your own (dev-managed) copy, without prompts. The name\n" +
|
|
3237
3501
|
" defaults to the manifest's name, then the folder name; the GitHub\n" +
|
|
3238
3502
|
" slug to the git remote. -w writes that workspace (active or not)\n" +
|
|
3239
3503
|
" and leaves focus alone.\n" +
|
|
3240
|
-
" Without a path: choose a local checkout or a managed clone interactively
|
|
3504
|
+
" Without a path: choose a local checkout or a managed clone interactively;\n" +
|
|
3505
|
+
" a managed clone comes from the workspace's org, or --org <id>.\n",
|
|
3241
3506
|
);
|
|
3242
3507
|
return;
|
|
3243
3508
|
}
|
|
@@ -3245,7 +3510,9 @@ class DenCLI {
|
|
|
3245
3510
|
(a, i) =>
|
|
3246
3511
|
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3247
3512
|
);
|
|
3248
|
-
// Without a path, register prompts: with no terminal
|
|
3513
|
+
// Without a path, register prompts: with no terminal (stdin closed or
|
|
3514
|
+
// piped, where readline would throw ERR_USE_AFTER_CLOSE mid-prompt), say
|
|
3515
|
+
// how to name one. Usage error: exit 2.
|
|
3249
3516
|
if (!repoPathArg && !process.stdin.isTTY) {
|
|
3250
3517
|
process.stderr.write(
|
|
3251
3518
|
`register needs a terminal (stdin is not a TTY); without one: ` +
|
|
@@ -3282,7 +3549,157 @@ class DenCLI {
|
|
|
3282
3549
|
}
|
|
3283
3550
|
}
|
|
3284
3551
|
|
|
3285
|
-
/**
|
|
3552
|
+
/**
|
|
3553
|
+
* `relocate <repo> <path>`: a checkout moved on disk. Rewrites the path of
|
|
3554
|
+
* every dev-managed entry for <repo> (bare and `<repo>/<svc>` addresses) in
|
|
3555
|
+
* every workspace, or only the `-w` one. Managed entries are Den's own
|
|
3556
|
+
* clones and are left alone. Focus and the active set never change.
|
|
3557
|
+
*/
|
|
3558
|
+
private async cmdRelocate(args: string[]): Promise<void> {
|
|
3559
|
+
const usage = `${product().cli} relocate <repo> <path> [--dry-run] [--force] [-w <workspace>]`;
|
|
3560
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
3561
|
+
console.log(
|
|
3562
|
+
`\n ${usage}\n\n` +
|
|
3563
|
+
" A checkout moved: point every workspace's dev-managed entries for\n" +
|
|
3564
|
+
" <repo> at <path>. Refuses a missing path, one whose manifest\n" +
|
|
3565
|
+
" name or GitHub remote names a different repo, and one where\n" +
|
|
3566
|
+
" neither names this repo (--force accepts that last). --dry-run shows\n" +
|
|
3567
|
+
" the changes without writing. -w limits it to that workspace.\n" +
|
|
3568
|
+
" Focus and the active set never change.\n",
|
|
3569
|
+
);
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
3573
|
+
const force = args.includes("--force");
|
|
3574
|
+
const unknown = args.find(
|
|
3575
|
+
(a) => a.startsWith("-") && !["--dry-run", "-n", "--force"].includes(a),
|
|
3576
|
+
);
|
|
3577
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
3578
|
+
if (unknown || positional.length !== 2) {
|
|
3579
|
+
console.error(
|
|
3580
|
+
chalk.red(
|
|
3581
|
+
`\n ❌ ${unknown ? `Unknown flag: ${unknown}` : "Need a repo and a path"}.\n Usage: ${usage}\n`,
|
|
3582
|
+
),
|
|
3583
|
+
);
|
|
3584
|
+
process.exitCode = 1;
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
const [repoName, rawPath] = positional;
|
|
3588
|
+
const { existsSync, statSync } = await import("fs");
|
|
3589
|
+
const newPath = nodePath.resolve(rawPath);
|
|
3590
|
+
const fail = (msg: string) => {
|
|
3591
|
+
console.error(chalk.red(`\n ❌ ${msg}\n`));
|
|
3592
|
+
process.exitCode = 1;
|
|
3593
|
+
};
|
|
3594
|
+
const file = readWorkspacesFile();
|
|
3595
|
+
const only = process.env.DEN_WORKSPACE_OVERRIDE;
|
|
3596
|
+
const names = only ? [only] : Object.keys(file.workspaces);
|
|
3597
|
+
const belongs = (key: string) =>
|
|
3598
|
+
key === repoName || key.startsWith(`${repoName}/`);
|
|
3599
|
+
if (
|
|
3600
|
+
!names.some((ws) =>
|
|
3601
|
+
Object.keys(file.workspaces[ws]?.services ?? {}).some(belongs),
|
|
3602
|
+
)
|
|
3603
|
+
) {
|
|
3604
|
+
fail(
|
|
3605
|
+
`"${repoName}" is not in ${only ? `workspace "${only}"` : "any workspace"}.`,
|
|
3606
|
+
);
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
if (!existsSync(newPath) || !statSync(newPath).isDirectory()) {
|
|
3610
|
+
fail(`Path does not exist: ${newPath}`);
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// The new path must hold this repo: its manifest name and GitHub remote,
|
|
3615
|
+
// when present, must agree with the repo and its catalog entry.
|
|
3616
|
+
const manifest = readManifest(newPath);
|
|
3617
|
+
const leaf = repoName.split("/").pop() ?? repoName;
|
|
3618
|
+
if (
|
|
3619
|
+
manifest?.name &&
|
|
3620
|
+
manifest.name !== repoName &&
|
|
3621
|
+
manifest.name !== leaf
|
|
3622
|
+
) {
|
|
3623
|
+
fail(
|
|
3624
|
+
`${newPath} holds "${manifest.name}", not "${repoName}" (its manifest name).`,
|
|
3625
|
+
);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
const catalogGh = readCatalog()[repoName]?.gh?.trim();
|
|
3629
|
+
const coords = getGitHubCoords(newPath);
|
|
3630
|
+
const remote = coords ? `${coords.owner}/${coords.repo}` : undefined;
|
|
3631
|
+
if (
|
|
3632
|
+
catalogGh &&
|
|
3633
|
+
remote &&
|
|
3634
|
+
catalogGh.toLowerCase() !== remote.toLowerCase()
|
|
3635
|
+
) {
|
|
3636
|
+
fail(
|
|
3637
|
+
`${newPath} is a checkout of ${remote}; the catalog has "${repoName}" as ${catalogGh}.`,
|
|
3638
|
+
);
|
|
3639
|
+
return;
|
|
3640
|
+
}
|
|
3641
|
+
// Not contradicting is not enough: an empty or unrelated folder has no
|
|
3642
|
+
// manifest and no remote. Something must say this is the repo.
|
|
3643
|
+
const identified =
|
|
3644
|
+
manifest?.name === repoName ||
|
|
3645
|
+
manifest?.name === leaf ||
|
|
3646
|
+
(!!catalogGh &&
|
|
3647
|
+
!!remote &&
|
|
3648
|
+
catalogGh.toLowerCase() === remote.toLowerCase()) ||
|
|
3649
|
+
(!catalogGh && coords?.repo.toLowerCase() === leaf.toLowerCase());
|
|
3650
|
+
if (!identified && !force) {
|
|
3651
|
+
fail(
|
|
3652
|
+
`Nothing at ${newPath} says it is "${repoName}": no manifest named ` +
|
|
3653
|
+
`"${leaf}" and no GitHub remote ${catalogGh || `…/${leaf}`}.\n` +
|
|
3654
|
+
` If it is, say so: ${product().cli} relocate ${repoName} ${rawPath} --force${dryRun ? " --dry-run" : ""}${only ? ` -w ${only}` : ""}`,
|
|
3655
|
+
);
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
const changes: { ws: string; addr: string; from: string }[] = [];
|
|
3660
|
+
const managed: { ws: string; addr: string }[] = [];
|
|
3661
|
+
for (const ws of names) {
|
|
3662
|
+
const entries = file.workspaces[ws]?.services ?? {};
|
|
3663
|
+
for (const [addr, entry] of Object.entries(entries)) {
|
|
3664
|
+
if (!belongs(addr)) continue;
|
|
3665
|
+
if (entry.mode !== "dev-managed") {
|
|
3666
|
+
managed.push({ ws, addr });
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
if (entry.path && nodePath.resolve(entry.path) === newPath) continue;
|
|
3670
|
+
changes.push({ ws, addr, from: entry.path ?? "(default)" });
|
|
3671
|
+
if (!dryRun) entry.path = newPath;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
if (changes.length > 0 && !dryRun) writeWorkspacesFile(file);
|
|
3676
|
+
|
|
3677
|
+
const verb = dryRun ? "Would point" : "Pointed";
|
|
3678
|
+
if (changes.length === 0) {
|
|
3679
|
+
console.log(
|
|
3680
|
+
chalk.gray(
|
|
3681
|
+
`\n Nothing to change: "${repoName}" is already at ${newPath}.`,
|
|
3682
|
+
),
|
|
3683
|
+
);
|
|
3684
|
+
} else {
|
|
3685
|
+
console.log(chalk.green(`\n ✓ ${verb} at ${newPath}:\n`));
|
|
3686
|
+
for (const c of changes) {
|
|
3687
|
+
console.log(
|
|
3688
|
+
` ${chalk.bold(c.addr)} ${chalk.gray(`${c.ws} was ${c.from}`)}`,
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
for (const m of managed) {
|
|
3693
|
+
console.log(
|
|
3694
|
+
chalk.gray(
|
|
3695
|
+
` ${m.addr} ${m.ws} managed (Den's clone): left alone`,
|
|
3696
|
+
),
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3699
|
+
console.log();
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/** Non-interactive `register <path>`: a dev-managed entry in the focused (or -w) workspace. */
|
|
3286
3703
|
private async registerPath(
|
|
3287
3704
|
ws: {
|
|
3288
3705
|
name: string;
|
|
@@ -3751,14 +4168,32 @@ class DenCLI {
|
|
|
3751
4168
|
const only = orgFlag(args.slice(1));
|
|
3752
4169
|
const { resolved, warnings } = orgListRows();
|
|
3753
4170
|
const orgOfWs = new Map(resolved.map((w) => [w.workspace, w.org]));
|
|
3754
|
-
const table = newTable({
|
|
3755
|
-
head: ["Workspace", "Org", "Active", "Focus", "Services", "Type"],
|
|
3756
|
-
style: { head: ["cyan"] },
|
|
3757
|
-
});
|
|
3758
4171
|
const shown = Object.entries(file.workspaces).filter(
|
|
3759
4172
|
([name]) => !only || sameOrg(orgOfWs.get(name) ?? null, only),
|
|
3760
4173
|
);
|
|
4174
|
+
// A Stacks column only when a shown workspace's stack namespace is
|
|
4175
|
+
// not its default suffix (or is invalid): the list is otherwise as before.
|
|
4176
|
+
const nsRows = new Map(
|
|
4177
|
+
stackNamespaceRows(shown.map(([n]) => n)).map((r) => [
|
|
4178
|
+
r.workspace,
|
|
4179
|
+
r,
|
|
4180
|
+
]),
|
|
4181
|
+
);
|
|
4182
|
+
const nsCol = [...nsRows.values()].some((r) => r.differs || r.error);
|
|
4183
|
+
const table = newTable({
|
|
4184
|
+
head: [
|
|
4185
|
+
"Workspace",
|
|
4186
|
+
"Org",
|
|
4187
|
+
"Active",
|
|
4188
|
+
"Focus",
|
|
4189
|
+
"Services",
|
|
4190
|
+
"Type",
|
|
4191
|
+
...(nsCol ? ["Stacks"] : []),
|
|
4192
|
+
],
|
|
4193
|
+
style: { head: ["cyan"] },
|
|
4194
|
+
});
|
|
3761
4195
|
for (const [name, cfg] of shown) {
|
|
4196
|
+
const ns = nsRows.get(name);
|
|
3762
4197
|
table.push([
|
|
3763
4198
|
chalk.bold(name),
|
|
3764
4199
|
orgOfWs.get(name) ?? chalk.gray("none"),
|
|
@@ -3766,9 +4201,23 @@ class DenCLI {
|
|
|
3766
4201
|
name === focus ? chalk.cyan("●") : "",
|
|
3767
4202
|
Object.keys(cfg.services).join(", ") || chalk.gray("none"),
|
|
3768
4203
|
cfg.directory ? chalk.magenta("feature") : chalk.blue("default"),
|
|
4204
|
+
...(nsCol
|
|
4205
|
+
? [
|
|
4206
|
+
ns?.error
|
|
4207
|
+
? chalk.red("invalid")
|
|
4208
|
+
: ns?.differs
|
|
4209
|
+
? `${ns.stack_namespace} ${chalk.gray(`(${ns.source})`)}`
|
|
4210
|
+
: chalk.gray(ns?.default ?? ""),
|
|
4211
|
+
]
|
|
4212
|
+
: []),
|
|
3769
4213
|
]);
|
|
3770
4214
|
}
|
|
3771
4215
|
console.log(table.toString());
|
|
4216
|
+
printOrgWarnings(
|
|
4217
|
+
[...nsRows.values()].flatMap((r) =>
|
|
4218
|
+
r.error ? [`workspace ${r.workspace}: ${r.error}`] : r.warnings,
|
|
4219
|
+
),
|
|
4220
|
+
);
|
|
3772
4221
|
if (only && shown.length === 0)
|
|
3773
4222
|
console.log(chalk.yellow(` No workspace in org ${only}.`));
|
|
3774
4223
|
const names = new Set(shown.map(([n]) => n));
|
|
@@ -4075,6 +4524,58 @@ class DenCLI {
|
|
|
4075
4524
|
// ── den validate ────────────────────────────────────────────────────────
|
|
4076
4525
|
|
|
4077
4526
|
private async cmdValidate(args: string[]): Promise<void> {
|
|
4527
|
+
// The org config first (0.3.0-dev.40, a consumer's 0.2.8 nit: a
|
|
4528
|
+
// ConfigError went unreported): each problem with its file and key
|
|
4529
|
+
// path, a non-zero exit, and the manifests are still checked, reading
|
|
4530
|
+
// the config without its org data.
|
|
4531
|
+
try {
|
|
4532
|
+
assertDenConfigValid();
|
|
4533
|
+
} catch (e) {
|
|
4534
|
+
if (!isConfigError(e)) throw e;
|
|
4535
|
+
console.error(chalk.red(`\n ✗ ${product().configFile} is invalid:`));
|
|
4536
|
+
const issues = e.issues.length
|
|
4537
|
+
? e.issues
|
|
4538
|
+
: [{ file: product().configFile, key: "", message: e.message }];
|
|
4539
|
+
for (const i of issues)
|
|
4540
|
+
console.error(
|
|
4541
|
+
chalk.red(` ✗ ${i.file} ${chalk.bold(i.key)} ${i.message}`),
|
|
4542
|
+
);
|
|
4543
|
+
process.exitCode = 1;
|
|
4544
|
+
waiveInvalidOrgs();
|
|
4545
|
+
}
|
|
4546
|
+
// The distribution's secrets config, once: a 1password backend with no
|
|
4547
|
+
// refs leaves the vault-backed features off (no built-in vault/item).
|
|
4548
|
+
try {
|
|
4549
|
+
const { secretsConfigWarnings } =
|
|
4550
|
+
await import("../../shared/secrets/index.js");
|
|
4551
|
+
for (const w of secretsConfigWarnings())
|
|
4552
|
+
console.warn(chalk.yellow(`\n ⚠ ${w.replace(/\n/g, "\n ")}`));
|
|
4553
|
+
} catch {
|
|
4554
|
+
/* an unreadable config is reported elsewhere */
|
|
4555
|
+
}
|
|
4556
|
+
// The edge's TLS strategy, when there is an edge: its config, the
|
|
4557
|
+
// implicit-mkcert warning (once), and detect() (mkcert on PATH).
|
|
4558
|
+
{
|
|
4559
|
+
const { hasEdgeBlock, readEdgeConfig } =
|
|
4560
|
+
await import("../../shared/edge/config.js");
|
|
4561
|
+
const { edgeTlsWarnings, tlsStrategyFor, warnImplicitTlsOnce } =
|
|
4562
|
+
await import("../../shared/edge/tls.js");
|
|
4563
|
+
let edge: ReturnType<typeof readEdgeConfig> | null = null;
|
|
4564
|
+
try {
|
|
4565
|
+
if (hasEdgeBlock()) edge = readEdgeConfig();
|
|
4566
|
+
} catch (e) {
|
|
4567
|
+
console.error(chalk.red(`\n ✗ edge: ${(e as Error).message}`));
|
|
4568
|
+
process.exitCode = 1;
|
|
4569
|
+
}
|
|
4570
|
+
if (edge) {
|
|
4571
|
+
const warn = (w: string) => console.warn(chalk.yellow(`\n ⚠ ${w}`));
|
|
4572
|
+
warnImplicitTlsOnce(edge, warn);
|
|
4573
|
+
for (const w of edgeTlsWarnings(edge))
|
|
4574
|
+
if (edge.tlsFrom !== "implicit") warn(w);
|
|
4575
|
+
const d = tlsStrategyFor(edge).detect();
|
|
4576
|
+
if (!d.available) warn(`edge.tls ${edge.tls.strategy}: ${d.reason}`);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4078
4579
|
const positional = args.filter((a) => !a.startsWith("-"));
|
|
4079
4580
|
const held = args.includes("--held");
|
|
4080
4581
|
// Bare `validate` in the distribution root (which has no manifest of its
|
|
@@ -4279,7 +4780,7 @@ class DenCLI {
|
|
|
4279
4780
|
" restart [service...] Restart services (-A <app> for a whole application)",
|
|
4280
4781
|
);
|
|
4281
4782
|
console.log(
|
|
4282
|
-
" status Show service health for
|
|
4783
|
+
" status Show service health for the view scope (-A <app>, --scope <ws>)",
|
|
4283
4784
|
);
|
|
4284
4785
|
console.log(
|
|
4285
4786
|
" apps Org > Application > Repo > Services (--json)",
|
|
@@ -4300,6 +4801,9 @@ class DenCLI {
|
|
|
4300
4801
|
console.log(
|
|
4301
4802
|
" register [path] Register a repo in the service catalog",
|
|
4302
4803
|
);
|
|
4804
|
+
console.log(
|
|
4805
|
+
" relocate <repo> <path> Point a moved checkout's workspace entries at its new path",
|
|
4806
|
+
);
|
|
4303
4807
|
console.log();
|
|
4304
4808
|
console.log(
|
|
4305
4809
|
" setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
|
|
@@ -4317,6 +4821,22 @@ class DenCLI {
|
|
|
4317
4821
|
console.log(
|
|
4318
4822
|
" service install Run the web UI as a launchd service (status, print, uninstall)",
|
|
4319
4823
|
);
|
|
4824
|
+
console.log(
|
|
4825
|
+
" theme add <repo>@<ref> Install a web UI theme (list, validate, remove)",
|
|
4826
|
+
);
|
|
4827
|
+
console.log(
|
|
4828
|
+
" plugin list Local and added plugins, checked (validate, add, update, remove)",
|
|
4829
|
+
);
|
|
4830
|
+
console.log(
|
|
4831
|
+
" sessions start -w <ws> --name <n> -- <argv…> A terminal that outlives the web server",
|
|
4832
|
+
);
|
|
4833
|
+
console.log(
|
|
4834
|
+
" sessions list|attach|kill List, attach to (ctrl-] detaches) or stop sessions",
|
|
4835
|
+
);
|
|
4836
|
+
console.log(" agents list -w <ws> The workspace's agents");
|
|
4837
|
+
console.log(
|
|
4838
|
+
" agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, personas)",
|
|
4839
|
+
);
|
|
4320
4840
|
console.log();
|
|
4321
4841
|
console.log(
|
|
4322
4842
|
" workspace list List workspaces (active set + focus)",
|