mcproc 0.14.2 → 0.15.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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/program.ts
4
- import fs19 from "fs";
4
+ import fs20 from "fs";
5
5
  import { Command, Option } from "commander";
6
6
 
7
7
  // src/version.ts
@@ -259,9 +259,9 @@ function ensureOk(res, host, what) {
259
259
  }
260
260
 
261
261
  // src/runtime/unpack.ts
262
- import fs5 from "fs";
263
- import os3 from "os";
264
- import path7 from "path";
262
+ import fs7 from "fs";
263
+ import os5 from "os";
264
+ import path10 from "path";
265
265
 
266
266
  // src/core/pm2.ts
267
267
  import { createRequire as createRequire2 } from "module";
@@ -459,7 +459,8 @@ async function resolvePm2Bin(host, transport) {
459
459
  }
460
460
 
461
461
  // src/core/resolve.ts
462
- import path4 from "path";
462
+ import os2 from "os";
463
+ import path7 from "path";
463
464
 
464
465
  // src/core/manifest.ts
465
466
  import fs2 from "fs";
@@ -1553,6 +1554,166 @@ function loadManifest(startDir) {
1553
1554
  );
1554
1555
  }
1555
1556
 
1557
+ // src/core/registry.ts
1558
+ import fs4 from "fs";
1559
+ import path6 from "path";
1560
+
1561
+ // src/core/slots.ts
1562
+ import crypto from "crypto";
1563
+ import fs3 from "fs";
1564
+ import path4 from "path";
1565
+ function registryKey(baseProject, repo) {
1566
+ return `${baseProject}-${crypto.createHash("sha256").update(realOf(repo)).digest("hex").slice(0, 8)}`;
1567
+ }
1568
+ function registryPath(stateDir2, baseProject, repo) {
1569
+ return path4.join(stateDir2, "slots", `${registryKey(baseProject, repo)}.json`);
1570
+ }
1571
+ function emptyRegistry(repo) {
1572
+ return { repo, slots: {} };
1573
+ }
1574
+ function readRegistry(file) {
1575
+ if (!fs3.existsSync(file)) return null;
1576
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
1577
+ }
1578
+ function writeRegistry(file, r) {
1579
+ fs3.mkdirSync(path4.dirname(file), { recursive: true });
1580
+ const tmp = `${file}.${process.pid}.tmp`;
1581
+ fs3.writeFileSync(tmp, JSON.stringify(r, null, 2));
1582
+ fs3.renameSync(tmp, file);
1583
+ }
1584
+ function withMain(r, entry) {
1585
+ return { ...r, slots: { ...r.slots, "0": entry } };
1586
+ }
1587
+ function claimSlot(r, opts) {
1588
+ if (opts.want !== void 0) {
1589
+ if (opts.want < 1 || opts.want > opts.max)
1590
+ throw new McprocError(
1591
+ "BAD_ARGUMENT",
1592
+ `slot ${opts.want} is outside 1..${opts.max} (slot 0 is the main checkout)`
1593
+ );
1594
+ if (Object.hasOwn(r.slots, String(opts.want)))
1595
+ throw new McprocError(
1596
+ "SLOT_TAKEN",
1597
+ `slot ${opts.want} is taken by ${r.slots[String(opts.want)].path}`,
1598
+ { hint: "Pick another slot, or remove that worktree first." }
1599
+ );
1600
+ return {
1601
+ registry: { ...r, slots: { ...r.slots, [opts.want]: opts.entry } },
1602
+ slot: opts.want
1603
+ };
1604
+ }
1605
+ for (let s = 1; s <= opts.max; s++)
1606
+ if (!Object.hasOwn(r.slots, String(s)))
1607
+ return {
1608
+ registry: { ...r, slots: { ...r.slots, [s]: opts.entry } },
1609
+ slot: s
1610
+ };
1611
+ throw new McprocError(
1612
+ "NO_FREE_SLOT",
1613
+ `all ${opts.max} worktree slots of ${r.repo} are taken`,
1614
+ {
1615
+ hint: "Remove a worktree (worktree remove), or raise worktree.slots in the manifest."
1616
+ }
1617
+ );
1618
+ }
1619
+ function releaseSlot(r, slot) {
1620
+ const slots = { ...r.slots };
1621
+ delete slots[String(slot)];
1622
+ return { ...r, slots };
1623
+ }
1624
+ function realOf(p) {
1625
+ try {
1626
+ return fs3.realpathSync(p);
1627
+ } catch {
1628
+ return path4.resolve(p);
1629
+ }
1630
+ }
1631
+ function reclaimGone(r, exists) {
1632
+ const freed = [];
1633
+ let out = r;
1634
+ for (const [k, e] of Object.entries(r.slots)) {
1635
+ if (k === "0" || exists(realOf(e.path))) continue;
1636
+ out = releaseSlot(out, Number(k));
1637
+ freed.push(Number(k));
1638
+ }
1639
+ return { registry: out, freed: freed.sort((a, b) => a - b) };
1640
+ }
1641
+ function siblingsOf(r, root) {
1642
+ return Object.entries(r.slots).filter(([, e]) => realOf(e.path) !== realOf(root)).map(([k, entry]) => ({ slot: Number(k), entry })).sort((a, b) => a.slot - b.slot);
1643
+ }
1644
+
1645
+ // src/core/state.ts
1646
+ import os from "os";
1647
+ import path5 from "path";
1648
+ function stateDir(env = process.env) {
1649
+ if (env.MCPROC_STATE_DIR) return path5.resolve(env.MCPROC_STATE_DIR);
1650
+ return path5.join(
1651
+ env.XDG_STATE_HOME || path5.join(os.homedir(), ".local", "state"),
1652
+ "mcproc"
1653
+ );
1654
+ }
1655
+
1656
+ // src/core/registry.ts
1657
+ var projectsDir = (dir) => path6.join(dir, "projects");
1658
+ function projectFile(dir, project, root) {
1659
+ return path6.join(projectsDir(dir), `${registryKey(project, root)}.json`);
1660
+ }
1661
+ function recordProject(m, dir = stateDir()) {
1662
+ try {
1663
+ const file = projectFile(dir, m.project, m.root);
1664
+ const text = JSON.stringify(
1665
+ {
1666
+ project: m.project,
1667
+ root: m.root,
1668
+ manifest: m.path
1669
+ },
1670
+ null,
1671
+ 2
1672
+ );
1673
+ try {
1674
+ if (fs4.readFileSync(file, "utf8") === text) return;
1675
+ } catch {
1676
+ }
1677
+ fs4.mkdirSync(path6.dirname(file), { recursive: true });
1678
+ const tmp = `${file}.${process.pid}.tmp`;
1679
+ fs4.writeFileSync(tmp, text);
1680
+ fs4.renameSync(tmp, file);
1681
+ } catch {
1682
+ }
1683
+ }
1684
+ function forgetProject(project, root, dir = stateDir()) {
1685
+ try {
1686
+ fs4.rmSync(projectFile(dir, project, root), { force: true });
1687
+ } catch {
1688
+ }
1689
+ }
1690
+ function isEntry(v) {
1691
+ const e = v;
1692
+ return typeof e === "object" && e !== null && typeof e.project === "string" && typeof e.root === "string" && typeof e.manifest === "string";
1693
+ }
1694
+ function listProjects(dir = stateDir()) {
1695
+ let names;
1696
+ try {
1697
+ names = fs4.readdirSync(projectsDir(dir)).filter((n) => n.endsWith(".json"));
1698
+ } catch (e) {
1699
+ const code = e.code;
1700
+ return { entries: [], error: code === "ENOENT" ? null : String(code ?? e) };
1701
+ }
1702
+ const entries = [];
1703
+ for (const n of names.sort()) {
1704
+ const file = path6.join(projectsDir(dir), n);
1705
+ try {
1706
+ const e = JSON.parse(fs4.readFileSync(file, "utf8"));
1707
+ if (!isEntry(e)) continue;
1708
+ if (fs4.existsSync(e.manifest)) entries.push(e);
1709
+ else if (fs4.existsSync(path6.dirname(e.root)))
1710
+ fs4.rmSync(file, { force: true });
1711
+ } catch {
1712
+ }
1713
+ }
1714
+ return { entries, error: null };
1715
+ }
1716
+
1556
1717
  // src/core/urlcheck.ts
1557
1718
  import http from "http";
1558
1719
  import https from "https";
