@roulabs/mx 1.2.1 → 1.7.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 (2) hide show
  1. package/bin/mx.js +271 -105
  2. package/package.json +1 -1
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 path7 from "path";
5
+ import * as path8 from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
 
8
8
  // ../../packages/core/src/errors.ts
@@ -133,6 +133,11 @@ function writeWork(root, work) {
133
133
  function findWorktree(work, repo) {
134
134
  return (work.worktrees ?? []).find((w) => w.repo === repo) ?? null;
135
135
  }
136
+ function countSessions(root, workName) {
137
+ const dir = path3.join(workDir(root, workName), "sessions");
138
+ if (!exists(dir)) return 0;
139
+ return fs4.readdirSync(dir).filter((n) => n.endsWith(".md")).length;
140
+ }
136
141
  function inferContext(root) {
137
142
  const cwd = realpath(process.cwd());
138
143
  const segmentsUnder = (base) => {
@@ -167,11 +172,23 @@ function initRuntime(target0, templatesDir2) {
167
172
  removeStaleRuntimeReadme(target);
168
173
  return { runtime: target, created };
169
174
  }
175
+ function ensureWorkScaffolding(root, workName) {
176
+ const created = [];
177
+ const sessions = path3.join(workDir(root, workName), "sessions");
178
+ if (!exists(sessions)) {
179
+ fs4.mkdirSync(sessions, { recursive: true });
180
+ created.push(sessions);
181
+ }
182
+ return created;
183
+ }
170
184
  function updateRuntime(root, templatesDir2) {
171
- const dest = stampClaudeMd(root, templatesDir2);
172
- const updated = [dest];
185
+ const updated = [];
186
+ updated.push(stampClaudeMd(root, templatesDir2));
173
187
  const ctxIndex = stampContextIndex(root, templatesDir2);
174
188
  if (ctxIndex) updated.push(ctxIndex);
189
+ for (const workName of listWorkNames(root)) {
190
+ updated.push(...ensureWorkScaffolding(root, workName));
191
+ }
175
192
  removeStaleRuntimeReadme(root);
176
193
  return { runtime: root, updated };
177
194
  }
@@ -309,26 +326,20 @@ function workNew(root, name, description = "") {
309
326
  const dir = workDir(root, name);
310
327
  if (exists(dir)) throw new MxError(`work already exists: ${name}`, "EXISTS");
311
328
  fs6.mkdirSync(dir, { recursive: true });
312
- fs6.mkdirSync(path4.join(dir, "sessions"), { recursive: true });
313
329
  const work = { name, description, worktrees: [] };
314
330
  writeWork(root, work);
315
331
  writeJson(workspaceFile(root, name), { folders: [], settings: {} });
332
+ ensureWorkScaffolding(root, name);
316
333
  return { ...work, path: dir };
317
334
  }
318
335
  function listWorksInfo(root, opts = {}) {
319
- return listWorkNames(root).map((name) => {
320
- const w = readWork(root, name);
321
- return {
322
- name,
323
- description: w.description ?? "",
324
- worktrees: (w.worktrees ?? []).length,
325
- isArchived: w.isArchived === true,
326
- archived_at: w.archived_at ?? null
327
- };
328
- }).filter((s) => {
329
- if (opts.onlyArchived) return s.isArchived;
336
+ return listWorkNames(root).map((name) => ({
337
+ ...readWork(root, name),
338
+ sessions: countSessions(root, name)
339
+ })).filter((w) => {
340
+ if (opts.onlyArchived) return w.isArchived === true;
330
341
  if (opts.includeArchived) return true;
331
- return !s.isArchived;
342
+ return w.isArchived !== true;
332
343
  });
333
344
  }
334
345
  function workInfo(root, name) {
@@ -555,10 +566,24 @@ function portList(root, name) {
555
566
  }
556
567
 
557
568
  // ../../packages/core/src/status.ts
569
+ import * as path5 from "path";
570
+ function countContextEntries(root) {
571
+ const indexPath = path5.join(root, "context", "INDEX.json");
572
+ if (!exists(indexPath)) return 0;
573
+ try {
574
+ const data = readJson(indexPath);
575
+ return Array.isArray(data) ? data.length : 0;
576
+ } catch {
577
+ return 0;
578
+ }
579
+ }
558
580
  function statusRuntime(root) {
559
- const repos = listReposInfo(root);
560
- const works = listWorkNames(root).map((name) => readWork(root, name));
561
- return { runtime: root, repos, works };
581
+ return {
582
+ runtime: root,
583
+ context: { entries: countContextEntries(root) },
584
+ repos: listReposInfo(root),
585
+ works: listWorksInfo(root, { includeArchived: true })
586
+ };
562
587
  }
563
588
 
564
589
  // src/args.ts
@@ -577,7 +602,6 @@ function parseArgs(argv) {
577
602
  help: false,
578
603
  version: false,
579
604
  force: false,
580
- all: false,
581
605
  archived: false
582
606
  };
583
607
  for (let i = 0; i < argv.length; i++) {
@@ -590,8 +614,6 @@ function parseArgs(argv) {
590
614
  flags.version = true;
591
615
  } else if (a === "--force") {
592
616
  flags.force = true;
593
- } else if (a === "--all") {
594
- flags.all = true;
595
617
  } else if (a === "--archived") {
596
618
  flags.archived = true;
597
619
  } else if (a.startsWith("--") && a.includes("=")) {
@@ -609,6 +631,22 @@ function parseArgs(argv) {
609
631
  }
610
632
 
611
633
  // src/output.ts
634
+ var USE_STYLE = process.stdout.isTTY && !process.env.NO_COLOR;
635
+ function wrap(s, code) {
636
+ return USE_STYLE ? `\x1B[${code}m${s}\x1B[0m` : s;
637
+ }
638
+ function dim(s) {
639
+ return wrap(s, 2);
640
+ }
641
+ function bold(s) {
642
+ return wrap(s, 1);
643
+ }
644
+ function check() {
645
+ return "\u2713";
646
+ }
647
+ function warn() {
648
+ return "\u26A0";
649
+ }
612
650
  var porcelain = false;
613
651
  function setPorcelain(value) {
614
652
  porcelain = value;
@@ -628,7 +666,9 @@ function fail(err) {
628
666
  if (porcelain) {
629
667
  process.stdout.write(JSON.stringify({ error: message, code }, null, 2) + "\n");
630
668
  } else {
631
- process.stderr.write(`mx: ${message}
669
+ const useStyle = process.stderr.isTTY && !process.env.NO_COLOR;
670
+ const prefix = useStyle ? `\x1B[1mmx:\x1B[0m` : "mx:";
671
+ process.stderr.write(`${prefix} ${message}
632
672
  `);
633
673
  }
634
674
  process.exit(1);
@@ -639,8 +679,8 @@ var HELP = `mx \u2014 control panel for the mx runtime
639
679
 
640
680
  Global:
641
681
  mx init [path] scaffold/adopt a runtime (default ~/mx)
642
- mx status [--porcelain] show runtime, repos, works, ports
643
- mx update re-stamp runtime CLAUDE.md from templates
682
+ mx status [--porcelain] show runtime, repos, works, ports (aliases: mx s, mx st)
683
+ mx update re-sync runtime from current templates + backfill structural scaffolding
644
684
  mx help | version
645
685
 
646
686
  Repos (pristine clones):
@@ -652,7 +692,7 @@ Repos (pristine clones):
652
692
 
653
693
  Works (features):
654
694
  mx work new <name> [--description <t>] creates folder + empty work.json + sessions/
655
- mx work ls [--all|--archived] [--porcelain] default: active only
695
+ mx work ls [--archived] [--porcelain] default: all works (archived marked); --archived filters to archived only
656
696
  mx work -n <name> info [--porcelain]
657
697
  mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
658
698
  mx work -n <name> describe <text>
@@ -674,33 +714,33 @@ Runtime discovery: --runtime <path> -> $MX_RUNTIME -> default ~/mx.
674
714
  `;
675
715
 
676
716
  // src/commands/global.ts
677
- import * as path6 from "path";
717
+ import * as path7 from "path";
678
718
 
679
719
  // src/paths.ts
680
- import * as path5 from "path";
720
+ import * as path6 from "path";
681
721
  import { fileURLToPath } from "url";
682
722
  function templatesDir() {
683
723
  if (process.env.MX_TEMPLATES_DIR) return process.env.MX_TEMPLATES_DIR;
684
- const here = path5.dirname(fileURLToPath(import.meta.url));
685
- return path5.join(here, "..", "templates");
724
+ const here = path6.dirname(fileURLToPath(import.meta.url));
725
+ return path6.join(here, "..", "templates");
686
726
  }
687
727
 
688
728
  // src/commands/global.ts
689
729
  function runtimeEnvHint(runtime) {
690
- const envRuntime = process.env.MX_RUNTIME ? path6.resolve(process.env.MX_RUNTIME) : null;
730
+ const envRuntime = process.env.MX_RUNTIME ? path7.resolve(process.env.MX_RUNTIME) : null;
691
731
  if (envRuntime === runtime) {
692
- return ["", `$MX_RUNTIME already points here \u2014 you're set.`];
732
+ return ["", `${dim("$MX_RUNTIME already points here \u2014 you're set.")}`];
693
733
  }
694
734
  if (runtime === defaultRuntime() && !envRuntime) {
695
- return ["", `This is the default mx runtime (~/mx) \u2014 no MX_RUNTIME setup needed.`];
735
+ return ["", `${dim("This is the default mx runtime (~/mx) \u2014 no MX_RUNTIME setup needed.")}`];
696
736
  }
697
737
  return [
698
738
  "",
699
- `Point mx at this runtime by adding to your shell config (~/.zshrc, ~/.bashrc):`,
739
+ `Point mx at this runtime by adding to your shell config ${dim("(~/.zshrc, ~/.bashrc)")}:`,
700
740
  "",
701
- ` export MX_RUNTIME="${runtime}"`,
741
+ ` ${bold(`export MX_RUNTIME="${runtime}"`)}`,
702
742
  "",
703
- `Without it, future \`mx\` commands fall back to the default ~/mx.`
743
+ dim("Without it, future `mx` commands fall back to the default ~/mx.")
704
744
  ];
705
745
  }
706
746
  function runGlobal(positionals, flags) {
@@ -709,8 +749,8 @@ function runGlobal(positionals, flags) {
709
749
  const target = positionals[1] || discoverRuntime({ runtime: flags.runtime });
710
750
  const res = initRuntime(target, templatesDir());
711
751
  emit(() => {
712
- console.log(`Runtime ready at ${res.runtime}`);
713
- for (const c of res.created) console.log(` + ${c}`);
752
+ console.log(`${check()} Runtime ready at ${bold(res.runtime)}`);
753
+ for (const c of res.created) console.log(` ${dim(`+ ${c}`)}`);
714
754
  for (const line of runtimeEnvHint(res.runtime)) console.log(line);
715
755
  }, res);
716
756
  return;
@@ -718,31 +758,15 @@ function runGlobal(positionals, flags) {
718
758
  case "status": {
719
759
  const root = requireRuntime({ runtime: flags.runtime });
720
760
  const data = statusRuntime(root);
721
- emit(() => {
722
- console.log(`runtime: ${data.runtime}
723
- `);
724
- console.log(`repos (${data.repos.length}):`);
725
- for (const r of data.repos) {
726
- console.log(` ${r.name} [${r.branch}] ${r.remote ?? "(no remote)"}`);
727
- }
728
- console.log(`
729
- works (${data.works.length}):`);
730
- for (const w of data.works) {
731
- console.log(` ${w.name}${w.description ? ` \u2014 ${w.description}` : ""}`);
732
- for (const wt of w.worktrees) {
733
- const ports = Object.entries(wt.ports).map(([s, p]) => `${s}:${p}`).join(", ");
734
- console.log(` ${wt.repo} [${wt.branch}]${ports ? ` (${ports})` : ""}`);
735
- }
736
- }
737
- }, data);
761
+ emit(() => renderStatus(data), data);
738
762
  return;
739
763
  }
740
764
  case "update": {
741
765
  const root = requireRuntime({ runtime: flags.runtime });
742
766
  const res = updateRuntime(root, templatesDir());
743
767
  emit(() => {
744
- console.log(`Updated runtime at ${res.runtime}`);
745
- for (const p of res.updated) console.log(` + ${p}`);
768
+ console.log(`${check()} Updated runtime at ${bold(res.runtime)}`);
769
+ for (const p of res.updated) console.log(` ${dim(`+ ${p}`)}`);
746
770
  }, res);
747
771
  return;
748
772
  }
@@ -750,6 +774,61 @@ works (${data.works.length}):`);
750
774
  throw new MxError(`unknown command: ${positionals[0]}`, "BAD_ARGS");
751
775
  }
752
776
  }
777
+ function renderStatus(data) {
778
+ console.log();
779
+ console.log(` ${bold("mx")} ${dim("\xB7")} ${data.runtime}`);
780
+ console.log();
781
+ console.log(` ${bold("context")} ${dim(`(${data.context.entries})`)}`);
782
+ console.log();
783
+ console.log(` ${bold("repos")}`);
784
+ if (data.repos.length === 0) {
785
+ console.log(` ${dim("none yet \u2014 `mx repo add <git-url>`")}`);
786
+ } else {
787
+ const nameW = Math.max(...data.repos.map((r) => r.name.length));
788
+ const branchW = Math.max(...data.repos.map((r) => r.branch.length));
789
+ for (const r of data.repos) {
790
+ const name = r.name.padEnd(nameW);
791
+ const branch = dim(r.branch.padEnd(branchW));
792
+ const remote = dim(r.remote ?? "(no remote)");
793
+ console.log(` \u2022 ${name} ${branch} ${remote}`);
794
+ }
795
+ }
796
+ 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})`);
800
+ console.log(` ${bold("works")} ${worksCount}`);
801
+ if (data.works.length === 0) {
802
+ console.log(` ${dim("none yet \u2014 `mx work new <name>`")}`);
803
+ console.log();
804
+ return;
805
+ }
806
+ const ordered = [...active, ...archived];
807
+ const wtRepoW = Math.max(
808
+ 0,
809
+ ...ordered.flatMap((w) => (w.worktrees ?? []).map((wt) => wt.repo.length))
810
+ );
811
+ for (let i = 0; i < ordered.length; i++) {
812
+ if (i > 0) console.log();
813
+ const w = ordered[i];
814
+ const wts = w.worktrees ?? [];
815
+ const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
816
+ const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
817
+ console.log(` \u2022 ${styledName}${chip}`);
818
+ if (wts.length === 0) {
819
+ console.log(` ${dim("(no worktrees)")}`);
820
+ continue;
821
+ }
822
+ for (const t of wts) {
823
+ const repo = dim(t.repo.padEnd(wtRepoW));
824
+ const branch = dim(`[${t.branch}]`);
825
+ const ports = Object.entries(t.ports ?? {}).map(([s, p]) => dim(`${s}:${p}`)).join(" ");
826
+ const portsCol = ports ? ` ${ports}` : "";
827
+ console.log(` ${repo} ${branch}${portsCol}`);
828
+ }
829
+ }
830
+ console.log();
831
+ }
753
832
 
754
833
  // src/commands/repo.ts
755
834
  function need(v, msg) {
@@ -764,13 +843,24 @@ function dispatchRepo(positionals, flags) {
764
843
  case "add": {
765
844
  const url = need(positionals[2], "usage: mx repo add <git-url> [--name <n>]");
766
845
  const res = repoAdd(root, url, flags.name);
767
- emit(() => console.log(`cloned ${res.name} -> ${res.path}`), res);
846
+ emit(() => console.log(`${check()} cloned ${bold(res.name)} ${dim(`\u2192 ${res.path}`)}`), res);
768
847
  return;
769
848
  }
770
849
  case "ls": {
771
850
  const repos = listReposInfo(root);
772
851
  emit(() => {
773
- for (const r of repos) console.log(`${r.name} [${r.branch}] ${r.remote ?? "(no remote)"}`);
852
+ if (repos.length === 0) {
853
+ console.log(dim("no repos yet \u2014 `mx repo add <git-url>`"));
854
+ return;
855
+ }
856
+ const nameW = Math.max(...repos.map((r) => r.name.length));
857
+ const branchW = Math.max(...repos.map((r) => r.branch.length));
858
+ for (const r of repos) {
859
+ const name = r.name.padEnd(nameW);
860
+ const branch = dim(r.branch.padEnd(branchW));
861
+ const remote = dim(r.remote ?? "(no remote)");
862
+ console.log(`\u2022 ${name} ${branch} ${remote}`);
863
+ }
774
864
  }, repos);
775
865
  return;
776
866
  }
@@ -782,7 +872,7 @@ function dispatchRepo(positionals, flags) {
782
872
  const res = repoFetch(root, name);
783
873
  emit(
784
874
  () => console.log(
785
- `fetched ${res.name} \u2014 ${res.remoteBranches.length} branch(es) on origin, now on ${res.branch}`
875
+ `${check()} fetched ${bold(res.name)} ${dim(`\u2014 ${res.remoteBranches.length} branch(es) on origin, now on ${res.branch}`)}`
786
876
  ),
787
877
  res
788
878
  );
@@ -795,15 +885,12 @@ function dispatchRepo(positionals, flags) {
795
885
  );
796
886
  const res = repoInfo(root, name);
797
887
  emit(() => {
798
- console.log(
799
- `${res.name}
800
- path: ${res.path}
801
- branch: ${res.branch}
802
- remote: ${res.remote ?? "(none)"}`
803
- );
804
- console.log(
805
- ` used by works: ${res.worktreesInWorks.length ? res.worktreesInWorks.join(", ") : "(none)"}`
806
- );
888
+ console.log(bold(res.name));
889
+ console.log(` ${dim("path ")} ${dim(res.path)}`);
890
+ console.log(` ${dim("branch")} ${dim(res.branch)}`);
891
+ console.log(` ${dim("remote")} ${dim(res.remote ?? "(none)")}`);
892
+ const usedBy = res.worktreesInWorks.length ? res.worktreesInWorks.join(", ") : "(none)";
893
+ console.log(` ${dim("used ")} ${dim(usedBy)}`);
807
894
  }, res);
808
895
  return;
809
896
  }
@@ -813,7 +900,7 @@ function dispatchRepo(positionals, flags) {
813
900
  "which repo? pass -n <name> or run inside a repo (mx repo -n <name> rm)"
814
901
  );
815
902
  const res = repoRemove(root, name);
816
- emit(() => console.log(`removed repo ${res.name}`), res);
903
+ emit(() => console.log(`${check()} removed repo ${bold(res.name)}`), res);
817
904
  return;
818
905
  }
819
906
  default:
@@ -833,23 +920,48 @@ function dispatchWork(positionals, flags) {
833
920
  const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>]");
834
921
  const res = workNew(root2, name2, flags.description ?? "");
835
922
  emit(() => {
836
- console.log(`created work ${res.name}`);
837
- console.log(` ${res.path}`);
923
+ console.log(`${check()} created work ${bold(res.name)}`);
924
+ console.log(` ${dim(res.path)}`);
838
925
  }, res);
839
926
  return;
840
927
  }
841
928
  if (action === "ls") {
842
929
  const root2 = requireRuntime({ runtime: flags.runtime });
843
930
  const works = listWorksInfo(root2, {
844
- includeArchived: flags.all,
931
+ includeArchived: true,
845
932
  onlyArchived: flags.archived
846
933
  });
847
934
  emit(() => {
848
- for (const w of works) {
849
- const chip = w.isArchived ? `[archived ${(w.archived_at ?? "").slice(0, 10)}] ` : "";
850
- const desc = w.description ? ` \u2014 ${w.description}` : "";
851
- const wts = `(${w.worktrees} worktree${w.worktrees === 1 ? "" : "s"})`;
852
- console.log(`${chip}${w.name} ${wts}${desc}`);
935
+ const ordered = [
936
+ ...works.filter((w) => w.isArchived !== true),
937
+ ...works.filter((w) => w.isArchived === true)
938
+ ];
939
+ if (ordered.length === 0) {
940
+ console.log(dim("no works yet \u2014 `mx work new <name>`"));
941
+ return;
942
+ }
943
+ for (let i = 0; i < ordered.length; i++) {
944
+ if (i > 0) console.log();
945
+ const w = ordered[i];
946
+ const wts = w.worktrees ?? [];
947
+ const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
948
+ const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
949
+ console.log(`\u2022 ${styledName}${chip}`);
950
+ if (w.description) {
951
+ console.log(` ${dim(`\u2014 ${w.description}`)}`);
952
+ }
953
+ if (wts.length === 0) {
954
+ console.log(` ${dim("(no worktrees)")}`);
955
+ } else {
956
+ const repoW = Math.max(...wts.map((t) => t.repo.length));
957
+ for (const t of wts) {
958
+ const repo = dim(t.repo.padEnd(repoW));
959
+ const branch = dim(`[${t.branch}]`);
960
+ const ports = Object.entries(t.ports ?? {}).map(([s, p]) => `${dim(`${s}:${p}`)}`).join(" ");
961
+ const portsCol = ports ? ` ${ports}` : "";
962
+ console.log(` ${repo} ${branch}${portsCol}`);
963
+ }
964
+ }
853
965
  }
854
966
  }, works);
855
967
  return;
@@ -862,7 +974,19 @@ function dispatchWork(positionals, flags) {
862
974
  switch (action) {
863
975
  case "info": {
864
976
  const work = workInfo(root, name);
865
- emit(() => console.log(JSON.stringify(work, null, 2)), work);
977
+ emit(() => {
978
+ const archivedChip = work.isArchived === true ? ` ${dim(`[archived ${(work.archived_at ?? "").slice(0, 10)}]`)}` : "";
979
+ const styledName = work.isArchived === true ? dim(work.name) : bold(work.name);
980
+ console.log(`${styledName}${archivedChip}`);
981
+ if (work.description) console.log(` ${dim("description")} ${dim(work.description)}`);
982
+ const wts = work.worktrees ?? [];
983
+ console.log(` ${dim("worktrees ")} ${dim(`${wts.length}`)}`);
984
+ for (const wt of wts) {
985
+ const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${dim(`${s}:`)}${dim(String(p))}`).join(" ");
986
+ const portsCol = ports ? ` ${ports}` : "";
987
+ console.log(` ${dim(wt.repo)} ${dim(`[${wt.branch}]`)}${portsCol}`);
988
+ }
989
+ }, work);
866
990
  return;
867
991
  }
868
992
  case "path": {
@@ -873,7 +997,7 @@ function dispatchWork(positionals, flags) {
873
997
  case "describe": {
874
998
  const text = need2(positionals[2], "usage: mx work -n <name> describe <text>");
875
999
  const work = workDescribe(root, name, text);
876
- emit(() => console.log(`updated description of ${name}`), work);
1000
+ emit(() => console.log(`${check()} updated description of ${bold(name)}`), work);
877
1001
  return;
878
1002
  }
879
1003
  case "worktree":
@@ -883,33 +1007,32 @@ function dispatchWork(positionals, flags) {
883
1007
  case "destroy": {
884
1008
  if (flags.force && !flags.porcelain) {
885
1009
  process.stderr.write(
886
- `\u26A0 permanently removing work "${name}" \u2014 folder and any session summaries will be deleted (branches kept). This cannot be undone.
1010
+ `${warn()} ${dim(`permanently removing work "${name}" \u2014 folder and any session summaries will be deleted (branches kept). This cannot be undone.`)}
887
1011
  `
888
1012
  );
889
1013
  }
890
1014
  const res = workDestroy(root, name, { force: flags.force });
891
- emit(
892
- () => console.log(
893
- `destroyed work ${name} (worktrees removed: ${res.removedWorktrees.join(", ") || "none"}; branches kept)`
894
- ),
895
- res
896
- );
1015
+ emit(() => {
1016
+ const removed = res.removedWorktrees.join(", ") || "none";
1017
+ console.log(`${check()} destroyed work ${bold(name)}`);
1018
+ console.log(` ${dim(`worktrees removed: ${removed}; branches kept`)}`);
1019
+ }, res);
897
1020
  return;
898
1021
  }
899
1022
  case "archive": {
900
1023
  if (!flags.porcelain) {
901
1024
  process.stderr.write(
902
- `Reminder: write any pending session summary into works/${name}/sessions/ before archiving.
1025
+ `${warn()} ${dim(`Reminder: write any pending session summary into works/${name}/sessions/ before archiving.`)}
903
1026
  `
904
1027
  );
905
1028
  }
906
1029
  const res = archiveWork(root, name);
907
- emit(
908
- () => console.log(
909
- `archived work ${name} at ${res.archived_at} (worktrees removed: ${res.removedWorktrees.join(", ") || "none"}; branches kept)`
910
- ),
911
- res
912
- );
1030
+ emit(() => {
1031
+ const removed = res.removedWorktrees.join(", ") || "none";
1032
+ console.log(`${check()} archived work ${bold(name)}`);
1033
+ console.log(` ${dim(`at ${res.archived_at}`)}`);
1034
+ console.log(` ${dim(`worktrees removed: ${removed}; branches kept`)}`);
1035
+ }, res);
913
1036
  return;
914
1037
  }
915
1038
  case "unarchive": {
@@ -926,8 +1049,10 @@ function dispatchWork(positionals, flags) {
926
1049
  }
927
1050
  const res = unarchiveWork(root, name, overrides);
928
1051
  emit(() => {
929
- console.log(`unarchived work ${name}`);
930
- for (const r of res.restored) console.log(` ${r.repo} [${r.branch}] -> ${r.path}`);
1052
+ console.log(`${check()} unarchived work ${bold(name)}`);
1053
+ for (const r of res.restored) {
1054
+ console.log(` ${r.repo} ${dim(`[${r.branch}]`)} ${dim(`\u2192 ${r.path}`)}`);
1055
+ }
931
1056
  }, res);
932
1057
  return;
933
1058
  }
@@ -944,15 +1069,28 @@ function workWorktree(root, name, positionals, flags) {
944
1069
  "usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]"
945
1070
  );
946
1071
  const res = worktreeAdd(root, name, repo, { branch: flags.branch, base: flags.base });
947
- emit(() => console.log(`added worktree ${res.repo} [${res.branch}] -> ${res.path}`), res);
1072
+ emit(
1073
+ () => console.log(
1074
+ `${check()} added worktree ${bold(res.repo)} ${dim(`[${res.branch}]`)} ${dim(`\u2192 ${res.path}`)}`
1075
+ ),
1076
+ res
1077
+ );
948
1078
  return;
949
1079
  }
950
1080
  case "ls": {
951
1081
  const list = worktreeList(root, name);
952
1082
  emit(() => {
1083
+ if (list.length === 0) {
1084
+ console.log(dim("no worktrees yet \u2014 `mx work -n <name> worktree add <repo>`"));
1085
+ return;
1086
+ }
1087
+ const repoW = Math.max(...list.map((wt) => wt.repo.length));
953
1088
  for (const wt of list) {
954
- const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${s}:${p}`).join(", ");
955
- console.log(`${wt.repo} [${wt.branch}]${ports ? ` (${ports})` : ""}`);
1089
+ const repo = dim(wt.repo.padEnd(repoW));
1090
+ const branch = dim(`[${wt.branch}]`);
1091
+ const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${dim(`${s}:`)}${dim(String(p))}`).join(" ");
1092
+ const portsCol = ports ? ` ${ports}` : "";
1093
+ console.log(`${repo} ${branch}${portsCol}`);
956
1094
  }
957
1095
  }, list);
958
1096
  return;
@@ -960,7 +1098,12 @@ function workWorktree(root, name, positionals, flags) {
960
1098
  case "rm": {
961
1099
  const repo = need2(positionals[3], "usage: mx work -n <name> worktree rm <repo>");
962
1100
  const res = worktreeRemove(root, name, repo);
963
- emit(() => console.log(`removed worktree ${res.repo} from ${name} (branch ${res.branch} kept)`), res);
1101
+ emit(
1102
+ () => console.log(
1103
+ `${check()} removed worktree ${bold(res.repo)} ${dim(`from ${name} (branch ${res.branch} kept)`)}`
1104
+ ),
1105
+ res
1106
+ );
964
1107
  return;
965
1108
  }
966
1109
  default:
@@ -981,7 +1124,12 @@ function workPort(root, name, positionals) {
981
1124
  if (!Number.isInteger(port)) throw new MxError(`invalid port: ${portArg}`, "BAD_ARGS");
982
1125
  }
983
1126
  const res = portSet(root, name, repo, service, port);
984
- emit(() => console.log(`${res.repo}.${res.service} -> ${res.port}`), res);
1127
+ emit(
1128
+ () => console.log(
1129
+ `${check()} ${res.repo}${dim(".")}${res.service} ${dim("\u2192")} ${dim(String(res.port))}`
1130
+ ),
1131
+ res
1132
+ );
985
1133
  return;
986
1134
  }
987
1135
  case "unset": {
@@ -989,17 +1137,34 @@ function workPort(root, name, positionals) {
989
1137
  const repo = need2(positionals[3], usage);
990
1138
  const service = need2(positionals[4], usage);
991
1139
  const res = portUnset(root, name, repo, service);
992
- emit(() => console.log(`unset ${res.repo}.${res.service} (was ${res.released})`), res);
1140
+ emit(
1141
+ () => console.log(
1142
+ `${check()} unset ${res.repo}${dim(".")}${res.service} ${dim(`(was ${res.released})`)}`
1143
+ ),
1144
+ res
1145
+ );
993
1146
  return;
994
1147
  }
995
1148
  case "ls": {
996
1149
  const map = portList(root, name);
997
1150
  emit(() => {
1151
+ const entries = [];
998
1152
  for (const [repo, ports] of Object.entries(map)) {
999
1153
  for (const [service, port] of Object.entries(ports)) {
1000
- console.log(`${repo}.${service} -> ${port}`);
1154
+ entries.push({ repo, service, port });
1001
1155
  }
1002
1156
  }
1157
+ if (entries.length === 0) {
1158
+ console.log(dim("no ports allocated yet \u2014 `mx work -n <name> port set <repo> <service>`"));
1159
+ return;
1160
+ }
1161
+ const lhsW = Math.max(...entries.map((e) => `${e.repo}.${e.service}`.length));
1162
+ for (const e of entries) {
1163
+ const lhs = `${e.repo}${dim(".")}${e.service}`;
1164
+ const plain = `${e.repo}.${e.service}`;
1165
+ const pad = " ".repeat(lhsW - plain.length);
1166
+ console.log(`${lhs}${pad} ${dim("\u2192")} ${dim(String(e.port))}`);
1167
+ }
1003
1168
  }, map);
1004
1169
  return;
1005
1170
  }
@@ -1010,13 +1175,14 @@ function workPort(root, name, positionals) {
1010
1175
 
1011
1176
  // src/main.ts
1012
1177
  var VERSION = (() => {
1013
- const here = path7.dirname(fileURLToPath2(import.meta.url));
1014
- const pkg = JSON.parse(readFileSync2(path7.join(here, "..", "package.json"), "utf8"));
1178
+ const here = path8.dirname(fileURLToPath2(import.meta.url));
1179
+ const pkg = JSON.parse(readFileSync2(path8.join(here, "..", "package.json"), "utf8"));
1015
1180
  return pkg.version;
1016
1181
  })();
1017
1182
  function main() {
1018
1183
  const { positionals, flags } = parseArgs(process.argv.slice(2));
1019
1184
  setPorcelain(flags.porcelain);
1185
+ if (positionals[0] === "s" || positionals[0] === "st") positionals[0] = "status";
1020
1186
  try {
1021
1187
  if (flags.version || positionals[0] === "version") {
1022
1188
  emit(() => console.log(`mx ${VERSION}`), { version: VERSION });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roulabs/mx",
3
- "version": "1.2.1",
3
+ "version": "1.7.1",
4
4
  "description": "mx — run several features in parallel across shared repos using git worktrees",
5
5
  "type": "module",
6
6
  "bin": {