launch-unity 0.13.0 → 0.14.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, parseArgs, findUnityProjectBfs, getUnityVersion, launch, 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,EAEhB,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,MAAM,EACN,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, findRunningUnityProcess, focusUnityProcess, killRunningUnity, quitRunningUnity, handleStaleLockfile, ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs, } from './lib.js';
package/dist/launch.d.ts CHANGED
@@ -6,6 +6,7 @@ export type LaunchOptions = {
6
6
  unityArgs: string[];
7
7
  searchMaxDepth: number;
8
8
  restart: boolean;
9
+ quit: boolean;
9
10
  addUnityHub: boolean;
10
11
  favoriteUnityHub: boolean;
11
12
  };
@@ -25,6 +26,7 @@ export declare function findRunningUnityProcess(projectPath: string): Promise<Un
25
26
  export declare function focusUnityProcess(pid: number): Promise<void>;
26
27
  export declare function handleStaleLockfile(projectPath: string): Promise<void>;
27
28
  export declare function killRunningUnity(projectPath: string): Promise<void>;
29
+ export declare function quitRunningUnity(projectPath: string): Promise<void>;
28
30
  export declare function findUnityProjectBfs(rootDir: string, maxDepth: number): string | undefined;
29
31
  export declare function launch(opts: LaunchResolvedOptions): Promise<void>;
30
32
  //# 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":";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,IAAI,EAAE,OAAO,CAAC;IACd,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,CA2HvD;AA0CD,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,CA0B5E;AAkCD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzE;AA0CD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BzE;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,CA6DvE"}
package/dist/launch.js CHANGED
@@ -9,7 +9,7 @@ import { rm } from "node:fs/promises";
9
9
  import { dirname, join, resolve } from "node:path";
10
10
  import { fileURLToPath, pathToFileURL } from "node:url";
11
11
  import { promisify } from "node:util";
