@monster-orange/den 0.1.0 → 0.2.1
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 +204 -0
- package/README.md +1 -1
- package/bin/den +8 -0
- package/docs/distributions.md +283 -12
- package/package.json +4 -2
- package/src/pillars/journeys/sandbox.ts +2 -5
- package/src/pillars/security/discover.ts +17 -1
- package/src/pillars/security/run.ts +31 -3
- package/src/shared/agent-actor.ts +77 -0
- package/src/shared/den-config.ts +122 -0
- package/src/shared/edge/status.ts +74 -0
- package/src/shared/environment-manager/health.ts +54 -8
- package/src/shared/environment-manager/load-applications.ts +31 -1
- package/src/shared/environment-manager/log-tail.ts +5 -4
- package/src/shared/environment-manager/opctl.ts +2 -1
- package/src/shared/environment-manager/service-container.ts +47 -0
- package/src/shared/environment-manager/service-runner.ts +19 -2
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +25 -1
- package/src/shared/manifest/merge-override.ts +75 -0
- package/src/shared/manifest/placement.ts +333 -0
- package/src/shared/manifest/read-manifest.ts +156 -38
- package/src/shared/manifest/schema.ts +14 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +66 -6
- package/src/shared/paths.ts +12 -0
- package/src/shared/redact.ts +107 -0
- package/src/shared/stacks/manifest-commands.ts +30 -0
- package/src/shared/stacks/status.ts +99 -0
- package/src/shared/types/index.ts +23 -0
- package/src/ui/cli/applications-command.ts +34 -2
- package/src/ui/cli/edge-command.ts +9 -19
- package/src/ui/cli/graph-command.ts +50 -4
- package/src/ui/cli/index.ts +184 -55
- package/src/ui/cli/mcp-connect.ts +350 -0
- package/src/ui/cli/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -0
- package/src/ui/cli/named-target.ts +11 -0
- package/src/ui/cli/stack-command.ts +13 -40
- package/src/ui/cli/test-runner.ts +8 -1
- package/src/ui/mcp/api.ts +286 -0
- package/src/ui/mcp/connect.ts +304 -0
- package/src/ui/mcp/guards.ts +145 -0
- package/src/ui/mcp/server.ts +149 -0
- package/src/ui/mcp/tools.ts +644 -0
- package/src/ui/server/audit.ts +137 -0
- package/src/ui/server/changelog-parser.ts +11 -0
- package/src/ui/server/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/index.ts +256 -61
- package/src/ui/server/lifecycle-exec.ts +188 -0
- package/src/ui/server/log-history.ts +4 -3
- package/src/ui/server/public/environments.html +4 -2
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +980 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +7 -5
- package/src/ui/server/public/local-dev.html +130 -2191
- package/src/ui/server/public/logs.html +11 -4
- package/src/ui/server/public/panel.js +790 -0
- package/src/ui/server/public/services.html +6 -1
- package/src/ui/server/readonly-routes.ts +209 -0
- package/src/ui/server/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +39 -8
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<cli> mcp connect | disconnect | clients | config`: register this
|
|
3
|
+
* distribution's MCP server with a developer's agents. Planning lives in
|
|
4
|
+
* ../mcp/connect.ts; this file does the I/O.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import * as os from "os";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import {
|
|
12
|
+
MCP_CLIENTS,
|
|
13
|
+
findClient,
|
|
14
|
+
genericSnippet,
|
|
15
|
+
jsonHasEntry,
|
|
16
|
+
mergeJsonConfig,
|
|
17
|
+
serverSpec,
|
|
18
|
+
type ConnectAction,
|
|
19
|
+
type ConnectEnv,
|
|
20
|
+
type McpClient,
|
|
21
|
+
type Scope,
|
|
22
|
+
type ServerSpec,
|
|
23
|
+
} from "../mcp/connect.js";
|
|
24
|
+
import { product } from "../../shared/product.js";
|
|
25
|
+
import { getDistRoot } from "../../shared/paths.js";
|
|
26
|
+
import { readWebPort } from "../../shared/edge/config.js";
|
|
27
|
+
import { DEFAULT_PORT } from "../../shared/constants.js";
|
|
28
|
+
import { readMcpConfig } from "../../shared/den-config.js";
|
|
29
|
+
|
|
30
|
+
export const MCP_CONNECT_SUBCOMMANDS = [
|
|
31
|
+
"connect",
|
|
32
|
+
"disconnect",
|
|
33
|
+
"clients",
|
|
34
|
+
"config",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function onPath(bin: string): boolean {
|
|
38
|
+
const exts =
|
|
39
|
+
process.platform === "win32"
|
|
40
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD").split(";")
|
|
41
|
+
: [""];
|
|
42
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
43
|
+
if (!dir) continue;
|
|
44
|
+
for (const ext of exts) {
|
|
45
|
+
try {
|
|
46
|
+
fs.accessSync(path.join(dir, bin + ext), fs.constants.X_OK);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
/* next */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function connectEnv(projectDir: string): ConnectEnv {
|
|
57
|
+
return {
|
|
58
|
+
home: os.homedir(),
|
|
59
|
+
platform: process.platform,
|
|
60
|
+
appData: process.env.APPDATA,
|
|
61
|
+
projectDir,
|
|
62
|
+
which: onPath,
|
|
63
|
+
exists: (p) => fs.existsSync(p),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface Flags {
|
|
68
|
+
clients: string[];
|
|
69
|
+
all: boolean;
|
|
70
|
+
project: string | null;
|
|
71
|
+
port?: number;
|
|
72
|
+
dryRun: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseFlags(argv: string[]): Flags {
|
|
76
|
+
const f: Flags = { clients: [], all: false, project: null, dryRun: false };
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const a = argv[i];
|
|
79
|
+
if (a === "--all" || a === "-a") f.all = true;
|
|
80
|
+
else if (a === "--dry-run" || a === "-n") f.dryRun = true;
|
|
81
|
+
else if (a === "--project" || a === "-p") {
|
|
82
|
+
const next = argv[i + 1];
|
|
83
|
+
if (next && !next.startsWith("-") && !findClient(next)) {
|
|
84
|
+
f.project = path.resolve(next);
|
|
85
|
+
i++;
|
|
86
|
+
} else f.project = process.env.DEN_INVOKE_CWD ?? process.cwd();
|
|
87
|
+
} else if (a === "--port") {
|
|
88
|
+
const n = Number(argv[++i]);
|
|
89
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535)
|
|
90
|
+
throw new Error(`--port must be a port number`);
|
|
91
|
+
f.port = n;
|
|
92
|
+
} else if (a.startsWith("-")) throw new Error(`unknown flag ${a}`);
|
|
93
|
+
else f.clients.push(a);
|
|
94
|
+
}
|
|
95
|
+
return f;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function spec(port?: number): ServerSpec {
|
|
99
|
+
const cli = product().cli;
|
|
100
|
+
const s = serverSpec({
|
|
101
|
+
cli,
|
|
102
|
+
distRoot: getDistRoot(),
|
|
103
|
+
nodeBinDir: path.dirname(process.execPath),
|
|
104
|
+
platform: process.platform,
|
|
105
|
+
port,
|
|
106
|
+
});
|
|
107
|
+
if (!fs.existsSync(s.command))
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${s.command} does not exist: run this from a distribution with a bin/${cli} shim`,
|
|
110
|
+
);
|
|
111
|
+
return s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const show = (argv: string[]): string =>
|
|
115
|
+
argv.map((a) => (/^[\w./:=@-]+$/.test(a) ? a : `'${a}'`)).join(" ");
|
|
116
|
+
|
|
117
|
+
function run(argv: string[]): { code: number; out: string } {
|
|
118
|
+
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf-8" });
|
|
119
|
+
return {
|
|
120
|
+
code: r.status ?? 1,
|
|
121
|
+
out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readText(file: string): string | null {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readFileSync(file, "utf-8");
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Write `text` to `file` atomically, keeping the previous copy as .bak. */
|
|
134
|
+
function writeConfig(file: string, text: string): void {
|
|
135
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
136
|
+
if (fs.existsSync(file)) fs.copyFileSync(file, `${file}.bak`);
|
|
137
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
138
|
+
fs.writeFileSync(tmp, text);
|
|
139
|
+
fs.renameSync(tmp, file);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function apply(
|
|
143
|
+
a: ConnectAction,
|
|
144
|
+
remove: boolean,
|
|
145
|
+
dryRun: boolean,
|
|
146
|
+
): { ok: boolean; said: string } {
|
|
147
|
+
if (a.kind === "json") {
|
|
148
|
+
const before = readText(a.file);
|
|
149
|
+
const after = mergeJsonConfig(
|
|
150
|
+
before,
|
|
151
|
+
a.key,
|
|
152
|
+
a.name,
|
|
153
|
+
remove ? null : a.entry,
|
|
154
|
+
);
|
|
155
|
+
if (!dryRun) writeConfig(a.file, after);
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
said: `${dryRun ? "would write" : remove ? "removed from" : "wrote"} ${a.file}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (remove) {
|
|
162
|
+
if (!a.remove)
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
said: "remove it from the client's own MCP settings",
|
|
166
|
+
};
|
|
167
|
+
if (dryRun) return { ok: true, said: `would run ${show(a.remove)}` };
|
|
168
|
+
const r = run(a.remove);
|
|
169
|
+
return { ok: r.code === 0, said: r.out || show(a.remove) };
|
|
170
|
+
}
|
|
171
|
+
if (dryRun) return { ok: true, said: `would run ${show(a.add)}` };
|
|
172
|
+
if (a.remove) run(a.remove);
|
|
173
|
+
const r = run(a.add);
|
|
174
|
+
return {
|
|
175
|
+
ok: r.code === 0,
|
|
176
|
+
said:
|
|
177
|
+
r.code === 0 ? `ran ${a.add[0]} ${a.add.slice(1, 3).join(" ")}` : r.out,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function status(
|
|
182
|
+
c: McpClient,
|
|
183
|
+
s: ServerSpec,
|
|
184
|
+
scope: Scope,
|
|
185
|
+
env: ConnectEnv,
|
|
186
|
+
): string {
|
|
187
|
+
const a = c.plan(s, scope, env);
|
|
188
|
+
if (a.kind === "json") {
|
|
189
|
+
const st = jsonHasEntry(readText(a.file), a.key, a.name, a.entry);
|
|
190
|
+
return st === "connected"
|
|
191
|
+
? chalk.green("connected")
|
|
192
|
+
: st === "different"
|
|
193
|
+
? chalk.yellow("connected (different command: reconnect)")
|
|
194
|
+
: chalk.gray("not connected");
|
|
195
|
+
}
|
|
196
|
+
if (!a.check) return chalk.gray("unknown (check the client's MCP list)");
|
|
197
|
+
return run(a.check).code === 0
|
|
198
|
+
? chalk.green("connected")
|
|
199
|
+
: chalk.gray("not connected");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function webServerLine(port?: number): Promise<string> {
|
|
203
|
+
let p = port;
|
|
204
|
+
if (p === undefined) {
|
|
205
|
+
try {
|
|
206
|
+
p = readWebPort();
|
|
207
|
+
} catch {
|
|
208
|
+
p = DEFAULT_PORT;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const url = `http://localhost:${p}`;
|
|
212
|
+
try {
|
|
213
|
+
const r = await fetch(`${url}/api/server-info`, {
|
|
214
|
+
signal: AbortSignal.timeout(1500),
|
|
215
|
+
});
|
|
216
|
+
const info = (await r.json()) as { version?: string };
|
|
217
|
+
return (
|
|
218
|
+
chalk.green(`web server up at ${url}`) +
|
|
219
|
+
(info.version ? chalk.gray(` (Den ${info.version})`) : "")
|
|
220
|
+
);
|
|
221
|
+
} catch {
|
|
222
|
+
return chalk.yellow(
|
|
223
|
+
`web server not running at ${url}: agents get read errors until \`${product().cli} web up\``,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function usage(): string {
|
|
229
|
+
const cli = product().cli;
|
|
230
|
+
return [
|
|
231
|
+
`Usage:`,
|
|
232
|
+
` ${cli} mcp clients which agents are installed and connected`,
|
|
233
|
+
` ${cli} mcp connect [<client>...|--all] register ${cli}'s MCP server (user scope)`,
|
|
234
|
+
` [--project [dir]] in a project's config instead (default: here)`,
|
|
235
|
+
` [--port <n>] [--dry-run]`,
|
|
236
|
+
` ${cli} mcp disconnect <client>... [--project [dir]]`,
|
|
237
|
+
` ${cli} mcp config [--port <n>] JSON for any other MCP client`,
|
|
238
|
+
``,
|
|
239
|
+
`Clients: ${MCP_CLIENTS.map((c) => c.id).join(", ")}`,
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function runMcpConnect(argv: string[]): Promise<number> {
|
|
244
|
+
const [sub, ...rest] = argv;
|
|
245
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
246
|
+
console.log(usage());
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
let flags: Flags;
|
|
250
|
+
try {
|
|
251
|
+
flags = parseFlags(rest);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.error(chalk.red(`${(e as Error).message}\n`) + usage());
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
const s = spec(flags.port);
|
|
257
|
+
const scope: Scope = flags.project ? "project" : "user";
|
|
258
|
+
const env = connectEnv(
|
|
259
|
+
flags.project ?? process.env.DEN_INVOKE_CWD ?? process.cwd(),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (sub === "config") {
|
|
263
|
+
console.log(genericSnippet(s));
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (sub === "clients") {
|
|
268
|
+
console.log(
|
|
269
|
+
chalk.bold(
|
|
270
|
+
`\n ${product().title} MCP server: ${s.command} ${s.args.join(" ")}`,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
console.log(` ${await webServerLine(flags.port)}`);
|
|
274
|
+
const actions = readMcpConfig().actions;
|
|
275
|
+
console.log(
|
|
276
|
+
chalk.gray(
|
|
277
|
+
` tools: read${actions.length ? ` + actions ${actions.join(", ")}` : " only (mcp.actions is empty)"}\n`,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
for (const c of MCP_CLIENTS) {
|
|
281
|
+
const here = c.detect(env);
|
|
282
|
+
const st =
|
|
283
|
+
here && c.scopes.includes(scope)
|
|
284
|
+
? status(c, s, scope, env)
|
|
285
|
+
: chalk.gray(here ? `no ${scope} scope` : "not installed");
|
|
286
|
+
console.log(
|
|
287
|
+
` ${here ? "●" : "○"} ${c.id.padEnd(15)} ${c.title.padEnd(24)} ${st}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
console.log(
|
|
291
|
+
chalk.gray(
|
|
292
|
+
`\n Connect: ${product().cli} mcp connect <client> (or --all for every installed one)\n`,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (sub !== "connect" && sub !== "disconnect") {
|
|
299
|
+
console.log(usage());
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
const remove = sub === "disconnect";
|
|
303
|
+
let targets: McpClient[];
|
|
304
|
+
if (flags.all)
|
|
305
|
+
targets = MCP_CLIENTS.filter(
|
|
306
|
+
(c) => c.detect(env) && c.scopes.includes(scope),
|
|
307
|
+
);
|
|
308
|
+
else {
|
|
309
|
+
const unknown = flags.clients.filter((id) => !findClient(id));
|
|
310
|
+
if (unknown.length) {
|
|
311
|
+
console.error(
|
|
312
|
+
chalk.red(`unknown client: ${unknown.join(", ")}\n`) + usage(),
|
|
313
|
+
);
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
targets = flags.clients.map((id) => findClient(id)!);
|
|
317
|
+
}
|
|
318
|
+
if (targets.length === 0) {
|
|
319
|
+
console.log(usage());
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let failed = 0;
|
|
324
|
+
for (const c of targets) {
|
|
325
|
+
if (!c.scopes.includes(scope)) {
|
|
326
|
+
console.log(` ${chalk.yellow("•")} ${c.title}: no ${scope} scope`);
|
|
327
|
+
failed++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const r = apply(c.plan(s, scope, env), remove, flags.dryRun);
|
|
332
|
+
console.log(
|
|
333
|
+
` ${r.ok ? chalk.green("✓") : chalk.red("✗")} ${c.title}: ${r.said}`,
|
|
334
|
+
);
|
|
335
|
+
if (!r.ok) failed++;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.log(` ${chalk.red("✗")} ${c.title}: ${(e as Error).message}`);
|
|
338
|
+
failed++;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!remove && !flags.dryRun && failed < targets.length) {
|
|
342
|
+
console.log(
|
|
343
|
+
chalk.gray(
|
|
344
|
+
`\n Restart the agent (or start a new session) to load "${s.name}".`,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
console.log(` ${await webServerLine(flags.port)}\n`);
|
|
348
|
+
}
|
|
349
|
+
return failed ? 1 : 0;
|
|
350
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for `<cli> mcp` (bin/den routes `mcp` here). Kept apart from
|
|
3
|
+
* the main CLI so nothing but the MCP protocol is ever written to stdout.
|
|
4
|
+
*/
|
|
5
|
+
import "./mcp-stdout-guard.js";
|
|
6
|
+
import { runMcp } from "../mcp/server.js";
|
|
7
|
+
|
|
8
|
+
runMcp(process.argv.slice(2)).catch((error: unknown) => {
|
|
9
|
+
process.stderr.write(
|
|
10
|
+
`mcp: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imported first by the `mcp` entry: stdout carries the MCP protocol only,
|
|
3
|
+
* so anything that logs through console goes to stderr instead.
|
|
4
|
+
*/
|
|
5
|
+
const toStderr = (...args: unknown[]): void => console.error(...args);
|
|
6
|
+
console.log = toStderr;
|
|
7
|
+
console.info = toStderr;
|
|
8
|
+
console.debug = toStderr;
|
|
9
|
+
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `address` ("repo" or "repo/service") is something the user named on
|
|
3
|
+
* the command line: the address itself, or the repo it belongs to.
|
|
4
|
+
*/
|
|
5
|
+
export function isNamedTarget(
|
|
6
|
+
address: string,
|
|
7
|
+
named: readonly string[],
|
|
8
|
+
): boolean {
|
|
9
|
+
const repo = address.split("/")[0];
|
|
10
|
+
return named.some((n) => n === address || n === repo);
|
|
11
|
+
}
|
|
@@ -6,15 +6,8 @@
|
|
|
6
6
|
* https://<route>.<stack>.<edge.domain>.
|
|
7
7
|
*/
|
|
8
8
|
import chalk from "chalk";
|
|
9
|
-
import { readEdgeConfig } from "../../shared/edge/config.js";
|
|
10
9
|
import { routeUrl } from "../../shared/edge/render.js";
|
|
11
10
|
import { edgeUp } from "../../shared/edge/runtime.js";
|
|
12
|
-
import { readStacks } from "../../shared/stacks/config.js";
|
|
13
|
-
import {
|
|
14
|
-
getFocusedWorkspaceName,
|
|
15
|
-
getWorkspaceByName,
|
|
16
|
-
} from "../../shared/environment-manager/workspaces.js";
|
|
17
|
-
import { stackWorkspace } from "../../shared/stacks/render.js";
|
|
18
11
|
import { projectName, startOrder } from "../../shared/stacks/render.js";
|
|
19
12
|
import {
|
|
20
13
|
stackDown,
|
|
@@ -22,44 +15,26 @@ import {
|
|
|
22
15
|
stackLogs,
|
|
23
16
|
stackPs,
|
|
24
17
|
stackUp,
|
|
25
|
-
type StackContext,
|
|
26
18
|
} from "../../shared/stacks/runtime.js";
|
|
19
|
+
import { loadStackContext, stackStatuses } from "../../shared/stacks/status.js";
|
|
27
20
|
|
|
28
|
-
function
|
|
29
|
-
fkWs: string;
|
|
30
|
-
ws: string;
|
|
31
|
-
service?: string;
|
|
32
|
-
rest: string[];
|
|
33
|
-
} {
|
|
21
|
+
function serviceFlag(args: string[]): { service?: string; rest: string[] } {
|
|
34
22
|
const rest: string[] = [];
|
|
35
|
-
// The global -w/--workspace is parsed (and checked) before we run and
|
|
36
|
-
// arrives as DEN_WORKSPACE_OVERRIDE.
|
|
37
|
-
const fkWs =
|
|
38
|
-
process.env.DEN_WORKSPACE_OVERRIDE ||
|
|
39
|
-
getFocusedWorkspaceName() ||
|
|
40
|
-
"default";
|
|
41
23
|
let service: string | undefined;
|
|
42
24
|
for (let i = 0; i < args.length; i++) {
|
|
43
25
|
if (args[i] === "--service") service = args[++i];
|
|
44
26
|
else rest.push(args[i]);
|
|
45
27
|
}
|
|
46
|
-
|
|
47
|
-
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(ws))
|
|
48
|
-
throw new Error(
|
|
49
|
-
`workspace "${ws}" must be lowercase letters, digits and dashes`,
|
|
50
|
-
);
|
|
51
|
-
return { fkWs, ws, service, rest };
|
|
28
|
+
return { service, rest };
|
|
52
29
|
}
|
|
53
30
|
|
|
54
31
|
export async function cmdStack(argv: string[]): Promise<void> {
|
|
55
|
-
const {
|
|
32
|
+
const { service, rest } = serviceFlag(argv);
|
|
56
33
|
const [sub = "ls", ...args] = rest;
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
const ctx: StackContext = { edge, stacks, workspace: ws };
|
|
34
|
+
// The global -w/--workspace is parsed (and checked) before we run and
|
|
35
|
+
// arrives as DEN_WORKSPACE_OVERRIDE; loadStackContext reads it.
|
|
36
|
+
const ctx = loadStackContext();
|
|
37
|
+
const { edge, stacks } = ctx;
|
|
63
38
|
const names = args.filter((a) => !a.startsWith("-"));
|
|
64
39
|
const pick = () => {
|
|
65
40
|
if (!names.length)
|
|
@@ -71,17 +46,15 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
71
46
|
|
|
72
47
|
switch (sub) {
|
|
73
48
|
case "ls":
|
|
74
|
-
for (const s of
|
|
49
|
+
for (const s of stackStatuses(ctx)) {
|
|
75
50
|
console.log(
|
|
76
51
|
chalk.bold(`${s.name}`) +
|
|
77
52
|
chalk.gray(
|
|
78
|
-
` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${
|
|
53
|
+
` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${s.project}`,
|
|
79
54
|
),
|
|
80
55
|
);
|
|
81
|
-
for (const route of
|
|
82
|
-
console.log(
|
|
83
|
-
` ${routeUrl(`${route}.${s.name}`, edge)} → ${s.routes[route]}`,
|
|
84
|
-
);
|
|
56
|
+
for (const route of s.routes)
|
|
57
|
+
console.log(` ${route.url} → ${route.target}`);
|
|
85
58
|
if (s.uses.length)
|
|
86
59
|
console.log(chalk.gray(` uses ${s.uses.join(", ")}`));
|
|
87
60
|
}
|
|
@@ -90,7 +63,7 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
90
63
|
// Re-render the edge so its cert covers every stack, then start the stacks.
|
|
91
64
|
await edgeUp(edge);
|
|
92
65
|
for (const n of startOrder(pick(), stacks)) {
|
|
93
|
-
console.log(chalk.cyan(`\n▶ ${n} (${projectName(n,
|
|
66
|
+
console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ctx.workspace)})`));
|
|
94
67
|
stackUp(ctx, stacks[n], { build: args.includes("--build") });
|
|
95
68
|
for (const route of Object.keys(stacks[n].routes))
|
|
96
69
|
console.log(chalk.green(` ✓ ${routeUrl(`${route}.${n}`, edge)}`));
|
|
@@ -447,10 +447,16 @@ export class TestRunner {
|
|
|
447
447
|
): CommandStep[] {
|
|
448
448
|
const names = options.modules ?? [];
|
|
449
449
|
const script = "src/pillars/security/run.ts";
|
|
450
|
+
// An explicitly chosen stronger mode is passed on; the entrypoint's
|
|
451
|
+
// per-env ceiling refuses it where it isn't allowed.
|
|
452
|
+
const mode =
|
|
453
|
+
options.securityMode && options.securityMode !== "baseline"
|
|
454
|
+
? [`--mode=${options.securityMode}`]
|
|
455
|
+
: [];
|
|
450
456
|
if (names.length === 0) {
|
|
451
457
|
return [
|
|
452
458
|
{
|
|
453
|
-
...tsxCommand(script, `--env=${options.environment}
|
|
459
|
+
...tsxCommand(script, `--env=${options.environment}`, ...mode),
|
|
454
460
|
env: baseEnv,
|
|
455
461
|
label: "Run all security scans",
|
|
456
462
|
},
|
|
@@ -461,6 +467,7 @@ export class TestRunner {
|
|
|
461
467
|
script,
|
|
462
468
|
`--env=${options.environment}`,
|
|
463
469
|
`--service=${name}`,
|
|
470
|
+
...mode,
|
|
464
471
|
),
|
|
465
472
|
env: baseEnv,
|
|
466
473
|
label: `Run security scan ${name}`,
|