@getmonoceros/workbench 1.9.6 → 1.10.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 +427 -14
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -251,25 +251,25 @@ function detectHelpRequest(argv, main2) {
|
|
|
251
251
|
const separatorIdx = argv.indexOf("--");
|
|
252
252
|
if (helpIdx === -1) return null;
|
|
253
253
|
if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
|
|
254
|
-
const
|
|
254
|
+
const path18 = [];
|
|
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
|
+
path18.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
|
+
path18.push(tok);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
270
|
break;
|
|
271
271
|
}
|
|
272
|
-
return { path:
|
|
272
|
+
return { path: path18, cmd: cursor };
|
|
273
273
|
}
|
|
274
274
|
async function maybeRenderHelp(argv, main2) {
|
|
275
275
|
const hit = detectHelpRequest(argv, main2);
|
|
@@ -301,7 +301,7 @@ function getInnerArgs() {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// src/main.ts
|
|
304
|
-
import { defineCommand as
|
|
304
|
+
import { defineCommand as defineCommand30 } from "citty";
|
|
305
305
|
|
|
306
306
|
// src/commands/add-apt-packages.ts
|
|
307
307
|
import { defineCommand } from "citty";
|
|
@@ -1638,7 +1638,8 @@ var SERVICE_CATALOG = {
|
|
|
1638
1638
|
// the recommended mount is the parent directory; pre-18 used
|
|
1639
1639
|
// /var/lib/postgresql/data directly. See
|
|
1640
1640
|
// https://github.com/docker-library/postgres/pull/1259.
|
|
1641
|
-
dataMount: "/var/lib/postgresql"
|
|
1641
|
+
dataMount: "/var/lib/postgresql",
|
|
1642
|
+
defaultPort: 5432
|
|
1642
1643
|
},
|
|
1643
1644
|
mysql: {
|
|
1644
1645
|
id: "mysql",
|
|
@@ -1647,12 +1648,14 @@ var SERVICE_CATALOG = {
|
|
|
1647
1648
|
MYSQL_ROOT_PASSWORD: "monoceros",
|
|
1648
1649
|
MYSQL_DATABASE: "monoceros"
|
|
1649
1650
|
},
|
|
1650
|
-
dataMount: "/var/lib/mysql"
|
|
1651
|
+
dataMount: "/var/lib/mysql",
|
|
1652
|
+
defaultPort: 3306
|
|
1651
1653
|
},
|
|
1652
1654
|
redis: {
|
|
1653
1655
|
id: "redis",
|
|
1654
1656
|
image: "redis:8",
|
|
1655
|
-
dataMount: "/data"
|
|
1657
|
+
dataMount: "/data",
|
|
1658
|
+
defaultPort: 6379
|
|
1656
1659
|
}
|
|
1657
1660
|
};
|
|
1658
1661
|
function knownLanguages() {
|
|
@@ -2726,6 +2729,15 @@ function removeFeatureFromDoc(doc, ref) {
|
|
|
2726
2729
|
if (!seq || !isSeq(seq)) return false;
|
|
2727
2730
|
const idx = seq.items.findIndex((i) => isMap2(i) && i.get("ref") === ref);
|
|
2728
2731
|
if (idx < 0) return false;
|
|
2732
|
+
if (idx > 0) {
|
|
2733
|
+
const prev = seq.items[idx - 1];
|
|
2734
|
+
if (prev && typeof prev.comment === "string" && prev.comment.length > 0) {
|
|
2735
|
+
const blank = prev.comment.match(/\n[ \t]*\n/);
|
|
2736
|
+
if (blank && blank.index !== void 0) {
|
|
2737
|
+
prev.comment = prev.comment.slice(0, blank.index);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2729
2741
|
seq.items.splice(idx, 1);
|
|
2730
2742
|
pruneEmptySeq(doc, "features");
|
|
2731
2743
|
return true;
|
|
@@ -2737,8 +2749,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
2737
2749
|
if (!isMap2(item)) return false;
|
|
2738
2750
|
const url = item.get("url");
|
|
2739
2751
|
if (url === urlOrPath) return true;
|
|
2740
|
-
const
|
|
2741
|
-
const effectivePath = typeof
|
|
2752
|
+
const path18 = item.get("path");
|
|
2753
|
+
const effectivePath = typeof path18 === "string" ? path18 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
2742
2754
|
return effectivePath === urlOrPath;
|
|
2743
2755
|
});
|
|
2744
2756
|
if (idx < 0) return false;
|
|
@@ -2788,7 +2800,7 @@ async function runAddRepo(input) {
|
|
|
2788
2800
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
2789
2801
|
);
|
|
2790
2802
|
}
|
|
2791
|
-
const
|
|
2803
|
+
const path18 = (input.path ?? deriveRepoName(url)).trim();
|
|
2792
2804
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
2793
2805
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
2794
2806
|
if (hasName !== hasEmail) {
|
|
@@ -2817,7 +2829,7 @@ async function runAddRepo(input) {
|
|
|
2817
2829
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
2818
2830
|
const entry2 = {
|
|
2819
2831
|
url,
|
|
2820
|
-
path:
|
|
2832
|
+
path: path18,
|
|
2821
2833
|
...hasName && hasEmail ? {
|
|
2822
2834
|
gitUser: {
|
|
2823
2835
|
name: input.gitName.trim(),
|
|
@@ -4588,7 +4600,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4588
4600
|
}
|
|
4589
4601
|
|
|
4590
4602
|
// src/version.ts
|
|
4591
|
-
var CLI_VERSION = true ? "1.
|
|
4603
|
+
var CLI_VERSION = true ? "1.10.0" : "dev";
|
|
4592
4604
|
|
|
4593
4605
|
// src/commands/_dispatch.ts
|
|
4594
4606
|
import { consola as consola12 } from "consola";
|
|
@@ -4995,6 +5007,7 @@ var ALL_COMMANDS = [
|
|
|
4995
5007
|
"remove-repo",
|
|
4996
5008
|
"remove-port",
|
|
4997
5009
|
"port",
|
|
5010
|
+
"tunnel",
|
|
4998
5011
|
"completion"
|
|
4999
5012
|
];
|
|
5000
5013
|
var containerName = (ctx) => listContainerNames(ctx);
|
|
@@ -5089,6 +5102,13 @@ var COMMAND_SPECS = {
|
|
|
5089
5102
|
flags: { "--default": { type: "boolean" } },
|
|
5090
5103
|
innerArgs: () => []
|
|
5091
5104
|
},
|
|
5105
|
+
tunnel: {
|
|
5106
|
+
positionals: [containerName, () => listServiceNames()],
|
|
5107
|
+
flags: {
|
|
5108
|
+
"--local-port": { type: "value" },
|
|
5109
|
+
"--local-address": { type: "value" }
|
|
5110
|
+
}
|
|
5111
|
+
},
|
|
5092
5112
|
completion: {
|
|
5093
5113
|
positionals: [() => listShellNames()]
|
|
5094
5114
|
},
|
|
@@ -6719,8 +6739,400 @@ var stopCommand = defineCommand28({
|
|
|
6719
6739
|
}
|
|
6720
6740
|
});
|
|
6721
6741
|
|
|
6742
|
+
// src/commands/tunnel.ts
|
|
6743
|
+
import { defineCommand as defineCommand29 } from "citty";
|
|
6744
|
+
import { consola as consola33 } from "consola";
|
|
6745
|
+
|
|
6746
|
+
// src/tunnel/run.ts
|
|
6747
|
+
import { spawn as spawn9 } from "child_process";
|
|
6748
|
+
import { consola as consola32 } from "consola";
|
|
6749
|
+
|
|
6750
|
+
// src/tunnel/resolve.ts
|
|
6751
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6752
|
+
import path17 from "path";
|
|
6753
|
+
async function resolveTunnelTarget(opts) {
|
|
6754
|
+
const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
|
|
6755
|
+
if (!existsSync12(ymlPath)) {
|
|
6756
|
+
throw new Error(
|
|
6757
|
+
`No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
|
|
6758
|
+
);
|
|
6759
|
+
}
|
|
6760
|
+
const parsed = await readConfig(ymlPath);
|
|
6761
|
+
const config = parsed.config;
|
|
6762
|
+
const containerRoot = containerDir(opts.name, opts.monocerosHome);
|
|
6763
|
+
if (!existsSync12(containerRoot)) {
|
|
6764
|
+
throw new Error(
|
|
6765
|
+
`Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
|
|
6766
|
+
);
|
|
6767
|
+
}
|
|
6768
|
+
const composePath = path17.join(containerRoot, ".devcontainer", "compose.yaml");
|
|
6769
|
+
const isCompose = existsSync12(composePath);
|
|
6770
|
+
const parsedTarget = parseTargetArg(opts.target, config);
|
|
6771
|
+
const docker = opts.docker ?? defaultDockerExec;
|
|
6772
|
+
if (isCompose) {
|
|
6773
|
+
return resolveCompose2({
|
|
6774
|
+
name: opts.name,
|
|
6775
|
+
containerRoot,
|
|
6776
|
+
parsedTarget
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
6779
|
+
return resolveImageMode({
|
|
6780
|
+
name: opts.name,
|
|
6781
|
+
containerRoot,
|
|
6782
|
+
parsedTarget,
|
|
6783
|
+
config,
|
|
6784
|
+
docker
|
|
6785
|
+
});
|
|
6786
|
+
}
|
|
6787
|
+
function parseTargetArg(raw, config) {
|
|
6788
|
+
const asNumber = Number(raw);
|
|
6789
|
+
if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
|
|
6790
|
+
return { kind: "port", port: asNumber };
|
|
6791
|
+
}
|
|
6792
|
+
const entry2 = SERVICE_CATALOG[raw];
|
|
6793
|
+
if (!entry2) {
|
|
6794
|
+
const candidates = knownServices().join(", ");
|
|
6795
|
+
throw new Error(
|
|
6796
|
+
`Unknown service '${raw}'. Known services: ${candidates}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
|
|
6797
|
+
);
|
|
6798
|
+
}
|
|
6799
|
+
if (!config.services.includes(raw)) {
|
|
6800
|
+
throw new Error(
|
|
6801
|
+
`Service '${raw}' is not declared in this container's yml. Add it with \`monoceros add-service ${config.services.length === 0 ? "<name>" : "\u2026"} ${raw}\` and re-apply.`
|
|
6802
|
+
);
|
|
6803
|
+
}
|
|
6804
|
+
return { kind: "service", service: raw, port: entry2.defaultPort };
|
|
6805
|
+
}
|
|
6806
|
+
function resolveCompose2(args) {
|
|
6807
|
+
const network = `${composeProjectName(args.containerRoot)}_default`;
|
|
6808
|
+
if (args.parsedTarget.kind === "service") {
|
|
6809
|
+
return {
|
|
6810
|
+
network,
|
|
6811
|
+
targetHost: args.parsedTarget.service,
|
|
6812
|
+
internalPort: args.parsedTarget.port,
|
|
6813
|
+
display: `${args.name}/${args.parsedTarget.service}`
|
|
6814
|
+
};
|
|
6815
|
+
}
|
|
6816
|
+
return {
|
|
6817
|
+
network,
|
|
6818
|
+
targetHost: "workspace",
|
|
6819
|
+
internalPort: args.parsedTarget.port,
|
|
6820
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
async function resolveImageMode(args) {
|
|
6824
|
+
if (args.parsedTarget.kind === "service") {
|
|
6825
|
+
throw new Error(
|
|
6826
|
+
`Service '${args.parsedTarget.service}' is declared in the yml but '${args.name}' is image-mode (no compose.yaml). Services need compose mode \u2014 re-apply with at least one \`services:\` entry to get a compose setup.`
|
|
6827
|
+
);
|
|
6828
|
+
}
|
|
6829
|
+
const ports = args.config.routing?.ports ?? [];
|
|
6830
|
+
if (ports.length > 0) {
|
|
6831
|
+
return {
|
|
6832
|
+
network: PROXY_NETWORK_NAME,
|
|
6833
|
+
targetHost: args.name,
|
|
6834
|
+
internalPort: args.parsedTarget.port,
|
|
6835
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6836
|
+
};
|
|
6837
|
+
}
|
|
6838
|
+
const { network, ip } = await lookupContainerNetwork({
|
|
6839
|
+
containerRoot: args.containerRoot,
|
|
6840
|
+
docker: args.docker
|
|
6841
|
+
});
|
|
6842
|
+
return {
|
|
6843
|
+
network,
|
|
6844
|
+
targetHost: ip,
|
|
6845
|
+
internalPort: args.parsedTarget.port,
|
|
6846
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6847
|
+
};
|
|
6848
|
+
}
|
|
6849
|
+
async function lookupContainerNetwork(args) {
|
|
6850
|
+
const psResult = await args.docker([
|
|
6851
|
+
"ps",
|
|
6852
|
+
"-q",
|
|
6853
|
+
"--filter",
|
|
6854
|
+
`label=devcontainer.local_folder=${args.containerRoot}`
|
|
6855
|
+
]);
|
|
6856
|
+
if (psResult.exitCode !== 0) {
|
|
6857
|
+
throw new Error(
|
|
6858
|
+
`docker ps failed: ${psResult.stderr.trim() || `exit ${psResult.exitCode}`}`
|
|
6859
|
+
);
|
|
6860
|
+
}
|
|
6861
|
+
const containerId = psResult.stdout.trim().split("\n")[0]?.trim();
|
|
6862
|
+
if (!containerId) {
|
|
6863
|
+
throw new Error(
|
|
6864
|
+
`No running container for '${args.containerRoot}'. Start it with \`monoceros start <name>\` (or open a shell with \`monoceros shell <name>\`) and retry.`
|
|
6865
|
+
);
|
|
6866
|
+
}
|
|
6867
|
+
const inspect = await args.docker([
|
|
6868
|
+
"inspect",
|
|
6869
|
+
"--format",
|
|
6870
|
+
"{{json .NetworkSettings.Networks}}",
|
|
6871
|
+
containerId
|
|
6872
|
+
]);
|
|
6873
|
+
if (inspect.exitCode !== 0) {
|
|
6874
|
+
throw new Error(
|
|
6875
|
+
`docker inspect failed: ${inspect.stderr.trim() || `exit ${inspect.exitCode}`}`
|
|
6876
|
+
);
|
|
6877
|
+
}
|
|
6878
|
+
let networks = null;
|
|
6879
|
+
try {
|
|
6880
|
+
networks = JSON.parse(inspect.stdout);
|
|
6881
|
+
} catch {
|
|
6882
|
+
throw new Error(
|
|
6883
|
+
`Unexpected docker inspect output: ${inspect.stdout.slice(0, 200)}`
|
|
6884
|
+
);
|
|
6885
|
+
}
|
|
6886
|
+
if (!networks) {
|
|
6887
|
+
throw new Error(
|
|
6888
|
+
`Container ${containerId} reports no networks. Restart it and retry.`
|
|
6889
|
+
);
|
|
6890
|
+
}
|
|
6891
|
+
for (const [name, settings] of Object.entries(networks)) {
|
|
6892
|
+
if (settings.IPAddress && settings.IPAddress.length > 0) {
|
|
6893
|
+
return { network: name, ip: settings.IPAddress };
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
throw new Error(
|
|
6897
|
+
`Container ${containerId} has no network with a reachable IP. Restart it and retry.`
|
|
6898
|
+
);
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6901
|
+
// src/tunnel/port-check.ts
|
|
6902
|
+
import { Socket as Socket2 } from "net";
|
|
6903
|
+
var CONNECT_TIMEOUT_MS2 = 750;
|
|
6904
|
+
var realPortProbe2 = (port, address) => {
|
|
6905
|
+
const probeHost = address === "0.0.0.0" ? "127.0.0.1" : address;
|
|
6906
|
+
return new Promise((resolve) => {
|
|
6907
|
+
const socket = new Socket2();
|
|
6908
|
+
let settled = false;
|
|
6909
|
+
const settle = (result) => {
|
|
6910
|
+
if (settled) return;
|
|
6911
|
+
settled = true;
|
|
6912
|
+
socket.destroy();
|
|
6913
|
+
resolve(result);
|
|
6914
|
+
};
|
|
6915
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS2);
|
|
6916
|
+
socket.once("connect", () => {
|
|
6917
|
+
settle({
|
|
6918
|
+
ok: false,
|
|
6919
|
+
code: "EADDRINUSE",
|
|
6920
|
+
message: `another process is listening on ${port}`
|
|
6921
|
+
});
|
|
6922
|
+
});
|
|
6923
|
+
socket.once("timeout", () => {
|
|
6924
|
+
settle({ ok: true });
|
|
6925
|
+
});
|
|
6926
|
+
socket.once("error", (err) => {
|
|
6927
|
+
const code = err.code ?? "UNKNOWN";
|
|
6928
|
+
if (code === "ECONNREFUSED") {
|
|
6929
|
+
settle({ ok: true });
|
|
6930
|
+
} else {
|
|
6931
|
+
settle({ ok: false, code, message: err.message });
|
|
6932
|
+
}
|
|
6933
|
+
});
|
|
6934
|
+
socket.connect(port, probeHost);
|
|
6935
|
+
});
|
|
6936
|
+
};
|
|
6937
|
+
async function preflightLocalPort(opts) {
|
|
6938
|
+
const probe = opts.probe ?? realPortProbe2;
|
|
6939
|
+
const result = await probe(opts.port, opts.address);
|
|
6940
|
+
if (result.ok) return;
|
|
6941
|
+
throw new Error(formatLocalPortHeldError(opts.port, opts.address, result));
|
|
6942
|
+
}
|
|
6943
|
+
function formatLocalPortHeldError(port, address, result) {
|
|
6944
|
+
const lines = [];
|
|
6945
|
+
if (result.code === "EADDRINUSE") {
|
|
6946
|
+
lines.push(`Local port ${port} on ${address} is already in use.`);
|
|
6947
|
+
lines.push("");
|
|
6948
|
+
lines.push("Identify the holder, then either stop it or pick a different");
|
|
6949
|
+
lines.push("port for the tunnel:");
|
|
6950
|
+
lines.push("");
|
|
6951
|
+
lines.push(` sudo lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
|
|
6952
|
+
lines.push(` # or: sudo ss -tlnp | grep ":${port}\\b"`);
|
|
6953
|
+
lines.push("");
|
|
6954
|
+
lines.push("Re-run with an explicit local port:");
|
|
6955
|
+
lines.push(` monoceros tunnel \u2026 --local-port=${port + 1}`);
|
|
6956
|
+
} else {
|
|
6957
|
+
lines.push(
|
|
6958
|
+
`Cannot probe local port ${port} on ${address}: ${result.message}`
|
|
6959
|
+
);
|
|
6960
|
+
lines.push("");
|
|
6961
|
+
lines.push(
|
|
6962
|
+
"Most likely the host network stack (firewall, namespace) is interfering."
|
|
6963
|
+
);
|
|
6964
|
+
lines.push("Try a different local port via `--local-port=<n>`.");
|
|
6965
|
+
}
|
|
6966
|
+
return lines.join("\n");
|
|
6967
|
+
}
|
|
6968
|
+
|
|
6969
|
+
// src/tunnel/run.ts
|
|
6970
|
+
var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
|
|
6971
|
+
var defaultDockerSpawn = (args) => {
|
|
6972
|
+
const child = spawn9("docker", args, {
|
|
6973
|
+
stdio: "inherit"
|
|
6974
|
+
});
|
|
6975
|
+
const exited = new Promise((resolve, reject) => {
|
|
6976
|
+
child.on("error", reject);
|
|
6977
|
+
child.on("exit", (code, signal) => {
|
|
6978
|
+
if (typeof code === "number") resolve(code);
|
|
6979
|
+
else if (signal) resolve(128 + signalNumber(signal));
|
|
6980
|
+
else resolve(0);
|
|
6981
|
+
});
|
|
6982
|
+
});
|
|
6983
|
+
return {
|
|
6984
|
+
exited,
|
|
6985
|
+
kill: (signal) => {
|
|
6986
|
+
try {
|
|
6987
|
+
child.kill(signal);
|
|
6988
|
+
} catch {
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
};
|
|
6992
|
+
};
|
|
6993
|
+
function signalNumber(signal) {
|
|
6994
|
+
switch (signal) {
|
|
6995
|
+
case "SIGINT":
|
|
6996
|
+
return 2;
|
|
6997
|
+
case "SIGTERM":
|
|
6998
|
+
return 15;
|
|
6999
|
+
default:
|
|
7000
|
+
return 1;
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
var installSigintDefault = (handler) => {
|
|
7004
|
+
process.on("SIGINT", handler);
|
|
7005
|
+
return () => process.off("SIGINT", handler);
|
|
7006
|
+
};
|
|
7007
|
+
var DEFAULT_LOCAL_ADDRESS = "127.0.0.1";
|
|
7008
|
+
async function runTunnel(opts) {
|
|
7009
|
+
const log = opts.logger ?? {
|
|
7010
|
+
info: (m) => consola32.info(m),
|
|
7011
|
+
warn: (m) => consola32.warn(m)
|
|
7012
|
+
};
|
|
7013
|
+
const resolve = opts.resolve ?? resolveTunnelTarget;
|
|
7014
|
+
const resolveArgs2 = {
|
|
7015
|
+
name: opts.name,
|
|
7016
|
+
target: opts.target,
|
|
7017
|
+
...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
|
|
7018
|
+
};
|
|
7019
|
+
const resolved = await resolve(resolveArgs2);
|
|
7020
|
+
const localPort = opts.localPort ?? resolved.internalPort;
|
|
7021
|
+
const localAddress = opts.localAddress ?? DEFAULT_LOCAL_ADDRESS;
|
|
7022
|
+
validateLocalAddress(localAddress);
|
|
7023
|
+
const preflight = opts.preflight ?? preflightLocalPort;
|
|
7024
|
+
await preflight({ port: localPort, address: localAddress });
|
|
7025
|
+
const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
|
|
7026
|
+
const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
|
|
7027
|
+
const dockerArgs = buildDockerArgs({
|
|
7028
|
+
localAddress,
|
|
7029
|
+
localPort,
|
|
7030
|
+
internalPort: resolved.internalPort,
|
|
7031
|
+
network: resolved.network,
|
|
7032
|
+
targetHost: resolved.targetHost
|
|
7033
|
+
});
|
|
7034
|
+
log.info(
|
|
7035
|
+
`Tunnel: ${localAddress}:${localPort} \u2192 ${resolved.display}:${resolved.internalPort} (Ctrl+C to stop)`
|
|
7036
|
+
);
|
|
7037
|
+
const handle = dockerSpawn(dockerArgs);
|
|
7038
|
+
const uninstall = installSignalHandler(() => {
|
|
7039
|
+
});
|
|
7040
|
+
try {
|
|
7041
|
+
const exitCode = await handle.exited;
|
|
7042
|
+
if (exitCode === 130) return 0;
|
|
7043
|
+
return exitCode;
|
|
7044
|
+
} finally {
|
|
7045
|
+
uninstall();
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
function buildDockerArgs(input) {
|
|
7049
|
+
return [
|
|
7050
|
+
"run",
|
|
7051
|
+
"--rm",
|
|
7052
|
+
"-i",
|
|
7053
|
+
`--network=${input.network}`,
|
|
7054
|
+
"-p",
|
|
7055
|
+
`${input.localAddress}:${input.localPort}:${input.internalPort}`,
|
|
7056
|
+
SOCAT_IMAGE,
|
|
7057
|
+
`TCP-LISTEN:${input.internalPort},fork,reuseaddr`,
|
|
7058
|
+
`TCP:${input.targetHost}:${input.internalPort}`
|
|
7059
|
+
];
|
|
7060
|
+
}
|
|
7061
|
+
var IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7062
|
+
function validateLocalAddress(addr) {
|
|
7063
|
+
if (addr === "localhost") return;
|
|
7064
|
+
if (IPV4_RE.test(addr)) {
|
|
7065
|
+
for (const part of addr.split(".")) {
|
|
7066
|
+
const n = Number(part);
|
|
7067
|
+
if (n < 0 || n > 255) {
|
|
7068
|
+
throw new Error(
|
|
7069
|
+
`Invalid --local-address '${addr}': each dotted-quad octet must be 0-255.`
|
|
7070
|
+
);
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
return;
|
|
7074
|
+
}
|
|
7075
|
+
throw new Error(
|
|
7076
|
+
`Invalid --local-address '${addr}'. Use 127.0.0.1 (default), 0.0.0.0, or a specific IPv4 address.`
|
|
7077
|
+
);
|
|
7078
|
+
}
|
|
7079
|
+
|
|
7080
|
+
// src/commands/tunnel.ts
|
|
7081
|
+
var tunnelCommand = defineCommand29({
|
|
7082
|
+
meta: {
|
|
7083
|
+
name: "tunnel",
|
|
7084
|
+
group: "discovery",
|
|
7085
|
+
description: "Open a TCP tunnel from the host to a service or port inside the container. Foreground process \u2014 Ctrl+C closes the tunnel. Pass a service name (e.g. `postgres`, `mysql`, `redis`) from the container yml or a bare in-container port number. See ADR 0009."
|
|
7086
|
+
},
|
|
7087
|
+
args: {
|
|
7088
|
+
name: {
|
|
7089
|
+
type: "positional",
|
|
7090
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
7091
|
+
required: true
|
|
7092
|
+
},
|
|
7093
|
+
target: {
|
|
7094
|
+
type: "positional",
|
|
7095
|
+
description: "Service name from the container yml (e.g. `postgres`) or an in-container port number (e.g. `8080`).",
|
|
7096
|
+
required: true
|
|
7097
|
+
},
|
|
7098
|
+
"local-port": {
|
|
7099
|
+
type: "string",
|
|
7100
|
+
description: "Host port the tunnel listens on. Default: same as the internal port (e.g. postgres \u2192 5432). Pass a different value when the default is busy."
|
|
7101
|
+
},
|
|
7102
|
+
"local-address": {
|
|
7103
|
+
type: "string",
|
|
7104
|
+
description: "Host interface the tunnel binds to. Default: 127.0.0.1 (loopback only \u2014 same machine). Pass 0.0.0.0 to expose on all interfaces (LAN, other devices on the same network)."
|
|
7105
|
+
}
|
|
7106
|
+
},
|
|
7107
|
+
async run({ args }) {
|
|
7108
|
+
try {
|
|
7109
|
+
const localPort = parseLocalPort(args["local-port"]);
|
|
7110
|
+
const exitCode = await runTunnel({
|
|
7111
|
+
name: args.name,
|
|
7112
|
+
target: args.target,
|
|
7113
|
+
...localPort !== void 0 ? { localPort } : {},
|
|
7114
|
+
...args["local-address"] ? { localAddress: args["local-address"] } : {}
|
|
7115
|
+
});
|
|
7116
|
+
process.exit(exitCode);
|
|
7117
|
+
} catch (err) {
|
|
7118
|
+
consola33.error(err instanceof Error ? err.message : String(err));
|
|
7119
|
+
process.exit(1);
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
});
|
|
7123
|
+
function parseLocalPort(raw) {
|
|
7124
|
+
if (raw === void 0) return void 0;
|
|
7125
|
+
const n = Number(raw);
|
|
7126
|
+
if (!Number.isInteger(n) || n <= 0 || n >= 65536) {
|
|
7127
|
+
throw new Error(
|
|
7128
|
+
`Invalid --local-port '${raw}': must be an integer between 1 and 65535.`
|
|
7129
|
+
);
|
|
7130
|
+
}
|
|
7131
|
+
return n;
|
|
7132
|
+
}
|
|
7133
|
+
|
|
6722
7134
|
// src/main.ts
|
|
6723
|
-
var main =
|
|
7135
|
+
var main = defineCommand30({
|
|
6724
7136
|
meta: {
|
|
6725
7137
|
name: "monoceros",
|
|
6726
7138
|
version: CLI_VERSION,
|
|
@@ -6753,6 +7165,7 @@ var main = defineCommand29({
|
|
|
6753
7165
|
"remove-repo": removeRepoCommand,
|
|
6754
7166
|
"remove-port": removePortCommand,
|
|
6755
7167
|
port: portCommand,
|
|
7168
|
+
tunnel: tunnelCommand,
|
|
6756
7169
|
completion: completionCommand,
|
|
6757
7170
|
__complete: __completeCommand
|
|
6758
7171
|
}
|