@getmonoceros/workbench 1.26.5 → 1.27.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.2.0" : 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,32 @@ 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"
2955
2967
  }
2956
2968
  ];
2957
2969
  }
2970
+ function ideStateVolumesForRuntime(name, version) {
2971
+ if (!version) return [];
2972
+ return ideStateVolumes(name).filter(
2973
+ (v) => compareRuntimeVersions(version, v.minRuntime) >= 0
2974
+ );
2975
+ }
2958
2976
  function buildDevcontainerJson(opts, dockerMode = "rootful") {
2959
2977
  const resolvedFeatures = resolveFeatures(opts);
2960
2978
  const features = {};
@@ -2986,6 +3004,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
2986
3004
  }
2987
3005
  }
2988
3006
  } : void 0;
3007
+ const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
2989
3008
  if (needsCompose(opts)) {
2990
3009
  return {
2991
3010
  name: opts.name,
@@ -2996,20 +3015,22 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
2996
3015
  remoteUser: "node",
2997
3016
  forwardPorts: ports,
2998
3017
  postCreateCommand: ".devcontainer/post-create.sh",
3018
+ ...sshPostStart,
2999
3019
  ...featuresField ?? {},
3000
3020
  ...customizationsField ?? {}
3001
3021
  };
3002
3022
  }
3003
- const ideMounts = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name).map(
3004
- (v) => `source=${v.volume},target=${v.target},type=volume`
3005
- ) : [];
3023
+ const ideMounts = ideStateVolumesForRuntime(
3024
+ opts.name,
3025
+ opts.runtimeVersion
3026
+ ).map((v) => `source=${v.volume},target=${v.target},type=volume`);
3006
3027
  const mounts = [...homeMounts, ...ideMounts];
3007
3028
  const mountsField = mounts.length > 0 ? { mounts } : {};
3008
3029
  const workspaceMountField = {
3009
3030
  workspaceMount: `source=\${localWorkspaceFolder},target=/workspaces/${opts.name},type=bind,consistency=cached`,
3010
3031
  workspaceFolder: `/workspaces/${opts.name}`
3011
3032
  };
3012
- const runArgs = ["--cap-add=NET_ADMIN"];
3033
+ const runArgs = ["--cap-add=NET_ADMIN", `--name=monoceros-${opts.name}`];
3013
3034
  if (ports.length > 0) {
3014
3035
  runArgs.push("--network=monoceros-proxy");
3015
3036
  runArgs.push(`--network-alias=${opts.name}`);
@@ -3023,6 +3044,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3023
3044
  runArgs,
3024
3045
  forwardPorts: ports,
3025
3046
  postCreateCommand: ".devcontainer/post-create.sh",
3047
+ ...sshPostStart,
3026
3048
  ...featuresField ?? {},
3027
3049
  ...customizationsField ?? {}
3028
3050
  };
@@ -3043,9 +3065,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3043
3065
  void dockerMode;
3044
3066
  const hasPorts = (opts.ports?.length ?? 0) > 0;
3045
3067
  const lines = ["services:"];
3046
- const ideVolumes = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name) : [];
3068
+ const ideVolumes = ideStateVolumesForRuntime(opts.name, opts.runtimeVersion);
3047
3069
  lines.push(" workspace:");
3048
3070
  lines.push(` image: ${resolveRuntimeImage(opts.runtimeVersion)}`);
3071
+ lines.push(` container_name: monoceros-${opts.name}`);
3049
3072
  lines.push(" command: 'sleep infinity'");
3050
3073
  lines.push(" cap_add:");
3051
3074
  lines.push(" - NET_ADMIN");
@@ -3917,8 +3940,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
3917
3940
  if (!isMap2(item)) return false;
3918
3941
  const url = item.get("url");
3919
3942
  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;
3943
+ const path30 = item.get("path");
3944
+ const effectivePath = typeof path30 === "string" ? path30 : typeof url === "string" ? deriveRepoName(url) : void 0;
3922
3945
  return effectivePath === urlOrPath;
3923
3946
  });
3924
3947
  if (idx < 0) return false;
@@ -4040,7 +4063,7 @@ async function runAddRepo(input) {
4040
4063
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
4041
4064
  );
4042
4065
  }
