@getmonoceros/workbench 1.33.8 → 1.34.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
@@ -2396,7 +2396,7 @@ var init_catalog = __esm({
2396
2396
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2397
2397
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2398
2398
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2399
- DEFAULT_RUNTIME_VERSION = true ? "1.3.6" : readFileSync3(
2399
+ DEFAULT_RUNTIME_VERSION = true ? "1.4.0" : readFileSync3(
2400
2400
  fileURLToPath2(
2401
2401
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2402
2402
  ),
@@ -3932,6 +3932,10 @@ function buildPostCreateScript(opts) {
3932
3932
  "# identity values in.",
3933
3933
  `git config --global include.path "/workspaces/${opts.name}/.monoceros/gitconfig"`,
3934
3934
  "",
3935
+ "# Container-global gitignore: keeps each app's .monoceros/ launch-config",
3936
+ "# dir out of the app repo by default. Per-app opt-in re-includes it.",
3937
+ `git config --global core.excludesFile "/workspaces/${opts.name}/.monoceros/global-gitignore"`,
3938
+ "",
3935
3939
  "# Per-feature post-create hooks. Each Monoceros-curated feature",
3936
3940
  "# may drop a script into /usr/local/share/monoceros/post-create.d/",
3937
3941
  "# during its install.sh \u2014 typical job is a non-interactive login",
@@ -4072,7 +4076,11 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4072
4076
  }
4073
4077
  await fs8.writeFile(
4074
4078
  path11.join(monocerosDir, ".gitignore"),
4075
- "git-credentials*\ngitconfig\n"
4079
+ "git-credentials*\ngitconfig\nglobal-gitignore\n"
4080
+ );
4081
+ await fs8.writeFile(
4082
+ path11.join(monocerosDir, "global-gitignore"),
4083
+ ".monoceros/\n"
4076
4084
  );
4077
4085
  const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
4078
4086
  await writeIfChanged(
@@ -4630,8 +4638,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
4630
4638
  if (!isMap2(item)) return false;
4631
4639
  const url = item.get("url");
4632
4640
  if (url === urlOrPath) return true;
4633
- const path31 = item.get("path");
4634
- const effectivePath = typeof path31 === "string" ? path31 : typeof url === "string" ? deriveRepoName(url) : void 0;
4641
+ const path32 = item.get("path");
4642
+ const effectivePath = typeof path32 === "string" ? path32 : typeof url === "string" ? deriveRepoName(url) : void 0;
4635
4643
  return effectivePath === urlOrPath;
4636
4644
  });
4637
4645
  if (idx < 0) return false;
@@ -4754,7 +4762,7 @@ async function runAddRepo(input) {
4754
4762
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
4755
4763
  );
4756
4764
  }
4757
- const path31 = (input.path ?? deriveRepoName(url)).trim();
4765
+ const path32 = (input.path ?? deriveRepoName(url)).trim();
4758
4766
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
4759
4767
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
4760
4768
  if (hasName !== hasEmail) {
@@ -4791,7 +4799,7 @@ async function runAddRepo(input) {
4791
4799
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
4792
4800
  const entry2 = {
4793
4801
  url,
4794
- path: path31,
4802
+ path: path32,
4795
4803
  ...hasName && hasEmail ? {
4796
4804
  gitUser: {
4797
4805
  name: input.gitName.trim(),
@@ -6611,10 +6619,14 @@ function spawnBridgeDaemon(root) {
6611
6619
  const self = process.argv[1];
6612
6620
  if (!self) return;
6613
6621
  mkdirSync(relayDir(root), { recursive: true });
6614
- const child = spawn8(process.execPath, [self, "__bridge", root], {
6615
- detached: true,
6616
- stdio: "ignore"
6617
- });
6622
+ const child = spawn8(
6623
+ process.execPath,
6624
+ [...process.execArgv, self, "__bridge", root],
6625
+ {
6626
+ detached: true,
6627
+ stdio: "ignore"
6628
+ }
6629
+ );
6618
6630
  if (typeof child.pid === "number") {
6619
6631
  try {
6620
6632
  writeFileSync(bridgePidFile(root), String(child.pid));
@@ -7211,46 +7223,58 @@ function generateAgentsMd(input) {
7211
7223
  lines.push("");
7212
7224
  lines.push(
7213
7225
  "When you build something that serves on a port (a web app, an API),",
7214
- "start it as a **detached** background process so it keeps running after",
7215
- "this session ends. A plain `npm start` (or any foreground start) dies",
7216
- "the moment the user exits you or closes the terminal \u2014 and then",
7217
- `\`${input.containerName}.localhost\` returns 502 Bad Gateway.`
7226
+ "it must keep running after this session ends. A plain `npm start` (or",
7227
+ "any foreground start) dies the moment the user exits you or closes the",
7228
+ `terminal, and then \`${input.containerName}.localhost\` returns 502 Bad Gateway.`
7218
7229
  );
7219
7230
  lines.push("");
7220
7231
  lines.push(
7221
- "Launch it in a new session with `setsid`, using the project's own",
7222
- "start command, and record the process-group PID + log under the",
7223
- "container's logs directory:"
7232
+ "Declare it in the app's own launch config at",
7233
+ "`projects/<app>/.monoceros/launch.json`, then start it with",
7234
+ "`monoceros-ctl`. Add or update an entry whenever you set up a",
7235
+ "long-running server. The file travels with the app, so the human can",
7236
+ "restart it later without knowing your start command:"
7224
7237
  );
7225
7238
  lines.push("");
7226
- lines.push("```");
7227
- lines.push(
7228
- `setsid sh -c 'echo $$ >/workspaces/${input.containerName}/logs/<app>.pid; \\`
7229
- );
7239
+ lines.push("```json");
7240
+ lines.push("{");
7241
+ lines.push(' "configurations": [');
7230
7242
  lines.push(
7231
- ` exec <the project's start command> >/workspaces/${input.containerName}/logs/<app>.log 2>&1' </dev/null &`
7243
+ ` { "name": "web", "command": "<the project's start command>", "port": ${input.ports[0]}, "default": true }`
7232
7244
  );
7245
+ lines.push(" ]");
7246
+ lines.push("}");
7233
7247
  lines.push("```");
7234
7248
  lines.push("");
7235
7249
  lines.push(
7236
- "Use whatever start command the project actually uses (`npm start`,",
7237
- "`./mvnw spring-boot:run`, `python app.py`, `go run .`, \u2026) \u2014 do not force",
7238
- "a language-specific one. `<app>` is a short name you choose."
7250
+ "Use whatever start command the project actually uses (`npm run dev`,",
7251
+ "`./mvnw spring-boot:run`, `python manage.py runserver`, `go run .`, \u2026).",
7252
+ "Do not force a language-specific one. `<app>` is the path under",
7253
+ "`projects/`; `port` must be a port already exposed on the container."
7239
7254
  );
7240
7255
  lines.push("");
7241
- lines.push("To stop it, kill the whole process group (also stops children");
7242
- lines.push("like node under npm or java under maven):");
7256
+ lines.push("Start it, stop it, tail its log:");
7243
7257
  lines.push("");
7244
7258
  lines.push("```");
7259
+ lines.push("monoceros-ctl start <app>");
7260
+ lines.push("monoceros-ctl stop <app>");
7261
+ lines.push("monoceros-ctl logs <app>");
7262
+ lines.push("```");
7263
+ lines.push("");
7245
7264
  lines.push(
7246
- `kill -TERM -$(cat /workspaces/${input.containerName}/logs/<app>.pid)`
7265
+ "`start` launches it detached (it survives your session) and, when a",
7266
+ "`port` is set, waits until it actually listens before returning. Add",
7267
+ "`--target <name>` when the app declares more than one configuration.",
7268
+ "The human can do the same from the host with",
7269
+ `\`monoceros start ${input.containerName} <app>\` / \`monoceros stop ${input.containerName} <app>\`,`,
7270
+ `and follow output with \`monoceros logs ${input.containerName} <app>\`.`
7247
7271
  );
7248
- lines.push("```");
7249
7272
  lines.push("");
7250
7273
  lines.push(
7251
- `The user can follow the output with \`monoceros logs ${input.containerName} <app>\``,
7252
- "on the host. The server must listen on `0.0.0.0` (not `127.0.0.1`) on",
7253
- "the exposed port, or Traefik cannot reach it."
7274
+ "The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
7275
+ "port, or Traefik cannot reach it. You only have the ports already",
7276
+ "declared on the container; if you need another, ask the human to add it",
7277
+ `on the host (\`monoceros add-port ${input.containerName} <port>\`) and re-apply.`
7254
7278
  );
7255
7279
  lines.push("");
7256
7280
  }
@@ -8661,7 +8685,7 @@ var CLI_VERSION;
8661
8685
  var init_version = __esm({
8662
8686
  "src/version.ts"() {
8663
8687
  "use strict";
8664
- CLI_VERSION = true ? "1.33.8" : "dev";
8688
+ CLI_VERSION = true ? "1.34.0" : "dev";
8665
8689
  }
8666
8690
  });
8667
8691
 
@@ -8894,9 +8918,144 @@ var init_bridge = __esm({
8894
8918
  }
8895
8919
  });
8896
8920
 
8897
- // src/completion/resolve.ts
8898
- import { existsSync as existsSync15, promises as fs14 } from "fs";
8921
+ // src/config/launch-config.ts
8922
+ import { promises as fs14 } from "fs";
8899
8923
  import path24 from "path";
8924
+ function launchConfigPath(name, appRel, home) {
8925
+ return path24.join(
8926
+ containerDir(name, home),
8927
+ "projects",
8928
+ appRel,
8929
+ LAUNCH_DIRNAME,
8930
+ LAUNCH_FILENAME
8931
+ );
8932
+ }
8933
+ function validate(parsed, where) {
8934
+ if (typeof parsed !== "object" || parsed === null) {
8935
+ throw new Error(`${where}: expected a JSON object`);
8936
+ }
8937
+ const obj = parsed;
8938
+ if (!Array.isArray(obj.configurations)) {
8939
+ throw new Error(`${where}: missing "configurations" array`);
8940
+ }
8941
+ const seen = /* @__PURE__ */ new Set();
8942
+ let defaults = 0;
8943
+ const configurations = obj.configurations.map((raw, i) => {
8944
+ if (typeof raw !== "object" || raw === null) {
8945
+ throw new Error(`${where}: configuration #${i} is not an object`);
8946
+ }
8947
+ const t = raw;
8948
+ if (typeof t.name !== "string" || t.name.length === 0) {
8949
+ throw new Error(`${where}: configuration #${i} is missing "name"`);
8950
+ }
8951
+ if (seen.has(t.name)) {
8952
+ throw new Error(`${where}: duplicate target name "${t.name}"`);
8953
+ }
8954
+ seen.add(t.name);
8955
+ if (typeof t.command !== "string" || t.command.length === 0) {
8956
+ throw new Error(`${where}: target "${t.name}" is missing "command"`);
8957
+ }
8958
+ if (t.default === true) defaults += 1;
8959
+ return {
8960
+ name: t.name,
8961
+ command: t.command,
8962
+ ...typeof t.cwd === "string" ? { cwd: t.cwd } : {},
8963
+ ...typeof t.port === "number" ? { port: t.port } : {},
8964
+ ...t.env && typeof t.env === "object" ? { env: t.env } : {},
8965
+ ...t.default === true ? { default: true } : {}
8966
+ };
8967
+ });
8968
+ if (defaults > 1) {
8969
+ throw new Error(`${where}: more than one target marked "default"`);
8970
+ }
8971
+ return {
8972
+ version: typeof obj.version === "number" ? obj.version : 1,
8973
+ configurations
8974
+ };
8975
+ }
8976
+ async function readLaunchConfig(name, appRel, home) {
8977
+ const file = launchConfigPath(name, appRel, home);
8978
+ let content;
8979
+ try {
8980
+ content = await fs14.readFile(file, "utf8");
8981
+ } catch {
8982
+ return void 0;
8983
+ }
8984
+ let parsed;
8985
+ try {
8986
+ parsed = JSON.parse(content);
8987
+ } catch (err) {
8988
+ throw new Error(
8989
+ `${file}: invalid JSON (${err instanceof Error ? err.message : String(err)})`
8990
+ );
8991
+ }
8992
+ return validate(parsed, file);
8993
+ }
8994
+ function resolveTarget(config, targetName, appRel) {
8995
+ if (targetName) {
8996
+ const found = config.configurations.find((t) => t.name === targetName);
8997
+ if (!found) {
8998
+ throw new Error(
8999
+ `No target "${targetName}" in ${appRel} (have: ${config.configurations.map((t) => t.name).join(", ") || "none"})`
9000
+ );
9001
+ }
9002
+ return found;
9003
+ }
9004
+ const marked = config.configurations.find((t) => t.default);
9005
+ if (marked) return marked;
9006
+ if (config.configurations.length === 1) return config.configurations[0];
9007
+ throw new Error(
9008
+ `${appRel} has ${config.configurations.length} targets and no default \u2014 pass --target (${config.configurations.map((t) => t.name).join(", ")})`
9009
+ );
9010
+ }
9011
+ async function listApps(name, home) {
9012
+ const projectsRoot = path24.join(containerDir(name, home), "projects");
9013
+ const out = [];
9014
+ async function walk(at, rel, depth) {
9015
+ let entries;
9016
+ try {
9017
+ entries = await fs14.readdir(at, { withFileTypes: true });
9018
+ } catch {
9019
+ return;
9020
+ }
9021
+ const hasLaunch = entries.some(
9022
+ (e) => e.isDirectory() && e.name === LAUNCH_DIRNAME
9023
+ );
9024
+ if (hasLaunch) {
9025
+ try {
9026
+ await fs14.access(path24.join(at, LAUNCH_DIRNAME, LAUNCH_FILENAME));
9027
+ if (rel) out.push(rel);
9028
+ } catch {
9029
+ }
9030
+ }
9031
+ if (depth >= APP_SEARCH_MAX_DEPTH) return;
9032
+ for (const e of entries) {
9033
+ if (!e.isDirectory()) continue;
9034
+ if (e.name.startsWith(".")) continue;
9035
+ await walk(
9036
+ path24.join(at, e.name),
9037
+ rel ? `${rel}/${e.name}` : e.name,
9038
+ depth + 1
9039
+ );
9040
+ }
9041
+ }
9042
+ await walk(projectsRoot, "", 0);
9043
+ return out.sort();
9044
+ }
9045
+ var LAUNCH_DIRNAME, LAUNCH_FILENAME, APP_SEARCH_MAX_DEPTH;
9046
+ var init_launch_config = __esm({
9047
+ "src/config/launch-config.ts"() {
9048
+ "use strict";
9049
+ init_paths();
9050
+ LAUNCH_DIRNAME = ".monoceros";
9051
+ LAUNCH_FILENAME = "launch.json";
9052
+ APP_SEARCH_MAX_DEPTH = 4;
9053
+ }
9054
+ });
9055
+
9056
+ // src/completion/resolve.ts
9057
+ import { existsSync as existsSync15, promises as fs15 } from "fs";
9058
+ import path25 from "path";
8900
9059
  async function resolveCompletions(line, point, opts = {}) {
8901
9060
  const { prev, current } = parseCompletionLine(line, point);
8902
9061
  const ctx = { prev, current, opts };
@@ -9045,9 +9204,9 @@ function filterPrefix(values, fragment) {
9045
9204
  }
9046
9205
  async function listContainerNames(ctx) {
9047
9206
  const home = ctx.opts.monocerosHome ?? monocerosHome();
9048
- const dir = path24.join(home, "container-configs");
9207
+ const dir = path25.join(home, "container-configs");
9049
9208
  if (!existsSync15(dir)) return [];
9050
- const entries = await fs14.readdir(dir);
9209
+ const entries = await fs15.readdir(dir);
9051
9210
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
9052
9211
  }
9053
9212
  function containerNameFromCtx(ctx) {
@@ -9059,12 +9218,42 @@ function containerNameFromCtx(ctx) {
9059
9218
  }
9060
9219
  return void 0;
9061
9220
  }
9221
+ function positionalFromCtx(ctx, n) {
9222
+ const positionals = [];
9223
+ for (let i = 2; i < ctx.prev.length; i++) {
9224
+ const t = ctx.prev[i];
9225
+ if (t === "--") break;
9226
+ if (t.startsWith("-")) continue;
9227
+ positionals.push(t);
9228
+ }
9229
+ return positionals[n];
9230
+ }
9231
+ async function listAppCandidates(ctx) {
9232
+ const name = containerNameFromCtx(ctx);
9233
+ if (!name) return [];
9234
+ try {
9235
+ return await listApps(name, ctx.opts.monocerosHome);
9236
+ } catch {
9237
+ return [];
9238
+ }
9239
+ }
9240
+ async function listTargetCandidates(ctx) {
9241
+ const name = containerNameFromCtx(ctx);
9242
+ const app = positionalFromCtx(ctx, 1);
9243
+ if (!name || !app) return [];
9244
+ try {
9245
+ const cfg = await readLaunchConfig(name, app, ctx.opts.monocerosHome);
9246
+ return cfg ? cfg.configurations.map((t) => t.name) : [];
9247
+ } catch {
9248
+ return [];
9249
+ }
9250
+ }
9062
9251
  async function collectDirs(dir, maxDepth) {
9063
9252
  const out = [];
9064
9253
  async function walk(at, rel, depth) {
9065
9254
  let entries;
9066
9255
  try {
9067
- entries = await fs14.readdir(at, { withFileTypes: true });
9256
+ entries = await fs15.readdir(at, { withFileTypes: true });
9068
9257
  } catch {
9069
9258
  return;
9070
9259
  }
@@ -9075,7 +9264,7 @@ async function collectDirs(dir, maxDepth) {
9075
9264
  const childRel = rel ? `${rel}/${e.name}` : e.name;
9076
9265
  out.push(childRel);
9077
9266
  if (depth + 1 < maxDepth) {
9078
- await walk(path24.join(at, e.name), childRel, depth + 1);
9267
+ await walk(path25.join(at, e.name), childRel, depth + 1);
9079
9268
  }
9080
9269
  }
9081
9270
  }
@@ -9083,9 +9272,9 @@ async function collectDirs(dir, maxDepth) {
9083
9272
  return out;
9084
9273
  }
9085
9274
  async function listContainerWorkspaceDirs(home, name) {
9086
- const root = path24.join(home, "container", name);
9275
+ const root = path25.join(home, "container", name);
9087
9276
  const top = await collectDirs(root, 1);
9088
- const projects = (await collectDirs(path24.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9277
+ const projects = (await collectDirs(path25.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9089
9278
  return [.../* @__PURE__ */ new Set([...top, ...projects])].sort();
9090
9279
  }
9091
9280
  async function listRunInDirs(ctx) {
@@ -9161,6 +9350,7 @@ var init_resolve = __esm({
9161
9350
  "src/completion/resolve.ts"() {
9162
9351
  "use strict";
9163
9352
  init_paths();
9353
+ init_launch_config();
9164
9354
  init_components();
9165
9355
  init_manifest();
9166
9356
  init_catalog();
@@ -9170,6 +9360,7 @@ var init_resolve = __esm({
9170
9360
  PROJECTS_DIR_MAX_DEPTH = 4;
9171
9361
  ALL_COMMANDS = [
9172
9362
  "init",
9363
+ "list-apps",
9173
9364
  "list-components",
9174
9365
  "shell",
9175
9366
  "open",
@@ -9246,13 +9437,27 @@ var init_resolve = __esm({
9246
9437
  positionals: [containerName],
9247
9438
  flags: { "--in": { type: "value", values: (ctx) => listRunInDirs(ctx) } }
9248
9439
  },
9249
- logs: { positionals: [containerName] },
9440
+ logs: {
9441
+ positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9442
+ flags: {
9443
+ "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9444
+ }
9445
+ },
9250
9446
  start: {
9251
- positionals: [containerName],
9252
- flags: { "--open": { type: "value", values: () => [...OPEN_TOOLS] } }
9447
+ positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9448
+ flags: {
9449
+ "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) },
9450
+ "--open": { type: "value", values: () => [...OPEN_TOOLS] }
9451
+ }
9452
+ },
9453
+ stop: {
9454
+ positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9455
+ flags: {
9456
+ "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9457
+ }
9253
9458
  },
9254
- stop: { positionals: [containerName] },
9255
9459
  status: { positionals: [containerName] },
9460
+ "list-apps": { positionals: [containerName] },
9256
9461
  "add-language": {
9257
9462
  positionals: [containerName, () => listLanguageNames()]
9258
9463
  },
@@ -9915,8 +10120,8 @@ var init_generator = __esm({
9915
10120
  });
9916
10121
 
9917
10122
  // src/init/index.ts
9918
- import { existsSync as existsSync16, promises as fs15 } from "fs";
9919
- import path25 from "path";
10123
+ import { existsSync as existsSync16, promises as fs16 } from "fs";
10124
+ import path26 from "path";
9920
10125
  import { consola as consola16 } from "consola";
9921
10126
  async function runInit(opts) {
9922
10127
  const home = opts.monocerosHome ?? monocerosHome();
@@ -9935,7 +10140,7 @@ async function runInit(opts) {
9935
10140
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
9936
10141
  );
9937
10142
  }
9938
- const componentsRoot = opts.workbenchRoot ? path25.join(opts.workbenchRoot, "components") : componentsRootDir();
10143
+ const componentsRoot = opts.workbenchRoot ? path26.join(opts.workbenchRoot, "components") : componentsRootDir();
9939
10144
  const catalog = await loadComponentCatalog(componentsRoot);
9940
10145
  if (catalog.size === 0) {
9941
10146
  throw new Error(
@@ -10001,9 +10206,9 @@ async function runInit(opts) {
10001
10206
  } else {
10002
10207
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
10003
10208
  }
10004
- await fs15.mkdir(containerConfigsDir(home), { recursive: true });
10209
+ await fs16.mkdir(containerConfigsDir(home), { recursive: true });
10005
10210
  await ensureEnvGitignored(containerConfigsDir(home));
10006
- await fs15.writeFile(dest, text, "utf8");
10211
+ await fs16.writeFile(dest, text, "utf8");
10007
10212
  const envPath = containerEnvPath(opts.name, home);
10008
10213
  const seedVars = {};
10009
10214
  for (const f of composed.features) {
@@ -10026,8 +10231,8 @@ async function runInit(opts) {
10026
10231
  }
10027
10232
  await ensureEnvVars(envPath, opts.name, seedVars);
10028
10233
  const documented = !anyComposed;
10029
- const ymlRel = path25.relative(home, dest);
10030
- const envRel = path25.relative(home, envPath);
10234
+ const ymlRel = path26.relative(home, dest);
10235
+ const envRel = path26.relative(home, envPath);
10031
10236
  if (documented) {
10032
10237
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
10033
10238
  logger.info(
@@ -10305,6 +10510,75 @@ var init_init2 = __esm({
10305
10510
  }
10306
10511
  });
10307
10512
 
10513
+ // src/commands/list-apps.ts
10514
+ import { defineCommand as defineCommand14 } from "citty";
10515
+ import { consola as consola18 } from "consola";
10516
+ var listAppsCommand;
10517
+ var init_list_apps = __esm({
10518
+ "src/commands/list-apps.ts"() {
10519
+ "use strict";
10520
+ init_launch_config();
10521
+ init_format();
10522
+ listAppsCommand = defineCommand14({
10523
+ meta: {
10524
+ name: "list-apps",
10525
+ group: "discovery",
10526
+ description: "List the apps under the named container that declare a launch config (projects/<app>/.monoceros/launch.json), with their targets and which is the default. Pure host-side filesystem read \u2014 works with the container stopped. Parallels `list-components`."
10527
+ },
10528
+ args: {
10529
+ name: {
10530
+ type: "positional",
10531
+ description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10532
+ required: true
10533
+ }
10534
+ },
10535
+ async run({ args }) {
10536
+ try {
10537
+ const apps = await listApps(args.name);
10538
+ if (apps.length === 0) {
10539
+ consola18.info(
10540
+ `No apps with a launch config under "${args.name}" (projects/<app>/.monoceros/launch.json).`
10541
+ );
10542
+ process.exit(0);
10543
+ }
10544
+ const fmt = colorsFor(process.stdout);
10545
+ const isTty2 = process.stdout.isTTY ?? false;
10546
+ for (const app of apps) {
10547
+ const cfg = await readLaunchConfig(args.name, app);
10548
+ if (!cfg) continue;
10549
+ if (isTty2) {
10550
+ process.stdout.write(`${fmt.cyan(app)}
10551
+ `);
10552
+ } else {
10553
+ process.stdout.write(`${app}
10554
+ `);
10555
+ }
10556
+ for (const t of cfg.configurations) {
10557
+ const flags = [];
10558
+ if (t.default) flags.push("default");
10559
+ if (typeof t.port === "number") flags.push(`port ${t.port}`);
10560
+ const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
10561
+ if (isTty2) {
10562
+ process.stdout.write(` ${t.name}${suffix}
10563
+ `);
10564
+ } else {
10565
+ process.stdout.write(
10566
+ `${app} ${t.name} ${t.default ? "default" : ""} ${t.port ?? ""}
10567
+ `
10568
+ );
10569
+ }
10570
+ }
10571
+ }
10572
+ process.exit(0);
10573
+ } catch (err) {
10574
+ consola18.error(err instanceof Error ? err.message : String(err));
10575
+ process.exit(1);
10576
+ }
10577
+ }
10578
+ });
10579
+ }
10580
+ });
10581
+
10308
10582
  // src/catalog/expand.ts
10309
10583
  function expandSelectable(catalog) {
10310
10584
  const out = /* @__PURE__ */ new Map();
@@ -10413,8 +10687,8 @@ var init_catalog_json = __esm({
10413
10687
  });
10414
10688
 
10415
10689
  // src/commands/list-components.ts
10416
- import { defineCommand as defineCommand14 } from "citty";
10417
- import { consola as consola18 } from "consola";
10690
+ import { defineCommand as defineCommand15 } from "citty";
10691
+ import { consola as consola19 } from "consola";
10418
10692
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
10419
10693
  var init_list_components = __esm({
10420
10694
  "src/commands/list-components.ts"() {
@@ -10434,7 +10708,7 @@ var init_list_components = __esm({
10434
10708
  "service",
10435
10709
  "feature"
10436
10710
  ];
10437
- listComponentsCommand = defineCommand14({
10711
+ listComponentsCommand = defineCommand15({
10438
10712
  meta: {
10439
10713
  name: "list-components",
10440
10714
  group: "discovery",
@@ -10457,7 +10731,7 @@ var init_list_components = __esm({
10457
10731
  }
10458
10732
  const catalog = expandSelectable(descriptors);
10459
10733
  if (catalog.size === 0) {
10460
- consola18.warn(
10734
+ consola19.warn(
10461
10735
  "No components found. The workbench checkout looks incomplete."
10462
10736
  );
10463
10737
  process.exit(0);
@@ -10508,7 +10782,7 @@ var init_list_components = __esm({
10508
10782
  }
10509
10783
  process.exit(0);
10510
10784
  } catch (err) {
10511
- consola18.error(err instanceof Error ? err.message : String(err));
10785
+ consola19.error(err instanceof Error ? err.message : String(err));
10512
10786
  process.exit(1);
10513
10787
  }
10514
10788
  }
@@ -10518,9 +10792,9 @@ var init_list_components = __esm({
10518
10792
 
10519
10793
  // src/commands/logs.ts
10520
10794
  import { spawn as spawn13 } from "child_process";
10521
- import { existsSync as existsSync17 } from "fs";
10522
- import path26 from "path";
10523
- import { defineCommand as defineCommand15 } from "citty";
10795
+ import path27 from "path";
10796
+ import { defineCommand as defineCommand16 } from "citty";
10797
+ import { consola as consola20 } from "consola";
10524
10798
  function tailLogFile(file, follow) {
10525
10799
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
10526
10800
  return new Promise((resolve, reject) => {
@@ -10534,13 +10808,14 @@ var init_logs = __esm({
10534
10808
  "src/commands/logs.ts"() {
10535
10809
  "use strict";
10536
10810
  init_paths();
10811
+ init_launch_config();
10537
10812
  init_compose();
10538
10813
  init_dispatch();
10539
- logsCommand = defineCommand15({
10814
+ logsCommand = defineCommand16({
10540
10815
  meta: {
10541
10816
  name: "logs",
10542
10817
  group: "run",
10543
- description: "Tail logs from a compose service of the named dev-container, or from a long-running app started inside it (logs/<app>.log). Pass --no-follow for a one-shot dump."
10818
+ description: "Tail logs from a compose service of the named dev-container, or from a long-running app started inside it. Pass --no-follow for a one-shot dump."
10544
10819
  },
10545
10820
  args: {
10546
10821
  name: {
@@ -10548,9 +10823,14 @@ var init_logs = __esm({
10548
10823
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10549
10824
  required: true
10550
10825
  },
10551
- service: {
10826
+ app: {
10827
+ type: "positional",
10828
+ description: "An app (a path under projects/ with .monoceros/launch.json) whose log to tail, or a compose service (e.g. postgres). Defaults to all compose services.",
10829
+ required: false
10830
+ },
10831
+ target: {
10552
10832
  type: "string",
10553
- description: "A compose service (e.g. postgres) or an app whose log is at logs/<service>.log. Defaults to all compose services."
10833
+ description: 'Which launch target of the app to tail (defaults to its "default" target, or its only one).'
10554
10834
  },
10555
10835
  follow: {
10556
10836
  type: "boolean",
@@ -10560,34 +10840,47 @@ var init_logs = __esm({
10560
10840
  }
10561
10841
  },
10562
10842
  run({ args }) {
10563
- const service = typeof args.service === "string" ? args.service : void 0;
10564
- if (service) {
10565
- const logFile = path26.join(containerLogsDir(args.name), `${service}.log`);
10566
- if (existsSync17(logFile)) {
10567
- return dispatch(() => tailLogFile(logFile, args.follow));
10843
+ const app = typeof args.app === "string" ? args.app : void 0;
10844
+ const target = typeof args.target === "string" ? args.target : void 0;
10845
+ return dispatch(async () => {
10846
+ if (app) {
10847
+ const cfg = await readLaunchConfig(args.name, app);
10848
+ if (cfg) {
10849
+ const t = resolveTarget(cfg, target, app);
10850
+ const logFile = path27.join(
10851
+ containerLogsDir(args.name),
10852
+ app,
10853
+ `${t.name}.log`
10854
+ );
10855
+ return tailLogFile(logFile, args.follow);
10856
+ }
10857
+ if (target) {
10858
+ consola20.warn(
10859
+ `No launch config for "${app}" \u2014 ignoring --target and treating "${app}" as a compose service.`
10860
+ );
10861
+ }
10862
+ return runLogs({
10863
+ root: containerDir(args.name),
10864
+ service: app,
10865
+ follow: args.follow
10866
+ });
10568
10867
  }
10569
- }
10570
- return dispatch(
10571
- () => runLogs({
10572
- root: containerDir(args.name),
10573
- ...service ? { service } : {},
10574
- follow: args.follow
10575
- })
10576
- );
10868
+ return runLogs({ root: containerDir(args.name), follow: args.follow });
10869
+ });
10577
10870
  }
10578
10871
  });
10579
10872
  }
10580
10873
  });
10581
10874
 
10582
10875
  // src/commands/open.ts
10583
- import { defineCommand as defineCommand16 } from "citty";
10876
+ import { defineCommand as defineCommand17 } from "citty";
10584
10877
  var openCommand;
10585
10878
  var init_open2 = __esm({
10586
10879
  "src/commands/open.ts"() {
10587
10880
  "use strict";
10588
10881
  init_open();
10589
10882
  init_dispatch();
10590
- openCommand = defineCommand16({
10883
+ openCommand = defineCommand17({
10591
10884
  meta: {
10592
10885
  name: "open",
10593
10886
  group: "run",
@@ -10613,11 +10906,11 @@ var init_open2 = __esm({
10613
10906
  });
10614
10907
 
10615
10908
  // src/commands/port.ts
10616
- import { defineCommand as defineCommand17 } from "citty";
10617
- import { consola as consola19 } from "consola";
10909
+ import { defineCommand as defineCommand18 } from "citty";
10910
+ import { consola as consola21 } from "consola";
10618
10911
  async function runPortListing(opts) {
10619
10912
  const out = opts.out ?? process.stdout;
10620
- const info = opts.info ?? ((m) => consola19.info(m));
10913
+ const info = opts.info ?? ((m) => consola21.info(m));
10621
10914
  const parsed = await readConfig(
10622
10915
  containerConfigPath(opts.name, opts.monocerosHome)
10623
10916
  );
@@ -10675,7 +10968,7 @@ var init_port = __esm({
10675
10968
  init_schema();
10676
10969
  init_dynamic();
10677
10970
  init_format();
10678
- portCommand = defineCommand17({
10971
+ portCommand = defineCommand18({
10679
10972
  meta: {
10680
10973
  name: "port",
10681
10974
  group: "discovery",
@@ -10693,7 +10986,7 @@ var init_port = __esm({
10693
10986
  const code = await runPortListing({ name: args.name });
10694
10987
  process.exit(code);
10695
10988
  } catch (err) {
10696
- consola19.error(err instanceof Error ? err.message : String(err));
10989
+ consola21.error(err instanceof Error ? err.message : String(err));
10697
10990
  process.exit(1);
10698
10991
  }
10699
10992
  }
@@ -10702,15 +10995,15 @@ var init_port = __esm({
10702
10995
  });
10703
10996
 
10704
10997
  // src/commands/remove-apt-packages.ts
10705
- import { defineCommand as defineCommand18 } from "citty";
10706
- import { consola as consola20 } from "consola";
10998
+ import { defineCommand as defineCommand19 } from "citty";
10999
+ import { consola as consola22 } from "consola";
10707
11000
  var removeAptPackagesCommand;
10708
11001
  var init_remove_apt_packages = __esm({
10709
11002
  "src/commands/remove-apt-packages.ts"() {
10710
11003
  "use strict";
10711
11004
  init_inner_args();
10712
11005
  init_modify();
10713
- removeAptPackagesCommand = defineCommand18({
11006
+ removeAptPackagesCommand = defineCommand19({
10714
11007
  meta: {
10715
11008
  name: "remove-apt-packages",
10716
11009
  group: "edit",
@@ -10732,7 +11025,7 @@ var init_remove_apt_packages = __esm({
10732
11025
  async run({ args }) {
10733
11026
  const packages = [...getInnerArgs()];
10734
11027
  if (packages.length === 0) {
10735
- consola20.error(
11028
+ consola22.error(
10736
11029
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
10737
11030
  );
10738
11031
  process.exit(1);
@@ -10745,7 +11038,7 @@ var init_remove_apt_packages = __esm({
10745
11038
  });
10746
11039
  process.exit(result.status === "aborted" ? 1 : 0);
10747
11040
  } catch (err) {
10748
- consola20.error(err instanceof Error ? err.message : String(err));
11041
+ consola22.error(err instanceof Error ? err.message : String(err));
10749
11042
  process.exit(1);
10750
11043
  }
10751
11044
  }
@@ -10754,14 +11047,14 @@ var init_remove_apt_packages = __esm({
10754
11047
  });
10755
11048
 
10756
11049
  // src/commands/remove-feature.ts
10757
- import { defineCommand as defineCommand19 } from "citty";
10758
- import { consola as consola21 } from "consola";
11050
+ import { defineCommand as defineCommand20 } from "citty";
11051
+ import { consola as consola23 } from "consola";
10759
11052
  var removeFeatureCommand;
10760
11053
  var init_remove_feature = __esm({
10761
11054
  "src/commands/remove-feature.ts"() {
10762
11055
  "use strict";
10763
11056
  init_modify();
10764
- removeFeatureCommand = defineCommand19({
11057
+ removeFeatureCommand = defineCommand20({
10765
11058
  meta: {
10766
11059
  name: "remove-feature",
10767
11060
  group: "edit",
@@ -10794,7 +11087,7 @@ var init_remove_feature = __esm({
10794
11087
  });
10795
11088
  process.exit(result.status === "aborted" ? 1 : 0);
10796
11089
  } catch (err) {
10797
- consola21.error(err instanceof Error ? err.message : String(err));
11090
+ consola23.error(err instanceof Error ? err.message : String(err));
10798
11091
  process.exit(1);
10799
11092
  }
10800
11093
  }
@@ -10803,15 +11096,15 @@ var init_remove_feature = __esm({
10803
11096
  });
10804
11097
 
10805
11098
  // src/remove/index.ts
10806
- import { existsSync as existsSync18, promises as fs16 } from "fs";
10807
- import path27 from "path";
10808
- import { consola as consola22 } from "consola";
11099
+ import { existsSync as existsSync17, promises as fs17 } from "fs";
11100
+ import path28 from "path";
11101
+ import { consola as consola24 } from "consola";
10809
11102
  async function runRemove(opts) {
10810
11103
  const home = opts.monocerosHome ?? monocerosHome();
10811
11104
  const logger = opts.logger ?? {
10812
- info: (msg) => consola22.info(msg),
10813
- success: (msg) => consola22.success(msg),
10814
- warn: (msg) => consola22.warn(msg)
11105
+ info: (msg) => consola24.info(msg),
11106
+ success: (msg) => consola24.success(msg),
11107
+ warn: (msg) => consola24.warn(msg)
10815
11108
  };
10816
11109
  if (!REGEX.solutionName.test(opts.name)) {
10817
11110
  throw new Error(
@@ -10821,9 +11114,9 @@ async function runRemove(opts) {
10821
11114
  const ymlPath = containerConfigPath(opts.name, home);
10822
11115
  const envPath = containerEnvPath(opts.name, home);
10823
11116
  const containerPath = containerDir(opts.name, home);
10824
- const hasYml = existsSync18(ymlPath);
10825
- const hasEnv = existsSync18(envPath);
10826
- const hasContainer = existsSync18(containerPath);
11117
+ const hasYml = existsSync17(ymlPath);
11118
+ const hasEnv = existsSync17(envPath);
11119
+ const hasContainer = existsSync17(containerPath);
10827
11120
  if (!hasYml && !hasContainer) {
10828
11121
  throw new Error(
10829
11122
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -10849,16 +11142,16 @@ async function runRemove(opts) {
10849
11142
  let backupPath = null;
10850
11143
  if (!opts.noBackup && (hasYml || hasContainer)) {
10851
11144
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10852
- backupPath = path27.join(home, "container-backups", `${opts.name}-${ts}`);
10853
- await fs16.mkdir(backupPath, { recursive: true });
11145
+ backupPath = path28.join(home, "container-backups", `${opts.name}-${ts}`);
11146
+ await fs17.mkdir(backupPath, { recursive: true });
10854
11147
  if (hasYml) {
10855
- await fs16.copyFile(ymlPath, path27.join(backupPath, `${opts.name}.yml`));
11148
+ await fs17.copyFile(ymlPath, path28.join(backupPath, `${opts.name}.yml`));
10856
11149
  }
10857
11150
  if (hasEnv) {
10858
- await fs16.copyFile(envPath, path27.join(backupPath, `${opts.name}.env`));
11151
+ await fs17.copyFile(envPath, path28.join(backupPath, `${opts.name}.env`));
10859
11152
  }
10860
11153
  if (hasContainer) {
10861
- await fs16.cp(containerPath, path27.join(backupPath, "container"), {
11154
+ await fs17.cp(containerPath, path28.join(backupPath, "container"), {
10862
11155
  recursive: true
10863
11156
  });
10864
11157
  }
@@ -10868,14 +11161,14 @@ async function runRemove(opts) {
10868
11161
  await stopBridgeDaemon(containerPath);
10869
11162
  }
10870
11163
  if (hasYml) {
10871
- await fs16.rm(ymlPath, { force: true });
11164
+ await fs17.rm(ymlPath, { force: true });
10872
11165
  }
10873
11166
  if (hasEnv) {
10874
- await fs16.rm(envPath, { force: true });
11167
+ await fs17.rm(envPath, { force: true });
10875
11168
  }
10876
11169
  if (hasContainer) {
10877
11170
  try {
10878
- await fs16.rm(containerPath, { recursive: true, force: true });
11171
+ await fs17.rm(containerPath, { recursive: true, force: true });
10879
11172
  } catch (err) {
10880
11173
  const code = err.code;
10881
11174
  if (code !== "EACCES" && code !== "EPERM") {
@@ -10897,7 +11190,7 @@ async function runRemove(opts) {
10897
11190
  "-delete"
10898
11191
  ]);
10899
11192
  try {
10900
- await fs16.rm(containerPath, { recursive: true, force: true });
11193
+ await fs17.rm(containerPath, { recursive: true, force: true });
10901
11194
  } catch (err2) {
10902
11195
  const code2 = err2.code;
10903
11196
  throw new Error(
@@ -10961,15 +11254,15 @@ var init_remove = __esm({
10961
11254
  });
10962
11255
 
10963
11256
  // src/commands/remove.ts
10964
- import { defineCommand as defineCommand20 } from "citty";
10965
- import { consola as consola23 } from "consola";
11257
+ import { defineCommand as defineCommand21 } from "citty";
11258
+ import { consola as consola25 } from "consola";
10966
11259
  import { createInterface } from "readline/promises";
10967
11260
  var removeCommand;
10968
11261
  var init_remove2 = __esm({
10969
11262
  "src/commands/remove.ts"() {
10970
11263
  "use strict";
10971
11264
  init_remove();
10972
- removeCommand = defineCommand20({
11265
+ removeCommand = defineCommand21({
10973
11266
  meta: {
10974
11267
  name: "remove",
10975
11268
  group: "lifecycle",
@@ -11006,7 +11299,7 @@ var init_remove2 = __esm({
11006
11299
  const skipPrompt = args.yes === true;
11007
11300
  if (!skipPrompt) {
11008
11301
  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.`;
11009
- consola23.warn(warning);
11302
+ consola25.warn(warning);
11010
11303
  const rl = createInterface({
11011
11304
  input: process.stdin,
11012
11305
  output: process.stdout
@@ -11014,7 +11307,7 @@ var init_remove2 = __esm({
11014
11307
  const answer = await rl.question("Continue? [y/N] ");
11015
11308
  rl.close();
11016
11309
  if (!/^y(es)?$/i.test(answer.trim())) {
11017
- consola23.info("Aborted. Nothing changed.");
11310
+ consola25.info("Aborted. Nothing changed.");
11018
11311
  process.exit(0);
11019
11312
  }
11020
11313
  }
@@ -11023,7 +11316,7 @@ var init_remove2 = __esm({
11023
11316
  ...noBackup ? { noBackup: true } : {}
11024
11317
  });
11025
11318
  } catch (err) {
11026
- consola23.error(err instanceof Error ? err.message : String(err));
11319
+ consola25.error(err instanceof Error ? err.message : String(err));
11027
11320
  process.exit(1);
11028
11321
  }
11029
11322
  }
@@ -11032,24 +11325,24 @@ var init_remove2 = __esm({
11032
11325
  });
11033
11326
 
11034
11327
  // src/restore/index.ts
11035
- import { existsSync as existsSync19, promises as fs17 } from "fs";
11036
- import path28 from "path";
11037
- import { consola as consola24 } from "consola";
11328
+ import { existsSync as existsSync18, promises as fs18 } from "fs";
11329
+ import path29 from "path";
11330
+ import { consola as consola26 } from "consola";
11038
11331
  async function runRestore(opts) {
11039
11332
  const home = opts.monocerosHome ?? monocerosHome();
11040
11333
  const logger = opts.logger ?? {
11041
- info: (msg) => consola24.info(msg),
11042
- success: (msg) => consola24.success(msg)
11334
+ info: (msg) => consola26.info(msg),
11335
+ success: (msg) => consola26.success(msg)
11043
11336
  };
11044
- const backup = path28.resolve(opts.backupPath);
11045
- if (!existsSync19(backup)) {
11337
+ const backup = path29.resolve(opts.backupPath);
11338
+ if (!existsSync18(backup)) {
11046
11339
  throw new Error(`Backup not found: ${backup}.`);
11047
11340
  }
11048
- const stat = await fs17.stat(backup);
11341
+ const stat = await fs18.stat(backup);
11049
11342
  if (!stat.isDirectory()) {
11050
11343
  throw new Error(`Backup path is not a directory: ${backup}.`);
11051
11344
  }
11052
- const entries = await fs17.readdir(backup);
11345
+ const entries = await fs18.readdir(backup);
11053
11346
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
11054
11347
  if (ymlFiles.length === 0) {
11055
11348
  throw new Error(
@@ -11063,29 +11356,29 @@ async function runRestore(opts) {
11063
11356
  }
11064
11357
  const ymlFile = ymlFiles[0];
11065
11358
  const name = ymlFile.replace(/\.yml$/, "");
11066
- const containerInBackup = path28.join(backup, "container");
11067
- const hasContainer = existsSync19(containerInBackup);
11068
- const envInBackup = path28.join(backup, `${name}.env`);
11069
- const hasEnv = existsSync19(envInBackup);
11359
+ const containerInBackup = path29.join(backup, "container");
11360
+ const hasContainer = existsSync18(containerInBackup);
11361
+ const envInBackup = path29.join(backup, `${name}.env`);
11362
+ const hasEnv = existsSync18(envInBackup);
11070
11363
  const destYml = containerConfigPath(name, home);
11071
11364
  const destContainer = containerDir(name, home);
11072
- if (existsSync19(destYml)) {
11365
+ if (existsSync18(destYml)) {
11073
11366
  throw new Error(
11074
11367
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
11075
11368
  );
11076
11369
  }
11077
- if (hasContainer && existsSync19(destContainer)) {
11370
+ if (hasContainer && existsSync18(destContainer)) {
11078
11371
  throw new Error(
11079
11372
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
11080
11373
  );
11081
11374
  }
11082
- await fs17.mkdir(containerConfigsDir(home), { recursive: true });
11083
- await fs17.copyFile(path28.join(backup, ymlFile), destYml);
11375
+ await fs18.mkdir(containerConfigsDir(home), { recursive: true });
11376
+ await fs18.copyFile(path29.join(backup, ymlFile), destYml);
11084
11377
  if (hasEnv) {
11085
- await fs17.copyFile(envInBackup, containerEnvPath(name, home));
11378
+ await fs18.copyFile(envInBackup, containerEnvPath(name, home));
11086
11379
  }
11087
11380
  if (hasContainer) {
11088
- await fs17.cp(containerInBackup, destContainer, { recursive: true });
11381
+ await fs18.cp(containerInBackup, destContainer, { recursive: true });
11089
11382
  }
11090
11383
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
11091
11384
  logger.info(
@@ -11105,14 +11398,14 @@ var init_restore = __esm({
11105
11398
  });
11106
11399
 
11107
11400
  // src/commands/restore.ts
11108
- import { defineCommand as defineCommand21 } from "citty";
11109
- import { consola as consola25 } from "consola";
11401
+ import { defineCommand as defineCommand22 } from "citty";
11402
+ import { consola as consola27 } from "consola";
11110
11403
  var restoreCommand;
11111
11404
  var init_restore2 = __esm({
11112
11405
  "src/commands/restore.ts"() {
11113
11406
  "use strict";
11114
11407
  init_restore();
11115
- restoreCommand = defineCommand21({
11408
+ restoreCommand = defineCommand22({
11116
11409
  meta: {
11117
11410
  name: "restore",
11118
11411
  group: "lifecycle",
@@ -11129,7 +11422,7 @@ var init_restore2 = __esm({
11129
11422
  try {
11130
11423
  await runRestore({ backupPath: args["backup-path"] });
11131
11424
  } catch (err) {
11132
- consola25.error(err instanceof Error ? err.message : String(err));
11425
+ consola27.error(err instanceof Error ? err.message : String(err));
11133
11426
  process.exit(1);
11134
11427
  }
11135
11428
  }
@@ -11138,14 +11431,14 @@ var init_restore2 = __esm({
11138
11431
  });
11139
11432
 
11140
11433
  // src/commands/remove-from-url.ts
11141
- import { defineCommand as defineCommand22 } from "citty";
11142
- import { consola as consola26 } from "consola";
11434
+ import { defineCommand as defineCommand23 } from "citty";
11435
+ import { consola as consola28 } from "consola";
11143
11436
  var removeFromUrlCommand;
11144
11437
  var init_remove_from_url = __esm({
11145
11438
  "src/commands/remove-from-url.ts"() {
11146
11439
  "use strict";
11147
11440
  init_modify();
11148
- removeFromUrlCommand = defineCommand22({
11441
+ removeFromUrlCommand = defineCommand23({
11149
11442
  meta: {
11150
11443
  name: "remove-from-url",
11151
11444
  group: "edit",
@@ -11178,7 +11471,7 @@ var init_remove_from_url = __esm({
11178
11471
  });
11179
11472
  process.exit(result.status === "aborted" ? 1 : 0);
11180
11473
  } catch (err) {
11181
- consola26.error(err instanceof Error ? err.message : String(err));
11474
+ consola28.error(err instanceof Error ? err.message : String(err));
11182
11475
  process.exit(1);
11183
11476
  }
11184
11477
  }
@@ -11187,14 +11480,14 @@ var init_remove_from_url = __esm({
11187
11480
  });
11188
11481
 
11189
11482
  // src/commands/remove-language.ts
11190
- import { defineCommand as defineCommand23 } from "citty";
11191
- import { consola as consola27 } from "consola";
11483
+ import { defineCommand as defineCommand24 } from "citty";
11484
+ import { consola as consola29 } from "consola";
11192
11485
  var removeLanguageCommand;
11193
11486
  var init_remove_language = __esm({
11194
11487
  "src/commands/remove-language.ts"() {
11195
11488
  "use strict";
11196
11489
  init_modify();
11197
- removeLanguageCommand = defineCommand23({
11490
+ removeLanguageCommand = defineCommand24({
11198
11491
  meta: {
11199
11492
  name: "remove-language",
11200
11493
  group: "edit",
@@ -11227,7 +11520,7 @@ var init_remove_language = __esm({
11227
11520
  });
11228
11521
  process.exit(result.status === "aborted" ? 1 : 0);
11229
11522
  } catch (err) {
11230
- consola27.error(err instanceof Error ? err.message : String(err));
11523
+ consola29.error(err instanceof Error ? err.message : String(err));
11231
11524
  process.exit(1);
11232
11525
  }
11233
11526
  }
@@ -11236,8 +11529,8 @@ var init_remove_language = __esm({
11236
11529
  });
11237
11530
 
11238
11531
  // src/commands/remove-port.ts
11239
- import { defineCommand as defineCommand24 } from "citty";
11240
- import { consola as consola28 } from "consola";
11532
+ import { defineCommand as defineCommand25 } from "citty";
11533
+ import { consola as consola30 } from "consola";
11241
11534
  function coerceToken2(raw) {
11242
11535
  const n = Number(raw);
11243
11536
  return Number.isFinite(n) ? n : raw;
@@ -11248,7 +11541,7 @@ var init_remove_port = __esm({
11248
11541
  "use strict";
11249
11542
  init_inner_args();
11250
11543
  init_modify();
11251
- removePortCommand = defineCommand24({
11544
+ removePortCommand = defineCommand25({
11252
11545
  meta: {
11253
11546
  name: "remove-port",
11254
11547
  group: "edit",
@@ -11270,7 +11563,7 @@ var init_remove_port = __esm({
11270
11563
  async run({ args }) {
11271
11564
  const tokens = [...getInnerArgs()];
11272
11565
  if (tokens.length === 0) {
11273
- consola28.error(
11566
+ consola30.error(
11274
11567
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
11275
11568
  );
11276
11569
  process.exit(1);
@@ -11283,7 +11576,7 @@ var init_remove_port = __esm({
11283
11576
  });
11284
11577
  process.exit(result.status === "aborted" ? 1 : 0);
11285
11578
  } catch (err) {
11286
- consola28.error(err instanceof Error ? err.message : String(err));
11579
+ consola30.error(err instanceof Error ? err.message : String(err));
11287
11580
  process.exit(1);
11288
11581
  }
11289
11582
  }
@@ -11292,14 +11585,14 @@ var init_remove_port = __esm({
11292
11585
  });
11293
11586
 
11294
11587
  // src/commands/remove-repo.ts
11295
- import { defineCommand as defineCommand25 } from "citty";
11296
- import { consola as consola29 } from "consola";
11588
+ import { defineCommand as defineCommand26 } from "citty";
11589
+ import { consola as consola31 } from "consola";
11297
11590
  var removeRepoCommand;
11298
11591
  var init_remove_repo = __esm({
11299
11592
  "src/commands/remove-repo.ts"() {
11300
11593
  "use strict";
11301
11594
  init_modify();
11302
- removeRepoCommand = defineCommand25({
11595
+ removeRepoCommand = defineCommand26({
11303
11596
  meta: {
11304
11597
  name: "remove-repo",
11305
11598
  group: "edit",
@@ -11332,7 +11625,7 @@ var init_remove_repo = __esm({
11332
11625
  });
11333
11626
  process.exit(result.status === "aborted" ? 1 : 0);
11334
11627
  } catch (err) {
11335
- consola29.error(err instanceof Error ? err.message : String(err));
11628
+ consola31.error(err instanceof Error ? err.message : String(err));
11336
11629
  process.exit(1);
11337
11630
  }
11338
11631
  }
@@ -11341,14 +11634,14 @@ var init_remove_repo = __esm({
11341
11634
  });
11342
11635
 
11343
11636
  // src/commands/remove-service.ts
11344
- import { defineCommand as defineCommand26 } from "citty";
11345
- import { consola as consola30 } from "consola";
11637
+ import { defineCommand as defineCommand27 } from "citty";
11638
+ import { consola as consola32 } from "consola";
11346
11639
  var removeServiceCommand;
11347
11640
  var init_remove_service = __esm({
11348
11641
  "src/commands/remove-service.ts"() {
11349
11642
  "use strict";
11350
11643
  init_modify();
11351
- removeServiceCommand = defineCommand26({
11644
+ removeServiceCommand = defineCommand27({
11352
11645
  meta: {
11353
11646
  name: "remove-service",
11354
11647
  group: "edit",
@@ -11381,7 +11674,7 @@ var init_remove_service = __esm({
11381
11674
  });
11382
11675
  process.exit(result.status === "aborted" ? 1 : 0);
11383
11676
  } catch (err) {
11384
- consola30.error(err instanceof Error ? err.message : String(err));
11677
+ consola32.error(err instanceof Error ? err.message : String(err));
11385
11678
  process.exit(1);
11386
11679
  }
11387
11680
  }
@@ -11390,16 +11683,16 @@ var init_remove_service = __esm({
11390
11683
  });
11391
11684
 
11392
11685
  // src/devcontainer/claude-trust.ts
11393
- import { existsSync as existsSync20, promises as fsp7 } from "fs";
11394
- import path29 from "path";
11686
+ import { existsSync as existsSync19, promises as fsp7 } from "fs";
11687
+ import path30 from "path";
11395
11688
  function resolveContainerCwd(name, cwd) {
11396
11689
  const workspace = `/workspaces/${name}`;
11397
11690
  if (!cwd) return workspace;
11398
- return path29.posix.isAbsolute(cwd) ? cwd : path29.posix.join(workspace, cwd);
11691
+ return path30.posix.isAbsolute(cwd) ? cwd : path30.posix.join(workspace, cwd);
11399
11692
  }
11400
11693
  async function preApproveClaudeProject(opts) {
11401
- const file = path29.join(opts.root, "home", ".claude.json");
11402
- if (!existsSync20(file)) return;
11694
+ const file = path30.join(opts.root, "home", ".claude.json");
11695
+ if (!existsSync19(file)) return;
11403
11696
  try {
11404
11697
  const raw = await fsp7.readFile(file, "utf8");
11405
11698
  const config = raw.trim() ? JSON.parse(raw) : {};
@@ -11490,8 +11783,8 @@ var init_run = __esm({
11490
11783
  });
11491
11784
 
11492
11785
  // src/commands/run.ts
11493
- import { defineCommand as defineCommand27 } from "citty";
11494
- import { consola as consola31 } from "consola";
11786
+ import { defineCommand as defineCommand28 } from "citty";
11787
+ import { consola as consola33 } from "consola";
11495
11788
  var runCommand;
11496
11789
  var init_run2 = __esm({
11497
11790
  "src/commands/run.ts"() {
@@ -11499,7 +11792,7 @@ var init_run2 = __esm({
11499
11792
  init_paths();
11500
11793
  init_run();
11501
11794
  init_inner_args();
11502
- runCommand = defineCommand27({
11795
+ runCommand = defineCommand28({
11503
11796
  meta: {
11504
11797
  name: "run",
11505
11798
  group: "run",
@@ -11519,7 +11812,7 @@ var init_run2 = __esm({
11519
11812
  async run({ args }) {
11520
11813
  const command = [...getInnerArgs()];
11521
11814
  if (command.length === 0) {
11522
- consola31.error(
11815
+ consola33.error(
11523
11816
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
11524
11817
  );
11525
11818
  process.exit(1);
@@ -11533,7 +11826,7 @@ var init_run2 = __esm({
11533
11826
  });
11534
11827
  process.exit(exitCode);
11535
11828
  } catch (err) {
11536
- consola31.error(err instanceof Error ? err.message : String(err));
11829
+ consola33.error(err instanceof Error ? err.message : String(err));
11537
11830
  process.exit(1);
11538
11831
  }
11539
11832
  }
@@ -11542,15 +11835,15 @@ var init_run2 = __esm({
11542
11835
  });
11543
11836
 
11544
11837
  // src/commands/shell.ts
11545
- import { defineCommand as defineCommand28 } from "citty";
11546
- import { consola as consola32 } from "consola";
11838
+ import { defineCommand as defineCommand29 } from "citty";
11839
+ import { consola as consola34 } from "consola";
11547
11840
  var shellCommand;
11548
11841
  var init_shell2 = __esm({
11549
11842
  "src/commands/shell.ts"() {
11550
11843
  "use strict";
11551
11844
  init_paths();
11552
11845
  init_shell();
11553
- shellCommand = defineCommand28({
11846
+ shellCommand = defineCommand29({
11554
11847
  meta: {
11555
11848
  name: "shell",
11556
11849
  group: "run",
@@ -11571,7 +11864,7 @@ var init_shell2 = __esm({
11571
11864
  });
11572
11865
  process.exit(exitCode);
11573
11866
  } catch (err) {
11574
- consola32.error(err instanceof Error ? err.message : String(err));
11867
+ consola34.error(err instanceof Error ? err.message : String(err));
11575
11868
  process.exit(1);
11576
11869
  }
11577
11870
  }
@@ -11579,9 +11872,94 @@ var init_shell2 = __esm({
11579
11872
  }
11580
11873
  });
11581
11874
 
11875
+ // src/devcontainer/app-control.ts
11876
+ function findRunningContainer(name, opts = {}) {
11877
+ return findRunningContainerByLocalFolder(containerDir(name), {
11878
+ ...opts.docker ? { docker: opts.docker } : {}
11879
+ });
11880
+ }
11881
+ async function runAppCtl(name, ctlArgs2, opts = {}) {
11882
+ const id = await findRunningContainer(name, opts);
11883
+ if (!id) {
11884
+ throw new Error(
11885
+ `Container "${name}" is not running. Run \`monoceros start ${name}\` first.`
11886
+ );
11887
+ }
11888
+ const exec = opts.exec ?? realContainerExec;
11889
+ const result = await exec(id, ["monoceros-ctl", ...ctlArgs2]);
11890
+ return result.exitCode;
11891
+ }
11892
+ function ctlArgs(sub, app, target, extra = []) {
11893
+ return [sub, app, ...target ? ["--target", target] : [], ...extra];
11894
+ }
11895
+ var init_app_control = __esm({
11896
+ "src/devcontainer/app-control.ts"() {
11897
+ "use strict";
11898
+ init_paths();
11899
+ init_locate_running();
11900
+ }
11901
+ });
11902
+
11582
11903
  // src/commands/start.ts
11583
- import { defineCommand as defineCommand29 } from "citty";
11584
- import { consola as consola33 } from "consola";
11904
+ import { defineCommand as defineCommand30 } from "citty";
11905
+ import { consola as consola35 } from "consola";
11906
+ async function bringContainerUp(name, openTool) {
11907
+ {
11908
+ const args = { name, open: openTool };
11909
+ let needsProxy = false;
11910
+ let hostPort = 80;
11911
+ let deferred = [];
11912
+ let runtimeVersion;
11913
+ try {
11914
+ const parsed = await readConfig(containerConfigPath(args.name));
11915
+ runtimeVersion = parsed.config.runtimeVersion;
11916
+ if ((parsed.config.routing?.ports ?? []).length > 0) {
11917
+ needsProxy = true;
11918
+ const global = await readMonocerosConfig();
11919
+ hostPort = proxyHostPort(global);
11920
+ }
11921
+ deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
11922
+ } catch (err) {
11923
+ consola35.warn(
11924
+ `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
11925
+ );
11926
+ }
11927
+ if (needsProxy) {
11928
+ await preflightHostPort(hostPort);
11929
+ await ensureProxy({ hostPort });
11930
+ }
11931
+ const exitCode = await runStart({ root: containerDir(args.name) });
11932
+ if (exitCode === 0 && runtimeSupportsBrowserBridge(runtimeVersion)) {
11933
+ spawnBridgeDaemon(containerDir(args.name));
11934
+ }
11935
+ if (exitCode === 0 && deferred.length > 0) {
11936
+ try {
11937
+ const deferExit = await startDeferredServices({
11938
+ root: containerDir(args.name),
11939
+ services: deferred,
11940
+ logger: consola35
11941
+ });
11942
+ if (deferExit !== 0) {
11943
+ consola35.warn(
11944
+ `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
11945
+ );
11946
+ }
11947
+ } catch (err) {
11948
+ consola35.warn(
11949
+ `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
11950
+ );
11951
+ }
11952
+ }
11953
+ if (args.open && exitCode === 0) {
11954
+ try {
11955
+ await runOpen({ name: args.name, tool: args.open });
11956
+ } catch (err) {
11957
+ consola35.warn(err instanceof Error ? err.message : String(err));
11958
+ }
11959
+ }
11960
+ return exitCode;
11961
+ }
11962
+ }
11585
11963
  var startCommand;
11586
11964
  var init_start = __esm({
11587
11965
  "src/commands/start.ts"() {
@@ -11595,12 +11973,13 @@ var init_start = __esm({
11595
11973
  init_open();
11596
11974
  init_proxy();
11597
11975
  init_port_check();
11976
+ init_app_control();
11598
11977
  init_dispatch();
11599
- startCommand = defineCommand29({
11978
+ startCommand = defineCommand30({
11600
11979
  meta: {
11601
11980
  name: "start",
11602
11981
  group: "run",
11603
- description: "Bring the named dev-container up via `devcontainer up` (workspace + runServices, postCreate, features)."
11982
+ description: "Bring the named dev-container up. With an <app>, start that app inside it (per its projects/<app>/.monoceros/launch.json); the container is brought up first if needed."
11604
11983
  },
11605
11984
  args: {
11606
11985
  name: {
@@ -11608,73 +11987,45 @@ var init_start = __esm({
11608
11987
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
11609
11988
  required: true
11610
11989
  },
11990
+ app: {
11991
+ type: "positional",
11992
+ description: "App to start (a path under projects/ with .monoceros/launch.json). Omit to just bring the container up.",
11993
+ required: false
11994
+ },
11995
+ target: {
11996
+ type: "string",
11997
+ description: `Which launch target to start (defaults to the app's "default" target, or its only one).`
11998
+ },
11611
11999
  open: {
11612
12000
  type: "string",
11613
12001
  description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
11614
12002
  }
11615
12003
  },
11616
12004
  run({ args }) {
11617
- return dispatch(async () => {
11618
- let needsProxy = false;
11619
- let hostPort = 80;
11620
- let deferred = [];
11621
- let runtimeVersion;
11622
- try {
11623
- const parsed = await readConfig(containerConfigPath(args.name));
11624
- runtimeVersion = parsed.config.runtimeVersion;
11625
- if ((parsed.config.routing?.ports ?? []).length > 0) {
11626
- needsProxy = true;
11627
- const global = await readMonocerosConfig();
11628
- hostPort = proxyHostPort(global);
12005
+ if (typeof args.app === "string" && args.app.length > 0) {
12006
+ const app = args.app;
12007
+ const target = typeof args.target === "string" ? args.target : void 0;
12008
+ return dispatch(async () => {
12009
+ if (!await findRunningContainer(args.name)) {
12010
+ const up = await bringContainerUp(args.name, void 0);
12011
+ if (up !== 0) return up;
11629
12012
  }
11630
- deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
11631
- } catch (err) {
11632
- consola33.warn(
11633
- `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
11634
- );
11635
- }
11636
- if (needsProxy) {
11637
- await preflightHostPort(hostPort);
11638
- await ensureProxy({ hostPort });
11639
- }
11640
- const exitCode = await runStart({ root: containerDir(args.name) });
11641
- if (exitCode === 0 && runtimeSupportsBrowserBridge(runtimeVersion)) {
11642
- spawnBridgeDaemon(containerDir(args.name));
11643
- }
11644
- if (exitCode === 0 && deferred.length > 0) {
11645
- try {
11646
- const deferExit = await startDeferredServices({
11647
- root: containerDir(args.name),
11648
- services: deferred,
11649
- logger: consola33
11650
- });
11651
- if (deferExit !== 0) {
11652
- consola33.warn(
11653
- `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
11654
- );
11655
- }
11656
- } catch (err) {
11657
- consola33.warn(
11658
- `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
11659
- );
11660
- }
11661
- }
11662
- if (args.open && exitCode === 0) {
11663
- try {
11664
- await runOpen({ name: args.name, tool: args.open });
11665
- } catch (err) {
11666
- consola33.warn(err instanceof Error ? err.message : String(err));
11667
- }
11668
- }
11669
- return exitCode;
11670
- });
12013
+ return runAppCtl(args.name, ctlArgs("start", app, target));
12014
+ });
12015
+ }
12016
+ return dispatch(
12017
+ () => bringContainerUp(
12018
+ args.name,
12019
+ typeof args.open === "string" ? args.open : void 0
12020
+ )
12021
+ );
11671
12022
  }
11672
12023
  });
11673
12024
  }
11674
12025
  });
11675
12026
 
11676
12027
  // src/commands/status.ts
11677
- import { defineCommand as defineCommand30 } from "citty";
12028
+ import { defineCommand as defineCommand31 } from "citty";
11678
12029
  var statusCommand;
11679
12030
  var init_status = __esm({
11680
12031
  "src/commands/status.ts"() {
@@ -11682,7 +12033,7 @@ var init_status = __esm({
11682
12033
  init_paths();
11683
12034
  init_compose();
11684
12035
  init_dispatch();
11685
- statusCommand = defineCommand30({
12036
+ statusCommand = defineCommand31({
11686
12037
  meta: {
11687
12038
  name: "status",
11688
12039
  group: "run",
@@ -11712,8 +12063,8 @@ var init_status = __esm({
11712
12063
  });
11713
12064
 
11714
12065
  // src/commands/stop.ts
11715
- import { defineCommand as defineCommand31 } from "citty";
11716
- import { consola as consola34 } from "consola";
12066
+ import { defineCommand as defineCommand32 } from "citty";
12067
+ import { consola as consola36 } from "consola";
11717
12068
  var stopCommand;
11718
12069
  var init_stop = __esm({
11719
12070
  "src/commands/stop.ts"() {
@@ -11721,12 +12072,13 @@ var init_stop = __esm({
11721
12072
  init_paths();
11722
12073
  init_compose();
11723
12074
  init_proxy();
12075
+ init_app_control();
11724
12076
  init_dispatch();
11725
- stopCommand = defineCommand31({
12077
+ stopCommand = defineCommand32({
11726
12078
  meta: {
11727
12079
  name: "stop",
11728
12080
  group: "run",
11729
- description: "Stop the compose services for the named dev-container. Volumes are preserved."
12081
+ description: "Stop the compose services for the named dev-container. With an <app>, stop that long-running app inside it instead (kills its process group)."
11730
12082
  },
11731
12083
  args: {
11732
12084
  name: {
@@ -11734,12 +12086,26 @@ var init_stop = __esm({
11734
12086
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
11735
12087
  required: true
11736
12088
  },
12089
+ app: {
12090
+ type: "positional",
12091
+ description: "App to stop (a path under projects/ with .monoceros/launch.json). Omit to stop the container.",
12092
+ required: false
12093
+ },
12094
+ target: {
12095
+ type: "string",
12096
+ description: `Which launch target to stop (defaults to the app's "default" target, or its only one).`
12097
+ },
11737
12098
  service: {
11738
12099
  type: "string",
11739
12100
  description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
11740
12101
  }
11741
12102
  },
11742
12103
  run({ args }) {
12104
+ if (typeof args.app === "string" && args.app.length > 0) {
12105
+ const app = args.app;
12106
+ const target = typeof args.target === "string" ? args.target : void 0;
12107
+ return dispatch(() => runAppCtl(args.name, ctlArgs("stop", app, target)));
12108
+ }
11743
12109
  return dispatch(async () => {
11744
12110
  const exit = await runStop({
11745
12111
  root: containerDir(args.name),
@@ -11747,10 +12113,10 @@ var init_stop = __esm({
11747
12113
  });
11748
12114
  try {
11749
12115
  await maybeStopProxy({
11750
- logger: { info: (msg) => consola34.info(msg) }
12116
+ logger: { info: (msg) => consola36.info(msg) }
11751
12117
  });
11752
12118
  } catch (err) {
11753
- consola34.warn(
12119
+ consola36.warn(
11754
12120
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
11755
12121
  );
11756
12122
  }
@@ -11762,11 +12128,11 @@ var init_stop = __esm({
11762
12128
  });
11763
12129
 
11764
12130
  // src/tunnel/resolve.ts
11765
- import { existsSync as existsSync21 } from "fs";
11766
- import path30 from "path";
12131
+ import { existsSync as existsSync20 } from "fs";
12132
+ import path31 from "path";
11767
12133
  async function resolveTunnelTarget(opts) {
11768
12134
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
11769
- if (!existsSync21(ymlPath)) {
12135
+ if (!existsSync20(ymlPath)) {
11770
12136
  throw new Error(
11771
12137
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
11772
12138
  );
@@ -11774,13 +12140,13 @@ async function resolveTunnelTarget(opts) {
11774
12140
  const parsed = await readConfig(ymlPath);
11775
12141
  const config = parsed.config;
11776
12142
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
11777
- if (!existsSync21(containerRoot)) {
12143
+ if (!existsSync20(containerRoot)) {
11778
12144
  throw new Error(
11779
12145
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
11780
12146
  );
11781
12147
  }
11782
- const composePath = path30.join(containerRoot, ".devcontainer", "compose.yaml");
11783
- const isCompose = existsSync21(composePath);
12148
+ const composePath = path31.join(containerRoot, ".devcontainer", "compose.yaml");
12149
+ const isCompose = existsSync20(composePath);
11784
12150
  const parsedTarget = parseTargetArg(opts.target, config);
11785
12151
  const docker = opts.docker ?? defaultDockerExec;
11786
12152
  if (isCompose) {
@@ -12016,7 +12382,7 @@ var init_port_check2 = __esm({
12016
12382
 
12017
12383
  // src/tunnel/run.ts
12018
12384
  import { spawn as spawn14 } from "child_process";
12019
- import { consola as consola35 } from "consola";
12385
+ import { consola as consola37 } from "consola";
12020
12386
  function signalNumber(signal) {
12021
12387
  switch (signal) {
12022
12388
  case "SIGINT":
@@ -12029,8 +12395,8 @@ function signalNumber(signal) {
12029
12395
  }
12030
12396
  async function runTunnel(opts) {
12031
12397
  const log = opts.logger ?? {
12032
- info: (m) => consola35.info(m),
12033
- warn: (m) => consola35.warn(m)
12398
+ info: (m) => consola37.info(m),
12399
+ warn: (m) => consola37.warn(m)
12034
12400
  };
12035
12401
  const resolve = opts.resolve ?? resolveTunnelTarget;
12036
12402
  const resolveArgs3 = {
@@ -12136,8 +12502,8 @@ var init_run3 = __esm({
12136
12502
  });
12137
12503
 
12138
12504
  // src/commands/tunnel.ts
12139
- import { defineCommand as defineCommand32 } from "citty";
12140
- import { consola as consola36 } from "consola";
12505
+ import { defineCommand as defineCommand33 } from "citty";
12506
+ import { consola as consola38 } from "consola";
12141
12507
  function parseLocalPort(raw) {
12142
12508
  if (raw === void 0) return void 0;
12143
12509
  const n = Number(raw);
@@ -12153,7 +12519,7 @@ var init_tunnel = __esm({
12153
12519
  "src/commands/tunnel.ts"() {
12154
12520
  "use strict";
12155
12521
  init_run3();
12156
- tunnelCommand = defineCommand32({
12522
+ tunnelCommand = defineCommand33({
12157
12523
  meta: {
12158
12524
  name: "tunnel",
12159
12525
  group: "discovery",
@@ -12190,7 +12556,7 @@ var init_tunnel = __esm({
12190
12556
  });
12191
12557
  process.exit(exitCode);
12192
12558
  } catch (err) {
12193
- consola36.error(err instanceof Error ? err.message : String(err));
12559
+ consola38.error(err instanceof Error ? err.message : String(err));
12194
12560
  process.exit(1);
12195
12561
  }
12196
12562
  }
@@ -12260,8 +12626,8 @@ var init_prune = __esm({
12260
12626
  });
12261
12627
 
12262
12628
  // src/upgrade/index.ts
12263
- import { existsSync as existsSync22, promises as fs18 } from "fs";
12264
- import { consola as consola37 } from "consola";
12629
+ import { existsSync as existsSync21, promises as fs19 } from "fs";
12630
+ import { consola as consola39 } from "consola";
12265
12631
  async function fetchRuntimeVersions() {
12266
12632
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
12267
12633
  const tokenRes = await fetch(tokenUrl);
@@ -12299,7 +12665,7 @@ ${yml}`;
12299
12665
  }
12300
12666
  async function runUpgrade(opts) {
12301
12667
  const home = opts.monocerosHome ?? monocerosHome();
12302
- const logger = opts.logger ?? consola37;
12668
+ const logger = opts.logger ?? consola39;
12303
12669
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
12304
12670
  if (opts.list) {
12305
12671
  const versions = await fetchVersions();
@@ -12325,7 +12691,7 @@ async function runUpgrade(opts) {
12325
12691
  );
12326
12692
  }
12327
12693
  }
12328
- if (opts.name && !existsSync22(containerConfigPath(opts.name, home))) {
12694
+ if (opts.name && !existsSync21(containerConfigPath(opts.name, home))) {
12329
12695
  throw new Error(
12330
12696
  `No such config: ${containerConfigPath(opts.name, home)}. Run \`monoceros init <template> ${opts.name}\` first.`
12331
12697
  );
@@ -12376,11 +12742,11 @@ async function runUpgrade(opts) {
12376
12742
  let bumped = 0;
12377
12743
  for (const name of targets) {
12378
12744
  const ymlPath = containerConfigPath(name, home);
12379
- if (!existsSync22(ymlPath)) continue;
12380
- const raw = await fs18.readFile(ymlPath, "utf8");
12745
+ if (!existsSync21(ymlPath)) continue;
12746
+ const raw = await fs19.readFile(ymlPath, "utf8");
12381
12747
  const updated = setRuntimeVersion(raw, pinVersion);
12382
12748
  if (updated !== raw) {
12383
- await fs18.writeFile(ymlPath, updated);
12749
+ await fs19.writeFile(ymlPath, updated);
12384
12750
  bumped += 1;
12385
12751
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
12386
12752
  }
@@ -12422,7 +12788,7 @@ function formatPruneLine(prune) {
12422
12788
  }
12423
12789
  async function listContainerNames2(home) {
12424
12790
  try {
12425
- const entries = await fs18.readdir(containerConfigsDir(home));
12791
+ const entries = await fs19.readdir(containerConfigsDir(home));
12426
12792
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
12427
12793
  } catch {
12428
12794
  return [];
@@ -12445,7 +12811,7 @@ var init_upgrade = __esm({
12445
12811
  });
12446
12812
 
12447
12813
  // src/commands/upgrade.ts
12448
- import { defineCommand as defineCommand33 } from "citty";
12814
+ import { defineCommand as defineCommand34 } from "citty";
12449
12815
  var upgradeCommand;
12450
12816
  var init_upgrade2 = __esm({
12451
12817
  "src/commands/upgrade.ts"() {
@@ -12453,7 +12819,7 @@ var init_upgrade2 = __esm({
12453
12819
  init_upgrade();
12454
12820
  init_version();
12455
12821
  init_dispatch();
12456
- upgradeCommand = defineCommand33({
12822
+ upgradeCommand = defineCommand34({
12457
12823
  meta: {
12458
12824
  name: "upgrade",
12459
12825
  group: "lifecycle",
@@ -12495,7 +12861,7 @@ var main_exports = {};
12495
12861
  __export(main_exports, {
12496
12862
  main: () => main
12497
12863
  });
12498
- import { defineCommand as defineCommand34 } from "citty";
12864
+ import { defineCommand as defineCommand35 } from "citty";
12499
12865
  var main;
12500
12866
  var init_main = __esm({
12501
12867
  "src/main.ts"() {
@@ -12513,6 +12879,7 @@ var init_main = __esm({
12513
12879
  init_complete();
12514
12880
  init_update_check();
12515
12881
  init_init2();
12882
+ init_list_apps();
12516
12883
  init_list_components();
12517
12884
  init_logs();
12518
12885
  init_open2();
@@ -12534,7 +12901,7 @@ var init_main = __esm({
12534
12901
  init_tunnel();
12535
12902
  init_upgrade2();
12536
12903
  init_version();
12537
- main = defineCommand34({
12904
+ main = defineCommand35({
12538
12905
  meta: {
12539
12906
  name: "monoceros",
12540
12907
  version: CLI_VERSION,
@@ -12542,6 +12909,7 @@ var init_main = __esm({
12542
12909
  },
12543
12910
  subCommands: {
12544
12911
  init: initCommand,
12912
+ "list-apps": listAppsCommand,
12545
12913
  "list-components": listComponentsCommand,
12546
12914
  shell: shellCommand,
12547
12915
  open: openCommand,
@@ -12833,25 +13201,25 @@ function detectHelpRequest(argv, main2) {
12833
13201
  const separatorIdx = argv.indexOf("--");
12834
13202
  if (helpIdx === -1) return null;
12835
13203
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
12836
- const path31 = [];
13204
+ const path32 = [];
12837
13205
  const tokens = argv.slice(
12838
13206
  0,
12839
13207
  separatorIdx === -1 ? argv.length : separatorIdx
12840
13208
  );
12841
13209
  let cursor = main2;
12842
13210
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
12843
- path31.push(mainName);
13211
+ path32.push(mainName);
12844
13212
  for (const tok of tokens) {
12845
13213
  if (tok.startsWith("-")) continue;
12846
13214
  const subs = cursor.subCommands ?? {};
12847
13215
  if (tok in subs) {
12848
13216
  cursor = subs[tok];
12849
- path31.push(tok);
13217
+ path32.push(tok);
12850
13218
  continue;
12851
13219
  }
12852
13220
  break;
12853
13221
  }
12854
- return { path: path31, cmd: cursor };
13222
+ return { path: path32, cmd: cursor };
12855
13223
  }
12856
13224
  async function maybeRenderHelp(argv, main2) {
12857
13225
  const hit = detectHelpRequest(argv, main2);