@getmonoceros/workbench 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1185 -519
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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 path21 = [];
|
|
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
|
+
path21.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
|
+
path21.push(tok);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
271
271
|
}
|
|
272
|
-
return { path:
|
|
272
|
+
return { path: path21, cmd: cursor };
|
|
273
273
|
}
|
|
274
274
|
async function maybeRenderHelp(argv, main2) {
|
|
275
275
|
const hit = detectHelpRequest(argv, main2);
|
|
@@ -311,7 +311,7 @@ import { consola as consola2 } from "consola";
|
|
|
311
311
|
import { promises as fs8 } from "fs";
|
|
312
312
|
import { consola } from "consola";
|
|
313
313
|
import { createPatch } from "diff";
|
|
314
|
-
import
|
|
314
|
+
import path9 from "path";
|
|
315
315
|
|
|
316
316
|
// src/config/io.ts
|
|
317
317
|
import { promises as fs } from "fs";
|
|
@@ -404,6 +404,68 @@ var RoutingSchema = z.object({
|
|
|
404
404
|
ports: z.array(PortEntrySchema).default([]),
|
|
405
405
|
vscodeAutoForward: z.boolean().optional()
|
|
406
406
|
});
|
|
407
|
+
var SERVICE_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
408
|
+
var ServiceEnvValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]).transform((v) => v === null ? "" : String(v));
|
|
409
|
+
var ServiceHealthcheckSchema = z.object({
|
|
410
|
+
// Compose accepts both forms and they differ semantically:
|
|
411
|
+
// - string → run via the shell (CMD-SHELL)
|
|
412
|
+
// - ["CMD", …] → exec the args directly, no shell
|
|
413
|
+
// - ["CMD-SHELL", …]
|
|
414
|
+
// We accept either and render it back faithfully.
|
|
415
|
+
test: z.union([
|
|
416
|
+
z.string().min(1, "Healthcheck test must not be empty."),
|
|
417
|
+
z.array(z.string().min(1)).min(1, "Healthcheck test array must not be empty.")
|
|
418
|
+
]),
|
|
419
|
+
interval: z.string().optional(),
|
|
420
|
+
timeout: z.string().optional(),
|
|
421
|
+
retries: z.number().int().min(1).optional(),
|
|
422
|
+
startPeriod: z.string().optional()
|
|
423
|
+
});
|
|
424
|
+
var SERVICE_RESTART_VALUES = [
|
|
425
|
+
"no",
|
|
426
|
+
"always",
|
|
427
|
+
"on-failure",
|
|
428
|
+
"unless-stopped"
|
|
429
|
+
];
|
|
430
|
+
function isValidServiceVolume(spec) {
|
|
431
|
+
const parts = spec.split(":");
|
|
432
|
+
if (parts.length < 2 || parts.length > 3) return false;
|
|
433
|
+
const [src, dest, mode] = parts;
|
|
434
|
+
if (!src || !dest) return false;
|
|
435
|
+
if (!dest.startsWith("/")) return false;
|
|
436
|
+
if (mode !== void 0 && !/^(ro|rw|cached|delegated|z|Z)$/.test(mode)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
if (src === "data") return true;
|
|
440
|
+
if (src.startsWith("/")) return false;
|
|
441
|
+
const looksLikePath = src.startsWith("./") || src.includes("/");
|
|
442
|
+
if (!looksLikePath) return false;
|
|
443
|
+
const normalized = src.startsWith("./") ? src.slice(2) : src;
|
|
444
|
+
if (normalized.split("/").some((s) => s === ".." || s === ".")) return false;
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
var ServiceObjectSchema = z.object({
|
|
448
|
+
name: z.string().regex(
|
|
449
|
+
SERVICE_NAME_RE,
|
|
450
|
+
"Invalid service name. Use lowercase letters, digits, '_' or '-' (must start with a letter or digit)."
|
|
451
|
+
),
|
|
452
|
+
image: z.string().min(1, "Service image must not be empty."),
|
|
453
|
+
// In-container port the service listens on. Used by
|
|
454
|
+
// `monoceros tunnel <name> <service>` to forward without an explicit
|
|
455
|
+
// port argument. NOT a host port mapping — host exposure goes through
|
|
456
|
+
// routing.ports (Traefik) or `monoceros tunnel`.
|
|
457
|
+
port: z.number().int().min(1, "Port must be \u2265 1.").max(65535).optional(),
|
|
458
|
+
env: z.record(z.string(), ServiceEnvValueSchema).optional(),
|
|
459
|
+
volumes: z.array(
|
|
460
|
+
z.string().refine(
|
|
461
|
+
isValidServiceVolume,
|
|
462
|
+
"Invalid volume. Use 'data:/container/path' for the per-service persistent dir, or a relative host path ('projects/app/init.sql:/...:ro', './config:/...'). Docker named volumes (a bare name like 'rustfs_data') are not supported; absolute host paths and '..' are rejected."
|
|
463
|
+
)
|
|
464
|
+
).optional(),
|
|
465
|
+
healthcheck: ServiceHealthcheckSchema.optional(),
|
|
466
|
+
restart: z.enum(SERVICE_RESTART_VALUES).optional(),
|
|
467
|
+
command: z.string().optional()
|
|
468
|
+
});
|
|
407
469
|
var ExternalServicesSchema = z.object({
|
|
408
470
|
postgres: z.string().regex(
|
|
409
471
|
POSTGRES_URL_RE,
|
|
@@ -430,7 +492,7 @@ var SolutionConfigSchema = z.object({
|
|
|
430
492
|
"Invalid install URL. Must start with 'https://' and contain only URL-safe characters (no shell metacharacters)."
|
|
431
493
|
)
|
|
432
494
|
).default([]),
|
|
433
|
-
services: z.array(
|
|
495
|
+
services: z.array(ServiceObjectSchema).default([]),
|
|
434
496
|
repos: z.array(RepoEntrySchema).default([]),
|
|
435
497
|
routing: RoutingSchema.optional(),
|
|
436
498
|
externalServices: ExternalServicesSchema.default({}),
|
|
@@ -549,6 +611,9 @@ function containerConfigsDir(home = monocerosHome()) {
|
|
|
549
611
|
function containerConfigPath(name, home = monocerosHome()) {
|
|
550
612
|
return path.join(containerConfigsDir(home), `${name}.yml`);
|
|
551
613
|
}
|
|
614
|
+
function containerEnvPath(name, home = monocerosHome()) {
|
|
615
|
+
return path.join(containerConfigsDir(home), `${name}.env`);
|
|
616
|
+
}
|
|
552
617
|
function containersDir(home = monocerosHome()) {
|
|
553
618
|
return path.join(home, "container");
|
|
554
619
|
}
|
|
@@ -569,10 +634,323 @@ function prettyPath(p) {
|
|
|
569
634
|
return p;
|
|
570
635
|
}
|
|
571
636
|
|
|
637
|
+
// src/config/env-file.ts
|
|
638
|
+
import { existsSync as existsSync2, readFileSync, promises as fsp } from "fs";
|
|
639
|
+
import path2 from "path";
|
|
640
|
+
var ENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
|
|
641
|
+
function parseEnvFile(content) {
|
|
642
|
+
const out = {};
|
|
643
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
644
|
+
const trimmed = raw.trim();
|
|
645
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
646
|
+
const m = ENV_LINE_RE.exec(raw);
|
|
647
|
+
if (!m) continue;
|
|
648
|
+
const key = m[1];
|
|
649
|
+
let val = m[2].trim();
|
|
650
|
+
if (val.length >= 2 && (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'"))) {
|
|
651
|
+
val = val.slice(1, -1);
|
|
652
|
+
}
|
|
653
|
+
out[key] = val;
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function readEnvFile(envPath) {
|
|
658
|
+
if (!existsSync2(envPath)) return {};
|
|
659
|
+
return parseEnvFile(readFileSync(envPath, "utf8"));
|
|
660
|
+
}
|
|
661
|
+
async function ensureEnvGitignored(configsDir) {
|
|
662
|
+
const gitignorePath = path2.join(configsDir, ".gitignore");
|
|
663
|
+
const pattern = "*.env";
|
|
664
|
+
let existing = "";
|
|
665
|
+
if (existsSync2(gitignorePath)) {
|
|
666
|
+
existing = readFileSync(gitignorePath, "utf8");
|
|
667
|
+
const lines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
668
|
+
if (lines.includes(pattern)) return;
|
|
669
|
+
}
|
|
670
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
671
|
+
const header = existing.length === 0 ? "# Per-container env files hold the secrets behind the yml ${VAR}\n# references. Never commit them.\n" : "";
|
|
672
|
+
await fsp.appendFile(gitignorePath, `${prefix}${header}${pattern}
|
|
673
|
+
`);
|
|
674
|
+
}
|
|
675
|
+
var VAR_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
676
|
+
function interpolate(value, vars) {
|
|
677
|
+
const missing = [];
|
|
678
|
+
const out = value.replace(VAR_RE, (_match, name) => {
|
|
679
|
+
if (Object.prototype.hasOwnProperty.call(vars, name)) return vars[name];
|
|
680
|
+
missing.push(name);
|
|
681
|
+
return _match;
|
|
682
|
+
});
|
|
683
|
+
return { value: out, missing };
|
|
684
|
+
}
|
|
685
|
+
function interpolateServices(services, vars) {
|
|
686
|
+
const missing = [];
|
|
687
|
+
const resolved = services.map((svc) => {
|
|
688
|
+
const interp = (raw, field) => {
|
|
689
|
+
const r = interpolate(raw, vars);
|
|
690
|
+
for (const name of r.missing) {
|
|
691
|
+
missing.push({ location: `services.${svc.name}.${field}`, name });
|
|
692
|
+
}
|
|
693
|
+
return r.value;
|
|
694
|
+
};
|
|
695
|
+
const next = {
|
|
696
|
+
...svc,
|
|
697
|
+
image: interp(svc.image, "image"),
|
|
698
|
+
env: Object.fromEntries(
|
|
699
|
+
Object.entries(svc.env).map(([k, v]) => [k, interp(v, `env.${k}`)])
|
|
700
|
+
),
|
|
701
|
+
volumes: svc.volumes.map((v, i) => interp(v, `volumes[${i}]`))
|
|
702
|
+
};
|
|
703
|
+
if (svc.command !== void 0) {
|
|
704
|
+
next.command = interp(svc.command, "command");
|
|
705
|
+
}
|
|
706
|
+
if (svc.healthcheck) {
|
|
707
|
+
const hc = svc.healthcheck;
|
|
708
|
+
next.healthcheck = {
|
|
709
|
+
...hc,
|
|
710
|
+
test: Array.isArray(hc.test) ? hc.test.map((t, i) => interp(t, `healthcheck.test[${i}]`)) : interp(hc.test, "healthcheck.test"),
|
|
711
|
+
...hc.interval !== void 0 ? { interval: interp(hc.interval, "healthcheck.interval") } : {},
|
|
712
|
+
...hc.timeout !== void 0 ? { timeout: interp(hc.timeout, "healthcheck.timeout") } : {},
|
|
713
|
+
...hc.startPeriod !== void 0 ? { startPeriod: interp(hc.startPeriod, "healthcheck.startPeriod") } : {}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return next;
|
|
717
|
+
});
|
|
718
|
+
return { services: resolved, missing };
|
|
719
|
+
}
|
|
720
|
+
function interpolateFeatures(features, vars) {
|
|
721
|
+
const missing = [];
|
|
722
|
+
const out = {};
|
|
723
|
+
for (const [ref, options] of Object.entries(features)) {
|
|
724
|
+
const next = {};
|
|
725
|
+
for (const [key, value] of Object.entries(options)) {
|
|
726
|
+
if (typeof value !== "string") {
|
|
727
|
+
next[key] = value;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const r = interpolate(value, vars);
|
|
731
|
+
for (const name of r.missing) {
|
|
732
|
+
missing.push({ location: `features.${ref}.${key}`, name });
|
|
733
|
+
}
|
|
734
|
+
next[key] = r.value;
|
|
735
|
+
}
|
|
736
|
+
out[ref] = next;
|
|
737
|
+
}
|
|
738
|
+
return { features: out, missing };
|
|
739
|
+
}
|
|
740
|
+
function buildEnvStub(name) {
|
|
741
|
+
return `# Secrets and values for \${VAR} references in ${name}.yml.
|
|
742
|
+
`;
|
|
743
|
+
}
|
|
744
|
+
async function ensureEnvVars(envPath, name, vars) {
|
|
745
|
+
const exists = existsSync2(envPath);
|
|
746
|
+
let content = exists ? readFileSync(envPath, "utf8") : buildEnvStub(name);
|
|
747
|
+
const present = new Set(Object.keys(parseEnvFile(content)));
|
|
748
|
+
const added = [...new Set(vars)].filter((v) => !present.has(v));
|
|
749
|
+
if (!exists || added.length > 0) {
|
|
750
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
751
|
+
for (const v of added) content += `${v}=
|
|
752
|
+
`;
|
|
753
|
+
await fsp.mkdir(path2.dirname(envPath), { recursive: true });
|
|
754
|
+
await fsp.writeFile(envPath, content);
|
|
755
|
+
}
|
|
756
|
+
return { created: !exists, added };
|
|
757
|
+
}
|
|
758
|
+
function formatMissingVarsError(missing, envPathPretty) {
|
|
759
|
+
const lines = missing.map((m) => ` - \${${m.name}} (${m.location})`);
|
|
760
|
+
const uniqueNames = [...new Set(missing.map((m) => m.name))];
|
|
761
|
+
return `Unresolved \${VAR} references in the container yml:
|
|
762
|
+
${lines.join("\n")}
|
|
763
|
+
|
|
764
|
+
Define them in ${envPathPretty}, e.g.
|
|
765
|
+
` + uniqueNames.map((n) => ` ${n}=<value>`).join("\n");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/init/feature-doc.ts
|
|
769
|
+
function buildFeatureHeaderLines(summary, width) {
|
|
770
|
+
const paragraphs = buildHeaderParagraphs(summary);
|
|
771
|
+
const wrapped = [];
|
|
772
|
+
for (const para of paragraphs) {
|
|
773
|
+
for (const line of wrapToComment(para, width)) {
|
|
774
|
+
wrapped.push(line);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return wrapped;
|
|
778
|
+
}
|
|
779
|
+
function buildFeatureHeaderCommentBefore(summary, width) {
|
|
780
|
+
const lines = buildFeatureHeaderLines(summary, width);
|
|
781
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
782
|
+
}
|
|
783
|
+
function buildHeaderParagraphs(summary) {
|
|
784
|
+
if (!summary) return [];
|
|
785
|
+
const out = [];
|
|
786
|
+
const tagline = summary.name?.trim();
|
|
787
|
+
const description = summary.description?.trim();
|
|
788
|
+
if (tagline && description) {
|
|
789
|
+
out.push(`${tagline} \u2014 ${description}`);
|
|
790
|
+
} else if (tagline) {
|
|
791
|
+
out.push(tagline);
|
|
792
|
+
} else if (description) {
|
|
793
|
+
out.push(description);
|
|
794
|
+
}
|
|
795
|
+
for (const note of summary.usageNotes) {
|
|
796
|
+
const trimmed = note.trim();
|
|
797
|
+
if (trimmed.length > 0) out.push(trimmed);
|
|
798
|
+
}
|
|
799
|
+
if (summary.optionHints.length > 0) {
|
|
800
|
+
const parts = summary.optionHints.map((key) => {
|
|
801
|
+
const desc = summary.optionDescriptions[key];
|
|
802
|
+
const short = desc ? shortenOptionDescription(desc) : void 0;
|
|
803
|
+
return short ? `${key} (${short})` : key;
|
|
804
|
+
});
|
|
805
|
+
out.push(`Options: ${parts.join(", ")}.`);
|
|
806
|
+
}
|
|
807
|
+
if (summary.documentationURL) {
|
|
808
|
+
out.push(`See ${summary.documentationURL} for further information.`);
|
|
809
|
+
}
|
|
810
|
+
return out;
|
|
811
|
+
}
|
|
812
|
+
function shortenOptionDescription(desc) {
|
|
813
|
+
const firstSentence = desc.split(/(?<=[.!?])\s+/)[0]?.trim() ?? desc.trim();
|
|
814
|
+
return firstSentence.replace(/[.!?]+$/, "").trim();
|
|
815
|
+
}
|
|
816
|
+
function wrapToComment(text, width) {
|
|
817
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
818
|
+
if (words.length === 0) return [""];
|
|
819
|
+
const usable = Math.max(width, 20);
|
|
820
|
+
const lines = [];
|
|
821
|
+
let current = "";
|
|
822
|
+
for (const w of words) {
|
|
823
|
+
if (current.length === 0) {
|
|
824
|
+
current = w;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (current.length + 1 + w.length <= usable) {
|
|
828
|
+
current += " " + w;
|
|
829
|
+
} else {
|
|
830
|
+
lines.push(current);
|
|
831
|
+
current = w;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (current.length > 0) lines.push(current);
|
|
835
|
+
return lines;
|
|
836
|
+
}
|
|
837
|
+
var FEATURE_HEADER_WIDTH = 76 - 2;
|
|
838
|
+
function featureOptionVarName(ref, optionKey) {
|
|
839
|
+
const leaf = ref.split("/").pop() ?? ref;
|
|
840
|
+
const id = leaf.split("@")[0].split(":")[0];
|
|
841
|
+
const idSnake = id.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
842
|
+
const optSnake = optionKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
843
|
+
return `${idSnake}_${optSnake}`;
|
|
844
|
+
}
|
|
845
|
+
function featureOptionHints(summary, ref, activeKeys = []) {
|
|
846
|
+
return (summary?.optionHints ?? []).filter((key) => !activeKeys.includes(key)).map((key) => {
|
|
847
|
+
const envVar = featureOptionVarName(ref, key);
|
|
848
|
+
return { key, envVar, placeholder: `\${${envVar}}` };
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/init/manifest.ts
|
|
853
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
854
|
+
import path3 from "path";
|
|
855
|
+
|
|
856
|
+
// src/util/ref.ts
|
|
857
|
+
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
858
|
+
var FEATURE_TAG_CHARSET = "[a-z0-9._-]+";
|
|
859
|
+
var MONOCEROS_FEATURE_RE = new RegExp(
|
|
860
|
+
`^ghcr\\.io/getmonoceros/monoceros-features/(${FEATURE_NAME_CHARSET}):${FEATURE_TAG_CHARSET}$`
|
|
861
|
+
);
|
|
862
|
+
var DEPRECATED_MONOCEROS_FEATURE_RE = new RegExp(
|
|
863
|
+
`^ghcr\\.io/monoceros/features/(${FEATURE_NAME_CHARSET}):(${FEATURE_TAG_CHARSET})$`
|
|
864
|
+
);
|
|
865
|
+
function matchMonocerosFeature(ref) {
|
|
866
|
+
const match = MONOCEROS_FEATURE_RE.exec(ref);
|
|
867
|
+
if (!match) return null;
|
|
868
|
+
return { name: match[1] };
|
|
869
|
+
}
|
|
870
|
+
function migrateDeprecatedFeatureRef(ref) {
|
|
871
|
+
const match = DEPRECATED_MONOCEROS_FEATURE_RE.exec(ref);
|
|
872
|
+
if (!match) return null;
|
|
873
|
+
const name = match[1];
|
|
874
|
+
const tag = match[2];
|
|
875
|
+
return `ghcr.io/getmonoceros/monoceros-features/${name}:${tag}`;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/init/manifest.ts
|
|
879
|
+
function resolveManifestPath(name, checkoutRoot) {
|
|
880
|
+
if (checkoutRoot) {
|
|
881
|
+
const checkoutPath = path3.join(
|
|
882
|
+
checkoutRoot,
|
|
883
|
+
"images",
|
|
884
|
+
"features",
|
|
885
|
+
name,
|
|
886
|
+
"devcontainer-feature.json"
|
|
887
|
+
);
|
|
888
|
+
if (existsSync3(checkoutPath)) return checkoutPath;
|
|
889
|
+
}
|
|
890
|
+
const bundlePath = path3.join(
|
|
891
|
+
bundledFeaturesDir(),
|
|
892
|
+
name,
|
|
893
|
+
"devcontainer-feature.json"
|
|
894
|
+
);
|
|
895
|
+
if (existsSync3(bundlePath)) return bundlePath;
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
899
|
+
const match = matchMonocerosFeature(ref);
|
|
900
|
+
if (!match) return void 0;
|
|
901
|
+
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
902
|
+
if (!manifestPath) return void 0;
|
|
903
|
+
try {
|
|
904
|
+
const text = readFileSync2(manifestPath, "utf8");
|
|
905
|
+
const parsed = JSON.parse(text);
|
|
906
|
+
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
907
|
+
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
908
|
+
(x) => typeof x === "string" && x.length > 0
|
|
909
|
+
) : [];
|
|
910
|
+
const rawNotes = parsed["x-monoceros"]?.usageNotes;
|
|
911
|
+
const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
|
|
912
|
+
(x) => typeof x === "string" && x.length > 0
|
|
913
|
+
) : [];
|
|
914
|
+
const optionDescriptions = {};
|
|
915
|
+
const optionTypes = {};
|
|
916
|
+
const optionNames = [];
|
|
917
|
+
if (parsed.options) {
|
|
918
|
+
for (const [key, opt] of Object.entries(parsed.options)) {
|
|
919
|
+
if (!opt || typeof opt !== "object") continue;
|
|
920
|
+
optionNames.push(key);
|
|
921
|
+
if (typeof opt.description === "string" && opt.description.length > 0) {
|
|
922
|
+
optionDescriptions[key] = opt.description;
|
|
923
|
+
}
|
|
924
|
+
if (opt.type === "boolean") {
|
|
925
|
+
optionTypes[key] = "boolean";
|
|
926
|
+
} else if (opt.type === "string") {
|
|
927
|
+
optionTypes[key] = "string";
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const name = typeof parsed.name === "string" ? parsed.name : "";
|
|
932
|
+
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
933
|
+
const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
|
|
934
|
+
const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
|
|
935
|
+
return {
|
|
936
|
+
name,
|
|
937
|
+
description,
|
|
938
|
+
documentationURL,
|
|
939
|
+
optionHints,
|
|
940
|
+
optionDescriptions,
|
|
941
|
+
optionNames,
|
|
942
|
+
optionTypes,
|
|
943
|
+
usageNotes
|
|
944
|
+
};
|
|
945
|
+
} catch {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
572
950
|
// src/devcontainer/credentials.ts
|
|
573
951
|
import { spawn } from "child_process";
|
|
574
952
|
import { promises as fs2 } from "fs";
|
|
575
|
-
import
|
|
953
|
+
import path4 from "path";
|
|
576
954
|
|
|
577
955
|
// src/util/format.ts
|
|
578
956
|
var ESC = "\x1B[";
|
|
@@ -788,8 +1166,8 @@ function formatCredentialLine(host, username, password) {
|
|
|
788
1166
|
return `https://${encUser}:${encPass}@${host}`;
|
|
789
1167
|
}
|
|
790
1168
|
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
791
|
-
const credsDir =
|
|
792
|
-
const credentialsPath =
|
|
1169
|
+
const credsDir = path4.join(devContainerRoot, ".monoceros");
|
|
1170
|
+
const credentialsPath = path4.join(credsDir, "git-credentials");
|
|
793
1171
|
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
794
1172
|
const approveFn = options.approve ?? realGitCredentialApprove;
|
|
795
1173
|
const logger = options.logger ?? { info: () => {
|
|
@@ -1186,8 +1564,8 @@ ${existing}` : leakedComment;
|
|
|
1186
1564
|
}
|
|
1187
1565
|
|
|
1188
1566
|
// src/init/components.ts
|
|
1189
|
-
import { existsSync as
|
|
1190
|
-
import
|
|
1567
|
+
import { existsSync as existsSync4, promises as fs4 } from "fs";
|
|
1568
|
+
import path5 from "path";
|
|
1191
1569
|
import { z as z3 } from "zod";
|
|
1192
1570
|
import { parse as parseYaml } from "yaml";
|
|
1193
1571
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -1234,7 +1612,7 @@ var ComponentFileSchema = z3.object({
|
|
|
1234
1612
|
}
|
|
1235
1613
|
});
|
|
1236
1614
|
async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
1237
|
-
if (!
|
|
1615
|
+
if (!existsSync4(rootDir)) {
|
|
1238
1616
|
return /* @__PURE__ */ new Map();
|
|
1239
1617
|
}
|
|
1240
1618
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1244,14 +1622,14 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
1244
1622
|
async function walk(baseDir, currentDir, out) {
|
|
1245
1623
|
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
1246
1624
|
for (const entry2 of entries) {
|
|
1247
|
-
const full =
|
|
1625
|
+
const full = path5.join(currentDir, entry2.name);
|
|
1248
1626
|
if (entry2.isDirectory()) {
|
|
1249
1627
|
await walk(baseDir, full, out);
|
|
1250
1628
|
continue;
|
|
1251
1629
|
}
|
|
1252
1630
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
1253
|
-
const relative =
|
|
1254
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
1631
|
+
const relative = path5.relative(baseDir, full);
|
|
1632
|
+
const name = relative.replace(/\.yml$/, "").split(path5.sep).join("/");
|
|
1255
1633
|
const text = await fs4.readFile(full, "utf8");
|
|
1256
1634
|
let raw;
|
|
1257
1635
|
try {
|
|
@@ -1273,42 +1651,6 @@ ${issues}`);
|
|
|
1273
1651
|
out.set(name, { name, sourcePath: full, file: parsed.data });
|
|
1274
1652
|
}
|
|
1275
1653
|
}
|
|
1276
|
-
function mergeComponents(resolved) {
|
|
1277
|
-
const languages = [];
|
|
1278
|
-
const services = [];
|
|
1279
|
-
const featureByRef = /* @__PURE__ */ new Map();
|
|
1280
|
-
for (const entry2 of resolved) {
|
|
1281
|
-
const c = isResolvedComponent(entry2) ? entry2.component : entry2;
|
|
1282
|
-
const version = isResolvedComponent(entry2) ? entry2.version : void 0;
|
|
1283
|
-
const ct = c.file.contributes;
|
|
1284
|
-
for (const lang of ct.languages ?? []) {
|
|
1285
|
-
const value = version !== void 0 ? `${lang}:${version}` : lang;
|
|
1286
|
-
if (!languages.includes(value)) languages.push(value);
|
|
1287
|
-
}
|
|
1288
|
-
for (const svc of ct.services ?? []) {
|
|
1289
|
-
if (!services.includes(svc)) services.push(svc);
|
|
1290
|
-
}
|
|
1291
|
-
for (const f of ct.features ?? []) {
|
|
1292
|
-
const existing = featureByRef.get(f.ref);
|
|
1293
|
-
if (!existing) {
|
|
1294
|
-
featureByRef.set(f.ref, {
|
|
1295
|
-
ref: f.ref,
|
|
1296
|
-
options: { ...f.options ?? {} }
|
|
1297
|
-
});
|
|
1298
|
-
continue;
|
|
1299
|
-
}
|
|
1300
|
-
existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
return {
|
|
1304
|
-
languages,
|
|
1305
|
-
services,
|
|
1306
|
-
features: [...featureByRef.values()]
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
function isResolvedComponent(x) {
|
|
1310
|
-
return "component" in x;
|
|
1311
|
-
}
|
|
1312
1654
|
function mergeFeatureOptions(a, b) {
|
|
1313
1655
|
const result = { ...a };
|
|
1314
1656
|
for (const [key, valueB] of Object.entries(b)) {
|
|
@@ -1321,39 +1663,11 @@ function mergeFeatureOptions(a, b) {
|
|
|
1321
1663
|
}
|
|
1322
1664
|
return result;
|
|
1323
1665
|
}
|
|
1324
|
-
function resolveComponents(catalog, names) {
|
|
1325
|
-
const unknown = [];
|
|
1326
|
-
const out = [];
|
|
1327
|
-
for (const raw of names) {
|
|
1328
|
-
const colon = raw.indexOf(":");
|
|
1329
|
-
const name = colon === -1 ? raw : raw.slice(0, colon);
|
|
1330
|
-
const version = colon === -1 ? void 0 : raw.slice(colon + 1);
|
|
1331
|
-
const c = catalog.get(name);
|
|
1332
|
-
if (!c) {
|
|
1333
|
-
unknown.push(raw);
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (version !== void 0 && c.file.category !== "language") {
|
|
1337
|
-
throw new Error(
|
|
1338
|
-
`Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
|
|
1339
|
-
);
|
|
1340
|
-
}
|
|
1341
|
-
out.push({ component: c, ...version !== void 0 ? { version } : {} });
|
|
1342
|
-
}
|
|
1343
|
-
if (unknown.length > 0) {
|
|
1344
|
-
const available = [...catalog.keys()].sort();
|
|
1345
|
-
throw new Error(
|
|
1346
|
-
`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
1347
|
-
Available: ${available.join(", ")}.`
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
return out;
|
|
1351
|
-
}
|
|
1352
1666
|
|
|
1353
1667
|
// src/proxy/index.ts
|
|
1354
1668
|
import { spawn as spawn3 } from "child_process";
|
|
1355
1669
|
import { promises as fs5 } from "fs";
|
|
1356
|
-
import
|
|
1670
|
+
import path6 from "path";
|
|
1357
1671
|
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
1358
1672
|
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
1359
1673
|
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
@@ -1379,7 +1693,7 @@ var defaultDockerExec = (args) => {
|
|
|
1379
1693
|
};
|
|
1380
1694
|
var realDocker = defaultDockerExec;
|
|
1381
1695
|
function proxyDynamicDir(home) {
|
|
1382
|
-
return
|
|
1696
|
+
return path6.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
1383
1697
|
}
|
|
1384
1698
|
async function ensureProxy(opts = {}) {
|
|
1385
1699
|
const docker = opts.docker ?? realDocker;
|
|
@@ -1471,7 +1785,7 @@ async function maybeStopProxy(opts = {}) {
|
|
|
1471
1785
|
|
|
1472
1786
|
// src/proxy/dynamic.ts
|
|
1473
1787
|
import { promises as fs6 } from "fs";
|
|
1474
|
-
import
|
|
1788
|
+
import path7 from "path";
|
|
1475
1789
|
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
1476
1790
|
if (ports.length === 0) {
|
|
1477
1791
|
throw new Error(
|
|
@@ -1480,12 +1794,12 @@ async function writeDynamicConfig(name, ports, opts = {}) {
|
|
|
1480
1794
|
}
|
|
1481
1795
|
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
1482
1796
|
await fs6.mkdir(dir, { recursive: true });
|
|
1483
|
-
const file =
|
|
1797
|
+
const file = path7.join(dir, `${name}.yml`);
|
|
1484
1798
|
await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
1485
1799
|
return file;
|
|
1486
1800
|
}
|
|
1487
1801
|
async function removeDynamicConfig(name, opts = {}) {
|
|
1488
|
-
const file =
|
|
1802
|
+
const file = path7.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
1489
1803
|
await fs6.rm(file, { force: true });
|
|
1490
1804
|
}
|
|
1491
1805
|
function renderDynamicConfig(name, ports) {
|
|
@@ -1688,34 +2002,97 @@ function knownLanguages() {
|
|
|
1688
2002
|
function knownServices() {
|
|
1689
2003
|
return Object.keys(SERVICE_CATALOG).sort();
|
|
1690
2004
|
}
|
|
2005
|
+
function resolveService(entry2) {
|
|
2006
|
+
return {
|
|
2007
|
+
name: entry2.name,
|
|
2008
|
+
image: entry2.image,
|
|
2009
|
+
...entry2.port !== void 0 ? { port: entry2.port } : {},
|
|
2010
|
+
env: entry2.env ? { ...entry2.env } : {},
|
|
2011
|
+
volumes: entry2.volumes ? [...entry2.volumes] : [],
|
|
2012
|
+
...entry2.healthcheck ? { healthcheck: entry2.healthcheck } : {},
|
|
2013
|
+
...entry2.restart ? { restart: entry2.restart } : {},
|
|
2014
|
+
...entry2.command ? { command: entry2.command } : {}
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function isCuratedService(name) {
|
|
2018
|
+
return Object.prototype.hasOwnProperty.call(SERVICE_CATALOG, name);
|
|
2019
|
+
}
|
|
2020
|
+
function expandCuratedService(name) {
|
|
2021
|
+
const def = SERVICE_CATALOG[name];
|
|
2022
|
+
if (!def) {
|
|
2023
|
+
throw new Error(
|
|
2024
|
+
`Unknown service '${name}'. Known catalog services: ${knownServices().join(", ")}.`
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
name: def.id,
|
|
2029
|
+
image: def.image,
|
|
2030
|
+
port: def.defaultPort,
|
|
2031
|
+
...def.env ? { env: { ...def.env } } : {},
|
|
2032
|
+
...def.dataMount ? { volumes: [`data:${def.dataMount}`] } : {}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function deriveServiceName(image) {
|
|
2036
|
+
const lastSegment = image.split("/").pop() ?? image;
|
|
2037
|
+
const noTag = lastSegment.split("@")[0].split(":")[0];
|
|
2038
|
+
return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
2039
|
+
}
|
|
1691
2040
|
|
|
1692
|
-
// src/
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
);
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (
|
|
1708
|
-
|
|
2041
|
+
// src/init/service-doc.ts
|
|
2042
|
+
function renderServiceObjectBody(svc) {
|
|
2043
|
+
const lines = [`name: ${svc.name}`, `image: ${svc.image}`];
|
|
2044
|
+
if (svc.port !== void 0) lines.push(`port: ${svc.port}`);
|
|
2045
|
+
if (svc.env && Object.keys(svc.env).length > 0) {
|
|
2046
|
+
lines.push("env:");
|
|
2047
|
+
for (const [k, v] of Object.entries(svc.env)) {
|
|
2048
|
+
lines.push(` ${k}: ${v}`);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (svc.volumes && svc.volumes.length > 0) {
|
|
2052
|
+
lines.push("volumes:");
|
|
2053
|
+
for (const vol of svc.volumes) lines.push(` - ${vol}`);
|
|
2054
|
+
}
|
|
2055
|
+
if (svc.restart) lines.push(`restart: ${svc.restart}`);
|
|
2056
|
+
if (svc.command !== void 0) lines.push(`command: ${svc.command}`);
|
|
2057
|
+
if (svc.healthcheck) {
|
|
2058
|
+
lines.push("healthcheck:");
|
|
2059
|
+
const test = svc.healthcheck.test;
|
|
2060
|
+
lines.push(
|
|
2061
|
+
Array.isArray(test) ? ` test: [${test.map((t) => JSON.stringify(t)).join(", ")}]` : ` test: ${test}`
|
|
2062
|
+
);
|
|
2063
|
+
if (svc.healthcheck.interval)
|
|
2064
|
+
lines.push(` interval: ${svc.healthcheck.interval}`);
|
|
2065
|
+
if (svc.healthcheck.timeout)
|
|
2066
|
+
lines.push(` timeout: ${svc.healthcheck.timeout}`);
|
|
2067
|
+
if (svc.healthcheck.retries !== void 0)
|
|
2068
|
+
lines.push(` retries: ${svc.healthcheck.retries}`);
|
|
2069
|
+
if (svc.healthcheck.startPeriod)
|
|
2070
|
+
lines.push(` startPeriod: ${svc.healthcheck.startPeriod}`);
|
|
2071
|
+
}
|
|
2072
|
+
return lines;
|
|
1709
2073
|
}
|
|
1710
|
-
function
|
|
1711
|
-
const
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2074
|
+
function renderCustomService(name, image) {
|
|
2075
|
+
const bodyLines = [`name: ${name}`, `image: ${image}`];
|
|
2076
|
+
const comment = [
|
|
2077
|
+
" port: 8080 # in-container port \u2192 `monoceros tunnel`",
|
|
2078
|
+
" env: # values resolved from <name>.env",
|
|
2079
|
+
" KEY: ${SOME_VAR}",
|
|
2080
|
+
" volumes:",
|
|
2081
|
+
` - data:/data # persistent host bind-mount under data/${name}`,
|
|
2082
|
+
" - rel/host/path:/in/container:ro",
|
|
2083
|
+
" healthcheck:",
|
|
2084
|
+
" test: curl -f http://localhost:8080/health",
|
|
2085
|
+
" restart: unless-stopped"
|
|
2086
|
+
].join("\n");
|
|
2087
|
+
return { bodyLines, comment };
|
|
2088
|
+
}
|
|
2089
|
+
function customServiceHint(name) {
|
|
2090
|
+
return `'${name}' is a custom image \u2014 Monoceros doesn't know its env, ports or volumes. Review the commented block under services[].${name} in the yml and fill in what the image needs.`;
|
|
1716
2091
|
}
|
|
1717
2092
|
|
|
1718
2093
|
// src/create/scaffold.ts
|
|
2094
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, promises as fs7 } from "fs";
|
|
2095
|
+
import path8 from "path";
|
|
1719
2096
|
var APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
|
|
1720
2097
|
var FEATURE_REF_RE2 = /^[a-z0-9.-]+(\/[a-z0-9._-]+)+:[a-z0-9._-]+$/;
|
|
1721
2098
|
var INSTALL_URL_RE2 = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
|
|
@@ -1745,12 +2122,24 @@ function validateOptions(opts) {
|
|
|
1745
2122
|
);
|
|
1746
2123
|
}
|
|
1747
2124
|
}
|
|
2125
|
+
const seenServiceNames = /* @__PURE__ */ new Set();
|
|
1748
2126
|
for (const svc of opts.services) {
|
|
1749
|
-
if (!
|
|
2127
|
+
if (!svc.image) {
|
|
2128
|
+
throw new Error(
|
|
2129
|
+
`Service ${JSON.stringify(svc.name)} has no image. Every service needs an 'image:'.`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
if (svc.name === "workspace") {
|
|
1750
2133
|
throw new Error(
|
|
1751
|
-
`
|
|
2134
|
+
`Invalid service name 'workspace': it collides with the reserved devcontainer workspace service. Pick another name.`
|
|
1752
2135
|
);
|
|
1753
2136
|
}
|
|
2137
|
+
if (seenServiceNames.has(svc.name)) {
|
|
2138
|
+
throw new Error(
|
|
2139
|
+
`Duplicate service name: ${JSON.stringify(svc.name)}. Each services[] entry must have a unique name.`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
seenServiceNames.add(svc.name);
|
|
1754
2143
|
}
|
|
1755
2144
|
for (const pkg of opts.aptPackages ?? []) {
|
|
1756
2145
|
if (!APT_PACKAGE_NAME_RE2.test(pkg)) {
|
|
@@ -1800,10 +2189,14 @@ function validateOptions(opts) {
|
|
|
1800
2189
|
}
|
|
1801
2190
|
function normalizeOptions(opts) {
|
|
1802
2191
|
const languages = [...new Set(opts.languages)].sort();
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2192
|
+
const serviceByName = /* @__PURE__ */ new Map();
|
|
2193
|
+
for (const svc of opts.services) {
|
|
2194
|
+
if (opts.postgresUrl && svc.name === "postgres") continue;
|
|
2195
|
+
serviceByName.set(svc.name, svc);
|
|
1806
2196
|
}
|
|
2197
|
+
const services = [...serviceByName.values()].sort(
|
|
2198
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
2199
|
+
);
|
|
1807
2200
|
const aptPackages = [...new Set(opts.aptPackages ?? [])].sort();
|
|
1808
2201
|
const features = opts.features ? Object.fromEntries(
|
|
1809
2202
|
Object.entries(opts.features).sort(([a], [b]) => a.localeCompare(b))
|
|
@@ -1862,8 +2255,8 @@ function resolveFeatures(opts) {
|
|
|
1862
2255
|
if (match) {
|
|
1863
2256
|
const name = match.name;
|
|
1864
2257
|
const checkout = workbenchCheckoutRoot();
|
|
1865
|
-
const localSourceDir = checkout ?
|
|
1866
|
-
if (localSourceDir &&
|
|
2258
|
+
const localSourceDir = checkout ? path8.join(checkout, "images", "features", name) : null;
|
|
2259
|
+
if (localSourceDir && existsSync5(localSourceDir)) {
|
|
1867
2260
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
1868
2261
|
resolved.push({
|
|
1869
2262
|
devcontainerKey: `./features/${name}`,
|
|
@@ -1887,9 +2280,9 @@ function resolveFeatures(opts) {
|
|
|
1887
2280
|
return resolved;
|
|
1888
2281
|
}
|
|
1889
2282
|
function readPersistentHomeEntries(localSourceDir) {
|
|
1890
|
-
const manifestPath =
|
|
2283
|
+
const manifestPath = path8.join(localSourceDir, "devcontainer-feature.json");
|
|
1891
2284
|
try {
|
|
1892
|
-
const text =
|
|
2285
|
+
const text = readFileSync3(manifestPath, "utf8");
|
|
1893
2286
|
const parsed = JSON.parse(text);
|
|
1894
2287
|
return {
|
|
1895
2288
|
paths: filterSubpaths(parsed["x-monoceros"]?.persistentHomePaths),
|
|
@@ -1964,7 +2357,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
1964
2357
|
name: opts.name,
|
|
1965
2358
|
dockerComposeFile: "compose.yaml",
|
|
1966
2359
|
service: "workspace",
|
|
1967
|
-
...opts.services.length > 0 ? { runServices: opts.services } : {},
|
|
2360
|
+
...opts.services.length > 0 ? { runServices: opts.services.map((s) => s.name) } : {},
|
|
1968
2361
|
workspaceFolder: `/workspaces/${opts.name}`,
|
|
1969
2362
|
remoteUser: "node",
|
|
1970
2363
|
forwardPorts: ports,
|
|
@@ -1997,6 +2390,18 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
1997
2390
|
...customizationsField ?? {}
|
|
1998
2391
|
};
|
|
1999
2392
|
}
|
|
2393
|
+
function composeScalar(value) {
|
|
2394
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
2395
|
+
return `"${escaped}"`;
|
|
2396
|
+
}
|
|
2397
|
+
function composeVolumeSource(spec, serviceName) {
|
|
2398
|
+
const parts = spec.split(":");
|
|
2399
|
+
const src = parts[0];
|
|
2400
|
+
const rest = parts.slice(1).join(":");
|
|
2401
|
+
if (src === "data") return `../data/${serviceName}:${rest}`;
|
|
2402
|
+
const relative = src.startsWith("./") ? src.slice(2) : src;
|
|
2403
|
+
return `../${relative}:${rest}`;
|
|
2404
|
+
}
|
|
2000
2405
|
function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
2001
2406
|
void dockerMode;
|
|
2002
2407
|
const hasPorts = (opts.ports?.length ?? 0) > 0;
|
|
@@ -2025,20 +2430,42 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
|
2025
2430
|
lines.push(` - ../home/${sub}:/home/node/${sub}`);
|
|
2026
2431
|
}
|
|
2027
2432
|
}
|
|
2028
|
-
for (const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2433
|
+
for (const svc of opts.services) {
|
|
2434
|
+
lines.push(` ${svc.name}:`);
|
|
2435
|
+
lines.push(` image: ${svc.image}`);
|
|
2436
|
+
if (svc.restart) {
|
|
2437
|
+
lines.push(` restart: ${svc.restart}`);
|
|
2438
|
+
}
|
|
2439
|
+
if (svc.command !== void 0) {
|
|
2440
|
+
lines.push(` command: ${composeScalar(svc.command)}`);
|
|
2441
|
+
}
|
|
2442
|
+
const envKeys = Object.keys(svc.env);
|
|
2443
|
+
if (envKeys.length > 0) {
|
|
2034
2444
|
lines.push(" environment:");
|
|
2035
|
-
for (const
|
|
2036
|
-
lines.push(` ${k}: ${
|
|
2445
|
+
for (const k of envKeys) {
|
|
2446
|
+
lines.push(` ${k}: ${composeScalar(svc.env[k])}`);
|
|
2037
2447
|
}
|
|
2038
2448
|
}
|
|
2039
|
-
if (
|
|
2449
|
+
if (svc.volumes.length > 0) {
|
|
2040
2450
|
lines.push(" volumes:");
|
|
2041
|
-
|
|
2451
|
+
for (const vol of svc.volumes) {
|
|
2452
|
+
lines.push(` - ${composeVolumeSource(vol, svc.name)}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (svc.healthcheck) {
|
|
2456
|
+
const hc = svc.healthcheck;
|
|
2457
|
+
lines.push(" healthcheck:");
|
|
2458
|
+
if (Array.isArray(hc.test)) {
|
|
2459
|
+
lines.push(` test: [${hc.test.map(composeScalar).join(", ")}]`);
|
|
2460
|
+
} else {
|
|
2461
|
+
lines.push(` test: ${composeScalar(hc.test)}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (hc.interval) lines.push(` interval: ${hc.interval}`);
|
|
2464
|
+
if (hc.timeout) lines.push(` timeout: ${hc.timeout}`);
|
|
2465
|
+
if (hc.retries !== void 0) lines.push(` retries: ${hc.retries}`);
|
|
2466
|
+
if (hc.startPeriod) {
|
|
2467
|
+
lines.push(` start_period: ${hc.startPeriod}`);
|
|
2468
|
+
}
|
|
2042
2469
|
}
|
|
2043
2470
|
}
|
|
2044
2471
|
if (hasPorts) {
|
|
@@ -2172,245 +2599,99 @@ function buildPostCreateScript(opts) {
|
|
|
2172
2599
|
return lines.join("\n") + "\n";
|
|
2173
2600
|
}
|
|
2174
2601
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
2175
|
-
const dest =
|
|
2602
|
+
const dest = path8.join(devcontainerDir, "post-create.sh");
|
|
2176
2603
|
await fs7.writeFile(dest, buildPostCreateScript(opts));
|
|
2177
2604
|
await fs7.chmod(dest, 493);
|
|
2178
2605
|
}
|
|
2179
2606
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
2180
2607
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
2181
|
-
const devcontainerDir =
|
|
2182
|
-
const monocerosDir =
|
|
2183
|
-
const projectsDir =
|
|
2184
|
-
const homeDir =
|
|
2185
|
-
const dataDir =
|
|
2608
|
+
const devcontainerDir = path8.join(targetDir, ".devcontainer");
|
|
2609
|
+
const monocerosDir = path8.join(targetDir, ".monoceros");
|
|
2610
|
+
const projectsDir = path8.join(targetDir, "projects");
|
|
2611
|
+
const homeDir = path8.join(targetDir, "home");
|
|
2612
|
+
const dataDir = path8.join(targetDir, "data");
|
|
2186
2613
|
await fs7.mkdir(devcontainerDir, { recursive: true });
|
|
2187
2614
|
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2188
2615
|
await fs7.mkdir(projectsDir, { recursive: true });
|
|
2189
2616
|
await fs7.mkdir(homeDir, { recursive: true });
|
|
2190
2617
|
if (needsCompose(opts)) {
|
|
2191
2618
|
await fs7.mkdir(dataDir, { recursive: true });
|
|
2192
|
-
for (const
|
|
2193
|
-
const
|
|
2194
|
-
if (
|
|
2195
|
-
await fs7.mkdir(
|
|
2619
|
+
for (const svc of opts.services) {
|
|
2620
|
+
const hasDataVolume = svc.volumes.some((v) => v.split(":")[0] === "data");
|
|
2621
|
+
if (hasDataVolume) {
|
|
2622
|
+
await fs7.mkdir(path8.join(dataDir, svc.name), { recursive: true });
|
|
2196
2623
|
}
|
|
2197
2624
|
}
|
|
2198
2625
|
}
|
|
2199
|
-
const containerGitignore =
|
|
2626
|
+
const containerGitignore = path8.join(targetDir, ".gitignore");
|
|
2200
2627
|
await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
2201
|
-
const gitkeep =
|
|
2202
|
-
if (!
|
|
2628
|
+
const gitkeep = path8.join(projectsDir, ".gitkeep");
|
|
2629
|
+
if (!existsSync5(gitkeep)) {
|
|
2203
2630
|
await fs7.writeFile(gitkeep, "");
|
|
2204
2631
|
}
|
|
2205
2632
|
await fs7.writeFile(
|
|
2206
|
-
|
|
2633
|
+
path8.join(monocerosDir, ".gitignore"),
|
|
2207
2634
|
"git-credentials*\ngitconfig\n"
|
|
2208
2635
|
);
|
|
2209
2636
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
2210
2637
|
await fs7.writeFile(
|
|
2211
|
-
|
|
2638
|
+
path8.join(devcontainerDir, "devcontainer.json"),
|
|
2212
2639
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
2213
2640
|
);
|
|
2214
|
-
const featuresDir =
|
|
2215
|
-
if (
|
|
2641
|
+
const featuresDir = path8.join(devcontainerDir, "features");
|
|
2642
|
+
if (existsSync5(featuresDir)) {
|
|
2216
2643
|
await fs7.rm(featuresDir, { recursive: true, force: true });
|
|
2217
2644
|
}
|
|
2218
2645
|
const resolvedFeatures = resolveFeatures(opts);
|
|
2219
2646
|
for (const f of resolvedFeatures) {
|
|
2220
2647
|
if (!f.localSourceDir || !f.localName) continue;
|
|
2221
|
-
const dest =
|
|
2222
|
-
await fs7.mkdir(dest, { recursive: true });
|
|
2223
|
-
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2224
|
-
}
|
|
2225
|
-
for (const f of resolvedFeatures) {
|
|
2226
|
-
for (const sub of f.persistentHomePaths) {
|
|
2227
|
-
await fs7.mkdir(
|
|
2228
|
-
}
|
|
2229
|
-
for (const entry2 of f.persistentHomeFiles) {
|
|
2230
|
-
const filePath = path6.join(homeDir, entry2.path);
|
|
2231
|
-
await fs7.mkdir(path6.dirname(filePath), { recursive: true });
|
|
2232
|
-
if (!existsSync3(filePath)) {
|
|
2233
|
-
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
await writePostCreateScript(devcontainerDir, opts);
|
|
2238
|
-
const composePath = path6.join(devcontainerDir, "compose.yaml");
|
|
2239
|
-
if (needsCompose(opts)) {
|
|
2240
|
-
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2241
|
-
} else if (existsSync3(composePath)) {
|
|
2242
|
-
await fs7.rm(composePath);
|
|
2243
|
-
}
|
|
2244
|
-
const workspacePath = path6.join(targetDir, `${opts.name}.code-workspace`);
|
|
2245
|
-
let existingWorkspace;
|
|
2246
|
-
try {
|
|
2247
|
-
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
2248
|
-
existingWorkspace = JSON.parse(raw);
|
|
2249
|
-
} catch {
|
|
2250
|
-
existingWorkspace = void 0;
|
|
2251
|
-
}
|
|
2252
|
-
const generated = buildCodeWorkspaceJson(opts);
|
|
2253
|
-
const merged = mergeCodeWorkspace(existingWorkspace, generated);
|
|
2254
|
-
await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
// src/modify/yml.ts
|
|
2258
|
-
import {
|
|
2259
|
-
isMap as isMap2,
|
|
2260
|
-
isScalar,
|
|
2261
|
-
isSeq,
|
|
2262
|
-
Pair as Pair2,
|
|
2263
|
-
Scalar as Scalar2,
|
|
2264
|
-
YAMLMap as YAMLMap2,
|
|
2265
|
-
YAMLSeq
|
|
2266
|
-
} from "yaml";
|
|
2267
|
-
|
|
2268
|
-
// src/init/manifest.ts
|
|
2269
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
2270
|
-
import path7 from "path";
|
|
2271
|
-
function resolveManifestPath(name, checkoutRoot) {
|
|
2272
|
-
if (checkoutRoot) {
|
|
2273
|
-
const checkoutPath = path7.join(
|
|
2274
|
-
checkoutRoot,
|
|
2275
|
-
"images",
|
|
2276
|
-
"features",
|
|
2277
|
-
name,
|
|
2278
|
-
"devcontainer-feature.json"
|
|
2279
|
-
);
|
|
2280
|
-
if (existsSync4(checkoutPath)) return checkoutPath;
|
|
2281
|
-
}
|
|
2282
|
-
const bundlePath = path7.join(
|
|
2283
|
-
bundledFeaturesDir(),
|
|
2284
|
-
name,
|
|
2285
|
-
"devcontainer-feature.json"
|
|
2286
|
-
);
|
|
2287
|
-
if (existsSync4(bundlePath)) return bundlePath;
|
|
2288
|
-
return null;
|
|
2289
|
-
}
|
|
2290
|
-
function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
|
|
2291
|
-
const match = matchMonocerosFeature(ref);
|
|
2292
|
-
if (!match) return void 0;
|
|
2293
|
-
const manifestPath = resolveManifestPath(match.name, checkoutRoot);
|
|
2294
|
-
if (!manifestPath) return void 0;
|
|
2295
|
-
try {
|
|
2296
|
-
const text = readFileSync2(manifestPath, "utf8");
|
|
2297
|
-
const parsed = JSON.parse(text);
|
|
2298
|
-
const rawHints = parsed["x-monoceros"]?.optionHints;
|
|
2299
|
-
const optionHints = Array.isArray(rawHints) ? rawHints.filter(
|
|
2300
|
-
(x) => typeof x === "string" && x.length > 0
|
|
2301
|
-
) : [];
|
|
2302
|
-
const rawNotes = parsed["x-monoceros"]?.usageNotes;
|
|
2303
|
-
const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
|
|
2304
|
-
(x) => typeof x === "string" && x.length > 0
|
|
2305
|
-
) : [];
|
|
2306
|
-
const optionDescriptions = {};
|
|
2307
|
-
const optionTypes = {};
|
|
2308
|
-
const optionNames = [];
|
|
2309
|
-
if (parsed.options) {
|
|
2310
|
-
for (const [key, opt] of Object.entries(parsed.options)) {
|
|
2311
|
-
if (!opt || typeof opt !== "object") continue;
|
|
2312
|
-
optionNames.push(key);
|
|
2313
|
-
if (typeof opt.description === "string" && opt.description.length > 0) {
|
|
2314
|
-
optionDescriptions[key] = opt.description;
|
|
2315
|
-
}
|
|
2316
|
-
if (opt.type === "boolean") {
|
|
2317
|
-
optionTypes[key] = "boolean";
|
|
2318
|
-
} else if (opt.type === "string") {
|
|
2319
|
-
optionTypes[key] = "string";
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
}
|
|
2323
|
-
const name = typeof parsed.name === "string" ? parsed.name : "";
|
|
2324
|
-
const description = typeof parsed.description === "string" ? parsed.description : "";
|
|
2325
|
-
const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
|
|
2326
|
-
const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
|
|
2327
|
-
return {
|
|
2328
|
-
name,
|
|
2329
|
-
description,
|
|
2330
|
-
documentationURL,
|
|
2331
|
-
optionHints,
|
|
2332
|
-
optionDescriptions,
|
|
2333
|
-
optionNames,
|
|
2334
|
-
optionTypes,
|
|
2335
|
-
usageNotes
|
|
2336
|
-
};
|
|
2337
|
-
} catch {
|
|
2338
|
-
return void 0;
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
// src/init/feature-doc.ts
|
|
2343
|
-
function buildFeatureHeaderLines(summary, width) {
|
|
2344
|
-
const paragraphs = buildHeaderParagraphs(summary);
|
|
2345
|
-
const wrapped = [];
|
|
2346
|
-
for (const para of paragraphs) {
|
|
2347
|
-
for (const line of wrapToComment(para, width)) {
|
|
2348
|
-
wrapped.push(line);
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
return wrapped;
|
|
2352
|
-
}
|
|
2353
|
-
function buildFeatureHeaderCommentBefore(summary, width) {
|
|
2354
|
-
const lines = buildFeatureHeaderLines(summary, width);
|
|
2355
|
-
return lines.map((l) => ` ${l}`).join("\n");
|
|
2356
|
-
}
|
|
2357
|
-
function buildHeaderParagraphs(summary) {
|
|
2358
|
-
if (!summary) return [];
|
|
2359
|
-
const out = [];
|
|
2360
|
-
const tagline = summary.name?.trim();
|
|
2361
|
-
const description = summary.description?.trim();
|
|
2362
|
-
if (tagline && description) {
|
|
2363
|
-
out.push(`${tagline} \u2014 ${description}`);
|
|
2364
|
-
} else if (tagline) {
|
|
2365
|
-
out.push(tagline);
|
|
2366
|
-
} else if (description) {
|
|
2367
|
-
out.push(description);
|
|
2368
|
-
}
|
|
2369
|
-
for (const note of summary.usageNotes) {
|
|
2370
|
-
const trimmed = note.trim();
|
|
2371
|
-
if (trimmed.length > 0) out.push(trimmed);
|
|
2372
|
-
}
|
|
2373
|
-
if (summary.optionHints.length > 0) {
|
|
2374
|
-
const parts = summary.optionHints.map((key) => {
|
|
2375
|
-
const desc = summary.optionDescriptions[key];
|
|
2376
|
-
const short = desc ? shortenOptionDescription(desc) : void 0;
|
|
2377
|
-
return short ? `${key} (${short})` : key;
|
|
2378
|
-
});
|
|
2379
|
-
out.push(`Options: ${parts.join(", ")}.`);
|
|
2380
|
-
}
|
|
2381
|
-
if (summary.documentationURL) {
|
|
2382
|
-
out.push(`See ${summary.documentationURL} for further information.`);
|
|
2383
|
-
}
|
|
2384
|
-
return out;
|
|
2385
|
-
}
|
|
2386
|
-
function shortenOptionDescription(desc) {
|
|
2387
|
-
const firstSentence = desc.split(/(?<=[.!?])\s+/)[0]?.trim() ?? desc.trim();
|
|
2388
|
-
return firstSentence.replace(/[.!?]+$/, "").trim();
|
|
2389
|
-
}
|
|
2390
|
-
function wrapToComment(text, width) {
|
|
2391
|
-
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
2392
|
-
if (words.length === 0) return [""];
|
|
2393
|
-
const usable = Math.max(width, 20);
|
|
2394
|
-
const lines = [];
|
|
2395
|
-
let current = "";
|
|
2396
|
-
for (const w of words) {
|
|
2397
|
-
if (current.length === 0) {
|
|
2398
|
-
current = w;
|
|
2399
|
-
continue;
|
|
2648
|
+
const dest = path8.join(featuresDir, f.localName);
|
|
2649
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
2650
|
+
await fs7.cp(f.localSourceDir, dest, { recursive: true });
|
|
2651
|
+
}
|
|
2652
|
+
for (const f of resolvedFeatures) {
|
|
2653
|
+
for (const sub of f.persistentHomePaths) {
|
|
2654
|
+
await fs7.mkdir(path8.join(homeDir, sub), { recursive: true });
|
|
2400
2655
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2656
|
+
for (const entry2 of f.persistentHomeFiles) {
|
|
2657
|
+
const filePath = path8.join(homeDir, entry2.path);
|
|
2658
|
+
await fs7.mkdir(path8.dirname(filePath), { recursive: true });
|
|
2659
|
+
if (!existsSync5(filePath)) {
|
|
2660
|
+
await fs7.writeFile(filePath, entry2.initialContent);
|
|
2661
|
+
}
|
|
2406
2662
|
}
|
|
2407
2663
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2664
|
+
await writePostCreateScript(devcontainerDir, opts);
|
|
2665
|
+
const composePath = path8.join(devcontainerDir, "compose.yaml");
|
|
2666
|
+
if (needsCompose(opts)) {
|
|
2667
|
+
await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
2668
|
+
} else if (existsSync5(composePath)) {
|
|
2669
|
+
await fs7.rm(composePath);
|
|
2670
|
+
}
|
|
2671
|
+
const workspacePath = path8.join(targetDir, `${opts.name}.code-workspace`);
|
|
2672
|
+
let existingWorkspace;
|
|
2673
|
+
try {
|
|
2674
|
+
const raw = await fs7.readFile(workspacePath, "utf8");
|
|
2675
|
+
existingWorkspace = JSON.parse(raw);
|
|
2676
|
+
} catch {
|
|
2677
|
+
existingWorkspace = void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const generated = buildCodeWorkspaceJson(opts);
|
|
2680
|
+
const merged = mergeCodeWorkspace(existingWorkspace, generated);
|
|
2681
|
+
await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
|
|
2410
2682
|
}
|
|
2411
|
-
var FEATURE_HEADER_WIDTH = 76 - 2;
|
|
2412
2683
|
|
|
2413
2684
|
// src/modify/yml.ts
|
|
2685
|
+
import {
|
|
2686
|
+
isMap as isMap2,
|
|
2687
|
+
isScalar,
|
|
2688
|
+
isSeq,
|
|
2689
|
+
Pair as Pair2,
|
|
2690
|
+
parseDocument as parseDocument3,
|
|
2691
|
+
Scalar as Scalar2,
|
|
2692
|
+
YAMLMap as YAMLMap2,
|
|
2693
|
+
YAMLSeq
|
|
2694
|
+
} from "yaml";
|
|
2414
2695
|
function ensureSeq(doc, key) {
|
|
2415
2696
|
const existing = doc.get(key, true);
|
|
2416
2697
|
if (existing && isSeq(existing)) return existing;
|
|
@@ -2433,11 +2714,24 @@ function addLanguageToDoc(doc, lang) {
|
|
|
2433
2714
|
seq.add(lang);
|
|
2434
2715
|
return true;
|
|
2435
2716
|
}
|
|
2436
|
-
function
|
|
2717
|
+
function findServiceItem(seq, name) {
|
|
2718
|
+
for (const item of seq.items) {
|
|
2719
|
+
if (isMap2(item) && item.get("name") === name) return item;
|
|
2720
|
+
}
|
|
2721
|
+
return void 0;
|
|
2722
|
+
}
|
|
2723
|
+
function addServiceEntryToDoc(doc, name, image, bodyLines, scaffoldComment) {
|
|
2437
2724
|
const seq = ensureSeq(doc, "services");
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2725
|
+
const existing = findServiceItem(seq, name);
|
|
2726
|
+
if (existing) {
|
|
2727
|
+
const existingImage = existing.get("image");
|
|
2728
|
+
if (existingImage === image) return { outcome: "exists" };
|
|
2729
|
+
return { outcome: "conflict", existingImage: String(existingImage) };
|
|
2730
|
+
}
|
|
2731
|
+
const node = parseDocument3(bodyLines.join("\n")).contents;
|
|
2732
|
+
if (scaffoldComment) node.comment = scaffoldComment;
|
|
2733
|
+
seq.add(node);
|
|
2734
|
+
return { outcome: "added" };
|
|
2441
2735
|
}
|
|
2442
2736
|
function addAptPackagesToDoc(doc, packages) {
|
|
2443
2737
|
const seq = ensureSeq(doc, "aptPackages");
|
|
@@ -2657,6 +2951,12 @@ function addFeatureToDoc(doc, ref, options = {}, displayName) {
|
|
|
2657
2951
|
entry2.commentBefore = headerBefore;
|
|
2658
2952
|
entry2.spaceBefore = true;
|
|
2659
2953
|
}
|
|
2954
|
+
const hints = featureOptionHints(summary, ref, Object.keys(options));
|
|
2955
|
+
if (hints.length > 0) {
|
|
2956
|
+
const commentLines = [" options:"];
|
|
2957
|
+
for (const h of hints) commentLines.push(` ${h.key}: ${h.placeholder}`);
|
|
2958
|
+
entry2.comment = commentLines.join("\n");
|
|
2959
|
+
}
|
|
2660
2960
|
seq.add(entry2);
|
|
2661
2961
|
return true;
|
|
2662
2962
|
}
|
|
@@ -2733,7 +3033,15 @@ function removeLanguageFromDoc(doc, lang) {
|
|
|
2733
3033
|
return removeScalarFromSeq(doc, "languages", lang);
|
|
2734
3034
|
}
|
|
2735
3035
|
function removeServiceFromDoc(doc, service) {
|
|
2736
|
-
|
|
3036
|
+
const node = doc.get("services", true);
|
|
3037
|
+
if (!node || !isSeq(node)) return false;
|
|
3038
|
+
const idx = node.items.findIndex(
|
|
3039
|
+
(i) => isMap2(i) && i.get("name") === service
|
|
3040
|
+
);
|
|
3041
|
+
if (idx === -1) return false;
|
|
3042
|
+
node.items.splice(idx, 1);
|
|
3043
|
+
pruneEmptySeq(doc, "services");
|
|
3044
|
+
return true;
|
|
2737
3045
|
}
|
|
2738
3046
|
function removeAptPackageFromDoc(doc, pkg) {
|
|
2739
3047
|
return removeScalarFromSeq(doc, "aptPackages", pkg);
|
|
@@ -2773,8 +3081,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
2773
3081
|
if (!isMap2(item)) return false;
|
|
2774
3082
|
const url = item.get("url");
|
|
2775
3083
|
if (url === urlOrPath) return true;
|
|
2776
|
-
const
|
|
2777
|
-
const effectivePath = typeof
|
|
3084
|
+
const path21 = item.get("path");
|
|
3085
|
+
const effectivePath = typeof path21 === "string" ? path21 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
2778
3086
|
return effectivePath === urlOrPath;
|
|
2779
3087
|
});
|
|
2780
3088
|
if (idx < 0) return false;
|
|
@@ -2801,13 +3109,38 @@ function runAddLanguage(input) {
|
|
|
2801
3109
|
}
|
|
2802
3110
|
return mutate(input, (doc) => addLanguageToDoc(doc, input.language));
|
|
2803
3111
|
}
|
|
2804
|
-
function runAddService(input) {
|
|
2805
|
-
|
|
3112
|
+
async function runAddService(input) {
|
|
3113
|
+
const arg = input.service;
|
|
3114
|
+
const curated = isCuratedService(arg);
|
|
3115
|
+
if (input.as !== void 0 && !/^[a-z0-9][a-z0-9_-]*$/.test(input.as)) {
|
|
2806
3116
|
throw new Error(
|
|
2807
|
-
`
|
|
3117
|
+
`Invalid --as name ${JSON.stringify(input.as)}. Use lowercase letters, digits, '_' or '-' (must start with a letter or digit).`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const name = input.as ?? (curated ? arg : deriveServiceName(arg));
|
|
3121
|
+
const image = curated ? expandCuratedService(arg).image : arg;
|
|
3122
|
+
const custom = curated ? null : renderCustomService(name, arg);
|
|
3123
|
+
const bodyLines = curated ? renderServiceObjectBody({ ...expandCuratedService(arg), name }) : custom.bodyLines;
|
|
3124
|
+
const scaffoldComment = curated ? void 0 : custom.comment;
|
|
3125
|
+
const result = await mutate(input, (doc) => {
|
|
3126
|
+
const r = addServiceEntryToDoc(
|
|
3127
|
+
doc,
|
|
3128
|
+
name,
|
|
3129
|
+
image,
|
|
3130
|
+
bodyLines,
|
|
3131
|
+
scaffoldComment
|
|
2808
3132
|
);
|
|
3133
|
+
if (r.outcome === "conflict") {
|
|
3134
|
+
throw new Error(
|
|
3135
|
+
`A service named '${name}' already exists with a different image (${r.existingImage}). Add it under a different name with \`--as <name>\`, or remove the existing one first (\`monoceros remove-service ${input.name} ${name}\`).`
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
return r.outcome === "added";
|
|
3139
|
+
});
|
|
3140
|
+
if (result.status === "updated" && !curated) {
|
|
3141
|
+
(input.logger ?? defaultLogger()).info(customServiceHint(name));
|
|
2809
3142
|
}
|
|
2810
|
-
return
|
|
3143
|
+
return result;
|
|
2811
3144
|
}
|
|
2812
3145
|
function runAddAptPackages(input) {
|
|
2813
3146
|
if (input.packages.length === 0) {
|
|
@@ -2824,7 +3157,7 @@ async function runAddRepo(input) {
|
|
|
2824
3157
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
2825
3158
|
);
|
|
2826
3159
|
}
|
|
2827
|
-
const
|
|
3160
|
+
const path21 = (input.path ?? deriveRepoName(url)).trim();
|
|
2828
3161
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
2829
3162
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
2830
3163
|
if (hasName !== hasEmail) {
|
|
@@ -2853,7 +3186,7 @@ async function runAddRepo(input) {
|
|
|
2853
3186
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
2854
3187
|
const entry2 = {
|
|
2855
3188
|
url,
|
|
2856
|
-
path:
|
|
3189
|
+
path: path21,
|
|
2857
3190
|
...hasName && hasEmail ? {
|
|
2858
3191
|
gitUser: {
|
|
2859
3192
|
name: input.gitName.trim(),
|
|
@@ -2969,7 +3302,7 @@ async function tryCloneInRunningContainer(input, entry2) {
|
|
|
2969
3302
|
logger.info(
|
|
2970
3303
|
`Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
|
|
2971
3304
|
);
|
|
2972
|
-
void
|
|
3305
|
+
void path9;
|
|
2973
3306
|
}
|
|
2974
3307
|
function shquote(value) {
|
|
2975
3308
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -3046,10 +3379,33 @@ async function runAddFeature(input) {
|
|
|
3046
3379
|
...resolved.defaultOptions,
|
|
3047
3380
|
...input.options ?? {}
|
|
3048
3381
|
};
|
|
3049
|
-
|
|
3382
|
+
const result = await mutate(
|
|
3050
3383
|
input,
|
|
3051
3384
|
(doc) => addFeatureToDoc(doc, resolved.ref, merged, raw)
|
|
3052
3385
|
);
|
|
3386
|
+
if (result.status === "updated") {
|
|
3387
|
+
const summary = loadFeatureManifestSummary(resolved.ref);
|
|
3388
|
+
const vars = featureOptionHints(
|
|
3389
|
+
summary,
|
|
3390
|
+
resolved.ref,
|
|
3391
|
+
Object.keys(merged)
|
|
3392
|
+
).map((h) => h.envVar);
|
|
3393
|
+
if (vars.length > 0) {
|
|
3394
|
+
const home = input.monocerosHome ?? monocerosHome();
|
|
3395
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
3396
|
+
const seeded = await ensureEnvVars(
|
|
3397
|
+
containerEnvPath(input.name, home),
|
|
3398
|
+
input.name,
|
|
3399
|
+
vars
|
|
3400
|
+
);
|
|
3401
|
+
if (seeded.added.length > 0) {
|
|
3402
|
+
(input.logger ?? defaultLogger()).info(
|
|
3403
|
+
`Seeded ${seeded.added.join(", ")} into ${input.name}.env \u2014 fill in the values.`
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return result;
|
|
3053
3409
|
}
|
|
3054
3410
|
async function resolveFeatureRefOrShortname(input) {
|
|
3055
3411
|
if (REGEX.featureRef.test(input)) {
|
|
@@ -3609,7 +3965,7 @@ var addServiceCommand = defineCommand7({
|
|
|
3609
3965
|
meta: {
|
|
3610
3966
|
name: "add-service",
|
|
3611
3967
|
group: "edit",
|
|
3612
|
-
description: "Add a
|
|
3968
|
+
description: "Add a backing service to the container config. A curated name (postgres, mysql, redis) expands to a full editable block; any other image (e.g. rustfs/rustfs:latest) drops in name + image plus a commented scaffold. Idempotent, prints a diff before writing."
|
|
3613
3969
|
},
|
|
3614
3970
|
args: {
|
|
3615
3971
|
name: {
|
|
@@ -3619,9 +3975,13 @@ var addServiceCommand = defineCommand7({
|
|
|
3619
3975
|
},
|
|
3620
3976
|
service: {
|
|
3621
3977
|
type: "positional",
|
|
3622
|
-
description: "
|
|
3978
|
+
description: "Curated name (postgres, mysql, redis) or any image ref (e.g. rustfs/rustfs:latest).",
|
|
3623
3979
|
required: true
|
|
3624
3980
|
},
|
|
3981
|
+
as: {
|
|
3982
|
+
type: "string",
|
|
3983
|
+
description: "Override the service name (the compose service / DNS name / data dir). Lets you add the same image more than once \u2014 e.g. two postgres servers as postgres-app and postgres-analytics."
|
|
3984
|
+
},
|
|
3625
3985
|
yes: {
|
|
3626
3986
|
type: "boolean",
|
|
3627
3987
|
description: "Skip the interactive confirmation and apply the diff.",
|
|
@@ -3634,6 +3994,7 @@ var addServiceCommand = defineCommand7({
|
|
|
3634
3994
|
const result = await runAddService({
|
|
3635
3995
|
name: args.name,
|
|
3636
3996
|
service: args.service,
|
|
3997
|
+
...args.as ? { as: args.as } : {},
|
|
3637
3998
|
yes: args.yes
|
|
3638
3999
|
});
|
|
3639
4000
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
@@ -3648,12 +4009,12 @@ var addServiceCommand = defineCommand7({
|
|
|
3648
4009
|
import { defineCommand as defineCommand8 } from "citty";
|
|
3649
4010
|
|
|
3650
4011
|
// src/apply/index.ts
|
|
3651
|
-
import { existsSync as
|
|
4012
|
+
import { existsSync as existsSync8, promises as fs12 } from "fs";
|
|
3652
4013
|
import { consola as consola11 } from "consola";
|
|
3653
4014
|
|
|
3654
4015
|
// src/config/state.ts
|
|
3655
4016
|
import { promises as fs9 } from "fs";
|
|
3656
|
-
import
|
|
4017
|
+
import path10 from "path";
|
|
3657
4018
|
function buildStateFile(opts) {
|
|
3658
4019
|
return {
|
|
3659
4020
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
@@ -3663,7 +4024,7 @@ function buildStateFile(opts) {
|
|
|
3663
4024
|
};
|
|
3664
4025
|
}
|
|
3665
4026
|
function stateFilePath(targetDir) {
|
|
3666
|
-
return
|
|
4027
|
+
return path10.join(targetDir, ".monoceros", "state.json");
|
|
3667
4028
|
}
|
|
3668
4029
|
async function readStateFile(targetDir) {
|
|
3669
4030
|
try {
|
|
@@ -3674,7 +4035,7 @@ async function readStateFile(targetDir) {
|
|
|
3674
4035
|
}
|
|
3675
4036
|
}
|
|
3676
4037
|
async function writeStateFile(targetDir, state) {
|
|
3677
|
-
const monocerosDir =
|
|
4038
|
+
const monocerosDir = path10.join(targetDir, ".monoceros");
|
|
3678
4039
|
await fs9.mkdir(monocerosDir, { recursive: true });
|
|
3679
4040
|
await fs9.writeFile(
|
|
3680
4041
|
stateFilePath(targetDir),
|
|
@@ -3695,7 +4056,11 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
|
3695
4056
|
const result = {
|
|
3696
4057
|
name: config.name,
|
|
3697
4058
|
languages: [...config.languages],
|
|
3698
|
-
services
|
|
4059
|
+
// Normalize every services[] entry (curated string or explicit
|
|
4060
|
+
// object) to the canonical ResolvedService shape. `${VAR}` values
|
|
4061
|
+
// survive untouched here — apply interpolates them against
|
|
4062
|
+
// <name>.env afterwards.
|
|
4063
|
+
services: config.services.map(resolveService)
|
|
3699
4064
|
};
|
|
3700
4065
|
if (config.externalServices.postgres !== void 0) {
|
|
3701
4066
|
result.postgresUrl = config.externalServices.postgres;
|
|
@@ -3746,8 +4111,8 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
|
3746
4111
|
|
|
3747
4112
|
// src/devcontainer/compose.ts
|
|
3748
4113
|
import { spawn as spawn5 } from "child_process";
|
|
3749
|
-
import { existsSync as
|
|
3750
|
-
import
|
|
4114
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4115
|
+
import path12 from "path";
|
|
3751
4116
|
import { consola as consola9 } from "consola";
|
|
3752
4117
|
|
|
3753
4118
|
// src/util/mask-secrets.ts
|
|
@@ -3808,9 +4173,9 @@ function createSecretMaskStream() {
|
|
|
3808
4173
|
|
|
3809
4174
|
// src/devcontainer/cli.ts
|
|
3810
4175
|
import { spawn as spawn4 } from "child_process";
|
|
3811
|
-
import { readFileSync as
|
|
4176
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3812
4177
|
import { createRequire } from "module";
|
|
3813
|
-
import
|
|
4178
|
+
import path11 from "path";
|
|
3814
4179
|
|
|
3815
4180
|
// src/devcontainer/runtime-pull-hint.ts
|
|
3816
4181
|
import { Transform as Transform2 } from "stream";
|
|
@@ -3856,12 +4221,12 @@ var cachedBinaryPath = null;
|
|
|
3856
4221
|
function devcontainerCliPath() {
|
|
3857
4222
|
if (cachedBinaryPath) return cachedBinaryPath;
|
|
3858
4223
|
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
3859
|
-
const pkg = JSON.parse(
|
|
4224
|
+
const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
|
|
3860
4225
|
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
3861
4226
|
if (!binEntry) {
|
|
3862
4227
|
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
3863
4228
|
}
|
|
3864
|
-
cachedBinaryPath =
|
|
4229
|
+
cachedBinaryPath = path11.resolve(path11.dirname(pkgJsonPath), binEntry);
|
|
3865
4230
|
return cachedBinaryPath;
|
|
3866
4231
|
}
|
|
3867
4232
|
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
@@ -3979,16 +4344,16 @@ async function cleanupDockerObjects(opts) {
|
|
|
3979
4344
|
return { exitCode: rmExit, removedIds: ids };
|
|
3980
4345
|
}
|
|
3981
4346
|
function composeProjectName(root) {
|
|
3982
|
-
return `${
|
|
4347
|
+
return `${path12.basename(root)}_devcontainer`;
|
|
3983
4348
|
}
|
|
3984
4349
|
function resolveCompose(root) {
|
|
3985
|
-
if (!
|
|
4350
|
+
if (!existsSync6(path12.join(root, ".devcontainer"))) {
|
|
3986
4351
|
throw new Error(
|
|
3987
4352
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
3988
4353
|
);
|
|
3989
4354
|
}
|
|
3990
|
-
const composeFile =
|
|
3991
|
-
if (!
|
|
4355
|
+
const composeFile = path12.join(root, ".devcontainer", "compose.yaml");
|
|
4356
|
+
if (!existsSync6(composeFile)) {
|
|
3992
4357
|
throw new Error(
|
|
3993
4358
|
`No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
|
|
3994
4359
|
);
|
|
@@ -4179,6 +4544,12 @@ function formatUnreachableReposError(failures) {
|
|
|
4179
4544
|
lines.push(headerForKind(kind));
|
|
4180
4545
|
for (const e of entries) {
|
|
4181
4546
|
lines.push(` \u2022 ${e.url}`);
|
|
4547
|
+
if (e.detail) {
|
|
4548
|
+
for (const detailLine of e.detail.split("\n")) {
|
|
4549
|
+
const trimmed = detailLine.trim();
|
|
4550
|
+
if (trimmed) lines.push(` git: ${trimmed}`);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4182
4553
|
}
|
|
4183
4554
|
for (const advice of adviceForKind(kind)) {
|
|
4184
4555
|
lines.push(` - ${advice}`);
|
|
@@ -4225,11 +4596,85 @@ function adviceForKind(kind) {
|
|
|
4225
4596
|
}
|
|
4226
4597
|
}
|
|
4227
4598
|
|
|
4228
|
-
// src/devcontainer/
|
|
4599
|
+
// src/devcontainer/repo-clone.ts
|
|
4229
4600
|
import { spawn as spawn7 } from "child_process";
|
|
4601
|
+
import { existsSync as existsSync7, promises as fs10 } from "fs";
|
|
4602
|
+
import path13 from "path";
|
|
4603
|
+
var realGitClone = (url, dest) => {
|
|
4604
|
+
return new Promise((resolve, reject) => {
|
|
4605
|
+
const child = spawn7("git", ["clone", "--", url, dest], {
|
|
4606
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4607
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
4608
|
+
});
|
|
4609
|
+
let stdout = "";
|
|
4610
|
+
let stderr = "";
|
|
4611
|
+
child.stdout.on("data", (c) => {
|
|
4612
|
+
stdout += c.toString();
|
|
4613
|
+
});
|
|
4614
|
+
child.stderr.on("data", (c) => {
|
|
4615
|
+
stderr += c.toString();
|
|
4616
|
+
});
|
|
4617
|
+
child.on("error", reject);
|
|
4618
|
+
child.on(
|
|
4619
|
+
"exit",
|
|
4620
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
4621
|
+
);
|
|
4622
|
+
});
|
|
4623
|
+
};
|
|
4624
|
+
async function cloneReposHostSide(containerRoot, repos, options = {}) {
|
|
4625
|
+
const spawnFn = options.spawn ?? realGitClone;
|
|
4626
|
+
const results = [];
|
|
4627
|
+
for (const repo of repos) {
|
|
4628
|
+
const dest = path13.join(containerRoot, "projects", repo.path);
|
|
4629
|
+
if (existsSync7(dest)) {
|
|
4630
|
+
results.push({ path: repo.path, url: repo.url, status: "skipped" });
|
|
4631
|
+
continue;
|
|
4632
|
+
}
|
|
4633
|
+
await fs10.mkdir(path13.dirname(dest), { recursive: true });
|
|
4634
|
+
let r;
|
|
4635
|
+
try {
|
|
4636
|
+
r = await spawnFn(repo.url, dest);
|
|
4637
|
+
} catch (err) {
|
|
4638
|
+
results.push({
|
|
4639
|
+
path: repo.path,
|
|
4640
|
+
url: repo.url,
|
|
4641
|
+
status: "failed",
|
|
4642
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4643
|
+
});
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
results.push(
|
|
4647
|
+
r.exitCode === 0 ? { path: repo.path, url: repo.url, status: "cloned" } : {
|
|
4648
|
+
path: repo.path,
|
|
4649
|
+
url: repo.url,
|
|
4650
|
+
status: "failed",
|
|
4651
|
+
detail: r.stderr.trim()
|
|
4652
|
+
}
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
return results;
|
|
4656
|
+
}
|
|
4657
|
+
function formatCloneFailuresError(failures) {
|
|
4658
|
+
const lines = failures.length === 1 ? [`Failed to clone declared repo: ${failures[0].url}`, ""] : [`Failed to clone ${failures.length} declared repos:`, ""];
|
|
4659
|
+
for (const f of failures) {
|
|
4660
|
+
lines.push(` \u2022 ${f.url} \u2192 projects/${f.path}`);
|
|
4661
|
+
if (f.detail) lines.push(` ${f.detail}`);
|
|
4662
|
+
}
|
|
4663
|
+
lines.push("");
|
|
4664
|
+
lines.push(
|
|
4665
|
+
"Reachability was confirmed earlier, so this is usually a local issue"
|
|
4666
|
+
);
|
|
4667
|
+
lines.push(
|
|
4668
|
+
"(disk space, a leftover non-empty target dir). Fix it and re-run " + cyan2("monoceros apply") + "."
|
|
4669
|
+
);
|
|
4670
|
+
return lines.join("\n");
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
// src/devcontainer/docker-mode.ts
|
|
4674
|
+
import { spawn as spawn8 } from "child_process";
|
|
4230
4675
|
var realDockerInfo = () => {
|
|
4231
4676
|
return new Promise((resolve, reject) => {
|
|
4232
|
-
const child =
|
|
4677
|
+
const child = spawn8(
|
|
4233
4678
|
"docker",
|
|
4234
4679
|
["info", "--format", "{{json .SecurityOptions}}"],
|
|
4235
4680
|
{
|
|
@@ -4288,13 +4733,13 @@ function formatRootlessNotSupportedError() {
|
|
|
4288
4733
|
}
|
|
4289
4734
|
|
|
4290
4735
|
// src/devcontainer/identity.ts
|
|
4291
|
-
import { spawn as
|
|
4292
|
-
import { promises as
|
|
4293
|
-
import
|
|
4736
|
+
import { spawn as spawn9 } from "child_process";
|
|
4737
|
+
import { promises as fs11 } from "fs";
|
|
4738
|
+
import path14 from "path";
|
|
4294
4739
|
import { consola as consola10 } from "consola";
|
|
4295
4740
|
var realGitConfigGet = (key) => {
|
|
4296
4741
|
return new Promise((resolve, reject) => {
|
|
4297
|
-
const child =
|
|
4742
|
+
const child = spawn9("git", ["config", "--global", "--get", key], {
|
|
4298
4743
|
stdio: ["ignore", "pipe", "inherit"]
|
|
4299
4744
|
});
|
|
4300
4745
|
let stdout = "";
|
|
@@ -4404,8 +4849,8 @@ async function resolveIdentityWithPrompt(options = {}) {
|
|
|
4404
4849
|
};
|
|
4405
4850
|
}
|
|
4406
4851
|
async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
4407
|
-
const gitconfigDir =
|
|
4408
|
-
const gitconfigPath =
|
|
4852
|
+
const gitconfigDir = path14.join(devContainerRoot, ".monoceros");
|
|
4853
|
+
const gitconfigPath = path14.join(gitconfigDir, "gitconfig");
|
|
4409
4854
|
const logger = options.logger ?? { info: () => {
|
|
4410
4855
|
}, warn: () => {
|
|
4411
4856
|
} };
|
|
@@ -4418,8 +4863,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
4418
4863
|
const lines = ["[user]"];
|
|
4419
4864
|
if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
|
|
4420
4865
|
if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
|
|
4421
|
-
await
|
|
4422
|
-
await
|
|
4866
|
+
await fs11.mkdir(gitconfigDir, { recursive: true });
|
|
4867
|
+
await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
4423
4868
|
return {
|
|
4424
4869
|
...resolved.name !== void 0 ? { name: resolved.name } : {},
|
|
4425
4870
|
...resolved.email !== void 0 ? { email: resolved.email } : {},
|
|
@@ -4462,7 +4907,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
|
|
|
4462
4907
|
}
|
|
4463
4908
|
async function readExistingGitconfig(filePath) {
|
|
4464
4909
|
try {
|
|
4465
|
-
const content = await
|
|
4910
|
+
const content = await fs11.readFile(filePath, "utf8");
|
|
4466
4911
|
const result = {};
|
|
4467
4912
|
const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
|
|
4468
4913
|
const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
|
|
@@ -4495,7 +4940,7 @@ ${sectionLine(label)}
|
|
|
4495
4940
|
);
|
|
4496
4941
|
}
|
|
4497
4942
|
const ymlPath = containerConfigPath(opts.name, home);
|
|
4498
|
-
if (!
|
|
4943
|
+
if (!existsSync8(ymlPath)) {
|
|
4499
4944
|
throw new Error(
|
|
4500
4945
|
`No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
|
|
4501
4946
|
);
|
|
@@ -4512,6 +4957,20 @@ ${sectionLine(label)}
|
|
|
4512
4957
|
globalConfig?.defaults?.features ?? {}
|
|
4513
4958
|
)
|
|
4514
4959
|
);
|
|
4960
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
4961
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
4962
|
+
const envVars = readEnvFile(envPath);
|
|
4963
|
+
const interpServices = interpolateServices(createOpts.services, envVars);
|
|
4964
|
+
const interpFeatures = interpolateFeatures(
|
|
4965
|
+
createOpts.features ?? {},
|
|
4966
|
+
envVars
|
|
4967
|
+
);
|
|
4968
|
+
const missingVars = [...interpServices.missing, ...interpFeatures.missing];
|
|
4969
|
+
if (missingVars.length > 0) {
|
|
4970
|
+
throw new Error(formatMissingVarsError(missingVars, prettyPath(envPath)));
|
|
4971
|
+
}
|
|
4972
|
+
createOpts.services = interpServices.services;
|
|
4973
|
+
if (createOpts.features) createOpts.features = interpFeatures.features;
|
|
4515
4974
|
validateOptions(createOpts);
|
|
4516
4975
|
logger.success(`yml validated ${dim(`(${prettyPath(ymlPath)})`)}`);
|
|
4517
4976
|
const hasRepos = (createOpts.repos ?? []).length > 0;
|
|
@@ -4566,7 +5025,7 @@ ${sectionLine(label)}
|
|
|
4566
5025
|
if (dockerMode === "rootless") {
|
|
4567
5026
|
throw new Error(formatRootlessNotSupportedError());
|
|
4568
5027
|
}
|
|
4569
|
-
await
|
|
5028
|
+
await fs12.mkdir(targetDir, { recursive: true });
|
|
4570
5029
|
await writeScaffold(createOpts, targetDir, { dockerMode });
|
|
4571
5030
|
await writeStateFile(
|
|
4572
5031
|
targetDir,
|
|
@@ -4577,6 +5036,23 @@ ${sectionLine(label)}
|
|
|
4577
5036
|
})
|
|
4578
5037
|
);
|
|
4579
5038
|
logger.success(`materialized into ${prettyPath(targetDir)}`);
|
|
5039
|
+
const reposToClone = createOpts.repos ?? [];
|
|
5040
|
+
if (reposToClone.length > 0) {
|
|
5041
|
+
const cloneResults = await cloneReposHostSide(targetDir, reposToClone, {
|
|
5042
|
+
...opts.cloneSpawn ? { spawn: opts.cloneSpawn } : {}
|
|
5043
|
+
});
|
|
5044
|
+
for (const r of cloneResults) {
|
|
5045
|
+
if (r.status === "cloned") {
|
|
5046
|
+
logger.success(`cloned ${cyan2(r.path)} ${dim(`(${r.url})`)}`);
|
|
5047
|
+
} else if (r.status === "skipped") {
|
|
5048
|
+
logger.info(`projects/${r.path} already present \u2014 skipped clone`);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
const cloneFailures = cloneResults.filter((r) => r.status === "failed");
|
|
5052
|
+
if (cloneFailures.length > 0) {
|
|
5053
|
+
throw new Error(formatCloneFailuresError(cloneFailures));
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
4580
5056
|
section("Container");
|
|
4581
5057
|
const featureRefs = parsed.config.features.map((f) => f.ref);
|
|
4582
5058
|
if (featureRefs.length > 0) {
|
|
@@ -4624,8 +5100,8 @@ ${sectionLine(label)}
|
|
|
4624
5100
|
return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
|
|
4625
5101
|
}
|
|
4626
5102
|
async function assertSafeTargetDir(targetDir, expectedOrigin) {
|
|
4627
|
-
if (!
|
|
4628
|
-
const entries = await
|
|
5103
|
+
if (!existsSync8(targetDir)) return;
|
|
5104
|
+
const entries = await fs12.readdir(targetDir);
|
|
4629
5105
|
if (entries.length === 0) return;
|
|
4630
5106
|
const state = await readStateFile(targetDir);
|
|
4631
5107
|
if (state) {
|
|
@@ -4695,7 +5171,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4695
5171
|
}
|
|
4696
5172
|
if (wantContainer) {
|
|
4697
5173
|
try {
|
|
4698
|
-
const text = await
|
|
5174
|
+
const text = await fs12.readFile(ymlPath, "utf8");
|
|
4699
5175
|
const parsed = parseConfig(text, ymlPath);
|
|
4700
5176
|
const changed = setContainerGitUserInDoc(parsed.doc, {
|
|
4701
5177
|
name: prompted.name,
|
|
@@ -4703,7 +5179,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4703
5179
|
});
|
|
4704
5180
|
if (changed) {
|
|
4705
5181
|
const out = stringifyConfig(parsed.doc);
|
|
4706
|
-
await
|
|
5182
|
+
await fs12.writeFile(ymlPath, out, "utf8");
|
|
4707
5183
|
logger.info(
|
|
4708
5184
|
`Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
|
|
4709
5185
|
);
|
|
@@ -4717,7 +5193,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4717
5193
|
}
|
|
4718
5194
|
|
|
4719
5195
|
// src/version.ts
|
|
4720
|
-
var CLI_VERSION = true ? "1.
|
|
5196
|
+
var CLI_VERSION = true ? "1.13.0" : "dev";
|
|
4721
5197
|
|
|
4722
5198
|
// src/commands/_dispatch.ts
|
|
4723
5199
|
import { consola as consola12 } from "consola";
|
|
@@ -4877,8 +5353,8 @@ var completionCommand = defineCommand9({
|
|
|
4877
5353
|
import { defineCommand as defineCommand10 } from "citty";
|
|
4878
5354
|
|
|
4879
5355
|
// src/completion/resolve.ts
|
|
4880
|
-
import { existsSync as
|
|
4881
|
-
import
|
|
5356
|
+
import { existsSync as existsSync9, promises as fs13 } from "fs";
|
|
5357
|
+
import path15 from "path";
|
|
4882
5358
|
async function resolveCompletions(line, point, opts = {}) {
|
|
4883
5359
|
const { prev, current } = parseCompletionLine(line, point);
|
|
4884
5360
|
const ctx = { prev, current, opts };
|
|
@@ -5026,15 +5502,11 @@ function filterPrefix(values, fragment) {
|
|
|
5026
5502
|
}
|
|
5027
5503
|
async function listContainerNames(ctx) {
|
|
5028
5504
|
const home = ctx.opts.monocerosHome ?? monocerosHome();
|
|
5029
|
-
const dir =
|
|
5030
|
-
if (!
|
|
5031
|
-
const entries = await
|
|
5505
|
+
const dir = path15.join(home, "container-configs");
|
|
5506
|
+
if (!existsSync9(dir)) return [];
|
|
5507
|
+
const entries = await fs13.readdir(dir);
|
|
5032
5508
|
return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
|
|
5033
5509
|
}
|
|
5034
|
-
async function listAllCatalogComponents() {
|
|
5035
|
-
const catalog = await loadComponentCatalog();
|
|
5036
|
-
return [...catalog.keys()].sort();
|
|
5037
|
-
}
|
|
5038
5510
|
async function listFeatureComponents() {
|
|
5039
5511
|
const catalog = await loadComponentCatalog();
|
|
5040
5512
|
return [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
|
|
@@ -5136,8 +5608,14 @@ var COMMAND_SPECS = {
|
|
|
5136
5608
|
// flag suggestions.
|
|
5137
5609
|
positionalCount: 1,
|
|
5138
5610
|
flags: {
|
|
5139
|
-
"--with": { type: "value", values: () =>
|
|
5140
|
-
"--with-
|
|
5611
|
+
"--with-languages": { type: "value", values: () => listLanguageNames() },
|
|
5612
|
+
"--with-features": {
|
|
5613
|
+
type: "value",
|
|
5614
|
+
values: () => listFeatureComponents()
|
|
5615
|
+
},
|
|
5616
|
+
"--with-services": { type: "value", values: () => listServiceNames() },
|
|
5617
|
+
"--with-apt-packages": { type: "value" },
|
|
5618
|
+
"--with-repos": { type: "value" },
|
|
5141
5619
|
"--with-ports": { type: "value" }
|
|
5142
5620
|
}
|
|
5143
5621
|
},
|
|
@@ -5281,22 +5759,22 @@ import { defineCommand as defineCommand11 } from "citty";
|
|
|
5281
5759
|
import { consola as consola14 } from "consola";
|
|
5282
5760
|
|
|
5283
5761
|
// src/init/index.ts
|
|
5284
|
-
import { existsSync as
|
|
5762
|
+
import { existsSync as existsSync10, promises as fs14 } from "fs";
|
|
5763
|
+
import path16 from "path";
|
|
5285
5764
|
import { consola as consola13 } from "consola";
|
|
5286
5765
|
|
|
5287
5766
|
// src/init/generator.ts
|
|
5288
5767
|
var SCHEMA_HEADER_ACTIVE = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Edit any section, then run `monoceros apply <name>` to (re-)build.";
|
|
5289
5768
|
var SCHEMA_HEADER_DOCUMENTED = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Every section is commented out by default; un-comment what you need\n# (strip one `#` per line of the block), then run `monoceros apply <name>`.";
|
|
5290
5769
|
var COMMENT_WIDTH = 76;
|
|
5291
|
-
function generateComposedYml(name,
|
|
5292
|
-
const merged = mergeComponents(components);
|
|
5770
|
+
function generateComposedYml(name, composed, lookupManifest, repoUrls = [], ports = []) {
|
|
5293
5771
|
const lines = [];
|
|
5294
5772
|
pushHeader(lines, SCHEMA_HEADER_ACTIVE, name);
|
|
5295
5773
|
lines.push("");
|
|
5296
5774
|
lines.push("schemaVersion: 1");
|
|
5297
5775
|
lines.push(`name: ${name}`);
|
|
5298
5776
|
lines.push("");
|
|
5299
|
-
if (
|
|
5777
|
+
if (composed.languages.length > 0) {
|
|
5300
5778
|
pushSectionHeader(
|
|
5301
5779
|
lines,
|
|
5302
5780
|
LANGUAGES_HEADER,
|
|
@@ -5304,10 +5782,21 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5304
5782
|
false
|
|
5305
5783
|
);
|
|
5306
5784
|
lines.push("languages:");
|
|
5307
|
-
for (const lang of
|
|
5785
|
+
for (const lang of composed.languages) lines.push(` - ${lang}`);
|
|
5786
|
+
lines.push("");
|
|
5787
|
+
}
|
|
5788
|
+
if (composed.aptPackages.length > 0) {
|
|
5789
|
+
pushSectionHeader(
|
|
5790
|
+
lines,
|
|
5791
|
+
APT_PACKAGES_HEADER,
|
|
5792
|
+
/* commented */
|
|
5793
|
+
false
|
|
5794
|
+
);
|
|
5795
|
+
lines.push("aptPackages:");
|
|
5796
|
+
for (const pkg of composed.aptPackages) lines.push(` - ${pkg}`);
|
|
5308
5797
|
lines.push("");
|
|
5309
5798
|
}
|
|
5310
|
-
if (
|
|
5799
|
+
if (composed.services.length > 0) {
|
|
5311
5800
|
pushSectionHeader(
|
|
5312
5801
|
lines,
|
|
5313
5802
|
SERVICES_HEADER,
|
|
@@ -5315,10 +5804,10 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5315
5804
|
false
|
|
5316
5805
|
);
|
|
5317
5806
|
lines.push("services:");
|
|
5318
|
-
for (const svc of
|
|
5807
|
+
for (const svc of composed.services) pushServiceEntry(lines, svc);
|
|
5319
5808
|
lines.push("");
|
|
5320
5809
|
}
|
|
5321
|
-
if (
|
|
5810
|
+
if (composed.features.length > 0) {
|
|
5322
5811
|
pushSectionHeader(
|
|
5323
5812
|
lines,
|
|
5324
5813
|
FEATURES_HEADER_ACTIVE,
|
|
@@ -5326,7 +5815,7 @@ function generateComposedYml(name, components, lookupManifest, repoUrls = [], po
|
|
|
5326
5815
|
false
|
|
5327
5816
|
);
|
|
5328
5817
|
lines.push("features:");
|
|
5329
|
-
for (const f of
|
|
5818
|
+
for (const f of composed.features) {
|
|
5330
5819
|
lines.push("");
|
|
5331
5820
|
renderFeatureBlock(
|
|
5332
5821
|
lines,
|
|
@@ -5407,7 +5896,9 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
5407
5896
|
lines.push("# services:");
|
|
5408
5897
|
for (const c of byCategory.service) {
|
|
5409
5898
|
for (const svc of c.file.contributes.services ?? []) {
|
|
5410
|
-
|
|
5899
|
+
const body = renderServiceObjectBody(expandCuratedService(svc));
|
|
5900
|
+
lines.push(`# - ${body[0]}`);
|
|
5901
|
+
for (const line of body.slice(1)) lines.push(`# ${line}`);
|
|
5411
5902
|
}
|
|
5412
5903
|
}
|
|
5413
5904
|
lines.push("");
|
|
@@ -5514,6 +6005,22 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
5514
6005
|
}
|
|
5515
6006
|
var LANGUAGES_HEADER = "Language runtimes installed inside the dev-container. Pick the ones your projects build against. The catalog of available runtimes is shown by `monoceros list-components`.";
|
|
5516
6007
|
var SERVICES_HEADER = "Sibling containers that run alongside the dev-container (databases, caches, message queues, \u2026). Each service is reachable from inside the dev-container by its name as hostname (e.g. `postgres://postgres:5432`). Activating any service switches the container to docker-compose mode automatically.";
|
|
6008
|
+
var APT_PACKAGES_HEADER = "Debian/Ubuntu apt packages installed in the dev-container at build time. No curated list \u2014 any apt package name works; an invalid name surfaces as an apt error during build.";
|
|
6009
|
+
function pushServiceEntry(out, svc) {
|
|
6010
|
+
if (svc.kind === "custom") {
|
|
6011
|
+
const { bodyLines, comment } = renderCustomService(
|
|
6012
|
+
svc.name,
|
|
6013
|
+
svc.image ?? ""
|
|
6014
|
+
);
|
|
6015
|
+
out.push(` - ${bodyLines[0]}`);
|
|
6016
|
+
for (const line of bodyLines.slice(1)) out.push(` ${line}`);
|
|
6017
|
+
for (const cl of comment.split("\n")) out.push(` #${cl}`);
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
const body = renderServiceObjectBody(expandCuratedService(svc.name));
|
|
6021
|
+
out.push(` - ${body[0]}`);
|
|
6022
|
+
for (const line of body.slice(1)) out.push(` ${line}`);
|
|
6023
|
+
}
|
|
5517
6024
|
var FEATURES_HEADER_ACTIVE = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. The features active for this container are listed below; adjust their options as needed. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
5518
6025
|
var FEATURES_HEADER_DOCUMENTED = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. Un-comment the blocks below for the features you want active. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
|
|
5519
6026
|
var REPOS_HEADER = "Git repositories cloned into `projects/` on container start-up. HTTPS URLs only. The provider is auto-detected for github.com / gitlab.com / bitbucket.org; for any other host (self-hosted GitLab, Gitea, \u2026) declare `provider:` explicitly. Add more later with `monoceros add-repo`.";
|
|
@@ -5528,15 +6035,15 @@ function renderFeatureBlock(out, feature, summary, commented) {
|
|
|
5528
6035
|
out.push(`${yamlPrefix} - ref: ${feature.ref}`);
|
|
5529
6036
|
const options = feature.options ?? {};
|
|
5530
6037
|
const activeKeys = Object.entries(options);
|
|
5531
|
-
const
|
|
5532
|
-
if (activeKeys.length === 0 &&
|
|
6038
|
+
const hints = featureOptionHints(summary, feature.ref, Object.keys(options));
|
|
6039
|
+
if (activeKeys.length === 0 && hints.length === 0) return;
|
|
5533
6040
|
if (commented) {
|
|
5534
6041
|
out.push(`${yamlPrefix} options:`);
|
|
5535
6042
|
for (const [key, value] of activeKeys) {
|
|
5536
6043
|
out.push(`${yamlPrefix} ${key}: ${renderScalarValue(value)}`);
|
|
5537
6044
|
}
|
|
5538
|
-
for (const
|
|
5539
|
-
out.push(`${yamlPrefix} ${key}
|
|
6045
|
+
for (const hint of hints) {
|
|
6046
|
+
out.push(`${yamlPrefix} ${hint.key}: ${hint.placeholder}`);
|
|
5540
6047
|
}
|
|
5541
6048
|
return;
|
|
5542
6049
|
}
|
|
@@ -5546,10 +6053,10 @@ function renderFeatureBlock(out, feature, summary, commented) {
|
|
|
5546
6053
|
out.push(` ${key}: ${renderScalarValue(value)}`);
|
|
5547
6054
|
}
|
|
5548
6055
|
}
|
|
5549
|
-
if (
|
|
6056
|
+
if (hints.length > 0) {
|
|
5550
6057
|
out.push(` # options:`);
|
|
5551
|
-
for (const
|
|
5552
|
-
out.push(` # ${key}
|
|
6058
|
+
for (const hint of hints) {
|
|
6059
|
+
out.push(` # ${hint.key}: ${hint.placeholder}`);
|
|
5553
6060
|
}
|
|
5554
6061
|
}
|
|
5555
6062
|
}
|
|
@@ -5603,7 +6110,7 @@ async function runInit(opts) {
|
|
|
5603
6110
|
);
|
|
5604
6111
|
}
|
|
5605
6112
|
const dest = containerConfigPath(opts.name, home);
|
|
5606
|
-
if (
|
|
6113
|
+
if (existsSync10(dest)) {
|
|
5607
6114
|
throw new Error(
|
|
5608
6115
|
`Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
|
|
5609
6116
|
);
|
|
@@ -5673,15 +6180,28 @@ async function runInit(opts) {
|
|
|
5673
6180
|
});
|
|
5674
6181
|
}
|
|
5675
6182
|
let text;
|
|
5676
|
-
const
|
|
5677
|
-
|
|
6183
|
+
const composed = resolveComposedInit(catalog, {
|
|
6184
|
+
languages: opts.languages ?? [],
|
|
6185
|
+
features: opts.features ?? [],
|
|
6186
|
+
services: opts.services ?? [],
|
|
6187
|
+
aptPackages: opts.aptPackages ?? []
|
|
6188
|
+
});
|
|
6189
|
+
const anyComposed = composed.languages.length > 0 || composed.features.length > 0 || composed.services.length > 0 || composed.aptPackages.length > 0;
|
|
6190
|
+
if (!anyComposed) {
|
|
5678
6191
|
text = generateDocumentedYml(opts.name, catalog, lookup, repos, ports);
|
|
5679
6192
|
} else {
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
}
|
|
5683
|
-
await
|
|
5684
|
-
await
|
|
6193
|
+
text = generateComposedYml(opts.name, composed, lookup, repos, ports);
|
|
6194
|
+
}
|
|
6195
|
+
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
6196
|
+
await ensureEnvGitignored(containerConfigsDir(home));
|
|
6197
|
+
await fs14.writeFile(dest, text, "utf8");
|
|
6198
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
6199
|
+
const featureVars = composed.features.flatMap(
|
|
6200
|
+
(f) => featureOptionHints(lookup(f.ref), f.ref, Object.keys(f.options ?? {})).map(
|
|
6201
|
+
(h) => h.envVar
|
|
6202
|
+
)
|
|
6203
|
+
);
|
|
6204
|
+
await ensureEnvVars(envPath, opts.name, featureVars);
|
|
5685
6205
|
if (promptedIdentity?.prompted) {
|
|
5686
6206
|
const { name, email, scope } = promptedIdentity.prompted;
|
|
5687
6207
|
if (scope === "g" || scope === "b") {
|
|
@@ -5711,11 +6231,11 @@ async function runInit(opts) {
|
|
|
5711
6231
|
}
|
|
5712
6232
|
if (scope === "c" || scope === "b") {
|
|
5713
6233
|
try {
|
|
5714
|
-
const written = await
|
|
6234
|
+
const written = await fs14.readFile(dest, "utf8");
|
|
5715
6235
|
const parsed = parseConfig(written, dest);
|
|
5716
6236
|
const changed = setContainerGitUserInDoc(parsed.doc, { name, email });
|
|
5717
6237
|
if (changed) {
|
|
5718
|
-
await
|
|
6238
|
+
await fs14.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
|
|
5719
6239
|
logger.info(
|
|
5720
6240
|
`Saved identity in ${prettyPath(dest)} (container-level git.user).`
|
|
5721
6241
|
);
|
|
@@ -5727,29 +6247,136 @@ async function runInit(opts) {
|
|
|
5727
6247
|
}
|
|
5728
6248
|
}
|
|
5729
6249
|
}
|
|
5730
|
-
const documented =
|
|
5731
|
-
const
|
|
6250
|
+
const documented = !anyComposed;
|
|
6251
|
+
const ymlRel = path16.relative(home, dest);
|
|
6252
|
+
const envRel = path16.relative(home, envPath);
|
|
5732
6253
|
if (documented) {
|
|
5733
|
-
logger.success(
|
|
5734
|
-
|
|
6254
|
+
logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
|
|
6255
|
+
logger.info(
|
|
6256
|
+
`Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
|
|
5735
6257
|
);
|
|
5736
6258
|
} else {
|
|
5737
|
-
logger.success(
|
|
5738
|
-
`Composed ${requested.length} component(s) into ${displayPath}: ${requested.join(", ")}`
|
|
5739
|
-
);
|
|
6259
|
+
logger.success(`Composed into ${ymlRel} and ${envRel}.`);
|
|
5740
6260
|
logger.info(
|
|
5741
|
-
`Edit the
|
|
6261
|
+
`Edit the files if you need to tweak, then \`monoceros apply ${opts.name}\`.`
|
|
5742
6262
|
);
|
|
5743
6263
|
}
|
|
5744
6264
|
return { configPath: dest, documented };
|
|
5745
6265
|
}
|
|
6266
|
+
function resolveComposedInit(catalog, raw) {
|
|
6267
|
+
return {
|
|
6268
|
+
languages: resolveInitLanguages(raw.languages),
|
|
6269
|
+
aptPackages: resolveInitAptPackages(raw.aptPackages),
|
|
6270
|
+
services: resolveInitServices(raw.services),
|
|
6271
|
+
features: resolveInitFeatures(catalog, raw.features)
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
function resolveInitLanguages(entries) {
|
|
6275
|
+
const known = new Set(knownLanguages());
|
|
6276
|
+
const out = [];
|
|
6277
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6278
|
+
const unknown = [];
|
|
6279
|
+
for (const raw of entries) {
|
|
6280
|
+
const e = raw.trim();
|
|
6281
|
+
if (!e || seen.has(e)) continue;
|
|
6282
|
+
const spec = parseLanguageSpec(e);
|
|
6283
|
+
if (!spec || !known.has(spec.name)) {
|
|
6284
|
+
unknown.push(e);
|
|
6285
|
+
continue;
|
|
6286
|
+
}
|
|
6287
|
+
seen.add(e);
|
|
6288
|
+
out.push(e);
|
|
6289
|
+
}
|
|
6290
|
+
if (unknown.length > 0) {
|
|
6291
|
+
throw new Error(
|
|
6292
|
+
`Unknown language${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}. Known: ${knownLanguages().join(", ")}.`
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
return out;
|
|
6296
|
+
}
|
|
6297
|
+
function resolveInitAptPackages(entries) {
|
|
6298
|
+
const out = [];
|
|
6299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6300
|
+
const bad = [];
|
|
6301
|
+
for (const raw of entries) {
|
|
6302
|
+
const e = raw.trim();
|
|
6303
|
+
if (!e || seen.has(e)) continue;
|
|
6304
|
+
if (!REGEX.aptPackage.test(e)) {
|
|
6305
|
+
bad.push(e);
|
|
6306
|
+
continue;
|
|
6307
|
+
}
|
|
6308
|
+
seen.add(e);
|
|
6309
|
+
out.push(e);
|
|
6310
|
+
}
|
|
6311
|
+
if (bad.length > 0) {
|
|
6312
|
+
throw new Error(
|
|
6313
|
+
`Invalid apt package name${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}. Expected lowercase alphanumeric plus '.+-'.`
|
|
6314
|
+
);
|
|
6315
|
+
}
|
|
6316
|
+
return out;
|
|
6317
|
+
}
|
|
6318
|
+
function resolveInitServices(entries) {
|
|
6319
|
+
const out = [];
|
|
6320
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6321
|
+
for (const raw of entries) {
|
|
6322
|
+
const e = raw.trim();
|
|
6323
|
+
if (!e) continue;
|
|
6324
|
+
const svc = isCuratedService(e) ? { kind: "curated", name: e } : { kind: "custom", name: deriveServiceName(e), image: e };
|
|
6325
|
+
const existing = byName.get(svc.name);
|
|
6326
|
+
if (existing) {
|
|
6327
|
+
if (existing.kind === svc.kind && existing.image === svc.image) continue;
|
|
6328
|
+
throw new Error(
|
|
6329
|
+
`Two --with-services entries resolve to the service name '${svc.name}'. Add one after init with \`monoceros add-service ${"<name>"} <image> --as=<other>\`.`
|
|
6330
|
+
);
|
|
6331
|
+
}
|
|
6332
|
+
byName.set(svc.name, svc);
|
|
6333
|
+
out.push(svc);
|
|
6334
|
+
}
|
|
6335
|
+
return out;
|
|
6336
|
+
}
|
|
6337
|
+
function resolveInitFeatures(catalog, entries) {
|
|
6338
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
6339
|
+
const unknown = [];
|
|
6340
|
+
for (const raw of entries) {
|
|
6341
|
+
const e = raw.trim();
|
|
6342
|
+
if (!e) continue;
|
|
6343
|
+
if (REGEX.featureRef.test(e)) {
|
|
6344
|
+
if (!byRef.has(e)) byRef.set(e, { ref: e, options: {} });
|
|
6345
|
+
continue;
|
|
6346
|
+
}
|
|
6347
|
+
const c = catalog.get(e);
|
|
6348
|
+
if (!c || c.file.category !== "feature") {
|
|
6349
|
+
unknown.push(e);
|
|
6350
|
+
continue;
|
|
6351
|
+
}
|
|
6352
|
+
for (const f of c.file.contributes.features ?? []) {
|
|
6353
|
+
const existing = byRef.get(f.ref);
|
|
6354
|
+
if (!existing) {
|
|
6355
|
+
byRef.set(f.ref, { ref: f.ref, options: { ...f.options ?? {} } });
|
|
6356
|
+
} else {
|
|
6357
|
+
existing.options = mergeFeatureOptions(
|
|
6358
|
+
existing.options,
|
|
6359
|
+
f.options ?? {}
|
|
6360
|
+
);
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
if (unknown.length > 0) {
|
|
6365
|
+
const featureNames = [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
|
|
6366
|
+
throw new Error(
|
|
6367
|
+
`Unknown feature${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
|
|
6368
|
+
Use a catalog short name (${featureNames.join(", ")}) or a full OCI ref (ghcr.io/\u2026/<name>:<tag>).`
|
|
6369
|
+
);
|
|
6370
|
+
}
|
|
6371
|
+
return [...byRef.values()];
|
|
6372
|
+
}
|
|
5746
6373
|
|
|
5747
6374
|
// src/commands/init.ts
|
|
5748
6375
|
var initCommand = defineCommand11({
|
|
5749
6376
|
meta: {
|
|
5750
6377
|
name: "init",
|
|
5751
6378
|
group: "lifecycle",
|
|
5752
|
-
description: "Create a fresh container-config yml at
|
|
6379
|
+
description: "Create a fresh container-config yml at <MONOCEROS_HOME>/container-configs/<name>.yml. Without any --with-* flag, the file is a documented default with every component commented out. With --with-languages / --with-features / --with-services / --with-apt-packages, the named pieces are composed into an active, immediately-applyable yml. Then run `monoceros apply <name>`."
|
|
5753
6380
|
},
|
|
5754
6381
|
args: {
|
|
5755
6382
|
name: {
|
|
@@ -5757,14 +6384,29 @@ var initCommand = defineCommand11({
|
|
|
5757
6384
|
description: "Config name. The yml lands at <MONOCEROS_HOME>/container-configs/<name>.yml and becomes the source-of-truth for `monoceros apply <name>`.",
|
|
5758
6385
|
required: true
|
|
5759
6386
|
},
|
|
5760
|
-
with: {
|
|
6387
|
+
"with-languages": {
|
|
6388
|
+
type: "string",
|
|
6389
|
+
description: "Language runtimes to install, comma-separated or repeated, e.g. --with-languages=java,node. Optional :version (java:17). Curated catalog only \u2014 see `monoceros list-components`.",
|
|
6390
|
+
required: false
|
|
6391
|
+
},
|
|
6392
|
+
"with-features": {
|
|
6393
|
+
type: "string",
|
|
6394
|
+
description: "Features (AI tools, language CLIs, \u2026), comma-separated or repeated. Catalog short name (claude, atlassian/twg) or a full OCI ref (ghcr.io/foo/bar:1).",
|
|
6395
|
+
required: false
|
|
6396
|
+
},
|
|
6397
|
+
"with-services": {
|
|
6398
|
+
type: "string",
|
|
6399
|
+
description: "Backing services, comma-separated or repeated. Curated name (postgres, mysql, redis) \u2192 full editable block; any other image (rustfs/rustfs:latest) \u2192 name + image + commented scaffold.",
|
|
6400
|
+
required: false
|
|
6401
|
+
},
|
|
6402
|
+
"with-apt-packages": {
|
|
5761
6403
|
type: "string",
|
|
5762
|
-
description: "
|
|
6404
|
+
description: "Debian/Ubuntu apt packages to install, comma-separated or repeated, e.g. --with-apt-packages=openssl,make. No curated list.",
|
|
5763
6405
|
required: false
|
|
5764
6406
|
},
|
|
5765
|
-
"with-
|
|
6407
|
+
"with-repos": {
|
|
5766
6408
|
type: "string",
|
|
5767
|
-
description: "Git
|
|
6409
|
+
description: "Git URLs to clone into projects/ on first apply, comma-separated or repeated. Folder name derived from URL (foo.git \u2192 projects/foo/); use `monoceros add-repo --path=...` post-init for subfolder paths. Canonical hosts only (github.com / gitlab.com / bitbucket.org).",
|
|
5768
6410
|
required: false
|
|
5769
6411
|
},
|
|
5770
6412
|
"with-ports": {
|
|
@@ -5775,14 +6417,20 @@ var initCommand = defineCommand11({
|
|
|
5775
6417
|
},
|
|
5776
6418
|
async run({ args, rawArgs }) {
|
|
5777
6419
|
try {
|
|
5778
|
-
const
|
|
5779
|
-
const
|
|
5780
|
-
const
|
|
6420
|
+
const languages = collectListFlag("--with-languages", rawArgs);
|
|
6421
|
+
const features = collectListFlag("--with-features", rawArgs);
|
|
6422
|
+
const services = collectListFlag("--with-services", rawArgs);
|
|
6423
|
+
const aptPackages = collectListFlag("--with-apt-packages", rawArgs);
|
|
6424
|
+
const repos = collectListFlag("--with-repos", rawArgs);
|
|
6425
|
+
const ports = collectWithPortsList(args["with-ports"], rawArgs);
|
|
5781
6426
|
await runInit({
|
|
5782
6427
|
name: args.name,
|
|
5783
|
-
...
|
|
5784
|
-
...
|
|
5785
|
-
...
|
|
6428
|
+
...languages.length > 0 ? { languages } : {},
|
|
6429
|
+
...features.length > 0 ? { features } : {},
|
|
6430
|
+
...services.length > 0 ? { services } : {},
|
|
6431
|
+
...aptPackages.length > 0 ? { aptPackages } : {},
|
|
6432
|
+
...repos.length > 0 ? { withRepo: repos } : {},
|
|
6433
|
+
...ports && ports.length > 0 ? { withPorts: ports } : {}
|
|
5786
6434
|
});
|
|
5787
6435
|
} catch (err) {
|
|
5788
6436
|
consola14.error(err instanceof Error ? err.message : String(err));
|
|
@@ -5790,6 +6438,30 @@ var initCommand = defineCommand11({
|
|
|
5790
6438
|
}
|
|
5791
6439
|
}
|
|
5792
6440
|
});
|
|
6441
|
+
function collectListFlag(flag, rawArgs) {
|
|
6442
|
+
const eq = `${flag}=`;
|
|
6443
|
+
const pieces = [];
|
|
6444
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
6445
|
+
const t = rawArgs[i];
|
|
6446
|
+
let scanStart = -1;
|
|
6447
|
+
if (t === flag) {
|
|
6448
|
+
scanStart = i + 1;
|
|
6449
|
+
} else if (t.startsWith(eq)) {
|
|
6450
|
+
pieces.push(t.slice(eq.length));
|
|
6451
|
+
scanStart = i + 1;
|
|
6452
|
+
}
|
|
6453
|
+
if (scanStart < 0) continue;
|
|
6454
|
+
let j = scanStart;
|
|
6455
|
+
while (j < rawArgs.length) {
|
|
6456
|
+
const u = rawArgs[j];
|
|
6457
|
+
if (u.startsWith("-")) break;
|
|
6458
|
+
pieces.push(u);
|
|
6459
|
+
j += 1;
|
|
6460
|
+
}
|
|
6461
|
+
i = j - 1;
|
|
6462
|
+
}
|
|
6463
|
+
return pieces.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6464
|
+
}
|
|
5793
6465
|
function collectWithPortsList(_withPortsArg, rawArgs) {
|
|
5794
6466
|
const pieces = [];
|
|
5795
6467
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
@@ -5825,43 +6497,6 @@ function collectWithPortsList(_withPortsArg, rawArgs) {
|
|
|
5825
6497
|
}
|
|
5826
6498
|
return out;
|
|
5827
6499
|
}
|
|
5828
|
-
function collectWithRepoList(rawArgs) {
|
|
5829
|
-
const urls = [];
|
|
5830
|
-
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
5831
|
-
const t = rawArgs[i];
|
|
5832
|
-
if (t === "--with-repo") {
|
|
5833
|
-
const next = rawArgs[i + 1];
|
|
5834
|
-
if (typeof next === "string" && !next.startsWith("-")) {
|
|
5835
|
-
urls.push(next);
|
|
5836
|
-
i += 1;
|
|
5837
|
-
}
|
|
5838
|
-
} else if (t.startsWith("--with-repo=")) {
|
|
5839
|
-
urls.push(t.slice("--with-repo=".length));
|
|
5840
|
-
}
|
|
5841
|
-
}
|
|
5842
|
-
return urls;
|
|
5843
|
-
}
|
|
5844
|
-
function collectWithList(withArg, rawArgs) {
|
|
5845
|
-
if (typeof withArg !== "string" || withArg.trim().length === 0) {
|
|
5846
|
-
return void 0;
|
|
5847
|
-
}
|
|
5848
|
-
let combined = withArg.trim();
|
|
5849
|
-
const startIdx = rawArgs.findIndex(
|
|
5850
|
-
(t) => t === "--with" || t.startsWith("--with=")
|
|
5851
|
-
);
|
|
5852
|
-
if (startIdx >= 0) {
|
|
5853
|
-
let scanFrom = startIdx + 1;
|
|
5854
|
-
if (rawArgs[startIdx] === "--with") scanFrom += 1;
|
|
5855
|
-
for (let i = scanFrom; i < rawArgs.length; i += 1) {
|
|
5856
|
-
const t = rawArgs[i];
|
|
5857
|
-
if (t.startsWith("--") || t === "-h" || t === "--help") break;
|
|
5858
|
-
const sep = combined.endsWith(",") ? "" : ",";
|
|
5859
|
-
combined += sep + t;
|
|
5860
|
-
}
|
|
5861
|
-
}
|
|
5862
|
-
const pieces = combined.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5863
|
-
return pieces.length > 0 ? pieces : void 0;
|
|
5864
|
-
}
|
|
5865
6500
|
|
|
5866
6501
|
// src/commands/list-components.ts
|
|
5867
6502
|
import { defineCommand as defineCommand12 } from "citty";
|
|
@@ -6149,8 +6784,8 @@ import { consola as consola20 } from "consola";
|
|
|
6149
6784
|
import { createInterface } from "readline/promises";
|
|
6150
6785
|
|
|
6151
6786
|
// src/remove/index.ts
|
|
6152
|
-
import { existsSync as
|
|
6153
|
-
import
|
|
6787
|
+
import { existsSync as existsSync11, promises as fs15 } from "fs";
|
|
6788
|
+
import path17 from "path";
|
|
6154
6789
|
import { consola as consola19 } from "consola";
|
|
6155
6790
|
async function runRemove(opts) {
|
|
6156
6791
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -6165,9 +6800,11 @@ async function runRemove(opts) {
|
|
|
6165
6800
|
);
|
|
6166
6801
|
}
|
|
6167
6802
|
const ymlPath = containerConfigPath(opts.name, home);
|
|
6803
|
+
const envPath = containerEnvPath(opts.name, home);
|
|
6168
6804
|
const containerPath = containerDir(opts.name, home);
|
|
6169
|
-
const hasYml =
|
|
6170
|
-
const
|
|
6805
|
+
const hasYml = existsSync11(ymlPath);
|
|
6806
|
+
const hasEnv = existsSync11(envPath);
|
|
6807
|
+
const hasContainer = existsSync11(containerPath);
|
|
6171
6808
|
if (!hasYml && !hasContainer) {
|
|
6172
6809
|
throw new Error(
|
|
6173
6810
|
`Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
|
|
@@ -6191,24 +6828,30 @@ async function runRemove(opts) {
|
|
|
6191
6828
|
let backupPath = null;
|
|
6192
6829
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
6193
6830
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6194
|
-
backupPath =
|
|
6195
|
-
await
|
|
6831
|
+
backupPath = path17.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
6832
|
+
await fs15.mkdir(backupPath, { recursive: true });
|
|
6196
6833
|
if (hasYml) {
|
|
6197
|
-
await
|
|
6834
|
+
await fs15.copyFile(ymlPath, path17.join(backupPath, `${opts.name}.yml`));
|
|
6835
|
+
}
|
|
6836
|
+
if (hasEnv) {
|
|
6837
|
+
await fs15.copyFile(envPath, path17.join(backupPath, `${opts.name}.env`));
|
|
6198
6838
|
}
|
|
6199
6839
|
if (hasContainer) {
|
|
6200
|
-
await
|
|
6840
|
+
await fs15.cp(containerPath, path17.join(backupPath, "container"), {
|
|
6201
6841
|
recursive: true
|
|
6202
6842
|
});
|
|
6203
6843
|
}
|
|
6204
6844
|
logger.info(`Backup written to ${prettyPath(backupPath)}.`);
|
|
6205
6845
|
}
|
|
6206
6846
|
if (hasYml) {
|
|
6207
|
-
await
|
|
6847
|
+
await fs15.rm(ymlPath, { force: true });
|
|
6848
|
+
}
|
|
6849
|
+
if (hasEnv) {
|
|
6850
|
+
await fs15.rm(envPath, { force: true });
|
|
6208
6851
|
}
|
|
6209
6852
|
if (hasContainer) {
|
|
6210
6853
|
try {
|
|
6211
|
-
await
|
|
6854
|
+
await fs15.rm(containerPath, { recursive: true, force: true });
|
|
6212
6855
|
} catch (err) {
|
|
6213
6856
|
const code = err.code;
|
|
6214
6857
|
if (code !== "EACCES" && code !== "EPERM") {
|
|
@@ -6234,7 +6877,7 @@ async function runRemove(opts) {
|
|
|
6234
6877
|
`docker-based cleanup of ${containerPath} exited ${exit}. Inspect with \`sudo ls -la ${containerPath}\` and clean manually.`
|
|
6235
6878
|
);
|
|
6236
6879
|
}
|
|
6237
|
-
await
|
|
6880
|
+
await fs15.rm(containerPath, { recursive: true, force: true });
|
|
6238
6881
|
}
|
|
6239
6882
|
}
|
|
6240
6883
|
logger.success(
|
|
@@ -6337,8 +6980,8 @@ import { defineCommand as defineCommand18 } from "citty";
|
|
|
6337
6980
|
import { consola as consola22 } from "consola";
|
|
6338
6981
|
|
|
6339
6982
|
// src/restore/index.ts
|
|
6340
|
-
import { existsSync as
|
|
6341
|
-
import
|
|
6983
|
+
import { existsSync as existsSync12, promises as fs16 } from "fs";
|
|
6984
|
+
import path18 from "path";
|
|
6342
6985
|
import { consola as consola21 } from "consola";
|
|
6343
6986
|
async function runRestore(opts) {
|
|
6344
6987
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
@@ -6346,15 +6989,15 @@ async function runRestore(opts) {
|
|
|
6346
6989
|
info: (msg) => consola21.info(msg),
|
|
6347
6990
|
success: (msg) => consola21.success(msg)
|
|
6348
6991
|
};
|
|
6349
|
-
const backup =
|
|
6350
|
-
if (!
|
|
6992
|
+
const backup = path18.resolve(opts.backupPath);
|
|
6993
|
+
if (!existsSync12(backup)) {
|
|
6351
6994
|
throw new Error(`Backup not found: ${backup}.`);
|
|
6352
6995
|
}
|
|
6353
|
-
const stat = await
|
|
6996
|
+
const stat = await fs16.stat(backup);
|
|
6354
6997
|
if (!stat.isDirectory()) {
|
|
6355
6998
|
throw new Error(`Backup path is not a directory: ${backup}.`);
|
|
6356
6999
|
}
|
|
6357
|
-
const entries = await
|
|
7000
|
+
const entries = await fs16.readdir(backup);
|
|
6358
7001
|
const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
|
|
6359
7002
|
if (ymlFiles.length === 0) {
|
|
6360
7003
|
throw new Error(
|
|
@@ -6368,24 +7011,29 @@ async function runRestore(opts) {
|
|
|
6368
7011
|
}
|
|
6369
7012
|
const ymlFile = ymlFiles[0];
|
|
6370
7013
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
6371
|
-
const containerInBackup =
|
|
6372
|
-
const hasContainer =
|
|
7014
|
+
const containerInBackup = path18.join(backup, "container");
|
|
7015
|
+
const hasContainer = existsSync12(containerInBackup);
|
|
7016
|
+
const envInBackup = path18.join(backup, `${name}.env`);
|
|
7017
|
+
const hasEnv = existsSync12(envInBackup);
|
|
6373
7018
|
const destYml = containerConfigPath(name, home);
|
|
6374
7019
|
const destContainer = containerDir(name, home);
|
|
6375
|
-
if (
|
|
7020
|
+
if (existsSync12(destYml)) {
|
|
6376
7021
|
throw new Error(
|
|
6377
7022
|
`Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
|
|
6378
7023
|
);
|
|
6379
7024
|
}
|
|
6380
|
-
if (hasContainer &&
|
|
7025
|
+
if (hasContainer && existsSync12(destContainer)) {
|
|
6381
7026
|
throw new Error(
|
|
6382
7027
|
`Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
|
|
6383
7028
|
);
|
|
6384
7029
|
}
|
|
6385
|
-
await
|
|
6386
|
-
await
|
|
7030
|
+
await fs16.mkdir(containerConfigsDir(home), { recursive: true });
|
|
7031
|
+
await fs16.copyFile(path18.join(backup, ymlFile), destYml);
|
|
7032
|
+
if (hasEnv) {
|
|
7033
|
+
await fs16.copyFile(envInBackup, containerEnvPath(name, home));
|
|
7034
|
+
}
|
|
6387
7035
|
if (hasContainer) {
|
|
6388
|
-
await
|
|
7036
|
+
await fs16.cp(containerInBackup, destContainer, { recursive: true });
|
|
6389
7037
|
}
|
|
6390
7038
|
logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
|
|
6391
7039
|
logger.info(
|
|
@@ -6643,8 +7291,8 @@ import { defineCommand as defineCommand24 } from "citty";
|
|
|
6643
7291
|
import { consola as consola28 } from "consola";
|
|
6644
7292
|
|
|
6645
7293
|
// src/devcontainer/shell.ts
|
|
6646
|
-
import { existsSync as
|
|
6647
|
-
import
|
|
7294
|
+
import { existsSync as existsSync13 } from "fs";
|
|
7295
|
+
import path19 from "path";
|
|
6648
7296
|
async function runShell(opts) {
|
|
6649
7297
|
assertContainerExists(opts.root);
|
|
6650
7298
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -6667,7 +7315,7 @@ async function runShell(opts) {
|
|
|
6667
7315
|
);
|
|
6668
7316
|
}
|
|
6669
7317
|
function assertContainerExists(root) {
|
|
6670
|
-
if (!
|
|
7318
|
+
if (!existsSync13(path19.join(root, ".devcontainer"))) {
|
|
6671
7319
|
throw new Error(
|
|
6672
7320
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
6673
7321
|
);
|
|
@@ -6879,15 +7527,15 @@ import { defineCommand as defineCommand29 } from "citty";
|
|
|
6879
7527
|
import { consola as consola33 } from "consola";
|
|
6880
7528
|
|
|
6881
7529
|
// src/tunnel/run.ts
|
|
6882
|
-
import { spawn as
|
|
7530
|
+
import { spawn as spawn10 } from "child_process";
|
|
6883
7531
|
import { consola as consola32 } from "consola";
|
|
6884
7532
|
|
|
6885
7533
|
// src/tunnel/resolve.ts
|
|
6886
|
-
import { existsSync as
|
|
6887
|
-
import
|
|
7534
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7535
|
+
import path20 from "path";
|
|
6888
7536
|
async function resolveTunnelTarget(opts) {
|
|
6889
7537
|
const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
|
|
6890
|
-
if (!
|
|
7538
|
+
if (!existsSync14(ymlPath)) {
|
|
6891
7539
|
throw new Error(
|
|
6892
7540
|
`No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
|
|
6893
7541
|
);
|
|
@@ -6895,13 +7543,13 @@ async function resolveTunnelTarget(opts) {
|
|
|
6895
7543
|
const parsed = await readConfig(ymlPath);
|
|
6896
7544
|
const config = parsed.config;
|
|
6897
7545
|
const containerRoot = containerDir(opts.name, opts.monocerosHome);
|
|
6898
|
-
if (!
|
|
7546
|
+
if (!existsSync14(containerRoot)) {
|
|
6899
7547
|
throw new Error(
|
|
6900
7548
|
`Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
|
|
6901
7549
|
);
|
|
6902
7550
|
}
|
|
6903
|
-
const composePath =
|
|
6904
|
-
const isCompose =
|
|
7551
|
+
const composePath = path20.join(containerRoot, ".devcontainer", "compose.yaml");
|
|
7552
|
+
const isCompose = existsSync14(composePath);
|
|
6905
7553
|
const parsedTarget = parseTargetArg(opts.target, config);
|
|
6906
7554
|
const docker = opts.docker ?? defaultDockerExec;
|
|
6907
7555
|
if (isCompose) {
|
|
@@ -6920,23 +7568,41 @@ async function resolveTunnelTarget(opts) {
|
|
|
6920
7568
|
});
|
|
6921
7569
|
}
|
|
6922
7570
|
function parseTargetArg(raw, config) {
|
|
7571
|
+
const colon = raw.indexOf(":");
|
|
7572
|
+
if (colon > 0) {
|
|
7573
|
+
const name = raw.slice(0, colon);
|
|
7574
|
+
const port = Number(raw.slice(colon + 1));
|
|
7575
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
7576
|
+
throw new Error(
|
|
7577
|
+
`Invalid target '${raw}'. Use <service>:<port> with a numeric port (1\u201365535), a bare port number, or a configured service name.`
|
|
7578
|
+
);
|
|
7579
|
+
}
|
|
7580
|
+
findConfiguredService(config, name);
|
|
7581
|
+
return { kind: "service", service: name, port };
|
|
7582
|
+
}
|
|
6923
7583
|
const asNumber = Number(raw);
|
|
6924
7584
|
if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
|
|
6925
7585
|
return { kind: "port", port: asNumber };
|
|
6926
7586
|
}
|
|
6927
|
-
const
|
|
6928
|
-
if (
|
|
6929
|
-
const candidates = knownServices().join(", ");
|
|
7587
|
+
const match = findConfiguredService(config, raw);
|
|
7588
|
+
if (match.port === void 0) {
|
|
6930
7589
|
throw new Error(
|
|
6931
|
-
`
|
|
7590
|
+
`Service '${raw}' declares no port, so tunnel can't know what to forward. Add \`port: <n>\` to the service in the yml and re-apply, or pass one explicitly: \`monoceros tunnel <name> ${raw}:<port>\`.`
|
|
6932
7591
|
);
|
|
6933
7592
|
}
|
|
6934
|
-
|
|
7593
|
+
return { kind: "service", service: raw, port: match.port };
|
|
7594
|
+
}
|
|
7595
|
+
function findConfiguredService(config, name) {
|
|
7596
|
+
const services = config.services.map(resolveService);
|
|
7597
|
+
const match = services.find((s) => s.name === name);
|
|
7598
|
+
if (!match) {
|
|
7599
|
+
const names = services.map((s) => s.name);
|
|
7600
|
+
const list = names.length > 0 ? names.join(", ") : "(none configured)";
|
|
6935
7601
|
throw new Error(
|
|
6936
|
-
`Service '${
|
|
7602
|
+
`Service '${name}' is not configured in this container's yml. Configured services: ${list}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
|
|
6937
7603
|
);
|
|
6938
7604
|
}
|
|
6939
|
-
return
|
|
7605
|
+
return match;
|
|
6940
7606
|
}
|
|
6941
7607
|
function resolveCompose2(args) {
|
|
6942
7608
|
const network = `${composeProjectName(args.containerRoot)}_default`;
|
|
@@ -6945,7 +7611,7 @@ function resolveCompose2(args) {
|
|
|
6945
7611
|
network,
|
|
6946
7612
|
targetHost: args.parsedTarget.service,
|
|
6947
7613
|
internalPort: args.parsedTarget.port,
|
|
6948
|
-
display: `${args.name}/${args.parsedTarget.service}`
|
|
7614
|
+
display: `${args.name}/${args.parsedTarget.service}:${args.parsedTarget.port}`
|
|
6949
7615
|
};
|
|
6950
7616
|
}
|
|
6951
7617
|
return {
|
|
@@ -7104,7 +7770,7 @@ function formatLocalPortHeldError(port, address, result) {
|
|
|
7104
7770
|
// src/tunnel/run.ts
|
|
7105
7771
|
var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
|
|
7106
7772
|
var defaultDockerSpawn = (args) => {
|
|
7107
|
-
const child =
|
|
7773
|
+
const child = spawn10("docker", args, {
|
|
7108
7774
|
stdio: "inherit"
|
|
7109
7775
|
});
|
|
7110
7776
|
const exited = new Promise((resolve, reject) => {
|
|
@@ -7227,7 +7893,7 @@ var tunnelCommand = defineCommand29({
|
|
|
7227
7893
|
},
|
|
7228
7894
|
target: {
|
|
7229
7895
|
type: "positional",
|
|
7230
|
-
description: "Service name from the container yml (e.g. `postgres`)
|
|
7896
|
+
description: "Service name from the container yml (e.g. `postgres`), `service:port` for an explicit in-container port (e.g. `rustfs:9001`), or a bare in-container port number \u2192 workspace (e.g. `8080`).",
|
|
7231
7897
|
required: true
|
|
7232
7898
|
},
|
|
7233
7899
|
"local-port": {
|