unity-hub-cli 0.4.0 → 0.6.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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +329 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
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`.
6
6
 
7
+ <img width="1678" height="1460" alt="スクリーンショット 2025-10-27 23 44 40" src="https://github.com/user-attachments/assets/db3cc995-820e-490b-a43b-393893197ab4" />
8
+
7
9
  ## Requirements
8
10
 
9
11
  - macOS
package/dist/index.js CHANGED
@@ -57,6 +57,41 @@ var LaunchProjectUseCase = class {
57
57
  await this.unityHubProjectsReader.updateLastModified(project.path, /* @__PURE__ */ new Date());
58
58
  }
59
59
  };
60
+ var TerminateProjectUseCase = class {
61
+ constructor(unityProcessReader, unityProcessTerminator, unityTempDirectoryCleaner) {
62
+ this.unityProcessReader = unityProcessReader;
63
+ this.unityProcessTerminator = unityProcessTerminator;
64
+ this.unityTempDirectoryCleaner = unityTempDirectoryCleaner;
65
+ }
66
+ async execute(project) {
67
+ const unityProcess = await this.unityProcessReader.findByProjectPath(project.path);
68
+ if (!unityProcess) {
69
+ return {
70
+ terminated: false,
71
+ message: "No Unity process is running for this project."
72
+ };
73
+ }
74
+ const terminated = await this.unityProcessTerminator.terminate(unityProcess);
75
+ if (!terminated) {
76
+ return {
77
+ terminated: false,
78
+ message: "Failed to terminate the Unity process."
79
+ };
80
+ }
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
+ return {
90
+ terminated: true,
91
+ message: cleanupMessage
92
+ };
93
+ }
94
+ };
60
95
 
61
96
  // src/infrastructure/editor.ts
62
97
  import { constants } from "fs";
@@ -70,7 +105,7 @@ var MacEditorPathResolver = class {
70
105
  try {
71
106
  await access(editorPath, constants.X_OK);
72
107
  } catch {
73
- throw new Error(`\u5BFE\u5FDC\u3059\u308BUnity Editor\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${version.value}\uFF09`);
108
+ throw new Error(`Unity Editor not found for version ${version.value}.`);
74
109
  }
75
110
  return editorPath;
76
111
  }
@@ -161,7 +196,7 @@ import { spawn } from "child_process";
161
196
  var NodeProcessLauncher = class {
162
197
  async launch(command, args, options) {
163
198
  const detached = options?.detached ?? false;
164
- await new Promise((resolve2, reject) => {
199
+ await new Promise((resolve3, reject) => {
165
200
  const child = spawn(command, args, {
166
201
  detached,
167
202
  stdio: "ignore"
@@ -173,7 +208,7 @@ var NodeProcessLauncher = class {
173
208
  const handleSpawn = () => {
174
209
  child.off("error", handleError);
175
210
  child.unref();
176
- resolve2();
211
+ resolve3();
177
212
  };
178
213
  child.once("error", handleError);
179
214
  child.once("spawn", handleSpawn);
@@ -234,17 +269,17 @@ var UnityHubProjectsReader = class {
234
269
  content = await readFile2(HUB_PROJECTS_PATH, "utf8");
235
270
  } catch {
236
271
  throw new Error(
237
- `Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${HUB_PROJECTS_PATH}\uFF09`
272
+ `Unity Hub project list not found (${HUB_PROJECTS_PATH}).`
238
273
  );
239
274
  }
240
275
  let json;
241
276
  try {
242
277
  json = JSON.parse(content);
243
278
  } catch {
244
- throw new Error("Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\uFF08\u6A29\u9650/\u5F62\u5F0F\u30A8\u30E9\u30FC\uFF09");
279
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
245
280
  }
246
281
  if (json.schema_version && json.schema_version !== schemaVersion) {
247
- throw new Error(`\u672A\u5BFE\u5FDC\u306Eschema_version\u3067\u3059\uFF08${json.schema_version}\uFF09`);
282
+ throw new Error(`Unsupported schema_version (${json.schema_version}).`);
248
283
  }
249
284
  const entries = Object.values(json.data ?? {});
250
285
  if (entries.length === 0) {
@@ -259,14 +294,14 @@ var UnityHubProjectsReader = class {
259
294
  content = await readFile2(HUB_PROJECTS_PATH, "utf8");
260
295
  } catch {
261
296
  throw new Error(
262
- `Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${HUB_PROJECTS_PATH}\uFF09`
297
+ `Unity Hub project list not found (${HUB_PROJECTS_PATH}).`
263
298
  );
264
299
  }
265
300
  let json;
266
301
  try {
267
302
  json = JSON.parse(content);
268
303
  } catch {
269
- throw new Error("Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\uFF08\u6A29\u9650/\u5F62\u5F0F\u30A8\u30E9\u30FC\uFF09");
304
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
270
305
  }
271
306
  if (!json.data) {
272
307
  return;
@@ -312,11 +347,17 @@ var UnityHubProjectsReader = class {
312
347
  };
313
348
 
314
349
  // src/infrastructure/unityLock.ts
350
+ import { execFile } from "child_process";
315
351
  import { constants as constants2, createReadStream, createWriteStream } from "fs";
316
352
  import { access as access2, rm } from "fs/promises";
317
353
  import { join as join3 } from "path";
318
354
  import readline from "readline";
355
+ import { promisify } from "util";
319
356
  var RAW_PROMPT_MESSAGE = "Delete UnityLockfile and continue? Type 'y' to continue; anything else aborts: ";
357
+ var execFileAsync = promisify(execFile);
358
+ var buildBringToFrontScript = (pid) => {
359
+ return `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
360
+ };
320
361
  var isRawModeSupported = () => {
321
362
  const stdin = process.stdin;
322
363
  return Boolean(stdin?.isTTY && typeof stdin.setRawMode === "function" && process.stdout.isTTY);
@@ -369,7 +410,7 @@ var promptYesNoSingleKey = async () => {
369
410
  const supportsRaw = isRawModeSupported();
370
411
  const previousRaw = supportsRaw ? stdin.isRaw === true : false;
371
412
  const wasPaused = stdin.isPaused();
372
- return await new Promise((resolve2) => {
413
+ return await new Promise((resolve3) => {
373
414
  const cleanup = () => {
374
415
  stdin.removeListener("data", handleData);
375
416
  if (wasPaused) {
@@ -392,7 +433,7 @@ var promptYesNoSingleKey = async () => {
392
433
  }
393
434
  process.stdout.write("\n");
394
435
  cleanup();
395
- resolve2(result);
436
+ resolve3(result);
396
437
  };
397
438
  process.stdout.write(RAW_PROMPT_MESSAGE);
398
439
  if (supportsRaw) {
@@ -410,9 +451,9 @@ var promptYesNoLine = async () => {
410
451
  console.error("UnityLockfile exists. No interactive console available for confirmation.");
411
452
  return false;
412
453
  }
413
- const confirmed = await new Promise((resolve2) => {
454
+ const confirmed = await new Promise((resolve3) => {
414
455
  prompt.rl.question(RAW_PROMPT_MESSAGE, (answer) => {
415
- resolve2(answer.trim() === "y");
456
+ resolve3(answer.trim() === "y");
416
457
  });
417
458
  });
418
459
  prompt.close();
@@ -427,7 +468,18 @@ var pathExists = async (target) => {
427
468
  }
428
469
  };
429
470
  var UnityLockChecker = class {
471
+ constructor(unityProcessReader) {
472
+ this.unityProcessReader = unityProcessReader;
473
+ }
430
474
  async check(projectPath) {
475
+ const activeProcess = await this.unityProcessReader.findByProjectPath(projectPath);
476
+ if (activeProcess) {
477
+ console.log(
478
+ `Unity process already running for project: ${activeProcess.projectPath} (PID: ${activeProcess.pid})`
479
+ );
480
+ await this.bringUnityToFront(activeProcess.pid);
481
+ return "skip";
482
+ }
431
483
  const lockfilePath = join3(projectPath, "Temp", "UnityLockfile");
432
484
  const hasLockfile = await pathExists(lockfilePath);
433
485
  if (!hasLockfile) {
@@ -444,6 +496,18 @@ var UnityLockChecker = class {
444
496
  console.log("Deleted UnityLockfile. Continuing launch.");
445
497
  return "allow";
446
498
  }
499
+ async bringUnityToFront(pid) {
500
+ if (process.platform !== "darwin") {
501
+ return;
502
+ }
503
+ try {
504
+ const script = buildBringToFrontScript(pid);
505
+ await execFileAsync("osascript", ["-e", script]);
506
+ } catch (error) {
507
+ const message = error instanceof Error ? error.message : String(error);
508
+ console.error(`Failed to bring Unity to front: ${message}`);
509
+ }
510
+ }
447
511
  };
448
512
  var UnityLockStatusReader = class {
449
513
  async isLocked(projectPath) {
@@ -452,6 +516,166 @@ var UnityLockStatusReader = class {
452
516
  }
453
517
  };
454
518
 
519
+ // src/infrastructure/unityProcess.ts
520
+ import { execFile as execFile2 } from "child_process";
521
+ import { resolve as resolve2 } from "path";
522
+ import { promisify as promisify2 } from "util";
523
+ var execFileAsync2 = promisify2(execFile2);
524
+ var UNITY_EXECUTABLE_PATTERN = /Unity\.app\/Contents\/MacOS\/Unity/i;
525
+ var PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
526
+ var PROCESS_LIST_ARGS = ["-axo", "pid=,command=", "-ww"];
527
+ var PROCESS_LIST_COMMAND = "ps";
528
+ var TERMINATE_TIMEOUT_MILLIS = 5e3;
529
+ var TERMINATE_POLL_INTERVAL_MILLIS = 200;
530
+ var delay = async (duration) => {
531
+ await new Promise((resolveDelay) => {
532
+ setTimeout(() => {
533
+ resolveDelay();
534
+ }, duration);
535
+ });
536
+ };
537
+ var normalizePath = (target) => {
538
+ const resolved = resolve2(target);
539
+ if (resolved.endsWith("/")) {
540
+ return resolved.slice(0, -1);
541
+ }
542
+ return resolved;
543
+ };
544
+ var arePathsEqual = (left, right) => {
545
+ const normalizedLeft = normalizePath(left);
546
+ const normalizedRight = normalizePath(right);
547
+ return normalizedLeft.localeCompare(normalizedRight, void 0, { sensitivity: "base" }) === 0;
548
+ };
549
+ var extractProjectPath = (command) => {
550
+ const match = command.match(PROJECT_PATH_PATTERN);
551
+ if (!match) {
552
+ return void 0;
553
+ }
554
+ const raw = match[1];
555
+ if (!raw) {
556
+ return void 0;
557
+ }
558
+ const trimmed = raw.trim();
559
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
560
+ return trimmed.slice(1, -1);
561
+ }
562
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
563
+ return trimmed.slice(1, -1);
564
+ }
565
+ return trimmed;
566
+ };
567
+ var isUnityMainProcess = (command) => {
568
+ return UNITY_EXECUTABLE_PATTERN.test(command);
569
+ };
570
+ var isProcessMissingError = (error) => {
571
+ if (typeof error !== "object" || error === null) {
572
+ return false;
573
+ }
574
+ const nodeError = error;
575
+ return nodeError.code === "ESRCH";
576
+ };
577
+ var ensureProcessAlive = (pid) => {
578
+ try {
579
+ process.kill(pid, 0);
580
+ return true;
581
+ } catch (error) {
582
+ if (isProcessMissingError(error)) {
583
+ return false;
584
+ }
585
+ throw error;
586
+ }
587
+ };
588
+ var MacUnityProcessReader = class {
589
+ async findByProjectPath(projectPath) {
590
+ const normalizedTarget = normalizePath(projectPath);
591
+ const processes = await this.listUnityProcesses();
592
+ return processes.find((candidate) => arePathsEqual(candidate.projectPath, normalizedTarget));
593
+ }
594
+ async listUnityProcesses() {
595
+ let stdout;
596
+ try {
597
+ const result = await execFileAsync2(PROCESS_LIST_COMMAND, PROCESS_LIST_ARGS);
598
+ stdout = result.stdout;
599
+ } catch (error) {
600
+ throw new Error(`Failed to retrieve Unity process list: ${error instanceof Error ? error.message : String(error)}`);
601
+ }
602
+ const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
603
+ return lines.map((line) => {
604
+ const match = line.match(/^(\d+)\s+(.*)$/);
605
+ if (!match) {
606
+ return void 0;
607
+ }
608
+ const pidValue = Number.parseInt(match[1] ?? "", 10);
609
+ if (!Number.isFinite(pidValue)) {
610
+ return void 0;
611
+ }
612
+ const command = match[2] ?? "";
613
+ if (!isUnityMainProcess(command)) {
614
+ return void 0;
615
+ }
616
+ const projectArgument = extractProjectPath(command);
617
+ if (!projectArgument) {
618
+ return void 0;
619
+ }
620
+ return {
621
+ pid: pidValue,
622
+ projectPath: normalizePath(projectArgument)
623
+ };
624
+ }).filter((process3) => Boolean(process3));
625
+ }
626
+ };
627
+ var MacUnityProcessTerminator = class {
628
+ async terminate(unityProcess) {
629
+ try {
630
+ process.kill(unityProcess.pid, "SIGTERM");
631
+ } catch (error) {
632
+ if (isProcessMissingError(error)) {
633
+ return false;
634
+ }
635
+ throw new Error(
636
+ `Failed to terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
637
+ );
638
+ }
639
+ const deadline = Date.now() + TERMINATE_TIMEOUT_MILLIS;
640
+ while (Date.now() < deadline) {
641
+ await delay(TERMINATE_POLL_INTERVAL_MILLIS);
642
+ const alive = ensureProcessAlive(unityProcess.pid);
643
+ if (!alive) {
644
+ return true;
645
+ }
646
+ }
647
+ try {
648
+ process.kill(unityProcess.pid, "SIGKILL");
649
+ } catch (error) {
650
+ if (isProcessMissingError(error)) {
651
+ return true;
652
+ }
653
+ throw new Error(
654
+ `Failed to forcefully terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
655
+ );
656
+ }
657
+ await delay(TERMINATE_POLL_INTERVAL_MILLIS);
658
+ return !ensureProcessAlive(unityProcess.pid);
659
+ }
660
+ };
661
+
662
+ // src/infrastructure/unityTemp.ts
663
+ import { rm as rm2 } from "fs/promises";
664
+ import { join as join4 } from "path";
665
+ var TEMP_DIRECTORY_NAME = "Temp";
666
+ var UnityTempDirectoryCleaner = class {
667
+ async clean(projectPath) {
668
+ const tempDirectoryPath = join4(projectPath, TEMP_DIRECTORY_NAME);
669
+ try {
670
+ await rm2(tempDirectoryPath, {
671
+ recursive: true,
672
+ force: true
673
+ });
674
+ } catch {
675
+ }
676
+ }
677
+ };
678
+
455
679
  // src/presentation/App.tsx
456
680
  import clipboard from "clipboardy";
457
681
  import { Box, Text, useApp, useInput, useStdout } from "ink";
@@ -540,7 +764,7 @@ var formatUpdatedText = (lastModified) => {
540
764
  var homeDirectory = process.env.HOME ?? "";
541
765
  var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
542
766
  var minimumVisibleProjectCount = 4;
543
- var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Copy cd path with c \xB7 Exit with Ctrl+C";
767
+ var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Quit Unity with q \xB7 Copy cd path with c";
544
768
  var PROJECT_COLOR = "#abd8e7";
545
769
  var BRANCH_COLOR = "#e3839c";
546
770
  var PATH_COLOR = "#719bd8";
@@ -565,6 +789,7 @@ var buildCdCommand = (targetPath) => {
565
789
  var App = ({
566
790
  projects,
567
791
  onLaunch,
792
+ onTerminate,
568
793
  useGitRootName = true,
569
794
  showBranch = true,
570
795
  showPath = true
@@ -575,6 +800,8 @@ var App = ({
575
800
  const [index, setIndex] = useState(0);
576
801
  const [hint, setHint] = useState(defaultHintMessage);
577
802
  const [windowStart, setWindowStart] = useState(0);
803
+ const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
804
+ const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
578
805
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
579
806
  const sortedProjects = useMemo(() => {
580
807
  const fallbackTime = 0;
@@ -748,6 +975,19 @@ var App = ({
748
975
  }
749
976
  try {
750
977
  await onLaunch(project);
978
+ setLaunchedProjects((previous) => {
979
+ const next = new Set(previous);
980
+ next.add(project.id);
981
+ return next;
982
+ });
983
+ setReleasedProjects((previous) => {
984
+ if (!previous.has(project.id)) {
985
+ return previous;
986
+ }
987
+ const next = new Set(previous);
988
+ next.delete(project.id);
989
+ return next;
990
+ });
751
991
  setHint(`Launched: ${project.title}`);
752
992
  setTimeout(() => {
753
993
  setHint(defaultHintMessage);
@@ -767,6 +1007,53 @@ var App = ({
767
1007
  }, 3e3);
768
1008
  }
769
1009
  }, [index, onLaunch, sortedProjects]);
1010
+ const terminateSelected = useCallback(async () => {
1011
+ const projectView = sortedProjects[index];
1012
+ if (!projectView) {
1013
+ setHint("No project to terminate");
1014
+ setTimeout(() => {
1015
+ setHint(defaultHintMessage);
1016
+ }, 2e3);
1017
+ return;
1018
+ }
1019
+ try {
1020
+ const result = await onTerminate(projectView.project);
1021
+ if (!result.terminated) {
1022
+ setHint(result.message ?? "No running Unity for this project");
1023
+ setTimeout(() => {
1024
+ setHint(defaultHintMessage);
1025
+ }, 3e3);
1026
+ return;
1027
+ }
1028
+ setHint(`Stopped Unity: ${projectView.project.title}`);
1029
+ setTimeout(() => {
1030
+ setHint(defaultHintMessage);
1031
+ }, 3e3);
1032
+ setLaunchedProjects((previous) => {
1033
+ if (!previous.has(projectView.project.id)) {
1034
+ return previous;
1035
+ }
1036
+ const next = new Set(previous);
1037
+ next.delete(projectView.project.id);
1038
+ return next;
1039
+ });
1040
+ setReleasedProjects((previous) => {
1041
+ const next = new Set(previous);
1042
+ next.add(projectView.project.id);
1043
+ return next;
1044
+ });
1045
+ } catch (error) {
1046
+ const message = error instanceof Error ? error.message : String(error);
1047
+ setHint(`Failed to stop: ${message}`);
1048
+ setTimeout(() => {
1049
+ setHint(defaultHintMessage);
1050
+ }, 3e3);
1051
+ }
1052
+ }, [index, onTerminate, sortedProjects]);
1053
+ useEffect(() => {
1054
+ setReleasedProjects(/* @__PURE__ */ new Set());
1055
+ setLaunchedProjects(/* @__PURE__ */ new Set());
1056
+ }, [projects]);
770
1057
  useInput((input, key) => {
771
1058
  if (input === "j" || key.downArrow) {
772
1059
  move(1);
@@ -774,8 +1061,13 @@ var App = ({
774
1061
  if (input === "k" || key.upArrow) {
775
1062
  move(-1);
776
1063
  }
1064
+ if (input === "q") {
1065
+ void terminateSelected();
1066
+ return;
1067
+ }
777
1068
  if (input === "o") {
778
1069
  void launchSelected();
1070
+ return;
779
1071
  }
780
1072
  if (input === "c") {
781
1073
  copyProjectPath();
@@ -833,6 +1125,7 @@ var App = ({
833
1125
  const updatedText = formatUpdatedText(project.lastModified);
834
1126
  const pathLine = shortenHomePath(project.path);
835
1127
  const branchLine = formatBranch(repository?.branch);
1128
+ const activeLock = isLocked && !releasedProjects.has(project.id) || launchedProjects.has(project.id);
836
1129
  const baseScrollbarIndex = offset * linesPerProject;
837
1130
  const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
838
1131
  const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
@@ -851,7 +1144,7 @@ var App = ({
851
1144
  versionLabel
852
1145
  ] }),
853
1146
  updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
854
- isLocked ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
1147
+ activeLock ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
855
1148
  ] }),
856
1149
  showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
857
1150
  " ",
@@ -871,9 +1164,19 @@ var App = ({
871
1164
  ] })
872
1165
  ] }, project.id);
873
1166
  });
874
- }, [index, scrollbarChars, showBranch, showPath, startIndex, useGitRootName, visibleProjects]);
1167
+ }, [
1168
+ index,
1169
+ launchedProjects,
1170
+ releasedProjects,
1171
+ scrollbarChars,
1172
+ showBranch,
1173
+ showPath,
1174
+ startIndex,
1175
+ useGitRootName,
1176
+ visibleProjects
1177
+ ]);
875
1178
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
876
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F" }) : rows }),
1179
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "No Unity Hub projects were found." }) : rows }),
877
1180
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: hint }) })
878
1181
  ] });
879
1182
  };
@@ -884,6 +1187,7 @@ var bootstrap = async () => {
884
1187
  const unityHubReader = new UnityHubProjectsReader();
885
1188
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
886
1189
  const lockStatusReader = new UnityLockStatusReader();
1190
+ const unityProcessReader = new MacUnityProcessReader();
887
1191
  const listProjectsUseCase = new ListProjectsUseCase(
888
1192
  unityHubReader,
889
1193
  gitRepositoryInfoReader,
@@ -892,7 +1196,9 @@ var bootstrap = async () => {
892
1196
  );
893
1197
  const editorPathResolver = new MacEditorPathResolver();
894
1198
  const processLauncher = new NodeProcessLauncher();
895
- const lockChecker = new UnityLockChecker();
1199
+ const lockChecker = new UnityLockChecker(unityProcessReader);
1200
+ const unityProcessTerminator = new MacUnityProcessTerminator();
1201
+ const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
896
1202
  const launchProjectUseCase = new LaunchProjectUseCase(
897
1203
  editorPathResolver,
898
1204
  processLauncher,
@@ -900,6 +1206,11 @@ var bootstrap = async () => {
900
1206
  unityHubReader,
901
1207
  lockChecker
902
1208
  );
1209
+ const terminateProjectUseCase = new TerminateProjectUseCase(
1210
+ unityProcessReader,
1211
+ unityProcessTerminator,
1212
+ unityTempDirectoryCleaner
1213
+ );
903
1214
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
904
1215
  const showBranch = !process2.argv.includes("--hide-branch");
905
1216
  const showPath = !process2.argv.includes("--hide-path");
@@ -911,6 +1222,7 @@ var bootstrap = async () => {
911
1222
  {
912
1223
  projects,
913
1224
  onLaunch: (project) => launchProjectUseCase.execute(project),
1225
+ onTerminate: (project) => terminateProjectUseCase.execute(project),
914
1226
  useGitRootName,
915
1227
  showBranch,
916
1228
  showPath
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": {