unity-hub-cli 0.6.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +130 -143
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,28 +6,42 @@ import { render } from "ink";
6
6
 
7
7
  // src/application/usecases.ts
8
8
  var ListProjectsUseCase = class {
9
- constructor(unityHubProjectsReader, gitRepositoryInfoReader, unityProjectOptionsReader, lockReader) {
9
+ constructor(unityHubProjectsReader, gitRepositoryInfoReader, unityProjectOptionsReader, lockReader, unityProcessReader) {
10
10
  this.unityHubProjectsReader = unityHubProjectsReader;
11
11
  this.gitRepositoryInfoReader = gitRepositoryInfoReader;
12
12
  this.unityProjectOptionsReader = unityProjectOptionsReader;
13
13
  this.lockReader = lockReader;
14
+ this.unityProcessReader = unityProcessReader;
14
15
  }
15
16
  async execute() {
16
17
  const projects = await this.unityHubProjectsReader.listProjects();
17
- const [repositoryInfoResults, lockResults] = await Promise.all([
18
+ const [repositoryInfoResults, lockResults, processResults] = await Promise.all([
18
19
  Promise.allSettled(
19
20
  projects.map((project) => this.gitRepositoryInfoReader.readRepositoryInfo(project.path))
20
21
  ),
21
- Promise.allSettled(projects.map((project) => this.lockReader.isLocked(project.path)))
22
+ Promise.allSettled(projects.map((project) => this.lockReader.isLocked(project.path))),
23
+ Promise.allSettled(projects.map((project) => this.unityProcessReader.findByProjectPath(project.path)))
22
24
  ]);
23
25
  return projects.map((project, index) => {
24
26
  const repositoryResult = repositoryInfoResults[index];
25
27
  const lockResult = lockResults[index];
28
+ const processResult = processResults[index];
26
29
  const repository = repositoryResult.status === "fulfilled" ? repositoryResult.value ?? void 0 : void 0;
27
30
  const isLocked = lockResult.status === "fulfilled" ? Boolean(lockResult.value) : false;
28
- return { project, repository, isLocked };
31
+ const hasRunningProcess = processResult.status === "fulfilled" ? Boolean(processResult.value) : false;
32
+ const launchStatus = this.determineLaunchStatus(hasRunningProcess, isLocked);
33
+ return { project, repository, isLocked, launchStatus };
29
34
  });
30
35
  }
36
+ determineLaunchStatus(hasRunningProcess, isLocked) {
37
+ if (hasRunningProcess) {
38
+ return "running";
39
+ }
40
+ if (isLocked) {
41
+ return "crashed";
42
+ }
43
+ return "idle";
44
+ }
31
45
  };
32
46
  var LaunchCancelledError = class extends Error {
33
47
  constructor() {
@@ -58,10 +72,9 @@ var LaunchProjectUseCase = class {
58
72
  }
59
73
  };
60
74
  var TerminateProjectUseCase = class {
61
- constructor(unityProcessReader, unityProcessTerminator, unityTempDirectoryCleaner) {
75
+ constructor(unityProcessReader, unityProcessTerminator) {
62
76
  this.unityProcessReader = unityProcessReader;
63
77
  this.unityProcessTerminator = unityProcessTerminator;
64
- this.unityTempDirectoryCleaner = unityTempDirectoryCleaner;
65
78
  }
66
79
  async execute(project) {
67
80
  const unityProcess = await this.unityProcessReader.findByProjectPath(project.path);
@@ -78,17 +91,8 @@ var TerminateProjectUseCase = class {
78
91
  message: "Failed to terminate the Unity process."
79
92
  };
80
93
  }
81
- let cleanupMessage = void 0;
82
- try {
83
- await this.unityTempDirectoryCleaner.clean(project.path);
84
- } catch (error) {
85
- const message = error instanceof Error ? error.message : String(error);
86
- console.error("Failed to clean Unity Temp directory:", message);
87
- cleanupMessage = `Unity terminated, but failed to clean Temp: ${message}`;
88
- }
89
94
  return {
90
- terminated: true,
91
- message: cleanupMessage
95
+ terminated: true
92
96
  };
93
97
  }
94
98
  };
@@ -348,117 +352,14 @@ var UnityHubProjectsReader = class {
348
352
 
349
353
  // src/infrastructure/unityLock.ts
350
354
  import { execFile } from "child_process";
351
- import { constants as constants2, createReadStream, createWriteStream } from "fs";
355
+ import { constants as constants2 } from "fs";
352
356
  import { access as access2, rm } from "fs/promises";
353
357
  import { join as join3 } from "path";
354
- import readline from "readline";
355
358
  import { promisify } from "util";
356
- var RAW_PROMPT_MESSAGE = "Delete UnityLockfile and continue? Type 'y' to continue; anything else aborts: ";
357
359
  var execFileAsync = promisify(execFile);
358
360
  var buildBringToFrontScript = (pid) => {
359
361
  return `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
360
362
  };
361
- var isRawModeSupported = () => {
362
- const stdin = process.stdin;
363
- return Boolean(stdin?.isTTY && typeof stdin.setRawMode === "function" && process.stdout.isTTY);
364
- };
365
- var createPromptInterface = () => {
366
- if (process.stdin.isTTY && process.stdout.isTTY) {
367
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
368
- const close = () => rl.close();
369
- return { rl, close };
370
- }
371
- try {
372
- if (process.platform === "win32") {
373
- const inCandidates = ["\\\\.\\CONIN$", "CONIN$"];
374
- const outCandidates = ["\\\\.\\CONOUT$", "CONOUT$"];
375
- for (const inPath of inCandidates) {
376
- for (const outPath of outCandidates) {
377
- try {
378
- const input = createReadStream(inPath);
379
- const output = createWriteStream(outPath);
380
- const rl = readline.createInterface({ input, output });
381
- const close = () => {
382
- rl.close();
383
- input.destroy();
384
- output.end();
385
- };
386
- return { rl, close };
387
- } catch {
388
- continue;
389
- }
390
- }
391
- }
392
- } else {
393
- const input = createReadStream("/dev/tty");
394
- const output = createWriteStream("/dev/tty");
395
- const rl = readline.createInterface({ input, output });
396
- const close = () => {
397
- rl.close();
398
- input.destroy();
399
- output.end();
400
- };
401
- return { rl, close };
402
- }
403
- } catch {
404
- return void 0;
405
- }
406
- return void 0;
407
- };
408
- var promptYesNoSingleKey = async () => {
409
- const stdin = process.stdin;
410
- const supportsRaw = isRawModeSupported();
411
- const previousRaw = supportsRaw ? stdin.isRaw === true : false;
412
- const wasPaused = stdin.isPaused();
413
- return await new Promise((resolve3) => {
414
- const cleanup = () => {
415
- stdin.removeListener("data", handleData);
416
- if (wasPaused) {
417
- stdin.pause();
418
- }
419
- if (supportsRaw) {
420
- stdin.setRawMode(previousRaw);
421
- }
422
- };
423
- const handleData = (data) => {
424
- const char = data.toString();
425
- const firstByte = data[0] ?? 0;
426
- let result = false;
427
- if (char === "y") {
428
- result = true;
429
- } else if (char === "n" || char === "N" || firstByte === 3 || firstByte === 27 || firstByte === 13) {
430
- result = false;
431
- } else {
432
- result = false;
433
- }
434
- process.stdout.write("\n");
435
- cleanup();
436
- resolve3(result);
437
- };
438
- process.stdout.write(RAW_PROMPT_MESSAGE);
439
- if (supportsRaw) {
440
- stdin.setRawMode(true);
441
- }
442
- if (wasPaused) {
443
- stdin.resume();
444
- }
445
- stdin.once("data", handleData);
446
- });
447
- };
448
- var promptYesNoLine = async () => {
449
- const prompt = createPromptInterface();
450
- if (!prompt) {
451
- console.error("UnityLockfile exists. No interactive console available for confirmation.");
452
- return false;
453
- }
454
- const confirmed = await new Promise((resolve3) => {
455
- prompt.rl.question(RAW_PROMPT_MESSAGE, (answer) => {
456
- resolve3(answer.trim() === "y");
457
- });
458
- });
459
- prompt.close();
460
- return confirmed;
461
- };
462
363
  var pathExists = async (target) => {
463
364
  try {
464
365
  await access2(target, constants2.F_OK);
@@ -468,8 +369,9 @@ var pathExists = async (target) => {
468
369
  }
469
370
  };
470
371
  var UnityLockChecker = class {
471
- constructor(unityProcessReader) {
372
+ constructor(unityProcessReader, tempDirectoryCleaner) {
472
373
  this.unityProcessReader = unityProcessReader;
374
+ this.tempDirectoryCleaner = tempDirectoryCleaner;
473
375
  }
474
376
  async check(projectPath) {
475
377
  const activeProcess = await this.unityProcessReader.findByProjectPath(projectPath);
@@ -485,15 +387,22 @@ var UnityLockChecker = class {
485
387
  if (!hasLockfile) {
486
388
  return "allow";
487
389
  }
488
- console.log(`UnityLockfile found: ${lockfilePath}`);
489
- console.log("Another Unity process may be using this project.");
490
- const confirmed = isRawModeSupported() ? await promptYesNoSingleKey() : await promptYesNoLine();
491
- if (!confirmed) {
492
- console.log("Aborted by user.");
493
- return "skip";
390
+ console.log(`UnityLockfile found without active Unity process: ${lockfilePath}`);
391
+ console.log("Assuming previous crash. Cleaning Temp directory and continuing launch.");
392
+ try {
393
+ await this.tempDirectoryCleaner.clean(projectPath);
394
+ } catch (error) {
395
+ const message = error instanceof Error ? error.message : String(error);
396
+ console.error(`Failed to clean Temp directory: ${message}`);
397
+ }
398
+ try {
399
+ await rm(lockfilePath, { force: true });
400
+ console.log("Deleted UnityLockfile.");
401
+ } catch (error) {
402
+ const message = error instanceof Error ? error.message : String(error);
403
+ console.error(`Failed to delete UnityLockfile: ${message}`);
494
404
  }
495
- await rm(lockfilePath, { force: true });
496
- console.log("Deleted UnityLockfile. Continuing launch.");
405
+ console.log("Continuing launch.");
497
406
  return "allow";
498
407
  }
499
408
  async bringUnityToFront(pid) {
@@ -764,12 +673,16 @@ var formatUpdatedText = (lastModified) => {
764
673
  var homeDirectory = process.env.HOME ?? "";
765
674
  var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
766
675
  var minimumVisibleProjectCount = 4;
767
- var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Quit Unity with q \xB7 Copy cd path with c";
676
+ var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Quit Unity with q \xB7 Refresh with r \xB7 Copy cd path with c";
768
677
  var PROJECT_COLOR = "#abd8e7";
769
678
  var BRANCH_COLOR = "#e3839c";
770
679
  var PATH_COLOR = "#719bd8";
771
680
  var LOCK_COLOR = "yellow";
772
- var LOCK_LABEL = "[running]";
681
+ var STATUS_LABELS = {
682
+ idle: "",
683
+ running: "[running]",
684
+ crashed: "[crash]"
685
+ };
773
686
  var shortenHomePath = (targetPath) => {
774
687
  if (!homeDirectory) {
775
688
  return targetPath;
@@ -790,18 +703,21 @@ var App = ({
790
703
  projects,
791
704
  onLaunch,
792
705
  onTerminate,
706
+ onRefresh,
793
707
  useGitRootName = true,
794
708
  showBranch = true,
795
709
  showPath = true
796
710
  }) => {
797
711
  const { exit } = useApp();
798
712
  const { stdout } = useStdout();
713
+ const [projectViews, setProjectViews] = useState(projects);
799
714
  const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
800
715
  const [index, setIndex] = useState(0);
801
716
  const [hint, setHint] = useState(defaultHintMessage);
802
717
  const [windowStart, setWindowStart] = useState(0);
803
718
  const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
804
719
  const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
720
+ const [isRefreshing, setIsRefreshing] = useState(false);
805
721
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
806
722
  const sortedProjects = useMemo(() => {
807
723
  const fallbackTime = 0;
@@ -815,7 +731,7 @@ var App = ({
815
731
  return view.project.title.toLocaleLowerCase();
816
732
  };
817
733
  const toTieBreaker = (view) => view.project.path.toLocaleLowerCase();
818
- return [...projects].sort((a, b) => {
734
+ return [...projectViews].sort((a, b) => {
819
735
  if (a.project.favorite !== b.project.favorite) {
820
736
  return a.project.favorite ? -1 : 1;
821
737
  }
@@ -831,7 +747,7 @@ var App = ({
831
747
  }
832
748
  return keyA.localeCompare(keyB);
833
749
  });
834
- }, [projects, useGitRootName]);
750
+ }, [projectViews, useGitRootName]);
835
751
  useEffect(() => {
836
752
  const handleSigint = () => {
837
753
  exit();
@@ -1050,9 +966,63 @@ var App = ({
1050
966
  }
1051
967
  }, [index, onTerminate, sortedProjects]);
1052
968
  useEffect(() => {
969
+ setProjectViews(projects);
1053
970
  setReleasedProjects(/* @__PURE__ */ new Set());
1054
971
  setLaunchedProjects(/* @__PURE__ */ new Set());
1055
972
  }, [projects]);
973
+ const refreshProjects = useCallback(async () => {
974
+ if (!onRefresh) {
975
+ setHint("Refresh not available");
976
+ setTimeout(() => {
977
+ setHint(defaultHintMessage);
978
+ }, 2e3);
979
+ return;
980
+ }
981
+ if (isRefreshing) {
982
+ setHint("Already refreshing");
983
+ setTimeout(() => {
984
+ setHint(defaultHintMessage);
985
+ }, 2e3);
986
+ return;
987
+ }
988
+ setIsRefreshing(true);
989
+ setHint("Refreshing projects...");
990
+ try {
991
+ const updatedProjects = await onRefresh();
992
+ setProjectViews(updatedProjects);
993
+ setReleasedProjects(/* @__PURE__ */ new Set());
994
+ setLaunchedProjects(/* @__PURE__ */ new Set());
995
+ setIndex((previousIndex) => {
996
+ if (updatedProjects.length === 0) {
997
+ return 0;
998
+ }
999
+ const previousProject = sortedProjects[previousIndex]?.project;
1000
+ if (!previousProject) {
1001
+ return Math.min(previousIndex, updatedProjects.length - 1);
1002
+ }
1003
+ const nextIndex = updatedProjects.findIndex(
1004
+ (candidate) => candidate.project.id === previousProject.id
1005
+ );
1006
+ if (nextIndex === -1) {
1007
+ return Math.min(previousIndex, updatedProjects.length - 1);
1008
+ }
1009
+ return nextIndex;
1010
+ });
1011
+ setWindowStart(0);
1012
+ setHint("Project list refreshed");
1013
+ setTimeout(() => {
1014
+ setHint(defaultHintMessage);
1015
+ }, 2e3);
1016
+ } catch (error) {
1017
+ const message = error instanceof Error ? error.message : String(error);
1018
+ setHint(`Failed to refresh: ${message}`);
1019
+ setTimeout(() => {
1020
+ setHint(defaultHintMessage);
1021
+ }, 3e3);
1022
+ } finally {
1023
+ setIsRefreshing(false);
1024
+ }
1025
+ }, [isRefreshing, onRefresh, sortedProjects]);
1056
1026
  useInput((input, key) => {
1057
1027
  if (input === "j" || key.downArrow) {
1058
1028
  move(1);
@@ -1068,6 +1038,10 @@ var App = ({
1068
1038
  void launchSelected();
1069
1039
  return;
1070
1040
  }
1041
+ if (input === "r") {
1042
+ void refreshProjects();
1043
+ return;
1044
+ }
1071
1045
  if (input === "c") {
1072
1046
  copyProjectPath();
1073
1047
  }
@@ -1088,7 +1062,7 @@ var App = ({
1088
1062
  };
1089
1063
  }, [limit, sortedProjects, windowStart]);
1090
1064
  const scrollbarChars = useMemo(() => {
1091
- const totalProjects = projects.length;
1065
+ const totalProjects = projectViews.length;
1092
1066
  const totalLines = totalProjects * linesPerProject;
1093
1067
  const windowProjects = visibleProjects.length;
1094
1068
  const visibleLines = windowProjects * linesPerProject;
@@ -1113,9 +1087,9 @@ var App = ({
1113
1087
  }
1114
1088
  return "|";
1115
1089
  });
1116
- }, [linesPerProject, projects.length, startIndex, visibleProjects]);
1090
+ }, [linesPerProject, projectViews.length, startIndex, visibleProjects]);
1117
1091
  const rows = useMemo(() => {
1118
- return visibleProjects.map(({ project, repository, isLocked }, offset) => {
1092
+ return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
1119
1093
  const rowIndex = startIndex + offset;
1120
1094
  const isSelected = rowIndex === index;
1121
1095
  const arrow = isSelected ? ">" : " ";
@@ -1124,12 +1098,24 @@ var App = ({
1124
1098
  const updatedText = formatUpdatedText(project.lastModified);
1125
1099
  const pathLine = shortenHomePath(project.path);
1126
1100
  const branchLine = formatBranch(repository?.branch);
1127
- const activeLock = isLocked && !releasedProjects.has(project.id) || launchedProjects.has(project.id);
1101
+ const hasReleasedLock = releasedProjects.has(project.id);
1102
+ const isLocallyLaunched = launchedProjects.has(project.id);
1103
+ const displayStatus = (() => {
1104
+ if (isLocallyLaunched) {
1105
+ return "running";
1106
+ }
1107
+ if (hasReleasedLock) {
1108
+ return "idle";
1109
+ }
1110
+ return launchStatus;
1111
+ })();
1128
1112
  const baseScrollbarIndex = offset * linesPerProject;
1129
1113
  const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
1130
1114
  const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
1131
1115
  const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
1132
1116
  const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
1117
+ const statusLabel = STATUS_LABELS[displayStatus];
1118
+ const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
1133
1119
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1134
1120
  /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
1135
1121
  /* @__PURE__ */ jsxs(Text, { children: [
@@ -1143,7 +1129,7 @@ var App = ({
1143
1129
  versionLabel
1144
1130
  ] }),
1145
1131
  updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
1146
- activeLock ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
1132
+ statusLabel && statusColor ? /* @__PURE__ */ jsx(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
1147
1133
  ] }),
1148
1134
  showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
1149
1135
  " ",
@@ -1187,17 +1173,18 @@ var bootstrap = async () => {
1187
1173
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
1188
1174
  const lockStatusReader = new UnityLockStatusReader();
1189
1175
  const unityProcessReader = new MacUnityProcessReader();
1176
+ const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
1190
1177
  const listProjectsUseCase = new ListProjectsUseCase(
1191
1178
  unityHubReader,
1192
1179
  gitRepositoryInfoReader,
1193
1180
  unityHubReader,
1194
- lockStatusReader
1181
+ lockStatusReader,
1182
+ unityProcessReader
1195
1183
  );
1196
1184
  const editorPathResolver = new MacEditorPathResolver();
1197
1185
  const processLauncher = new NodeProcessLauncher();
1198
- const lockChecker = new UnityLockChecker(unityProcessReader);
1186
+ const lockChecker = new UnityLockChecker(unityProcessReader, unityTempDirectoryCleaner);
1199
1187
  const unityProcessTerminator = new MacUnityProcessTerminator();
1200
- const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
1201
1188
  const launchProjectUseCase = new LaunchProjectUseCase(
1202
1189
  editorPathResolver,
1203
1190
  processLauncher,
@@ -1207,8 +1194,7 @@ var bootstrap = async () => {
1207
1194
  );
1208
1195
  const terminateProjectUseCase = new TerminateProjectUseCase(
1209
1196
  unityProcessReader,
1210
- unityProcessTerminator,
1211
- unityTempDirectoryCleaner
1197
+ unityProcessTerminator
1212
1198
  );
1213
1199
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
1214
1200
  const showBranch = !process2.argv.includes("--hide-branch");
@@ -1222,6 +1208,7 @@ var bootstrap = async () => {
1222
1208
  projects,
1223
1209
  onLaunch: (project) => launchProjectUseCase.execute(project),
1224
1210
  onTerminate: (project) => terminateProjectUseCase.execute(project),
1211
+ onRefresh: () => listProjectsUseCase.execute(),
1225
1212
  useGitRootName,
1226
1213
  showBranch,
1227
1214
  showPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.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": {