@getmonoceros/workbench 1.26.5 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -2146,9 +2146,9 @@ function compareRuntimeVersions(a, b) {
2146
2146
  }
2147
2147
  return 0;
2148
2148
  }
2149
- function runtimeSupportsIdeVolumes(version) {
2149
+ function runtimeSupportsSshAttach(version) {
2150
2150
  if (!version) return false;
2151
- return compareRuntimeVersions(version, MIN_RUNTIME_FOR_IDE_VOLUMES) >= 0;
2151
+ return compareRuntimeVersions(version, MIN_RUNTIME_FOR_SSH_ATTACH) >= 0;
2152
2152
  }
2153
2153
  function descriptorOptionDefaults(options) {
2154
2154
  const out = {};
@@ -2269,7 +2269,7 @@ function deriveServiceName(image) {
2269
2269
  const noTag = lastSegment.split("@")[0].split(":")[0];
2270
2270
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
2271
2271
  }
2272
- var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_IDE_VOLUMES, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG;
2272
+ var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG;
2273
2273
  var init_catalog = __esm({
2274
2274
  "src/create/catalog.ts"() {
2275
2275
  "use strict";
@@ -2278,13 +2278,13 @@ var init_catalog = __esm({
2278
2278
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2279
2279
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2280
2280
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2281
- DEFAULT_RUNTIME_VERSION = true ? "1.1.0" : readFileSync3(
2281
+ DEFAULT_RUNTIME_VERSION = true ? "1.3.2" : readFileSync3(
2282
2282
  fileURLToPath2(
2283
2283
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2284
2284
  ),
2285
2285
  "utf8"
2286
2286
  ).trim();
2287
- MIN_RUNTIME_FOR_IDE_VOLUMES = "1.1.0";
2287
+ MIN_RUNTIME_FOR_SSH_ATTACH = "1.2.0";
2288
2288
  DESCRIPTORS = loadDescriptorCatalogSync();
2289
2289
  BUILTIN_LANGUAGES = new Set(
2290
2290
  [...DESCRIPTORS.values()].filter((c) => c.category === "language" && c.descriptor.language?.builtin).map(descriptorSelector)
@@ -2947,14 +2947,61 @@ function ideStateVolumes(name) {
2947
2947
  return [
2948
2948
  {
2949
2949
  volume: `monoceros-${name}-vscode-extensions`,
2950
- target: "/home/node/.vscode-server/extensions"
2950
+ target: "/home/node/.vscode-server/extensions",
2951
+ minRuntime: "1.1.0"
2951
2952
  },
2952
2953
  {
2953
2954
  volume: `monoceros-${name}-vscode-userdata`,
2954
- target: "/home/node/.vscode-server/data/User"
2955
+ target: "/home/node/.vscode-server/data/User",
2956
+ minRuntime: "1.1.0"
2957
+ },
2958
+ {
2959
+ volume: `monoceros-${name}-vscodium-extensions`,
2960
+ target: "/home/node/.vscodium-server/extensions",
2961
+ minRuntime: "1.2.0"
2962
+ },
2963
+ {
2964
+ volume: `monoceros-${name}-vscodium-userdata`,
2965
+ target: "/home/node/.vscodium-server/data/User",
2966
+ minRuntime: "1.2.0"
2967
+ },
2968
+ // JetBrains (ADR 0022). Only the backend DISTRIBUTION
2969
+ // (`~/.cache/JetBrains/RemoteDev/dist`, ~3 GB, identical across
2970
+ // containers) is shared machine-wide - downloaded once. The sibling
2971
+ // RemoteDev state (`active/`, `recent/`, `remote-dev-worker/`) is
2972
+ // per-user session state and stays per-container, inside the
2973
+ // per-container `~/.cache/JetBrains` volume; sharing it pooled every
2974
+ // container's recent-projects/active-sessions (the "wild" Gateway
2975
+ // list). The shared `dist` mount nests two levels into that volume.
2976
+ {
2977
+ volume: "monoceros-jetbrains-dist",
2978
+ target: "/home/node/.cache/JetBrains/RemoteDev/dist",
2979
+ minRuntime: "1.3.2",
2980
+ shared: true
2981
+ },
2982
+ {
2983
+ volume: `monoceros-${name}-jetbrains-cache`,
2984
+ target: "/home/node/.cache/JetBrains",
2985
+ minRuntime: "1.3.0"
2986
+ },
2987
+ {
2988
+ volume: `monoceros-${name}-jetbrains-config`,
2989
+ target: "/home/node/.config/JetBrains",
2990
+ minRuntime: "1.3.0"
2991
+ },
2992
+ {
2993
+ volume: `monoceros-${name}-jetbrains-data`,
2994
+ target: "/home/node/.local/share/JetBrains",
2995
+ minRuntime: "1.3.0"
2955
2996
  }
2956
2997
  ];
2957
2998
  }
2999
+ function ideStateVolumesForRuntime(name, version) {
3000
+ if (!version) return [];
3001
+ return ideStateVolumes(name).filter(
3002
+ (v) => compareRuntimeVersions(version, v.minRuntime) >= 0
3003
+ );
3004
+ }
2958
3005
  function buildDevcontainerJson(opts, dockerMode = "rootful") {
2959
3006
  const resolvedFeatures = resolveFeatures(opts);
2960
3007
  const features = {};
@@ -2986,6 +3033,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
2986
3033
  }
2987
3034
  }
2988
3035
  } : void 0;
3036
+ const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
2989
3037
  if (needsCompose(opts)) {
2990
3038
  return {
2991
3039
  name: opts.name,
@@ -2996,20 +3044,22 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
2996
3044
  remoteUser: "node",
2997
3045
  forwardPorts: ports,
2998
3046
  postCreateCommand: ".devcontainer/post-create.sh",
3047
+ ...sshPostStart,
2999
3048
  ...featuresField ?? {},
3000
3049
  ...customizationsField ?? {}
3001
3050
  };
3002
3051
  }
3003
- const ideMounts = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name).map(
3004
- (v) => `source=${v.volume},target=${v.target},type=volume`
3005
- ) : [];
3052
+ const ideMounts = ideStateVolumesForRuntime(
3053
+ opts.name,
3054
+ opts.runtimeVersion
3055
+ ).map((v) => `source=${v.volume},target=${v.target},type=volume`);
3006
3056
  const mounts = [...homeMounts, ...ideMounts];
3007
3057
  const mountsField = mounts.length > 0 ? { mounts } : {};
3008
3058
  const workspaceMountField = {
3009
3059
  workspaceMount: `source=\${localWorkspaceFolder},target=/workspaces/${opts.name},type=bind,consistency=cached`,
3010
3060
  workspaceFolder: `/workspaces/${opts.name}`
3011
3061
  };
3012
- const runArgs = ["--cap-add=NET_ADMIN"];
3062
+ const runArgs = ["--cap-add=NET_ADMIN", `--name=monoceros-${opts.name}`];
3013
3063
  if (ports.length > 0) {
3014
3064
  runArgs.push("--network=monoceros-proxy");
3015
3065
  runArgs.push(`--network-alias=${opts.name}`);
@@ -3023,6 +3073,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3023
3073
  runArgs,
3024
3074
  forwardPorts: ports,
3025
3075
  postCreateCommand: ".devcontainer/post-create.sh",
3076
+ ...sshPostStart,
3026
3077
  ...featuresField ?? {},
3027
3078
  ...customizationsField ?? {}
3028
3079
  };
@@ -3043,9 +3094,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3043
3094
  void dockerMode;
3044
3095
  const hasPorts = (opts.ports?.length ?? 0) > 0;
3045
3096
  const lines = ["services:"];
3046
- const ideVolumes = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name) : [];
3097
+ const ideVolumes = ideStateVolumesForRuntime(opts.name, opts.runtimeVersion);
3047
3098
  lines.push(" workspace:");
3048
3099
  lines.push(` image: ${resolveRuntimeImage(opts.runtimeVersion)}`);
3100
+ lines.push(` container_name: monoceros-${opts.name}`);
3049
3101
  lines.push(" command: 'sleep infinity'");
3050
3102
  lines.push(" cap_add:");
3051
3103
  lines.push(" - NET_ADMIN");
@@ -3917,8 +3969,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
3917
3969
  if (!isMap2(item)) return false;
3918
3970
  const url = item.get("url");
3919
3971
  if (url === urlOrPath) return true;
3920
- const path28 = item.get("path");
3921
- const effectivePath = typeof path28 === "string" ? path28 : typeof url === "string" ? deriveRepoName(url) : void 0;
3972
+ const path30 = item.get("path");
3973
+ const effectivePath = typeof path30 === "string" ? path30 : typeof url === "string" ? deriveRepoName(url) : void 0;
3922
3974
  return effectivePath === urlOrPath;
3923
3975
  });
3924
3976
  if (idx < 0) return false;
@@ -4040,7 +4092,7 @@ async function runAddRepo(input) {
4040
4092
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
4041
4093
  );
4042
4094
  }
4043
- const path28 = (input.path ?? deriveRepoName(url)).trim();
4095
+ const path30 = (input.path ?? deriveRepoName(url)).trim();
4044
4096
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
4045
4097
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
4046
4098
  if (hasName !== hasEmail) {
@@ -4077,7 +4129,7 @@ async function runAddRepo(input) {
4077
4129
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
4078
4130
  const entry2 = {
4079
4131
  url,
4080
- path: path28,
4132
+ path: path30,
4081
4133
  ...hasName && hasEmail ? {
4082
4134
  gitUser: {
4083
4135
  name: input.gitName.trim(),
@@ -5120,9 +5172,163 @@ var init_transform = __esm({
5120
5172
  }
5121
5173
  });
5122
5174
 
5175
+ // src/devcontainer/ssh-attach.ts
5176
+ import { spawn as spawn4 } from "child_process";
5177
+ import { promises as fs10 } from "fs";
5178
+ import { existsSync as existsSync8 } from "fs";
5179
+ import os2 from "os";
5180
+ import path13 from "path";
5181
+ function sshHomeDir(home) {
5182
+ return path13.join(home, "ssh");
5183
+ }
5184
+ function sshConfigEntryPath(home, name) {
5185
+ return path13.join(sshHomeDir(home), "config.d", name);
5186
+ }
5187
+ function sshProxyScriptPath(home, name) {
5188
+ return path13.join(sshHomeDir(home), `exec-${name}.sh`);
5189
+ }
5190
+ function privateKeyPath(targetDir) {
5191
+ return path13.join(targetDir, ".monoceros", "ssh", "id_ed25519");
5192
+ }
5193
+ async function ensureKeypair(targetDir, name, keygen, logger) {
5194
+ const privateKey = privateKeyPath(targetDir);
5195
+ const publicKey = `${privateKey}.pub`;
5196
+ if (existsSync8(privateKey) && existsSync8(publicKey)) {
5197
+ return { privateKey, publicKey };
5198
+ }
5199
+ await fs10.mkdir(path13.dirname(privateKey), { recursive: true });
5200
+ try {
5201
+ const res = await keygen([
5202
+ "-t",
5203
+ "ed25519",
5204
+ "-N",
5205
+ "",
5206
+ "-f",
5207
+ privateKey,
5208
+ "-C",
5209
+ `monoceros-${name}`,
5210
+ "-q"
5211
+ ]);
5212
+ if (res.exitCode !== 0) {
5213
+ logger.warn(
5214
+ `ssh-keygen failed (exit ${res.exitCode})${res.stderr ? `: ${res.stderr.trim()}` : ""}; IDE SSH attach not set up.`
5215
+ );
5216
+ return null;
5217
+ }
5218
+ } catch (err) {
5219
+ logger.warn(
5220
+ `ssh-keygen not runnable (${err instanceof Error ? err.message : String(err)}); IDE SSH attach not set up.`
5221
+ );
5222
+ return null;
5223
+ }
5224
+ return { privateKey, publicKey };
5225
+ }
5226
+ function proxyScriptContent(name, targetDir) {
5227
+ return `#!/bin/sh
5228
+ # Generated by Monoceros (ADR 0022) - do not edit.
5229
+ # Portless SSH transport into the running dev container '${name}'.
5230
+ # Resolves the container by the devcontainer.local_folder label (the
5231
+ # same handle 'monoceros shell' uses), so it follows rebuilds.
5232
+ set -e
5233
+ cid=$(docker ps -q \\
5234
+ --filter "label=devcontainer.local_folder=${targetDir}" \\
5235
+ --filter status=running | head -n1)
5236
+ if [ -z "$cid" ]; then
5237
+ echo "monoceros: dev container '${name}' is not running. Start it with: monoceros apply ${name}" >&2
5238
+ exit 1
5239
+ fi
5240
+ exec docker exec -i "$cid" socat - TCP:127.0.0.1:22
5241
+ `;
5242
+ }
5243
+ function configEntryContent(name, hostAlias, privateKey, proxyScript) {
5244
+ return `# Generated by Monoceros (ADR 0022) - do not edit.
5245
+ # Attach an SSH-capable IDE (Codium, IntelliJ, Zed) to host
5246
+ # '${hostAlias}', or run: ssh ${hostAlias}
5247
+ Host ${hostAlias}
5248
+ User node
5249
+ IdentityFile "${privateKey}"
5250
+ IdentitiesOnly yes
5251
+ StrictHostKeyChecking no
5252
+ UserKnownHostsFile /dev/null
5253
+ ProxyCommand "${proxyScript}"
5254
+ `;
5255
+ }
5256
+ async function ensureInclude(userSshDir, home) {
5257
+ const configPath = path13.join(userSshDir, "config");
5258
+ const includeTarget = path13.join(sshHomeDir(home), "config.d", "*");
5259
+ const includeLine = `Include "${includeTarget}"`;
5260
+ await fs10.mkdir(userSshDir, { recursive: true });
5261
+ let existing = "";
5262
+ try {
5263
+ existing = await fs10.readFile(configPath, "utf8");
5264
+ } catch {
5265
+ existing = "";
5266
+ }
5267
+ if (existing.includes(includeTarget)) return;
5268
+ const banner = "# Added by Monoceros (ADR 0022): per-container SSH attach entries.";
5269
+ const prefix = `${banner}
5270
+ ${includeLine}
5271
+ `;
5272
+ const next = existing.length > 0 ? `${prefix}
5273
+ ${existing}` : prefix;
5274
+ await fs10.writeFile(configPath, next, { mode: 384 });
5275
+ await fs10.chmod(configPath, 384).catch(() => {
5276
+ });
5277
+ }
5278
+ async function setupSshAttach(opts) {
5279
+ const hostAlias = `monoceros-${opts.name}`;
5280
+ const logger = opts.logger ?? { info: () => {
5281
+ }, warn: () => {
5282
+ } };
5283
+ const keygen = opts.keygen ?? realKeygen;
5284
+ const userSshDir = opts.userSshDir ?? path13.join(os2.homedir(), ".ssh");
5285
+ const keys = await ensureKeypair(opts.targetDir, opts.name, keygen, logger);
5286
+ if (!keys) return { hostAlias, configured: false };
5287
+ const proxyScript = sshProxyScriptPath(opts.home, opts.name);
5288
+ const configEntry = sshConfigEntryPath(opts.home, opts.name);
5289
+ await fs10.mkdir(path13.dirname(proxyScript), { recursive: true });
5290
+ await fs10.mkdir(path13.dirname(configEntry), { recursive: true });
5291
+ await fs10.writeFile(
5292
+ proxyScript,
5293
+ proxyScriptContent(opts.name, opts.targetDir),
5294
+ { mode: 493 }
5295
+ );
5296
+ await fs10.chmod(proxyScript, 493).catch(() => {
5297
+ });
5298
+ await fs10.writeFile(
5299
+ configEntry,
5300
+ configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
5301
+ );
5302
+ await ensureInclude(userSshDir, opts.home);
5303
+ return { hostAlias, configured: true };
5304
+ }
5305
+ async function removeSshAttach(home, name) {
5306
+ await fs10.rm(sshProxyScriptPath(home, name), { force: true });
5307
+ await fs10.rm(sshConfigEntryPath(home, name), { force: true });
5308
+ }
5309
+ var realKeygen;
5310
+ var init_ssh_attach = __esm({
5311
+ "src/devcontainer/ssh-attach.ts"() {
5312
+ "use strict";
5313
+ realKeygen = (args) => {
5314
+ return new Promise((resolve, reject) => {
5315
+ const child = spawn4("ssh-keygen", args, {
5316
+ stdio: ["ignore", "ignore", "pipe"]
5317
+ });
5318
+ let stderr = "";
5319
+ child.stderr.on("data", (chunk) => {
5320
+ stderr += chunk.toString();
5321
+ });
5322
+ child.on("error", reject);
5323
+ child.on("exit", (code) => resolve({ exitCode: code ?? 0, stderr }));
5324
+ });
5325
+ };
5326
+ }
5327
+ });
5328
+
5123
5329
  // src/apply/apply-log.ts
5124
5330
  import { createWriteStream, mkdirSync } from "fs";
5125
- import path13 from "path";
5331
+ import path14 from "path";
5126
5332
  import { Writable } from "stream";
5127
5333
  function safeIsoStamp(d) {
5128
5334
  return d.toISOString().replace(/[:.]/g, "-");
@@ -5132,7 +5338,7 @@ function createApplyLog(opts) {
5132
5338
  const dir = containerLogsDir(opts.name, opts.home);
5133
5339
  mkdirSync(dir, { recursive: true });
5134
5340
  const file = `apply-${opts.name}-${safeIsoStamp(now)}.log`;
5135
- const fullPath = path13.join(dir, file);
5341
+ const fullPath = path14.join(dir, file);
5136
5342
  const stream = createWriteStream(fullPath, { flags: "w" });
5137
5343
  const header = [
5138
5344
  `# monoceros apply log`,
@@ -5999,8 +6205,8 @@ var init_markers = __esm({
5999
6205
  });
6000
6206
 
6001
6207
  // src/briefing/index.ts
6002
- import { promises as fs10 } from "fs";
6003
- import path14 from "path";
6208
+ import { promises as fs11 } from "fs";
6209
+ import path15 from "path";
6004
6210
  async function writeBriefing(input) {
6005
6211
  const subCommands = input.subCommands ?? await loadSubCommandsDynamic();
6006
6212
  const manifestLoader = input.manifestLoader ?? ((ref) => loadFeatureManifestSummary(ref));
@@ -6013,12 +6219,12 @@ async function writeBriefing(input) {
6013
6219
  );
6014
6220
  const claudeBody = generateClaudeMd();
6015
6221
  const commandsBody = generateCommandsMd(subCommands);
6016
- await writeMarkerAware(path14.join(input.targetDir, "AGENTS.md"), agentsBody);
6017
- await writeMarkerAware(path14.join(input.targetDir, "CLAUDE.md"), claudeBody);
6018
- const monocerosDir = path14.join(input.targetDir, ".monoceros");
6019
- await fs10.mkdir(monocerosDir, { recursive: true });
6020
- await fs10.writeFile(
6021
- path14.join(monocerosDir, "commands.md"),
6222
+ await writeMarkerAware(path15.join(input.targetDir, "AGENTS.md"), agentsBody);
6223
+ await writeMarkerAware(path15.join(input.targetDir, "CLAUDE.md"), claudeBody);
6224
+ const monocerosDir = path15.join(input.targetDir, ".monoceros");
6225
+ await fs11.mkdir(monocerosDir, { recursive: true });
6226
+ await fs11.writeFile(
6227
+ path15.join(monocerosDir, "commands.md"),
6022
6228
  commandsBody,
6023
6229
  "utf8"
6024
6230
  );
@@ -6038,18 +6244,18 @@ function featureDisplayMap(components) {
6038
6244
  async function writeMarkerAware(filePath, body) {
6039
6245
  let existing = null;
6040
6246
  try {
6041
- existing = await fs10.readFile(filePath, "utf8");
6247
+ existing = await fs11.readFile(filePath, "utf8");
6042
6248
  } catch (err) {
6043
6249
  if (err.code !== "ENOENT") throw err;
6044
6250
  }
6045
6251
  if (existing) {
6046
6252
  const updated = replaceMarkerBlock(existing, body);
6047
6253
  if (updated !== null) {
6048
- await fs10.writeFile(filePath, updated, "utf8");
6254
+ await fs11.writeFile(filePath, updated, "utf8");
6049
6255
  return;
6050
6256
  }
6051
6257
  }
6052
- await fs10.writeFile(filePath, wrapWithMarkers(body), "utf8");
6258
+ await fs11.writeFile(filePath, wrapWithMarkers(body), "utf8");
6053
6259
  }
6054
6260
  async function loadSubCommandsDynamic() {
6055
6261
  const mod = await Promise.resolve().then(() => (init_main(), main_exports));
@@ -6174,10 +6380,10 @@ var init_runtime_pull_hint = __esm({
6174
6380
  });
6175
6381
 
6176
6382
  // src/devcontainer/cli.ts
6177
- import { spawn as spawn4 } from "child_process";
6383
+ import { spawn as spawn5 } from "child_process";
6178
6384
  import { readFileSync as readFileSync4 } from "fs";
6179
6385
  import { createRequire } from "module";
6180
- import path15 from "path";
6386
+ import path16 from "path";
6181
6387
  function devcontainerCliPath() {
6182
6388
  if (cachedBinaryPath) return cachedBinaryPath;
6183
6389
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
@@ -6186,7 +6392,7 @@ function devcontainerCliPath() {
6186
6392
  if (!binEntry) {
6187
6393
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
6188
6394
  }
6189
- cachedBinaryPath = path15.resolve(path15.dirname(pkgJsonPath), binEntry);
6395
+ cachedBinaryPath = path16.resolve(path16.dirname(pkgJsonPath), binEntry);
6190
6396
  return cachedBinaryPath;
6191
6397
  }
6192
6398
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -6202,7 +6408,7 @@ var init_cli = __esm({
6202
6408
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
6203
6409
  return new Promise((resolve, reject) => {
6204
6410
  if (options.interactive) {
6205
- const child2 = spawn4(process.execPath, [binPath, ...args], {
6411
+ const child2 = spawn5(process.execPath, [binPath, ...args], {
6206
6412
  cwd,
6207
6413
  env,
6208
6414
  stdio: "inherit"
@@ -6211,7 +6417,7 @@ var init_cli = __esm({
6211
6417
  child2.on("exit", (code) => resolve(code ?? 0));
6212
6418
  return;
6213
6419
  }
6214
- const child = spawn4(process.execPath, [binPath, ...args], {
6420
+ const child = spawn5(process.execPath, [binPath, ...args], {
6215
6421
  cwd,
6216
6422
  env,
6217
6423
  stdio: ["ignore", "pipe", "pipe"]
@@ -6259,9 +6465,9 @@ var init_cli = __esm({
6259
6465
  });
6260
6466
 
6261
6467
  // src/devcontainer/compose.ts
6262
- import { spawn as spawn5 } from "child_process";
6263
- import { existsSync as existsSync8 } from "fs";
6264
- import path16 from "path";
6468
+ import { spawn as spawn6 } from "child_process";
6469
+ import { existsSync as existsSync9 } from "fs";
6470
+ import path17 from "path";
6265
6471
  import { Writable as Writable3 } from "stream";
6266
6472
  import { consola as consola10 } from "consola";
6267
6473
  async function findContainerIds(filters, exec = spawnDocker) {
@@ -6311,18 +6517,24 @@ async function cleanupDockerObjects(opts) {
6311
6517
  return { exitCode: rmExit, removedIds: ids };
6312
6518
  }
6313
6519
  function composeProjectName(root) {
6314
- return `${path16.basename(root)}_devcontainer`;
6520
+ return `${path17.basename(root)}_devcontainer`;
6315
6521
  }
6316
- function resolveCompose(root) {
6317
- if (!existsSync8(path16.join(root, ".devcontainer"))) {
6522
+ function assertDevcontainer(root) {
6523
+ if (!existsSync9(path17.join(root, ".devcontainer"))) {
6318
6524
  throw new Error(
6319
6525
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
6320
6526
  );
6321
6527
  }
6322
- const composeFile = path16.join(root, ".devcontainer", "compose.yaml");
6323
- if (!existsSync8(composeFile)) {
6528
+ }
6529
+ function isComposeMode(root) {
6530
+ return existsSync9(path17.join(root, ".devcontainer", "compose.yaml"));
6531
+ }
6532
+ function resolveCompose(root) {
6533
+ assertDevcontainer(root);
6534
+ const composeFile = path17.join(root, ".devcontainer", "compose.yaml");
6535
+ if (!existsSync9(composeFile)) {
6324
6536
  throw new Error(
6325
- `No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
6537
+ `No compose.yaml at ${composeFile}. \`monoceros logs\` tails compose service logs, which require services configured via \`monoceros add-service <name> <svc>\`. For a bare container, use \`monoceros shell <name>\` or read logs/<app>.log.`
6326
6538
  );
6327
6539
  }
6328
6540
  return { composeFile, projectName: composeProjectName(root) };
@@ -6334,7 +6546,7 @@ async function runComposeAction(buildSubArgs, opts) {
6334
6546
  return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
6335
6547
  }
6336
6548
  async function runStart(opts) {
6337
- resolveCompose(opts.root);
6549
+ assertDevcontainer(opts.root);
6338
6550
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6339
6551
  const spawnFn = opts.spawn ?? spawnDevcontainer;
6340
6552
  logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
@@ -6477,16 +6689,71 @@ and retry \`monoceros apply\`.`
6477
6689
  );
6478
6690
  }
6479
6691
  function runStop(opts) {
6480
- return runComposeAction(
6481
- (service) => ["stop", ...service ? [service] : []],
6482
- opts
6483
- );
6692
+ assertDevcontainer(opts.root);
6693
+ if (isComposeMode(opts.root)) {
6694
+ return runComposeAction(
6695
+ (service) => ["stop", ...service ? [service] : []],
6696
+ opts
6697
+ );
6698
+ }
6699
+ return stopImageContainer(opts);
6484
6700
  }
6485
6701
  function runStatus(opts) {
6486
- return runComposeAction(
6487
- (service) => ["ps", ...service ? [service] : []],
6488
- opts
6702
+ assertDevcontainer(opts.root);
6703
+ if (isComposeMode(opts.root)) {
6704
+ return runComposeAction(
6705
+ (service) => ["ps", ...service ? [service] : []],
6706
+ opts
6707
+ );
6708
+ }
6709
+ return statusImageContainer(opts);
6710
+ }
6711
+ function imageContainerFilter(root) {
6712
+ return `label=devcontainer.local_folder=${root}`;
6713
+ }
6714
+ async function stopImageContainer(opts) {
6715
+ const exec = opts.dockerExec ?? spawnDocker;
6716
+ const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6717
+ const name = path17.basename(opts.root);
6718
+ const ps = await exec([
6719
+ "ps",
6720
+ "-q",
6721
+ "--filter",
6722
+ imageContainerFilter(opts.root),
6723
+ "--filter",
6724
+ "status=running"
6725
+ ]);
6726
+ const id = ps.stdout.split("\n").map((s) => s.trim()).find((s) => s.length > 0);
6727
+ if (!id) {
6728
+ logger.info(`Container '${name}' is not running.`);
6729
+ return 0;
6730
+ }
6731
+ const res = await exec(["stop", id]);
6732
+ if (res.exitCode === 0) logger.info(`Stopped '${name}'.`);
6733
+ return res.exitCode;
6734
+ }
6735
+ async function statusImageContainer(opts) {
6736
+ const exec = opts.dockerExec ?? spawnDocker;
6737
+ const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6738
+ const name = path17.basename(opts.root);
6739
+ const res = await exec([
6740
+ "ps",
6741
+ "-a",
6742
+ "--filter",
6743
+ imageContainerFilter(opts.root)
6744
+ ]);
6745
+ const rows = res.stdout.split("\n").filter((l) => l.trim().length > 0);
6746
+ if (rows.length <= 1) {
6747
+ logger.info(
6748
+ `Container '${name}' does not exist. Run \`monoceros apply ${name}\`.`
6749
+ );
6750
+ return 0;
6751
+ }
6752
+ process.stdout.write(
6753
+ res.stdout.endsWith("\n") ? res.stdout : `${res.stdout}
6754
+ `
6489
6755
  );
6756
+ return res.exitCode;
6490
6757
  }
6491
6758
  function runLogs(opts) {
6492
6759
  const follow = opts.follow ?? true;
@@ -6509,7 +6776,7 @@ var init_compose = __esm({
6509
6776
  init_proxy();
6510
6777
  spawnDockerCompose = (args, cwd) => {
6511
6778
  return new Promise((resolve, reject) => {
6512
- const child = spawn5("docker", ["compose", ...args], {
6779
+ const child = spawn6("docker", ["compose", ...args], {
6513
6780
  cwd,
6514
6781
  stdio: ["inherit", "pipe", "pipe"]
6515
6782
  });
@@ -6521,7 +6788,7 @@ var init_compose = __esm({
6521
6788
  };
6522
6789
  spawnDocker = (args) => {
6523
6790
  return new Promise((resolve, reject) => {
6524
- const child = spawn5("docker", args, {
6791
+ const child = spawn6("docker", args, {
6525
6792
  stdio: ["ignore", "pipe", "pipe"]
6526
6793
  });
6527
6794
  let stdout = "";
@@ -6582,9 +6849,9 @@ var init_images = __esm({
6582
6849
 
6583
6850
  // src/config/machine-state.ts
6584
6851
  import { promises as fsp4 } from "fs";
6585
- import path17 from "path";
6852
+ import path18 from "path";
6586
6853
  function machineStatePath(home = monocerosHome()) {
6587
- return path17.join(home, ".machine-state.json");
6854
+ return path18.join(home, ".machine-state.json");
6588
6855
  }
6589
6856
  async function readMachineState(home = monocerosHome()) {
6590
6857
  try {
@@ -6639,7 +6906,7 @@ var init_machine_state = __esm({
6639
6906
  });
6640
6907
 
6641
6908
  // src/devcontainer/docker-mode.ts
6642
- import { spawn as spawn6 } from "child_process";
6909
+ import { spawn as spawn7 } from "child_process";
6643
6910
  async function detectDockerMode(options = {}) {
6644
6911
  const spawnFn = options.spawn ?? realDockerInfo;
6645
6912
  try {
@@ -6689,7 +6956,7 @@ var init_docker_mode = __esm({
6689
6956
  init_format();
6690
6957
  realDockerInfo = () => {
6691
6958
  return new Promise((resolve, reject) => {
6692
- const child = spawn6(
6959
+ const child = spawn7(
6693
6960
  "docker",
6694
6961
  ["info", "--format", "{{json .SecurityOptions}}"],
6695
6962
  {
@@ -6708,9 +6975,9 @@ var init_docker_mode = __esm({
6708
6975
  });
6709
6976
 
6710
6977
  // src/devcontainer/identity.ts
6711
- import { spawn as spawn7 } from "child_process";
6712
- import { promises as fs11 } from "fs";
6713
- import path18 from "path";
6978
+ import { spawn as spawn8 } from "child_process";
6979
+ import { promises as fs12 } from "fs";
6980
+ import path19 from "path";
6714
6981
  import { consola as consola11 } from "consola";
6715
6982
  async function resolveIdentityWithPrompt(options = {}) {
6716
6983
  const spawnFn = options.spawn ?? realGitConfigGet;
@@ -6766,8 +7033,8 @@ async function resolveIdentityWithPrompt(options = {}) {
6766
7033
  };
6767
7034
  }
6768
7035
  async function collectGitIdentity(devContainerRoot, options = {}) {
6769
- const gitconfigDir = path18.join(devContainerRoot, ".monoceros");
6770
- const gitconfigPath = path18.join(gitconfigDir, "gitconfig");
7036
+ const gitconfigDir = path19.join(devContainerRoot, ".monoceros");
7037
+ const gitconfigPath = path19.join(gitconfigDir, "gitconfig");
6771
7038
  const logger = options.logger ?? { info: () => {
6772
7039
  }, warn: () => {
6773
7040
  } };
@@ -6780,8 +7047,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
6780
7047
  const lines = ["[user]"];
6781
7048
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
6782
7049
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
6783
- await fs11.mkdir(gitconfigDir, { recursive: true });
6784
- await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
7050
+ await fs12.mkdir(gitconfigDir, { recursive: true });
7051
+ await fs12.writeFile(gitconfigPath, lines.join("\n") + "\n");
6785
7052
  return {
6786
7053
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
6787
7054
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -6824,7 +7091,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
6824
7091
  }
6825
7092
  async function readExistingGitconfig(filePath) {
6826
7093
  try {
6827
- const content = await fs11.readFile(filePath, "utf8");
7094
+ const content = await fs12.readFile(filePath, "utf8");
6828
7095
  const result = {};
6829
7096
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
6830
7097
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -6841,7 +7108,7 @@ var init_identity = __esm({
6841
7108
  "use strict";
6842
7109
  realGitConfigGet = (key) => {
6843
7110
  return new Promise((resolve, reject) => {
6844
- const child = spawn7("git", ["config", "--global", "--get", key], {
7111
+ const child = spawn8("git", ["config", "--global", "--get", key], {
6845
7112
  stdio: ["ignore", "pipe", "inherit"]
6846
7113
  });
6847
7114
  let stdout = "";
@@ -6901,7 +7168,7 @@ var init_identity = __esm({
6901
7168
  });
6902
7169
 
6903
7170
  // src/apply/index.ts
6904
- import { existsSync as existsSync9, promises as fs12 } from "fs";
7171
+ import { existsSync as existsSync10, promises as fs13 } from "fs";
6905
7172
  import { consola as consola12 } from "consola";
6906
7173
  async function runApply(opts) {
6907
7174
  const home = opts.monocerosHome ?? monocerosHome();
@@ -6923,7 +7190,7 @@ ${sectionLine(label)}
6923
7190
  );
6924
7191
  }
6925
7192
  const ymlPath = containerConfigPath(opts.name, home);
6926
- if (!existsSync9(ymlPath)) {
7193
+ if (!existsSync10(ymlPath)) {
6927
7194
  throw new Error(
6928
7195
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
6929
7196
  );
@@ -7041,7 +7308,7 @@ Fix the value in the env file (or the yml).`
7041
7308
  if (dockerMode === "rootless") {
7042
7309
  throw new Error(formatRootlessNotSupportedError());
7043
7310
  }
7044
- await fs12.mkdir(targetDir, { recursive: true });
7311
+ await fs13.mkdir(targetDir, { recursive: true });
7045
7312
  await writeScaffold(createOpts, targetDir, { dockerMode });
7046
7313
  await writeStateFile(
7047
7314
  targetDir,
@@ -7052,6 +7319,27 @@ Fix the value in the env file (or the yml).`
7052
7319
  ...opts.now ? { now: opts.now } : {}
7053
7320
  })
7054
7321
  );
7322
+ if (runtimeSupportsSshAttach(createOpts.runtimeVersion)) {
7323
+ try {
7324
+ const ssh = await setupSshAttach({
7325
+ name: opts.name,
7326
+ targetDir,
7327
+ home,
7328
+ ...opts.sshKeygen ? { keygen: opts.sshKeygen } : {},
7329
+ ...opts.sshUserSshDir ? { userSshDir: opts.sshUserSshDir } : {},
7330
+ logger: idLogger
7331
+ });
7332
+ if (ssh.configured) {
7333
+ logger.info(
7334
+ `SSH attach: \`ssh ${ssh.hostAlias}\` (or pick it in your IDE)`
7335
+ );
7336
+ }
7337
+ } catch (err) {
7338
+ (logger.warn ?? logger.info)(
7339
+ `SSH attach setup skipped: ${err instanceof Error ? err.message : String(err)}`
7340
+ );
7341
+ }
7342
+ }
7055
7343
  try {
7056
7344
  const components = await loadComponentCatalog();
7057
7345
  await writeBriefing({ targetDir, createOpts, components });
@@ -7196,8 +7484,8 @@ ${stripAnsi(formatted)}
7196
7484
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
7197
7485
  }
7198
7486
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
7199
- if (!existsSync9(targetDir)) return;
7200
- const entries = await fs12.readdir(targetDir);
7487
+ if (!existsSync10(targetDir)) return;
7488
+ const entries = await fs13.readdir(targetDir);
7201
7489
  if (entries.length === 0) return;
7202
7490
  const state = await readStateFile(targetDir);
7203
7491
  if (state) {
@@ -7267,7 +7555,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7267
7555
  }
7268
7556
  if (wantContainer) {
7269
7557
  try {
7270
- const text = await fs12.readFile(ymlPath, "utf8");
7558
+ const text = await fs13.readFile(ymlPath, "utf8");
7271
7559
  const parsed = parseConfig(text, ymlPath);
7272
7560
  const changed = setContainerGitUserInDoc(parsed.doc, {
7273
7561
  name: prompted.name,
@@ -7275,7 +7563,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7275
7563
  });
7276
7564
  if (changed) {
7277
7565
  const out = stringifyConfig(parsed.doc);
7278
- await fs12.writeFile(ymlPath, out, "utf8");
7566
+ await fs13.writeFile(ymlPath, out, "utf8");
7279
7567
  logger.info(
7280
7568
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
7281
7569
  );
@@ -7298,6 +7586,7 @@ var init_apply = __esm({
7298
7586
  init_state();
7299
7587
  init_transform();
7300
7588
  init_catalog();
7589
+ init_ssh_attach();
7301
7590
  init_scaffold();
7302
7591
  init_format();
7303
7592
  init_ref();
@@ -7321,82 +7610,378 @@ var init_apply = __esm({
7321
7610
  }
7322
7611
  });
7323
7612
 
7324
- // src/version.ts
7325
- var CLI_VERSION;
7326
- var init_version = __esm({
7327
- "src/version.ts"() {
7328
- "use strict";
7329
- CLI_VERSION = true ? "1.26.5" : "dev";
7330
- }
7331
- });
7332
-
7333
- // src/commands/_dispatch.ts
7334
- import { consola as consola13 } from "consola";
7335
- async function dispatch(runner) {
7613
+ // src/devcontainer/browser-bridge.ts
7614
+ import { spawn as spawn9 } from "child_process";
7615
+ import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync5 } from "fs";
7616
+ import http from "http";
7617
+ import path20 from "path";
7618
+ function parseCallbackTarget(authUrl) {
7336
7619
  try {
7337
- const exitCode = await runner();
7338
- process.exit(exitCode);
7339
- } catch (err) {
7340
- consola13.error(err instanceof Error ? err.message : String(err));
7341
- process.exit(1);
7620
+ const u = new URL(authUrl);
7621
+ const redirect = u.searchParams.get("redirect_uri");
7622
+ if (!redirect) return null;
7623
+ const r = new URL(redirect);
7624
+ if (r.hostname !== "localhost" && r.hostname !== "127.0.0.1") return null;
7625
+ const port = Number(r.port);
7626
+ if (!Number.isInteger(port) || port <= 0) return null;
7627
+ return { port, pathname: r.pathname };
7628
+ } catch {
7629
+ return null;
7342
7630
  }
7343
7631
  }
7344
- var init_dispatch = __esm({
7345
- "src/commands/_dispatch.ts"() {
7346
- "use strict";
7632
+ function nextRelayUrl(content, lastOpened) {
7633
+ const url = content.trim();
7634
+ return url && url !== lastOpened ? url : null;
7635
+ }
7636
+ function wrapExec(command, opts) {
7637
+ const leading = [];
7638
+ const stmts = [];
7639
+ if (opts.pathPrepend) {
7640
+ leading.push(opts.pathPrepend);
7641
+ const i = leading.length;
7642
+ stmts.push(`export PATH="$${i}:$PATH"`);
7643
+ stmts.push(`export BROWSER="$${i}/xdg-open"`);
7347
7644
  }
7348
- });
7349
-
7350
- // src/commands/apply.ts
7351
- import { defineCommand as defineCommand8 } from "citty";
7352
- var applyCommand;
7353
- var init_apply2 = __esm({
7354
- "src/commands/apply.ts"() {
7355
- "use strict";
7356
- init_apply();
7357
- init_version();
7358
- init_dispatch();
7359
- applyCommand = defineCommand8({
7360
- meta: {
7361
- name: "apply",
7362
- group: "lifecycle",
7363
- description: "Materialize a container config into $MONOCEROS_HOME/container/<name>/ and bring the dev-container up. Close any VS Code Remote Containers session for the target first \u2014 the extension auto-recreates and races with apply."
7364
- },
7365
- args: {
7366
- name: {
7367
- type: "positional",
7368
- description: "Config name. Resolves to $MONOCEROS_HOME/container-configs/<name>.yml.",
7369
- required: true
7370
- },
7371
- verbose: {
7372
- type: "boolean",
7373
- description: "Stream the raw @devcontainers/cli output to stderr instead of showing a phase spinner. Auto-enabled when stderr is not a TTY.",
7374
- default: false
7375
- }
7376
- },
7377
- run({ args }) {
7378
- return dispatch(async () => {
7379
- const result = await runApply({
7380
- name: args.name,
7381
- cliVersion: CLI_VERSION,
7382
- verbose: args.verbose
7383
- });
7384
- return result.containerExitCode;
7385
- });
7386
- }
7645
+ if (opts.cwd) {
7646
+ leading.push(opts.cwd);
7647
+ stmts.push(`cd -- "$${leading.length}"`);
7648
+ }
7649
+ if (leading.length === 0) return command;
7650
+ const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
7651
+ const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
7652
+ return ["bash", "-lc", script, "bash", ...leading, ...command];
7653
+ }
7654
+ function openInBrowser(url) {
7655
+ const platform = process.platform;
7656
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
7657
+ try {
7658
+ const child = spawn9(cmd, args, {
7659
+ stdio: "ignore",
7660
+ detached: true
7661
+ });
7662
+ child.on("error", () => {
7387
7663
  });
7664
+ child.unref();
7665
+ } catch {
7388
7666
  }
7389
- });
7390
-
7391
- // src/commands/completion.ts
7392
- import { defineCommand as defineCommand9 } from "citty";
7393
- function renderCompletionScript(shell) {
7394
- if (shell === "bash") {
7395
- return [
7396
- "# bash completion for monoceros",
7397
- "# install: source this file from .bashrc, e.g.",
7398
- "# monoceros completion bash > ~/.bash_completion.d/monoceros",
7399
- '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
7667
+ }
7668
+ async function startBrowserBridge(opts) {
7669
+ const relayDir = path20.join(opts.root, RELAY_DIRNAME);
7670
+ const relayScript = path20.join(relayDir, "xdg-open");
7671
+ const urlFile = path20.join(relayDir, "url");
7672
+ await fsp5.mkdir(relayDir, { recursive: true });
7673
+ await fsp5.rm(urlFile, { force: true });
7674
+ await fsp5.writeFile(
7675
+ relayScript,
7676
+ `#!/bin/sh
7677
+ printf '%s\\n' "$1" > "$(dirname "$0")/url"
7678
+ exit 0
7679
+ `,
7680
+ { mode: 493 }
7681
+ );
7682
+ await fsp5.chmod(relayScript, 493);
7683
+ const servers = [];
7684
+ let lastOpened = null;
7685
+ const onUrl = (url) => {
7686
+ openInBrowser(url);
7687
+ const target = parseCallbackTarget(url);
7688
+ if (!target) return;
7689
+ const server = http.createServer((req, res) => {
7690
+ const reqUrl = req.url ?? target.pathname;
7691
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
7692
+ res.end(
7693
+ '<html><body style="font-family:sans-serif;padding:3rem">You are signed in. You can close this tab and return to the terminal.</body></html>'
7694
+ );
7695
+ void opts.spawn(
7696
+ [
7697
+ "exec",
7698
+ "--workspace-folder",
7699
+ opts.root,
7700
+ "--mount-workspace-git-root=false",
7701
+ "curl",
7702
+ "-fsS",
7703
+ `http://localhost:${target.port}${reqUrl}`
7704
+ ],
7705
+ opts.root,
7706
+ { quiet: true }
7707
+ );
7708
+ });
7709
+ server.on("error", () => {
7710
+ });
7711
+ server.listen(target.port, "127.0.0.1");
7712
+ servers.push(server);
7713
+ };
7714
+ const poll = setInterval(() => {
7715
+ if (!existsSync11(urlFile)) return;
7716
+ let content = "";
7717
+ try {
7718
+ content = readFileSync5(urlFile, "utf8");
7719
+ } catch {
7720
+ return;
7721
+ }
7722
+ const url = nextRelayUrl(content, lastOpened);
7723
+ if (!url) return;
7724
+ lastOpened = url;
7725
+ onUrl(url);
7726
+ }, 250);
7727
+ return {
7728
+ relayDirInContainer: `/workspaces/${opts.name}/${RELAY_DIRNAME}`,
7729
+ async dispose() {
7730
+ clearInterval(poll);
7731
+ for (const s of servers) s.close();
7732
+ await fsp5.rm(relayDir, { recursive: true, force: true });
7733
+ }
7734
+ };
7735
+ }
7736
+ var RELAY_DIRNAME;
7737
+ var init_browser_bridge = __esm({
7738
+ "src/devcontainer/browser-bridge.ts"() {
7739
+ "use strict";
7740
+ RELAY_DIRNAME = ".monoceros-bridge";
7741
+ }
7742
+ });
7743
+
7744
+ // src/devcontainer/shell.ts
7745
+ import { existsSync as existsSync12 } from "fs";
7746
+ import path21 from "path";
7747
+ async function runShell(opts) {
7748
+ assertContainerExists(opts.root);
7749
+ const spawnFn = opts.spawn ?? spawnDevcontainer;
7750
+ const upCode = await spawnFn(
7751
+ ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
7752
+ opts.root,
7753
+ { quiet: true }
7754
+ );
7755
+ if (upCode !== 0) return upCode;
7756
+ const name = opts.name ?? path21.basename(opts.root);
7757
+ const isTty2 = opts.isTty ?? process.stdout.isTTY;
7758
+ const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
7759
+ try {
7760
+ const innerExec = wrapExec(
7761
+ ["bash"],
7762
+ bridge ? { pathPrepend: bridge.relayDirInContainer } : {}
7763
+ );
7764
+ return await spawnFn(
7765
+ [
7766
+ "exec",
7767
+ "--workspace-folder",
7768
+ opts.root,
7769
+ "--mount-workspace-git-root=false",
7770
+ ...innerExec
7771
+ ],
7772
+ opts.root,
7773
+ { interactive: true }
7774
+ );
7775
+ } finally {
7776
+ if (bridge) await bridge.dispose();
7777
+ }
7778
+ }
7779
+ function assertContainerExists(root) {
7780
+ if (!existsSync12(path21.join(root, ".devcontainer"))) {
7781
+ throw new Error(
7782
+ `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
7783
+ );
7784
+ }
7785
+ }
7786
+ var init_shell = __esm({
7787
+ "src/devcontainer/shell.ts"() {
7788
+ "use strict";
7789
+ init_browser_bridge();
7790
+ init_cli();
7791
+ }
7792
+ });
7793
+
7794
+ // src/open/index.ts
7795
+ import { spawn as spawn10 } from "child_process";
7796
+ import { accessSync, constants, existsSync as existsSync13 } from "fs";
7797
+ import path22 from "path";
7798
+ import { consola as consola13 } from "consola";
7799
+ function resolveOnPath(bin) {
7800
+ const dirs = (process.env.PATH ?? "").split(path22.delimiter).filter(Boolean);
7801
+ for (const dir of dirs) {
7802
+ const candidate = path22.join(dir, bin);
7803
+ try {
7804
+ accessSync(candidate, constants.X_OK);
7805
+ return candidate;
7806
+ } catch {
7807
+ }
7808
+ }
7809
+ return null;
7810
+ }
7811
+ function resolveEditorBinary(tool) {
7812
+ if (resolveOnPath(tool.bin)) return tool.bin;
7813
+ if (process.platform === "darwin" && existsSync13(tool.macAppBin)) {
7814
+ return tool.macAppBin;
7815
+ }
7816
+ return null;
7817
+ }
7818
+ function realLaunch(bin, args) {
7819
+ const child = spawn10(bin, args, {
7820
+ detached: true,
7821
+ stdio: "ignore"
7822
+ });
7823
+ child.unref();
7824
+ }
7825
+ async function runOpen(opts) {
7826
+ const home = opts.monocerosHome ?? monocerosHome();
7827
+ const logger = opts.logger ?? {
7828
+ info: (m) => consola13.info(m),
7829
+ warn: (m) => consola13.warn(m)
7830
+ };
7831
+ const root = containerDir(opts.name, home);
7832
+ if (opts.tool === "shell") {
7833
+ const shellRunner = opts.shellRunner ?? runShell;
7834
+ return shellRunner({ root, name: opts.name });
7835
+ }
7836
+ const editor = EDITORS[opts.tool];
7837
+ if (!editor) {
7838
+ throw new Error(
7839
+ `Unknown tool '${opts.tool}'. Supported: ${OPEN_TOOLS.join(", ")}.`
7840
+ );
7841
+ }
7842
+ if (!existsSync13(sshConfigEntryPath(home, opts.name))) {
7843
+ throw new Error(
7844
+ `SSH attach isn't set up for '${opts.name}'. Run \`monoceros apply ${opts.name}\` first (needs a runtime >= 1.2.0).`
7845
+ );
7846
+ }
7847
+ const running = await findRunningContainerByLocalFolder(
7848
+ root,
7849
+ opts.dockerLookup ? { docker: opts.dockerLookup } : {}
7850
+ );
7851
+ if (!running) {
7852
+ throw new Error(
7853
+ `Container '${opts.name}' isn't running. Bring it up with \`monoceros apply ${opts.name}\`, then retry.`
7854
+ );
7855
+ }
7856
+ const resolve = opts.binResolver ?? resolveEditorBinary;
7857
+ const bin = resolve(editor);
7858
+ if (!bin) {
7859
+ const where = process.platform === "darwin" ? " on PATH or in /Applications" : " on PATH";
7860
+ throw new Error(
7861
+ `${editor.label} ('${editor.bin}') not found${where}. In ${editor.label}, ${editor.setupHint}.`
7862
+ );
7863
+ }
7864
+ const uri = `vscode-remote://ssh-remote+monoceros-${opts.name}/workspaces/${opts.name}/${opts.name}.code-workspace`;
7865
+ const launch = opts.launch ?? realLaunch;
7866
+ launch(bin, ["--file-uri", uri]);
7867
+ logger.info(`Opening '${opts.name}' in ${editor.label}...`);
7868
+ return 0;
7869
+ }
7870
+ var EDITORS, OPEN_TOOLS;
7871
+ var init_open = __esm({
7872
+ "src/open/index.ts"() {
7873
+ "use strict";
7874
+ init_paths();
7875
+ init_locate_running();
7876
+ init_shell();
7877
+ init_ssh_attach();
7878
+ EDITORS = {
7879
+ code: {
7880
+ label: "VS Code",
7881
+ bin: "code",
7882
+ macAppBin: "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
7883
+ setupHint: `install the Remote-SSH extension and run "Shell Command: Install 'code' command in PATH"`
7884
+ },
7885
+ codium: {
7886
+ label: "VS Codium",
7887
+ bin: "codium",
7888
+ macAppBin: "/Applications/VSCodium.app/Contents/Resources/app/bin/codium",
7889
+ setupHint: 'install the "Open Remote - SSH" extension (the codium CLI ships with the app)'
7890
+ }
7891
+ };
7892
+ OPEN_TOOLS = [...Object.keys(EDITORS), "shell"];
7893
+ }
7894
+ });
7895
+
7896
+ // src/version.ts
7897
+ var CLI_VERSION;
7898
+ var init_version = __esm({
7899
+ "src/version.ts"() {
7900
+ "use strict";
7901
+ CLI_VERSION = true ? "1.28.0" : "dev";
7902
+ }
7903
+ });
7904
+
7905
+ // src/commands/_dispatch.ts
7906
+ import { consola as consola14 } from "consola";
7907
+ async function dispatch(runner) {
7908
+ try {
7909
+ const exitCode = await runner();
7910
+ process.exit(exitCode);
7911
+ } catch (err) {
7912
+ consola14.error(err instanceof Error ? err.message : String(err));
7913
+ process.exit(1);
7914
+ }
7915
+ }
7916
+ var init_dispatch = __esm({
7917
+ "src/commands/_dispatch.ts"() {
7918
+ "use strict";
7919
+ }
7920
+ });
7921
+
7922
+ // src/commands/apply.ts
7923
+ import { defineCommand as defineCommand8 } from "citty";
7924
+ import { consola as consola15 } from "consola";
7925
+ var applyCommand;
7926
+ var init_apply2 = __esm({
7927
+ "src/commands/apply.ts"() {
7928
+ "use strict";
7929
+ init_apply();
7930
+ init_open();
7931
+ init_version();
7932
+ init_dispatch();
7933
+ applyCommand = defineCommand8({
7934
+ meta: {
7935
+ name: "apply",
7936
+ group: "lifecycle",
7937
+ description: "Materialize a container config into $MONOCEROS_HOME/container/<name>/ and bring the dev-container up. Close any VS Code Remote Containers session for the target first \u2014 the extension auto-recreates and races with apply."
7938
+ },
7939
+ args: {
7940
+ name: {
7941
+ type: "positional",
7942
+ description: "Config name. Resolves to $MONOCEROS_HOME/container-configs/<name>.yml.",
7943
+ required: true
7944
+ },
7945
+ verbose: {
7946
+ type: "boolean",
7947
+ description: "Stream the raw @devcontainers/cli output to stderr instead of showing a phase spinner. Auto-enabled when stderr is not a TTY.",
7948
+ default: false
7949
+ },
7950
+ open: {
7951
+ type: "string",
7952
+ description: `After a successful apply, open the container in this tool (${OPEN_TOOLS.join("|")}).`
7953
+ }
7954
+ },
7955
+ run({ args }) {
7956
+ return dispatch(async () => {
7957
+ const result = await runApply({
7958
+ name: args.name,
7959
+ cliVersion: CLI_VERSION,
7960
+ verbose: args.verbose
7961
+ });
7962
+ if (args.open && result.containerExitCode === 0) {
7963
+ try {
7964
+ await runOpen({ name: args.name, tool: args.open });
7965
+ } catch (err) {
7966
+ consola15.warn(err instanceof Error ? err.message : String(err));
7967
+ }
7968
+ }
7969
+ return result.containerExitCode;
7970
+ });
7971
+ }
7972
+ });
7973
+ }
7974
+ });
7975
+
7976
+ // src/commands/completion.ts
7977
+ import { defineCommand as defineCommand9 } from "citty";
7978
+ function renderCompletionScript(shell) {
7979
+ if (shell === "bash") {
7980
+ return [
7981
+ "# bash completion for monoceros",
7982
+ "# install: source this file from .bashrc, e.g.",
7983
+ "# monoceros completion bash > ~/.bash_completion.d/monoceros",
7984
+ '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
7400
7985
  "#",
7401
7986
  "# The work is done by `monoceros __complete --line --point`; this",
7402
7987
  "# shell wrapper only forwards the cursor view.",
@@ -7516,8 +8101,8 @@ var init_completion = __esm({
7516
8101
  });
7517
8102
 
7518
8103
  // src/completion/resolve.ts
7519
- import { existsSync as existsSync10, promises as fs13 } from "fs";
7520
- import path19 from "path";
8104
+ import { existsSync as existsSync14, promises as fs14 } from "fs";
8105
+ import path23 from "path";
7521
8106
  async function resolveCompletions(line, point, opts = {}) {
7522
8107
  const { prev, current } = parseCompletionLine(line, point);
7523
8108
  const ctx = { prev, current, opts };
@@ -7666,9 +8251,9 @@ function filterPrefix(values, fragment) {
7666
8251
  }
7667
8252
  async function listContainerNames(ctx) {
7668
8253
  const home = ctx.opts.monocerosHome ?? monocerosHome();
7669
- const dir = path19.join(home, "container-configs");
7670
- if (!existsSync10(dir)) return [];
7671
- const entries = await fs13.readdir(dir);
8254
+ const dir = path23.join(home, "container-configs");
8255
+ if (!existsSync14(dir)) return [];
8256
+ const entries = await fs14.readdir(dir);
7672
8257
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
7673
8258
  }
7674
8259
  async function listFeatureComponents() {
@@ -7742,10 +8327,12 @@ var init_resolve = __esm({
7742
8327
  init_manifest();
7743
8328
  init_catalog();
7744
8329
  init_schema();
8330
+ init_open();
7745
8331
  ALL_COMMANDS = [
7746
8332
  "init",
7747
8333
  "list-components",
7748
8334
  "shell",
8335
+ "open",
7749
8336
  "run",
7750
8337
  "logs",
7751
8338
  "start",
@@ -7795,7 +8382,10 @@ var init_resolve = __esm({
7795
8382
  },
7796
8383
  apply: {
7797
8384
  positionals: [containerName],
7798
- flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
8385
+ flags: {
8386
+ "--yes": { type: "boolean", aliases: ["-y"] },
8387
+ "--open": { type: "value", values: () => [...OPEN_TOOLS] }
8388
+ }
7799
8389
  },
7800
8390
  upgrade: {
7801
8391
  // First positional is a container name; the second is a version
@@ -7811,12 +8401,16 @@ var init_resolve = __esm({
7811
8401
  }
7812
8402
  },
7813
8403
  shell: { positionals: [containerName] },
8404
+ open: { positionals: [containerName, () => [...OPEN_TOOLS]] },
7814
8405
  run: {
7815
8406
  positionals: [containerName],
7816
8407
  flags: { "--in": { type: "value" } }
7817
8408
  },
7818
8409
  logs: { positionals: [containerName] },
7819
- start: { positionals: [containerName] },
8410
+ start: {
8411
+ positionals: [containerName],
8412
+ flags: { "--open": { type: "value", values: () => [...OPEN_TOOLS] } }
8413
+ },
7820
8414
  stop: { positionals: [containerName] },
7821
8415
  status: { positionals: [containerName] },
7822
8416
  "add-language": {
@@ -8335,14 +8929,14 @@ var init_generator = __esm({
8335
8929
  });
8336
8930
 
8337
8931
  // src/init/index.ts
8338
- import { existsSync as existsSync11, promises as fs14 } from "fs";
8339
- import path20 from "path";
8340
- import { consola as consola14 } from "consola";
8932
+ import { existsSync as existsSync15, promises as fs15 } from "fs";
8933
+ import path24 from "path";
8934
+ import { consola as consola16 } from "consola";
8341
8935
  async function runInit(opts) {
8342
8936
  const home = opts.monocerosHome ?? monocerosHome();
8343
8937
  const logger = opts.logger ?? {
8344
- success: (msg) => consola14.success(msg),
8345
- info: (msg) => consola14.info(msg)
8938
+ success: (msg) => consola16.success(msg),
8939
+ info: (msg) => consola16.info(msg)
8346
8940
  };
8347
8941
  if (!REGEX.solutionName.test(opts.name)) {
8348
8942
  throw new Error(
@@ -8350,12 +8944,12 @@ async function runInit(opts) {
8350
8944
  );
8351
8945
  }
8352
8946
  const dest = containerConfigPath(opts.name, home);
8353
- if (existsSync11(dest)) {
8947
+ if (existsSync15(dest)) {
8354
8948
  throw new Error(
8355
8949
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
8356
8950
  );
8357
8951
  }
8358
- const componentsRoot = opts.workbenchRoot ? path20.join(opts.workbenchRoot, "components") : componentsRootDir();
8952
+ const componentsRoot = opts.workbenchRoot ? path24.join(opts.workbenchRoot, "components") : componentsRootDir();
8359
8953
  const catalog = await loadComponentCatalog(componentsRoot);
8360
8954
  if (catalog.size === 0) {
8361
8955
  throw new Error(
@@ -8421,9 +9015,9 @@ async function runInit(opts) {
8421
9015
  } else {
8422
9016
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
8423
9017
  }
8424
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
9018
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
8425
9019
  await ensureEnvGitignored(containerConfigsDir(home));
8426
- await fs14.writeFile(dest, text, "utf8");
9020
+ await fs15.writeFile(dest, text, "utf8");
8427
9021
  const envPath = containerEnvPath(opts.name, home);
8428
9022
  const seedVars = {};
8429
9023
  for (const f of composed.features) {
@@ -8446,8 +9040,8 @@ async function runInit(opts) {
8446
9040
  }
8447
9041
  await ensureEnvVars(envPath, opts.name, seedVars);
8448
9042
  const documented = !anyComposed;
8449
- const ymlRel = path20.relative(home, dest);
8450
- const envRel = path20.relative(home, envPath);
9043
+ const ymlRel = path24.relative(home, dest);
9044
+ const envRel = path24.relative(home, envPath);
8451
9045
  if (documented) {
8452
9046
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
8453
9047
  logger.info(
@@ -8591,7 +9185,7 @@ var init_init = __esm({
8591
9185
 
8592
9186
  // src/commands/init.ts
8593
9187
  import { defineCommand as defineCommand11 } from "citty";
8594
- import { consola as consola15 } from "consola";
9188
+ import { consola as consola17 } from "consola";
8595
9189
  function collectListFlag(flag, rawArgs) {
8596
9190
  const eq = `${flag}=`;
8597
9191
  const pieces = [];
@@ -8717,7 +9311,7 @@ var init_init2 = __esm({
8717
9311
  ...ports && ports.length > 0 ? { withPorts: ports } : {}
8718
9312
  });
8719
9313
  } catch (err) {
8720
- consola15.error(err instanceof Error ? err.message : String(err));
9314
+ consola17.error(err instanceof Error ? err.message : String(err));
8721
9315
  process.exit(1);
8722
9316
  }
8723
9317
  }
@@ -8766,7 +9360,7 @@ var init_expand = __esm({
8766
9360
 
8767
9361
  // src/commands/list-components.ts
8768
9362
  import { defineCommand as defineCommand12 } from "citty";
8769
- import { consola as consola16 } from "consola";
9363
+ import { consola as consola18 } from "consola";
8770
9364
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
8771
9365
  var init_list_components = __esm({
8772
9366
  "src/commands/list-components.ts"() {
@@ -8795,7 +9389,7 @@ var init_list_components = __esm({
8795
9389
  try {
8796
9390
  const catalog = expandSelectable(await loadDescriptorCatalog());
8797
9391
  if (catalog.size === 0) {
8798
- consola16.warn(
9392
+ consola18.warn(
8799
9393
  "No components found. The workbench checkout looks incomplete."
8800
9394
  );
8801
9395
  process.exit(0);
@@ -8846,7 +9440,7 @@ var init_list_components = __esm({
8846
9440
  }
8847
9441
  process.exit(0);
8848
9442
  } catch (err) {
8849
- consola16.error(err instanceof Error ? err.message : String(err));
9443
+ consola18.error(err instanceof Error ? err.message : String(err));
8850
9444
  process.exit(1);
8851
9445
  }
8852
9446
  }
@@ -8855,14 +9449,14 @@ var init_list_components = __esm({
8855
9449
  });
8856
9450
 
8857
9451
  // src/commands/logs.ts
8858
- import { spawn as spawn8 } from "child_process";
8859
- import { existsSync as existsSync12 } from "fs";
8860
- import path21 from "path";
9452
+ import { spawn as spawn11 } from "child_process";
9453
+ import { existsSync as existsSync16 } from "fs";
9454
+ import path25 from "path";
8861
9455
  import { defineCommand as defineCommand13 } from "citty";
8862
9456
  function tailLogFile(file, follow) {
8863
9457
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
8864
9458
  return new Promise((resolve, reject) => {
8865
- const child = spawn8(cmd, args, { stdio: "inherit" });
9459
+ const child = spawn11(cmd, args, { stdio: "inherit" });
8866
9460
  child.on("error", reject);
8867
9461
  child.on("exit", (code) => resolve(code ?? 0));
8868
9462
  });
@@ -8900,8 +9494,8 @@ var init_logs = __esm({
8900
9494
  run({ args }) {
8901
9495
  const service = typeof args.service === "string" ? args.service : void 0;
8902
9496
  if (service) {
8903
- const logFile = path21.join(containerLogsDir(args.name), `${service}.log`);
8904
- if (existsSync12(logFile)) {
9497
+ const logFile = path25.join(containerLogsDir(args.name), `${service}.log`);
9498
+ if (existsSync16(logFile)) {
8905
9499
  return dispatch(() => tailLogFile(logFile, args.follow));
8906
9500
  }
8907
9501
  }
@@ -8917,12 +9511,45 @@ var init_logs = __esm({
8917
9511
  }
8918
9512
  });
8919
9513
 
8920
- // src/commands/port.ts
9514
+ // src/commands/open.ts
8921
9515
  import { defineCommand as defineCommand14 } from "citty";
8922
- import { consola as consola17 } from "consola";
9516
+ var openCommand;
9517
+ var init_open2 = __esm({
9518
+ "src/commands/open.ts"() {
9519
+ "use strict";
9520
+ init_open();
9521
+ init_dispatch();
9522
+ openCommand = defineCommand14({
9523
+ meta: {
9524
+ name: "open",
9525
+ group: "run",
9526
+ description: `Attach an editor to the named dev-container over SSH, or drop into a shell. Tools: ${OPEN_TOOLS.join(", ")}. The container must be applied (runtime >= 1.2.0) and running.`
9527
+ },
9528
+ args: {
9529
+ name: {
9530
+ type: "positional",
9531
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
9532
+ required: true
9533
+ },
9534
+ tool: {
9535
+ type: "positional",
9536
+ description: `What to open it in: ${OPEN_TOOLS.join(", ")}.`,
9537
+ required: true
9538
+ }
9539
+ },
9540
+ run({ args }) {
9541
+ return dispatch(() => runOpen({ name: args.name, tool: args.tool }));
9542
+ }
9543
+ });
9544
+ }
9545
+ });
9546
+
9547
+ // src/commands/port.ts
9548
+ import { defineCommand as defineCommand15 } from "citty";
9549
+ import { consola as consola19 } from "consola";
8923
9550
  async function runPortListing(opts) {
8924
9551
  const out = opts.out ?? process.stdout;
8925
- const info = opts.info ?? ((m) => consola17.info(m));
9552
+ const info = opts.info ?? ((m) => consola19.info(m));
8926
9553
  const parsed = await readConfig(
8927
9554
  containerConfigPath(opts.name, opts.monocerosHome)
8928
9555
  );
@@ -8980,7 +9607,7 @@ var init_port = __esm({
8980
9607
  init_schema();
8981
9608
  init_dynamic();
8982
9609
  init_format();
8983
- portCommand = defineCommand14({
9610
+ portCommand = defineCommand15({
8984
9611
  meta: {
8985
9612
  name: "port",
8986
9613
  group: "discovery",
@@ -8998,7 +9625,7 @@ var init_port = __esm({
8998
9625
  const code = await runPortListing({ name: args.name });
8999
9626
  process.exit(code);
9000
9627
  } catch (err) {
9001
- consola17.error(err instanceof Error ? err.message : String(err));
9628
+ consola19.error(err instanceof Error ? err.message : String(err));
9002
9629
  process.exit(1);
9003
9630
  }
9004
9631
  }
@@ -9007,15 +9634,15 @@ var init_port = __esm({
9007
9634
  });
9008
9635
 
9009
9636
  // src/commands/remove-apt-packages.ts
9010
- import { defineCommand as defineCommand15 } from "citty";
9011
- import { consola as consola18 } from "consola";
9637
+ import { defineCommand as defineCommand16 } from "citty";
9638
+ import { consola as consola20 } from "consola";
9012
9639
  var removeAptPackagesCommand;
9013
9640
  var init_remove_apt_packages = __esm({
9014
9641
  "src/commands/remove-apt-packages.ts"() {
9015
9642
  "use strict";
9016
9643
  init_inner_args();
9017
9644
  init_modify();
9018
- removeAptPackagesCommand = defineCommand15({
9645
+ removeAptPackagesCommand = defineCommand16({
9019
9646
  meta: {
9020
9647
  name: "remove-apt-packages",
9021
9648
  group: "edit",
@@ -9037,7 +9664,7 @@ var init_remove_apt_packages = __esm({
9037
9664
  async run({ args }) {
9038
9665
  const packages = [...getInnerArgs()];
9039
9666
  if (packages.length === 0) {
9040
- consola18.error(
9667
+ consola20.error(
9041
9668
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
9042
9669
  );
9043
9670
  process.exit(1);
@@ -9050,7 +9677,7 @@ var init_remove_apt_packages = __esm({
9050
9677
  });
9051
9678
  process.exit(result.status === "aborted" ? 1 : 0);
9052
9679
  } catch (err) {
9053
- consola18.error(err instanceof Error ? err.message : String(err));
9680
+ consola20.error(err instanceof Error ? err.message : String(err));
9054
9681
  process.exit(1);
9055
9682
  }
9056
9683
  }
@@ -9059,14 +9686,14 @@ var init_remove_apt_packages = __esm({
9059
9686
  });
9060
9687
 
9061
9688
  // src/commands/remove-feature.ts
9062
- import { defineCommand as defineCommand16 } from "citty";
9063
- import { consola as consola19 } from "consola";
9689
+ import { defineCommand as defineCommand17 } from "citty";
9690
+ import { consola as consola21 } from "consola";
9064
9691
  var removeFeatureCommand;
9065
9692
  var init_remove_feature = __esm({
9066
9693
  "src/commands/remove-feature.ts"() {
9067
9694
  "use strict";
9068
9695
  init_modify();
9069
- removeFeatureCommand = defineCommand16({
9696
+ removeFeatureCommand = defineCommand17({
9070
9697
  meta: {
9071
9698
  name: "remove-feature",
9072
9699
  group: "edit",
@@ -9099,7 +9726,7 @@ var init_remove_feature = __esm({
9099
9726
  });
9100
9727
  process.exit(result.status === "aborted" ? 1 : 0);
9101
9728
  } catch (err) {
9102
- consola19.error(err instanceof Error ? err.message : String(err));
9729
+ consola21.error(err instanceof Error ? err.message : String(err));
9103
9730
  process.exit(1);
9104
9731
  }
9105
9732
  }
@@ -9108,15 +9735,15 @@ var init_remove_feature = __esm({
9108
9735
  });
9109
9736
 
9110
9737
  // src/remove/index.ts
9111
- import { existsSync as existsSync13, promises as fs15 } from "fs";
9112
- import path22 from "path";
9113
- import { consola as consola20 } from "consola";
9738
+ import { existsSync as existsSync17, promises as fs16 } from "fs";
9739
+ import path26 from "path";
9740
+ import { consola as consola22 } from "consola";
9114
9741
  async function runRemove(opts) {
9115
9742
  const home = opts.monocerosHome ?? monocerosHome();
9116
9743
  const logger = opts.logger ?? {
9117
- info: (msg) => consola20.info(msg),
9118
- success: (msg) => consola20.success(msg),
9119
- warn: (msg) => consola20.warn(msg)
9744
+ info: (msg) => consola22.info(msg),
9745
+ success: (msg) => consola22.success(msg),
9746
+ warn: (msg) => consola22.warn(msg)
9120
9747
  };
9121
9748
  if (!REGEX.solutionName.test(opts.name)) {
9122
9749
  throw new Error(
@@ -9126,9 +9753,9 @@ async function runRemove(opts) {
9126
9753
  const ymlPath = containerConfigPath(opts.name, home);
9127
9754
  const envPath = containerEnvPath(opts.name, home);
9128
9755
  const containerPath = containerDir(opts.name, home);
9129
- const hasYml = existsSync13(ymlPath);
9130
- const hasEnv = existsSync13(envPath);
9131
- const hasContainer = existsSync13(containerPath);
9756
+ const hasYml = existsSync17(ymlPath);
9757
+ const hasEnv = existsSync17(envPath);
9758
+ const hasContainer = existsSync17(containerPath);
9132
9759
  if (!hasYml && !hasContainer) {
9133
9760
  throw new Error(
9134
9761
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -9149,35 +9776,35 @@ async function runRemove(opts) {
9149
9776
  logger,
9150
9777
  exec: dockerExec
9151
9778
  });
9152
- const ideVolumes = ideStateVolumes(opts.name).map((v) => v.volume);
9779
+ const ideVolumes = ideStateVolumes(opts.name).filter((v) => !v.shared).map((v) => v.volume);
9153
9780
  await dockerExec(["volume", "rm", "-f", ...ideVolumes]);
9154
9781
  let backupPath = null;
9155
9782
  if (!opts.noBackup && (hasYml || hasContainer)) {
9156
9783
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9157
- backupPath = path22.join(home, "container-backups", `${opts.name}-${ts}`);
9158
- await fs15.mkdir(backupPath, { recursive: true });
9784
+ backupPath = path26.join(home, "container-backups", `${opts.name}-${ts}`);
9785
+ await fs16.mkdir(backupPath, { recursive: true });
9159
9786
  if (hasYml) {
9160
- await fs15.copyFile(ymlPath, path22.join(backupPath, `${opts.name}.yml`));
9787
+ await fs16.copyFile(ymlPath, path26.join(backupPath, `${opts.name}.yml`));
9161
9788
  }
9162
9789
  if (hasEnv) {
9163
- await fs15.copyFile(envPath, path22.join(backupPath, `${opts.name}.env`));
9790
+ await fs16.copyFile(envPath, path26.join(backupPath, `${opts.name}.env`));
9164
9791
  }
9165
9792
  if (hasContainer) {
9166
- await fs15.cp(containerPath, path22.join(backupPath, "container"), {
9793
+ await fs16.cp(containerPath, path26.join(backupPath, "container"), {
9167
9794
  recursive: true
9168
9795
  });
9169
9796
  }
9170
9797
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
9171
9798
  }
9172
9799
  if (hasYml) {
9173
- await fs15.rm(ymlPath, { force: true });
9800
+ await fs16.rm(ymlPath, { force: true });
9174
9801
  }
9175
9802
  if (hasEnv) {
9176
- await fs15.rm(envPath, { force: true });
9803
+ await fs16.rm(envPath, { force: true });
9177
9804
  }
9178
9805
  if (hasContainer) {
9179
9806
  try {
9180
- await fs15.rm(containerPath, { recursive: true, force: true });
9807
+ await fs16.rm(containerPath, { recursive: true, force: true });
9181
9808
  } catch (err) {
9182
9809
  const code = err.code;
9183
9810
  if (code !== "EACCES" && code !== "EPERM") {
@@ -9199,7 +9826,7 @@ async function runRemove(opts) {
9199
9826
  "-delete"
9200
9827
  ]);
9201
9828
  try {
9202
- await fs15.rm(containerPath, { recursive: true, force: true });
9829
+ await fs16.rm(containerPath, { recursive: true, force: true });
9203
9830
  } catch (err2) {
9204
9831
  const code2 = err2.code;
9205
9832
  throw new Error(
@@ -9213,7 +9840,14 @@ async function runRemove(opts) {
9213
9840
  );
9214
9841
  if (!backupPath) {
9215
9842
  logger.warn?.(
9216
- "No backup created (--no-backup). The host-side state is gone for good."
9843
+ "No backup created (--no-backup). The host-side state is gone for good."
9844
+ );
9845
+ }
9846
+ try {
9847
+ await removeSshAttach(home, opts.name);
9848
+ } catch (err) {
9849
+ logger.warn?.(
9850
+ `Could not remove SSH attach config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
9217
9851
  );
9218
9852
  }
9219
9853
  try {
@@ -9248,21 +9882,22 @@ var init_remove = __esm({
9248
9882
  init_schema();
9249
9883
  init_compose();
9250
9884
  init_scaffold();
9885
+ init_ssh_attach();
9251
9886
  init_proxy();
9252
9887
  init_dynamic();
9253
9888
  }
9254
9889
  });
9255
9890
 
9256
9891
  // src/commands/remove.ts
9257
- import { defineCommand as defineCommand17 } from "citty";
9258
- import { consola as consola21 } from "consola";
9892
+ import { defineCommand as defineCommand18 } from "citty";
9893
+ import { consola as consola23 } from "consola";
9259
9894
  import { createInterface } from "readline/promises";
9260
9895
  var removeCommand;
9261
9896
  var init_remove2 = __esm({
9262
9897
  "src/commands/remove.ts"() {
9263
9898
  "use strict";
9264
9899
  init_remove();
9265
- removeCommand = defineCommand17({
9900
+ removeCommand = defineCommand18({
9266
9901
  meta: {
9267
9902
  name: "remove",
9268
9903
  group: "lifecycle",
@@ -9299,7 +9934,7 @@ var init_remove2 = __esm({
9299
9934
  const skipPrompt = args.yes === true;
9300
9935
  if (!skipPrompt) {
9301
9936
  const warning = noBackup ? `About to remove '${args.name}' WITHOUT a backup. Docker objects, container-configs entry, and container directory will all be deleted.` : `About to remove '${args.name}'. A backup will be written to container-backups/ first, then docker objects, container-configs entry, and container directory will all be deleted.`;
9302
- consola21.warn(warning);
9937
+ consola23.warn(warning);
9303
9938
  const rl = createInterface({
9304
9939
  input: process.stdin,
9305
9940
  output: process.stdout
@@ -9307,7 +9942,7 @@ var init_remove2 = __esm({
9307
9942
  const answer = await rl.question("Continue? [y/N] ");
9308
9943
  rl.close();
9309
9944
  if (!/^y(es)?$/i.test(answer.trim())) {
9310
- consola21.info("Aborted. Nothing changed.");
9945
+ consola23.info("Aborted. Nothing changed.");
9311
9946
  process.exit(0);
9312
9947
  }
9313
9948
  }
@@ -9316,7 +9951,7 @@ var init_remove2 = __esm({
9316
9951
  ...noBackup ? { noBackup: true } : {}
9317
9952
  });
9318
9953
  } catch (err) {
9319
- consola21.error(err instanceof Error ? err.message : String(err));
9954
+ consola23.error(err instanceof Error ? err.message : String(err));
9320
9955
  process.exit(1);
9321
9956
  }
9322
9957
  }
@@ -9325,24 +9960,24 @@ var init_remove2 = __esm({
9325
9960
  });
9326
9961
 
9327
9962
  // src/restore/index.ts
9328
- import { existsSync as existsSync14, promises as fs16 } from "fs";
9329
- import path23 from "path";
9330
- import { consola as consola22 } from "consola";
9963
+ import { existsSync as existsSync18, promises as fs17 } from "fs";
9964
+ import path27 from "path";
9965
+ import { consola as consola24 } from "consola";
9331
9966
  async function runRestore(opts) {
9332
9967
  const home = opts.monocerosHome ?? monocerosHome();
9333
9968
  const logger = opts.logger ?? {
9334
- info: (msg) => consola22.info(msg),
9335
- success: (msg) => consola22.success(msg)
9969
+ info: (msg) => consola24.info(msg),
9970
+ success: (msg) => consola24.success(msg)
9336
9971
  };
9337
- const backup = path23.resolve(opts.backupPath);
9338
- if (!existsSync14(backup)) {
9972
+ const backup = path27.resolve(opts.backupPath);
9973
+ if (!existsSync18(backup)) {
9339
9974
  throw new Error(`Backup not found: ${backup}.`);
9340
9975
  }
9341
- const stat = await fs16.stat(backup);
9976
+ const stat = await fs17.stat(backup);
9342
9977
  if (!stat.isDirectory()) {
9343
9978
  throw new Error(`Backup path is not a directory: ${backup}.`);
9344
9979
  }
9345
- const entries = await fs16.readdir(backup);
9980
+ const entries = await fs17.readdir(backup);
9346
9981
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
9347
9982
  if (ymlFiles.length === 0) {
9348
9983
  throw new Error(
@@ -9356,29 +9991,29 @@ async function runRestore(opts) {
9356
9991
  }
9357
9992
  const ymlFile = ymlFiles[0];
9358
9993
  const name = ymlFile.replace(/\.yml$/, "");
9359
- const containerInBackup = path23.join(backup, "container");
9360
- const hasContainer = existsSync14(containerInBackup);
9361
- const envInBackup = path23.join(backup, `${name}.env`);
9362
- const hasEnv = existsSync14(envInBackup);
9994
+ const containerInBackup = path27.join(backup, "container");
9995
+ const hasContainer = existsSync18(containerInBackup);
9996
+ const envInBackup = path27.join(backup, `${name}.env`);
9997
+ const hasEnv = existsSync18(envInBackup);
9363
9998
  const destYml = containerConfigPath(name, home);
9364
9999
  const destContainer = containerDir(name, home);
9365
- if (existsSync14(destYml)) {
10000
+ if (existsSync18(destYml)) {
9366
10001
  throw new Error(
9367
10002
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
9368
10003
  );
9369
10004
  }
9370
- if (hasContainer && existsSync14(destContainer)) {
10005
+ if (hasContainer && existsSync18(destContainer)) {
9371
10006
  throw new Error(
9372
10007
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
9373
10008
  );
9374
10009
  }
9375
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
9376
- await fs16.copyFile(path23.join(backup, ymlFile), destYml);
10010
+ await fs17.mkdir(containerConfigsDir(home), { recursive: true });
10011
+ await fs17.copyFile(path27.join(backup, ymlFile), destYml);
9377
10012
  if (hasEnv) {
9378
- await fs16.copyFile(envInBackup, containerEnvPath(name, home));
10013
+ await fs17.copyFile(envInBackup, containerEnvPath(name, home));
9379
10014
  }
9380
10015
  if (hasContainer) {
9381
- await fs16.cp(containerInBackup, destContainer, { recursive: true });
10016
+ await fs17.cp(containerInBackup, destContainer, { recursive: true });
9382
10017
  }
9383
10018
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
9384
10019
  logger.info(
@@ -9398,14 +10033,14 @@ var init_restore = __esm({
9398
10033
  });
9399
10034
 
9400
10035
  // src/commands/restore.ts
9401
- import { defineCommand as defineCommand18 } from "citty";
9402
- import { consola as consola23 } from "consola";
10036
+ import { defineCommand as defineCommand19 } from "citty";
10037
+ import { consola as consola25 } from "consola";
9403
10038
  var restoreCommand;
9404
10039
  var init_restore2 = __esm({
9405
10040
  "src/commands/restore.ts"() {
9406
10041
  "use strict";
9407
10042
  init_restore();
9408
- restoreCommand = defineCommand18({
10043
+ restoreCommand = defineCommand19({
9409
10044
  meta: {
9410
10045
  name: "restore",
9411
10046
  group: "lifecycle",
@@ -9422,7 +10057,7 @@ var init_restore2 = __esm({
9422
10057
  try {
9423
10058
  await runRestore({ backupPath: args["backup-path"] });
9424
10059
  } catch (err) {
9425
- consola23.error(err instanceof Error ? err.message : String(err));
10060
+ consola25.error(err instanceof Error ? err.message : String(err));
9426
10061
  process.exit(1);
9427
10062
  }
9428
10063
  }
@@ -9431,14 +10066,14 @@ var init_restore2 = __esm({
9431
10066
  });
9432
10067
 
9433
10068
  // src/commands/remove-from-url.ts
9434
- import { defineCommand as defineCommand19 } from "citty";
9435
- import { consola as consola24 } from "consola";
10069
+ import { defineCommand as defineCommand20 } from "citty";
10070
+ import { consola as consola26 } from "consola";
9436
10071
  var removeFromUrlCommand;
9437
10072
  var init_remove_from_url = __esm({
9438
10073
  "src/commands/remove-from-url.ts"() {
9439
10074
  "use strict";
9440
10075
  init_modify();
9441
- removeFromUrlCommand = defineCommand19({
10076
+ removeFromUrlCommand = defineCommand20({
9442
10077
  meta: {
9443
10078
  name: "remove-from-url",
9444
10079
  group: "edit",
@@ -9471,7 +10106,7 @@ var init_remove_from_url = __esm({
9471
10106
  });
9472
10107
  process.exit(result.status === "aborted" ? 1 : 0);
9473
10108
  } catch (err) {
9474
- consola24.error(err instanceof Error ? err.message : String(err));
10109
+ consola26.error(err instanceof Error ? err.message : String(err));
9475
10110
  process.exit(1);
9476
10111
  }
9477
10112
  }
@@ -9480,14 +10115,14 @@ var init_remove_from_url = __esm({
9480
10115
  });
9481
10116
 
9482
10117
  // src/commands/remove-language.ts
9483
- import { defineCommand as defineCommand20 } from "citty";
9484
- import { consola as consola25 } from "consola";
10118
+ import { defineCommand as defineCommand21 } from "citty";
10119
+ import { consola as consola27 } from "consola";
9485
10120
  var removeLanguageCommand;
9486
10121
  var init_remove_language = __esm({
9487
10122
  "src/commands/remove-language.ts"() {
9488
10123
  "use strict";
9489
10124
  init_modify();
9490
- removeLanguageCommand = defineCommand20({
10125
+ removeLanguageCommand = defineCommand21({
9491
10126
  meta: {
9492
10127
  name: "remove-language",
9493
10128
  group: "edit",
@@ -9520,7 +10155,7 @@ var init_remove_language = __esm({
9520
10155
  });
9521
10156
  process.exit(result.status === "aborted" ? 1 : 0);
9522
10157
  } catch (err) {
9523
- consola25.error(err instanceof Error ? err.message : String(err));
10158
+ consola27.error(err instanceof Error ? err.message : String(err));
9524
10159
  process.exit(1);
9525
10160
  }
9526
10161
  }
@@ -9529,8 +10164,8 @@ var init_remove_language = __esm({
9529
10164
  });
9530
10165
 
9531
10166
  // src/commands/remove-port.ts
9532
- import { defineCommand as defineCommand21 } from "citty";
9533
- import { consola as consola26 } from "consola";
10167
+ import { defineCommand as defineCommand22 } from "citty";
10168
+ import { consola as consola28 } from "consola";
9534
10169
  function coerceToken2(raw) {
9535
10170
  const n = Number(raw);
9536
10171
  return Number.isFinite(n) ? n : raw;
@@ -9541,7 +10176,7 @@ var init_remove_port = __esm({
9541
10176
  "use strict";
9542
10177
  init_inner_args();
9543
10178
  init_modify();
9544
- removePortCommand = defineCommand21({
10179
+ removePortCommand = defineCommand22({
9545
10180
  meta: {
9546
10181
  name: "remove-port",
9547
10182
  group: "edit",
@@ -9563,7 +10198,7 @@ var init_remove_port = __esm({
9563
10198
  async run({ args }) {
9564
10199
  const tokens = [...getInnerArgs()];
9565
10200
  if (tokens.length === 0) {
9566
- consola26.error(
10201
+ consola28.error(
9567
10202
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
9568
10203
  );
9569
10204
  process.exit(1);
@@ -9576,7 +10211,7 @@ var init_remove_port = __esm({
9576
10211
  });
9577
10212
  process.exit(result.status === "aborted" ? 1 : 0);
9578
10213
  } catch (err) {
9579
- consola26.error(err instanceof Error ? err.message : String(err));
10214
+ consola28.error(err instanceof Error ? err.message : String(err));
9580
10215
  process.exit(1);
9581
10216
  }
9582
10217
  }
@@ -9585,14 +10220,14 @@ var init_remove_port = __esm({
9585
10220
  });
9586
10221
 
9587
10222
  // src/commands/remove-repo.ts
9588
- import { defineCommand as defineCommand22 } from "citty";
9589
- import { consola as consola27 } from "consola";
10223
+ import { defineCommand as defineCommand23 } from "citty";
10224
+ import { consola as consola29 } from "consola";
9590
10225
  var removeRepoCommand;
9591
10226
  var init_remove_repo = __esm({
9592
10227
  "src/commands/remove-repo.ts"() {
9593
10228
  "use strict";
9594
10229
  init_modify();
9595
- removeRepoCommand = defineCommand22({
10230
+ removeRepoCommand = defineCommand23({
9596
10231
  meta: {
9597
10232
  name: "remove-repo",
9598
10233
  group: "edit",
@@ -9625,7 +10260,7 @@ var init_remove_repo = __esm({
9625
10260
  });
9626
10261
  process.exit(result.status === "aborted" ? 1 : 0);
9627
10262
  } catch (err) {
9628
- consola27.error(err instanceof Error ? err.message : String(err));
10263
+ consola29.error(err instanceof Error ? err.message : String(err));
9629
10264
  process.exit(1);
9630
10265
  }
9631
10266
  }
@@ -9634,14 +10269,14 @@ var init_remove_repo = __esm({
9634
10269
  });
9635
10270
 
9636
10271
  // src/commands/remove-service.ts
9637
- import { defineCommand as defineCommand23 } from "citty";
9638
- import { consola as consola28 } from "consola";
10272
+ import { defineCommand as defineCommand24 } from "citty";
10273
+ import { consola as consola30 } from "consola";
9639
10274
  var removeServiceCommand;
9640
10275
  var init_remove_service = __esm({
9641
10276
  "src/commands/remove-service.ts"() {
9642
10277
  "use strict";
9643
10278
  init_modify();
9644
- removeServiceCommand = defineCommand23({
10279
+ removeServiceCommand = defineCommand24({
9645
10280
  meta: {
9646
10281
  name: "remove-service",
9647
10282
  group: "edit",
@@ -9674,7 +10309,7 @@ var init_remove_service = __esm({
9674
10309
  });
9675
10310
  process.exit(result.status === "aborted" ? 1 : 0);
9676
10311
  } catch (err) {
9677
- consola28.error(err instanceof Error ? err.message : String(err));
10312
+ consola30.error(err instanceof Error ? err.message : String(err));
9678
10313
  process.exit(1);
9679
10314
  }
9680
10315
  }
@@ -9682,148 +10317,17 @@ var init_remove_service = __esm({
9682
10317
  }
9683
10318
  });
9684
10319
 
9685
- // src/devcontainer/browser-bridge.ts
9686
- import { spawn as spawn9 } from "child_process";
9687
- import { existsSync as existsSync15, promises as fsp5, readFileSync as readFileSync5 } from "fs";
9688
- import http from "http";
9689
- import path24 from "path";
9690
- function parseCallbackTarget(authUrl) {
9691
- try {
9692
- const u = new URL(authUrl);
9693
- const redirect = u.searchParams.get("redirect_uri");
9694
- if (!redirect) return null;
9695
- const r = new URL(redirect);
9696
- if (r.hostname !== "localhost" && r.hostname !== "127.0.0.1") return null;
9697
- const port = Number(r.port);
9698
- if (!Number.isInteger(port) || port <= 0) return null;
9699
- return { port, pathname: r.pathname };
9700
- } catch {
9701
- return null;
9702
- }
9703
- }
9704
- function nextRelayUrl(content, lastOpened) {
9705
- const url = content.trim();
9706
- return url && url !== lastOpened ? url : null;
9707
- }
9708
- function wrapExec(command, opts) {
9709
- const leading = [];
9710
- const stmts = [];
9711
- if (opts.pathPrepend) {
9712
- leading.push(opts.pathPrepend);
9713
- const i = leading.length;
9714
- stmts.push(`export PATH="$${i}:$PATH"`);
9715
- stmts.push(`export BROWSER="$${i}/xdg-open"`);
9716
- }
9717
- if (opts.cwd) {
9718
- leading.push(opts.cwd);
9719
- stmts.push(`cd -- "$${leading.length}"`);
9720
- }
9721
- if (leading.length === 0) return command;
9722
- const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
9723
- const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
9724
- return ["bash", "-lc", script, "bash", ...leading, ...command];
9725
- }
9726
- function openInBrowser(url) {
9727
- const platform = process.platform;
9728
- const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
9729
- try {
9730
- const child = spawn9(cmd, args, {
9731
- stdio: "ignore",
9732
- detached: true
9733
- });
9734
- child.on("error", () => {
9735
- });
9736
- child.unref();
9737
- } catch {
9738
- }
9739
- }
9740
- async function startBrowserBridge(opts) {
9741
- const relayDir = path24.join(opts.root, RELAY_DIRNAME);
9742
- const relayScript = path24.join(relayDir, "xdg-open");
9743
- const urlFile = path24.join(relayDir, "url");
9744
- await fsp5.mkdir(relayDir, { recursive: true });
9745
- await fsp5.rm(urlFile, { force: true });
9746
- await fsp5.writeFile(
9747
- relayScript,
9748
- `#!/bin/sh
9749
- printf '%s\\n' "$1" > "$(dirname "$0")/url"
9750
- exit 0
9751
- `,
9752
- { mode: 493 }
9753
- );
9754
- await fsp5.chmod(relayScript, 493);
9755
- const servers = [];
9756
- let lastOpened = null;
9757
- const onUrl = (url) => {
9758
- openInBrowser(url);
9759
- const target = parseCallbackTarget(url);
9760
- if (!target) return;
9761
- const server = http.createServer((req, res) => {
9762
- const reqUrl = req.url ?? target.pathname;
9763
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
9764
- res.end(
9765
- '<html><body style="font-family:sans-serif;padding:3rem">You are signed in. You can close this tab and return to the terminal.</body></html>'
9766
- );
9767
- void opts.spawn(
9768
- [
9769
- "exec",
9770
- "--workspace-folder",
9771
- opts.root,
9772
- "--mount-workspace-git-root=false",
9773
- "curl",
9774
- "-fsS",
9775
- `http://localhost:${target.port}${reqUrl}`
9776
- ],
9777
- opts.root,
9778
- { quiet: true }
9779
- );
9780
- });
9781
- server.on("error", () => {
9782
- });
9783
- server.listen(target.port, "127.0.0.1");
9784
- servers.push(server);
9785
- };
9786
- const poll = setInterval(() => {
9787
- if (!existsSync15(urlFile)) return;
9788
- let content = "";
9789
- try {
9790
- content = readFileSync5(urlFile, "utf8");
9791
- } catch {
9792
- return;
9793
- }
9794
- const url = nextRelayUrl(content, lastOpened);
9795
- if (!url) return;
9796
- lastOpened = url;
9797
- onUrl(url);
9798
- }, 250);
9799
- return {
9800
- relayDirInContainer: `/workspaces/${opts.name}/${RELAY_DIRNAME}`,
9801
- async dispose() {
9802
- clearInterval(poll);
9803
- for (const s of servers) s.close();
9804
- await fsp5.rm(relayDir, { recursive: true, force: true });
9805
- }
9806
- };
9807
- }
9808
- var RELAY_DIRNAME;
9809
- var init_browser_bridge = __esm({
9810
- "src/devcontainer/browser-bridge.ts"() {
9811
- "use strict";
9812
- RELAY_DIRNAME = ".monoceros-bridge";
9813
- }
9814
- });
9815
-
9816
10320
  // src/devcontainer/claude-trust.ts
9817
- import { existsSync as existsSync16, promises as fsp6 } from "fs";
9818
- import path25 from "path";
10321
+ import { existsSync as existsSync19, promises as fsp6 } from "fs";
10322
+ import path28 from "path";
9819
10323
  function resolveContainerCwd(name, cwd) {
9820
10324
  const workspace = `/workspaces/${name}`;
9821
10325
  if (!cwd) return workspace;
9822
- return path25.posix.isAbsolute(cwd) ? cwd : path25.posix.join(workspace, cwd);
10326
+ return path28.posix.isAbsolute(cwd) ? cwd : path28.posix.join(workspace, cwd);
9823
10327
  }
9824
10328
  async function preApproveClaudeProject(opts) {
9825
- const file = path25.join(opts.root, "home", ".claude.json");
9826
- if (!existsSync16(file)) return;
10329
+ const file = path28.join(opts.root, "home", ".claude.json");
10330
+ if (!existsSync19(file)) return;
9827
10331
  try {
9828
10332
  const raw = await fsp6.readFile(file, "utf8");
9829
10333
  const config = raw.trim() ? JSON.parse(raw) : {};
@@ -9856,56 +10360,6 @@ var init_claude_trust = __esm({
9856
10360
  }
9857
10361
  });
9858
10362
 
9859
- // src/devcontainer/shell.ts
9860
- import { existsSync as existsSync17 } from "fs";
9861
- import path26 from "path";
9862
- async function runShell(opts) {
9863
- assertContainerExists(opts.root);
9864
- const spawnFn = opts.spawn ?? spawnDevcontainer;
9865
- const upCode = await spawnFn(
9866
- ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
9867
- opts.root,
9868
- { quiet: true }
9869
- );
9870
- if (upCode !== 0) return upCode;
9871
- const name = opts.name ?? path26.basename(opts.root);
9872
- const isTty2 = opts.isTty ?? process.stdout.isTTY;
9873
- const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
9874
- try {
9875
- const innerExec = wrapExec(
9876
- ["bash"],
9877
- bridge ? { pathPrepend: bridge.relayDirInContainer } : {}
9878
- );
9879
- return await spawnFn(
9880
- [
9881
- "exec",
9882
- "--workspace-folder",
9883
- opts.root,
9884
- "--mount-workspace-git-root=false",
9885
- ...innerExec
9886
- ],
9887
- opts.root,
9888
- { interactive: true }
9889
- );
9890
- } finally {
9891
- if (bridge) await bridge.dispose();
9892
- }
9893
- }
9894
- function assertContainerExists(root) {
9895
- if (!existsSync17(path26.join(root, ".devcontainer"))) {
9896
- throw new Error(
9897
- `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
9898
- );
9899
- }
9900
- }
9901
- var init_shell = __esm({
9902
- "src/devcontainer/shell.ts"() {
9903
- "use strict";
9904
- init_browser_bridge();
9905
- init_cli();
9906
- }
9907
- });
9908
-
9909
10363
  // src/devcontainer/run.ts
9910
10364
  async function runInContainer(opts) {
9911
10365
  if (opts.command.length === 0) {
@@ -9964,8 +10418,8 @@ var init_run = __esm({
9964
10418
  });
9965
10419
 
9966
10420
  // src/commands/run.ts
9967
- import { defineCommand as defineCommand24 } from "citty";
9968
- import { consola as consola29 } from "consola";
10421
+ import { defineCommand as defineCommand25 } from "citty";
10422
+ import { consola as consola31 } from "consola";
9969
10423
  var runCommand;
9970
10424
  var init_run2 = __esm({
9971
10425
  "src/commands/run.ts"() {
@@ -9973,7 +10427,7 @@ var init_run2 = __esm({
9973
10427
  init_paths();
9974
10428
  init_run();
9975
10429
  init_inner_args();
9976
- runCommand = defineCommand24({
10430
+ runCommand = defineCommand25({
9977
10431
  meta: {
9978
10432
  name: "run",
9979
10433
  group: "run",
@@ -9993,7 +10447,7 @@ var init_run2 = __esm({
9993
10447
  async run({ args }) {
9994
10448
  const command = [...getInnerArgs()];
9995
10449
  if (command.length === 0) {
9996
- consola29.error(
10450
+ consola31.error(
9997
10451
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
9998
10452
  );
9999
10453
  process.exit(1);
@@ -10007,7 +10461,7 @@ var init_run2 = __esm({
10007
10461
  });
10008
10462
  process.exit(exitCode);
10009
10463
  } catch (err) {
10010
- consola29.error(err instanceof Error ? err.message : String(err));
10464
+ consola31.error(err instanceof Error ? err.message : String(err));
10011
10465
  process.exit(1);
10012
10466
  }
10013
10467
  }
@@ -10016,15 +10470,15 @@ var init_run2 = __esm({
10016
10470
  });
10017
10471
 
10018
10472
  // src/commands/shell.ts
10019
- import { defineCommand as defineCommand25 } from "citty";
10020
- import { consola as consola30 } from "consola";
10473
+ import { defineCommand as defineCommand26 } from "citty";
10474
+ import { consola as consola32 } from "consola";
10021
10475
  var shellCommand;
10022
10476
  var init_shell2 = __esm({
10023
10477
  "src/commands/shell.ts"() {
10024
10478
  "use strict";
10025
10479
  init_paths();
10026
10480
  init_shell();
10027
- shellCommand = defineCommand25({
10481
+ shellCommand = defineCommand26({
10028
10482
  meta: {
10029
10483
  name: "shell",
10030
10484
  group: "run",
@@ -10045,7 +10499,7 @@ var init_shell2 = __esm({
10045
10499
  });
10046
10500
  process.exit(exitCode);
10047
10501
  } catch (err) {
10048
- consola30.error(err instanceof Error ? err.message : String(err));
10502
+ consola32.error(err instanceof Error ? err.message : String(err));
10049
10503
  process.exit(1);
10050
10504
  }
10051
10505
  }
@@ -10054,8 +10508,8 @@ var init_shell2 = __esm({
10054
10508
  });
10055
10509
 
10056
10510
  // src/commands/start.ts
10057
- import { defineCommand as defineCommand26 } from "citty";
10058
- import { consola as consola31 } from "consola";
10511
+ import { defineCommand as defineCommand27 } from "citty";
10512
+ import { consola as consola33 } from "consola";
10059
10513
  var startCommand;
10060
10514
  var init_start = __esm({
10061
10515
  "src/commands/start.ts"() {
@@ -10064,10 +10518,11 @@ var init_start = __esm({
10064
10518
  init_io();
10065
10519
  init_paths();
10066
10520
  init_compose();
10521
+ init_open();
10067
10522
  init_proxy();
10068
10523
  init_port_check();
10069
10524
  init_dispatch();
10070
- startCommand = defineCommand26({
10525
+ startCommand = defineCommand27({
10071
10526
  meta: {
10072
10527
  name: "start",
10073
10528
  group: "run",
@@ -10078,6 +10533,10 @@ var init_start = __esm({
10078
10533
  type: "positional",
10079
10534
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10080
10535
  required: true
10536
+ },
10537
+ open: {
10538
+ type: "string",
10539
+ description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
10081
10540
  }
10082
10541
  },
10083
10542
  run({ args }) {
@@ -10092,7 +10551,7 @@ var init_start = __esm({
10092
10551
  hostPort = proxyHostPort(global);
10093
10552
  }
10094
10553
  } catch (err) {
10095
- consola31.warn(
10554
+ consola33.warn(
10096
10555
  `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
10097
10556
  );
10098
10557
  }
@@ -10100,7 +10559,15 @@ var init_start = __esm({
10100
10559
  await preflightHostPort(hostPort);
10101
10560
  await ensureProxy({ hostPort });
10102
10561
  }
10103
- return runStart({ root: containerDir(args.name) });
10562
+ const exitCode = await runStart({ root: containerDir(args.name) });
10563
+ if (args.open && exitCode === 0) {
10564
+ try {
10565
+ await runOpen({ name: args.name, tool: args.open });
10566
+ } catch (err) {
10567
+ consola33.warn(err instanceof Error ? err.message : String(err));
10568
+ }
10569
+ }
10570
+ return exitCode;
10104
10571
  });
10105
10572
  }
10106
10573
  });
@@ -10108,7 +10575,7 @@ var init_start = __esm({
10108
10575
  });
10109
10576
 
10110
10577
  // src/commands/status.ts
10111
- import { defineCommand as defineCommand27 } from "citty";
10578
+ import { defineCommand as defineCommand28 } from "citty";
10112
10579
  var statusCommand;
10113
10580
  var init_status = __esm({
10114
10581
  "src/commands/status.ts"() {
@@ -10116,7 +10583,7 @@ var init_status = __esm({
10116
10583
  init_paths();
10117
10584
  init_compose();
10118
10585
  init_dispatch();
10119
- statusCommand = defineCommand27({
10586
+ statusCommand = defineCommand28({
10120
10587
  meta: {
10121
10588
  name: "status",
10122
10589
  group: "run",
@@ -10146,8 +10613,8 @@ var init_status = __esm({
10146
10613
  });
10147
10614
 
10148
10615
  // src/commands/stop.ts
10149
- import { defineCommand as defineCommand28 } from "citty";
10150
- import { consola as consola32 } from "consola";
10616
+ import { defineCommand as defineCommand29 } from "citty";
10617
+ import { consola as consola34 } from "consola";
10151
10618
  var stopCommand;
10152
10619
  var init_stop = __esm({
10153
10620
  "src/commands/stop.ts"() {
@@ -10156,7 +10623,7 @@ var init_stop = __esm({
10156
10623
  init_compose();
10157
10624
  init_proxy();
10158
10625
  init_dispatch();
10159
- stopCommand = defineCommand28({
10626
+ stopCommand = defineCommand29({
10160
10627
  meta: {
10161
10628
  name: "stop",
10162
10629
  group: "run",
@@ -10181,10 +10648,10 @@ var init_stop = __esm({
10181
10648
  });
10182
10649
  try {
10183
10650
  await maybeStopProxy({
10184
- logger: { info: (msg) => consola32.info(msg) }
10651
+ logger: { info: (msg) => consola34.info(msg) }
10185
10652
  });
10186
10653
  } catch (err) {
10187
- consola32.warn(
10654
+ consola34.warn(
10188
10655
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
10189
10656
  );
10190
10657
  }
@@ -10196,11 +10663,11 @@ var init_stop = __esm({
10196
10663
  });
10197
10664
 
10198
10665
  // src/tunnel/resolve.ts
10199
- import { existsSync as existsSync18 } from "fs";
10200
- import path27 from "path";
10666
+ import { existsSync as existsSync20 } from "fs";
10667
+ import path29 from "path";
10201
10668
  async function resolveTunnelTarget(opts) {
10202
10669
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
10203
- if (!existsSync18(ymlPath)) {
10670
+ if (!existsSync20(ymlPath)) {
10204
10671
  throw new Error(
10205
10672
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
10206
10673
  );
@@ -10208,13 +10675,13 @@ async function resolveTunnelTarget(opts) {
10208
10675
  const parsed = await readConfig(ymlPath);
10209
10676
  const config = parsed.config;
10210
10677
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
10211
- if (!existsSync18(containerRoot)) {
10678
+ if (!existsSync20(containerRoot)) {
10212
10679
  throw new Error(
10213
10680
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
10214
10681
  );
10215
10682
  }
10216
- const composePath = path27.join(containerRoot, ".devcontainer", "compose.yaml");
10217
- const isCompose = existsSync18(composePath);
10683
+ const composePath = path29.join(containerRoot, ".devcontainer", "compose.yaml");
10684
+ const isCompose = existsSync20(composePath);
10218
10685
  const parsedTarget = parseTargetArg(opts.target, config);
10219
10686
  const docker = opts.docker ?? defaultDockerExec;
10220
10687
  if (isCompose) {
@@ -10449,8 +10916,8 @@ var init_port_check2 = __esm({
10449
10916
  });
10450
10917
 
10451
10918
  // src/tunnel/run.ts
10452
- import { spawn as spawn10 } from "child_process";
10453
- import { consola as consola33 } from "consola";
10919
+ import { spawn as spawn12 } from "child_process";
10920
+ import { consola as consola35 } from "consola";
10454
10921
  function signalNumber(signal) {
10455
10922
  switch (signal) {
10456
10923
  case "SIGINT":
@@ -10463,8 +10930,8 @@ function signalNumber(signal) {
10463
10930
  }
10464
10931
  async function runTunnel(opts) {
10465
10932
  const log = opts.logger ?? {
10466
- info: (m) => consola33.info(m),
10467
- warn: (m) => consola33.warn(m)
10933
+ info: (m) => consola35.info(m),
10934
+ warn: (m) => consola35.warn(m)
10468
10935
  };
10469
10936
  const resolve = opts.resolve ?? resolveTunnelTarget;
10470
10937
  const resolveArgs3 = {
@@ -10539,7 +11006,7 @@ var init_run3 = __esm({
10539
11006
  init_port_check2();
10540
11007
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
10541
11008
  defaultDockerSpawn = (args) => {
10542
- const child = spawn10("docker", args, {
11009
+ const child = spawn12("docker", args, {
10543
11010
  stdio: "inherit"
10544
11011
  });
10545
11012
  const exited = new Promise((resolve, reject) => {
@@ -10570,8 +11037,8 @@ var init_run3 = __esm({
10570
11037
  });
10571
11038
 
10572
11039
  // src/commands/tunnel.ts
10573
- import { defineCommand as defineCommand29 } from "citty";
10574
- import { consola as consola34 } from "consola";
11040
+ import { defineCommand as defineCommand30 } from "citty";
11041
+ import { consola as consola36 } from "consola";
10575
11042
  function parseLocalPort(raw) {
10576
11043
  if (raw === void 0) return void 0;
10577
11044
  const n = Number(raw);
@@ -10587,7 +11054,7 @@ var init_tunnel = __esm({
10587
11054
  "src/commands/tunnel.ts"() {
10588
11055
  "use strict";
10589
11056
  init_run3();
10590
- tunnelCommand = defineCommand29({
11057
+ tunnelCommand = defineCommand30({
10591
11058
  meta: {
10592
11059
  name: "tunnel",
10593
11060
  group: "discovery",
@@ -10624,7 +11091,7 @@ var init_tunnel = __esm({
10624
11091
  });
10625
11092
  process.exit(exitCode);
10626
11093
  } catch (err) {
10627
- consola34.error(err instanceof Error ? err.message : String(err));
11094
+ consola36.error(err instanceof Error ? err.message : String(err));
10628
11095
  process.exit(1);
10629
11096
  }
10630
11097
  }
@@ -10694,8 +11161,8 @@ var init_prune = __esm({
10694
11161
  });
10695
11162
 
10696
11163
  // src/upgrade/index.ts
10697
- import { existsSync as existsSync19, promises as fs17 } from "fs";
10698
- import { consola as consola35 } from "consola";
11164
+ import { existsSync as existsSync21, promises as fs18 } from "fs";
11165
+ import { consola as consola37 } from "consola";
10699
11166
  async function fetchRuntimeVersions() {
10700
11167
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
10701
11168
  const tokenRes = await fetch(tokenUrl);
@@ -10733,7 +11200,7 @@ ${yml}`;
10733
11200
  }
10734
11201
  async function runUpgrade(opts) {
10735
11202
  const home = opts.monocerosHome ?? monocerosHome();
10736
- const logger = opts.logger ?? consola35;
11203
+ const logger = opts.logger ?? consola37;
10737
11204
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
10738
11205
  if (opts.list) {
10739
11206
  const versions = await fetchVersions();
@@ -10759,7 +11226,7 @@ async function runUpgrade(opts) {
10759
11226
  );
10760
11227
  }
10761
11228
  }
10762
- if (opts.name && !existsSync19(containerConfigPath(opts.name, home))) {
11229
+ if (opts.name && !existsSync21(containerConfigPath(opts.name, home))) {
10763
11230
  throw new Error(
10764
11231
  `No such config: ${containerConfigPath(opts.name, home)}. Run \`monoceros init <template> ${opts.name}\` first.`
10765
11232
  );
@@ -10810,11 +11277,11 @@ async function runUpgrade(opts) {
10810
11277
  let bumped = 0;
10811
11278
  for (const name of targets) {
10812
11279
  const ymlPath = containerConfigPath(name, home);
10813
- if (!existsSync19(ymlPath)) continue;
10814
- const raw = await fs17.readFile(ymlPath, "utf8");
11280
+ if (!existsSync21(ymlPath)) continue;
11281
+ const raw = await fs18.readFile(ymlPath, "utf8");
10815
11282
  const updated = setRuntimeVersion(raw, pinVersion);
10816
11283
  if (updated !== raw) {
10817
- await fs17.writeFile(ymlPath, updated);
11284
+ await fs18.writeFile(ymlPath, updated);
10818
11285
  bumped += 1;
10819
11286
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
10820
11287
  }
@@ -10856,7 +11323,7 @@ function formatPruneLine(prune) {
10856
11323
  }
10857
11324
  async function listContainerNames2(home) {
10858
11325
  try {
10859
- const entries = await fs17.readdir(containerConfigsDir(home));
11326
+ const entries = await fs18.readdir(containerConfigsDir(home));
10860
11327
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
10861
11328
  } catch {
10862
11329
  return [];
@@ -10879,7 +11346,7 @@ var init_upgrade = __esm({
10879
11346
  });
10880
11347
 
10881
11348
  // src/commands/upgrade.ts
10882
- import { defineCommand as defineCommand30 } from "citty";
11349
+ import { defineCommand as defineCommand31 } from "citty";
10883
11350
  var upgradeCommand;
10884
11351
  var init_upgrade2 = __esm({
10885
11352
  "src/commands/upgrade.ts"() {
@@ -10887,7 +11354,7 @@ var init_upgrade2 = __esm({
10887
11354
  init_upgrade();
10888
11355
  init_version();
10889
11356
  init_dispatch();
10890
- upgradeCommand = defineCommand30({
11357
+ upgradeCommand = defineCommand31({
10891
11358
  meta: {
10892
11359
  name: "upgrade",
10893
11360
  group: "lifecycle",
@@ -10929,7 +11396,7 @@ var main_exports = {};
10929
11396
  __export(main_exports, {
10930
11397
  main: () => main
10931
11398
  });
10932
- import { defineCommand as defineCommand31 } from "citty";
11399
+ import { defineCommand as defineCommand32 } from "citty";
10933
11400
  var main;
10934
11401
  var init_main = __esm({
10935
11402
  "src/main.ts"() {
@@ -10947,6 +11414,7 @@ var init_main = __esm({
10947
11414
  init_init2();
10948
11415
  init_list_components();
10949
11416
  init_logs();
11417
+ init_open2();
10950
11418
  init_port();
10951
11419
  init_remove_apt_packages();
10952
11420
  init_remove_feature();
@@ -10965,7 +11433,7 @@ var init_main = __esm({
10965
11433
  init_tunnel();
10966
11434
  init_upgrade2();
10967
11435
  init_version();
10968
- main = defineCommand31({
11436
+ main = defineCommand32({
10969
11437
  meta: {
10970
11438
  name: "monoceros",
10971
11439
  version: CLI_VERSION,
@@ -10975,6 +11443,7 @@ var init_main = __esm({
10975
11443
  init: initCommand,
10976
11444
  "list-components": listComponentsCommand,
10977
11445
  shell: shellCommand,
11446
+ open: openCommand,
10978
11447
  run: runCommand,
10979
11448
  logs: logsCommand,
10980
11449
  start: startCommand,
@@ -11261,25 +11730,25 @@ function detectHelpRequest(argv, main2) {
11261
11730
  const separatorIdx = argv.indexOf("--");
11262
11731
  if (helpIdx === -1) return null;
11263
11732
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
11264
- const path28 = [];
11733
+ const path30 = [];
11265
11734
  const tokens = argv.slice(
11266
11735
  0,
11267
11736
  separatorIdx === -1 ? argv.length : separatorIdx
11268
11737
  );
11269
11738
  let cursor = main2;
11270
11739
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
11271
- path28.push(mainName);
11740
+ path30.push(mainName);
11272
11741
  for (const tok of tokens) {
11273
11742
  if (tok.startsWith("-")) continue;
11274
11743
  const subs = cursor.subCommands ?? {};
11275
11744
  if (tok in subs) {
11276
11745
  cursor = subs[tok];
11277
- path28.push(tok);
11746
+ path30.push(tok);
11278
11747
  continue;
11279
11748
  }
11280
11749
  break;
11281
11750
  }
11282
- return { path: path28, cmd: cursor };
11751
+ return { path: path30, cmd: cursor };
11283
11752
  }
11284
11753
  async function maybeRenderHelp(argv, main2) {
11285
11754
  const hit = detectHelpRequest(argv, main2);