@getmonoceros/workbench 1.6.11 → 1.7.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 +1121 -304
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
- package/templates/monoceros-config.sample.yml +76 -38
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 path15 = [];
|
|
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
|
+
path15.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
|
+
path15.push(tok);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
271
271
|
}
|
|
272
|
-
return { path:
|
|
272
|
+
return { path: path15, cmd: cursor };
|
|
273
273
|
}
|
|
274
274
|
async function maybeRenderHelp(argv, main2) {
|
|
275
275
|
const hit = detectHelpRequest(argv, main2);
|
|
@@ -301,14 +301,14 @@ function getInnerArgs() {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// src/main.ts
|
|
304
|
-
import { defineCommand as
|
|
304
|
+
import { defineCommand as defineCommand28 } from "citty";
|
|
305
305
|
|
|
306
306
|
// src/commands/add-apt-packages.ts
|
|
307
307
|
import { defineCommand } from "citty";
|
|
308
308
|
import { consola as consola2 } from "consola";
|
|
309
309
|
|
|
310
310
|
// src/modify/index.ts
|
|
311
|
-
import { promises as
|
|
311
|
+
import { promises as fs6 } from "fs";
|
|
312
312
|
import { consola } from "consola";
|
|
313
313
|
import { createPatch } from "diff";
|
|
314
314
|
|
|
@@ -392,6 +392,16 @@ var RepoEntrySchema = z.object({
|
|
|
392
392
|
// parse time — see ADR 0006.
|
|
393
393
|
provider: z.enum(PROVIDER_VALUES).optional()
|
|
394
394
|
});
|
|
395
|
+
var PortEntrySchema = z.union([
|
|
396
|
+
z.number().int().min(1, "Port must be \u2265 1.").max(65535, "Port must be \u2264 65535."),
|
|
397
|
+
z.object({
|
|
398
|
+
port: z.number().int().min(1, "Port must be \u2265 1.").max(65535, "Port must be \u2264 65535.")
|
|
399
|
+
})
|
|
400
|
+
]);
|
|
401
|
+
var RoutingSchema = z.object({
|
|
402
|
+
ports: z.array(PortEntrySchema).default([]),
|
|
403
|
+
vscodeAutoForward: z.boolean().optional()
|
|
404
|
+
});
|
|
395
405
|
var ExternalServicesSchema = z.object({
|
|
396
406
|
postgres: z.string().regex(
|
|
397
407
|
POSTGRES_URL_RE,
|
|
@@ -420,11 +430,15 @@ var SolutionConfigSchema = z.object({
|
|
|
420
430
|
).default([]),
|
|
421
431
|
services: z.array(z.string().min(1)).default([]),
|
|
422
432
|
repos: z.array(RepoEntrySchema).default([]),
|
|
433
|
+
routing: RoutingSchema.optional(),
|
|
423
434
|
externalServices: ExternalServicesSchema.default({}),
|
|
424
435
|
git: z.object({
|
|
425
436
|
user: GitUserSchema.optional()
|
|
426
437
|
}).optional()
|
|
427
438
|
});
|
|
439
|
+
function portNumber(entry2) {
|
|
440
|
+
return typeof entry2 === "number" ? entry2 : entry2.port;
|
|
441
|
+
}
|
|
428
442
|
function validateConfig(input) {
|
|
429
443
|
const result = SolutionConfigSchema.safeParse(input);
|
|
430
444
|
if (!result.success) {
|
|
@@ -553,6 +567,343 @@ function prettyPath(p) {
|
|
|
553
567
|
return p;
|
|
554
568
|
}
|
|
555
569
|
|
|
570
|
+
// src/config/global.ts
|
|
571
|
+
import { promises as fs2 } from "fs";
|
|
572
|
+
import { z as z2 } from "zod";
|
|
573
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
574
|
+
var SCHEMA_VERSION = 1;
|
|
575
|
+
var MonocerosConfigSchema = z2.object({
|
|
576
|
+
schemaVersion: z2.literal(SCHEMA_VERSION),
|
|
577
|
+
// .nullish() (= .optional().nullable()) on defaults so the shipped
|
|
578
|
+
// sample yml — where `defaults:` is uncommented but every sub-block
|
|
579
|
+
// is commented out — parses cleanly. YAML produces `defaults: null`
|
|
580
|
+
// in that case; without .nullish() the schema would reject it and
|
|
581
|
+
// we'd be back to forcing builders to comment-juggle three lines.
|
|
582
|
+
defaults: z2.object({
|
|
583
|
+
// .nullish() (not just .optional()) so the sample yml can leave
|
|
584
|
+
// `git:` uncommented as a category marker — YAML produces
|
|
585
|
+
// `git: null` for an empty mapping, which zod's plain
|
|
586
|
+
// `.optional()` would reject.
|
|
587
|
+
git: z2.object({
|
|
588
|
+
user: GitUserSchema.optional()
|
|
589
|
+
}).nullish(),
|
|
590
|
+
// .nullish() for the same reason as `git` — the sample keeps
|
|
591
|
+
// `features:` uncommented as a category marker.
|
|
592
|
+
features: z2.record(
|
|
593
|
+
z2.string().regex(
|
|
594
|
+
REGEX.featureRef,
|
|
595
|
+
"Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
|
|
596
|
+
),
|
|
597
|
+
z2.record(z2.string(), FeatureOptionValueSchema)
|
|
598
|
+
).nullish()
|
|
599
|
+
}).nullish(),
|
|
600
|
+
// Machine-global routing settings — one Traefik per builder, so
|
|
601
|
+
// host-port and similar live here rather than in any container yml.
|
|
602
|
+
// See ADR 0007.
|
|
603
|
+
routing: z2.object({
|
|
604
|
+
hostPort: z2.number().int().min(1).max(65535).optional().describe(
|
|
605
|
+
"Host port the Traefik singleton binds. Default 80. Set this when 80 is held by another service on your machine \u2014 URLs then become http://<name>.localhost:<port>/."
|
|
606
|
+
)
|
|
607
|
+
}).nullish()
|
|
608
|
+
});
|
|
609
|
+
async function readMonocerosConfig(opts = {}) {
|
|
610
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
611
|
+
const filePath = monocerosConfigPath(home);
|
|
612
|
+
let text;
|
|
613
|
+
try {
|
|
614
|
+
text = await fs2.readFile(filePath, "utf8");
|
|
615
|
+
} catch {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
const doc = parseDocument2(text, { prettyErrors: true });
|
|
619
|
+
if (doc.errors.length > 0) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
625
|
+
if (!result.success) {
|
|
626
|
+
const issues = result.error.issues.map((issue) => {
|
|
627
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
628
|
+
return ` - ${where}: ${issue.message}`;
|
|
629
|
+
}).join("\n");
|
|
630
|
+
throw new Error(
|
|
631
|
+
`Invalid ${filePath}:
|
|
632
|
+
${issues}
|
|
633
|
+
|
|
634
|
+
See ${filePath.replace(
|
|
635
|
+
/\.yml$/,
|
|
636
|
+
".sample.yml"
|
|
637
|
+
)} for a valid example.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return result.data;
|
|
641
|
+
}
|
|
642
|
+
var DEFAULT_PROXY_HOST_PORT = 80;
|
|
643
|
+
function proxyHostPort(config) {
|
|
644
|
+
return config?.routing?.hostPort ?? DEFAULT_PROXY_HOST_PORT;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/proxy/index.ts
|
|
648
|
+
import { spawn } from "child_process";
|
|
649
|
+
import { promises as fs3 } from "fs";
|
|
650
|
+
import path2 from "path";
|
|
651
|
+
var PROXY_CONTAINER_NAME = "monoceros-proxy";
|
|
652
|
+
var PROXY_NETWORK_NAME = "monoceros-proxy";
|
|
653
|
+
var TRAEFIK_IMAGE = "traefik:v3.3";
|
|
654
|
+
var defaultDockerExec = (args) => {
|
|
655
|
+
return new Promise((resolve, reject) => {
|
|
656
|
+
const child = spawn("docker", args, {
|
|
657
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
658
|
+
});
|
|
659
|
+
let stdout = "";
|
|
660
|
+
let stderr = "";
|
|
661
|
+
child.stdout.on("data", (chunk) => {
|
|
662
|
+
stdout += chunk.toString();
|
|
663
|
+
});
|
|
664
|
+
child.stderr.on("data", (chunk) => {
|
|
665
|
+
stderr += chunk.toString();
|
|
666
|
+
});
|
|
667
|
+
child.on("error", reject);
|
|
668
|
+
child.on(
|
|
669
|
+
"exit",
|
|
670
|
+
(code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
var realDocker = defaultDockerExec;
|
|
675
|
+
function proxyDynamicDir(home) {
|
|
676
|
+
return path2.join(home ?? monocerosHome(), "traefik", "dynamic");
|
|
677
|
+
}
|
|
678
|
+
async function ensureProxy(opts = {}) {
|
|
679
|
+
const docker = opts.docker ?? realDocker;
|
|
680
|
+
const dyn = proxyDynamicDir(opts.monocerosHome);
|
|
681
|
+
await fs3.mkdir(dyn, { recursive: true });
|
|
682
|
+
const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
|
|
683
|
+
if (netInspect.exitCode !== 0) {
|
|
684
|
+
const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
|
|
685
|
+
if (create.exitCode !== 0) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`Could not create docker network ${PROXY_NETWORK_NAME}: ${create.stderr.trim() || `exit ${create.exitCode}`}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const state = await docker([
|
|
692
|
+
"inspect",
|
|
693
|
+
"--format",
|
|
694
|
+
"{{.State.Running}}",
|
|
695
|
+
PROXY_CONTAINER_NAME
|
|
696
|
+
]);
|
|
697
|
+
if (state.exitCode === 0) {
|
|
698
|
+
if (state.stdout.trim() === "true") return;
|
|
699
|
+
const start = await docker(["start", PROXY_CONTAINER_NAME]);
|
|
700
|
+
if (start.exitCode !== 0) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`Could not start existing ${PROXY_CONTAINER_NAME} container: ${start.stderr.trim() || `exit ${start.exitCode}`}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const hostPort = opts.hostPort ?? 80;
|
|
708
|
+
const run = await docker([
|
|
709
|
+
"run",
|
|
710
|
+
"-d",
|
|
711
|
+
"--name",
|
|
712
|
+
PROXY_CONTAINER_NAME,
|
|
713
|
+
"--network",
|
|
714
|
+
PROXY_NETWORK_NAME,
|
|
715
|
+
"-p",
|
|
716
|
+
`${hostPort}:80`,
|
|
717
|
+
"-v",
|
|
718
|
+
`${dyn}:/etc/traefik/dynamic:ro`,
|
|
719
|
+
"--label",
|
|
720
|
+
"monoceros.role=proxy",
|
|
721
|
+
TRAEFIK_IMAGE,
|
|
722
|
+
"--entrypoints.web.address=:80",
|
|
723
|
+
"--providers.file.directory=/etc/traefik/dynamic",
|
|
724
|
+
"--providers.file.watch=true",
|
|
725
|
+
"--providers.docker=false",
|
|
726
|
+
"--api.dashboard=false",
|
|
727
|
+
"--log.level=INFO"
|
|
728
|
+
]);
|
|
729
|
+
if (run.exitCode !== 0) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`Could not start ${PROXY_CONTAINER_NAME}: ${run.stderr.trim() || `exit ${run.exitCode}`}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
opts.logger?.info(
|
|
735
|
+
`Started ${PROXY_CONTAINER_NAME} (Traefik on :${hostPort}).`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
async function maybeStopProxy(opts = {}) {
|
|
739
|
+
const docker = opts.docker ?? realDocker;
|
|
740
|
+
const logger = opts.logger;
|
|
741
|
+
const inspect = await docker([
|
|
742
|
+
"network",
|
|
743
|
+
"inspect",
|
|
744
|
+
PROXY_NETWORK_NAME,
|
|
745
|
+
"--format",
|
|
746
|
+
"{{range $k, $v := .Containers}}{{$v.Name}}\n{{end}}"
|
|
747
|
+
]);
|
|
748
|
+
if (inspect.exitCode !== 0) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const others = inspect.stdout.split("\n").map((n) => n.trim()).filter((n) => n.length > 0 && n !== PROXY_CONTAINER_NAME);
|
|
752
|
+
if (others.length > 0) return;
|
|
753
|
+
await docker(["rm", "-f", PROXY_CONTAINER_NAME]);
|
|
754
|
+
const netRm = await docker(["network", "rm", PROXY_NETWORK_NAME]);
|
|
755
|
+
if (netRm.exitCode !== 0) {
|
|
756
|
+
logger?.warn?.(
|
|
757
|
+
`Could not remove docker network ${PROXY_NETWORK_NAME}: ${netRm.stderr.trim() || `exit ${netRm.exitCode}`}`
|
|
758
|
+
);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
logger?.info(
|
|
762
|
+
`Stopped ${PROXY_CONTAINER_NAME} (no dev-containers with ports left).`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/proxy/dynamic.ts
|
|
767
|
+
import { promises as fs4 } from "fs";
|
|
768
|
+
import path3 from "path";
|
|
769
|
+
async function writeDynamicConfig(name, ports, opts = {}) {
|
|
770
|
+
if (ports.length === 0) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`writeDynamicConfig requires at least one port. For empty port lists, call removeDynamicConfig(${JSON.stringify(name)}).`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const dir = proxyDynamicDir(opts.monocerosHome);
|
|
776
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
777
|
+
const file = path3.join(dir, `${name}.yml`);
|
|
778
|
+
await fs4.writeFile(file, renderDynamicConfig(name, ports), "utf8");
|
|
779
|
+
return file;
|
|
780
|
+
}
|
|
781
|
+
async function removeDynamicConfig(name, opts = {}) {
|
|
782
|
+
const file = path3.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
|
|
783
|
+
await fs4.rm(file, { force: true });
|
|
784
|
+
}
|
|
785
|
+
function renderDynamicConfig(name, ports) {
|
|
786
|
+
const lines = [];
|
|
787
|
+
lines.push("# Generated by Monoceros \u2014 do not edit by hand.");
|
|
788
|
+
lines.push(`# Container: ${name}`);
|
|
789
|
+
lines.push(`# Ports: ${ports.join(", ")}`);
|
|
790
|
+
lines.push("# Traefik file-provider re-reads this on change (~100 ms);");
|
|
791
|
+
lines.push(
|
|
792
|
+
"# to change routing, edit container-configs/" + name + ".yml or use"
|
|
793
|
+
);
|
|
794
|
+
lines.push("# `monoceros add-port` / `monoceros remove-port`.");
|
|
795
|
+
lines.push("http:");
|
|
796
|
+
lines.push(" routers:");
|
|
797
|
+
ports.forEach((port, idx) => {
|
|
798
|
+
const router = `${name}-${port}`;
|
|
799
|
+
const hostExplicit = `${name}-${port}.localhost`;
|
|
800
|
+
const rule = idx === 0 ? `"Host(\`${name}.localhost\`) || Host(\`${hostExplicit}\`)"` : `"Host(\`${hostExplicit}\`)"`;
|
|
801
|
+
lines.push(` ${router}:`);
|
|
802
|
+
lines.push(` rule: ${rule}`);
|
|
803
|
+
lines.push(` service: ${router}`);
|
|
804
|
+
lines.push(" entryPoints:");
|
|
805
|
+
lines.push(" - web");
|
|
806
|
+
});
|
|
807
|
+
lines.push(" services:");
|
|
808
|
+
for (const port of ports) {
|
|
809
|
+
const svc = `${name}-${port}`;
|
|
810
|
+
lines.push(` ${svc}:`);
|
|
811
|
+
lines.push(" loadBalancer:");
|
|
812
|
+
lines.push(" servers:");
|
|
813
|
+
lines.push(` - url: "http://${name}:${port}"`);
|
|
814
|
+
}
|
|
815
|
+
return lines.join("\n") + "\n";
|
|
816
|
+
}
|
|
817
|
+
function proxyUrlsFor(name, ports, hostPort = 80) {
|
|
818
|
+
const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
|
|
819
|
+
return ports.map((port, idx) => ({
|
|
820
|
+
port,
|
|
821
|
+
url: `http://${name}-${port}.localhost${portSuffix}`,
|
|
822
|
+
isDefault: idx === 0
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/proxy/port-check.ts
|
|
827
|
+
import { createServer } from "net";
|
|
828
|
+
var realPortProbe = (port) => {
|
|
829
|
+
return new Promise((resolve) => {
|
|
830
|
+
const server = createServer();
|
|
831
|
+
server.unref();
|
|
832
|
+
server.once("error", (err) => {
|
|
833
|
+
resolve({
|
|
834
|
+
ok: false,
|
|
835
|
+
code: err.code ?? "UNKNOWN",
|
|
836
|
+
message: err.message
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
server.once("listening", () => {
|
|
840
|
+
server.close(() => resolve({ ok: true }));
|
|
841
|
+
});
|
|
842
|
+
server.listen(port, "0.0.0.0");
|
|
843
|
+
});
|
|
844
|
+
};
|
|
845
|
+
async function preflightHostPort(hostPort, opts = {}) {
|
|
846
|
+
const docker = opts.docker ?? defaultDockerExec;
|
|
847
|
+
const inspect = await docker([
|
|
848
|
+
"inspect",
|
|
849
|
+
"--format",
|
|
850
|
+
"{{.State.Running}}",
|
|
851
|
+
PROXY_CONTAINER_NAME
|
|
852
|
+
]);
|
|
853
|
+
if (inspect.exitCode === 0 && inspect.stdout.trim() === "true") {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const probe = opts.portProbe ?? realPortProbe;
|
|
857
|
+
const result = await probe(hostPort);
|
|
858
|
+
if (result.ok) return;
|
|
859
|
+
throw new Error(
|
|
860
|
+
formatHostPortHeldError(hostPort, result.code, result.message)
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
function formatHostPortHeldError(hostPort, code, systemMessage) {
|
|
864
|
+
const isInUse = code === "EADDRINUSE";
|
|
865
|
+
const lines = [];
|
|
866
|
+
if (isInUse) {
|
|
867
|
+
lines.push(`Host port ${hostPort} is already in use by another process.`);
|
|
868
|
+
lines.push("");
|
|
869
|
+
lines.push(`Monoceros needs that port for its Traefik proxy (the thing`);
|
|
870
|
+
lines.push(`that routes <name>.localhost / <name>-<port>.localhost to`);
|
|
871
|
+
lines.push(`your dev-container). Two ways out:`);
|
|
872
|
+
lines.push("");
|
|
873
|
+
lines.push(" 1) Recommended: free the port.");
|
|
874
|
+
lines.push(" Identify the process holding it:");
|
|
875
|
+
lines.push(` sudo lsof -iTCP:${hostPort} -sTCP:LISTEN -n -P`);
|
|
876
|
+
lines.push(` # or: sudo ss -tlnp | grep ":${hostPort}\\b"`);
|
|
877
|
+
lines.push(" Then stop or reconfigure that service.");
|
|
878
|
+
lines.push("");
|
|
879
|
+
lines.push(" 2) Move Monoceros off port 80. Edit (or create)");
|
|
880
|
+
lines.push(" ~/.monoceros/monoceros-config.yml and add:");
|
|
881
|
+
lines.push("");
|
|
882
|
+
lines.push(" schemaVersion: 1");
|
|
883
|
+
lines.push(" routing:");
|
|
884
|
+
lines.push(" hostPort: 8080 # any free port");
|
|
885
|
+
lines.push("");
|
|
886
|
+
lines.push(" URLs will become http://<name>.localhost:8080/.");
|
|
887
|
+
lines.push("");
|
|
888
|
+
lines.push(`Aborting \u2014 re-run after the conflict is resolved.`);
|
|
889
|
+
} else {
|
|
890
|
+
lines.push(`Cannot bind host port ${hostPort}: ${systemMessage}`);
|
|
891
|
+
lines.push("");
|
|
892
|
+
if (code === "EACCES") {
|
|
893
|
+
lines.push(`Port ${hostPort} is a privileged port (<1024) and your`);
|
|
894
|
+
lines.push(`current Docker setup can't bind it. For rootful Docker`);
|
|
895
|
+
lines.push(`(what Monoceros requires) this should normally work \u2014`);
|
|
896
|
+
lines.push(`check that the docker daemon is running as root.`);
|
|
897
|
+
lines.push("");
|
|
898
|
+
}
|
|
899
|
+
lines.push("You can also move Monoceros off this port by setting");
|
|
900
|
+
lines.push("`routing.hostPort` in ~/.monoceros/monoceros-config.yml.");
|
|
901
|
+
lines.push("");
|
|
902
|
+
lines.push(`Aborting \u2014 re-run after the issue is resolved.`);
|
|
903
|
+
}
|
|
904
|
+
return lines.join("\n");
|
|
905
|
+
}
|
|
906
|
+
|
|
556
907
|
// src/create/catalog.ts
|
|
557
908
|
var DEFAULT_BASE_IMAGE = "ghcr.io/getmonoceros/monoceros-runtime:1";
|
|
558
909
|
var override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
|
|
@@ -610,8 +961,8 @@ function knownServices() {
|
|
|
610
961
|
}
|
|
611
962
|
|
|
612
963
|
// src/create/scaffold.ts
|
|
613
|
-
import { existsSync as existsSync2, readFileSync, promises as
|
|
614
|
-
import
|
|
964
|
+
import { existsSync as existsSync2, readFileSync, promises as fs5 } from "fs";
|
|
965
|
+
import path4 from "path";
|
|
615
966
|
|
|
616
967
|
// src/util/ref.ts
|
|
617
968
|
var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
|
|
@@ -732,6 +1083,7 @@ function normalizeOptions(opts) {
|
|
|
732
1083
|
const repos = opts.repos ? Array.from(
|
|
733
1084
|
new Map(opts.repos.map((r) => [`${r.url}${r.path}`, r])).values()
|
|
734
1085
|
) : void 0;
|
|
1086
|
+
const ports = opts.ports ? [...new Set(opts.ports)] : void 0;
|
|
735
1087
|
return {
|
|
736
1088
|
name: opts.name,
|
|
737
1089
|
languages,
|
|
@@ -740,7 +1092,9 @@ function normalizeOptions(opts) {
|
|
|
740
1092
|
...aptPackages.length > 0 ? { aptPackages } : {},
|
|
741
1093
|
...features && Object.keys(features).length > 0 ? { features } : {},
|
|
742
1094
|
...installUrls && installUrls.length > 0 ? { installUrls } : {},
|
|
743
|
-
...repos && repos.length > 0 ? { repos } : {}
|
|
1095
|
+
...repos && repos.length > 0 ? { repos } : {},
|
|
1096
|
+
...ports && ports.length > 0 ? { ports } : {},
|
|
1097
|
+
...opts.vscodeAutoForward !== void 0 ? { vscodeAutoForward: opts.vscodeAutoForward } : {}
|
|
744
1098
|
};
|
|
745
1099
|
}
|
|
746
1100
|
function needsCompose(opts) {
|
|
@@ -779,7 +1133,7 @@ function resolveFeatures(opts) {
|
|
|
779
1133
|
if (match) {
|
|
780
1134
|
const name = match.name;
|
|
781
1135
|
const checkout = workbenchCheckoutRoot();
|
|
782
|
-
const localSourceDir = checkout ?
|
|
1136
|
+
const localSourceDir = checkout ? path4.join(checkout, "images", "features", name) : null;
|
|
783
1137
|
if (localSourceDir && existsSync2(localSourceDir)) {
|
|
784
1138
|
const { paths, files } = readPersistentHomeEntries(localSourceDir);
|
|
785
1139
|
resolved.push({
|
|
@@ -804,7 +1158,7 @@ function resolveFeatures(opts) {
|
|
|
804
1158
|
return resolved;
|
|
805
1159
|
}
|
|
806
1160
|
function readPersistentHomeEntries(localSourceDir) {
|
|
807
|
-
const manifestPath =
|
|
1161
|
+
const manifestPath = path4.join(localSourceDir, "devcontainer-feature.json");
|
|
808
1162
|
try {
|
|
809
1163
|
const text = readFileSync(manifestPath, "utf8");
|
|
810
1164
|
const parsed = JSON.parse(text);
|
|
@@ -866,6 +1220,16 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
866
1220
|
);
|
|
867
1221
|
}
|
|
868
1222
|
}
|
|
1223
|
+
const ports = opts.ports ?? [];
|
|
1224
|
+
const customizationsField = ports.length > 0 ? {
|
|
1225
|
+
customizations: {
|
|
1226
|
+
vscode: {
|
|
1227
|
+
settings: {
|
|
1228
|
+
"remote.autoForwardPorts": opts.vscodeAutoForward ?? false
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
} : void 0;
|
|
869
1233
|
if (needsCompose(opts)) {
|
|
870
1234
|
return {
|
|
871
1235
|
name: opts.name,
|
|
@@ -874,34 +1238,49 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
|
|
|
874
1238
|
...opts.services.length > 0 ? { runServices: opts.services } : {},
|
|
875
1239
|
workspaceFolder: `/workspaces/${opts.name}`,
|
|
876
1240
|
remoteUser: "node",
|
|
877
|
-
forwardPorts:
|
|
1241
|
+
forwardPorts: ports,
|
|
878
1242
|
postCreateCommand: ".devcontainer/post-create.sh",
|
|
879
|
-
...featuresField ?? {}
|
|
1243
|
+
...featuresField ?? {},
|
|
1244
|
+
...customizationsField ?? {}
|
|
880
1245
|
};
|
|
881
1246
|
}
|
|
882
1247
|
const mounts = [...homeMounts];
|
|
883
1248
|
const mountsField = mounts.length > 0 ? { mounts } : {};
|
|
884
1249
|
const workspaceMountField = {};
|
|
1250
|
+
const runArgs = ["--cap-add=NET_ADMIN"];
|
|
1251
|
+
if (ports.length > 0) {
|
|
1252
|
+
runArgs.push("--network=monoceros-proxy");
|
|
1253
|
+
runArgs.push(`--network-alias=${opts.name}`);
|
|
1254
|
+
}
|
|
885
1255
|
return {
|
|
886
1256
|
name: opts.name,
|
|
887
1257
|
image: BASE_IMAGE,
|
|
888
1258
|
remoteUser: "node",
|
|
889
1259
|
...workspaceMountField,
|
|
890
1260
|
...mountsField,
|
|
891
|
-
runArgs
|
|
892
|
-
forwardPorts:
|
|
1261
|
+
runArgs,
|
|
1262
|
+
forwardPorts: ports,
|
|
893
1263
|
postCreateCommand: ".devcontainer/post-create.sh",
|
|
894
|
-
...featuresField ?? {}
|
|
1264
|
+
...featuresField ?? {},
|
|
1265
|
+
...customizationsField ?? {}
|
|
895
1266
|
};
|
|
896
1267
|
}
|
|
897
1268
|
function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
898
1269
|
void dockerMode;
|
|
1270
|
+
const hasPorts = (opts.ports?.length ?? 0) > 0;
|
|
899
1271
|
const lines = ["services:"];
|
|
900
1272
|
lines.push(" workspace:");
|
|
901
1273
|
lines.push(` image: ${BASE_IMAGE}`);
|
|
902
1274
|
lines.push(" command: 'sleep infinity'");
|
|
903
1275
|
lines.push(" cap_add:");
|
|
904
1276
|
lines.push(" - NET_ADMIN");
|
|
1277
|
+
if (hasPorts) {
|
|
1278
|
+
lines.push(" networks:");
|
|
1279
|
+
lines.push(" default: {}");
|
|
1280
|
+
lines.push(" monoceros-proxy:");
|
|
1281
|
+
lines.push(" aliases:");
|
|
1282
|
+
lines.push(` - ${opts.name}`);
|
|
1283
|
+
}
|
|
905
1284
|
lines.push(" volumes:");
|
|
906
1285
|
lines.push(` - ..:/workspaces/${opts.name}:cached`);
|
|
907
1286
|
const resolvedFeatures = resolveFeatures(opts);
|
|
@@ -930,6 +1309,11 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
|
|
|
930
1309
|
lines.push(` - ../data/${def.id}:${def.dataMount}`);
|
|
931
1310
|
}
|
|
932
1311
|
}
|
|
1312
|
+
if (hasPorts) {
|
|
1313
|
+
lines.push("networks:");
|
|
1314
|
+
lines.push(" monoceros-proxy:");
|
|
1315
|
+
lines.push(" external: true");
|
|
1316
|
+
}
|
|
933
1317
|
return lines.join("\n") + "\n";
|
|
934
1318
|
}
|
|
935
1319
|
function buildCodeWorkspaceJson(opts) {
|
|
@@ -1039,77 +1423,77 @@ function buildPostCreateScript(opts) {
|
|
|
1039
1423
|
return lines.join("\n") + "\n";
|
|
1040
1424
|
}
|
|
1041
1425
|
async function writePostCreateScript(devcontainerDir, opts) {
|
|
1042
|
-
const dest =
|
|
1043
|
-
await
|
|
1044
|
-
await
|
|
1426
|
+
const dest = path4.join(devcontainerDir, "post-create.sh");
|
|
1427
|
+
await fs5.writeFile(dest, buildPostCreateScript(opts));
|
|
1428
|
+
await fs5.chmod(dest, 493);
|
|
1045
1429
|
}
|
|
1046
1430
|
async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
1047
1431
|
const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
|
|
1048
|
-
const devcontainerDir =
|
|
1049
|
-
const monocerosDir =
|
|
1050
|
-
const projectsDir =
|
|
1051
|
-
const homeDir =
|
|
1052
|
-
const dataDir =
|
|
1053
|
-
await
|
|
1054
|
-
await
|
|
1055
|
-
await
|
|
1056
|
-
await
|
|
1432
|
+
const devcontainerDir = path4.join(targetDir, ".devcontainer");
|
|
1433
|
+
const monocerosDir = path4.join(targetDir, ".monoceros");
|
|
1434
|
+
const projectsDir = path4.join(targetDir, "projects");
|
|
1435
|
+
const homeDir = path4.join(targetDir, "home");
|
|
1436
|
+
const dataDir = path4.join(targetDir, "data");
|
|
1437
|
+
await fs5.mkdir(devcontainerDir, { recursive: true });
|
|
1438
|
+
await fs5.mkdir(monocerosDir, { recursive: true });
|
|
1439
|
+
await fs5.mkdir(projectsDir, { recursive: true });
|
|
1440
|
+
await fs5.mkdir(homeDir, { recursive: true });
|
|
1057
1441
|
if (needsCompose(opts)) {
|
|
1058
|
-
await
|
|
1442
|
+
await fs5.mkdir(dataDir, { recursive: true });
|
|
1059
1443
|
for (const svcId of opts.services) {
|
|
1060
1444
|
const def = SERVICE_CATALOG[svcId];
|
|
1061
1445
|
if (def?.dataMount) {
|
|
1062
|
-
await
|
|
1446
|
+
await fs5.mkdir(path4.join(dataDir, def.id), { recursive: true });
|
|
1063
1447
|
}
|
|
1064
1448
|
}
|
|
1065
1449
|
}
|
|
1066
|
-
const containerGitignore =
|
|
1067
|
-
await
|
|
1068
|
-
const gitkeep =
|
|
1450
|
+
const containerGitignore = path4.join(targetDir, ".gitignore");
|
|
1451
|
+
await fs5.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
|
|
1452
|
+
const gitkeep = path4.join(projectsDir, ".gitkeep");
|
|
1069
1453
|
if (!existsSync2(gitkeep)) {
|
|
1070
|
-
await
|
|
1454
|
+
await fs5.writeFile(gitkeep, "");
|
|
1071
1455
|
}
|
|
1072
|
-
await
|
|
1073
|
-
|
|
1456
|
+
await fs5.writeFile(
|
|
1457
|
+
path4.join(monocerosDir, ".gitignore"),
|
|
1074
1458
|
"git-credentials*\ngitconfig\n"
|
|
1075
1459
|
);
|
|
1076
1460
|
const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
|
|
1077
|
-
await
|
|
1078
|
-
|
|
1461
|
+
await fs5.writeFile(
|
|
1462
|
+
path4.join(devcontainerDir, "devcontainer.json"),
|
|
1079
1463
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
1080
1464
|
);
|
|
1081
|
-
const featuresDir =
|
|
1465
|
+
const featuresDir = path4.join(devcontainerDir, "features");
|
|
1082
1466
|
if (existsSync2(featuresDir)) {
|
|
1083
|
-
await
|
|
1467
|
+
await fs5.rm(featuresDir, { recursive: true, force: true });
|
|
1084
1468
|
}
|
|
1085
1469
|
const resolvedFeatures = resolveFeatures(opts);
|
|
1086
1470
|
for (const f of resolvedFeatures) {
|
|
1087
1471
|
if (!f.localSourceDir || !f.localName) continue;
|
|
1088
|
-
const dest =
|
|
1089
|
-
await
|
|
1090
|
-
await
|
|
1472
|
+
const dest = path4.join(featuresDir, f.localName);
|
|
1473
|
+
await fs5.mkdir(dest, { recursive: true });
|
|
1474
|
+
await fs5.cp(f.localSourceDir, dest, { recursive: true });
|
|
1091
1475
|
}
|
|
1092
1476
|
for (const f of resolvedFeatures) {
|
|
1093
1477
|
for (const sub of f.persistentHomePaths) {
|
|
1094
|
-
await
|
|
1478
|
+
await fs5.mkdir(path4.join(homeDir, sub), { recursive: true });
|
|
1095
1479
|
}
|
|
1096
1480
|
for (const entry2 of f.persistentHomeFiles) {
|
|
1097
|
-
const filePath =
|
|
1098
|
-
await
|
|
1481
|
+
const filePath = path4.join(homeDir, entry2.path);
|
|
1482
|
+
await fs5.mkdir(path4.dirname(filePath), { recursive: true });
|
|
1099
1483
|
if (!existsSync2(filePath)) {
|
|
1100
|
-
await
|
|
1484
|
+
await fs5.writeFile(filePath, entry2.initialContent);
|
|
1101
1485
|
}
|
|
1102
1486
|
}
|
|
1103
1487
|
}
|
|
1104
1488
|
await writePostCreateScript(devcontainerDir, opts);
|
|
1105
|
-
const composePath =
|
|
1489
|
+
const composePath = path4.join(devcontainerDir, "compose.yaml");
|
|
1106
1490
|
if (needsCompose(opts)) {
|
|
1107
|
-
await
|
|
1491
|
+
await fs5.writeFile(composePath, buildComposeYaml(opts, dockerMode));
|
|
1108
1492
|
} else if (existsSync2(composePath)) {
|
|
1109
|
-
await
|
|
1493
|
+
await fs5.rm(composePath);
|
|
1110
1494
|
}
|
|
1111
|
-
await
|
|
1112
|
-
|
|
1495
|
+
await fs5.writeFile(
|
|
1496
|
+
path4.join(targetDir, `${opts.name}.code-workspace`),
|
|
1113
1497
|
JSON.stringify(buildCodeWorkspaceJson(opts), null, 2) + "\n"
|
|
1114
1498
|
);
|
|
1115
1499
|
}
|
|
@@ -1154,6 +1538,82 @@ function addAptPackagesToDoc(doc, packages) {
|
|
|
1154
1538
|
}
|
|
1155
1539
|
return changed;
|
|
1156
1540
|
}
|
|
1541
|
+
function portOfItem(item) {
|
|
1542
|
+
const scalar = scalarValue(item);
|
|
1543
|
+
if (typeof scalar === "number" && Number.isInteger(scalar)) {
|
|
1544
|
+
return scalar;
|
|
1545
|
+
}
|
|
1546
|
+
if (isMap(item)) {
|
|
1547
|
+
const p = item.get("port");
|
|
1548
|
+
if (typeof p === "number" && Number.isInteger(p)) return p;
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
function ensureRoutingMap(doc) {
|
|
1553
|
+
const existing = doc.get("routing", true);
|
|
1554
|
+
if (existing && isMap(existing)) return existing;
|
|
1555
|
+
const map = new YAMLMap();
|
|
1556
|
+
doc.set("routing", map);
|
|
1557
|
+
return map;
|
|
1558
|
+
}
|
|
1559
|
+
function setDefaultPortInDoc(doc, port) {
|
|
1560
|
+
const routing = ensureRoutingMap(doc);
|
|
1561
|
+
const existing = routing.get("ports", true);
|
|
1562
|
+
let seq;
|
|
1563
|
+
if (existing && isSeq(existing)) {
|
|
1564
|
+
seq = existing;
|
|
1565
|
+
} else {
|
|
1566
|
+
seq = new YAMLSeq();
|
|
1567
|
+
routing.set("ports", seq);
|
|
1568
|
+
}
|
|
1569
|
+
const currentIdx = seq.items.findIndex((i) => portOfItem(i) === port);
|
|
1570
|
+
if (currentIdx === 0) return false;
|
|
1571
|
+
if (currentIdx > 0) {
|
|
1572
|
+
const [item] = seq.items.splice(currentIdx, 1);
|
|
1573
|
+
seq.items.unshift(item);
|
|
1574
|
+
return true;
|
|
1575
|
+
}
|
|
1576
|
+
seq.items.unshift(port);
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
function addPortsToDoc(doc, ports) {
|
|
1580
|
+
const routing = ensureRoutingMap(doc);
|
|
1581
|
+
const existing = routing.get("ports", true);
|
|
1582
|
+
let seq;
|
|
1583
|
+
if (existing && isSeq(existing)) {
|
|
1584
|
+
seq = existing;
|
|
1585
|
+
} else {
|
|
1586
|
+
seq = new YAMLSeq();
|
|
1587
|
+
routing.set("ports", seq);
|
|
1588
|
+
}
|
|
1589
|
+
let changed = false;
|
|
1590
|
+
for (const port of ports) {
|
|
1591
|
+
if (seq.items.some((i) => portOfItem(i) === port)) continue;
|
|
1592
|
+
seq.add(port);
|
|
1593
|
+
changed = true;
|
|
1594
|
+
}
|
|
1595
|
+
return changed;
|
|
1596
|
+
}
|
|
1597
|
+
function removePortsFromDoc(doc, ports) {
|
|
1598
|
+
const routing = doc.get("routing", true);
|
|
1599
|
+
if (!routing || !isMap(routing)) return false;
|
|
1600
|
+
const seq = routing.get("ports", true);
|
|
1601
|
+
if (!seq || !isSeq(seq)) return false;
|
|
1602
|
+
const targets = new Set(ports);
|
|
1603
|
+
let changed = false;
|
|
1604
|
+
for (let i = seq.items.length - 1; i >= 0; i--) {
|
|
1605
|
+
const p = portOfItem(seq.items[i]);
|
|
1606
|
+
if (p !== null && targets.has(p)) {
|
|
1607
|
+
seq.items.splice(i, 1);
|
|
1608
|
+
changed = true;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (changed) {
|
|
1612
|
+
if (seq.items.length === 0) routing.delete("ports");
|
|
1613
|
+
if (routing.items.length === 0) doc.delete("routing");
|
|
1614
|
+
}
|
|
1615
|
+
return changed;
|
|
1616
|
+
}
|
|
1157
1617
|
function addInstallUrlToDoc(doc, url) {
|
|
1158
1618
|
const seq = ensureSeq(doc, "installUrls");
|
|
1159
1619
|
if (seq.items.some((i) => scalarValue(i) === url)) return false;
|
|
@@ -1274,8 +1734,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
1274
1734
|
if (!isMap(item)) return false;
|
|
1275
1735
|
const url = item.get("url");
|
|
1276
1736
|
if (url === urlOrPath) return true;
|
|
1277
|
-
const
|
|
1278
|
-
const effectivePath = typeof
|
|
1737
|
+
const path15 = item.get("path");
|
|
1738
|
+
const effectivePath = typeof path15 === "string" ? path15 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
1279
1739
|
return effectivePath === urlOrPath;
|
|
1280
1740
|
});
|
|
1281
1741
|
if (idx < 0) return false;
|
|
@@ -1325,7 +1785,7 @@ async function runAddRepo(input) {
|
|
|
1325
1785
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
1326
1786
|
);
|
|
1327
1787
|
}
|
|
1328
|
-
const
|
|
1788
|
+
const path15 = (input.path ?? deriveRepoName(url)).trim();
|
|
1329
1789
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
1330
1790
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
1331
1791
|
if (hasName !== hasEmail) {
|
|
@@ -1354,7 +1814,7 @@ async function runAddRepo(input) {
|
|
|
1354
1814
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
1355
1815
|
const entry2 = {
|
|
1356
1816
|
url,
|
|
1357
|
-
path:
|
|
1817
|
+
path: path15,
|
|
1358
1818
|
...hasName && hasEmail ? {
|
|
1359
1819
|
gitUser: {
|
|
1360
1820
|
name: input.gitName.trim(),
|
|
@@ -1386,6 +1846,45 @@ function runAddFromUrl(input) {
|
|
|
1386
1846
|
}
|
|
1387
1847
|
return mutate(input, (doc) => addInstallUrlToDoc(doc, url));
|
|
1388
1848
|
}
|
|
1849
|
+
async function runAddPort(input) {
|
|
1850
|
+
if (input.ports.length === 0) {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
"No ports given. Usage: monoceros add-port <containername> -- <port> [<port> \u2026]."
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
const ports = normalizePorts(input.ports);
|
|
1856
|
+
if (input.asDefault && ports.length > 1) {
|
|
1857
|
+
throw new Error(
|
|
1858
|
+
`--default takes exactly one port. Got: ${ports.join(", ")}. Run add-port once with --default for the new default, then again (without --default) for the rest.`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
const result = await mutate(input, (doc) => {
|
|
1862
|
+
if (input.asDefault) {
|
|
1863
|
+
return setDefaultPortInDoc(doc, ports[0]);
|
|
1864
|
+
}
|
|
1865
|
+
return addPortsToDoc(doc, ports);
|
|
1866
|
+
});
|
|
1867
|
+
if (result.status === "updated") {
|
|
1868
|
+
await syncPortsToProxy(input);
|
|
1869
|
+
}
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
function normalizePorts(raw) {
|
|
1873
|
+
const result = [];
|
|
1874
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1875
|
+
for (const item of raw) {
|
|
1876
|
+
const n = typeof item === "number" ? item : Number(item);
|
|
1877
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
`Invalid port: ${JSON.stringify(item)}. Expected an integer between 1 and 65535.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
if (seen.has(n)) continue;
|
|
1883
|
+
seen.add(n);
|
|
1884
|
+
result.push(n);
|
|
1885
|
+
}
|
|
1886
|
+
return result;
|
|
1887
|
+
}
|
|
1389
1888
|
function runAddFeature(input) {
|
|
1390
1889
|
const ref = input.ref.trim();
|
|
1391
1890
|
if (ref.length === 0) {
|
|
@@ -1427,6 +1926,19 @@ function runRemoveFromUrl(input) {
|
|
|
1427
1926
|
}
|
|
1428
1927
|
return mutate(input, (doc) => removeInstallUrlFromDoc(doc, url));
|
|
1429
1928
|
}
|
|
1929
|
+
async function runRemovePort(input) {
|
|
1930
|
+
if (input.ports.length === 0) {
|
|
1931
|
+
throw new Error(
|
|
1932
|
+
"No ports given. Usage: monoceros remove-port <containername> -- <port> [<port> \u2026]."
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
const ports = normalizePorts(input.ports);
|
|
1936
|
+
const result = await mutate(input, (doc) => removePortsFromDoc(doc, ports));
|
|
1937
|
+
if (result.status === "updated") {
|
|
1938
|
+
await syncPortsToProxy(input);
|
|
1939
|
+
}
|
|
1940
|
+
return result;
|
|
1941
|
+
}
|
|
1430
1942
|
function runRemoveRepo(input) {
|
|
1431
1943
|
const target = input.target.trim();
|
|
1432
1944
|
if (target.length === 0) {
|
|
@@ -1447,7 +1959,7 @@ async function mutate(opts, apply) {
|
|
|
1447
1959
|
const logger = opts.logger ?? defaultLogger();
|
|
1448
1960
|
let oldText;
|
|
1449
1961
|
try {
|
|
1450
|
-
oldText = await
|
|
1962
|
+
oldText = await fs6.readFile(ymlPath, "utf8");
|
|
1451
1963
|
} catch {
|
|
1452
1964
|
throw new Error(
|
|
1453
1965
|
`No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
|
|
@@ -1471,7 +1983,7 @@ async function mutate(opts, apply) {
|
|
|
1471
1983
|
return { status: "aborted" };
|
|
1472
1984
|
}
|
|
1473
1985
|
}
|
|
1474
|
-
await
|
|
1986
|
+
await fs6.writeFile(ymlPath, newText, "utf8");
|
|
1475
1987
|
logger.success(`Updated ${ymlPath}.`);
|
|
1476
1988
|
logger.info(
|
|
1477
1989
|
`Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
|
|
@@ -1492,6 +2004,61 @@ var defaultConfirm = async (message) => {
|
|
|
1492
2004
|
});
|
|
1493
2005
|
return result === true;
|
|
1494
2006
|
};
|
|
2007
|
+
async function syncPortsToProxy(input) {
|
|
2008
|
+
const home = input.monocerosHome ?? monocerosHome();
|
|
2009
|
+
const ymlPath = containerConfigPath(input.name, home);
|
|
2010
|
+
const logger = input.logger ?? defaultLogger();
|
|
2011
|
+
let allPorts;
|
|
2012
|
+
try {
|
|
2013
|
+
const parsed = await readConfig(ymlPath);
|
|
2014
|
+
allPorts = (parsed.config.routing?.ports ?? []).map(portNumber);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
logger.warn(
|
|
2017
|
+
`Could not re-read yml after edit to sync Traefik routes: ${err instanceof Error ? err.message : String(err)}. The yml is correct; \`monoceros apply ${input.name}\` will rebuild the routes.`
|
|
2018
|
+
);
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
let hostPort = 80;
|
|
2022
|
+
try {
|
|
2023
|
+
const globalConfig = await readMonocerosConfig({ monocerosHome: home });
|
|
2024
|
+
hostPort = proxyHostPort(globalConfig);
|
|
2025
|
+
} catch {
|
|
2026
|
+
}
|
|
2027
|
+
if (allPorts.length > 0) {
|
|
2028
|
+
await preflightHostPort(hostPort, {
|
|
2029
|
+
...input.proxyDocker ? { docker: input.proxyDocker } : {}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
try {
|
|
2033
|
+
if (allPorts.length > 0) {
|
|
2034
|
+
await writeDynamicConfig(input.name, allPorts, { monocerosHome: home });
|
|
2035
|
+
await ensureProxy({
|
|
2036
|
+
monocerosHome: home,
|
|
2037
|
+
hostPort,
|
|
2038
|
+
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
2039
|
+
logger: { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
2040
|
+
});
|
|
2041
|
+
const urls = proxyUrlsFor(input.name, allPorts, hostPort);
|
|
2042
|
+
const lines = urls.map((u) => {
|
|
2043
|
+
const tag = u.isDefault ? " (default)" : "";
|
|
2044
|
+
return ` ${u.url}${tag}`;
|
|
2045
|
+
});
|
|
2046
|
+
logger.info(`Traefik routes refreshed:
|
|
2047
|
+
${lines.join("\n")}`);
|
|
2048
|
+
} else {
|
|
2049
|
+
await removeDynamicConfig(input.name, { monocerosHome: home });
|
|
2050
|
+
await maybeStopProxy({
|
|
2051
|
+
monocerosHome: home,
|
|
2052
|
+
...input.proxyDocker ? { docker: input.proxyDocker } : {},
|
|
2053
|
+
logger: { info: (m) => logger.info(m), warn: (m) => logger.warn(m) }
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
logger.warn(
|
|
2058
|
+
`Could not sync Traefik routes after yml edit: ${err instanceof Error ? err.message : String(err)}. The yml is correct; \`monoceros apply ${input.name}\` will rebuild the routes.`
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
1495
2062
|
|
|
1496
2063
|
// src/commands/add-apt-packages.ts
|
|
1497
2064
|
var addAptPackagesCommand = defineCommand({
|
|
@@ -1785,14 +2352,14 @@ var addLanguageCommand = defineCommand5({
|
|
|
1785
2352
|
}
|
|
1786
2353
|
});
|
|
1787
2354
|
|
|
1788
|
-
// src/commands/add-
|
|
2355
|
+
// src/commands/add-port.ts
|
|
1789
2356
|
import { defineCommand as defineCommand6 } from "citty";
|
|
1790
2357
|
import { consola as consola7 } from "consola";
|
|
1791
|
-
var
|
|
2358
|
+
var addPortCommand = defineCommand6({
|
|
1792
2359
|
meta: {
|
|
1793
|
-
name: "add-
|
|
2360
|
+
name: "add-port",
|
|
1794
2361
|
group: "edit",
|
|
1795
|
-
description: "Add
|
|
2362
|
+
description: "Add one or more ports to the container config so they become reachable from the host via Traefik (`<container>.localhost` / `<container>-<port>.localhost`). Pass port numbers after `--` (e.g. `monoceros add-port sandbox -- 3000 5173 6006`). Idempotent. Persisted in the yml so later `monoceros apply` runs restore the routes. Pass `--default` together with a single port to make it the bare `<container>.localhost` route \u2014 the port is inserted at position 0 (or moved there if it already exists)."
|
|
1796
2363
|
},
|
|
1797
2364
|
args: {
|
|
1798
2365
|
name: {
|
|
@@ -1800,24 +2367,32 @@ var addServiceCommand = defineCommand6({
|
|
|
1800
2367
|
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
1801
2368
|
required: true
|
|
1802
2369
|
},
|
|
1803
|
-
service: {
|
|
1804
|
-
type: "positional",
|
|
1805
|
-
description: "Service identifier (postgres, mysql, redis).",
|
|
1806
|
-
required: true
|
|
1807
|
-
},
|
|
1808
2370
|
yes: {
|
|
1809
2371
|
type: "boolean",
|
|
1810
2372
|
description: "Skip the interactive confirmation and apply the diff.",
|
|
1811
2373
|
alias: ["y"],
|
|
1812
2374
|
default: false
|
|
2375
|
+
},
|
|
2376
|
+
default: {
|
|
2377
|
+
type: "boolean",
|
|
2378
|
+
description: "Make the (single) port the new default route at `<container>.localhost`. Inserts the port at position 0 of `routing.ports`, or moves it there if it already exists. Errors when more than one port is passed.",
|
|
2379
|
+
default: false
|
|
1813
2380
|
}
|
|
1814
2381
|
},
|
|
1815
2382
|
async run({ args }) {
|
|
2383
|
+
const tokens = [...getInnerArgs()];
|
|
2384
|
+
if (tokens.length === 0) {
|
|
2385
|
+
consola7.error(
|
|
2386
|
+
"No ports given. Usage: `monoceros add-port <containername> [--yes] [--default] -- <port> [<port> \u2026]`."
|
|
2387
|
+
);
|
|
2388
|
+
process.exit(1);
|
|
2389
|
+
}
|
|
1816
2390
|
try {
|
|
1817
|
-
const result = await
|
|
2391
|
+
const result = await runAddPort({
|
|
1818
2392
|
name: args.name,
|
|
1819
|
-
|
|
1820
|
-
yes: args.yes
|
|
2393
|
+
ports: tokens.map(coerceToken),
|
|
2394
|
+
yes: args.yes,
|
|
2395
|
+
asDefault: args.default
|
|
1821
2396
|
});
|
|
1822
2397
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
1823
2398
|
} catch (err) {
|
|
@@ -1826,76 +2401,63 @@ var addServiceCommand = defineCommand6({
|
|
|
1826
2401
|
}
|
|
1827
2402
|
}
|
|
1828
2403
|
});
|
|
2404
|
+
function coerceToken(raw) {
|
|
2405
|
+
const n = Number(raw);
|
|
2406
|
+
return Number.isFinite(n) ? n : raw;
|
|
2407
|
+
}
|
|
1829
2408
|
|
|
1830
|
-
// src/commands/
|
|
2409
|
+
// src/commands/add-service.ts
|
|
1831
2410
|
import { defineCommand as defineCommand7 } from "citty";
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
return void 0;
|
|
1870
|
-
}
|
|
1871
|
-
const doc = parseDocument2(text, { prettyErrors: true });
|
|
1872
|
-
if (doc.errors.length > 0) {
|
|
1873
|
-
throw new Error(
|
|
1874
|
-
`yaml parse error in ${filePath}: ${doc.errors[0].message}`
|
|
1875
|
-
);
|
|
1876
|
-
}
|
|
1877
|
-
const result = MonocerosConfigSchema.safeParse(doc.toJS());
|
|
1878
|
-
if (!result.success) {
|
|
1879
|
-
const issues = result.error.issues.map((issue) => {
|
|
1880
|
-
const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1881
|
-
return ` - ${where}: ${issue.message}`;
|
|
1882
|
-
}).join("\n");
|
|
1883
|
-
throw new Error(
|
|
1884
|
-
`Invalid ${filePath}:
|
|
1885
|
-
${issues}
|
|
1886
|
-
|
|
1887
|
-
See ${filePath.replace(
|
|
1888
|
-
/\.yml$/,
|
|
1889
|
-
".sample.yml"
|
|
1890
|
-
)} for a valid example.`
|
|
1891
|
-
);
|
|
2411
|
+
import { consola as consola8 } from "consola";
|
|
2412
|
+
var addServiceCommand = defineCommand7({
|
|
2413
|
+
meta: {
|
|
2414
|
+
name: "add-service",
|
|
2415
|
+
group: "edit",
|
|
2416
|
+
description: "Add a compose service (postgres, mysql, redis, \u2026) to the container config. Idempotent, prints a diff before writing."
|
|
2417
|
+
},
|
|
2418
|
+
args: {
|
|
2419
|
+
name: {
|
|
2420
|
+
type: "positional",
|
|
2421
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
2422
|
+
required: true
|
|
2423
|
+
},
|
|
2424
|
+
service: {
|
|
2425
|
+
type: "positional",
|
|
2426
|
+
description: "Service identifier (postgres, mysql, redis).",
|
|
2427
|
+
required: true
|
|
2428
|
+
},
|
|
2429
|
+
yes: {
|
|
2430
|
+
type: "boolean",
|
|
2431
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
2432
|
+
alias: ["y"],
|
|
2433
|
+
default: false
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
2436
|
+
async run({ args }) {
|
|
2437
|
+
try {
|
|
2438
|
+
const result = await runAddService({
|
|
2439
|
+
name: args.name,
|
|
2440
|
+
service: args.service,
|
|
2441
|
+
yes: args.yes
|
|
2442
|
+
});
|
|
2443
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
consola8.error(err instanceof Error ? err.message : String(err));
|
|
2446
|
+
process.exit(1);
|
|
2447
|
+
}
|
|
1892
2448
|
}
|
|
1893
|
-
|
|
1894
|
-
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
// src/commands/apply.ts
|
|
2452
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
2453
|
+
|
|
2454
|
+
// src/apply/index.ts
|
|
2455
|
+
import { existsSync as existsSync4, promises as fs10 } from "fs";
|
|
2456
|
+
import { consola as consola11 } from "consola";
|
|
1895
2457
|
|
|
1896
2458
|
// src/config/state.ts
|
|
1897
|
-
import { promises as
|
|
1898
|
-
import
|
|
2459
|
+
import { promises as fs7 } from "fs";
|
|
2460
|
+
import path5 from "path";
|
|
1899
2461
|
function buildStateFile(opts) {
|
|
1900
2462
|
return {
|
|
1901
2463
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
@@ -1905,20 +2467,20 @@ function buildStateFile(opts) {
|
|
|
1905
2467
|
};
|
|
1906
2468
|
}
|
|
1907
2469
|
function stateFilePath(targetDir) {
|
|
1908
|
-
return
|
|
2470
|
+
return path5.join(targetDir, ".monoceros", "state.json");
|
|
1909
2471
|
}
|
|
1910
2472
|
async function readStateFile(targetDir) {
|
|
1911
2473
|
try {
|
|
1912
|
-
const content = await
|
|
2474
|
+
const content = await fs7.readFile(stateFilePath(targetDir), "utf8");
|
|
1913
2475
|
return JSON.parse(content);
|
|
1914
2476
|
} catch {
|
|
1915
2477
|
return void 0;
|
|
1916
2478
|
}
|
|
1917
2479
|
}
|
|
1918
2480
|
async function writeStateFile(targetDir, state) {
|
|
1919
|
-
const monocerosDir =
|
|
1920
|
-
await
|
|
1921
|
-
await
|
|
2481
|
+
const monocerosDir = path5.join(targetDir, ".monoceros");
|
|
2482
|
+
await fs7.mkdir(monocerosDir, { recursive: true });
|
|
2483
|
+
await fs7.writeFile(
|
|
1922
2484
|
stateFilePath(targetDir),
|
|
1923
2485
|
JSON.stringify(state, null, 2) + "\n"
|
|
1924
2486
|
);
|
|
@@ -1960,6 +2522,21 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
|
|
|
1960
2522
|
...r.provider ? { provider: r.provider } : {}
|
|
1961
2523
|
}));
|
|
1962
2524
|
}
|
|
2525
|
+
const routingPorts = config.routing?.ports ?? [];
|
|
2526
|
+
if (routingPorts.length > 0) {
|
|
2527
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2528
|
+
const ports = [];
|
|
2529
|
+
for (const entry2 of routingPorts) {
|
|
2530
|
+
const n = portNumber(entry2);
|
|
2531
|
+
if (seen.has(n)) continue;
|
|
2532
|
+
seen.add(n);
|
|
2533
|
+
ports.push(n);
|
|
2534
|
+
}
|
|
2535
|
+
result.ports = ports;
|
|
2536
|
+
}
|
|
2537
|
+
if (config.routing?.vscodeAutoForward !== void 0) {
|
|
2538
|
+
result.vscodeAutoForward = config.routing.vscodeAutoForward;
|
|
2539
|
+
}
|
|
1963
2540
|
return result;
|
|
1964
2541
|
}
|
|
1965
2542
|
|
|
@@ -1994,10 +2571,10 @@ var dim = stderrPalette.dim;
|
|
|
1994
2571
|
var sectionLine = stderrPalette.sectionLine;
|
|
1995
2572
|
|
|
1996
2573
|
// src/devcontainer/compose.ts
|
|
1997
|
-
import { spawn as
|
|
2574
|
+
import { spawn as spawn3 } from "child_process";
|
|
1998
2575
|
import { existsSync as existsSync3 } from "fs";
|
|
1999
|
-
import
|
|
2000
|
-
import { consola as
|
|
2576
|
+
import path7 from "path";
|
|
2577
|
+
import { consola as consola9 } from "consola";
|
|
2001
2578
|
|
|
2002
2579
|
// src/util/mask-secrets.ts
|
|
2003
2580
|
import { Transform } from "stream";
|
|
@@ -2056,10 +2633,10 @@ function createSecretMaskStream() {
|
|
|
2056
2633
|
}
|
|
2057
2634
|
|
|
2058
2635
|
// src/devcontainer/cli.ts
|
|
2059
|
-
import { spawn } from "child_process";
|
|
2636
|
+
import { spawn as spawn2 } from "child_process";
|
|
2060
2637
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2061
2638
|
import { createRequire } from "module";
|
|
2062
|
-
import
|
|
2639
|
+
import path6 from "path";
|
|
2063
2640
|
var require_ = createRequire(import.meta.url);
|
|
2064
2641
|
var cachedBinaryPath = null;
|
|
2065
2642
|
function devcontainerCliPath() {
|
|
@@ -2070,14 +2647,14 @@ function devcontainerCliPath() {
|
|
|
2070
2647
|
if (!binEntry) {
|
|
2071
2648
|
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
2072
2649
|
}
|
|
2073
|
-
cachedBinaryPath =
|
|
2650
|
+
cachedBinaryPath = path6.resolve(path6.dirname(pkgJsonPath), binEntry);
|
|
2074
2651
|
return cachedBinaryPath;
|
|
2075
2652
|
}
|
|
2076
2653
|
var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
2077
2654
|
const binPath = devcontainerCliPath();
|
|
2078
2655
|
return new Promise((resolve, reject) => {
|
|
2079
2656
|
if (options.interactive) {
|
|
2080
|
-
const child2 =
|
|
2657
|
+
const child2 = spawn2(process.execPath, [binPath, ...args], {
|
|
2081
2658
|
cwd,
|
|
2082
2659
|
stdio: "inherit"
|
|
2083
2660
|
});
|
|
@@ -2085,7 +2662,7 @@ var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
|
2085
2662
|
child2.on("exit", (code) => resolve(code ?? 0));
|
|
2086
2663
|
return;
|
|
2087
2664
|
}
|
|
2088
|
-
const child =
|
|
2665
|
+
const child = spawn2(process.execPath, [binPath, ...args], {
|
|
2089
2666
|
cwd,
|
|
2090
2667
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2091
2668
|
});
|
|
@@ -2119,7 +2696,7 @@ var spawnDevcontainer = (args, cwd, options = {}) => {
|
|
|
2119
2696
|
// src/devcontainer/compose.ts
|
|
2120
2697
|
var spawnDockerCompose = (args, cwd) => {
|
|
2121
2698
|
return new Promise((resolve, reject) => {
|
|
2122
|
-
const child =
|
|
2699
|
+
const child = spawn3("docker", ["compose", ...args], {
|
|
2123
2700
|
cwd,
|
|
2124
2701
|
stdio: ["inherit", "pipe", "pipe"]
|
|
2125
2702
|
});
|
|
@@ -2131,7 +2708,7 @@ var spawnDockerCompose = (args, cwd) => {
|
|
|
2131
2708
|
};
|
|
2132
2709
|
var spawnBash = (args, cwd) => {
|
|
2133
2710
|
return new Promise((resolve, reject) => {
|
|
2134
|
-
const child =
|
|
2711
|
+
const child = spawn3("bash", args, {
|
|
2135
2712
|
cwd,
|
|
2136
2713
|
stdio: ["inherit", "pipe", "pipe"]
|
|
2137
2714
|
});
|
|
@@ -2142,15 +2719,15 @@ var spawnBash = (args, cwd) => {
|
|
|
2142
2719
|
});
|
|
2143
2720
|
};
|
|
2144
2721
|
function composeProjectName(root) {
|
|
2145
|
-
return `${
|
|
2722
|
+
return `${path7.basename(root)}_devcontainer`;
|
|
2146
2723
|
}
|
|
2147
2724
|
function resolveCompose(root) {
|
|
2148
|
-
if (!existsSync3(
|
|
2725
|
+
if (!existsSync3(path7.join(root, ".devcontainer"))) {
|
|
2149
2726
|
throw new Error(
|
|
2150
2727
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
2151
2728
|
);
|
|
2152
2729
|
}
|
|
2153
|
-
const composeFile =
|
|
2730
|
+
const composeFile = path7.join(root, ".devcontainer", "compose.yaml");
|
|
2154
2731
|
if (!existsSync3(composeFile)) {
|
|
2155
2732
|
throw new Error(
|
|
2156
2733
|
`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.`
|
|
@@ -2166,7 +2743,7 @@ async function runComposeAction(buildSubArgs, opts) {
|
|
|
2166
2743
|
}
|
|
2167
2744
|
async function runStart(opts) {
|
|
2168
2745
|
resolveCompose(opts.root);
|
|
2169
|
-
const logger = opts.logger ?? { info: (msg) =>
|
|
2746
|
+
const logger = opts.logger ?? { info: (msg) => consola9.info(msg) };
|
|
2170
2747
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
2171
2748
|
logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
|
|
2172
2749
|
return spawnFn(
|
|
@@ -2241,12 +2818,12 @@ function runLogs(opts) {
|
|
|
2241
2818
|
}
|
|
2242
2819
|
|
|
2243
2820
|
// src/devcontainer/credentials.ts
|
|
2244
|
-
import { spawn as
|
|
2245
|
-
import { promises as
|
|
2246
|
-
import
|
|
2821
|
+
import { spawn as spawn4 } from "child_process";
|
|
2822
|
+
import { promises as fs8 } from "fs";
|
|
2823
|
+
import path8 from "path";
|
|
2247
2824
|
var realGitCredentialFill = (input) => {
|
|
2248
2825
|
return new Promise((resolve, reject) => {
|
|
2249
|
-
const child =
|
|
2826
|
+
const child = spawn4("git", ["credential", "fill"], {
|
|
2250
2827
|
stdio: ["pipe", "pipe", "inherit"],
|
|
2251
2828
|
env: {
|
|
2252
2829
|
...process.env,
|
|
@@ -2414,8 +2991,8 @@ function formatCredentialLine(host, username, password) {
|
|
|
2414
2991
|
return `https://${encUser}:${encPass}@${host}`;
|
|
2415
2992
|
}
|
|
2416
2993
|
async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
|
|
2417
|
-
const credsDir =
|
|
2418
|
-
const credentialsPath =
|
|
2994
|
+
const credsDir = path8.join(devContainerRoot, ".monoceros");
|
|
2995
|
+
const credentialsPath = path8.join(credsDir, "git-credentials");
|
|
2419
2996
|
const spawnFn = options.spawn ?? realGitCredentialFill;
|
|
2420
2997
|
const logger = options.logger ?? { info: () => {
|
|
2421
2998
|
}, warn: () => {
|
|
@@ -2468,8 +3045,8 @@ host=${host}
|
|
|
2468
3045
|
lines.push(formatCredentialLine(host, username, password));
|
|
2469
3046
|
perHost.push({ host, provider, status: "ok", detail: "" });
|
|
2470
3047
|
}
|
|
2471
|
-
await
|
|
2472
|
-
await
|
|
3048
|
+
await fs8.mkdir(credsDir, { recursive: true });
|
|
3049
|
+
await fs8.writeFile(
|
|
2473
3050
|
credentialsPath,
|
|
2474
3051
|
lines.join("\n") + (lines.length > 0 ? "\n" : ""),
|
|
2475
3052
|
{
|
|
@@ -2528,10 +3105,10 @@ function formatUnknownProviderError(hosts) {
|
|
|
2528
3105
|
}
|
|
2529
3106
|
|
|
2530
3107
|
// src/devcontainer/repo-reachability.ts
|
|
2531
|
-
import { spawn as
|
|
3108
|
+
import { spawn as spawn5 } from "child_process";
|
|
2532
3109
|
var realGitLsRemote = (url) => {
|
|
2533
3110
|
return new Promise((resolve, reject) => {
|
|
2534
|
-
const child =
|
|
3111
|
+
const child = spawn5("git", ["ls-remote", "--heads", "--", url], {
|
|
2535
3112
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2536
3113
|
env: {
|
|
2537
3114
|
...process.env,
|
|
@@ -2669,10 +3246,10 @@ function adviceForKind(kind) {
|
|
|
2669
3246
|
}
|
|
2670
3247
|
|
|
2671
3248
|
// src/devcontainer/docker-mode.ts
|
|
2672
|
-
import { spawn as
|
|
3249
|
+
import { spawn as spawn6 } from "child_process";
|
|
2673
3250
|
var realDockerInfo = () => {
|
|
2674
3251
|
return new Promise((resolve, reject) => {
|
|
2675
|
-
const child =
|
|
3252
|
+
const child = spawn6(
|
|
2676
3253
|
"docker",
|
|
2677
3254
|
["info", "--format", "{{json .SecurityOptions}}"],
|
|
2678
3255
|
{
|
|
@@ -2711,28 +3288,34 @@ function formatRootlessNotSupportedError() {
|
|
|
2711
3288
|
``,
|
|
2712
3289
|
`To fix, switch back to standard rootful Docker:`,
|
|
2713
3290
|
``,
|
|
2714
|
-
cyan2(
|
|
3291
|
+
cyan2(
|
|
3292
|
+
` systemctl --user stop docker.service docker.socket 2>/dev/null || true`
|
|
3293
|
+
),
|
|
2715
3294
|
cyan2(` dockerd-rootless-setuptool.sh uninstall`),
|
|
2716
|
-
cyan2(` rm -rf ~/.local/share/docker`),
|
|
2717
|
-
cyan2(` unset DOCKER_HOST`),
|
|
3295
|
+
cyan2(` rootlesskit rm -rf ~/.local/share/docker`),
|
|
3296
|
+
cyan2(` unset DOCKER_HOST DOCKER_CONTEXT`),
|
|
2718
3297
|
cyan2(` sudo systemctl enable --now docker`),
|
|
2719
3298
|
cyan2(` sudo usermod -aG docker $USER`),
|
|
2720
3299
|
``,
|
|
2721
|
-
`
|
|
2722
|
-
|
|
3300
|
+
`If you added DOCKER_HOST or DOCKER_CONTEXT to ~/.bashrc /`,
|
|
3301
|
+
`~/.profile (the rootless setup may have suggested it), remove`,
|
|
3302
|
+
`those lines too \u2014 the 'unset' above only affects your current`,
|
|
3303
|
+
`shell. Otherwise new terminals keep pointing at the rootless`,
|
|
3304
|
+
`socket and Monoceros's auto-recovery has nothing to fall back to.`,
|
|
2723
3305
|
``,
|
|
2724
|
-
`Background: see ${cyan2("docs/docker-on-linux.md")} in
|
|
3306
|
+
`Then re-run. Background: see ${cyan2("docs/docker-on-linux.md")} in`,
|
|
3307
|
+
`the workbench repo.`
|
|
2725
3308
|
].join("\n");
|
|
2726
3309
|
}
|
|
2727
3310
|
|
|
2728
3311
|
// src/devcontainer/identity.ts
|
|
2729
|
-
import { spawn as
|
|
2730
|
-
import { promises as
|
|
2731
|
-
import
|
|
2732
|
-
import { consola as
|
|
3312
|
+
import { spawn as spawn7 } from "child_process";
|
|
3313
|
+
import { promises as fs9 } from "fs";
|
|
3314
|
+
import path9 from "path";
|
|
3315
|
+
import { consola as consola10 } from "consola";
|
|
2733
3316
|
var realGitConfigGet = (key) => {
|
|
2734
3317
|
return new Promise((resolve, reject) => {
|
|
2735
|
-
const child =
|
|
3318
|
+
const child = spawn7("git", ["config", "--global", "--get", key], {
|
|
2736
3319
|
stdio: ["ignore", "pipe", "inherit"]
|
|
2737
3320
|
});
|
|
2738
3321
|
let stdout = "";
|
|
@@ -2751,14 +3334,14 @@ var realIdentityPrompt = async (key) => {
|
|
|
2751
3334
|
return void 0;
|
|
2752
3335
|
}
|
|
2753
3336
|
const label = key === "user.name" ? "Git user.name for this dev container (full name)" : "Git user.email for this dev container";
|
|
2754
|
-
const value = await
|
|
3337
|
+
const value = await consola10.prompt(`${label}:`, { type: "text" });
|
|
2755
3338
|
if (typeof value !== "string") return void 0;
|
|
2756
3339
|
const trimmed = value.trim();
|
|
2757
3340
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
2758
3341
|
};
|
|
2759
3342
|
async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
2760
|
-
const gitconfigDir =
|
|
2761
|
-
const gitconfigPath =
|
|
3343
|
+
const gitconfigDir = path9.join(devContainerRoot, ".monoceros");
|
|
3344
|
+
const gitconfigPath = path9.join(gitconfigDir, "gitconfig");
|
|
2762
3345
|
const spawnFn = options.spawn ?? realGitConfigGet;
|
|
2763
3346
|
const promptFn = options.prompt ?? realIdentityPrompt;
|
|
2764
3347
|
const logger = options.logger ?? { info: () => {
|
|
@@ -2784,8 +3367,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
2784
3367
|
const lines = ["[user]"];
|
|
2785
3368
|
if (name !== void 0) lines.push(` name = ${name}`);
|
|
2786
3369
|
if (email !== void 0) lines.push(` email = ${email}`);
|
|
2787
|
-
await
|
|
2788
|
-
await
|
|
3370
|
+
await fs9.mkdir(gitconfigDir, { recursive: true });
|
|
3371
|
+
await fs9.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
2789
3372
|
return {
|
|
2790
3373
|
...name !== void 0 ? { name } : {},
|
|
2791
3374
|
...email !== void 0 ? { email } : {},
|
|
@@ -2827,7 +3410,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
|
|
|
2827
3410
|
}
|
|
2828
3411
|
async function readExistingGitconfig(filePath) {
|
|
2829
3412
|
try {
|
|
2830
|
-
const content = await
|
|
3413
|
+
const content = await fs9.readFile(filePath, "utf8");
|
|
2831
3414
|
const result = {};
|
|
2832
3415
|
const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
|
|
2833
3416
|
const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
|
|
@@ -2843,9 +3426,9 @@ async function readExistingGitconfig(filePath) {
|
|
|
2843
3426
|
async function runApply(opts) {
|
|
2844
3427
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
2845
3428
|
const logger = opts.logger ?? {
|
|
2846
|
-
info: (msg) =>
|
|
2847
|
-
success: (msg) =>
|
|
2848
|
-
warn: (msg) =>
|
|
3429
|
+
info: (msg) => consola11.info(msg),
|
|
3430
|
+
success: (msg) => consola11.success(msg),
|
|
3431
|
+
warn: (msg) => consola11.warn(msg),
|
|
2849
3432
|
// Default section renderer: empty line, bold-underlined "▸ Label",
|
|
2850
3433
|
// empty line. Mirrors install.sh's section visuals.
|
|
2851
3434
|
section: (label) => process.stderr.write(`
|
|
@@ -2927,7 +3510,7 @@ ${sectionLine(label)}
|
|
|
2927
3510
|
if (dockerMode === "rootless") {
|
|
2928
3511
|
throw new Error(formatRootlessNotSupportedError());
|
|
2929
3512
|
}
|
|
2930
|
-
await
|
|
3513
|
+
await fs10.mkdir(targetDir, { recursive: true });
|
|
2931
3514
|
await writeScaffold(createOpts, targetDir, { dockerMode });
|
|
2932
3515
|
await writeStateFile(
|
|
2933
3516
|
targetDir,
|
|
@@ -2948,6 +3531,30 @@ ${sectionLine(label)}
|
|
|
2948
3531
|
'Pulling runtime image and building feature layers. First apply takes ~1\u20132 min (Docker downloads the multi-arch base); subsequent applies are cached and fast. devcontainer-cli may log a "No manifest found" line \u2014 harmless, the pull continues.'
|
|
2949
3532
|
)
|
|
2950
3533
|
);
|
|
3534
|
+
const ports = createOpts.ports ?? [];
|
|
3535
|
+
const hasPorts = ports.length > 0;
|
|
3536
|
+
if (hasPorts) {
|
|
3537
|
+
await preflightHostPort(proxyHostPort(globalConfig), {
|
|
3538
|
+
...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
try {
|
|
3542
|
+
if (hasPorts) {
|
|
3543
|
+
await writeDynamicConfig(opts.name, ports, { monocerosHome: home });
|
|
3544
|
+
await ensureProxy({
|
|
3545
|
+
...opts.proxyDocker ? { docker: opts.proxyDocker } : {},
|
|
3546
|
+
monocerosHome: home,
|
|
3547
|
+
hostPort: proxyHostPort(globalConfig),
|
|
3548
|
+
logger
|
|
3549
|
+
});
|
|
3550
|
+
} else {
|
|
3551
|
+
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
3552
|
+
}
|
|
3553
|
+
} catch (err) {
|
|
3554
|
+
logger.warn?.(
|
|
3555
|
+
`Could not sync Traefik routes: ${err instanceof Error ? err.message : String(err)}. The container will start, but \`<name>.localhost\` routing may not work until the next \`monoceros apply\`.`
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
2951
3558
|
const exitCode = await runContainerCycle(targetDir, {
|
|
2952
3559
|
hasCompose: needsCompose(createOpts),
|
|
2953
3560
|
...opts.cleanupSpawn !== void 0 ? { cleanupSpawn: opts.cleanupSpawn } : {},
|
|
@@ -2962,7 +3569,7 @@ ${sectionLine(label)}
|
|
|
2962
3569
|
}
|
|
2963
3570
|
async function assertSafeTargetDir(targetDir, expectedOrigin) {
|
|
2964
3571
|
if (!existsSync4(targetDir)) return;
|
|
2965
|
-
const entries = await
|
|
3572
|
+
const entries = await fs10.readdir(targetDir);
|
|
2966
3573
|
if (entries.length === 0) return;
|
|
2967
3574
|
const state = await readStateFile(targetDir);
|
|
2968
3575
|
if (state) {
|
|
@@ -3004,22 +3611,22 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
|
|
|
3004
3611
|
}
|
|
3005
3612
|
|
|
3006
3613
|
// src/version.ts
|
|
3007
|
-
var CLI_VERSION = true ? "1.
|
|
3614
|
+
var CLI_VERSION = true ? "1.7.0" : "dev";
|
|
3008
3615
|
|
|
3009
3616
|
// src/commands/_dispatch.ts
|
|
3010
|
-
import { consola as
|
|
3617
|
+
import { consola as consola12 } from "consola";
|
|
3011
3618
|
async function dispatch(runner) {
|
|
3012
3619
|
try {
|
|
3013
3620
|
const exitCode = await runner();
|
|
3014
3621
|
process.exit(exitCode);
|
|
3015
3622
|
} catch (err) {
|
|
3016
|
-
|
|
3623
|
+
consola12.error(err instanceof Error ? err.message : String(err));
|
|
3017
3624
|
process.exit(1);
|
|
3018
3625
|
}
|
|
3019
3626
|
}
|
|
3020
3627
|
|
|
3021
3628
|
// src/commands/apply.ts
|
|
3022
|
-
var applyCommand =
|
|
3629
|
+
var applyCommand = defineCommand8({
|
|
3023
3630
|
meta: {
|
|
3024
3631
|
name: "apply",
|
|
3025
3632
|
group: "lifecycle",
|
|
@@ -3044,7 +3651,7 @@ var applyCommand = defineCommand7({
|
|
|
3044
3651
|
});
|
|
3045
3652
|
|
|
3046
3653
|
// src/commands/completion.ts
|
|
3047
|
-
import { defineCommand as
|
|
3654
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
3048
3655
|
var ALL_COMMANDS = [
|
|
3049
3656
|
"init",
|
|
3050
3657
|
"list-components",
|
|
@@ -3063,12 +3670,15 @@ var ALL_COMMANDS = [
|
|
|
3063
3670
|
"add-feature",
|
|
3064
3671
|
"add-from-url",
|
|
3065
3672
|
"add-repo",
|
|
3673
|
+
"add-port",
|
|
3066
3674
|
"remove-service",
|
|
3067
3675
|
"remove-language",
|
|
3068
3676
|
"remove-apt-packages",
|
|
3069
3677
|
"remove-feature",
|
|
3070
3678
|
"remove-from-url",
|
|
3071
3679
|
"remove-repo",
|
|
3680
|
+
"remove-port",
|
|
3681
|
+
"port",
|
|
3072
3682
|
"completion"
|
|
3073
3683
|
];
|
|
3074
3684
|
var COMMANDS_WITH_CONTAINER_ARG = [
|
|
@@ -3086,12 +3696,15 @@ var COMMANDS_WITH_CONTAINER_ARG = [
|
|
|
3086
3696
|
"add-feature",
|
|
3087
3697
|
"add-from-url",
|
|
3088
3698
|
"add-repo",
|
|
3699
|
+
"add-port",
|
|
3089
3700
|
"remove-service",
|
|
3090
3701
|
"remove-language",
|
|
3091
3702
|
"remove-apt-packages",
|
|
3092
3703
|
"remove-feature",
|
|
3093
3704
|
"remove-from-url",
|
|
3094
|
-
"remove-repo"
|
|
3705
|
+
"remove-repo",
|
|
3706
|
+
"remove-port",
|
|
3707
|
+
"port"
|
|
3095
3708
|
];
|
|
3096
3709
|
var SHELLS = ["bash", "zsh", "pwsh"];
|
|
3097
3710
|
function renderCompletionScript(shell) {
|
|
@@ -3224,7 +3837,7 @@ function renderCompletionScript(shell) {
|
|
|
3224
3837
|
""
|
|
3225
3838
|
].join("\n");
|
|
3226
3839
|
}
|
|
3227
|
-
var completionCommand =
|
|
3840
|
+
var completionCommand = defineCommand9({
|
|
3228
3841
|
meta: {
|
|
3229
3842
|
name: "completion",
|
|
3230
3843
|
group: "tooling",
|
|
@@ -3251,16 +3864,16 @@ var completionCommand = defineCommand8({
|
|
|
3251
3864
|
});
|
|
3252
3865
|
|
|
3253
3866
|
// src/commands/init.ts
|
|
3254
|
-
import { defineCommand as
|
|
3255
|
-
import { consola as
|
|
3867
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
3868
|
+
import { consola as consola14 } from "consola";
|
|
3256
3869
|
|
|
3257
3870
|
// src/init/index.ts
|
|
3258
|
-
import { existsSync as existsSync7, promises as
|
|
3259
|
-
import { consola as
|
|
3871
|
+
import { existsSync as existsSync7, promises as fs12 } from "fs";
|
|
3872
|
+
import { consola as consola13 } from "consola";
|
|
3260
3873
|
|
|
3261
3874
|
// src/init/components.ts
|
|
3262
|
-
import { existsSync as existsSync5, promises as
|
|
3263
|
-
import
|
|
3875
|
+
import { existsSync as existsSync5, promises as fs11 } from "fs";
|
|
3876
|
+
import path10 from "path";
|
|
3264
3877
|
import { z as z3 } from "zod";
|
|
3265
3878
|
import { parse as parseYaml } from "yaml";
|
|
3266
3879
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -3315,17 +3928,17 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
3315
3928
|
return out;
|
|
3316
3929
|
}
|
|
3317
3930
|
async function walk(baseDir, currentDir, out) {
|
|
3318
|
-
const entries = await
|
|
3931
|
+
const entries = await fs11.readdir(currentDir, { withFileTypes: true });
|
|
3319
3932
|
for (const entry2 of entries) {
|
|
3320
|
-
const full =
|
|
3933
|
+
const full = path10.join(currentDir, entry2.name);
|
|
3321
3934
|
if (entry2.isDirectory()) {
|
|
3322
3935
|
await walk(baseDir, full, out);
|
|
3323
3936
|
continue;
|
|
3324
3937
|
}
|
|
3325
3938
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
3326
|
-
const relative =
|
|
3327
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
3328
|
-
const text = await
|
|
3939
|
+
const relative = path10.relative(baseDir, full);
|
|
3940
|
+
const name = relative.replace(/\.yml$/, "").split(path10.sep).join("/");
|
|
3941
|
+
const text = await fs11.readFile(full, "utf8");
|
|
3329
3942
|
let raw;
|
|
3330
3943
|
try {
|
|
3331
3944
|
raw = parseYaml(text);
|
|
@@ -3581,6 +4194,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = []) {
|
|
|
3581
4194
|
/* commented */
|
|
3582
4195
|
repoUrls.length === 0
|
|
3583
4196
|
);
|
|
4197
|
+
renderRoutingBlock(lines);
|
|
3584
4198
|
return ensureTrailingNewline(lines.join("\n"));
|
|
3585
4199
|
}
|
|
3586
4200
|
var COMMENT_WIDTH = 72;
|
|
@@ -3680,6 +4294,31 @@ function renderReposBlock(out, urls, commented) {
|
|
|
3680
4294
|
}
|
|
3681
4295
|
out.push("");
|
|
3682
4296
|
}
|
|
4297
|
+
function renderRoutingBlock(out) {
|
|
4298
|
+
out.push("# Routing \u2014 expose container ports to the host through the");
|
|
4299
|
+
out.push("# shared Traefik singleton. Once any port is declared the");
|
|
4300
|
+
out.push("# container joins the monoceros-proxy network and the proxy");
|
|
4301
|
+
out.push("# routes <name>.localhost (default port) and");
|
|
4302
|
+
out.push("# <name>-<port>.localhost (explicit). `monoceros add-port`");
|
|
4303
|
+
out.push("# manages the list; the block appears on first add.");
|
|
4304
|
+
out.push("#");
|
|
4305
|
+
out.push("# routing:");
|
|
4306
|
+
out.push("# ports: # internal container ports");
|
|
4307
|
+
out.push(
|
|
4308
|
+
"# - 3000 # first entry doubles as <name>.localhost"
|
|
4309
|
+
);
|
|
4310
|
+
out.push("# - 5173");
|
|
4311
|
+
out.push(
|
|
4312
|
+
"# vscodeAutoForward: false # default: false. Traefik is the single"
|
|
4313
|
+
);
|
|
4314
|
+
out.push(
|
|
4315
|
+
"# # source of truth \u2014 set true only if you"
|
|
4316
|
+
);
|
|
4317
|
+
out.push(
|
|
4318
|
+
"# # want VS Code's port panel as primary."
|
|
4319
|
+
);
|
|
4320
|
+
out.push("");
|
|
4321
|
+
}
|
|
3683
4322
|
function deriveDefaultPath(url) {
|
|
3684
4323
|
let last = url;
|
|
3685
4324
|
const slash = url.lastIndexOf("/");
|
|
@@ -3734,10 +4373,10 @@ function ensureTrailingNewline(s) {
|
|
|
3734
4373
|
|
|
3735
4374
|
// src/init/manifest.ts
|
|
3736
4375
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
3737
|
-
import
|
|
4376
|
+
import path11 from "path";
|
|
3738
4377
|
function resolveManifestPath(name, checkoutRoot) {
|
|
3739
4378
|
if (checkoutRoot) {
|
|
3740
|
-
const checkoutPath =
|
|
4379
|
+
const checkoutPath = path11.join(
|
|
3741
4380
|
checkoutRoot,
|
|
3742
4381
|
"images",
|
|
3743
4382
|
"features",
|
|
@@ -3746,7 +4385,7 @@ function resolveManifestPath(name, checkoutRoot) {
|
|
|
3746
4385
|
);
|
|
3747
4386
|
if (existsSync6(checkoutPath)) return checkoutPath;
|
|
3748
4387
|
}
|
|
3749
|
-
const bundlePath =
|
|
4388
|
+
const bundlePath = path11.join(
|
|
3750
4389
|
bundledFeaturesDir(),
|
|
3751
4390
|
name,
|
|
3752
4391
|
"devcontainer-feature.json"
|
|
@@ -3789,8 +4428,8 @@ async function runInit(opts) {
|
|
|
3789
4428
|
const workbench = opts.workbenchRoot ?? workbenchRoot();
|
|
3790
4429
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
3791
4430
|
const logger = opts.logger ?? {
|
|
3792
|
-
success: (msg) =>
|
|
3793
|
-
info: (msg) =>
|
|
4431
|
+
success: (msg) => consola13.success(msg),
|
|
4432
|
+
info: (msg) => consola13.info(msg)
|
|
3794
4433
|
};
|
|
3795
4434
|
if (!REGEX.solutionName.test(opts.name)) {
|
|
3796
4435
|
throw new Error(
|
|
@@ -3851,8 +4490,8 @@ async function runInit(opts) {
|
|
|
3851
4490
|
const components = resolveComponents(catalog, requested);
|
|
3852
4491
|
text = generateComposedYml(opts.name, components, lookup, repos);
|
|
3853
4492
|
}
|
|
3854
|
-
await
|
|
3855
|
-
await
|
|
4493
|
+
await fs12.mkdir(containerConfigsDir(home), { recursive: true });
|
|
4494
|
+
await fs12.writeFile(dest, text, "utf8");
|
|
3856
4495
|
const documented = requested.length === 0;
|
|
3857
4496
|
const displayPath = prettyPath(dest);
|
|
3858
4497
|
if (documented) {
|
|
@@ -3871,7 +4510,7 @@ async function runInit(opts) {
|
|
|
3871
4510
|
}
|
|
3872
4511
|
|
|
3873
4512
|
// src/commands/init.ts
|
|
3874
|
-
var initCommand =
|
|
4513
|
+
var initCommand = defineCommand10({
|
|
3875
4514
|
meta: {
|
|
3876
4515
|
name: "init",
|
|
3877
4516
|
group: "lifecycle",
|
|
@@ -3904,7 +4543,7 @@ var initCommand = defineCommand9({
|
|
|
3904
4543
|
...withRepoList.length > 0 ? { withRepo: withRepoList } : {}
|
|
3905
4544
|
});
|
|
3906
4545
|
} catch (err) {
|
|
3907
|
-
|
|
4546
|
+
consola14.error(err instanceof Error ? err.message : String(err));
|
|
3908
4547
|
process.exit(1);
|
|
3909
4548
|
}
|
|
3910
4549
|
}
|
|
@@ -3948,8 +4587,8 @@ function collectWithList(withArg, rawArgs) {
|
|
|
3948
4587
|
}
|
|
3949
4588
|
|
|
3950
4589
|
// src/commands/list-components.ts
|
|
3951
|
-
import { defineCommand as
|
|
3952
|
-
import { consola as
|
|
4590
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4591
|
+
import { consola as consola15 } from "consola";
|
|
3953
4592
|
var CATEGORY_LABELS = {
|
|
3954
4593
|
language: "Languages",
|
|
3955
4594
|
service: "Services",
|
|
@@ -3960,7 +4599,7 @@ var CATEGORY_ORDER = [
|
|
|
3960
4599
|
"service",
|
|
3961
4600
|
"feature"
|
|
3962
4601
|
];
|
|
3963
|
-
var listComponentsCommand =
|
|
4602
|
+
var listComponentsCommand = defineCommand11({
|
|
3964
4603
|
meta: {
|
|
3965
4604
|
name: "list-components",
|
|
3966
4605
|
group: "discovery",
|
|
@@ -3971,7 +4610,7 @@ var listComponentsCommand = defineCommand10({
|
|
|
3971
4610
|
try {
|
|
3972
4611
|
const catalog = await loadComponentCatalog();
|
|
3973
4612
|
if (catalog.size === 0) {
|
|
3974
|
-
|
|
4613
|
+
consola15.warn(
|
|
3975
4614
|
"No components found. The workbench checkout looks incomplete."
|
|
3976
4615
|
);
|
|
3977
4616
|
process.exit(0);
|
|
@@ -4022,15 +4661,15 @@ var listComponentsCommand = defineCommand10({
|
|
|
4022
4661
|
}
|
|
4023
4662
|
process.exit(0);
|
|
4024
4663
|
} catch (err) {
|
|
4025
|
-
|
|
4664
|
+
consola15.error(err instanceof Error ? err.message : String(err));
|
|
4026
4665
|
process.exit(1);
|
|
4027
4666
|
}
|
|
4028
4667
|
}
|
|
4029
4668
|
});
|
|
4030
4669
|
|
|
4031
4670
|
// src/commands/logs.ts
|
|
4032
|
-
import { defineCommand as
|
|
4033
|
-
var logsCommand =
|
|
4671
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
4672
|
+
var logsCommand = defineCommand12({
|
|
4034
4673
|
meta: {
|
|
4035
4674
|
name: "logs",
|
|
4036
4675
|
group: "run",
|
|
@@ -4064,10 +4703,87 @@ var logsCommand = defineCommand11({
|
|
|
4064
4703
|
}
|
|
4065
4704
|
});
|
|
4066
4705
|
|
|
4706
|
+
// src/commands/port.ts
|
|
4707
|
+
import { defineCommand as defineCommand13 } from "citty";
|
|
4708
|
+
import { consola as consola16 } from "consola";
|
|
4709
|
+
async function runPortListing(opts) {
|
|
4710
|
+
const out = opts.out ?? process.stdout;
|
|
4711
|
+
const info = opts.info ?? ((m) => consola16.info(m));
|
|
4712
|
+
const parsed = await readConfig(
|
|
4713
|
+
containerConfigPath(opts.name, opts.monocerosHome)
|
|
4714
|
+
);
|
|
4715
|
+
const portEntries = parsed.config.routing?.ports ?? [];
|
|
4716
|
+
if (portEntries.length === 0) {
|
|
4717
|
+
info(
|
|
4718
|
+
`No ports declared in ${opts.name}.yml. Run \`monoceros add-port ${opts.name} -- <port>\` to expose one.`
|
|
4719
|
+
);
|
|
4720
|
+
return 0;
|
|
4721
|
+
}
|
|
4722
|
+
const ports = portEntries.map(portNumber);
|
|
4723
|
+
const globalConfig = await readMonocerosConfig({
|
|
4724
|
+
...opts.monocerosHome ? { monocerosHome: opts.monocerosHome } : {}
|
|
4725
|
+
});
|
|
4726
|
+
const hostPort = proxyHostPort(globalConfig);
|
|
4727
|
+
const urls = proxyUrlsFor(opts.name, ports, hostPort);
|
|
4728
|
+
const isTty2 = out.isTTY ?? false;
|
|
4729
|
+
const fmt = colorsFor(out);
|
|
4730
|
+
const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
|
|
4731
|
+
const rows = [];
|
|
4732
|
+
rows.push({
|
|
4733
|
+
port: urls[0].port,
|
|
4734
|
+
url: `http://${opts.name}.localhost${portSuffix}`,
|
|
4735
|
+
tag: "default"
|
|
4736
|
+
});
|
|
4737
|
+
for (const u of urls) {
|
|
4738
|
+
rows.push({ port: u.port, url: u.url, tag: "" });
|
|
4739
|
+
}
|
|
4740
|
+
if (!isTty2) {
|
|
4741
|
+
for (const r of rows) {
|
|
4742
|
+
out.write(`${r.port} ${r.url} ${r.tag}
|
|
4743
|
+
`);
|
|
4744
|
+
}
|
|
4745
|
+
return 0;
|
|
4746
|
+
}
|
|
4747
|
+
const portWidth = Math.max(...rows.map((r) => String(r.port).length));
|
|
4748
|
+
const urlWidth = Math.max(...rows.map((r) => r.url.length));
|
|
4749
|
+
const gutter = 2;
|
|
4750
|
+
for (const r of rows) {
|
|
4751
|
+
const portStr = String(r.port).padStart(portWidth);
|
|
4752
|
+
const urlPad = " ".repeat(urlWidth - r.url.length + gutter);
|
|
4753
|
+
const tag = r.tag ? fmt.dim(`(${r.tag})`) : "";
|
|
4754
|
+
out.write(` ${fmt.cyan(portStr)} \u2192 ${r.url}${urlPad}${tag}
|
|
4755
|
+
`);
|
|
4756
|
+
}
|
|
4757
|
+
return 0;
|
|
4758
|
+
}
|
|
4759
|
+
var portCommand = defineCommand13({
|
|
4760
|
+
meta: {
|
|
4761
|
+
name: "port",
|
|
4762
|
+
group: "discovery",
|
|
4763
|
+
description: "List the Traefik URLs for a container. Reads ports from `routing.ports` in the container yml and the host port from `routing.hostPort` in monoceros-config.yml (default 80). When piped, drops formatting and emits `port<TAB>url<TAB>tag` per line for grep/awk consumption."
|
|
4764
|
+
},
|
|
4765
|
+
args: {
|
|
4766
|
+
name: {
|
|
4767
|
+
type: "positional",
|
|
4768
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
4769
|
+
required: true
|
|
4770
|
+
}
|
|
4771
|
+
},
|
|
4772
|
+
async run({ args }) {
|
|
4773
|
+
try {
|
|
4774
|
+
const code = await runPortListing({ name: args.name });
|
|
4775
|
+
process.exit(code);
|
|
4776
|
+
} catch (err) {
|
|
4777
|
+
consola16.error(err instanceof Error ? err.message : String(err));
|
|
4778
|
+
process.exit(1);
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
|
|
4067
4783
|
// src/commands/remove-apt-packages.ts
|
|
4068
|
-
import { defineCommand as
|
|
4069
|
-
import { consola as
|
|
4070
|
-
var removeAptPackagesCommand =
|
|
4784
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
4785
|
+
import { consola as consola17 } from "consola";
|
|
4786
|
+
var removeAptPackagesCommand = defineCommand14({
|
|
4071
4787
|
meta: {
|
|
4072
4788
|
name: "remove-apt-packages",
|
|
4073
4789
|
group: "edit",
|
|
@@ -4089,7 +4805,7 @@ var removeAptPackagesCommand = defineCommand12({
|
|
|
4089
4805
|
async run({ args }) {
|
|
4090
4806
|
const packages = [...getInnerArgs()];
|
|
4091
4807
|
if (packages.length === 0) {
|
|
4092
|
-
|
|
4808
|
+
consola17.error(
|
|
4093
4809
|
"No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
|
|
4094
4810
|
);
|
|
4095
4811
|
process.exit(1);
|
|
@@ -4102,16 +4818,16 @@ var removeAptPackagesCommand = defineCommand12({
|
|
|
4102
4818
|
});
|
|
4103
4819
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4104
4820
|
} catch (err) {
|
|
4105
|
-
|
|
4821
|
+
consola17.error(err instanceof Error ? err.message : String(err));
|
|
4106
4822
|
process.exit(1);
|
|
4107
4823
|
}
|
|
4108
4824
|
}
|
|
4109
4825
|
});
|
|
4110
4826
|
|
|
4111
4827
|
// src/commands/remove-feature.ts
|
|
4112
|
-
import { defineCommand as
|
|
4113
|
-
import { consola as
|
|
4114
|
-
var removeFeatureCommand =
|
|
4828
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
4829
|
+
import { consola as consola18 } from "consola";
|
|
4830
|
+
var removeFeatureCommand = defineCommand15({
|
|
4115
4831
|
meta: {
|
|
4116
4832
|
name: "remove-feature",
|
|
4117
4833
|
group: "edit",
|
|
@@ -4144,27 +4860,27 @@ var removeFeatureCommand = defineCommand13({
|
|
|
4144
4860
|
});
|
|
4145
4861
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4146
4862
|
} catch (err) {
|
|
4147
|
-
|
|
4863
|
+
consola18.error(err instanceof Error ? err.message : String(err));
|
|
4148
4864
|
process.exit(1);
|
|
4149
4865
|
}
|
|
4150
4866
|
}
|
|
4151
4867
|
});
|
|
4152
4868
|
|
|
4153
4869
|
// src/commands/remove.ts
|
|
4154
|
-
import { defineCommand as
|
|
4155
|
-
import { consola as
|
|
4870
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
4871
|
+
import { consola as consola20 } from "consola";
|
|
4156
4872
|
import { createInterface } from "readline/promises";
|
|
4157
4873
|
|
|
4158
4874
|
// src/remove/index.ts
|
|
4159
|
-
import { existsSync as existsSync8, promises as
|
|
4160
|
-
import
|
|
4161
|
-
import { consola as
|
|
4875
|
+
import { existsSync as existsSync8, promises as fs13 } from "fs";
|
|
4876
|
+
import path12 from "path";
|
|
4877
|
+
import { consola as consola19 } from "consola";
|
|
4162
4878
|
async function runRemove(opts) {
|
|
4163
4879
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
4164
4880
|
const logger = opts.logger ?? {
|
|
4165
|
-
info: (msg) =>
|
|
4166
|
-
success: (msg) =>
|
|
4167
|
-
warn: (msg) =>
|
|
4881
|
+
info: (msg) => consola19.info(msg),
|
|
4882
|
+
success: (msg) => consola19.success(msg),
|
|
4883
|
+
warn: (msg) => consola19.warn(msg)
|
|
4168
4884
|
};
|
|
4169
4885
|
if (!REGEX.solutionName.test(opts.name)) {
|
|
4170
4886
|
throw new Error(
|
|
@@ -4208,23 +4924,23 @@ async function runRemove(opts) {
|
|
|
4208
4924
|
let backupPath = null;
|
|
4209
4925
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
4210
4926
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4211
|
-
backupPath =
|
|
4212
|
-
await
|
|
4927
|
+
backupPath = path12.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
4928
|
+
await fs13.mkdir(backupPath, { recursive: true });
|
|
4213
4929
|
if (hasYml) {
|
|
4214
|
-
await
|
|
4930
|
+
await fs13.copyFile(ymlPath, path12.join(backupPath, `${opts.name}.yml`));
|
|
4215
4931
|
}
|
|
4216
4932
|
if (hasContainer) {
|
|
4217
|
-
await
|
|
4933
|
+
await fs13.cp(containerPath, path12.join(backupPath, "container"), {
|
|
4218
4934
|
recursive: true
|
|
4219
4935
|
});
|
|
4220
4936
|
}
|
|
4221
4937
|
logger.info(`Backup written to ${prettyPath(backupPath)}.`);
|
|
4222
4938
|
}
|
|
4223
4939
|
if (hasYml) {
|
|
4224
|
-
await
|
|
4940
|
+
await fs13.rm(ymlPath, { force: true });
|
|
4225
4941
|
}
|
|
4226
4942
|
if (hasContainer) {
|
|
4227
|
-
await
|
|
4943
|
+
await fs13.rm(containerPath, { recursive: true, force: true });
|
|
4228
4944
|
}
|
|
4229
4945
|
logger.success(
|
|
4230
4946
|
`Removed '${opts.name}': docker objects gone, container-configs entry deleted, container directory deleted.`
|
|
@@ -4234,6 +4950,24 @@ async function runRemove(opts) {
|
|
|
4234
4950
|
"No backup created (--no-backup). The host-side state is gone for good."
|
|
4235
4951
|
);
|
|
4236
4952
|
}
|
|
4953
|
+
try {
|
|
4954
|
+
await removeDynamicConfig(opts.name, { monocerosHome: home });
|
|
4955
|
+
} catch (err) {
|
|
4956
|
+
logger.warn?.(
|
|
4957
|
+
`Could not remove Traefik dynamic config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
4958
|
+
);
|
|
4959
|
+
}
|
|
4960
|
+
try {
|
|
4961
|
+
await maybeStopProxy({
|
|
4962
|
+
...opts.proxyDocker ? { docker: opts.proxyDocker } : {},
|
|
4963
|
+
monocerosHome: home,
|
|
4964
|
+
logger: { info: (msg) => logger.info(msg), warn: logger.warn }
|
|
4965
|
+
});
|
|
4966
|
+
} catch (err) {
|
|
4967
|
+
logger.warn?.(
|
|
4968
|
+
`Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
4969
|
+
);
|
|
4970
|
+
}
|
|
4237
4971
|
return {
|
|
4238
4972
|
configPath: hasYml ? ymlPath : null,
|
|
4239
4973
|
containerPath: hasContainer ? containerPath : null,
|
|
@@ -4243,7 +4977,7 @@ async function runRemove(opts) {
|
|
|
4243
4977
|
}
|
|
4244
4978
|
|
|
4245
4979
|
// src/commands/remove.ts
|
|
4246
|
-
var removeCommand =
|
|
4980
|
+
var removeCommand = defineCommand16({
|
|
4247
4981
|
meta: {
|
|
4248
4982
|
name: "remove",
|
|
4249
4983
|
group: "lifecycle",
|
|
@@ -4280,7 +5014,7 @@ var removeCommand = defineCommand14({
|
|
|
4280
5014
|
const skipPrompt = args.yes === true;
|
|
4281
5015
|
if (!skipPrompt) {
|
|
4282
5016
|
const warning = noBackup ? `About to remove '${args.name}' WITHOUT a backup. Docker objects, container-configs entry, and container directory will all be deleted.` : `About to remove '${args.name}'. A backup will be written to container-backups/ first, then docker objects, container-configs entry, and container directory will all be deleted.`;
|
|
4283
|
-
|
|
5017
|
+
consola20.warn(warning);
|
|
4284
5018
|
const rl = createInterface({
|
|
4285
5019
|
input: process.stdin,
|
|
4286
5020
|
output: process.stdout
|
|
@@ -4288,7 +5022,7 @@ var removeCommand = defineCommand14({
|
|
|
4288
5022
|
const answer = await rl.question("Continue? [y/N] ");
|
|
4289
5023
|
rl.close();
|
|
4290
5024
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
4291
|
-
|
|
5025
|
+
consola20.info("Aborted. Nothing changed.");
|
|
4292
5026
|
process.exit(0);
|
|
4293
5027
|
}
|
|
4294
5028
|
}
|
|
@@ -4297,35 +5031,35 @@ var removeCommand = defineCommand14({
|
|
|
4297
5031
|
...noBackup ? { noBackup: true } : {}
|
|
4298
5032
|
});
|
|
4299
5033
|
} catch (err) {
|
|
4300
|
-
|
|
5034
|
+
consola20.error(err instanceof Error ? err.message : String(err));
|
|
4301
5035
|
process.exit(1);
|
|
4302
5036
|
}
|
|
4303
5037
|
}
|
|
4304
5038
|
});
|
|
4305
5039
|
|
|
4306
5040
|
// src/commands/restore.ts
|
|
4307
|
-
import { defineCommand as
|
|
4308
|
-
import { consola as
|
|
5041
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
5042
|
+
import { consola as consola22 } from "consola";
|
|
4309
5043
|
|
|
4310
5044
|
// src/restore/index.ts
|
|
4311
|
-
import { existsSync as existsSync9, promises as
|
|
4312
|
-
import
|
|
4313
|
-
import { consola as
|
|
5045
|
+
import { existsSync as existsSync9, promises as fs14 } from "fs";
|
|
5046
|
+
import path13 from "path";
|
|
5047
|
+
import { consola as consola21 } from "consola";
|
|
4314
5048
|
async function runRestore(opts) {
|
|
4315
5049
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
4316
5050
|
const logger = opts.logger ?? {
|
|
4317
|
-
info: (msg) =>
|
|
4318
|
-
success: (msg) =>
|
|
5051
|
+
info: (msg) => consola21.info(msg),
|
|
5052
|
+
success: (msg) => consola21.success(msg)
|
|
4319
5053
|
};
|
|
4320
|
-
const backup =
|
|
5054
|
+
const backup = path13.resolve(opts.backupPath);
|
|
4321
5055
|
if (!existsSync9(backup)) {
|
|
4322
5056
|
throw new Error(`Backup not found: ${backup}.`);
|
|
4323
5057
|
}
|
|
4324
|
-
const stat = await
|
|
5058
|
+
const stat = await fs14.stat(backup);
|
|
4325
5059
|
if (!stat.isDirectory()) {
|
|
4326
5060
|
throw new Error(`Backup path is not a directory: ${backup}.`);
|
|
4327
5061
|
}
|
|
4328
|
-
const entries = await
|
|
5062
|
+
const entries = await fs14.readdir(backup);
|
|
4329
5063
|
const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
|
|
4330
5064
|
if (ymlFiles.length === 0) {
|
|
4331
5065
|
throw new Error(
|
|
@@ -4339,7 +5073,7 @@ async function runRestore(opts) {
|
|
|
4339
5073
|
}
|
|
4340
5074
|
const ymlFile = ymlFiles[0];
|
|
4341
5075
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
4342
|
-
const containerInBackup =
|
|
5076
|
+
const containerInBackup = path13.join(backup, "container");
|
|
4343
5077
|
const hasContainer = existsSync9(containerInBackup);
|
|
4344
5078
|
const destYml = containerConfigPath(name, home);
|
|
4345
5079
|
const destContainer = containerDir(name, home);
|
|
@@ -4353,10 +5087,10 @@ async function runRestore(opts) {
|
|
|
4353
5087
|
`Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
|
|
4354
5088
|
);
|
|
4355
5089
|
}
|
|
4356
|
-
await
|
|
4357
|
-
await
|
|
5090
|
+
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
5091
|
+
await fs14.copyFile(path13.join(backup, ymlFile), destYml);
|
|
4358
5092
|
if (hasContainer) {
|
|
4359
|
-
await
|
|
5093
|
+
await fs14.cp(containerInBackup, destContainer, { recursive: true });
|
|
4360
5094
|
}
|
|
4361
5095
|
logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
|
|
4362
5096
|
logger.info(
|
|
@@ -4370,7 +5104,7 @@ async function runRestore(opts) {
|
|
|
4370
5104
|
}
|
|
4371
5105
|
|
|
4372
5106
|
// src/commands/restore.ts
|
|
4373
|
-
var restoreCommand =
|
|
5107
|
+
var restoreCommand = defineCommand17({
|
|
4374
5108
|
meta: {
|
|
4375
5109
|
name: "restore",
|
|
4376
5110
|
group: "lifecycle",
|
|
@@ -4387,16 +5121,16 @@ var restoreCommand = defineCommand15({
|
|
|
4387
5121
|
try {
|
|
4388
5122
|
await runRestore({ backupPath: args["backup-path"] });
|
|
4389
5123
|
} catch (err) {
|
|
4390
|
-
|
|
5124
|
+
consola22.error(err instanceof Error ? err.message : String(err));
|
|
4391
5125
|
process.exit(1);
|
|
4392
5126
|
}
|
|
4393
5127
|
}
|
|
4394
5128
|
});
|
|
4395
5129
|
|
|
4396
5130
|
// src/commands/remove-from-url.ts
|
|
4397
|
-
import { defineCommand as
|
|
4398
|
-
import { consola as
|
|
4399
|
-
var removeFromUrlCommand =
|
|
5131
|
+
import { defineCommand as defineCommand18 } from "citty";
|
|
5132
|
+
import { consola as consola23 } from "consola";
|
|
5133
|
+
var removeFromUrlCommand = defineCommand18({
|
|
4400
5134
|
meta: {
|
|
4401
5135
|
name: "remove-from-url",
|
|
4402
5136
|
group: "edit",
|
|
@@ -4429,16 +5163,16 @@ var removeFromUrlCommand = defineCommand16({
|
|
|
4429
5163
|
});
|
|
4430
5164
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4431
5165
|
} catch (err) {
|
|
4432
|
-
|
|
5166
|
+
consola23.error(err instanceof Error ? err.message : String(err));
|
|
4433
5167
|
process.exit(1);
|
|
4434
5168
|
}
|
|
4435
5169
|
}
|
|
4436
5170
|
});
|
|
4437
5171
|
|
|
4438
5172
|
// src/commands/remove-language.ts
|
|
4439
|
-
import { defineCommand as
|
|
4440
|
-
import { consola as
|
|
4441
|
-
var removeLanguageCommand =
|
|
5173
|
+
import { defineCommand as defineCommand19 } from "citty";
|
|
5174
|
+
import { consola as consola24 } from "consola";
|
|
5175
|
+
var removeLanguageCommand = defineCommand19({
|
|
4442
5176
|
meta: {
|
|
4443
5177
|
name: "remove-language",
|
|
4444
5178
|
group: "edit",
|
|
@@ -4471,16 +5205,64 @@ var removeLanguageCommand = defineCommand17({
|
|
|
4471
5205
|
});
|
|
4472
5206
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4473
5207
|
} catch (err) {
|
|
4474
|
-
|
|
5208
|
+
consola24.error(err instanceof Error ? err.message : String(err));
|
|
5209
|
+
process.exit(1);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
});
|
|
5213
|
+
|
|
5214
|
+
// src/commands/remove-port.ts
|
|
5215
|
+
import { defineCommand as defineCommand20 } from "citty";
|
|
5216
|
+
import { consola as consola25 } from "consola";
|
|
5217
|
+
var removePortCommand = defineCommand20({
|
|
5218
|
+
meta: {
|
|
5219
|
+
name: "remove-port",
|
|
5220
|
+
group: "edit",
|
|
5221
|
+
description: "Remove one or more ports from the container config. Pass port numbers after `--` (e.g. `monoceros remove-port sandbox -- 3000 5173`). Idempotent \u2014 ports not present are skipped silently."
|
|
5222
|
+
},
|
|
5223
|
+
args: {
|
|
5224
|
+
name: {
|
|
5225
|
+
type: "positional",
|
|
5226
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
5227
|
+
required: true
|
|
5228
|
+
},
|
|
5229
|
+
yes: {
|
|
5230
|
+
type: "boolean",
|
|
5231
|
+
description: "Skip the interactive confirmation and apply the diff.",
|
|
5232
|
+
alias: ["y"],
|
|
5233
|
+
default: false
|
|
5234
|
+
}
|
|
5235
|
+
},
|
|
5236
|
+
async run({ args }) {
|
|
5237
|
+
const tokens = [...getInnerArgs()];
|
|
5238
|
+
if (tokens.length === 0) {
|
|
5239
|
+
consola25.error(
|
|
5240
|
+
"No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
|
|
5241
|
+
);
|
|
5242
|
+
process.exit(1);
|
|
5243
|
+
}
|
|
5244
|
+
try {
|
|
5245
|
+
const result = await runRemovePort({
|
|
5246
|
+
name: args.name,
|
|
5247
|
+
ports: tokens.map(coerceToken2),
|
|
5248
|
+
yes: args.yes
|
|
5249
|
+
});
|
|
5250
|
+
process.exit(result.status === "aborted" ? 1 : 0);
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
consola25.error(err instanceof Error ? err.message : String(err));
|
|
4475
5253
|
process.exit(1);
|
|
4476
5254
|
}
|
|
4477
5255
|
}
|
|
4478
5256
|
});
|
|
5257
|
+
function coerceToken2(raw) {
|
|
5258
|
+
const n = Number(raw);
|
|
5259
|
+
return Number.isFinite(n) ? n : raw;
|
|
5260
|
+
}
|
|
4479
5261
|
|
|
4480
5262
|
// src/commands/remove-repo.ts
|
|
4481
|
-
import { defineCommand as
|
|
4482
|
-
import { consola as
|
|
4483
|
-
var removeRepoCommand =
|
|
5263
|
+
import { defineCommand as defineCommand21 } from "citty";
|
|
5264
|
+
import { consola as consola26 } from "consola";
|
|
5265
|
+
var removeRepoCommand = defineCommand21({
|
|
4484
5266
|
meta: {
|
|
4485
5267
|
name: "remove-repo",
|
|
4486
5268
|
group: "edit",
|
|
@@ -4513,16 +5295,16 @@ var removeRepoCommand = defineCommand18({
|
|
|
4513
5295
|
});
|
|
4514
5296
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4515
5297
|
} catch (err) {
|
|
4516
|
-
|
|
5298
|
+
consola26.error(err instanceof Error ? err.message : String(err));
|
|
4517
5299
|
process.exit(1);
|
|
4518
5300
|
}
|
|
4519
5301
|
}
|
|
4520
5302
|
});
|
|
4521
5303
|
|
|
4522
5304
|
// src/commands/remove-service.ts
|
|
4523
|
-
import { defineCommand as
|
|
4524
|
-
import { consola as
|
|
4525
|
-
var removeServiceCommand =
|
|
5305
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
5306
|
+
import { consola as consola27 } from "consola";
|
|
5307
|
+
var removeServiceCommand = defineCommand22({
|
|
4526
5308
|
meta: {
|
|
4527
5309
|
name: "remove-service",
|
|
4528
5310
|
group: "edit",
|
|
@@ -4555,19 +5337,19 @@ var removeServiceCommand = defineCommand19({
|
|
|
4555
5337
|
});
|
|
4556
5338
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4557
5339
|
} catch (err) {
|
|
4558
|
-
|
|
5340
|
+
consola27.error(err instanceof Error ? err.message : String(err));
|
|
4559
5341
|
process.exit(1);
|
|
4560
5342
|
}
|
|
4561
5343
|
}
|
|
4562
5344
|
});
|
|
4563
5345
|
|
|
4564
5346
|
// src/commands/run.ts
|
|
4565
|
-
import { defineCommand as
|
|
4566
|
-
import { consola as
|
|
5347
|
+
import { defineCommand as defineCommand23 } from "citty";
|
|
5348
|
+
import { consola as consola28 } from "consola";
|
|
4567
5349
|
|
|
4568
5350
|
// src/devcontainer/shell.ts
|
|
4569
5351
|
import { existsSync as existsSync10 } from "fs";
|
|
4570
|
-
import
|
|
5352
|
+
import path14 from "path";
|
|
4571
5353
|
async function runShell(opts) {
|
|
4572
5354
|
assertContainerExists(opts.root);
|
|
4573
5355
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -4590,7 +5372,7 @@ async function runShell(opts) {
|
|
|
4590
5372
|
);
|
|
4591
5373
|
}
|
|
4592
5374
|
function assertContainerExists(root) {
|
|
4593
|
-
if (!existsSync10(
|
|
5375
|
+
if (!existsSync10(path14.join(root, ".devcontainer"))) {
|
|
4594
5376
|
throw new Error(
|
|
4595
5377
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
4596
5378
|
);
|
|
@@ -4626,7 +5408,7 @@ async function runInContainer(opts) {
|
|
|
4626
5408
|
}
|
|
4627
5409
|
|
|
4628
5410
|
// src/commands/run.ts
|
|
4629
|
-
var runCommand =
|
|
5411
|
+
var runCommand = defineCommand23({
|
|
4630
5412
|
meta: {
|
|
4631
5413
|
name: "run",
|
|
4632
5414
|
group: "run",
|
|
@@ -4642,7 +5424,7 @@ var runCommand = defineCommand20({
|
|
|
4642
5424
|
async run({ args }) {
|
|
4643
5425
|
const command = [...getInnerArgs()];
|
|
4644
5426
|
if (command.length === 0) {
|
|
4645
|
-
|
|
5427
|
+
consola28.error(
|
|
4646
5428
|
"No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
|
|
4647
5429
|
);
|
|
4648
5430
|
process.exit(1);
|
|
@@ -4654,16 +5436,16 @@ var runCommand = defineCommand20({
|
|
|
4654
5436
|
});
|
|
4655
5437
|
process.exit(exitCode);
|
|
4656
5438
|
} catch (err) {
|
|
4657
|
-
|
|
5439
|
+
consola28.error(err instanceof Error ? err.message : String(err));
|
|
4658
5440
|
process.exit(1);
|
|
4659
5441
|
}
|
|
4660
5442
|
}
|
|
4661
5443
|
});
|
|
4662
5444
|
|
|
4663
5445
|
// src/commands/shell.ts
|
|
4664
|
-
import { defineCommand as
|
|
4665
|
-
import { consola as
|
|
4666
|
-
var shellCommand =
|
|
5446
|
+
import { defineCommand as defineCommand24 } from "citty";
|
|
5447
|
+
import { consola as consola29 } from "consola";
|
|
5448
|
+
var shellCommand = defineCommand24({
|
|
4667
5449
|
meta: {
|
|
4668
5450
|
name: "shell",
|
|
4669
5451
|
group: "run",
|
|
@@ -4681,15 +5463,16 @@ var shellCommand = defineCommand21({
|
|
|
4681
5463
|
const exitCode = await runShell({ root: containerDir(args.name) });
|
|
4682
5464
|
process.exit(exitCode);
|
|
4683
5465
|
} catch (err) {
|
|
4684
|
-
|
|
5466
|
+
consola29.error(err instanceof Error ? err.message : String(err));
|
|
4685
5467
|
process.exit(1);
|
|
4686
5468
|
}
|
|
4687
5469
|
}
|
|
4688
5470
|
});
|
|
4689
5471
|
|
|
4690
5472
|
// src/commands/start.ts
|
|
4691
|
-
import { defineCommand as
|
|
4692
|
-
|
|
5473
|
+
import { defineCommand as defineCommand25 } from "citty";
|
|
5474
|
+
import { consola as consola30 } from "consola";
|
|
5475
|
+
var startCommand = defineCommand25({
|
|
4693
5476
|
meta: {
|
|
4694
5477
|
name: "start",
|
|
4695
5478
|
group: "run",
|
|
@@ -4703,13 +5486,33 @@ var startCommand = defineCommand22({
|
|
|
4703
5486
|
}
|
|
4704
5487
|
},
|
|
4705
5488
|
run({ args }) {
|
|
4706
|
-
return dispatch(() =>
|
|
5489
|
+
return dispatch(async () => {
|
|
5490
|
+
let needsProxy = false;
|
|
5491
|
+
let hostPort = 80;
|
|
5492
|
+
try {
|
|
5493
|
+
const parsed = await readConfig(containerConfigPath(args.name));
|
|
5494
|
+
if ((parsed.config.routing?.ports ?? []).length > 0) {
|
|
5495
|
+
needsProxy = true;
|
|
5496
|
+
const global = await readMonocerosConfig();
|
|
5497
|
+
hostPort = proxyHostPort(global);
|
|
5498
|
+
}
|
|
5499
|
+
} catch (err) {
|
|
5500
|
+
consola30.warn(
|
|
5501
|
+
`Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
|
|
5502
|
+
);
|
|
5503
|
+
}
|
|
5504
|
+
if (needsProxy) {
|
|
5505
|
+
await preflightHostPort(hostPort);
|
|
5506
|
+
await ensureProxy({ hostPort });
|
|
5507
|
+
}
|
|
5508
|
+
return runStart({ root: containerDir(args.name) });
|
|
5509
|
+
});
|
|
4707
5510
|
}
|
|
4708
5511
|
});
|
|
4709
5512
|
|
|
4710
5513
|
// src/commands/status.ts
|
|
4711
|
-
import { defineCommand as
|
|
4712
|
-
var statusCommand =
|
|
5514
|
+
import { defineCommand as defineCommand26 } from "citty";
|
|
5515
|
+
var statusCommand = defineCommand26({
|
|
4713
5516
|
meta: {
|
|
4714
5517
|
name: "status",
|
|
4715
5518
|
group: "run",
|
|
@@ -4737,8 +5540,9 @@ var statusCommand = defineCommand23({
|
|
|
4737
5540
|
});
|
|
4738
5541
|
|
|
4739
5542
|
// src/commands/stop.ts
|
|
4740
|
-
import { defineCommand as
|
|
4741
|
-
|
|
5543
|
+
import { defineCommand as defineCommand27 } from "citty";
|
|
5544
|
+
import { consola as consola31 } from "consola";
|
|
5545
|
+
var stopCommand = defineCommand27({
|
|
4742
5546
|
meta: {
|
|
4743
5547
|
name: "stop",
|
|
4744
5548
|
group: "run",
|
|
@@ -4756,17 +5560,27 @@ var stopCommand = defineCommand24({
|
|
|
4756
5560
|
}
|
|
4757
5561
|
},
|
|
4758
5562
|
run({ args }) {
|
|
4759
|
-
return dispatch(
|
|
4760
|
-
|
|
5563
|
+
return dispatch(async () => {
|
|
5564
|
+
const exit = await runStop({
|
|
4761
5565
|
root: containerDir(args.name),
|
|
4762
5566
|
...typeof args.service === "string" ? { service: args.service } : {}
|
|
4763
|
-
})
|
|
4764
|
-
|
|
5567
|
+
});
|
|
5568
|
+
try {
|
|
5569
|
+
await maybeStopProxy({
|
|
5570
|
+
logger: { info: (msg) => consola31.info(msg) }
|
|
5571
|
+
});
|
|
5572
|
+
} catch (err) {
|
|
5573
|
+
consola31.warn(
|
|
5574
|
+
`Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
|
|
5575
|
+
);
|
|
5576
|
+
}
|
|
5577
|
+
return exit;
|
|
5578
|
+
});
|
|
4765
5579
|
}
|
|
4766
5580
|
});
|
|
4767
5581
|
|
|
4768
5582
|
// src/main.ts
|
|
4769
|
-
var main =
|
|
5583
|
+
var main = defineCommand28({
|
|
4770
5584
|
meta: {
|
|
4771
5585
|
name: "monoceros",
|
|
4772
5586
|
version: CLI_VERSION,
|
|
@@ -4790,12 +5604,15 @@ var main = defineCommand25({
|
|
|
4790
5604
|
"add-feature": addFeatureCommand,
|
|
4791
5605
|
"add-from-url": addFromUrlCommand,
|
|
4792
5606
|
"add-repo": addRepoCommand,
|
|
5607
|
+
"add-port": addPortCommand,
|
|
4793
5608
|
"remove-service": removeServiceCommand,
|
|
4794
5609
|
"remove-language": removeLanguageCommand,
|
|
4795
5610
|
"remove-apt-packages": removeAptPackagesCommand,
|
|
4796
5611
|
"remove-feature": removeFeatureCommand,
|
|
4797
5612
|
"remove-from-url": removeFromUrlCommand,
|
|
4798
5613
|
"remove-repo": removeRepoCommand,
|
|
5614
|
+
"remove-port": removePortCommand,
|
|
5615
|
+
port: portCommand,
|
|
4799
5616
|
completion: completionCommand
|
|
4800
5617
|
}
|
|
4801
5618
|
});
|