@getmonoceros/workbench 1.36.3 → 1.36.5

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 CHANGED
@@ -2132,58 +2132,89 @@ async function preflightHostPort(hostPort, opts = {}) {
2132
2132
  const probe = opts.portProbe ?? realPortProbe;
2133
2133
  const result = await probe(hostPort);
2134
2134
  if (result.ok) return;
2135
- throw new Error(
2136
- formatHostPortHeldError(hostPort, result.code, result.message)
2137
- );
2138
- }
2139
- function formatHostPortHeldError(hostPort, code, systemMessage) {
2140
- const isInUse = code === "EADDRINUSE";
2141
- const lines = [];
2142
- if (isInUse) {
2143
- lines.push(`Host port ${hostPort} is already in use by another process.`);
2144
- lines.push("");
2145
- lines.push(`Monoceros needs that port for its Traefik proxy (the thing`);
2146
- lines.push(`that routes <name>.localhost / <name>-<port>.localhost to`);
2147
- lines.push(`your dev-container). Two ways out:`);
2148
- lines.push("");
2149
- lines.push(" 1) Recommended: free the port.");
2150
- lines.push(" Identify the process holding it:");
2151
- lines.push(` sudo lsof -iTCP:${hostPort} -sTCP:LISTEN -n -P`);
2152
- lines.push(` # or: sudo ss -tlnp | grep ":${hostPort}\\b"`);
2153
- lines.push(" Then stop or reconfigure that service.");
2154
- lines.push("");
2155
- lines.push(" 2) Move Monoceros off port 80. Edit (or create)");
2156
- lines.push(" ~/.monoceros/monoceros-config.yml and add:");
2157
- lines.push("");
2158
- lines.push(" schemaVersion: 1");
2159
- lines.push(" routing:");
2160
- lines.push(" hostPort: 8080 # any free port");
2161
- lines.push("");
2162
- lines.push(" URLs will become http://<name>.localhost:8080/.");
2163
- lines.push("");
2164
- lines.push(`Aborting \u2014 re-run after the conflict is resolved.`);
2165
- } else {
2166
- lines.push(`Cannot reach host port ${hostPort}: ${systemMessage}`);
2167
- lines.push("");
2168
- lines.push(`This is not the typical "port already in use" case \u2014`);
2169
- lines.push(`Monoceros's pre-flight uses a TCP-connect probe (not a`);
2170
- lines.push(`bind), so EACCES / privileged-port errors normally don't`);
2171
- lines.push(`appear here. Most likely something on your host network`);
2172
- lines.push(`stack (firewall, network namespace, \u2026) is interfering with`);
2173
- lines.push(`loopback connects.`);
2174
- lines.push("");
2175
- lines.push("Workaround: move Monoceros off this port by setting");
2176
- lines.push("`routing.hostPort` in ~/.monoceros/monoceros-config.yml.");
2177
- lines.push("");
2178
- lines.push(`Aborting \u2014 re-run after the issue is resolved.`);
2135
+ if (result.code !== "EADDRINUSE") {
2136
+ throw new Error(
2137
+ formatHostPortHeldError(hostPort, result.code, result.message)
2138
+ );
2179
2139
  }
2180
- return lines.join("\n");
2140
+ const live = await containersPublishing(docker, hostPort);
2141
+ if (live.length > 0) {
2142
+ throw new Error(formatLiveContainerError(hostPort, live));
2143
+ }
2144
+ throw new Error(formatLeftoverHolderError(hostPort));
2145
+ }
2146
+ async function containersPublishing(docker, hostPort) {
2147
+ const ps = await docker([
2148
+ "ps",
2149
+ "--filter",
2150
+ `publish=${hostPort}`,
2151
+ "--format",
2152
+ "{{.Names}} ({{.Image}})"
2153
+ ]);
2154
+ if (ps.exitCode !== 0) return [];
2155
+ return ps.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2156
+ }
2157
+ function hostPortFallbackLines(hostPort) {
2158
+ return [
2159
+ dim("Or set a different proxy port in ") + cyan2("~/.monoceros/monoceros-config.yml") + dim(":"),
2160
+ " " + cyan2("schemaVersion: 1"),
2161
+ " " + cyan2("routing:"),
2162
+ " " + cyan2(" hostPort: 8080"),
2163
+ "",
2164
+ dim(
2165
+ `Aborting. Re-run once port ${hostPort} is free or a different port is set.`
2166
+ )
2167
+ ];
2168
+ }
2169
+ function formatLiveContainerError(hostPort, containers) {
2170
+ const firstName = containers[0].split(" ")[0];
2171
+ return [
2172
+ `Host port ${hostPort} is in use: it is published by a running container.`,
2173
+ "",
2174
+ dim("Held by:"),
2175
+ ...containers.map((c) => " " + cyan2(c)),
2176
+ "",
2177
+ dim("Stop or re-map it, then re-run:"),
2178
+ " " + cyan2(`docker stop ${firstName}`),
2179
+ "",
2180
+ ...hostPortFallbackLines(hostPort)
2181
+ ].join("\n");
2182
+ }
2183
+ function formatLeftoverHolderError(hostPort) {
2184
+ return [
2185
+ `Host port ${hostPort} is in use, but no running container publishes it.`,
2186
+ "",
2187
+ dim("Most likely a leftover docker-proxy whose container is already gone"),
2188
+ dim("(a native dockerd in WSL strands these on restart), or a holder in"),
2189
+ dim("another Docker engine. Fix it one of two ways, then re-run:"),
2190
+ "",
2191
+ dim("Reap the leftover by restarting the Docker daemon (harmless):"),
2192
+ " " + cyan2("sudo systemctl restart docker"),
2193
+ "",
2194
+ ...hostPortFallbackLines(hostPort)
2195
+ ].join("\n");
2196
+ }
2197
+ function formatHostPortHeldError(hostPort, _code, systemMessage) {
2198
+ return [
2199
+ `Cannot reach host port ${hostPort}: ${systemMessage}`,
2200
+ "",
2201
+ dim('This is not the typical "port already in use" case. The pre-flight'),
2202
+ dim("uses a TCP-connect probe (not a bind), so EACCES / privileged-port"),
2203
+ dim("errors normally do not appear here. Most likely something on your"),
2204
+ dim("host network stack (firewall, network namespace) is interfering with"),
2205
+ dim("loopback connects."),
2206
+ "",
2207
+ dim("Workaround: move Monoceros off this port by setting ") + cyan2("routing.hostPort") + dim(" in ") + cyan2("~/.monoceros/monoceros-config.yml") + dim("."),
2208
+ "",
2209
+ dim("Aborting. Re-run after the issue is resolved.")
2210
+ ].join("\n");
2181
2211
  }
2182
2212
  var CONNECT_TIMEOUT_MS, realPortProbe;
2183
2213
  var init_port_check = __esm({
2184
2214
  "src/proxy/port-check.ts"() {
2185
2215
  "use strict";
2186
2216
  init_proxy();
2217
+ init_format();
2187
2218
  CONNECT_TIMEOUT_MS = 750;
2188
2219
  realPortProbe = (port) => {
2189
2220
  return new Promise((resolve) => {
@@ -4308,6 +4339,8 @@ var init_transform = __esm({
4308
4339
  // src/briefing/agents-md.ts
4309
4340
  function generateAgentsMd(input) {
4310
4341
  const lines = [];
4342
+ const hostPort = input.hostPort ?? 80;
4343
+ const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
4311
4344
  lines.push("# Monoceros Container \u2014 Stack Briefing");
4312
4345
  lines.push("");
4313
4346
  lines.push(
@@ -4435,18 +4468,18 @@ function generateAgentsMd(input) {
4435
4468
  const port = input.ports[i];
4436
4469
  if (i === 0) {
4437
4470
  lines.push(
4438
- `- ${port} (default route) \u2192 http://${input.containerName}.localhost`
4471
+ `- ${port} (default route) \u2192 http://${input.containerName}.localhost${portSuffix}`
4439
4472
  );
4440
4473
  } else {
4441
4474
  lines.push(
4442
- `- ${port} \u2192 http://${input.containerName}-${port}.localhost`
4475
+ `- ${port} \u2192 http://${input.containerName}-${port}.localhost${portSuffix}`
4443
4476
  );
4444
4477
  }
4445
4478
  }
4446
4479
  lines.push("");
4447
4480
  lines.push(
4448
4481
  "To show the user a running app, open it in their host browser with",
4449
- `\`xdg-open http://${input.containerName}.localhost\` \u2014 Monoceros relays`,
4482
+ `\`xdg-open http://${input.containerName}.localhost${portSuffix}\` \u2014 Monoceros relays`,
4450
4483
  "browser-opens from the container to the host machine. Also tell the user",
4451
4484
  "the URL, so they can open it themselves if no bridge is active."
4452
4485
  );
@@ -4466,7 +4499,7 @@ function generateAgentsMd(input) {
4466
4499
  " Angular `--allowed-hosts`, CRA `DANGEROUSLY_DISABLE_HOST_CHECK`;",
4467
4500
  "- it does **not pin the HMR/live-reload socket** to a fixed host or port",
4468
4501
  " \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",
4502
+ ` \`server.hmr.clientPort\`), so HMR works on \`<name>.localhost${portSuffix}\` and over`,
4470
4503
  " the LAN alike;",
4471
4504
  "- the **backend is reached via the dev-server proxy** under a relative",
4472
4505
  " path (Vite `server.proxy`, Angular `proxy.conf.json`, CRA",
@@ -4551,7 +4584,7 @@ function generateAgentsMd(input) {
4551
4584
  "When you build something that serves on a port (a web app, an API),",
4552
4585
  "it must keep running after this session ends. A plain `npm start` (or",
4553
4586
  "any foreground start) dies the moment the user exits you or closes the",
4554
- `terminal, and then \`${input.containerName}.localhost\` returns 502 Bad Gateway.`
4587
+ `terminal, and then \`${input.containerName}.localhost${portSuffix}\` returns 502 Bad Gateway.`
4555
4588
  );
4556
4589
  lines.push("");
4557
4590
  lines.push(
@@ -4643,7 +4676,7 @@ function formatServiceLine(svc) {
4643
4676
  }
4644
4677
  return `- **${svc.name}** (custom image \`${svc.image}\`) \u2014 reachable at \`${reach}\``;
4645
4678
  }
4646
- function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader) {
4679
+ function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader, hostPort = 80) {
4647
4680
  const features = [];
4648
4681
  for (const [ref, userOptions] of Object.entries(opts.features ?? {})) {
4649
4682
  const manifest = manifestLoader?.(ref);
@@ -4663,7 +4696,8 @@ function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader
4663
4696
  services: opts.services,
4664
4697
  features,
4665
4698
  repos: opts.repos ?? [],
4666
- ports: opts.ports ?? []
4699
+ ports: opts.ports ?? [],
4700
+ hostPort
4667
4701
  };
4668
4702
  }
4669
4703
  function resolveFeatureLines(ref, userOptions, manifest, featureDisplayMap2) {
@@ -4929,7 +4963,8 @@ async function writeBriefing(input) {
4929
4963
  agentsMdInputFromCreateOptions(
4930
4964
  input.createOpts,
4931
4965
  featureDisplayMap(input.components),
4932
- manifestLoader
4966
+ manifestLoader,
4967
+ input.hostPort ?? 80
4933
4968
  )
4934
4969
  );
4935
4970
  const claudeBody = generateClaudeMd();
@@ -8317,7 +8352,12 @@ Fix the value in the env file (or the yml).`
8317
8352
  }
8318
8353
  try {
8319
8354
  const components = await loadComponentCatalog();
8320
- await writeBriefing({ targetDir, createOpts, components });
8355
+ await writeBriefing({
8356
+ targetDir,
8357
+ createOpts,
8358
+ components,
8359
+ hostPort: proxyHostPort(globalConfig)
8360
+ });
8321
8361
  } catch (err) {
8322
8362
  const msg = `briefing files not written: ${err instanceof Error ? err.message : String(err)}`;
8323
8363
  (logger.warn ?? logger.info)(msg);
@@ -8810,7 +8850,7 @@ var CLI_VERSION;
8810
8850
  var init_version = __esm({
8811
8851
  "src/version.ts"() {
8812
8852
  "use strict";
8813
- CLI_VERSION = true ? "1.36.3" : "dev";
8853
+ CLI_VERSION = true ? "1.36.5" : "dev";
8814
8854
  }
8815
8855
  });
8816
8856