@getmonoceros/workbench 1.6.12 → 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 +1109 -298
- 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
|
{
|
|
@@ -2732,13 +3309,13 @@ function formatRootlessNotSupportedError() {
|
|
|
2732
3309
|
}
|
|
2733
3310
|
|
|
2734
3311
|
// src/devcontainer/identity.ts
|
|
2735
|
-
import { spawn as
|
|
2736
|
-
import { promises as
|
|
2737
|
-
import
|
|
2738
|
-
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";
|
|
2739
3316
|
var realGitConfigGet = (key) => {
|
|
2740
3317
|
return new Promise((resolve, reject) => {
|
|
2741
|
-
const child =
|
|
3318
|
+
const child = spawn7("git", ["config", "--global", "--get", key], {
|
|
2742
3319
|
stdio: ["ignore", "pipe", "inherit"]
|
|
2743
3320
|
});
|
|
2744
3321
|
let stdout = "";
|
|
@@ -2757,14 +3334,14 @@ var realIdentityPrompt = async (key) => {
|
|
|
2757
3334
|
return void 0;
|
|
2758
3335
|
}
|
|
2759
3336
|
const label = key === "user.name" ? "Git user.name for this dev container (full name)" : "Git user.email for this dev container";
|
|
2760
|
-
const value = await
|
|
3337
|
+
const value = await consola10.prompt(`${label}:`, { type: "text" });
|
|
2761
3338
|
if (typeof value !== "string") return void 0;
|
|
2762
3339
|
const trimmed = value.trim();
|
|
2763
3340
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
2764
3341
|
};
|
|
2765
3342
|
async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
2766
|
-
const gitconfigDir =
|
|
2767
|
-
const gitconfigPath =
|
|
3343
|
+
const gitconfigDir = path9.join(devContainerRoot, ".monoceros");
|
|
3344
|
+
const gitconfigPath = path9.join(gitconfigDir, "gitconfig");
|
|
2768
3345
|
const spawnFn = options.spawn ?? realGitConfigGet;
|
|
2769
3346
|
const promptFn = options.prompt ?? realIdentityPrompt;
|
|
2770
3347
|
const logger = options.logger ?? { info: () => {
|
|
@@ -2790,8 +3367,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
|
|
|
2790
3367
|
const lines = ["[user]"];
|
|
2791
3368
|
if (name !== void 0) lines.push(` name = ${name}`);
|
|
2792
3369
|
if (email !== void 0) lines.push(` email = ${email}`);
|
|
2793
|
-
await
|
|
2794
|
-
await
|
|
3370
|
+
await fs9.mkdir(gitconfigDir, { recursive: true });
|
|
3371
|
+
await fs9.writeFile(gitconfigPath, lines.join("\n") + "\n");
|
|
2795
3372
|
return {
|
|
2796
3373
|
...name !== void 0 ? { name } : {},
|
|
2797
3374
|
...email !== void 0 ? { email } : {},
|
|
@@ -2833,7 +3410,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
|
|
|
2833
3410
|
}
|
|
2834
3411
|
async function readExistingGitconfig(filePath) {
|
|
2835
3412
|
try {
|
|
2836
|
-
const content = await
|
|
3413
|
+
const content = await fs9.readFile(filePath, "utf8");
|
|
2837
3414
|
const result = {};
|
|
2838
3415
|
const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
|
|
2839
3416
|
const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
|
|
@@ -2849,9 +3426,9 @@ async function readExistingGitconfig(filePath) {
|
|
|
2849
3426
|
async function runApply(opts) {
|
|
2850
3427
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
2851
3428
|
const logger = opts.logger ?? {
|
|
2852
|
-
info: (msg) =>
|
|
2853
|
-
success: (msg) =>
|
|
2854
|
-
warn: (msg) =>
|
|
3429
|
+
info: (msg) => consola11.info(msg),
|
|
3430
|
+
success: (msg) => consola11.success(msg),
|
|
3431
|
+
warn: (msg) => consola11.warn(msg),
|
|
2855
3432
|
// Default section renderer: empty line, bold-underlined "▸ Label",
|
|
2856
3433
|
// empty line. Mirrors install.sh's section visuals.
|
|
2857
3434
|
section: (label) => process.stderr.write(`
|
|
@@ -2933,7 +3510,7 @@ ${sectionLine(label)}
|
|
|
2933
3510
|
if (dockerMode === "rootless") {
|
|
2934
3511
|
throw new Error(formatRootlessNotSupportedError());
|
|
2935
3512
|
}
|
|
2936
|
-
await
|
|
3513
|
+
await fs10.mkdir(targetDir, { recursive: true });
|
|
2937
3514
|
await writeScaffold(createOpts, targetDir, { dockerMode });
|
|
2938
3515
|
await writeStateFile(
|
|
2939
3516
|
targetDir,
|
|
@@ -2954,6 +3531,30 @@ ${sectionLine(label)}
|
|
|
2954
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.'
|
|
2955
3532
|
)
|
|
2956
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
|
+
}
|
|
2957
3558
|
const exitCode = await runContainerCycle(targetDir, {
|
|
2958
3559
|
hasCompose: needsCompose(createOpts),
|
|
2959
3560
|
...opts.cleanupSpawn !== void 0 ? { cleanupSpawn: opts.cleanupSpawn } : {},
|
|
@@ -2968,7 +3569,7 @@ ${sectionLine(label)}
|
|
|
2968
3569
|
}
|
|
2969
3570
|
async function assertSafeTargetDir(targetDir, expectedOrigin) {
|
|
2970
3571
|
if (!existsSync4(targetDir)) return;
|
|
2971
|
-
const entries = await
|
|
3572
|
+
const entries = await fs10.readdir(targetDir);
|
|
2972
3573
|
if (entries.length === 0) return;
|
|
2973
3574
|
const state = await readStateFile(targetDir);
|
|
2974
3575
|
if (state) {
|
|
@@ -3010,22 +3611,22 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
|
|
|
3010
3611
|
}
|
|
3011
3612
|
|
|
3012
3613
|
// src/version.ts
|
|
3013
|
-
var CLI_VERSION = true ? "1.
|
|
3614
|
+
var CLI_VERSION = true ? "1.7.0" : "dev";
|
|
3014
3615
|
|
|
3015
3616
|
// src/commands/_dispatch.ts
|
|
3016
|
-
import { consola as
|
|
3617
|
+
import { consola as consola12 } from "consola";
|
|
3017
3618
|
async function dispatch(runner) {
|
|
3018
3619
|
try {
|
|
3019
3620
|
const exitCode = await runner();
|
|
3020
3621
|
process.exit(exitCode);
|
|
3021
3622
|
} catch (err) {
|
|
3022
|
-
|
|
3623
|
+
consola12.error(err instanceof Error ? err.message : String(err));
|
|
3023
3624
|
process.exit(1);
|
|
3024
3625
|
}
|
|
3025
3626
|
}
|
|
3026
3627
|
|
|
3027
3628
|
// src/commands/apply.ts
|
|
3028
|
-
var applyCommand =
|
|
3629
|
+
var applyCommand = defineCommand8({
|
|
3029
3630
|
meta: {
|
|
3030
3631
|
name: "apply",
|
|
3031
3632
|
group: "lifecycle",
|
|
@@ -3050,7 +3651,7 @@ var applyCommand = defineCommand7({
|
|
|
3050
3651
|
});
|
|
3051
3652
|
|
|
3052
3653
|
// src/commands/completion.ts
|
|
3053
|
-
import { defineCommand as
|
|
3654
|
+
import { defineCommand as defineCommand9 } from "citty";
|
|
3054
3655
|
var ALL_COMMANDS = [
|
|
3055
3656
|
"init",
|
|
3056
3657
|
"list-components",
|
|
@@ -3069,12 +3670,15 @@ var ALL_COMMANDS = [
|
|
|
3069
3670
|
"add-feature",
|
|
3070
3671
|
"add-from-url",
|
|
3071
3672
|
"add-repo",
|
|
3673
|
+
"add-port",
|
|
3072
3674
|
"remove-service",
|
|
3073
3675
|
"remove-language",
|
|
3074
3676
|
"remove-apt-packages",
|
|
3075
3677
|
"remove-feature",
|
|
3076
3678
|
"remove-from-url",
|
|
3077
3679
|
"remove-repo",
|
|
3680
|
+
"remove-port",
|
|
3681
|
+
"port",
|
|
3078
3682
|
"completion"
|
|
3079
3683
|
];
|
|
3080
3684
|
var COMMANDS_WITH_CONTAINER_ARG = [
|
|
@@ -3092,12 +3696,15 @@ var COMMANDS_WITH_CONTAINER_ARG = [
|
|
|
3092
3696
|
"add-feature",
|
|
3093
3697
|
"add-from-url",
|
|
3094
3698
|
"add-repo",
|
|
3699
|
+
"add-port",
|
|
3095
3700
|
"remove-service",
|
|
3096
3701
|
"remove-language",
|
|
3097
3702
|
"remove-apt-packages",
|
|
3098
3703
|
"remove-feature",
|
|
3099
3704
|
"remove-from-url",
|
|
3100
|
-
"remove-repo"
|
|
3705
|
+
"remove-repo",
|
|
3706
|
+
"remove-port",
|
|
3707
|
+
"port"
|
|
3101
3708
|
];
|
|
3102
3709
|
var SHELLS = ["bash", "zsh", "pwsh"];
|
|
3103
3710
|
function renderCompletionScript(shell) {
|
|
@@ -3230,7 +3837,7 @@ function renderCompletionScript(shell) {
|
|
|
3230
3837
|
""
|
|
3231
3838
|
].join("\n");
|
|
3232
3839
|
}
|
|
3233
|
-
var completionCommand =
|
|
3840
|
+
var completionCommand = defineCommand9({
|
|
3234
3841
|
meta: {
|
|
3235
3842
|
name: "completion",
|
|
3236
3843
|
group: "tooling",
|
|
@@ -3257,16 +3864,16 @@ var completionCommand = defineCommand8({
|
|
|
3257
3864
|
});
|
|
3258
3865
|
|
|
3259
3866
|
// src/commands/init.ts
|
|
3260
|
-
import { defineCommand as
|
|
3261
|
-
import { consola as
|
|
3867
|
+
import { defineCommand as defineCommand10 } from "citty";
|
|
3868
|
+
import { consola as consola14 } from "consola";
|
|
3262
3869
|
|
|
3263
3870
|
// src/init/index.ts
|
|
3264
|
-
import { existsSync as existsSync7, promises as
|
|
3265
|
-
import { consola as
|
|
3871
|
+
import { existsSync as existsSync7, promises as fs12 } from "fs";
|
|
3872
|
+
import { consola as consola13 } from "consola";
|
|
3266
3873
|
|
|
3267
3874
|
// src/init/components.ts
|
|
3268
|
-
import { existsSync as existsSync5, promises as
|
|
3269
|
-
import
|
|
3875
|
+
import { existsSync as existsSync5, promises as fs11 } from "fs";
|
|
3876
|
+
import path10 from "path";
|
|
3270
3877
|
import { z as z3 } from "zod";
|
|
3271
3878
|
import { parse as parseYaml } from "yaml";
|
|
3272
3879
|
var CategorySchema = z3.enum(["language", "service", "feature"]);
|
|
@@ -3321,17 +3928,17 @@ async function loadComponentCatalog(rootDir = componentsDir()) {
|
|
|
3321
3928
|
return out;
|
|
3322
3929
|
}
|
|
3323
3930
|
async function walk(baseDir, currentDir, out) {
|
|
3324
|
-
const entries = await
|
|
3931
|
+
const entries = await fs11.readdir(currentDir, { withFileTypes: true });
|
|
3325
3932
|
for (const entry2 of entries) {
|
|
3326
|
-
const full =
|
|
3933
|
+
const full = path10.join(currentDir, entry2.name);
|
|
3327
3934
|
if (entry2.isDirectory()) {
|
|
3328
3935
|
await walk(baseDir, full, out);
|
|
3329
3936
|
continue;
|
|
3330
3937
|
}
|
|
3331
3938
|
if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
|
|
3332
|
-
const relative =
|
|
3333
|
-
const name = relative.replace(/\.yml$/, "").split(
|
|
3334
|
-
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");
|
|
3335
3942
|
let raw;
|
|
3336
3943
|
try {
|
|
3337
3944
|
raw = parseYaml(text);
|
|
@@ -3587,6 +4194,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = []) {
|
|
|
3587
4194
|
/* commented */
|
|
3588
4195
|
repoUrls.length === 0
|
|
3589
4196
|
);
|
|
4197
|
+
renderRoutingBlock(lines);
|
|
3590
4198
|
return ensureTrailingNewline(lines.join("\n"));
|
|
3591
4199
|
}
|
|
3592
4200
|
var COMMENT_WIDTH = 72;
|
|
@@ -3686,6 +4294,31 @@ function renderReposBlock(out, urls, commented) {
|
|
|
3686
4294
|
}
|
|
3687
4295
|
out.push("");
|
|
3688
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
|
+
}
|
|
3689
4322
|
function deriveDefaultPath(url) {
|
|
3690
4323
|
let last = url;
|
|
3691
4324
|
const slash = url.lastIndexOf("/");
|
|
@@ -3740,10 +4373,10 @@ function ensureTrailingNewline(s) {
|
|
|
3740
4373
|
|
|
3741
4374
|
// src/init/manifest.ts
|
|
3742
4375
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
3743
|
-
import
|
|
4376
|
+
import path11 from "path";
|
|
3744
4377
|
function resolveManifestPath(name, checkoutRoot) {
|
|
3745
4378
|
if (checkoutRoot) {
|
|
3746
|
-
const checkoutPath =
|
|
4379
|
+
const checkoutPath = path11.join(
|
|
3747
4380
|
checkoutRoot,
|
|
3748
4381
|
"images",
|
|
3749
4382
|
"features",
|
|
@@ -3752,7 +4385,7 @@ function resolveManifestPath(name, checkoutRoot) {
|
|
|
3752
4385
|
);
|
|
3753
4386
|
if (existsSync6(checkoutPath)) return checkoutPath;
|
|
3754
4387
|
}
|
|
3755
|
-
const bundlePath =
|
|
4388
|
+
const bundlePath = path11.join(
|
|
3756
4389
|
bundledFeaturesDir(),
|
|
3757
4390
|
name,
|
|
3758
4391
|
"devcontainer-feature.json"
|
|
@@ -3795,8 +4428,8 @@ async function runInit(opts) {
|
|
|
3795
4428
|
const workbench = opts.workbenchRoot ?? workbenchRoot();
|
|
3796
4429
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
3797
4430
|
const logger = opts.logger ?? {
|
|
3798
|
-
success: (msg) =>
|
|
3799
|
-
info: (msg) =>
|
|
4431
|
+
success: (msg) => consola13.success(msg),
|
|
4432
|
+
info: (msg) => consola13.info(msg)
|
|
3800
4433
|
};
|
|
3801
4434
|
if (!REGEX.solutionName.test(opts.name)) {
|
|
3802
4435
|
throw new Error(
|
|
@@ -3857,8 +4490,8 @@ async function runInit(opts) {
|
|
|
3857
4490
|
const components = resolveComponents(catalog, requested);
|
|
3858
4491
|
text = generateComposedYml(opts.name, components, lookup, repos);
|
|
3859
4492
|
}
|
|
3860
|
-
await
|
|
3861
|
-
await
|
|
4493
|
+
await fs12.mkdir(containerConfigsDir(home), { recursive: true });
|
|
4494
|
+
await fs12.writeFile(dest, text, "utf8");
|
|
3862
4495
|
const documented = requested.length === 0;
|
|
3863
4496
|
const displayPath = prettyPath(dest);
|
|
3864
4497
|
if (documented) {
|
|
@@ -3877,7 +4510,7 @@ async function runInit(opts) {
|
|
|
3877
4510
|
}
|
|
3878
4511
|
|
|
3879
4512
|
// src/commands/init.ts
|
|
3880
|
-
var initCommand =
|
|
4513
|
+
var initCommand = defineCommand10({
|
|
3881
4514
|
meta: {
|
|
3882
4515
|
name: "init",
|
|
3883
4516
|
group: "lifecycle",
|
|
@@ -3910,7 +4543,7 @@ var initCommand = defineCommand9({
|
|
|
3910
4543
|
...withRepoList.length > 0 ? { withRepo: withRepoList } : {}
|
|
3911
4544
|
});
|
|
3912
4545
|
} catch (err) {
|
|
3913
|
-
|
|
4546
|
+
consola14.error(err instanceof Error ? err.message : String(err));
|
|
3914
4547
|
process.exit(1);
|
|
3915
4548
|
}
|
|
3916
4549
|
}
|
|
@@ -3954,8 +4587,8 @@ function collectWithList(withArg, rawArgs) {
|
|
|
3954
4587
|
}
|
|
3955
4588
|
|
|
3956
4589
|
// src/commands/list-components.ts
|
|
3957
|
-
import { defineCommand as
|
|
3958
|
-
import { consola as
|
|
4590
|
+
import { defineCommand as defineCommand11 } from "citty";
|
|
4591
|
+
import { consola as consola15 } from "consola";
|
|
3959
4592
|
var CATEGORY_LABELS = {
|
|
3960
4593
|
language: "Languages",
|
|
3961
4594
|
service: "Services",
|
|
@@ -3966,7 +4599,7 @@ var CATEGORY_ORDER = [
|
|
|
3966
4599
|
"service",
|
|
3967
4600
|
"feature"
|
|
3968
4601
|
];
|
|
3969
|
-
var listComponentsCommand =
|
|
4602
|
+
var listComponentsCommand = defineCommand11({
|
|
3970
4603
|
meta: {
|
|
3971
4604
|
name: "list-components",
|
|
3972
4605
|
group: "discovery",
|
|
@@ -3977,7 +4610,7 @@ var listComponentsCommand = defineCommand10({
|
|
|
3977
4610
|
try {
|
|
3978
4611
|
const catalog = await loadComponentCatalog();
|
|
3979
4612
|
if (catalog.size === 0) {
|
|
3980
|
-
|
|
4613
|
+
consola15.warn(
|
|
3981
4614
|
"No components found. The workbench checkout looks incomplete."
|
|
3982
4615
|
);
|
|
3983
4616
|
process.exit(0);
|
|
@@ -4028,15 +4661,15 @@ var listComponentsCommand = defineCommand10({
|
|
|
4028
4661
|
}
|
|
4029
4662
|
process.exit(0);
|
|
4030
4663
|
} catch (err) {
|
|
4031
|
-
|
|
4664
|
+
consola15.error(err instanceof Error ? err.message : String(err));
|
|
4032
4665
|
process.exit(1);
|
|
4033
4666
|
}
|
|
4034
4667
|
}
|
|
4035
4668
|
});
|
|
4036
4669
|
|
|
4037
4670
|
// src/commands/logs.ts
|
|
4038
|
-
import { defineCommand as
|
|
4039
|
-
var logsCommand =
|
|
4671
|
+
import { defineCommand as defineCommand12 } from "citty";
|
|
4672
|
+
var logsCommand = defineCommand12({
|
|
4040
4673
|
meta: {
|
|
4041
4674
|
name: "logs",
|
|
4042
4675
|
group: "run",
|
|
@@ -4070,10 +4703,87 @@ var logsCommand = defineCommand11({
|
|
|
4070
4703
|
}
|
|
4071
4704
|
});
|
|
4072
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
|
+
|
|
4073
4783
|
// src/commands/remove-apt-packages.ts
|
|
4074
|
-
import { defineCommand as
|
|
4075
|
-
import { consola as
|
|
4076
|
-
var removeAptPackagesCommand =
|
|
4784
|
+
import { defineCommand as defineCommand14 } from "citty";
|
|
4785
|
+
import { consola as consola17 } from "consola";
|
|
4786
|
+
var removeAptPackagesCommand = defineCommand14({
|
|
4077
4787
|
meta: {
|
|
4078
4788
|
name: "remove-apt-packages",
|
|
4079
4789
|
group: "edit",
|
|
@@ -4095,7 +4805,7 @@ var removeAptPackagesCommand = defineCommand12({
|
|
|
4095
4805
|
async run({ args }) {
|
|
4096
4806
|
const packages = [...getInnerArgs()];
|
|
4097
4807
|
if (packages.length === 0) {
|
|
4098
|
-
|
|
4808
|
+
consola17.error(
|
|
4099
4809
|
"No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
|
|
4100
4810
|
);
|
|
4101
4811
|
process.exit(1);
|
|
@@ -4108,16 +4818,16 @@ var removeAptPackagesCommand = defineCommand12({
|
|
|
4108
4818
|
});
|
|
4109
4819
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4110
4820
|
} catch (err) {
|
|
4111
|
-
|
|
4821
|
+
consola17.error(err instanceof Error ? err.message : String(err));
|
|
4112
4822
|
process.exit(1);
|
|
4113
4823
|
}
|
|
4114
4824
|
}
|
|
4115
4825
|
});
|
|
4116
4826
|
|
|
4117
4827
|
// src/commands/remove-feature.ts
|
|
4118
|
-
import { defineCommand as
|
|
4119
|
-
import { consola as
|
|
4120
|
-
var removeFeatureCommand =
|
|
4828
|
+
import { defineCommand as defineCommand15 } from "citty";
|
|
4829
|
+
import { consola as consola18 } from "consola";
|
|
4830
|
+
var removeFeatureCommand = defineCommand15({
|
|
4121
4831
|
meta: {
|
|
4122
4832
|
name: "remove-feature",
|
|
4123
4833
|
group: "edit",
|
|
@@ -4150,27 +4860,27 @@ var removeFeatureCommand = defineCommand13({
|
|
|
4150
4860
|
});
|
|
4151
4861
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4152
4862
|
} catch (err) {
|
|
4153
|
-
|
|
4863
|
+
consola18.error(err instanceof Error ? err.message : String(err));
|
|
4154
4864
|
process.exit(1);
|
|
4155
4865
|
}
|
|
4156
4866
|
}
|
|
4157
4867
|
});
|
|
4158
4868
|
|
|
4159
4869
|
// src/commands/remove.ts
|
|
4160
|
-
import { defineCommand as
|
|
4161
|
-
import { consola as
|
|
4870
|
+
import { defineCommand as defineCommand16 } from "citty";
|
|
4871
|
+
import { consola as consola20 } from "consola";
|
|
4162
4872
|
import { createInterface } from "readline/promises";
|
|
4163
4873
|
|
|
4164
4874
|
// src/remove/index.ts
|
|
4165
|
-
import { existsSync as existsSync8, promises as
|
|
4166
|
-
import
|
|
4167
|
-
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";
|
|
4168
4878
|
async function runRemove(opts) {
|
|
4169
4879
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
4170
4880
|
const logger = opts.logger ?? {
|
|
4171
|
-
info: (msg) =>
|
|
4172
|
-
success: (msg) =>
|
|
4173
|
-
warn: (msg) =>
|
|
4881
|
+
info: (msg) => consola19.info(msg),
|
|
4882
|
+
success: (msg) => consola19.success(msg),
|
|
4883
|
+
warn: (msg) => consola19.warn(msg)
|
|
4174
4884
|
};
|
|
4175
4885
|
if (!REGEX.solutionName.test(opts.name)) {
|
|
4176
4886
|
throw new Error(
|
|
@@ -4214,23 +4924,23 @@ async function runRemove(opts) {
|
|
|
4214
4924
|
let backupPath = null;
|
|
4215
4925
|
if (!opts.noBackup && (hasYml || hasContainer)) {
|
|
4216
4926
|
const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4217
|
-
backupPath =
|
|
4218
|
-
await
|
|
4927
|
+
backupPath = path12.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
4928
|
+
await fs13.mkdir(backupPath, { recursive: true });
|
|
4219
4929
|
if (hasYml) {
|
|
4220
|
-
await
|
|
4930
|
+
await fs13.copyFile(ymlPath, path12.join(backupPath, `${opts.name}.yml`));
|
|
4221
4931
|
}
|
|
4222
4932
|
if (hasContainer) {
|
|
4223
|
-
await
|
|
4933
|
+
await fs13.cp(containerPath, path12.join(backupPath, "container"), {
|
|
4224
4934
|
recursive: true
|
|
4225
4935
|
});
|
|
4226
4936
|
}
|
|
4227
4937
|
logger.info(`Backup written to ${prettyPath(backupPath)}.`);
|
|
4228
4938
|
}
|
|
4229
4939
|
if (hasYml) {
|
|
4230
|
-
await
|
|
4940
|
+
await fs13.rm(ymlPath, { force: true });
|
|
4231
4941
|
}
|
|
4232
4942
|
if (hasContainer) {
|
|
4233
|
-
await
|
|
4943
|
+
await fs13.rm(containerPath, { recursive: true, force: true });
|
|
4234
4944
|
}
|
|
4235
4945
|
logger.success(
|
|
4236
4946
|
`Removed '${opts.name}': docker objects gone, container-configs entry deleted, container directory deleted.`
|
|
@@ -4240,6 +4950,24 @@ async function runRemove(opts) {
|
|
|
4240
4950
|
"No backup created (--no-backup). The host-side state is gone for good."
|
|
4241
4951
|
);
|
|
4242
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
|
+
}
|
|
4243
4971
|
return {
|
|
4244
4972
|
configPath: hasYml ? ymlPath : null,
|
|
4245
4973
|
containerPath: hasContainer ? containerPath : null,
|
|
@@ -4249,7 +4977,7 @@ async function runRemove(opts) {
|
|
|
4249
4977
|
}
|
|
4250
4978
|
|
|
4251
4979
|
// src/commands/remove.ts
|
|
4252
|
-
var removeCommand =
|
|
4980
|
+
var removeCommand = defineCommand16({
|
|
4253
4981
|
meta: {
|
|
4254
4982
|
name: "remove",
|
|
4255
4983
|
group: "lifecycle",
|
|
@@ -4286,7 +5014,7 @@ var removeCommand = defineCommand14({
|
|
|
4286
5014
|
const skipPrompt = args.yes === true;
|
|
4287
5015
|
if (!skipPrompt) {
|
|
4288
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.`;
|
|
4289
|
-
|
|
5017
|
+
consola20.warn(warning);
|
|
4290
5018
|
const rl = createInterface({
|
|
4291
5019
|
input: process.stdin,
|
|
4292
5020
|
output: process.stdout
|
|
@@ -4294,7 +5022,7 @@ var removeCommand = defineCommand14({
|
|
|
4294
5022
|
const answer = await rl.question("Continue? [y/N] ");
|
|
4295
5023
|
rl.close();
|
|
4296
5024
|
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
4297
|
-
|
|
5025
|
+
consola20.info("Aborted. Nothing changed.");
|
|
4298
5026
|
process.exit(0);
|
|
4299
5027
|
}
|
|
4300
5028
|
}
|
|
@@ -4303,35 +5031,35 @@ var removeCommand = defineCommand14({
|
|
|
4303
5031
|
...noBackup ? { noBackup: true } : {}
|
|
4304
5032
|
});
|
|
4305
5033
|
} catch (err) {
|
|
4306
|
-
|
|
5034
|
+
consola20.error(err instanceof Error ? err.message : String(err));
|
|
4307
5035
|
process.exit(1);
|
|
4308
5036
|
}
|
|
4309
5037
|
}
|
|
4310
5038
|
});
|
|
4311
5039
|
|
|
4312
5040
|
// src/commands/restore.ts
|
|
4313
|
-
import { defineCommand as
|
|
4314
|
-
import { consola as
|
|
5041
|
+
import { defineCommand as defineCommand17 } from "citty";
|
|
5042
|
+
import { consola as consola22 } from "consola";
|
|
4315
5043
|
|
|
4316
5044
|
// src/restore/index.ts
|
|
4317
|
-
import { existsSync as existsSync9, promises as
|
|
4318
|
-
import
|
|
4319
|
-
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";
|
|
4320
5048
|
async function runRestore(opts) {
|
|
4321
5049
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
4322
5050
|
const logger = opts.logger ?? {
|
|
4323
|
-
info: (msg) =>
|
|
4324
|
-
success: (msg) =>
|
|
5051
|
+
info: (msg) => consola21.info(msg),
|
|
5052
|
+
success: (msg) => consola21.success(msg)
|
|
4325
5053
|
};
|
|
4326
|
-
const backup =
|
|
5054
|
+
const backup = path13.resolve(opts.backupPath);
|
|
4327
5055
|
if (!existsSync9(backup)) {
|
|
4328
5056
|
throw new Error(`Backup not found: ${backup}.`);
|
|
4329
5057
|
}
|
|
4330
|
-
const stat = await
|
|
5058
|
+
const stat = await fs14.stat(backup);
|
|
4331
5059
|
if (!stat.isDirectory()) {
|
|
4332
5060
|
throw new Error(`Backup path is not a directory: ${backup}.`);
|
|
4333
5061
|
}
|
|
4334
|
-
const entries = await
|
|
5062
|
+
const entries = await fs14.readdir(backup);
|
|
4335
5063
|
const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
|
|
4336
5064
|
if (ymlFiles.length === 0) {
|
|
4337
5065
|
throw new Error(
|
|
@@ -4345,7 +5073,7 @@ async function runRestore(opts) {
|
|
|
4345
5073
|
}
|
|
4346
5074
|
const ymlFile = ymlFiles[0];
|
|
4347
5075
|
const name = ymlFile.replace(/\.yml$/, "");
|
|
4348
|
-
const containerInBackup =
|
|
5076
|
+
const containerInBackup = path13.join(backup, "container");
|
|
4349
5077
|
const hasContainer = existsSync9(containerInBackup);
|
|
4350
5078
|
const destYml = containerConfigPath(name, home);
|
|
4351
5079
|
const destContainer = containerDir(name, home);
|
|
@@ -4359,10 +5087,10 @@ async function runRestore(opts) {
|
|
|
4359
5087
|
`Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
|
|
4360
5088
|
);
|
|
4361
5089
|
}
|
|
4362
|
-
await
|
|
4363
|
-
await
|
|
5090
|
+
await fs14.mkdir(containerConfigsDir(home), { recursive: true });
|
|
5091
|
+
await fs14.copyFile(path13.join(backup, ymlFile), destYml);
|
|
4364
5092
|
if (hasContainer) {
|
|
4365
|
-
await
|
|
5093
|
+
await fs14.cp(containerInBackup, destContainer, { recursive: true });
|
|
4366
5094
|
}
|
|
4367
5095
|
logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
|
|
4368
5096
|
logger.info(
|
|
@@ -4376,7 +5104,7 @@ async function runRestore(opts) {
|
|
|
4376
5104
|
}
|
|
4377
5105
|
|
|
4378
5106
|
// src/commands/restore.ts
|
|
4379
|
-
var restoreCommand =
|
|
5107
|
+
var restoreCommand = defineCommand17({
|
|
4380
5108
|
meta: {
|
|
4381
5109
|
name: "restore",
|
|
4382
5110
|
group: "lifecycle",
|
|
@@ -4393,16 +5121,16 @@ var restoreCommand = defineCommand15({
|
|
|
4393
5121
|
try {
|
|
4394
5122
|
await runRestore({ backupPath: args["backup-path"] });
|
|
4395
5123
|
} catch (err) {
|
|
4396
|
-
|
|
5124
|
+
consola22.error(err instanceof Error ? err.message : String(err));
|
|
4397
5125
|
process.exit(1);
|
|
4398
5126
|
}
|
|
4399
5127
|
}
|
|
4400
5128
|
});
|
|
4401
5129
|
|
|
4402
5130
|
// src/commands/remove-from-url.ts
|
|
4403
|
-
import { defineCommand as
|
|
4404
|
-
import { consola as
|
|
4405
|
-
var removeFromUrlCommand =
|
|
5131
|
+
import { defineCommand as defineCommand18 } from "citty";
|
|
5132
|
+
import { consola as consola23 } from "consola";
|
|
5133
|
+
var removeFromUrlCommand = defineCommand18({
|
|
4406
5134
|
meta: {
|
|
4407
5135
|
name: "remove-from-url",
|
|
4408
5136
|
group: "edit",
|
|
@@ -4435,16 +5163,16 @@ var removeFromUrlCommand = defineCommand16({
|
|
|
4435
5163
|
});
|
|
4436
5164
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4437
5165
|
} catch (err) {
|
|
4438
|
-
|
|
5166
|
+
consola23.error(err instanceof Error ? err.message : String(err));
|
|
4439
5167
|
process.exit(1);
|
|
4440
5168
|
}
|
|
4441
5169
|
}
|
|
4442
5170
|
});
|
|
4443
5171
|
|
|
4444
5172
|
// src/commands/remove-language.ts
|
|
4445
|
-
import { defineCommand as
|
|
4446
|
-
import { consola as
|
|
4447
|
-
var removeLanguageCommand =
|
|
5173
|
+
import { defineCommand as defineCommand19 } from "citty";
|
|
5174
|
+
import { consola as consola24 } from "consola";
|
|
5175
|
+
var removeLanguageCommand = defineCommand19({
|
|
4448
5176
|
meta: {
|
|
4449
5177
|
name: "remove-language",
|
|
4450
5178
|
group: "edit",
|
|
@@ -4477,16 +5205,64 @@ var removeLanguageCommand = defineCommand17({
|
|
|
4477
5205
|
});
|
|
4478
5206
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4479
5207
|
} catch (err) {
|
|
4480
|
-
|
|
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));
|
|
4481
5253
|
process.exit(1);
|
|
4482
5254
|
}
|
|
4483
5255
|
}
|
|
4484
5256
|
});
|
|
5257
|
+
function coerceToken2(raw) {
|
|
5258
|
+
const n = Number(raw);
|
|
5259
|
+
return Number.isFinite(n) ? n : raw;
|
|
5260
|
+
}
|
|
4485
5261
|
|
|
4486
5262
|
// src/commands/remove-repo.ts
|
|
4487
|
-
import { defineCommand as
|
|
4488
|
-
import { consola as
|
|
4489
|
-
var removeRepoCommand =
|
|
5263
|
+
import { defineCommand as defineCommand21 } from "citty";
|
|
5264
|
+
import { consola as consola26 } from "consola";
|
|
5265
|
+
var removeRepoCommand = defineCommand21({
|
|
4490
5266
|
meta: {
|
|
4491
5267
|
name: "remove-repo",
|
|
4492
5268
|
group: "edit",
|
|
@@ -4519,16 +5295,16 @@ var removeRepoCommand = defineCommand18({
|
|
|
4519
5295
|
});
|
|
4520
5296
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4521
5297
|
} catch (err) {
|
|
4522
|
-
|
|
5298
|
+
consola26.error(err instanceof Error ? err.message : String(err));
|
|
4523
5299
|
process.exit(1);
|
|
4524
5300
|
}
|
|
4525
5301
|
}
|
|
4526
5302
|
});
|
|
4527
5303
|
|
|
4528
5304
|
// src/commands/remove-service.ts
|
|
4529
|
-
import { defineCommand as
|
|
4530
|
-
import { consola as
|
|
4531
|
-
var removeServiceCommand =
|
|
5305
|
+
import { defineCommand as defineCommand22 } from "citty";
|
|
5306
|
+
import { consola as consola27 } from "consola";
|
|
5307
|
+
var removeServiceCommand = defineCommand22({
|
|
4532
5308
|
meta: {
|
|
4533
5309
|
name: "remove-service",
|
|
4534
5310
|
group: "edit",
|
|
@@ -4561,19 +5337,19 @@ var removeServiceCommand = defineCommand19({
|
|
|
4561
5337
|
});
|
|
4562
5338
|
process.exit(result.status === "aborted" ? 1 : 0);
|
|
4563
5339
|
} catch (err) {
|
|
4564
|
-
|
|
5340
|
+
consola27.error(err instanceof Error ? err.message : String(err));
|
|
4565
5341
|
process.exit(1);
|
|
4566
5342
|
}
|
|
4567
5343
|
}
|
|
4568
5344
|
});
|
|
4569
5345
|
|
|
4570
5346
|
// src/commands/run.ts
|
|
4571
|
-
import { defineCommand as
|
|
4572
|
-
import { consola as
|
|
5347
|
+
import { defineCommand as defineCommand23 } from "citty";
|
|
5348
|
+
import { consola as consola28 } from "consola";
|
|
4573
5349
|
|
|
4574
5350
|
// src/devcontainer/shell.ts
|
|
4575
5351
|
import { existsSync as existsSync10 } from "fs";
|
|
4576
|
-
import
|
|
5352
|
+
import path14 from "path";
|
|
4577
5353
|
async function runShell(opts) {
|
|
4578
5354
|
assertContainerExists(opts.root);
|
|
4579
5355
|
const spawnFn = opts.spawn ?? spawnDevcontainer;
|
|
@@ -4596,7 +5372,7 @@ async function runShell(opts) {
|
|
|
4596
5372
|
);
|
|
4597
5373
|
}
|
|
4598
5374
|
function assertContainerExists(root) {
|
|
4599
|
-
if (!existsSync10(
|
|
5375
|
+
if (!existsSync10(path14.join(root, ".devcontainer"))) {
|
|
4600
5376
|
throw new Error(
|
|
4601
5377
|
`No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
|
|
4602
5378
|
);
|
|
@@ -4632,7 +5408,7 @@ async function runInContainer(opts) {
|
|
|
4632
5408
|
}
|
|
4633
5409
|
|
|
4634
5410
|
// src/commands/run.ts
|
|
4635
|
-
var runCommand =
|
|
5411
|
+
var runCommand = defineCommand23({
|
|
4636
5412
|
meta: {
|
|
4637
5413
|
name: "run",
|
|
4638
5414
|
group: "run",
|
|
@@ -4648,7 +5424,7 @@ var runCommand = defineCommand20({
|
|
|
4648
5424
|
async run({ args }) {
|
|
4649
5425
|
const command = [...getInnerArgs()];
|
|
4650
5426
|
if (command.length === 0) {
|
|
4651
|
-
|
|
5427
|
+
consola28.error(
|
|
4652
5428
|
"No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
|
|
4653
5429
|
);
|
|
4654
5430
|
process.exit(1);
|
|
@@ -4660,16 +5436,16 @@ var runCommand = defineCommand20({
|
|
|
4660
5436
|
});
|
|
4661
5437
|
process.exit(exitCode);
|
|
4662
5438
|
} catch (err) {
|
|
4663
|
-
|
|
5439
|
+
consola28.error(err instanceof Error ? err.message : String(err));
|
|
4664
5440
|
process.exit(1);
|
|
4665
5441
|
}
|
|
4666
5442
|
}
|
|
4667
5443
|
});
|
|
4668
5444
|
|
|
4669
5445
|
// src/commands/shell.ts
|
|
4670
|
-
import { defineCommand as
|
|
4671
|
-
import { consola as
|
|
4672
|
-
var shellCommand =
|
|
5446
|
+
import { defineCommand as defineCommand24 } from "citty";
|
|
5447
|
+
import { consola as consola29 } from "consola";
|
|
5448
|
+
var shellCommand = defineCommand24({
|
|
4673
5449
|
meta: {
|
|
4674
5450
|
name: "shell",
|
|
4675
5451
|
group: "run",
|
|
@@ -4687,15 +5463,16 @@ var shellCommand = defineCommand21({
|
|
|
4687
5463
|
const exitCode = await runShell({ root: containerDir(args.name) });
|
|
4688
5464
|
process.exit(exitCode);
|
|
4689
5465
|
} catch (err) {
|
|
4690
|
-
|
|
5466
|
+
consola29.error(err instanceof Error ? err.message : String(err));
|
|
4691
5467
|
process.exit(1);
|
|
4692
5468
|
}
|
|
4693
5469
|
}
|
|
4694
5470
|
});
|
|
4695
5471
|
|
|
4696
5472
|
// src/commands/start.ts
|
|
4697
|
-
import { defineCommand as
|
|
4698
|
-
|
|
5473
|
+
import { defineCommand as defineCommand25 } from "citty";
|
|
5474
|
+
import { consola as consola30 } from "consola";
|
|
5475
|
+
var startCommand = defineCommand25({
|
|
4699
5476
|
meta: {
|
|
4700
5477
|
name: "start",
|
|
4701
5478
|
group: "run",
|
|
@@ -4709,13 +5486,33 @@ var startCommand = defineCommand22({
|
|
|
4709
5486
|
}
|
|
4710
5487
|
},
|
|
4711
5488
|
run({ args }) {
|
|
4712
|
-
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
|
+
});
|
|
4713
5510
|
}
|
|
4714
5511
|
});
|
|
4715
5512
|
|
|
4716
5513
|
// src/commands/status.ts
|
|
4717
|
-
import { defineCommand as
|
|
4718
|
-
var statusCommand =
|
|
5514
|
+
import { defineCommand as defineCommand26 } from "citty";
|
|
5515
|
+
var statusCommand = defineCommand26({
|
|
4719
5516
|
meta: {
|
|
4720
5517
|
name: "status",
|
|
4721
5518
|
group: "run",
|
|
@@ -4743,8 +5540,9 @@ var statusCommand = defineCommand23({
|
|
|
4743
5540
|
});
|
|
4744
5541
|
|
|
4745
5542
|
// src/commands/stop.ts
|
|
4746
|
-
import { defineCommand as
|
|
4747
|
-
|
|
5543
|
+
import { defineCommand as defineCommand27 } from "citty";
|
|
5544
|
+
import { consola as consola31 } from "consola";
|
|
5545
|
+
var stopCommand = defineCommand27({
|
|
4748
5546
|
meta: {
|
|
4749
5547
|
name: "stop",
|
|
4750
5548
|
group: "run",
|
|
@@ -4762,17 +5560,27 @@ var stopCommand = defineCommand24({
|
|
|
4762
5560
|
}
|
|
4763
5561
|
},
|
|
4764
5562
|
run({ args }) {
|
|
4765
|
-
return dispatch(
|
|
4766
|
-
|
|
5563
|
+
return dispatch(async () => {
|
|
5564
|
+
const exit = await runStop({
|
|
4767
5565
|
root: containerDir(args.name),
|
|
4768
5566
|
...typeof args.service === "string" ? { service: args.service } : {}
|
|
4769
|
-
})
|
|
4770
|
-
|
|
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
|
+
});
|
|
4771
5579
|
}
|
|
4772
5580
|
});
|
|
4773
5581
|
|
|
4774
5582
|
// src/main.ts
|
|
4775
|
-
var main =
|
|
5583
|
+
var main = defineCommand28({
|
|
4776
5584
|
meta: {
|
|
4777
5585
|
name: "monoceros",
|
|
4778
5586
|
version: CLI_VERSION,
|
|
@@ -4796,12 +5604,15 @@ var main = defineCommand25({
|
|
|
4796
5604
|
"add-feature": addFeatureCommand,
|
|
4797
5605
|
"add-from-url": addFromUrlCommand,
|
|
4798
5606
|
"add-repo": addRepoCommand,
|
|
5607
|
+
"add-port": addPortCommand,
|
|
4799
5608
|
"remove-service": removeServiceCommand,
|
|
4800
5609
|
"remove-language": removeLanguageCommand,
|
|
4801
5610
|
"remove-apt-packages": removeAptPackagesCommand,
|
|
4802
5611
|
"remove-feature": removeFeatureCommand,
|
|
4803
5612
|
"remove-from-url": removeFromUrlCommand,
|
|
4804
5613
|
"remove-repo": removeRepoCommand,
|
|
5614
|
+
"remove-port": removePortCommand,
|
|
5615
|
+
port: portCommand,
|
|
4805
5616
|
completion: completionCommand
|
|
4806
5617
|
}
|
|
4807
5618
|
});
|