launch-unity 0.12.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
- export declare function launch(opts: LaunchResolvedOptions): void;
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,wBAAgB,MAAM,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CA4BxD"}
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 } 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) {
@@ -695,10 +769,9 @@ export function findUnityProjectBfs(rootDir, maxDepth) {
695
769
  }
696
770
  return undefined;
697
771
  }
698
- export function launch(opts) {
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,16 +779,48 @@ export 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) {
712
786
  args.push("-buildTarget", platform);
713
787
  }
788
+ const hubCliArgs = await getProjectCliArgs(projectPath);
789
+ if (hubCliArgs.length > 0) {
790
+ console.log("Unity Hub launch options:");
791
+ for (const line of groupCliArgs(hubCliArgs)) {
792
+ console.log(` ${line}`);
793
+ }
794
+ args.push(...hubCliArgs);
795
+ }
796
+ else {
797
+ console.log("Unity Hub launch options: none");
798
+ }
714
799
  if (unityArgs.length > 0) {
715
800
  args.push(...unityArgs);
716
801
  }
717
- const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
718
- 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
+ });
719
824
  }
720
825
  async function main() {
721
826
  const options = parseArgs(process.argv);
@@ -727,14 +832,14 @@ async function main() {
727
832
  if (!resolvedProjectPath) {
728
833
  const searchRoot = process.cwd();
729
834
  const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
730
- console.log(`No PROJECT_PATH provided. Searching under ${searchRoot} (max-depth: ${depthInfo})...`);
835
+ console.log(`Searching for Unity project under ${searchRoot} (max-depth: ${depthInfo})...`);
731
836
  const found = findUnityProjectBfs(searchRoot, options.searchMaxDepth);
732
837
  if (!found) {
733
838
  console.error(`Error: Unity project not found under ${searchRoot}.`);
734
839
  process.exit(1);
735
840
  return;
736
841
  }
737
- console.log(`Selected project: ${found}`);
842
+ console.log();
738
843
  resolvedProjectPath = found;
739
844
  }
740
845
  ensureProjectPath(resolvedProjectPath);
@@ -755,6 +860,14 @@ async function main() {
755
860
  }
756
861
  return;
757
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
+ }
758
871
  if (options.restart) {
759
872
  await killRunningUnity(resolvedProjectPath);
760
873
  }
@@ -774,7 +887,7 @@ async function main() {
774
887
  unityArgs: options.unityArgs,
775
888
  unityVersion,
776
889
  };
777
- launch(resolved);
890
+ await launch(resolved);
778
891
  // Best-effort update of Unity Hub's lastModified timestamp.
779
892
  const now = new Date();
780
893
  try {
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 } 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
- export declare function launch(opts: LaunchResolvedOptions): void;
34
- export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists };
35
+ export declare function launch(opts: LaunchResolvedOptions): Promise<void>;
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,MAAM,eAAe,CAAC;AAExF,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,wBAAgB,MAAM,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CA4BxD;AAGD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,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 } 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) {
@@ -533,25 +605,56 @@ export function findUnityProjectBfs(rootDir, maxDepth) {
533
605
  }
534
606
  return undefined;
535
607
  }
536
- export function launch(opts) {
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) {
548
620
  args.push("-buildTarget", platform);
549
621
  }
622
+ const hubCliArgs = await getProjectCliArgs(projectPath);
623
+ if (hubCliArgs.length > 0) {
624
+ console.log("Unity Hub launch options:");
625
+ for (const line of groupCliArgs(hubCliArgs)) {
626
+ console.log(` ${line}`);
627
+ }
628
+ args.push(...hubCliArgs);
629
+ }
630
+ else {
631
+ console.log("Unity Hub launch options: none");
632
+ }
550
633
  if (unityArgs.length > 0) {
551
634
  args.push(...unityArgs);
552
635
  }
553
- const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
554
- 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
+ });
555
658
  }
556
659
  // Re-export Unity Hub functions
