@roulabs/mx 1.2.2 → 1.10.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 +492 -110
  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 path9 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) => {
@@ -190,6 +195,7 @@ function updateRuntime(root, templatesDir2) {
190
195
 
191
196
  // ../../packages/core/src/repos.ts
192
197
  import * as fs5 from "fs";
198
+ import * as path4 from "path";
193
199
 
194
200
  // ../../packages/core/src/git.ts
195
201
  import { execFileSync } from "child_process";
@@ -279,6 +285,93 @@ function repoInfo(root, name) {
279
285
  worktreesInWorks: usedBy
280
286
  };
281
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
+ }
282
375
  function repoRemove(root, name) {
283
376
  const rp = repoPath(root, name);
284
377
  if (!isGitRepo(rp)) throw new MxError(`no such repo: ${name}`, "NO_REPO");
@@ -295,7 +388,7 @@ function repoRemove(root, name) {
295
388
 
296
389
  // ../../packages/core/src/works.ts
297
390
  import * as fs6 from "fs";
298
- import * as path4 from "path";
391
+ import * as path5 from "path";
299
392
  function addFolderToWorkspace(root, name, repo) {
300
393
  const file = workspaceFile(root, name);
301
394
  const ws = exists(file) ? readJson(file) : { folders: [], settings: {} };
@@ -328,19 +421,13 @@ function workNew(root, name, description = "") {
328
421
  return { ...work, path: dir };
329
422
  }
330
423
  function listWorksInfo(root, opts = {}) {
331
- return listWorkNames(root).map((name) => {
332
- const w = readWork(root, name);
333
- return {
334
- name,
335
- description: w.description ?? "",
336
- worktrees: (w.worktrees ?? []).length,
337
- isArchived: w.isArchived === true,
338
- archived_at: w.archived_at ?? null
339
- };
340
- }).filter((s) => {
341
- if (opts.onlyArchived) return s.isArchived;
424
+ return listWorkNames(root).map((name) => ({
425
+ ...readWork(root, name),
426
+ sessions: countSessions(root, name)
427
+ })).filter((w) => {
428
+ if (opts.onlyArchived) return w.isArchived === true;
342
429
  if (opts.includeArchived) return true;
343
- return !s.isArchived;
430
+ return w.isArchived !== true;
344
431
  });
345
432
  }
346
433
  function workInfo(root, name) {
@@ -365,7 +452,7 @@ function worktreeAdd(root, name, repo, opts = {}) {
365
452
  throw new MxError(`work "${name}" already has worktree for ${repo}`, "EXISTS");
366
453
  }
367
454
  const branch = opts.branch || name;
368
- const dest = path4.join(workDir(root, name), repo);
455
+ const dest = path5.join(workDir(root, name), repo);
369
456
  if (branchExists(rp, branch)) {
370
457
  git(["-C", rp, "worktree", "add", dest, branch]);
371
458
  } else {
@@ -392,7 +479,7 @@ function worktreeRemove(root, name, repo) {
392
479
  const work = readWork(root, name);
393
480
  const wt = findWorktree(work, repo);
394
481
  if (!wt) throw new MxError(`work "${name}" has no worktree for ${repo}`, "NO_WORKTREE");
395
- const dest = path4.join(workDir(root, name), repo);
482
+ const dest = path5.join(workDir(root, name), repo);
396
483
  if (exists(dest) && isDirty(dest)) {
397
484
  throw new MxError(`worktree ${repo} has uncommitted changes \u2014 commit or discard them first`, "DIRTY");
398
485
  }
@@ -412,7 +499,7 @@ function workDestroy(root, name, opts = {}) {
412
499
  const work = readWork(root, name);
413
500
  const dirty = [];
414
501
  for (const wt of work.worktrees ?? []) {
415
- const dest = path4.join(workDir(root, name), wt.repo);
502
+ const dest = path5.join(workDir(root, name), wt.repo);
416
503
  if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
417
504
  }
418
505
  if (dirty.length) {
@@ -423,7 +510,7 @@ function workDestroy(root, name, opts = {}) {
423
510
  }
424
511
  const removed = [];
425
512
  for (const wt of work.worktrees ?? []) {
426
- const dest = path4.join(workDir(root, name), wt.repo);
513
+ const dest = path5.join(workDir(root, name), wt.repo);
427
514
  if (exists(dest)) git(["-C", repoPath(root, wt.repo), "worktree", "remove", dest]);
428
515
  removed.push(wt.repo);
429
516
  }
@@ -437,7 +524,7 @@ function archiveWork(root, name) {
437
524
  }
438
525
  const dirty = [];
439
526
  for (const wt of work.worktrees ?? []) {
440
- const dest = path4.join(workDir(root, name), wt.repo);
527
+ const dest = path5.join(workDir(root, name), wt.repo);
441
528
  if (exists(dest) && isDirty(dest)) dirty.push(wt.repo);
442
529
  }
443
530
  if (dirty.length) {
@@ -448,7 +535,7 @@ function archiveWork(root, name) {
448
535
  }
449
536
  const removed = [];
450
537
  for (const wt of work.worktrees ?? []) {
451
- const dest = path4.join(workDir(root, name), wt.repo);
538
+ const dest = path5.join(workDir(root, name), wt.repo);
452
539
  if (exists(dest)) git(["-C", repoPath(root, wt.repo), "worktree", "remove", dest]);
453
540
  removed.push(wt.repo);
454
541
  }
@@ -487,7 +574,7 @@ function unarchiveWork(root, name, overrides = {}) {
487
574
  }
488
575
  const restored = [];
489
576
  for (const d of desired) {
490
- const dest = path4.join(workDir(root, name), d.repo);
577
+ const dest = path5.join(workDir(root, name), d.repo);
491
578
  git(["-C", repoPath(root, d.repo), "worktree", "add", dest, d.branch]);
492
579
  addFolderToWorkspace(root, name, d.repo);
493
580
  restored.push({ repo: d.repo, branch: d.branch, path: dest, ports: d.ports });
@@ -567,10 +654,28 @@ function portList(root, name) {
567
654
  }
568
655
 
569
656
  // ../../packages/core/src/status.ts
570
- function statusRuntime(root) {
571
- const repos = listReposInfo(root);
572
- const works = listWorkNames(root).map((name) => readWork(root, name));
573
- return { runtime: root, repos, works };
657
+ import * as path6 from "path";
658
+ function countContextEntries(root) {
659
+ const indexPath = path6.join(root, "context", "INDEX.json");
660
+ if (!exists(indexPath)) return 0;
661
+ try {
662
+ const data = readJson(indexPath);
663
+ return Array.isArray(data) ? data.length : 0;
664
+ } catch {
665
+ return 0;
666
+ }
667
+ }
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);
672
+ return {
673
+ runtime: root,
674
+ context: { entries: countContextEntries(root) },
675
+ repos: listReposInfo(root),
676
+ works,
677
+ archivedWorksCount
678
+ };
574
679
  }
575
680
 
576
681
  // src/args.ts
@@ -589,6 +694,7 @@ function parseArgs(argv) {
589
694
  help: false,
590
695
  version: false,
591
696
  force: false,
697
+ yes: false,
592
698
  all: false,
593
699
  archived: false
594
700
  };
@@ -602,6 +708,8 @@ function parseArgs(argv) {
602
708
  flags.version = true;
603
709
  } else if (a === "--force") {
604
710
  flags.force = true;
711
+ } else if (a === "--yes" || a === "-y") {
712
+ flags.yes = true;
605
713
  } else if (a === "--all") {
606
714
  flags.all = true;
607
715
  } else if (a === "--archived") {
@@ -621,6 +729,35 @@ function parseArgs(argv) {
621
729
  }
622
730
 
623
731
  // src/output.ts
732
+ import { spawnSync } from "child_process";
733
+ var USE_STYLE = process.stdout.isTTY && !process.env.NO_COLOR;
734
+ function wrap(s, code) {
735
+ return USE_STYLE ? `\x1B[${code}m${s}\x1B[0m` : s;
736
+ }
737
+ function dim(s) {
738
+ return wrap(s, 2);
739
+ }
740
+ function bold(s) {
741
+ return wrap(s, 1);
742
+ }
743
+ function check() {
744
+ return "\u2713";
745
+ }
746
+ function warn() {
747
+ return "\u26A0";
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
+ }
624
761
  var porcelain = false;
625
762
  function setPorcelain(value) {
626
763
  porcelain = value;
@@ -640,7 +777,9 @@ function fail(err) {
640
777
  if (porcelain) {
641
778
  process.stdout.write(JSON.stringify({ error: message, code }, null, 2) + "\n");
642
779
  } else {
643
- process.stderr.write(`mx: ${message}
780
+ const useStyle = process.stderr.isTTY && !process.env.NO_COLOR;
781
+ const prefix = useStyle ? `\x1B[1mmx:\x1B[0m` : "mx:";
782
+ process.stderr.write(`${prefix} ${message}
644
783
  `);
645
784
  }
646
785
  process.exit(1);
@@ -651,8 +790,8 @@ var HELP = `mx \u2014 control panel for the mx runtime
651
790
 
652
791
  Global:
653
792
  mx init [path] scaffold/adopt a runtime (default ~/mx)
654
- mx status [--porcelain] show runtime, repos, works, ports
655
- mx update re-stamp runtime CLAUDE.md from templates
793
+ mx status [--all] [--porcelain] show runtime, repos, works, ports (active only by default; --all to include archived; aliases: mx s, mx st)
794
+ mx update re-sync runtime from current templates + backfill structural scaffolding
656
795
  mx help | version
657
796
 
658
797
  Repos (pristine clones):
@@ -660,11 +799,13 @@ Repos (pristine clones):
660
799
  mx repo ls [--porcelain]
661
800
  mx repo -n <name> fetch git fetch (+ ff current branch)
662
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
663
804
  mx repo -n <name> rm refuses if any work uses it
664
805
 
665
806
  Works (features):
666
807
  mx work new <name> [--description <t>] creates folder + empty work.json + sessions/
667
- mx work ls [--all|--archived] [--porcelain] default: active only
808
+ mx work ls [--all|--archived] [--porcelain] default: active only; --all includes archived; --archived shows archived only
668
809
  mx work -n <name> info [--porcelain]
669
810
  mx work -n <name> path print the work folder path (cd "$(mx work -n <name> path)")
670
811
  mx work -n <name> describe <text>
@@ -674,7 +815,7 @@ Works (features):
674
815
  mx work -n <name> port set <repo> <service> [<port>] auto-picks a free port if omitted
675
816
  mx work -n <name> port unset <repo> <service>
676
817
  mx work -n <name> port ls [--porcelain]
677
- 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)
678
819
  mx work -n <name> unarchive [<repo>=<branch>...] re-creates worktrees from work.json; override per-repo branch if recorded one is missing
679
820
  mx work -n <name> destroy --force PERMANENT: deletes the work folder including session summaries (branches kept). Prefer archive.
680
821
 
@@ -686,33 +827,33 @@ Runtime discovery: --runtime <path> -> $MX_RUNTIME -> default ~/mx.
686
827
  `;
687
828
 
688
829
  // src/commands/global.ts
689
- import * as path6 from "path";
830
+ import * as path8 from "path";
690
831
 
691
832
  // src/paths.ts
692
- import * as path5 from "path";
833
+ import * as path7 from "path";
693
834
  import { fileURLToPath } from "url";
694
835
  function templatesDir() {
695
836
  if (process.env.MX_TEMPLATES_DIR) return process.env.MX_TEMPLATES_DIR;
696
- const here = path5.dirname(fileURLToPath(import.meta.url));
697
- return path5.join(here, "..", "templates");
837
+ const here = path7.dirname(fileURLToPath(import.meta.url));
838
+ return path7.join(here, "..", "templates");
698
839
  }
699
840
 
700
841
  // src/commands/global.ts
701
842
  function runtimeEnvHint(runtime) {
702
- const envRuntime = process.env.MX_RUNTIME ? path6.resolve(process.env.MX_RUNTIME) : null;
843
+ const envRuntime = process.env.MX_RUNTIME ? path8.resolve(process.env.MX_RUNTIME) : null;
703
844
  if (envRuntime === runtime) {
704
- return ["", `$MX_RUNTIME already points here \u2014 you're set.`];
845
+ return ["", `${dim("$MX_RUNTIME already points here \u2014 you're set.")}`];
705
846
  }
706
847
  if (runtime === defaultRuntime() && !envRuntime) {
707
- return ["", `This is the default mx runtime (~/mx) \u2014 no MX_RUNTIME setup needed.`];
848
+ return ["", `${dim("This is the default mx runtime (~/mx) \u2014 no MX_RUNTIME setup needed.")}`];
708
849
  }
709
850
  return [
710
851
  "",
711
- `Point mx at this runtime by adding to your shell config (~/.zshrc, ~/.bashrc):`,
852
+ `Point mx at this runtime by adding to your shell config ${dim("(~/.zshrc, ~/.bashrc)")}:`,
712
853
  "",
713
- ` export MX_RUNTIME="${runtime}"`,
854
+ ` ${bold(`export MX_RUNTIME="${runtime}"`)}`,
714
855
  "",
715
- `Without it, future \`mx\` commands fall back to the default ~/mx.`
856
+ dim("Without it, future `mx` commands fall back to the default ~/mx.")
716
857
  ];
717
858
  }
718
859
  function runGlobal(positionals, flags) {
@@ -721,40 +862,24 @@ function runGlobal(positionals, flags) {
721
862
  const target = positionals[1] || discoverRuntime({ runtime: flags.runtime });
722
863
  const res = initRuntime(target, templatesDir());
723
864
  emit(() => {
724
- console.log(`Runtime ready at ${res.runtime}`);
725
- for (const c of res.created) console.log(` + ${c}`);
865
+ console.log(`${check()} Runtime ready at ${bold(res.runtime)}`);
866
+ for (const c of res.created) console.log(` ${dim(`+ ${c}`)}`);
726
867
  for (const line of runtimeEnvHint(res.runtime)) console.log(line);
727
868
  }, res);
728
869
  return;
729
870
  }
730
871
  case "status": {
731
872
  const root = requireRuntime({ runtime: flags.runtime });
732
- const data = statusRuntime(root);
733
- emit(() => {
734
- console.log(`runtime: ${data.runtime}
735
- `);
736
- console.log(`repos (${data.repos.length}):`);
737
- for (const r of data.repos) {
738
- console.log(` ${r.name} [${r.branch}] ${r.remote ?? "(no remote)"}`);
739
- }
740
- console.log(`
741
- works (${data.works.length}):`);
742
- for (const w of data.works) {
743
- console.log(` ${w.name}${w.description ? ` \u2014 ${w.description}` : ""}`);
744
- for (const wt of w.worktrees) {
745
- const ports = Object.entries(wt.ports).map(([s, p]) => `${s}:${p}`).join(", ");
746
- console.log(` ${wt.repo} [${wt.branch}]${ports ? ` (${ports})` : ""}`);
747
- }
748
- }
749
- }, data);
873
+ const data = statusRuntime(root, { includeArchived: flags.all });
874
+ emit(() => renderStatus(data), data);
750
875
  return;
751
876
  }
752
877
  case "update": {
753
878
  const root = requireRuntime({ runtime: flags.runtime });
754
879
  const res = updateRuntime(root, templatesDir());
755
880
  emit(() => {
756
- console.log(`Updated runtime at ${res.runtime}`);
757
- for (const p of res.updated) console.log(` + ${p}`);
881
+ console.log(`${check()} Updated runtime at ${bold(res.runtime)}`);
882
+ for (const p of res.updated) console.log(` ${dim(`+ ${p}`)}`);
758
883
  }, res);
759
884
  return;
760
885
  }
@@ -762,6 +887,63 @@ works (${data.works.length}):`);
762
887
  throw new MxError(`unknown command: ${positionals[0]}`, "BAD_ARGS");
763
888
  }
764
889
  }
890
+ function renderStatus(data) {
891
+ console.log();
892
+ console.log(` ${bold("mx")} ${dim("\xB7")} ${data.runtime}`);
893
+ console.log();
894
+ console.log(` ${bold("context")} ${dim(`(${data.context.entries})`)}`);
895
+ console.log();
896
+ console.log(` ${bold("repos")}`);
897
+ if (data.repos.length === 0) {
898
+ console.log(` ${dim("none yet \u2014 `mx repo add <git-url>`")}`);
899
+ } else {
900
+ const nameW = Math.max(...data.repos.map((r) => r.name.length));
901
+ const branchW = Math.max(...data.repos.map((r) => r.branch.length));
902
+ for (const r of data.repos) {
903
+ const name = r.name.padEnd(nameW);
904
+ const branch = dim(r.branch.padEnd(branchW));
905
+ const remote = dim(r.remote ?? "(no remote)");
906
+ console.log(` \u2022 ${name} ${branch} ${remote}`);
907
+ }
908
+ }
909
+ console.log();
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})`);
914
+ console.log(` ${bold("works")} ${worksCount}`);
915
+ if (data.works.length === 0) {
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)}`);
918
+ console.log();
919
+ return;
920
+ }
921
+ const ordered = [...visibleActive, ...visibleArchived];
922
+ const wtRepoW = Math.max(
923
+ 0,
924
+ ...ordered.flatMap((w) => (w.worktrees ?? []).map((wt) => wt.repo.length))
925
+ );
926
+ for (let i = 0; i < ordered.length; i++) {
927
+ if (i > 0) console.log();
928
+ const w = ordered[i];
929
+ const wts = w.worktrees ?? [];
930
+ const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
931
+ const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
932
+ console.log(` \u2022 ${styledName}${chip}`);
933
+ if (wts.length === 0) {
934
+ console.log(` ${dim("(no worktrees)")}`);
935
+ continue;
936
+ }
937
+ for (const t of wts) {
938
+ const repo = dim(t.repo.padEnd(wtRepoW));
939
+ const branch = dim(`[${t.branch}]`);
940
+ const ports = Object.entries(t.ports ?? {}).map(([s, p]) => dim(`${s}:${p}`)).join(" ");
941
+ const portsCol = ports ? ` ${ports}` : "";
942
+ console.log(` ${repo} ${branch}${portsCol}`);
943
+ }
944
+ }
945
+ console.log();
946
+ }
765
947
 
766
948
  // src/commands/repo.ts
767
949
  function need(v, msg) {
@@ -776,13 +958,24 @@ function dispatchRepo(positionals, flags) {
776
958
  case "add": {
777
959
  const url = need(positionals[2], "usage: mx repo add <git-url> [--name <n>]");
778
960
  const res = repoAdd(root, url, flags.name);
779
- emit(() => console.log(`cloned ${res.name} -> ${res.path}`), res);
961
+ emit(() => console.log(`${check()} cloned ${bold(res.name)} ${dim(`\u2192 ${res.path}`)}`), res);
780
962
  return;
781
963
  }
782
964
  case "ls": {
783
965
  const repos = listReposInfo(root);
784
966
  emit(() => {
785
- for (const r of repos) console.log(`${r.name} [${r.branch}] ${r.remote ?? "(no remote)"}`);
967
+ if (repos.length === 0) {
968
+ console.log(dim("no repos yet \u2014 `mx repo add <git-url>`"));
969
+ return;
970
+ }
971
+ const nameW = Math.max(...repos.map((r) => r.name.length));
972
+ const branchW = Math.max(...repos.map((r) => r.branch.length));
973
+ for (const r of repos) {
974
+ const name = r.name.padEnd(nameW);
975
+ const branch = dim(r.branch.padEnd(branchW));
976
+ const remote = dim(r.remote ?? "(no remote)");
977
+ console.log(`\u2022 ${name} ${branch} ${remote}`);
978
+ }
786
979
  }, repos);
787
980
  return;
788
981
  }
@@ -794,7 +987,7 @@ function dispatchRepo(positionals, flags) {
794
987
  const res = repoFetch(root, name);
795
988
  emit(
796
989
  () => console.log(
797
- `fetched ${res.name} \u2014 ${res.remoteBranches.length} branch(es) on origin, now on ${res.branch}`
990
+ `${check()} fetched ${bold(res.name)} ${dim(`\u2014 ${res.remoteBranches.length} branch(es) on origin, now on ${res.branch}`)}`
798
991
  ),
799
992
  res
800
993
  );
@@ -807,15 +1000,12 @@ function dispatchRepo(positionals, flags) {
807
1000
  );
808
1001
  const res = repoInfo(root, name);
809
1002
  emit(() => {
810
- console.log(
811
- `${res.name}
812
- path: ${res.path}
813
- branch: ${res.branch}
814
- remote: ${res.remote ?? "(none)"}`
815
- );
816
- console.log(
817
- ` used by works: ${res.worktreesInWorks.length ? res.worktreesInWorks.join(", ") : "(none)"}`
818
- );
1003
+ console.log(bold(res.name));
1004
+ console.log(` ${dim("path ")} ${dim(res.path)}`);
1005
+ console.log(` ${dim("branch")} ${dim(res.branch)}`);
1006
+ console.log(` ${dim("remote")} ${dim(res.remote ?? "(none)")}`);
1007
+ const usedBy = res.worktreesInWorks.length ? res.worktreesInWorks.join(", ") : "(none)";
1008
+ console.log(` ${dim("used ")} ${dim(usedBy)}`);
819
1009
  }, res);
820
1010
  return;
821
1011
  }
@@ -825,13 +1015,108 @@ function dispatchRepo(positionals, flags) {
825
1015
  "which repo? pass -n <name> or run inside a repo (mx repo -n <name> rm)"
826
1016
  );
827
1017
  const res = repoRemove(root, name);
828
- emit(() => console.log(`removed repo ${res.name}`), res);
1018
+ emit(() => console.log(`${check()} removed repo ${bold(res.name)}`), res);
1019
+ return;
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
+ }
829
1030
  return;
830
1031
  }
831
1032
  default:
832
1033
  throw new MxError(`unknown repo command: ${action ?? "(none)"}`, "BAD_ARGS");
833
1034
  }
834
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
+ }
835
1120
 
836
1121
  // src/commands/work.ts
837
1122
  function need2(v, msg) {
@@ -845,8 +1130,8 @@ function dispatchWork(positionals, flags) {
845
1130
  const name2 = need2(positionals[2], "usage: mx work new <name> [--description <text>]");
846
1131
  const res = workNew(root2, name2, flags.description ?? "");
847
1132
  emit(() => {
848
- console.log(`created work ${res.name}`);
849
- console.log(` ${res.path}`);
1133
+ console.log(`${check()} created work ${bold(res.name)}`);
1134
+ console.log(` ${dim(res.path)}`);
850
1135
  }, res);
851
1136
  return;
852
1137
  }
@@ -857,11 +1142,36 @@ function dispatchWork(positionals, flags) {
857
1142
  onlyArchived: flags.archived
858
1143
  });
859
1144
  emit(() => {
860
- for (const w of works) {
861
- const chip = w.isArchived ? `[archived ${(w.archived_at ?? "").slice(0, 10)}] ` : "";
862
- const desc = w.description ? ` \u2014 ${w.description}` : "";
863
- const wts = `(${w.worktrees} worktree${w.worktrees === 1 ? "" : "s"})`;
864
- console.log(`${chip}${w.name} ${wts}${desc}`);
1145
+ const ordered = [
1146
+ ...works.filter((w) => w.isArchived !== true),
1147
+ ...works.filter((w) => w.isArchived === true)
1148
+ ];
1149
+ if (ordered.length === 0) {
1150
+ console.log(dim("no works yet \u2014 `mx work new <name>`"));
1151
+ return;
1152
+ }
1153
+ for (let i = 0; i < ordered.length; i++) {
1154
+ if (i > 0) console.log();
1155
+ const w = ordered[i];
1156
+ const wts = w.worktrees ?? [];
1157
+ const chip = w.isArchived === true ? ` ${dim(`[archived ${(w.archived_at ?? "").slice(0, 10)}]`)}` : "";
1158
+ const styledName = w.isArchived === true ? dim(w.name) : bold(w.name);
1159
+ console.log(`\u2022 ${styledName}${chip}`);
1160
+ if (w.description) {
1161
+ console.log(` ${dim(`\u2014 ${w.description}`)}`);
1162
+ }
1163
+ if (wts.length === 0) {
1164
+ console.log(` ${dim("(no worktrees)")}`);
1165
+ } else {
1166
+ const repoW = Math.max(...wts.map((t) => t.repo.length));
1167
+ for (const t of wts) {
1168
+ const repo = dim(t.repo.padEnd(repoW));
1169
+ const branch = dim(`[${t.branch}]`);
1170
+ const ports = Object.entries(t.ports ?? {}).map(([s, p]) => `${dim(`${s}:${p}`)}`).join(" ");
1171
+ const portsCol = ports ? ` ${ports}` : "";
1172
+ console.log(` ${repo} ${branch}${portsCol}`);
1173
+ }
1174
+ }
865
1175
  }
866
1176
  }, works);
867
1177
  return;
@@ -874,7 +1184,19 @@ function dispatchWork(positionals, flags) {
874
1184
  switch (action) {
875
1185
  case "info": {
876
1186
  const work = workInfo(root, name);
877
- emit(() => console.log(JSON.stringify(work, null, 2)), work);
1187
+ emit(() => {
1188
+ const archivedChip = work.isArchived === true ? ` ${dim(`[archived ${(work.archived_at ?? "").slice(0, 10)}]`)}` : "";
1189
+ const styledName = work.isArchived === true ? dim(work.name) : bold(work.name);
1190
+ console.log(`${styledName}${archivedChip}`);
1191
+ if (work.description) console.log(` ${dim("description")} ${dim(work.description)}`);
1192
+ const wts = work.worktrees ?? [];
1193
+ console.log(` ${dim("worktrees ")} ${dim(`${wts.length}`)}`);
1194
+ for (const wt of wts) {
1195
+ const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${dim(`${s}:`)}${dim(String(p))}`).join(" ");
1196
+ const portsCol = ports ? ` ${ports}` : "";
1197
+ console.log(` ${dim(wt.repo)} ${dim(`[${wt.branch}]`)}${portsCol}`);
1198
+ }
1199
+ }, work);
878
1200
  return;
879
1201
  }
880
1202
  case "path": {
@@ -885,7 +1207,7 @@ function dispatchWork(positionals, flags) {
885
1207
  case "describe": {
886
1208
  const text = need2(positionals[2], "usage: mx work -n <name> describe <text>");
887
1209
  const work = workDescribe(root, name, text);
888
- emit(() => console.log(`updated description of ${name}`), work);
1210
+ emit(() => console.log(`${check()} updated description of ${bold(name)}`), work);
889
1211
  return;
890
1212
  }
891
1213
  case "worktree":
@@ -895,33 +1217,50 @@ function dispatchWork(positionals, flags) {
895
1217
  case "destroy": {
896
1218
  if (flags.force && !flags.porcelain) {
897
1219
  process.stderr.write(
898
- `\u26A0 permanently removing work "${name}" \u2014 folder and any session summaries will be deleted (branches kept). This cannot be undone.
1220
+ `${warn()} ${dim(`permanently removing work "${name}" \u2014 folder and any session summaries will be deleted (branches kept). This cannot be undone.`)}
899
1221
  `
900
1222
  );
901
1223
  }
902
1224
  const res = workDestroy(root, name, { force: flags.force });
903
- emit(
904
- () => console.log(
905
- `destroyed work ${name} (worktrees removed: ${res.removedWorktrees.join(", ") || "none"}; branches kept)`
906
- ),
907
- res
908
- );
1225
+ emit(() => {
1226
+ const removed = res.removedWorktrees.join(", ") || "none";
1227
+ console.log(`${check()} destroyed work ${bold(name)}`);
1228
+ console.log(` ${dim(`worktrees removed: ${removed}; branches kept`)}`);
1229
+ }, res);
909
1230
  return;
910
1231
  }
911
1232
  case "archive": {
912
- 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
+ `);
1242
+ process.stderr.write(
1243
+ `${dim(` Worktrees will be removed; folder, work.json, branches, and sessions/ are preserved.`)}
1244
+ `
1245
+ );
913
1246
  process.stderr.write(
914
- `Reminder: write any pending session summary into works/${name}/sessions/ before archiving.
1247
+ `${dim(` Make sure any pending session summary is written into works/${name}/sessions/ first.`)}
915
1248
  `
916
1249
  );
1250
+ process.stderr.write("\n");
1251
+ if (!confirmYesNo("Proceed? (y/N) ")) {
1252
+ process.stderr.write(`${dim("Aborted.")}
1253
+ `);
1254
+ return;
1255
+ }
917
1256
  }
918
1257
  const res = archiveWork(root, name);
919
- emit(
920
- () => console.log(
921
- `archived work ${name} at ${res.archived_at} (worktrees removed: ${res.removedWorktrees.join(", ") || "none"}; branches kept)`
922
- ),
923
- res
924
- );
1258
+ emit(() => {
1259
+ const removed = res.removedWorktrees.join(", ") || "none";
1260
+ console.log(`${check()} archived work ${bold(name)}`);
1261
+ console.log(` ${dim(`at ${res.archived_at}`)}`);
1262
+ console.log(` ${dim(`worktrees removed: ${removed}; branches kept`)}`);
1263
+ }, res);
925
1264
  return;
926
1265
  }
927
1266
  case "unarchive": {
@@ -938,8 +1277,10 @@ function dispatchWork(positionals, flags) {
938
1277
  }
939
1278
  const res = unarchiveWork(root, name, overrides);
940
1279
  emit(() => {
941
- console.log(`unarchived work ${name}`);
942
- for (const r of res.restored) console.log(` ${r.repo} [${r.branch}] -> ${r.path}`);
1280
+ console.log(`${check()} unarchived work ${bold(name)}`);
1281
+ for (const r of res.restored) {
1282
+ console.log(` ${r.repo} ${dim(`[${r.branch}]`)} ${dim(`\u2192 ${r.path}`)}`);
1283
+ }
943
1284
  }, res);
944
1285
  return;
945
1286
  }
@@ -956,15 +1297,28 @@ function workWorktree(root, name, positionals, flags) {
956
1297
  "usage: mx work -n <name> worktree add <repo> [--branch <b>] [--base <ref>]"
957
1298
  );
958
1299
  const res = worktreeAdd(root, name, repo, { branch: flags.branch, base: flags.base });
959
- emit(() => console.log(`added worktree ${res.repo} [${res.branch}] -> ${res.path}`), res);
1300
+ emit(
1301
+ () => console.log(
1302
+ `${check()} added worktree ${bold(res.repo)} ${dim(`[${res.branch}]`)} ${dim(`\u2192 ${res.path}`)}`
1303
+ ),
1304
+ res
1305
+ );
960
1306
  return;
961
1307
  }
962
1308
  case "ls": {
963
1309
  const list = worktreeList(root, name);
964
1310
  emit(() => {
1311
+ if (list.length === 0) {
1312
+ console.log(dim("no worktrees yet \u2014 `mx work -n <name> worktree add <repo>`"));
1313
+ return;
1314
+ }
1315
+ const repoW = Math.max(...list.map((wt) => wt.repo.length));
965
1316
  for (const wt of list) {
966
- const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${s}:${p}`).join(", ");
967
- console.log(`${wt.repo} [${wt.branch}]${ports ? ` (${ports})` : ""}`);
1317
+ const repo = dim(wt.repo.padEnd(repoW));
1318
+ const branch = dim(`[${wt.branch}]`);
1319
+ const ports = Object.entries(wt.ports ?? {}).map(([s, p]) => `${dim(`${s}:`)}${dim(String(p))}`).join(" ");
1320
+ const portsCol = ports ? ` ${ports}` : "";
1321
+ console.log(`${repo} ${branch}${portsCol}`);
968
1322
  }
969
1323
  }, list);
970
1324
  return;
@@ -972,7 +1326,12 @@ function workWorktree(root, name, positionals, flags) {
972
1326
  case "rm": {
973
1327
  const repo = need2(positionals[3], "usage: mx work -n <name> worktree rm <repo>");
974
1328
  const res = worktreeRemove(root, name, repo);
975
- emit(() => console.log(`removed worktree ${res.repo} from ${name} (branch ${res.branch} kept)`), res);
1329
+ emit(
1330
+ () => console.log(
1331
+ `${check()} removed worktree ${bold(res.repo)} ${dim(`from ${name} (branch ${res.branch} kept)`)}`
1332
+ ),
1333
+ res
1334
+ );
976
1335
  return;
977
1336
  }
978
1337
  default:
@@ -993,7 +1352,12 @@ function workPort(root, name, positionals) {
993
1352
  if (!Number.isInteger(port)) throw new MxError(`invalid port: ${portArg}`, "BAD_ARGS");
994
1353
  }
995
1354
  const res = portSet(root, name, repo, service, port);
996
- emit(() => console.log(`${res.repo}.${res.service} -> ${res.port}`), res);
1355
+ emit(
1356
+ () => console.log(
1357
+ `${check()} ${res.repo}${dim(".")}${res.service} ${dim("\u2192")} ${dim(String(res.port))}`
1358
+ ),
1359
+ res
1360
+ );
997
1361
  return;
998
1362
  }
999
1363
  case "unset": {
@@ -1001,17 +1365,34 @@ function workPort(root, name, positionals) {
1001
1365
  const repo = need2(positionals[3], usage);
1002
1366
  const service = need2(positionals[4], usage);
1003
1367
  const res = portUnset(root, name, repo, service);
1004
- emit(() => console.log(`unset ${res.repo}.${res.service} (was ${res.released})`), res);
1368
+ emit(
1369
+ () => console.log(
1370
+ `${check()} unset ${res.repo}${dim(".")}${res.service} ${dim(`(was ${res.released})`)}`
1371
+ ),
1372
+ res
1373
+ );
1005
1374
  return;
1006
1375
  }
1007
1376
  case "ls": {
1008
1377
  const map = portList(root, name);
1009
1378
  emit(() => {
1379
+ const entries = [];
1010
1380
  for (const [repo, ports] of Object.entries(map)) {
1011
1381
  for (const [service, port] of Object.entries(ports)) {
1012
- console.log(`${repo}.${service} -> ${port}`);
1382
+ entries.push({ repo, service, port });
1013
1383
  }
1014
1384
  }
1385
+ if (entries.length === 0) {
1386
+ console.log(dim("no ports allocated yet \u2014 `mx work -n <name> port set <repo> <service>`"));
1387
+ return;
1388
+ }
1389
+ const lhsW = Math.max(...entries.map((e) => `${e.repo}.${e.service}`.length));
1390
+ for (const e of entries) {
1391
+ const lhs = `${e.repo}${dim(".")}${e.service}`;
1392
+ const plain = `${e.repo}.${e.service}`;
1393
+ const pad = " ".repeat(lhsW - plain.length);
1394
+ console.log(`${lhs}${pad} ${dim("\u2192")} ${dim(String(e.port))}`);
1395
+ }
1015
1396
  }, map);
1016
1397
  return;
1017
1398
  }
@@ -1022,13 +1403,14 @@ function workPort(root, name, positionals) {
1022
1403
 
1023
1404
  // src/main.ts
1024
1405
  var VERSION = (() => {
1025
- const here = path7.dirname(fileURLToPath2(import.meta.url));
1026
- const pkg = JSON.parse(readFileSync2(path7.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"));
1027
1408
  return pkg.version;
1028
1409
  })();
1029
1410
  function main() {
1030
1411
  const { positionals, flags } = parseArgs(process.argv.slice(2));
1031
1412
  setPorcelain(flags.porcelain);
1413
+ if (positionals[0] === "s" || positionals[0] === "st") positionals[0] = "status";
1032
1414
  try {
1033
1415
  if (flags.version || positionals[0] === "version") {
1034
1416
  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.2",
3
+ "version": "1.10.1",
4
4
  "description": "mx — run several features in parallel across shared repos using git worktrees",
5
5
  "type": "module",
6
6
  "bin": {