unity-hub-cli 0.17.0 → 0.18.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 +234 -32
  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,13 @@
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 chalk from "chalk";
5
11
  import { render } from "ink";
6
12
 
7
13
  // src/application/usecases.ts
@@ -1332,7 +1338,7 @@ var WinUnityProcessTerminator = class {
1332
1338
  ],
1333
1339
  { env: getMsysDisabledEnv() }
1334
1340
  );
1335
- } catch (error) {
1341
+ } catch {
1336
1342
  if (!ensureProcessAlive2(unityProcess.pid)) {
1337
1343
  return { terminated: true, stage: "sigterm" };
1338
1344
  }
@@ -1358,7 +1364,7 @@ var WinUnityProcessTerminator = class {
1358
1364
  ],
1359
1365
  { env: getMsysDisabledEnv() }
1360
1366
  );
1361
- } catch (error) {
1367
+ } catch {
1362
1368
  if (!ensureProcessAlive2(unityProcess.pid)) {
1363
1369
  return { terminated: true, stage: "sigkill" };
1364
1370
  }
@@ -1999,7 +2005,9 @@ var App = ({
1999
2005
  onLaunchEditorOnly,
2000
2006
  onTerminate,
2001
2007
  onRefresh,
2002
- useGitRootName = true
2008
+ useGitRootName = true,
2009
+ outputPathOnExit = false,
2010
+ onSetExitPath
2003
2011
  }) => {
2004
2012
  const { exit } = useApp();
2005
2013
  const { stdout } = useStdout2();
@@ -2196,20 +2204,14 @@ var App = ({
2196
2204
  return;
2197
2205
  }
2198
2206
  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
- }
2207
+ const cdTarget = getCopyTargetPath(projectView);
2211
2208
  try {
2212
2209
  await onLaunch(project);
2210
+ if (outputPathOnExit) {
2211
+ onSetExitPath?.(cdTarget);
2212
+ exit();
2213
+ return;
2214
+ }
2213
2215
  setLaunchedProjects((previous) => {
2214
2216
  const next = new Set(previous);
2215
2217
  next.add(project.id);
@@ -2229,6 +2231,11 @@ var App = ({
2229
2231
  }, 3e3);
2230
2232
  } catch (error) {
2231
2233
  if (error instanceof LaunchCancelledError) {
2234
+ if (outputPathOnExit) {
2235
+ onSetExitPath?.(cdTarget);
2236
+ exit();
2237
+ return;
2238
+ }
2232
2239
  setHint("Launch cancelled");
2233
2240
  setTimeout(() => {
2234
2241
  setHint(defaultHintMessage);
@@ -2241,7 +2248,7 @@ var App = ({
2241
2248
  setHint(defaultHintMessage);
2242
2249
  }, 3e3);
2243
2250
  }
2244
- }, [index, onLaunch, sortedProjects]);
2251
+ }, [exit, index, onLaunch, onSetExitPath, outputPathOnExit, sortedProjects]);
2245
2252
  const launchSelectedWithEditor = useCallback(async () => {
2246
2253
  if (!onLaunchWithEditor) {
2247
2254
  setHint("Launch with editor not available");
@@ -2259,20 +2266,14 @@ var App = ({
2259
2266
  return;
2260
2267
  }
2261
2268
  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
- }
2269
+ const cdTarget = getCopyTargetPath(projectView);
2274
2270
  try {
2275
2271
  const result = await onLaunchWithEditor(project);
2272
+ if (outputPathOnExit) {
2273
+ onSetExitPath?.(cdTarget);
2274
+ exit();
2275
+ return;
2276
+ }
2276
2277
  setLaunchedProjects((previous) => {
2277
2278
  const next = new Set(previous);
2278
2279
  next.add(project.id);
@@ -2292,6 +2293,11 @@ var App = ({
2292
2293
  }, 3e3);
2293
2294
  } catch (error) {
2294
2295
  if (error instanceof LaunchCancelledError) {
2296
+ if (outputPathOnExit) {
2297
+ onSetExitPath?.(cdTarget);
2298
+ exit();
2299
+ return;
2300
+ }
2295
2301
  setHint("Launch cancelled");
2296
2302
  setTimeout(() => {
2297
2303
  setHint(defaultHintMessage);
@@ -2304,7 +2310,7 @@ var App = ({
2304
2310
  setHint(defaultHintMessage);
2305
2311
  }, 3e3);
2306
2312
  }
2307
- }, [index, onLaunchWithEditor, sortedProjects]);
2313
+ }, [exit, index, onLaunchWithEditor, onSetExitPath, outputPathOnExit, sortedProjects]);
2308
2314
  const launchEditorOnly = useCallback(async () => {
2309
2315
  if (!onLaunchEditorOnly) {
2310
2316
  setHint("Launch editor only not available");
@@ -2634,7 +2640,186 @@ var App = ({
2634
2640
 
2635
2641
  // src/index.tsx
2636
2642
  import { jsx as jsx7 } from "react/jsx-runtime";
2643
+ var SHELL_INIT_MARKER_START = "# >>> unity-hub-cli >>>";
2644
+ var SHELL_INIT_MARKER_END = "# <<< unity-hub-cli <<<";
2645
+ var getShellConfigPath = () => {
2646
+ const shell = process3.env["SHELL"] ?? "";
2647
+ const home = homedir();
2648
+ if (shell.includes("zsh")) {
2649
+ return join9(home, ".zshrc");
2650
+ }
2651
+ if (shell.includes("bash")) {
2652
+ const bashrcPath = join9(home, ".bashrc");
2653
+ const profilePath = join9(home, ".bash_profile");
2654
+ return existsSync3(bashrcPath) ? bashrcPath : profilePath;
2655
+ }
2656
+ if (shell.includes("fish")) {
2657
+ return join9(home, ".config", "fish", "config.fish");
2658
+ }
2659
+ if (process3.platform === "win32") {
2660
+ return join9(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
2661
+ }
2662
+ return void 0;
2663
+ };
2664
+ var getShellInitScriptWithMarkers = () => {
2665
+ const script = getShellInitScript();
2666
+ return `${SHELL_INIT_MARKER_START}
2667
+ ${script}
2668
+ ${SHELL_INIT_MARKER_END}`;
2669
+ };
2670
+ var askConfirmation = (question) => {
2671
+ const rl = createInterface2({
2672
+ input: process3.stdin,
2673
+ output: process3.stdout
2674
+ });
2675
+ return new Promise((resolve4) => {
2676
+ rl.question(question, (answer) => {
2677
+ rl.close();
2678
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
2679
+ });
2680
+ });
2681
+ };
2682
+ var previewShellInit = () => {
2683
+ const configPath = getShellConfigPath();
2684
+ console.log("=== Shell Integration Preview ===\n");
2685
+ console.log(`Target file: ${configPath ?? "Unknown (unsupported shell)"}
2686
+ `);
2687
+ console.log("Content to be added:\n");
2688
+ console.log(getShellInitScriptWithMarkers());
2689
+ };
2690
+ var installShellInit = () => {
2691
+ const configPath = getShellConfigPath();
2692
+ if (!configPath) {
2693
+ return { success: false, message: "Unsupported shell. Please copy the function from the README into your shell config manually." };
2694
+ }
2695
+ const scriptWithMarkers = getShellInitScriptWithMarkers();
2696
+ const markerPattern = new RegExp(
2697
+ `${SHELL_INIT_MARKER_START}[\\s\\S]*?${SHELL_INIT_MARKER_END}`,
2698
+ "g"
2699
+ );
2700
+ let content = "";
2701
+ if (existsSync3(configPath)) {
2702
+ content = readFileSync(configPath, "utf-8");
2703
+ }
2704
+ const existingMatch = content.match(markerPattern);
2705
+ if (existingMatch && existingMatch[0] === scriptWithMarkers) {
2706
+ return { success: true, message: `Shell integration is already up to date in ${configPath}` };
2707
+ }
2708
+ let newContent;
2709
+ let action;
2710
+ if (existingMatch) {
2711
+ newContent = content.replace(markerPattern, scriptWithMarkers);
2712
+ action = "updated";
2713
+ } else {
2714
+ newContent = content.trimEnd() + "\n\n" + scriptWithMarkers + "\n";
2715
+ action = "installed";
2716
+ }
2717
+ mkdirSync(dirname2(configPath), { recursive: true });
2718
+ writeFileSync(configPath, newContent, "utf-8");
2719
+ return { success: true, message: `Shell integration ${action} in ${configPath}` };
2720
+ };
2721
+ var getNodePath = () => {
2722
+ const isWindows = process3.platform === "win32";
2723
+ const command = isWindows ? "where node" : "which node";
2724
+ try {
2725
+ const result = execSync(command, { encoding: "utf-8" }).trim().split("\n")[0];
2726
+ if (result && existsSync3(result)) {
2727
+ return result;
2728
+ }
2729
+ } catch {
2730
+ }
2731
+ return "node";
2732
+ };
2733
+ var getUnityHubCliPath = () => {
2734
+ const isWindows = process3.platform === "win32";
2735
+ try {
2736
+ const prefix = execSync("npm config get prefix", { encoding: "utf-8" }).trim();
2737
+ const binDir = isWindows ? prefix : `${prefix}/bin`;
2738
+ const cliPath = isWindows ? `${binDir}/unity-hub-cli.cmd` : `${binDir}/unity-hub-cli`;
2739
+ if (existsSync3(cliPath)) {
2740
+ return cliPath;
2741
+ }
2742
+ } catch {
2743
+ }
2744
+ return "unity-hub-cli";
2745
+ };
2746
+ var getShellInitScript = () => {
2747
+ const shell = process3.env["SHELL"] ?? "";
2748
+ const isWindows = process3.platform === "win32";
2749
+ const nodePath = getNodePath();
2750
+ const cliPath = getUnityHubCliPath();
2751
+ if (shell.includes("fish")) {
2752
+ return `function unity-hub
2753
+ set -l tmpfile (mktemp)
2754
+ ${nodePath} ${cliPath} --output-path-on-exit > $tmpfile
2755
+ set -l dir (cat $tmpfile)
2756
+ rm -f $tmpfile
2757
+ if test -n "$dir"
2758
+ cd $dir
2759
+ end
2760
+ end`;
2761
+ }
2762
+ if (shell.includes("bash") || shell.includes("zsh")) {
2763
+ return `unity-hub() {
2764
+ local tmpfile=$(mktemp)
2765
+ ${nodePath} ${cliPath} --output-path-on-exit >| "$tmpfile"
2766
+ local dir=$(cat "$tmpfile")
2767
+ rm -f "$tmpfile"
2768
+ if [ -n "$dir" ]; then
2769
+ cd "$dir"
2770
+ fi
2771
+ }`;
2772
+ }
2773
+ if (isWindows) {
2774
+ return `function unity-hub {
2775
+ $tmpfile = [System.IO.Path]::GetTempFileName()
2776
+ & "${cliPath}" --output-path-on-exit > $tmpfile
2777
+ $dir = Get-Content $tmpfile
2778
+ Remove-Item $tmpfile
2779
+ if ($dir) {
2780
+ Set-Location $dir
2781
+ }
2782
+ }`;
2783
+ }
2784
+ return `unity-hub() {
2785
+ local tmpfile=$(mktemp)
2786
+ ${nodePath} ${cliPath} --output-path-on-exit >| "$tmpfile"
2787
+ local dir=$(cat "$tmpfile")
2788
+ rm -f "$tmpfile"
2789
+ if [ -n "$dir" ]; then
2790
+ cd "$dir"
2791
+ fi
2792
+ }`;
2793
+ };
2637
2794
  var bootstrap = async () => {
2795
+ const args = process3.argv.slice(2);
2796
+ if (args.includes("--shell-init")) {
2797
+ const isDryRun = args.includes("--dry-run");
2798
+ if (isDryRun) {
2799
+ previewShellInit();
2800
+ return;
2801
+ }
2802
+ const configPath = getShellConfigPath();
2803
+ if (!configPath) {
2804
+ console.log("Unsupported shell. Please copy the function from the README into your shell config manually.");
2805
+ process3.exitCode = 1;
2806
+ return;
2807
+ }
2808
+ console.log(`This will install the unity-hub function to: ${configPath}
2809
+ `);
2810
+ previewShellInit();
2811
+ console.log("");
2812
+ const confirmed = await askConfirmation("Proceed with installation? (y/n): ");
2813
+ if (!confirmed) {
2814
+ console.log("Installation cancelled.");
2815
+ return;
2816
+ }
2817
+ const result = installShellInit();
2818
+ console.log(result.message);
2819
+ process3.exitCode = result.success ? 0 : 1;
2820
+ return;
2821
+ }
2822
+ const outputPathOnExit = args.includes("--output-path-on-exit");
2638
2823
  const isWindows = process3.platform === "win32";
2639
2824
  const unityHubReader = isWindows ? new WinUnityHubProjectsReader() : new MacUnityHubProjectsReader();
2640
2825
  const gitRepositoryInfoReader = new GitRepositoryInfoReader();
@@ -2694,8 +2879,13 @@ var bootstrap = async () => {
2694
2879
  process3.exitCode = 1;
2695
2880
  return;
2696
2881
  }
2882
+ if (outputPathOnExit && process3.stderr.isTTY) {
2883
+ chalk.level = 3;
2884
+ }
2697
2885
  const theme = await detectTerminalTheme();
2886
+ let lastOpenedPath;
2698
2887
  const projects = await listProjectsUseCase.execute();
2888
+ const renderOptions = outputPathOnExit ? { stdout: process3.stderr, stdin: process3.stdin } : void 0;
2699
2889
  const { waitUntilExit } = render(
2700
2890
  /* @__PURE__ */ jsx7(ThemeProvider, { theme, children: /* @__PURE__ */ jsx7(
2701
2891
  App,
@@ -2706,12 +2896,24 @@ var bootstrap = async () => {
2706
2896
  onLaunchEditorOnly: (project) => launchEditorOnlyUseCase.execute(project),
2707
2897
  onTerminate: (project) => terminateProjectUseCase.execute(project),
2708
2898
  onRefresh: () => listProjectsUseCase.execute(),
2709
- useGitRootName
2899
+ useGitRootName,
2900
+ outputPathOnExit,
2901
+ onSetExitPath: (path) => {
2902
+ lastOpenedPath = path;
2903
+ }
2710
2904
  }
2711
- ) })
2905
+ ) }),
2906
+ renderOptions
2712
2907
  );
2713
2908
  await waitUntilExit();
2714
- process3.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2909
+ if (outputPathOnExit) {
2910
+ process3.stderr.write("\x1B[2J\x1B[3J\x1B[H");
2911
+ if (lastOpenedPath) {
2912
+ process3.stdout.write(lastOpenedPath);
2913
+ }
2914
+ } else {
2915
+ process3.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2916
+ }
2715
2917
  } catch (error) {
2716
2918
  const message = error instanceof Error ? error.message : String(error);
2717
2919
  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.18.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.3",
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
  }