@getmonoceros/workbench 1.36.2 → 1.36.4

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,52 +2132,90 @@ 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 formatLiveContainerError(hostPort, containers) {
2158
+ const firstName = containers[0].split(" ")[0];
2159
+ return [
2160
+ `Host port ${hostPort} is in use: it is published by a running container.`,
2161
+ "",
2162
+ `Monoceros needs that port for its Traefik proxy. The container(s) holding it:`,
2163
+ ...containers.map((c) => ` - ${c}`),
2164
+ "",
2165
+ `Stop or re-map it, then re-run, e.g.:`,
2166
+ "",
2167
+ ` docker stop ${firstName}`,
2168
+ "",
2169
+ `Or move Monoceros off the port: set \`routing.hostPort\` in`,
2170
+ `~/.monoceros/monoceros-config.yml.`,
2171
+ "",
2172
+ `Aborting. Re-run after the conflict is resolved.`
2173
+ ].join("\n");
2174
+ }
2175
+ function formatLeftoverHolderError(hostPort) {
2176
+ return [
2177
+ `Host port ${hostPort} is in use, but no Docker container in the active`,
2178
+ `engine publishes it.`,
2179
+ "",
2180
+ `This is almost always a leftover Docker port-forwarder (docker-proxy)`,
2181
+ `whose container is already gone. It is left behind when a native Docker`,
2182
+ `daemon is restarted while a container held the port, which is common with`,
2183
+ `dockerd-via-systemd inside WSL (WSL restarts it on every shutdown). The`,
2184
+ `forwarder keeps the port bound with nothing behind it. Two ways out:`,
2185
+ "",
2186
+ ` 1) Recommended: reap the leftover by restarting the Docker daemon`,
2187
+ ` (harmless, it just clears stale forwarders):`,
2188
+ ` WSL / native dockerd: sudo systemctl restart docker`,
2189
+ ` Docker Desktop: restart it from its menu`,
2190
+ ` Then re-run.`,
2191
+ "",
2192
+ ` 2) Or move Monoceros off port ${hostPort}. Edit (or create)`,
2193
+ ` ~/.monoceros/monoceros-config.yml and add:`,
2194
+ "",
2195
+ ` schemaVersion: 1`,
2196
+ ` routing:`,
2197
+ ` hostPort: 8080 # any free port`,
2198
+ "",
2199
+ ` URLs then become http://<name>.localhost:8080/.`,
2200
+ "",
2201
+ `Aborting. Re-run after the conflict is resolved.`
2202
+ ].join("\n");
2203
+ }
2204
+ function formatHostPortHeldError(hostPort, _code, systemMessage) {
2205
+ return [
2206
+ `Cannot reach host port ${hostPort}: ${systemMessage}`,
2207
+ "",
2208
+ `This is not the typical "port already in use" case.`,
2209
+ `Monoceros's pre-flight uses a TCP-connect probe (not a bind), so`,
2210
+ `EACCES / privileged-port errors normally don't appear here. Most`,
2211
+ `likely something on your host network stack (firewall, network`,
2212
+ `namespace, \u2026) is interfering with loopback connects.`,
2213
+ "",
2214
+ "Workaround: move Monoceros off this port by setting",
2215
+ "`routing.hostPort` in ~/.monoceros/monoceros-config.yml.",
2216
+ "",
2217
+ `Aborting. Re-run after the issue is resolved.`
2218
+ ].join("\n");
2181
2219
  }
2182
2220
  var CONNECT_TIMEOUT_MS, realPortProbe;
