@roulabs/mx 1.7.1 → 1.11.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/bin/mx.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/main.ts
4
4
  import { readFileSync as readFileSync2 } from "fs";
5
- import * as path8 from "path";
5
+ import * as path9 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // ../../packages/core/src/errors.ts
@@ -195,6 +195,7 @@ function updateRuntime(root, templatesDir2) {
195
195
 
196
196
  // ../../packages/core/src/repos.ts
197
197
  import * as fs5 from "fs";
198
+ import * as path4 from "path";
198
199
 
199
200
  // ../../packages/core/src/git.ts
200
201
  import { execFileSync } from "child_process";
@@ -284,6 +285,93 @@ function repoInfo(root, name) {
284
285
  worktreesInWorks: usedBy
285
286
  };
286
287
  }
288
+ function originDefaultBranch(rp) {
289
+ const out = gitQuiet(["-C", rp, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
290
+ if (!out) return null;
291
+ const m = out.match(/^origin\/(.+)$/);
292
+ return m ? m[1] : null;
293
+ }
294
+ function currentBranchOrNull(rp) {
295
+ const out = gitQuiet(["-C", rp, "symbolic-ref", "--short", "-q", "HEAD"]);
296
+ return out || null;
297
+ }
298
+ function statusCounts(rp) {
299
+ const out = gitQuiet(["-C", rp, "status", "--porcelain"]);
300
+ if (!out) return [0, 0];
301
+ let uncommitted = 0;
302
+ let untracked = 0;
303
+ for (const line of out.split("\n")) {
304
+ if (line.startsWith("??")) untracked++;
305
+ else if (line.length > 0) uncommitted++;
306
+ }
307
+ return [uncommitted, untracked];
308
+ }
309
+ function aheadBehind(rp, branch) {
310
+ if (!branch) return [null, null];
311
+ const upstream = `refs/remotes/origin/${branch}`;
312
+ const upstreamExists = gitQuiet(["-C", rp, "rev-parse", "--verify", upstream]);
313
+ if (!upstreamExists) return [null, null];
314
+ const out = gitQuiet(["-C", rp, "rev-list", "--left-right", "--count", `HEAD...${upstream}`]);
315
+ if (!out) return [null, null];
316
+ const [aheadStr, behindStr] = out.split(/\s+/);
317
+ return [Number(aheadStr), Number(behindStr)];
318
+ }
319
+ function lastFetched(rp) {
320
+ const fh = path4.join(rp, ".git", "FETCH_HEAD");
321
+ if (!exists(fh)) return null;
322
+ return fs5.statSync(fh).mtime.toISOString();
323
+ }
324
+ function repoHealth(root, name) {
325
+ const rp = repoPath(root, name);
326
+ if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
327
+ const defaultBranch = originDefaultBranch(rp);
328
+ const current = currentBranchOrNull(rp);
329
+ const isOnDefault = current !== null && defaultBranch !== null && current === defaultBranch;
330
+ const [uncommittedChanges, untrackedFiles] = statusCounts(rp);
331
+ const [aheadOfOrigin, behindOfOrigin] = aheadBehind(rp, current);
332
+ const lastFetchedAt = lastFetched(rp);
333
+ const worktreesInWorks = listWorkNames(root).filter(
334
+ (w) => findWorktree(readWork(root, w), name)
335
+ );
336
+ const issues = [];
337
+ if (current === null) {
338
+ issues.push("HEAD is detached");
339
+ } else if (defaultBranch && !isOnDefault) {
340
+ issues.push(`on ${current} (default: ${defaultBranch})`);
341
+ }
342
+ if (uncommittedChanges > 0) {
343
+ issues.push(
344
+ `${uncommittedChanges} uncommitted change${uncommittedChanges === 1 ? "" : "s"}`
345
+ );
346
+ }
347
+ if (untrackedFiles > 0) {
348
+ issues.push(`${untrackedFiles} untracked file${untrackedFiles === 1 ? "" : "s"}`);
349
+ }
350
+ if (behindOfOrigin !== null && behindOfOrigin > 0) {
351
+ issues.push(`${behindOfOrigin} commit${behindOfOrigin === 1 ? "" : "s"} behind origin/${current}`);
352
+ }
353
+ if (aheadOfOrigin !== null && aheadOfOrigin > 0) {
354
+ issues.push(`${aheadOfOrigin} commit${aheadOfOrigin === 1 ? "" : "s"} ahead of origin/${current}`);
355
+ }
356
+ return {
357
+ name,
358
+ path: rp,
359
+ defaultBranch,
360
+ currentBranch: current,
361
+ isOnDefault,
362
+ uncommittedChanges,
363
+ untrackedFiles,
364
+ aheadOfOrigin,
365
+ behindOfOrigin,
366
+ lastFetchedAt,
367
+ worktreesInWorks,
368
+ healthy: issues.length === 0,
369
+ issues
370
+ };
371
+ }
372
+ function listRepoHealth(root) {
373
+ return listRepoNames(root).map((name) => repoHealth(root, name));
374
+ }
287
375
  function repoRemove(root, name) {
288
376
  const rp = repoPath(root, name);
289
377
  if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
@@ -300,7 +388,7 @@ function repoRemove(root, name) {
300
388
 
301
389
  // ../../packages/core/src/works.ts
302
390
  import * as fs6 from "fs";
303
- import * as path4 from "path";
391
+ import * as path5 from "path";
304
392
  function addFolderToWorkspace(root, name, repo) {
305
393
  const file = workspaceFile(root, name);
306
394
  const ws = exists(file) ? readJson(file) : { folders: [], settings: {} };
@@ -364,7 +452,7 @@ function worktreeAdd(root, name, repo, opts = {}) {
364
452
  throw new MxError(`work "${name}" already has worktree for ${repo}`, "EXISTS");
365
453
  }
366
454
  const branch = opts.branch || name;
367
- const dest = path4.join(workDir(root, name), repo);
455
+ const dest = path5.join(workDir(root, name), repo);
368
456
  if (branchExists(rp, branch)) {
369
457
  git(["-C", rp, "worktree", "add", dest, branch]);
370
458
  } else {
@@ -391,7 +479,7 @@ function worktreeRemove(root, name, repo) {
391
479
  const work = readWork(root, name);
392
480
  const wt = findWorktree(work, repo);
393
481
  if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
394
- const dest = path4.join(workDir(root, name), repo);
482
+ const dest = path5.join(workDir(root, name), repo);
395
483
  if (exists(dest) && isDirty(dest)) {
396
484
  throw new MxError(`worktree ${repo} has uncommitted changes \u2014 commit or discard them first`, "DIRTY");
397
485
  }
@@ -411,7 +499,7 @@ function workDestroy(root, name, opts = {}) {
411
499
  const work = readWork(root, name);
412
500
  const dirty = [];
413
501
  for (const wt of work.worktrees ?? []) {
414
- const dest = path4.join(workDir(root, name), wt.repo);
502
+ const dest = path5.join(workDir(root, name), wt.repo);
415
503
  if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
416
504
  }
417
505
  if (dirty.length) {
@@ -422,7 +510,7 @@ function workDestroy(root, name, opts = {}) {
422
510
  }
423
511
  const removed = [];
424
512
  for (const wt of work.worktrees ?? []) {
425
- const dest = path4.join(workDir(root, name), wt.repo);
513
+ const dest = path5.join(workDir(root, name), wt.repo);
426
514
  if (exists(dest)) git(["-C", repoPath(root, wt.repo), "worktree", "remove", dest]);
427
515
  removed.push(wt.repo);
428
516
  }
@@ -436,7 +524,7 @@ function archiveWork(root, name) {
436
524
  }
437
525
  const dirty = [];
438
526
  for (const wt of work.worktrees ?? []) {
439
- const dest = path4.join(workDir(root, name), wt.repo);
527
+ const dest = path5.join(workDir(root, name), wt.repo);
440
528
  if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
441
529
  }
442
530
  if (dirty.length) {
@@ -447,7 +535,7 @@ function archiveWork(root, name) {
447
535
  }
448
536
  const removed = [];
449
537
  for (const wt of work.worktrees ?? []) {
450
- const dest = path4.join(workDir(root, name), wt.repo);
538
+ const dest = path5.join(workDir(root, name), wt.repo);
451
539
  if (exists(dest)) git(["-C", repoPath(root, wt.repo), "worktree", "remove", dest]);
452
540
  removed.push(wt.repo);
453
541
  }
@@ -486,7 +574,7 @@ function unarchiveWork(root, name, overrides = {}) {
486
574
  }
487
575
  const restored = [];
488
576
  for (const d of desired) {
489
- const dest = path4.join(workDir(root, name), d.repo);
577
+ const dest = path5.join(workDir(root, name), d.repo);
490
578
  git(["-C", repoPath(root, d.repo), "worktree", "add", dest, d.branch]);
491
579
  addFolderToWorkspace(root, name, d.repo);
492
580
  restored.push({ repo: d.repo, branch: d.branch, path: dest, ports: d.ports });
@@ -566,9 +654,9 @@ function portList(root, name) {
566
654
  }
567
655
 
568
656
  // ../../packages/core/src/status.ts
569
- import * as path5 from "path";
657
+ import * as path6 from "path";
570
658
  function countContextEntries(root) {
571
- const indexPath = path5.join(root, "context", "INDEX.json");
659
+ const indexPath = path6.join(root, "context", "INDEX.json");
572
660
  if (!exists(indexPath)) return 0;
573
661
  try {
574
662
  const data = readJson(indexPath);
@@ -577,12 +665,16 @@ function countContextEntries(root) {
577
665
  return 0;
578
666
  }
579
667
  }
580
- function statusRuntime(root) {
668
+ function statusRuntime(root, opts = {}) {
669
+ const all = listWorksInfo(root, { includeArchived: true });
670
+ const archivedWorksCount = all.filter((w) => w.isArchived === true).length;
671
+ const works = opts.onlyArchived ? all.filter((w) => w.isArchived === true) : opts.includeArchived ? all : all.filter((w) => w.isArchived !== true);
581
672
  return {
582
673
  runtime: root,
583
674
  context: { entries: countContextEntries(root) },
584
675
  repos: listReposInfo(root),
585
- works: listWorksInfo(root, { includeArchived: true })
676
+ works,
677
+ archivedWorksCount
586
678
  };
587
679
  }
588
680
 
@@ -602,6 +694,8 @@ function parseArgs(argv) {
602
694
  help: false,
603
695
  version: false,
604
696
  force: false,
697
+ yes: false,
698
+ all: false,
605
699
  archived: false
606
700
  };
607
701
  for (let i = 0; i < argv.length; i++) {
@@ -614,6 +708,10 @@ function parseArgs(argv) {
614
708
  flags.version = true;
615
709
  } else if (a === "--force") {
616
710
  flags.force = true;
711
+ } else if (a === "--yes" || a === "-y") {
712
+ flags.yes = true;
713
+ } else if (a === "--all") {
714
+ flags.all = true;
617
715
  } else if (a === "--archived") {
618
716
  flags.archived = true;
619
717
  } else if (a.startsWith("--") && a.includes("=")) {
@@ -631,6 +729,7 @@ function parseArgs(argv) {
631
729
  }
632
730
 
633
731
  // src/output.ts
732
+ import { spawnSync } from "child_process";
634
733
  var USE_STYLE = process.stdout.isTTY && !process.env.NO_COLOR;
635
734
  function wrap(s, code) {
636
735
  return USE_STYLE ? `\x1B[${code}m${s}\x1B[0m` : s;
@@ -647,6 +746,18 @@ function check() {
647
746
  function warn() {
648
747
  return "\u26A0";
649
748
  }
749
+ function confirmYesNo(prompt) {
750
+ if (!process.stdin.isTTY) return false;
751
+ process.stdout.write(prompt);
752
+ const result = spawnSync(
753
+ "/bin/sh",
754
+ ["-c", `IFS= read -r REPLY && printf '%s' "$REPLY"`],
755
+ { stdio: ["inherit", "pipe", "inherit"], encoding: "utf8" }
756
+ );
757
+ if (result.status !== 0 || result.signal) return false;
758
+ const answer = (result.stdout ?? "").trim().toLowerCase();
759
+ return answer === "y" || answer === "yes";
760
+ }
650
761
  var porcelain = false;
651
762
  function setPorcelain(value) {
652
763
  porcelain = value;
@@ -679,7 +790,7 @@ var HELP = `mx \u2014 control panel for the mx runtime
679
790
 
680
791
  Global:
681
792
  mx init [path] scaffold/adopt a runtime (default ~/mx)
682
- mx status [--porcelain] show runtime, repos, works, ports (aliases: mx s, mx st)
793
+ mx status [--all] [--porcelain] show runtime, repos, works, ports (active only by default; --all to include archived; aliases: mx s, mx st)
683
794
  mx update re-sync runtime from current templates + backfill structural scaffolding
684
795
  mx help | version
685
796
 
@@ -688,11 +799,13 @@ Repos (pristine clones):
688
799
  mx repo ls [--porcelain]
689
800
  mx repo -n <name> fetch git fetch (+ ff current branch)
690
801
  mx repo -n <name> info [--porcelain]
802
+ mx repo health [--porcelain] pure-local health summary for every pristine clone
803
+ mx repo -n <name> health [--porcelain] detailed health for one pristine clone
691
804
  mx repo -n <name> rm refuses if any work uses it
692
805
 
693
806
  Works (features):
694
807
  mx work new <name> [--description <t>] creates folder + empty work.json + sessions/
695
- mx work ls [--archived] [--porcelain] default: all works (archived marked); --archived filters to archived only
808
+ mx work ls [--all|--archived] [--porcelain] default: active only; --all includes archived; --archived shows archived only
696
809
  mx work -n <name> info [--porcelain]
697
810
  mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
698
811
  mx work -n <name> describe <text>
@@ -702,7 +815,7 @@ Works (features):
702
815
  mx work -n <name> port set <repo> <service> [<port>] auto-picks a free port if omitted
703
816
  mx work -n <name> port unset <repo> <service>
704
817
  mx work -n <name> port ls [--porcelain]
705
- mx work -n <name> archive removes worktrees; keeps folder + work.json + sessions; recoverable
818
+ mx work -n <name> archive [--yes|-y] removes worktrees; keeps folder + work.json + sessions; prompts for confirmation (use --yes to skip)
706
819
  mx work -n <name> unarchive [<repo>=<branch>...] re-creates worktrees from work.json; override per-repo branch if recorded one is missing
707
820
  mx work -n <name> destroy --force PERMANENT: deletes the work folder including session summaries (branches kept). Prefer archive.
708
821
 
@@ -714,20 +827,20 @@ Runtime discovery: --runtime <path> -> $MX_RUNTIME -> default ~/mx.
714
827
  `;
715
828
 
716
829
  // src/commands/global.ts
717
- import * as path7 from "path";
830
+ import * as path8 from "path";
718
831
 
719
832
  // src/paths.ts
720
- import * as path6 from "path";
833
+ import * as path7 from "path";
721
834
  import { fileURLToPath } from "url";
722
835
  function templatesDir() {
723
836
  if (process.env.MX_TEMPLATES_DIR) return process.env.MX_TEMPLATES_DIR;
724
- const here = path6.dirname(fileURLToPath(import.meta.url));
725
- return path6.join(here, "..", "templates");
837
+ const here = path7.dirname(fileURLToPath(import.meta.url));
838
+ return path7.join(here, "..", "templates");
726
839
  }
727
840
 
728
841
  // src/commands/global.ts
729
842
  function runtimeEnvHint(runtime) {
730
- const envRuntime = process.env.MX_RUNTIME ? path7.resolve(process.env.MX_RUNTIME) : null;
843
+ const envRuntime = process.env.MX_RUNTIME ? path8.resolve(process.env.MX_RUNTIME) : null;
731
844
  if (envRuntime === runtime) {
732
845
  return ["", `${dim("$MX_RUNTIME already points here \u2014 you're set.")}`];
733
846
  }
@@ -757,7 +870,7 @@ function runGlobal(positionals, flags) {
757
870
  }
758
871
  case "status": {
759
872
  const root = requireRuntime({ runtime: flags.runtime });
760
- const data = statusRuntime(root);
873
+ const data = statusRuntime(root, { includeArchived: flags.all });
761
874
  emit(() => renderStatus(data), data);
762
875
  return;
763
876
  }
@@ -794,16 +907,18 @@ function renderStatus(data) {
794
907
  }
795
908
  }
796
909
  console.log();
797
- const active = data.works.filter((w) => w.isArchived !== true);
798
- const archived = data.works.filter((w) => w.isArchived === true);
799
- const worksCount = archived.length > 0 ? dim(`(${active.length} active, ${archived.length} archived)`) : dim(`(${data.works.length})`);
910
+ const visibleArchived = data.works.filter((w) => w.isArchived === true);
911
+ const visibleActive = data.works.filter((w) => w.isArchived !== true);
912
+ const hiddenArchived = data.archivedWorksCount - visibleArchived.length;
913
+ const worksCount = data.archivedWorksCount > 0 ? dim(`(${visibleActive.length} active, ${data.archivedWorksCount} archived${hiddenArchived > 0 ? ` \u2014 pass --all to show` : ""})`) : dim(`(${data.works.length})`);
800
914
  console.log(` ${bold("works")} ${worksCount}`);
801
915
  if (data.works.length === 0) {
802
- console.log(` ${dim("none yet \u2014 `mx work new <name>`")}`);
916
+ const empty = hiddenArchived > 0 ? `${hiddenArchived} archived hidden \u2014 pass --all to show` : "none yet \u2014 `mx work new <name>`";
917
+ console.log(` ${dim(empty)}`);
803
918
  console.log();
804
919
  return;
805
920
  }
806
- const ordered = [...active, ...archived];
921
+ const ordered = [...visibleActive, ...visibleArchived];
807
922
  const wtRepoW = Math.max(
808
923
  0,
809
924
  ...ordered.flatMap((w) => (w.worktrees ?? []).map((wt) => wt.repo.length))
@@ -903,10 +1018,105 @@ function dispatchRepo(positionals, flags) {
903
1018
  emit(() => console.log(`${check()} removed repo ${bold(res.name)}`), res);
904
1019
  return;
905
1020
  }
1021
+ case "health": {
1022
+ const name = flags.name || ctxRepo;
1023
+ if (name) {
1024
+ const h = repoHealth(root, name);
1025
+ emit(() => renderHealthDetail(h), h);
1026
+ } else {
1027
+ const list = listRepoHealth(root);
1028
+ emit(() => renderHealthList(list), list);
1029
+ }
1030
+ return;
1031
+ }
906
1032
  default:
907
1033
  throw new MxError(`unknown repo command: ${action ?? "(none)"}`, "BAD_ARGS");
908
1034
  }
909
1035
  }
1036
+ function relativeTime(iso) {
1037
+ if (!iso) return "never";
1038
+ const then = Date.parse(iso);
1039
+ const seconds = Math.max(0, Math.floor((Date.now() - then) / 1e3));
1040
+ if (seconds < 60) return `${seconds} second${seconds === 1 ? "" : "s"} ago`;
1041
+ const minutes = Math.floor(seconds / 60);
1042
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
1043
+ const hours = Math.floor(minutes / 60);
1044
+ if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
1045
+ const days = Math.floor(hours / 24);
1046
+ if (days < 30) return `${days} day${days === 1 ? "" : "s"} ago`;
1047
+ const months = Math.floor(days / 30);
1048
+ if (months < 12) return `${months} month${months === 1 ? "" : "s"} ago`;
1049
+ const years = Math.floor(months / 12);
1050
+ return `${years} year${years === 1 ? "" : "s"} ago`;
1051
+ }
1052
+ function renderHealthList(list) {
1053
+ if (list.length === 0) {
1054
+ console.log(dim("no repos yet \u2014 `mx repo add <git-url>`"));
1055
+ return;
1056
+ }
1057
+ const nameW = Math.max(...list.map((h) => h.name.length));
1058
+ for (const h of list) {
1059
+ const marker = h.healthy ? check() : warn();
1060
+ const name = h.name.padEnd(nameW);
1061
+ const detail = h.healthy ? "" : ` ${dim(h.issues.join("; "))}`;
1062
+ console.log(`${marker} ${name}${detail}`);
1063
+ }
1064
+ }
1065
+ function renderHealthDetail(h) {
1066
+ const rows = [];
1067
+ const addRow = (label, value, ok, hint) => {
1068
+ const marker = ok === void 0 ? void 0 : ok ? check() : warn();
1069
+ rows.push({ label, value, marker, hint });
1070
+ };
1071
+ addRow("default branch", h.defaultBranch ?? "(unknown)");
1072
+ const currentBranchText = h.currentBranch ?? "(detached HEAD)";
1073
+ const branchOk = h.currentBranch !== null && h.isOnDefault;
1074
+ addRow(
1075
+ "current branch",
1076
+ currentBranchText,
1077
+ branchOk,
1078
+ branchOk ? void 0 : h.currentBranch === null ? "HEAD is detached" : `should be ${h.defaultBranch ?? "(default)"}`
1079
+ );
1080
+ addRow(
1081
+ "uncommitted",
1082
+ `${h.uncommittedChanges} change${h.uncommittedChanges === 1 ? "" : "s"}`,
1083
+ h.uncommittedChanges === 0,
1084
+ h.uncommittedChanges === 0 ? void 0 : "commit or discard"
1085
+ );
1086
+ addRow(
1087
+ "untracked",
1088
+ `${h.untrackedFiles} file${h.untrackedFiles === 1 ? "" : "s"}`,
1089
+ h.untrackedFiles === 0
1090
+ );
1091
+ addRow(
1092
+ "ahead of origin",
1093
+ h.aheadOfOrigin === null ? "(no upstream)" : `${h.aheadOfOrigin} commit${h.aheadOfOrigin === 1 ? "" : "s"}`,
1094
+ h.aheadOfOrigin === null ? void 0 : h.aheadOfOrigin === 0
1095
+ );
1096
+ addRow(
1097
+ "behind of origin",
1098
+ h.behindOfOrigin === null ? "(no upstream)" : `${h.behindOfOrigin} commit${h.behindOfOrigin === 1 ? "" : "s"}`,
1099
+ h.behindOfOrigin === null ? void 0 : h.behindOfOrigin === 0,
1100
+ (h.behindOfOrigin ?? 0) > 0 ? `run \`mx repo -n ${h.name} fetch\`` : void 0
1101
+ );
1102
+ addRow("last fetched", relativeTime(h.lastFetchedAt));
1103
+ const usedByCount = h.worktreesInWorks.length;
1104
+ rows.push({
1105
+ label: "worktrees in works",
1106
+ value: String(usedByCount),
1107
+ hint: usedByCount ? `used by: ${h.worktreesInWorks.join(", ")}` : void 0
1108
+ });
1109
+ const labelW = Math.max(...rows.map((r) => r.label.length));
1110
+ const valueW = Math.max(...rows.map((r) => r.value.length));
1111
+ console.log(bold(h.name));
1112
+ for (const r of rows) {
1113
+ const label = dim(r.label.padEnd(labelW));
1114
+ const value = r.value.padEnd(valueW);
1115
+ const marker = r.marker ? ` ${r.marker}` : " ";
1116
+ const hint = r.hint ? ` ${dim(r.hint)}` : "";
1117
+ console.log(` ${label} ${value}${marker}${hint}`);
1118
+ }
1119
+ }
910
1120
 
911
1121
  // src/commands/work.ts
912
1122
  function need2(v, msg) {
@@ -928,7 +1138,7 @@ function dispatchWork(positionals, flags) {
928
1138
  if (action === "ls") {
929
1139
  const root2 = requireRuntime({ runtime: flags.runtime });
930
1140
  const works = listWorksInfo(root2, {
931
- includeArchived: true,
1141
+ includeArchived: flags.all,
932
1142
  onlyArchived: flags.archived
933
1143
  });
934
1144
  emit(() => {
@@ -1020,11 +1230,29 @@ function dispatchWork(positionals, flags) {
1020
1230
  return;
1021
1231
  }
1022
1232
  case "archive": {
1023
- if (!flags.porcelain) {
1233
+ if (!flags.yes) {
1234
+ if (flags.porcelain || !process.stdin.isTTY) {
1235
+ throw new MxError(
1236
+ `archive requires confirmation \u2014 pass --yes when running non-interactively or with --porcelain`,
1237
+ "NEED_CONFIRMATION"
1238
+ );
1239
+ }
1240
+ process.stderr.write(`${warn()} About to archive work ${bold(name)}.
1241
+ `);
1024
1242
  process.stderr.write(
1025
- `${warn()} ${dim(`Reminder: write any pending session summary into works/${name}/sessions/ before archiving.`)}
1243
+ `${dim(` Worktrees will be removed; folder, work.json, branches, and sessions/ are preserved.`)}
1026
1244
  `
1027
1245
  );
1246
+ process.stderr.write(
1247
+ `${dim(` Make sure any pending session summary is written into works/${name}/sessions/ first.`)}
1248
+ `
1249
+ );
1250
+ process.stderr.write("\n");
1251
+ if (!confirmYesNo("Proceed? (y/N) ")) {
1252
+ process.stderr.write(`${dim("Aborted.")}
1253
+ `);
1254
+ return;
1255
+ }
1028
1256
  }
1029
1257
  const res = archiveWork(root, name);
1030
1258
  emit(() => {
@@ -1175,8 +1403,8 @@ function workPort(root, name, positionals) {
1175
1403
 
1176
1404
  // src/main.ts
1177
1405
  var VERSION = (() => {
1178
- const here = path8.dirname(fileURLToPath2(import.meta.url));
1179
- const pkg = JSON.parse(readFileSync2(path8.join(here, "..", "package.json"), "utf8"));
1406
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
1407
+ const pkg = JSON.parse(readFileSync2(path9.join(here, "..", "package.json"), "utf8"));
1180
1408
  return pkg.version;
1181
1409
  })();
1182
1410
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roulabs/mx",
3
- "version": "1.7.1",
3
+ "version": "1.11.0",
4
4
  "description": "mx — run several features in parallel across shared repos using git worktrees",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,10 +20,22 @@ Every read command takes `--porcelain` for stable JSON; parse that instead of sc
20
20
  ## What this runtime is for
21
21
 
22
22
  `mx/` is where **feature work** happens. Sessions launched here implement a feature inside a
23
- `works/<feature>/` folder. mx *itself* — this template, the `mx` CLI — is maintained in a separate
24
- **mx source checkout** (the `github.com/roulabs/mx` repo), outside this tree. If you were opened
25
- here to change how mx works, you're in the wrong place: switch to that repo. Don't edit `repos/`,
26
- `works/`, or the runtime files from here.
23
+ `works/<feature>/` folder. mx *itself* — this template, the `mx` CLI — is maintained in the
24
+ `github.com/roulabs/mx` source repo. There are two valid setups for that source:
25
+
26
+ 1. **It lives elsewhere** (the default): if you were opened here to change how mx works, you're in
27
+ the wrong place — switch to that repo. Don't edit `repos/`, `works/`, or the runtime files from
28
+ here.
29
+
30
+ 2. **It's hosted as a work in this runtime** (self-hosting / dogfooding): someone has run
31
+ `mx repo add git@github.com:roulabs/mx.git` and created one or more `works/<feature>/mx/`
32
+ worktrees for parallel mx development. In that case, working inside one of those worktrees IS
33
+ valid — follow that worktree's own `CLAUDE.md` for the developer rules. The runtime rule that
34
+ still applies here: **never run the worktree's locally-built mx CLI against this runtime**.
35
+ Locally-built mx (`pnpm mx`, `node npm/bin/mx.js`) must be pointed at a sandbox (`$PWD/.mx` or
36
+ `/tmp/...`) for any testing — otherwise it may re-stamp this `CLAUDE.md` with a work-in-progress
37
+ template. The published `mx` on `$PATH` (from `npm i -g @roulabs/mx`) is the safe one against
38
+ this runtime.
27
39
 
28
40
  ## Layout
29
41