@roulabs/mx 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/bin/mx.js +80 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,7 @@ mx repo add git@github.com:you/app.git # clone a pristine repo into the runtim
30
30
  mx work new my-feature # create a work (prints its folder path)
31
31
  mx work -n my-feature worktree add app # add a worktree on branch my-feature
32
32
  mx work -n my-feature port set app web # allocate a free port (across all works)
33
- mx status # see repos, works, worktrees, ports
33
+ mx info # see repos, works, worktrees, ports
34
34
  ```
35
35
 
36
36
  Inside a work folder or worktree you can drop `-n` — mx infers the work/repo from your cwd. Read commands accept `--porcelain` for stable JSON; errors are `{"error","code"}` with a non-zero exit.
@@ -40,7 +40,7 @@ Inside a work folder or worktree you can drop `-n` — mx infers the work/repo f
40
40
  | command | does |
41
41
  |---|---|
42
42
  | `mx init [path]` | scaffold/adopt a runtime (`repos/`, `works/`, `.mx-root`, `mx.json`, `CLAUDE.md`) |
43
- | `mx status [--all] [--porcelain]` | list repos, works, worktrees, ports |
43
+ | `mx info [--all] [--porcelain]` | list repos, works, worktrees, ports |
44
44
  | `mx sync` | re-stamp the runtime's mx-owned files (`CLAUDE.md`, per-repo/per-work scaffolding) from the current CLI — same-major, non-destructive |
45
45
  | `mx update` | self-update the CLI within its major (`npm i -g`); flags a newer major if one exists |
46
46
  | `mx migrate` | upgrade an older-version runtime to the version this CLI supports (the only command allowed on a version-mismatched runtime) |
package/bin/mx.js CHANGED
@@ -491,7 +491,12 @@ function repoFetch(root, name) {
491
491
  const rp = repoGitDir(root, name);
492
492
  if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
493
493
  git(["-C", rp, "fetch", "--all", "--prune", "--tags"]);
494
+ const current = currentBranch(rp);
494
495
  gitQuiet(["-C", rp, "merge", "--ff-only", "@{u}"]);
496
+ const base = originDefaultBranch(rp);
497
+ if (base && base !== current) {
498
+ gitQuiet(["-C", rp, "fetch", ".", `refs/remotes/origin/${base}:refs/heads/${base}`]);
499
+ }
495
500
  return { name, branch: currentBranch(rp), remoteBranches: remoteBranchList(rp) };
496
501
  }
497
502
  function repoInfo(root, name) {
@@ -919,6 +924,7 @@ function statusRuntime(root, opts = {}) {
919
924
  const works = opts.onlyArchived ? all.filter((w) => w.isArchived === true) : opts.includeArchived ? all : all.filter((w) => w.isArchived !== true);
920
925
  return {
921
926
  runtime: root,
927
+ version: readRuntimeVersion(root),
922
928
  context: { entries: countContextEntries(root) },
923
929
  repos: listReposInfo(root),
924
930
  works,
@@ -1050,7 +1056,7 @@ var HELP = `mx \u2014 control panel for the mx runtime
1050
1056
 
1051
1057
  Global:
1052
1058
  mx init [path] scaffold/adopt a runtime (default ~/mx)
1053
- mx status [--all] [--porcelain] show runtime, repos, works, ports (active only by default; --all to include archived; aliases: mx s, mx st)
1059
+ mx info [--all] [--porcelain] show runtime version, repos, works, ports (active only by default; --all to include archived; alias: mx i)
1054
1060
  mx sync re-stamp runtime files (CLAUDE.md, scaffolding) from current templates \u2014 same-major, non-breaking
1055
1061
  mx update self-update the mx CLI within its major (npm i -g); flags a newer major if one exists
1056
1062
  mx migrate upgrade an older-version runtime to the version this CLI supports (the only command allowed on a mismatched runtime)
@@ -1059,7 +1065,9 @@ Global:
1059
1065
  Repos (pristine clones):
1060
1066
  mx repo add <git-url> [--name <n>] clone a repo into the runtime
1061
1067
  mx repo ls [--porcelain]
1062
- mx repo -n <name> fetch git fetch (+ ff current branch)
1068
+ mx repo -n <name> path print the repo container path (cd "$(mx repo -n <name> path)")
1069
+ mx repo -n <name> fetch git fetch (+ ff the checked-out and base branches)
1070
+ mx repo fetch --all fetch every repo, one by one
1063
1071
  mx repo -n <name> info [--porcelain]
1064
1072
  mx repo health [--porcelain] pure-local health summary for every pristine clone
1065
1073
  mx repo -n <name> health [--porcelain] detailed health for one pristine clone
@@ -1070,6 +1078,7 @@ Works (features):
1070
1078
  mx work ls [--all|--archived] [--porcelain] default: active only; --all includes archived; --archived shows archived only