2183
2221
  var init_port_check = __esm({
@@ -4308,6 +4346,8 @@ var init_transform = __esm({
4308
4346
  // src/briefing/agents-md.ts
4309
4347
  function generateAgentsMd(input) {
4310
4348
  const lines = [];
4349
+ const hostPort = input.hostPort ?? 80;
4350
+ const portSuffix = hostPort === 80 ? "" : `:${hostPort}`;
4311
4351
  lines.push("# Monoceros Container \u2014 Stack Briefing");
4312
4352
  lines.push("");
4313
4353
  lines.push(
@@ -4435,18 +4475,18 @@ function generateAgentsMd(input) {
4435
4475
  const port = input.ports[i];
4436
4476
  if (i === 0) {
4437
4477
  lines.push(
4438
- `- ${port} (default route) \u2192 http://${input.containerName}.localhost`
4478
+ `- ${port} (default route) \u2192 http://${input.containerName}.localhost${portSuffix}`
4439
4479
  );
4440
4480
  } else {
4441
4481
  lines.push(
4442
- `- ${port} \u2192 http://${input.containerName}-${port}.localhost`
4482
+ `- ${port} \u2192 http://${input.containerName}-${port}.localhost${portSuffix}`
4443
4483
  );
4444
4484
  }
4445
4485
  }
4446
4486
  lines.push("");
4447
4487
  lines.push(
4448
4488
  "To show the user a running app, open it in their host browser with",
4449
- `\`xdg-open http://${input.containerName}.localhost\` \u2014 Monoceros relays`,
4489
+ `\`xdg-open http://${input.containerName}.localhost${portSuffix}\` \u2014 Monoceros relays`,
4450
4490
  "browser-opens from the container to the host machine. Also tell the user",
4451
4491
  "the URL, so they can open it themselves if no bridge is active."
4452
4492
  );
@@ -4466,7 +4506,7 @@ function generateAgentsMd(input) {
4466
4506
  " Angular `--allowed-hosts`, CRA `DANGEROUSLY_DISABLE_HOST_CHECK`;",
4467
4507
  "- it does **not pin the HMR/live-reload socket** to a fixed host or port",
4468
4508
  " \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",
4509
+ ` \`server.hmr.clientPort\`), so HMR works on \`<name>.localhost${portSuffix}\` and over`,
4470
4510
  " the LAN alike;",
4471
4511
  "- the **backend is reached via the dev-server proxy** under a relative",
4472
4512
  " path (Vite `server.proxy`, Angular `proxy.conf.json`, CRA",
@@ -4551,7 +4591,7 @@ function generateAgentsMd(input) {
4551
4591
  "When you build something that serves on a port (a web app, an API),",
4552
4592
  "it must keep running after this session ends. A plain `npm start` (or",
4553
4593
  "any foreground start) dies the moment the user exits you or closes the",
4554
- `terminal, and then \`${input.containerName}.localhost\` returns 502 Bad Gateway.`
4594
+ `terminal, and then \`${input.containerName}.localhost${portSuffix}\` returns 502 Bad Gateway.`
4555
4595
  );
4556
4596
  lines.push("");
4557
4597
  lines.push(
@@ -4643,7 +4683,7 @@ function formatServiceLine(svc) {
4643
4683
  }
4644
4684
  return `- **${svc.name}** (custom image \`${svc.image}\`) \u2014 reachable at \`${reach}\``;
4645
4685
  }
4646
- function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader) {
4686
+ function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader, hostPort = 80) {
4647
4687
  const features = [];
4648
4688
  for (const [ref, userOptions] of Object.entries(opts.features ?? {})) {
4649
4689
  const manifest = manifestLoader?.(ref);
@@ -4663,7 +4703,8 @@ function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader
4663
4703
  services: opts.services,
4664
4704
  features,
4665
4705
  repos: opts.repos ?? [],
4666
- ports: opts.ports ?? []
4706
+ ports: opts.ports ?? [],
4707
+ hostPort
4667
4708
  };
4668
4709
  }
4669
4710
  function resolveFeatureLines(ref, userOptions, manifest, featureDisplayMap2) {
@@ -4929,7 +4970,8 @@ async function writeBriefing(input) {
4929
4970
  agentsMdInputFromCreateOptions(
4930
4971
  input.createOpts,
4931
4972
  featureDisplayMap(input.components),
4932
- manifestLoader
4973
+ manifestLoader,
4974
+ input.hostPort ?? 80
4933
4975
  )
4934
4976
  );
4935
4977
  const claudeBody = generateClaudeMd();
@@ -7065,7 +7107,18 @@ async function runComposeAction(buildSubArgs, opts) {
7065
7107
  const { composeFile, projectName } = resolveCompose(opts.root);
7066
7108
  const spawnFn = opts.spawn ?? spawnDockerCompose;
7067
7109
  const subArgs = buildSubArgs(opts.service);
7068
- return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
7110
+ return spawnFn(
7111
+ [
7112
+ "-f",
7113
+ composeFile,
7114
+ "-p",
7115
+ projectName,
7116
+ "--profile",
7117
+ DEFERRED_SERVICE_PROFILE,
7118
+ ...subArgs
7119
+ ],
7120
+ opts.root
7121
+ );
7069
7122
  }
7070
7123
  async function startDeferredServices(opts) {
7071
7124
  if (opts.services.length === 0) return 0;
@@ -8306,7 +8359,12 @@ Fix the value in the env file (or the yml).`
8306
8359
  }
8307
8360
  try {
8308
8361
  const components = await loadComponentCatalog();
8309
- await writeBriefing({ targetDir, createOpts, components });
8362
+ await writeBriefing({
8363
+ targetDir,
8364
+ createOpts,
8365
+ components,
8366
+ hostPort: proxyHostPort(globalConfig)
8367
+ });
8310
8368
  } catch (err) {
8311
8369
  const msg = `briefing files not written: ${err instanceof Error ? err.message : String(err)}`;
8312
8370
  (logger.warn ?? logger.info)(msg);
@@ -8799,7 +8857,7 @@ var CLI_VERSION;
8799
8857
  var init_version = __esm({
8800
8858
  "src/version.ts"() {
8801
8859
  "use strict";
8802
- CLI_VERSION = true ? "1.36.2" : "dev";
8860
+ CLI_VERSION = true ? "1.36.4" : "dev";
8803
8861
  }
8804
8862
  });
8805
8863