mcproc 0.14.3 → 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 fs9 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 fs8 from "fs";
2692
- import path12 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) => {
@@ -2731,95 +2902,11 @@ function buildEcosystem(m, host) {
2731
2902
  return { apps };
2732
2903
  }
2733
2904
 
2734
- // src/core/slots.ts
2735
- import crypto from "crypto";
2736
- import fs6 from "fs";
2737
- import path10 from "path";
2738
- function registryKey(baseProject, repo) {
2739
- return `${baseProject}-${crypto.createHash("sha256").update(realOf(repo)).digest("hex").slice(0, 8)}`;
2740
- }
2741
- function registryPath(stateDir2, baseProject, repo) {
2742
- return path10.join(stateDir2, "slots", `${registryKey(baseProject, repo)}.json`);
2743
- }
2744
- function emptyRegistry(repo) {
2745
- return { repo, slots: {} };
2746
- }
2747
- function readRegistry(file) {
2748
- if (!fs6.existsSync(file)) return null;
2749
- return JSON.parse(fs6.readFileSync(file, "utf8"));
2750
- }
2751
- function writeRegistry(file, r) {
2752
- fs6.mkdirSync(path10.dirname(file), { recursive: true });
2753
- const tmp = `${file}.${process.pid}.tmp`;
2754
- fs6.writeFileSync(tmp, JSON.stringify(r, null, 2));
2755
- fs6.renameSync(tmp, file);
2756
- }
2757
- function withMain(r, entry) {
2758
- return { ...r, slots: { ...r.slots, "0": entry } };
2759
- }
2760
- function claimSlot(r, opts) {
2761
- if (opts.want !== void 0) {
2762
- if (opts.want < 1 || opts.want > opts.max)
2763
- throw new McprocError(
2764
- "BAD_ARGUMENT",
2765
- `slot ${opts.want} is outside 1..${opts.max} (slot 0 is the main checkout)`
2766
- );
2767
- if (Object.hasOwn(r.slots, String(opts.want)))
2768
- throw new McprocError(
2769
- "SLOT_TAKEN",
2770
- `slot ${opts.want} is taken by ${r.slots[String(opts.want)].path}`,
2771
- { hint: "Pick another slot, or remove that worktree first." }
2772
- );
2773
- return {
2774
- registry: { ...r, slots: { ...r.slots, [opts.want]: opts.entry } },
2775
- slot: opts.want
2776
- };
2777
- }
2778
- for (let s = 1; s <= opts.max; s++)
2779
- if (!Object.hasOwn(r.slots, String(s)))
2780
- return {
2781
- registry: { ...r, slots: { ...r.slots, [s]: opts.entry } },
2782
- slot: s
2783
- };
2784
- throw new McprocError(
2785
- "NO_FREE_SLOT",
2786
- `all ${opts.max} worktree slots of ${r.repo} are taken`,
2787
- {
2788
- hint: "Remove a worktree (worktree remove), or raise worktree.slots in the manifest."
2789
- }
2790
- );
2791
- }
2792
- function releaseSlot(r, slot) {
2793
- const slots = { ...r.slots };
2794
- delete slots[String(slot)];
2795
- return { ...r, slots };
2796
- }
2797
- function realOf(p) {
2798
- try {
2799
- return fs6.realpathSync(p);
2800
- } catch {
2801
- return path10.resolve(p);
2802
- }
2803
- }
2804
- function reclaimGone(r, exists) {
2805
- const freed = [];
2806
- let out = r;
2807
- for (const [k, e] of Object.entries(r.slots)) {
2808
- if (k === "0" || exists(realOf(e.path))) continue;
2809
- out = releaseSlot(out, Number(k));
2810
- freed.push(Number(k));
2811
- }
2812
- return { registry: out, freed: freed.sort((a, b) => a - b) };
2813
- }
2814
- function siblingsOf(r, root) {
2815
- 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);
2816
- }
2817
-
2818
2905
  // src/core/leases/store.ts
2819
2906
  import crypto2 from "crypto";
