launch-unity 0.11.0 → 0.13.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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * launch-unity library entry point.
3
+ * Exports core functions for programmatic usage.
4
+ * Uses lib.ts which has no CLI side effects.
5
+ */
6
+ export { LaunchOptions, LaunchResolvedOptions, UnityProcessInfo, parseArgs, findUnityProjectBfs, getUnityVersion, launch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, } from './lib.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEL,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAEhB,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,MAAM,EACN,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * launch-unity library entry point.
3
+ * Exports core functions for programmatic usage.
4
+ * Uses lib.ts which has no CLI side effects.
5
+ */
6
+ export {
7
+ // Functions
8
+ parseArgs, findUnityProjectBfs, getUnityVersion, launch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, } from './lib.js';
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ export type LaunchOptions = {
3
+ subcommand?: "update";
4
+ projectPath?: string;
5
+ platform?: string | undefined;
6
+ unityArgs: string[];
7
+ searchMaxDepth: number;
8
+ restart: boolean;
9
+ addUnityHub: boolean;
10
+ favoriteUnityHub: boolean;
11
+ };
12
+ export type LaunchResolvedOptions = {
13
+ projectPath: string;
14
+ platform?: string | undefined;
15
+ unityArgs: string[];
16
+ unityVersion: string;
17
+ };
18
+ export type UnityProcessInfo = {
19
+ pid: number;
20
+ projectPath: string;
21
+ };
22
+ export declare function parseArgs(argv: string[]): LaunchOptions;
23
+ export declare function getUnityVersion(projectPath: string): string;
24
+ export declare function findRunningUnityProcess(projectPath: string): Promise<UnityProcessInfo | undefined>;
25
+ export declare function focusUnityProcess(pid: number): Promise<void>;
26
+ export declare function handleStaleLockfile(projectPath: string): Promise<void>;
27
+ export declare function killRunningUnity(projectPath: string): Promise<void>;
28
+ export declare function findUnityProjectBfs(rootDir: string, maxDepth: number): string | undefined;
29
+ export declare function launch(opts: LaunchResolvedOptions): Promise<void>;
30
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":";AAeA,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAyJF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAqHvD;AAyCD,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAe3D;AA2ND,wBAAsB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAIxG;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AA4CD,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB5E;AAiCD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzE;AAgDD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqDzF;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
package/dist/launch.js CHANGED
@@ -7,9 +7,9 @@ import { execFile, spawn } from "node:child_process";
7
7
  import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
8
8
  import { rm } from "node:fs/promises";
9
9
  import { dirname, join, resolve } from "node:path";
10
- import { fileURLToPath } from "node:url";
10
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { promisify } from "node:util";
12
- import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists } from "./unityHub.js";
12
+ import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs } from "./unityHub.js";
13
13
  const execFileAsync = promisify(execFile);
14
14
  const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
15
15
  const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
@@ -130,7 +130,7 @@ const compareSemverTriplet = (left, right) => {
130
130
  }
131
131
  return 0;
132
132
  };
