@getmonoceros/workbench 1.11.11 → 1.13.0
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/dist/bin.js +2153 -1592
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -51,111 +51,23 @@ function shellQuote(arg) {
|
|
|
51
51
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// src/devcontainer/wsl-backend-bootstrap.ts
|
|
55
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
56
|
-
|
|
57
|
-
// src/util/format.ts
|
|
58
|
-
var ESC = "\x1B[";
|
|
59
|
-
var ANSI_BOLD = `${ESC}1m`;
|
|
60
|
-
var ANSI_UNDERLINE = `${ESC}4m`;
|
|
61
|
-
var ANSI_CYAN = `${ESC}36m`;
|
|
62
|
-
var ANSI_GREY = `${ESC}90m`;
|
|
63
|
-
var ANSI_RESET = `${ESC}0m`;
|
|
64
|
-
function makeWrap(isTty2) {
|
|
65
|
-
return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET : s;
|
|
66
|
-
}
|
|
67
|
-
function makePalette(isTty2) {
|
|
68
|
-
const wrap = makeWrap(isTty2);
|
|
69
|
-
return {
|
|
70
|
-
bold: (s) => wrap(s, ANSI_BOLD),
|
|
71
|
-
underline: (s) => wrap(s, ANSI_UNDERLINE),
|
|
72
|
-
cyan: (s) => wrap(s, ANSI_CYAN),
|
|
73
|
-
dim: (s) => wrap(s, ANSI_GREY),
|
|
74
|
-
sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD, ANSI_UNDERLINE)
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function colorsFor(stream) {
|
|
78
|
-
return makePalette(stream.isTTY ?? false);
|
|
79
|
-
}
|
|
80
|
-
var stderrPalette = makePalette(process.stderr.isTTY ?? false);
|
|
81
|
-
var bold = stderrPalette.bold;
|
|
82
|
-
var underline = stderrPalette.underline;
|
|
83
|
-
var cyan = stderrPalette.cyan;
|
|
84
|
-
var dim = stderrPalette.dim;
|
|
85
|
-
var sectionLine = stderrPalette.sectionLine;
|
|
86
|
-
|
|
87
|
-
// src/devcontainer/wsl-backend-bootstrap.ts
|
|
88
|
-
function bootstrapWslBackend(opts = {}) {
|
|
89
|
-
const platform = opts.platform ?? process.platform;
|
|
90
|
-
if (platform !== "win32") return;
|
|
91
|
-
const listDistros = opts.wslDistros ?? defaultWslDistros;
|
|
92
|
-
const raw = listDistros();
|
|
93
|
-
if (raw !== null && hasWsl2Distro(raw)) return;
|
|
94
|
-
const probe = opts.probe ?? defaultProbe2;
|
|
95
|
-
if (probe("docker", ["--version"]) !== 0) return;
|
|
96
|
-
const warn = opts.warn ?? ((m) => process.stderr.write(`${m}
|
|
97
|
-
`));
|
|
98
|
-
warn(formatWslBackendHint());
|
|
99
|
-
}
|
|
100
|
-
function hasWsl2Distro(raw) {
|
|
101
|
-
const text = raw.split(String.fromCharCode(0)).join("");
|
|
102
|
-
for (const line of text.split(/\r?\n/)) {
|
|
103
|
-
const trimmed = line.trim();
|
|
104
|
-
if (!trimmed) continue;
|
|
105
|
-
if (/\bNAME\b/i.test(trimmed) && /\bVERSION\b/i.test(trimmed)) continue;
|
|
106
|
-
const tokens = trimmed.replace(/^\*\s*/, "").split(/\s+/);
|
|
107
|
-
if (tokens[tokens.length - 1] === "2") return true;
|
|
108
|
-
}
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
function formatWslBackendHint() {
|
|
112
|
-
return [
|
|
113
|
-
`Docker's daemon isn't reachable, and no WSL 2 distro is registered.`,
|
|
114
|
-
`Docker Desktop runs on the WSL 2 backend, so without a distro it`,
|
|
115
|
-
`can't start -- often shown as the misleading "Virtualization support`,
|
|
116
|
-
`not detected" (even with virtualization enabled in BIOS).`,
|
|
117
|
-
``,
|
|
118
|
-
`Fix it in an elevated PowerShell:`,
|
|
119
|
-
``,
|
|
120
|
-
cyan(` wsl --set-default-version 2`),
|
|
121
|
-
cyan(` wsl --update`),
|
|
122
|
-
cyan(` wsl --install -d Ubuntu`),
|
|
123
|
-
``,
|
|
124
|
-
`Then reboot and start Docker Desktop.`
|
|
125
|
-
].join("\n");
|
|
126
|
-
}
|
|
127
|
-
function defaultProbe2(cmd, args) {
|
|
128
|
-
const result = spawnSync2(cmd, [...args], { stdio: "ignore", timeout: 1e4 });
|
|
129
|
-
return result.status ?? 1;
|
|
130
|
-
}
|
|
131
|
-
function defaultWslDistros() {
|
|
132
|
-
const result = spawnSync2("wsl", ["-l", "-v"], {
|
|
133
|
-
encoding: "utf8",
|
|
134
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
135
|
-
env: { ...process.env, WSL_UTF8: "1" },
|
|
136
|
-
timeout: 5e3
|
|
137
|
-
});
|
|
138
|
-
if (result.status !== 0 || typeof result.stdout !== "string") return null;
|
|
139
|
-
return result.stdout;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
54
|
// src/help.ts
|
|
143
|
-
var
|
|
144
|
-
var
|
|
145
|
-
var
|
|
146
|
-
var
|
|
147
|
-
var
|
|
55
|
+
var ANSI_BOLD = "\x1B[1m";
|
|
56
|
+
var ANSI_UNDERLINE = "\x1B[4m";
|
|
57
|
+
var ANSI_CYAN = "\x1B[36m";
|
|
58
|
+
var ANSI_GREY = "\x1B[90m";
|
|
59
|
+
var ANSI_RESET = "\x1B[0m";
|
|
148
60
|
function isTty() {
|
|
149
61
|
return process.stdout.isTTY ?? false;
|
|
150
62
|
}
|
|
151
63
|
function color(text, ...codes) {
|
|
152
64
|
if (!isTty()) return text;
|
|
153
|
-
return codes.join("") + text +
|
|
65
|
+
return codes.join("") + text + ANSI_RESET;
|
|
154
66
|
}
|
|
155
|
-
var
|
|
156
|
-
var
|
|
157
|
-
var
|
|
158
|
-
var grey = (s) => color(s,
|
|
67
|
+
var bold = (s) => color(s, ANSI_BOLD);
|
|
68
|
+
var underline = (s) => color(s, ANSI_UNDERLINE);
|
|
69
|
+
var cyan = (s) => color(s, ANSI_CYAN);
|
|
70
|
+
var grey = (s) => color(s, ANSI_GREY);
|
|
159
71
|
var GROUPS = [
|
|
160
72
|
{ key: "lifecycle", label: "Container lifecycle" },
|
|
161
73
|
{ key: "run", label: "Run + inspect" },
|
|
@@ -248,7 +160,7 @@ function collectSubCommands(cmd) {
|
|
|
248
160
|
function renderCommandsBlock(entries) {
|
|
249
161
|
if (entries.length === 0) return [];
|
|
250
162
|
const lines = [];
|
|
251
|
-
lines.push(
|
|
163
|
+
lines.push(underline(bold("COMMANDS")));
|
|
252
164
|
const byGroup = /* @__PURE__ */ new Map();
|
|
253
165
|
for (const entry2 of entries) {
|
|
254
166
|
const arr = byGroup.get(entry2.group) ?? [];
|
|
@@ -258,10 +170,10 @@ function renderCommandsBlock(entries) {
|
|
|
258
170
|
const renderSection = (label, items) => {
|
|
259
171
|
if (items.length === 0) return;
|
|
260
172
|
lines.push("");
|
|
261
|
-
lines.push(
|
|
173
|
+
lines.push(underline(grey(label)));
|
|
262
174
|
lines.push("");
|
|
263
175
|
const rows = items.map((e) => [
|
|
264
|
-
|
|
176
|
+
cyan(e.name),
|
|
265
177
|
e.description
|
|
266
178
|
]);
|
|
267
179
|
lines.push(alignTable(rows, ""));
|
|
@@ -298,27 +210,27 @@ function renderUsageBlock(cmd, commandPath) {
|
|
|
298
210
|
lines.push(grey(wrapText(header, terminalWidth(), "")));
|
|
299
211
|
lines.push("");
|
|
300
212
|
lines.push(
|
|
301
|
-
`${
|
|
213
|
+
`${underline(bold("USAGE"))} ${cyan([fullName, ...usageTokens].join(" "))}`
|
|
302
214
|
);
|
|
303
215
|
lines.push("");
|
|
304
216
|
if (positionals.length > 0) {
|
|
305
|
-
lines.push(
|
|
217
|
+
lines.push(underline(bold("ARGUMENTS")));
|
|
306
218
|
lines.push("");
|
|
307
219
|
const rows = positionals.map((p) => {
|
|
308
220
|
const isRequired = p.required !== false && p.default === void 0;
|
|
309
|
-
return [
|
|
221
|
+
return [cyan(p.name.toUpperCase()), renderArgDescription(p, isRequired)];
|
|
310
222
|
});
|
|
311
223
|
lines.push(alignTable(rows, " "));
|
|
312
224
|
lines.push("");
|
|
313
225
|
}
|
|
314
226
|
if (flags.length > 0) {
|
|
315
|
-
lines.push(
|
|
227
|
+
lines.push(underline(bold("OPTIONS")));
|
|
316
228
|
lines.push("");
|
|
317
229
|
const rows = flags.map((f) => {
|
|
318
230
|
const isRequired = f.required === true && f.default === void 0;
|
|
319
231
|
const aliases = (Array.isArray(f.alias) ? f.alias : f.alias ? [f.alias] : []).map((a) => `-${a}`);
|
|
320
232
|
const label = [...aliases, `--${f.name}`].join(", ") + renderValueHint(f);
|
|
321
|
-
return [
|
|
233
|
+
return [cyan(label), renderArgDescription(f, isRequired)];
|
|
322
234
|
});
|
|
323
235
|
lines.push(alignTable(rows, " "));
|
|
324
236
|
lines.push("");
|
|
@@ -328,7 +240,7 @@ function renderUsageBlock(cmd, commandPath) {
|
|
|
328
240
|
lines.push(line);
|
|
329
241
|
}
|
|
330
242
|
lines.push(
|
|
331
|
-
`Use ${
|
|
243
|
+
`Use ${cyan(`${fullName} <command> --help`)} for more information about a command.`
|
|
332
244
|
);
|
|
333
245
|
lines.push("");
|
|
334
246
|
}
|
|
@@ -339,25 +251,25 @@ function detectHelpRequest(argv, main2) {
|
|
|
339
251
|
const separatorIdx = argv.indexOf("--");
|
|
340
252
|
if (helpIdx === -1) return null;
|
|
341
253
|
if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
|
|
342
|
-
const
|
|
254
|
+
const path21 = [];
|
|
343
255
|
const tokens = argv.slice(
|
|
344
256
|
0,
|
|
345
257
|
separatorIdx === -1 ? argv.length : separatorIdx
|
|
346
258
|
);
|
|
347
259
|
let cursor = main2;
|
|
348
260
|
const mainName = (main2.meta ?? {}).name ?? "monoceros";
|
|
349
|
-
|
|
261
|
+
path21.push(mainName);
|
|
350
262
|
for (const tok of tokens) {
|
|
351
263
|
if (tok.startsWith("-")) continue;
|
|
352
264
|
const subs = cursor.subCommands ?? {};
|
|
353
265
|
if (tok in subs) {
|
|
354
266
|
cursor = subs[tok];
|
|
355
|
-
|
|
267
|
+
path21.push(tok);
|
|
356
268
|
continue;
|
|
357
269
|
}
|
|
358
270
|
break;
|
|
359
271
|
}
|
|
360
|
-
return { path:
|
|
272
|
+
return { path: path21, cmd: cursor };
|
|
361
273
|
}
|
|
362
274
|
async function maybeRenderHelp(argv, main2) {
|
|
363
275
|
const hit = detectHelpRequest(argv, main2);
|
|
@@ -393,13 +305,13 @@ import { defineCommand as defineCommand30 } from "citty";
|
|
|
393
305
|
|
|
394
306
|
// src/commands/add-apt-packages.ts
|
|
395
307
|
import { defineCommand } from "citty";
|
|
396
|
-
import { consola as
|
|
308
|
+
import { consola as consola2 } from "consola";
|
|
397
309
|
|
|
398
310
|
// src/modify/index.ts
|
|
399
311
|
import { promises as fs8 } from "fs";
|
|
400
|
-
import { consola
|
|
312
|
+
import { consola } from "consola";
|
|
401
313
|
import { createPatch } from "diff";
|
|
402
|
-
import
|
|
314
|
+
import path9 from "path";
|
|
403
315
|
|
|
404
316
|
// src/config/io.ts
|
|
405
317
|
import { promises as fs } from "fs";
|
|
@@ -492,6 +404,68 @@ var RoutingSchema = z.object({
|
|
|
492
404
|
ports: z.array(PortEntrySchema).default([]),
|
|
493
405
|
vscodeAutoForward: z.boolean().optional()
|
|
494
406
|
});
|
|
407
|
+
var SERVICE_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
408
|
+
var ServiceEnvValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]).transform((v) => v === null ? "" : String(v));
|
|
409
|
+
var ServiceHealthcheckSchema = z.object({
|
|
410
|
+
// Compose accepts both forms and they differ semantically:
|
|
411
|
+
// - string → run via the shell (CMD-SHELL)
|
|
412
|
+
// - ["CMD", …] → exec the args directly, no shell
|
|
413
|
+
// - ["CMD-SHELL", …]
|
|
414
|
+
// We accept either and render it back faithfully.
|
|
415
|
+
test: z.union([
|
|
416
|
+
z.string().min(1, "Healthcheck test must not be empty."),
|
|
417
|
+
z.array(z.string().min(1)).min(1, "Healthcheck test array must not be empty.")
|
|
418
|
+
]),
|
|
419
|
+
interval: z.string().optional(),
|
|
420
|
+
timeout: z.string().optional(),
|
|
421
|
+
retries: z.number().int().min(1).optional(),
|
|
422
|
+
startPeriod: z.string().optional()
|
|
423
|
+
});
|
|
424
|
+
var SERVICE_RESTART_VALUES = [
|
|
425
|
+
"no",
|
|
426
|
+
"always",
|
|
427
|
+
"on-failure",
|
|
428
|
+
"unless-stopped"
|
|
429
|
+
];
|
|
430
|
+
function isValidServiceVolume(spec) {
|
|
431
|
+
const parts = spec.split(":");
|
|
432
|
+
if (parts.length < 2 || parts.length > 3) return false;
|
|
433
|
+
const [src, dest, mode] = parts;
|
|
434
|
+
if (!src || !dest) return false;
|
|
435
|
+
if (!dest.startsWith("/")) return false;
|
|
436
|
+
if (mode !== void 0 && !/^(ro|rw|cached|delegated|z|Z)$/.test(mode)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (src === "data") return true;
|
|
440
|
+
if (src.startsWith("/")) return false;
|
|
441
|
+
const looksLikePath = src.startsWith("./") || src.includes("/");
|
|
442
|
+
if (!looksLikePath) return false;
|
|
443
|
+
const normalized = src.startsWith("./") ? src.slice(2) : src;
|
|
444
|
+
if (normalized.split("/").some((s) => s === ".." || s === ".")) return false;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
var ServiceObjectSchema = z.object({
|
|
448
|
+
name: z.string().regex(
|
|
449
|
+
SERVICE_NAME_RE,
|
|
450
|
+
"Invalid service name. Use lowercase letters, digits, '_' or '-' (must start with a letter or digit)."
|
|
451
|
+
),
|
|
452
|
+
image: z.string().min(1, "Service image must not be empty."),
|
|
453
|
+
// In-container port the service listens on. Used by
|
|
454
|
+
// `monoceros tunnel <name> <service>` to forward without an explicit
|
|
455
|
+
// port argument. NOT a host port mapping — host exposure goes through
|
|
456
|
+
// routing.ports (Traefik) or `monoceros tunnel`.
|
|
457
|
+
port: z.number().int().min(1, "Port must be \u2265 1.").max(65535).optional(),
|
|
458
|
+
env: z.record(z.string(), ServiceEnvValueSchema).optional(),
|
|
459
|
+
volumes: z.array(
|
|
460
|
+
z.string().refine(
|
|
461
|
+
isValidServiceVolume,
|
|
462
|
+
"Invalid volume. Use 'data:/container/path' for the per-service persistent dir, or a relative host path ('projects/app/init.sql:/...:ro', './config:/...'). Docker named volumes (a bare name like 'rustfs_data') are not supported; absolute host paths and '..' are rejected."
|
|
463
|
+
)
|
|
464
|
+
).optional(),
|
|
465
|
+
healthcheck: ServiceHealthcheckSchema.optional(),
|
|
466
|
+
restart: z.enum(SERVICE_RESTART_VALUES).optional(),
|
|
467
|
+
command: z.string().optional()
|
|
468
|
+
});
|
|
495
469
|
var ExternalServicesSchema = z.object({
|
|
496
470
|
postgres: z.string().regex(
|
|
497
471
|
POSTGRES_URL_RE,
|
|
@@ -518,7 +492,7 @@ var SolutionConfigSchema = z.object({
|
|
|
518
492
|
"Invalid install URL. Must start with 'https://' and contain only URL-safe characters (no shell metacharacters)."
|
|
519
493
|
)
|
|
520
494
|
).default([]),
|
|
521
|
-
services: z.array(
|
|
495
|
+
services: z.array(ServiceObjectSchema).default([]),
|
|
522
496
|
repos: z.array(RepoEntrySchema).default([]),
|
|
523
497
|
routing: RoutingSchema.optional(),
|
|
524
498
|
externalServices: ExternalServicesSchema.default({}),
|
|
@@ -637,6 +611,9 @@ function containerConfigsDir(home = monocerosHome()) {
|
|
|
637
611
|
function containerConfigPath(name, home = monocerosHome()) {
|
|
638
612
|
return path.join(containerConfigsDir(home), `${name}.yml`);
|
|
639
613
|
}
|
|
614
|
+
function containerEnvPath(name, home = monocerosHome()) {
|
|
615
|
+
return path.join(containerConfigsDir(home), `${name}.env`);
|
|
616
|
+
}
|
|
640
617
|
function containersDir(home = monocerosHome()) {
|
|
641
618
|
return path.join(home, "container");
|
|
642
619
|
}
|
|
@@ -657,813 +634,667 @@ function prettyPath(p) {
|
|
|
657
634
|
return p;
|
|
658
635
|
}
|
|
659
636
|
|
|
660
|
-
// src/
|
|
661
|
-
import {
|
|
662
|
-
import { promises as fs2 } from "fs";
|
|
637
|
+
// src/config/env-file.ts
|
|
638
|
+
import { existsSync as existsSync2, readFileSync, promises as fsp } from "fs";
|
|
663
639
|
import path2 from "path";
|
|
664
|
-
var
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
let
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
|
|
679
|
-
child.stdin.write(input);
|
|
680
|
-
child.stdin.end();
|
|
681
|
-
});
|
|
682
|
-
};
|
|
683
|
-
var realGitCredentialApprove = (input) => {
|
|
684
|
-
return new Promise((resolve, reject) => {
|
|
685
|
-
const child = spawn("git", ["credential", "approve"], {
|
|
686
|
-
stdio: ["pipe", "ignore", "inherit"],
|
|
687
|
-
env: {
|
|
688
|
-
...process.env,
|
|
689
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
child.on("error", reject);
|
|
693
|
-
child.on("exit", () => resolve());
|
|
694
|
-
child.stdin.write(input);
|
|
695
|
-
child.stdin.end();
|
|
696
|
-
});
|
|
697
|
-
};
|
|
698
|
-
function resolveProvider(host, explicit) {
|
|
699
|
-
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
700
|
-
if (canonical) return canonical;
|
|
701
|
-
return explicit ?? "unknown";
|
|
702
|
-
}
|
|
703
|
-
function uniqueHttpsHosts(repos) {
|
|
704
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
705
|
-
for (const repo of repos) {
|
|
706
|
-
if (!repo.url.startsWith("https://")) continue;
|
|
707
|
-
let host;
|
|
708
|
-
try {
|
|
709
|
-
host = new URL(repo.url).hostname;
|
|
710
|
-
} catch {
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
if (byHost.has(host)) continue;
|
|
714
|
-
byHost.set(host, { host, provider: resolveProvider(host, repo.provider) });
|
|
640
|
+
var ENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
641
|
+
function parseEnvFile(content) {
|
|
642
|
+
const out = {};
|
|
643
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
644
|
+
const trimmed = raw.trim();
|
|
645
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
646
|
+
const m = ENV_LINE_RE.exec(raw);
|
|
647
|
+
if (!m) continue;
|
|
648
|
+
const key = m[1];
|
|
649
|
+
let val = m[2].trim();
|
|
650
|
+
if (val.length >= 2 && (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"))) {
|
|
651
|
+
val = val.slice(1, -1);
|
|
652
|
+
}
|
|
653
|
+
out[key] = val;
|
|
715
654
|
}
|
|
716
|
-
return
|
|
655
|
+
return out;
|
|
717
656
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
657
|
+
function readEnvFile(envPath) {
|
|
658
|
+
if (!existsSync2(envPath)) return {};
|
|
659
|
+
return parseEnvFile(readFileSync(envPath, "utf8"));
|
|
660
|
+
}
|
|
661
|
+
async function ensureEnvGitignored(configsDir) {
|
|
662
|
+
const gitignorePath = path2.join(configsDir, ".gitignore");
|
|
663
|
+
const pattern = "*.env";
|
|
664
|
+
let existing = "";
|
|
665
|
+
if (existsSync2(gitignorePath)) {
|
|
666
|
+
existing = readFileSync(gitignorePath, "utf8");
|
|
667
|
+
const lines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
668
|
+
if (lines.includes(pattern)) return;
|
|
669
|
+
}
|
|
670
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
671
|
+
const header = existing.length === 0 ? "# Per-container env files hold the secrets behind the yml ${VAR}\n# references. Never commit them.\n" : "";
|
|
672
|
+
await fsp.appendFile(gitignorePath, `${prefix}${header}${pattern}
|
|
673
|
+
`);
|
|
736
674
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
"",
|
|
757
|
-
"`gh auth login` walks through OAuth in your browser.",
|
|
758
|
-
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
759
|
-
].join("\n")
|
|
675
|
+
var VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
676
|
+
function interpolate(value, vars) {
|
|
677
|
+
const missing = [];
|
|
678
|
+
const out = value.replace(VAR_RE, (_match, name) => {
|
|
679
|
+
if (Object.prototype.hasOwnProperty.call(vars, name)) return vars[name];
|
|
680
|
+
missing.push(name);
|
|
681
|
+
return _match;
|
|
682
|
+
});
|
|
683
|
+
return { value: out, missing };
|
|
684
|
+
}
|
|
685
|
+
function interpolateServices(services, vars) {
|
|
686
|
+
const missing = [];
|
|
687
|
+
const resolved = services.map((svc) => {
|
|
688
|
+
const interp = (raw, field) => {
|
|
689
|
+
const r = interpolate(raw, vars);
|
|
690
|
+
for (const name of r.missing) {
|
|
691
|
+
missing.push({ location: `services.${svc.name}.${field}`, name });
|
|
692
|
+
}
|
|
693
|
+
return r.value;
|
|
760
694
|
};
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
linuxBrew: "brew install glab",
|
|
769
|
-
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
770
|
-
});
|
|
771
|
-
return {
|
|
772
|
-
title: `${host} \u2014 GitLab`,
|
|
773
|
-
body: [
|
|
774
|
-
"Install the GitLab CLI (glab):",
|
|
775
|
-
install,
|
|
776
|
-
"",
|
|
777
|
-
"Then run once:",
|
|
778
|
-
cyan(`glab auth login${hostArg}`),
|
|
779
|
-
"",
|
|
780
|
-
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
781
|
-
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
782
|
-
"configures itself as the git credential helper."
|
|
783
|
-
].join("\n")
|
|
695
|
+
const next = {
|
|
696
|
+
...svc,
|
|
697
|
+
image: interp(svc.image, "image"),
|
|
698
|
+
env: Object.fromEntries(
|
|
699
|
+
Object.entries(svc.env).map(([k, v]) => [k, interp(v, `env.${k}`)])
|
|
700
|
+
),
|
|
701
|
+
volumes: svc.volumes.map((v, i) => interp(v, `volumes[${i}]`))
|
|
784
702
|
};
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
"Then store it via your OS credential helper:",
|
|
797
|
-
cyan(
|
|
798
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
799
|
-
)
|
|
800
|
-
].join("\n")
|
|
703
|
+
if (svc.command !== void 0) {
|
|
704
|
+
next.command = interp(svc.command, "command");
|
|
705
|
+
}
|
|
706
|
+
if (svc.healthcheck) {
|
|
707
|
+
const hc = svc.healthcheck;
|
|
708
|
+
next.healthcheck = {
|
|
709
|
+
...hc,
|
|
710
|
+
test: Array.isArray(hc.test) ? hc.test.map((t, i) => interp(t, `healthcheck.test[${i}]`)) : interp(hc.test, "healthcheck.test"),
|
|
711
|
+
...hc.interval !== void 0 ? { interval: interp(hc.interval, "healthcheck.interval") } : {},
|
|
712
|
+
...hc.timeout !== void 0 ? { timeout: interp(hc.timeout, "healthcheck.timeout") } : {},
|
|
713
|
+
...hc.startPeriod !== void 0 ? { startPeriod: interp(hc.startPeriod, "healthcheck.startPeriod") } : {}
|
|
801
714
|
};
|
|
802
715
|
}
|
|
803
|
-
return
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
716
|
+
return next;
|
|
717
|
+
});
|
|
718
|
+
return { services: resolved, missing };
|
|
719
|
+
}
|
|
720
|
+
function interpolateFeatures(features, vars) {
|
|
721
|
+
const missing = [];
|
|
722
|
+
const out = {};
|
|
723
|
+
for (const [ref, options] of Object.entries(features)) {
|
|
724
|
+
const next = {};
|
|
725
|
+
for (const [key, value] of Object.entries(options)) {
|
|
726
|
+
if (typeof value !== "string") {
|
|
727
|
+
next[key] = value;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const r = interpolate(value, vars);
|
|
731
|
+
for (const name of r.missing) {
|
|
732
|
+
missing.push({ location: `features.${ref}.${key}`, name });
|
|
733
|
+
}
|
|
734
|
+
next[key] = r.value;
|
|
735
|
+
}
|
|
736
|
+
out[ref] = next;
|
|
818
737
|
}
|
|
819
|
-
return {
|
|
820
|
-
title: `${host} \u2014 Gitea`,
|
|
821
|
-
body: [
|
|
822
|
-
"Gitea has no first-party CLI helper for git-credentials (the",
|
|
823
|
-
"`tea` CLI logs into its own config, not into your git credential",
|
|
824
|
-
"helper), so this is a manual one-time setup. Generate an access",
|
|
825
|
-
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
826
|
-
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
827
|
-
"least the `read:repository` scope (add `write:repository` if you",
|
|
828
|
-
"need push from the container).",
|
|
829
|
-
"",
|
|
830
|
-
"Then store it via your OS credential helper:",
|
|
831
|
-
cyan(
|
|
832
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
833
|
-
)
|
|
834
|
-
].join("\n")
|
|
835
|
-
};
|
|
738
|
+
return { features: out, missing };
|
|
836
739
|
}
|
|
837
|
-
function
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
740
|
+
function buildEnvStub(name) {
|
|
741
|
+
return `# Secrets and values for \${VAR} references in ${name}.yml.
|
|
742
|
+
`;
|
|
743
|
+
}
|
|
744
|
+
async function ensureEnvVars(envPath, name, vars) {
|
|
745
|
+
const exists = existsSync2(envPath);
|
|
746
|
+
let content = exists ? readFileSync(envPath, "utf8") : buildEnvStub(name);
|
|
747
|
+
const present = new Set(Object.keys(parseEnvFile(content)));
|
|
748
|
+
const added = [...new Set(vars)].filter((v) => !present.has(v));
|
|
749
|
+
if (!exists || added.length > 0) {
|
|
750
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
751
|
+
for (const v of added) content += `${v}=
|
|
752
|
+
`;
|
|
753
|
+
await fsp.mkdir(path2.dirname(envPath), { recursive: true });
|
|
754
|
+
await fsp.writeFile(envPath, content);
|
|
846
755
|
}
|
|
847
|
-
return
|
|
756
|
+
return { created: !exists, added };
|
|
848
757
|
}
|
|
849
|
-
function
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
return `
|
|
758
|
+
function formatMissingVarsError(missing, envPathPretty) {
|
|
759
|
+
const lines = missing.map((m) => ` - \${${m.name}} (${m.location})`);
|
|
760
|
+
const uniqueNames = [...new Set(missing.map((m) => m.name))];
|
|
761
|
+
return `Unresolved \${VAR} references in the container yml:
|
|
762
|
+
${lines.join("\n")}
|
|
763
|
+
|
|
764
|
+
Define them in ${envPathPretty}, e.g.
|
|
765
|
+
` + uniqueNames.map((n) => ` ${n}=<value>`).join("\n");
|
|
853
766
|
}
|
|
854
|
-
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
855
|
-
const credsDir = path2.join(devContainerRoot, ".monoceros");
|
|
856
|
-
const credentialsPath = path2.join(credsDir, "git-credentials");
|
|
857
|
-
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
858
|
-
const approveFn = options.approve ?? realGitCredentialApprove;
|
|
859
|
-
const logger = options.logger ?? { info: () => {
|
|
860
|
-
}, warn: () => {
|
|
861
|
-
} };
|
|
862
|
-
const lines = [];
|
|
863
|
-
const perHost = [];
|
|
864
|
-
for (const { host, provider } of hosts) {
|
|
865
|
-
if (provider === "unknown") {
|
|
866
|
-
perHost.push({
|
|
867
|
-
host,
|
|
868
|
-
provider: "github",
|
|
869
|
-
// placeholder — never rendered because pre-flight already bailed
|
|
870
|
-
status: "no-credentials",
|
|
871
|
-
detail: "provider not declared (internal: should not reach here)"
|
|
872
|
-
});
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
|
-
logger.info(`Fetching credentials for ${host} from host git\u2026`);
|
|
876
|
-
const input = `protocol=https
|
|
877
|
-
host=${host}
|
|
878
767
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
888
|
-
if (result.exitCode !== 0) {
|
|
889
|
-
perHost.push({
|
|
890
|
-
host,
|
|
891
|
-
provider,
|
|
892
|
-
status: "non-zero-exit",
|
|
893
|
-
detail: `exit code ${result.exitCode}`
|
|
894
|
-
});
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
const { username, password } = parseCredentialFillOutput(result.stdout);
|
|
898
|
-
if (!username || !password) {
|
|
899
|
-
perHost.push({
|
|
900
|
-
host,
|
|
901
|
-
provider,
|
|
902
|
-
status: "no-credentials",
|
|
903
|
-
detail: "host credential helper returned no username/password"
|
|
904
|
-
});
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
lines.push(formatCredentialLine(host, username, password));
|
|
908
|
-
perHost.push({ host, provider, status: "ok", detail: "" });
|
|
909
|
-
const approveInput = `protocol=https
|
|
910
|
-
host=${host}
|
|
911
|
-
username=${username}
|
|
912
|
-
password=${password}
|
|
913
|
-
|
|
914
|
-
`;
|
|
915
|
-
try {
|
|
916
|
-
await approveFn(approveInput);
|
|
917
|
-
} catch {
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
await fs2.mkdir(credsDir, { recursive: true });
|
|
921
|
-
await fs2.writeFile(
|
|
922
|
-
credentialsPath,
|
|
923
|
-
lines.join("\n") + (lines.length > 0 ? "\n" : ""),
|
|
924
|
-
{
|
|
925
|
-
mode: 384
|
|
768
|
+
// src/init/feature-doc.ts
|
|
769
|
+
function buildFeatureHeaderLines(summary, width) {
|
|
770
|
+
const paragraphs = buildHeaderParagraphs(summary);
|
|
771
|
+
const wrapped = [];
|
|
772
|
+
for (const para of paragraphs) {
|
|
773
|
+
for (const line of wrapToComment(para, width)) {
|
|
774
|
+
wrapped.push(line);
|
|
926
775
|
}
|
|
927
|
-
);
|
|
928
|
-
return {
|
|
929
|
-
hostsWritten: lines.length,
|
|
930
|
-
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
931
|
-
perHost,
|
|
932
|
-
credentialsPath
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
function formatMissingCredentialsError(missing) {
|
|
936
|
-
if (missing.length === 1) {
|
|
937
|
-
const m = missing[0];
|
|
938
|
-
const hint = providerSetupHint(m.host, m.provider);
|
|
939
|
-
return [
|
|
940
|
-
`Missing Git credentials: ${hint.title}`,
|
|
941
|
-
"",
|
|
942
|
-
hint.body,
|
|
943
|
-
"",
|
|
944
|
-
`Then re-run ${cyan("monoceros apply")}.`
|
|
945
|
-
].join("\n");
|
|
946
776
|
}
|
|
947
|
-
|
|
948
|
-
`Missing Git credentials for ${missing.length} hosts:`,
|
|
949
|
-
""
|
|
950
|
-
];
|
|
951
|
-
for (const m of missing) {
|
|
952
|
-
const hint = providerSetupHint(m.host, m.provider);
|
|
953
|
-
lines.push(hint.title);
|
|
954
|
-
lines.push("");
|
|
955
|
-
lines.push(hint.body);
|
|
956
|
-
lines.push("");
|
|
957
|
-
}
|
|
958
|
-
lines.push(`Then re-run ${cyan("monoceros apply")}.`);
|
|
959
|
-
return lines.join("\n");
|
|
960
|
-
}
|
|
961
|
-
function formatUnknownProviderError(hosts) {
|
|
962
|
-
const sorted = [...new Set(hosts)].sort();
|
|
963
|
-
const lines = [
|
|
964
|
-
sorted.length === 1 ? `Unknown Git provider for host ${sorted[0]}.` : `Unknown Git provider for ${sorted.length} hosts: ${sorted.join(", ")}.`,
|
|
965
|
-
"",
|
|
966
|
-
"Monoceros auto-detects only github.com / gitlab.com / bitbucket.org.",
|
|
967
|
-
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
968
|
-
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
969
|
-
"",
|
|
970
|
-
cyan(" repos:"),
|
|
971
|
-
cyan(` - url: https://${sorted[0]}/\u2026`),
|
|
972
|
-
cyan(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
973
|
-
"",
|
|
974
|
-
`Or re-add with ${cyan("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
975
|
-
];
|
|
976
|
-
return lines.join("\n");
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// src/devcontainer/locate-running.ts
|
|
980
|
-
import { spawn as spawn5 } from "child_process";
|
|
981
|
-
|
|
982
|
-
// src/devcontainer/compose.ts
|
|
983
|
-
import { spawn as spawn4 } from "child_process";
|
|
984
|
-
import { existsSync as existsSync2 } from "fs";
|
|
985
|
-
import path5 from "path";
|
|
986
|
-
import { consola } from "consola";
|
|
987
|
-
|
|
988
|
-
// src/proxy/index.ts
|
|
989
|
-
import { spawn as spawn2 } from "child_process";
|
|
990
|
-
import { promises as fs3 } from "fs";
|
|
991
|
-
import path3 from "path";
|
|
992
|
-
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
993
|
-
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
994
|
-
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
995
|
-
var defaultDockerExec = (args) => {
|
|
996
|
-
return new Promise((resolve, reject) => {
|
|
997
|
-
const child = spawn2("docker", args, {
|
|
998
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
999
|
-
});
|
|
1000
|
-
let stdout = "";
|
|
1001
|
-
let stderr = "";
|
|
1002
|
-
child.stdout.on("data", (chunk) => {
|
|
1003
|
-
stdout += chunk.toString();
|
|
1004
|
-
});
|
|
1005
|
-
child.stderr.on("data", (chunk) => {
|
|
1006
|
-
stderr += chunk.toString();
|
|
1007
|
-
});
|
|
1008
|
-
child.on("error", reject);
|
|
1009
|
-
child.on(
|
|
1010
|
-
"exit",
|
|
1011
|
-
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1012
|
-
);
|
|
1013
|
-
});
|
|
1014
|
-
};
|
|
1015
|
-
var realDocker = defaultDockerExec;
|
|
1016
|
-
function proxyDynamicDir(home) {
|
|
1017
|
-
return path3.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
777
|
+
return wrapped;
|
|
1018
778
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const inspect = await docker([
|
|
1023
|
-
"inspect",
|
|
1024
|
-
"--format",
|
|
1025
|
-
"{{.State.Running}}",
|
|
1026
|
-
PROXY_CONTAINER_NAME
|
|
1027
|
-
]);
|
|
1028
|
-
if (inspect.exitCode !== 0) return;
|
|
1029
|
-
if (inspect.stdout.trim() !== "true") return;
|
|
1030
|
-
await docker(["restart", PROXY_CONTAINER_NAME]);
|
|
779
|
+
function buildFeatureHeaderCommentBefore(summary, width) {
|
|
780
|
+
const lines = buildFeatureHeaderLines(summary, width);
|
|
781
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
1031
782
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
const state = await docker([
|
|
1046
|
-
"inspect",
|
|
1047
|
-
"--format",
|
|
1048
|
-
"{{.State.Running}}",
|
|
1049
|
-
PROXY_CONTAINER_NAME
|
|
1050
|
-
]);
|
|
1051
|
-
if (state.exitCode === 0) {
|
|
1052
|
-
if (state.stdout.trim() === "true") return;
|
|
1053
|
-
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
1054
|
-
if (start.exitCode !== 0) {
|
|
1055
|
-
throw new Error(
|
|
1056
|
-
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const hostPort = opts.hostPort ?? 80;
|
|
1062
|
-
const run = await docker([
|
|
1063
|
-
"run",
|
|
1064
|
-
"-d",
|
|
1065
|
-
"--name",
|
|
1066
|
-
PROXY_CONTAINER_NAME,
|
|
1067
|
-
"--network",
|
|
1068
|
-
PROXY_NETWORK_NAME,
|
|
1069
|
-
"-p",
|
|
1070
|
-
`${hostPort}:80`,
|
|
1071
|
-
"-v",
|
|
1072
|
-
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
1073
|
-
"--label",
|
|
1074
|
-
"monoceros.role=proxy",
|
|
1075
|
-
TRAEFIK_IMAGE,
|
|
1076
|
-
"--entrypoints.web.address=:80",
|
|
1077
|
-
"--providers.file.directory=/etc/traefik/dynamic",
|
|
1078
|
-
"--providers.file.watch=true",
|
|
1079
|
-
"--providers.docker=false",
|
|
1080
|
-
"--api.dashboard=false",
|
|
1081
|
-
"--log.level=INFO"
|
|
1082
|
-
]);
|
|
1083
|
-
if (run.exitCode !== 0) {
|
|
1084
|
-
throw new Error(
|
|
1085
|
-
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
1086
|
-
);
|
|
783
|
+
function buildHeaderParagraphs(summary) {
|
|
784
|
+
if (!summary) return [];
|
|
785
|
+
const out = [];
|
|
786
|
+
const tagline = summary.name?.trim();
|
|
787
|
+
const description = summary.description?.trim();
|
|
788
|
+
if (tagline && description) {
|
|
789
|
+
out.push(`${tagline} \u2014 ${description}`);
|
|
790
|
+
} else if (tagline) {
|
|
791
|
+
out.push(tagline);
|
|
792
|
+
} else if (description) {
|
|
793
|
+
out.push(description);
|
|
1087
794
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1092
|
-
async function maybeStopProxy(opts = {}) {
|
|
1093
|
-
const docker = opts.docker ?? realDocker;
|
|
1094
|
-
const logger = opts.logger;
|
|
1095
|
-
const inspect = await docker([
|
|
1096
|
-
"network",
|
|
1097
|
-
"inspect",
|
|
1098
|
-
PROXY_NETWORK_NAME,
|
|
1099
|
-
"--format",
|
|
1100
|
-
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
1101
|
-
]);
|
|
1102
|
-
if (inspect.exitCode !== 0) {
|
|
1103
|
-
return;
|
|
795
|
+
for (const note of summary.usageNotes) {
|
|
796
|
+
const trimmed = note.trim();
|
|
797
|
+
if (trimmed.length > 0) out.push(trimmed);
|
|
1104
798
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
);
|
|
1113
|
-
return;
|
|
799
|
+
if (summary.optionHints.length > 0) {
|
|
800
|
+
const parts = summary.optionHints.map((key) => {
|
|
801
|
+
const desc = summary.optionDescriptions[key];
|
|
802
|
+
const short = desc ? shortenOptionDescription(desc) : void 0;
|
|
803
|
+
return short ? `${key} (${short})` : key;
|
|
804
|
+
});
|
|
805
|
+
out.push(`Options: ${parts.join(", ")}.`);
|
|
1114
806
|
}
|
|
1115
|
-
|
|
1116
|
-
`
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// src/util/mask-secrets.ts
|
|
1121
|
-
import { Transform } from "stream";
|
|
1122
|
-
var PATTERNS = [
|
|
1123
|
-
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
1124
|
-
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
1125
|
-
// matching unrelated all-caps words.
|
|
1126
|
-
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
1127
|
-
// Bitbucket Cloud app password.
|
|
1128
|
-
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
1129
|
-
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
1130
|
-
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
1131
|
-
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
1132
|
-
// GitHub fine-grained PAT.
|
|
1133
|
-
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
1134
|
-
// Anthropic API key.
|
|
1135
|
-
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
1136
|
-
];
|
|
1137
|
-
function maskSecrets(text) {
|
|
1138
|
-
let result = text;
|
|
1139
|
-
for (const { re } of PATTERNS) {
|
|
1140
|
-
result = result.replace(re, maskOne);
|
|
807
|
+
if (summary.documentationURL) {
|
|
808
|
+
out.push(`See ${summary.documentationURL} for further information.`);
|
|
1141
809
|
}
|
|
1142
|
-
return
|
|
810
|
+
return out;
|
|
1143
811
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
return
|
|
812
|
+
function shortenOptionDescription(desc) {
|
|
813
|
+
const firstSentence = desc.split(/(?<=[.!?])\s+/)[0]?.trim() ?? desc.trim();
|
|
814
|
+
return firstSentence.replace(/[.!?]+$/, "").trim();
|
|
1147
815
|
}
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
return
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
flush(cb) {
|
|
1165
|
-
if (buffer.length > 0) {
|
|
1166
|
-
const tail = maskSecrets(buffer);
|
|
1167
|
-
buffer = "";
|
|
1168
|
-
cb(null, tail);
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
cb(null);
|
|
816
|
+
function wrapToComment(text, width) {
|
|
817
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
818
|
+
if (words.length === 0) return [""];
|
|
819
|
+
const usable = Math.max(width, 20);
|
|
820
|
+
const lines = [];
|
|
821
|
+
let current = "";
|
|
822
|
+
for (const w of words) {
|
|
823
|
+
if (current.length === 0) {
|
|
824
|
+
current = w;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (current.length + 1 + w.length <= usable) {
|
|
828
|
+
current += " " + w;
|
|
829
|
+
} else {
|
|
830
|
+
lines.push(current);
|
|
831
|
+
current = w;
|
|
1172
832
|
}
|
|
833
|
+
}
|
|
834
|
+
if (current.length > 0) lines.push(current);
|
|
835
|
+
return lines;
|
|
836
|
+
}
|
|
837
|
+
var FEATURE_HEADER_WIDTH = 76 - 2;
|
|
838
|
+
function featureOptionVarName(ref, optionKey) {
|
|
839
|
+
const leaf = ref.split("/").pop() ?? ref;
|
|
840
|
+
const id = leaf.split("@")[0].split(":")[0];
|
|
841
|
+
const idSnake = id.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
842
|
+
const optSnake = optionKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
843
|
+
return `${idSnake}_${optSnake}`;
|
|
844
|
+
}
|
|
845
|
+
function featureOptionHints(summary, ref, activeKeys = []) {
|
|
846
|
+
return (summary?.optionHints ?? []).filter((key) => !activeKeys.includes(key)).map((key) => {
|
|
847
|
+
const envVar = featureOptionVarName(ref, key);
|
|
848
|
+
return { key, envVar, placeholder: `\${${envVar}}` };
|
|
1173
849
|
});
|
|
1174
850
|
}
|
|
1175
851
|
|
|
1176
|
-
// src/
|
|
1177
|
-
import {
|
|
1178
|
-
import
|
|
1179
|
-
import { createRequire } from "module";
|
|
1180
|
-
import path4 from "path";
|
|
852
|
+
// src/init/manifest.ts
|
|
853
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
854
|
+
import path3 from "path";
|
|
1181
855
|
|
|
1182
|
-
// src/
|
|
1183
|
-
|
|
1184
|
-
var
|
|
1185
|
-
var
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
return
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
const flushable = buffer.slice(0, lastNewline + 1);
|
|
1205
|
-
buffer = buffer.slice(lastNewline + 1);
|
|
1206
|
-
cb(null, appendHintIfMarker(flushable));
|
|
1207
|
-
},
|
|
1208
|
-
flush(cb) {
|
|
1209
|
-
if (buffer.length === 0) {
|
|
1210
|
-
cb(null);
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
const tail = buffer;
|
|
1214
|
-
buffer = "";
|
|
1215
|
-
cb(null, appendHintIfMarker(tail));
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
856
|
+
// src/util/ref.ts
|
|
857
|
+
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
858
|
+
var FEATURE_TAG_CHARSET = "[a-z0-9._-]+";
|
|
859
|
+
var MONOCEROS_FEATURE_RE = new RegExp(
|
|
860
|
+
`^ghcr\\.io/getmonoceros/monoceros-features/(${FEATURE_NAME_CHARSET}):${FEATURE_TAG_CHARSET}$`
|
|
861
|
+
);
|
|
862
|
+
var DEPRECATED_MONOCEROS_FEATURE_RE = new RegExp(
|
|
863
|
+
`^ghcr\\.io/monoceros/features/(${FEATURE_NAME_CHARSET}):(${FEATURE_TAG_CHARSET})$`
|
|
864
|
+
);
|
|
865
|
+
function matchMonocerosFeature(ref) {
|
|
866
|
+
const match = MONOCEROS_FEATURE_RE.exec(ref);
|
|
867
|
+
if (!match) return null;
|
|
868
|
+
return { name: match[1] };
|
|
869
|
+
}
|
|
870
|
+
function migrateDeprecatedFeatureRef(ref) {
|
|
871
|
+
const match = DEPRECATED_MONOCEROS_FEATURE_RE.exec(ref);
|
|
872
|
+
if (!match) return null;
|
|
873
|
+
const name = match[1];
|
|
874
|
+
const tag = match[2];
|
|
875
|
+
return `ghcr.io/getmonoceros/monoceros-features/${name}:${tag}`;
|
|
1218
876
|
}
|
|
1219
877
|
|
|
1220
|
-
// src/
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
878
|
+
// src/init/manifest.ts
|
|
879
|
+
function resolveManifestPath(name, checkoutRoot) {
|
|
880
|
+
if (checkoutRoot) {
|
|
881
|
+
const checkoutPath = path3.join(
|
|
882
|
+
checkoutRoot,
|
|
883
|
+
"images",
|
|
884
|
+
"features",
|
|
885
|
+
name,
|
|
886
|
+
"devcontainer-feature.json"
|
|
887
|
+
);
|
|
888
|
+
if (existsSync3(checkoutPath)) return checkoutPath;
|
|
1230
889
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
890
|
+
const bundlePath = path3.join(
|
|
891
|
+
bundledFeaturesDir(),
|
|
892
|
+
name,
|
|
893
|
+
"devcontainer-feature.json"
|
|
894
|
+
);
|
|
895
|
+
if (existsSync3(bundlePath)) return bundlePath;
|
|
896
|
+
return null;
|
|
1233
897
|
}
|
|
1234
|
-
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
process.stderr.write(
|
|
1260
|
-
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
1261
|
-
);
|
|
1262
|
-
process.stderr.write(
|
|
1263
|
-
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
1264
|
-
);
|
|
898
|
+
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
899
|
+
const match = matchMonocerosFeature(ref);
|
|
900
|
+
if (!match) return void 0;
|
|
901
|
+
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
902
|
+
if (!manifestPath) return void 0;
|
|
903
|
+
try {
|
|
904
|
+
const text = readFileSync2(manifestPath, "utf8");
|
|
905
|
+
const parsed = JSON.parse(text);
|
|
906
|
+
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
907
|
+
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
908
|
+
(x) => typeof x === "string" && x.length > 0
|
|
909
|
+
) : [];
|
|
910
|
+
const rawNotes = parsed["x-monoceros"]?.usageNotes;
|
|
911
|
+
const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
|
|
912
|
+
(x) => typeof x === "string" && x.length > 0
|
|
913
|
+
) : [];
|
|
914
|
+
const optionDescriptions = {};
|
|
915
|
+
const optionTypes = {};
|
|
916
|
+
const optionNames = [];
|
|
917
|
+
if (parsed.options) {
|
|
918
|
+
for (const [key, opt] of Object.entries(parsed.options)) {
|
|
919
|
+
if (!opt || typeof opt !== "object") continue;
|
|
920
|
+
optionNames.push(key);
|
|
921
|
+
if (typeof opt.description === "string" && opt.description.length > 0) {
|
|
922
|
+
optionDescriptions[key] = opt.description;
|
|
1265
923
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
924
|
+
if (opt.type === "boolean") {
|
|
925
|
+
optionTypes[key] = "boolean";
|
|
926
|
+
} else if (opt.type === "string") {
|
|
927
|
+
optionTypes[key] = "string";
|
|
928
|
+
}
|
|
929
|
+
}
|
|
1269
930
|
}
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
931
|
+
const name = typeof parsed.name === "string" ? parsed.name : "";
|
|
932
|
+
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
933
|
+
const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
|
|
934
|
+
const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
|
|
935
|
+
return {
|
|
936
|
+
name,
|
|
937
|
+
description,
|
|
938
|
+
documentationURL,
|
|
939
|
+
optionHints,
|
|
940
|
+
optionDescriptions,
|
|
941
|
+
optionNames,
|
|
942
|
+
optionTypes,
|
|
943
|
+
usageNotes
|
|
944
|
+
};
|
|
945
|
+
} catch {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
1277
949
|
|
|
1278
|
-
// src/devcontainer/
|
|
1279
|
-
|
|
950
|
+
// src/devcontainer/credentials.ts
|
|
951
|
+
import { spawn } from "child_process";
|
|
952
|
+
import { promises as fs2 } from "fs";
|
|
953
|
+
import path4 from "path";
|
|
954
|
+
|
|
955
|
+
// src/util/format.ts
|
|
956
|
+
var ESC = "\x1B[";
|
|
957
|
+
var ANSI_BOLD2 = `${ESC}1m`;
|
|
958
|
+
var ANSI_UNDERLINE2 = `${ESC}4m`;
|
|
959
|
+
var ANSI_CYAN2 = `${ESC}36m`;
|
|
960
|
+
var ANSI_GREY2 = `${ESC}90m`;
|
|
961
|
+
var ANSI_RESET2 = `${ESC}0m`;
|
|
962
|
+
function makeWrap(isTty2) {
|
|
963
|
+
return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET2 : s;
|
|
964
|
+
}
|
|
965
|
+
function makePalette(isTty2) {
|
|
966
|
+
const wrap = makeWrap(isTty2);
|
|
967
|
+
return {
|
|
968
|
+
bold: (s) => wrap(s, ANSI_BOLD2),
|
|
969
|
+
underline: (s) => wrap(s, ANSI_UNDERLINE2),
|
|
970
|
+
cyan: (s) => wrap(s, ANSI_CYAN2),
|
|
971
|
+
dim: (s) => wrap(s, ANSI_GREY2),
|
|
972
|
+
sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function colorsFor(stream) {
|
|
976
|
+
return makePalette(stream.isTTY ?? false);
|
|
977
|
+
}
|
|
978
|
+
var stderrPalette = makePalette(process.stderr.isTTY ?? false);
|
|
979
|
+
var bold2 = stderrPalette.bold;
|
|
980
|
+
var underline2 = stderrPalette.underline;
|
|
981
|
+
var cyan2 = stderrPalette.cyan;
|
|
982
|
+
var dim = stderrPalette.dim;
|
|
983
|
+
var sectionLine = stderrPalette.sectionLine;
|
|
984
|
+
|
|
985
|
+
// src/devcontainer/credentials.ts
|
|
986
|
+
var realGitCredentialFill = (input) => {
|
|
1280
987
|
return new Promise((resolve, reject) => {
|
|
1281
|
-
const child =
|
|
1282
|
-
|
|
1283
|
-
|
|
988
|
+
const child = spawn("git", ["credential", "fill"], {
|
|
989
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
990
|
+
env: {
|
|
991
|
+
...process.env,
|
|
992
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
let stdout = "";
|
|
996
|
+
child.stdout.on("data", (chunk) => {
|
|
997
|
+
stdout += chunk.toString();
|
|
1284
998
|
});
|
|
1285
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
1286
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
1287
999
|
child.on("error", reject);
|
|
1288
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
1000
|
+
child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
|
|
1001
|
+
child.stdin.write(input);
|
|
1002
|
+
child.stdin.end();
|
|
1289
1003
|
});
|
|
1290
1004
|
};
|
|
1291
|
-
var
|
|
1005
|
+
var realGitCredentialApprove = (input) => {
|
|
1292
1006
|
return new Promise((resolve, reject) => {
|
|
1293
|
-
const child =
|
|
1294
|
-
stdio: ["
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
stdout += chunk.toString("utf8");
|
|
1300
|
-
});
|
|
1301
|
-
child.stderr?.on("data", (chunk) => {
|
|
1302
|
-
stderr += chunk.toString("utf8");
|
|
1007
|
+
const child = spawn("git", ["credential", "approve"], {
|
|
1008
|
+
stdio: ["pipe", "ignore", "inherit"],
|
|
1009
|
+
env: {
|
|
1010
|
+
...process.env,
|
|
1011
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1012
|
+
}
|
|
1303
1013
|
});
|
|
1304
1014
|
child.on("error", reject);
|
|
1305
|
-
child.on(
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
);
|
|
1015
|
+
child.on("exit", () => resolve());
|
|
1016
|
+
child.stdin.write(input);
|
|
1017
|
+
child.stdin.end();
|
|
1309
1018
|
});
|
|
1310
1019
|
};
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1020
|
+
function resolveProvider(host, explicit) {
|
|
1021
|
+
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
1022
|
+
if (canonical) return canonical;
|
|
1023
|
+
return explicit ?? "unknown";
|
|
1024
|
+
}
|
|
1025
|
+
function uniqueHttpsHosts(repos) {
|
|
1026
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
1027
|
+
for (const repo of repos) {
|
|
1028
|
+
if (!repo.url.startsWith("https://")) continue;
|
|
1029
|
+
let host;
|
|
1030
|
+
try {
|
|
1031
|
+
host = new URL(repo.url).hostname;
|
|
1032
|
+
} catch {
|
|
1033
|
+
continue;
|
|
1319
1034
|
}
|
|
1035
|
+
if (byHost.has(host)) continue;
|
|
1036
|
+
byHost.set(host, { host, provider: resolveProvider(host, repo.provider) });
|
|
1320
1037
|
}
|
|
1321
|
-
return [...
|
|
1038
|
+
return [...byHost.values()];
|
|
1322
1039
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1040
|
+
var BREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
|
|
1041
|
+
function installCommandForOS(opts) {
|
|
1042
|
+
const withBrewBootstrap = (cmd) => [
|
|
1043
|
+
"",
|
|
1044
|
+
cyan2(BREW_INSTALL_COMMAND),
|
|
1045
|
+
cyan2(cmd),
|
|
1046
|
+
"",
|
|
1047
|
+
dim("(Skip the first line if you already have Homebrew.)")
|
|
1048
|
+
].join("\n");
|
|
1049
|
+
if (process.platform === "darwin") return withBrewBootstrap(opts.brew);
|
|
1050
|
+
if (opts.linuxBrew) return withBrewBootstrap(opts.linuxBrew);
|
|
1051
|
+
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
1052
|
+
}
|
|
1053
|
+
function providerSetupHint(host, provider) {
|
|
1054
|
+
if (provider === "github") {
|
|
1055
|
+
const isSaas = host.toLowerCase() === "github.com";
|
|
1056
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
1057
|
+
const install = installCommandForOS({
|
|
1058
|
+
brew: "brew install gh",
|
|
1059
|
+
linuxBrew: "brew install gh",
|
|
1060
|
+
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
1061
|
+
});
|
|
1062
|
+
return {
|
|
1063
|
+
title: `${host} \u2014 GitHub`,
|
|
1064
|
+
body: [
|
|
1065
|
+
"Install the GitHub CLI:",
|
|
1066
|
+
install,
|
|
1067
|
+
"",
|
|
1068
|
+
"Then run once:",
|
|
1069
|
+
cyan2(`gh auth login${hostArg}`),
|
|
1070
|
+
cyan2(`gh auth setup-git${hostArg}`),
|
|
1071
|
+
"",
|
|
1072
|
+
"`gh auth login` walks through OAuth in your browser.",
|
|
1073
|
+
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
1074
|
+
].join("\n")
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
if (provider === "gitlab") {
|
|
1078
|
+
const isSaas = host.toLowerCase() === "gitlab.com";
|
|
1079
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
1080
|
+
const install = installCommandForOS({
|
|
1081
|
+
brew: "brew install glab",
|
|
1082
|
+
linuxBrew: "brew install glab",
|
|
1083
|
+
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
1084
|
+
});
|
|
1085
|
+
return {
|
|
1086
|
+
title: `${host} \u2014 GitLab`,
|
|
1087
|
+
body: [
|
|
1088
|
+
"Install the GitLab CLI (glab):",
|
|
1089
|
+
install,
|
|
1090
|
+
"",
|
|
1091
|
+
"Then run once:",
|
|
1092
|
+
cyan2(`glab auth login${hostArg}`),
|
|
1093
|
+
"",
|
|
1094
|
+
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
1095
|
+
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
1096
|
+
"configures itself as the git credential helper."
|
|
1097
|
+
].join("\n")
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (provider === "bitbucket") {
|
|
1101
|
+
const isCloud = host.toLowerCase() === "bitbucket.org";
|
|
1102
|
+
if (isCloud) {
|
|
1103
|
+
return {
|
|
1104
|
+
title: `${host} \u2014 Bitbucket Cloud`,
|
|
1105
|
+
body: [
|
|
1106
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
1107
|
+
"is a manual one-time setup. Generate an Atlassian API token at",
|
|
1108
|
+
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
1109
|
+
"",
|
|
1110
|
+
"Then store it via your OS credential helper:",
|
|
1111
|
+
cyan2(
|
|
1112
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
1113
|
+
)
|
|
1114
|
+
].join("\n")
|
|
1115
|
+
};
|
|
1335
1116
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1117
|
+
return {
|
|
1118
|
+
title: `${host} \u2014 Bitbucket Data Center`,
|
|
1119
|
+
body: [
|
|
1120
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
1121
|
+
"is a manual one-time setup. Generate a personal HTTP access",
|
|
1122
|
+
`token in your Bitbucket UI: profile picture (top right on ${host})`,
|
|
1123
|
+
"\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
|
|
1124
|
+
"at least repo-read + repo-write scopes for the repos you need.",
|
|
1125
|
+
"",
|
|
1126
|
+
"Then store it via your OS credential helper:",
|
|
1127
|
+
cyan2(
|
|
1128
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
1129
|
+
)
|
|
1130
|
+
].join("\n")
|
|
1131
|
+
};
|
|
1338
1132
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1133
|
+
return {
|
|
1134
|
+
title: `${host} \u2014 Gitea`,
|
|
1135
|
+
body: [
|
|
1136
|
+
"Gitea has no first-party CLI helper for git-credentials (the",
|
|
1137
|
+
"`tea` CLI logs into its own config, not into your git credential",
|
|
1138
|
+
"helper), so this is a manual one-time setup. Generate an access",
|
|
1139
|
+
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
1140
|
+
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
1141
|
+
"least the `read:repository` scope (add `write:repository` if you",
|
|
1142
|
+
"need push from the container).",
|
|
1143
|
+
"",
|
|
1144
|
+
"Then store it via your OS credential helper:",
|
|
1145
|
+
cyan2(
|
|
1146
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
1147
|
+
)
|
|
1148
|
+
].join("\n")
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
function parseCredentialFillOutput(output) {
|
|
1152
|
+
const result = {};
|
|
1153
|
+
for (const line of output.split("\n")) {
|
|
1154
|
+
const eqIdx = line.indexOf("=");
|
|
1155
|
+
if (eqIdx <= 0) continue;
|
|
1156
|
+
const key = line.slice(0, eqIdx);
|
|
1157
|
+
const value = line.slice(eqIdx + 1);
|
|
1158
|
+
if (key === "username") result.username = value;
|
|
1159
|
+
if (key === "password") result.password = value;
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
function formatCredentialLine(host, username, password) {
|
|
1164
|
+
const encUser = encodeURIComponent(username);
|
|
1165
|
+
const encPass = encodeURIComponent(password);
|
|
1166
|
+
return `https://${encUser}:${encPass}@${host}`;
|
|
1167
|
+
}
|
|
1168
|
+
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
1169
|
+
const credsDir = path4.join(devContainerRoot, ".monoceros");
|
|
1170
|
+
const credentialsPath = path4.join(credsDir, "git-credentials");
|
|
1171
|
+
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
1172
|
+
const approveFn = options.approve ?? realGitCredentialApprove;
|
|
1173
|
+
const logger = options.logger ?? { info: () => {
|
|
1174
|
+
}, warn: () => {
|
|
1175
|
+
} };
|
|
1176
|
+
const lines = [];
|
|
1177
|
+
const perHost = [];
|
|
1178
|
+
for (const { host, provider } of hosts) {
|
|
1179
|
+
if (provider === "unknown") {
|
|
1180
|
+
perHost.push({
|
|
1181
|
+
host,
|
|
1182
|
+
provider: "github",
|
|
1183
|
+
// placeholder — never rendered because pre-flight already bailed
|
|
1184
|
+
status: "no-credentials",
|
|
1185
|
+
detail: "provider not declared (internal: should not reach here)"
|
|
1186
|
+
});
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
logger.info(`Fetching credentials for ${host} from host git\u2026`);
|
|
1190
|
+
const input = `protocol=https
|
|
1191
|
+
host=${host}
|
|
1192
|
+
|
|
1193
|
+
`;
|
|
1194
|
+
let result;
|
|
1195
|
+
try {
|
|
1196
|
+
result = await spawnFn(input);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1199
|
+
perHost.push({ host, provider, status: "spawn-error", detail });
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (result.exitCode !== 0) {
|
|
1203
|
+
perHost.push({
|
|
1204
|
+
host,
|
|
1205
|
+
provider,
|
|
1206
|
+
status: "non-zero-exit",
|
|
1207
|
+
detail: `exit code ${result.exitCode}`
|
|
1208
|
+
});
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const { username, password } = parseCredentialFillOutput(result.stdout);
|
|
1212
|
+
if (!username || !password) {
|
|
1213
|
+
perHost.push({
|
|
1214
|
+
host,
|
|
1215
|
+
provider,
|
|
1216
|
+
status: "no-credentials",
|
|
1217
|
+
detail: "host credential helper returned no username/password"
|
|
1218
|
+
});
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
lines.push(formatCredentialLine(host, username, password));
|
|
1222
|
+
perHost.push({ host, provider, status: "ok", detail: "" });
|
|
1223
|
+
const approveInput = `protocol=https
|
|
1224
|
+
host=${host}
|
|
1225
|
+
username=${username}
|
|
1226
|
+
password=${password}
|
|
1227
|
+
|
|
1228
|
+
`;
|
|
1229
|
+
try {
|
|
1230
|
+
await approveFn(approveInput);
|
|
1231
|
+
} catch {
|
|
1343
1232
|
}
|
|
1344
1233
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
(_, drive) => `${drive.toLowerCase()}:`
|
|
1234
|
+
await fs2.mkdir(credsDir, { recursive: true });
|
|
1235
|
+
await fs2.writeFile(
|
|
1236
|
+
credentialsPath,
|
|
1237
|
+
lines.join("\n") + (lines.length > 0 ? "\n" : ""),
|
|
1238
|
+
{
|
|
1239
|
+
mode: 384
|
|
1240
|
+
}
|
|
1353
1241
|
);
|
|
1242
|
+
return {
|
|
1243
|
+
hostsWritten: lines.length,
|
|
1244
|
+
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
1245
|
+
perHost,
|
|
1246
|
+
credentialsPath
|
|
1247
|
+
};
|
|
1354
1248
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
throw new Error(
|
|
1367
|
-
`No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
|
|
1368
|
-
);
|
|
1249
|
+
function formatMissingCredentialsError(missing) {
|
|
1250
|
+
if (missing.length === 1) {
|
|
1251
|
+
const m = missing[0];
|
|
1252
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
1253
|
+
return [
|
|
1254
|
+
`Missing Git credentials: ${hint.title}`,
|
|
1255
|
+
"",
|
|
1256
|
+
hint.body,
|
|
1257
|
+
"",
|
|
1258
|
+
`Then re-run ${cyan2("monoceros apply")}.`
|
|
1259
|
+
].join("\n");
|
|
1369
1260
|
}
|
|
1370
|
-
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
const logger = opts.logger ?? { info: (msg) => consola.info(msg) };
|
|
1381
|
-
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
1382
|
-
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
1383
|
-
return spawnFn(
|
|
1384
|
-
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
1385
|
-
opts.root
|
|
1386
|
-
);
|
|
1387
|
-
}
|
|
1388
|
-
async function runContainerCycle(root, opts) {
|
|
1389
|
-
const { hasCompose, logger } = opts;
|
|
1390
|
-
if (hasCompose) {
|
|
1391
|
-
const projectName = composeProjectName(root);
|
|
1392
|
-
logger.info(
|
|
1393
|
-
`Force-removing existing ${projectName} containers (volumes preserved)\u2026`
|
|
1394
|
-
);
|
|
1395
|
-
const exec = opts.dockerExec ?? spawnDocker;
|
|
1396
|
-
const filters = [
|
|
1397
|
-
`label=com.docker.compose.project=${projectName}`,
|
|
1398
|
-
`name=^${projectName}-`
|
|
1399
|
-
];
|
|
1400
|
-
const { exitCode: rmExit } = await cleanupDockerObjects({
|
|
1401
|
-
projectName,
|
|
1402
|
-
filters,
|
|
1403
|
-
network: `${projectName}_default`,
|
|
1404
|
-
logger,
|
|
1405
|
-
exec
|
|
1406
|
-
});
|
|
1407
|
-
if (rmExit !== 0) return rmExit;
|
|
1408
|
-
const remaining = await findContainerIds(filters, exec);
|
|
1409
|
-
if (remaining.length > 0) {
|
|
1410
|
-
const warn = logger.warn ?? logger.info;
|
|
1411
|
-
warn(
|
|
1412
|
-
`ERROR: containers under project ${projectName} reappeared after removal.
|
|
1413
|
-
This typically means VS Code's Remote Containers extension is connected
|
|
1414
|
-
to this devcontainer and auto-recreated it. Close the dev container
|
|
1415
|
-
session in VS Code (Cmd+Shift+P \u2192 'Dev Containers: Close Remote Connection')
|
|
1416
|
-
and retry \`monoceros apply\`.`
|
|
1417
|
-
);
|
|
1418
|
-
return 1;
|
|
1419
|
-
}
|
|
1420
|
-
return runStart({
|
|
1421
|
-
root,
|
|
1422
|
-
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
1423
|
-
logger
|
|
1424
|
-
});
|
|
1261
|
+
const lines = [
|
|
1262
|
+
`Missing Git credentials for ${missing.length} hosts:`,
|
|
1263
|
+
""
|
|
1264
|
+
];
|
|
1265
|
+
for (const m of missing) {
|
|
1266
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
1267
|
+
lines.push(hint.title);
|
|
1268
|
+
lines.push("");
|
|
1269
|
+
lines.push(hint.body);
|
|
1270
|
+
lines.push("");
|
|
1425
1271
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
return spawnFn(
|
|
1429
|
-
[
|
|
1430
|
-
"up",
|
|
1431
|
-
"--workspace-folder",
|
|
1432
|
-
root,
|
|
1433
|
-
"--mount-workspace-git-root=false",
|
|
1434
|
-
"--remove-existing-container"
|
|
1435
|
-
],
|
|
1436
|
-
root
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
function runStop(opts) {
|
|
1440
|
-
return runComposeAction(
|
|
1441
|
-
(service) => ["stop", ...service ? [service] : []],
|
|
1442
|
-
opts
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
function runStatus(opts) {
|
|
1446
|
-
return runComposeAction(
|
|
1447
|
-
(service) => ["ps", ...service ? [service] : []],
|
|
1448
|
-
opts
|
|
1449
|
-
);
|
|
1272
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
1273
|
+
return lines.join("\n");
|
|
1450
1274
|
}
|
|
1451
|
-
function
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
)
|
|
1275
|
+
function formatUnknownProviderError(hosts) {
|
|
1276
|
+
const sorted = [...new Set(hosts)].sort();
|
|
1277
|
+
const lines = [
|
|
1278
|
+
sorted.length === 1 ? `Unknown Git provider for host ${sorted[0]}.` : `Unknown Git provider for ${sorted.length} hosts: ${sorted.join(", ")}.`,
|
|
1279
|
+
"",
|
|
1280
|
+
"Monoceros auto-detects only github.com / gitlab.com / bitbucket.org.",
|
|
1281
|
+
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
1282
|
+
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
1283
|
+
"",
|
|
1284
|
+
cyan2(" repos:"),
|
|
1285
|
+
cyan2(` - url: https://${sorted[0]}/\u2026`),
|
|
1286
|
+
cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
1287
|
+
"",
|
|
1288
|
+
`Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
1289
|
+
];
|
|
1290
|
+
return lines.join("\n");
|
|
1461
1291
|
}
|
|
1462
1292
|
|
|
1463
1293
|
// src/devcontainer/locate-running.ts
|
|
1294
|
+
import { spawn as spawn2 } from "child_process";
|
|
1464
1295
|
var realDockerLookup = (args) => {
|
|
1465
1296
|
return new Promise((resolve, reject) => {
|
|
1466
|
-
const child =
|
|
1297
|
+
const child = spawn2("docker", args, {
|
|
1467
1298
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1468
1299
|
});
|
|
1469
1300
|
let stdout = "";
|
|
@@ -1487,7 +1318,7 @@ async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
|
1487
1318
|
"ps",
|
|
1488
1319
|
"-q",
|
|
1489
1320
|
"--filter",
|
|
1490
|
-
`label=devcontainer.local_folder=${
|
|
1321
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
1491
1322
|
"--filter",
|
|
1492
1323
|
"status=running"
|
|
1493
1324
|
]);
|
|
@@ -1497,7 +1328,7 @@ async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
|
1497
1328
|
}
|
|
1498
1329
|
var realContainerExec = (containerId, argv) => {
|
|
1499
1330
|
return new Promise((resolve, reject) => {
|
|
1500
|
-
const child =
|
|
1331
|
+
const child = spawn2("docker", ["exec", containerId, ...argv], {
|
|
1501
1332
|
// Inherit stdio so live git output reaches the user.
|
|
1502
1333
|
stdio: ["ignore", "inherit", "inherit"]
|
|
1503
1334
|
});
|
|
@@ -1507,7 +1338,7 @@ var realContainerExec = (containerId, argv) => {
|
|
|
1507
1338
|
};
|
|
1508
1339
|
|
|
1509
1340
|
// src/config/global.ts
|
|
1510
|
-
import { promises as
|
|
1341
|
+
import { promises as fs3 } from "fs";
|
|
1511
1342
|
import { z as z2 } from "zod";
|
|
1512
1343
|
import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
|
|
1513
1344
|
var SCHEMA_VERSION = 1;
|
|
@@ -1550,7 +1381,7 @@ async function readMonocerosConfig(opts = {}) {
|
|
|
1550
1381
|
const filePath = monocerosConfigPath(home);
|
|
1551
1382
|
let text;
|
|
1552
1383
|
try {
|
|
1553
|
-
text = await
|
|
1384
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
1554
1385
|
} catch {
|
|
1555
1386
|
return void 0;
|
|
1556
1387
|
}
|
|
@@ -1587,7 +1418,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
|
1587
1418
|
const filePath = monocerosConfigPath(home);
|
|
1588
1419
|
let text;
|
|
1589
1420
|
try {
|
|
1590
|
-
text = await
|
|
1421
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
1591
1422
|
} catch {
|
|
1592
1423
|
text = void 0;
|
|
1593
1424
|
}
|
|
@@ -1604,8 +1435,8 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
|
1604
1435
|
` email: ${user.email}`,
|
|
1605
1436
|
""
|
|
1606
1437
|
].join("\n");
|
|
1607
|
-
await
|
|
1608
|
-
await
|
|
1438
|
+
await fs3.mkdir(home, { recursive: true });
|
|
1439
|
+
await fs3.writeFile(filePath, fresh, "utf8");
|
|
1609
1440
|
return { filePath, created: true, alreadySet: false };
|
|
1610
1441
|
}
|
|
1611
1442
|
const doc = parseDocument2(text, { prettyErrors: true });
|
|
@@ -1626,7 +1457,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
|
1626
1457
|
userMap.set("name", user.name);
|
|
1627
1458
|
userMap.set("email", user.email);
|
|
1628
1459
|
const newText = String(doc);
|
|
1629
|
-
await
|
|
1460
|
+
await fs3.writeFile(filePath, newText, "utf8");
|
|
1630
1461
|
return { filePath, created: false, alreadySet: false };
|
|
1631
1462
|
}
|
|
1632
1463
|
function ensureMap(doc, key) {
|
|
@@ -1733,8 +1564,8 @@ ${existing}` : leakedComment;
|
|
|
1733
1564
|
}
|
|
1734
1565
|
|
|
1735
1566
|
// src/init/components.ts
|
|
1736
|
-
import { existsSync as
|
|
1737
|
-
import
|
|
1567
|
+
import { existsSync as existsSync4, promises as fs4 } from "fs";
|
|
1568
|
+
import path5 from "path";
|
|
1738
1569
|
import { z as z3 } from "zod";
|
|
1739
1570
|
import { parse as parseYaml } from "yaml";
|
|
1740
1571
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -1781,7 +1612,7 @@ var ComponentFileSchema = z3.object({
|
|
|
1781
1612
|
}
|
|
1782
1613
|
});
|
|
1783
1614
|
async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
1784
|
-
if (!
|
|
1615
|
+
if (!existsSync4(rootDir)) {
|
|
1785
1616
|
return /* @__PURE__ */ new Map();
|
|
1786
1617
|
}
|
|
1787
1618
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1789,17 +1620,17 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
1789
1620
|
return out;
|
|
1790
1621
|
}
|
|
1791
1622
|
async function walk(baseDir, currentDir, out) {
|
|
1792
|
-
const entries = await
|
|
1623
|
+
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
1793
1624
|
for (const entry2 of entries) {
|
|
1794
|
-
const full =
|
|
1625
|
+
const full = path5.join(currentDir, entry2.name);
|
|
1795
1626
|
if (entry2.isDirectory()) {
|
|
1796
1627
|
await walk(baseDir, full, out);
|
|
1797
1628
|
continue;
|
|
1798
1629
|
}
|
|
1799
1630
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
1800
|
-
const relative =
|
|
1801
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
1802
|
-
const text = await
|
|
1631
|
+
const relative = path5.relative(baseDir, full);
|
|
1632
|
+
const name = relative.replace(/\.yml$/, "").split(path5.sep).join("/");
|
|
1633
|
+
const text = await fs4.readFile(full, "utf8");
|
|
1803
1634
|
let raw;
|
|
1804
1635
|
try {
|
|
1805
1636
|
raw = parseYaml(text);
|
|
@@ -1820,42 +1651,6 @@ ${issues}`);
|
|
|
1820
1651
|
out.set(name, { name, sourcePath: full, file: parsed.data });
|
|
1821
1652
|
}
|
|
1822
1653
|
}
|
|
1823
|
-
function mergeComponents(resolved) {
|
|
1824
|
-
const languages = [];
|
|
1825
|
-
const services = [];
|
|
1826
|
-
const featureByRef = /* @__PURE__ */ new Map();
|
|
1827
|
-
for (const entry2 of resolved) {
|
|
1828
|
-
const c = isResolvedComponent(entry2) ? entry2.component : entry2;
|
|
1829
|
-
const version = isResolvedComponent(entry2) ? entry2.version : void 0;
|
|
1830
|
-
const ct = c.file.contributes;
|
|
1831
|
-
for (const lang of ct.languages ?? []) {
|
|
1832
|
-
const value = version !== void 0 ? `${lang}:${version}` : lang;
|
|
1833
|
-
if (!languages.includes(value)) languages.push(value);
|
|
1834
|
-
}
|
|
1835
|
-
for (const svc of ct.services ?? []) {
|
|
1836
|
-
if (!services.includes(svc)) services.push(svc);
|
|
1837
|
-
}
|
|
1838
|
-
for (const f of ct.features ?? []) {
|
|
1839
|
-
const existing = featureByRef.get(f.ref);
|
|
1840
|
-
if (!existing) {
|
|
1841
|
-
featureByRef.set(f.ref, {
|
|
1842
|
-
ref: f.ref,
|
|
1843
|
-
options: { ...f.options ?? {} }
|
|
1844
|
-
});
|
|
1845
|
-
continue;
|
|
1846
|
-
}
|
|
1847
|
-
existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
return {
|
|
1851
|
-
languages,
|
|
1852
|
-
services,
|
|
1853
|
-
features: [...featureByRef.values()]
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
function isResolvedComponent(x) {
|
|
1857
|
-
return "component" in x;
|
|
1858
|
-
}
|
|
1859
1654
|
function mergeFeatureOptions(a, b) {
|
|
1860
1655
|
const result = { ...a };
|
|
1861
1656
|
for (const [key, valueB] of Object.entries(b)) {
|
|
@@ -1868,33 +1663,124 @@ function mergeFeatureOptions(a, b) {
|
|
|
1868
1663
|
}
|
|
1869
1664
|
return result;
|
|
1870
1665
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1666
|
+
|
|
1667
|
+
// src/proxy/index.ts
|
|
1668
|
+
import { spawn as spawn3 } from "child_process";
|
|
1669
|
+
import { promises as fs5 } from "fs";
|
|
1670
|
+
import path6 from "path";
|
|
1671
|
+
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
1672
|
+
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
1673
|
+
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
1674
|
+
var defaultDockerExec = (args) => {
|
|
1675
|
+
return new Promise((resolve, reject) => {
|
|
1676
|
+
const child = spawn3("docker", args, {
|
|
1677
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1678
|
+
});
|
|
1679
|
+
let stdout = "";
|
|
1680
|
+
let stderr = "";
|
|
1681
|
+
child.stdout.on("data", (chunk) => {
|
|
1682
|
+
stdout += chunk.toString();
|
|
1683
|
+
});
|
|
1684
|
+
child.stderr.on("data", (chunk) => {
|
|
1685
|
+
stderr += chunk.toString();
|
|
1686
|
+
});
|
|
1687
|
+
child.on("error", reject);
|
|
1688
|
+
child.on(
|
|
1689
|
+
"exit",
|
|
1690
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
};
|
|
1694
|
+
var realDocker = defaultDockerExec;
|
|
1695
|
+
function proxyDynamicDir(home) {
|
|
1696
|
+
return path6.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
1697
|
+
}
|
|
1698
|
+
async function ensureProxy(opts = {}) {
|
|
1699
|
+
const docker = opts.docker ?? realDocker;
|
|
1700
|
+
const dyn = proxyDynamicDir(opts.monocerosHome);
|
|
1701
|
+
await fs5.mkdir(dyn, { recursive: true });
|
|
1702
|
+
const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
|
|
1703
|
+
if (netInspect.exitCode !== 0) {
|
|
1704
|
+
const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
|
|
1705
|
+
if (create.exitCode !== 0) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`Could not create docker network ${PROXY_NETWORK_NAME}: ${create.stderr.trim() || `exit ${create.exitCode}`}`
|
|
1708
|
+
);
|
|
1882
1709
|
}
|
|
1883
|
-
|
|
1710
|
+
}
|
|
1711
|
+
const state = await docker([
|
|
1712
|
+
"inspect",
|
|
1713
|
+
"--format",
|
|
1714
|
+
"{{.State.Running}}",
|
|
1715
|
+
PROXY_CONTAINER_NAME
|
|
1716
|
+
]);
|
|
1717
|
+
if (state.exitCode === 0) {
|
|
1718
|
+
if (state.stdout.trim() === "true") return;
|
|
1719
|
+
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
1720
|
+
if (start.exitCode !== 0) {
|
|
1884
1721
|
throw new Error(
|
|
1885
|
-
`
|
|
1722
|
+
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
1886
1723
|
);
|
|
1887
1724
|
}
|
|
1888
|
-
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const hostPort = opts.hostPort ?? 80;
|
|
1728
|
+
const run = await docker([
|
|
1729
|
+
"run",
|
|
1730
|
+
"-d",
|
|
1731
|
+
"--name",
|
|
1732
|
+
PROXY_CONTAINER_NAME,
|
|
1733
|
+
"--network",
|
|
1734
|
+
PROXY_NETWORK_NAME,
|
|
1735
|
+
"-p",
|
|
1736
|
+
`${hostPort}:80`,
|
|
1737
|
+
"-v",
|
|
1738
|
+
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
1739
|
+
"--label",
|
|
1740
|
+
"monoceros.role=proxy",
|
|
1741
|
+
TRAEFIK_IMAGE,
|
|
1742
|
+
"--entrypoints.web.address=:80",
|
|
1743
|
+
"--providers.file.directory=/etc/traefik/dynamic",
|
|
1744
|
+
"--providers.file.watch=true",
|
|
1745
|
+
"--providers.docker=false",
|
|
1746
|
+
"--api.dashboard=false",
|
|
1747
|
+
"--log.level=INFO"
|
|
1748
|
+
]);
|
|
1749
|
+
if (run.exitCode !== 0) {
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
opts.logger?.info(
|
|
1755
|
+
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
async function maybeStopProxy(opts = {}) {
|
|
1759
|
+
const docker = opts.docker ?? realDocker;
|
|
1760
|
+
const logger = opts.logger;
|
|
1761
|
+
const inspect = await docker([
|
|
1762
|
+
"network",
|
|
1763
|
+
"inspect",
|
|
1764
|
+
PROXY_NETWORK_NAME,
|
|
1765
|
+
"--format",
|
|
1766
|
+
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
1767
|
+
]);
|
|
1768
|
+
if (inspect.exitCode !== 0) {
|
|
1769
|
+
return;
|
|
1889
1770
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1771
|
+
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
1772
|
+
if (others.length > 0) return;
|
|
1773
|
+
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
1774
|
+
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
1775
|
+
if (netRm.exitCode !== 0) {
|
|
1776
|
+
logger?.warn?.(
|
|
1777
|
+
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
1895
1778
|
);
|
|
1779
|
+
return;
|
|
1896
1780
|
}
|
|
1897
|
-
|
|
1781
|
+
logger?.info(
|
|
1782
|
+
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1783
|
+
);
|
|
1898
1784
|
}
|
|
1899
1785
|
|
|
1900
1786
|
// src/proxy/dynamic.ts
|
|
@@ -2116,34 +2002,97 @@ function knownLanguages() {
|
|
|
2116
2002
|
function knownServices() {
|
|
2117
2003
|
return Object.keys(SERVICE_CATALOG).sort();
|
|
2118
2004
|
}
|
|
2005
|
+
function resolveService(entry2) {
|
|
2006
|
+
return {
|
|
2007
|
+
name: entry2.name,
|
|
2008
|
+
image: entry2.image,
|
|
2009
|
+
...entry2.port !== void 0 ? { port: entry2.port } : {},
|
|
2010
|
+
env: entry2.env ? { ...entry2.env } : {},
|
|
2011
|
+
volumes: entry2.volumes ? [...entry2.volumes] : [],
|
|
2012
|
+
...entry2.healthcheck ? { healthcheck: entry2.healthcheck } : {},
|
|
2013
|
+
...entry2.restart ? { restart: entry2.restart } : {},
|
|
2014
|
+
...entry2.command ? { command: entry2.command } : {}
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function isCuratedService(name) {
|
|
2018
|
+
return Object.prototype.hasOwnProperty.call(SERVICE_CATALOG, name);
|
|
2019
|
+
}
|
|
2020
|
+
function expandCuratedService(name) {
|
|
2021
|
+
const def = SERVICE_CATALOG[name];
|
|
2022
|
+
if (!def) {
|
|
2023
|
+
throw new Error(
|
|
2024
|
+
`Unknown service '${name}'. Known catalog services: ${knownServices().join(", ")}.`
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
name: def.id,
|
|
2029
|
+
image: def.image,
|
|
2030
|
+
port: def.defaultPort,
|
|
2031
|
+
...def.env ? { env: { ...def.env } } : {},
|
|
2032
|
+
...def.dataMount ? { volumes: [`data:${def.dataMount}`] } : {}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function deriveServiceName(image) {
|
|
2036
|
+
const lastSegment = image.split("/").pop() ?? image;
|
|
2037
|
+
const noTag = lastSegment.split("@")[0].split(":")[0];
|
|
2038
|
+
return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
2039
|
+
}
|
|
2119
2040
|
|
|
2120
|
-
// src/
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
);
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
if (
|
|
2136
|
-
|
|
2041
|
+
// src/init/service-doc.ts
|
|
2042
|
+
function renderServiceObjectBody(svc) {
|
|
2043
|
+
const lines = [`name: ${svc.name}`, `image: ${svc.image}`];
|
|
2044
|
+
if (svc.port !== void 0) lines.push(`port: ${svc.port}`);
|
|
2045
|
+
if (svc.env && Object.keys(svc.env).length > 0) {
|
|
2046
|
+
lines.push("env:");
|
|
2047
|
+
for (const [k, v] of Object.entries(svc.env)) {
|
|
2048
|
+
lines.push(` ${k}: ${v}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (svc.volumes && svc.volumes.length > 0) {
|
|
2052
|
+
lines.push("volumes:");
|
|
2053
|
+
for (const vol of svc.volumes) lines.push(` - ${vol}`);
|
|
2054
|
+
}
|
|
2055
|
+
if (svc.restart) lines.push(`restart: ${svc.restart}`);
|
|
2056
|
+
if (svc.command !== void 0) lines.push(`command: ${svc.command}`);
|
|
2057
|
+
if (svc.healthcheck) {
|
|
2058
|
+
lines.push("healthcheck:");
|
|
2059
|
+
const test = svc.healthcheck.test;
|
|
2060
|
+
lines.push(
|
|
2061
|
+
Array.isArray(test) ? ` test: [${test.map((t) => JSON.stringify(t)).join(", ")}]` : ` test: ${test}`
|
|
2062
|
+
);
|
|
2063
|
+
if (svc.healthcheck.interval)
|
|
2064
|
+
lines.push(` interval: ${svc.healthcheck.interval}`);
|
|
2065
|
+
if (svc.healthcheck.timeout)
|
|
2066
|
+
lines.push(` timeout: ${svc.healthcheck.timeout}`);
|
|
2067
|
+
if (svc.healthcheck.retries !== void 0)
|
|
2068
|
+
lines.push(` retries: ${svc.healthcheck.retries}`);
|
|
2069
|
+
if (svc.healthcheck.startPeriod)
|
|
2070
|
+
lines.push(` startPeriod: ${svc.healthcheck.startPeriod}`);
|
|
2071
|
+
}
|
|
2072
|
+
return lines;
|
|
2137
2073
|
}
|
|
2138
|
-
function
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2074
|
+
function renderCustomService(name, image) {
|
|
2075
|
+
const bodyLines = [`name: ${name}`, `image: ${image}`];
|
|
2076
|
+
const comment = [
|
|
2077
|
+
" port: 8080 # in-container port \u2192 `monoceros tunnel`",
|
|
2078
|
+
" env: # values resolved from <name>.env",
|
|
2079
|
+
" KEY: ${SOME_VAR}",
|
|
2080
|
+
" volumes:",
|
|
2081
|
+
` - data:/data # persistent host bind-mount under data/${name}`,
|
|
2082
|
+
" - rel/host/path:/in/container:ro",
|
|
2083
|
+
" healthcheck:",
|
|
2084
|
+
" test: curl -f http://localhost:8080/health",
|
|
2085
|
+
" restart: unless-stopped"
|
|
2086
|
+
].join("\n");
|
|
2087
|
+
return { bodyLines, comment };
|
|
2088
|
+
}
|
|
2089
|
+
function customServiceHint(name) {
|
|
2090
|
+
return `'${name}' is a custom image \u2014 Monoceros doesn't know its env, ports or volumes. Review the commented block under services[].${name} in the yml and fill in what the image needs.`;
|
|
2144
2091
|
}
|
|
2145
2092
|
|
|
2146
2093
|
// src/create/scaffold.ts
|
|
2094
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, promises as fs7 } from "fs";
|
|
2095
|
+
import path8 from "path";
|
|
2147
2096
|
var APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
|
|
2148
2097
|
var FEATURE_REF_RE2 = /^[a-z0-9.-]+(\/[a-z0-9._-]+)+:[a-z0-9._-]+$/;
|
|
2149
2098
|
var INSTALL_URL_RE2 = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
|
|
@@ -2173,12 +2122,24 @@ function validateOptions(opts) {
|
|
|
2173
2122
|
);
|
|
2174
2123
|
}
|
|
2175
2124
|
}
|
|
2125
|
+
const seenServiceNames = /* @__PURE__ */ new Set();
|
|
2176
2126
|
for (const svc of opts.services) {
|
|
2177
|
-
if (!
|
|
2127
|
+
if (!svc.image) {
|
|
2128
|
+
throw new Error(
|
|
2129
|
+
`Service ${JSON.stringify(svc.name)} has no image. Every service needs an 'image:'.`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
if (svc.name === "workspace") {
|
|
2178
2133
|
throw new Error(
|
|
2179
|
-
`
|
|
2134
|
+
`Invalid service name 'workspace': it collides with the reserved devcontainer workspace service. Pick another name.`
|
|
2180
2135
|
);
|
|
2181
2136
|
}
|
|
2137
|
+
if (seenServiceNames.has(svc.name)) {
|
|
2138
|
+
throw new Error(
|
|
2139
|
+
`Duplicate service name: ${JSON.stringify(svc.name)}. Each services[] entry must have a unique name.`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
seenServiceNames.add(svc.name);
|
|
2182
2143
|
}
|
|
2183
2144
|
for (const pkg of opts.aptPackages ?? []) {
|
|
2184
2145
|
if (!APT_PACKAGE_NAME_RE2.test(pkg)) {
|
|
@@ -2228,10 +2189,14 @@ function validateOptions(opts) {
|
|
|
2228
2189
|
}
|
|
2229
2190
|
function normalizeOptions(opts) {
|
|
2230
2191
|
const languages = [...new Set(opts.languages)].sort();
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2192
|
+
const serviceByName = /* @__PURE__ */ new Map();
|
|
2193
|
+
for (const svc of opts.services) {
|
|
2194
|
+
if (opts.postgresUrl && svc.name === "postgres") continue;
|
|
2195
|
+
serviceByName.set(svc.name, svc);
|
|
2234
2196
|
}
|
|
2197
|
+
const services = [...serviceByName.values()].sort(
|
|
2198
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
2199
|
+
);
|
|
2235
2200
|
const aptPackages = [...new Set(opts.aptPackages ?? [])].sort();
|
|
2236
2201
|
const features = opts.features ? Object.fromEntries(
|
|
2237
2202
|
Object.entries(opts.features).sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -2291,7 +2256,7 @@ function resolveFeatures(opts) {
|
|
|
2291
2256
|
const name = match.name;
|
|
2292
2257
|
const checkout = workbenchCheckoutRoot();
|
|
2293
2258
|
const localSourceDir = checkout ? path8.join(checkout, "images", "features", name) : null;
|
|
2294
|
-
if (localSourceDir &&
|
|
2259
|
+
if (localSourceDir && existsSync5(localSourceDir)) {
|
|
2295
2260
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
2296
2261
|
resolved.push({
|
|
2297
2262
|
devcontainerKey: `./features/${name}`,
|
|
@@ -2317,7 +2282,7 @@ function resolveFeatures(opts) {
|
|
|
2317
2282
|
function readPersistentHomeEntries(localSourceDir) {
|
|
2318
2283
|
const manifestPath = path8.join(localSourceDir, "devcontainer-feature.json");
|
|
2319
2284
|
try {
|
|
2320
|
-
const text =
|
|
2285
|
+
const text = readFileSync3(manifestPath, "utf8");
|
|
2321
2286
|
const parsed = JSON.parse(text);
|
|
2322
2287
|
return {
|
|
2323
2288
|
paths: filterSubpaths(parsed["x-monoceros"]?.persistentHomePaths),
|
|
@@ -2392,7 +2357,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
2392
2357
|
name: opts.name,
|
|
2393
2358
|
dockerComposeFile: "compose.yaml",
|
|
2394
2359
|
service: "workspace",
|
|
2395
|
-
...opts.services.length > 0 ? { runServices: opts.services } : {},
|
|
2360
|
+
...opts.services.length > 0 ? { runServices: opts.services.map((s) => s.name) } : {},
|
|
2396
2361
|
workspaceFolder: `/workspaces/${opts.name}`,
|
|
2397
2362
|
remoteUser: "node",
|
|
2398
2363
|
forwardPorts: ports,
|
|
@@ -2425,6 +2390,18 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
2425
2390
|
...customizationsField ?? {}
|
|
2426
2391
|
};
|
|
2427
2392
|
}
|
|
2393
|
+
function composeScalar(value) {
|
|
2394
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
2395
|
+
return `"${escaped}"`;
|
|
2396
|
+
}
|
|
2397
|
+
function composeVolumeSource(spec, serviceName) {
|
|
2398
|
+
const parts = spec.split(":");
|
|
2399
|
+
const src = parts[0];
|
|
2400
|
+
const rest = parts.slice(1).join(":");
|
|
2401
|
+
if (src === "data") return `../data/${serviceName}:${rest}`;
|
|
2402
|
+
const relative = src.startsWith("./") ? src.slice(2) : src;
|
|
2403
|
+
return `../${relative}:${rest}`;
|
|
2404
|
+
}
|
|
2428
2405
|
function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
2429
2406
|
void dockerMode;
|
|
2430
2407
|
const hasPorts = (opts.ports?.length ?? 0) > 0;
|
|
@@ -2453,20 +2430,42 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
|
2453
2430
|
lines.push(` - ../home/${sub}:/home/node/${sub}`);
|
|
2454
2431
|
}
|
|
2455
2432
|
}
|
|
2456
|
-
for (const
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2433
|
+
for (const svc of opts.services) {
|
|
2434
|
+
lines.push(` ${svc.name}:`);
|
|
2435
|
+
lines.push(` image: ${svc.image}`);
|
|
2436
|
+
if (svc.restart) {
|
|
2437
|
+
lines.push(` restart: ${svc.restart}`);
|
|
2438
|
+
}
|
|
2439
|
+
if (svc.command !== void 0) {
|
|
2440
|
+
lines.push(` command: ${composeScalar(svc.command)}`);
|
|
2441
|
+
}
|
|
2442
|
+
const envKeys = Object.keys(svc.env);
|
|
2443
|
+
if (envKeys.length > 0) {
|
|
2462
2444
|
lines.push(" environment:");
|
|
2463
|
-
for (const
|
|
2464
|
-
lines.push(` ${k}: ${
|
|
2445
|
+
for (const k of envKeys) {
|
|
2446
|
+
lines.push(` ${k}: ${composeScalar(svc.env[k])}`);
|
|
2465
2447
|
}
|
|
2466
2448
|
}
|
|
2467
|
-
if (
|
|
2449
|
+
if (svc.volumes.length > 0) {
|
|
2468
2450
|
lines.push(" volumes:");
|
|
2469
|
-
|
|
2451
|
+
for (const vol of svc.volumes) {
|
|
2452
|
+
lines.push(` - ${composeVolumeSource(vol, svc.name)}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (svc.healthcheck) {
|
|
2456
|
+
const hc = svc.healthcheck;
|
|
2457
|
+
lines.push(" healthcheck:");
|
|
2458
|
+
if (Array.isArray(hc.test)) {
|
|
2459
|
+
lines.push(` test: [${hc.test.map(composeScalar).join(", ")}]`);
|
|
2460
|
+
} else {
|
|
2461
|
+
lines.push(` test: ${composeScalar(hc.test)}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (hc.interval) lines.push(` interval: ${hc.interval}`);
|
|
2464
|
+
if (hc.timeout) lines.push(` timeout: ${hc.timeout}`);
|
|
2465
|
+
if (hc.retries !== void 0) lines.push(` retries: ${hc.retries}`);
|
|
2466
|
+
if (hc.startPeriod) {
|
|
2467
|
+
lines.push(` start_period: ${hc.startPeriod}`);
|
|
2468
|
+
}
|
|
2470
2469
|
}
|
|
2471
2470
|
}
|
|
2472
2471
|
if (hasPorts) {
|
|
@@ -2602,243 +2601,97 @@ function buildPostCreateScript(opts) {
|
|
|
2602
2601
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
2603
2602
|
const dest = path8.join(devcontainerDir, "post-create.sh");
|
|
2604
2603
|
await fs7.writeFile(dest, buildPostCreateScript(opts));
|
|
2605
|
-
await fs7.chmod(dest, 493);
|
|
2606
|
-
}
|
|
2607
|
-
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
2608
|
-
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
2609
|
-
const devcontainerDir = path8.join(targetDir, ".devcontainer");
|
|
2610
|
-
const monocerosDir = path8.join(targetDir, ".monoceros");
|
|
2611
|
-
const projectsDir = path8.join(targetDir, "projects");
|
|
2612
|
-
const homeDir = path8.join(targetDir, "home");
|
|
2613
|
-
const dataDir = path8.join(targetDir, "data");
|
|
2614
|
-
await fs7.mkdir(devcontainerDir, { recursive: true });
|
|
2615
|
-
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2616
|
-
await fs7.mkdir(projectsDir, { recursive: true });
|
|
2617
|
-
await fs7.mkdir(homeDir, { recursive: true });
|
|
2618
|
-
if (needsCompose(opts)) {
|
|
2619
|
-
await fs7.mkdir(dataDir, { recursive: true });
|
|
2620
|
-
for (const svcId of opts.services) {
|
|
2621
|
-
const def = SERVICE_CATALOG[svcId];
|
|
2622
|
-
if (def?.dataMount) {
|
|
2623
|
-
await fs7.mkdir(path8.join(dataDir, def.id), { recursive: true });
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
const containerGitignore = path8.join(targetDir, ".gitignore");
|
|
2628
|
-
await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2629
|
-
const gitkeep = path8.join(projectsDir, ".gitkeep");
|
|
2630
|
-
if (!existsSync4(gitkeep)) {
|
|
2631
|
-
await fs7.writeFile(gitkeep, "");
|
|
2632
|
-
}
|
|
2633
|
-
await fs7.writeFile(
|
|
2634
|
-
path8.join(monocerosDir, ".gitignore"),
|
|
2635
|
-
"git-credentials*\ngitconfig\n"
|
|
2636
|
-
);
|
|
2637
|
-
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
2638
|
-
await fs7.writeFile(
|
|
2639
|
-
path8.join(devcontainerDir, "devcontainer.json"),
|
|
2640
|
-
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
2641
|
-
);
|
|
2642
|
-
const featuresDir = path8.join(devcontainerDir, "features");
|
|
2643
|
-
if (existsSync4(featuresDir)) {
|
|
2644
|
-
await fs7.rm(featuresDir, { recursive: true, force: true });
|
|
2645
|
-
}
|
|
2646
|
-
const resolvedFeatures = resolveFeatures(opts);
|
|
2647
|
-
for (const f of resolvedFeatures) {
|
|
2648
|
-
if (!f.localSourceDir || !f.localName) continue;
|
|
2649
|
-
const dest = path8.join(featuresDir, f.localName);
|
|
2650
|
-
await fs7.mkdir(dest, { recursive: true });
|
|
2651
|
-
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2652
|
-
}
|
|
2653
|
-
for (const f of resolvedFeatures) {
|
|
2654
|
-
for (const sub of f.persistentHomePaths) {
|
|
2655
|
-
await fs7.mkdir(path8.join(homeDir, sub), { recursive: true });
|
|
2656
|
-
}
|
|
2657
|
-
for (const entry2 of f.persistentHomeFiles) {
|
|
2658
|
-
const filePath = path8.join(homeDir, entry2.path);
|
|
2659
|
-
await fs7.mkdir(path8.dirname(filePath), { recursive: true });
|
|
2660
|
-
if (!existsSync4(filePath)) {
|
|
2661
|
-
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
await writePostCreateScript(devcontainerDir, opts);
|
|
2666
|
-
const composePath = path8.join(devcontainerDir, "compose.yaml");
|
|
2667
|
-
if (needsCompose(opts)) {
|
|
2668
|
-
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2669
|
-
} else if (existsSync4(composePath)) {
|
|
2670
|
-
await fs7.rm(composePath);
|
|
2671
|
-
}
|
|
2672
|
-
const workspacePath = path8.join(targetDir, `${opts.name}.code-workspace`);
|
|
2673
|
-
let existingWorkspace;
|
|
2674
|
-
try {
|
|
2675
|
-
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
2676
|
-
existingWorkspace = JSON.parse(raw);
|
|
2677
|
-
} catch {
|
|
2678
|
-
existingWorkspace = void 0;
|
|
2679
|
-
}
|
|
2680
|
-
const generated = buildCodeWorkspaceJson(opts);
|
|
2681
|
-
const merged = mergeCodeWorkspace(existingWorkspace, generated);
|
|
2682
|
-
await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
// src/modify/yml.ts
|
|
2686
|
-
import {
|
|
2687
|
-
isMap as isMap2,
|
|
2688
|
-
isScalar,
|
|
2689
|
-
isSeq,
|
|
2690
|
-
Pair as Pair2,
|
|
2691
|
-
Scalar as Scalar2,
|
|
2692
|
-
YAMLMap as YAMLMap2,
|
|
2693
|
-
YAMLSeq
|
|
2694
|
-
} from "yaml";
|
|
2695
|
-
|
|
2696
|
-
// src/init/manifest.ts
|
|
2697
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
2698
|
-
import path9 from "path";
|
|
2699
|
-
function resolveManifestPath(name, checkoutRoot) {
|
|
2700
|
-
if (checkoutRoot) {
|
|
2701
|
-
const checkoutPath = path9.join(
|
|
2702
|
-
checkoutRoot,
|
|
2703
|
-
"images",
|
|
2704
|
-
"features",
|
|
2705
|
-
name,
|
|
2706
|
-
"devcontainer-feature.json"
|
|
2707
|
-
);
|
|
2708
|
-
if (existsSync5(checkoutPath)) return checkoutPath;
|
|
2709
|
-
}
|
|
2710
|
-
const bundlePath = path9.join(
|
|
2711
|
-
bundledFeaturesDir(),
|
|
2712
|
-
name,
|
|
2713
|
-
"devcontainer-feature.json"
|
|
2714
|
-
);
|
|
2715
|
-
if (existsSync5(bundlePath)) return bundlePath;
|
|
2716
|
-
return null;
|
|
2717
|
-
}
|
|
2718
|
-
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
2719
|
-
const match = matchMonocerosFeature(ref);
|
|
2720
|
-
if (!match) return void 0;
|
|
2721
|
-
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
2722
|
-
if (!manifestPath) return void 0;
|
|
2723
|
-
try {
|
|
2724
|
-
const text = readFileSync3(manifestPath, "utf8");
|
|
2725
|
-
const parsed = JSON.parse(text);
|
|
2726
|
-
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
2727
|
-
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
2728
|
-
(x) => typeof x === "string" && x.length > 0
|
|
2729
|
-
) : [];
|
|
2730
|
-
const rawNotes = parsed["x-monoceros"]?.usageNotes;
|
|
2731
|
-
const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
|
|
2732
|
-
(x) => typeof x === "string" && x.length > 0
|
|
2733
|
-
) : [];
|
|
2734
|
-
const optionDescriptions = {};
|
|
2735
|
-
const optionTypes = {};
|
|
2736
|
-
const optionNames = [];
|
|
2737
|
-
if (parsed.options) {
|
|
2738
|
-
for (const [key, opt] of Object.entries(parsed.options)) {
|
|
2739
|
-
if (!opt || typeof opt !== "object") continue;
|
|
2740
|
-
optionNames.push(key);
|
|
2741
|
-
if (typeof opt.description === "string" && opt.description.length > 0) {
|
|
2742
|
-
optionDescriptions[key] = opt.description;
|
|
2743
|
-
}
|
|
2744
|
-
if (opt.type === "boolean") {
|
|
2745
|
-
optionTypes[key] = "boolean";
|
|
2746
|
-
} else if (opt.type === "string") {
|
|
2747
|
-
optionTypes[key] = "string";
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
const name = typeof parsed.name === "string" ? parsed.name : "";
|
|
2752
|
-
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
2753
|
-
const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
|
|
2754
|
-
const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
|
|
2755
|
-
return {
|
|
2756
|
-
name,
|
|
2757
|
-
description,
|
|
2758
|
-
documentationURL,
|
|
2759
|
-
optionHints,
|
|
2760
|
-
optionDescriptions,
|
|
2761
|
-
optionNames,
|
|
2762
|
-
optionTypes,
|
|
2763
|
-
usageNotes
|
|
2764
|
-
};
|
|
2765
|
-
} catch {
|
|
2766
|
-
return void 0;
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
// src/init/feature-doc.ts
|
|
2771
|
-
function buildFeatureHeaderLines(summary, width) {
|
|
2772
|
-
const paragraphs = buildHeaderParagraphs(summary);
|
|
2773
|
-
const wrapped = [];
|
|
2774
|
-
for (const para of paragraphs) {
|
|
2775
|
-
for (const line of wrapToComment(para, width)) {
|
|
2776
|
-
wrapped.push(line);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
return wrapped;
|
|
2780
|
-
}
|
|
2781
|
-
function buildFeatureHeaderCommentBefore(summary, width) {
|
|
2782
|
-
const lines = buildFeatureHeaderLines(summary, width);
|
|
2783
|
-
return lines.map((l) => ` ${l}`).join("\n");
|
|
2604
|
+
await fs7.chmod(dest, 493);
|
|
2784
2605
|
}
|
|
2785
|
-
function
|
|
2786
|
-
|
|
2787
|
-
const
|
|
2788
|
-
const
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2606
|
+
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
2607
|
+
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
2608
|
+
const devcontainerDir = path8.join(targetDir, ".devcontainer");
|
|
2609
|
+
const monocerosDir = path8.join(targetDir, ".monoceros");
|
|
2610
|
+
const projectsDir = path8.join(targetDir, "projects");
|
|
2611
|
+
const homeDir = path8.join(targetDir, "home");
|
|
2612
|
+
const dataDir = path8.join(targetDir, "data");
|
|
2613
|
+
await fs7.mkdir(devcontainerDir, { recursive: true });
|
|
2614
|
+
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2615
|
+
await fs7.mkdir(projectsDir, { recursive: true });
|
|
2616
|
+
await fs7.mkdir(homeDir, { recursive: true });
|
|
2617
|
+
if (needsCompose(opts)) {
|
|
2618
|
+
await fs7.mkdir(dataDir, { recursive: true });
|
|
2619
|
+
for (const svc of opts.services) {
|
|
2620
|
+
const hasDataVolume = svc.volumes.some((v) => v.split(":")[0] === "data");
|
|
2621
|
+
if (hasDataVolume) {
|
|
2622
|
+
await fs7.mkdir(path8.join(dataDir, svc.name), { recursive: true });
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2796
2625
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2626
|
+
const containerGitignore = path8.join(targetDir, ".gitignore");
|
|
2627
|
+
await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2628
|
+
const gitkeep = path8.join(projectsDir, ".gitkeep");
|
|
2629
|
+
if (!existsSync5(gitkeep)) {
|
|
2630
|
+
await fs7.writeFile(gitkeep, "");
|
|
2800
2631
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2632
|
+
await fs7.writeFile(
|
|
2633
|
+
path8.join(monocerosDir, ".gitignore"),
|
|
2634
|
+
"git-credentials*\ngitconfig\n"
|
|
2635
|
+
);
|
|
2636
|
+
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
2637
|
+
await fs7.writeFile(
|
|
2638
|
+
path8.join(devcontainerDir, "devcontainer.json"),
|
|
2639
|
+
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
2640
|
+
);
|
|
2641
|
+
const featuresDir = path8.join(devcontainerDir, "features");
|
|
2642
|
+
if (existsSync5(featuresDir)) {
|
|
2643
|
+
await fs7.rm(featuresDir, { recursive: true, force: true });
|
|
2808
2644
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2645
|
+
const resolvedFeatures = resolveFeatures(opts);
|
|
2646
|
+
for (const f of resolvedFeatures) {
|
|
2647
|
+
if (!f.localSourceDir || !f.localName) continue;
|
|
2648
|
+
const dest = path8.join(featuresDir, f.localName);
|
|
2649
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
2650
|
+
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2811
2651
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
const firstSentence = desc.split(/(?<=[.!?])\s+/)[0]?.trim() ?? desc.trim();
|
|
2816
|
-
return firstSentence.replace(/[.!?]+$/, "").trim();
|
|
2817
|
-
}
|
|
2818
|
-
function wrapToComment(text, width) {
|
|
2819
|
-
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
2820
|
-
if (words.length === 0) return [""];
|
|
2821
|
-
const usable = Math.max(width, 20);
|
|
2822
|
-
const lines = [];
|
|
2823
|
-
let current = "";
|
|
2824
|
-
for (const w of words) {
|
|
2825
|
-
if (current.length === 0) {
|
|
2826
|
-
current = w;
|
|
2827
|
-
continue;
|
|
2652
|
+
for (const f of resolvedFeatures) {
|
|
2653
|
+
for (const sub of f.persistentHomePaths) {
|
|
2654
|
+
await fs7.mkdir(path8.join(homeDir, sub), { recursive: true });
|
|
2828
2655
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2656
|
+
for (const entry2 of f.persistentHomeFiles) {
|
|
2657
|
+
const filePath = path8.join(homeDir, entry2.path);
|
|
2658
|
+
await fs7.mkdir(path8.dirname(filePath), { recursive: true });
|
|
2659
|
+
if (!existsSync5(filePath)) {
|
|
2660
|
+
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2661
|
+
}
|
|
2834
2662
|
}
|
|
2835
2663
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2664
|
+
await writePostCreateScript(devcontainerDir, opts);
|
|
2665
|
+
const composePath = path8.join(devcontainerDir, "compose.yaml");
|
|
2666
|
+
if (needsCompose(opts)) {
|
|
2667
|
+
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2668
|
+
} else if (existsSync5(composePath)) {
|
|
2669
|
+
await fs7.rm(composePath);
|
|
2670
|
+
}
|
|
2671
|
+
const workspacePath = path8.join(targetDir, `${opts.name}.code-workspace`);
|
|
2672
|
+
let existingWorkspace;
|
|
2673
|
+
try {
|
|
2674
|
+
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
2675
|
+
existingWorkspace = JSON.parse(raw);
|
|
2676
|
+
} catch {
|
|
2677
|
+
existingWorkspace = void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const generated = buildCodeWorkspaceJson(opts);
|
|
2680
|
+
const merged = mergeCodeWorkspace(existingWorkspace, generated);
|
|
2681
|
+
await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
|
|
2838
2682
|
}
|
|
2839
|
-
var FEATURE_HEADER_WIDTH = 76 - 2;
|
|
2840
2683
|
|
|
2841
2684
|
// src/modify/yml.ts
|
|
2685
|
+
import {
|
|
2686
|
+
isMap as isMap2,
|
|
2687
|
+
isScalar,
|
|
2688
|
+
isSeq,
|
|
2689
|
+
Pair as Pair2,
|
|
2690
|
+
parseDocument as parseDocument3,
|
|
2691
|
+
Scalar as Scalar2,
|
|
2692
|
+
YAMLMap as YAMLMap2,
|
|
2693
|
+
YAMLSeq
|
|
2694
|
+
} from "yaml";
|
|
2842
2695
|
function ensureSeq(doc, key) {
|
|
2843
2696
|
const existing = doc.get(key, true);
|
|
2844
2697
|
if (existing && isSeq(existing)) return existing;
|
|
@@ -2861,11 +2714,24 @@ function addLanguageToDoc(doc, lang) {
|
|
|
2861
2714
|
seq.add(lang);
|
|
2862
2715
|
return true;
|
|
2863
2716
|
}
|
|
2864
|
-
function
|
|
2717
|
+
function findServiceItem(seq, name) {
|
|
2718
|
+
for (const item of seq.items) {
|
|
2719
|
+
if (isMap2(item) && item.get("name") === name) return item;
|
|
2720
|
+
}
|
|
2721
|
+
return void 0;
|
|
2722
|
+
}
|
|
2723
|
+
function addServiceEntryToDoc(doc, name, image, bodyLines, scaffoldComment) {
|
|
2865
2724
|
const seq = ensureSeq(doc, "services");
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2725
|
+
const existing = findServiceItem(seq, name);
|
|
2726
|
+
if (existing) {
|
|
2727
|
+
const existingImage = existing.get("image");
|
|
2728
|
+
if (existingImage === image) return { outcome: "exists" };
|
|
2729
|
+
return { outcome: "conflict", existingImage: String(existingImage) };
|
|
2730
|
+
}
|
|
2731
|
+
const node = parseDocument3(bodyLines.join("\n")).contents;
|
|
2732
|
+
if (scaffoldComment) node.comment = scaffoldComment;
|
|
2733
|
+
seq.add(node);
|
|
2734
|
+
return { outcome: "added" };
|
|
2869
2735
|
}
|
|
2870
2736
|
function addAptPackagesToDoc(doc, packages) {
|
|
2871
2737
|
const seq = ensureSeq(doc, "aptPackages");
|
|
@@ -3085,6 +2951,12 @@ function addFeatureToDoc(doc, ref, options = {}, displayName) {
|
|
|
3085
2951
|
entry2.commentBefore = headerBefore;
|
|
3086
2952
|
entry2.spaceBefore = true;
|
|
3087
2953
|
}
|
|
2954
|
+
const hints = featureOptionHints(summary, ref, Object.keys(options));
|
|
2955
|
+
if (hints.length > 0) {
|
|
2956
|
+
const commentLines = [" options:"];
|
|
2957
|
+
for (const h of hints) commentLines.push(` ${h.key}: ${h.placeholder}`);
|
|
2958
|
+
entry2.comment = commentLines.join("\n");
|
|
2959
|
+
}
|
|
3088
2960
|
seq.add(entry2);
|
|
3089
2961
|
return true;
|
|
3090
2962
|
}
|
|
@@ -3161,7 +3033,15 @@ function removeLanguageFromDoc(doc, lang) {
|
|
|
3161
3033
|
return removeScalarFromSeq(doc, "languages", lang);
|
|
3162
3034
|
}
|
|
3163
3035
|
function removeServiceFromDoc(doc, service) {
|
|
3164
|
-
|
|
3036
|
+
const node = doc.get("services", true);
|
|
3037
|
+
if (!node || !isSeq(node)) return false;
|
|
3038
|
+
const idx = node.items.findIndex(
|
|
3039
|
+
(i) => isMap2(i) && i.get("name") === service
|
|
3040
|
+
);
|
|
3041
|
+
if (idx === -1) return false;
|
|
3042
|
+
node.items.splice(idx, 1);
|
|
3043
|
+
pruneEmptySeq(doc, "services");
|
|
3044
|
+
return true;
|
|
3165
3045
|
}
|
|
3166
3046
|
function removeAptPackageFromDoc(doc, pkg) {
|
|
3167
3047
|
return removeScalarFromSeq(doc, "aptPackages", pkg);
|
|
@@ -3201,8 +3081,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
3201
3081
|
if (!isMap2(item)) return false;
|
|
3202
3082
|
const url = item.get("url");
|
|
3203
3083
|
if (url === urlOrPath) return true;
|
|
3204
|
-
const
|
|
3205
|
-
const effectivePath = typeof
|
|
3084
|
+
const path21 = item.get("path");
|
|
3085
|
+
const effectivePath = typeof path21 === "string" ? path21 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
3206
3086
|
return effectivePath === urlOrPath;
|
|
3207
3087
|
});
|
|
3208
3088
|
if (idx < 0) return false;
|
|
@@ -3229,13 +3109,38 @@ function runAddLanguage(input) {
|
|
|
3229
3109
|
}
|
|
3230
3110
|
return mutate(input, (doc) => addLanguageToDoc(doc, input.language));
|
|
3231
3111
|
}
|
|
3232
|
-
function runAddService(input) {
|
|
3233
|
-
|
|
3112
|
+
async function runAddService(input) {
|
|
3113
|
+
const arg = input.service;
|
|
3114
|
+
const curated = isCuratedService(arg);
|
|
3115
|
+
if (input.as !== void 0 && !/^[a-z0-9][a-z0-9_-]*$/.test(input.as)) {
|
|
3234
3116
|
throw new Error(
|
|
3235
|
-
`
|
|
3117
|
+
`Invalid --as name ${JSON.stringify(input.as)}. Use lowercase letters, digits, '_' or '-' (must start with a letter or digit).`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const name = input.as ?? (curated ? arg : deriveServiceName(arg));
|
|
3121
|
+
const image = curated ? expandCuratedService(arg).image : arg;
|
|
3122
|
+
const custom = curated ? null : renderCustomService(name, arg);
|
|
3123
|
+
const bodyLines = curated ? renderServiceObjectBody({ ...expandCuratedService(arg), name }) : custom.bodyLines;
|
|
3124
|
+
const scaffoldComment = curated ? void 0 : custom.comment;
|
|
3125
|
+
const result = await mutate(input, (doc) => {
|
|
3126
|
+
const r = addServiceEntryToDoc(
|
|
3127
|
+
doc,
|
|
3128
|
+
name,
|
|
3129
|
+
image,
|
|
3130
|
+
bodyLines,
|
|
3131
|
+
scaffoldComment
|
|
3236
3132
|
);
|
|
3133
|
+
if (r.outcome === "conflict") {
|
|
3134
|
+
throw new Error(
|
|
3135
|
+
`A service named '${name}' already exists with a different image (${r.existingImage}). Add it under a different name with \`--as <name>\`, or remove the existing one first (\`monoceros remove-service ${input.name} ${name}\`).`
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
return r.outcome === "added";
|
|
3139
|
+
});
|
|
3140
|
+
if (result.status === "updated" && !curated) {
|
|
3141
|
+
(input.logger ?? defaultLogger()).info(customServiceHint(name));
|
|
3237
3142
|
}
|
|
3238
|
-
return
|
|
3143
|
+
return result;
|
|
3239
3144
|
}
|
|
3240
3145
|
function runAddAptPackages(input) {
|
|
3241
3146
|
if (input.packages.length === 0) {
|
|
@@ -3252,7 +3157,7 @@ async function runAddRepo(input) {
|
|
|
3252
3157
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
3253
3158
|
);
|
|
3254
3159
|
}
|
|
3255
|
-
const
|
|
3160
|
+
const path21 = (input.path ?? deriveRepoName(url)).trim();
|
|
3256
3161
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
3257
3162
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
3258
3163
|
if (hasName !== hasEmail) {
|
|
@@ -3281,7 +3186,7 @@ async function runAddRepo(input) {
|
|
|
3281
3186
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
3282
3187
|
const entry2 = {
|
|
3283
3188
|
url,
|
|
3284
|
-
path:
|
|
3189
|
+
path: path21,
|
|
3285
3190
|
...hasName && hasEmail ? {
|
|
3286
3191
|
gitUser: {
|
|
3287
3192
|
name: input.gitName.trim(),
|
|
@@ -3397,7 +3302,7 @@ async function tryCloneInRunningContainer(input, entry2) {
|
|
|
3397
3302
|
logger.info(
|
|
3398
3303
|
`Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
|
|
3399
3304
|
);
|
|
3400
|
-
void
|
|
3305
|
+
void path9;
|
|
3401
3306
|
}
|
|
3402
3307
|
function shquote(value) {
|
|
3403
3308
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -3474,10 +3379,33 @@ async function runAddFeature(input) {
|
|
|
3474
3379
|
...resolved.defaultOptions,
|
|
3475
3380
|
...input.options ?? {}
|
|
3476
3381
|
};
|
|
3477
|
-
|
|
3382
|
+
const result = await mutate(
|
|
3478
3383
|
input,
|
|
3479
3384
|
(doc) => addFeatureToDoc(doc, resolved.ref, merged, raw)
|
|
3480
3385
|
);
|
|
3386
|
+
if (result.status === "updated") {
|
|
3387
|
+
const summary = loadFeatureManifestSummary(resolved.ref);
|
|
3388
|
+
const vars = featureOptionHints(
|
|
3389
|
+
summary,
|
|
3390
|
+
resolved.ref,
|
|
3391
|
+
Object.keys(merged)
|
|
3392
|
+
).map((h) => h.envVar);
|
|
3393
|
+
if (vars.length > 0) {
|
|
3394
|
+
const home = input.monocerosHome ?? monocerosHome();
|
|
3395
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
3396
|
+
const seeded = await ensureEnvVars(
|
|
3397
|
+
containerEnvPath(input.name, home),
|
|
3398
|
+
input.name,
|
|
3399
|
+
vars
|
|
3400
|
+
);
|
|
3401
|
+
if (seeded.added.length > 0) {
|
|
3402
|
+
(input.logger ?? defaultLogger()).info(
|
|
3403
|
+
`Seeded ${seeded.added.join(", ")} into ${input.name}.env \u2014 fill in the values.`
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return result;
|
|
3481
3409
|
}
|
|
3482
3410
|
async function resolveFeatureRefOrShortname(input) {
|
|
3483
3411
|
if (REGEX.featureRef.test(input)) {
|
|
@@ -3616,13 +3544,13 @@ async function mutate(opts, apply) {
|
|
|
3616
3544
|
}
|
|
3617
3545
|
function defaultLogger() {
|
|
3618
3546
|
return {
|
|
3619
|
-
info: (m) =>
|
|
3620
|
-
success: (m) =>
|
|
3621
|
-
warn: (m) =>
|
|
3547
|
+
info: (m) => consola.info(m),
|
|
3548
|
+
success: (m) => consola.success(m),
|
|
3549
|
+
warn: (m) => consola.warn(m)
|
|
3622
3550
|
};
|
|
3623
3551
|
}
|
|
3624
3552
|
var defaultConfirm = async (message) => {
|
|
3625
|
-
const result = await
|
|
3553
|
+
const result = await consola.prompt(message, {
|
|
3626
3554
|
type: "confirm",
|
|
3627
3555
|
initial: false
|
|
3628
3556
|
});
|
|
@@ -3662,9 +3590,6 @@ async function syncPortsToProxy(input) {
|
|
|
3662
3590
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
3663
3591
|
logger: { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
3664
3592
|
});
|
|
3665
|
-
await kickProxyReload({
|
|
3666
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3667
|
-
});
|
|
3668
3593
|
const urls = proxyUrlsFor(input.name, allPorts, hostPort);
|
|
3669
3594
|
const lines = urls.map((u) => {
|
|
3670
3595
|
const tag = u.isDefault ? " (default)" : "";
|
|
@@ -3674,9 +3599,6 @@ async function syncPortsToProxy(input) {
|
|
|
3674
3599
|
${lines.join("\n")}`);
|
|
3675
3600
|
} else {
|
|
3676
3601
|
await removeDynamicConfig(input.name, { monocerosHome: home });
|
|
3677
|
-
await kickProxyReload({
|
|
3678
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3679
|
-
});
|
|
3680
3602
|
await maybeStopProxy({
|
|
3681
3603
|
monocerosHome: home,
|
|
3682
3604
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
@@ -3713,7 +3635,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3713
3635
|
async run({ args }) {
|
|
3714
3636
|
const packages = [...getInnerArgs()];
|
|
3715
3637
|
if (packages.length === 0) {
|
|
3716
|
-
|
|
3638
|
+
consola2.error(
|
|
3717
3639
|
"No package names given. Usage: `monoceros add-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
|
|
3718
3640
|
);
|
|
3719
3641
|
process.exit(1);
|
|
@@ -3726,7 +3648,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3726
3648
|
});
|
|
3727
3649
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3728
3650
|
} catch (err) {
|
|
3729
|
-
|
|
3651
|
+
consola2.error(err instanceof Error ? err.message : String(err));
|
|
3730
3652
|
process.exit(1);
|
|
3731
3653
|
}
|
|
3732
3654
|
}
|
|
@@ -3734,7 +3656,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3734
3656
|
|
|
3735
3657
|
// src/commands/add-feature.ts
|
|
3736
3658
|
import { defineCommand as defineCommand2 } from "citty";
|
|
3737
|
-
import { consola as
|
|
3659
|
+
import { consola as consola3 } from "consola";
|
|
3738
3660
|
var addFeatureCommand = defineCommand2({
|
|
3739
3661
|
meta: {
|
|
3740
3662
|
name: "add-feature",
|
|
@@ -3764,7 +3686,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3764
3686
|
try {
|
|
3765
3687
|
options = parseOptionsAfterDashes(getInnerArgs());
|
|
3766
3688
|
} catch (err) {
|
|
3767
|
-
|
|
3689
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3768
3690
|
process.exit(1);
|
|
3769
3691
|
}
|
|
3770
3692
|
try {
|
|
@@ -3776,7 +3698,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3776
3698
|
});
|
|
3777
3699
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3778
3700
|
} catch (err) {
|
|
3779
|
-
|
|
3701
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3780
3702
|
process.exit(1);
|
|
3781
3703
|
}
|
|
3782
3704
|
}
|
|
@@ -3808,7 +3730,7 @@ function coerce(value) {
|
|
|
3808
3730
|
|
|
3809
3731
|
// src/commands/add-from-url.ts
|
|
3810
3732
|
import { defineCommand as defineCommand3 } from "citty";
|
|
3811
|
-
import { consola as
|
|
3733
|
+
import { consola as consola4 } from "consola";
|
|
3812
3734
|
var addFromUrlCommand = defineCommand3({
|
|
3813
3735
|
meta: {
|
|
3814
3736
|
name: "add-from-url",
|
|
@@ -3845,7 +3767,7 @@ var addFromUrlCommand = defineCommand3({
|
|
|
3845
3767
|
});
|
|
3846
3768
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3847
3769
|
} catch (err) {
|
|
3848
|
-
|
|
3770
|
+
consola4.error(err instanceof Error ? err.message : String(err));
|
|
3849
3771
|
process.exit(1);
|
|
3850
3772
|
}
|
|
3851
3773
|
}
|
|
@@ -3880,7 +3802,7 @@ function printSecurityWarning(url) {
|
|
|
3880
3802
|
|
|
3881
3803
|
// src/commands/add-repo.ts
|
|
3882
3804
|
import { defineCommand as defineCommand4 } from "citty";
|
|
3883
|
-
import { consola as
|
|
3805
|
+
import { consola as consola5 } from "consola";
|
|
3884
3806
|
var addRepoCommand = defineCommand4({
|
|
3885
3807
|
meta: {
|
|
3886
3808
|
name: "add-repo",
|
|
@@ -3934,7 +3856,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3934
3856
|
});
|
|
3935
3857
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3936
3858
|
} catch (err) {
|
|
3937
|
-
|
|
3859
|
+
consola5.error(err instanceof Error ? err.message : String(err));
|
|
3938
3860
|
process.exit(1);
|
|
3939
3861
|
}
|
|
3940
3862
|
}
|
|
@@ -3942,7 +3864,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3942
3864
|
|
|
3943
3865
|
// src/commands/add-language.ts
|
|
3944
3866
|
import { defineCommand as defineCommand5 } from "citty";
|
|
3945
|
-
import { consola as
|
|
3867
|
+
import { consola as consola6 } from "consola";
|
|
3946
3868
|
var addLanguageCommand = defineCommand5({
|
|
3947
3869
|
meta: {
|
|
3948
3870
|
name: "add-language",
|
|
@@ -3955,11 +3877,111 @@ var addLanguageCommand = defineCommand5({
|
|
|
3955
3877
|
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3956
3878
|
required: true
|
|
3957
3879
|
},
|
|
3958
|
-
language: {
|
|
3880
|
+
language: {
|
|
3881
|
+
type: "positional",
|
|
3882
|
+
description: "Language identifier from the feature whitelist (e.g. python, java, rust).",
|
|
3883
|
+
required: true
|
|
3884
|
+
},
|
|
3885
|
+
yes: {
|
|
3886
|
+
type: "boolean",
|
|
3887
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3888
|
+
alias: ["y"],
|
|
3889
|
+
default: false
|
|
3890
|
+
}
|
|
3891
|
+
},
|
|
3892
|
+
async run({ args }) {
|
|
3893
|
+
try {
|
|
3894
|
+
const result = await runAddLanguage({
|
|
3895
|
+
name: args.name,
|
|
3896
|
+
language: args.language,
|
|
3897
|
+
yes: args.yes
|
|
3898
|
+
});
|
|
3899
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3900
|
+
} catch (err) {
|
|
3901
|
+
consola6.error(err instanceof Error ? err.message : String(err));
|
|
3902
|
+
process.exit(1);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
// src/commands/add-port.ts
|
|
3908
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
3909
|
+
import { consola as consola7 } from "consola";
|
|
3910
|
+
var addPortCommand = defineCommand6({
|
|
3911
|
+
meta: {
|
|
3912
|
+
name: "add-port",
|
|
3913
|
+
group: "edit",
|
|
3914
|
+
description: "Add one or more ports to the container config so they become reachable from the host via Traefik (`<container>.localhost` / `<container>-<port>.localhost`). Pass port numbers after `--` (e.g. `monoceros add-port sandbox -- 3000 5173 6006`). Idempotent. Persisted in the yml so later `monoceros apply` runs restore the routes. Pass `--default` together with a single port to make it the bare `<container>.localhost` route \u2014 the port is inserted at position 0 (or moved there if it already exists)."
|
|
3915
|
+
},
|
|
3916
|
+
args: {
|
|
3917
|
+
name: {
|
|
3918
|
+
type: "positional",
|
|
3919
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3920
|
+
required: true
|
|
3921
|
+
},
|
|
3922
|
+
yes: {
|
|
3923
|
+
type: "boolean",
|
|
3924
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3925
|
+
alias: ["y"],
|
|
3926
|
+
default: false
|
|
3927
|
+
},
|
|
3928
|
+
default: {
|
|
3929
|
+
type: "boolean",
|
|
3930
|
+
description: "Make the (single) port the new default route at `<container>.localhost`. Inserts the port at position 0 of `routing.ports`, or moves it there if it already exists. Errors when more than one port is passed.",
|
|
3931
|
+
default: false
|
|
3932
|
+
}
|
|
3933
|
+
},
|
|
3934
|
+
async run({ args }) {
|
|
3935
|
+
const tokens = [...getInnerArgs()];
|
|
3936
|
+
if (tokens.length === 0) {
|
|
3937
|
+
consola7.error(
|
|
3938
|
+
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
3939
|
+
);
|
|
3940
|
+
process.exit(1);
|
|
3941
|
+
}
|
|
3942
|
+
try {
|
|
3943
|
+
const result = await runAddPort({
|
|
3944
|
+
name: args.name,
|
|
3945
|
+
ports: tokens.map(coerceToken),
|
|
3946
|
+
yes: args.yes,
|
|
3947
|
+
asDefault: args.default
|
|
3948
|
+
});
|
|
3949
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3950
|
+
} catch (err) {
|
|
3951
|
+
consola7.error(err instanceof Error ? err.message : String(err));
|
|
3952
|
+
process.exit(1);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
});
|
|
3956
|
+
function coerceToken(raw) {
|
|
3957
|
+
const n = Number(raw);
|
|
3958
|
+
return Number.isFinite(n) ? n : raw;
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
// src/commands/add-service.ts
|
|
3962
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3963
|
+
import { consola as consola8 } from "consola";
|
|
3964
|
+
var addServiceCommand = defineCommand7({
|
|
3965
|
+
meta: {
|
|
3966
|
+
name: "add-service",
|
|
3967
|
+
group: "edit",
|
|
3968
|
+
description: "Add a backing service to the container config. A curated name (postgres, mysql, redis) expands to a full editable block; any other image (e.g. rustfs/rustfs:latest) drops in name + image plus a commented scaffold. Idempotent, prints a diff before writing."
|
|
3969
|
+
},
|
|
3970
|
+
args: {
|
|
3971
|
+
name: {
|
|
3972
|
+
type: "positional",
|
|
3973
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3974
|
+
required: true
|
|
3975
|
+
},
|
|
3976
|
+
service: {
|
|
3959
3977
|
type: "positional",
|
|
3960
|
-
description: "
|
|
3978
|
+
description: "Curated name (postgres, mysql, redis) or any image ref (e.g. rustfs/rustfs:latest).",
|
|
3961
3979
|
required: true
|
|
3962
3980
|
},
|
|
3981
|
+
as: {
|
|
3982
|
+
type: "string",
|
|
3983
|
+
description: "Override the service name (the compose service / DNS name / data dir). Lets you add the same image more than once \u2014 e.g. two postgres servers as postgres-app and postgres-analytics."
|
|
3984
|
+
},
|
|
3963
3985
|
yes: {
|
|
3964
3986
|
type: "boolean",
|
|
3965
3987
|
description: "Skip the interactive confirmation and apply the diff.",
|
|
@@ -3969,213 +3991,464 @@ var addLanguageCommand = defineCommand5({
|
|
|
3969
3991
|
},
|
|
3970
3992
|
async run({ args }) {
|
|
3971
3993
|
try {
|
|
3972
|
-
const result = await
|
|
3994
|
+
const result = await runAddService({
|
|
3973
3995
|
name: args.name,
|
|
3974
|
-
|
|
3996
|
+
service: args.service,
|
|
3997
|
+
...args.as ? { as: args.as } : {},
|
|
3975
3998
|
yes: args.yes
|
|
3976
3999
|
});
|
|
3977
4000
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3978
4001
|
} catch (err) {
|
|
3979
|
-
|
|
4002
|
+
consola8.error(err instanceof Error ? err.message : String(err));
|
|
3980
4003
|
process.exit(1);
|
|
3981
4004
|
}
|
|
3982
4005
|
}
|
|
3983
4006
|
});
|
|
3984
4007
|
|
|
3985
|
-
// src/commands/
|
|
3986
|
-
import { defineCommand as
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4008
|
+
// src/commands/apply.ts
|
|
4009
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
4010
|
+
|
|
4011
|
+
// src/apply/index.ts
|
|
4012
|
+
import { existsSync as existsSync8, promises as fs12 } from "fs";
|
|
4013
|
+
import { consola as consola11 } from "consola";
|
|
4014
|
+
|
|
4015
|
+
// src/config/state.ts
|
|
4016
|
+
import { promises as fs9 } from "fs";
|
|
4017
|
+
import path10 from "path";
|
|
4018
|
+
function buildStateFile(opts) {
|
|
4019
|
+
return {
|
|
4020
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
4021
|
+
origin: opts.origin,
|
|
4022
|
+
monocerosCliVersion: opts.cliVersion,
|
|
4023
|
+
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
function stateFilePath(targetDir) {
|
|
4027
|
+
return path10.join(targetDir, ".monoceros", "state.json");
|
|
4028
|
+
}
|
|
4029
|
+
async function readStateFile(targetDir) {
|
|
4030
|
+
try {
|
|
4031
|
+
const content = await fs9.readFile(stateFilePath(targetDir), "utf8");
|
|
4032
|
+
return JSON.parse(content);
|
|
4033
|
+
} catch {
|
|
4034
|
+
return void 0;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
async function writeStateFile(targetDir, state) {
|
|
4038
|
+
const monocerosDir = path10.join(targetDir, ".monoceros");
|
|
4039
|
+
await fs9.mkdir(monocerosDir, { recursive: true });
|
|
4040
|
+
await fs9.writeFile(
|
|
4041
|
+
stateFilePath(targetDir),
|
|
4042
|
+
JSON.stringify(state, null, 2) + "\n"
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
// src/config/transform.ts
|
|
4047
|
+
function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
4048
|
+
const featureRecord = {};
|
|
4049
|
+
for (const entry2 of config.features) {
|
|
4050
|
+
const defaults = featureDefaults[entry2.ref] ?? {};
|
|
4051
|
+
const containerOpts = Object.fromEntries(
|
|
4052
|
+
Object.entries(entry2.options ?? {}).filter(([, v]) => v !== "")
|
|
4053
|
+
);
|
|
4054
|
+
featureRecord[entry2.ref] = { ...defaults, ...containerOpts };
|
|
4055
|
+
}
|
|
4056
|
+
const result = {
|
|
4057
|
+
name: config.name,
|
|
4058
|
+
languages: [...config.languages],
|
|
4059
|
+
// Normalize every services[] entry (curated string or explicit
|
|
4060
|
+
// object) to the canonical ResolvedService shape. `${VAR}` values
|
|
4061
|
+
// survive untouched here — apply interpolates them against
|
|
4062
|
+
// <name>.env afterwards.
|
|
4063
|
+
services: config.services.map(resolveService)
|
|
4064
|
+
};
|
|
4065
|
+
if (config.externalServices.postgres !== void 0) {
|
|
4066
|
+
result.postgresUrl = config.externalServices.postgres;
|
|
4067
|
+
}
|
|
4068
|
+
if (config.aptPackages.length > 0) {
|
|
4069
|
+
result.aptPackages = [...config.aptPackages];
|
|
4070
|
+
}
|
|
4071
|
+
if (Object.keys(featureRecord).length > 0) {
|
|
4072
|
+
result.features = featureRecord;
|
|
4073
|
+
}
|
|
4074
|
+
if (config.installUrls.length > 0) {
|
|
4075
|
+
result.installUrls = [...config.installUrls];
|
|
4076
|
+
}
|
|
4077
|
+
if (config.repos.length > 0) {
|
|
4078
|
+
result.repos = config.repos.map((r) => ({
|
|
4079
|
+
url: r.url,
|
|
4080
|
+
// `path` is optional in the yml; CreateOptions requires it.
|
|
4081
|
+
// When the yml omits `path`, fall back to the URL-derived
|
|
4082
|
+
// single-segment default (`https://.../foo.git` → `foo`),
|
|
4083
|
+
// which lands the clone at `projects/foo/`.
|
|
4084
|
+
path: r.path ?? deriveRepoName(r.url),
|
|
4085
|
+
// gitUser is forwarded only when BOTH name + email are set.
|
|
4086
|
+
// The relaxed GitUserSchema accepts nullable / empty strings
|
|
4087
|
+
// (so a yml placeholder `name:` parses without error), so we
|
|
4088
|
+
// re-check here before downstream code, which expects both
|
|
4089
|
+
// values to be non-empty.
|
|
4090
|
+
...r.git?.user?.name && r.git.user.email ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
4091
|
+
...r.provider ? { provider: r.provider } : {}
|
|
4092
|
+
}));
|
|
4093
|
+
}
|
|
4094
|
+
const routingPorts = config.routing?.ports ?? [];
|
|
4095
|
+
if (routingPorts.length > 0) {
|
|
4096
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4097
|
+
const ports = [];
|
|
4098
|
+
for (const entry2 of routingPorts) {
|
|
4099
|
+
const n = portNumber(entry2);
|
|
4100
|
+
if (seen.has(n)) continue;
|
|
4101
|
+
seen.add(n);
|
|
4102
|
+
ports.push(n);
|
|
4103
|
+
}
|
|
4104
|
+
result.ports = ports;
|
|
4105
|
+
}
|
|
4106
|
+
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
4107
|
+
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
4108
|
+
}
|
|
4109
|
+
return result;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
// src/devcontainer/compose.ts
|
|
4113
|
+
import { spawn as spawn5 } from "child_process";
|
|
4114
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4115
|
+
import path12 from "path";
|
|
4116
|
+
import { consola as consola9 } from "consola";
|
|
4117
|
+
|
|
4118
|
+
// src/util/mask-secrets.ts
|
|
4119
|
+
import { Transform } from "stream";
|
|
4120
|
+
var PATTERNS = [
|
|
4121
|
+
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
4122
|
+
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
4123
|
+
// matching unrelated all-caps words.
|
|
4124
|
+
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
4125
|
+
// Bitbucket Cloud app password.
|
|
4126
|
+
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
4127
|
+
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
4128
|
+
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
4129
|
+
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
4130
|
+
// GitHub fine-grained PAT.
|
|
4131
|
+
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
4132
|
+
// Anthropic API key.
|
|
4133
|
+
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
4134
|
+
];
|
|
4135
|
+
function maskSecrets(text) {
|
|
4136
|
+
let result = text;
|
|
4137
|
+
for (const { re } of PATTERNS) {
|
|
4138
|
+
result = result.replace(re, maskOne);
|
|
4139
|
+
}
|
|
4140
|
+
return result;
|
|
4141
|
+
}
|
|
4142
|
+
function maskOne(token) {
|
|
4143
|
+
if (token.length <= 12) return token;
|
|
4144
|
+
return `${token.slice(0, 5)}\u2026${token.slice(-6)}`;
|
|
4145
|
+
}
|
|
4146
|
+
function createSecretMaskStream() {
|
|
4147
|
+
let buffer = "";
|
|
4148
|
+
return new Transform({
|
|
4149
|
+
decodeStrings: true,
|
|
4150
|
+
transform(chunk, _enc, cb) {
|
|
4151
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
4152
|
+
buffer += text;
|
|
4153
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
4154
|
+
if (lastNewline === -1) {
|
|
4155
|
+
cb(null);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
const flushable = buffer.slice(0, lastNewline + 1);
|
|
4159
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
4160
|
+
cb(null, maskSecrets(flushable));
|
|
3999
4161
|
},
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4162
|
+
flush(cb) {
|
|
4163
|
+
if (buffer.length > 0) {
|
|
4164
|
+
const tail = maskSecrets(buffer);
|
|
4165
|
+
buffer = "";
|
|
4166
|
+
cb(null, tail);
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
cb(null);
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
// src/devcontainer/cli.ts
|
|
4175
|
+
import { spawn as spawn4 } from "child_process";
|
|
4176
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4177
|
+
import { createRequire } from "module";
|
|
4178
|
+
import path11 from "path";
|
|
4179
|
+
|
|
4180
|
+
// src/devcontainer/runtime-pull-hint.ts
|
|
4181
|
+
import { Transform as Transform2 } from "stream";
|
|
4182
|
+
var RUNTIME_PULL_MARKER = "No manifest found for ghcr.io/getmonoceros/monoceros-runtime";
|
|
4183
|
+
var RUNTIME_PULL_HINT = 'Downloading the Monoceros runtime image now -- expected on first apply, takes ~1-2 min (Docker pulls the multi-arch base with no progress output). The "No manifest found" line above is harmless. Please wait...';
|
|
4184
|
+
function createRuntimePullHintStream(state) {
|
|
4185
|
+
let buffer = "";
|
|
4186
|
+
const appendHintIfMarker = (block) => {
|
|
4187
|
+
if (state.hinted || !block.includes(RUNTIME_PULL_MARKER)) return block;
|
|
4188
|
+
state.hinted = true;
|
|
4189
|
+
return `${block}${dim(`(i) ${RUNTIME_PULL_HINT}`)}
|
|
4190
|
+
`;
|
|
4191
|
+
};
|
|
4192
|
+
return new Transform2({
|
|
4193
|
+
decodeStrings: true,
|
|
4194
|
+
transform(chunk, _enc, cb) {
|
|
4195
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
4196
|
+
buffer += text;
|
|
4197
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
4198
|
+
if (lastNewline === -1) {
|
|
4199
|
+
cb(null);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
const flushable = buffer.slice(0, lastNewline + 1);
|
|
4203
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
4204
|
+
cb(null, appendHintIfMarker(flushable));
|
|
4005
4205
|
},
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4206
|
+
flush(cb) {
|
|
4207
|
+
if (buffer.length === 0) {
|
|
4208
|
+
cb(null);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
const tail = buffer;
|
|
4212
|
+
buffer = "";
|
|
4213
|
+
cb(null, appendHintIfMarker(tail));
|
|
4010
4214
|
}
|
|
4011
|
-
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
// src/devcontainer/cli.ts
|
|
4219
|
+
var require_ = createRequire(import.meta.url);
|
|
4220
|
+
var cachedBinaryPath = null;
|
|
4221
|
+
function devcontainerCliPath() {
|
|
4222
|
+
if (cachedBinaryPath) return cachedBinaryPath;
|
|
4223
|
+
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
4224
|
+
const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
|
|
4225
|
+
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
4226
|
+
if (!binEntry) {
|
|
4227
|
+
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
4228
|
+
}
|
|
4229
|
+
cachedBinaryPath = path11.resolve(path11.dirname(pkgJsonPath), binEntry);
|
|
4230
|
+
return cachedBinaryPath;
|
|
4231
|
+
}
|
|
4232
|
+
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
4233
|
+
const binPath = devcontainerCliPath();
|
|
4234
|
+
return new Promise((resolve, reject) => {
|
|
4235
|
+
if (options.interactive) {
|
|
4236
|
+
const child2 = spawn4(process.execPath, [binPath, ...args], {
|
|
4237
|
+
cwd,
|
|
4238
|
+
stdio: "inherit"
|
|
4239
|
+
});
|
|
4240
|
+
child2.on("error", reject);
|
|
4241
|
+
child2.on("exit", (code) => resolve(code ?? 0));
|
|
4242
|
+
return;
|
|
4019
4243
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4244
|
+
const child = spawn4(process.execPath, [binPath, ...args], {
|
|
4245
|
+
cwd,
|
|
4246
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4247
|
+
});
|
|
4248
|
+
if (options.quiet) {
|
|
4249
|
+
const stdoutChunks = [];
|
|
4250
|
+
const stderrChunks = [];
|
|
4251
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
4252
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
4253
|
+
child.on("error", reject);
|
|
4254
|
+
child.on("exit", (code) => {
|
|
4255
|
+
const exitCode = code ?? 0;
|
|
4256
|
+
if (exitCode !== 0) {
|
|
4257
|
+
process.stderr.write(
|
|
4258
|
+
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
4259
|
+
);
|
|
4260
|
+
process.stderr.write(
|
|
4261
|
+
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
resolve(exitCode);
|
|
4026
4265
|
});
|
|
4027
|
-
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
const pullHint = { hinted: false };
|
|
4269
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stdout);
|
|
4270
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stderr);
|
|
4271
|
+
child.on("error", reject);
|
|
4272
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
4273
|
+
});
|
|
4274
|
+
};
|
|
4275
|
+
|
|
4276
|
+
// src/devcontainer/compose.ts
|
|
4277
|
+
var spawnDockerCompose = (args, cwd) => {
|
|
4278
|
+
return new Promise((resolve, reject) => {
|
|
4279
|
+
const child = spawn5("docker", ["compose", ...args], {
|
|
4280
|
+
cwd,
|
|
4281
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
4282
|
+
});
|
|
4283
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
4284
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
4285
|
+
child.on("error", reject);
|
|
4286
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
4287
|
+
});
|
|
4288
|
+
};
|
|
4289
|
+
var spawnDocker = (args) => {
|
|
4290
|
+
return new Promise((resolve, reject) => {
|
|
4291
|
+
const child = spawn5("docker", args, {
|
|
4292
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4293
|
+
});
|
|
4294
|
+
let stdout = "";
|
|
4295
|
+
let stderr = "";
|
|
4296
|
+
child.stdout?.on("data", (chunk) => {
|
|
4297
|
+
stdout += chunk.toString("utf8");
|
|
4298
|
+
});
|
|
4299
|
+
child.stderr?.on("data", (chunk) => {
|
|
4300
|
+
stderr += chunk.toString("utf8");
|
|
4301
|
+
});
|
|
4302
|
+
child.on("error", reject);
|
|
4303
|
+
child.on(
|
|
4304
|
+
"exit",
|
|
4305
|
+
(code) => resolve({ exitCode: code ?? 0, stdout, stderr })
|
|
4306
|
+
);
|
|
4307
|
+
});
|
|
4308
|
+
};
|
|
4309
|
+
async function findContainerIds(filters, exec = spawnDocker) {
|
|
4310
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4311
|
+
for (const filter of filters) {
|
|
4312
|
+
const result = await exec(["ps", "-aq", "--filter", filter]);
|
|
4313
|
+
if (result.exitCode !== 0) continue;
|
|
4314
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
4315
|
+
const id = line.trim();
|
|
4316
|
+
if (id) ids.add(id);
|
|
4031
4317
|
}
|
|
4032
4318
|
}
|
|
4033
|
-
|
|
4034
|
-
function coerceToken(raw) {
|
|
4035
|
-
const n = Number(raw);
|
|
4036
|
-
return Number.isFinite(n) ? n : raw;
|
|
4319
|
+
return [...ids];
|
|
4037
4320
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
type: "positional",
|
|
4051
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
4052
|
-
required: true
|
|
4053
|
-
},
|
|
4054
|
-
service: {
|
|
4055
|
-
type: "positional",
|
|
4056
|
-
description: "Service identifier (postgres, mysql, redis).",
|
|
4057
|
-
required: true
|
|
4058
|
-
},
|
|
4059
|
-
yes: {
|
|
4060
|
-
type: "boolean",
|
|
4061
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
4062
|
-
alias: ["y"],
|
|
4063
|
-
default: false
|
|
4321
|
+
async function cleanupDockerObjects(opts) {
|
|
4322
|
+
const exec = opts.exec ?? spawnDocker;
|
|
4323
|
+
const tag = opts.logTag ?? "cleanup";
|
|
4324
|
+
opts.logger.info(`[${tag}] tearing down docker project ${opts.projectName}\u2026`);
|
|
4325
|
+
const ids = await findContainerIds(opts.filters, exec);
|
|
4326
|
+
let rmExit = 0;
|
|
4327
|
+
if (ids.length > 0) {
|
|
4328
|
+
opts.logger.info(`[${tag}] removing containers: ${ids.join(" ")}`);
|
|
4329
|
+
const rmResult = await exec(["rm", "-f", ...ids]);
|
|
4330
|
+
rmExit = rmResult.exitCode;
|
|
4331
|
+
if (rmExit !== 0 && rmResult.stderr.trim()) {
|
|
4332
|
+
opts.logger.info(`[${tag}] ${rmResult.stderr.trim()}`);
|
|
4064
4333
|
}
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
});
|
|
4073
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4074
|
-
} catch (err) {
|
|
4075
|
-
consola9.error(err instanceof Error ? err.message : String(err));
|
|
4076
|
-
process.exit(1);
|
|
4334
|
+
} else {
|
|
4335
|
+
opts.logger.info(`[${tag}] no containers found`);
|
|
4336
|
+
}
|
|
4337
|
+
if (opts.network) {
|
|
4338
|
+
const netResult = await exec(["network", "rm", opts.network]);
|
|
4339
|
+
if (netResult.exitCode === 0) {
|
|
4340
|
+
opts.logger.info(`[${tag}] network ${opts.network} removed`);
|
|
4077
4341
|
}
|
|
4078
4342
|
}
|
|
4079
|
-
});
|
|
4080
|
-
|
|
4081
|
-
// src/commands/apply.ts
|
|
4082
|
-
import { defineCommand as defineCommand8 } from "citty";
|
|
4083
|
-
|
|
4084
|
-
// src/apply/index.ts
|
|
4085
|
-
import { existsSync as existsSync6, promises as fs11 } from "fs";
|
|
4086
|
-
import { consola as consola11 } from "consola";
|
|
4087
|
-
|
|
4088
|
-
// src/config/state.ts
|
|
4089
|
-
import { promises as fs9 } from "fs";
|
|
4090
|
-
import path11 from "path";
|
|
4091
|
-
function buildStateFile(opts) {
|
|
4092
|
-
return {
|
|
4093
|
-
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
4094
|
-
origin: opts.origin,
|
|
4095
|
-
monocerosCliVersion: opts.cliVersion,
|
|
4096
|
-
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
4097
|
-
};
|
|
4343
|
+
opts.logger.info(`[${tag}] docker cleanup done`);
|
|
4344
|
+
return { exitCode: rmExit, removedIds: ids };
|
|
4098
4345
|
}
|
|
4099
|
-
function
|
|
4100
|
-
return
|
|
4346
|
+
function composeProjectName(root) {
|
|
4347
|
+
return `${path12.basename(root)}_devcontainer`;
|
|
4101
4348
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4349
|
+
function resolveCompose(root) {
|
|
4350
|
+
if (!existsSync6(path12.join(root, ".devcontainer"))) {
|
|
4351
|
+
throw new Error(
|
|
4352
|
+
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
const composeFile = path12.join(root, ".devcontainer", "compose.yaml");
|
|
4356
|
+
if (!existsSync6(composeFile)) {
|
|
4357
|
+
throw new Error(
|
|
4358
|
+
`No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
|
|
4359
|
+
);
|
|
4108
4360
|
}
|
|
4361
|
+
return { composeFile, projectName: composeProjectName(root) };
|
|
4109
4362
|
}
|
|
4110
|
-
async function
|
|
4111
|
-
const
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4363
|
+
async function runComposeAction(buildSubArgs, opts) {
|
|
4364
|
+
const { composeFile, projectName } = resolveCompose(opts.root);
|
|
4365
|
+
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
4366
|
+
const subArgs = buildSubArgs(opts.service);
|
|
4367
|
+
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
4368
|
+
}
|
|
4369
|
+
async function runStart(opts) {
|
|
4370
|
+
resolveCompose(opts.root);
|
|
4371
|
+
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
4372
|
+
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
4373
|
+
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
4374
|
+
return spawnFn(
|
|
4375
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
4376
|
+
opts.root
|
|
4116
4377
|
);
|
|
4117
4378
|
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const containerOpts = Object.fromEntries(
|
|
4125
|
-
Object.entries(entry2.options ?? {}).filter(([, v]) => v !== "")
|
|
4379
|
+
async function runContainerCycle(root, opts) {
|
|
4380
|
+
const { hasCompose, logger } = opts;
|
|
4381
|
+
if (hasCompose) {
|
|
4382
|
+
const projectName = composeProjectName(root);
|
|
4383
|
+
logger.info(
|
|
4384
|
+
`Force-removing existing ${projectName} containers (volumes preserved)\u2026`
|
|
4126
4385
|
);
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
// single-segment default (`https://.../foo.git` → `foo`),
|
|
4152
|
-
// which lands the clone at `projects/foo/`.
|
|
4153
|
-
path: r.path ?? deriveRepoName(r.url),
|
|
4154
|
-
// gitUser is forwarded only when BOTH name + email are set.
|
|
4155
|
-
// The relaxed GitUserSchema accepts nullable / empty strings
|
|
4156
|
-
// (so a yml placeholder `name:` parses without error), so we
|
|
4157
|
-
// re-check here before downstream code, which expects both
|
|
4158
|
-
// values to be non-empty.
|
|
4159
|
-
...r.git?.user?.name && r.git.user.email ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
4160
|
-
...r.provider ? { provider: r.provider } : {}
|
|
4161
|
-
}));
|
|
4162
|
-
}
|
|
4163
|
-
const routingPorts = config.routing?.ports ?? [];
|
|
4164
|
-
if (routingPorts.length > 0) {
|
|
4165
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4166
|
-
const ports = [];
|
|
4167
|
-
for (const entry2 of routingPorts) {
|
|
4168
|
-
const n = portNumber(entry2);
|
|
4169
|
-
if (seen.has(n)) continue;
|
|
4170
|
-
seen.add(n);
|
|
4171
|
-
ports.push(n);
|
|
4386
|
+
const exec = opts.dockerExec ?? spawnDocker;
|
|
4387
|
+
const filters = [
|
|
4388
|
+
`label=com.docker.compose.project=${projectName}`,
|
|
4389
|
+
`name=^${projectName}-`
|
|
4390
|
+
];
|
|
4391
|
+
const { exitCode: rmExit } = await cleanupDockerObjects({
|
|
4392
|
+
projectName,
|
|
4393
|
+
filters,
|
|
4394
|
+
network: `${projectName}_default`,
|
|
4395
|
+
logger,
|
|
4396
|
+
exec
|
|
4397
|
+
});
|
|
4398
|
+
if (rmExit !== 0) return rmExit;
|
|
4399
|
+
const remaining = await findContainerIds(filters, exec);
|
|
4400
|
+
if (remaining.length > 0) {
|
|
4401
|
+
const warn = logger.warn ?? logger.info;
|
|
4402
|
+
warn(
|
|
4403
|
+
`ERROR: containers under project ${projectName} reappeared after removal.
|
|
4404
|
+
This typically means VS Code's Remote Containers extension is connected
|
|
4405
|
+
to this devcontainer and auto-recreated it. Close the dev container
|
|
4406
|
+
session in VS Code (Cmd+Shift+P \u2192 'Dev Containers: Close Remote Connection')
|
|
4407
|
+
and retry \`monoceros apply\`.`
|
|
4408
|
+
);
|
|
4409
|
+
return 1;
|
|
4172
4410
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4411
|
+
return runStart({
|
|
4412
|
+
root,
|
|
4413
|
+
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
4414
|
+
logger
|
|
4415
|
+
});
|
|
4177
4416
|
}
|
|
4178
|
-
|
|
4417
|
+
logger.info(`Recreating image-mode devcontainer at ${root}\u2026`);
|
|
4418
|
+
const spawnFn = opts.devcontainerSpawn ?? spawnDevcontainer;
|
|
4419
|
+
return spawnFn(
|
|
4420
|
+
[
|
|
4421
|
+
"up",
|
|
4422
|
+
"--workspace-folder",
|
|
4423
|
+
root,
|
|
4424
|
+
"--mount-workspace-git-root=false",
|
|
4425
|
+
"--remove-existing-container"
|
|
4426
|
+
],
|
|
4427
|
+
root
|
|
4428
|
+
);
|
|
4429
|
+
}
|
|
4430
|
+
function runStop(opts) {
|
|
4431
|
+
return runComposeAction(
|
|
4432
|
+
(service) => ["stop", ...service ? [service] : []],
|
|
4433
|
+
opts
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
function runStatus(opts) {
|
|
4437
|
+
return runComposeAction(
|
|
4438
|
+
(service) => ["ps", ...service ? [service] : []],
|
|
4439
|
+
opts
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
function runLogs(opts) {
|
|
4443
|
+
const follow = opts.follow ?? true;
|
|
4444
|
+
return runComposeAction(
|
|
4445
|
+
(service) => [
|
|
4446
|
+
"logs",
|
|
4447
|
+
...follow ? ["-f"] : [],
|
|
4448
|
+
...service ? [service] : []
|
|
4449
|
+
],
|
|
4450
|
+
opts
|
|
4451
|
+
);
|
|
4179
4452
|
}
|
|
4180
4453
|
|
|
4181
4454
|
// src/devcontainer/repo-reachability.ts
|
|
@@ -4271,13 +4544,19 @@ function formatUnreachableReposError(failures) {
|
|
|
4271
4544
|
lines.push(headerForKind(kind));
|
|
4272
4545
|
for (const e of entries) {
|
|
4273
4546
|
lines.push(` \u2022 ${e.url}`);
|
|
4547
|
+
if (e.detail) {
|
|
4548
|
+
for (const detailLine of e.detail.split("\n")) {
|
|
4549
|
+
const trimmed = detailLine.trim();
|
|
4550
|
+
if (trimmed) lines.push(` git: ${trimmed}`);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4274
4553
|
}
|
|
4275
4554
|
for (const advice of adviceForKind(kind)) {
|
|
4276
4555
|
lines.push(` - ${advice}`);
|
|
4277
4556
|
}
|
|
4278
4557
|
lines.push("");
|
|
4279
4558
|
}
|
|
4280
|
-
lines.push(`Then re-run ${
|
|
4559
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
4281
4560
|
return lines.join("\n");
|
|
4282
4561
|
}
|
|
4283
4562
|
function headerForKind(kind) {
|
|
@@ -4317,11 +4596,85 @@ function adviceForKind(kind) {
|
|
|
4317
4596
|
}
|
|
4318
4597
|
}
|
|
4319
4598
|
|
|
4320
|
-
// src/devcontainer/
|
|
4599
|
+
// src/devcontainer/repo-clone.ts
|
|
4321
4600
|
import { spawn as spawn7 } from "child_process";
|
|
4601
|
+
import { existsSync as existsSync7, promises as fs10 } from "fs";
|
|
4602
|
+
import path13 from "path";
|
|
4603
|
+
var realGitClone = (url, dest) => {
|
|
4604
|
+
return new Promise((resolve, reject) => {
|
|
4605
|
+
const child = spawn7("git", ["clone", "--", url, dest], {
|
|
4606
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4607
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
4608
|
+
});
|
|
4609
|
+
let stdout = "";
|
|
4610
|
+
let stderr = "";
|
|
4611
|
+
child.stdout.on("data", (c) => {
|
|
4612
|
+
stdout += c.toString();
|
|
4613
|
+
});
|
|
4614
|
+
child.stderr.on("data", (c) => {
|
|
4615
|
+
stderr += c.toString();
|
|
4616
|
+
});
|
|
4617
|
+
child.on("error", reject);
|
|
4618
|
+
child.on(
|
|
4619
|
+
"exit",
|
|
4620
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
4621
|
+
);
|
|
4622
|
+
});
|
|
4623
|
+
};
|
|
4624
|
+
async function cloneReposHostSide(containerRoot, repos, options = {}) {
|
|
4625
|
+
const spawnFn = options.spawn ?? realGitClone;
|
|
4626
|
+
const results = [];
|
|
4627
|
+
for (const repo of repos) {
|
|
4628
|
+
const dest = path13.join(containerRoot, "projects", repo.path);
|
|
4629
|
+
if (existsSync7(dest)) {
|
|
4630
|
+
results.push({ path: repo.path, url: repo.url, status: "skipped" });
|
|
4631
|
+
continue;
|
|
4632
|
+
}
|
|
4633
|
+
await fs10.mkdir(path13.dirname(dest), { recursive: true });
|
|
4634
|
+
let r;
|
|
4635
|
+
try {
|
|
4636
|
+
r = await spawnFn(repo.url, dest);
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
results.push({
|
|
4639
|
+
path: repo.path,
|
|
4640
|
+
url: repo.url,
|
|
4641
|
+
status: "failed",
|
|
4642
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4643
|
+
});
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
results.push(
|
|
4647
|
+
r.exitCode === 0 ? { path: repo.path, url: repo.url, status: "cloned" } : {
|
|
4648
|
+
path: repo.path,
|
|
4649
|
+
url: repo.url,
|
|
4650
|
+
status: "failed",
|
|
4651
|
+
detail: r.stderr.trim()
|
|
4652
|
+
}
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
return results;
|
|
4656
|
+
}
|
|
4657
|
+
function formatCloneFailuresError(failures) {
|
|
4658
|
+
const lines = failures.length === 1 ? [`Failed to clone declared repo: ${failures[0].url}`, ""] : [`Failed to clone ${failures.length} declared repos:`, ""];
|
|
4659
|
+
for (const f of failures) {
|
|
4660
|
+
lines.push(` \u2022 ${f.url} \u2192 projects/${f.path}`);
|
|
4661
|
+
if (f.detail) lines.push(` ${f.detail}`);
|
|
4662
|
+
}
|
|
4663
|
+
lines.push("");
|
|
4664
|
+
lines.push(
|
|
4665
|
+
"Reachability was confirmed earlier, so this is usually a local issue"
|
|
4666
|
+
);
|
|
4667
|
+
lines.push(
|
|
4668
|
+
"(disk space, a leftover non-empty target dir). Fix it and re-run " + cyan2("monoceros apply") + "."
|
|
4669
|
+
);
|
|
4670
|
+
return lines.join("\n");
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
// src/devcontainer/docker-mode.ts
|
|
4674
|
+
import { spawn as spawn8 } from "child_process";
|
|
4322
4675
|
var realDockerInfo = () => {
|
|
4323
4676
|
return new Promise((resolve, reject) => {
|
|
4324
|
-
const child =
|
|
4677
|
+
const child = spawn8(
|
|
4325
4678
|
"docker",
|
|
4326
4679
|
["info", "--format", "{{json .SecurityOptions}}"],
|
|
4327
4680
|
{
|
|
@@ -4360,14 +4713,14 @@ function formatRootlessNotSupportedError() {
|
|
|
4360
4713
|
``,
|
|
4361
4714
|
`To fix, switch back to standard rootful Docker:`,
|
|
4362
4715
|
``,
|
|
4363
|
-
|
|
4716
|
+
cyan2(
|
|
4364
4717
|
` systemctl --user stop docker.service docker.socket 2>/dev/null || true`
|
|
4365
4718
|
),
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4719
|
+
cyan2(` dockerd-rootless-setuptool.sh uninstall`),
|
|
4720
|
+
cyan2(` rootlesskit rm -rf ~/.local/share/docker`),
|
|
4721
|
+
cyan2(` unset DOCKER_HOST DOCKER_CONTEXT`),
|
|
4722
|
+
cyan2(` sudo systemctl enable --now docker`),
|
|
4723
|
+
cyan2(` sudo usermod -aG docker $USER`),
|
|
4371
4724
|
``,
|
|
4372
4725
|
`If you added DOCKER_HOST or DOCKER_CONTEXT to ~/.bashrc /`,
|
|
4373
4726
|
`~/.profile (the rootless setup may have suggested it), remove`,
|
|
@@ -4380,13 +4733,13 @@ function formatRootlessNotSupportedError() {
|
|
|
4380
4733
|
}
|
|
4381
4734
|
|
|
4382
4735
|
// src/devcontainer/identity.ts
|
|
4383
|
-
import { spawn as
|
|
4384
|
-
import { promises as
|
|
4385
|
-
import
|
|
4736
|
+
import { spawn as spawn9 } from "child_process";
|
|
4737
|
+
import { promises as fs11 } from "fs";
|
|
4738
|
+
import path14 from "path";
|
|
4386
4739
|
import { consola as consola10 } from "consola";
|
|
4387
4740
|
var realGitConfigGet = (key) => {
|
|
4388
4741
|
return new Promise((resolve, reject) => {
|
|
4389
|
-
const child =
|
|
4742
|
+
const child = spawn9("git", ["config", "--global", "--get", key], {
|
|
4390
4743
|
stdio: ["ignore", "pipe", "inherit"]
|
|
4391
4744
|
});
|
|
4392
4745
|
let stdout = "";
|
|
@@ -4496,8 +4849,8 @@ async function resolveIdentityWithPrompt(options = {}) {
|
|
|
4496
4849
|
};
|
|
4497
4850
|
}
|
|
4498
4851
|
async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
4499
|
-
const gitconfigDir =
|
|
4500
|
-
const gitconfigPath =
|
|
4852
|
+
const gitconfigDir = path14.join(devContainerRoot, ".monoceros");
|
|
4853
|
+
const gitconfigPath = path14.join(gitconfigDir, "gitconfig");
|
|
4501
4854
|
const logger = options.logger ?? { info: () => {
|
|
4502
4855
|
}, warn: () => {
|
|
4503
4856
|
} };
|
|
@@ -4510,8 +4863,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
4510
4863
|
const lines = ["[user]"];
|
|
4511
4864
|
if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
|
|
4512
4865
|
if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
|
|
4513
|
-
await
|
|
4514
|
-
await
|
|
4866
|
+
await fs11.mkdir(gitconfigDir, { recursive: true });
|
|
4867
|
+
await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
4515
4868
|
return {
|
|
4516
4869
|
...resolved.name !== void 0 ? { name: resolved.name } : {},
|
|
4517
4870
|
...resolved.email !== void 0 ? { email: resolved.email } : {},
|
|
@@ -4554,7 +4907,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
|
|
|
4554
4907
|
}
|
|
4555
4908
|
async function readExistingGitconfig(filePath) {
|
|
4556
4909
|
try {
|
|
4557
|
-
const content = await
|
|
4910
|
+
const content = await fs11.readFile(filePath, "utf8");
|
|
4558
4911
|
const result = {};
|
|
4559
4912
|
const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
|
|
4560
4913
|
const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
|
|
@@ -4587,7 +4940,7 @@ ${sectionLine(label)}
|
|
|
4587
4940
|
);
|
|
4588
4941
|
}
|
|
4589
4942
|
const ymlPath = containerConfigPath(opts.name, home);
|
|
4590
|
-
if (!
|
|
4943
|
+
if (!existsSync8(ymlPath)) {
|
|
4591
4944
|
throw new Error(
|
|
4592
4945
|
`No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
|
|
4593
4946
|
);
|
|
@@ -4604,6 +4957,20 @@ ${sectionLine(label)}
|
|
|
4604
4957
|
globalConfig?.defaults?.features ?? {}
|
|
4605
4958
|
)
|
|
4606
4959
|
);
|
|
4960
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
4961
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
4962
|
+
const envVars = readEnvFile(envPath);
|
|
4963
|
+
const interpServices = interpolateServices(createOpts.services, envVars);
|
|
4964
|
+
const interpFeatures = interpolateFeatures(
|
|
4965
|
+
createOpts.features ?? {},
|
|
4966
|
+
envVars
|
|
4967
|
+
);
|
|
4968
|
+
const missingVars = [...interpServices.missing, ...interpFeatures.missing];
|
|
4969
|
+
if (missingVars.length > 0) {
|
|
4970
|
+
throw new Error(formatMissingVarsError(missingVars, prettyPath(envPath)));
|
|
4971
|
+
}
|
|
4972
|
+
createOpts.services = interpServices.services;
|
|
4973
|
+
if (createOpts.features) createOpts.features = interpFeatures.features;
|
|
4607
4974
|
validateOptions(createOpts);
|
|
4608
4975
|
logger.success(`yml validated ${dim(`(${prettyPath(ymlPath)})`)}`);
|
|
4609
4976
|
const hasRepos = (createOpts.repos ?? []).length > 0;
|
|
@@ -4658,7 +5025,7 @@ ${sectionLine(label)}
|
|
|
4658
5025
|
if (dockerMode === "rootless") {
|
|
4659
5026
|
throw new Error(formatRootlessNotSupportedError());
|
|
4660
5027
|
}
|
|
4661
|
-
await
|
|
5028
|
+
await fs12.mkdir(targetDir, { recursive: true });
|
|
4662
5029
|
await writeScaffold(createOpts, targetDir, { dockerMode });
|
|
4663
5030
|
await writeStateFile(
|
|
4664
5031
|
targetDir,
|
|
@@ -4669,10 +5036,27 @@ ${sectionLine(label)}
|
|
|
4669
5036
|
})
|
|
4670
5037
|
);
|
|
4671
5038
|
logger.success(`materialized into ${prettyPath(targetDir)}`);
|
|
5039
|
+
const reposToClone = createOpts.repos ?? [];
|
|
5040
|
+
if (reposToClone.length > 0) {
|
|
5041
|
+
const cloneResults = await cloneReposHostSide(targetDir, reposToClone, {
|
|
5042
|
+
...opts.cloneSpawn ? { spawn: opts.cloneSpawn } : {}
|
|
5043
|
+
});
|
|
5044
|
+
for (const r of cloneResults) {
|
|
5045
|
+
if (r.status === "cloned") {
|
|
5046
|
+
logger.success(`cloned ${cyan2(r.path)} ${dim(`(${r.url})`)}`);
|
|
5047
|
+
} else if (r.status === "skipped") {
|
|
5048
|
+
logger.info(`projects/${r.path} already present \u2014 skipped clone`);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
const cloneFailures = cloneResults.filter((r) => r.status === "failed");
|
|
5052
|
+
if (cloneFailures.length > 0) {
|
|
5053
|
+
throw new Error(formatCloneFailuresError(cloneFailures));
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
4672
5056
|
section("Container");
|
|
4673
5057
|
const featureRefs = parsed.config.features.map((f) => f.ref);
|
|
4674
5058
|
if (featureRefs.length > 0) {
|
|
4675
|
-
logger.info(`Features: ${featureRefs.map((r) =>
|
|
5059
|
+
logger.info(`Features: ${featureRefs.map((r) => cyan2(r)).join(", ")}`);
|
|
4676
5060
|
}
|
|
4677
5061
|
logger.info(
|
|
4678
5062
|
dim(
|
|
@@ -4695,14 +5079,8 @@ ${sectionLine(label)}
|
|
|
4695
5079
|
hostPort: proxyHostPort(globalConfig),
|
|
4696
5080
|
logger
|
|
4697
5081
|
});
|
|
4698
|
-
await kickProxyReload({
|
|
4699
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4700
|
-
});
|
|
4701
5082
|
} else {
|
|
4702
5083
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
4703
|
-
await kickProxyReload({
|
|
4704
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4705
|
-
});
|
|
4706
5084
|
}
|
|
4707
5085
|
} catch (err) {
|
|
4708
5086
|
logger.warn?.(
|
|
@@ -4717,13 +5095,13 @@ ${sectionLine(label)}
|
|
|
4717
5095
|
});
|
|
4718
5096
|
if (exitCode === 0) {
|
|
4719
5097
|
section("Next steps");
|
|
4720
|
-
logger.info(` ${
|
|
5098
|
+
logger.info(` ${cyan2(`monoceros shell ${opts.name}`)}`);
|
|
4721
5099
|
}
|
|
4722
5100
|
return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
|
|
4723
5101
|
}
|
|
4724
5102
|
async function assertSafeTargetDir(targetDir, expectedOrigin) {
|
|
4725
|
-
if (!
|
|
4726
|
-
const entries = await
|
|
5103
|
+
if (!existsSync8(targetDir)) return;
|
|
5104
|
+
const entries = await fs12.readdir(targetDir);
|
|
4727
5105
|
if (entries.length === 0) return;
|
|
4728
5106
|
const state = await readStateFile(targetDir);
|
|
4729
5107
|
if (state) {
|
|
@@ -4793,7 +5171,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4793
5171
|
}
|
|
4794
5172
|
if (wantContainer) {
|
|
4795
5173
|
try {
|
|
4796
|
-
const text = await
|
|
5174
|
+
const text = await fs12.readFile(ymlPath, "utf8");
|
|
4797
5175
|
const parsed = parseConfig(text, ymlPath);
|
|
4798
5176
|
const changed = setContainerGitUserInDoc(parsed.doc, {
|
|
4799
5177
|
name: prompted.name,
|
|
@@ -4801,7 +5179,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4801
5179
|
});
|
|
4802
5180
|
if (changed) {
|
|
4803
5181
|
const out = stringifyConfig(parsed.doc);
|
|
4804
|
-
await
|
|
5182
|
+
await fs12.writeFile(ymlPath, out, "utf8");
|
|
4805
5183
|
logger.info(
|
|
4806
5184
|
`Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
|
|
4807
5185
|
);
|
|
@@ -4815,7 +5193,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4815
5193
|
}
|
|
4816
5194
|
|
|
4817
5195
|
// src/version.ts
|
|
4818
|
-
var CLI_VERSION = true ? "1.
|
|
5196
|
+
var CLI_VERSION = true ? "1.13.0" : "dev";
|
|
4819
5197
|
|
|
4820
5198
|
// src/commands/_dispatch.ts
|
|
4821
5199
|
import { consola as consola12 } from "consola";
|
|
@@ -4975,8 +5353,8 @@ var completionCommand = defineCommand9({
|
|
|
4975
5353
|
import { defineCommand as defineCommand10 } from "citty";
|
|
4976
5354
|
|
|
4977
5355
|
// src/completion/resolve.ts
|
|
4978
|
-
import { existsSync as
|
|
4979
|
-
import
|
|
5356
|
+
import { existsSync as existsSync9, promises as fs13 } from "fs";
|
|
5357
|
+
import path15 from "path";
|
|
4980
5358
|
async function resolveCompletions(line, point, opts = {}) {
|
|
4981
5359
|
const { prev, current } = parseCompletionLine(line, point);
|
|
4982
5360
|
const ctx = { prev, current, opts };
|
|
@@ -5124,15 +5502,11 @@ function filterPrefix(values, fragment) {
|
|
|
5124
5502
|
}
|
|
5125
5503
|
async function listContainerNames(ctx) {
|
|
5126
5504
|
const home = ctx.opts.monocerosHome ?? monocerosHome();
|
|
5127
|
-
const dir =
|
|
5128
|
-
if (!
|
|
5129
|
-
const entries = await
|
|
5505
|
+
const dir = path15.join(home, "container-configs");
|
|
5506
|
+
if (!existsSync9(dir)) return [];
|
|
5507
|
+
const entries = await fs13.readdir(dir);
|
|
5130
5508
|
return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
|
|
5131
5509
|
}
|
|
5132
|
-
async function listAllCatalogComponents() {
|
|
5133
|
-
const catalog = await loadComponentCatalog();
|
|
5134
|
-
return [...catalog.keys()].sort();
|
|
5135
|
-
}
|
|
5136
5510
|
async function listFeatureComponents() {
|
|
5137
5511
|
const catalog = await loadComponentCatalog();
|
|
5138
5512
|
return [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
|
|
@@ -5234,8 +5608,14 @@ var COMMAND_SPECS = {
|
|
|
5234
5608
|
// flag suggestions.
|
|
5235
5609
|
positionalCount: 1,
|
|
5236
5610
|
flags: {
|
|
5237
|
-
"--with": { type: "value", values: () =>
|
|
5238
|
-
"--with-
|
|
5611
|
+
"--with-languages": { type: "value", values: () => listLanguageNames() },
|
|
5612
|
+
"--with-features": {
|
|
5613
|
+
type: "value",
|
|
5614
|
+
values: () => listFeatureComponents()
|
|
5615
|
+
},
|
|
5616
|
+
"--with-services": { type: "value", values: () => listServiceNames() },
|
|
5617
|
+
"--with-apt-packages": { type: "value" },
|
|
5618
|
+
"--with-repos": { type: "value" },
|
|
5239
5619
|
"--with-ports": { type: "value" }
|
|
5240
5620
|
}
|
|
5241
5621
|
},
|
|
@@ -5379,22 +5759,22 @@ import { defineCommand as defineCommand11 } from "citty";
|
|
|
5379
5759
|
import { consola as consola14 } from "consola";
|
|
5380
5760
|
|
|
5381
5761
|
// src/init/index.ts
|
|
5382
|
-
import { existsSync as
|
|
5762
|
+
import { existsSync as existsSync10, promises as fs14 } from "fs";
|
|
5763
|
+
import path16 from "path";
|
|
5383
5764
|
import { consola as consola13 } from "consola";
|
|
5384
5765
|
|
|
5385
5766
|
// src/init/generator.ts
|
|
5386
5767
|
var SCHEMA_HEADER_ACTIVE = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Edit any section, then run `monoceros apply <name>` to (re-)build.";
|
|
5387
5768
|
var SCHEMA_HEADER_DOCUMENTED = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Every section is commented out by default; un-comment what you need\n# (strip one `#` per line of the block), then run `monoceros apply <name>`.";
|
|
5388
5769
|
var COMMENT_WIDTH = 76;
|
|
5389
|
-
function generateComposedYml(name,
|
|
5390
|
-
const merged = mergeComponents(components);
|
|
5770
|
+
function generateComposedYml(name, composed, lookupManifest, repoUrls = [], ports = []) {
|
|
5391
5771
|
const lines = [];
|
|
5392
5772
|
pushHeader(lines, SCHEMA_HEADER_ACTIVE, name);
|
|
5393
5773
|
lines.push("");
|
|
5394
5774
|
lines.push("schemaVersion: 1");
|
|
5395
5775
|
lines.push(`name: ${name}`);
|
|
5396
5776
|
lines.push("");
|
|
5397
|
-
if (
|
|
5777
|
+
if (composed.languages.length > 0) {
|
|
5398
5778
|
pushSectionHeader(
|
|
5399
5779
|
lines,
|
|
5400
5780
|
LANGUAGES_HEADER,
|
|
@@ -5402,10 +5782,21 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5402
5782
|
false
|
|
5403
5783
|
);
|
|
5404
5784
|
lines.push("languages:");
|
|
5405
|
-
for (const lang of
|
|
5785
|
+
for (const lang of composed.languages) lines.push(` - ${lang}`);
|
|
5406
5786
|
lines.push("");
|
|
5407
5787
|
}
|
|
5408
|
-
if (
|
|
5788
|
+
if (composed.aptPackages.length > 0) {
|
|
5789
|
+
pushSectionHeader(
|
|
5790
|
+
lines,
|
|
5791
|
+
APT_PACKAGES_HEADER,
|
|
5792
|
+
/* commented */
|
|
5793
|
+
false
|
|
5794
|
+
);
|
|
5795
|
+
lines.push("aptPackages:");
|
|
5796
|
+
for (const pkg of composed.aptPackages) lines.push(` - ${pkg}`);
|
|
5797
|
+
lines.push("");
|
|
5798
|
+
}
|
|
5799
|
+
if (composed.services.length > 0) {
|
|
5409
5800
|
pushSectionHeader(
|
|
5410
5801
|
lines,
|
|
5411
5802
|
SERVICES_HEADER,
|
|
@@ -5413,10 +5804,10 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5413
5804
|
false
|
|
5414
5805
|
);
|
|
5415
5806
|
lines.push("services:");
|
|
5416
|
-
for (const svc of
|
|
5807
|
+
for (const svc of composed.services) pushServiceEntry(lines, svc);
|
|
5417
5808
|
lines.push("");
|
|
5418
5809
|
}
|
|
5419
|
-
if (
|
|
5810
|
+
if (composed.features.length > 0) {
|
|
5420
5811
|
pushSectionHeader(
|
|
5421
5812
|
lines,
|
|
5422
5813
|
FEATURES_HEADER_ACTIVE,
|
|
@@ -5424,7 +5815,7 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5424
5815
|
false
|
|
5425
5816
|
);
|
|
5426
5817
|
lines.push("features:");
|
|
5427
|
-
for (const f of
|
|
5818
|
+
for (const f of composed.features) {
|
|
5428
5819
|
lines.push("");
|
|
5429
5820
|
renderFeatureBlock(
|
|
5430
5821
|
lines,
|
|
@@ -5505,7 +5896,9 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
5505
5896
|
lines.push("# services:");
|
|
5506
5897
|
for (const c of byCategory.service) {
|
|
5507
5898
|
for (const svc of c.file.contributes.services ?? []) {
|
|
5508
|
-
|
|
5899
|
+
const body = renderServiceObjectBody(expandCuratedService(svc));
|
|
5900
|
+
lines.push(`# - ${body[0]}`);
|
|
5901
|
+
for (const line of body.slice(1)) lines.push(`# ${line}`);
|
|
5509
5902
|
}
|
|
5510
5903
|
}
|
|
5511
5904
|
lines.push("");
|
|
@@ -5612,6 +6005,22 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
5612
6005
|
}
|
|
5613
6006
|
var LANGUAGES_HEADER = "Language runtimes installed inside the dev-container. Pick the ones your projects build against. The catalog of available runtimes is shown by `monoceros list-components`.";
|
|
5614
6007
|
var SERVICES_HEADER = "Sibling containers that run alongside the dev-container (databases, caches, message queues, \u2026). Each service is reachable from inside the dev-container by its name as hostname (e.g. `postgres://postgres:5432`). Activating any service switches the container to docker-compose mode automatically.";
|
|
6008
|
+
var APT_PACKAGES_HEADER = "Debian/Ubuntu apt packages installed in the dev-container at build time. No curated list \u2014 any apt package name works; an invalid name surfaces as an apt error during build.";
|
|
6009
|
+
function pushServiceEntry(out, svc) {
|
|
6010
|
+
if (svc.kind === "custom") {
|
|
6011
|
+
const { bodyLines, comment } = renderCustomService(
|
|
6012
|
+
svc.name,
|
|
6013
|
+
svc.image ?? ""
|
|
6014
|
+
);
|
|
6015
|
+
out.push(` - ${bodyLines[0]}`);
|
|
6016
|
+
for (const line of bodyLines.slice(1)) out.push(` ${line}`);
|
|
6017
|
+
for (const cl of comment.split("\n")) out.push(` #${cl}`);
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
const body = renderServiceObjectBody(expandCuratedService(svc.name));
|
|
6021
|
+
out.push(` - ${body[0]}`);
|
|
6022
|
+
for (const line of body.slice(1)) out.push(` ${line}`);
|
|
6023
|
+
}
|
|
5615
6024
|
var FEATURES_HEADER_ACTIVE = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. The features active for this container are listed below; adjust their options as needed. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
5616
6025
|
var FEATURES_HEADER_DOCUMENTED = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. Un-comment the blocks below for the features you want active. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
5617
6026
|
var REPOS_HEADER = "Git repositories cloned into `projects/` on container start-up. HTTPS URLs only. The provider is auto-detected for github.com / gitlab.com / bitbucket.org; for any other host (self-hosted GitLab, Gitea, \u2026) declare `provider:` explicitly. Add more later with `monoceros add-repo`.";
|
|
@@ -5626,15 +6035,15 @@ function renderFeatureBlock(out, feature, summary, commented) {
|
|
|
5626
6035
|
out.push(`${yamlPrefix} - ref: ${feature.ref}`);
|
|
5627
6036
|
const options = feature.options ?? {};
|
|
5628
6037
|
const activeKeys = Object.entries(options);
|
|
5629
|
-
const
|
|
5630
|
-
if (activeKeys.length === 0 &&
|
|
6038
|
+
const hints = featureOptionHints(summary, feature.ref, Object.keys(options));
|
|
6039
|
+
if (activeKeys.length === 0 && hints.length === 0) return;
|
|
5631
6040
|
if (commented) {
|
|
5632
6041
|
out.push(`${yamlPrefix} options:`);
|
|
5633
6042
|
for (const [key, value] of activeKeys) {
|
|
5634
6043
|
out.push(`${yamlPrefix} ${key}: ${renderScalarValue(value)}`);
|
|
5635
6044
|
}
|
|
5636
|
-
for (const
|
|
5637
|
-
out.push(`${yamlPrefix} ${key}
|
|
6045
|
+
for (const hint of hints) {
|
|
6046
|
+
out.push(`${yamlPrefix} ${hint.key}: ${hint.placeholder}`);
|
|
5638
6047
|
}
|
|
5639
6048
|
return;
|
|
5640
6049
|
}
|
|
@@ -5644,10 +6053,10 @@ function renderFeatureBlock(out, feature, summary, commented) {
|
|
|
5644
6053
|
out.push(` ${key}: ${renderScalarValue(value)}`);
|
|
5645
6054
|
}
|
|
5646
6055
|
}
|
|
5647
|
-
if (
|
|
6056
|
+
if (hints.length > 0) {
|
|
5648
6057
|
out.push(` # options:`);
|
|
5649
|
-
for (const
|
|
5650
|
-
out.push(` # ${key}
|
|
6058
|
+
for (const hint of hints) {
|
|
6059
|
+
out.push(` # ${hint.key}: ${hint.placeholder}`);
|
|
5651
6060
|
}
|
|
5652
6061
|
}
|
|
5653
6062
|
}
|
|
@@ -5701,7 +6110,7 @@ async function runInit(opts) {
|
|
|
5701
6110
|
);
|
|
5702
6111
|
}
|
|
5703
6112
|
const dest = containerConfigPath(opts.name, home);
|
|
5704
|
-
if (
|
|
6113
|
+
if (existsSync10(dest)) {
|
|
5705
6114
|
throw new Error(
|
|
5706
6115
|
`Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
|
|
5707
6116
|
);
|
|
@@ -5771,15 +6180,28 @@ async function runInit(opts) {
|
|
|
5771
6180
|
});
|
|
5772
6181
|
}
|
|
5773
6182
|
let text;
|
|
5774
|
-
const
|
|
5775
|
-
|
|
6183
|
+
const composed = resolveComposedInit(catalog, {
|
|
6184
|
+
languages: opts.languages ?? [],
|
|
6185
|
+
features: opts.features ?? [],
|
|
6186
|
+
services: opts.services ?? [],
|
|
6187
|
+
aptPackages: opts.aptPackages ?? []
|
|
6188
|
+
});
|
|
6189
|
+
const anyComposed = composed.languages.length > 0 || composed.features.length > 0 || composed.services.length > 0 || composed.aptPackages.length > 0;
|
|
6190
|
+
if (!anyComposed) {
|
|
5776
6191
|
text = generateDocumentedYml(opts.name, catalog, lookup, repos, ports);
|
|
5777
6192
|
} else {
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
}
|
|
5781
|
-
await
|
|
5782
|
-
await
|
|
6193
|
+
text = generateComposedYml(opts.name, composed, lookup, repos, ports);
|
|
6194
|
+
}
|
|
6195
|
+
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
6196
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
6197
|
+
await fs14.writeFile(dest, text, "utf8");
|
|
6198
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
6199
|
+
const featureVars = composed.features.flatMap(
|
|
6200
|
+
(f) => featureOptionHints(lookup(f.ref), f.ref, Object.keys(f.options ?? {})).map(
|
|
6201
|
+
(h) => h.envVar
|
|
6202
|
+
)
|
|
6203
|
+
);
|
|
6204
|
+
await ensureEnvVars(envPath, opts.name, featureVars);
|
|
5783
6205
|
if (promptedIdentity?.prompted) {
|
|
5784
6206
|
const { name, email, scope } = promptedIdentity.prompted;
|
|
5785
6207
|
if (scope === "g" || scope === "b") {
|
|
@@ -5809,11 +6231,11 @@ async function runInit(opts) {
|
|
|
5809
6231
|
}
|
|
5810
6232
|
if (scope === "c" || scope === "b") {
|
|
5811
6233
|
try {
|
|
5812
|
-
const written = await
|
|
6234
|
+
const written = await fs14.readFile(dest, "utf8");
|
|
5813
6235
|
const parsed = parseConfig(written, dest);
|
|
5814
6236
|
const changed = setContainerGitUserInDoc(parsed.doc, { name, email });
|
|
5815
6237
|
if (changed) {
|
|
5816
|
-
await
|
|
6238
|
+
await fs14.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
|
|
5817
6239
|
logger.info(
|
|
5818
6240
|
`Saved identity in ${prettyPath(dest)} (container-level git.user).`
|
|
5819
6241
|
);
|
|
@@ -5825,29 +6247,136 @@ async function runInit(opts) {
|
|
|
5825
6247
|
}
|
|
5826
6248
|
}
|
|
5827
6249
|
}
|
|
5828
|
-
const documented =
|
|
5829
|
-
const
|
|
6250
|
+
const documented = !anyComposed;
|
|
6251
|
+
const ymlRel = path16.relative(home, dest);
|
|
6252
|
+
const envRel = path16.relative(home, envPath);
|
|
5830
6253
|
if (documented) {
|
|
5831
|
-
logger.success(
|
|
5832
|
-
|
|
6254
|
+
logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
|
|
6255
|
+
logger.info(
|
|
6256
|
+
`Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
|
|
5833
6257
|
);
|
|
5834
6258
|
} else {
|
|
5835
|
-
logger.success(
|
|
5836
|
-
`Composed ${requested.length} component(s) into ${displayPath}: ${requested.join(", ")}`
|
|
5837
|
-
);
|
|
6259
|
+
logger.success(`Composed into ${ymlRel} and ${envRel}.`);
|
|
5838
6260
|
logger.info(
|
|
5839
|
-
`Edit the
|
|
6261
|
+
`Edit the files if you need to tweak, then \`monoceros apply ${opts.name}\`.`
|
|
5840
6262
|
);
|
|
5841
6263
|
}
|
|
5842
6264
|
return { configPath: dest, documented };
|
|
5843
6265
|
}
|
|
6266
|
+
function resolveComposedInit(catalog, raw) {
|
|
6267
|
+
return {
|
|
6268
|
+
languages: resolveInitLanguages(raw.languages),
|
|
6269
|
+
aptPackages: resolveInitAptPackages(raw.aptPackages),
|
|
6270
|
+
services: resolveInitServices(raw.services),
|
|
6271
|
+
features: resolveInitFeatures(catalog, raw.features)
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
function resolveInitLanguages(entries) {
|
|
6275
|
+
const known = new Set(knownLanguages());
|
|
6276
|
+
const out = [];
|
|
6277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6278
|
+
const unknown = [];
|
|
6279
|
+
for (const raw of entries) {
|
|
6280
|
+
const e = raw.trim();
|
|
6281
|
+
if (!e || seen.has(e)) continue;
|
|
6282
|
+
const spec = parseLanguageSpec(e);
|
|
6283
|
+
if (!spec || !known.has(spec.name)) {
|
|
6284
|
+
unknown.push(e);
|
|
6285
|
+
continue;
|
|
6286
|
+
}
|
|
6287
|
+
seen.add(e);
|
|
6288
|
+
out.push(e);
|
|
6289
|
+
}
|
|
6290
|
+
if (unknown.length > 0) {
|
|
6291
|
+
throw new Error(
|
|
6292
|
+
`Unknown language${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Known: ${knownLanguages().join(", ")}.`
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
return out;
|
|
6296
|
+
}
|
|
6297
|
+
function resolveInitAptPackages(entries) {
|
|
6298
|
+
const out = [];
|
|
6299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6300
|
+
const bad = [];
|
|
6301
|
+
for (const raw of entries) {
|
|
6302
|
+
const e = raw.trim();
|
|
6303
|
+
if (!e || seen.has(e)) continue;
|
|
6304
|
+
if (!REGEX.aptPackage.test(e)) {
|
|
6305
|
+
bad.push(e);
|
|
6306
|
+
continue;
|
|
6307
|
+
}
|
|
6308
|
+
seen.add(e);
|
|
6309
|
+
out.push(e);
|
|
6310
|
+
}
|
|
6311
|
+
if (bad.length > 0) {
|
|
6312
|
+
throw new Error(
|
|
6313
|
+
`Invalid apt package name${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}. Expected lowercase alphanumeric plus '.+-'.`
|
|
6314
|
+
);
|
|
6315
|
+
}
|
|
6316
|
+
return out;
|
|
6317
|
+
}
|
|
6318
|
+
function resolveInitServices(entries) {
|
|
6319
|
+
const out = [];
|
|
6320
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6321
|
+
for (const raw of entries) {
|
|
6322
|
+
const e = raw.trim();
|
|
6323
|
+
if (!e) continue;
|
|
6324
|
+
const svc = isCuratedService(e) ? { kind: "curated", name: e } : { kind: "custom", name: deriveServiceName(e), image: e };
|
|
6325
|
+
const existing = byName.get(svc.name);
|
|
6326
|
+
if (existing) {
|
|
6327
|
+
if (existing.kind === svc.kind && existing.image === svc.image) continue;
|
|
6328
|
+
throw new Error(
|
|
6329
|
+
`Two --with-services entries resolve to the service name '${svc.name}'. Add one after init with \`monoceros add-service ${"<name>"} <image> --as=<other>\`.`
|
|
6330
|
+
);
|
|
6331
|
+
}
|
|
6332
|
+
byName.set(svc.name, svc);
|
|
6333
|
+
out.push(svc);
|
|
6334
|
+
}
|
|
6335
|
+
return out;
|
|
6336
|
+
}
|
|
6337
|
+
function resolveInitFeatures(catalog, entries) {
|
|
6338
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
6339
|
+
const unknown = [];
|
|
6340
|
+
for (const raw of entries) {
|
|
6341
|
+
const e = raw.trim();
|
|
6342
|
+
if (!e) continue;
|
|
6343
|
+
if (REGEX.featureRef.test(e)) {
|
|
6344
|
+
if (!byRef.has(e)) byRef.set(e, { ref: e, options: {} });
|
|
6345
|
+
continue;
|
|
6346
|
+
}
|
|
6347
|
+
const c = catalog.get(e);
|
|
6348
|
+
if (!c || c.file.category !== "feature") {
|
|
6349
|
+
unknown.push(e);
|
|
6350
|
+
continue;
|
|
6351
|
+
}
|
|
6352
|
+
for (const f of c.file.contributes.features ?? []) {
|
|
6353
|
+
const existing = byRef.get(f.ref);
|
|
6354
|
+
if (!existing) {
|
|
6355
|
+
byRef.set(f.ref, { ref: f.ref, options: { ...f.options ?? {} } });
|
|
6356
|
+
} else {
|
|
6357
|
+
existing.options = mergeFeatureOptions(
|
|
6358
|
+
existing.options,
|
|
6359
|
+
f.options ?? {}
|
|
6360
|
+
);
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
if (unknown.length > 0) {
|
|
6365
|
+
const featureNames = [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
|
|
6366
|
+
throw new Error(
|
|
6367
|
+
`Unknown feature${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
6368
|
+
Use a catalog short name (${featureNames.join(", ")}) or a full OCI ref (ghcr.io/\u2026/<name>:<tag>).`
|
|
6369
|
+
);
|
|
6370
|
+
}
|
|
6371
|
+
return [...byRef.values()];
|
|
6372
|
+
}
|
|
5844
6373
|
|
|
5845
6374
|
// src/commands/init.ts
|
|
5846
6375
|
var initCommand = defineCommand11({
|
|
5847
6376
|
meta: {
|
|
5848
6377
|
name: "init",
|
|
5849
6378
|
group: "lifecycle",
|
|
5850
|
-
description: "Create a fresh container-config yml at
|
|
6379
|
+
description: "Create a fresh container-config yml at <MONOCEROS_HOME>/container-configs/<name>.yml. Without any --with-* flag, the file is a documented default with every component commented out. With --with-languages / --with-features / --with-services / --with-apt-packages, the named pieces are composed into an active, immediately-applyable yml. Then run `monoceros apply <name>`."
|
|
5851
6380
|
},
|
|
5852
6381
|
args: {
|
|
5853
6382
|
name: {
|
|
@@ -5855,14 +6384,29 @@ var initCommand = defineCommand11({
|
|
|
5855
6384
|
description: "Config name. The yml lands at <MONOCEROS_HOME>/container-configs/<name>.yml and becomes the source-of-truth for `monoceros apply <name>`.",
|
|
5856
6385
|
required: true
|
|
5857
6386
|
},
|
|
5858
|
-
with: {
|
|
6387
|
+
"with-languages": {
|
|
6388
|
+
type: "string",
|
|
6389
|
+
description: "Language runtimes to install, comma-separated or repeated, e.g. --with-languages=java,node. Optional :version (java:17). Curated catalog only \u2014 see `monoceros list-components`.",
|
|
6390
|
+
required: false
|
|
6391
|
+
},
|
|
6392
|
+
"with-features": {
|
|
6393
|
+
type: "string",
|
|
6394
|
+
description: "Features (AI tools, language CLIs, \u2026), comma-separated or repeated. Catalog short name (claude, atlassian/twg) or a full OCI ref (ghcr.io/foo/bar:1).",
|
|
6395
|
+
required: false
|
|
6396
|
+
},
|
|
6397
|
+
"with-services": {
|
|
6398
|
+
type: "string",
|
|
6399
|
+
description: "Backing services, comma-separated or repeated. Curated name (postgres, mysql, redis) \u2192 full editable block; any other image (rustfs/rustfs:latest) \u2192 name + image + commented scaffold.",
|
|
6400
|
+
required: false
|
|
6401
|
+
},
|
|
6402
|
+
"with-apt-packages": {
|
|
5859
6403
|
type: "string",
|
|
5860
|
-
description: "
|
|
6404
|
+
description: "Debian/Ubuntu apt packages to install, comma-separated or repeated, e.g. --with-apt-packages=openssl,make. No curated list.",
|
|
5861
6405
|
required: false
|
|
5862
6406
|
},
|
|
5863
|
-
"with-
|
|
6407
|
+
"with-repos": {
|
|
5864
6408
|
type: "string",
|
|
5865
|
-
description: "Git
|
|
6409
|
+
description: "Git URLs to clone into projects/ on first apply, comma-separated or repeated. Folder name derived from URL (foo.git \u2192 projects/foo/); use `monoceros add-repo --path=...` post-init for subfolder paths. Canonical hosts only (github.com / gitlab.com / bitbucket.org).",
|
|
5866
6410
|
required: false
|
|
5867
6411
|
},
|
|
5868
6412
|
"with-ports": {
|
|
@@ -5873,14 +6417,20 @@ var initCommand = defineCommand11({
|
|
|
5873
6417
|
},
|
|
5874
6418
|
async run({ args, rawArgs }) {
|
|
5875
6419
|
try {
|
|
5876
|
-
const
|
|
5877
|
-
const
|
|
5878
|
-
const
|
|
6420
|
+
const languages = collectListFlag("--with-languages", rawArgs);
|
|
6421
|
+
const features = collectListFlag("--with-features", rawArgs);
|
|
6422
|
+
const services = collectListFlag("--with-services", rawArgs);
|
|
6423
|
+
const aptPackages = collectListFlag("--with-apt-packages", rawArgs);
|
|
6424
|
+
const repos = collectListFlag("--with-repos", rawArgs);
|
|
6425
|
+
const ports = collectWithPortsList(args["with-ports"], rawArgs);
|
|
5879
6426
|
await runInit({
|
|
5880
6427
|
name: args.name,
|
|
5881
|
-
...
|
|
5882
|
-
...
|
|
5883
|
-
...
|
|
6428
|
+
...languages.length > 0 ? { languages } : {},
|
|
6429
|
+
...features.length > 0 ? { features } : {},
|
|
6430
|
+
...services.length > 0 ? { services } : {},
|
|
6431
|
+
...aptPackages.length > 0 ? { aptPackages } : {},
|
|
6432
|
+
...repos.length > 0 ? { withRepo: repos } : {},
|
|
6433
|
+
...ports && ports.length > 0 ? { withPorts: ports } : {}
|
|
5884
6434
|
});
|
|
5885
6435
|
} catch (err) {
|
|
5886
6436
|
consola14.error(err instanceof Error ? err.message : String(err));
|
|
@@ -5888,6 +6438,30 @@ var initCommand = defineCommand11({
|
|
|
5888
6438
|
}
|
|
5889
6439
|
}
|
|
5890
6440
|
});
|
|
6441
|
+
function collectListFlag(flag, rawArgs) {
|
|
6442
|
+
const eq = `${flag}=`;
|
|
6443
|
+
const pieces = [];
|
|
6444
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
6445
|
+
const t = rawArgs[i];
|
|
6446
|
+
let scanStart = -1;
|
|
6447
|
+
if (t === flag) {
|
|
6448
|
+
scanStart = i + 1;
|
|
6449
|
+
} else if (t.startsWith(eq)) {
|
|
6450
|
+
pieces.push(t.slice(eq.length));
|
|
6451
|
+
scanStart = i + 1;
|
|
6452
|
+
}
|
|
6453
|
+
if (scanStart < 0) continue;
|
|
6454
|
+
let j = scanStart;
|
|
6455
|
+
while (j < rawArgs.length) {
|
|
6456
|
+
const u = rawArgs[j];
|
|
6457
|
+
if (u.startsWith("-")) break;
|
|
6458
|
+
pieces.push(u);
|
|
6459
|
+
j += 1;
|
|
6460
|
+
}
|
|
6461
|
+
i = j - 1;
|
|
6462
|
+
}
|
|
6463
|
+
return pieces.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6464
|
+
}
|
|
5891
6465
|
function collectWithPortsList(_withPortsArg, rawArgs) {
|
|
5892
6466
|
const pieces = [];
|
|
5893
6467
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
@@ -5923,43 +6497,6 @@ function collectWithPortsList(_withPortsArg, rawArgs) {
|
|
|
5923
6497
|
}
|
|
5924
6498
|
return out;
|
|
5925
6499
|
}
|
|
5926
|
-
function collectWithRepoList(rawArgs) {
|
|
5927
|
-
const urls = [];
|
|
5928
|
-
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
5929
|
-
const t = rawArgs[i];
|
|
5930
|
-
if (t === "--with-repo") {
|
|
5931
|
-
const next = rawArgs[i + 1];
|
|
5932
|
-
if (typeof next === "string" && !next.startsWith("-")) {
|
|
5933
|
-
urls.push(next);
|
|
5934
|
-
i += 1;
|
|
5935
|
-
}
|
|
5936
|
-
} else if (t.startsWith("--with-repo=")) {
|
|
5937
|
-
urls.push(t.slice("--with-repo=".length));
|
|
5938
|
-
}
|
|
5939
|
-
}
|
|
5940
|
-
return urls;
|
|
5941
|
-
}
|
|
5942
|
-
function collectWithList(withArg, rawArgs) {
|
|
5943
|
-
if (typeof withArg !== "string" || withArg.trim().length === 0) {
|
|
5944
|
-
return void 0;
|
|
5945
|
-
}
|
|
5946
|
-
let combined = withArg.trim();
|
|
5947
|
-
const startIdx = rawArgs.findIndex(
|
|
5948
|
-
(t) => t === "--with" || t.startsWith("--with=")
|
|
5949
|
-
);
|
|
5950
|
-
if (startIdx >= 0) {
|
|
5951
|
-
let scanFrom = startIdx + 1;
|
|
5952
|
-
if (rawArgs[startIdx] === "--with") scanFrom += 1;
|
|
5953
|
-
for (let i = scanFrom; i < rawArgs.length; i += 1) {
|
|
5954
|
-
const t = rawArgs[i];
|
|
5955
|
-
if (t.startsWith("--") || t === "-h" || t === "--help") break;
|
|
5956
|
-
const sep = combined.endsWith(",") ? "" : ",";
|
|
5957
|
-
combined += sep + t;
|
|
5958
|
-
}
|
|
5959
|
-
}
|
|
5960
|
-
const pieces = combined.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5961
|
-
return pieces.length > 0 ? pieces : void 0;
|
|
5962
|
-
}
|
|
5963
6500
|
|
|
5964
6501
|
// src/commands/list-components.ts
|
|
5965
6502
|
import { defineCommand as defineCommand12 } from "citty";
|
|
@@ -6247,8 +6784,8 @@ import { consola as consola20 } from "consola";
|
|
|
6247
6784
|
import { createInterface } from "readline/promises";
|
|
6248
6785
|
|
|
6249
6786
|
// src/remove/index.ts
|
|
6250
|
-
import { existsSync as
|
|
6251
|
-
import
|
|
6787
|
+
import { existsSync as existsSync11, promises as fs15 } from "fs";
|
|
6788
|
+
import path17 from "path";
|
|
6252
6789
|
import { consola as consola19 } from "consola";
|
|
6253
6790
|
async function runRemove(opts) {
|
|
6254
6791
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -6263,9 +6800,11 @@ async function runRemove(opts) {
|
|
|
6263
6800
|
);
|
|
6264
6801
|
}
|
|
6265
6802
|
const ymlPath = containerConfigPath(opts.name, home);
|
|
6803
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
6266
6804
|
const containerPath = containerDir(opts.name, home);
|
|
6267
|
-
const hasYml =
|
|
6268
|
-
const
|
|
6805
|
+
const hasYml = existsSync11(ymlPath);
|
|
6806
|
+
const hasEnv = existsSync11(envPath);
|
|
6807
|
+
const hasContainer = existsSync11(containerPath);
|
|
6269
6808
|
if (!hasYml && !hasContainer) {
|
|
6270
6809
|
throw new Error(
|
|
6271
6810
|
`Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
|
|
@@ -6277,7 +6816,7 @@ async function runRemove(opts) {
|
|
|
6277
6816
|
projectName,
|
|
6278
6817
|
filters: [
|
|
6279
6818
|
`label=com.docker.compose.project=${projectName}`,
|
|
6280
|
-
`label=devcontainer.local_folder=${
|
|
6819
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
6281
6820
|
`name=^${projectName}-`,
|
|
6282
6821
|
`name=^vsc-${opts.name}-`
|
|
6283
6822
|
],
|
|
@@ -6289,24 +6828,30 @@ async function runRemove(opts) {
|
|
|
6289
6828
|
let backupPath = null;
|
|
6290
6829
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
6291
6830
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6292
|
-
backupPath =
|
|
6293
|
-
await
|
|
6831
|
+
backupPath = path17.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
6832
|
+
await fs15.mkdir(backupPath, { recursive: true });
|
|
6294
6833
|
if (hasYml) {
|
|
6295
|
-
await
|
|
6834
|
+
await fs15.copyFile(ymlPath, path17.join(backupPath, `${opts.name}.yml`));
|
|
6835
|
+
}
|
|
6836
|
+
if (hasEnv) {
|
|
6837
|
+
await fs15.copyFile(envPath, path17.join(backupPath, `${opts.name}.env`));
|
|
6296
6838
|
}
|
|
6297
6839
|
if (hasContainer) {
|
|
6298
|
-
await
|
|
6840
|
+
await fs15.cp(containerPath, path17.join(backupPath, "container"), {
|
|
6299
6841
|
recursive: true
|
|
6300
6842
|
});
|
|
6301
6843
|
}
|
|
6302
6844
|
logger.info(`Backup written to ${prettyPath(backupPath)}.`);
|
|
6303
6845
|
}
|
|
6304
6846
|
if (hasYml) {
|
|
6305
|
-
await
|
|
6847
|
+
await fs15.rm(ymlPath, { force: true });
|
|
6848
|
+
}
|
|
6849
|
+
if (hasEnv) {
|
|
6850
|
+
await fs15.rm(envPath, { force: true });
|
|
6306
6851
|
}
|
|
6307
6852
|
if (hasContainer) {
|
|
6308
6853
|
try {
|
|
6309
|
-
await
|
|
6854
|
+
await fs15.rm(containerPath, { recursive: true, force: true });
|
|
6310
6855
|
} catch (err) {
|
|
6311
6856
|
const code = err.code;
|
|
6312
6857
|
if (code !== "EACCES" && code !== "EPERM") {
|
|
@@ -6332,7 +6877,7 @@ async function runRemove(opts) {
|
|
|
6332
6877
|
`docker-based cleanup of ${containerPath} exited ${exit}. Inspect with \`sudo ls -la ${containerPath}\` and clean manually.`
|
|
6333
6878
|
);
|
|
6334
6879
|
}
|
|
6335
|
-
await
|
|
6880
|
+
await fs15.rm(containerPath, { recursive: true, force: true });
|
|
6336
6881
|
}
|
|
6337
6882
|
}
|
|
6338
6883
|
logger.success(
|
|
@@ -6345,9 +6890,6 @@ async function runRemove(opts) {
|
|
|
6345
6890
|
}
|
|
6346
6891
|
try {
|
|
6347
6892
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
6348
|
-
await kickProxyReload({
|
|
6349
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
6350
|
-
});
|
|
6351
6893
|
} catch (err) {
|
|
6352
6894
|
logger.warn?.(
|
|
6353
6895
|
`Could not remove Traefik dynamic config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
@@ -6438,8 +6980,8 @@ import { defineCommand as defineCommand18 } from "citty";
|
|
|
6438
6980
|
import { consola as consola22 } from "consola";
|
|
6439
6981
|
|
|
6440
6982
|
// src/restore/index.ts
|
|
6441
|
-
import { existsSync as
|
|
6442
|
-
import
|
|
6983
|
+
import { existsSync as existsSync12, promises as fs16 } from "fs";
|
|
6984
|
+
import path18 from "path";
|
|
6443
6985
|
import { consola as consola21 } from "consola";
|
|
6444
6986
|
async function runRestore(opts) {
|
|
6445
6987
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -6447,15 +6989,15 @@ async function runRestore(opts) {
|
|
|
6447
6989
|
info: (msg) => consola21.info(msg),
|
|
6448
6990
|
success: (msg) => consola21.success(msg)
|
|
6449
6991
|
};
|
|
6450
|
-
const backup =
|
|
6451
|
-
if (!
|
|
6992
|
+
const backup = path18.resolve(opts.backupPath);
|
|
6993
|
+
if (!existsSync12(backup)) {
|
|
6452
6994
|
throw new Error(`Backup not found: ${backup}.`);
|
|
6453
6995
|
}
|
|
6454
|
-
const stat = await
|
|
6996
|
+
const stat = await fs16.stat(backup);
|
|
6455
6997
|
if (!stat.isDirectory()) {
|
|
6456
6998
|
throw new Error(`Backup path is not a directory: ${backup}.`);
|
|
6457
6999
|
}
|
|
6458
|
-
const entries = await
|
|
7000
|
+
const entries = await fs16.readdir(backup);
|
|
6459
7001
|
const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
|
|
6460
7002
|
if (ymlFiles.length === 0) {
|
|
6461
7003
|
throw new Error(
|
|
@@ -6469,24 +7011,29 @@ async function runRestore(opts) {
|
|
|
6469
7011
|
}
|
|
6470
7012
|
const ymlFile = ymlFiles[0];
|
|
6471
7013
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
6472
|
-
const containerInBackup =
|
|
6473
|
-
const hasContainer =
|
|
7014
|
+
const containerInBackup = path18.join(backup, "container");
|
|
7015
|
+
const hasContainer = existsSync12(containerInBackup);
|
|
7016
|
+
const envInBackup = path18.join(backup, `${name}.env`);
|
|
7017
|
+
const hasEnv = existsSync12(envInBackup);
|
|
6474
7018
|
const destYml = containerConfigPath(name, home);
|
|
6475
7019
|
const destContainer = containerDir(name, home);
|
|
6476
|
-
if (
|
|
7020
|
+
if (existsSync12(destYml)) {
|
|
6477
7021
|
throw new Error(
|
|
6478
7022
|
`Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
|
|
6479
7023
|
);
|
|
6480
7024
|
}
|
|
6481
|
-
if (hasContainer &&
|
|
7025
|
+
if (hasContainer && existsSync12(destContainer)) {
|
|
6482
7026
|
throw new Error(
|
|
6483
7027
|
`Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
|
|
6484
7028
|
);
|
|
6485
7029
|
}
|
|
6486
|
-
await
|
|
6487
|
-
await
|
|
7030
|
+
await fs16.mkdir(containerConfigsDir(home), { recursive: true });
|
|
7031
|
+
await fs16.copyFile(path18.join(backup, ymlFile), destYml);
|
|
7032
|
+
if (hasEnv) {
|
|
7033
|
+
await fs16.copyFile(envInBackup, containerEnvPath(name, home));
|
|
7034
|
+
}
|
|
6488
7035
|
if (hasContainer) {
|
|
6489
|
-
await
|
|
7036
|
+
await fs16.cp(containerInBackup, destContainer, { recursive: true });
|
|
6490
7037
|
}
|
|
6491
7038
|
logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
|
|
6492
7039
|
logger.info(
|
|
@@ -6744,8 +7291,8 @@ import { defineCommand as defineCommand24 } from "citty";
|
|
|
6744
7291
|
import { consola as consola28 } from "consola";
|
|
6745
7292
|
|
|
6746
7293
|
// src/devcontainer/shell.ts
|
|
6747
|
-
import { existsSync as
|
|
6748
|
-
import
|
|
7294
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7295
|
+
import path19 from "path";
|
|
6749
7296
|
async function runShell(opts) {
|
|
6750
7297
|
assertContainerExists(opts.root);
|
|
6751
7298
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -6768,7 +7315,7 @@ async function runShell(opts) {
|
|
|
6768
7315
|
);
|
|
6769
7316
|
}
|
|
6770
7317
|
function assertContainerExists(root) {
|
|
6771
|
-
if (!
|
|
7318
|
+
if (!existsSync13(path19.join(root, ".devcontainer"))) {
|
|
6772
7319
|
throw new Error(
|
|
6773
7320
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
6774
7321
|
);
|
|
@@ -6980,15 +7527,15 @@ import { defineCommand as defineCommand29 } from "citty";
|
|
|
6980
7527
|
import { consola as consola33 } from "consola";
|
|
6981
7528
|
|
|
6982
7529
|
// src/tunnel/run.ts
|
|
6983
|
-
import { spawn as
|
|
7530
|
+
import { spawn as spawn10 } from "child_process";
|
|
6984
7531
|
import { consola as consola32 } from "consola";
|
|
6985
7532
|
|
|
6986
7533
|
// src/tunnel/resolve.ts
|
|
6987
|
-
import { existsSync as
|
|
6988
|
-
import
|
|
7534
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7535
|
+
import path20 from "path";
|
|
6989
7536
|
async function resolveTunnelTarget(opts) {
|
|
6990
7537
|
const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
|
|
6991
|
-
if (!
|
|
7538
|
+
if (!existsSync14(ymlPath)) {
|
|
6992
7539
|
throw new Error(
|
|
6993
7540
|
`No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
|
|
6994
7541
|
);
|
|
@@ -6996,13 +7543,13 @@ async function resolveTunnelTarget(opts) {
|
|
|
6996
7543
|
const parsed = await readConfig(ymlPath);
|
|
6997
7544
|
const config = parsed.config;
|
|
6998
7545
|
const containerRoot = containerDir(opts.name, opts.monocerosHome);
|
|
6999
|
-
if (!
|
|
7546
|
+
if (!existsSync14(containerRoot)) {
|
|
7000
7547
|
throw new Error(
|
|
7001
7548
|
`Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
|
|
7002
7549
|
);
|
|
7003
7550
|
}
|
|
7004
|
-
const composePath =
|
|
7005
|
-
const isCompose =
|
|
7551
|
+
const composePath = path20.join(containerRoot, ".devcontainer", "compose.yaml");
|
|
7552
|
+
const isCompose = existsSync14(composePath);
|
|
7006
7553
|
const parsedTarget = parseTargetArg(opts.target, config);
|
|
7007
7554
|
const docker = opts.docker ?? defaultDockerExec;
|
|
7008
7555
|
if (isCompose) {
|
|
@@ -7021,23 +7568,41 @@ async function resolveTunnelTarget(opts) {
|
|
|
7021
7568
|
});
|
|
7022
7569
|
}
|
|
7023
7570
|
function parseTargetArg(raw, config) {
|
|
7571
|
+
const colon = raw.indexOf(":");
|
|
7572
|
+
if (colon > 0) {
|
|
7573
|
+
const name = raw.slice(0, colon);
|
|
7574
|
+
const port = Number(raw.slice(colon + 1));
|
|
7575
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
7576
|
+
throw new Error(
|
|
7577
|
+
`Invalid target '${raw}'. Use <service>:<port> with a numeric port (1\u201365535), a bare port number, or a configured service name.`
|
|
7578
|
+
);
|
|
7579
|
+
}
|
|
7580
|
+
findConfiguredService(config, name);
|
|
7581
|
+
return { kind: "service", service: name, port };
|
|
7582
|
+
}
|
|
7024
7583
|
const asNumber = Number(raw);
|
|
7025
7584
|
if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
|
|
7026
7585
|
return { kind: "port", port: asNumber };
|
|
7027
7586
|
}
|
|
7028
|
-
const
|
|
7029
|
-
if (
|
|
7030
|
-
const candidates = knownServices().join(", ");
|
|
7587
|
+
const match = findConfiguredService(config, raw);
|
|
7588
|
+
if (match.port === void 0) {
|
|
7031
7589
|
throw new Error(
|
|
7032
|
-
`
|
|
7590
|
+
`Service '${raw}' declares no port, so tunnel can't know what to forward. Add \`port: <n>\` to the service in the yml and re-apply, or pass one explicitly: \`monoceros tunnel <name> ${raw}:<port>\`.`
|
|
7033
7591
|
);
|
|
7034
7592
|
}
|
|
7035
|
-
|
|
7593
|
+
return { kind: "service", service: raw, port: match.port };
|
|
7594
|
+
}
|
|
7595
|
+
function findConfiguredService(config, name) {
|
|
7596
|
+
const services = config.services.map(resolveService);
|
|
7597
|
+
const match = services.find((s) => s.name === name);
|
|
7598
|
+
if (!match) {
|
|
7599
|
+
const names = services.map((s) => s.name);
|
|
7600
|
+
const list = names.length > 0 ? names.join(", ") : "(none configured)";
|
|
7036
7601
|
throw new Error(
|
|
7037
|
-
`Service '${
|
|
7602
|
+
`Service '${name}' is not configured in this container's yml. Configured services: ${list}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
|
|
7038
7603
|
);
|
|
7039
7604
|
}
|
|
7040
|
-
return
|
|
7605
|
+
return match;
|
|
7041
7606
|
}
|
|
7042
7607
|
function resolveCompose2(args) {
|
|
7043
7608
|
const network = `${composeProjectName(args.containerRoot)}_default`;
|
|
@@ -7046,7 +7611,7 @@ function resolveCompose2(args) {
|
|
|
7046
7611
|
network,
|
|
7047
7612
|
targetHost: args.parsedTarget.service,
|
|
7048
7613
|
internalPort: args.parsedTarget.port,
|
|
7049
|
-
display: `${args.name}/${args.parsedTarget.service}`
|
|
7614
|
+
display: `${args.name}/${args.parsedTarget.service}:${args.parsedTarget.port}`
|
|
7050
7615
|
};
|
|
7051
7616
|
}
|
|
7052
7617
|
return {
|
|
@@ -7087,10 +7652,7 @@ async function lookupContainerNetwork(args) {
|
|
|
7087
7652
|
"ps",
|
|
7088
7653
|
"-q",
|
|
7089
7654
|
"--filter",
|
|
7090
|
-
|
|
7091
|
-
// when stamping the label, docker filter is byte-exact. No-op
|
|
7092
|
-
// off Windows.
|
|
7093
|
-
`label=devcontainer.local_folder=${dockerLocalFolderLabel(args.containerRoot)}`
|
|
7655
|
+
`label=devcontainer.local_folder=${args.containerRoot}`
|
|
7094
7656
|
]);
|
|
7095
7657
|
if (psResult.exitCode !== 0) {
|
|
7096
7658
|
throw new Error(
|
|
@@ -7208,7 +7770,7 @@ function formatLocalPortHeldError(port, address, result) {
|
|
|
7208
7770
|
// src/tunnel/run.ts
|
|
7209
7771
|
var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
|
|
7210
7772
|
var defaultDockerSpawn = (args) => {
|
|
7211
|
-
const child =
|
|
7773
|
+
const child = spawn10("docker", args, {
|
|
7212
7774
|
stdio: "inherit"
|
|
7213
7775
|
});
|
|
7214
7776
|
const exited = new Promise((resolve, reject) => {
|
|
@@ -7331,7 +7893,7 @@ var tunnelCommand = defineCommand29({
|
|
|
7331
7893
|
},
|
|
7332
7894
|
target: {
|
|
7333
7895
|
type: "positional",
|
|
7334
|
-
description: "Service name from the container yml (e.g. `postgres`)
|
|
7896
|
+
description: "Service name from the container yml (e.g. `postgres`), `service:port` for an explicit in-container port (e.g. `rustfs:9001`), or a bare in-container port number \u2192 workspace (e.g. `8080`).",
|
|
7335
7897
|
required: true
|
|
7336
7898
|
},
|
|
7337
7899
|
"local-port": {
|
|
@@ -7412,7 +7974,6 @@ var main = defineCommand30({
|
|
|
7412
7974
|
|
|
7413
7975
|
// src/bin.ts
|
|
7414
7976
|
bootstrapDockerGroup();
|
|
7415
|
-
bootstrapWslBackend();
|
|
7416
7977
|
consumeInnerArgsFromProcessArgv();
|
|
7417
7978
|
async function entry() {
|
|
7418
7979
|
if (await maybeRenderHelp(process.argv.slice(2), main)) {
|