@getmonoceros/workbench 1.33.7 → 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
  ),
@@ -3668,7 +3668,11 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3668
3668
  workspaceMount: `source=\${localWorkspaceFolder},target=/workspaces/${opts.name},type=bind,consistency=cached`,
3669
3669
  workspaceFolder: `/workspaces/${opts.name}`
3670
3670
  };
3671
- const runArgs = ["--cap-add=NET_ADMIN", `--name=monoceros-${opts.name}`];
3671
+ const runArgs = [
3672
+ "--cap-add=NET_ADMIN",
3673
+ `--name=monoceros-${opts.name}`,
3674
+ `--restart=${GROUP_RESTART_POLICY}`
3675
+ ];
3672
3676
  if (ports.length > 0) {
3673
3677
  runArgs.push("--network=monoceros-proxy");
3674
3678
  runArgs.push(`--network-alias=${opts.name}`);
@@ -3717,6 +3721,7 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3717
3721
  lines.push(` image: ${resolveRuntimeImage(opts.runtimeVersion)}`);
3718
3722
  lines.push(` container_name: monoceros-${opts.name}`);
3719
3723
  lines.push(" command: 'sleep infinity'");
3724
+ lines.push(` restart: ${GROUP_RESTART_POLICY}`);
3720
3725
  lines.push(" cap_add:");
3721
3726
  lines.push(" - NET_ADMIN");
3722
3727
  if (sshBridgePort !== null) {
@@ -3767,9 +3772,7 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3767
3772
  if (svc.user !== void 0) {
3768
3773
  lines.push(` user: ${composeScalar(svc.user)}`);
3769
3774
  }
3770
- if (svc.restart) {
3771
- lines.push(` restart: ${svc.restart}`);
3772
- }
3775
+ lines.push(` restart: ${svc.restart ?? GROUP_RESTART_POLICY}`);
3773
3776
  if (svc.command !== void 0) {
3774
3777
  lines.push(` command: ${composeScalar(svc.command)}`);
3775
3778
  }
@@ -3929,6 +3932,10 @@ function buildPostCreateScript(opts) {
3929
3932
  "# identity values in.",
3930
3933
  `git config --global include.path "/workspaces/${opts.name}/.monoceros/gitconfig"`,
3931
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
+ "",
3932
3939
  "# Per-feature post-create hooks. Each Monoceros-curated feature",
3933
3940
  "# may drop a script into /usr/local/share/monoceros/post-create.d/",
3934
3941
  "# during its install.sh \u2014 typical job is a non-interactive login",
@@ -4069,7 +4076,11 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4069
4076
  }
4070
4077
  await fs8.writeFile(
4071
4078
  path11.join(monocerosDir, ".gitignore"),
4072
- "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"
4073
4084
  );
4074
4085
  const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
4075
4086
  await writeIfChanged(
@@ -4149,7 +4160,7 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4149
4160
  JSON.stringify(mergeVscodeSettings(existingSettings), null, 2) + "\n"
4150
4161
  );
4151
4162
  }
4152
- var APT_PACKAGE_NAME_RE2, FEATURE_REF_RE2, INSTALL_URL_RE2, REPO_URL_RE2, REPO_PATH_RE2, HOME_SUBPATH_RE, WORKSPACE_ROOT_LABEL, REPO_HOST_EXTENSIONS, ROOT_DENOISE_EXCLUDES;
4163
+ var APT_PACKAGE_NAME_RE2, FEATURE_REF_RE2, INSTALL_URL_RE2, REPO_URL_RE2, REPO_PATH_RE2, GROUP_RESTART_POLICY, HOME_SUBPATH_RE, WORKSPACE_ROOT_LABEL, REPO_HOST_EXTENSIONS, ROOT_DENOISE_EXCLUDES;
4153
4164
  var init_scaffold = __esm({
4154
4165
  "src/create/scaffold.ts"() {
4155
4166
  "use strict";
@@ -4166,6 +4177,7 @@ var init_scaffold = __esm({
4166
4177
  INSTALL_URL_RE2 = /^https:\/\/[A-Za-z0-9.\-_~/:?#[\]@!&'()*+,;=%]+$/;
4167
4178
  REPO_URL_RE2 = /^[A-Za-z0-9@:/+_~.#=&?-]+$/;
4168
4179
  REPO_PATH_RE2 = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
4180
+ GROUP_RESTART_POLICY = "unless-stopped";
4169
4181
  HOME_SUBPATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
4170
4182
  WORKSPACE_ROOT_LABEL = "\u{1F984} Monoceros";
4171
4183
  REPO_HOST_EXTENSIONS = {
@@ -4626,8 +4638,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
4626
4638
  if (!isMap2(item)) return false;
4627
4639
  const url = item.get("url");
4628
4640
  if (url === urlOrPath) return true;
4629
- const path31 = item.get("path");
4630
- 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;
4631
4643
  return effectivePath === urlOrPath;
4632
4644
  });
4633
4645
  if (idx < 0) return false;
@@ -4750,7 +4762,7 @@ async function runAddRepo(input) {
4750
4762
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
4751
4763
  );
4752
4764
  }
4753
- const path31 = (input.path ?? deriveRepoName(url)).trim();
4765
+ const path32 = (input.path ?? deriveRepoName(url)).trim();
4754
4766
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
4755
4767
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
4756
4768
  if (hasName !== hasEmail) {
@@ -4787,7 +4799,7 @@ async function runAddRepo(input) {
4787
4799
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
4788
4800
  const entry2 = {
4789
4801
  url,
4790
- path: path31,
4802
+ path: path32,
4791
4803
  ...hasName && hasEmail ? {
4792
4804
  gitUser: {
4793
4805
  name: input.gitName.trim(),
@@ -6607,10 +6619,14 @@ function spawnBridgeDaemon(root) {
6607
6619
  const self = process.argv[1];
6608
6620
  if (!self) return;
6609
6621
  mkdirSync(relayDir(root), { recursive: true });
6610
- const child = spawn8(process.execPath, [self, "__bridge", root], {
6611
- detached: true,
6612
- stdio: "ignore"
6613
- });
6622
+ const child = spawn8(
6623
+ process.execPath,
6624
+ [...process.execArgv, self, "__bridge", root],
6625
+ {
6626
+ detached: true,
6627
+ stdio: "ignore"
6628
+ }
6629
+ );
6614
6630
  if (typeof child.pid === "number") {
6615
6631
  try {
6616
6632
  writeFileSync(bridgePidFile(root), String(child.pid));
@@ -7207,46 +7223,58 @@ function generateAgentsMd(input) {
7207
7223
  lines.push("");
7208
7224
  lines.push(
7209
7225
  "When you build something that serves on a port (a web app, an API),",
7210
- "start it as a **detached** background process so it keeps running after",
7211
- "this session ends. A plain `npm start` (or any foreground start) dies",
7212
- "the moment the user exits you or closes the terminal \u2014 and then",
7213
- `\`${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.`
7214
7229
  );
7215
7230
  lines.push("");
7216
7231
  lines.push(
7217
- "Launch it in a new session with `setsid`, using the project's own",
7218
- "start command, and record the process-group PID + log under the",
7219
- "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:"
7220
7237
  );
7221
7238
  lines.push("");
7222
- lines.push("```");
7239
+ lines.push("```json");
7240
+ lines.push("{");
7241
+ lines.push(' "configurations": [');
7223
7242
  lines.push(
7224
- `setsid sh -c 'echo $$ >/workspaces/${input.containerName}/logs/<app>.pid; \\`
7225
- );
7226
- lines.push(
7227
- ` 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 }`
7228
7244
  );
7245
+ lines.push(" ]");
7246
+ lines.push("}");
7229
7247
  lines.push("```");
7230
7248
  lines.push("");
7231
7249
  lines.push(
7232
- "Use whatever start command the project actually uses (`npm start`,",
7233
- "`./mvnw spring-boot:run`, `python app.py`, `go run .`, \u2026) \u2014 do not force",
7234
- "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."
7235
7254
  );
7236
7255
  lines.push("");
7237
- lines.push("To stop it, kill the whole process group (also stops children");
7238
- lines.push("like node under npm or java under maven):");
7256
+ lines.push("Start it, stop it, tail its log:");
7239
7257
  lines.push("");
7240
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("");
7241
7264
  lines.push(
7242
- `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>\`.`
7243
7271
  );
7244
- lines.push("```");
7245
7272
  lines.push("");
7246
7273
  lines.push(
7247
- `The user can follow the output with \`monoceros logs ${input.containerName} <app>\``,
7248
- "on the host. The server must listen on `0.0.0.0` (not `127.0.0.1`) on",
7249
- "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.`
7250
7278
  );
7251
7279
  lines.push("");
7252
7280
  }
@@ -8657,7 +8685,7 @@ var CLI_VERSION;
8657
8685
  var init_version = __esm({
8658
8686
  "src/version.ts"() {
8659
8687
  "use strict";
8660
- CLI_VERSION = true ? "1.33.7" : "dev";
8688
+ CLI_VERSION = true ? "1.34.0" : "dev";
8661
8689
  }
8662
8690
  });
8663
8691
 
@@ -8890,9 +8918,144 @@ var init_bridge = __esm({
8890
8918
  }
8891
8919
  });
8892
8920
 
8893
- // src/completion/resolve.ts
8894
- import { existsSync as existsSync15, promises as fs14 } from "fs";
8921
+ // src/config/launch-config.ts
8922
+ import { promises as fs14 } from "fs";
8895
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";
8896
9059
  async function resolveCompletions(line, point, opts = {}) {
8897
9060
  const { prev, current } = parseCompletionLine(line, point);
8898
9061
  const ctx = { prev, current, opts };
@@ -9041,9 +9204,9 @@ function filterPrefix(values, fragment) {
9041
9204
  }
9042
9205
  async function listContainerNames(ctx) {
9043
9206
  const home = ctx.opts.monocerosHome ?? monocerosHome();
9044
- const dir = path24.join(home, "container-configs");
9207
+ const dir = path25.join(home, "container-configs");
9045
9208
  if (!existsSync15(dir)) return [];
9046
- const entries = await fs14.readdir(dir);
9209
+ const entries = await fs15.readdir(dir);
9047
9210
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
9048
9211
  }
9049
9212
  function containerNameFromCtx(ctx) {
@@ -9055,12 +9218,42 @@ function containerNameFromCtx(ctx) {
9055
9218
  }
9056
9219
  return void 0;
9057
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
+ }
9058
9251
  async function collectDirs(dir, maxDepth) {
9059
9252
  const out = [];
9060
9253
  async function walk(at, rel, depth) {
9061
9254
  let entries;
9062
9255
  try {
9063
- entries = await fs14.readdir(at, { withFileTypes: true });
9256
+ entries = await fs15.readdir(at, { withFileTypes: true });
9064
9257
  } catch {
9065
9258
  return;
9066
9259
  }
@@ -9071,7 +9264,7 @@ async function collectDirs(dir, maxDepth) {
9071
9264
  const childRel = rel ? `${rel}/${e.name}` : e.name;
9072
9265
  out.push(childRel);
9073
9266
  if (depth + 1 < maxDepth) {
9074
- await walk(path24.join(at, e.name), childRel, depth + 1);
9267
+ await walk(path25.join(at, e.name), childRel, depth + 1);
9075
9268
  }
9076
9269
  }
9077
9270
  }
@@ -9079,9 +9272,9 @@ async function collectDirs(dir, maxDepth) {
9079
9272
  return out;
9080
9273
  }
9081
9274
  async function listContainerWorkspaceDirs(home, name) {
9082
- const root = path24.join(home, "container", name);
9275
+ const root = path25.join(home, "container", name);
9083
9276
  const top = await collectDirs(root, 1);
9084
- 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}`);
9085
9278
  return [.../* @__PURE__ */ new Set([...top, ...projects])].sort();
9086
9279
  }
9087
9280
  async function listRunInDirs(ctx) {
@@ -9157,6 +9350,7 @@ var init_resolve = __esm({
9157
9350
  "src/completion/resolve.ts"() {
9158
9351
  "use strict";
9159
9352
  init_paths();
9353
+ init_launch_config();
9160
9354
  init_components();
9161
9355
  init_manifest();
9162
9356
  init_catalog();
@@ -9166,6 +9360,7 @@ var init_resolve = __esm({
9166
9360
  PROJECTS_DIR_MAX_DEPTH = 4;
9167
9361
  ALL_COMMANDS = [
9168
9362
  "init",
9363
+ "list-apps",
9169
9364
  "list-components",
9170
9365
  "shell",
9171
9366
  "open",
@@ -9242,13 +9437,27 @@ var init_resolve = __esm({
9242
9437
  positionals: [containerName],
9243
9438
  flags: { "--in": { type: "value", values: (ctx) => listRunInDirs(ctx) } }
9244
9439
  },
9245
- logs: { positionals: [containerName] },
9440
+ logs: {
9441
+ positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9442
+ flags: {
9443
+ "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9444
+ }
9445
+ },
9246
9446
  start: {
9247
- positionals: [containerName],
9248
- 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
+ }
9249
9458
  },
9250
- stop: { positionals: [containerName] },
9251
9459
  status: { positionals: [containerName] },
9460
+ "list-apps": { positionals: [containerName] },
9252
9461
  "add-language": {
9253
9462
  positionals: [containerName, () => listLanguageNames()]
9254
9463
  },
@@ -9911,8 +10120,8 @@ var init_generator = __esm({
9911
10120
  });
9912
10121
 
9913
10122
  // src/init/index.ts
9914
- import { existsSync as existsSync16, promises as fs15 } from "fs";
9915
- import path25 from "path";
10123
+ import { existsSync as existsSync16, promises as fs16 } from "fs";
10124
+ import path26 from "path";
9916
10125
  import { consola as consola16 } from "consola";
9917
10126
  async function runInit(opts) {
9918
10127
  const home = opts.monocerosHome ?? monocerosHome();
@@ -9931,7 +10140,7 @@ async function runInit(opts) {
9931
10140
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
9932
10141
  );
9933
10142
  }
9934
- const componentsRoot = opts.workbenchRoot ? path25.join(opts.workbenchRoot, "components") : componentsRootDir();
10143
+ const componentsRoot = opts.workbenchRoot ? path26.join(opts.workbenchRoot, "components") : componentsRootDir();
9935
10144
  const catalog = await loadComponentCatalog(componentsRoot);
9936
10145
  if (catalog.size === 0) {
9937
10146
  throw new Error(
@@ -9997,9 +10206,9 @@ async function runInit(opts) {
9997
10206
  } else {
9998
10207
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
9999
10208
  }
10000
- await fs15.mkdir(containerConfigsDir(home), { recursive: true });
10209
+ await fs16.mkdir(containerConfigsDir(home), { recursive: true });
10001
10210
  await ensureEnvGitignored(containerConfigsDir(home));
10002
- await fs15.writeFile(dest, text, "utf8");
10211
+ await fs16.writeFile(dest, text, "utf8");
10003
10212
  const envPath = containerEnvPath(opts.name, home);
10004
10213
  const seedVars = {};
10005
10214
  for (const f of composed.features) {
@@ -10022,8 +10231,8 @@ async function runInit(opts) {
10022
10231
  }
10023
10232
  await ensureEnvVars(envPath, opts.name, seedVars);
10024
10233
  const documented = !anyComposed;
10025
- const ymlRel = path25.relative(home, dest);
10026
- const envRel = path25.relative(home, envPath);
10234
+ const ymlRel = path26.relative(home, dest);
10235
+ const envRel = path26.relative(home, envPath);
10027
10236
  if (documented) {
10028
10237
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
10029
10238
  logger.info(
@@ -10301,6 +10510,75 @@ var init_init2 = __esm({
10301
10510
  }
10302
10511
  });
10303
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
+
10304
10582
  // src/catalog/expand.ts
10305
10583
  function expandSelectable(catalog) {
10306
10584
  const out = /* @__PURE__ */ new Map();
@@ -10409,8 +10687,8 @@ var init_catalog_json = __esm({
10409
10687
  });
10410
10688
 
10411
10689
  // src/commands/list-components.ts
10412
- import { defineCommand as defineCommand14 } from "citty";
10413
- import { consola as consola18 } from "consola";
10690
+ import { defineCommand as defineCommand15 } from "citty";
10691
+ import { consola as consola19 } from "consola";
10414
10692
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
10415
10693
  var init_list_components = __esm({
10416
10694
  "src/commands/list-components.ts"() {
@@ -10430,7 +10708,7 @@ var init_list_components = __esm({
10430
10708
  "service",
10431
10709
  "feature"
10432
10710
  ];
10433
- listComponentsCommand = defineCommand14({
10711
+ listComponentsCommand = defineCommand15({
10434
10712
  meta: {
10435
10713
  name: "list-components",
10436
10714
  group: "discovery",
@@ -10453,7 +10731,7 @@ var init_list_components = __esm({
10453
10731
  }
10454
10732
  const catalog = expandSelectable(descriptors);
10455
10733
  if (catalog.size === 0) {
10456
- consola18.warn(
10734
+ consola19.warn(
10457
10735
  "No components found. The workbench checkout looks incomplete."
10458
10736
  );
10459
10737
  process.exit(0);
@@ -10504,7 +10782,7 @@ var init_list_components = __esm({
10504
10782
  }
10505
10783
  process.exit(0);
10506
10784
  } catch (err) {
10507
- consola18.error(err instanceof Error ? err.message : String(err));
10785
+ consola19.error(err instanceof Error ? err.message : String(err));
10508
10786
  process.exit(1);
10509
10787
  }
10510
10788
  }
@@ -10514,9 +10792,9 @@ var init_list_components = __esm({
10514
10792
 
10515
10793
  // src/commands/logs.ts
10516
10794
  import { spawn as spawn13 } from "child_process";
10517
- import { existsSync as existsSync17 } from "fs";
10518
- import path26 from "path";
10519
- 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";
10520
10798
  function tailLogFile(file, follow) {
10521
10799
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
10522
10800
  return new Promise((resolve, reject) => {
@@ -10530,13 +10808,14 @@ var init_logs = __esm({
10530
10808
  "src/commands/logs.ts"() {
10531
10809
  "use strict";
10532
10810
  init_paths();
10811
+ init_launch_config();
10533
10812
  init_compose();
10534
10813
  init_dispatch();
10535
- logsCommand = defineCommand15({
10814
+ logsCommand = defineCommand16({
10536
10815
  meta: {
10537
10816
  name: "logs",
10538
10817
  group: "run",
10539
- 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."
10540
10819
  },
10541
10820
  args: {
10542
10821
  name: {
@@ -10544,9 +10823,14 @@ var init_logs = __esm({
10544
10823
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
10545
10824
  required: true
10546
10825
  },
10547
- 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: {
10548
10832
  type: "string",
10549
- 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).'
10550
10834
  },
10551
10835
  follow: {
10552
10836
  type: "boolean",
@@ -10556,34 +10840,47 @@ var init_logs = __esm({
10556
10840
  }
10557
10841
  },
10558
10842
  run({ args }) {
10559
- const service = typeof args.service === "string" ? args.service : void 0;
10560
- if (service) {
10561
- const logFile = path26.join(containerLogsDir(args.name), `${service}.log`);
10562
- if (existsSync17(logFile)) {
10563
- 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
+ });
10564
10867
  }
10565
- }
10566
- return dispatch(
10567
- () => runLogs({
10568
- root: containerDir(args.name),
10569
- ...service ? { service } : {},
10570
- follow: args.follow
10571
- })
10572
- );
10868
+ return runLogs({ root: containerDir(args.name), follow: args.follow });
10869
+ });
10573
10870
  }
10574
10871
  });
10575
10872
  }
10576
10873
  });
10577
10874
 
10578
10875
  // src/commands/open.ts
10579
- import { defineCommand as defineCommand16 } from "citty";
10876
+ import { defineCommand as defineCommand17 } from "citty";
10580
10877
  var openCommand;
10581
10878
  var init_open2 = __esm({
10582
10879
  "src/commands/open.ts"() {
10583
10880
  "use strict";
10584
10881
  init_open();
10585
10882
  init_dispatch();
10586
- openCommand = defineCommand16({
10883
+ openCommand = defineCommand17({
10587
10884
  meta: {
10588
10885
  name: "open",
10589
10886
  group: "run",
@@ -10609,11 +10906,11 @@ var init_open2 = __esm({
10609
10906
  });
10610
10907
 
10611
10908
  // src/commands/port.ts
10612
- import { defineCommand as defineCommand17 } from "citty";
10613
- import { consola as consola19 } from "consola";
10909
+ import { defineCommand as defineCommand18 } from "citty";
10910
+ import { consola as consola21 } from "consola";
10614
10911
  async function runPortListing(opts) {
10615
10912
  const out = opts.out ?? process.stdout;
10616
- const info = opts.info ?? ((m) => consola19.info(m));
10913
+ const info = opts.info ?? ((m) => consola21.info(m));
10617
10914
  const parsed = await readConfig(
10618
10915
  containerConfigPath(opts.name, opts.monocerosHome)
10619
10916
  );
@@ -10671,7 +10968,7 @@ var init_port = __esm({
10671
10968
  init_schema();
10672
10969
  init_dynamic();
10673
10970
  init_format();
10674
- portCommand = defineCommand17({
10971
+ portCommand = defineCommand18({
10675
10972
  meta: {
10676
10973
  name: "port",
10677
10974
  group: "discovery",
@@ -10689,7 +10986,7 @@ var init_port = __esm({
10689
10986
  const code = await runPortListing({ name: args.name });
10690
10987
  process.exit(code);
10691
10988
  } catch (err) {
10692
- consola19.error(err instanceof Error ? err.message : String(err));
10989
+ consola21.error(err instanceof Error ? err.message : String(err));
10693
10990
  process.exit(1);
10694
10991
  }
10695
10992
  }
@@ -10698,15 +10995,15 @@ var init_port = __esm({
10698
10995
  });
10699
10996
 
10700
10997
  // src/commands/remove-apt-packages.ts
10701
- import { defineCommand as defineCommand18 } from "citty";
10702
- import { consola as consola20 } from "consola";
10998
+ import { defineCommand as defineCommand19 } from "citty";
10999
+ import { consola as consola22 } from "consola";
10703
11000
  var removeAptPackagesCommand;
10704
11001
  var init_remove_apt_packages = __esm({
10705
11002
  "src/commands/remove-apt-packages.ts"() {
10706
11003
  "use strict";
10707
11004
  init_inner_args();
10708
11005
  init_modify();
10709
- removeAptPackagesCommand = defineCommand18({
11006
+ removeAptPackagesCommand = defineCommand19({
10710
11007
  meta: {
10711
11008
  name: "remove-apt-packages",
10712
11009
  group: "edit",
@@ -10728,7 +11025,7 @@ var init_remove_apt_packages = __esm({
10728
11025
  async run({ args }) {
10729
11026
  const packages = [...getInnerArgs()];
10730
11027
  if (packages.length === 0) {
10731
- consola20.error(
11028
+ consola22.error(
10732
11029
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
10733
11030
  );
10734
11031
  process.exit(1);
@@ -10741,7 +11038,7 @@ var init_remove_apt_packages = __esm({
10741
11038
  });
10742
11039
  process.exit(result.status === "aborted" ? 1 : 0);
10743
11040
  } catch (err) {
10744
- consola20.error(err instanceof Error ? err.message : String(err));
11041
+ consola22.error(err instanceof Error ? err.message : String(err));
10745
11042
  process.exit(1);
10746
11043
  }
10747
11044
  }
@@ -10750,14 +11047,14 @@ var init_remove_apt_packages = __esm({
10750
11047
  });
10751
11048
 
10752
11049
  // src/commands/remove-feature.ts
10753
- import { defineCommand as defineCommand19 } from "citty";
10754
- import { consola as consola21 } from "consola";
11050
+ import { defineCommand as defineCommand20 } from "citty";
11051
+ import { consola as consola23 } from "consola";
10755
11052
  var removeFeatureCommand;
10756
11053
  var init_remove_feature = __esm({
10757
11054
  "src/commands/remove-feature.ts"() {
10758
11055
  "use strict";
10759
11056
  init_modify();
10760
- removeFeatureCommand = defineCommand19({
11057
+ removeFeatureCommand = defineCommand20({
10761
11058
  meta: {
10762
11059
  name: "remove-feature",
10763
11060
  group: "edit",
@@ -10790,7 +11087,7 @@ var init_remove_feature = __esm({
10790
11087
  });
10791
11088
  process.exit(result.status === "aborted" ? 1 : 0);
10792
11089
  } catch (err) {
10793
- consola21.error(err instanceof Error ? err.message : String(err));
11090
+ consola23.error(err instanceof Error ? err.message : String(err));
10794
11091
  process.exit(1);
10795
11092
  }
10796
11093
  }
@@ -10799,15 +11096,15 @@ var init_remove_feature = __esm({
10799
11096
  });
10800
11097
 
10801
11098
  // src/remove/index.ts
10802
- import { existsSync as existsSync18, promises as fs16 } from "fs";
10803
- import path27 from "path";
10804
- 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";
10805
11102
  async function runRemove(opts) {
10806
11103
  const home = opts.monocerosHome ?? monocerosHome();
10807
11104
  const logger = opts.logger ?? {
10808
- info: (msg) => consola22.info(msg),
10809
- success: (msg) => consola22.success(msg),
10810
- warn: (msg) => consola22.warn(msg)
11105
+ info: (msg) => consola24.info(msg),
11106
+ success: (msg) => consola24.success(msg),
11107
+ warn: (msg) => consola24.warn(msg)
10811
11108
  };
10812
11109
  if (!REGEX.solutionName.test(opts.name)) {
10813
11110
  throw new Error(
@@ -10817,9 +11114,9 @@ async function runRemove(opts) {
10817
11114
  const ymlPath = containerConfigPath(opts.name, home);
10818
11115
  const envPath = containerEnvPath(opts.name, home);
10819
11116
  const containerPath = containerDir(opts.name, home);
10820
- const hasYml = existsSync18(ymlPath);
10821
- const hasEnv = existsSync18(envPath);
10822
- const hasContainer = existsSync18(containerPath);
11117
+ const hasYml = existsSync17(ymlPath);
11118
+ const hasEnv = existsSync17(envPath);
11119
+ const hasContainer = existsSync17(containerPath);
10823
11120
  if (!hasYml && !hasContainer) {
10824
11121
  throw new Error(
10825
11122
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -10845,16 +11142,16 @@ async function runRemove(opts) {
10845
11142
  let backupPath = null;
10846
11143
  if (!opts.noBackup && (hasYml || hasContainer)) {
10847
11144
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10848
- backupPath = path27.join(home, "container-backups", `${opts.name}-${ts}`);
10849
- await fs16.mkdir(backupPath, { recursive: true });
11145
+ backupPath = path28.join(home, "container-backups", `${opts.name}-${ts}`);
11146
+ await fs17.mkdir(backupPath, { recursive: true });
10850
11147
  if (hasYml) {
10851
- await fs16.copyFile(ymlPath, path27.join(backupPath, `${opts.name}.yml`));
11148
+ await fs17.copyFile(ymlPath, path28.join(backupPath, `${opts.name}.yml`));
10852
11149
  }
10853
11150
  if (hasEnv) {
10854
- await fs16.copyFile(envPath, path27.join(backupPath, `${opts.name}.env`));
11151
+ await fs17.copyFile(envPath, path28.join(backupPath, `${opts.name}.env`));
10855
11152
  }
10856
11153
  if (hasContainer) {
10857
- await fs16.cp(containerPath, path27.join(backupPath, "container"), {
11154
+ await fs17.cp(containerPath, path28.join(backupPath, "container"), {
10858
11155
  recursive: true
10859
11156
  });
10860
11157
  }
@@ -10864,14 +11161,14 @@ async function runRemove(opts) {
10864
11161
  await stopBridgeDaemon(containerPath);
10865
11162
  }
10866
11163
  if (hasYml) {
10867
- await fs16.rm(ymlPath, { force: true });
11164
+ await fs17.rm(ymlPath, { force: true });
10868
11165
  }
10869
11166
  if (hasEnv) {
10870
- await fs16.rm(envPath, { force: true });
11167
+ await fs17.rm(envPath, { force: true });
10871
11168
  }
10872
11169
  if (hasContainer) {
10873
11170
  try {
10874
- await fs16.rm(containerPath, { recursive: true, force: true });
11171
+ await fs17.rm(containerPath, { recursive: true, force: true });
10875
11172
  } catch (err) {
10876
11173
  const code = err.code;
10877
11174
  if (code !== "EACCES" && code !== "EPERM") {
@@ -10893,7 +11190,7 @@ async function runRemove(opts) {
10893
11190
  "-delete"
10894
11191
  ]);
10895
11192
  try {
10896
- await fs16.rm(containerPath, { recursive: true, force: true });
11193
+ await fs17.rm(containerPath, { recursive: true, force: true });
10897
11194
  } catch (err2) {
10898
11195
  const code2 = err2.code;
10899
11196
  throw new Error(
@@ -10957,15 +11254,15 @@ var init_remove = __esm({
10957
11254
  });
10958
11255
 
10959
11256
  // src/commands/remove.ts
10960
- import { defineCommand as defineCommand20 } from "citty";
10961
- import { consola as consola23 } from "consola";
11257
+ import { defineCommand as defineCommand21 } from "citty";
11258
+ import { consola as consola25 } from "consola";
10962
11259
  import { createInterface } from "readline/promises";
10963
11260
  var removeCommand;
10964
11261
  var init_remove2 = __esm({
10965
11262
  "src/commands/remove.ts"() {
10966
11263
  "use strict";
10967
11264
  init_remove();
10968
- removeCommand = defineCommand20({
11265
+ removeCommand = defineCommand21({
10969
11266
  meta: {
10970
11267
  name: "remove",
10971
11268
  group: "lifecycle",
@@ -11002,7 +11299,7 @@ var init_remove2 = __esm({
11002
11299
  const skipPrompt = args.yes === true;
11003
11300
  if (!skipPrompt) {
11004
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.`;
11005
- consola23.warn(warning);
11302
+ consola25.warn(warning);
11006
11303
  const rl = createInterface({
11007
11304
  input: process.stdin,
11008
11305
  output: process.stdout
@@ -11010,7 +11307,7 @@ var init_remove2 = __esm({
11010
11307
  const answer = await rl.question("Continue? [y/N] ");
11011
11308
  rl.close();
11012
11309
  if (!/^y(es)?$/i.test(answer.trim())) {
11013
- consola23.info("Aborted. Nothing changed.");
11310
+ consola25.info("Aborted. Nothing changed.");
11014
11311
  process.exit(0);
11015
11312
  }
11016
11313
  }
@@ -11019,7 +11316,7 @@ var init_remove2 = __esm({
11019
11316
  ...noBackup ? { noBackup: true } : {}
11020
11317
  });
11021
11318
  } catch (err) {
11022
- consola23.error(err instanceof Error ? err.message : String(err));
11319
+ consola25.error(err instanceof Error ? err.message : String(err));
11023
11320
  process.exit(1);
11024
11321
  }
11025
11322
  }
@@ -11028,24 +11325,24 @@ var init_remove2 = __esm({
11028
11325
  });
11029
11326
 
11030
11327
  // src/restore/index.ts
11031
- import { existsSync as existsSync19, promises as fs17 } from "fs";
11032
- import path28 from "path";
11033
- 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";
11034
11331
  async function runRestore(opts) {
11035
11332
  const home = opts.monocerosHome ?? monocerosHome();
11036
11333
  const logger = opts.logger ?? {
11037
- info: (msg) => consola24.info(msg),
11038
- success: (msg) => consola24.success(msg)
11334
+ info: (msg) => consola26.info(msg),
11335
+ success: (msg) => consola26.success(msg)
11039
11336
  };
11040
- const backup = path28.resolve(opts.backupPath);
11041
- if (!existsSync19(backup)) {
11337
+ const backup = path29.resolve(opts.backupPath);
11338
+ if (!existsSync18(backup)) {
11042
11339
  throw new Error(`Backup not found: ${backup}.`);
11043
11340
  }
11044
- const stat = await fs17.stat(backup);
11341
+ const stat = await fs18.stat(backup);
11045
11342
  if (!stat.isDirectory()) {
11046
11343
  throw new Error(`Backup path is not a directory: ${backup}.`);
11047
11344
  }
11048
- const entries = await fs17.readdir(backup);
11345
+ const entries = await fs18.readdir(backup);
11049
11346
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
11050
11347
  if (ymlFiles.length === 0) {
11051
11348
  throw new Error(
@@ -11059,29 +11356,29 @@ async function runRestore(opts) {
11059
11356
  }
11060
11357
  const ymlFile = ymlFiles[0];
11061
11358
  const name = ymlFile.replace(/\.yml$/, "");
11062
- const containerInBackup = path28.join(backup, "container");
11063
- const hasContainer = existsSync19(containerInBackup);
11064
- const envInBackup = path28.join(backup, `${name}.env`);
11065
- 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);
11066
11363
  const destYml = containerConfigPath(name, home);
11067
11364
  const destContainer = containerDir(name, home);
11068
- if (existsSync19(destYml)) {
11365
+ if (existsSync18(destYml)) {
11069
11366
  throw new Error(
11070
11367
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
11071
11368
  );
11072
11369
  }
11073
- if (hasContainer && existsSync19(destContainer)) {
11370
+ if (hasContainer && existsSync18(destContainer)) {
11074
11371
  throw new Error(
11075
11372
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
11076
11373
  );
11077
11374
  }
11078
- await fs17.mkdir(containerConfigsDir(home), { recursive: true });
11079
- 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);
11080
11377
  if (hasEnv) {
11081
- await fs17.copyFile(envInBackup, containerEnvPath(name, home));
11378
+ await fs18.copyFile(envInBackup, containerEnvPath(name, home));
11082
11379
  }
11083
11380
  if (hasContainer) {
11084
- await fs17.cp(containerInBackup, destContainer, { recursive: true });
11381
+ await fs18.cp(containerInBackup, destContainer, { recursive: true });
11085
11382
  }
11086
11383
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
11087
11384
  logger.info(
@@ -11101,14 +11398,14 @@ var init_restore = __esm({
11101
11398
  });
11102
11399
 
11103
11400
  // src/commands/restore.ts
11104
- import { defineCommand as defineCommand21 } from "citty";
11105
- import { consola as consola25 } from "consola";
11401
+ import { defineCommand as defineCommand22 } from "citty";
11402
+ import { consola as consola27 } from "consola";
11106
11403
  var restoreCommand;
11107
11404
  var init_restore2 = __esm({
11108
11405
  "src/commands/restore.ts"() {
11109
11406
  "use strict";
11110
11407
  init_restore();
11111
- restoreCommand = defineCommand21({
11408
+ restoreCommand = defineCommand22({
11112
11409
  meta: {
11113
11410
  name: "restore",
11114
11411
  group: "lifecycle",
@@ -11125,7 +11422,7 @@ var init_restore2 = __esm({
11125
11422
  try {
11126
11423
  await runRestore({ backupPath: args["backup-path"] });
11127
11424
  } catch (err) {
11128
- consola25.error(err instanceof Error ? err.message : String(err));
11425
+ consola27.error(err instanceof Error ? err.message : String(err));
11129
11426
  process.exit(1);
11130
11427
  }
11131
11428
  }
@@ -11134,14 +11431,14 @@ var init_restore2 = __esm({
11134
11431
  });
11135
11432
 
11136
11433
  // src/commands/remove-from-url.ts
11137
- import { defineCommand as defineCommand22 } from "citty";
11138
- import { consola as consola26 } from "consola";
11434
+ import { defineCommand as defineCommand23 } from "citty";
11435
+ import { consola as consola28 } from "consola";
11139
11436
  var removeFromUrlCommand;
11140
11437
  var init_remove_from_url = __esm({
11141
11438
  "src/commands/remove-from-url.ts"() {
11142
11439
  "use strict";
11143
11440
  init_modify();
11144
- removeFromUrlCommand = defineCommand22({
11441
+ removeFromUrlCommand = defineCommand23({
11145
11442
  meta: {
11146
11443
  name: "remove-from-url",
11147
11444
  group: "edit",
@@ -11174,7 +11471,7 @@ var init_remove_from_url = __esm({
11174
11471
  });
11175
11472
  process.exit(result.status === "aborted" ? 1 : 0);
11176
11473
  } catch (err) {
11177
- consola26.error(err instanceof Error ? err.message : String(err));
11474
+ consola28.error(err instanceof Error ? err.message : String(err));
11178
11475
  process.exit(1);
11179
11476
  }
11180
11477
  }
@@ -11183,14 +11480,14 @@ var init_remove_from_url = __esm({
11183
11480
  });
11184
11481
 
11185
11482
  // src/commands/remove-language.ts
11186
- import { defineCommand as defineCommand23 } from "citty";
11187
- import { consola as consola27 } from "consola";
11483
+ import { defineCommand as defineCommand24 } from "citty";
11484
+ import { consola as consola29 } from "consola";
11188
11485
  var removeLanguageCommand;
11189
11486
  var init_remove_language = __esm({
11190
11487
  "src/commands/remove-language.ts"() {
11191
11488
  "use strict";
11192
11489
  init_modify();
11193
- removeLanguageCommand = defineCommand23({
11490
+ removeLanguageCommand = defineCommand24({
11194
11491
  meta: {
11195
11492
  name: "remove-language",
11196
11493
  group: "edit",
@@ -11223,7 +11520,7 @@ var init_remove_language = __esm({
11223
11520
  });
11224
11521
  process.exit(result.status === "aborted" ? 1 : 0);
11225
11522
  } catch (err) {
11226
- consola27.error(err instanceof Error ? err.message : String(err));
11523
+ consola29.error(err instanceof Error ? err.message : String(err));
11227
11524
  process.exit(1);
11228
11525
  }
11229
11526
  }
@@ -11232,8 +11529,8 @@ var init_remove_language = __esm({
11232
11529
  });
11233
11530
 
11234
11531
  // src/commands/remove-port.ts
11235
- import { defineCommand as defineCommand24 } from "citty";
11236
- import { consola as consola28 } from "consola";
11532
+ import { defineCommand as defineCommand25 } from "citty";
11533
+ import { consola as consola30 } from "consola";
11237
11534
  function coerceToken2(raw) {
11238
11535
  const n = Number(raw);
11239
11536
  return Number.isFinite(n) ? n : raw;
@@ -11244,7 +11541,7 @@ var init_remove_port = __esm({
11244
11541
  "use strict";
11245
11542
  init_inner_args();
11246
11543
  init_modify();
11247
- removePortCommand = defineCommand24({
11544
+ removePortCommand = defineCommand25({
11248
11545
  meta: {
11249
11546
  name: "remove-port",
11250
11547
  group: "edit",
@@ -11266,7 +11563,7 @@ var init_remove_port = __esm({
11266
11563
  async run({ args }) {
11267
11564
  const tokens = [...getInnerArgs()];
11268
11565
  if (tokens.length === 0) {
11269
- consola28.error(
11566
+ consola30.error(
11270
11567
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
11271
11568
  );
11272
11569
  process.exit(1);
@@ -11279,7 +11576,7 @@ var init_remove_port = __esm({
11279
11576
  });
11280
11577
  process.exit(result.status === "aborted" ? 1 : 0);
11281
11578
  } catch (err) {
11282
- consola28.error(err instanceof Error ? err.message : String(err));
11579
+ consola30.error(err instanceof Error ? err.message : String(err));
11283
11580
  process.exit(1);
11284
11581
  }
11285
11582
  }
@@ -11288,14 +11585,14 @@ var init_remove_port = __esm({
11288
11585
  });
11289
11586
 
11290
11587
  // src/commands/remove-repo.ts
11291
- import { defineCommand as defineCommand25 } from "citty";
11292
- import { consola as consola29 } from "consola";
11588
+ import { defineCommand as defineCommand26 } from "citty";
11589
+ import { consola as consola31 } from "consola";
11293
11590
  var removeRepoCommand;
11294
11591
  var init_remove_repo = __esm({
11295
11592
  "src/commands/remove-repo.ts"() {
11296
11593
  "use strict";
11297
11594
  init_modify();
11298
- removeRepoCommand = defineCommand25({
11595
+ removeRepoCommand = defineCommand26({
11299
11596
  meta: {
11300
11597
  name: "remove-repo",
11301
11598
  group: "edit",
@@ -11328,7 +11625,7 @@ var init_remove_repo = __esm({
11328
11625
  });
11329
11626
  process.exit(result.status === "aborted" ? 1 : 0);
11330
11627
  } catch (err) {
11331
- consola29.error(err instanceof Error ? err.message : String(err));
11628
+ consola31.error(err instanceof Error ? err.message : String(err));
11332
11629
  process.exit(1);
11333
11630
  }
11334
11631
  }
@@ -11337,14 +11634,14 @@ var init_remove_repo = __esm({
11337
11634
  });
11338
11635
 
11339
11636
  // src/commands/remove-service.ts
11340
- import { defineCommand as defineCommand26 } from "citty";
11341
- import { consola as consola30 } from "consola";
11637
+ import { defineCommand as defineCommand27 } from "citty";
11638
+ import { consola as consola32 } from "consola";
11342
11639
  var removeServiceCommand;
11343
11640
  var init_remove_service = __esm({
11344
11641
  "src/commands/remove-service.ts"() {
11345
11642
  "use strict";
11346
11643
  init_modify();
11347
- removeServiceCommand = defineCommand26({
11644
+ removeServiceCommand = defineCommand27({
11348
11645
  meta: {
11349
11646
  name: "remove-service",
11350
11647
  group: "edit",
@@ -11377,7 +11674,7 @@ var init_remove_service = __esm({
11377
11674
  });
11378
11675
  process.exit(result.status === "aborted" ? 1 : 0);
11379
11676
  } catch (err) {
11380
- consola30.error(err instanceof Error ? err.message : String(err));
11677
+ consola32.error(err instanceof Error ? err.message : String(err));
11381
11678
  process.exit(1);
11382
11679
  }
11383
11680
  }
@@ -11386,16 +11683,16 @@ var init_remove_service = __esm({
11386
11683
  });
11387
11684
 
11388
11685
  // src/devcontainer/claude-trust.ts
11389
- import { existsSync as existsSync20, promises as fsp7 } from "fs";
11390
- import path29 from "path";
11686
+ import { existsSync as existsSync19, promises as fsp7 } from "fs";
11687
+ import path30 from "path";
11391
11688
  function resolveContainerCwd(name, cwd) {
11392
11689
  const workspace = `/workspaces/${name}`;
11393
11690
  if (!cwd) return workspace;
11394
- return path29.posix.isAbsolute(cwd) ? cwd : path29.posix.join(workspace, cwd);
11691
+ return path30.posix.isAbsolute(cwd) ? cwd : path30.posix.join(workspace, cwd);
11395
11692
  }
11396
11693
  async function preApproveClaudeProject(opts) {
11397
- const file = path29.join(opts.root, "home", ".claude.json");
11398
- if (!existsSync20(file)) return;
11694
+ const file = path30.join(opts.root, "home", ".claude.json");
11695
+ if (!existsSync19(file)) return;
11399
11696
  try {
11400
11697
  const raw = await fsp7.readFile(file, "utf8");
11401
11698
  const config = raw.trim() ? JSON.parse(raw) : {};
@@ -11486,8 +11783,8 @@ var init_run = __esm({
11486
11783
  });
11487
11784
 
11488
11785
  // src/commands/run.ts
11489
- import { defineCommand as defineCommand27 } from "citty";
11490
- import { consola as consola31 } from "consola";
11786
+ import { defineCommand as defineCommand28 } from "citty";
11787
+ import { consola as consola33 } from "consola";
11491
11788
  var runCommand;
11492
11789
  var init_run2 = __esm({
11493
11790
  "src/commands/run.ts"() {
@@ -11495,7 +11792,7 @@ var init_run2 = __esm({
11495
11792
  init_paths();
11496
11793
  init_run();
11497
11794
  init_inner_args();
11498
- runCommand = defineCommand27({
11795
+ runCommand = defineCommand28({
11499
11796
  meta: {
11500
11797
  name: "run",
11501
11798
  group: "run",
@@ -11515,7 +11812,7 @@ var init_run2 = __esm({
11515
11812
  async run({ args }) {
11516
11813
  const command = [...getInnerArgs()];
11517
11814
  if (command.length === 0) {
11518
- consola31.error(
11815
+ consola33.error(
11519
11816
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
11520
11817
  );
11521
11818
  process.exit(1);
@@ -11529,7 +11826,7 @@ var init_run2 = __esm({
11529
11826
  });
11530
11827
  process.exit(exitCode);
11531
11828
  } catch (err) {
11532
- consola31.error(err instanceof Error ? err.message : String(err));
11829
+ consola33.error(err instanceof Error ? err.message : String(err));
11533
11830
  process.exit(1);
11534
11831
  }
11535
11832
  }
@@ -11538,15 +11835,15 @@ var init_run2 = __esm({
11538
11835
  });
11539
11836
 
11540
11837
  // src/commands/shell.ts
11541
- import { defineCommand as defineCommand28 } from "citty";
11542
- import { consola as consola32 } from "consola";
11838
+ import { defineCommand as defineCommand29 } from "citty";
11839
+ import { consola as consola34 } from "consola";
11543
11840
  var shellCommand;
11544
11841
  var init_shell2 = __esm({
11545
11842
  "src/commands/shell.ts"() {
11546
11843
  "use strict";
11547
11844
  init_paths();
11548
11845
  init_shell();
11549
- shellCommand = defineCommand28({
11846
+ shellCommand = defineCommand29({
11550
11847
  meta: {
11551
11848
  name: "shell",
11552
11849
  group: "run",
@@ -11567,7 +11864,7 @@ var init_shell2 = __esm({
11567
11864
  });
11568
11865
  process.exit(exitCode);
11569
11866
  } catch (err) {
11570
- consola32.error(err instanceof Error ? err.message : String(err));
11867
+ consola34.error(err instanceof Error ? err.message : String(err));
11571
11868
  process.exit(1);
11572
11869
  }
11573
11870
  }
@@ -11575,9 +11872,94 @@ var init_shell2 = __esm({
11575
11872
  }
11576
11873
  });
11577
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
+
11578
11903
  // src/commands/start.ts
11579
- import { defineCommand as defineCommand29 } from "citty";
11580
- 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
+ }
11581
11963
  var startCommand;
11582
11964
  var init_start = __esm({
11583
11965
  "src/commands/start.ts"() {
@@ -11591,12 +11973,13 @@ var init_start = __esm({
11591
11973
  init_open();
11592
11974
  init_proxy();
11593
11975
  init_port_check();
11976
+ init_app_control();
11594
11977
  init_dispatch();
11595
- startCommand = defineCommand29({
11978
+ startCommand = defineCommand30({
11596
11979
  meta: {
11597
11980
  name: "start",
11598
11981
  group: "run",
11599
- 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."
11600
11983
  },
11601
11984
  args: {
11602
11985
  name: {
@@ -11604,73 +11987,45 @@ var init_start = __esm({
11604
11987
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
11605
11988
  required: true
11606
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
+ },
11607
11999
  open: {
11608
12000
  type: "string",
11609
12001
  description: `After a successful start, open the container in this tool (${OPEN_TOOLS.join("|")}).`
11610
12002
  }
11611
12003
  },
11612
12004
  run({ args }) {
11613
- return dispatch(async () => {
11614
- let needsProxy = false;
11615
- let hostPort = 80;
11616
- let deferred = [];
11617
- let runtimeVersion;
11618
- try {
11619
- const parsed = await readConfig(containerConfigPath(args.name));
11620
- runtimeVersion = parsed.config.runtimeVersion;
11621
- if ((parsed.config.routing?.ports ?? []).length > 0) {
11622
- needsProxy = true;
11623
- const global = await readMonocerosConfig();
11624
- hostPort = proxyHostPort(global);
11625
- }
11626
- deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
11627
- } catch (err) {
11628
- consola33.warn(
11629
- `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
11630
- );
11631
- }
11632
- if (needsProxy) {
11633
- await preflightHostPort(hostPort);
11634
- await ensureProxy({ hostPort });
11635
- }
11636
- const exitCode = await runStart({ root: containerDir(args.name) });
11637
- if (exitCode === 0 && runtimeSupportsBrowserBridge(runtimeVersion)) {
11638
- spawnBridgeDaemon(containerDir(args.name));
11639
- }
11640
- if (exitCode === 0 && deferred.length > 0) {
11641
- try {
11642
- const deferExit = await startDeferredServices({
11643
- root: containerDir(args.name),
11644
- services: deferred,
11645
- logger: consola33
11646
- });
11647
- if (deferExit !== 0) {
11648
- consola33.warn(
11649
- `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
11650
- );
11651
- }
11652
- } catch (err) {
11653
- consola33.warn(
11654
- `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
11655
- );
11656
- }
11657
- }
11658
- if (args.open && exitCode === 0) {
11659
- try {
11660
- await runOpen({ name: args.name, tool: args.open });
11661
- } catch (err) {
11662
- consola33.warn(err instanceof Error ? err.message : String(err));
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;
11663
12012
  }
11664
- }
11665
- return exitCode;
11666
- });
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
+ );
11667
12022
  }
11668
12023
  });
11669
12024
  }
11670
12025
  });
11671
12026
 
11672
12027
  // src/commands/status.ts
11673
- import { defineCommand as defineCommand30 } from "citty";
12028
+ import { defineCommand as defineCommand31 } from "citty";
11674
12029
  var statusCommand;
11675
12030
  var init_status = __esm({
11676
12031
  "src/commands/status.ts"() {
@@ -11678,7 +12033,7 @@ var init_status = __esm({
11678
12033
  init_paths();
11679
12034
  init_compose();
11680
12035
  init_dispatch();
11681
- statusCommand = defineCommand30({
12036
+ statusCommand = defineCommand31({
11682
12037
  meta: {
11683
12038
  name: "status",
11684
12039
  group: "run",
@@ -11708,8 +12063,8 @@ var init_status = __esm({
11708
12063
  });
11709
12064
 
11710
12065
  // src/commands/stop.ts
11711
- import { defineCommand as defineCommand31 } from "citty";
11712
- import { consola as consola34 } from "consola";
12066
+ import { defineCommand as defineCommand32 } from "citty";
12067
+ import { consola as consola36 } from "consola";
11713
12068
  var stopCommand;
11714
12069
  var init_stop = __esm({
11715
12070
  "src/commands/stop.ts"() {
@@ -11717,12 +12072,13 @@ var init_stop = __esm({
11717
12072
  init_paths();
11718
12073
  init_compose();
11719
12074
  init_proxy();
12075
+ init_app_control();
11720
12076
  init_dispatch();
11721
- stopCommand = defineCommand31({
12077
+ stopCommand = defineCommand32({
11722
12078
  meta: {
11723
12079
  name: "stop",
11724
12080
  group: "run",
11725
- 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)."
11726
12082
  },
11727
12083
  args: {
11728
12084
  name: {
@@ -11730,12 +12086,26 @@ var init_stop = __esm({
11730
12086
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
11731
12087
  required: true
11732
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
+ },
11733
12098
  service: {
11734
12099
  type: "string",
11735
12100
  description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
11736
12101
  }
11737
12102
  },
11738
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
+ }
11739
12109
  return dispatch(async () => {
11740
12110
  const exit = await runStop({
11741
12111
  root: containerDir(args.name),
@@ -11743,10 +12113,10 @@ var init_stop = __esm({
11743
12113
  });
11744
12114
  try {
11745
12115
  await maybeStopProxy({
11746
- logger: { info: (msg) => consola34.info(msg) }
12116
+ logger: { info: (msg) => consola36.info(msg) }
11747
12117
  });
11748
12118
  } catch (err) {
11749
- consola34.warn(
12119
+ consola36.warn(
11750
12120
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
11751
12121
  );
11752
12122
  }
@@ -11758,11 +12128,11 @@ var init_stop = __esm({
11758
12128
  });
11759
12129
 
11760
12130
  // src/tunnel/resolve.ts
11761
- import { existsSync as existsSync21 } from "fs";
11762
- import path30 from "path";
12131
+ import { existsSync as existsSync20 } from "fs";
12132
+ import path31 from "path";
11763
12133
  async function resolveTunnelTarget(opts) {
11764
12134
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
11765
- if (!existsSync21(ymlPath)) {
12135
+ if (!existsSync20(ymlPath)) {
11766
12136
  throw new Error(
11767
12137
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
11768
12138
  );
@@ -11770,13 +12140,13 @@ async function resolveTunnelTarget(opts) {
11770
12140
  const parsed = await readConfig(ymlPath);
11771
12141
  const config = parsed.config;
11772
12142
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
11773
- if (!existsSync21(containerRoot)) {
12143
+ if (!existsSync20(containerRoot)) {
11774
12144
  throw new Error(
11775
12145
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
11776
12146
  );
11777
12147
  }
11778
- const composePath = path30.join(containerRoot, ".devcontainer", "compose.yaml");
11779
- const isCompose = existsSync21(composePath);
12148
+ const composePath = path31.join(containerRoot, ".devcontainer", "compose.yaml");
12149
+ const isCompose = existsSync20(composePath);
11780
12150
  const parsedTarget = parseTargetArg(opts.target, config);
11781
12151
  const docker = opts.docker ?? defaultDockerExec;
11782
12152
  if (isCompose) {
@@ -12012,7 +12382,7 @@ var init_port_check2 = __esm({
12012
12382
 
12013
12383
  // src/tunnel/run.ts
12014
12384
  import { spawn as spawn14 } from "child_process";
12015
- import { consola as consola35 } from "consola";
12385
+ import { consola as consola37 } from "consola";
12016
12386
  function signalNumber(signal) {
12017
12387
  switch (signal) {
12018
12388
  case "SIGINT":
@@ -12025,8 +12395,8 @@ function signalNumber(signal) {
12025
12395
  }
12026
12396
  async function runTunnel(opts) {
12027
12397
  const log = opts.logger ?? {
12028
- info: (m) => consola35.info(m),
12029
- warn: (m) => consola35.warn(m)
12398
+ info: (m) => consola37.info(m),
12399
+ warn: (m) => consola37.warn(m)
12030
12400
  };
12031
12401
  const resolve = opts.resolve ?? resolveTunnelTarget;
12032
12402
  const resolveArgs3 = {
@@ -12132,8 +12502,8 @@ var init_run3 = __esm({
12132
12502
  });
12133
12503
 
12134
12504
  // src/commands/tunnel.ts
12135
- import { defineCommand as defineCommand32 } from "citty";
12136
- import { consola as consola36 } from "consola";
12505
+ import { defineCommand as defineCommand33 } from "citty";
12506
+ import { consola as consola38 } from "consola";
12137
12507
  function parseLocalPort(raw) {
12138
12508
  if (raw === void 0) return void 0;
12139
12509
  const n = Number(raw);
@@ -12149,7 +12519,7 @@ var init_tunnel = __esm({
12149
12519
  "src/commands/tunnel.ts"() {
12150
12520
  "use strict";
12151
12521
  init_run3();
12152
- tunnelCommand = defineCommand32({
12522
+ tunnelCommand = defineCommand33({
12153
12523
  meta: {
12154
12524
  name: "tunnel",
12155
12525
  group: "discovery",
@@ -12186,7 +12556,7 @@ var init_tunnel = __esm({
12186
12556
  });
12187
12557
  process.exit(exitCode);
12188
12558
  } catch (err) {
12189
- consola36.error(err instanceof Error ? err.message : String(err));
12559
+ consola38.error(err instanceof Error ? err.message : String(err));
12190
12560
  process.exit(1);
12191
12561
  }
12192
12562
  }
@@ -12256,8 +12626,8 @@ var init_prune = __esm({
12256
12626
  });
12257
12627
 
12258
12628
  // src/upgrade/index.ts
12259
- import { existsSync as existsSync22, promises as fs18 } from "fs";
12260
- import { consola as consola37 } from "consola";
12629
+ import { existsSync as existsSync21, promises as fs19 } from "fs";
12630
+ import { consola as consola39 } from "consola";
12261
12631
  async function fetchRuntimeVersions() {
12262
12632
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
12263
12633
  const tokenRes = await fetch(tokenUrl);
@@ -12295,7 +12665,7 @@ ${yml}`;
12295
12665
  }
12296
12666
  async function runUpgrade(opts) {
12297
12667
  const home = opts.monocerosHome ?? monocerosHome();
12298
- const logger = opts.logger ?? consola37;
12668
+ const logger = opts.logger ?? consola39;
12299
12669
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
12300
12670
  if (opts.list) {
12301
12671
  const versions = await fetchVersions();
@@ -12321,7 +12691,7 @@ async function runUpgrade(opts) {
12321
12691
  );
12322
12692
  }
12323
12693
  }
12324
- if (opts.name && !existsSync22(containerConfigPath(opts.name, home))) {
12694
+ if (opts.name && !existsSync21(containerConfigPath(opts.name, home))) {
12325
12695
  throw new Error(
12326
12696
  `No such config: ${containerConfigPath(opts.name, home)}. Run \`monoceros init <template> ${opts.name}\` first.`
12327
12697
  );
@@ -12372,11 +12742,11 @@ async function runUpgrade(opts) {
12372
12742
  let bumped = 0;
12373
12743
  for (const name of targets) {
12374
12744
  const ymlPath = containerConfigPath(name, home);
12375
- if (!existsSync22(ymlPath)) continue;
12376
- const raw = await fs18.readFile(ymlPath, "utf8");
12745
+ if (!existsSync21(ymlPath)) continue;
12746
+ const raw = await fs19.readFile(ymlPath, "utf8");
12377
12747
  const updated = setRuntimeVersion(raw, pinVersion);
12378
12748
  if (updated !== raw) {
12379
- await fs18.writeFile(ymlPath, updated);
12749
+ await fs19.writeFile(ymlPath, updated);
12380
12750
  bumped += 1;
12381
12751
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
12382
12752
  }
@@ -12418,7 +12788,7 @@ function formatPruneLine(prune) {
12418
12788
  }
12419
12789
  async function listContainerNames2(home) {
12420
12790
  try {
12421
- const entries = await fs18.readdir(containerConfigsDir(home));
12791
+ const entries = await fs19.readdir(containerConfigsDir(home));
12422
12792
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
12423
12793
  } catch {
12424
12794
  return [];
@@ -12441,7 +12811,7 @@ var init_upgrade = __esm({
12441
12811
  });
12442
12812
 
12443
12813
  // src/commands/upgrade.ts
12444
- import { defineCommand as defineCommand33 } from "citty";
12814
+ import { defineCommand as defineCommand34 } from "citty";
12445
12815
  var upgradeCommand;
12446
12816
  var init_upgrade2 = __esm({
12447
12817
  "src/commands/upgrade.ts"() {
@@ -12449,7 +12819,7 @@ var init_upgrade2 = __esm({
12449
12819
  init_upgrade();
12450
12820
  init_version();
12451
12821
  init_dispatch();
12452
- upgradeCommand = defineCommand33({
12822
+ upgradeCommand = defineCommand34({
12453
12823
  meta: {
12454
12824
  name: "upgrade",
12455
12825
  group: "lifecycle",
@@ -12491,7 +12861,7 @@ var main_exports = {};
12491
12861
  __export(main_exports, {
12492
12862
  main: () => main
12493
12863
  });
12494
- import { defineCommand as defineCommand34 } from "citty";
12864
+ import { defineCommand as defineCommand35 } from "citty";
12495
12865
  var main;
12496
12866
  var init_main = __esm({
12497
12867
  "src/main.ts"() {
@@ -12509,6 +12879,7 @@ var init_main = __esm({
12509
12879
  init_complete();
12510
12880
  init_update_check();
12511
12881
  init_init2();
12882
+ init_list_apps();
12512
12883
  init_list_components();
12513
12884
  init_logs();
12514
12885
  init_open2();
@@ -12530,7 +12901,7 @@ var init_main = __esm({
12530
12901
  init_tunnel();
12531
12902
  init_upgrade2();
12532
12903
  init_version();
12533
- main = defineCommand34({
12904
+ main = defineCommand35({
12534
12905
  meta: {
12535
12906
  name: "monoceros",
12536
12907
  version: CLI_VERSION,
@@ -12538,6 +12909,7 @@ var init_main = __esm({
12538
12909
  },
12539
12910
  subCommands: {
12540
12911
  init: initCommand,
12912
+ "list-apps": listAppsCommand,
12541
12913
  "list-components": listComponentsCommand,
12542
12914
  shell: shellCommand,
12543
12915
  open: openCommand,
@@ -12829,25 +13201,25 @@ function detectHelpRequest(argv, main2) {
12829
13201
  const separatorIdx = argv.indexOf("--");
12830
13202
  if (helpIdx === -1) return null;
12831
13203
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
12832
- const path31 = [];
13204
+ const path32 = [];
12833
13205
  const tokens = argv.slice(
12834
13206
  0,
12835
13207
  separatorIdx === -1 ? argv.length : separatorIdx
12836
13208
  );
12837
13209
  let cursor = main2;
12838
13210
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
12839
- path31.push(mainName);
13211
+ path32.push(mainName);
12840
13212
  for (const tok of tokens) {
12841
13213
  if (tok.startsWith("-")) continue;
12842
13214
  const subs = cursor.subCommands ?? {};
12843
13215
  if (tok in subs) {
12844
13216
  cursor = subs[tok];
12845
- path31.push(tok);
13217
+ path32.push(tok);
12846
13218
  continue;
12847
13219
  }
12848
13220
  break;
12849
13221
  }
12850
- return { path: path31, cmd: cursor };
13222
+ return { path: path32, cmd: cursor };
12851
13223
  }
12852
13224
  async function maybeRenderHelp(argv, main2) {
12853
13225
  const hit = detectHelpRequest(argv, main2);