@monster-orange/den 0.2.8 → 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 +1547 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +507 -4
- 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/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 +141 -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/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/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/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 +12 -3
- 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 +7 -2
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +548 -60
- package/src/ui/cli/notice.ts +20 -0
- 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/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +22 -1
- package/src/ui/mcp/tools.ts +50 -7
- package/src/ui/server/appearance-prefs.ts +95 -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 +532 -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 +12 -1
- 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 +45 -3
- 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,11 @@ 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";
|
|
176
196
|
import { cmdService } from "./service-command.js";
|
|
177
197
|
import { cmdStack } from "./stack-command.js";
|
|
198
|
+
import { cmdTheme } from "./theme-command.js";
|
|
199
|
+
import { cmdPlugin } from "./plugin-command.js";
|
|
178
200
|
import {
|
|
179
201
|
cmdOrg,
|
|
180
202
|
orgFlag,
|
|
@@ -209,6 +231,9 @@ import {
|
|
|
209
231
|
formatHealthDot,
|
|
210
232
|
formatHealthLabel,
|
|
211
233
|
formatHealthReason,
|
|
234
|
+
formatRollupLabel,
|
|
235
|
+
healthReasonText,
|
|
236
|
+
rollupReasonText,
|
|
212
237
|
} from "./health-display.js";
|
|
213
238
|
import { DEN_BANNER_LINES, DEN_DIVIDER } from "../../shared/banner.js";
|
|
214
239
|
|
|
@@ -300,9 +325,12 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
300
325
|
],
|
|
301
326
|
},
|
|
302
327
|
status: {
|
|
303
|
-
usage: "status [-A <app>]",
|
|
328
|
+
usage: "status [-A <app>] [--scope <all|workspace>] [--verbose]",
|
|
304
329
|
lines: [
|
|
305
|
-
"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.",
|
|
306
334
|
],
|
|
307
335
|
},
|
|
308
336
|
logs: {
|
|
@@ -319,7 +347,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
319
347
|
lines: ["List all registered services in the catalog."],
|
|
320
348
|
},
|
|
321
349
|
register: {
|
|
322
|
-
usage: "register [path] [
|
|
350
|
+
usage: "register [path] [-w <workspace>] [--org <id>]",
|
|
323
351
|
lines: [
|
|
324
352
|
"With a path: add that checkout (dev-managed) under its manifest name.",
|
|
325
353
|
"Without one: pick the repo interactively. A managed clone is picked",
|
|
@@ -327,6 +355,15 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
327
355
|
"-w writes that workspace (active or not) and leaves focus alone.",
|
|
328
356
|
],
|
|
329
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.",
|
|
365
|
+
],
|
|
366
|
+
},
|
|
330
367
|
setup: {
|
|
331
368
|
usage:
|
|
332
369
|
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
@@ -417,7 +454,33 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
417
454
|
edge: {
|
|
418
455
|
usage: "edge [up|down|status|trust|untrust|routes]",
|
|
419
456
|
lines: [
|
|
420
|
-
"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.",
|
|
421
484
|
],
|
|
422
485
|
},
|
|
423
486
|
mcp: {
|
|
@@ -438,7 +501,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
438
501
|
"local-dev": {
|
|
439
502
|
usage: "local-dev [on|off]",
|
|
440
503
|
lines: [
|
|
441
|
-
"Toggle the local-dev experience (web
|
|
504
|
+
"Toggle the local-dev experience (web Workspaces page + service mgmt).",
|
|
442
505
|
],
|
|
443
506
|
},
|
|
444
507
|
menu: {
|
|
@@ -466,6 +529,7 @@ function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
|
|
|
466
529
|
const TELEMETRY_COMMANDS = new Set<string>([
|
|
467
530
|
"run",
|
|
468
531
|
"view-results",
|
|
532
|
+
"down",
|
|
469
533
|
"menu",
|
|
470
534
|
"web",
|
|
471
535
|
"up",
|
|
@@ -476,6 +540,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
476
540
|
"services",
|
|
477
541
|
"graph",
|
|
478
542
|
"register",
|
|
543
|
+
"relocate",
|
|
479
544
|
"use-local",
|
|
480
545
|
`use-managed`,
|
|
481
546
|
"setup",
|
|
@@ -488,19 +553,24 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
488
553
|
"local-dev",
|
|
489
554
|
"config",
|
|
490
555
|
"edge",
|
|
556
|
+
"doctor",
|
|
491
557
|
"stack",
|
|
492
558
|
"apps",
|
|
493
559
|
"applications",
|
|
494
560
|
"mcp",
|
|
495
561
|
"service",
|
|
562
|
+
"theme",
|
|
563
|
+
"plugin",
|
|
496
564
|
]);
|
|
497
565
|
|
|
498
566
|
/**
|
|
499
567
|
* Commands that run with a config that fails validation: the web and MCP
|
|
500
568
|
* servers (each route or tool reports it), and those that read neither orgs,
|
|
501
569
|
* the catalog nor clones (manifest validate, create, the per-engineer
|
|
502
|
-
* config and local-dev toggles, saved results)
|
|
503
|
-
*
|
|
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.
|
|
504
574
|
*/
|
|
505
575
|
const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
506
576
|
"web",
|
|
@@ -510,6 +580,7 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
|
510
580
|
"config",
|
|
511
581
|
"local-dev",
|
|
512
582
|
"view-results",
|
|
583
|
+
"down",
|
|
513
584
|
]);
|
|
514
585
|
|
|
515
586
|
function configCheckedCommand(command: string): boolean {
|
|
@@ -743,6 +814,22 @@ class DenCLI {
|
|
|
743
814
|
// anything: some (sync over existing clones, register, workspace
|
|
744
815
|
// create) never read `orgs:` and used to carry on (ORG-1d).
|
|
745
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
|
+
}
|
|
746
833
|
|
|
747
834
|
switch (command) {
|
|
748
835
|
case "run":
|
|
@@ -798,6 +885,10 @@ class DenCLI {
|
|
|
798
885
|
await this.cmdRegister(args.slice(1));
|
|
799
886
|
break;
|
|
800
887
|
|
|
888
|
+
case "relocate":
|
|
889
|
+
await this.cmdRelocate(args.slice(1));
|
|
890
|
+
break;
|
|
891
|
+
|
|
801
892
|
case "use-local":
|
|
802
893
|
await this.cmdUseMode(args.slice(1), "dev-managed");
|
|
803
894
|
break;
|
|
@@ -872,6 +963,10 @@ class DenCLI {
|
|
|
872
963
|
await cmdEdge(args.slice(1));
|
|
873
964
|
break;
|
|
874
965
|
|
|
966
|
+
case "doctor":
|
|
967
|
+
process.exitCode = cmdDoctor(args.slice(1));
|
|
968
|
+
break;
|
|
969
|
+
|
|
875
970
|
case "service":
|
|
876
971
|
process.exitCode = await cmdService(args.slice(1));
|
|
877
972
|
break;
|
|
@@ -880,6 +975,14 @@ class DenCLI {
|
|
|
880
975
|
await cmdStack(args.slice(1));
|
|
881
976
|
break;
|
|
882
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
|
+
|
|
883
986
|
case "apps":
|
|
884
987
|
case "applications":
|
|
885
988
|
await this.ensureWorkspaceSeeded();
|
|
@@ -1597,7 +1700,15 @@ class DenCLI {
|
|
|
1597
1700
|
// The same lookup a real run makes, so the dry run says what it does.
|
|
1598
1701
|
const previews = ordered.map((a): ServiceCommandPreview => {
|
|
1599
1702
|
const remote = remoteRefusal(a);
|
|
1600
|
-
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
|
+
}
|
|
1601
1712
|
// As the loop below: a named remote target fails, any other is
|
|
1602
1713
|
// said and skipped.
|
|
1603
1714
|
return isNamedTarget(a, opts?.named ?? [])
|
|
@@ -1620,7 +1731,7 @@ class DenCLI {
|
|
|
1620
1731
|
),
|
|
1621
1732
|
);
|
|
1622
1733
|
for (const p of previews) {
|
|
1623
|
-
if (p.kind
|
|
1734
|
+
if (p.kind !== "error" && p.note)
|
|
1624
1735
|
console.log(chalk.gray(` ${p.note}`));
|
|
1625
1736
|
if (p.kind === "error") console.log(chalk.red(` ✗ ${p.note}`));
|
|
1626
1737
|
}
|
|
@@ -1698,12 +1809,27 @@ class DenCLI {
|
|
|
1698
1809
|
const resolved = resolveService(address, workspace);
|
|
1699
1810
|
if (resolved) {
|
|
1700
1811
|
const health = await checkServiceHealth(resolved, "local");
|
|
1701
|
-
|
|
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") {
|
|
1702
1831
|
// Skipping must not lose the refcount — this workspace is an owner
|
|
1703
1832
|
// of what's running, whether den tracked it or not.
|
|
1704
|
-
const claim = workspace
|
|
1705
|
-
? claimHealthyService(resolved, workspace)
|
|
1706
|
-
: "untracked";
|
|
1707
1833
|
const suffix =
|
|
1708
1834
|
claim === "adopted"
|
|
1709
1835
|
? " (adopted its running containers)"
|
|
@@ -1804,6 +1930,8 @@ class DenCLI {
|
|
|
1804
1930
|
let result: ReturnType<typeof runServiceCommand> | null = null;
|
|
1805
1931
|
let detachable = false;
|
|
1806
1932
|
let interactive = false;
|
|
1933
|
+
// A down that found nothing to stop is said as such, never as ✔.
|
|
1934
|
+
let downOutcome: DownOutcome | null = null;
|
|
1807
1935
|
|
|
1808
1936
|
const settleFromClose = (exitCode: number): void => {
|
|
1809
1937
|
if (settled) return;
|
|
@@ -1820,6 +1948,17 @@ class DenCLI {
|
|
|
1820
1948
|
),
|
|
1821
1949
|
);
|
|
1822
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
|
+
}
|
|
1823
1962
|
} else if (exitCode === 0) {
|
|
1824
1963
|
spinner.succeed(`${command} ${chalk.bold(address)}`);
|
|
1825
1964
|
} else {
|
|
@@ -1873,8 +2012,21 @@ class DenCLI {
|
|
|
1873
2012
|
runtime,
|
|
1874
2013
|
workspace,
|
|
1875
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
|
+
},
|
|
1876
2021
|
secretEnv: secrets.secretEnv,
|
|
1877
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
|
+
}
|
|
1878
2030
|
spinner.text = `${command} ${chalk.bold(address)}: ${chalk.gray(line.slice(0, 80))}`;
|
|
1879
2031
|
const trimmed = line.trim();
|
|
1880
2032
|
if (trimmed) {
|
|
@@ -2130,11 +2282,15 @@ class DenCLI {
|
|
|
2130
2282
|
this.renderUpClonePlan(rawOrder);
|
|
2131
2283
|
return;
|
|
2132
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] : []);
|
|
2133
2288
|
if (dryRun) {
|
|
2289
|
+
const up = this.upTargets(order, targets, graph);
|
|
2134
2290
|
await this.runServicesCommand(
|
|
2135
|
-
order,
|
|
2291
|
+
up.order,
|
|
2136
2292
|
"up",
|
|
2137
|
-
{ order },
|
|
2293
|
+
{ order: up.order },
|
|
2138
2294
|
{ named, dryRun: true },
|
|
2139
2295
|
);
|
|
2140
2296
|
return;
|
|
@@ -2158,12 +2314,18 @@ class DenCLI {
|
|
|
2158
2314
|
);
|
|
2159
2315
|
return;
|
|
2160
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
|
+
}
|
|
2161
2322
|
await this.runServicesCommand(
|
|
2162
|
-
order,
|
|
2323
|
+
up.order,
|
|
2163
2324
|
"up",
|
|
2164
2325
|
{
|
|
2165
|
-
order,
|
|
2166
|
-
waitDepsFor: (address) =>
|
|
2326
|
+
order: up.order,
|
|
2327
|
+
waitDepsFor: (address) =>
|
|
2328
|
+
up.depsAs(hardDepsOf(graph, repoOfAddress(address))),
|
|
2167
2329
|
failFast: true,
|
|
2168
2330
|
skipHealthyAll: true,
|
|
2169
2331
|
},
|
|
@@ -2171,6 +2333,37 @@ class DenCLI {
|
|
|
2171
2333
|
);
|
|
2172
2334
|
}
|
|
2173
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
|
+
|
|
2174
2367
|
/**
|
|
2175
2368
|
* Stop the seed repo and the dependencies it was started with — the same
|
|
2176
2369
|
* closure `den up <seed>` brings up, torn down in reverse (seed first,
|
|
@@ -2388,19 +2581,18 @@ class DenCLI {
|
|
|
2388
2581
|
// is skipped, not fatal. `-a` starts independent peers — one unrunnable
|
|
2389
2582
|
// repo must not strand the rest (runServiceCommand exits 1 on a missing
|
|
2390
2583
|
// 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
|
-
}
|
|
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));
|
|
2401
2593
|
console.log(
|
|
2402
2594
|
chalk.cyan(
|
|
2403
|
-
`\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")}`,
|
|
2404
2596
|
),
|
|
2405
2597
|
);
|
|
2406
2598
|
console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
|
|
@@ -2412,13 +2604,7 @@ class DenCLI {
|
|
|
2412
2604
|
),
|
|
2413
2605
|
);
|
|
2414
2606
|
}
|
|
2415
|
-
|
|
2416
|
-
console.log(
|
|
2417
|
-
chalk.gray(
|
|
2418
|
-
` Skipping (no local up command): ${skippedNoUp.join(", ")}\n`,
|
|
2419
|
-
),
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2607
|
+
this.sayUpTargets(up);
|
|
2422
2608
|
if (order.length === 0) {
|
|
2423
2609
|
console.log(chalk.yellow(" Nothing to start.\n"));
|
|
2424
2610
|
return;
|
|
@@ -2428,13 +2614,17 @@ class DenCLI {
|
|
|
2428
2614
|
// healthy-skip. Unlike runDerivedUp there is NO failFast on the start
|
|
2429
2615
|
// loop: `-a`'s systems are independent peers — one failure must not
|
|
2430
2616
|
// strand the rest (the web master button continues too).
|
|
2431
|
-
const repoSet = new Set(
|
|
2617
|
+
const repoSet = new Set(systems);
|
|
2432
2618
|
await this.runServicesCommand(order, "up", {
|
|
2433
2619
|
order,
|
|
2434
2620
|
// Hard deps outside the workspace can't be waited on — filter to the set
|
|
2435
2621
|
// we are actually starting.
|
|
2436
2622
|
waitDepsFor: (address) =>
|
|
2437
|
-
|
|
2623
|
+
up.depsAs(
|
|
2624
|
+
hardDepsOf(graph, repoOfAddress(address)).filter((dep) =>
|
|
2625
|
+
repoSet.has(dep),
|
|
2626
|
+
),
|
|
2627
|
+
),
|
|
2438
2628
|
skipHealthyAll: true,
|
|
2439
2629
|
});
|
|
2440
2630
|
}
|
|
@@ -2470,6 +2660,14 @@ class DenCLI {
|
|
|
2470
2660
|
return;
|
|
2471
2661
|
}
|
|
2472
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
|
+
);
|
|
2473
2671
|
}
|
|
2474
2672
|
|
|
2475
2673
|
private printUpHelp(): void {
|
|
@@ -2993,15 +3191,49 @@ class DenCLI {
|
|
|
2993
3191
|
|
|
2994
3192
|
private async cmdServiceStatus(argv: string[] = []): Promise<void> {
|
|
2995
3193
|
await this.ensureWorkspaceSeeded();
|
|
2996
|
-
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);
|
|
2997
3224
|
const only = app ? focusedAppRepos(app) : null;
|
|
2998
3225
|
const verbose = args.includes("--verbose") || args.includes("--all");
|
|
2999
3226
|
const spinner = ora("Checking service health...").start();
|
|
3000
|
-
const raw = (
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
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.
|
|
3005
3237
|
const rolled = verbose ? raw : rollupRepoHealth(raw);
|
|
3006
3238
|
// Order top-down like the graph: entry repo first (shop-web) … foundation
|
|
3007
3239
|
// last (infra-stack), subsystems grouped under their repo.
|
|
@@ -3019,10 +3251,7 @@ class DenCLI {
|
|
|
3019
3251
|
return;
|
|
3020
3252
|
}
|
|
3021
3253
|
|
|
3022
|
-
|
|
3023
|
-
console.log(
|
|
3024
|
-
chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
|
|
3025
|
-
);
|
|
3254
|
+
console.log(chalk.cyan(`\n ${scopeLine(view)}\n`));
|
|
3026
3255
|
|
|
3027
3256
|
const table = newTable({
|
|
3028
3257
|
head: ["Service", "Mode", "Branch", "Health", "Reason"],
|
|
@@ -3030,8 +3259,20 @@ class DenCLI {
|
|
|
3030
3259
|
});
|
|
3031
3260
|
|
|
3032
3261
|
for (const r of results) {
|
|
3033
|
-
const healthStr =
|
|
3034
|
-
|
|
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)) : "");
|
|
3035
3276
|
const placed = placementSuffix({
|
|
3036
3277
|
runs: r.runs ?? "none",
|
|
3037
3278
|
target: r.target ?? null,
|
|
@@ -3039,6 +3280,7 @@ class DenCLI {
|
|
|
3039
3280
|
|
|
3040
3281
|
table.push([
|
|
3041
3282
|
this.labelWithAutostart(r.address, noAutostart) +
|
|
3283
|
+
chalk.gray(ownersSuffix(r.workspaces)) +
|
|
3042
3284
|
(placed ? chalk.cyan(` ${placed}`) : ""),
|
|
3043
3285
|
r.mode === "dev-managed"
|
|
3044
3286
|
? chalk.blue("dev-managed")
|
|
@@ -3060,7 +3302,7 @@ class DenCLI {
|
|
|
3060
3302
|
if (!verbose && raw.length !== results.length) {
|
|
3061
3303
|
console.log(
|
|
3062
3304
|
chalk.gray(
|
|
3063
|
-
` ▪
|
|
3305
|
+
` ▪ one row per repo, rolled up from its checked services — \`${product().cli} status --verbose\` for each`,
|
|
3064
3306
|
),
|
|
3065
3307
|
);
|
|
3066
3308
|
}
|
|
@@ -3231,13 +3473,14 @@ class DenCLI {
|
|
|
3231
3473
|
private async cmdRegister(args: string[]): Promise<void> {
|
|
3232
3474
|
if (args.includes("-h") || args.includes("--help")) {
|
|
3233
3475
|
console.log(
|
|
3234
|
-
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>]\n\n` +
|
|
3476
|
+
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>] [--org <id>]\n\n` +
|
|
3235
3477
|
" With a path: add that checkout to the catalog and the focused\n" +
|
|
3236
3478
|
" workspace as your own (dev-managed) copy, without prompts. The name\n" +
|
|
3237
3479
|
" defaults to the manifest's name, then the folder name; the GitHub\n" +
|
|
3238
3480
|
" slug to the git remote. -w writes that workspace (active or not)\n" +
|
|
3239
3481
|
" and leaves focus alone.\n" +
|
|
3240
|
-
" Without a path: choose a local checkout or a managed clone interactively
|
|
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",
|
|
3241
3484
|
);
|
|
3242
3485
|
return;
|
|
3243
3486
|
}
|
|
@@ -3245,7 +3488,9 @@ class DenCLI {
|
|
|
3245
3488
|
(a, i) =>
|
|
3246
3489
|
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3247
3490
|
);
|
|
3248
|
-
// Without a path, register prompts: with no terminal
|
|
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.
|
|
3249
3494
|
if (!repoPathArg && !process.stdin.isTTY) {
|
|
3250
3495
|
process.stderr.write(
|
|
3251
3496
|
`register needs a terminal (stdin is not a TTY); without one: ` +
|
|
@@ -3282,7 +3527,157 @@ class DenCLI {
|
|
|
3282
3527
|
}
|
|
3283
3528
|
}
|
|
3284
3529
|
|
|
3285
|
-
/**
|
|
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. */
|
|
3286
3681
|
private async registerPath(
|
|
3287
3682
|
ws: {
|
|
3288
3683
|
name: string;
|
|
@@ -3751,14 +4146,32 @@ class DenCLI {
|
|
|
3751
4146
|
const only = orgFlag(args.slice(1));
|
|
3752
4147
|
const { resolved, warnings } = orgListRows();
|
|
3753
4148
|
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
4149
|
const shown = Object.entries(file.workspaces).filter(
|
|
3759
4150
|
([name]) => !only || sameOrg(orgOfWs.get(name) ?? null, only),
|
|
3760
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
|
+
});
|
|
3761
4173
|
for (const [name, cfg] of shown) {
|
|
4174
|
+
const ns = nsRows.get(name);
|
|
3762
4175
|
table.push([
|
|
3763
4176
|
chalk.bold(name),
|
|
3764
4177
|
orgOfWs.get(name) ?? chalk.gray("none"),
|
|
@@ -3766,9 +4179,23 @@ class DenCLI {
|
|
|
3766
4179
|
name === focus ? chalk.cyan("●") : "",
|
|
3767
4180
|
Object.keys(cfg.services).join(", ") || chalk.gray("none"),
|
|
3768
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
|
+
: []),
|
|
3769
4191
|
]);
|
|
3770
4192
|
}
|
|
3771
4193
|
console.log(table.toString());
|
|
4194
|
+
printOrgWarnings(
|
|
4195
|
+
[...nsRows.values()].flatMap((r) =>
|
|
4196
|
+
r.error ? [`workspace ${r.workspace}: ${r.error}`] : r.warnings,
|
|
4197
|
+
),
|
|
4198
|
+
);
|
|
3772
4199
|
if (only && shown.length === 0)
|
|
3773
4200
|
console.log(chalk.yellow(` No workspace in org ${only}.`));
|
|
3774
4201
|
const names = new Set(shown.map(([n]) => n));
|
|
@@ -4075,6 +4502,58 @@ class DenCLI {
|
|
|
4075
4502
|
// ── den validate ────────────────────────────────────────────────────────
|
|
4076
4503
|
|
|
4077
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
|
+
}
|
|
4078
4557
|
const positional = args.filter((a) => !a.startsWith("-"));
|
|
4079
4558
|
const held = args.includes("--held");
|
|
4080
4559
|
// Bare `validate` in the distribution root (which has no manifest of its
|
|
@@ -4279,7 +4758,7 @@ class DenCLI {
|
|
|
4279
4758
|
" restart [service...] Restart services (-A <app> for a whole application)",
|
|
4280
4759
|
);
|
|
4281
4760
|
console.log(
|
|
4282
|
-
" status Show service health for
|
|
4761
|
+
" status Show service health for the view scope (-A <app>, --scope <ws>)",
|
|
4283
4762
|
);
|
|
4284
4763
|
console.log(
|
|
4285
4764
|
" apps Org > Application > Repo > Services (--json)",
|
|
@@ -4300,6 +4779,9 @@ class DenCLI {
|
|
|
4300
4779
|
console.log(
|
|
4301
4780
|
" register [path] Register a repo in the service catalog",
|
|
4302
4781
|
);
|
|
4782
|
+
console.log(
|
|
4783
|
+
" relocate <repo> <path> Point a moved checkout's workspace entries at its new path",
|
|
4784
|
+
);
|
|
4303
4785
|
console.log();
|
|
4304
4786
|
console.log(
|
|
4305
4787
|
" setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
|
|
@@ -4317,6 +4799,12 @@ class DenCLI {
|
|
|
4317
4799
|
console.log(
|
|
4318
4800
|
" service install Run the web UI as a launchd service (status, print, uninstall)",
|
|
4319
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
|
+
);
|
|
4320
4808
|
console.log();
|
|
4321
4809
|
console.log(
|
|
4322
4810
|
" workspace list List workspaces (active set + focus)",
|