unity-hub-cli 0.4.0 → 0.5.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 +299 -16
  2. package/package.json +1 -1
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;
@@ -369,7 +404,7 @@ var promptYesNoSingleKey = async () => {
369
404
  const supportsRaw = isRawModeSupported();
370
405
  const previousRaw = supportsRaw ? stdin.isRaw === true : false;
371
406
  const wasPaused = stdin.isPaused();
372
- return await new Promise((resolve2) => {
407
+ return await new Promise((resolve3) => {
373
408
  const cleanup = () => {
374
409
  stdin.removeListener("data", handleData);
375
410
  if (wasPaused) {
@@ -392,7 +427,7 @@ var promptYesNoSingleKey = async () => {
392
427
  }
393
428
  process.stdout.write("\n");
394
429
  cleanup();
395
- resolve2(result);
430
+ resolve3(result);
396
431
  };
397
432
  process.stdout.write(RAW_PROMPT_MESSAGE);
398
433
  if (supportsRaw) {
@@ -410,9 +445,9 @@ var promptYesNoLine = async () => {
410
445
  console.error("UnityLockfile exists. No interactive console available for confirmation.");
411
446
  return false;
412
447
  }
413
- const confirmed = await new Promise((resolve2) => {
448
+ const confirmed = await new Promise((resolve3) => {
414
449
  prompt.rl.question(RAW_PROMPT_MESSAGE, (answer) => {
415
- resolve2(answer.trim() === "y");
450
+ resolve3(answer.trim() === "y");
416
451
  });
417
452
  });
418
453
  prompt.close();
@@ -452,6 +487,166 @@ var UnityLockStatusReader = class {
452
487
  }
453
488
  };
454
489
 
490
+ // src/infrastructure/unityProcess.ts
491
+ import { execFile } from "child_process";
492
+ import { resolve as resolve2 } from "path";
493
+ import { promisify } from "util";
494
+ var execFileAsync = promisify(execFile);
495
+ var UNITY_EXECUTABLE_PATTERN = /Unity\.app\/Contents\/MacOS\/Unity/i;
496
+ var PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)\s+("[^"]+"|'[^']+'|[^\s"']+)/i;
497
+ var PROCESS_LIST_ARGS = ["-axo", "pid=,command=", "-ww"];
498
+ var PROCESS_LIST_COMMAND = "ps";
499
+ var TERMINATE_TIMEOUT_MILLIS = 5e3;
500
+ var TERMINATE_POLL_INTERVAL_MILLIS = 200;
501
+ var delay = async (duration) => {
502
+ await new Promise((resolveDelay) => {
503
+ setTimeout(() => {
504
+ resolveDelay();
505
+ }, duration);
506
+ });
507
+ };
508
+ var normalizePath = (target) => {
509
+ const resolved = resolve2(target);
510
+ if (resolved.endsWith("/")) {
511
+ return resolved.slice(0, -1);
512
+ }
513
+ return resolved;
514
+ };
515
+ var arePathsEqual = (left, right) => {
516
+ const normalizedLeft = normalizePath(left);
517
+ const normalizedRight = normalizePath(right);
518
+ return normalizedLeft.localeCompare(normalizedRight, void 0, { sensitivity: "base" }) === 0;
519
+ };
520
+ var extractProjectPath = (command) => {
521
+ const match = command.match(PROJECT_PATH_PATTERN);
522
+ if (!match) {
523
+ return void 0;
524
+ }
525
+ const raw = match[1];
526
+ if (!raw) {
527
+ return void 0;
528
+ }
529
+ const trimmed = raw.trim();
530
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
531
+ return trimmed.slice(1, -1);
532
+ }
533
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
534
+ return trimmed.slice(1, -1);
535
+ }
536
+ return trimmed;
537
+ };
538
+ var isUnityMainProcess = (command) => {
539
+ return UNITY_EXECUTABLE_PATTERN.test(command);
540
+ };
541
+ var isProcessMissingError = (error) => {
542
+ if (typeof error !== "object" || error === null) {
543
+ return false;
544
+ }
545
+ const nodeError = error;
546
+ return nodeError.code === "ESRCH";
547
+ };
548
+ var ensureProcessAlive = (pid) => {
549
+ try {
550
+ process.kill(pid, 0);
551
+ return true;
552
+ } catch (error) {
553
+ if (isProcessMissingError(error)) {
554
+ return false;
555
+ }
556
+ throw error;
557
+ }
558
+ };
559
+ var MacUnityProcessReader = class {
560
+ async findByProjectPath(projectPath) {
561
+ const normalizedTarget = normalizePath(projectPath);
562
+ const processes = await this.listUnityProcesses();
563
+ return processes.find((candidate) => arePathsEqual(candidate.projectPath, normalizedTarget));
564
+ }
565
+ async listUnityProcesses() {
566
+ let stdout;
567
+ try {
568
+ const result = await execFileAsync(PROCESS_LIST_COMMAND, PROCESS_LIST_ARGS);
569
+ stdout = result.stdout;
570
+ } catch (error) {
571
+ throw new Error(`Failed to retrieve Unity process list: ${error instanceof Error ? error.message : String(error)}`);
572
+ }
573
+ const lines = stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
574
+ return lines.map((line) => {
575
+ const match = line.match(/^(\d+)\s+(.*)$/);
576
+ if (!match) {
577
+ return void 0;
578
+ }
579
+ const pidValue = Number.parseInt(match[1] ?? "", 10);
580
+ if (!Number.isFinite(pidValue)) {
581
+ return void 0;
582
+ }
583
+ const command = match[2] ?? "";
584
+ if (!isUnityMainProcess(command)) {
585
+ return void 0;
586
+ }
587
+ const projectArgument = extractProjectPath(command);
588
+ if (!projectArgument) {
589
+ return void 0;
590
+ }
591
+ return {
592
+ pid: pidValue,
593
+ projectPath: normalizePath(projectArgument)
594
+ };
595
+ }).filter((process3) => Boolean(process3));
596
+ }
597
+ };
598
+ var MacUnityProcessTerminator = class {
599
+ async terminate(unityProcess) {
600
+ try {
601
+ process.kill(unityProcess.pid, "SIGTERM");
602
+ } catch (error) {
603
+ if (isProcessMissingError(error)) {
604
+ return false;
605
+ }
606
+ throw new Error(
607
+ `Failed to terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
608
+ );
609
+ }
610
+ const deadline = Date.now() + TERMINATE_TIMEOUT_MILLIS;
611
+ while (Date.now() < deadline) {
612
+ await delay(TERMINATE_POLL_INTERVAL_MILLIS);
613
+ const alive = ensureProcessAlive(unityProcess.pid);
614
+ if (!alive) {
615
+ return true;
616
+ }
617
+ }
618
+ try {
619
+ process.kill(unityProcess.pid, "SIGKILL");
620
+ } catch (error) {
621
+ if (isProcessMissingError(error)) {
622
+ return true;
623
+ }
624
+ throw new Error(
625
+ `Failed to forcefully terminate the Unity process (PID: ${unityProcess.pid}): ${error instanceof Error ? error.message : String(error)}`
626
+ );
627
+ }
628
+ await delay(TERMINATE_POLL_INTERVAL_MILLIS);
629
+ return !ensureProcessAlive(unityProcess.pid);
630
+ }
631
+ };
632
+
633
+ // src/infrastructure/unityTemp.ts
634
+ import { rm as rm2 } from "fs/promises";
635
+ import { join as join4 } from "path";
636
+ var TEMP_DIRECTORY_NAME = "Temp";
637
+ var UnityTempDirectoryCleaner = class {
638
+ async clean(projectPath) {
639
+ const tempDirectoryPath = join4(projectPath, TEMP_DIRECTORY_NAME);
640
+ try {
641
+ await rm2(tempDirectoryPath, {
642
+ recursive: true,
643
+ force: true
644
+ });
645
+ } catch {
646
+ }
647
+ }
648
+ };
649
+
455
650
  // src/presentation/App.tsx
456
651
  import clipboard from "clipboardy";
457
652
  import { Box, Text, useApp, useInput, useStdout } from "ink";
@@ -540,7 +735,7 @@ var formatUpdatedText = (lastModified) => {
540
735
  var homeDirectory = process.env.HOME ?? "";
541
736
  var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
542
737
  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";
738
+ var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Quit Unity with q \xB7 Copy cd path with c";
544
739
  var PROJECT_COLOR = "#abd8e7";
545
740
  var BRANCH_COLOR = "#e3839c";
546
741
  var PATH_COLOR = "#719bd8";
@@ -565,6 +760,7 @@ var buildCdCommand = (targetPath) => {
565
760
  var App = ({
566
761
  projects,
567
762
  onLaunch,
763
+ onTerminate,
568
764
  useGitRootName = true,
569
765
  showBranch = true,
570
766
  showPath = true
@@ -575,6 +771,8 @@ var App = ({
575
771
  const [index, setIndex] = useState(0);
576
772
  const [hint, setHint] = useState(defaultHintMessage);
577
773
  const [windowStart, setWindowStart] = useState(0);
774
+ const [releasedProjects, setReleasedProjects] = useState(/* @__PURE__ */ new Set());
775
+ const [launchedProjects, setLaunchedProjects] = useState(/* @__PURE__ */ new Set());
578
776
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
579
777
  const sortedProjects = useMemo(() => {
580
778
  const fallbackTime = 0;
@@ -748,6 +946,19 @@ var App = ({
748
946
  }
749
947
  try {
750
948
  await onLaunch(project);
949
+ setLaunchedProjects((previous) => {
950
+ const next = new Set(previous);
951
+ next.add(project.id);
952
+ return next;
953
+ });
954
+ setReleasedProjects((previous) => {
955
+ if (!previous.has(project.id)) {
956
+ return previous;
957
+ }
958
+ const next = new Set(previous);
959
+ next.delete(project.id);
960
+ return next;
961
+ });
751
962
  setHint(`Launched: ${project.title}`);
752
963
  setTimeout(() => {
753
964
  setHint(defaultHintMessage);
@@ -767,6 +978,53 @@ var App = ({
767
978
  }, 3e3);
768
979
  }
769
980
  }, [index, onLaunch, sortedProjects]);
981
+ const terminateSelected = useCallback(async () => {
982
+ const projectView = sortedProjects[index];
983
+ if (!projectView) {
984
+ setHint("No project to terminate");
985
+ setTimeout(() => {
986
+ setHint(defaultHintMessage);
987
+ }, 2e3);
988
+ return;
989
+ }
990
+ try {
991
+ const result = await onTerminate(projectView.project);
992
+ if (!result.terminated) {
993
+ setHint(result.message ?? "No running Unity for this project");
994
+ setTimeout(() => {
995
+ setHint(defaultHintMessage);
996
+ }, 3e3);
997
+ return;
998
+ }
999
+ setHint(`Stopped Unity: ${projectView.project.title}`);
1000
+ setTimeout(() => {
1001
+ setHint(defaultHintMessage);
1002
+ }, 3e3);
1003
+ setLaunchedProjects((previous) => {
1004
+ if (!previous.has(projectView.project.id)) {
1005
+ return previous;
1006
+ }
1007
+ const next = new Set(previous);
1008
+ next.delete(projectView.project.id);
1009
+ return next;
1010
+ });
1011
+ setReleasedProjects((previous) => {
1012
+ const next = new Set(previous);
1013
+ next.add(projectView.project.id);
1014
+ return next;
1015
+ });
1016
+ } catch (error) {
1017
+ const message = error instanceof Error ? error.message : String(error);
1018
+ setHint(`Failed to stop: ${message}`);
1019
+ setTimeout(() => {
1020
+ setHint(defaultHintMessage);
1021
+ }, 3e3);
1022
+ }
1023
+ }, [index, onTerminate, sortedProjects]);
1024
+ useEffect(() => {
1025
+ setReleasedProjects(/* @__PURE__ */ new Set());
1026
+ setLaunchedProjects(/* @__PURE__ */ new Set());
1027
+ }, [projects]);
770
1028
  useInput((input, key) => {
771
1029
  if (input === "j" || key.downArrow) {
772
1030
  move(1);
@@ -774,8 +1032,13 @@ var App = ({
774
1032
  if (input === "k" || key.upArrow) {
775
1033
  move(-1);
776
1034
  }
1035
+ if (input === "q") {
1036
+ void terminateSelected();
1037
+ return;
1038
+ }
777
1039
  if (input === "o") {
778
1040
  void launchSelected();
1041
+ return;
779
1042
  }
780
1043
  if (input === "c") {
781
1044
  copyProjectPath();
@@ -833,6 +1096,7 @@ var App = ({
833
1096
  const updatedText = formatUpdatedText(project.lastModified);
834
1097
  const pathLine = shortenHomePath(project.path);
835
1098
  const branchLine = formatBranch(repository?.branch);
1099
+ const activeLock = isLocked && !releasedProjects.has(project.id) || launchedProjects.has(project.id);
836
1100
  const baseScrollbarIndex = offset * linesPerProject;
837
1101
  const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
838
1102
  const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
@@ -851,7 +1115,7 @@ var App = ({
851
1115
  versionLabel
852
1116
  ] }),
853
1117
  updatedText ? /* @__PURE__ */ jsx(Text, { color: isSelected ? "green" : void 0, children: ` ${updatedText}` }) : null,
854
- isLocked ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
1118
+ activeLock ? /* @__PURE__ */ jsx(Text, { color: LOCK_COLOR, children: ` ${LOCK_LABEL}` }) : null
855
1119
  ] }),
856
1120
  showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
857
1121
  " ",
@@ -871,9 +1135,19 @@ var App = ({
871
1135
  ] })
872
1136
  ] }, project.id);
873
1137
  });
874
- }, [index, scrollbarChars, showBranch, showPath, startIndex, useGitRootName, visibleProjects]);
1138
+ }, [
1139
+ index,
1140
+ launchedProjects,
1141
+ releasedProjects,
1142
+ scrollbarChars,
1143
+ showBranch,
1144
+ showPath,
1145
+ startIndex,
1146
+ useGitRootName,
1147
+ visibleProjects
1148
+ ]);
875
1149
  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 }),
1150
+ /* @__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
1151
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: hint }) })
878
1152
  ] });
879
1153
  };
@@ -893,6 +1167,9 @@ var bootstrap = async () => {
893
1167
  const editorPathResolver = new MacEditorPathResolver();
894
1168
  const processLauncher = new NodeProcessLauncher();
895
1169
  const lockChecker = new UnityLockChecker();
1170
+ const unityProcessReader = new MacUnityProcessReader();
1171
+ const unityProcessTerminator = new MacUnityProcessTerminator();
1172
+ const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
896
1173
  const launchProjectUseCase = new LaunchProjectUseCase(
897
1174
  editorPathResolver,
898
1175
  processLauncher,
@@ -900,6 +1177,11 @@ var bootstrap = async () => {
900
1177
  unityHubReader,
901
1178
  lockChecker
902
1179
  );
1180
+ const terminateProjectUseCase = new TerminateProjectUseCase(
1181
+ unityProcessReader,
1182
+ unityProcessTerminator,
1183
+ unityTempDirectoryCleaner
1184
+ );
903
1185
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
904
1186
  const showBranch = !process2.argv.includes("--hide-branch");
905
1187
  const showPath = !process2.argv.includes("--hide-path");
@@ -911,6 +1193,7 @@ var bootstrap = async () => {
911
1193
  {
912
1194
  projects,
913
1195
  onLaunch: (project) => launchProjectUseCase.execute(project),
1196
+ onTerminate: (project) => terminateProjectUseCase.execute(project),
914
1197
  useGitRootName,
915
1198
  showBranch,
916
1199
  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.5.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": {