@getmonoceros/workbench 1.36.1 → 1.36.2

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.
Files changed (3) hide show
  1. package/dist/bin.js +1052 -879
  2. package/dist/bin.js.map +1 -1
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -2860,7 +2860,7 @@ function windowsSshPort(name) {
2860
2860
  for (let i = 0; i < name.length; i++) {
2861
2861
  h = h * 31 + name.charCodeAt(i) >>> 0;
2862
2862
  }
2863
- return 49152 + h % 16384;
2863
+ return 2e4 + h % 2e4;
2864
2864
  }
2865
2865
  function windowsHostBlock(hostAlias, keyWin, port) {
2866
2866
  const head = [
@@ -4451,6 +4451,33 @@ function generateAgentsMd(input) {
4451
4451
  "the URL, so they can open it themselves if no bridge is active."
4452
4452
  );
4453
4453
  lines.push("");
4454
+ lines.push("### Dev servers (so the proxy and LAN can reach them)");
4455
+ lines.push("");
4456
+ lines.push(
4457
+ "A dev server you start must be reachable through the Monoceros proxy",
4458
+ "(and, when the user shares it to their phone/LAN, over the network).",
4459
+ "Configure it so:"
4460
+ );
4461
+ lines.push("");
4462
+ lines.push(
4463
+ "- it **listens on `0.0.0.0`**, not `127.0.0.1` (otherwise the proxy",
4464
+ " cannot reach it);",
4465
+ "- it **accepts the proxy/LAN hostnames** \u2014 Vite `server.allowedHosts`,",
4466
+ " Angular `--allowed-hosts`, CRA `DANGEROUSLY_DISABLE_HOST_CHECK`;",
4467
+ "- it does **not pin the HMR/live-reload socket** to a fixed host or port",
4468
+ " \u2014 let it follow the page URL (e.g. for Vite, do not set",
4469
+ " `server.hmr.clientPort`), so HMR works on `<name>.localhost` and over",
4470
+ " the LAN alike;",
4471
+ "- the **backend is reached via the dev-server proxy** under a relative",
4472
+ " path (Vite `server.proxy`, Angular `proxy.conf.json`, CRA",
4473
+ " `setupProxy.js`) so the browser only ever talks to one origin."
4474
+ );
4475
+ lines.push("");
4476
+ lines.push(
4477
+ "These are dev-server-only settings (a production build ignores them), so",
4478
+ "they are safe to keep."
4479
+ );
4480
+ lines.push("");
4454
4481
  }
4455
4482
  lines.push("## How to extend this container");
4456
4483
  lines.push("");
@@ -7012,7 +7039,7 @@ async function cleanupDockerObjects(opts) {
7012
7039
  return { exitCode: rmExit, removedIds: ids };
7013
7040
  }
7014
7041
  function composeProjectName(root) {
7015
- return `${path18.basename(root)}_devcontainer`;
7042
+ return `${path18.basename(root)}_devcontainer`.toLowerCase().replace(/[^a-z0-9_-]/g, "");
7016
7043
  }
7017
7044
  function assertDevcontainer(root) {
7018
7045
  if (!existsSync10(path18.join(root, ".devcontainer"))) {
@@ -8772,7 +8799,7 @@ var CLI_VERSION;
8772
8799
  var init_version = __esm({
8773
8800
  "src/version.ts"() {
8774
8801
  "use strict";
8775
- CLI_VERSION = true ? "1.36.1" : "dev";
8802
+ CLI_VERSION = true ? "1.36.2" : "dev";
8776
8803
  }
8777
8804
  });
8778
8805
 
@@ -9494,6 +9521,7 @@ var init_resolve = __esm({
9494
9521
  "remove-port",
9495
9522
  "port",
9496
9523
  "tunnel",
9524
+ "share",
9497
9525
  "completion"
9498
9526
  ];
9499
9527
  containerName = (ctx) => listContainerNames(ctx);
@@ -9633,6 +9661,9 @@ var init_resolve = __esm({
9633
9661
  "--local-address": { type: "value" }
9634
9662
  }
9635
9663
  },
9664
+ share: {
9665
+ positionals: [containerName, (ctx) => listAppCandidates(ctx)]
9666
+ },
9636
9667
  completion: {
9637
9668
  positionals: [() => listShellNames()]
9638
9669
  },
@@ -11941,530 +11972,494 @@ var init_run2 = __esm({
11941
11972
  }
11942
11973
  });
11943
11974
 
11944
- // src/commands/shell.ts
11945
- import { defineCommand as defineCommand29 } from "citty";
11946
- import { consola as consola34 } from "consola";
11947
- var shellCommand;
11948
- var init_shell2 = __esm({
11949
- "src/commands/shell.ts"() {
11950
- "use strict";
11951
- init_paths();
11952
- init_shell();
11953
- shellCommand = defineCommand29({
11954
- meta: {
11955
- name: "shell",
11956
- group: "run",
11957
- description: "Open an interactive bash session inside the named dev-container."
11958
- },
11959
- args: {
11960
- name: {
11961
- type: "positional",
11962
- description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
11963
- required: true
11964
- }
11965
- },
11966
- async run({ args }) {
11967
- try {
11968
- const exitCode = await runShell({
11969
- root: containerDir(args.name),
11970
- name: args.name
11971
- });
11972
- process.exit(exitCode);
11973
- } catch (err) {
11974
- consola34.error(err instanceof Error ? err.message : String(err));
11975
- process.exit(1);
11976
- }
11977
- }
11975
+ // src/tunnel/resolve.ts
11976
+ import { existsSync as existsSync20 } from "fs";
11977
+ import path32 from "path";
11978
+ async function resolveTunnelTarget(opts) {
11979
+ const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
11980
+ if (!existsSync20(ymlPath)) {
11981
+ throw new Error(
11982
+ `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
11983
+ );
11984
+ }
11985
+ const parsed = await readConfig(ymlPath);
11986
+ const config = parsed.config;
11987
+ const containerRoot = containerDir(opts.name, opts.monocerosHome);
11988
+ if (!existsSync20(containerRoot)) {
11989
+ throw new Error(
11990
+ `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
11991
+ );
11992
+ }
11993
+ const composePath = path32.join(containerRoot, ".devcontainer", "compose.yaml");
11994
+ const isCompose = existsSync20(composePath);
11995
+ const parsedTarget = parseTargetArg(opts.target, config);
11996
+ const docker = opts.docker ?? defaultDockerExec;
11997
+ if (isCompose) {
11998
+ return resolveCompose2({
11999
+ name: opts.name,
12000
+ containerRoot,
12001
+ parsedTarget
11978
12002
  });
11979
12003
  }
11980
- });
11981
-
11982
- // src/commands/start.ts
11983
- import { defineCommand as defineCommand30 } from "citty";
11984
- import { consola as consola35 } from "consola";
11985
- async function bringContainerUp(name, openTool) {
11986
- {
11987
- const args = { name, open: openTool };
11988
- let needsProxy = false;
11989
- let hostPort = 80;
11990
- let deferred = [];
11991
- let runtimeVersion;
11992
- try {
11993
- const parsed = await readConfig(containerConfigPath(args.name));
11994
- runtimeVersion = parsed.config.runtimeVersion;
11995
- if ((parsed.config.routing?.ports ?? []).length > 0) {
11996
- needsProxy = true;
11997
- const global = await readMonocerosConfig();
11998
- hostPort = proxyHostPort(global);
11999
- }
12000
- deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
12001
- } catch (err) {
12002
- consola35.warn(
12003
- `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
12004
+ return resolveImageMode({
12005
+ name: opts.name,
12006
+ containerRoot,
12007
+ parsedTarget,
12008
+ config,
12009
+ docker
12010
+ });
12011
+ }
12012
+ function parseTargetArg(raw, config) {
12013
+ const colon = raw.indexOf(":");
12014
+ if (colon > 0) {
12015
+ const name = raw.slice(0, colon);
12016
+ const port = Number(raw.slice(colon + 1));
12017
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
12018
+ throw new Error(
12019
+ `Invalid target '${raw}'. Use <service>:<port> with a numeric port (1\u201365535), a bare port number, or a configured service name.`
12004
12020
  );
12005
12021
  }
12006
- if (needsProxy) {
12007
- await preflightHostPort(hostPort);
12008
- await ensureProxy({ hostPort });
12009
- }
12010
- const exitCode = await runStart({
12011
- root: containerDir(args.name),
12012
- silent: true,
12013
- logger: { info: () => {
12014
- } }
12015
- });
12016
- if (exitCode === 0 && runtimeSupportsBrowserBridge(runtimeVersion)) {
12017
- spawnBridgeDaemon(containerDir(args.name));
12018
- }
12019
- if (exitCode === 0 && deferred.length > 0) {
12020
- try {
12021
- const deferExit = await startDeferredServices({
12022
- root: containerDir(args.name),
12023
- services: deferred,
12024
- logger: consola35
12025
- });
12026
- if (deferExit !== 0) {
12027
- consola35.warn(
12028
- `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
12029
- );
12030
- }
12031
- } catch (err) {
12032
- consola35.warn(
12033
- `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
12034
- );
12035
- }
12036
- }
12037
- if (exitCode === 0) {
12038
- consola35.success(`Container '${args.name}' is up.`);
12039
- }
12040
- if (args.open && exitCode === 0) {
12041
- try {
12042
- await runOpen({ name: args.name, tool: args.open });
12043
- } catch (err) {
12044
- consola35.warn(err instanceof Error ? err.message : String(err));
12045
- }
12046
- }
12047
- return exitCode;
12022
+ findConfiguredService(config, name);
12023
+ return { kind: "service", service: name, port };
12024
+ }
12025
+ const asNumber = Number(raw);
12026
+ if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
12027
+ return { kind: "port", port: asNumber };
12048
12028
  }
12029
+ const match = findConfiguredService(config, raw);
12030
+ if (match.port === void 0) {
12031
+ throw new Error(
12032
+ `Service '${raw}' declares no port, so tunnel can't know what to forward. Add \`port: <n>\` to the service in the yml and re-apply, or pass one explicitly: \`monoceros tunnel <name> ${raw}:<port>\`.`
12033
+ );
12034
+ }
12035
+ return { kind: "service", service: raw, port: match.port };
12049
12036
  }
12050
- var startCommand;
12051
- var init_start = __esm({
12052
- "src/commands/start.ts"() {
12053
- "use strict";
12054
- init_global();
12055
- init_io();
12056
- init_paths();
12057
- init_bridge_daemon();
12058
- init_compose();
12059
- init_catalog();
12060
- init_open();
12061
- init_proxy();
12062
- init_port_check();
12063
- init_app_control();
12064
- init_dispatch();
12065
- startCommand = defineCommand30({
12066
- meta: {
12067
- name: "start",
12068
- group: "run",
12069
- description: "Bring the named dev-container up. With an <app>, start that app inside it (per its projects/<app>/.monoceros/launch.json); the container is brought up first if needed."
12070
- },
12071
- args: {
12072
- name: {
12073
- type: "positional",
12074
- description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
12075
- required: true
12076
- },
12077
- app: {
12078
- type: "positional",
12079
- description: "App to start (a path under projects/ with .monoceros/launch.json). Omit to just bring the container up.",
12080
- required: false
12081
- },
12082
- target: {
12083
- type: "string",
12084
- description: `Which launch target to start (defaults to the app's "default" target, or its only one).`
12085
- },
12086
- open: {
12087
- type: "string",
12088
- description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
12089
- }
12090
- },
12091
- run({ args }) {
12092
- if (typeof args.app === "string" && args.app.length > 0) {
12093
- const app = args.app;
12094
- const target = typeof args.target === "string" ? args.target : void 0;
12095
- return dispatch(async () => {
12096
- if (!await findRunningContainer(args.name)) {
12097
- const up = await bringContainerUp(args.name, void 0);
12098
- if (up !== 0) return up;
12099
- }
12100
- return runAppCtl(args.name, ctlArgs("start", app, target));
12101
- });
12102
- }
12103
- return dispatch(
12104
- () => bringContainerUp(
12105
- args.name,
12106
- typeof args.open === "string" ? args.open : void 0
12107
- )
12108
- );
12109
- }
12110
- });
12037
+ function findConfiguredService(config, name) {
12038
+ const services = config.services.map(resolveService);
12039
+ const match = services.find((s) => s.name === name);
12040
+ if (!match) {
12041
+ const names = services.map((s) => s.name);
12042
+ const list = names.length > 0 ? names.join(", ") : "(none configured)";
12043
+ throw new Error(
12044
+ `Service '${name}' is not configured in this container's yml. Configured services: ${list}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
12045
+ );
12111
12046
  }
12112
- });
12113
-
12114
- // src/status/index.ts
12115
- function shortFeatureName2(ref) {
12116
- const withoutTag = ref.replace(/:[^:/@]+$/, "");
12117
- const idx = withoutTag.lastIndexOf("/");
12118
- return idx >= 0 ? withoutTag.slice(idx + 1) : withoutTag;
12047
+ return match;
12119
12048
  }
12120
- function cols(line) {
12121
- return line.split(" ");
12049
+ function resolveCompose2(args) {
12050
+ const network = `${composeProjectName(args.containerRoot)}_default`;
12051
+ if (args.parsedTarget.kind === "service") {
12052
+ return {
12053
+ network,
12054
+ targetHost: args.parsedTarget.service,
12055
+ internalPort: args.parsedTarget.port,
12056
+ display: `${args.name}/${args.parsedTarget.service}:${args.parsedTarget.port}`
12057
+ };
12058
+ }
12059
+ return {
12060
+ network,
12061
+ targetHost: "workspace",
12062
+ internalPort: args.parsedTarget.port,
12063
+ display: `${args.name}:${args.parsedTarget.port}`
12064
+ };
12122
12065
  }
12123
- function nonEmptyLines(s) {
12124
- return s.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
12125
- }
12126
- async function gatherStatus(name, opts = {}) {
12127
- const docker = opts.docker ?? realDockerLookup;
12128
- const root = containerDir(name, opts.home);
12129
- let runtimeVersion;
12130
- let languages = [];
12131
- let features = [];
12132
- let declaredServices = [];
12133
- let declaredPorts = [];
12134
- let configured = false;
12135
- try {
12136
- const parsed = await readConfig(containerConfigPath(name, opts.home));
12137
- const createOpts = solutionConfigToCreateOptions(parsed.config);
12138
- configured = true;
12139
- runtimeVersion = createOpts.runtimeVersion;
12140
- languages = createOpts.languages;
12141
- features = Object.keys(createOpts.features ?? {}).map(shortFeatureName2);
12142
- declaredServices = createOpts.services.map((s) => ({
12143
- name: s.name,
12144
- ...typeof s.port === "number" ? { port: s.port } : {}
12145
- }));
12146
- declaredPorts = createOpts.ports ?? [];
12147
- } catch {
12066
+ async function resolveImageMode(args) {
12067
+ if (args.parsedTarget.kind === "service") {
12068
+ throw new Error(
12069
+ `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.`
12070
+ );
12148
12071
  }
12149
- let container = {
12150
- exists: false,
12151
- running: false,
12152
- status: "",
12153
- dockerName: ""
12072
+ const ports = args.config.routing?.ports ?? [];
12073
+ if (ports.length > 0) {
12074
+ return {
12075
+ network: PROXY_NETWORK_NAME,
12076
+ targetHost: args.name,
12077
+ internalPort: args.parsedTarget.port,
12078
+ display: `${args.name}:${args.parsedTarget.port}`
12079
+ };
12080
+ }
12081
+ const { network, ip } = await lookupContainerNetwork({
12082
+ containerRoot: args.containerRoot,
12083
+ docker: args.docker
12084
+ });
12085
+ return {
12086
+ network,
12087
+ targetHost: ip,
12088
+ internalPort: args.parsedTarget.port,
12089
+ display: `${args.name}:${args.parsedTarget.port}`
12154
12090
  };
12155
- let containerId = "";
12156
- let composeProject = "";
12157
- const cps = await docker([
12091
+ }
12092
+ async function lookupContainerNetwork(args) {
12093
+ const psResult = await args.docker([
12158
12094
  "ps",
12159
- "-a",
12160
- "--no-trunc",
12095
+ "-q",
12161
12096
  "--filter",
12162
- `label=devcontainer.local_folder=${root}`,
12163
- "--format",
12164
- '{{.ID}} {{.State}} {{.Status}} {{.Names}} {{.Label "com.docker.compose.project"}}'
12097
+ `label=devcontainer.local_folder=${args.containerRoot}`
12165
12098
  ]);
12166
- const row2 = nonEmptyLines(cps.stdout)[0];
12167
- if (row2) {
12168
- const [id, state, status, names, project] = cols(row2);
12169
- containerId = id ?? "";
12170
- composeProject = project ?? "";
12171
- container = {
12172
- exists: true,
12173
- running: state === "running",
12174
- status: status ?? "",
12175
- dockerName: names ?? ""
12176
- };
12177
- }
12178
- const services = [];
12179
- if (declaredServices.length > 0) {
12180
- const live = /* @__PURE__ */ new Map();
12181
- if (composeProject) {
12182
- const sps = await docker([
12183
- "ps",
12184
- "-a",
12185
- "--filter",
12186
- `label=com.docker.compose.project=${composeProject}`,
12187
- "--format",
12188
- '{{.Label "com.docker.compose.service"}} {{.State}} {{.Status}}'
12189
- ]);
12190
- for (const l of nonEmptyLines(sps.stdout)) {
12191
- const [svc, state, status] = cols(l);
12192
- if (svc)
12193
- live.set(svc, { running: state === "running", status: status ?? "" });
12194
- }
12195
- }
12196
- for (const s of declaredServices) {
12197
- const m = live.get(s.name);
12198
- services.push({
12199
- name: s.name,
12200
- running: m?.running ?? false,
12201
- status: m?.status ?? "not created",
12202
- ...typeof s.port === "number" ? { port: s.port } : {}
12203
- });
12204
- }
12205
- }
12206
- const apps = [];
12207
- for (const app of await listApps(name, opts.home)) {
12208
- const cfg = await readLaunchConfig(name, app, opts.home);
12209
- if (!cfg) continue;
12210
- for (const t of cfg.configurations) {
12211
- apps.push({
12212
- app,
12213
- target: t.name,
12214
- port: typeof t.port === "number" ? t.port : null,
12215
- default: t.default === true,
12216
- running: void 0,
12217
- pid: null
12218
- });
12219
- }
12099
+ if (psResult.exitCode !== 0) {
12100
+ throw new Error(
12101
+ `docker ps failed: ${psResult.stderr.trim() || `exit ${psResult.exitCode}`}`
12102
+ );
12220
12103
  }
12221
- const appsSupported = runtimeSupportsAppStatus(runtimeVersion);
12222
- let appStateKnown = container.running && appsSupported && containerId !== "";
12223
- let appStateNote;
12224
- if (appStateKnown) {
12225
- const res = await docker([
12226
- "exec",
12227
- containerId,
12228
- "monoceros-ctl",
12229
- "list",
12230
- "--json"
12231
- ]);
12232
- if (res.exitCode === 0) {
12233
- const byKey = /* @__PURE__ */ new Map();
12234
- for (const l of nonEmptyLines(res.stdout)) {
12235
- try {
12236
- const o = JSON.parse(l);
12237
- byKey.set(`${o.app}\0${o.target}`, {
12238
- running: !!o.running,
12239
- pid: o.pid ?? null
12240
- });
12241
- } catch {
12242
- }
12243
- }
12244
- for (const a of apps) {
12245
- const o = byKey.get(`${a.app}\0${a.target}`);
12246
- if (o) {
12247
- a.running = o.running;
12248
- a.pid = o.pid;
12249
- } else {
12250
- a.running = false;
12251
- }
12252
- }
12253
- } else {
12254
- appStateKnown = false;
12255
- appStateNote = `could not read run state from the runner - rebuild the runtime image, then \`monoceros upgrade ${name}\``;
12256
- }
12104
+ const containerId = psResult.stdout.trim().split("\n")[0]?.trim();
12105
+ if (!containerId) {
12106
+ throw new Error(
12107
+ `No running container for '${args.containerRoot}'. Start it with \`monoceros start <name>\` (or open a shell with \`monoceros shell <name>\`) and retry.`
12108
+ );
12257
12109
  }
12258
- if (apps.length > 0 && !appStateKnown && !appStateNote) {
12259
- appStateNote = !container.running ? `start the container to see run state (\`monoceros start ${name}\`)` : `run state needs runtime 1.6.0+ (this one: ${runtimeVersion ?? "unpinned"})`;
12110
+ const inspect = await args.docker([
12111
+ "inspect",
12112
+ "--format",
12113
+ "{{json .NetworkSettings.Networks}}",
12114
+ containerId
12115
+ ]);
12116
+ if (inspect.exitCode !== 0) {
12117
+ throw new Error(
12118
+ `docker inspect failed: ${inspect.stderr.trim() || `exit ${inspect.exitCode}`}`
12119
+ );
12260
12120
  }
12261
- let hostPort = 80;
12121
+ let networks = null;
12262
12122
  try {
12263
- hostPort = proxyHostPort(
12264
- await readMonocerosConfig({
12265
- ...opts.home ? { monocerosHome: opts.home } : {}
12266
- })
12267
- );
12123
+ networks = JSON.parse(inspect.stdout);
12268
12124
  } catch {
12125
+ throw new Error(
12126
+ `Unexpected docker inspect output: ${inspect.stdout.slice(0, 200)}`
12127
+ );
12269
12128
  }
12270
- const ports = proxyUrlsFor(name, declaredPorts, hostPort);
12271
- return {
12272
- name,
12273
- configured,
12274
- container,
12275
- services,
12276
- apps,
12277
- appStateKnown,
12278
- ...appStateNote ? { appStateNote } : {},
12279
- ports,
12280
- builtIn: { languages, features }
12281
- };
12282
- }
12283
- function row(p, marker, name, detail, pad, indent = 2) {
12284
- const gap = Math.max(2, pad - name.length + 2);
12285
- return `${" ".repeat(indent)}${marker} ${p.cyan(name)}${" ".repeat(gap)}${detail}`;
12286
- }
12287
- function liveMarker(p, running) {
12288
- if (running === void 0) return " ";
12289
- return running ? p.green("\u2713") : p.dim("\xB7");
12290
- }
12291
- function renderContainer(p, m) {
12292
- const out = [p.sectionLine(m.name)];
12293
- if (!m.container.exists) {
12294
- out.push(
12295
- row(
12296
- p,
12297
- p.dim("\xB7"),
12298
- "(not created)",
12299
- p.dim(`run \`monoceros apply ${m.name}\``),
12300
- 14
12301
- )
12129
+ if (!networks) {
12130
+ throw new Error(
12131
+ `Container ${containerId} reports no networks. Restart it and retry.`
12302
12132
  );
12303
- return out;
12304
12133
  }
12305
- const detail = m.container.running ? p.dim(m.container.status || "running") : p.dim(m.container.status || "stopped");
12306
- out.push(
12307
- row(
12308
- p,
12309
- liveMarker(p, m.container.running),
12310
- m.container.dockerName,
12311
- detail,
12312
- 14
12313
- )
12134
+ for (const [name, settings] of Object.entries(networks)) {
12135
+ if (settings.IPAddress && settings.IPAddress.length > 0) {
12136
+ return { network: name, ip: settings.IPAddress };
12137
+ }
12138
+ }
12139
+ throw new Error(
12140
+ `Container ${containerId} has no network with a reachable IP. Restart it and retry.`
12314
12141
  );
12315
- return out;
12316
12142
  }
12317
- function renderServices(p, m) {
12318
- if (m.services.length === 0) return [];
12319
- const pad = Math.max(...m.services.map((s) => s.name.length));
12320
- const out = ["", p.sectionLine("Services")];
12321
- for (const s of m.services) {
12322
- const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12323
- out.push(row(p, liveMarker(p, s.running), s.name, detail, pad));
12143
+ var init_resolve2 = __esm({
12144
+ "src/tunnel/resolve.ts"() {
12145
+ "use strict";
12146
+ init_paths();
12147
+ init_io();
12148
+ init_compose();
12149
+ init_catalog();
12150
+ init_proxy();
12324
12151
  }
12325
- return out;
12326
- }
12327
- function appTargetDetail(p, name, t) {
12328
- const bits = [];
12329
- if (t.running === true) {
12330
- bits.push(
12331
- t.port !== null ? p.dim(`http://${name}-${t.port}.localhost`) : p.dim("running")
12152
+ });
12153
+
12154
+ // src/tunnel/port-check.ts
12155
+ import { Socket as Socket2 } from "net";
12156
+ async function preflightLocalPort(opts) {
12157
+ const probe = opts.probe ?? realPortProbe2;
12158
+ const result = await probe(opts.port, opts.address);
12159
+ if (result.ok) return;
12160
+ throw new Error(formatLocalPortHeldError(opts.port, opts.address, result));
12161
+ }
12162
+ function formatLocalPortHeldError(port, address, result) {
12163
+ const lines = [];
12164
+ if (result.code === "EADDRINUSE") {
12165
+ lines.push(`Local port ${port} on ${address} is already in use.`);
12166
+ lines.push("");
12167
+ lines.push("Identify the holder, then either stop it or pick a different");
12168
+ lines.push("port for the tunnel:");
12169
+ lines.push("");
12170
+ lines.push(` sudo lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
12171
+ lines.push(` # or: sudo ss -tlnp | grep ":${port}\\b"`);
12172
+ lines.push("");
12173
+ lines.push("Re-run with an explicit local port:");
12174
+ lines.push(` monoceros tunnel \u2026 --local-port=${port + 1}`);
12175
+ } else {
12176
+ lines.push(
12177
+ `Cannot probe local port ${port} on ${address}: ${result.message}`
12332
12178
  );
12333
- if (t.pid !== null) bits.push(p.dim(`pid ${t.pid}`));
12334
- } else if (t.running === false) {
12335
- bits.push(p.dim("stopped"));
12179
+ lines.push("");
12180
+ lines.push(
12181
+ "Most likely the host network stack (firewall, namespace) is interfering."
12182
+ );
12183
+ lines.push("Try a different local port via `--local-port=<n>`.");
12336
12184
  }
12337
- if (t.default) bits.push(p.dim("(default)"));
12338
- return bits.join(" ");
12185
+ return lines.join("\n");
12339
12186
  }
12340
- function renderApps(p, m) {
12341
- if (m.apps.length === 0) return [];
12342
- const out = ["", p.sectionLine("Apps")];
12343
- if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12344
- const byApp = /* @__PURE__ */ new Map();
12345
- for (const a of m.apps) {
12346
- const list = byApp.get(a.app) ?? [];
12347
- list.push(a);
12348
- byApp.set(a.app, list);
12187
+ var CONNECT_TIMEOUT_MS2, realPortProbe2;
12188
+ var init_port_check2 = __esm({
12189
+ "src/tunnel/port-check.ts"() {
12190
+ "use strict";
12191
+ CONNECT_TIMEOUT_MS2 = 750;
12192
+ realPortProbe2 = (port, address) => {
12193
+ const probeHost = address === "0.0.0.0" ? "127.0.0.1" : address;
12194
+ return new Promise((resolve) => {
12195
+ const socket = new Socket2();
12196
+ let settled = false;
12197
+ const settle = (result) => {
12198
+ if (settled) return;
12199
+ settled = true;
12200
+ socket.destroy();
12201
+ resolve(result);
12202
+ };
12203
+ socket.setTimeout(CONNECT_TIMEOUT_MS2);
12204
+ socket.once("connect", () => {
12205
+ settle({
12206
+ ok: false,
12207
+ code: "EADDRINUSE",
12208
+ message: `another process is listening on ${port}`
12209
+ });
12210
+ });
12211
+ socket.once("timeout", () => {
12212
+ settle({ ok: true });
12213
+ });
12214
+ socket.once("error", (err) => {
12215
+ const code = err.code ?? "UNKNOWN";
12216
+ if (code === "ECONNREFUSED") {
12217
+ settle({ ok: true });
12218
+ } else {
12219
+ settle({ ok: false, code, message: err.message });
12220
+ }
12221
+ });
12222
+ socket.connect(port, probeHost);
12223
+ });
12224
+ };
12349
12225
  }
12350
- for (const [app, targets] of byApp) {
12351
- out.push(` ${p.cyan(app)}`);
12352
- const pad = Math.max(...targets.map((t) => t.target.length));
12353
- for (const t of targets) {
12354
- out.push(
12355
- row(
12356
- p,
12357
- liveMarker(p, t.running),
12358
- t.target,
12359
- appTargetDetail(p, m.name, t),
12360
- pad,
12361
- 4
12362
- )
12363
- );
12364
- }
12226
+ });
12227
+
12228
+ // src/tunnel/run.ts
12229
+ import { spawn as spawn14 } from "child_process";
12230
+ import { consola as consola34 } from "consola";
12231
+ function signalNumber(signal) {
12232
+ switch (signal) {
12233
+ case "SIGINT":
12234
+ return 2;
12235
+ case "SIGTERM":
12236
+ return 15;
12237
+ default:
12238
+ return 1;
12365
12239
  }
12366
- return out;
12367
12240
  }
12368
- function renderPorts(p, m) {
12369
- if (m.ports.length === 0) return [];
12370
- const pad = Math.max(...m.ports.map((r) => String(r.port).length));
12371
- const out = ["", p.sectionLine("Ports")];
12372
- for (const r of m.ports) {
12373
- const hosts = r.isDefault ? `${m.name}.localhost \xB7 ${m.name}-${r.port}.localhost` : `${m.name}-${r.port}.localhost`;
12374
- out.push(row(p, " ", String(r.port), p.dim(hosts), pad));
12241
+ async function runTunnel(opts) {
12242
+ const log = opts.logger ?? {
12243
+ info: (m) => consola34.info(m),
12244
+ warn: (m) => consola34.warn(m)
12245
+ };
12246
+ const resolve = opts.resolve ?? resolveTunnelTarget;
12247
+ const resolveArgs3 = {
12248
+ name: opts.name,
12249
+ target: opts.target,
12250
+ ...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
12251
+ };
12252
+ const resolved = await resolve(resolveArgs3);
12253
+ const localPort = opts.localPort ?? resolved.internalPort;
12254
+ const localAddress = opts.localAddress ?? DEFAULT_LOCAL_ADDRESS;
12255
+ validateLocalAddress(localAddress);
12256
+ const preflight = opts.preflight ?? preflightLocalPort;
12257
+ await preflight({ port: localPort, address: localAddress });
12258
+ const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
12259
+ const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
12260
+ const dockerArgs = buildDockerArgs({
12261
+ localAddress,
12262
+ localPort,
12263
+ internalPort: resolved.internalPort,
12264
+ network: resolved.network,
12265
+ targetHost: resolved.targetHost
12266
+ });
12267
+ log.info(
12268
+ `Tunnel: ${localAddress}:${localPort} \u2192 ${resolved.display}:${resolved.internalPort} (Ctrl+C to stop)`
12269
+ );
12270
+ const handle = dockerSpawn(dockerArgs);
12271
+ const uninstall = installSignalHandler(() => {
12272
+ });
12273
+ try {
12274
+ const exitCode = await handle.exited;
12275
+ if (exitCode === 130) return 0;
12276
+ return exitCode;
12277
+ } finally {
12278
+ uninstall();
12375
12279
  }
12376
- return out;
12377
12280
  }
12378
- function renderBuiltIn(p, m) {
12379
- const lines = [];
12380
- if (m.builtIn.languages.length > 0)
12381
- lines.push({ label: "Languages", values: m.builtIn.languages });
12382
- if (m.builtIn.features.length > 0)
12383
- lines.push({ label: "Features", values: m.builtIn.features });
12384
- if (lines.length === 0) return [];
12385
- const labelWidth = Math.max(...lines.map((l) => l.label.length));
12386
- const out = ["", `${p.sectionLine("Built in")} ${p.dim("from the yml")}`];
12387
- for (const l of lines) {
12388
- out.push(` ${l.label.padEnd(labelWidth)} ${p.cyan(l.values.join(", "))}`);
12281
+ function buildDockerArgs(input) {
12282
+ return [
12283
+ "run",
12284
+ "--rm",
12285
+ "-i",
12286
+ `--network=${input.network}`,
12287
+ "-p",
12288
+ `${input.localAddress}:${input.localPort}:${input.internalPort}`,
12289
+ SOCAT_IMAGE,
12290
+ `TCP-LISTEN:${input.internalPort},fork,reuseaddr`,
12291
+ `TCP:${input.targetHost}:${input.internalPort}`
12292
+ ];
12293
+ }
12294
+ function validateLocalAddress(addr) {
12295
+ if (addr === "localhost") return;
12296
+ if (IPV4_RE.test(addr)) {
12297
+ for (const part of addr.split(".")) {
12298
+ const n = Number(part);
12299
+ if (n < 0 || n > 255) {
12300
+ throw new Error(
12301
+ `Invalid --local-address '${addr}': each dotted-quad octet must be 0-255.`
12302
+ );
12303
+ }
12304
+ }
12305
+ return;
12389
12306
  }
12390
- return out;
12307
+ throw new Error(
12308
+ `Invalid --local-address '${addr}'. Use 127.0.0.1 (default), 0.0.0.0, or a specific IPv4 address.`
12309
+ );
12391
12310
  }
12392
- function renderStatus(m, p) {
12393
- return [
12394
- ...renderContainer(p, m),
12395
- ...renderServices(p, m),
12396
- ...renderApps(p, m),
12397
- ...renderPorts(p, m),
12398
- ...renderBuiltIn(p, m)
12399
- ].join("\n");
12311
+ var SOCAT_IMAGE, defaultDockerSpawn, installSigintDefault, DEFAULT_LOCAL_ADDRESS, IPV4_RE;
12312
+ var init_run3 = __esm({
12313
+ "src/tunnel/run.ts"() {
12314
+ "use strict";
12315
+ init_resolve2();
12316
+ init_port_check2();
12317
+ SOCAT_IMAGE = "alpine/socat:1.8.0.3";
12318
+ defaultDockerSpawn = (args) => {
12319
+ const child = spawn14("docker", args, {
12320
+ stdio: "inherit"
12321
+ });
12322
+ const exited = new Promise((resolve, reject) => {
12323
+ child.on("error", reject);
12324
+ child.on("exit", (code, signal) => {
12325
+ if (typeof code === "number") resolve(code);
12326
+ else if (signal) resolve(128 + signalNumber(signal));
12327
+ else resolve(0);
12328
+ });
12329
+ });
12330
+ return {
12331
+ exited,
12332
+ kill: (signal) => {
12333
+ try {
12334
+ child.kill(signal);
12335
+ } catch {
12336
+ }
12337
+ }
12338
+ };
12339
+ };
12340
+ installSigintDefault = (handler) => {
12341
+ process.on("SIGINT", handler);
12342
+ return () => process.off("SIGINT", handler);
12343
+ };
12344
+ DEFAULT_LOCAL_ADDRESS = "127.0.0.1";
12345
+ IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
12346
+ }
12347
+ });
12348
+
12349
+ // src/share/run.ts
12350
+ import os3 from "os";
12351
+ import { consola as consola35 } from "consola";
12352
+ function realHostAddresses() {
12353
+ let ip;
12354
+ for (const list of Object.values(os3.networkInterfaces())) {
12355
+ for (const addr of list ?? []) {
12356
+ if (addr.family === "IPv4" && !addr.internal) {
12357
+ ip = addr.address;
12358
+ break;
12359
+ }
12360
+ }
12361
+ if (ip) break;
12362
+ }
12363
+ const hn = os3.hostname();
12364
+ const mdnsName = hn.endsWith(".local") ? hn : `${hn}.local`;
12365
+ return { ...ip ? { ip } : {}, mdnsName };
12400
12366
  }
12401
- function renderApp(m, app, p) {
12402
- const targets = m.apps.filter((a) => a.app === app);
12403
- if (targets.length === 0) {
12404
- const known = [...new Set(m.apps.map((a) => a.app))];
12367
+ async function runShare(opts) {
12368
+ const log = opts.logger ?? {
12369
+ info: (m) => consola35.info(m),
12370
+ warn: (m) => consola35.warn(m)
12371
+ };
12372
+ const cfg = await readLaunchConfig(opts.name, opts.app, opts.monocerosHome);
12373
+ if (!cfg) {
12405
12374
  throw new Error(
12406
- `No app "${app}" in ${m.name} (have: ${known.join(", ") || "none"}).`
12375
+ `No launch config for '${opts.app}' (expected projects/${opts.app}/.monoceros/launch.json). Nothing to share.`
12407
12376
  );
12408
12377
  }
12409
- const out = [p.sectionLine(app)];
12410
- if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12411
- const pad = Math.max(...targets.map((t) => t.target.length));
12412
- for (const t of targets) {
12413
- out.push(
12414
- row(
12415
- p,
12416
- liveMarker(p, t.running),
12417
- t.target,
12418
- appTargetDetail(p, m.name, t),
12419
- pad,
12420
- 2
12421
- )
12378
+ const ported = cfg.configurations.filter(
12379
+ (t) => typeof t.port === "number"
12380
+ );
12381
+ if (ported.length === 0) {
12382
+ throw new Error(
12383
+ `No target in '${opts.app}' declares a port, so there is nothing to share. Add a \`port\` to a target in its launch.json.`
12422
12384
  );
12423
12385
  }
12424
- return out.join("\n");
12425
- }
12426
- function renderService(m, name, p) {
12427
- const s = m.services.find((svc) => svc.name === name);
12428
- if (!s) {
12429
- throw new Error(
12430
- `No service "${name}" in ${m.name} (have: ${m.services.map((x) => x.name).join(", ") || "none"}).`
12386
+ const ports = [...new Set(ported.map((t) => t.port))];
12387
+ const resolve = opts.resolve ?? resolveTunnelTarget;
12388
+ const base = await resolve({
12389
+ name: opts.name,
12390
+ target: String(ports[0]),
12391
+ ...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
12392
+ });
12393
+ const preflight = opts.preflight ?? preflightLocalPort;
12394
+ for (const port of ports) {
12395
+ await preflight({ port, address: SHARE_ADDRESS });
12396
+ }
12397
+ const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
12398
+ const handles = ports.map(
12399
+ (port) => dockerSpawn(
12400
+ buildDockerArgs({
12401
+ localAddress: SHARE_ADDRESS,
12402
+ localPort: port,
12403
+ internalPort: port,
12404
+ network: base.network,
12405
+ targetHost: base.targetHost
12406
+ })
12407
+ )
12408
+ );
12409
+ const { ip, mdnsName } = (opts.hostAddresses ?? realHostAddresses)();
12410
+ const host = ip ?? mdnsName ?? "<host-ip>";
12411
+ log.info(
12412
+ `Sharing ${opts.name}/${opts.app} on the local network (Ctrl+C to stop):`
12413
+ );
12414
+ for (const t of ported) {
12415
+ log.info(` ${cyan2(t.name)} http://${host}:${t.port}`);
12416
+ }
12417
+ if (mdnsName && ip) {
12418
+ log.info(
12419
+ dim(` also reachable as http://${mdnsName}:<port> where mDNS resolves`)
12431
12420
  );
12432
12421
  }
12433
- const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12434
- return [
12435
- p.sectionLine("Services"),
12436
- row(p, liveMarker(p, s.running), s.name, detail, s.name.length)
12437
- ].join("\n");
12422
+ const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
12423
+ let stopping = false;
12424
+ const uninstall = installSignalHandler(() => {
12425
+ if (stopping) return;
12426
+ stopping = true;
12427
+ for (const h of handles) h.kill("SIGTERM");
12428
+ });
12429
+ try {
12430
+ const codes = await Promise.all(handles.map((h) => h.exited));
12431
+ const bad = codes.find((c) => c !== 0 && c !== 130);
12432
+ return bad ?? 0;
12433
+ } finally {
12434
+ uninstall();
12435
+ }
12438
12436
  }
12439
- var init_status = __esm({
12440
- "src/status/index.ts"() {
12437
+ var SHARE_ADDRESS;
12438
+ var init_run4 = __esm({
12439
+ "src/share/run.ts"() {
12441
12440
  "use strict";
12442
- init_io();
12443
- init_paths();
12444
- init_global();
12445
- init_transform();
12446
12441
  init_launch_config();
12447
- init_catalog();
12448
- init_locate_running();
12449
- init_dynamic();
12442
+ init_resolve2();
12443
+ init_port_check2();
12444
+ init_run3();
12450
12445
  init_format();
12446
+ SHARE_ADDRESS = "0.0.0.0";
12451
12447
  }
12452
12448
  });
12453
12449
 
12454
- // src/commands/status.ts
12455
- import { defineCommand as defineCommand31 } from "citty";
12456
- var statusCommand;
12457
- var init_status2 = __esm({
12458
- "src/commands/status.ts"() {
12450
+ // src/commands/share.ts
12451
+ import { defineCommand as defineCommand29 } from "citty";
12452
+ import { consola as consola36 } from "consola";
12453
+ var shareCommand;
12454
+ var init_share = __esm({
12455
+ "src/commands/share.ts"() {
12459
12456
  "use strict";
12460
- init_status();
12461
- init_format();
12462
- init_dispatch();
12463
- statusCommand = defineCommand31({
12457
+ init_run4();
12458
+ shareCommand = defineCommand29({
12464
12459
  meta: {
12465
- name: "status",
12466
- group: "run",
12467
- description: "Show the dev-container at a glance: container, services and apps (up/down), the ports it routes, and what the yml built in. With an <app> (or a service name), narrow to just that."
12460
+ name: "share",
12461
+ group: "discovery",
12462
+ description: "Expose an app's ports to the local network (phone, tablet, other devices) so any device can open it - reached via the host's LAN IP (or `.local` name). Every target in the app's launch.json that declares a port is shared. Foreground: Ctrl+C stops sharing. See ADR 0030."
12468
12463
  },
12469
12464
  args: {
12470
12465
  name: {
@@ -12474,49 +12469,149 @@ var init_status2 = __esm({
12474
12469
  },
12475
12470
  app: {
12476
12471
  type: "positional",
12477
- description: "Narrow to one app (a path under projects/ with .monoceros/launch.json) or one compose service (e.g. postgres). Omit for the whole stack.",
12478
- required: false
12472
+ description: "App to share (a path under projects/ with .monoceros/launch.json). Every target with a `port` is exposed on the LAN.",
12473
+ required: true
12479
12474
  }
12480
12475
  },
12481
- run({ args }) {
12482
- const filter = typeof args.app === "string" ? args.app : void 0;
12483
- return dispatch(async () => {
12484
- const model = await gatherStatus(args.name);
12485
- const p = colorsFor(process.stdout);
12486
- let block;
12487
- if (filter) {
12488
- const isApp = model.apps.some((a) => a.app === filter);
12489
- block = isApp ? renderApp(model, filter, p) : renderService(model, filter, p);
12490
- } else {
12491
- block = renderStatus(model, p);
12492
- }
12493
- process.stdout.write(`
12494
- ${block}
12495
- `);
12496
- return 0;
12497
- });
12476
+ async run({ args }) {
12477
+ try {
12478
+ const exitCode = await runShare({ name: args.name, app: args.app });
12479
+ process.exit(exitCode);
12480
+ } catch (err) {
12481
+ consola36.error(err instanceof Error ? err.message : String(err));
12482
+ process.exit(1);
12483
+ }
12498
12484
  }
12499
12485
  });
12500
12486
  }
12501
12487
  });
12502
12488
 
12503
- // src/commands/stop.ts
12504
- import { defineCommand as defineCommand32 } from "citty";
12505
- import { consola as consola36 } from "consola";
12506
- var stopCommand;
12507
- var init_stop = __esm({
12508
- "src/commands/stop.ts"() {
12489
+ // src/commands/shell.ts
12490
+ import { defineCommand as defineCommand30 } from "citty";
12491
+ import { consola as consola37 } from "consola";
12492
+ var shellCommand;
12493
+ var init_shell2 = __esm({
12494
+ "src/commands/shell.ts"() {
12495
+ "use strict";
12496
+ init_paths();
12497
+ init_shell();
12498
+ shellCommand = defineCommand30({
12499
+ meta: {
12500
+ name: "shell",
12501
+ group: "run",
12502
+ description: "Open an interactive bash session inside the named dev-container."
12503
+ },
12504
+ args: {
12505
+ name: {
12506
+ type: "positional",
12507
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
12508
+ required: true
12509
+ }
12510
+ },
12511
+ async run({ args }) {
12512
+ try {
12513
+ const exitCode = await runShell({
12514
+ root: containerDir(args.name),
12515
+ name: args.name
12516
+ });
12517
+ process.exit(exitCode);
12518
+ } catch (err) {
12519
+ consola37.error(err instanceof Error ? err.message : String(err));
12520
+ process.exit(1);
12521
+ }
12522
+ }
12523
+ });
12524
+ }
12525
+ });
12526
+
12527
+ // src/commands/start.ts
12528
+ import { defineCommand as defineCommand31 } from "citty";
12529
+ import { consola as consola38 } from "consola";
12530
+ async function bringContainerUp(name, openTool) {
12531
+ {
12532
+ const args = { name, open: openTool };
12533
+ let needsProxy = false;
12534
+ let hostPort = 80;
12535
+ let deferred = [];
12536
+ let runtimeVersion;
12537
+ try {
12538
+ const parsed = await readConfig(containerConfigPath(args.name));
12539
+ runtimeVersion = parsed.config.runtimeVersion;
12540
+ if ((parsed.config.routing?.ports ?? []).length > 0) {
12541
+ needsProxy = true;
12542
+ const global = await readMonocerosConfig();
12543
+ hostPort = proxyHostPort(global);
12544
+ }
12545
+ deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
12546
+ } catch (err) {
12547
+ consola38.warn(
12548
+ `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
12549
+ );
12550
+ }
12551
+ if (needsProxy) {
12552
+ await preflightHostPort(hostPort);
12553
+ await ensureProxy({ hostPort });
12554
+ }
12555
+ const exitCode = await runStart({
12556
+ root: containerDir(args.name),
12557
+ silent: true,
12558
+ logger: { info: () => {
12559
+ } }
12560
+ });
12561
+ if (exitCode === 0 && runtimeSupportsBrowserBridge(runtimeVersion)) {
12562
+ spawnBridgeDaemon(containerDir(args.name));
12563
+ }
12564
+ if (exitCode === 0 && deferred.length > 0) {
12565
+ try {
12566
+ const deferExit = await startDeferredServices({
12567
+ root: containerDir(args.name),
12568
+ services: deferred,
12569
+ logger: consola38
12570
+ });
12571
+ if (deferExit !== 0) {
12572
+ consola38.warn(
12573
+ `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
12574
+ );
12575
+ }
12576
+ } catch (err) {
12577
+ consola38.warn(
12578
+ `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
12579
+ );
12580
+ }
12581
+ }
12582
+ if (exitCode === 0) {
12583
+ consola38.success(`Container '${args.name}' is up.`);
12584
+ }
12585
+ if (args.open && exitCode === 0) {
12586
+ try {
12587
+ await runOpen({ name: args.name, tool: args.open });
12588
+ } catch (err) {
12589
+ consola38.warn(err instanceof Error ? err.message : String(err));
12590
+ }
12591
+ }
12592
+ return exitCode;
12593
+ }
12594
+ }
12595
+ var startCommand;
12596
+ var init_start = __esm({
12597
+ "src/commands/start.ts"() {
12509
12598
  "use strict";
12599
+ init_global();
12600
+ init_io();
12510
12601
  init_paths();
12602
+ init_bridge_daemon();
12511
12603
  init_compose();
12604
+ init_catalog();
12605
+ init_open();
12512
12606
  init_proxy();
12607
+ init_port_check();
12513
12608
  init_app_control();
12514
12609
  init_dispatch();
12515
- stopCommand = defineCommand32({
12610
+ startCommand = defineCommand31({
12516
12611
  meta: {
12517
- name: "stop",
12612
+ name: "start",
12518
12613
  group: "run",
12519
- description: "Stop the compose services for the named dev-container. With an <app>, stop that long-running app inside it instead (kills its process group)."
12614
+ description: "Bring the named dev-container up. With an <app>, start that app inside it (per its projects/<app>/.monoceros/launch.json); the container is brought up first if needed."
12520
12615
  },
12521
12616
  args: {
12522
12617
  name: {
@@ -12526,430 +12621,506 @@ var init_stop = __esm({
12526
12621
  },
12527
12622
  app: {
12528
12623
  type: "positional",
12529
- description: "App to stop (a path under projects/ with .monoceros/launch.json). Omit to stop the container.",
12624
+ description: "App to start (a path under projects/ with .monoceros/launch.json). Omit to just bring the container up.",
12530
12625
  required: false
12531
12626
  },
12532
12627
  target: {
12533
12628
  type: "string",
12534
- description: `Which launch target to stop (defaults to the app's "default" target, or its only one).`
12629
+ description: `Which launch target to start (defaults to the app's "default" target, or its only one).`
12535
12630
  },
12536
- service: {
12631
+ open: {
12537
12632
  type: "string",
12538
- description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
12633
+ description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
12539
12634
  }
12540
12635
  },
12541
12636
  run({ args }) {
12542
12637
  if (typeof args.app === "string" && args.app.length > 0) {
12543
12638
  const app = args.app;
12544
12639
  const target = typeof args.target === "string" ? args.target : void 0;
12545
- return dispatch(() => runAppCtl(args.name, ctlArgs("stop", app, target)));
12546
- }
12547
- return dispatch(async () => {
12548
- const service = typeof args.service === "string" ? args.service : void 0;
12549
- const exit = await runStop({
12550
- root: containerDir(args.name),
12551
- ...service ? { service } : {},
12552
- logger: { info: () => {
12553
- } }
12554
- });
12555
- if (exit === 0) {
12556
- consola36.success(
12557
- service ? `Container '${args.name}' service '${service}' stopped.` : `Container '${args.name}' stopped.`
12558
- );
12559
- }
12560
- try {
12561
- await maybeStopProxy({
12562
- logger: { info: (msg) => consola36.info(msg) }
12563
- });
12564
- } catch (err) {
12565
- consola36.warn(
12566
- `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
12567
- );
12568
- }
12569
- return exit;
12570
- });
12640
+ return dispatch(async () => {
12641
+ if (!await findRunningContainer(args.name)) {
12642
+ const up = await bringContainerUp(args.name, void 0);
12643
+ if (up !== 0) return up;
12644
+ }
12645
+ return runAppCtl(args.name, ctlArgs("start", app, target));
12646
+ });
12647
+ }
12648
+ return dispatch(
12649
+ () => bringContainerUp(
12650
+ args.name,
12651
+ typeof args.open === "string" ? args.open : void 0
12652
+ )
12653
+ );
12571
12654
  }
12572
12655
  });
12573
12656
  }
12574
12657
  });
12575
12658
 
12576
- // src/tunnel/resolve.ts
12577
- import { existsSync as existsSync20 } from "fs";
12578
- import path32 from "path";
12579
- async function resolveTunnelTarget(opts) {
12580
- const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
12581
- if (!existsSync20(ymlPath)) {
12582
- throw new Error(
12583
- `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
12584
- );
12585
- }
12586
- const parsed = await readConfig(ymlPath);
12587
- const config = parsed.config;
12588
- const containerRoot = containerDir(opts.name, opts.monocerosHome);
12589
- if (!existsSync20(containerRoot)) {
12590
- throw new Error(
12591
- `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
12592
- );
12593
- }
12594
- const composePath = path32.join(containerRoot, ".devcontainer", "compose.yaml");
12595
- const isCompose = existsSync20(composePath);
12596
- const parsedTarget = parseTargetArg(opts.target, config);
12597
- const docker = opts.docker ?? defaultDockerExec;
12598
- if (isCompose) {
12599
- return resolveCompose2({
12600
- name: opts.name,
12601
- containerRoot,
12602
- parsedTarget
12603
- });
12604
- }
12605
- return resolveImageMode({
12606
- name: opts.name,
12607
- containerRoot,
12608
- parsedTarget,
12609
- config,
12610
- docker
12611
- });
12612
- }
12613
- function parseTargetArg(raw, config) {
12614
- const colon = raw.indexOf(":");
12615
- if (colon > 0) {
12616
- const name = raw.slice(0, colon);
12617
- const port = Number(raw.slice(colon + 1));
12618
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
12619
- throw new Error(
12620
- `Invalid target '${raw}'. Use <service>:<port> with a numeric port (1\u201365535), a bare port number, or a configured service name.`
12621
- );
12622
- }
12623
- findConfiguredService(config, name);
12624
- return { kind: "service", service: name, port };
12625
- }
12626
- const asNumber = Number(raw);
12627
- if (Number.isInteger(asNumber) && asNumber > 0 && asNumber < 65536) {
12628
- return { kind: "port", port: asNumber };
12629
- }
12630
- const match = findConfiguredService(config, raw);
12631
- if (match.port === void 0) {
12632
- throw new Error(
12633
- `Service '${raw}' declares no port, so tunnel can't know what to forward. Add \`port: <n>\` to the service in the yml and re-apply, or pass one explicitly: \`monoceros tunnel <name> ${raw}:<port>\`.`
12634
- );
12635
- }
12636
- return { kind: "service", service: raw, port: match.port };
12659
+ // src/status/index.ts
12660
+ function shortFeatureName2(ref) {
12661
+ const withoutTag = ref.replace(/:[^:/@]+$/, "");
12662
+ const idx = withoutTag.lastIndexOf("/");
12663
+ return idx >= 0 ? withoutTag.slice(idx + 1) : withoutTag;
12637
12664
  }
12638
- function findConfiguredService(config, name) {
12639
- const services = config.services.map(resolveService);
12640
- const match = services.find((s) => s.name === name);
12641
- if (!match) {
12642
- const names = services.map((s) => s.name);
12643
- const list = names.length > 0 ? names.join(", ") : "(none configured)";
12644
- throw new Error(
12645
- `Service '${name}' is not configured in this container's yml. Configured services: ${list}. Or pass a port number (e.g. \`monoceros tunnel <name> 8080\`).`
12646
- );
12647
- }
12648
- return match;
12665
+ function cols(line) {
12666
+ return line.split(" ");
12649
12667
  }
12650
- function resolveCompose2(args) {
12651
- const network = `${composeProjectName(args.containerRoot)}_default`;
12652
- if (args.parsedTarget.kind === "service") {
12653
- return {
12654
- network,
12655
- targetHost: args.parsedTarget.service,
12656
- internalPort: args.parsedTarget.port,
12657
- display: `${args.name}/${args.parsedTarget.service}:${args.parsedTarget.port}`
12658
- };
12659
- }
12660
- return {
12661
- network,
12662
- targetHost: "workspace",
12663
- internalPort: args.parsedTarget.port,
12664
- display: `${args.name}:${args.parsedTarget.port}`
12665
- };
12668
+ function nonEmptyLines(s) {
12669
+ return s.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
12666
12670
  }
12667
- async function resolveImageMode(args) {
12668
- if (args.parsedTarget.kind === "service") {
12669
- throw new Error(
12670
- `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.`
12671
- );
12672
- }
12673
- const ports = args.config.routing?.ports ?? [];
12674
- if (ports.length > 0) {
12675
- return {
12676
- network: PROXY_NETWORK_NAME,
12677
- targetHost: args.name,
12678
- internalPort: args.parsedTarget.port,
12679
- display: `${args.name}:${args.parsedTarget.port}`
12680
- };
12671
+ async function gatherStatus(name, opts = {}) {
12672
+ const docker = opts.docker ?? realDockerLookup;
12673
+ const root = containerDir(name, opts.home);
12674
+ let runtimeVersion;
12675
+ let languages = [];
12676
+ let features = [];
12677
+ let declaredServices = [];
12678
+ let declaredPorts = [];
12679
+ let configured = false;
12680
+ try {
12681
+ const parsed = await readConfig(containerConfigPath(name, opts.home));
12682
+ const createOpts = solutionConfigToCreateOptions(parsed.config);
12683
+ configured = true;
12684
+ runtimeVersion = createOpts.runtimeVersion;
12685
+ languages = createOpts.languages;
12686
+ features = Object.keys(createOpts.features ?? {}).map(shortFeatureName2);
12687
+ declaredServices = createOpts.services.map((s) => ({
12688
+ name: s.name,
12689
+ ...typeof s.port === "number" ? { port: s.port } : {}
12690
+ }));
12691
+ declaredPorts = createOpts.ports ?? [];
12692
+ } catch {
12681
12693
  }
12682
- const { network, ip } = await lookupContainerNetwork({
12683
- containerRoot: args.containerRoot,
12684
- docker: args.docker
12685
- });
12686
- return {
12687
- network,
12688
- targetHost: ip,
12689
- internalPort: args.parsedTarget.port,
12690
- display: `${args.name}:${args.parsedTarget.port}`
12694
+ let container = {
12695
+ exists: false,
12696
+ running: false,
12697
+ status: "",
12698
+ dockerName: ""
12691
12699
  };
12692
- }
12693
- async function lookupContainerNetwork(args) {
12694
- const psResult = await args.docker([
12700
+ let containerId = "";
12701
+ let composeProject = "";
12702
+ const cps = await docker([
12695
12703
  "ps",
12696
- "-q",
12704
+ "-a",
12705
+ "--no-trunc",
12697
12706
  "--filter",
12698
- `label=devcontainer.local_folder=${args.containerRoot}`
12707
+ `label=devcontainer.local_folder=${root}`,
12708
+ "--format",
12709
+ '{{.ID}} {{.State}} {{.Status}} {{.Names}} {{.Label "com.docker.compose.project"}}'
12699
12710
  ]);
12700
- if (psResult.exitCode !== 0) {
12701
- throw new Error(
12702
- `docker ps failed: ${psResult.stderr.trim() || `exit ${psResult.exitCode}`}`
12703
- );
12711
+ const row2 = nonEmptyLines(cps.stdout)[0];
12712
+ if (row2) {
12713
+ const [id, state, status, names, project] = cols(row2);
12714
+ containerId = id ?? "";
12715
+ composeProject = project ?? "";
12716
+ container = {
12717
+ exists: true,
12718
+ running: state === "running",
12719
+ status: status ?? "",
12720
+ dockerName: names ?? ""
12721
+ };
12704
12722
  }
12705
- const containerId = psResult.stdout.trim().split("\n")[0]?.trim();
12706
- if (!containerId) {
12707
- throw new Error(
12708
- `No running container for '${args.containerRoot}'. Start it with \`monoceros start <name>\` (or open a shell with \`monoceros shell <name>\`) and retry.`
12709
- );
12723
+ const services = [];
12724
+ if (declaredServices.length > 0) {
12725
+ const live = /* @__PURE__ */ new Map();
12726
+ if (composeProject) {
12727
+ const sps = await docker([
12728
+ "ps",
12729
+ "-a",
12730
+ "--filter",
12731
+ `label=com.docker.compose.project=${composeProject}`,
12732
+ "--format",
12733
+ '{{.Label "com.docker.compose.service"}} {{.State}} {{.Status}}'
12734
+ ]);
12735
+ for (const l of nonEmptyLines(sps.stdout)) {
12736
+ const [svc, state, status] = cols(l);
12737
+ if (svc)
12738
+ live.set(svc, { running: state === "running", status: status ?? "" });
12739
+ }
12740
+ }
12741
+ for (const s of declaredServices) {
12742
+ const m = live.get(s.name);
12743
+ services.push({
12744
+ name: s.name,
12745
+ running: m?.running ?? false,
12746
+ status: m?.status ?? "not created",
12747
+ ...typeof s.port === "number" ? { port: s.port } : {}
12748
+ });
12749
+ }
12750
+ }
12751
+ const apps = [];
12752
+ for (const app of await listApps(name, opts.home)) {
12753
+ const cfg = await readLaunchConfig(name, app, opts.home);
12754
+ if (!cfg) continue;
12755
+ for (const t of cfg.configurations) {
12756
+ apps.push({
12757
+ app,
12758
+ target: t.name,
12759
+ port: typeof t.port === "number" ? t.port : null,
12760
+ default: t.default === true,
12761
+ running: void 0,
12762
+ pid: null
12763
+ });
12764
+ }
12765
+ }
12766
+ const appsSupported = runtimeSupportsAppStatus(runtimeVersion);
12767
+ let appStateKnown = container.running && appsSupported && containerId !== "";
12768
+ let appStateNote;
12769
+ if (appStateKnown) {
12770
+ const res = await docker([
12771
+ "exec",
12772
+ containerId,
12773
+ "monoceros-ctl",
12774
+ "list",
12775
+ "--json"
12776
+ ]);
12777
+ if (res.exitCode === 0) {
12778
+ const byKey = /* @__PURE__ */ new Map();
12779
+ for (const l of nonEmptyLines(res.stdout)) {
12780
+ try {
12781
+ const o = JSON.parse(l);
12782
+ byKey.set(`${o.app}\0${o.target}`, {
12783
+ running: !!o.running,
12784
+ pid: o.pid ?? null
12785
+ });
12786
+ } catch {
12787
+ }
12788
+ }
12789
+ for (const a of apps) {
12790
+ const o = byKey.get(`${a.app}\0${a.target}`);
12791
+ if (o) {
12792
+ a.running = o.running;
12793
+ a.pid = o.pid;
12794
+ } else {
12795
+ a.running = false;
12796
+ }
12797
+ }
12798
+ } else {
12799
+ appStateKnown = false;
12800
+ appStateNote = `could not read run state from the runner - rebuild the runtime image, then \`monoceros upgrade ${name}\``;
12801
+ }
12710
12802
  }
12711
- const inspect = await args.docker([
12712
- "inspect",
12713
- "--format",
12714
- "{{json .NetworkSettings.Networks}}",
12715
- containerId
12716
- ]);
12717
- if (inspect.exitCode !== 0) {
12718
- throw new Error(
12719
- `docker inspect failed: ${inspect.stderr.trim() || `exit ${inspect.exitCode}`}`
12720
- );
12803
+ if (apps.length > 0 && !appStateKnown && !appStateNote) {
12804
+ appStateNote = !container.running ? `start the container to see run state (\`monoceros start ${name}\`)` : `run state needs runtime 1.6.0+ (this one: ${runtimeVersion ?? "unpinned"})`;
12721
12805
  }
12722
- let networks = null;
12806
+ let hostPort = 80;
12723
12807
  try {
12724
- networks = JSON.parse(inspect.stdout);
12725
- } catch {
12726
- throw new Error(
12727
- `Unexpected docker inspect output: ${inspect.stdout.slice(0, 200)}`
12808
+ hostPort = proxyHostPort(
12809
+ await readMonocerosConfig({
12810
+ ...opts.home ? { monocerosHome: opts.home } : {}
12811
+ })
12728
12812
  );
12813
+ } catch {
12729
12814
  }
12730
- if (!networks) {
12731
- throw new Error(
12732
- `Container ${containerId} reports no networks. Restart it and retry.`
12815
+ const ports = proxyUrlsFor(name, declaredPorts, hostPort);
12816
+ return {
12817
+ name,
12818
+ configured,
12819
+ container,
12820
+ services,
12821
+ apps,
12822
+ appStateKnown,
12823
+ ...appStateNote ? { appStateNote } : {},
12824
+ ports,
12825
+ builtIn: { languages, features }
12826
+ };
12827
+ }
12828
+ function row(p, marker, name, detail, pad, indent = 2) {
12829
+ const gap = Math.max(2, pad - name.length + 2);
12830
+ return `${" ".repeat(indent)}${marker} ${p.cyan(name)}${" ".repeat(gap)}${detail}`;
12831
+ }
12832
+ function liveMarker(p, running) {
12833
+ if (running === void 0) return " ";
12834
+ return running ? p.green("\u2713") : p.dim("\xB7");
12835
+ }
12836
+ function renderContainer(p, m) {
12837
+ const out = [p.sectionLine(m.name)];
12838
+ if (!m.container.exists) {
12839
+ out.push(
12840
+ row(
12841
+ p,
12842
+ p.dim("\xB7"),
12843
+ "(not created)",
12844
+ p.dim(`run \`monoceros apply ${m.name}\``),
12845
+ 14
12846
+ )
12733
12847
  );
12848
+ return out;
12734
12849
  }
12735
- for (const [name, settings] of Object.entries(networks)) {
12736
- if (settings.IPAddress && settings.IPAddress.length > 0) {
12737
- return { network: name, ip: settings.IPAddress };
12738
- }
12739
- }
12740
- throw new Error(
12741
- `Container ${containerId} has no network with a reachable IP. Restart it and retry.`
12850
+ const detail = m.container.running ? p.dim(m.container.status || "running") : p.dim(m.container.status || "stopped");
12851
+ out.push(
12852
+ row(
12853
+ p,
12854
+ liveMarker(p, m.container.running),
12855
+ m.container.dockerName,
12856
+ detail,
12857
+ 14
12858
+ )
12742
12859
  );
12860
+ return out;
12743
12861
  }
12744
- var init_resolve2 = __esm({
12745
- "src/tunnel/resolve.ts"() {
12746
- "use strict";
12747
- init_paths();
12748
- init_io();
12749
- init_compose();
12750
- init_catalog();
12751
- init_proxy();
12862
+ function renderServices(p, m) {
12863
+ if (m.services.length === 0) return [];
12864
+ const pad = Math.max(...m.services.map((s) => s.name.length));
12865
+ const out = ["", p.sectionLine("Services")];
12866
+ for (const s of m.services) {
12867
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12868
+ out.push(row(p, liveMarker(p, s.running), s.name, detail, pad));
12752
12869
  }
12753
- });
12754
-
12755
- // src/tunnel/port-check.ts
12756
- import { Socket as Socket2 } from "net";
12757
- async function preflightLocalPort(opts) {
12758
- const probe = opts.probe ?? realPortProbe2;
12759
- const result = await probe(opts.port, opts.address);
12760
- if (result.ok) return;
12761
- throw new Error(formatLocalPortHeldError(opts.port, opts.address, result));
12870
+ return out;
12762
12871
  }
12763
- function formatLocalPortHeldError(port, address, result) {
12764
- const lines = [];
12765
- if (result.code === "EADDRINUSE") {
12766
- lines.push(`Local port ${port} on ${address} is already in use.`);
12767
- lines.push("");
12768
- lines.push("Identify the holder, then either stop it or pick a different");
12769
- lines.push("port for the tunnel:");
12770
- lines.push("");
12771
- lines.push(` sudo lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
12772
- lines.push(` # or: sudo ss -tlnp | grep ":${port}\\b"`);
12773
- lines.push("");
12774
- lines.push("Re-run with an explicit local port:");
12775
- lines.push(` monoceros tunnel \u2026 --local-port=${port + 1}`);
12776
- } else {
12777
- lines.push(
12778
- `Cannot probe local port ${port} on ${address}: ${result.message}`
12779
- );
12780
- lines.push("");
12781
- lines.push(
12782
- "Most likely the host network stack (firewall, namespace) is interfering."
12872
+ function appTargetDetail(p, name, t) {
12873
+ const bits = [];
12874
+ if (t.running === true) {
12875
+ bits.push(
12876
+ t.port !== null ? p.dim(`http://${name}-${t.port}.localhost`) : p.dim("running")
12783
12877
  );
12784
- lines.push("Try a different local port via `--local-port=<n>`.");
12878
+ if (t.pid !== null) bits.push(p.dim(`pid ${t.pid}`));
12879
+ } else if (t.running === false) {
12880
+ bits.push(p.dim("stopped"));
12785
12881
  }
12786
- return lines.join("\n");
12882
+ if (t.default) bits.push(p.dim("(default)"));
12883
+ return bits.join(" ");
12787
12884
  }
12788
- var CONNECT_TIMEOUT_MS2, realPortProbe2;
12789
- var init_port_check2 = __esm({
12790
- "src/tunnel/port-check.ts"() {
12791
- "use strict";
12792
- CONNECT_TIMEOUT_MS2 = 750;
12793
- realPortProbe2 = (port, address) => {
12794
- const probeHost = address === "0.0.0.0" ? "127.0.0.1" : address;
12795
- return new Promise((resolve) => {
12796
- const socket = new Socket2();
12797
- let settled = false;
12798
- const settle = (result) => {
12799
- if (settled) return;
12800
- settled = true;
12801
- socket.destroy();
12802
- resolve(result);
12803
- };
12804
- socket.setTimeout(CONNECT_TIMEOUT_MS2);
12805
- socket.once("connect", () => {
12806
- settle({
12807
- ok: false,
12808
- code: "EADDRINUSE",
12809
- message: `another process is listening on ${port}`
12810
- });
12811
- });
12812
- socket.once("timeout", () => {
12813
- settle({ ok: true });
12814
- });
12815
- socket.once("error", (err) => {
12816
- const code = err.code ?? "UNKNOWN";
12817
- if (code === "ECONNREFUSED") {
12818
- settle({ ok: true });
12819
- } else {
12820
- settle({ ok: false, code, message: err.message });
12821
- }
12822
- });
12823
- socket.connect(port, probeHost);
12824
- });
12825
- };
12885
+ function renderApps(p, m) {
12886
+ if (m.apps.length === 0) return [];
12887
+ const out = ["", p.sectionLine("Apps")];
12888
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12889
+ const byApp = /* @__PURE__ */ new Map();
12890
+ for (const a of m.apps) {
12891
+ const list = byApp.get(a.app) ?? [];
12892
+ list.push(a);
12893
+ byApp.set(a.app, list);
12826
12894
  }
12827
- });
12828
-
12829
- // src/tunnel/run.ts
12830
- import { spawn as spawn14 } from "child_process";
12831
- import { consola as consola37 } from "consola";
12832
- function signalNumber(signal) {
12833
- switch (signal) {
12834
- case "SIGINT":
12835
- return 2;
12836
- case "SIGTERM":
12837
- return 15;
12838
- default:
12839
- return 1;
12895
+ for (const [app, targets] of byApp) {
12896
+ out.push(` ${p.cyan(app)}`);
12897
+ const pad = Math.max(...targets.map((t) => t.target.length));
12898
+ for (const t of targets) {
12899
+ out.push(
12900
+ row(
12901
+ p,
12902
+ liveMarker(p, t.running),
12903
+ t.target,
12904
+ appTargetDetail(p, m.name, t),
12905
+ pad,
12906
+ 4
12907
+ )
12908
+ );
12909
+ }
12840
12910
  }
12841
- }
12842
- async function runTunnel(opts) {
12843
- const log = opts.logger ?? {
12844
- info: (m) => consola37.info(m),
12845
- warn: (m) => consola37.warn(m)
12846
- };
12847
- const resolve = opts.resolve ?? resolveTunnelTarget;
12848
- const resolveArgs3 = {
12849
- name: opts.name,
12850
- target: opts.target,
12851
- ...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
12852
- };
12853
- const resolved = await resolve(resolveArgs3);
12854
- const localPort = opts.localPort ?? resolved.internalPort;
12855
- const localAddress = opts.localAddress ?? DEFAULT_LOCAL_ADDRESS;
12856
- validateLocalAddress(localAddress);
12857
- const preflight = opts.preflight ?? preflightLocalPort;
12858
- await preflight({ port: localPort, address: localAddress });
12859
- const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
12860
- const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
12861
- const dockerArgs = buildDockerArgs({
12862
- localAddress,
12863
- localPort,
12864
- internalPort: resolved.internalPort,
12865
- network: resolved.network,
12866
- targetHost: resolved.targetHost
12867
- });
12868
- log.info(
12869
- `Tunnel: ${localAddress}:${localPort} \u2192 ${resolved.display}:${resolved.internalPort} (Ctrl+C to stop)`
12870
- );
12871
- const handle = dockerSpawn(dockerArgs);
12872
- const uninstall = installSignalHandler(() => {
12873
- });
12874
- try {
12875
- const exitCode = await handle.exited;
12876
- if (exitCode === 130) return 0;
12877
- return exitCode;
12878
- } finally {
12879
- uninstall();
12911
+ return out;
12912
+ }
12913
+ function renderPorts(p, m) {
12914
+ if (m.ports.length === 0) return [];
12915
+ const pad = Math.max(...m.ports.map((r) => String(r.port).length));
12916
+ const out = ["", p.sectionLine("Ports")];
12917
+ for (const r of m.ports) {
12918
+ const hosts = r.isDefault ? `${m.name}.localhost \xB7 ${m.name}-${r.port}.localhost` : `${m.name}-${r.port}.localhost`;
12919
+ out.push(row(p, " ", String(r.port), p.dim(hosts), pad));
12880
12920
  }
12921
+ return out;
12881
12922
  }
12882
- function buildDockerArgs(input) {
12923
+ function renderBuiltIn(p, m) {
12924
+ const lines = [];
12925
+ if (m.builtIn.languages.length > 0)
12926
+ lines.push({ label: "Languages", values: m.builtIn.languages });
12927
+ if (m.builtIn.features.length > 0)
12928
+ lines.push({ label: "Features", values: m.builtIn.features });
12929
+ if (lines.length === 0) return [];
12930
+ const labelWidth = Math.max(...lines.map((l) => l.label.length));
12931
+ const out = ["", `${p.sectionLine("Built in")} ${p.dim("from the yml")}`];
12932
+ for (const l of lines) {
12933
+ out.push(` ${l.label.padEnd(labelWidth)} ${p.cyan(l.values.join(", "))}`);
12934
+ }
12935
+ return out;
12936
+ }
12937
+ function renderStatus(m, p) {
12883
12938
  return [
12884
- "run",
12885
- "--rm",
12886
- "-i",
12887
- `--network=${input.network}`,
12888
- "-p",
12889
- `${input.localAddress}:${input.localPort}:${input.internalPort}`,
12890
- SOCAT_IMAGE,
12891
- `TCP-LISTEN:${input.internalPort},fork,reuseaddr`,
12892
- `TCP:${input.targetHost}:${input.internalPort}`
12893
- ];
12939
+ ...renderContainer(p, m),
12940
+ ...renderServices(p, m),
12941
+ ...renderApps(p, m),
12942
+ ...renderPorts(p, m),
12943
+ ...renderBuiltIn(p, m)
12944
+ ].join("\n");
12894
12945
  }
12895
- function validateLocalAddress(addr) {
12896
- if (addr === "localhost") return;
12897
- if (IPV4_RE.test(addr)) {
12898
- for (const part of addr.split(".")) {
12899
- const n = Number(part);
12900
- if (n < 0 || n > 255) {
12901
- throw new Error(
12902
- `Invalid --local-address '${addr}': each dotted-quad octet must be 0-255.`
12903
- );
12904
- }
12905
- }
12906
- return;
12946
+ function renderApp(m, app, p) {
12947
+ const targets = m.apps.filter((a) => a.app === app);
12948
+ if (targets.length === 0) {
12949
+ const known = [...new Set(m.apps.map((a) => a.app))];
12950
+ throw new Error(
12951
+ `No app "${app}" in ${m.name} (have: ${known.join(", ") || "none"}).`
12952
+ );
12907
12953
  }
12908
- throw new Error(
12909
- `Invalid --local-address '${addr}'. Use 127.0.0.1 (default), 0.0.0.0, or a specific IPv4 address.`
12910
- );
12954
+ const out = [p.sectionLine(app)];
12955
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12956
+ const pad = Math.max(...targets.map((t) => t.target.length));
12957
+ for (const t of targets) {
12958
+ out.push(
12959
+ row(
12960
+ p,
12961
+ liveMarker(p, t.running),
12962
+ t.target,
12963
+ appTargetDetail(p, m.name, t),
12964
+ pad,
12965
+ 2
12966
+ )
12967
+ );
12968
+ }
12969
+ return out.join("\n");
12911
12970
  }
12912
- var SOCAT_IMAGE, defaultDockerSpawn, installSigintDefault, DEFAULT_LOCAL_ADDRESS, IPV4_RE;
12913
- var init_run3 = __esm({
12914
- "src/tunnel/run.ts"() {
12971
+ function renderService(m, name, p) {
12972
+ const s = m.services.find((svc) => svc.name === name);
12973
+ if (!s) {
12974
+ throw new Error(
12975
+ `No service "${name}" in ${m.name} (have: ${m.services.map((x) => x.name).join(", ") || "none"}).`
12976
+ );
12977
+ }
12978
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12979
+ return [
12980
+ p.sectionLine("Services"),
12981
+ row(p, liveMarker(p, s.running), s.name, detail, s.name.length)
12982
+ ].join("\n");
12983
+ }
12984
+ var init_status = __esm({
12985
+ "src/status/index.ts"() {
12915
12986
  "use strict";
12916
- init_resolve2();
12917
- init_port_check2();
12918
- SOCAT_IMAGE = "alpine/socat:1.8.0.3";
12919
- defaultDockerSpawn = (args) => {
12920
- const child = spawn14("docker", args, {
12921
- stdio: "inherit"
12922
- });
12923
- const exited = new Promise((resolve, reject) => {
12924
- child.on("error", reject);
12925
- child.on("exit", (code, signal) => {
12926
- if (typeof code === "number") resolve(code);
12927
- else if (signal) resolve(128 + signalNumber(signal));
12928
- else resolve(0);
12987
+ init_io();
12988
+ init_paths();
12989
+ init_global();
12990
+ init_transform();
12991
+ init_launch_config();
12992
+ init_catalog();
12993
+ init_locate_running();
12994
+ init_dynamic();
12995
+ init_format();
12996
+ }
12997
+ });
12998
+
12999
+ // src/commands/status.ts
13000
+ import { defineCommand as defineCommand32 } from "citty";
13001
+ var statusCommand;
13002
+ var init_status2 = __esm({
13003
+ "src/commands/status.ts"() {
13004
+ "use strict";
13005
+ init_status();
13006
+ init_format();
13007
+ init_dispatch();
13008
+ statusCommand = defineCommand32({
13009
+ meta: {
13010
+ name: "status",
13011
+ group: "run",
13012
+ description: "Show the dev-container at a glance: container, services and apps (up/down), the ports it routes, and what the yml built in. With an <app> (or a service name), narrow to just that."
13013
+ },
13014
+ args: {
13015
+ name: {
13016
+ type: "positional",
13017
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
13018
+ required: true
13019
+ },
13020
+ app: {
13021
+ type: "positional",
13022
+ description: "Narrow to one app (a path under projects/ with .monoceros/launch.json) or one compose service (e.g. postgres). Omit for the whole stack.",
13023
+ required: false
13024
+ }
13025
+ },
13026
+ run({ args }) {
13027
+ const filter = typeof args.app === "string" ? args.app : void 0;
13028
+ return dispatch(async () => {
13029
+ const model = await gatherStatus(args.name);
13030
+ const p = colorsFor(process.stdout);
13031
+ let block;
13032
+ if (filter) {
13033
+ const isApp = model.apps.some((a) => a.app === filter);
13034
+ block = isApp ? renderApp(model, filter, p) : renderService(model, filter, p);
13035
+ } else {
13036
+ block = renderStatus(model, p);
13037
+ }
13038
+ process.stdout.write(`
13039
+ ${block}
13040
+ `);
13041
+ return 0;
12929
13042
  });
12930
- });
12931
- return {
12932
- exited,
12933
- kill: (signal) => {
13043
+ }
13044
+ });
13045
+ }
13046
+ });
13047
+
13048
+ // src/commands/stop.ts
13049
+ import { defineCommand as defineCommand33 } from "citty";
13050
+ import { consola as consola39 } from "consola";
13051
+ var stopCommand;
13052
+ var init_stop = __esm({
13053
+ "src/commands/stop.ts"() {
13054
+ "use strict";
13055
+ init_paths();
13056
+ init_compose();
13057
+ init_proxy();
13058
+ init_app_control();
13059
+ init_dispatch();
13060
+ stopCommand = defineCommand33({
13061
+ meta: {
13062
+ name: "stop",
13063
+ group: "run",
13064
+ description: "Stop the compose services for the named dev-container. With an <app>, stop that long-running app inside it instead (kills its process group)."
13065
+ },
13066
+ args: {
13067
+ name: {
13068
+ type: "positional",
13069
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
13070
+ required: true
13071
+ },
13072
+ app: {
13073
+ type: "positional",
13074
+ description: "App to stop (a path under projects/ with .monoceros/launch.json). Omit to stop the container.",
13075
+ required: false
13076
+ },
13077
+ target: {
13078
+ type: "string",
13079
+ description: `Which launch target to stop (defaults to the app's "default" target, or its only one).`
13080
+ },
13081
+ service: {
13082
+ type: "string",
13083
+ description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
13084
+ }
13085
+ },
13086
+ run({ args }) {
13087
+ if (typeof args.app === "string" && args.app.length > 0) {
13088
+ const app = args.app;
13089
+ const target = typeof args.target === "string" ? args.target : void 0;
13090
+ return dispatch(() => runAppCtl(args.name, ctlArgs("stop", app, target)));
13091
+ }
13092
+ return dispatch(async () => {
13093
+ const service = typeof args.service === "string" ? args.service : void 0;
13094
+ const exit = await runStop({
13095
+ root: containerDir(args.name),
13096
+ ...service ? { service } : {},
13097
+ logger: { info: () => {
13098
+ } }
13099
+ });
13100
+ if (exit === 0) {
13101
+ consola39.success(
13102
+ service ? `Container '${args.name}' service '${service}' stopped.` : `Container '${args.name}' stopped.`
13103
+ );
13104
+ }
12934
13105
  try {
12935
- child.kill(signal);
12936
- } catch {
13106
+ await maybeStopProxy({
13107
+ logger: { info: (msg) => consola39.info(msg) }
13108
+ });
13109
+ } catch (err) {
13110
+ consola39.warn(
13111
+ `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
13112
+ );
12937
13113
  }
12938
- }
12939
- };
12940
- };
12941
- installSigintDefault = (handler) => {
12942
- process.on("SIGINT", handler);
12943
- return () => process.off("SIGINT", handler);
12944
- };
12945
- DEFAULT_LOCAL_ADDRESS = "127.0.0.1";
12946
- IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
13114
+ return exit;
13115
+ });
13116
+ }
13117
+ });
12947
13118
  }
12948
13119
  });
12949
13120
 
12950
13121
  // src/commands/tunnel.ts
12951
- import { defineCommand as defineCommand33 } from "citty";
12952
- import { consola as consola38 } from "consola";
13122
+ import { defineCommand as defineCommand34 } from "citty";
13123
+ import { consola as consola40 } from "consola";
12953
13124
  function parseLocalPort(raw) {
12954
13125
  if (raw === void 0) return void 0;
12955
13126
  const n = Number(raw);
@@ -12965,7 +13136,7 @@ var init_tunnel = __esm({
12965
13136
  "src/commands/tunnel.ts"() {
12966
13137
  "use strict";
12967
13138
  init_run3();
12968
- tunnelCommand = defineCommand33({
13139
+ tunnelCommand = defineCommand34({
12969
13140
  meta: {
12970
13141
  name: "tunnel",
12971
13142
  group: "discovery",
@@ -13002,7 +13173,7 @@ var init_tunnel = __esm({
13002
13173
  });
13003
13174
  process.exit(exitCode);
13004
13175
  } catch (err) {
13005
- consola38.error(err instanceof Error ? err.message : String(err));
13176
+ consola40.error(err instanceof Error ? err.message : String(err));
13006
13177
  process.exit(1);
13007
13178
  }
13008
13179
  }
@@ -13073,7 +13244,7 @@ var init_prune = __esm({
13073
13244
 
13074
13245
  // src/upgrade/index.ts
13075
13246
  import { existsSync as existsSync21, promises as fs20 } from "fs";
13076
- import { consola as consola39 } from "consola";
13247
+ import { consola as consola41 } from "consola";
13077
13248
  async function fetchRuntimeVersions() {
13078
13249
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
13079
13250
  const tokenRes = await fetch(tokenUrl);
@@ -13111,7 +13282,7 @@ ${yml}`;
13111
13282
  }
13112
13283
  async function runUpgrade(opts) {
13113
13284
  const home = opts.monocerosHome ?? monocerosHome();
13114
- const logger = opts.logger ?? consola39;
13285
+ const logger = opts.logger ?? consola41;
13115
13286
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
13116
13287
  if (opts.list) {
13117
13288
  const versions = await fetchVersions();
@@ -13257,7 +13428,7 @@ var init_upgrade = __esm({
13257
13428
  });
13258
13429
 
13259
13430
  // src/commands/upgrade.ts
13260
- import { defineCommand as defineCommand34 } from "citty";
13431
+ import { defineCommand as defineCommand35 } from "citty";
13261
13432
  var upgradeCommand;
13262
13433
  var init_upgrade2 = __esm({
13263
13434
  "src/commands/upgrade.ts"() {
@@ -13265,7 +13436,7 @@ var init_upgrade2 = __esm({
13265
13436
  init_upgrade();
13266
13437
  init_version();
13267
13438
  init_dispatch();
13268
- upgradeCommand = defineCommand34({
13439
+ upgradeCommand = defineCommand35({
13269
13440
  meta: {
13270
13441
  name: "upgrade",
13271
13442
  group: "lifecycle",
@@ -13307,7 +13478,7 @@ var main_exports = {};
13307
13478
  __export(main_exports, {
13308
13479
  main: () => main
13309
13480
  });
13310
- import { defineCommand as defineCommand35 } from "citty";
13481
+ import { defineCommand as defineCommand36 } from "citty";
13311
13482
  var main;
13312
13483
  var init_main = __esm({
13313
13484
  "src/main.ts"() {
@@ -13340,6 +13511,7 @@ var init_main = __esm({
13340
13511
  init_remove_repo();
13341
13512
  init_remove_service();
13342
13513
  init_run2();
13514
+ init_share();
13343
13515
  init_shell2();
13344
13516
  init_start();
13345
13517
  init_status2();
@@ -13347,7 +13519,7 @@ var init_main = __esm({
13347
13519
  init_tunnel();
13348
13520
  init_upgrade2();
13349
13521
  init_version();
13350
- main = defineCommand35({
13522
+ main = defineCommand36({
13351
13523
  meta: {
13352
13524
  name: "monoceros",
13353
13525
  version: CLI_VERSION,
@@ -13384,6 +13556,7 @@ var init_main = __esm({
13384
13556
  "remove-port": removePortCommand,
13385
13557
  port: portCommand,
13386
13558
  tunnel: tunnelCommand,
13559
+ share: shareCommand,
13387
13560
  completion: completionCommand,
13388
13561
  __complete: __completeCommand,
13389
13562
  "__update-check": __updateCheckCommand,