unity-hub-cli 0.6.0 → 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 +132 -146
  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();
@@ -849,8 +765,7 @@ var App = ({
849
765
  }
850
766
  const borderRows = 2;
851
767
  const hintRows = 1;
852
- const hintMarginRows = 1;
853
- const reservedRows = borderRows + hintRows + hintMarginRows;
768
+ const reservedRows = borderRows + hintRows;
854
769
  const availableRows = stdout.rows - reservedRows;
855
770
  const rowsPerProject = Math.max(linesPerProject, 1);
856
771
  const calculatedCount = Math.max(
@@ -1051,9 +966,63 @@ var App = ({
1051
966
  }
1052
967
  }, [index, onTerminate, sortedProjects]);
1053
968
  useEffect(() => {
969
+ setProjectViews(projects);
1054
970
  setReleasedProjects(/* @__PURE__ */ new Set());
1055
971
  setLaunchedProjects(/* @__PURE__ */ new Set());
1056
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]);
1057
1026
  useInput((input, key) => {
1058
1027
  if (input === "j" || key.downArrow) {
1059
1028
  move(1);
@@ -1069,6 +1038,10 @@ var App = ({
1069
1038
  void launchSelected();
1070
1039
  return;
1071
1040
  }
1041
+ if (input === "r") {
1042
+ void refreshProjects();
1043
+ return;
1044
+ }
1072
1045
  if (input === "c") {
1073
1046
  copyProjectPath();
1074
1047
  }
@@ -1089,7 +1062,7 @@ var App = ({
1089
1062
  };
1090
1063
  }, [limit, sortedProjects, windowStart]);
1091
1064
  const scrollbarChars = useMemo(() => {
1092
- const totalProjects = projects.length;
1065
+ const totalProjects = projectViews.length;
1093
1066
  const totalLines = totalProjects * linesPerProject;
1094
1067
  const windowProjects = visibleProjects.length;
1095
1068
  const visibleLines = windowProjects * linesPerProject;
@@ -1114,9 +1087,9 @@ var App = ({
1114
1087
  }
1115
1088
  return "|";
1116
1089
  });
1117
- }, [linesPerProject, projects.length, startIndex, visibleProjects]);
1090
+ }, [linesPerProject, projectViews.length, startIndex, visibleProjects]);
1118
1091
  const rows = useMemo(() => {
1119
- return visibleProjects.map(({ project, repository, isLocked }, offset) => {
1092
+ return visibleProjects.map(({ project, repository, launchStatus }, offset) => {
1120
1093
  const rowIndex = startIndex + offset;
1121
1094
  const isSelected = rowIndex === index;
1122
1095
  const arrow = isSelected ? ">" : " ";
@@ -1125,12 +1098,24 @@ var App = ({
1125
1098
  const updatedText = formatUpdatedText(project.lastModified);
1126
1099
  const pathLine = shortenHomePath(project.path);
1127
1100
  const branchLine = formatBranch(repository?.branch);
1128
- 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
+ })();
1129
1112
  const baseScrollbarIndex = offset * linesPerProject;
1130
1113
  const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
1131
1114
  const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
1132
1115
  const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
1133
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;
1134
1119
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
1135
1120
  /* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
1136
1121
  /* @__PURE__ */ jsxs(Text, { children: [
@@ -1144,7 +1129,7 @@ var App = ({
1144
1129
  versionLabel
1145
1130
  ] }),
1146
1131
  updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
1147
- activeLock ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
1132
+ statusLabel && statusColor ? /* @__PURE__ */ jsx(Text, { color: statusColor, children: ` ${statusLabel}` }) : null
1148
1133
  ] }),
1149
1134
  showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
1150
1135
  " ",
@@ -1177,7 +1162,7 @@ var App = ({
1177
1162
  ]);
1178
1163
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1179
1164
  /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "No Unity Hub projects were found." }) : rows }),
1180
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: hint }) })
1165
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: hint }) })
1181
1166
  ] });
1182
1167
  };
1183
1168
 
@@ -1188,17 +1173,18 @@ var bootstrap = async () => {
1188
1173
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
1189
1174
  const lockStatusReader = new UnityLockStatusReader();
1190
1175
  const unityProcessReader = new MacUnityProcessReader();
1176
+ const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
1191
1177
  const listProjectsUseCase = new ListProjectsUseCase(
1192
1178
  unityHubReader,
1193
1179
  gitRepositoryInfoReader,
1194
1180
  unityHubReader,
1195
- lockStatusReader
1181
+ lockStatusReader,
1182
+ unityProcessReader
1196
1183
  );
1197
1184
  const editorPathResolver = new MacEditorPathResolver();
1198
1185
  const processLauncher = new NodeProcessLauncher();
1199
- const lockChecker = new UnityLockChecker(unityProcessReader);
1186
+ const lockChecker = new UnityLockChecker(unityProcessReader, unityTempDirectoryCleaner);
1200
1187
  const unityProcessTerminator = new MacUnityProcessTerminator();
1201
- const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
1202
1188
  const launchProjectUseCase = new LaunchProjectUseCase(
1203
1189
  editorPathResolver,
1204
1190
  processLauncher,
@@ -1208,8 +1194,7 @@ var bootstrap = async () => {
1208
1194
  );
1209
1195
  const terminateProjectUseCase = new TerminateProjectUseCase(
1210
1196
  unityProcessReader,
1211
- unityProcessTerminator,
1212
- unityTempDirectoryCleaner
1197
+ unityProcessTerminator
1213
1198
  );
1214
1199
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
1215
1200
  const showBranch = !process2.argv.includes("--hide-branch");
@@ -1223,6 +1208,7 @@ var bootstrap = async () => {
1223
1208
  projects,
1224
1209
  onLaunch: (project) => launchProjectUseCase.execute(project),
1225
1210
  onTerminate: (project) => terminateProjectUseCase.execute(project),
1211
+ onRefresh: () => listProjectsUseCase.execute(),
1226
1212
  useGitRootName,
1227
1213
  showBranch,
1228
1214
  showPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.6.0",
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": {