unity-hub-cli 0.12.0 → 0.13.1

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 +13 -2
  2. package/dist/index.js +432 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,9 +8,12 @@ A CLI tool that displays the same content as Unity Hub in an Ink-based TUI, allo
8
8
 
9
9
  ## Requirements
10
10
 
11
- - macOS
11
+ - macOS or Windows 10/11
12
12
  - Node.js 20+
13
- - Unity Hub (with `~/Library/Application Support/UnityHub/projects-v1.json` present)
13
+ - Unity Hub
14
+ - macOS: `~/Library/Application Support/UnityHub/projects-v1.json`
15
+ - Windows: `%APPDATA%\UnityHub\projects-v1.json`
16
+ - Windows Editor path (default): `C:\\Program Files\\Unity\\Hub\\Editor\\<version>\\Editor\\Unity.exe`
14
17
 
15
18
  ## Usage
16
19
 
@@ -37,6 +40,14 @@ npx unity-hub-cli
37
40
  node dist/index.js
38
41
  ```
39
42
 
43
+ On Windows, it works from PowerShell and CMD. Git Bash is supported when running inside a ConPTY-based terminal (Windows Terminal or VS Code/Cursor integrated terminal). On standalone Git Bash (MinTTY), raw mode is not supported; use PowerShell/CMD/Windows Terminal. If you must use MinTTY Git Bash, run one of the following:
44
+
45
+ - `winpty cmd.exe /c npx unity-hub-cli`
46
+ - `winpty powershell.exe -NoProfile -Command npx unity-hub-cli`
47
+ - If already built: `npm run build && winpty node dist/index.js`
48
+
49
+ See `https://github.com/vadimdemedes/ink/#israwmodesupported`.
50
+
40
51
  By default, the project list uses the Git repository root folder name when available.
41
52
 
42
53
  ### CLI Options
package/dist/index.js CHANGED
@@ -124,9 +124,43 @@ var MacEditorPathResolver = class {
124
124
  }
125
125
  };
126
126
 
127
+ // src/infrastructure/editor.win.ts
128
+ import { constants as constants2 } from "fs";
129
+ import { access as access2 } from "fs/promises";
130
+ import { join as join2 } from "path";
131
+ var buildCandidateBases = () => {
132
+ const candidates = [];
133
+ const programFiles = process.env.PROGRAMFILES ?? "C:\\Program Files";
134
+ const programW6432 = process.env.ProgramW6432 ?? process.env.PROGRAMFILES;
135
+ const localAppData = process.env.LOCALAPPDATA;
136
+ candidates.push(join2(programFiles, "Unity", "Hub", "Editor"));
137
+ if (programW6432) {
138
+ candidates.push(join2(programW6432, "Unity", "Hub", "Editor"));
139
+ }
140
+ if (localAppData) {
141
+ candidates.push(join2(localAppData, "Unity", "Hub", "Editor"));
142
+ }
143
+ return Array.from(new Set(candidates));
144
+ };
145
+ var WinEditorPathResolver = class {
146
+ async resolve(version) {
147
+ const tried = [];
148
+ for (const base of buildCandidateBases()) {
149
+ const candidate = join2(base, version.value, "Editor", "Unity.exe");
150
+ try {
151
+ await access2(candidate, constants2.F_OK);
152
+ return candidate;
153
+ } catch {
154
+ tried.push(candidate);
155
+ }
156
+ }
157
+ throw new Error(`Unity Editor not found for version ${version.value}. Tried: ${tried.join(" , ")}`);
158
+ }
159
+ };
160
+
127
161
  // src/infrastructure/git.ts
128
162
  import { readFile, stat } from "fs/promises";
129
- import { dirname, join as join2, resolve } from "path";
163
+ import { dirname, join as join3, resolve } from "path";
130
164
  var HEAD_FILE = "HEAD";
131
165
  var GIT_DIR = ".git";
132
166
  var MAX_ASCENT = 50;
