@getmonoceros/workbench 1.26.4 → 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
@@ -74,7 +74,7 @@ ${issues}`);
74
74
  }
75
75
  return result.data;
76
76
  }
77
- var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, POSTGRES_URL_RE, REGEX, PROVIDER_VALUES, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, ExternalServicesSchema, LanguageOptionValueSchema, LanguageEntrySchema, SolutionConfigSchema;
77
+ var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, REGEX, PROVIDER_VALUES, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, LanguageOptionValueSchema, LanguageEntrySchema, SolutionConfigSchema;
78
78
  var init_schema = __esm({
79
79
  "src/config/schema.ts"() {
80
80
  "use strict";
@@ -85,15 +85,13 @@ var init_schema = __esm({
85
85
  INSTALL_URL_RE = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
86
86
  REPO_URL_RE = /^https:\/\/[A-Za-z0-9@:/+_~.#=&?-]+$/;
87
87
  REPO_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
88
- POSTGRES_URL_RE = /^postgres(ql)?:\/\//;
89
88
  REGEX = {
90
89
  solutionName: SOLUTION_NAME_RE,
91
90
  aptPackage: APT_PACKAGE_NAME_RE,
92
91
  featureRef: FEATURE_REF_RE,
93
92
  installUrl: INSTALL_URL_RE,
94
93
  repoUrl: REPO_URL_RE,
95
- repoPath: REPO_PATH_RE,
96
- postgresUrl: POSTGRES_URL_RE
94
+ repoPath: REPO_PATH_RE
97
95
  };
98
96
  PROVIDER_VALUES = [
99
97
  "github",
@@ -217,12 +215,6 @@ var init_schema = __esm({
217
215
  // are left untouched by `interpolateServices`.
218
216
  connectionEnv: z.record(z.string(), z.string()).optional()
219
217
  });
220
- ExternalServicesSchema = z.object({
221
- postgres: z.string().regex(
222
- POSTGRES_URL_RE,
223
- "Postgres URL must start with 'postgres://' or 'postgresql://'"
224
- ).optional()
225
- });
226
218
  LanguageOptionValueSchema = z.union([
227
219
  z.string(),
228
220
  z.number(),
@@ -269,7 +261,6 @@ var init_schema = __esm({
269
261
  services: z.array(ServiceObjectSchema).default([]),
270
262
  repos: z.array(RepoEntrySchema).default([]),
271
263
  routing: RoutingSchema.optional(),
272
- externalServices: ExternalServicesSchema.default({}),
273
264
  git: z.object({
274
265
  user: GitUserSchema.optional()
275
266
  }).optional()
@@ -2155,9 +2146,9 @@ function compareRuntimeVersions(a, b) {
2155
2146
  }
2156
2147
  return 0;
2157
2148
  }
2158
- function runtimeSupportsIdeVolumes(version) {
2149
+ function runtimeSupportsSshAttach(version) {
2159
2150
  if (!version) return false;
2160
- return compareRuntimeVersions(version, MIN_RUNTIME_FOR_IDE_VOLUMES) >= 0;
2151
+ return compareRuntimeVersions(version, MIN_RUNTIME_FOR_SSH_ATTACH) >= 0;
2161
2152
  }
2162
2153
  function descriptorOptionDefaults(options) {
2163
2154
  const out = {};
@@ -2278,7 +2269,7 @@ function deriveServiceName(image) {
2278
2269
  const noTag = lastSegment.split("@")[0].split(":")[0];
2279
2270
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
2280
2271
  }
2281
- 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;
2282
2273
  var init_catalog = __esm({
2283
2274
  "src/create/catalog.ts"() {
2284
2275
  "use strict";
@@ -2287,13 +2278,13 @@ var init_catalog = __esm({
2287
2278
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2288
2279
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2289
2280
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2290
- DEFAULT_RUNTIME_VERSION = true ? "1.1.0" : readFileSync3(
2281
+ DEFAULT_RUNTIME_VERSION = true ? "1.2.0" : readFileSync3(
2291
2282
  fileURLToPath2(
2292
2283
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2293
2284
  ),
2294
2285
  "utf8"
2295
2286
  ).trim();
2296
- MIN_RUNTIME_FOR_IDE_VOLUMES = "1.1.0";
2287
+ MIN_RUNTIME_FOR_SSH_ATTACH = "1.2.0";
2297
2288
  DESCRIPTORS = loadDescriptorCatalogSync();
2298
2289
  BUILTIN_LANGUAGES = new Set(
2299
2290
  [...DESCRIPTORS.values()].filter((c) => c.category === "language" && c.descriptor.language?.builtin).map(descriptorSelector)
@@ -2798,7 +2789,6 @@ function normalizeOptions(opts) {
2798
2789
  const languages = [...new Set(opts.languages)].sort();
2799
2790
  const serviceByName = /* @__PURE__ */ new Map();
2800
2791
  for (const svc of opts.services) {
2801
- if (opts.postgresUrl && svc.name === "postgres") continue;
2802
2792
  serviceByName.set(svc.name, svc);
2803
2793
  }
2804
2794
  const services = [...serviceByName.values()].sort(
@@ -2819,7 +2809,6 @@ function normalizeOptions(opts) {
2819
2809
  languages,
2820
2810
  ...opts.languageOptions && Object.keys(opts.languageOptions).length > 0 ? { languageOptions: opts.languageOptions } : {},
2821
2811
  services,
2822
- postgresUrl: opts.postgresUrl,
2823
2812
  ...aptPackages.length > 0 ? { aptPackages } : {},
2824
2813
  ...features && Object.keys(features).length > 0 ? { features } : {},
2825
2814
  ...installUrls && installUrls.length > 0 ? { installUrls } : {},
@@ -2958,14 +2947,32 @@ function ideStateVolumes(name) {
2958
2947
  return [
2959
2948
  {
2960
2949
  volume: `monoceros-${name}-vscode-extensions`,
2961
- target: "/home/node/.vscode-server/extensions"
2950
+ target: "/home/node/.vscode-server/extensions",
2951
+ minRuntime: "1.1.0"
2962
2952
  },
2963
2953
  {
2964
2954
  volume: `monoceros-${name}-vscode-userdata`,
2965
- 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"
2966
2967
  }
2967
2968
  ];
2968
2969
  }
2970
+ function ideStateVolumesForRuntime(name, version) {
2971
+ if (!version) return [];
2972
+ return ideStateVolumes(name).filter(
2973
+ (v) => compareRuntimeVersions(version, v.minRuntime) >= 0
2974
+ );
2975
+ }
2969
2976
  function buildDevcontainerJson(opts, dockerMode = "rootful") {
2970
2977
  const resolvedFeatures = resolveFeatures(opts);
2971
2978
  const features = {};
@@ -2997,6 +3004,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
2997
3004
  }
2998
3005
  }
2999
3006
  } : void 0;
3007
+ const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
3000
3008
  if (needsCompose(opts)) {
3001
3009
  return {
3002
3010
  name: opts.name,
@@ -3007,20 +3015,22 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3007
3015
  remoteUser: "node",
3008
3016
  forwardPorts: ports,
3009
3017
  postCreateCommand: ".devcontainer/post-create.sh",
3018
+ ...sshPostStart,
3010
3019
  ...featuresField ?? {},
3011
3020
  ...customizationsField ?? {}
3012
3021
  };
3013
3022
  }
3014
- const ideMounts = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name).map(
3015
- (v) => `source=${v.volume},target=${v.target},type=volume`
3016
- ) : [];
3023
+ const ideMounts = ideStateVolumesForRuntime(
3024
+ opts.name,
3025
+ opts.runtimeVersion
3026
+ ).map((v) => `source=${v.volume},target=${v.target},type=volume`);
3017
3027
  const mounts = [...homeMounts, ...ideMounts];
3018
3028
  const mountsField = mounts.length > 0 ? { mounts } : {};
3019
3029
  const workspaceMountField = {
3020
3030
  workspaceMount: `source=\${localWorkspaceFolder},target=/workspaces/${opts.name},type=bind,consistency=cached`,
3021
3031
  workspaceFolder: `/workspaces/${opts.name}`
3022
3032
  };
3023
- const runArgs = ["--cap-add=NET_ADMIN"];
3033
+ const runArgs = ["--cap-add=NET_ADMIN", `--name=monoceros-${opts.name}`];
3024
3034
  if (ports.length > 0) {
3025
3035
  runArgs.push("--network=monoceros-proxy");
3026
3036
  runArgs.push(`--network-alias=${opts.name}`);
@@ -3034,6 +3044,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3034
3044
  runArgs,
3035
3045
  forwardPorts: ports,
3036
3046
  postCreateCommand: ".devcontainer/post-create.sh",
3047
+ ...sshPostStart,
3037
3048
  ...featuresField ?? {},
3038
3049
  ...customizationsField ?? {}
3039
3050
  };
@@ -3054,9 +3065,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3054
3065
  void dockerMode;
3055
3066
  const hasPorts = (opts.ports?.length ?? 0) > 0;
3056
3067
  const lines = ["services:"];
3057
- const ideVolumes = runtimeSupportsIdeVolumes(opts.runtimeVersion) ? ideStateVolumes(opts.name) : [];
3068
+ const ideVolumes = ideStateVolumesForRuntime(opts.name, opts.runtimeVersion);
3058
3069
  lines.push(" workspace:");
3059
3070
  lines.push(` image: ${resolveRuntimeImage(opts.runtimeVersion)}`);
3071
+ lines.push(` container_name: monoceros-${opts.name}`);
3060
3072
  lines.push(" command: 'sleep infinity'");
3061
3073
  lines.push(" cap_add:");
3062
3074
  lines.push(" - NET_ADMIN");
@@ -3928,8 +3940,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
3928
3940
  if (!isMap2(item)) return false;
3929
3941
  const url = item.get("url");
3930
3942
  if (url === urlOrPath) return true;
3931
- const path28 = item.get("path");
3932
- 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;
3933
3945
  return effectivePath === urlOrPath;
3934
3946
  });
3935
3947
  if (idx < 0) return false;
@@ -4051,7 +4063,7 @@ async function runAddRepo(input) {
4051
4063
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
4052
4064
  );
4053
4065
  }
4054
- const path28 = (input.path ?? deriveRepoName(url)).trim();
4066
+ const path30 = (input.path ?? deriveRepoName(url)).trim();
4055
4067
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
4056
4068
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
4057
4069
  if (hasName !== hasEmail) {
@@ -4088,7 +4100,7 @@ async function runAddRepo(input) {
4088
4100
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
4089
4101
  const entry2 = {
4090
4102
  url,
4091
- path: path28,
4103
+ path: path30,
4092
4104
  ...hasName && hasEmail ? {
4093
4105
  gitUser: {
4094
4106
  name: input.gitName.trim(),
@@ -5079,9 +5091,6 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
5079
5091
  if (Object.keys(languageOptions).length > 0) {
5080
5092
  result.languageOptions = languageOptions;
5081
5093
  }
5082
- if (config.externalServices.postgres !== void 0) {
5083
- result.postgresUrl = config.externalServices.postgres;
5084
- }
5085
5094
  if (config.aptPackages.length > 0) {
5086
5095
  result.aptPackages = [...config.aptPackages];
5087
5096
  }
@@ -5134,9 +5143,163 @@ var init_transform = __esm({
5134
5143
  }
5135
5144
  });
5136
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
+
5137
5300
  // src/apply/apply-log.ts
5138
5301
  import { createWriteStream, mkdirSync } from "fs";
5139
- import path13 from "path";
5302
+ import path14 from "path";
5140
5303
  import { Writable } from "stream";
5141
5304
  function safeIsoStamp(d) {
5142
5305
  return d.toISOString().replace(/[:.]/g, "-");
@@ -5146,7 +5309,7 @@ function createApplyLog(opts) {
5146
5309
  const dir = containerLogsDir(opts.name, opts.home);
5147
5310
  mkdirSync(dir, { recursive: true });
5148
5311
  const file = `apply-${opts.name}-${safeIsoStamp(now)}.log`;
5149
- const fullPath = path13.join(dir, file);
5312
+ const fullPath = path14.join(dir, file);
5150
5313
  const stream = createWriteStream(fullPath, { flags: "w" });
5151
5314
  const header = [
5152
5315
  `# monoceros apply log`,
