launch-unity 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/launch.d.ts +1 -28
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +22 -517
- package/dist/lib.d.ts +36 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +170 -8
- package/dist/unityHub.d.ts +1 -0
- package/dist/unityHub.d.ts.map +1 -1
- package/dist/unityHub.js +21 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Exports core functions for programmatic usage.
|
|
4
4
|
* Uses lib.ts which has no CLI side effects.
|
|
5
5
|
*/
|
|
6
|
-
export { LaunchOptions, LaunchResolvedOptions, UnityProcessInfo, parseArgs, findUnityProjectBfs, getUnityVersion, launch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, } from './lib.js';
|
|
6
|
+
export { LaunchOptions, LaunchResolvedOptions, UnityProcessInfo, OrchestrateOptions, OrchestrateResult, parseArgs, findUnityProjectBfs, getUnityVersion, launch, orchestrateLaunch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, quitRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs, } from './lib.js';
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAEL,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EAEjB,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,MAAM,EACN,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,2BAA2B,EAC3B,0BAA0B,EAC1B,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export {
|
|
7
7
|
// Functions
|
|
8
|
-
parseArgs, findUnityProjectBfs, getUnityVersion, launch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, } from './lib.js';
|
|
8
|
+
parseArgs, findUnityProjectBfs, getUnityVersion, launch, orchestrateLaunch, findRunningUnityProcess, focusUnityProcess, killRunningUnity, quitRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs, } from './lib.js';
|
package/dist/launch.d.ts
CHANGED
|
@@ -1,30 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export
|
|
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>;
|
|
2
|
+
export {};
|
|
30
3
|
//# sourceMappingURL=launch.d.ts.map
|
package/dist/launch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":""}
|
package/dist/launch.js
CHANGED
|
@@ -3,22 +3,11 @@
|
|
|
3
3
|
launch-unity: Open a Unity project with the matching Editor version.
|
|
4
4
|
Platforms: macOS, Windows
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { rm } from "node:fs/promises";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
9
8
|
import { dirname, join, resolve } from "node:path";
|
|
10
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
-
import {
|
|
12
|
-
import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs } from "./unityHub.js";
|
|
13
|
-
const execFileAsync = promisify(execFile);
|
|
14
|
-
const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
|
|
15
|
-
const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
|
|
16
|
-
const PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
|
|
17
|
-
const PROCESS_LIST_COMMAND_MAC = "ps";
|
|
18
|
-
const PROCESS_LIST_ARGS_MAC = ["-axo", "pid=,command=", "-ww"];
|
|
19
|
-
const WINDOWS_POWERSHELL = "powershell";
|
|
20
|
-
const UNITY_LOCKFILE_NAME = "UnityLockfile";
|
|
21
|
-
const TEMP_DIRECTORY_NAME = "Temp";
|
|
10
|
+
import { orchestrateLaunch } from "./lib.js";
|
|
22
11
|
const npmExecutableName = () => {
|
|
23
12
|
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
24
13
|
};
|
|
@@ -130,7 +119,7 @@ const compareSemverTriplet = (left, right) => {
|
|
|
130
119
|
}
|
|
131
120
|
return 0;
|
|
132
121
|
};
|
|
133
|
-
|
|
122
|
+
function parseArgs(argv) {
|
|
134
123
|
const args = argv.slice(2);
|
|
135
124
|
const doubleDashIndex = args.indexOf("--");
|
|
136
125
|
let cliArgs = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
|
|
@@ -144,6 +133,7 @@ export function parseArgs(argv) {
|
|
|
144
133
|
const positionals = [];
|
|
145
134
|
let maxDepth = 3; // default 3; -1 means unlimited
|
|
146
135
|
let restart = false;
|
|
136
|
+
let quit = false;
|
|
147
137
|
let addUnityHub = false;
|
|
148
138
|
let favoriteUnityHub = false;
|
|
149
139
|
let platform;
|
|
@@ -161,6 +151,10 @@ export function parseArgs(argv) {
|
|
|
161
151
|
restart = true;
|
|
162
152
|
continue;
|
|
163
153
|
}
|
|
154
|
+
if (arg === "-q" || arg === "--quit") {
|
|
155
|
+
quit = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
164
158
|
if (arg === "-u" ||
|
|
165
159
|
arg === "-a" ||
|
|
166
160
|
arg === "--unity-hub-entry" ||
|
|
@@ -226,6 +220,7 @@ export function parseArgs(argv) {
|
|
|
226
220
|
unityArgs,
|
|
227
221
|
searchMaxDepth: maxDepth,
|
|
228
222
|
restart,
|
|
223
|
+
quit,
|
|
229
224
|
addUnityHub,
|
|
230
225
|
favoriteUnityHub,
|
|
231
226
|
};
|
|
@@ -266,6 +261,7 @@ Options:
|
|
|
266
261
|
-h, --help Show this help message
|
|
267
262
|
-v, --version Show version number
|
|
268
263
|
-r, --restart Kill running Unity and restart
|
|
264
|
+
-q, --quit Quit running Unity gracefully (force-kill on timeout)
|
|
269
265
|
-p, --platform <P> Passed to Unity as -buildTarget (e.g., StandaloneOSX, Android, iOS)
|
|
270
266
|
--max-depth <N> Search depth when PROJECT_PATH is omitted (default 3, -1 unlimited)
|
|
271
267
|
-u, -a, --unity-hub-entry, --add-unity-hub
|
|
@@ -277,516 +273,25 @@ Commands:
|
|
|
277
273
|
`;
|
|
278
274
|
process.stdout.write(help);
|
|
279
275
|
}
|
|
280
|
-
export function getUnityVersion(projectPath) {
|
|
281
|
-
const versionFile = join(projectPath, "ProjectSettings", "ProjectVersion.txt");
|
|
282
|
-
if (!existsSync(versionFile)) {
|
|
283
|
-
console.error(`Error: ProjectVersion.txt not found at ${versionFile}`);
|
|
284
|
-
console.error("This does not appear to be a Unity project.");
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
const content = readFileSync(versionFile, "utf8");
|
|
288
|
-
const version = content.match(/m_EditorVersion:\s*([^\s\n]+)/)?.[1];
|
|
289
|
-
if (!version) {
|
|
290
|
-
console.error(`Error: Could not extract Unity version from ${versionFile}`);
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
return version;
|
|
294
|
-
}
|
|
295
|
-
function getUnityPathWindows(version) {
|
|
296
|
-
const candidates = [];
|
|
297
|
-
const programFiles = process.env["PROGRAMFILES"];
|
|
298
|
-
const programFilesX86 = process.env["PROGRAMFILES(X86)"];
|
|
299
|
-
const localAppData = process.env["LOCALAPPDATA"];
|
|
300
|
-
const addCandidate = (base) => {
|
|
301
|
-
if (!base) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
candidates.push(join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
305
|
-
};
|
|
306
|
-
addCandidate(programFiles);
|
|
307
|
-
addCandidate(programFilesX86);
|
|
308
|
-
addCandidate(localAppData);
|
|
309
|
-
candidates.push(join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
310
|
-
for (const candidate of candidates) {
|
|
311
|
-
if (existsSync(candidate)) {
|
|
312
|
-
return candidate;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return candidates[0] ?? join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe");
|
|
316
|
-
}
|
|
317
|
-
function getUnityPath(version) {
|
|
318
|
-
if (process.platform === "darwin") {
|
|
319
|
-
return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
|
|
320
|
-
}
|
|
321
|
-
if (process.platform === "win32") {
|
|
322
|
-
return getUnityPathWindows(version);
|
|
323
|
-
}
|
|
324
|
-
return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
|
|
325
|
-
}
|
|
326
|
-
function ensureProjectPath(projectPath) {
|
|
327
|
-
if (!existsSync(projectPath)) {
|
|
328
|
-
console.error(`Error: Project directory not found: ${projectPath}`);
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const removeTrailingSeparators = (target) => {
|
|
333
|
-
let trimmed = target;
|
|
334
|
-
while (trimmed.length > 1 && (trimmed.endsWith("/") || trimmed.endsWith("\\"))) {
|
|
335
|
-
trimmed = trimmed.slice(0, -1);
|
|
336
|
-
}
|
|
337
|
-
return trimmed;
|
|
338
|
-
};
|
|
339
|
-
const normalizePath = (target) => {
|
|
340
|
-
const resolvedPath = resolve(target);
|
|
341
|
-
const trimmed = removeTrailingSeparators(resolvedPath);
|
|
342
|
-
return trimmed;
|
|
343
|
-
};
|
|
344
|
-
const toComparablePath = (value) => {
|
|
345
|
-
return value.replace(/\\/g, "/").toLocaleLowerCase();
|
|
346
|
-
};
|
|
347
|
-
const pathsEqual = (left, right) => {
|
|
348
|
-
return toComparablePath(normalizePath(left)) === toComparablePath(normalizePath(right));
|
|
349
|
-
};
|
|
350
|
-
function extractProjectPath(command) {
|
|
351
|
-
const match = command.match(PROJECT_PATH_PATTERN);
|
|
352
|
-
if (!match) {
|
|
353
|
-
return undefined;
|
|
354
|
-
}
|
|
355
|
-
const raw = match[1];
|
|
356
|
-
if (!raw) {
|
|
357
|
-
return undefined;
|
|
358
|
-
}
|
|
359
|
-
const trimmed = raw.trim();
|
|
360
|
-
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
361
|
-
return trimmed.slice(1, -1);
|
|
362
|
-
}
|
|
363
|
-
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
364
|
-
return trimmed.slice(1, -1);
|
|
365
|
-
}
|
|
366
|
-
return trimmed;
|
|
367
|
-
}
|
|
368
|
-
const isUnityAuxiliaryProcess = (command) => {
|
|
369
|
-
const normalizedCommand = command.toLowerCase();
|
|
370
|
-
if (normalizedCommand.includes("-batchmode")) {
|
|
371
|
-
return true;
|
|
372
|
-
}
|
|
373
|
-
return normalizedCommand.includes("assetimportworker");
|
|
374
|
-
};
|
|
375
|
-
async function listUnityProcessesMac() {
|
|
376
|
-
let stdout = "";
|
|
377
|
-
try {
|
|
378
|
-
const result = await execFileAsync(PROCESS_LIST_COMMAND_MAC, PROCESS_LIST_ARGS_MAC);
|
|
379
|
-
stdout = result.stdout;
|
|
380
|
-
}
|
|
381
|
-
catch (error) {
|
|
382
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
383
|
-
console.error(`Failed to retrieve Unity process list: ${message}`);
|
|
384
|
-
return [];
|
|
385
|
-
}
|
|
386
|
-
const lines = stdout
|
|
387
|
-
.split("\n")
|
|
388
|
-
.map((line) => line.trim())
|
|
389
|
-
.filter((line) => line.length > 0);
|
|
390
|
-
const processes = [];
|
|
391
|
-
for (const line of lines) {
|
|
392
|
-
const match = line.match(/^(\d+)\s+(.*)$/);
|
|
393
|
-
if (!match) {
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
const pidValue = Number.parseInt(match[1] ?? "", 10);
|
|
397
|
-
if (!Number.isFinite(pidValue)) {
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const command = match[2] ?? "";
|
|
401
|
-
if (!UNITY_EXECUTABLE_PATTERN_MAC.test(command)) {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (isUnityAuxiliaryProcess(command)) {
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const projectArgument = extractProjectPath(command);
|
|
408
|
-
if (!projectArgument) {
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
processes.push({
|
|
412
|
-
pid: pidValue,
|
|
413
|
-
projectPath: normalizePath(projectArgument),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
return processes;
|
|
417
|
-
}
|
|
418
|
-
async function listUnityProcessesWindows() {
|
|
419
|
-
const scriptLines = [
|
|
420
|
-
"$ErrorActionPreference = 'Stop'",
|
|
421
|
-
"$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }",
|
|
422
|
-
"foreach ($process in $processes) {",
|
|
423
|
-
" $commandLine = $process.CommandLine -replace \"`r\", ' ' -replace \"`n\", ' '",
|
|
424
|
-
" Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)",
|
|
425
|
-
"}",
|
|
426
|
-
];
|
|
427
|
-
let stdout = "";
|
|
428
|
-
try {
|
|
429
|
-
const result = await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
430
|
-
stdout = result.stdout ?? "";
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
434
|
-
console.error(`Failed to retrieve Unity process list on Windows: ${message}`);
|
|
435
|
-
return [];
|
|
436
|
-
}
|
|
437
|
-
const lines = stdout
|
|
438
|
-
.split("\n")
|
|
439
|
-
.map((line) => line.trim())
|
|
440
|
-
.filter((line) => line.length > 0);
|
|
441
|
-
const processes = [];
|
|
442
|
-
for (const line of lines) {
|
|
443
|
-
const delimiterIndex = line.indexOf("|");
|
|
444
|
-
if (delimiterIndex < 0) {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
const pidText = line.slice(0, delimiterIndex).trim();
|
|
448
|
-
const command = line.slice(delimiterIndex + 1).trim();
|
|
449
|
-
const pidValue = Number.parseInt(pidText, 10);
|
|
450
|
-
if (!Number.isFinite(pidValue)) {
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (!UNITY_EXECUTABLE_PATTERN_WINDOWS.test(command)) {
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
if (isUnityAuxiliaryProcess(command)) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
const projectArgument = extractProjectPath(command);
|
|
460
|
-
if (!projectArgument) {
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
processes.push({
|
|
464
|
-
pid: pidValue,
|
|
465
|
-
projectPath: normalizePath(projectArgument),
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
return processes;
|
|
469
|
-
}
|
|
470
|
-
async function listUnityProcesses() {
|
|
471
|
-
if (process.platform === "darwin") {
|
|
472
|
-
return await listUnityProcessesMac();
|
|
473
|
-
}
|
|
474
|
-
if (process.platform === "win32") {
|
|
475
|
-
return await listUnityProcessesWindows();
|
|
476
|
-
}
|
|
477
|
-
return [];
|
|
478
|
-
}
|
|
479
|
-
export async function findRunningUnityProcess(projectPath) {
|
|
480
|
-
const normalizedTarget = normalizePath(projectPath);
|
|
481
|
-
const processes = await listUnityProcesses();
|
|
482
|
-
return processes.find((candidate) => pathsEqual(candidate.projectPath, normalizedTarget));
|
|
483
|
-
}
|
|
484
|
-
export async function focusUnityProcess(pid) {
|
|
485
|
-
if (process.platform === "darwin") {
|
|
486
|
-
await focusUnityProcessMac(pid);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
if (process.platform === "win32") {
|
|
490
|
-
await focusUnityProcessWindows(pid);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
async function focusUnityProcessMac(pid) {
|
|
494
|
-
const script = `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
|
|
495
|
-
try {
|
|
496
|
-
await execFileAsync("osascript", ["-e", script]);
|
|
497
|
-
console.log("Brought existing Unity to the front.");
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
501
|
-
console.warn(`Failed to bring Unity to front: ${message}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
async function focusUnityProcessWindows(pid) {
|
|
505
|
-
const addTypeLines = [
|
|
506
|
-
"Add-Type -TypeDefinition @\"",
|
|
507
|
-
"using System;",
|
|
508
|
-
"using System.Runtime.InteropServices;",
|
|
509
|
-
"public static class Win32Interop {",
|
|
510
|
-
" [DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);",
|
|
511
|
-
" [DllImport(\"user32.dll\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);",
|
|
512
|
-
"}",
|
|
513
|
-
"\"@",
|
|
514
|
-
];
|
|
515
|
-
const scriptLines = [
|
|
516
|
-
"$ErrorActionPreference = 'Stop'",
|
|
517
|
-
...addTypeLines,
|
|
518
|
-
`try { $process = Get-Process -Id ${pid} -ErrorAction Stop } catch { return }`,
|
|
519
|
-
"$handle = $process.MainWindowHandle",
|
|
520
|
-
"if ($handle -eq 0) { return }",
|
|
521
|
-
"[Win32Interop]::ShowWindowAsync($handle, 9) | Out-Null",
|
|
522
|
-
"[Win32Interop]::SetForegroundWindow($handle) | Out-Null",
|
|
523
|
-
];
|
|
524
|
-
try {
|
|
525
|
-
await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
526
|
-
console.log("Brought existing Unity to the front.");
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
530
|
-
console.warn(`Failed to bring Unity to front on Windows: ${message}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
export async function handleStaleLockfile(projectPath) {
|
|
534
|
-
const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
|
|
535
|
-
const lockfilePath = join(tempDirectoryPath, UNITY_LOCKFILE_NAME);
|
|
536
|
-
if (!existsSync(lockfilePath)) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
console.log(`UnityLockfile found without active Unity process: ${lockfilePath}`);
|
|
540
|
-
console.log("Assuming previous crash. Cleaning Temp directory and continuing launch.");
|
|
541
|
-
try {
|
|
542
|
-
await rm(tempDirectoryPath, { recursive: true, force: true });
|
|
543
|
-
console.log("Deleted Temp directory.");
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
547
|
-
console.warn(`Failed to delete Temp directory: ${message}`);
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
await rm(lockfilePath, { force: true });
|
|
551
|
-
console.log("Deleted UnityLockfile.");
|
|
552
|
-
}
|
|
553
|
-
catch (error) {
|
|
554
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
555
|
-
console.warn(`Failed to delete UnityLockfile: ${message}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const KILL_POLL_INTERVAL_MS = 100;
|
|
559
|
-
const KILL_TIMEOUT_MS = 10000;
|
|
560
|
-
function isProcessAlive(pid) {
|
|
561
|
-
try {
|
|
562
|
-
process.kill(pid, 0);
|
|
563
|
-
return true;
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
function killProcess(pid) {
|
|
570
|
-
try {
|
|
571
|
-
process.kill(pid, "SIGKILL");
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
// Process already exited
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
async function waitForProcessExit(pid) {
|
|
578
|
-
const start = Date.now();
|
|
579
|
-
while (Date.now() - start < KILL_TIMEOUT_MS) {
|
|
580
|
-
if (!isProcessAlive(pid)) {
|
|
581
|
-
return true;
|
|
582
|
-
}
|
|
583
|
-
await new Promise((resolve) => setTimeout(resolve, KILL_POLL_INTERVAL_MS));
|
|
584
|
-
}
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
export async function killRunningUnity(projectPath) {
|
|
588
|
-
const processInfo = await findRunningUnityProcess(projectPath);
|
|
589
|
-
if (!processInfo) {
|
|
590
|
-
console.log("No running Unity process found for this project.");
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
const pid = processInfo.pid;
|
|
594
|
-
console.log(`Killing Unity (PID: ${pid})...`);
|
|
595
|
-
killProcess(pid);
|
|
596
|
-
const exited = await waitForProcessExit(pid);
|
|
597
|
-
if (!exited) {
|
|
598
|
-
console.error(`Error: Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
|
|
599
|
-
process.exit(1);
|
|
600
|
-
}
|
|
601
|
-
console.log("Unity killed.");
|
|
602
|
-
}
|
|
603
|
-
function hasBuildTargetArg(unityArgs) {
|
|
604
|
-
for (const arg of unityArgs) {
|
|
605
|
-
if (arg === "-buildTarget") {
|
|
606
|
-
return true;
|
|
607
|
-
}
|
|
608
|
-
if (arg.startsWith("-buildTarget=")) {
|
|
609
|
-
return true;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
|
-
const EXCLUDED_DIR_NAMES = new Set([
|
|
615
|
-
"library",
|
|
616
|
-
"temp",
|
|
617
|
-
"logs",
|
|
618
|
-
"obj",
|
|
619
|
-
".git",
|
|
620
|
-
"node_modules",
|
|
621
|
-
".idea",
|
|
622
|
-
".vscode",
|
|
623
|
-
".vs",
|
|
624
|
-
]);
|
|
625
|
-
function isUnityProjectRoot(candidateDir) {
|
|
626
|
-
const versionFile = join(candidateDir, "ProjectSettings", "ProjectVersion.txt");
|
|
627
|
-
return existsSync(versionFile);
|
|
628
|
-
}
|
|
629
|
-
function listSubdirectoriesSorted(dir) {
|
|
630
|
-
let entries = [];
|
|
631
|
-
try {
|
|
632
|
-
const dirents = readdirSync(dir, { withFileTypes: true });
|
|
633
|
-
const subdirs = dirents
|
|
634
|
-
.filter((d) => d.isDirectory())
|
|
635
|
-
.map((d) => d.name)
|
|
636
|
-
.filter((name) => !EXCLUDED_DIR_NAMES.has(name.toLocaleLowerCase()));
|
|
637
|
-
subdirs.sort((a, b) => a.localeCompare(b));
|
|
638
|
-
entries = subdirs.map((name) => join(dir, name));
|
|
639
|
-
}
|
|
640
|
-
catch {
|
|
641
|
-
// Ignore directories we cannot read
|
|
642
|
-
entries = [];
|
|
643
|
-
}
|
|
644
|
-
return entries;
|
|
645
|
-
}
|
|
646
|
-
export function findUnityProjectBfs(rootDir, maxDepth) {
|
|
647
|
-
const queue = [];
|
|
648
|
-
let rootCanonical;
|
|
649
|
-
try {
|
|
650
|
-
rootCanonical = realpathSync(rootDir);
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
rootCanonical = rootDir;
|
|
654
|
-
}
|
|
655
|
-
queue.push({ dir: rootCanonical, depth: 0 });
|
|
656
|
-
const visited = new Set([toComparablePath(normalizePath(rootCanonical))]);
|
|
657
|
-
while (queue.length > 0) {
|
|
658
|
-
const current = queue.shift();
|
|
659
|
-
if (!current) {
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
const { dir, depth } = current;
|
|
663
|
-
if (isUnityProjectRoot(dir)) {
|
|
664
|
-
return normalizePath(dir);
|
|
665
|
-
}
|
|
666
|
-
const canDescend = maxDepth === -1 || depth < maxDepth;
|
|
667
|
-
if (!canDescend) {
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
const children = listSubdirectoriesSorted(dir);
|
|
671
|
-
for (const child of children) {
|
|
672
|
-
let childCanonical = child;
|
|
673
|
-
try {
|
|
674
|
-
const stat = lstatSync(child);
|
|
675
|
-
if (stat.isSymbolicLink()) {
|
|
676
|
-
try {
|
|
677
|
-
childCanonical = realpathSync(child);
|
|
678
|
-
}
|
|
679
|
-
catch {
|
|
680
|
-
// Broken symlink: skip
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
catch {
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
const key = toComparablePath(normalizePath(childCanonical));
|
|
689
|
-
if (visited.has(key)) {
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
visited.add(key);
|
|
693
|
-
queue.push({ dir: childCanonical, depth: depth + 1 });
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return undefined;
|
|
697
|
-
}
|
|
698
|
-
export async function launch(opts) {
|
|
699
|
-
const { projectPath, platform, unityArgs, unityVersion } = opts;
|
|
700
|
-
const unityPath = getUnityPath(unityVersion);
|
|
701
|
-
console.log(`Detected Unity version: ${unityVersion}`);
|
|
702
|
-
if (!existsSync(unityPath)) {
|
|
703
|
-
console.error(`Error: Unity ${unityVersion} not found at ${unityPath}`);
|
|
704
|
-
console.error("Please install Unity through Unity Hub.");
|
|
705
|
-
process.exit(1);
|
|
706
|
-
}
|
|
707
|
-
console.log("Opening Unity...");
|
|
708
|
-
console.log(`Project Path: ${projectPath}`);
|
|
709
|
-
const args = ["-projectPath", projectPath];
|
|
710
|
-
const unityArgsContainBuildTarget = hasBuildTargetArg(unityArgs);
|
|
711
|
-
if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
|
|
712
|
-
args.push("-buildTarget", platform);
|
|
713
|
-
}
|
|
714
|
-
const hubCliArgs = await getProjectCliArgs(projectPath);
|
|
715
|
-
if (hubCliArgs.length > 0) {
|
|
716
|
-
args.push(...hubCliArgs);
|
|
717
|
-
}
|
|
718
|
-
if (unityArgs.length > 0) {
|
|
719
|
-
args.push(...unityArgs);
|
|
720
|
-
}
|
|
721
|
-
const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
|
|
722
|
-
child.unref();
|
|
723
|
-
}
|
|
724
276
|
async function main() {
|
|
725
277
|
const options = parseArgs(process.argv);
|
|
726
278
|
if (options.subcommand === "update") {
|
|
727
279
|
await runSelfUpdate();
|
|
728
280
|
return;
|
|
729
281
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
console.log(`No PROJECT_PATH provided. Searching under ${searchRoot} (max-depth: ${depthInfo})...`);
|
|
735
|
-
const found = findUnityProjectBfs(searchRoot, options.searchMaxDepth);
|
|
736
|
-
if (!found) {
|
|
737
|
-
console.error(`Error: Unity project not found under ${searchRoot}.`);
|
|
738
|
-
process.exit(1);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
console.log(`Selected project: ${found}`);
|
|
742
|
-
resolvedProjectPath = found;
|
|
743
|
-
}
|
|
744
|
-
ensureProjectPath(resolvedProjectPath);
|
|
745
|
-
const unityVersion = getUnityVersion(resolvedProjectPath);
|
|
746
|
-
// Unity Hub only mode: -a or -f flags skip launching Unity
|
|
747
|
-
const unityHubOnlyMode = options.addUnityHub || options.favoriteUnityHub;
|
|
748
|
-
if (unityHubOnlyMode) {
|
|
749
|
-
console.log(`Detected Unity version: ${unityVersion}`);
|
|
750
|
-
console.log(`Project Path: ${resolvedProjectPath}`);
|
|
751
|
-
const now = new Date();
|
|
752
|
-
try {
|
|
753
|
-
await ensureProjectEntryAndUpdate(resolvedProjectPath, unityVersion, now, options.favoriteUnityHub);
|
|
754
|
-
console.log("Unity Hub entry updated.");
|
|
755
|
-
}
|
|
756
|
-
catch (error) {
|
|
757
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
758
|
-
console.warn(`Failed to update Unity Hub: ${message}`);
|
|
759
|
-
}
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
if (options.restart) {
|
|
763
|
-
await killRunningUnity(resolvedProjectPath);
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
const runningProcess = await findRunningUnityProcess(resolvedProjectPath);
|
|
767
|
-
if (runningProcess) {
|
|
768
|
-
console.log(`Unity process already running for project: ${resolvedProjectPath} (PID: ${runningProcess.pid})`);
|
|
769
|
-
await focusUnityProcess(runningProcess.pid);
|
|
770
|
-
process.exit(0);
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
await handleStaleLockfile(resolvedProjectPath);
|
|
775
|
-
const resolved = {
|
|
776
|
-
projectPath: resolvedProjectPath,
|
|
282
|
+
const result = await orchestrateLaunch({
|
|
283
|
+
projectPath: options.projectPath,
|
|
284
|
+
searchRoot: process.cwd(),
|
|
285
|
+
searchMaxDepth: options.searchMaxDepth,
|
|
777
286
|
platform: options.platform,
|
|
778
287
|
unityArgs: options.unityArgs,
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
787
|
-
catch (error) {
|
|
788
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
789
|
-
console.warn(`Failed to update Unity Hub lastModified: ${message}`);
|
|
288
|
+
restart: options.restart,
|
|
289
|
+
quit: options.quit,
|
|
290
|
+
addUnityHub: options.addUnityHub,
|
|
291
|
+
favoriteUnityHub: options.favoriteUnityHub,
|
|
292
|
+
});
|
|
293
|
+
if (result.action === "focused") {
|
|
294
|
+
process.exit(0);
|
|
790
295
|
}
|
|
791
296
|
}
|
|
792
297
|
async function runSelfUpdate() {
|
package/dist/lib.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* launch-unity core library functions.
|
|
3
3
|
* Pure library code without CLI entry point or side effects.
|
|
4
4
|
*/
|
|
5
|
-
import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs } from "./unityHub.js";
|
|
5
|
+
import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs } from "./unityHub.js";
|
|
6
6
|
export type LaunchOptions = {
|
|
7
7
|
subcommand?: "update";
|
|
8
8
|
projectPath?: string;
|
|
@@ -10,6 +10,7 @@ export type LaunchOptions = {
|
|
|
10
10
|
unityArgs: string[];
|
|
11
11
|
searchMaxDepth: number;
|
|
12
12
|
restart: boolean;
|
|
13
|
+
quit: boolean;
|
|
13
14
|
addUnityHub: boolean;
|
|
14
15
|
favoriteUnityHub: boolean;
|
|
15
16
|
};
|
|
@@ -29,7 +30,40 @@ export declare function findRunningUnityProcess(projectPath: string): Promise<Un
|
|
|
29
30
|
export declare function focusUnityProcess(pid: number): Promise<void>;
|
|
30
31
|
export declare function handleStaleLockfile(projectPath: string): Promise<void>;
|
|
31
32
|
export declare function killRunningUnity(projectPath: string): Promise<void>;
|
|
33
|
+
export declare function quitRunningUnity(projectPath: string): Promise<void>;
|
|
32
34
|
export declare function findUnityProjectBfs(rootDir: string, maxDepth: number): string | undefined;
|
|
33
35
|
export declare function launch(opts: LaunchResolvedOptions): Promise<void>;
|
|
34
|
-
export
|
|
36
|
+
export type OrchestrateOptions = {
|
|
37
|
+
projectPath?: string | undefined;
|
|
38
|
+
searchRoot: string;
|
|
39
|
+
searchMaxDepth: number;
|
|
40
|
+
platform?: string | undefined;
|
|
41
|
+
unityArgs: string[];
|
|
42
|
+
restart: boolean;
|
|
43
|
+
quit: boolean;
|
|
44
|
+
addUnityHub: boolean;
|
|
45
|
+
favoriteUnityHub: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type OrchestrateResult = {
|
|
48
|
+
action: "launched";
|
|
49
|
+
projectPath: string;
|
|
50
|
+
unityVersion: string;
|
|
51
|
+
} | {
|
|
52
|
+
action: "focused";
|
|
53
|
+
projectPath: string;
|
|
54
|
+
pid: number;
|
|
55
|
+
} | {
|
|
56
|
+
action: "quit";
|
|
57
|
+
projectPath: string;
|
|
58
|
+
} | {
|
|
59
|
+
action: "killed-and-launched";
|
|
60
|
+
projectPath: string;
|
|
61
|
+
unityVersion: string;
|
|
62
|
+
} | {
|
|
63
|
+
action: "hub-updated";
|
|
64
|
+
projectPath: string;
|
|
65
|
+
unityVersion: string;
|
|
66
|
+
};
|
|
67
|
+
export declare function orchestrateLaunch(options: OrchestrateOptions): Promise<OrchestrateResult>;
|
|
68
|
+
export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs };
|
|
35
69
|
//# sourceMappingURL=lib.d.ts.map
|
package/dist/lib.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"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"}
|
package/dist/lib.js
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "
|
|
|
7
7
|
import { rm } from "node:fs/promises";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
|
-
import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs } from "./unityHub.js";
|
|
10
|
+
import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs } from "./unityHub.js";
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
|
|
13
13
|
const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
|
|
@@ -31,6 +31,7 @@ export function parseArgs(argv) {
|
|
|
31
31
|
const positionals = [];
|
|
32
32
|
let maxDepth = 3; // default 3; -1 means unlimited
|
|
33
33
|
let restart = false;
|
|
34
|
+
let quit = false;
|
|
34
35
|
let addUnityHub = false;
|
|
35
36
|
let favoriteUnityHub = false;
|
|
36
37
|
let platform;
|
|
@@ -46,6 +47,10 @@ export function parseArgs(argv) {
|
|
|
46
47
|
restart = true;
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
50
|
+
if (arg === "-q" || arg === "--quit") {
|
|
51
|
+
quit = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
49
54
|
if (arg === "-u" ||
|
|
50
55
|
arg === "-a" ||
|
|
51
56
|
arg === "--unity-hub-entry" ||
|
|
@@ -111,6 +116,7 @@ export function parseArgs(argv) {
|
|
|
111
116
|
unityArgs,
|
|
112
117
|
searchMaxDepth: maxDepth,
|
|
113
118
|
restart,
|
|
119
|
+
quit,
|
|
114
120
|
addUnityHub,
|
|
115
121
|
favoriteUnityHub,
|
|
116
122
|
};
|
|
@@ -393,9 +399,11 @@ export async function handleStaleLockfile(projectPath) {
|
|
|
393
399
|
const message = error instanceof Error ? error.message : String(error);
|
|
394
400
|
console.warn(`Failed to delete UnityLockfile: ${message}`);
|
|
395
401
|
}
|
|
402
|
+
console.log();
|
|
396
403
|
}
|
|
397
404
|
const KILL_POLL_INTERVAL_MS = 100;
|
|
398
405
|
const KILL_TIMEOUT_MS = 10000;
|
|
406
|
+
const GRACEFUL_QUIT_TIMEOUT_MS = 10000;
|
|
399
407
|
function isProcessAlive(pid) {
|
|
400
408
|
try {
|
|
401
409
|
process.kill(pid, 0);
|
|
@@ -413,9 +421,9 @@ function killProcess(pid) {
|
|
|
413
421
|
// Process already exited
|
|
414
422
|
}
|
|
415
423
|
}
|
|
416
|
-
async function waitForProcessExit(pid) {
|
|
424
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
417
425
|
const start = Date.now();
|
|
418
|
-
while (Date.now() - start <
|
|
426
|
+
while (Date.now() - start < timeoutMs) {
|
|
419
427
|
if (!isProcessAlive(pid)) {
|
|
420
428
|
return true;
|
|
421
429
|
}
|
|
@@ -427,16 +435,80 @@ export async function killRunningUnity(projectPath) {
|
|
|
427
435
|
const processInfo = await findRunningUnityProcess(projectPath);
|
|
428
436
|
if (!processInfo) {
|
|
429
437
|
console.log("No running Unity process found for this project.");
|
|
438
|
+
console.log();
|
|
430
439
|
return;
|
|
431
440
|
}
|
|
432
441
|
const pid = processInfo.pid;
|
|
433
442
|
console.log(`Killing Unity (PID: ${pid})...`);
|
|
434
443
|
killProcess(pid);
|
|
435
|
-
const exited = await waitForProcessExit(pid);
|
|
444
|
+
const exited = await waitForProcessExit(pid, KILL_TIMEOUT_MS);
|
|
436
445
|
if (!exited) {
|
|
437
446
|
throw new Error(`Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
|
|
438
447
|
}
|
|
439
448
|
console.log("Unity killed.");
|
|
449
|
+
console.log();
|
|
450
|
+
}
|
|
451
|
+
async function sendGracefulQuitMac(pid) {
|
|
452
|
+
// System Events quit and "tell application to quit" leave UnityLockfile behind,
|
|
453
|
+
// so we send Cmd+Q keystroke to trigger Unity's normal user-initiated shutdown flow
|
|
454
|
+
const script = [
|
|
455
|
+
'tell application "System Events"',
|
|
456
|
+
` set frontmost of (first process whose unix id is ${pid}) to true`,
|
|
457
|
+
' keystroke "q" using {command down}',
|
|
458
|
+
"end tell",
|
|
459
|
+
].join("\n");
|
|
460
|
+
try {
|
|
461
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// Process may have already exited
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function sendGracefulQuitWindows(pid) {
|
|
468
|
+
// process.kill(pid, "SIGTERM") forcefully kills on Windows, so use CloseMainWindow() to send WM_CLOSE
|
|
469
|
+
const scriptLines = [
|
|
470
|
+
"$ErrorActionPreference = 'Stop'",
|
|
471
|
+
`try { $proc = Get-Process -Id ${pid} -ErrorAction Stop; $proc.CloseMainWindow() | Out-Null } catch { }`,
|
|
472
|
+
];
|
|
473
|
+
try {
|
|
474
|
+
await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Process may have already exited
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function sendGracefulQuit(pid) {
|
|
481
|
+
if (process.platform === "darwin") {
|
|
482
|
+
await sendGracefulQuitMac(pid);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (process.platform === "win32") {
|
|
486
|
+
await sendGracefulQuitWindows(pid);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
export async function quitRunningUnity(projectPath) {
|
|
491
|
+
const processInfo = await findRunningUnityProcess(projectPath);
|
|
492
|
+
if (!processInfo) {
|
|
493
|
+
console.log("No running Unity process found for this project.");
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const pid = processInfo.pid;
|
|
497
|
+
console.log(`Quitting Unity (PID: ${pid})...`);
|
|
498
|
+
await sendGracefulQuit(pid);
|
|
499
|
+
console.log(`Sent graceful quit signal. Waiting up to ${GRACEFUL_QUIT_TIMEOUT_MS / 1000}s...`);
|
|
500
|
+
const exitedGracefully = await waitForProcessExit(pid, GRACEFUL_QUIT_TIMEOUT_MS);
|
|
501
|
+
if (exitedGracefully) {
|
|
502
|
+
console.log("Unity quit gracefully.");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
console.log("Unity did not respond to graceful quit. Force killing...");
|
|
506
|
+
killProcess(pid);
|
|
507
|
+
const exitedAfterKill = await waitForProcessExit(pid, KILL_TIMEOUT_MS);
|
|
508
|
+
if (!exitedAfterKill) {
|
|
509
|
+
throw new Error(`Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
|
|
510
|
+
}
|
|
511
|
+
console.log("Unity force killed.");
|
|
440
512
|
}
|
|
441
513
|
function hasBuildTargetArg(unityArgs) {
|
|
442
514
|
for (const arg of unityArgs) {
|
|
@@ -536,12 +608,12 @@ export function findUnityProjectBfs(rootDir, maxDepth) {
|
|
|
536
608
|
export async function launch(opts) {
|
|
537
609
|
const { projectPath, platform, unityArgs, unityVersion } = opts;
|
|
538
610
|
const unityPath = getUnityPath(unityVersion);
|
|
539
|
-
console.log(`Detected Unity version: ${unityVersion}`);
|
|
540
611
|
if (!existsSync(unityPath)) {
|
|
541
612
|
throw new Error(`Unity ${unityVersion} not found at ${unityPath}. Please install Unity through Unity Hub.`);
|
|
542
613
|
}
|
|
543
614
|
console.log("Opening Unity...");
|
|
544
615
|
console.log(`Project Path: ${projectPath}`);
|
|
616
|
+
console.log(`Detected Unity version: ${unityVersion}`);
|
|
545
617
|
const args = ["-projectPath", projectPath];
|
|
546
618
|
const unityArgsContainBuildTarget = hasBuildTargetArg(unityArgs);
|
|
547
619
|
if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
|
|
@@ -549,13 +621,103 @@ export async function launch(opts) {
|
|
|
549
621
|
}
|
|
550
622
|
const hubCliArgs = await getProjectCliArgs(projectPath);
|
|
551
623
|
if (hubCliArgs.length > 0) {
|
|
624
|
+
console.log("Unity Hub launch options:");
|
|
625
|
+
for (const line of groupCliArgs(hubCliArgs)) {
|
|
626
|
+
console.log(` ${line}`);
|
|
627
|
+
}
|
|
552
628
|
args.push(...hubCliArgs);
|
|
553
629
|
}
|
|
630
|
+
else {
|
|
631
|
+
console.log("Unity Hub launch options: none");
|
|
632
|
+
}
|
|
554
633
|
if (unityArgs.length > 0) {
|
|
555
634
|
args.push(...unityArgs);
|
|
556
635
|
}
|
|
557
|
-
|
|
558
|
-
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
const child = spawn(unityPath, args, {
|
|
638
|
+
stdio: "ignore",
|
|
639
|
+
detached: true,
|
|
640
|
+
// Git Bash (MSYS) がWindows パスをUnix 形式に自動変換するのを防ぐ
|
|
641
|
+
env: {
|
|
642
|
+
...process.env,
|
|
643
|
+
MSYS_NO_PATHCONV: "1",
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
const handleError = (error) => {
|
|
647
|
+
child.removeListener("spawn", handleSpawn);
|
|
648
|
+
reject(new Error(`Failed to launch Unity: ${error.message}`));
|
|
649
|
+
};
|
|
650
|
+
const handleSpawn = () => {
|
|
651
|
+
child.removeListener("error", handleError);
|
|
652
|
+
child.unref();
|
|
653
|
+
resolve();
|
|
654
|
+
};
|
|
655
|
+
child.once("error", handleError);
|
|
656
|
+
child.once("spawn", handleSpawn);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
export async function orchestrateLaunch(options) {
|
|
660
|
+
if (options.quit && options.restart) {
|
|
661
|
+
throw new Error("--quit and --restart cannot be used together.");
|
|
662
|
+
}
|
|
663
|
+
let resolvedProjectPath = options.projectPath;
|
|
664
|
+
if (!resolvedProjectPath) {
|
|
665
|
+
const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
|
|
666
|
+
console.log(`Searching for Unity project under ${options.searchRoot} (max-depth: ${depthInfo})...`);
|
|
667
|
+
const found = findUnityProjectBfs(options.searchRoot, options.searchMaxDepth);
|
|
668
|
+
if (!found) {
|
|
669
|
+
throw new Error(`Unity project not found under ${options.searchRoot}.`);
|
|
670
|
+
}
|
|
671
|
+
console.log();
|
|
672
|
+
resolvedProjectPath = found;
|
|
673
|
+
}
|
|
674
|
+
if (!existsSync(resolvedProjectPath)) {
|
|
675
|
+
throw new Error(`Project directory not found: ${resolvedProjectPath}`);
|
|
676
|
+
}
|
|
677
|
+
const unityVersion = getUnityVersion(resolvedProjectPath);
|
|
678
|
+
if (options.addUnityHub || options.favoriteUnityHub) {
|
|
679
|
+
console.log(`Detected Unity version: ${unityVersion}`);
|
|
680
|
+
console.log(`Project Path: ${resolvedProjectPath}`);
|
|
681
|
+
const now = new Date();
|
|
682
|
+
await ensureProjectEntryAndUpdate(resolvedProjectPath, unityVersion, now, options.favoriteUnityHub);
|
|
683
|
+
console.log("Unity Hub entry updated.");
|
|
684
|
+
return { action: "hub-updated", projectPath: resolvedProjectPath, unityVersion };
|
|
685
|
+
}
|
|
686
|
+
if (options.quit) {
|
|
687
|
+
await quitRunningUnity(resolvedProjectPath);
|
|
688
|
+
return { action: "quit", projectPath: resolvedProjectPath };
|
|
689
|
+
}
|
|
690
|
+
const isRestart = options.restart;
|
|
691
|
+
if (isRestart) {
|
|
692
|
+
await killRunningUnity(resolvedProjectPath);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
const runningProcess = await findRunningUnityProcess(resolvedProjectPath);
|
|
696
|
+
if (runningProcess) {
|
|
697
|
+
console.log(`Unity process already running for project: ${resolvedProjectPath} (PID: ${runningProcess.pid})`);
|
|
698
|
+
await focusUnityProcess(runningProcess.pid);
|
|
699
|
+
return { action: "focused", projectPath: resolvedProjectPath, pid: runningProcess.pid };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
await handleStaleLockfile(resolvedProjectPath);
|
|
703
|
+
const resolved = {
|
|
704
|
+
projectPath: resolvedProjectPath,
|
|
705
|
+
platform: options.platform,
|
|
706
|
+
unityArgs: options.unityArgs,
|
|
707
|
+
unityVersion,
|
|
708
|
+
};
|
|
709
|
+
await launch(resolved);
|
|
710
|
+
// Hub timestamp update is non-critical external I/O; failure should not block after successful launch
|
|
711
|
+
const now = new Date();
|
|
712
|
+
try {
|
|
713
|
+
await updateLastModifiedIfExists(resolvedProjectPath, now);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
717
|
+
console.warn(`Failed to update Unity Hub lastModified: ${message}`);
|
|
718
|
+
}
|
|
719
|
+
const action = isRestart ? "killed-and-launched" : "launched";
|
|
720
|
+
return { action, projectPath: resolvedProjectPath, unityVersion };
|
|
559
721
|
}
|
|
560
722
|
// Re-export Unity Hub functions
|
|
561
|
-
export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs };
|
|
723
|
+
export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs };
|
package/dist/unityHub.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const ensureProjectEntryAndUpdate: (projectPath: string, version: string, when: Date, setFavorite?: boolean) => Promise<void>;
|
|
2
2
|
export declare const updateLastModifiedIfExists: (projectPath: string, when: Date) => Promise<void>;
|
|
3
3
|
export declare const parseCliArgs: (cliArgsString: string) => string[];
|
|
4
|
+
export declare const groupCliArgs: (args: readonly string[]) => string[];
|
|
4
5
|
export declare const getProjectCliArgs: (projectPath: string) => Promise<string[]>;
|
|
5
6
|
//# sourceMappingURL=unityHub.d.ts.map
|
package/dist/unityHub.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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;AAGF,eAAO,MAAM,YAAY,GAAI,MAAM,SAAS,MAAM,EAAE,KAAG,MAAM,EAiB5D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CA8C7E,CAAC"}
|
package/dist/unityHub.js
CHANGED
|
@@ -225,6 +225,27 @@ export const parseCliArgs = (cliArgsString) => {
|
|
|
225
225
|
}
|
|
226
226
|
return tokens;
|
|
227
227
|
};
|
|
228
|
+
// "-flag value" pairs are grouped into single strings for display (e.g. ["-foo", "bar", "-baz", "qux"] -> ["-foo bar", "-baz qux"])
|
|
229
|
+
export const groupCliArgs = (args) => {
|
|
230
|
+
const groups = [];
|
|
231
|
+
let current = "";
|
|
232
|
+
for (const arg of args) {
|
|
233
|
+
if (arg.startsWith("-") && current.length > 0) {
|
|
234
|
+
groups.push(current);
|
|
235
|
+
current = arg;
|
|
236
|
+
}
|
|
237
|
+
else if (current.length === 0) {
|
|
238
|
+
current = arg;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
current += ` ${arg}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (current.length > 0) {
|
|
245
|
+
groups.push(current);
|
|
246
|
+
}
|
|
247
|
+
return groups;
|
|
248
|
+
};
|
|
228
249
|
export const getProjectCliArgs = async (projectPath) => {
|
|
229
250
|
assert(projectPath !== null && projectPath !== undefined, "projectPath must not be null");
|
|
230
251
|
const infoFilePath = resolveUnityHubProjectsInfoFile();
|