@getmonoceros/workbench 1.35.0 → 1.36.1

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
@@ -1166,6 +1166,7 @@ function makePalette(isTty2) {
1166
1166
  bold: (s) => wrap(s, ANSI_BOLD2),
1167
1167
  underline: (s) => wrap(s, ANSI_UNDERLINE2),
1168
1168
  cyan: (s) => wrap(s, ANSI_CYAN2),
1169
+ green: (s) => wrap(s, ANSI_GREEN),
1169
1170
  dim: (s) => wrap(s, ANSI_GREY2),
1170
1171
  sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
1171
1172
  };
@@ -1173,7 +1174,7 @@ function makePalette(isTty2) {
1173
1174
  function colorsFor(stream) {
1174
1175
  return makePalette(stream.isTTY ?? false);
1175
1176
  }
1176
- var ESC, ANSI_BOLD2, ANSI_UNDERLINE2, ANSI_CYAN2, ANSI_GREY2, ANSI_RESET2, ANSI_RE2, stderrPalette, bold2, underline2, cyan2, dim, sectionLine;
1177
+ var ESC, ANSI_BOLD2, ANSI_UNDERLINE2, ANSI_CYAN2, ANSI_GREEN, ANSI_GREY2, ANSI_RESET2, ANSI_RE2, stderrPalette, bold2, underline2, cyan2, green, dim, sectionLine;
1177
1178
  var init_format = __esm({
1178
1179
  "src/util/format.ts"() {
1179
1180
  "use strict";
@@ -1181,6 +1182,7 @@ var init_format = __esm({
1181
1182
  ANSI_BOLD2 = `${ESC}1m`;
1182
1183
  ANSI_UNDERLINE2 = `${ESC}4m`;
1183
1184
  ANSI_CYAN2 = `${ESC}36m`;
1185
+ ANSI_GREEN = `${ESC}32m`;
1184
1186
  ANSI_GREY2 = `${ESC}90m`;
1185
1187
  ANSI_RESET2 = `${ESC}0m`;
1186
1188
  ANSI_RE2 = /\x1b\[[0-9;]*m/g;
@@ -1188,6 +1190,7 @@ var init_format = __esm({
1188
1190
  bold2 = stderrPalette.bold;
1189
1191
  underline2 = stderrPalette.underline;
1190
1192
  cyan2 = stderrPalette.cyan;
1193
+ green = stderrPalette.green;
1191
1194
  dim = stderrPalette.dim;
1192
1195
  sectionLine = stderrPalette.sectionLine;
1193
1196
  }
@@ -2255,6 +2258,14 @@ function runtimeSupportsEntrypointSshd(version) {
2255
2258
  if (!version) return false;
2256
2259
  return compareRuntimeVersions(version, MIN_RUNTIME_FOR_ENTRYPOINT_SSHD) >= 0;
2257
2260
  }
2261
+ function runtimeSupportsAppRestart(version) {
2262
+ if (!version) return false;
2263
+ return compareRuntimeVersions(version, MIN_RUNTIME_FOR_APP_RESTART) >= 0;
2264
+ }
2265
+ function runtimeSupportsAppStatus(version) {
2266
+ if (!version) return false;
2267
+ return compareRuntimeVersions(version, MIN_RUNTIME_FOR_APP_STATUS) >= 0;
2268
+ }
2258
2269
  function descriptorOptionDefaults(options) {
2259
2270
  const out = {};
2260
2271
  for (const [key, spec] of Object.entries(options)) {
@@ -2387,7 +2398,7 @@ function deriveServiceName(image) {
2387
2398
  const noTag = lastSegment.split("@")[0].split(":")[0];
2388
2399
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
2389
2400
  }
2390
- var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, MIN_RUNTIME_FOR_HOST_KEY_PINNING, MIN_RUNTIME_FOR_ENTRYPOINT_SSHD, MIN_RUNTIME_FOR_BROWSER_BRIDGE, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG, DEFERRED_SERVICE_PROFILE;
2401
+ var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, MIN_RUNTIME_FOR_HOST_KEY_PINNING, MIN_RUNTIME_FOR_ENTRYPOINT_SSHD, MIN_RUNTIME_FOR_BROWSER_BRIDGE, MIN_RUNTIME_FOR_APP_RESTART, MIN_RUNTIME_FOR_APP_STATUS, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG, DEFERRED_SERVICE_PROFILE;
2391
2402
  var init_catalog = __esm({
2392
2403
  "src/create/catalog.ts"() {
2393
2404
  "use strict";
@@ -2396,7 +2407,7 @@ var init_catalog = __esm({
2396
2407
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2397
2408
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2398
2409
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2399
- DEFAULT_RUNTIME_VERSION = true ? "1.5.0" : readFileSync3(
2410
+ DEFAULT_RUNTIME_VERSION = true ? "1.6.0" : readFileSync3(
2400
2411
  fileURLToPath2(
2401
2412
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2402
2413
  ),
@@ -2406,6 +2417,8 @@ var init_catalog = __esm({
2406
2417
  MIN_RUNTIME_FOR_HOST_KEY_PINNING = "1.3.5";
2407
2418
  MIN_RUNTIME_FOR_ENTRYPOINT_SSHD = "1.3.6";
2408
2419
  MIN_RUNTIME_FOR_BROWSER_BRIDGE = "1.3.3";
2420
+ MIN_RUNTIME_FOR_APP_RESTART = "1.6.0";
2421
+ MIN_RUNTIME_FOR_APP_STATUS = "1.6.0";
2409
2422
  DESCRIPTORS = loadDescriptorCatalogSync();
2410
2423
  BUILTIN_LANGUAGES = new Set(
2411
2424
  [...DESCRIPTORS.values()].filter((c) => c.category === "language" && c.descriptor.language?.builtin).map(descriptorSelector)
@@ -5386,8 +5399,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
5386
5399
  if (!isMap2(item)) return false;
5387
5400
  const url = item.get("url");
5388
5401
  if (url === urlOrPath) return true;
5389
- const path32 = item.get("path");
5390
- const effectivePath = typeof path32 === "string" ? path32 : typeof url === "string" ? deriveRepoName(url) : void 0;
5402
+ const path33 = item.get("path");
5403
+ const effectivePath = typeof path33 === "string" ? path33 : typeof url === "string" ? deriveRepoName(url) : void 0;
5391
5404
  return effectivePath === urlOrPath;
5392
5405
  });
5393
5406
  if (idx < 0) return false;
@@ -5531,7 +5544,7 @@ async function runAddRepo(input) {
5531
5544
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
5532
5545
  );
5533
5546
  }
5534
- const path32 = (input.path ?? deriveRepoName(url)).trim();
5547
+ const path33 = (input.path ?? deriveRepoName(url)).trim();
5535
5548
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
5536
5549
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
5537
5550
  if (hasName !== hasEmail) {
@@ -5568,7 +5581,7 @@ async function runAddRepo(input) {
5568
5581
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
5569
5582
  const entry2 = {
5570
5583
  url,
5571
- path: path32,
5584
+ path: path33,
5572
5585
  ...hasName && hasEmail ? {
5573
5586
  gitUser: {
5574
5587
  name: input.gitName.trim(),
@@ -6536,16 +6549,66 @@ var init_state = __esm({
6536
6549
  }
6537
6550
  });
6538
6551
 
6552
+ // src/devcontainer/app-control.ts
6553
+ import { promises as fs12 } from "fs";
6554
+ import path15 from "path";
6555
+ function findRunningContainer(name, opts = {}) {
6556
+ return findRunningContainerByLocalFolder(containerDir(name), {
6557
+ ...opts.docker ? { docker: opts.docker } : {}
6558
+ });
6559
+ }
6560
+ async function runAppCtl(name, ctlArgs2, opts = {}) {
6561
+ const id = await findRunningContainer(name, opts);
6562
+ if (!id) {
6563
+ throw new Error(
6564
+ `Container "${name}" is not running. Run \`monoceros start ${name}\` first.`
6565
+ );
6566
+ }
6567
+ const exec = opts.exec ?? realContainerExec;
6568
+ const result = await exec(id, ["monoceros-ctl", ...ctlArgs2]);
6569
+ return result.exitCode;
6570
+ }
6571
+ async function hasWantedApps(name, home) {
6572
+ const runRoot = path15.join(containerDir(name, home), ".monoceros", "run");
6573
+ async function anyPid(dir) {
6574
+ let entries;
6575
+ try {
6576
+ entries = await fs12.readdir(dir, { withFileTypes: true });
6577
+ } catch {
6578
+ return false;
6579
+ }
6580
+ for (const e of entries) {
6581
+ if (e.isDirectory()) {
6582
+ if (await anyPid(path15.join(dir, e.name))) return true;
6583
+ } else if (e.isFile() && e.name.endsWith(".pid")) {
6584
+ return true;
6585
+ }
6586
+ }
6587
+ return false;
6588
+ }
6589
+ return anyPid(runRoot);
6590
+ }
6591
+ function ctlArgs(sub, app, target, extra = []) {
6592
+ return [sub, app, ...target ? ["--target", target] : [], ...extra];
6593
+ }
6594
+ var init_app_control = __esm({
6595
+ "src/devcontainer/app-control.ts"() {
6596
+ "use strict";
6597
+ init_paths();
6598
+ init_locate_running();
6599
+ }
6600
+ });
6601
+
6539
6602
  // src/devcontainer/browser-bridge.ts
6540
6603
  import { spawn as spawn5 } from "child_process";
6541
6604
  import { existsSync as existsSync9, promises as fsp4, readFileSync as readFileSync5 } from "fs";
6542
6605
  import http from "http";
6543
- import path15 from "path";
6606
+ import path16 from "path";
6544
6607
  function relayDir(root) {
6545
- return path15.join(root, RELAY_DIRNAME);
6608
+ return path16.join(root, RELAY_DIRNAME);
6546
6609
  }
6547
6610
  function relayUrlFile(root) {
6548
- return path15.join(relayDir(root), "url");
6611
+ return path16.join(relayDir(root), "url");
6549
6612
  }
6550
6613
  function parseCallbackTarget(authUrl) {
6551
6614
  try {
@@ -6599,7 +6662,7 @@ function openInBrowser(url) {
6599
6662
  }
6600
6663
  async function startBrowserBridge(opts) {
6601
6664
  const dir = relayDir(opts.root);
6602
- const relayScript = path15.join(dir, "xdg-open");
6665
+ const relayScript = path16.join(dir, "xdg-open");
6603
6666
  const urlFile = relayUrlFile(opts.root);
6604
6667
  await fsp4.mkdir(dir, { recursive: true });
6605
6668
  await fsp4.rm(urlFile, { force: true });
@@ -6796,7 +6859,7 @@ var init_runtime_pull_hint = __esm({
6796
6859
  import { spawn as spawn6 } from "child_process";
6797
6860
  import { readFileSync as readFileSync6 } from "fs";
6798
6861
  import { createRequire } from "module";
6799
- import path16 from "path";
6862
+ import path17 from "path";
6800
6863
  function devcontainerCliPath() {
6801
6864
  if (cachedBinaryPath) return cachedBinaryPath;
6802
6865
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
@@ -6805,7 +6868,7 @@ function devcontainerCliPath() {
6805
6868
  if (!binEntry) {
6806
6869
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
6807
6870
  }
6808
- cachedBinaryPath = path16.resolve(path16.dirname(pkgJsonPath), binEntry);
6871
+ cachedBinaryPath = path17.resolve(path17.dirname(pkgJsonPath), binEntry);
6809
6872
  return cachedBinaryPath;
6810
6873
  }
6811
6874
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -6880,7 +6943,7 @@ var init_cli = __esm({
6880
6943
  // src/devcontainer/compose.ts
6881
6944
  import { spawn as spawn7 } from "child_process";
6882
6945
  import { existsSync as existsSync10 } from "fs";
6883
- import path17 from "path";
6946
+ import path18 from "path";
6884
6947
  import { Writable } from "stream";
6885
6948
  import { consola as consola10 } from "consola";
6886
6949
  function spawnDockerComposeTo(opts) {
@@ -6949,21 +7012,21 @@ async function cleanupDockerObjects(opts) {
6949
7012
  return { exitCode: rmExit, removedIds: ids };
6950
7013
  }
6951
7014
  function composeProjectName(root) {
6952
- return `${path17.basename(root)}_devcontainer`;
7015
+ return `${path18.basename(root)}_devcontainer`;
6953
7016
  }
6954
7017
  function assertDevcontainer(root) {
6955
- if (!existsSync10(path17.join(root, ".devcontainer"))) {
7018
+ if (!existsSync10(path18.join(root, ".devcontainer"))) {
6956
7019
  throw new Error(
6957
7020
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
6958
7021
  );
6959
7022
  }
6960
7023
  }
6961
7024
  function isComposeMode(root) {
6962
- return existsSync10(path17.join(root, ".devcontainer", "compose.yaml"));
7025
+ return existsSync10(path18.join(root, ".devcontainer", "compose.yaml"));
6963
7026
  }
6964
7027
  function resolveCompose(root) {
6965
7028
  assertDevcontainer(root);
6966
- const composeFile = path17.join(root, ".devcontainer", "compose.yaml");
7029
+ const composeFile = path18.join(root, ".devcontainer", "compose.yaml");
6967
7030
  if (!existsSync10(composeFile)) {
6968
7031
  throw new Error(
6969
7032
  `No compose.yaml at ${composeFile}. \`monoceros logs\` tails compose service logs, which require services configured via \`monoceros add-service <name> <svc>\`. For a bare container, use \`monoceros shell <name>\` or read logs/<app>.log.`
@@ -7156,23 +7219,13 @@ function runStop(opts) {
7156
7219
  }
7157
7220
  return stopImageContainer(opts);
7158
7221
  }
7159
- function runStatus(opts) {
7160
- assertDevcontainer(opts.root);
7161
- if (isComposeMode(opts.root)) {
7162
- return runComposeAction(
7163
- (service) => ["ps", ...service ? [service] : []],
7164
- opts
7165
- );
7166
- }
7167
- return statusImageContainer(opts);
7168
- }
7169
7222
  function imageContainerFilter(root) {
7170
7223
  return `label=devcontainer.local_folder=${root}`;
7171
7224
  }
7172
7225
  async function stopImageContainer(opts) {
7173
7226
  const exec = opts.dockerExec ?? spawnDocker;
7174
7227
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
7175
- const name = path17.basename(opts.root);
7228
+ const name = path18.basename(opts.root);
7176
7229
  const ps = await exec([
7177
7230
  "ps",
7178
7231
  "-q",
@@ -7190,29 +7243,6 @@ async function stopImageContainer(opts) {
7190
7243
  if (res.exitCode === 0) logger.info(`Stopped '${name}'.`);
7191
7244
  return res.exitCode;
7192
7245
  }
7193
- async function statusImageContainer(opts) {
7194
- const exec = opts.dockerExec ?? spawnDocker;
7195
- const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
7196
- const name = path17.basename(opts.root);
7197
- const res = await exec([
7198
- "ps",
7199
- "-a",
7200
- "--filter",
7201
- imageContainerFilter(opts.root)
7202
- ]);
7203
- const rows = res.stdout.split("\n").filter((l) => l.trim().length > 0);
7204
- if (rows.length <= 1) {
7205
- logger.info(
7206
- `Container '${name}' does not exist. Run \`monoceros apply ${name}\`.`
7207
- );
7208
- return 0;
7209
- }
7210
- process.stdout.write(
7211
- res.stdout.endsWith("\n") ? res.stdout : `${res.stdout}
7212
- `
7213
- );
7214
- return res.exitCode;
7215
- }
7216
7246
  function runLogs(opts) {
7217
7247
  const follow = opts.follow ?? true;
7218
7248
  return runComposeAction(
@@ -7280,9 +7310,9 @@ import {
7280
7310
  readFileSync as readFileSync7,
7281
7311
  writeFileSync
7282
7312
  } from "fs";
7283
- import path18 from "path";
7313
+ import path19 from "path";
7284
7314
  function bridgePidFile(root) {
7285
- return path18.join(relayDir(root), "daemon.pid");
7315
+ return path19.join(relayDir(root), "daemon.pid");
7286
7316
  }
7287
7317
  function pidAlive(pid) {
7288
7318
  try {
@@ -7386,7 +7416,7 @@ var init_bridge_daemon = __esm({
7386
7416
 
7387
7417
  // src/apply/apply-log.ts
7388
7418
  import { createWriteStream, mkdirSync as mkdirSync2 } from "fs";
7389
- import path19 from "path";
7419
+ import path20 from "path";
7390
7420
  import { Writable as Writable2 } from "stream";
7391
7421
  function safeIsoStamp(d) {
7392
7422
  return d.toISOString().replace(/[:.]/g, "-");
@@ -7396,7 +7426,7 @@ function createApplyLog(opts) {
7396
7426
  const dir = containerLogsDir(opts.name, opts.home);
7397
7427
  mkdirSync2(dir, { recursive: true });
7398
7428
  const file = `apply-${opts.name}-${safeIsoStamp(now)}.log`;
7399
- const fullPath = path19.join(dir, file);
7429
+ const fullPath = path20.join(dir, file);
7400
7430
  const stream = createWriteStream(fullPath, { flags: "w" });
7401
7431
  const header = [
7402
7432
  `# monoceros apply log`,
@@ -7733,9 +7763,9 @@ var init_images = __esm({
7733
7763
 
7734
7764
  // src/config/machine-state.ts
7735
7765
  import { promises as fsp6, readFileSync as readFileSync8 } from "fs";
7736
- import path20 from "path";
7766
+ import path21 from "path";
7737
7767
  function machineStatePath(home = monocerosHome()) {
7738
- return path20.join(home, ".machine-state.json");
7768
+ return path21.join(home, ".machine-state.json");
7739
7769
  }
7740
7770
  async function readMachineState(home = monocerosHome()) {
7741
7771
  try {
@@ -7878,8 +7908,8 @@ var init_docker_mode = __esm({
7878
7908
 
7879
7909
  // src/devcontainer/identity.ts
7880
7910
  import { spawn as spawn10 } from "child_process";
7881
- import { promises as fs12 } from "fs";
7882
- import path21 from "path";
7911
+ import { promises as fs13 } from "fs";
7912
+ import path22 from "path";
7883
7913
  import { consola as consola11 } from "consola";
7884
7914
  async function resolveIdentityWithPrompt(options = {}) {
7885
7915
  const spawnFn = options.spawn ?? realGitConfigGet;
@@ -7935,8 +7965,8 @@ async function resolveIdentityWithPrompt(options = {}) {
7935
7965
  };
7936
7966
  }
7937
7967
  async function collectGitIdentity(devContainerRoot, options = {}) {
7938
- const gitconfigDir = path21.join(devContainerRoot, ".monoceros");
7939
- const gitconfigPath = path21.join(gitconfigDir, "gitconfig");
7968
+ const gitconfigDir = path22.join(devContainerRoot, ".monoceros");
7969
+ const gitconfigPath = path22.join(gitconfigDir, "gitconfig");
7940
7970
  const logger = options.logger ?? { info: () => {
7941
7971
  }, warn: () => {
7942
7972
  } };
@@ -7949,8 +7979,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
7949
7979
  const lines = ["[user]"];
7950
7980
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
7951
7981
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
7952
- await fs12.mkdir(gitconfigDir, { recursive: true });
7953
- await fs12.writeFile(gitconfigPath, lines.join("\n") + "\n");
7982
+ await fs13.mkdir(gitconfigDir, { recursive: true });
7983
+ await fs13.writeFile(gitconfigPath, lines.join("\n") + "\n");
7954
7984
  return {
7955
7985
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
7956
7986
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -7993,7 +8023,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
7993
8023
  }
7994
8024
  async function readExistingGitconfig(filePath) {
7995
8025
  try {
7996
- const content = await fs12.readFile(filePath, "utf8");
8026
+ const content = await fs13.readFile(filePath, "utf8");
7997
8027
  const result = {};
7998
8028
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
7999
8029
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -8070,7 +8100,7 @@ var init_identity = __esm({
8070
8100
  });
8071
8101
 
8072
8102
  // src/apply/index.ts
8073
- import { existsSync as existsSync12, promises as fs13 } from "fs";
8103
+ import { existsSync as existsSync12, promises as fs14 } from "fs";
8074
8104
  import { consola as consola12 } from "consola";
8075
8105
  async function runApply(opts) {
8076
8106
  const home = opts.monocerosHome ?? monocerosHome();
@@ -8210,7 +8240,7 @@ Fix the value in the env file (or the yml).`
8210
8240
  if (dockerMode === "rootless") {
8211
8241
  throw new Error(formatRootlessNotSupportedError());
8212
8242
  }
8213
- await fs13.mkdir(targetDir, { recursive: true });
8243
+ await fs14.mkdir(targetDir, { recursive: true });
8214
8244
  await writeScaffold(createOpts, targetDir, { dockerMode });
8215
8245
  await writeStateFile(
8216
8246
  targetDir,
@@ -8382,6 +8412,18 @@ Fix the value in the env file (or the yml).`
8382
8412
  );
8383
8413
  }
8384
8414
  }
8415
+ if (runtimeSupportsAppRestart(createOpts.runtimeVersion) && await hasWantedApps(opts.name, home)) {
8416
+ progressOut.write(`
8417
+ ${dim("restoring apps that were running\u2026")}
8418
+ `);
8419
+ try {
8420
+ await runAppCtl(opts.name, ["reconcile"]);
8421
+ } catch (err) {
8422
+ (logger.warn ?? logger.info)(
8423
+ `Restoring running apps skipped: ${err instanceof Error ? err.message : String(err)}. Bring them back with \`monoceros start ${opts.name} <app>\`.`
8424
+ );
8425
+ }
8426
+ }
8385
8427
  const summaryLines = buildApplySummary(createOpts);
8386
8428
  if (summaryLines.length > 0) {
8387
8429
  const formatted = formatApplySummary(summaryLines);
@@ -8432,7 +8474,7 @@ ${stripAnsi(formatted)}
8432
8474
  }
8433
8475
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
8434
8476
  if (!existsSync12(targetDir)) return;
8435
- const entries = await fs13.readdir(targetDir);
8477
+ const entries = await fs14.readdir(targetDir);
8436
8478
  if (entries.length === 0) return;
8437
8479
  const state = await readStateFile(targetDir);
8438
8480
  if (state) {
@@ -8502,7 +8544,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
8502
8544
  }
8503
8545
  if (wantContainer) {
8504
8546
  try {
8505
- const text = await fs13.readFile(ymlPath, "utf8");
8547
+ const text = await fs14.readFile(ymlPath, "utf8");
8506
8548
  const parsed = parseConfig(text, ymlPath);
8507
8549
  const changed = setContainerGitUserInDoc(parsed.doc, {
8508
8550
  name: prompted.name,
@@ -8510,7 +8552,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
8510
8552
  });
8511
8553
  if (changed) {
8512
8554
  const out = stringifyConfig(parsed.doc);
8513
- await fs13.writeFile(ymlPath, out, "utf8");
8555
+ await fs14.writeFile(ymlPath, out, "utf8");
8514
8556
  logger.info(
8515
8557
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
8516
8558
  );
@@ -8533,6 +8575,7 @@ var init_apply = __esm({
8533
8575
  init_state();
8534
8576
  init_transform();
8535
8577
  init_catalog();
8578
+ init_app_control();
8536
8579
  init_bridge_daemon();
8537
8580
  init_ssh_attach();
8538
8581
  init_scaffold();
@@ -8560,7 +8603,7 @@ var init_apply = __esm({
8560
8603
 
8561
8604
  // src/devcontainer/shell.ts
8562
8605
  import { existsSync as existsSync13 } from "fs";
8563
- import path22 from "path";
8606
+ import path23 from "path";
8564
8607
  async function runShell(opts) {
8565
8608
  assertContainerExists(opts.root);
8566
8609
  const spawnFn = opts.spawn ?? spawnDevcontainer;
@@ -8570,7 +8613,7 @@ async function runShell(opts) {
8570
8613
  { quiet: true }
8571
8614
  );
8572
8615
  if (upCode !== 0) return upCode;
8573
- const name = opts.name ?? path22.basename(opts.root);
8616
+ const name = opts.name ?? path23.basename(opts.root);
8574
8617
  const isTty2 = opts.isTty ?? process.stdout.isTTY;
8575
8618
  const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
8576
8619
  try {
@@ -8594,7 +8637,7 @@ async function runShell(opts) {
8594
8637
  }
8595
8638
  }
8596
8639
  function assertContainerExists(root) {
8597
- if (!existsSync13(path22.join(root, ".devcontainer"))) {
8640
+ if (!existsSync13(path23.join(root, ".devcontainer"))) {
8598
8641
  throw new Error(
8599
8642
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
8600
8643
  );
@@ -8611,12 +8654,12 @@ var init_shell = __esm({
8611
8654
  // src/open/index.ts
8612
8655
  import { spawn as spawn11 } from "child_process";
8613
8656
  import { accessSync, constants, existsSync as existsSync14 } from "fs";
8614
- import path23 from "path";
8657
+ import path24 from "path";
8615
8658
  import { consola as consola13 } from "consola";
8616
8659
  function resolveOnPath(bin) {
8617
- const dirs = (process.env.PATH ?? "").split(path23.delimiter).filter(Boolean);
8660
+ const dirs = (process.env.PATH ?? "").split(path24.delimiter).filter(Boolean);
8618
8661
  for (const dir of dirs) {
8619
- const candidate = path23.join(dir, bin);
8662
+ const candidate = path24.join(dir, bin);
8620
8663
  try {
8621
8664
  accessSync(candidate, constants.X_OK);
8622
8665
  return candidate;
@@ -8729,7 +8772,7 @@ var CLI_VERSION;
8729
8772
  var init_version = __esm({
8730
8773
  "src/version.ts"() {
8731
8774
  "use strict";
8732
- CLI_VERSION = true ? "1.35.0" : "dev";
8775
+ CLI_VERSION = true ? "1.36.1" : "dev";
8733
8776
  }
8734
8777
  });
8735
8778
 
@@ -8963,10 +9006,10 @@ var init_bridge = __esm({
8963
9006
  });
8964
9007
 
8965
9008
  // src/config/launch-config.ts
8966
- import { promises as fs14 } from "fs";
8967
- import path24 from "path";
9009
+ import { promises as fs15 } from "fs";
9010
+ import path25 from "path";
8968
9011
  function launchConfigPath(name, appRel, home) {
8969
- return path24.join(
9012
+ return path25.join(
8970
9013
  containerDir(name, home),
8971
9014
  "projects",
8972
9015
  appRel,
@@ -9016,7 +9059,7 @@ async function readLaunchConfig(name, appRel, home) {
9016
9059
  const file = launchConfigPath(name, appRel, home);
9017
9060
  let content;
9018
9061
  try {
9019
- content = await fs14.readFile(file, "utf8");
9062
+ content = await fs15.readFile(file, "utf8");
9020
9063
  } catch {
9021
9064
  return void 0;
9022
9065
  }
@@ -9053,12 +9096,12 @@ function resolveTarget(config, targetName, appRel) {
9053
9096
  );
9054
9097
  }
9055
9098
  async function listApps(name, home) {
9056
- const projectsRoot = path24.join(containerDir(name, home), "projects");
9099
+ const projectsRoot = path25.join(containerDir(name, home), "projects");
9057
9100
  const out = [];
9058
9101
  async function walk(at, rel, depth) {
9059
9102
  let entries;
9060
9103
  try {
9061
- entries = await fs14.readdir(at, { withFileTypes: true });
9104
+ entries = await fs15.readdir(at, { withFileTypes: true });
9062
9105
  } catch {
9063
9106
  return;
9064
9107
  }
@@ -9067,7 +9110,7 @@ async function listApps(name, home) {
9067
9110
  );
9068
9111
  if (hasLaunch) {
9069
9112
  try {
9070
- await fs14.access(path24.join(at, LAUNCH_DIRNAME, LAUNCH_FILENAME));
9113
+ await fs15.access(path25.join(at, LAUNCH_DIRNAME, LAUNCH_FILENAME));
9071
9114
  if (rel) out.push(rel);
9072
9115
  } catch {
9073
9116
  }
@@ -9077,7 +9120,7 @@ async function listApps(name, home) {
9077
9120
  if (!e.isDirectory()) continue;
9078
9121
  if (e.name.startsWith(".")) continue;
9079
9122
  await walk(
9080
- path24.join(at, e.name),
9123
+ path25.join(at, e.name),
9081
9124
  rel ? `${rel}/${e.name}` : e.name,
9082
9125
  depth + 1
9083
9126
  );
@@ -9098,8 +9141,8 @@ var init_launch_config = __esm({
9098
9141
  });
9099
9142
 
9100
9143
  // src/completion/resolve.ts
9101
- import { existsSync as existsSync15, promises as fs15 } from "fs";
9102
- import path25 from "path";
9144
+ import { existsSync as existsSync15, promises as fs16 } from "fs";
9145
+ import path26 from "path";
9103
9146
  async function resolveCompletions(line, point, opts = {}) {
9104
9147
  const { prev, current } = parseCompletionLine(line, point);
9105
9148
  const ctx = { prev, current, opts };
@@ -9248,9 +9291,9 @@ function filterPrefix(values, fragment) {
9248
9291
  }
9249
9292
  async function listContainerNames(ctx) {
9250
9293
  const home = ctx.opts.monocerosHome ?? monocerosHome();
9251
- const dir = path25.join(home, "container-configs");
9294
+ const dir = path26.join(home, "container-configs");
9252
9295
  if (!existsSync15(dir)) return [];
9253
- const entries = await fs15.readdir(dir);
9296
+ const entries = await fs16.readdir(dir);
9254
9297
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
9255
9298
  }
9256
9299
  function containerNameFromCtx(ctx) {
@@ -9281,6 +9324,22 @@ async function listAppCandidates(ctx) {
9281
9324
  return [];
9282
9325
  }
9283
9326
  }
9327
+ async function listAppOrServiceCandidates(ctx) {
9328
+ const name = containerNameFromCtx(ctx);
9329
+ if (!name) return [];
9330
+ const apps = await listApps(name, ctx.opts.monocerosHome).catch(() => []);
9331
+ let services = [];
9332
+ try {
9333
+ const parsed = await readConfig(
9334
+ containerConfigPath(name, ctx.opts.monocerosHome)
9335
+ );
9336
+ services = solutionConfigToCreateOptions(parsed.config).services.map(
9337
+ (s) => s.name
9338
+ );
9339
+ } catch {
9340
+ }
9341
+ return [.../* @__PURE__ */ new Set([...apps, ...services])].sort();
9342
+ }
9284
9343
  async function listTargetCandidates(ctx) {
9285
9344
  const name = containerNameFromCtx(ctx);
9286
9345
  const app = positionalFromCtx(ctx, 1);
@@ -9297,7 +9356,7 @@ async function collectDirs(dir, maxDepth) {
9297
9356
  async function walk(at, rel, depth) {
9298
9357
  let entries;
9299
9358
  try {
9300
- entries = await fs15.readdir(at, { withFileTypes: true });
9359
+ entries = await fs16.readdir(at, { withFileTypes: true });
9301
9360
  } catch {
9302
9361
  return;
9303
9362
  }
@@ -9308,7 +9367,7 @@ async function collectDirs(dir, maxDepth) {
9308
9367
  const childRel = rel ? `${rel}/${e.name}` : e.name;
9309
9368
  out.push(childRel);
9310
9369
  if (depth + 1 < maxDepth) {
9311
- await walk(path25.join(at, e.name), childRel, depth + 1);
9370
+ await walk(path26.join(at, e.name), childRel, depth + 1);
9312
9371
  }
9313
9372
  }
9314
9373
  }
@@ -9316,9 +9375,9 @@ async function collectDirs(dir, maxDepth) {
9316
9375
  return out;
9317
9376
  }
9318
9377
  async function listContainerWorkspaceDirs(home, name) {
9319
- const root = path25.join(home, "container", name);
9378
+ const root = path26.join(home, "container", name);
9320
9379
  const top = await collectDirs(root, 1);
9321
- const projects = (await collectDirs(path25.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9380
+ const projects = (await collectDirs(path26.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9322
9381
  return [.../* @__PURE__ */ new Set([...top, ...projects])].sort();
9323
9382
  }
9324
9383
  async function listRunInDirs(ctx) {
@@ -9394,6 +9453,8 @@ var init_resolve = __esm({
9394
9453
  "src/completion/resolve.ts"() {
9395
9454
  "use strict";
9396
9455
  init_paths();
9456
+ init_io();
9457
+ init_transform();
9397
9458
  init_launch_config();
9398
9459
  init_components();
9399
9460
  init_manifest();
@@ -9500,7 +9561,9 @@ var init_resolve = __esm({
9500
9561
  "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9501
9562
  }
9502
9563
  },
9503
- status: { positionals: [containerName] },
9564
+ status: {
9565
+ positionals: [containerName, (ctx) => listAppOrServiceCandidates(ctx)]
9566
+ },
9504
9567
  "list-apps": { positionals: [containerName] },
9505
9568
  "add-language": {
9506
9569
  positionals: [containerName, () => listLanguageNames()]
@@ -10164,8 +10227,8 @@ var init_generator = __esm({
10164
10227
  });
10165
10228
 
10166
10229
  // src/init/index.ts
10167
- import { existsSync as existsSync16, promises as fs16 } from "fs";
10168
- import path26 from "path";
10230
+ import { existsSync as existsSync16, promises as fs17 } from "fs";
10231
+ import path27 from "path";
10169
10232
  import { consola as consola16 } from "consola";
10170
10233
  async function runInit(opts) {
10171
10234
  const home = opts.monocerosHome ?? monocerosHome();
@@ -10184,7 +10247,7 @@ async function runInit(opts) {
10184
10247
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
10185
10248
  );
10186
10249
  }
10187
- const componentsRoot = opts.workbenchRoot ? path26.join(opts.workbenchRoot, "components") : componentsRootDir();
10250
+ const componentsRoot = opts.workbenchRoot ? path27.join(opts.workbenchRoot, "components") : componentsRootDir();
10188
10251
  const catalog = await loadComponentCatalog(componentsRoot);
10189
10252
  if (catalog.size === 0) {
10190
10253
  throw new Error(
@@ -10250,9 +10313,9 @@ async function runInit(opts) {
10250
10313
  } else {
10251
10314
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
10252
10315
  }
10253
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
10316
+ await fs17.mkdir(containerConfigsDir(home), { recursive: true });
10254
10317
  await ensureEnvGitignored(containerConfigsDir(home));
10255
- await fs16.writeFile(dest, text, "utf8");
10318
+ await fs17.writeFile(dest, text, "utf8");
10256
10319
  const envPath = containerEnvPath(opts.name, home);
10257
10320
  const seedVars = {};
10258
10321
  for (const f of composed.features) {
@@ -10275,8 +10338,8 @@ async function runInit(opts) {
10275
10338
  }
10276
10339
  await ensureEnvVars(envPath, opts.name, seedVars);
10277
10340
  const documented = !anyComposed;
10278
- const ymlRel = path26.relative(home, dest);
10279
- const envRel = path26.relative(home, envPath);
10341
+ const ymlRel = path27.relative(home, dest);
10342
+ const envRel = path27.relative(home, envPath);
10280
10343
  if (documented) {
10281
10344
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
10282
10345
  logger.info(
@@ -10836,7 +10899,7 @@ var init_list_components = __esm({
10836
10899
 
10837
10900
  // src/commands/logs.ts
10838
10901
  import { spawn as spawn13 } from "child_process";
10839
- import path27 from "path";
10902
+ import path28 from "path";
10840
10903
  import { defineCommand as defineCommand16 } from "citty";
10841
10904
  import { consola as consola20 } from "consola";
10842
10905
  function tailLogFile(file, follow) {
@@ -10891,7 +10954,7 @@ var init_logs = __esm({
10891
10954
  const cfg = await readLaunchConfig(args.name, app);
10892
10955
  if (cfg) {
10893
10956
  const t = resolveTarget(cfg, target, app);
10894
- const logFile = path27.join(
10957
+ const logFile = path28.join(
10895
10958
  containerLogsDir(args.name),
10896
10959
  app,
10897
10960
  `${t.name}.log`
@@ -11140,8 +11203,8 @@ var init_remove_feature = __esm({
11140
11203
  });
11141
11204
 
11142
11205
  // src/remove/index.ts
11143
- import { existsSync as existsSync17, promises as fs17 } from "fs";
11144
- import path28 from "path";
11206
+ import { existsSync as existsSync17, promises as fs18 } from "fs";
11207
+ import path29 from "path";
11145
11208
  import { consola as consola24 } from "consola";
11146
11209
  async function runRemove(opts) {
11147
11210
  const home = opts.monocerosHome ?? monocerosHome();
@@ -11186,16 +11249,16 @@ async function runRemove(opts) {
11186
11249
  let backupPath = null;
11187
11250
  if (!opts.noBackup && (hasYml || hasContainer)) {
11188
11251
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11189
- backupPath = path28.join(home, "container-backups", `${opts.name}-${ts}`);
11190
- await fs17.mkdir(backupPath, { recursive: true });
11252
+ backupPath = path29.join(home, "container-backups", `${opts.name}-${ts}`);
11253
+ await fs18.mkdir(backupPath, { recursive: true });
11191
11254
  if (hasYml) {
11192
- await fs17.copyFile(ymlPath, path28.join(backupPath, `${opts.name}.yml`));
11255
+ await fs18.copyFile(ymlPath, path29.join(backupPath, `${opts.name}.yml`));
11193
11256
  }
11194
11257
  if (hasEnv) {
11195
- await fs17.copyFile(envPath, path28.join(backupPath, `${opts.name}.env`));
11258
+ await fs18.copyFile(envPath, path29.join(backupPath, `${opts.name}.env`));
11196
11259
  }
11197
11260
  if (hasContainer) {
11198
- await fs17.cp(containerPath, path28.join(backupPath, "container"), {
11261
+ await fs18.cp(containerPath, path29.join(backupPath, "container"), {
11199
11262
  recursive: true
11200
11263
  });
11201
11264
  }
@@ -11205,14 +11268,14 @@ async function runRemove(opts) {
11205
11268
  await stopBridgeDaemon(containerPath);
11206
11269
  }
11207
11270
  if (hasYml) {
11208
- await fs17.rm(ymlPath, { force: true });
11271
+ await fs18.rm(ymlPath, { force: true });
11209
11272
  }
11210
11273
  if (hasEnv) {
11211
- await fs17.rm(envPath, { force: true });
11274
+ await fs18.rm(envPath, { force: true });
11212
11275
  }
11213
11276
  if (hasContainer) {
11214
11277
  try {
11215
- await fs17.rm(containerPath, { recursive: true, force: true });
11278
+ await fs18.rm(containerPath, { recursive: true, force: true });
11216
11279
  } catch (err) {
11217
11280
  const code = err.code;
11218
11281
  if (code !== "EACCES" && code !== "EPERM") {
@@ -11234,7 +11297,7 @@ async function runRemove(opts) {
11234
11297
  "-delete"
11235
11298
  ]);
11236
11299
  try {
11237
- await fs17.rm(containerPath, { recursive: true, force: true });
11300
+ await fs18.rm(containerPath, { recursive: true, force: true });
11238
11301
  } catch (err2) {
11239
11302
  const code2 = err2.code;
11240
11303
  throw new Error(
@@ -11369,8 +11432,8 @@ var init_remove2 = __esm({
11369
11432
  });
11370
11433
 
11371
11434
  // src/restore/index.ts
11372
- import { existsSync as existsSync18, promises as fs18 } from "fs";
11373
- import path29 from "path";
11435
+ import { existsSync as existsSync18, promises as fs19 } from "fs";
11436
+ import path30 from "path";
11374
11437
  import { consola as consola26 } from "consola";
11375
11438
  async function runRestore(opts) {
11376
11439
  const home = opts.monocerosHome ?? monocerosHome();
@@ -11378,15 +11441,15 @@ async function runRestore(opts) {
11378
11441
  info: (msg) => consola26.info(msg),
11379
11442
  success: (msg) => consola26.success(msg)
11380
11443
  };
11381
- const backup = path29.resolve(opts.backupPath);
11444
+ const backup = path30.resolve(opts.backupPath);
11382
11445
  if (!existsSync18(backup)) {
11383
11446
  throw new Error(`Backup not found: ${backup}.`);
11384
11447
  }
11385
- const stat = await fs18.stat(backup);
11448
+ const stat = await fs19.stat(backup);
11386
11449
  if (!stat.isDirectory()) {
11387
11450
  throw new Error(`Backup path is not a directory: ${backup}.`);
11388
11451
  }
11389
- const entries = await fs18.readdir(backup);
11452
+ const entries = await fs19.readdir(backup);
11390
11453
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
11391
11454
  if (ymlFiles.length === 0) {
11392
11455
  throw new Error(
@@ -11400,9 +11463,9 @@ async function runRestore(opts) {
11400
11463
  }
11401
11464
  const ymlFile = ymlFiles[0];
11402
11465
  const name = ymlFile.replace(/\.yml$/, "");
11403
- const containerInBackup = path29.join(backup, "container");
11466
+ const containerInBackup = path30.join(backup, "container");
11404
11467
  const hasContainer = existsSync18(containerInBackup);
11405
- const envInBackup = path29.join(backup, `${name}.env`);
11468
+ const envInBackup = path30.join(backup, `${name}.env`);
11406
11469
  const hasEnv = existsSync18(envInBackup);
11407
11470
  const destYml = containerConfigPath(name, home);
11408
11471
  const destContainer = containerDir(name, home);
@@ -11416,13 +11479,13 @@ async function runRestore(opts) {
11416
11479
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
11417
11480
  );
11418
11481
  }
11419
- await fs18.mkdir(containerConfigsDir(home), { recursive: true });
11420
- await fs18.copyFile(path29.join(backup, ymlFile), destYml);
11482
+ await fs19.mkdir(containerConfigsDir(home), { recursive: true });
11483
+ await fs19.copyFile(path30.join(backup, ymlFile), destYml);
11421
11484
  if (hasEnv) {
11422
- await fs18.copyFile(envInBackup, containerEnvPath(name, home));
11485
+ await fs19.copyFile(envInBackup, containerEnvPath(name, home));
11423
11486
  }
11424
11487
  if (hasContainer) {
11425
- await fs18.cp(containerInBackup, destContainer, { recursive: true });
11488
+ await fs19.cp(containerInBackup, destContainer, { recursive: true });
11426
11489
  }
11427
11490
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
11428
11491
  logger.info(
@@ -11728,14 +11791,14 @@ var init_remove_service = __esm({
11728
11791
 
11729
11792
  // src/devcontainer/claude-trust.ts
11730
11793
  import { existsSync as existsSync19, promises as fsp7 } from "fs";
11731
- import path30 from "path";
11794
+ import path31 from "path";
11732
11795
  function resolveContainerCwd(name, cwd) {
11733
11796
  const workspace = `/workspaces/${name}`;
11734
11797
  if (!cwd) return workspace;
11735
- return path30.posix.isAbsolute(cwd) ? cwd : path30.posix.join(workspace, cwd);
11798
+ return path31.posix.isAbsolute(cwd) ? cwd : path31.posix.join(workspace, cwd);
11736
11799
  }
11737
11800
  async function preApproveClaudeProject(opts) {
11738
- const file = path30.join(opts.root, "home", ".claude.json");
11801
+ const file = path31.join(opts.root, "home", ".claude.json");
11739
11802
  if (!existsSync19(file)) return;
11740
11803
  try {
11741
11804
  const raw = await fsp7.readFile(file, "utf8");
@@ -11916,34 +11979,6 @@ var init_shell2 = __esm({
11916
11979
  }
11917
11980
  });
11918
11981
 
11919
- // src/devcontainer/app-control.ts
11920
- function findRunningContainer(name, opts = {}) {
11921
- return findRunningContainerByLocalFolder(containerDir(name), {
11922
- ...opts.docker ? { docker: opts.docker } : {}
11923
- });
11924
- }
11925
- async function runAppCtl(name, ctlArgs2, opts = {}) {
11926
- const id = await findRunningContainer(name, opts);
11927
- if (!id) {
11928
- throw new Error(
11929
- `Container "${name}" is not running. Run \`monoceros start ${name}\` first.`
11930
- );
11931
- }
11932
- const exec = opts.exec ?? realContainerExec;
11933
- const result = await exec(id, ["monoceros-ctl", ...ctlArgs2]);
11934
- return result.exitCode;
11935
- }
11936
- function ctlArgs(sub, app, target, extra = []) {
11937
- return [sub, app, ...target ? ["--target", target] : [], ...extra];
11938
- }
11939
- var init_app_control = __esm({
11940
- "src/devcontainer/app-control.ts"() {
11941
- "use strict";
11942
- init_paths();
11943
- init_locate_running();
11944
- }
11945
- });
11946
-
11947
11982
  // src/commands/start.ts
11948
11983
  import { defineCommand as defineCommand30 } from "citty";
11949
11984
  import { consola as consola35 } from "consola";
@@ -12076,20 +12111,360 @@ var init_start = __esm({
12076
12111
  }
12077
12112
  });
12078
12113
 
12114
+ // src/status/index.ts
12115
+ function shortFeatureName2(ref) {
12116
+ const withoutTag = ref.replace(/:[^:/@]+$/, "");
12117
+ const idx = withoutTag.lastIndexOf("/");
12118
+ return idx >= 0 ? withoutTag.slice(idx + 1) : withoutTag;
12119
+ }
12120
+ function cols(line) {
12121
+ return line.split(" ");
12122
+ }
12123
+ function nonEmptyLines(s) {
12124
+ return s.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
12125
+ }
12126
+ async function gatherStatus(name, opts = {}) {
12127
+ const docker = opts.docker ?? realDockerLookup;
12128
+ const root = containerDir(name, opts.home);
12129
+ let runtimeVersion;
12130
+ let languages = [];
12131
+ let features = [];
12132
+ let declaredServices = [];
12133
+ let declaredPorts = [];
12134
+ let configured = false;
12135
+ try {
12136
+ const parsed = await readConfig(containerConfigPath(name, opts.home));
12137
+ const createOpts = solutionConfigToCreateOptions(parsed.config);
12138
+ configured = true;
12139
+ runtimeVersion = createOpts.runtimeVersion;
12140
+ languages = createOpts.languages;
12141
+ features = Object.keys(createOpts.features ?? {}).map(shortFeatureName2);
12142
+ declaredServices = createOpts.services.map((s) => ({
12143
+ name: s.name,
12144
+ ...typeof s.port === "number" ? { port: s.port } : {}
12145
+ }));
12146
+ declaredPorts = createOpts.ports ?? [];
12147
+ } catch {
12148
+ }
12149
+ let container = {
12150
+ exists: false,
12151
+ running: false,
12152
+ status: "",
12153
+ dockerName: ""
12154
+ };
12155
+ let containerId = "";
12156
+ let composeProject = "";
12157
+ const cps = await docker([
12158
+ "ps",
12159
+ "-a",
12160
+ "--no-trunc",
12161
+ "--filter",
12162
+ `label=devcontainer.local_folder=${root}`,
12163
+ "--format",
12164
+ '{{.ID}} {{.State}} {{.Status}} {{.Names}} {{.Label "com.docker.compose.project"}}'
12165
+ ]);
12166
+ const row2 = nonEmptyLines(cps.stdout)[0];
12167
+ if (row2) {
12168
+ const [id, state, status, names, project] = cols(row2);
12169
+ containerId = id ?? "";
12170
+ composeProject = project ?? "";
12171
+ container = {
12172
+ exists: true,
12173
+ running: state === "running",
12174
+ status: status ?? "",
12175
+ dockerName: names ?? ""
12176
+ };
12177
+ }
12178
+ const services = [];
12179
+ if (declaredServices.length > 0) {
12180
+ const live = /* @__PURE__ */ new Map();
12181
+ if (composeProject) {
12182
+ const sps = await docker([
12183
+ "ps",
12184
+ "-a",
12185
+ "--filter",
12186
+ `label=com.docker.compose.project=${composeProject}`,
12187
+ "--format",
12188
+ '{{.Label "com.docker.compose.service"}} {{.State}} {{.Status}}'
12189
+ ]);
12190
+ for (const l of nonEmptyLines(sps.stdout)) {
12191
+ const [svc, state, status] = cols(l);
12192
+ if (svc)
12193
+ live.set(svc, { running: state === "running", status: status ?? "" });
12194
+ }
12195
+ }
12196
+ for (const s of declaredServices) {
12197
+ const m = live.get(s.name);
12198
+ services.push({
12199
+ name: s.name,
12200
+ running: m?.running ?? false,
12201
+ status: m?.status ?? "not created",
12202
+ ...typeof s.port === "number" ? { port: s.port } : {}
12203
+ });
12204
+ }
12205
+ }
12206
+ const apps = [];
12207
+ for (const app of await listApps(name, opts.home)) {
12208
+ const cfg = await readLaunchConfig(name, app, opts.home);
12209
+ if (!cfg) continue;
12210
+ for (const t of cfg.configurations) {
12211
+ apps.push({
12212
+ app,
12213
+ target: t.name,
12214
+ port: typeof t.port === "number" ? t.port : null,
12215
+ default: t.default === true,
12216
+ running: void 0,
12217
+ pid: null
12218
+ });
12219
+ }
12220
+ }
12221
+ const appsSupported = runtimeSupportsAppStatus(runtimeVersion);
12222
+ let appStateKnown = container.running && appsSupported && containerId !== "";
12223
+ let appStateNote;
12224
+ if (appStateKnown) {
12225
+ const res = await docker([
12226
+ "exec",
12227
+ containerId,
12228
+ "monoceros-ctl",
12229
+ "list",
12230
+ "--json"
12231
+ ]);
12232
+ if (res.exitCode === 0) {
12233
+ const byKey = /* @__PURE__ */ new Map();
12234
+ for (const l of nonEmptyLines(res.stdout)) {
12235
+ try {
12236
+ const o = JSON.parse(l);
12237
+ byKey.set(`${o.app}\0${o.target}`, {
12238
+ running: !!o.running,
12239
+ pid: o.pid ?? null
12240
+ });
12241
+ } catch {
12242
+ }
12243
+ }
12244
+ for (const a of apps) {
12245
+ const o = byKey.get(`${a.app}\0${a.target}`);
12246
+ if (o) {
12247
+ a.running = o.running;
12248
+ a.pid = o.pid;
12249
+ } else {
12250
+ a.running = false;
12251
+ }
12252
+ }
12253
+ } else {
12254
+ appStateKnown = false;
12255
+ appStateNote = `could not read run state from the runner - rebuild the runtime image, then \`monoceros upgrade ${name}\``;
12256
+ }
12257
+ }
12258
+ if (apps.length > 0 && !appStateKnown && !appStateNote) {
12259
+ appStateNote = !container.running ? `start the container to see run state (\`monoceros start ${name}\`)` : `run state needs runtime 1.6.0+ (this one: ${runtimeVersion ?? "unpinned"})`;
12260
+ }
12261
+ let hostPort = 80;
12262
+ try {
12263
+ hostPort = proxyHostPort(
12264
+ await readMonocerosConfig({
12265
+ ...opts.home ? { monocerosHome: opts.home } : {}
12266
+ })
12267
+ );
12268
+ } catch {
12269
+ }
12270
+ const ports = proxyUrlsFor(name, declaredPorts, hostPort);
12271
+ return {
12272
+ name,
12273
+ configured,
12274
+ container,
12275
+ services,
12276
+ apps,
12277
+ appStateKnown,
12278
+ ...appStateNote ? { appStateNote } : {},
12279
+ ports,
12280
+ builtIn: { languages, features }
12281
+ };
12282
+ }
12283
+ function row(p, marker, name, detail, pad, indent = 2) {
12284
+ const gap = Math.max(2, pad - name.length + 2);
12285
+ return `${" ".repeat(indent)}${marker} ${p.cyan(name)}${" ".repeat(gap)}${detail}`;
12286
+ }
12287
+ function liveMarker(p, running) {
12288
+ if (running === void 0) return " ";
12289
+ return running ? p.green("\u2713") : p.dim("\xB7");
12290
+ }
12291
+ function renderContainer(p, m) {
12292
+ const out = [p.sectionLine(m.name)];
12293
+ if (!m.container.exists) {
12294
+ out.push(
12295
+ row(
12296
+ p,
12297
+ p.dim("\xB7"),
12298
+ "(not created)",
12299
+ p.dim(`run \`monoceros apply ${m.name}\``),
12300
+ 14
12301
+ )
12302
+ );
12303
+ return out;
12304
+ }
12305
+ const detail = m.container.running ? p.dim(m.container.status || "running") : p.dim(m.container.status || "stopped");
12306
+ out.push(
12307
+ row(
12308
+ p,
12309
+ liveMarker(p, m.container.running),
12310
+ m.container.dockerName,
12311
+ detail,
12312
+ 14
12313
+ )
12314
+ );
12315
+ return out;
12316
+ }
12317
+ function renderServices(p, m) {
12318
+ if (m.services.length === 0) return [];
12319
+ const pad = Math.max(...m.services.map((s) => s.name.length));
12320
+ const out = ["", p.sectionLine("Services")];
12321
+ for (const s of m.services) {
12322
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12323
+ out.push(row(p, liveMarker(p, s.running), s.name, detail, pad));
12324
+ }
12325
+ return out;
12326
+ }
12327
+ function appTargetDetail(p, name, t) {
12328
+ const bits = [];
12329
+ if (t.running === true) {
12330
+ bits.push(
12331
+ t.port !== null ? p.dim(`http://${name}-${t.port}.localhost`) : p.dim("running")
12332
+ );
12333
+ if (t.pid !== null) bits.push(p.dim(`pid ${t.pid}`));
12334
+ } else if (t.running === false) {
12335
+ bits.push(p.dim("stopped"));
12336
+ }
12337
+ if (t.default) bits.push(p.dim("(default)"));
12338
+ return bits.join(" ");
12339
+ }
12340
+ function renderApps(p, m) {
12341
+ if (m.apps.length === 0) return [];
12342
+ const out = ["", p.sectionLine("Apps")];
12343
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12344
+ const byApp = /* @__PURE__ */ new Map();
12345
+ for (const a of m.apps) {
12346
+ const list = byApp.get(a.app) ?? [];
12347
+ list.push(a);
12348
+ byApp.set(a.app, list);
12349
+ }
12350
+ for (const [app, targets] of byApp) {
12351
+ out.push(` ${p.cyan(app)}`);
12352
+ const pad = Math.max(...targets.map((t) => t.target.length));
12353
+ for (const t of targets) {
12354
+ out.push(
12355
+ row(
12356
+ p,
12357
+ liveMarker(p, t.running),
12358
+ t.target,
12359
+ appTargetDetail(p, m.name, t),
12360
+ pad,
12361
+ 4
12362
+ )
12363
+ );
12364
+ }
12365
+ }
12366
+ return out;
12367
+ }
12368
+ function renderPorts(p, m) {
12369
+ if (m.ports.length === 0) return [];
12370
+ const pad = Math.max(...m.ports.map((r) => String(r.port).length));
12371
+ const out = ["", p.sectionLine("Ports")];
12372
+ for (const r of m.ports) {
12373
+ const hosts = r.isDefault ? `${m.name}.localhost \xB7 ${m.name}-${r.port}.localhost` : `${m.name}-${r.port}.localhost`;
12374
+ out.push(row(p, " ", String(r.port), p.dim(hosts), pad));
12375
+ }
12376
+ return out;
12377
+ }
12378
+ function renderBuiltIn(p, m) {
12379
+ const lines = [];
12380
+ if (m.builtIn.languages.length > 0)
12381
+ lines.push({ label: "Languages", values: m.builtIn.languages });
12382
+ if (m.builtIn.features.length > 0)
12383
+ lines.push({ label: "Features", values: m.builtIn.features });
12384
+ if (lines.length === 0) return [];
12385
+ const labelWidth = Math.max(...lines.map((l) => l.label.length));
12386
+ const out = ["", `${p.sectionLine("Built in")} ${p.dim("from the yml")}`];
12387
+ for (const l of lines) {
12388
+ out.push(` ${l.label.padEnd(labelWidth)} ${p.cyan(l.values.join(", "))}`);
12389
+ }
12390
+ return out;
12391
+ }
12392
+ function renderStatus(m, p) {
12393
+ return [
12394
+ ...renderContainer(p, m),
12395
+ ...renderServices(p, m),
12396
+ ...renderApps(p, m),
12397
+ ...renderPorts(p, m),
12398
+ ...renderBuiltIn(p, m)
12399
+ ].join("\n");
12400
+ }
12401
+ function renderApp(m, app, p) {
12402
+ const targets = m.apps.filter((a) => a.app === app);
12403
+ if (targets.length === 0) {
12404
+ const known = [...new Set(m.apps.map((a) => a.app))];
12405
+ throw new Error(
12406
+ `No app "${app}" in ${m.name} (have: ${known.join(", ") || "none"}).`
12407
+ );
12408
+ }
12409
+ const out = [p.sectionLine(app)];
12410
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12411
+ const pad = Math.max(...targets.map((t) => t.target.length));
12412
+ for (const t of targets) {
12413
+ out.push(
12414
+ row(
12415
+ p,
12416
+ liveMarker(p, t.running),
12417
+ t.target,
12418
+ appTargetDetail(p, m.name, t),
12419
+ pad,
12420
+ 2
12421
+ )
12422
+ );
12423
+ }
12424
+ return out.join("\n");
12425
+ }
12426
+ function renderService(m, name, p) {
12427
+ const s = m.services.find((svc) => svc.name === name);
12428
+ if (!s) {
12429
+ throw new Error(
12430
+ `No service "${name}" in ${m.name} (have: ${m.services.map((x) => x.name).join(", ") || "none"}).`
12431
+ );
12432
+ }
12433
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12434
+ return [
12435
+ p.sectionLine("Services"),
12436
+ row(p, liveMarker(p, s.running), s.name, detail, s.name.length)
12437
+ ].join("\n");
12438
+ }
12439
+ var init_status = __esm({
12440
+ "src/status/index.ts"() {
12441
+ "use strict";
12442
+ init_io();
12443
+ init_paths();
12444
+ init_global();
12445
+ init_transform();
12446
+ init_launch_config();
12447
+ init_catalog();
12448
+ init_locate_running();
12449
+ init_dynamic();
12450
+ init_format();
12451
+ }
12452
+ });
12453
+
12079
12454
  // src/commands/status.ts
12080
12455
  import { defineCommand as defineCommand31 } from "citty";
12081
12456
  var statusCommand;
12082
- var init_status = __esm({
12457
+ var init_status2 = __esm({
12083
12458
  "src/commands/status.ts"() {
12084
12459
  "use strict";
12085
- init_paths();
12086
- init_compose();
12460
+ init_status();
12461
+ init_format();
12087
12462
  init_dispatch();
12088
12463
  statusCommand = defineCommand31({
12089
12464
  meta: {
12090
12465
  name: "status",
12091
12466
  group: "run",
12092
- description: "Show whether the compose services for the named dev-container are running."
12467
+ description: "Show the dev-container at a glance: container, services and apps (up/down), the ports it routes, and what the yml built in. With an <app> (or a service name), narrow to just that."
12093
12468
  },
12094
12469
  args: {
12095
12470
  name: {
@@ -12097,18 +12472,29 @@ var init_status = __esm({
12097
12472
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
12098
12473
  required: true
12099
12474
  },
12100
- service: {
12101
- type: "string",
12102
- description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
12475
+ app: {
12476
+ type: "positional",
12477
+ description: "Narrow to one app (a path under projects/ with .monoceros/launch.json) or one compose service (e.g. postgres). Omit for the whole stack.",
12478
+ required: false
12103
12479
  }
12104
12480
  },
12105
12481
  run({ args }) {
12106
- return dispatch(
12107
- () => runStatus({
12108
- root: containerDir(args.name),
12109
- ...typeof args.service === "string" ? { service: args.service } : {}
12110
- })
12111
- );
12482
+ const filter = typeof args.app === "string" ? args.app : void 0;
12483
+ return dispatch(async () => {
12484
+ const model = await gatherStatus(args.name);
12485
+ const p = colorsFor(process.stdout);
12486
+ let block;
12487
+ if (filter) {
12488
+ const isApp = model.apps.some((a) => a.app === filter);
12489
+ block = isApp ? renderApp(model, filter, p) : renderService(model, filter, p);
12490
+ } else {
12491
+ block = renderStatus(model, p);
12492
+ }
12493
+ process.stdout.write(`
12494
+ ${block}
12495
+ `);
12496
+ return 0;
12497
+ });
12112
12498
  }
12113
12499
  });
12114
12500
  }
@@ -12189,7 +12575,7 @@ var init_stop = __esm({
12189
12575
 
12190
12576
  // src/tunnel/resolve.ts
12191
12577
  import { existsSync as existsSync20 } from "fs";
12192
- import path31 from "path";
12578
+ import path32 from "path";
12193
12579
  async function resolveTunnelTarget(opts) {
12194
12580
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
12195
12581
  if (!existsSync20(ymlPath)) {
@@ -12205,7 +12591,7 @@ async function resolveTunnelTarget(opts) {
12205
12591
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
12206
12592
  );
12207
12593
  }
12208
- const composePath = path31.join(containerRoot, ".devcontainer", "compose.yaml");
12594
+ const composePath = path32.join(containerRoot, ".devcontainer", "compose.yaml");
12209
12595
  const isCompose = existsSync20(composePath);
12210
12596
  const parsedTarget = parseTargetArg(opts.target, config);
12211
12597
  const docker = opts.docker ?? defaultDockerExec;
@@ -12686,7 +13072,7 @@ var init_prune = __esm({
12686
13072
  });
12687
13073
 
12688
13074
  // src/upgrade/index.ts
12689
- import { existsSync as existsSync21, promises as fs19 } from "fs";
13075
+ import { existsSync as existsSync21, promises as fs20 } from "fs";
12690
13076
  import { consola as consola39 } from "consola";
12691
13077
  async function fetchRuntimeVersions() {
12692
13078
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
@@ -12803,10 +13189,10 @@ async function runUpgrade(opts) {
12803
13189
  for (const name of targets) {
12804
13190
  const ymlPath = containerConfigPath(name, home);
12805
13191
  if (!existsSync21(ymlPath)) continue;
12806
- const raw = await fs19.readFile(ymlPath, "utf8");
13192
+ const raw = await fs20.readFile(ymlPath, "utf8");
12807
13193
  const updated = setRuntimeVersion(raw, pinVersion);
12808
13194
  if (updated !== raw) {
12809
- await fs19.writeFile(ymlPath, updated);
13195
+ await fs20.writeFile(ymlPath, updated);
12810
13196
  bumped += 1;
12811
13197
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
12812
13198
  }
@@ -12848,7 +13234,7 @@ function formatPruneLine(prune) {
12848
13234
  }
12849
13235
  async function listContainerNames2(home) {
12850
13236
  try {
12851
- const entries = await fs19.readdir(containerConfigsDir(home));
13237
+ const entries = await fs20.readdir(containerConfigsDir(home));
12852
13238
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
12853
13239
  } catch {
12854
13240
  return [];
@@ -12956,7 +13342,7 @@ var init_main = __esm({
12956
13342
  init_run2();
12957
13343
  init_shell2();
12958
13344
  init_start();
12959
- init_status();
13345
+ init_status2();
12960
13346
  init_stop();
12961
13347
  init_tunnel();
12962
13348
  init_upgrade2();
@@ -13261,25 +13647,25 @@ function detectHelpRequest(argv, main2) {
13261
13647
  const separatorIdx = argv.indexOf("--");
13262
13648
  if (helpIdx === -1) return null;
13263
13649
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
13264
- const path32 = [];
13650
+ const path33 = [];
13265
13651
  const tokens = argv.slice(
13266
13652
  0,
13267
13653
  separatorIdx === -1 ? argv.length : separatorIdx
13268
13654
  );
13269
13655
  let cursor = main2;
13270
13656
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
13271
- path32.push(mainName);
13657
+ path33.push(mainName);
13272
13658
  for (const tok of tokens) {
13273
13659
  if (tok.startsWith("-")) continue;
13274
13660
  const subs = cursor.subCommands ?? {};
13275
13661
  if (tok in subs) {
13276
13662
  cursor = subs[tok];
13277
- path32.push(tok);
13663
+ path33.push(tok);
13278
13664
  continue;
13279
13665
  }
13280
13666
  break;
13281
13667
  }
13282
- return { path: path32, cmd: cursor };
13668
+ return { path: path33, cmd: cursor };
13283
13669
  }
13284
13670
  async function maybeRenderHelp(argv, main2) {
13285
13671
  const hit = detectHelpRequest(argv, main2);