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 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
@@ -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,EAEhB,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,MAAM,EACN,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,UAAU,CAAC"}
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 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>;
2
+ export {};
30
3
  //# sourceMappingURL=launch.d.ts.map
@@ -1 +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"}
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 { execFile, spawn } from "node:child_process";
7
- import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from "node:fs";
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 { promisify } from "node:util";
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
- export function parseArgs(argv) {
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
- let resolvedProjectPath = options.projectPath;
731
- if (!resolvedProjectPath) {
732
- const searchRoot = process.cwd();
733
- const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
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
- unityVersion,
780
- };
781
- await launch(resolved);
782
- // Best-effort update of Unity Hub's lastModified timestamp.
783
- const now = new Date();
784
- try {
785
- await updateLastModifiedIfExists(resolvedProjectPath, now);
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 { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs };
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;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"}
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 < KILL_TIMEOUT_MS) {
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
- const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
558
- child.unref();
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 };
@@ -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
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launch-unity",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Open a Unity project with the matching Editor version (macOS/Windows)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",