@getmonoceros/workbench 1.11.10 → 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 +1232 -1329
- 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"], {
|
|
@@ -715,16 +659,18 @@ function uniqueHttpsHosts(repos) {
|
|
|
715
659
|
}
|
|
716
660
|
return [...byHost.values()];
|
|
717
661
|
}
|
|
662
|
+
var BREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
|
|
718
663
|
function installCommandForOS(opts) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
664
|
+
const withBrewBootstrap = (cmd) => [
|
|
665
|
+
"",
|
|
666
|
+
cyan2(BREW_INSTALL_COMMAND),
|
|
667
|
+
cyan2(cmd),
|
|
668
|
+
"",
|
|
669
|
+
dim("(Skip the first line if you already have Homebrew.)")
|
|
670
|
+
].join("\n");
|
|
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.`;
|
|
728
674
|
}
|
|
729
675
|
function providerSetupHint(host, provider) {
|
|
730
676
|
if (provider === "github") {
|
|
@@ -732,7 +678,6 @@ function providerSetupHint(host, provider) {
|
|
|
732
678
|
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
733
679
|
const install = installCommandForOS({
|
|
734
680
|
brew: "brew install gh",
|
|
735
|
-
winget: "winget install --id GitHub.cli",
|
|
736
681
|
linuxBrew: "brew install gh",
|
|
737
682
|
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
738
683
|
});
|
|
@@ -743,8 +688,8 @@ function providerSetupHint(host, provider) {
|
|
|
743
688
|
install,
|
|
744
689
|
"",
|
|
745
690
|
"Then run once:",
|
|
746
|
-
|
|
747
|
-
|
|
691
|
+
cyan2(`gh auth login${hostArg}`),
|
|
692
|
+
cyan2(`gh auth setup-git${hostArg}`),
|
|
748
693
|
"",
|
|
749
694
|
"`gh auth login` walks through OAuth in your browser.",
|
|
750
695
|
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
@@ -756,7 +701,6 @@ function providerSetupHint(host, provider) {
|
|
|
756
701
|
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
757
702
|
const install = installCommandForOS({
|
|
758
703
|
brew: "brew install glab",
|
|
759
|
-
winget: "winget install --id GLab.GLab",
|
|
760
704
|
linuxBrew: "brew install glab",
|
|
761
705
|
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
762
706
|
});
|
|
@@ -767,7 +711,7 @@ function providerSetupHint(host, provider) {
|
|
|
767
711
|
install,
|
|
768
712
|
"",
|
|
769
713
|
"Then run once:",
|
|
770
|
-
|
|
714
|
+
cyan2(`glab auth login${hostArg}`),
|
|
771
715
|
"",
|
|
772
716
|
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
773
717
|
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
@@ -786,7 +730,7 @@ function providerSetupHint(host, provider) {
|
|
|
786
730
|
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
787
731
|
"",
|
|
788
732
|
"Then store it via your OS credential helper:",
|
|
789
|
-
|
|
733
|
+
cyan2(
|
|
790
734
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
791
735
|
)
|
|
792
736
|
].join("\n")
|
|
@@ -802,7 +746,7 @@ function providerSetupHint(host, provider) {
|
|
|
802
746
|
"at least repo-read + repo-write scopes for the repos you need.",
|
|
803
747
|
"",
|
|
804
748
|
"Then store it via your OS credential helper:",
|
|
805
|
-
|
|
749
|
+
cyan2(
|
|
806
750
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
807
751
|
)
|
|
808
752
|
].join("\n")
|
|
@@ -820,7 +764,7 @@ function providerSetupHint(host, provider) {
|
|
|
820
764
|
"need push from the container).",
|
|
821
765
|
"",
|
|
822
766
|
"Then store it via your OS credential helper:",
|
|
823
|
-
|
|
767
|
+
cyan2(
|
|
824
768
|
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
825
769
|
)
|
|
826
770
|
].join("\n")
|
|
@@ -933,7 +877,7 @@ function formatMissingCredentialsError(missing) {
|
|
|
933
877
|
"",
|
|
934
878
|
hint.body,
|
|
935
879
|
"",
|
|
936
|
-
`Then re-run ${
|
|
880
|
+
`Then re-run ${cyan2("monoceros apply")}.`
|
|
937
881
|
].join("\n");
|
|
938
882
|
}
|
|
939
883
|
const lines = [
|
|
@@ -947,7 +891,7 @@ function formatMissingCredentialsError(missing) {
|
|
|
947
891
|
lines.push(hint.body);
|
|
948
892
|
lines.push("");
|
|
949
893
|
}
|
|
950
|
-
lines.push(`Then re-run ${
|
|
894
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
951
895
|
return lines.join("\n");
|
|
952
896
|
}
|
|
953
897
|
function formatUnknownProviderError(hosts) {
|
|
@@ -959,32 +903,18 @@ function formatUnknownProviderError(hosts) {
|
|
|
959
903
|
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
960
904
|
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
961
905
|
"",
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
906
|
+
cyan2(" repos:"),
|
|
907
|
+
cyan2(` - url: https://${sorted[0]}/\u2026`),
|
|
908
|
+
cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
965
909
|
"",
|
|
966
|
-
`Or re-add with ${
|
|
910
|
+
`Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
967
911
|
];
|
|
968
912
|
return lines.join("\n");
|
|
969
913
|
}
|
|
970
914
|
|
|
971
915
|
// src/devcontainer/locate-running.ts
|
|
972
|
-
import { spawn as spawn5 } from "child_process";
|
|
973
|
-
|
|
974
|
-
// src/devcontainer/compose.ts
|
|
975
|
-
import { spawn as spawn4 } from "child_process";
|
|
976
|
-
import { existsSync as existsSync2 } from "fs";
|
|
977
|
-
import path5 from "path";
|
|
978
|
-
import { consola } from "consola";
|
|
979
|
-
|
|
980
|
-
// src/proxy/index.ts
|
|
981
916
|
import { spawn as spawn2 } from "child_process";
|
|
982
|
-
|
|
983
|
-
import path3 from "path";
|
|
984
|
-
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
985
|
-
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
986
|
-
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
987
|
-
var defaultDockerExec = (args) => {
|
|
917
|
+
var realDockerLookup = (args) => {
|
|
988
918
|
return new Promise((resolve, reject) => {
|
|
989
919
|
const child = spawn2("docker", args, {
|
|
990
920
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1004,988 +934,638 @@ var defaultDockerExec = (args) => {
|
|
|
1004
934
|
);
|
|
1005
935
|
});
|
|
1006
936
|
};
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
"
|
|
1016
|
-
"--format",
|
|
1017
|
-
"{{.State.Running}}",
|
|
1018
|
-
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"
|
|
1019
946
|
]);
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1022
|
-
|
|
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;
|
|
1023
950
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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;
|
|
1052
1009
|
}
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
"run",
|
|
1056
|
-
"-d",
|
|
1057
|
-
"--name",
|
|
1058
|
-
PROXY_CONTAINER_NAME,
|
|
1059
|
-
"--network",
|
|
1060
|
-
PROXY_NETWORK_NAME,
|
|
1061
|
-
"-p",
|
|
1062
|
-
`${hostPort}:80`,
|
|
1063
|
-
"-v",
|
|
1064
|
-
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
1065
|
-
"--label",
|
|
1066
|
-
"monoceros.role=proxy",
|
|
1067
|
-
TRAEFIK_IMAGE,
|
|
1068
|
-
"--entrypoints.web.address=:80",
|
|
1069
|
-
"--providers.file.directory=/etc/traefik/dynamic",
|
|
1070
|
-
"--providers.file.watch=true",
|
|
1071
|
-
"--providers.docker=false",
|
|
1072
|
-
"--api.dashboard=false",
|
|
1073
|
-
"--log.level=INFO"
|
|
1074
|
-
]);
|
|
1075
|
-
if (run.exitCode !== 0) {
|
|
1010
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1011
|
+
if (doc.errors.length > 0) {
|
|
1076
1012
|
throw new Error(
|
|
1077
|
-
`
|
|
1013
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1078
1014
|
);
|
|
1079
1015
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (inspect.exitCode !== 0) {
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
1098
|
-
if (others.length > 0) return;
|
|
1099
|
-
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
1100
|
-
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
1101
|
-
if (netRm.exitCode !== 0) {
|
|
1102
|
-
logger?.warn?.(
|
|
1103
|
-
`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.`
|
|
1104
1030
|
);
|
|
1105
|
-
return;
|
|
1106
1031
|
}
|
|
1107
|
-
|
|
1108
|
-
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1109
|
-
);
|
|
1032
|
+
return result.data;
|
|
1110
1033
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
1124
|
-
// GitHub fine-grained PAT.
|
|
1125
|
-
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
1126
|
-
// Anthropic API key.
|
|
1127
|
-
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
1128
|
-
];
|
|
1129
|
-
function maskSecrets(text) {
|
|
1130
|
-
let result = text;
|
|
1131
|
-
for (const { re } of PATTERNS) {
|
|
1132
|
-
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;
|
|
1133
1046
|
}
|
|
1134
|
-
|
|
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 };
|
|
1135
1084
|
}
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
|
|
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;
|
|
1139
1091
|
}
|
|
1140
|
-
function
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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;
|
|
1162
1127
|
}
|
|
1163
|
-
cb(null);
|
|
1164
1128
|
}
|
|
1165
|
-
|
|
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;
|
|
1166
1136
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
function createRuntimePullHintStream(state) {
|
|
1179
|
-
let buffer = "";
|
|
1180
|
-
const appendHintIfMarker = (block) => {
|
|
1181
|
-
if (state.hinted || !block.includes(RUNTIME_PULL_MARKER)) return block;
|
|
1182
|
-
state.hinted = true;
|
|
1183
|
-
return `${block}${dim(`(i) ${RUNTIME_PULL_HINT}`)}
|
|
1184
|
-
`;
|
|
1185
|
-
};
|
|
1186
|
-
return new Transform2({
|
|
1187
|
-
decodeStrings: true,
|
|
1188
|
-
transform(chunk, _enc, cb) {
|
|
1189
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1190
|
-
buffer += text;
|
|
1191
|
-
const lastNewline = buffer.lastIndexOf("\n");
|
|
1192
|
-
if (lastNewline === -1) {
|
|
1193
|
-
cb(null);
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
const flushable = buffer.slice(0, lastNewline + 1);
|
|
1197
|
-
buffer = buffer.slice(lastNewline + 1);
|
|
1198
|
-
cb(null, appendHintIfMarker(flushable));
|
|
1199
|
-
},
|
|
1200
|
-
flush(cb) {
|
|
1201
|
-
if (buffer.length === 0) {
|
|
1202
|
-
cb(null);
|
|
1203
|
-
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++;
|
|
1204
1148
|
}
|
|
1205
|
-
|
|
1206
|
-
buffer = "";
|
|
1207
|
-
cb(null, appendHintIfMarker(tail));
|
|
1149
|
+
continue;
|
|
1208
1150
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
// src/devcontainer/cli.ts
|
|
1213
|
-
var require_ = createRequire(import.meta.url);
|
|
1214
|
-
var cachedBinaryPath = null;
|
|
1215
|
-
function devcontainerCliPath() {
|
|
1216
|
-
if (cachedBinaryPath) return cachedBinaryPath;
|
|
1217
|
-
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
1218
|
-
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
1219
|
-
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
1220
|
-
if (!binEntry) {
|
|
1221
|
-
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
1151
|
+
out.push(line);
|
|
1152
|
+
i++;
|
|
1222
1153
|
}
|
|
1223
|
-
|
|
1224
|
-
return cachedBinaryPath;
|
|
1154
|
+
return out.join("\n");
|
|
1225
1155
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
child2.on("exit", (code) => resolve(code ?? 0));
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
const child = spawn3(process.execPath, [binPath, ...args], {
|
|
1239
|
-
cwd,
|
|
1240
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1241
|
-
});
|
|
1242
|
-
if (options.quiet) {
|
|
1243
|
-
const stdoutChunks = [];
|
|
1244
|
-
const stderrChunks = [];
|
|
1245
|
-
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
1246
|
-
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
1247
|
-
child.on("error", reject);
|
|
1248
|
-
child.on("exit", (code) => {
|
|
1249
|
-
const exitCode = code ?? 0;
|
|
1250
|
-
if (exitCode !== 0) {
|
|
1251
|
-
process.stderr.write(
|
|
1252
|
-
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
1253
|
-
);
|
|
1254
|
-
process.stderr.write(
|
|
1255
|
-
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
resolve(exitCode);
|
|
1259
|
-
});
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
const pullHint = { hinted: false };
|
|
1263
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stdout);
|
|
1264
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stderr);
|
|
1265
|
-
child.on("error", reject);
|
|
1266
|
-
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;
|
|
1267
1165
|
});
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
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"
|
|
1276
1218
|
});
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
}
|
|
1283
|
-
|
|
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(", ")}`
|
|
1225
|
+
});
|
|
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]}`
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
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;
|
|
1243
|
+
}
|
|
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;
|
|
1251
|
+
}
|
|
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 });
|
|
1274
|
+
}
|
|
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 ?? {});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
languages,
|
|
1305
|
+
services,
|
|
1306
|
+
features: [...featureByRef.values()]
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function isResolvedComponent(x) {
|
|
1310
|
+
return "component" in x;
|
|
1311
|
+
}
|
|
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;
|
|
1323
|
+
}
|
|
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 } : {} });
|
|
1342
|
+
}
|
|
1343
|
+
if (unknown.length > 0) {
|
|
1344
|
+
const available = [...catalog.keys()].sort();
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
1347
|
+
Available: ${available.join(", ")}.`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
return out;
|
|
1351
|
+
}
|
|
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) => {
|
|
1284
1361
|
return new Promise((resolve, reject) => {
|
|
1285
|
-
const child =
|
|
1362
|
+
const child = spawn3("docker", args, {
|
|
1286
1363
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1287
1364
|
});
|
|
1288
1365
|
let stdout = "";
|
|
1289
1366
|
let stderr = "";
|
|
1290
|
-
child.stdout
|
|
1291
|
-
stdout += chunk.toString(
|
|
1367
|
+
child.stdout.on("data", (chunk) => {
|
|
1368
|
+
stdout += chunk.toString();
|
|
1292
1369
|
});
|
|
1293
|
-
child.stderr
|
|
1294
|
-
stderr += chunk.toString(
|
|
1370
|
+
child.stderr.on("data", (chunk) => {
|
|
1371
|
+
stderr += chunk.toString();
|
|
1295
1372
|
});
|
|
1296
1373
|
child.on("error", reject);
|
|
1297
1374
|
child.on(
|
|
1298
1375
|
"exit",
|
|
1299
|
-
(code) => resolve({ exitCode: code ?? 0
|
|
1376
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1300
1377
|
);
|
|
1301
1378
|
});
|
|
1302
1379
|
};
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1380
|
+
var realDocker = defaultDockerExec;
|
|
1381
|
+
function proxyDynamicDir(home) {
|
|
1382
|
+
return path4.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
1383
|
+
}
|
|
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}`}`
|
|
1394
|
+
);
|
|
1311
1395
|
}
|
|
1312
1396
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
opts.logger.info(`[${tag}] ${rmResult.stderr.trim()}`);
|
|
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
|
+
);
|
|
1327
1410
|
}
|
|
1328
|
-
|
|
1329
|
-
opts.logger.info(`[${tag}] no containers found`);
|
|
1411
|
+
return;
|
|
1330
1412
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
+
);
|
|
1336
1439
|
}
|
|
1337
|
-
opts.logger
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
function dockerLocalFolderLabel(p) {
|
|
1341
|
-
if (process.platform !== "win32") return p;
|
|
1342
|
-
return p.replace(
|
|
1343
|
-
/^([A-Z]):/,
|
|
1344
|
-
(_, drive) => `${drive.toLowerCase()}:`
|
|
1440
|
+
opts.logger?.info(
|
|
1441
|
+
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
1345
1442
|
);
|
|
1346
1443
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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;
|
|
1355
1456
|
}
|
|
1356
|
-
const
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
|
|
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}`}`
|
|
1360
1464
|
);
|
|
1465
|
+
return;
|
|
1361
1466
|
}
|
|
1362
|
-
|
|
1363
|
-
}
|
|
1364
|
-
async function runComposeAction(buildSubArgs, opts) {
|
|
1365
|
-
const { composeFile, projectName } = resolveCompose(opts.root);
|
|
1366
|
-
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
1367
|
-
const subArgs = buildSubArgs(opts.service);
|
|
1368
|
-
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
1369
|
-
}
|
|
1370
|
-
async function runStart(opts) {
|
|
1371
|
-
resolveCompose(opts.root);
|
|
1372
|
-
const logger = opts.logger ?? { info: (msg) => consola.info(msg) };
|
|
1373
|
-
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
1374
|
-
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
1375
|
-
return spawnFn(
|
|
1376
|
-
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
1377
|
-
opts.root
|
|
1467
|
+
logger?.info(
|
|
1468
|
+
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1378
1469
|
);
|
|
1379
1470
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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)}).`
|
|
1386
1479
|
);
|
|
1387
|
-
const exec = opts.dockerExec ?? spawnDocker;
|
|
1388
|
-
const filters = [
|
|
1389
|
-
`label=com.docker.compose.project=${projectName}`,
|
|
1390
|
-
`name=^${projectName}-`
|
|
1391
|
-
];
|
|
1392
|
-
const { exitCode: rmExit } = await cleanupDockerObjects({
|
|
1393
|
-
projectName,
|
|
1394
|
-
filters,
|
|
1395
|
-
network: `${projectName}_default`,
|
|
1396
|
-
logger,
|
|
1397
|
-
exec
|
|
1398
|
-
});
|
|
1399
|
-
if (rmExit !== 0) return rmExit;
|
|
1400
|
-
const remaining = await findContainerIds(filters, exec);
|
|
1401
|
-
if (remaining.length > 0) {
|
|
1402
|
-
const warn = logger.warn ?? logger.info;
|
|
1403
|
-
warn(
|
|
1404
|
-
`ERROR: containers under project ${projectName} reappeared after removal.
|
|
1405
|
-
This typically means VS Code's Remote Containers extension is connected
|
|
1406
|
-
to this devcontainer and auto-recreated it. Close the dev container
|
|
1407
|
-
session in VS Code (Cmd+Shift+P \u2192 'Dev Containers: Close Remote Connection')
|
|
1408
|
-
and retry \`monoceros apply\`.`
|
|
1409
|
-
);
|
|
1410
|
-
return 1;
|
|
1411
|
-
}
|
|
1412
|
-
return runStart({
|
|
1413
|
-
root,
|
|
1414
|
-
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
1415
|
-
logger
|
|
1416
|
-
});
|
|
1417
1480
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
"--workspace-folder",
|
|
1424
|
-
root,
|
|
1425
|
-
"--mount-workspace-git-root=false",
|
|
1426
|
-
"--remove-existing-container"
|
|
1427
|
-
],
|
|
1428
|
-
root
|
|
1429
|
-
);
|
|
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;
|
|
1430
1486
|
}
|
|
1431
|
-
function
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
opts
|
|
1435
|
-
);
|
|
1487
|
+
async function removeDynamicConfig(name, opts = {}) {
|
|
1488
|
+
const file = path5.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1489
|
+
await fs6.rm(file, { force: true });
|
|
1436
1490
|
}
|
|
1437
|
-
function
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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"
|
|
1441
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";
|
|
1442
1522
|
}
|
|
1443
|
-
function
|
|
1444
|
-
const
|
|
1445
|
-
return
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
],
|
|
1451
|
-
opts
|
|
1452
|
-
);
|
|
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
|
+
}));
|
|
1453
1530
|
}
|
|
1454
1531
|
|
|
1455
|
-
// src/
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
+
});
|
|
1460
1552
|
});
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
child.stdout.on("data", (chunk) => {
|
|
1464
|
-
stdout += chunk.toString();
|
|
1553
|
+
socket.once("timeout", () => {
|
|
1554
|
+
settle({ ok: true });
|
|
1465
1555
|
});
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
+
}
|
|
1468
1567
|
});
|
|
1469
|
-
|
|
1470
|
-
child.on(
|
|
1471
|
-
"exit",
|
|
1472
|
-
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1473
|
-
);
|
|
1474
|
-
});
|
|
1475
|
-
};
|
|
1476
|
-
async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
1477
|
-
const docker = opts.docker ?? realDockerLookup;
|
|
1478
|
-
const result = await docker([
|
|
1479
|
-
"ps",
|
|
1480
|
-
"-q",
|
|
1481
|
-
"--filter",
|
|
1482
|
-
`label=devcontainer.local_folder=${dockerLocalFolderLabel(containerPath)}`,
|
|
1483
|
-
"--filter",
|
|
1484
|
-
"status=running"
|
|
1485
|
-
]);
|
|
1486
|
-
if (result.exitCode !== 0) return null;
|
|
1487
|
-
const ids = result.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1488
|
-
return ids[0] ?? null;
|
|
1489
|
-
}
|
|
1490
|
-
var realContainerExec = (containerId, argv) => {
|
|
1491
|
-
return new Promise((resolve, reject) => {
|
|
1492
|
-
const child = spawn5("docker", ["exec", containerId, ...argv], {
|
|
1493
|
-
// Inherit stdio so live git output reaches the user.
|
|
1494
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
1495
|
-
});
|
|
1496
|
-
child.on("error", reject);
|
|
1497
|
-
child.on("exit", (code) => resolve({ exitCode: code ?? 0 }));
|
|
1498
|
-
});
|
|
1499
|
-
};
|
|
1500
|
-
|
|
1501
|
-
// src/config/global.ts
|
|
1502
|
-
import { promises as fs4 } from "fs";
|
|
1503
|
-
import { z as z2 } from "zod";
|
|
1504
|
-
import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
|
|
1505
|
-
var SCHEMA_VERSION = 1;
|
|
1506
|
-
var MonocerosConfigSchema = z2.object({
|
|
1507
|
-
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
1508
|
-
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
1509
|
-
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
1510
|
-
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
1511
|
-
// in that case; without .nullish() the schema would reject it and
|
|
1512
|
-
// we'd be back to forcing builders to comment-juggle three lines.
|
|
1513
|
-
defaults: z2.object({
|
|
1514
|
-
// .nullish() (not just .optional()) so the sample yml can leave
|
|
1515
|
-
// `git:` uncommented as a category marker — YAML produces
|
|
1516
|
-
// `git: null` for an empty mapping, which zod's plain
|
|
1517
|
-
// `.optional()` would reject.
|
|
1518
|
-
git: z2.object({
|
|
1519
|
-
user: GitUserSchema.optional()
|
|
1520
|
-
}).nullish(),
|
|
1521
|
-
// .nullish() for the same reason as `git` — the sample keeps
|
|
1522
|
-
// `features:` uncommented as a category marker.
|
|
1523
|
-
features: z2.record(
|
|
1524
|
-
z2.string().regex(
|
|
1525
|
-
REGEX.featureRef,
|
|
1526
|
-
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
1527
|
-
),
|
|
1528
|
-
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
1529
|
-
).nullish()
|
|
1530
|
-
}).nullish(),
|
|
1531
|
-
// Machine-global routing settings — one Traefik per builder, so
|
|
1532
|
-
// host-port and similar live here rather than in any container yml.
|
|
1533
|
-
// See ADR 0007.
|
|
1534
|
-
routing: z2.object({
|
|
1535
|
-
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
1536
|
-
"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>/."
|
|
1537
|
-
)
|
|
1538
|
-
}).nullish()
|
|
1539
|
-
});
|
|
1540
|
-
async function readMonocerosConfig(opts = {}) {
|
|
1541
|
-
const home = opts.monocerosHome ?? monocerosHome();
|
|
1542
|
-
const filePath = monocerosConfigPath(home);
|
|
1543
|
-
let text;
|
|
1544
|
-
try {
|
|
1545
|
-
text = await fs4.readFile(filePath, "utf8");
|
|
1546
|
-
} catch {
|
|
1547
|
-
return void 0;
|
|
1548
|
-
}
|
|
1549
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1550
|
-
if (doc.errors.length > 0) {
|
|
1551
|
-
throw new Error(
|
|
1552
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1553
|
-
);
|
|
1554
|
-
}
|
|
1555
|
-
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
1556
|
-
if (!result.success) {
|
|
1557
|
-
const issues = result.error.issues.map((issue) => {
|
|
1558
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1559
|
-
return ` - ${where}: ${issue.message}`;
|
|
1560
|
-
}).join("\n");
|
|
1561
|
-
throw new Error(
|
|
1562
|
-
`Invalid ${filePath}:
|
|
1563
|
-
${issues}
|
|
1564
|
-
|
|
1565
|
-
See ${filePath.replace(
|
|
1566
|
-
/\.yml$/,
|
|
1567
|
-
".sample.yml"
|
|
1568
|
-
)} for a valid example.`
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
return result.data;
|
|
1572
|
-
}
|
|
1573
|
-
var DEFAULT_PROXY_HOST_PORT = 80;
|
|
1574
|
-
function proxyHostPort(config) {
|
|
1575
|
-
return config?.routing?.hostPort ?? DEFAULT_PROXY_HOST_PORT;
|
|
1576
|
-
}
|
|
1577
|
-
async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
1578
|
-
const home = opts.monocerosHome ?? monocerosHome();
|
|
1579
|
-
const filePath = monocerosConfigPath(home);
|
|
1580
|
-
let text;
|
|
1581
|
-
try {
|
|
1582
|
-
text = await fs4.readFile(filePath, "utf8");
|
|
1583
|
-
} catch {
|
|
1584
|
-
text = void 0;
|
|
1585
|
-
}
|
|
1586
|
-
if (text === void 0) {
|
|
1587
|
-
const fresh = [
|
|
1588
|
-
"# Optional \u2014 global defaults for monoceros containers.",
|
|
1589
|
-
"",
|
|
1590
|
-
"schemaVersion: 1",
|
|
1591
|
-
"",
|
|
1592
|
-
"defaults:",
|
|
1593
|
-
" git:",
|
|
1594
|
-
" user:",
|
|
1595
|
-
` name: ${user.name}`,
|
|
1596
|
-
` email: ${user.email}`,
|
|
1597
|
-
""
|
|
1598
|
-
].join("\n");
|
|
1599
|
-
await fs4.mkdir(home, { recursive: true });
|
|
1600
|
-
await fs4.writeFile(filePath, fresh, "utf8");
|
|
1601
|
-
return { filePath, created: true, alreadySet: false };
|
|
1602
|
-
}
|
|
1603
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1604
|
-
if (doc.errors.length > 0) {
|
|
1605
|
-
throw new Error(
|
|
1606
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1609
|
-
const defaultsMap = ensureMap(doc, "defaults");
|
|
1610
|
-
const gitMap = ensureSubMapAtTop(defaultsMap, "git");
|
|
1611
|
-
const userMap = ensureSubMap(gitMap, "user");
|
|
1612
|
-
const existingName = userMap.get("name");
|
|
1613
|
-
const existingEmail = userMap.get("email");
|
|
1614
|
-
if (typeof existingName === "string" && existingName.length > 0 && typeof existingEmail === "string" && existingEmail.length > 0) {
|
|
1615
|
-
return { filePath, created: false, alreadySet: true };
|
|
1616
|
-
}
|
|
1617
|
-
relocateLeakedLeafComments(userMap, defaultsMap, "git");
|
|
1618
|
-
userMap.set("name", user.name);
|
|
1619
|
-
userMap.set("email", user.email);
|
|
1620
|
-
const newText = String(doc);
|
|
1621
|
-
await fs4.writeFile(filePath, newText, "utf8");
|
|
1622
|
-
return { filePath, created: false, alreadySet: false };
|
|
1623
|
-
}
|
|
1624
|
-
function ensureMap(doc, key) {
|
|
1625
|
-
const node = doc.get(key, true);
|
|
1626
|
-
if (node && isMap(node)) return node;
|
|
1627
|
-
const m = new YAMLMap();
|
|
1628
|
-
doc.set(key, m);
|
|
1629
|
-
return m;
|
|
1630
|
-
}
|
|
1631
|
-
function ensureSubMap(parent, key) {
|
|
1632
|
-
const node = parent.get(key, true);
|
|
1633
|
-
if (node && isMap(node)) return node;
|
|
1634
|
-
const m = new YAMLMap();
|
|
1635
|
-
parent.set(key, m);
|
|
1636
|
-
return m;
|
|
1637
|
-
}
|
|
1638
|
-
function ensureSubMapAtTop(parent, key) {
|
|
1639
|
-
const node = parent.get(key, true);
|
|
1640
|
-
if (node && isMap(node)) return node;
|
|
1641
|
-
const parentMaybe = parent;
|
|
1642
|
-
const newKey = new Scalar(key);
|
|
1643
|
-
if (parent.items.length > 0 && typeof parentMaybe.commentBefore === "string" && parentMaybe.commentBefore.length > 0) {
|
|
1644
|
-
const cleaned = stripCommentedKeySkeleton(parentMaybe.commentBefore, key);
|
|
1645
|
-
const blankMatch = cleaned.match(/\n[ \t]*\n/);
|
|
1646
|
-
let head;
|
|
1647
|
-
let tail;
|
|
1648
|
-
if (blankMatch && blankMatch.index !== void 0) {
|
|
1649
|
-
head = cleaned.slice(0, blankMatch.index);
|
|
1650
|
-
tail = cleaned.slice(blankMatch.index + blankMatch[0].length);
|
|
1651
|
-
} else {
|
|
1652
|
-
head = cleaned;
|
|
1653
|
-
tail = "";
|
|
1654
|
-
}
|
|
1655
|
-
if (head.length > 0) {
|
|
1656
|
-
newKey.commentBefore = head;
|
|
1657
|
-
if (parentMaybe.spaceBefore) newKey.spaceBefore = true;
|
|
1658
|
-
}
|
|
1659
|
-
if (tail.length > 0) {
|
|
1660
|
-
const firstKey = parent.items[0].key;
|
|
1661
|
-
if (firstKey && typeof firstKey === "object") {
|
|
1662
|
-
const existing = firstKey.commentBefore ?? "";
|
|
1663
|
-
firstKey.commentBefore = existing ? `${tail}
|
|
1664
|
-
${existing}` : tail;
|
|
1665
|
-
firstKey.spaceBefore = true;
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
parentMaybe.commentBefore = null;
|
|
1669
|
-
parentMaybe.spaceBefore = false;
|
|
1670
|
-
}
|
|
1671
|
-
const m = new YAMLMap();
|
|
1672
|
-
const pair = new Pair(newKey, m);
|
|
1673
|
-
parent.items.unshift(pair);
|
|
1674
|
-
return m;
|
|
1675
|
-
}
|
|
1676
|
-
function stripCommentedKeySkeleton(commentBody, key) {
|
|
1677
|
-
const lines = commentBody.split("\n");
|
|
1678
|
-
const headRe = new RegExp(`^ ${escapeRegExp(key)}:\\s*$`);
|
|
1679
|
-
const out = [];
|
|
1680
|
-
let i = 0;
|
|
1681
|
-
while (i < lines.length) {
|
|
1682
|
-
const line = lines[i];
|
|
1683
|
-
if (headRe.test(line)) {
|
|
1684
|
-
i++;
|
|
1685
|
-
while (i < lines.length && /^ {2,}\S/.test(lines[i])) {
|
|
1686
|
-
i++;
|
|
1687
|
-
}
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
out.push(line);
|
|
1691
|
-
i++;
|
|
1692
|
-
}
|
|
1693
|
-
return out.join("\n");
|
|
1694
|
-
}
|
|
1695
|
-
function escapeRegExp(s) {
|
|
1696
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1697
|
-
}
|
|
1698
|
-
function relocateLeakedLeafComments(leafMap, parent, ancestorKey) {
|
|
1699
|
-
const items = parent.items;
|
|
1700
|
-
const ancestorIdx = items.findIndex((p) => {
|
|
1701
|
-
const k = p.key;
|
|
1702
|
-
const v = typeof k === "string" ? k : k?.value ?? null;
|
|
1703
|
-
return v === ancestorKey;
|
|
1704
|
-
});
|
|
1705
|
-
if (ancestorIdx < 0 || ancestorIdx + 1 >= items.length) return;
|
|
1706
|
-
const target = items[ancestorIdx + 1];
|
|
1707
|
-
for (const pair of leafMap.items) {
|
|
1708
|
-
const value = pair.value;
|
|
1709
|
-
if (!value || typeof value !== "object") continue;
|
|
1710
|
-
const leakedComment = value.comment;
|
|
1711
|
-
const leakedSpace = value.spaceBefore;
|
|
1712
|
-
if (!leakedComment && !leakedSpace) continue;
|
|
1713
|
-
if (leakedComment) {
|
|
1714
|
-
const targetKey = target.key;
|
|
1715
|
-
if (targetKey && typeof targetKey === "object") {
|
|
1716
|
-
const existing = targetKey.commentBefore ?? "";
|
|
1717
|
-
targetKey.commentBefore = existing ? `${leakedComment}
|
|
1718
|
-
${existing}` : leakedComment;
|
|
1719
|
-
if (leakedSpace) targetKey.spaceBefore = true;
|
|
1720
|
-
}
|
|
1721
|
-
value.comment = null;
|
|
1722
|
-
}
|
|
1723
|
-
if (leakedSpace) value.spaceBefore = false;
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
// src/init/components.ts
|
|
1728
|
-
import { existsSync as existsSync3, promises as fs5 } from "fs";
|
|
1729
|
-
import path6 from "path";
|
|
1730
|
-
import { z as z3 } from "zod";
|
|
1731
|
-
import { parse as parseYaml } from "yaml";
|
|
1732
|
-
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
1733
|
-
var FeatureContributionSchema = z3.object({
|
|
1734
|
-
ref: z3.string().regex(REGEX.featureRef),
|
|
1735
|
-
options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
|
|
1736
|
-
});
|
|
1737
|
-
var ComponentFileSchema = z3.object({
|
|
1738
|
-
displayName: z3.string().min(1),
|
|
1739
|
-
description: z3.string().min(1),
|
|
1740
|
-
category: CategorySchema,
|
|
1741
|
-
contributes: z3.object({
|
|
1742
|
-
languages: z3.array(z3.string().min(1)).optional(),
|
|
1743
|
-
services: z3.array(z3.string().min(1)).optional(),
|
|
1744
|
-
features: z3.array(FeatureContributionSchema).optional()
|
|
1745
|
-
})
|
|
1746
|
-
}).superRefine((data, ctx) => {
|
|
1747
|
-
const c = data.contributes;
|
|
1748
|
-
const filled = [
|
|
1749
|
-
c.languages && c.languages.length > 0 ? "languages" : null,
|
|
1750
|
-
c.services && c.services.length > 0 ? "services" : null,
|
|
1751
|
-
c.features && c.features.length > 0 ? "features" : null
|
|
1752
|
-
].filter((x) => x !== null);
|
|
1753
|
-
if (filled.length === 0) {
|
|
1754
|
-
ctx.addIssue({
|
|
1755
|
-
code: z3.ZodIssueCode.custom,
|
|
1756
|
-
message: "contributes must set at least one of languages/services/features"
|
|
1757
|
-
});
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
if (filled.length > 1) {
|
|
1761
|
-
ctx.addIssue({
|
|
1762
|
-
code: z3.ZodIssueCode.custom,
|
|
1763
|
-
message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
|
|
1764
|
-
});
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
|
|
1768
|
-
if (filled[0] !== expected) {
|
|
1769
|
-
ctx.addIssue({
|
|
1770
|
-
code: z3.ZodIssueCode.custom,
|
|
1771
|
-
message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
|
|
1772
|
-
});
|
|
1773
|
-
}
|
|
1774
|
-
});
|
|
1775
|
-
async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
1776
|
-
if (!existsSync3(rootDir)) {
|
|
1777
|
-
return /* @__PURE__ */ new Map();
|
|
1778
|
-
}
|
|
1779
|
-
const out = /* @__PURE__ */ new Map();
|
|
1780
|
-
await walk(rootDir, rootDir, out);
|
|
1781
|
-
return out;
|
|
1782
|
-
}
|
|
1783
|
-
async function walk(baseDir, currentDir, out) {
|
|
1784
|
-
const entries = await fs5.readdir(currentDir, { withFileTypes: true });
|
|
1785
|
-
for (const entry2 of entries) {
|
|
1786
|
-
const full = path6.join(currentDir, entry2.name);
|
|
1787
|
-
if (entry2.isDirectory()) {
|
|
1788
|
-
await walk(baseDir, full, out);
|
|
1789
|
-
continue;
|
|
1790
|
-
}
|
|
1791
|
-
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
1792
|
-
const relative = path6.relative(baseDir, full);
|
|
1793
|
-
const name = relative.replace(/\.yml$/, "").split(path6.sep).join("/");
|
|
1794
|
-
const text = await fs5.readFile(full, "utf8");
|
|
1795
|
-
let raw;
|
|
1796
|
-
try {
|
|
1797
|
-
raw = parseYaml(text);
|
|
1798
|
-
} catch (err) {
|
|
1799
|
-
throw new Error(
|
|
1800
|
-
`Failed to parse component ${name} (${full}): ${err.message}`
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
const parsed = ComponentFileSchema.safeParse(raw);
|
|
1804
|
-
if (!parsed.success) {
|
|
1805
|
-
const issues = parsed.error.issues.map((issue) => {
|
|
1806
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1807
|
-
return ` - ${where}: ${issue.message}`;
|
|
1808
|
-
}).join("\n");
|
|
1809
|
-
throw new Error(`Invalid component ${name} (${full}):
|
|
1810
|
-
${issues}`);
|
|
1811
|
-
}
|
|
1812
|
-
out.set(name, { name, sourcePath: full, file: parsed.data });
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
function mergeComponents(resolved) {
|
|
1816
|
-
const languages = [];
|
|
1817
|
-
const services = [];
|
|
1818
|
-
const featureByRef = /* @__PURE__ */ new Map();
|
|
1819
|
-
for (const entry2 of resolved) {
|
|
1820
|
-
const c = isResolvedComponent(entry2) ? entry2.component : entry2;
|
|
1821
|
-
const version = isResolvedComponent(entry2) ? entry2.version : void 0;
|
|
1822
|
-
const ct = c.file.contributes;
|
|
1823
|
-
for (const lang of ct.languages ?? []) {
|
|
1824
|
-
const value = version !== void 0 ? `${lang}:${version}` : lang;
|
|
1825
|
-
if (!languages.includes(value)) languages.push(value);
|
|
1826
|
-
}
|
|
1827
|
-
for (const svc of ct.services ?? []) {
|
|
1828
|
-
if (!services.includes(svc)) services.push(svc);
|
|
1829
|
-
}
|
|
1830
|
-
for (const f of ct.features ?? []) {
|
|
1831
|
-
const existing = featureByRef.get(f.ref);
|
|
1832
|
-
if (!existing) {
|
|
1833
|
-
featureByRef.set(f.ref, {
|
|
1834
|
-
ref: f.ref,
|
|
1835
|
-
options: { ...f.options ?? {} }
|
|
1836
|
-
});
|
|
1837
|
-
continue;
|
|
1838
|
-
}
|
|
1839
|
-
existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
return {
|
|
1843
|
-
languages,
|
|
1844
|
-
services,
|
|
1845
|
-
features: [...featureByRef.values()]
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
1848
|
-
function isResolvedComponent(x) {
|
|
1849
|
-
return "component" in x;
|
|
1850
|
-
}
|
|
1851
|
-
function mergeFeatureOptions(a, b) {
|
|
1852
|
-
const result = { ...a };
|
|
1853
|
-
for (const [key, valueB] of Object.entries(b)) {
|
|
1854
|
-
const valueA = result[key];
|
|
1855
|
-
if (typeof valueA === "boolean" && typeof valueB === "boolean") {
|
|
1856
|
-
result[key] = valueA || valueB;
|
|
1857
|
-
continue;
|
|
1858
|
-
}
|
|
1859
|
-
result[key] = valueB;
|
|
1860
|
-
}
|
|
1861
|
-
return result;
|
|
1862
|
-
}
|
|
1863
|
-
function resolveComponents(catalog, names) {
|
|
1864
|
-
const unknown = [];
|
|
1865
|
-
const out = [];
|
|
1866
|
-
for (const raw of names) {
|
|
1867
|
-
const colon = raw.indexOf(":");
|
|
1868
|
-
const name = colon === -1 ? raw : raw.slice(0, colon);
|
|
1869
|
-
const version = colon === -1 ? void 0 : raw.slice(colon + 1);
|
|
1870
|
-
const c = catalog.get(name);
|
|
1871
|
-
if (!c) {
|
|
1872
|
-
unknown.push(raw);
|
|
1873
|
-
continue;
|
|
1874
|
-
}
|
|
1875
|
-
if (version !== void 0 && c.file.category !== "language") {
|
|
1876
|
-
throw new Error(
|
|
1877
|
-
`Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
|
|
1878
|
-
);
|
|
1879
|
-
}
|
|
1880
|
-
out.push({ component: c, ...version !== void 0 ? { version } : {} });
|
|
1881
|
-
}
|
|
1882
|
-
if (unknown.length > 0) {
|
|
1883
|
-
const available = [...catalog.keys()].sort();
|
|
1884
|
-
throw new Error(
|
|
1885
|
-
`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
1886
|
-
Available: ${available.join(", ")}.`
|
|
1887
|
-
);
|
|
1888
|
-
}
|
|
1889
|
-
return out;
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// src/proxy/dynamic.ts
|
|
1893
|
-
import { promises as fs6 } from "fs";
|
|
1894
|
-
import path7 from "path";
|
|
1895
|
-
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
1896
|
-
if (ports.length === 0) {
|
|
1897
|
-
throw new Error(
|
|
1898
|
-
`writeDynamicConfig requires at least one port. For empty port lists, call removeDynamicConfig(${JSON.stringify(name)}).`
|
|
1899
|
-
);
|
|
1900
|
-
}
|
|
1901
|
-
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
1902
|
-
await fs6.mkdir(dir, { recursive: true });
|
|
1903
|
-
const file = path7.join(dir, `${name}.yml`);
|
|
1904
|
-
await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
1905
|
-
return file;
|
|
1906
|
-
}
|
|
1907
|
-
async function removeDynamicConfig(name, opts = {}) {
|
|
1908
|
-
const file = path7.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1909
|
-
await fs6.rm(file, { force: true });
|
|
1910
|
-
}
|
|
1911
|
-
function renderDynamicConfig(name, ports) {
|
|
1912
|
-
const lines = [];
|
|
1913
|
-
lines.push("# Generated by Monoceros \u2014 do not edit by hand.");
|
|
1914
|
-
lines.push(`# Container: ${name}`);
|
|
1915
|
-
lines.push(`# Ports: ${ports.join(", ")}`);
|
|
1916
|
-
lines.push("# Traefik file-provider re-reads this on change (~100 ms);");
|
|
1917
|
-
lines.push(
|
|
1918
|
-
"# to change routing, edit container-configs/" + name + ".yml or use"
|
|
1919
|
-
);
|
|
1920
|
-
lines.push("# `monoceros add-port` / `monoceros remove-port`.");
|
|
1921
|
-
lines.push("http:");
|
|
1922
|
-
lines.push(" routers:");
|
|
1923
|
-
ports.forEach((port, idx) => {
|
|
1924
|
-
const router = `${name}-${port}`;
|
|
1925
|
-
const hostExplicit = `${name}-${port}.localhost`;
|
|
1926
|
-
const rule = idx === 0 ? `"Host(\`${name}.localhost\`) || Host(\`${hostExplicit}\`)"` : `"Host(\`${hostExplicit}\`)"`;
|
|
1927
|
-
lines.push(` ${router}:`);
|
|
1928
|
-
lines.push(` rule: ${rule}`);
|
|
1929
|
-
lines.push(` service: ${router}`);
|
|
1930
|
-
lines.push(" entryPoints:");
|
|
1931
|
-
lines.push(" - web");
|
|
1932
|
-
});
|
|
1933
|
-
lines.push(" services:");
|
|
1934
|
-
for (const port of ports) {
|
|
1935
|
-
const svc = `${name}-${port}`;
|
|
1936
|
-
lines.push(` ${svc}:`);
|
|
1937
|
-
lines.push(" loadBalancer:");
|
|
1938
|
-
lines.push(" servers:");
|
|
1939
|
-
lines.push(` - url: "http://${name}:${port}"`);
|
|
1940
|
-
}
|
|
1941
|
-
return lines.join("\n") + "\n";
|
|
1942
|
-
}
|
|
1943
|
-
function proxyUrlsFor(name, ports, hostPort = 80) {
|
|
1944
|
-
const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
|
|
1945
|
-
return ports.map((port, idx) => ({
|
|
1946
|
-
port,
|
|
1947
|
-
url: `http://${name}-${port}.localhost${portSuffix}`,
|
|
1948
|
-
isDefault: idx === 0
|
|
1949
|
-
}));
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// src/proxy/port-check.ts
|
|
1953
|
-
import { Socket } from "net";
|
|
1954
|
-
var CONNECT_TIMEOUT_MS = 750;
|
|
1955
|
-
var realPortProbe = (port) => {
|
|
1956
|
-
return new Promise((resolve) => {
|
|
1957
|
-
const socket = new Socket();
|
|
1958
|
-
let settled = false;
|
|
1959
|
-
const settle = (result) => {
|
|
1960
|
-
if (settled) return;
|
|
1961
|
-
settled = true;
|
|
1962
|
-
socket.destroy();
|
|
1963
|
-
resolve(result);
|
|
1964
|
-
};
|
|
1965
|
-
socket.setTimeout(CONNECT_TIMEOUT_MS);
|
|
1966
|
-
socket.once("connect", () => {
|
|
1967
|
-
settle({
|
|
1968
|
-
ok: false,
|
|
1969
|
-
code: "EADDRINUSE",
|
|
1970
|
-
message: `another process is listening on ${port}`
|
|
1971
|
-
});
|
|
1972
|
-
});
|
|
1973
|
-
socket.once("timeout", () => {
|
|
1974
|
-
settle({ ok: true });
|
|
1975
|
-
});
|
|
1976
|
-
socket.once("error", (err) => {
|
|
1977
|
-
const code = err.code ?? "UNKNOWN";
|
|
1978
|
-
if (code === "ECONNREFUSED") {
|
|
1979
|
-
settle({ ok: true });
|
|
1980
|
-
} else {
|
|
1981
|
-
settle({
|
|
1982
|
-
ok: false,
|
|
1983
|
-
code,
|
|
1984
|
-
message: err.message
|
|
1985
|
-
});
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
socket.connect(port, "127.0.0.1");
|
|
1568
|
+
socket.connect(port, "127.0.0.1");
|
|
1989
1569
|
});
|
|
1990
1570
|
};
|
|
1991
1571
|
async function preflightHostPort(hostPort, opts = {}) {
|
|
@@ -2110,8 +1690,8 @@ function knownServices() {
|
|
|
2110
1690
|
}
|
|
2111
1691
|
|
|
2112
1692
|
// src/create/scaffold.ts
|
|
2113
|
-
import { existsSync as
|
|
2114
|
-
import
|
|
1693
|
+
import { existsSync as existsSync3, readFileSync, promises as fs7 } from "fs";
|
|
1694
|
+
import path6 from "path";
|
|
2115
1695
|
|
|
2116
1696
|
// src/util/ref.ts
|
|
2117
1697
|
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
@@ -2282,8 +1862,8 @@ function resolveFeatures(opts) {
|
|
|
2282
1862
|
if (match) {
|
|
2283
1863
|
const name = match.name;
|
|
2284
1864
|
const checkout = workbenchCheckoutRoot();
|
|
2285
|
-
const localSourceDir = checkout ?
|
|
2286
|
-
if (localSourceDir &&
|
|
1865
|
+
const localSourceDir = checkout ? path6.join(checkout, "images", "features", name) : null;
|
|
1866
|
+
if (localSourceDir && existsSync3(localSourceDir)) {
|
|
2287
1867
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
2288
1868
|
resolved.push({
|
|
2289
1869
|
devcontainerKey: `./features/${name}`,
|
|
@@ -2307,9 +1887,9 @@ function resolveFeatures(opts) {
|
|
|
2307
1887
|
return resolved;
|
|
2308
1888
|
}
|
|
2309
1889
|
function readPersistentHomeEntries(localSourceDir) {
|
|
2310
|
-
const manifestPath =
|
|
1890
|
+
const manifestPath = path6.join(localSourceDir, "devcontainer-feature.json");
|
|
2311
1891
|
try {
|
|
2312
|
-
const text =
|
|
1892
|
+
const text = readFileSync(manifestPath, "utf8");
|
|
2313
1893
|
const parsed = JSON.parse(text);
|
|
2314
1894
|
return {
|
|
2315
1895
|
paths: filterSubpaths(parsed["x-monoceros"]?.persistentHomePaths),
|
|
@@ -2592,17 +2172,17 @@ function buildPostCreateScript(opts) {
|
|
|
2592
2172
|
return lines.join("\n") + "\n";
|
|
2593
2173
|
}
|
|
2594
2174
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
2595
|
-
const dest =
|
|
2175
|
+
const dest = path6.join(devcontainerDir, "post-create.sh");
|
|
2596
2176
|
await fs7.writeFile(dest, buildPostCreateScript(opts));
|
|
2597
2177
|
await fs7.chmod(dest, 493);
|
|
2598
2178
|
}
|
|
2599
2179
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
2600
2180
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
2601
|
-
const devcontainerDir =
|
|
2602
|
-
const monocerosDir =
|
|
2603
|
-
const projectsDir =
|
|
2604
|
-
const homeDir =
|
|
2605
|
-
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");
|
|
2606
2186
|
await fs7.mkdir(devcontainerDir, { recursive: true });
|
|
2607
2187
|
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2608
2188
|
await fs7.mkdir(projectsDir, { recursive: true });
|
|
@@ -2612,56 +2192,56 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
|
2612
2192
|
for (const svcId of opts.services) {
|
|
2613
2193
|
const def = SERVICE_CATALOG[svcId];
|
|
2614
2194
|
if (def?.dataMount) {
|
|
2615
|
-
await fs7.mkdir(
|
|
2195
|
+
await fs7.mkdir(path6.join(dataDir, def.id), { recursive: true });
|
|
2616
2196
|
}
|
|
2617
2197
|
}
|
|
2618
2198
|
}
|
|
2619
|
-
const containerGitignore =
|
|
2199
|
+
const containerGitignore = path6.join(targetDir, ".gitignore");
|
|
2620
2200
|
await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2621
|
-
const gitkeep =
|
|
2622
|
-
if (!
|
|
2201
|
+
const gitkeep = path6.join(projectsDir, ".gitkeep");
|
|
2202
|
+
if (!existsSync3(gitkeep)) {
|
|
2623
2203
|
await fs7.writeFile(gitkeep, "");
|
|
2624
2204
|
}
|
|
2625
2205
|
await fs7.writeFile(
|
|
2626
|
-
|
|
2206
|
+
path6.join(monocerosDir, ".gitignore"),
|
|
2627
2207
|
"git-credentials*\ngitconfig\n"
|
|
2628
2208
|
);
|
|
2629
2209
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
2630
2210
|
await fs7.writeFile(
|
|
2631
|
-
|
|
2211
|
+
path6.join(devcontainerDir, "devcontainer.json"),
|
|
2632
2212
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
2633
2213
|
);
|
|
2634
|
-
const featuresDir =
|
|
2635
|
-
if (
|
|
2214
|
+
const featuresDir = path6.join(devcontainerDir, "features");
|
|
2215
|
+
if (existsSync3(featuresDir)) {
|
|
2636
2216
|
await fs7.rm(featuresDir, { recursive: true, force: true });
|
|
2637
2217
|
}
|
|
2638
2218
|
const resolvedFeatures = resolveFeatures(opts);
|
|
2639
2219
|
for (const f of resolvedFeatures) {
|
|
2640
2220
|
if (!f.localSourceDir || !f.localName) continue;
|
|
2641
|
-
const dest =
|
|
2221
|
+
const dest = path6.join(featuresDir, f.localName);
|
|
2642
2222
|
await fs7.mkdir(dest, { recursive: true });
|
|
2643
2223
|
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2644
2224
|
}
|
|
2645
2225
|
for (const f of resolvedFeatures) {
|
|
2646
2226
|
for (const sub of f.persistentHomePaths) {
|
|
2647
|
-
await fs7.mkdir(
|
|
2227
|
+
await fs7.mkdir(path6.join(homeDir, sub), { recursive: true });
|
|
2648
2228
|
}
|
|
2649
2229
|
for (const entry2 of f.persistentHomeFiles) {
|
|
2650
|
-
const filePath =
|
|
2651
|
-
await fs7.mkdir(
|
|
2652
|
-
if (!
|
|
2230
|
+
const filePath = path6.join(homeDir, entry2.path);
|
|
2231
|
+
await fs7.mkdir(path6.dirname(filePath), { recursive: true });
|
|
2232
|
+
if (!existsSync3(filePath)) {
|
|
2653
2233
|
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2654
2234
|
}
|
|
2655
2235
|
}
|
|
2656
2236
|
}
|
|
2657
2237
|
await writePostCreateScript(devcontainerDir, opts);
|
|
2658
|
-
const composePath =
|
|
2238
|
+
const composePath = path6.join(devcontainerDir, "compose.yaml");
|
|
2659
2239
|
if (needsCompose(opts)) {
|
|
2660
2240
|
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2661
|
-
} else if (
|
|
2241
|
+
} else if (existsSync3(composePath)) {
|
|
2662
2242
|
await fs7.rm(composePath);
|
|
2663
2243
|
}
|
|
2664
|
-
const workspacePath =
|
|
2244
|
+
const workspacePath = path6.join(targetDir, `${opts.name}.code-workspace`);
|
|
2665
2245
|
let existingWorkspace;
|
|
2666
2246
|
try {
|
|
2667
2247
|
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
@@ -2686,25 +2266,25 @@ import {
|
|
|
2686
2266
|
} from "yaml";
|
|
2687
2267
|
|
|
2688
2268
|
// src/init/manifest.ts
|
|
2689
|
-
import { existsSync as
|
|
2690
|
-
import
|
|
2269
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
2270
|
+
import path7 from "path";
|
|
2691
2271
|
function resolveManifestPath(name, checkoutRoot) {
|
|
2692
2272
|
if (checkoutRoot) {
|
|
2693
|
-
const checkoutPath =
|
|
2273
|
+
const checkoutPath = path7.join(
|
|
2694
2274
|
checkoutRoot,
|
|
2695
2275
|
"images",
|
|
2696
2276
|
"features",
|
|
2697
2277
|
name,
|
|
2698
2278
|
"devcontainer-feature.json"
|
|
2699
2279
|
);
|
|
2700
|
-
if (
|
|
2280
|
+
if (existsSync4(checkoutPath)) return checkoutPath;
|
|
2701
2281
|
}
|
|
2702
|
-
const bundlePath =
|
|
2282
|
+
const bundlePath = path7.join(
|
|
2703
2283
|
bundledFeaturesDir(),
|
|
2704
2284
|
name,
|
|
2705
2285
|
"devcontainer-feature.json"
|
|
2706
2286
|
);
|
|
2707
|
-
if (
|
|
2287
|
+
if (existsSync4(bundlePath)) return bundlePath;
|
|
2708
2288
|
return null;
|
|
2709
2289
|
}
|
|
2710
2290
|
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
@@ -2713,7 +2293,7 @@ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot())
|
|
|
2713
2293
|
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
2714
2294
|
if (!manifestPath) return void 0;
|
|
2715
2295
|
try {
|
|
2716
|
-
const text =
|
|
2296
|
+
const text = readFileSync2(manifestPath, "utf8");
|
|
2717
2297
|
const parsed = JSON.parse(text);
|
|
2718
2298
|
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
2719
2299
|
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
@@ -3389,7 +2969,7 @@ async function tryCloneInRunningContainer(input, entry2) {
|
|
|
3389
2969
|
logger.info(
|
|
3390
2970
|
`Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
|
|
3391
2971
|
);
|
|
3392
|
-
void
|
|
2972
|
+
void path8;
|
|
3393
2973
|
}
|
|
3394
2974
|
function shquote(value) {
|
|
3395
2975
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -3608,13 +3188,13 @@ async function mutate(opts, apply) {
|
|
|
3608
3188
|
}
|
|
3609
3189
|
function defaultLogger() {
|
|
3610
3190
|
return {
|
|
3611
|
-
info: (m) =>
|
|
3612
|
-
success: (m) =>
|
|
3613
|
-
warn: (m) =>
|
|
3191
|
+
info: (m) => consola.info(m),
|
|
3192
|
+
success: (m) => consola.success(m),
|
|
3193
|
+
warn: (m) => consola.warn(m)
|
|
3614
3194
|
};
|
|
3615
3195
|
}
|
|
3616
3196
|
var defaultConfirm = async (message) => {
|
|
3617
|
-
const result = await
|
|
3197
|
+
const result = await consola.prompt(message, {
|
|
3618
3198
|
type: "confirm",
|
|
3619
3199
|
initial: false
|
|
3620
3200
|
});
|
|
@@ -3654,9 +3234,6 @@ async function syncPortsToProxy(input) {
|
|
|
3654
3234
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
3655
3235
|
logger: { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
3656
3236
|
});
|
|
3657
|
-
await kickProxyReload({
|
|
3658
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3659
|
-
});
|
|
3660
3237
|
const urls = proxyUrlsFor(input.name, allPorts, hostPort);
|
|
3661
3238
|
const lines = urls.map((u) => {
|
|
3662
3239
|
const tag = u.isDefault ? " (default)" : "";
|
|
@@ -3666,9 +3243,6 @@ async function syncPortsToProxy(input) {
|
|
|
3666
3243
|
${lines.join("\n")}`);
|
|
3667
3244
|
} else {
|
|
3668
3245
|
await removeDynamicConfig(input.name, { monocerosHome: home });
|
|
3669
|
-
await kickProxyReload({
|
|
3670
|
-
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
3671
|
-
});
|
|
3672
3246
|
await maybeStopProxy({
|
|
3673
3247
|
monocerosHome: home,
|
|
3674
3248
|
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
@@ -3705,7 +3279,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3705
3279
|
async run({ args }) {
|
|
3706
3280
|
const packages = [...getInnerArgs()];
|
|
3707
3281
|
if (packages.length === 0) {
|
|
3708
|
-
|
|
3282
|
+
consola2.error(
|
|
3709
3283
|
"No package names given. Usage: `monoceros add-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
|
|
3710
3284
|
);
|
|
3711
3285
|
process.exit(1);
|
|
@@ -3718,7 +3292,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3718
3292
|
});
|
|
3719
3293
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3720
3294
|
} catch (err) {
|
|
3721
|
-
|
|
3295
|
+
consola2.error(err instanceof Error ? err.message : String(err));
|
|
3722
3296
|
process.exit(1);
|
|
3723
3297
|
}
|
|
3724
3298
|
}
|
|
@@ -3726,7 +3300,7 @@ var addAptPackagesCommand = defineCommand({
|
|
|
3726
3300
|
|
|
3727
3301
|
// src/commands/add-feature.ts
|
|
3728
3302
|
import { defineCommand as defineCommand2 } from "citty";
|
|
3729
|
-
import { consola as
|
|
3303
|
+
import { consola as consola3 } from "consola";
|
|
3730
3304
|
var addFeatureCommand = defineCommand2({
|
|
3731
3305
|
meta: {
|
|
3732
3306
|
name: "add-feature",
|
|
@@ -3756,7 +3330,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3756
3330
|
try {
|
|
3757
3331
|
options = parseOptionsAfterDashes(getInnerArgs());
|
|
3758
3332
|
} catch (err) {
|
|
3759
|
-
|
|
3333
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3760
3334
|
process.exit(1);
|
|
3761
3335
|
}
|
|
3762
3336
|
try {
|
|
@@ -3768,7 +3342,7 @@ var addFeatureCommand = defineCommand2({
|
|
|
3768
3342
|
});
|
|
3769
3343
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3770
3344
|
} catch (err) {
|
|
3771
|
-
|
|
3345
|
+
consola3.error(err instanceof Error ? err.message : String(err));
|
|
3772
3346
|
process.exit(1);
|
|
3773
3347
|
}
|
|
3774
3348
|
}
|
|
@@ -3800,7 +3374,7 @@ function coerce(value) {
|
|
|
3800
3374
|
|
|
3801
3375
|
// src/commands/add-from-url.ts
|
|
3802
3376
|
import { defineCommand as defineCommand3 } from "citty";
|
|
3803
|
-
import { consola as
|
|
3377
|
+
import { consola as consola4 } from "consola";
|
|
3804
3378
|
var addFromUrlCommand = defineCommand3({
|
|
3805
3379
|
meta: {
|
|
3806
3380
|
name: "add-from-url",
|
|
@@ -3837,7 +3411,7 @@ var addFromUrlCommand = defineCommand3({
|
|
|
3837
3411
|
});
|
|
3838
3412
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3839
3413
|
} catch (err) {
|
|
3840
|
-
|
|
3414
|
+
consola4.error(err instanceof Error ? err.message : String(err));
|
|
3841
3415
|
process.exit(1);
|
|
3842
3416
|
}
|
|
3843
3417
|
}
|
|
@@ -3872,7 +3446,7 @@ function printSecurityWarning(url) {
|
|
|
3872
3446
|
|
|
3873
3447
|
// src/commands/add-repo.ts
|
|
3874
3448
|
import { defineCommand as defineCommand4 } from "citty";
|
|
3875
|
-
import { consola as
|
|
3449
|
+
import { consola as consola5 } from "consola";
|
|
3876
3450
|
var addRepoCommand = defineCommand4({
|
|
3877
3451
|
meta: {
|
|
3878
3452
|
name: "add-repo",
|
|
@@ -3926,7 +3500,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3926
3500
|
});
|
|
3927
3501
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3928
3502
|
} catch (err) {
|
|
3929
|
-
|
|
3503
|
+
consola5.error(err instanceof Error ? err.message : String(err));
|
|
3930
3504
|
process.exit(1);
|
|
3931
3505
|
}
|
|
3932
3506
|
}
|
|
@@ -3934,7 +3508,7 @@ var addRepoCommand = defineCommand4({
|
|
|
3934
3508
|
|
|
3935
3509
|
// src/commands/add-language.ts
|
|
3936
3510
|
import { defineCommand as defineCommand5 } from "citty";
|
|
3937
|
-
import { consola as
|
|
3511
|
+
import { consola as consola6 } from "consola";
|
|
3938
3512
|
var addLanguageCommand = defineCommand5({
|
|
3939
3513
|
meta: {
|
|
3940
3514
|
name: "add-language",
|
|
@@ -3967,207 +3541,549 @@ var addLanguageCommand = defineCommand5({
|
|
|
3967
3541
|
yes: args.yes
|
|
3968
3542
|
});
|
|
3969
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);
|
|
3970
3594
|
} catch (err) {
|
|
3971
3595
|
consola7.error(err instanceof Error ? err.message : String(err));
|
|
3972
3596
|
process.exit(1);
|
|
3973
3597
|
}
|
|
3974
3598
|
}
|
|
3975
|
-
});
|
|
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
|
+
}
|
|
3976
3746
|
|
|
3977
|
-
// src/
|
|
3978
|
-
import {
|
|
3979
|
-
import {
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
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));
|
|
3991
3796
|
},
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
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));
|
|
3997
3840
|
},
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
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));
|
|
4002
3849
|
}
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
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;
|
|
4011
3878
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
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);
|
|
4018
3900
|
});
|
|
4019
|
-
|
|
4020
|
-
}
|
|
4021
|
-
|
|
4022
|
-
|
|
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);
|
|
4023
3952
|
}
|
|
4024
3953
|
}
|
|
4025
|
-
|
|
4026
|
-
function coerceToken(raw) {
|
|
4027
|
-
const n = Number(raw);
|
|
4028
|
-
return Number.isFinite(n) ? n : raw;
|
|
3954
|
+
return [...ids];
|
|
4029
3955
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
type: "positional",
|
|
4043
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
4044
|
-
required: true
|
|
4045
|
-
},
|
|
4046
|
-
service: {
|
|
4047
|
-
type: "positional",
|
|
4048
|
-
description: "Service identifier (postgres, mysql, redis).",
|
|
4049
|
-
required: true
|
|
4050
|
-
},
|
|
4051
|
-
yes: {
|
|
4052
|
-
type: "boolean",
|
|
4053
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
4054
|
-
alias: ["y"],
|
|
4055
|
-
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()}`);
|
|
4056
3968
|
}
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
});
|
|
4065
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4066
|
-
} catch (err) {
|
|
4067
|
-
consola9.error(err instanceof Error ? err.message : String(err));
|
|
4068
|
-
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`);
|
|
4069
3976
|
}
|
|
4070
3977
|
}
|
|
4071
|
-
});
|
|
4072
|
-
|
|
4073
|
-
// src/commands/apply.ts
|
|
4074
|
-
import { defineCommand as defineCommand8 } from "citty";
|
|
4075
|
-
|
|
4076
|
-
// src/apply/index.ts
|
|
4077
|
-
import { existsSync as existsSync6, promises as fs11 } from "fs";
|
|
4078
|
-
import { consola as consola11 } from "consola";
|
|
4079
|
-
|
|
4080
|
-
// src/config/state.ts
|
|
4081
|
-
import { promises as fs9 } from "fs";
|
|
4082
|
-
import path11 from "path";
|
|
4083
|
-
function buildStateFile(opts) {
|
|
4084
|
-
return {
|
|
4085
|
-
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
4086
|
-
origin: opts.origin,
|
|
4087
|
-
monocerosCliVersion: opts.cliVersion,
|
|
4088
|
-
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
4089
|
-
};
|
|
3978
|
+
opts.logger.info(`[${tag}] docker cleanup done`);
|
|
3979
|
+
return { exitCode: rmExit, removedIds: ids };
|
|
4090
3980
|
}
|
|
4091
|
-
function
|
|
4092
|
-
return path11.
|
|
3981
|
+
function composeProjectName(root) {
|
|
3982
|
+
return `${path11.basename(root)}_devcontainer`;
|
|
4093
3983
|
}
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
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
|
+
);
|
|
4100
3995
|
}
|
|
3996
|
+
return { composeFile, projectName: composeProjectName(root) };
|
|
4101
3997
|
}
|
|
4102
|
-
async function
|
|
4103
|
-
const
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
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
|
|
4108
4012
|
);
|
|
4109
4013
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
const containerOpts = Object.fromEntries(
|
|
4117
|
-
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`
|
|
4118
4020
|
);
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
// single-segment default (`https://.../foo.git` → `foo`),
|
|
4144
|
-
// which lands the clone at `projects/foo/`.
|
|
4145
|
-
path: r.path ?? deriveRepoName(r.url),
|
|
4146
|
-
// gitUser is forwarded only when BOTH name + email are set.
|
|
4147
|
-
// The relaxed GitUserSchema accepts nullable / empty strings
|
|
4148
|
-
// (so a yml placeholder `name:` parses without error), so we
|
|
4149
|
-
// re-check here before downstream code, which expects both
|
|
4150
|
-
// values to be non-empty.
|
|
4151
|
-
...r.git?.user?.name && r.git.user.email ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
4152
|
-
...r.provider ? { provider: r.provider } : {}
|
|
4153
|
-
}));
|
|
4154
|
-
}
|
|
4155
|
-
const routingPorts = config.routing?.ports ?? [];
|
|
4156
|
-
if (routingPorts.length > 0) {
|
|
4157
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4158
|
-
const ports = [];
|
|
4159
|
-
for (const entry2 of routingPorts) {
|
|
4160
|
-
const n = portNumber(entry2);
|
|
4161
|
-
if (seen.has(n)) continue;
|
|
4162
|
-
seen.add(n);
|
|
4163
|
-
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;
|
|
4164
4045
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4046
|
+
return runStart({
|
|
4047
|
+
root,
|
|
4048
|
+
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
4049
|
+
logger
|
|
4050
|
+
});
|
|
4169
4051
|
}
|
|
4170
|
-
|
|
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
|
+
);
|
|
4171
4087
|
}
|
|
4172
4088
|
|
|
4173
4089
|
// src/devcontainer/repo-reachability.ts
|
|
@@ -4269,7 +4185,7 @@ function formatUnreachableReposError(failures) {
|
|
|
4269
4185
|
}
|
|
4270
4186
|
lines.push("");
|
|
4271
4187
|
}
|
|
4272
|
-
lines.push(`Then re-run ${
|
|
4188
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
4273
4189
|
return lines.join("\n");
|
|
4274
4190
|
}
|
|
4275
4191
|
function headerForKind(kind) {
|
|
@@ -4352,14 +4268,14 @@ function formatRootlessNotSupportedError() {
|
|
|
4352
4268
|
``,
|
|
4353
4269
|
`To fix, switch back to standard rootful Docker:`,
|
|
4354
4270
|
``,
|
|
4355
|
-
|
|
4271
|
+
cyan2(
|
|
4356
4272
|
` systemctl --user stop docker.service docker.socket 2>/dev/null || true`
|
|
4357
4273
|
),
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
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`),
|
|
4363
4279
|
``,
|
|
4364
4280
|
`If you added DOCKER_HOST or DOCKER_CONTEXT to ~/.bashrc /`,
|
|
4365
4281
|
`~/.profile (the rootless setup may have suggested it), remove`,
|
|
@@ -4664,7 +4580,7 @@ ${sectionLine(label)}
|
|
|
4664
4580
|
section("Container");
|
|
4665
4581
|
const featureRefs = parsed.config.features.map((f) => f.ref);
|
|
4666
4582
|
if (featureRefs.length > 0) {
|
|
4667
|
-
logger.info(`Features: ${featureRefs.map((r) =>
|
|
4583
|
+
logger.info(`Features: ${featureRefs.map((r) => cyan2(r)).join(", ")}`);
|
|
4668
4584
|
}
|
|
4669
4585
|
logger.info(
|
|
4670
4586
|
dim(
|
|
@@ -4687,14 +4603,8 @@ ${sectionLine(label)}
|
|
|
4687
4603
|
hostPort: proxyHostPort(globalConfig),
|
|
4688
4604
|
logger
|
|
4689
4605
|
});
|
|
4690
|
-
await kickProxyReload({
|
|
4691
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4692
|
-
});
|
|
4693
4606
|
} else {
|
|
4694
4607
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
4695
|
-
await kickProxyReload({
|
|
4696
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
4697
|
-
});
|
|
4698
4608
|
}
|
|
4699
4609
|
} catch (err) {
|
|
4700
4610
|
logger.warn?.(
|
|
@@ -4709,7 +4619,7 @@ ${sectionLine(label)}
|
|
|
4709
4619
|
});
|
|
4710
4620
|
if (exitCode === 0) {
|
|
4711
4621
|
section("Next steps");
|
|
4712
|
-
logger.info(` ${
|
|
4622
|
+
logger.info(` ${cyan2(`monoceros shell ${opts.name}`)}`);
|
|
4713
4623
|
}
|
|
4714
4624
|
return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
|
|
4715
4625
|
}
|
|
@@ -4807,7 +4717,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4807
4717
|
}
|
|
4808
4718
|
|
|
4809
4719
|
// src/version.ts
|
|
4810
|
-
var CLI_VERSION = true ? "1.
|
|
4720
|
+
var CLI_VERSION = true ? "1.12.0" : "dev";
|
|
4811
4721
|
|
|
4812
4722
|
// src/commands/_dispatch.ts
|
|
4813
4723
|
import { consola as consola12 } from "consola";
|
|
@@ -6269,7 +6179,7 @@ async function runRemove(opts) {
|
|
|
6269
6179
|
projectName,
|
|
6270
6180
|
filters: [
|
|
6271
6181
|
`label=com.docker.compose.project=${projectName}`,
|
|
6272
|
-
`label=devcontainer.local_folder=${
|
|
6182
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
6273
6183
|
`name=^${projectName}-`,
|
|
6274
6184
|
`name=^vsc-${opts.name}-`
|
|
6275
6185
|
],
|
|
@@ -6337,9 +6247,6 @@ async function runRemove(opts) {
|
|
|
6337
6247
|
}
|
|
6338
6248
|
try {
|
|
6339
6249
|
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
6340
|
-
await kickProxyReload({
|
|
6341
|
-
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
6342
|
-
});
|
|
6343
6250
|
} catch (err) {
|
|
6344
6251
|
logger.warn?.(
|
|
6345
6252
|
`Could not remove Traefik dynamic config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
@@ -7079,10 +6986,7 @@ async function lookupContainerNetwork(args) {
|
|
|
7079
6986
|
"ps",
|
|
7080
6987
|
"-q",
|
|
7081
6988
|
"--filter",
|
|
7082
|
-
|
|
7083
|
-
// when stamping the label, docker filter is byte-exact. No-op
|
|
7084
|
-
// off Windows.
|
|
7085
|
-
`label=devcontainer.local_folder=${dockerLocalFolderLabel(args.containerRoot)}`
|
|
6989
|
+
`label=devcontainer.local_folder=${args.containerRoot}`
|
|
7086
6990
|
]);
|
|
7087
6991
|
if (psResult.exitCode !== 0) {
|
|
7088
6992
|
throw new Error(
|
|
@@ -7404,7 +7308,6 @@ var main = defineCommand30({
|
|
|
7404
7308
|
|
|
7405
7309
|
// src/bin.ts
|
|
7406
7310
|
bootstrapDockerGroup();
|
|
7407
|
-
bootstrapWslBackend();
|
|
7408
7311
|
consumeInnerArgsFromProcessArgv();
|
|
7409
7312
|
async function entry() {
|
|
7410
7313
|
if (await maybeRenderHelp(process.argv.slice(2), main)) {
|