4043
- const path28 = (input.path ?? deriveRepoName(url)).trim();
4066
+ const path30 = (input.path ?? deriveRepoName(url)).trim();
4044
4067
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
4045
4068
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
4046
4069
  if (hasName !== hasEmail) {
@@ -4077,7 +4100,7 @@ async function runAddRepo(input) {
4077
4100
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
4078
4101
  const entry2 = {
4079
4102
  url,
4080
- path: path28,
4103
+ path: path30,
4081
4104
  ...hasName && hasEmail ? {
4082
4105
  gitUser: {
4083
4106
  name: input.gitName.trim(),
@@ -5120,9 +5143,163 @@ var init_transform = __esm({
5120
5143
  }
5121
5144
  });
5122
5145
 
5146
+ // src/devcontainer/ssh-attach.ts
5147
+ import { spawn as spawn4 } from "child_process";
5148
+ import { promises as fs10 } from "fs";
5149
+ import { existsSync as existsSync8 } from "fs";
5150
+ import os2 from "os";
5151
+ import path13 from "path";
5152
+ function sshHomeDir(home) {
5153
+ return path13.join(home, "ssh");
5154
+ }
5155
+ function sshConfigEntryPath(home, name) {
5156
+ return path13.join(sshHomeDir(home), "config.d", name);
5157
+ }
5158
+ function sshProxyScriptPath(home, name) {
5159
+ return path13.join(sshHomeDir(home), `exec-${name}.sh`);
5160
+ }
5161
+ function privateKeyPath(targetDir) {
5162
+ return path13.join(targetDir, ".monoceros", "ssh", "id_ed25519");
5163
+ }
5164
+ async function ensureKeypair(targetDir, name, keygen, logger) {
5165
+ const privateKey = privateKeyPath(targetDir);
5166
+ const publicKey = `${privateKey}.pub`;
5167
+ if (existsSync8(privateKey) && existsSync8(publicKey)) {
5168
+ return { privateKey, publicKey };
5169
+ }
5170
+ await fs10.mkdir(path13.dirname(privateKey), { recursive: true });
5171
+ try {
5172
+ const res = await keygen([
5173
+ "-t",
5174
+ "ed25519",
5175
+ "-N",
5176
+ "",
5177
+ "-f",
5178
+ privateKey,
5179
+ "-C",
5180
+ `monoceros-${name}`,
5181
+ "-q"
5182
+ ]);
5183
+ if (res.exitCode !== 0) {
5184
+ logger.warn(
5185
+ `ssh-keygen failed (exit ${res.exitCode})${res.stderr ? `: ${res.stderr.trim()}` : ""}; IDE SSH attach not set up.`
5186
+ );
5187
+ return null;
5188
+ }
5189
+ } catch (err) {
5190
+ logger.warn(
5191
+ `ssh-keygen not runnable (${err instanceof Error ? err.message : String(err)}); IDE SSH attach not set up.`
5192
+ );
5193
+ return null;
5194
+ }
5195
+ return { privateKey, publicKey };
5196
+ }
5197
+ function proxyScriptContent(name, targetDir) {
5198
+ return `#!/bin/sh
5199
+ # Generated by Monoceros (ADR 0022) - do not edit.
5200
+ # Portless SSH transport into the running dev container '${name}'.
5201
+ # Resolves the container by the devcontainer.local_folder label (the
5202
+ # same handle 'monoceros shell' uses), so it follows rebuilds.
5203
+ set -e
5204
+ cid=$(docker ps -q \\
5205
+ --filter "label=devcontainer.local_folder=${targetDir}" \\
5206
+ --filter status=running | head -n1)
5207
+ if [ -z "$cid" ]; then
5208
+ echo "monoceros: dev container '${name}' is not running. Start it with: monoceros apply ${name}" >&2
5209
+ exit 1
5210
+ fi
5211
+ exec docker exec -i "$cid" socat - TCP:127.0.0.1:22
5212
+ `;
5213
+ }
5214
+ function configEntryContent(name, hostAlias, privateKey, proxyScript) {
5215
+ return `# Generated by Monoceros (ADR 0022) - do not edit.
5216
+ # Attach an SSH-capable IDE (Codium, IntelliJ, Zed) to host
5217
+ # '${hostAlias}', or run: ssh ${hostAlias}
5218
+ Host ${hostAlias}
5219
+ User node
5220
+ IdentityFile "${privateKey}"
5221
+ IdentitiesOnly yes
5222
+ StrictHostKeyChecking no
5223
+ UserKnownHostsFile /dev/null
5224
+ ProxyCommand "${proxyScript}"
5225
+ `;
5226
+ }
5227
+ async function ensureInclude(userSshDir, home) {
5228
+ const configPath = path13.join(userSshDir, "config");
5229
+ const includeTarget = path13.join(sshHomeDir(home), "config.d", "*");
5230
+ const includeLine = `Include "${includeTarget}"`;
5231
+ await fs10.mkdir(userSshDir, { recursive: true });
5232
+ let existing = "";
5233
+ try {
5234
+ existing = await fs10.readFile(configPath, "utf8");
5235
+ } catch {
5236
+ existing = "";
5237
+ }
5238
+ if (existing.includes(includeTarget)) return;
5239
+ const banner = "# Added by Monoceros (ADR 0022): per-container SSH attach entries.";
5240
+ const prefix = `${banner}
5241
+ ${includeLine}
5242
+ `;
5243
+ const next = existing.length > 0 ? `${prefix}
5244
+ ${existing}` : prefix;
5245
+ await fs10.writeFile(configPath, next, { mode: 384 });
5246
+ await fs10.chmod(configPath, 384).catch(() => {
5247
+ });
5248
+ }
5249
+ async function setupSshAttach(opts) {
5250
+ const hostAlias = `monoceros-${opts.name}`;
5251
+ const logger = opts.logger ?? { info: () => {
5252
+ }, warn: () => {
5253
+ } };
5254
+ const keygen = opts.keygen ?? realKeygen;
5255
+ const userSshDir = opts.userSshDir ?? path13.join(os2.homedir(), ".ssh");
5256
+ const keys = await ensureKeypair(opts.targetDir, opts.name, keygen, logger);
5257
+ if (!keys) return { hostAlias, configured: false };
5258
+ const proxyScript = sshProxyScriptPath(opts.home, opts.name);
5259
+ const configEntry = sshConfigEntryPath(opts.home, opts.name);
5260
+ await fs10.mkdir(path13.dirname(proxyScript), { recursive: true });
5261
+ await fs10.mkdir(path13.dirname(configEntry), { recursive: true });
5262
+ await fs10.writeFile(
5263
+ proxyScript,
5264
+ proxyScriptContent(opts.name, opts.targetDir),
5265
+ { mode: 493 }
5266
+ );
5267
+ await fs10.chmod(proxyScript, 493).catch(() => {
5268
+ });
5269
+ await fs10.writeFile(
5270
+ configEntry,
5271
+ configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
5272
+ );
5273
+ await ensureInclude(userSshDir, opts.home);
5274
+ return { hostAlias, configured: true };
5275
+ }
5276
+ async function removeSshAttach(home, name) {
5277
+ await fs10.rm(sshProxyScriptPath(home, name), { force: true });
5278
+ await fs10.rm(sshConfigEntryPath(home, name), { force: true });
5279
+ }
5280
+ var realKeygen;
5281
+ var init_ssh_attach = __esm({
5282
+ "src/devcontainer/ssh-attach.ts"() {
5283
+ "use strict";
5284
+ realKeygen = (args) => {
5285
+ return new Promise((resolve, reject) => {
5286
+ const child = spawn4("ssh-keygen", args, {
5287
+ stdio: ["ignore", "ignore", "pipe"]
5288
+ });
5289
+ let stderr = "";
5290
+ child.stderr.on("data", (chunk) => {
5291
+ stderr += chunk.toString();
5292
+ });
5293
+ child.on("error", reject);
5294
+ child.on("exit", (code) => resolve({ exitCode: code ?? 0, stderr }));
5295
+ });
5296
+ };
5297
+ }
5298
+ });
5299
+
5123
5300
  // src/apply/apply-log.ts
5124
5301
  import { createWriteStream, mkdirSync } from "fs";
5125
- import path13 from "path";
5302
+ import path14 from "path";
5126
5303
  import { Writable } from "stream";
5127
5304
  function safeIsoStamp(d) {
5128
5305
  return d.toISOString().replace(/[:.]/g, "-");
@@ -5132,7 +5309,7 @@ function createApplyLog(opts) {
5132
5309
  const dir = containerLogsDir(opts.name, opts.home);
5133
5310
  mkdirSync(dir, { recursive: true });
5134
5311
  const file = `apply-${opts.name}-${safeIsoStamp(now)}.log`;
5135
- const fullPath = path13.join(dir, file);
5312
+ const fullPath = path14.join(dir, file);
5136
5313
  const stream = createWriteStream(fullPath, { flags: "w" });
5137
5314
  const header = [
5138
5315
  `# monoceros apply log`,
@@ -5999,8 +6176,8 @@ var init_markers = __esm({
5999
6176
  });
6000
6177
 
6001
6178
  // src/briefing/index.ts
6002
- import { promises as fs10 } from "fs";
6003
- import path14 from "path";
6179
+ import { promises as fs11 } from "fs";
6180
+ import path15 from "path";
6004
6181
  async function writeBriefing(input) {
6005
6182
  const subCommands = input.subCommands ?? await loadSubCommandsDynamic();
6006
6183
  const manifestLoader = input.manifestLoader ?? ((ref) => loadFeatureManifestSummary(ref));
@@ -6013,12 +6190,12 @@ async function writeBriefing(input) {
6013
6190
  );
6014
6191
  const claudeBody = generateClaudeMd();
6015
6192
  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"),
6193
+ await writeMarkerAware(path15.join(input.targetDir, "AGENTS.md"), agentsBody);
6194
+ await writeMarkerAware(path15.join(input.targetDir, "CLAUDE.md"), claudeBody);
6195
+ const monocerosDir = path15.join(input.targetDir, ".monoceros");
6196
+ await fs11.mkdir(monocerosDir, { recursive: true });
6197
+ await fs11.writeFile(
6198
+ path15.join(monocerosDir, "commands.md"),
6022
6199
  commandsBody,
6023
6200
  "utf8"
6024
6201
  );
@@ -6038,18 +6215,18 @@ function featureDisplayMap(components) {
6038
6215
  async function writeMarkerAware(filePath, body) {
6039
6216
  let existing = null;
6040
6217
  try {
6041
- existing = await fs10.readFile(filePath, "utf8");
6218
+ existing = await fs11.readFile(filePath, "utf8");
6042
6219
  } catch (err) {
6043
6220
  if (err.code !== "ENOENT") throw err;
6044
6221
  }
6045
6222
  if (existing) {
6046
6223
  const updated = replaceMarkerBlock(existing, body);
6047
6224
  if (updated !== null) {
6048
- await fs10.writeFile(filePath, updated, "utf8");
6225
+ await fs11.writeFile(filePath, updated, "utf8");
6049
6226
  return;
6050
6227
  }
6051
6228
  }
6052
- await fs10.writeFile(filePath, wrapWithMarkers(body), "utf8");
6229
+ await fs11.writeFile(filePath, wrapWithMarkers(body), "utf8");
6053
6230
  }
6054
6231
  async function loadSubCommandsDynamic() {
6055
6232
  const mod = await Promise.resolve().then(() => (init_main(), main_exports));
@@ -6174,10 +6351,10 @@ var init_runtime_pull_hint = __esm({
6174
6351
  });
6175
6352
 
6176
6353
  // src/devcontainer/cli.ts
6177
- import { spawn as spawn4 } from "child_process";
6354
+ import { spawn as spawn5 } from "child_process";
6178
6355
  import { readFileSync as readFileSync4 } from "fs";
6179
6356
  import { createRequire } from "module";
6180
- import path15 from "path";
6357
+ import path16 from "path";
6181
6358
  function devcontainerCliPath() {
6182
6359
  if (cachedBinaryPath) return cachedBinaryPath;
6183
6360
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
@@ -6186,7 +6363,7 @@ function devcontainerCliPath() {
6186
6363
  if (!binEntry) {
6187
6364
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
6188
6365
  }
6189
- cachedBinaryPath = path15.resolve(path15.dirname(pkgJsonPath), binEntry);
6366
+ cachedBinaryPath = path16.resolve(path16.dirname(pkgJsonPath), binEntry);
6190
6367
  return cachedBinaryPath;
6191
6368
  }
6192
6369
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -6202,7 +6379,7 @@ var init_cli = __esm({
6202
6379
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
6203
6380
  return new Promise((resolve, reject) => {
6204
6381
  if (options.interactive) {
6205
- const child2 = spawn4(process.execPath, [binPath, ...args], {
6382
+ const child2 = spawn5(process.execPath, [binPath, ...args], {
6206
6383
  cwd,
6207
6384
  env,
6208
6385
  stdio: "inherit"
@@ -6211,7 +6388,7 @@ var init_cli = __esm({
6211
6388
  child2.on("exit", (code) => resolve(code ?? 0));
6212
6389
  return;
6213
6390
  }
6214
- const child = spawn4(process.execPath, [binPath, ...args], {
6391
+ const child = spawn5(process.execPath, [binPath, ...args], {
6215
6392
  cwd,
6216
6393
  env,
6217
6394
  stdio: ["ignore", "pipe", "pipe"]
@@ -6259,9 +6436,9 @@ var init_cli = __esm({
6259
6436
  });
6260
6437
 
6261
6438
  // src/devcontainer/compose.ts
6262
- import { spawn as spawn5 } from "child_process";
6263
- import { existsSync as existsSync8 } from "fs";
6264
- import path16 from "path";
6439
+ import { spawn as spawn6 } from "child_process";
6440
+ import { existsSync as existsSync9 } from "fs";
6441
+ import path17 from "path";
6265
6442
  import { Writable as Writable3 } from "stream";
6266
6443
  import { consola as consola10 } from "consola";
6267
6444
  async function findContainerIds(filters, exec = spawnDocker) {
@@ -6311,18 +6488,24 @@ async function cleanupDockerObjects(opts) {
6311
6488
  return { exitCode: rmExit, removedIds: ids };
6312
6489
  }
6313
6490
  function composeProjectName(root) {
6314
- return `${path16.basename(root)}_devcontainer`;
6491
+ return `${path17.basename(root)}_devcontainer`;
6315
6492
  }
6316
- function resolveCompose(root) {
6317
- if (!existsSync8(path16.join(root, ".devcontainer"))) {
6493
+ function assertDevcontainer(root) {
6494
+ if (!existsSync9(path17.join(root, ".devcontainer"))) {
6318
6495
  throw new Error(
6319
6496
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
6320
6497
  );
6321
6498
  }
6322
- const composeFile = path16.join(root, ".devcontainer", "compose.yaml");
6323
- if (!existsSync8(composeFile)) {
6499
+ }
6500
+ function isComposeMode(root) {
6501
+ return existsSync9(path17.join(root, ".devcontainer", "compose.yaml"));
6502
+ }
6503
+ function resolveCompose(root) {
6504
+ assertDevcontainer(root);
6505
+ const composeFile = path17.join(root, ".devcontainer", "compose.yaml");
6506
+ if (!existsSync9(composeFile)) {
6324
6507
  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.`
6508
+ `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
6509
  );
6327
6510
  }
6328
6511
  return { composeFile, projectName: composeProjectName(root) };
@@ -6334,7 +6517,7 @@ async function runComposeAction(buildSubArgs, opts) {
6334
6517
  return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
6335
6518
  }
6336
6519
  async function runStart(opts) {
6337
- resolveCompose(opts.root);
6520
+ assertDevcontainer(opts.root);
6338
6521
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6339
6522
  const spawnFn = opts.spawn ?? spawnDevcontainer;
6340
6523
  logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
@@ -6477,16 +6660,71 @@ and retry \`monoceros apply\`.`
6477
6660
  );
6478
6661
  }
6479
6662
  function runStop(opts) {
6480
- return runComposeAction(
6481
- (service) => ["stop", ...service ? [service] : []],
6482
- opts
6483
- );
6663
+ assertDevcontainer(opts.root);
6664
+ if (isComposeMode(opts.root)) {
6665
+ return runComposeAction(
6666
+ (service) => ["stop", ...service ? [service] : []],
6667
+ opts
6668
+ );
6669
+ }
6670
+ return stopImageContainer(opts);
6484
6671
  }
6485
6672
  function runStatus(opts) {
6486
- return runComposeAction(
6487
- (service) => ["ps", ...service ? [service] : []],
6488
- opts
6673
+ assertDevcontainer(opts.root);
6674
+ if (isComposeMode(opts.root)) {
6675
+ return runComposeAction(
6676
+ (service) => ["ps", ...service ? [service] : []],
6677
+ opts
6678
+ );
6679
+ }
6680
+ return statusImageContainer(opts);
6681
+ }
6682
+ function imageContainerFilter(root) {
6683
+ return `label=devcontainer.local_folder=${root}`;
6684
+ }
6685
+ async function stopImageContainer(opts) {
6686
+ const exec = opts.dockerExec ?? spawnDocker;
6687
+ const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6688
+ const name = path17.basename(opts.root);
6689
+ const ps = await exec([
6690
+ "ps",
6691
+ "-q",
6692
+ "--filter",
6693
+ imageContainerFilter(opts.root),
6694
+ "--filter",
6695
+ "status=running"
6696
+ ]);
6697
+ const id = ps.stdout.split("\n").map((s) => s.trim()).find((s) => s.length > 0);
6698
+ if (!id) {
6699
+ logger.info(`Container '${name}' is not running.`);
6700
+ return 0;
6701
+ }
6702
+ const res = await exec(["stop", id]);
6703
+ if (res.exitCode === 0) logger.info(`Stopped '${name}'.`);
6704
+ return res.exitCode;
6705
+ }
6706
+ async function statusImageContainer(opts) {
6707
+ const exec = opts.dockerExec ?? spawnDocker;
6708
+ const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6709
+ const name = path17.basename(opts.root);
6710
+ const res = await exec([
6711
+ "ps",
6712
+ "-a",
6713
+ "--filter",
6714
+ imageContainerFilter(opts.root)
6715
+ ]);
6716
+ const rows = res.stdout.split("\n").filter((l) => l.trim().length > 0);
6717
+ if (rows.length <= 1) {
6718
+ logger.info(
6719
+ `Container '${name}' does not exist. Run \`monoceros apply ${name}\`.`
6720
+ );
6721
+ return 0;
6722
+ }
6723
+ process.stdout.write(
6724
+ res.stdout.endsWith("\n") ? res.stdout : `${res.stdout}
6725
+ `
6489
6726
  );
6727
+ return res.exitCode;
6490
6728
  }
6491
6729
  function runLogs(opts) {
6492
6730
  const follow = opts.follow ?? true;
@@ -6509,7 +6747,7 @@ var init_compose = __esm({
6509
6747
  init_proxy();
6510
6748
  spawnDockerCompose = (args, cwd) => {
6511
6749
  return new Promise((resolve, reject) => {
6512
- const child = spawn5("docker", ["compose", ...args], {
6750
+ const child = spawn6("docker", ["compose", ...args], {
6513
6751
  cwd,
6514
6752
  stdio: ["inherit", "pipe", "pipe"]
6515
6753
  });
@@ -6521,7 +6759,7 @@ var init_compose = __esm({
6521
6759
  };
6522
6760
  spawnDocker = (args) => {
6523
6761
  return new Promise((resolve, reject) => {
6524
- const child = spawn5("docker", args, {
6762
+ const child = spawn6("docker", args, {
6525
6763
  stdio: ["ignore", "pipe", "pipe"]
6526
6764
  });
6527
6765
  let stdout = "";
@@ -6582,9 +6820,9 @@ var init_images = __esm({
6582
6820
 
6583
6821
  // src/config/machine-state.ts
6584
6822
  import { promises as fsp4 } from "fs";
6585
- import path17 from "path";
6823
+ import path18 from "path";
6586
6824
  function machineStatePath(home = monocerosHome()) {
6587
- return path17.join(home, ".machine-state.json");
6825
+ return path18.join(home, ".machine-state.json");
6588
6826
  }
6589
6827
  async function readMachineState(home = monocerosHome()) {
6590
6828
  try {
@@ -6639,7 +6877,7 @@ var init_machine_state = __esm({
6639
6877
  });
6640
6878
 
6641
6879
  // src/devcontainer/docker-mode.ts
6642
- import { spawn as spawn6 } from "child_process";
6880
+ import { spawn as spawn7 } from "child_process";
6643
6881
  async function detectDockerMode(options = {}) {
6644
6882
  const spawnFn = options.spawn ?? realDockerInfo;
6645
6883
  try {
@@ -6689,7 +6927,7 @@ var init_docker_mode = __esm({
6689
6927
  init_format();
6690
6928
  realDockerInfo = () => {
6691
6929
  return new Promise((resolve, reject) => {
6692
- const child = spawn6(
6930
+ const child = spawn7(
6693
6931
  "docker",
6694
6932
  ["info", "--format", "{{json .SecurityOptions}}"],
6695
6933
  {
@@ -6708,9 +6946,9 @@ var init_docker_mode = __esm({
6708
6946
  });
6709
6947
 
6710
6948
  // src/devcontainer/identity.ts
6711
- import { spawn as spawn7 } from "child_process";
6712
- import { promises as fs11 } from "fs";
6713
- import path18 from "path";
6949
+ import { spawn as spawn8 } from "child_process";
6950
+ import { promises as fs12 } from "fs";
6951
+ import path19 from "path";
6714
6952
  import { consola as consola11 } from "consola";
6715
6953
  async function resolveIdentityWithPrompt(options = {}) {
6716
6954
  const spawnFn = options.spawn ?? realGitConfigGet;
@@ -6766,8 +7004,8 @@ async function resolveIdentityWithPrompt(options = {}) {
6766
7004
  };
6767
7005
  }
6768
7006
  async function collectGitIdentity(devContainerRoot, options = {}) {
6769
- const gitconfigDir = path18.join(devContainerRoot, ".monoceros");
6770
- const gitconfigPath = path18.join(gitconfigDir, "gitconfig");
7007
+ const gitconfigDir = path19.join(devContainerRoot, ".monoceros");
7008
+ const gitconfigPath = path19.join(gitconfigDir, "gitconfig");
6771
7009
  const logger = options.logger ?? { info: () => {
6772
7010
  }, warn: () => {
6773
7011
  } };
@@ -6780,8 +7018,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
6780
7018
  const lines = ["[user]"];
6781
7019
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
6782
7020
  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");
7021
+ await fs12.mkdir(gitconfigDir, { recursive: true });
7022
+ await fs12.writeFile(gitconfigPath, lines.join("\n") + "\n");
6785
7023
  return {
6786
7024
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
6787
7025
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -6824,7 +7062,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
6824
7062
  }
6825
7063
  async function readExistingGitconfig(filePath) {
6826
7064
  try {
6827
- const content = await fs11.readFile(filePath, "utf8");
7065
+ const content = await fs12.readFile(filePath, "utf8");
6828
7066
  const result = {};
6829
7067
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
6830
7068
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -6841,7 +7079,7 @@ var init_identity = __esm({
6841
7079
  "use strict";
6842
7080
  realGitConfigGet = (key) => {
6843
7081
  return new Promise((resolve, reject) => {
6844
- const child = spawn7("git", ["config", "--global", "--get", key], {
7082
+ const child = spawn8("git", ["config", "--global", "--get", key], {
6845
7083
  stdio: ["ignore", "pipe", "inherit"]
6846
7084
  });
6847
7085
  let stdout = "";
@@ -6901,7 +7139,7 @@ var init_identity = __esm({
6901
7139
  });
6902
7140
 
6903
7141
  // src/apply/index.ts
6904
- import { existsSync as existsSync9, promises as fs12 } from "fs";
7142
+ import { existsSync as existsSync10, promises as fs13 } from "fs";
6905
7143
  import { consola as consola12 } from "consola";
6906
7144
  async function runApply(opts) {
6907
7145
  const home = opts.monocerosHome ?? monocerosHome();
@@ -6923,7 +7161,7 @@ ${sectionLine(label)}
6923
7161
  );
6924
7162
  }
6925
7163
  const ymlPath = containerConfigPath(opts.name, home);
6926
- if (!existsSync9(ymlPath)) {
7164
+ if (!existsSync10(ymlPath)) {
6927
7165
  throw new Error(
6928
7166
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
6929
7167
  );
@@ -7041,7 +7279,7 @@ Fix the value in the env file (or the yml).`
7041
7279
  if (dockerMode === "rootless") {
7042
7280
  throw new Error(formatRootlessNotSupportedError());
7043
7281
  }
7044
- await fs12.mkdir(targetDir, { recursive: true });
7282
+ await fs13.mkdir(targetDir, { recursive: true });
7045
7283
  await writeScaffold(createOpts, targetDir, { dockerMode });
7046
7284
  await writeStateFile(
7047
7285
  targetDir,
@@ -7052,6 +7290,27 @@ Fix the value in the env file (or the yml).`
7052
7290
  ...opts.now ? { now: opts.now } : {}
7053
7291
  })
7054
7292
  );
7293
+ if (runtimeSupportsSshAttach(createOpts.runtimeVersion)) {
7294
+ try {
7295
+ const ssh = await setupSshAttach({
7296
+ name: opts.name,
7297
+ targetDir,
7298
+ home,
7299
+ ...opts.sshKeygen ? { keygen: opts.sshKeygen } : {},
7300
+ ...opts.sshUserSshDir ? { userSshDir: opts.sshUserSshDir } : {},
7301
+ logger: idLogger
7302
+ });
7303
+ if (ssh.configured) {
7304
+ logger.info(
7305
+ `SSH attach: \`ssh ${ssh.hostAlias}\` (or pick it in your IDE)`
7306
+ );
7307
+ }
7308
+ } catch (err) {
7309
+ (logger.warn ?? logger.info)(
7310
+ `SSH attach setup skipped: ${err instanceof Error ? err.message : String(err)}`
7311
+ );
7312
+ }
7313
+ }
7055
7314
  try {
7056
7315
  const components = await loadComponentCatalog();
7057
7316
  await writeBriefing({ targetDir, createOpts, components });
@@ -7196,8 +7455,8 @@ ${stripAnsi(formatted)}
7196
7455
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
7197
7456
  }
7198
7457
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
7199
- if (!existsSync9(targetDir)) return;
7200
- const entries = await fs12.readdir(targetDir);
7458
+ if (!existsSync10(targetDir)) return;
7459
+ const entries = await fs13.readdir(targetDir);
7201
7460
  if (entries.length === 0) return;
7202
7461
  const state = await readStateFile(targetDir);
7203
7462
  if (state) {
@@ -7267,7 +7526,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7267
7526
  }
7268
7527
  if (wantContainer) {
7269
7528
  try {
7270
- const text = await fs12.readFile(ymlPath, "utf8");
7529
+ const text = await fs13.readFile(ymlPath, "utf8");
7271
7530
  const parsed = parseConfig(text, ymlPath);
7272
7531
  const changed = setContainerGitUserInDoc(parsed.doc, {
7273
7532
  name: prompted.name,
@@ -7275,7 +7534,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7275
7534
  });
7276
7535
  if (changed) {
7277
7536
  const out = stringifyConfig(parsed.doc);
7278
- await fs12.writeFile(ymlPath, out, "utf8");
7537
+ await fs13.writeFile(ymlPath, out, "utf8");
7279
7538
  logger.info(
7280
7539
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
7281
7540
  );
@@ -7298,6 +7557,7 @@ var init_apply = __esm({
7298
7557
  init_state();
7299
7558
  init_transform();
7300
7559
  init_catalog();
7560
+ init_ssh_attach();
7301
7561
  init_scaffold();
7302
7562
  init_format();
7303
7563
  init_ref();
@@ -7321,90 +7581,386 @@ var init_apply = __esm({
7321
7581
  }
7322
7582
  });
7323
7583
 
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) {
7584
+ // src/devcontainer/browser-bridge.ts
7585
+ import { spawn as spawn9 } from "child_process";
7586
+ import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync5 } from "fs";
7587
+ import http from "http";
7588
+ import path20 from "path";
7589
+ function parseCallbackTarget(authUrl) {
7336
7590
  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);
7591
+ const u = new URL(authUrl);
7592
+ const redirect = u.searchParams.get("redirect_uri");
7593
+ if (!redirect) return null;
7594
+ const r = new URL(redirect);
7595
+ if (r.hostname !== "localhost" && r.hostname !== "127.0.0.1") return null;
7596
+ const port = Number(r.port);
7597
+ if (!Number.isInteger(port) || port <= 0) return null;
7598
+ return { port, pathname: r.pathname };
7599
+ } catch {
7600
+ return null;
7342
7601
  }
7343
7602
  }
7344
- var init_dispatch = __esm({
7345
- "src/commands/_dispatch.ts"() {
7346
- "use strict";
7603
+ function nextRelayUrl(content, lastOpened) {
7604
+ const url = content.trim();
7605
+ return url && url !== lastOpened ? url : null;
7606
+ }
7607
+ function wrapExec(command, opts) {
7608
+ const leading = [];
7609
+ const stmts = [];
7610
+ if (opts.pathPrepend) {
7611
+ leading.push(opts.pathPrepend);
7612
+ const i = leading.length;
7613
+ stmts.push(`export PATH="$${i}:$PATH"`);
7614
+ stmts.push(`export BROWSER="$${i}/xdg-open"`);
7347
7615
  }
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
- }
7616
+ if (opts.cwd) {
7617
+ leading.push(opts.cwd);
7618
+ stmts.push(`cd -- "$${leading.length}"`);
7619
+ }
7620
+ if (leading.length === 0) return command;
7621
+ const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
7622
+ const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
7623
+ return ["bash", "-lc", script, "bash", ...leading, ...command];
7624
+ }
7625
+ function openInBrowser(url) {
7626
+ const platform = process.platform;
7627
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
7628
+ try {
7629
+ const child = spawn9(cmd, args, {
7630
+ stdio: "ignore",
7631
+ detached: true
7632
+ });
7633
+ child.on("error", () => {
7387
7634
  });
7635
+ child.unref();
7636
+ } catch {
7388
7637
  }
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',
7400
- "#",
7401
- "# The work is done by `monoceros __complete --line --point`; this",
7402
- "# shell wrapper only forwards the cursor view.",
7403
- "",
7404
- "_monoceros() {",
7405
- " local IFS=$'\\n'",
7406
- " local candidates",
7407
- ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
7638
+ }
7639
+ async function startBrowserBridge(opts) {
7640
+ const relayDir = path20.join(opts.root, RELAY_DIRNAME);
7641
+ const relayScript = path20.join(relayDir, "xdg-open");
7642
+ const urlFile = path20.join(relayDir, "url");
7643
+ await fsp5.mkdir(relayDir, { recursive: true });
7644
+ await fsp5.rm(urlFile, { force: true });
7645
+ await fsp5.writeFile(
7646
+ relayScript,
7647
+ `#!/bin/sh
7648
+ printf '%s\\n' "$1" > "$(dirname "$0")/url"
7649
+ exit 0
7650
+ `,
7651
+ { mode: 493 }
7652
+ );
7653
+ await fsp5.chmod(relayScript, 493);
7654
+ const servers = [];
7655
+ let lastOpened = null;
7656
+ const onUrl = (url) => {
7657
+ openInBrowser(url);
7658
+ const target = parseCallbackTarget(url);
7659
+ if (!target) return;
7660
+ const server = http.createServer((req, res) => {
7661
+ const reqUrl = req.url ?? target.pathname;
7662
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
7663
+ res.end(
7664
+ '<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>'
7665
+ );
7666
+ void opts.spawn(
7667
+ [
7668
+ "exec",
7669
+ "--workspace-folder",
7670
+ opts.root,
7671
+ "--mount-workspace-git-root=false",
7672
+ "curl",
7673
+ "-fsS",
7674
+ `http://localhost:${target.port}${reqUrl}`
7675
+ ],
7676
+ opts.root,
7677
+ { quiet: true }
7678
+ );
7679
+ });
7680
+ server.on("error", () => {
7681
+ });
7682
+ server.listen(target.port, "127.0.0.1");
7683
+ servers.push(server);
7684
+ };
7685
+ const poll = setInterval(() => {
7686
+ if (!existsSync11(urlFile)) return;
7687
+ let content = "";
7688
+ try {
7689
+ content = readFileSync5(urlFile, "utf8");
7690
+ } catch {
7691
+ return;
7692
+ }
7693
+ const url = nextRelayUrl(content, lastOpened);
7694
+ if (!url) return;
7695
+ lastOpened = url;
7696
+ onUrl(url);
7697
+ }, 250);
7698
+ return {
7699
+ relayDirInContainer: `/workspaces/${opts.name}/${RELAY_DIRNAME}`,
7700
+ async dispose() {
7701
+ clearInterval(poll);
7702
+ for (const s of servers) s.close();
7703
+ await fsp5.rm(relayDir, { recursive: true, force: true });
7704
+ }
7705
+ };
7706
+ }
7707
+ var RELAY_DIRNAME;
7708
+ var init_browser_bridge = __esm({
7709
+ "src/devcontainer/browser-bridge.ts"() {
7710
+ "use strict";
7711
+ RELAY_DIRNAME = ".monoceros-bridge";
7712
+ }
7713
+ });
7714
+
7715
+ // src/devcontainer/shell.ts
7716
+ import { existsSync as existsSync12 } from "fs";
7717
+ import path21 from "path";
7718
+ async function runShell(opts) {
7719
+ assertContainerExists(opts.root);
7720
+ const spawnFn = opts.spawn ?? spawnDevcontainer;
7721
+ const upCode = await spawnFn(
7722
+ ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
7723
+ opts.root,
7724
+ { quiet: true }
7725
+ );
7726
+ if (upCode !== 0) return upCode;
7727
+ const name = opts.name ?? path21.basename(opts.root);
7728
+ const isTty2 = opts.isTty ?? process.stdout.isTTY;
7729
+ const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
7730
+ try {
7731
+ const innerExec = wrapExec(
7732
+ ["bash"],
7733
+ bridge ? { pathPrepend: bridge.relayDirInContainer } : {}
7734
+ );
7735
+ return await spawnFn(
7736
+ [
7737
+ "exec",
7738
+ "--workspace-folder",
7739
+ opts.root,
7740
+ "--mount-workspace-git-root=false",
7741
+ ...innerExec
7742
+ ],
7743
+ opts.root,
7744
+ { interactive: true }
7745
+ );
7746
+ } finally {
7747
+ if (bridge) await bridge.dispose();
7748
+ }
7749
+ }
7750
+ function assertContainerExists(root) {
7751
+ if (!existsSync12(path21.join(root, ".devcontainer"))) {
7752
+ throw new Error(
7753
+ `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
7754
+ );
7755
+ }
7756
+ }
7757
+ var init_shell = __esm({
7758
+ "src/devcontainer/shell.ts"() {
7759
+ "use strict";
7760
+ init_browser_bridge();
7761
+ init_cli();
7762
+ }
7763
+ });
7764
+
7765
+ // src/open/index.ts
7766
+ import { spawn as spawn10 } from "child_process";
7767
+ import { accessSync, constants, existsSync as existsSync13 } from "fs";
7768
+ import path22 from "path";
7769
+ import { consola as consola13 } from "consola";
7770
+ function resolveOnPath(bin) {
7771
+ const dirs = (process.env.PATH ?? "").split(path22.delimiter).filter(Boolean);
7772
+ for (const dir of dirs) {
7773
+ const candidate = path22.join(dir, bin);
7774
+ try {
7775
+ accessSync(candidate, constants.X_OK);
7776
+ return candidate;
7777
+ } catch {
7778
+ }
7779
+ }
7780
+ return null;
7781
+ }
7782
+ function resolveEditorBinary(tool) {
7783
+ if (resolveOnPath(tool.bin)) return tool.bin;
7784
+ if (process.platform === "darwin" && existsSync13(tool.macAppBin)) {
7785
+ return tool.macAppBin;
7786
+ }
7787
+ return null;
7788
+ }
7789
+ function realLaunch(bin, args) {
7790
+ const child = spawn10(bin, args, {
7791
+ detached: true,
7792
+ stdio: "ignore"
7793
+ });
7794
+ child.unref();
7795
+ }
7796
+ async function runOpen(opts) {
7797
+ const home = opts.monocerosHome ?? monocerosHome();
7798
+ const logger = opts.logger ?? {
7799
+ info: (m) => consola13.info(m),
7800
+ warn: (m) => consola13.warn(m)
7801
+ };
7802
+ const root = containerDir(opts.name, home);
7803
+ if (opts.tool === "shell") {
7804
+ const shellRunner = opts.shellRunner ?? runShell;
7805
+ return shellRunner({ root, name: opts.name });
7806
+ }
7807
+ const editor = EDITORS[opts.tool];
7808
+ if (!editor) {
7809
+ throw new Error(
7810
+ `Unknown tool '${opts.tool}'. Supported: ${OPEN_TOOLS.join(", ")}.`
7811
+ );
7812
+ }
7813
+ if (!existsSync13(sshConfigEntryPath(home, opts.name))) {
7814
+ throw new Error(
7815
+ `SSH attach isn't set up for '${opts.name}'. Run \`monoceros apply ${opts.name}\` first (needs a runtime >= 1.2.0).`
7816
+ );
7817
+ }
7818
+ const running = await findRunningContainerByLocalFolder(
7819
+ root,
7820
+ opts.dockerLookup ? { docker: opts.dockerLookup } : {}
7821
+ );
7822
+ if (!running) {
7823
+ throw new Error(
7824
+ `Container '${opts.name}' isn't running. Bring it up with \`monoceros apply ${opts.name}\`, then retry.`
7825
+ );
7826
+ }
7827
+ const resolve = opts.binResolver ?? resolveEditorBinary;
7828
+ const bin = resolve(editor);
7829
+ if (!bin) {
7830
+ const where = process.platform === "darwin" ? " on PATH or in /Applications" : " on PATH";
7831
+ throw new Error(
7832
+ `${editor.label} ('${editor.bin}') not found${where}. In ${editor.label}, ${editor.setupHint}.`
7833
+ );
7834
+ }
7835
+ const uri = `vscode-remote://ssh-remote+monoceros-${opts.name}/workspaces/${opts.name}/${opts.name}.code-workspace`;
7836
+ const launch = opts.launch ?? realLaunch;
7837
+ launch(bin, ["--file-uri", uri]);
7838
+ logger.info(`Opening '${opts.name}' in ${editor.label}...`);
7839
+ return 0;
7840
+ }
7841
+ var EDITORS, OPEN_TOOLS;
7842
+ var init_open = __esm({
7843
+ "src/open/index.ts"() {
7844
+ "use strict";
7845
+ init_paths();
7846
+ init_locate_running();
7847
+ init_shell();
7848
+ init_ssh_attach();
7849
+ EDITORS = {
7850
+ code: {
7851
+ label: "VS Code",
7852
+ bin: "code",
7853
+ macAppBin: "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
7854
+ setupHint: `install the Remote-SSH extension and run "Shell Command: Install 'code' command in PATH"`
7855
+ },
7856
+ codium: {
7857
+ label: "VS Codium",
7858
+ bin: "codium",
7859
+ macAppBin: "/Applications/VSCodium.app/Contents/Resources/app/bin/codium",
7860
+ setupHint: 'install the "Open Remote - SSH" extension (the codium CLI ships with the app)'
7861
+ }
7862
+ };
7863
+ OPEN_TOOLS = [...Object.keys(EDITORS), "shell"];
7864
+ }
7865
+ });
7866
+
7867
+ // src/version.ts
7868
+ var CLI_VERSION;
7869
+ var init_version = __esm({
7870
+ "src/version.ts"() {
7871
+ "use strict";
7872
+ CLI_VERSION = true ? "1.27.0" : "dev";
7873
+ }
7874
+ });
7875
+
7876
+ // src/commands/_dispatch.ts
7877
+ import { consola as consola14 } from "consola";
7878
+ async function dispatch(runner) {
7879
+ try {
7880
+ const exitCode = await runner();
7881
+ process.exit(exitCode);
7882
+ } catch (err) {
7883
+ consola14.error(err instanceof Error ? err.message : String(err));
7884
+ process.exit(1);
7885
+ }
7886
+ }
7887
+ var init_dispatch = __esm({
7888
+ "src/commands/_dispatch.ts"() {
7889
+ "use strict";
7890
+ }
7891
+ });
7892
+
7893
+ // src/commands/apply.ts
7894
+ import { defineCommand as defineCommand8 } from "citty";
7895
+ import { consola as consola15 } from "consola";
7896
+ var applyCommand;
7897
+ var init_apply2 = __esm({
7898
+ "src/commands/apply.ts"() {
7899
+ "use strict";
7900
+ init_apply();
7901
+ init_open();
7902
+ init_version();
7903
+ init_dispatch();
7904
+ applyCommand = defineCommand8({
7905
+ meta: {
7906
+ name: "apply",
7907
+ group: "lifecycle",
7908
+ 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."
7909
+ },
7910
+ args: {
7911
+ name: {
7912
+ type: "positional",
7913
+ description: "Config name. Resolves to $MONOCEROS_HOME/container-configs/<name>.yml.",
7914
+ required: true
7915
+ },
7916
+ verbose: {
7917
+ type: "boolean",
7918
+ description: "Stream the raw @devcontainers/cli output to stderr instead of showing a phase spinner. Auto-enabled when stderr is not a TTY.",
7919
+ default: false
7920
+ },
7921
+ open: {
7922
+ type: "string",
7923
+ description: `After a successful apply, open the container in this tool (${OPEN_TOOLS.join("|")}).`
7924
+ }
7925
+ },
7926
+ run({ args }) {
7927
+ return dispatch(async () => {
7928
+ const result = await runApply({
7929
+ name: args.name,
7930
+ cliVersion: CLI_VERSION,
7931
+ verbose: args.verbose
7932
+ });
7933
+ if (args.open && result.containerExitCode === 0) {
7934
+ try {
7935
+ await runOpen({ name: args.name, tool: args.open });
7936
+ } catch (err) {
7937
+ consola15.warn(err instanceof Error ? err.message : String(err));
7938
+ }
7939
+ }
7940
+ return result.containerExitCode;
7941
+ });
7942
+ }
7943
+ });
7944
+ }
7945
+ });
7946
+
7947
+ // src/commands/completion.ts
7948
+ import { defineCommand as defineCommand9 } from "citty";
7949
+ function renderCompletionScript(shell) {
7950
+ if (shell === "bash") {
7951
+ return [
7952
+ "# bash completion for monoceros",
7953
+ "# install: source this file from .bashrc, e.g.",
7954
+ "# monoceros completion bash > ~/.bash_completion.d/monoceros",
7955
+ '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
7956
+ "#",
7957
+ "# The work is done by `monoceros __complete --line --point`; this",
7958
+ "# shell wrapper only forwards the cursor view.",
7959
+ "",
7960
+ "_monoceros() {",
7961
+ " local IFS=$'\\n'",
7962
+ " local candidates",
7963
+ ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
7408
7964
  ' local cur="${COMP_WORDS[COMP_CWORD]}"',
7409
7965
  ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
7410
7966
  " # Suppress the trailing space when bash narrowed the candidate",
@@ -7516,8 +8072,8 @@ var init_completion = __esm({
7516
8072
  });
7517
8073
 
7518
8074
  // src/completion/resolve.ts
7519
- import { existsSync as existsSync10, promises as fs13 } from "fs";
7520
- import path19 from "path";
8075
+ import { existsSync as existsSync14, promises as fs14 } from "fs";
8076
+ import path23 from "path";
7521
8077
  async function resolveCompletions(line, point, opts = {}) {
7522
8078
  const { prev, current } = parseCompletionLine(line, point);
7523
8079
  const ctx = { prev, current, opts };
@@ -7666,9 +8222,9 @@ function filterPrefix(values, fragment) {
7666
8222
  }
7667
8223
  async function listContainerNames(ctx) {
7668
8224
  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);
8225
+ const dir = path23.join(home, "container-configs");
8226
+ if (!existsSync14(dir)) return [];
8227
+ const entries = await fs14.readdir(dir);
7672
8228
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
7673
8229
  }
7674
8230
  async function listFeatureComponents() {
@@ -7742,10 +8298,12 @@ var init_resolve = __esm({
7742
8298
  init_manifest();
7743
8299
  init_catalog();
7744
8300
  init_schema();
8301
+ init_open();
7745
8302
  ALL_COMMANDS = [
7746
8303
  "init",
7747
8304
  "list-components",
7748
8305
  "shell",
8306
+ "open",
7749
8307
  "run",
7750
8308
  "logs",
7751
8309
  "start",
@@ -7795,7 +8353,10 @@ var init_resolve = __esm({
7795
8353
  },
7796
8354
  apply: {
7797
8355
  positionals: [containerName],
7798
- flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
8356
+ flags: {
8357
+ "--yes": { type: "boolean", aliases: ["-y"] },
8358
+ "--open": { type: "value", values: () => [...OPEN_TOOLS] }
8359
+ }
7799
8360
  },
7800
8361
  upgrade: {
7801
8362
  // First positional is a container name; the second is a version
@@ -7811,12 +8372,16 @@ var init_resolve = __esm({
7811
8372
  }
7812
8373
  },
7813
8374
  shell: { positionals: [containerName] },
8375
+ open: { positionals: [containerName, () => [...OPEN_TOOLS]] },
7814
8376
  run: {
7815
8377
  positionals: [containerName],
7816
8378
  flags: { "--in": { type: "value" } }
7817
8379
  },
7818
8380
  logs: { positionals: [containerName] },
7819
- start: { positionals: [containerName] },
8381
+ start: {
8382
+ positionals: [containerName],
8383
+ flags: { "--open": { type: "value", values: () => [...OPEN_TOOLS] } }
8384
+ },
7820
8385
  stop: { positionals: [containerName] },
7821
8386
  status: { positionals: [containerName] },
7822
8387
  "add-language": {
@@ -8335,14 +8900,14 @@ var init_generator = __esm({
8335
8900
  });
8336
8901
 
8337
8902
  // 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";
8903
+ import { existsSync as existsSync15, promises as fs15 } from "fs";
8904
+ import path24 from "path";
8905
+ import { consola as consola16 } from "consola";
8341
8906
  async function runInit(opts) {
8342
8907
  const home = opts.monocerosHome ?? monocerosHome();
8343
8908
  const logger = opts.logger ?? {
8344
- success: (msg) => consola14.success(msg),
8345
- info: (msg) => consola14.info(msg)
8909
+ success: (msg) => consola16.success(msg),
8910
+ info: (msg) => consola16.info(msg)
8346
8911
  };
8347
8912
  if (!REGEX.solutionName.test(opts.name)) {
8348
8913
  throw new Error(
@@ -8350,12 +8915,12 @@ async function runInit(opts) {
8350
8915
  );
8351
8916
  }
8352
8917
  const dest = containerConfigPath(opts.name, home);
8353
- if (existsSync11(dest)) {
8918
+ if (existsSync15(dest)) {
8354
8919
  throw new Error(
8355
8920
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
8356
8921
  );
8357
8922
  }
8358
- const componentsRoot = opts.workbenchRoot ? path20.join(opts.workbenchRoot, "components") : componentsRootDir();
8923
+ const componentsRoot = opts.workbenchRoot ? path24.join(opts.workbenchRoot, "components") : componentsRootDir();
8359
8924
  const catalog = await loadComponentCatalog(componentsRoot);
8360
8925
  if (catalog.size === 0) {
8361
8926
  throw new Error(
@@ -8421,9 +8986,9 @@ async function runInit(opts) {
8421
8986
  } else {
8422
8987
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
8423
8988
  }
8424
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
8989
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
8425
8990
  await ensureEnvGitignored(containerConfigsDir(home));
8426
- await fs14.writeFile(dest, text, "utf8");
8991
+ await fs15.writeFile(dest, text, "utf8");
8427
8992
  const envPath = containerEnvPath(opts.name, home);
8428
8993
  const seedVars = {};
8429
8994
  for (const f of composed.features) {
@@ -8446,8 +9011,8 @@ async function runInit(opts) {
8446
9011
  }
8447
9012
  await ensureEnvVars(envPath, opts.name, seedVars);
8448
9013
  const documented = !anyComposed;
8449
- const ymlRel = path20.relative(home, dest);
8450
- const envRel = path20.relative(home, envPath);
9014
+ const ymlRel = path24.relative(home, dest);
9015
+ const envRel = path24.relative(home, envPath);
8451
9016
  if (documented) {
8452
9017
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
8453
9018
  logger.info(
@@ -8591,7 +9156,7 @@ var init_init = __esm({
8591
9156
 
8592
9157
  // src/commands/init.ts
8593
9158
  import { defineCommand as defineCommand11 } from "citty";
8594
- import { consola as consola15 } from "consola";
9159
+ import { consola as consola17 } from "consola";
8595
9160
  function collectListFlag(flag, rawArgs) {
8596
9161
  const eq = `${flag}=`;
8597
9162
  const pieces = [];
@@ -8717,7 +9282,7 @@ var init_init2 = __esm({
8717
9282
  ...ports && ports.length > 0 ? { withPorts: ports } : {}
8718
9283
  });
8719
9284
  } catch (err) {
8720
- consola15.error(err instanceof Error ? err.message : String(err));
9285
+ consola17.error(err instanceof Error ? err.message : String(err));
8721
9286
  process.exit(1);
8722
9287
  }
8723
9288
  }
@@ -8766,7 +9331,7 @@ var init_expand = __esm({
8766
9331
 
8767
9332
  // src/commands/list-components.ts
8768
9333
  import { defineCommand as defineCommand12 } from "citty";
8769
- import { consola as consola16 } from "consola";
9334
+ import { consola as consola18 } from "consola";
8770
9335
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
8771
9336
  var init_list_components = __esm({
8772
9337
  "src/commands/list-components.ts"() {
@@ -8795,7 +9360,7 @@ var init_list_components = __esm({
8795
9360
  try {
8796
9361
  const catalog = expandSelectable(await loadDescriptorCatalog());
8797
9362
  if (catalog.size === 0) {
8798
- consola16.warn(
9363
+ consola18.warn(
8799
9364
  "No components found. The workbench checkout looks incomplete."
8800
9365
  );
8801
9366
  process.exit(0);
@@ -8846,7 +9411,7 @@ var init_list_components = __esm({
8846
9411
  }
8847
9412
  process.exit(0);
8848
9413
  } catch (err) {
8849
- consola16.error(err instanceof Error ? err.message : String(err));
9414
+ consola18.error(err instanceof Error ? err.message : String(err));
8850
9415
  process.exit(1);
8851
9416
  }
8852
9417
  }
@@ -8855,14 +9420,14 @@ var init_list_components = __esm({
8855
9420
  });
8856
9421
 
8857
9422
  // src/commands/logs.ts
8858
- import { spawn as spawn8 } from "child_process";
8859
- import { existsSync as existsSync12 } from "fs";
8860
- import path21 from "path";
9423
+ import { spawn as spawn11 } from "child_process";
9424
+ import { existsSync as existsSync16 } from "fs";
9425
+ import path25 from "path";
8861
9426
  import { defineCommand as defineCommand13 } from "citty";
8862
9427
  function tailLogFile(file, follow) {
8863
9428
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
8864
9429
  return new Promise((resolve, reject) => {
8865
- const child = spawn8(cmd, args, { stdio: "inherit" });
9430
+ const child = spawn11(cmd, args, { stdio: "inherit" });
8866
9431
  child.on("error", reject);
8867
9432
  child.on("exit", (code) => resolve(code ?? 0));
8868
9433
  });
@@ -8900,8 +9465,8 @@ var init_logs = __esm({
8900
9465
  run({ args }) {
8901
9466
  const service = typeof args.service === "string" ? args.service : void 0;
8902
9467
  if (service) {
8903
- const logFile = path21.join(containerLogsDir(args.name), `${service}.log`);
8904
- if (existsSync12(logFile)) {
9468
+ const logFile = path25.join(containerLogsDir(args.name), `${service}.log`);
9469
+ if (existsSync16(logFile)) {
8905
9470
  return dispatch(() => tailLogFile(logFile, args.follow));
8906
9471
  }
8907
9472
  }
@@ -8917,12 +9482,45 @@ var init_logs = __esm({
8917
9482
  }
8918
9483
  });
8919
9484
 
8920
- // src/commands/port.ts
9485
+ // src/commands/open.ts
8921
9486
  import { defineCommand as defineCommand14 } from "citty";
8922
- import { consola as consola17 } from "consola";
9487
+ var openCommand;
9488
+ var init_open2 = __esm({
9489
+ "src/commands/open.ts"() {
9490
+ "use strict";
9491
+ init_open();
9492
+ init_dispatch();
9493
+ openCommand = defineCommand14({
9494
+ meta: {
9495
+ name: "open",
9496
+ group: "run",
9497
+ 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.`
9498
+ },
9499
+ args: {
9500
+ name: {
9501
+ type: "positional",
9502
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
9503
+ required: true
9504
+ },
9505
+ tool: {
9506
+ type: "positional",
9507
+ description: `What to open it in: ${OPEN_TOOLS.join(", ")}.`,
9508
+ required: true
9509
+ }
9510
+ },
9511
+ run({ args }) {
9512
+ return dispatch(() => runOpen({ name: args.name, tool: args.tool }));
9513
+ }
9514
+ });
9515
+ }
9516
+ });
9517
+
9518
+ // src/commands/port.ts
9519
+ import { defineCommand as defineCommand15 } from "citty";
9520
+ import { consola as consola19 } from "consola";
8923
9521
  async function runPortListing(opts) {
8924
9522
  const out = opts.out ?? process.stdout;
8925
- const info = opts.info ?? ((m) => consola17.info(m));
9523
+ const info = opts.info ?? ((m) => consola19.info(m));
8926
9524
  const parsed = await readConfig(
8927
9525
  containerConfigPath(opts.name, opts.monocerosHome)
8928
9526
  );
@@ -8980,7 +9578,7 @@ var init_port = __esm({
8980
9578
  init_schema();
8981
9579
  init_dynamic();
8982
9580
  init_format();
8983
- portCommand = defineCommand14({
9581
+ portCommand = defineCommand15({
8984
9582
  meta: {
8985
9583
  name: "port",
8986
9584
  group: "discovery",
@@ -8998,7 +9596,7 @@ var init_port = __esm({
8998
9596
  const code = await runPortListing({ name: args.name });
8999
9597
  process.exit(code);
9000
9598
  } catch (err) {
9001
- consola17.error(err instanceof Error ? err.message : String(err));
9599
+ consola19.error(err instanceof Error ? err.message : String(err));
9002
9600
  process.exit(1);
9003
9601
  }
9004
9602
  }
@@ -9007,15 +9605,15 @@ var init_port = __esm({
9007
9605
  });
9008
9606
 
9009
9607
  // src/commands/remove-apt-packages.ts
9010
- import { defineCommand as defineCommand15 } from "citty";
9011
- import { consola as consola18 } from "consola";
9608
+ import { defineCommand as defineCommand16 } from "citty";
9609
+ import { consola as consola20 } from "consola";
9012
9610
  var removeAptPackagesCommand;
9013
9611
  var init_remove_apt_packages = __esm({
9014
9612
  "src/commands/remove-apt-packages.ts"() {
9015
9613
  "use strict";
9016
9614
  init_inner_args();
9017
9615
  init_modify();
9018
- removeAptPackagesCommand = defineCommand15({
9616
+ removeAptPackagesCommand = defineCommand16({
9019
9617
  meta: {
9020
9618
  name: "remove-apt-packages",
9021
9619
  group: "edit",
@@ -9037,7 +9635,7 @@ var init_remove_apt_packages = __esm({
9037
9635
  async run({ args }) {
9038
9636
  const packages = [...getInnerArgs()];
9039
9637
  if (packages.length === 0) {
9040
- consola18.error(
9638
+ consola20.error(
9041
9639
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
9042
9640
  );
9043
9641
  process.exit(1);
@@ -9050,7 +9648,7 @@ var init_remove_apt_packages = __esm({
9050
9648
  });
9051
9649
  process.exit(result.status === "aborted" ? 1 : 0);
9052
9650
  } catch (err) {
9053
- consola18.error(err instanceof Error ? err.message : String(err));
9651
+ consola20.error(err instanceof Error ? err.message : String(err));
9054
9652
  process.exit(1);
9055
9653
  }
9056
9654
  }
@@ -9059,14 +9657,14 @@ var init_remove_apt_packages = __esm({
9059
9657
  });
9060
9658
 
9061
9659
  // src/commands/remove-feature.ts
9062
- import { defineCommand as defineCommand16 } from "citty";
9063
- import { consola as consola19 } from "consola";
9660
+ import { defineCommand as defineCommand17 } from "citty";
9661
+ import { consola as consola21 } from "consola";
9064
9662
  var removeFeatureCommand;
9065
9663
  var init_remove_feature = __esm({
9066
9664
  "src/commands/remove-feature.ts"() {
9067
9665
  "use strict";
9068
9666
  init_modify();
9069
- removeFeatureCommand = defineCommand16({
9667
+ removeFeatureCommand = defineCommand17({
9070
9668
  meta: {
9071
9669
  name: "remove-feature",
9072
9670
  group: "edit",
@@ -9099,7 +9697,7 @@ var init_remove_feature = __esm({
9099
9697
  });
9100
9698
  process.exit(result.status === "aborted" ? 1 : 0);
9101
9699
  } catch (err) {
9102
- consola19.error(err instanceof Error ? err.message : String(err));
9700
+ consola21.error(err instanceof Error ? err.message : String(err));
9103
9701
  process.exit(1);
9104
9702
  }
9105
9703
  }
@@ -9108,15 +9706,15 @@ var init_remove_feature = __esm({
9108
9706
  });
9109
9707
 
9110
9708
  // 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";
9709
+ import { existsSync as existsSync17, promises as fs16 } from "fs";
9710
+ import path26 from "path";
9711
+ import { consola as consola22 } from "consola";
9114
9712
  async function runRemove(opts) {
9115
9713
  const home = opts.monocerosHome ?? monocerosHome();
9116
9714
  const logger = opts.logger ?? {
9117
- info: (msg) => consola20.info(msg),
9118
- success: (msg) => consola20.success(msg),
9119
- warn: (msg) => consola20.warn(msg)
9715
+ info: (msg) => consola22.info(msg),
9716
+ success: (msg) => consola22.success(msg),
9717
+ warn: (msg) => consola22.warn(msg)
9120
9718
  };
9121
9719
  if (!REGEX.solutionName.test(opts.name)) {
9122
9720
  throw new Error(
@@ -9126,9 +9724,9 @@ async function runRemove(opts) {
9126
9724
  const ymlPath = containerConfigPath(opts.name, home);
9127
9725
  const envPath = containerEnvPath(opts.name, home);
9128
9726
  const containerPath = containerDir(opts.name, home);
9129
- const hasYml = existsSync13(ymlPath);
9130
- const hasEnv = existsSync13(envPath);
9131
- const hasContainer = existsSync13(containerPath);
9727
+ const hasYml = existsSync17(ymlPath);
9728
+ const hasEnv = existsSync17(envPath);
9729
+ const hasContainer = existsSync17(containerPath);
9132
9730
  if (!hasYml && !hasContainer) {
9133
9731
  throw new Error(
9134
9732
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -9154,30 +9752,30 @@ async function runRemove(opts) {
9154
9752
  let backupPath = null;
9155
9753
  if (!opts.noBackup && (hasYml || hasContainer)) {
9156
9754
  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 });
9755
+ backupPath = path26.join(home, "container-backups", `${opts.name}-${ts}`);
9756
+ await fs16.mkdir(backupPath, { recursive: true });
9159
9757
  if (hasYml) {
9160
- await fs15.copyFile(ymlPath, path22.join(backupPath, `${opts.name}.yml`));
9758
+ await fs16.copyFile(ymlPath, path26.join(backupPath, `${opts.name}.yml`));
9161
9759
  }
9162
9760
  if (hasEnv) {
9163
- await fs15.copyFile(envPath, path22.join(backupPath, `${opts.name}.env`));
9761
+ await fs16.copyFile(envPath, path26.join(backupPath, `${opts.name}.env`));
9164
9762
  }
9165
9763
  if (hasContainer) {
9166
- await fs15.cp(containerPath, path22.join(backupPath, "container"), {
9764
+ await fs16.cp(containerPath, path26.join(backupPath, "container"), {
9167
9765
  recursive: true
9168
9766
  });
9169
9767
  }
9170
9768
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
9171
9769
  }
9172
9770
  if (hasYml) {
9173
- await fs15.rm(ymlPath, { force: true });
9771
+ await fs16.rm(ymlPath, { force: true });
9174
9772
  }
9175
9773
  if (hasEnv) {
9176
- await fs15.rm(envPath, { force: true });
9774
+ await fs16.rm(envPath, { force: true });
9177
9775
  }
9178
9776
  if (hasContainer) {
9179
9777
  try {
9180
- await fs15.rm(containerPath, { recursive: true, force: true });
9778
+ await fs16.rm(containerPath, { recursive: true, force: true });
9181
9779
  } catch (err) {
9182
9780
  const code = err.code;
9183
9781
  if (code !== "EACCES" && code !== "EPERM") {
@@ -9199,7 +9797,7 @@ async function runRemove(opts) {
9199
9797
  "-delete"
9200
9798
  ]);
9201
9799
  try {
9202
- await fs15.rm(containerPath, { recursive: true, force: true });
9800
+ await fs16.rm(containerPath, { recursive: true, force: true });
9203
9801
  } catch (err2) {
9204
9802
  const code2 = err2.code;
9205
9803
  throw new Error(
@@ -9213,7 +9811,14 @@ async function runRemove(opts) {
9213
9811
  );
9214
9812
  if (!backupPath) {
9215
9813
  logger.warn?.(
9216
- "No backup created (--no-backup). The host-side state is gone for good."
9814
+ "No backup created (--no-backup). The host-side state is gone for good."
9815
+ );
9816
+ }
9817
+ try {
9818
+ await removeSshAttach(home, opts.name);
9819
+ } catch (err) {
9820
+ logger.warn?.(
9821
+ `Could not remove SSH attach config for ${opts.name}: ${err instanceof Error ? err.message : String(err)}. Ignored.`
9217
9822
  );
9218
9823
  }
9219
9824
  try {
@@ -9248,21 +9853,22 @@ var init_remove = __esm({
9248
9853
  init_schema();
9249
9854
  init_compose();
9250
9855
  init_scaffold();
9856
+ init_ssh_attach();
9251
9857
  init_proxy();
9252
9858
  init_dynamic();
9253
9859
  }
9254
9860
  });
9255
9861
 
9256
9862
  // src/commands/remove.ts
9257
- import { defineCommand as defineCommand17 } from "citty";
9258
- import { consola as consola21 } from "consola";
9863
+ import { defineCommand as defineCommand18 } from "citty";
9864
+ import { consola as consola23 } from "consola";
9259
9865
  import { createInterface } from "readline/promises";
9260
9866
  var removeCommand;
9261
9867
  var init_remove2 = __esm({
9262
9868
  "src/commands/remove.ts"() {
9263
9869
  "use strict";
9264
9870
  init_remove();
9265
- removeCommand = defineCommand17({
9871
+ removeCommand = defineCommand18({
9266
9872
  meta: {
9267
9873
  name: "remove",
9268
9874
  group: "lifecycle",
@@ -9299,7 +9905,7 @@ var init_remove2 = __esm({
9299
9905
  const skipPrompt = args.yes === true;
9300
9906
  if (!skipPrompt) {
9301
9907
  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);
9908
+ consola23.warn(warning);
9303
9909
  const rl = createInterface({
9304
9910
  input: process.stdin,
9305
9911
  output: process.stdout
@@ -9307,7 +9913,7 @@ var init_remove2 = __esm({
9307
9913
  const answer = await rl.question("Continue? [y/N] ");
9308
9914
  rl.close();
9309
9915
  if (!/^y(es)?$/i.test(answer.trim())) {
9310
- consola21.info("Aborted. Nothing changed.");
9916
+ consola23.info("Aborted. Nothing changed.");
9311
9917
  process.exit(0);
9312
9918
  }
9313
9919
  }
@@ -9316,7 +9922,7 @@ var init_remove2 = __esm({
9316
9922
  ...noBackup ? { noBackup: true } : {}
9317
9923
  });
9318
9924
  } catch (err) {
9319
- consola21.error(err instanceof Error ? err.message : String(err));
9925
+ consola23.error(err instanceof Error ? err.message : String(err));
9320
9926
  process.exit(1);
9321
9927
  }
9322
9928
  }
@@ -9325,24 +9931,24 @@ var init_remove2 = __esm({
9325
9931
  });
9326
9932
 
9327
9933
  // 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";
9934
+ import { existsSync as existsSync18, promises as fs17 } from "fs";
9935
+ import path27 from "path";
9936
+ import { consola as consola24 } from "consola";
9331
9937
  async function runRestore(opts) {
9332
9938
  const home = opts.monocerosHome ?? monocerosHome();
9333
9939
  const logger = opts.logger ?? {
9334
- info: (msg) => consola22.info(msg),
9335
- success: (msg) => consola22.success(msg)
9940
+ info: (msg) => consola24.info(msg),
9941
+ success: (msg) => consola24.success(msg)
9336
9942
  };
9337
- const backup = path23.resolve(opts.backupPath);
9338
- if (!existsSync14(backup)) {
9943
+ const backup = path27.resolve(opts.backupPath);
9944
+ if (!existsSync18(backup)) {
9339
9945
  throw new Error(`Backup not found: ${backup}.`);
9340
9946
  }
9341
- const stat = await fs16.stat(backup);
9947
+ const stat = await fs17.stat(backup);
9342
9948
  if (!stat.isDirectory()) {
9343
9949
  throw new Error(`Backup path is not a directory: ${backup}.`);
9344
9950
  }
9345
- const entries = await fs16.readdir(backup);
9951
+ const entries = await fs17.readdir(backup);
9346
9952
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
9347
9953
  if (ymlFiles.length === 0) {
9348
9954
  throw new Error(
@@ -9356,29 +9962,29 @@ async function runRestore(opts) {
9356
9962
  }
9357
9963
  const ymlFile = ymlFiles[0];
9358
9964
  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);
9965
+ const containerInBackup = path27.join(backup, "container");
9966
+ const hasContainer = existsSync18(containerInBackup);
9967
+ const envInBackup = path27.join(backup, `${name}.env`);
9968
+ const hasEnv = existsSync18(envInBackup);
9363
9969
  const destYml = containerConfigPath(name, home);
9364
9970
  const destContainer = containerDir(name, home);
9365
- if (existsSync14(destYml)) {
9971
+ if (existsSync18(destYml)) {
9366
9972
  throw new Error(
9367
9973
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
9368
9974
  );
9369
9975
  }
9370
- if (hasContainer && existsSync14(destContainer)) {
9976
+ if (hasContainer && existsSync18(destContainer)) {
9371
9977
  throw new Error(
9372
9978
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
9373
9979
  );
9374
9980
  }
9375
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
9376
- await fs16.copyFile(path23.join(backup, ymlFile), destYml);
9981
+ await fs17.mkdir(containerConfigsDir(home), { recursive: true });
9982
+ await fs17.copyFile(path27.join(backup, ymlFile), destYml);
9377
9983
  if (hasEnv) {
9378
- await fs16.copyFile(envInBackup, containerEnvPath(name, home));
9984
+ await fs17.copyFile(envInBackup, containerEnvPath(name, home));
9379
9985
  }
9380
9986
  if (hasContainer) {
9381
- await fs16.cp(containerInBackup, destContainer, { recursive: true });
9987
+ await fs17.cp(containerInBackup, destContainer, { recursive: true });
9382
9988
  }
9383
9989
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
9384
9990
  logger.info(
@@ -9398,14 +10004,14 @@ var init_restore = __esm({
9398
10004
  });
9399
10005
 
9400
10006
  // src/commands/restore.ts
9401
- import { defineCommand as defineCommand18 } from "citty";
9402
- import { consola as consola23 } from "consola";
10007
+ import { defineCommand as defineCommand19 } from "citty";
10008
+ import { consola as consola25 } from "consola";
9403
10009
  var restoreCommand;
9404
10010
  var init_restore2 = __esm({
9405
10011
  "src/commands/restore.ts"() {
9406
10012
  "use strict";
9407
10013
  init_restore();
9408
- restoreCommand = defineCommand18({
10014
+ restoreCommand = defineCommand19({
9409
10015
  meta: {
9410
10016
  name: "restore",
9411
10017
  group: "lifecycle",
@@ -9422,7 +10028,7 @@ var init_restore2 = __esm({
9422
10028
  try {
9423
10029
  await runRestore({ backupPath: args["backup-path"] });
9424
10030
  } catch (err) {
9425
- consola23.error(err instanceof Error ? err.message : String(err));
10031
+ consola25.error(err instanceof Error ? err.message : String(err));
9426
10032
  process.exit(1);
9427
10033
  }
9428
10034
  }
@@ -9431,14 +10037,14 @@ var init_restore2 = __esm({
9431
10037
  });
9432
10038
 
9433
10039
  // src/commands/remove-from-url.ts
9434
- import { defineCommand as defineCommand19 } from "citty";
9435
- import { consola as consola24 } from "consola";
10040
+ import { defineCommand as defineCommand20 } from "citty";
10041
+ import { consola as consola26 } from "consola";
9436
10042
  var removeFromUrlCommand;
9437
10043
  var init_remove_from_url = __esm({
9438
10044
  "src/commands/remove-from-url.ts"() {
9439
10045
  "use strict";
9440
10046
  init_modify();
9441
- removeFromUrlCommand = defineCommand19({
10047
+ removeFromUrlCommand = defineCommand20({
9442
10048
  meta: {
9443
10049
  name: "remove-from-url",
9444
10050
  group: "edit",
@@ -9471,7 +10077,7 @@ var init_remove_from_url = __esm({
9471
10077
  });
9472
10078
  process.exit(result.status === "aborted" ? 1 : 0);
9473
10079
  } catch (err) {
9474
- consola24.error(err instanceof Error ? err.message : String(err));
10080
+ consola26.error(err instanceof Error ? err.message : String(err));
9475
10081
  process.exit(1);
9476
10082
  }
9477
10083
  }
@@ -9480,14 +10086,14 @@ var init_remove_from_url = __esm({
9480
10086
  });
9481
10087
 
9482
10088
  // src/commands/remove-language.ts
9483
- import { defineCommand as defineCommand20 } from "citty";
9484
- import { consola as consola25 } from "consola";
10089
+ import { defineCommand as defineCommand21 } from "citty";
10090
+ import { consola as consola27 } from "consola";
9485
10091
  var removeLanguageCommand;
9486
10092
  var init_remove_language = __esm({
9487
10093
  "src/commands/remove-language.ts"() {
9488
10094
  "use strict";
9489
10095
  init_modify();
9490
- removeLanguageCommand = defineCommand20({
10096
+ removeLanguageCommand = defineCommand21({
9491
10097
  meta: {
9492
10098
  name: "remove-language",
9493
10099
  group: "edit",
@@ -9520,7 +10126,7 @@ var init_remove_language = __esm({
9520
10126
  });
9521
10127
  process.exit(result.status === "aborted" ? 1 : 0);
9522
10128
  } catch (err) {
9523
- consola25.error(err instanceof Error ? err.message : String(err));
10129
+ consola27.error(err instanceof Error ? err.message : String(err));
9524
10130
  process.exit(1);
9525
10131
  }
9526
10132
  }
@@ -9529,8 +10135,8 @@ var init_remove_language = __esm({
9529
10135
  });
9530
10136
 
9531
10137
  // src/commands/remove-port.ts
9532
- import { defineCommand as defineCommand21 } from "citty";
9533
- import { consola as consola26 } from "consola";
10138
+ import { defineCommand as defineCommand22 } from "citty";
10139
+ import { consola as consola28 } from "consola";
9534
10140
  function coerceToken2(raw) {
9535
10141
  const n = Number(raw);
9536
10142
  return Number.isFinite(n) ? n : raw;
@@ -9541,7 +10147,7 @@ var init_remove_port = __esm({
9541
10147
  "use strict";
9542
10148
  init_inner_args();
9543
10149
  init_modify();
9544
- removePortCommand = defineCommand21({
10150
+ removePortCommand = defineCommand22({
9545
10151
  meta: {
9546
10152
  name: "remove-port",
9547
10153
  group: "edit",
@@ -9563,7 +10169,7 @@ var init_remove_port = __esm({
9563
10169
  async run({ args }) {
9564
10170
  const tokens = [...getInnerArgs()];
9565
10171
  if (tokens.length === 0) {
9566
- consola26.error(
10172
+ consola28.error(
9567
10173
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
9568
10174
  );
9569
10175
  process.exit(1);
@@ -9576,7 +10182,7 @@ var init_remove_port = __esm({
9576
10182
  });
9577
10183
  process.exit(result.status === "aborted" ? 1 : 0);
9578
10184
  } catch (err) {
9579
- consola26.error(err instanceof Error ? err.message : String(err));
10185
+ consola28.error(err instanceof Error ? err.message : String(err));
9580
10186
  process.exit(1);
9581
10187
  }
9582
10188
  }
@@ -9585,14 +10191,14 @@ var init_remove_port = __esm({
9585
10191
  });
9586
10192
 
9587
10193
  // src/commands/remove-repo.ts
9588
- import { defineCommand as defineCommand22 } from "citty";
9589
- import { consola as consola27 } from "consola";
10194
+ import { defineCommand as defineCommand23 } from "citty";
10195
+ import { consola as consola29 } from "consola";
9590
10196
  var removeRepoCommand;
9591
10197
  var init_remove_repo = __esm({
9592
10198
  "src/commands/remove-repo.ts"() {
9593
10199
  "use strict";
9594
10200
  init_modify();
9595
- removeRepoCommand = defineCommand22({
10201
+ removeRepoCommand = defineCommand23({
9596
10202
  meta: {
9597
10203
  name: "remove-repo",
9598
10204
  group: "edit",
@@ -9625,7 +10231,7 @@ var init_remove_repo = __esm({
9625
10231
  });
9626
10232
  process.exit(result.status === "aborted" ? 1 : 0);
9627
10233
  } catch (err) {
9628
- consola27.error(err instanceof Error ? err.message : String(err));
10234
+ consola29.error(err instanceof Error ? err.message : String(err));
9629
10235
  process.exit(1);
9630
10236
  }
9631
10237
  }
@@ -9634,14 +10240,14 @@ var init_remove_repo = __esm({
9634
10240
  });
9635
10241
 
9636
10242
  // src/commands/remove-service.ts
9637
- import { defineCommand as defineCommand23 } from "citty";
9638
- import { consola as consola28 } from "consola";
10243
+ import { defineCommand as defineCommand24 } from "citty";
10244
+ import { consola as consola30 } from "consola";
9639
10245
  var removeServiceCommand;
9640
10246
  var init_remove_service = __esm({
9641
10247
  "src/commands/remove-service.ts"() {
9642
10248
  "use strict";
9643
10249
  init_modify();
9644
- removeServiceCommand = defineCommand23({
10250
+ removeServiceCommand = defineCommand24({
9645
10251
  meta: {
9646
10252
  name: "remove-service",
9647
10253
  group: "edit",
@@ -9674,7 +10280,7 @@ var init_remove_service = __esm({
9674
10280
  });
9675
10281
  process.exit(result.status === "aborted" ? 1 : 0);
9676
10282
  } catch (err) {
9677
- consola28.error(err instanceof Error ? err.message : String(err));
10283
+ consola30.error(err instanceof Error ? err.message : String(err));
9678
10284
  process.exit(1);
9679
10285
  }
9680
10286
  }
@@ -9682,148 +10288,17 @@ var init_remove_service = __esm({
9682
10288
  }
9683
10289
  });
9684
10290
 
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
10291
  // src/devcontainer/claude-trust.ts
9817
- import { existsSync as existsSync16, promises as fsp6 } from "fs";
9818
- import path25 from "path";
10292
+ import { existsSync as existsSync19, promises as fsp6 } from "fs";
10293
+ import path28 from "path";
9819
10294
  function resolveContainerCwd(name, cwd) {
9820
10295
  const workspace = `/workspaces/${name}`;
9821
10296
  if (!cwd) return workspace;
9822
- return path25.posix.isAbsolute(cwd) ? cwd : path25.posix.join(workspace, cwd);
10297
+ return path28.posix.isAbsolute(cwd) ? cwd : path28.posix.join(workspace, cwd);
9823
10298
  }
9824
10299
  async function preApproveClaudeProject(opts) {
9825
- const file = path25.join(opts.root, "home", ".claude.json");
9826
- if (!existsSync16(file)) return;
10300
+ const file = path28.join(opts.root, "home", ".claude.json");
10301
+ if (!existsSync19(file)) return;
9827
10302
  try {
9828
10303
  const raw = await fsp6.readFile(file, "utf8");
9829
10304
  const config = raw.trim() ? JSON.parse(raw) : {};
@@ -9856,56 +10331,6 @@ var init_claude_trust = __esm({
9856
10331
  }
9857
10332
  });
9858
10333
 
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
10334
  // src/devcontainer/run.ts
9910
10335
  async function runInContainer(opts) {
9911
10336
  if (opts.command.length === 0) {
@@ -9964,8 +10389,8 @@ var init_run = __esm({
9964
10389
  });
9965
10390
 
9966
10391
  // src/commands/run.ts
9967
- import { defineCommand as defineCommand24 } from "citty";
9968
- import { consola as consola29 } from "consola";
10392
+ import { defineCommand as defineCommand25 } from "citty";
10393
+ import { consola as consola31 } from "consola";
9969
10394
  var runCommand;
9970
10395
  var init_run2 = __esm({
9971
10396
  "src/commands/run.ts"() {
@@ -9973,7 +10398,7 @@ var init_run2 = __esm({
9973
10398
  init_paths();
9974
10399
  init_run();
9975
10400
  init_inner_args();
9976
- runCommand = defineCommand24({
10401
+ runCommand = defineCommand25({
9977
10402
  meta: {
9978
10403
  name: "run",
9979
10404
  group: "run",
@@ -9993,7 +10418,7 @@ var init_run2 = __esm({
9993
10418
  async run({ args }) {
9994
10419
  const command = [...getInnerArgs()];
9995
10420
  if (command.length === 0) {
9996
- consola29.error(
10421
+ consola31.error(
9997
10422
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
9998
10423
  );
9999
10424
  process.exit(1);
@@ -10007,7 +10432,7 @@ var init_run2 = __esm({
10007
10432
  });
10008
10433
  process.exit(exitCode);
10009
10434
  } catch (err) {
10010
- consola29.error(err instanceof Error ? err.message : String(err));
10435
+ consola31.error(err instanceof Error ? err.message : String(err));
10011
10436
  process.exit(1);
10012
10437
  }
10013
10438
  }
@@ -10016,15 +10441,15 @@ var init_run2 = __esm({
10016
10441
  });
10017
10442
 
10018
10443
  // src/commands/shell.ts
10019
- import { defineCommand as defineCommand25 } from "citty";
10020
- import { consola as consola30 } from "consola";
10444
+ import { defineCommand as defineCommand26 } from "citty";
10445
+ import { consola as consola32 } from "consola";
10021
10446
  var shellCommand;
10022
10447
  var init_shell2 = __esm({
10023
10448
  "src/commands/shell.ts"() {
10024
10449
  "use strict";
10025
10450
  init_paths();
10026
10451
  init_shell();
10027
- shellCommand = defineCommand25({
10452
+ shellCommand = defineCommand26({
10028
10453
  meta: {
10029
10454
  name: "shell",
10030
10455
  group: "run",
@@ -10045,7 +10470,7 @@ var init_shell2 = __esm({
10045
10470
  });
10046
10471
  process.exit(exitCode);
10047
10472
  } catch (err) {
10048
- consola30.error(err instanceof Error ? err.message : String(err));
10473
+ consola32.error(err instanceof Error ? err.message : String(err));
10049
10474
  process.exit(1);
10050
10475
  }
10051
10476
  }
@@ -10054,8 +10479,8 @@ var init_shell2 = __esm({
10054
10479
  });
10055
10480
 
10056
10481
  // src/commands/start.ts
10057
- import { defineCommand as defineCommand26 } from "citty";
10058
- import { consola as consola31 } from "consola";
10482
+ import { defineCommand as defineCommand27 } from "citty";
10483
+ import { consola as consola33 } from "consola";
10059
10484
  var startCommand;
10060
10485
  var init_start = __esm({
10061
10486
  "src/commands/start.ts"() {
@@ -10064,10 +10489,11 @@ var init_start = __esm({
10064
10489
  init_io();
10065
10490
  init_paths();
10066
10491
  init_compose();
10492
+ init_open();
10067
10493
  init_proxy();
10068
10494
  init_port_check();
10069
10495
  init_dispatch();
10070
- startCommand = defineCommand26({
10496
+ startCommand = defineCommand27({
10071
10497
  meta: {
10072
10498
  name: "start",
10073
10499
  group: "run",
@@ -10078,6 +10504,10 @@ var init_start = __esm({
10078
10504
  type: "positional",
10079
10505
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10080
10506
  required: true
10507
+ },
10508
+ open: {
10509
+ type: "string",
10510
+ description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
10081
10511
  }
10082
10512
  },
10083
10513
  run({ args }) {
@@ -10092,7 +10522,7 @@ var init_start = __esm({
10092
10522
  hostPort = proxyHostPort(global);
10093
10523
  }
10094
10524
  } catch (err) {
10095
- consola31.warn(
10525
+ consola33.warn(
10096
10526
  `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
10097
10527
  );
10098
10528
  }
@@ -10100,7 +10530,15 @@ var init_start = __esm({
10100
10530
  await preflightHostPort(hostPort);
10101
10531
  await ensureProxy({ hostPort });
10102
10532
  }
10103
- return runStart({ root: containerDir(args.name) });
10533
+ const exitCode = await runStart({ root: containerDir(args.name) });
10534
+ if (args.open && exitCode === 0) {
10535
+ try {
10536
+ await runOpen({ name: args.name, tool: args.open });
10537
+ } catch (err) {
10538
+ consola33.warn(err instanceof Error ? err.message : String(err));
10539
+ }
10540
+ }
10541
+ return exitCode;
10104
10542
  });
10105
10543
  }
10106
10544
  });
@@ -10108,7 +10546,7 @@ var init_start = __esm({
10108
10546
  });
10109
10547
 
10110
10548
  // src/commands/status.ts
10111
- import { defineCommand as defineCommand27 } from "citty";
10549
+ import { defineCommand as defineCommand28 } from "citty";
10112
10550
  var statusCommand;
10113
10551
  var init_status = __esm({
10114
10552
  "src/commands/status.ts"() {
@@ -10116,7 +10554,7 @@ var init_status = __esm({
10116
10554
  init_paths();
10117
10555
  init_compose();
10118
10556
  init_dispatch();
10119
- statusCommand = defineCommand27({
10557
+ statusCommand = defineCommand28({
10120
10558
  meta: {
10121
10559
  name: "status",
10122
10560
  group: "run",
@@ -10146,8 +10584,8 @@ var init_status = __esm({
10146
10584
  });
10147
10585
 
10148
10586
  // src/commands/stop.ts
10149
- import { defineCommand as defineCommand28 } from "citty";
10150
- import { consola as consola32 } from "consola";
10587
+ import { defineCommand as defineCommand29 } from "citty";
10588
+ import { consola as consola34 } from "consola";
10151
10589
  var stopCommand;
10152
10590
  var init_stop = __esm({
10153
10591
  "src/commands/stop.ts"() {
@@ -10156,7 +10594,7 @@ var init_stop = __esm({
10156
10594
  init_compose();
10157
10595
  init_proxy();
10158
10596
  init_dispatch();
10159
- stopCommand = defineCommand28({
10597
+ stopCommand = defineCommand29({
10160
10598
  meta: {
10161
10599
  name: "stop",
10162
10600
  group: "run",
@@ -10181,10 +10619,10 @@ var init_stop = __esm({
10181
10619
  });
10182
10620
  try {
10183
10621
  await maybeStopProxy({
10184
- logger: { info: (msg) => consola32.info(msg) }
10622
+ logger: { info: (msg) => consola34.info(msg) }
10185
10623
  });
10186
10624
  } catch (err) {
10187
- consola32.warn(
10625
+ consola34.warn(
10188
10626
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
10189
10627
  );
10190
10628
  }
@@ -10196,11 +10634,11 @@ var init_stop = __esm({
10196
10634
  });
10197
10635
 
10198
10636
  // src/tunnel/resolve.ts
10199
- import { existsSync as existsSync18 } from "fs";
10200
- import path27 from "path";
10637
+ import { existsSync as existsSync20 } from "fs";
10638
+ import path29 from "path";
10201
10639
  async function resolveTunnelTarget(opts) {
10202
10640
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
10203
- if (!existsSync18(ymlPath)) {
10641
+ if (!existsSync20(ymlPath)) {
10204
10642
  throw new Error(
10205
10643
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
10206
10644
  );
@@ -10208,13 +10646,13 @@ async function resolveTunnelTarget(opts) {
10208
10646
  const parsed = await readConfig(ymlPath);
10209
10647
  const config = parsed.config;
10210
10648
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
10211
- if (!existsSync18(containerRoot)) {
10649
+ if (!existsSync20(containerRoot)) {
10212
10650
  throw new Error(
10213
10651
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
10214
10652
  );
10215
10653
  }
10216
- const composePath = path27.join(containerRoot, ".devcontainer", "compose.yaml");
10217
- const isCompose = existsSync18(composePath);
10654
+ const composePath = path29.join(containerRoot, ".devcontainer", "compose.yaml");
10655
+ const isCompose = existsSync20(composePath);
10218
10656
  const parsedTarget = parseTargetArg(opts.target, config);
10219
10657
  const docker = opts.docker ?? defaultDockerExec;
10220
10658
  if (isCompose) {
@@ -10449,8 +10887,8 @@ var init_port_check2 = __esm({
10449
10887
  });
10450
10888
 
10451
10889
  // src/tunnel/run.ts
10452
- import { spawn as spawn10 } from "child_process";
10453
- import { consola as consola33 } from "consola";
10890
+ import { spawn as spawn12 } from "child_process";
10891
+ import { consola as consola35 } from "consola";
10454
10892
  function signalNumber(signal) {
10455
10893
  switch (signal) {
10456
10894
  case "SIGINT":
@@ -10463,8 +10901,8 @@ function signalNumber(signal) {
10463
10901
  }
10464
10902
  async function runTunnel(opts) {
10465
10903
  const log = opts.logger ?? {
10466
- info: (m) => consola33.info(m),
10467
- warn: (m) => consola33.warn(m)
10904
+ info: (m) => consola35.info(m),
10905
+ warn: (m) => consola35.warn(m)
10468
10906
  };
10469
10907
  const resolve = opts.resolve ?? resolveTunnelTarget;
10470
10908
  const resolveArgs3 = {
@@ -10539,7 +10977,7 @@ var init_run3 = __esm({
10539
10977
  init_port_check2();
10540
10978
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
10541
10979
  defaultDockerSpawn = (args) => {
10542
- const child = spawn10("docker", args, {
10980
+ const child = spawn12("docker", args, {
10543
10981
  stdio: "inherit"
10544
10982
  });
10545
10983
  const exited = new Promise((resolve, reject) => {
@@ -10570,8 +11008,8 @@ var init_run3 = __esm({
10570
11008
  });
10571
11009
 
10572
11010
  // src/commands/tunnel.ts
10573
- import { defineCommand as defineCommand29 } from "citty";
10574
- import { consola as consola34 } from "consola";
11011
+ import { defineCommand as defineCommand30 } from "citty";
11012
+ import { consola as consola36 } from "consola";
10575
11013
  function parseLocalPort(raw) {
10576
11014
  if (raw === void 0) return void 0;
10577
11015
  const n = Number(raw);
@@ -10587,7 +11025,7 @@ var init_tunnel = __esm({
10587
11025
  "src/commands/tunnel.ts"() {
10588
11026
  "use strict";
10589
11027
  init_run3();
10590
- tunnelCommand = defineCommand29({
11028
+ tunnelCommand = defineCommand30({
10591
11029
  meta: {
10592
11030
  name: "tunnel",
10593
11031
  group: "discovery",
@@ -10624,7 +11062,7 @@ var init_tunnel = __esm({
10624
11062
  });
10625
11063
  process.exit(exitCode);
10626
11064
  } catch (err) {
10627
- consola34.error(err instanceof Error ? err.message : String(err));
11065
+ consola36.error(err instanceof Error ? err.message : String(err));
10628
11066
  process.exit(1);
10629
11067
  }
10630
11068
  }
@@ -10694,8 +11132,8 @@ var init_prune = __esm({
10694
11132
  });
10695
11133
 
10696
11134
  // src/upgrade/index.ts
10697
- import { existsSync as existsSync19, promises as fs17 } from "fs";
10698
- import { consola as consola35 } from "consola";
11135
+ import { existsSync as existsSync21, promises as fs18 } from "fs";
11136
+ import { consola as consola37 } from "consola";
10699
11137
  async function fetchRuntimeVersions() {
10700
11138
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
10701
11139
  const tokenRes = await fetch(tokenUrl);
@@ -10733,7 +11171,7 @@ ${yml}`;
10733
11171
  }
10734
11172
  async function runUpgrade(opts) {
10735
11173
  const home = opts.monocerosHome ?? monocerosHome();
10736
- const logger = opts.logger ?? consola35;
11174
+ const logger = opts.logger ?? consola37;
10737
11175
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
10738
11176
  if (opts.list) {
10739
11177
  const versions = await fetchVersions();
@@ -10759,7 +11197,7 @@ async function runUpgrade(opts) {
10759
11197
  );
10760
11198
  }
10761
11199
  }
10762
- if (opts.name && !existsSync19(containerConfigPath(opts.name, home))) {
11200
+ if (opts.name && !existsSync21(containerConfigPath(opts.name, home))) {
10763
11201
  throw new Error(
10764
11202
  `No such config: ${containerConfigPath(opts.name, home)}. Run \`monoceros init <template> ${opts.name}\` first.`
10765
11203
  );
@@ -10810,11 +11248,11 @@ async function runUpgrade(opts) {
10810
11248
  let bumped = 0;
10811
11249
  for (const name of targets) {
10812
11250
  const ymlPath = containerConfigPath(name, home);
10813
- if (!existsSync19(ymlPath)) continue;
10814
- const raw = await fs17.readFile(ymlPath, "utf8");
11251
+ if (!existsSync21(ymlPath)) continue;
11252
+ const raw = await fs18.readFile(ymlPath, "utf8");
10815
11253
  const updated = setRuntimeVersion(raw, pinVersion);
10816
11254
  if (updated !== raw) {
10817
- await fs17.writeFile(ymlPath, updated);
11255
+ await fs18.writeFile(ymlPath, updated);
10818
11256
  bumped += 1;
10819
11257
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
10820
11258
  }
@@ -10856,7 +11294,7 @@ function formatPruneLine(prune) {
10856
11294
  }
10857
11295
  async function listContainerNames2(home) {
10858
11296
  try {
10859
- const entries = await fs17.readdir(containerConfigsDir(home));
11297
+ const entries = await fs18.readdir(containerConfigsDir(home));
10860
11298
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
10861
11299
  } catch {
10862
11300
  return [];
@@ -10879,7 +11317,7 @@ var init_upgrade = __esm({
10879
11317
  });
10880
11318
 
10881
11319
  // src/commands/upgrade.ts
10882
- import { defineCommand as defineCommand30 } from "citty";
11320
+ import { defineCommand as defineCommand31 } from "citty";
10883
11321
  var upgradeCommand;
10884
11322
  var init_upgrade2 = __esm({
10885
11323
  "src/commands/upgrade.ts"() {
@@ -10887,7 +11325,7 @@ var init_upgrade2 = __esm({
10887
11325
  init_upgrade();
10888
11326
  init_version();
10889
11327
  init_dispatch();
10890
- upgradeCommand = defineCommand30({
11328
+ upgradeCommand = defineCommand31({
10891
11329
  meta: {
10892
11330
  name: "upgrade",
10893
11331
  group: "lifecycle",
@@ -10929,7 +11367,7 @@ var main_exports = {};
10929
11367
  __export(main_exports, {
10930
11368
  main: () => main
10931
11369
  });
10932
- import { defineCommand as defineCommand31 } from "citty";
11370
+ import { defineCommand as defineCommand32 } from "citty";
10933
11371
  var main;
10934
11372
  var init_main = __esm({
10935
11373
  "src/main.ts"() {
@@ -10947,6 +11385,7 @@ var init_main = __esm({
10947
11385
  init_init2();
10948
11386
  init_list_components();
10949
11387
  init_logs();
11388
+ init_open2();
10950
11389
  init_port();
10951
11390
  init_remove_apt_packages();
10952
11391
  init_remove_feature();
@@ -10965,7 +11404,7 @@ var init_main = __esm({
10965
11404
  init_tunnel();
10966
11405
  init_upgrade2();
10967
11406
  init_version();
10968
- main = defineCommand31({
11407
+ main = defineCommand32({
10969
11408
  meta: {
10970
11409
  name: "monoceros",
10971
11410
  version: CLI_VERSION,
@@ -10975,6 +11414,7 @@ var init_main = __esm({
10975
11414
  init: initCommand,
10976
11415
  "list-components": listComponentsCommand,
10977
11416
  shell: shellCommand,
11417
+ open: openCommand,
10978
11418
  run: runCommand,
10979
11419
  logs: logsCommand,
10980
11420
  start: startCommand,
@@ -11261,25 +11701,25 @@ function detectHelpRequest(argv, main2) {
11261
11701
  const separatorIdx = argv.indexOf("--");
11262
11702
  if (helpIdx === -1) return null;
11263
11703
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
11264
- const path28 = [];
11704
+ const path30 = [];
11265
11705
  const tokens = argv.slice(
11266
11706
  0,
11267
11707
  separatorIdx === -1 ? argv.length : separatorIdx
11268
11708
  );
11269
11709
  let cursor = main2;
11270
11710
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
11271
- path28.push(mainName);
11711
+ path30.push(mainName);
11272
11712
  for (const tok of tokens) {
11273
11713
  if (tok.startsWith("-")) continue;
11274
11714
  const subs = cursor.subCommands ?? {};
11275
11715
  if (tok in subs) {
11276
11716
  cursor = subs[tok];
11277
- path28.push(tok);
11717
+ path30.push(tok);
11278
11718
  continue;
11279
11719
  }
11280
11720
  break;
11281
11721
  }
11282
- return { path: path28, cmd: cursor };
11722
+ return { path: path30, cmd: cursor };
11283
11723
  }
11284
11724
  async function maybeRenderHelp(argv, main2) {
11285
11725
  const hit = detectHelpRequest(argv, main2);