@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> setup --plan` and the clone half of `<cli> up --plan` (ORG-1a):
|
|
3
|
+
* each repo's resolved org and the URL it would be cloned from. Read-only:
|
|
4
|
+
* nothing is cloned, seeded or written (the catalog included).
|
|
5
|
+
*
|
|
6
|
+
* On a fresh machine (no workspace set up yet, a blueprint in the config)
|
|
7
|
+
* the plan is what `<cli> workspace seed` and then setup would do, for every
|
|
8
|
+
* blueprint workspace. Otherwise it is the focused workspace's repos, the
|
|
9
|
+
* ones setup works on.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import { readCatalog } from "../../shared/environment-manager/catalog.js";
|
|
14
|
+
import {
|
|
15
|
+
blueprintWorkspaces,
|
|
16
|
+
catalogFill,
|
|
17
|
+
isFreshWorkspaceSetup,
|
|
18
|
+
isMultiBlueprint,
|
|
19
|
+
} from "../../shared/environment-manager/default-workspace.js";
|
|
20
|
+
import {
|
|
21
|
+
DEN_SERVICES_DIR,
|
|
22
|
+
getActiveWorkspace,
|
|
23
|
+
parseAddress,
|
|
24
|
+
} from "../../shared/environment-manager/workspaces.js";
|
|
25
|
+
import { ghCloneUrl } from "../../shared/environment-manager/workspace-manager.js";
|
|
26
|
+
import {
|
|
27
|
+
githubOrgFor,
|
|
28
|
+
readOrgContext,
|
|
29
|
+
resolveWorkspaceOrg,
|
|
30
|
+
type OrgContext,
|
|
31
|
+
type OrgSource,
|
|
32
|
+
} from "../../shared/environment-manager/workspace-org.js";
|
|
33
|
+
import { orgOf } from "../../shared/manifest/service-links.js";
|
|
34
|
+
import type { ServiceCatalog } from "../../shared/types/index.js";
|
|
35
|
+
|
|
36
|
+
export type ClonePlanState =
|
|
37
|
+
| "cloned"
|
|
38
|
+
| "would-clone"
|
|
39
|
+
| "not-in-catalog"
|
|
40
|
+
| "checkout"
|
|
41
|
+
| "no-org";
|
|
42
|
+
|
|
43
|
+
export interface ClonePlanRow {
|
|
44
|
+
repo: string;
|
|
45
|
+
mode: "managed" | "dev-managed";
|
|
46
|
+
branch: string;
|
|
47
|
+
/** The GitHub owner the repo clones from (its slug's owner), or null. */
|
|
48
|
+
owner: string | null;
|
|
49
|
+
url: string | null;
|
|
50
|
+
state: ClonePlanState;
|
|
51
|
+
/** A dev-managed checkout's path, or a managed clone's directory. */
|
|
52
|
+
path?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ClonePlanWorkspace {
|
|
56
|
+
name: string;
|
|
57
|
+
org: string | null;
|
|
58
|
+
source: OrgSource | null;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
rows: ClonePlanRow[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ClonePlan {
|
|
64
|
+
/** True when planned from the blueprint (nothing seeded yet). */
|
|
65
|
+
fromBlueprint: boolean;
|
|
66
|
+
workspaces: ClonePlanWorkspace[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ownerOf = (slug: string): string => slug.slice(0, slug.indexOf("/"));
|
|
70
|
+
|
|
71
|
+
function cloneDir(repo: string, branch: string): string {
|
|
72
|
+
return path.join(DEN_SERVICES_DIR, repo, branch);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The slug setup and up would clone `repo` from, or null without an org. */
|
|
76
|
+
function slugFor(
|
|
77
|
+
repo: string,
|
|
78
|
+
workspace: string,
|
|
79
|
+
catalog: ServiceCatalog,
|
|
80
|
+
ctx: OrgContext,
|
|
81
|
+
): string | null {
|
|
82
|
+
const gh = catalog[repo]?.gh?.trim();
|
|
83
|
+
if (gh) return gh;
|
|
84
|
+
try {
|
|
85
|
+
return `${githubOrgFor(workspace, ctx)}/${repo}`;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function managedRow(
|
|
92
|
+
repo: string,
|
|
93
|
+
branch: string,
|
|
94
|
+
slug: string | null,
|
|
95
|
+
inCatalog: boolean,
|
|
96
|
+
): ClonePlanRow {
|
|
97
|
+
const dir = cloneDir(repo, branch);
|
|
98
|
+
const cloned = fs.existsSync(dir);
|
|
99
|
+
return {
|
|
100
|
+
repo,
|
|
101
|
+
mode: "managed",
|
|
102
|
+
branch,
|
|
103
|
+
owner: slug ? ownerOf(slug) : null,
|
|
104
|
+
url: slug ? ghCloneUrl(slug) : null,
|
|
105
|
+
state: cloned
|
|
106
|
+
? "cloned"
|
|
107
|
+
: !slug
|
|
108
|
+
? "no-org"
|
|
109
|
+
: inCatalog
|
|
110
|
+
? "would-clone"
|
|
111
|
+
: "not-in-catalog",
|
|
112
|
+
path: dir,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The plan for one workspace on this machine, limited to `repos` when given. */
|
|
117
|
+
export function workspaceClonePlan(
|
|
118
|
+
name: string,
|
|
119
|
+
repos?: readonly string[],
|
|
120
|
+
ctx: OrgContext = readOrgContext(),
|
|
121
|
+
): ClonePlanWorkspace {
|
|
122
|
+
const resolved = resolveWorkspaceOrg(name, ctx);
|
|
123
|
+
const ws = ctx.file.workspaces[name];
|
|
124
|
+
const want = repos ? new Set(repos) : null;
|
|
125
|
+
const rows: ClonePlanRow[] = [];
|
|
126
|
+
const seen = new Set<string>();
|
|
127
|
+
for (const [address, entry] of Object.entries(ws?.services ?? {})) {
|
|
128
|
+
const { repoName } = parseAddress(address, ctx.catalog);
|
|
129
|
+
if (seen.has(repoName)) continue;
|
|
130
|
+
if (want && !want.has(repoName) && !want.has(address)) continue;
|
|
131
|
+
seen.add(repoName);
|
|
132
|
+
const branch = entry.branch ?? "main";
|
|
133
|
+
if (entry.mode === "dev-managed") {
|
|
134
|
+
const owner = orgOf(repoName, ctx.catalog);
|
|
135
|
+
rows.push({
|
|
136
|
+
repo: repoName,
|
|
137
|
+
mode: "dev-managed",
|
|
138
|
+
branch,
|
|
139
|
+
owner: owner || null,
|
|
140
|
+
url: null,
|
|
141
|
+
state: "checkout",
|
|
142
|
+
...(entry.path ? { path: entry.path } : {}),
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
rows.push(
|
|
147
|
+
managedRow(
|
|
148
|
+
repoName,
|
|
149
|
+
branch,
|
|
150
|
+
slugFor(repoName, name, ctx.catalog, ctx),
|
|
151
|
+
Boolean(ctx.catalog[repoName]),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return { ...resolved, name, rows };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** What `<cli> workspace seed` and then setup would clone, per blueprint workspace. */
|
|
159
|
+
function blueprintClonePlan(ctx: OrgContext): ClonePlanWorkspace[] {
|
|
160
|
+
const multi = isMultiBlueprint();
|
|
161
|
+
const bps = blueprintWorkspaces().map((b) => ({
|
|
162
|
+
...b,
|
|
163
|
+
name: multi ? b.name : "default",
|
|
164
|
+
}));
|
|
165
|
+
let fill = new Map<string, string>();
|
|
166
|
+
try {
|
|
167
|
+
fill = catalogFill(bps, ctx.catalog, ctx);
|
|
168
|
+
} catch {
|
|
169
|
+
// No org for some repo: those rows say so below.
|
|
170
|
+
}
|
|
171
|
+
return bps.map((bp) => ({
|
|
172
|
+
...resolveWorkspaceOrg(bp.name, ctx),
|
|
173
|
+
name: bp.name,
|
|
174
|
+
rows: bp.repos.map((repo) =>
|
|
175
|
+
managedRow(
|
|
176
|
+
repo,
|
|
177
|
+
bp.branch,
|
|
178
|
+
ctx.catalog[repo]?.gh?.trim() || fill.get(repo) || null,
|
|
179
|
+
true, // seeding writes every blueprint repo into the catalog
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `<cli> setup --plan`: the plan for `names` (every repo when empty). */
|
|
186
|
+
export function buildSetupPlan(names: readonly string[] = []): ClonePlan {
|
|
187
|
+
const ctx = readOrgContext();
|
|
188
|
+
if (isFreshWorkspaceSetup(ctx.file) && blueprintWorkspaces().length > 0)
|
|
189
|
+
return { fromBlueprint: true, workspaces: blueprintClonePlan(ctx) };
|
|
190
|
+
const ws = getActiveWorkspace();
|
|
191
|
+
if (!ws) return { fromBlueprint: false, workspaces: [] };
|
|
192
|
+
return {
|
|
193
|
+
fromBlueprint: false,
|
|
194
|
+
workspaces: [
|
|
195
|
+
workspaceClonePlan(ws.name, names.length ? names : undefined, ctx),
|
|
196
|
+
],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const STATE_TEXT: Record<ClonePlanState, string> = {
|
|
201
|
+
cloned: "cloned",
|
|
202
|
+
"would-clone": "would clone",
|
|
203
|
+
"not-in-catalog": "not in the catalog: setup skips it, up clones it",
|
|
204
|
+
checkout: "your checkout (setup skips it without --force)",
|
|
205
|
+
"no-org": "no org: set github_org, or org: on the workspace",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** The plan as plain lines (the caller colours and prints them). */
|
|
209
|
+
export function renderClonePlan(
|
|
210
|
+
plan: ClonePlan,
|
|
211
|
+
cli: string,
|
|
212
|
+
catalog: ServiceCatalog = readCatalog(),
|
|
213
|
+
): string[] {
|
|
214
|
+
const out: string[] = [];
|
|
215
|
+
if (plan.fromBlueprint)
|
|
216
|
+
out.push(
|
|
217
|
+
`From the blueprint: nothing is set up yet. \`${cli} workspace seed\` creates these workspaces.`,
|
|
218
|
+
"",
|
|
219
|
+
);
|
|
220
|
+
if (plan.workspaces.length === 0) out.push("No workspace to plan.");
|
|
221
|
+
for (const ws of plan.workspaces) {
|
|
222
|
+
out.push(
|
|
223
|
+
`Workspace ${ws.name} org: ${ws.org ?? "(none)"}${ws.source ? ` (${ws.source})` : ""}`,
|
|
224
|
+
);
|
|
225
|
+
if (ws.rows.length === 0) out.push(" (no repos)");
|
|
226
|
+
for (const r of ws.rows) {
|
|
227
|
+
const where =
|
|
228
|
+
r.state === "cloned" || r.state === "checkout"
|
|
229
|
+
? r.path
|
|
230
|
+
? ` ${r.path}`
|
|
231
|
+
: ""
|
|
232
|
+
: "";
|
|
233
|
+
const owner = r.owner ?? orgOf(r.repo, catalog) ?? "";
|
|
234
|
+
out.push(
|
|
235
|
+
` ${r.repo} ${r.mode} ${r.branch} org: ${owner || "(unknown)"}` +
|
|
236
|
+
`${r.url ? ` ${r.url}` : ""} ${STATE_TEXT[r.state]}${where}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
for (const w of ws.warnings) out.push(` ⚠ ${w}`);
|
|
240
|
+
}
|
|
241
|
+
out.push("", "Plan only: nothing was cloned, seeded or written.");
|
|
242
|
+
return out;
|
|
243
|
+
}
|
package/src/ui/cli/create.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as fs from "node:fs";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { ENGINE_PACKAGE, getEngineRoot } from "../../shared/paths.js";
|
|
17
17
|
import { profileFrom } from "../../shared/profile.js";
|
|
18
|
+
import { plural } from "../../shared/plural.js";
|
|
18
19
|
|
|
19
20
|
export interface CreateOptions {
|
|
20
21
|
/** Product name: lower-case `[a-z][a-z0-9-]*`. */
|
|
@@ -345,7 +346,7 @@ export function runCreate(argv: string[], cwd: string): number {
|
|
|
345
346
|
const cli = args.cli ?? args.name;
|
|
346
347
|
const rel = path.relative(cwd, args.dir) || ".";
|
|
347
348
|
console.log(
|
|
348
|
-
`Created ${args.name} in ${args.dir} (${written.length
|
|
349
|
+
`Created ${args.name} in ${args.dir} (${plural(written.length, "file")}).`,
|
|
349
350
|
);
|
|
350
351
|
console.log("");
|
|
351
352
|
console.log("Next:");
|