@monster-orange/den 0.2.7 → 0.3.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 +1655 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +619 -3
- 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 +9 -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/agent-actor.ts +38 -3
- 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 +63 -0
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +185 -7
- 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/environment-manager/catalog.ts +52 -1
- package/src/shared/environment-manager/default-workspace.ts +26 -3
- package/src/shared/environment-manager/git-remote.ts +49 -0
- 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/workspace-manager.ts +67 -25
- package/src/shared/environment-manager/workspace-org.ts +26 -11
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +328 -0
- 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/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/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 +24 -5
- package/src/ui/cli/clone-plan.ts +154 -23
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +62 -3
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +649 -72
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/org-command.ts +12 -2
- package/src/ui/cli/plugin-command.ts +849 -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/up-args.ts +15 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +298 -0
- package/src/ui/mcp/server.ts +70 -8
- package/src/ui/mcp/tools.ts +151 -24
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +90 -3
- 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 +29 -45
- 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 +590 -45
- 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 +15 -3
- 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 +24 -4
- package/src/ui/server/release-service.ts +25 -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 +55 -9
- 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
|
|
@@ -32,10 +33,12 @@ import { moduleScanner } from "../../shared/test-discovery/module-scanner.js";
|
|
|
32
33
|
import { resolvePerformanceModules } from "./module-selection.js";
|
|
33
34
|
import {
|
|
34
35
|
readCatalog,
|
|
35
|
-
|
|
36
|
+
needsGhRepair,
|
|
37
|
+
resolveRepoCloneSource,
|
|
36
38
|
upsertCatalogEntry,
|
|
37
39
|
} from "../../shared/environment-manager/catalog.js";
|
|
38
40
|
import { seedSecretsFromSibling } from "../../shared/environment-manager/secrets-check.js";
|
|
41
|
+
import { upNeedsTarget } from "./up-args.js";
|
|
39
42
|
import {
|
|
40
43
|
readWorkspacesFile,
|
|
41
44
|
writeWorkspacesFile,
|
|
@@ -56,6 +59,13 @@ import {
|
|
|
56
59
|
DEN_SERVICES_DIR,
|
|
57
60
|
DEN_WORKSPACES_DIR,
|
|
58
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";
|
|
59
69
|
import {
|
|
60
70
|
deleteWorkspaceDirectory,
|
|
61
71
|
checkBehindCount,
|
|
@@ -68,9 +78,11 @@ import {
|
|
|
68
78
|
markSetupSucceeded,
|
|
69
79
|
fetchAndFastForward,
|
|
70
80
|
} from "../../shared/environment-manager/workspace-manager.js";
|
|
81
|
+
import { stackNamespaceRows } from "../../shared/stacks/namespace.js";
|
|
71
82
|
import {
|
|
72
83
|
runServiceCommand,
|
|
73
84
|
claimHealthyService,
|
|
85
|
+
daemonsOutsideWorkspaces,
|
|
74
86
|
getWorkspaceServices,
|
|
75
87
|
ensureDaemonRegistryLoaded,
|
|
76
88
|
hasRunningOpctlContainers,
|
|
@@ -102,13 +114,18 @@ import { waitForReady } from "../../shared/environment-manager/readiness.js";
|
|
|
102
114
|
import {
|
|
103
115
|
canonicalServiceAddress,
|
|
104
116
|
previewServiceCommand,
|
|
117
|
+
previewUpSettle,
|
|
105
118
|
previewServiceSecretEnv,
|
|
106
119
|
prepareServiceSecretEnv,
|
|
107
120
|
resolveService,
|
|
121
|
+
type DownOutcome,
|
|
108
122
|
type ServiceCommandPreview,
|
|
109
123
|
} from "../../shared/environment-manager/service-runner.js";
|
|
110
124
|
import { getGitHubCoords } from "../../shared/smoke/git-remote.js";
|
|
125
|
+
import { isConfigError } from "../../shared/config-error.js";
|
|
111
126
|
import {
|
|
127
|
+
assertDenConfigValid,
|
|
128
|
+
waiveInvalidOrgs,
|
|
112
129
|
getReposRoot,
|
|
113
130
|
mergeUserConfig,
|
|
114
131
|
readDenConfig,
|
|
@@ -123,6 +140,11 @@ import {
|
|
|
123
140
|
deriveStartTargets,
|
|
124
141
|
} from "../../shared/environment-manager/derivation/derived-up.js";
|
|
125
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";
|
|
126
148
|
import { loadAliasResolver } from "../../shared/environment-manager/derivation/load-alias.js";
|
|
127
149
|
import {
|
|
128
150
|
cmdApps,
|
|
@@ -170,8 +192,11 @@ import {
|
|
|
170
192
|
import { cleanupStaleTabs } from "../../shared/cli-tabs.js";
|
|
171
193
|
import { cmdConfig } from "./config-command.js";
|
|
172
194
|
import { cmdEdge } from "./edge-command.js";
|
|
195
|
+
import { cmdDoctor } from "./doctor-command.js";
|
|
173
196
|
import { cmdService } from "./service-command.js";
|
|
174
197
|
import { cmdStack } from "./stack-command.js";
|
|
198
|
+
import { cmdTheme } from "./theme-command.js";
|
|
199
|
+
import { cmdPlugin } from "./plugin-command.js";
|
|
175
200
|
import {
|
|
176
201
|
cmdOrg,
|
|
177
202
|
orgFlag,
|
|
@@ -191,6 +216,7 @@ import { cmdGraph } from "./graph-command.js";
|
|
|
191
216
|
import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
|
|
192
217
|
import {
|
|
193
218
|
parseCliArgs,
|
|
219
|
+
takeWorkspaceFlags,
|
|
194
220
|
unknownCommandFlags,
|
|
195
221
|
workspaceFlagRequiresActive,
|
|
196
222
|
type ParsedCliArgs,
|
|
@@ -205,6 +231,9 @@ import {
|
|
|
205
231
|
formatHealthDot,
|
|
206
232
|
formatHealthLabel,
|
|
207
233
|
formatHealthReason,
|
|
234
|
+
formatRollupLabel,
|
|
235
|
+
healthReasonText,
|
|
236
|
+
rollupReasonText,
|
|
208
237
|
} from "./health-display.js";
|
|
209
238
|
import { DEN_BANNER_LINES, DEN_DIVIDER } from "../../shared/banner.js";
|
|
210
239
|
|
|
@@ -296,9 +325,12 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
296
325
|
],
|
|
297
326
|
},
|
|
298
327
|
status: {
|
|
299
|
-
usage: "status [-A <app>]",
|
|
328
|
+
usage: "status [-A <app>] [--scope <all|workspace>] [--verbose]",
|
|
300
329
|
lines: [
|
|
301
|
-
"Show service health for
|
|
330
|
+
"Show service health for what is in view: every active workspace by",
|
|
331
|
+
"default (the web UI's scope), or --scope <workspace> for one. A",
|
|
332
|
+
"service several workspaces share is listed once, with its owners.",
|
|
333
|
+
"--verbose (or --all) shows one row per service instead of per repo.",
|
|
302
334
|
],
|
|
303
335
|
},
|
|
304
336
|
logs: {
|
|
@@ -315,15 +347,26 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
315
347
|
lines: ["List all registered services in the catalog."],
|
|
316
348
|
},
|
|
317
349
|
register: {
|
|
318
|
-
usage: "register [path] [--org <id>]",
|
|
350
|
+
usage: "register [path] [-w <workspace>] [--org <id>]",
|
|
319
351
|
lines: [
|
|
320
352
|
"With a path: add that checkout (dev-managed) under its manifest name.",
|
|
321
353
|
"Without one: pick the repo interactively. A managed clone is picked",
|
|
322
354
|
"from the focused workspace's org; --org <id> picks from another.",
|
|
355
|
+
"-w writes that workspace (active or not) and leaves focus alone.",
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
relocate: {
|
|
359
|
+
usage: "relocate <repo> <path> [--dry-run] [--force] [-w <workspace>]",
|
|
360
|
+
lines: [
|
|
361
|
+
"Point every workspace's dev-managed entries for <repo> at <path>.",
|
|
362
|
+
"Refuses a path that is missing, holds a different repo, or has",
|
|
363
|
+
"neither a manifest nor a remote naming this one (unless --force).",
|
|
364
|
+
"-w limits it to that workspace; focus never changes.",
|
|
323
365
|
],
|
|
324
366
|
},
|
|
325
367
|
setup: {
|
|
326
|
-
usage:
|
|
368
|
+
usage:
|
|
369
|
+
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
327
370
|
lines: [
|
|
328
371
|
"Run 'make setup' once per system (clones missing managed repos first).",
|
|
329
372
|
"Default targets MANAGED clones only — setup's codegen/installs",
|
|
@@ -331,6 +374,8 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
331
374
|
"--force (-f) includes dev-managed repos; naming a service always works.",
|
|
332
375
|
"--plan prints each repo's org and clone URL and stops: nothing is",
|
|
333
376
|
"cloned, seeded or written (on a fresh machine: the blueprint's plan).",
|
|
377
|
+
"It plans the focused workspace; -w <workspace> (repeatable) plans those,",
|
|
378
|
+
"--all-active every active one. Repos only other workspaces hold are named.",
|
|
334
379
|
],
|
|
335
380
|
},
|
|
336
381
|
org: {
|
|
@@ -409,7 +454,33 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
409
454
|
edge: {
|
|
410
455
|
usage: "edge [up|down|status|trust|untrust|routes]",
|
|
411
456
|
lines: [
|
|
412
|
-
"This org's
|
|
457
|
+
"This org's edge: routes at <name>.<edge.domain>, TLS as edge.tls says",
|
|
458
|
+
"(none | mkcert | files). trust/untrust exist only under mkcert.",
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
doctor: {
|
|
462
|
+
usage: "doctor",
|
|
463
|
+
lines: [
|
|
464
|
+
"Each strategy kind (secrets, tls): the one selected, whether it was implicit,",
|
|
465
|
+
"and whether it can work here (a PATH check; nothing is run or prompted).",
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
plugin: {
|
|
469
|
+
usage:
|
|
470
|
+
"plugin [list] | validate [dir] | add <repo>[/<subpath>]@<ref> [--track] [--yes] | update <id> @<ref> | remove <id>",
|
|
471
|
+
lines: [
|
|
472
|
+
"Plugins (den.plugin/v1) in plugins/<id>/: local (the distribution's own, no lock entry)",
|
|
473
|
+
"or added (vendored at an exact ref, pinned in plugins.lock.json). list checks each",
|
|
474
|
+
"against the lock; validate checks every manifest. add refuses file:// sources.",
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
theme: {
|
|
478
|
+
usage:
|
|
479
|
+
"theme [list] | validate [dir] | new <id> | add <repo>[/<subpath>]@<ref> | remove <id>",
|
|
480
|
+
lines: [
|
|
481
|
+
"Web UI themes (den.theme/v1). add fetches one at an exact ref, validates it",
|
|
482
|
+
"(schema, contrast in both modes, files) and vendors it into plugins/<id>/",
|
|
483
|
+
"with a plugins.lock.json entry; commit both. list checks them against the lock.",
|
|
413
484
|
],
|
|
414
485
|
},
|
|
415
486
|
mcp: {
|
|
@@ -430,7 +501,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
430
501
|
"local-dev": {
|
|
431
502
|
usage: "local-dev [on|off]",
|
|
432
503
|
lines: [
|
|
433
|
-
"Toggle the local-dev experience (web
|
|
504
|
+
"Toggle the local-dev experience (web Workspaces page + service mgmt).",
|
|
434
505
|
],
|
|
435
506
|
},
|
|
436
507
|
menu: {
|
|
@@ -458,6 +529,7 @@ function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
|
458
529
|
const TELEMETRY_COMMANDS = new Set<string>([
|
|
459
530
|
"run",
|
|
460
531
|
"view-results",
|
|
532
|
+
"down",
|
|
461
533
|
"menu",
|
|
462
534
|
"web",
|
|
463
535
|
"up",
|
|
@@ -468,6 +540,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
468
540
|
"services",
|
|
469
541
|
"graph",
|
|
470
542
|
"register",
|
|
543
|
+
"relocate",
|
|
471
544
|
"use-local",
|
|
472
545
|
`use-managed`,
|
|
473
546
|
"setup",
|
|
@@ -480,13 +553,40 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
480
553
|
"local-dev",
|
|
481
554
|
"config",
|
|
482
555
|
"edge",
|
|
556
|
+
"doctor",
|
|
483
557
|
"stack",
|
|
484
558
|
"apps",
|
|
485
559
|
"applications",
|
|
486
560
|
"mcp",
|
|
487
561
|
"service",
|
|
562
|
+
"theme",
|
|
563
|
+
"plugin",
|
|
564
|
+
]);
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Commands that run with a config that fails validation: the web and MCP
|
|
568
|
+
* servers (each route or tool reports it), and those that read neither orgs,
|
|
569
|
+
* the catalog nor clones (manifest validate, create, the per-engineer
|
|
570
|
+
* config and local-dev toggles, saved results), and `down` (0.3.0-dev.40:
|
|
571
|
+
* stopping is never stranded by a config that fails; it stops from the
|
|
572
|
+
* daemon records and compose projects, reading no org data). Every other
|
|
573
|
+
* registered command checks the config first.
|
|
574
|
+
*/
|
|
575
|
+
const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
576
|
+
"web",
|
|
577
|
+
"mcp",
|
|
578
|
+
"validate",
|
|
579
|
+
"create",
|
|
580
|
+
"config",
|
|
581
|
+
"local-dev",
|
|
582
|
+
"view-results",
|
|
583
|
+
"down",
|
|
488
584
|
]);
|
|
489
585
|
|
|
586
|
+
function configCheckedCommand(command: string): boolean {
|
|
587
|
+
return TELEMETRY_COMMANDS.has(command) && !CONFIG_FREE_COMMANDS.has(command);
|
|
588
|
+
}
|
|
589
|
+
|
|
490
590
|
/** Map a raw command token to a PII-safe, registered telemetry command name. */
|
|
491
591
|
function telemetryCommandName(command?: string): string {
|
|
492
592
|
if (!command) return "none";
|
|
@@ -709,6 +809,28 @@ class DenCLI {
|
|
|
709
809
|
}
|
|
710
810
|
|
|
711
811
|
try {
|
|
812
|
+
// A config that fails validation stops every command that touches
|
|
813
|
+
// orgs, the catalog, clones or releases before it reads or writes
|
|
814
|
+
// anything: some (sync over existing clones, register, workspace
|
|
815
|
+
// create) never read `orgs:` and used to carry on (ORG-1d).
|
|
816
|
+
if (command && configCheckedCommand(command)) assertDenConfigValid();
|
|
817
|
+
// `down` is never stranded by an invalid config (0.3.0-dev.40): it
|
|
818
|
+
// says so and stops from the daemon records and compose projects,
|
|
819
|
+
// reading the config without its org data.
|
|
820
|
+
if (command === "down") {
|
|
821
|
+
try {
|
|
822
|
+
assertDenConfigValid();
|
|
823
|
+
} catch (e) {
|
|
824
|
+
if (!isConfigError(e)) throw e;
|
|
825
|
+
console.log(
|
|
826
|
+
chalk.yellow(
|
|
827
|
+
`\n ⚠ ${e.message}\n Stopping anyway, without the config's org data. Fix it, then \`${product().cli} validate\`.`,
|
|
828
|
+
),
|
|
829
|
+
);
|
|
830
|
+
waiveInvalidOrgs();
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
712
834
|
switch (command) {
|
|
713
835
|
case "run":
|
|
714
836
|
await this.runTestsCommand(args.slice(1));
|
|
@@ -763,6 +885,10 @@ class DenCLI {
|
|
|
763
885
|
await this.cmdRegister(args.slice(1));
|
|
764
886
|
break;
|
|
765
887
|
|
|
888
|
+
case "relocate":
|
|
889
|
+
await this.cmdRelocate(args.slice(1));
|
|
890
|
+
break;
|
|
891
|
+
|
|
766
892
|
case "use-local":
|
|
767
893
|
await this.cmdUseMode(args.slice(1), "dev-managed");
|
|
768
894
|
break;
|
|
@@ -837,6 +963,10 @@ class DenCLI {
|
|
|
837
963
|
await cmdEdge(args.slice(1));
|
|
838
964
|
break;
|
|
839
965
|
|
|
966
|
+
case "doctor":
|
|
967
|
+
process.exitCode = cmdDoctor(args.slice(1));
|
|
968
|
+
break;
|
|
969
|
+
|
|
840
970
|
case "service":
|
|
841
971
|
process.exitCode = await cmdService(args.slice(1));
|
|
842
972
|
break;
|
|
@@ -845,6 +975,14 @@ class DenCLI {
|
|
|
845
975
|
await cmdStack(args.slice(1));
|
|
846
976
|
break;
|
|
847
977
|
|
|
978
|
+
case "theme":
|
|
979
|
+
process.exitCode = await cmdTheme(args.slice(1));
|
|
980
|
+
break;
|
|
981
|
+
|
|
982
|
+
case "plugin":
|
|
983
|
+
process.exitCode = await cmdPlugin(args.slice(1));
|
|
984
|
+
break;
|
|
985
|
+
|
|
848
986
|
case "apps":
|
|
849
987
|
case "applications":
|
|
850
988
|
await this.ensureWorkspaceSeeded();
|
|
@@ -1142,10 +1280,11 @@ class DenCLI {
|
|
|
1142
1280
|
try {
|
|
1143
1281
|
// An entry with `gh: ""` clones from the workspace's org, as `up`
|
|
1144
1282
|
// does (and `setup --plan` says), instead of failing on an empty slug.
|
|
1145
|
-
|
|
1283
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
1284
|
+
const ghSlug = resolveRepoCloneSource(repoName, catalog, () =>
|
|
1146
1285
|
githubOrgFor(ws.name),
|
|
1147
1286
|
);
|
|
1148
|
-
if (
|
|
1287
|
+
if (needsGhRepair(catalogEntry, ghSlug))
|
|
1149
1288
|
upsertCatalogEntry(repoName, { ...catalogEntry, gh: ghSlug });
|
|
1150
1289
|
ensureManagedServiceClone(repoName, ghSlug, branch);
|
|
1151
1290
|
} catch (err) {
|
|
@@ -1248,10 +1387,12 @@ class DenCLI {
|
|
|
1248
1387
|
// Catalog entries written by the dev-managed add flow carry `gh: ""`, so
|
|
1249
1388
|
// an entry can exist yet be unclonable. Derive the conventional slug and
|
|
1250
1389
|
// persist the repair rather than cloning from an empty URL.
|
|
1251
|
-
|
|
1390
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
1391
|
+
const ghSlug = resolveRepoCloneSource(repoName, catalog, () =>
|
|
1252
1392
|
githubOrgFor(ws.name),
|
|
1253
1393
|
);
|
|
1254
|
-
if (
|
|
1394
|
+
if (needsGhRepair(catalog[repoName], ghSlug))
|
|
1395
|
+
upsertCatalogEntry(repoName, { ...catalog[repoName], gh: ghSlug });
|
|
1255
1396
|
try {
|
|
1256
1397
|
// runSetup=false: clone only — setup is run below, hidden + non-interactive.
|
|
1257
1398
|
ensureManagedServiceClone(repoName, ghSlug, branch, false);
|
|
@@ -1488,6 +1629,8 @@ class DenCLI {
|
|
|
1488
1629
|
* in order, and run nothing. Read-only on every path.
|
|
1489
1630
|
*/
|
|
1490
1631
|
dryRun?: boolean;
|
|
1632
|
+
/** `--plan`: a preview that would run nothing exits 1. */
|
|
1633
|
+
plan?: boolean;
|
|
1491
1634
|
},
|
|
1492
1635
|
): Promise<void> {
|
|
1493
1636
|
// How long to show startup output before handing control back to the menu
|
|
@@ -1557,7 +1700,15 @@ class DenCLI {
|
|
|
1557
1700
|
// The same lookup a real run makes, so the dry run says what it does.
|
|
1558
1701
|
const previews = ordered.map((a): ServiceCommandPreview => {
|
|
1559
1702
|
const remote = remoteRefusal(a);
|
|
1560
|
-
if (!remote)
|
|
1703
|
+
if (!remote) {
|
|
1704
|
+
const p = previewServiceCommand(a, command, workspace);
|
|
1705
|
+
// What up does first about a pre-dev.37 record or a held port.
|
|
1706
|
+
return (
|
|
1707
|
+
(command === "up" && p.kind === "run"
|
|
1708
|
+
? previewUpSettle(a, workspace)
|
|
1709
|
+
: null) ?? p
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1561
1712
|
// As the loop below: a named remote target fails, any other is
|
|
1562
1713
|
// said and skipped.
|
|
1563
1714
|
return isNamedTarget(a, opts?.named ?? [])
|
|
@@ -1580,7 +1731,7 @@ class DenCLI {
|
|
|
1580
1731
|
),
|
|
1581
1732
|
);
|
|
1582
1733
|
for (const p of previews) {
|
|
1583
|
-
if (p.kind
|
|
1734
|
+
if (p.kind !== "error" && p.note)
|
|
1584
1735
|
console.log(chalk.gray(` ${p.note}`));
|
|
1585
1736
|
if (p.kind === "error") console.log(chalk.red(` ✗ ${p.note}`));
|
|
1586
1737
|
}
|
|
@@ -1599,6 +1750,7 @@ class DenCLI {
|
|
|
1599
1750
|
);
|
|
1600
1751
|
}
|
|
1601
1752
|
if (previews.some((p) => p.kind === "error")) process.exitCode = 1;
|
|
1753
|
+
if (opts?.plan && runs.length === 0) process.exitCode = 1;
|
|
1602
1754
|
console.log(chalk.gray(` (--dry-run: not ${doing} anything)\n`));
|
|
1603
1755
|
return;
|
|
1604
1756
|
}
|
|
@@ -1657,12 +1809,27 @@ class DenCLI {
|
|
|
1657
1809
|
const resolved = resolveService(address, workspace);
|
|
1658
1810
|
if (resolved) {
|
|
1659
1811
|
const health = await checkServiceHealth(resolved, "local");
|
|
1660
|
-
|
|
1812
|
+
// `conflict`: a shared stack namespace runs another checkout's
|
|
1813
|
+
// project; the real up below refuses it, naming both.
|
|
1814
|
+
const claim =
|
|
1815
|
+
health.health === "ok" && workspace
|
|
1816
|
+
? claimHealthyService(resolved, workspace, printNotice)
|
|
1817
|
+
: "untracked";
|
|
1818
|
+
// Healthy, but another process answers on its port: said (above),
|
|
1819
|
+
// never claimed, and nothing is started on top of it.
|
|
1820
|
+
if (health.health === "ok" && claim === "busy") {
|
|
1821
|
+
console.log(
|
|
1822
|
+
chalk.red(
|
|
1823
|
+
` ✗ ${chalk.bold(address)} not started: its port is in use by a process ${product().cli} won't adopt`,
|
|
1824
|
+
),
|
|
1825
|
+
);
|
|
1826
|
+
// A dependency only warns; the service you named failed.
|
|
1827
|
+
if (isNamedTarget(address, opts?.named ?? [])) process.exitCode = 1;
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (health.health === "ok" && claim !== "conflict") {
|
|
1661
1831
|
// Skipping must not lose the refcount — this workspace is an owner
|
|
1662
1832
|
// of what's running, whether den tracked it or not.
|
|
1663
|
-
const claim = workspace
|
|
1664
|
-
? claimHealthyService(resolved, workspace)
|
|
1665
|
-
: "untracked";
|
|
1666
1833
|
const suffix =
|
|
1667
1834
|
claim === "adopted"
|
|
1668
1835
|
? " (adopted its running containers)"
|
|
@@ -1763,6 +1930,8 @@ class DenCLI {
|
|
|
1763
1930
|
let result: ReturnType<typeof runServiceCommand> | null = null;
|
|
1764
1931
|
let detachable = false;
|
|
1765
1932
|
let interactive = false;
|
|
1933
|
+
// A down that found nothing to stop is said as such, never as ✔.
|
|
1934
|
+
let downOutcome: DownOutcome | null = null;
|
|
1766
1935
|
|
|
1767
1936
|
const settleFromClose = (exitCode: number): void => {
|
|
1768
1937
|
if (settled) return;
|
|
@@ -1779,6 +1948,17 @@ class DenCLI {
|
|
|
1779
1948
|
),
|
|
1780
1949
|
);
|
|
1781
1950
|
}
|
|
1951
|
+
} else if (exitCode === 0 && downOutcome === "nothing-recorded") {
|
|
1952
|
+
spinner.info(
|
|
1953
|
+
`${command} ${chalk.bold(address)} ${chalk.gray("(nothing to stop)")}`,
|
|
1954
|
+
);
|
|
1955
|
+
} else if (exitCode === 0 && downOutcome === "unstoppable") {
|
|
1956
|
+
spinner.warn(
|
|
1957
|
+
`${command} ${chalk.bold(address)} ${chalk.yellow("(could not stop it)")}`,
|
|
1958
|
+
);
|
|
1959
|
+
for (const l of lastLines) {
|
|
1960
|
+
console.log(chalk.gray(` ${l}`));
|
|
1961
|
+
}
|
|
1782
1962
|
} else if (exitCode === 0) {
|
|
1783
1963
|
spinner.succeed(`${command} ${chalk.bold(address)}`);
|
|
1784
1964
|
} else {
|
|
@@ -1832,8 +2012,21 @@ class DenCLI {
|
|
|
1832
2012
|
runtime,
|
|
1833
2013
|
workspace,
|
|
1834
2014
|
force: command === "down" ? force : false,
|
|
2015
|
+
// Down fails when it cannot stop a service the user named; one
|
|
2016
|
+
// reached as part of a workspace-wide down warns instead.
|
|
2017
|
+
explicit: isNamedTarget(address, opts?.named ?? []),
|
|
2018
|
+
onDownOutcome: (outcome) => {
|
|
2019
|
+
downOutcome = outcome;
|
|
2020
|
+
},
|
|
1835
2021
|
secretEnv: secrets.secretEnv,
|
|
1836
2022
|
onLine: (line) => {
|
|
2023
|
+
// What Den decided about a record or a port is said, not
|
|
2024
|
+
// only flashed in the spinner (0.3.0-dev.39).
|
|
2025
|
+
if (isNotice(line)) {
|
|
2026
|
+
spinner.clear();
|
|
2027
|
+
printNotice(line);
|
|
2028
|
+
if (spinner.isEnabled) spinner.render();
|
|
2029
|
+
}
|
|
1837
2030
|
spinner.text = `${command} ${chalk.bold(address)}: ${chalk.gray(line.slice(0, 80))}`;
|
|
1838
2031
|
const trimmed = line.trim();
|
|
1839
2032
|
if (trimmed) {
|
|
@@ -2042,6 +2235,7 @@ class DenCLI {
|
|
|
2042
2235
|
`\n ${chalk.bold(seed)} has no derived dependencies to start.\n`,
|
|
2043
2236
|
),
|
|
2044
2237
|
);
|
|
2238
|
+
if (plan) process.exitCode = 1;
|
|
2045
2239
|
return;
|
|
2046
2240
|
}
|
|
2047
2241
|
// Systems the user toggled to auto-start OFF in this workspace are not
|
|
@@ -2088,11 +2282,15 @@ class DenCLI {
|
|
|
2088
2282
|
this.renderUpClonePlan(rawOrder);
|
|
2089
2283
|
return;
|
|
2090
2284
|
}
|
|
2285
|
+
// A repo with no up of its own starts its services, one with no up at
|
|
2286
|
+
// all is skipped: the decision `up -a` makes (resolveUpTargets).
|
|
2287
|
+
const targets = new Set(include ? [seed] : []);
|
|
2091
2288
|
if (dryRun) {
|
|
2289
|
+
const up = this.upTargets(order, targets, graph);
|
|
2092
2290
|
await this.runServicesCommand(
|
|
2093
|
-
order,
|
|
2291
|
+
up.order,
|
|
2094
2292
|
"up",
|
|
2095
|
-
{ order },
|
|
2293
|
+
{ order: up.order },
|
|
2096
2294
|
{ named, dryRun: true },
|
|
2097
2295
|
);
|
|
2098
2296
|
return;
|
|
@@ -2116,12 +2314,18 @@ class DenCLI {
|
|
|
2116
2314
|
);
|
|
2117
2315
|
return;
|
|
2118
2316
|
}
|
|
2317
|
+
const up = this.upTargets(order, targets, graph);
|
|
2318
|
+
if (up.order.length === 0) {
|
|
2319
|
+
console.log(chalk.yellow(" Nothing to start.\n"));
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2119
2322
|
await this.runServicesCommand(
|
|
2120
|
-
order,
|
|
2323
|
+
up.order,
|
|
2121
2324
|
"up",
|
|
2122
2325
|
{
|
|
2123
|
-
order,
|
|
2124
|
-
waitDepsFor: (address) =>
|
|
2326
|
+
order: up.order,
|
|
2327
|
+
waitDepsFor: (address) =>
|
|
2328
|
+
up.depsAs(hardDepsOf(graph, repoOfAddress(address))),
|
|
2125
2329
|
failFast: true,
|
|
2126
2330
|
skipHealthyAll: true,
|
|
2127
2331
|
},
|
|
@@ -2129,6 +2333,37 @@ class DenCLI {
|
|
|
2129
2333
|
);
|
|
2130
2334
|
}
|
|
2131
2335
|
|
|
2336
|
+
/**
|
|
2337
|
+
* The start order `up` runs (`up <repo>` and `up -a` alike): a repo with
|
|
2338
|
+
* no up of its own replaced by its services' ups, one with no up at all
|
|
2339
|
+
* skipped (resolveUpTargets), said as it decides.
|
|
2340
|
+
*/
|
|
2341
|
+
private upTargets(
|
|
2342
|
+
order: string[],
|
|
2343
|
+
targets: ReadonlySet<string>,
|
|
2344
|
+
graph: DerivedGraph,
|
|
2345
|
+
): UpTargets {
|
|
2346
|
+
const up = resolveUpTargets(order, this.focusedWs(), { targets, graph });
|
|
2347
|
+
this.sayUpTargets(up);
|
|
2348
|
+
return up;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/** What resolveUpTargets decided, in `up`'s words. */
|
|
2352
|
+
private sayUpTargets(up: UpTargets): void {
|
|
2353
|
+
for (const [repo, addrs] of up.expanded)
|
|
2354
|
+
console.log(
|
|
2355
|
+
chalk.gray(
|
|
2356
|
+
` ${repo} has no up of its own: starting ${addrs.join(", ")}`,
|
|
2357
|
+
),
|
|
2358
|
+
);
|
|
2359
|
+
if (up.skippedNoUp.length > 0)
|
|
2360
|
+
console.log(
|
|
2361
|
+
chalk.gray(
|
|
2362
|
+
` Skipping (no local up command): ${up.skippedNoUp.join(", ")}\n`,
|
|
2363
|
+
),
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2132
2367
|
/**
|
|
2133
2368
|
* Stop the seed repo and the dependencies it was started with — the same
|
|
2134
2369
|
* closure `den up <seed>` brings up, torn down in reverse (seed first,
|
|
@@ -2283,6 +2518,7 @@ class DenCLI {
|
|
|
2283
2518
|
"\n No services in the active workspace — nothing to start.\n",
|
|
2284
2519
|
),
|
|
2285
2520
|
);
|
|
2521
|
+
if (plan) process.exitCode = 1;
|
|
2286
2522
|
return;
|
|
2287
2523
|
}
|
|
2288
2524
|
const { rawOrder, graph } = target;
|
|
@@ -2345,19 +2581,18 @@ class DenCLI {
|
|
|
2345
2581
|
// is skipped, not fatal. `-a` starts independent peers — one unrunnable
|
|
2346
2582
|
// repo must not strand the rest (runServiceCommand exits 1 on a missing
|
|
2347
2583
|
// command, and a fail-fast would turn that into a full abort). Now that
|
|
2348
|
-
// clones exist, the manifest lookup is meaningful.
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
}
|
|
2584
|
+
// clones exist, the manifest lookup is meaningful. A repo with no up of
|
|
2585
|
+
// its own but per-service ups starts those services: the same decision
|
|
2586
|
+
// `up <repo>` makes for a dependency (upTargets).
|
|
2587
|
+
const up = resolveUpTargets(candidates, this.focusedWs(), {
|
|
2588
|
+
targets: new Set(candidates),
|
|
2589
|
+
graph,
|
|
2590
|
+
});
|
|
2591
|
+
const order = up.order;
|
|
2592
|
+
const systems = candidates.filter((r) => !up.skippedNoUp.includes(r));
|
|
2358
2593
|
console.log(
|
|
2359
2594
|
chalk.cyan(
|
|
2360
|
-
`\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(
|
|
2595
|
+
`\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(systems.length))} ${noun(systems.length, "system")}`,
|
|
2361
2596
|
),
|
|
2362
2597
|
);
|
|
2363
2598
|
console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
|
|
@@ -2369,13 +2604,7 @@ class DenCLI {
|
|
|
2369
2604
|
),
|
|
2370
2605
|
);
|
|
2371
2606
|
}
|
|
2372
|
-
|
|
2373
|
-
console.log(
|
|
2374
|
-
chalk.gray(
|
|
2375
|
-
` Skipping (no local up command): ${skippedNoUp.join(", ")}\n`,
|
|
2376
|
-
),
|
|
2377
|
-
);
|
|
2378
|
-
}
|
|
2607
|
+
this.sayUpTargets(up);
|
|
2379
2608
|
if (order.length === 0) {
|
|
2380
2609
|
console.log(chalk.yellow(" Nothing to start.\n"));
|
|
2381
2610
|
return;
|
|
@@ -2385,13 +2614,17 @@ class DenCLI {
|
|
|
2385
2614
|
// healthy-skip. Unlike runDerivedUp there is NO failFast on the start
|
|
2386
2615
|
// loop: `-a`'s systems are independent peers — one failure must not
|
|
2387
2616
|
// strand the rest (the web master button continues too).
|
|
2388
|
-
const repoSet = new Set(
|
|
2617
|
+
const repoSet = new Set(systems);
|
|
2389
2618
|
await this.runServicesCommand(order, "up", {
|
|
2390
2619
|
order,
|
|
2391
2620
|
// Hard deps outside the workspace can't be waited on — filter to the set
|
|
2392
2621
|
// we are actually starting.
|
|
2393
2622
|
waitDepsFor: (address) =>
|
|
2394
|
-
|
|
2623
|
+
up.depsAs(
|
|
2624
|
+
hardDepsOf(graph, repoOfAddress(address)).filter((dep) =>
|
|
2625
|
+
repoSet.has(dep),
|
|
2626
|
+
),
|
|
2627
|
+
),
|
|
2395
2628
|
skipHealthyAll: true,
|
|
2396
2629
|
});
|
|
2397
2630
|
}
|
|
@@ -2427,6 +2660,14 @@ class DenCLI {
|
|
|
2427
2660
|
return;
|
|
2428
2661
|
}
|
|
2429
2662
|
await this.runServicesCommand(order, "down", { order }, { force });
|
|
2663
|
+
// Records no workspace reaches: -a stops every workspace's services,
|
|
2664
|
+
// not these. Said, and left alone (0.3.0-dev.39, a consumer's finding).
|
|
2665
|
+
if (!only)
|
|
2666
|
+
for (const d of daemonsOutsideWorkspaces())
|
|
2667
|
+
printNotice(
|
|
2668
|
+
`Warning: ${d.address} is recorded as running (${d.repoPath}) but is in no active workspace: ` +
|
|
2669
|
+
`down -a left it alone. Stop it yourself if it still runs.`,
|
|
2670
|
+
);
|
|
2430
2671
|
}
|
|
2431
2672
|
|
|
2432
2673
|
private printUpHelp(): void {
|
|
@@ -2714,10 +2955,10 @@ class DenCLI {
|
|
|
2714
2955
|
}
|
|
2715
2956
|
|
|
2716
2957
|
private async cmdServiceUp(args: string[]): Promise<void> {
|
|
2717
|
-
await this.ensureWorkspaceSeeded();
|
|
2718
2958
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
2719
2959
|
// never be read as up's own flags (`den up -- -a` is NOT `up -a`).
|
|
2720
2960
|
const terminator = args.indexOf("--");
|
|
2961
|
+
await this.ensureWorkspaceSeeded();
|
|
2721
2962
|
const { app, rest } = takeAppFlag(
|
|
2722
2963
|
terminator === -1 ? args : args.slice(0, terminator),
|
|
2723
2964
|
);
|
|
@@ -2781,6 +3022,7 @@ class DenCLI {
|
|
|
2781
3022
|
await this.cmdServiceUpManifest(
|
|
2782
3023
|
ownArgs.filter((a) => !a.startsWith("-")),
|
|
2783
3024
|
dryRun || plan,
|
|
3025
|
+
plan,
|
|
2784
3026
|
);
|
|
2785
3027
|
}
|
|
2786
3028
|
|
|
@@ -2788,6 +3030,7 @@ class DenCLI {
|
|
|
2788
3030
|
private async cmdServiceUpManifest(
|
|
2789
3031
|
addresses: string[],
|
|
2790
3032
|
dryRun = false,
|
|
3033
|
+
plan = false,
|
|
2791
3034
|
): Promise<void> {
|
|
2792
3035
|
// Resolve targets BEFORE the secrets preflight — otherwise running
|
|
2793
3036
|
// `den up` from a non-service dir (e.g. the den repo root) triggers
|
|
@@ -2806,6 +3049,16 @@ class DenCLI {
|
|
|
2806
3049
|
` or run \`${product().cli} graph\` to see the workspace.\n`,
|
|
2807
3050
|
),
|
|
2808
3051
|
);
|
|
3052
|
+
} else if (process.env.DEN_WORKSPACE_OVERRIDE) {
|
|
3053
|
+
// `up -w <ws>` with nothing to start: -w picks the workspace, not
|
|
3054
|
+
// what in it to start. A usage error, not an empty plan.
|
|
3055
|
+
console.error(
|
|
3056
|
+
chalk.red(
|
|
3057
|
+
`\n ${upNeedsTarget(product().cli, process.env.DEN_WORKSPACE_OVERRIDE, plan)}\n`,
|
|
3058
|
+
),
|
|
3059
|
+
);
|
|
3060
|
+
process.exitCode = 2;
|
|
3061
|
+
return;
|
|
2809
3062
|
} else {
|
|
2810
3063
|
console.log(
|
|
2811
3064
|
chalk.yellow(
|
|
@@ -2813,12 +3066,15 @@ class DenCLI {
|
|
|
2813
3066
|
),
|
|
2814
3067
|
);
|
|
2815
3068
|
}
|
|
3069
|
+
// A plan that resolves nothing is a failed plan (scripts check it).
|
|
3070
|
+
if (plan) process.exitCode = 1;
|
|
2816
3071
|
return;
|
|
2817
3072
|
}
|
|
2818
3073
|
if (!dryRun) preflightSecrets();
|
|
2819
3074
|
await this.runServicesCommand(targets, "up", undefined, {
|
|
2820
3075
|
named: addresses,
|
|
2821
3076
|
dryRun,
|
|
3077
|
+
plan,
|
|
2822
3078
|
});
|
|
2823
3079
|
}
|
|
2824
3080
|
|
|
@@ -2935,15 +3191,49 @@ class DenCLI {
|
|
|
2935
3191
|
|
|
2936
3192
|
private async cmdServiceStatus(argv: string[] = []): Promise<void> {
|
|
2937
3193
|
await this.ensureWorkspaceSeeded();
|
|
2938
|
-
const { app, rest:
|
|
3194
|
+
const { app, rest: appRest } = takeAppFlag(argv);
|
|
3195
|
+
// The view scope (as the web UI's): every active workspace by default,
|
|
3196
|
+
// or `--scope <workspace>` for one. A view only: focus is unchanged.
|
|
3197
|
+
const scoped = takeScopeFlag(appRest);
|
|
3198
|
+
const args = scoped.rest;
|
|
3199
|
+
const scopeArg =
|
|
3200
|
+
scoped.scope === null
|
|
3201
|
+
? null
|
|
3202
|
+
: parseScope(scoped.scope, getActiveWorkspaces());
|
|
3203
|
+
if (scoped.error || (scopeArg && !scopeArg.ok)) {
|
|
3204
|
+
console.log(
|
|
3205
|
+
chalk.red(
|
|
3206
|
+
`\n ${scoped.error ?? (scopeArg && !scopeArg.ok ? scopeArg.error : "")}\n`,
|
|
3207
|
+
),
|
|
3208
|
+
);
|
|
3209
|
+
process.exit(1);
|
|
3210
|
+
}
|
|
3211
|
+
// `-w <workspace>` still means that workspace only (active or not).
|
|
3212
|
+
const named = process.env.DEN_WORKSPACE_OVERRIDE
|
|
3213
|
+
? getActiveWorkspace()
|
|
3214
|
+
: null;
|
|
3215
|
+
const view =
|
|
3216
|
+
named && !scopeArg
|
|
3217
|
+
? {
|
|
3218
|
+
scope: named.name,
|
|
3219
|
+
fellBack: false,
|
|
3220
|
+
workspaces: [named],
|
|
3221
|
+
focus: named.name,
|
|
3222
|
+
}
|
|
3223
|
+
: scopeView(scopeArg?.ok ? scopeArg.value : undefined);
|
|
2939
3224
|
const only = app ? focusedAppRepos(app) : null;
|
|
2940
3225
|
const verbose = args.includes("--verbose") || args.includes("--all");
|
|
2941
3226
|
const spinner = ora("Checking service health...").start();
|
|
2942
|
-
const raw = (
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
3227
|
+
const raw = (
|
|
3228
|
+
await checkCatalogHealth(
|
|
3229
|
+
undefined,
|
|
3230
|
+
"local",
|
|
3231
|
+
view.workspaces.filter((w) => w.config),
|
|
3232
|
+
)
|
|
3233
|
+
).filter((h) => !only || only.has(h.repoName));
|
|
3234
|
+
// One row per repo, rolled up from every checked service by the rule the
|
|
3235
|
+
// web UI's header and graph use: entry down → down, another checked
|
|
3236
|
+
// service down → degraded; unchecked services never count.
|
|
2947
3237
|
const rolled = verbose ? raw : rollupRepoHealth(raw);
|
|
2948
3238
|
// Order top-down like the graph: entry repo first (shop-web) … foundation
|
|
2949
3239
|
// last (infra-stack), subsystems grouped under their repo.
|
|
@@ -2961,10 +3251,7 @@ class DenCLI {
|
|
|
2961
3251
|
return;
|
|
2962
3252
|
}
|
|
2963
3253
|
|
|
2964
|
-
|
|
2965
|
-
console.log(
|
|
2966
|
-
chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
|
|
2967
|
-
);
|
|
3254
|
+
console.log(chalk.cyan(`\n ${scopeLine(view)}\n`));
|
|
2968
3255
|
|
|
2969
3256
|
const table = newTable({
|
|
2970
3257
|
head: ["Service", "Mode", "Branch", "Health", "Reason"],
|
|
@@ -2972,8 +3259,20 @@ class DenCLI {
|
|
|
2972
3259
|
});
|
|
2973
3260
|
|
|
2974
3261
|
for (const r of results) {
|
|
2975
|
-
const healthStr =
|
|
2976
|
-
|
|
3262
|
+
const healthStr = r.rollup
|
|
3263
|
+
? formatRollupLabel(r.rollup)
|
|
3264
|
+
: formatHealthLabel(r.health);
|
|
3265
|
+
const rolledWhy = r.rollup ? rollupReasonText(r.rollup) : "";
|
|
3266
|
+
const reasonStr =
|
|
3267
|
+
r.rollup && r.rollup.state !== "unknown"
|
|
3268
|
+
? chalk.gray(
|
|
3269
|
+
[r.rollup.state === "down" ? healthReasonText(r) : "", rolledWhy]
|
|
3270
|
+
.filter(Boolean)
|
|
3271
|
+
.join("; ")
|
|
3272
|
+
.slice(0, 60),
|
|
3273
|
+
)
|
|
3274
|
+
: formatHealthReason(r) ||
|
|
3275
|
+
(rolledWhy ? chalk.gray(rolledWhy.slice(0, 60)) : "");
|
|
2977
3276
|
const placed = placementSuffix({
|
|
2978
3277
|
runs: r.runs ?? "none",
|
|
2979
3278
|
target: r.target ?? null,
|
|
@@ -2981,6 +3280,7 @@ class DenCLI {
|
|
|
2981
3280
|
|
|
2982
3281
|
table.push([
|
|
2983
3282
|
this.labelWithAutostart(r.address, noAutostart) +
|
|
3283
|
+
chalk.gray(ownersSuffix(r.workspaces)) +
|
|
2984
3284
|
(placed ? chalk.cyan(` ${placed}`) : ""),
|
|
2985
3285
|
r.mode === "dev-managed"
|
|
2986
3286
|
? chalk.blue("dev-managed")
|
|
@@ -3002,7 +3302,7 @@ class DenCLI {
|
|
|
3002
3302
|
if (!verbose && raw.length !== results.length) {
|
|
3003
3303
|
console.log(
|
|
3004
3304
|
chalk.gray(
|
|
3005
|
-
` ▪
|
|
3305
|
+
` ▪ one row per repo, rolled up from its checked services — \`${product().cli} status --verbose\` for each`,
|
|
3006
3306
|
),
|
|
3007
3307
|
);
|
|
3008
3308
|
}
|
|
@@ -3173,15 +3473,32 @@ class DenCLI {
|
|
|
3173
3473
|
private async cmdRegister(args: string[]): Promise<void> {
|
|
3174
3474
|
if (args.includes("-h") || args.includes("--help")) {
|
|
3175
3475
|
console.log(
|
|
3176
|
-
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>]\n\n` +
|
|
3476
|
+
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>] [--org <id>]\n\n` +
|
|
3177
3477
|
" With a path: add that checkout to the catalog and the focused\n" +
|
|
3178
3478
|
" workspace as your own (dev-managed) copy, without prompts. The name\n" +
|
|
3179
3479
|
" defaults to the manifest's name, then the folder name; the GitHub\n" +
|
|
3180
|
-
" slug to the git remote
|
|
3181
|
-
"
|
|
3480
|
+
" slug to the git remote. -w writes that workspace (active or not)\n" +
|
|
3481
|
+
" and leaves focus alone.\n" +
|
|
3482
|
+
" Without a path: choose a local checkout or a managed clone interactively;\n" +
|
|
3483
|
+
" a managed clone comes from the workspace's org, or --org <id>.\n",
|
|
3182
3484
|
);
|
|
3183
3485
|
return;
|
|
3184
3486
|
}
|
|
3487
|
+
const repoPathArg = args.find(
|
|
3488
|
+
(a, i) =>
|
|
3489
|
+
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3490
|
+
);
|
|
3491
|
+
// Without a path, register prompts: with no terminal (stdin closed or
|
|
3492
|
+
// piped, where readline would throw ERR_USE_AFTER_CLOSE mid-prompt), say
|
|
3493
|
+
// how to name one. Usage error: exit 2.
|
|
3494
|
+
if (!repoPathArg && !process.stdin.isTTY) {
|
|
3495
|
+
process.stderr.write(
|
|
3496
|
+
`register needs a terminal (stdin is not a TTY); without one: ` +
|
|
3497
|
+
`${product().cli} register <path> [--name <repo>] [--gh <owner/repo>] [-w <workspace>]\n`,
|
|
3498
|
+
);
|
|
3499
|
+
process.exitCode = 2;
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3185
3502
|
const ws = getActiveWorkspace();
|
|
3186
3503
|
if (!ws) {
|
|
3187
3504
|
console.log(
|
|
@@ -3192,10 +3509,6 @@ class DenCLI {
|
|
|
3192
3509
|
return;
|
|
3193
3510
|
}
|
|
3194
3511
|
|
|
3195
|
-
const repoPathArg = args.find(
|
|
3196
|
-
(a, i) =>
|
|
3197
|
-
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3198
|
-
);
|
|
3199
3512
|
if (repoPathArg) {
|
|
3200
3513
|
const flag = (n: string) => {
|
|
3201
3514
|
const i = args.indexOf(n);
|
|
@@ -3214,7 +3527,157 @@ class DenCLI {
|
|
|
3214
3527
|
}
|
|
3215
3528
|
}
|
|
3216
3529
|
|
|
3217
|
-
/**
|
|
3530
|
+
/**
|
|
3531
|
+
* `relocate <repo> <path>`: a checkout moved on disk. Rewrites the path of
|
|
3532
|
+
* every dev-managed entry for <repo> (bare and `<repo>/<svc>` addresses) in
|
|
3533
|
+
* every workspace, or only the `-w` one. Managed entries are Den's own
|
|
3534
|
+
* clones and are left alone. Focus and the active set never change.
|
|
3535
|
+
*/
|
|
3536
|
+
private async cmdRelocate(args: string[]): Promise<void> {
|
|
3537
|
+
const usage = `${product().cli} relocate <repo> <path> [--dry-run] [--force] [-w <workspace>]`;
|
|
3538
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
3539
|
+
console.log(
|
|
3540
|
+
`\n ${usage}\n\n` +
|
|
3541
|
+
" A checkout moved: point every workspace's dev-managed entries for\n" +
|
|
3542
|
+
" <repo> at <path>. Refuses a missing path, one whose manifest\n" +
|
|
3543
|
+
" name or GitHub remote names a different repo, and one where\n" +
|
|
3544
|
+
" neither names this repo (--force accepts that last). --dry-run shows\n" +
|
|
3545
|
+
" the changes without writing. -w limits it to that workspace.\n" +
|
|
3546
|
+
" Focus and the active set never change.\n",
|
|
3547
|
+
);
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
3551
|
+
const force = args.includes("--force");
|
|
3552
|
+
const unknown = args.find(
|
|
3553
|
+
(a) => a.startsWith("-") && !["--dry-run", "-n", "--force"].includes(a),
|
|
3554
|
+
);
|
|
3555
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
3556
|
+
if (unknown || positional.length !== 2) {
|
|
3557
|
+
console.error(
|
|
3558
|
+
chalk.red(
|
|
3559
|
+
`\n ❌ ${unknown ? `Unknown flag: ${unknown}` : "Need a repo and a path"}.\n Usage: ${usage}\n`,
|
|
3560
|
+
),
|
|
3561
|
+
);
|
|
3562
|
+
process.exitCode = 1;
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
const [repoName, rawPath] = positional;
|
|
3566
|
+
const { existsSync, statSync } = await import("fs");
|
|
3567
|
+
const newPath = nodePath.resolve(rawPath);
|
|
3568
|
+
const fail = (msg: string) => {
|
|
3569
|
+
console.error(chalk.red(`\n ❌ ${msg}\n`));
|
|
3570
|
+
process.exitCode = 1;
|
|
3571
|
+
};
|
|
3572
|
+
const file = readWorkspacesFile();
|
|
3573
|
+
const only = process.env.DEN_WORKSPACE_OVERRIDE;
|
|
3574
|
+
const names = only ? [only] : Object.keys(file.workspaces);
|
|
3575
|
+
const belongs = (key: string) =>
|
|
3576
|
+
key === repoName || key.startsWith(`${repoName}/`);
|
|
3577
|
+
if (
|
|
3578
|
+
!names.some((ws) =>
|
|
3579
|
+
Object.keys(file.workspaces[ws]?.services ?? {}).some(belongs),
|
|
3580
|
+
)
|
|
3581
|
+
) {
|
|
3582
|
+
fail(
|
|
3583
|
+
`"${repoName}" is not in ${only ? `workspace "${only}"` : "any workspace"}.`,
|
|
3584
|
+
);
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
if (!existsSync(newPath) || !statSync(newPath).isDirectory()) {
|
|
3588
|
+
fail(`Path does not exist: ${newPath}`);
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// The new path must hold this repo: its manifest name and GitHub remote,
|
|
3593
|
+
// when present, must agree with the repo and its catalog entry.
|
|
3594
|
+
const manifest = readManifest(newPath);
|
|
3595
|
+
const leaf = repoName.split("/").pop() ?? repoName;
|
|
3596
|
+
if (
|
|
3597
|
+
manifest?.name &&
|
|
3598
|
+
manifest.name !== repoName &&
|
|
3599
|
+
manifest.name !== leaf
|
|
3600
|
+
) {
|
|
3601
|
+
fail(
|
|
3602
|
+
`${newPath} holds "${manifest.name}", not "${repoName}" (its manifest name).`,
|
|
3603
|
+
);
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
const catalogGh = readCatalog()[repoName]?.gh?.trim();
|
|
3607
|
+
const coords = getGitHubCoords(newPath);
|
|
3608
|
+
const remote = coords ? `${coords.owner}/${coords.repo}` : undefined;
|
|
3609
|
+
if (
|
|
3610
|
+
catalogGh &&
|
|
3611
|
+
remote &&
|
|
3612
|
+
catalogGh.toLowerCase() !== remote.toLowerCase()
|
|
3613
|
+
) {
|
|
3614
|
+
fail(
|
|
3615
|
+
`${newPath} is a checkout of ${remote}; the catalog has "${repoName}" as ${catalogGh}.`,
|
|
3616
|
+
);
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
3619
|
+
// Not contradicting is not enough: an empty or unrelated folder has no
|
|
3620
|
+
// manifest and no remote. Something must say this is the repo.
|
|
3621
|
+
const identified =
|
|
3622
|
+
manifest?.name === repoName ||
|
|
3623
|
+
manifest?.name === leaf ||
|
|
3624
|
+
(!!catalogGh &&
|
|
3625
|
+
!!remote &&
|
|
3626
|
+
catalogGh.toLowerCase() === remote.toLowerCase()) ||
|
|
3627
|
+
(!catalogGh && coords?.repo.toLowerCase() === leaf.toLowerCase());
|
|
3628
|
+
if (!identified && !force) {
|
|
3629
|
+
fail(
|
|
3630
|
+
`Nothing at ${newPath} says it is "${repoName}": no manifest named ` +
|
|
3631
|
+
`"${leaf}" and no GitHub remote ${catalogGh || `…/${leaf}`}.\n` +
|
|
3632
|
+
` If it is, say so: ${product().cli} relocate ${repoName} ${rawPath} --force${dryRun ? " --dry-run" : ""}${only ? ` -w ${only}` : ""}`,
|
|
3633
|
+
);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
const changes: { ws: string; addr: string; from: string }[] = [];
|
|
3638
|
+
const managed: { ws: string; addr: string }[] = [];
|
|
3639
|
+
for (const ws of names) {
|
|
3640
|
+
const entries = file.workspaces[ws]?.services ?? {};
|
|
3641
|
+
for (const [addr, entry] of Object.entries(entries)) {
|
|
3642
|
+
if (!belongs(addr)) continue;
|
|
3643
|
+
if (entry.mode !== "dev-managed") {
|
|
3644
|
+
managed.push({ ws, addr });
|
|
3645
|
+
continue;
|
|
3646
|
+
}
|
|
3647
|
+
if (entry.path && nodePath.resolve(entry.path) === newPath) continue;
|
|
3648
|
+
changes.push({ ws, addr, from: entry.path ?? "(default)" });
|
|
3649
|
+
if (!dryRun) entry.path = newPath;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (changes.length > 0 && !dryRun) writeWorkspacesFile(file);
|
|
3654
|
+
|
|
3655
|
+
const verb = dryRun ? "Would point" : "Pointed";
|
|
3656
|
+
if (changes.length === 0) {
|
|
3657
|
+
console.log(
|
|
3658
|
+
chalk.gray(
|
|
3659
|
+
`\n Nothing to change: "${repoName}" is already at ${newPath}.`,
|
|
3660
|
+
),
|
|
3661
|
+
);
|
|
3662
|
+
} else {
|
|
3663
|
+
console.log(chalk.green(`\n ✓ ${verb} at ${newPath}:\n`));
|
|
3664
|
+
for (const c of changes) {
|
|
3665
|
+
console.log(
|
|
3666
|
+
` ${chalk.bold(c.addr)} ${chalk.gray(`${c.ws} was ${c.from}`)}`,
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
for (const m of managed) {
|
|
3671
|
+
console.log(
|
|
3672
|
+
chalk.gray(
|
|
3673
|
+
` ${m.addr} ${m.ws} managed (Den's clone): left alone`,
|
|
3674
|
+
),
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
console.log();
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/** Non-interactive `register <path>`: a dev-managed entry in the focused (or -w) workspace. */
|
|
3218
3681
|
private async registerPath(
|
|
3219
3682
|
ws: {
|
|
3220
3683
|
name: string;
|
|
@@ -3306,7 +3769,8 @@ class DenCLI {
|
|
|
3306
3769
|
console.log(
|
|
3307
3770
|
chalk.red(
|
|
3308
3771
|
`\n ❌ No GitHub org to list: set github_org in ${product().configFile}, ` +
|
|
3309
|
-
`org: on workspace ${ws.name}, or pass --org <id
|
|
3772
|
+
`org: on workspace ${ws.name}, or pass --org <id> ` +
|
|
3773
|
+
`(an org with no GitHub source in orgs.<id>.sources has none)\n`,
|
|
3310
3774
|
),
|
|
3311
3775
|
);
|
|
3312
3776
|
return;
|
|
@@ -3559,18 +4023,38 @@ class DenCLI {
|
|
|
3559
4023
|
console.log(chalk.gray(` revert: ${revert}\n`));
|
|
3560
4024
|
}
|
|
3561
4025
|
|
|
3562
|
-
private async cmdSetup(
|
|
4026
|
+
private async cmdSetup(rawArgs: string[]): Promise<void> {
|
|
4027
|
+
// `-w <ws>` (repeatable) and `--all-active` pick the workspaces to plan.
|
|
4028
|
+
const { workspaces: planWs, rest: args } = takeWorkspaceFlags(rawArgs);
|
|
4029
|
+
const allActive = args.includes("--all-active");
|
|
3563
4030
|
const force = args.includes("--force") || args.includes("-f");
|
|
3564
4031
|
const names = args.filter((a) => !a.startsWith("-"));
|
|
3565
4032
|
|
|
3566
4033
|
// `--plan`: each repo's org and clone URL, then stop. Read-only.
|
|
3567
4034
|
if (args.includes("--plan")) {
|
|
4035
|
+
let plan: ReturnType<typeof buildSetupPlan>;
|
|
4036
|
+
try {
|
|
4037
|
+
plan = buildSetupPlan(names, { workspaces: planWs, allActive });
|
|
4038
|
+
} catch (e) {
|
|
4039
|
+
console.error(chalk.red(`\n ${(e as Error).message}\n`));
|
|
4040
|
+
process.exitCode = 1;
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
3568
4043
|
console.log(chalk.bold("\n Setup plan (no changes will be made):\n"));
|
|
3569
|
-
for (const line of renderClonePlan(
|
|
4044
|
+
for (const line of renderClonePlan(plan, product().cli))
|
|
3570
4045
|
console.log(` ${line}`.trimEnd());
|
|
3571
4046
|
console.log();
|
|
3572
4047
|
return;
|
|
3573
4048
|
}
|
|
4049
|
+
if (planWs.length || allActive) {
|
|
4050
|
+
console.error(
|
|
4051
|
+
chalk.red(
|
|
4052
|
+
`\n -w and --all-active go with --plan: setup itself works on the focused workspace.\n`,
|
|
4053
|
+
),
|
|
4054
|
+
);
|
|
4055
|
+
process.exitCode = 1;
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
3574
4058
|
|
|
3575
4059
|
// Clone any managed repos that aren't on disk yet before reading manifests.
|
|
3576
4060
|
this.cloneMissingManagedRepos();
|
|
@@ -3662,14 +4146,32 @@ class DenCLI {
|
|
|
3662
4146
|
const only = orgFlag(args.slice(1));
|
|
3663
4147
|
const { resolved, warnings } = orgListRows();
|
|
3664
4148
|
const orgOfWs = new Map(resolved.map((w) => [w.workspace, w.org]));
|
|
3665
|
-
const table = newTable({
|
|
3666
|
-
head: ["Workspace", "Org", "Active", "Focus", "Services", "Type"],
|
|
3667
|
-
style: { head: ["cyan"] },
|
|
3668
|
-
});
|
|
3669
4149
|
const shown = Object.entries(file.workspaces).filter(
|
|
3670
4150
|
([name]) => !only || sameOrg(orgOfWs.get(name) ?? null, only),
|
|
3671
4151
|
);
|
|
4152
|
+
// A Stacks column only when a shown workspace's stack namespace is
|
|
4153
|
+
// not its default suffix (or is invalid): the list is otherwise as before.
|
|
4154
|
+
const nsRows = new Map(
|
|
4155
|
+
stackNamespaceRows(shown.map(([n]) => n)).map((r) => [
|
|
4156
|
+
r.workspace,
|
|
4157
|
+
r,
|
|
4158
|
+
]),
|
|
4159
|
+
);
|
|
4160
|
+
const nsCol = [...nsRows.values()].some((r) => r.differs || r.error);
|
|
4161
|
+
const table = newTable({
|
|
4162
|
+
head: [
|
|
4163
|
+
"Workspace",
|
|
4164
|
+
"Org",
|
|
4165
|
+
"Active",
|
|
4166
|
+
"Focus",
|
|
4167
|
+
"Services",
|
|
4168
|
+
"Type",
|
|
4169
|
+
...(nsCol ? ["Stacks"] : []),
|
|
4170
|
+
],
|
|
4171
|
+
style: { head: ["cyan"] },
|
|
4172
|
+
});
|
|
3672
4173
|
for (const [name, cfg] of shown) {
|
|
4174
|
+
const ns = nsRows.get(name);
|
|
3673
4175
|
table.push([
|
|
3674
4176
|
chalk.bold(name),
|
|
3675
4177
|
orgOfWs.get(name) ?? chalk.gray("none"),
|
|
@@ -3677,9 +4179,23 @@ class DenCLI {
|
|
|
3677
4179
|
name === focus ? chalk.cyan("●") : "",
|
|
3678
4180
|
Object.keys(cfg.services).join(", ") || chalk.gray("none"),
|
|
3679
4181
|
cfg.directory ? chalk.magenta("feature") : chalk.blue("default"),
|
|
4182
|
+
...(nsCol
|
|
4183
|
+
? [
|
|
4184
|
+
ns?.error
|
|
4185
|
+
? chalk.red("invalid")
|
|
4186
|
+
: ns?.differs
|
|
4187
|
+
? `${ns.stack_namespace} ${chalk.gray(`(${ns.source})`)}`
|
|
4188
|
+
: chalk.gray(ns?.default ?? ""),
|
|
4189
|
+
]
|
|
4190
|
+
: []),
|
|
3680
4191
|
]);
|
|
3681
4192
|
}
|
|
3682
4193
|
console.log(table.toString());
|
|
4194
|
+
printOrgWarnings(
|
|
4195
|
+
[...nsRows.values()].flatMap((r) =>
|
|
4196
|
+
r.error ? [`workspace ${r.workspace}: ${r.error}`] : r.warnings,
|
|
4197
|
+
),
|
|
4198
|
+
);
|
|
3683
4199
|
if (only && shown.length === 0)
|
|
3684
4200
|
console.log(chalk.yellow(` No workspace in org ${only}.`));
|
|
3685
4201
|
const names = new Set(shown.map(([n]) => n));
|
|
@@ -3986,6 +4502,58 @@ class DenCLI {
|
|
|
3986
4502
|
// ── den validate ────────────────────────────────────────────────────────
|
|
3987
4503
|
|
|
3988
4504
|
private async cmdValidate(args: string[]): Promise<void> {
|
|
4505
|
+
// The org config first (0.3.0-dev.40, a consumer's 0.2.8 nit: a
|
|
4506
|
+
// ConfigError went unreported): each problem with its file and key
|
|
4507
|
+
// path, a non-zero exit, and the manifests are still checked, reading
|
|
4508
|
+
// the config without its org data.
|
|
4509
|
+
try {
|
|
4510
|
+
assertDenConfigValid();
|
|
4511
|
+
} catch (e) {
|
|
4512
|
+
if (!isConfigError(e)) throw e;
|
|
4513
|
+
console.error(chalk.red(`\n ✗ ${product().configFile} is invalid:`));
|
|
4514
|
+
const issues = e.issues.length
|
|
4515
|
+
? e.issues
|
|
4516
|
+
: [{ file: product().configFile, key: "", message: e.message }];
|
|
4517
|
+
for (const i of issues)
|
|
4518
|
+
console.error(
|
|
4519
|
+
chalk.red(` ✗ ${i.file} ${chalk.bold(i.key)} ${i.message}`),
|
|
4520
|
+
);
|
|
4521
|
+
process.exitCode = 1;
|
|
4522
|
+
waiveInvalidOrgs();
|
|
4523
|
+
}
|
|
4524
|
+
// The distribution's secrets config, once: a 1password backend with no
|
|
4525
|
+
// refs leaves the vault-backed features off (no built-in vault/item).
|
|
4526
|
+
try {
|
|
4527
|
+
const { secretsConfigWarnings } =
|
|
4528
|
+
await import("../../shared/secrets/index.js");
|
|
4529
|
+
for (const w of secretsConfigWarnings())
|
|
4530
|
+
console.warn(chalk.yellow(`\n ⚠ ${w.replace(/\n/g, "\n ")}`));
|
|
4531
|
+
} catch {
|
|
4532
|
+
/* an unreadable config is reported elsewhere */
|
|
4533
|
+
}
|
|
4534
|
+
// The edge's TLS strategy, when there is an edge: its config, the
|
|
4535
|
+
// implicit-mkcert warning (once), and detect() (mkcert on PATH).
|
|
4536
|
+
{
|
|
4537
|
+
const { hasEdgeBlock, readEdgeConfig } =
|
|
4538
|
+
await import("../../shared/edge/config.js");
|
|
4539
|
+
const { edgeTlsWarnings, tlsStrategyFor, warnImplicitTlsOnce } =
|
|
4540
|
+
await import("../../shared/edge/tls.js");
|
|
4541
|
+
let edge: ReturnType<typeof readEdgeConfig> | null = null;
|
|
4542
|
+
try {
|
|
4543
|
+
if (hasEdgeBlock()) edge = readEdgeConfig();
|
|
4544
|
+
} catch (e) {
|
|
4545
|
+
console.error(chalk.red(`\n ✗ edge: ${(e as Error).message}`));
|
|
4546
|
+
process.exitCode = 1;
|
|
4547
|
+
}
|
|
4548
|
+
if (edge) {
|
|
4549
|
+
const warn = (w: string) => console.warn(chalk.yellow(`\n ⚠ ${w}`));
|
|
4550
|
+
warnImplicitTlsOnce(edge, warn);
|
|
4551
|
+
for (const w of edgeTlsWarnings(edge))
|
|
4552
|
+
if (edge.tlsFrom !== "implicit") warn(w);
|
|
4553
|
+
const d = tlsStrategyFor(edge).detect();
|
|
4554
|
+
if (!d.available) warn(`edge.tls ${edge.tls.strategy}: ${d.reason}`);
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
3989
4557
|
const positional = args.filter((a) => !a.startsWith("-"));
|
|
3990
4558
|
const held = args.includes("--held");
|
|
3991
4559
|
// Bare `validate` in the distribution root (which has no manifest of its
|
|
@@ -4190,7 +4758,7 @@ class DenCLI {
|
|
|
4190
4758
|
" restart [service...] Restart services (-A <app> for a whole application)",
|
|
4191
4759
|
);
|
|
4192
4760
|
console.log(
|
|
4193
|
-
" status Show service health for
|
|
4761
|
+
" status Show service health for the view scope (-A <app>, --scope <ws>)",
|
|
4194
4762
|
);
|
|
4195
4763
|
console.log(
|
|
4196
4764
|
" apps Org > Application > Repo > Services (--json)",
|
|
@@ -4211,6 +4779,9 @@ class DenCLI {
|
|
|
4211
4779
|
console.log(
|
|
4212
4780
|
" register [path] Register a repo in the service catalog",
|
|
4213
4781
|
);
|
|
4782
|
+
console.log(
|
|
4783
|
+
" relocate <repo> <path> Point a moved checkout's workspace entries at its new path",
|
|
4784
|
+
);
|
|
4214
4785
|
console.log();
|
|
4215
4786
|
console.log(
|
|
4216
4787
|
" setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
|
|
@@ -4228,6 +4799,12 @@ class DenCLI {
|
|
|
4228
4799
|
console.log(
|
|
4229
4800
|
" service install Run the web UI as a launchd service (status, print, uninstall)",
|
|
4230
4801
|
);
|
|
4802
|
+
console.log(
|
|
4803
|
+
" theme add <repo>@<ref> Install a web UI theme (list, validate, remove)",
|
|
4804
|
+
);
|
|
4805
|
+
console.log(
|
|
4806
|
+
" plugin list Local and added plugins, checked (validate, add, update, remove)",
|
|
4807
|
+
);
|
|
4231
4808
|
console.log();
|
|
4232
4809
|
console.log(
|
|
4233
4810
|
" workspace list List workspaces (active set + focus)",
|