unity-hub-cli 0.1.0 → 0.2.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 +174 -38
- 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 twice";
|
|
533
|
+
var defaultHintMessage = "Move with arrows or j/k \xB7 Launch & exit with o \xB7 Copy cd path with c \xB7 Exit with Ctrl+C twice";
|
|
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,9 @@ 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(
|
|
564
|
+
const [hint, setHint] = useState(defaultHintMessage);
|
|
509
565
|
const [pendingExit, setPendingExit] = useState(false);
|
|
566
|
+
const [windowStart, setWindowStart] = useState(0);
|
|
510
567
|
const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
|
|
511
568
|
const sortedProjects = useMemo(() => {
|
|
512
569
|
const fallbackTime = 0;
|
|
@@ -561,7 +618,10 @@ var App = ({
|
|
|
561
618
|
setVisibleCount(minimumVisibleProjectCount);
|
|
562
619
|
return;
|
|
563
620
|
}
|
|
564
|
-
const
|
|
621
|
+
const borderRows = 2;
|
|
622
|
+
const hintRows = 1;
|
|
623
|
+
const hintMarginRows = 1;
|
|
624
|
+
const reservedRows = borderRows + hintRows + hintMarginRows;
|
|
565
625
|
const availableRows = stdout.rows - reservedRows;
|
|
566
626
|
const rowsPerProject = Math.max(linesPerProject, 1);
|
|
567
627
|
const calculatedCount = Math.max(
|
|
@@ -576,6 +636,7 @@ var App = ({
|
|
|
576
636
|
stdout?.off("resize", updateVisibleCount);
|
|
577
637
|
};
|
|
578
638
|
}, [linesPerProject, stdout]);
|
|
639
|
+
const limit = Math.max(minimumVisibleProjectCount, visibleCount);
|
|
579
640
|
const move = useCallback(
|
|
580
641
|
(delta) => {
|
|
581
642
|
setIndex((prev) => {
|
|
@@ -591,20 +652,102 @@ var App = ({
|
|
|
591
652
|
}
|
|
592
653
|
return next;
|
|
593
654
|
});
|
|
655
|
+
setWindowStart((prevStart) => {
|
|
656
|
+
if (sortedProjects.length <= limit) {
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
660
|
+
let nextStart = prevStart;
|
|
661
|
+
if (delta > 0) {
|
|
662
|
+
const nextIndex = Math.min(sortedProjects.length - 1, index + delta);
|
|
663
|
+
if (nextIndex >= prevStart + limit) {
|
|
664
|
+
nextStart = nextIndex - limit + 1;
|
|
665
|
+
}
|
|
666
|
+
} else if (delta < 0) {
|
|
667
|
+
const nextIndex = Math.max(0, index + delta);
|
|
668
|
+
if (nextIndex < prevStart) {
|
|
669
|
+
nextStart = nextIndex;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (nextStart < 0) {
|
|
673
|
+
nextStart = 0;
|
|
674
|
+
}
|
|
675
|
+
if (nextStart > maxStart) {
|
|
676
|
+
nextStart = maxStart;
|
|
677
|
+
}
|
|
678
|
+
return nextStart;
|
|
679
|
+
});
|
|
594
680
|
},
|
|
595
|
-
[sortedProjects.length]
|
|
681
|
+
[index, limit, sortedProjects.length]
|
|
596
682
|
);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
683
|
+
useEffect(() => {
|
|
684
|
+
setWindowStart((prevStart) => {
|
|
685
|
+
if (sortedProjects.length <= limit) {
|
|
686
|
+
return prevStart === 0 ? prevStart : 0;
|
|
687
|
+
}
|
|
688
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
689
|
+
let nextStart = prevStart;
|
|
690
|
+
if (index < prevStart) {
|
|
691
|
+
nextStart = index;
|
|
692
|
+
} else if (index >= prevStart + limit) {
|
|
693
|
+
nextStart = index - limit + 1;
|
|
694
|
+
}
|
|
695
|
+
if (nextStart < 0) {
|
|
696
|
+
nextStart = 0;
|
|
697
|
+
}
|
|
698
|
+
if (nextStart > maxStart) {
|
|
699
|
+
nextStart = maxStart;
|
|
700
|
+
}
|
|
701
|
+
return nextStart;
|
|
702
|
+
});
|
|
703
|
+
}, [index, limit, sortedProjects.length]);
|
|
704
|
+
const copyProjectPath = useCallback(() => {
|
|
705
|
+
const projectPath = sortedProjects[index]?.project.path;
|
|
706
|
+
if (!projectPath) {
|
|
707
|
+
setHint("No project to copy");
|
|
708
|
+
setTimeout(() => {
|
|
709
|
+
setHint(defaultHintMessage);
|
|
710
|
+
}, 2e3);
|
|
600
711
|
return;
|
|
601
712
|
}
|
|
602
713
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
714
|
+
const command = buildCdCommand(projectPath);
|
|
715
|
+
clipboard.writeSync(command);
|
|
716
|
+
const displayPath = shortenHomePath(projectPath);
|
|
717
|
+
setHint(`Copied command: cd "${displayPath}"`);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
720
|
+
setHint(`Failed to copy: ${message}`);
|
|
721
|
+
}
|
|
722
|
+
setTimeout(() => {
|
|
723
|
+
setHint(defaultHintMessage);
|
|
724
|
+
}, 2e3);
|
|
725
|
+
}, [index, sortedProjects]);
|
|
726
|
+
const launchSelectedAndExit = useCallback(async () => {
|
|
727
|
+
const projectView = sortedProjects[index];
|
|
728
|
+
if (!projectView) {
|
|
729
|
+
setHint("No project to launch");
|
|
605
730
|
setTimeout(() => {
|
|
606
731
|
setHint(defaultHintMessage);
|
|
607
732
|
}, 2e3);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const { project } = projectView;
|
|
736
|
+
try {
|
|
737
|
+
const command = buildCdCommand(project.path);
|
|
738
|
+
clipboard.writeSync(command);
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
741
|
+
setHint(`Failed to copy: ${message}`);
|
|
742
|
+
setTimeout(() => {
|
|
743
|
+
setHint(defaultHintMessage);
|
|
744
|
+
}, 3e3);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
await onLaunch(project);
|
|
749
|
+
stdout?.write("\x1B[2J\x1B[H");
|
|
750
|
+
exit();
|
|
608
751
|
} catch (error) {
|
|
609
752
|
if (error instanceof LaunchCancelledError) {
|
|
610
753
|
setHint("Launch cancelled");
|
|
@@ -619,7 +762,7 @@ var App = ({
|
|
|
619
762
|
setHint(defaultHintMessage);
|
|
620
763
|
}, 3e3);
|
|
621
764
|
}
|
|
622
|
-
}, [index, onLaunch, sortedProjects]);
|
|
765
|
+
}, [exit, index, onLaunch, sortedProjects, stdout]);
|
|
623
766
|
useInput((input, key) => {
|
|
624
767
|
if (input === "j" || key.downArrow) {
|
|
625
768
|
move(1);
|
|
@@ -628,34 +771,27 @@ var App = ({
|
|
|
628
771
|
move(-1);
|
|
629
772
|
}
|
|
630
773
|
if (input === "o") {
|
|
631
|
-
void
|
|
774
|
+
void launchSelectedAndExit();
|
|
775
|
+
}
|
|
776
|
+
if (input === "c") {
|
|
777
|
+
copyProjectPath();
|
|
632
778
|
}
|
|
633
779
|
});
|
|
634
780
|
const { startIndex, visibleProjects } = useMemo(() => {
|
|
635
|
-
const limit = Math.max(minimumVisibleProjectCount, visibleCount);
|
|
636
781
|
if (sortedProjects.length <= limit) {
|
|
637
782
|
return {
|
|
638
783
|
startIndex: 0,
|
|
639
|
-
endIndex: sortedProjects.length,
|
|
640
784
|
visibleProjects: sortedProjects
|
|
641
785
|
};
|
|
642
786
|
}
|
|
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
|
-
}
|
|
787
|
+
const maxStart = Math.max(0, sortedProjects.length - limit);
|
|
788
|
+
const clampedStart = Math.min(Math.max(0, windowStart), maxStart);
|
|
789
|
+
const end = Math.min(clampedStart + limit, sortedProjects.length);
|
|
654
790
|
return {
|
|
655
|
-
startIndex:
|
|
656
|
-
visibleProjects: sortedProjects.slice(
|
|
791
|
+
startIndex: clampedStart,
|
|
792
|
+
visibleProjects: sortedProjects.slice(clampedStart, end)
|
|
657
793
|
};
|
|
658
|
-
}, [
|
|
794
|
+
}, [limit, sortedProjects, windowStart]);
|
|
659
795
|
const scrollbarChars = useMemo(() => {
|
|
660
796
|
const totalProjects = projects.length;
|
|
661
797
|
const totalLines = totalProjects * linesPerProject;
|
|
@@ -688,7 +824,9 @@ var App = ({
|
|
|
688
824
|
const rowIndex = startIndex + offset;
|
|
689
825
|
const isSelected = rowIndex === index;
|
|
690
826
|
const arrow = isSelected ? ">" : " ";
|
|
691
|
-
const
|
|
827
|
+
const projectName = formatProjectName(project, repository, useGitRootName);
|
|
828
|
+
const versionLabel = `(${project.version.value})`;
|
|
829
|
+
const updatedText = formatUpdatedText(project.lastModified);
|
|
692
830
|
const pathLine = shortenHomePath(project.path);
|
|
693
831
|
const branchLine = formatBranch(repository?.branch);
|
|
694
832
|
const baseScrollbarIndex = offset * linesPerProject;
|
|
@@ -696,21 +834,19 @@ var App = ({
|
|
|
696
834
|
const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
|
|
697
835
|
const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
|
|
698
836
|
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
837
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
703
838
|
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
|
|
704
839
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
705
840
|
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
|
|
706
841
|
arrow,
|
|
707
842
|
" ",
|
|
708
|
-
|
|
843
|
+
projectName
|
|
709
844
|
] }),
|
|
710
|
-
|
|
845
|
+
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
|
|
711
846
|
" ",
|
|
712
|
-
|
|
713
|
-
] })
|
|
847
|
+
versionLabel
|
|
848
|
+
] }),
|
|
849
|
+
updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null
|
|
714
850
|
] }),
|
|
715
851
|
showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
|
|
716
852
|
" ",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unity-hub-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|