unity-hub-cli 0.14.0 → 0.15.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 +402 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
- import process2 from "process";
4
+ import process3 from "process";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/application/usecases.ts
@@ -105,6 +105,108 @@ var TerminateProjectUseCase = class {
105
105
  };
106
106
  }
107
107
  };
108
+ var LaunchWithEditorUseCase = class {
109
+ constructor(launchProjectUseCase, externalEditorPathReader, externalEditorLauncher) {
110
+ this.launchProjectUseCase = launchProjectUseCase;
111
+ this.externalEditorPathReader = externalEditorPathReader;
112
+ this.externalEditorLauncher = externalEditorLauncher;
113
+ }
114
+ /**
115
+ * Launches the Unity project and attempts to open the external editor.
116
+ * @param project - The Unity project to launch.
117
+ * @returns The result of the launch operation.
118
+ */
119
+ async execute(project) {
120
+ const editorResult = await this.externalEditorPathReader.read();
121
+ let editorLaunched = false;
122
+ let editorMessage = "";
123
+ if (editorResult.status === "found") {
124
+ editorLaunched = await this.tryLaunchEditor(editorResult, project.path);
125
+ editorMessage = editorLaunched ? ` + ${editorResult.name}` : " (Editor launch failed)";
126
+ } else {
127
+ editorMessage = this.buildEditorStatusMessage(editorResult);
128
+ }
129
+ await this.launchProjectUseCase.execute(project);
130
+ return {
131
+ unityLaunched: true,
132
+ editorLaunched,
133
+ message: `Launched: ${project.title}${editorMessage}`
134
+ };
135
+ }
136
+ /**
137
+ * Attempts to launch the external editor.
138
+ * @param editorResult - The found external editor result.
139
+ * @param projectPath - The path to the project root.
140
+ * @returns Whether the editor was successfully launched.
141
+ */
142
+ async tryLaunchEditor(editorResult, projectPath) {
143
+ try {
144
+ await this.externalEditorLauncher.launch(editorResult.path, projectPath);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Builds a status message for non-found editor results.
152
+ * @param editorResult - The external editor result.
153
+ * @returns The status message.
154
+ */
155
+ buildEditorStatusMessage(editorResult) {
156
+ if (editorResult.status === "not_configured") {
157
+ return " (Editor not configured)";
158
+ }
159
+ if (editorResult.status === "not_found") {
160
+ return ` (Editor not found: ${editorResult.configuredPath})`;
161
+ }
162
+ return "";
163
+ }
164
+ };
165
+ var LaunchEditorOnlyUseCase = class {
166
+ constructor(externalEditorPathReader, externalEditorLauncher) {
167
+ this.externalEditorPathReader = externalEditorPathReader;
168
+ this.externalEditorLauncher = externalEditorLauncher;
169
+ }
170
+ /**
171
+ * Launches only the external editor for the specified project.
172
+ * @param project - The Unity project to open in the editor.
173
+ * @returns The result of the launch operation.
174
+ */
175
+ async execute(project) {
176
+ const editorResult = await this.externalEditorPathReader.read();
177
+ if (editorResult.status === "not_configured") {
178
+ return {
179
+ editorLaunched: false,
180
+ message: "Editor not configured in Unity preferences"
181
+ };
182
+ }
183
+ if (editorResult.status === "not_found") {
184
+ return {
185
+ editorLaunched: false,
186
+ message: `Editor not found: ${editorResult.configuredPath}`
187
+ };
188
+ }
189
+ const launched = await this.tryLaunchEditor(editorResult, project.path);
190
+ return {
191
+ editorLaunched: launched,
192
+ message: launched ? `Launched: ${editorResult.name}` : `Failed to launch ${editorResult.name}`
193
+ };
194
+ }
195
+ /**
196
+ * Attempts to launch the external editor.
197
+ * @param editorResult - The found external editor result.
198
+ * @param projectPath - The path to the project root.
199
+ * @returns Whether the editor was successfully launched.
200
+ */
201
+ async tryLaunchEditor(editorResult, projectPath) {
202
+ try {
203
+ await this.externalEditorLauncher.launch(editorResult.path, projectPath);
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+ };
108
210
 
109
211
  // src/infrastructure/editor.ts
110
212
  import { constants } from "fs";
@@ -158,9 +260,139 @@ var WinEditorPathResolver = class {
158
260
  }
159
261
  };
160
262
 
263
+ // src/infrastructure/externalEditor.ts
264
+ import { execFile } from "child_process";
265
+ import { constants as constants3, existsSync } from "fs";
266
+ import { access as access3 } from "fs/promises";
267
+ import { basename, join as join3 } from "path";
268
+ import { promisify } from "util";
269
+ var execFileAsync = promisify(execFile);
270
+ var PLIST_DOMAIN = "com.unity3d.UnityEditor5.x";
271
+ var PLIST_KEY = "kScriptsDefaultApp";
272
+ var MacExternalEditorPathReader = class {
273
+ /**
274
+ * Reads the external editor configuration from Unity preferences.
275
+ * @returns The external editor result with status and path information.
276
+ */
277
+ async read() {
278
+ let configuredPath;
279
+ try {
280
+ const result = await execFileAsync("defaults", ["read", PLIST_DOMAIN, PLIST_KEY]);
281
+ configuredPath = result.stdout.trim();
282
+ } catch {
283
+ return { status: "not_configured" };
284
+ }
285
+ if (!configuredPath) {
286
+ return { status: "not_configured" };
287
+ }
288
+ try {
289
+ await access3(configuredPath, constants3.F_OK);
290
+ } catch {
291
+ return { status: "not_found", configuredPath };
292
+ }
293
+ const name = basename(configuredPath, ".app");
294
+ return { status: "found", path: configuredPath, name };
295
+ }
296
+ };
297
+ var MacExternalEditorLauncher = class {
298
+ /**
299
+ * Launches the external editor with the specified project root.
300
+ * If a .sln file exists with the project name, it will be opened directly.
301
+ * This allows Rider to open the solution without showing a selection dialog.
302
+ * @param editorPath - The path to the editor application.
303
+ * @param projectRoot - The project root directory to open.
304
+ */
305
+ async launch(editorPath, projectRoot) {
306
+ const projectName = basename(projectRoot);
307
+ const slnFilePath = join3(projectRoot, `${projectName}.sln`);
308
+ const targetPath = existsSync(slnFilePath) ? slnFilePath : projectRoot;
309
+ await execFileAsync("open", ["-a", editorPath, targetPath]);
310
+ }
311
+ };
312
+
313
+ // src/infrastructure/externalEditor.win.ts
314
+ import { execFile as execFile2, spawn } from "child_process";
315
+ import { constants as constants4, existsSync as existsSync2 } from "fs";
316
+ import { access as access4 } from "fs/promises";
317
+ import { basename as basename2, join as join4 } from "path";
318
+ import { promisify as promisify2 } from "util";
319
+ var execFileAsync2 = promisify2(execFile2);
320
+ var REGISTRY_PATH = "HKEY_CURRENT_USER\\Software\\Unity Technologies\\Unity Editor 5.x";
321
+ var parseRegistryOutput = (stdout) => {
322
+ const lines = stdout.split("\n");
323
+ for (const line of lines) {
324
+ const match = line.match(/kScriptsDefaultApp[^\s]*\s+REG_SZ\s+(.+)/i);
325
+ if (match?.[1]) {
326
+ return match[1].trim();
327
+ }
328
+ }
329
+ return void 0;
330
+ };
331
+ var WinExternalEditorPathReader = class {
332
+ /**
333
+ * Reads the external editor configuration from Unity preferences.
334
+ * @returns The external editor result with status and path information.
335
+ */
336
+ async read() {
337
+ let configuredPath;
338
+ try {
339
+ const result = await execFileAsync2("reg", [
340
+ "query",
341
+ REGISTRY_PATH,
342
+ "/v",
343
+ "kScriptsDefaultApp"
344
+ ]);
345
+ configuredPath = parseRegistryOutput(result.stdout);
346
+ } catch {
347
+ return { status: "not_configured" };
348
+ }
349
+ if (!configuredPath) {
350
+ return { status: "not_configured" };
351
+ }
352
+ try {
353
+ await access4(configuredPath, constants4.F_OK);
354
+ } catch {
355
+ return { status: "not_found", configuredPath };
356
+ }
357
+ const name = basename2(configuredPath, ".exe");
358
+ return { status: "found", path: configuredPath, name };
359
+ }
360
+ };
361
+ var WinExternalEditorLauncher = class {
362
+ /**
363
+ * Launches the external editor with the specified project root.
364
+ * If a .sln file exists with the project name, it will be opened directly.
365
+ * This allows Rider to open the solution without showing a selection dialog.
366
+ * @param editorPath - The path to the editor executable.
367
+ * @param projectRoot - The project root directory to open.
368
+ */
369
+ async launch(editorPath, projectRoot) {
370
+ const projectName = basename2(projectRoot);
371
+ const slnFilePath = join4(projectRoot, `${projectName}.sln`);
372
+ const targetPath = existsSync2(slnFilePath) ? slnFilePath : projectRoot;
373
+ await new Promise((resolve4, reject) => {
374
+ const child = spawn(editorPath, [targetPath], {
375
+ detached: true,
376
+ stdio: "ignore"
377
+ });
378
+ const handleError = (error) => {
379
+ child.off("spawn", handleSpawn);
380
+ reject(error);
381
+ };
382
+ const handleSpawn = () => {
383
+ child.off("error", handleError);
384
+ child.unref();
385
+ resolve4();
386
+ };
387
+ child.once("error", handleError);
388
+ child.once("spawn", handleSpawn);
389
+ });
390
+ }
391
+ };
392
+
161
393
  // src/infrastructure/git.ts
162
394
  import { readFile, stat } from "fs/promises";
163
- import { dirname, join as join3, resolve } from "path";
395
+ import { dirname, join as join5, resolve } from "path";
164
396
  var HEAD_FILE = "HEAD";
165
397
  var GIT_DIR = ".git";
166
398
  var MAX_ASCENT = 50;
@@ -183,7 +415,7 @@ var isFile = async (path) => {
183
415
  var findGitDir = async (start) => {
184
416
  let current = resolve(start);
185
417
  for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
186
- const candidate = join3(current, GIT_DIR);
418
+ const candidate = join5(current, GIT_DIR);
187
419
  if (await isDirectory(candidate)) {
188
420
  return candidate;
189
421
  }
@@ -227,7 +459,7 @@ var GitRepositoryInfoReader = class {
227
459
  return void 0;
228
460
  }
229
461
  try {
230
- const headPath = join3(gitDir, HEAD_FILE);
462
+ const headPath = join5(gitDir, HEAD_FILE);
231
463
  const content = await readFile(headPath, "utf8");
232
464
  const branch = parseHead(content);
233
465
  const root = dirname(gitDir);
@@ -239,12 +471,12 @@ var GitRepositoryInfoReader = class {
239
471
  };
240
472
 
241
473
  // src/infrastructure/process.ts
242
- import { spawn } from "child_process";
474
+ import { spawn as spawn2 } from "child_process";
243
475
  var NodeProcessLauncher = class {
244
476
  async launch(command, args, options) {
245
477
  const detached = options?.detached ?? false;
246
478
  await new Promise((resolve4, reject) => {
247
- const child = spawn(command, args, {
479
+ const child = spawn2(command, args, {
248
480
  detached,
249
481
  stdio: "ignore"
250
482
  });
@@ -360,7 +592,7 @@ var detectTerminalTheme = async (timeoutMs = 100) => {
360
592
 
361
593
  // src/infrastructure/unityhub.ts
362
594
  import { readFile as readFile2, writeFile } from "fs/promises";
363
- import { basename } from "path";
595
+ import { basename as basename3 } from "path";
364
596
  var HUB_PROJECTS_PATH = `${process.env.HOME ?? ""}/Library/Application Support/UnityHub/projects-v1.json`;
365
597
  var schemaVersion = "v1";
366
598
  var toUnityProject = (entry) => {
@@ -375,7 +607,7 @@ var toUnityProject = (entry) => {
375
607
  const lastModified = typeof entry.lastModified === "number" ? new Date(entry.lastModified) : void 0;
376
608
  return {
377
609
  id: safePath,
378
- title: entry.title?.trim() || basename(safePath),
610
+ title: entry.title?.trim() || basename3(safePath),
379
611
  path: safePath,
380
612
  version: { value: version },
381
613
  lastModified,
@@ -490,9 +722,9 @@ var MacUnityHubProjectsReader = class {
490
722
 
491
723
  // src/infrastructure/unityhub.win.ts
492
724
  import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
493
- import { basename as basename2, join as join4 } from "path";
494
- var HUB_DIR = join4(process.env.APPDATA ?? "", "UnityHub");
495
- var HUB_PROJECTS_PATH2 = join4(HUB_DIR, "projects-v1.json");
725
+ import { basename as basename4, join as join6 } from "path";
726
+ var HUB_DIR = join6(process.env.APPDATA ?? "", "UnityHub");
727
+ var HUB_PROJECTS_PATH2 = join6(HUB_DIR, "projects-v1.json");
496
728
  var schemaVersion2 = "v1";
497
729
  var toUnityProject2 = (entry) => {
498
730
  const safePath = entry.path;
@@ -506,7 +738,7 @@ var toUnityProject2 = (entry) => {
506
738
  const lastModified = typeof entry.lastModified === "number" ? new Date(entry.lastModified) : void 0;
507
739
  return {
508
740
  id: safePath,
509
- title: entry.title?.trim() || basename2(safePath),
741
+ title: entry.title?.trim() || basename4(safePath),
510
742
  path: safePath,
511
743
  version: { value: version },
512
744
  lastModified,
@@ -594,7 +826,7 @@ var WinUnityHubProjectsReader = class {
594
826
  await writeFile2(HUB_PROJECTS_PATH2, JSON.stringify(json, void 0, 2), "utf8");
595
827
  }
596
828
  async readCliArgs(projectPath) {
597
- const infoPath = join4(HUB_DIR, "projectsInfo.json");
829
+ const infoPath = join6(HUB_DIR, "projectsInfo.json");
598
830
  let content;
599
831
  try {
600
832
  content = await readFile3(infoPath, "utf8");
@@ -620,18 +852,18 @@ var WinUnityHubProjectsReader = class {
620
852
  };
621
853
 
622
854
  // src/infrastructure/unityLock.ts
623
- import { execFile } from "child_process";
624
- import { constants as constants3 } from "fs";
625
- import { access as access3, rm } from "fs/promises";
626
- import { join as join5 } from "path";
627
- import { promisify } from "util";
628
- var execFileAsync = promisify(execFile);
855
+ import { execFile as execFile3 } from "child_process";
856
+ import { constants as constants5 } from "fs";
857
+ import { access as access5, rm } from "fs/promises";
858
+ import { join as join7 } from "path";
859
+ import { promisify as promisify3 } from "util";
860
+ var execFileAsync3 = promisify3(execFile3);
629
861
  var buildBringToFrontScript = (pid) => {
630
862
  return `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
631
863
  };
632
864
  var pathExists = async (target) => {
633
865
  try {
634
- await access3(target, constants3.F_OK);
866
+ await access5(target, constants5.F_OK);
635
867
  return true;
636
868
  } catch {
637
869
  return false;
@@ -651,7 +883,7 @@ var UnityLockChecker = class {
651
883
  await this.bringUnityToFront(activeProcess.pid);
652
884
  return "skip";
653
885
  }
654
- const lockfilePath = join5(projectPath, "Temp", "UnityLockfile");
886
+ const lockfilePath = join7(projectPath, "Temp", "UnityLockfile");
655
887
  const hasLockfile = await pathExists(lockfilePath);
656
888
  if (!hasLockfile) {
657
889
  return "allow";
@@ -680,7 +912,7 @@ var UnityLockChecker = class {
680
912
  }
681
913
  try {
682
914
  const script = buildBringToFrontScript(pid);
683
- await execFileAsync("osascript", ["-e", script]);
915
+ await execFileAsync3("osascript", ["-e", script]);
684
916
  } catch (error) {
685
917
  const message = error instanceof Error ? error.message : String(error);
686
918
  console.error(`Failed to bring Unity to front: ${message}`);
@@ -689,16 +921,16 @@ var UnityLockChecker = class {
689
921
  };
690
922
  var UnityLockStatusReader = class {
691
923
  async isLocked(projectPath) {
692
- const lockfilePath = join5(projectPath, "Temp", "UnityLockfile");
924
+ const lockfilePath = join7(projectPath, "Temp", "UnityLockfile");
693
925
  return await pathExists(lockfilePath);
694
926
  }
695
927
  };
696
928
 
697
929
  // src/infrastructure/unityProcess.ts
698
- import { execFile as execFile2 } from "child_process";
930
+ import { execFile as execFile4 } from "child_process";
699
931
  import { resolve as resolve2 } from "path";
700
- import { promisify as promisify2 } from "util";
701
- var execFileAsync2 = promisify2(execFile2);
932
+ import { promisify as promisify4 } from "util";
933
+ var execFileAsync4 = promisify4(execFile4);
702
934
  var UNITY_EXECUTABLE_PATTERN = /Unity\.app\/Contents\/MacOS\/Unity/i;
703
935
  var PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
704
936
  var PROCESS_LIST_ARGS = ["-axo", "pid=,command=", "-ww"];
@@ -781,7 +1013,7 @@ var MacUnityProcessReader = class {
781
1013
  async listUnityProcesses() {
782
1014
  let stdout;
783
1015
  try {
784
- const result = await execFileAsync2(PROCESS_LIST_COMMAND, PROCESS_LIST_ARGS);
1016
+ const result = await execFileAsync4(PROCESS_LIST_COMMAND, PROCESS_LIST_ARGS);
785
1017
  stdout = result.stdout;
786
1018
  } catch (error) {
787
1019
  throw new Error(`Failed to retrieve Unity process list: ${error instanceof Error ? error.message : String(error)}`);
@@ -811,7 +1043,7 @@ var MacUnityProcessReader = class {
811
1043
  pid: pidValue,
812
1044
  projectPath: normalizePath(projectArgument)
813
1045
  };
814
- }).filter((process3) => Boolean(process3));
1046
+ }).filter((process4) => Boolean(process4));
815
1047
  }
816
1048
  };
817
1049
  var MacUnityProcessTerminator = class {
@@ -826,7 +1058,7 @@ var MacUnityProcessTerminator = class {
826
1058
  ' keystroke "q" using {command down}',
827
1059
  "end tell"
828
1060
  ].join("\n");
829
- await execFileAsync2("osascript", ["-e", script]);
1061
+ await execFileAsync4("osascript", ["-e", script]);
830
1062
  const deadlineGraceful = Date.now() + GRACEFUL_QUIT_TIMEOUT_MILLIS;
831
1063
  while (Date.now() < deadlineGraceful) {
832
1064
  await delay(GRACEFUL_QUIT_POLL_INTERVAL_MILLIS);
@@ -873,10 +1105,10 @@ var MacUnityProcessTerminator = class {
873
1105
  };
874
1106
 
875
1107
  // src/infrastructure/unityProcess.win.ts
876
- import { execFile as execFile3 } from "child_process";
1108
+ import { execFile as execFile5 } from "child_process";
877
1109
  import { resolve as resolve3 } from "path";
878
- import { promisify as promisify3 } from "util";
879
- var execFileAsync3 = promisify3(execFile3);
1110
+ import { promisify as promisify5 } from "util";
1111
+ var execFileAsync5 = promisify5(execFile5);
880
1112
  var PROJECT_PATH_PATTERN2 = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
881
1113
  var TERMINATE_TIMEOUT_MILLIS2 = 5e3;
882
1114
  var TERMINATE_POLL_INTERVAL_MILLIS2 = 200;
@@ -967,7 +1199,7 @@ var WinUnityProcessReader = class {
967
1199
  ].join(" ");
968
1200
  let stdout;
969
1201
  try {
970
- const result = await execFileAsync3(
1202
+ const result = await execFileAsync5(
971
1203
  "powershell.exe",
972
1204
  [
973
1205
  "-NoProfile",
@@ -1004,7 +1236,7 @@ var WinUnityProcessReader = class {
1004
1236
  var WinUnityProcessTerminator = class {
1005
1237
  async terminate(unityProcess) {
1006
1238
  try {
1007
- await execFileAsync3("powershell.exe", [
1239
+ await execFileAsync5("powershell.exe", [
1008
1240
  "-NoProfile",
1009
1241
  "-NonInteractive",
1010
1242
  "-ExecutionPolicy",
@@ -1026,7 +1258,7 @@ var WinUnityProcessTerminator = class {
1026
1258
  }
1027
1259
  }
1028
1260
  try {
1029
- await execFileAsync3("powershell.exe", [
1261
+ await execFileAsync5("powershell.exe", [
1030
1262
  "-NoProfile",
1031
1263
  "-NonInteractive",
1032
1264
  "-ExecutionPolicy",
@@ -1048,11 +1280,11 @@ var WinUnityProcessTerminator = class {
1048
1280
 
1049
1281
  // src/infrastructure/unityTemp.ts
1050
1282
  import { rm as rm2 } from "fs/promises";
1051
- import { join as join6 } from "path";
1283
+ import { join as join8 } from "path";
1052
1284
  var TEMP_DIRECTORY_NAME = "Temp";
1053
1285
  var UnityTempDirectoryCleaner = class {
1054
1286
  async clean(projectPath) {
1055
- const tempDirectoryPath = join6(projectPath, TEMP_DIRECTORY_NAME);
1287
+ const tempDirectoryPath = join8(projectPath, TEMP_DIRECTORY_NAME);
1056
1288
  try {
1057
1289
  await rm2(tempDirectoryPath, {
1058
1290
  recursive: true,
@@ -1064,7 +1296,8 @@ var UnityTempDirectoryCleaner = class {
1064
1296
  };
1065
1297
 
1066
1298
  // src/presentation/App.tsx
1067
- import { basename as basename4 } from "path";
1299
+ import { basename as basename6 } from "path";
1300
+ import process2 from "process";
1068
1301
  import clipboard from "clipboardy";
1069
1302
  import { Box as Box6, Text as Text4, useApp, useInput, useStdout as useStdout2 } from "ink";
1070
1303
  import { useCallback, useEffect as useEffect4, useMemo as useMemo2, useState as useState4 } from "react";
@@ -1108,7 +1341,7 @@ var LayoutManager = ({
1108
1341
  };
1109
1342
 
1110
1343
  // src/presentation/components/ProjectList.tsx
1111
- import { basename as basename3 } from "path";
1344
+ import { basename as basename5 } from "path";
1112
1345
  import { Box as Box3 } from "ink";
1113
1346
  import { useMemo } from "react";
1114
1347
 
@@ -1258,7 +1491,7 @@ var extractRootFolder = (repository) => {
1258
1491
  if (!repository?.root) {
1259
1492
  return void 0;
1260
1493
  }
1261
- const base = basename3(repository.root);
1494
+ const base = basename5(repository.root);
1262
1495
  return base || void 0;
1263
1496
  };
1264
1497
  var formatProjectName = (projectTitle, repository, useGitRootName) => {
@@ -1691,11 +1924,12 @@ var extractRootFolder2 = (repository) => {
1691
1924
  if (!repository?.root) {
1692
1925
  return void 0;
1693
1926
  }
1694
- const base = basename4(repository.root);
1927
+ const base = basename6(repository.root);
1695
1928
  return base || void 0;
1696
1929
  };
1697
1930
  var minimumVisibleProjectCount = 4;
1698
- var defaultHintMessage = "j/k Select \xB7 [o]pen [q]uit [r]efresh [c]opy [s]ort [v]isibility \xB7 ^C Exit";
1931
+ var editorOnlyKey = process2.platform === "darwin" ? "\u2325o" : "Alt+o";
1932
+ var defaultHintMessage = `j/k Select \xB7 [o]pen [O]+Editor [${editorOnlyKey}]Editor [q]uit [r]efresh [c]opy [s]ort [v]isibility \xB7 ^C Exit`;
1699
1933
  var getCopyTargetPath = (view) => {
1700
1934
  const root = view.repository?.root;
1701
1935
  return root && root.length > 0 ? root : view.project.path;
@@ -1703,6 +1937,8 @@ var getCopyTargetPath = (view) => {
1703
1937
  var App = ({
1704
1938
  projects,
1705
1939
  onLaunch,
1940
+ onLaunchWithEditor,
1941
+ onLaunchEditorOnly,
1706
1942
  onTerminate,
1707
1943
  onRefresh,
1708
1944
  useGitRootName = true
@@ -1799,9 +2035,9 @@ var App = ({
1799
2035
  const handleSigint = () => {
1800
2036
  exit();
1801
2037
  };
1802
- process.on("SIGINT", handleSigint);
2038
+ process2.on("SIGINT", handleSigint);
1803
2039
  return () => {
1804
- process.off("SIGINT", handleSigint);
2040
+ process2.off("SIGINT", handleSigint);
1805
2041
  };
1806
2042
  }, [exit]);
1807
2043
  const limit = Math.max(1, visibleCount);
@@ -1948,6 +2184,100 @@ var App = ({
1948
2184
  }, 3e3);
1949
2185
  }
1950
2186
  }, [index, onLaunch, sortedProjects]);
2187
+ const launchSelectedWithEditor = useCallback(async () => {
2188
+ if (!onLaunchWithEditor) {
2189
+ setHint("Launch with editor not available");
2190
+ setTimeout(() => {
2191
+ setHint(defaultHintMessage);
2192
+ }, 2e3);
2193
+ return;
2194
+ }
2195
+ const projectView = sortedProjects[index];
2196
+ if (!projectView) {
2197
+ setHint("No project to launch");
2198
+ setTimeout(() => {
2199
+ setHint(defaultHintMessage);
2200
+ }, 2e3);
2201
+ return;
2202
+ }
2203
+ const { project } = projectView;
2204
+ try {
2205
+ const cdTarget = getCopyTargetPath(projectView);
2206
+ const command = buildCdCommand(cdTarget);
2207
+ clipboard.writeSync(command);
2208
+ } catch (error) {
2209
+ const message = error instanceof Error ? error.message : String(error);
2210
+ setHint(`Failed to copy: ${message}`);
2211
+ setTimeout(() => {
2212
+ setHint(defaultHintMessage);
2213
+ }, 3e3);
2214
+ return;
2215
+ }
2216
+ try {
2217
+ const result = await onLaunchWithEditor(project);
2218
+ setLaunchedProjects((previous) => {
2219
+ const next = new Set(previous);
2220
+ next.add(project.id);
2221
+ return next;
2222
+ });
2223
+ setReleasedProjects((previous) => {
2224
+ if (!previous.has(project.id)) {
2225
+ return previous;
2226
+ }
2227
+ const next = new Set(previous);
2228
+ next.delete(project.id);
2229
+ return next;
2230
+ });
2231
+ setHint(result.message);
2232
+ setTimeout(() => {
2233
+ setHint(defaultHintMessage);
2234
+ }, 3e3);
2235
+ } catch (error) {
2236
+ if (error instanceof LaunchCancelledError) {
2237
+ setHint("Launch cancelled");
2238
+ setTimeout(() => {
2239
+ setHint(defaultHintMessage);
2240
+ }, 3e3);
2241
+ return;
2242
+ }
2243
+ const message = error instanceof Error ? error.message : String(error);
2244
+ setHint(`Failed to launch: ${message}`);
2245
+ setTimeout(() => {
2246
+ setHint(defaultHintMessage);
2247
+ }, 3e3);
2248
+ }
2249
+ }, [index, onLaunchWithEditor, sortedProjects]);
2250
+ const launchEditorOnly = useCallback(async () => {
2251
+ if (!onLaunchEditorOnly) {
2252
+ setHint("Launch editor only not available");
2253
+ setTimeout(() => {
2254
+ setHint(defaultHintMessage);
2255
+ }, 2e3);
2256
+ return;
2257
+ }
2258
+ const projectView = sortedProjects[index];
2259
+ if (!projectView) {
2260
+ setHint("No project to open");
2261
+ setTimeout(() => {
2262
+ setHint(defaultHintMessage);
2263
+ }, 2e3);
2264
+ return;
2265
+ }
2266
+ const { project } = projectView;
2267
+ try {
2268
+ const result = await onLaunchEditorOnly(project);
2269
+ setHint(result.message);
2270
+ setTimeout(() => {
2271
+ setHint(defaultHintMessage);
2272
+ }, 3e3);
2273
+ } catch (error) {
2274
+ const message = error instanceof Error ? error.message : String(error);
2275
+ setHint(`Failed to launch editor: ${message}`);
2276
+ setTimeout(() => {
2277
+ setHint(defaultHintMessage);
2278
+ }, 3e3);
2279
+ }
2280
+ }, [index, onLaunchEditorOnly, sortedProjects]);
1951
2281
  const terminateSelected = useCallback(async () => {
1952
2282
  const projectView = sortedProjects[index];
1953
2283
  if (!projectView) {
@@ -2151,10 +2481,18 @@ var App = ({
2151
2481
  void terminateSelected();
2152
2482
  return;
2153
2483
  }
2484
+ if (input === "\xF8" || input === "o" && key.meta) {
2485
+ void launchEditorOnly();
2486
+ return;
2487
+ }
2154
2488
  if (input === "o") {
2155
2489
  void launchSelected();
2156
2490
  return;
2157
2491
  }
2492
+ if (input === "O") {
2493
+ void launchSelectedWithEditor();
2494
+ return;
2495
+ }
2158
2496
  if (input === "r") {
2159
2497
  void refreshProjects();
2160
2498
  return;
@@ -2239,7 +2577,7 @@ var App = ({
2239
2577
  // src/index.tsx
2240
2578
  import { jsx as jsx7 } from "react/jsx-runtime";
2241
2579
  var bootstrap = async () => {
2242
- const isWindows = process2.platform === "win32";
2580
+ const isWindows = process3.platform === "win32";
2243
2581
  const unityHubReader = isWindows ? new WinUnityHubProjectsReader() : new MacUnityHubProjectsReader();
2244
2582
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
2245
2583
  const lockStatusReader = new UnityLockStatusReader();
@@ -2268,10 +2606,21 @@ var bootstrap = async () => {
2268
2606
  unityProcessTerminator,
2269
2607
  unityTempDirectoryCleaner
2270
2608
  );
2271
- const useGitRootName = !process2.argv.includes("--no-git-root-name");
2609
+ const externalEditorPathReader = isWindows ? new WinExternalEditorPathReader() : new MacExternalEditorPathReader();
2610
+ const externalEditorLauncher = isWindows ? new WinExternalEditorLauncher() : new MacExternalEditorLauncher();
2611
+ const launchWithEditorUseCase = new LaunchWithEditorUseCase(
2612
+ launchProjectUseCase,
2613
+ externalEditorPathReader,
2614
+ externalEditorLauncher
2615
+ );
2616
+ const launchEditorOnlyUseCase = new LaunchEditorOnlyUseCase(
2617
+ externalEditorPathReader,
2618
+ externalEditorLauncher
2619
+ );
2620
+ const useGitRootName = !process3.argv.includes("--no-git-root-name");
2272
2621
  try {
2273
2622
  const rawModeSupported = Boolean(
2274
- process2.stdin.isTTY && typeof process2.stdin.setRawMode === "function"
2623
+ process3.stdin.isTTY && typeof process3.stdin.setRawMode === "function"
2275
2624
  );
2276
2625
  if (!rawModeSupported) {
2277
2626
  const message = [
@@ -2284,7 +2633,7 @@ var bootstrap = async () => {
2284
2633
  "Details: https://github.com/vadimdemedes/ink/#israwmodesupported"
2285
2634
  ].join("\n");
2286
2635
  console.error(message);
2287
- process2.exitCode = 1;
2636
+ process3.exitCode = 1;
2288
2637
  return;
2289
2638
  }
2290
2639
  const theme = await detectTerminalTheme();
@@ -2295,6 +2644,8 @@ var bootstrap = async () => {
2295
2644
  {
2296
2645
  projects,
2297
2646
  onLaunch: (project) => launchProjectUseCase.execute(project),
2647
+ onLaunchWithEditor: (project) => launchWithEditorUseCase.execute(project),
2648
+ onLaunchEditorOnly: (project) => launchEditorOnlyUseCase.execute(project),
2298
2649
  onTerminate: (project) => terminateProjectUseCase.execute(project),
2299
2650
  onRefresh: () => listProjectsUseCase.execute(),
2300
2651
  useGitRootName
@@ -2302,15 +2653,15 @@ var bootstrap = async () => {
2302
2653
  ) })
2303
2654
  );
2304
2655
  await waitUntilExit();
2305
- process2.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2656
+ process3.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2306
2657
  } catch (error) {
2307
2658
  const message = error instanceof Error ? error.message : String(error);
2308
2659
  console.error(message);
2309
- process2.exitCode = 1;
2660
+ process3.exitCode = 1;
2310
2661
  }
2311
2662
  };
2312
2663
  void bootstrap().catch((error) => {
2313
2664
  const message = error instanceof Error ? error.message : String(error);
2314
2665
  console.error(message);
2315
- process2.exitCode = 1;
2666
+ process3.exitCode = 1;
2316
2667
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.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": {