@monster-orange/den 0.2.5 → 0.2.7

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/bin/den +9 -0
  3. package/docs/distributions.md +202 -22
  4. package/package.json +1 -1
  5. package/service/install.sh +41 -2
  6. package/src/pillars/security/discover.ts +1 -1
  7. package/src/pillars/smoke/setup-global.ts +2 -1
  8. package/src/scripts/configure-aws-profiles.ts +2 -1
  9. package/src/scripts/register-repos.ts +4 -3
  10. package/src/shared/den-config.ts +38 -1
  11. package/src/shared/environment-manager/catalog.ts +9 -2
  12. package/src/shared/environment-manager/default-workspace.ts +92 -14
  13. package/src/shared/environment-manager/health.ts +96 -17
  14. package/src/shared/environment-manager/service-runner.ts +51 -5
  15. package/src/shared/environment-manager/workspace-manager.ts +2 -1
  16. package/src/shared/environment-manager/workspace-org.ts +264 -0
  17. package/src/shared/manifest/placement.ts +1 -1
  18. package/src/shared/manifest/schema.ts +47 -0
  19. package/src/shared/manifest/service-links.ts +1 -1
  20. package/src/shared/manifest/validate-manifest.ts +1 -1
  21. package/src/shared/plural.ts +13 -0
  22. package/src/shared/service-url.ts +2 -5
  23. package/src/shared/stacks/compose-health.ts +106 -14
  24. package/src/shared/stacks/runtime.ts +2 -0
  25. package/src/shared/types/index.ts +20 -1
  26. package/src/ui/cli/clone-plan.ts +243 -0
  27. package/src/ui/cli/create.ts +2 -1
  28. package/src/ui/cli/flag-parser.ts +1 -1
  29. package/src/ui/cli/index.ts +274 -61
  30. package/src/ui/cli/mcp-connect.ts +36 -5
  31. package/src/ui/cli/org-command.ts +162 -0
  32. package/src/ui/cli/results-viewer.ts +5 -5
  33. package/src/ui/cli/server-identity.ts +233 -0
  34. package/src/ui/cli/service-command.ts +136 -12
  35. package/src/ui/cli/spinner.ts +48 -0
  36. package/src/ui/cli/table.ts +20 -0
  37. package/src/ui/cli/validate-all.ts +171 -0
  38. package/src/ui/cli/web-command.ts +76 -25
  39. package/src/ui/mcp/api.ts +94 -1
  40. package/src/ui/mcp/server.ts +30 -2
  41. package/src/ui/mcp/tools.ts +6 -2
  42. package/src/ui/server/daemon-reaper.ts +22 -4
  43. package/src/ui/server/health-row.ts +6 -3
  44. package/src/ui/server/index.ts +63 -18
  45. package/src/ui/server/release-service.ts +16 -3
  46. package/src/ui/server/workspace-routes.ts +4 -1
@@ -12,19 +12,12 @@ import {
12
12
  } from "../../shared/stacks/status.js";
13
13
  import { isNamedTarget } from "./named-target.js";
14
14
  import chalk from "chalk";
15
- import oraSpinner from "ora";
16
-
17
- /**
18
- * Spinners are progress, not diagnostics: on a terminal they animate on
19
- * stderr as usual; piped (CI, scripts) their lines go to stdout so stderr
20
- * carries only real warnings and errors.
21
- */
22
- const ora = (text: string) =>
23
- oraSpinner({
24
- text,
25
- stream: process.stderr.isTTY ? process.stderr : process.stdout,
26
- });
27
- import Table from "cli-table3";
15
+ import { noun, plural } from "../../shared/plural.js";
16
+ // Spinners: progress on stderr (a terminal) or stdout (piped); plain lines
17
+ // where they cannot animate. See spinner.ts.
18
+ import { spinner as ora } from "./spinner.js";
19
+ import { newTable } from "./table.js";
20
+ import { validateAll, validateAllFailed } from "./validate-all.js";
28
21
  import { cliPrompts } from "./prompts.js";
29
22
  import { resultsViewer } from "./results-viewer.js";
