@getmonoceros/workbench 1.7.4 → 1.8.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 +1992 -1328
- package/dist/bin.js.map +1 -1
- package/features/atlassian/devcontainer-feature.json +9 -12
- package/features/claude-code/devcontainer-feature.json +5 -7
- package/features/github-cli/devcontainer-feature.json +4 -6
- package/package.json +1 -1
- package/templates/monoceros-config.sample.yml +54 -72
package/dist/bin.js
CHANGED
|
@@ -251,25 +251,25 @@ function detectHelpRequest(argv, main2) {
|
|
|
251
251
|
const separatorIdx = argv.indexOf("--");
|
|
252
252
|
if (helpIdx === -1) return null;
|
|
253
253
|
if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
|
|
254
|
-
const
|
|
254
|
+
const path16 = [];
|
|
255
255
|
const tokens = argv.slice(
|
|
256
256
|
0,
|
|
257
257
|
separatorIdx === -1 ? argv.length : separatorIdx
|
|
258
258
|
);
|
|
259
259
|
let cursor = main2;
|
|
260
260
|
const mainName = (main2.meta ?? {}).name ?? "monoceros";
|
|
261
|
-
|
|
261
|
+
path16.push(mainName);
|
|
262
262
|
for (const tok of tokens) {
|
|
263
263
|
if (tok.startsWith("-")) continue;
|
|
264
264
|
const subs = cursor.subCommands ?? {};
|
|
265
265
|
if (tok in subs) {
|
|
266
266
|
cursor = subs[tok];
|
|
267
|
-
|
|
267
|
+
path16.push(tok);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
271
271
|
}
|
|
272
|
-
return { path:
|
|
272
|
+
return { path: path16, cmd: cursor };
|
|
273
273
|
}
|
|
274
274
|
async function maybeRenderHelp(argv, main2) {
|
|
275
275
|
const hit = detectHelpRequest(argv, main2);
|
|
@@ -308,9 +308,10 @@ import { defineCommand } from "citty";
|
|
|
308
308
|
import { consola as consola2 } from "consola";
|
|
309
309
|
|
|
310
310
|
// src/modify/index.ts
|
|
311
|
-
import { promises as
|
|
311
|
+
import { promises as fs7 } from "fs";
|
|
312
312
|
import { consola } from "consola";
|
|
313
313
|
import { createPatch } from "diff";
|
|
314
|
+
import path6 from "path";
|
|
314
315
|
|
|
315
316
|
// src/config/io.ts
|
|
316
317
|
import { promises as fs } from "fs";
|
|
@@ -346,11 +347,7 @@ var KNOWN_PROVIDER_HOSTS = {
|
|
|
346
347
|
"bitbucket.org": "bitbucket"
|
|
347
348
|
};
|
|
348
349
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
349
|
-
var FeatureOptionValueSchema = z.union([
|
|
350
|
-
z.string(),
|
|
351
|
-
z.number(),
|
|
352
|
-
z.boolean()
|
|
353
|
-
]);
|
|
350
|
+
var FeatureOptionValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]).transform((v) => v === null ? "" : v);
|
|
354
351
|
var FeatureEntrySchema = z.object({
|
|
355
352
|
ref: z.string().regex(
|
|
356
353
|
FEATURE_REF_RE,
|
|
@@ -358,9 +355,14 @@ var FeatureEntrySchema = z.object({
|
|
|
358
355
|
),
|
|
359
356
|
options: z.record(z.string(), FeatureOptionValueSchema).optional()
|
|
360
357
|
});
|
|
358
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
361
359
|
var GitUserSchema = z.object({
|
|
362
|
-
name: z.string().min(1),
|
|
363
|
-
email: z.
|
|
360
|
+
name: z.union([z.literal(""), z.null(), z.string().min(1)]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0),
|
|
361
|
+
email: z.union([
|
|
362
|
+
z.literal(""),
|
|
363
|
+
z.null(),
|
|
364
|
+
z.string().regex(EMAIL_RE, "Invalid email")
|
|
365
|
+
]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0)
|
|
364
366
|
});
|
|
365
367
|
var RepoEntrySchema = z.object({
|
|
366
368
|
url: z.string().regex(
|
|
@@ -567,222 +569,737 @@ function prettyPath(p) {
|
|
|
567
569
|
return p;
|
|
568
570
|
}
|
|
569
571
|
|
|
570
|
-
// src/
|
|
572
|
+
// src/devcontainer/credentials.ts
|
|
573
|
+
import { spawn } from "child_process";
|
|
571
574
|
import { promises as fs2 } from "fs";
|
|
572
|
-
import
|
|
573
|
-
import { parseDocument as parseDocument2 } from "yaml";
|
|
574
|
-
var SCHEMA_VERSION = 1;
|
|
575
|
-
var MonocerosConfigSchema = z2.object({
|
|
576
|
-
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
577
|
-
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
578
|
-
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
579
|
-
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
580
|
-
// in that case; without .nullish() the schema would reject it and
|
|
581
|
-
// we'd be back to forcing builders to comment-juggle three lines.
|
|
582
|
-
defaults: z2.object({
|
|
583
|
-
// .nullish() (not just .optional()) so the sample yml can leave
|
|
584
|
-
// `git:` uncommented as a category marker — YAML produces
|
|
585
|
-
// `git: null` for an empty mapping, which zod's plain
|
|
586
|
-
// `.optional()` would reject.
|
|
587
|
-
git: z2.object({
|
|
588
|
-
user: GitUserSchema.optional()
|
|
589
|
-
}).nullish(),
|
|
590
|
-
// .nullish() for the same reason as `git` — the sample keeps
|
|
591
|
-
// `features:` uncommented as a category marker.
|
|
592
|
-
features: z2.record(
|
|
593
|
-
z2.string().regex(
|
|
594
|
-
REGEX.featureRef,
|
|
595
|
-
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
596
|
-
),
|
|
597
|
-
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
598
|
-
).nullish()
|
|
599
|
-
}).nullish(),
|
|
600
|
-
// Machine-global routing settings — one Traefik per builder, so
|
|
601
|
-
// host-port and similar live here rather than in any container yml.
|
|
602
|
-
// See ADR 0007.
|
|
603
|
-
routing: z2.object({
|
|
604
|
-
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
605
|
-
"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>/."
|
|
606
|
-
)
|
|
607
|
-
}).nullish()
|
|
608
|
-
});
|
|
609
|
-
async function readMonocerosConfig(opts = {}) {
|
|
610
|
-
const home = opts.monocerosHome ?? monocerosHome();
|
|
611
|
-
const filePath = monocerosConfigPath(home);
|
|
612
|
-
let text;
|
|
613
|
-
try {
|
|
614
|
-
text = await fs2.readFile(filePath, "utf8");
|
|
615
|
-
} catch {
|
|
616
|
-
return void 0;
|
|
617
|
-
}
|
|
618
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
619
|
-
if (doc.errors.length > 0) {
|
|
620
|
-
throw new Error(
|
|
621
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
622
|
-
);
|
|
623
|
-
}
|
|
624
|
-
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
625
|
-
if (!result.success) {
|
|
626
|
-
const issues = result.error.issues.map((issue) => {
|
|
627
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
628
|
-
return ` - ${where}: ${issue.message}`;
|
|
629
|
-
}).join("\n");
|
|
630
|
-
throw new Error(
|
|
631
|
-
`Invalid ${filePath}:
|
|
632
|
-
${issues}
|
|
575
|
+
import path2 from "path";
|
|
633
576
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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;
|
|
641
586
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return
|
|
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);
|
|
645
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;
|
|
646
606
|
|
|
647
|
-
// src/
|
|
648
|
-
|
|
649
|
-
import { promises as fs3 } from "fs";
|
|
650
|
-
import path2 from "path";
|
|
651
|
-
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
652
|
-
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
653
|
-
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
654
|
-
var defaultDockerExec = (args) => {
|
|
607
|
+
// src/devcontainer/credentials.ts
|
|
608
|
+
var realGitCredentialFill = (input) => {
|
|
655
609
|
return new Promise((resolve, reject) => {
|
|
656
|
-
const child = spawn("
|
|
657
|
-
stdio: ["
|
|
610
|
+
const child = spawn("git", ["credential", "fill"], {
|
|
611
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
612
|
+
env: {
|
|
613
|
+
...process.env,
|
|
614
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
615
|
+
GIT_ASKPASS: "",
|
|
616
|
+
SSH_ASKPASS: ""
|
|
617
|
+
}
|
|
658
618
|
});
|
|
659
619
|
let stdout = "";
|
|
660
|
-
let stderr = "";
|
|
661
620
|
child.stdout.on("data", (chunk) => {
|
|
662
621
|
stdout += chunk.toString();
|
|
663
622
|
});
|
|
664
|
-
child.stderr.on("data", (chunk) => {
|
|
665
|
-
stderr += chunk.toString();
|
|
666
|
-
});
|
|
667
623
|
child.on("error", reject);
|
|
668
|
-
child.on(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
);
|
|
624
|
+
child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
|
|
625
|
+
child.stdin.write(input);
|
|
626
|
+
child.stdin.end();
|
|
672
627
|
});
|
|
673
628
|
};
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
629
|
+
function resolveProvider(host, explicit) {
|
|
630
|
+
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
631
|
+
if (canonical) return canonical;
|
|
632
|
+
return explicit ?? "unknown";
|
|
677
633
|
}
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
`Could not create docker network ${PROXY_NETWORK_NAME}: ${create.stderr.trim() || `exit ${create.exitCode}`}`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const state = await docker([
|
|
692
|
-
"inspect",
|
|
693
|
-
"--format",
|
|
694
|
-
"{{.State.Running}}",
|
|
695
|
-
PROXY_CONTAINER_NAME
|
|
696
|
-
]);
|
|
697
|
-
if (state.exitCode === 0) {
|
|
698
|
-
if (state.stdout.trim() === "true") return;
|
|
699
|
-
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
700
|
-
if (start.exitCode !== 0) {
|
|
701
|
-
throw new Error(
|
|
702
|
-
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
703
|
-
);
|
|
634
|
+
function uniqueHttpsHosts(repos) {
|
|
635
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
636
|
+
for (const repo of repos) {
|
|
637
|
+
if (!repo.url.startsWith("https://")) continue;
|
|
638
|
+
let host;
|
|
639
|
+
try {
|
|
640
|
+
host = new URL(repo.url).hostname;
|
|
641
|
+
} catch {
|
|
642
|
+
continue;
|
|
704
643
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const hostPort = opts.hostPort ?? 80;
|
|
708
|
-
const run = await docker([
|
|
709
|
-
"run",
|
|
710
|
-
"-d",
|
|
711
|
-
"--name",
|
|
712
|
-
PROXY_CONTAINER_NAME,
|
|
713
|
-
"--network",
|
|
714
|
-
PROXY_NETWORK_NAME,
|
|
715
|
-
"-p",
|
|
716
|
-
`${hostPort}:80`,
|
|
717
|
-
"-v",
|
|
718
|
-
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
719
|
-
"--label",
|
|
720
|
-
"monoceros.role=proxy",
|
|
721
|
-
TRAEFIK_IMAGE,
|
|
722
|
-
"--entrypoints.web.address=:80",
|
|
723
|
-
"--providers.file.directory=/etc/traefik/dynamic",
|
|
724
|
-
"--providers.file.watch=true",
|
|
725
|
-
"--providers.docker=false",
|
|
726
|
-
"--api.dashboard=false",
|
|
727
|
-
"--log.level=INFO"
|
|
728
|
-
]);
|
|
729
|
-
if (run.exitCode !== 0) {
|
|
730
|
-
throw new Error(
|
|
731
|
-
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
opts.logger?.info(
|
|
735
|
-
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
async function maybeStopProxy(opts = {}) {
|
|
739
|
-
const docker = opts.docker ?? realDocker;
|
|
740
|
-
const logger = opts.logger;
|
|
741
|
-
const inspect = await docker([
|
|
742
|
-
"network",
|
|
743
|
-
"inspect",
|
|
744
|
-
PROXY_NETWORK_NAME,
|
|
745
|
-
"--format",
|
|
746
|
-
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
747
|
-
]);
|
|
748
|
-
if (inspect.exitCode !== 0) {
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
752
|
-
if (others.length > 0) return;
|
|
753
|
-
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
754
|
-
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
755
|
-
if (netRm.exitCode !== 0) {
|
|
756
|
-
logger?.warn?.(
|
|
757
|
-
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
758
|
-
);
|
|
759
|
-
return;
|
|
644
|
+
if (byHost.has(host)) continue;
|
|
645
|
+
byHost.set(host, { host, provider: resolveProvider(host, repo.provider) });
|
|
760
646
|
}
|
|
761
|
-
|
|
762
|
-
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
763
|
-
);
|
|
647
|
+
return [...byHost.values()];
|
|
764
648
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
649
|
+
function installCommandForOS(opts) {
|
|
650
|
+
switch (process.platform) {
|
|
651
|
+
case "darwin":
|
|
652
|
+
return cyan2(opts.brew);
|
|
653
|
+
case "win32":
|
|
654
|
+
return cyan2(opts.winget);
|
|
655
|
+
default:
|
|
656
|
+
if (opts.linuxBrew) return cyan2(opts.linuxBrew);
|
|
657
|
+
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
774
658
|
}
|
|
775
|
-
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
776
|
-
await fs4.mkdir(dir, { recursive: true });
|
|
777
|
-
const file = path3.join(dir, `${name}.yml`);
|
|
778
|
-
await fs4.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
779
|
-
return file;
|
|
780
|
-
}
|
|
781
|
-
async function removeDynamicConfig(name, opts = {}) {
|
|
782
|
-
const file = path3.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
783
|
-
await fs4.rm(file, { force: true });
|
|
784
659
|
}
|
|
785
|
-
function
|
|
660
|
+
function providerSetupHint(host, provider) {
|
|
661
|
+
if (provider === "github") {
|
|
662
|
+
const isSaas = host.toLowerCase() === "github.com";
|
|
663
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
664
|
+
const install = installCommandForOS({
|
|
665
|
+
brew: "brew install gh",
|
|
666
|
+
winget: "winget install --id GitHub.cli",
|
|
667
|
+
linuxBrew: "brew install gh",
|
|
668
|
+
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
669
|
+
});
|
|
670
|
+
return {
|
|
671
|
+
title: `${host} \u2014 GitHub`,
|
|
672
|
+
body: [
|
|
673
|
+
"Install the GitHub CLI:",
|
|
674
|
+
install,
|
|
675
|
+
"",
|
|
676
|
+
"Then run once:",
|
|
677
|
+
cyan2(`gh auth login${hostArg}`),
|
|
678
|
+
cyan2(`gh auth setup-git${hostArg}`),
|
|
679
|
+
"",
|
|
680
|
+
"`gh auth login` walks through OAuth in your browser.",
|
|
681
|
+
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
682
|
+
].join("\n")
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (provider === "gitlab") {
|
|
686
|
+
const isSaas = host.toLowerCase() === "gitlab.com";
|
|
687
|
+
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
688
|
+
const install = installCommandForOS({
|
|
689
|
+
brew: "brew install glab",
|
|
690
|
+
winget: "winget install --id GLab.GLab",
|
|
691
|
+
linuxBrew: "brew install glab",
|
|
692
|
+
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
title: `${host} \u2014 GitLab`,
|
|
696
|
+
body: [
|
|
697
|
+
"Install the GitLab CLI (glab):",
|
|
698
|
+
install,
|
|
699
|
+
"",
|
|
700
|
+
"Then run once:",
|
|
701
|
+
cyan2(`glab auth login${hostArg}`),
|
|
702
|
+
"",
|
|
703
|
+
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
704
|
+
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
705
|
+
"configures itself as the git credential helper."
|
|
706
|
+
].join("\n")
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (provider === "bitbucket") {
|
|
710
|
+
const isCloud = host.toLowerCase() === "bitbucket.org";
|
|
711
|
+
if (isCloud) {
|
|
712
|
+
return {
|
|
713
|
+
title: `${host} \u2014 Bitbucket Cloud`,
|
|
714
|
+
body: [
|
|
715
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
716
|
+
"is a manual one-time setup. Generate an Atlassian API token at",
|
|
717
|
+
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
718
|
+
"",
|
|
719
|
+
"Then store it via your OS credential helper:",
|
|
720
|
+
cyan2(
|
|
721
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
|
|
722
|
+
)
|
|
723
|
+
].join("\n")
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
title: `${host} \u2014 Bitbucket Data Center`,
|
|
728
|
+
body: [
|
|
729
|
+
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
730
|
+
"is a manual one-time setup. Generate a personal HTTP access",
|
|
731
|
+
`token in your Bitbucket UI: profile picture (top right on ${host})`,
|
|
732
|
+
"\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
|
|
733
|
+
"at least repo-read + repo-write scopes for the repos you need.",
|
|
734
|
+
"",
|
|
735
|
+
"Then store it via your OS credential helper:",
|
|
736
|
+
cyan2(
|
|
737
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
738
|
+
)
|
|
739
|
+
].join("\n")
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
title: `${host} \u2014 Gitea`,
|
|
744
|
+
body: [
|
|
745
|
+
"Gitea has no first-party CLI helper for git-credentials (the",
|
|
746
|
+
"`tea` CLI logs into its own config, not into your git credential",
|
|
747
|
+
"helper), so this is a manual one-time setup. Generate an access",
|
|
748
|
+
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
749
|
+
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
750
|
+
"least the `read:repository` scope (add `write:repository` if you",
|
|
751
|
+
"need push from the container).",
|
|
752
|
+
"",
|
|
753
|
+
"Then store it via your OS credential helper:",
|
|
754
|
+
cyan2(
|
|
755
|
+
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
756
|
+
)
|
|
757
|
+
].join("\n")
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function parseCredentialFillOutput(output) {
|
|
761
|
+
const result = {};
|
|
762
|
+
for (const line of output.split("\n")) {
|
|
763
|
+
const eqIdx = line.indexOf("=");
|
|
764
|
+
if (eqIdx <= 0) continue;
|
|
765
|
+
const key = line.slice(0, eqIdx);
|
|
766
|
+
const value = line.slice(eqIdx + 1);
|
|
767
|
+
if (key === "username") result.username = value;
|
|
768
|
+
if (key === "password") result.password = value;
|
|
769
|
+
}
|
|
770
|
+
return result;
|
|
771
|
+
}
|
|
772
|
+
function formatCredentialLine(host, username, password) {
|
|
773
|
+
const encUser = encodeURIComponent(username);
|
|
774
|
+
const encPass = encodeURIComponent(password);
|
|
775
|
+
return `https://${encUser}:${encPass}@${host}`;
|
|
776
|
+
}
|
|
777
|
+
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
778
|
+
const credsDir = path2.join(devContainerRoot, ".monoceros");
|
|
779
|
+
const credentialsPath = path2.join(credsDir, "git-credentials");
|
|
780
|
+
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
781
|
+
const logger = options.logger ?? { info: () => {
|
|
782
|
+
}, warn: () => {
|
|
783
|
+
} };
|
|
784
|
+
const lines = [];
|
|
785
|
+
const perHost = [];
|
|
786
|
+
for (const { host, provider } of hosts) {
|
|
787
|
+
if (provider === "unknown") {
|
|
788
|
+
perHost.push({
|
|
789
|
+
host,
|
|
790
|
+
provider: "github",
|
|
791
|
+
// placeholder — never rendered because pre-flight already bailed
|
|
792
|
+
status: "no-credentials",
|
|
793
|
+
detail: "provider not declared (internal: should not reach here)"
|
|
794
|
+
});
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
logger.info(`Fetching credentials for ${host} from host git\u2026`);
|
|
798
|
+
const input = `protocol=https
|
|
799
|
+
host=${host}
|
|
800
|
+
|
|
801
|
+
`;
|
|
802
|
+
let result;
|
|
803
|
+
try {
|
|
804
|
+
result = await spawnFn(input);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
807
|
+
perHost.push({ host, provider, status: "spawn-error", detail });
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (result.exitCode !== 0) {
|
|
811
|
+
perHost.push({
|
|
812
|
+
host,
|
|
813
|
+
provider,
|
|
814
|
+
status: "non-zero-exit",
|
|
815
|
+
detail: `exit code ${result.exitCode}`
|
|
816
|
+
});
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const { username, password } = parseCredentialFillOutput(result.stdout);
|
|
820
|
+
if (!username || !password) {
|
|
821
|
+
perHost.push({
|
|
822
|
+
host,
|
|
823
|
+
provider,
|
|
824
|
+
status: "no-credentials",
|
|
825
|
+
detail: "host credential helper returned no username/password"
|
|
826
|
+
});
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
lines.push(formatCredentialLine(host, username, password));
|
|
830
|
+
perHost.push({ host, provider, status: "ok", detail: "" });
|
|
831
|
+
}
|
|
832
|
+
await fs2.mkdir(credsDir, { recursive: true });
|
|
833
|
+
await fs2.writeFile(
|
|
834
|
+
credentialsPath,
|
|
835
|
+
lines.join("\n") + (lines.length > 0 ? "\n" : ""),
|
|
836
|
+
{
|
|
837
|
+
mode: 384
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
return {
|
|
841
|
+
hostsWritten: lines.length,
|
|
842
|
+
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
843
|
+
perHost,
|
|
844
|
+
credentialsPath
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function formatMissingCredentialsError(missing) {
|
|
848
|
+
if (missing.length === 1) {
|
|
849
|
+
const m = missing[0];
|
|
850
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
851
|
+
return [
|
|
852
|
+
`Missing Git credentials: ${hint.title}`,
|
|
853
|
+
"",
|
|
854
|
+
hint.body,
|
|
855
|
+
"",
|
|
856
|
+
`Then re-run ${cyan2("monoceros apply")}.`
|
|
857
|
+
].join("\n");
|
|
858
|
+
}
|
|
859
|
+
const lines = [
|
|
860
|
+
`Missing Git credentials for ${missing.length} hosts:`,
|
|
861
|
+
""
|
|
862
|
+
];
|
|
863
|
+
for (const m of missing) {
|
|
864
|
+
const hint = providerSetupHint(m.host, m.provider);
|
|
865
|
+
lines.push(hint.title);
|
|
866
|
+
lines.push("");
|
|
867
|
+
lines.push(hint.body);
|
|
868
|
+
lines.push("");
|
|
869
|
+
}
|
|
870
|
+
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
871
|
+
return lines.join("\n");
|
|
872
|
+
}
|
|
873
|
+
function formatUnknownProviderError(hosts) {
|
|
874
|
+
const sorted = [...new Set(hosts)].sort();
|
|
875
|
+
const lines = [
|
|
876
|
+
sorted.length === 1 ? `Unknown Git provider for host ${sorted[0]}.` : `Unknown Git provider for ${sorted.length} hosts: ${sorted.join(", ")}.`,
|
|
877
|
+
"",
|
|
878
|
+
"Monoceros auto-detects only github.com / gitlab.com / bitbucket.org.",
|
|
879
|
+
"For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
|
|
880
|
+
"declare the provider explicitly in the yml. Edit the repo entry:",
|
|
881
|
+
"",
|
|
882
|
+
cyan2(" repos:"),
|
|
883
|
+
cyan2(` - url: https://${sorted[0]}/\u2026`),
|
|
884
|
+
cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
|
|
885
|
+
"",
|
|
886
|
+
`Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
|
|
887
|
+
];
|
|
888
|
+
return lines.join("\n");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/devcontainer/locate-running.ts
|
|
892
|
+
import { spawn as spawn2 } from "child_process";
|
|
893
|
+
var realDockerLookup = (args) => {
|
|
894
|
+
return new Promise((resolve, reject) => {
|
|
895
|
+
const child = spawn2("docker", args, {
|
|
896
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
897
|
+
});
|
|
898
|
+
let stdout = "";
|
|
899
|
+
let stderr = "";
|
|
900
|
+
child.stdout.on("data", (chunk) => {
|
|
901
|
+
stdout += chunk.toString();
|
|
902
|
+
});
|
|
903
|
+
child.stderr.on("data", (chunk) => {
|
|
904
|
+
stderr += chunk.toString();
|
|
905
|
+
});
|
|
906
|
+
child.on("error", reject);
|
|
907
|
+
child.on(
|
|
908
|
+
"exit",
|
|
909
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
|
|
914
|
+
const docker = opts.docker ?? realDockerLookup;
|
|
915
|
+
const result = await docker([
|
|
916
|
+
"ps",
|
|
917
|
+
"-q",
|
|
918
|
+
"--filter",
|
|
919
|
+
`label=devcontainer.local_folder=${containerPath}`,
|
|
920
|
+
"--filter",
|
|
921
|
+
"status=running"
|
|
922
|
+
]);
|
|
923
|
+
if (result.exitCode !== 0) return null;
|
|
924
|
+
const ids = result.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
925
|
+
return ids[0] ?? null;
|
|
926
|
+
}
|
|
927
|
+
var realContainerExec = (containerId, argv) => {
|
|
928
|
+
return new Promise((resolve, reject) => {
|
|
929
|
+
const child = spawn2("docker", ["exec", containerId, ...argv], {
|
|
930
|
+
// Inherit stdio so live git output reaches the user.
|
|
931
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
932
|
+
});
|
|
933
|
+
child.on("error", reject);
|
|
934
|
+
child.on("exit", (code) => resolve({ exitCode: code ?? 0 }));
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
// src/config/global.ts
|
|
939
|
+
import { promises as fs3 } from "fs";
|
|
940
|
+
import { z as z2 } from "zod";
|
|
941
|
+
import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
|
|
942
|
+
var SCHEMA_VERSION = 1;
|
|
943
|
+
var MonocerosConfigSchema = z2.object({
|
|
944
|
+
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
945
|
+
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
946
|
+
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
947
|
+
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
948
|
+
// in that case; without .nullish() the schema would reject it and
|
|
949
|
+
// we'd be back to forcing builders to comment-juggle three lines.
|
|
950
|
+
defaults: z2.object({
|
|
951
|
+
// .nullish() (not just .optional()) so the sample yml can leave
|
|
952
|
+
// `git:` uncommented as a category marker — YAML produces
|
|
953
|
+
// `git: null` for an empty mapping, which zod's plain
|
|
954
|
+
// `.optional()` would reject.
|
|
955
|
+
git: z2.object({
|
|
956
|
+
user: GitUserSchema.optional()
|
|
957
|
+
}).nullish(),
|
|
958
|
+
// .nullish() for the same reason as `git` — the sample keeps
|
|
959
|
+
// `features:` uncommented as a category marker.
|
|
960
|
+
features: z2.record(
|
|
961
|
+
z2.string().regex(
|
|
962
|
+
REGEX.featureRef,
|
|
963
|
+
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
964
|
+
),
|
|
965
|
+
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
966
|
+
).nullish()
|
|
967
|
+
}).nullish(),
|
|
968
|
+
// Machine-global routing settings — one Traefik per builder, so
|
|
969
|
+
// host-port and similar live here rather than in any container yml.
|
|
970
|
+
// See ADR 0007.
|
|
971
|
+
routing: z2.object({
|
|
972
|
+
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
973
|
+
"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>/."
|
|
974
|
+
)
|
|
975
|
+
}).nullish()
|
|
976
|
+
});
|
|
977
|
+
async function readMonocerosConfig(opts = {}) {
|
|
978
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
979
|
+
const filePath = monocerosConfigPath(home);
|
|
980
|
+
let text;
|
|
981
|
+
try {
|
|
982
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
983
|
+
} catch {
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
987
|
+
if (doc.errors.length > 0) {
|
|
988
|
+
throw new Error(
|
|
989
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
993
|
+
if (!result.success) {
|
|
994
|
+
const issues = result.error.issues.map((issue) => {
|
|
995
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
996
|
+
return ` - ${where}: ${issue.message}`;
|
|
997
|
+
}).join("\n");
|
|
998
|
+
throw new Error(
|
|
999
|
+
`Invalid ${filePath}:
|
|
1000
|
+
${issues}
|
|
1001
|
+
|
|
1002
|
+
See ${filePath.replace(
|
|
1003
|
+
/\.yml$/,
|
|
1004
|
+
".sample.yml"
|
|
1005
|
+
)} for a valid example.`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return result.data;
|
|
1009
|
+
}
|
|
1010
|
+
var DEFAULT_PROXY_HOST_PORT = 80;
|
|
1011
|
+
function proxyHostPort(config) {
|
|
1012
|
+
return config?.routing?.hostPort ?? DEFAULT_PROXY_HOST_PORT;
|
|
1013
|
+
}
|
|
1014
|
+
async function writeGlobalDefaultGitUser(user, opts = {}) {
|
|
1015
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
1016
|
+
const filePath = monocerosConfigPath(home);
|
|
1017
|
+
let text;
|
|
1018
|
+
try {
|
|
1019
|
+
text = await fs3.readFile(filePath, "utf8");
|
|
1020
|
+
} catch {
|
|
1021
|
+
text = void 0;
|
|
1022
|
+
}
|
|
1023
|
+
if (text === void 0) {
|
|
1024
|
+
const fresh = [
|
|
1025
|
+
"# Optional \u2014 global defaults for monoceros containers.",
|
|
1026
|
+
"",
|
|
1027
|
+
"schemaVersion: 1",
|
|
1028
|
+
"",
|
|
1029
|
+
"defaults:",
|
|
1030
|
+
" git:",
|
|
1031
|
+
" user:",
|
|
1032
|
+
` name: ${user.name}`,
|
|
1033
|
+
` email: ${user.email}`,
|
|
1034
|
+
""
|
|
1035
|
+
].join("\n");
|
|
1036
|
+
await fs3.mkdir(home, { recursive: true });
|
|
1037
|
+
await fs3.writeFile(filePath, fresh, "utf8");
|
|
1038
|
+
return { filePath, created: true, alreadySet: false };
|
|
1039
|
+
}
|
|
1040
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1041
|
+
if (doc.errors.length > 0) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const defaultsMap = ensureMap(doc, "defaults");
|
|
1047
|
+
const gitMap = ensureSubMapAtTop(defaultsMap, "git");
|
|
1048
|
+
const userMap = ensureSubMap(gitMap, "user");
|
|
1049
|
+
const existingName = userMap.get("name");
|
|
1050
|
+
const existingEmail = userMap.get("email");
|
|
1051
|
+
if (typeof existingName === "string" && existingName.length > 0 && typeof existingEmail === "string" && existingEmail.length > 0) {
|
|
1052
|
+
return { filePath, created: false, alreadySet: true };
|
|
1053
|
+
}
|
|
1054
|
+
relocateLeakedLeafComments(userMap, defaultsMap, "git");
|
|
1055
|
+
userMap.set("name", user.name);
|
|
1056
|
+
userMap.set("email", user.email);
|
|
1057
|
+
const newText = String(doc);
|
|
1058
|
+
await fs3.writeFile(filePath, newText, "utf8");
|
|
1059
|
+
return { filePath, created: false, alreadySet: false };
|
|
1060
|
+
}
|
|
1061
|
+
function ensureMap(doc, key) {
|
|
1062
|
+
const node = doc.get(key, true);
|
|
1063
|
+
if (node && isMap(node)) return node;
|
|
1064
|
+
const m = new YAMLMap();
|
|
1065
|
+
doc.set(key, m);
|
|
1066
|
+
return m;
|
|
1067
|
+
}
|
|
1068
|
+
function ensureSubMap(parent, key) {
|
|
1069
|
+
const node = parent.get(key, true);
|
|
1070
|
+
if (node && isMap(node)) return node;
|
|
1071
|
+
const m = new YAMLMap();
|
|
1072
|
+
parent.set(key, m);
|
|
1073
|
+
return m;
|
|
1074
|
+
}
|
|
1075
|
+
function ensureSubMapAtTop(parent, key) {
|
|
1076
|
+
const node = parent.get(key, true);
|
|
1077
|
+
if (node && isMap(node)) return node;
|
|
1078
|
+
const parentMaybe = parent;
|
|
1079
|
+
const newKey = new Scalar(key);
|
|
1080
|
+
if (parent.items.length > 0 && typeof parentMaybe.commentBefore === "string" && parentMaybe.commentBefore.length > 0) {
|
|
1081
|
+
const cleaned = stripCommentedKeySkeleton(parentMaybe.commentBefore, key);
|
|
1082
|
+
const blankMatch = cleaned.match(/\n[ \t]*\n/);
|
|
1083
|
+
let head;
|
|
1084
|
+
let tail;
|
|
1085
|
+
if (blankMatch && blankMatch.index !== void 0) {
|
|
1086
|
+
head = cleaned.slice(0, blankMatch.index);
|
|
1087
|
+
tail = cleaned.slice(blankMatch.index + blankMatch[0].length);
|
|
1088
|
+
} else {
|
|
1089
|
+
head = cleaned;
|
|
1090
|
+
tail = "";
|
|
1091
|
+
}
|
|
1092
|
+
if (head.length > 0) {
|
|
1093
|
+
newKey.commentBefore = head;
|
|
1094
|
+
if (parentMaybe.spaceBefore) newKey.spaceBefore = true;
|
|
1095
|
+
}
|
|
1096
|
+
if (tail.length > 0) {
|
|
1097
|
+
const firstKey = parent.items[0].key;
|
|
1098
|
+
if (firstKey && typeof firstKey === "object") {
|
|
1099
|
+
const existing = firstKey.commentBefore ?? "";
|
|
1100
|
+
firstKey.commentBefore = existing ? `${tail}
|
|
1101
|
+
${existing}` : tail;
|
|
1102
|
+
firstKey.spaceBefore = true;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
parentMaybe.commentBefore = null;
|
|
1106
|
+
parentMaybe.spaceBefore = false;
|
|
1107
|
+
}
|
|
1108
|
+
const m = new YAMLMap();
|
|
1109
|
+
const pair = new Pair(newKey, m);
|
|
1110
|
+
parent.items.unshift(pair);
|
|
1111
|
+
return m;
|
|
1112
|
+
}
|
|
1113
|
+
function stripCommentedKeySkeleton(commentBody, key) {
|
|
1114
|
+
const lines = commentBody.split("\n");
|
|
1115
|
+
const headRe = new RegExp(`^ ${escapeRegExp(key)}:\\s*$`);
|
|
1116
|
+
const out = [];
|
|
1117
|
+
let i = 0;
|
|
1118
|
+
while (i < lines.length) {
|
|
1119
|
+
const line = lines[i];
|
|
1120
|
+
if (headRe.test(line)) {
|
|
1121
|
+
i++;
|
|
1122
|
+
while (i < lines.length && /^ {2,}\S/.test(lines[i])) {
|
|
1123
|
+
i++;
|
|
1124
|
+
}
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
out.push(line);
|
|
1128
|
+
i++;
|
|
1129
|
+
}
|
|
1130
|
+
return out.join("\n");
|
|
1131
|
+
}
|
|
1132
|
+
function escapeRegExp(s) {
|
|
1133
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1134
|
+
}
|
|
1135
|
+
function relocateLeakedLeafComments(leafMap, parent, ancestorKey) {
|
|
1136
|
+
const items = parent.items;
|
|
1137
|
+
const ancestorIdx = items.findIndex((p) => {
|
|
1138
|
+
const k = p.key;
|
|
1139
|
+
const v = typeof k === "string" ? k : k?.value ?? null;
|
|
1140
|
+
return v === ancestorKey;
|
|
1141
|
+
});
|
|
1142
|
+
if (ancestorIdx < 0 || ancestorIdx + 1 >= items.length) return;
|
|
1143
|
+
const target = items[ancestorIdx + 1];
|
|
1144
|
+
for (const pair of leafMap.items) {
|
|
1145
|
+
const value = pair.value;
|
|
1146
|
+
if (!value || typeof value !== "object") continue;
|
|
1147
|
+
const leakedComment = value.comment;
|
|
1148
|
+
const leakedSpace = value.spaceBefore;
|
|
1149
|
+
if (!leakedComment && !leakedSpace) continue;
|
|
1150
|
+
if (leakedComment) {
|
|
1151
|
+
const targetKey = target.key;
|
|
1152
|
+
if (targetKey && typeof targetKey === "object") {
|
|
1153
|
+
const existing = targetKey.commentBefore ?? "";
|
|
1154
|
+
targetKey.commentBefore = existing ? `${leakedComment}
|
|
1155
|
+
${existing}` : leakedComment;
|
|
1156
|
+
if (leakedSpace) targetKey.spaceBefore = true;
|
|
1157
|
+
}
|
|
1158
|
+
value.comment = null;
|
|
1159
|
+
}
|
|
1160
|
+
if (leakedSpace) value.spaceBefore = false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/proxy/index.ts
|
|
1165
|
+
import { spawn as spawn3 } from "child_process";
|
|
1166
|
+
import { promises as fs4 } from "fs";
|
|
1167
|
+
import path3 from "path";
|
|
1168
|
+
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
1169
|
+
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
1170
|
+
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
1171
|
+
var defaultDockerExec = (args) => {
|
|
1172
|
+
return new Promise((resolve, reject) => {
|
|
1173
|
+
const child = spawn3("docker", args, {
|
|
1174
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1175
|
+
});
|
|
1176
|
+
let stdout = "";
|
|
1177
|
+
let stderr = "";
|
|
1178
|
+
child.stdout.on("data", (chunk) => {
|
|
1179
|
+
stdout += chunk.toString();
|
|
1180
|
+
});
|
|
1181
|
+
child.stderr.on("data", (chunk) => {
|
|
1182
|
+
stderr += chunk.toString();
|
|
1183
|
+
});
|
|
1184
|
+
child.on("error", reject);
|
|
1185
|
+
child.on(
|
|
1186
|
+
"exit",
|
|
1187
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
1188
|
+
);
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
var realDocker = defaultDockerExec;
|
|
1192
|
+
function proxyDynamicDir(home) {
|
|
1193
|
+
return path3.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
1194
|
+
}
|
|
1195
|
+
async function ensureProxy(opts = {}) {
|
|
1196
|
+
const docker = opts.docker ?? realDocker;
|
|
1197
|
+
const dyn = proxyDynamicDir(opts.monocerosHome);
|
|
1198
|
+
await fs4.mkdir(dyn, { recursive: true });
|
|
1199
|
+
const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
|
|
1200
|
+
if (netInspect.exitCode !== 0) {
|
|
1201
|
+
const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
|
|
1202
|
+
if (create.exitCode !== 0) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`Could not create docker network ${PROXY_NETWORK_NAME}: ${create.stderr.trim() || `exit ${create.exitCode}`}`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const state = await docker([
|
|
1209
|
+
"inspect",
|
|
1210
|
+
"--format",
|
|
1211
|
+
"{{.State.Running}}",
|
|
1212
|
+
PROXY_CONTAINER_NAME
|
|
1213
|
+
]);
|
|
1214
|
+
if (state.exitCode === 0) {
|
|
1215
|
+
if (state.stdout.trim() === "true") return;
|
|
1216
|
+
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
1217
|
+
if (start.exitCode !== 0) {
|
|
1218
|
+
throw new Error(
|
|
1219
|
+
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const hostPort = opts.hostPort ?? 80;
|
|
1225
|
+
const run = await docker([
|
|
1226
|
+
"run",
|
|
1227
|
+
"-d",
|
|
1228
|
+
"--name",
|
|
1229
|
+
PROXY_CONTAINER_NAME,
|
|
1230
|
+
"--network",
|
|
1231
|
+
PROXY_NETWORK_NAME,
|
|
1232
|
+
"-p",
|
|
1233
|
+
`${hostPort}:80`,
|
|
1234
|
+
"-v",
|
|
1235
|
+
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
1236
|
+
"--label",
|
|
1237
|
+
"monoceros.role=proxy",
|
|
1238
|
+
TRAEFIK_IMAGE,
|
|
1239
|
+
"--entrypoints.web.address=:80",
|
|
1240
|
+
"--providers.file.directory=/etc/traefik/dynamic",
|
|
1241
|
+
"--providers.file.watch=true",
|
|
1242
|
+
"--providers.docker=false",
|
|
1243
|
+
"--api.dashboard=false",
|
|
1244
|
+
"--log.level=INFO"
|
|
1245
|
+
]);
|
|
1246
|
+
if (run.exitCode !== 0) {
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
opts.logger?.info(
|
|
1252
|
+
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
async function maybeStopProxy(opts = {}) {
|
|
1256
|
+
const docker = opts.docker ?? realDocker;
|
|
1257
|
+
const logger = opts.logger;
|
|
1258
|
+
const inspect = await docker([
|
|
1259
|
+
"network",
|
|
1260
|
+
"inspect",
|
|
1261
|
+
PROXY_NETWORK_NAME,
|
|
1262
|
+
"--format",
|
|
1263
|
+
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
1264
|
+
]);
|
|
1265
|
+
if (inspect.exitCode !== 0) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
1269
|
+
if (others.length > 0) return;
|
|
1270
|
+
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
1271
|
+
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
1272
|
+
if (netRm.exitCode !== 0) {
|
|
1273
|
+
logger?.warn?.(
|
|
1274
|
+
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
1275
|
+
);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
logger?.info(
|
|
1279
|
+
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/proxy/dynamic.ts
|
|
1284
|
+
import { promises as fs5 } from "fs";
|
|
1285
|
+
import path4 from "path";
|
|
1286
|
+
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
1287
|
+
if (ports.length === 0) {
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
`writeDynamicConfig requires at least one port. For empty port lists, call removeDynamicConfig(${JSON.stringify(name)}).`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
1293
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1294
|
+
const file = path4.join(dir, `${name}.yml`);
|
|
1295
|
+
await fs5.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
1296
|
+
return file;
|
|
1297
|
+
}
|
|
1298
|
+
async function removeDynamicConfig(name, opts = {}) {
|
|
1299
|
+
const file = path4.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1300
|
+
await fs5.rm(file, { force: true });
|
|
1301
|
+
}
|
|
1302
|
+
function renderDynamicConfig(name, ports) {
|
|
786
1303
|
const lines = [];
|
|
787
1304
|
lines.push("# Generated by Monoceros \u2014 do not edit by hand.");
|
|
788
1305
|
lines.push(`# Container: ${name}`);
|
|
@@ -981,8 +1498,8 @@ function knownServices() {
|
|
|
981
1498
|
}
|
|
982
1499
|
|
|
983
1500
|
// src/create/scaffold.ts
|
|
984
|
-
import { existsSync as existsSync2, readFileSync, promises as
|
|
985
|
-
import
|
|
1501
|
+
import { existsSync as existsSync2, readFileSync, promises as fs6 } from "fs";
|
|
1502
|
+
import path5 from "path";
|
|
986
1503
|
|
|
987
1504
|
// src/util/ref.ts
|
|
988
1505
|
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
@@ -1153,7 +1670,7 @@ function resolveFeatures(opts) {
|
|
|
1153
1670
|
if (match) {
|
|
1154
1671
|
const name = match.name;
|
|
1155
1672
|
const checkout = workbenchCheckoutRoot();
|
|
1156
|
-
const localSourceDir = checkout ?
|
|
1673
|
+
const localSourceDir = checkout ? path5.join(checkout, "images", "features", name) : null;
|
|
1157
1674
|
if (localSourceDir && existsSync2(localSourceDir)) {
|
|
1158
1675
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
1159
1676
|
resolved.push({
|
|
@@ -1178,7 +1695,7 @@ function resolveFeatures(opts) {
|
|
|
1178
1695
|
return resolved;
|
|
1179
1696
|
}
|
|
1180
1697
|
function readPersistentHomeEntries(localSourceDir) {
|
|
1181
|
-
const manifestPath =
|
|
1698
|
+
const manifestPath = path5.join(localSourceDir, "devcontainer-feature.json");
|
|
1182
1699
|
try {
|
|
1183
1700
|
const text = readFileSync(manifestPath, "utf8");
|
|
1184
1701
|
const parsed = JSON.parse(text);
|
|
@@ -1266,7 +1783,10 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
1266
1783
|
}
|
|
1267
1784
|
const mounts = [...homeMounts];
|
|
1268
1785
|
const mountsField = mounts.length > 0 ? { mounts } : {};
|
|
1269
|
-
const workspaceMountField = {
|
|
1786
|
+
const workspaceMountField = {
|
|
1787
|
+
workspaceMount: `source=\${localWorkspaceFolder},target=/workspaces/${opts.name},type=bind,consistency=cached`,
|
|
1788
|
+
workspaceFolder: `/workspaces/${opts.name}`
|
|
1789
|
+
};
|
|
1270
1790
|
const runArgs = ["--cap-add=NET_ADMIN"];
|
|
1271
1791
|
if (ports.length > 0) {
|
|
1272
1792
|
runArgs.push("--network=monoceros-proxy");
|
|
@@ -1347,6 +1867,23 @@ function buildCodeWorkspaceJson(opts) {
|
|
|
1347
1867
|
}
|
|
1348
1868
|
return { folders };
|
|
1349
1869
|
}
|
|
1870
|
+
function mergeCodeWorkspace(existing, generated) {
|
|
1871
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !Array.isArray(existing.folders)) {
|
|
1872
|
+
return { ...generated };
|
|
1873
|
+
}
|
|
1874
|
+
const existingObj = existing;
|
|
1875
|
+
const existingFolders = existingObj.folders;
|
|
1876
|
+
const existingPaths = new Set(
|
|
1877
|
+
existingFolders.map((f) => f && typeof f === "object" ? f.path : void 0).filter((p) => typeof p === "string")
|
|
1878
|
+
);
|
|
1879
|
+
const merged = [...existingFolders];
|
|
1880
|
+
for (const g of generated.folders) {
|
|
1881
|
+
if (!existingPaths.has(g.path)) merged.push(g);
|
|
1882
|
+
}
|
|
1883
|
+
const out = { ...existingObj };
|
|
1884
|
+
out.folders = merged;
|
|
1885
|
+
return out;
|
|
1886
|
+
}
|
|
1350
1887
|
function buildPostCreateScript(opts) {
|
|
1351
1888
|
const lines = [
|
|
1352
1889
|
"#!/usr/bin/env bash",
|
|
@@ -1443,83 +1980,98 @@ function buildPostCreateScript(opts) {
|
|
|
1443
1980
|
return lines.join("\n") + "\n";
|
|
1444
1981
|
}
|
|
1445
1982
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
1446
|
-
const dest =
|
|
1447
|
-
await
|
|
1448
|
-
await
|
|
1983
|
+
const dest = path5.join(devcontainerDir, "post-create.sh");
|
|
1984
|
+
await fs6.writeFile(dest, buildPostCreateScript(opts));
|
|
1985
|
+
await fs6.chmod(dest, 493);
|
|
1449
1986
|
}
|
|
1450
1987
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
1451
1988
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
1452
|
-
const devcontainerDir =
|
|
1453
|
-
const monocerosDir =
|
|
1454
|
-
const projectsDir =
|
|
1455
|
-
const homeDir =
|
|
1456
|
-
const dataDir =
|
|
1457
|
-
await
|
|
1458
|
-
await
|
|
1459
|
-
await
|
|
1460
|
-
await
|
|
1989
|
+
const devcontainerDir = path5.join(targetDir, ".devcontainer");
|
|
1990
|
+
const monocerosDir = path5.join(targetDir, ".monoceros");
|
|
1991
|
+
const projectsDir = path5.join(targetDir, "projects");
|
|
1992
|
+
const homeDir = path5.join(targetDir, "home");
|
|
1993
|
+
const dataDir = path5.join(targetDir, "data");
|
|
1994
|
+
await fs6.mkdir(devcontainerDir, { recursive: true });
|
|
1995
|
+
await fs6.mkdir(monocerosDir, { recursive: true });
|
|
1996
|
+
await fs6.mkdir(projectsDir, { recursive: true });
|
|
1997
|
+
await fs6.mkdir(homeDir, { recursive: true });
|
|
1461
1998
|
if (needsCompose(opts)) {
|
|
1462
|
-
await
|
|
1999
|
+
await fs6.mkdir(dataDir, { recursive: true });
|
|
1463
2000
|
for (const svcId of opts.services) {
|
|
1464
2001
|
const def = SERVICE_CATALOG[svcId];
|
|
1465
2002
|
if (def?.dataMount) {
|
|
1466
|
-
await
|
|
2003
|
+
await fs6.mkdir(path5.join(dataDir, def.id), { recursive: true });
|
|
1467
2004
|
}
|
|
1468
2005
|
}
|
|
1469
2006
|
}
|
|
1470
|
-
const containerGitignore =
|
|
1471
|
-
await
|
|
1472
|
-
const gitkeep =
|
|
2007
|
+
const containerGitignore = path5.join(targetDir, ".gitignore");
|
|
2008
|
+
await fs6.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2009
|
+
const gitkeep = path5.join(projectsDir, ".gitkeep");
|
|
1473
2010
|
if (!existsSync2(gitkeep)) {
|
|
1474
|
-
await
|
|
2011
|
+
await fs6.writeFile(gitkeep, "");
|
|
1475
2012
|
}
|
|
1476
|
-
await
|
|
1477
|
-
|
|
2013
|
+
await fs6.writeFile(
|
|
2014
|
+
path5.join(monocerosDir, ".gitignore"),
|
|
1478
2015
|
"git-credentials*\ngitconfig\n"
|
|
1479
2016
|
);
|
|
1480
2017
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
1481
|
-
await
|
|
1482
|
-
|
|
2018
|
+
await fs6.writeFile(
|
|
2019
|
+
path5.join(devcontainerDir, "devcontainer.json"),
|
|
1483
2020
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
1484
2021
|
);
|
|
1485
|
-
const featuresDir =
|
|
2022
|
+
const featuresDir = path5.join(devcontainerDir, "features");
|
|
1486
2023
|
if (existsSync2(featuresDir)) {
|
|
1487
|
-
await
|
|
2024
|
+
await fs6.rm(featuresDir, { recursive: true, force: true });
|
|
1488
2025
|
}
|
|
1489
2026
|
const resolvedFeatures = resolveFeatures(opts);
|
|
1490
2027
|
for (const f of resolvedFeatures) {
|
|
1491
2028
|
if (!f.localSourceDir || !f.localName) continue;
|
|
1492
|
-
const dest =
|
|
1493
|
-
await
|
|
1494
|
-
await
|
|
2029
|
+
const dest = path5.join(featuresDir, f.localName);
|
|
2030
|
+
await fs6.mkdir(dest, { recursive: true });
|
|
2031
|
+
await fs6.cp(f.localSourceDir, dest, { recursive: true });
|
|
1495
2032
|
}
|
|
1496
2033
|
for (const f of resolvedFeatures) {
|
|
1497
2034
|
for (const sub of f.persistentHomePaths) {
|
|
1498
|
-
await
|
|
2035
|
+
await fs6.mkdir(path5.join(homeDir, sub), { recursive: true });
|
|
1499
2036
|
}
|
|
1500
2037
|
for (const entry2 of f.persistentHomeFiles) {
|
|
1501
|
-
const filePath =
|
|
1502
|
-
await
|
|
2038
|
+
const filePath = path5.join(homeDir, entry2.path);
|
|
2039
|
+
await fs6.mkdir(path5.dirname(filePath), { recursive: true });
|
|
1503
2040
|
if (!existsSync2(filePath)) {
|
|
1504
|
-
await
|
|
2041
|
+
await fs6.writeFile(filePath, entry2.initialContent);
|
|
1505
2042
|
}
|
|
1506
2043
|
}
|
|
1507
2044
|
}
|
|
1508
2045
|
await writePostCreateScript(devcontainerDir, opts);
|
|
1509
|
-
const composePath =
|
|
2046
|
+
const composePath = path5.join(devcontainerDir, "compose.yaml");
|
|
1510
2047
|
if (needsCompose(opts)) {
|
|
1511
|
-
await
|
|
2048
|
+
await fs6.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
1512
2049
|
} else if (existsSync2(composePath)) {
|
|
1513
|
-
await
|
|
2050
|
+
await fs6.rm(composePath);
|
|
1514
2051
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
2052
|
+
const workspacePath = path5.join(targetDir, `${opts.name}.code-workspace`);
|
|
2053
|
+
let existingWorkspace;
|
|
2054
|
+
try {
|
|
2055
|
+
const raw = await fs6.readFile(workspacePath, "utf8");
|
|
2056
|
+
existingWorkspace = JSON.parse(raw);
|
|
2057
|
+
} catch {
|
|
2058
|
+
existingWorkspace = void 0;
|
|
2059
|
+
}
|
|
2060
|
+
const generated = buildCodeWorkspaceJson(opts);
|
|
2061
|
+
const merged = mergeCodeWorkspace(existingWorkspace, generated);
|
|
2062
|
+
await fs6.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
|
|
1519
2063
|
}
|
|
1520
2064
|
|
|
1521
2065
|
// src/modify/yml.ts
|
|
1522
|
-
import {
|
|
2066
|
+
import {
|
|
2067
|
+
isMap as isMap2,
|
|
2068
|
+
isScalar,
|
|
2069
|
+
isSeq,
|
|
2070
|
+
Pair as Pair2,
|
|
2071
|
+
Scalar as Scalar2,
|
|
2072
|
+
YAMLMap as YAMLMap2,
|
|
2073
|
+
YAMLSeq
|
|
2074
|
+
} from "yaml";
|
|
1523
2075
|
function ensureSeq(doc, key) {
|
|
1524
2076
|
const existing = doc.get(key, true);
|
|
1525
2077
|
if (existing && isSeq(existing)) return existing;
|
|
@@ -1558,12 +2110,108 @@ function addAptPackagesToDoc(doc, packages) {
|
|
|
1558
2110
|
}
|
|
1559
2111
|
return changed;
|
|
1560
2112
|
}
|
|
2113
|
+
function setContainerGitUserInDoc(doc, user) {
|
|
2114
|
+
const gitNode = doc.get("git", true);
|
|
2115
|
+
let gitMap;
|
|
2116
|
+
let createdNew = false;
|
|
2117
|
+
if (gitNode && isMap2(gitNode)) {
|
|
2118
|
+
gitMap = gitNode;
|
|
2119
|
+
} else {
|
|
2120
|
+
gitMap = new YAMLMap2();
|
|
2121
|
+
insertTopLevelAfterName(doc, "git", gitMap, GIT_USER_HEADER_COMMENT);
|
|
2122
|
+
createdNew = true;
|
|
2123
|
+
}
|
|
2124
|
+
const userNode = gitMap.get("user", true);
|
|
2125
|
+
let userMap;
|
|
2126
|
+
if (userNode && isMap2(userNode)) {
|
|
2127
|
+
userMap = userNode;
|
|
2128
|
+
} else {
|
|
2129
|
+
userMap = new YAMLMap2();
|
|
2130
|
+
gitMap.set("user", userMap);
|
|
2131
|
+
}
|
|
2132
|
+
const currentName = userMap.get("name");
|
|
2133
|
+
const currentEmail = userMap.get("email");
|
|
2134
|
+
if (!createdNew && currentName === user.name && currentEmail === user.email) {
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
userMap.set("name", user.name);
|
|
2138
|
+
userMap.set("email", user.email);
|
|
2139
|
+
relocateLeakedSectionComments(doc);
|
|
2140
|
+
return true;
|
|
2141
|
+
}
|
|
2142
|
+
function relocateLeakedSectionComments(doc) {
|
|
2143
|
+
const root = doc.contents;
|
|
2144
|
+
if (!root || !isMap2(root)) return;
|
|
2145
|
+
const items = root.items;
|
|
2146
|
+
for (let i = 0; i < items.length - 1; i++) {
|
|
2147
|
+
const here = items[i];
|
|
2148
|
+
const next = items[i + 1];
|
|
2149
|
+
const leak = takeTrailingLeafComment(here.value);
|
|
2150
|
+
if (!leak) continue;
|
|
2151
|
+
const nextKey = next.key;
|
|
2152
|
+
if (!nextKey || typeof nextKey !== "object") continue;
|
|
2153
|
+
const existing = nextKey.commentBefore ?? "";
|
|
2154
|
+
nextKey.commentBefore = existing ? `${leak}
|
|
2155
|
+
${existing}` : leak;
|
|
2156
|
+
nextKey.spaceBefore = true;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
function takeTrailingLeafComment(node) {
|
|
2160
|
+
if (!node) return null;
|
|
2161
|
+
const c = node;
|
|
2162
|
+
if (typeof c.comment === "string" && c.comment.length > 0) {
|
|
2163
|
+
const blankMatch = c.comment.match(/\n[ \t]*\n/);
|
|
2164
|
+
if (blankMatch && blankMatch.index !== void 0) {
|
|
2165
|
+
const tail = c.comment.slice(blankMatch.index + blankMatch[0].length);
|
|
2166
|
+
c.comment = c.comment.slice(0, blankMatch.index);
|
|
2167
|
+
if (tail.length > 0) return tail;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (isMap2(node) && node.items.length > 0) {
|
|
2171
|
+
for (let i = node.items.length - 1; i >= 0; i--) {
|
|
2172
|
+
const value = node.items[i].value;
|
|
2173
|
+
const found = takeTrailingLeafComment(value);
|
|
2174
|
+
if (found) return found;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (isSeq(node) && node.items.length > 0) {
|
|
2178
|
+
for (let i = node.items.length - 1; i >= 0; i--) {
|
|
2179
|
+
const found = takeTrailingLeafComment(node.items[i]);
|
|
2180
|
+
if (found) return found;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
function insertTopLevelAfterName(doc, key, value, comment) {
|
|
2186
|
+
const root = doc.contents;
|
|
2187
|
+
if (!root || !isMap2(root)) {
|
|
2188
|
+
doc.set(key, value);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const keyScalar = new Scalar2(key);
|
|
2192
|
+
if (comment) {
|
|
2193
|
+
keyScalar.commentBefore = comment;
|
|
2194
|
+
keyScalar.spaceBefore = true;
|
|
2195
|
+
}
|
|
2196
|
+
const pair = new Pair2(keyScalar, value);
|
|
2197
|
+
const nameIdx = root.items.findIndex((p) => {
|
|
2198
|
+
const k = p.key;
|
|
2199
|
+
return (typeof k === "string" ? k : k?.value ?? null) === "name";
|
|
2200
|
+
});
|
|
2201
|
+
const insertAt = nameIdx >= 0 ? nameIdx + 1 : Math.min(1, root.items.length);
|
|
2202
|
+
root.items.splice(insertAt, 0, pair);
|
|
2203
|
+
}
|
|
2204
|
+
var GIT_USER_HEADER_COMMENT = [
|
|
2205
|
+
" Git committer identity for this container. Overrides",
|
|
2206
|
+
" monoceros-config.yml's defaults.git.user. Applies to every repo",
|
|
2207
|
+
" below unless that repo declares its own `git.user` override."
|
|
2208
|
+
].join("\n");
|
|
1561
2209
|
function portOfItem(item) {
|
|
1562
2210
|
const scalar = scalarValue(item);
|
|
1563
2211
|
if (typeof scalar === "number" && Number.isInteger(scalar)) {
|
|
1564
2212
|
return scalar;
|
|
1565
2213
|
}
|
|
1566
|
-
if (
|
|
2214
|
+
if (isMap2(item)) {
|
|
1567
2215
|
const p = item.get("port");
|
|
1568
2216
|
if (typeof p === "number" && Number.isInteger(p)) return p;
|
|
1569
2217
|
}
|
|
@@ -1571,8 +2219,8 @@ function portOfItem(item) {
|
|
|
1571
2219
|
}
|
|
1572
2220
|
function ensureRoutingMap(doc) {
|
|
1573
2221
|
const existing = doc.get("routing", true);
|
|
1574
|
-
if (existing &&
|
|
1575
|
-
const map = new
|
|
2222
|
+
if (existing && isMap2(existing)) return existing;
|
|
2223
|
+
const map = new YAMLMap2();
|
|
1576
2224
|
doc.set("routing", map);
|
|
1577
2225
|
return map;
|
|
1578
2226
|
}
|
|
@@ -1616,7 +2264,7 @@ function addPortsToDoc(doc, ports) {
|
|
|
1616
2264
|
}
|
|
1617
2265
|
function removePortsFromDoc(doc, ports) {
|
|
1618
2266
|
const routing = doc.get("routing", true);
|
|
1619
|
-
if (!routing || !
|
|
2267
|
+
if (!routing || !isMap2(routing)) return false;
|
|
1620
2268
|
const seq = routing.get("ports", true);
|
|
1621
2269
|
if (!seq || !isSeq(seq)) return false;
|
|
1622
2270
|
const targets = new Set(ports);
|
|
@@ -1643,7 +2291,7 @@ function addInstallUrlToDoc(doc, url) {
|
|
|
1643
2291
|
function addFeatureToDoc(doc, ref, options = {}) {
|
|
1644
2292
|
const seq = ensureSeq(doc, "features");
|
|
1645
2293
|
for (const item of seq.items) {
|
|
1646
|
-
if (!
|
|
2294
|
+
if (!isMap2(item)) continue;
|
|
1647
2295
|
const itemRef = item.get("ref");
|
|
1648
2296
|
if (itemRef !== ref) continue;
|
|
1649
2297
|
const itemJs = item.toJS(doc);
|
|
@@ -1655,7 +2303,7 @@ function addFeatureToDoc(doc, ref, options = {}) {
|
|
|
1655
2303
|
`Feature ${ref} is already configured with different options. Remove it first (\`monoceros remove-feature ${ref}\`) before re-adding.`
|
|
1656
2304
|
);
|
|
1657
2305
|
}
|
|
1658
|
-
const entry2 = new
|
|
2306
|
+
const entry2 = new YAMLMap2();
|
|
1659
2307
|
entry2.set("ref", ref);
|
|
1660
2308
|
if (Object.keys(options).length > 0) {
|
|
1661
2309
|
entry2.set("options", options);
|
|
@@ -1666,16 +2314,16 @@ function addFeatureToDoc(doc, ref, options = {}) {
|
|
|
1666
2314
|
function addRepoToDoc(doc, repo) {
|
|
1667
2315
|
const seq = ensureSeq(doc, "repos");
|
|
1668
2316
|
for (const item of seq.items) {
|
|
1669
|
-
if (!
|
|
2317
|
+
if (!isMap2(item)) continue;
|
|
1670
2318
|
const url = item.get("url");
|
|
1671
2319
|
if (url !== repo.url) continue;
|
|
1672
2320
|
const existingPath = item.get("path");
|
|
1673
2321
|
const effectivePath = typeof existingPath === "string" ? existingPath : deriveRepoName(url);
|
|
1674
2322
|
if (effectivePath !== repo.path) continue;
|
|
1675
2323
|
const existingGit = item.get("git", true);
|
|
1676
|
-
const existingUser = existingGit &&
|
|
1677
|
-
const existingName = existingUser &&
|
|
1678
|
-
const existingEmail = existingUser &&
|
|
2324
|
+
const existingUser = existingGit && isMap2(existingGit) ? existingGit.get("user", true) : null;
|
|
2325
|
+
const existingName = existingUser && isMap2(existingUser) ? existingUser.get("name") : null;
|
|
2326
|
+
const existingEmail = existingUser && isMap2(existingUser) ? existingUser.get("email") : null;
|
|
1679
2327
|
const existingGitUser = typeof existingName === "string" && typeof existingEmail === "string" ? { name: existingName, email: existingEmail } : void 0;
|
|
1680
2328
|
const sameGitUser = (existingGitUser?.name ?? null) === (repo.gitUser?.name ?? null) && (existingGitUser?.email ?? null) === (repo.gitUser?.email ?? null);
|
|
1681
2329
|
const existingProvider = item.get("provider");
|
|
@@ -1684,8 +2332,8 @@ function addRepoToDoc(doc, repo) {
|
|
|
1684
2332
|
return false;
|
|
1685
2333
|
}
|
|
1686
2334
|
if (repo.gitUser) {
|
|
1687
|
-
const gitMap = new
|
|
1688
|
-
const userMap = new
|
|
2335
|
+
const gitMap = new YAMLMap2();
|
|
2336
|
+
const userMap = new YAMLMap2();
|
|
1689
2337
|
userMap.set("name", repo.gitUser.name);
|
|
1690
2338
|
userMap.set("email", repo.gitUser.email);
|
|
1691
2339
|
gitMap.set("user", userMap);
|
|
@@ -1698,16 +2346,18 @@ function addRepoToDoc(doc, repo) {
|
|
|
1698
2346
|
} else {
|
|
1699
2347
|
item.delete("provider");
|
|
1700
2348
|
}
|
|
2349
|
+
relocateLeakedSectionComments(doc);
|
|
1701
2350
|
return true;
|
|
1702
2351
|
}
|
|
1703
|
-
const entry2 = new
|
|
2352
|
+
const entry2 = new YAMLMap2();
|
|
1704
2353
|
entry2.set("url", repo.url);
|
|
1705
|
-
|
|
2354
|
+
const persistPath = repo.path !== deriveRepoName(repo.url);
|
|
2355
|
+
if (persistPath) {
|
|
1706
2356
|
entry2.set("path", repo.path);
|
|
1707
2357
|
}
|
|
1708
2358
|
if (repo.gitUser) {
|
|
1709
|
-
const gitMap = new
|
|
1710
|
-
const userMap = new
|
|
2359
|
+
const gitMap = new YAMLMap2();
|
|
2360
|
+
const userMap = new YAMLMap2();
|
|
1711
2361
|
userMap.set("name", repo.gitUser.name);
|
|
1712
2362
|
userMap.set("email", repo.gitUser.email);
|
|
1713
2363
|
gitMap.set("user", userMap);
|
|
@@ -1716,7 +2366,20 @@ function addRepoToDoc(doc, repo) {
|
|
|
1716
2366
|
if (repo.provider) {
|
|
1717
2367
|
entry2.set("provider", repo.provider);
|
|
1718
2368
|
}
|
|
2369
|
+
const hintLines = [];
|
|
2370
|
+
if (!persistPath) hintLines.push(" path:");
|
|
2371
|
+
if (!repo.provider) hintLines.push(" provider:");
|
|
2372
|
+
if (!repo.gitUser) {
|
|
2373
|
+
hintLines.push(" git:");
|
|
2374
|
+
hintLines.push(" user:");
|
|
2375
|
+
hintLines.push(" name:");
|
|
2376
|
+
hintLines.push(" email:");
|
|
2377
|
+
}
|
|
2378
|
+
if (hintLines.length > 0) {
|
|
2379
|
+
entry2.comment = hintLines.join("\n");
|
|
2380
|
+
}
|
|
1719
2381
|
seq.add(entry2);
|
|
2382
|
+
relocateLeakedSectionComments(doc);
|
|
1720
2383
|
return true;
|
|
1721
2384
|
}
|
|
1722
2385
|
function removeLanguageFromDoc(doc, lang) {
|
|
@@ -1741,7 +2404,7 @@ function removeInstallUrlFromDoc(doc, url) {
|
|
|
1741
2404
|
function removeFeatureFromDoc(doc, ref) {
|
|
1742
2405
|
const seq = doc.get("features", true);
|
|
1743
2406
|
if (!seq || !isSeq(seq)) return false;
|
|
1744
|
-
const idx = seq.items.findIndex((i) =>
|
|
2407
|
+
const idx = seq.items.findIndex((i) => isMap2(i) && i.get("ref") === ref);
|
|
1745
2408
|
if (idx < 0) return false;
|
|
1746
2409
|
seq.items.splice(idx, 1);
|
|
1747
2410
|
pruneEmptySeq(doc, "features");
|
|
@@ -1751,11 +2414,11 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
1751
2414
|
const seq = doc.get("repos", true);
|
|
1752
2415
|
if (!seq || !isSeq(seq)) return false;
|
|
1753
2416
|
const idx = seq.items.findIndex((item) => {
|
|
1754
|
-
if (!
|
|
2417
|
+
if (!isMap2(item)) return false;
|
|
1755
2418
|
const url = item.get("url");
|
|
1756
2419
|
if (url === urlOrPath) return true;
|
|
1757
|
-
const
|
|
1758
|
-
const effectivePath = typeof
|
|
2420
|
+
const path16 = item.get("path");
|
|
2421
|
+
const effectivePath = typeof path16 === "string" ? path16 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
1759
2422
|
return effectivePath === urlOrPath;
|
|
1760
2423
|
});
|
|
1761
2424
|
if (idx < 0) return false;
|
|
@@ -1805,7 +2468,7 @@ async function runAddRepo(input) {
|
|
|
1805
2468
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
1806
2469
|
);
|
|
1807
2470
|
}
|
|
1808
|
-
const
|
|
2471
|
+
const path16 = (input.path ?? deriveRepoName(url)).trim();
|
|
1809
2472
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
1810
2473
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
1811
2474
|
if (hasName !== hasEmail) {
|
|
@@ -1834,7 +2497,7 @@ async function runAddRepo(input) {
|
|
|
1834
2497
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
1835
2498
|
const entry2 = {
|
|
1836
2499
|
url,
|
|
1837
|
-
path:
|
|
2500
|
+
path: path16,
|
|
1838
2501
|
...hasName && hasEmail ? {
|
|
1839
2502
|
gitUser: {
|
|
1840
2503
|
name: input.gitName.trim(),
|
|
@@ -1843,7 +2506,117 @@ async function runAddRepo(input) {
|
|
|
1843
2506
|
} : {},
|
|
1844
2507
|
...providerToWrite ? { provider: providerToWrite } : {}
|
|
1845
2508
|
};
|
|
1846
|
-
|
|
2509
|
+
const result = await mutate(input, (doc) => addRepoToDoc(doc, entry2));
|
|
2510
|
+
if (result.status === "updated") {
|
|
2511
|
+
await tryCloneInRunningContainer(input, entry2);
|
|
2512
|
+
}
|
|
2513
|
+
return result;
|
|
2514
|
+
}
|
|
2515
|
+
async function tryCloneInRunningContainer(input, entry2) {
|
|
2516
|
+
const home = input.monocerosHome ?? monocerosHome();
|
|
2517
|
+
const root = containerDir(input.name, home);
|
|
2518
|
+
const logger = input.logger ?? defaultLogger();
|
|
2519
|
+
let containerId;
|
|
2520
|
+
try {
|
|
2521
|
+
containerId = await findRunningContainerByLocalFolder(root, {
|
|
2522
|
+
...input.containerLookupDocker ? { docker: input.containerLookupDocker } : {}
|
|
2523
|
+
});
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
logger.warn(
|
|
2526
|
+
`Could not check whether the container is running: ${err instanceof Error ? err.message : String(err)}. The yml is updated \u2014 run \`monoceros apply ${input.name}\` to clone.`
|
|
2527
|
+
);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (!containerId) {
|
|
2531
|
+
logger.info(
|
|
2532
|
+
`Container not running \u2014 yml updated only. Clone happens on \`monoceros apply ${input.name}\`.`
|
|
2533
|
+
);
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
let urlHost;
|
|
2537
|
+
try {
|
|
2538
|
+
urlHost = new URL(entry2.url).hostname;
|
|
2539
|
+
} catch {
|
|
2540
|
+
logger.warn(
|
|
2541
|
+
`Cannot parse URL host from ${entry2.url}. The yml is updated \u2014 clone manually inside the container or rerun with a fixed URL.`
|
|
2542
|
+
);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
const provider = resolveProvider(urlHost, entry2.provider);
|
|
2546
|
+
if (provider === "unknown") {
|
|
2547
|
+
logger.warn(
|
|
2548
|
+
`Could not resolve provider for host ${urlHost}. The yml is updated; clone happens at the next \`monoceros apply\` if you set the provider.`
|
|
2549
|
+
);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
try {
|
|
2553
|
+
const credsResult = await collectGitCredentials(
|
|
2554
|
+
root,
|
|
2555
|
+
[{ host: urlHost, provider }],
|
|
2556
|
+
{
|
|
2557
|
+
...input.credentialsSpawn ? { spawn: input.credentialsSpawn } : {},
|
|
2558
|
+
logger: { info: () => {
|
|
2559
|
+
}, warn: (m) => logger.warn(m) }
|
|
2560
|
+
}
|
|
2561
|
+
);
|
|
2562
|
+
const status = credsResult.perHost.find((h) => h.host === urlHost);
|
|
2563
|
+
if (!status || status.status !== "ok") {
|
|
2564
|
+
const detail = status?.detail ? `: ${status.detail}` : "";
|
|
2565
|
+
logger.warn(
|
|
2566
|
+
`No HTTPS credentials available for ${urlHost}${detail}. The yml is updated; set up credentials (e.g. \`gh auth login\`) and re-run \`monoceros apply ${input.name}\` or rerun this add-repo.`
|
|
2567
|
+
);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
logger.warn(
|
|
2572
|
+
`Credential fetch for ${urlHost} failed: ${err instanceof Error ? err.message : String(err)}. The yml is updated.`
|
|
2573
|
+
);
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const containerName = input.name;
|
|
2577
|
+
const targetRel = `projects/${entry2.path}`;
|
|
2578
|
+
const parentRel = entry2.path.includes("/") ? `projects/${entry2.path.split("/").slice(0, -1).join("/")}` : "projects";
|
|
2579
|
+
const credentialsFile = `/workspaces/${containerName}/.monoceros/git-credentials`;
|
|
2580
|
+
const credentialHelper = `store --file=${credentialsFile}`;
|
|
2581
|
+
const script = [
|
|
2582
|
+
`set -eu`,
|
|
2583
|
+
`cd /workspaces/${containerName}`,
|
|
2584
|
+
`if [ -d ${shquote(targetRel)} ]; then`,
|
|
2585
|
+
` echo "[add-repo] ${targetRel} already exists \u2014 skipping clone."`,
|
|
2586
|
+
` exit 0`,
|
|
2587
|
+
`fi`,
|
|
2588
|
+
`mkdir -p ${shquote(parentRel)}`,
|
|
2589
|
+
`git -c ${shquote(`credential.helper=${credentialHelper}`)} clone ${shquote(entry2.url)} ${shquote(targetRel)}`
|
|
2590
|
+
];
|
|
2591
|
+
if (entry2.gitUser) {
|
|
2592
|
+
script.push(
|
|
2593
|
+
`git -C ${shquote(targetRel)} config user.name ${shquote(entry2.gitUser.name)}`,
|
|
2594
|
+
`git -C ${shquote(targetRel)} config user.email ${shquote(entry2.gitUser.email)}`
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
const execFn = input.containerExec ?? realContainerExec;
|
|
2598
|
+
let exit;
|
|
2599
|
+
try {
|
|
2600
|
+
exit = await execFn(containerId, ["bash", "-c", script.join("\n")]);
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
logger.warn(
|
|
2603
|
+
`In-container clone for ${entry2.url} failed: ${err instanceof Error ? err.message : String(err)}. The yml is updated; \`monoceros apply ${input.name}\` retries.`
|
|
2604
|
+
);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if (exit.exitCode !== 0) {
|
|
2608
|
+
logger.warn(
|
|
2609
|
+
`In-container clone for ${entry2.url} exited ${exit.exitCode}. The yml is updated; \`monoceros apply ${input.name}\` retries.`
|
|
2610
|
+
);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
logger.info(
|
|
2614
|
+
`Cloned ${entry2.url} into /workspaces/${containerName}/${targetRel} inside the running container.`
|
|
2615
|
+
);
|
|
2616
|
+
void path6;
|
|
2617
|
+
}
|
|
2618
|
+
function shquote(value) {
|
|
2619
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1847
2620
|
}
|
|
1848
2621
|
function normalizeProvider(raw) {
|
|
1849
2622
|
if (typeof raw !== "string") return void 0;
|
|
@@ -1979,7 +2752,7 @@ async function mutate(opts, apply) {
|
|
|
1979
2752
|
const logger = opts.logger ?? defaultLogger();
|
|
1980
2753
|
let oldText;
|
|
1981
2754
|
try {
|
|
1982
|
-
oldText = await
|
|
2755
|
+
oldText = await fs7.readFile(ymlPath, "utf8");
|
|
1983
2756
|
} catch {
|
|
1984
2757
|
throw new Error(
|
|
1985
2758
|
`No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
|
|
@@ -2003,7 +2776,7 @@ async function mutate(opts, apply) {
|
|
|
2003
2776
|
return { status: "aborted" };
|
|
2004
2777
|
}
|
|
2005
2778
|
}
|
|
2006
|
-
await
|
|
2779
|
+
await fs7.writeFile(ymlPath, newText, "utf8");
|
|
2007
2780
|
logger.success(`Updated ${ymlPath}.`);
|
|
2008
2781
|
logger.info(
|
|
2009
2782
|
`Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
|
|
@@ -2261,179 +3034,21 @@ function printSecurityWarning(url) {
|
|
|
2261
3034
|
w(
|
|
2262
3035
|
" 2. Verify the maintainer is who you think they are (HTTPS cert, repo)."
|
|
2263
3036
|
);
|
|
2264
|
-
w(" 3. Ideally, vendor the install steps as `add-apt-packages` or");
|
|
2265
|
-
w(
|
|
2266
|
-
" `add-feature` instead \u2014 those reference signed/versioned artifacts."
|
|
2267
|
-
);
|
|
2268
|
-
w("");
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
// src/commands/add-repo.ts
|
|
2272
|
-
import { defineCommand as defineCommand4 } from "citty";
|
|
2273
|
-
import { consola as consola5 } from "consola";
|
|
2274
|
-
var addRepoCommand = defineCommand4({
|
|
2275
|
-
meta: {
|
|
2276
|
-
name: "add-repo",
|
|
2277
|
-
group: "edit",
|
|
2278
|
-
description: "Add a git repo to the container config. Cloned into projects/<path>/ on container build. Idempotent \u2014 existing project subfolders are left alone. Destination path derived from URL by default; override with --path (supports nested subfolders like apps/web). Branches/PRs are git-level concerns: clone, then `git checkout` inside the container."
|
|
2279
|
-
},
|
|
2280
|
-
args: {
|
|
2281
|
-
name: {
|
|
2282
|
-
type: "positional",
|
|
2283
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2284
|
-
required: true
|
|
2285
|
-
},
|
|
2286
|
-
url: {
|
|
2287
|
-
type: "positional",
|
|
2288
|
-
description: "Git URL (HTTPS or SSH/git@ form). E.g. https://github.com/foo/bar.git, git@github.com:foo/bar.git.",
|
|
2289
|
-
required: true
|
|
2290
|
-
},
|
|
2291
|
-
path: {
|
|
2292
|
-
type: "string",
|
|
2293
|
-
description: "Destination under projects/. Subfolders via `/` (e.g. apps/web). Default: URL-derived single segment (bar.git \u2192 bar)."
|
|
2294
|
-
},
|
|
2295
|
-
"git-name": {
|
|
2296
|
-
type: "string",
|
|
2297
|
-
description: "Per-repo git committer name. Overrides the container-level git.user.name for this repo only. Pair with --git-email."
|
|
2298
|
-
},
|
|
2299
|
-
"git-email": {
|
|
2300
|
-
type: "string",
|
|
2301
|
-
description: "Per-repo git committer email. Overrides the container-level git.user.email for this repo only. Pair with --git-name."
|
|
2302
|
-
},
|
|
2303
|
-
provider: {
|
|
2304
|
-
type: "string",
|
|
2305
|
-
description: "Git provider for credential-helper guidance: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right CLI (gh / glab / Atlassian token) on missing credentials."
|
|
2306
|
-
},
|
|
2307
|
-
yes: {
|
|
2308
|
-
type: "boolean",
|
|
2309
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2310
|
-
alias: ["y"],
|
|
2311
|
-
default: false
|
|
2312
|
-
}
|
|
2313
|
-
},
|
|
2314
|
-
async run({ args }) {
|
|
2315
|
-
try {
|
|
2316
|
-
const result = await runAddRepo({
|
|
2317
|
-
name: args.name,
|
|
2318
|
-
url: args.url,
|
|
2319
|
-
...typeof args.path === "string" ? { path: args.path } : {},
|
|
2320
|
-
...typeof args["git-name"] === "string" ? { gitName: args["git-name"] } : {},
|
|
2321
|
-
...typeof args["git-email"] === "string" ? { gitEmail: args["git-email"] } : {},
|
|
2322
|
-
...typeof args.provider === "string" ? { provider: args.provider } : {},
|
|
2323
|
-
yes: args.yes
|
|
2324
|
-
});
|
|
2325
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2326
|
-
} catch (err) {
|
|
2327
|
-
consola5.error(err instanceof Error ? err.message : String(err));
|
|
2328
|
-
process.exit(1);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// src/commands/add-language.ts
|
|
2334
|
-
import { defineCommand as defineCommand5 } from "citty";
|
|
2335
|
-
import { consola as consola6 } from "consola";
|
|
2336
|
-
var addLanguageCommand = defineCommand5({
|
|
2337
|
-
meta: {
|
|
2338
|
-
name: "add-language",
|
|
2339
|
-
group: "edit",
|
|
2340
|
-
description: "Add a language toolchain (devcontainer feature) to the container config. Idempotent, prints a diff before writing."
|
|
2341
|
-
},
|
|
2342
|
-
args: {
|
|
2343
|
-
name: {
|
|
2344
|
-
type: "positional",
|
|
2345
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2346
|
-
required: true
|
|
2347
|
-
},
|
|
2348
|
-
language: {
|
|
2349
|
-
type: "positional",
|
|
2350
|
-
description: "Language identifier from the feature whitelist (e.g. python, java, rust).",
|
|
2351
|
-
required: true
|
|
2352
|
-
},
|
|
2353
|
-
yes: {
|
|
2354
|
-
type: "boolean",
|
|
2355
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2356
|
-
alias: ["y"],
|
|
2357
|
-
default: false
|
|
2358
|
-
}
|
|
2359
|
-
},
|
|
2360
|
-
async run({ args }) {
|
|
2361
|
-
try {
|
|
2362
|
-
const result = await runAddLanguage({
|
|
2363
|
-
name: args.name,
|
|
2364
|
-
language: args.language,
|
|
2365
|
-
yes: args.yes
|
|
2366
|
-
});
|
|
2367
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2368
|
-
} catch (err) {
|
|
2369
|
-
consola6.error(err instanceof Error ? err.message : String(err));
|
|
2370
|
-
process.exit(1);
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
|
|
2375
|
-
// src/commands/add-port.ts
|
|
2376
|
-
import { defineCommand as defineCommand6 } from "citty";
|
|
2377
|
-
import { consola as consola7 } from "consola";
|
|
2378
|
-
var addPortCommand = defineCommand6({
|
|
2379
|
-
meta: {
|
|
2380
|
-
name: "add-port",
|
|
2381
|
-
group: "edit",
|
|
2382
|
-
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)."
|
|
2383
|
-
},
|
|
2384
|
-
args: {
|
|
2385
|
-
name: {
|
|
2386
|
-
type: "positional",
|
|
2387
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2388
|
-
required: true
|
|
2389
|
-
},
|
|
2390
|
-
yes: {
|
|
2391
|
-
type: "boolean",
|
|
2392
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2393
|
-
alias: ["y"],
|
|
2394
|
-
default: false
|
|
2395
|
-
},
|
|
2396
|
-
default: {
|
|
2397
|
-
type: "boolean",
|
|
2398
|
-
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.",
|
|
2399
|
-
default: false
|
|
2400
|
-
}
|
|
2401
|
-
},
|
|
2402
|
-
async run({ args }) {
|
|
2403
|
-
const tokens = [...getInnerArgs()];
|
|
2404
|
-
if (tokens.length === 0) {
|
|
2405
|
-
consola7.error(
|
|
2406
|
-
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
2407
|
-
);
|
|
2408
|
-
process.exit(1);
|
|
2409
|
-
}
|
|
2410
|
-
try {
|
|
2411
|
-
const result = await runAddPort({
|
|
2412
|
-
name: args.name,
|
|
2413
|
-
ports: tokens.map(coerceToken),
|
|
2414
|
-
yes: args.yes,
|
|
2415
|
-
asDefault: args.default
|
|
2416
|
-
});
|
|
2417
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2418
|
-
} catch (err) {
|
|
2419
|
-
consola7.error(err instanceof Error ? err.message : String(err));
|
|
2420
|
-
process.exit(1);
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
});
|
|
2424
|
-
function coerceToken(raw) {
|
|
2425
|
-
const n = Number(raw);
|
|
2426
|
-
return Number.isFinite(n) ? n : raw;
|
|
3037
|
+
w(" 3. Ideally, vendor the install steps as `add-apt-packages` or");
|
|
3038
|
+
w(
|
|
3039
|
+
" `add-feature` instead \u2014 those reference signed/versioned artifacts."
|
|
3040
|
+
);
|
|
3041
|
+
w("");
|
|
2427
3042
|
}
|
|
2428
3043
|
|
|
2429
|
-
// src/commands/add-
|
|
2430
|
-
import { defineCommand as
|
|
2431
|
-
import { consola as
|
|
2432
|
-
var
|
|
3044
|
+
// src/commands/add-repo.ts
|
|
3045
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
3046
|
+
import { consola as consola5 } from "consola";
|
|
3047
|
+
var addRepoCommand = defineCommand4({
|
|
2433
3048
|
meta: {
|
|
2434
|
-
name: "add-
|
|
3049
|
+
name: "add-repo",
|
|
2435
3050
|
group: "edit",
|
|
2436
|
-
description: "Add a
|
|
3051
|
+
description: "Add a git repo to the container config. Cloned into projects/<path>/ on container build. Idempotent \u2014 existing project subfolders are left alone. Destination path derived from URL by default; override with --path (supports nested subfolders like apps/web). Branches/PRs are git-level concerns: clone, then `git checkout` inside the container."
|
|
2437
3052
|
},
|
|
2438
3053
|
args: {
|
|
2439
3054
|
name: {
|
|
@@ -2441,11 +3056,27 @@ var addServiceCommand = defineCommand7({
|
|
|
2441
3056
|
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2442
3057
|
required: true
|
|
2443
3058
|
},
|
|
2444
|
-
|
|
3059
|
+
url: {
|
|
2445
3060
|
type: "positional",
|
|
2446
|
-
description: "
|
|
3061
|
+
description: "Git URL (HTTPS or SSH/git@ form). E.g. https://github.com/foo/bar.git, git@github.com:foo/bar.git.",
|
|
2447
3062
|
required: true
|
|
2448
3063
|
},
|
|
3064
|
+
path: {
|
|
3065
|
+
type: "string",
|
|
3066
|
+
description: "Destination under projects/. Subfolders via `/` (e.g. apps/web). Default: URL-derived single segment (bar.git \u2192 bar)."
|
|
3067
|
+
},
|
|
3068
|
+
"git-name": {
|
|
3069
|
+
type: "string",
|
|
3070
|
+
description: "Per-repo git committer name. Overrides the container-level git.user.name for this repo only. Pair with --git-email."
|
|
3071
|
+
},
|
|
3072
|
+
"git-email": {
|
|
3073
|
+
type: "string",
|
|
3074
|
+
description: "Per-repo git committer email. Overrides the container-level git.user.email for this repo only. Pair with --git-name."
|
|
3075
|
+
},
|
|
3076
|
+
provider: {
|
|
3077
|
+
type: "string",
|
|
3078
|
+
description: "Git provider for credential-helper guidance: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right CLI (gh / glab / Atlassian token) on missing credentials."
|
|
3079
|
+
},
|
|
2449
3080
|
yes: {
|
|
2450
3081
|
type: "boolean",
|
|
2451
3082
|
description: "Skip the interactive confirmation and apply the diff.",
|
|
@@ -2455,680 +3086,513 @@ var addServiceCommand = defineCommand7({
|
|
|
2455
3086
|
},
|
|
2456
3087
|
async run({ args }) {
|
|
2457
3088
|
try {
|
|
2458
|
-
const result = await
|
|
3089
|
+
const result = await runAddRepo({
|
|
2459
3090
|
name: args.name,
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
process.exit(1);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
});
|
|
2470
|
-
|
|
2471
|
-
// src/commands/apply.ts
|
|
2472
|
-
import { defineCommand as defineCommand8 } from "citty";
|
|
2473
|
-
|
|
2474
|
-
// src/apply/index.ts
|
|
2475
|
-
import { existsSync as existsSync4, promises as fs10 } from "fs";
|
|
2476
|
-
import { consola as consola11 } from "consola";
|
|
2477
|
-
|
|
2478
|
-
// src/config/state.ts
|
|
2479
|
-
import { promises as fs7 } from "fs";
|
|
2480
|
-
import path5 from "path";
|
|
2481
|
-
function buildStateFile(opts) {
|
|
2482
|
-
return {
|
|
2483
|
-
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
2484
|
-
origin: opts.origin,
|
|
2485
|
-
monocerosCliVersion: opts.cliVersion,
|
|
2486
|
-
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
2487
|
-
};
|
|
2488
|
-
}
|
|
2489
|
-
function stateFilePath(targetDir) {
|
|
2490
|
-
return path5.join(targetDir, ".monoceros", "state.json");
|
|
2491
|
-
}
|
|
2492
|
-
async function readStateFile(targetDir) {
|
|
2493
|
-
try {
|
|
2494
|
-
const content = await fs7.readFile(stateFilePath(targetDir), "utf8");
|
|
2495
|
-
return JSON.parse(content);
|
|
2496
|
-
} catch {
|
|
2497
|
-
return void 0;
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
async function writeStateFile(targetDir, state) {
|
|
2501
|
-
const monocerosDir = path5.join(targetDir, ".monoceros");
|
|
2502
|
-
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2503
|
-
await fs7.writeFile(
|
|
2504
|
-
stateFilePath(targetDir),
|
|
2505
|
-
JSON.stringify(state, null, 2) + "\n"
|
|
2506
|
-
);
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
// src/config/transform.ts
|
|
2510
|
-
function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
2511
|
-
const featureRecord = {};
|
|
2512
|
-
for (const entry2 of config.features) {
|
|
2513
|
-
const defaults = featureDefaults[entry2.ref] ?? {};
|
|
2514
|
-
featureRecord[entry2.ref] = { ...defaults, ...entry2.options ?? {} };
|
|
2515
|
-
}
|
|
2516
|
-
const result = {
|
|
2517
|
-
name: config.name,
|
|
2518
|
-
languages: [...config.languages],
|
|
2519
|
-
services: [...config.services]
|
|
2520
|
-
};
|
|
2521
|
-
if (config.externalServices.postgres !== void 0) {
|
|
2522
|
-
result.postgresUrl = config.externalServices.postgres;
|
|
2523
|
-
}
|
|
2524
|
-
if (config.aptPackages.length > 0) {
|
|
2525
|
-
result.aptPackages = [...config.aptPackages];
|
|
2526
|
-
}
|
|
2527
|
-
if (Object.keys(featureRecord).length > 0) {
|
|
2528
|
-
result.features = featureRecord;
|
|
2529
|
-
}
|
|
2530
|
-
if (config.installUrls.length > 0) {
|
|
2531
|
-
result.installUrls = [...config.installUrls];
|
|
2532
|
-
}
|
|
2533
|
-
if (config.repos.length > 0) {
|
|
2534
|
-
result.repos = config.repos.map((r) => ({
|
|
2535
|
-
url: r.url,
|
|
2536
|
-
// `path` is optional in the yml; CreateOptions requires it.
|
|
2537
|
-
// When the yml omits `path`, fall back to the URL-derived
|
|
2538
|
-
// single-segment default (`https://.../foo.git` → `foo`),
|
|
2539
|
-
// which lands the clone at `projects/foo/`.
|
|
2540
|
-
path: r.path ?? deriveRepoName(r.url),
|
|
2541
|
-
...r.git?.user ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
2542
|
-
...r.provider ? { provider: r.provider } : {}
|
|
2543
|
-
}));
|
|
2544
|
-
}
|
|
2545
|
-
const routingPorts = config.routing?.ports ?? [];
|
|
2546
|
-
if (routingPorts.length > 0) {
|
|
2547
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2548
|
-
const ports = [];
|
|
2549
|
-
for (const entry2 of routingPorts) {
|
|
2550
|
-
const n = portNumber(entry2);
|
|
2551
|
-
if (seen.has(n)) continue;
|
|
2552
|
-
seen.add(n);
|
|
2553
|
-
ports.push(n);
|
|
2554
|
-
}
|
|
2555
|
-
result.ports = ports;
|
|
2556
|
-
}
|
|
2557
|
-
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
2558
|
-
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
2559
|
-
}
|
|
2560
|
-
return result;
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
// src/util/format.ts
|
|
2564
|
-
var ESC = "\x1B[";
|
|
2565
|
-
var ANSI_BOLD2 = `${ESC}1m`;
|
|
2566
|
-
var ANSI_UNDERLINE2 = `${ESC}4m`;
|
|
2567
|
-
var ANSI_CYAN2 = `${ESC}36m`;
|
|
2568
|
-
var ANSI_GREY2 = `${ESC}90m`;
|
|
2569
|
-
var ANSI_RESET2 = `${ESC}0m`;
|
|
2570
|
-
function makeWrap(isTty2) {
|
|
2571
|
-
return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET2 : s;
|
|
2572
|
-
}
|
|
2573
|
-
function makePalette(isTty2) {
|
|
2574
|
-
const wrap = makeWrap(isTty2);
|
|
2575
|
-
return {
|
|
2576
|
-
bold: (s) => wrap(s, ANSI_BOLD2),
|
|
2577
|
-
underline: (s) => wrap(s, ANSI_UNDERLINE2),
|
|
2578
|
-
cyan: (s) => wrap(s, ANSI_CYAN2),
|
|
2579
|
-
dim: (s) => wrap(s, ANSI_GREY2),
|
|
2580
|
-
sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
function colorsFor(stream) {
|
|
2584
|
-
return makePalette(stream.isTTY ?? false);
|
|
2585
|
-
}
|
|
2586
|
-
var stderrPalette = makePalette(process.stderr.isTTY ?? false);
|
|
2587
|
-
var bold2 = stderrPalette.bold;
|
|
2588
|
-
var underline2 = stderrPalette.underline;
|
|
2589
|
-
var cyan2 = stderrPalette.cyan;
|
|
2590
|
-
var dim = stderrPalette.dim;
|
|
2591
|
-
var sectionLine = stderrPalette.sectionLine;
|
|
2592
|
-
|
|
2593
|
-
// src/devcontainer/compose.ts
|
|
2594
|
-
import { spawn as spawn3 } from "child_process";
|
|
2595
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2596
|
-
import path7 from "path";
|
|
2597
|
-
import { consola as consola9 } from "consola";
|
|
2598
|
-
|
|
2599
|
-
// src/util/mask-secrets.ts
|
|
2600
|
-
import { Transform } from "stream";
|
|
2601
|
-
var PATTERNS = [
|
|
2602
|
-
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
2603
|
-
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
2604
|
-
// matching unrelated all-caps words.
|
|
2605
|
-
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
2606
|
-
// Bitbucket Cloud app password.
|
|
2607
|
-
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
2608
|
-
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
2609
|
-
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
2610
|
-
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
2611
|
-
// GitHub fine-grained PAT.
|
|
2612
|
-
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
2613
|
-
// Anthropic API key.
|
|
2614
|
-
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
2615
|
-
];
|
|
2616
|
-
function maskSecrets(text) {
|
|
2617
|
-
let result = text;
|
|
2618
|
-
for (const { re } of PATTERNS) {
|
|
2619
|
-
result = result.replace(re, maskOne);
|
|
2620
|
-
}
|
|
2621
|
-
return result;
|
|
2622
|
-
}
|
|
2623
|
-
function maskOne(token) {
|
|
2624
|
-
if (token.length <= 12) return token;
|
|
2625
|
-
return `${token.slice(0, 5)}\u2026${token.slice(-6)}`;
|
|
2626
|
-
}
|
|
2627
|
-
function createSecretMaskStream() {
|
|
2628
|
-
let buffer = "";
|
|
2629
|
-
return new Transform({
|
|
2630
|
-
decodeStrings: true,
|
|
2631
|
-
transform(chunk, _enc, cb) {
|
|
2632
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2633
|
-
buffer += text;
|
|
2634
|
-
const lastNewline = buffer.lastIndexOf("\n");
|
|
2635
|
-
if (lastNewline === -1) {
|
|
2636
|
-
cb(null);
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
const flushable = buffer.slice(0, lastNewline + 1);
|
|
2640
|
-
buffer = buffer.slice(lastNewline + 1);
|
|
2641
|
-
cb(null, maskSecrets(flushable));
|
|
2642
|
-
},
|
|
2643
|
-
flush(cb) {
|
|
2644
|
-
if (buffer.length > 0) {
|
|
2645
|
-
const tail = maskSecrets(buffer);
|
|
2646
|
-
buffer = "";
|
|
2647
|
-
cb(null, tail);
|
|
2648
|
-
return;
|
|
2649
|
-
}
|
|
2650
|
-
cb(null);
|
|
2651
|
-
}
|
|
2652
|
-
});
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
// src/devcontainer/cli.ts
|
|
2656
|
-
import { spawn as spawn2 } from "child_process";
|
|
2657
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
2658
|
-
import { createRequire } from "module";
|
|
2659
|
-
import path6 from "path";
|
|
2660
|
-
var require_ = createRequire(import.meta.url);
|
|
2661
|
-
var cachedBinaryPath = null;
|
|
2662
|
-
function devcontainerCliPath() {
|
|
2663
|
-
if (cachedBinaryPath) return cachedBinaryPath;
|
|
2664
|
-
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
2665
|
-
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
2666
|
-
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
2667
|
-
if (!binEntry) {
|
|
2668
|
-
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
2669
|
-
}
|
|
2670
|
-
cachedBinaryPath = path6.resolve(path6.dirname(pkgJsonPath), binEntry);
|
|
2671
|
-
return cachedBinaryPath;
|
|
2672
|
-
}
|
|
2673
|
-
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
2674
|
-
const binPath = devcontainerCliPath();
|
|
2675
|
-
return new Promise((resolve, reject) => {
|
|
2676
|
-
if (options.interactive) {
|
|
2677
|
-
const child2 = spawn2(process.execPath, [binPath, ...args], {
|
|
2678
|
-
cwd,
|
|
2679
|
-
stdio: "inherit"
|
|
3091
|
+
url: args.url,
|
|
3092
|
+
...typeof args.path === "string" ? { path: args.path } : {},
|
|
3093
|
+
...typeof args["git-name"] === "string" ? { gitName: args["git-name"] } : {},
|
|
3094
|
+
...typeof args["git-email"] === "string" ? { gitEmail: args["git-email"] } : {},
|
|
3095
|
+
...typeof args.provider === "string" ? { provider: args.provider } : {},
|
|
3096
|
+
yes: args.yes
|
|
2680
3097
|
});
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
3098
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3099
|
+
} catch (err) {
|
|
3100
|
+
consola5.error(err instanceof Error ? err.message : String(err));
|
|
3101
|
+
process.exit(1);
|
|
2684
3102
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
|
|
3106
|
+
// src/commands/add-language.ts
|
|
3107
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
3108
|
+
import { consola as consola6 } from "consola";
|
|
3109
|
+
var addLanguageCommand = defineCommand5({
|
|
3110
|
+
meta: {
|
|
3111
|
+
name: "add-language",
|
|
3112
|
+
group: "edit",
|
|
3113
|
+
description: "Add a language toolchain (devcontainer feature) to the container config. Idempotent, prints a diff before writing."
|
|
3114
|
+
},
|
|
3115
|
+
args: {
|
|
3116
|
+
name: {
|
|
3117
|
+
type: "positional",
|
|
3118
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3119
|
+
required: true
|
|
3120
|
+
},
|
|
3121
|
+
language: {
|
|
3122
|
+
type: "positional",
|
|
3123
|
+
description: "Language identifier from the feature whitelist (e.g. python, java, rust).",
|
|
3124
|
+
required: true
|
|
3125
|
+
},
|
|
3126
|
+
yes: {
|
|
3127
|
+
type: "boolean",
|
|
3128
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3129
|
+
alias: ["y"],
|
|
3130
|
+
default: false
|
|
3131
|
+
}
|
|
3132
|
+
},
|
|
3133
|
+
async run({ args }) {
|
|
3134
|
+
try {
|
|
3135
|
+
const result = await runAddLanguage({
|
|
3136
|
+
name: args.name,
|
|
3137
|
+
language: args.language,
|
|
3138
|
+
yes: args.yes
|
|
2706
3139
|
});
|
|
2707
|
-
|
|
3140
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
consola6.error(err instanceof Error ? err.message : String(err));
|
|
3143
|
+
process.exit(1);
|
|
2708
3144
|
}
|
|
2709
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2710
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2711
|
-
child.on("error", reject);
|
|
2712
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2713
|
-
});
|
|
2714
|
-
};
|
|
2715
|
-
|
|
2716
|
-
// src/devcontainer/compose.ts
|
|
2717
|
-
var spawnDockerCompose = (args, cwd) => {
|
|
2718
|
-
return new Promise((resolve, reject) => {
|
|
2719
|
-
const child = spawn3("docker", ["compose", ...args], {
|
|
2720
|
-
cwd,
|
|
2721
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
2722
|
-
});
|
|
2723
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2724
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2725
|
-
child.on("error", reject);
|
|
2726
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2727
|
-
});
|
|
2728
|
-
};
|
|
2729
|
-
var spawnBash = (args, cwd) => {
|
|
2730
|
-
return new Promise((resolve, reject) => {
|
|
2731
|
-
const child = spawn3("bash", args, {
|
|
2732
|
-
cwd,
|
|
2733
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
2734
|
-
});
|
|
2735
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2736
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2737
|
-
child.on("error", reject);
|
|
2738
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2739
|
-
});
|
|
2740
|
-
};
|
|
2741
|
-
function composeProjectName(root) {
|
|
2742
|
-
return `${path7.basename(root)}_devcontainer`;
|
|
2743
|
-
}
|
|
2744
|
-
function resolveCompose(root) {
|
|
2745
|
-
if (!existsSync3(path7.join(root, ".devcontainer"))) {
|
|
2746
|
-
throw new Error(
|
|
2747
|
-
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
2748
|
-
);
|
|
2749
3145
|
}
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3146
|
+
});
|
|
3147
|
+
|
|
3148
|
+
// src/commands/add-port.ts
|
|
3149
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
3150
|
+
import { consola as consola7 } from "consola";
|
|
3151
|
+
var addPortCommand = defineCommand6({
|
|
3152
|
+
meta: {
|
|
3153
|
+
name: "add-port",
|
|
3154
|
+
group: "edit",
|
|
3155
|
+
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)."
|
|
3156
|
+
},
|
|
3157
|
+
args: {
|
|
3158
|
+
name: {
|
|
3159
|
+
type: "positional",
|
|
3160
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3161
|
+
required: true
|
|
3162
|
+
},
|
|
3163
|
+
yes: {
|
|
3164
|
+
type: "boolean",
|
|
3165
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3166
|
+
alias: ["y"],
|
|
3167
|
+
default: false
|
|
3168
|
+
},
|
|
3169
|
+
default: {
|
|
3170
|
+
type: "boolean",
|
|
3171
|
+
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.",
|
|
3172
|
+
default: false
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
3175
|
+
async run({ args }) {
|
|
3176
|
+
const tokens = [...getInnerArgs()];
|
|
3177
|
+
if (tokens.length === 0) {
|
|
3178
|
+
consola7.error(
|
|
3179
|
+
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
3180
|
+
);
|
|
3181
|
+
process.exit(1);
|
|
3182
|
+
}
|
|
3183
|
+
try {
|
|
3184
|
+
const result = await runAddPort({
|
|
3185
|
+
name: args.name,
|
|
3186
|
+
ports: tokens.map(coerceToken),
|
|
3187
|
+
yes: args.yes,
|
|
3188
|
+
asDefault: args.default
|
|
3189
|
+
});
|
|
3190
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3191
|
+
} catch (err) {
|
|
3192
|
+
consola7.error(err instanceof Error ? err.message : String(err));
|
|
3193
|
+
process.exit(1);
|
|
3194
|
+
}
|
|
2755
3195
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
2761
|
-
const subArgs = buildSubArgs(opts.service);
|
|
2762
|
-
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
2763
|
-
}
|
|
2764
|
-
async function runStart(opts) {
|
|
2765
|
-
resolveCompose(opts.root);
|
|
2766
|
-
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
2767
|
-
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
2768
|
-
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
2769
|
-
return spawnFn(
|
|
2770
|
-
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
2771
|
-
opts.root
|
|
2772
|
-
);
|
|
3196
|
+
});
|
|
3197
|
+
function coerceToken(raw) {
|
|
3198
|
+
const n = Number(raw);
|
|
3199
|
+
return Number.isFinite(n) ? n : raw;
|
|
2773
3200
|
}
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
3201
|
+
|
|
3202
|
+
// src/commands/add-service.ts
|
|
3203
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3204
|
+
import { consola as consola8 } from "consola";
|
|
3205
|
+
var addServiceCommand = defineCommand7({
|
|
3206
|
+
meta: {
|
|
3207
|
+
name: "add-service",
|
|
3208
|
+
group: "edit",
|
|
3209
|
+
description: "Add a compose service (postgres, mysql, redis, \u2026) to the container config. Idempotent, prints a diff before writing."
|
|
3210
|
+
},
|
|
3211
|
+
args: {
|
|
3212
|
+
name: {
|
|
3213
|
+
type: "positional",
|
|
3214
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
3215
|
+
required: true
|
|
3216
|
+
},
|
|
3217
|
+
service: {
|
|
3218
|
+
type: "positional",
|
|
3219
|
+
description: "Service identifier (postgres, mysql, redis).",
|
|
3220
|
+
required: true
|
|
3221
|
+
},
|
|
3222
|
+
yes: {
|
|
3223
|
+
type: "boolean",
|
|
3224
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
3225
|
+
alias: ["y"],
|
|
3226
|
+
default: false
|
|
3227
|
+
}
|
|
3228
|
+
},
|
|
3229
|
+
async run({ args }) {
|
|
3230
|
+
try {
|
|
3231
|
+
const result = await runAddService({
|
|
3232
|
+
name: args.name,
|
|
3233
|
+
service: args.service,
|
|
3234
|
+
yes: args.yes
|
|
3235
|
+
});
|
|
3236
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
consola8.error(err instanceof Error ? err.message : String(err));
|
|
3239
|
+
process.exit(1);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
// src/commands/apply.ts
|
|
3245
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
3246
|
+
|
|
3247
|
+
// src/apply/index.ts
|
|
3248
|
+
import { existsSync as existsSync4, promises as fs10 } from "fs";
|
|
3249
|
+
import { consola as consola11 } from "consola";
|
|
3250
|
+
|
|
3251
|
+
// src/config/state.ts
|
|
3252
|
+
import { promises as fs8 } from "fs";
|
|
3253
|
+
import path7 from "path";
|
|
3254
|
+
function buildStateFile(opts) {
|
|
3255
|
+
return {
|
|
3256
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
3257
|
+
origin: opts.origin,
|
|
3258
|
+
monocerosCliVersion: opts.cliVersion,
|
|
3259
|
+
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
3260
|
+
};
|
|
2815
3261
|
}
|
|
2816
|
-
function
|
|
2817
|
-
return
|
|
2818
|
-
(service) => ["stop", ...service ? [service] : []],
|
|
2819
|
-
opts
|
|
2820
|
-
);
|
|
3262
|
+
function stateFilePath(targetDir) {
|
|
3263
|
+
return path7.join(targetDir, ".monoceros", "state.json");
|
|
2821
3264
|
}
|
|
2822
|
-
function
|
|
2823
|
-
|
|
2824
|
-
(
|
|
2825
|
-
|
|
2826
|
-
|
|
3265
|
+
async function readStateFile(targetDir) {
|
|
3266
|
+
try {
|
|
3267
|
+
const content = await fs8.readFile(stateFilePath(targetDir), "utf8");
|
|
3268
|
+
return JSON.parse(content);
|
|
3269
|
+
} catch {
|
|
3270
|
+
return void 0;
|
|
3271
|
+
}
|
|
2827
3272
|
}
|
|
2828
|
-
function
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
...service ? [service] : []
|
|
2835
|
-
],
|
|
2836
|
-
opts
|
|
3273
|
+
async function writeStateFile(targetDir, state) {
|
|
3274
|
+
const monocerosDir = path7.join(targetDir, ".monoceros");
|
|
3275
|
+
await fs8.mkdir(monocerosDir, { recursive: true });
|
|
3276
|
+
await fs8.writeFile(
|
|
3277
|
+
stateFilePath(targetDir),
|
|
3278
|
+
JSON.stringify(state, null, 2) + "\n"
|
|
2837
3279
|
);
|
|
2838
3280
|
}
|
|
2839
3281
|
|
|
2840
|
-
// src/
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
...process.env,
|
|
2850
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
2851
|
-
GIT_ASKPASS: "",
|
|
2852
|
-
SSH_ASKPASS: ""
|
|
2853
|
-
}
|
|
2854
|
-
});
|
|
2855
|
-
let stdout = "";
|
|
2856
|
-
child.stdout.on("data", (chunk) => {
|
|
2857
|
-
stdout += chunk.toString();
|
|
2858
|
-
});
|
|
2859
|
-
child.on("error", reject);
|
|
2860
|
-
child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
|
|
2861
|
-
child.stdin.write(input);
|
|
2862
|
-
child.stdin.end();
|
|
2863
|
-
});
|
|
2864
|
-
};
|
|
2865
|
-
function resolveProvider(host, explicit) {
|
|
2866
|
-
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
2867
|
-
if (canonical) return canonical;
|
|
2868
|
-
return explicit ?? "unknown";
|
|
2869
|
-
}
|
|
2870
|
-
function uniqueHttpsHosts(repos) {
|
|
2871
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
2872
|
-
for (const repo of repos) {
|
|
2873
|
-
if (!repo.url.startsWith("https://")) continue;
|
|
2874
|
-
let host;
|
|
2875
|
-
try {
|
|
2876
|
-
host = new URL(repo.url).hostname;
|
|
2877
|
-
} catch {
|
|
2878
|
-
continue;
|
|
2879
|
-
}
|
|
2880
|
-
if (byHost.has(host)) continue;
|
|
2881
|
-
byHost.set(host, { host, provider: resolveProvider(host, repo.provider) });
|
|
3282
|
+
// src/config/transform.ts
|
|
3283
|
+
function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
3284
|
+
const featureRecord = {};
|
|
3285
|
+
for (const entry2 of config.features) {
|
|
3286
|
+
const defaults = featureDefaults[entry2.ref] ?? {};
|
|
3287
|
+
const containerOpts = Object.fromEntries(
|
|
3288
|
+
Object.entries(entry2.options ?? {}).filter(([, v]) => v !== "")
|
|
3289
|
+
);
|
|
3290
|
+
featureRecord[entry2.ref] = { ...defaults, ...containerOpts };
|
|
2882
3291
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
return cyan2(opts.winget);
|
|
2891
|
-
default:
|
|
2892
|
-
if (opts.linuxBrew) return cyan2(opts.linuxBrew);
|
|
2893
|
-
return `See ${opts.linuxDocsUrl} for package instructions.`;
|
|
3292
|
+
const result = {
|
|
3293
|
+
name: config.name,
|
|
3294
|
+
languages: [...config.languages],
|
|
3295
|
+
services: [...config.services]
|
|
3296
|
+
};
|
|
3297
|
+
if (config.externalServices.postgres !== void 0) {
|
|
3298
|
+
result.postgresUrl = config.externalServices.postgres;
|
|
2894
3299
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
if (provider === "github") {
|
|
2898
|
-
const isSaas = host.toLowerCase() === "github.com";
|
|
2899
|
-
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2900
|
-
const install = installCommandForOS({
|
|
2901
|
-
brew: "brew install gh",
|
|
2902
|
-
winget: "winget install --id GitHub.cli",
|
|
2903
|
-
linuxBrew: "brew install gh",
|
|
2904
|
-
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
2905
|
-
});
|
|
2906
|
-
return {
|
|
2907
|
-
title: `${host} \u2014 GitHub`,
|
|
2908
|
-
body: [
|
|
2909
|
-
"Install the GitHub CLI:",
|
|
2910
|
-
install,
|
|
2911
|
-
"",
|
|
2912
|
-
"Then run once:",
|
|
2913
|
-
cyan2(`gh auth login${hostArg}`),
|
|
2914
|
-
cyan2(`gh auth setup-git${hostArg}`),
|
|
2915
|
-
"",
|
|
2916
|
-
"`gh auth login` walks through OAuth in your browser.",
|
|
2917
|
-
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
2918
|
-
].join("\n")
|
|
2919
|
-
};
|
|
3300
|
+
if (config.aptPackages.length > 0) {
|
|
3301
|
+
result.aptPackages = [...config.aptPackages];
|
|
2920
3302
|
}
|
|
2921
|
-
if (
|
|
2922
|
-
|
|
2923
|
-
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2924
|
-
const install = installCommandForOS({
|
|
2925
|
-
brew: "brew install glab",
|
|
2926
|
-
winget: "winget install --id GLab.GLab",
|
|
2927
|
-
linuxBrew: "brew install glab",
|
|
2928
|
-
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
2929
|
-
});
|
|
2930
|
-
return {
|
|
2931
|
-
title: `${host} \u2014 GitLab`,
|
|
2932
|
-
body: [
|
|
2933
|
-
"Install the GitLab CLI (glab):",
|
|
2934
|
-
install,
|
|
2935
|
-
"",
|
|
2936
|
-
"Then run once:",
|
|
2937
|
-
cyan2(`glab auth login${hostArg}`),
|
|
2938
|
-
"",
|
|
2939
|
-
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
2940
|
-
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
2941
|
-
"configures itself as the git credential helper."
|
|
2942
|
-
].join("\n")
|
|
2943
|
-
};
|
|
3303
|
+
if (Object.keys(featureRecord).length > 0) {
|
|
3304
|
+
result.features = featureRecord;
|
|
2944
3305
|
}
|
|
2945
|
-
if (
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3306
|
+
if (config.installUrls.length > 0) {
|
|
3307
|
+
result.installUrls = [...config.installUrls];
|
|
3308
|
+
}
|
|
3309
|
+
if (config.repos.length > 0) {
|
|
3310
|
+
result.repos = config.repos.map((r) => ({
|
|
3311
|
+
url: r.url,
|
|
3312
|
+
// `path` is optional in the yml; CreateOptions requires it.
|
|
3313
|
+
// When the yml omits `path`, fall back to the URL-derived
|
|
3314
|
+
// single-segment default (`https://.../foo.git` → `foo`),
|
|
3315
|
+
// which lands the clone at `projects/foo/`.
|
|
3316
|
+
path: r.path ?? deriveRepoName(r.url),
|
|
3317
|
+
// gitUser is forwarded only when BOTH name + email are set.
|
|
3318
|
+
// The relaxed GitUserSchema accepts nullable / empty strings
|
|
3319
|
+
// (so a yml placeholder `name:` parses without error), so we
|
|
3320
|
+
// re-check here before downstream code, which expects both
|
|
3321
|
+
// values to be non-empty.
|
|
3322
|
+
...r.git?.user?.name && r.git.user.email ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
3323
|
+
...r.provider ? { provider: r.provider } : {}
|
|
3324
|
+
}));
|
|
3325
|
+
}
|
|
3326
|
+
const routingPorts = config.routing?.ports ?? [];
|
|
3327
|
+
if (routingPorts.length > 0) {
|
|
3328
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3329
|
+
const ports = [];
|
|
3330
|
+
for (const entry2 of routingPorts) {
|
|
3331
|
+
const n = portNumber(entry2);
|
|
3332
|
+
if (seen.has(n)) continue;
|
|
3333
|
+
seen.add(n);
|
|
3334
|
+
ports.push(n);
|
|
2961
3335
|
}
|
|
2962
|
-
|
|
2963
|
-
title: `${host} \u2014 Bitbucket Data Center`,
|
|
2964
|
-
body: [
|
|
2965
|
-
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
2966
|
-
"is a manual one-time setup. Generate a personal HTTP access",
|
|
2967
|
-
`token in your Bitbucket UI: profile picture (top right on ${host})`,
|
|
2968
|
-
"\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
|
|
2969
|
-
"at least repo-read + repo-write scopes for the repos you need.",
|
|
2970
|
-
"",
|
|
2971
|
-
"Then store it via your OS credential helper:",
|
|
2972
|
-
cyan2(
|
|
2973
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
2974
|
-
)
|
|
2975
|
-
].join("\n")
|
|
2976
|
-
};
|
|
3336
|
+
result.ports = ports;
|
|
2977
3337
|
}
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
"`tea` CLI logs into its own config, not into your git credential",
|
|
2983
|
-
"helper), so this is a manual one-time setup. Generate an access",
|
|
2984
|
-
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
2985
|
-
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
2986
|
-
"least the `read:repository` scope (add `write:repository` if you",
|
|
2987
|
-
"need push from the container).",
|
|
2988
|
-
"",
|
|
2989
|
-
"Then store it via your OS credential helper:",
|
|
2990
|
-
cyan2(
|
|
2991
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
2992
|
-
)
|
|
2993
|
-
].join("\n")
|
|
2994
|
-
};
|
|
3338
|
+
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
3339
|
+
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
3340
|
+
}
|
|
3341
|
+
return result;
|
|
2995
3342
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3343
|
+
|
|
3344
|
+
// src/devcontainer/compose.ts
|
|
3345
|
+
import { spawn as spawn5 } from "child_process";
|
|
3346
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3347
|
+
import path9 from "path";
|
|
3348
|
+
import { consola as consola9 } from "consola";
|
|
3349
|
+
|
|
3350
|
+
// src/util/mask-secrets.ts
|
|
3351
|
+
import { Transform } from "stream";
|
|
3352
|
+
var PATTERNS = [
|
|
3353
|
+
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
3354
|
+
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
3355
|
+
// matching unrelated all-caps words.
|
|
3356
|
+
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
3357
|
+
// Bitbucket Cloud app password.
|
|
3358
|
+
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
3359
|
+
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
3360
|
+
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
3361
|
+
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
3362
|
+
// GitHub fine-grained PAT.
|
|
3363
|
+
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
3364
|
+
// Anthropic API key.
|
|
3365
|
+
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
3366
|
+
];
|
|
3367
|
+
function maskSecrets(text) {
|
|
3368
|
+
let result = text;
|
|
3369
|
+
for (const { re } of PATTERNS) {
|
|
3370
|
+
result = result.replace(re, maskOne);
|
|
3005
3371
|
}
|
|
3006
3372
|
return result;
|
|
3007
3373
|
}
|
|
3008
|
-
function
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
return `https://${encUser}:${encPass}@${host}`;
|
|
3374
|
+
function maskOne(token) {
|
|
3375
|
+
if (token.length <= 12) return token;
|
|
3376
|
+
return `${token.slice(0, 5)}\u2026${token.slice(-6)}`;
|
|
3012
3377
|
}
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3378
|
+
function createSecretMaskStream() {
|
|
3379
|
+
let buffer = "";
|
|
3380
|
+
return new Transform({
|
|
3381
|
+
decodeStrings: true,
|
|
3382
|
+
transform(chunk, _enc, cb) {
|
|
3383
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3384
|
+
buffer += text;
|
|
3385
|
+
const lastNewline = buffer.lastIndexOf("\n");
|
|
3386
|
+
if (lastNewline === -1) {
|
|
3387
|
+
cb(null);
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
const flushable = buffer.slice(0, lastNewline + 1);
|
|
3391
|
+
buffer = buffer.slice(lastNewline + 1);
|
|
3392
|
+
cb(null, maskSecrets(flushable));
|
|
3393
|
+
},
|
|
3394
|
+
flush(cb) {
|
|
3395
|
+
if (buffer.length > 0) {
|
|
3396
|
+
const tail = maskSecrets(buffer);
|
|
3397
|
+
buffer = "";
|
|
3398
|
+
cb(null, tail);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
cb(null);
|
|
3032
3402
|
}
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
host=${host}
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3036
3405
|
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3406
|
+
// src/devcontainer/cli.ts
|
|
3407
|
+
import { spawn as spawn4 } from "child_process";
|
|
3408
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3409
|
+
import { createRequire } from "module";
|
|
3410
|
+
import path8 from "path";
|
|
3411
|
+
var require_ = createRequire(import.meta.url);
|
|
3412
|
+
var cachedBinaryPath = null;
|
|
3413
|
+
function devcontainerCliPath() {
|
|
3414
|
+
if (cachedBinaryPath) return cachedBinaryPath;
|
|
3415
|
+
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
3416
|
+
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
3417
|
+
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
3418
|
+
if (!binEntry) {
|
|
3419
|
+
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
3420
|
+
}
|
|
3421
|
+
cachedBinaryPath = path8.resolve(path8.dirname(pkgJsonPath), binEntry);
|
|
3422
|
+
return cachedBinaryPath;
|
|
3423
|
+
}
|
|
3424
|
+
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
3425
|
+
const binPath = devcontainerCliPath();
|
|
3426
|
+
return new Promise((resolve, reject) => {
|
|
3427
|
+
if (options.interactive) {
|
|
3428
|
+
const child2 = spawn4(process.execPath, [binPath, ...args], {
|
|
3429
|
+
cwd,
|
|
3430
|
+
stdio: "inherit"
|
|
3052
3431
|
});
|
|
3053
|
-
|
|
3432
|
+
child2.on("error", reject);
|
|
3433
|
+
child2.on("exit", (code) => resolve(code ?? 0));
|
|
3434
|
+
return;
|
|
3054
3435
|
}
|
|
3055
|
-
const
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3436
|
+
const child = spawn4(process.execPath, [binPath, ...args], {
|
|
3437
|
+
cwd,
|
|
3438
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3439
|
+
});
|
|
3440
|
+
if (options.quiet) {
|
|
3441
|
+
const stdoutChunks = [];
|
|
3442
|
+
const stderrChunks = [];
|
|
3443
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
3444
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
3445
|
+
child.on("error", reject);
|
|
3446
|
+
child.on("exit", (code) => {
|
|
3447
|
+
const exitCode = code ?? 0;
|
|
3448
|
+
if (exitCode !== 0) {
|
|
3449
|
+
process.stderr.write(
|
|
3450
|
+
maskSecrets(Buffer.concat(stderrChunks).toString("utf8"))
|
|
3451
|
+
);
|
|
3452
|
+
process.stderr.write(
|
|
3453
|
+
maskSecrets(Buffer.concat(stdoutChunks).toString("utf8"))
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
resolve(exitCode);
|
|
3062
3457
|
});
|
|
3063
|
-
|
|
3458
|
+
return;
|
|
3064
3459
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3460
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
3461
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
3462
|
+
child.on("error", reject);
|
|
3463
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
3464
|
+
});
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
// src/devcontainer/compose.ts
|
|
3468
|
+
var spawnDockerCompose = (args, cwd) => {
|
|
3469
|
+
return new Promise((resolve, reject) => {
|
|
3470
|
+
const child = spawn5("docker", ["compose", ...args], {
|
|
3471
|
+
cwd,
|
|
3472
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
3473
|
+
});
|
|
3474
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
3475
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
3476
|
+
child.on("error", reject);
|
|
3477
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
3478
|
+
});
|
|
3479
|
+
};
|
|
3480
|
+
var spawnBash = (args, cwd) => {
|
|
3481
|
+
return new Promise((resolve, reject) => {
|
|
3482
|
+
const child = spawn5("bash", args, {
|
|
3483
|
+
cwd,
|
|
3484
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
3485
|
+
});
|
|
3486
|
+
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
3487
|
+
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
3488
|
+
child.on("error", reject);
|
|
3489
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
3490
|
+
});
|
|
3491
|
+
};
|
|
3492
|
+
function composeProjectName(root) {
|
|
3493
|
+
return `${path9.basename(root)}_devcontainer`;
|
|
3494
|
+
}
|
|
3495
|
+
function resolveCompose(root) {
|
|
3496
|
+
if (!existsSync3(path9.join(root, ".devcontainer"))) {
|
|
3497
|
+
throw new Error(
|
|
3498
|
+
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
3499
|
+
);
|
|
3067
3500
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3501
|
+
const composeFile = path9.join(root, ".devcontainer", "compose.yaml");
|
|
3502
|
+
if (!existsSync3(composeFile)) {
|
|
3503
|
+
throw new Error(
|
|
3504
|
+
`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.`
|
|
3505
|
+
);
|
|
3506
|
+
}
|
|
3507
|
+
return { composeFile, projectName: composeProjectName(root) };
|
|
3508
|
+
}
|
|
3509
|
+
async function runComposeAction(buildSubArgs, opts) {
|
|
3510
|
+
const { composeFile, projectName } = resolveCompose(opts.root);
|
|
3511
|
+
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
3512
|
+
const subArgs = buildSubArgs(opts.service);
|
|
3513
|
+
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
3514
|
+
}
|
|
3515
|
+
async function runStart(opts) {
|
|
3516
|
+
resolveCompose(opts.root);
|
|
3517
|
+
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
3518
|
+
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
3519
|
+
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
3520
|
+
return spawnFn(
|
|
3521
|
+
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
3522
|
+
opts.root
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
async function runContainerCycle(root, opts) {
|
|
3526
|
+
const { hasCompose, logger } = opts;
|
|
3527
|
+
if (hasCompose) {
|
|
3528
|
+
const projectName = composeProjectName(root);
|
|
3529
|
+
logger.info(
|
|
3530
|
+
`Force-removing existing ${projectName} containers (volumes preserved)\u2026`
|
|
3531
|
+
);
|
|
3532
|
+
const cleanupSpawn = opts.cleanupSpawn ?? spawnBash;
|
|
3533
|
+
const script = [
|
|
3534
|
+
`set -u`,
|
|
3535
|
+
`echo "[cleanup] checking project ${projectName}\u2026"`,
|
|
3536
|
+
`by_label=$(docker ps -aq --filter "label=com.docker.compose.project=${projectName}" 2>/dev/null || true)`,
|
|
3537
|
+
`by_name=$(docker ps -aq --filter "name=^${projectName}-" 2>/dev/null || true)`,
|
|
3538
|
+
`to_remove=$(printf "%s\\n%s\\n" "$by_label" "$by_name" | sort -u | grep -v "^$" || true)`,
|
|
3539
|
+
`if [ -n "$to_remove" ]; then echo "[cleanup] removing: $(echo $to_remove | tr "\\n" " ")"; docker rm -f $to_remove >/dev/null || true; else echo "[cleanup] no containers to remove"; fi`,
|
|
3540
|
+
`docker network rm ${projectName}_default 2>/dev/null && echo "[cleanup] network ${projectName}_default removed" || echo "[cleanup] network ${projectName}_default not present"`,
|
|
3541
|
+
`remaining_label=$(docker ps -aq --filter "label=com.docker.compose.project=${projectName}" 2>/dev/null || true)`,
|
|
3542
|
+
`remaining_name=$(docker ps -aq --filter "name=^${projectName}-" 2>/dev/null || true)`,
|
|
3543
|
+
`if [ -n "$remaining_label" ] || [ -n "$remaining_name" ]; then echo "" >&2; echo "ERROR: containers under project ${projectName} reappeared after removal." >&2; echo "This typically means VS Code's Remote Containers extension is connected to" >&2; echo "this devcontainer and auto-recreated it. Close the dev container session" >&2; echo "in VS Code (Cmd+Shift+P \u2192 'Dev Containers: Close Remote Connection')" >&2; echo "and retry \\\`monoceros apply\\\`." >&2; exit 1; fi`,
|
|
3544
|
+
`echo "[cleanup] done"`
|
|
3545
|
+
].join("; ");
|
|
3546
|
+
const cleanupCode = await cleanupSpawn(["-c", script], root);
|
|
3547
|
+
if (cleanupCode !== 0) return cleanupCode;
|
|
3548
|
+
return runStart({
|
|
3549
|
+
root,
|
|
3550
|
+
...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
|
|
3551
|
+
logger
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
logger.info(`Recreating image-mode devcontainer at ${root}\u2026`);
|
|
3555
|
+
const spawnFn = opts.devcontainerSpawn ?? spawnDevcontainer;
|
|
3556
|
+
return spawnFn(
|
|
3557
|
+
[
|
|
3558
|
+
"up",
|
|
3559
|
+
"--workspace-folder",
|
|
3560
|
+
root,
|
|
3561
|
+
"--mount-workspace-git-root=false",
|
|
3562
|
+
"--remove-existing-container"
|
|
3563
|
+
],
|
|
3564
|
+
root
|
|
3075
3565
|
);
|
|
3076
|
-
return {
|
|
3077
|
-
hostsWritten: lines.length,
|
|
3078
|
-
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
3079
|
-
perHost,
|
|
3080
|
-
credentialsPath
|
|
3081
|
-
};
|
|
3082
3566
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
`Missing Git credentials: ${hint.title}`,
|
|
3089
|
-
"",
|
|
3090
|
-
hint.body,
|
|
3091
|
-
"",
|
|
3092
|
-
`Then re-run ${cyan2("monoceros apply")}.`
|
|
3093
|
-
].join("\n");
|
|
3094
|
-
}
|
|
3095
|
-
const lines = [
|
|
3096
|
-
`Missing Git credentials for ${missing.length} hosts:`,
|
|
3097
|
-
""
|
|
3098
|
-
];
|
|
3099
|
-
for (const m of missing) {
|
|
3100
|
-
const hint = providerSetupHint(m.host, m.provider);
|
|
3101
|
-
lines.push(hint.title);
|
|
3102
|
-
lines.push("");
|
|
3103
|
-
lines.push(hint.body);
|
|
3104
|
-
lines.push("");
|
|
3105
|
-
}
|
|
3106
|
-
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
3107
|
-
return lines.join("\n");
|
|
3567
|
+
function runStop(opts) {
|
|
3568
|
+
return runComposeAction(
|
|
3569
|
+
(service) => ["stop", ...service ? [service] : []],
|
|
3570
|
+
opts
|
|
3571
|
+
);
|
|
3108
3572
|
}
|
|
3109
|
-
function
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3573
|
+
function runStatus(opts) {
|
|
3574
|
+
return runComposeAction(
|
|
3575
|
+
(service) => ["ps", ...service ? [service] : []],
|
|
3576
|
+
opts
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
function runLogs(opts) {
|
|
3580
|
+
const follow = opts.follow ?? true;
|
|
3581
|
+
return runComposeAction(
|
|
3582
|
+
(service) => [
|
|
3583
|
+
"logs",
|
|
3584
|
+
...follow ? ["-f"] : [],
|
|
3585
|
+
...service ? [service] : []
|
|
3586
|
+
],
|
|
3587
|
+
opts
|
|
3588
|
+
);
|
|
3125
3589
|
}
|
|
3126
3590
|
|
|
3127
3591
|
// src/devcontainer/repo-reachability.ts
|
|
3128
|
-
import { spawn as
|
|
3592
|
+
import { spawn as spawn6 } from "child_process";
|
|
3129
3593
|
var realGitLsRemote = (url) => {
|
|
3130
3594
|
return new Promise((resolve, reject) => {
|
|
3131
|
-
const child =
|
|
3595
|
+
const child = spawn6("git", ["ls-remote", "--heads", "--", url], {
|
|
3132
3596
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3133
3597
|
env: {
|
|
3134
3598
|
...process.env,
|
|
@@ -3266,10 +3730,10 @@ function adviceForKind(kind) {
|
|
|
3266
3730
|
}
|
|
3267
3731
|
|
|
3268
3732
|
// src/devcontainer/docker-mode.ts
|
|
3269
|
-
import { spawn as
|
|
3733
|
+
import { spawn as spawn7 } from "child_process";
|
|
3270
3734
|
var realDockerInfo = () => {
|
|
3271
3735
|
return new Promise((resolve, reject) => {
|
|
3272
|
-
const child =
|
|
3736
|
+
const child = spawn7(
|
|
3273
3737
|
"docker",
|
|
3274
3738
|
["info", "--format", "{{json .SecurityOptions}}"],
|
|
3275
3739
|
{
|
|
@@ -3328,13 +3792,13 @@ function formatRootlessNotSupportedError() {
|
|
|
3328
3792
|
}
|
|
3329
3793
|
|
|
3330
3794
|
// src/devcontainer/identity.ts
|
|
3331
|
-
import { spawn as
|
|
3795
|
+
import { spawn as spawn8 } from "child_process";
|
|
3332
3796
|
import { promises as fs9 } from "fs";
|
|
3333
|
-
import
|
|
3797
|
+
import path10 from "path";
|
|
3334
3798
|
import { consola as consola10 } from "consola";
|
|
3335
3799
|
var realGitConfigGet = (key) => {
|
|
3336
3800
|
return new Promise((resolve, reject) => {
|
|
3337
|
-
const child =
|
|
3801
|
+
const child = spawn8("git", ["config", "--global", "--get", key], {
|
|
3338
3802
|
stdio: ["ignore", "pipe", "inherit"]
|
|
3339
3803
|
});
|
|
3340
3804
|
let stdout = "";
|
|
@@ -3358,20 +3822,51 @@ var realIdentityPrompt = async (key) => {
|
|
|
3358
3822
|
const trimmed = value.trim();
|
|
3359
3823
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
3360
3824
|
};
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3825
|
+
var realScopePrompt = async (ctx) => {
|
|
3826
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3827
|
+
return ctx.reason === "prompt" ? "g" : "n";
|
|
3828
|
+
}
|
|
3829
|
+
const heading = ctx.reason === "persisted" ? `Found identity in .monoceros/gitconfig: ${ctx.name} <${ctx.email}>. Promote where?` : "Save this identity where?";
|
|
3830
|
+
const choice = await consola10.prompt(heading, {
|
|
3831
|
+
type: "select",
|
|
3832
|
+
options: [
|
|
3833
|
+
{
|
|
3834
|
+
label: "Globally \u2014 every container uses it as default",
|
|
3835
|
+
value: "g"
|
|
3836
|
+
},
|
|
3837
|
+
{
|
|
3838
|
+
label: "In this container only",
|
|
3839
|
+
value: "c"
|
|
3840
|
+
},
|
|
3841
|
+
{
|
|
3842
|
+
label: "Both \u2014 global default plus container-level entry",
|
|
3843
|
+
value: "b"
|
|
3844
|
+
},
|
|
3845
|
+
{
|
|
3846
|
+
label: "Keep as-is \u2014 do not write to monoceros-config or yml",
|
|
3847
|
+
value: "n"
|
|
3848
|
+
}
|
|
3849
|
+
],
|
|
3850
|
+
initial: "g"
|
|
3851
|
+
});
|
|
3852
|
+
if (choice === "g" || choice === "c" || choice === "b" || choice === "n") {
|
|
3853
|
+
return choice;
|
|
3854
|
+
}
|
|
3855
|
+
return void 0;
|
|
3856
|
+
};
|
|
3857
|
+
async function resolveIdentityWithPrompt(options = {}) {
|
|
3364
3858
|
const spawnFn = options.spawn ?? realGitConfigGet;
|
|
3365
3859
|
const promptFn = options.prompt ?? realIdentityPrompt;
|
|
3860
|
+
const scopePromptFn = options.scopePrompt ?? realScopePrompt;
|
|
3366
3861
|
const logger = options.logger ?? { info: () => {
|
|
3367
3862
|
}, warn: () => {
|
|
3368
3863
|
} };
|
|
3369
|
-
const
|
|
3864
|
+
const persisted = options.persistedValues ?? {};
|
|
3370
3865
|
const name = await resolveKey("user.name", {
|
|
3371
3866
|
override: options.containerOverride?.name,
|
|
3372
3867
|
defaultValue: options.defaults?.name,
|
|
3373
3868
|
spawnFn,
|
|
3374
|
-
persistedValue:
|
|
3869
|
+
persistedValue: persisted.name,
|
|
3375
3870
|
promptFn,
|
|
3376
3871
|
logger
|
|
3377
3872
|
});
|
|
@@ -3379,35 +3874,77 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
3379
3874
|
override: options.containerOverride?.email,
|
|
3380
3875
|
defaultValue: options.defaults?.email,
|
|
3381
3876
|
spawnFn,
|
|
3382
|
-
persistedValue:
|
|
3877
|
+
persistedValue: persisted.email,
|
|
3383
3878
|
promptFn,
|
|
3384
3879
|
logger
|
|
3385
3880
|
});
|
|
3881
|
+
const alreadyCanonical = !!options.containerOverride?.name || !!options.containerOverride?.email || !!options.defaults?.name || !!options.defaults?.email;
|
|
3882
|
+
const promptableSources = [
|
|
3883
|
+
"prompt",
|
|
3884
|
+
"persisted"
|
|
3885
|
+
];
|
|
3886
|
+
const bothPromotable = name?.source !== void 0 && email?.source !== void 0 && promptableSources.includes(name.source) && promptableSources.includes(email.source) && name.source === email.source;
|
|
3887
|
+
let promptedScope;
|
|
3888
|
+
if (!alreadyCanonical && bothPromotable && name?.value && email?.value) {
|
|
3889
|
+
promptedScope = await scopePromptFn({
|
|
3890
|
+
reason: name.source,
|
|
3891
|
+
name: name.value,
|
|
3892
|
+
email: email.value
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
return {
|
|
3896
|
+
...name?.value !== void 0 ? { name: name.value } : {},
|
|
3897
|
+
...email?.value !== void 0 ? { email: email.value } : {},
|
|
3898
|
+
// Only surface `prompted` when the scope is a persistence target
|
|
3899
|
+
// (`g`/`c`/`b`). `'n'` means "do nothing" — no point passing it
|
|
3900
|
+
// to the caller as a "go persist" signal.
|
|
3901
|
+
...promptedScope && promptedScope !== "n" && name?.value && email?.value ? {
|
|
3902
|
+
prompted: {
|
|
3903
|
+
name: name.value,
|
|
3904
|
+
email: email.value,
|
|
3905
|
+
scope: promptedScope
|
|
3906
|
+
}
|
|
3907
|
+
} : {}
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
3911
|
+
const gitconfigDir = path10.join(devContainerRoot, ".monoceros");
|
|
3912
|
+
const gitconfigPath = path10.join(gitconfigDir, "gitconfig");
|
|
3913
|
+
const logger = options.logger ?? { info: () => {
|
|
3914
|
+
}, warn: () => {
|
|
3915
|
+
} };
|
|
3916
|
+
const existing = await readExistingGitconfig(gitconfigPath);
|
|
3917
|
+
const resolved = await resolveIdentityWithPrompt({
|
|
3918
|
+
...options,
|
|
3919
|
+
persistedValues: existing,
|
|
3920
|
+
logger
|
|
3921
|
+
});
|
|
3386
3922
|
const lines = ["[user]"];
|
|
3387
|
-
if (name !== void 0) lines.push(` name = ${name}`);
|
|
3388
|
-
if (email !== void 0) lines.push(` email = ${email}`);
|
|
3923
|
+
if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
|
|
3924
|
+
if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
|
|
3389
3925
|
await fs9.mkdir(gitconfigDir, { recursive: true });
|
|
3390
3926
|
await fs9.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
3391
3927
|
return {
|
|
3392
|
-
...name !== void 0 ? { name } : {},
|
|
3393
|
-
...email !== void 0 ? { email } : {},
|
|
3394
|
-
gitconfigPath
|
|
3928
|
+
...resolved.name !== void 0 ? { name: resolved.name } : {},
|
|
3929
|
+
...resolved.email !== void 0 ? { email: resolved.email } : {},
|
|
3930
|
+
gitconfigPath,
|
|
3931
|
+
...resolved.prompted ? { prompted: resolved.prompted } : {}
|
|
3395
3932
|
};
|
|
3396
3933
|
}
|
|
3397
3934
|
async function resolveKey(key, opts) {
|
|
3398
3935
|
if (opts.override !== void 0 && opts.override.length > 0) {
|
|
3399
|
-
return opts.override;
|
|
3936
|
+
return { value: opts.override, source: "container" };
|
|
3400
3937
|
}
|
|
3401
3938
|
if (opts.defaultValue !== void 0 && opts.defaultValue.length > 0) {
|
|
3402
|
-
return opts.defaultValue;
|
|
3939
|
+
return { value: opts.defaultValue, source: "defaults" };
|
|
3403
3940
|
}
|
|
3404
3941
|
const hostValue = await readKeyFromHost(opts.spawnFn, key, opts.logger);
|
|
3405
|
-
if (hostValue !== void 0) return hostValue;
|
|
3942
|
+
if (hostValue !== void 0) return { value: hostValue, source: "host" };
|
|
3406
3943
|
if (opts.persistedValue !== void 0 && opts.persistedValue.length > 0) {
|
|
3407
|
-
return opts.persistedValue;
|
|
3944
|
+
return { value: opts.persistedValue, source: "persisted" };
|
|
3408
3945
|
}
|
|
3409
3946
|
const prompted = await opts.promptFn(key);
|
|
3410
|
-
if (prompted !== void 0) return prompted;
|
|
3947
|
+
if (prompted !== void 0) return { value: prompted, source: "prompt" };
|
|
3411
3948
|
opts.logger.warn(
|
|
3412
3949
|
`No ${key} resolvable (yml override, monoceros-config.yml defaults, host \`git config --global\`, persisted .monoceros/gitconfig, prompt). Container git will have no ${key} until set explicitly.`
|
|
3413
3950
|
);
|
|
@@ -3489,13 +4026,17 @@ ${sectionLine(label)}
|
|
|
3489
4026
|
warn: logger.warn ?? logger.info
|
|
3490
4027
|
};
|
|
3491
4028
|
if (hasRepos || hasContainerGitUser || hasDefaultGitUser) {
|
|
3492
|
-
await collectGitIdentity(targetDir, {
|
|
4029
|
+
const identity = await collectGitIdentity(targetDir, {
|
|
3493
4030
|
...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
|
|
3494
4031
|
...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
|
|
4032
|
+
...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
|
|
3495
4033
|
...parsed.config.git?.user ? { containerOverride: parsed.config.git.user } : {},
|
|
3496
4034
|
...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
|
|
3497
4035
|
logger: idLogger
|
|
3498
4036
|
});
|
|
4037
|
+
if (identity.prompted) {
|
|
4038
|
+
await persistPromptedIdentity(identity.prompted, ymlPath, home, logger);
|
|
4039
|
+
}
|
|
3499
4040
|
}
|
|
3500
4041
|
const hostsToFetch = uniqueHttpsHosts(createOpts.repos ?? []);
|
|
3501
4042
|
const unknownProviderHosts = hostsToFetch.filter((h) => h.provider === "unknown").map((h) => h.host);
|
|
@@ -3628,9 +4169,59 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
|
|
|
3628
4169
|
}
|
|
3629
4170
|
}
|
|
3630
4171
|
}
|
|
4172
|
+
async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
4173
|
+
const wantGlobal = prompted.scope === "g" || prompted.scope === "b";
|
|
4174
|
+
const wantContainer = prompted.scope === "c" || prompted.scope === "b";
|
|
4175
|
+
if (wantGlobal) {
|
|
4176
|
+
try {
|
|
4177
|
+
const result = await writeGlobalDefaultGitUser(
|
|
4178
|
+
{ name: prompted.name, email: prompted.email },
|
|
4179
|
+
{ monocerosHome: home }
|
|
4180
|
+
);
|
|
4181
|
+
if (result.alreadySet) {
|
|
4182
|
+
logger.warn?.(
|
|
4183
|
+
`monoceros-config.yml already has a defaults.git.user \u2014 left it alone. To replace, edit ${prettyPath(result.filePath)} by hand.`
|
|
4184
|
+
);
|
|
4185
|
+
} else if (result.created) {
|
|
4186
|
+
logger.info(
|
|
4187
|
+
`Saved identity globally \u2014 created ${prettyPath(result.filePath)} with defaults.git.user.`
|
|
4188
|
+
);
|
|
4189
|
+
} else {
|
|
4190
|
+
logger.info(
|
|
4191
|
+
`Saved identity globally \u2014 wrote defaults.git.user into ${prettyPath(result.filePath)}.`
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
} catch (err) {
|
|
4195
|
+
logger.warn?.(
|
|
4196
|
+
`Could not persist identity to monoceros-config.yml: ${err instanceof Error ? err.message : String(err)}. The values are still active for this apply via .monoceros/gitconfig.`
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
if (wantContainer) {
|
|
4201
|
+
try {
|
|
4202
|
+
const text = await fs10.readFile(ymlPath, "utf8");
|
|
4203
|
+
const parsed = parseConfig(text, ymlPath);
|
|
4204
|
+
const changed = setContainerGitUserInDoc(parsed.doc, {
|
|
4205
|
+
name: prompted.name,
|
|
4206
|
+
email: prompted.email
|
|
4207
|
+
});
|
|
4208
|
+
if (changed) {
|
|
4209
|
+
const out = stringifyConfig(parsed.doc);
|
|
4210
|
+
await fs10.writeFile(ymlPath, out, "utf8");
|
|
4211
|
+
logger.info(
|
|
4212
|
+
`Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
4215
|
+
} catch (err) {
|
|
4216
|
+
logger.warn?.(
|
|
4217
|
+
`Could not persist identity to ${prettyPath(ymlPath)}: ${err instanceof Error ? err.message : String(err)}. The values are still active for this apply via .monoceros/gitconfig.`
|
|
4218
|
+
);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
3631
4222
|
|
|
3632
4223
|
// src/version.ts
|
|
3633
|
-
var CLI_VERSION = true ? "1.
|
|
4224
|
+
var CLI_VERSION = true ? "1.8.0" : "dev";
|
|
3634
4225
|
|
|
3635
4226
|
// src/commands/_dispatch.ts
|
|
3636
4227
|
import { consola as consola12 } from "consola";
|
|
@@ -3892,7 +4483,7 @@ import { consola as consola13 } from "consola";
|
|
|
3892
4483
|
|
|
3893
4484
|
// src/init/components.ts
|
|
3894
4485
|
import { existsSync as existsSync5, promises as fs11 } from "fs";
|
|
3895
|
-
import
|
|
4486
|
+
import path11 from "path";
|
|
3896
4487
|
import { z as z3 } from "zod";
|
|
3897
4488
|
import { parse as parseYaml } from "yaml";
|
|
3898
4489
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -3949,14 +4540,14 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
3949
4540
|
async function walk(baseDir, currentDir, out) {
|
|
3950
4541
|
const entries = await fs11.readdir(currentDir, { withFileTypes: true });
|
|
3951
4542
|
for (const entry2 of entries) {
|
|
3952
|
-
const full =
|
|
4543
|
+
const full = path11.join(currentDir, entry2.name);
|
|
3953
4544
|
if (entry2.isDirectory()) {
|
|
3954
4545
|
await walk(baseDir, full, out);
|
|
3955
4546
|
continue;
|
|
3956
4547
|
}
|
|
3957
4548
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
3958
|
-
const relative =
|
|
3959
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
4549
|
+
const relative = path11.relative(baseDir, full);
|
|
4550
|
+
const name = relative.replace(/\.yml$/, "").split(path11.sep).join("/");
|
|
3960
4551
|
const text = await fs11.readFile(full, "utf8");
|
|
3961
4552
|
let raw;
|
|
3962
4553
|
try {
|
|
@@ -4056,35 +4647,49 @@ Available: ${available.join(", ")}.`
|
|
|
4056
4647
|
}
|
|
4057
4648
|
|
|
4058
4649
|
// src/init/generator.ts
|
|
4059
|
-
var
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
"#",
|
|
4063
|
-
"# Each section below carries inline comments for the options it",
|
|
4064
|
-
"# accepts. Features expose additional knobs as commented hints",
|
|
4065
|
-
"# under their `options:` block \u2014 un-comment what you need."
|
|
4066
|
-
];
|
|
4650
|
+
var SCHEMA_HEADER_ACTIVE = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Edit any section, then run `monoceros apply <name>` to (re-)build.";
|
|
4651
|
+
var SCHEMA_HEADER_DOCUMENTED = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Every section is commented out by default; un-comment what you need\n# (strip one `#` per line of the block), then run `monoceros apply <name>`.";
|
|
4652
|
+
var COMMENT_WIDTH = 76;
|
|
4067
4653
|
function generateComposedYml(name, components, lookupManifest, repoUrls = [], ports = []) {
|
|
4068
4654
|
const merged = mergeComponents(components);
|
|
4069
4655
|
const lines = [];
|
|
4070
|
-
|
|
4656
|
+
pushHeader(lines, SCHEMA_HEADER_ACTIVE, name);
|
|
4071
4657
|
lines.push("");
|
|
4072
4658
|
lines.push("schemaVersion: 1");
|
|
4073
4659
|
lines.push(`name: ${name}`);
|
|
4074
4660
|
lines.push("");
|
|
4075
4661
|
if (merged.languages.length > 0) {
|
|
4662
|
+
pushSectionHeader(
|
|
4663
|
+
lines,
|
|
4664
|
+
LANGUAGES_HEADER,
|
|
4665
|
+
/* commented */
|
|
4666
|
+
false
|
|
4667
|
+
);
|
|
4076
4668
|
lines.push("languages:");
|
|
4077
4669
|
for (const lang of merged.languages) lines.push(` - ${lang}`);
|
|
4078
4670
|
lines.push("");
|
|
4079
4671
|
}
|
|
4080
4672
|
if (merged.services.length > 0) {
|
|
4673
|
+
pushSectionHeader(
|
|
4674
|
+
lines,
|
|
4675
|
+
SERVICES_HEADER,
|
|
4676
|
+
/* commented */
|
|
4677
|
+
false
|
|
4678
|
+
);
|
|
4081
4679
|
lines.push("services:");
|
|
4082
4680
|
for (const svc of merged.services) lines.push(` - ${svc}`);
|
|
4083
4681
|
lines.push("");
|
|
4084
4682
|
}
|
|
4085
4683
|
if (merged.features.length > 0) {
|
|
4684
|
+
pushSectionHeader(
|
|
4685
|
+
lines,
|
|
4686
|
+
FEATURES_HEADER_ACTIVE,
|
|
4687
|
+
/* commented */
|
|
4688
|
+
false
|
|
4689
|
+
);
|
|
4086
4690
|
lines.push("features:");
|
|
4087
4691
|
for (const f of merged.features) {
|
|
4692
|
+
lines.push("");
|
|
4088
4693
|
renderFeatureBlock(
|
|
4089
4694
|
lines,
|
|
4090
4695
|
f,
|
|
@@ -4096,87 +4701,86 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
4096
4701
|
lines.push("");
|
|
4097
4702
|
}
|
|
4098
4703
|
if (repoUrls.length > 0) {
|
|
4099
|
-
|
|
4704
|
+
pushSectionHeader(
|
|
4100
4705
|
lines,
|
|
4101
|
-
|
|
4706
|
+
REPOS_HEADER,
|
|
4102
4707
|
/* commented */
|
|
4103
4708
|
false
|
|
4104
4709
|
);
|
|
4710
|
+
lines.push("repos:");
|
|
4711
|
+
for (const url of repoUrls) {
|
|
4712
|
+
lines.push(` - url: ${url}`);
|
|
4713
|
+
lines.push(" # path:");
|
|
4714
|
+
lines.push(" # provider:");
|
|
4715
|
+
lines.push(" # git:");
|
|
4716
|
+
lines.push(" # user:");
|
|
4717
|
+
lines.push(" # name:");
|
|
4718
|
+
lines.push(" # email:");
|
|
4719
|
+
}
|
|
4720
|
+
lines.push("");
|
|
4105
4721
|
}
|
|
4106
4722
|
if (ports.length > 0) {
|
|
4107
|
-
|
|
4723
|
+
pushSectionHeader(
|
|
4724
|
+
lines,
|
|
4725
|
+
routingHeader(name),
|
|
4726
|
+
/* commented */
|
|
4727
|
+
false
|
|
4728
|
+
);
|
|
4729
|
+
lines.push("routing:");
|
|
4730
|
+
lines.push(" ports:");
|
|
4731
|
+
for (const port of ports) {
|
|
4732
|
+
lines.push(` - ${port}`);
|
|
4733
|
+
}
|
|
4734
|
+
lines.push(" # vscodeAutoForward: false");
|
|
4735
|
+
lines.push("");
|
|
4108
4736
|
}
|
|
4109
4737
|
return ensureTrailingNewline(lines.join("\n"));
|
|
4110
4738
|
}
|
|
4111
4739
|
function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], ports = []) {
|
|
4112
4740
|
const byCategory = groupByCategory(catalog);
|
|
4113
4741
|
const lines = [];
|
|
4114
|
-
|
|
4115
|
-
lines.push("#");
|
|
4116
|
-
lines.push("# Below is the full set of components shipped with this");
|
|
4117
|
-
lines.push("# workbench, every one commented out. Un-comment the lines");
|
|
4118
|
-
lines.push("# you want active. The same effect (and a cleaner yml) is");
|
|
4119
|
-
lines.push("# achievable by running `monoceros init <name> --with=\u2026`");
|
|
4120
|
-
lines.push("# with a comma-separated list of component names.");
|
|
4742
|
+
pushHeader(lines, SCHEMA_HEADER_DOCUMENTED, name);
|
|
4121
4743
|
lines.push("");
|
|
4122
4744
|
lines.push("schemaVersion: 1");
|
|
4123
4745
|
lines.push(`name: ${name}`);
|
|
4124
4746
|
lines.push("");
|
|
4125
4747
|
if (byCategory.language.length > 0) {
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4748
|
+
pushSectionHeader(
|
|
4749
|
+
lines,
|
|
4750
|
+
LANGUAGES_HEADER,
|
|
4751
|
+
/* commented */
|
|
4752
|
+
true
|
|
4131
4753
|
);
|
|
4132
|
-
const width = Math.max(...items.map((i) => i.value.length)) + 2;
|
|
4133
|
-
lines.push("# Languages \u2014 runtime toolchains.");
|
|
4134
4754
|
lines.push("# languages:");
|
|
4135
|
-
for (const
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4755
|
+
for (const c of byCategory.language) {
|
|
4756
|
+
for (const lang of c.file.contributes.languages ?? []) {
|
|
4757
|
+
lines.push(`# - ${lang}`);
|
|
4758
|
+
}
|
|
4138
4759
|
}
|
|
4139
4760
|
lines.push("");
|
|
4140
4761
|
}
|
|
4141
4762
|
if (byCategory.service.length > 0) {
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4763
|
+
pushSectionHeader(
|
|
4764
|
+
lines,
|
|
4765
|
+
SERVICES_HEADER,
|
|
4766
|
+
/* commented */
|
|
4767
|
+
true
|
|
4147
4768
|
);
|
|
4148
|
-
const width = Math.max(...items.map((i) => i.value.length)) + 2;
|
|
4149
|
-
lines.push("# Services \u2014 compose-mode siblings of the workspace");
|
|
4150
|
-
lines.push("# container (compose mode kicks in as soon as at least");
|
|
4151
|
-
lines.push("# one service is active).");
|
|
4152
4769
|
lines.push("# services:");
|
|
4153
|
-
for (const
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4770
|
+
for (const c of byCategory.service) {
|
|
4771
|
+
for (const svc of c.file.contributes.services ?? []) {
|
|
4772
|
+
lines.push(`# - ${svc}`);
|
|
4773
|
+
}
|
|
4156
4774
|
}
|
|
4157
4775
|
lines.push("");
|
|
4158
4776
|
}
|
|
4159
4777
|
if (byCategory.feature.length > 0) {
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
lines.push("#");
|
|
4167
|
-
lines.push("# Catalog:");
|
|
4168
|
-
lines.push("#");
|
|
4169
|
-
const nameColumnWidth = Math.max(...byCategory.feature.map((c) => c.name.length)) + 2;
|
|
4170
|
-
for (const c of byCategory.feature) {
|
|
4171
|
-
const pad = " ".repeat(nameColumnWidth - c.name.length);
|
|
4172
|
-
lines.push(`# ${c.name}${pad}${c.file.displayName}`);
|
|
4173
|
-
}
|
|
4174
|
-
lines.push("#");
|
|
4175
|
-
lines.push("# Below: one block per feature ref. Un-comment what");
|
|
4176
|
-
lines.push("# you want active. Sub-components share their parent's");
|
|
4177
|
-
lines.push("# block \u2014 pick the parent for the full preset, swap to");
|
|
4178
|
-
lines.push("# a sub-component name for a partial install.");
|
|
4179
|
-
lines.push("#");
|
|
4778
|
+
pushSectionHeader(
|
|
4779
|
+
lines,
|
|
4780
|
+
FEATURES_HEADER_DOCUMENTED,
|
|
4781
|
+
/* commented */
|
|
4782
|
+
true
|
|
4783
|
+
);
|
|
4180
4784
|
lines.push("# features:");
|
|
4181
4785
|
const renderedRefs = /* @__PURE__ */ new Set();
|
|
4182
4786
|
const topLevel = byCategory.feature.filter((c) => !c.name.includes("/"));
|
|
@@ -4184,6 +4788,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4184
4788
|
for (const f of c.file.contributes.features ?? []) {
|
|
4185
4789
|
if (renderedRefs.has(f.ref)) continue;
|
|
4186
4790
|
renderedRefs.add(f.ref);
|
|
4791
|
+
lines.push("#");
|
|
4187
4792
|
renderFeatureBlock(
|
|
4188
4793
|
lines,
|
|
4189
4794
|
f,
|
|
@@ -4198,6 +4803,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4198
4803
|
for (const f of c.file.contributes.features ?? []) {
|
|
4199
4804
|
if (renderedRefs.has(f.ref)) continue;
|
|
4200
4805
|
renderedRefs.add(f.ref);
|
|
4806
|
+
lines.push("#");
|
|
4201
4807
|
renderFeatureBlock(
|
|
4202
4808
|
lines,
|
|
4203
4809
|
f,
|
|
@@ -4209,165 +4815,156 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4209
4815
|
}
|
|
4210
4816
|
lines.push("");
|
|
4211
4817
|
}
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4818
|
+
if (repoUrls.length > 0) {
|
|
4819
|
+
pushSectionHeader(
|
|
4820
|
+
lines,
|
|
4821
|
+
REPOS_HEADER,
|
|
4822
|
+
/* commented */
|
|
4823
|
+
false
|
|
4824
|
+
);
|
|
4825
|
+
lines.push("repos:");
|
|
4826
|
+
for (const url of repoUrls) {
|
|
4827
|
+
lines.push(` - url: ${url}`);
|
|
4828
|
+
}
|
|
4829
|
+
lines.push("");
|
|
4830
|
+
} else {
|
|
4831
|
+
pushSectionHeader(
|
|
4832
|
+
lines,
|
|
4833
|
+
REPOS_HEADER,
|
|
4834
|
+
/* commented */
|
|
4835
|
+
true
|
|
4836
|
+
);
|
|
4837
|
+
lines.push("# repos:");
|
|
4838
|
+
lines.push("# - url: https://github.com/<org>/<repo>.git");
|
|
4839
|
+
lines.push("# path: <folder>");
|
|
4840
|
+
lines.push("# provider: github");
|
|
4841
|
+
lines.push("# git:");
|
|
4842
|
+
lines.push("# user:");
|
|
4843
|
+
lines.push("# name: Your Name");
|
|
4844
|
+
lines.push("# email: you@example.com");
|
|
4845
|
+
lines.push("");
|
|
4846
|
+
}
|
|
4218
4847
|
if (ports.length > 0) {
|
|
4219
|
-
|
|
4848
|
+
pushSectionHeader(
|
|
4849
|
+
lines,
|
|
4850
|
+
routingHeader(name),
|
|
4851
|
+
/* commented */
|
|
4852
|
+
false
|
|
4853
|
+
);
|
|
4854
|
+
lines.push("routing:");
|
|
4855
|
+
lines.push(" ports:");
|
|
4856
|
+
for (const port of ports) {
|
|
4857
|
+
lines.push(` - ${port}`);
|
|
4858
|
+
}
|
|
4859
|
+
lines.push(" # vscodeAutoForward: false");
|
|
4860
|
+
lines.push("");
|
|
4220
4861
|
} else {
|
|
4221
|
-
|
|
4862
|
+
pushSectionHeader(
|
|
4863
|
+
lines,
|
|
4864
|
+
routingHeader(name),
|
|
4865
|
+
/* commented */
|
|
4866
|
+
true
|
|
4867
|
+
);
|
|
4868
|
+
lines.push("# routing:");
|
|
4869
|
+
lines.push("# ports:");
|
|
4870
|
+
lines.push("# - 3000");
|
|
4871
|
+
lines.push("# - 5173");
|
|
4872
|
+
lines.push("# vscodeAutoForward: false");
|
|
4873
|
+
lines.push("");
|
|
4222
4874
|
}
|
|
4223
4875
|
return ensureTrailingNewline(lines.join("\n"));
|
|
4224
4876
|
}
|
|
4225
|
-
var
|
|
4877
|
+
var LANGUAGES_HEADER = "Language runtimes installed inside the dev-container. Pick the ones your projects build against. The catalog of available runtimes is shown by `monoceros list-components`.";
|
|
4878
|
+
var SERVICES_HEADER = "Sibling containers that run alongside the dev-container (databases, caches, message queues, \u2026). Each service is reachable from inside the dev-container by its name as hostname (e.g. `postgres://postgres:5432`). Activating any service switches the container to docker-compose mode automatically.";
|
|
4879
|
+
var FEATURES_HEADER_ACTIVE = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. The features active for this container are listed below; adjust their options as needed. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
4880
|
+
var FEATURES_HEADER_DOCUMENTED = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. Un-comment the blocks below for the features you want active. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
4881
|
+
var REPOS_HEADER = "Git repositories cloned into `projects/` on container start-up. HTTPS URLs only. The provider is auto-detected for github.com / gitlab.com / bitbucket.org; for any other host (self-hosted GitLab, Gitea, \u2026) declare `provider:` explicitly. Add more later with `monoceros add-repo`.";
|
|
4882
|
+
function routingHeader(name) {
|
|
4883
|
+
return `Container ports exposed to the host through Traefik. Reach them in your browser as ${name}-<port>.localhost (e.g. ${name}-3000.localhost). The first entry is the default route and is also reachable as the bare ${name}.localhost. Manage the list with \`monoceros add-port\`.`;
|
|
4884
|
+
}
|
|
4226
4885
|
function renderFeatureBlock(out, feature, summary, commented) {
|
|
4227
|
-
const
|
|
4228
|
-
const
|
|
4229
|
-
const
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
)) {
|
|
4237
|
-
out.push(`${c}# ${line}`);
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
out.push(`${c}- ref: ${feature.ref}`);
|
|
4886
|
+
const yamlPrefix = commented ? "# " : "";
|
|
4887
|
+
const headerLines = buildHeaderLines(summary);
|
|
4888
|
+
for (const line of headerLines) {
|
|
4889
|
+
const wrapped = wrapToComment(line, COMMENT_WIDTH - 2);
|
|
4890
|
+
for (const wl of wrapped) {
|
|
4891
|
+
out.push(`# ${wl}`.trimEnd());
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
out.push(`${yamlPrefix} - ref: ${feature.ref}`);
|
|
4241
4895
|
const options = feature.options ?? {};
|
|
4242
|
-
const
|
|
4243
|
-
const
|
|
4244
|
-
if (
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
if (remainingHints.length > 0) {
|
|
4250
|
-
out.push(
|
|
4251
|
-
`${c} # Optional \u2014 override monoceros-config.yml defaults.features:`
|
|
4252
|
-
);
|
|
4253
|
-
for (const hint of remainingHints) {
|
|
4254
|
-
emitHint(out, hint, optionDescriptions[hint], `${c} `);
|
|
4255
|
-
}
|
|
4896
|
+
const activeKeys = Object.entries(options);
|
|
4897
|
+
const hintKeys = (summary?.optionHints ?? []).filter((h) => !(h in options));
|
|
4898
|
+
if (activeKeys.length === 0 && hintKeys.length === 0) return;
|
|
4899
|
+
if (commented) {
|
|
4900
|
+
out.push(`${yamlPrefix} options:`);
|
|
4901
|
+
for (const [key, value] of activeKeys) {
|
|
4902
|
+
out.push(`${yamlPrefix} ${key}: ${renderScalarValue(value)}`);
|
|
4256
4903
|
}
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4904
|
+
for (const key of hintKeys) {
|
|
4905
|
+
out.push(`${yamlPrefix} ${key}:`);
|
|
4906
|
+
}
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
4909
|
+
if (activeKeys.length > 0) {
|
|
4910
|
+
out.push(` options:`);
|
|
4911
|
+
for (const [key, value] of activeKeys) {
|
|
4912
|
+
out.push(` ${key}: ${renderScalarValue(value)}`);
|
|
4264
4913
|
}
|
|
4265
4914
|
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
description,
|
|
4271
|
-
COMMENT_WIDTH - linePrefix.length
|
|
4272
|
-
)) {
|
|
4273
|
-
out.push(`${linePrefix}# ${line}`);
|
|
4915
|
+
if (hintKeys.length > 0) {
|
|
4916
|
+
out.push(` # options:`);
|
|
4917
|
+
for (const key of hintKeys) {
|
|
4918
|
+
out.push(` # ${key}:`);
|
|
4274
4919
|
}
|
|
4275
4920
|
}
|
|
4276
|
-
out.push(`${linePrefix}${hint}:`);
|
|
4277
4921
|
}
|
|
4278
|
-
function
|
|
4279
|
-
out
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
if (
|
|
4286
|
-
out.push(
|
|
4287
|
-
|
|
4288
|
-
out.push(
|
|
4289
|
-
|
|
4290
|
-
);
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
);
|
|
4294
|
-
out.push(
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4922
|
+
function buildHeaderLines(summary) {
|
|
4923
|
+
const out = [];
|
|
4924
|
+
if (!summary) {
|
|
4925
|
+
return out;
|
|
4926
|
+
}
|
|
4927
|
+
const tagline = summary.name?.trim();
|
|
4928
|
+
const description = summary.description?.trim();
|
|
4929
|
+
if (tagline && description) {
|
|
4930
|
+
out.push(`${tagline} \u2014 ${description}`);
|
|
4931
|
+
} else if (tagline) {
|
|
4932
|
+
out.push(tagline);
|
|
4933
|
+
} else if (description) {
|
|
4934
|
+
out.push(description);
|
|
4935
|
+
}
|
|
4936
|
+
for (const note of summary.usageNotes) {
|
|
4937
|
+
const trimmed = note.trim();
|
|
4938
|
+
if (trimmed.length > 0) out.push(trimmed);
|
|
4939
|
+
}
|
|
4940
|
+
if (summary.optionHints.length > 0) {
|
|
4941
|
+
const parts = summary.optionHints.map((key) => {
|
|
4942
|
+
const desc = summary.optionDescriptions[key];
|
|
4943
|
+
const short = desc ? shortenOptionDescription(desc) : void 0;
|
|
4944
|
+
return short ? `${key} (${short})` : key;
|
|
4945
|
+
});
|
|
4946
|
+
out.push(`Options: ${parts.join(", ")}.`);
|
|
4302
4947
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
out.push(
|
|
4308
|
-
` # path: ${derivedPath} # subfolder under projects/; default: URL-derived (${derivedPath})`
|
|
4309
|
-
);
|
|
4310
|
-
out.push(
|
|
4311
|
-
" # provider: github # github | gitlab | bitbucket | gitea"
|
|
4312
|
-
);
|
|
4313
|
-
out.push(
|
|
4314
|
-
" # git: # per-repo committer identity override"
|
|
4315
|
-
);
|
|
4316
|
-
out.push(" # user:");
|
|
4317
|
-
out.push(" # name: Your Name");
|
|
4318
|
-
out.push(" # email: you@example.com");
|
|
4319
|
-
}
|
|
4320
|
-
out.push("");
|
|
4321
|
-
}
|
|
4322
|
-
function renderRoutingHintBlock(out) {
|
|
4323
|
-
out.push("# Routing \u2014 expose container ports to the host through the");
|
|
4324
|
-
out.push("# shared Traefik singleton. Once any port is declared the");
|
|
4325
|
-
out.push("# container joins the monoceros-proxy network and the proxy");
|
|
4326
|
-
out.push("# routes <name>.localhost (default port) and");
|
|
4327
|
-
out.push("# <name>-<port>.localhost (explicit). `monoceros add-port`");
|
|
4328
|
-
out.push("# manages the list; the block appears on first add. You can");
|
|
4329
|
-
out.push("# also pre-seed at init time via `--with-ports=3000,5173,\u2026`.");
|
|
4330
|
-
out.push("#");
|
|
4331
|
-
out.push("# routing:");
|
|
4332
|
-
out.push("# ports: # internal container ports");
|
|
4333
|
-
out.push(
|
|
4334
|
-
"# - 3000 # first entry doubles as <name>.localhost"
|
|
4335
|
-
);
|
|
4336
|
-
out.push("# - 5173");
|
|
4337
|
-
out.push(
|
|
4338
|
-
"# vscodeAutoForward: false # default: false. Traefik is the single"
|
|
4339
|
-
);
|
|
4340
|
-
out.push(
|
|
4341
|
-
"# # source of truth \u2014 set true only if you"
|
|
4342
|
-
);
|
|
4343
|
-
out.push(
|
|
4344
|
-
"# # want VS Code's port panel as primary."
|
|
4345
|
-
);
|
|
4346
|
-
out.push("");
|
|
4347
|
-
}
|
|
4348
|
-
function renderActiveRoutingBlock(out, name, ports) {
|
|
4349
|
-
out.push("# Routing \u2014 expose these container ports to the host through");
|
|
4350
|
-
out.push("# the shared Traefik singleton. First entry doubles as");
|
|
4351
|
-
out.push(`# http://${name}.localhost (the default route).`);
|
|
4352
|
-
out.push("routing:");
|
|
4353
|
-
out.push(" ports:");
|
|
4354
|
-
ports.forEach((port, idx) => {
|
|
4355
|
-
if (idx === 0) {
|
|
4356
|
-
out.push(` - ${port} # default \u2192 http://${name}.localhost`);
|
|
4357
|
-
} else {
|
|
4358
|
-
out.push(` - ${port}`);
|
|
4359
|
-
}
|
|
4360
|
-
});
|
|
4361
|
-
out.push(" # vscodeAutoForward: false # set true to keep VS Code's");
|
|
4362
|
-
out.push(" # # port-panel alongside Traefik");
|
|
4363
|
-
out.push("");
|
|
4948
|
+
if (summary.documentationURL) {
|
|
4949
|
+
out.push(`See ${summary.documentationURL} for further information.`);
|
|
4950
|
+
}
|
|
4951
|
+
return out;
|
|
4364
4952
|
}
|
|
4365
|
-
function
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4953
|
+
function shortenOptionDescription(desc) {
|
|
4954
|
+
const firstSentence = desc.split(/(?<=[.!?])\s+/)[0]?.trim() ?? desc.trim();
|
|
4955
|
+
return firstSentence.replace(/[.!?]+$/, "").trim();
|
|
4956
|
+
}
|
|
4957
|
+
function pushHeader(out, header, name) {
|
|
4958
|
+
for (const line of header.replace(/<name>/g, name).split("\n")) {
|
|
4959
|
+
out.push(line);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
function pushSectionHeader(out, text, _commented) {
|
|
4963
|
+
void _commented;
|
|
4964
|
+
const wrapped = wrapToComment(text, COMMENT_WIDTH - 2);
|
|
4965
|
+
for (const wl of wrapped) {
|
|
4966
|
+
out.push(`# ${wl}`.trimEnd());
|
|
4967
|
+
}
|
|
4371
4968
|
}
|
|
4372
4969
|
function wrapToComment(text, width) {
|
|
4373
4970
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -4416,10 +5013,10 @@ function ensureTrailingNewline(s) {
|
|
|
4416
5013
|
|
|
4417
5014
|
// src/init/manifest.ts
|
|
4418
5015
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
4419
|
-
import
|
|
5016
|
+
import path12 from "path";
|
|
4420
5017
|
function resolveManifestPath(name, checkoutRoot) {
|
|
4421
5018
|
if (checkoutRoot) {
|
|
4422
|
-
const checkoutPath =
|
|
5019
|
+
const checkoutPath = path12.join(
|
|
4423
5020
|
checkoutRoot,
|
|
4424
5021
|
"images",
|
|
4425
5022
|
"features",
|
|
@@ -4428,7 +5025,7 @@ function resolveManifestPath(name, checkoutRoot) {
|
|
|
4428
5025
|
);
|
|
4429
5026
|
if (existsSync6(checkoutPath)) return checkoutPath;
|
|
4430
5027
|
}
|
|
4431
|
-
const bundlePath =
|
|
5028
|
+
const bundlePath = path12.join(
|
|
4432
5029
|
bundledFeaturesDir(),
|
|
4433
5030
|
name,
|
|
4434
5031
|
"devcontainer-feature.json"
|
|
@@ -4460,7 +5057,18 @@ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot())
|
|
|
4460
5057
|
}
|
|
4461
5058
|
}
|
|
4462
5059
|
}
|
|
4463
|
-
|
|
5060
|
+
const name = typeof parsed.name === "string" ? parsed.name : "";
|
|
5061
|
+
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
5062
|
+
const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
|
|
5063
|
+
const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
|
|
5064
|
+
return {
|
|
5065
|
+
name,
|
|
5066
|
+
description,
|
|
5067
|
+
documentationURL,
|
|
5068
|
+
optionHints,
|
|
5069
|
+
optionDescriptions,
|
|
5070
|
+
usageNotes
|
|
5071
|
+
};
|
|
4464
5072
|
} catch {
|
|
4465
5073
|
return void 0;
|
|
4466
5074
|
}
|
|
@@ -4538,6 +5146,17 @@ async function runInit(opts) {
|
|
|
4538
5146
|
seenPorts.add(raw);
|
|
4539
5147
|
ports.push(raw);
|
|
4540
5148
|
}
|
|
5149
|
+
let promptedIdentity;
|
|
5150
|
+
if (repos.length > 0) {
|
|
5151
|
+
const globalConfig = await readMonocerosConfig({ monocerosHome: home });
|
|
5152
|
+
promptedIdentity = await resolveIdentityWithPrompt({
|
|
5153
|
+
...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
|
|
5154
|
+
...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
|
|
5155
|
+
...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
|
|
5156
|
+
...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
|
|
5157
|
+
logger: { info: logger.info, warn: logger.info }
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
4541
5160
|
let text;
|
|
4542
5161
|
const requested = opts.with ?? [];
|
|
4543
5162
|
if (requested.length === 0) {
|
|
@@ -4548,6 +5167,51 @@ async function runInit(opts) {
|
|
|
4548
5167
|
}
|
|
4549
5168
|
await fs12.mkdir(containerConfigsDir(home), { recursive: true });
|
|
4550
5169
|
await fs12.writeFile(dest, text, "utf8");
|
|
5170
|
+
if (promptedIdentity?.prompted) {
|
|
5171
|
+
const { name, email, scope } = promptedIdentity.prompted;
|
|
5172
|
+
if (scope === "g" || scope === "b") {
|
|
5173
|
+
try {
|
|
5174
|
+
const result = await writeGlobalDefaultGitUser(
|
|
5175
|
+
{ name, email },
|
|
5176
|
+
{ monocerosHome: home }
|
|
5177
|
+
);
|
|
5178
|
+
if (result.alreadySet) {
|
|
5179
|
+
logger.info(
|
|
5180
|
+
`monoceros-config.yml already had a defaults.git.user \u2014 left it alone.`
|
|
5181
|
+
);
|
|
5182
|
+
} else if (result.created) {
|
|
5183
|
+
logger.info(
|
|
5184
|
+
`Saved identity globally \u2014 created ${prettyPath(result.filePath)} with defaults.git.user.`
|
|
5185
|
+
);
|
|
5186
|
+
} else {
|
|
5187
|
+
logger.info(
|
|
5188
|
+
`Saved identity globally to ${prettyPath(result.filePath)}.`
|
|
5189
|
+
);
|
|
5190
|
+
}
|
|
5191
|
+
} catch (err) {
|
|
5192
|
+
logger.info(
|
|
5193
|
+
`Could not persist identity to monoceros-config.yml: ${err instanceof Error ? err.message : String(err)}. \`monoceros apply\` will re-prompt.`
|
|
5194
|
+
);
|
|
5195
|
+
}
|
|
5196
|
+
}
|
|
5197
|
+
if (scope === "c" || scope === "b") {
|
|
5198
|
+
try {
|
|
5199
|
+
const written = await fs12.readFile(dest, "utf8");
|
|
5200
|
+
const parsed = parseConfig(written, dest);
|
|
5201
|
+
const changed = setContainerGitUserInDoc(parsed.doc, { name, email });
|
|
5202
|
+
if (changed) {
|
|
5203
|
+
await fs12.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
|
|
5204
|
+
logger.info(
|
|
5205
|
+
`Saved identity in ${prettyPath(dest)} (container-level git.user).`
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
} catch (err) {
|
|
5209
|
+
logger.info(
|
|
5210
|
+
`Could not persist identity into ${prettyPath(dest)}: ${err instanceof Error ? err.message : String(err)}. \`monoceros apply\` will re-prompt.`
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
4551
5215
|
const documented = requested.length === 0;
|
|
4552
5216
|
const displayPath = prettyPath(dest);
|
|
4553
5217
|
if (documented) {
|
|
@@ -4971,7 +5635,7 @@ import { createInterface } from "readline/promises";
|
|
|
4971
5635
|
|
|
4972
5636
|
// src/remove/index.ts
|
|
4973
5637
|
import { existsSync as existsSync8, promises as fs13 } from "fs";
|
|
4974
|
-
import
|
|
5638
|
+
import path13 from "path";
|
|
4975
5639
|
import { consola as consola19 } from "consola";
|
|
4976
5640
|
async function runRemove(opts) {
|
|
4977
5641
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -5022,13 +5686,13 @@ async function runRemove(opts) {
|
|
|
5022
5686
|
let backupPath = null;
|
|
5023
5687
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
5024
5688
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5025
|
-
backupPath =
|
|
5689
|
+
backupPath = path13.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
5026
5690
|
await fs13.mkdir(backupPath, { recursive: true });
|
|
5027
5691
|
if (hasYml) {
|
|
5028
|
-
await fs13.copyFile(ymlPath,
|
|
5692
|
+
await fs13.copyFile(ymlPath, path13.join(backupPath, `${opts.name}.yml`));
|
|
5029
5693
|
}
|
|
5030
5694
|
if (hasContainer) {
|
|
5031
|
-
await fs13.cp(containerPath,
|
|
5695
|
+
await fs13.cp(containerPath, path13.join(backupPath, "container"), {
|
|
5032
5696
|
recursive: true
|
|
5033
5697
|
});
|
|
5034
5698
|
}
|
|
@@ -5141,7 +5805,7 @@ import { consola as consola22 } from "consola";
|
|
|
5141
5805
|
|
|
5142
5806
|
// src/restore/index.ts
|
|
5143
5807
|
import { existsSync as existsSync9, promises as fs14 } from "fs";
|
|
5144
|
-
import
|
|
5808
|
+
import path14 from "path";
|
|
5145
5809
|
import { consola as consola21 } from "consola";
|
|
5146
5810
|
async function runRestore(opts) {
|
|
5147
5811
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -5149,7 +5813,7 @@ async function runRestore(opts) {
|
|
|
5149
5813
|
info: (msg) => consola21.info(msg),
|
|
5150
5814
|
success: (msg) => consola21.success(msg)
|
|
5151
5815
|
};
|
|
5152
|
-
const backup =
|
|
5816
|
+
const backup = path14.resolve(opts.backupPath);
|
|
5153
5817
|
if (!existsSync9(backup)) {
|
|
5154
5818
|
throw new Error(`Backup not found: ${backup}.`);
|
|
5155
5819
|
}
|
|
@@ -5171,7 +5835,7 @@ async function runRestore(opts) {
|
|
|
5171
5835
|
}
|
|
5172
5836
|
const ymlFile = ymlFiles[0];
|
|
5173
5837
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
5174
|
-
const containerInBackup =
|
|
5838
|
+
const containerInBackup = path14.join(backup, "container");
|
|
5175
5839
|
const hasContainer = existsSync9(containerInBackup);
|
|
5176
5840
|
const destYml = containerConfigPath(name, home);
|
|
5177
5841
|
const destContainer = containerDir(name, home);
|
|
@@ -5186,7 +5850,7 @@ async function runRestore(opts) {
|
|
|
5186
5850
|
);
|
|
5187
5851
|
}
|
|
5188
5852
|
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
5189
|
-
await fs14.copyFile(
|
|
5853
|
+
await fs14.copyFile(path14.join(backup, ymlFile), destYml);
|
|
5190
5854
|
if (hasContainer) {
|
|
5191
5855
|
await fs14.cp(containerInBackup, destContainer, { recursive: true });
|
|
5192
5856
|
}
|
|
@@ -5447,7 +6111,7 @@ import { consola as consola28 } from "consola";
|
|
|
5447
6111
|
|
|
5448
6112
|
// src/devcontainer/shell.ts
|
|
5449
6113
|
import { existsSync as existsSync10 } from "fs";
|
|
5450
|
-
import
|
|
6114
|
+
import path15 from "path";
|
|
5451
6115
|
async function runShell(opts) {
|
|
5452
6116
|
assertContainerExists(opts.root);
|
|
5453
6117
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -5470,7 +6134,7 @@ async function runShell(opts) {
|
|
|
5470
6134
|
);
|
|
5471
6135
|
}
|
|
5472
6136
|
function assertContainerExists(root) {
|
|
5473
|
-
if (!existsSync10(
|
|
6137
|
+
if (!existsSync10(path15.join(root, ".devcontainer"))) {
|
|
5474
6138
|
throw new Error(
|
|
5475
6139
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
5476
6140
|
);
|