unity-hub-cli 0.17.0 → 0.19.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 +40 -1
  2. package/dist/index.js +355 -34
  3. package/package.json +31 -18
package/README.md CHANGED
@@ -58,14 +58,53 @@ The display includes Git branch (if present), Unity version, project path, and l
58
58
  ## CLI Options
59
59
 
60
60
  - `--no-git-root-name`: Display Unity project titles instead of Git repository root folder names.
61
+ - `--shell-init`: Install shell function for automatic `cd` integration (with confirmation prompt).
62
+ - `--shell-init --dry-run`: Preview the shell function without installing.
63
+
64
+ ## Shell Integration
65
+
66
+ You can add a shell function to automatically `cd` to the project directory after opening Unity.
67
+
68
+ ### Setup
69
+
70
+ 1. Install globally:
71
+ ```bash
72
+ npm install -g unity-hub-cli
73
+ ```
74
+
75
+ 2. Run the shell init command (auto-detects your shell):
76
+ ```bash
77
+ unity-hub-cli --shell-init
78
+ ```
79
+
80
+ This automatically adds the `unity-hub` function to your shell config file (`.zshrc`, `.bashrc`, `config.fish`, or PowerShell profile).
81
+
82
+ 3. Reload your shell:
83
+ ```bash
84
+ source ~/.zshrc # or restart your terminal
85
+ ```
86
+
87
+ ### Usage
88
+
89
+ Now you can use `unity-hub` to:
90
+ 1. Browse and select Unity projects
91
+ 2. Press `o` to launch Unity
92
+ 3. Your terminal automatically `cd`s to the project directory
93
+
94
+ ### Notes
95
+
96
+ - Running `--shell-init` multiple times is safe - it updates the existing function using marker comments
97
+ - The function uses absolute paths detected from your environment
98
+ - **Windows**: Shell integration supports PowerShell only. CMD is not supported because it lacks shell functions required for automatic `cd` after launching Unity
61
99
 
62
100
  ## Security
63
101
 
64
102
  This project implements supply chain attack prevention measures:
65
103
 
66
104
  - **ignore-scripts**: Disables automatic script execution during `npm install`
105
+ - **@lavamoat/allow-scripts**: Explicitly controls which packages can run install scripts
67
106
  - **Dependabot**: Automated weekly security updates
68
- - **Security audit CI**: Runs `npm audit` and `lockfile-lint` on every PR
107
+ - **Security audit CI**: Runs `npm audit`, `lockfile-lint`, and OSV-Scanner on every PR
69
108
  - **Pinned versions**: All dependencies use exact versions (no `^` or `~`)
70
109
 
71
110
  ## License
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.tsx
4
+ import { execSync } from "child_process";
5
+ import { existsSync as existsSync3, mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { homedir } from "os";
7
+ import { dirname as dirname2, join as join9 } from "path";
4
8
  import process3 from "process";
9
+ import { createInterface as createInterface2 } from "readline";
10
+ import { fileURLToPath } from "url";
11
+ import chalk from "chalk";
5
12
  import { render } from "ink";
6
13
 
7
14
  // src/application/usecases.ts
@@ -778,6 +785,38 @@ var MacUnityHubProjectsReader = class {
778
785
  };
779
786
  await writeFile(HUB_PROJECTS_PATH, JSON.stringify(json, void 0, 2), "utf8");
780
787
  }
788
+ async toggleFavorite(projectPath) {
789
+ let content;
790
+ try {
791
+ content = await readFile2(HUB_PROJECTS_PATH, "utf8");
792
+ } catch {
793
+ throw new Error(`Unity Hub project list not found (${HUB_PROJECTS_PATH}).`);
794
+ }
795
+ let json;
796
+ try {
797
+ json = JSON.parse(content);
798
+ } catch {
799
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
800
+ }
801
+ if (!json.data) {
802
+ return false;
803
+ }
804
+ const projectKey = Object.keys(json.data).find((key) => json.data?.[key]?.path === projectPath);
805
+ if (!projectKey) {
806
+ return false;
807
+ }
808
+ const original = json.data[projectKey];
809
+ if (!original) {
810
+ return false;
811
+ }
812
+ const newFavorite = original.isFavorite !== true;
813
+ json.data[projectKey] = {
814
+ ...original,
815
+ isFavorite: newFavorite
816
+ };
817
+ await writeFile(HUB_PROJECTS_PATH, JSON.stringify(json, void 0, 2), "utf8");
818
+ return newFavorite;
819
+ }
781
820
  async readCliArgs(projectPath) {
782
821
  const infoPath = `${process.env.HOME ?? ""}/Library/Application Support/UnityHub/projectsInfo.json`;
783
822
  let content;
@@ -909,6 +948,38 @@ var WinUnityHubProjectsReader = class {
909
948
  };
910
949
  await writeFile2(HUB_PROJECTS_PATH2, JSON.stringify(json, void 0, 2), "utf8");
911
950
  }