12
- import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs } from "./unityHub.js";
12
+ import { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, groupCliArgs } 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;
@@ -144,6 +144,7 @@ export function parseArgs(argv) {
144
144
  const positionals = [];
145
145
  let maxDepth = 3; // default 3; -1 means unlimited
146
146
  let restart = false;
147
+ let quit = false;
147
148
  let addUnityHub = false;
148
149
  let favoriteUnityHub = false;
149
150
  let platform;
@@ -161,6 +162,10 @@ export function parseArgs(argv) {
161
162
  restart = true;
162
163
  continue;
163
164
  }
165
+ if (arg === "-q" || arg === "--quit") {
166
+ quit = true;
167
+ continue;
168
+ }
164
169
  if (arg === "-u" ||
165
170
  arg === "-a" ||
166
171
  arg === "--unity-hub-entry" ||
@@ -226,6 +231,7 @@ export function parseArgs(argv) {
226
231
  unityArgs,
227
232
  searchMaxDepth: maxDepth,
228
233
  restart,
234
+ quit,
229
235
  addUnityHub,
230
236
  favoriteUnityHub,
231
237
  };
@@ -266,6 +272,7 @@ Options:
266
272
  -h, --help Show this help message
267
273
  -v, --version Show version number
268
274
  -r, --restart Kill running Unity and restart
275
+ -q, --quit Quit running Unity gracefully (force-kill on timeout)
269
276
  -p, --platform <P> Passed to Unity as -buildTarget (e.g., StandaloneOSX, Android, iOS)
270
277
  --max-depth <N> Search depth when PROJECT_PATH is omitted (default 3, -1 unlimited)
271
278
  -u, -a, --unity-hub-entry, --add-unity-hub
@@ -554,9 +561,11 @@ export async function handleStaleLockfile(projectPath) {
554
561
  const message = error instanceof Error ? error.message : String(error);
555
562
  console.warn(`Failed to delete UnityLockfile: ${message}`);
556
563
  }
564
+ console.log();
557
565
  }
558
566
  const KILL_POLL_INTERVAL_MS = 100;
559
567
  const KILL_TIMEOUT_MS = 10000;
568
+ const GRACEFUL_QUIT_TIMEOUT_MS = 10000;
560
569
  function isProcessAlive(pid) {
561
570
  try {
562
571
  process.kill(pid, 0);
@@ -574,9 +583,9 @@ function killProcess(pid) {
574
583
  // Process already exited
575
584
  }
576
585
  }
577
- async function waitForProcessExit(pid) {
586
+ async function waitForProcessExit(pid, timeoutMs) {
578
587
  const start = Date.now();
579
- while (Date.now() - start < KILL_TIMEOUT_MS) {
588
+ while (Date.now() - start < timeoutMs) {
580
589
  if (!isProcessAlive(pid)) {
581
590
  return true;
582
591
  }
@@ -588,17 +597,82 @@ export async function killRunningUnity(projectPath) {
588
597
  const processInfo = await findRunningUnityProcess(projectPath);
589
598
  if (!processInfo) {
590
599
  console.log("No running Unity process found for this project.");
600
+ console.log();
591
601
  return;
592
602
  }
593
603
  const pid = processInfo.pid;
594
604
  console.log(`Killing Unity (PID: ${pid})...`);
595
605
  killProcess(pid);
596
- const exited = await waitForProcessExit(pid);
606
+ const exited = await waitForProcessExit(pid, KILL_TIMEOUT_MS);
597
607
  if (!exited) {
598
608
  console.error(`Error: Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
599
609
  process.exit(1);
600
610
  }
601
611
  console.log("Unity killed.");
612
+ console.log();
613
+ }
614
+ async function sendGracefulQuitMac(pid) {
615
+ // System Events quit and "tell application to quit" leave UnityLockfile behind,
616
+ // so we send Cmd+Q keystroke to trigger Unity's normal user-initiated shutdown flow
617
+ const script = [
618
+ 'tell application "System Events"',
619
+ ` set frontmost of (first process whose unix id is ${pid}) to true`,
620
+ ' keystroke "q" using {command down}',
621
+ "end tell",
622
+ ].join("\n");
623
+ try {
624
+ await execFileAsync("osascript", ["-e", script]);
625
+ }
626
+ catch {
627
+ // Process may have already exited
628
+ }
629
+ }
630
+ async function sendGracefulQuitWindows(pid) {
631
+ // process.kill(pid, "SIGTERM") forcefully kills on Windows, so use CloseMainWindow() to send WM_CLOSE
632
+ const scriptLines = [
633
+ "$ErrorActionPreference = 'Stop'",
634
+ `try { $proc = Get-Process -Id ${pid} -ErrorAction Stop; $proc.CloseMainWindow() | Out-Null } catch { }`,
635
+ ];
636
+ try {
637
+ await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
638
+ }
639
+ catch {
640
+ // Process may have already exited
641
+ }
642
+ }
643
+ async function sendGracefulQuit(pid) {
644
+ if (process.platform === "darwin") {
645
+ await sendGracefulQuitMac(pid);
646
+ return;
647
+ }
648
+ if (process.platform === "win32") {
649
+ await sendGracefulQuitWindows(pid);
650
+ return;
651
+ }
652
+ }
653
+ export async function quitRunningUnity(projectPath) {
654
+ const processInfo = await findRunningUnityProcess(projectPath);
655
+ if (!processInfo) {
656
+ console.log("No running Unity process found for this project.");
657
+ return;
658
+ }
659
+ const pid = processInfo.pid;
660
+ console.log(`Quitting Unity (PID: ${pid})...`);
661
+ await sendGracefulQuit(pid);
662
+ console.log(`Sent graceful quit signal. Waiting up to ${GRACEFUL_QUIT_TIMEOUT_MS / 1000}s...`);
663
+ const exitedGracefully = await waitForProcessExit(pid, GRACEFUL_QUIT_TIMEOUT_MS);
664
+ if (exitedGracefully) {
665
+ console.log("Unity quit gracefully.");
666
+ return;
667
+ }
668
+ console.log("Unity did not respond to graceful quit. Force killing...");
669
+ killProcess(pid);
670
+ const exitedAfterKill = await waitForProcessExit(pid, KILL_TIMEOUT_MS);
671
+ if (!exitedAfterKill) {
672
+ console.error(`Error: Failed to kill Unity (PID: ${pid}) within ${KILL_TIMEOUT_MS / 1000}s.`);
673
+ process.exit(1);
674
+ }
675
+ console.log("Unity force killed.");
602
676
  }
603
677
  function hasBuildTargetArg(unityArgs) {
604
678
  for (const arg of unityArgs) {
@@ -698,7 +772,6 @@ export function findUnityProjectBfs(rootDir, maxDepth) {
698
772
  export async function launch(opts) {
699
773
  const { projectPath, platform, unityArgs, unityVersion } = opts;
700
774
  const unityPath = getUnityPath(unityVersion);
701
- console.log(`Detected Unity version: ${unityVersion}`);
702
775
  if (!existsSync(unityPath)) {
703
776
  console.error(`Error: Unity ${unityVersion} not found at ${unityPath}`);
704
777
  console.error("Please install Unity through Unity Hub.");
@@ -706,6 +779,7 @@ export async function launch(opts) {
706
779
  }
707
780
  console.log("Opening Unity...");
708
781
  console.log(`Project Path: ${projectPath}`);
782
+ console.log(`Detected Unity version: ${unityVersion}`);
709
783
  const args = ["-projectPath", projectPath];
710
784
  const unityArgsContainBuildTarget = hasBuildTargetArg(unityArgs);
711
785
  if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
@@ -713,13 +787,40 @@ export async function launch(opts) {
713
787
  }
714
788
  const hubCliArgs = await getProjectCliArgs(projectPath);
715
789
  if (hubCliArgs.length > 0) {
790
+ console.log("Unity Hub launch options:");
791
+ for (const line of groupCliArgs(hubCliArgs)) {
792
+ console.log(` ${line}`);
793
+ }
716
794
  args.push(...hubCliArgs);
717
795
  }
796
+ else {
797
+ console.log("Unity Hub launch options: none");
798
+ }
718
799
  if (unityArgs.length > 0) {
719
800
  args.push(...unityArgs);
720
801
  }
721
- const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
722
- child.unref();
802
+ return new Promise((resolve, reject) => {
803
+ const child = spawn(unityPath, args, {
804
+ stdio: "ignore",
805
+ detached: true,
806
+ // Git Bash (MSYS) がWindows パスをUnix 形式に自動変換するのを防ぐ
807
+ env: {
808
+ ...process.env,
809
+ MSYS_NO_PATHCONV: "1",
810
+ },
811
+ });
812
+ const handleError = (error) => {
813
+ child.removeListener("spawn", handleSpawn);
814
+ reject(new Error(`Failed to launch Unity: ${error.message}`));
815
+ };
816
+ const handleSpawn = () => {
817
+ child.removeListener("error", handleError);
818
+ child.unref();
819
+ resolve();
820
+ };
821
+ child.once("error", handleError);
822
+ child.once("spawn", handleSpawn);
823
+ });
723
824
  }
724
825
  async function main() {
725
826
  const options = parseArgs(process.argv);
@@ -731,14 +832,14 @@ async function main() {
731
832
  if (!resolvedProjectPath) {
732
833
  const searchRoot = process.cwd();
733
834
  const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
734
- console.log(`No PROJECT_PATH provided. Searching under ${searchRoot} (max-depth: ${depthInfo})...`);
835
+ console.log(`Searching for Unity project under ${searchRoot} (max-depth: ${depthInfo})...`);
735
836
  const found = findUnityProjectBfs(searchRoot, options.searchMaxDepth);
736
837
  if (!found) {
737
838
  console.error(`Error: Unity project not found under ${searchRoot}.`);
738
839
  process.exit(1);
739
840
  return;
740
841
  }
741
- console.log(`Selected project: ${found}`);
842
+ console.log();
742
843
  resolvedProjectPath = found;
743
844
  }
744
845
  ensureProjectPath(resolvedProjectPath);
@@ -759,6 +860,14 @@ async function main() {
759
860
  }
760
861
  return;
761
862
  }
863
+ if (options.quit && options.restart) {
864
+ console.error("Error: --quit and --restart cannot be used together.");
865
+ process.exit(1);
866
+ }
867
+ if (options.quit) {
868
+ await quitRunningUnity(resolvedProjectPath);
869
+ return;
870
+ }
762
871
  if (options.restart) {
763
872
  await killRunningUnity(resolvedProjectPath);
764
873
  }
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,8 @@ 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 { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs };
35
37
  //# 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":"AAAA;;;GAGG;AAQH,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAEvI,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,IAAI,EAAE,OAAO,CAAC;IACd,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,CAyHvD;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,CA0B5E;AAkCD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBzE;AA0CD,wBAAsB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BzE;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,CA6DvE;AAGD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC"}
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,40 @@ 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
+ });
559
658
  }
560
659
  // Re-export Unity Hub functions
561
- export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs };
660
+ 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.14.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",