133
- function parseArgs(argv) {
133
+ export function parseArgs(argv) {
134
134
  const args = argv.slice(2);
135
135
  const doubleDashIndex = args.indexOf("--");
136
136
  let cliArgs = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
@@ -277,7 +277,7 @@ Commands:
277
277
  `;
278
278
  process.stdout.write(help);
279
279
  }
280
- function getUnityVersion(projectPath) {
280
+ export function getUnityVersion(projectPath) {
281
281
  const versionFile = join(projectPath, "ProjectSettings", "ProjectVersion.txt");
282
282
  if (!existsSync(versionFile)) {
283
283
  console.error(`Error: ProjectVersion.txt not found at ${versionFile}`);
@@ -476,12 +476,12 @@ async function listUnityProcesses() {
476
476
  }
477
477
  return [];
478
478
  }
479
- async function findRunningUnityProcess(projectPath) {
479
+ export async function findRunningUnityProcess(projectPath) {
480
480
  const normalizedTarget = normalizePath(projectPath);
481
481
  const processes = await listUnityProcesses();
482
482
  return processes.find((candidate) => pathsEqual(candidate.projectPath, normalizedTarget));
483
483
  }
484
- async function focusUnityProcess(pid) {
484
+ export async function focusUnityProcess(pid) {
485
485
  if (process.platform === "darwin") {
486
486
  await focusUnityProcessMac(pid);
487
487
  return;
@@ -530,7 +530,7 @@ async function focusUnityProcessWindows(pid) {
530
530
  console.warn(`Failed to bring Unity to front on Windows: ${message}`);
531
531
  }
532
532
  }
533
- async function handleStaleLockfile(projectPath) {
533
+ export async function handleStaleLockfile(projectPath) {
534
534
  const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
535
535
  const lockfilePath = join(tempDirectoryPath, UNITY_LOCKFILE_NAME);
536
536
  if (!existsSync(lockfilePath)) {
@@ -584,7 +584,7 @@ async function waitForProcessExit(pid) {
584
584
  }
585
585
  return false;
586
586
  }
587
- async function killRunningUnity(projectPath) {
587
+ export async function killRunningUnity(projectPath) {
588
588
  const processInfo = await findRunningUnityProcess(projectPath);
589
589
  if (!processInfo) {
590
590
  console.log("No running Unity process found for this project.");
@@ -643,7 +643,7 @@ function listSubdirectoriesSorted(dir) {
643
643
  }
644
644
  return entries;
645
645
  }
646
- function findUnityProjectBfs(rootDir, maxDepth) {
646
+ export function findUnityProjectBfs(rootDir, maxDepth) {
647
647
  const queue = [];
648
648
  let rootCanonical;
649
649
  try {
@@ -695,7 +695,7 @@ function findUnityProjectBfs(rootDir, maxDepth) {
695
695
  }
696
696
  return undefined;
697
697
  }
698
- function launch(opts) {
698
+ export async function launch(opts) {
699
699
  const { projectPath, platform, unityArgs, unityVersion } = opts;
700
700
  const unityPath = getUnityPath(unityVersion);
701
701
  console.log(`Detected Unity version: ${unityVersion}`);
@@ -711,6 +711,10 @@ function launch(opts) {
711
711
  if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
712
712
  args.push("-buildTarget", platform);
713
713
  }
714
+ const hubCliArgs = await getProjectCliArgs(projectPath);
715
+ if (hubCliArgs.length > 0) {
716
+ args.push(...hubCliArgs);
717
+ }
714
718
  if (unityArgs.length > 0) {
715
719
  args.push(...unityArgs);
716
720
  }
@@ -774,7 +778,7 @@ async function main() {
774
778
  unityArgs: options.unityArgs,
775
779
  unityVersion,
776
780
  };
777
- launch(resolved);
781
+ await launch(resolved);
778
782
  // Best-effort update of Unity Hub's lastModified timestamp.
779
783
  const now = new Date();
780
784
  try {
@@ -838,7 +842,20 @@ async function runSelfUpdate() {
838
842
  }
839
843
  process.exit(1);
840
844
  }
841
- main().catch((error) => {
842
- console.error(error);
843
- process.exit(1);
844
- });
845
+ // Only run main() when this file is executed directly (not when imported as a library)
846
+ let isDirectExecution = false;
847
+ if (typeof process.argv[1] === "string") {
848
+ try {
849
+ isDirectExecution =
850
+ import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
851
+ }
852
+ catch {
853
+ isDirectExecution = false;
854
+ }
855
+ }
856
+ if (isDirectExecution) {
857
+ main().catch((error) => {
858
+ console.error(error);
859
+ process.exit(1);
860
+ });
861
+ }
package/dist/lib.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * launch-unity core library functions.
3
+ * Pure library code without CLI entry point or side effects.
4
+ */
5
+ import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs } from "./unityHub.js";
6
+ export type LaunchOptions = {
7
+ subcommand?: "update";
8
+ projectPath?: string;
9
+ platform?: string | undefined;
10
+ unityArgs: string[];
11
+ searchMaxDepth: number;
12
+ restart: boolean;
13
+ addUnityHub: boolean;
14
+ favoriteUnityHub: boolean;
15
+ };
16
+ export type LaunchResolvedOptions = {
17
+ projectPath: string;
18
+ platform?: string | undefined;
19
+ unityArgs: string[];
20
+ unityVersion: string;
21
+ };
22
+ export type UnityProcessInfo = {
23
+ pid: number;
24
+ projectPath: string;
25
+ };
26
+ export declare function parseArgs(argv: string[]): LaunchOptions;
27
+ export declare function getUnityVersion(projectPath: string): string;
28
+ export declare function findRunningUnityProcess(projectPath: string): Promise<UnityProcessInfo | undefined>;
29
+ export declare function focusUnityProcess(pid: number): Promise<void>;
30
+ export declare function handleStaleLockfile(projectPath: string): Promise<void>;
31
+ export declare function killRunningUnity(projectPath: string): Promise<void>;
32
+ export declare function findUnityProjectBfs(rootDir: string, maxDepth: number): string | undefined;
33
+ export declare function launch(opts: LaunchResolvedOptions): Promise<void>;
34
+ export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs };
35
+ //# sourceMappingURL=lib.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEzH,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAYF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAmHvD;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAc3D;AAoND,wBAAsB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAIxG;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AA4CD,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB5E;AAiCD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzE;AAgDD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqDzF;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE;AAGD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAC"}
package/dist/lib.js ADDED
@@ -0,0 +1,561 @@
1
+ /**
2
+ * launch-unity core library functions.
3
+ * Pure library code without CLI entry point or side effects.
4
+ */
5
+ import { execFile, spawn } from "node:child_process";
6
+ import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
7
+ import { rm } from "node:fs/promises";
8
+ import { join, resolve } from "node:path";
9
+ import { promisify } from "node:util";
10
+ import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs } from "./unityHub.js";
11
+ const execFileAsync = promisify(execFile);
12
+ const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
13
+ const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
14
+ const PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
15
+ const PROCESS_LIST_COMMAND_MAC = "ps";
16
+ const PROCESS_LIST_ARGS_MAC = ["-axo", "pid=,command=", "-ww"];
17
+ const WINDOWS_POWERSHELL = "powershell";
18
+ const UNITY_LOCKFILE_NAME = "UnityLockfile";
19
+ const TEMP_DIRECTORY_NAME = "Temp";
20
+ export function parseArgs(argv) {
21
+ const args = argv.slice(2);
22
+ const doubleDashIndex = args.indexOf("--");
23
+ let cliArgs = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
24
+ const unityArgs = doubleDashIndex >= 0 ? args.slice(doubleDashIndex + 1) : [];
25
+ let subcommand;
26
+ const firstToken = cliArgs[0];
27
+ if (firstToken === "update") {
28
+ subcommand = "update";
29
+ cliArgs = cliArgs.slice(1);
30
+ }
31
+ const positionals = [];
32
+ let maxDepth = 3; // default 3; -1 means unlimited
33
+ let restart = false;
34
+ let addUnityHub = false;
35
+ let favoriteUnityHub = false;
36
+ let platform;
37
+ for (let i = 0; i < cliArgs.length; i++) {
38
+ const arg = cliArgs[i] ?? "";
39
+ if (arg === "--help" || arg === "-h") {
40
+ continue; // Skip help flag - caller should handle
41
+ }
42
+ if (arg === "--version" || arg === "-v") {
43
+ continue; // Skip version flag - caller should handle
44
+ }
45
+ if (arg === "-r" || arg === "--restart") {
46
+ restart = true;
47
+ continue;
48
+ }
49
+ if (arg === "-u" ||
50
+ arg === "-a" ||
51
+ arg === "--unity-hub-entry" ||
52
+ arg === "--add-unity-hub") {
53
+ addUnityHub = true;
54
+ continue;
55
+ }
56
+ if (arg === "-f" || arg === "--favorite") {
57
+ favoriteUnityHub = true;
58
+ continue;
59
+ }
60
+ if (arg === "-p" || arg === "--platform") {
61
+ const next = cliArgs[i + 1];
62
+ if (typeof next === "string" && !next.startsWith("-")) {
63
+ platform = next;
64
+ i += 1;
65
+ }
66
+ continue;
67
+ }
68
+ if (arg.startsWith("--platform=")) {
69
+ const value = arg.slice("--platform=".length);
70
+ if (value.length > 0) {
71
+ platform = value;
72
+ }
73
+ continue;
74
+ }
75
+ if (arg.startsWith("--max-depth")) {
76
+ const parts = arg.split("=");
77
+ if (parts.length === 2) {
78
+ const value = Number.parseInt(parts[1] ?? "", 10);
79
+ if (Number.isFinite(value)) {
80
+ maxDepth = value;
81
+ }
82
+ continue;
83
+ }
84
+ const next = cliArgs[i + 1];
85
+ if (typeof next === "string" && !next.startsWith("-")) {
86
+ const value = Number.parseInt(next, 10);
87
+ if (Number.isFinite(value)) {
88
+ maxDepth = value;
89
+ }
90
+ i += 1;
91
+ continue;
92
+ }
93
+ continue;
94
+ }
95
+ if (arg.startsWith("-")) {
96
+ console.warn(`Warning: Unknown option ignored: ${arg}`);
97
+ continue;
98
+ }
99
+ positionals.push(arg);
100
+ }
101
+ let projectPath;
102
+ if (positionals.length > 0) {
103
+ projectPath = resolve(positionals[0] ?? "");
104
+ }
105
+ if (positionals.length > 1) {
106
+ const ignored = positionals.slice(1).join(", ");
107
+ console.warn(`Warning: Extra arguments ignored: ${ignored}`);
108
+ console.warn(" Use -p option for platform: launch-unity -p <platform>");
109
+ }
110
+ const options = {
111
+ unityArgs,
112
+ searchMaxDepth: maxDepth,
113
+ restart,
114
+ addUnityHub,
115
+ favoriteUnityHub,
116
+ };
117
+ if (subcommand) {
118
+ options.subcommand = subcommand;
119
+ }
120
+ if (projectPath !== undefined) {
121
+ options.projectPath = projectPath;
122
+ }
123
+ if (platform !== undefined) {
124
+ options.platform = platform;
125
+ }
126
+ return options;
127
+ }
128
+ export function getUnityVersion(projectPath) {
129
+ const versionFile = join(projectPath, "ProjectSettings", "ProjectVersion.txt");
130
+ if (!existsSync(versionFile)) {
131
+ throw new Error(`ProjectVersion.txt not found at ${versionFile}. This does not appear to be a Unity project.`);
132
+ }
133
+ const content = readFileSync(versionFile, "utf8");
134
+ const version = content.match(/m_EditorVersion:\s*([^\s\n]+)/)?.[1];
135
+ if (!version) {
136
+ throw new Error(`Could not extract Unity version from ${versionFile}`);
137
+ }
138
+ return version;
139
+ }
140
+ function getUnityPathWindows(version) {
141
+ const candidates = [];
142
+ const programFiles = process.env["PROGRAMFILES"];
143
+ const programFilesX86 = process.env["PROGRAMFILES(X86)"];
144
+ const localAppData = process.env["LOCALAPPDATA"];
145
+ const addCandidate = (base) => {
146
+ if (!base) {
147
+ return;
148
+ }
149
+ candidates.push(join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
150
+ };
151
+ addCandidate(programFiles);
152
+ addCandidate(programFilesX86);
153
+ addCandidate(localAppData);
154
+ candidates.push(join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
155
+ for (const candidate of candidates) {
156
+ if (existsSync(candidate)) {
157
+ return candidate;
158
+ }
159
+ }
160
+ return candidates[0] ?? join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe");
161
+ }
162
+ function getUnityPath(version) {
163
+ if (process.platform === "darwin") {
164
+ return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
165
+ }
166
+ if (process.platform === "win32") {
167
+ return getUnityPathWindows(version);
168
+ }
169
+ return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
170
+ }
171
+ const removeTrailingSeparators = (target) => {
172
+ let trimmed = target;
173
+ while (trimmed.length > 1 && (trimmed.endsWith("/") || trimmed.endsWith("\\"))) {
174
+ trimmed = trimmed.slice(0, -1);
175
+ }
176
+ return trimmed;
177
+ };
178
+ const normalizePath = (target) => {
179
+ const resolvedPath = resolve(target);
180
+ const trimmed = removeTrailingSeparators(resolvedPath);
181
+ return trimmed;
182
+ };
183
+ const toComparablePath = (value) => {
184
+ return value.replace(/\\/g, "/").toLocaleLowerCase();
185
+ };
186
+ const pathsEqual = (left, right) => {
187
+ return toComparablePath(normalizePath(left)) === toComparablePath(normalizePath(right));
188
+ };
189
+ function extractProjectPath(command) {
190
+ const match = command.match(PROJECT_PATH_PATTERN);
191
+ if (!match) {
192
+ return undefined;
193
+ }
194
+ const raw = match[1];
195
+ if (!raw) {
196
+ return undefined;
197
+ }
198
+ const trimmed = raw.trim();
199
+ if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
200
+ return trimmed.slice(1, -1);
201
+ }
202
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
203
+ return trimmed.slice(1, -1);
204
+ }
205
+ return trimmed;
206
+ }
207
+ const isUnityAuxiliaryProcess = (command) => {
208
+ const normalizedCommand = command.toLowerCase();
209
+ if (normalizedCommand.includes("-batchmode")) {
210
+ return true;
211
+ }
212
+ return normalizedCommand.includes("assetimportworker");
213
+ };
214
+ async function listUnityProcessesMac() {
215
+ let stdout = "";
216
+ try {
217
+ const result = await execFileAsync(PROCESS_LIST_COMMAND_MAC, PROCESS_LIST_ARGS_MAC);
218
+ stdout = result.stdout;
219
+ }
220
+ catch (error) {
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ console.error(`Failed to retrieve Unity process list: ${message}`);
223
+ return [];
224
+ }
225
+ const lines = stdout
226
+ .split("\n")
227
+ .map((line) => line.trim())
228
+ .filter((line) => line.length > 0);
229
+ const processes = [];
230
+ for (const line of lines) {
231
+ const match = line.match(/^(\d+)\s+(.*)$/);
232
+ if (!match) {
233
+ continue;
234
+ }
235
+ const pidValue = Number.parseInt(match[1] ?? "", 10);
236
+ if (!Number.isFinite(pidValue)) {
237
+ continue;
238
+ }
239
+ const command = match[2] ?? "";
240
+ if (!UNITY_EXECUTABLE_PATTERN_MAC.test(command)) {
241
+ continue;
242
+ }
243
+ if (isUnityAuxiliaryProcess(command)) {
244
+ continue;
245
+ }
246
+ const projectArgument = extractProjectPath(command);
247
+ if (!projectArgument) {
248
+ continue;
249
+ }
250
+ processes.push({
251
+ pid: pidValue,
252
+ projectPath: normalizePath(projectArgument),
253
+ });
254
+ }
255
+ return processes;
256
+ }
257
+ async function listUnityProcessesWindows() {
258
+ const scriptLines = [
259
+ "$ErrorActionPreference = 'Stop'",
260
+ "$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }",
261
+ "foreach ($process in $processes) {",
262
+ " $commandLine = $process.CommandLine -replace \"`r\", ' ' -replace \"`n\", ' '",
263
+ " Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)",
264
+ "}",
265
+ ];
266
+ let stdout = "";
267
+ try {
268
+ const result = await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
269
+ stdout = result.stdout ?? "";
270
+ }
271
+ catch (error) {
272
+ const message = error instanceof Error ? error.message : String(error);
273
+ console.error(`Failed to retrieve Unity process list on Windows: ${message}`);
274
+ return [];
275
+ }
276
+ const lines = stdout
277
+ .split("\n")
278
+ .map((line) => line.trim())
279
+ .filter((line) => line.length > 0);
280
+ const processes = [];
281
+ for (const line of lines) {
282
+ const delimiterIndex = line.indexOf("|");
283
+ if (delimiterIndex < 0) {
284
+ continue;
285
+ }
286
+ const pidText = line.slice(0, delimiterIndex).trim();
287
+ const command = line.slice(delimiterIndex + 1).trim();
288
+ const pidValue = Number.parseInt(pidText, 10);
289
+ if (!Number.isFinite(pidValue)) {
290
+ continue;
291
+ }
292
+ if (!UNITY_EXECUTABLE_PATTERN_WINDOWS.test(command)) {
293
+ continue;
294
+ }
295
+ if (isUnityAuxiliaryProcess(command)) {
296
+ continue;
297
+ }
298
+ const projectArgument = extractProjectPath(command);
299
+ if (!projectArgument) {
300
+ continue;
301
+ }
302
+ processes.push({
303
+ pid: pidValue,
304
+ projectPath: normalizePath(projectArgument),
305
+ });
306
+ }
307
+ return processes;
308
+ }
309
+ async function listUnityProcesses() {
310
+ if (process.platform === "darwin") {
311
+ return await listUnityProcessesMac();
312
+ }
313
+ if (process.platform === "win32") {
314
+ return await listUnityProcessesWindows();
315
+ }
316
+ return [];
317
+ }
318
+ export async function findRunningUnityProcess(projectPath) {
319
+ const normalizedTarget = normalizePath(projectPath);
320
+ const processes = await listUnityProcesses();
321
+ return processes.find((candidate) => pathsEqual(candidate.projectPath, normalizedTarget));
322
+ }
323
+ export async function focusUnityProcess(pid) {
324
+ if (process.platform === "darwin") {
325
+ await focusUnityProcessMac(pid);
326
+ return;
327
+ }
328
+ if (process.platform === "win32") {
329
+ await focusUnityProcessWindows(pid);
330
+ }
331
+ }
332
+ async function focusUnityProcessMac(pid) {
333
+ const script = `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
334
+ try {
335
+ await execFileAsync("osascript", ["-e", script]);
336
+ console.log("Brought existing Unity to the front.");
337
+ }
338
+ catch (error) {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ console.warn(`Failed to bring Unity to front: ${message}`);
341
+ }
342
+ }
343
+ async function focusUnityProcessWindows(pid) {
344
+ const addTypeLines = [
345
+ "Add-Type -TypeDefinition @\"",
346
+ "using System;",
347
+ "using System.Runtime.InteropServices;",
348
+ "public static class Win32Interop {",
349
+ " [DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);",
350
+ " [DllImport(\"user32.dll\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);",
351
+ "}",
352
+ "\"@",
353
+ ];
354
+ const scriptLines = [
355
+ "$ErrorActionPreference = 'Stop'",
356
+ ...addTypeLines,
357
+ `try { $process = Get-Process -Id ${pid} -ErrorAction Stop } catch { return }`,
358
+ "$handle = $process.MainWindowHandle",
359
+ "if ($handle -eq 0) { return }",
360
+ "[Win32Interop]::ShowWindowAsync($handle, 9) | Out-Null",
361
+ "[Win32Interop]::SetForegroundWindow($handle) | Out-Null",
362
+ ];
363
+ try {
364
+ await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
365
+ console.log("Brought existing Unity to the front.");
366
+ }
367
+ catch (error) {
368
+ const message = error instanceof Error ? error.message : String(error);
369
+ console.warn(`Failed to bring Unity to front on Windows: ${message}`);
370
+ }
371
+ }
372
+ export async function handleStaleLockfile(projectPath) {
373
+ const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
374
+ const lockfilePath = join(tempDirectoryPath, UNITY_LOCKFILE_NAME);
375
+ if (!existsSync(lockfilePath)) {
376
+ return;
377
+ }
378
+ console.log(`UnityLockfile found without active Unity process: ${lockfilePath}`);
379
+ console.log("Assuming previous crash. Cleaning Temp directory and continuing launch.");
380
+ try {
381
+ await rm(tempDirectoryPath, { recursive: true, force: true });
382
+ console.log("Deleted Temp directory.");
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ console.warn(`Failed to delete Temp directory: ${message}`);
387
+ }
388
+ try {
389
+ await rm(lockfilePath, { force: true });
390
+ console.log("Deleted UnityLockfile.");
391
+ }
392
+ catch (error) {
393
+ const message = error instanceof Error ? error.message : String(error);
394
+ console.warn(`Failed to delete UnityLockfile: ${message}`);
395
+ }
396
+ }
397
+ const KILL_POLL_INTERVAL_MS = 100;
398
+ const KILL_TIMEOUT_MS = 10000;
399
+ function isProcessAlive(pid) {
400
+ try {
401
+ process.kill(pid, 0);
402
+ return true;
403
+ }
404
+ catch {
405
+ return false;
406
+ }
407
+ }
408
+ function killProcess(pid) {
409
+ try {
410
+ process.kill(pid, "SIGKILL");
411
+ }
412
+ catch {
413
+ // Process already exited
414
+ }
415
+ }
416
+ async function waitForProcessExit(pid) {
417
+ const start = Date.now();
418
+ while (Date.now() - start < KILL_TIMEOUT_MS) {
419
+ if (!isProcessAlive(pid)) {
420
+ return true;
421
+ }
422
+ await new Promise((resolve) => setTimeout(resolve, KILL_POLL_INTERVAL_MS));
423
+ }
424
+ return false;
425
+ }
426
+ export async function killRunningUnity(projectPath) {
427
+ const processInfo = await findRunningUnityProcess(projectPath);
428
+ if (!processInfo) {
429
+ console.log("No running Unity process found for this project.");
430
+ return;
431
+ }
432
+ const pid = processInfo.pid;
433
+ console.log(`Killing Unity (PID: ${pid})...`);
434
+ killProcess(pid);
435
+ const exited = await waitForProcessExit(pid);
436
+ if (!exited) {
437
+ throw new Error(`Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
438
+ }
439
+ console.log("Unity killed.");
440
+ }
441
+ function hasBuildTargetArg(unityArgs) {
442
+ for (const arg of unityArgs) {
443
+ if (arg === "-buildTarget") {
444
+ return true;
445
+ }
446
+ if (arg.startsWith("-buildTarget=")) {
447
+ return true;
448
+ }
449
+ }
450
+ return false;
451
+ }
452
+ const EXCLUDED_DIR_NAMES = new Set([
453
+ "library",
454
+ "temp",
455
+ "logs",
456
+ "obj",
457
+ ".git",
458
+ "node_modules",
459
+ ".idea",
460
+ ".vscode",
461
+ ".vs",
462
+ ]);
463
+ function isUnityProjectRoot(candidateDir) {
464
+ const versionFile = join(candidateDir, "ProjectSettings", "ProjectVersion.txt");
465
+ return existsSync(versionFile);
466
+ }
467
+ function listSubdirectoriesSorted(dir) {
468
+ let entries = [];
469
+ try {
470
+ const dirents = readdirSync(dir, { withFileTypes: true });
471
+ const subdirs = dirents
472
+ .filter((d) => d.isDirectory())
473
+ .map((d) => d.name)
474
+ .filter((name) => !EXCLUDED_DIR_NAMES.has(name.toLocaleLowerCase()));
475
+ subdirs.sort((a, b) => a.localeCompare(b));
476
+ entries = subdirs.map((name) => join(dir, name));
477
+ }
478
+ catch {
479
+ // Ignore directories we cannot read
480
+ entries = [];
481
+ }
482
+ return entries;
483
+ }
484
+ export function findUnityProjectBfs(rootDir, maxDepth) {
485
+ const queue = [];
486
+ let rootCanonical;
487
+ try {
488
+ rootCanonical = realpathSync(rootDir);
489
+ }
490
+ catch {
491
+ rootCanonical = rootDir;
492
+ }
493
+ queue.push({ dir: rootCanonical, depth: 0 });
494
+ const visited = new Set([toComparablePath(normalizePath(rootCanonical))]);
495
+ while (queue.length > 0) {
496
+ const current = queue.shift();
497
+ if (!current) {
498
+ continue;
499
+ }
500
+ const { dir, depth } = current;
501
+ if (isUnityProjectRoot(dir)) {
502
+ return normalizePath(dir);
503
+ }
504
+ const canDescend = maxDepth === -1 || depth < maxDepth;
505
+ if (!canDescend) {
506
+ continue;
507
+ }
508
+ const children = listSubdirectoriesSorted(dir);
509
+ for (const child of children) {
510
+ let childCanonical = child;
511
+ try {
512
+ const stat = lstatSync(child);
513
+ if (stat.isSymbolicLink()) {
514
+ try {
515
+ childCanonical = realpathSync(child);
516
+ }
517
+ catch {
518
+ // Broken symlink: skip
519
+ continue;
520
+ }
521
+ }
522
+ }
523
+ catch {
524
+ continue;
525
+ }
526
+ const key = toComparablePath(normalizePath(childCanonical));
527
+ if (visited.has(key)) {
528
+ continue;
529
+ }
530
+ visited.add(key);
531
+ queue.push({ dir: childCanonical, depth: depth + 1 });
532
+ }
533
+ }
534
+ return undefined;
535
+ }
536
+ export async function launch(opts) {
537
+ const { projectPath, platform, unityArgs, unityVersion } = opts;
538
+ const unityPath = getUnityPath(unityVersion);
539
+ console.log(`Detected Unity version: ${unityVersion}`);
540
+ if (!existsSync(unityPath)) {
541
+ throw new Error(`Unity ${unityVersion} not found at ${unityPath}. Please install Unity through Unity Hub.`);
542
+ }
543
+ console.log("Opening Unity...");
544
+ console.log(`Project Path: ${projectPath}`);
545
+ const args = ["-projectPath", projectPath];
546
+ const unityArgsContainBuildTarget = hasBuildTargetArg(unityArgs);
547
+ if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
548
+ args.push("-buildTarget", platform);
549
+ }
550
+ const hubCliArgs = await getProjectCliArgs(projectPath);
551
+ if (hubCliArgs.length > 0) {
552
+ args.push(...hubCliArgs);
553
+ }
554
+ if (unityArgs.length > 0) {
555
+ args.push(...unityArgs);
556
+ }
557
+ const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
558
+ child.unref();
559
+ }
560
+ // Re-export Unity Hub functions
561
+ export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs };
@@ -0,0 +1,5 @@
1
+ export declare const ensureProjectEntryAndUpdate: (projectPath: string, version: string, when: Date, setFavorite?: boolean) => Promise<void>;
2
+ export declare const updateLastModifiedIfExists: (projectPath: string, when: Date) => Promise<void>;
3
+ export declare const parseCliArgs: (cliArgsString: string) => string[];
4
+ export declare const getProjectCliArgs: (projectPath: string) => Promise<string[]>;
5
+ //# sourceMappingURL=unityHub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unityHub.d.ts","sourceRoot":"","sources":["../src/unityHub.ts"],"names":[],"mappings":"AAwFA,eAAO,MAAM,2BAA2B,GACtC,aAAa,MAAM,EACnB,SAAS,MAAM,EACf,MAAM,IAAI,EACV,qBAAmB,KAClB,OAAO,CAAC,IAAI,CAgEd,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,aAAa,MAAM,EACnB,MAAM,IAAI,KACT,OAAO,CAAC,IAAI,CAuDd,CAAC;AAoBF,eAAO,MAAM,YAAY,GAAI,eAAe,MAAM,KAAG,MAAM,EA6C1D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CA8C7E,CAAC"}
package/dist/unityHub.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
+ import assert from "node:assert";
4
5
  const resolveUnityHubProjectFiles = () => {
5
6
  if (process.platform === "darwin") {
6
7
  const home = process.env.HOME;
@@ -168,3 +169,99 @@ export const updateLastModifiedIfExists = async (projectPath, when) => {
168
169
  return;
169
170
  }
170
171
  };
172
+ const resolveUnityHubProjectsInfoFile = () => {
173
+ if (process.platform === "darwin") {
174
+ const home = process.env.HOME;
175
+ if (!home) {
176
+ return undefined;
177
+ }
178
+ return join(home, "Library", "Application Support", "UnityHub", "projectsInfo.json");
179
+ }
180
+ if (process.platform === "win32") {
181
+ const appData = process.env.APPDATA;
182
+ if (!appData) {
183
+ return undefined;
184
+ }
185
+ return join(appData, "UnityHub", "projectsInfo.json");
186
+ }
187
+ return undefined;
188
+ };
189
+ export const parseCliArgs = (cliArgsString) => {
190
+ assert(cliArgsString !== null && cliArgsString !== undefined, "cliArgsString must not be null");
191
+ const trimmed = cliArgsString.trim();
192
+ if (trimmed.length === 0) {
193
+ return [];
194
+ }
195
+ const tokens = [];
196
+ let current = "";
197
+ let inQuote = null;
198
+ for (const char of trimmed) {
199
+ if (inQuote !== null) {
200
+ if (char === inQuote) {
201
+ tokens.push(current);
202
+ current = "";
203
+ inQuote = null;
204
+ }
205
+ else {
206
+ current += char;
207
+ }
208
+ continue;
209
+ }
210
+ if (char === '"' || char === "'") {
211
+ inQuote = char;
212
+ continue;
213
+ }
214
+ if (char === " ") {
215
+ if (current.length > 0) {
216
+ tokens.push(current);
217
+ current = "";
218
+ }
219
+ continue;
220
+ }
221
+ current += char;
222
+ }
223
+ if (current.length > 0) {
224
+ tokens.push(current);
225
+ }
226
+ return tokens;
227
+ };
228
+ export const getProjectCliArgs = async (projectPath) => {
229
+ assert(projectPath !== null && projectPath !== undefined, "projectPath must not be null");
230
+ const infoFilePath = resolveUnityHubProjectsInfoFile();
231
+ if (!infoFilePath) {
232
+ logDebug("projectsInfo.json path could not be resolved.");
233
+ return [];
234
+ }
235
+ logDebug(`Reading projectsInfo.json: ${infoFilePath}`);
236
+ let content;
237
+ try {
238
+ content = await readFile(infoFilePath, "utf8");
239
+ }
240
+ catch {
241
+ logDebug("projectsInfo.json not found or not readable.");
242
+ return [];
243
+ }
244
+ let json;
245
+ try {
246
+ json = JSON.parse(content);
247
+ }
248
+ catch {
249
+ logDebug("projectsInfo.json parse failed.");
250
+ return [];
251
+ }
252
+ const normalizedProjectPath = normalizePath(projectPath);
253
+ const projectKey = Object.keys(json).find((key) => pathsEqual(key, normalizedProjectPath));
254
+ if (!projectKey) {
255
+ logDebug(`No entry found for project: ${normalizedProjectPath}`);
256
+ return [];
257
+ }
258
+ const projectInfo = json[projectKey];
259
+ const cliArgsString = projectInfo?.cliArgs;
260
+ if (!cliArgsString || cliArgsString.trim().length === 0) {
261
+ logDebug("cliArgs is empty or not defined.");
262
+ return [];
263
+ }
264
+ const parsed = parseCliArgs(cliArgsString);
265
+ logDebug(`Parsed Unity Hub cliArgs: ${JSON.stringify(parsed)}`);
266
+ return parsed;
267
+ };
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "launch-unity",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Open a Unity project with the matching Editor version (macOS/Windows)",
5
5
  "type": "module",
6
- "main": "dist/launch.js",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
7
14
  "bin": {
8
15
  "launch-unity": "dist/launch.js"
9
16
  },
@@ -38,16 +45,16 @@
38
45
  "node": ">=18"
39
46
  },
40
47
  "devDependencies": {
41
- "@types/node": "25.0.6",
42
- "@typescript-eslint/eslint-plugin": "8.53.0",
43
- "@typescript-eslint/parser": "8.53.0",
48
+ "@types/node": "25.2.0",
49
+ "@typescript-eslint/eslint-plugin": "8.54.0",
50
+ "@typescript-eslint/parser": "8.54.0",
44
51
  "eslint": "9.39.2",
45
52
  "eslint-config-prettier": "10.1.8",
46
- "prettier": "3.7.4",
53
+ "prettier": "3.8.1",
47
54
  "typescript": "5.9.3"
48
55
  },
49
56
  "dependencies": {
50
- "typescript-eslint": "8.53.0"
57
+ "typescript-eslint": "8.54.0"
51
58
  },
52
59
  "overrides": {
53
60
  "js-yaml": "4.1.1"