951
+ async toggleFavorite(projectPath) {
952
+ let content;
953
+ try {
954
+ content = await readFile3(HUB_PROJECTS_PATH2, "utf8");
955
+ } catch {
956
+ throw new Error(`Unity Hub project list not found (${HUB_PROJECTS_PATH2}).`);
957
+ }
958
+ let json;
959
+ try {
960
+ json = JSON.parse(content);
961
+ } catch {
962
+ throw new Error("Unable to read the Unity Hub project list (permissions/format error).");
963
+ }
964
+ if (!json.data) {
965
+ return false;
966
+ }
967
+ const projectKey = Object.keys(json.data).find((key) => json.data?.[key]?.path === projectPath);
968
+ if (!projectKey) {
969
+ return false;
970
+ }
971
+ const original = json.data[projectKey];
972
+ if (!original) {
973
+ return false;
974
+ }
975
+ const newFavorite = original.isFavorite !== true;
976
+ json.data[projectKey] = {
977
+ ...original,
978
+ isFavorite: newFavorite
979
+ };
980
+ await writeFile2(HUB_PROJECTS_PATH2, JSON.stringify(json, void 0, 2), "utf8");
981
+ return newFavorite;
982
+ }
912
983
  async readCliArgs(projectPath) {
913
984
  const infoPath = join6(HUB_DIR, "projectsInfo.json");
914
985
  let content;
@@ -1332,7 +1403,7 @@ var WinUnityProcessTerminator = class {
1332
1403
  ],
1333
1404
  { env: getMsysDisabledEnv() }
1334
1405
  );
1335
- } catch (error) {
1406
+ } catch {
1336
1407
  if (!ensureProcessAlive2(unityProcess.pid)) {
1337
1408
  return { terminated: true, stage: "sigterm" };
1338
1409
  }
@@ -1358,7 +1429,7 @@ var WinUnityProcessTerminator = class {
1358
1429
  ],
1359
1430
  { env: getMsysDisabledEnv() }
1360
1431
  );