@@ -149,7 +183,7 @@ var isFile = async (path) => {
149
183
  var findGitDir = async (start) => {
150
184
  let current = resolve(start);
151
185
  for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
152
- const candidate = join2(current, GIT_DIR);
186
+ const candidate = join3(current, GIT_DIR);
153
187
  if (await isDirectory(candidate)) {
154
188
  return candidate;
155
189
  }
@@ -193,7 +227,7 @@ var GitRepositoryInfoReader = class {
193
227
  return void 0;
194
228
  }
195
229
  try {
196
- const headPath = join2(gitDir, HEAD_FILE);
230
+ const headPath = join3(gitDir, HEAD_FILE);
197
231
  const content = await readFile(headPath, "utf8");
198
232
  const branch = parseHead(content);
199
233
  const root = dirname(gitDir);
@@ -209,7 +243,7 @@ import { spawn } from "child_process";
209
243
  var NodeProcessLauncher = class {
210
244
  async launch(command, args, options) {
211
245
  const detached = options?.detached ?? false;
212
- await new Promise((resolve3, reject) => {
246
+ await new Promise((resolve4, reject) => {
213
247
  const child = spawn(command, args, {
214
248
  detached,
215
249
  stdio: "ignore"
@@ -221,7 +255,7 @@ var NodeProcessLauncher = class {
221
255
  const handleSpawn = () => {
222
256
  child.off("error", handleError);
223
257
  child.unref();
224
- resolve3();
258
+ resolve4();
225
259
  };
226
260
  child.once("error", handleError);
227
261
  child.once("spawn", handleSpawn);
@@ -275,7 +309,7 @@ var sortByFavoriteThenLastModified = (projects) => {
275
309
  return timeB - timeA;
276
310
  });
277
311
  };
278
- var UnityHubProjectsReader = class {
312
+ var MacUnityHubProjectsReader = class {
279
313
  async listProjects() {
280
314
  let content;
281
315
  try {
@@ -359,11 +393,142 @@ var UnityHubProjectsReader = class {
359
393
  }
360
394
  };
361
395
 
396
+ // src/infrastructure/unityhub.win.ts
397
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
398
+ import { basename as basename2, join as join4 } from "path";
399
+ var HUB_DIR = join4(process.env.APPDATA ?? "", "UnityHub");
400
+ var HUB_PROJECTS_PATH2 = join4(HUB_DIR, "projects-v1.json");
401
+ var schemaVersion2 = "v1";
402
+ var toUnityProject2 = (entry) => {
403
+ const safePath = entry.path;
404
+ if (!safePath) {
405
+ throw new Error("Unity Hub entry is missing project path");
406
+ }
407
+ const version = entry.version;
408
+ if (!version) {
409
+ throw new Error(`Unity Hub entry ${safePath} is missing version`);
410
+ }
411
+ const lastModified = typeof entry.lastModified === "number" ? new Date(entry.lastModified) : void 0;
412
+ return {
413
+ id: safePath,
414
+ title: entry.title?.trim() || basename2(safePath),
415
+ path: safePath,
416
+ version: { value: version },
417
+ lastModified,
418
+ favorite: entry.isFavorite === true
419
+ };
420
+ };
421
+ var normalizeValue2 = (value) => value.toLocaleLowerCase();
422
+ var sortByFavoriteThenLastModified2 = (projects) => {
423
+ return [...projects].sort((a, b) => {
424
+ const favoriteRankA = a.favorite ? 0 : 1;
425
+ const favoriteRankB = b.favorite ? 0 : 1;
426
+ if (favoriteRankA !== favoriteRankB) {
427
+ return favoriteRankA - favoriteRankB;
428
+ }
429
+ const fallbackTime = 0;
430
+ const timeA = a.lastModified?.getTime() ?? fallbackTime;
431
+ const timeB = b.lastModified?.getTime() ?? fallbackTime;
432
+ if (timeA === timeB) {
433
+ const titleA = normalizeValue2(a.title);
434
+ const titleB = normalizeValue2(b.title);
435
+ if (titleA === titleB) {
436
+ return normalizeValue2(a.path).localeCompare(normalizeValue2(b.path));
437
+ }
438
+ return titleA.localeCompare(titleB);
439
+ }
440
+ return timeB - timeA;
441
+ });
442
+ };
443
+ var WinUnityHubProjectsReader = class {
444
+ async listProjects() {
445
+ let content;
446
+ try {
447
+ content = await readFile3(HUB_PROJECTS_PATH2, "utf8");
448
+ } catch {
449
+ throw new Error(
450
+ `Unity Hub project list not found (${HUB_PROJECTS_PATH2}).`
451
+ );
452
+ }
453
+ let json;
454
+ try {
455
+ json = JSON.parse(content);
456
+ } catch {
457
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
458
+ }
459
+ if (json.schema_version && json.schema_version !== schemaVersion2) {
460
+ throw new Error(`Unsupported schema_version (${json.schema_version}).`);
461
+ }
462
+ const entries = Object.values(json.data ?? {});
463
+ if (entries.length === 0) {
464
+ return [];
465
+ }
466
+ const projects = entries.map(toUnityProject2);
467
+ return sortByFavoriteThenLastModified2(projects);
468
+ }
469
+ async updateLastModified(projectPath, date) {
470
+ let content;
471
+ try {
472
+ content = await readFile3(HUB_PROJECTS_PATH2, "utf8");
473
+ } catch {
474
+ throw new Error(
475
+ `Unity Hub project list not found (${HUB_PROJECTS_PATH2}).`
476
+ );
477
+ }
478
+ let json;
479
+ try {
480
+ json = JSON.parse(content);
481
+ } catch {
482
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
483
+ }
484
+ if (!json.data) {
485
+ return;
486
+ }
487
+ const projectKey = Object.keys(json.data).find((key) => json.data?.[key]?.path === projectPath);
488
+ if (!projectKey) {
489
+ return;
490
+ }
491
+ const original = json.data[projectKey];
492
+ if (!original) {
493
+ return;
494
+ }
495
+ json.data[projectKey] = {
496
+ ...original,
497
+ lastModified: date.getTime()
498
+ };
499
+ await writeFile2(HUB_PROJECTS_PATH2, JSON.stringify(json, void 0, 2), "utf8");
500
+ }
501
+ async readCliArgs(projectPath) {
502
+ const infoPath = join4(HUB_DIR, "projectsInfo.json");
503
+ let content;
504
+ try {
505
+ content = await readFile3(infoPath, "utf8");
506
+ } catch {
507
+ return [];
508
+ }
509
+ let json;
510
+ try {
511
+ json = JSON.parse(content);
512
+ } catch {
513
+ return [];
514
+ }
515
+ const entry = json[projectPath];
516
+ if (!entry?.cliArgs) {
517
+ return [];
518
+ }
519
+ const tokens = entry.cliArgs.match(/(?:"[^"]*"|'[^']*'|[^\s"']+)/g);
520
+ if (!tokens) {
521
+ return [];
522
+ }
523
+ return tokens.map((token) => token.replace(/^['"]|['"]$/g, ""));
524
+ }
525
+ };
526
+
362
527
  // src/infrastructure/unityLock.ts
363
528
  import { execFile } from "child_process";
364
- import { constants as constants2 } from "fs";
365
- import { access as access2, rm } from "fs/promises";
366
- import { join as join3 } from "path";
529
+ import { constants as constants3 } from "fs";
530
+ import { access as access3, rm } from "fs/promises";
531
+ import { join as join5 } from "path";
367
532
  import { promisify } from "util";
368
533
  var execFileAsync = promisify(execFile);
369
534
  var buildBringToFrontScript = (pid) => {
@@ -371,7 +536,7 @@ var buildBringToFrontScript = (pid) => {
371
536
  };
372
537
  var pathExists = async (target) => {
373
538
  try {
374
- await access2(target, constants2.F_OK);
539
+ await access3(target, constants3.F_OK);
375
540
  return true;
376
541
  } catch {
377
542
  return false;
@@ -391,7 +556,7 @@ var UnityLockChecker = class {
391
556
  await this.bringUnityToFront(activeProcess.pid);
392
557
  return "skip";
393
558
  }
394
- const lockfilePath = join3(projectPath, "Temp", "UnityLockfile");
559
+ const lockfilePath = join5(projectPath, "Temp", "UnityLockfile");
395
560
  const hasLockfile = await pathExists(lockfilePath);
396
561
  if (!hasLockfile) {
397
562
  return "allow";
@@ -429,7 +594,7 @@ var UnityLockChecker = class {
429
594
  };
430
595
  var UnityLockStatusReader = class {
431
596
  async isLocked(projectPath) {
432
- const lockfilePath = join3(projectPath, "Temp", "UnityLockfile");
597
+ const lockfilePath = join5(projectPath, "Temp", "UnityLockfile");
433
598
  return await pathExists(lockfilePath);
434
599
  }
435
600
  };
@@ -602,13 +767,187 @@ var MacUnityProcessTerminator = class {
602
767
  }
603
768
  };
604
769
 
770
+ // src/infrastructure/unityProcess.win.ts
771
+ import { execFile as execFile3 } from "child_process";
772
+ import { resolve as resolve3 } from "path";
773
+ import { promisify as promisify3 } from "util";
774
+ var execFileAsync3 = promisify3(execFile3);
775
+ var PROJECT_PATH_PATTERN2 = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
776
+ var TERMINATE_TIMEOUT_MILLIS2 = 5e3;
777
+ var TERMINATE_POLL_INTERVAL_MILLIS2 = 200;
778
+ var delay2 = async (duration) => {
779
+ await new Promise((resolveDelay) => {
780
+ setTimeout(() => {
781
+ resolveDelay();
782
+ }, duration);
783
+ });
784
+ };
785
+ var normalizePath2 = (target) => {
786
+ const resolved = resolve3(target);
787
+ if (resolved.endsWith("/") || resolved.endsWith("\\")) {
788
+ return resolved.slice(0, -1);
789
+ }
790
+ return resolved;
791
+ };
792
+ var arePathsEqual2 = (left, right) => {
793
+ const normalizedLeft = normalizePath2(left);
794
+ const normalizedRight = normalizePath2(right);
795
+ return normalizedLeft.localeCompare(normalizedRight, void 0, { sensitivity: "base" }) === 0;
796
+ };
797
+ var extractProjectPath2 = (command) => {
798
+ const match = command.match(PROJECT_PATH_PATTERN2);
799
+ if (!match) {
800
+ return void 0;
801
+ }
802
+ const raw = match[1];
803
+ if (!raw) {
804
+ return void 0;
805
+ }
806
+ const trimmed = raw.trim();
807
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
808
+ return trimmed.slice(1, -1);
809
+ }
810
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
811
+ return trimmed.slice(1, -1);
812
+ }
813
+ return trimmed;
814
+ };
815
+ var isProcessMissingError2 = (error) => {
816
+ if (typeof error !== "object" || error === null) {
817
+ return false;
818
+ }
819
+ const nodeError = error;
820
+ return nodeError.code === "ESRCH";
821
+ };
822
+ var ensureProcessAlive2 = (pid) => {
823
+ try {
824
+ process.kill(pid, 0);
825
+ return true;
826
+ } catch (error) {
827
+ if (isProcessMissingError2(error)) {
828
+ return false;
829
+ }
830
+ return false;
831
+ }
832
+ };
833
+ var parsePowershellJson = (jsonText) => {
834
+ try {
835
+ const parsed = JSON.parse(jsonText);
836
+ if (!parsed) {
837
+ return [];
838
+ }
839
+ if (Array.isArray(parsed)) {
840
+ return parsed.filter((r) => r && typeof r === "object" && typeof r.ProcessId === "number");
841
+ }
842
+ const single = parsed;
843
+ if (typeof single.ProcessId === "number") {
844
+ return [single];
845
+ }
846
+ return [];
847
+ } catch {
848
+ return [];
849
+ }
850
+ };
851
+ var WinUnityProcessReader = class {
852
+ async findByProjectPath(projectPath) {
853
+ const normalizedTarget = normalizePath2(projectPath);
854
+ const processes = await this.listUnityProcesses();
855
+ return processes.find((candidate) => arePathsEqual2(candidate.projectPath, normalizedTarget));
856
+ }
857
+ async listUnityProcesses() {
858
+ const psCommand = [
859
+ "$ErrorActionPreference = 'SilentlyContinue';",
860
+ `$procs = Get-CimInstance Win32_Process -Filter "Name='Unity.exe'";`,
861
+ "$procs | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
862
+ ].join(" ");
863
+ let stdout;
864
+ try {
865
+ const result = await execFileAsync3(
866
+ "powershell.exe",
867
+ [
868
+ "-NoProfile",
869
+ "-NonInteractive",
870
+ "-ExecutionPolicy",
871
+ "Bypass",
872
+ "-Command",
873
+ psCommand
874
+ ],
875
+ { encoding: "utf8" }
876
+ );
877
+ stdout = (result.stdout ?? "").trim();
878
+ } catch (error) {
879
+ throw new Error(`Failed to retrieve Unity process list: ${error instanceof Error ? error.message : String(error)}`);
880
+ }
881
+ const rows = parsePowershellJson(stdout);
882
+ return rows.map((row) => {
883
+ const pidValue = row.ProcessId;
884
+ if (!Number.isFinite(pidValue)) {
885
+ return void 0;
886
+ }
887
+ const commandLine = row.CommandLine ?? "";
888
+ const projectArgument = extractProjectPath2(commandLine);
889
+ if (!projectArgument) {
890
+ return void 0;
891
+ }
892
+ return {
893
+ pid: pidValue,
894
+ projectPath: normalizePath2(projectArgument)
895
+ };
896
+ }).filter((p) => Boolean(p));
897
+ }
898
+ };
899
+ var WinUnityProcessTerminator = class {
900
+ async terminate(unityProcess) {
901
+ try {
902
+ await execFileAsync3("powershell.exe", [
903
+ "-NoProfile",
904
+ "-NonInteractive",
905
+ "-ExecutionPolicy",
906
+ "Bypass",
907
+ "-Command",
908
+ `Stop-Process -Id ${unityProcess.pid}`
909
+ ]);
910
+ } catch (error) {
911
+ if (!ensureProcessAlive2(unityProcess.pid)) {
912
+ return { terminated: true, stage: "sigterm" };
913
+ }
914
+ }
915
+ const deadline = Date.now() + TERMINATE_TIMEOUT_MILLIS2;
916
+ while (Date.now() < deadline) {
917
+ await delay2(TERMINATE_POLL_INTERVAL_MILLIS2);
918
+ const alive = ensureProcessAlive2(unityProcess.pid);
919
+ if (!alive) {
920
+ return { terminated: true, stage: "sigterm" };
921
+ }
922
+ }
923
+ try {
924
+ await execFileAsync3("powershell.exe", [
925
+ "-NoProfile",
926
+ "-NonInteractive",
927
+ "-ExecutionPolicy",
928
+ "Bypass",
929
+ "-Command",
930
+ `Stop-Process -Id ${unityProcess.pid} -Force`
931
+ ]);
932
+ } catch (error) {
933
+ if (!ensureProcessAlive2(unityProcess.pid)) {
934
+ return { terminated: true, stage: "sigkill" };
935
+ }
936
+ return { terminated: false };
937
+ }
938
+ await delay2(TERMINATE_POLL_INTERVAL_MILLIS2);
939
+ const aliveAfterKill = ensureProcessAlive2(unityProcess.pid);
940
+ return aliveAfterKill ? { terminated: false } : { terminated: true, stage: "sigkill" };
941
+ }
942
+ };
943
+
605
944
  // src/infrastructure/unityTemp.ts
606
945
  import { rm as rm2 } from "fs/promises";
607
- import { join as join4 } from "path";
946
+ import { join as join6 } from "path";
608
947
  var TEMP_DIRECTORY_NAME = "Temp";
609
948
  var UnityTempDirectoryCleaner = class {
610
949
  async clean(projectPath) {
611
- const tempDirectoryPath = join4(projectPath, TEMP_DIRECTORY_NAME);
950
+ const tempDirectoryPath = join6(projectPath, TEMP_DIRECTORY_NAME);
612
951
  try {
613
952
  await rm2(tempDirectoryPath, {
614
953
  recursive: true,
@@ -620,6 +959,7 @@ var UnityTempDirectoryCleaner = class {
620
959
  };
621
960
 
622
961
  // src/presentation/App.tsx
962
+ import { basename as basename4 } from "path";
623
963
  import clipboard from "clipboardy";
624
964
  import { Box as Box6, Text as Text4, useApp, useInput, useStdout as useStdout2 } from "ink";
625
965
  import { useCallback, useEffect as useEffect4, useMemo as useMemo2, useState as useState4 } from "react";
@@ -663,6 +1003,7 @@ var LayoutManager = ({
663
1003
  };
664
1004
 
665
1005
  // src/presentation/components/ProjectList.tsx
1006
+ import { basename as basename3 } from "path";
666
1007
  import { Box as Box3 } from "ink";
667
1008
  import { useMemo } from "react";
668
1009
 
@@ -685,6 +1026,13 @@ var shortenHomePath = (targetPath) => {
685
1026
  };
686
1027
  var buildCdCommand = (targetPath) => {
687
1028
  if (process.platform === "win32") {
1029
+ const isGitBash = Boolean(process.env.MSYSTEM) || /bash/i.test(process.env.SHELL ?? "");
1030
+ if (isGitBash) {
1031
+ const windowsPath = targetPath;
1032
+ const msysPath = windowsPath.replace(/^([A-Za-z]):[\\/]/, (_, drive) => `/${drive.toLowerCase()}/`).replace(/\\/g, "/");
1033
+ const escapedForPosix2 = msysPath.replace(/'/g, "'\\''");
1034
+ return `cd '${escapedForPosix2}'`;
1035
+ }
688
1036
  const escapedForWindows = targetPath.replace(/"/g, '""');
689
1037
  return `cd "${escapedForWindows}"`;
690
1038
  }
@@ -708,7 +1056,8 @@ var ProjectRow = ({
708
1056
  pathLine,
709
1057
  showBranch,
710
1058
  showPath,
711
- scrollbar
1059
+ scrollbar,
1060
+ showSpacer
712
1061
  }) => {
713
1062
  const { stdout } = useStdout();
714
1063
  const computedCenterWidth = typeof stdout?.columns === "number" ? Math.max(0, stdout.columns - 6) : void 0;
@@ -731,13 +1080,13 @@ var ProjectRow = ({
731
1080
  ] }),
732
1081
  showBranch ? /* @__PURE__ */ jsx2(Text, { color: "#e3839c", wrap: "truncate", children: branchLine }) : null,
733
1082
  showPath ? /* @__PURE__ */ jsx2(Text, { color: "#719bd8", wrap: "truncate", children: pathLine }) : null,
734
- /* @__PURE__ */ jsx2(Text, { children: " " })
1083
+ showSpacer ? /* @__PURE__ */ jsx2(Text, { children: " " }) : null
735
1084
  ] }),
736
1085
  /* @__PURE__ */ jsxs2(Box2, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
737
1086
  /* @__PURE__ */ jsx2(Text, { children: scrollbar.title }),
738
1087
  showBranch ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.branch }) : null,
739
1088
  showPath ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.path }) : null,
740
- /* @__PURE__ */ jsx2(Text, { children: scrollbar.spacer })
1089
+ showSpacer ? /* @__PURE__ */ jsx2(Text, { children: scrollbar.spacer }) : null
741
1090
  ] })
742
1091
  ] });
743
1092
  };
@@ -749,17 +1098,14 @@ var LOCK_COLOR = "yellow";
749
1098
  var STATUS_LABELS = {
750
1099
  idle: "",
751
1100
  running: "[running]",
752
- crashed: "[crash]"
1101
+ crashed: ""
753
1102
  };
754
1103
  var extractRootFolder = (repository) => {
755
1104
  if (!repository?.root) {
756
1105
  return void 0;
757
1106
  }
758
- const segments = repository.root.split("/").filter((segment) => segment.length > 0);
759
- if (segments.length === 0) {
760
- return void 0;
761
- }
762
- return segments[segments.length - 1];
1107
+ const base = basename3(repository.root);
1108
+ return base || void 0;
763
1109
  };
764
1110
  var formatProjectName = (projectTitle, repository, useGitRootName) => {
765
1111
  if (!useGitRootName) {
@@ -851,7 +1197,7 @@ var ProjectList = ({
851
1197
  return [];
852
1198
  }
853
1199
  if (totalLines <= visibleLines) {
854
- return Array.from({ length: visibleLines }, () => "\u2588");
1200
+ return Array.from({ length: visibleLines }, () => " ");
855
1201
  }
856
1202
  const trackLength = visibleLines;
857
1203
  const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
@@ -896,7 +1242,7 @@ var ProjectList = ({
896
1242
  const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
897
1243
  const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
898
1244
  const statusLabel = STATUS_LABELS[displayStatus];
899
- const statusColor = displayStatus === "running" ? LOCK_COLOR : displayStatus === "crashed" ? "red" : void 0;
1245
+ const statusColor = displayStatus === "running" ? LOCK_COLOR : void 0;
900
1246
  return /* @__PURE__ */ jsx3(
901
1247
  ProjectRow,
902
1248
  {
@@ -917,7 +1263,8 @@ var ProjectList = ({
917
1263
  branch: branchScrollbar,
918
1264
  path: pathScrollbar,
919
1265
  spacer: spacerScrollbar
920
- }
1266
+ },
1267
+ showSpacer: offset < visibleProjects.length - 1
921
1268
  },
922
1269
  project.id
923
1270
  );
@@ -1015,7 +1362,7 @@ var VisibilityPanel = ({ visibility, focusedIndex, width }) => {
1015
1362
  import { useEffect, useState } from "react";
1016
1363
 
1017
1364
  // src/infrastructure/config.ts
1018
- import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1365
+ import { mkdir, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1019
1366
  var defaultSortPreferences = {
1020
1367
  favoritesFirst: true,
1021
1368
  primary: "updated",
@@ -1030,6 +1377,10 @@ var defaultAppConfig = {
1030
1377
  visibility: defaultVisibilityPreferences
1031
1378
  };
1032
1379
  var getConfigDir = () => {
1380
+ if (process.platform === "win32") {
1381
+ const appdata = process.env.APPDATA ?? "";
1382
+ return appdata ? `${appdata}\\UnityHubCli` : "UnityHubCli";
1383
+ }
1033
1384
  const home = process.env.HOME ?? "";
1034
1385
  return `${home}/Library/Application Support/UnityHubCli`;
1035
1386
  };
@@ -1066,7 +1417,7 @@ var sanitizeAppConfig = (input) => {
1066
1417
  };
1067
1418
  var readAppConfig = async () => {
1068
1419
  try {
1069
- const content = await readFile3(getConfigPath(), "utf8");
1420
+ const content = await readFile4(getConfigPath(), "utf8");
1070
1421
  const json = JSON.parse(content);
1071
1422
  return sanitizeAppConfig(json);
1072
1423
  } catch {
@@ -1079,7 +1430,7 @@ var writeAppConfig = async (config) => {
1079
1430
  } catch {
1080
1431
  }
1081
1432
  const json = JSON.stringify(sanitizeAppConfig(config), void 0, 2);
1082
- await writeFile2(getConfigPath(), json, "utf8");
1433
+ await writeFile3(getConfigPath(), json, "utf8");
1083
1434
  };
1084
1435
  var readSortPreferences = async () => {
1085
1436
  const config = await readAppConfig();
@@ -1152,14 +1503,14 @@ var useVisibilityPreferences = () => {
1152
1503
 
1153
1504
  // src/presentation/hooks/useVisibleCount.ts
1154
1505
  import { useEffect as useEffect3, useState as useState3 } from "react";
1155
- var useVisibleCount = (stdout, linesPerProject, panelVisible, panelHeight, minimumVisibleProjectCount2) => {
1506
+ var useVisibleCount = (stdout, linesPerProject, panelVisible, panelHeight, minimumVisibleProjectCount2, statusBarRows) => {
1156
1507
  const compute = () => {
1157
1508
  if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
1158
1509
  return minimumVisibleProjectCount2;
1159
1510
  }
1160
1511
  const borderRows = 2;
1161
- const hintRows = 1;
1162
- const reservedRows = borderRows + hintRows + (panelVisible ? panelHeight : 0);
1512
+ const hintRows = Math.max(1, statusBarRows);
1513
+ const reservedRows = borderRows + hintRows + (panelVisible ? panelHeight + 1 : 0);
1163
1514
  const availableRows = Math.max(0, stdout.rows - reservedRows);
1164
1515
  const rowsPerProject = Math.max(linesPerProject, 1);
1165
1516
  const calculatedCount = Math.max(1, Math.floor(availableRows / rowsPerProject));
@@ -1173,7 +1524,7 @@ var useVisibleCount = (stdout, linesPerProject, panelVisible, panelHeight, minim
1173
1524
  return () => {
1174
1525
  stdout?.off("resize", updateVisible);
1175
1526
  };
1176
- }, [stdout, linesPerProject, panelVisible, panelHeight]);
1527
+ }, [stdout, linesPerProject, panelVisible, panelHeight, statusBarRows]);
1177
1528
  return visibleCount;
1178
1529
  };
1179
1530
 
@@ -1183,14 +1534,11 @@ var extractRootFolder2 = (repository) => {
1183
1534
  if (!repository?.root) {
1184
1535
  return void 0;
1185
1536
  }
1186
- const segments = repository.root.split("/").filter((segment) => segment.length > 0);
1187
- if (segments.length === 0) {
1188
- return void 0;
1189
- }
1190
- return segments[segments.length - 1];
1537
+ const base = basename4(repository.root);
1538
+ return base || void 0;
1191
1539
  };
1192
1540
  var minimumVisibleProjectCount = 4;
1193
- var defaultHintMessage = "Select: j/k \xB7 Open: o \xB7 Quit: q \xB7 Refresh: r \xB7 CopyPath: c \xB7 Sort: s \xB7 Visibility: v \xB7 Close: ctrl + c";
1541
+ var defaultHintMessage = "j/k Select \xB7 [o]pen [q]uit [r]efresh [c]opy [s]ort [v]isibility \xB7 ^C Exit";
1194
1542
  var getCopyTargetPath = (view) => {
1195
1543
  const root = view.repository?.root;
1196
1544
  return root && root.length > 0 ? root : view.project.path;
@@ -1215,7 +1563,6 @@ var App = ({
1215
1563
  const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
1216
1564
  const isAnyMenuOpen = isSortMenuOpen || isVisibilityMenuOpen;
1217
1565
  const panelHeight = isVisibilityMenuOpen ? visibilityPanelHeight : sortPanelHeight;
1218
- const visibleCount = useVisibleCount(stdout, linesPerProject, isAnyMenuOpen, panelHeight, minimumVisibleProjectCount);
1219
1566
  const [index, setIndex] = useState4(0);
1220
1567
  const [hint, setHint] = useState4(defaultHintMessage);
1221
1568
  const [windowStart, setWindowStart] = useState4(0);
@@ -1223,7 +1570,18 @@ var App = ({
1223
1570
  const [launchedProjects, setLaunchedProjects] = useState4(/* @__PURE__ */ new Set());
1224
1571
  const [isRefreshing, setIsRefreshing] = useState4(false);
1225
1572
  const [sortMenuIndex, setSortMenuIndex] = useState4(0);
1573
+ const [directionManuallyChanged, setDirectionManuallyChanged] = useState4(false);
1226
1574
  const { sortPreferences, setSortPreferences } = useSortPreferences();
1575
+ const columns = typeof stdout?.columns === "number" ? stdout.columns : void 0;
1576
+ const statusBarRows = isAnyMenuOpen ? 1 : Math.max(1, typeof columns === "number" && columns > 0 ? Math.ceil(hint.length / columns) : 1);
1577
+ const visibleCount = useVisibleCount(
1578
+ stdout,
1579
+ linesPerProject,
1580
+ isAnyMenuOpen,
1581
+ panelHeight,
1582
+ minimumVisibleProjectCount,
1583
+ statusBarRows
1584
+ );
1227
1585
  const clearScreen = useCallback(() => {
1228
1586
  stdout?.write("\x1B[2J\x1B[3J\x1B[H");
1229
1587
  }, [stdout]);
@@ -1558,10 +1916,15 @@ var App = ({
1558
1916
  }
1559
1917
  const toggleCurrent = () => {
1560
1918
  if (sortMenuIndex === 0) {
1561
- setSortPreferences((prev) => ({ ...prev, primary: prev.primary === "updated" ? "name" : "updated" }));
1919
+ setSortPreferences((prev) => {
1920
+ const nextPrimary = prev.primary === "updated" ? "name" : "updated";
1921
+ const nextDirection = nextPrimary === "name" && !directionManuallyChanged ? "asc" : prev.direction;
1922
+ return { ...prev, primary: nextPrimary, direction: nextDirection };
1923
+ });
1562
1924
  return;
1563
1925
  }
1564
1926
  if (sortMenuIndex === 1) {
1927
+ setDirectionManuallyChanged(true);
1565
1928
  setSortPreferences((prev) => ({ ...prev, direction: prev.direction === "asc" ? "desc" : "asc" }));
1566
1929
  return;
1567
1930
  }
@@ -1710,7 +2073,7 @@ var App = ({
1710
2073
  ) })
1711
2074
  ] })
1712
2075
  ] }),
1713
- statusBar: isAnyMenuOpen ? /* @__PURE__ */ jsx6(Text4, { wrap: "truncate", children: "Select: j/k, Toggle: Space, Back: Esc" }) : /* @__PURE__ */ jsx6(Text4, { wrap: "truncate", children: hint })
2076
+ statusBar: isAnyMenuOpen ? /* @__PURE__ */ jsx6(Text4, { wrap: "truncate", children: "Select: j/k, Toggle: Space, Back: Esc" }) : /* @__PURE__ */ jsx6(Text4, { wrap: "wrap", children: hint })
1714
2077
  }
1715
2078
  );
1716
2079
  };
@@ -1718,10 +2081,11 @@ var App = ({
1718
2081
  // src/index.tsx
1719
2082
  import { jsx as jsx7 } from "react/jsx-runtime";
1720
2083
  var bootstrap = async () => {
1721
- const unityHubReader = new UnityHubProjectsReader();
2084
+ const isWindows = process2.platform === "win32";
2085
+ const unityHubReader = isWindows ? new WinUnityHubProjectsReader() : new MacUnityHubProjectsReader();
1722
2086
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
1723
2087
  const lockStatusReader = new UnityLockStatusReader();
1724
- const unityProcessReader = new MacUnityProcessReader();
2088
+ const unityProcessReader = isWindows ? new WinUnityProcessReader() : new MacUnityProcessReader();
1725
2089
  const unityTempDirectoryCleaner = new UnityTempDirectoryCleaner();
1726
2090
  const listProjectsUseCase = new ListProjectsUseCase(
1727
2091
  unityHubReader,
@@ -1730,10 +2094,10 @@ var bootstrap = async () => {
1730
2094
  lockStatusReader,
1731
2095
  unityProcessReader
1732
2096
  );
1733
- const editorPathResolver = new MacEditorPathResolver();
2097
+ const editorPathResolver = isWindows ? new WinEditorPathResolver() : new MacEditorPathResolver();
1734
2098
  const processLauncher = new NodeProcessLauncher();
1735
2099
  const lockChecker = new UnityLockChecker(unityProcessReader, unityTempDirectoryCleaner);
1736
- const unityProcessTerminator = new MacUnityProcessTerminator();
2100
+ const unityProcessTerminator = isWindows ? new WinUnityProcessTerminator() : new MacUnityProcessTerminator();
1737
2101
  const launchProjectUseCase = new LaunchProjectUseCase(
1738
2102
  editorPathResolver,
1739
2103
  processLauncher,
@@ -1748,6 +2112,23 @@ var bootstrap = async () => {
1748
2112
  );
1749
2113
  const useGitRootName = !process2.argv.includes("--no-git-root-name");
1750
2114
  try {
2115
+ const rawModeSupported = Boolean(
2116
+ process2.stdin.isTTY && typeof process2.stdin.setRawMode === "function"
2117
+ );
2118
+ if (!rawModeSupported) {
2119
+ const message = [
2120
+ "\u3053\u306E\u7AEF\u672B\u3067\u306F\u5BFE\u8A71\u5165\u529B\uFF08Raw mode\uFF09\u304C\u4F7F\u3048\u307E\u305B\u3093\u3002",
2121
+ "PowerShell / cmd.exe \u3067\u5B9F\u884C\u3059\u308B\u304B\u3001ConPTY \u30D9\u30FC\u30B9\u306E\u30BF\u30FC\u30DF\u30CA\u30EB\uFF08Windows Terminal, VS Code/Cursor \u306E\u7D71\u5408\u30BF\u30FC\u30DF\u30CA\u30EB\uFF09\u3067 Git Bash \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2122
+ "MinTTY \u306E Git Bash \u3067\u306F\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044:",
2123
+ " - winpty cmd.exe /c npx unity-hub-cli",
2124
+ " - winpty powershell.exe -NoProfile -Command npx unity-hub-cli",
2125
+ "\uFF08\u30D3\u30EB\u30C9\u6E08\u307F\u306E\u5834\u5408\uFF09npm run build && winpty node dist/index.js",
2126
+ "\u8A73\u3057\u304F: https://github.com/vadimdemedes/ink/#israwmodesupported"
2127
+ ].join("\n");
2128
+ console.error(message);
2129
+ process2.exitCode = 1;
2130
+ return;
2131
+ }
1751
2132
  const projects = await listProjectsUseCase.execute();
1752
2133
  const { waitUntilExit } = render(
1753
2134
  /* @__PURE__ */ jsx7(
@@ -1769,4 +2150,8 @@ var bootstrap = async () => {
1769
2150
  process2.exitCode = 1;
1770
2151
  }
1771
2152
  };
1772
- await bootstrap();
2153
+ void bootstrap().catch((error) => {
2154
+ const message = error instanceof Error ? error.message : String(error);
2155
+ console.error(message);
2156
+ process2.exitCode = 1;
2157
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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": {