@monster-orange/den 0.2.6 → 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 +117 -0
- package/bin/den +9 -0
- package/docs/distributions.md +92 -6
- package/package.json +1 -1
- package/service/install.sh +41 -2
- 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 +45 -14
- package/src/shared/environment-manager/health.ts +51 -8
- 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/types/index.ts +7 -0
- package/src/ui/cli/clone-plan.ts +243 -0
- package/src/ui/cli/flag-parser.ts +1 -1
- package/src/ui/cli/index.ts +249 -40
- package/src/ui/cli/org-command.ts +162 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/mcp/server.ts +24 -1
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/index.ts +31 -4
- 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
|
@@ -16,7 +16,8 @@ import { noun, plural } from "../../shared/plural.js";
|
|
|
16
16
|
// Spinners: progress on stderr (a terminal) or stdout (piped); plain lines
|
|
17
17
|
// where they cannot animate. See spinner.ts.
|
|
18
18
|
import { spinner as ora } from "./spinner.js";
|
|
19
|
-
import
|
|
19
|
+
import { newTable } from "./table.js";
|
|
20
|
+
import { validateAll, validateAllFailed } from "./validate-all.js";
|
|
20
21
|
import { cliPrompts } from "./prompts.js";
|
|
21
22
|
import { resultsViewer } from "./results-viewer.js";
|
|
22
23
|
import { testRunner } from "./test-runner.js";
|
|
@@ -139,7 +140,10 @@ import {
|
|
|
139
140
|
} from "../../shared/environment-manager/up-plan.js";
|
|
140
141
|
import { execSync } from "child_process";
|
|
141
142
|
import * as fs from "fs";
|
|
142
|
-
import {
|
|
143
|
+
import {
|
|
144
|
+
heldManifestsDir,
|
|
145
|
+
readManifest,
|
|
146
|
+
} from "../../shared/manifest/read-manifest.js";
|
|
143
147
|
import * as nodePath from "path";
|
|
144
148
|
import { runInit } from "./init.js";
|
|
145
149
|
import {
|
|
@@ -168,6 +172,20 @@ import { cmdConfig } from "./config-command.js";
|
|
|
168
172
|
import { cmdEdge } from "./edge-command.js";
|
|
169
173
|
import { cmdService } from "./service-command.js";
|
|
170
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";
|
|
171
189
|
import { cmdWeb, displayWebHelp } from "./web-command.js";
|
|
172
190
|
import { cmdGraph } from "./graph-command.js";
|
|
173
191
|
import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
|
|
@@ -205,8 +223,11 @@ const WORKSPACE_SUBCOMMANDS: {
|
|
|
205
223
|
}[] = [
|
|
206
224
|
{
|
|
207
225
|
name: "list",
|
|
208
|
-
usage: "list",
|
|
209
|
-
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
|
+
],
|
|
210
231
|
},
|
|
211
232
|
{
|
|
212
233
|
name: "activate",
|
|
@@ -294,19 +315,31 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
294
315
|
lines: ["List all registered services in the catalog."],
|
|
295
316
|
},
|
|
296
317
|
register: {
|
|
297
|
-
usage: "register [path]",
|
|
318
|
+
usage: "register [path] [--org <id>]",
|
|
298
319
|
lines: [
|
|
299
320
|
"With a path: add that checkout (dev-managed) under its manifest name.",
|
|
300
|
-
"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.",
|
|
301
323
|
],
|
|
302
324
|
},
|
|
303
325
|
setup: {
|
|
304
|
-
usage: "setup [service...] [--force|-f]",
|
|
326
|
+
usage: "setup [service...] [--force|-f] [--plan]",
|
|
305
327
|
lines: [
|
|
306
328
|
"Run 'make setup' once per system (clones missing managed repos first).",
|
|
307
329
|
"Default targets MANAGED clones only — setup's codegen/installs",
|
|
308
330
|
"shouldn't touch your own dev-managed checkouts uninvited.",
|
|
309
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.",
|
|
310
343
|
],
|
|
311
344
|
},
|
|
312
345
|
sync: {
|
|
@@ -439,6 +472,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
439
472
|
`use-managed`,
|
|
440
473
|
"setup",
|
|
441
474
|
"workspace",
|
|
475
|
+
"org",
|
|
442
476
|
"sync",
|
|
443
477
|
"init",
|
|
444
478
|
"validate",
|
|
@@ -745,6 +779,10 @@ class DenCLI {
|
|
|
745
779
|
await this.cmdWorkspace(args.slice(1));
|
|
746
780
|
break;
|
|
747
781
|
|
|
782
|
+
case "org":
|
|
783
|
+
cmdOrg(args.slice(1));
|
|
784
|
+
break;
|
|
785
|
+
|
|
748
786
|
case "sync":
|
|
749
787
|
await this.cmdSync(args.slice(1));
|
|
750
788
|
break;
|
|
@@ -752,9 +790,23 @@ class DenCLI {
|
|
|
752
790
|
case "init": {
|
|
753
791
|
// In the den root, `init` seeds/resets the default workspace; pointed
|
|
754
792
|
// at a service repo it scaffolds that repo's .den.yml (manifest init).
|
|
755
|
-
const initTarget =
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
);
|
|
758
810
|
} else {
|
|
759
811
|
await runInit(initTarget);
|
|
760
812
|
}
|
|
@@ -1088,7 +1140,14 @@ class DenCLI {
|
|
|
1088
1140
|
if (!catalogEntry) continue;
|
|
1089
1141
|
const branch = wsEntry.branch ?? "main";
|
|
1090
1142
|
try {
|
|
1091
|
-
|
|
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);
|
|
1092
1151
|
} catch (err) {
|
|
1093
1152
|
console.log(
|
|
1094
1153
|
chalk.red(
|
|
@@ -1189,7 +1248,9 @@ class DenCLI {
|
|
|
1189
1248
|
// Catalog entries written by the dev-managed add flow carry `gh: ""`, so
|
|
1190
1249
|
// an entry can exist yet be unclonable. Derive the conventional slug and
|
|
1191
1250
|
// persist the repair rather than cloning from an empty URL.
|
|
1192
|
-
const ghSlug = resolveRepoGhSlug(repoName, catalog)
|
|
1251
|
+
const ghSlug = resolveRepoGhSlug(repoName, catalog, () =>
|
|
1252
|
+
githubOrgFor(ws.name),
|
|
1253
|
+
);
|
|
1193
1254
|
if (!catalog[repoName]?.gh) upsertCatalogEntry(repoName, { gh: ghSlug });
|
|
1194
1255
|
try {
|
|
1195
1256
|
// runSetup=false: clone only — setup is run below, hidden + non-interactive.
|
|
@@ -1892,6 +1953,25 @@ class DenCLI {
|
|
|
1892
1953
|
return seed && knownRepos.has(seed) ? seed : undefined;
|
|
1893
1954
|
}
|
|
1894
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
|
+
|
|
1895
1975
|
/** Render the read-only `den up --plan` view: per-node command + flags. */
|
|
1896
1976
|
private renderUpPlan(nodes: NodePlan[]): void {
|
|
1897
1977
|
const tag = (n: NodePlan): string => {
|
|
@@ -2005,6 +2085,7 @@ class DenCLI {
|
|
|
2005
2085
|
this.renderUpPlan(
|
|
2006
2086
|
buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
|
|
2007
2087
|
);
|
|
2088
|
+
this.renderUpClonePlan(rawOrder);
|
|
2008
2089
|
return;
|
|
2009
2090
|
}
|
|
2010
2091
|
if (dryRun) {
|
|
@@ -2216,6 +2297,7 @@ class DenCLI {
|
|
|
2216
2297
|
this.renderUpPlan(
|
|
2217
2298
|
buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
|
|
2218
2299
|
);
|
|
2300
|
+
this.renderUpClonePlan(rawOrder);
|
|
2219
2301
|
return;
|
|
2220
2302
|
}
|
|
2221
2303
|
if (dryRun) {
|
|
@@ -2494,6 +2576,8 @@ class DenCLI {
|
|
|
2494
2576
|
*/
|
|
2495
2577
|
private async seedDefaultWorkspaceFlow(
|
|
2496
2578
|
reason: "auto" | "init",
|
|
2579
|
+
reset = false,
|
|
2580
|
+
via: "init" | "workspace seed" = "workspace seed",
|
|
2497
2581
|
): Promise<boolean> {
|
|
2498
2582
|
const bps = blueprintWorkspaces();
|
|
2499
2583
|
if (bps.length === 0) {
|
|
@@ -2520,6 +2604,23 @@ class DenCLI {
|
|
|
2520
2604
|
}
|
|
2521
2605
|
const willReset = defaultWorkspaceHasServices();
|
|
2522
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
|
+
}
|
|
2523
2624
|
|
|
2524
2625
|
for (const bp of bps) {
|
|
2525
2626
|
const label = multi ? `Workspace ${bp.name}` : "Default workspace";
|
|
@@ -2865,7 +2966,7 @@ class DenCLI {
|
|
|
2865
2966
|
chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
|
|
2866
2967
|
);
|
|
2867
2968
|
|
|
2868
|
-
const table =
|
|
2969
|
+
const table = newTable({
|
|
2869
2970
|
head: ["Service", "Mode", "Branch", "Health", "Reason"],
|
|
2870
2971
|
style: { head: ["cyan"] },
|
|
2871
2972
|
});
|
|
@@ -3027,7 +3128,7 @@ class DenCLI {
|
|
|
3027
3128
|
return;
|
|
3028
3129
|
}
|
|
3029
3130
|
|
|
3030
|
-
const table =
|
|
3131
|
+
const table = newTable({
|
|
3031
3132
|
head: ["Service", "Mode", "Health", "Commands"],
|
|
3032
3133
|
style: { head: ["cyan"] },
|
|
3033
3134
|
});
|
|
@@ -3092,7 +3193,8 @@ class DenCLI {
|
|
|
3092
3193
|
}
|
|
3093
3194
|
|
|
3094
3195
|
const repoPathArg = args.find(
|
|
3095
|
-
(a, i) =>
|
|
3196
|
+
(a, i) =>
|
|
3197
|
+
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3096
3198
|
);
|
|
3097
3199
|
if (repoPathArg) {
|
|
3098
3200
|
const flag = (n: string) => {
|
|
@@ -3108,7 +3210,7 @@ class DenCLI {
|
|
|
3108
3210
|
if (source === "local") {
|
|
3109
3211
|
await this.registerLinked(ws);
|
|
3110
3212
|
} else {
|
|
3111
|
-
await this.registerManaged(ws);
|
|
3213
|
+
await this.registerManaged(ws, orgFlag(args));
|
|
3112
3214
|
}
|
|
3113
3215
|
}
|
|
3114
3216
|
|
|
@@ -3187,26 +3289,24 @@ class DenCLI {
|
|
|
3187
3289
|
);
|
|
3188
3290
|
}
|
|
3189
3291
|
|
|
3190
|
-
/**
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
const
|
|
3203
|
-
|
|
3204
|
-
// fallback (inside a package it would name the engine's repo, not the org's).
|
|
3205
|
-
const org = readDenConfig().github_org ?? denCoords?.owner;
|
|
3206
|
-
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) {
|
|
3207
3306
|
console.log(
|
|
3208
3307
|
chalk.red(
|
|
3209
|
-
`\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`,
|
|
3210
3310
|
),
|
|
3211
3311
|
);
|
|
3212
3312
|
return;
|
|
@@ -3463,6 +3563,15 @@ class DenCLI {
|
|
|
3463
3563
|
const force = args.includes("--force") || args.includes("-f");
|
|
3464
3564
|
const names = args.filter((a) => !a.startsWith("-"));
|
|
3465
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
|
+
|
|
3466
3575
|
// Clone any managed repos that aren't on disk yet before reading manifests.
|
|
3467
3576
|
this.cloneMissingManagedRepos();
|
|
3468
3577
|
|
|
@@ -3550,13 +3659,20 @@ class DenCLI {
|
|
|
3550
3659
|
|
|
3551
3660
|
switch (sub) {
|
|
3552
3661
|
case "list": {
|
|
3553
|
-
const
|
|
3554
|
-
|
|
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"],
|
|
3555
3667
|
style: { head: ["cyan"] },
|
|
3556
3668
|
});
|
|
3557
|
-
|
|
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) {
|
|
3558
3673
|
table.push([
|
|
3559
3674
|
chalk.bold(name),
|
|
3675
|
+
orgOfWs.get(name) ?? chalk.gray("none"),
|
|
3560
3676
|
activeSet.has(name) ? chalk.green("✓") : "",
|
|
3561
3677
|
name === focus ? chalk.cyan("●") : "",
|
|
3562
3678
|
Object.keys(cfg.services).join(", ") || chalk.gray("none"),
|
|
@@ -3564,6 +3680,14 @@ class DenCLI {
|
|
|
3564
3680
|
]);
|
|
3565
3681
|
}
|
|
3566
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
|
+
);
|
|
3567
3691
|
break;
|
|
3568
3692
|
}
|
|
3569
3693
|
case "activate": {
|
|
@@ -3669,7 +3793,7 @@ class DenCLI {
|
|
|
3669
3793
|
break;
|
|
3670
3794
|
}
|
|
3671
3795
|
case "seed": {
|
|
3672
|
-
await this.seedDefaultWorkspaceFlow("init");
|
|
3796
|
+
await this.seedDefaultWorkspaceFlow("init", args.includes("--reset"));
|
|
3673
3797
|
break;
|
|
3674
3798
|
}
|
|
3675
3799
|
case "create": {
|
|
@@ -3862,7 +3986,15 @@ class DenCLI {
|
|
|
3862
3986
|
// ── den validate ────────────────────────────────────────────────────────
|
|
3863
3987
|
|
|
3864
3988
|
private async cmdValidate(args: string[]): Promise<void> {
|
|
3865
|
-
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();
|
|
3866
3998
|
const fs = await import("fs");
|
|
3867
3999
|
const nodePath = await import("path");
|
|
3868
4000
|
|
|
@@ -3928,6 +4060,78 @@ class DenCLI {
|
|
|
3928
4060
|
if (!result.valid) process.exit(1);
|
|
3929
4061
|
}
|
|
3930
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
|
+
|
|
3931
4135
|
// ── den local-dev ───────────────────────────────────────────────────────
|
|
3932
4136
|
|
|
3933
4137
|
private async cmdLocalDev(arg?: string): Promise<void> {
|
|
@@ -4008,7 +4212,9 @@ class DenCLI {
|
|
|
4008
4212
|
" register [path] Register a repo in the service catalog",
|
|
4009
4213
|
);
|
|
4010
4214
|
console.log();
|
|
4011
|
-
console.log(
|
|
4215
|
+
console.log(
|
|
4216
|
+
" setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
|
|
4217
|
+
);
|
|
4012
4218
|
console.log(" sync [service...] Check for updates and offer to pull");
|
|
4013
4219
|
console.log(
|
|
4014
4220
|
" create <name> [dir] Scaffold a new distribution of your own (--title, --cli, --env-prefix)",
|
|
@@ -4039,6 +4245,9 @@ class DenCLI {
|
|
|
4039
4245
|
console.log(
|
|
4040
4246
|
" workspace code [n] Open workspace in VS Code (generates .code-workspace)",
|
|
4041
4247
|
);
|
|
4248
|
+
console.log(
|
|
4249
|
+
" org list [--org <id>] List orgs: label, workspaces, default, warnings",
|
|
4250
|
+
);
|
|
4042
4251
|
console.log("\nTest commands:");
|
|
4043
4252
|
console.log(
|
|
4044
4253
|
" menu Open the interactive menu (pillar/module/env picker)",
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> org list` (ORG-1a): every org with its label, its workspaces on
|
|
3
|
+
* this machine, whether it is the default (`github_org`), and the org
|
|
4
|
+
* warnings (C1). The orgs are `/api/orgs`'s (catalog owners plus `orgs:`),
|
|
5
|
+
* then the default org and any workspace's org that list does not name.
|
|
6
|
+
*
|
|
7
|
+
* `--org <id>` (on `org list` and `workspace list`) keeps one org's rows.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { product } from "../../shared/product.js";
|
|
11
|
+
import { listOrgs } from "../../shared/manifest/service-links.js";
|
|
12
|
+
import {
|
|
13
|
+
defaultOrg,
|
|
14
|
+
githubOrgFor,
|
|
15
|
+
readOrgContext,
|
|
16
|
+
resolveAllWorkspaceOrgs,
|
|
17
|
+
withOrgMembership,
|
|
18
|
+
type OrgContext,
|
|
19
|
+
type WorkspaceOrg,
|
|
20
|
+
} from "../../shared/environment-manager/workspace-org.js";
|
|
21
|
+
import { newTable } from "./table.js";
|
|
22
|
+
|
|
23
|
+
/** `--org <id>`'s value from `args` (also `--org=<id>`), or undefined. */
|
|
24
|
+
export function orgFlag(args: readonly string[]): string | undefined {
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const a = args[i];
|
|
27
|
+
if (a === "--org") return args[i + 1];
|
|
28
|
+
if (a.startsWith("--org=")) return a.slice("--org=".length);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `args` without `--org <id>` / `--org=<id>`. */
|
|
34
|
+
export function withoutOrgFlag(args: readonly string[]): string[] {
|
|
35
|
+
const out: string[] = [];
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i] === "--org") {
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (args[i].startsWith("--org=")) continue;
|
|
42
|
+
out.push(args[i]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const sameOrg = (a: string | null, b: string): boolean =>
|
|
48
|
+
a !== null && a.toLowerCase() === b.toLowerCase();
|
|
49
|
+
|
|
50
|
+
export interface OrgListRow {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
default: boolean;
|
|
54
|
+
workspaces: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The rows `org list` prints, and every workspace's warnings. */
|
|
58
|
+
export function orgListRows(ctx: OrgContext = readOrgContext()): {
|
|
59
|
+
rows: OrgListRow[];
|
|
60
|
+
warnings: string[];
|
|
61
|
+
resolved: WorkspaceOrg[];
|
|
62
|
+
} {
|
|
63
|
+
const resolved = resolveAllWorkspaceOrgs(ctx);
|
|
64
|
+
const listed = withOrgMembership(
|
|
65
|
+
listOrgs(ctx.catalog, ctx.config.orgs ?? {}),
|
|
66
|
+
resolved,
|
|
67
|
+
ctx.file,
|
|
68
|
+
ctx.config,
|
|
69
|
+
).map((o) => ({
|
|
70
|
+
id: o.id,
|
|
71
|
+
label: o.label,
|
|
72
|
+
default: o.default,
|
|
73
|
+
workspaces: o.workspaces,
|
|
74
|
+
}));
|
|
75
|
+
const local = new Set(Object.keys(ctx.file.workspaces));
|
|
76
|
+
const extra = new Map<string, OrgListRow>();
|
|
77
|
+
const add = (id: string, isDefault: boolean): void => {
|
|
78
|
+
if (listed.some((r) => sameOrg(r.id, id))) return;
|
|
79
|
+
const key = id.toLowerCase();
|
|
80
|
+
if (!extra.has(key))
|
|
81
|
+
extra.set(key, { id, label: id, default: isDefault, workspaces: [] });
|
|
82
|
+
};
|
|
83
|
+
const d = defaultOrg(ctx.config);
|
|
84
|
+
if (d) add(d, true);
|
|
85
|
+
for (const w of resolved)
|
|
86
|
+
if (w.org && local.has(w.workspace)) {
|
|
87
|
+
add(w.org, sameOrg(d, w.org));
|
|
88
|
+
extra.get(w.org.toLowerCase())?.workspaces.push(w.workspace);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
rows: [...listed, ...extra.values()],
|
|
92
|
+
warnings: resolved
|
|
93
|
+
.filter((w) => local.has(w.workspace))
|
|
94
|
+
.flatMap((w) => w.warnings),
|
|
95
|
+
resolved,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The GitHub org `register` lists repos from: `--org <id>`, else the
|
|
101
|
+
* workspace's resolved org. Never Den's own checkout or remote (SEC-0).
|
|
102
|
+
* Undefined when there is none, or it is not a GitHub owner name.
|
|
103
|
+
*/
|
|
104
|
+
export function registerOrg(
|
|
105
|
+
workspace: string,
|
|
106
|
+
orgArg?: string,
|
|
107
|
+
ctx?: OrgContext,
|
|
108
|
+
): string | undefined {
|
|
109
|
+
let org = orgArg?.trim();
|
|
110
|
+
if (!org) {
|
|
111
|
+
try {
|
|
112
|
+
org = githubOrgFor(workspace, ctx);
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(org) ? org : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Print the org warnings under a listing. */
|
|
121
|
+
export function printOrgWarnings(warnings: readonly string[]): void {
|
|
122
|
+
for (const w of warnings) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
123
|
+
if (warnings.length) console.log();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function cmdOrg(args: string[]): void {
|
|
127
|
+
const sub = args[0] ?? "list";
|
|
128
|
+
if (sub !== "list") {
|
|
129
|
+
console.log(chalk.red(`Usage: ${product().cli} org list [--org <id>]`));
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const only = orgFlag(args.slice(1));
|
|
134
|
+
const { rows, warnings, resolved } = orgListRows();
|
|
135
|
+
const shown = only ? rows.filter((r) => sameOrg(r.id, only)) : rows;
|
|
136
|
+
const table = newTable({
|
|
137
|
+
head: ["Org", "Label", "Default", "Workspaces"],
|
|
138
|
+
style: { head: ["cyan"] },
|
|
139
|
+
});
|
|
140
|
+
for (const r of shown)
|
|
141
|
+
table.push([
|
|
142
|
+
chalk.bold(r.id),
|
|
143
|
+
r.label,
|
|
144
|
+
r.default ? chalk.green("✓") : "",
|
|
145
|
+
r.workspaces.join(", ") || chalk.gray("none"),
|
|
146
|
+
]);
|
|
147
|
+
console.log(table.toString());
|
|
148
|
+
if (only && shown.length === 0)
|
|
149
|
+
console.log(chalk.yellow(` No org ${only}.`));
|
|
150
|
+
const inScope = only
|
|
151
|
+
? new Set(
|
|
152
|
+
resolved.filter((w) => sameOrg(w.org, only)).map((w) => w.workspace),
|
|
153
|
+
)
|
|
154
|
+
: null;
|
|
155
|
+
printOrgWarnings(
|
|
156
|
+
inScope
|
|
157
|
+
? warnings.filter((w) =>
|
|
158
|
+
[...inScope].some((n) => w.startsWith(`workspace ${n}:`)),
|
|
159
|
+
)
|
|
160
|
+
: warnings,
|
|
161
|
+
);
|
|
162
|
+
}
|