1361
- } catch (error) {
1432
+ } catch {
1362
1433
  if (!ensureProcessAlive2(unityProcess.pid)) {
1363
1434
  return { terminated: true, stage: "sigkill" };
1364
1435
  }
@@ -1667,7 +1738,8 @@ var ProjectList = ({
1667
1738
  const rowIndex = startIndex + offset;
1668
1739
  const isSelected = rowIndex === selectedIndex;
1669
1740
  const selectionBar = isSelected ? "\u2503" : " ";
1670
- const projectName = formatProjectName(project.title, repository, useGitRootName);
1741
+ const baseName = formatProjectName(project.title, repository, useGitRootName);
1742
+ const projectName = project.favorite ? `\u2B51 ${baseName}` : baseName;
1671
1743
  const versionLabel = `(${project.version.value})`;
1672
1744
  const updatedText = formatUpdatedText(project.lastModified);
1673
1745
  const pathLine = shortenHomePath(project.path);
@@ -1987,7 +2059,7 @@ var extractRootFolder2 = (repository) => {
1987
2059
  return base || void 0;
1988
2060
  };
1989
2061
  var minimumVisibleProjectCount = 4;
1990
- var defaultHintMessage = `j/k Select \xB7 [o]pen [O]+Editor [i]de [q]uit [r]efresh [c]opy [s]ort [v]isibility \xB7 ^C Exit`;
2062
+ var defaultHintMessage = `j/k Select \xB7 [o]pen [O]+Editor [i]de [q]uit [r]efresh [c]opy [f]av [s]ort [v]isibility \xB7 ^C Exit`;
1991
2063
  var getCopyTargetPath = (view) => {
1992
2064
  const root = view.repository?.root;
1993
2065
  return root && root.length > 0 ? root : view.project.path;
@@ -1999,7 +2071,10 @@ var App = ({
1999
2071
  onLaunchEditorOnly,
2000
2072
  onTerminate,
2001
2073
  onRefresh,
2002
- useGitRootName = true
2074
+ onToggleFavorite,
2075
+ useGitRootName = true,
2076
+ outputPathOnExit = false,
2077
+ onSetExitPath
2003
2078
  }) => {
2004
2079
  const { exit } = useApp();
2005
2080
  const { stdout } = useStdout2();
@@ -2196,20 +2271,14 @@ var App = ({
2196
2271
  return;
2197
2272
  }
2198
2273
  const { project } = projectView;
2199
- try {
2200
- const cdTarget = getCopyTargetPath(projectView);
2201
- const command = buildCdCommand(cdTarget);
2202
- clipboard.writeSync(command);
2203
- } catch (error) {
2204
- const message = error instanceof Error ? error.message : String(error);
2205
- setHint(`Failed to copy: ${message}`);
2206
- setTimeout(() => {
2207
- setHint(defaultHintMessage);
2208
- }, 3e3);
2209
- return;
2210
- }
2274
+ const cdTarget = getCopyTargetPath(projectView);
2211
2275
  try {
2212
2276
  await onLaunch(project);
2277
+ if (outputPathOnExit) {
2278
+ onSetExitPath?.(cdTarget);
2279
+ exit();
2280
+ return;
2281
+ }
2213
2282
  setLaunchedProjects((previous) => {
2214
2283
  const next = new Set(previous);
2215
2284
  next.add(project.id);
@@ -2229,6 +2298,11 @@ var App = ({
2229
2298
  }, 3e3);
2230
2299
  } catch (error) {
2231
2300
  if (error instanceof LaunchCancelledError) {
2301
+ if (outputPathOnExit) {
2302
+ onSetExitPath?.(cdTarget);
2303
+ exit();
2304
+ return;
2305
+ }
2232
2306
  setHint("Launch cancelled");
2233
2307
  setTimeout(() => {
2234
2308
  setHint(defaultHintMessage);
@@ -2241,7 +2315,7 @@ var App = ({
2241
2315
  setHint(defaultHintMessage);
2242
2316
  }, 3e3);
2243
2317
  }
2244
- }, [index, onLaunch, sortedProjects]);
2318
+ }, [exit, index, onLaunch, onSetExitPath, outputPathOnExit, sortedProjects]);
2245
2319
  const launchSelectedWithEditor = useCallback(async () => {
2246
2320
  if (!onLaunchWithEditor) {
2247
2321
  setHint("Launch with editor not available");
@@ -2259,20 +2333,14 @@ var App = ({
2259
2333
  return;
2260
2334
  }
2261
2335
  const { project } = projectView;
2262
- try {
2263
- const cdTarget = getCopyTargetPath(projectView);
2264
- const command = buildCdCommand(cdTarget);
2265
- clipboard.writeSync(command);
2266
- } catch (error) {
2267
- const message = error instanceof Error ? error.message : String(error);
2268
- setHint(`Failed to copy: ${message}`);
2269
- setTimeout(() => {
2270
- setHint(defaultHintMessage);
2271
- }, 3e3);
2272
- return;
2273
- }
2336
+ const cdTarget = getCopyTargetPath(projectView);
2274
2337
  try {
2275
2338
  const result = await onLaunchWithEditor(project);
2339
+ if (outputPathOnExit) {
2340
+ onSetExitPath?.(cdTarget);
2341
+ exit();
2342
+ return;
2343
+ }
2276
2344
  setLaunchedProjects((previous) => {
2277
2345
  const next = new Set(previous);
2278
2346
  next.add(project.id);
@@ -2292,6 +2360,11 @@ var App = ({
2292
2360
  }, 3e3);
2293
2361
  } catch (error) {
2294
2362
  if (error instanceof LaunchCancelledError) {
2363
+ if (outputPathOnExit) {
2364
+ onSetExitPath?.(cdTarget);
2365
+ exit();
2366
+ return;
2367
+ }
2295
2368
  setHint("Launch cancelled");
2296
2369
  setTimeout(() => {
2297
2370
  setHint(defaultHintMessage);
@@ -2304,7 +2377,7 @@ var App = ({
2304
2377
  setHint(defaultHintMessage);
2305
2378
  }, 3e3);
2306
2379
  }
2307
- }, [index, onLaunchWithEditor, sortedProjects]);
2380
+ }, [exit, index, onLaunchWithEditor, onSetExitPath, outputPathOnExit, sortedProjects]);
2308
2381
  const launchEditorOnly = useCallback(async () => {
2309
2382
  if (!onLaunchEditorOnly) {
2310
2383
  setHint("Launch editor only not available");
@@ -2437,6 +2510,43 @@ var App = ({
2437
2510
  setIsRefreshing(false);
2438
2511
  }
2439
2512
  }, [isRefreshing, onRefresh, sortedProjects]);
2513
+ const toggleFavoriteSelected = useCallback(async () => {
2514
+ if (!onToggleFavorite) {
2515
+ setHint("Toggle favorite not available");
2516
+ setTimeout(() => {
2517
+ setHint(defaultHintMessage);
2518
+ }, 2e3);
2519
+ return;
2520
+ }
2521
+ const projectView = sortedProjects[index];
2522
+ if (!projectView) {
2523
+ setHint("No project to toggle favorite");
2524
+ setTimeout(() => {
2525
+ setHint(defaultHintMessage);
2526
+ }, 2e3);
2527
+ return;
2528
+ }
2529
+ const { project } = projectView;
2530
+ try {
2531
+ const newFavorite = await onToggleFavorite(project);
2532
+ setProjectViews(
2533
+ (prev) => prev.map(
2534
+ (pv) => pv.project.id === project.id ? { ...pv, project: { ...pv.project, favorite: newFavorite } } : pv
2535
+ )
2536
+ );
2537
+ const status = newFavorite ? "added to" : "removed from";
2538
+ setHint(`${project.title} ${status} favorites`);
2539
+ setTimeout(() => {
2540
+ setHint(defaultHintMessage);
2541
+ }, 2e3);
2542
+ } catch (error) {
2543
+ const message = error instanceof Error ? error.message : String(error);
2544
+ setHint(`Failed to toggle favorite: ${message}`);
2545
+ setTimeout(() => {
2546
+ setHint(defaultHintMessage);
2547
+ }, 3e3);
2548
+ }
2549
+ }, [index, onToggleFavorite, sortedProjects]);
2440
2550
  useInput((input, key) => {
2441
2551
  if (isSortMenuOpen) {
2442
2552
  if (key.escape || input === "\x1B") {
@@ -2557,6 +2667,10 @@ var App = ({
2557
2667
  }
2558
2668
  if (input === "c") {
2559
2669
  copyProjectPath();
2670
+ return;
2671
+ }
2672
+ if (input === "f" || input === "F") {
2673
+ void toggleFavoriteSelected();
2560
2674
  }
2561
2675
  });
2562
2676
  const { startIndex, visibleProjects } = useMemo2(() => {
@@ -2634,7 +2748,196 @@ var App = ({
2634
2748
 
2635
2749
  // src/index.tsx
2636
2750
  import { jsx as jsx7 } from "react/jsx-runtime";
2751
+ var SHELL_INIT_MARKER_START = "# >>> unity-hub-cli >>>";
2752
+ var SHELL_INIT_MARKER_END = "# <<< unity-hub-cli <<<";
2753
+ var getVersion = () => {
2754
+ const currentDir = dirname2(fileURLToPath(import.meta.url));
2755
+ const packageJsonPath = join9(currentDir, "..", "package.json");
2756
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2757
+ return packageJson.version;
2758
+ };
2759
+ var getShellConfigPath = () => {
2760
+ const shell = process3.env["SHELL"] ?? "";
2761
+ const home = homedir();
2762
+ if (shell.includes("zsh")) {
2763
+ return join9(home, ".zshrc");
2764
+ }
2765
+ if (shell.includes("bash")) {
2766
+ const bashrcPath = join9(home, ".bashrc");
2767
+ const profilePath = join9(home, ".bash_profile");
2768
+ return existsSync3(bashrcPath) ? bashrcPath : profilePath;
2769
+ }
2770
+ if (shell.includes("fish")) {
2771
+ return join9(home, ".config", "fish", "config.fish");
2772
+ }
2773
+ if (process3.platform === "win32") {
2774
+ return join9(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
2775
+ }
2776
+ return void 0;
2777
+ };
2778
+ var getShellInitScriptWithMarkers = () => {
2779
+ const script = getShellInitScript();
2780
+ return `${SHELL_INIT_MARKER_START}
2781
+ ${script}
2782
+ ${SHELL_INIT_MARKER_END}`;
2783
+ };
2784
+ var askConfirmation = (question) => {
2785
+ const rl = createInterface2({
2786
+ input: process3.stdin,
2787
+ output: process3.stdout
2788
+ });
2789
+ return new Promise((resolve4) => {
2790
+ rl.question(question, (answer) => {
2791
+ rl.close();
2792
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
2793
+ });
2794
+ });
2795
+ };
2796
+ var previewShellInit = () => {
2797
+ const configPath = getShellConfigPath();
2798
+ console.log("=== Shell Integration Preview ===\n");
2799
+ console.log(`Target file: ${configPath ?? "Unknown (unsupported shell)"}
2800
+ `);
2801
+ console.log("Content to be added:\n");
2802
+ console.log(getShellInitScriptWithMarkers());
2803
+ };
2804
+ var installShellInit = () => {
2805
+ const configPath = getShellConfigPath();
2806
+ if (!configPath) {
2807
+ return { success: false, message: "Unsupported shell. Please copy the function from the README into your shell config manually." };
2808
+ }
2809
+ const scriptWithMarkers = getShellInitScriptWithMarkers();
2810
+ const markerPattern = new RegExp(
2811
+ `${SHELL_INIT_MARKER_START}[\\s\\S]*?${SHELL_INIT_MARKER_END}`,
2812
+ "g"
2813
+ );
2814
+ let content = "";
2815
+ if (existsSync3(configPath)) {
2816
+ content = readFileSync(configPath, "utf-8");
2817
+ }
2818
+ const existingMatch = content.match(markerPattern);
2819
+ if (existingMatch && existingMatch[0] === scriptWithMarkers) {
2820
+ return { success: true, message: `Shell integration is already up to date in ${configPath}` };
2821
+ }
2822
+ let newContent;
2823
+ let action;
2824
+ if (existingMatch) {
2825
+ newContent = content.replace(markerPattern, scriptWithMarkers);
2826
+ action = "updated";
2827
+ } else {
2828
+ newContent = content.trimEnd() + "\n\n" + scriptWithMarkers + "\n";
2829
+ action = "installed";
2830
+ }
2831
+ mkdirSync(dirname2(configPath), { recursive: true });
2832
+ writeFileSync(configPath, newContent, "utf-8");
2833
+ return { success: true, message: `Shell integration ${action} in ${configPath}` };
2834
+ };
2835
+ var getNodePath = () => {
2836
+ const isWindows = process3.platform === "win32";
2837
+ const command = isWindows ? "where node" : "which node";
2838
+ try {
2839
+ const result = execSync(command, { encoding: "utf-8" }).trim().split("\n")[0];
2840
+ if (result && existsSync3(result)) {
2841
+ return result;
2842
+ }
2843
+ } catch {
2844
+ }
2845
+ return "node";
2846
+ };
2847
+ var getUnityHubCliPath = () => {
2848
+ const isWindows = process3.platform === "win32";
2849
+ try {
2850
+ const prefix = execSync("npm config get prefix", { encoding: "utf-8" }).trim();
2851
+ const binDir = isWindows ? prefix : `${prefix}/bin`;
2852
+ const cliPath = isWindows ? `${binDir}/unity-hub-cli.cmd` : `${binDir}/unity-hub-cli`;
2853
+ if (existsSync3(cliPath)) {
2854
+ return cliPath;
2855
+ }
2856
+ } catch {
2857
+ }
2858
+ return "unity-hub-cli";
2859
+ };
2860
+ var getShellInitScript = () => {
2861
+ const shell = process3.env["SHELL"] ?? "";
2862
+ const isWindows = process3.platform === "win32";
2863
+ const nodePath = getNodePath();
2864
+ const cliPath = getUnityHubCliPath();
2865
+ if (shell.includes("fish")) {
2866
+ return `function unity-hub
2867
+ set -l tmpfile (mktemp)
2868
+ ${nodePath} ${cliPath} --output-path-on-exit > $tmpfile
2869
+ set -l dir (cat $tmpfile)
2870
+ rm -f $tmpfile
2871
+ if test -n "$dir"
2872
+ cd $dir
2873
+ end
2874
+ end`;
2875
+ }
2876
+ if (shell.includes("bash") || shell.includes("zsh")) {
2877
+ return `unity-hub() {
2878
+ local tmpfile=$(mktemp)
2879
+ ${nodePath} ${cliPath} --output-path-on-exit >| "$tmpfile"
2880
+ local dir=$(cat "$tmpfile")
2881
+ rm -f "$tmpfile"
2882
+ if [ -n "$dir" ]; then
2883
+ cd "$dir"
2884
+ fi
2885
+ }`;
2886
+ }
2887
+ if (isWindows) {
2888
+ return `function unity-hub {
2889
+ $tmpfile = [System.IO.Path]::GetTempFileName()
2890
+ & "${cliPath}" --output-path-on-exit > $tmpfile
2891
+ $dir = Get-Content $tmpfile
2892
+ Remove-Item $tmpfile
2893
+ if ($dir) {
2894
+ Set-Location $dir
2895
+ }
2896
+ }`;
2897
+ }
2898
+ return `unity-hub() {
2899
+ local tmpfile=$(mktemp)
2900
+ ${nodePath} ${cliPath} --output-path-on-exit >| "$tmpfile"
2901
+ local dir=$(cat "$tmpfile")
2902
+ rm -f "$tmpfile"
2903
+ if [ -n "$dir" ]; then
2904
+ cd "$dir"
2905
+ fi
2906
+ }`;
2907
+ };
2637
2908
  var bootstrap = async () => {
2909
+ const args = process3.argv.slice(2);
2910
+ if (args.includes("-v") || args.includes("--version")) {
2911
+ console.log(getVersion());
2912
+ return;
2913
+ }
2914
+ if (args.includes("--shell-init")) {
2915
+ const isDryRun = args.includes("--dry-run");
2916
+ if (isDryRun) {
2917
+ previewShellInit();
2918
+ return;
2919
+ }
2920
+ const configPath = getShellConfigPath();
2921
+ if (!configPath) {
2922
+ console.log("Unsupported shell. Please copy the function from the README into your shell config manually.");
2923
+ process3.exitCode = 1;
2924
+ return;
2925
+ }
2926
+ console.log(`This will install the unity-hub function to: ${configPath}
2927
+ `);
2928
+ previewShellInit();
2929
+ console.log("");
2930
+ const confirmed = await askConfirmation("Proceed with installation? (y/n): ");
2931
+ if (!confirmed) {
2932
+ console.log("Installation cancelled.");
2933
+ return;
2934
+ }
2935
+ const result = installShellInit();
2936
+ console.log(result.message);
2937
+ process3.exitCode = result.success ? 0 : 1;
2938
+ return;
2939
+ }
2940
+ const outputPathOnExit = args.includes("--output-path-on-exit");
2638
2941
  const isWindows = process3.platform === "win32";
2639
2942
  const unityHubReader = isWindows ? new WinUnityHubProjectsReader() : new MacUnityHubProjectsReader();
2640
2943
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
@@ -2694,8 +2997,13 @@ var bootstrap = async () => {
2694
2997
  process3.exitCode = 1;
2695
2998
  return;
2696
2999
  }
3000
+ if (outputPathOnExit && process3.stderr.isTTY) {
3001
+ chalk.level = 3;
3002
+ }
2697
3003
  const theme = await detectTerminalTheme();
3004
+ let lastOpenedPath;
2698
3005
  const projects = await listProjectsUseCase.execute();
3006
+ const renderOptions = outputPathOnExit ? { stdout: process3.stderr, stdin: process3.stdin } : void 0;
2699
3007
  const { waitUntilExit } = render(
2700
3008
  /* @__PURE__ */ jsx7(ThemeProvider, { theme, children: /* @__PURE__ */ jsx7(
2701
3009
  App,
@@ -2706,12 +3014,25 @@ var bootstrap = async () => {
2706
3014
  onLaunchEditorOnly: (project) => launchEditorOnlyUseCase.execute(project),
2707
3015
  onTerminate: (project) => terminateProjectUseCase.execute(project),
2708
3016
  onRefresh: () => listProjectsUseCase.execute(),
2709
- useGitRootName
3017
+ onToggleFavorite: (project) => unityHubReader.toggleFavorite(project.path),
3018
+ useGitRootName,
3019
+ outputPathOnExit,
3020
+ onSetExitPath: (path) => {
3021
+ lastOpenedPath = path;
3022
+ }
2710
3023
  }
2711
- ) })
3024
+ ) }),
3025
+ renderOptions
2712
3026
  );
2713
3027
  await waitUntilExit();
2714
- process3.stdout.write("\x1B[2J\x1B[3J\x1B[H");
3028
+ if (outputPathOnExit) {
3029
+ process3.stderr.write("\x1B[2J\x1B[3J\x1B[H");
3030
+ if (lastOpenedPath) {
3031
+ process3.stdout.write(lastOpenedPath);
3032
+ }
3033
+ } else {
3034
+ process3.stdout.write("\x1B[2J\x1B[3J\x1B[H");
3035
+ }
2715
3036
  } catch (error) {
2716
3037
  const message = error instanceof Error ? error.message : String(error);
2717
3038
  console.error(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-hub-cli",
3
- "version": "0.17.0",
3
+ "version": "0.19.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": {
@@ -8,10 +8,11 @@
8
8
  "dev": "tsx src/index.ts",
9
9
  "build": "tsup",
10
10
  "start": "node dist/index.js",
11
- "lint": "eslint . --ext .ts,.tsx",
12
- "lint:fix": "eslint . --ext .ts,.tsx --fix",
11
+ "lint": "eslint .",
12
+ "lint:fix": "eslint . --fix",
13
13
  "format": "prettier --write .",
14
- "typecheck": "tsc -noEmit"
14
+ "typecheck": "tsc -noEmit",
15
+ "allow-scripts": "allow-scripts"
15
16
  },
16
17
  "repository": {
17
18
  "type": "git",
@@ -36,26 +37,38 @@
36
37
  "dist"
37
38
  ],
38
39
  "dependencies": {
39
- "clipboardy": "4.0.0",
40
- "ink": "4.4.1",
41
- "react": "18.3.1"
40
+ "chalk": "5.6.2",
41
+ "clipboardy": "5.0.1",
42
+ "ink": "6.5.1",
43
+ "react": "19.2.1"
42
44
  },
43
45
  "engines": {
44
46
  "node": ">=18"
45
47
  },
48
+ "lavamoat": {
49
+ "allowScripts": {
50
+ "eslint-import-resolver-typescript>unrs-resolver": false,
51
+ "tsup>bundle-require>esbuild": false,
52
+ "tsup>esbuild": false,
53
+ "tsx>esbuild": false
54
+ }
55
+ },
46
56
  "devDependencies": {
47
- "@types/node": "20.19.20",
48
- "@types/react": "18.3.26",
49
- "@typescript-eslint/eslint-plugin": "7.18.0",
50
- "@typescript-eslint/parser": "7.18.0",
51
- "eslint": "8.57.0",
52
- "eslint-config-prettier": "9.1.2",
53
- "eslint-import-resolver-typescript": "3.10.1",
57
+ "@eslint/js": "^9.39.1",
58
+ "@lavamoat/allow-scripts": "3.4.1",
59
+ "@types/node": "24.10.1",
60
+ "@types/react": "19.2.7",
61
+ "@typescript-eslint/eslint-plugin": "8.48.1",
62
+ "@typescript-eslint/parser": "8.48.1",
63
+ "eslint": "9.39.1",
64
+ "eslint-config-prettier": "10.1.8",
65
+ "eslint-import-resolver-typescript": "4.4.4",
54
66
  "eslint-plugin-import": "2.32.0",
55
- "prettier": "3.6.2",
56
- "tsup": "8.5.0",
57
- "tsx": "4.20.6",
67
+ "prettier": "3.7.4",
68
+ "tsup": "8.5.1",
69
+ "tsx": "4.21.0",
58
70
  "typescript": "5.9.3",
59
- "vitest": "4.0.14"
71
+ "typescript-eslint": "^8.48.1",
72
+ "vitest": "4.0.15"
60
73
  }
61
74
  }