@getmonoceros/workbench 1.11.11 → 1.12.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 +1231 -1336
- 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
|
}
|
|
@@ -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 path8 from "path";
|
|
403
315
|
|
|
404
316
|
// src/config/io.ts
|
|
405
317
|
import { promises as fs } from "fs";
|
|
@@ -661,6 +573,38 @@ function prettyPath(p) {
|
|
|
661
573
|
import { spawn } from "child_process";
|
|
662
574
|
import { promises as fs2 } from "fs";
|
|
663
575
|
import path2 from "path";
|
|
576
|
+
|
|
577
|
+
// src/util/format.ts
|
|
578
|
+
var ESC = "\x1B[";
|
|
579
|
+
var ANSI_BOLD2 = `${ESC}1m`;
|
|
580
|
+
var ANSI_UNDERLINE2 = `${ESC}4m`;
|
|
581
|
+
var ANSI_CYAN2 = `${ESC}36m`;
|
|
582
|
+
var ANSI_GREY2 = `${ESC}90m`;
|
|
583
|
+
var ANSI_RESET2 = `${ESC}0m`;
|
|
584
|
+
function makeWrap(isTty2) {
|
|
585
|
+
return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET2 : s;
|
|
586
|
+
}
|
|
587
|
+
function makePalette(isTty2) {
|
|
588
|
+
const wrap = makeWrap(isTty2);
|
|
589
|
+
return {
|
|
590
|
+
bold: (s) => wrap(s, ANSI_BOLD2),
|
|
591
|
+
underline: (s) => wrap(s, ANSI_UNDERLINE2),
|
|
592
|
+
cyan: (s) => wrap(s, ANSI_CYAN2),
|
|
593
|
+
dim: (s) => wrap(s, ANSI_GREY2),
|
|
594
|
+
sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function colorsFor(stream) {
|
|
598
|
+
return makePalette(stream.isTTY ?? false);
|
|
599
|
+
}
|
|
600
|
+
var stderrPalette = makePalette(process.stderr.isTTY ?? false);
|
|
601
|
+
var bold2 = stderrPalette.bold;
|
|
602
|
+
var underline2 = stderrPalette.underline;
|
|
603
|
+
var cyan2 = stderrPalette.cyan;
|
|
604
|
+
var dim = stderrPalette.dim;
|
|
605
|
+
var sectionLine = stderrPalette.sectionLine;
|
|
606
|
+
|
|
607
|
+
// src/devcontainer/credentials.ts
|
|
664
608
|
var realGitCredentialFill = (input) => {
|
|
665
609
|
return new Promise((resolve, reject) => {
|
|
666
610
|
const child = spawn("git", ["credential", "fill"], {
|
|
@@ -719,20 +663,14 @@ var BREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercon
|
|
|
719
663
|
function installCommandForOS(opts) {
|
|
720
664
|
const withBrewBootstrap = (cmd) => [
|
|
721
665
|
"",
|
|
722
|
-
|
|
723
|
-
|
|
666
|
+
cyan2(BREW_INSTALL_COMMAND),
|
|
667
|
+
cyan2(cmd),
|
|
724
668
|
"",
|
|
725
669
|
dim("(Skip the first line if you already have Homebrew.)")
|
|
726
670
|
].join("\n");
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
case "win32":
|
|
731
|
-
return ["", cyan(opts.winget)].join("\n");
|
|
732
|
-
default:
|
|
733
|
-
if (opts.linuxBrew) return withBrewBootstrap(opts.linuxBrew);
|
|
734
|
-
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
735
|
-
}
|
|
671
|
+
if (process.platform === "darwin") return withBrewBootstrap(opts.brew);
|
|
672
|
+
if (opts.linuxBrew) return withBrewBootstrap(opts.linuxBrew);
|
|
673
|
+
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
736
674
|
}
|
|
737
675
|
function providerSetupHint(host, provider) {
|
|
738
676
|
if (provider === "github") {
|
|
@@ -740,7 +678,6 @@ function providerSetupHint(host, provider) {
|
|
|
740
678
|
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
741
679
|
const install = installCommandForOS({
|
|
742
680
|
brew: "brew install gh",
|
|
743
|
-
winget: "winget install --id GitHub.cli",
|
|
744
681
|
linuxBrew: "brew install gh",
|
|
745
682
|
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
746
683
|
});
|
|
@@ -751,8 +688,8 @@ function providerSetupHint(host, provider) {
|
|
|
751
688
|
install,
|
|
752
689
|
"",
|
|
753
690
|
"Then run once:",
|
|
754
|
-
|
|
755
|
-
|
|
691
|
+
cyan2(`gh auth login${hostArg}`),
|
|
692
|
+
cyan2(`gh auth setup-git${hostArg}`),
|
|
756
693
|
"",
|
|
757
694
|
"`gh auth login` walks through OAuth in your browser.",
|
|
758
695
|
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
@@ -764,7 +701,6 @@ function providerSetupHint(host, provider) {
|
|
|
764
701
|
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
765
702
|
const install = installCommandForOS({
|
|
766
703
|
brew: "brew install glab",
|
|
767
|
-
winget: "winget install --id GLab.GLab",
|
|
768
704
|
linuxBrew: "brew install glab",
|
|
769
705
|
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
770
706
|
});
|
|
@@ -775,7 +711,7 @@ function providerSetupHint(host, provider) {
|
|
|
775
711
|
install,
|
|
776
712
|
"",
|
|
777
713
|
"Then run once:",
|
|
778
|
-
|
|
714
|
+
cyan2(`glab auth login${hostArg}`),
|
|
779
715
|
"",
|
|
780
716
|
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
781
717
|
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
@@ -794,7 +730,7 @@ function providerSetupHint(host, provider) {
|
|
|
794
730
|
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
795
731
|
"",
|
|
796
732
|
"Then store it via your OS credential helper:",
|
|
797
|
-
|
|
733
|
+
cyan2(
|
|
798
734
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
799
735
|
)
|
|
800
736
|
].join("\n")
|
|
@@ -810,7 +746,7 @@ function providerSetupHint(host, provider) {
|
|
|
810
746
|
"at least repo-read + repo-write scopes for the repos you need.",
|
|
811
747
|
"",
|
|
812
748
|
"Then store it via your OS credential helper:",
|
|
813
|
-
|
|
749
|
+
cyan2(
|
|
814
750
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
815
751
|
)
|
|
816
752
|
].join("\n")
|
|
@@ -828,7 +764,7 @@ function providerSetupHint(host, provider) {
|
|
|
828
764
|
"need push from the container).",
|
|
829
765
|
"",
|
|
830
766
|
"Then store it via your OS credential helper:",
|
|
831
|
-
|
|
767
|
+
cyan2(
|
|
832
768
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
833
769
|
)
|
|
834
770
|
].join("\n")
|
|
@@ -941,7 +877,7 @@ function formatMissingCredentialsError(missing) {
|
|
|
941
877
|
"",
|
|
942
878
|
hint.body,
|
|
943
879
|
"",
|
|
944
|
-
`Then re-run ${
|
|
880
|
+
`Then re-run ${cyan2("monoceros apply")}.`
|
|
945
881
|
].join("\n");
|
|
946
882
|
}
|
|
947
883
|
const lines = [
|
|
@@ -955,7 +891,7 @@ function formatMissingCredentialsError(missing) {
|
|
|
955
891
|
lines.push(hint.body);
|
|
956
892
|
lines.push("");
|
|
957
893
|
}
|
|
958
|
-
lines.push(`Then re-run ${
|
|
894
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
959
895
|
return lines.join("\n");
|
|
960
896
|
}
|
|
961
897
|
function formatUnknownProviderError(hosts) {
|
|
@@ -967,32 +903,18 @@ function formatUnknownProviderError(hosts) {
|
|
|
967
903
|
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
968
904
|
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
969
905
|
"",
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
906
|
+
cyan2(" repos:"),
|
|
907
|
+
cyan2(` - url: https://${sorted[0]}/\u2026`),
|
|
908
|
+
cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
973
909
|
"",
|
|
974
|
-
`Or re-add with ${
|
|
910
|
+
`Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
975
911
|
];
|
|
976
912
|
return lines.join("\n");
|
|
977
913
|
}
|
|
978
914
|
|
|
979
915
|
// 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
916
|
import { spawn as spawn2 } from "child_process";
|
|
990
|
-
|
|
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) => {
|
|
917
|
+
var realDockerLookup = (args) => {
|
|
996
918
|
return new Promise((resolve, reject) => {
|
|
997
919
|
const child = spawn2("docker", args, {
|
|
998
920
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1012,988 +934,638 @@ var defaultDockerExec = (args) => {
|
|
|
1012
934
|
);
|
|
1013
935
|
});
|
|
1014
936
|
};
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
"
|
|
1024
|
-
"--format",
|
|
1025
|
-
"{{.State.Running}}",
|
|
1026
|
-
PROXY_CONTAINER_NAME
|
|
937
|
+
async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
938
|
+
const docker = opts.docker ?? realDockerLookup;
|
|
939
|
+
const result = await docker([
|
|
940
|
+
"ps",
|
|
941
|
+
"-q",
|
|
942
|
+
"--filter",
|
|
943
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
944
|
+
"--filter",
|
|
945
|
+
"status=running"
|
|
1027
946
|
]);
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
947
|
+
if (result.exitCode !== 0) return null;
|
|
948
|
+
const ids = result.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
949
|
+
return ids[0] ?? null;
|
|
1031
950
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
951
|
+
var realContainerExec = (containerId, argv) => {
|
|
952
|
+
return new Promise((resolve, reject) => {
|
|
953
|
+
const child = spawn2("docker", ["exec", containerId, ...argv], {
|
|
954
|
+
// Inherit stdio so live git output reaches the user.
|
|
955
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
956
|
+
});
|
|
957
|
+
child.on("error", reject);
|
|
958
|
+
child.on("exit", (code) => resolve({ exitCode: code ?? 0 }));
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/config/global.ts
|
|
963
|
+
import { promises as fs3 } from "fs";
|
|
964
|
+
import { z as z2 } from "zod";
|
|
965
|
+
import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
|
|
966
|
+
var SCHEMA_VERSION = 1;
|
|
967
|
+
var MonocerosConfigSchema = z2.object({
|
|
968
|
+
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
969
|
+
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
970
|
+
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
971
|
+
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
972
|
+
// in that case; without .nullish() the schema would reject it and
|
|
973
|
+
// we'd be back to forcing builders to comment-juggle three lines.
|
|
974
|
+
defaults: z2.object({
|
|
975
|
+
// .nullish() (not just .optional()) so the sample yml can leave
|
|
976
|
+
// `git:` uncommented as a category marker — YAML produces
|
|
977
|
+
// `git: null` for an empty mapping, which zod's plain
|
|
978
|
+
// `.optional()` would reject.
|
|
979
|
+
git: z2.object({
|
|
980
|
+
user: GitUserSchema.optional()
|
|
981
|
+
}).nullish(),
|
|
982
|
+
// .nullish() for the same reason as `git` — the sample keeps
|
|
983
|
+
// `features:` uncommented as a category marker.
|
|
984
|
+
features: z2.record(
|
|
985
|
+
z2.string().regex(
|
|
986
|
+
REGEX.featureRef,
|
|
987
|
+
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
988
|
+
),
|
|
989
|
+
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
990
|
+
).nullish()
|
|
991
|
+
}).nullish(),
|
|
992
|
+
// Machine-global routing settings — one Traefik per builder, so
|
|
993
|
+
// host-port and similar live here rather than in any container yml.
|
|
994
|
+
// See ADR 0007.
|
|
995
|
+
routing: z2.object({
|
|
996
|
+
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
997
|
+
"Host port the Traefik singleton binds. Default 80. Set this when 80 is held by another service on your machine \u2014 URLs then become http://<name>.localhost:<port>/."
|
|
998
|
+
)
|
|
999
|
+
}).nullish()
|
|
1000
|
+
});
|
|
1001
|
+
async function readMonocerosConfig(opts = {}) {
|
|
1002
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
1003
|
+
const filePath = monocerosConfigPath(home);
|
|
1004
|
+
let text;
|
|
1005
|
+
try {
|
|
1006
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
1007
|
+
} catch {
|
|
1008
|
+
return void 0;
|
|
1060
1009
|
}
|
|
1061
|
-
const
|
|
1062
|
-
|
|
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) {
|
|
1010
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1011
|
+
if (doc.errors.length > 0) {
|
|
1084
1012
|
throw new Error(
|
|
1085
|
-
`
|
|
1013
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1086
1014
|
);
|
|
1087
1015
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (inspect.exitCode !== 0) {
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
1106
|
-
if (others.length > 0) return;
|
|
1107
|
-
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
1108
|
-
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
1109
|
-
if (netRm.exitCode !== 0) {
|
|
1110
|
-
logger?.warn?.(
|
|
1111
|
-
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
1016
|
+
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
1017
|
+
if (!result.success) {
|
|
1018
|
+
const issues = result.error.issues.map((issue) => {
|
|
1019
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1020
|
+
return ` - ${where}: ${issue.message}`;
|
|
1021
|
+
}).join("\n");
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`Invalid ${filePath}:
|
|
1024
|
+
${issues}
|
|
1025
|
+
|
|
1026
|
+
See ${filePath.replace(
|
|
1027
|
+
/\.yml$/,
|
|
1028
|
+
".sample.yml"
|
|
1029
|
+
)} for a valid example.`
|
|
1112
1030
|
);
|
|
1113
|
-
return;
|
|
1114
1031
|
}
|
|
1115
|
-
|
|
1116
|
-
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1117
|
-
);
|
|
1032
|
+
return result.data;
|
|
1118
1033
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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);
|
|
1034
|
+
var DEFAULT_PROXY_HOST_PORT = 80;
|
|
1035
|
+
function proxyHostPort(config) {
|
|
1036
|
+
return config?.routing?.hostPort ?? DEFAULT_PROXY_HOST_PORT;
|
|
1037
|
+
}
|
|
1038
|
+
async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
1039
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
1040
|
+
const filePath = monocerosConfigPath(home);
|
|
1041
|
+
let text;
|
|
1042
|
+
try {
|
|
1043
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
1044
|
+
} catch {
|
|
1045
|
+
text = void 0;
|
|
1141
1046
|
}
|
|
1142
|
-
|
|
1047
|
+
if (text === void 0) {
|
|
1048
|
+
const fresh = [
|
|
1049
|
+
"# Optional \u2014 global defaults for monoceros containers.",
|
|
1050
|
+
"",
|
|
1051
|
+
"schemaVersion: 1",
|
|
1052
|
+
"",
|
|
1053
|
+
"defaults:",
|
|
1054
|
+
" git:",
|
|
1055
|
+
" user:",
|
|
1056
|
+
` name: ${user.name}`,
|
|
1057
|
+
` email: ${user.email}`,
|
|
1058
|
+
""
|
|
1059
|
+
].join("\n");
|
|
1060
|
+
await fs3.mkdir(home, { recursive: true });
|
|
1061
|
+
await fs3.writeFile(filePath, fresh, "utf8");
|
|
1062
|
+
return { filePath, created: true, alreadySet: false };
|
|
1063
|
+
}
|
|
1064
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1065
|
+
if (doc.errors.length > 0) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const defaultsMap = ensureMap(doc, "defaults");
|
|
1071
|
+
const gitMap = ensureSubMapAtTop(defaultsMap, "git");
|
|
1072
|
+
const userMap = ensureSubMap(gitMap, "user");
|
|
1073
|
+
const existingName = userMap.get("name");
|
|
1074
|
+
const existingEmail = userMap.get("email");
|
|
1075
|
+
if (typeof existingName === "string" && existingName.length > 0 && typeof existingEmail === "string" && existingEmail.length > 0) {
|
|
1076
|
+
return { filePath, created: false, alreadySet: true };
|
|
1077
|
+
}
|
|
1078
|
+
relocateLeakedLeafComments(userMap, defaultsMap, "git");
|
|
1079
|
+
userMap.set("name", user.name);
|
|
1080
|
+
userMap.set("email", user.email);
|
|
1081
|
+
const newText = String(doc);
|
|
1082
|
+
await fs3.writeFile(filePath, newText, "utf8");
|
|
1083
|
+
return { filePath, created: false, alreadySet: false };
|
|
1143
1084
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
|
|
1085
|
+
function ensureMap(doc, key) {
|
|
1086
|
+
const node = doc.get(key, true);
|
|
1087
|
+
if (node && isMap(node)) return node;
|
|
1088
|
+
const m = new YAMLMap();
|
|
1089
|
+
doc.set(key, m);
|
|
1090
|
+
return m;
|
|
1147
1091
|
}
|
|
1148
|
-
function
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1092
|
+
function ensureSubMap(parent, key) {
|
|
1093
|
+
const node = parent.get(key, true);
|
|
1094
|
+
if (node && isMap(node)) return node;
|
|
1095
|
+
const m = new YAMLMap();
|
|
1096
|
+
parent.set(key, m);
|
|
1097
|
+
return m;
|
|
1098
|
+
}
|
|
1099
|
+
function ensureSubMapAtTop(parent, key) {
|
|
1100
|
+
const node = parent.get(key, true);
|
|
1101
|
+
if (node && isMap(node)) return node;
|
|
1102
|
+
const parentMaybe = parent;
|
|
1103
|
+
const newKey = new Scalar(key);
|
|
1104
|
+
if (parent.items.length > 0 && typeof parentMaybe.commentBefore === "string" && parentMaybe.commentBefore.length > 0) {
|
|
1105
|
+
const cleaned = stripCommentedKeySkeleton(parentMaybe.commentBefore, key);
|
|
1106
|
+
const blankMatch = cleaned.match(/\n[ \t]*\n/);
|
|
1107
|
+
let head;
|
|
1108
|
+
let tail;
|
|
1109
|
+
if (blankMatch && blankMatch.index !== void 0) {
|
|
1110
|
+
head = cleaned.slice(0, blankMatch.index);
|
|
1111
|
+
tail = cleaned.slice(blankMatch.index + blankMatch[0].length);
|
|
1112
|
+
} else {
|
|
1113
|
+
head = cleaned;
|
|
1114
|
+
tail = "";
|
|
1115
|
+
}
|
|
1116
|
+
if (head.length > 0) {
|
|
1117
|
+
newKey.commentBefore = head;
|
|
1118
|
+
if (parentMaybe.spaceBefore) newKey.spaceBefore = true;
|
|
1119
|
+
}
|
|
1120
|
+
if (tail.length > 0) {
|
|
1121
|
+
const firstKey = parent.items[0].key;
|
|
1122
|
+
if (firstKey && typeof firstKey === "object") {
|
|
1123
|
+
const existing = firstKey.commentBefore ?? "";
|
|
1124
|
+
firstKey.commentBefore = existing ? `${tail}
|
|
1125
|
+
${existing}` : tail;
|
|
1126
|
+
firstKey.spaceBefore = true;
|
|
1170
1127
|
}
|
|
1171
|
-
cb(null);
|
|
1172
1128
|
}
|
|
1173
|
-
|
|
1129
|
+
parentMaybe.commentBefore = null;
|
|
1130
|
+
parentMaybe.spaceBefore = false;
|
|
1131
|
+
}
|
|
1132
|
+
const m = new YAMLMap();
|
|
1133
|
+
const pair = new Pair(newKey, m);
|
|
1134
|
+
parent.items.unshift(pair);
|
|
1135
|
+
return m;
|
|
1174
1136
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
function createRuntimePullHintStream(state) {
|
|
1187
|
-
let buffer = "";
|
|
1188
|
-
const appendHintIfMarker = (block) => {
|
|
1189
|
-
if (state.hinted || !block.includes(RUNTIME_PULL_MARKER)) return block;
|
|
1190
|
-
state.hinted = true;
|
|
1191
|
-
return `${block}${dim(`(i) ${RUNTIME_PULL_HINT}`)}
|
|
1192
|
-
`;
|
|
1193
|
-
};
|
|
1194
|
-
return new Transform2({
|
|
1195
|
-
decodeStrings: true,
|
|
1196
|
-
transform(chunk, _enc, cb) {
|
|
1197
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1198
|
-
buffer += text;
|
|
1199
|
-
const lastNewline = buffer.lastIndexOf("\n");
|
|
1200
|
-
if (lastNewline === -1) {
|
|
1201
|
-
cb(null);
|
|
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;
|
|
1137
|
+
function stripCommentedKeySkeleton(commentBody, key) {
|
|
1138
|
+
const lines = commentBody.split("\n");
|
|
1139
|
+
const headRe = new RegExp(`^ ${escapeRegExp(key)}:\\s*$`);
|
|
1140
|
+
const out = [];
|
|
1141
|
+
let i = 0;
|
|
1142
|
+
while (i < lines.length) {
|
|
1143
|
+
const line = lines[i];
|
|
1144
|
+
if (headRe.test(line)) {
|
|
1145
|
+
i++;
|
|
1146
|
+
while (i < lines.length && /^ {2,}\S/.test(lines[i])) {
|
|
1147
|
+
i++;
|
|
1212
1148
|
}
|
|
1213
|
-
|
|
1214
|
-
buffer = "";
|
|
1215
|
-
cb(null, appendHintIfMarker(tail));
|
|
1149
|
+
continue;
|
|
1216
1150
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
// src/devcontainer/cli.ts
|
|
1221
|
-
var require_ = createRequire(import.meta.url);
|
|
1222
|
-
var cachedBinaryPath = null;
|
|
1223
|
-
function devcontainerCliPath() {
|
|
1224
|
-
if (cachedBinaryPath) return cachedBinaryPath;
|
|
1225
|
-
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
1226
|
-
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1227
|
-
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
1228
|
-
if (!binEntry) {
|
|
1229
|
-
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
1151
|
+
out.push(line);
|
|
1152
|
+
i++;
|
|
1230
1153
|
}
|
|
1231
|
-
|
|
1232
|
-
return cachedBinaryPath;
|
|
1154
|
+
return out.join("\n");
|
|
1233
1155
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
child2.on("exit", (code) => resolve(code ?? 0));
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
const child = spawn3(process.execPath, [binPath, ...args], {
|
|
1247
|
-
cwd,
|
|
1248
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1249
|
-
});
|
|
1250
|
-
if (options.quiet) {
|
|
1251
|
-
const stdoutChunks = [];
|
|
1252
|
-
const stderrChunks = [];
|
|
1253
|
-
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
1254
|
-
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
1255
|
-
child.on("error", reject);
|
|
1256
|
-
child.on("exit", (code) => {
|
|
1257
|
-
const exitCode = code ?? 0;
|
|
1258
|
-
if (exitCode !== 0) {
|
|
1259
|
-
process.stderr.write(
|
|
1260
|
-
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
1261
|
-
);
|
|
1262
|
-
process.stderr.write(
|
|
1263
|
-
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
resolve(exitCode);
|
|
1267
|
-
});
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
const pullHint = { hinted: false };
|
|
1271
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stdout);
|
|
1272
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stderr);
|
|
1273
|
-
child.on("error", reject);
|
|
1274
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
1156
|
+
function escapeRegExp(s) {
|
|
1157
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1158
|
+
}
|
|
1159
|
+
function relocateLeakedLeafComments(leafMap, parent, ancestorKey) {
|
|
1160
|
+
const items = parent.items;
|
|
1161
|
+
const ancestorIdx = items.findIndex((p) => {
|
|
1162
|
+
const k = p.key;
|
|
1163
|
+
const v = typeof k === "string" ? k : k?.value ?? null;
|
|
1164
|
+
return v === ancestorKey;
|
|
1275
1165
|
});
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1166
|
+
if (ancestorIdx < 0 || ancestorIdx + 1 >= items.length) return;
|
|
1167
|
+
const target = items[ancestorIdx + 1];
|
|
1168
|
+
for (const pair of leafMap.items) {
|
|
1169
|
+
const value = pair.value;
|
|
1170
|
+
if (!value || typeof value !== "object") continue;
|
|
1171
|
+
const leakedComment = value.comment;
|
|
1172
|
+
const leakedSpace = value.spaceBefore;
|
|
1173
|
+
if (!leakedComment && !leakedSpace) continue;
|
|
1174
|
+
if (leakedComment) {
|
|
1175
|
+
const targetKey = target.key;
|
|
1176
|
+
if (targetKey && typeof targetKey === "object") {
|
|
1177
|
+
const existing = targetKey.commentBefore ?? "";
|
|
1178
|
+
targetKey.commentBefore = existing ? `${leakedComment}
|
|
1179
|
+
${existing}` : leakedComment;
|
|
1180
|
+
if (leakedSpace) targetKey.spaceBefore = true;
|
|
1181
|
+
}
|
|
1182
|
+
value.comment = null;
|
|
1183
|
+
}
|
|
1184
|
+
if (leakedSpace) value.spaceBefore = false;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/init/components.ts
|
|
1189
|
+
import { existsSync as existsSync2, promises as fs4 } from "fs";
|
|
1190
|
+
import path3 from "path";
|
|
1191
|
+
import { z as z3 } from "zod";
|
|
1192
|
+
import { parse as parseYaml } from "yaml";
|
|
1193
|
+
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
1194
|
+
var FeatureContributionSchema = z3.object({
|
|
1195
|
+
ref: z3.string().regex(REGEX.featureRef),
|
|
1196
|
+
options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
|
|
1197
|
+
});
|
|
1198
|
+
var ComponentFileSchema = z3.object({
|
|
1199
|
+
displayName: z3.string().min(1),
|
|
1200
|
+
description: z3.string().min(1),
|
|
1201
|
+
category: CategorySchema,
|
|
1202
|
+
contributes: z3.object({
|
|
1203
|
+
languages: z3.array(z3.string().min(1)).optional(),
|
|
1204
|
+
services: z3.array(z3.string().min(1)).optional(),
|
|
1205
|
+
features: z3.array(FeatureContributionSchema).optional()
|
|
1206
|
+
})
|
|
1207
|
+
}).superRefine((data, ctx) => {
|
|
1208
|
+
const c = data.contributes;
|
|
1209
|
+
const filled = [
|
|
1210
|
+
c.languages && c.languages.length > 0 ? "languages" : null,
|
|
1211
|
+
c.services && c.services.length > 0 ? "services" : null,
|
|
1212
|
+
c.features && c.features.length > 0 ? "features" : null
|
|
1213
|
+
].filter((x) => x !== null);
|
|
1214
|
+
if (filled.length === 0) {
|
|
1215
|
+
ctx.addIssue({
|
|
1216
|
+
code: z3.ZodIssueCode.custom,
|
|
1217
|
+
message: "contributes must set at least one of languages/services/features"
|
|
1295
1218
|
});
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (filled.length > 1) {
|
|
1222
|
+
ctx.addIssue({
|
|
1223
|
+
code: z3.ZodIssueCode.custom,
|
|
1224
|
+
message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
|
|
1300
1225
|
});
|
|
1301
|
-
|
|
1302
|
-
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
|
|
1229
|
+
if (filled[0] !== expected) {
|
|
1230
|
+
ctx.addIssue({
|
|
1231
|
+
code: z3.ZodIssueCode.custom,
|
|
1232
|
+
message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
|
|
1303
1233
|
});
|
|
1304
|
-
child.on("error", reject);
|
|
1305
|
-
child.on(
|
|
1306
|
-
"exit",
|
|
1307
|
-
(code) => resolve({ exitCode: code ?? 0, stdout, stderr })
|
|
1308
|
-
);
|
|
1309
|
-
});
|
|
1310
|
-
};
|
|
1311
|
-
async function findContainerIds(filters, exec = spawnDocker) {
|
|
1312
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1313
|
-
for (const filter of filters) {
|
|
1314
|
-
const result = await exec(["ps", "-aq", "--filter", filter]);
|
|
1315
|
-
if (result.exitCode !== 0) continue;
|
|
1316
|
-
for (const line of result.stdout.split(/\r?\n/)) {
|
|
1317
|
-
const id = line.trim();
|
|
1318
|
-
if (id) ids.add(id);
|
|
1319
|
-
}
|
|
1320
1234
|
}
|
|
1321
|
-
|
|
1235
|
+
});
|
|
1236
|
+
async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
1237
|
+
if (!existsSync2(rootDir)) {
|
|
1238
|
+
return /* @__PURE__ */ new Map();
|
|
1239
|
+
}
|
|
1240
|
+
const out = /* @__PURE__ */ new Map();
|
|
1241
|
+
await walk(rootDir, rootDir, out);
|
|
1242
|
+
return out;
|
|
1322
1243
|
}
|
|
1323
|
-
async function
|
|
1324
|
-
const
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
opts.logger.info(`[${tag}] removing containers: ${ids.join(" ")}`);
|
|
1331
|
-
const rmResult = await exec(["rm", "-f", ...ids]);
|
|
1332
|
-
rmExit = rmResult.exitCode;
|
|
1333
|
-
if (rmExit !== 0 && rmResult.stderr.trim()) {
|
|
1334
|
-
opts.logger.info(`[${tag}] ${rmResult.stderr.trim()}`);
|
|
1244
|
+
async function walk(baseDir, currentDir, out) {
|
|
1245
|
+
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
1246
|
+
for (const entry2 of entries) {
|
|
1247
|
+
const full = path3.join(currentDir, entry2.name);
|
|
1248
|
+
if (entry2.isDirectory()) {
|
|
1249
|
+
await walk(baseDir, full, out);
|
|
1250
|
+
continue;
|
|
1335
1251
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1252
|
+
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
1253
|
+
const relative = path3.relative(baseDir, full);
|
|
1254
|
+
const name = relative.replace(/\.yml$/, "").split(path3.sep).join("/");
|
|
1255
|
+
const text = await fs4.readFile(full, "utf8");
|
|
1256
|
+
let raw;
|
|
1257
|
+
try {
|
|
1258
|
+
raw = parseYaml(text);
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Failed to parse component ${name} (${full}): ${err.message}`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const parsed = ComponentFileSchema.safeParse(raw);
|
|
1265
|
+
if (!parsed.success) {
|
|
1266
|
+
const issues = parsed.error.issues.map((issue) => {
|
|
1267
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1268
|
+
return ` - ${where}: ${issue.message}`;
|
|
1269
|
+
}).join("\n");
|
|
1270
|
+
throw new Error(`Invalid component ${name} (${full}):
|
|
1271
|
+
${issues}`);
|
|
1272
|
+
}
|
|
1273
|
+
out.set(name, { name, sourcePath: full, file: parsed.data });
|
|
1338
1274
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1275
|
+
}
|
|
1276
|
+
function mergeComponents(resolved) {
|
|
1277
|
+
const languages = [];
|
|
1278
|
+
const services = [];
|
|
1279
|
+
const featureByRef = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const entry2 of resolved) {
|
|
1281
|
+
const c = isResolvedComponent(entry2) ? entry2.component : entry2;
|
|
1282
|
+
const version = isResolvedComponent(entry2) ? entry2.version : void 0;
|
|
1283
|
+
const ct = c.file.contributes;
|
|
1284
|
+
for (const lang of ct.languages ?? []) {
|
|
1285
|
+
const value = version !== void 0 ? `${lang}:${version}` : lang;
|
|
1286
|
+
if (!languages.includes(value)) languages.push(value);
|
|
1287
|
+
}
|
|
1288
|
+
for (const svc of ct.services ?? []) {
|
|
1289
|
+
if (!services.includes(svc)) services.push(svc);
|
|
1290
|
+
}
|
|
1291
|
+
for (const f of ct.features ?? []) {
|
|
1292
|
+
const existing = featureByRef.get(f.ref);
|
|
1293
|
+
if (!existing) {
|
|
1294
|
+
featureByRef.set(f.ref, {
|
|
1295
|
+
ref: f.ref,
|
|
1296
|
+
options: { ...f.options ?? {} }
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
|
|
1343
1301
|
}
|
|
1344
1302
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1303
|
+
return {
|
|
1304
|
+
languages,
|
|
1305
|
+
services,
|
|
1306
|
+
features: [...featureByRef.values()]
|
|
1307
|
+
};
|
|
1347
1308
|
}
|
|
1348
|
-
function
|
|
1349
|
-
|
|
1350
|
-
return p.replace(
|
|
1351
|
-
/^([A-Z]):/,
|
|
1352
|
-
(_, drive) => `${drive.toLowerCase()}:`
|
|
1353
|
-
);
|
|
1309
|
+
function isResolvedComponent(x) {
|
|
1310
|
+
return "component" in x;
|
|
1354
1311
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1312
|
+
function mergeFeatureOptions(a, b) {
|
|
1313
|
+
const result = { ...a };
|
|
1314
|
+
for (const [key, valueB] of Object.entries(b)) {
|
|
1315
|
+
const valueA = result[key];
|
|
1316
|
+
if (typeof valueA === "boolean" && typeof valueB === "boolean") {
|
|
1317
|
+
result[key] = valueA || valueB;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
result[key] = valueB;
|
|
1321
|
+
}
|
|
1322
|
+
return result;
|
|
1357
1323
|
}
|
|
1358
|
-
function
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
);
|
|
1324
|
+
function resolveComponents(catalog, names) {
|
|
1325
|
+
const unknown = [];
|
|
1326
|
+
const out = [];
|
|
1327
|
+
for (const raw of names) {
|
|
1328
|
+
const colon = raw.indexOf(":");
|
|
1329
|
+
const name = colon === -1 ? raw : raw.slice(0, colon);
|
|
1330
|
+
const version = colon === -1 ? void 0 : raw.slice(colon + 1);
|
|
1331
|
+
const c = catalog.get(name);
|
|
1332
|
+
if (!c) {
|
|
1333
|
+
unknown.push(raw);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (version !== void 0 && c.file.category !== "language") {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
out.push({ component: c, ...version !== void 0 ? { version } : {} });
|
|
1363
1342
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1343
|
+
if (unknown.length > 0) {
|
|
1344
|
+
const available = [...catalog.keys()].sort();
|
|
1366
1345
|
throw new Error(
|
|
1367
|
-
`
|
|
1346
|
+
`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
1347
|
+
Available: ${available.join(", ")}.`
|
|
1368
1348
|
);
|
|
1369
1349
|
}
|
|
1370
|
-
return
|
|
1350
|
+
return out;
|
|
1371
1351
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1352
|
+
|
|
1353
|
+
// src/proxy/index.ts
|
|
1354
|
+
import { spawn as spawn3 } from "child_process";
|
|
1355
|
+
import { promises as fs5 } from "fs";
|
|
1356
|
+
import path4 from "path";
|
|
1357
|
+
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
1358
|
+
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
1359
|
+
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
1360
|
+
var defaultDockerExec = (args) => {
|
|
1361
|
+
return new Promise((resolve, reject) => {
|
|
1362
|
+
const child = spawn3("docker", args, {
|
|
1363
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1364
|
+
});
|
|
1365
|
+
let stdout = "";
|
|
1366
|
+
let stderr = "";
|
|
1367
|
+
child.stdout.on("data", (chunk) => {
|
|
1368
|
+
stdout += chunk.toString();
|
|
1369
|
+
});
|
|
1370
|
+
child.stderr.on("data", (chunk) => {
|
|
1371
|
+
stderr += chunk.toString();
|
|
1372
|
+
});
|
|
1373
|
+
child.on("error", reject);
|
|
1374
|
+
child.on(
|
|
1375
|
+
"exit",
|
|
1376
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1377
|
+
);
|
|
1378
|
+
});
|
|
1379
|
+
};
|
|
1380
|
+
var realDocker = defaultDockerExec;
|
|
1381
|
+
function proxyDynamicDir(home) {
|
|
1382
|
+
return path4.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
1377
1383
|
}
|
|
1378
|
-
async function
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
["
|
|
1385
|
-
|
|
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\`.`
|
|
1384
|
+
async function ensureProxy(opts = {}) {
|
|
1385
|
+
const docker = opts.docker ?? realDocker;
|
|
1386
|
+
const dyn = proxyDynamicDir(opts.monocerosHome);
|
|
1387
|
+
await fs5.mkdir(dyn, { recursive: true });
|
|
1388
|
+
const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
|
|
1389
|
+
if (netInspect.exitCode !== 0) {
|
|
1390
|
+
const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
|
|
1391
|
+
if (create.exitCode !== 0) {
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
`Could not create docker network ${PROXY_NETWORK_NAME}: ${create.stderr.trim() || `exit ${create.exitCode}`}`
|
|
1417
1394
|
);
|
|
1418
|
-
return 1;
|
|
1419
1395
|
}
|
|
1420
|
-
return runStart({
|
|
1421
|
-
root,
|
|
1422
|
-
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
1423
|
-
logger
|
|
1424
|
-
});
|
|
1425
1396
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1397
|
+
const state = await docker([
|
|
1398
|
+
"inspect",
|
|
1399
|
+
"--format",
|
|
1400
|
+
"{{.State.Running}}",
|
|
1401
|
+
PROXY_CONTAINER_NAME
|
|
1402
|
+
]);
|
|
1403
|
+
if (state.exitCode === 0) {
|
|
1404
|
+
if (state.stdout.trim() === "true") return;
|
|
1405
|
+
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
1406
|
+
if (start.exitCode !== 0) {
|
|
1407
|
+
throw new Error(
|
|
1408
|
+
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const hostPort = opts.hostPort ?? 80;
|
|
1414
|
+
const run = await docker([
|
|
1415
|
+
"run",
|
|
1416
|
+
"-d",
|
|
1417
|
+
"--name",
|
|
1418
|
+
PROXY_CONTAINER_NAME,
|
|
1419
|
+
"--network",
|
|
1420
|
+
PROXY_NETWORK_NAME,
|
|
1421
|
+
"-p",
|
|
1422
|
+
`${hostPort}:80`,
|
|
1423
|
+
"-v",
|
|
1424
|
+
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
1425
|
+
"--label",
|
|
1426
|
+
"monoceros.role=proxy",
|
|
1427
|
+
TRAEFIK_IMAGE,
|
|
1428
|
+
"--entrypoints.web.address=:80",
|
|
1429
|
+
"--providers.file.directory=/etc/traefik/dynamic",
|
|
1430
|
+
"--providers.file.watch=true",
|
|
1431
|
+
"--providers.docker=false",
|
|
1432
|
+
"--api.dashboard=false",
|
|
1433
|
+
"--log.level=INFO"
|
|
1434
|
+
]);
|
|
1435
|
+
if (run.exitCode !== 0) {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
opts.logger?.info(
|
|
1441
|
+
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
1437
1442
|
);
|
|
1438
1443
|
}
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1444
|
+
async function maybeStopProxy(opts = {}) {
|
|
1445
|
+
const docker = opts.docker ?? realDocker;
|
|
1446
|
+
const logger = opts.logger;
|
|
1447
|
+
const inspect = await docker([
|
|
1448
|
+
"network",
|
|
1449
|
+
"inspect",
|
|
1450
|
+
PROXY_NETWORK_NAME,
|
|
1451
|
+
"--format",
|
|
1452
|
+
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
1453
|
+
]);
|
|
1454
|
+
if (inspect.exitCode !== 0) {
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
1458
|
+
if (others.length > 0) return;
|
|
1459
|
+
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
1460
|
+
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
1461
|
+
if (netRm.exitCode !== 0) {
|
|
1462
|
+
logger?.warn?.(
|
|
1463
|
+
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
1464
|
+
);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
logger?.info(
|
|
1468
|
+
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1443
1469
|
);
|
|
1444
1470
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1471
|
+
|
|
1472
|
+
// src/proxy/dynamic.ts
|
|
1473
|
+
import { promises as fs6 } from "fs";
|
|
1474
|
+
import path5 from "path";
|
|
1475
|
+
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
1476
|
+
if (ports.length === 0) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`writeDynamicConfig requires at least one port. For empty port lists, call removeDynamicConfig(${JSON.stringify(name)}).`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
1482
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1483
|
+
const file = path5.join(dir, `${name}.yml`);
|
|
1484
|
+
await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
1485
|
+
return file;
|
|
1450
1486
|
}
|
|
1451
|
-
function
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1487
|
+
async function removeDynamicConfig(name, opts = {}) {
|
|
1488
|
+
const file = path5.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1489
|
+
await fs6.rm(file, { force: true });
|
|
1490
|
+
}
|
|
1491
|
+
function renderDynamicConfig(name, ports) {
|
|
1492
|
+
const lines = [];
|
|
1493
|
+
lines.push("# Generated by Monoceros \u2014 do not edit by hand.");
|
|
1494
|
+
lines.push(`# Container: ${name}`);
|
|
1495
|
+
lines.push(`# Ports: ${ports.join(", ")}`);
|
|
1496
|
+
lines.push("# Traefik file-provider re-reads this on change (~100 ms);");
|
|
1497
|
+
lines.push(
|
|
1498
|
+
"# to change routing, edit container-configs/" + name + ".yml or use"
|
|
1460
1499
|
);
|
|
1500
|
+
lines.push("# `monoceros add-port` / `monoceros remove-port`.");
|
|
1501
|
+
lines.push("http:");
|
|
1502
|
+
lines.push(" routers:");
|
|
1503
|
+
ports.forEach((port, idx) => {
|
|
1504
|
+
const router = `${name}-${port}`;
|
|
1505
|
+
const hostExplicit = `${name}-${port}.localhost`;
|
|
1506
|
+
const rule = idx === 0 ? `"Host(\`${name}.localhost\`) || Host(\`${hostExplicit}\`)"` : `"Host(\`${hostExplicit}\`)"`;
|
|
1507
|
+
lines.push(` ${router}:`);
|
|
1508
|
+
lines.push(` rule: ${rule}`);
|
|
1509
|
+
lines.push(` service: ${router}`);
|
|
1510
|
+
lines.push(" entryPoints:");
|
|
1511
|
+
lines.push(" - web");
|
|
1512
|
+
});
|
|
1513
|
+
lines.push(" services:");
|
|
1514
|
+
for (const port of ports) {
|
|
1515
|
+
const svc = `${name}-${port}`;
|
|
1516
|
+
lines.push(` ${svc}:`);
|
|
1517
|
+
lines.push(" loadBalancer:");
|
|
1518
|
+
lines.push(" servers:");
|
|
1519
|
+
lines.push(` - url: "http://${name}:${port}"`);
|
|
1520
|
+
}
|
|
1521
|
+
return lines.join("\n") + "\n";
|
|
1522
|
+
}
|
|
1523
|
+
function proxyUrlsFor(name, ports, hostPort = 80) {
|
|
1524
|
+
const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
|
|
1525
|
+
return ports.map((port, idx) => ({
|
|
1526
|
+
port,
|
|
1527
|
+
url: `http://${name}-${port}.localhost${portSuffix}`,
|
|
1528
|
+
isDefault: idx === 0
|
|
1529
|
+
}));
|
|
1461
1530
|
}
|
|
1462
1531
|
|
|
1463
|
-
// src/
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1532
|
+
// src/proxy/port-check.ts
|
|
1533
|
+
import { Socket } from "net";
|
|
1534
|
+
var CONNECT_TIMEOUT_MS = 750;
|
|
1535
|
+
var realPortProbe = (port) => {
|
|
1536
|
+
return new Promise((resolve) => {
|
|
1537
|
+
const socket = new Socket();
|
|
1538
|
+
let settled = false;
|
|
1539
|
+
const settle = (result) => {
|
|
1540
|
+
if (settled) return;
|
|
1541
|
+
settled = true;
|
|
1542
|
+
socket.destroy();
|
|
1543
|
+
resolve(result);
|
|
1544
|
+
};
|
|
1545
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS);
|
|
1546
|
+
socket.once("connect", () => {
|
|
1547
|
+
settle({
|
|
1548
|
+
ok: false,
|
|
1549
|
+
code: "EADDRINUSE",
|
|
1550
|
+
message: `another process is listening on ${port}`
|
|
1551
|
+
});
|
|
1468
1552
|
});
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
child.stdout.on("data", (chunk) => {
|
|
1472
|
-
stdout += chunk.toString();
|
|
1553
|
+
socket.once("timeout", () => {
|
|
1554
|
+
settle({ ok: true });
|
|
1473
1555
|
});
|
|
1474
|
-
|
|
1475
|
-
|
|
1556
|
+
socket.once("error", (err) => {
|
|
1557
|
+
const code = err.code ?? "UNKNOWN";
|
|
1558
|
+
if (code === "ECONNREFUSED") {
|
|
1559
|
+
settle({ ok: true });
|
|
1560
|
+
} else {
|
|
1561
|
+
settle({
|
|
1562
|
+
ok: false,
|
|
1563
|
+
code,
|
|
1564
|
+
message: err.message
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1476
1567
|
});
|
|
1477
|
-
|
|
1478
|
-
child.on(
|
|
1479
|
-
"exit",
|
|
1480
|
-
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1481
|
-
);
|
|
1482
|
-
});
|
|
1483
|
-
};
|
|
1484
|
-
async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
1485
|
-
const docker = opts.docker ?? realDockerLookup;
|
|
1486
|
-
const result = await docker([
|
|
1487
|
-
"ps",
|
|
1488
|
-
"-q",
|
|
1489
|
-
"--filter",
|
|
1490
|
-
`label=devcontainer.local_folder=${dockerLocalFolderLabel(containerPath)}`,
|
|
1491
|
-
"--filter",
|
|
1492
|
-
"status=running"
|
|
1493
|
-
]);
|
|
1494
|
-
if (result.exitCode !== 0) return null;
|
|
1495
|
-
const ids = result.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1496
|
-
return ids[0] ?? null;
|
|
1497
|
-
}
|
|
1498
|
-
var realContainerExec = (containerId, argv) => {
|
|
1499
|
-
return new Promise((resolve, reject) => {
|
|
1500
|
-
const child = spawn5("docker", ["exec", containerId, ...argv], {
|
|
1501
|
-
// Inherit stdio so live git output reaches the user.
|
|
1502
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
1503
|
-
});
|
|
1504
|
-
child.on("error", reject);
|
|
1505
|
-
child.on("exit", (code) => resolve({ exitCode: code ?? 0 }));
|
|
1506
|
-
});
|
|
1507
|
-
};
|
|
1508
|
-
|
|
1509
|
-
// src/config/global.ts
|
|
1510
|
-
import { promises as fs4 } from "fs";
|
|
1511
|
-
import { z as z2 } from "zod";
|
|
1512
|
-
import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
|
|
1513
|
-
var SCHEMA_VERSION = 1;
|
|
1514
|
-
var MonocerosConfigSchema = z2.object({
|
|
1515
|
-
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
1516
|
-
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
1517
|
-
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
1518
|
-
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
1519
|
-
// in that case; without .nullish() the schema would reject it and
|
|
1520
|
-
// we'd be back to forcing builders to comment-juggle three lines.
|
|
1521
|
-
defaults: z2.object({
|
|
1522
|
-
// .nullish() (not just .optional()) so the sample yml can leave
|
|
1523
|
-
// `git:` uncommented as a category marker — YAML produces
|
|
1524
|
-
// `git: null` for an empty mapping, which zod's plain
|
|
1525
|
-
// `.optional()` would reject.
|
|
1526
|
-
git: z2.object({
|
|
1527
|
-
user: GitUserSchema.optional()
|
|
1528
|
-
}).nullish(),
|
|
1529
|
-
// .nullish() for the same reason as `git` — the sample keeps
|
|
1530
|
-
// `features:` uncommented as a category marker.
|
|
1531
|
-
features: z2.record(
|
|
1532
|
-
z2.string().regex(
|
|
1533
|
-
REGEX.featureRef,
|
|
1534
|
-
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
1535
|
-
),
|
|
1536
|
-
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
1537
|
-
).nullish()
|
|
1538
|
-
}).nullish(),
|
|
1539
|
-
// Machine-global routing settings — one Traefik per builder, so
|
|
1540
|
-
// host-port and similar live here rather than in any container yml.
|
|
1541
|
-
// See ADR 0007.
|
|
1542
|
-
routing: z2.object({
|
|
1543
|
-
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
1544
|
-
"Host port the Traefik singleton binds. Default 80. Set this when 80 is held by another service on your machine \u2014 URLs then become http://<name>.localhost:<port>/."
|
|
1545
|
-
)
|
|
1546
|
-
}).nullish()
|
|
1547
|
-
});
|
|
1548
|
-
async function readMonocerosConfig(opts = {}) {
|
|
1549
|
-
const home = opts.monocerosHome ?? monocerosHome();
|
|
1550
|
-
const filePath = monocerosConfigPath(home);
|
|
1551
|
-
let text;
|
|
1552
|
-
try {
|
|
1553
|
-
text = await fs4.readFile(filePath, "utf8");
|
|
1554
|
-
} catch {
|
|
1555
|
-
return void 0;
|
|
1556
|
-
}
|
|
1557
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1558
|
-
if (doc.errors.length > 0) {
|
|
1559
|
-
throw new Error(
|
|
1560
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1561
|
-
);
|
|
1562
|
-
}
|
|
1563
|
-
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
1564
|
-
if (!result.success) {
|
|
1565
|
-
const issues = result.error.issues.map((issue) => {
|
|
1566
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1567
|
-
return ` - ${where}: ${issue.message}`;
|
|
1568
|
-
}).join("\n");
|
|
1569
|
-
throw new Error(
|
|
1570
|
-
`Invalid ${filePath}:
|
|
1571
|
-
${issues}
|
|
1572
|
-
|
|
1573
|
-
See ${filePath.replace(
|
|
1574
|
-
/\.yml$/,
|
|
1575
|
-
".sample.yml"
|
|
1576
|
-
)} for a valid example.`
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
return result.data;
|
|
1580
|
-
}
|
|
1581
|
-
var DEFAULT_PROXY_HOST_PORT = 80;
|
|
1582
|
-
function proxyHostPort(config) {
|
|
1583
|
-
return config?.routing?.hostPort ?? DEFAULT_PROXY_HOST_PORT;
|
|
1584
|
-
}
|
|
1585
|
-
async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
1586
|
-
const home = opts.monocerosHome ?? monocerosHome();
|
|
1587
|
-
const filePath = monocerosConfigPath(home);
|
|
1588
|
-
let text;
|
|
1589
|
-
try {
|
|
1590
|
-
text = await fs4.readFile(filePath, "utf8");
|
|
1591
|
-
} catch {
|
|
1592
|
-
text = void 0;
|
|
1593
|
-
}
|
|
1594
|
-
if (text === void 0) {
|
|
1595
|
-
const fresh = [
|
|
1596
|
-
"# Optional \u2014 global defaults for monoceros containers.",
|
|
1597
|
-
"",
|
|
1598
|
-
"schemaVersion: 1",
|
|
1599
|
-
"",
|
|
1600
|
-
"defaults:",
|
|
1601
|
-
" git:",
|
|
1602
|
-
" user:",
|
|
1603
|
-
` name: ${user.name}`,
|
|
1604
|
-
` email: ${user.email}`,
|
|
1605
|
-
""
|
|
1606
|
-
].join("\n");
|
|
1607
|
-
await fs4.mkdir(home, { recursive: true });
|
|
1608
|
-
await fs4.writeFile(filePath, fresh, "utf8");
|
|
1609
|
-
return { filePath, created: true, alreadySet: false };
|
|
1610
|
-
}
|
|
1611
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1612
|
-
if (doc.errors.length > 0) {
|
|
1613
|
-
throw new Error(
|
|
1614
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1615
|
-
);
|
|
1616
|
-
}
|
|
1617
|
-
const defaultsMap = ensureMap(doc, "defaults");
|
|
1618
|
-
const gitMap = ensureSubMapAtTop(defaultsMap, "git");
|
|
1619
|
-
const userMap = ensureSubMap(gitMap, "user");
|
|
1620
|
-
const existingName = userMap.get("name");
|
|
1621
|
-
const existingEmail = userMap.get("email");
|
|
1622
|
-
if (typeof existingName === "string" && existingName.length > 0 && typeof existingEmail === "string" && existingEmail.length > 0) {
|
|
1623
|
-
return { filePath, created: false, alreadySet: true };
|
|
1624
|
-
}
|
|
1625
|
-
relocateLeakedLeafComments(userMap, defaultsMap, "git");
|
|
1626
|
-
userMap.set("name", user.name);
|
|
1627
|
-
userMap.set("email", user.email);
|
|
1628
|
-
const newText = String(doc);
|
|
1629
|
-
await fs4.writeFile(filePath, newText, "utf8");
|
|
1630
|
-
return { filePath, created: false, alreadySet: false };
|
|
1631
|
-
}
|
|
1632
|
-
function ensureMap(doc, key) {
|
|
1633
|
-
const node = doc.get(key, true);
|
|
1634
|
-
if (node && isMap(node)) return node;
|
|
1635
|
-
const m = new YAMLMap();
|
|
1636
|
-
doc.set(key, m);
|
|
1637
|
-
return m;
|
|
1638
|
-
}
|
|
1639
|
-
function ensureSubMap(parent, key) {
|
|
1640
|
-
const node = parent.get(key, true);
|
|
1641
|
-
if (node && isMap(node)) return node;
|
|
1642
|
-
const m = new YAMLMap();
|
|
1643
|
-
parent.set(key, m);
|
|
1644
|
-
return m;
|
|
1645
|
-
}
|
|
1646
|
-
function ensureSubMapAtTop(parent, key) {
|
|
1647
|
-
const node = parent.get(key, true);
|
|
1648
|
-
if (node && isMap(node)) return node;
|
|
1649
|
-
const parentMaybe = parent;
|
|
1650
|
-
const newKey = new Scalar(key);
|
|
1651
|
-
if (parent.items.length > 0 && typeof parentMaybe.commentBefore === "string" && parentMaybe.commentBefore.length > 0) {
|
|
1652
|
-
const cleaned = stripCommentedKeySkeleton(parentMaybe.commentBefore, key);
|
|
1653
|
-
const blankMatch = cleaned.match(/\n[ \t]*\n/);
|
|
1654
|
-
let head;
|
|
1655
|
-
let tail;
|
|
1656
|
-
if (blankMatch && blankMatch.index !== void 0) {
|
|
1657
|
-
head = cleaned.slice(0, blankMatch.index);
|
|
1658
|
-
tail = cleaned.slice(blankMatch.index + blankMatch[0].length);
|
|
1659
|
-
} else {
|
|
1660
|
-
head = cleaned;
|
|
1661
|
-
tail = "";
|
|
1662
|
-
}
|
|
1663
|
-
if (head.length > 0) {
|
|
1664
|
-
newKey.commentBefore = head;
|
|
1665
|
-
if (parentMaybe.spaceBefore) newKey.spaceBefore = true;
|
|
1666
|
-
}
|
|
1667
|
-
if (tail.length > 0) {
|
|
1668
|
-
const firstKey = parent.items[0].key;
|
|
1669
|
-
if (firstKey && typeof firstKey === "object") {
|
|
1670
|
-
const existing = firstKey.commentBefore ?? "";
|
|
1671
|
-
firstKey.commentBefore = existing ? `${tail}
|
|
1672
|
-
${existing}` : tail;
|
|
1673
|
-
firstKey.spaceBefore = true;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
parentMaybe.commentBefore = null;
|
|
1677
|
-
parentMaybe.spaceBefore = false;
|
|
1678
|
-
}
|
|
1679
|
-
const m = new YAMLMap();
|
|
1680
|
-
const pair = new Pair(newKey, m);
|
|
1681
|
-
parent.items.unshift(pair);
|
|
1682
|
-
return m;
|
|
1683
|
-
}
|
|
1684
|
-
function stripCommentedKeySkeleton(commentBody, key) {
|
|
1685
|
-
const lines = commentBody.split("\n");
|
|
1686
|
-
const headRe = new RegExp(`^ ${escapeRegExp(key)}:\\s*$`);
|
|
1687
|
-
const out = [];
|
|
1688
|
-
let i = 0;
|
|
1689
|
-
while (i < lines.length) {
|
|
1690
|
-
const line = lines[i];
|
|
1691
|
-
if (headRe.test(line)) {
|
|
1692
|
-
i++;
|
|
1693
|
-
while (i < lines.length && /^ {2,}\S/.test(lines[i])) {
|
|
1694
|
-
i++;
|
|
1695
|
-
}
|
|
1696
|
-
continue;
|
|
1697
|
-
}
|
|
1698
|
-
out.push(line);
|
|
1699
|
-
i++;
|
|
1700
|
-
}
|
|
1701
|
-
return out.join("\n");
|
|
1702
|
-
}
|
|
1703
|
-
function escapeRegExp(s) {
|
|
1704
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1705
|
-
}
|
|
1706
|
-
function relocateLeakedLeafComments(leafMap, parent, ancestorKey) {
|
|
1707
|
-
const items = parent.items;
|
|
1708
|
-
const ancestorIdx = items.findIndex((p) => {
|
|
1709
|
-
const k = p.key;
|
|
1710
|
-
const v = typeof k === "string" ? k : k?.value ?? null;
|
|
1711
|
-
return v === ancestorKey;
|
|
1712
|
-
});
|
|
1713
|
-
if (ancestorIdx < 0 || ancestorIdx + 1 >= items.length) return;
|
|
1714
|
-
const target = items[ancestorIdx + 1];
|
|
1715
|
-
for (const pair of leafMap.items) {
|
|
1716
|
-
const value = pair.value;
|
|
1717
|
-
if (!value || typeof value !== "object") continue;
|
|
1718
|
-
const leakedComment = value.comment;
|
|
1719
|
-
const leakedSpace = value.spaceBefore;
|
|
1720
|
-
if (!leakedComment && !leakedSpace) continue;
|
|
1721
|
-
if (leakedComment) {
|
|
1722
|
-
const targetKey = target.key;
|
|
1723
|
-
if (targetKey && typeof targetKey === "object") {
|
|
1724
|
-
const existing = targetKey.commentBefore ?? "";
|
|
1725
|
-
targetKey.commentBefore = existing ? `${leakedComment}
|
|
1726
|
-
${existing}` : leakedComment;
|
|
1727
|
-
if (leakedSpace) targetKey.spaceBefore = true;
|
|
1728
|
-
}
|
|
1729
|
-
value.comment = null;
|
|
1730
|
-
}
|
|
1731
|
-
if (leakedSpace) value.spaceBefore = false;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
// src/init/components.ts
|
|
1736
|
-
import { existsSync as existsSync3, promises as fs5 } from "fs";
|
|
1737
|
-
import path6 from "path";
|
|
1738
|
-
import { z as z3 } from "zod";
|
|
1739
|
-
import { parse as parseYaml } from "yaml";
|
|
1740
|
-
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
1741
|
-
var FeatureContributionSchema = z3.object({
|
|
1742
|
-
ref: z3.string().regex(REGEX.featureRef),
|
|
1743
|
-
options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
|
|
1744
|
-
});
|
|
1745
|
-
var ComponentFileSchema = z3.object({
|
|
1746
|
-
displayName: z3.string().min(1),
|
|
1747
|
-
description: z3.string().min(1),
|
|
1748
|
-
category: CategorySchema,
|
|
1749
|
-
contributes: z3.object({
|
|
1750
|
-
languages: z3.array(z3.string().min(1)).optional(),
|
|
1751
|
-
services: z3.array(z3.string().min(1)).optional(),
|
|
1752
|
-
features: z3.array(FeatureContributionSchema).optional()
|
|
1753
|
-
})
|
|
1754
|
-
}).superRefine((data, ctx) => {
|
|
1755
|
-
const c = data.contributes;
|
|
1756
|
-
const filled = [
|
|
1757
|
-
c.languages && c.languages.length > 0 ? "languages" : null,
|
|
1758
|
-
c.services && c.services.length > 0 ? "services" : null,
|
|
1759
|
-
c.features && c.features.length > 0 ? "features" : null
|
|
1760
|
-
].filter((x) => x !== null);
|
|
1761
|
-
if (filled.length === 0) {
|
|
1762
|
-
ctx.addIssue({
|
|
1763
|
-
code: z3.ZodIssueCode.custom,
|
|
1764
|
-
message: "contributes must set at least one of languages/services/features"
|
|
1765
|
-
});
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
if (filled.length > 1) {
|
|
1769
|
-
ctx.addIssue({
|
|
1770
|
-
code: z3.ZodIssueCode.custom,
|
|
1771
|
-
message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
|
|
1772
|
-
});
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
|
|
1776
|
-
if (filled[0] !== expected) {
|
|
1777
|
-
ctx.addIssue({
|
|
1778
|
-
code: z3.ZodIssueCode.custom,
|
|
1779
|
-
message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
|
|
1780
|
-
});
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
1783
|
-
async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
1784
|
-
if (!existsSync3(rootDir)) {
|
|
1785
|
-
return /* @__PURE__ */ new Map();
|
|
1786
|
-
}
|
|
1787
|
-
const out = /* @__PURE__ */ new Map();
|
|
1788
|
-
await walk(rootDir, rootDir, out);
|
|
1789
|
-
return out;
|
|
1790
|
-
}
|
|
1791
|
-
async function walk(baseDir, currentDir, out) {
|
|
1792
|
-
const entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
1793
|
-
for (const entry2 of entries) {
|
|
1794
|
-
const full = path6.join(currentDir, entry2.name);
|
|
1795
|
-
if (entry2.isDirectory()) {
|
|
1796
|
-
await walk(baseDir, full, out);
|
|
1797
|
-
continue;
|
|
1798
|
-
}
|
|
1799
|
-
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
1800
|
-
const relative = path6.relative(baseDir, full);
|
|
1801
|
-
const name = relative.replace(/\.yml$/, "").split(path6.sep).join("/");
|
|
1802
|
-
const text = await fs5.readFile(full, "utf8");
|
|
1803
|
-
let raw;
|
|
1804
|
-
try {
|
|
1805
|
-
raw = parseYaml(text);
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
throw new Error(
|
|
1808
|
-
`Failed to parse component ${name} (${full}): ${err.message}`
|
|
1809
|
-
);
|
|
1810
|
-
}
|
|
1811
|
-
const parsed = ComponentFileSchema.safeParse(raw);
|
|
1812
|
-
if (!parsed.success) {
|
|
1813
|
-
const issues = parsed.error.issues.map((issue) => {
|
|
1814
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1815
|
-
return ` - ${where}: ${issue.message}`;
|
|
1816
|
-
}).join("\n");
|
|
1817
|
-
throw new Error(`Invalid component ${name} (${full}):
|
|
1818
|
-
${issues}`);
|
|
1819
|
-
}
|
|
1820
|
-
out.set(name, { name, sourcePath: full, file: parsed.data });
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
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
|
-
function mergeFeatureOptions(a, b) {
|
|
1860
|
-
const result = { ...a };
|
|
1861
|
-
for (const [key, valueB] of Object.entries(b)) {
|
|
1862
|
-
const valueA = result[key];
|
|
1863
|
-
if (typeof valueA === "boolean" && typeof valueB === "boolean") {
|
|
1864
|
-
result[key] = valueA || valueB;
|
|
1865
|
-
continue;
|
|
1866
|
-
}
|
|
1867
|
-
result[key] = valueB;
|
|
1868
|
-
}
|
|
1869
|
-
return result;
|
|
1870
|
-
}
|
|
1871
|
-
function resolveComponents(catalog, names) {
|
|
1872
|
-
const unknown = [];
|
|
1873
|
-
const out = [];
|
|
1874
|
-
for (const raw of names) {
|
|
1875
|
-
const colon = raw.indexOf(":");
|
|
1876
|
-
const name = colon === -1 ? raw : raw.slice(0, colon);
|
|
1877
|
-
const version = colon === -1 ? void 0 : raw.slice(colon + 1);
|
|
1878
|
-
const c = catalog.get(name);
|
|
1879
|
-
if (!c) {
|
|
1880
|
-
unknown.push(raw);
|
|
1881
|
-
continue;
|
|
1882
|
-
}
|
|
1883
|
-
if (version !== void 0 && c.file.category !== "language") {
|
|
1884
|
-
throw new Error(
|
|
1885
|
-
`Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
|
|
1886
|
-
);
|
|
1887
|
-
}
|
|
1888
|
-
out.push({ component: c, ...version !== void 0 ? { version } : {} });
|
|
1889
|
-
}
|
|
1890
|
-
if (unknown.length > 0) {
|
|
1891
|
-
const available = [...catalog.keys()].sort();
|
|
1892
|
-
throw new Error(
|
|
1893
|
-
`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
1894
|
-
Available: ${available.join(", ")}.`
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
return out;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
// src/proxy/dynamic.ts
|
|
1901
|
-
import { promises as fs6 } from "fs";
|
|
1902
|
-
import path7 from "path";
|
|
1903
|
-
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
1904
|
-
if (ports.length === 0) {
|
|
1905
|
-
throw new Error(
|
|
1906
|
-
`writeDynamicConfig requires at least one port. For empty port lists, call removeDynamicConfig(${JSON.stringify(name)}).`
|
|
1907
|
-
);
|
|
1908
|
-
}
|
|
1909
|
-
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
1910
|
-
await fs6.mkdir(dir, { recursive: true });
|
|
1911
|
-
const file = path7.join(dir, `${name}.yml`);
|
|
1912
|
-
await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
1913
|
-
return file;
|
|
1914
|
-
}
|
|
1915
|
-
async function removeDynamicConfig(name, opts = {}) {
|
|
1916
|
-
const file = path7.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1917
|
-
await fs6.rm(file, { force: true });
|
|
1918
|
-
}
|
|
1919
|
-
function renderDynamicConfig(name, ports) {
|
|
1920
|
-
const lines = [];
|
|
1921
|
-
lines.push("# Generated by Monoceros \u2014 do not edit by hand.");
|
|
1922
|
-
lines.push(`# Container: ${name}`);
|
|
1923
|
-
lines.push(`# Ports: ${ports.join(", ")}`);
|
|
1924
|
-
lines.push("# Traefik file-provider re-reads this on change (~100 ms);");
|
|
1925
|
-
lines.push(
|
|
1926
|
-
"# to change routing, edit container-configs/" + name + ".yml or use"
|
|
1927
|
-
);
|
|
1928
|
-
lines.push("# `monoceros add-port` / `monoceros remove-port`.");
|
|
1929
|
-
lines.push("http:");
|
|
1930
|
-
lines.push(" routers:");
|
|
1931
|
-
ports.forEach((port, idx) => {
|
|
1932
|
-
const router = `${name}-${port}`;
|
|
1933
|
-
const hostExplicit = `${name}-${port}.localhost`;
|
|
1934
|
-
const rule = idx === 0 ? `"Host(\`${name}.localhost\`) || Host(\`${hostExplicit}\`)"` : `"Host(\`${hostExplicit}\`)"`;
|
|
1935
|
-
lines.push(` ${router}:`);
|
|
1936
|
-
lines.push(` rule: ${rule}`);
|
|
1937
|
-
lines.push(` service: ${router}`);
|
|
1938
|
-
lines.push(" entryPoints:");
|
|
1939
|
-
lines.push(" - web");
|
|
1940
|
-
});
|
|
1941
|
-
lines.push(" services:");
|
|
1942
|
-
for (const port of ports) {
|
|
1943
|
-
const svc = `${name}-${port}`;
|
|
1944
|
-
lines.push(` ${svc}:`);
|
|
1945
|
-
lines.push(" loadBalancer:");
|
|
1946
|
-
lines.push(" servers:");
|
|
1947
|
-
lines.push(` - url: "http://${name}:${port}"`);
|
|
1948
|
-
}
|
|
1949
|
-
return lines.join("\n") + "\n";
|
|
1950
|
-
}
|
|
1951
|
-
function proxyUrlsFor(name, ports, hostPort = 80) {
|
|
1952
|
-
const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
|
|
1953
|
-
return ports.map((port, idx) => ({
|
|
1954
|
-
port,
|
|
1955
|
-
url: `http://${name}-${port}.localhost${portSuffix}`,
|
|
1956
|
-
isDefault: idx === 0
|
|
1957
|
-
}));
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
// src/proxy/port-check.ts
|
|
1961
|
-
import { Socket } from "net";
|
|
1962
|
-
var CONNECT_TIMEOUT_MS = 750;
|
|
1963
|
-
var realPortProbe = (port) => {
|
|
1964
|
-
return new Promise((resolve) => {
|
|
1965
|
-
const socket = new Socket();
|
|
1966
|
-
let settled = false;
|
|
1967
|
-
const settle = (result) => {
|
|
1968
|
-
if (settled) return;
|
|
1969
|
-
settled = true;
|
|
1970
|
-
socket.destroy();
|
|
1971
|
-
resolve(result);
|
|
1972
|
-
};
|
|
1973
|
-
socket.setTimeout(CONNECT_TIMEOUT_MS);
|
|
1974
|
-
socket.once("connect", () => {
|
|
1975
|
-
settle({
|
|
1976
|
-
ok: false,
|
|
1977
|
-
code: "EADDRINUSE",
|
|
1978
|
-
message: `another process is listening on ${port}`
|
|
1979
|
-
});
|
|
1980
|
-
});
|
|
1981
|
-
socket.once("timeout", () => {
|
|
1982
|
-
settle({ ok: true });
|
|
1983
|
-
});
|
|
1984
|
-
socket.once("error", (err) => {
|
|
1985
|
-
const code = err.code ?? "UNKNOWN";
|
|
1986
|
-
if (code === "ECONNREFUSED") {
|
|
1987
|
-
settle({ ok: true });
|
|
1988
|
-
} else {
|
|
1989
|
-
settle({
|
|
1990
|
-
ok: false,
|
|
1991
|
-
code,
|
|
1992
|
-
message: err.message
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
1996
|
-
socket.connect(port, "127.0.0.1");
|
|
1568
|
+
socket.connect(port, "127.0.0.1");
|
|
1997
1569
|
});
|
|
1998
1570
|
};
|
|
1999
1571
|
async function preflightHostPort(hostPort, opts = {}) {
|
|
@@ -2118,8 +1690,8 @@ function knownServices() {
|
|
|
2118
1690
|
}
|
|
2119
1691
|
|
|
2120
1692
|
// src/create/scaffold.ts
|
|
2121
|
-
import { existsSync as
|
|
2122
|
-
import
|
|
1693
|
+
import { existsSync as existsSync3, readFileSync, promises as fs7 } from "fs";
|
|
1694
|
+
import path6 from "path";
|
|
2123
1695
|
|
|
2124
1696
|
// src/util/ref.ts
|
|
2125
1697
|
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
@@ -2290,8 +1862,8 @@ function resolveFeatures(opts) {
|
|
|
2290
1862
|
if (match) {
|
|
2291
1863
|
const name = match.name;
|
|
2292
1864
|
const checkout = workbenchCheckoutRoot();
|
|
2293
|
-
const localSourceDir = checkout ?
|
|
2294
|
-
if (localSourceDir &&
|
|
1865
|
+
const localSourceDir = checkout ? path6.join(checkout, "images", "features", name) : null;
|
|
1866
|
+
if (localSourceDir && existsSync3(localSourceDir)) {
|
|
2295
1867
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
2296
1868
|
resolved.push({
|
|
2297
1869
|
devcontainerKey: `./features/${name}`,
|
|
@@ -2315,9 +1887,9 @@ function resolveFeatures(opts) {
|
|
|
2315
1887
|
return resolved;
|
|
2316
1888
|
}
|
|
2317
1889
|
function readPersistentHomeEntries(localSourceDir) {
|
|
2318
|
-
const manifestPath =
|
|
1890
|
+
const manifestPath = path6.join(localSourceDir, "devcontainer-feature.json");
|
|
2319
1891
|
try {
|
|
2320
|
-
const text =
|
|
1892
|
+
const text = readFileSync(manifestPath, "utf8");
|
|
2321
1893
|
const parsed = JSON.parse(text);
|
|
2322
1894
|
return {
|
|
2323
1895
|
paths: filterSubpaths(parsed["x-monoceros"]?.persistentHomePaths),
|
|
@@ -2600,17 +2172,17 @@ function buildPostCreateScript(opts) {
|
|
|
2600
2172
|
return lines.join("\n") + "\n";
|
|
2601
2173
|
}
|
|
2602
2174
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
2603
|
-
const dest =
|
|
2175
|
+
const dest = path6.join(devcontainerDir, "post-create.sh");
|
|
2604
2176
|
await fs7.writeFile(dest, buildPostCreateScript(opts));
|
|
2605
2177
|
await fs7.chmod(dest, 493);
|
|
2606
2178
|
}
|
|
2607
2179
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
2608
2180
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
2609
|
-
const devcontainerDir =
|
|
2610
|
-
const monocerosDir =
|
|
2611
|
-
const projectsDir =
|
|
2612
|
-
const homeDir =
|
|
2613
|
-
const dataDir =
|
|
2181
|
+
const devcontainerDir = path6.join(targetDir, ".devcontainer");
|
|
2182
|
+
const monocerosDir = path6.join(targetDir, ".monoceros");
|
|
2183
|
+
const projectsDir = path6.join(targetDir, "projects");
|
|
2184
|
+
const homeDir = path6.join(targetDir, "home");
|
|
2185
|
+
const dataDir = path6.join(targetDir, "data");
|
|
2614
2186
|
await fs7.mkdir(devcontainerDir, { recursive: true });
|
|
2615
2187
|
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2616
2188
|
await fs7.mkdir(projectsDir, { recursive: true });
|
|
@@ -2620,56 +2192,56 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
|
2620
2192
|
for (const svcId of opts.services) {
|
|
2621
2193
|
const def = SERVICE_CATALOG[svcId];
|
|
2622
2194
|
if (def?.dataMount) {
|
|
2623
|
-
await fs7.mkdir(
|
|
2195
|
+
await fs7.mkdir(path6.join(dataDir, def.id), { recursive: true });
|
|
2624
2196
|
}
|
|
2625
2197
|
}
|
|
2626
2198
|
}
|
|
2627
|
-
const containerGitignore =
|
|
2199
|
+
const containerGitignore = path6.join(targetDir, ".gitignore");
|
|
2628
2200
|
await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2629
|
-
const gitkeep =
|
|
2630
|
-
if (!
|
|
2201
|
+
const gitkeep = path6.join(projectsDir, ".gitkeep");
|
|
2202
|
+
if (!existsSync3(gitkeep)) {
|
|
2631
2203
|
await fs7.writeFile(gitkeep, "");
|
|
2632
2204
|
}
|
|
2633
2205
|
await fs7.writeFile(
|
|
2634
|
-
|
|
2206
|
+
path6.join(monocerosDir, ".gitignore"),
|
|
2635
2207
|
"git-credentials*\ngitconfig\n"
|
|
2636
2208
|
);
|
|
2637
2209
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
2638
2210
|
await fs7.writeFile(
|
|
2639
|
-
|
|
2211
|
+
path6.join(devcontainerDir, "devcontainer.json"),
|
|
2640
2212
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
2641
2213
|
);
|
|
2642
|
-
const featuresDir =
|
|
2643
|
-
if (
|
|
2214
|
+
const featuresDir = path6.join(devcontainerDir, "features");
|
|
2215
|
+
if (existsSync3(featuresDir)) {
|
|
2644
2216
|
await fs7.rm(featuresDir, { recursive: true, force: true });
|
|
2645
2217
|
}
|
|
2646
2218
|
const resolvedFeatures = resolveFeatures(opts);
|
|
2647
2219
|
for (const f of resolvedFeatures) {
|
|
2648
2220
|
if (!f.localSourceDir || !f.localName) continue;
|
|
2649
|
-
const dest =
|
|
2221
|
+
const dest = path6.join(featuresDir, f.localName);
|
|
2650
2222
|
await fs7.mkdir(dest, { recursive: true });
|
|
2651
2223
|
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2652
2224
|
}
|
|
2653
2225
|
for (const f of resolvedFeatures) {
|
|
2654
2226
|
for (const sub of f.persistentHomePaths) {
|
|
2655
|
-
await fs7.mkdir(
|
|
2227
|
+
await fs7.mkdir(path6.join(homeDir, sub), { recursive: true });
|
|
2656
2228
|
}
|
|
2657
2229
|
for (const entry2 of f.persistentHomeFiles) {
|
|
2658
|
-
const filePath =
|
|
2659
|
-
await fs7.mkdir(
|
|
2660
|
-
if (!
|
|
2230
|
+
const filePath = path6.join(homeDir, entry2.path);
|
|
2231
|
+
await fs7.mkdir(path6.dirname(filePath), { recursive: true });
|
|
2232
|
+
if (!existsSync3(filePath)) {
|
|
2661
2233
|
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2662
2234
|
}
|
|
2663
2235
|
}
|
|
2664
2236
|
}
|
|
2665
2237
|
await writePostCreateScript(devcontainerDir, opts);
|
|
2666
|
-
const composePath =
|
|
2238
|
+
const composePath = path6.join(devcontainerDir, "compose.yaml");
|
|
2667
2239
|
if (needsCompose(opts)) {
|
|
2668
2240
|
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2669
|
-
} else if (
|
|
2241
|
+
} else if (existsSync3(composePath)) {
|
|
2670
2242
|
await fs7.rm(composePath);
|
|
2671
2243
|
}
|
|
2672
|
-
const workspacePath =
|
|
2244
|
+
const workspacePath = path6.join(targetDir, `${opts.name}.code-workspace`);
|
|
2673
2245
|
let existingWorkspace;
|
|
2674
2246
|
try {
|
|
2675
2247
|
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
@@ -2694,25 +2266,25 @@ import {
|
|
|
2694
2266
|
} from "yaml";
|
|
2695
2267
|
|
|
2696
2268
|
// src/init/manifest.ts
|
|
2697
|
-
import { existsSync as
|
|
2698
|
-
import
|
|
2269
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
2270
|
+
import path7 from "path";
|
|
2699
2271
|
function resolveManifestPath(name, checkoutRoot) {
|
|
2700
2272
|
if (checkoutRoot) {
|
|
2701
|
-
const checkoutPath =
|
|
2273
|
+
const checkoutPath = path7.join(
|
|
2702
2274
|
checkoutRoot,
|
|
2703
2275
|
"images",
|
|
2704
2276
|
"features",
|
|
2705
2277
|
name,
|
|
2706
2278
|
"devcontainer-feature.json"
|
|
2707
2279
|
);
|
|
2708
|
-
if (
|
|
2280
|
+
if (existsSync4(checkoutPath)) return checkoutPath;
|
|
2709
2281
|
}
|
|
2710
|
-
const bundlePath =
|
|
2282
|
+
const bundlePath = path7.join(
|
|
2711
2283
|
bundledFeaturesDir(),
|
|
2712
2284
|
name,
|
|
2713
2285
|
"devcontainer-feature.json"
|
|
2714
2286
|
);
|
|
2715
|
-
if (
|
|
2287
|
+
if (existsSync4(bundlePath)) return bundlePath;
|
|
2716
2288
|
return null;
|
|
2717
2289
|
}
|
|
2718
2290
|
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
@@ -2721,7 +2293,7 @@ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot())
|
|
|
2721
2293
|
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
2722
2294
|
if (!manifestPath) return void 0;
|
|
2723
2295
|
try {
|
|
2724
|
-
const text =
|
|
2296
|
+
const text = readFileSync2(manifestPath, "utf8");
|
|
2725
2297
|
const parsed = JSON.parse(text);
|
|
2726
2298
|
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
2727
2299
|
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
@@ -3397,7 +2969,7 @@ async function tryCloneInRunningContainer(input, entry2) {
|
|
|
3397
2969
|
logger.info(
|
|
3398
2970
|
`Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
|
|
3399
2971
|
);
|
|
3400
|
-
void
|
|
2972
|
+
void path8;
|
|
3401
2973
|
}
|
|
3402
2974
|
function shquote(value) {
|
|
3403
2975
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -3616,13 +3188,13 @@ async function mutate(opts, apply) {
|
|
|
3616
3188
|
}
|
|
3617
3189
|
function defaultLogger() {
|
|
3618
3190
|
return {
|
|
3619
|
-
info: (m) =>
|
|
3620
|
-
success: (m) =>
|
|
3621
|
-
warn: (m) =>
|
|
3191
|
+
info: (m) => consola.info(m),
|
|
3192
|
+
success: (m) => consola.success(m),
|
|
3193
|
+
warn: (m) => consola.warn(m)
|
|
3622
3194
|
};
|
|
3623
3195
|
}
|
|
3624
3196
|
var defaultConfirm = async (message) => {
|
|
3625
|
-
const result = await
|
|
3197
|
+
const result = await consola.prompt(message, {
|
|
3626
3198
|
type: "confirm",
|
|
3627
3199
|
initial: false
|
|
3628
3200
|
});
|
|
@@ -3662,9 +3234,6 @@ async function syncPortsToProxy(input) {
|
|
|
3662
3234
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
3663
3235
|
logger: { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
3664
3236
|
});
|
|
3665
|
-
await kickProxyReload({
|
|
3666
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3667
|
-
});
|
|
3668
3237
|
const urls = proxyUrlsFor(input.name, allPorts, hostPort);
|
|
3669
3238
|
const lines = urls.map((u) => {
|
|
3670
3239
|
const tag = u.isDefault ? " (default)" : "";
|
|
@@ -3674,9 +3243,6 @@ async function syncPortsToProxy(input) {
|
|
|
3674
3243
|
${lines.join("\n")}`);
|
|
3675
3244
|
} else {
|
|
3676
3245
|
await removeDynamicConfig(input.name, { monocerosHome: home });
|
|
3677
|
-
await kickProxyReload({
|
|
3678
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3679
|
-
});
|
|
3680
3246
|
await maybeStopProxy({
|
|
3681
3247
|
monocerosHome: home,
|
|
3682
3248
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
@@ -3713,7 +3279,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3713
3279
|
async run({ args }) {
|
|
3714
3280
|
const packages = [...getInnerArgs()];
|
|
3715
3281
|
if (packages.length === 0) {
|
|
3716
|
-
|
|
3282
|
+
consola2.error(
|
|
3717
3283
|
"No package names given. Usage: `monoceros add-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
|
|
3718
3284
|
);
|
|
3719
3285
|
process.exit(1);
|
|
@@ -3726,7 +3292,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3726
3292
|
});
|
|
3727
3293
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3728
3294
|
} catch (err) {
|
|
3729
|
-
|
|
3295
|
+
consola2.error(err instanceof Error ? err.message : String(err));
|
|
3730
3296
|
process.exit(1);
|
|
3731
3297
|
}
|
|
3732
3298
|
}
|
|
@@ -3734,7 +3300,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3734
3300
|
|
|
3735
3301
|
// src/commands/add-feature.ts
|
|
3736
3302
|
import { defineCommand as defineCommand2 } from "citty";
|
|
3737
|
-
import { consola as
|
|
3303
|
+
import { consola as consola3 } from "consola";
|
|
3738
3304
|
var addFeatureCommand = defineCommand2({
|
|
3739
3305
|
meta: {
|
|
3740
3306
|
name: "add-feature",
|
|
@@ -3764,7 +3330,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3764
3330
|
try {
|
|
3765
3331
|
options = parseOptionsAfterDashes(getInnerArgs());
|
|
3766
3332
|
} catch (err) {
|
|
3767
|
-
|
|
3333
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3768
3334
|
process.exit(1);
|
|
3769
3335
|
}
|
|
3770
3336
|
try {
|
|
@@ -3776,7 +3342,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3776
3342
|
});
|
|
3777
3343
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3778
3344
|
} catch (err) {
|
|
3779
|
-
|
|
3345
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3780
3346
|
process.exit(1);
|
|
3781
3347
|
}
|
|
3782
3348
|
}
|
|
@@ -3808,7 +3374,7 @@ function coerce(value) {
|
|
|
3808
3374
|
|
|
3809
3375
|
// src/commands/add-from-url.ts
|
|
3810
3376
|
import { defineCommand as defineCommand3 } from "citty";
|
|
3811
|
-
import { consola as
|
|
3377
|
+
import { consola as consola4 } from "consola";
|
|
3812
3378
|
var addFromUrlCommand = defineCommand3({
|
|
3813
3379
|
meta: {
|
|
3814
3380
|
name: "add-from-url",
|
|
@@ -3845,7 +3411,7 @@ var addFromUrlCommand = defineCommand3({
|
|
|
3845
3411
|
});
|
|
3846
3412
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3847
3413
|
} catch (err) {
|
|
3848
|
-
|
|
3414
|
+
consola4.error(err instanceof Error ? err.message : String(err));
|
|
3849
3415
|
process.exit(1);
|
|
3850
3416
|
}
|
|
3851
3417
|
}
|
|
@@ -3880,7 +3446,7 @@ function printSecurityWarning(url) {
|
|
|
3880
3446
|
|
|
3881
3447
|
// src/commands/add-repo.ts
|
|
3882
3448
|
import { defineCommand as defineCommand4 } from "citty";
|
|
3883
|
-
import { consola as
|
|
3449
|
+
import { consola as consola5 } from "consola";
|
|
3884
3450
|
var addRepoCommand = defineCommand4({
|
|
3885
3451
|
meta: {
|
|
3886
3452
|
name: "add-repo",
|
|
@@ -3934,7 +3500,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3934
3500
|
});
|
|
3935
3501
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3936
3502
|
} catch (err) {
|
|
3937
|
-
|
|
3503
|
+
consola5.error(err instanceof Error ? err.message : String(err));
|
|
3938
3504
|
process.exit(1);
|
|
3939
3505
|
}
|
|
3940
3506
|
}
|
|
@@ -3942,7 +3508,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3942
3508
|
|
|
3943
3509
|
// src/commands/add-language.ts
|
|
3944
3510
|
import { defineCommand as defineCommand5 } from "citty";
|
|
3945
|
-
import { consola as
|
|
3511
|
+
import { consola as consola6 } from "consola";
|
|
3946
3512
|
var addLanguageCommand = defineCommand5({
|
|
3947
3513
|
meta: {
|
|
3948
3514
|
name: "add-language",
|
|
@@ -3975,207 +3541,549 @@ var addLanguageCommand = defineCommand5({
|
|
|
3975
3541
|
yes: args.yes
|
|
3976
3542
|
});
|
|
3977
3543
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
consola6.error(err instanceof Error ? err.message : String(err));
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
// src/commands/add-port.ts
|
|
3552
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
3553
|
+
import { consola as consola7 } from "consola";
|
|
3554
|
+
var addPortCommand = defineCommand6({
|
|
3555
|
+
meta: {
|
|
3556
|
+
name: "add-port",
|
|
3557
|
+
group: "edit",
|
|
3558
|
+
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)."
|
|
3559
|
+
},
|
|
3560
|
+
args: {
|
|
3561
|
+
name: {
|
|
3562
|
+
type: "positional",
|
|
3563
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3564
|
+
required: true
|
|
3565
|
+
},
|
|
3566
|
+
yes: {
|
|
3567
|
+
type: "boolean",
|
|
3568
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3569
|
+
alias: ["y"],
|
|
3570
|
+
default: false
|
|
3571
|
+
},
|
|
3572
|
+
default: {
|
|
3573
|
+
type: "boolean",
|
|
3574
|
+
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.",
|
|
3575
|
+
default: false
|
|
3576
|
+
}
|
|
3577
|
+
},
|
|
3578
|
+
async run({ args }) {
|
|
3579
|
+
const tokens = [...getInnerArgs()];
|
|
3580
|
+
if (tokens.length === 0) {
|
|
3581
|
+
consola7.error(
|
|
3582
|
+
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
3583
|
+
);
|
|
3584
|
+
process.exit(1);
|
|
3585
|
+
}
|
|
3586
|
+
try {
|
|
3587
|
+
const result = await runAddPort({
|
|
3588
|
+
name: args.name,
|
|
3589
|
+
ports: tokens.map(coerceToken),
|
|
3590
|
+
yes: args.yes,
|
|
3591
|
+
asDefault: args.default
|
|
3592
|
+
});
|
|
3593
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3978
3594
|
} catch (err) {
|
|
3979
3595
|
consola7.error(err instanceof Error ? err.message : String(err));
|
|
3980
3596
|
process.exit(1);
|
|
3981
3597
|
}
|
|
3982
3598
|
}
|
|
3983
|
-
});
|
|
3599
|
+
});
|
|
3600
|
+
function coerceToken(raw) {
|
|
3601
|
+
const n = Number(raw);
|
|
3602
|
+
return Number.isFinite(n) ? n : raw;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// src/commands/add-service.ts
|
|
3606
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3607
|
+
import { consola as consola8 } from "consola";
|
|
3608
|
+
var addServiceCommand = defineCommand7({
|
|
3609
|
+
meta: {
|
|
3610
|
+
name: "add-service",
|
|
3611
|
+
group: "edit",
|
|
3612
|
+
description: "Add a compose service (postgres, mysql, redis, \u2026) to the container config. Idempotent, prints a diff before writing."
|
|
3613
|
+
},
|
|
3614
|
+
args: {
|
|
3615
|
+
name: {
|
|
3616
|
+
type: "positional",
|
|
3617
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3618
|
+
required: true
|
|
3619
|
+
},
|
|
3620
|
+
service: {
|
|
3621
|
+
type: "positional",
|
|
3622
|
+
description: "Service identifier (postgres, mysql, redis).",
|
|
3623
|
+
required: true
|
|
3624
|
+
},
|
|
3625
|
+
yes: {
|
|
3626
|
+
type: "boolean",
|
|
3627
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3628
|
+
alias: ["y"],
|
|
3629
|
+
default: false
|
|
3630
|
+
}
|
|
3631
|
+
},
|
|
3632
|
+
async run({ args }) {
|
|
3633
|
+
try {
|
|
3634
|
+
const result = await runAddService({
|
|
3635
|
+
name: args.name,
|
|
3636
|
+
service: args.service,
|
|
3637
|
+
yes: args.yes
|
|
3638
|
+
});
|
|
3639
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3640
|
+
} catch (err) {
|
|
3641
|
+
consola8.error(err instanceof Error ? err.message : String(err));
|
|
3642
|
+
process.exit(1);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
|
|
3647
|
+
// src/commands/apply.ts
|
|
3648
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
3649
|
+
|
|
3650
|
+
// src/apply/index.ts
|
|
3651
|
+
import { existsSync as existsSync6, promises as fs11 } from "fs";
|
|
3652
|
+
import { consola as consola11 } from "consola";
|
|
3653
|
+
|
|
3654
|
+
// src/config/state.ts
|
|
3655
|
+
import { promises as fs9 } from "fs";
|
|
3656
|
+
import path9 from "path";
|
|
3657
|
+
function buildStateFile(opts) {
|
|
3658
|
+
return {
|
|
3659
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
3660
|
+
origin: opts.origin,
|
|
3661
|
+
monocerosCliVersion: opts.cliVersion,
|
|
3662
|
+
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
function stateFilePath(targetDir) {
|
|
3666
|
+
return path9.join(targetDir, ".monoceros", "state.json");
|
|
3667
|
+
}
|
|
3668
|
+
async function readStateFile(targetDir) {
|
|
3669
|
+
try {
|
|
3670
|
+
const content = await fs9.readFile(stateFilePath(targetDir), "utf8");
|
|
3671
|
+
return JSON.parse(content);
|
|
3672
|
+
} catch {
|
|
3673
|
+
return void 0;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
async function writeStateFile(targetDir, state) {
|
|
3677
|
+
const monocerosDir = path9.join(targetDir, ".monoceros");
|
|
3678
|
+
await fs9.mkdir(monocerosDir, { recursive: true });
|
|
3679
|
+
await fs9.writeFile(
|
|
3680
|
+
stateFilePath(targetDir),
|
|
3681
|
+
JSON.stringify(state, null, 2) + "\n"
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// src/config/transform.ts
|
|
3686
|
+
function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
3687
|
+
const featureRecord = {};
|
|
3688
|
+
for (const entry2 of config.features) {
|
|
3689
|
+
const defaults = featureDefaults[entry2.ref] ?? {};
|
|
3690
|
+
const containerOpts = Object.fromEntries(
|
|
3691
|
+
Object.entries(entry2.options ?? {}).filter(([, v]) => v !== "")
|
|
3692
|
+
);
|
|
3693
|
+
featureRecord[entry2.ref] = { ...defaults, ...containerOpts };
|
|
3694
|
+
}
|
|
3695
|
+
const result = {
|
|
3696
|
+
name: config.name,
|
|
3697
|
+
languages: [...config.languages],
|
|
3698
|
+
services: [...config.services]
|
|
3699
|
+
};
|
|
3700
|
+
if (config.externalServices.postgres !== void 0) {
|
|
3701
|
+
result.postgresUrl = config.externalServices.postgres;
|
|
3702
|
+
}
|
|
3703
|
+
if (config.aptPackages.length > 0) {
|
|
3704
|
+
result.aptPackages = [...config.aptPackages];
|
|
3705
|
+
}
|
|
3706
|
+
if (Object.keys(featureRecord).length > 0) {
|
|
3707
|
+
result.features = featureRecord;
|
|
3708
|
+
}
|
|
3709
|
+
if (config.installUrls.length > 0) {
|
|
3710
|
+
result.installUrls = [...config.installUrls];
|
|
3711
|
+
}
|
|
3712
|
+
if (config.repos.length > 0) {
|
|
3713
|
+
result.repos = config.repos.map((r) => ({
|
|
3714
|
+
url: r.url,
|
|
3715
|
+
// `path` is optional in the yml; CreateOptions requires it.
|
|
3716
|
+
// When the yml omits `path`, fall back to the URL-derived
|
|
3717
|
+
// single-segment default (`https://.../foo.git` → `foo`),
|
|
3718
|
+
// which lands the clone at `projects/foo/`.
|
|
3719
|
+
path: r.path ?? deriveRepoName(r.url),
|
|
3720
|
+
// gitUser is forwarded only when BOTH name + email are set.
|
|
3721
|
+
// The relaxed GitUserSchema accepts nullable / empty strings
|
|
3722
|
+
// (so a yml placeholder `name:` parses without error), so we
|
|
3723
|
+
// re-check here before downstream code, which expects both
|
|
3724
|
+
// values to be non-empty.
|
|
3725
|
+
...r.git?.user?.name && r.git.user.email ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
3726
|
+
...r.provider ? { provider: r.provider } : {}
|
|
3727
|
+
}));
|
|
3728
|
+
}
|
|
3729
|
+
const routingPorts = config.routing?.ports ?? [];
|
|
3730
|
+
if (routingPorts.length > 0) {
|
|
3731
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3732
|
+
const ports = [];
|
|
3733
|
+
for (const entry2 of routingPorts) {
|
|
3734
|
+
const n = portNumber(entry2);
|
|
3735
|
+
if (seen.has(n)) continue;
|
|
3736
|
+
seen.add(n);
|
|
3737
|
+
ports.push(n);
|
|
3738
|
+
}
|
|
3739
|
+
result.ports = ports;
|
|
3740
|
+
}
|
|
3741
|
+
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
3742
|
+
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
3743
|
+
}
|
|
3744
|
+
return result;
|
|
3745
|
+
}
|
|
3984
3746
|
|
|
3985
|
-
// src/
|
|
3986
|
-
import {
|
|
3987
|
-
import {
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3747
|
+
// src/devcontainer/compose.ts
|
|
3748
|
+
import { spawn as spawn5 } from "child_process";
|
|
3749
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3750
|
+
import path11 from "path";
|
|
3751
|
+
import { consola as consola9 } from "consola";
|
|
3752
|
+
|
|
3753
|
+
// src/util/mask-secrets.ts
|
|
3754
|
+
import { Transform } from "stream";
|
|
3755
|
+
var PATTERNS = [
|
|
3756
|
+
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
3757
|
+
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
3758
|
+
// matching unrelated all-caps words.
|
|
3759
|
+
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
3760
|
+
// Bitbucket Cloud app password.
|
|
3761
|
+
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
3762
|
+
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
3763
|
+
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
3764
|
+
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
3765
|
+
// GitHub fine-grained PAT.
|
|
3766
|
+
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
3767
|
+
// Anthropic API key.
|
|
3768
|
+
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
3769
|
+
];
|
|
3770
|
+
function maskSecrets(text) {
|
|
3771
|
+
let result = text;
|
|
3772
|
+
for (const { re } of PATTERNS) {
|
|
3773
|
+
result = result.replace(re, maskOne);
|
|
3774
|
+
}
|
|
3775
|
+
return result;
|
|
3776
|
+
}
|
|
3777
|
+
function maskOne(token) {
|
|
3778
|
+
if (token.length <= 12) return token;
|
|
3779
|
+
return `${token.slice(0, 5)}\u2026${token.slice(-6)}`;
|
|
3780
|
+
}
|
|
3781
|
+
function createSecretMaskStream() {
|
|
3782
|
+
let buffer = "";
|
|
3783
|
+
return new Transform({
|
|
3784
|
+
decodeStrings: true,
|
|
3785
|
+
transform(chunk, _enc, cb) {
|
|
3786
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3787
|
+
buffer += text;
|
|
3788
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
3789
|
+
if (lastNewline === -1) {
|
|
3790
|
+
cb(null);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
const flushable = buffer.slice(0, lastNewline + 1);
|
|
3794
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
3795
|
+
cb(null, maskSecrets(flushable));
|
|
3999
3796
|
},
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
3797
|
+
flush(cb) {
|
|
3798
|
+
if (buffer.length > 0) {
|
|
3799
|
+
const tail = maskSecrets(buffer);
|
|
3800
|
+
buffer = "";
|
|
3801
|
+
cb(null, tail);
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
cb(null);
|
|
3805
|
+
}
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
// src/devcontainer/cli.ts
|
|
3810
|
+
import { spawn as spawn4 } from "child_process";
|
|
3811
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3812
|
+
import { createRequire } from "module";
|
|
3813
|
+
import path10 from "path";
|
|
3814
|
+
|
|
3815
|
+
// src/devcontainer/runtime-pull-hint.ts
|
|
3816
|
+
import { Transform as Transform2 } from "stream";
|
|
3817
|
+
var RUNTIME_PULL_MARKER = "No manifest found for ghcr.io/getmonoceros/monoceros-runtime";
|
|
3818
|
+
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...';
|
|
3819
|
+
function createRuntimePullHintStream(state) {
|
|
3820
|
+
let buffer = "";
|
|
3821
|
+
const appendHintIfMarker = (block) => {
|
|
3822
|
+
if (state.hinted || !block.includes(RUNTIME_PULL_MARKER)) return block;
|
|
3823
|
+
state.hinted = true;
|
|
3824
|
+
return `${block}${dim(`(i) ${RUNTIME_PULL_HINT}`)}
|
|
3825
|
+
`;
|
|
3826
|
+
};
|
|
3827
|
+
return new Transform2({
|
|
3828
|
+
decodeStrings: true,
|
|
3829
|
+
transform(chunk, _enc, cb) {
|
|
3830
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3831
|
+
buffer += text;
|
|
3832
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
3833
|
+
if (lastNewline === -1) {
|
|
3834
|
+
cb(null);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
const flushable = buffer.slice(0, lastNewline + 1);
|
|
3838
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
3839
|
+
cb(null, appendHintIfMarker(flushable));
|
|
4005
3840
|
},
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
3841
|
+
flush(cb) {
|
|
3842
|
+
if (buffer.length === 0) {
|
|
3843
|
+
cb(null);
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
const tail = buffer;
|
|
3847
|
+
buffer = "";
|
|
3848
|
+
cb(null, appendHintIfMarker(tail));
|
|
4010
3849
|
}
|
|
4011
|
-
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// src/devcontainer/cli.ts
|
|
3854
|
+
var require_ = createRequire(import.meta.url);
|
|
3855
|
+
var cachedBinaryPath = null;
|
|
3856
|
+
function devcontainerCliPath() {
|
|
3857
|
+
if (cachedBinaryPath) return cachedBinaryPath;
|
|
3858
|
+
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
3859
|
+
const pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
3860
|
+
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
3861
|
+
if (!binEntry) {
|
|
3862
|
+
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
3863
|
+
}
|
|
3864
|
+
cachedBinaryPath = path10.resolve(path10.dirname(pkgJsonPath), binEntry);
|
|
3865
|
+
return cachedBinaryPath;
|
|
3866
|
+
}
|
|
3867
|
+
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
3868
|
+
const binPath = devcontainerCliPath();
|
|
3869
|
+
return new Promise((resolve, reject) => {
|
|
3870
|
+
if (options.interactive) {
|
|
3871
|
+
const child2 = spawn4(process.execPath, [binPath, ...args], {
|
|
3872
|
+
cwd,
|
|
3873
|
+
stdio: "inherit"
|
|
3874
|
+
});
|
|
3875
|
+
child2.on("error", reject);
|
|
3876
|
+
child2.on("exit", (code) => resolve(code ?? 0));
|
|
3877
|
+
return;
|
|
4019
3878
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
3879
|
+
const child = spawn4(process.execPath, [binPath, ...args], {
|
|
3880
|
+
cwd,
|
|
3881
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3882
|
+
});
|
|
3883
|
+
if (options.quiet) {
|
|
3884
|
+
const stdoutChunks = [];
|
|
3885
|
+
const stderrChunks = [];
|
|
3886
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
3887
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
3888
|
+
child.on("error", reject);
|
|
3889
|
+
child.on("exit", (code) => {
|
|
3890
|
+
const exitCode = code ?? 0;
|
|
3891
|
+
if (exitCode !== 0) {
|
|
3892
|
+
process.stderr.write(
|
|
3893
|
+
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
3894
|
+
);
|
|
3895
|
+
process.stderr.write(
|
|
3896
|
+
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
3897
|
+
);
|
|
3898
|
+
}
|
|
3899
|
+
resolve(exitCode);
|
|
4026
3900
|
});
|
|
4027
|
-
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
const pullHint = { hinted: false };
|
|
3904
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stdout);
|
|
3905
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stderr);
|
|
3906
|
+
child.on("error", reject);
|
|
3907
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
3908
|
+
});
|
|
3909
|
+
};
|
|
3910
|
+
|
|
3911
|
+
// src/devcontainer/compose.ts
|
|
3912
|
+
var spawnDockerCompose = (args, cwd) => {
|
|
3913
|
+
return new Promise((resolve, reject) => {
|
|
3914
|
+
const child = spawn5("docker", ["compose", ...args], {
|
|
3915
|
+
cwd,
|
|
3916
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
3917
|
+
});
|
|
3918
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
3919
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
3920
|
+
child.on("error", reject);
|
|
3921
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
3922
|
+
});
|
|
3923
|
+
};
|
|
3924
|
+
var spawnDocker = (args) => {
|
|
3925
|
+
return new Promise((resolve, reject) => {
|
|
3926
|
+
const child = spawn5("docker", args, {
|
|
3927
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3928
|
+
});
|
|
3929
|
+
let stdout = "";
|
|
3930
|
+
let stderr = "";
|
|
3931
|
+
child.stdout?.on("data", (chunk) => {
|
|
3932
|
+
stdout += chunk.toString("utf8");
|
|
3933
|
+
});
|
|
3934
|
+
child.stderr?.on("data", (chunk) => {
|
|
3935
|
+
stderr += chunk.toString("utf8");
|
|
3936
|
+
});
|
|
3937
|
+
child.on("error", reject);
|
|
3938
|
+
child.on(
|
|
3939
|
+
"exit",
|
|
3940
|
+
(code) => resolve({ exitCode: code ?? 0, stdout, stderr })
|
|
3941
|
+
);
|
|
3942
|
+
});
|
|
3943
|
+
};
|
|
3944
|
+
async function findContainerIds(filters, exec = spawnDocker) {
|
|
3945
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3946
|
+
for (const filter of filters) {
|
|
3947
|
+
const result = await exec(["ps", "-aq", "--filter", filter]);
|
|
3948
|
+
if (result.exitCode !== 0) continue;
|
|
3949
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
3950
|
+
const id = line.trim();
|
|
3951
|
+
if (id) ids.add(id);
|
|
4031
3952
|
}
|
|
4032
3953
|
}
|
|
4033
|
-
|
|
4034
|
-
function coerceToken(raw) {
|
|
4035
|
-
const n = Number(raw);
|
|
4036
|
-
return Number.isFinite(n) ? n : raw;
|
|
3954
|
+
return [...ids];
|
|
4037
3955
|
}
|
|
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
|
|
3956
|
+
async function cleanupDockerObjects(opts) {
|
|
3957
|
+
const exec = opts.exec ?? spawnDocker;
|
|
3958
|
+
const tag = opts.logTag ?? "cleanup";
|
|
3959
|
+
opts.logger.info(`[${tag}] tearing down docker project ${opts.projectName}\u2026`);
|
|
3960
|
+
const ids = await findContainerIds(opts.filters, exec);
|
|
3961
|
+
let rmExit = 0;
|
|
3962
|
+
if (ids.length > 0) {
|
|
3963
|
+
opts.logger.info(`[${tag}] removing containers: ${ids.join(" ")}`);
|
|
3964
|
+
const rmResult = await exec(["rm", "-f", ...ids]);
|
|
3965
|
+
rmExit = rmResult.exitCode;
|
|
3966
|
+
if (rmExit !== 0 && rmResult.stderr.trim()) {
|
|
3967
|
+
opts.logger.info(`[${tag}] ${rmResult.stderr.trim()}`);
|
|
4064
3968
|
}
|
|
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);
|
|
3969
|
+
} else {
|
|
3970
|
+
opts.logger.info(`[${tag}] no containers found`);
|
|
3971
|
+
}
|
|
3972
|
+
if (opts.network) {
|
|
3973
|
+
const netResult = await exec(["network", "rm", opts.network]);
|
|
3974
|
+
if (netResult.exitCode === 0) {
|
|
3975
|
+
opts.logger.info(`[${tag}] network ${opts.network} removed`);
|
|
4077
3976
|
}
|
|
4078
3977
|
}
|
|
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
|
-
};
|
|
3978
|
+
opts.logger.info(`[${tag}] docker cleanup done`);
|
|
3979
|
+
return { exitCode: rmExit, removedIds: ids };
|
|
4098
3980
|
}
|
|
4099
|
-
function
|
|
4100
|
-
return path11.
|
|
3981
|
+
function composeProjectName(root) {
|
|
3982
|
+
return `${path11.basename(root)}_devcontainer`;
|
|
4101
3983
|
}
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
3984
|
+
function resolveCompose(root) {
|
|
3985
|
+
if (!existsSync5(path11.join(root, ".devcontainer"))) {
|
|
3986
|
+
throw new Error(
|
|
3987
|
+
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
const composeFile = path11.join(root, ".devcontainer", "compose.yaml");
|
|
3991
|
+
if (!existsSync5(composeFile)) {
|
|
3992
|
+
throw new Error(
|
|
3993
|
+
`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.`
|
|
3994
|
+
);
|
|
4108
3995
|
}
|
|
3996
|
+
return { composeFile, projectName: composeProjectName(root) };
|
|
4109
3997
|
}
|
|
4110
|
-
async function
|
|
4111
|
-
const
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
3998
|
+
async function runComposeAction(buildSubArgs, opts) {
|
|
3999
|
+
const { composeFile, projectName } = resolveCompose(opts.root);
|
|
4000
|
+
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
4001
|
+
const subArgs = buildSubArgs(opts.service);
|
|
4002
|
+
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
4003
|
+
}
|
|
4004
|
+
async function runStart(opts) {
|
|
4005
|
+
resolveCompose(opts.root);
|
|
4006
|
+
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
4007
|
+
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
4008
|
+
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
4009
|
+
return spawnFn(
|
|
4010
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
4011
|
+
opts.root
|
|
4116
4012
|
);
|
|
4117
4013
|
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
const containerOpts = Object.fromEntries(
|
|
4125
|
-
Object.entries(entry2.options ?? {}).filter(([, v]) => v !== "")
|
|
4014
|
+
async function runContainerCycle(root, opts) {
|
|
4015
|
+
const { hasCompose, logger } = opts;
|
|
4016
|
+
if (hasCompose) {
|
|
4017
|
+
const projectName = composeProjectName(root);
|
|
4018
|
+
logger.info(
|
|
4019
|
+
`Force-removing existing ${projectName} containers (volumes preserved)\u2026`
|
|
4126
4020
|
);
|
|
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);
|
|
4021
|
+
const exec = opts.dockerExec ?? spawnDocker;
|
|
4022
|
+
const filters = [
|
|
4023
|
+
`label=com.docker.compose.project=${projectName}`,
|
|
4024
|
+
`name=^${projectName}-`
|
|
4025
|
+
];
|
|
4026
|
+
const { exitCode: rmExit } = await cleanupDockerObjects({
|
|
4027
|
+
projectName,
|
|
4028
|
+
filters,
|
|
4029
|
+
network: `${projectName}_default`,
|
|
4030
|
+
logger,
|
|
4031
|
+
exec
|
|
4032
|
+
});
|
|
4033
|
+
if (rmExit !== 0) return rmExit;
|
|
4034
|
+
const remaining = await findContainerIds(filters, exec);
|
|
4035
|
+
if (remaining.length > 0) {
|
|
4036
|
+
const warn = logger.warn ?? logger.info;
|
|
4037
|
+
warn(
|
|
4038
|
+
`ERROR: containers under project ${projectName} reappeared after removal.
|
|
4039
|
+
This typically means VS Code's Remote Containers extension is connected
|
|
4040
|
+
to this devcontainer and auto-recreated it. Close the dev container
|
|
4041
|
+
session in VS Code (Cmd+Shift+P \u2192 'Dev Containers: Close Remote Connection')
|
|
4042
|
+
and retry \`monoceros apply\`.`
|
|
4043
|
+
);
|
|
4044
|
+
return 1;
|
|
4172
4045
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4046
|
+
return runStart({
|
|
4047
|
+
root,
|
|
4048
|
+
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
4049
|
+
logger
|
|
4050
|
+
});
|
|
4177
4051
|
}
|
|
4178
|
-
|
|
4052
|
+
logger.info(`Recreating image-mode devcontainer at ${root}\u2026`);
|
|
4053
|
+
const spawnFn = opts.devcontainerSpawn ?? spawnDevcontainer;
|
|
4054
|
+
return spawnFn(
|
|
4055
|
+
[
|
|
4056
|
+
"up",
|
|
4057
|
+
"--workspace-folder",
|
|
4058
|
+
root,
|
|
4059
|
+
"--mount-workspace-git-root=false",
|
|
4060
|
+
"--remove-existing-container"
|
|
4061
|
+
],
|
|
4062
|
+
root
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
4065
|
+
function runStop(opts) {
|
|
4066
|
+
return runComposeAction(
|
|
4067
|
+
(service) => ["stop", ...service ? [service] : []],
|
|
4068
|
+
opts
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
function runStatus(opts) {
|
|
4072
|
+
return runComposeAction(
|
|
4073
|
+
(service) => ["ps", ...service ? [service] : []],
|
|
4074
|
+
opts
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
function runLogs(opts) {
|
|
4078
|
+
const follow = opts.follow ?? true;
|
|
4079
|
+
return runComposeAction(
|
|
4080
|
+
(service) => [
|
|
4081
|
+
"logs",
|
|
4082
|
+
...follow ? ["-f"] : [],
|
|
4083
|
+
...service ? [service] : []
|
|
4084
|
+
],
|
|
4085
|
+
opts
|
|
4086
|
+
);
|
|
4179
4087
|
}
|
|
4180
4088
|
|
|
4181
4089
|
// src/devcontainer/repo-reachability.ts
|
|
@@ -4277,7 +4185,7 @@ function formatUnreachableReposError(failures) {
|
|
|
4277
4185
|
}
|
|
4278
4186
|
lines.push("");
|
|
4279
4187
|
}
|
|
4280
|
-
lines.push(`Then re-run ${
|
|
4188
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
4281
4189
|
return lines.join("\n");
|
|
4282
4190
|
}
|
|
4283
4191
|
function headerForKind(kind) {
|
|
@@ -4360,14 +4268,14 @@ function formatRootlessNotSupportedError() {
|
|
|
4360
4268
|
``,
|
|
4361
4269
|
`To fix, switch back to standard rootful Docker:`,
|
|
4362
4270
|
``,
|
|
4363
|
-
|
|
4271
|
+
cyan2(
|
|
4364
4272
|
` systemctl --user stop docker.service docker.socket 2>/dev/null || true`
|
|
4365
4273
|
),
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4274
|
+
cyan2(` dockerd-rootless-setuptool.sh uninstall`),
|
|
4275
|
+
cyan2(` rootlesskit rm -rf ~/.local/share/docker`),
|
|
4276
|
+
cyan2(` unset DOCKER_HOST DOCKER_CONTEXT`),
|
|
4277
|
+
cyan2(` sudo systemctl enable --now docker`),
|
|
4278
|
+
cyan2(` sudo usermod -aG docker $USER`),
|
|
4371
4279
|
``,
|
|
4372
4280
|
`If you added DOCKER_HOST or DOCKER_CONTEXT to ~/.bashrc /`,
|
|
4373
4281
|
`~/.profile (the rootless setup may have suggested it), remove`,
|
|
@@ -4672,7 +4580,7 @@ ${sectionLine(label)}
|
|
|
4672
4580
|
section("Container");
|
|
4673
4581
|
const featureRefs = parsed.config.features.map((f) => f.ref);
|
|
4674
4582
|
if (featureRefs.length > 0) {
|
|
4675
|
-
logger.info(`Features: ${featureRefs.map((r) =>
|
|
4583
|
+
logger.info(`Features: ${featureRefs.map((r) => cyan2(r)).join(", ")}`);
|
|
4676
4584
|
}
|
|
4677
4585
|
logger.info(
|
|
4678
4586
|
dim(
|
|
@@ -4695,14 +4603,8 @@ ${sectionLine(label)}
|
|
|
4695
4603
|
hostPort: proxyHostPort(globalConfig),
|
|
4696
4604
|
logger
|
|
4697
4605
|
});
|
|
4698
|
-
await kickProxyReload({
|
|
4699
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4700
|
-
});
|
|
4701
4606
|
} else {
|
|
4702
4607
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
4703
|
-
await kickProxyReload({
|
|
4704
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4705
|
-
});
|
|
4706
4608
|
}
|
|
4707
4609
|
} catch (err) {
|
|
4708
4610
|
logger.warn?.(
|
|
@@ -4717,7 +4619,7 @@ ${sectionLine(label)}
|
|
|
4717
4619
|
});
|
|
4718
4620
|
if (exitCode === 0) {
|
|
4719
4621
|
section("Next steps");
|
|
4720
|
-
logger.info(` ${
|
|
4622
|
+
logger.info(` ${cyan2(`monoceros shell ${opts.name}`)}`);
|
|
4721
4623
|
}
|
|
4722
4624
|
return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
|
|
4723
4625
|
}
|
|
@@ -4815,7 +4717,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4815
4717
|
}
|
|
4816
4718
|
|
|
4817
4719
|
// src/version.ts
|
|
4818
|
-
var CLI_VERSION = true ? "1.
|
|
4720
|
+
var CLI_VERSION = true ? "1.12.0" : "dev";
|
|
4819
4721
|
|
|
4820
4722
|
// src/commands/_dispatch.ts
|
|
4821
4723
|
import { consola as consola12 } from "consola";
|
|
@@ -6277,7 +6179,7 @@ async function runRemove(opts) {
|
|
|
6277
6179
|
projectName,
|
|
6278
6180
|
filters: [
|
|
6279
6181
|
`label=com.docker.compose.project=${projectName}`,
|
|
6280
|
-
`label=devcontainer.local_folder=${
|
|
6182
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
6281
6183
|
`name=^${projectName}-`,
|
|
6282
6184
|
`name=^vsc-${opts.name}-`
|
|
6283
6185
|
],
|
|
@@ -6345,9 +6247,6 @@ async function runRemove(opts) {
|
|
|
6345
6247
|
}
|
|
6346
6248
|
try {
|
|
6347
6249
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
6348
|
-
await kickProxyReload({
|
|
6349
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
6350
|
-
});
|
|
6351
6250
|
} catch (err) {
|
|
6352
6251
|
logger.warn?.(
|
|
6353
6252
|
`Could not remove Traefik dynamic config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
@@ -7087,10 +6986,7 @@ async function lookupContainerNetwork(args) {
|
|
|
7087
6986
|
"ps",
|
|
7088
6987
|
"-q",
|
|
7089
6988
|
"--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)}`
|
|
6989
|
+
`label=devcontainer.local_folder=${args.containerRoot}`
|
|
7094
6990
|
]);
|
|
7095
6991
|
if (psResult.exitCode !== 0) {
|
|
7096
6992
|
throw new Error(
|
|
@@ -7412,7 +7308,6 @@ var main = defineCommand30({
|
|
|
7412
7308
|
|
|
7413
7309
|
// src/bin.ts
|
|
7414
7310
|
bootstrapDockerGroup();
|
|
7415
|
-
bootstrapWslBackend();
|
|
7416
7311
|
consumeInnerArgsFromProcessArgv();
|
|
7417
7312
|
async function entry() {
|
|
7418
7313
|
if (await maybeRenderHelp(process.argv.slice(2), main)) {
|