@@ -6013,8 +6176,8 @@ var init_markers = __esm({
6013
6176
  });
6014
6177
 
6015
6178
  // src/briefing/index.ts
6016
- import { promises as fs10 } from "fs";
6017
- import path14 from "path";
6179
+ import { promises as fs11 } from "fs";
6180
+ import path15 from "path";
6018
6181
  async function writeBriefing(input) {
6019
6182
  const subCommands = input.subCommands ?? await loadSubCommandsDynamic();
6020
6183
  const manifestLoader = input.manifestLoader ?? ((ref) => loadFeatureManifestSummary(ref));
@@ -6027,12 +6190,12 @@ async function writeBriefing(input) {
6027
6190
  );
6028
6191
  const claudeBody = generateClaudeMd();
6029
6192
  const commandsBody = generateCommandsMd(subCommands);
6030
- await writeMarkerAware(path14.join(input.targetDir, "AGENTS.md"), agentsBody);
6031
- await writeMarkerAware(path14.join(input.targetDir, "CLAUDE.md"), claudeBody);
6032
- const monocerosDir = path14.join(input.targetDir, ".monoceros");
6033
- await fs10.mkdir(monocerosDir, { recursive: true });
6034
- await fs10.writeFile(
6035
- 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"),
6036
6199
  commandsBody,
6037
6200
  "utf8"
6038
6201
  );
@@ -6052,18 +6215,18 @@ function featureDisplayMap(components) {
6052
6215
  async function writeMarkerAware(filePath, body) {
6053
6216
  let existing = null;
6054
6217
  try {
6055
- existing = await fs10.readFile(filePath, "utf8");
6218
+ existing = await fs11.readFile(filePath, "utf8");
6056
6219
  } catch (err) {
6057
6220
  if (err.code !== "ENOENT") throw err;
6058
6221
  }
6059
6222
  if (existing) {
6060
6223
  const updated = replaceMarkerBlock(existing, body);
6061
6224
  if (updated !== null) {
6062
- await fs10.writeFile(filePath, updated, "utf8");
6225
+ await fs11.writeFile(filePath, updated, "utf8");
6063
6226
  return;
6064
6227
  }
6065
6228
  }
6066
- await fs10.writeFile(filePath, wrapWithMarkers(body), "utf8");
6229
+ await fs11.writeFile(filePath, wrapWithMarkers(body), "utf8");
6067
6230
  }
6068
6231
  async function loadSubCommandsDynamic() {
6069
6232
  const mod = await Promise.resolve().then(() => (init_main(), main_exports));
@@ -6188,10 +6351,10 @@ var init_runtime_pull_hint = __esm({
6188
6351
  });
6189
6352
 
6190
6353
  // src/devcontainer/cli.ts
6191
- import { spawn as spawn4 } from "child_process";
6354
+ import { spawn as spawn5 } from "child_process";
6192
6355
  import { readFileSync as readFileSync4 } from "fs";
6193
6356
  import { createRequire } from "module";
6194
- import path15 from "path";
6357
+ import path16 from "path";
6195
6358
  function devcontainerCliPath() {
6196
6359
  if (cachedBinaryPath) return cachedBinaryPath;
6197
6360
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
@@ -6200,7 +6363,7 @@ function devcontainerCliPath() {
6200
6363
  if (!binEntry) {
6201
6364
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
6202
6365
  }
6203
- cachedBinaryPath = path15.resolve(path15.dirname(pkgJsonPath), binEntry);
6366
+ cachedBinaryPath = path16.resolve(path16.dirname(pkgJsonPath), binEntry);
6204
6367
  return cachedBinaryPath;
6205
6368
  }
6206
6369
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -6216,7 +6379,7 @@ var init_cli = __esm({
6216
6379
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
6217
6380
  return new Promise((resolve, reject) => {
6218
6381
  if (options.interactive) {
6219
- const child2 = spawn4(process.execPath, [binPath, ...args], {
6382
+ const child2 = spawn5(process.execPath, [binPath, ...args], {
6220
6383
  cwd,
6221
6384
  env,
6222
6385
  stdio: "inherit"
@@ -6225,7 +6388,7 @@ var init_cli = __esm({
6225
6388
  child2.on("exit", (code) => resolve(code ?? 0));
6226
6389
  return;
6227
6390
  }
6228
- const child = spawn4(process.execPath, [binPath, ...args], {
6391
+ const child = spawn5(process.execPath, [binPath, ...args], {
6229
6392
  cwd,
6230
6393
  env,
6231
6394
  stdio: ["ignore", "pipe", "pipe"]
@@ -6273,9 +6436,9 @@ var init_cli = __esm({
6273
6436
  });
6274
6437
 
6275
6438
  // src/devcontainer/compose.ts
6276
- import { spawn as spawn5 } from "child_process";
6277
- import { existsSync as existsSync8 } from "fs";
6278
- import path16 from "path";
6439
+ import { spawn as spawn6 } from "child_process";
6440
+ import { existsSync as existsSync9 } from "fs";
6441
+ import path17 from "path";
6279
6442
  import { Writable as Writable3 } from "stream";
6280
6443
  import { consola as consola10 } from "consola";
6281
6444
  async function findContainerIds(filters, exec = spawnDocker) {
@@ -6325,18 +6488,24 @@ async function cleanupDockerObjects(opts) {
6325
6488
  return { exitCode: rmExit, removedIds: ids };
6326
6489
  }
6327
6490
  function composeProjectName(root) {
6328
- return `${path16.basename(root)}_devcontainer`;
6491
+ return `${path17.basename(root)}_devcontainer`;
6329
6492
  }
6330
- function resolveCompose(root) {
6331
- if (!existsSync8(path16.join(root, ".devcontainer"))) {
6493
+ function assertDevcontainer(root) {
6494
+ if (!existsSync9(path17.join(root, ".devcontainer"))) {
6332
6495
  throw new Error(
6333
6496
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
6334
6497
  );
6335
6498
  }
6336
- const composeFile = path16.join(root, ".devcontainer", "compose.yaml");
6337
- 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)) {
6338
6507
  throw new Error(
6339
- `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.`
6340
6509
  );
6341
6510
  }
6342
6511
  return { composeFile, projectName: composeProjectName(root) };
@@ -6348,7 +6517,7 @@ async function runComposeAction(buildSubArgs, opts) {
6348
6517
  return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
6349
6518
  }
6350
6519
  async function runStart(opts) {
6351
- resolveCompose(opts.root);
6520
+ assertDevcontainer(opts.root);
6352
6521
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6353
6522
  const spawnFn = opts.spawn ?? spawnDevcontainer;
6354
6523
  logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
@@ -6491,16 +6660,71 @@ and retry \`monoceros apply\`.`
6491
6660
  );
6492
6661
  }
6493
6662
  function runStop(opts) {
6494
- return runComposeAction(
6495
- (service) => ["stop", ...service ? [service] : []],
6496
- opts
6497
- );
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);
6498
6671
  }
6499
6672
  function runStatus(opts) {
6500
- return runComposeAction(
6501
- (service) => ["ps", ...service ? [service] : []],
6502
- 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
+ `
6503
6726
  );
6727
+ return res.exitCode;
6504
6728
  }
6505
6729
  function runLogs(opts) {
6506
6730
  const follow = opts.follow ?? true;
@@ -6523,7 +6747,7 @@ var init_compose = __esm({
6523
6747
  init_proxy();
6524
6748
  spawnDockerCompose = (args, cwd) => {
6525
6749
  return new Promise((resolve, reject) => {
6526
- const child = spawn5("docker", ["compose", ...args], {
6750
+ const child = spawn6("docker", ["compose", ...args], {
6527
6751
  cwd,
6528
6752
  stdio: ["inherit", "pipe", "pipe"]
6529
6753
  });
@@ -6535,7 +6759,7 @@ var init_compose = __esm({
6535
6759
  };
6536
6760
  spawnDocker = (args) => {
6537
6761
  return new Promise((resolve, reject) => {
6538
- const child = spawn5("docker", args, {
6762
+ const child = spawn6("docker", args, {
6539
6763
  stdio: ["ignore", "pipe", "pipe"]
6540
6764
  });
6541
6765
  let stdout = "";
@@ -6596,9 +6820,9 @@ var init_images = __esm({
6596
6820
 
6597
6821
  // src/config/machine-state.ts
6598
6822
  import { promises as fsp4 } from "fs";
6599
- import path17 from "path";
6823
+ import path18 from "path";
6600
6824
  function machineStatePath(home = monocerosHome()) {
6601
- return path17.join(home, ".machine-state.json");
6825
+ return path18.join(home, ".machine-state.json");
6602
6826
  }
6603
6827
  async function readMachineState(home = monocerosHome()) {
6604
6828
  try {
@@ -6653,7 +6877,7 @@ var init_machine_state = __esm({
6653
6877
  });
6654
6878
 
6655
6879
  // src/devcontainer/docker-mode.ts
6656
- import { spawn as spawn6 } from "child_process";
6880
+ import { spawn as spawn7 } from "child_process";
6657
6881
  async function detectDockerMode(options = {}) {
6658
6882
  const spawnFn = options.spawn ?? realDockerInfo;
6659
6883
  try {
@@ -6703,7 +6927,7 @@ var init_docker_mode = __esm({
6703
6927
  init_format();
6704
6928
  realDockerInfo = () => {
6705
6929
  return new Promise((resolve, reject) => {
6706
- const child = spawn6(
6930
+ const child = spawn7(
6707
6931
  "docker",
6708
6932
  ["info", "--format", "{{json .SecurityOptions}}"],
6709
6933
  {
@@ -6722,9 +6946,9 @@ var init_docker_mode = __esm({
6722
6946
  });
6723
6947
 
6724
6948
  // src/devcontainer/identity.ts
6725
- import { spawn as spawn7 } from "child_process";
6726
- import { promises as fs11 } from "fs";
6727
- import path18 from "path";
6949
+ import { spawn as spawn8 } from "child_process";
6950
+ import { promises as fs12 } from "fs";
6951
+ import path19 from "path";
6728
6952
  import { consola as consola11 } from "consola";
6729
6953
  async function resolveIdentityWithPrompt(options = {}) {
6730
6954
  const spawnFn = options.spawn ?? realGitConfigGet;
@@ -6780,8 +7004,8 @@ async function resolveIdentityWithPrompt(options = {}) {
6780
7004
  };
6781
7005
  }
6782
7006
  async function collectGitIdentity(devContainerRoot, options = {}) {
6783
- const gitconfigDir = path18.join(devContainerRoot, ".monoceros");
6784
- const gitconfigPath = path18.join(gitconfigDir, "gitconfig");
7007
+ const gitconfigDir = path19.join(devContainerRoot, ".monoceros");
7008
+ const gitconfigPath = path19.join(gitconfigDir, "gitconfig");
6785
7009
  const logger = options.logger ?? { info: () => {
6786
7010
  }, warn: () => {
6787
7011
  } };
@@ -6794,8 +7018,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
6794
7018
  const lines = ["[user]"];
6795
7019
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
6796
7020
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
6797
- await fs11.mkdir(gitconfigDir, { recursive: true });
6798
- await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
7021
+ await fs12.mkdir(gitconfigDir, { recursive: true });
7022
+ await fs12.writeFile(gitconfigPath, lines.join("\n") + "\n");
6799
7023
  return {
6800
7024
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
6801
7025
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -6838,7 +7062,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
6838
7062
  }
6839
7063
  async function readExistingGitconfig(filePath) {
6840
7064
  try {
6841
- const content = await fs11.readFile(filePath, "utf8");
7065
+ const content = await fs12.readFile(filePath, "utf8");
6842
7066
  const result = {};
6843
7067
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
6844
7068
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -6855,7 +7079,7 @@ var init_identity = __esm({
6855
7079
  "use strict";
6856
7080
  realGitConfigGet = (key) => {
6857
7081
  return new Promise((resolve, reject) => {
6858
- const child = spawn7("git", ["config", "--global", "--get", key], {
7082
+ const child = spawn8("git", ["config", "--global", "--get", key], {
6859
7083
  stdio: ["ignore", "pipe", "inherit"]
6860
7084
  });
6861
7085
  let stdout = "";
@@ -6915,7 +7139,7 @@ var init_identity = __esm({
6915
7139
  });
6916
7140
 
6917
7141
  // src/apply/index.ts
6918
- import { existsSync as existsSync9, promises as fs12 } from "fs";
7142
+ import { existsSync as existsSync10, promises as fs13 } from "fs";
6919
7143
  import { consola as consola12 } from "consola";
6920
7144
  async function runApply(opts) {
6921
7145
  const home = opts.monocerosHome ?? monocerosHome();
@@ -6937,7 +7161,7 @@ ${sectionLine(label)}
6937
7161
  );
6938
7162
  }
6939
7163
  const ymlPath = containerConfigPath(opts.name, home);
6940
- if (!existsSync9(ymlPath)) {
7164
+ if (!existsSync10(ymlPath)) {
6941
7165
  throw new Error(
6942
7166
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
6943
7167
  );
@@ -7055,7 +7279,7 @@ Fix the value in the env file (or the yml).`
7055
7279
  if (dockerMode === "rootless") {
7056
7280
  throw new Error(formatRootlessNotSupportedError());
7057
7281
  }
7058
- await fs12.mkdir(targetDir, { recursive: true });
7282
+ await fs13.mkdir(targetDir, { recursive: true });
7059
7283
  await writeScaffold(createOpts, targetDir, { dockerMode });
7060
7284
  await writeStateFile(
7061
7285
  targetDir,
@@ -7066,6 +7290,27 @@ Fix the value in the env file (or the yml).`
7066
7290
  ...opts.now ? { now: opts.now } : {}
7067
7291
  })
7068
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
+ }
7069
7314
  try {
7070
7315
  const components = await loadComponentCatalog();
7071
7316
  await writeBriefing({ targetDir, createOpts, components });
@@ -7210,8 +7455,8 @@ ${stripAnsi(formatted)}
7210
7455
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
7211
7456
  }
7212
7457
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
7213
- if (!existsSync9(targetDir)) return;
7214
- const entries = await fs12.readdir(targetDir);
7458
+ if (!existsSync10(targetDir)) return;
7459
+ const entries = await fs13.readdir(targetDir);
7215
7460
  if (entries.length === 0) return;
7216
7461
  const state = await readStateFile(targetDir);
7217
7462
  if (state) {
@@ -7281,7 +7526,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7281
7526
  }
7282
7527
  if (wantContainer) {
7283
7528
  try {
7284
- const text = await fs12.readFile(ymlPath, "utf8");
7529
+ const text = await fs13.readFile(ymlPath, "utf8");
7285
7530
  const parsed = parseConfig(text, ymlPath);
7286
7531
  const changed = setContainerGitUserInDoc(parsed.doc, {
7287
7532
  name: prompted.name,
@@ -7289,7 +7534,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
7289
7534
  });
7290
7535
  if (changed) {
7291
7536
  const out = stringifyConfig(parsed.doc);
7292
- await fs12.writeFile(ymlPath, out, "utf8");
7537
+ await fs13.writeFile(ymlPath, out, "utf8");
7293
7538
  logger.info(
7294
7539
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
7295
7540
  );
@@ -7312,6 +7557,7 @@ var init_apply = __esm({
7312
7557
  init_state();
7313
7558
  init_transform();
7314
7559
  init_catalog();
7560
+ init_ssh_attach();
7315
7561
  init_scaffold();
7316
7562
  init_format();
7317
7563
  init_ref();
@@ -7335,87 +7581,383 @@ var init_apply = __esm({
7335
7581
  }
7336
7582
  });
7337
7583
 
7338
- // src/version.ts
7339
- var CLI_VERSION;
7340
- var init_version = __esm({
7341
- "src/version.ts"() {
7342
- "use strict";
7343
- CLI_VERSION = true ? "1.26.4" : "dev";
7344
- }
7345
- });
7346
-
7347
- // src/commands/_dispatch.ts
7348
- import { consola as consola13 } from "consola";
7349
- 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) {
7350
7590
  try {
7351
- const exitCode = await runner();
7352
- process.exit(exitCode);
7353
- } catch (err) {
7354
- consola13.error(err instanceof Error ? err.message : String(err));
7355
- 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;
7356
7601
  }
7357
7602
  }
7358
- var init_dispatch = __esm({
7359
- "src/commands/_dispatch.ts"() {
7360
- "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"`);
7361
7615
  }
7362
- });
7363
-
7364
- // src/commands/apply.ts
7365
- import { defineCommand as defineCommand8 } from "citty";
7366
- var applyCommand;
7367
- var init_apply2 = __esm({
7368
- "src/commands/apply.ts"() {
7369
- "use strict";
7370
- init_apply();
7371
- init_version();
7372
- init_dispatch();
7373
- applyCommand = defineCommand8({
7374
- meta: {
7375
- name: "apply",
7376
- group: "lifecycle",
7377
- 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."
7378
- },
7379
- args: {
7380
- name: {
7381
- type: "positional",
7382
- description: "Config name. Resolves to $MONOCEROS_HOME/container-configs/<name>.yml.",
7383
- required: true
7384
- },
7385
- verbose: {
7386
- type: "boolean",
7387
- description: "Stream the raw @devcontainers/cli output to stderr instead of showing a phase spinner. Auto-enabled when stderr is not a TTY.",
7388
- default: false
7389
- }
7390
- },
7391
- run({ args }) {
7392
- return dispatch(async () => {
7393
- const result = await runApply({
7394
- name: args.name,
7395
- cliVersion: CLI_VERSION,
7396
- verbose: args.verbose
7397
- });
7398
- return result.containerExitCode;
7399
- });
7400
- }
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
7401
7632
  });
7633
+ child.on("error", () => {
7634
+ });
7635
+ child.unref();
7636
+ } catch {
7402
7637
  }
7403
- });
7404
-
7405
- // src/commands/completion.ts
7406
- import { defineCommand as defineCommand9 } from "citty";
7407
- function renderCompletionScript(shell) {
7408
- if (shell === "bash") {
7409
- return [
7410
- "# bash completion for monoceros",
7411
- "# install: source this file from .bashrc, e.g.",
7412
- "# monoceros completion bash > ~/.bash_completion.d/monoceros",
7413
- '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
7414
- "#",
7415
- "# The work is done by `monoceros __complete --line --point`; this",
7416
- "# shell wrapper only forwards the cursor view.",
7417
- "",
7418
- "_monoceros() {",
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() {",
7419
7961
  " local IFS=$'\\n'",
7420
7962
  " local candidates",
7421
7963
  ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
@@ -7530,8 +8072,8 @@ var init_completion = __esm({
7530
8072
  });
7531
8073
 
7532
8074
  // src/completion/resolve.ts
7533
- import { existsSync as existsSync10, promises as fs13 } from "fs";
7534
- import path19 from "path";
8075
+ import { existsSync as existsSync14, promises as fs14 } from "fs";
8076
+ import path23 from "path";
7535
8077
  async function resolveCompletions(line, point, opts = {}) {
7536
8078
  const { prev, current } = parseCompletionLine(line, point);
7537
8079
  const ctx = { prev, current, opts };
@@ -7680,9 +8222,9 @@ function filterPrefix(values, fragment) {
7680
8222
  }
7681
8223
  async function listContainerNames(ctx) {
7682
8224
  const home = ctx.opts.monocerosHome ?? monocerosHome();
7683
- const dir = path19.join(home, "container-configs");
7684
- if (!existsSync10(dir)) return [];
7685
- 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);
7686
8228
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
7687
8229
  }
7688
8230
  async function listFeatureComponents() {
@@ -7756,10 +8298,12 @@ var init_resolve = __esm({
7756
8298
  init_manifest();
7757
8299
  init_catalog();
7758
8300
  init_schema();
8301
+ init_open();
7759
8302
  ALL_COMMANDS = [
7760
8303
  "init",
7761
8304
  "list-components",
7762
8305
  "shell",
8306
+ "open",
7763
8307
  "run",
7764
8308
  "logs",
7765
8309
  "start",
@@ -7809,7 +8353,10 @@ var init_resolve = __esm({
7809
8353
  },
7810
8354
  apply: {
7811
8355
  positionals: [containerName],
7812
- flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
8356
+ flags: {
8357
+ "--yes": { type: "boolean", aliases: ["-y"] },
8358
+ "--open": { type: "value", values: () => [...OPEN_TOOLS] }
8359
+ }
7813
8360
  },
7814
8361
  upgrade: {
7815
8362
  // First positional is a container name; the second is a version
@@ -7825,12 +8372,16 @@ var init_resolve = __esm({
7825
8372
  }
7826
8373
  },
7827
8374
  shell: { positionals: [containerName] },
8375
+ open: { positionals: [containerName, () => [...OPEN_TOOLS]] },
7828
8376
  run: {
7829
8377
  positionals: [containerName],
7830
8378
  flags: { "--in": { type: "value" } }
7831
8379
  },
7832
8380
  logs: { positionals: [containerName] },
7833
- start: { positionals: [containerName] },
8381
+ start: {
8382
+ positionals: [containerName],
8383
+ flags: { "--open": { type: "value", values: () => [...OPEN_TOOLS] } }
8384
+ },
7834
8385
  stop: { positionals: [containerName] },
7835
8386
  status: { positionals: [containerName] },
7836
8387
  "add-language": {
@@ -8349,14 +8900,14 @@ var init_generator = __esm({
8349
8900
  });
8350
8901
 
8351
8902
  // src/init/index.ts
8352
- import { existsSync as existsSync11, promises as fs14 } from "fs";
8353
- import path20 from "path";
8354
- 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";
8355
8906
  async function runInit(opts) {
8356
8907
  const home = opts.monocerosHome ?? monocerosHome();
8357
8908
  const logger = opts.logger ?? {
8358
- success: (msg) => consola14.success(msg),
8359
- info: (msg) => consola14.info(msg)
8909
+ success: (msg) => consola16.success(msg),
8910
+ info: (msg) => consola16.info(msg)
8360
8911
  };
8361
8912
  if (!REGEX.solutionName.test(opts.name)) {
8362
8913
  throw new Error(
@@ -8364,12 +8915,12 @@ async function runInit(opts) {
8364
8915
  );
8365
8916
  }
8366
8917
  const dest = containerConfigPath(opts.name, home);
8367
- if (existsSync11(dest)) {
8918
+ if (existsSync15(dest)) {
8368
8919
  throw new Error(
8369
8920
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
8370
8921
  );
8371
8922
  }
8372
- const componentsRoot = opts.workbenchRoot ? path20.join(opts.workbenchRoot, "components") : componentsRootDir();
8923
+ const componentsRoot = opts.workbenchRoot ? path24.join(opts.workbenchRoot, "components") : componentsRootDir();
8373
8924
  const catalog = await loadComponentCatalog(componentsRoot);
8374
8925
  if (catalog.size === 0) {
8375
8926
  throw new Error(
@@ -8435,9 +8986,9 @@ async function runInit(opts) {
8435
8986
  } else {
8436
8987
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
8437
8988
  }
8438
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
8989
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
8439
8990
  await ensureEnvGitignored(containerConfigsDir(home));
8440
- await fs14.writeFile(dest, text, "utf8");
8991
+ await fs15.writeFile(dest, text, "utf8");
8441
8992
  const envPath = containerEnvPath(opts.name, home);
8442
8993
  const seedVars = {};
8443
8994
  for (const f of composed.features) {
@@ -8460,8 +9011,8 @@ async function runInit(opts) {
8460
9011
  }
8461
9012
  await ensureEnvVars(envPath, opts.name, seedVars);
8462
9013
  const documented = !anyComposed;
8463
- const ymlRel = path20.relative(home, dest);
8464
- const envRel = path20.relative(home, envPath);
9014
+ const ymlRel = path24.relative(home, dest);
9015
+ const envRel = path24.relative(home, envPath);
8465
9016
  if (documented) {
8466
9017
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
8467
9018
  logger.info(
@@ -8605,7 +9156,7 @@ var init_init = __esm({
8605
9156
 
8606
9157
  // src/commands/init.ts
8607
9158
  import { defineCommand as defineCommand11 } from "citty";
8608
- import { consola as consola15 } from "consola";
9159
+ import { consola as consola17 } from "consola";
8609
9160
  function collectListFlag(flag, rawArgs) {
8610
9161
  const eq = `${flag}=`;
8611
9162
  const pieces = [];
@@ -8731,7 +9282,7 @@ var init_init2 = __esm({
8731
9282
  ...ports && ports.length > 0 ? { withPorts: ports } : {}
8732
9283
  });
8733
9284
  } catch (err) {
8734
- consola15.error(err instanceof Error ? err.message : String(err));
9285
+ consola17.error(err instanceof Error ? err.message : String(err));
8735
9286
  process.exit(1);
8736
9287
  }
8737
9288
  }
@@ -8780,7 +9331,7 @@ var init_expand = __esm({
8780
9331
 
8781
9332
  // src/commands/list-components.ts
8782
9333
  import { defineCommand as defineCommand12 } from "citty";
8783
- import { consola as consola16 } from "consola";
9334
+ import { consola as consola18 } from "consola";
8784
9335
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
8785
9336
  var init_list_components = __esm({
8786
9337
  "src/commands/list-components.ts"() {
@@ -8809,7 +9360,7 @@ var init_list_components = __esm({
8809
9360
  try {
8810
9361
  const catalog = expandSelectable(await loadDescriptorCatalog());
8811
9362
  if (catalog.size === 0) {
8812
- consola16.warn(
9363
+ consola18.warn(
8813
9364
  "No components found. The workbench checkout looks incomplete."
8814
9365
  );
8815
9366
  process.exit(0);
@@ -8860,7 +9411,7 @@ var init_list_components = __esm({
8860
9411
  }
8861
9412
  process.exit(0);
8862
9413
  } catch (err) {
8863
- consola16.error(err instanceof Error ? err.message : String(err));
9414
+ consola18.error(err instanceof Error ? err.message : String(err));
8864
9415
  process.exit(1);
8865
9416
  }
8866
9417
  }
@@ -8869,14 +9420,14 @@ var init_list_components = __esm({
8869
9420
  });
8870
9421
 
8871
9422
  // src/commands/logs.ts
8872
- import { spawn as spawn8 } from "child_process";
8873
- import { existsSync as existsSync12 } from "fs";
8874
- import path21 from "path";
9423
+ import { spawn as spawn11 } from "child_process";
9424
+ import { existsSync as existsSync16 } from "fs";
9425
+ import path25 from "path";
8875
9426
  import { defineCommand as defineCommand13 } from "citty";
8876
9427
  function tailLogFile(file, follow) {
8877
9428
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
8878
9429
  return new Promise((resolve, reject) => {
8879
- const child = spawn8(cmd, args, { stdio: "inherit" });
9430
+ const child = spawn11(cmd, args, { stdio: "inherit" });
8880
9431
  child.on("error", reject);
8881
9432
  child.on("exit", (code) => resolve(code ?? 0));
8882
9433
  });
@@ -8914,8 +9465,8 @@ var init_logs = __esm({
8914
9465
  run({ args }) {
8915
9466
  const service = typeof args.service === "string" ? args.service : void 0;
8916
9467
  if (service) {
8917
- const logFile = path21.join(containerLogsDir(args.name), `${service}.log`);
8918
- if (existsSync12(logFile)) {
9468
+ const logFile = path25.join(containerLogsDir(args.name), `${service}.log`);
9469
+ if (existsSync16(logFile)) {
8919
9470
  return dispatch(() => tailLogFile(logFile, args.follow));
8920
9471
  }
8921
9472
  }
@@ -8931,12 +9482,45 @@ var init_logs = __esm({
8931
9482
  }
8932
9483
  });
8933
9484
 
8934
- // src/commands/port.ts
9485
+ // src/commands/open.ts
8935
9486
  import { defineCommand as defineCommand14 } from "citty";
8936
- 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";
8937
9521
  async function runPortListing(opts) {
8938
9522
  const out = opts.out ?? process.stdout;
8939
- const info = opts.info ?? ((m) => consola17.info(m));
9523
+ const info = opts.info ?? ((m) => consola19.info(m));
8940
9524
  const parsed = await readConfig(
8941
9525
  containerConfigPath(opts.name, opts.monocerosHome)
8942
9526
  );
@@ -8994,7 +9578,7 @@ var init_port = __esm({
8994
9578
  init_schema();
8995
9579
  init_dynamic();
8996
9580
  init_format();
8997
- portCommand = defineCommand14({
9581
+ portCommand = defineCommand15({
8998
9582
  meta: {
8999
9583
  name: "port",
9000
9584
  group: "discovery",
@@ -9012,7 +9596,7 @@ var init_port = __esm({
9012
9596
  const code = await runPortListing({ name: args.name });
9013
9597
  process.exit(code);
9014
9598
  } catch (err) {
9015
- consola17.error(err instanceof Error ? err.message : String(err));
9599
+ consola19.error(err instanceof Error ? err.message : String(err));
9016
9600
  process.exit(1);
9017
9601
  }
9018
9602
  }
@@ -9021,15 +9605,15 @@ var init_port = __esm({
9021
9605
  });
9022
9606
 
9023
9607
  // src/commands/remove-apt-packages.ts
9024
- import { defineCommand as defineCommand15 } from "citty";
9025
- import { consola as consola18 } from "consola";
9608
+ import { defineCommand as defineCommand16 } from "citty";
9609
+ import { consola as consola20 } from "consola";
9026
9610
  var removeAptPackagesCommand;
9027
9611
  var init_remove_apt_packages = __esm({
9028
9612
  "src/commands/remove-apt-packages.ts"() {
9029
9613
  "use strict";
9030
9614
  init_inner_args();
9031
9615
  init_modify();
9032
- removeAptPackagesCommand = defineCommand15({
9616
+ removeAptPackagesCommand = defineCommand16({
9033
9617
  meta: {
9034
9618
  name: "remove-apt-packages",
9035
9619
  group: "edit",
@@ -9051,7 +9635,7 @@ var init_remove_apt_packages = __esm({
9051
9635
  async run({ args }) {
9052
9636
  const packages = [...getInnerArgs()];
9053
9637
  if (packages.length === 0) {
9054
- consola18.error(
9638
+ consola20.error(
9055
9639
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
9056
9640
  );
9057
9641
  process.exit(1);
@@ -9064,7 +9648,7 @@ var init_remove_apt_packages = __esm({
9064
9648
  });
9065
9649
  process.exit(result.status === "aborted" ? 1 : 0);
9066
9650
  } catch (err) {
9067
- consola18.error(err instanceof Error ? err.message : String(err));
9651
+ consola20.error(err instanceof Error ? err.message : String(err));
9068
9652
  process.exit(1);
9069
9653
  }
9070
9654
  }
@@ -9073,14 +9657,14 @@ var init_remove_apt_packages = __esm({
9073
9657
  });
9074
9658
 
9075
9659
  // src/commands/remove-feature.ts
9076
- import { defineCommand as defineCommand16 } from "citty";
9077
- import { consola as consola19 } from "consola";
9660
+ import { defineCommand as defineCommand17 } from "citty";
9661
+ import { consola as consola21 } from "consola";
9078
9662
  var removeFeatureCommand;
9079
9663
  var init_remove_feature = __esm({
9080
9664
  "src/commands/remove-feature.ts"() {
9081
9665
  "use strict";
9082
9666
  init_modify();
9083
- removeFeatureCommand = defineCommand16({
9667
+ removeFeatureCommand = defineCommand17({
9084
9668
  meta: {
9085
9669
  name: "remove-feature",
9086
9670
  group: "edit",
@@ -9113,7 +9697,7 @@ var init_remove_feature = __esm({
9113
9697
  });
9114
9698
  process.exit(result.status === "aborted" ? 1 : 0);
9115
9699
  } catch (err) {
9116
- consola19.error(err instanceof Error ? err.message : String(err));
9700
+ consola21.error(err instanceof Error ? err.message : String(err));
9117
9701
  process.exit(1);
9118
9702
  }
9119
9703
  }
@@ -9122,15 +9706,15 @@ var init_remove_feature = __esm({
9122
9706
  });
9123
9707
 
9124
9708
  // src/remove/index.ts
9125
- import { existsSync as existsSync13, promises as fs15 } from "fs";
9126
- import path22 from "path";
9127
- 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";
9128
9712
  async function runRemove(opts) {
9129
9713
  const home = opts.monocerosHome ?? monocerosHome();
9130
9714
  const logger = opts.logger ?? {
9131
- info: (msg) => consola20.info(msg),
9132
- success: (msg) => consola20.success(msg),
9133
- warn: (msg) => consola20.warn(msg)
9715
+ info: (msg) => consola22.info(msg),
9716
+ success: (msg) => consola22.success(msg),
9717
+ warn: (msg) => consola22.warn(msg)
9134
9718
  };
9135
9719
  if (!REGEX.solutionName.test(opts.name)) {
9136
9720
  throw new Error(
@@ -9140,9 +9724,9 @@ async function runRemove(opts) {
9140
9724
  const ymlPath = containerConfigPath(opts.name, home);
9141
9725
  const envPath = containerEnvPath(opts.name, home);
9142
9726
  const containerPath = containerDir(opts.name, home);
9143
- const hasYml = existsSync13(ymlPath);
9144
- const hasEnv = existsSync13(envPath);
9145
- const hasContainer = existsSync13(containerPath);
9727
+ const hasYml = existsSync17(ymlPath);
9728
+ const hasEnv = existsSync17(envPath);
9729
+ const hasContainer = existsSync17(containerPath);
9146
9730
  if (!hasYml && !hasContainer) {
9147
9731
  throw new Error(
9148
9732
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -9168,30 +9752,30 @@ async function runRemove(opts) {
9168
9752
  let backupPath = null;
9169
9753
  if (!opts.noBackup && (hasYml || hasContainer)) {
9170
9754
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9171
- backupPath = path22.join(home, "container-backups", `${opts.name}-${ts}`);
9172
- await fs15.mkdir(backupPath, { recursive: true });
9755
+ backupPath = path26.join(home, "container-backups", `${opts.name}-${ts}`);
9756
+ await fs16.mkdir(backupPath, { recursive: true });
9173
9757
  if (hasYml) {
9174
- await fs15.copyFile(ymlPath, path22.join(backupPath, `${opts.name}.yml`));
9758
+ await fs16.copyFile(ymlPath, path26.join(backupPath, `${opts.name}.yml`));
9175
9759
  }
9176
9760
  if (hasEnv) {
9177
- await fs15.copyFile(envPath, path22.join(backupPath, `${opts.name}.env`));
9761
+ await fs16.copyFile(envPath, path26.join(backupPath, `${opts.name}.env`));
9178
9762
  }
9179
9763
  if (hasContainer) {
9180
- await fs15.cp(containerPath, path22.join(backupPath, "container"), {
9764
+ await fs16.cp(containerPath, path26.join(backupPath, "container"), {
9181
9765
  recursive: true
9182
9766
  });
9183
9767
  }
9184
9768
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
9185
9769
  }
9186
9770
  if (hasYml) {
9187
- await fs15.rm(ymlPath, { force: true });
9771
+ await fs16.rm(ymlPath, { force: true });
9188
9772
  }
9189
9773
  if (hasEnv) {
9190
- await fs15.rm(envPath, { force: true });
9774
+ await fs16.rm(envPath, { force: true });
9191
9775
  }
9192
9776
  if (hasContainer) {
9193
9777
  try {
9194
- await fs15.rm(containerPath, { recursive: true, force: true });
9778
+ await fs16.rm(containerPath, { recursive: true, force: true });
9195
9779
  } catch (err) {
9196
9780
  const code = err.code;
9197
9781
  if (code !== "EACCES" && code !== "EPERM") {
@@ -9213,7 +9797,7 @@ async function runRemove(opts) {
9213
9797
  "-delete"
9214
9798
  ]);
9215
9799
  try {
9216
- await fs15.rm(containerPath, { recursive: true, force: true });
9800
+ await fs16.rm(containerPath, { recursive: true, force: true });
9217
9801
  } catch (err2) {
9218
9802
  const code2 = err2.code;
9219
9803
  throw new Error(
@@ -9227,7 +9811,14 @@ async function runRemove(opts) {
9227
9811
  );
9228
9812
  if (!backupPath) {
9229
9813
  logger.warn?.(
9230
- "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.`
9231
9822
  );
9232
9823
  }
9233
9824
  try {
@@ -9262,21 +9853,22 @@ var init_remove = __esm({
9262
9853
  init_schema();
9263
9854
  init_compose();
9264
9855
  init_scaffold();
9856
+ init_ssh_attach();
9265
9857
  init_proxy();
9266
9858
  init_dynamic();
9267
9859
  }
9268
9860
  });
9269
9861
 
9270
9862
  // src/commands/remove.ts
9271
- import { defineCommand as defineCommand17 } from "citty";
9272
- import { consola as consola21 } from "consola";
9863
+ import { defineCommand as defineCommand18 } from "citty";
9864
+ import { consola as consola23 } from "consola";
9273
9865
  import { createInterface } from "readline/promises";
9274
9866
  var removeCommand;
9275
9867
  var init_remove2 = __esm({
9276
9868
  "src/commands/remove.ts"() {
9277
9869
  "use strict";
9278
9870
  init_remove();
9279
- removeCommand = defineCommand17({
9871
+ removeCommand = defineCommand18({
9280
9872
  meta: {
9281
9873
  name: "remove",
9282
9874
  group: "lifecycle",
@@ -9313,7 +9905,7 @@ var init_remove2 = __esm({
9313
9905
  const skipPrompt = args.yes === true;
9314
9906
  if (!skipPrompt) {
9315
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.`;
9316
- consola21.warn(warning);
9908
+ consola23.warn(warning);
9317
9909
  const rl = createInterface({
9318
9910
  input: process.stdin,
9319
9911
  output: process.stdout
@@ -9321,7 +9913,7 @@ var init_remove2 = __esm({
9321
9913
  const answer = await rl.question("Continue? [y/N] ");
9322
9914
  rl.close();
9323
9915
  if (!/^y(es)?$/i.test(answer.trim())) {
9324
- consola21.info("Aborted. Nothing changed.");
9916
+ consola23.info("Aborted. Nothing changed.");
9325
9917
  process.exit(0);
9326
9918
  }
9327
9919
  }
@@ -9330,7 +9922,7 @@ var init_remove2 = __esm({
9330
9922
  ...noBackup ? { noBackup: true } : {}
9331
9923
  });
9332
9924
  } catch (err) {
9333
- consola21.error(err instanceof Error ? err.message : String(err));
9925
+ consola23.error(err instanceof Error ? err.message : String(err));
9334
9926
  process.exit(1);
9335
9927
  }
9336
9928
  }
@@ -9339,24 +9931,24 @@ var init_remove2 = __esm({
9339
9931
  });
9340
9932
 
9341
9933
  // src/restore/index.ts
9342
- import { existsSync as existsSync14, promises as fs16 } from "fs";
9343
- import path23 from "path";
9344
- 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";
9345
9937
  async function runRestore(opts) {
9346
9938
  const home = opts.monocerosHome ?? monocerosHome();
9347
9939
  const logger = opts.logger ?? {
9348
- info: (msg) => consola22.info(msg),
9349
- success: (msg) => consola22.success(msg)
9940
+ info: (msg) => consola24.info(msg),
9941
+ success: (msg) => consola24.success(msg)
9350
9942
  };
9351
- const backup = path23.resolve(opts.backupPath);
9352
- if (!existsSync14(backup)) {
9943
+ const backup = path27.resolve(opts.backupPath);
9944
+ if (!existsSync18(backup)) {
9353
9945
  throw new Error(`Backup not found: ${backup}.`);
9354
9946
  }
9355
- const stat = await fs16.stat(backup);
9947
+ const stat = await fs17.stat(backup);
9356
9948
  if (!stat.isDirectory()) {
9357
9949
  throw new Error(`Backup path is not a directory: ${backup}.`);
9358
9950
  }
9359
- const entries = await fs16.readdir(backup);
9951
+ const entries = await fs17.readdir(backup);
9360
9952
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
9361
9953
  if (ymlFiles.length === 0) {
9362
9954
  throw new Error(
@@ -9370,29 +9962,29 @@ async function runRestore(opts) {
9370
9962
  }
9371
9963
  const ymlFile = ymlFiles[0];
9372
9964
  const name = ymlFile.replace(/\.yml$/, "");
9373
- const containerInBackup = path23.join(backup, "container");
9374
- const hasContainer = existsSync14(containerInBackup);
9375
- const envInBackup = path23.join(backup, `${name}.env`);
9376
- 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);
9377
9969
  const destYml = containerConfigPath(name, home);
9378
9970
  const destContainer = containerDir(name, home);
9379
- if (existsSync14(destYml)) {
9971
+ if (existsSync18(destYml)) {
9380
9972
  throw new Error(
9381
9973
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
9382
9974
  );
9383
9975
  }
9384
- if (hasContainer && existsSync14(destContainer)) {
9976
+ if (hasContainer && existsSync18(destContainer)) {
9385
9977
  throw new Error(
9386
9978
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
9387
9979
  );
9388
9980
  }
9389
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
9390
- 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);
9391
9983
  if (hasEnv) {
9392
- await fs16.copyFile(envInBackup, containerEnvPath(name, home));
9984
+ await fs17.copyFile(envInBackup, containerEnvPath(name, home));
9393
9985
  }
9394
9986
  if (hasContainer) {
9395
- await fs16.cp(containerInBackup, destContainer, { recursive: true });
9987
+ await fs17.cp(containerInBackup, destContainer, { recursive: true });
9396
9988
  }
9397
9989
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
9398
9990
  logger.info(
@@ -9412,14 +10004,14 @@ var init_restore = __esm({
9412
10004
  });
9413
10005
 
9414
10006
  // src/commands/restore.ts
9415
- import { defineCommand as defineCommand18 } from "citty";
9416
- import { consola as consola23 } from "consola";
10007
+ import { defineCommand as defineCommand19 } from "citty";
10008
+ import { consola as consola25 } from "consola";
9417
10009
  var restoreCommand;
9418
10010
  var init_restore2 = __esm({
9419
10011
  "src/commands/restore.ts"() {
9420
10012
  "use strict";
9421
10013
  init_restore();
9422
- restoreCommand = defineCommand18({
10014
+ restoreCommand = defineCommand19({
9423
10015
  meta: {
9424
10016
  name: "restore",
9425
10017
  group: "lifecycle",
@@ -9436,7 +10028,7 @@ var init_restore2 = __esm({
9436
10028
  try {
9437
10029
  await runRestore({ backupPath: args["backup-path"] });
9438
10030
  } catch (err) {
9439
- consola23.error(err instanceof Error ? err.message : String(err));
10031
+ consola25.error(err instanceof Error ? err.message : String(err));
9440
10032
  process.exit(1);
9441
10033
  }
9442
10034
  }
@@ -9445,14 +10037,14 @@ var init_restore2 = __esm({
9445
10037
  });
9446
10038
 
9447
10039
  // src/commands/remove-from-url.ts
9448
- import { defineCommand as defineCommand19 } from "citty";
9449
- import { consola as consola24 } from "consola";
10040
+ import { defineCommand as defineCommand20 } from "citty";
10041
+ import { consola as consola26 } from "consola";
9450
10042
  var removeFromUrlCommand;
9451
10043
  var init_remove_from_url = __esm({
9452
10044
  "src/commands/remove-from-url.ts"() {
9453
10045
  "use strict";
9454
10046
  init_modify();
9455
- removeFromUrlCommand = defineCommand19({
10047
+ removeFromUrlCommand = defineCommand20({
9456
10048
  meta: {
9457
10049
  name: "remove-from-url",
9458
10050
  group: "edit",
@@ -9485,7 +10077,7 @@ var init_remove_from_url = __esm({
9485
10077
  });
9486
10078
  process.exit(result.status === "aborted" ? 1 : 0);
9487
10079
  } catch (err) {
9488
- consola24.error(err instanceof Error ? err.message : String(err));
10080
+ consola26.error(err instanceof Error ? err.message : String(err));
9489
10081
  process.exit(1);
9490
10082
  }
9491
10083
  }
@@ -9494,14 +10086,14 @@ var init_remove_from_url = __esm({
9494
10086
  });
9495
10087
 
9496
10088
  // src/commands/remove-language.ts
9497
- import { defineCommand as defineCommand20 } from "citty";
9498
- import { consola as consola25 } from "consola";
10089
+ import { defineCommand as defineCommand21 } from "citty";
10090
+ import { consola as consola27 } from "consola";
9499
10091
  var removeLanguageCommand;
9500
10092
  var init_remove_language = __esm({
9501
10093
  "src/commands/remove-language.ts"() {
9502
10094
  "use strict";
9503
10095
  init_modify();
9504
- removeLanguageCommand = defineCommand20({
10096
+ removeLanguageCommand = defineCommand21({
9505
10097
  meta: {
9506
10098
  name: "remove-language",
9507
10099
  group: "edit",
@@ -9534,7 +10126,7 @@ var init_remove_language = __esm({
9534
10126
  });
9535
10127
  process.exit(result.status === "aborted" ? 1 : 0);
9536
10128
  } catch (err) {
9537
- consola25.error(err instanceof Error ? err.message : String(err));
10129
+ consola27.error(err instanceof Error ? err.message : String(err));
9538
10130
  process.exit(1);
9539
10131
  }
9540
10132
  }
@@ -9543,8 +10135,8 @@ var init_remove_language = __esm({
9543
10135
  });
9544
10136
 
9545
10137
  // src/commands/remove-port.ts
9546
- import { defineCommand as defineCommand21 } from "citty";
9547
- import { consola as consola26 } from "consola";
10138
+ import { defineCommand as defineCommand22 } from "citty";
10139
+ import { consola as consola28 } from "consola";
9548
10140
  function coerceToken2(raw) {
9549
10141
  const n = Number(raw);
9550
10142
  return Number.isFinite(n) ? n : raw;
@@ -9555,7 +10147,7 @@ var init_remove_port = __esm({
9555
10147
  "use strict";
9556
10148
  init_inner_args();
9557
10149
  init_modify();
9558
- removePortCommand = defineCommand21({
10150
+ removePortCommand = defineCommand22({
9559
10151
  meta: {
9560
10152
  name: "remove-port",
9561
10153
  group: "edit",
@@ -9577,7 +10169,7 @@ var init_remove_port = __esm({
9577
10169
  async run({ args }) {
9578
10170
  const tokens = [...getInnerArgs()];
9579
10171
  if (tokens.length === 0) {
9580
- consola26.error(
10172
+ consola28.error(
9581
10173
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
9582
10174
  );
9583
10175
  process.exit(1);
@@ -9590,7 +10182,7 @@ var init_remove_port = __esm({
9590
10182
  });
9591
10183
  process.exit(result.status === "aborted" ? 1 : 0);
9592
10184
  } catch (err) {
9593
- consola26.error(err instanceof Error ? err.message : String(err));
10185
+ consola28.error(err instanceof Error ? err.message : String(err));
9594
10186
  process.exit(1);
9595
10187
  }
9596
10188
  }
@@ -9599,14 +10191,14 @@ var init_remove_port = __esm({
9599
10191
  });
9600
10192
 
9601
10193
  // src/commands/remove-repo.ts
9602
- import { defineCommand as defineCommand22 } from "citty";
9603
- import { consola as consola27 } from "consola";
10194
+ import { defineCommand as defineCommand23 } from "citty";
10195
+ import { consola as consola29 } from "consola";
9604
10196
  var removeRepoCommand;
9605
10197
  var init_remove_repo = __esm({
9606
10198
  "src/commands/remove-repo.ts"() {
9607
10199
  "use strict";
9608
10200
  init_modify();
9609
- removeRepoCommand = defineCommand22({
10201
+ removeRepoCommand = defineCommand23({
9610
10202
  meta: {
9611
10203
  name: "remove-repo",
9612
10204
  group: "edit",
@@ -9639,7 +10231,7 @@ var init_remove_repo = __esm({
9639
10231
  });
9640
10232
  process.exit(result.status === "aborted" ? 1 : 0);
9641
10233
  } catch (err) {
9642
- consola27.error(err instanceof Error ? err.message : String(err));
10234
+ consola29.error(err instanceof Error ? err.message : String(err));
9643
10235
  process.exit(1);
9644
10236
  }
9645
10237
  }
@@ -9648,14 +10240,14 @@ var init_remove_repo = __esm({
9648
10240
  });
9649
10241
 
9650
10242
  // src/commands/remove-service.ts
9651
- import { defineCommand as defineCommand23 } from "citty";
9652
- import { consola as consola28 } from "consola";
10243
+ import { defineCommand as defineCommand24 } from "citty";
10244
+ import { consola as consola30 } from "consola";
9653
10245
  var removeServiceCommand;
9654
10246
  var init_remove_service = __esm({
9655
10247
  "src/commands/remove-service.ts"() {
9656
10248
  "use strict";
9657
10249
  init_modify();
9658
- removeServiceCommand = defineCommand23({
10250
+ removeServiceCommand = defineCommand24({
9659
10251
  meta: {
9660
10252
  name: "remove-service",
9661
10253
  group: "edit",
@@ -9688,7 +10280,7 @@ var init_remove_service = __esm({
9688
10280
  });
9689
10281
  process.exit(result.status === "aborted" ? 1 : 0);
9690
10282
  } catch (err) {
9691
- consola28.error(err instanceof Error ? err.message : String(err));
10283
+ consola30.error(err instanceof Error ? err.message : String(err));
9692
10284
  process.exit(1);
9693
10285
  }
9694
10286
  }
@@ -9696,148 +10288,17 @@ var init_remove_service = __esm({
9696
10288
  }
9697
10289
  });
9698
10290
 
9699
- // src/devcontainer/browser-bridge.ts
9700
- import { spawn as spawn9 } from "child_process";
9701
- import { existsSync as existsSync15, promises as fsp5, readFileSync as readFileSync5 } from "fs";
9702
- import http from "http";
9703
- import path24 from "path";
9704
- function parseCallbackTarget(authUrl) {
9705
- try {
9706
- const u = new URL(authUrl);
9707
- const redirect = u.searchParams.get("redirect_uri");
9708
- if (!redirect) return null;
9709
- const r = new URL(redirect);
9710
- if (r.hostname !== "localhost" && r.hostname !== "127.0.0.1") return null;
9711
- const port = Number(r.port);
9712
- if (!Number.isInteger(port) || port <= 0) return null;
9713
- return { port, pathname: r.pathname };
9714
- } catch {
9715
- return null;
9716
- }
9717
- }
9718
- function nextRelayUrl(content, lastOpened) {
9719
- const url = content.trim();
9720
- return url && url !== lastOpened ? url : null;
9721
- }
9722
- function wrapExec(command, opts) {
9723
- const leading = [];
9724
- const stmts = [];
9725
- if (opts.pathPrepend) {
9726
- leading.push(opts.pathPrepend);
9727
- const i = leading.length;
9728
- stmts.push(`export PATH="$${i}:$PATH"`);
9729
- stmts.push(`export BROWSER="$${i}/xdg-open"`);
9730
- }
9731
- if (opts.cwd) {
9732
- leading.push(opts.cwd);
9733
- stmts.push(`cd -- "$${leading.length}"`);
9734
- }
9735
- if (leading.length === 0) return command;
9736
- const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
9737
- const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
9738
- return ["bash", "-lc", script, "bash", ...leading, ...command];
9739
- }
9740
- function openInBrowser(url) {
9741
- const platform = process.platform;
9742
- const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
9743
- try {
9744
- const child = spawn9(cmd, args, {
9745
- stdio: "ignore",
9746
- detached: true
9747
- });
9748
- child.on("error", () => {
9749
- });
9750
- child.unref();
9751
- } catch {
9752
- }
9753
- }
9754
- async function startBrowserBridge(opts) {
9755
- const relayDir = path24.join(opts.root, RELAY_DIRNAME);
9756
- const relayScript = path24.join(relayDir, "xdg-open");
9757
- const urlFile = path24.join(relayDir, "url");
9758
- await fsp5.mkdir(relayDir, { recursive: true });
9759
- await fsp5.rm(urlFile, { force: true });
9760
- await fsp5.writeFile(
9761
- relayScript,
9762
- `#!/bin/sh
9763
- printf '%s\\n' "$1" > "$(dirname "$0")/url"
9764
- exit 0
9765
- `,
9766
- { mode: 493 }
9767
- );
9768
- await fsp5.chmod(relayScript, 493);
9769
- const servers = [];
9770
- let lastOpened = null;
9771
- const onUrl = (url) => {
9772
- openInBrowser(url);
9773
- const target = parseCallbackTarget(url);
9774
- if (!target) return;
9775
- const server = http.createServer((req, res) => {
9776
- const reqUrl = req.url ?? target.pathname;
9777
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
9778
- res.end(
9779
- '<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>'
9780
- );
9781
- void opts.spawn(
9782
- [
9783
- "exec",
9784
- "--workspace-folder",
9785
- opts.root,
9786
- "--mount-workspace-git-root=false",
9787
- "curl",
9788
- "-fsS",
9789
- `http://localhost:${target.port}${reqUrl}`
9790
- ],
9791
- opts.root,
9792
- { quiet: true }
9793
- );
9794
- });
9795
- server.on("error", () => {
9796
- });
9797
- server.listen(target.port, "127.0.0.1");
9798
- servers.push(server);
9799
- };
9800
- const poll = setInterval(() => {
9801
- if (!existsSync15(urlFile)) return;
9802
- let content = "";
9803
- try {
9804
- content = readFileSync5(urlFile, "utf8");
9805
- } catch {
9806
- return;
9807
- }
9808
- const url = nextRelayUrl(content, lastOpened);
9809
- if (!url) return;
9810
- lastOpened = url;
9811
- onUrl(url);
9812
- }, 250);
9813
- return {
9814
- relayDirInContainer: `/workspaces/${opts.name}/${RELAY_DIRNAME}`,
9815
- async dispose() {
9816
- clearInterval(poll);
9817
- for (const s of servers) s.close();
9818
- await fsp5.rm(relayDir, { recursive: true, force: true });
9819
- }
9820
- };
9821
- }
9822
- var RELAY_DIRNAME;
9823
- var init_browser_bridge = __esm({
9824
- "src/devcontainer/browser-bridge.ts"() {
9825
- "use strict";
9826
- RELAY_DIRNAME = ".monoceros-bridge";
9827
- }
9828
- });
9829
-
9830
10291
  // src/devcontainer/claude-trust.ts
9831
- import { existsSync as existsSync16, promises as fsp6 } from "fs";
9832
- import path25 from "path";
10292
+ import { existsSync as existsSync19, promises as fsp6 } from "fs";
10293
+ import path28 from "path";
9833
10294
  function resolveContainerCwd(name, cwd) {
9834
10295
  const workspace = `/workspaces/${name}`;
9835
10296
  if (!cwd) return workspace;
9836
- return path25.posix.isAbsolute(cwd) ? cwd : path25.posix.join(workspace, cwd);
10297
+ return path28.posix.isAbsolute(cwd) ? cwd : path28.posix.join(workspace, cwd);
9837
10298
  }
9838
10299
  async function preApproveClaudeProject(opts) {
9839
- const file = path25.join(opts.root, "home", ".claude.json");
9840
- if (!existsSync16(file)) return;
10300
+ const file = path28.join(opts.root, "home", ".claude.json");
10301
+ if (!existsSync19(file)) return;
9841
10302
  try {
9842
10303
  const raw = await fsp6.readFile(file, "utf8");
9843
10304
  const config = raw.trim() ? JSON.parse(raw) : {};
@@ -9870,56 +10331,6 @@ var init_claude_trust = __esm({
9870
10331
  }
9871
10332
  });
9872
10333
 
9873
- // src/devcontainer/shell.ts
9874
- import { existsSync as existsSync17 } from "fs";
9875
- import path26 from "path";
9876
- async function runShell(opts) {
9877
- assertContainerExists(opts.root);
9878
- const spawnFn = opts.spawn ?? spawnDevcontainer;
9879
- const upCode = await spawnFn(
9880
- ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
9881
- opts.root,
9882
- { quiet: true }
9883
- );
9884
- if (upCode !== 0) return upCode;
9885
- const name = opts.name ?? path26.basename(opts.root);
9886
- const isTty2 = opts.isTty ?? process.stdout.isTTY;
9887
- const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
9888
- try {
9889
- const innerExec = wrapExec(
9890
- ["bash"],
9891
- bridge ? { pathPrepend: bridge.relayDirInContainer } : {}
9892
- );
9893
- return await spawnFn(
9894
- [
9895
- "exec",
9896
- "--workspace-folder",
9897
- opts.root,
9898
- "--mount-workspace-git-root=false",
9899
- ...innerExec
9900
- ],
9901
- opts.root,
9902
- { interactive: true }
9903
- );
9904
- } finally {
9905
- if (bridge) await bridge.dispose();
9906
- }
9907
- }
9908
- function assertContainerExists(root) {
9909
- if (!existsSync17(path26.join(root, ".devcontainer"))) {
9910
- throw new Error(
9911
- `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
9912
- );
9913
- }
9914
- }
9915
- var init_shell = __esm({
9916
- "src/devcontainer/shell.ts"() {
9917
- "use strict";
9918
- init_browser_bridge();
9919
- init_cli();
9920
- }
9921
- });
9922
-
9923
10334
  // src/devcontainer/run.ts
9924
10335
  async function runInContainer(opts) {
9925
10336
  if (opts.command.length === 0) {
@@ -9978,8 +10389,8 @@ var init_run = __esm({
9978
10389
  });
9979
10390
 
9980
10391
  // src/commands/run.ts
9981
- import { defineCommand as defineCommand24 } from "citty";
9982
- import { consola as consola29 } from "consola";
10392
+ import { defineCommand as defineCommand25 } from "citty";
10393
+ import { consola as consola31 } from "consola";
9983
10394
  var runCommand;
9984
10395
  var init_run2 = __esm({
9985
10396
  "src/commands/run.ts"() {
@@ -9987,7 +10398,7 @@ var init_run2 = __esm({
9987
10398
  init_paths();
9988
10399
  init_run();
9989
10400
  init_inner_args();
9990
- runCommand = defineCommand24({
10401
+ runCommand = defineCommand25({
9991
10402
  meta: {
9992
10403
  name: "run",
9993
10404
  group: "run",
@@ -10007,7 +10418,7 @@ var init_run2 = __esm({
10007
10418
  async run({ args }) {
10008
10419
  const command = [...getInnerArgs()];
10009
10420
  if (command.length === 0) {
10010
- consola29.error(
10421
+ consola31.error(
10011
10422
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
10012
10423
  );
10013
10424
  process.exit(1);
@@ -10021,7 +10432,7 @@ var init_run2 = __esm({
10021
10432
  });
10022
10433
  process.exit(exitCode);
10023
10434
  } catch (err) {
10024
- consola29.error(err instanceof Error ? err.message : String(err));
10435
+ consola31.error(err instanceof Error ? err.message : String(err));
10025
10436
  process.exit(1);
10026
10437
  }
10027
10438
  }
@@ -10030,15 +10441,15 @@ var init_run2 = __esm({
10030
10441
  });
10031
10442
 
10032
10443
  // src/commands/shell.ts
10033
- import { defineCommand as defineCommand25 } from "citty";
10034
- import { consola as consola30 } from "consola";
10444
+ import { defineCommand as defineCommand26 } from "citty";
10445
+ import { consola as consola32 } from "consola";
10035
10446
  var shellCommand;
10036
10447
  var init_shell2 = __esm({
10037
10448
  "src/commands/shell.ts"() {
10038
10449
  "use strict";
10039
10450
  init_paths();
10040
10451
  init_shell();
10041
- shellCommand = defineCommand25({
10452
+ shellCommand = defineCommand26({
10042
10453
  meta: {
10043
10454
  name: "shell",
10044
10455
  group: "run",
@@ -10059,7 +10470,7 @@ var init_shell2 = __esm({
10059
10470
  });
10060
10471
  process.exit(exitCode);
10061
10472
  } catch (err) {
10062
- consola30.error(err instanceof Error ? err.message : String(err));
10473
+ consola32.error(err instanceof Error ? err.message : String(err));
10063
10474
  process.exit(1);
10064
10475
  }
10065
10476
  }
@@ -10068,8 +10479,8 @@ var init_shell2 = __esm({
10068
10479
  });
10069
10480
 
10070
10481
  // src/commands/start.ts
10071
- import { defineCommand as defineCommand26 } from "citty";
10072
- import { consola as consola31 } from "consola";
10482
+ import { defineCommand as defineCommand27 } from "citty";
10483
+ import { consola as consola33 } from "consola";
10073
10484
  var startCommand;
10074
10485
  var init_start = __esm({
10075
10486
  "src/commands/start.ts"() {
@@ -10078,10 +10489,11 @@ var init_start = __esm({
10078
10489
  init_io();
10079
10490
  init_paths();
10080
10491
  init_compose();
10492
+ init_open();
10081
10493
  init_proxy();
10082
10494
  init_port_check();
10083
10495
  init_dispatch();
10084
- startCommand = defineCommand26({
10496
+ startCommand = defineCommand27({
10085
10497
  meta: {
10086
10498
  name: "start",
10087
10499
  group: "run",
@@ -10092,6 +10504,10 @@ var init_start = __esm({
10092
10504
  type: "positional",
10093
10505
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10094
10506
  required: true
10507
+ },
10508
+ open: {
10509
+ type: "string",
10510
+ description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
10095
10511
  }
10096
10512
  },
10097
10513
  run({ args }) {
@@ -10106,7 +10522,7 @@ var init_start = __esm({
10106
10522
  hostPort = proxyHostPort(global);
10107
10523
  }
10108
10524
  } catch (err) {
10109
- consola31.warn(
10525
+ consola33.warn(
10110
10526
  `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
10111
10527
  );
10112
10528
  }
@@ -10114,7 +10530,15 @@ var init_start = __esm({
10114
10530
  await preflightHostPort(hostPort);
10115
10531
  await ensureProxy({ hostPort });
10116
10532
  }
10117
- 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;
10118
10542
  });
10119
10543
  }
10120
10544
  });
@@ -10122,7 +10546,7 @@ var init_start = __esm({
10122
10546
  });
10123
10547
 
10124
10548
  // src/commands/status.ts
10125
- import { defineCommand as defineCommand27 } from "citty";
10549
+ import { defineCommand as defineCommand28 } from "citty";
10126
10550
  var statusCommand;
10127
10551
  var init_status = __esm({
10128
10552
  "src/commands/status.ts"() {
@@ -10130,7 +10554,7 @@ var init_status = __esm({
10130
10554
  init_paths();
10131
10555
  init_compose();
10132
10556
  init_dispatch();
10133
- statusCommand = defineCommand27({
10557
+ statusCommand = defineCommand28({
10134
10558
  meta: {
10135
10559
  name: "status",
10136
10560
  group: "run",
@@ -10160,8 +10584,8 @@ var init_status = __esm({
10160
10584
  });
10161
10585
 
10162
10586
  // src/commands/stop.ts
10163
- import { defineCommand as defineCommand28 } from "citty";
10164
- import { consola as consola32 } from "consola";
10587
+ import { defineCommand as defineCommand29 } from "citty";
10588
+ import { consola as consola34 } from "consola";
10165
10589
  var stopCommand;
10166
10590
  var init_stop = __esm({
10167
10591
  "src/commands/stop.ts"() {
@@ -10170,7 +10594,7 @@ var init_stop = __esm({
10170
10594
  init_compose();
10171
10595
  init_proxy();
10172
10596
  init_dispatch();
10173
- stopCommand = defineCommand28({
10597
+ stopCommand = defineCommand29({
10174
10598
  meta: {
10175
10599
  name: "stop",
10176
10600
  group: "run",
@@ -10195,10 +10619,10 @@ var init_stop = __esm({
10195
10619
  });
10196
10620
  try {
10197
10621
  await maybeStopProxy({
10198
- logger: { info: (msg) => consola32.info(msg) }
10622
+ logger: { info: (msg) => consola34.info(msg) }
10199
10623
  });
10200
10624
  } catch (err) {
10201
- consola32.warn(
10625
+ consola34.warn(
10202
10626
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
10203
10627
  );
10204
10628
  }
@@ -10210,11 +10634,11 @@ var init_stop = __esm({
10210
10634
  });
10211
10635
 
10212
10636
  // src/tunnel/resolve.ts
10213
- import { existsSync as existsSync18 } from "fs";
10214
- import path27 from "path";
10637
+ import { existsSync as existsSync20 } from "fs";
10638
+ import path29 from "path";
10215
10639
  async function resolveTunnelTarget(opts) {
10216
10640
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
10217
- if (!existsSync18(ymlPath)) {
10641
+ if (!existsSync20(ymlPath)) {
10218
10642
  throw new Error(
10219
10643
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
10220
10644
  );
@@ -10222,13 +10646,13 @@ async function resolveTunnelTarget(opts) {
10222
10646
  const parsed = await readConfig(ymlPath);
10223
10647
  const config = parsed.config;
10224
10648
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
10225
- if (!existsSync18(containerRoot)) {
10649
+ if (!existsSync20(containerRoot)) {
10226
10650
  throw new Error(
10227
10651
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
10228
10652
  );
10229
10653
  }
10230
- const composePath = path27.join(containerRoot, ".devcontainer", "compose.yaml");
10231
- const isCompose = existsSync18(composePath);
10654
+ const composePath = path29.join(containerRoot, ".devcontainer", "compose.yaml");
10655
+ const isCompose = existsSync20(composePath);
10232
10656
  const parsedTarget = parseTargetArg(opts.target, config);
10233
10657
  const docker = opts.docker ?? defaultDockerExec;
10234
10658
  if (isCompose) {
@@ -10463,8 +10887,8 @@ var init_port_check2 = __esm({
10463
10887
  });
10464
10888
 
10465
10889
  // src/tunnel/run.ts
10466
- import { spawn as spawn10 } from "child_process";
10467
- import { consola as consola33 } from "consola";
10890
+ import { spawn as spawn12 } from "child_process";
10891
+ import { consola as consola35 } from "consola";
10468
10892
  function signalNumber(signal) {
10469
10893
  switch (signal) {
10470
10894
  case "SIGINT":
@@ -10477,8 +10901,8 @@ function signalNumber(signal) {
10477
10901
  }
10478
10902
  async function runTunnel(opts) {
10479
10903
  const log = opts.logger ?? {
10480
- info: (m) => consola33.info(m),
10481
- warn: (m) => consola33.warn(m)
10904
+ info: (m) => consola35.info(m),
10905
+ warn: (m) => consola35.warn(m)
10482
10906
  };
10483
10907
  const resolve = opts.resolve ?? resolveTunnelTarget;
10484
10908
  const resolveArgs3 = {
@@ -10553,7 +10977,7 @@ var init_run3 = __esm({
10553
10977
  init_port_check2();
10554
10978
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
10555
10979
  defaultDockerSpawn = (args) => {
10556
- const child = spawn10("docker", args, {
10980
+ const child = spawn12("docker", args, {
10557
10981
  stdio: "inherit"
10558
10982
  });
10559
10983
  const exited = new Promise((resolve, reject) => {
@@ -10584,8 +11008,8 @@ var init_run3 = __esm({
10584
11008
  });
10585
11009
 
10586
11010
  // src/commands/tunnel.ts
10587
- import { defineCommand as defineCommand29 } from "citty";
10588
- import { consola as consola34 } from "consola";
11011
+ import { defineCommand as defineCommand30 } from "citty";
11012
+ import { consola as consola36 } from "consola";
10589
11013
  function parseLocalPort(raw) {
10590
11014
  if (raw === void 0) return void 0;
10591
11015
  const n = Number(raw);
@@ -10601,7 +11025,7 @@ var init_tunnel = __esm({
10601
11025
  "src/commands/tunnel.ts"() {
10602
11026
  "use strict";
10603
11027
  init_run3();
10604
- tunnelCommand = defineCommand29({
11028
+ tunnelCommand = defineCommand30({
10605
11029
  meta: {
10606
11030
  name: "tunnel",
10607
11031
  group: "discovery",
@@ -10638,7 +11062,7 @@ var init_tunnel = __esm({
10638
11062
  });
10639
11063
  process.exit(exitCode);
10640
11064
  } catch (err) {
10641
- consola34.error(err instanceof Error ? err.message : String(err));
11065
+ consola36.error(err instanceof Error ? err.message : String(err));
10642
11066
  process.exit(1);
10643
11067
  }
10644
11068
  }
@@ -10708,8 +11132,8 @@ var init_prune = __esm({
10708
11132
  });
10709
11133
 
10710
11134
  // src/upgrade/index.ts
10711
- import { existsSync as existsSync19, promises as fs17 } from "fs";
10712
- import { consola as consola35 } from "consola";
11135
+ import { existsSync as existsSync21, promises as fs18 } from "fs";
11136
+ import { consola as consola37 } from "consola";
10713
11137
  async function fetchRuntimeVersions() {
10714
11138
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
10715
11139
  const tokenRes = await fetch(tokenUrl);
@@ -10747,7 +11171,7 @@ ${yml}`;
10747
11171
  }
10748
11172
  async function runUpgrade(opts) {
10749
11173
  const home = opts.monocerosHome ?? monocerosHome();
10750
- const logger = opts.logger ?? consola35;
11174
+ const logger = opts.logger ?? consola37;
10751
11175
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
10752
11176
  if (opts.list) {
10753
11177
  const versions = await fetchVersions();
@@ -10773,7 +11197,7 @@ async function runUpgrade(opts) {
10773
11197
  );
10774
11198
  }
10775
11199
  }
10776
- if (opts.name && !existsSync19(containerConfigPath(opts.name, home))) {
11200
+ if (opts.name && !existsSync21(containerConfigPath(opts.name, home))) {
10777
11201
  throw new Error(
10778
11202
  `No such config: ${containerConfigPath(opts.name, home)}. Run \`monoceros init <template> ${opts.name}\` first.`
10779
11203
  );
@@ -10824,11 +11248,11 @@ async function runUpgrade(opts) {
10824
11248
  let bumped = 0;
10825
11249
  for (const name of targets) {
10826
11250
  const ymlPath = containerConfigPath(name, home);
10827
- if (!existsSync19(ymlPath)) continue;
10828
- const raw = await fs17.readFile(ymlPath, "utf8");
11251
+ if (!existsSync21(ymlPath)) continue;
11252
+ const raw = await fs18.readFile(ymlPath, "utf8");
10829
11253
  const updated = setRuntimeVersion(raw, pinVersion);
10830
11254
  if (updated !== raw) {
10831
- await fs17.writeFile(ymlPath, updated);
11255
+ await fs18.writeFile(ymlPath, updated);
10832
11256
  bumped += 1;
10833
11257
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
10834
11258
  }
@@ -10870,7 +11294,7 @@ function formatPruneLine(prune) {
10870
11294
  }
10871
11295
  async function listContainerNames2(home) {
10872
11296
  try {
10873
- const entries = await fs17.readdir(containerConfigsDir(home));
11297
+ const entries = await fs18.readdir(containerConfigsDir(home));
10874
11298
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
10875
11299
  } catch {
10876
11300
  return [];
@@ -10893,7 +11317,7 @@ var init_upgrade = __esm({
10893
11317
  });
10894
11318
 
10895
11319
  // src/commands/upgrade.ts
10896
- import { defineCommand as defineCommand30 } from "citty";
11320
+ import { defineCommand as defineCommand31 } from "citty";
10897
11321
  var upgradeCommand;
10898
11322
  var init_upgrade2 = __esm({
10899
11323
  "src/commands/upgrade.ts"() {
@@ -10901,7 +11325,7 @@ var init_upgrade2 = __esm({
10901
11325
  init_upgrade();
10902
11326
  init_version();
10903
11327
  init_dispatch();
10904
- upgradeCommand = defineCommand30({
11328
+ upgradeCommand = defineCommand31({
10905
11329
  meta: {
10906
11330
  name: "upgrade",
10907
11331
  group: "lifecycle",
@@ -10943,7 +11367,7 @@ var main_exports = {};
10943
11367
  __export(main_exports, {
10944
11368
  main: () => main
10945
11369
  });
10946
- import { defineCommand as defineCommand31 } from "citty";
11370
+ import { defineCommand as defineCommand32 } from "citty";
10947
11371
  var main;
10948
11372
  var init_main = __esm({
10949
11373
  "src/main.ts"() {
@@ -10961,6 +11385,7 @@ var init_main = __esm({
10961
11385
  init_init2();
10962
11386
  init_list_components();
10963
11387
  init_logs();
11388
+ init_open2();
10964
11389
  init_port();
10965
11390
  init_remove_apt_packages();
10966
11391
  init_remove_feature();
@@ -10979,7 +11404,7 @@ var init_main = __esm({
10979
11404
  init_tunnel();
10980
11405
  init_upgrade2();
10981
11406
  init_version();
10982
- main = defineCommand31({
11407
+ main = defineCommand32({
10983
11408
  meta: {
10984
11409
  name: "monoceros",
10985
11410
  version: CLI_VERSION,
@@ -10989,6 +11414,7 @@ var init_main = __esm({
10989
11414
  init: initCommand,
10990
11415
  "list-components": listComponentsCommand,
10991
11416
  shell: shellCommand,
11417
+ open: openCommand,
10992
11418
  run: runCommand,
10993
11419
  logs: logsCommand,
10994
11420
  start: startCommand,
@@ -11275,25 +11701,25 @@ function detectHelpRequest(argv, main2) {
11275
11701
  const separatorIdx = argv.indexOf("--");
11276
11702
  if (helpIdx === -1) return null;
11277
11703
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
11278
- const path28 = [];
11704
+ const path30 = [];
11279
11705
  const tokens = argv.slice(
11280
11706
  0,
11281
11707
  separatorIdx === -1 ? argv.length : separatorIdx
11282
11708
  );
11283
11709
  let cursor = main2;
11284
11710
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
11285
- path28.push(mainName);
11711
+ path30.push(mainName);
11286
11712
  for (const tok of tokens) {
11287
11713
  if (tok.startsWith("-")) continue;
11288
11714
  const subs = cursor.subCommands ?? {};
11289
11715
  if (tok in subs) {
11290
11716
  cursor = subs[tok];
11291
- path28.push(tok);
11717
+ path30.push(tok);
11292
11718
  continue;
11293
11719
  }
11294
11720
  break;
11295
11721
  }
11296
- return { path: path28, cmd: cursor };
11722
+ return { path: path30, cmd: cursor };
11297
11723
  }
11298
11724
  async function maybeRenderHelp(argv, main2) {
11299
11725
  const hit = detectHelpRequest(argv, main2);