30
23
  import { testRunner } from "./test-runner.js";
@@ -87,6 +80,7 @@ import {
87
80
  import {
88
81
  blueprintWorkspaces,
89
82
  defaultWorkspaceNeedsSeeding,
83
+ blueprintAlreadySeeded,
90
84
  defaultWorkspaceHasServices,
91
85
  isMultiBlueprint,
92
86
  seedBlueprintWorkspaces,
@@ -146,7 +140,10 @@ import {
146
140
  } from "../../shared/environment-manager/up-plan.js";
147
141
  import { execSync } from "child_process";
148
142
  import * as fs from "fs";
149
- import { readManifest } from "../../shared/manifest/read-manifest.js";
143
+ import {
144
+ heldManifestsDir,
145
+ readManifest,
146
+ } from "../../shared/manifest/read-manifest.js";
150
147
  import * as nodePath from "path";
151
148
  import { runInit } from "./init.js";
152
149
  import {
@@ -175,6 +172,20 @@ import { cmdConfig } from "./config-command.js";
175
172
  import { cmdEdge } from "./edge-command.js";
176
173
  import { cmdService } from "./service-command.js";
177
174
  import { cmdStack } from "./stack-command.js";
175
+ import {
176
+ cmdOrg,
177
+ orgFlag,
178
+ orgListRows,
179
+ printOrgWarnings,
180
+ registerOrg,
181
+ sameOrg,
182
+ } from "./org-command.js";
183
+ import {
184
+ buildSetupPlan,
185
+ renderClonePlan,
186
+ workspaceClonePlan,
187
+ } from "./clone-plan.js";
188
+ import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
178
189
  import { cmdWeb, displayWebHelp } from "./web-command.js";
179
190
  import { cmdGraph } from "./graph-command.js";
180
191
  import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
@@ -212,8 +223,11 @@ const WORKSPACE_SUBCOMMANDS: {
212
223
  }[] = [
213
224
  {
214
225
  name: "list",
215
- usage: "list",
216
- lines: ["every workspace, the active set and the focus"],
226
+ usage: "list [--org <id>]",
227
+ lines: [
228
+ "every workspace, the active set, the focus and its org;",
229
+ "--org <id> lists one org's workspaces",
230
+ ],
217
231
  },
218
232
  {
219
233
  name: "activate",
@@ -301,19 +315,31 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
301
315
  lines: ["List all registered services in the catalog."],
302
316
  },
303
317
  register: {
304
- usage: "register [path]",
318
+ usage: "register [path] [--org <id>]",
305
319
  lines: [
306
320
  "With a path: add that checkout (dev-managed) under its manifest name.",
307
- "Without one: pick the repo interactively.",
321
+ "Without one: pick the repo interactively. A managed clone is picked",
322
+ "from the focused workspace's org; --org <id> picks from another.",
308
323
  ],
309
324
  },
310
325
  setup: {
311
- usage: "setup [service...] [--force|-f]",
326
+ usage: "setup [service...] [--force|-f] [--plan]",
312
327
  lines: [
313
328
  "Run 'make setup' once per system (clones missing managed repos first).",
314
329
  "Default targets MANAGED clones only — setup's codegen/installs",
315
330
  "shouldn't touch your own dev-managed checkouts uninvited.",
316
331
  "--force (-f) includes dev-managed repos; naming a service always works.",
332
+ "--plan prints each repo's org and clone URL and stops: nothing is",
333
+ "cloned, seeded or written (on a fresh machine: the blueprint's plan).",
334
+ ],
335
+ },
336
+ org: {
337
+ usage: "org list [--org <id>]",
338
+ lines: [
339
+ "Every org: its label, its workspaces, whether it is the default",
340
+ "(github_org), and any org warnings. A workspace's org is its own org:,",
341
+ "then the blueprint's org:, then its repos' one catalog owner, then",
342
+ "github_org.",
317
343
  ],
318
344
  },
319
345
  sync: {
@@ -446,6 +472,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
446
472
  `use-managed`,
447
473
  "setup",
448
474
  "workspace",
475
+ "org",
449
476
  "sync",
450
477
  "init",
451
478
  "validate",
@@ -496,8 +523,6 @@ function printValidation(
496
523
  layer?: string,
497
524
  ): void {
498
525
  const tag = layer ? `${layer}, ` : "";
499
- const plural = (n: number, word: string): string =>
500
- `${n} ${word}${n === 1 ? "" : "s"}`;
501
526
  if (result.valid && result.warnings.length === 0) {
502
527
  console.log(
503
528
  chalk.green(` ✓ ${label}`) +
@@ -754,6 +779,10 @@ class DenCLI {
754
779
  await this.cmdWorkspace(args.slice(1));
755
780
  break;
756
781
 
782
+ case "org":
783
+ cmdOrg(args.slice(1));
784
+ break;
785
+
757
786
  case "sync":
758
787
  await this.cmdSync(args.slice(1));
759
788
  break;
@@ -761,9 +790,23 @@ class DenCLI {
761
790
  case "init": {
762
791
  // In the den root, `init` seeds/resets the default workspace; pointed
763
792
  // at a service repo it scaffolds that repo's .den.yml (manifest init).
764
- const initTarget = args[1] ?? getInvokeCwd();
765
- if (nodePath.resolve(initTarget) === getDistRoot()) {
766
- await this.seedDefaultWorkspaceFlow("init");
793
+ const initTarget =
794
+ args.slice(1).find((a) => !a.startsWith("-")) ?? getInvokeCwd();
795
+ // Compare real paths: a symlinked tmp or home (/var → /private/var)
796
+ // must not turn a seed into a manifest scaffold.
797
+ const real = (p: string) => {
798
+ try {
799
+ return fs.realpathSync(nodePath.resolve(p));
800
+ } catch {
801
+ return nodePath.resolve(p);
802
+ }
803
+ };
804
+ if (real(initTarget) === real(getDistRoot())) {
805
+ await this.seedDefaultWorkspaceFlow(
806
+ "init",
807
+ args.slice(1).includes("--reset"),
808
+ "init",
809
+ );
767
810
  } else {
768
811
  await runInit(initTarget);
769
812
  }
@@ -1097,7 +1140,14 @@ class DenCLI {
1097
1140
  if (!catalogEntry) continue;
1098
1141
  const branch = wsEntry.branch ?? "main";
1099
1142
  try {
1100
- ensureManagedServiceClone(repoName, catalogEntry.gh, branch);
1143
+ // An entry with `gh: ""` clones from the workspace's org, as `up`
1144
+ // does (and `setup --plan` says), instead of failing on an empty slug.
1145
+ const ghSlug = resolveRepoGhSlug(repoName, catalog, () =>
1146
+ githubOrgFor(ws.name),
1147
+ );
1148
+ if (!catalogEntry.gh?.trim())
1149
+ upsertCatalogEntry(repoName, { ...catalogEntry, gh: ghSlug });
1150
+ ensureManagedServiceClone(repoName, ghSlug, branch);
1101
1151
  } catch (err) {
1102
1152
  console.log(
1103
1153
  chalk.red(
@@ -1198,7 +1248,9 @@ class DenCLI {
1198
1248
  // Catalog entries written by the dev-managed add flow carry `gh: ""`, so
1199
1249
  // an entry can exist yet be unclonable. Derive the conventional slug and
1200
1250
  // persist the repair rather than cloning from an empty URL.
1201
- const ghSlug = resolveRepoGhSlug(repoName, catalog);
1251
+ const ghSlug = resolveRepoGhSlug(repoName, catalog, () =>
1252
+ githubOrgFor(ws.name),
1253
+ );
1202
1254
  if (!catalog[repoName]?.gh) upsertCatalogEntry(repoName, { gh: ghSlug });
1203
1255
  try {
1204
1256
  // runSetup=false: clone only — setup is run below, hidden + non-interactive.
@@ -1901,6 +1953,25 @@ class DenCLI {
1901
1953
  return seed && knownRepos.has(seed) ? seed : undefined;
1902
1954
  }
1903
1955
 
1956
+ /**
1957
+ * `up --plan`'s clone half (ORG-1a): each repo's org and the URL a missing
1958
+ * managed clone would come from. Read-only, like the rest of the plan.
1959
+ */
1960
+ private renderUpClonePlan(repos: readonly string[]): void {
1961
+ const ws = getActiveWorkspace();
1962
+ if (!ws) return;
1963
+ const lines = renderClonePlan(
1964
+ {
1965
+ fromBlueprint: false,
1966
+ workspaces: [workspaceClonePlan(ws.name, repos)],
1967
+ },
1968
+ product().cli,
1969
+ );
1970
+ console.log(chalk.bold(" Repos and clone URLs:\n"));
1971
+ for (const line of lines) console.log(chalk.gray(` ${line}`.trimEnd()));
1972
+ console.log();
1973
+ }
1974
+
1904
1975
  /** Render the read-only `den up --plan` view: per-node command + flags. */
1905
1976
  private renderUpPlan(nodes: NodePlan[]): void {
1906
1977
  const tag = (n: NodePlan): string => {
@@ -2014,6 +2085,7 @@ class DenCLI {
2014
2085
  this.renderUpPlan(
2015
2086
  buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
2016
2087
  );
2088
+ this.renderUpClonePlan(rawOrder);
2017
2089
  return;
2018
2090
  }
2019
2091
  if (dryRun) {
@@ -2225,6 +2297,7 @@ class DenCLI {
2225
2297
  this.renderUpPlan(
2226
2298
  buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
2227
2299
  );
2300
+ this.renderUpClonePlan(rawOrder);
2228
2301
  return;
2229
2302
  }
2230
2303
  if (dryRun) {
@@ -2284,7 +2357,7 @@ class DenCLI {
2284
2357
  }
2285
2358
  console.log(
2286
2359
  chalk.cyan(
2287
- `\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(order.length))} systems`,
2360
+ `\n ${only ? "Application" : "Workspace-wide"} up — ${chalk.bold(String(order.length))} ${noun(order.length, "system")}`,
2288
2361
  ),
2289
2362
  );
2290
2363
  console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
@@ -2345,7 +2418,7 @@ class DenCLI {
2345
2418
  const order = [...target.rawOrder].reverse();
2346
2419
  console.log(
2347
2420
  chalk.cyan(
2348
- `\n ${only ? "Application" : "Workspace-wide"} down — ${chalk.bold(String(order.length))} systems`,
2421
+ `\n ${only ? "Application" : "Workspace-wide"} down — ${chalk.bold(String(order.length))} ${noun(order.length, "system")}`,
2349
2422
  ),
2350
2423
  );
2351
2424
  console.log(chalk.gray(` Order: ${order.join(" → ")}\n`));
@@ -2503,6 +2576,8 @@ class DenCLI {
2503
2576
  */
2504
2577
  private async seedDefaultWorkspaceFlow(
2505
2578
  reason: "auto" | "init",
2579
+ reset = false,
2580
+ via: "init" | "workspace seed" = "workspace seed",
2506
2581
  ): Promise<boolean> {
2507
2582
  const bps = blueprintWorkspaces();
2508
2583
  if (bps.length === 0) {
@@ -2516,13 +2591,43 @@ class DenCLI {
2516
2591
  return false;
2517
2592
  }
2518
2593
  const multi = isMultiBlueprint();
2594
+ // A repeat seed: say so, and touch nothing.
2595
+ if (blueprintAlreadySeeded()) {
2596
+ console.log(
2597
+ chalk.green(
2598
+ multi
2599
+ ? `\n ✓ ${plural(bps.length, "workspace")} already seeded (${bps.map((b) => b.name).join(", ")}): nothing changed.\n`
2600
+ : `\n ✓ Default workspace already seeded — ${plural(bps[0].repos.length, "repo")} (managed): nothing changed.\n`,
2601
+ ),
2602
+ );
2603
+ return false;
2604
+ }
2519
2605
  const willReset = defaultWorkspaceHasServices();
2520
2606
  const interactive = !!(process.stdin.isTTY && process.stdout.isTTY);
2607
+ // A reset replaces every entry, dev-managed checkouts included. On a TTY
2608
+ // the user is asked (default No); without one there is nobody to ask, so
2609
+ // only an explicit --reset may do it (SEED-0).
2610
+ if (willReset && !interactive && !reset) {
2611
+ const what = multi
2612
+ ? "the blueprint's workspaces"
2613
+ : "the default workspace";
2614
+ console.error(
2615
+ chalk.red(
2616
+ `\n ❌ Not resetting ${what}: it has entries, and a reset replaces them all\n` +
2617
+ ` (your own dev-managed checkouts included) with managed clones.\n` +
2618
+ ` Nothing changed. To reset anyway: ${product().cli} ${via} --reset\n`,
2619
+ ),
2620
+ );
2621
+ process.exitCode = 1;
2622
+ return false;
2623
+ }
2521
2624
 
2522
2625
  for (const bp of bps) {
2523
2626
  const label = multi ? `Workspace ${bp.name}` : "Default workspace";
2524
2627
  console.log(
2525
- chalk.cyan(`\n ${label} — ${bp.repos.length} repos, all managed:`),
2628
+ chalk.cyan(
2629
+ `\n ${label} — ${plural(bp.repos.length, "repo")}, all managed:`,
2630
+ ),
2526
2631
  );
2527
2632
  console.log(chalk.gray(` ${bp.repos.join(", ")}`));
2528
2633
  }
@@ -2563,7 +2668,7 @@ class DenCLI {
2563
2668
  const focus = readWorkspacesFile().focus;
2564
2669
  console.log(
2565
2670
  chalk.green(
2566
- ` ✓ ${seeded.length} workspaces ready (${seeded.map((w) => w.name).join(", ")}), all active; focus: ${focus}. ` +
2671
+ ` ✓ ${plural(seeded.length, "workspace")} ${willReset ? "reset to the blueprint" : "ready"} (${seeded.map((w) => w.name).join(", ")}), all active; focus: ${focus}. ` +
2567
2672
  `Run \`${product().cli} up <repo>\` to bring up its closure.\n`,
2568
2673
  ),
2569
2674
  );
@@ -2572,7 +2677,7 @@ class DenCLI {
2572
2677
  const seeded = seedDefaultWorkspace();
2573
2678
  console.log(
2574
2679
  chalk.green(
2575
- ` ✓ Default workspace ready — ${seeded.length} repos (managed). ` +
2680
+ ` ✓ Default workspace ${willReset ? "reset to the blueprint" : "ready"} — ${plural(seeded.length, "repo")} (managed). ` +
2576
2681
  `Run \`${product().cli} up <repo>\` to bring up its closure.\n`,
2577
2682
  ),
2578
2683
  );
@@ -2602,7 +2707,7 @@ class DenCLI {
2602
2707
  if (added.length > 0) {
2603
2708
  console.log(
2604
2709
  chalk.gray(
2605
- ` default workspace: added standard repo(s) ${added.join(", ")} (managed)`,
2710
+ ` default workspace: added standard ${noun(added.length, "repo")} ${added.join(", ")} (managed)`,
2606
2711
  ),
2607
2712
  );
2608
2713
  }
@@ -2861,7 +2966,7 @@ class DenCLI {
2861
2966
  chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
2862
2967
  );
2863
2968
 
2864
- const table = new Table({
2969
+ const table = newTable({
2865
2970
  head: ["Service", "Mode", "Branch", "Health", "Reason"],
2866
2971
  style: { head: ["cyan"] },
2867
2972
  });
@@ -3023,7 +3128,7 @@ class DenCLI {
3023
3128
  return;
3024
3129
  }
3025
3130
 
3026
- const table = new Table({
3131
+ const table = newTable({
3027
3132
  head: ["Service", "Mode", "Health", "Commands"],
3028
3133
  style: { head: ["cyan"] },
3029
3134
  });
@@ -3088,7 +3193,8 @@ class DenCLI {
3088
3193
  }
3089
3194
 
3090
3195
  const repoPathArg = args.find(
3091
- (a, i) => !a.startsWith("-") && !/^--(name|gh)$/.test(args[i - 1] ?? ""),
3196
+ (a, i) =>
3197
+ !a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
3092
3198
  );
3093
3199
  if (repoPathArg) {
3094
3200
  const flag = (n: string) => {
@@ -3104,7 +3210,7 @@ class DenCLI {
3104
3210
  if (source === "local") {
3105
3211
  await this.registerLinked(ws);
3106
3212
  } else {
3107
- await this.registerManaged(ws);
3213
+ await this.registerManaged(ws, orgFlag(args));
3108
3214
  }
3109
3215
  }
3110
3216
 
@@ -3183,26 +3289,24 @@ class DenCLI {
3183
3289
  );
3184
3290
  }
3185
3291
 
3186
- /** List org repos via `gh`, let user pick one + branch → managed service. */
3187
- private async registerManaged(ws: {
3188
- name: string;
3189
- config: import("../../shared/types/index.js").WorkspaceConfig;
3190
- }): Promise<void> {
3191
- // Derive org from den's own git remote
3192
- const { fileURLToPath: ftu } = await import("url");
3193
- const { default: pathMod } = await import("path");
3194
- const distRoot = pathMod.resolve(
3195
- pathMod.dirname(ftu(import.meta.url)),
3196
- "../../..",
3197
- );
3198
- const denCoords = getGitHubCoords(distRoot);
3199
- // The instance's config names its org; the checkout's own remote is only a
3200
- // fallback (inside a package it would name the engine's repo, not the org's).
3201
- const org = readDenConfig().github_org ?? denCoords?.owner;
3202
- if (!org || !/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(org)) {
3292
+ /**
3293
+ * List an org's repos via `gh`, let the user pick one + branch → managed
3294
+ * service. The org is `--org <id>`, else the focused workspace's resolved
3295
+ * org (ORG-1a). Never Den's own checkout or remote (SEC-0).
3296
+ */
3297
+ private async registerManaged(
3298
+ ws: {
3299
+ name: string;
3300
+ config: import("../../shared/types/index.js").WorkspaceConfig;
3301
+ },
3302
+ orgArg?: string,
3303
+ ): Promise<void> {
3304
+ const org = registerOrg(ws.name, orgArg);
3305
+ if (!org) {
3203
3306
  console.log(
3204
3307
  chalk.red(
3205
- `\n ❌ No GitHub org to list: set github_org in ${product().configFile}\n`,
3308
+ `\n ❌ No GitHub org to list: set github_org in ${product().configFile}, ` +
3309
+ `org: on workspace ${ws.name}, or pass --org <id>\n`,
3206
3310
  ),
3207
3311
  );
3208
3312
  return;
@@ -3459,6 +3563,15 @@ class DenCLI {
3459
3563
  const force = args.includes("--force") || args.includes("-f");
3460
3564
  const names = args.filter((a) => !a.startsWith("-"));
3461
3565
 
3566
+ // `--plan`: each repo's org and clone URL, then stop. Read-only.
3567
+ if (args.includes("--plan")) {
3568
+ console.log(chalk.bold("\n Setup plan (no changes will be made):\n"));
3569
+ for (const line of renderClonePlan(buildSetupPlan(names), product().cli))
3570
+ console.log(` ${line}`.trimEnd());
3571
+ console.log();
3572
+ return;
3573
+ }
3574
+
3462
3575
  // Clone any managed repos that aren't on disk yet before reading manifests.
3463
3576
  this.cloneMissingManagedRepos();
3464
3577
 
@@ -3546,13 +3659,20 @@ class DenCLI {
3546
3659
 
3547
3660
  switch (sub) {
3548
3661
  case "list": {
3549
- const table = new Table({
3550
- head: ["Workspace", "Active", "Focus", "Services", "Type"],
3662
+ const only = orgFlag(args.slice(1));
3663
+ const { resolved, warnings } = orgListRows();
3664
+ const orgOfWs = new Map(resolved.map((w) => [w.workspace, w.org]));
3665
+ const table = newTable({
3666
+ head: ["Workspace", "Org", "Active", "Focus", "Services", "Type"],
3551
3667
  style: { head: ["cyan"] },
3552
3668
  });
3553
- for (const [name, cfg] of Object.entries(file.workspaces)) {
3669
+ const shown = Object.entries(file.workspaces).filter(
3670
+ ([name]) => !only || sameOrg(orgOfWs.get(name) ?? null, only),
3671
+ );
3672
+ for (const [name, cfg] of shown) {
3554
3673
  table.push([
3555
3674
  chalk.bold(name),
3675
+ orgOfWs.get(name) ?? chalk.gray("none"),
3556
3676
  activeSet.has(name) ? chalk.green("✓") : "",
3557
3677
  name === focus ? chalk.cyan("●") : "",
3558
3678
  Object.keys(cfg.services).join(", ") || chalk.gray("none"),
@@ -3560,6 +3680,14 @@ class DenCLI {
3560
3680
  ]);
3561
3681
  }
3562
3682
  console.log(table.toString());
3683
+ if (only && shown.length === 0)
3684
+ console.log(chalk.yellow(` No workspace in org ${only}.`));
3685
+ const names = new Set(shown.map(([n]) => n));
3686
+ printOrgWarnings(
3687
+ warnings.filter((w) =>
3688
+ [...names].some((n) => w.startsWith(`workspace ${n}:`)),
3689
+ ),
3690
+ );
3563
3691
  break;
3564
3692
  }
3565
3693
  case "activate": {
@@ -3665,7 +3793,7 @@ class DenCLI {
3665
3793
  break;
3666
3794
  }
3667
3795
  case "seed": {
3668
- await this.seedDefaultWorkspaceFlow("init");
3796
+ await this.seedDefaultWorkspaceFlow("init", args.includes("--reset"));
3669
3797
  break;
3670
3798
  }
3671
3799
  case "create": {
@@ -3802,7 +3930,7 @@ class DenCLI {
3802
3930
  if (missing.length > 0) {
3803
3931
  console.log(
3804
3932
  chalk.cyan(
3805
- `\n Cloning ${missing.length} managed repo(s) not on disk yet: ${missing.join(", ")}`,
3933
+ `\n Cloning ${plural(missing.length, "managed repo")} not on disk yet: ${missing.join(", ")}`,
3806
3934
  ),
3807
3935
  );
3808
3936
  await this.ensureManagedClonesFor(missing);
@@ -3858,7 +3986,15 @@ class DenCLI {
3858
3986
  // ── den validate ────────────────────────────────────────────────────────
3859
3987
 
3860
3988
  private async cmdValidate(args: string[]): Promise<void> {
3861
- const target = args[0] ?? getInvokeCwd();
3989
+ const positional = args.filter((a) => !a.startsWith("-"));
3990
+ const held = args.includes("--held");
3991
+ // Bare `validate` in the distribution root (which has no manifest of its
3992
+ // own), or `--held`: every manifest the distribution uses.
3993
+ if (held || (positional.length === 0 && this.invokedInDistRoot())) {
3994
+ this.validateEverything(held);
3995
+ return;
3996
+ }
3997
+ const target = positional[0] ?? getInvokeCwd();
3862
3998
  const fs = await import("fs");
3863
3999
  const nodePath = await import("path");
3864
4000
 
@@ -3924,6 +4060,78 @@ class DenCLI {
3924
4060
  if (!result.valid) process.exit(1);
3925
4061
  }
3926
4062
 
4063
+ /** Whether the CLI was run from the distribution root (real paths). */
4064
+ private invokedInDistRoot(): boolean {
4065
+ const real = (p: string) => {
4066
+ try {
4067
+ return fs.realpathSync(nodePath.resolve(p));
4068
+ } catch {
4069
+ return nodePath.resolve(p);
4070
+ }
4071
+ };
4072
+ return (
4073
+ real(getInvokeCwd()) === real(getDistRoot()) &&
4074
+ !fs.existsSync(nodePath.join(getDistRoot(), product().manifestYml))
4075
+ );
4076
+ }
4077
+
4078
+ /** `validate` (bare, in the root) / `validate --held`: see validate-all.ts. */
4079
+ private validateEverything(held: boolean): void {
4080
+ const results = validateAll({ held, validate: validateManifestLayers });
4081
+ console.log();
4082
+ if (results.length === 0) {
4083
+ console.log(
4084
+ chalk.yellow(
4085
+ held
4086
+ ? ` No manifests in ${heldManifestsDir()}.`
4087
+ : " No repos in any workspace and no held manifests.",
4088
+ ),
4089
+ );
4090
+ console.log();
4091
+ return;
4092
+ }
4093
+ const width = Math.max(...results.map((r) => r.repo.length));
4094
+ for (const r of results) {
4095
+ const where = r.checkout ?? "manifests/";
4096
+ const ws = r.workspaces.length ? ` [${r.workspaces.join(", ")}]` : "";
4097
+ const note = r.note ? ` (${r.note})` : "";
4098
+ const head = `${r.repo.padEnd(width)} ${r.source.padEnd(15)} ${where}${ws}${note}`;
4099
+ if (r.missing) {
4100
+ console.log(chalk.yellow(` ⚠ ${head}`));
4101
+ continue;
4102
+ }
4103
+ const warnings = r.result.layers.reduce(
4104
+ (n, l) => n + l.result.warnings.length,
4105
+ 0,
4106
+ );
4107
+ if (r.result.valid && warnings === 0) {
4108
+ console.log(chalk.green(` ✓ ${head}`));
4109
+ continue;
4110
+ }
4111
+ console.log(
4112
+ r.result.valid
4113
+ ? chalk.yellow(` ⚠ ${head}`)
4114
+ : chalk.red(` ✗ ${head}`),
4115
+ );
4116
+ for (const l of r.result.layers)
4117
+ if (!l.result.valid || l.result.warnings.length)
4118
+ printValidation(
4119
+ ` ${l.file ?? "merged"}`,
4120
+ l.result,
4121
+ l.layer === "merged" ? "merged" : l.layer,
4122
+ );
4123
+ }
4124
+ const failed = results.filter((r) => !r.missing && !r.result.valid).length;
4125
+ const missing = results.filter((r) => r.missing).length;
4126
+ console.log();
4127
+ console.log(
4128
+ ` ${plural(results.length, held ? "manifest" : "repo")}: ${failed ? chalk.red(`${failed} failed`) : chalk.green("all valid")}` +
4129
+ (missing ? chalk.yellow(`, ${missing} with no manifest to check`) : ""),
4130
+ );
4131
+ console.log();
4132
+ if (validateAllFailed(results)) process.exitCode = 1;
4133
+ }
4134
+
3927
4135
  // ── den local-dev ───────────────────────────────────────────────────────
3928
4136
 
3929
4137
  private async cmdLocalDev(arg?: string): Promise<void> {
@@ -4004,7 +4212,9 @@ class DenCLI {
4004
4212
  " register [path] Register a repo in the service catalog",
4005
4213
  );
4006
4214
  console.log();
4007
- console.log(" setup [service...] Run make setup for services");
4215
+ console.log(
4216
+ " setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
4217
+ );
4008
4218
  console.log(" sync [service...] Check for updates and offer to pull");
4009
4219
  console.log(
4010
4220
  " create <name> [dir] Scaffold a new distribution of your own (--title, --cli, --env-prefix)",
@@ -4035,6 +4245,9 @@ class DenCLI {
4035
4245
  console.log(
4036
4246
  " workspace code [n] Open workspace in VS Code (generates .code-workspace)",
4037
4247
  );
4248
+ console.log(
4249
+ " org list [--org <id>] List orgs: label, workspaces, default, warnings",
4250
+ );
4038
4251
  console.log("\nTest commands:");
4039
4252
  console.log(
4040
4253
  " menu Open the interactive menu (pillar/module/env picker)",