@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 +1 -1
- package/bin/mx.js +58 -24
- package/package.json +1 -1
- package/templates/CLAUDE.md +5 -2
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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))
|
|
480
|
-
|
|
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
|
|
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
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|