unity-hub-cli 0.1.0 → 0.3.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/README.md +4 -2
- package/dist/index.js +177 -47
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# UnityHub CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[日本語版はこちら / Japanese version](README_ja.md)
|
|
4
|
+
|
|
5
|
+
A CLI tool that displays the same content as Unity Hub in an Ink-based TUI, allows navigation with arrow keys/`j`/`k`, and launches Unity Editor by pressing `o`.
|
|
4
6
|
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
@@ -83,7 +85,7 @@ npm audit signatures
|
|
|
83
85
|
## Controls
|
|
84
86
|
|
|
85
87
|
- Arrow keys / `j` / `k`: Navigate selection
|
|
86
|
-
|
|
88
|
+
- `o`: Launch selected project in Unity
|
|
87
89
|
- Ctrl + C (twice): Exit
|
|
88
90
|
|
|
89
91
|
The display includes Git branch (if present), Unity version, project path, and last modified time (`lastModified`).
|
package/dist/index.js
CHANGED
|
@@ -443,6 +443,7 @@ var UnityLockChecker = class {
|
|
|
443
443
|
};
|
|
444
444
|
|
|
445
445
|
// src/presentation/App.tsx
|
|
446
|
+
import clipboard from "clipboardy";
|
|
446
447
|
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
447
448
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
448
449
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -458,13 +459,13 @@ var extractRootFolder = (repository) => {
|
|
|
458
459
|
};
|
|
459
460
|
var formatProjectName = (project, repository, useGitRootName) => {
|
|
460
461
|
if (!useGitRootName) {
|
|
461
|
-
return
|
|
462
|
+
return project.title;
|
|
462
463
|
}
|
|
463
464
|
const rootFolder = extractRootFolder(repository);
|
|
464
465
|
if (!rootFolder) {
|
|
465
|
-
return
|
|
466
|
+
return project.title;
|
|
466
467
|
}
|
|
467
|
-
return
|
|
468
|
+
return rootFolder;
|
|
468
469
|
};
|
|
469
470
|
var formatBranch = (branch) => {
|
|
470
471
|
if (!branch) {
|
|
@@ -475,10 +476,61 @@ var formatBranch = (branch) => {
|
|
|
475
476
|
}
|
|
476
477
|
return `detached@${branch.sha}`;
|
|
477
478
|
};
|
|
479
|
+
var formatRelativeTime = (lastModified) => {
|
|
480
|
+
if (!lastModified) {
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
const now = /* @__PURE__ */ new Date();
|
|
484
|
+
const diffMillis = now.getTime() - lastModified.getTime();
|
|
485
|
+
if (Number.isNaN(diffMillis)) {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
const safeMillis = Math.max(0, diffMillis);
|
|
489
|
+
const secondsPerMinute = 60;
|
|
490
|
+
const secondsPerHour = secondsPerMinute * 60;
|
|
491
|
+
const secondsPerDay = secondsPerHour * 24;
|
|
492
|
+
const secondsPerMonth = secondsPerDay * 30;
|
|
493
|
+
const secondsPerYear = secondsPerDay * 365;
|
|
494
|
+
const totalSeconds = Math.floor(safeMillis / 1e3);
|
|
495
|
+
if (totalSeconds < 45) {
|
|
496
|
+
return "a few seconds ago";
|
|
497
|
+
}
|
|
498
|
+
if (totalSeconds < 45 * secondsPerMinute) {
|
|
499
|
+
const minutes = Math.max(1, Math.floor(totalSeconds / secondsPerMinute));
|
|
500
|
+
const suffix2 = minutes === 1 ? "minute" : "minutes";
|
|
501
|
+
return `${minutes} ${suffix2} ago`;
|
|
502
|
+
}
|
|
503
|
+
if (totalSeconds < 30 * secondsPerHour) {
|
|
504
|
+
const hours = Math.max(1, Math.floor(totalSeconds / secondsPerHour));
|
|
505
|
+
const suffix2 = hours === 1 ? "hour" : "hours";
|
|
506
|
+
return `${hours} ${suffix2} ago`;
|
|
507
|
+
}
|
|
508
|
+
if (totalSeconds < secondsPerMonth) {
|
|
509
|
+
const days = Math.max(1, Math.floor(totalSeconds / secondsPerDay));
|
|
510
|
+
const suffix2 = days === 1 ? "day" : "days";
|
|
511
|
+
return `${days} ${suffix2} ago`;
|
|
512
|
+
}
|
|
513
|
+
if (totalSeconds < secondsPerYear) {
|
|
514
|
+
const months = Math.max(1, Math.floor(totalSeconds / secondsPerMonth));
|
|
515
|
+
const suffix2 = months === 1 ? "month" : "months";
|
|
516
|
+
return `${months} ${suffix2} ago`;
|
|
517
|
+
}
|
|
518
|
+
const years = Math.max(1, Math.floor(totalSeconds / secondsPerYear));
|
|
519
|
+
const suffix = years === 1 ? "year" : "years";
|
|
520
|
+
return `${years} ${suffix} ago`;
|
|
521
|
+
};
|
|
522
|
+
var UPDATED_LABEL = "Last:";
|
|
523
|
+
var formatUpdatedText = (lastModified) => {
|
|
524
|
+
const relativeTime = formatRelativeTime(lastModified);
|
|
525
|
+
if (!relativeTime) {
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
return `${UPDATED_LABEL} ${relativeTime}`;
|
|
529
|
+
};
|
|
478
530
|
var homeDirectory = process.env.HOME ?? "";
|
|
479
531
|
var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
|
|
480
532
|
var minimumVisibleProjectCount = 4;
|
|
481
|
-
var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Exit with Ctrl+C
|
|
533
|
+
var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Copy cd path with c \xB7 Exit with Ctrl+C";
|
|
482
534
|
var PROJECT_COLOR = "#abd8e7";
|
|
483
535
|
var BRANCH_COLOR = "#e3839c";
|
|
484
536
|
var PATH_COLOR = "#719bd8";
|
|
@@ -494,6 +546,10 @@ var shortenHomePath = (targetPath) => {
|
|
|
494
546
|
}
|
|
495
547
|
return targetPath;
|
|
496
548
|
};
|
|
549
|
+
var buildCdCommand = (targetPath) => {
|
|
550
|
+
const escapedPath = targetPath.replaceAll('"', '\\"');
|
|
551
|
+
return `cd "${escapedPath}"`;
|
|
552
|
+
};
|
|
497
553
|
var App = ({
|
|
498
554
|
projects,
|
|
499
555
|
onLaunch,
|
|
@@ -505,8 +561,8 @@ var App = ({
|
|
|
505
561
|
const { stdout } = useStdout();
|
|
506
562
|
const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
|
|
507
563
|
const [index, setIndex] = useState(0);
|
|
508
|
-
const [hint, setHint] = useState(
|
|
509
|
-
const [
|
|
564
|
+
const [hint, setHint] = useState(defaultHintMessage);
|
|
565
|
+
const [windowStart, setWindowStart] = useState(0);
|
|
510
566
|
const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
|
|
511
567
|
const sortedProjects = useMemo(() => {
|
|
512
568
|
const fallbackTime = 0;
|
|
@@ -539,29 +595,23 @@ var App = ({
|
|
|
539
595
|
}, [projects, useGitRootName]);
|
|
540
596
|
useEffect(() => {
|
|
541
597
|
const handleSigint = () => {
|
|
542
|
-
if (!pendingExit) {
|
|
543
|
-
setPendingExit(true);
|
|
544
|
-
setHint("Press Ctrl+C again to exit");
|
|
545
|
-
setTimeout(() => {
|
|
546
|
-
setPendingExit(false);
|
|
547
|
-
setHint(defaultHintMessage);
|
|
548
|
-
}, 2e3);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
598
|
exit();
|
|
552
599
|
};
|
|
553
600
|
process.on("SIGINT", handleSigint);
|
|
554
601
|
return () => {
|
|
555
602
|
process.off("SIGINT", handleSigint);
|
|
556
603
|
};
|
|
557
|
-
}, [exit
|
|
604
|
+
}, [exit]);
|
|
558
605
|
useEffect(() => {
|
|
559
606
|
const updateVisibleCount = () => {
|
|
560
607
|
if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
|
|
561
608
|
setVisibleCount(minimumVisibleProjectCount);
|
|
562
609
|
return;
|
|
563
610
|
}
|
|
564
|
-
const
|
|
611
|
+
const borderRows = 2;
|
|
612
|
+
const hintRows = 1;
|
|
613
|
+
const hintMarginRows = 1;
|
|
614
|
+
const reservedRows = borderRows + hintRows + hintMarginRows;
|
|
565
615
|
const availableRows = stdout.rows - reservedRows;
|
|
566
616
|
const rowsPerProject = Math.max(linesPerProject, 1);
|
|
567
617
|
const calculatedCount = Math.max(
|
|
@@ -576,6 +626,7 @@ var App = ({
|
|
|
576
626
|
stdout?.off("resize", updateVisibleCount);
|
|
577
627
|
};
|
|
578
628
|
}, [linesPerProject, stdout]);
|
|
629
|
+
const limit = Math.max(minimumVisibleProjectCount, visibleCount);
|
|
579
630
|
const move = useCallback(
|
|
580
631
|
(delta) => {
|
|
581
632
|
setIndex((prev) => {
|
|
@@ -591,20 +642,104 @@ var App = ({
|
|
|
591
642
|
}
|
|
592
643
|
return next;
|
|
593
644
|
});
|
|
645
|
+
setWindowStart((prevStart) => {
|
|
646
|
+
if (sortedProjects.length <= limit) {
|
|
647
|
+
return 0;
|
|
648
|
+
}
|
|
649
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
650
|
+
let nextStart = prevStart;
|
|
651
|
+
if (delta > 0) {
|
|
652
|
+
const nextIndex = Math.min(sortedProjects.length - 1, index + delta);
|
|
653
|
+
if (nextIndex >= prevStart + limit) {
|
|
654
|
+
nextStart = nextIndex - limit + 1;
|
|
655
|
+
}
|
|
656
|
+
} else if (delta < 0) {
|
|
657
|
+
const nextIndex = Math.max(0, index + delta);
|
|
658
|
+
if (nextIndex < prevStart) {
|
|
659
|
+
nextStart = nextIndex;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (nextStart < 0) {
|
|
663
|
+
nextStart = 0;
|
|
664
|
+
}
|
|
665
|
+
if (nextStart > maxStart) {
|
|
666
|
+
nextStart = maxStart;
|
|
667
|
+
}
|
|
668
|
+
return nextStart;
|
|
669
|
+
});
|
|
594
670
|
},
|
|
595
|
-
[sortedProjects.length]
|
|
671
|
+
[index, limit, sortedProjects.length]
|
|
596
672
|
);
|
|
673
|
+
useEffect(() => {
|
|
674
|
+
setWindowStart((prevStart) => {
|
|
675
|
+
if (sortedProjects.length <= limit) {
|
|
676
|
+
return prevStart === 0 ? prevStart : 0;
|
|
677
|
+
}
|
|
678
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
679
|
+
let nextStart = prevStart;
|
|
680
|
+
if (index < prevStart) {
|
|
681
|
+
nextStart = index;
|
|
682
|
+
} else if (index >= prevStart + limit) {
|
|
683
|
+
nextStart = index - limit + 1;
|
|
684
|
+
}
|
|
685
|
+
if (nextStart < 0) {
|
|
686
|
+
nextStart = 0;
|
|
687
|
+
}
|
|
688
|
+
if (nextStart > maxStart) {
|
|
689
|
+
nextStart = maxStart;
|
|
690
|
+
}
|
|
691
|
+
return nextStart;
|
|
692
|
+
});
|
|
693
|
+
}, [index, limit, sortedProjects.length]);
|
|
694
|
+
const copyProjectPath = useCallback(() => {
|
|
695
|
+
const projectPath = sortedProjects[index]?.project.path;
|
|
696
|
+
if (!projectPath) {
|
|
697
|
+
setHint("No project to copy");
|
|
698
|
+
setTimeout(() => {
|
|
699
|
+
setHint(defaultHintMessage);
|
|
700
|
+
}, 2e3);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const command = buildCdCommand(projectPath);
|
|
705
|
+
clipboard.writeSync(command);
|
|
706
|
+
const displayPath = shortenHomePath(projectPath);
|
|
707
|
+
setHint(`Copied command: cd "${displayPath}"`);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
710
|
+
setHint(`Failed to copy: ${message}`);
|
|
711
|
+
}
|
|
712
|
+
setTimeout(() => {
|
|
713
|
+
setHint(defaultHintMessage);
|
|
714
|
+
}, 2e3);
|
|
715
|
+
}, [index, sortedProjects]);
|
|
597
716
|
const launchSelected = useCallback(async () => {
|
|
598
|
-
const
|
|
599
|
-
if (!
|
|
717
|
+
const projectView = sortedProjects[index];
|
|
718
|
+
if (!projectView) {
|
|
719
|
+
setHint("No project to launch");
|
|
720
|
+
setTimeout(() => {
|
|
721
|
+
setHint(defaultHintMessage);
|
|
722
|
+
}, 2e3);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const { project } = projectView;
|
|
726
|
+
try {
|
|
727
|
+
const command = buildCdCommand(project.path);
|
|
728
|
+
clipboard.writeSync(command);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
731
|
+
setHint(`Failed to copy: ${message}`);
|
|
732
|
+
setTimeout(() => {
|
|
733
|
+
setHint(defaultHintMessage);
|
|
734
|
+
}, 3e3);
|
|
600
735
|
return;
|
|
601
736
|
}
|
|
602
737
|
try {
|
|
603
738
|
await onLaunch(project);
|
|
604
|
-
setHint(`Launched
|
|
739
|
+
setHint(`Launched: ${project.title}`);
|
|
605
740
|
setTimeout(() => {
|
|
606
741
|
setHint(defaultHintMessage);
|
|
607
|
-
},
|
|
742
|
+
}, 3e3);
|
|
608
743
|
} catch (error) {
|
|
609
744
|
if (error instanceof LaunchCancelledError) {
|
|
610
745
|
setHint("Launch cancelled");
|
|
@@ -630,32 +765,25 @@ var App = ({
|
|
|
630
765
|
if (input === "o") {
|
|
631
766
|
void launchSelected();
|
|
632
767
|
}
|
|
768
|
+
if (input === "c") {
|
|
769
|
+
copyProjectPath();
|
|
770
|
+
}
|
|
633
771
|
});
|
|
634
772
|
const { startIndex, visibleProjects } = useMemo(() => {
|
|
635
|
-
const limit = Math.max(minimumVisibleProjectCount, visibleCount);
|
|
636
773
|
if (sortedProjects.length <= limit) {
|
|
637
774
|
return {
|
|
638
775
|
startIndex: 0,
|
|
639
|
-
endIndex: sortedProjects.length,
|
|
640
776
|
visibleProjects: sortedProjects
|
|
641
777
|
};
|
|
642
778
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (start < 0) {
|
|
647
|
-
start = 0;
|
|
648
|
-
end = limit;
|
|
649
|
-
}
|
|
650
|
-
if (end > sortedProjects.length) {
|
|
651
|
-
end = sortedProjects.length;
|
|
652
|
-
start = Math.max(0, end - limit);
|
|
653
|
-
}
|
|
779
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
780
|
+
const clampedStart = Math.min(Math.max(0, windowStart), maxStart);
|
|
781
|
+
const end = Math.min(clampedStart + limit, sortedProjects.length);
|
|
654
782
|
return {
|
|
655
|
-
startIndex:
|
|
656
|
-
visibleProjects: sortedProjects.slice(
|
|
783
|
+
startIndex: clampedStart,
|
|
784
|
+
visibleProjects: sortedProjects.slice(clampedStart, end)
|
|
657
785
|
};
|
|
658
|
-
}, [
|
|
786
|
+
}, [limit, sortedProjects, windowStart]);
|
|
659
787
|
const scrollbarChars = useMemo(() => {
|
|
660
788
|
const totalProjects = projects.length;
|
|
661
789
|
const totalLines = totalProjects * linesPerProject;
|
|
@@ -688,7 +816,9 @@ var App = ({
|
|
|
688
816
|
const rowIndex = startIndex + offset;
|
|
689
817
|
const isSelected = rowIndex === index;
|
|
690
818
|
const arrow = isSelected ? ">" : " ";
|
|
691
|
-
const
|
|
819
|
+
const projectName = formatProjectName(project, repository, useGitRootName);
|
|
820
|
+
const versionLabel = `(${project.version.value})`;
|
|
821
|
+
const updatedText = formatUpdatedText(project.lastModified);
|
|
692
822
|
const pathLine = shortenHomePath(project.path);
|
|
693
823
|
const branchLine = formatBranch(repository?.branch);
|
|
694
824
|
const baseScrollbarIndex = offset * linesPerProject;
|
|
@@ -696,21 +826,19 @@ var App = ({
|
|
|
696
826
|
const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
|
|
697
827
|
const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
|
|
698
828
|
const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
|
|
699
|
-
const versionStart = titleLine.indexOf("(");
|
|
700
|
-
const versionText = versionStart >= 0 ? titleLine.slice(versionStart) : "";
|
|
701
|
-
const nameText = versionStart >= 0 ? titleLine.slice(0, versionStart).trimEnd() : titleLine;
|
|
702
829
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
703
830
|
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
|
|
704
831
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
705
832
|
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
|
|
706
833
|
arrow,
|
|
707
834
|
" ",
|
|
708
|
-
|
|
835
|
+
projectName
|
|
709
836
|
] }),
|
|
710
|
-
|
|
837
|
+
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
|
|
711
838
|
" ",
|
|
712
|
-
|
|
713
|
-
] })
|
|
839
|
+
versionLabel
|
|
840
|
+
] }),
|
|
841
|
+
updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null
|
|
714
842
|
] }),
|
|
715
843
|
showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
|
|
716
844
|
" ",
|
|
@@ -762,7 +890,7 @@ var bootstrap = async () => {
|
|
|
762
890
|
const showPath = !process2.argv.includes("--hide-path");
|
|
763
891
|
try {
|
|
764
892
|
const projects = await listProjectsUseCase.execute();
|
|
765
|
-
render(
|
|
893
|
+
const { waitUntilExit } = render(
|
|
766
894
|
/* @__PURE__ */ jsx2(
|
|
767
895
|
App,
|
|
768
896
|
{
|
|
@@ -774,6 +902,8 @@ var bootstrap = async () => {
|
|
|
774
902
|
}
|
|
775
903
|
)
|
|
776
904
|
);
|
|
905
|
+
await waitUntilExit();
|
|
906
|
+
process2.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
777
907
|
} catch (error) {
|
|
778
908
|
const message = error instanceof Error ? error.message : String(error);
|
|
779
909
|
console.error(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unity-hub-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A CLI tool that reads Unity Hub's projects and launches Unity Editor with an interactive TUI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -36,9 +36,13 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"clipboardy": "^4.0.0",
|
|
39
40
|
"ink": "^4.4.1",
|
|
40
41
|
"react": "^18.3.1"
|
|
41
42
|
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
42
46
|
"devDependencies": {
|
|
43
47
|
"@types/node": "^20.19.20",
|
|
44
48
|
"@types/react": "^18.3.26",
|