@getmonoceros/workbench 1.9.7 → 1.10.1
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 +442 -15
- 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() {
|
|
@@ -2746,8 +2749,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
2746
2749
|
if (!isMap2(item)) return false;
|
|
2747
2750
|
const url = item.get("url");
|
|
2748
2751
|
if (url === urlOrPath) return true;
|
|
2749
|
-
const
|
|
2750
|
-
const effectivePath = typeof
|
|
2752
|
+
const path18 = item.get("path");
|
|
2753
|
+
const effectivePath = typeof path18 === "string" ? path18 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
2751
2754
|
return effectivePath === urlOrPath;
|
|
2752
2755
|
});
|
|
2753
2756
|
if (idx < 0) return false;
|
|
@@ -2797,7 +2800,7 @@ async function runAddRepo(input) {
|
|
|
2797
2800
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
2798
2801
|
);
|
|
2799
2802
|
}
|
|
2800
|
-
const
|
|
2803
|
+
const path18 = (input.path ?? deriveRepoName(url)).trim();
|
|
2801
2804
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
2802
2805
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
2803
2806
|
if (hasName !== hasEmail) {
|
|
@@ -2826,7 +2829,7 @@ async function runAddRepo(input) {
|
|
|
2826
2829
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
2827
2830
|
const entry2 = {
|
|
2828
2831
|
url,
|
|
2829
|
-
path:
|
|
2832
|
+
path: path18,
|
|
2830
2833
|
...hasName && hasEmail ? {
|
|
2831
2834
|
gitUser: {
|
|
2832
2835
|
name: input.gitName.trim(),
|
|
@@ -4597,7 +4600,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
|
|
|
4597
4600
|
}
|
|
4598
4601
|
|
|
4599
4602
|
// src/version.ts
|
|
4600
|
-
var CLI_VERSION = true ? "1.
|
|
4603
|
+
var CLI_VERSION = true ? "1.10.1" : "dev";
|
|
4601
4604
|
|
|
4602
4605
|
// src/commands/_dispatch.ts
|
|
4603
4606
|
import { consola as consola12 } from "consola";
|
|
@@ -5004,6 +5007,7 @@ var ALL_COMMANDS = [
|
|
|
5004
5007
|
"remove-repo",
|
|
5005
5008
|
"remove-port",
|
|
5006
5009
|
"port",
|
|
5010
|
+
"tunnel",
|
|
5007
5011
|
"completion"
|
|
5008
5012
|
];
|
|
5009
5013
|
var containerName = (ctx) => listContainerNames(ctx);
|
|
@@ -5098,6 +5102,13 @@ var COMMAND_SPECS = {
|
|
|
5098
5102
|
flags: { "--default": { type: "boolean" } },
|
|
5099
5103
|
innerArgs: () => []
|
|
5100
5104
|
},
|
|
5105
|
+
tunnel: {
|
|
5106
|
+
positionals: [containerName, () => listServiceNames()],
|
|
5107
|
+
flags: {
|
|
5108
|
+
"--local-port": { type: "value" },
|
|
5109
|
+
"--local-address": { type: "value" }
|
|
5110
|
+
}
|
|
5111
|
+
},
|
|
5101
5112
|
completion: {
|
|
5102
5113
|
positionals: [() => listShellNames()]
|
|
5103
5114
|
},
|
|
@@ -6089,7 +6100,30 @@ async function runRemove(opts) {
|
|
|
6089
6100
|
await fs14.rm(ymlPath, { force: true });
|
|
6090
6101
|
}
|
|
6091
6102
|
if (hasContainer) {
|
|
6092
|
-
|
|
6103
|
+
try {
|
|
6104
|
+
await fs14.rm(containerPath, { recursive: true, force: true });
|
|
6105
|
+
} catch (err) {
|
|
6106
|
+
const code = err.code;
|
|
6107
|
+
if (code !== "EACCES" && code !== "EPERM") {
|
|
6108
|
+
throw err;
|
|
6109
|
+
}
|
|
6110
|
+
logger.info(
|
|
6111
|
+
`[remove] host-side rm hit ${code} on ${prettyPath(containerPath)}; using a throw-away alpine container to clean root-owned files\u2026`
|
|
6112
|
+
);
|
|
6113
|
+
const exit = await dockerSpawn(
|
|
6114
|
+
[
|
|
6115
|
+
"-c",
|
|
6116
|
+
`docker run --rm -v "${containerPath}":/target alpine:3.21 find /target -mindepth 1 -delete`
|
|
6117
|
+
],
|
|
6118
|
+
home
|
|
6119
|
+
);
|
|
6120
|
+
if (exit !== 0) {
|
|
6121
|
+
throw new Error(
|
|
6122
|
+
`docker-based cleanup of ${containerPath} exited ${exit}. Inspect with \`sudo ls -la ${containerPath}\` and clean manually.`
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
await fs14.rm(containerPath, { recursive: true, force: true });
|
|
6126
|
+
}
|
|
6093
6127
|
}
|
|
6094
6128
|
logger.success(
|
|
6095
6129
|
`Removed '${opts.name}': docker objects gone, container-configs entry deleted, container directory deleted.`
|
|
@@ -6728,8 +6762,400 @@ var stopCommand = defineCommand28({
|
|
|
6728
6762
|
}
|
|
6729
6763
|
});
|
|
6730
6764
|
|
|
6765
|
+
// src/commands/tunnel.ts
|
|
6766
|
+
import { defineCommand as defineCommand29 } from "citty";
|
|
6767
|
+
import { consola as consola33 } from "consola";
|
|
6768
|
+
|
|
6769
|
+
// src/tunnel/run.ts
|
|
6770
|
+
import { spawn as spawn9 } from "child_process";
|
|
6771
|
+
import { consola as consola32 } from "consola";
|
|
6772
|
+
|
|
6773
|
+
// src/tunnel/resolve.ts
|
|
6774
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6775
|
+
import path17 from "path";
|
|
6776
|
+
async function resolveTunnelTarget(opts) {
|
|
6777
|
+
const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
|
|
6778
|
+
if (!existsSync12(ymlPath)) {
|
|
6779
|
+
throw new Error(
|
|
6780
|
+
`No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
|
|
6781
|
+
);
|
|
6782
|
+
}
|
|
6783
|
+
const parsed = await readConfig(ymlPath);
|
|
6784
|
+
const config = parsed.config;
|
|
6785
|
+
const containerRoot = containerDir(opts.name, opts.monocerosHome);
|
|
6786
|
+
if (!existsSync12(containerRoot)) {
|
|
6787
|
+
throw new Error(
|
|
6788
|
+
`Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
|
|
6789
|
+
);
|
|
6790
|
+
}
|
|
6791
|
+
const composePath = path17.join(containerRoot, ".devcontainer", "compose.yaml");
|
|
6792
|
+
const isCompose = existsSync12(composePath);
|
|
6793
|
+
const parsedTarget = parseTargetArg(opts.target, config);
|
|
6794
|
+
const docker = opts.docker ?? defaultDockerExec;
|
|
6795
|
+
if (isCompose) {
|
|
6796
|
+
return resolveCompose2({
|
|
6797
|
+
name: opts.name,
|
|
6798
|
+
containerRoot,
|
|
6799
|
+
parsedTarget
|
|
6800
|
+
});
|
|
6801
|
+
}
|
|
6802
|
+
return resolveImageMode({
|
|
6803
|
+
name: opts.name,
|
|
6804
|
+
containerRoot,
|
|
6805
|
+
parsedTarget,
|
|
6806
|
+
config,
|
|
6807
|
+
docker
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6810
|
+
function parseTargetArg(raw, config) {
|
|
6811
|
+
const asNumber = Number(raw);
|
|
6812
|
+
if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
|
|
6813
|
+
return { kind: "port", port: asNumber };
|
|
6814
|
+
}
|
|
6815
|
+
const entry2 = SERVICE_CATALOG[raw];
|
|
6816
|
+
if (!entry2) {
|
|
6817
|
+
const candidates = knownServices().join(", ");
|
|
6818
|
+
throw new Error(
|
|
6819
|
+
`Unknown service '${raw}'. Known services: ${candidates}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
|
|
6820
|
+
);
|
|
6821
|
+
}
|
|
6822
|
+
if (!config.services.includes(raw)) {
|
|
6823
|
+
throw new Error(
|
|
6824
|
+
`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.`
|
|
6825
|
+
);
|
|
6826
|
+
}
|
|
6827
|
+
return { kind: "service", service: raw, port: entry2.defaultPort };
|
|
6828
|
+
}
|
|
6829
|
+
function resolveCompose2(args) {
|
|
6830
|
+
const network = `${composeProjectName(args.containerRoot)}_default`;
|
|
6831
|
+
if (args.parsedTarget.kind === "service") {
|
|
6832
|
+
return {
|
|
6833
|
+
network,
|
|
6834
|
+
targetHost: args.parsedTarget.service,
|
|
6835
|
+
internalPort: args.parsedTarget.port,
|
|
6836
|
+
display: `${args.name}/${args.parsedTarget.service}`
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
6839
|
+
return {
|
|
6840
|
+
network,
|
|
6841
|
+
targetHost: "workspace",
|
|
6842
|
+
internalPort: args.parsedTarget.port,
|
|
6843
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6844
|
+
};
|
|
6845
|
+
}
|
|
6846
|
+
async function resolveImageMode(args) {
|
|
6847
|
+
if (args.parsedTarget.kind === "service") {
|
|
6848
|
+
throw new Error(
|
|
6849
|
+
`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.`
|
|
6850
|
+
);
|
|
6851
|
+
}
|
|
6852
|
+
const ports = args.config.routing?.ports ?? [];
|
|
6853
|
+
if (ports.length > 0) {
|
|
6854
|
+
return {
|
|
6855
|
+
network: PROXY_NETWORK_NAME,
|
|
6856
|
+
targetHost: args.name,
|
|
6857
|
+
internalPort: args.parsedTarget.port,
|
|
6858
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6859
|
+
};
|
|
6860
|
+
}
|
|
6861
|
+
const { network, ip } = await lookupContainerNetwork({
|
|
6862
|
+
containerRoot: args.containerRoot,
|
|
6863
|
+
docker: args.docker
|
|
6864
|
+
});
|
|
6865
|
+
return {
|
|
6866
|
+
network,
|
|
6867
|
+
targetHost: ip,
|
|
6868
|
+
internalPort: args.parsedTarget.port,
|
|
6869
|
+
display: `${args.name}:${args.parsedTarget.port}`
|
|
6870
|
+
};
|
|
6871
|
+
}
|
|
6872
|
+
async function lookupContainerNetwork(args) {
|
|
6873
|
+
const psResult = await args.docker([
|
|
6874
|
+
"ps",
|
|
6875
|
+
"-q",
|
|
6876
|
+
"--filter",
|
|
6877
|
+
`label=devcontainer.local_folder=${args.containerRoot}`
|
|
6878
|
+
]);
|
|
6879
|
+
if (psResult.exitCode !== 0) {
|
|
6880
|
+
throw new Error(
|
|
6881
|
+
`docker ps failed: ${psResult.stderr.trim() || `exit ${psResult.exitCode}`}`
|
|
6882
|
+
);
|
|
6883
|
+
}
|
|
6884
|
+
const containerId = psResult.stdout.trim().split("\n")[0]?.trim();
|
|
6885
|
+
if (!containerId) {
|
|
6886
|
+
throw new Error(
|
|
6887
|
+
`No running container for '${args.containerRoot}'. Start it with \`monoceros start <name>\` (or open a shell with \`monoceros shell <name>\`) and retry.`
|
|
6888
|
+
);
|
|
6889
|
+
}
|
|
6890
|
+
const inspect = await args.docker([
|
|
6891
|
+
"inspect",
|
|
6892
|
+
"--format",
|
|
6893
|
+
"{{json .NetworkSettings.Networks}}",
|
|
6894
|
+
containerId
|
|
6895
|
+
]);
|
|
6896
|
+
if (inspect.exitCode !== 0) {
|
|
6897
|
+
throw new Error(
|
|
6898
|
+
`docker inspect failed: ${inspect.stderr.trim() || `exit ${inspect.exitCode}`}`
|
|
6899
|
+
);
|
|
6900
|
+
}
|
|
6901
|
+
let networks = null;
|
|
6902
|
+
try {
|
|
6903
|
+
networks = JSON.parse(inspect.stdout);
|
|
6904
|
+
} catch {
|
|
6905
|
+
throw new Error(
|
|
6906
|
+
`Unexpected docker inspect output: ${inspect.stdout.slice(0, 200)}`
|
|
6907
|
+
);
|
|
6908
|
+
}
|
|
6909
|
+
if (!networks) {
|
|
6910
|
+
throw new Error(
|
|
6911
|
+
`Container ${containerId} reports no networks. Restart it and retry.`
|
|
6912
|
+
);
|
|
6913
|
+
}
|
|
6914
|
+
for (const [name, settings] of Object.entries(networks)) {
|
|
6915
|
+
if (settings.IPAddress && settings.IPAddress.length > 0) {
|
|
6916
|
+
return { network: name, ip: settings.IPAddress };
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
throw new Error(
|
|
6920
|
+
`Container ${containerId} has no network with a reachable IP. Restart it and retry.`
|
|
6921
|
+
);
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
// src/tunnel/port-check.ts
|
|
6925
|
+
import { Socket as Socket2 } from "net";
|
|
6926
|
+
var CONNECT_TIMEOUT_MS2 = 750;
|
|
6927
|
+
var realPortProbe2 = (port, address) => {
|
|
6928
|
+
const probeHost = address === "0.0.0.0" ? "127.0.0.1" : address;
|
|
6929
|
+
return new Promise((resolve) => {
|
|
6930
|
+
const socket = new Socket2();
|
|
6931
|
+
let settled = false;
|
|
6932
|
+
const settle = (result) => {
|
|
6933
|
+
if (settled) return;
|
|
6934
|
+
settled = true;
|
|
6935
|
+
socket.destroy();
|
|
6936
|
+
resolve(result);
|
|
6937
|
+
};
|
|
6938
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS2);
|
|
6939
|
+
socket.once("connect", () => {
|
|
6940
|
+
settle({
|
|
6941
|
+
ok: false,
|
|
6942
|
+
code: "EADDRINUSE",
|
|
6943
|
+
message: `another process is listening on ${port}`
|
|
6944
|
+
});
|
|
6945
|
+
});
|
|
6946
|
+
socket.once("timeout", () => {
|
|
6947
|
+
settle({ ok: true });
|
|
6948
|
+
});
|
|
6949
|
+
socket.once("error", (err) => {
|
|
6950
|
+
const code = err.code ?? "UNKNOWN";
|
|
6951
|
+
if (code === "ECONNREFUSED") {
|
|
6952
|
+
settle({ ok: true });
|
|
6953
|
+
} else {
|
|
6954
|
+
settle({ ok: false, code, message: err.message });
|
|
6955
|
+
}
|
|
6956
|
+
});
|
|
6957
|
+
socket.connect(port, probeHost);
|
|
6958
|
+
});
|
|
6959
|
+
};
|
|
6960
|
+
async function preflightLocalPort(opts) {
|
|
6961
|
+
const probe = opts.probe ?? realPortProbe2;
|
|
6962
|
+
const result = await probe(opts.port, opts.address);
|
|
6963
|
+
if (result.ok) return;
|
|
6964
|
+
throw new Error(formatLocalPortHeldError(opts.port, opts.address, result));
|
|
6965
|
+
}
|
|
6966
|
+
function formatLocalPortHeldError(port, address, result) {
|
|
6967
|
+
const lines = [];
|
|
6968
|
+
if (result.code === "EADDRINUSE") {
|
|
6969
|
+
lines.push(`Local port ${port} on ${address} is already in use.`);
|
|
6970
|
+
lines.push("");
|
|
6971
|
+
lines.push("Identify the holder, then either stop it or pick a different");
|
|
6972
|
+
lines.push("port for the tunnel:");
|
|
6973
|
+
lines.push("");
|
|
6974
|
+
lines.push(` sudo lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
|
|
6975
|
+
lines.push(` # or: sudo ss -tlnp | grep ":${port}\\b"`);
|
|
6976
|
+
lines.push("");
|
|
6977
|
+
lines.push("Re-run with an explicit local port:");
|
|
6978
|
+
lines.push(` monoceros tunnel \u2026 --local-port=${port + 1}`);
|
|
6979
|
+
} else {
|
|
6980
|
+
lines.push(
|
|
6981
|
+
`Cannot probe local port ${port} on ${address}: ${result.message}`
|
|
6982
|
+
);
|
|
6983
|
+
lines.push("");
|
|
6984
|
+
lines.push(
|
|
6985
|
+
"Most likely the host network stack (firewall, namespace) is interfering."
|
|
6986
|
+
);
|
|
6987
|
+
lines.push("Try a different local port via `--local-port=<n>`.");
|
|
6988
|
+
}
|
|
6989
|
+
return lines.join("\n");
|
|
6990
|
+
}
|
|
6991
|
+
|
|
6992
|
+
// src/tunnel/run.ts
|
|
6993
|
+
var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
|
|
6994
|
+
var defaultDockerSpawn = (args) => {
|
|
6995
|
+
const child = spawn9("docker", args, {
|
|
6996
|
+
stdio: "inherit"
|
|
6997
|
+
});
|
|
6998
|
+
const exited = new Promise((resolve, reject) => {
|
|
6999
|
+
child.on("error", reject);
|
|
7000
|
+
child.on("exit", (code, signal) => {
|
|
7001
|
+
if (typeof code === "number") resolve(code);
|
|
7002
|
+
else if (signal) resolve(128 + signalNumber(signal));
|
|
7003
|
+
else resolve(0);
|
|
7004
|
+
});
|
|
7005
|
+
});
|
|
7006
|
+
return {
|
|
7007
|
+
exited,
|
|
7008
|
+
kill: (signal) => {
|
|
7009
|
+
try {
|
|
7010
|
+
child.kill(signal);
|
|
7011
|
+
} catch {
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
};
|
|
7015
|
+
};
|
|
7016
|
+
function signalNumber(signal) {
|
|
7017
|
+
switch (signal) {
|
|
7018
|
+
case "SIGINT":
|
|
7019
|
+
return 2;
|
|
7020
|
+
case "SIGTERM":
|
|
7021
|
+
return 15;
|
|
7022
|
+
default:
|
|
7023
|
+
return 1;
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
var installSigintDefault = (handler) => {
|
|
7027
|
+
process.on("SIGINT", handler);
|
|
7028
|
+
return () => process.off("SIGINT", handler);
|
|
7029
|
+
};
|
|
7030
|
+
var DEFAULT_LOCAL_ADDRESS = "127.0.0.1";
|
|
7031
|
+
async function runTunnel(opts) {
|
|
7032
|
+
const log = opts.logger ?? {
|
|
7033
|
+
info: (m) => consola32.info(m),
|
|
7034
|
+
warn: (m) => consola32.warn(m)
|
|
7035
|
+
};
|
|
7036
|
+
const resolve = opts.resolve ?? resolveTunnelTarget;
|
|
7037
|
+
const resolveArgs2 = {
|
|
7038
|
+
name: opts.name,
|
|
7039
|
+
target: opts.target,
|
|
7040
|
+
...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
|
|
7041
|
+
};
|
|
7042
|
+
const resolved = await resolve(resolveArgs2);
|
|
7043
|
+
const localPort = opts.localPort ?? resolved.internalPort;
|
|
7044
|
+
const localAddress = opts.localAddress ?? DEFAULT_LOCAL_ADDRESS;
|
|
7045
|
+
validateLocalAddress(localAddress);
|
|
7046
|
+
const preflight = opts.preflight ?? preflightLocalPort;
|
|
7047
|
+
await preflight({ port: localPort, address: localAddress });
|
|
7048
|
+
const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
|
|
7049
|
+
const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
|
|
7050
|
+
const dockerArgs = buildDockerArgs({
|
|
7051
|
+
localAddress,
|
|
7052
|
+
localPort,
|
|
7053
|
+
internalPort: resolved.internalPort,
|
|
7054
|
+
network: resolved.network,
|
|
7055
|
+
targetHost: resolved.targetHost
|
|
7056
|
+
});
|
|
7057
|
+
log.info(
|
|
7058
|
+
`Tunnel: ${localAddress}:${localPort} \u2192 ${resolved.display}:${resolved.internalPort} (Ctrl+C to stop)`
|
|
7059
|
+
);
|
|
7060
|
+
const handle = dockerSpawn(dockerArgs);
|
|
7061
|
+
const uninstall = installSignalHandler(() => {
|
|
7062
|
+
});
|
|
7063
|
+
try {
|
|
7064
|
+
const exitCode = await handle.exited;
|
|
7065
|
+
if (exitCode === 130) return 0;
|
|
7066
|
+
return exitCode;
|
|
7067
|
+
} finally {
|
|
7068
|
+
uninstall();
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
function buildDockerArgs(input) {
|
|
7072
|
+
return [
|
|
7073
|
+
"run",
|
|
7074
|
+
"--rm",
|
|
7075
|
+
"-i",
|
|
7076
|
+
`--network=${input.network}`,
|
|
7077
|
+
"-p",
|
|
7078
|
+
`${input.localAddress}:${input.localPort}:${input.internalPort}`,
|
|
7079
|
+
SOCAT_IMAGE,
|
|
7080
|
+
`TCP-LISTEN:${input.internalPort},fork,reuseaddr`,
|
|
7081
|
+
`TCP:${input.targetHost}:${input.internalPort}`
|
|
7082
|
+
];
|
|
7083
|
+
}
|
|
7084
|
+
var IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
7085
|
+
function validateLocalAddress(addr) {
|
|
7086
|
+
if (addr === "localhost") return;
|
|
7087
|
+
if (IPV4_RE.test(addr)) {
|
|
7088
|
+
for (const part of addr.split(".")) {
|
|
7089
|
+
const n = Number(part);
|
|
7090
|
+
if (n < 0 || n > 255) {
|
|
7091
|
+
throw new Error(
|
|
7092
|
+
`Invalid --local-address '${addr}': each dotted-quad octet must be 0-255.`
|
|
7093
|
+
);
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
return;
|
|
7097
|
+
}
|
|
7098
|
+
throw new Error(
|
|
7099
|
+
`Invalid --local-address '${addr}'. Use 127.0.0.1 (default), 0.0.0.0, or a specific IPv4 address.`
|
|
7100
|
+
);
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
// src/commands/tunnel.ts
|
|
7104
|
+
var tunnelCommand = defineCommand29({
|
|
7105
|
+
meta: {
|
|
7106
|
+
name: "tunnel",
|
|
7107
|
+
group: "discovery",
|
|
7108
|
+
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."
|
|
7109
|
+
},
|
|
7110
|
+
args: {
|
|
7111
|
+
name: {
|
|
7112
|
+
type: "positional",
|
|
7113
|
+
description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
|
|
7114
|
+
required: true
|
|
7115
|
+
},
|
|
7116
|
+
target: {
|
|
7117
|
+
type: "positional",
|
|
7118
|
+
description: "Service name from the container yml (e.g. `postgres`) or an in-container port number (e.g. `8080`).",
|
|
7119
|
+
required: true
|
|
7120
|
+
},
|
|
7121
|
+
"local-port": {
|
|
7122
|
+
type: "string",
|
|
7123
|
+
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."
|
|
7124
|
+
},
|
|
7125
|
+
"local-address": {
|
|
7126
|
+
type: "string",
|
|
7127
|
+
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)."
|
|
7128
|
+
}
|
|
7129
|
+
},
|
|
7130
|
+
async run({ args }) {
|
|
7131
|
+
try {
|
|
7132
|
+
const localPort = parseLocalPort(args["local-port"]);
|
|
7133
|
+
const exitCode = await runTunnel({
|
|
7134
|
+
name: args.name,
|
|
7135
|
+
target: args.target,
|
|
7136
|
+
...localPort !== void 0 ? { localPort } : {},
|
|
7137
|
+
...args["local-address"] ? { localAddress: args["local-address"] } : {}
|
|
7138
|
+
});
|
|
7139
|
+
process.exit(exitCode);
|
|
7140
|
+
} catch (err) {
|
|
7141
|
+
consola33.error(err instanceof Error ? err.message : String(err));
|
|
7142
|
+
process.exit(1);
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
});
|
|
7146
|
+
function parseLocalPort(raw) {
|
|
7147
|
+
if (raw === void 0) return void 0;
|
|
7148
|
+
const n = Number(raw);
|
|
7149
|
+
if (!Number.isInteger(n) || n <= 0 || n >= 65536) {
|
|
7150
|
+
throw new Error(
|
|
7151
|
+
`Invalid --local-port '${raw}': must be an integer between 1 and 65535.`
|
|
7152
|
+
);
|
|
7153
|
+
}
|
|
7154
|
+
return n;
|
|
7155
|
+
}
|
|
7156
|
+
|
|
6731
7157
|
// src/main.ts
|
|
6732
|
-
var main =
|
|
7158
|
+
var main = defineCommand30({
|
|
6733
7159
|
meta: {
|
|
6734
7160
|
name: "monoceros",
|
|
6735
7161
|
version: CLI_VERSION,
|
|
@@ -6762,6 +7188,7 @@ var main = defineCommand29({
|
|
|
6762
7188
|
"remove-repo": removeRepoCommand,
|
|
6763
7189
|
"remove-port": removePortCommand,
|
|
6764
7190
|
port: portCommand,
|
|
7191
|
+
tunnel: tunnelCommand,
|
|
6765
7192
|
completion: completionCommand,
|
|
6766
7193
|
__complete: __completeCommand
|
|
6767
7194
|
}
|