2820
- import fs7 from "fs";
2821
- import os5 from "os";
2822
- import path11 from "path";
2907
+ import fs8 from "fs";
2908
+ import os6 from "os";
2909
+ import path12 from "path";
2823
2910
  var ALLOWED = {
2824
2911
  pending: ["starting", "running", "cancelled"],
2825
2912
  starting: ["running", "failed", "cancelled"],
@@ -2860,7 +2947,7 @@ function parseHeld(raw) {
2860
2947
  }
2861
2948
  function readHeld(file) {
2862
2949
  try {
2863
- return parseHeld(fs7.readFileSync(file, "utf8"));
2950
+ return parseHeld(fs8.readFileSync(file, "utf8"));
2864
2951
  } catch {
2865
2952
  return null;
2866
2953
  }
@@ -2875,13 +2962,13 @@ var LeaseStore = class {
2875
2962
  * `beforeUnlink` (tests only) runs while a breaker holds the break guard, before the re-read that decides the unlink.
2876
2963
  */
2877
2964
  constructor(stateDir2, deps2 = {}) {
2878
- this.dir = path11.join(stateDir2, "leases");
2965
+ this.dir = path12.join(stateDir2, "leases");
2879
2966
  this.now = deps2.now ?? (() => Date.now());
2880
2967
  this.pid = deps2.pid ?? process.pid;
2881
2968
  this.beforeUnlink = deps2.beforeUnlink;
2882
2969
  }
2883
2970
  file(l) {
2884
- return path11.join(
2971
+ return path12.join(
2885
2972
  this.dir,
2886
2973
  `${String(l.seq).padStart(6, "0")}-${l.id}.json`
2887
2974
  );
@@ -2890,11 +2977,11 @@ var LeaseStore = class {
2890
2977
  return new Date(this.now()).toISOString();
2891
2978
  }
2892
2979
  write(l) {
2893
- fs7.mkdirSync(this.dir, { recursive: true });
2980
+ fs8.mkdirSync(this.dir, { recursive: true });
2894
2981
  const f = this.file(l);
2895
2982
  const tmp = `${f}.${this.pid}.tmp`;
2896
- fs7.writeFileSync(tmp, JSON.stringify(l, null, 2));
2897
- fs7.renameSync(tmp, f);
2983
+ fs8.writeFileSync(tmp, JSON.stringify(l, null, 2));
2984
+ fs8.renameSync(tmp, f);
2898
2985
  return l;
2899
2986
  }
2900
2987
  /**
@@ -2904,13 +2991,13 @@ var LeaseStore = class {
2904
2991
  * breakIfStale), never by moving it aside, so the lock path is never left free while a live holder has it.
2905
2992
  */
2906
2993
  async withLock(fn) {
2907
- fs7.mkdirSync(this.dir, { recursive: true });
2908
- const lock = path11.join(this.dir, ".lock");
2994
+ fs8.mkdirSync(this.dir, { recursive: true });
2995
+ const lock = path12.join(this.dir, ".lock");
2909
2996
  const token = `${this.pid}-${crypto2.randomBytes(6).toString("hex")}`;
2910
- const tmp = path11.join(this.dir, `.lock.${token}.tmp`);
2997
+ const tmp = path12.join(this.dir, `.lock.${token}.tmp`);
2911
2998
  const writeToken = () => {
2912
- fs7.mkdirSync(this.dir, { recursive: true });
2913
- fs7.writeFileSync(
2999
+ fs8.mkdirSync(this.dir, { recursive: true });
3000
+ fs8.writeFileSync(
2914
3001
  tmp,
2915
3002
  JSON.stringify({ pid: this.pid, at: this.now(), token })
2916
3003
  );
@@ -2941,7 +3028,7 @@ var LeaseStore = class {
2941
3028
  await new Promise((r2) => setTimeout(r2, 50));
2942
3029
  }
2943
3030
  } finally {
2944
- fs7.rmSync(tmp, { force: true });
3031
+ fs8.rmSync(tmp, { force: true });
2945
3032
  }
2946
3033
  LIVE_TOKENS.add(token);
2947
3034
  try {
@@ -2956,7 +3043,7 @@ var LeaseStore = class {
2956
3043
  }
2957
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. */
2958
3045
  heldStale(held) {
2959
- if (held.at < this.now() - os5.uptime() * 1e3) return true;
3046
+ if (held.at < this.now() - os6.uptime() * 1e3) return true;
2960
3047
  if (held.pid === process.pid && !LIVE_TOKENS.has(held.token)) return true;
2961
3048
  return this.now() - held.at > LOCK_STALE_MS && !pidAlive(held.pid);
2962
3049
  }
@@ -2967,15 +3054,15 @@ var LeaseStore = class {
2967
3054
  sweepStrays() {
2968
3055
  let names;
2969
3056
  try {
2970
- names = fs7.readdirSync(this.dir);
3057
+ names = fs8.readdirSync(this.dir);
2971
3058
  } catch {
2972
3059
  return;
2973
3060
  }
2974
3061
  for (const n of names) {
2975
3062
  if (!STRAY.test(n)) continue;
2976
- const f = path11.join(this.dir, n);
3063
+ const f = path12.join(this.dir, n);
2977
3064
  if (this.mtimeAgeMs(f) > LOCK_STALE_MS)
2978
- fs7.rmSync(f, { recursive: true, force: true });
3065
+ fs8.rmSync(f, { recursive: true, force: true });
2979
3066
  }
2980
3067
  }
2981
3068
  /**
@@ -2986,12 +3073,12 @@ var LeaseStore = class {
2986
3073
  tryLink(tmp, lock, rewrite) {
2987
3074
  for (let attempt = 0; ; attempt++) {
2988
3075
  try {
2989
- fs7.linkSync(tmp, lock);
3076
+ fs8.linkSync(tmp, lock);
2990
3077
  return true;
2991
3078
  } catch (e) {
2992
3079
  const code = e.code;
2993
3080
  if (code === "EEXIST") return false;
2994
- if (code === "ENOENT" && attempt === 0 && !fs7.existsSync(tmp)) {
3081
+ if (code === "ENOENT" && attempt === 0 && !fs8.existsSync(tmp)) {
2995
3082
  rewrite();
2996
3083
  continue;
2997
3084
  }
@@ -3003,7 +3090,7 @@ var LeaseStore = class {
3003
3090
  stillStale(lock, judged) {
3004
3091
  let raw;
3005
3092
  try {
3006
- raw = fs7.readFileSync(lock, "utf8");
3093
+ raw = fs8.readFileSync(lock, "utf8");
3007
3094
  } catch (e) {
3008
3095
  if (e.code === "ENOENT") return false;
3009
3096
  throw e;
@@ -3016,7 +3103,7 @@ var LeaseStore = class {
3016
3103
  * or, with no readable holder, the directory's mtime is older than LOCK_STALE_MS.
3017
3104
  */
3018
3105
  guardStale(guard) {
3019
- const held = readHeld(path11.join(guard, "holder"));
3106
+ const held = readHeld(path12.join(guard, "holder"));
3020
3107
  if (held)
3021
3108
  return this.now() - held.at > LOCK_STALE_MS && !pidAlive(held.pid);
3022
3109
  return this.mtimeAgeMs(guard) > LOCK_STALE_MS;
@@ -3034,7 +3121,7 @@ var LeaseStore = class {
3034
3121
  breakIfStale(lock, token, take) {
3035
3122
  let raw;
3036
3123
  try {
3037
- raw = fs7.readFileSync(lock, "utf8");
3124
+ raw = fs8.readFileSync(lock, "utf8");
3038
3125
  } catch (e) {
3039
3126
  if (e.code === "ENOENT") return "retry";
3040
3127
  throw e;
@@ -3044,57 +3131,57 @@ var LeaseStore = class {
3044
3131
  if (!stale) return "wait";
3045
3132
  const guard = `${lock}.break`;
3046
3133
  try {
3047
- fs7.mkdirSync(guard);
3134
+ fs8.mkdirSync(guard);
3048
3135
  } catch (e) {
3049
3136
  if (e.code !== "EEXIST") throw e;
3050
3137
  if (!this.guardStale(guard)) return "wait";
3051
3138
  const moved = `${guard}.${token}.stale`;
3052
3139
  try {
3053
- fs7.renameSync(guard, moved);
3140
+ fs8.renameSync(guard, moved);
3054
3141
  } catch (err) {
3055
3142
  if (err.code === "ENOENT") return "retry";
3056
3143
  throw err;
3057
3144
  }
3058
- fs7.rmSync(moved, { recursive: true, force: true });
3145
+ fs8.rmSync(moved, { recursive: true, force: true });
3059
3146
  return "retry";
3060
3147
  }
3061
3148
  try {
3062
- fs7.writeFileSync(
3063
- path11.join(guard, "holder"),
3149
+ fs8.writeFileSync(
3150
+ path12.join(guard, "holder"),
3064
3151
  JSON.stringify({ pid: this.pid, at: this.now(), token })
3065
3152
  );
3066
3153
  this.beforeUnlink?.();
3067
3154
  if (!this.stillStale(lock, held)) return "retry";
3068
3155
  try {
3069
- fs7.unlinkSync(lock);
3156
+ fs8.unlinkSync(lock);
3070
3157
  } catch (e) {
3071
3158
  if (e.code !== "ENOENT") throw e;
3072
3159
  }
3073
3160
  return take() ? "taken" : "retry";
3074
3161
  } finally {
3075
- fs7.rmSync(guard, { recursive: true, force: true });
3162
+ fs8.rmSync(guard, { recursive: true, force: true });
3076
3163
  }
3077
3164
  }
3078
3165
  /** Age of a lock file by its modification time on the file system's clock; 0 when it is gone. */
3079
3166
  mtimeAgeMs(lock) {
3080
3167
  try {
3081
- return Date.now() - fs7.statSync(lock).mtimeMs;
3168
+ return Date.now() - fs8.statSync(lock).mtimeMs;
3082
3169
  } catch {
3083
3170
  return 0;
3084
3171
  }
3085
3172
  }
3086
3173
  /** Unlinks the lock only when it still carries this holder's token; a lock it does not own is never removed. */
3087
3174
  release(lock, token) {
3088
- if (readHeld(lock)?.token === token) fs7.rmSync(lock, { force: true });
3175
+ if (readHeld(lock)?.token === token) fs8.rmSync(lock, { force: true });
3089
3176
  }
3090
3177
  /** Every readable lease, in sequence order; a half-written file is skipped. */
3091
3178
  list() {
3092
- if (!fs7.existsSync(this.dir)) return [];
3093
- return fs7.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) => {
3094
3181
  try {
3095
3182
  return [
3096
3183
  JSON.parse(
3097
- fs7.readFileSync(path11.join(this.dir, f), "utf8")
3184
+ fs8.readFileSync(path12.join(this.dir, f), "utf8")
3098
3185
  )
3099
3186
  ];
3100
3187
  } catch {
@@ -3107,21 +3194,21 @@ var LeaseStore = class {
3107
3194
  return this.list().find((l) => l.id === id) ?? null;
3108
3195
  }
3109
3196
  nextSeq() {
3110
- const f = path11.join(this.dir, ".seq");
3197
+ const f = path12.join(this.dir, ".seq");
3111
3198
  const onDisk = Math.max(
3112
3199
  0,
3113
- ...fs7.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)
3114
3201
  );
3115
- const parsed = fs7.existsSync(f) ? Number(fs7.readFileSync(f, "utf8")) : 0;
3202
+ const parsed = fs8.existsSync(f) ? Number(fs8.readFileSync(f, "utf8")) : 0;
3116
3203
  const n = Math.max(Number.isFinite(parsed) ? parsed : 0, onDisk);
3117
3204
  const tmp = `${f}.${this.pid}.tmp`;
3118
- fs7.writeFileSync(tmp, String(n + 1));
3119
- fs7.renameSync(tmp, f);
3205
+ fs8.writeFileSync(tmp, String(n + 1));
3206
+ fs8.renameSync(tmp, f);
3120
3207
  return n + 1;
3121
3208
  }
3122
3209
  /** A new lease, pending (queued) or running (admitted at once), with `child_pid` the process running its start; call it under the lock. */
3123
3210
  async create(input, state, opts = {}) {
3124
- fs7.mkdirSync(this.dir, { recursive: true });
3211
+ fs8.mkdirSync(this.dir, { recursive: true });
3125
3212
  const at = this.stamp();
3126
3213
  const l = {
3127
3214
  ...input,
@@ -3203,7 +3290,7 @@ var LeaseStore = class {
3203
3290
  for (const l of this.list()) {
3204
3291
  if (!ENDED.has(l.state) || !l.ended_at) continue;
3205
3292
  if (this.now() - Date.parse(l.ended_at) > maxAgeMs) {
3206
- fs7.rmSync(this.file(l), { force: true });
3293
+ fs8.rmSync(this.file(l), { force: true });
3207
3294
  n++;
3208
3295
  }
3209
3296
  }
@@ -3221,7 +3308,7 @@ var EXIT_POLL_MS = 250;
3221
3308
  function exitingAt(dir) {
3222
3309
  try {
3223
3310
  const v = JSON.parse(
3224
- fs8.readFileSync(path12.join(dir, WATCHER_NOTE), "utf8")
3311
+ fs9.readFileSync(path13.join(dir, WATCHER_NOTE), "utf8")
3225
3312
  );
3226
3313
  const at = typeof v.exiting_at === "string" ? Date.parse(v.exiting_at) : Number.NaN;
3227
3314
  return Number.isFinite(at) ? at : null;
@@ -3230,7 +3317,7 @@ function exitingAt(dir) {
3230
3317
  }
3231
3318
  }
3232
3319
  function watcherEntry(env = process.env, argv = process.argv) {
3233
- if (env.MCPROC_CLI_ENTRY) return path12.resolve(env.MCPROC_CLI_ENTRY);
3320
+ if (env.MCPROC_CLI_ENTRY) return path13.resolve(env.MCPROC_CLI_ENTRY);
3234
3321
  const own2 = argv[1] ?? "";
3235
3322
  const real = own2 ? realOf(own2) : "";
3236
3323
  if (real.endsWith("cli.js")) return real;
@@ -3265,10 +3352,10 @@ async function lookAndLaunch(ctx, dir, leaving) {
3265
3352
  if (await watcherOnline(ctx) && (leaving === null || exitingAt(dir) !== leaving))
3266
3353
  return "online";
3267
3354
  const entry = watcherEntry();
3268
- const file = path12.join(dir, "watcher.ecosystem.json");
3269
- fs8.mkdirSync(dir, { recursive: true });
3270
- fs8.rmSync(path12.join(dir, WATCHER_NOTE), { force: true });
3271
- fs8.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(
3272
3359
  file,
3273
3360
  JSON.stringify(
3274
3361
  {
@@ -3304,7 +3391,7 @@ async function lookAndLaunch(ctx, dir, leaving) {
3304
3391
  // src/core/leases/view.ts
3305
3392
  async function leasesView(dir = stateDir(), ctx) {
3306
3393
  const store = new LeaseStore(dir);
3307
- if (fs9.existsSync(store.dir))
3394
+ if (fs10.existsSync(store.dir))
3308
3395
  await store.withLock(() => store.prune()).catch(() => {
3309
3396
  });
3310
3397
  const leases = store.list().map((l) => ({ ...l, position: store.positionOf(l.id) }));
@@ -3359,17 +3446,12 @@ function cancelLease(id, dir = stateDir()) {
3359
3446
  });
3360
3447
  }
3361
3448
 
3362
- // src/core/leases/watcher.ts
3363
- import { spawn as spawn2 } from "child_process";
3364
- import fs12 from "fs";
3365
- import path16 from "path";
3366
-
3367
- // src/core/actions.ts
3368
- import path15 from "path";
3449
+ // src/core/stacks/collect.ts
3450
+ import os8 from "os";
3369
3451
 
3370
3452
  // src/core/list.ts
3371
- import fs10 from "fs";
3372
- import path13 from "path";
3453
+ import fs11 from "fs";
3454
+ import path14 from "path";
3373
3455
 
3374
3456
  // src/core/metrics.ts
3375
3457
  function cpuPercent(pids, before, after, clkTck, intervalSec) {
@@ -3557,7 +3639,7 @@ function parseRead(stdout, id, offset, opts = {}) {
3557
3639
  }
3558
3640
  const skipped = offset !== null && offset >= 0 && size >= offset ? Math.max(0, start - offset) : 0;
3559
3641
  const nextOffset = Math.max(start, size - partial);
3560
- const short = buf.length < size - ctxStart;
3642
+ const short2 = buf.length < size - ctxStart;
3561
3643
  const anchor = hex && HEX.test(hex) ? { at: size, hex } : null;
3562
3644
  return {
3563
3645
  size,
@@ -3566,17 +3648,17 @@ function parseRead(stdout, id, offset, opts = {}) {
3566
3648
  lines,
3567
3649
  contextOnly,
3568
3650
  nextOffset,
3569
- short,
3651
+ short: short2,
3570
3652
  anchor,
3571
- nextAnchor: anchorAt(buf, nextOffset, { size, ctxStart, short, anchor })
3653
+ nextAnchor: anchorAt(buf, nextOffset, { size, ctxStart, short: short2, anchor })
3572
3654
  };
3573
3655
  }
3574
3656
  function anchorAt(buf, at, read) {
3575
- const { size, ctxStart, short, anchor } = read;
3657
+ const { size, ctxStart, short: short2, anchor } = read;
3576
3658
  if (at === size) return anchor;
3577
3659
  const n = Math.min(16, at);
3578
3660
  const end = buf.length - (size - at);
3579
- if (!anchor || short || n <= 0 || at - n < ctxStart || end - n < 0)
3661
+ if (!anchor || short2 || n <= 0 || at - n < ctxStart || end - n < 0)
3580
3662
  return null;
3581
3663
  const tail = buf.subarray(end - n);
3582
3664
  if (tail.includes(REPLACEMENT)) return null;
@@ -3720,8 +3802,8 @@ var LogMarks = class {
3720
3802
  this.m.set(key, e);
3721
3803
  }
3722
3804
  };
3723
- var sinceMark = (path22, r) => ({
3724
- path: path22,
3805
+ var sinceMark = (path23, r) => ({
3806
+ path: path23,
3725
3807
  offset: r.nextOffset,
3726
3808
  ...r.nextAnchor ? { at: r.nextAnchor.at, anchor: r.nextAnchor.hex } : {}
3727
3809
  });
@@ -3752,15 +3834,15 @@ async function readSince(fleet, names, opts) {
3752
3834
  );
3753
3835
  const files = targets.flatMap(
3754
3836
  (t) => streams2.map((stream2) => {
3755
- 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;
3756
3838
  const key = LogMarks.key(fleet.manifest.path, g.host, t.app);
3757
3839
  const mark = spec.kind === "last" ? opts.marks?.get(key)?.[stream2] : void 0;
3758
- const same = mark !== void 0 && mark.path === path22;
3840
+ const same = mark !== void 0 && mark.path === path23;
3759
3841
  const anchor = same && mark.at !== void 0 && mark.anchor !== void 0 ? { at: mark.at, hex: mark.anchor } : null;
3760
3842
  return {
3761
3843
  app: t.app,
3762
3844
  stream: stream2,
3763
- path: path22,
3845
+ path: path23,
3764
3846
  key,
3765
3847
  offset: same ? mark.offset : null,
3766
3848
  anchor,
@@ -4193,38 +4275,14 @@ function urlFields(u) {
4193
4275
  url_reason: null
4194
4276
  };
4195
4277
  }
4196
- async function snapshot(ctx, opts = {}) {
4197
- const withMetrics = opts.metrics !== "none";
4198
- const cpuSample = (opts.cpu ?? "inline") === "inline" && withMetrics;
4199
- const which = opts.urls ?? "check";
4200
- const asked = which === "none" ? [] : Object.values(ctx.manifest.apps).flatMap(
4201
- (a) => a.url !== null && (which === "check" || which.includes(a.name)) ? [{ name: a.name, url: a.url, url_check: a.url_check }] : []
4202
- );
4203
- const urls = [
4204
- ...new Set(
4205
- asked.filter((a) => ctx.host.local || a.url_check !== "here").map((a) => a.url)
4206
- )
4207
- ];
4208
- const containers = withMetrics && opts.containers === "probe" ? [
4209
- ...new Set(
4210
- Object.values(ctx.manifest.apps).flatMap((a) => a.containers)
4211
- )
4212
- ] : [];
4278
+ async function probeHost(link, ask) {
4213
4279
  const res = ensureOk(
4214
- await ctx.transport.exec(
4215
- probeScript(ctx.pm2.bin, {
4216
- cpuSample,
4217
- urls,
4218
- metrics: withMetrics,
4219
- containers
4220
- }),
4221
- {
4222
- env: ctx.pm2.env(),
4223
- // each url adds its bounded lookup and request: many on a host whose resolver drops packets outlast the default
4224
- timeoutMs: probeTimeoutMs(urls.length)
4225
- }
4226
- ),
4227
- 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,
4228
4286
  "probe"
4229
4287
  );
4230
4288
  const sampledAt = Date.now();
@@ -4232,9 +4290,9 @@ async function snapshot(ctx, opts = {}) {
4232
4290
  const tail = (res.stderr || res.stdout).trim().split("\n").slice(-5).join("\n");
4233
4291
  throw new McprocError(
4234
4292
  "TRANSPORT_FAILED",
4235
- `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}`,
4236
4294
  {
4237
- 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.`
4238
4296
  }
4239
4297
  );
4240
4298
  }
@@ -4242,27 +4300,37 @@ async function snapshot(ctx, opts = {}) {
4242
4300
  if (data.jlistRc === 127)
4243
4301
  throw new McprocError(
4244
4302
  "PM2_NOT_FOUND",
4245
- `pm2 not found on ${ctx.transport.describe()}`,
4303
+ `pm2 not found on ${link.transport.describe()}`,
4246
4304
  { hint: PM2_NOT_FOUND_HINT }
4247
4305
  );
4248
4306
  if (data.jlistRc !== 0)
4249
4307
  throw new McprocError(
4250
4308
  "PM2_FAILED",
4251
- `pm2 jlist exited ${data.jlistRc} on ${ctx.transport.describe()}`,
4309
+ `pm2 jlist exited ${data.jlistRc} on ${link.transport.describe()}`,
4252
4310
  { details: res.stderr.slice(-500) }
4253
4311
  );
4254
- 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);
4255
4323
  const pids = appPids(
4256
- ctx.manifest.apps,
4324
+ view.manifest.apps,
4257
4325
  procs,
4258
4326
  data.tree,
4259
- ctx.manifest.project
4327
+ view.manifest.project
4260
4328
  );
4261
4329
  const metricOpts = opts.prev ? { prev: opts.prev.sample, intervalSec: (sampledAt - opts.prev.at) / 1e3 } : {};
4262
4330
  const metrics = {};
4263
4331
  for (const [name2, set] of pids)
4264
4332
  metrics[name2] = treeMetrics(set, data.stats, data.listeners, metricOpts);
4265
- for (const app of Object.values(ctx.manifest.apps)) {
4333
+ for (const app of Object.values(view.manifest.apps)) {
4266
4334
  const m = metrics[app.name];
4267
4335
  if (!m || app.containers.length === 0) continue;
4268
4336
  const mib = app.containers.reduce(
@@ -4278,13 +4346,13 @@ async function snapshot(ctx, opts = {}) {
4278
4346
  }
4279
4347
  const urlChecks = await settleUrls(
4280
4348
  asked,
4281
- { host: ctx.host.name, local: ctx.host.local },
4349
+ { host: view.host.name, local: view.host.local },
4282
4350
  data,
4283
- ctx.hereCheck
4351
+ view.hereCheck
4284
4352
  );
4285
4353
  return {
4286
4354
  procs,
4287
- ports: joinPorts(ctx.manifest.apps, procs, data, ctx.manifest.project),
4355
+ ports: joinPorts(view.manifest.apps, procs, data, view.manifest.project),
4288
4356
  listeners: data.listeners,
4289
4357
  tree: data.tree,
4290
4358
  stats: data.stats,
@@ -4293,9 +4361,30 @@ async function snapshot(ctx, opts = {}) {
4293
4361
  urlChecks,
4294
4362
  zone: data.zone,
4295
4363
  memory: data.memory,
4296
- leases: ctx.host.local && opts.leases !== "none" ? leasesCovering(stateDir(), ctx.manifest) : []
4364
+ leases: view.host.local && opts.leases !== "none" ? leasesCovering(stateDir(), view.manifest) : []
4297
4365
  };
4298
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
+ }
4299
4388
  var SIGNAL_EXITS = /* @__PURE__ */ new Set([130, 143]);
4300
4389
  function statusWord(p, app) {
4301
4390
  if (!p) return "not-started";
@@ -4430,8 +4519,8 @@ var depView = (d) => ({
4430
4519
  timeout_s: d.timeoutMs / 1e3
4431
4520
  });
4432
4521
  function describeProject(m) {
4433
- const readmePath = path13.join(m.root, ".mcproc", "README.md");
4434
- const readme = fs10.existsSync(readmePath) ? fs10.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;
4435
4524
  return {
4436
4525
  project: m.project,
4437
4526
  root: m.root,
@@ -4462,9 +4551,12 @@ function describeProject(m) {
4462
4551
  };
4463
4552
  }
4464
4553
 
4554
+ // src/core/actions.ts
4555
+ import path16 from "path";
4556
+
4465
4557
  // src/core/logfollow.ts
4466
4558
  import { open } from "fs/promises";
4467
- import path14 from "path";
4559
+ import path15 from "path";
4468
4560
 
4469
4561
  // src/core/logwatch.ts
4470
4562
  import { watch as fsWatch } from "fs";
@@ -4807,8 +4899,8 @@ function hostStreamFor(t) {
4807
4899
  var MAX_PENDING = 65536;
4808
4900
  var MAX_READY = 1e4;
4809
4901
  function chunkScript(reqs) {
4810
- return reqs.map(({ path: path22, off, anchor }, i) => {
4811
- const f = shq(path22);
4902
+ return reqs.map(({ path: path23, off, anchor }, i) => {
4903
+ const f = shq(path23);
4812
4904
  const tail = `st=$((sz>${HISTORY_BYTES}?sz-${HISTORY_BYTES}:0))`;
4813
4905
  const replaced = off !== null && anchor ? ` || [ "${hex16(f, String(off))}" != '${anchor}' ]` : "";
4814
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`;
@@ -4958,8 +5050,8 @@ var LogFollower = class {
4958
5050
  }
4959
5051
  const byDir = /* @__PURE__ */ new Map();
4960
5052
  for (const p of [this.paths.out, this.paths.err]) {
4961
- const dir = path14.dirname(p);
4962
- byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path14.basename(p)));
5053
+ const dir = path15.dirname(p);
5054
+ byDir.set(dir, (byDir.get(dir) ?? /* @__PURE__ */ new Set()).add(path15.basename(p)));
4963
5055
  }
4964
5056
  for (const [dir, names] of byDir)
4965
5057
  this.unwatch.push(
@@ -5332,6 +5424,9 @@ function detectReloads(input, rules, opts) {
5332
5424
  return [...t.push(input, 0), ...t.flush()];
5333
5425
  }
5334
5426
 
5427
+ // src/mcp/render.ts
5428
+ import os7 from "os";
5429
+
5335
5430
  // src/core/logstream.ts
5336
5431
  var WAKE_GAP_MS = 50;
5337
5432
  function signals() {
@@ -5527,6 +5622,135 @@ async function streamLogs(sources, opts, deps2) {
5527
5622
  }
5528
5623
  }
5529
5624
 
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
+
5530
5754
  // src/mcp/render.ts
5531
5755
  function fmtUptime(ms) {
5532
5756
  if (ms === null) return "-";
@@ -5556,13 +5780,84 @@ function fmtPorts(p) {
5556
5780
  for (const u of p.undeclared) parts.push(`:${u} (undeclared)`);
5557
5781
  return parts.length ? parts.join(", ") : "-";
5558
5782
  }
5559
- function table(rows) {
5560
- const w = rows[0].map(
5561
- (_, 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))))
5562
5840
  );
5563
- return rows.map(
5564
- (r) => r.map((c, i) => (c ?? "").padEnd(w[i])).join(" ").trimEnd()
5565
- ).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");
5566
5861
  }
5567
5862
  function fmtCpu(c) {
5568
5863
  return c === null ? "-" : `${c.toFixed(1)}%`;
@@ -5590,7 +5885,7 @@ function fmtRestarts(r) {
5590
5885
  const n = r.reloads;
5591
5886
  return n && n.count > 0 ? `${r.restarts} (+${n.partial ? "\u2265" : ""}${n.count})` : String(r.restarts);
5592
5887
  }
5593
- function renderLs(rows, host, errors = []) {
5888
+ function renderLs(rows, host, errors = [], width) {
5594
5889
  const hosts = [...new Set(rows.map((r) => r.host))];
5595
5890
  const multi = hosts.length > 1;
5596
5891
  const head = multi ? `hosts: ${hosts.join(", ")}` : `host: ${hosts[0] ?? host}`;
@@ -5599,40 +5894,45 @@ function renderLs(rows, host, errors = []) {
5599
5894
  rows.filter((r) => r.error).map((r) => [r.host, `${r.host}: ${r.error}`])
5600
5895
  ).values()
5601
5896
  ];
5897
+ const o = multi ? 1 : 0;
5898
+ const cap = (s, n) => width === void 0 ? cut2(s, n) : s;
5602
5899
  return [
5603
5900
  head,
5604
- table([
5901
+ table(
5605
5902
  [
5606
- "APP",
5607
- ...multi ? ["HOST"] : [],
5608
- "KIND",
5609
- "STATUS",
5610
- "PID",
5611
- "UPTIME",
5612
- "RESTARTS",
5613
- "MEM",
5614
- "CPU",
5615
- "PORTS",
5616
- "URL",
5617
- "RUNTIME",
5618
- "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
+ ])
5619
5933
  ],
5620
- ...rows.map((r) => [
5621
- r.app,
5622
- ...multi ? [r.host] : [],
5623
- r.kind,
5624
- r.status,
5625
- r.pid === null ? "-" : String(r.pid),
5626
- fmtUptime(r.uptime_ms),
5627
- fmtRestarts(r),
5628
- r.memory ? fmtBytes(r.memory) : "-",
5629
- r.status === "online" ? fmtCpu(r.cpu) : "-",
5630
- fmtPorts(r.ports),
5631
- fmtUrl(r),
5632
- r.runtime ? cut2(r.runtime, 32) : "-",
5633
- cut2(r.description.split("\n")[0], 40)
5634
- ])
5635
- ]),
5934
+ { width, flex: [8 + o, 9 + o, 10 + o, 11 + o], mode: "wrap" }
5935
+ ),
5636
5936
  ...unreachable,
5637
5937
  ...errors.map((e) => `${e.host}: ${e.message}`)
5638
5938
  ].join("\n");
@@ -5649,36 +5949,42 @@ function startNote(rows) {
5649
5949
  );
5650
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` : "";
5651
5951
  }
5652
- function renderPorts(rep, host) {
5952
+ function renderPorts(rep, host, width) {
5653
5953
  if (rep.listeners.length === 0)
5654
5954
  return `host: ${host}
5655
5955
  ${rep.port ? `nothing listens on port ${rep.port}` : "no listening sockets visible"}`;
5656
5956
  return `host: ${host}
5657
- ` + table([
5658
- ["PROTO", "ADDR", "PORT", "PID", "COMMAND", "APP", "CONTAINER"],
5659
- ...rep.listeners.map((l) => [
5660
- l.proto,
5661
- l.addr,
5662
- String(l.port),
5663
- l.pid === null ? "?" : String(l.pid),
5664
- l.command ?? "?",
5665
- l.app ?? "-",
5666
- l.container ?? "-"
5667
- ])
5668
- ]);
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
+ );
5669
5972
  }
5670
- function renderHosts(info) {
5671
- return table([
5672
- ["HOST", "TRANSPORT", "CWD", "READONLY", "PM2_HOME", "APPS"],
5673
- ...info.map((h) => [
5674
- h.name,
5675
- h.transport,
5676
- h.cwd,
5677
- h.readonly ? "yes" : "no",
5678
- h.pm2_home ?? "-",
5679
- h.apps.length ? h.apps.join(", ") : "-"
5680
- ])
5681
- ]);
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
+ );
5682
5988
  }
5683
5989
  function fmtEnv(e) {
5684
5990
  const kind = e.container === "none" ? "no container" : `container (${e.container})`;
@@ -5826,7 +6132,7 @@ function fmtDeps(deps2) {
5826
6132
  (d) => d.condition === "started" ? d.app : `${d.app} (${d.condition})`
5827
6133
  ).join(", ");
5828
6134
  }
5829
- function renderProject(d) {
6135
+ function renderProject(d, width) {
5830
6136
  const L = [`project: ${d.project} (${d.root})`];
5831
6137
  const w = d.mcproc.manifest;
5832
6138
  if (w)
@@ -5850,7 +6156,7 @@ function renderProject(d) {
5850
6156
  L.push(` ${p}: ${apps.join(", ")}`);
5851
6157
  L.push(
5852
6158
  "hosts:",
5853
- 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")
5854
6160
  );
5855
6161
  if (d.readme) L.push("", "README:", d.readme.trim());
5856
6162
  return L.join("\n");
@@ -5873,9 +6179,9 @@ function incidentLines(i, opts = {}) {
5873
6179
  function renderIncidents(a, opts = {}) {
5874
6180
  const tail = [];
5875
6181
  if (a.more > 0) {
5876
- const short = a.more_by === "size" ? " to keep this answer short" : "";
6182
+ const short2 = a.more_by === "size" ? " to keep this answer short" : "";
5877
6183
  tail.push(
5878
- 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)`
5879
6185
  );
5880
6186
  }
5881
6187
  for (const s of a.skipped)
@@ -5924,7 +6230,7 @@ function queuedNote(rows) {
5924
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)`;
5925
6231
  }).join("\n");
5926
6232
  }
5927
- function renderLeases(v) {
6233
+ function renderLeases(v, width) {
5928
6234
  const head = [
5929
6235
  `state: ${v.state_dir}`,
5930
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)",
@@ -5944,23 +6250,26 @@ function renderLeases(v) {
5944
6250
  ]);
5945
6251
  return [
5946
6252
  ...head,
5947
- table([
6253
+ table(
5948
6254
  [
5949
- "POS",
5950
- "STATE",
5951
- "KIND",
5952
- "PROJECT",
5953
- "TARGET",
5954
- "EST \u2192 NEED",
5955
- "FROM",
5956
- "WAITING FOR / ERROR",
5957
- "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
5958
6267
  ],
5959
- ...rows
5960
- ])
6268
+ { width, flex: [4, 7], mode: "wrap" }
6269
+ )
5961
6270
  ].join("\n");
5962
6271
  }
5963
- function renderMutation(rows, fallback, act, waits = []) {
6272
+ function renderMutation(rows, fallback, act, waits = [], width) {
5964
6273
  const bad3 = rows.filter((r) => r.error);
5965
6274
  const notes = [
5966
6275
  act === "start" || act === "restart" ? startNote(rows) : "",
@@ -5971,7 +6280,7 @@ function renderMutation(rows, fallback, act, waits = []) {
5971
6280
  const head = bad3.length ? `${act} partly failed: ${bad3.map((r) => `${r.app} (${r.host}): ${r.error}`).join("; ")}
5972
6281
  ` : "";
5973
6282
  return {
5974
- text: head + renderLs(rows, fallback) + notes.map((n) => `
6283
+ text: head + renderLs(rows, fallback, [], width) + notes.map((n) => `
5975
6284
  ${n}`).join(""),
5976
6285
  failed: bad3.length > 0
5977
6286
  };
@@ -6025,22 +6334,123 @@ function renderWorktreeAdded(r) {
6025
6334
  L.push(`next: ${r.next}`);
6026
6335
  return L.join("\n");
6027
6336
  }
6028
- function renderWorktrees(rows) {
6029
- return table([
6030
- ["SLOT", "PROJECT", "BRANCH", "ONLINE", "LEASE", "PATH"],
6031
- ...rows.map((r) => [
6032
- r.main ? `${r.slot} (main)` : String(r.slot),
6033
- r.project,
6034
- r.branch ?? "(detached)",
6035
- String(r.online),
6036
- r.lease ?? "-",
6037
- r.path
6038
- ])
6039
- ]);
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
+ );
6040
6352
  }
6041
6353
  function renderWorktreeRemoved(r) {
6042
6354
  return `removed ${r.path}: slot ${r.slot} is free, branch ${r.branch ?? "(detached)"} kept; stopped ${r.stopped.length ? r.stopped.join(", ") : "nothing"}`;
6043
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
+ }
6044
6454
 
6045
6455
  // src/core/wait.ts
6046
6456
  var WAIT_TIMEOUT_DEFAULT_MS = 6e4;
@@ -6416,7 +6826,7 @@ function assertNoCollision(ctx, procs, names) {
6416
6826
  const name2 = pm2Name(ctx.manifest.project, n);
6417
6827
  const expectedCwd = appCwd(ctx.host, ctx.manifest.apps[n]);
6418
6828
  const other = procs.find(
6419
- (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path15.posix.resolve(p.cwd) !== expectedCwd)
6829
+ (p) => p.name === name2 && (p.namespace !== ctx.manifest.project || p.cwd !== "" && path16.posix.resolve(p.cwd) !== expectedCwd)
6420
6830
  );
6421
6831
  if (other && other.namespace !== ctx.manifest.project) {
6422
6832
  throw new McprocError(
@@ -6443,7 +6853,7 @@ async function shipEcosystem(ctx) {
6443
6853
  const eco = buildEcosystem(ctx.manifest, ctx.host);
6444
6854
  const file = ecosystemPath(ctx.host);
6445
6855
  const dir = generatedDir(ctx.host);
6446
- const gi = path15.posix.join(path15.posix.dirname(dir), ".gitignore");
6856
+ const gi = path16.posix.join(path16.posix.dirname(dir), ".gitignore");
6447
6857
  const cmd = `mkdir -p ${shq(dir)} && { [ -e ${shq(gi)} ] || printf '*\\n' > ${shq(gi)}; } && cat > ${shq(file)}`;
6448
6858
  const res = ensureOk(
6449
6859
  await ctx.transport.exec(cmd, { stdin: JSON.stringify(eco, null, 2) }),
@@ -6620,7 +7030,7 @@ function admit(opts) {
6620
7030
  }
6621
7031
 
6622
7032
  // src/core/leases/siblings.ts
6623
- import fs11 from "fs";
7033
+ import fs12 from "fs";
6624
7034
  var MIB = 1024 * 1024;
6625
7035
  var repoOf = (m) => m.repo;
6626
7036
  function registryOf(m, dir = stateDir()) {
@@ -6630,7 +7040,7 @@ function ownsManifest(root) {
6630
7040
  const found = findManifestPath(root);
6631
7041
  if (!found) return false;
6632
7042
  try {
6633
- return fs11.realpathSync(found.root) === fs11.realpathSync(root);
7043
+ return fs12.realpathSync(found.root) === fs12.realpathSync(root);
6634
7044
  } catch {
6635
7045
  return false;
6636
7046
  }
@@ -6648,7 +7058,7 @@ async function listCheckout(root, spawner) {
6648
7058
  async function measureSiblings(m, deps2) {
6649
7059
  const out = /* @__PURE__ */ new Map();
6650
7060
  if (!deps2.registry) return out;
6651
- const exists = deps2.exists ?? ((p) => fs11.existsSync(p));
7061
+ const exists = deps2.exists ?? ((p) => fs12.existsSync(p));
6652
7062
  const read = await Promise.all(
6653
7063
  siblingsOf(deps2.registry, m.root).filter(({ entry }) => exists(entry.path)).map(async ({ slot, entry }) => {
6654
7064
  try {
@@ -7039,10 +7449,18 @@ function failedRows(fleet, r, e) {
7039
7449
  );
7040
7450
  }
7041
7451
  async function actOn(fleet, target, act, opts = {}) {
7042
- const named = fleet.select([target]);
7043
- const names = act === "start" ? fleet.withDeps(named) : named;
7044
- const runs = fleet.runs(names);
7045
- assertWritableRuns(fleet, runs, named);
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]);
7461
+ const names = act === "start" ? fleet.withDeps(named) : named;
7462
+ const runs = fleet.runs(names);
7463
+ assertWritableRuns(fleet, runs, named);
7046
7464
  if (act === "start") assertLocalBudgets(fleet, runs);
7047
7465
  const rows = [];
7048
7466
  let failed = null;
@@ -7081,6 +7499,7 @@ async function actOn(fleet, target, act, opts = {}) {
7081
7499
  r.apps,
7082
7500
  {
7083
7501
  target,
7502
+ // a start of a list is refused above
7084
7503
  host: fleet.requested ?? null,
7085
7504
  reload: opts.reload,
7086
7505
  note: opts.note,
@@ -7202,7 +7621,531 @@ async function describeFleetApp(fleet, app) {
7202
7621
  };
7203
7622
  }
7204
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);
8124
+ }
8125
+ }
8126
+ return notes;
8127
+ }
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
+ };
8144
+
7205
8145
  // src/core/leases/watcher.ts
8146
+ import { spawn as spawn2 } from "child_process";
8147
+ import fs13 from "fs";
8148
+ import path17 from "path";
7206
8149
  async function moveIf(store, id, from, to, patch = {}, also = () => true) {
7207
8150
  return store.withLock(async () => {
7208
8151
  const cur = store.get(id);
@@ -7395,13 +8338,13 @@ async function leaveIdle(deps2) {
7395
8338
  return store.withLock(() => {
7396
8339
  if (store.list().some((l) => l.state === "pending" || l.state === "starting"))
7397
8340
  return false;
7398
- const file = path16.join(path16.dirname(store.dir), WATCHER_NOTE);
8341
+ const file = path17.join(path17.dirname(store.dir), WATCHER_NOTE);
7399
8342
  const tmp = `${file}.${process.pid}.tmp`;
7400
- fs12.writeFileSync(
8343
+ fs13.writeFileSync(
7401
8344
  tmp,
7402
8345
  JSON.stringify({ exiting_at: new Date(deps2.now()).toISOString() })
7403
8346
  );
7404
- fs12.renameSync(tmp, file);
8347
+ fs13.renameSync(tmp, file);
7405
8348
  return true;
7406
8349
  });
7407
8350
  }
@@ -7512,7 +8455,7 @@ async function watchLeases(deps2, opts = {}) {
7512
8455
  }
7513
8456
  function leaseDirWatch(dir) {
7514
8457
  return (cb) => {
7515
- const w = fs12.watch(dir, (_e, name2) => {
8458
+ const w = fs13.watch(dir, (_e, name2) => {
7516
8459
  if (!name2?.startsWith(".")) cb();
7517
8460
  });
7518
8461
  return () => w.close();
@@ -7875,8 +8818,8 @@ async function runExec(ctx, opts, deps2) {
7875
8818
 
7876
8819
  // src/core/worktree.ts
7877
8820
  import { execFile } from "child_process";
7878
- import fs13 from "fs";
7879
- import path17 from "path";
8821
+ import fs14 from "fs";
8822
+ import path18 from "path";
7880
8823
  var gitRunner = (args, cwd) => new Promise((resolve) => {
7881
8824
  execFile(
7882
8825
  "git",
@@ -7901,7 +8844,7 @@ var gitOk = async (git, args, cwd) => {
7901
8844
  };
7902
8845
  var present = (p) => {
7903
8846
  try {
7904
- fs13.lstatSync(p);
8847
+ fs14.lstatSync(p);
7905
8848
  return true;
7906
8849
  } catch {
7907
8850
  return false;
@@ -7909,15 +8852,15 @@ var present = (p) => {
7909
8852
  };
7910
8853
  async function mainCheckout(root, git) {
7911
8854
  const common = (await gitOk(git, ["rev-parse", "--git-common-dir"], root)).trim();
7912
- return fs13.realpathSync(path17.dirname(path17.resolve(root, common)));
8855
+ return fs14.realpathSync(path18.dirname(path18.resolve(root, common)));
7913
8856
  }
7914
8857
  function worktreeDir(m, main) {
7915
- 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`;
7916
8859
  }
7917
8860
  function installCommand(m, root) {
7918
8861
  if (m.worktree.install === false) return null;
7919
8862
  if (typeof m.worktree.install === "string") return m.worktree.install;
7920
- const has = (f) => fs13.existsSync(path17.join(root, f));
8863
+ const has = (f) => fs14.existsSync(path18.join(root, f));
7921
8864
  if (has("bun.lock") || has("bun.lockb")) return "bun install";
7922
8865
  if (has("yarn.lock")) return "yarn install";
7923
8866
  if (has("pnpm-lock.yaml")) return "pnpm install";
@@ -7925,22 +8868,22 @@ function installCommand(m, root) {
7925
8868
  return null;
7926
8869
  }
7927
8870
  function planCopies(main, target, copy, link, fsDeps = {}) {
7928
- const exists = fsDeps.exists ?? ((p) => fs13.existsSync(p));
7929
- 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 }));
7930
8873
  const copies = [];
7931
8874
  const missing = [];
7932
8875
  for (const entry of copy) {
7933
- 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] : [];
7934
8877
  if (hits2.length === 0) {
7935
8878
  missing.push(entry);
7936
8879
  continue;
7937
8880
  }
7938
8881
  for (const h of hits2)
7939
- 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) });
7940
8883
  }
7941
8884
  const links = link.map((l) => ({
7942
- from: path17.join(main, l),
7943
- to: path17.join(target, l)
8885
+ from: path18.join(main, l),
8886
+ to: path18.join(target, l)
7944
8887
  }));
7945
8888
  return { copies, links, missing };
7946
8889
  }
@@ -7956,7 +8899,7 @@ function ownManifest(target, log, instead) {
7956
8899
  }
7957
8900
  }
7958
8901
  function excludePattern(rel) {
7959
- const p = path17.posix.normalize(rel.split(path17.sep).join("/")).replace(/\/+$/, "");
8902
+ const p = path18.posix.normalize(rel.split(path18.sep).join("/")).replace(/\/+$/, "");
7960
8903
  if (p === "." || p === "" || p === ".." || p.startsWith("../") || p.startsWith("/"))
7961
8904
  return null;
7962
8905
  return `/${p.replace(/[\\*?[\]]/g, (c) => `\\${c}`).replace(/ $/, "\\ ")}`;
@@ -7980,13 +8923,13 @@ async function excludeLinks(target, links, git, log) {
7980
8923
  );
7981
8924
  return [];
7982
8925
  }
7983
- const file = path17.resolve(target, r.stdout.trim());
7984
- fs13.mkdirSync(path17.dirname(file), { recursive: true });
7985
- 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") : "";
7986
8929
  const lines = new Set(had.split("\n"));
7987
8930
  const add2 = patterns.filter((p) => !lines.has(p));
7988
8931
  if (add2.length)
7989
- fs13.appendFileSync(
8932
+ fs14.appendFileSync(
7990
8933
  file,
7991
8934
  `${had.length && !had.endsWith("\n") ? "\n" : ""}${add2.join("\n")}
7992
8935
  `
@@ -8002,7 +8945,7 @@ function registryFor(m, main, mainBranch, deps2) {
8002
8945
  });
8003
8946
  const { registry, freed } = reclaimGone(
8004
8947
  read,
8005
- deps2.exists ?? ((p) => fs13.existsSync(p))
8948
+ deps2.exists ?? ((p) => fs14.existsSync(p))
8006
8949
  );
8007
8950
  for (const s of freed) deps2.log(`slot ${s} reclaimed: its checkout is gone`);
8008
8951
  return { file, registry };
@@ -8032,12 +8975,12 @@ async function worktreeList(main, git) {
8032
8975
  }
8033
8976
  function realAhead(p) {
8034
8977
  const rest = [];
8035
- let d = path17.resolve(p);
8036
- while (!present(d) && path17.dirname(d) !== d) {
8037
- rest.unshift(path17.basename(d));
8038
- 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);
8039
8982
  }
8040
- return path17.join(realOf(d), ...rest);
8983
+ return path18.join(realOf(d), ...rest);
8041
8984
  }
8042
8985
  var shPath = (p) => /^[\w./@%+:-]+$/.test(p) ? p : shq(p);
8043
8986
  async function worktreeAdd(m, opts, deps2) {
@@ -8074,7 +9017,7 @@ async function worktreeAdd(m, opts, deps2) {
8074
9017
  }
8075
9018
  );
8076
9019
  const target = realAhead(
8077
- path17.join(worktreeDir(m, main), ...opts.branch.split("/"))
9020
+ path18.join(worktreeDir(m, main), ...opts.branch.split("/"))
8078
9021
  );
8079
9022
  const taken = () => new McprocError("WORKTREE_EXISTS", `${target} exists already`, {
8080
9023
  hint: "Remove it, or pick another branch name."
@@ -8097,9 +9040,9 @@ async function worktreeAdd(m, opts, deps2) {
8097
9040
  `project slug ${project2} is the slug of slot ${k} (${e.path})`,
8098
9041
  { hint: "Pass another project slug." }
8099
9042
  );
8100
- fs13.mkdirSync(path17.dirname(target), { recursive: true });
9043
+ fs14.mkdirSync(path18.dirname(target), { recursive: true });
8101
9044
  try {
8102
- fs13.mkdirSync(target);
9045
+ fs14.mkdirSync(target);
8103
9046
  } catch (e) {
8104
9047
  if (e.code === "EEXIST") throw taken();
8105
9048
  throw e;
@@ -8134,7 +9077,7 @@ async function worktreeAdd(m, opts, deps2) {
8134
9077
  );
8135
9078
  } catch (e) {
8136
9079
  try {
8137
- fs13.rmdirSync(target);
9080
+ fs14.rmdirSync(target);
8138
9081
  } catch {
8139
9082
  }
8140
9083
  await store.withLock(() => {
@@ -8154,8 +9097,8 @@ async function worktreeAdd(m, opts, deps2) {
8154
9097
  repo: main,
8155
9098
  vars: (probe ?? m).slot !== null ? { slot } : void 0
8156
9099
  });
8157
- const gi = path17.join(target, ".mcproc", ".gitignore");
8158
- if (!present(gi)) fs13.writeFileSync(gi, "*\n");
9100
+ const gi = path18.join(target, ".mcproc", ".gitignore");
9101
+ if (!present(gi)) fs14.writeFileSync(gi, "*\n");
8159
9102
  const own2 = probe ? ownManifest(target, deps2.log, "the copies and the install") : null;
8160
9103
  const dm = own2 ?? m;
8161
9104
  const only = (xs, not) => [...new Set(xs)].filter((e) => !not.includes(e));
@@ -8185,15 +9128,15 @@ async function worktreeAdd(m, opts, deps2) {
8185
9128
  missing: [...ownPlan.missing, ...extraPlan.missing]
8186
9129
  };
8187
9130
  const fromOverride = [
8188
- ...addedCopies.map((c) => path17.relative(main, c.from)),
8189
- ...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))
8190
9133
  ];
8191
9134
  let step = "";
8192
9135
  try {
8193
9136
  for (const c of plan.copies) {
8194
9137
  step = `copying ${c.from}`;
8195
- fs13.mkdirSync(path17.dirname(c.to), { recursive: true });
8196
- fs13.cpSync(c.from, c.to, {
9138
+ fs14.mkdirSync(path18.dirname(c.to), { recursive: true });
9139
+ fs14.cpSync(c.from, c.to, {
8197
9140
  recursive: true,
8198
9141
  force: false,
8199
9142
  errorOnExist: false
@@ -8201,8 +9144,8 @@ async function worktreeAdd(m, opts, deps2) {
8201
9144
  }
8202
9145
  for (const l of plan.links) {
8203
9146
  step = `linking ${l.from}`;
8204
- fs13.mkdirSync(path17.dirname(l.to), { recursive: true });
8205
- 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);
8206
9149
  }
8207
9150
  } catch (e) {
8208
9151
  throw new McprocError(
@@ -8217,7 +9160,7 @@ async function worktreeAdd(m, opts, deps2) {
8217
9160
  deps2.log(`copy: ${miss} is not in ${main}, skipped`);
8218
9161
  const excluded = await excludeLinks(
8219
9162
  target,
8220
- plan.links.map((l) => path17.relative(target, l.to)),
9163
+ plan.links.map((l) => path18.relative(target, l.to)),
8221
9164
  deps2.git,
8222
9165
  deps2.log
8223
9166
  );
@@ -8246,8 +9189,8 @@ async function worktreeAdd(m, opts, deps2) {
8246
9189
  project,
8247
9190
  repo: main,
8248
9191
  manifest: own2 ? "worktree" : "main",
8249
- copied: plan.copies.map((c) => path17.relative(main, c.from)),
8250
- 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)),
8251
9194
  from_override: fromOverride,
8252
9195
  excluded,
8253
9196
  missing: plan.missing,
@@ -8264,7 +9207,7 @@ async function worktreeLs(m, deps2) {
8264
9207
  branch: await currentBranch(main, deps2.git),
8265
9208
  project: m.base_project
8266
9209
  });
8267
- const exists = deps2.exists ?? ((p) => fs13.existsSync(p));
9210
+ const exists = deps2.exists ?? ((p) => fs14.existsSync(p));
8268
9211
  const branches = await worktreeList(main, deps2.git);
8269
9212
  const procs = await deps2.jlist();
8270
9213
  const leases = new LeaseStore(deps2.stateDir).list();
@@ -8314,7 +9257,7 @@ async function worktreeRemove(m, ref, opts, deps2) {
8314
9257
  if (!(await worktreeList(main, deps2.git)).has(realOf(e.path))) {
8315
9258
  let empty = false;
8316
9259
  try {
8317
- empty = fs13.readdirSync(e.path).length === 0;
9260
+ empty = fs14.readdirSync(e.path).length === 0;
8318
9261
  } catch {
8319
9262
  }
8320
9263
  if (!empty)
@@ -8323,7 +9266,7 @@ async function worktreeRemove(m, ref, opts, deps2) {
8323
9266
  `${e.path} (slot ${k}) is no git worktree of ${main}`,
8324
9267
  { hint: "Look at what is in it, remove it by hand, then worktree ls." }
8325
9268
  );
8326
- fs13.rmdirSync(e.path);
9269
+ fs14.rmdirSync(e.path);
8327
9270
  deps2.log(`${e.path} was an empty folder git never filled: removed`);
8328
9271
  await free();
8329
9272
  return { path: e.path, slot: Number(k), branch: e.branch, stopped: [] };
@@ -8344,14 +9287,15 @@ async function worktreeRemove(m, ref, opts, deps2) {
8344
9287
  main
8345
9288
  );
8346
9289
  const top = realOf(worktreeDir(m, main));
8347
- 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)) {
8348
9291
  try {
8349
- fs13.rmdirSync(d);
9292
+ fs14.rmdirSync(d);
8350
9293
  } catch {
8351
9294
  break;
8352
9295
  }
8353
9296
  }
8354
9297
  await free();
9298
+ forgetProject(e.project, e.path, deps2.stateDir);
8355
9299
  return { path: e.path, slot: Number(k), branch: e.branch, stopped };
8356
9300
  }
8357
9301
  async function stopCheckout(root, opts) {
@@ -8411,10 +9355,10 @@ async function defaultWorktreeDeps(ctx, io, opts = {}) {
8411
9355
  }
8412
9356
 
8413
9357
  // src/core/incidents/store.ts
8414
- import fs14 from "fs";
9358
+ import fs15 from "fs";
8415
9359
  import { createHash as createHash3, randomUUID } from "crypto";
8416
9360
  import net from "net";
8417
- import path18 from "path";
9361
+ import path19 from "path";
8418
9362
 
8419
9363
  // src/core/incidents/taken.ts
8420
9364
  function takenPoints(s) {
@@ -8484,16 +9428,16 @@ async function pollIncidents(fleet, names, opts = {}) {
8484
9428
  };
8485
9429
  const files = targets.flatMap(
8486
9430
  (t) => STREAMS2.map((stream) => {
8487
- 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;
8488
9432
  const mark = t.prev?.[stream];
8489
- const same = mark && mark.path === path22;
9433
+ const same = mark && mark.path === path23;
8490
9434
  const emptied = opts.restart?.[markKey(g.host, t.app)]?.[stream];
8491
- const restarted = !!emptied && (emptied.includes(path22) || emptied.includes("*"));
9435
+ const restarted = !!emptied && (emptied.includes(path23) || emptied.includes("*"));
8492
9436
  const anchor = !restarted && same && mark.at !== void 0 && mark.anchor !== void 0 ? { at: mark.at, hex: mark.anchor } : null;
8493
9437
  return {
8494
9438
  t,
8495
9439
  stream,
8496
- path: path22,
9440
+ path: path23,
8497
9441
  offset: restarted ? 0 : same ? mark.offset : null,
8498
9442
  anchor,
8499
9443
  restarted
@@ -8757,7 +9701,7 @@ function processEvents(app, host, p, prev, errLines) {
8757
9701
  }
8758
9702
 
8759
9703
  // src/core/incidents/store.ts
8760
- var fileClearKey = (key, stream, path22) => `${key}\0${stream}\0${path22}`;
9704
+ var fileClearKey = (key, stream, path23) => `${key}\0${stream}\0${path23}`;
8761
9705
  var countedClearKey = (key) => `${key}\0counted`;
8762
9706
  var isAppClearKey = (k) => !k.includes("\0");
8763
9707
  function parseFileClearKey(k) {
@@ -8766,13 +9710,13 @@ function parseFileClearKey(k) {
8766
9710
  return { key, stream, path: rest.join("\0") };
8767
9711
  }
8768
9712
  var LOCK_WAIT_MS2 = 3e4;
8769
- var storePath = (manifestPath) => path18.join(path18.dirname(manifestPath), "incidents.json");
9713
+ var storePath = (manifestPath) => path19.join(path19.dirname(manifestPath), "incidents.json");
8770
9714
  var pad = (n) => String(n).padStart(2, "0");
8771
9715
  var localStamp = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
8772
9716
  function readStore(file) {
8773
9717
  try {
8774
9718
  const s = JSON.parse(
8775
- fs14.readFileSync(file, "utf8")
9719
+ fs15.readFileSync(file, "utf8")
8776
9720
  );
8777
9721
  const obj = (o) => typeof o === "object" && o !== null ? o : {};
8778
9722
  if (s && s.v === 1 && typeof s.apps === "object" && s.apps !== null)
@@ -8787,12 +9731,12 @@ function readStore(file) {
8787
9731
  return { v: 1, apps: {}, cleared: {}, clears: {} };
8788
9732
  }
8789
9733
  function lockAddress(file) {
8790
- let dir = path18.dirname(path18.resolve(file));
9734
+ let dir = path19.dirname(path19.resolve(file));
8791
9735
  try {
8792
- dir = fs14.realpathSync(dir);
9736
+ dir = fs15.realpathSync(dir);
8793
9737
  } catch {
8794
9738
  }
8795
- 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();
8796
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 };
8797
9741
  }
8798
9742
  function tryLock(addr) {
@@ -8828,8 +9772,8 @@ async function withStore(file, fn, waitMs = LOCK_WAIT_MS2) {
8828
9772
  try {
8829
9773
  const { result, next } = await fn(readStore(file));
8830
9774
  const tmp = `${file}.${randomUUID()}.tmp`;
8831
- fs14.writeFileSync(tmp, JSON.stringify(next));
8832
- fs14.renameSync(tmp, file);
9775
+ fs15.writeFileSync(tmp, JSON.stringify(next));
9776
+ fs15.renameSync(tmp, file);
8833
9777
  return result;
8834
9778
  } finally {
8835
9779
  await new Promise((r) => lock.close(r));
@@ -8896,8 +9840,8 @@ async function clearLogs(fleet, names, file = storePath(fleet.manifest.path), no
8896
9840
  for (const f of await flushLogs(await fleet.ctx(g.host), g.apps)) {
8897
9841
  const key = markKey(g.host, f.app);
8898
9842
  const prev = s.apps[key];
8899
- const mark = (path22, was) => f.failed.includes(path22) ? was : {
8900
- path: path22,
9843
+ const mark = (path23, was) => f.failed.includes(path23) ? was : {
9844
+ path: path23,
8901
9845
  offset: 0,
8902
9846
  ...was?.stamp ? { stamp: was.stamp } : {}
8903
9847
  };
@@ -8915,15 +9859,15 @@ async function clearLogs(fleet, names, file = storePath(fleet.manifest.path), no
8915
9859
  const ck = countedClearKey(key);
8916
9860
  next.clears[ck] = (s.clears[ck] ?? 0) + 1;
8917
9861
  }
8918
- for (const [stream, path22] of [
9862
+ for (const [stream, path23] of [
8919
9863
  ["out", f.proc.out_log],
8920
9864
  ["err", f.proc.err_log]
8921
9865
  ]) {
8922
- if (f.failed.includes(path22)) continue;
8923
- const fk = fileClearKey(key, stream, path22);
9866
+ if (f.failed.includes(path23)) continue;
9867
+ const fk = fileClearKey(key, stream, path23);
8924
9868
  next.clears[fk] = (s.clears[fk] ?? 0) + 1;
8925
9869
  }
8926
- const freed = (path22, bytes) => f.failed.includes(path22) ? 0 : bytes;
9870
+ const freed = (path23, bytes) => f.failed.includes(path23) ? 0 : bytes;
8927
9871
  out.push({
8928
9872
  app: f.app,
8929
9873
  host: g.host,
@@ -9169,22 +10113,22 @@ async function doctor(m, hostName, spawner, hereCheck) {
9169
10113
  }
9170
10114
 
9171
10115
  // src/core/init.ts
9172
- import fs16 from "fs";
9173
- import path20 from "path";
10116
+ import fs17 from "fs";
10117
+ import path21 from "path";
9174
10118
  import { parse as parseYaml3 } from "yaml";
9175
10119
 
9176
10120
  // src/core/turbo.ts
9177
- import fs15 from "fs";
9178
- import path19 from "path";
10121
+ import fs16 from "fs";
10122
+ import path20 from "path";
9179
10123
  function readJson2(p) {
9180
10124
  try {
9181
- return JSON.parse(fs15.readFileSync(p, "utf8"));
10125
+ return JSON.parse(fs16.readFileSync(p, "utf8"));
9182
10126
  } catch {
9183
10127
  return null;
9184
10128
  }
9185
10129
  }
9186
10130
  function readTurbo(root) {
9187
- const raw = readJson2(path19.join(root, "turbo.json"));
10131
+ const raw = readJson2(path20.join(root, "turbo.json"));
9188
10132
  const tasks = raw?.tasks ?? raw?.pipeline;
9189
10133
  if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) return null;
9190
10134
  const names = Object.keys(tasks);
@@ -9205,14 +10149,14 @@ function isDistLib(pkg2) {
9205
10149
  function loadWorkspace(root, dirs) {
9206
10150
  const out = [];
9207
10151
  for (const dir of dirs) {
9208
- const pkg2 = readJson2(path19.join(dir, "package.json"));
10152
+ const pkg2 = readJson2(path20.join(dir, "package.json"));
9209
10153
  if (!pkg2) continue;
9210
10154
  const deps2 = /* @__PURE__ */ new Set();
9211
10155
  for (const k of ["dependencies", "devDependencies", "peerDependencies"])
9212
10156
  for (const d of Object.keys(pkg2[k] ?? {})) deps2.add(d);
9213
10157
  out.push({
9214
- name: String(pkg2.name ?? path19.basename(dir)),
9215
- dir: path19.relative(root, dir) || ".",
10158
+ name: String(pkg2.name ?? path20.basename(dir)),
10159
+ dir: path20.relative(root, dir) || ".",
9216
10160
  scripts: { ...pkg2.scripts ?? {} },
9217
10161
  deps: [...deps2],
9218
10162
  distLib: isDistLib(pkg2)
@@ -9245,11 +10189,11 @@ function turboBin(pm) {
9245
10189
  function libsFilters(libs, globParents, all = []) {
9246
10190
  const folded = new Set(libs.map((l) => l.name));
9247
10191
  const shared = new Set(
9248
- 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))
9249
10193
  );
9250
10194
  const out = [];
9251
10195
  for (const l of libs) {
9252
- const parent = path19.dirname(l.dir);
10196
+ const parent = path20.dirname(l.dir);
9253
10197
  const f = globParents.includes(parent) && !shared.has(parent) ? `--filter='./${parent}/*'` : `--filter=${l.name}`;
9254
10198
  if (!out.includes(f)) out.push(f);
9255
10199
  }
@@ -9276,9 +10220,9 @@ function sanitizeName(s) {
9276
10220
  return n || "app";
9277
10221
  }
9278
10222
  function pmFor(root) {
9279
- if (fs16.existsSync(path20.join(root, "yarn.lock"))) return "yarn";
9280
- if (fs16.existsSync(path20.join(root, "pnpm-lock.yaml"))) return "pnpm";
9281
- 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")))
9282
10226
  return "bun run";
9283
10227
  return "npm run";
9284
10228
  }
@@ -9286,12 +10230,12 @@ function walk(dir, maxDepth, depth = 0) {
9286
10230
  const out = [];
9287
10231
  let entries;
9288
10232
  try {
9289
- entries = fs16.readdirSync(dir, { withFileTypes: true });
10233
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
9290
10234
  } catch {
9291
10235
  return out;
9292
10236
  }
9293
10237
  for (const e of entries) {
9294
- const p = path20.join(dir, e.name);
10238
+ const p = path21.join(dir, e.name);
9295
10239
  if (e.isDirectory()) {
9296
10240
  if (SKIP_DIRS.has(e.name) || e.name.startsWith(".")) continue;
9297
10241
  if (depth < maxDepth) out.push(...walk(p, maxDepth, depth + 1));
@@ -9372,7 +10316,7 @@ function vitePort(dir, section2, config) {
9372
10316
  let raw;
9373
10317
  for (const f of config ? [config] : VITE_CONFIGS) {
9374
10318
  try {
9375
- raw = fs16.readFileSync(path20.resolve(dir, f), "utf8");
10319
+ raw = fs17.readFileSync(path21.resolve(dir, f), "utf8");
9376
10320
  break;
9377
10321
  } catch {
9378
10322
  }
@@ -9396,9 +10340,9 @@ function guessPort(cmd, dir) {
9396
10340
  const m = /(?:\bPORT=|--port[= ]|\s-p\s)(\d{2,5})\b/.exec(cmd);
9397
10341
  if (m) return Number(m[1]);
9398
10342
  for (const f of [".env", ".env.local", ".env.development"]) {
9399
- const p = path20.join(dir, f);
9400
- if (!fs16.existsSync(p)) continue;
9401
- 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"));
9402
10346
  if (e) return Number(e[1]);
9403
10347
  }
9404
10348
  if (NEXT_RE.test(cmd)) return 3e3;
@@ -9409,10 +10353,10 @@ function guessPort(cmd, dir) {
9409
10353
  const p = vitePort(dir, section2, config);
9410
10354
  return p === null ? void 0 : p ?? (section2 === "server" ? 5173 : 4173);
9411
10355
  }
9412
- const src = path20.join(dir, "src");
9413
- if (fs16.existsSync(src)) {
10356
+ const src = path21.join(dir, "src");
10357
+ if (fs17.existsSync(src)) {
9414
10358
  for (const f of walk(src, 3).filter((x) => /\.(ts|js|mjs|cjs|tsx|jsx)$/.test(x)).slice(0, 200)) {
9415
- const text = fs16.readFileSync(f, "utf8");
10359
+ const text = fs17.readFileSync(f, "utf8");
9416
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(
9417
10361
  text
9418
10362
  ) ?? /\bPORT\s*\)?\s*(?:\?\?|\|\|)\s*["']?(\d{2,5})\b/.exec(text);
@@ -9435,10 +10379,10 @@ var notAwaited = (lib) => {
9435
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`;
9436
10380
  };
9437
10381
  function packageCandidates(root, pkgDir, pm, turbo) {
9438
- const pkg2 = readJson2(path20.join(pkgDir, "package.json"));
10382
+ const pkg2 = readJson2(path21.join(pkgDir, "package.json"));
9439
10383
  if (!pkg2?.scripts) return [];
9440
- const rel = path20.relative(root, pkgDir) || ".";
9441
- 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)));
9442
10386
  const prefix = rel === "." ? "" : `${appName}-`;
9443
10387
  const file = `${rel === "." ? "" : `${rel}/`}package.json`;
9444
10388
  const out = [];
@@ -9481,21 +10425,21 @@ function expandWorkspaces(root, patterns) {
9481
10425
  for (const pat of patterns) {
9482
10426
  const glob = /^(.*?)\/\*\*?$/.exec(pat);
9483
10427
  if (glob) {
9484
- const base = path20.join(root, glob[1]);
9485
- globParents.push(path20.relative(root, base));
10428
+ const base = path21.join(root, glob[1]);
10429
+ globParents.push(path21.relative(root, base));
9486
10430
  let names = [];
9487
10431
  try {
9488
- names = fs16.readdirSync(base);
10432
+ names = fs17.readdirSync(base);
9489
10433
  } catch {
9490
10434
  }
9491
10435
  names.sort();
9492
10436
  for (const d of names)
9493
- if (fs16.existsSync(path20.join(base, d, "package.json")))
9494
- dirs.add(path20.join(base, d));
10437
+ if (fs17.existsSync(path21.join(base, d, "package.json")))
10438
+ dirs.add(path21.join(base, d));
9495
10439
  } else if (pat.includes("*")) {
9496
10440
  unsupported.push(pat);
9497
- } else if (fs16.existsSync(path20.join(root, pat, "package.json"))) {
9498
- dirs.add(path20.join(root, pat));
10441
+ } else if (fs17.existsSync(path21.join(root, pat, "package.json"))) {
10442
+ dirs.add(path21.join(root, pat));
9499
10443
  }
9500
10444
  }
9501
10445
  return { dirs: [...dirs], unsupported, globParents };
@@ -9504,7 +10448,7 @@ function libsBuilders(folded, all, globParents, pm, version) {
9504
10448
  const bin = turboBin(pm);
9505
10449
  const ui = version === 2 ? " --ui=stream" : "";
9506
10450
  const filters = libsFilters(folded, globParents, all).join(" ");
9507
- 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(
9508
10452
  ", "
9509
10453
  );
9510
10454
  const libs = {
@@ -9563,12 +10507,12 @@ function discover(root) {
9563
10507
  const candidates = [];
9564
10508
  const notes = [];
9565
10509
  const pm = pmFor(root);
9566
- const pkg2 = readJson2(path20.join(root, "package.json"));
10510
+ const pkg2 = readJson2(path21.join(root, "package.json"));
9567
10511
  const ws = Array.isArray(pkg2?.workspaces) ? [...pkg2.workspaces] : Array.isArray(pkg2?.workspaces?.packages) ? [...pkg2.workspaces.packages] : [];
9568
- const pnpmWs = path20.join(root, "pnpm-workspace.yaml");
9569
- if (fs16.existsSync(pnpmWs)) {
10512
+ const pnpmWs = path21.join(root, "pnpm-workspace.yaml");
10513
+ if (fs17.existsSync(pnpmWs)) {
9570
10514
  try {
9571
- const y = parseYaml3(fs16.readFileSync(pnpmWs, "utf8"));
10515
+ const y = parseYaml3(fs17.readFileSync(pnpmWs, "utf8"));
9572
10516
  if (Array.isArray(y?.packages)) ws.push(...y.packages);
9573
10517
  } catch {
9574
10518
  notes.push("pnpm-workspace.yaml could not be parsed");
@@ -9576,8 +10520,8 @@ function discover(root) {
9576
10520
  }
9577
10521
  const expanded = expandWorkspaces(root, ws);
9578
10522
  const turbo = readTurbo(root);
9579
- const turboFile = path20.join(root, "turbo.json");
9580
- 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)
9581
10525
  notes.push(
9582
10526
  "turbo.json could not be parsed (comments are not read); drafted without turbo's tasks"
9583
10527
  );
@@ -9603,7 +10547,7 @@ function discover(root) {
9603
10547
  }
9604
10548
  candidates.push(...packageCandidates(root, root, pm));
9605
10549
  for (const dir of expanded.dirs) {
9606
- 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;
9607
10551
  candidates.push(
9608
10552
  ...packageCandidates(
9609
10553
  root,
@@ -9624,20 +10568,20 @@ function discover(root) {
9624
10568
  `workspace pattern "${pat}" is not expanded by init; add its packages' scripts by hand`
9625
10569
  );
9626
10570
  for (const f of walk(root, 4).filter(
9627
- (p) => path20.basename(p) === "project.json"
10571
+ (p) => path21.basename(p) === "project.json"
9628
10572
  )) {
9629
10573
  const pj = readJson2(f);
9630
10574
  if (!pj?.targets) continue;
9631
- const name2 = String(pj.name ?? path20.basename(path20.dirname(f)));
10575
+ const name2 = String(pj.name ?? path21.basename(path21.dirname(f)));
9632
10576
  for (const t of ["serve", "dev", "build"]) {
9633
10577
  if (!pj.targets[t]) continue;
9634
10578
  candidates.push({
9635
10579
  name: sanitizeName(`${name2}-${t}`),
9636
- source: `${path20.relative(root, f)}:${t}`,
10580
+ source: `${path21.relative(root, f)}:${t}`,
9637
10581
  run: `npx nx run ${name2}:${t}${t === "build" ? " --watch" : ""}`,
9638
10582
  cwd: ".",
9639
10583
  kind: t === "build" ? "build" : "server",
9640
- port: t === "build" ? void 0 : guessPort("", path20.dirname(f))
10584
+ port: t === "build" ? void 0 : guessPort("", path21.dirname(f))
9641
10585
  });
9642
10586
  }
9643
10587
  }
@@ -9652,7 +10596,7 @@ function discover(root) {
9652
10596
  kind: guessKind(t, "")
9653
10597
  });
9654
10598
  }
9655
- const eco = readJson2(path20.join(root, "ecosystem.json"));
10599
+ const eco = readJson2(path21.join(root, "ecosystem.json"));
9656
10600
  if (Array.isArray(eco?.apps)) {
9657
10601
  for (const a of eco.apps) {
9658
10602
  if (!a?.name) continue;
@@ -9673,14 +10617,14 @@ function discover(root) {
9673
10617
  "ecosystem.config.cjs",
9674
10618
  "ecosystem.config.mjs"
9675
10619
  ]) {
9676
- if (fs16.existsSync(path20.join(root, f)))
10620
+ if (fs17.existsSync(path21.join(root, f)))
9677
10621
  notes.push(
9678
10622
  `${f} found: JavaScript ecosystem files are not executed by init; port its apps by hand`
9679
10623
  );
9680
10624
  }
9681
- const procfile = path20.join(root, "Procfile");
9682
- if (fs16.existsSync(procfile)) {
9683
- 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")) {
9684
10628
  const m = /^\s*([A-Za-z0-9_-]+):\s*(.+)$/.exec(line);
9685
10629
  if (m) {
9686
10630
  const kind = guessKind(m[1], m[2]);
@@ -9814,20 +10758,20 @@ function localReadonly(text) {
9814
10758
  }
9815
10759
  }
9816
10760
  function initProject(rootIn, opts = {}) {
9817
- const root = path20.resolve(rootIn);
9818
- if (!fs16.existsSync(root))
10761
+ const root = path21.resolve(rootIn);
10762
+ if (!fs17.existsSync(root))
9819
10763
  throw new McprocError(
9820
10764
  "MANIFEST_NOT_FOUND",
9821
10765
  `Project directory does not exist: ${root}`
9822
10766
  );
9823
- const dir = path20.join(root, ".mcproc");
9824
- const file = path20.join(root, MANIFEST_REL);
9825
- 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) {
9826
10770
  throw new McprocError("ALREADY_EXISTS", `${file} already exists`, {
9827
10771
  hint: "Overwrite the draft with force (README and .gitignore are kept), or keep it and adopt the running apps' ports into it."
9828
10772
  });
9829
10773
  }
9830
- if (fs16.existsSync(file) && localReadonly(fs16.readFileSync(file, "utf8"))) {
10774
+ if (fs17.existsSync(file) && localReadonly(fs17.readFileSync(file, "utf8"))) {
9831
10775
  throw new McprocError(
9832
10776
  "READONLY_HOST",
9833
10777
  `${file} declares the local host readonly; init does not replace it, not even with force`,
@@ -9836,20 +10780,20 @@ function initProject(rootIn, opts = {}) {
9836
10780
  }
9837
10781
  );
9838
10782
  }
9839
- fs16.mkdirSync(dir, { recursive: true });
10783
+ fs17.mkdirSync(dir, { recursive: true });
9840
10784
  const created = [];
9841
- const gi = path20.join(dir, ".gitignore");
9842
- if (!fs16.existsSync(gi) || fs16.readFileSync(gi, "utf8").trim() !== "*") {
9843
- 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");
9844
10788
  created.push(gi);
9845
10789
  }
9846
- const readme = path20.join(dir, "README.md");
9847
- if (!fs16.existsSync(readme)) {
9848
- fs16.writeFileSync(readme, README_TEMPLATE);
10790
+ const readme = path21.join(dir, "README.md");
10791
+ if (!fs17.existsSync(readme)) {
10792
+ fs17.writeFileSync(readme, README_TEMPLATE);
9849
10793
  created.push(readme);
9850
10794
  }
9851
10795
  const d = discover(root);
9852
- fs16.writeFileSync(file, renderDraft(root, defaultProject(root), d));
10796
+ fs17.writeFileSync(file, renderDraft(root, defaultProject(root), d));
9853
10797
  created.push(file);
9854
10798
  const by_source = {};
9855
10799
  for (const c of d.candidates) {
@@ -9871,7 +10815,7 @@ function initProject(rootIn, opts = {}) {
9871
10815
  }
9872
10816
 
9873
10817
  // src/core/adopt.ts
9874
- import fs17 from "fs";
10818
+ import fs18 from "fs";
9875
10819
  import { isMap, isScalar, isSeq, parse, parseDocument } from "yaml";
9876
10820
  var EPHEMERAL_FROM = 32768;
9877
10821
  function planAdoption(rows, m) {
@@ -10154,7 +11098,7 @@ async function adoptPorts(fleet) {
10154
11098
  hint: "Edit the manifest by hand, or remove hosts.local.readonly first."
10155
11099
  }
10156
11100
  );
10157
- const before = fs17.readFileSync(m.path, "utf8");
11101
+ const before = fs18.readFileSync(m.path, "utf8");
10158
11102
  const current = parseManifest(before, m.root, m.path, loadOverride(m.root));
10159
11103
  const { rows, errors } = await listFleet(fleet, fleet.select(["all"]), {
10160
11104
  cpu: "none",
@@ -10184,13 +11128,13 @@ async function adoptPorts(fleet) {
10184
11128
  }
10185
11129
  );
10186
11130
  }
10187
- if (fs17.readFileSync(m.path, "utf8") !== before)
11131
+ if (fs18.readFileSync(m.path, "utf8") !== before)
10188
11132
  throw new McprocError(
10189
11133
  "ALREADY_EXISTS",
10190
11134
  `${m.path} changed while the ports were being probed; nothing written`,
10191
11135
  { hint: "Run the adoption again." }
10192
11136
  );
10193
- fs17.writeFileSync(m.path, splice.text);
11137
+ fs18.writeFileSync(m.path, splice.text);
10194
11138
  return {
10195
11139
  path: m.path,
10196
11140
  written: true,
@@ -10647,6 +11591,28 @@ function registerReadTools(server, deps2) {
10647
11591
  };
10648
11592
  })
10649
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
+ );
10650
11616
  server.registerTool(
10651
11617
  "incidents",
10652
11618
  {
@@ -11180,7 +12146,8 @@ function initialState(p) {
11180
12146
  incidentsCleared: null,
11181
12147
  incidentsErrors: [],
11182
12148
  incidentsTaken: {},
11183
- hideTaken: false
12149
+ hideTaken: false,
12150
+ toStacks: p.toStacks ?? false
11184
12151
  };
11185
12152
  }
11186
12153
  function visibleIncidents(s) {
@@ -11416,8 +12383,9 @@ function keyIntent(key, s, now = Date.now()) {
11416
12383
  }
11417
12384
  case "q":
11418
12385
  case "C-c":
11419
- case "escape":
11420
12386
  return { kind: "quit" };
12387
+ case "escape":
12388
+ return s.toStacks ? { kind: "back" } : { kind: "quit" };
11421
12389
  default:
11422
12390
  return null;
11423
12391
  }
@@ -11459,126 +12427,8 @@ function flatLineOf(app, l) {
11459
12427
  }
11460
12428
  var flatMemo = null;
11461
12429
 
11462
- // src/monit/screentext.ts
11463
- import { charWidth, chars } from "@pm2/blessed/lib/unicode.js";
11464
- var segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
11465
- var EMOJI_PRESENTATION = new RegExp("\\p{Emoji_Presentation}", "u");
11466
- var PICTOGRAPH = new RegExp("\\p{Extended_Pictographic}", "u");
11467
- var REGIONAL = new RegExp("\\p{Regional_Indicator}", "u");
11468
- var KEYCAP = /^[0-9#*]\uFE0F?\u20E3$/;
11469
- var INVISIBLE = /[\p{Cc}\p{Cf}]/u;
11470
- var LINE_BREAK = /\r\n|[\n\v\f\u0085\u2028\u2029]/g;
11471
- var MARK2 = new RegExp("\\p{M}", "u");
11472
- var SPACING_MARK = new RegExp("\\p{Mc}", "u");
11473
- var BLESSED_WIDE = new RegExp(chars.wide.source);
11474
- var NARROW = {
11475
- "\u2705": "\u2714",
11476
- "\u274C": "\u2716",
11477
- "\u274E": "\u2716",
11478
- "\u2757": "!",
11479
- "\u2755": "!",
11480
- "\u2753": "?",
11481
- "\u2754": "?"
11482
- };
11483
- var TAB = 8;
11484
- function displayWidth(s) {
11485
- let w = 0;
11486
- for (const ch of s.includes("\x1B") ? s.replace(SGR_RE, "") : s)
11487
- w += charWidth(ch.codePointAt(0)) === 2 ? 2 : 1;
11488
- return w;
11489
- }
11490
- function columns(s) {
11491
- let w = 0;
11492
- for (const ch of s) w += charWidth(ch.codePointAt(0));
11493
- return w;
11494
- }
11495
- function keepMark(ch, cp) {
11496
- return cp <= 65535 && charWidth(cp) === (SPACING_MARK.test(ch) ? 1 : 0);
11497
- }
11498
- function cluster(c) {
11499
- if (c === " ") return c;
11500
- const first = String.fromCodePoint(c.codePointAt(0));
11501
- if (REGIONAL.test(first))
11502
- return [...c].filter((ch) => REGIONAL.test(ch)).map((ch) => String.fromCharCode(ch.codePointAt(0) - 127462 + 65)).join("");
11503
- if (KEYCAP.test(c)) return c[0];
11504
- if (PICTOGRAPH.test(first) || EMOJI_PRESENTATION.test(first)) {
11505
- if (NARROW[first]) return NARROW[first];
11506
- return !EMOJI_PRESENTATION.test(first) && first.length === 1 ? first : "?";
11507
- }
11508
- let out = "";
11509
- for (const ch of c) {
11510
- if (INVISIBLE.test(ch)) continue;
11511
- const cp = ch.codePointAt(0);
11512
- if (MARK2.test(ch)) {
11513
- if (keepMark(ch, cp)) out += ch;
11514
- continue;
11515
- }
11516
- if (cp > 65535 || cp >= 55296 && cp <= 57343) {
11517
- out += "?";
11518
- continue;
11519
- }
11520
- out += charWidth(cp) === 2 === BLESSED_WIDE.test(ch) ? ch : "?";
11521
- }
11522
- return out;
11523
- }
11524
- function blessedSgr(code) {
11525
- if (!code.includes(":")) return code;
11526
- const groups = code.slice(2, -1).split(";").flatMap((g) => {
11527
- if (!g.includes(":")) return [g];
11528
- const [kind, mode, ...rest] = g.split(":");
11529
- if (kind === "4") return ["4"];
11530
- if (!["38", "48", "58"].includes(kind)) return [];
11531
- if (mode === "5" && rest[0]) return [`${kind};5;${rest[0]}`];
11532
- if (mode === "2" && rest.length >= 3)
11533
- return [`${kind};2;${rest.slice(-3).join(";")}`];
11534
- return [];
11535
- });
11536
- return groups.length ? `\x1B[${groups.join(";")}m` : "";
11537
- }
11538
- var SGR_SPLIT = /(\x1b\[[0-9;:]*m)/;
11539
- var visible = (t) => t.replace(SGR_RE, "");
11540
- function screenText(input, opts = {}) {
11541
- const parts = (opts.colors ? keepSgr(input) : stripAnsi(input)).replace(/\r\n/g, "\n").split("\r");
11542
- let last = parts.length - 1;
11543
- while (last > 0 && visible(parts[last]) === "") last--;
11544
- const carried = parts.slice(0, last).flatMap((p) => p.match(SGR_RE) ?? []).join("");
11545
- let out = "";
11546
- let col = 0;
11547
- for (const piece of (carried + parts[last]).split(SGR_SPLIT)) {
11548
- if (SGR_SPLIT.test(piece) && piece.startsWith("\x1B")) {
11549
- out += blessedSgr(piece);
11550
- continue;
11551
- }
11552
- for (const { segment } of segmenter.segment(
11553
- piece.replace(LINE_BREAK, " ").normalize("NFC")
11554
- )) {
11555
- const c = cluster(segment);
11556
- if (c === " ") {
11557
- const n = TAB - col % TAB;
11558
- out += " ".repeat(n);
11559
- col += n;
11560
- continue;
11561
- }
11562
- out += c;
11563
- col += columns(c);
11564
- }
11565
- }
11566
- return out;
11567
- }
11568
- function cutWidth(s, width) {
11569
- if (displayWidth(s) <= width) return s;
11570
- let out = "";
11571
- let w = 0;
11572
- for (const ch of s) {
11573
- const cw = displayWidth(ch);
11574
- if (w + cw > width - 1) break;
11575
- out += ch;
11576
- w += cw;
11577
- }
11578
- return `${out}\u2026`;
11579
- }
11580
-
11581
12430
  // src/monit/frame.ts
12431
+ var stacksKey = (s) => s.toStacks ? "esc stacks " : "";
11582
12432
  function pane(entries, scroll, shift, logH, rowsOf) {
11583
12433
  let filled = 0;
11584
12434
  let k = 0;
@@ -11813,7 +12663,7 @@ function renderFrame(s, size) {
11813
12663
  spec2.rowsOf
11814
12664
  );
11815
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`;
11816
- 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`;
11817
12667
  return {
11818
12668
  header,
11819
12669
  table: [],
@@ -11836,7 +12686,7 @@ function renderFrame(s, size) {
11836
12686
  spec2.logH,
11837
12687
  spec2.rowsOf
11838
12688
  );
11839
- 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`;
11840
12690
  return {
11841
12691
  header,
11842
12692
  table: [],
@@ -11848,32 +12698,37 @@ function renderFrame(s, size) {
11848
12698
  };
11849
12699
  }
11850
12700
  const multi = new Set(s.rows.map((r2) => r2.host)).size > 1;
11851
- const tableLines = table([
12701
+ const o = multi ? 1 : 0;
12702
+ const tableLines = table(
11852
12703
  [
11853
- "",
11854
- "APP",
11855
- ...multi ? ["HOST"] : [],
11856
- "STATUS",
11857
- "UP",
11858
- "MEM",
11859
- "CPU",
11860
- "PORTS",
11861
- "URL",
11862
- "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
+ ])
11863
12728
  ],
11864
- ...s.rows.map((r2, i) => [
11865
- i === s.selected ? "\u25B6" : " ",
11866
- r2.app,
11867
- ...multi ? [r2.host] : [],
11868
- r2.status,
11869
- fmtUptime(r2.uptime_ms),
11870
- r2.memory ? fmtBytes(r2.memory) : "-",
11871
- r2.status === "online" ? fmtCpu(r2.cpu) : "-",
11872
- cutWidth(screenText(fmtPorts(r2.ports)), 22),
11873
- cutWidth(screenText(fmtUrl(r2)), 36),
11874
- r2.runtime ? cutWidth(screenText(r2.runtime), 40) : "-"
11875
- ])
11876
- ]).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));
11877
12732
  const spec = paneSpec(s, size);
11878
12733
  const p = pane(
11879
12734
  spec.entries,
@@ -11905,7 +12760,7 @@ function renderFrame(s, size) {
11905
12760
  w
11906
12761
  )
11907
12762
  ] : ["no app selected"];
11908
- 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`;
11909
12764
  const footer = cutWidth(right ? `${right} \u2502 ${keys}` : keys, w);
11910
12765
  return {
11911
12766
  header,
@@ -12010,31 +12865,31 @@ async function monitOnce(opts, deps2 = {}) {
12010
12865
  }
12011
12866
 
12012
12867
  // src/cli/install.ts
12013
- import fs18 from "fs";
12014
- import os6 from "os";
12015
- import path21 from "path";
12868
+ import fs19 from "fs";
12869
+ import os9 from "os";
12870
+ import path22 from "path";
12016
12871
  var LAUNCHER_MARK = "# written by mcproc install";
12017
12872
  function rcFor(home, shell) {
12018
- const name2 = path21.basename(shell || "");
12019
- return path21.join(
12873
+ const name2 = path22.basename(shell || "");
12874
+ return path22.join(
12020
12875
  home,
12021
12876
  name2 === "zsh" ? ".zshrc" : name2 === "bash" ? ".bashrc" : ".profile"
12022
12877
  );
12023
12878
  }
12024
12879
  function installShortcut(o = {}) {
12025
- const home = o.home ?? os6.homedir();
12026
- const binDir = path21.resolve(o.binDir ?? path21.join(home, ".local", "bin"));
12027
- const cli = path21.resolve(
12028
- 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")
12029
12884
  );
12030
- const launcher = path21.join(binDir, "mcproc");
12885
+ const launcher = path22.join(binDir, "mcproc");
12031
12886
  const pathEnv = o.pathEnv ?? process.env.PATH ?? "";
12032
- 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);
12033
12888
  const next = [];
12034
12889
  if (o.uninstall) {
12035
- const exists = fs18.existsSync(launcher);
12036
- const ours = exists && fs18.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK);
12037
- 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);
12038
12893
  else if (exists)
12039
12894
  next.push(`${launcher} was not written by mcproc install; left in place`);
12040
12895
  else next.push(`nothing at ${launcher}`);
@@ -12050,43 +12905,43 @@ function installShortcut(o = {}) {
12050
12905
  next
12051
12906
  };
12052
12907
  }
12053
- fs18.mkdirSync(binDir, { recursive: true });
12908
+ fs19.mkdirSync(binDir, { recursive: true });
12054
12909
  let existing = null;
12055
12910
  try {
12056
- existing = fs18.lstatSync(launcher);
12911
+ existing = fs19.lstatSync(launcher);
12057
12912
  } catch {
12058
12913
  }
12059
12914
  if (existing?.isSymbolicLink()) {
12060
12915
  throw new McprocError(
12061
12916
  "BAD_ARGUMENT",
12062
- `${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`,
12063
12918
  { hint: "Remove it yourself, or pass another --bin-dir." }
12064
12919
  );
12065
12920
  }
12066
- if (existing && !fs18.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK)) {
12921
+ if (existing && !fs19.readFileSync(launcher, "utf8").includes(LAUNCHER_MARK)) {
12067
12922
  throw new McprocError(
12068
12923
  "BAD_ARGUMENT",
12069
12924
  `${launcher} exists and was not written by mcproc install; refusing to overwrite it`,
12070
12925
  { hint: "Remove it yourself, or pass another --bin-dir." }
12071
12926
  );
12072
12927
  }
12073
- fs18.writeFileSync(
12928
+ fs19.writeFileSync(
12074
12929
  launcher,
12075
12930
  `#!/bin/sh
12076
- ${LAUNCHER_MARK}: runs the mcproc at ${path21.dirname(path21.dirname(cli))}
12931
+ ${LAUNCHER_MARK}: runs the mcproc at ${path22.dirname(path22.dirname(cli))}
12077
12932
  exec node ${shq(cli)} "$@"
12078
12933
  `
12079
12934
  );
12080
- fs18.chmodSync(launcher, 493);
12935
+ fs19.chmodSync(launcher, 493);
12081
12936
  let rcFile = null;
12082
12937
  let rcUpdated = false;
12083
12938
  if (!onPath) {
12084
12939
  rcFile = rcFor(home, o.shell ?? process.env.SHELL ?? "");
12085
- 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;
12086
12941
  const line = `export PATH="${shown}:$PATH"`;
12087
- const current = fs18.existsSync(rcFile) ? fs18.readFileSync(rcFile, "utf8") : "";
12942
+ const current = fs19.existsSync(rcFile) ? fs19.readFileSync(rcFile, "utf8") : "";
12088
12943
  if (!current.includes(line)) {
12089
- fs18.appendFileSync(
12944
+ fs19.appendFileSync(
12090
12945
  rcFile,
12091
12946
  `${current === "" || current.endsWith("\n") ? "" : "\n"}
12092
12947
  # mcproc install: the shortcut lives in ${shown}
@@ -12194,6 +13049,7 @@ var MonitLoop = class {
12194
13049
  inc = freshView();
12195
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. */
12196
13051
  refresh(hosts) {
13052
+ if (this.closed) return Promise.resolve();
12197
13053
  this.readCleared();
12198
13054
  this.syncWatches();
12199
13055
  const gen = this.gen;
@@ -12303,6 +13159,7 @@ var MonitLoop = class {
12303
13159
  const probed = this.losses;
12304
13160
  try {
12305
13161
  const ctx = await this.fleet.ctx(g.host);
13162
+ if (gen !== this.gen) return;
12306
13163
  const snap = await this.deps.snapshot(ctx, {
12307
13164
  cpu: "none",
12308
13165
  prev: this.prev.get(g.host),
@@ -12512,6 +13369,8 @@ var MonitLoop = class {
12512
13369
  /** The screen is going away: no follower wakes it any more, and a running reload stops waiting for dependencies. */
12513
13370
  close() {
12514
13371
  this.closed = true;
13372
+ this.gen++;
13373
+ this.switchSeq++;
12515
13374
  this.reloading?.abort();
12516
13375
  for (const q of this.wakes.values()) {
12517
13376
  if (q.timer) clearTimeout(q.timer);
@@ -12665,10 +13524,10 @@ var MonitLoop = class {
12665
13524
  this.names.map((a) => markKey(this.fleet.hostOf(a), a))
12666
13525
  );
12667
13526
  const st = this.inc;
12668
- const add2 = (key, stream, path22) => {
13527
+ const add2 = (key, stream, path23) => {
12669
13528
  const by = st.pending[key] ?? {};
12670
13529
  const paths = by[stream] ?? /* @__PURE__ */ new Set();
12671
- paths.add(path22);
13530
+ paths.add(path23);
12672
13531
  by[stream] = paths;
12673
13532
  st.pending[key] = by;
12674
13533
  };
@@ -12811,9 +13670,11 @@ async function runMonit(opts) {
12811
13670
  project: manifest.project,
12812
13671
  host: viewLabel(fleet.requested),
12813
13672
  hosts: views.map(viewLabel),
12814
- apps: appInfos(manifest)
13673
+ apps: appInfos(manifest),
13674
+ toStacks: Boolean(opts.back)
12815
13675
  });
12816
- const screen = blessed.screen({
13676
+ const own2 = !opts.screen;
13677
+ const screen = opts.screen ?? blessed.screen({
12817
13678
  smartCSR: true,
12818
13679
  title: "mcproc monit",
12819
13680
  fullUnicode: true
@@ -12832,7 +13693,7 @@ async function runMonit(opts) {
12832
13693
  const detailsTitle = box({ bottom: 7, height: 1 });
12833
13694
  const detailsBox = box({ bottom: 1, height: 6 });
12834
13695
  const footer = box({ bottom: 0, height: 1 });
12835
- for (const b of [
13696
+ const boxes = [
12836
13697
  header,
12837
13698
  appsTitle,
12838
13699
  tableBox,
@@ -12841,8 +13702,8 @@ async function runMonit(opts) {
12841
13702
  detailsTitle,
12842
13703
  detailsBox,
12843
13704
  footer
12844
- ])
12845
- screen.append(b);
13705
+ ];
13706
+ for (const b of boxes) screen.append(b);
12846
13707
  const appColor = new Map(
12847
13708
  Object.keys(manifest.apps).map((a, i) => [a, colorFor(i)])
12848
13709
  );
@@ -12855,7 +13716,9 @@ async function runMonit(opts) {
12855
13716
  width: screen.width,
12856
13717
  height: screen.height
12857
13718
  });
13719
+ let left = false;
12858
13720
  const show = (f) => {
13721
+ if (left) return;
12859
13722
  const { width: w, height: h } = size();
12860
13723
  header.setContent(f.header);
12861
13724
  footer.setContent(f.footer);
@@ -12904,44 +13767,511 @@ async function runMonit(opts) {
12904
13767
  const timer = setInterval(() => {
12905
13768
  void loop.refresh();
12906
13769
  }, opts.intervalMs);
12907
- const quit = () => {
13770
+ const leave = (why) => {
13771
+ left = true;
12908
13772
  clearInterval(timer);
12909
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);
12910
14207
  screen.destroy();
12911
14208
  resolve();
12912
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
+ };
12913
14265
  screen.on(
12914
14266
  "keypress",
12915
14267
  (_ch, key) => {
12916
- const intent = keyIntent(key.full ?? key.name ?? "", state);
12917
- if (state.armedAt !== null && !(intent?.kind === "action" && intent.action.type === "arm"))
12918
- dispatch({ type: "arm", at: null });
12919
- if (!intent) return;
12920
- if (intent.kind === "quit") return quit();
12921
- if (intent.kind === "action") {
12922
- if (intent.action.type === "arm" && intent.action.at !== null) {
12923
- const { at, what } = intent.action;
12924
- setTimeout(() => {
12925
- if (state.armedAt === at && state.armed === what)
12926
- dispatch({ type: "arm", at: null });
12927
- }, CONFIRM_MS + 50).unref();
12928
- }
12929
- if (intent.action.type === "select")
12930
- loop.dispatchSelect(intent.action.delta);
12931
- else {
12932
- dispatch(intent.action);
12933
- if (intent.action.type === "view") void loop.refresh();
12934
- }
12935
- return;
12936
- }
12937
- if (intent.kind === "host-next") return void loop.nextView();
12938
- if (intent.kind === "clear") return void loop.runClear(intent.apps);
12939
- if (intent.kind === "reload-all") return void loop.reloadAll();
12940
- void loop.runCommand(intent.cmd, intent.app);
14268
+ if (!active) return;
14269
+ void onKey(key.full ?? key.name ?? "");
12941
14270
  }
12942
14271
  );
12943
14272
  screen.on("resize", paint);
12944
- void loop.refresh().then(paint);
14273
+ paint();
14274
+ void refresh();
12945
14275
  });
12946
14276
  }
12947
14277
 
@@ -12950,7 +14280,8 @@ var defaultIo = {
12950
14280
  out: (s) => process.stdout.write(`${s}
12951
14281
  `),
12952
14282
  err: (s) => process.stderr.write(`${s}
12953
- `)
14283
+ `),
14284
+ columns: () => process.stdout.isTTY ? process.stdout.columns || void 0 : void 0
12954
14285
  };
12955
14286
  async function printSince(fleet, apps, since, how, io, signal) {
12956
14287
  const marks = new LogMarks();
@@ -13002,10 +14333,20 @@ async function printLs(fleet, names, how, io) {
13002
14333
  how.reloads
13003
14334
  );
13004
14335
  io.out(
13005
- 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?.())
13006
14337
  );
13007
14338
  if (errors.length) process.exitCode = 1;
13008
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
+ }
13009
14350
  function buildProgram(io = defaultIo) {
13010
14351
  const program = new Command().name("mcproc").description(
13011
14352
  "pm2 process control for agents: one manifest per project, local and remote hosts, per-app ports"
@@ -13014,6 +14355,7 @@ function buildProgram(io = defaultIo) {
13014
14355
  "project root (default: MCPROC_PROJECT, CLAUDE_PROJECT_DIR, cwd)"
13015
14356
  ).option("--host <name>", "host from the manifest (default: local)").option("--json", "print JSON instead of text");
13016
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?.();
13017
14359
  const wrap = (fn) => async (...all) => {
13018
14360
  const cmd = all[all.length - 1];
13019
14361
  const opts = all[all.length - 2] ?? {};
@@ -13036,7 +14378,13 @@ Hint: ${e.hint}` : ""}`
13036
14378
  const fleet = fleetOf(g);
13037
14379
  const waits = [];
13038
14380
  const rows = await actOn(fleet, target, act, { ...opts, waits });
13039
- 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
+ );
13040
14388
  emit(g, text, { ...hostsOf(rows, fleet.fallback), rows, waits });
13041
14389
  if (failed) process.exitCode = 1;
13042
14390
  };
@@ -13065,7 +14413,7 @@ ${r.next}`, r);
13065
14413
  wrap(async (g) => {
13066
14414
  const { manifest } = prepare({ project: g.project });
13067
14415
  const h = hostsInfo(manifest);
13068
- emit(g, renderHosts(h), h);
14416
+ emit(g, renderHosts(h, width(g)), h);
13069
14417
  })
13070
14418
  );
13071
14419
  program.command("doctor").description(
@@ -13098,7 +14446,7 @@ ${r.next}`, r);
13098
14446
  const fleet = fleetOf(g);
13099
14447
  if (!app) {
13100
14448
  const d2 = describeProject(fleet.manifest);
13101
- emit(g, renderProject(d2), d2);
14449
+ emit(g, renderProject(d2, width(g)), d2);
13102
14450
  return;
13103
14451
  }
13104
14452
  const d = await describeFleetApp(fleet, app);
@@ -13108,7 +14456,7 @@ ${r.next}`, r);
13108
14456
  `${d.app.name} [${d.app.kind}]: ${d.app.description}`,
13109
14457
  `run: ${d.app.run}`,
13110
14458
  `cwd: ${d.app.cwd}`,
13111
- renderLs([d.row], d.host),
14459
+ renderLs([d.row], d.host, [], width(g)),
13112
14460
  urlNote(d.row),
13113
14461
  `logs: ${d.logs ? `${d.logs.out} | ${d.logs.err}` : "not started yet"}`
13114
14462
  ].filter(Boolean).join("\n"),
@@ -13310,7 +14658,7 @@ ${r.next}`, r);
13310
14658
  wrap(async (g, _o, [port]) => {
13311
14659
  const ctx = await ctxOf(g);
13312
14660
  const r = await portsReport(ctx, port ? Number(port) : void 0);
13313
- emit(g, renderPorts(r, ctx.host.name), r);
14661
+ emit(g, renderPorts(r, ctx.host.name, width(g)), r);
13314
14662
  })
13315
14663
  );
13316
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(
@@ -13336,7 +14684,7 @@ ${r.next}`, r);
13336
14684
  ctx = void 0;
13337
14685
  }
13338
14686
  const v = await leasesView(void 0, ctx);
13339
- emit(g, renderLeases(v), v);
14687
+ emit(g, renderLeases(v, width(g)), v);
13340
14688
  })
13341
14689
  );
13342
14690
  leases.command("cancel <id>").description("cancel a pending lease").action(
@@ -13345,6 +14693,22 @@ ${r.next}`, r);
13345
14693
  emit(g, `cancelled ${l.id} (${l.project} ${l.target})`, l);
13346
14694
  })
13347
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
+ );
13348
14712
  program.command("exec <command...>").description(
13349
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)"
13350
14714
  ).option(
@@ -13451,7 +14815,7 @@ ${r.next}`, r);
13451
14815
  wrap(async (g) => {
13452
14816
  const { manifest, deps: deps2 } = await worktreeCtx(g);
13453
14817
  const rows = await worktreeLs(manifest, deps2);
13454
- emit(g, renderWorktrees(rows), rows);
14818
+ emit(g, renderWorktrees(rows, width(g)), rows);
13455
14819
  })
13456
14820
  );
13457
14821
  worktree.command("remove <ref>").description(
@@ -13476,7 +14840,7 @@ ${r.next}`, r);
13476
14840
  const stop = () => ac.abort();
13477
14841
  process.once("SIGINT", stop);
13478
14842
  process.once("SIGTERM", stop);
13479
- fs19.mkdirSync(deps2.store.dir, { recursive: true });
14843
+ fs20.mkdirSync(deps2.store.dir, { recursive: true });
13480
14844
  deps2.watch = leaseDirWatch(deps2.store.dir);
13481
14845
  const r = await watchLeases(deps2, { signal: ac.signal });
13482
14846
  io.out(`watcher ${r}`);
@@ -13492,25 +14856,36 @@ ${r.next}`, r);
13492
14856
  })
13493
14857
  );
13494
14858
  program.command("monit").description(
13495
- "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"
13496
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(
13497
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
+ }
13498
14884
  if (o.once) {
13499
- io.out(
13500
- await monitOnce({
13501
- project: g.project,
13502
- host: g.host,
13503
- lines: Number(o.lines) || 200
13504
- })
13505
- );
14885
+ io.out(await monitOnce({ project: g.project, host: g.host, lines }));
13506
14886
  return;
13507
14887
  }
13508
- await runMonit({
13509
- project: g.project,
13510
- host: g.host,
13511
- intervalMs: Math.max(500, Number(o.interval) || 2e3),
13512
- lines: Number(o.lines) || 200
13513
- });
14888
+ await runMonit({ project: g.project, host: g.host, intervalMs, lines });
13514
14889
  })
13515
14890
  );
13516
14891
  program.command("unpack").description(