@@ -1878,7 +2039,18 @@ function reasonWord(reason) {
1878
2039
  var MAX_PM2_HOME = 90;
1879
2040
  function resolveProjectDir(explicit, roots = [], env = process.env) {
1880
2041
  const pick = explicit || env.MCPROC_PROJECT || env.CLAUDE_PROJECT_DIR || roots[0] || process.cwd();
1881
- return path4.resolve(pick);
2042
+ return path7.resolve(pick);
2043
+ }
2044
+ function bareLocalHost() {
2045
+ return {
2046
+ name: "local",
2047
+ local: true,
2048
+ ssh_args: [],
2049
+ cwd: os2.homedir(),
2050
+ readonly: false,
2051
+ shell: parseShell(DEFAULT_SHELL),
2052
+ env: {}
2053
+ };
1882
2054
  }
1883
2055
  function pickHost(m, name2 = "local") {
1884
2056
  const h = Object.hasOwn(m.hosts, name2) ? m.hosts[name2] : void 0;
@@ -1915,6 +2087,7 @@ function assertApp(m, name2) {
1915
2087
  }
1916
2088
  function prepare(opts) {
1917
2089
  const manifest = loadManifest(resolveProjectDir(opts.project, opts.roots));
2090
+ recordProject(manifest);
1918
2091
  return { manifest, host: pickHost(manifest, opts.host) };
1919
2092
  }
1920
2093
  var cache = /* @__PURE__ */ new Map();
@@ -1948,30 +2121,39 @@ async function connect(prep, opts = {}) {
1948
2121
  hereCheck: opts.hereCheck ?? checkHere
1949
2122
  };
1950
2123
  }
2124
+ async function connectHost(host, opts = {}) {
2125
+ const transport = createTransport(host, opts.spawner);
2126
+ const resolution = await resolvePm2Bin(host, transport);
2127
+ return {
2128
+ host,
2129
+ transport,
2130
+ pm2: new Pm2Client(transport, host, resolution.bin, host.pm2_home)
2131
+ };
2132
+ }
1951
2133
 
1952
2134
  // src/runtime/bundle.ts
1953
2135
  import { execFileSync } from "child_process";
1954
2136
  import { createHash } from "crypto";
1955
- import fs3 from "fs";
1956
- import os from "os";
1957
- import path5 from "path";
2137
+ import fs5 from "fs";
2138
+ import os3 from "os";
2139
+ import path8 from "path";
1958
2140
  import { fileURLToPath } from "url";
1959
2141
  var sha256 = (b) => createHash("sha256").update(b).digest("hex");
1960
2142
  var readJson = (p) => (
1961
2143
  // biome-ignore lint/suspicious/noExplicitAny: package.json files are untyped data, probed field by field
1962
- JSON.parse(fs3.readFileSync(p, "utf8"))
2144
+ JSON.parse(fs5.readFileSync(p, "utf8"))
1963
2145
  );
1964
- function findPackageRoot(from = path5.dirname(fileURLToPath(import.meta.url))) {
1965
- let dir = path5.resolve(from);
2146
+ function findPackageRoot(from = path8.dirname(fileURLToPath(import.meta.url))) {
2147
+ let dir = path8.resolve(from);
1966
2148
  for (; ; ) {
1967
- const p = path5.join(dir, "package.json");
1968
- if (fs3.existsSync(p)) {
2149
+ const p = path8.join(dir, "package.json");
2150
+ if (fs5.existsSync(p)) {
1969
2151
  try {
1970
2152
  if (readJson(p).name === "mcproc") return dir;
1971
2153
  } catch {
1972
2154
  }
1973
2155
  }
1974
- const parent = path5.dirname(dir);
2156
+ const parent = path8.dirname(dir);
1975
2157
  if (parent === dir)
1976
2158
  throw new McprocError(
1977
2159
  "BUNDLE_INVALID",
@@ -1982,12 +2164,12 @@ function findPackageRoot(from = path5.dirname(fileURLToPath(import.meta.url))) {
1982
2164
  }
1983
2165
  function lockfileHash(root) {
1984
2166
  return sha256(
1985
- fs3.readFileSync(path5.join(root, "runtime", "package-lock.json"))
2167
+ fs5.readFileSync(path8.join(root, "runtime", "package-lock.json"))
1986
2168
  );
1987
2169
  }
1988
2170
  function defaultPm2CacheDir(env = process.env) {
1989
- return path5.join(
1990
- env.XDG_CACHE_HOME || path5.join(os.homedir(), ".cache"),
2171
+ return path8.join(
2172
+ env.XDG_CACHE_HOME || path8.join(os3.homedir(), ".cache"),
1991
2173
  "mcproc",
1992
2174
  "pm2"
1993
2175
  );
@@ -2013,31 +2195,31 @@ async function ensurePm2Bundle(opts = {}) {
2013
2195
  const root = opts.root ?? findPackageRoot();
2014
2196
  const cacheDir = opts.cacheDir ?? defaultPm2CacheDir();
2015
2197
  const run = opts.run ?? defaultRunner;
2016
- const pin = readJson(path5.join(root, "package.json")).dependencies?.pm2 ?? "";
2198
+ const pin = readJson(path8.join(root, "package.json")).dependencies?.pm2 ?? "";
2017
2199
  if (!/^\d+\.\d+\.\d+$/.test(pin))
2018
2200
  throw new McprocError(
2019
2201
  "BUNDLE_INVALID",
2020
2202
  `pm2 must be pinned to an exact version in mcproc's package.json, got "${pin}"`
2021
2203
  );
2022
- const runtimePin = readJson(path5.join(root, "runtime", "package.json")).dependencies?.pm2 ?? "";
2204
+ const runtimePin = readJson(path8.join(root, "runtime", "package.json")).dependencies?.pm2 ?? "";
2023
2205
  if (runtimePin !== pin)
2024
2206
  throw new McprocError(
2025
2207
  "BUNDLE_INVALID",
2026
2208
  `runtime/package.json pins pm2 ${runtimePin}, package.json pins ${pin}`
2027
2209
  );
2028
2210
  const lockSha = lockfileHash(root);
2029
- const target = path5.join(cacheDir, `pm2-${pin}-${lockSha.slice(0, 12)}.tgz`);
2211
+ const target = path8.join(cacheDir, `pm2-${pin}-${lockSha.slice(0, 12)}.tgz`);
2030
2212
  const sidecar = `${target}.json`;
2031
- if (!opts.force && fs3.existsSync(target) && fs3.existsSync(sidecar)) {
2213
+ if (!opts.force && fs5.existsSync(target) && fs5.existsSync(sidecar)) {
2032
2214
  const meta = readJson(sidecar);
2033
- const data = fs3.readFileSync(target);
2215
+ const data = fs5.readFileSync(target);
2034
2216
  if (meta.pm2 === pin && meta.lock_sha256 === lockSha && sha256(data) === meta.sha256)
2035
2217
  return { version: pin, sha256: meta.sha256, data, path: target };
2036
2218
  }
2037
- const stage = fs3.mkdtempSync(path5.join(os.tmpdir(), "mcproc-pm2-"));
2219
+ const stage = fs5.mkdtempSync(path8.join(os3.tmpdir(), "mcproc-pm2-"));
2038
2220
  try {
2039
2221
  for (const f of ["package.json", "package-lock.json"])
2040
- fs3.copyFileSync(path5.join(root, "runtime", f), path5.join(stage, f));
2222
+ fs5.copyFileSync(path8.join(root, "runtime", f), path8.join(stage, f));
2041
2223
  try {
2042
2224
  run(
2043
2225
  "npm",
@@ -2054,14 +2236,14 @@ async function ensurePm2Bundle(opts = {}) {
2054
2236
  );
2055
2237
  }
2056
2238
  const got = readJson(
2057
- path5.join(stage, "node_modules", "pm2", "package.json")
2239
+ path8.join(stage, "node_modules", "pm2", "package.json")
2058
2240
  ).version;
2059
2241
  if (got !== pin)
2060
2242
  throw new McprocError(
2061
2243
  "BUNDLE_INVALID",
2062
2244
  `npm installed pm2 ${got}, expected ${pin}`
2063
2245
  );
2064
- fs3.rmSync(path5.join(stage, "node_modules", ".package-lock.json"), {
2246
+ fs5.rmSync(path8.join(stage, "node_modules", ".package-lock.json"), {
2065
2247
  force: true
2066
2248
  });
2067
2249
  const tgz = execFileSync(
@@ -2069,9 +2251,9 @@ async function ensurePm2Bundle(opts = {}) {
2069
2251
  [...tarFlags(), "-czf", "-", "node_modules"],
2070
2252
  { cwd: stage, maxBuffer: 1 << 30 }
2071
2253
  );
2072
- fs3.mkdirSync(cacheDir, { recursive: true });
2073
- fs3.writeFileSync(`${target}.part`, tgz);
2074
- fs3.renameSync(`${target}.part`, target);
2254
+ fs5.mkdirSync(cacheDir, { recursive: true });
2255
+ fs5.writeFileSync(`${target}.part`, tgz);
2256
+ fs5.renameSync(`${target}.part`, target);
2075
2257
  const meta = {
2076
2258
  pm2: pin,
2077
2259
  sha256: sha256(tgz),
@@ -2079,11 +2261,11 @@ async function ensurePm2Bundle(opts = {}) {
2079
2261
  lock_sha256: lockSha,
2080
2262
  built_at: (/* @__PURE__ */ new Date()).toISOString()
2081
2263
  };
2082
- fs3.writeFileSync(sidecar, `${JSON.stringify(meta, null, 2)}
2264
+ fs5.writeFileSync(sidecar, `${JSON.stringify(meta, null, 2)}
2083
2265
  `);
2084
2266
  return { version: pin, sha256: meta.sha256, data: tgz, path: target };
2085
2267
  } finally {
2086
- fs3.rmSync(stage, { recursive: true, force: true });
2268
+ fs5.rmSync(stage, { recursive: true, force: true });
2087
2269
  }
2088
2270
  }
2089
2271
 
@@ -2240,16 +2422,16 @@ ${original}`;
2240
2422
  }
2241
2423
 
2242
2424
  // src/runtime/node-dist.ts
2243
- import fs4 from "fs";
2244
- import os2 from "os";
2245
- import path6 from "path";
2425
+ import fs6 from "fs";
2426
+ import os4 from "os";
2427
+ import path9 from "path";
2246
2428
  import { createHash as createHash2 } from "crypto";
2247
2429
  var defaultFetcher = (url) => fetch(url, {
2248
2430
  signal: AbortSignal.timeout(url.endsWith(".tar.gz") ? 6e5 : 3e4)
2249
2431
  });
2250
2432
  function defaultCacheDir(env = process.env) {
2251
- return path6.join(
2252
- env.XDG_CACHE_HOME || path6.join(os2.homedir(), ".cache"),
2433
+ return path9.join(
2434
+ env.XDG_CACHE_HOME || path9.join(os4.homedir(), ".cache"),
2253
2435
  "mcproc",
2254
2436
  "node"
2255
2437
  );
@@ -2285,7 +2467,7 @@ async function getBytes(fetcher, url) {
2285
2467
  function cachedVersions(cacheDir, platform) {
2286
2468
  let names = [];
2287
2469
  try {
2288
- names = fs4.readdirSync(cacheDir);
2470
+ names = fs6.readdirSync(cacheDir);
2289
2471
  } catch {
2290
2472
  return [];
2291
2473
  }
@@ -2322,11 +2504,11 @@ async function fetchNodeTarball(version, platform, opts = {}) {
2322
2504
  const fetcher = opts.fetcher ?? defaultFetcher;
2323
2505
  const cacheDir = opts.cacheDir ?? defaultCacheDir();
2324
2506
  const file = nodeDistFile(version, platform);
2325
- const target = path6.join(cacheDir, file);
2507
+ const target = path9.join(cacheDir, file);
2326
2508
  const sidecar = `${target}.sha256`;
2327
- if (fs4.existsSync(target) && fs4.existsSync(sidecar)) {
2328
- const want2 = fs4.readFileSync(sidecar, "utf8").trim();
2329
- const data2 = fs4.readFileSync(target);
2509
+ if (fs6.existsSync(target) && fs6.existsSync(sidecar)) {
2510
+ const want2 = fs6.readFileSync(sidecar, "utf8").trim();
2511
+ const data2 = fs6.readFileSync(target);
2330
2512
  if (sha2562(data2) === want2)
2331
2513
  return {
2332
2514
  version,
@@ -2356,10 +2538,10 @@ async function fetchNodeTarball(version, platform, opts = {}) {
2356
2538
  `checksum mismatch for ${file}: SHASUMS256 says ${want}, downloaded ${got}`,
2357
2539
  { hint: HINT }
2358
2540
  );
2359
- fs4.mkdirSync(cacheDir, { recursive: true });
2360
- fs4.writeFileSync(`${target}.part`, data);
2361
- fs4.renameSync(`${target}.part`, target);
2362
- fs4.writeFileSync(sidecar, `${want}
2541
+ fs6.mkdirSync(cacheDir, { recursive: true });
2542
+ fs6.writeFileSync(`${target}.part`, data);
2543
+ fs6.renameSync(`${target}.part`, target);
2544
+ fs6.writeFileSync(sidecar, `${want}
2363
2545
  `);
2364
2546
  return {
2365
2547
  version,
@@ -2437,9 +2619,9 @@ function defaultUnpackDeps() {
2437
2619
  nodeVersion: (requested, platform) => resolveNodeVersion(requested, platform),
2438
2620
  nodeTarball: async (version, platform) => {
2439
2621
  const t = await fetchNodeTarball(version, platform);
2440
- return { data: fs5.readFileSync(t.path), sha256: t.sha256 };
2622
+ return { data: fs7.readFileSync(t.path), sha256: t.sha256 };
2441
2623
  },
2442
- hostname: () => os3.hostname(),
2624
+ hostname: () => os5.hostname(),
2443
2625
  now: () => /* @__PURE__ */ new Date()
2444
2626
  };
2445
2627
  }
@@ -2464,8 +2646,8 @@ async function unpack(target, opts, deps2) {
2464
2646
  }
2465
2647
  return res;
2466
2648
  };
2467
- const dotDir = path7.posix.join(host.cwd, ".mcproc");
2468
- const remoteManifest = path7.posix.join(dotDir, "mcproc.yaml");
2649
+ const dotDir = path10.posix.join(host.cwd, ".mcproc");
2650
+ const remoteManifest = path10.posix.join(dotDir, "mcproc.yaml");
2469
2651
  const pr = ensureOk(
2470
2652
  await transport.exec(unpackProbeScript(rt, remoteManifest), { env }),
2471
2653
  host,
@@ -2584,18 +2766,18 @@ async function unpack(target, opts, deps2) {
2584
2766
  `mkdir -p ${shq(`${dotDir}/.generated`)} && { [ -e ${shq(gi)} ] || printf '*\\n' > ${shq(gi)}; } && cat > ${shq(remoteManifest)}`,
2585
2767
  {
2586
2768
  stdin: manifestCopy(
2587
- fs5.readFileSync(manifest.path, "utf8"),
2769
+ fs7.readFileSync(manifest.path, "utf8"),
2588
2770
  deps2.hostname(),
2589
2771
  deps2.now()
2590
2772
  )
2591
2773
  }
2592
2774
  );
2593
- const readme = path7.join(manifest.root, ".mcproc", "README.md");
2594
- if (fs5.existsSync(readme))
2775
+ const readme = path10.join(manifest.root, ".mcproc", "README.md");
2776
+ if (fs7.existsSync(readme))
2595
2777
  await run(
2596
2778
  "readme copy",
2597
2779
  `[ -e ${shq(`${dotDir}/README.md`)} ] || cat > ${shq(`${dotDir}/README.md`)}`,
2598
- { stdin: fs5.readFileSync(readme, "utf8") }
2780
+ { stdin: fs7.readFileSync(readme, "utf8") }
2599
2781
  );
2600
2782
  manifestState = "written";
2601
2783
  }
@@ -2631,7 +2813,7 @@ async function unpack(target, opts, deps2) {
2631
2813
  }
2632
2814
 
2633
2815
  // src/core/leases/view.ts
2634
- import fs8 from "fs";
2816
+ import fs10 from "fs";
2635
2817
 
2636
2818
  // src/core/memory.ts
2637
2819
  var KB = 1024;
@@ -2676,33 +2858,22 @@ async function readHostMemory(transport, host) {
2676
2858
  return i < 0 ? null : parseMeminfo(res.stdout.slice(i + MARK.MEM.length));
2677
2859
  }
2678
2860
 
2679
- // src/core/state.ts
2680
- import os4 from "os";
2681
- import path8 from "path";
2682
- function stateDir(env = process.env) {
2683
- if (env.MCPROC_STATE_DIR) return path8.resolve(env.MCPROC_STATE_DIR);
2684
- return path8.join(
2685
- env.XDG_STATE_HOME || path8.join(os4.homedir(), ".local", "state"),
2686
- "mcproc"
2687
- );
2688
- }
2689
-
2690
2861
  // src/core/leases/ensure.ts
2691
- import fs7 from "fs";
2692
- import path11 from "path";
2862
+ import fs9 from "fs";
2863
+ import path13 from "path";
2693
2864
 
2694
2865
  // src/core/ecosystem.ts
2695
- import path9 from "path";
2866
+ import path11 from "path";
2696
2867
  var LOG_STAMP = "YYYY-MM-DDTHH:mm:ss.SSS";
2697
2868
  var STOP_TRAP = "trap 'exit 130' INT; trap 'exit 143' TERM; ";
2698
2869
  function appCwd(host, app) {
2699
- return path9.posix.resolve(host.cwd, app.cwd);
2870
+ return path11.posix.resolve(host.cwd, app.cwd);
2700
2871
  }
2701
2872
  function generatedDir(host) {
2702
- return path9.posix.join(host.cwd, ".mcproc", ".generated");
2873
+ return path11.posix.join(host.cwd, ".mcproc", ".generated");
2703
2874
  }
2704
2875
  function ecosystemPath(host) {
2705
- return path9.posix.join(generatedDir(host), "ecosystem.json");
2876
+ return path11.posix.join(generatedDir(host), "ecosystem.json");
2706
2877
  }
2707
2878
  function buildEcosystem(m, host) {
2708
2879
  const apps = Object.values(m.apps).map((app) => {
@@ -2732,10 +2903,10 @@ function buildEcosystem(m, host) {
2732
2903
  }
2733
2904
 
2734
2905
  // src/core/leases/store.ts
2735
- import crypto from "crypto";
2736
- import fs6 from "fs";
2737
- import os5 from "os";
2738
- import path10 from "path";
2906
+ import crypto2 from "crypto";
2907
+ import fs8 from "fs";
2908
+ import os6 from "os";
2909
+ import path12 from "path";
2739
2910
  var ALLOWED = {
2740
2911
  pending: ["starting", "running", "cancelled"],
2741
2912
  starting: ["running", "failed", "cancelled"],
@@ -2776,7 +2947,7 @@ function parseHeld(raw) {
2776
2947
  }
2777
2948
  function readHeld(file) {
2778
2949
  try {
2779
- return parseHeld(fs6.readFileSync(file, "utf8"));
2950
+ return parseHeld(fs8.readFileSync(file, "utf8"));
2780
2951
  } catch {
2781
2952
  return null;
2782
2953
  }
@@ -2791,13 +2962,13 @@ var LeaseStore = class {
2791
2962
  * `beforeUnlink` (tests only) runs while a breaker holds the break guard, before the re-read that decides the unlink.
2792
2963
  */
2793
2964
  constructor(stateDir2, deps2 = {}) {
2794
- this.dir = path10.join(stateDir2, "leases");
2965
+ this.dir = path12.join(stateDir2, "leases");
2795
2966
  this.now = deps2.now ?? (() => Date.now());
2796
2967
  this.pid = deps2.pid ?? process.pid;
2797
2968
  this.beforeUnlink = deps2.beforeUnlink;
2798
2969
  }
2799
2970
  file(l) {
2800
- return path10.join(
2971
+ return path12.join(
2801
2972
  this.dir,
2802
2973
  `${String(l.seq).padStart(6, "0")}-${l.id}.json`
2803
2974
  );
@@ -2806,11 +2977,11 @@ var LeaseStore = class {
2806
2977
  return new Date(this.now()).toISOString();
2807
2978
  }
2808
2979
  write(l) {
2809
- fs6.mkdirSync(this.dir, { recursive: true });
2980
+ fs8.mkdirSync(this.dir, { recursive: true });
2810
2981
  const f = this.file(l);
2811
2982
  const tmp = `${f}.${this.pid}.tmp`;
2812
- fs6.writeFileSync(tmp, JSON.stringify(l, null, 2));
2813
- fs6.renameSync(tmp, f);
2983
+ fs8.writeFileSync(tmp, JSON.stringify(l, null, 2));
2984
+ fs8.renameSync(tmp, f);
2814
2985
  return l;
2815
2986
  }
2816
2987
  /**
@@ -2820,13 +2991,13 @@ var LeaseStore = class {
2820
2991
  * breakIfStale), never by moving it aside, so the lock path is never left free while a live holder has it.
2821
2992
  */
2822
2993
  async withLock(fn) {
2823
- fs6.mkdirSync(this.dir, { recursive: true });
2824
- const lock = path10.join(this.dir, ".lock");
2825
- const token = `${this.pid}-${crypto.randomBytes(6).toString("hex")}`;
2826
- const tmp = path10.join(this.dir, `.lock.${token}.tmp`);
2994
+ fs8.mkdirSync(this.dir, { recursive: true });
2995
+ const lock = path12.join(this.dir, ".lock");
2996
+ const token = `${this.pid}-${crypto2.randomBytes(6).toString("hex")}`;
2997
+ const tmp = path12.join(this.dir, `.lock.${token}.tmp`);
2827
2998
  const writeToken = () => {
2828
- fs6.mkdirSync(this.dir, { recursive: true });
2829
- fs6.writeFileSync(
2999
+ fs8.mkdirSync(this.dir, { recursive: true });
3000
+ fs8.writeFileSync(
2830
3001
  tmp,
2831
3002
  JSON.stringify({ pid: this.pid, at: this.now(), token })
2832
3003
  );
@@ -2857,7 +3028,7 @@ var LeaseStore = class {
2857
3028
  await new Promise((r2) => setTimeout(r2, 50));
2858
3029
  }
2859
3030
  } finally {
2860
- fs6.rmSync(tmp, { force: true });
3031
+ fs8.rmSync(tmp, { force: true });
2861
3032
  }
2862
3033
  LIVE_TOKENS.add(token);
2863
3034
  try {
@@ -2872,7 +3043,7 @@ var LeaseStore = class {
2872
3043
  }
2873
3044
  /** Whether a parsed lock is a leftover: from before this boot, this process's own but not held, or old with a dead holder. */
2874
3045
  heldStale(held) {
2875
- if (held.at < this.now() - os5.uptime() * 1e3) return true;
3046
+ if (held.at < this.now() - os6.uptime() * 1e3) return true;
2876
3047
  if (held.pid === process.pid && !LIVE_TOKENS.has(held.token)) return true;
2877
3048
  return this.now() - held.at > LOCK_STALE_MS && !pidAlive(held.pid);
2878
3049
  }
@@ -2883,15 +3054,15 @@ var LeaseStore = class {
2883
3054
  sweepStrays() {
2884
3055
  let names;
2885
3056
  try {
2886
- names = fs6.readdirSync(this.dir);
3057
+ names = fs8.readdirSync(this.dir);
2887
3058
  } catch {
2888
3059
  return;
2889
3060
  }
2890
3061
  for (const n of names) {
2891
3062
  if (!STRAY.test(n)) continue;
2892
- const f = path10.join(this.dir, n);
3063
+ const f = path12.join(this.dir, n);
2893
3064
  if (this.mtimeAgeMs(f) > LOCK_STALE_MS)
2894
- fs6.rmSync(f, { recursive: true, force: true });
3065
+ fs8.rmSync(f, { recursive: true, force: true });
2895
3066
  }
2896
3067
  }
2897
3068
  /**
@@ -2902,12 +3073,12 @@ var LeaseStore = class {
2902
3073
  tryLink(tmp, lock, rewrite) {
2903
3074
  for (let attempt = 0; ; attempt++) {
2904
3075
  try {
2905
- fs6.linkSync(tmp, lock);
3076
+ fs8.linkSync(tmp, lock);
2906
3077
  return true;
2907
3078
  } catch (e) {
2908
3079
  const code = e.code;
2909
3080
  if (code === "EEXIST") return false;
2910
- if (code === "ENOENT" && attempt === 0 && !fs6.existsSync(tmp)) {
3081
+ if (code === "ENOENT" && attempt === 0 && !fs8.existsSync(tmp)) {
2911
3082
  rewrite();
2912
3083
  continue;
2913
3084
  }
@@ -2919,7 +3090,7 @@ var LeaseStore = class {
2919
3090
  stillStale(lock, judged) {
2920
3091
  let raw;
2921
3092
  try {
2922
- raw = fs6.readFileSync(lock, "utf8");
3093
+ raw = fs8.readFileSync(lock, "utf8");
2923
3094
  } catch (e) {
2924
3095
  if (e.code === "ENOENT") return false;
2925
3096
  throw e;
@@ -2932,7 +3103,7 @@ var LeaseStore = class {
2932
3103
  * or, with no readable holder, the directory's mtime is older than LOCK_STALE_MS.
2933
3104
  */
2934
3105
  guardStale(guard) {
2935
- const held = readHeld(path10.join(guard, "holder"));
3106
+ const held = readHeld(path12.join(guard, "holder"));
2936
3107
  if (held)
2937
3108
  return this.now() - held.at > LOCK_STALE_MS && !pidAlive(held.pid);
2938
3109
  return this.mtimeAgeMs(guard) > LOCK_STALE_MS;
@@ -2950,7 +3121,7 @@ var LeaseStore = class {
2950
3121
  breakIfStale(lock, token, take) {
2951
3122
  let raw;
2952
3123
  try {
2953
- raw = fs6.readFileSync(lock, "utf8");
3124
+ raw = fs8.readFileSync(lock, "utf8");
2954
3125
  } catch (e) {
2955
3126
  if (e.code === "ENOENT") return "retry";
2956
3127
  throw e;
@@ -2960,57 +3131,57 @@ var LeaseStore = class {
2960
3131
  if (!stale) return "wait";
2961
3132
  const guard = `${lock}.break`;
2962
3133
  try {
2963
- fs6.mkdirSync(guard);
3134
+ fs8.mkdirSync(guard);
2964
3135
  } catch (e) {
2965
3136
  if (e.code !== "EEXIST") throw e;
2966
3137
  if (!this.guardStale(guard)) return "wait";
2967
3138
  const moved = `${guard}.${token}.stale`;
2968
3139
  try {
2969
- fs6.renameSync(guard, moved);
3140
+ fs8.renameSync(guard, moved);
2970
3141
  } catch (err) {
2971
3142
  if (err.code === "ENOENT") return "retry";
2972
3143
  throw err;
2973
3144
  }
2974
- fs6.rmSync(moved, { recursive: true, force: true });
3145
+ fs8.rmSync(moved, { recursive: true, force: true });
2975
3146
  return "retry";
2976
3147
  }
2977
3148
  try {
2978
- fs6.writeFileSync(
2979
- path10.join(guard, "holder"),
3149
+ fs8.writeFileSync(
3150
+ path12.join(guard, "holder"),
2980
3151
  JSON.stringify({ pid: this.pid, at: this.now(), token })
2981
3152
  );
2982
3153
  this.beforeUnlink?.();
2983
3154
  if (!this.stillStale(lock, held)) return "retry";
2984
3155
  try {
2985
- fs6.unlinkSync(lock);
3156
+ fs8.unlinkSync(lock);
2986
3157
  } catch (e) {
2987
3158
  if (e.code !== "ENOENT") throw e;
2988
3159
  }
2989
3160
  return take() ? "taken" : "retry";
2990
3161
  } finally {
2991
- fs6.rmSync(guard, { recursive: true, force: true });
3162
+ fs8.rmSync(guard, { recursive: true, force: true });
2992
3163
  }
2993
3164
  }
2994
3165
  /** Age of a lock file by its modification time on the file system's clock; 0 when it is gone. */
2995
3166
  mtimeAgeMs(lock) {
2996
3167
  try {
2997
- return Date.now() - fs6.statSync(lock).mtimeMs;
3168
+ return Date.now() - fs8.statSync(lock).mtimeMs;
2998
3169
  } catch {
2999
3170
  return 0;
3000
3171
  }
3001
3172
  }
3002
3173
  /** Unlinks the lock only when it still carries this holder's token; a lock it does not own is never removed. */
3003
3174
  release(lock, token) {
3004
- if (readHeld(lock)?.token === token) fs6.rmSync(lock, { force: true });
3175
+ if (readHeld(lock)?.token === token) fs8.rmSync(lock, { force: true });
3005
3176
  }
3006
3177
  /** Every readable lease, in sequence order; a half-written file is skipped. */
3007
3178
  list() {
3008
- if (!fs6.existsSync(this.dir)) return [];
3009
- return fs6.readdirSync(this.dir).filter((f) => f.endsWith(".json")).sort().flatMap((f) => {
3179
+ if (!fs8.existsSync(this.dir)) return [];
3180
+ return fs8.readdirSync(this.dir).filter((f) => f.endsWith(".json")).sort().flatMap((f) => {
3010
3181
  try {
3011
3182
  return [
3012
3183
  JSON.parse(
3013
- fs6.readFileSync(path10.join(this.dir, f), "utf8")
3184
+ fs8.readFileSync(path12.join(this.dir, f), "utf8")
3014
3185
  )
3015
3186
  ];
3016
3187
  } catch {
@@ -3023,25 +3194,25 @@ var LeaseStore = class {
3023
3194
  return this.list().find((l) => l.id === id) ?? null;
3024
3195
  }
3025
3196
  nextSeq() {
3026
- const f = path10.join(this.dir, ".seq");
3197
+ const f = path12.join(this.dir, ".seq");
3027
3198
  const onDisk = Math.max(
3028
3199
  0,
3029
- ...fs6.readdirSync(this.dir).filter((name2) => name2.endsWith(".json")).map((name2) => Number(name2.split("-")[0])).filter(Number.isFinite)
3200
+ ...fs8.readdirSync(this.dir).filter((name2) => name2.endsWith(".json")).map((name2) => Number(name2.split("-")[0])).filter(Number.isFinite)
3030
3201
  );
3031
- const parsed = fs6.existsSync(f) ? Number(fs6.readFileSync(f, "utf8")) : 0;
3202
+ const parsed = fs8.existsSync(f) ? Number(fs8.readFileSync(f, "utf8")) : 0;
3032
3203
  const n = Math.max(Number.isFinite(parsed) ? parsed : 0, onDisk);
3033
3204
  const tmp = `${f}.${this.pid}.tmp`;
3034
- fs6.writeFileSync(tmp, String(n + 1));
3035
- fs6.renameSync(tmp, f);
3205
+ fs8.writeFileSync(tmp, String(n + 1));
3206
+ fs8.renameSync(tmp, f);
3036
3207
  return n + 1;
3037
3208
  }
3038
3209
  /** A new lease, pending (queued) or running (admitted at once), with `child_pid` the process running its start; call it under the lock. */
3039
3210
  async create(input, state, opts = {}) {
3040
- fs6.mkdirSync(this.dir, { recursive: true });
3211
+ fs8.mkdirSync(this.dir, { recursive: true });
3041
3212
  const at = this.stamp();
3042
3213
  const l = {
3043
3214
  ...input,
3044
- id: `l_${crypto.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().padEnd(10, "0").slice(0, 10)}`,
3215
+ id: `l_${crypto2.randomBytes(8).toString("base64url").replace(/[^a-z0-9]/gi, "").toLowerCase().padEnd(10, "0").slice(0, 10)}`,
3045
3216
  seq: this.nextSeq(),
3046
3217
  state,
3047
3218
  child_pid: opts.child_pid ?? null,
@@ -3119,7 +3290,7 @@ var LeaseStore = class {
3119
3290
  for (const l of this.list()) {
3120
3291
  if (!ENDED.has(l.state) || !l.ended_at) continue;
3121
3292
  if (this.now() - Date.parse(l.ended_at) > maxAgeMs) {
3122
- fs6.rmSync(this.file(l), { force: true });
3293
+ fs8.rmSync(this.file(l), { force: true });
3123
3294
  n++;
3124
3295
  }
3125
3296
  }
@@ -3137,7 +3308,7 @@ var EXIT_POLL_MS = 250;
3137
3308
  function exitingAt(dir) {
3138
3309
  try {
3139
3310
  const v = JSON.parse(
3140
- fs7.readFileSync(path11.join(dir, WATCHER_NOTE), "utf8")
3311
+ fs9.readFileSync(path13.join(dir, WATCHER_NOTE), "utf8")
3141
3312
  );
3142
3313
  const at = typeof v.exiting_at === "string" ? Date.parse(v.exiting_at) : Number.NaN;
3143
3314
  return Number.isFinite(at) ? at : null;
@@ -3146,9 +3317,10 @@ function exitingAt(dir) {
3146
3317
  }
3147
3318
  }
3148
3319
  function watcherEntry(env = process.env, argv = process.argv) {
3149
- if (env.MCPROC_CLI_ENTRY) return path11.resolve(env.MCPROC_CLI_ENTRY);
3320
+ if (env.MCPROC_CLI_ENTRY) return path13.resolve(env.MCPROC_CLI_ENTRY);
3150
3321
  const own2 = argv[1] ?? "";
3151
- if (own2.endsWith("cli.js")) return path11.resolve(own2);
3322
+ const real = own2 ? realOf(own2) : "";
3323
+ if (real.endsWith("cli.js")) return real;
3152
3324
  throw new McprocError(
3153
3325
  "NOT_STARTED",
3154
3326
  `cannot launch the lease watcher: this mcproc runs from ${own2 || "an unknown entry"}, not from a built cli.js`,
@@ -3180,10 +3352,10 @@ async function lookAndLaunch(ctx, dir, leaving) {
3180
3352
  if (await watcherOnline(ctx) && (leaving === null || exitingAt(dir) !== leaving))
3181
3353
  return "online";
3182
3354
  const entry = watcherEntry();
3183
- const file = path11.join(dir, "watcher.ecosystem.json");
3184
- fs7.mkdirSync(dir, { recursive: true });
3185
- fs7.rmSync(path11.join(dir, WATCHER_NOTE), { force: true });
3186
- fs7.writeFileSync(
3355
+ const file = path13.join(dir, "watcher.ecosystem.json");
3356
+ fs9.mkdirSync(dir, { recursive: true });
3357
+ fs9.rmSync(path13.join(dir, WATCHER_NOTE), { force: true });
3358
+ fs9.writeFileSync(
3187
3359
  file,
3188
3360
  JSON.stringify(
3189
3361
  {
@@ -3219,7 +3391,7 @@ async function lookAndLaunch(ctx, dir, leaving) {
3219
3391
  // src/core/leases/view.ts
3220
3392
  async function leasesView(dir = stateDir(), ctx) {
3221
3393
  const store = new LeaseStore(dir);
3222
- if (fs8.existsSync(store.dir))
3394
+ if (fs10.existsSync(store.dir))
3223
3395
  await store.withLock(() => store.prune()).catch(() => {
3224
3396
  });
3225
3397
  const leases = store.list().map((l) => ({ ...l, position: store.positionOf(l.id) }));
@@ -3274,17 +3446,12 @@ function cancelLease(id, dir = stateDir()) {
3274
3446
  });
3275
3447
  }
3276
3448
 
3277
- // src/core/leases/watcher.ts
3278
- import { spawn as spawn2 } from "child_process";
3279
- import fs12 from "fs";
3280
- import path16 from "path";
3281
-
3282
- // src/core/actions.ts
3283
- import path14 from "path";
3449
+ // src/core/stacks/collect.ts
3450
+ import os8 from "os";
3284
3451
 
3285
3452
  // src/core/list.ts
3286
- import fs9 from "fs";
3287
- import path12 from "path";
3453
+ import fs11 from "fs";
3454
+ import path14 from "path";
3288
3455
 
3289
3456
  // src/core/metrics.ts
3290
3457
  function cpuPercent(pids, before, after, clkTck, intervalSec) {
@@ -3472,7 +3639,7 @@ function parseRead(stdout, id, offset, opts = {}) {
3472
3639
  }
3473
3640
  const skipped = offset !== null && offset >= 0 && size >= offset ? Math.max(0, start - offset) : 0;
3474
3641
  const nextOffset = Math.max(start, size - partial);
3475
- const short = buf.length < size - ctxStart;
3642
+ const short2 = buf.length < size - ctxStart;
3476
3643
  const anchor = hex && HEX.test(hex) ? { at: size, hex } : null;
3477
3644
  return {
3478
3645
  size,
@@ -3481,17 +3648,17 @@ function parseRead(stdout, id, offset, opts = {}) {
3481
3648
  lines,
3482
3649
  contextOnly,
3483
3650
  nextOffset,
3484
- short,
3651
+ short: short2,
3485
3652
  anchor,
3486
- nextAnchor: anchorAt(buf, nextOffset, { size, ctxStart, short, anchor })
3653
+ nextAnchor: anchorAt(buf, nextOffset, { size, ctxStart, short: short2, anchor })
3487
3654
  };
3488
3655
  }
3489
3656
  function anchorAt(buf, at, read) {
3490
- const { size, ctxStart, short, anchor } = read;
3657
+ const { size, ctxStart, short: short2, anchor } = read;
3491
3658
  if (at === size) return anchor;
3492
3659
  const n = Math.min(16, at);
3493
3660
  const end = buf.length - (size - at);
3494
- if (!anchor || short || n <= 0 || at - n < ctxStart || end - n < 0)
3661
+ if (!anchor || short2 || n <= 0 || at - n < ctxStart || end - n < 0)
3495
3662
  return null;
3496
3663
  const tail = buf.subarray(end - n);
3497
3664
  if (tail.includes(REPLACEMENT)) return null;
@@ -3635,8 +3802,8 @@ var LogMarks = class {
3635
3802
  this.m.set(key, e);
3636
3803
  }
3637
3804
  };
3638
- var sinceMark = (path22, r) => ({
3639
- path: path22,
3805
+ var sinceMark = (path23, r) => ({
3806
+ path: path23,
3640
3807
  offset: r.nextOffset,
3641
3808
  ...r.nextAnchor ? { at: r.nextAnchor.at, anchor: r.nextAnchor.hex } : {}
3642
3809
  });
@@ -3667,15 +3834,15 @@ async function readSince(fleet, names, opts) {
3667
3834
  );
3668
3835
  const files = targets.flatMap(
3669
3836
  (t) => streams2.map((stream2) => {
3670
- const path22 = stream2 === "out" ? t.p.out_log : t.p.err_log;
3837
+ const path23 = stream2 === "out" ? t.p.out_log : t.p.err_log;
3671
3838
  const key = LogMarks.key(fleet.manifest.path, g.host, t.app);
3672
3839
  const mark = spec.kind === "last" ? opts.marks?.get(key)?.[stream2] : void 0;
3673
- const same = mark !== void 0 && mark.path === path22;
3840
+ const same = mark !== void 0 && mark.path === path23;
3674
3841
  const anchor = same && mark.at !== void 0 && mark.anchor !== void 0 ? { at: mark.at, hex: mark.anchor } : null;
3675
3842
  return {
3676
3843
  app: t.app,
3677
3844
  stream: stream2,
3678
- path: path22,
3845
+ path: path23,
3679
3846
  key,
3680
3847
  offset: same ? mark.offset : null,
3681
3848
  anchor,
@@ -4108,38 +4275,14 @@ function urlFields(u) {
4108
4275
  url_reason: null
4109
4276
  };
4110
4277
  }
4111
- async function snapshot(ctx, opts = {}) {
4112
- const withMetrics = opts.metrics !== "none";
4113
- const cpuSample = (opts.cpu ?? "inline") === "inline" && withMetrics;
4114
- const which = opts.urls ?? "check";
4115
- const asked = which === "none" ? [] : Object.values(ctx.manifest.apps).flatMap(
4116
- (a) => a.url !== null && (which === "check" || which.includes(a.name)) ? [{ name: a.name, url: a.url, url_check: a.url_check }] : []
4117
- );
4118
- const urls = [
4119
- ...new Set(
4120
- asked.filter((a) => ctx.host.local || a.url_check !== "here").map((a) => a.url)
4121
- )
4122
- ];
4123
- const containers = withMetrics && opts.containers === "probe" ? [
4124
- ...new Set(
4125
- Object.values(ctx.manifest.apps).flatMap((a) => a.containers)
4126
- )
4127
- ] : [];
4278
+ async function probeHost(link, ask) {
4128
4279
  const res = ensureOk(
4129
- await ctx.transport.exec(
4130
- probeScript(ctx.pm2.bin, {
4131
- cpuSample,
4132
- urls,
4133
- metrics: withMetrics,
4134
- containers
4135
- }),
4136
- {
4137
- env: ctx.pm2.env(),
4138
- // each url adds its bounded lookup and request: many on a host whose resolver drops packets outlast the default
4139
- timeoutMs: probeTimeoutMs(urls.length)
4140
- }
4141
- ),
4142
- ctx.host,
4280
+ await link.transport.exec(probeScript(link.pm2.bin, ask), {
4281
+ env: link.pm2.env(),
4282
+ // each url adds its bounded lookup and request: many on a host whose resolver drops packets outlast the default
4283
+ timeoutMs: probeTimeoutMs(ask.urls.length)
4284
+ }),
4285
+ link.host,
4143
4286
  "probe"
4144
4287
  );
4145
4288
  const sampledAt = Date.now();
@@ -4147,9 +4290,9 @@ async function snapshot(ctx, opts = {}) {
4147
4290
  const tail = (res.stderr || res.stdout).trim().split("\n").slice(-5).join("\n");
4148
4291
  throw new McprocError(
4149
4292
  "TRANSPORT_FAILED",
4150
- `the shell on ${ctx.transport.describe()} did not run the probe (exit ${res.code}): ${tail}`,
4293
+ `the shell on ${link.transport.describe()} did not run the probe (exit ${res.code}): ${tail}`,
4151
4294
  {
4152
- hint: `Check hosts.${ctx.host.name}.shell (default /bin/bash -lc) exists on that host.`
4295
+ hint: `Check hosts.${link.host.name}.shell (default /bin/bash -lc) exists on that host.`
4153
4296
  }
4154
4297
  );
4155
4298
  }
@@ -4157,27 +4300,37 @@ async function snapshot(ctx, opts = {}) {
4157
4300
  if (data.jlistRc === 127)
4158
4301
  throw new McprocError(
4159
4302
  "PM2_NOT_FOUND",
4160
- `pm2 not found on ${ctx.transport.describe()}`,
4303
+ `pm2 not found on ${link.transport.describe()}`,
4161
4304
  { hint: PM2_NOT_FOUND_HINT }
4162
4305
  );
4163
4306
  if (data.jlistRc !== 0)
4164
4307
  throw new McprocError(
4165
4308
  "PM2_FAILED",
4166
- `pm2 jlist exited ${data.jlistRc} on ${ctx.transport.describe()}`,
4309
+ `pm2 jlist exited ${data.jlistRc} on ${link.transport.describe()}`,
4167
4310
  { details: res.stderr.slice(-500) }
4168
4311
  );
4169
- const procs = parseJlist(data.jlistRaw);
4312
+ return { data, procs: parseJlist(data.jlistRaw), sampledAt };
4313
+ }
4314
+ function askedUrls(view, opts) {
4315
+ const which = opts.urls ?? "check";
4316
+ return which === "none" ? [] : Object.values(view.manifest.apps).flatMap(
4317
+ (a) => a.url !== null && (which === "check" || which.includes(a.name)) ? [{ name: a.name, url: a.url, url_check: a.url_check }] : []
4318
+ );
4319
+ }
4320
+ async function deriveSnapshot(view, probe, opts = {}) {
4321
+ const { data, procs, sampledAt } = probe;
4322
+ const asked = askedUrls(view, opts);
4170
4323
  const pids = appPids(
4171
- ctx.manifest.apps,
4324
+ view.manifest.apps,
4172
4325
  procs,
4173
4326
  data.tree,
4174
- ctx.manifest.project
4327
+ view.manifest.project
4175
4328
  );
4176
4329
  const metricOpts = opts.prev ? { prev: opts.prev.sample, intervalSec: (sampledAt - opts.prev.at) / 1e3 } : {};
4177
4330
  const metrics = {};
4178
4331
  for (const [name2, set] of pids)
4179
4332
  metrics[name2] = treeMetrics(set, data.stats, data.listeners, metricOpts);
4180
- for (const app of Object.values(ctx.manifest.apps)) {
4333
+ for (const app of Object.values(view.manifest.apps)) {
4181
4334
  const m = metrics[app.name];
4182
4335
  if (!m || app.containers.length === 0) continue;
4183
4336
  const mib = app.containers.reduce(
@@ -4193,13 +4346,13 @@ async function snapshot(ctx, opts = {}) {
4193
4346
  }
4194
4347
  const urlChecks = await settleUrls(
4195
4348
  asked,
4196
- { host: ctx.host.name, local: ctx.host.local },
4349
+ { host: view.host.name, local: view.host.local },
4197
4350
  data,
4198
- ctx.hereCheck
4351
+ view.hereCheck
4199
4352
  );
4200
4353
  return {
4201
4354
  procs,
4202
- ports: joinPorts(ctx.manifest.apps, procs, data, ctx.manifest.project),
4355
+ ports: joinPorts(view.manifest.apps, procs, data, view.manifest.project),
4203
4356
  listeners: data.listeners,
4204
4357
  tree: data.tree,
4205
4358
  stats: data.stats,
@@ -4208,9 +4361,30 @@ async function snapshot(ctx, opts = {}) {
4208
4361
  urlChecks,
4209
4362
  zone: data.zone,
4210
4363
  memory: data.memory,
4211
- leases: ctx.host.local && opts.leases !== "none" ? leasesCovering(stateDir(), ctx.manifest) : []
4364
+ leases: view.host.local && opts.leases !== "none" ? leasesCovering(stateDir(), view.manifest) : []
4212
4365
  };
4213
4366
  }
4367
+ async function snapshot(ctx, opts = {}) {
4368
+ const withMetrics = opts.metrics !== "none";
4369
+ const asked = askedUrls(ctx, opts);
4370
+ const urls = [
4371
+ ...new Set(
4372
+ asked.filter((a) => ctx.host.local || a.url_check !== "here").map((a) => a.url)
4373
+ )
4374
+ ];
4375
+ const containers = withMetrics && opts.containers === "probe" ? [
4376
+ ...new Set(
4377
+ Object.values(ctx.manifest.apps).flatMap((a) => a.containers)
4378
+ )
4379
+ ] : [];
4380
+ const probe = await probeHost(ctx, {
4381
+ cpuSample: (opts.cpu ?? "inline") === "inline" && withMetrics,
4382
+ metrics: withMetrics,
4383
+ urls,
4384
+ containers
4385
+ });
4386
+ return deriveSnapshot(ctx, probe, opts);
4387
+ }
4214
4388
  var SIGNAL_EXITS = /* @__PURE__ */ new Set([130, 143]);
4215
4389
  function statusWord(p, app) {
4216
4390
  if (!p) return "not-started";
@@ -4345,8 +4519,8 @@ var depView = (d) => ({
4345
4519
  timeout_s: d.timeoutMs / 1e3
4346
4520
  });
4347
4521
  function describeProject(m) {
4348
- const readmePath = path12.join(m.root, ".mcproc", "README.md");
4349
- const readme = fs9.existsSync(readmePath) ? fs9.readFileSync(readmePath, "utf8") : null;
4522
+ const readmePath = path14.join(m.root, ".mcproc", "README.md");
4523
+ const readme = fs11.existsSync(readmePath) ? fs11.readFileSync(readmePath, "utf8") : null;
4350
4524
  return {
4351
4525
  project: m.project,
4352
4526
  root: m.root,
@@ -4377,9 +4551,12 @@ function describeProject(m) {
4377
4551
  };
4378
4552
  }
4379
4553
 
4554
+ // src/core/actions.ts
4555
+ import path16 from "path";
4556
+
4380
4557
  // src/core/logfollow.ts
4381
4558
  import { open } from "fs/promises";
4382
- import path13 from "path";
4559
+ import path15 from "path";
4383
4560
 
4384
4561
  // src/core/logwatch.ts
4385
4562
  import { watch as fsWatch } from "fs";
@@ -4722,8 +4899,8 @@ function hostStreamFor(t) {
4722
4899
  var MAX_PENDING = 65536;
4723
4900
  var MAX_READY = 1e4;
4724
4901
  function chunkScript(reqs) {
4725
- return reqs.map(({ path: path22, off, anchor }, i) => {
4726
- const f = shq(path22);
4902
+ return reqs.map(({ path: path23, off, anchor }, i) => {
4903
+ const f = shq(path23);
4727
4904
  const tail = `st=$((sz>${HISTORY_BYTES}?sz-${HISTORY_BYTES}:0))`;
4728
4905
  const replaced = off !== null && anchor ? ` || [ "${hex16(f, String(off))}" != '${anchor}' ]` : "";
4729
4906
  const pick = off === null ? `k=H; ${tail}` : `if [ "$sz" -lt ${off} ]${replaced}; then k=R; ${tail}; elif [ $((sz-${off})) -gt ${READ_CAP2} ]; then k=S; st=$((sz-${READ_CAP2})); else k=N; st=${off}; fi`;
@@ -4873,8 +5050,8 @@ var LogFollower = class {
4873
5050
  }
4874
5051
  const byDir = /* @__PURE__ */ new Map();
4875
5052
  for (const p of [this.paths.out, this.paths.err]) {
4876
- const dir = path13.dirname(p);
4877
- byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path13.basename(p)));
5053
+ const dir = path15.dirname(p);
5054
+ byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path15.basename(p)));
4878
5055
  }
4879
5056
  for (const [dir, names] of byDir)
4880
5057
  this.unwatch.push(
@@ -5247,6 +5424,9 @@ function detectReloads(input, rules, opts) {
5247
5424
  return [...t.push(input, 0), ...t.flush()];
5248
5425
  }
5249
5426
 
5427
+ // src/mcp/render.ts
5428
+ import os7 from "os";
5429
+
5250
5430
  // src/core/logstream.ts
5251
5431
  var WAKE_GAP_MS = 50;
5252
5432
  function signals() {
@@ -5442,42 +5622,242 @@ async function streamLogs(sources, opts, deps2) {
5442
5622
  }
5443
5623
  }
5444
5624
 
5445
- // src/mcp/render.ts
5446
- function fmtUptime(ms) {
5447
- if (ms === null) return "-";
5448
- const s = Math.floor(ms / 1e3);
5449
- if (s < 60) return `${s}s`;
5450
- const m = Math.floor(s / 60);
5451
- if (m < 60) return `${m}m`;
5452
- const h = Math.floor(m / 60);
5453
- if (h < 48) return `${h}h${m % 60}m`;
5454
- return `${Math.floor(h / 24)}d`;
5455
- }
5456
- function fmtBytes(b) {
5457
- if (b < 1024) return `${b}B`;
5458
- if (b < 1024 ** 2) return `${(b / 1024).toFixed(0)}K`;
5459
- if (b < 1024 ** 3) return `${(b / 1024 ** 2).toFixed(1)}M`;
5460
- return `${(b / 1024 ** 3).toFixed(2)}G`;
5461
- }
5462
- function fmtPorts(p) {
5463
- const parts = [];
5464
- for (const c of p.checks) {
5465
- if (c.status === "listening") parts.push(`:${c.port} \u2713`);
5466
- else if (c.held_by) {
5467
- const who = c.held_by.app ? `app ${c.held_by.app}` : c.held_by.container ? `container ${c.held_by.container}` : c.held_by.pid !== null ? `pid ${c.held_by.pid}${c.held_by.command ? ` (${c.held_by.command})` : ""}` : "another user's process";
5468
- parts.push(`:${c.port} \u2717 held by ${who}`);
5469
- } else parts.push(`:${c.port} \u2717 nothing listens`);
5470
- }
5625
+ // src/monit/screentext.ts
5626
+ import { charWidth, chars } from "@pm2/blessed/lib/unicode.js";
5627
+ var segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
5628
+ var EMOJI_PRESENTATION = new RegExp("\\p{Emoji_Presentation}", "u");
5629
+ var PICTOGRAPH = new RegExp("\\p{Extended_Pictographic}", "u");
5630
+ var REGIONAL = new RegExp("\\p{Regional_Indicator}", "u");
5631
+ var KEYCAP = /^[0-9#*]\uFE0F?\u20E3$/;
5632
+ var INVISIBLE = /[\p{Cc}\p{Cf}]/u;
5633
+ var LINE_BREAK = /\r\n|[\n\v\f\u0085\u2028\u2029]/g;
5634
+ var MARK2 = new RegExp("\\p{M}", "u");
5635
+ var SPACING_MARK = new RegExp("\\p{Mc}", "u");
5636
+ var BLESSED_WIDE = new RegExp(chars.wide.source);
5637
+ var NARROW = {
5638
+ "\u2705": "\u2714",
5639
+ "\u274C": "\u2716",
5640
+ "\u274E": "\u2716",
5641
+ "\u2757": "!",
5642
+ "\u2755": "!",
5643
+ "\u2753": "?",
5644
+ "\u2754": "?"
5645
+ };
5646
+ var TAB = 8;
5647
+ function displayWidth(s) {
5648
+ let w = 0;
5649
+ for (const ch of s.includes("\x1B") ? s.replace(SGR_RE, "") : s)
5650
+ w += charWidth(ch.codePointAt(0)) === 2 ? 2 : 1;
5651
+ return w;
5652
+ }
5653
+ function columns(s) {
5654
+ let w = 0;
5655
+ for (const ch of s) w += charWidth(ch.codePointAt(0));
5656
+ return w;
5657
+ }
5658
+ function keepMark(ch, cp) {
5659
+ return cp <= 65535 && charWidth(cp) === (SPACING_MARK.test(ch) ? 1 : 0);
5660
+ }
5661
+ function cluster(c) {
5662
+ if (c === " ") return c;
5663
+ const first = String.fromCodePoint(c.codePointAt(0));
5664
+ if (REGIONAL.test(first))
5665
+ return [...c].filter((ch) => REGIONAL.test(ch)).map((ch) => String.fromCharCode(ch.codePointAt(0) - 127462 + 65)).join("");
5666
+ if (KEYCAP.test(c)) return c[0];
5667
+ if (PICTOGRAPH.test(first) || EMOJI_PRESENTATION.test(first)) {
5668
+ if (NARROW[first]) return NARROW[first];
5669
+ return !EMOJI_PRESENTATION.test(first) && first.length === 1 ? first : "?";
5670
+ }
5671
+ let out = "";
5672
+ for (const ch of c) {
5673
+ if (INVISIBLE.test(ch)) continue;
5674
+ const cp = ch.codePointAt(0);
5675
+ if (MARK2.test(ch)) {
5676
+ if (keepMark(ch, cp)) out += ch;
5677
+ continue;
5678
+ }
5679
+ if (cp > 65535 || cp >= 55296 && cp <= 57343) {
5680
+ out += "?";
5681
+ continue;
5682
+ }
5683
+ out += charWidth(cp) === 2 === BLESSED_WIDE.test(ch) ? ch : "?";
5684
+ }
5685
+ return out;
5686
+ }
5687
+ function blessedSgr(code) {
5688
+ if (!code.includes(":")) return code;
5689
+ const groups = code.slice(2, -1).split(";").flatMap((g) => {
5690
+ if (!g.includes(":")) return [g];
5691
+ const [kind, mode, ...rest] = g.split(":");
5692
+ if (kind === "4") return ["4"];
5693
+ if (!["38", "48", "58"].includes(kind)) return [];
5694
+ if (mode === "5" && rest[0]) return [`${kind};5;${rest[0]}`];
5695
+ if (mode === "2" && rest.length >= 3)
5696
+ return [`${kind};2;${rest.slice(-3).join(";")}`];
5697
+ return [];
5698
+ });
5699
+ return groups.length ? `\x1B[${groups.join(";")}m` : "";
5700
+ }
5701
+ var SGR_SPLIT = /(\x1b\[[0-9;:]*m)/;
5702
+ var visible = (t) => t.replace(SGR_RE, "");
5703
+ function screenText(input, opts = {}) {
5704
+ const parts = (opts.colors ? keepSgr(input) : stripAnsi(input)).replace(/\r\n/g, "\n").split("\r");
5705
+ let last = parts.length - 1;
5706
+ while (last > 0 && visible(parts[last]) === "") last--;
5707
+ const carried = parts.slice(0, last).flatMap((p) => p.match(SGR_RE) ?? []).join("");
5708
+ let out = "";
5709
+ let col = 0;
5710
+ for (const piece of (carried + parts[last]).split(SGR_SPLIT)) {
5711
+ if (SGR_SPLIT.test(piece) && piece.startsWith("\x1B")) {
5712
+ out += blessedSgr(piece);
5713
+ continue;
5714
+ }
5715
+ for (const { segment } of segmenter.segment(
5716
+ piece.replace(LINE_BREAK, " ").normalize("NFC")
5717
+ )) {
5718
+ const c = cluster(segment);
5719
+ if (c === " ") {
5720
+ const n = TAB - col % TAB;
5721
+ out += " ".repeat(n);
5722
+ col += n;
5723
+ continue;
5724
+ }
5725
+ out += c;
5726
+ col += columns(c);
5727
+ }
5728
+ }
5729
+ return out;
5730
+ }
5731
+ function cutWidth(s, width) {
5732
+ if (displayWidth(s) <= width) return s;
5733
+ let out = "";
5734
+ let w = 0;
5735
+ for (const ch of s) {
5736
+ const cw = displayWidth(ch);
5737
+ if (w + cw > width - 1) break;
5738
+ out += ch;
5739
+ w += cw;
5740
+ }
5741
+ return `${out}\u2026`;
5742
+ }
5743
+ function cutStartWidth(s, width) {
5744
+ if (displayWidth(s) <= width) return s;
5745
+ const all = [...s];
5746
+ let out = "";
5747
+ for (let i = all.length - 1; i >= 0; i--) {
5748
+ if (displayWidth(all[i] + out) > width - 1) break;
5749
+ out = all[i] + out;
5750
+ }
5751
+ return `\u2026${out}`;
5752
+ }
5753
+
5754
+ // src/mcp/render.ts
5755
+ function fmtUptime(ms) {
5756
+ if (ms === null) return "-";
5757
+ const s = Math.floor(ms / 1e3);
5758
+ if (s < 60) return `${s}s`;
5759
+ const m = Math.floor(s / 60);
5760
+ if (m < 60) return `${m}m`;
5761
+ const h = Math.floor(m / 60);
5762
+ if (h < 48) return `${h}h${m % 60}m`;
5763
+ return `${Math.floor(h / 24)}d`;
5764
+ }
5765
+ function fmtBytes(b) {
5766
+ if (b < 1024) return `${b}B`;
5767
+ if (b < 1024 ** 2) return `${(b / 1024).toFixed(0)}K`;
5768
+ if (b < 1024 ** 3) return `${(b / 1024 ** 2).toFixed(1)}M`;
5769
+ return `${(b / 1024 ** 3).toFixed(2)}G`;
5770
+ }
5771
+ function fmtPorts(p) {
5772
+ const parts = [];
5773
+ for (const c of p.checks) {
5774
+ if (c.status === "listening") parts.push(`:${c.port} \u2713`);
5775
+ else if (c.held_by) {
5776
+ const who = c.held_by.app ? `app ${c.held_by.app}` : c.held_by.container ? `container ${c.held_by.container}` : c.held_by.pid !== null ? `pid ${c.held_by.pid}${c.held_by.command ? ` (${c.held_by.command})` : ""}` : "another user's process";
5777
+ parts.push(`:${c.port} \u2717 held by ${who}`);
5778
+ } else parts.push(`:${c.port} \u2717 nothing listens`);
5779
+ }
5471
5780
  for (const u of p.undeclared) parts.push(`:${u} (undeclared)`);
5472
5781
  return parts.length ? parts.join(", ") : "-";
5473
5782
  }
5474
- function table(rows) {
5475
- const w = rows[0].map(
5476
- (_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length))
5783
+ var GUTTER = 2;
5784
+ var FLEX_MIN = 12;
5785
+ function fitWidths(natural, flex, width) {
5786
+ const w = [...natural];
5787
+ const floor = (i) => Math.min(FLEX_MIN, natural[i]);
5788
+ let total = w.reduce((a, b) => a + b, 0) + GUTTER * (w.length - 1);
5789
+ while (total > width) {
5790
+ let pick = -1;
5791
+ for (const i of flex)
5792
+ if (w[i] > floor(i) && (pick < 0 || w[i] > w[pick])) pick = i;
5793
+ if (pick < 0) break;
5794
+ w[pick]--;
5795
+ total--;
5796
+ }
5797
+ return w;
5798
+ }
5799
+ function takeWidth(s, width) {
5800
+ let out = "";
5801
+ for (const ch of s) {
5802
+ if (out && displayWidth(out + ch) > width) break;
5803
+ out += ch;
5804
+ }
5805
+ return out;
5806
+ }
5807
+ function wrapCell(s, width) {
5808
+ if (displayWidth(s) <= width) return [s];
5809
+ const lines = [];
5810
+ let line = "";
5811
+ for (let t of s.match(/[^\s/,:-]*[\s/,:-]*/g)?.filter(Boolean) ?? []) {
5812
+ if (displayWidth((line + t).trimEnd()) <= width) {
5813
+ line += t;
5814
+ continue;
5815
+ }
5816
+ if (line.trim()) lines.push(line.trimEnd());
5817
+ line = "";
5818
+ while (displayWidth(t.trimEnd()) > width) {
5819
+ const head = takeWidth(t, width);
5820
+ lines.push(head);
5821
+ t = t.slice(head.length);
5822
+ }
5823
+ line = t;
5824
+ }
5825
+ if (line.trim()) lines.push(line.trimEnd());
5826
+ return lines;
5827
+ }
5828
+ function table(rows, opts = {}) {
5829
+ if (opts.width === void 0) {
5830
+ const w = rows[0].map(
5831
+ (_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length))
5832
+ );
5833
+ return rows.map(
5834
+ (r) => r.map((c, i) => (c ?? "").padEnd(w[i])).join(" ").trimEnd()
5835
+ ).join("\n");
5836
+ }
5837
+ const at = (r, i) => r[i] ?? "";
5838
+ const natural = rows[0].map(
5839
+ (_, i) => Math.max(...rows.map((r) => displayWidth(at(r, i))))
5477
5840
  );
5478
- return rows.map(
5479
- (r) => r.map((c, i) => (c ?? "").padEnd(w[i])).join(" ").trimEnd()
5480
- ).join("\n");
5841
+ const widths = fitWidths(natural, opts.flex ?? [], opts.width);
5842
+ const flex = new Set(opts.flex ?? []);
5843
+ const starts = new Set(opts.cutStart ?? []);
5844
+ const pad2 = (s, w) => s + " ".repeat(Math.max(0, w - displayWidth(s)));
5845
+ const out = [];
5846
+ for (const r of rows) {
5847
+ const cells = widths.map((w, i) => {
5848
+ const s = at(r, i);
5849
+ if (!flex.has(i) || displayWidth(s) <= w) return [s];
5850
+ if (opts.mode === "fit")
5851
+ return [starts.has(i) ? cutStartWidth(s, w) : cutWidth(s, w)];
5852
+ return wrapCell(s, w);
5853
+ });
5854
+ const height = Math.max(...cells.map((c) => c.length));
5855
+ for (let k = 0; k < height; k++)
5856
+ out.push(
5857
+ cells.map((c, i) => pad2(c[k] ?? "", widths[i])).join(" ".repeat(GUTTER)).trimEnd()
5858
+ );
5859
+ }
5860
+ return out.join("\n");
5481
5861
  }
5482
5862
  function fmtCpu(c) {
5483
5863
  return c === null ? "-" : `${c.toFixed(1)}%`;
@@ -5505,7 +5885,7 @@ function fmtRestarts(r) {
5505
5885
  const n = r.reloads;
5506
5886
  return n && n.count > 0 ? `${r.restarts} (+${n.partial ? "\u2265" : ""}${n.count})` : String(r.restarts);
5507
5887
  }
5508
- function renderLs(rows, host, errors = []) {
5888
+ function renderLs(rows, host, errors = [], width) {
5509
5889
  const hosts = [...new Set(rows.map((r) => r.host))];
5510
5890
  const multi = hosts.length > 1;
5511
5891
  const head = multi ? `hosts: ${hosts.join(", ")}` : `host: ${hosts[0] ?? host}`;
@@ -5514,40 +5894,45 @@ function renderLs(rows, host, errors = []) {
5514
5894
  rows.filter((r) => r.error).map((r) => [r.host, `${r.host}: ${r.error}`])
5515
5895
  ).values()
5516
5896
  ];
5897
+ const o = multi ? 1 : 0;
5898
+ const cap = (s, n) => width === void 0 ? cut2(s, n) : s;
5517
5899
  return [
5518
5900
  head,
5519
- table([
5901
+ table(
5520
5902
  [
5521
- "APP",
5522
- ...multi ? ["HOST"] : [],
5523
- "KIND",
5524
- "STATUS",
5525
- "PID",
5526
- "UPTIME",
5527
- "RESTARTS",
5528
- "MEM",
5529
- "CPU",
5530
- "PORTS",
5531
- "URL",
5532
- "RUNTIME",
5533
- "DESCRIPTION"
5903
+ [
5904
+ "APP",
5905
+ ...multi ? ["HOST"] : [],
5906
+ "KIND",
5907
+ "STATUS",
5908
+ "PID",
5909
+ "UPTIME",
5910
+ "RESTARTS",
5911
+ "MEM",
5912
+ "CPU",
5913
+ "PORTS",
5914
+ "URL",
5915
+ "RUNTIME",
5916
+ "DESCRIPTION"
5917
+ ],
5918
+ ...rows.map((r) => [
5919
+ r.app,
5920
+ ...multi ? [r.host] : [],
5921
+ r.kind,
5922
+ r.status,
5923
+ r.pid === null ? "-" : String(r.pid),
5924
+ fmtUptime(r.uptime_ms),
5925
+ fmtRestarts(r),
5926
+ r.memory ? fmtBytes(r.memory) : "-",
5927
+ r.status === "online" ? fmtCpu(r.cpu) : "-",
5928
+ fmtPorts(r.ports),
5929
+ fmtUrl(r),
5930
+ r.runtime ? cap(r.runtime, 32) : "-",
5931
+ cap(r.description.split("\n")[0], 40)
5932
+ ])
5534
5933
  ],
5535
- ...rows.map((r) => [
5536
- r.app,
5537
- ...multi ? [r.host] : [],
5538
- r.kind,
5539
- r.status,
5540
- r.pid === null ? "-" : String(r.pid),
5541
- fmtUptime(r.uptime_ms),
5542
- fmtRestarts(r),
5543
- r.memory ? fmtBytes(r.memory) : "-",
5544
- r.status === "online" ? fmtCpu(r.cpu) : "-",
5545
- fmtPorts(r.ports),
5546
- fmtUrl(r),
5547
- r.runtime ? cut2(r.runtime, 32) : "-",
5548
- cut2(r.description.split("\n")[0], 40)
5549
- ])
5550
- ]),
5934
+ { width, flex: [8 + o, 9 + o, 10 + o, 11 + o], mode: "wrap" }
5935
+ ),
5551
5936
  ...unreachable,
5552
5937
  ...errors.map((e) => `${e.host}: ${e.message}`)
5553
5938
  ].join("\n");
@@ -5564,36 +5949,42 @@ function startNote(rows) {
5564
5949
  );
5565
5950
  return booting.length ? `note: ${booting.map((r) => r.app).join(", ")} not listening yet; ports are sampled right after start, call wait until listening instead of polling ls` : "";
5566
5951
  }
5567
- function renderPorts(rep, host) {
5952
+ function renderPorts(rep, host, width) {
5568
5953
  if (rep.listeners.length === 0)
5569
5954
  return `host: ${host}
5570
5955
  ${rep.port ? `nothing listens on port ${rep.port}` : "no listening sockets visible"}`;
5571
5956
  return `host: ${host}
5572
- ` + table([
5573
- ["PROTO", "ADDR", "PORT", "PID", "COMMAND", "APP", "CONTAINER"],
5574
- ...rep.listeners.map((l) => [
5575
- l.proto,
5576
- l.addr,
5577
- String(l.port),
5578
- l.pid === null ? "?" : String(l.pid),
5579
- l.command ?? "?",
5580
- l.app ?? "-",
5581
- l.container ?? "-"
5582
- ])
5583
- ]);
5957
+ ` + table(
5958
+ [
5959
+ ["PROTO", "ADDR", "PORT", "PID", "COMMAND", "APP", "CONTAINER"],
5960
+ ...rep.listeners.map((l) => [
5961
+ l.proto,
5962
+ l.addr,
5963
+ String(l.port),
5964
+ l.pid === null ? "?" : String(l.pid),
5965
+ l.command ?? "?",
5966
+ l.app ?? "-",
5967
+ l.container ?? "-"
5968
+ ])
5969
+ ],
5970
+ { width, flex: [4], mode: "wrap" }
5971
+ );
5584
5972
  }
5585
- function renderHosts(info) {
5586
- return table([
5587
- ["HOST", "TRANSPORT", "CWD", "READONLY", "PM2_HOME", "APPS"],
5588
- ...info.map((h) => [
5589
- h.name,
5590
- h.transport,
5591
- h.cwd,
5592
- h.readonly ? "yes" : "no",
5593
- h.pm2_home ?? "-",
5594
- h.apps.length ? h.apps.join(", ") : "-"
5595
- ])
5596
- ]);
5973
+ function renderHosts(info, width) {
5974
+ return table(
5975
+ [
5976
+ ["HOST", "TRANSPORT", "CWD", "READONLY", "PM2_HOME", "APPS"],
5977
+ ...info.map((h) => [
5978
+ h.name,
5979
+ h.transport,
5980
+ h.cwd,
5981
+ h.readonly ? "yes" : "no",
5982
+ h.pm2_home ?? "-",
5983
+ h.apps.length ? h.apps.join(", ") : "-"
5984
+ ])
5985
+ ],
5986
+ { width, flex: [1, 2, 4, 5], mode: "wrap" }
5987
+ );
5597
5988
  }
5598
5989
  function fmtEnv(e) {
5599
5990
  const kind = e.container === "none" ? "no container" : `container (${e.container})`;
@@ -5741,7 +6132,7 @@ function fmtDeps(deps2) {
5741
6132
  (d) => d.condition === "started" ? d.app : `${d.app} (${d.condition})`
5742
6133
  ).join(", ");
5743
6134
  }
5744
- function renderProject(d) {
6135
+ function renderProject(d, width) {
5745
6136
  const L = [`project: ${d.project} (${d.root})`];
5746
6137
  const w = d.mcproc.manifest;
5747
6138
  if (w)
@@ -5765,7 +6156,7 @@ function renderProject(d) {
5765
6156
  L.push(` ${p}: ${apps.join(", ")}`);
5766
6157
  L.push(
5767
6158
  "hosts:",
5768
- renderHosts(d.hosts).split("\n").map((x) => ` ${x}`).join("\n")
6159
+ renderHosts(d.hosts, width === void 0 ? void 0 : width - 2).split("\n").map((x) => ` ${x}`).join("\n")
5769
6160
  );
5770
6161
  if (d.readme) L.push("", "README:", d.readme.trim());
5771
6162
  return L.join("\n");
@@ -5788,9 +6179,9 @@ function incidentLines(i, opts = {}) {
5788
6179
  function renderIncidents(a, opts = {}) {
5789
6180
  const tail = [];
5790
6181
  if (a.more > 0) {
5791
- const short = a.more_by === "size" ? " to keep this answer short" : "";
6182
+ const short2 = a.more_by === "size" ? " to keep this answer short" : "";
5792
6183
  tail.push(
5793
- a.taken ? `+${a.more} older incidents not shown${short}, taken with these (read logs for them)` : short ? `+${a.more} more incidents not shown${short} (pass app for one app, or read logs)` : `+${a.more} more incidents (raise max)`
6184
+ a.taken ? `+${a.more} older incidents not shown${short2}, taken with these (read logs for them)` : short2 ? `+${a.more} more incidents not shown${short2} (pass app for one app, or read logs)` : `+${a.more} more incidents (raise max)`
5794
6185
  );
5795
6186
  }
5796
6187
  for (const s of a.skipped)
@@ -5839,7 +6230,7 @@ function queuedNote(rows) {
5839
6230
  return l.state === "starting" ? `${apps}: starting (lease ${l.id})` : `${apps}: queued #${l.position}${l.waiting_for ? `: ${l.waiting_for}` : ""} (lease ${l.id}; wait on an app, or leases)`;
5840
6231
  }).join("\n");
5841
6232
  }
5842
- function renderLeases(v) {
6233
+ function renderLeases(v, width) {
5843
6234
  const head = [
5844
6235
  `state: ${v.state_dir}`,
5845
6236
  v.memory ? `memory: ${fmtMib(v.memory.available_mib)} available of ${fmtMib(v.memory.total_mib)}` : "memory: no reading on this host (leases are admitted in FIFO order without a check)",
@@ -5859,23 +6250,26 @@ function renderLeases(v) {
5859
6250
  ]);
5860
6251
  return [
5861
6252
  ...head,
5862
- table([
6253
+ table(
5863
6254
  [
5864
- "POS",
5865
- "STATE",
5866
- "KIND",
5867
- "PROJECT",
5868
- "TARGET",
5869
- "EST \u2192 NEED",
5870
- "FROM",
5871
- "WAITING FOR / ERROR",
5872
- "ID"
6255
+ [
6256
+ "POS",
6257
+ "STATE",
6258
+ "KIND",
6259
+ "PROJECT",
6260
+ "TARGET",
6261
+ "EST \u2192 NEED",
6262
+ "FROM",
6263
+ "WAITING FOR / ERROR",
6264
+ "ID"
6265
+ ],
6266
+ ...rows
5873
6267
  ],
5874
- ...rows
5875
- ])
6268
+ { width, flex: [4, 7], mode: "wrap" }
6269
+ )
5876
6270
  ].join("\n");
5877
6271
  }
5878
- function renderMutation(rows, fallback, act, waits = []) {
6272
+ function renderMutation(rows, fallback, act, waits = [], width) {
5879
6273
  const bad3 = rows.filter((r) => r.error);
5880
6274
  const notes = [
5881
6275
  act === "start" || act === "restart" ? startNote(rows) : "",
@@ -5886,7 +6280,7 @@ function renderMutation(rows, fallback, act, waits = []) {
5886
6280
  const head = bad3.length ? `${act} partly failed: ${bad3.map((r) => `${r.app} (${r.host}): ${r.error}`).join("; ")}
5887
6281
  ` : "";
5888
6282
  return {
5889
- text: head + renderLs(rows, fallback) + notes.map((n) => `
6283
+ text: head + renderLs(rows, fallback, [], width) + notes.map((n) => `
5890
6284
  ${n}`).join(""),
5891
6285
  failed: bad3.length > 0
5892
6286
  };
@@ -5940,22 +6334,123 @@ function renderWorktreeAdded(r) {
5940
6334
  L.push(`next: ${r.next}`);
5941
6335
  return L.join("\n");
5942
6336
  }
5943
- function renderWorktrees(rows) {
5944
- return table([
5945
- ["SLOT", "PROJECT", "BRANCH", "ONLINE", "LEASE", "PATH"],
5946
- ...rows.map((r) => [
5947
- r.main ? `${r.slot} (main)` : String(r.slot),
5948
- r.project,
5949
- r.branch ?? "(detached)",
5950
- String(r.online),
5951
- r.lease ?? "-",
5952
- r.path
5953
- ])
5954
- ]);
6337
+ function renderWorktrees(rows, width) {
6338
+ return table(
6339
+ [
6340
+ ["SLOT", "PROJECT", "BRANCH", "ONLINE", "LEASE", "PATH"],
6341
+ ...rows.map((r) => [
6342
+ r.main ? `${r.slot} (main)` : String(r.slot),
6343
+ r.project,
6344
+ r.branch ?? "(detached)",
6345
+ String(r.online),
6346
+ r.lease ?? "-",
6347
+ r.path
6348
+ ])
6349
+ ],
6350
+ { width, flex: [2, 5], mode: "wrap" }
6351
+ );
5955
6352
  }
5956
6353
  function renderWorktreeRemoved(r) {
5957
6354
  return `removed ${r.path}: slot ${r.slot} is free, branch ${r.branch ?? "(detached)"} kept; stopped ${r.stopped.length ? r.stopped.join(", ") : "nothing"}`;
5958
6355
  }
6356
+ function tildePath(p, home = os7.homedir()) {
6357
+ return p === home || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
6358
+ }
6359
+ function stackLabel(s, nested) {
6360
+ if (s.base_project === s.project) return s.project;
6361
+ const wt = `${s.slot !== null ? `w${s.slot} ` : ""}${s.branch ?? s.project}`;
6362
+ return nested ? ` ${wt}` : `${s.base_project} ${wt}`;
6363
+ }
6364
+ function stackApps(s) {
6365
+ if (s.queue_position !== null && s.apps.online === 0)
6366
+ return `queued #${s.queue_position}`;
6367
+ return s.present ? `${s.apps.online}/${s.apps.total}` : "not running";
6368
+ }
6369
+ function stackPorts(s, max = 3) {
6370
+ const ports = [...new Set(s.ports.map((p) => p.port))].sort((a, b) => a - b);
6371
+ if (ports.length === 0) return "-";
6372
+ const shown = ports.slice(0, max).map((p) => `:${p}`).join(" ");
6373
+ return ports.length > max ? `${shown} +${ports.length - max}` : shown;
6374
+ }
6375
+ var stackCells = (s, nested, root) => [
6376
+ stackLabel(s, nested),
6377
+ root(s.root),
6378
+ s.hosts.map((h) => h.name).join(","),
6379
+ s.manifest_error ? "manifest error" : stackApps(s),
6380
+ String(s.apps.errored),
6381
+ s.memory ? fmtBytes(s.memory) : "-",
6382
+ s.cpu === null ? "-" : `${Math.round(s.cpu)}%`,
6383
+ stackPorts(s)
6384
+ ];
6385
+ var orphanCells = (o) => {
6386
+ const mem = o.processes.reduce((a, p) => a + p.memory, 0);
6387
+ return [
6388
+ `pm2 \xB7 ${o.namespace}`,
6389
+ "(no manifest)",
6390
+ o.endpoint,
6391
+ `${o.processes.filter((p) => p.status === "online").length}/${o.processes.length}`,
6392
+ "-",
6393
+ mem ? fmtBytes(mem) : "-",
6394
+ "-",
6395
+ "-"
6396
+ ];
6397
+ };
6398
+ var STACK_HEADER = [
6399
+ "STACK",
6400
+ "ROOT",
6401
+ "HOSTS",
6402
+ "APPS",
6403
+ "ERR",
6404
+ "MEM",
6405
+ "CPU",
6406
+ "PORTS"
6407
+ ];
6408
+ function stacksHostLine(h) {
6409
+ const mem = h.memory ? `RAM ${fmtMib(h.memory.available_mib)} free of ${fmtMib(h.memory.total_mib)}` : "no memory reading";
6410
+ const watcher = h.watcher ? h.watcher.online ? "watcher online" : "watcher stopped" : "no watcher";
6411
+ return `${h.name} \xB7 ${mem} \xB7 ${watcher} \xB7 queue ${h.queue}`;
6412
+ }
6413
+ function renderStacks(a, width, home = os7.homedir()) {
6414
+ const lines = [`host: ${stacksHostLine(a.host)}`, ...a.host.notes];
6415
+ if (a.stacks.length === 0 && a.orphans.length === 0)
6416
+ return [
6417
+ ...lines,
6418
+ "no stacks running on this host (--all lists the recorded ones)"
6419
+ ].join("\n");
6420
+ const bases = new Set(a.stacks.map((s) => s.project));
6421
+ const root = (p) => width === void 0 ? p : tildePath(p, home);
6422
+ lines.push(
6423
+ table(
6424
+ [
6425
+ STACK_HEADER,
6426
+ ...a.stacks.map(
6427
+ (s) => stackCells(
6428
+ s,
6429
+ s.base_project !== s.project && bases.has(s.base_project),
6430
+ root
6431
+ )
6432
+ ),
6433
+ ...a.orphans.map(orphanCells)
6434
+ ],
6435
+ { width, flex: [1, 7], mode: "wrap" }
6436
+ )
6437
+ );
6438
+ for (const s of a.stacks) {
6439
+ if (s.manifest_error)
6440
+ lines.push(
6441
+ `${s.project}: ${s.manifest_error.code}: ${s.manifest_error.message}${s.manifest_error.hint ? ` (${s.manifest_error.hint})` : ""}`
6442
+ );
6443
+ for (const n of s.notes) lines.push(`${s.project}: ${n}`);
6444
+ for (const h of s.hosts)
6445
+ if (h.error)
6446
+ lines.push(
6447
+ `${s.project} on ${h.name}: ${h.error}${h.age_s ? ` (last read ${h.age_s}s ago)` : ""}`
6448
+ );
6449
+ }
6450
+ for (const o of a.orphans)
6451
+ if (o.note) lines.push(`pm2 \xB7 ${o.namespace}: ${o.note}`);
6452
+ return lines.join("\n");
6453
+ }
5959
6454
 
5960
6455
  // src/core/wait.ts
5961
6456
  var WAIT_TIMEOUT_DEFAULT_MS = 6e4;
@@ -6331,7 +6826,7 @@ function assertNoCollision(ctx, procs, names) {
6331
6826
  const name2 = pm2Name(ctx.manifest.project, n);
6332
6827
  const expectedCwd = appCwd(ctx.host, ctx.manifest.apps[n]);
6333
6828
  const other = procs.find(
6334
- (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path14.posix.resolve(p.cwd) !== expectedCwd)
6829
+ (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path16.posix.resolve(p.cwd) !== expectedCwd)
6335
6830
  );
6336
6831
  if (other && other.namespace !== ctx.manifest.project) {
6337
6832
  throw new McprocError(
@@ -6358,7 +6853,7 @@ async function shipEcosystem(ctx) {
6358
6853
  const eco = buildEcosystem(ctx.manifest, ctx.host);
6359
6854
  const file = ecosystemPath(ctx.host);
6360
6855
  const dir = generatedDir(ctx.host);
6361
- const gi = path14.posix.join(path14.posix.dirname(dir), ".gitignore");
6856
+ const gi = path16.posix.join(path16.posix.dirname(dir), ".gitignore");
6362
6857
  const cmd = `mkdir -p ${shq(dir)} && { [ -e ${shq(gi)} ] || printf '*\\n' > ${shq(gi)}; } && cat > ${shq(file)}`;
6363
6858
  const res = ensureOk(
6364
6859
  await ctx.transport.exec(cmd, { stdin: JSON.stringify(eco, null, 2) }),
@@ -6535,93 +7030,7 @@ function admit(opts) {
6535
7030
  }
6536
7031
 
6537
7032
  // src/core/leases/siblings.ts
6538
- import fs11 from "fs";
6539
-
6540
- // src/core/slots.ts
6541
- import crypto2 from "crypto";
6542
- import fs10 from "fs";
6543
- import path15 from "path";
6544
- function registryKey(baseProject, repo) {
6545
- return `${baseProject}-${crypto2.createHash("sha256").update(realOf(repo)).digest("hex").slice(0, 8)}`;
6546
- }
6547
- function registryPath(stateDir2, baseProject, repo) {
6548
- return path15.join(stateDir2, "slots", `${registryKey(baseProject, repo)}.json`);
6549
- }
6550
- function emptyRegistry(repo) {
6551
- return { repo, slots: {} };
6552
- }
6553
- function readRegistry(file) {
6554
- if (!fs10.existsSync(file)) return null;
6555
- return JSON.parse(fs10.readFileSync(file, "utf8"));
6556
- }
6557
- function writeRegistry(file, r) {
6558
- fs10.mkdirSync(path15.dirname(file), { recursive: true });
6559
- const tmp = `${file}.${process.pid}.tmp`;
6560
- fs10.writeFileSync(tmp, JSON.stringify(r, null, 2));
6561
- fs10.renameSync(tmp, file);
6562
- }
6563
- function withMain(r, entry) {
6564
- return { ...r, slots: { ...r.slots, "0": entry } };
6565
- }
6566
- function claimSlot(r, opts) {
6567
- if (opts.want !== void 0) {
6568
- if (opts.want < 1 || opts.want > opts.max)
6569
- throw new McprocError(
6570
- "BAD_ARGUMENT",
6571
- `slot ${opts.want} is outside 1..${opts.max} (slot 0 is the main checkout)`
6572
- );
6573
- if (Object.hasOwn(r.slots, String(opts.want)))
6574
- throw new McprocError(
6575
- "SLOT_TAKEN",
6576
- `slot ${opts.want} is taken by ${r.slots[String(opts.want)].path}`,
6577
- { hint: "Pick another slot, or remove that worktree first." }
6578
- );
6579
- return {
6580
- registry: { ...r, slots: { ...r.slots, [opts.want]: opts.entry } },
6581
- slot: opts.want
6582
- };
6583
- }
6584
- for (let s = 1; s <= opts.max; s++)
6585
- if (!Object.hasOwn(r.slots, String(s)))
6586
- return {
6587
- registry: { ...r, slots: { ...r.slots, [s]: opts.entry } },
6588
- slot: s
6589
- };
6590
- throw new McprocError(
6591
- "NO_FREE_SLOT",
6592
- `all ${opts.max} worktree slots of ${r.repo} are taken`,
6593
- {
6594
- hint: "Remove a worktree (worktree remove), or raise worktree.slots in the manifest."
6595
- }
6596
- );
6597
- }
6598
- function releaseSlot(r, slot) {
6599
- const slots = { ...r.slots };
6600
- delete slots[String(slot)];
6601
- return { ...r, slots };
6602
- }
6603
- function realOf(p) {
6604
- try {
6605
- return fs10.realpathSync(p);
6606
- } catch {
6607
- return path15.resolve(p);
6608
- }
6609
- }
6610
- function reclaimGone(r, exists) {
6611
- const freed = [];
6612
- let out = r;
6613
- for (const [k, e] of Object.entries(r.slots)) {
6614
- if (k === "0" || exists(realOf(e.path))) continue;
6615
- out = releaseSlot(out, Number(k));
6616
- freed.push(Number(k));
6617
- }
6618
- return { registry: out, freed: freed.sort((a, b) => a - b) };
6619
- }
6620
- function siblingsOf(r, root) {
6621
- return Object.entries(r.slots).filter(([, e]) => realOf(e.path) !== realOf(root)).map(([k, entry]) => ({ slot: Number(k), entry })).sort((a, b) => a.slot - b.slot);
6622
- }
6623
-
6624
- // src/core/leases/siblings.ts
7033
+ import fs12 from "fs";
6625
7034
  var MIB = 1024 * 1024;
6626
7035
  var repoOf = (m) => m.repo;
6627
7036
  function registryOf(m, dir = stateDir()) {
@@ -6631,7 +7040,7 @@ function ownsManifest(root) {
6631
7040
  const found = findManifestPath(root);
6632
7041
  if (!found) return false;
6633
7042
  try {
6634
- return fs11.realpathSync(found.root) === fs11.realpathSync(root);
7043
+ return fs12.realpathSync(found.root) === fs12.realpathSync(root);
6635
7044
  } catch {
6636
7045
  return false;
6637
7046
  }
@@ -6649,7 +7058,7 @@ async function listCheckout(root, spawner) {
6649
7058
  async function measureSiblings(m, deps2) {
6650
7059
  const out = /* @__PURE__ */ new Map();
6651
7060
  if (!deps2.registry) return out;
6652
- const exists = deps2.exists ?? ((p) => fs11.existsSync(p));
7061
+ const exists = deps2.exists ?? ((p) => fs12.existsSync(p));
6653
7062
  const read = await Promise.all(
6654
7063
  siblingsOf(deps2.registry, m.root).filter(({ entry }) => exists(entry.path)).map(async ({ slot, entry }) => {
6655
7064
  try {
@@ -7040,7 +7449,15 @@ function failedRows(fleet, r, e) {
7040
7449
  );
7041
7450
  }
7042
7451
  async function actOn(fleet, target, act, opts = {}) {
7043
- const named = fleet.select([target]);
7452
+ if (act === "start" && Array.isArray(target))
7453
+ throw new McprocError(
7454
+ "BAD_ARGUMENT",
7455
+ `start takes one app or profile, not a list (${target.join(", ")})`,
7456
+ {
7457
+ hint: "Start one app or profile; a list of targets is for stop, restart and delete."
7458
+ }
7459
+ );
7460
+ const named = fleet.select(Array.isArray(target) ? target : [target]);
7044
7461
  const names = act === "start" ? fleet.withDeps(named) : named;
7045
7462
  const runs = fleet.runs(names);
7046
7463
  assertWritableRuns(fleet, runs, named);
@@ -7082,6 +7499,7 @@ async function actOn(fleet, target, act, opts = {}) {
7082
7499
  r.apps,
7083
7500
  {
7084
7501
  target,
7502
+ // a start of a list is refused above
7085
7503
  host: fleet.requested ?? null,
7086
7504
  reload: opts.reload,
7087
7505
  note: opts.note,
@@ -7148,62 +7566,586 @@ async function actOn(fleet, target, act, opts = {}) {
7148
7566
  rows.push(...failedRows(fleet, r, e));
7149
7567
  }
7150
7568
  }
7151
- if (failed && done === 0) throw failed.error;
7152
- const local = rows.filter((r) => pickHost(fleet.manifest, r.host).local);
7153
- if (act === "start" && opts.lease && local.length > 0)
7154
- await finishLeasedStart(gate.store, opts.lease, local);
7155
- return rows;
7156
- }
7157
- function probeFleet(fleet, names, take) {
7158
- return Promise.all(
7159
- fleet.groups(names).map(async (g) => {
7160
- try {
7161
- const ctx = await fleet.ctx(g.host);
7162
- return { ...g, ctx, snap: await take(ctx) };
7163
- } catch (e) {
7164
- return { ...g, error: e };
7569
+ if (failed && done === 0) throw failed.error;
7570
+ const local = rows.filter((r) => pickHost(fleet.manifest, r.host).local);
7571
+ if (act === "start" && opts.lease && local.length > 0)
7572
+ await finishLeasedStart(gate.store, opts.lease, local);
7573
+ return rows;
7574
+ }
7575
+ function probeFleet(fleet, names, take) {
7576
+ return Promise.all(
7577
+ fleet.groups(names).map(async (g) => {
7578
+ try {
7579
+ const ctx = await fleet.ctx(g.host);
7580
+ return { ...g, ctx, snap: await take(ctx) };
7581
+ } catch (e) {
7582
+ return { ...g, error: e };
7583
+ }
7584
+ })
7585
+ );
7586
+ }
7587
+ async function listFleet(fleet, names, opts = {}) {
7588
+ const probes = await probeFleet(
7589
+ fleet,
7590
+ names,
7591
+ (ctx) => snapshot(ctx, { urls: names, ...opts })
7592
+ );
7593
+ const failed = probes.filter((p) => p.error !== void 0);
7594
+ if (failed.length === probes.length && failed.length > 0)
7595
+ throw failed[0].error;
7596
+ const byApp = /* @__PURE__ */ new Map();
7597
+ for (const p of probes) {
7598
+ const rows = p.snap ? rowsFor(p.ctx, p.snap, p.apps) : p.apps.map(
7599
+ (a) => unreachableRow(fleet.manifest.apps[a], p.host, describeError(p.error))
7600
+ );
7601
+ for (const r of rows) byApp.set(r.app, r);
7602
+ }
7603
+ return {
7604
+ rows: names.map((n) => byApp.get(n)),
7605
+ errors: failed.map((p) => ({
7606
+ host: p.host,
7607
+ apps: p.apps,
7608
+ message: describeError(p.error)
7609
+ })),
7610
+ probes
7611
+ };
7612
+ }
7613
+ async function describeFleetApp(fleet, app) {
7614
+ const host = fleet.one(app);
7615
+ const d = await describeApp(await fleet.ctx(host), app);
7616
+ const full = fleet.manifest.apps[app];
7617
+ return {
7618
+ ...d,
7619
+ app: { ...full, depends_on: full.depends_on.map(depView) },
7620
+ host
7621
+ };
7622
+ }
7623
+
7624
+ // src/core/stacks/discover.ts
7625
+ function endpointKey(host) {
7626
+ const args = host.ssh && host.ssh_args.length ? ` [${host.ssh_args.join(" ")}]` : "";
7627
+ return `${describeHost(host)}${args}|${host.pm2_home ?? ""}`;
7628
+ }
7629
+ function endpointLabel(host) {
7630
+ if (!host.local) return host.name;
7631
+ return host.pm2_home ? `local (${host.pm2_home})` : "local";
7632
+ }
7633
+ var claimKey = (endpoint, namespace) => `${endpoint}\0${namespace}`;
7634
+ var under = (p, root) => p === root || p.startsWith(root.endsWith("/") ? root : `${root}/`);
7635
+ var bare = (dir) => dir.replace(/(.)\/+$/, "$1");
7636
+ function cwdsOn(p, key) {
7637
+ const m = p.manifest;
7638
+ if (!m) return [bare(p.root)];
7639
+ return Object.entries(p.endpoints).filter(([name2, k]) => k === key && Object.hasOwn(m.hosts, name2)).map(([name2]) => bare(m.hosts[name2].cwd));
7640
+ }
7641
+ function ownProcs(p, key, procs, claimants) {
7642
+ const mine = procs.filter((pr) => pr.namespace === p.project);
7643
+ if (claimants.length < 2) return mine;
7644
+ const dirs = claimants.flatMap(
7645
+ (c) => cwdsOn(c, key).map((dir) => ({ c, dir }))
7646
+ );
7647
+ return mine.filter((pr) => {
7648
+ let best = -1;
7649
+ let owners = /* @__PURE__ */ new Set();
7650
+ for (const d of dirs) {
7651
+ if (!under(pr.cwd, d.dir) || d.dir.length < best) continue;
7652
+ if (d.dir.length > best) {
7653
+ best = d.dir.length;
7654
+ owners = /* @__PURE__ */ new Set();
7655
+ }
7656
+ owners.add(d.c);
7657
+ }
7658
+ return owners.size === 1 && owners.has(p);
7659
+ });
7660
+ }
7661
+ function stackError(e) {
7662
+ if (!isMcprocError(e)) return { code: "ERROR", message: describeError(e) };
7663
+ return e.hint ? { code: e.code, message: e.message, hint: e.hint } : { code: e.code, message: e.message };
7664
+ }
7665
+ function procMemory(p, probe) {
7666
+ if (p.pid <= 0) return p.memory;
7667
+ const set = descendants2(probe.data.tree, p.pid);
7668
+ set.add(p.pid);
7669
+ return treeMetrics(set, probe.data.stats, probe.data.listeners)?.memory ?? p.memory;
7670
+ }
7671
+ var ERRORED = (s) => s === "errored" || s.startsWith("failed");
7672
+ var STOPPED = (s) => s === "stopped" || s === "done";
7673
+ function counts(statuses, queued) {
7674
+ return {
7675
+ total: statuses.length,
7676
+ online: statuses.filter((s) => s === "online").length,
7677
+ errored: statuses.filter(ERRORED).length,
7678
+ stopped: statuses.filter(STOPPED).length,
7679
+ queued
7680
+ };
7681
+ }
7682
+ function summary(rows) {
7683
+ const online = rows.filter((r) => r.status === "online");
7684
+ const queued = rows.flatMap(
7685
+ (r) => r.lease?.state === "pending" ? [r.lease.position] : []
7686
+ );
7687
+ const cpus = online.flatMap((r) => r.cpu === null ? [] : [r.cpu]);
7688
+ return {
7689
+ apps: counts(
7690
+ rows.map((r) => r.status),
7691
+ queued.length
7692
+ ),
7693
+ queue_position: queued.length ? Math.min(...queued) : null,
7694
+ memory: online.reduce((a, r) => a + r.memory, 0),
7695
+ cpu: cpus.length ? cpus.reduce((a, b) => a + b, 0) : null,
7696
+ ports: online.flatMap(
7697
+ (r) => [...new Set(r.ports.actual.map((l) => l.port))].sort((a, b) => a - b).map((port) => ({ app: r.app, port }))
7698
+ ),
7699
+ present: rows.some((r) => r.pm_id !== null || r.lease !== void 0)
7700
+ };
7701
+ }
7702
+ async function buildStacks(projects, reads, opts) {
7703
+ const claims = /* @__PURE__ */ new Map();
7704
+ for (const p of projects)
7705
+ for (const key of new Set(Object.values(p.endpoints))) {
7706
+ const k = claimKey(key, p.project);
7707
+ claims.set(k, [...claims.get(k) ?? [], p]);
7708
+ }
7709
+ const stacks = [];
7710
+ for (const p of projects) stacks.push(await stackOf(p, reads, claims, opts));
7711
+ const orphans = [];
7712
+ for (const r of reads.values()) {
7713
+ const probe = r.probe;
7714
+ if (!probe) continue;
7715
+ const byNs = /* @__PURE__ */ new Map();
7716
+ for (const pr of probe.procs)
7717
+ byNs.set(pr.namespace, [...byNs.get(pr.namespace) ?? [], pr]);
7718
+ for (const [ns, procs] of byNs) {
7719
+ if (ns === WATCHER_NS || claims.has(claimKey(r.key, ns))) continue;
7720
+ orphans.push({
7721
+ namespace: ns,
7722
+ endpoint: r.label,
7723
+ processes: procs.map((pr) => ({
7724
+ name: pr.name,
7725
+ status: pr.status,
7726
+ memory: procMemory(pr, probe)
7727
+ })),
7728
+ note: opts.orphanNotes?.get(claimKey(r.key, ns)) ?? null
7729
+ });
7730
+ }
7731
+ }
7732
+ orphans.sort(
7733
+ (a, b) => a.endpoint.localeCompare(b.endpoint) || a.namespace.localeCompare(b.namespace)
7734
+ );
7735
+ return { stacks, orphans };
7736
+ }
7737
+ async function stackOf(p, reads, claims, opts) {
7738
+ const hosts = Object.entries(p.endpoints).map(([name2, key]) => {
7739
+ const r = reads.get(key);
7740
+ return {
7741
+ name: name2,
7742
+ endpoint: r?.label ?? key,
7743
+ reachable: Boolean(r?.probe) && !r?.error,
7744
+ error: r?.error ?? null,
7745
+ age_s: r?.age_s ?? 0
7746
+ };
7747
+ });
7748
+ const claimantsOn = (key) => claims.get(claimKey(key, p.project)) ?? [];
7749
+ const shared = Math.max(
7750
+ 0,
7751
+ ...Object.values(p.endpoints).map((k) => claimantsOn(k).length)
7752
+ );
7753
+ const notes = shared > 1 ? [`slug shared by ${shared} checkouts`] : [];
7754
+ const rivals = [
7755
+ ...new Set(
7756
+ Object.values(p.endpoints).flatMap((k) => claimantsOn(k)).filter((c) => c !== p).map((c) => c.root)
7757
+ )
7758
+ ];
7759
+ const fromNamespace = (error) => {
7760
+ const key = p.endpoints.local;
7761
+ const probe = key ? reads.get(key)?.probe : void 0;
7762
+ const procs = probe ? ownProcs(p, key, probe.procs, claimantsOn(key)) : [];
7763
+ return {
7764
+ ...identity(p),
7765
+ hosts,
7766
+ apps: counts(
7767
+ procs.map((x) => x.status),
7768
+ 0
7769
+ ),
7770
+ queue_position: null,
7771
+ memory: probe ? procs.reduce((a, x) => a + procMemory(x, probe), 0) : 0,
7772
+ cpu: null,
7773
+ ports: [],
7774
+ manifest_error: error,
7775
+ notes,
7776
+ present: procs.length > 0,
7777
+ rows: [],
7778
+ rivals
7779
+ };
7780
+ };
7781
+ if (!p.manifest) return fromNamespace(p.error);
7782
+ let rows;
7783
+ try {
7784
+ rows = await manifestRows(p, p.manifest, reads, claimantsOn, opts);
7785
+ } catch (e) {
7786
+ return fromNamespace(stackError(e));
7787
+ }
7788
+ return {
7789
+ ...identity(p),
7790
+ hosts,
7791
+ ...summary(rows),
7792
+ manifest_error: null,
7793
+ notes,
7794
+ rows,
7795
+ rivals
7796
+ };
7797
+ }
7798
+ async function manifestRows(p, m, reads, claimantsOn, opts) {
7799
+ const byApp = /* @__PURE__ */ new Map();
7800
+ for (const g of new Fleet(m, void 0).groups(Object.keys(m.apps))) {
7801
+ const key = p.endpoints[g.host];
7802
+ const r = key ? reads.get(key) : void 0;
7803
+ let rows;
7804
+ if (!r?.probe) {
7805
+ rows = g.apps.map(
7806
+ (a) => unreachableRow(m.apps[a], g.host, r?.error ?? "not probed yet")
7807
+ );
7808
+ } else {
7809
+ const claimants = claimantsOn(key);
7810
+ const probe = claimants.length < 2 ? r.probe : {
7811
+ ...r.probe,
7812
+ procs: [
7813
+ ...r.probe.procs.filter((pr) => pr.namespace !== p.project),
7814
+ ...ownProcs(p, key, r.probe.procs, claimants)
7815
+ ]
7816
+ };
7817
+ const view = {
7818
+ manifest: scopeManifest(m, g.host, "local"),
7819
+ host: pickHost(m, g.host),
7820
+ hereCheck: opts.hereCheck
7821
+ };
7822
+ const snap = await deriveSnapshot(view, probe, {
7823
+ urls: "none",
7824
+ prev: opts.prev?.get(key)
7825
+ });
7826
+ rows = rowsFor(view, snap, g.apps);
7827
+ }
7828
+ for (const row of rows) byApp.set(row.app, row);
7829
+ }
7830
+ return Object.keys(m.apps).map((a) => byApp.get(a));
7831
+ }
7832
+ function identity(p) {
7833
+ return {
7834
+ project: p.project,
7835
+ root: p.root,
7836
+ base_project: p.manifest?.base_project ?? p.project,
7837
+ slot: p.slot,
7838
+ branch: p.branch,
7839
+ source: p.source
7840
+ };
7841
+ }
7842
+ function orderStacks(stacks, all) {
7843
+ const shown = stacks.filter((s) => all || s.present);
7844
+ const names = new Set(shown.map((s) => s.project));
7845
+ const child = (s) => s.base_project !== s.project && names.has(s.base_project);
7846
+ const bases = shown.filter((s) => !child(s)).sort(
7847
+ (a, b) => Number(b.apps.online > 0) - Number(a.apps.online > 0) || a.project.localeCompare(b.project)
7848
+ );
7849
+ const placed = /* @__PURE__ */ new Set();
7850
+ return bases.flatMap((b) => {
7851
+ if (placed.has(b.project)) return [b];
7852
+ placed.add(b.project);
7853
+ return [
7854
+ b,
7855
+ ...shown.filter((s) => child(s) && s.base_project === b.project).sort((x, y) => (x.slot ?? 0) - (y.slot ?? 0))
7856
+ ];
7857
+ });
7858
+ }
7859
+
7860
+ // src/core/stacks/collect.ts
7861
+ var REMOTE_EVERY_MS = 1e4;
7862
+ var RETRY_MS = 3e4;
7863
+ var REGISTRY_EVERY = 10;
7864
+ var ASK = {
7865
+ cpuSample: false,
7866
+ metrics: true,
7867
+ urls: [],
7868
+ containers: []
7869
+ };
7870
+ var FIRST_ASK = { ...ASK, cpuSample: true };
7871
+ function defaultCollectorDeps(spawner) {
7872
+ return {
7873
+ stateDir: stateDir(),
7874
+ now: Date.now,
7875
+ connectHost: (host) => connectHost(host, { spawner }),
7876
+ probeHost,
7877
+ loadManifest,
7878
+ findManifest: findManifestPath,
7879
+ hostname: os8.hostname,
7880
+ hereCheck: checkHere
7881
+ };
7882
+ }
7883
+ var short = (e) => describeError(e).split("\n")[0].slice(0, 120);
7884
+ var StacksCollector = class {
7885
+ constructor(deps2 = defaultCollectorDeps()) {
7886
+ this.deps = deps2;
7887
+ }
7888
+ deps;
7889
+ known = [];
7890
+ refreshes = 0;
7891
+ registryError = null;
7892
+ local = bareLocalHost();
7893
+ localKey = endpointKey(this.local);
7894
+ /** the host each endpoint is reached through: the first checkout naming it gives it; local's is the bare one */
7895
+ hosts = /* @__PURE__ */ new Map();
7896
+ /** the real roots of the checkouts this collector found on the daemon: they stay daemon-found for its life */
7897
+ adopted = /* @__PURE__ */ new Set();
7898
+ /** the walks that made no stack, by namespace and cwd, with the note each leaves; cleared with each registry read */
7899
+ misses = /* @__PURE__ */ new Map();
7900
+ links = /* @__PURE__ */ new Map();
7901
+ cache = /* @__PURE__ */ new Map();
7902
+ async collect(opts) {
7903
+ if (this.refreshes % REGISTRY_EVERY === 0) this.known = this.readKnown();
7904
+ this.refreshes++;
7905
+ const reads = /* @__PURE__ */ new Map();
7906
+ await this.readAll(reads);
7907
+ const notes = this.adopt(reads.get(this.localKey));
7908
+ await this.readAll(reads);
7909
+ const prev = /* @__PURE__ */ new Map();
7910
+ for (const key of reads.keys()) {
7911
+ const p = this.cache.get(key)?.prev;
7912
+ if (p) prev.set(key, p);
7913
+ }
7914
+ const built = await buildStacks(this.known, reads, {
7915
+ all: opts.all,
7916
+ hereCheck: this.deps.hereCheck,
7917
+ prev,
7918
+ orphanNotes: notes
7919
+ });
7920
+ const stacks = orderStacks(built.stacks, opts.all);
7921
+ const localRead = reads.get(this.localKey);
7922
+ const watcher = localRead?.probe?.procs.find(
7923
+ (p) => p.name === WATCHER_NAME && p.namespace === WATCHER_NS
7924
+ );
7925
+ const mem = localRead?.probe?.data.memory ?? null;
7926
+ let queue = 0;
7927
+ try {
7928
+ queue = new LeaseStore(this.deps.stateDir).list().filter((l) => l.state === "pending").length;
7929
+ } catch {
7930
+ }
7931
+ const answer = {
7932
+ host: {
7933
+ name: this.deps.hostname(),
7934
+ memory: mem ? { available_mib: mem.available_mib, total_mib: mem.total_mib } : null,
7935
+ watcher: watcher ? {
7936
+ online: watcher.status === "online",
7937
+ pid: watcher.pid > 0 ? watcher.pid : null
7938
+ } : null,
7939
+ queue,
7940
+ notes: [
7941
+ this.registryError ? `registry unreadable: ${this.registryError}` : "",
7942
+ localRead?.error ? `local pm2: ${localRead.error}` : ""
7943
+ ].filter(Boolean)
7944
+ },
7945
+ stacks: stacks.map(({ rows: _rows, rivals: _rivals, ...s }) => s),
7946
+ orphans: built.orphans
7947
+ };
7948
+ const manifests = new Map(
7949
+ this.known.flatMap(
7950
+ (p) => p.manifest ? [[p.root, p.manifest]] : []
7951
+ )
7952
+ );
7953
+ return { answer, stacks, manifests };
7954
+ }
7955
+ /** Reads every endpoint the known checkouts need, and the local daemon: those not read yet this refresh. */
7956
+ async readAll(reads) {
7957
+ const keys = /* @__PURE__ */ new Set([
7958
+ this.localKey,
7959
+ ...this.known.flatMap((p) => Object.values(p.endpoints))
7960
+ ]);
7961
+ await Promise.all(
7962
+ [...keys].filter((key) => !reads.has(key)).map(async (key) => {
7963
+ const host = this.hosts.get(key);
7964
+ if (host) reads.set(key, await this.read(key, host));
7965
+ })
7966
+ );
7967
+ }
7968
+ /** One endpoint's answer: a fresh probe when it is due, else (or when the probe fails) the last data with its age. */
7969
+ async read(key, host) {
7970
+ const now = this.deps.now();
7971
+ const c = this.cache.get(key);
7972
+ const answer = (e) => ({
7973
+ key,
7974
+ label: endpointLabel(host),
7975
+ probe: e.probe,
7976
+ error: e.error,
7977
+ age_s: e.probe ? Math.round((now - e.at) / 1e3) : 0
7978
+ });
7979
+ if (c && now < c.next) return answer(c);
7980
+ let entry;
7981
+ try {
7982
+ let link = this.links.get(key);
7983
+ if (!link) {
7984
+ link = this.deps.connectHost(host);
7985
+ this.links.set(key, link);
7986
+ }
7987
+ const last = c?.probe;
7988
+ const probe = await this.deps.probeHost(
7989
+ await link,
7990
+ last ? ASK : FIRST_ASK
7991
+ );
7992
+ const sample = last?.data.stats.samples.at(-1);
7993
+ entry = {
7994
+ probe,
7995
+ prev: last && sample ? { sample, at: last.sampledAt } : void 0,
7996
+ error: null,
7997
+ at: now,
7998
+ next: host.local ? now : now + REMOTE_EVERY_MS
7999
+ };
8000
+ } catch (e) {
8001
+ this.links.delete(key);
8002
+ entry = {
8003
+ probe: c?.probe ?? null,
8004
+ prev: c?.prev,
8005
+ error: short(e),
8006
+ at: c?.at ?? now,
8007
+ next: host.local ? now : now + RETRY_MS
8008
+ };
8009
+ }
8010
+ this.cache.set(key, entry);
8011
+ return answer(entry);
8012
+ }
8013
+ /** The checkouts the registry records, one per real root; an entry whose project changed follows its manifest. */
8014
+ readKnown() {
8015
+ const { entries, error } = listProjects(this.deps.stateDir);
8016
+ this.registryError = error;
8017
+ this.hosts = /* @__PURE__ */ new Map([[this.localKey, this.local]]);
8018
+ this.misses.clear();
8019
+ const byRoot = /* @__PURE__ */ new Map();
8020
+ for (const e of entries) {
8021
+ const source = this.adopted.has(realOf(e.root)) ? "daemon" : "registry";
8022
+ let m;
8023
+ try {
8024
+ m = this.deps.loadManifest(e.root);
8025
+ } catch (err) {
8026
+ byRoot.set(realOf(e.root), this.broken(e.project, e.root, source, err));
8027
+ continue;
8028
+ }
8029
+ if (m.project !== e.project) {
8030
+ forgetProject(e.project, e.root, this.deps.stateDir);
8031
+ recordProject(m, this.deps.stateDir);
8032
+ }
8033
+ byRoot.set(realOf(m.root), this.inputOf(m, source));
8034
+ }
8035
+ return [...byRoot.values()];
8036
+ }
8037
+ /** A checkout without a usable manifest: its error, and its namespace on the local daemon for what runs. */
8038
+ broken(project, root, source, e) {
8039
+ return {
8040
+ project,
8041
+ root,
8042
+ source,
8043
+ manifest: null,
8044
+ error: stackError(e),
8045
+ endpoints: { local: this.localKey },
8046
+ slot: null,
8047
+ branch: null
8048
+ };
8049
+ }
8050
+ /** A loaded checkout with its hosts' endpoints and its slot; one whose endpoints cannot be built carries the error. */
8051
+ inputOf(m, source) {
8052
+ const endpoints = {};
8053
+ const hosts = /* @__PURE__ */ new Map();
8054
+ try {
8055
+ for (const g of new Fleet(m, void 0).groups(Object.keys(m.apps))) {
8056
+ const host = pickHost(m, g.host);
8057
+ endpoints[g.host] = endpointKey(host);
8058
+ hosts.set(endpoints[g.host], host);
8059
+ }
8060
+ } catch (e) {
8061
+ return this.broken(m.project, m.root, source, e);
8062
+ }
8063
+ for (const [key, host] of hosts)
8064
+ if (!this.hosts.has(key)) this.hosts.set(key, host);
8065
+ let slot = m.slot;
8066
+ let branch = null;
8067
+ try {
8068
+ const reg = readRegistry(
8069
+ registryPath(this.deps.stateDir, m.base_project, m.repo)
8070
+ );
8071
+ const hit = reg && Object.entries(reg.slots).find(
8072
+ ([, s]) => realOf(s.path) === realOf(m.root)
8073
+ );
8074
+ if (hit) {
8075
+ slot = Number(hit[0]);
8076
+ branch = hit[1].branch;
8077
+ }
8078
+ } catch {
8079
+ }
8080
+ return {
8081
+ project: m.project,
8082
+ root: m.root,
8083
+ source,
8084
+ manifest: m,
8085
+ error: null,
8086
+ endpoints,
8087
+ slot,
8088
+ branch
8089
+ };
8090
+ }
8091
+ /**
8092
+ * The local daemon's namespaces no known checkout claims: a manifest above a process's cwd that names the namespace
8093
+ * makes it a stack, recorded from then on. One that names another project, or does not load, leaves a note on the
8094
+ * orphan; a checkout known already (its apps run elsewhere) is never a second stack. A walk that made no stack is
8095
+ * kept with its note until the registry is read again: a refresh does not parse the same manifest twice.
8096
+ */
8097
+ adopt(read) {
8098
+ const notes = /* @__PURE__ */ new Map();
8099
+ if (!read?.probe) return notes;
8100
+ const claimed = new Set(
8101
+ this.known.filter((p) => Object.values(p.endpoints).includes(this.localKey)).map((p) => p.project)
8102
+ );
8103
+ const roots = new Set(this.known.map((p) => realOf(p.root)));
8104
+ const cwds = /* @__PURE__ */ new Map();
8105
+ for (const p of read.probe.procs)
8106
+ if (p.cwd && p.namespace !== WATCHER_NS && !claimed.has(p.namespace))
8107
+ cwds.set(p.namespace, (cwds.get(p.namespace) ?? /* @__PURE__ */ new Set()).add(p.cwd));
8108
+ for (const [ns, dirs] of cwds) {
8109
+ const note = claimKey(this.localKey, ns);
8110
+ for (const cwd of dirs) {
8111
+ const missed = `${ns}\0${cwd}`;
8112
+ const r = this.misses.has(missed) ? { note: this.misses.get(missed) ?? null } : this.walk(ns, cwd, roots);
8113
+ if ("manifest" in r) {
8114
+ const real = realOf(r.manifest.root);
8115
+ recordProject(r.manifest, this.deps.stateDir);
8116
+ this.adopted.add(real);
8117
+ roots.add(real);
8118
+ this.known.push(this.inputOf(r.manifest, "daemon"));
8119
+ notes.delete(note);
8120
+ break;
8121
+ }
8122
+ this.misses.set(missed, r.note);
8123
+ if (r.note !== null) notes.set(note, r.note);
7165
8124
  }
7166
- })
7167
- );
7168
- }
7169
- async function listFleet(fleet, names, opts = {}) {
7170
- const probes = await probeFleet(
7171
- fleet,
7172
- names,
7173
- (ctx) => snapshot(ctx, { urls: names, ...opts })
7174
- );
7175
- const failed = probes.filter((p) => p.error !== void 0);
7176
- if (failed.length === probes.length && failed.length > 0)
7177
- throw failed[0].error;
7178
- const byApp = /* @__PURE__ */ new Map();
7179
- for (const p of probes) {
7180
- const rows = p.snap ? rowsFor(p.ctx, p.snap, p.apps) : p.apps.map(
7181
- (a) => unreachableRow(fleet.manifest.apps[a], p.host, describeError(p.error))
7182
- );
7183
- for (const r of rows) byApp.set(r.app, r);
8125
+ }
8126
+ return notes;
7184
8127
  }
7185
- return {
7186
- rows: names.map((n) => byApp.get(n)),
7187
- errors: failed.map((p) => ({
7188
- host: p.host,
7189
- apps: p.apps,
7190
- message: describeError(p.error)
7191
- })),
7192
- probes
7193
- };
7194
- }
7195
- async function describeFleetApp(fleet, app) {
7196
- const host = fleet.one(app);
7197
- const d = await describeApp(await fleet.ctx(host), app);
7198
- const full = fleet.manifest.apps[app];
7199
- return {
7200
- ...d,
7201
- app: { ...full, depends_on: full.depends_on.map(depView) },
7202
- host
7203
- };
7204
- }
8128
+ /** The walk up from one cwd of namespace `ns`: the manifest that makes it a stack, or the note it leaves (null: none). */
8129
+ walk(ns, cwd, roots) {
8130
+ const found = this.deps.findManifest(cwd);
8131
+ if (!found) return { note: null };
8132
+ let m;
8133
+ try {
8134
+ m = this.deps.loadManifest(found.root);
8135
+ } catch (e) {
8136
+ return { note: `manifest at ${found.path}: ${stackError(e).code}` };
8137
+ }
8138
+ if (m.project !== ns)
8139
+ return { note: `manifest at ${found.path} names ${m.project}` };
8140
+ if (roots.has(realOf(m.root))) return { note: null };
8141
+ return { manifest: m };
8142
+ }
8143
+ };
7205
8144
 
7206
8145
  // src/core/leases/watcher.ts
8146
+ import { spawn as spawn2 } from "child_process";
8147
+ import fs13 from "fs";
8148
+ import path17 from "path";
7207
8149
  async function moveIf(store, id, from, to, patch = {}, also = () => true) {
7208
8150
  return store.withLock(async () => {
7209
8151
  const cur = store.get(id);
@@ -7396,13 +8338,13 @@ async function leaveIdle(deps2) {
7396
8338
  return store.withLock(() => {
7397
8339
  if (store.list().some((l) => l.state === "pending" || l.state === "starting"))
7398
8340
  return false;
7399
- const file = path16.join(path16.dirname(store.dir), WATCHER_NOTE);
8341
+ const file = path17.join(path17.dirname(store.dir), WATCHER_NOTE);
7400
8342
  const tmp = `${file}.${process.pid}.tmp`;
7401
- fs12.writeFileSync(
8343
+ fs13.writeFileSync(
7402
8344
  tmp,
7403
8345
  JSON.stringify({ exiting_at: new Date(deps2.now()).toISOString() })
7404
8346
  );
7405
- fs12.renameSync(tmp, file);
8347
+ fs13.renameSync(tmp, file);
7406
8348
  return true;
7407
8349
  });
7408
8350
  }
@@ -7513,7 +8455,7 @@ async function watchLeases(deps2, opts = {}) {
7513
8455
  }
7514
8456
  function leaseDirWatch(dir) {
7515
8457
  return (cb) => {
7516
- const w = fs12.watch(dir, (_e, name2) => {
8458
+ const w = fs13.watch(dir, (_e, name2) => {
7517
8459
  if (!name2?.startsWith(".")) cb();
7518
8460
  });
7519
8461
  return () => w.close();
@@ -7876,8 +8818,8 @@ async function runExec(ctx, opts, deps2) {
7876
8818
 
7877
8819
  // src/core/worktree.ts
7878
8820
  import { execFile } from "child_process";
7879
- import fs13 from "fs";
7880
- import path17 from "path";
8821
+ import fs14 from "fs";
8822
+ import path18 from "path";
7881
8823
  var gitRunner = (args, cwd) => new Promise((resolve) => {
7882
8824
  execFile(
7883
8825
  "git",
@@ -7902,7 +8844,7 @@ var gitOk = async (git, args, cwd) => {
7902
8844
  };
7903
8845
  var present = (p) => {
7904
8846
  try {
7905
- fs13.lstatSync(p);
8847
+ fs14.lstatSync(p);
7906
8848
  return true;
7907
8849
  } catch {
7908
8850
  return false;
@@ -7910,15 +8852,15 @@ var present = (p) => {
7910
8852
  };
7911
8853
  async function mainCheckout(root, git) {
7912
8854
  const common = (await gitOk(git, ["rev-parse", "--git-common-dir"], root)).trim();
7913
- return fs13.realpathSync(path17.dirname(path17.resolve(root, common)));
8855
+ return fs14.realpathSync(path18.dirname(path18.resolve(root, common)));
7914
8856
  }
7915
8857
  function worktreeDir(m, main) {
7916
- return m.worktree.dir ? path17.resolve(main, m.worktree.dir) : `${main}.worktrees`;
8858
+ return m.worktree.dir ? path18.resolve(main, m.worktree.dir) : `${main}.worktrees`;
7917
8859
  }
7918
8860
  function installCommand(m, root) {
7919
8861
  if (m.worktree.install === false) return null;
7920
8862
  if (typeof m.worktree.install === "string") return m.worktree.install;
7921
- const has = (f) => fs13.existsSync(path17.join(root, f));
8863
+ const has = (f) => fs14.existsSync(path18.join(root, f));
7922
8864
  if (has("bun.lock") || has("bun.lockb")) return "bun install";
7923
8865
  if (has("yarn.lock")) return "yarn install";
7924
8866
  if (has("pnpm-lock.yaml")) return "pnpm install";
@@ -7926,22 +8868,22 @@ function installCommand(m, root) {
7926
8868
  return null;
7927
8869
  }
7928
8870
  function planCopies(main, target, copy, link, fsDeps = {}) {
7929
- const exists = fsDeps.exists ?? ((p) => fs13.existsSync(p));
7930
- const glob = fsDeps.glob ?? ((pattern, cwd) => fs13.globSync(pattern, { cwd }));
8871
+ const exists = fsDeps.exists ?? ((p) => fs14.existsSync(p));
8872
+ const glob = fsDeps.glob ?? ((pattern, cwd) => fs14.globSync(pattern, { cwd }));
7931
8873
  const copies = [];
7932
8874
  const missing = [];
7933
8875
  for (const entry of copy) {
7934
- const hits2 = /[*?[]/.test(entry) ? glob(entry, main).sort() : exists(path17.join(main, entry)) ? [entry] : [];
8876
+ const hits2 = /[*?[]/.test(entry) ? glob(entry, main).sort() : exists(path18.join(main, entry)) ? [entry] : [];
7935
8877
  if (hits2.length === 0) {
7936
8878
  missing.push(entry);
7937
8879
  continue;
7938
8880
  }
7939
8881
  for (const h of hits2)
7940
- copies.push({ from: path17.join(main, h), to: path17.join(target, h) });
8882
+ copies.push({ from: path18.join(main, h), to: path18.join(target, h) });
7941
8883
  }
7942
8884
  const links = link.map((l) => ({
7943
- from: path17.join(main, l),
7944
- to: path17.join(target, l)
8885
+ from: path18.join(main, l),
8886
+ to: path18.join(target, l)
7945
8887
  }));
7946
8888
  return { copies, links, missing };
7947
8889
  }
@@ -7957,7 +8899,7 @@ function ownManifest(target, log, instead) {
7957
8899
  }
7958
8900
  }
7959
8901
  function excludePattern(rel) {
7960
- const p = path17.posix.normalize(rel.split(path17.sep).join("/")).replace(/\/+$/, "");
8902
+ const p = path18.posix.normalize(rel.split(path18.sep).join("/")).replace(/\/+$/, "");
7961
8903
  if (p === "." || p === "" || p === ".." || p.startsWith("../") || p.startsWith("/"))
7962
8904
  return null;
7963
8905
  return `/${p.replace(/[\\*?[\]]/g, (c) => `\\${c}`).replace(/ $/, "\\ ")}`;
@@ -7981,13 +8923,13 @@ async function excludeLinks(target, links, git, log) {
7981
8923
  );
7982
8924
  return [];
7983
8925
  }
7984
- const file = path17.resolve(target, r.stdout.trim());
7985
- fs13.mkdirSync(path17.dirname(file), { recursive: true });
7986
- const had = fs13.existsSync(file) ? fs13.readFileSync(file, "utf8") : "";
8926
+ const file = path18.resolve(target, r.stdout.trim());
8927
+ fs14.mkdirSync(path18.dirname(file), { recursive: true });
8928
+ const had = fs14.existsSync(file) ? fs14.readFileSync(file, "utf8") : "";
7987
8929
  const lines = new Set(had.split("\n"));
7988
8930
  const add2 = patterns.filter((p) => !lines.has(p));
7989
8931
  if (add2.length)
7990
- fs13.appendFileSync(
8932
+ fs14.appendFileSync(
7991
8933
  file,
7992
8934
  `${had.length && !had.endsWith("\n") ? "\n" : ""}${add2.join("\n")}
7993
8935
  `
@@ -8003,7 +8945,7 @@ function registryFor(m, main, mainBranch, deps2) {
8003
8945
  });
8004
8946
  const { registry, freed } = reclaimGone(
8005
8947
  read,
8006
- deps2.exists ?? ((p) => fs13.existsSync(p))
8948
+ deps2.exists ?? ((p) => fs14.existsSync(p))
8007
8949
  );
8008
8950
  for (const s of freed) deps2.log(`slot ${s} reclaimed: its checkout is gone`);
8009
8951
  return { file, registry };
@@ -8033,12 +8975,12 @@ async function worktreeList(main, git) {
8033
8975
  }
8034
8976
  function realAhead(p) {
8035
8977
  const rest = [];
8036
- let d = path17.resolve(p);
8037
- while (!present(d) && path17.dirname(d) !== d) {
8038
- rest.unshift(path17.basename(d));
8039
- d = path17.dirname(d);
8978
+ let d = path18.resolve(p);
8979
+ while (!present(d) && path18.dirname(d) !== d) {
8980
+ rest.unshift(path18.basename(d));
8981
+ d = path18.dirname(d);
8040
8982
  }
8041
- return path17.join(realOf(d), ...rest);
8983
+ return path18.join(realOf(d), ...rest);
8042
8984
  }
8043
8985
  var shPath = (p) => /^[\w./@%+:-]+$/.test(p) ? p : shq(p);
8044
8986
  async function worktreeAdd(m, opts, deps2) {
@@ -8075,7 +9017,7 @@ async function worktreeAdd(m, opts, deps2) {
8075
9017
  }
8076
9018
  );
8077
9019
  const target = realAhead(
8078
- path17.join(worktreeDir(m, main), ...opts.branch.split("/"))
9020
+ path18.join(worktreeDir(m, main), ...opts.branch.split("/"))
8079
9021
  );
8080
9022
  const taken = () => new McprocError("WORKTREE_EXISTS", `${target} exists already`, {
8081
9023
  hint: "Remove it, or pick another branch name."
@@ -8098,9 +9040,9 @@ async function worktreeAdd(m, opts, deps2) {
8098
9040
  `project slug ${project2} is the slug of slot ${k} (${e.path})`,
8099
9041
  { hint: "Pass another project slug." }
8100
9042
  );
8101
- fs13.mkdirSync(path17.dirname(target), { recursive: true });
9043
+ fs14.mkdirSync(path18.dirname(target), { recursive: true });
8102
9044
  try {
8103
- fs13.mkdirSync(target);
9045
+ fs14.mkdirSync(target);
8104
9046
  } catch (e) {
8105
9047
  if (e.code === "EEXIST") throw taken();
8106
9048
  throw e;
@@ -8135,7 +9077,7 @@ async function worktreeAdd(m, opts, deps2) {
8135
9077
  );
8136
9078
  } catch (e) {
8137
9079
  try {
8138
- fs13.rmdirSync(target);
9080
+ fs14.rmdirSync(target);
8139
9081
  } catch {
8140
9082
  }
8141
9083
  await store.withLock(() => {
@@ -8155,8 +9097,8 @@ async function worktreeAdd(m, opts, deps2) {
8155
9097
  repo: main,
8156
9098
  vars: (probe ?? m).slot !== null ? { slot } : void 0
8157
9099
  });
8158
- const gi = path17.join(target, ".mcproc", ".gitignore");
8159
- if (!present(gi)) fs13.writeFileSync(gi, "*\n");
9100
+ const gi = path18.join(target, ".mcproc", ".gitignore");
9101
+ if (!present(gi)) fs14.writeFileSync(gi, "*\n");
8160
9102
  const own2 = probe ? ownManifest(target, deps2.log, "the copies and the install") : null;
8161
9103
  const dm = own2 ?? m;
8162
9104
  const only = (xs, not) => [...new Set(xs)].filter((e) => !not.includes(e));
@@ -8186,15 +9128,15 @@ async function worktreeAdd(m, opts, deps2) {
8186
9128
  missing: [...ownPlan.missing, ...extraPlan.missing]
8187
9129
  };
8188
9130
  const fromOverride = [
8189
- ...addedCopies.map((c) => path17.relative(main, c.from)),
8190
- ...addedLinks.map((l) => path17.relative(main, l.from))
9131
+ ...addedCopies.map((c) => path18.relative(main, c.from)),
9132
+ ...addedLinks.map((l) => path18.relative(main, l.from))
8191
9133
  ];
8192
9134
  let step = "";
8193
9135
  try {
8194
9136
  for (const c of plan.copies) {
8195
9137
  step = `copying ${c.from}`;
8196
- fs13.mkdirSync(path17.dirname(c.to), { recursive: true });
8197
- fs13.cpSync(c.from, c.to, {
9138
+ fs14.mkdirSync(path18.dirname(c.to), { recursive: true });
9139
+ fs14.cpSync(c.from, c.to, {
8198
9140
  recursive: true,
8199
9141
  force: false,
8200
9142
  errorOnExist: false
@@ -8202,8 +9144,8 @@ async function worktreeAdd(m, opts, deps2) {
8202
9144
  }
8203
9145
  for (const l of plan.links) {
8204
9146
  step = `linking ${l.from}`;
8205
- fs13.mkdirSync(path17.dirname(l.to), { recursive: true });
8206
- if (!present(l.to)) fs13.symlinkSync(l.from, l.to);
9147
+ fs14.mkdirSync(path18.dirname(l.to), { recursive: true });
9148
+ if (!present(l.to)) fs14.symlinkSync(l.from, l.to);
8207
9149
  }
8208
9150
  } catch (e) {
8209
9151
  throw new McprocError(
@@ -8218,7 +9160,7 @@ async function worktreeAdd(m, opts, deps2) {
8218
9160
  deps2.log(`copy: ${miss} is not in ${main}, skipped`);
8219
9161
  const excluded = await excludeLinks(
8220
9162
  target,
8221
- plan.links.map((l) => path17.relative(target, l.to)),
9163
+ plan.links.map((l) => path18.relative(target, l.to)),
8222
9164
  deps2.git,
8223
9165
  deps2.log
8224
9166
  );
@@ -8247,8 +9189,8 @@ async function worktreeAdd(m, opts, deps2) {
8247
9189
  project,
8248
9190
  repo: main,
8249
9191
  manifest: own2 ? "worktree" : "main",
8250
- copied: plan.copies.map((c) => path17.relative(main, c.from)),
8251
- linked: plan.links.map((l) => path17.relative(main, l.from)),
9192
+ copied: plan.copies.map((c) => path18.relative(main, c.from)),
9193
+ linked: plan.links.map((l) => path18.relative(main, l.from)),
8252
9194
  from_override: fromOverride,
8253
9195
  excluded,
8254
9196
  missing: plan.missing,
@@ -8265,7 +9207,7 @@ async function worktreeLs(m, deps2) {
8265
9207
  branch: await currentBranch(main, deps2.git),
8266
9208
  project: m.base_project
8267
9209
  });
8268
- const exists = deps2.exists ?? ((p) => fs13.existsSync(p));
9210
+ const exists = deps2.exists ?? ((p) => fs14.existsSync(p));
8269
9211
  const branches = await worktreeList(main, deps2.git);
8270
9212
  const procs = await deps2.jlist();
8271
9213
  const leases = new LeaseStore(deps2.stateDir).list();
@@ -8315,7 +9257,7 @@ async function worktreeRemove(m, ref, opts, deps2) {
8315
9257
  if (!(await worktreeList(main, deps2.git)).has(realOf(e.path))) {
8316
9258
  let empty = false;
8317
9259
  try {
8318
- empty = fs13.readdirSync(e.path).length === 0;
9260
+ empty = fs14.readdirSync(e.path).length === 0;
8319
9261
  } catch {
8320
9262
  }
8321
9263
  if (!empty)
@@ -8324,7 +9266,7 @@ async function worktreeRemove(m, ref, opts, deps2) {
8324
9266
  `${e.path} (slot ${k}) is no git worktree of ${main}`,
8325
9267
  { hint: "Look at what is in it, remove it by hand, then worktree ls." }
8326
9268
  );
8327
- fs13.rmdirSync(e.path);
9269
+ fs14.rmdirSync(e.path);
8328
9270
  deps2.log(`${e.path} was an empty folder git never filled: removed`);
8329
9271
  await free();
8330
9272
  return { path: e.path, slot: Number(k), branch: e.branch, stopped: [] };
@@ -8345,14 +9287,15 @@ async function worktreeRemove(m, ref, opts, deps2) {
8345
9287
  main
8346
9288
  );
8347
9289
  const top = realOf(worktreeDir(m, main));
8348
- for (let d = path17.dirname(e.path); d.startsWith(`${top}${path17.sep}`); d = path17.dirname(d)) {
9290
+ for (let d = path18.dirname(e.path); d.startsWith(`${top}${path18.sep}`); d = path18.dirname(d)) {
8349
9291
  try {
8350
- fs13.rmdirSync(d);
9292
+ fs14.rmdirSync(d);
8351
9293
  } catch {
8352
9294
  break;
8353
9295
  }
8354
9296
  }
8355
9297
  await free();
9298
+ forgetProject(e.project, e.path, deps2.stateDir);
8356
9299
  return { path: e.path, slot: Number(k), branch: e.branch, stopped };
8357
9300
  }
8358
9301
  async function stopCheckout(root, opts) {
@@ -8412,10 +9355,10 @@ async function defaultWorktreeDeps(ctx, io, opts = {}) {
8412
9355
  }
8413
9356
 
8414
9357
  // src/core/incidents/store.ts
8415
- import fs14 from "fs";
9358
+ import fs15 from "fs";
8416
9359
  import { createHash as createHash3, randomUUID } from "crypto";
8417
9360
  import net from "net";
8418
- import path18 from "path";
9361
+ import path19 from "path";
8419
9362
 
8420
9363
  // src/core/incidents/taken.ts
8421
9364
  function takenPoints(s) {
@@ -8485,16 +9428,16 @@ async function pollIncidents(fleet, names, opts = {}) {
8485
9428
  };
8486
9429
  const files = targets.flatMap(
8487
9430
  (t) => STREAMS2.map((stream) => {
8488
- const path22 = stream === "out" ? t.p.out_log : t.p.err_log;
9431
+ const path23 = stream === "out" ? t.p.out_log : t.p.err_log;
8489
9432
  const mark = t.prev?.[stream];
8490
- const same = mark && mark.path === path22;
9433
+ const same = mark && mark.path === path23;
8491
9434
  const emptied = opts.restart?.[markKey(g.host, t.app)]?.[stream];
8492
- const restarted = !!emptied && (emptied.includes(path22) || emptied.includes("*"));
9435
+ const restarted = !!emptied && (emptied.includes(path23) || emptied.includes("*"));
8493
9436
  const anchor = !restarted && same && mark.at !== void 0 && mark.anchor !== void 0 ? { at: mark.at, hex: mark.anchor } : null;
8494
9437
  return {
8495
9438
  t,
8496
9439
  stream,
8497
- path: path22,
9440
+ path: path23,
8498
9441
  offset: restarted ? 0 : same ? mark.offset : null,
8499
9442
  anchor,
8500
9443
  restarted
@@ -8758,7 +9701,7 @@ function processEvents(app, host, p, prev, errLines) {
8758
9701
  }
8759
9702
 
8760
9703
  // src/core/incidents/store.ts
8761
- var fileClearKey = (key, stream, path22) => `${key}\0${stream}\0${path22}`;
9704
+ var fileClearKey = (key, stream, path23) => `${key}\0${stream}\0${path23}`;
8762
9705
  var countedClearKey = (key) => `${key}\0counted`;
8763
9706
  var isAppClearKey = (k) => !k.includes("\0");
8764
9707
  function parseFileClearKey(k) {
@@ -8767,13 +9710,13 @@ function parseFileClearKey(k) {
8767
9710
  return { key, stream, path: rest.join("\0") };
8768
9711
  }
8769
9712
  var LOCK_WAIT_MS2 = 3e4;
8770
- var storePath = (manifestPath) => path18.join(path18.dirname(manifestPath), "incidents.json");
9713
+ var storePath = (manifestPath) => path19.join(path19.dirname(manifestPath), "incidents.json");
8771
9714
  var pad = (n) => String(n).padStart(2, "0");
8772
9715
  var localStamp = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
8773
9716
  function readStore(file) {
8774
9717
  try {
8775
9718
  const s = JSON.parse(
8776
- fs14.readFileSync(file, "utf8")
9719
+ fs15.readFileSync(file, "utf8")
8777
9720
  );
8778
9721
  const obj = (o) => typeof o === "object" && o !== null ? o : {};
8779
9722
  if (s && s.v === 1 && typeof s.apps === "object" && s.apps !== null)
@@ -8788,12 +9731,12 @@ function readStore(file) {
8788
9731
  return { v: 1, apps: {}, cleared: {}, clears: {} };
8789
9732
  }
8790
9733
  function lockAddress(file) {
8791
- let dir = path18.dirname(path18.resolve(file));
9734
+ let dir = path19.dirname(path19.resolve(file));
8792
9735
  try {
8793
- dir = fs14.realpathSync(dir);
9736
+ dir = fs15.realpathSync(dir);
8794
9737
  } catch {
8795
9738
  }
8796
- const id = createHash3("sha256").update(path18.join(dir, path18.basename(file))).digest();
9739
+ const id = createHash3("sha256").update(path19.join(dir, path19.basename(file))).digest();
8797
9740
  return process.platform === "linux" ? { path: `\0mcproc-incidents-${id.toString("hex").slice(0, 32)}` } : { host: "127.0.0.1", port: 4e4 + id.readUInt16BE(0) % 9e3 };
8798
9741
  }
8799
9742
  function tryLock(addr) {
@@ -8829,8 +9772,8 @@ async function withStore(file, fn, waitMs = LOCK_WAIT_MS2) {
8829
9772
  try {
8830
9773
  const { result, next } = await fn(readStore(file));
8831
9774
  const tmp = `${file}.${randomUUID()}.tmp`;
8832
- fs14.writeFileSync(tmp, JSON.stringify(next));
8833
- fs14.renameSync(tmp, file);
9775
+ fs15.writeFileSync(tmp, JSON.stringify(next));
9776
+ fs15.renameSync(tmp, file);
8834
9777
  return result;
8835
9778
  } finally {
8836
9779
  await new Promise((r) => lock.close(r));
@@ -8897,8 +9840,8 @@ async function clearLogs(fleet, names, file = storePath(fleet.manifest.path), no
8897
9840
  for (const f of await flushLogs(await fleet.ctx(g.host), g.apps)) {
8898
9841
  const key = markKey(g.host, f.app);
8899
9842
  const prev = s.apps[key];
8900
- const mark = (path22, was) => f.failed.includes(path22) ? was : {
8901
- path: path22,
9843
+ const mark = (path23, was) => f.failed.includes(path23) ? was : {
9844
+ path: path23,
8902
9845
  offset: 0,
8903
9846
  ...was?.stamp ? { stamp: was.stamp } : {}
8904
9847
  };
@@ -8916,15 +9859,15 @@ async function clearLogs(fleet, names, file = storePath(fleet.manifest.path), no
8916
9859
  const ck = countedClearKey(key);
8917
9860
  next.clears[ck] = (s.clears[ck] ?? 0) + 1;
8918
9861
  }
8919
- for (const [stream, path22] of [
9862
+ for (const [stream, path23] of [
8920
9863
  ["out", f.proc.out_log],
8921
9864
  ["err", f.proc.err_log]
8922
9865
  ]) {
8923
- if (f.failed.includes(path22)) continue;
8924
- const fk = fileClearKey(key, stream, path22);
9866
+ if (f.failed.includes(path23)) continue;
9867
+ const fk = fileClearKey(key, stream, path23);
8925
9868
  next.clears[fk] = (s.clears[fk] ?? 0) + 1;
8926
9869
  }
8927
- const freed = (path22, bytes) => f.failed.includes(path22) ? 0 : bytes;
9870
+ const freed = (path23, bytes) => f.failed.includes(path23) ? 0 : bytes;
8928
9871
  out.push({
8929
9872
  app: f.app,
8930
9873
  host: g.host,
@@ -9170,22 +10113,22 @@ async function doctor(m, hostName, spawner, hereCheck) {
9170
10113
  }
9171
10114
 
9172
10115
  // src/core/init.ts
9173
- import fs16 from "fs";
9174
- import path20 from "path";
10116
+ import fs17 from "fs";
10117
+ import path21 from "path";
9175
10118
  import { parse as parseYaml3 } from "yaml";
9176
10119
 
9177
10120
  // src/core/turbo.ts
9178
- import fs15 from "fs";
9179
- import path19 from "path";
10121
+ import fs16 from "fs";
10122
+ import path20 from "path";
9180
10123
  function readJson2(p) {
9181
10124
  try {
9182
- return JSON.parse(fs15.readFileSync(p, "utf8"));
10125
+ return JSON.parse(fs16.readFileSync(p, "utf8"));
9183
10126
  } catch {
9184
10127
  return null;
9185
10128
  }
9186
10129
  }
9187
10130
  function readTurbo(root) {
9188
- const raw = readJson2(path19.join(root, "turbo.json"));
10131
+ const raw = readJson2(path20.join(root, "turbo.json"));
9189
10132
  const tasks = raw?.tasks ?? raw?.pipeline;
9190
10133
  if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) return null;
9191
10134
  const names = Object.keys(tasks);
@@ -9206,14 +10149,14 @@ function isDistLib(pkg2) {
9206
10149
  function loadWorkspace(root, dirs) {
9207
10150
  const out = [];
9208
10151
  for (const dir of dirs) {
9209
- const pkg2 = readJson2(path19.join(dir, "package.json"));
10152
+ const pkg2 = readJson2(path20.join(dir, "package.json"));
9210
10153
  if (!pkg2) continue;
9211
10154
  const deps2 = /* @__PURE__ */ new Set();
9212
10155
  for (const k of ["dependencies", "devDependencies", "peerDependencies"])
9213
10156
  for (const d of Object.keys(pkg2[k] ?? {})) deps2.add(d);
9214
10157
  out.push({
9215
- name: String(pkg2.name ?? path19.basename(dir)),
9216
- dir: path19.relative(root, dir) || ".",
10158
+ name: String(pkg2.name ?? path20.basename(dir)),
10159
+ dir: path20.relative(root, dir) || ".",
9217
10160
  scripts: { ...pkg2.scripts ?? {} },
9218
10161
  deps: [...deps2],
9219
10162
  distLib: isDistLib(pkg2)
@@ -9246,11 +10189,11 @@ function turboBin(pm) {
9246
10189
  function libsFilters(libs, globParents, all = []) {
9247
10190
  const folded = new Set(libs.map((l) => l.name));
9248
10191
  const shared = new Set(
9249
- all.filter((p) => p.scripts.build && !folded.has(p.name)).map((p) => path19.dirname(p.dir))
10192
+ all.filter((p) => p.scripts.build && !folded.has(p.name)).map((p) => path20.dirname(p.dir))
9250
10193
  );
9251
10194
  const out = [];
9252
10195
  for (const l of libs) {
9253
- const parent = path19.dirname(l.dir);
10196
+ const parent = path20.dirname(l.dir);
9254
10197
  const f = globParents.includes(parent) && !shared.has(parent) ? `--filter='./${parent}/*'` : `--filter=${l.name}`;
9255
10198
  if (!out.includes(f)) out.push(f);
9256
10199
  }
@@ -9277,9 +10220,9 @@ function sanitizeName(s) {
9277
10220
  return n || "app";
9278
10221
  }
9279
10222
  function pmFor(root) {
9280
- if (fs16.existsSync(path20.join(root, "yarn.lock"))) return "yarn";
9281
- if (fs16.existsSync(path20.join(root, "pnpm-lock.yaml"))) return "pnpm";
9282
- if (fs16.existsSync(path20.join(root, "bun.lock")) || fs16.existsSync(path20.join(root, "bun.lockb")))
10223
+ if (fs17.existsSync(path21.join(root, "yarn.lock"))) return "yarn";
10224
+ if (fs17.existsSync(path21.join(root, "pnpm-lock.yaml"))) return "pnpm";
10225
+ if (fs17.existsSync(path21.join(root, "bun.lock")) || fs17.existsSync(path21.join(root, "bun.lockb")))
9283
10226
  return "bun run";
9284
10227
  return "npm run";
9285
10228
  }
@@ -9287,12 +10230,12 @@ function walk(dir, maxDepth, depth = 0) {
9287
10230
  const out = [];
9288
10231
  let entries;
9289
10232
  try {
9290
- entries = fs16.readdirSync(dir, { withFileTypes: true });
10233
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
9291
10234
  } catch {
9292
10235
  return out;
9293
10236
  }
9294
10237
  for (const e of entries) {
9295
- const p = path20.join(dir, e.name);
10238
+ const p = path21.join(dir, e.name);
9296
10239
  if (e.isDirectory()) {
9297
10240
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".")) continue;
9298
10241
  if (depth < maxDepth) out.push(...walk(p, maxDepth, depth + 1));
@@ -9373,7 +10316,7 @@ function vitePort(dir, section2, config) {
9373
10316
  let raw;
9374
10317
  for (const f of config ? [config] : VITE_CONFIGS) {
9375
10318
  try {
9376
- raw = fs16.readFileSync(path20.resolve(dir, f), "utf8");
10319
+ raw = fs17.readFileSync(path21.resolve(dir, f), "utf8");
9377
10320
  break;
9378
10321
  } catch {
9379
10322
  }
@@ -9397,9 +10340,9 @@ function guessPort(cmd, dir) {
9397
10340
  const m = /(?:\bPORT=|--port[= ]|\s-p\s)(\d{2,5})\b/.exec(cmd);
9398
10341
  if (m) return Number(m[1]);
9399
10342
  for (const f of [".env", ".env.local", ".env.development"]) {
9400
- const p = path20.join(dir, f);
9401
- if (!fs16.existsSync(p)) continue;
9402
- const e = /^\s*PORT=["']?(\d{2,5})/m.exec(fs16.readFileSync(p, "utf8"));
10343
+ const p = path21.join(dir, f);
10344
+ if (!fs17.existsSync(p)) continue;
10345
+ const e = /^\s*PORT=["']?(\d{2,5})/m.exec(fs17.readFileSync(p, "utf8"));
9403
10346
  if (e) return Number(e[1]);
9404
10347
  }
9405
10348
  if (NEXT_RE.test(cmd)) return 3e3;
@@ -9410,10 +10353,10 @@ function guessPort(cmd, dir) {
9410
10353
  const p = vitePort(dir, section2, config);
9411
10354
  return p === null ? void 0 : p ?? (section2 === "server" ? 5173 : 4173);
9412
10355
  }
9413
- const src = path20.join(dir, "src");
9414
- if (fs16.existsSync(src)) {
10356
+ const src = path21.join(dir, "src");
10357
+ if (fs17.existsSync(src)) {
9415
10358
  for (const f of walk(src, 3).filter((x) => /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(x)).slice(0, 200)) {
9416
- const text = fs16.readFileSync(f, "utf8");
10359
+ const text = fs17.readFileSync(f, "utf8");
9417
10360
  const l = /\.listen\(\s*(\d{2,5})\b/.exec(text) ?? /\b(?:DEFAULT_)?PORT\b\s*(?::\s*number)?\s*=\s*(\d{2,5})\b/.exec(
9418
10361
  text
9419
10362
  ) ?? /\bPORT\s*\)?\s*(?:\?\?|\|\|)\s*["']?(\d{2,5})\b/.exec(text);
@@ -9436,10 +10379,10 @@ var notAwaited = (lib) => {
9436
10379
  return `${app}'s first build is not awaited; uncomment ${app}-build and add { app: ${app}-build, condition: completed } to its depends_on to wait for it`;
9437
10380
  };
9438
10381
  function packageCandidates(root, pkgDir, pm, turbo) {
9439
- const pkg2 = readJson2(path20.join(pkgDir, "package.json"));
10382
+ const pkg2 = readJson2(path21.join(pkgDir, "package.json"));
9440
10383
  if (!pkg2?.scripts) return [];
9441
- const rel = path20.relative(root, pkgDir) || ".";
9442
- const appName = pkgAppName(String(pkg2.name ?? path20.basename(pkgDir)));
10384
+ const rel = path21.relative(root, pkgDir) || ".";
10385
+ const appName = pkgAppName(String(pkg2.name ?? path21.basename(pkgDir)));
9443
10386
  const prefix = rel === "." ? "" : `${appName}-`;
9444
10387
  const file = `${rel === "." ? "" : `${rel}/`}package.json`;
9445
10388
  const out = [];
@@ -9482,21 +10425,21 @@ function expandWorkspaces(root, patterns) {
9482
10425
  for (const pat of patterns) {
9483
10426
  const glob = /^(.*?)\/\*\*?$/.exec(pat);
9484
10427
  if (glob) {
9485
- const base = path20.join(root, glob[1]);
9486
- globParents.push(path20.relative(root, base));
10428
+ const base = path21.join(root, glob[1]);
10429
+ globParents.push(path21.relative(root, base));
9487
10430
  let names = [];
9488
10431
  try {
9489
- names = fs16.readdirSync(base);
10432
+ names = fs17.readdirSync(base);
9490
10433
  } catch {
9491
10434
  }
9492
10435
  names.sort();
9493
10436
  for (const d of names)
9494
- if (fs16.existsSync(path20.join(base, d, "package.json")))
9495
- dirs.add(path20.join(base, d));
10437
+ if (fs17.existsSync(path21.join(base, d, "package.json")))
10438
+ dirs.add(path21.join(base, d));
9496
10439
  } else if (pat.includes("*")) {
9497
10440
  unsupported.push(pat);
9498
- } else if (fs16.existsSync(path20.join(root, pat, "package.json"))) {
9499
- dirs.add(path20.join(root, pat));
10441
+ } else if (fs17.existsSync(path21.join(root, pat, "package.json"))) {
10442
+ dirs.add(path21.join(root, pat));
9500
10443
  }
9501
10444
  }
9502
10445
  return { dirs: [...dirs], unsupported, globParents };
@@ -9505,7 +10448,7 @@ function libsBuilders(folded, all, globParents, pm, version) {
9505
10448
  const bin = turboBin(pm);
9506
10449
  const ui = version === 2 ? " --ui=stream" : "";
9507
10450
  const filters = libsFilters(folded, globParents, all).join(" ");
9508
- const parents = [...new Set(folded.map((p) => path20.dirname(p.dir)))].join(
10451
+ const parents = [...new Set(folded.map((p) => path21.dirname(p.dir)))].join(
9509
10452
  ", "
9510
10453
  );
9511
10454
  const libs = {
@@ -9564,12 +10507,12 @@ function discover(root) {
9564
10507
  const candidates = [];
9565
10508
  const notes = [];
9566
10509
  const pm = pmFor(root);
9567
- const pkg2 = readJson2(path20.join(root, "package.json"));
10510
+ const pkg2 = readJson2(path21.join(root, "package.json"));
9568
10511
  const ws = Array.isArray(pkg2?.workspaces) ? [...pkg2.workspaces] : Array.isArray(pkg2?.workspaces?.packages) ? [...pkg2.workspaces.packages] : [];
9569
- const pnpmWs = path20.join(root, "pnpm-workspace.yaml");
9570
- if (fs16.existsSync(pnpmWs)) {
10512
+ const pnpmWs = path21.join(root, "pnpm-workspace.yaml");
10513
+ if (fs17.existsSync(pnpmWs)) {
9571
10514
  try {
9572
- const y = parseYaml3(fs16.readFileSync(pnpmWs, "utf8"));
10515
+ const y = parseYaml3(fs17.readFileSync(pnpmWs, "utf8"));
9573
10516
  if (Array.isArray(y?.packages)) ws.push(...y.packages);
9574
10517
  } catch {
9575
10518
  notes.push("pnpm-workspace.yaml could not be parsed");
@@ -9577,8 +10520,8 @@ function discover(root) {
9577
10520
  }
9578
10521
  const expanded = expandWorkspaces(root, ws);
9579
10522
  const turbo = readTurbo(root);
9580
- const turboFile = path20.join(root, "turbo.json");
9581
- if (!turbo && fs16.existsSync(turboFile) && readJson2(turboFile) === null)
10523
+ const turboFile = path21.join(root, "turbo.json");
10524
+ if (!turbo && fs17.existsSync(turboFile) && readJson2(turboFile) === null)
9582
10525
  notes.push(
9583
10526
  "turbo.json could not be parsed (comments are not read); drafted without turbo's tasks"
9584
10527
  );
@@ -9604,7 +10547,7 @@ function discover(root) {
9604
10547
  }
9605
10548
  candidates.push(...packageCandidates(root, root, pm));
9606
10549
  for (const dir of expanded.dirs) {
9607
- const p = liveTask ? pkgs.find((x) => x.dir === path20.relative(root, dir)) : void 0;
10550
+ const p = liveTask ? pkgs.find((x) => x.dir === path21.relative(root, dir)) : void 0;
9608
10551
  candidates.push(
9609
10552
  ...packageCandidates(
9610
10553
  root,
@@ -9625,20 +10568,20 @@ function discover(root) {
9625
10568
  `workspace pattern "${pat}" is not expanded by init; add its packages' scripts by hand`
9626
10569
  );
9627
10570
  for (const f of walk(root, 4).filter(
9628
- (p) => path20.basename(p) === "project.json"
10571
+ (p) => path21.basename(p) === "project.json"
9629
10572
  )) {
9630
10573
  const pj = readJson2(f);
9631
10574
  if (!pj?.targets) continue;
9632
- const name2 = String(pj.name ?? path20.basename(path20.dirname(f)));
10575
+ const name2 = String(pj.name ?? path21.basename(path21.dirname(f)));
9633
10576
  for (const t of ["serve", "dev", "build"]) {
9634
10577
  if (!pj.targets[t]) continue;
9635
10578
  candidates.push({
9636
10579
  name: sanitizeName(`${name2}-${t}`),
9637
- source: `${path20.relative(root, f)}:${t}`,
10580
+ source: `${path21.relative(root, f)}:${t}`,
9638
10581
  run: `npx nx run ${name2}:${t}${t === "build" ? " --watch" : ""}`,
9639
10582
  cwd: ".",
9640
10583
  kind: t === "build" ? "build" : "server",
9641
- port: t === "build" ? void 0 : guessPort("", path20.dirname(f))
10584
+ port: t === "build" ? void 0 : guessPort("", path21.dirname(f))
9642
10585
  });
9643
10586
  }
9644
10587
  }
@@ -9653,7 +10596,7 @@ function discover(root) {
9653
10596
  kind: guessKind(t, "")
9654
10597
  });
9655
10598
  }
9656
- const eco = readJson2(path20.join(root, "ecosystem.json"));
10599
+ const eco = readJson2(path21.join(root, "ecosystem.json"));
9657
10600
  if (Array.isArray(eco?.apps)) {
9658
10601
  for (const a of eco.apps) {
9659
10602
  if (!a?.name) continue;
@@ -9674,14 +10617,14 @@ function discover(root) {
9674
10617
  "ecosystem.config.cjs",
9675
10618
  "ecosystem.config.mjs"
9676
10619
  ]) {
9677
- if (fs16.existsSync(path20.join(root, f)))
10620
+ if (fs17.existsSync(path21.join(root, f)))
9678
10621
  notes.push(
9679
10622
  `${f} found: JavaScript ecosystem files are not executed by init; port its apps by hand`
9680
10623
  );
9681
10624
  }
9682
- const procfile = path20.join(root, "Procfile");
9683
- if (fs16.existsSync(procfile)) {
9684
- for (const line of fs16.readFileSync(procfile, "utf8").split("\n")) {
10625
+ const procfile = path21.join(root, "Procfile");
10626
+ if (fs17.existsSync(procfile)) {
10627
+ for (const line of fs17.readFileSync(procfile, "utf8").split("\n")) {
9685
10628
  const m = /^\s*([A-Za-z0-9_-]+):\s*(.+)$/.exec(line);
9686
10629
  if (m) {
9687
10630
  const kind = guessKind(m[1], m[2]);
@@ -9815,20 +10758,20 @@ function localReadonly(text) {
9815
10758
  }
9816
10759
  }
9817
10760
  function initProject(rootIn, opts = {}) {
9818
- const root = path20.resolve(rootIn);
9819
- if (!fs16.existsSync(root))
10761
+ const root = path21.resolve(rootIn);
10762
+ if (!fs17.existsSync(root))
9820
10763
  throw new McprocError(
9821
10764
  "MANIFEST_NOT_FOUND",
9822
10765
  `Project directory does not exist: ${root}`
9823
10766
  );
9824
- const dir = path20.join(root, ".mcproc");
9825
- const file = path20.join(root, MANIFEST_REL);
9826
- if (fs16.existsSync(file) && !opts.force) {
10767
+ const dir = path21.join(root, ".mcproc");
10768
+ const file = path21.join(root, MANIFEST_REL);
10769
+ if (fs17.existsSync(file) && !opts.force) {
9827
10770
  throw new McprocError("ALREADY_EXISTS", `${file} already exists`, {
9828
10771
  hint: "Overwrite the draft with force (README and .gitignore are kept), or keep it and adopt the running apps' ports into it."
9829
10772
  });
9830
10773
  }
9831
- if (fs16.existsSync(file) && localReadonly(fs16.readFileSync(file, "utf8"))) {
10774
+ if (fs17.existsSync(file) && localReadonly(fs17.readFileSync(file, "utf8"))) {
9832
10775
  throw new McprocError(
9833
10776
  "READONLY_HOST",
9834
10777
  `${file} declares the local host readonly; init does not replace it, not even with force`,
@@ -9837,20 +10780,20 @@ function initProject(rootIn, opts = {}) {
9837
10780
  }
9838
10781
  );
9839
10782
  }
9840
- fs16.mkdirSync(dir, { recursive: true });
10783
+ fs17.mkdirSync(dir, { recursive: true });
9841
10784
  const created = [];
9842
- const gi = path20.join(dir, ".gitignore");
9843
- if (!fs16.existsSync(gi) || fs16.readFileSync(gi, "utf8").trim() !== "*") {
9844
- fs16.writeFileSync(gi, "*\n");
10785
+ const gi = path21.join(dir, ".gitignore");
10786
+ if (!fs17.existsSync(gi) || fs17.readFileSync(gi, "utf8").trim() !== "*") {
10787
+ fs17.writeFileSync(gi, "*\n");
9845
10788
  created.push(gi);
9846
10789
  }
9847
- const readme = path20.join(dir, "README.md");
9848
- if (!fs16.existsSync(readme)) {
9849
- fs16.writeFileSync(readme, README_TEMPLATE);
10790
+ const readme = path21.join(dir, "README.md");
10791
+ if (!fs17.existsSync(readme)) {
10792
+ fs17.writeFileSync(readme, README_TEMPLATE);
9850
10793
  created.push(readme);
9851
10794
  }
9852
10795
  const d = discover(root);
9853
- fs16.writeFileSync(file, renderDraft(root, defaultProject(root), d));
10796
+ fs17.writeFileSync(file, renderDraft(root, defaultProject(root), d));
9854
10797
  created.push(file);
9855
10798
  const by_source = {};
9856
10799
  for (const c of d.candidates) {
@@ -9872,7 +10815,7 @@ function initProject(rootIn, opts = {}) {
9872
10815
  }
9873
10816
 
9874
10817
  // src/core/adopt.ts
9875
- import fs17 from "fs";
10818
+ import fs18 from "fs";
9876
10819
  import { isMap, isScalar, isSeq, parse, parseDocument } from "yaml";
9877
10820
  var EPHEMERAL_FROM = 32768;
9878
10821
  function planAdoption(rows, m) {
@@ -10155,7 +11098,7 @@ async function adoptPorts(fleet) {
10155
11098
  hint: "Edit the manifest by hand, or remove hosts.local.readonly first."
10156
11099
  }
10157
11100
  );
10158
- const before = fs17.readFileSync(m.path, "utf8");
11101
+ const before = fs18.readFileSync(m.path, "utf8");
10159
11102
  const current = parseManifest(before, m.root, m.path, loadOverride(m.root));
10160
11103
  const { rows, errors } = await listFleet(fleet, fleet.select(["all"]), {
10161
11104
  cpu: "none",
@@ -10185,13 +11128,13 @@ async function adoptPorts(fleet) {
10185
11128
  }
10186
11129
  );
10187
11130
  }
10188
- if (fs17.readFileSync(m.path, "utf8") !== before)
11131
+ if (fs18.readFileSync(m.path, "utf8") !== before)
10189
11132
  throw new McprocError(
10190
11133
  "ALREADY_EXISTS",
10191
11134
  `${m.path} changed while the ports were being probed; nothing written`,
10192
11135
  { hint: "Run the adoption again." }
10193
11136
  );
10194
- fs17.writeFileSync(m.path, splice.text);
11137
+ fs18.writeFileSync(m.path, splice.text);
10195
11138
  return {
10196
11139
  path: m.path,
10197
11140
  written: true,
@@ -10648,6 +11591,28 @@ function registerReadTools(server, deps2) {
10648
11591
  };
10649
11592
  })
10650
11593
  );
11594
+ server.registerTool(
11595
+ "stacks",
11596
+ {
11597
+ title: "Stacks",
11598
+ description: "Every mcproc stack on this host, not only this project: each checkout any mcproc call has seen or found in the local pm2 daemon, with its root, hosts, apps online/total, errors, memory, CPU and listening ports, plus pm2 processes no manifest knows. Use it to see what else runs here (another checkout holding a port or memory); act on another stack by passing its root as `project` to the other tools.",
11599
+ inputSchema: {
11600
+ all: z4.boolean().optional().describe("also the recorded checkouts with nothing in pm2")
11601
+ },
11602
+ annotations: RO
11603
+ },
11604
+ (args) => runTool(async () => {
11605
+ const deps22 = defaultCollectorDeps(deps2.spawner);
11606
+ if (deps2.hereCheck) deps22.hereCheck = deps2.hereCheck;
11607
+ const v = await new StacksCollector(deps22).collect({
11608
+ all: Boolean(args.all)
11609
+ });
11610
+ return {
11611
+ text: renderStacks(v.answer),
11612
+ data: v.answer
11613
+ };
11614
+ })
11615
+ );
10651
11616
  server.registerTool(
10652
11617
  "incidents",
10653
11618
  {
@@ -11181,7 +12146,8 @@ function initialState(p) {
11181
12146
  incidentsCleared: null,
11182
12147
  incidentsErrors: [],
11183
12148
  incidentsTaken: {},
11184
- hideTaken: false
12149
+ hideTaken: false,
12150
+ toStacks: p.toStacks ?? false
11185
12151
  };
11186
12152
  }
11187
12153
  function visibleIncidents(s) {
@@ -11405,181 +12371,64 @@ function keyIntent(key, s, now = Date.now()) {
11405
12371
  return confirms("clear") ? { kind: "clear", apps } : {
11406
12372
  kind: "action",
11407
12373
  action: { type: "arm", at: now, what: "clear", apps }
11408
- };
11409
- }
11410
- case "S-r": {
11411
- const apps = s.rows.map((r) => r.app);
11412
- if (s.busy || apps.length === 0) return null;
11413
- return confirms("reload") ? { kind: "reload-all" } : {
11414
- kind: "action",
11415
- action: { type: "arm", at: now, what: "reload", apps }
11416
- };
11417
- }
11418
- case "q":
11419
- case "C-c":
11420
- case "escape":
11421
- return { kind: "quit" };
11422
- default:
11423
- return null;
11424
- }
11425
- }
11426
- function selectedRow(s) {
11427
- return s.rows[s.selected] ?? null;
11428
- }
11429
- function visibleLogs(s) {
11430
- const all = s.logs[selectedApp(s) ?? ""] ?? [];
11431
- const lines = s.stream === "both" ? all : all.filter((l) => l.stream === s.stream);
11432
- return s.stream === "both" ? sortByStamp(lines) : lines;
11433
- }
11434
- function visibleFlat(s) {
11435
- const hostOf = new Map(s.rows.map((r) => [r.app, r.host]));
11436
- const entries = Object.entries(s.logs).filter(
11437
- ([, lines]) => lines.length > 0
11438
- );
11439
- const hosts = [...new Set(entries.map(([app]) => hostOf.get(app)))];
11440
- const key = `${s.stream}|${hosts.join(",")}`;
11441
- if (flatMemo && flatMemo.logs === s.logs && flatMemo.key === key)
11442
- return flatMemo.out;
11443
- const all = entries.flatMap(
11444
- ([app, lines]) => lines.map((l) => flatLineOf(app, l))
11445
- );
11446
- const shown = s.stream === "both" ? all : all.filter((l) => l.stream === s.stream);
11447
- const byArrival = shown.sort((a, b) => a.seq - b.seq);
11448
- const out = hosts.length <= 1 ? sortByStamp(byArrival) : byArrival;
11449
- flatMemo = { logs: s.logs, key, out };
11450
- return out;
11451
- }
11452
- var flatLines = /* @__PURE__ */ new WeakMap();
11453
- function flatLineOf(app, l) {
11454
- let f = flatLines.get(l);
11455
- if (!f) {
11456
- f = { app, stream: l.stream, text: l.text, seq: l.seq };
11457
- flatLines.set(l, f);
11458
- }
11459
- return f;
11460
- }
11461
- var flatMemo = null;
11462
-
11463
- // src/monit/screentext.ts
11464
- import { charWidth, chars } from "@pm2/blessed/lib/unicode.js";
11465
- var segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
11466
- var EMOJI_PRESENTATION = new RegExp("\\p{Emoji_Presentation}", "u");
11467
- var PICTOGRAPH = new RegExp("\\p{Extended_Pictographic}", "u");
11468
- var REGIONAL = new RegExp("\\p{Regional_Indicator}", "u");
11469
- var KEYCAP = /^[0-9#*]\uFE0F?\u20E3$/;
11470
- var INVISIBLE = /[\p{Cc}\p{Cf}]/u;
11471
- var LINE_BREAK = /\r\n|[\n\v\f\u0085\u2028\u2029]/g;
11472
- var MARK2 = new RegExp("\\p{M}", "u");
11473
- var SPACING_MARK = new RegExp("\\p{Mc}", "u");
11474
- var BLESSED_WIDE = new RegExp(chars.wide.source);
11475
- var NARROW = {
11476
- "\u2705": "\u2714",
11477
- "\u274C": "\u2716",
11478
- "\u274E": "\u2716",
11479
- "\u2757": "!",
11480
- "\u2755": "!",
11481
- "\u2753": "?",
11482
- "\u2754": "?"
11483
- };
11484
- var TAB = 8;
11485
- function displayWidth(s) {
11486
- let w = 0;
11487
- for (const ch of s.includes("\x1B") ? s.replace(SGR_RE, "") : s)
11488
- w += charWidth(ch.codePointAt(0)) === 2 ? 2 : 1;
11489
- return w;
11490
- }
11491
- function columns(s) {
11492
- let w = 0;
11493
- for (const ch of s) w += charWidth(ch.codePointAt(0));
11494
- return w;
11495
- }
11496
- function keepMark(ch, cp) {
11497
- return cp <= 65535 && charWidth(cp) === (SPACING_MARK.test(ch) ? 1 : 0);
11498
- }
11499
- function cluster(c) {
11500
- if (c === " ") return c;
11501
- const first = String.fromCodePoint(c.codePointAt(0));
11502
- if (REGIONAL.test(first))
11503
- return [...c].filter((ch) => REGIONAL.test(ch)).map((ch) => String.fromCharCode(ch.codePointAt(0) - 127462 + 65)).join("");
11504
- if (KEYCAP.test(c)) return c[0];
11505
- if (PICTOGRAPH.test(first) || EMOJI_PRESENTATION.test(first)) {
11506
- if (NARROW[first]) return NARROW[first];
11507
- return !EMOJI_PRESENTATION.test(first) && first.length === 1 ? first : "?";
11508
- }
11509
- let out = "";
11510
- for (const ch of c) {
11511
- if (INVISIBLE.test(ch)) continue;
11512
- const cp = ch.codePointAt(0);
11513
- if (MARK2.test(ch)) {
11514
- if (keepMark(ch, cp)) out += ch;
11515
- continue;
11516
- }
11517
- if (cp > 65535 || cp >= 55296 && cp <= 57343) {
11518
- out += "?";
11519
- continue;
11520
- }
11521
- out += charWidth(cp) === 2 === BLESSED_WIDE.test(ch) ? ch : "?";
11522
- }
11523
- return out;
11524
- }
11525
- function blessedSgr(code) {
11526
- if (!code.includes(":")) return code;
11527
- const groups = code.slice(2, -1).split(";").flatMap((g) => {
11528
- if (!g.includes(":")) return [g];
11529
- const [kind, mode, ...rest] = g.split(":");
11530
- if (kind === "4") return ["4"];
11531
- if (!["38", "48", "58"].includes(kind)) return [];
11532
- if (mode === "5" && rest[0]) return [`${kind};5;${rest[0]}`];
11533
- if (mode === "2" && rest.length >= 3)
11534
- return [`${kind};2;${rest.slice(-3).join(";")}`];
11535
- return [];
11536
- });
11537
- return groups.length ? `\x1B[${groups.join(";")}m` : "";
11538
- }
11539
- var SGR_SPLIT = /(\x1b\[[0-9;:]*m)/;
11540
- var visible = (t) => t.replace(SGR_RE, "");
11541
- function screenText(input, opts = {}) {
11542
- const parts = (opts.colors ? keepSgr(input) : stripAnsi(input)).replace(/\r\n/g, "\n").split("\r");
11543
- let last = parts.length - 1;
11544
- while (last > 0 && visible(parts[last]) === "") last--;
11545
- const carried = parts.slice(0, last).flatMap((p) => p.match(SGR_RE) ?? []).join("");
11546
- let out = "";
11547
- let col = 0;
11548
- for (const piece of (carried + parts[last]).split(SGR_SPLIT)) {
11549
- if (SGR_SPLIT.test(piece) && piece.startsWith("\x1B")) {
11550
- out += blessedSgr(piece);
11551
- continue;
11552
- }
11553
- for (const { segment } of segmenter.segment(
11554
- piece.replace(LINE_BREAK, " ").normalize("NFC")
11555
- )) {
11556
- const c = cluster(segment);
11557
- if (c === " ") {
11558
- const n = TAB - col % TAB;
11559
- out += " ".repeat(n);
11560
- col += n;
11561
- continue;
11562
- }
11563
- out += c;
11564
- col += columns(c);
12374
+ };
12375
+ }
12376
+ case "S-r": {
12377
+ const apps = s.rows.map((r) => r.app);
12378
+ if (s.busy || apps.length === 0) return null;
12379
+ return confirms("reload") ? { kind: "reload-all" } : {
12380
+ kind: "action",
12381
+ action: { type: "arm", at: now, what: "reload", apps }
12382
+ };
11565
12383
  }
12384
+ case "q":
12385
+ case "C-c":
12386
+ return { kind: "quit" };
12387
+ case "escape":
12388
+ return s.toStacks ? { kind: "back" } : { kind: "quit" };
12389
+ default:
12390
+ return null;
11566
12391
  }
12392
+ }
12393
+ function selectedRow(s) {
12394
+ return s.rows[s.selected] ?? null;
12395
+ }
12396
+ function visibleLogs(s) {
12397
+ const all = s.logs[selectedApp(s) ?? ""] ?? [];
12398
+ const lines = s.stream === "both" ? all : all.filter((l) => l.stream === s.stream);
12399
+ return s.stream === "both" ? sortByStamp(lines) : lines;
12400
+ }
12401
+ function visibleFlat(s) {
12402
+ const hostOf = new Map(s.rows.map((r) => [r.app, r.host]));
12403
+ const entries = Object.entries(s.logs).filter(
12404
+ ([, lines]) => lines.length > 0
12405
+ );
12406
+ const hosts = [...new Set(entries.map(([app]) => hostOf.get(app)))];
12407
+ const key = `${s.stream}|${hosts.join(",")}`;
12408
+ if (flatMemo && flatMemo.logs === s.logs && flatMemo.key === key)
12409
+ return flatMemo.out;
12410
+ const all = entries.flatMap(
12411
+ ([app, lines]) => lines.map((l) => flatLineOf(app, l))
12412
+ );
12413
+ const shown = s.stream === "both" ? all : all.filter((l) => l.stream === s.stream);
12414
+ const byArrival = shown.sort((a, b) => a.seq - b.seq);
12415
+ const out = hosts.length <= 1 ? sortByStamp(byArrival) : byArrival;
12416
+ flatMemo = { logs: s.logs, key, out };
11567
12417
  return out;
11568
12418
  }
11569
- function cutWidth(s, width) {
11570
- if (displayWidth(s) <= width) return s;
11571
- let out = "";
11572
- let w = 0;
11573
- for (const ch of s) {
11574
- const cw = displayWidth(ch);
11575
- if (w + cw > width - 1) break;
11576
- out += ch;
11577
- w += cw;
12419
+ var flatLines = /* @__PURE__ */ new WeakMap();
12420
+ function flatLineOf(app, l) {
12421
+ let f = flatLines.get(l);
12422
+ if (!f) {
12423
+ f = { app, stream: l.stream, text: l.text, seq: l.seq };
12424
+ flatLines.set(l, f);
11578
12425
  }
11579
- return `${out}\u2026`;
12426
+ return f;
11580
12427
  }
12428
+ var flatMemo = null;
11581
12429
 
11582
12430
  // src/monit/frame.ts
12431
+ var stacksKey = (s) => s.toStacks ? "esc stacks " : "";
11583
12432
  function pane(entries, scroll, shift, logH, rowsOf) {
11584
12433
  let filled = 0;
11585
12434
  let k = 0;
@@ -11814,7 +12663,7 @@ function renderFrame(s, size) {
11814
12663
  spec2.rowsOf
11815
12664
  );
11816
12665
  const empty = !s.incidentsPolled ? "reading incidents\u2026" : s.incidents.length ? "every incident here is taken (t shows them)" : `no incidents in the last ${DEFAULT_SINCE_MIN} min`;
11817
- const keys2 = `\u2191\u2193 scroll i back f flat t ${s.hideTaken ? "show" : "hide"} taken c clear R reload all h host q quit`;
12666
+ const keys2 = `${stacksKey(s)}\u2191\u2193 scroll i back f flat t ${s.hideTaken ? "show" : "hide"} taken c clear R reload all h host q quit`;
11818
12667
  return {
11819
12668
  header,
11820
12669
  table: [],
@@ -11837,7 +12686,7 @@ function renderFrame(s, size) {
11837
12686
  spec2.logH,
11838
12687
  spec2.rowsOf
11839
12688
  );
11840
- const keys2 = "\u2191\u2193 scroll c clear R reload all f dashboard o/e/b logs i incidents h host q quit";
12689
+ const keys2 = `${stacksKey(s)}\u2191\u2193 scroll c clear R reload all f dashboard o/e/b logs i incidents h host q quit`;
11841
12690
  return {
11842
12691
  header,
11843
12692
  table: [],
@@ -11849,32 +12698,37 @@ function renderFrame(s, size) {
11849
12698
  };
11850
12699
  }
11851
12700
  const multi = new Set(s.rows.map((r2) => r2.host)).size > 1;
11852
- const tableLines = table([
12701
+ const o = multi ? 1 : 0;
12702
+ const tableLines = table(
11853
12703
  [
11854
- "",
11855
- "APP",
11856
- ...multi ? ["HOST"] : [],
11857
- "STATUS",
11858
- "UP",
11859
- "MEM",
11860
- "CPU",
11861
- "PORTS",
11862
- "URL",
11863
- "RUNTIME"
12704
+ [
12705
+ "",
12706
+ "APP",
12707
+ ...multi ? ["HOST"] : [],
12708
+ "STATUS",
12709
+ "UP",
12710
+ "MEM",
12711
+ "CPU",
12712
+ "PORTS",
12713
+ "URL",
12714
+ "RUNTIME"
12715
+ ],
12716
+ ...s.rows.map((r2, i) => [
12717
+ i === s.selected ? "\u25B6" : " ",
12718
+ r2.app,
12719
+ ...multi ? [r2.host] : [],
12720
+ r2.status,
12721
+ fmtUptime(r2.uptime_ms),
12722
+ r2.memory ? fmtBytes(r2.memory) : "-",
12723
+ r2.status === "online" ? fmtCpu(r2.cpu) : "-",
12724
+ screenText(fmtPorts(r2.ports)),
12725
+ screenText(fmtUrl(r2)),
12726
+ r2.runtime ? screenText(r2.runtime) : "-"
12727
+ ])
11864
12728
  ],
11865
- ...s.rows.map((r2, i) => [
11866
- i === s.selected ? "\u25B6" : " ",
11867
- r2.app,
11868
- ...multi ? [r2.host] : [],
11869
- r2.status,
11870
- fmtUptime(r2.uptime_ms),
11871
- r2.memory ? fmtBytes(r2.memory) : "-",
11872
- r2.status === "online" ? fmtCpu(r2.cpu) : "-",
11873
- cutWidth(screenText(fmtPorts(r2.ports)), 22),
11874
- cutWidth(screenText(fmtUrl(r2)), 36),
11875
- r2.runtime ? cutWidth(screenText(r2.runtime), 40) : "-"
11876
- ])
11877
- ]).split("\n").map((l) => cutWidth(l.replace(/^(.)\s\s/, "$1"), w));
12729
+ // +2: the marker column's gutter is dropped below, so the table may use those two columns
12730
+ { width: w + 2, flex: [6 + o, 7 + o, 8 + o], mode: "fit" }
12731
+ ).split("\n").map((l) => cutWidth(l.replace(/^(.)\s\s/, "$1"), w));
11878
12732
  const spec = paneSpec(s, size);
11879
12733
  const p = pane(
11880
12734
  spec.entries,
@@ -11906,7 +12760,7 @@ function renderFrame(s, size) {
11906
12760
  w
11907
12761
  )
11908
12762
  ] : ["no app selected"];
11909
- const keys = "j/k app \u2191\u2193 scroll r restart R reload all s start x stop c clear o/e/b logs f flat i incidents h host q quit";
12763
+ const keys = `${stacksKey(s)}j/k app \u2191\u2193 scroll r restart R reload all s start x stop c clear o/e/b logs f flat i incidents h host q quit`;
11910
12764
  const footer = cutWidth(right ? `${right} \u2502 ${keys}` : keys, w);
11911
12765
  return {
11912
12766
  header,
@@ -12011,31 +12865,31 @@ async function monitOnce(opts, deps2 = {}) {
12011
12865
  }
12012
12866
 
12013
12867
  // src/cli/install.ts
12014
- import fs18 from "fs";
12015
- import os6 from "os";
12016
- import path21 from "path";
12868
+ import fs19 from "fs";
12869
+ import os9 from "os";
12870
+ import path22 from "path";
12017
12871
  var LAUNCHER_MARK = "# written by mcproc install";
12018
12872
  function rcFor(home, shell) {
12019
- const name2 = path21.basename(shell || "");
12020
- return path21.join(
12873
+ const name2 = path22.basename(shell || "");
12874
+ return path22.join(
12021
12875
  home,
12022
12876
  name2 === "zsh" ? ".zshrc" : name2 === "bash" ? ".bashrc" : ".profile"
12023
12877
  );
12024
12878
  }
12025
12879
  function installShortcut(o = {}) {
12026
- const home = o.home ?? os6.homedir();
12027
- const binDir = path21.resolve(o.binDir ?? path21.join(home, ".local", "bin"));
12028
- const cli = path21.resolve(
12029
- o.cliPath ?? path21.join(findPackageRoot(), "dist", "cli.js")
12880
+ const home = o.home ?? os9.homedir();
12881
+ const binDir = path22.resolve(o.binDir ?? path22.join(home, ".local", "bin"));
12882
+ const cli = path22.resolve(
12883
+ o.cliPath ?? path22.join(findPackageRoot(), "dist", "cli.js")
12030
12884
  );
12031
- const launcher = path21.join(binDir, "mcproc");
12885
+ const launcher = path22.join(binDir, "mcproc");
12032
12886
  const pathEnv = o.pathEnv ?? process.env.PATH ?? "";
12033
- const onPath = pathEnv.split(":").some((p) => p && path21.resolve(p.replace(/^~(?=\/|$)/, home)) === binDir);
12887
+ const onPath = pathEnv.split(":").some((p) => p && path22.resolve(p.replace(/^~(?=\/|$)/, home)) === binDir);
12034
12888
  const next = [];
12035
12889
  if (o.uninstall) {
12036
- const exists = fs18.existsSync(launcher);
12037
- const ours = exists && fs18.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK);
12038
- if (ours) fs18.unlinkSync(launcher);
12890
+ const exists = fs19.existsSync(launcher);
12891
+ const ours = exists && fs19.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK);
12892
+ if (ours) fs19.unlinkSync(launcher);
12039
12893
  else if (exists)
12040
12894
  next.push(`${launcher} was not written by mcproc install; left in place`);
12041
12895
  else next.push(`nothing at ${launcher}`);
@@ -12051,43 +12905,43 @@ function installShortcut(o = {}) {
12051
12905
  next
12052
12906
  };
12053
12907
  }
12054
- fs18.mkdirSync(binDir, { recursive: true });
12908
+ fs19.mkdirSync(binDir, { recursive: true });
12055
12909
  let existing = null;
12056
12910
  try {
12057
- existing = fs18.lstatSync(launcher);
12911
+ existing = fs19.lstatSync(launcher);
12058
12912
  } catch {
12059
12913
  }
12060
12914
  if (existing?.isSymbolicLink()) {
12061
12915
  throw new McprocError(
12062
12916
  "BAD_ARGUMENT",
12063
- `${launcher} is a symlink (to ${fs18.readlinkSync(launcher)}); refusing to write through it`,
12917
+ `${launcher} is a symlink (to ${fs19.readlinkSync(launcher)}); refusing to write through it`,
12064
12918
  { hint: "Remove it yourself, or pass another --bin-dir." }
12065
12919
  );
12066
12920
  }
12067
- if (existing && !fs18.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK)) {
12921
+ if (existing && !fs19.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK)) {
12068
12922
  throw new McprocError(
12069
12923
  "BAD_ARGUMENT",
12070
12924
  `${launcher} exists and was not written by mcproc install; refusing to overwrite it`,
12071
12925
  { hint: "Remove it yourself, or pass another --bin-dir." }
12072
12926
  );
12073
12927
  }
12074
- fs18.writeFileSync(
12928
+ fs19.writeFileSync(
12075
12929
  launcher,
12076
12930
  `#!/bin/sh
12077
- ${LAUNCHER_MARK}: runs the mcproc at ${path21.dirname(path21.dirname(cli))}
12931
+ ${LAUNCHER_MARK}: runs the mcproc at ${path22.dirname(path22.dirname(cli))}
12078
12932
  exec node ${shq(cli)} "$@"
12079
12933
  `
12080
12934
  );
12081
- fs18.chmodSync(launcher, 493);
12935
+ fs19.chmodSync(launcher, 493);
12082
12936
  let rcFile = null;
12083
12937
  let rcUpdated = false;
12084
12938
  if (!onPath) {
12085
12939
  rcFile = rcFor(home, o.shell ?? process.env.SHELL ?? "");
12086
- const shown = binDir.startsWith(home + path21.sep) ? `$HOME/${path21.relative(home, binDir)}` : binDir;
12940
+ const shown = binDir.startsWith(home + path22.sep) ? `$HOME/${path22.relative(home, binDir)}` : binDir;
12087
12941
  const line = `export PATH="${shown}:$PATH"`;
12088
- const current = fs18.existsSync(rcFile) ? fs18.readFileSync(rcFile, "utf8") : "";
12942
+ const current = fs19.existsSync(rcFile) ? fs19.readFileSync(rcFile, "utf8") : "";
12089
12943
  if (!current.includes(line)) {
12090
- fs18.appendFileSync(
12944
+ fs19.appendFileSync(
12091
12945
  rcFile,
12092
12946
  `${current === "" || current.endsWith("\n") ? "" : "\n"}
12093
12947
  # mcproc install: the shortcut lives in ${shown}
@@ -12195,6 +13049,7 @@ var MonitLoop = class {
12195
13049
  inc = freshView();
12196
13050
  /** One refresh of every host of the view (or of `hosts`); a host whose refresh of this generation is still in flight shares it. */
12197
13051
  refresh(hosts) {
13052
+ if (this.closed) return Promise.resolve();
12198
13053
  this.readCleared();
12199
13054
  this.syncWatches();
12200
13055
  const gen = this.gen;
@@ -12304,6 +13159,7 @@ var MonitLoop = class {
12304
13159
  const probed = this.losses;
12305
13160
  try {
12306
13161
  const ctx = await this.fleet.ctx(g.host);
13162
+ if (gen !== this.gen) return;
12307
13163
  const snap = await this.deps.snapshot(ctx, {
12308
13164
  cpu: "none",
12309
13165
  prev: this.prev.get(g.host),
@@ -12513,6 +13369,8 @@ var MonitLoop = class {
12513
13369
  /** The screen is going away: no follower wakes it any more, and a running reload stops waiting for dependencies. */
12514
13370
  close() {
12515
13371
  this.closed = true;
13372
+ this.gen++;
13373
+ this.switchSeq++;
12516
13374
  this.reloading?.abort();
12517
13375
  for (const q of this.wakes.values()) {
12518
13376
  if (q.timer) clearTimeout(q.timer);
@@ -12666,10 +13524,10 @@ var MonitLoop = class {
12666
13524
  this.names.map((a) => markKey(this.fleet.hostOf(a), a))
12667
13525
  );
12668
13526
  const st = this.inc;
12669
- const add2 = (key, stream, path22) => {
13527
+ const add2 = (key, stream, path23) => {
12670
13528
  const by = st.pending[key] ?? {};
12671
13529
  const paths = by[stream] ?? /* @__PURE__ */ new Set();
12672
- paths.add(path22);
13530
+ paths.add(path23);
12673
13531
  by[stream] = paths;
12674
13532
  st.pending[key] = by;
12675
13533
  };
@@ -12812,9 +13670,11 @@ async function runMonit(opts) {
12812
13670
  project: manifest.project,
12813
13671
  host: viewLabel(fleet.requested),
12814
13672
  hosts: views.map(viewLabel),
12815
- apps: appInfos(manifest)
13673
+ apps: appInfos(manifest),
13674
+ toStacks: Boolean(opts.back)
12816
13675
  });
12817
- const screen = blessed.screen({
13676
+ const own2 = !opts.screen;
13677
+ const screen = opts.screen ?? blessed.screen({
12818
13678
  smartCSR: true,
12819
13679
  title: "mcproc monit",
12820
13680
  fullUnicode: true
@@ -12833,7 +13693,7 @@ async function runMonit(opts) {
12833
13693
  const detailsTitle = box({ bottom: 7, height: 1 });
12834
13694
  const detailsBox = box({ bottom: 1, height: 6 });
12835
13695
  const footer = box({ bottom: 0, height: 1 });
12836
- for (const b of [
13696
+ const boxes = [
12837
13697
  header,
12838
13698
  appsTitle,
12839
13699
  tableBox,
@@ -12842,8 +13702,8 @@ async function runMonit(opts) {
12842
13702
  detailsTitle,
12843
13703
  detailsBox,
12844
13704
  footer
12845
- ])
12846
- screen.append(b);
13705
+ ];
13706
+ for (const b of boxes) screen.append(b);
12847
13707
  const appColor = new Map(
12848
13708
  Object.keys(manifest.apps).map((a, i) => [a, colorFor(i)])
12849
13709
  );
@@ -12856,7 +13716,9 @@ async function runMonit(opts) {
12856
13716
  width: screen.width,
12857
13717
  height: screen.height
12858
13718
  });
13719
+ let left = false;
12859
13720
  const show = (f) => {
13721
+ if (left) return;
12860
13722
  const { width: w, height: h } = size();
12861
13723
  header.setContent(f.header);
12862
13724
  footer.setContent(f.footer);
@@ -12905,44 +13767,511 @@ async function runMonit(opts) {
12905
13767
  const timer = setInterval(() => {
12906
13768
  void loop.refresh();
12907
13769
  }, opts.intervalMs);
12908
- const quit = () => {
13770
+ const leave = (why) => {
13771
+ left = true;
12909
13772
  clearInterval(timer);
12910
13773
  loop.close();
13774
+ screen.removeListener("keypress", onKey);
13775
+ screen.removeListener("resize", paint);
13776
+ if (own2) screen.destroy();
13777
+ else for (const b of boxes) b.detach();
13778
+ resolve(why);
13779
+ };
13780
+ const onKey = (_ch, key) => {
13781
+ const intent = keyIntent(key.full ?? key.name ?? "", state);
13782
+ if (state.armedAt !== null && !(intent?.kind === "action" && intent.action.type === "arm"))
13783
+ dispatch({ type: "arm", at: null });
13784
+ if (!intent) return;
13785
+ if (intent.kind === "quit") return leave("quit");
13786
+ if (intent.kind === "back") return leave("back");
13787
+ if (intent.kind === "action") {
13788
+ if (intent.action.type === "arm" && intent.action.at !== null) {
13789
+ const { at, what } = intent.action;
13790
+ setTimeout(() => {
13791
+ if (state.armedAt === at && state.armed === what)
13792
+ dispatch({ type: "arm", at: null });
13793
+ }, CONFIRM_MS + 50).unref();
13794
+ }
13795
+ if (intent.action.type === "select")
13796
+ loop.dispatchSelect(intent.action.delta);
13797
+ else {
13798
+ dispatch(intent.action);
13799
+ if (intent.action.type === "view") void loop.refresh();
13800
+ }
13801
+ return;
13802
+ }
13803
+ if (intent.kind === "host-next") return void loop.nextView();
13804
+ if (intent.kind === "clear") return void loop.runClear(intent.apps);
13805
+ if (intent.kind === "reload-all") return void loop.reloadAll();
13806
+ void loop.runCommand(intent.cmd, intent.app);
13807
+ };
13808
+ screen.on("keypress", onKey);
13809
+ screen.on("resize", paint);
13810
+ void loop.refresh().then(paint);
13811
+ });
13812
+ }
13813
+
13814
+ // src/monit/stacks-tui.ts
13815
+ import blessed2 from "@pm2/blessed";
13816
+
13817
+ // src/core/stacks/stop.ts
13818
+ async function stopStack(m, connector = defaultConnector()) {
13819
+ const out = { stopped: [], readonly: [], failed: [] };
13820
+ try {
13821
+ const fleet = new Fleet(m, void 0, connector);
13822
+ const ro = /* @__PURE__ */ new Map();
13823
+ const names = fleet.select(["all"]).filter((a) => {
13824
+ const h = fleet.hostOf(a);
13825
+ if (!pickHost(m, h).readonly) return true;
13826
+ ro.set(h, [...ro.get(h) ?? [], a]);
13827
+ return false;
13828
+ });
13829
+ out.readonly = [...ro].map(([host, apps]) => ({ host, apps }));
13830
+ const mine = await ownApps(m, fleet, names, out);
13831
+ if (mine.length === 0) return out;
13832
+ for (const r of await actOn(fleet, mine, "stop"))
13833
+ if (r.error)
13834
+ out.failed.push({ host: r.host, message: `${r.app}: ${r.error}` });
13835
+ else out.stopped.push(r.app);
13836
+ } catch (e) {
13837
+ out.failed.push({ host: "-", message: describeError(e) });
13838
+ }
13839
+ return out;
13840
+ }
13841
+ async function ownApps(m, fleet, names, out) {
13842
+ const keep = [];
13843
+ for (const g of fleet.groups(names)) {
13844
+ let procs;
13845
+ let host;
13846
+ try {
13847
+ const ctx = await fleet.ctx(g.host);
13848
+ host = ctx.host;
13849
+ procs = await ctx.pm2.jlist();
13850
+ } catch (e) {
13851
+ for (const a of g.apps)
13852
+ out.failed.push({ host: g.host, message: `${a}: ${describeError(e)}` });
13853
+ continue;
13854
+ }
13855
+ for (const a of g.apps) {
13856
+ const p = findProcess(procs, m.project, a);
13857
+ const own2 = appCwd(host, m.apps[a]);
13858
+ if (p?.cwd && p.cwd !== own2)
13859
+ out.failed.push({
13860
+ host: g.host,
13861
+ message: `${a}: runs from another checkout (${p.cwd}), left alone`
13862
+ });
13863
+ else keep.push(a);
13864
+ }
13865
+ }
13866
+ return names.filter((a) => keep.includes(a));
13867
+ }
13868
+
13869
+ // src/monit/stacks-state.ts
13870
+ var orphanKey = (o) => `orphan:${o.endpoint}:${o.namespace}`;
13871
+ function overviewRows(s) {
13872
+ if (!s.view) return [];
13873
+ return [
13874
+ ...s.view.stacks.map((stack) => ({
13875
+ kind: "stack",
13876
+ key: stack.root,
13877
+ stack
13878
+ })),
13879
+ ...s.view.answer.orphans.map((orphan) => ({
13880
+ kind: "orphan",
13881
+ key: orphanKey(orphan),
13882
+ orphan
13883
+ }))
13884
+ ];
13885
+ }
13886
+ function selectedOverviewRow(s) {
13887
+ return overviewRows(s)[s.index] ?? null;
13888
+ }
13889
+ function initialOverview(opts) {
13890
+ return {
13891
+ view: null,
13892
+ selected: opts.selected ?? null,
13893
+ index: 0,
13894
+ all: false,
13895
+ armedAt: null,
13896
+ notice: null,
13897
+ error: null,
13898
+ busy: false,
13899
+ updatedAt: null
13900
+ };
13901
+ }
13902
+ var running2 = (st) => st.rows.filter(
13903
+ (r) => r.pm_id !== null && r.status !== "stopped" && r.status !== "done"
13904
+ ).length;
13905
+ function stopPrompt(st) {
13906
+ const n = running2(st);
13907
+ return `press x again to stop ${n} app${n === 1 ? "" : "s"} of ${st.project}`;
13908
+ }
13909
+ var unloaded = (st) => `${st.project}: the manifest did not load (${st.manifest_error?.code})`;
13910
+ function reduceOverview(s, a) {
13911
+ switch (a.type) {
13912
+ case "view": {
13913
+ const next = { ...s, view: a.view, updatedAt: a.at, error: null };
13914
+ const rows = overviewRows(next);
13915
+ const found = rows.findIndex((r) => r.key === s.selected);
13916
+ const index = found >= 0 ? found : Math.min(s.index, Math.max(0, rows.length - 1));
13917
+ const selected = rows[index]?.key ?? null;
13918
+ const moved = s.armedAt !== null && selected !== s.selected;
13919
+ return {
13920
+ ...next,
13921
+ index,
13922
+ selected,
13923
+ ...moved ? { armedAt: null, notice: null } : {}
13924
+ };
13925
+ }
13926
+ case "select": {
13927
+ const rows = overviewRows(s);
13928
+ if (rows.length === 0) return s;
13929
+ const index = Math.max(0, Math.min(rows.length - 1, s.index + a.delta));
13930
+ return { ...s, index, selected: rows[index].key, armedAt: null };
13931
+ }
13932
+ case "arm": {
13933
+ const r = selectedOverviewRow(s);
13934
+ if (a.at === null)
13935
+ return {
13936
+ ...s,
13937
+ armedAt: null,
13938
+ notice: s.armedAt !== null ? null : s.notice
13939
+ };
13940
+ return {
13941
+ ...s,
13942
+ armedAt: a.at,
13943
+ notice: r?.kind === "stack" ? stopPrompt(r.stack) : s.notice
13944
+ };
13945
+ }
13946
+ case "toggle-all":
13947
+ return { ...s, all: !s.all };
13948
+ case "notice":
13949
+ return { ...s, notice: a.message, armedAt: null };
13950
+ case "error":
13951
+ return { ...s, error: a.message };
13952
+ case "busy":
13953
+ return { ...s, busy: a.busy };
13954
+ }
13955
+ }
13956
+ function sharedSlug(st, what) {
13957
+ const n = st.rivals.length + 1;
13958
+ return `${st.project}: ${n} checkouts share this slug on one pm2, so ${what === "open" ? "its monit" : "a stop"} could reach another one's processes; give each checkout its own project slug`;
13959
+ }
13960
+ function overviewKey(key, s, now = Date.now()) {
13961
+ const r = selectedOverviewRow(s);
13962
+ const notice = (message) => ({
13963
+ kind: "action",
13964
+ action: { type: "notice", message }
13965
+ });
13966
+ switch (key) {
13967
+ case "up":
13968
+ case "k":
13969
+ return { kind: "action", action: { type: "select", delta: -1 } };
13970
+ case "down":
13971
+ case "j":
13972
+ return { kind: "action", action: { type: "select", delta: 1 } };
13973
+ case "enter":
13974
+ case "return": {
13975
+ if (!r) return null;
13976
+ if (r.kind === "orphan")
13977
+ return notice("not an mcproc stack: no manifest to open");
13978
+ if (r.stack.manifest_error) return notice(unloaded(r.stack));
13979
+ if (r.stack.rivals?.length) return notice(sharedSlug(r.stack, "open"));
13980
+ if (!r.stack.hosts.some((h) => h.reachable)) {
13981
+ const why = r.stack.hosts.find((h) => h.error);
13982
+ return notice(
13983
+ `${r.stack.project}: no host of this stack answers${why ? ` (${why.name}: ${why.error})` : ""}`
13984
+ );
13985
+ }
13986
+ return { kind: "open", root: r.stack.root };
13987
+ }
13988
+ case "x": {
13989
+ if (!r || s.busy) return null;
13990
+ if (r.kind === "orphan") return notice("not an mcproc stack: read-only");
13991
+ if (r.stack.manifest_error) return notice(unloaded(r.stack));
13992
+ if (r.stack.rivals?.length) return notice(sharedSlug(r.stack, "stop"));
13993
+ if (s.armedAt !== null && now - s.armedAt <= CONFIRM_MS)
13994
+ return { kind: "stop", root: r.stack.root };
13995
+ return { kind: "action", action: { type: "arm", at: now } };
13996
+ }
13997
+ case "a":
13998
+ return { kind: "action", action: { type: "toggle-all" } };
13999
+ case "q":
14000
+ case "C-c":
14001
+ case "escape":
14002
+ return { kind: "quit" };
14003
+ default:
14004
+ return null;
14005
+ }
14006
+ }
14007
+
14008
+ // src/monit/stacks-frame.ts
14009
+ var KEYS = (all) => `j/k stack enter open x stop stack a ${all ? "running only" : "all known"} q quit`;
14010
+ var CHROME = 4;
14011
+ var MIN_APPS = 4;
14012
+ function stackLines(st, width) {
14013
+ const e = st.manifest_error;
14014
+ const lines = e ? [`${e.code}: ${e.message}`, ...e.hint ? [e.hint] : []] : table(
14015
+ [
14016
+ ["APP", "HOST", "STATUS", "UP", "MEM", "CPU", "PORTS"],
14017
+ ...st.rows.map((r) => [
14018
+ r.app,
14019
+ r.host,
14020
+ r.status,
14021
+ fmtUptime(r.uptime_ms),
14022
+ r.memory ? fmtBytes(r.memory) : "-",
14023
+ r.status === "online" ? fmtCpu(r.cpu) : "-",
14024
+ fmtPorts(r.ports)
14025
+ ])
14026
+ ].map((r) => r.map((c) => screenText(c))),
14027
+ { width, flex: [6], mode: "fit" }
14028
+ ).split("\n");
14029
+ for (const n of st.notes) lines.push(n);
14030
+ for (const h of st.hosts) if (h.error) lines.push(`${h.name}: ${h.error}`);
14031
+ return lines;
14032
+ }
14033
+ function renderOverview(s, size) {
14034
+ const w = size.width;
14035
+ const a = s.view?.answer;
14036
+ const at = s.updatedAt ? ` \xB7 ${s.updatedAt.toTimeString().slice(0, 8)}` : "";
14037
+ const header = cutWidth(
14038
+ screenText(
14039
+ a ? `mcproc \xB7 ${stacksHostLine(a.host)}${at}${a.host.notes.map((n) => ` \xB7 ${n}`).join("")}` : "mcproc \xB7 reading the stacks\u2026"
14040
+ ),
14041
+ w
14042
+ );
14043
+ const rows = overviewRows(s);
14044
+ const listed = new Set(s.view?.stacks.map((st) => st.project));
14045
+ const cells = rows.map((r, i) => {
14046
+ const mark = i === s.index ? "\u25B6" : " ";
14047
+ if (r.kind === "orphan") return [mark, ...orphanCells(r.orphan)];
14048
+ const st = r.stack;
14049
+ const nested = st.base_project !== st.project && listed.has(st.base_project);
14050
+ return [
14051
+ mark,
14052
+ stackLabel(st, nested),
14053
+ tildePath(st.root),
14054
+ st.hosts.map((h) => h.reachable ? h.name : `${h.name}\u2717`).join(","),
14055
+ st.manifest_error ? "manifest error" : stackApps(st),
14056
+ String(st.apps.errored),
14057
+ st.memory ? fmtBytes(st.memory) : "-",
14058
+ st.cpu === null ? "-" : `${Math.round(st.cpu)}%`,
14059
+ stackPorts(st)
14060
+ ];
14061
+ });
14062
+ const lines = table(
14063
+ [[" ", ...STACK_HEADER], ...cells].map((r) => r.map((c) => screenText(c))),
14064
+ { width: w + 2, flex: [2, 8], cutStart: [2], mode: "fit" }
14065
+ ).split("\n").map((l) => cutWidth(l.replace(/^(.)\s\s/, "$1"), w));
14066
+ const budget = Math.max(2, size.height - CHROME - MIN_APPS);
14067
+ const shown = budget - 1;
14068
+ const start = Math.max(0, Math.min(s.index + 1 - shown, rows.length - shown));
14069
+ const tableShown = [lines[0], ...lines.slice(1 + start, 1 + start + shown)];
14070
+ const sel = selectedOverviewRow(s);
14071
+ let appsTitle = "apps";
14072
+ let apps = [];
14073
+ if (sel?.kind === "stack") {
14074
+ appsTitle = `apps \xB7 ${sel.stack.project}`;
14075
+ apps = stackLines(sel.stack, w);
14076
+ } else if (sel?.kind === "orphan") {
14077
+ const o = sel.orphan;
14078
+ appsTitle = `processes \xB7 pm2 \xB7 ${o.namespace}`;
14079
+ apps = table(
14080
+ [
14081
+ ["NAME", "STATUS", "MEM"],
14082
+ ...o.processes.map((p) => [
14083
+ p.name,
14084
+ p.status,
14085
+ p.memory ? fmtBytes(p.memory) : "-"
14086
+ ])
14087
+ ].map((r) => r.map((c) => screenText(c))),
14088
+ { width: w, flex: [0], mode: "fit" }
14089
+ ).split("\n");
14090
+ if (o.note) apps.push(o.note);
14091
+ } else if (a) {
14092
+ apps = [
14093
+ ...a.host.notes,
14094
+ `no stacks running on this host \xB7 a: ${s.all ? "running only" : "all known"}`
14095
+ ];
14096
+ }
14097
+ const appsH = Math.max(1, size.height - CHROME - tableShown.length);
14098
+ const right = screenText(
14099
+ s.error ? `ERROR: ${s.error}` : s.notice ?? (s.busy ? "working\u2026" : "")
14100
+ );
14101
+ const footer = cutWidth(right ? `${right} \u2502 ${KEYS(s.all)}` : KEYS(s.all), w);
14102
+ return {
14103
+ header,
14104
+ title: paneTitle("stacks", w),
14105
+ table: tableShown,
14106
+ appsTitle: paneTitle(screenText(appsTitle), w),
14107
+ apps: apps.slice(0, appsH).map((l) => cutWidth(screenText(l), w)),
14108
+ footer
14109
+ };
14110
+ }
14111
+ function overviewToText(f) {
14112
+ return [f.header, "", ...f.table, "", ...f.apps, "", f.footer].join("\n");
14113
+ }
14114
+
14115
+ // src/monit/stacks-tui.ts
14116
+ function stopNotice(project, r) {
14117
+ const n = r.stopped.length;
14118
+ const parts = [`stopped ${n} app${n === 1 ? "" : "s"} of ${project}`];
14119
+ for (const x of r.readonly)
14120
+ parts.push(`readonly: ${x.host} (${x.apps.length})`);
14121
+ for (const f of r.failed) parts.push(`failed: ${f.host} ${f.message}`);
14122
+ return parts.join(" \xB7 ");
14123
+ }
14124
+ async function overviewOnce(opts) {
14125
+ const view = await new StacksCollector().collect({
14126
+ all: Boolean(opts.all)
14127
+ });
14128
+ const s = reduceOverview(initialOverview({}), {
14129
+ type: "view",
14130
+ view,
14131
+ at: /* @__PURE__ */ new Date()
14132
+ });
14133
+ return overviewToText(
14134
+ renderOverview(s, { width: opts.width ?? 120, height: 1e3 })
14135
+ );
14136
+ }
14137
+ async function runOverview(opts) {
14138
+ const screen = blessed2.screen({
14139
+ smartCSR: true,
14140
+ title: "mcproc monit",
14141
+ fullUnicode: true
14142
+ });
14143
+ const box = (o) => blessed2.box({ left: 0, width: "100%", wrap: false, ...o });
14144
+ const header = box({ top: 0, height: 1 });
14145
+ const title = box({ top: 1, height: 1 });
14146
+ const tableBox = box({ top: 2, height: 1 });
14147
+ const appsTitle = box({ top: 3, height: 1 });
14148
+ const appsBox = box({ top: 4, height: 1 });
14149
+ const footer = box({ bottom: 0, height: 1 });
14150
+ const boxes = [header, title, tableBox, appsTitle, appsBox, footer];
14151
+ const attach = () => {
14152
+ for (const b of boxes) screen.append(b);
14153
+ };
14154
+ const detach = () => {
14155
+ for (const b of boxes) b.detach();
14156
+ };
14157
+ attach();
14158
+ const collector = new StacksCollector();
14159
+ let state = initialOverview({ selected: opts.selected });
14160
+ let active = true;
14161
+ let closed = false;
14162
+ let armTimer = null;
14163
+ const paint = () => {
14164
+ if (!active || closed) return;
14165
+ const f = renderOverview(state, {
14166
+ width: screen.width,
14167
+ height: screen.height
14168
+ });
14169
+ header.setContent(f.header);
14170
+ title.setContent(f.title);
14171
+ tableBox.height = f.table.length;
14172
+ tableBox.setContent(f.table.join("\n"));
14173
+ appsTitle.top = 2 + f.table.length;
14174
+ appsTitle.setContent(f.appsTitle);
14175
+ appsBox.top = 3 + f.table.length;
14176
+ appsBox.height = Math.max(
14177
+ 1,
14178
+ screen.height - appsBox.top - 1
14179
+ );
14180
+ appsBox.setContent(f.apps.join("\n"));
14181
+ footer.setContent(f.footer);
14182
+ screen.render();
14183
+ };
14184
+ const act = (a) => {
14185
+ state = reduceOverview(state, a);
14186
+ paint();
14187
+ };
14188
+ let inflight = false;
14189
+ const refresh = async () => {
14190
+ if (inflight || closed) return;
14191
+ inflight = true;
14192
+ try {
14193
+ const view = await collector.collect({ all: state.all });
14194
+ act({ type: "view", view, at: /* @__PURE__ */ new Date() });
14195
+ } catch (e) {
14196
+ act({ type: "error", message: describeError(e) });
14197
+ } finally {
14198
+ inflight = false;
14199
+ }
14200
+ };
14201
+ let timer = setInterval(() => void refresh(), opts.intervalMs);
14202
+ return new Promise((resolve) => {
14203
+ const quit = () => {
14204
+ closed = true;
14205
+ clearInterval(timer);
14206
+ if (armTimer) clearTimeout(armTimer);
12911
14207
  screen.destroy();
12912
14208
  resolve();
12913
14209
  };
14210
+ const onKey = async (key) => {
14211
+ const intent = overviewKey(key, state);
14212
+ if (state.armedAt !== null && intent?.kind !== "stop" && !(intent?.kind === "action" && intent.action.type === "arm"))
14213
+ act({ type: "arm", at: null });
14214
+ if (!intent) return;
14215
+ if (intent.kind === "quit") return quit();
14216
+ if (intent.kind === "action") {
14217
+ act(intent.action);
14218
+ if (intent.action.type === "arm" && intent.action.at !== null) {
14219
+ const at = intent.action.at;
14220
+ if (armTimer) clearTimeout(armTimer);
14221
+ armTimer = setTimeout(() => {
14222
+ if (state.armedAt === at) act({ type: "arm", at: null });
14223
+ }, CONFIRM_MS + 50);
14224
+ armTimer.unref();
14225
+ }
14226
+ if (intent.action.type === "toggle-all") void refresh();
14227
+ return;
14228
+ }
14229
+ const m = state.view?.manifests.get(intent.root);
14230
+ if (!m) {
14231
+ act({ type: "notice", message: "that stack is gone; refreshing" });
14232
+ return void refresh();
14233
+ }
14234
+ if (intent.kind === "stop") {
14235
+ act({ type: "busy", busy: true });
14236
+ const r = await stopStack(m);
14237
+ if (closed) return;
14238
+ act({ type: "busy", busy: false });
14239
+ act({ type: "notice", message: stopNotice(m.project, r) });
14240
+ return void refresh();
14241
+ }
14242
+ active = false;
14243
+ clearInterval(timer);
14244
+ detach();
14245
+ const why = await runMonit({
14246
+ project: intent.root,
14247
+ intervalMs: opts.intervalMs,
14248
+ lines: opts.lines,
14249
+ screen,
14250
+ back: true
14251
+ }).catch((e) => {
14252
+ state = reduceOverview(state, {
14253
+ type: "error",
14254
+ message: describeError(e)
14255
+ });
14256
+ return "back";
14257
+ });
14258
+ if (why === "quit") return quit();
14259
+ attach();
14260
+ active = true;
14261
+ timer = setInterval(() => void refresh(), opts.intervalMs);
14262
+ paint();
14263
+ void refresh();
14264
+ };
12914
14265
  screen.on(
12915
14266
  "keypress",
12916
14267
  (_ch, key) => {
12917
- const intent = keyIntent(key.full ?? key.name ?? "", state);
12918
- if (state.armedAt !== null && !(intent?.kind === "action" && intent.action.type === "arm"))
12919
- dispatch({ type: "arm", at: null });
12920
- if (!intent) return;
12921
- if (intent.kind === "quit") return quit();
12922
- if (intent.kind === "action") {
12923
- if (intent.action.type === "arm" && intent.action.at !== null) {
12924
- const { at, what } = intent.action;
12925
- setTimeout(() => {
12926
- if (state.armedAt === at && state.armed === what)
12927
- dispatch({ type: "arm", at: null });
12928
- }, CONFIRM_MS + 50).unref();
12929
- }
12930
- if (intent.action.type === "select")
12931
- loop.dispatchSelect(intent.action.delta);
12932
- else {
12933
- dispatch(intent.action);
12934
- if (intent.action.type === "view") void loop.refresh();
12935
- }
12936
- return;
12937
- }
12938
- if (intent.kind === "host-next") return void loop.nextView();
12939
- if (intent.kind === "clear") return void loop.runClear(intent.apps);
12940
- if (intent.kind === "reload-all") return void loop.reloadAll();
12941
- void loop.runCommand(intent.cmd, intent.app);
14268
+ if (!active) return;
14269
+ void onKey(key.full ?? key.name ?? "");
12942
14270
  }
12943
14271
  );
12944
14272
  screen.on("resize", paint);
12945
- void loop.refresh().then(paint);
14273
+ paint();
14274
+ void refresh();
12946
14275
  });
12947
14276
  }
12948
14277
 
@@ -12951,7 +14280,8 @@ var defaultIo = {
12951
14280
  out: (s) => process.stdout.write(`${s}
12952
14281
  `),
12953
14282
  err: (s) => process.stderr.write(`${s}
12954
- `)
14283
+ `),
14284
+ columns: () => process.stdout.isTTY ? process.stdout.columns || void 0 : void 0
12955
14285
  };
12956
14286
  async function printSince(fleet, apps, since, how, io, signal) {
12957
14287
  const marks = new LogMarks();
@@ -13003,10 +14333,20 @@ async function printLs(fleet, names, how, io) {
13003
14333
  how.reloads
13004
14334
  );
13005
14335
  io.out(
13006
- how.json ? JSON.stringify(rows, null, 2) : renderLs(rows, fleet.fallback, unread)
14336
+ how.json ? JSON.stringify(rows, null, 2) : renderLs(rows, fleet.fallback, unread, io.columns?.())
13007
14337
  );
13008
14338
  if (errors.length) process.exitCode = 1;
13009
14339
  }
14340
+ function monitMode(g, all, env = process.env, cwd = process.cwd()) {
14341
+ const named = g.project || env.MCPROC_PROJECT || env.CLAUDE_PROJECT_DIR;
14342
+ const found = findManifestPath(
14343
+ named ? resolveProjectDir(g.project, [], env) : cwd
14344
+ );
14345
+ if (all)
14346
+ return found ? { mode: "overview", selected: found.root } : { mode: "overview" };
14347
+ if (!named && !found) return { mode: "overview" };
14348
+ return { mode: "project" };
14349
+ }
13010
14350
  function buildProgram(io = defaultIo) {
13011
14351
  const program = new Command().name("mcproc").description(
13012
14352
  "pm2 process control for agents: one manifest per project, local and remote hosts, per-app ports"
@@ -13015,6 +14355,7 @@ function buildProgram(io = defaultIo) {
13015
14355
  "project root (default: MCPROC_PROJECT, CLAUDE_PROJECT_DIR, cwd)"
13016
14356
  ).option("--host <name>", "host from the manifest (default: local)").option("--json", "print JSON instead of text");
13017
14357
  const emit = (g, text, data) => io.out(g.json ? JSON.stringify(data, null, 2) : text);
14358
+ const width = (g) => g.json ? void 0 : io.columns?.();
13018
14359
  const wrap = (fn) => async (...all) => {
13019
14360
  const cmd = all[all.length - 1];
13020
14361
  const opts = all[all.length - 2] ?? {};
@@ -13037,7 +14378,13 @@ Hint: ${e.hint}` : ""}`
13037
14378
  const fleet = fleetOf(g);
13038
14379
  const waits = [];
13039
14380
  const rows = await actOn(fleet, target, act, { ...opts, waits });
13040
- const { text, failed } = renderMutation(rows, fleet.fallback, act, waits);
14381
+ const { text, failed } = renderMutation(
14382
+ rows,
14383
+ fleet.fallback,
14384
+ act,
14385
+ waits,
14386
+ width(g)
14387
+ );
13041
14388
  emit(g, text, { ...hostsOf(rows, fleet.fallback), rows, waits });
13042
14389
  if (failed) process.exitCode = 1;
13043
14390
  };
@@ -13066,7 +14413,7 @@ ${r.next}`, r);
13066
14413
  wrap(async (g) => {
13067
14414
  const { manifest } = prepare({ project: g.project });
13068
14415
  const h = hostsInfo(manifest);
13069
- emit(g, renderHosts(h), h);
14416
+ emit(g, renderHosts(h, width(g)), h);
13070
14417
  })
13071
14418
  );
13072
14419
  program.command("doctor").description(
@@ -13099,7 +14446,7 @@ ${r.next}`, r);
13099
14446
  const fleet = fleetOf(g);
13100
14447
  if (!app) {
13101
14448
  const d2 = describeProject(fleet.manifest);
13102
- emit(g, renderProject(d2), d2);
14449
+ emit(g, renderProject(d2, width(g)), d2);
13103
14450
  return;
13104
14451
  }
13105
14452
  const d = await describeFleetApp(fleet, app);
@@ -13109,7 +14456,7 @@ ${r.next}`, r);
13109
14456
  `${d.app.name} [${d.app.kind}]: ${d.app.description}`,
13110
14457
  `run: ${d.app.run}`,
13111
14458
  `cwd: ${d.app.cwd}`,
13112
- renderLs([d.row], d.host),
14459
+ renderLs([d.row], d.host, [], width(g)),
13113
14460
  urlNote(d.row),
13114
14461
  `logs: ${d.logs ? `${d.logs.out} | ${d.logs.err}` : "not started yet"}`
13115
14462
  ].filter(Boolean).join("\n"),
@@ -13311,7 +14658,7 @@ ${r.next}`, r);
13311
14658
  wrap(async (g, _o, [port]) => {
13312
14659
  const ctx = await ctxOf(g);
13313
14660
  const r = await portsReport(ctx, port ? Number(port) : void 0);
13314
- emit(g, renderPorts(r, ctx.host.name), r);
14661
+ emit(g, renderPorts(r, ctx.host.name, width(g)), r);
13315
14662
  })
13316
14663
  );
13317
14664
  program.command("start <target>").description("start an app or profile (dependencies first)").option("--reload", "restart apps that are already online").option("--note <text>", "what this start is for, kept on its lease").addOption(new Option("--lease <id>").hideHelp()).action(
@@ -13337,7 +14684,7 @@ ${r.next}`, r);
13337
14684
  ctx = void 0;
13338
14685
  }
13339
14686
  const v = await leasesView(void 0, ctx);
13340
- emit(g, renderLeases(v), v);
14687
+ emit(g, renderLeases(v, width(g)), v);
13341
14688
  })
13342
14689
  );
13343
14690
  leases.command("cancel <id>").description("cancel a pending lease").action(
@@ -13346,6 +14693,22 @@ ${r.next}`, r);
13346
14693
  emit(g, `cancelled ${l.id} (${l.project} ${l.target})`, l);
13347
14694
  })
13348
14695
  );
14696
+ program.command("stacks").description(
14697
+ "every mcproc stack on this host: the checkouts any mcproc call has seen and those found in the local pm2, with their hosts, apps, memory and ports"
14698
+ ).option("--all", "also the recorded checkouts with nothing in pm2").action(
14699
+ wrap(async (g, o) => {
14700
+ if (g.host)
14701
+ throw new McprocError(
14702
+ "BAD_ARGUMENT",
14703
+ "stacks reads every host each stack declares; --host does not apply",
14704
+ {
14705
+ hint: "Drop --host."
14706
+ }
14707
+ );
14708
+ const v = await new StacksCollector().collect({ all: Boolean(o.all) });
14709
+ emit(g, renderStacks(v.answer, width(g)), v.answer);
14710
+ })
14711
+ );
13349
14712
  program.command("exec <command...>").description(
13350
14713
  "run a one-off command from the project root with the manifest's exec.env; with --memory, under a lease in the host's queue (use -- before the command)"
13351
14714
  ).option(
@@ -13452,7 +14815,7 @@ ${r.next}`, r);
13452
14815
  wrap(async (g) => {
13453
14816
  const { manifest, deps: deps2 } = await worktreeCtx(g);
13454
14817
  const rows = await worktreeLs(manifest, deps2);
13455
- emit(g, renderWorktrees(rows), rows);
14818
+ emit(g, renderWorktrees(rows, width(g)), rows);
13456
14819
  })
13457
14820
  );
13458
14821
  worktree.command("remove <ref>").description(
@@ -13477,7 +14840,7 @@ ${r.next}`, r);
13477
14840
  const stop = () => ac.abort();
13478
14841
  process.once("SIGINT", stop);
13479
14842
  process.once("SIGTERM", stop);
13480
- fs19.mkdirSync(deps2.store.dir, { recursive: true });
14843
+ fs20.mkdirSync(deps2.store.dir, { recursive: true });
13481
14844
  deps2.watch = leaseDirWatch(deps2.store.dir);
13482
14845
  const r = await watchLeases(deps2, { signal: ac.signal });
13483
14846
  io.out(`watcher ${r}`);
@@ -13493,25 +14856,36 @@ ${r.next}`, r);
13493
14856
  })
13494
14857
  );
13495
14858
  program.command("monit").description(
13496
- "terminal dashboard: apps with real memory/cpu/ports, log tail, details (--once prints one frame)"
14859
+ "terminal dashboard: apps with real memory/cpu/ports, log tail, details (--once prints one frame); outside a project, or with --all, every stack of this host"
14860
+ ).option(
14861
+ "--all",
14862
+ "the stacks overview: every stack of this host, Enter opens one"
13497
14863
  ).option("--once", "print one frame as text and exit").option("--interval <ms>", "refresh interval", "2000").option("--lines <n>", "log lines of history", "200").action(
13498
14864
  wrap(async (g, o) => {
14865
+ const lines = Number(o.lines) || 200;
14866
+ const intervalMs = Math.max(500, Number(o.interval) || 2e3);
14867
+ const m = monitMode(g, Boolean(o.all));
14868
+ if (m.mode === "overview") {
14869
+ if (g.host)
14870
+ throw new McprocError(
14871
+ "BAD_ARGUMENT",
14872
+ "--host names a host of one manifest; the overview reads every stack",
14873
+ {
14874
+ hint: "Drop --host, or open the project: mcproc --project <root> monit --host <name>."
14875
+ }
14876
+ );
14877
+ if (o.once) {
14878
+ io.out(await overviewOnce({ width: io.columns?.() }));
14879
+ return;
14880
+ }
14881
+ await runOverview({ intervalMs, lines, selected: m.selected });
14882
+ return;
14883
+ }
13499
14884
  if (o.once) {
13500
- io.out(
13501
- await monitOnce({
13502
- project: g.project,
13503
- host: g.host,
13504
- lines: Number(o.lines) || 200
13505
- })
13506
- );
14885
+ io.out(await monitOnce({ project: g.project, host: g.host, lines }));
13507
14886
  return;
13508
14887
  }
13509
- await runMonit({
13510
- project: g.project,
13511
- host: g.host,
13512
- intervalMs: Math.max(500, Number(o.interval) || 2e3),
13513
- lines: Number(o.lines) || 200
13514
- });
14888
+ await runMonit({ project: g.project, host: g.host, intervalMs, lines });
13515
14889
  })
13516
14890
  );
13517
14891
  program.command("unpack").description(