@monster-orange/den 0.2.6 → 0.2.8
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 +225 -0
- package/bin/den +9 -0
- package/docs/distributions.md +205 -6
- package/package.json +1 -1
- package/service/install.sh +41 -2
- package/src/shared/agent-actor.ts +38 -3
- package/src/shared/config-error.ts +33 -0
- package/src/shared/den-config.ts +84 -5
- package/src/shared/environment-manager/catalog.ts +61 -3
- package/src/shared/environment-manager/default-workspace.ts +53 -14
- package/src/shared/environment-manager/git-remote.ts +49 -0
- 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 +69 -26
- package/src/shared/environment-manager/workspace-org.ts +279 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/org-sources.ts +309 -0
- package/src/shared/types/index.ts +19 -2
- package/src/ui/cli/clone-plan.ts +374 -0
- package/src/ui/cli/flag-parser.ts +57 -3
- package/src/ui/cli/index.ts +346 -48
- package/src/ui/cli/org-command.ts +172 -0
- package/src/ui/cli/results-viewer.ts +5 -5
- package/src/ui/cli/table.ts +20 -0
- package/src/ui/cli/up-args.ts +15 -0
- package/src/ui/cli/validate-all.ts +171 -0
- package/src/ui/mcp/scope.ts +277 -0
- package/src/ui/mcp/server.ts +94 -9
- package/src/ui/mcp/tools.ts +101 -17
- package/src/ui/server/audit.ts +90 -3
- package/src/ui/server/daemon-reaper.ts +22 -4
- package/src/ui/server/github-service.ts +6 -1
- package/src/ui/server/index.ts +96 -5
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/readonly-routes.ts +12 -3
- package/src/ui/server/release-service.ts +40 -5
- package/src/ui/server/workspace-routes.ts +13 -6
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";
|
|
@@ -31,10 +32,12 @@ import { moduleScanner } from "../../shared/test-discovery/module-scanner.js";
|
|
|
31
32
|
import { resolvePerformanceModules } from "./module-selection.js";
|
|
32
33
|
import {
|
|
33
34
|
readCatalog,
|
|
34
|
-
|
|
35
|
+
needsGhRepair,
|
|
36
|
+
resolveRepoCloneSource,
|
|
35
37
|
upsertCatalogEntry,
|
|
36
38
|
} from "../../shared/environment-manager/catalog.js";
|
|
37
39
|
import { seedSecretsFromSibling } from "../../shared/environment-manager/secrets-check.js";
|
|
40
|
+
import { upNeedsTarget } from "./up-args.js";
|
|
38
41
|
import {
|
|
39
42
|
readWorkspacesFile,
|
|
40
43
|
writeWorkspacesFile,
|
|
@@ -108,6 +111,7 @@ import {
|
|
|
108
111
|
} from "../../shared/environment-manager/service-runner.js";
|
|
109
112
|
import { getGitHubCoords } from "../../shared/smoke/git-remote.js";
|
|
110
113
|
import {
|
|
114
|
+
assertDenConfigValid,
|
|
111
115
|
getReposRoot,
|
|
112
116
|
mergeUserConfig,
|
|
113
117
|
readDenConfig,
|
|
@@ -139,7 +143,10 @@ import {
|
|
|
139
143
|
} from "../../shared/environment-manager/up-plan.js";
|
|
140
144
|
import { execSync } from "child_process";
|
|
141
145
|
import * as fs from "fs";
|
|
142
|
-
import {
|
|
146
|
+
import {
|
|
147
|
+
heldManifestsDir,
|
|
148
|
+
readManifest,
|
|
149
|
+
} from "../../shared/manifest/read-manifest.js";
|
|
143
150
|
import * as nodePath from "path";
|
|
144
151
|
import { runInit } from "./init.js";
|
|
145
152
|
import {
|
|
@@ -168,11 +175,26 @@ import { cmdConfig } from "./config-command.js";
|
|
|
168
175
|
import { cmdEdge } from "./edge-command.js";
|
|
169
176
|
import { cmdService } from "./service-command.js";
|
|
170
177
|
import { cmdStack } from "./stack-command.js";
|
|
178
|
+
import {
|
|
179
|
+
cmdOrg,
|
|
180
|
+
orgFlag,
|
|
181
|
+
orgListRows,
|
|
182
|
+
printOrgWarnings,
|
|
183
|
+
registerOrg,
|
|
184
|
+
sameOrg,
|
|
185
|
+
} from "./org-command.js";
|
|
186
|
+
import {
|
|
187
|
+
buildSetupPlan,
|
|
188
|
+
renderClonePlan,
|
|
189
|
+
workspaceClonePlan,
|
|
190
|
+
} from "./clone-plan.js";
|
|
191
|
+
import { githubOrgFor } from "../../shared/environment-manager/workspace-org.js";
|
|
171
192
|
import { cmdWeb, displayWebHelp } from "./web-command.js";
|
|
172
193
|
import { cmdGraph } from "./graph-command.js";
|
|
173
194
|
import { resolveLogsTarget, tailServiceLogs } from "./logs-command.js";
|
|
174
195
|
import {
|
|
175
196
|
parseCliArgs,
|
|
197
|
+
takeWorkspaceFlags,
|
|
176
198
|
unknownCommandFlags,
|
|
177
199
|
workspaceFlagRequiresActive,
|
|
178
200
|
type ParsedCliArgs,
|
|
@@ -205,8 +227,11 @@ const WORKSPACE_SUBCOMMANDS: {
|
|
|
205
227
|
}[] = [
|
|
206
228
|
{
|
|
207
229
|
name: "list",
|
|
208
|
-
usage: "list",
|
|
209
|
-
lines: [
|
|
230
|
+
usage: "list [--org <id>]",
|
|
231
|
+
lines: [
|
|
232
|
+
"every workspace, the active set, the focus and its org;",
|
|
233
|
+
"--org <id> lists one org's workspaces",
|
|
234
|
+
],
|
|
210
235
|
},
|
|
211
236
|
{
|
|
212
237
|
name: "activate",
|
|
@@ -294,19 +319,35 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
|
|
|
294
319
|
lines: ["List all registered services in the catalog."],
|
|
295
320
|
},
|
|
296
321
|
register: {
|
|
297
|
-
usage: "register [path]",
|
|
322
|
+
usage: "register [path] [--org <id>] [-w <workspace>]",
|
|
298
323
|
lines: [
|
|
299
324
|
"With a path: add that checkout (dev-managed) under its manifest name.",
|
|
300
|
-
"Without one: pick the repo interactively.",
|
|
325
|
+
"Without one: pick the repo interactively. A managed clone is picked",
|
|
326
|
+
"from the focused workspace's org; --org <id> picks from another.",
|
|
327
|
+
"-w writes that workspace (active or not) and leaves focus alone.",
|
|
301
328
|
],
|
|
302
329
|
},
|
|
303
330
|
setup: {
|
|
304
|
-
usage:
|
|
331
|
+
usage:
|
|
332
|
+
"setup [service...] [--force|-f] [--plan [-w <workspace>]... [--all-active]]",
|
|
305
333
|
lines: [
|
|
306
334
|
"Run 'make setup' once per system (clones missing managed repos first).",
|
|
307
335
|
"Default targets MANAGED clones only — setup's codegen/installs",
|
|
308
336
|
"shouldn't touch your own dev-managed checkouts uninvited.",
|
|
309
337
|
"--force (-f) includes dev-managed repos; naming a service always works.",
|
|
338
|
+
"--plan prints each repo's org and clone URL and stops: nothing is",
|
|
339
|
+
"cloned, seeded or written (on a fresh machine: the blueprint's plan).",
|
|
340
|
+
"It plans the focused workspace; -w <workspace> (repeatable) plans those,",
|
|
341
|
+
"--all-active every active one. Repos only other workspaces hold are named.",
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
org: {
|
|
345
|
+
usage: "org list [--org <id>]",
|
|
346
|
+
lines: [
|
|
347
|
+
"Every org: its label, its workspaces, whether it is the default",
|
|
348
|
+
"(github_org), and any org warnings. A workspace's org is its own org:,",
|
|
349
|
+
"then the blueprint's org:, then its repos' one catalog owner, then",
|
|
350
|
+
"github_org.",
|
|
310
351
|
],
|
|
311
352
|
},
|
|
312
353
|
sync: {
|
|
@@ -439,6 +480,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
439
480
|
`use-managed`,
|
|
440
481
|
"setup",
|
|
441
482
|
"workspace",
|
|
483
|
+
"org",
|
|
442
484
|
"sync",
|
|
443
485
|
"init",
|
|
444
486
|
"validate",
|
|
@@ -453,6 +495,27 @@ const TELEMETRY_COMMANDS = new Set<string>([
|
|
|
453
495
|
"service",
|
|
454
496
|
]);
|
|
455
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Commands that run with a config that fails validation: the web and MCP
|
|
500
|
+
* servers (each route or tool reports it), and those that read neither orgs,
|
|
501
|
+
* the catalog nor clones (manifest validate, create, the per-engineer
|
|
502
|
+
* config and local-dev toggles, saved results). Every other registered
|
|
503
|
+
* command checks the config first.
|
|
504
|
+
*/
|
|
505
|
+
const CONFIG_FREE_COMMANDS = new Set<string>([
|
|
506
|
+
"web",
|
|
507
|
+
"mcp",
|
|
508
|
+
"validate",
|
|
509
|
+
"create",
|
|
510
|
+
"config",
|
|
511
|
+
"local-dev",
|
|
512
|
+
"view-results",
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
function configCheckedCommand(command: string): boolean {
|
|
516
|
+
return TELEMETRY_COMMANDS.has(command) && !CONFIG_FREE_COMMANDS.has(command);
|
|
517
|
+
}
|
|
518
|
+
|
|
456
519
|
/** Map a raw command token to a PII-safe, registered telemetry command name. */
|
|
457
520
|
function telemetryCommandName(command?: string): string {
|
|
458
521
|
if (!command) return "none";
|
|
@@ -675,6 +738,12 @@ class DenCLI {
|
|
|
675
738
|
}
|
|
676
739
|
|
|
677
740
|
try {
|
|
741
|
+
// A config that fails validation stops every command that touches
|
|
742
|
+
// orgs, the catalog, clones or releases before it reads or writes
|
|
743
|
+
// anything: some (sync over existing clones, register, workspace
|
|
744
|
+
// create) never read `orgs:` and used to carry on (ORG-1d).
|
|
745
|
+
if (command && configCheckedCommand(command)) assertDenConfigValid();
|
|
746
|
+
|
|
678
747
|
switch (command) {
|
|
679
748
|
case "run":
|
|
680
749
|
await this.runTestsCommand(args.slice(1));
|
|
@@ -745,6 +814,10 @@ class DenCLI {
|
|
|
745
814
|
await this.cmdWorkspace(args.slice(1));
|
|
746
815
|
break;
|
|
747
816
|
|
|
817
|
+
case "org":
|
|
818
|
+
cmdOrg(args.slice(1));
|
|
819
|
+
break;
|
|
820
|
+
|
|
748
821
|
case "sync":
|
|
749
822
|
await this.cmdSync(args.slice(1));
|
|
750
823
|
break;
|
|
@@ -752,9 +825,23 @@ class DenCLI {
|
|
|
752
825
|
case "init": {
|
|
753
826
|
// In the den root, `init` seeds/resets the default workspace; pointed
|
|
754
827
|
// at a service repo it scaffolds that repo's .den.yml (manifest init).
|
|
755
|
-
const initTarget =
|
|
756
|
-
|
|
757
|
-
|
|
828
|
+
const initTarget =
|
|
829
|
+
args.slice(1).find((a) => !a.startsWith("-")) ?? getInvokeCwd();
|
|
830
|
+
// Compare real paths: a symlinked tmp or home (/var → /private/var)
|
|
831
|
+
// must not turn a seed into a manifest scaffold.
|
|
832
|
+
const real = (p: string) => {
|
|
833
|
+
try {
|
|
834
|
+
return fs.realpathSync(nodePath.resolve(p));
|
|
835
|
+
} catch {
|
|
836
|
+
return nodePath.resolve(p);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
if (real(initTarget) === real(getDistRoot())) {
|
|
840
|
+
await this.seedDefaultWorkspaceFlow(
|
|
841
|
+
"init",
|
|
842
|
+
args.slice(1).includes("--reset"),
|
|
843
|
+
"init",
|
|
844
|
+
);
|
|
758
845
|
} else {
|
|
759
846
|
await runInit(initTarget);
|
|
760
847
|
}
|
|
@@ -1088,7 +1175,15 @@ class DenCLI {
|
|
|
1088
1175
|
if (!catalogEntry) continue;
|
|
1089
1176
|
const branch = wsEntry.branch ?? "main";
|
|
1090
1177
|
try {
|
|
1091
|
-
|
|
1178
|
+
// An entry with `gh: ""` clones from the workspace's org, as `up`
|
|
1179
|
+
// does (and `setup --plan` says), instead of failing on an empty slug.
|
|
1180
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
1181
|
+
const ghSlug = resolveRepoCloneSource(repoName, catalog, () =>
|
|
1182
|
+
githubOrgFor(ws.name),
|
|
1183
|
+
);
|
|
1184
|
+
if (needsGhRepair(catalogEntry, ghSlug))
|
|
1185
|
+
upsertCatalogEntry(repoName, { ...catalogEntry, gh: ghSlug });
|
|
1186
|
+
ensureManagedServiceClone(repoName, ghSlug, branch);
|
|
1092
1187
|
} catch (err) {
|
|
1093
1188
|
console.log(
|
|
1094
1189
|
chalk.red(
|
|
@@ -1189,8 +1284,12 @@ class DenCLI {
|
|
|
1189
1284
|
// Catalog entries written by the dev-managed add flow carry `gh: ""`, so
|
|
1190
1285
|
// an entry can exist yet be unclonable. Derive the conventional slug and
|
|
1191
1286
|
// persist the repair rather than cloning from an empty URL.
|
|
1192
|
-
|
|
1193
|
-
|
|
1287
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
1288
|
+
const ghSlug = resolveRepoCloneSource(repoName, catalog, () =>
|
|
1289
|
+
githubOrgFor(ws.name),
|
|
1290
|
+
);
|
|
1291
|
+
if (needsGhRepair(catalog[repoName], ghSlug))
|
|
1292
|
+
upsertCatalogEntry(repoName, { ...catalog[repoName], gh: ghSlug });
|
|
1194
1293
|
try {
|
|
1195
1294
|
// runSetup=false: clone only — setup is run below, hidden + non-interactive.
|
|
1196
1295
|
ensureManagedServiceClone(repoName, ghSlug, branch, false);
|
|
@@ -1427,6 +1526,8 @@ class DenCLI {
|
|
|
1427
1526
|
* in order, and run nothing. Read-only on every path.
|
|
1428
1527
|
*/
|
|
1429
1528
|
dryRun?: boolean;
|
|
1529
|
+
/** `--plan`: a preview that would run nothing exits 1. */
|
|
1530
|
+
plan?: boolean;
|
|
1430
1531
|
},
|
|
1431
1532
|
): Promise<void> {
|
|
1432
1533
|
// How long to show startup output before handing control back to the menu
|
|
@@ -1538,6 +1639,7 @@ class DenCLI {
|
|
|
1538
1639
|
);
|
|
1539
1640
|
}
|
|
1540
1641
|
if (previews.some((p) => p.kind === "error")) process.exitCode = 1;
|
|
1642
|
+
if (opts?.plan && runs.length === 0) process.exitCode = 1;
|
|
1541
1643
|
console.log(chalk.gray(` (--dry-run: not ${doing} anything)\n`));
|
|
1542
1644
|
return;
|
|
1543
1645
|
}
|
|
@@ -1892,6 +1994,25 @@ class DenCLI {
|
|
|
1892
1994
|
return seed && knownRepos.has(seed) ? seed : undefined;
|
|
1893
1995
|
}
|
|
1894
1996
|
|
|
1997
|
+
/**
|
|
1998
|
+
* `up --plan`'s clone half (ORG-1a): each repo's org and the URL a missing
|
|
1999
|
+
* managed clone would come from. Read-only, like the rest of the plan.
|
|
2000
|
+
*/
|
|
2001
|
+
private renderUpClonePlan(repos: readonly string[]): void {
|
|
2002
|
+
const ws = getActiveWorkspace();
|
|
2003
|
+
if (!ws) return;
|
|
2004
|
+
const lines = renderClonePlan(
|
|
2005
|
+
{
|
|
2006
|
+
fromBlueprint: false,
|
|
2007
|
+
workspaces: [workspaceClonePlan(ws.name, repos)],
|
|
2008
|
+
},
|
|
2009
|
+
product().cli,
|
|
2010
|
+
);
|
|
2011
|
+
console.log(chalk.bold(" Repos and clone URLs:\n"));
|
|
2012
|
+
for (const line of lines) console.log(chalk.gray(` ${line}`.trimEnd()));
|
|
2013
|
+
console.log();
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1895
2016
|
/** Render the read-only `den up --plan` view: per-node command + flags. */
|
|
1896
2017
|
private renderUpPlan(nodes: NodePlan[]): void {
|
|
1897
2018
|
const tag = (n: NodePlan): string => {
|
|
@@ -1962,6 +2083,7 @@ class DenCLI {
|
|
|
1962
2083
|
`\n ${chalk.bold(seed)} has no derived dependencies to start.\n`,
|
|
1963
2084
|
),
|
|
1964
2085
|
);
|
|
2086
|
+
if (plan) process.exitCode = 1;
|
|
1965
2087
|
return;
|
|
1966
2088
|
}
|
|
1967
2089
|
// Systems the user toggled to auto-start OFF in this workspace are not
|
|
@@ -2005,6 +2127,7 @@ class DenCLI {
|
|
|
2005
2127
|
this.renderUpPlan(
|
|
2006
2128
|
buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
|
|
2007
2129
|
);
|
|
2130
|
+
this.renderUpClonePlan(rawOrder);
|
|
2008
2131
|
return;
|
|
2009
2132
|
}
|
|
2010
2133
|
if (dryRun) {
|
|
@@ -2202,6 +2325,7 @@ class DenCLI {
|
|
|
2202
2325
|
"\n No services in the active workspace — nothing to start.\n",
|
|
2203
2326
|
),
|
|
2204
2327
|
);
|
|
2328
|
+
if (plan) process.exitCode = 1;
|
|
2205
2329
|
return;
|
|
2206
2330
|
}
|
|
2207
2331
|
const { rawOrder, graph } = target;
|
|
@@ -2216,6 +2340,7 @@ class DenCLI {
|
|
|
2216
2340
|
this.renderUpPlan(
|
|
2217
2341
|
buildUpPlan(graph, rawOrder, { noAutostart: noAutostartSet }),
|
|
2218
2342
|
);
|
|
2343
|
+
this.renderUpClonePlan(rawOrder);
|
|
2219
2344
|
return;
|
|
2220
2345
|
}
|
|
2221
2346
|
if (dryRun) {
|
|
@@ -2494,6 +2619,8 @@ class DenCLI {
|
|
|
2494
2619
|
*/
|
|
2495
2620
|
private async seedDefaultWorkspaceFlow(
|
|
2496
2621
|
reason: "auto" | "init",
|
|
2622
|
+
reset = false,
|
|
2623
|
+
via: "init" | "workspace seed" = "workspace seed",
|
|
2497
2624
|
): Promise<boolean> {
|
|
2498
2625
|
const bps = blueprintWorkspaces();
|
|
2499
2626
|
if (bps.length === 0) {
|
|
@@ -2520,6 +2647,23 @@ class DenCLI {
|
|
|
2520
2647
|
}
|
|
2521
2648
|
const willReset = defaultWorkspaceHasServices();
|
|
2522
2649
|
const interactive = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
2650
|
+
// A reset replaces every entry, dev-managed checkouts included. On a TTY
|
|
2651
|
+
// the user is asked (default No); without one there is nobody to ask, so
|
|
2652
|
+
// only an explicit --reset may do it (SEED-0).
|
|
2653
|
+
if (willReset && !interactive && !reset) {
|
|
2654
|
+
const what = multi
|
|
2655
|
+
? "the blueprint's workspaces"
|
|
2656
|
+
: "the default workspace";
|
|
2657
|
+
console.error(
|
|
2658
|
+
chalk.red(
|
|
2659
|
+
`\n ❌ Not resetting ${what}: it has entries, and a reset replaces them all\n` +
|
|
2660
|
+
` (your own dev-managed checkouts included) with managed clones.\n` +
|
|
2661
|
+
` Nothing changed. To reset anyway: ${product().cli} ${via} --reset\n`,
|
|
2662
|
+
),
|
|
2663
|
+
);
|
|
2664
|
+
process.exitCode = 1;
|
|
2665
|
+
return false;
|
|
2666
|
+
}
|
|
2523
2667
|
|
|
2524
2668
|
for (const bp of bps) {
|
|
2525
2669
|
const label = multi ? `Workspace ${bp.name}` : "Default workspace";
|
|
@@ -2613,10 +2757,10 @@ class DenCLI {
|
|
|
2613
2757
|
}
|
|
2614
2758
|
|
|
2615
2759
|
private async cmdServiceUp(args: string[]): Promise<void> {
|
|
2616
|
-
await this.ensureWorkspaceSeeded();
|
|
2617
2760
|
// Tokens after a `--` terminator belong to a child command — they must
|
|
2618
2761
|
// never be read as up's own flags (`den up -- -a` is NOT `up -a`).
|
|
2619
2762
|
const terminator = args.indexOf("--");
|
|
2763
|
+
await this.ensureWorkspaceSeeded();
|
|
2620
2764
|
const { app, rest } = takeAppFlag(
|
|
2621
2765
|
terminator === -1 ? args : args.slice(0, terminator),
|
|
2622
2766
|
);
|
|
@@ -2680,6 +2824,7 @@ class DenCLI {
|
|
|
2680
2824
|
await this.cmdServiceUpManifest(
|
|
2681
2825
|
ownArgs.filter((a) => !a.startsWith("-")),
|
|
2682
2826
|
dryRun || plan,
|
|
2827
|
+
plan,
|
|
2683
2828
|
);
|
|
2684
2829
|
}
|
|
2685
2830
|
|
|
@@ -2687,6 +2832,7 @@ class DenCLI {
|
|
|
2687
2832
|
private async cmdServiceUpManifest(
|
|
2688
2833
|
addresses: string[],
|
|
2689
2834
|
dryRun = false,
|
|
2835
|
+
plan = false,
|
|
2690
2836
|
): Promise<void> {
|
|
2691
2837
|
// Resolve targets BEFORE the secrets preflight — otherwise running
|
|
2692
2838
|
// `den up` from a non-service dir (e.g. the den repo root) triggers
|
|
@@ -2705,6 +2851,16 @@ class DenCLI {
|
|
|
2705
2851
|
` or run \`${product().cli} graph\` to see the workspace.\n`,
|
|
2706
2852
|
),
|
|
2707
2853
|
);
|
|
2854
|
+
} else if (process.env.DEN_WORKSPACE_OVERRIDE) {
|
|
2855
|
+
// `up -w <ws>` with nothing to start: -w picks the workspace, not
|
|
2856
|
+
// what in it to start. A usage error, not an empty plan.
|
|
2857
|
+
console.error(
|
|
2858
|
+
chalk.red(
|
|
2859
|
+
`\n ${upNeedsTarget(product().cli, process.env.DEN_WORKSPACE_OVERRIDE, plan)}\n`,
|
|
2860
|
+
),
|
|
2861
|
+
);
|
|
2862
|
+
process.exitCode = 2;
|
|
2863
|
+
return;
|
|
2708
2864
|
} else {
|
|
2709
2865
|
console.log(
|
|
2710
2866
|
chalk.yellow(
|
|
@@ -2712,12 +2868,15 @@ class DenCLI {
|
|
|
2712
2868
|
),
|
|
2713
2869
|
);
|
|
2714
2870
|
}
|
|
2871
|
+
// A plan that resolves nothing is a failed plan (scripts check it).
|
|
2872
|
+
if (plan) process.exitCode = 1;
|
|
2715
2873
|
return;
|
|
2716
2874
|
}
|
|
2717
2875
|
if (!dryRun) preflightSecrets();
|
|
2718
2876
|
await this.runServicesCommand(targets, "up", undefined, {
|
|
2719
2877
|
named: addresses,
|
|
2720
2878
|
dryRun,
|
|
2879
|
+
plan,
|
|
2721
2880
|
});
|
|
2722
2881
|
}
|
|
2723
2882
|
|
|
@@ -2865,7 +3024,7 @@ class DenCLI {
|
|
|
2865
3024
|
chalk.cyan(`\n Workspace: ${chalk.bold(ws?.name ?? "none")}\n`),
|
|
2866
3025
|
);
|
|
2867
3026
|
|
|
2868
|
-
const table =
|
|
3027
|
+
const table = newTable({
|
|
2869
3028
|
head: ["Service", "Mode", "Branch", "Health", "Reason"],
|
|
2870
3029
|
style: { head: ["cyan"] },
|
|
2871
3030
|
});
|
|
@@ -3027,7 +3186,7 @@ class DenCLI {
|
|
|
3027
3186
|
return;
|
|
3028
3187
|
}
|
|
3029
3188
|
|
|
3030
|
-
const table =
|
|
3189
|
+
const table = newTable({
|
|
3031
3190
|
head: ["Service", "Mode", "Health", "Commands"],
|
|
3032
3191
|
style: { head: ["cyan"] },
|
|
3033
3192
|
});
|
|
@@ -3072,15 +3231,29 @@ class DenCLI {
|
|
|
3072
3231
|
private async cmdRegister(args: string[]): Promise<void> {
|
|
3073
3232
|
if (args.includes("-h") || args.includes("--help")) {
|
|
3074
3233
|
console.log(
|
|
3075
|
-
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>]\n\n` +
|
|
3234
|
+
`\n ${product().cli} register [path] [--name <repo>] [--gh <owner/repo>] [-w <workspace>]\n\n` +
|
|
3076
3235
|
" With a path: add that checkout to the catalog and the focused\n" +
|
|
3077
3236
|
" workspace as your own (dev-managed) copy, without prompts. The name\n" +
|
|
3078
3237
|
" defaults to the manifest's name, then the folder name; the GitHub\n" +
|
|
3079
|
-
" slug to the git remote
|
|
3238
|
+
" slug to the git remote. -w writes that workspace (active or not)\n" +
|
|
3239
|
+
" and leaves focus alone.\n" +
|
|
3080
3240
|
" Without a path: choose a local checkout or a managed clone interactively.\n",
|
|
3081
3241
|
);
|
|
3082
3242
|
return;
|
|
3083
3243
|
}
|
|
3244
|
+
const repoPathArg = args.find(
|
|
3245
|
+
(a, i) =>
|
|
3246
|
+
!a.startsWith("-") && !/^--(name|gh|org)$/.test(args[i - 1] ?? ""),
|
|
3247
|
+
);
|
|
3248
|
+
// Without a path, register prompts: with no terminal, say how to name one.
|
|
3249
|
+
if (!repoPathArg && !process.stdin.isTTY) {
|
|
3250
|
+
process.stderr.write(
|
|
3251
|
+
`register needs a terminal (stdin is not a TTY); without one: ` +
|
|
3252
|
+
`${product().cli} register <path> [--name <repo>] [--gh <owner/repo>] [-w <workspace>]\n`,
|
|
3253
|
+
);
|
|
3254
|
+
process.exitCode = 2;
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3084
3257
|
const ws = getActiveWorkspace();
|
|
3085
3258
|
if (!ws) {
|
|
3086
3259
|
console.log(
|
|
@@ -3091,9 +3264,6 @@ class DenCLI {
|
|
|
3091
3264
|
return;
|
|
3092
3265
|
}
|
|
3093
3266
|
|
|
3094
|
-
const repoPathArg = args.find(
|
|
3095
|
-
(a, i) => !a.startsWith("-") && !/^--(name|gh)$/.test(args[i - 1] ?? ""),
|
|
3096
|
-
);
|
|
3097
3267
|
if (repoPathArg) {
|
|
3098
3268
|
const flag = (n: string) => {
|
|
3099
3269
|
const i = args.indexOf(n);
|
|
@@ -3108,7 +3278,7 @@ class DenCLI {
|
|
|
3108
3278
|
if (source === "local") {
|
|
3109
3279
|
await this.registerLinked(ws);
|
|
3110
3280
|
} else {
|
|
3111
|
-
await this.registerManaged(ws);
|
|
3281
|
+
await this.registerManaged(ws, orgFlag(args));
|
|
3112
3282
|
}
|
|
3113
3283
|
}
|
|
3114
3284
|
|
|
@@ -3187,26 +3357,25 @@ class DenCLI {
|
|
|
3187
3357
|
);
|
|
3188
3358
|
}
|
|
3189
3359
|
|
|
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)) {
|
|
3360
|
+
/**
|
|
3361
|
+
* List an org's repos via `gh`, let the user pick one + branch → managed
|
|
3362
|
+
* service. The org is `--org <id>`, else the focused workspace's resolved
|
|
3363
|
+
* org (ORG-1a). Never Den's own checkout or remote (SEC-0).
|
|
3364
|
+
*/
|
|
3365
|
+
private async registerManaged(
|
|
3366
|
+
ws: {
|
|
3367
|
+
name: string;
|
|
3368
|
+
config: import("../../shared/types/index.js").WorkspaceConfig;
|
|
3369
|
+
},
|
|
3370
|
+
orgArg?: string,
|
|
3371
|
+
): Promise<void> {
|
|
3372
|
+
const org = registerOrg(ws.name, orgArg);
|
|
3373
|
+
if (!org) {
|
|
3207
3374
|
console.log(
|
|
3208
3375
|
chalk.red(
|
|
3209
|
-
`\n ❌ No GitHub org to list: set github_org in ${product().configFile}
|
|
3376
|
+
`\n ❌ No GitHub org to list: set github_org in ${product().configFile}, ` +
|
|
3377
|
+
`org: on workspace ${ws.name}, or pass --org <id> ` +
|
|
3378
|
+
`(an org with no GitHub source in orgs.<id>.sources has none)\n`,
|
|
3210
3379
|
),
|
|
3211
3380
|
);
|
|
3212
3381
|
return;
|
|
@@ -3459,10 +3628,39 @@ class DenCLI {
|
|
|
3459
3628
|
console.log(chalk.gray(` revert: ${revert}\n`));
|
|
3460
3629
|
}
|
|
3461
3630
|
|
|
3462
|
-
private async cmdSetup(
|
|
3631
|
+
private async cmdSetup(rawArgs: string[]): Promise<void> {
|
|
3632
|
+
// `-w <ws>` (repeatable) and `--all-active` pick the workspaces to plan.
|
|
3633
|
+
const { workspaces: planWs, rest: args } = takeWorkspaceFlags(rawArgs);
|
|
3634
|
+
const allActive = args.includes("--all-active");
|
|
3463
3635
|
const force = args.includes("--force") || args.includes("-f");
|
|
3464
3636
|
const names = args.filter((a) => !a.startsWith("-"));
|
|
3465
3637
|
|
|
3638
|
+
// `--plan`: each repo's org and clone URL, then stop. Read-only.
|
|
3639
|
+
if (args.includes("--plan")) {
|
|
3640
|
+
let plan: ReturnType<typeof buildSetupPlan>;
|
|
3641
|
+
try {
|
|
3642
|
+
plan = buildSetupPlan(names, { workspaces: planWs, allActive });
|
|
3643
|
+
} catch (e) {
|
|
3644
|
+
console.error(chalk.red(`\n ${(e as Error).message}\n`));
|
|
3645
|
+
process.exitCode = 1;
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
console.log(chalk.bold("\n Setup plan (no changes will be made):\n"));
|
|
3649
|
+
for (const line of renderClonePlan(plan, product().cli))
|
|
3650
|
+
console.log(` ${line}`.trimEnd());
|
|
3651
|
+
console.log();
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
if (planWs.length || allActive) {
|
|
3655
|
+
console.error(
|
|
3656
|
+
chalk.red(
|
|
3657
|
+
`\n -w and --all-active go with --plan: setup itself works on the focused workspace.\n`,
|
|
3658
|
+
),
|
|
3659
|
+
);
|
|
3660
|
+
process.exitCode = 1;
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3466
3664
|
// Clone any managed repos that aren't on disk yet before reading manifests.
|
|
3467
3665
|
this.cloneMissingManagedRepos();
|
|
3468
3666
|
|
|
@@ -3550,13 +3748,20 @@ class DenCLI {
|
|
|
3550
3748
|
|
|
3551
3749
|
switch (sub) {
|
|
3552
3750
|
case "list": {
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3751
|
+
const only = orgFlag(args.slice(1));
|
|
3752
|
+
const { resolved, warnings } = orgListRows();
|
|
3753
|
+
const orgOfWs = new Map(resolved.map((w) => [w.workspace, w.org]));
|
|
3754
|
+
const table = newTable({
|
|
3755
|
+
head: ["Workspace", "Org", "Active", "Focus", "Services", "Type"],
|
|
3555
3756
|
style: { head: ["cyan"] },
|
|
3556
3757
|
});
|
|
3557
|
-
|
|
3758
|
+
const shown = Object.entries(file.workspaces).filter(
|
|
3759
|
+
([name]) => !only || sameOrg(orgOfWs.get(name) ?? null, only),
|
|
3760
|
+
);
|
|
3761
|
+
for (const [name, cfg] of shown) {
|
|
3558
3762
|
table.push([
|
|
3559
3763
|
chalk.bold(name),
|
|
3764
|
+
orgOfWs.get(name) ?? chalk.gray("none"),
|
|
3560
3765
|
activeSet.has(name) ? chalk.green("✓") : "",
|
|
3561
3766
|
name === focus ? chalk.cyan("●") : "",
|
|
3562
3767
|
Object.keys(cfg.services).join(", ") || chalk.gray("none"),
|
|
@@ -3564,6 +3769,14 @@ class DenCLI {
|
|
|
3564
3769
|
]);
|
|
3565
3770
|
}
|
|
3566
3771
|
console.log(table.toString());
|
|
3772
|
+
if (only && shown.length === 0)
|
|
3773
|
+
console.log(chalk.yellow(` No workspace in org ${only}.`));
|
|
3774
|
+
const names = new Set(shown.map(([n]) => n));
|
|
3775
|
+
printOrgWarnings(
|
|
3776
|
+
warnings.filter((w) =>
|
|
3777
|
+
[...names].some((n) => w.startsWith(`workspace ${n}:`)),
|
|
3778
|
+
),
|
|
3779
|
+
);
|
|
3567
3780
|
break;
|
|
3568
3781
|
}
|
|
3569
3782
|
case "activate": {
|
|
@@ -3669,7 +3882,7 @@ class DenCLI {
|
|
|
3669
3882
|
break;
|
|
3670
3883
|
}
|
|
3671
3884
|
case "seed": {
|
|
3672
|
-
await this.seedDefaultWorkspaceFlow("init");
|
|
3885
|
+
await this.seedDefaultWorkspaceFlow("init", args.includes("--reset"));
|
|
3673
3886
|
break;
|
|
3674
3887
|
}
|
|
3675
3888
|
case "create": {
|
|
@@ -3862,7 +4075,15 @@ class DenCLI {
|
|
|
3862
4075
|
// ── den validate ────────────────────────────────────────────────────────
|
|
3863
4076
|
|
|
3864
4077
|
private async cmdValidate(args: string[]): Promise<void> {
|
|
3865
|
-
const
|
|
4078
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
4079
|
+
const held = args.includes("--held");
|
|
4080
|
+
// Bare `validate` in the distribution root (which has no manifest of its
|
|
4081
|
+
// own), or `--held`: every manifest the distribution uses.
|
|
4082
|
+
if (held || (positional.length === 0 && this.invokedInDistRoot())) {
|
|
4083
|
+
this.validateEverything(held);
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
const target = positional[0] ?? getInvokeCwd();
|
|
3866
4087
|
const fs = await import("fs");
|
|
3867
4088
|
const nodePath = await import("path");
|
|
3868
4089
|
|
|
@@ -3928,6 +4149,78 @@ class DenCLI {
|
|
|
3928
4149
|
if (!result.valid) process.exit(1);
|
|
3929
4150
|
}
|
|
3930
4151
|
|
|
4152
|
+
/** Whether the CLI was run from the distribution root (real paths). */
|
|
4153
|
+
private invokedInDistRoot(): boolean {
|
|
4154
|
+
const real = (p: string) => {
|
|
4155
|
+
try {
|
|
4156
|
+
return fs.realpathSync(nodePath.resolve(p));
|
|
4157
|
+
} catch {
|
|
4158
|
+
return nodePath.resolve(p);
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
return (
|
|
4162
|
+
real(getInvokeCwd()) === real(getDistRoot()) &&
|
|
4163
|
+
!fs.existsSync(nodePath.join(getDistRoot(), product().manifestYml))
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
/** `validate` (bare, in the root) / `validate --held`: see validate-all.ts. */
|
|
4168
|
+
private validateEverything(held: boolean): void {
|
|
4169
|
+
const results = validateAll({ held, validate: validateManifestLayers });
|
|
4170
|
+
console.log();
|
|
4171
|
+
if (results.length === 0) {
|
|
4172
|
+
console.log(
|
|
4173
|
+
chalk.yellow(
|
|
4174
|
+
held
|
|
4175
|
+
? ` No manifests in ${heldManifestsDir()}.`
|
|
4176
|
+
: " No repos in any workspace and no held manifests.",
|
|
4177
|
+
),
|
|
4178
|
+
);
|
|
4179
|
+
console.log();
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
const width = Math.max(...results.map((r) => r.repo.length));
|
|
4183
|
+
for (const r of results) {
|
|
4184
|
+
const where = r.checkout ?? "manifests/";
|
|
4185
|
+
const ws = r.workspaces.length ? ` [${r.workspaces.join(", ")}]` : "";
|
|
4186
|
+
const note = r.note ? ` (${r.note})` : "";
|
|
4187
|
+
const head = `${r.repo.padEnd(width)} ${r.source.padEnd(15)} ${where}${ws}${note}`;
|
|
4188
|
+
if (r.missing) {
|
|
4189
|
+
console.log(chalk.yellow(` ⚠ ${head}`));
|
|
4190
|
+
continue;
|
|
4191
|
+
}
|
|
4192
|
+
const warnings = r.result.layers.reduce(
|
|
4193
|
+
(n, l) => n + l.result.warnings.length,
|
|
4194
|
+
0,
|
|
4195
|
+
);
|
|
4196
|
+
if (r.result.valid && warnings === 0) {
|
|
4197
|
+
console.log(chalk.green(` ✓ ${head}`));
|
|
4198
|
+
continue;
|
|
4199
|
+
}
|
|
4200
|
+
console.log(
|
|
4201
|
+
r.result.valid
|
|
4202
|
+
? chalk.yellow(` ⚠ ${head}`)
|
|
4203
|
+
: chalk.red(` ✗ ${head}`),
|
|
4204
|
+
);
|
|
4205
|
+
for (const l of r.result.layers)
|
|
4206
|
+
if (!l.result.valid || l.result.warnings.length)
|
|
4207
|
+
printValidation(
|
|
4208
|
+
` ${l.file ?? "merged"}`,
|
|
4209
|
+
l.result,
|
|
4210
|
+
l.layer === "merged" ? "merged" : l.layer,
|
|
4211
|
+
);
|
|
4212
|
+
}
|
|
4213
|
+
const failed = results.filter((r) => !r.missing && !r.result.valid).length;
|
|
4214
|
+
const missing = results.filter((r) => r.missing).length;
|
|
4215
|
+
console.log();
|
|
4216
|
+
console.log(
|
|
4217
|
+
` ${plural(results.length, held ? "manifest" : "repo")}: ${failed ? chalk.red(`${failed} failed`) : chalk.green("all valid")}` +
|
|
4218
|
+
(missing ? chalk.yellow(`, ${missing} with no manifest to check`) : ""),
|
|
4219
|
+
);
|
|
4220
|
+
console.log();
|
|
4221
|
+
if (validateAllFailed(results)) process.exitCode = 1;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
3931
4224
|
// ── den local-dev ───────────────────────────────────────────────────────
|
|
3932
4225
|
|
|
3933
4226
|
private async cmdLocalDev(arg?: string): Promise<void> {
|
|
@@ -4008,7 +4301,9 @@ class DenCLI {
|
|
|
4008
4301
|
" register [path] Register a repo in the service catalog",
|
|
4009
4302
|
);
|
|
4010
4303
|
console.log();
|
|
4011
|
-
console.log(
|
|
4304
|
+
console.log(
|
|
4305
|
+
" setup [service...] Run make setup for services (--plan: orgs + clone URLs, no changes)",
|
|
4306
|
+
);
|
|
4012
4307
|
console.log(" sync [service...] Check for updates and offer to pull");
|
|
4013
4308
|
console.log(
|
|
4014
4309
|
" create <name> [dir] Scaffold a new distribution of your own (--title, --cli, --env-prefix)",
|
|
@@ -4039,6 +4334,9 @@ class DenCLI {
|
|
|
4039
4334
|
console.log(
|
|
4040
4335
|
" workspace code [n] Open workspace in VS Code (generates .code-workspace)",
|
|
4041
4336
|
);
|
|
4337
|
+
console.log(
|
|
4338
|
+
" org list [--org <id>] List orgs: label, workspaces, default, warnings",
|
|
4339
|
+
);
|
|
4042
4340
|
console.log("\nTest commands:");
|
|
4043
4341
|
console.log(
|
|
4044
4342
|
" menu Open the interactive menu (pillar/module/env picker)",
|