@getmonoceros/workbench 1.7.5 → 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 +1988 -1327
- 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);
|
|
@@ -1350,6 +1867,23 @@ function buildCodeWorkspaceJson(opts) {
|
|
|
1350
1867
|
}
|
|
1351
1868
|
return { folders };
|
|
1352
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
|
+
}
|
|
1353
1887
|
function buildPostCreateScript(opts) {
|
|
1354
1888
|
const lines = [
|
|
1355
1889
|
"#!/usr/bin/env bash",
|
|
@@ -1446,83 +1980,98 @@ function buildPostCreateScript(opts) {
|
|
|
1446
1980
|
return lines.join("\n") + "\n";
|
|
1447
1981
|
}
|
|
1448
1982
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
1449
|
-
const dest =
|
|
1450
|
-
await
|
|
1451
|
-
await
|
|
1983
|
+
const dest = path5.join(devcontainerDir, "post-create.sh");
|
|
1984
|
+
await fs6.writeFile(dest, buildPostCreateScript(opts));
|
|
1985
|
+
await fs6.chmod(dest, 493);
|
|
1452
1986
|
}
|
|
1453
1987
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
1454
1988
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
1455
|
-
const devcontainerDir =
|
|
1456
|
-
const monocerosDir =
|
|
1457
|
-
const projectsDir =
|
|
1458
|
-
const homeDir =
|
|
1459
|
-
const dataDir =
|
|
1460
|
-
await
|
|
1461
|
-
await
|
|
1462
|
-
await
|
|
1463
|
-
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 });
|
|
1464
1998
|
if (needsCompose(opts)) {
|
|
1465
|
-
await
|
|
1999
|
+
await fs6.mkdir(dataDir, { recursive: true });
|
|
1466
2000
|
for (const svcId of opts.services) {
|
|
1467
2001
|
const def = SERVICE_CATALOG[svcId];
|
|
1468
2002
|
if (def?.dataMount) {
|
|
1469
|
-
await
|
|
2003
|
+
await fs6.mkdir(path5.join(dataDir, def.id), { recursive: true });
|
|
1470
2004
|
}
|
|
1471
2005
|
}
|
|
1472
2006
|
}
|
|
1473
|
-
const containerGitignore =
|
|
1474
|
-
await
|
|
1475
|
-
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");
|
|
1476
2010
|
if (!existsSync2(gitkeep)) {
|
|
1477
|
-
await
|
|
2011
|
+
await fs6.writeFile(gitkeep, "");
|
|
1478
2012
|
}
|
|
1479
|
-
await
|
|
1480
|
-
|
|
2013
|
+
await fs6.writeFile(
|
|
2014
|
+
path5.join(monocerosDir, ".gitignore"),
|
|
1481
2015
|
"git-credentials*\ngitconfig\n"
|
|
1482
2016
|
);
|
|
1483
2017
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
1484
|
-
await
|
|
1485
|
-
|
|
2018
|
+
await fs6.writeFile(
|
|
2019
|
+
path5.join(devcontainerDir, "devcontainer.json"),
|
|
1486
2020
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
1487
2021
|
);
|
|
1488
|
-
const featuresDir =
|
|
2022
|
+
const featuresDir = path5.join(devcontainerDir, "features");
|
|
1489
2023
|
if (existsSync2(featuresDir)) {
|
|
1490
|
-
await
|
|
2024
|
+
await fs6.rm(featuresDir, { recursive: true, force: true });
|
|
1491
2025
|
}
|
|
1492
2026
|
const resolvedFeatures = resolveFeatures(opts);
|
|
1493
2027
|
for (const f of resolvedFeatures) {
|
|
1494
2028
|
if (!f.localSourceDir || !f.localName) continue;
|
|
1495
|
-
const dest =
|
|
1496
|
-
await
|
|
1497
|
-
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 });
|
|
1498
2032
|
}
|
|
1499
2033
|
for (const f of resolvedFeatures) {
|
|
1500
2034
|
for (const sub of f.persistentHomePaths) {
|
|
1501
|
-
await
|
|
2035
|
+
await fs6.mkdir(path5.join(homeDir, sub), { recursive: true });
|
|
1502
2036
|
}
|
|
1503
2037
|
for (const entry2 of f.persistentHomeFiles) {
|
|
1504
|
-
const filePath =
|
|
1505
|
-
await
|
|
2038
|
+
const filePath = path5.join(homeDir, entry2.path);
|
|
2039
|
+
await fs6.mkdir(path5.dirname(filePath), { recursive: true });
|
|
1506
2040
|
if (!existsSync2(filePath)) {
|
|
1507
|
-
await
|
|
2041
|
+
await fs6.writeFile(filePath, entry2.initialContent);
|
|
1508
2042
|
}
|
|
1509
2043
|
}
|
|
1510
2044
|
}
|
|
1511
2045
|
await writePostCreateScript(devcontainerDir, opts);
|
|
1512
|
-
const composePath =
|
|
2046
|
+
const composePath = path5.join(devcontainerDir, "compose.yaml");
|
|
1513
2047
|
if (needsCompose(opts)) {
|
|
1514
|
-
await
|
|
2048
|
+
await fs6.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
1515
2049
|
} else if (existsSync2(composePath)) {
|
|
1516
|
-
await
|
|
2050
|
+
await fs6.rm(composePath);
|
|
1517
2051
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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");
|
|
1522
2063
|
}
|
|
1523
2064
|
|
|
1524
2065
|
// src/modify/yml.ts
|
|
1525
|
-
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";
|
|
1526
2075
|
function ensureSeq(doc, key) {
|
|
1527
2076
|
const existing = doc.get(key, true);
|
|
1528
2077
|
if (existing && isSeq(existing)) return existing;
|
|
@@ -1561,12 +2110,108 @@ function addAptPackagesToDoc(doc, packages) {
|
|
|
1561
2110
|
}
|
|
1562
2111
|
return changed;
|
|
1563
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");
|
|
1564
2209
|
function portOfItem(item) {
|
|
1565
2210
|
const scalar = scalarValue(item);
|
|
1566
2211
|
if (typeof scalar === "number" && Number.isInteger(scalar)) {
|
|
1567
2212
|
return scalar;
|
|
1568
2213
|
}
|
|
1569
|
-
if (
|
|
2214
|
+
if (isMap2(item)) {
|
|
1570
2215
|
const p = item.get("port");
|
|
1571
2216
|
if (typeof p === "number" && Number.isInteger(p)) return p;
|
|
1572
2217
|
}
|
|
@@ -1574,8 +2219,8 @@ function portOfItem(item) {
|
|
|
1574
2219
|
}
|
|
1575
2220
|
function ensureRoutingMap(doc) {
|
|
1576
2221
|
const existing = doc.get("routing", true);
|
|
1577
|
-
if (existing &&
|
|
1578
|
-
const map = new
|
|
2222
|
+
if (existing && isMap2(existing)) return existing;
|
|
2223
|
+
const map = new YAMLMap2();
|
|
1579
2224
|
doc.set("routing", map);
|
|
1580
2225
|
return map;
|
|
1581
2226
|
}
|
|
@@ -1619,7 +2264,7 @@ function addPortsToDoc(doc, ports) {
|
|
|
1619
2264
|
}
|
|
1620
2265
|
function removePortsFromDoc(doc, ports) {
|
|
1621
2266
|
const routing = doc.get("routing", true);
|
|
1622
|
-
if (!routing || !
|
|
2267
|
+
if (!routing || !isMap2(routing)) return false;
|
|
1623
2268
|
const seq = routing.get("ports", true);
|
|
1624
2269
|
if (!seq || !isSeq(seq)) return false;
|
|
1625
2270
|
const targets = new Set(ports);
|
|
@@ -1646,7 +2291,7 @@ function addInstallUrlToDoc(doc, url) {
|
|
|
1646
2291
|
function addFeatureToDoc(doc, ref, options = {}) {
|
|
1647
2292
|
const seq = ensureSeq(doc, "features");
|
|
1648
2293
|
for (const item of seq.items) {
|
|
1649
|
-
if (!
|
|
2294
|
+
if (!isMap2(item)) continue;
|
|
1650
2295
|
const itemRef = item.get("ref");
|
|
1651
2296
|
if (itemRef !== ref) continue;
|
|
1652
2297
|
const itemJs = item.toJS(doc);
|
|
@@ -1658,7 +2303,7 @@ function addFeatureToDoc(doc, ref, options = {}) {
|
|
|
1658
2303
|
`Feature ${ref} is already configured with different options. Remove it first (\`monoceros remove-feature ${ref}\`) before re-adding.`
|
|
1659
2304
|
);
|
|
1660
2305
|
}
|
|
1661
|
-
const entry2 = new
|
|
2306
|
+
const entry2 = new YAMLMap2();
|
|
1662
2307
|
entry2.set("ref", ref);
|
|
1663
2308
|
if (Object.keys(options).length > 0) {
|
|
1664
2309
|
entry2.set("options", options);
|
|
@@ -1669,16 +2314,16 @@ function addFeatureToDoc(doc, ref, options = {}) {
|
|
|
1669
2314
|
function addRepoToDoc(doc, repo) {
|
|
1670
2315
|
const seq = ensureSeq(doc, "repos");
|
|
1671
2316
|
for (const item of seq.items) {
|
|
1672
|
-
if (!
|
|
2317
|
+
if (!isMap2(item)) continue;
|
|
1673
2318
|
const url = item.get("url");
|
|
1674
2319
|
if (url !== repo.url) continue;
|
|
1675
2320
|
const existingPath = item.get("path");
|
|
1676
2321
|
const effectivePath = typeof existingPath === "string" ? existingPath : deriveRepoName(url);
|
|
1677
2322
|
if (effectivePath !== repo.path) continue;
|
|
1678
2323
|
const existingGit = item.get("git", true);
|
|
1679
|
-
const existingUser = existingGit &&
|
|
1680
|
-
const existingName = existingUser &&
|
|
1681
|
-
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;
|
|
1682
2327
|
const existingGitUser = typeof existingName === "string" && typeof existingEmail === "string" ? { name: existingName, email: existingEmail } : void 0;
|
|
1683
2328
|
const sameGitUser = (existingGitUser?.name ?? null) === (repo.gitUser?.name ?? null) && (existingGitUser?.email ?? null) === (repo.gitUser?.email ?? null);
|
|
1684
2329
|
const existingProvider = item.get("provider");
|
|
@@ -1687,8 +2332,8 @@ function addRepoToDoc(doc, repo) {
|
|
|
1687
2332
|
return false;
|
|
1688
2333
|
}
|
|
1689
2334
|
if (repo.gitUser) {
|
|
1690
|
-
const gitMap = new
|
|
1691
|
-
const userMap = new
|
|
2335
|
+
const gitMap = new YAMLMap2();
|
|
2336
|
+
const userMap = new YAMLMap2();
|
|
1692
2337
|
userMap.set("name", repo.gitUser.name);
|
|
1693
2338
|
userMap.set("email", repo.gitUser.email);
|
|
1694
2339
|
gitMap.set("user", userMap);
|
|
@@ -1701,16 +2346,18 @@ function addRepoToDoc(doc, repo) {
|
|
|
1701
2346
|
} else {
|
|
1702
2347
|
item.delete("provider");
|
|
1703
2348
|
}
|
|
2349
|
+
relocateLeakedSectionComments(doc);
|
|
1704
2350
|
return true;
|
|
1705
2351
|
}
|
|
1706
|
-
const entry2 = new
|
|
2352
|
+
const entry2 = new YAMLMap2();
|
|
1707
2353
|
entry2.set("url", repo.url);
|
|
1708
|
-
|
|
2354
|
+
const persistPath = repo.path !== deriveRepoName(repo.url);
|
|
2355
|
+
if (persistPath) {
|
|
1709
2356
|
entry2.set("path", repo.path);
|
|
1710
2357
|
}
|
|
1711
2358
|
if (repo.gitUser) {
|
|
1712
|
-
const gitMap = new
|
|
1713
|
-
const userMap = new
|
|
2359
|
+
const gitMap = new YAMLMap2();
|
|
2360
|
+
const userMap = new YAMLMap2();
|
|
1714
2361
|
userMap.set("name", repo.gitUser.name);
|
|
1715
2362
|
userMap.set("email", repo.gitUser.email);
|
|
1716
2363
|
gitMap.set("user", userMap);
|
|
@@ -1719,7 +2366,20 @@ function addRepoToDoc(doc, repo) {
|
|
|
1719
2366
|
if (repo.provider) {
|
|
1720
2367
|
entry2.set("provider", repo.provider);
|
|
1721
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
|
+
}
|
|
1722
2381
|
seq.add(entry2);
|
|
2382
|
+
relocateLeakedSectionComments(doc);
|
|
1723
2383
|
return true;
|
|
1724
2384
|
}
|
|
1725
2385
|
function removeLanguageFromDoc(doc, lang) {
|
|
@@ -1744,7 +2404,7 @@ function removeInstallUrlFromDoc(doc, url) {
|
|
|
1744
2404
|
function removeFeatureFromDoc(doc, ref) {
|
|
1745
2405
|
const seq = doc.get("features", true);
|
|
1746
2406
|
if (!seq || !isSeq(seq)) return false;
|
|
1747
|
-
const idx = seq.items.findIndex((i) =>
|
|
2407
|
+
const idx = seq.items.findIndex((i) => isMap2(i) && i.get("ref") === ref);
|
|
1748
2408
|
if (idx < 0) return false;
|
|
1749
2409
|
seq.items.splice(idx, 1);
|
|
1750
2410
|
pruneEmptySeq(doc, "features");
|
|
@@ -1754,11 +2414,11 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
1754
2414
|
const seq = doc.get("repos", true);
|
|
1755
2415
|
if (!seq || !isSeq(seq)) return false;
|
|
1756
2416
|
const idx = seq.items.findIndex((item) => {
|
|
1757
|
-
if (!
|
|
2417
|
+
if (!isMap2(item)) return false;
|
|
1758
2418
|
const url = item.get("url");
|
|
1759
2419
|
if (url === urlOrPath) return true;
|
|
1760
|
-
const
|
|
1761
|
-
const effectivePath = typeof
|
|
2420
|
+
const path16 = item.get("path");
|
|
2421
|
+
const effectivePath = typeof path16 === "string" ? path16 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
1762
2422
|
return effectivePath === urlOrPath;
|
|
1763
2423
|
});
|
|
1764
2424
|
if (idx < 0) return false;
|
|
@@ -1808,7 +2468,7 @@ async function runAddRepo(input) {
|
|
|
1808
2468
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
1809
2469
|
);
|
|
1810
2470
|
}
|
|
1811
|
-
const
|
|
2471
|
+
const path16 = (input.path ?? deriveRepoName(url)).trim();
|
|
1812
2472
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
1813
2473
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
1814
2474
|
if (hasName !== hasEmail) {
|
|
@@ -1837,7 +2497,7 @@ async function runAddRepo(input) {
|
|
|
1837
2497
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
1838
2498
|
const entry2 = {
|
|
1839
2499
|
url,
|
|
1840
|
-
path:
|
|
2500
|
+
path: path16,
|
|
1841
2501
|
...hasName && hasEmail ? {
|
|
1842
2502
|
gitUser: {
|
|
1843
2503
|
name: input.gitName.trim(),
|
|
@@ -1846,7 +2506,117 @@ async function runAddRepo(input) {
|
|
|
1846
2506
|
} : {},
|
|
1847
2507
|
...providerToWrite ? { provider: providerToWrite } : {}
|
|
1848
2508
|
};
|
|
1849
|
-
|
|
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, `'\\''`)}'`;
|
|
1850
2620
|
}
|
|
1851
2621
|
function normalizeProvider(raw) {
|
|
1852
2622
|
if (typeof raw !== "string") return void 0;
|
|
@@ -1982,7 +2752,7 @@ async function mutate(opts, apply) {
|
|
|
1982
2752
|
const logger = opts.logger ?? defaultLogger();
|
|
1983
2753
|
let oldText;
|
|
1984
2754
|
try {
|
|
1985
|
-
oldText = await
|
|
2755
|
+
oldText = await fs7.readFile(ymlPath, "utf8");
|
|
1986
2756
|
} catch {
|
|
1987
2757
|
throw new Error(
|
|
1988
2758
|
`No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
|
|
@@ -2006,7 +2776,7 @@ async function mutate(opts, apply) {
|
|
|
2006
2776
|
return { status: "aborted" };
|
|
2007
2777
|
}
|
|
2008
2778
|
}
|
|
2009
|
-
await
|
|
2779
|
+
await fs7.writeFile(ymlPath, newText, "utf8");
|
|
2010
2780
|
logger.success(`Updated ${ymlPath}.`);
|
|
2011
2781
|
logger.info(
|
|
2012
2782
|
`Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
|
|
@@ -2264,179 +3034,21 @@ function printSecurityWarning(url) {
|
|
|
2264
3034
|
w(
|
|
2265
3035
|
" 2. Verify the maintainer is who you think they are (HTTPS cert, repo)."
|
|
2266
3036
|
);
|
|
2267
|
-
w(" 3. Ideally, vendor the install steps as `add-apt-packages` or");
|
|
2268
|
-
w(
|
|
2269
|
-
" `add-feature` instead \u2014 those reference signed/versioned artifacts."
|
|
2270
|
-
);
|
|
2271
|
-
w("");
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
// src/commands/add-repo.ts
|
|
2275
|
-
import { defineCommand as defineCommand4 } from "citty";
|
|
2276
|
-
import { consola as consola5 } from "consola";
|
|
2277
|
-
var addRepoCommand = defineCommand4({
|
|
2278
|
-
meta: {
|
|
2279
|
-
name: "add-repo",
|
|
2280
|
-
group: "edit",
|
|
2281
|
-
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."
|
|
2282
|
-
},
|
|
2283
|
-
args: {
|
|
2284
|
-
name: {
|
|
2285
|
-
type: "positional",
|
|
2286
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2287
|
-
required: true
|
|
2288
|
-
},
|
|
2289
|
-
url: {
|
|
2290
|
-
type: "positional",
|
|
2291
|
-
description: "Git URL (HTTPS or SSH/git@ form). E.g. https://github.com/foo/bar.git, git@github.com:foo/bar.git.",
|
|
2292
|
-
required: true
|
|
2293
|
-
},
|
|
2294
|
-
path: {
|
|
2295
|
-
type: "string",
|
|
2296
|
-
description: "Destination under projects/. Subfolders via `/` (e.g. apps/web). Default: URL-derived single segment (bar.git \u2192 bar)."
|
|
2297
|
-
},
|
|
2298
|
-
"git-name": {
|
|
2299
|
-
type: "string",
|
|
2300
|
-
description: "Per-repo git committer name. Overrides the container-level git.user.name for this repo only. Pair with --git-email."
|
|
2301
|
-
},
|
|
2302
|
-
"git-email": {
|
|
2303
|
-
type: "string",
|
|
2304
|
-
description: "Per-repo git committer email. Overrides the container-level git.user.email for this repo only. Pair with --git-name."
|
|
2305
|
-
},
|
|
2306
|
-
provider: {
|
|
2307
|
-
type: "string",
|
|
2308
|
-
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."
|
|
2309
|
-
},
|
|
2310
|
-
yes: {
|
|
2311
|
-
type: "boolean",
|
|
2312
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2313
|
-
alias: ["y"],
|
|
2314
|
-
default: false
|
|
2315
|
-
}
|
|
2316
|
-
},
|
|
2317
|
-
async run({ args }) {
|
|
2318
|
-
try {
|
|
2319
|
-
const result = await runAddRepo({
|
|
2320
|
-
name: args.name,
|
|
2321
|
-
url: args.url,
|
|
2322
|
-
...typeof args.path === "string" ? { path: args.path } : {},
|
|
2323
|
-
...typeof args["git-name"] === "string" ? { gitName: args["git-name"] } : {},
|
|
2324
|
-
...typeof args["git-email"] === "string" ? { gitEmail: args["git-email"] } : {},
|
|
2325
|
-
...typeof args.provider === "string" ? { provider: args.provider } : {},
|
|
2326
|
-
yes: args.yes
|
|
2327
|
-
});
|
|
2328
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2329
|
-
} catch (err) {
|
|
2330
|
-
consola5.error(err instanceof Error ? err.message : String(err));
|
|
2331
|
-
process.exit(1);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
});
|
|
2335
|
-
|
|
2336
|
-
// src/commands/add-language.ts
|
|
2337
|
-
import { defineCommand as defineCommand5 } from "citty";
|
|
2338
|
-
import { consola as consola6 } from "consola";
|
|
2339
|
-
var addLanguageCommand = defineCommand5({
|
|
2340
|
-
meta: {
|
|
2341
|
-
name: "add-language",
|
|
2342
|
-
group: "edit",
|
|
2343
|
-
description: "Add a language toolchain (devcontainer feature) to the container config. Idempotent, prints a diff before writing."
|
|
2344
|
-
},
|
|
2345
|
-
args: {
|
|
2346
|
-
name: {
|
|
2347
|
-
type: "positional",
|
|
2348
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2349
|
-
required: true
|
|
2350
|
-
},
|
|
2351
|
-
language: {
|
|
2352
|
-
type: "positional",
|
|
2353
|
-
description: "Language identifier from the feature whitelist (e.g. python, java, rust).",
|
|
2354
|
-
required: true
|
|
2355
|
-
},
|
|
2356
|
-
yes: {
|
|
2357
|
-
type: "boolean",
|
|
2358
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2359
|
-
alias: ["y"],
|
|
2360
|
-
default: false
|
|
2361
|
-
}
|
|
2362
|
-
},
|
|
2363
|
-
async run({ args }) {
|
|
2364
|
-
try {
|
|
2365
|
-
const result = await runAddLanguage({
|
|
2366
|
-
name: args.name,
|
|
2367
|
-
language: args.language,
|
|
2368
|
-
yes: args.yes
|
|
2369
|
-
});
|
|
2370
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2371
|
-
} catch (err) {
|
|
2372
|
-
consola6.error(err instanceof Error ? err.message : String(err));
|
|
2373
|
-
process.exit(1);
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
});
|
|
2377
|
-
|
|
2378
|
-
// src/commands/add-port.ts
|
|
2379
|
-
import { defineCommand as defineCommand6 } from "citty";
|
|
2380
|
-
import { consola as consola7 } from "consola";
|
|
2381
|
-
var addPortCommand = defineCommand6({
|
|
2382
|
-
meta: {
|
|
2383
|
-
name: "add-port",
|
|
2384
|
-
group: "edit",
|
|
2385
|
-
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)."
|
|
2386
|
-
},
|
|
2387
|
-
args: {
|
|
2388
|
-
name: {
|
|
2389
|
-
type: "positional",
|
|
2390
|
-
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2391
|
-
required: true
|
|
2392
|
-
},
|
|
2393
|
-
yes: {
|
|
2394
|
-
type: "boolean",
|
|
2395
|
-
description: "Skip the interactive confirmation and apply the diff.",
|
|
2396
|
-
alias: ["y"],
|
|
2397
|
-
default: false
|
|
2398
|
-
},
|
|
2399
|
-
default: {
|
|
2400
|
-
type: "boolean",
|
|
2401
|
-
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.",
|
|
2402
|
-
default: false
|
|
2403
|
-
}
|
|
2404
|
-
},
|
|
2405
|
-
async run({ args }) {
|
|
2406
|
-
const tokens = [...getInnerArgs()];
|
|
2407
|
-
if (tokens.length === 0) {
|
|
2408
|
-
consola7.error(
|
|
2409
|
-
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
2410
|
-
);
|
|
2411
|
-
process.exit(1);
|
|
2412
|
-
}
|
|
2413
|
-
try {
|
|
2414
|
-
const result = await runAddPort({
|
|
2415
|
-
name: args.name,
|
|
2416
|
-
ports: tokens.map(coerceToken),
|
|
2417
|
-
yes: args.yes,
|
|
2418
|
-
asDefault: args.default
|
|
2419
|
-
});
|
|
2420
|
-
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2421
|
-
} catch (err) {
|
|
2422
|
-
consola7.error(err instanceof Error ? err.message : String(err));
|
|
2423
|
-
process.exit(1);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
});
|
|
2427
|
-
function coerceToken(raw) {
|
|
2428
|
-
const n = Number(raw);
|
|
2429
|
-
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("");
|
|
2430
3042
|
}
|
|
2431
3043
|
|
|
2432
|
-
// src/commands/add-
|
|
2433
|
-
import { defineCommand as
|
|
2434
|
-
import { consola as
|
|
2435
|
-
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({
|
|
2436
3048
|
meta: {
|
|
2437
|
-
name: "add-
|
|
3049
|
+
name: "add-repo",
|
|
2438
3050
|
group: "edit",
|
|
2439
|
-
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."
|
|
2440
3052
|
},
|
|
2441
3053
|
args: {
|
|
2442
3054
|
name: {
|
|
@@ -2444,11 +3056,27 @@ var addServiceCommand = defineCommand7({
|
|
|
2444
3056
|
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2445
3057
|
required: true
|
|
2446
3058
|
},
|
|
2447
|
-
|
|
3059
|
+
url: {
|
|
2448
3060
|
type: "positional",
|
|
2449
|
-
description: "
|
|
3061
|
+
description: "Git URL (HTTPS or SSH/git@ form). E.g. https://github.com/foo/bar.git, git@github.com:foo/bar.git.",
|
|
2450
3062
|
required: true
|
|
2451
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
|
+
},
|
|
2452
3080
|
yes: {
|
|
2453
3081
|
type: "boolean",
|
|
2454
3082
|
description: "Skip the interactive confirmation and apply the diff.",
|
|
@@ -2458,680 +3086,513 @@ var addServiceCommand = defineCommand7({
|
|
|
2458
3086
|
},
|
|
2459
3087
|
async run({ args }) {
|
|
2460
3088
|
try {
|
|
2461
|
-
const result = await
|
|
3089
|
+
const result = await runAddRepo({
|
|
2462
3090
|
name: args.name,
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
process.exit(1);
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
});
|
|
2473
|
-
|
|
2474
|
-
// src/commands/apply.ts
|
|
2475
|
-
import { defineCommand as defineCommand8 } from "citty";
|
|
2476
|
-
|
|
2477
|
-
// src/apply/index.ts
|
|
2478
|
-
import { existsSync as existsSync4, promises as fs10 } from "fs";
|
|
2479
|
-
import { consola as consola11 } from "consola";
|
|
2480
|
-
|
|
2481
|
-
// src/config/state.ts
|
|
2482
|
-
import { promises as fs7 } from "fs";
|
|
2483
|
-
import path5 from "path";
|
|
2484
|
-
function buildStateFile(opts) {
|
|
2485
|
-
return {
|
|
2486
|
-
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
2487
|
-
origin: opts.origin,
|
|
2488
|
-
monocerosCliVersion: opts.cliVersion,
|
|
2489
|
-
materializedAt: (opts.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
2490
|
-
};
|
|
2491
|
-
}
|
|
2492
|
-
function stateFilePath(targetDir) {
|
|
2493
|
-
return path5.join(targetDir, ".monoceros", "state.json");
|
|
2494
|
-
}
|
|
2495
|
-
async function readStateFile(targetDir) {
|
|
2496
|
-
try {
|
|
2497
|
-
const content = await fs7.readFile(stateFilePath(targetDir), "utf8");
|
|
2498
|
-
return JSON.parse(content);
|
|
2499
|
-
} catch {
|
|
2500
|
-
return void 0;
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
async function writeStateFile(targetDir, state) {
|
|
2504
|
-
const monocerosDir = path5.join(targetDir, ".monoceros");
|
|
2505
|
-
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2506
|
-
await fs7.writeFile(
|
|
2507
|
-
stateFilePath(targetDir),
|
|
2508
|
-
JSON.stringify(state, null, 2) + "\n"
|
|
2509
|
-
);
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
// src/config/transform.ts
|
|
2513
|
-
function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
2514
|
-
const featureRecord = {};
|
|
2515
|
-
for (const entry2 of config.features) {
|
|
2516
|
-
const defaults = featureDefaults[entry2.ref] ?? {};
|
|
2517
|
-
featureRecord[entry2.ref] = { ...defaults, ...entry2.options ?? {} };
|
|
2518
|
-
}
|
|
2519
|
-
const result = {
|
|
2520
|
-
name: config.name,
|
|
2521
|
-
languages: [...config.languages],
|
|
2522
|
-
services: [...config.services]
|
|
2523
|
-
};
|
|
2524
|
-
if (config.externalServices.postgres !== void 0) {
|
|
2525
|
-
result.postgresUrl = config.externalServices.postgres;
|
|
2526
|
-
}
|
|
2527
|
-
if (config.aptPackages.length > 0) {
|
|
2528
|
-
result.aptPackages = [...config.aptPackages];
|
|
2529
|
-
}
|
|
2530
|
-
if (Object.keys(featureRecord).length > 0) {
|
|
2531
|
-
result.features = featureRecord;
|
|
2532
|
-
}
|
|
2533
|
-
if (config.installUrls.length > 0) {
|
|
2534
|
-
result.installUrls = [...config.installUrls];
|
|
2535
|
-
}
|
|
2536
|
-
if (config.repos.length > 0) {
|
|
2537
|
-
result.repos = config.repos.map((r) => ({
|
|
2538
|
-
url: r.url,
|
|
2539
|
-
// `path` is optional in the yml; CreateOptions requires it.
|
|
2540
|
-
// When the yml omits `path`, fall back to the URL-derived
|
|
2541
|
-
// single-segment default (`https://.../foo.git` → `foo`),
|
|
2542
|
-
// which lands the clone at `projects/foo/`.
|
|
2543
|
-
path: r.path ?? deriveRepoName(r.url),
|
|
2544
|
-
...r.git?.user ? { gitUser: { name: r.git.user.name, email: r.git.user.email } } : {},
|
|
2545
|
-
...r.provider ? { provider: r.provider } : {}
|
|
2546
|
-
}));
|
|
2547
|
-
}
|
|
2548
|
-
const routingPorts = config.routing?.ports ?? [];
|
|
2549
|
-
if (routingPorts.length > 0) {
|
|
2550
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2551
|
-
const ports = [];
|
|
2552
|
-
for (const entry2 of routingPorts) {
|
|
2553
|
-
const n = portNumber(entry2);
|
|
2554
|
-
if (seen.has(n)) continue;
|
|
2555
|
-
seen.add(n);
|
|
2556
|
-
ports.push(n);
|
|
2557
|
-
}
|
|
2558
|
-
result.ports = ports;
|
|
2559
|
-
}
|
|
2560
|
-
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
2561
|
-
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
2562
|
-
}
|
|
2563
|
-
return result;
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
// src/util/format.ts
|
|
2567
|
-
var ESC = "\x1B[";
|
|
2568
|
-
var ANSI_BOLD2 = `${ESC}1m`;
|
|
2569
|
-
var ANSI_UNDERLINE2 = `${ESC}4m`;
|
|
2570
|
-
var ANSI_CYAN2 = `${ESC}36m`;
|
|
2571
|
-
var ANSI_GREY2 = `${ESC}90m`;
|
|
2572
|
-
var ANSI_RESET2 = `${ESC}0m`;
|
|
2573
|
-
function makeWrap(isTty2) {
|
|
2574
|
-
return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET2 : s;
|
|
2575
|
-
}
|
|
2576
|
-
function makePalette(isTty2) {
|
|
2577
|
-
const wrap = makeWrap(isTty2);
|
|
2578
|
-
return {
|
|
2579
|
-
bold: (s) => wrap(s, ANSI_BOLD2),
|
|
2580
|
-
underline: (s) => wrap(s, ANSI_UNDERLINE2),
|
|
2581
|
-
cyan: (s) => wrap(s, ANSI_CYAN2),
|
|
2582
|
-
dim: (s) => wrap(s, ANSI_GREY2),
|
|
2583
|
-
sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
|
|
2584
|
-
};
|
|
2585
|
-
}
|
|
2586
|
-
function colorsFor(stream) {
|
|
2587
|
-
return makePalette(stream.isTTY ?? false);
|
|
2588
|
-
}
|
|
2589
|
-
var stderrPalette = makePalette(process.stderr.isTTY ?? false);
|
|
2590
|
-
var bold2 = stderrPalette.bold;
|
|
2591
|
-
var underline2 = stderrPalette.underline;
|
|
2592
|
-
var cyan2 = stderrPalette.cyan;
|
|
2593
|
-
var dim = stderrPalette.dim;
|
|
2594
|
-
var sectionLine = stderrPalette.sectionLine;
|
|
2595
|
-
|
|
2596
|
-
// src/devcontainer/compose.ts
|
|
2597
|
-
import { spawn as spawn3 } from "child_process";
|
|
2598
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2599
|
-
import path7 from "path";
|
|
2600
|
-
import { consola as consola9 } from "consola";
|
|
2601
|
-
|
|
2602
|
-
// src/util/mask-secrets.ts
|
|
2603
|
-
import { Transform } from "stream";
|
|
2604
|
-
var PATTERNS = [
|
|
2605
|
-
// Atlassian Cloud API token. Starts with literal `ATATT3xFf` plus
|
|
2606
|
-
// a long URL-safe-base64 tail. Tightened to that prefix to avoid
|
|
2607
|
-
// matching unrelated all-caps words.
|
|
2608
|
-
{ name: "atlassian-api", re: /ATATT3xFf[A-Za-z0-9+/=_-]{20,}/g },
|
|
2609
|
-
// Bitbucket Cloud app password.
|
|
2610
|
-
{ name: "bitbucket-app", re: /ATBB[A-Za-z0-9+/=_-]{20,}/g },
|
|
2611
|
-
// GitHub PAT (classic), OAuth, user, server, refresh — all share
|
|
2612
|
-
// the `gh<lower-letter>_<base62>` shape per GitHub's token format.
|
|
2613
|
-
{ name: "github-token", re: /gh[a-z]_[A-Za-z0-9]{20,}/g },
|
|
2614
|
-
// GitHub fine-grained PAT.
|
|
2615
|
-
{ name: "github-pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
2616
|
-
// Anthropic API key.
|
|
2617
|
-
{ name: "anthropic-api", re: /sk-ant-[A-Za-z0-9_-]{20,}/g }
|
|
2618
|
-
];
|
|
2619
|
-
function maskSecrets(text) {
|
|
2620
|
-
let result = text;
|
|
2621
|
-
for (const { re } of PATTERNS) {
|
|
2622
|
-
result = result.replace(re, maskOne);
|
|
2623
|
-
}
|
|
2624
|
-
return result;
|
|
2625
|
-
}
|
|
2626
|
-
function maskOne(token) {
|
|
2627
|
-
if (token.length <= 12) return token;
|
|
2628
|
-
return `${token.slice(0, 5)}\u2026${token.slice(-6)}`;
|
|
2629
|
-
}
|
|
2630
|
-
function createSecretMaskStream() {
|
|
2631
|
-
let buffer = "";
|
|
2632
|
-
return new Transform({
|
|
2633
|
-
decodeStrings: true,
|
|
2634
|
-
transform(chunk, _enc, cb) {
|
|
2635
|
-
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2636
|
-
buffer += text;
|
|
2637
|
-
const lastNewline = buffer.lastIndexOf("\n");
|
|
2638
|
-
if (lastNewline === -1) {
|
|
2639
|
-
cb(null);
|
|
2640
|
-
return;
|
|
2641
|
-
}
|
|
2642
|
-
const flushable = buffer.slice(0, lastNewline + 1);
|
|
2643
|
-
buffer = buffer.slice(lastNewline + 1);
|
|
2644
|
-
cb(null, maskSecrets(flushable));
|
|
2645
|
-
},
|
|
2646
|
-
flush(cb) {
|
|
2647
|
-
if (buffer.length > 0) {
|
|
2648
|
-
const tail = maskSecrets(buffer);
|
|
2649
|
-
buffer = "";
|
|
2650
|
-
cb(null, tail);
|
|
2651
|
-
return;
|
|
2652
|
-
}
|
|
2653
|
-
cb(null);
|
|
2654
|
-
}
|
|
2655
|
-
});
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
// src/devcontainer/cli.ts
|
|
2659
|
-
import { spawn as spawn2 } from "child_process";
|
|
2660
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
2661
|
-
import { createRequire } from "module";
|
|
2662
|
-
import path6 from "path";
|
|
2663
|
-
var require_ = createRequire(import.meta.url);
|
|
2664
|
-
var cachedBinaryPath = null;
|
|
2665
|
-
function devcontainerCliPath() {
|
|
2666
|
-
if (cachedBinaryPath) return cachedBinaryPath;
|
|
2667
|
-
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
2668
|
-
const pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
|
|
2669
|
-
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
2670
|
-
if (!binEntry) {
|
|
2671
|
-
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
2672
|
-
}
|
|
2673
|
-
cachedBinaryPath = path6.resolve(path6.dirname(pkgJsonPath), binEntry);
|
|
2674
|
-
return cachedBinaryPath;
|
|
2675
|
-
}
|
|
2676
|
-
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
2677
|
-
const binPath = devcontainerCliPath();
|
|
2678
|
-
return new Promise((resolve, reject) => {
|
|
2679
|
-
if (options.interactive) {
|
|
2680
|
-
const child2 = spawn2(process.execPath, [binPath, ...args], {
|
|
2681
|
-
cwd,
|
|
2682
|
-
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
|
|
2683
3097
|
});
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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);
|
|
2687
3102
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
|
2709
3139
|
});
|
|
2710
|
-
|
|
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);
|
|
2711
3144
|
}
|
|
2712
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2713
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2714
|
-
child.on("error", reject);
|
|
2715
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2716
|
-
});
|
|
2717
|
-
};
|
|
2718
|
-
|
|
2719
|
-
// src/devcontainer/compose.ts
|
|
2720
|
-
var spawnDockerCompose = (args, cwd) => {
|
|
2721
|
-
return new Promise((resolve, reject) => {
|
|
2722
|
-
const child = spawn3("docker", ["compose", ...args], {
|
|
2723
|
-
cwd,
|
|
2724
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
2725
|
-
});
|
|
2726
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2727
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2728
|
-
child.on("error", reject);
|
|
2729
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2730
|
-
});
|
|
2731
|
-
};
|
|
2732
|
-
var spawnBash = (args, cwd) => {
|
|
2733
|
-
return new Promise((resolve, reject) => {
|
|
2734
|
-
const child = spawn3("bash", args, {
|
|
2735
|
-
cwd,
|
|
2736
|
-
stdio: ["inherit", "pipe", "pipe"]
|
|
2737
|
-
});
|
|
2738
|
-
child.stdout?.pipe(createSecretMaskStream()).pipe(process.stdout);
|
|
2739
|
-
child.stderr?.pipe(createSecretMaskStream()).pipe(process.stderr);
|
|
2740
|
-
child.on("error", reject);
|
|
2741
|
-
child.on("exit", (code) => resolve(code ?? 0));
|
|
2742
|
-
});
|
|
2743
|
-
};
|
|
2744
|
-
function composeProjectName(root) {
|
|
2745
|
-
return `${path7.basename(root)}_devcontainer`;
|
|
2746
|
-
}
|
|
2747
|
-
function resolveCompose(root) {
|
|
2748
|
-
if (!existsSync3(path7.join(root, ".devcontainer"))) {
|
|
2749
|
-
throw new Error(
|
|
2750
|
-
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
2751
|
-
);
|
|
2752
3145
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
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
|
+
}
|
|
2758
3195
|
}
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
const spawnFn = opts.spawn ?? spawnDockerCompose;
|
|
2764
|
-
const subArgs = buildSubArgs(opts.service);
|
|
2765
|
-
return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
|
|
2766
|
-
}
|
|
2767
|
-
async function runStart(opts) {
|
|
2768
|
-
resolveCompose(opts.root);
|
|
2769
|
-
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
2770
|
-
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
2771
|
-
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
2772
|
-
return spawnFn(
|
|
2773
|
-
["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
|
|
2774
|
-
opts.root
|
|
2775
|
-
);
|
|
3196
|
+
});
|
|
3197
|
+
function coerceToken(raw) {
|
|
3198
|
+
const n = Number(raw);
|
|
3199
|
+
return Number.isFinite(n) ? n : raw;
|
|
2776
3200
|
}
|
|
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
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
+
};
|
|
2818
3261
|
}
|
|
2819
|
-
function
|
|
2820
|
-
return
|
|
2821
|
-
(service) => ["stop", ...service ? [service] : []],
|
|
2822
|
-
opts
|
|
2823
|
-
);
|
|
3262
|
+
function stateFilePath(targetDir) {
|
|
3263
|
+
return path7.join(targetDir, ".monoceros", "state.json");
|
|
2824
3264
|
}
|
|
2825
|
-
function
|
|
2826
|
-
|
|
2827
|
-
(
|
|
2828
|
-
|
|
2829
|
-
|
|
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
|
+
}
|
|
2830
3272
|
}
|
|
2831
|
-
function
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
...service ? [service] : []
|
|
2838
|
-
],
|
|
2839
|
-
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"
|
|
2840
3279
|
);
|
|
2841
3280
|
}
|
|
2842
3281
|
|
|
2843
|
-
// src/
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
...process.env,
|
|
2853
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
2854
|
-
GIT_ASKPASS: "",
|
|
2855
|
-
SSH_ASKPASS: ""
|
|
2856
|
-
}
|
|
2857
|
-
});
|
|
2858
|
-
let stdout = "";
|
|
2859
|
-
child.stdout.on("data", (chunk) => {
|
|
2860
|
-
stdout += chunk.toString();
|
|
2861
|
-
});
|
|
2862
|
-
child.on("error", reject);
|
|
2863
|
-
child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
|
|
2864
|
-
child.stdin.write(input);
|
|
2865
|
-
child.stdin.end();
|
|
2866
|
-
});
|
|
2867
|
-
};
|
|
2868
|
-
function resolveProvider(host, explicit) {
|
|
2869
|
-
const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
|
|
2870
|
-
if (canonical) return canonical;
|
|
2871
|
-
return explicit ?? "unknown";
|
|
2872
|
-
}
|
|
2873
|
-
function uniqueHttpsHosts(repos) {
|
|
2874
|
-
const byHost = /* @__PURE__ */ new Map();
|
|
2875
|
-
for (const repo of repos) {
|
|
2876
|
-
if (!repo.url.startsWith("https://")) continue;
|
|
2877
|
-
let host;
|
|
2878
|
-
try {
|
|
2879
|
-
host = new URL(repo.url).hostname;
|
|
2880
|
-
} catch {
|
|
2881
|
-
continue;
|
|
2882
|
-
}
|
|
2883
|
-
if (byHost.has(host)) continue;
|
|
2884
|
-
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 };
|
|
2885
3291
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
return cyan2(opts.winget);
|
|
2894
|
-
default:
|
|
2895
|
-
if (opts.linuxBrew) return cyan2(opts.linuxBrew);
|
|
2896
|
-
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;
|
|
2897
3299
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
if (provider === "github") {
|
|
2901
|
-
const isSaas = host.toLowerCase() === "github.com";
|
|
2902
|
-
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2903
|
-
const install = installCommandForOS({
|
|
2904
|
-
brew: "brew install gh",
|
|
2905
|
-
winget: "winget install --id GitHub.cli",
|
|
2906
|
-
linuxBrew: "brew install gh",
|
|
2907
|
-
linuxDocsUrl: "https://github.com/cli/cli#installation"
|
|
2908
|
-
});
|
|
2909
|
-
return {
|
|
2910
|
-
title: `${host} \u2014 GitHub`,
|
|
2911
|
-
body: [
|
|
2912
|
-
"Install the GitHub CLI:",
|
|
2913
|
-
install,
|
|
2914
|
-
"",
|
|
2915
|
-
"Then run once:",
|
|
2916
|
-
cyan2(`gh auth login${hostArg}`),
|
|
2917
|
-
cyan2(`gh auth setup-git${hostArg}`),
|
|
2918
|
-
"",
|
|
2919
|
-
"`gh auth login` walks through OAuth in your browser.",
|
|
2920
|
-
"`gh auth setup-git` wires gh into git as a credential helper."
|
|
2921
|
-
].join("\n")
|
|
2922
|
-
};
|
|
3300
|
+
if (config.aptPackages.length > 0) {
|
|
3301
|
+
result.aptPackages = [...config.aptPackages];
|
|
2923
3302
|
}
|
|
2924
|
-
if (
|
|
2925
|
-
|
|
2926
|
-
const hostArg = isSaas ? "" : ` --hostname ${host}`;
|
|
2927
|
-
const install = installCommandForOS({
|
|
2928
|
-
brew: "brew install glab",
|
|
2929
|
-
winget: "winget install --id GLab.GLab",
|
|
2930
|
-
linuxBrew: "brew install glab",
|
|
2931
|
-
linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
|
|
2932
|
-
});
|
|
2933
|
-
return {
|
|
2934
|
-
title: `${host} \u2014 GitLab`,
|
|
2935
|
-
body: [
|
|
2936
|
-
"Install the GitLab CLI (glab):",
|
|
2937
|
-
install,
|
|
2938
|
-
"",
|
|
2939
|
-
"Then run once:",
|
|
2940
|
-
cyan2(`glab auth login${hostArg}`),
|
|
2941
|
-
"",
|
|
2942
|
-
"Choose `HTTPS` when asked for git-protocol, then accept",
|
|
2943
|
-
'"Authenticate Git with your GitLab credentials" \u2014 glab',
|
|
2944
|
-
"configures itself as the git credential helper."
|
|
2945
|
-
].join("\n")
|
|
2946
|
-
};
|
|
3303
|
+
if (Object.keys(featureRecord).length > 0) {
|
|
3304
|
+
result.features = featureRecord;
|
|
2947
3305
|
}
|
|
2948
|
-
if (
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
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);
|
|
2964
3335
|
}
|
|
2965
|
-
|
|
2966
|
-
title: `${host} \u2014 Bitbucket Data Center`,
|
|
2967
|
-
body: [
|
|
2968
|
-
"Bitbucket has no first-party CLI for git-credentials, so this",
|
|
2969
|
-
"is a manual one-time setup. Generate a personal HTTP access",
|
|
2970
|
-
`token in your Bitbucket UI: profile picture (top right on ${host})`,
|
|
2971
|
-
"\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
|
|
2972
|
-
"at least repo-read + repo-write scopes for the repos you need.",
|
|
2973
|
-
"",
|
|
2974
|
-
"Then store it via your OS credential helper:",
|
|
2975
|
-
cyan2(
|
|
2976
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
|
|
2977
|
-
)
|
|
2978
|
-
].join("\n")
|
|
2979
|
-
};
|
|
3336
|
+
result.ports = ports;
|
|
2980
3337
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
"`tea` CLI logs into its own config, not into your git credential",
|
|
2986
|
-
"helper), so this is a manual one-time setup. Generate an access",
|
|
2987
|
-
`token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
|
|
2988
|
-
'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
|
|
2989
|
-
"least the `read:repository` scope (add `write:repository` if you",
|
|
2990
|
-
"need push from the container).",
|
|
2991
|
-
"",
|
|
2992
|
-
"Then store it via your OS credential helper:",
|
|
2993
|
-
cyan2(
|
|
2994
|
-
`git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
|
|
2995
|
-
)
|
|
2996
|
-
].join("\n")
|
|
2997
|
-
};
|
|
3338
|
+
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
3339
|
+
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
3340
|
+
}
|
|
3341
|
+
return result;
|
|
2998
3342
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
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);
|
|
3008
3371
|
}
|
|
3009
3372
|
return result;
|
|
3010
3373
|
}
|
|
3011
|
-
function
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
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)}`;
|
|
3015
3377
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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);
|
|
3035
3402
|
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
host=${host}
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3039
3405
|
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
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"
|
|
3055
3431
|
});
|
|
3056
|
-
|
|
3432
|
+
child2.on("error", reject);
|
|
3433
|
+
child2.on("exit", (code) => resolve(code ?? 0));
|
|
3434
|
+
return;
|
|
3057
3435
|
}
|
|
3058
|
-
const
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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);
|
|
3065
3457
|
});
|
|
3066
|
-
|
|
3458
|
+
return;
|
|
3067
3459
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
+
);
|
|
3070
3500
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
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
|
|
3078
3565
|
);
|
|
3079
|
-
return {
|
|
3080
|
-
hostsWritten: lines.length,
|
|
3081
|
-
hostsSkipped: perHost.filter((p) => p.status !== "ok").length,
|
|
3082
|
-
perHost,
|
|
3083
|
-
credentialsPath
|
|
3084
|
-
};
|
|
3085
3566
|
}
|
|
3086
|
-
function
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
`Missing Git credentials: ${hint.title}`,
|
|
3092
|
-
"",
|
|
3093
|
-
hint.body,
|
|
3094
|
-
"",
|
|
3095
|
-
`Then re-run ${cyan2("monoceros apply")}.`
|
|
3096
|
-
].join("\n");
|
|
3097
|
-
}
|
|
3098
|
-
const lines = [
|
|
3099
|
-
`Missing Git credentials for ${missing.length} hosts:`,
|
|
3100
|
-
""
|
|
3101
|
-
];
|
|
3102
|
-
for (const m of missing) {
|
|
3103
|
-
const hint = providerSetupHint(m.host, m.provider);
|
|
3104
|
-
lines.push(hint.title);
|
|
3105
|
-
lines.push("");
|
|
3106
|
-
lines.push(hint.body);
|
|
3107
|
-
lines.push("");
|
|
3108
|
-
}
|
|
3109
|
-
lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
|
|
3110
|
-
return lines.join("\n");
|
|
3567
|
+
function runStop(opts) {
|
|
3568
|
+
return runComposeAction(
|
|
3569
|
+
(service) => ["stop", ...service ? [service] : []],
|
|
3570
|
+
opts
|
|
3571
|
+
);
|
|
3111
3572
|
}
|
|
3112
|
-
function
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
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
|
+
);
|
|
3128
3589
|
}
|
|
3129
3590
|
|
|
3130
3591
|
// src/devcontainer/repo-reachability.ts
|
|
3131
|
-
import { spawn as
|
|
3592
|
+
import { spawn as spawn6 } from "child_process";
|
|
3132
3593
|
var realGitLsRemote = (url) => {
|
|
3133
3594
|
return new Promise((resolve, reject) => {
|
|
3134
|
-
const child =
|
|
3595
|
+
const child = spawn6("git", ["ls-remote", "--heads", "--", url], {
|
|
3135
3596
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3136
3597
|
env: {
|
|
3137
3598
|
...process.env,
|
|
@@ -3269,10 +3730,10 @@ function adviceForKind(kind) {
|
|
|
3269
3730
|
}
|
|
3270
3731
|
|
|
3271
3732
|
// src/devcontainer/docker-mode.ts
|
|
3272
|
-
import { spawn as
|
|
3733
|
+
import { spawn as spawn7 } from "child_process";
|
|
3273
3734
|
var realDockerInfo = () => {
|
|
3274
3735
|
return new Promise((resolve, reject) => {
|
|
3275
|
-
const child =
|
|
3736
|
+
const child = spawn7(
|
|
3276
3737
|
"docker",
|
|
3277
3738
|
["info", "--format", "{{json .SecurityOptions}}"],
|
|
3278
3739
|
{
|
|
@@ -3331,13 +3792,13 @@ function formatRootlessNotSupportedError() {
|
|
|
3331
3792
|
}
|
|
3332
3793
|
|
|
3333
3794
|
// src/devcontainer/identity.ts
|
|
3334
|
-
import { spawn as
|
|
3795
|
+
import { spawn as spawn8 } from "child_process";
|
|
3335
3796
|
import { promises as fs9 } from "fs";
|
|
3336
|
-
import
|
|
3797
|
+
import path10 from "path";
|
|
3337
3798
|
import { consola as consola10 } from "consola";
|
|
3338
3799
|
var realGitConfigGet = (key) => {
|
|
3339
3800
|
return new Promise((resolve, reject) => {
|
|
3340
|
-
const child =
|
|
3801
|
+
const child = spawn8("git", ["config", "--global", "--get", key], {
|
|
3341
3802
|
stdio: ["ignore", "pipe", "inherit"]
|
|
3342
3803
|
});
|
|
3343
3804
|
let stdout = "";
|
|
@@ -3361,20 +3822,51 @@ var realIdentityPrompt = async (key) => {
|
|
|
3361
3822
|
const trimmed = value.trim();
|
|
3362
3823
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
3363
3824
|
};
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
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 = {}) {
|
|
3367
3858
|
const spawnFn = options.spawn ?? realGitConfigGet;
|
|
3368
3859
|
const promptFn = options.prompt ?? realIdentityPrompt;
|
|
3860
|
+
const scopePromptFn = options.scopePrompt ?? realScopePrompt;
|
|
3369
3861
|
const logger = options.logger ?? { info: () => {
|
|
3370
3862
|
}, warn: () => {
|
|
3371
3863
|
} };
|
|
3372
|
-
const
|
|
3864
|
+
const persisted = options.persistedValues ?? {};
|
|
3373
3865
|
const name = await resolveKey("user.name", {
|
|
3374
3866
|
override: options.containerOverride?.name,
|
|
3375
3867
|
defaultValue: options.defaults?.name,
|
|
3376
3868
|
spawnFn,
|
|
3377
|
-
persistedValue:
|
|
3869
|
+
persistedValue: persisted.name,
|
|
3378
3870
|
promptFn,
|
|
3379
3871
|
logger
|
|
3380
3872
|
});
|
|
@@ -3382,35 +3874,77 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
3382
3874
|
override: options.containerOverride?.email,
|
|
3383
3875
|
defaultValue: options.defaults?.email,
|
|
3384
3876
|
spawnFn,
|
|
3385
|
-
persistedValue:
|
|
3877
|
+
persistedValue: persisted.email,
|
|
3386
3878
|
promptFn,
|
|
3387
3879
|
logger
|
|
3388
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
|
+
});
|
|
3389
3922
|
const lines = ["[user]"];
|
|
3390
|
-
if (name !== void 0) lines.push(` name = ${name}`);
|
|
3391
|
-
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}`);
|
|
3392
3925
|
await fs9.mkdir(gitconfigDir, { recursive: true });
|
|
3393
3926
|
await fs9.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
3394
3927
|
return {
|
|
3395
|
-
...name !== void 0 ? { name } : {},
|
|
3396
|
-
...email !== void 0 ? { email } : {},
|
|
3397
|
-
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 } : {}
|
|
3398
3932
|
};
|
|
3399
3933
|
}
|
|
3400
3934
|
async function resolveKey(key, opts) {
|
|
3401
3935
|
if (opts.override !== void 0 && opts.override.length > 0) {
|
|
3402
|
-
return opts.override;
|
|
3936
|
+
return { value: opts.override, source: "container" };
|
|
3403
3937
|
}
|
|
3404
3938
|
if (opts.defaultValue !== void 0 && opts.defaultValue.length > 0) {
|
|
3405
|
-
return opts.defaultValue;
|
|
3939
|
+
return { value: opts.defaultValue, source: "defaults" };
|
|
3406
3940
|
}
|
|
3407
3941
|
const hostValue = await readKeyFromHost(opts.spawnFn, key, opts.logger);
|
|
3408
|
-
if (hostValue !== void 0) return hostValue;
|
|
3942
|
+
if (hostValue !== void 0) return { value: hostValue, source: "host" };
|
|
3409
3943
|
if (opts.persistedValue !== void 0 && opts.persistedValue.length > 0) {
|
|
3410
|
-
return opts.persistedValue;
|
|
3944
|
+
return { value: opts.persistedValue, source: "persisted" };
|
|
3411
3945
|
}
|
|
3412
3946
|
const prompted = await opts.promptFn(key);
|
|
3413
|
-
if (prompted !== void 0) return prompted;
|
|
3947
|
+
if (prompted !== void 0) return { value: prompted, source: "prompt" };
|
|
3414
3948
|
opts.logger.warn(
|
|
3415
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.`
|
|
3416
3950
|
);
|
|
@@ -3492,13 +4026,17 @@ ${sectionLine(label)}
|
|
|
3492
4026
|
warn: logger.warn ?? logger.info
|
|
3493
4027
|
};
|
|
3494
4028
|
if (hasRepos || hasContainerGitUser || hasDefaultGitUser) {
|
|
3495
|
-
await collectGitIdentity(targetDir, {
|
|
4029
|
+
const identity = await collectGitIdentity(targetDir, {
|
|
3496
4030
|
...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
|
|
3497
4031
|
...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
|
|
4032
|
+
...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
|
|
3498
4033
|
...parsed.config.git?.user ? { containerOverride: parsed.config.git.user } : {},
|
|
3499
4034
|
...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
|
|
3500
4035
|
logger: idLogger
|
|
3501
4036
|
});
|
|
4037
|
+
if (identity.prompted) {
|
|
4038
|
+
await persistPromptedIdentity(identity.prompted, ymlPath, home, logger);
|
|
4039
|
+
}
|
|
3502
4040
|
}
|
|
3503
4041
|
const hostsToFetch = uniqueHttpsHosts(createOpts.repos ?? []);
|
|
3504
4042
|
const unknownProviderHosts = hostsToFetch.filter((h) => h.provider === "unknown").map((h) => h.host);
|
|
@@ -3631,9 +4169,59 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
|
|
|
3631
4169
|
}
|
|
3632
4170
|
}
|
|
3633
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
|
+
}
|
|
3634
4222
|
|
|
3635
4223
|
// src/version.ts
|
|
3636
|
-
var CLI_VERSION = true ? "1.
|
|
4224
|
+
var CLI_VERSION = true ? "1.8.0" : "dev";
|
|
3637
4225
|
|
|
3638
4226
|
// src/commands/_dispatch.ts
|
|
3639
4227
|
import { consola as consola12 } from "consola";
|
|
@@ -3895,7 +4483,7 @@ import { consola as consola13 } from "consola";
|
|
|
3895
4483
|
|
|
3896
4484
|
// src/init/components.ts
|
|
3897
4485
|
import { existsSync as existsSync5, promises as fs11 } from "fs";
|
|
3898
|
-
import
|
|
4486
|
+
import path11 from "path";
|
|
3899
4487
|
import { z as z3 } from "zod";
|
|
3900
4488
|
import { parse as parseYaml } from "yaml";
|
|
3901
4489
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -3952,14 +4540,14 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
3952
4540
|
async function walk(baseDir, currentDir, out) {
|
|
3953
4541
|
const entries = await fs11.readdir(currentDir, { withFileTypes: true });
|
|
3954
4542
|
for (const entry2 of entries) {
|
|
3955
|
-
const full =
|
|
4543
|
+
const full = path11.join(currentDir, entry2.name);
|
|
3956
4544
|
if (entry2.isDirectory()) {
|
|
3957
4545
|
await walk(baseDir, full, out);
|
|
3958
4546
|
continue;
|
|
3959
4547
|
}
|
|
3960
4548
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
3961
|
-
const relative =
|
|
3962
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
4549
|
+
const relative = path11.relative(baseDir, full);
|
|
4550
|
+
const name = relative.replace(/\.yml$/, "").split(path11.sep).join("/");
|
|
3963
4551
|
const text = await fs11.readFile(full, "utf8");
|
|
3964
4552
|
let raw;
|
|
3965
4553
|
try {
|
|
@@ -4059,35 +4647,49 @@ Available: ${available.join(", ")}.`
|
|
|
4059
4647
|
}
|
|
4060
4648
|
|
|
4061
4649
|
// src/init/generator.ts
|
|
4062
|
-
var
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
"#",
|
|
4066
|
-
"# Each section below carries inline comments for the options it",
|
|
4067
|
-
"# accepts. Features expose additional knobs as commented hints",
|
|
4068
|
-
"# under their `options:` block \u2014 un-comment what you need."
|
|
4069
|
-
];
|
|
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;
|
|
4070
4653
|
function generateComposedYml(name, components, lookupManifest, repoUrls = [], ports = []) {
|
|
4071
4654
|
const merged = mergeComponents(components);
|
|
4072
4655
|
const lines = [];
|
|
4073
|
-
|
|
4656
|
+
pushHeader(lines, SCHEMA_HEADER_ACTIVE, name);
|
|
4074
4657
|
lines.push("");
|
|
4075
4658
|
lines.push("schemaVersion: 1");
|
|
4076
4659
|
lines.push(`name: ${name}`);
|
|
4077
4660
|
lines.push("");
|
|
4078
4661
|
if (merged.languages.length > 0) {
|
|
4662
|
+
pushSectionHeader(
|
|
4663
|
+
lines,
|
|
4664
|
+
LANGUAGES_HEADER,
|
|
4665
|
+
/* commented */
|
|
4666
|
+
false
|
|
4667
|
+
);
|
|
4079
4668
|
lines.push("languages:");
|
|
4080
4669
|
for (const lang of merged.languages) lines.push(` - ${lang}`);
|
|
4081
4670
|
lines.push("");
|
|
4082
4671
|
}
|
|
4083
4672
|
if (merged.services.length > 0) {
|
|
4673
|
+
pushSectionHeader(
|
|
4674
|
+
lines,
|
|
4675
|
+
SERVICES_HEADER,
|
|
4676
|
+
/* commented */
|
|
4677
|
+
false
|
|
4678
|
+
);
|
|
4084
4679
|
lines.push("services:");
|
|
4085
4680
|
for (const svc of merged.services) lines.push(` - ${svc}`);
|
|
4086
4681
|
lines.push("");
|
|
4087
4682
|
}
|
|
4088
4683
|
if (merged.features.length > 0) {
|
|
4684
|
+
pushSectionHeader(
|
|
4685
|
+
lines,
|
|
4686
|
+
FEATURES_HEADER_ACTIVE,
|
|
4687
|
+
/* commented */
|
|
4688
|
+
false
|
|
4689
|
+
);
|
|
4089
4690
|
lines.push("features:");
|
|
4090
4691
|
for (const f of merged.features) {
|
|
4692
|
+
lines.push("");
|
|
4091
4693
|
renderFeatureBlock(
|
|
4092
4694
|
lines,
|
|
4093
4695
|
f,
|
|
@@ -4099,87 +4701,86 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
4099
4701
|
lines.push("");
|
|
4100
4702
|
}
|
|
4101
4703
|
if (repoUrls.length > 0) {
|
|
4102
|
-
|
|
4704
|
+
pushSectionHeader(
|
|
4103
4705
|
lines,
|
|
4104
|
-
|
|
4706
|
+
REPOS_HEADER,
|
|
4105
4707
|
/* commented */
|
|
4106
4708
|
false
|
|
4107
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("");
|
|
4108
4721
|
}
|
|
4109
4722
|
if (ports.length > 0) {
|
|
4110
|
-
|
|
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("");
|
|
4111
4736
|
}
|
|
4112
4737
|
return ensureTrailingNewline(lines.join("\n"));
|
|
4113
4738
|
}
|
|
4114
4739
|
function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], ports = []) {
|
|
4115
4740
|
const byCategory = groupByCategory(catalog);
|
|
4116
4741
|
const lines = [];
|
|
4117
|
-
|
|
4118
|
-
lines.push("#");
|
|
4119
|
-
lines.push("# Below is the full set of components shipped with this");
|
|
4120
|
-
lines.push("# workbench, every one commented out. Un-comment the lines");
|
|
4121
|
-
lines.push("# you want active. The same effect (and a cleaner yml) is");
|
|
4122
|
-
lines.push("# achievable by running `monoceros init <name> --with=\u2026`");
|
|
4123
|
-
lines.push("# with a comma-separated list of component names.");
|
|
4742
|
+
pushHeader(lines, SCHEMA_HEADER_DOCUMENTED, name);
|
|
4124
4743
|
lines.push("");
|
|
4125
4744
|
lines.push("schemaVersion: 1");
|
|
4126
4745
|
lines.push(`name: ${name}`);
|
|
4127
4746
|
lines.push("");
|
|
4128
4747
|
if (byCategory.language.length > 0) {
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4748
|
+
pushSectionHeader(
|
|
4749
|
+
lines,
|
|
4750
|
+
LANGUAGES_HEADER,
|
|
4751
|
+
/* commented */
|
|
4752
|
+
true
|
|
4134
4753
|
);
|
|
4135
|
-
const width = Math.max(...items.map((i) => i.value.length)) + 2;
|
|
4136
|
-
lines.push("# Languages \u2014 runtime toolchains.");
|
|
4137
4754
|
lines.push("# languages:");
|
|
4138
|
-
for (const
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4755
|
+
for (const c of byCategory.language) {
|
|
4756
|
+
for (const lang of c.file.contributes.languages ?? []) {
|
|
4757
|
+
lines.push(`# - ${lang}`);
|
|
4758
|
+
}
|
|
4141
4759
|
}
|
|
4142
4760
|
lines.push("");
|
|
4143
4761
|
}
|
|
4144
4762
|
if (byCategory.service.length > 0) {
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4763
|
+
pushSectionHeader(
|
|
4764
|
+
lines,
|
|
4765
|
+
SERVICES_HEADER,
|
|
4766
|
+
/* commented */
|
|
4767
|
+
true
|
|
4150
4768
|
);
|
|
4151
|
-
const width = Math.max(...items.map((i) => i.value.length)) + 2;
|
|
4152
|
-
lines.push("# Services \u2014 compose-mode siblings of the workspace");
|
|
4153
|
-
lines.push("# container (compose mode kicks in as soon as at least");
|
|
4154
|
-
lines.push("# one service is active).");
|
|
4155
4769
|
lines.push("# services:");
|
|
4156
|
-
for (const
|
|
4157
|
-
const
|
|
4158
|
-
|
|
4770
|
+
for (const c of byCategory.service) {
|
|
4771
|
+
for (const svc of c.file.contributes.services ?? []) {
|
|
4772
|
+
lines.push(`# - ${svc}`);
|
|
4773
|
+
}
|
|
4159
4774
|
}
|
|
4160
4775
|
lines.push("");
|
|
4161
4776
|
}
|
|
4162
4777
|
if (byCategory.feature.length > 0) {
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
lines.push("#");
|
|
4170
|
-
lines.push("# Catalog:");
|
|
4171
|
-
lines.push("#");
|
|
4172
|
-
const nameColumnWidth = Math.max(...byCategory.feature.map((c) => c.name.length)) + 2;
|
|
4173
|
-
for (const c of byCategory.feature) {
|
|
4174
|
-
const pad = " ".repeat(nameColumnWidth - c.name.length);
|
|
4175
|
-
lines.push(`# ${c.name}${pad}${c.file.displayName}`);
|
|
4176
|
-
}
|
|
4177
|
-
lines.push("#");
|
|
4178
|
-
lines.push("# Below: one block per feature ref. Un-comment what");
|
|
4179
|
-
lines.push("# you want active. Sub-components share their parent's");
|
|
4180
|
-
lines.push("# block \u2014 pick the parent for the full preset, swap to");
|
|
4181
|
-
lines.push("# a sub-component name for a partial install.");
|
|
4182
|
-
lines.push("#");
|
|
4778
|
+
pushSectionHeader(
|
|
4779
|
+
lines,
|
|
4780
|
+
FEATURES_HEADER_DOCUMENTED,
|
|
4781
|
+
/* commented */
|
|
4782
|
+
true
|
|
4783
|
+
);
|
|
4183
4784
|
lines.push("# features:");
|
|
4184
4785
|
const renderedRefs = /* @__PURE__ */ new Set();
|
|
4185
4786
|
const topLevel = byCategory.feature.filter((c) => !c.name.includes("/"));
|
|
@@ -4187,6 +4788,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4187
4788
|
for (const f of c.file.contributes.features ?? []) {
|
|
4188
4789
|
if (renderedRefs.has(f.ref)) continue;
|
|
4189
4790
|
renderedRefs.add(f.ref);
|
|
4791
|
+
lines.push("#");
|
|
4190
4792
|
renderFeatureBlock(
|
|
4191
4793
|
lines,
|
|
4192
4794
|
f,
|
|
@@ -4201,6 +4803,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4201
4803
|
for (const f of c.file.contributes.features ?? []) {
|
|
4202
4804
|
if (renderedRefs.has(f.ref)) continue;
|
|
4203
4805
|
renderedRefs.add(f.ref);
|
|
4806
|
+
lines.push("#");
|
|
4204
4807
|
renderFeatureBlock(
|
|
4205
4808
|
lines,
|
|
4206
4809
|
f,
|
|
@@ -4212,165 +4815,156 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
4212
4815
|
}
|
|
4213
4816
|
lines.push("");
|
|
4214
4817
|
}
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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
|
+
}
|
|
4221
4847
|
if (ports.length > 0) {
|
|
4222
|
-
|
|
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("");
|
|
4223
4861
|
} else {
|
|
4224
|
-
|
|
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("");
|
|
4225
4874
|
}
|
|
4226
4875
|
return ensureTrailingNewline(lines.join("\n"));
|
|
4227
4876
|
}
|
|
4228
|
-
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
|
+
}
|
|
4229
4885
|
function renderFeatureBlock(out, feature, summary, commented) {
|
|
4230
|
-
const
|
|
4231
|
-
const
|
|
4232
|
-
const
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
)) {
|
|
4240
|
-
out.push(`${c}# ${line}`);
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
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}`);
|
|
4244
4895
|
const options = feature.options ?? {};
|
|
4245
|
-
const
|
|
4246
|
-
const
|
|
4247
|
-
if (
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
if (remainingHints.length > 0) {
|
|
4253
|
-
out.push(
|
|
4254
|
-
`${c} # Optional \u2014 override monoceros-config.yml defaults.features:`
|
|
4255
|
-
);
|
|
4256
|
-
for (const hint of remainingHints) {
|
|
4257
|
-
emitHint(out, hint, optionDescriptions[hint], `${c} `);
|
|
4258
|
-
}
|
|
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)}`);
|
|
4259
4903
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
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)}`);
|
|
4267
4913
|
}
|
|
4268
4914
|
}
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
description,
|
|
4274
|
-
COMMENT_WIDTH - linePrefix.length
|
|
4275
|
-
)) {
|
|
4276
|
-
out.push(`${linePrefix}# ${line}`);
|
|
4915
|
+
if (hintKeys.length > 0) {
|
|
4916
|
+
out.push(` # options:`);
|
|
4917
|
+
for (const key of hintKeys) {
|
|
4918
|
+
out.push(` # ${key}:`);
|
|
4277
4919
|
}
|
|
4278
4920
|
}
|
|
4279
|
-
out.push(`${linePrefix}${hint}:`);
|
|
4280
4921
|
}
|
|
4281
|
-
function
|
|
4282
|
-
out
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
if (
|
|
4289
|
-
out.push(
|
|
4290
|
-
|
|
4291
|
-
out.push(
|
|
4292
|
-
|
|
4293
|
-
);
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
);
|
|
4297
|
-
out.push(
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
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(", ")}.`);
|
|
4305
4947
|
}
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
out.push(
|
|
4311
|
-
` # path: ${derivedPath} # subfolder under projects/; default: URL-derived (${derivedPath})`
|
|
4312
|
-
);
|
|
4313
|
-
out.push(
|
|
4314
|
-
" # provider: github # github | gitlab | bitbucket | gitea"
|
|
4315
|
-
);
|
|
4316
|
-
out.push(
|
|
4317
|
-
" # git: # per-repo committer identity override"
|
|
4318
|
-
);
|
|
4319
|
-
out.push(" # user:");
|
|
4320
|
-
out.push(" # name: Your Name");
|
|
4321
|
-
out.push(" # email: you@example.com");
|
|
4322
|
-
}
|
|
4323
|
-
out.push("");
|
|
4324
|
-
}
|
|
4325
|
-
function renderRoutingHintBlock(out) {
|
|
4326
|
-
out.push("# Routing \u2014 expose container ports to the host through the");
|
|
4327
|
-
out.push("# shared Traefik singleton. Once any port is declared the");
|
|
4328
|
-
out.push("# container joins the monoceros-proxy network and the proxy");
|
|
4329
|
-
out.push("# routes <name>.localhost (default port) and");
|
|
4330
|
-
out.push("# <name>-<port>.localhost (explicit). `monoceros add-port`");
|
|
4331
|
-
out.push("# manages the list; the block appears on first add. You can");
|
|
4332
|
-
out.push("# also pre-seed at init time via `--with-ports=3000,5173,\u2026`.");
|
|
4333
|
-
out.push("#");
|
|
4334
|
-
out.push("# routing:");
|
|
4335
|
-
out.push("# ports: # internal container ports");
|
|
4336
|
-
out.push(
|
|
4337
|
-
"# - 3000 # first entry doubles as <name>.localhost"
|
|
4338
|
-
);
|
|
4339
|
-
out.push("# - 5173");
|
|
4340
|
-
out.push(
|
|
4341
|
-
"# vscodeAutoForward: false # default: false. Traefik is the single"
|
|
4342
|
-
);
|
|
4343
|
-
out.push(
|
|
4344
|
-
"# # source of truth \u2014 set true only if you"
|
|
4345
|
-
);
|
|
4346
|
-
out.push(
|
|
4347
|
-
"# # want VS Code's port panel as primary."
|
|
4348
|
-
);
|
|
4349
|
-
out.push("");
|
|
4350
|
-
}
|
|
4351
|
-
function renderActiveRoutingBlock(out, name, ports) {
|
|
4352
|
-
out.push("# Routing \u2014 expose these container ports to the host through");
|
|
4353
|
-
out.push("# the shared Traefik singleton. First entry doubles as");
|
|
4354
|
-
out.push(`# http://${name}.localhost (the default route).`);
|
|
4355
|
-
out.push("routing:");
|
|
4356
|
-
out.push(" ports:");
|
|
4357
|
-
ports.forEach((port, idx) => {
|
|
4358
|
-
if (idx === 0) {
|
|
4359
|
-
out.push(` - ${port} # default \u2192 http://${name}.localhost`);
|
|
4360
|
-
} else {
|
|
4361
|
-
out.push(` - ${port}`);
|
|
4362
|
-
}
|
|
4363
|
-
});
|
|
4364
|
-
out.push(" # vscodeAutoForward: false # set true to keep VS Code's");
|
|
4365
|
-
out.push(" # # port-panel alongside Traefik");
|
|
4366
|
-
out.push("");
|
|
4948
|
+
if (summary.documentationURL) {
|
|
4949
|
+
out.push(`See ${summary.documentationURL} for further information.`);
|
|
4950
|
+
}
|
|
4951
|
+
return out;
|
|
4367
4952
|
}
|
|
4368
|
-
function
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
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
|
+
}
|
|
4374
4968
|
}
|
|
4375
4969
|
function wrapToComment(text, width) {
|
|
4376
4970
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -4419,10 +5013,10 @@ function ensureTrailingNewline(s) {
|
|
|
4419
5013
|
|
|
4420
5014
|
// src/init/manifest.ts
|
|
4421
5015
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
4422
|
-
import
|
|
5016
|
+
import path12 from "path";
|
|
4423
5017
|
function resolveManifestPath(name, checkoutRoot) {
|
|
4424
5018
|
if (checkoutRoot) {
|
|
4425
|
-
const checkoutPath =
|
|
5019
|
+
const checkoutPath = path12.join(
|
|
4426
5020
|
checkoutRoot,
|
|
4427
5021
|
"images",
|
|
4428
5022
|
"features",
|
|
@@ -4431,7 +5025,7 @@ function resolveManifestPath(name, checkoutRoot) {
|
|
|
4431
5025
|
);
|
|
4432
5026
|
if (existsSync6(checkoutPath)) return checkoutPath;
|
|
4433
5027
|
}
|
|
4434
|
-
const bundlePath =
|
|
5028
|
+
const bundlePath = path12.join(
|
|
4435
5029
|
bundledFeaturesDir(),
|
|
4436
5030
|
name,
|
|
4437
5031
|
"devcontainer-feature.json"
|
|
@@ -4463,7 +5057,18 @@ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot())
|
|
|
4463
5057
|
}
|
|
4464
5058
|
}
|
|
4465
5059
|
}
|
|
4466
|
-
|
|
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
|
+
};
|
|
4467
5072
|
} catch {
|
|
4468
5073
|
return void 0;
|
|
4469
5074
|
}
|
|
@@ -4541,6 +5146,17 @@ async function runInit(opts) {
|
|
|
4541
5146
|
seenPorts.add(raw);
|
|
4542
5147
|
ports.push(raw);
|
|
4543
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
|
+
}
|
|
4544
5160
|
let text;
|
|
4545
5161
|
const requested = opts.with ?? [];
|
|
4546
5162
|
if (requested.length === 0) {
|
|
@@ -4551,6 +5167,51 @@ async function runInit(opts) {
|
|
|
4551
5167
|
}
|
|
4552
5168
|
await fs12.mkdir(containerConfigsDir(home), { recursive: true });
|
|
4553
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
|
+
}
|
|
4554
5215
|
const documented = requested.length === 0;
|
|
4555
5216
|
const displayPath = prettyPath(dest);
|
|
4556
5217
|
if (documented) {
|
|
@@ -4974,7 +5635,7 @@ import { createInterface } from "readline/promises";
|
|
|
4974
5635
|
|
|
4975
5636
|
// src/remove/index.ts
|
|
4976
5637
|
import { existsSync as existsSync8, promises as fs13 } from "fs";
|
|
4977
|
-
import
|
|
5638
|
+
import path13 from "path";
|
|
4978
5639
|
import { consola as consola19 } from "consola";
|
|
4979
5640
|
async function runRemove(opts) {
|
|
4980
5641
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -5025,13 +5686,13 @@ async function runRemove(opts) {
|
|
|
5025
5686
|
let backupPath = null;
|
|
5026
5687
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
5027
5688
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5028
|
-
backupPath =
|
|
5689
|
+
backupPath = path13.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
5029
5690
|
await fs13.mkdir(backupPath, { recursive: true });
|
|
5030
5691
|
if (hasYml) {
|
|
5031
|
-
await fs13.copyFile(ymlPath,
|
|
5692
|
+
await fs13.copyFile(ymlPath, path13.join(backupPath, `${opts.name}.yml`));
|
|
5032
5693
|
}
|
|
5033
5694
|
if (hasContainer) {
|
|
5034
|
-
await fs13.cp(containerPath,
|
|
5695
|
+
await fs13.cp(containerPath, path13.join(backupPath, "container"), {
|
|
5035
5696
|
recursive: true
|
|
5036
5697
|
});
|
|
5037
5698
|
}
|
|
@@ -5144,7 +5805,7 @@ import { consola as consola22 } from "consola";
|
|
|
5144
5805
|
|
|
5145
5806
|
// src/restore/index.ts
|
|
5146
5807
|
import { existsSync as existsSync9, promises as fs14 } from "fs";
|
|
5147
|
-
import
|
|
5808
|
+
import path14 from "path";
|
|
5148
5809
|
import { consola as consola21 } from "consola";
|
|
5149
5810
|
async function runRestore(opts) {
|
|
5150
5811
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -5152,7 +5813,7 @@ async function runRestore(opts) {
|
|
|
5152
5813
|
info: (msg) => consola21.info(msg),
|
|
5153
5814
|
success: (msg) => consola21.success(msg)
|
|
5154
5815
|
};
|
|
5155
|
-
const backup =
|
|
5816
|
+
const backup = path14.resolve(opts.backupPath);
|
|
5156
5817
|
if (!existsSync9(backup)) {
|
|
5157
5818
|
throw new Error(`Backup not found: ${backup}.`);
|
|
5158
5819
|
}
|
|
@@ -5174,7 +5835,7 @@ async function runRestore(opts) {
|
|
|
5174
5835
|
}
|
|
5175
5836
|
const ymlFile = ymlFiles[0];
|
|
5176
5837
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
5177
|
-
const containerInBackup =
|
|
5838
|
+
const containerInBackup = path14.join(backup, "container");
|
|
5178
5839
|
const hasContainer = existsSync9(containerInBackup);
|
|
5179
5840
|
const destYml = containerConfigPath(name, home);
|
|
5180
5841
|
const destContainer = containerDir(name, home);
|
|
@@ -5189,7 +5850,7 @@ async function runRestore(opts) {
|
|
|
5189
5850
|
);
|
|
5190
5851
|
}
|
|
5191
5852
|
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
5192
|
-
await fs14.copyFile(
|
|
5853
|
+
await fs14.copyFile(path14.join(backup, ymlFile), destYml);
|
|
5193
5854
|
if (hasContainer) {
|
|
5194
5855
|
await fs14.cp(containerInBackup, destContainer, { recursive: true });
|
|
5195
5856
|
}
|
|
@@ -5450,7 +6111,7 @@ import { consola as consola28 } from "consola";
|
|
|
5450
6111
|
|
|
5451
6112
|
// src/devcontainer/shell.ts
|
|
5452
6113
|
import { existsSync as existsSync10 } from "fs";
|
|
5453
|
-
import
|
|
6114
|
+
import path15 from "path";
|
|
5454
6115
|
async function runShell(opts) {
|
|
5455
6116
|
assertContainerExists(opts.root);
|
|
5456
6117
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -5473,7 +6134,7 @@ async function runShell(opts) {
|
|
|
5473
6134
|
);
|
|
5474
6135
|
}
|
|
5475
6136
|
function assertContainerExists(root) {
|
|
5476
|
-
if (!existsSync10(
|
|
6137
|
+
if (!existsSync10(path15.join(root, ".devcontainer"))) {
|
|
5477
6138
|
throw new Error(
|
|
5478
6139
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
5479
6140
|
);
|