@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.
- package/bin/mx.js +492 -110
- 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
|
|
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
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
|
|
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.
|
|
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]
|
|
655
|
-
mx update re-
|
|
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
|
|
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
|
|
830
|
+
import * as path8 from "path";
|
|
690
831
|
|
|
691
832
|
// src/paths.ts
|
|
692
|
-
import * as
|
|
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 =
|
|
697
|
-
return
|
|
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 ?
|
|
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 ["",
|
|
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
|
-
|
|
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(
|
|
725
|
-
for (const c of res.created) console.log(`
|
|
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(
|
|
757
|
-
for (const p of res.updated) console.log(`
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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(() =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
()
|
|
905
|
-
|
|
906
|
-
)
|
|
907
|
-
|
|
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.
|
|
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
|
-
`
|
|
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
|
-
()
|
|
921
|
-
|
|
922
|
-
)
|
|
923
|
-
|
|
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(
|
|
942
|
-
for (const r of res.restored)
|
|
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(
|
|
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
|
|
967
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1026
|
-
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"));
|
|
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 });
|