557
- export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists };
660
+ export { ensureProjectEntryAndUpdate, updateLastModifiedIfExists, getProjectCliArgs, parseCliArgs, groupCliArgs };
@@ -1,3 +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
+ export declare const parseCliArgs: (cliArgsString: string) => string[];
4
+ export declare const groupCliArgs: (args: readonly string[]) => string[];
5
+ export declare const getProjectCliArgs: (projectPath: string) => Promise<string[]>;
3
6
  //# sourceMappingURL=unityHub.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"unityHub.d.ts","sourceRoot":"","sources":["../src/unityHub.ts"],"names":[],"mappings":"AAiFA,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"}
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
@@ -1,6 +1,7 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
+ import assert from "node:assert";
4
5
  const resolveUnityHubProjectFiles = () => {
5
6
  if (process.platform === "darwin") {
6
7
  const home = process.env.HOME;
@@ -168,3 +169,120 @@ export const updateLastModifiedIfExists = async (projectPath, when) => {
168
169
  return;
169
170
  }
170
171
  };
172
+ const resolveUnityHubProjectsInfoFile = () => {
173
+ if (process.platform === "darwin") {
174
+ const home = process.env.HOME;
175
+ if (!home) {
176
+ return undefined;
177
+ }
178
+ return join(home, "Library", "Application Support", "UnityHub", "projectsInfo.json");
179
+ }
180
+ if (process.platform === "win32") {
181
+ const appData = process.env.APPDATA;
182
+ if (!appData) {
183
+ return undefined;
184
+ }
185
+ return join(appData, "UnityHub", "projectsInfo.json");
186
+ }
187
+ return undefined;
188
+ };
189
+ export const parseCliArgs = (cliArgsString) => {
190
+ assert(cliArgsString !== null && cliArgsString !== undefined, "cliArgsString must not be null");
191
+ const trimmed = cliArgsString.trim();
192
+ if (trimmed.length === 0) {
193
+ return [];
194
+ }
195
+ const tokens = [];
196
+ let current = "";
197
+ let inQuote = null;
198
+ for (const char of trimmed) {
199
+ if (inQuote !== null) {
200
+ if (char === inQuote) {
201
+ tokens.push(current);
202
+ current = "";
203
+ inQuote = null;
204
+ }
205
+ else {
206
+ current += char;
207
+ }
208
+ continue;
209
+ }
210
+ if (char === '"' || char === "'") {
211
+ inQuote = char;
212
+ continue;
213
+ }
214
+ if (char === " ") {
215
+ if (current.length > 0) {
216
+ tokens.push(current);
217
+ current = "";
218
+ }
219
+ continue;
220
+ }
221
+ current += char;
222
+ }
223
+ if (current.length > 0) {
224
+ tokens.push(current);
225
+ }
226
+ return tokens;
227
+ };
228
+ // "-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
+ };
249
+ export const getProjectCliArgs = async (projectPath) => {
250
+ assert(projectPath !== null && projectPath !== undefined, "projectPath must not be null");
251
+ const infoFilePath = resolveUnityHubProjectsInfoFile();
252
+ if (!infoFilePath) {
253
+ logDebug("projectsInfo.json path could not be resolved.");
254
+ return [];
255
+ }
256
+ logDebug(`Reading projectsInfo.json: ${infoFilePath}`);
257
+ let content;
258
+ try {
259
+ content = await readFile(infoFilePath, "utf8");
260
+ }
261
+ catch {
262
+ logDebug("projectsInfo.json not found or not readable.");
263
+ return [];
264
+ }
265
+ let json;
266
+ try {
267
+ json = JSON.parse(content);
268
+ }
269
+ catch {
270
+ logDebug("projectsInfo.json parse failed.");
271
+ return [];
272
+ }
273
+ const normalizedProjectPath = normalizePath(projectPath);
274
+ const projectKey = Object.keys(json).find((key) => pathsEqual(key, normalizedProjectPath));
275
+ if (!projectKey) {
276
+ logDebug(`No entry found for project: ${normalizedProjectPath}`);
277
+ return [];
278
+ }
279
+ const projectInfo = json[projectKey];
280
+ const cliArgsString = projectInfo?.cliArgs;
281
+ if (!cliArgsString || cliArgsString.trim().length === 0) {
282
+ logDebug("cliArgs is empty or not defined.");
283
+ return [];
284
+ }
285
+ const parsed = parseCliArgs(cliArgsString);
286
+ logDebug(`Parsed Unity Hub cliArgs: ${JSON.stringify(parsed)}`);
287
+ return parsed;
288
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launch-unity",
3
- "version": "0.12.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",
@@ -45,16 +45,16 @@
45
45
  "node": ">=18"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "25.0.10",
49
- "@typescript-eslint/eslint-plugin": "8.53.0",
50
- "@typescript-eslint/parser": "8.53.0",
48
+ "@types/node": "25.2.0",
49
+ "@typescript-eslint/eslint-plugin": "8.54.0",
50
+ "@typescript-eslint/parser": "8.54.0",
51
51
  "eslint": "9.39.2",
52
52
  "eslint-config-prettier": "10.1.8",
53
53
  "prettier": "3.8.1",
54
54
  "typescript": "5.9.3"
55
55
  },
56
56
  "dependencies": {
57
- "typescript-eslint": "8.53.1"
57
+ "typescript-eslint": "8.54.0"
58
58
  },
59
59
  "overrides": {
60
60
  "js-yaml": "4.1.1"