@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 +260 -32
- package/package.json +1 -1
- package/templates/CLAUDE.md +16 -4
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
657
|
+
import * as path6 from "path";
|
|
570
658
|
function countContextEntries(root) {
|
|
571
|
-
const indexPath =
|
|
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
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
830
|
+
import * as path8 from "path";
|
|
718
831
|
|
|
719
832
|
// src/paths.ts
|
|
720
|
-
import * as
|
|
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 =
|
|
725
|
-
return
|
|
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 ?
|
|
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
|
|
798
|
-
const
|
|
799
|
-
const
|
|
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
|
-
|
|
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 = [...
|
|
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:
|
|
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.
|
|
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
|
-
`${
|
|
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 =
|
|
1179
|
-
const pkg = JSON.parse(readFileSync2(
|
|
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
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|