@roulabs/mx 2.2.0 → 2.4.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/README.md CHANGED
@@ -43,7 +43,7 @@ Inside a work folder or worktree you can drop `-n` — mx infers the work/repo f
43
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
- | `mx migrate` | upgrade an older-version runtime to the version this CLI supports (the only command allowed on a version-mismatched runtime) |
46
+ | `mx migrate [--dry-run]` | upgrade an older-version runtime to the version this CLI supports (the only command allowed on a version-mismatched runtime); `--dry-run` previews the plan without changing anything |
47
47
  | `mx repo add <git-url> [--name <n>]` | clone a pristine repo (into `repos/<repo>/git`; stamps its `hydrate.sh`/`health.sh`) |
48
48
  | `mx repo ls` / `mx repo -n <name> fetch\|info\|rm` | manage pristine repos |
49
49
  | `mx repo health` / `mx repo -n <name> health` | local-only health check (augmented by the repo's `health.sh`) |
package/bin/mx.js CHANGED
@@ -222,13 +222,18 @@ function requireRuntime(opts = {}) {
222
222
  function listRepoNames(root) {
223
223
  return listDirs(reposDir(root)).filter((n) => isGitRepo(repoGitDir(root, n)));
224
224
  }
225
- function migrateRepoLayout(root) {
225
+ function migrateRepoLayout(root, opts = {}) {
226
+ const dry = opts.dryRun === true;
226
227
  const migrated = [];
227
228
  for (const name of listDirs(reposDir(root))) {
228
229
  const container = repoPath(root, name);
229
230
  const gitdir = repoGitDir(root, name);
230
231
  if (isGitRepo(gitdir)) continue;
231
232
  if (!isGitRepo(container)) continue;
233
+ if (dry) {
234
+ migrated.push(container);
235
+ continue;
236
+ }
232
237
  const tmp = path3.join(reposDir(root), `.${name}.mxmig`);
233
238
  if (exists(tmp)) fs4.rmSync(tmp, { recursive: true, force: true });
234
239
  fs4.renameSync(container, tmp);
@@ -242,7 +247,8 @@ function migrateRepoLayout(root) {
242
247
  }
243
248
  return migrated;
244
249
  }
245
- function migrateWorkLayout(root) {
250
+ function migrateWorkLayout(root, opts = {}) {
251
+ const dry = opts.dryRun === true;
246
252
  const changed = [];
247
253
  for (const name of listWorkNames(root)) {
248
254
  let work;
@@ -257,6 +263,10 @@ function migrateWorkLayout(root) {
257
263
  const flat = path3.join(wd, wt.repo);
258
264
  const dest = path3.join(wtDir, wt.repo);
259
265
  if (exists(dest) || !exists(flat)) continue;
266
+ if (dry) {
267
+ changed.push(dest);
268
+ continue;
269
+ }
260
270
  fs4.mkdirSync(wtDir, { recursive: true });
261
271
  try {
262
272
  git(["-C", repoGitDir(root, wt.repo), "worktree", "move", flat, dest]);
@@ -276,12 +286,12 @@ function migrateWorkLayout(root) {
276
286
  let touched = false;
277
287
  for (const f of ws.folders ?? []) {
278
288
  if (f.path && !f.path.startsWith("wt/") && repos.has(f.path)) {
279
- f.path = `wt/${f.path}`;
289
+ if (!dry) f.path = `wt/${f.path}`;
280
290
  touched = true;
281
291
  }
282
292
  }
283
293
  if (touched) {
284
- writeJson(wsFile, ws);
294
+ if (!dry) writeJson(wsFile, ws);
285
295
  changed.push(wsFile);
286
296
  }
287
297
  }
@@ -360,32 +370,37 @@ function initRuntime(target0, templatesDir2) {
360
370
  removeStaleRuntimeReadme(target);
361
371
  return { runtime: target, created };
362
372
  }
363
- function ensureWorkScaffolding(root, workName) {
373
+ function ensureWorkScaffolding(root, workName, opts = {}) {
374
+ const dry = opts.dryRun === true;
364
375
  const created = [];
365
376
  const wd = workDir(root, workName);
366
- for (const d of ["wt", "scripts", "files", "tmp", "sessions", "hooks"]) {
377
+ for (const d of ["wt", "scripts", "bin", "files", "tmp", "sessions", "hooks"]) {
367
378
  const p = path3.join(wd, d);
368
379
  if (!exists(p)) {
369
- fs4.mkdirSync(p, { recursive: true });
380
+ if (!dry) fs4.mkdirSync(p, { recursive: true });
370
381
  created.push(p);
371
382
  }
372
383
  }
373
384
  const claudeMd = path3.join(wd, "CLAUDE.md");
374
385
  if (!exists(claudeMd)) {
375
- fs4.writeFileSync(claudeMd, workClaudeMd(workName));
386
+ if (!dry) fs4.writeFileSync(claudeMd, workClaudeMd(workName));
376
387
  created.push(claudeMd);
377
388
  }
378
389
  const settings = path3.join(wd, ".claude", "settings.json");
379
390
  if (!exists(settings)) {
380
- fs4.mkdirSync(path3.dirname(settings), { recursive: true });
381
- fs4.writeFileSync(settings, workClaudeSettings(root));
391
+ if (!dry) {
392
+ fs4.mkdirSync(path3.dirname(settings), { recursive: true });
393
+ fs4.writeFileSync(settings, workClaudeSettings(root));
394
+ }
382
395
  created.push(settings);
383
396
  }
384
397
  for (const event of WORK_HOOK_EVENTS) {
385
398
  const hook = workHookScript(root, workName, event);
386
399
  if (!exists(hook)) {
387
- fs4.writeFileSync(hook, workHookScriptBody(event));
388
- fs4.chmodSync(hook, 493);
400
+ if (!dry) {
401
+ fs4.writeFileSync(hook, workHookScriptBody(event));
402
+ fs4.chmodSync(hook, 493);
403
+ }
389
404
  created.push(hook);
390
405
  }
391
406
  }
@@ -472,20 +487,23 @@ var STEPS = {
472
487
  1: {
473
488
  from: 1,
474
489
  to: 2,
475
- run: (root) => {
490
+ run: (root, { dryRun }) => {
476
491
  const changed = [];
477
- changed.push(...migrateRepoLayout(root));
478
- changed.push(...migrateWorkLayout(root));
479
- for (const work of listWorkNames(root)) changed.push(...ensureWorkScaffolding(root, work));
480
- writeRuntimeVersion(root, 2);
492
+ changed.push(...migrateRepoLayout(root, { dryRun }));
493
+ changed.push(...migrateWorkLayout(root, { dryRun }));
494
+ for (const work of listWorkNames(root)) {
495
+ changed.push(...ensureWorkScaffolding(root, work, { dryRun }));
496
+ }
497
+ if (!dryRun) writeRuntimeVersion(root, 2);
481
498
  return changed;
482
499
  }
483
500
  }
484
501
  };
485
- function migrateRuntime(root) {
502
+ function migrateRuntime(root, opts = {}) {
503
+ const dryRun = opts.dryRun === true;
486
504
  const from = readRuntimeVersion(root);
487
505
  if (from === RUNTIME_VERSION) {
488
- return { from, to: RUNTIME_VERSION, applied: [], changed: [] };
506
+ return { from, to: RUNTIME_VERSION, applied: [], changed: [], dryRun };
489
507
  }
490
508
  if (from > RUNTIME_VERSION) {
491
509
  throw new MxError(
@@ -505,10 +523,10 @@ function migrateRuntime(root) {
505
523
  const changed = [];
506
524
  for (let v = from; v < RUNTIME_VERSION; v++) {
507
525
  const step = STEPS[v];
508
- changed.push(...step.run(root));
526
+ changed.push(...step.run(root, { dryRun }));
509
527
  applied.push({ from: step.from, to: step.to });
510
528
  }
511
- return { from, to: RUNTIME_VERSION, applied, changed };
529
+ return { from, to: RUNTIME_VERSION, applied, changed, dryRun };
512
530
  }
513
531
 
514
532
  // ../../packages/core/src/repos.ts
@@ -1002,7 +1020,8 @@ function parseArgs(argv) {
1002
1020
  all: false,
1003
1021
  archived: false,
1004
1022
  open: false,
1005
- noHydrate: false
1023
+ noHydrate: false,
1024
+ dryRun: false
1006
1025
  };
1007
1026
  for (let i = 0; i < argv.length; i++) {
1008
1027
  const a = argv[i];
@@ -1024,6 +1043,8 @@ function parseArgs(argv) {
1024
1043
  flags.open = true;
1025
1044
  } else if (a === "--no-hydrate") {
1026
1045
  flags.noHydrate = true;
1046
+ } else if (a === "--dry-run") {
1047
+ flags.dryRun = true;
1027
1048
  } else if (a.startsWith("--") && a.includes("=")) {
1028
1049
  const eq = a.indexOf("=");
1029
1050
  const key = VALUE_FLAGS[a.slice(0, eq)];
@@ -1109,7 +1130,7 @@ Global:
1109
1130
  mx info [--all] [--porcelain] show runtime version, repos, works, ports (active only by default; --all to include archived; alias: mx i)
1110
1131
  mx sync re-stamp runtime files (CLAUDE.md, scaffolding) from current templates \u2014 same-major, non-breaking
1111
1132
  mx update self-update the mx CLI within its major (npm i -g); flags a newer major if one exists
1112
- mx migrate upgrade an older-version runtime to the version this CLI supports (the only command allowed on a mismatched runtime)
1133
+ mx migrate [--dry-run] upgrade an older-version runtime to the version this CLI supports (the only command allowed on a mismatched runtime); --dry-run previews the plan without changing anything
1113
1134
  mx help | version
1114
1135
 
1115
1136
  Repos (pristine clones):
@@ -1276,13 +1297,26 @@ function runGlobal(positionals, flags) {
1276
1297
  }
1277
1298
  case "migrate": {
1278
1299
  const root = requireRuntime({ runtime: flags.runtime, allowVersionMismatch: true });
1279
- const res = migrateRuntime(root);
1300
+ const res = migrateRuntime(root, { dryRun: flags.dryRun });
1280
1301
  emit(() => {
1281
1302
  if (res.applied.length === 0) {
1282
1303
  console.log(`${check()} Runtime already at v${res.to} \u2014 nothing to migrate.`);
1283
1304
  return;
1284
1305
  }
1285
1306
  const steps = res.applied.map((a) => `v${a.from}\u2192v${a.to}`).join(", ");
1307
+ if (res.dryRun) {
1308
+ console.log(
1309
+ `${dim("[dry run]")} Would migrate runtime ${bold(`v${res.from}\u2192v${res.to}`)} ${dim(`(${steps})`)}`
1310
+ );
1311
+ if (res.changed.length === 0) {
1312
+ console.log(` ${dim("(no path changes \u2014 version stamp only)")}`);
1313
+ } else {
1314
+ for (const p of res.changed) console.log(` ${dim(`+ ${p}`)}`);
1315
+ }
1316
+ console.log();
1317
+ console.log(` ${dim("No changes were made. Re-run without --dry-run to apply.")}`);
1318
+ return;
1319
+ }
1286
1320
  console.log(
1287
1321
  `${check()} Migrated runtime ${bold(`v${res.from}\u2192v${res.to}`)} ${dim(`(${steps})`)}`
1288
1322
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roulabs/mx",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "mx — run several features in parallel across shared repos using git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
@@ -70,6 +70,7 @@ mx/
70
70
  │ ├── repo-a/ # worktree of repo-a on this feature's branch
71
71
  │ └── repo-b/ # worktree of repo-b on this feature's branch
72
72
  ├── scripts/ # ad-hoc per-work scripts
73
+ ├── bin/ # executables/binaries a session builds or fetches
73
74
  ├── files/ # artifacts worth keeping (agent/user drop zone)
74
75
  ├── tmp/ # throwaway scratch — may be deleted at any time
75
76
  ├── hooks/ # per-work lifecycle hooks (see § Work lifecycle hooks)
@@ -91,7 +92,7 @@ mx/
91
92
  - `works/<feature>/hooks/` holds **per-work lifecycle hooks** — mx-owned scripts mx runs around
92
93
  `mx work archive`/`unarchive`. mx stamps documented no-op scripts you customize (see § Work
93
94
  lifecycle hooks).
94
- - `works/<feature>/{scripts,files,tmp}/` are the only places to put non-mx files in a work — see
95
+ - `works/<feature>/{scripts,bin,files,tmp}/` are the only places to put non-mx files in a work — see
95
96
  § The work folder holds mx-native files only.
96
97
 
97
98
  ## Work lifecycle hooks
@@ -131,6 +132,8 @@ subfolders. When you or the user need to write anything else, use one of these,
131
132
  - **`tmp/`** — throwaway scratch. Its contents may be deleted at **any** time, with no guarantees —
132
133
  never rely on anything here persisting.
133
134
  - **`scripts/`** — ad-hoc scripts for this work.
135
+ - **`bin/`** — executables and binaries this work needs: tools you compile, CLIs you download, helper
136
+ binaries. Add it to `PATH` for the work if useful. Starts empty.
134
137
 
135
138
  The one exception: a runtime file a session legitimately needs to create at the work root for tooling
136
139
  to work (e.g. an MCP connection file like `.<something>-mcp`) is fine. The rule targets *ad-hoc*
@@ -306,7 +309,7 @@ clarity; dropping it works while you're inside the work.
306
309
  5. **Don't destroy anything unless asked.** Worktrees stay until the user confirms the feature is merged.
307
310
  Teardown keeps feature branches; never delete them.
308
311
  6. **Never create ad-hoc files in the work-folder root.** Keepable artifacts go in `files/`, throwaway
309
- scratch in `tmp/`, scripts in `scripts/`. The root is mx-native only (only exception: a tooling
312
+ scratch in `tmp/`, scripts in `scripts/`, executables/binaries in `bin/`. The root is mx-native only (only exception: a tooling
310
313
  file a session genuinely needs there, e.g. an MCP connection file). See § The work folder holds
311
314
  mx-native files only.
312
315