@getmonoceros/workbench 1.35.0 → 1.36.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
@@ -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.0" : "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) {
@@ -9297,7 +9340,7 @@ async function collectDirs(dir, maxDepth) {
9297
9340
  async function walk(at, rel, depth) {
9298
9341
  let entries;
9299
9342
  try {
9300
- entries = await fs15.readdir(at, { withFileTypes: true });
9343
+ entries = await fs16.readdir(at, { withFileTypes: true });
9301
9344
  } catch {
9302
9345
  return;
9303
9346
  }
@@ -9308,7 +9351,7 @@ async function collectDirs(dir, maxDepth) {
9308
9351
  const childRel = rel ? `${rel}/${e.name}` : e.name;
9309
9352
  out.push(childRel);
9310
9353
  if (depth + 1 < maxDepth) {
9311
- await walk(path25.join(at, e.name), childRel, depth + 1);
9354
+ await walk(path26.join(at, e.name), childRel, depth + 1);
9312
9355
  }
9313
9356
  }
9314
9357
  }
@@ -9316,9 +9359,9 @@ async function collectDirs(dir, maxDepth) {
9316
9359
  return out;
9317
9360
  }
9318
9361
  async function listContainerWorkspaceDirs(home, name) {
9319
- const root = path25.join(home, "container", name);
9362
+ const root = path26.join(home, "container", name);
9320
9363
  const top = await collectDirs(root, 1);
9321
- const projects = (await collectDirs(path25.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9364
+ const projects = (await collectDirs(path26.join(root, "projects"), PROJECTS_DIR_MAX_DEPTH)).map((p) => `projects/${p}`);
9322
9365
  return [.../* @__PURE__ */ new Set([...top, ...projects])].sort();
9323
9366
  }
9324
9367
  async function listRunInDirs(ctx) {
@@ -10164,8 +10207,8 @@ var init_generator = __esm({
10164
10207
  });
10165
10208
 
10166
10209
  // src/init/index.ts
10167
- import { existsSync as existsSync16, promises as fs16 } from "fs";
10168
- import path26 from "path";
10210
+ import { existsSync as existsSync16, promises as fs17 } from "fs";
10211
+ import path27 from "path";
10169
10212
  import { consola as consola16 } from "consola";
10170
10213
  async function runInit(opts) {
10171
10214
  const home = opts.monocerosHome ?? monocerosHome();
@@ -10184,7 +10227,7 @@ async function runInit(opts) {
10184
10227
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
10185
10228
  );
10186
10229
  }
10187
- const componentsRoot = opts.workbenchRoot ? path26.join(opts.workbenchRoot, "components") : componentsRootDir();
10230
+ const componentsRoot = opts.workbenchRoot ? path27.join(opts.workbenchRoot, "components") : componentsRootDir();
10188
10231
  const catalog = await loadComponentCatalog(componentsRoot);
10189
10232
  if (catalog.size === 0) {
10190
10233
  throw new Error(
@@ -10250,9 +10293,9 @@ async function runInit(opts) {
10250
10293
  } else {
10251
10294
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
10252
10295
  }
10253
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
10296
+ await fs17.mkdir(containerConfigsDir(home), { recursive: true });
10254
10297
  await ensureEnvGitignored(containerConfigsDir(home));
10255
- await fs16.writeFile(dest, text, "utf8");
10298
+ await fs17.writeFile(dest, text, "utf8");
10256
10299
  const envPath = containerEnvPath(opts.name, home);
10257
10300
  const seedVars = {};
10258
10301
  for (const f of composed.features) {
@@ -10275,8 +10318,8 @@ async function runInit(opts) {
10275
10318
  }
10276
10319
  await ensureEnvVars(envPath, opts.name, seedVars);
10277
10320
  const documented = !anyComposed;
10278
- const ymlRel = path26.relative(home, dest);
10279
- const envRel = path26.relative(home, envPath);
10321
+ const ymlRel = path27.relative(home, dest);
10322
+ const envRel = path27.relative(home, envPath);
10280
10323
  if (documented) {
10281
10324
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
10282
10325
  logger.info(
@@ -10836,7 +10879,7 @@ var init_list_components = __esm({
10836
10879
 
10837
10880
  // src/commands/logs.ts
10838
10881
  import { spawn as spawn13 } from "child_process";
10839
- import path27 from "path";
10882
+ import path28 from "path";
10840
10883
  import { defineCommand as defineCommand16 } from "citty";
10841
10884
  import { consola as consola20 } from "consola";
10842
10885
  function tailLogFile(file, follow) {
@@ -10891,7 +10934,7 @@ var init_logs = __esm({
10891
10934
  const cfg = await readLaunchConfig(args.name, app);
10892
10935
  if (cfg) {
10893
10936
  const t = resolveTarget(cfg, target, app);
10894
- const logFile = path27.join(
10937
+ const logFile = path28.join(
10895
10938
  containerLogsDir(args.name),
10896
10939
  app,
10897
10940
  `${t.name}.log`
@@ -11140,8 +11183,8 @@ var init_remove_feature = __esm({
11140
11183
  });
11141
11184
 
11142
11185
  // src/remove/index.ts
11143
- import { existsSync as existsSync17, promises as fs17 } from "fs";
11144
- import path28 from "path";
11186
+ import { existsSync as existsSync17, promises as fs18 } from "fs";
11187
+ import path29 from "path";
11145
11188
  import { consola as consola24 } from "consola";
11146
11189
  async function runRemove(opts) {
11147
11190
  const home = opts.monocerosHome ?? monocerosHome();
@@ -11186,16 +11229,16 @@ async function runRemove(opts) {
11186
11229
  let backupPath = null;
11187
11230
  if (!opts.noBackup && (hasYml || hasContainer)) {
11188
11231
  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 });
11232
+ backupPath = path29.join(home, "container-backups", `${opts.name}-${ts}`);
11233
+ await fs18.mkdir(backupPath, { recursive: true });
11191
11234
  if (hasYml) {
11192
- await fs17.copyFile(ymlPath, path28.join(backupPath, `${opts.name}.yml`));
11235
+ await fs18.copyFile(ymlPath, path29.join(backupPath, `${opts.name}.yml`));
11193
11236
  }
11194
11237
  if (hasEnv) {
11195
- await fs17.copyFile(envPath, path28.join(backupPath, `${opts.name}.env`));
11238
+ await fs18.copyFile(envPath, path29.join(backupPath, `${opts.name}.env`));
11196
11239
  }
11197
11240
  if (hasContainer) {
11198
- await fs17.cp(containerPath, path28.join(backupPath, "container"), {
11241
+ await fs18.cp(containerPath, path29.join(backupPath, "container"), {
11199
11242
  recursive: true
11200
11243
  });
11201
11244
  }
@@ -11205,14 +11248,14 @@ async function runRemove(opts) {
11205
11248
  await stopBridgeDaemon(containerPath);
11206
11249
  }
11207
11250
  if (hasYml) {
11208
- await fs17.rm(ymlPath, { force: true });
11251
+ await fs18.rm(ymlPath, { force: true });
11209
11252
  }
11210
11253
  if (hasEnv) {
11211
- await fs17.rm(envPath, { force: true });
11254
+ await fs18.rm(envPath, { force: true });
11212
11255
  }
11213
11256
  if (hasContainer) {
11214
11257
  try {
11215
- await fs17.rm(containerPath, { recursive: true, force: true });
11258
+ await fs18.rm(containerPath, { recursive: true, force: true });
11216
11259
  } catch (err) {
11217
11260
  const code = err.code;
11218
11261
  if (code !== "EACCES" && code !== "EPERM") {
@@ -11234,7 +11277,7 @@ async function runRemove(opts) {
11234
11277
  "-delete"
11235
11278
  ]);
11236
11279
  try {
11237
- await fs17.rm(containerPath, { recursive: true, force: true });
11280
+ await fs18.rm(containerPath, { recursive: true, force: true });
11238
11281
  } catch (err2) {
11239
11282
  const code2 = err2.code;
11240
11283
  throw new Error(
@@ -11369,8 +11412,8 @@ var init_remove2 = __esm({
11369
11412
  });
11370
11413
 
11371
11414
  // src/restore/index.ts
11372
- import { existsSync as existsSync18, promises as fs18 } from "fs";
11373
- import path29 from "path";
11415
+ import { existsSync as existsSync18, promises as fs19 } from "fs";
11416
+ import path30 from "path";
11374
11417
  import { consola as consola26 } from "consola";
11375
11418
  async function runRestore(opts) {
11376
11419
  const home = opts.monocerosHome ?? monocerosHome();
@@ -11378,15 +11421,15 @@ async function runRestore(opts) {
11378
11421
  info: (msg) => consola26.info(msg),
11379
11422
  success: (msg) => consola26.success(msg)
11380
11423
  };
11381
- const backup = path29.resolve(opts.backupPath);
11424
+ const backup = path30.resolve(opts.backupPath);
11382
11425
  if (!existsSync18(backup)) {
11383
11426
  throw new Error(`Backup not found: ${backup}.`);
11384
11427
  }
11385
- const stat = await fs18.stat(backup);
11428
+ const stat = await fs19.stat(backup);
11386
11429
  if (!stat.isDirectory()) {
11387
11430
  throw new Error(`Backup path is not a directory: ${backup}.`);
11388
11431
  }
11389
- const entries = await fs18.readdir(backup);
11432
+ const entries = await fs19.readdir(backup);
11390
11433
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
11391
11434
  if (ymlFiles.length === 0) {
11392
11435
  throw new Error(
@@ -11400,9 +11443,9 @@ async function runRestore(opts) {
11400
11443
  }
11401
11444
  const ymlFile = ymlFiles[0];
11402
11445
  const name = ymlFile.replace(/\.yml$/, "");
11403
- const containerInBackup = path29.join(backup, "container");
11446
+ const containerInBackup = path30.join(backup, "container");
11404
11447
  const hasContainer = existsSync18(containerInBackup);
11405
- const envInBackup = path29.join(backup, `${name}.env`);
11448
+ const envInBackup = path30.join(backup, `${name}.env`);
11406
11449
  const hasEnv = existsSync18(envInBackup);
11407
11450
  const destYml = containerConfigPath(name, home);
11408
11451
  const destContainer = containerDir(name, home);
@@ -11416,13 +11459,13 @@ async function runRestore(opts) {
11416
11459
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
11417
11460
  );
11418
11461
  }
11419
- await fs18.mkdir(containerConfigsDir(home), { recursive: true });
11420
- await fs18.copyFile(path29.join(backup, ymlFile), destYml);
11462
+ await fs19.mkdir(containerConfigsDir(home), { recursive: true });
11463
+ await fs19.copyFile(path30.join(backup, ymlFile), destYml);
11421
11464
  if (hasEnv) {
11422
- await fs18.copyFile(envInBackup, containerEnvPath(name, home));
11465
+ await fs19.copyFile(envInBackup, containerEnvPath(name, home));
11423
11466
  }
11424
11467
  if (hasContainer) {
11425
- await fs18.cp(containerInBackup, destContainer, { recursive: true });
11468
+ await fs19.cp(containerInBackup, destContainer, { recursive: true });
11426
11469
  }
11427
11470
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
11428
11471
  logger.info(
@@ -11728,14 +11771,14 @@ var init_remove_service = __esm({
11728
11771
 
11729
11772
  // src/devcontainer/claude-trust.ts
11730
11773
  import { existsSync as existsSync19, promises as fsp7 } from "fs";
11731
- import path30 from "path";
11774
+ import path31 from "path";
11732
11775
  function resolveContainerCwd(name, cwd) {
11733
11776
  const workspace = `/workspaces/${name}`;
11734
11777
  if (!cwd) return workspace;
11735
- return path30.posix.isAbsolute(cwd) ? cwd : path30.posix.join(workspace, cwd);
11778
+ return path31.posix.isAbsolute(cwd) ? cwd : path31.posix.join(workspace, cwd);
11736
11779
  }
11737
11780
  async function preApproveClaudeProject(opts) {
11738
- const file = path30.join(opts.root, "home", ".claude.json");
11781
+ const file = path31.join(opts.root, "home", ".claude.json");
11739
11782
  if (!existsSync19(file)) return;
11740
11783
  try {
11741
11784
  const raw = await fsp7.readFile(file, "utf8");
@@ -11916,34 +11959,6 @@ var init_shell2 = __esm({
11916
11959
  }
11917
11960
  });
11918
11961
 
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
11962
  // src/commands/start.ts
11948
11963
  import { defineCommand as defineCommand30 } from "citty";
11949
11964
  import { consola as consola35 } from "consola";
@@ -12076,20 +12091,360 @@ var init_start = __esm({
12076
12091
  }
12077
12092
  });
12078
12093
 
12094
+ // src/status/index.ts
12095
+ function shortFeatureName2(ref) {
12096
+ const withoutTag = ref.replace(/:[^:/@]+$/, "");
12097
+ const idx = withoutTag.lastIndexOf("/");
12098
+ return idx >= 0 ? withoutTag.slice(idx + 1) : withoutTag;
12099
+ }
12100
+ function cols(line) {
12101
+ return line.split(" ");
12102
+ }
12103
+ function nonEmptyLines(s) {
12104
+ return s.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
12105
+ }
12106
+ async function gatherStatus(name, opts = {}) {
12107
+ const docker = opts.docker ?? realDockerLookup;
12108
+ const root = containerDir(name, opts.home);
12109
+ let runtimeVersion;
12110
+ let languages = [];
12111
+ let features = [];
12112
+ let declaredServices = [];
12113
+ let declaredPorts = [];
12114
+ let configured = false;
12115
+ try {
12116
+ const parsed = await readConfig(containerConfigPath(name, opts.home));
12117
+ const createOpts = solutionConfigToCreateOptions(parsed.config);
12118
+ configured = true;
12119
+ runtimeVersion = createOpts.runtimeVersion;
12120
+ languages = createOpts.languages;
12121
+ features = Object.keys(createOpts.features ?? {}).map(shortFeatureName2);
12122
+ declaredServices = createOpts.services.map((s) => ({
12123
+ name: s.name,
12124
+ ...typeof s.port === "number" ? { port: s.port } : {}
12125
+ }));
12126
+ declaredPorts = createOpts.ports ?? [];
12127
+ } catch {
12128
+ }
12129
+ let container = {
12130
+ exists: false,
12131
+ running: false,
12132
+ status: "",
12133
+ dockerName: ""
12134
+ };
12135
+ let containerId = "";
12136
+ let composeProject = "";
12137
+ const cps = await docker([
12138
+ "ps",
12139
+ "-a",
12140
+ "--no-trunc",
12141
+ "--filter",
12142
+ `label=devcontainer.local_folder=${root}`,
12143
+ "--format",
12144
+ '{{.ID}} {{.State}} {{.Status}} {{.Names}} {{.Label "com.docker.compose.project"}}'
12145
+ ]);
12146
+ const row2 = nonEmptyLines(cps.stdout)[0];
12147
+ if (row2) {
12148
+ const [id, state, status, names, project] = cols(row2);
12149
+ containerId = id ?? "";
12150
+ composeProject = project ?? "";
12151
+ container = {
12152
+ exists: true,
12153
+ running: state === "running",
12154
+ status: status ?? "",
12155
+ dockerName: names ?? ""
12156
+ };
12157
+ }
12158
+ const services = [];
12159
+ if (declaredServices.length > 0) {
12160
+ const live = /* @__PURE__ */ new Map();
12161
+ if (composeProject) {
12162
+ const sps = await docker([
12163
+ "ps",
12164
+ "-a",
12165
+ "--filter",
12166
+ `label=com.docker.compose.project=${composeProject}`,
12167
+ "--format",
12168
+ '{{.Label "com.docker.compose.service"}} {{.State}} {{.Status}}'
12169
+ ]);
12170
+ for (const l of nonEmptyLines(sps.stdout)) {
12171
+ const [svc, state, status] = cols(l);
12172
+ if (svc)
12173
+ live.set(svc, { running: state === "running", status: status ?? "" });
12174
+ }
12175
+ }
12176
+ for (const s of declaredServices) {
12177
+ const m = live.get(s.name);
12178
+ services.push({
12179
+ name: s.name,
12180
+ running: m?.running ?? false,
12181
+ status: m?.status ?? "not created",
12182
+ ...typeof s.port === "number" ? { port: s.port } : {}
12183
+ });
12184
+ }
12185
+ }
12186
+ const apps = [];
12187
+ for (const app of await listApps(name, opts.home)) {
12188
+ const cfg = await readLaunchConfig(name, app, opts.home);
12189
+ if (!cfg) continue;
12190
+ for (const t of cfg.configurations) {
12191
+ apps.push({
12192
+ app,
12193
+ target: t.name,
12194
+ port: typeof t.port === "number" ? t.port : null,
12195
+ default: t.default === true,
12196
+ running: void 0,
12197
+ pid: null
12198
+ });
12199
+ }
12200
+ }
12201
+ const appsSupported = runtimeSupportsAppStatus(runtimeVersion);
12202
+ let appStateKnown = container.running && appsSupported && containerId !== "";
12203
+ let appStateNote;
12204
+ if (appStateKnown) {
12205
+ const res = await docker([
12206
+ "exec",
12207
+ containerId,
12208
+ "monoceros-ctl",
12209
+ "list",
12210
+ "--json"
12211
+ ]);
12212
+ if (res.exitCode === 0) {
12213
+ const byKey = /* @__PURE__ */ new Map();
12214
+ for (const l of nonEmptyLines(res.stdout)) {
12215
+ try {
12216
+ const o = JSON.parse(l);
12217
+ byKey.set(`${o.app}\0${o.target}`, {
12218
+ running: !!o.running,
12219
+ pid: o.pid ?? null
12220
+ });
12221
+ } catch {
12222
+ }
12223
+ }
12224
+ for (const a of apps) {
12225
+ const o = byKey.get(`${a.app}\0${a.target}`);
12226
+ if (o) {
12227
+ a.running = o.running;
12228
+ a.pid = o.pid;
12229
+ } else {
12230
+ a.running = false;
12231
+ }
12232
+ }
12233
+ } else {
12234
+ appStateKnown = false;
12235
+ appStateNote = `could not read run state from the runner - rebuild the runtime image, then \`monoceros upgrade ${name}\``;
12236
+ }
12237
+ }
12238
+ if (apps.length > 0 && !appStateKnown && !appStateNote) {
12239
+ appStateNote = !container.running ? `start the container to see run state (\`monoceros start ${name}\`)` : `run state needs runtime 1.6.0+ (this one: ${runtimeVersion ?? "unpinned"})`;
12240
+ }
12241
+ let hostPort = 80;
12242
+ try {
12243
+ hostPort = proxyHostPort(
12244
+ await readMonocerosConfig({
12245
+ ...opts.home ? { monocerosHome: opts.home } : {}
12246
+ })
12247
+ );
12248
+ } catch {
12249
+ }
12250
+ const ports = proxyUrlsFor(name, declaredPorts, hostPort);
12251
+ return {
12252
+ name,
12253
+ configured,
12254
+ container,
12255
+ services,
12256
+ apps,
12257
+ appStateKnown,
12258
+ ...appStateNote ? { appStateNote } : {},
12259
+ ports,
12260
+ builtIn: { languages, features }
12261
+ };
12262
+ }
12263
+ function row(p, marker, name, detail, pad, indent = 2) {
12264
+ const gap = Math.max(2, pad - name.length + 2);
12265
+ return `${" ".repeat(indent)}${marker} ${p.cyan(name)}${" ".repeat(gap)}${detail}`;
12266
+ }
12267
+ function liveMarker(p, running) {
12268
+ if (running === void 0) return " ";
12269
+ return running ? p.green("\u2713") : p.dim("\xB7");
12270
+ }
12271
+ function renderContainer(p, m) {
12272
+ const out = [p.sectionLine(m.name)];
12273
+ if (!m.container.exists) {
12274
+ out.push(
12275
+ row(
12276
+ p,
12277
+ p.dim("\xB7"),
12278
+ "(not created)",
12279
+ p.dim(`run \`monoceros apply ${m.name}\``),
12280
+ 14
12281
+ )
12282
+ );
12283
+ return out;
12284
+ }
12285
+ const detail = m.container.running ? p.dim(m.container.status || "running") : p.dim(m.container.status || "stopped");
12286
+ out.push(
12287
+ row(
12288
+ p,
12289
+ liveMarker(p, m.container.running),
12290
+ m.container.dockerName,
12291
+ detail,
12292
+ 14
12293
+ )
12294
+ );
12295
+ return out;
12296
+ }
12297
+ function renderServices(p, m) {
12298
+ if (m.services.length === 0) return [];
12299
+ const pad = Math.max(...m.services.map((s) => s.name.length));
12300
+ const out = ["", p.sectionLine("Services")];
12301
+ for (const s of m.services) {
12302
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12303
+ out.push(row(p, liveMarker(p, s.running), s.name, detail, pad));
12304
+ }
12305
+ return out;
12306
+ }
12307
+ function appTargetDetail(p, name, t) {
12308
+ const bits = [];
12309
+ if (t.running === true) {
12310
+ bits.push(
12311
+ t.port !== null ? p.dim(`http://${name}-${t.port}.localhost`) : p.dim("running")
12312
+ );
12313
+ if (t.pid !== null) bits.push(p.dim(`pid ${t.pid}`));
12314
+ } else if (t.running === false) {
12315
+ bits.push(p.dim("stopped"));
12316
+ }
12317
+ if (t.default) bits.push(p.dim("(default)"));
12318
+ return bits.join(" ");
12319
+ }
12320
+ function renderApps(p, m) {
12321
+ if (m.apps.length === 0) return [];
12322
+ const out = ["", p.sectionLine("Apps")];
12323
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12324
+ const byApp = /* @__PURE__ */ new Map();
12325
+ for (const a of m.apps) {
12326
+ const list = byApp.get(a.app) ?? [];
12327
+ list.push(a);
12328
+ byApp.set(a.app, list);
12329
+ }
12330
+ for (const [app, targets] of byApp) {
12331
+ out.push(` ${p.cyan(app)}`);
12332
+ const pad = Math.max(...targets.map((t) => t.target.length));
12333
+ for (const t of targets) {
12334
+ out.push(
12335
+ row(
12336
+ p,
12337
+ liveMarker(p, t.running),
12338
+ t.target,
12339
+ appTargetDetail(p, m.name, t),
12340
+ pad,
12341
+ 4
12342
+ )
12343
+ );
12344
+ }
12345
+ }
12346
+ return out;
12347
+ }
12348
+ function renderPorts(p, m) {
12349
+ if (m.ports.length === 0) return [];
12350
+ const pad = Math.max(...m.ports.map((r) => String(r.port).length));
12351
+ const out = ["", p.sectionLine("Ports")];
12352
+ for (const r of m.ports) {
12353
+ const hosts = r.isDefault ? `${m.name}.localhost \xB7 ${m.name}-${r.port}.localhost` : `${m.name}-${r.port}.localhost`;
12354
+ out.push(row(p, " ", String(r.port), p.dim(hosts), pad));
12355
+ }
12356
+ return out;
12357
+ }
12358
+ function renderBuiltIn(p, m) {
12359
+ const lines = [];
12360
+ if (m.builtIn.languages.length > 0)
12361
+ lines.push({ label: "Languages", values: m.builtIn.languages });
12362
+ if (m.builtIn.features.length > 0)
12363
+ lines.push({ label: "Features", values: m.builtIn.features });
12364
+ if (lines.length === 0) return [];
12365
+ const labelWidth = Math.max(...lines.map((l) => l.label.length));
12366
+ const out = ["", `${p.sectionLine("Built in")} ${p.dim("from the yml")}`];
12367
+ for (const l of lines) {
12368
+ out.push(` ${l.label.padEnd(labelWidth)} ${p.cyan(l.values.join(", "))}`);
12369
+ }
12370
+ return out;
12371
+ }
12372
+ function renderStatus(m, p) {
12373
+ return [
12374
+ ...renderContainer(p, m),
12375
+ ...renderServices(p, m),
12376
+ ...renderApps(p, m),
12377
+ ...renderPorts(p, m),
12378
+ ...renderBuiltIn(p, m)
12379
+ ].join("\n");
12380
+ }
12381
+ function renderApp(m, app, p) {
12382
+ const targets = m.apps.filter((a) => a.app === app);
12383
+ if (targets.length === 0) {
12384
+ const known = [...new Set(m.apps.map((a) => a.app))];
12385
+ throw new Error(
12386
+ `No app "${app}" in ${m.name} (have: ${known.join(", ") || "none"}).`
12387
+ );
12388
+ }
12389
+ const out = [p.sectionLine(app)];
12390
+ if (m.appStateNote) out.push(` ${p.dim(m.appStateNote)}`);
12391
+ const pad = Math.max(...targets.map((t) => t.target.length));
12392
+ for (const t of targets) {
12393
+ out.push(
12394
+ row(
12395
+ p,
12396
+ liveMarker(p, t.running),
12397
+ t.target,
12398
+ appTargetDetail(p, m.name, t),
12399
+ pad,
12400
+ 2
12401
+ )
12402
+ );
12403
+ }
12404
+ return out.join("\n");
12405
+ }
12406
+ function renderService(m, name, p) {
12407
+ const s = m.services.find((svc) => svc.name === name);
12408
+ if (!s) {
12409
+ throw new Error(
12410
+ `No service "${name}" in ${m.name} (have: ${m.services.map((x) => x.name).join(", ") || "none"}).`
12411
+ );
12412
+ }
12413
+ const detail = s.running ? p.dim(typeof s.port === "number" ? `running :${s.port}` : "running") : p.dim(s.status || "stopped");
12414
+ return [
12415
+ p.sectionLine("Services"),
12416
+ row(p, liveMarker(p, s.running), s.name, detail, s.name.length)
12417
+ ].join("\n");
12418
+ }
12419
+ var init_status = __esm({
12420
+ "src/status/index.ts"() {
12421
+ "use strict";
12422
+ init_io();
12423
+ init_paths();
12424
+ init_global();
12425
+ init_transform();
12426
+ init_launch_config();
12427
+ init_catalog();
12428
+ init_locate_running();
12429
+ init_dynamic();
12430
+ init_format();
12431
+ }
12432
+ });
12433
+
12079
12434
  // src/commands/status.ts
12080
12435
  import { defineCommand as defineCommand31 } from "citty";
12081
12436
  var statusCommand;
12082
- var init_status = __esm({
12437
+ var init_status2 = __esm({
12083
12438
  "src/commands/status.ts"() {
12084
12439
  "use strict";
12085
- init_paths();
12086
- init_compose();
12440
+ init_status();
12441
+ init_format();
12087
12442
  init_dispatch();
12088
12443
  statusCommand = defineCommand31({
12089
12444
  meta: {
12090
12445
  name: "status",
12091
12446
  group: "run",
12092
- description: "Show whether the compose services for the named dev-container are running."
12447
+ 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
12448
  },
12094
12449
  args: {
12095
12450
  name: {
@@ -12097,18 +12452,29 @@ var init_status = __esm({
12097
12452
  description: "Container name (yml in $MONOCEROS_HOME/container-configs/).",
12098
12453
  required: true
12099
12454
  },
12100
- service: {
12101
- type: "string",
12102
- description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
12455
+ app: {
12456
+ type: "positional",
12457
+ 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.",
12458
+ required: false
12103
12459
  }
12104
12460
  },
12105
12461
  run({ args }) {
12106
- return dispatch(
12107
- () => runStatus({
12108
- root: containerDir(args.name),
12109
- ...typeof args.service === "string" ? { service: args.service } : {}
12110
- })
12111
- );
12462
+ const filter = typeof args.app === "string" ? args.app : void 0;
12463
+ return dispatch(async () => {
12464
+ const model = await gatherStatus(args.name);
12465
+ const p = colorsFor(process.stdout);
12466
+ let block;
12467
+ if (filter) {
12468
+ const isApp = model.apps.some((a) => a.app === filter);
12469
+ block = isApp ? renderApp(model, filter, p) : renderService(model, filter, p);
12470
+ } else {
12471
+ block = renderStatus(model, p);
12472
+ }
12473
+ process.stdout.write(`
12474
+ ${block}
12475
+ `);
12476
+ return 0;
12477
+ });
12112
12478
  }
12113
12479
  });
12114
12480
  }
@@ -12189,7 +12555,7 @@ var init_stop = __esm({
12189
12555
 
12190
12556
  // src/tunnel/resolve.ts
12191
12557
  import { existsSync as existsSync20 } from "fs";
12192
- import path31 from "path";
12558
+ import path32 from "path";
12193
12559
  async function resolveTunnelTarget(opts) {
12194
12560
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
12195
12561
  if (!existsSync20(ymlPath)) {
@@ -12205,7 +12571,7 @@ async function resolveTunnelTarget(opts) {
12205
12571
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
12206
12572
  );
12207
12573
  }
12208
- const composePath = path31.join(containerRoot, ".devcontainer", "compose.yaml");
12574
+ const composePath = path32.join(containerRoot, ".devcontainer", "compose.yaml");
12209
12575
  const isCompose = existsSync20(composePath);
12210
12576
  const parsedTarget = parseTargetArg(opts.target, config);
12211
12577
  const docker = opts.docker ?? defaultDockerExec;
@@ -12686,7 +13052,7 @@ var init_prune = __esm({
12686
13052
  });
12687
13053
 
12688
13054
  // src/upgrade/index.ts
12689
- import { existsSync as existsSync21, promises as fs19 } from "fs";
13055
+ import { existsSync as existsSync21, promises as fs20 } from "fs";
12690
13056
  import { consola as consola39 } from "consola";
12691
13057
  async function fetchRuntimeVersions() {
12692
13058
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
@@ -12803,10 +13169,10 @@ async function runUpgrade(opts) {
12803
13169
  for (const name of targets) {
12804
13170
  const ymlPath = containerConfigPath(name, home);
12805
13171
  if (!existsSync21(ymlPath)) continue;
12806
- const raw = await fs19.readFile(ymlPath, "utf8");
13172
+ const raw = await fs20.readFile(ymlPath, "utf8");
12807
13173
  const updated = setRuntimeVersion(raw, pinVersion);
12808
13174
  if (updated !== raw) {
12809
- await fs19.writeFile(ymlPath, updated);
13175
+ await fs20.writeFile(ymlPath, updated);
12810
13176
  bumped += 1;
12811
13177
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
12812
13178
  }
@@ -12848,7 +13214,7 @@ function formatPruneLine(prune) {
12848
13214
  }
12849
13215
  async function listContainerNames2(home) {
12850
13216
  try {
12851
- const entries = await fs19.readdir(containerConfigsDir(home));
13217
+ const entries = await fs20.readdir(containerConfigsDir(home));
12852
13218
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
12853
13219
  } catch {
12854
13220
  return [];
@@ -12956,7 +13322,7 @@ var init_main = __esm({
12956
13322
  init_run2();
12957
13323
  init_shell2();
12958
13324
  init_start();
12959
- init_status();
13325
+ init_status2();
12960
13326
  init_stop();
12961
13327
  init_tunnel();
12962
13328
  init_upgrade2();
@@ -13261,25 +13627,25 @@ function detectHelpRequest(argv, main2) {
13261
13627
  const separatorIdx = argv.indexOf("--");
13262
13628
  if (helpIdx === -1) return null;
13263
13629
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
13264
- const path32 = [];
13630
+ const path33 = [];
13265
13631
  const tokens = argv.slice(
13266
13632
  0,
13267
13633
  separatorIdx === -1 ? argv.length : separatorIdx
13268
13634
  );
13269
13635
  let cursor = main2;
13270
13636
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
13271
- path32.push(mainName);
13637
+ path33.push(mainName);
13272
13638
  for (const tok of tokens) {
13273
13639
  if (tok.startsWith("-")) continue;
13274
13640
  const subs = cursor.subCommands ?? {};
13275
13641
  if (tok in subs) {
13276
13642
  cursor = subs[tok];
13277
- path32.push(tok);
13643
+ path33.push(tok);
13278
13644
  continue;
13279
13645
  }
13280
13646
  break;
13281
13647
  }
13282
- return { path: path32, cmd: cursor };
13648
+ return { path: path33, cmd: cursor };
13283
13649
  }
13284
13650
  async function maybeRenderHelp(argv, main2) {
13285
13651
  const hit = detectHelpRequest(argv, main2);