@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.
- package/CHANGELOG.md +215 -0
- package/bin/den +9 -0
- package/docs/distributions.md +202 -22
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/pillars/security/discover.ts +1 -1
- package/src/pillars/smoke/setup-global.ts +2 -1
- package/src/scripts/configure-aws-profiles.ts +2 -1
- package/src/scripts/register-repos.ts +4 -3
- package/src/shared/den-config.ts +38 -1
- package/src/shared/environment-manager/catalog.ts +9 -2
- package/src/shared/environment-manager/default-workspace.ts +92 -14
- package/src/shared/environment-manager/health.ts +96 -17
- package/src/shared/environment-manager/service-runner.ts +51 -5
- package/src/shared/environment-manager/workspace-manager.ts +2 -1
- package/src/shared/environment-manager/workspace-org.ts +264 -0
- package/src/shared/manifest/placement.ts +1 -1
- package/src/shared/manifest/schema.ts +47 -0
- package/src/shared/manifest/service-links.ts +1 -1
- package/src/shared/manifest/validate-manifest.ts +1 -1
- package/src/shared/plural.ts +13 -0
- package/src/shared/service-url.ts +2 -5
- package/src/shared/stacks/compose-health.ts +106 -14
- package/src/shared/stacks/runtime.ts +2 -0
- package/src/shared/types/index.ts +20 -1
- package/src/ui/cli/clone-plan.ts +243 -0
- package/src/ui/cli/create.ts +2 -1
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/index.ts +274 -61
- package/src/ui/cli/mcp-connect.ts +36 -5
- package/src/ui/cli/org-command.ts +162 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/server-identity.ts +233 -0
- package/src/ui/cli/service-command.ts +136 -12
- package/src/ui/cli/spinner.ts +48 -0
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/cli/web-command.ts +76 -25
- package/src/ui/mcp/api.ts +94 -1
- package/src/ui/mcp/server.ts +30 -2
- package/src/ui/mcp/tools.ts +6 -2
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/health-row.ts +6 -3
- package/src/ui/server/index.ts +63 -18
- package/src/ui/server/release-service.ts +16 -3
- package/src/ui/server/workspace-routes.ts +4 -1
package/src/ui/cli/index.ts
CHANGED
|
@@ -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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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 {
|
|
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: [
|
|
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 =
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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))}
|
|
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))}
|
|
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(
|
|
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}
|
|
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}
|
|
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
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
-
/**
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
const
|
|
3199
|
-
|
|
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}
|
|
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
|
|
3550
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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)",
|