1071
1079
  mx work -n <name> info [--porcelain]
1072
1080
  mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
1081
+ mx work -n <name> open (or -o) open the work's fullscreen Terminal + editor layout (macOS)
1073
1082
  mx work -n <name> describe <text>
1074
1083
  mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>] [--no-hydrate] runs the repo's hydrate.sh after add unless --no-hydrate
1075
1084
  mx work -n <name> worktree ls [--porcelain]
@@ -1200,7 +1209,7 @@ function runGlobal(positionals, flags) {
1200
1209
  }, res);
1201
1210
  return;
1202
1211
  }
1203
- case "status": {
1212
+ case "info": {
1204
1213
  const root = requireRuntime({ runtime: flags.runtime });
1205
1214
  const data = statusRuntime(root, { includeArchived: flags.all });
1206
1215
  emit(() => renderStatus(data), data);
@@ -1264,7 +1273,7 @@ function renderSelfUpdate(info) {
1264
1273
  }
1265
1274
  function renderStatus(data) {
1266
1275
  console.log();
1267
- console.log(` ${bold("mx")} ${dim("\xB7")} ${data.runtime}`);
1276
+ console.log(` ${bold("mx")} ${dim(`v${data.version}`)} ${dim("\xB7")} ${data.runtime}`);
1268
1277
  console.log();
1269
1278
  console.log(` ${bold("context")} ${dim(`(${data.context.entries})`)}`);
1270
1279
  console.log();
@@ -1272,21 +1281,19 @@ function renderStatus(data) {
1272
1281
  if (data.repos.length === 0) {
1273
1282
  console.log(` ${dim("none yet \u2014 `mx repo add <git-url>`")}`);
1274
1283
  } else {
1275
- const nameW = Math.max(...data.repos.map((r) => r.name.length));
1276
- const branchW = Math.max(...data.repos.map((r) => r.branch.length));
1277
- for (const r of data.repos) {
1278
- const name = r.name.padEnd(nameW);
1279
- const branch = dim(r.branch.padEnd(branchW));
1280
- const remote = dim(r.remote ?? "(no remote)");
1281
- console.log(` \u2022 ${name} ${branch} ${remote}`);
1284
+ for (let i = 0; i < data.repos.length; i++) {
1285
+ if (i > 0) console.log();
1286
+ const r = data.repos[i];
1287
+ console.log(` \u2022 ${bold(r.name)}`);
1288
+ console.log(` ${dim(tildify(r.path))}`);
1289
+ console.log(` ${dim(`${r.branch} ${r.remote ?? "(no remote)"}`)}`);
1282
1290
  }
1283
1291
  }
1284
1292
  console.log();
1285
1293
  const visibleArchived = data.works.filter((w) => w.isArchived === true);
1286
1294
  const visibleActive = data.works.filter((w) => w.isArchived !== true);
1287
1295
  const hiddenArchived = data.archivedWorksCount - visibleArchived.length;
1288
- const worksCount = data.archivedWorksCount > 0 ? dim(`(${visibleActive.length} active, ${data.archivedWorksCount} archived${hiddenArchived > 0 ? ` \u2014 pass --all to show` : ""})`) : dim(`(${data.works.length})`);
1289
- console.log(` ${bold("works")} ${worksCount}`);
1296
+ console.log(` ${bold("works")}`);
1290
1297
  if (data.works.length === 0) {
1291
1298
  const empty = hiddenArchived > 0 ? `${hiddenArchived} archived hidden \u2014 pass --all to show` : "none yet \u2014 `mx work new <name>`";
1292
1299
  console.log(` ${dim(empty)}`);
@@ -1305,6 +1312,7 @@ function renderStatus(data) {
1305
1312
  const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
1306
1313
  const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
1307
1314
  console.log(` \u2022 ${styledName}${chip}`);
1315
+ console.log(` ${dim(tildify(w.path))}`);
1308
1316
  if (wts.length === 0) {
1309
1317
  console.log(` ${dim("(no worktrees)")}`);
1310
1318
  continue;
@@ -1344,22 +1352,53 @@ function dispatchRepo(positionals, flags) {
1344
1352
  console.log(dim("no repos yet \u2014 `mx repo add <git-url>`"));
1345
1353
  return;
1346
1354
  }
1347
- const nameW = Math.max(...repos.map((r) => r.name.length));
1348
- const branchW = Math.max(...repos.map((r) => r.branch.length));
1349
- for (const r of repos) {
1350
- const name = r.name.padEnd(nameW);
1351
- const branch = dim(r.branch.padEnd(branchW));
1352
- const remote = dim(r.remote ?? "(no remote)");
1353
- console.log(`\u2022 ${name} ${branch} ${remote}`);
1355
+ for (let i = 0; i < repos.length; i++) {
1356
+ if (i > 0) console.log();
1357
+ const r = repos[i];
1358
+ console.log(`\u2022 ${bold(r.name)}`);
1354
1359
  console.log(` ${dim(tildify(r.path))}`);
1360
+ console.log(` ${dim(`${r.branch} ${r.remote ?? "(no remote)"}`)}`);
1355
1361
  }
1356
1362
  }, repos);
1357
1363
  return;
1358
1364
  }
1365
+ case "path": {
1366
+ const name = need(
1367
+ flags.name || ctxRepo,
1368
+ "which repo? pass -n <name> or run inside a repo (mx repo -n <name> path)"
1369
+ );
1370
+ const res = repoInfo(root, name);
1371
+ emit(() => console.log(res.path), { name: res.name, path: res.path });
1372
+ return;
1373
+ }
1359
1374
  case "fetch": {
1375
+ if (flags.all) {
1376
+ const names = listReposInfo(root).map((r) => r.name);
1377
+ if (names.length === 0) {
1378
+ emit(() => console.log(dim("no repos yet \u2014 `mx repo add <git-url>`")), []);
1379
+ return;
1380
+ }
1381
+ const out = [];
1382
+ const lines = [];
1383
+ for (const n of names) {
1384
+ try {
1385
+ const r = repoFetch(root, n);
1386
+ out.push(r);
1387
+ lines.push(
1388
+ `${check()} ${bold(r.name)} ${dim(`\u2014 ${r.remoteBranches.length} branch(es) on origin, now on ${r.branch}`)}`
1389
+ );
1390
+ } catch (e) {
1391
+ const msg = e instanceof MxError ? e.message : String(e);
1392
+ out.push({ name: n, error: msg });
1393
+ lines.push(`${warn()} ${bold(n)} ${dim(`\u2014 ${msg}`)}`);
1394
+ }
1395
+ }
1396
+ emit(() => lines.forEach((l) => console.log(l)), out);
1397
+ return;
1398
+ }
1360
1399
  const name = need(
1361
1400
  flags.name || ctxRepo,
1362
- "which repo? pass -n <name> or run inside a repo (mx repo -n <name> fetch)"
1401
+ "which repo? pass -n <name>, run inside a repo, or use --all (mx repo -n <name> fetch)"
1363
1402
  );
1364
1403
  const res = repoFetch(root, name);
1365
1404
  emit(
@@ -1610,7 +1649,8 @@ function need2(v, msg) {
1610
1649
  return v;
1611
1650
  }
1612
1651
  function dispatchWork(positionals, flags) {
1613
- const action = positionals[1];
1652
+ let action = positionals[1];
1653
+ if (!action && flags.open) action = "open";
1614
1654
  if (action === "new") {
1615
1655
  const root2 = requireRuntime({ runtime: flags.runtime });
1616
1656
  const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>] [-o|--open]");
@@ -1700,6 +1740,22 @@ function dispatchWork(positionals, flags) {
1700
1740
  emit(() => console.log(res.path), res);
1701
1741
  return;
1702
1742
  }
1743
+ case "open": {
1744
+ const res = workPath(root, name);
1745
+ try {
1746
+ openWorkLayout(res.path, workspaceFile(root, name));
1747
+ } catch (e) {
1748
+ const msg = e instanceof MxError ? e.message : String(e);
1749
+ process.stderr.write(`${warn()} ${dim(`could not open layout: ${msg}`)}
1750
+ `);
1751
+ return;
1752
+ }
1753
+ emit(
1754
+ () => console.log(`${check()} opened ${bold(name)} ${dim("(Terminal + editor)")}`),
1755
+ { work: name, opened: true }
1756
+ );
1757
+ return;
1758
+ }
1703
1759
  case "describe": {
1704
1760
  const text = need2(positionals[2], "usage: mx work -n <name> describe <text>");
1705
1761
  const work = workDescribe(root, name, text);
@@ -1943,7 +1999,7 @@ var VERSION = (() => {
1943
1999
  function main() {
1944
2000
  const { positionals, flags } = parseArgs(process.argv.slice(2));
1945
2001
  setPorcelain(flags.porcelain);
1946
- if (positionals[0] === "s" || positionals[0] === "st") positionals[0] = "status";
2002
+ if (positionals[0] === "i") positionals[0] = "info";
1947
2003
  try {
1948
2004
  if (flags.version || positionals[0] === "version") {
1949
2005
  emit(() => console.log(`mx ${VERSION}`), { version: VERSION });
@@ -1955,7 +2011,7 @@ function main() {
1955
2011
  }
1956
2012
  switch (positionals[0]) {
1957
2013
  case "init":
1958
- case "status":
2014
+ case "info":
1959
2015
  case "sync":
1960
2016
  case "update":
1961
2017
  case "migrate":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roulabs/mx",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "mx — run several features in parallel across shared repos using git worktrees",
5
5
  "type": "module",
6
6
  "bin": {