launch-unity 0.6.0 → 0.6.2
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/README.ja.md +1 -17
- package/README.md +1 -14
- package/dist/launch.js +555 -0
- package/dist/unityHub.js +90 -0
- package/package.json +2 -3
package/README.ja.md
CHANGED
|
@@ -18,7 +18,7 @@ npx launch-unity
|
|
|
18
18
|
## 使用例
|
|
19
19
|
```bash
|
|
20
20
|
# NPX(推奨 / インストール不要)
|
|
21
|
-
npx launch-unity #
|
|
21
|
+
npx launch-unity # カレントディレクトリから3階層下までUnityプロジェクトを探索して開く
|
|
22
22
|
npx launch-unity /path/to/Proj # プロジェクトを指定
|
|
23
23
|
npx launch-unity /path Android # ビルドターゲットを指定
|
|
24
24
|
npx -y launch-unity # npx の「Ok to proceed?」確認をスキップ
|
|
@@ -31,11 +31,6 @@ npx launch-unity /path Android -- -executeMethod My.Build.Entry
|
|
|
31
31
|
launch-unity
|
|
32
32
|
launch-unity /path/to/MyUnityProject Android
|
|
33
33
|
launch-unity . -- -buildTarget iOS -projectPath . # 上書きも可能
|
|
34
|
-
|
|
35
|
-
# プロジェクトを開いている Unity を終了
|
|
36
|
-
quit-unity # カレントディレクトリのプロジェクトの Unity を終了
|
|
37
|
-
quit-unity /path/to/Proj # 指定プロジェクトの Unity を終了
|
|
38
|
-
quit-unity . --timeout 20000 --force
|
|
39
34
|
```
|
|
40
35
|
|
|
41
36
|
指定した Unity プロジェクトの `ProjectSettings/ProjectVersion.txt` から必要な Unity Editor のバージョンを読み取り、
|
|
@@ -61,17 +56,6 @@ Unity Hub でインストール済みの該当バージョンを起動する mac
|
|
|
61
56
|
- macOS / Windows: Unity Hub のデフォルトインストールパスを前提にサポート。
|
|
62
57
|
- Linux: 未対応です。対応 PR を歓迎します。
|
|
63
58
|
|
|
64
|
-
## quit-unity 使い方(詳細)
|
|
65
|
-
```bash
|
|
66
|
-
# 基本構文
|
|
67
|
-
quit-unity [PROJECT_PATH] [--timeout <ms>] [--force]
|
|
68
|
-
|
|
69
|
-
# フラグ
|
|
70
|
-
# -t, --timeout <ms> 正常終了を待つ時間(既定: 15000ms)
|
|
71
|
-
# -f, --force タイムアウト後に強制終了
|
|
72
|
-
# -h, --help ヘルプ
|
|
73
|
-
```
|
|
74
|
-
|
|
75
59
|
## セキュリティ
|
|
76
60
|
|
|
77
61
|
このプロジェクトはサプライチェーン攻撃対策を実施しています:
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ npx launch-unity
|
|
|
18
18
|
## Usage Examples
|
|
19
19
|
```bash
|
|
20
20
|
# NPX (recommended, zero install)
|
|
21
|
-
npx launch-unity #
|
|
21
|
+
npx launch-unity # Search up to 3 levels deep for a Unity project and open it
|
|
22
22
|
npx launch-unity /path/to/Proj # Open a specific project
|
|
23
23
|
npx launch-unity /path Android # Specify build target
|
|
24
24
|
npx -y launch-unity # Skip npx "Ok to proceed?" prompt
|
|
@@ -31,11 +31,6 @@ npx launch-unity /path Android -- -executeMethod My.Build.Entry
|
|
|
31
31
|
launch-unity
|
|
32
32
|
launch-unity /path/to/MyUnityProject Android
|
|
33
33
|
launch-unity . -- -buildTarget iOS -projectPath . # You can override
|
|
34
|
-
|
|
35
|
-
# Quit the Unity instance holding a project open
|
|
36
|
-
quit-unity # Quit Unity for current directory project
|
|
37
|
-
quit-unity /path/to/Proj # Quit Unity for a specific project
|
|
38
|
-
quit-unity . --timeout 20000 --force
|
|
39
34
|
```
|
|
40
35
|
|
|
41
36
|
A TypeScript CLI for macOS and Windows that reads the required Unity Editor version from
|
|
@@ -63,14 +58,6 @@ launch-unity [PROJECT_PATH] [PLATFORM]
|
|
|
63
58
|
|
|
64
59
|
# Flags
|
|
65
60
|
# -h, --help Show help
|
|
66
|
-
|
|
67
|
-
# Quit syntax
|
|
68
|
-
quit-unity [PROJECT_PATH] [--timeout <ms>] [--force]
|
|
69
|
-
|
|
70
|
-
# Flags (quit-unity)
|
|
71
|
-
# -t, --timeout <ms> Time to wait for graceful quit (default: 15000)
|
|
72
|
-
# -f, --force Force kill if not exited within timeout
|
|
73
|
-
# -h, --help Show help
|
|
74
61
|
```
|
|
75
62
|
|
|
76
63
|
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
launch-unity: Open a Unity project with the matching Editor version.
|
|
4
|
+
Platforms: macOS, Windows
|
|
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";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { updateLastModifiedIfExists } from "./unityHub.js";
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const UNITY_EXECUTABLE_PATTERN_MAC = /Unity\.app\/Contents\/MacOS\/Unity/i;
|
|
14
|
+
const UNITY_EXECUTABLE_PATTERN_WINDOWS = /Unity\.exe/i;
|
|
15
|
+
const PROJECT_PATH_PATTERN = /-(?:projectPath|projectpath)(?:=|\s+)("[^"]+"|'[^']+'|[^\s"']+)/i;
|
|
16
|
+
const PROCESS_LIST_COMMAND_MAC = "ps";
|
|
17
|
+
const PROCESS_LIST_ARGS_MAC = ["-axo", "pid=,command=", "-ww"];
|
|
18
|
+
const WINDOWS_POWERSHELL = "powershell";
|
|
19
|
+
const UNITY_LOCKFILE_NAME = "UnityLockfile";
|
|
20
|
+
const TEMP_DIRECTORY_NAME = "Temp";
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = argv.slice(2);
|
|
23
|
+
const doubleDashIndex = args.indexOf("--");
|
|
24
|
+
const cliArgs = doubleDashIndex >= 0 ? args.slice(0, doubleDashIndex) : args;
|
|
25
|
+
const unityArgs = doubleDashIndex >= 0 ? args.slice(doubleDashIndex + 1) : [];
|
|
26
|
+
const positionals = [];
|
|
27
|
+
let maxDepth = 3; // default 3; -1 means unlimited
|
|
28
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
29
|
+
const arg = cliArgs[i] ?? "";
|
|
30
|
+
if (arg === "--help" || arg === "-h") {
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (arg.startsWith("--max-depth")) {
|
|
35
|
+
const parts = arg.split("=");
|
|
36
|
+
if (parts.length === 2) {
|
|
37
|
+
const value = Number.parseInt(parts[1] ?? "", 10);
|
|
38
|
+
if (Number.isFinite(value)) {
|
|
39
|
+
maxDepth = value;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const next = cliArgs[i + 1];
|
|
44
|
+
if (typeof next === "string" && !next.startsWith("-")) {
|
|
45
|
+
const value = Number.parseInt(next, 10);
|
|
46
|
+
if (Number.isFinite(value)) {
|
|
47
|
+
maxDepth = value;
|
|
48
|
+
}
|
|
49
|
+
i += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg.startsWith("-")) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
positionals.push(arg);
|
|
58
|
+
}
|
|
59
|
+
let projectPath;
|
|
60
|
+
let platform;
|
|
61
|
+
if (positionals.length === 0) {
|
|
62
|
+
projectPath = undefined; // trigger search
|
|
63
|
+
platform = undefined;
|
|
64
|
+
}
|
|
65
|
+
else if (positionals.length === 1) {
|
|
66
|
+
const first = positionals[0] ?? "";
|
|
67
|
+
const resolvedFirst = resolve(first);
|
|
68
|
+
if (existsSync(resolvedFirst)) {
|
|
69
|
+
projectPath = resolvedFirst;
|
|
70
|
+
platform = undefined;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Treat as platform when path does not exist
|
|
74
|
+
projectPath = undefined; // trigger search
|
|
75
|
+
platform = String(first);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
projectPath = resolve(positionals[0] ?? "");
|
|
80
|
+
platform = String(positionals[1] ?? "");
|
|
81
|
+
}
|
|
82
|
+
const options = { unityArgs, searchMaxDepth: maxDepth };
|
|
83
|
+
if (projectPath !== undefined) {
|
|
84
|
+
options.projectPath = projectPath;
|
|
85
|
+
}
|
|
86
|
+
if (platform !== undefined) {
|
|
87
|
+
options.platform = platform;
|
|
88
|
+
}
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
function printHelp() {
|
|
92
|
+
const help = `
|
|
93
|
+
Usage: launch-unity [PROJECT_PATH] [PLATFORM] -- [UNITY_ARGS...]
|
|
94
|
+
|
|
95
|
+
Open a Unity project with the matching Unity Editor version installed by Unity Hub.
|
|
96
|
+
|
|
97
|
+
Arguments:
|
|
98
|
+
PROJECT_PATH Optional. Defaults to current directory
|
|
99
|
+
PLATFORM Optional. Passed to Unity as -buildTarget (e.g., StandaloneOSX, Android, iOS)
|
|
100
|
+
|
|
101
|
+
Forwarding:
|
|
102
|
+
Everything after -- is forwarded to Unity unchanged.
|
|
103
|
+
If UNITY_ARGS includes -buildTarget, the PLATFORM argument is ignored.
|
|
104
|
+
|
|
105
|
+
Flags:
|
|
106
|
+
-h, --help Show this help message
|
|
107
|
+
--max-depth <N> Search depth when PROJECT_PATH is omitted (default 3, -1 unlimited)
|
|
108
|
+
`;
|
|
109
|
+
process.stdout.write(help);
|
|
110
|
+
}
|
|
111
|
+
function getUnityVersion(projectPath) {
|
|
112
|
+
const versionFile = join(projectPath, "ProjectSettings", "ProjectVersion.txt");
|
|
113
|
+
if (!existsSync(versionFile)) {
|
|
114
|
+
console.error(`Error: ProjectVersion.txt not found at ${versionFile}`);
|
|
115
|
+
console.error("This does not appear to be a Unity project.");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const content = readFileSync(versionFile, "utf8");
|
|
119
|
+
const version = content.match(/m_EditorVersion:\s*([^\s\n]+)/)?.[1];
|
|
120
|
+
if (!version) {
|
|
121
|
+
console.error(`Error: Could not extract Unity version from ${versionFile}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
return version;
|
|
125
|
+
}
|
|
126
|
+
function getUnityPathWindows(version) {
|
|
127
|
+
const candidates = [];
|
|
128
|
+
const programFiles = process.env["PROGRAMFILES"];
|
|
129
|
+
const programFilesX86 = process.env["PROGRAMFILES(X86)"];
|
|
130
|
+
const localAppData = process.env["LOCALAPPDATA"];
|
|
131
|
+
const addCandidate = (base) => {
|
|
132
|
+
if (!base) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
candidates.push(join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
136
|
+
};
|
|
137
|
+
addCandidate(programFiles);
|
|
138
|
+
addCandidate(programFilesX86);
|
|
139
|
+
addCandidate(localAppData);
|
|
140
|
+
candidates.push(join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"));
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
if (existsSync(candidate)) {
|
|
143
|
+
return candidate;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return candidates[0] ?? join("C:\\", "Program Files", "Unity", "Hub", "Editor", version, "Editor", "Unity.exe");
|
|
147
|
+
}
|
|
148
|
+
function getUnityPath(version) {
|
|
149
|
+
if (process.platform === "darwin") {
|
|
150
|
+
return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
|
|
151
|
+
}
|
|
152
|
+
if (process.platform === "win32") {
|
|
153
|
+
return getUnityPathWindows(version);
|
|
154
|
+
}
|
|
155
|
+
return `/Applications/Unity/Hub/Editor/${version}/Unity.app/Contents/MacOS/Unity`;
|
|
156
|
+
}
|
|
157
|
+
function ensureProjectPath(projectPath) {
|
|
158
|
+
if (!existsSync(projectPath)) {
|
|
159
|
+
console.error(`Error: Project directory not found: ${projectPath}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const removeTrailingSeparators = (target) => {
|
|
164
|
+
let trimmed = target;
|
|
165
|
+
while (trimmed.length > 1 && (trimmed.endsWith("/") || trimmed.endsWith("\\"))) {
|
|
166
|
+
trimmed = trimmed.slice(0, -1);
|
|
167
|
+
}
|
|
168
|
+
return trimmed;
|
|
169
|
+
};
|
|
170
|
+
const normalizePath = (target) => {
|
|
171
|
+
const resolvedPath = resolve(target);
|
|
172
|
+
const trimmed = removeTrailingSeparators(resolvedPath);
|
|
173
|
+
return trimmed;
|
|
174
|
+
};
|
|
175
|
+
const toComparablePath = (value) => {
|
|
176
|
+
return value.replace(/\\/g, "/").toLocaleLowerCase();
|
|
177
|
+
};
|
|
178
|
+
const pathsEqual = (left, right) => {
|
|
179
|
+
return toComparablePath(normalizePath(left)) === toComparablePath(normalizePath(right));
|
|
180
|
+
};
|
|
181
|
+
function extractProjectPath(command) {
|
|
182
|
+
const match = command.match(PROJECT_PATH_PATTERN);
|
|
183
|
+
if (!match) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const raw = match[1];
|
|
187
|
+
if (!raw) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const trimmed = raw.trim();
|
|
191
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
|
|
192
|
+
return trimmed.slice(1, -1);
|
|
193
|
+
}
|
|
194
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
195
|
+
return trimmed.slice(1, -1);
|
|
196
|
+
}
|
|
197
|
+
return trimmed;
|
|
198
|
+
}
|
|
199
|
+
const isUnityAuxiliaryProcess = (command) => {
|
|
200
|
+
const normalizedCommand = command.toLowerCase();
|
|
201
|
+
if (normalizedCommand.includes("-batchmode")) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return normalizedCommand.includes("assetimportworker");
|
|
205
|
+
};
|
|
206
|
+
async function listUnityProcessesMac() {
|
|
207
|
+
let stdout = "";
|
|
208
|
+
try {
|
|
209
|
+
const result = await execFileAsync(PROCESS_LIST_COMMAND_MAC, PROCESS_LIST_ARGS_MAC);
|
|
210
|
+
stdout = result.stdout;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
console.error(`Failed to retrieve Unity process list: ${message}`);
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const lines = stdout
|
|
218
|
+
.split("\n")
|
|
219
|
+
.map((line) => line.trim())
|
|
220
|
+
.filter((line) => line.length > 0);
|
|
221
|
+
const processes = [];
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const match = line.match(/^(\d+)\s+(.*)$/);
|
|
224
|
+
if (!match) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const pidValue = Number.parseInt(match[1] ?? "", 10);
|
|
228
|
+
if (!Number.isFinite(pidValue)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const command = match[2] ?? "";
|
|
232
|
+
if (!UNITY_EXECUTABLE_PATTERN_MAC.test(command)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (isUnityAuxiliaryProcess(command)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const projectArgument = extractProjectPath(command);
|
|
239
|
+
if (!projectArgument) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
processes.push({
|
|
243
|
+
pid: pidValue,
|
|
244
|
+
projectPath: normalizePath(projectArgument),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return processes;
|
|
248
|
+
}
|
|
249
|
+
async function listUnityProcessesWindows() {
|
|
250
|
+
const scriptLines = [
|
|
251
|
+
"$ErrorActionPreference = 'Stop'",
|
|
252
|
+
"$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }",
|
|
253
|
+
"foreach ($process in $processes) {",
|
|
254
|
+
" $commandLine = $process.CommandLine -replace \"`r\", ' ' -replace \"`n\", ' '",
|
|
255
|
+
" Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)",
|
|
256
|
+
"}",
|
|
257
|
+
];
|
|
258
|
+
let stdout = "";
|
|
259
|
+
try {
|
|
260
|
+
const result = await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
261
|
+
stdout = result.stdout ?? "";
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
console.error(`Failed to retrieve Unity process list on Windows: ${message}`);
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
const lines = stdout
|
|
269
|
+
.split("\n")
|
|
270
|
+
.map((line) => line.trim())
|
|
271
|
+
.filter((line) => line.length > 0);
|
|
272
|
+
const processes = [];
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
const delimiterIndex = line.indexOf("|");
|
|
275
|
+
if (delimiterIndex < 0) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const pidText = line.slice(0, delimiterIndex).trim();
|
|
279
|
+
const command = line.slice(delimiterIndex + 1).trim();
|
|
280
|
+
const pidValue = Number.parseInt(pidText, 10);
|
|
281
|
+
if (!Number.isFinite(pidValue)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!UNITY_EXECUTABLE_PATTERN_WINDOWS.test(command)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (isUnityAuxiliaryProcess(command)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const projectArgument = extractProjectPath(command);
|
|
291
|
+
if (!projectArgument) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
processes.push({
|
|
295
|
+
pid: pidValue,
|
|
296
|
+
projectPath: normalizePath(projectArgument),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return processes;
|
|
300
|
+
}
|
|
301
|
+
async function listUnityProcesses() {
|
|
302
|
+
if (process.platform === "darwin") {
|
|
303
|
+
return await listUnityProcessesMac();
|
|
304
|
+
}
|
|
305
|
+
if (process.platform === "win32") {
|
|
306
|
+
return await listUnityProcessesWindows();
|
|
307
|
+
}
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
async function findRunningUnityProcess(projectPath) {
|
|
311
|
+
const normalizedTarget = normalizePath(projectPath);
|
|
312
|
+
const processes = await listUnityProcesses();
|
|
313
|
+
return processes.find((candidate) => pathsEqual(candidate.projectPath, normalizedTarget));
|
|
314
|
+
}
|
|
315
|
+
async function focusUnityProcess(pid) {
|
|
316
|
+
if (process.platform === "darwin") {
|
|
317
|
+
await focusUnityProcessMac(pid);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (process.platform === "win32") {
|
|
321
|
+
await focusUnityProcessWindows(pid);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function focusUnityProcessMac(pid) {
|
|
325
|
+
const script = `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
|
|
326
|
+
try {
|
|
327
|
+
await execFileAsync("osascript", ["-e", script]);
|
|
328
|
+
console.log("Brought existing Unity to the front.");
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
|
+
console.warn(`Failed to bring Unity to front: ${message}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function focusUnityProcessWindows(pid) {
|
|
336
|
+
const addTypeLines = [
|
|
337
|
+
"Add-Type -TypeDefinition @\"",
|
|
338
|
+
"using System;",
|
|
339
|
+
"using System.Runtime.InteropServices;",
|
|
340
|
+
"public static class Win32Interop {",
|
|
341
|
+
" [DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);",
|
|
342
|
+
" [DllImport(\"user32.dll\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);",
|
|
343
|
+
"}",
|
|
344
|
+
"\"@",
|
|
345
|
+
];
|
|
346
|
+
const scriptLines = [
|
|
347
|
+
"$ErrorActionPreference = 'Stop'",
|
|
348
|
+
...addTypeLines,
|
|
349
|
+
`try { $process = Get-Process -Id ${pid} -ErrorAction Stop } catch { return }`,
|
|
350
|
+
"$handle = $process.MainWindowHandle",
|
|
351
|
+
"if ($handle -eq 0) { return }",
|
|
352
|
+
"[Win32Interop]::ShowWindowAsync($handle, 9) | Out-Null",
|
|
353
|
+
"[Win32Interop]::SetForegroundWindow($handle) | Out-Null",
|
|
354
|
+
];
|
|
355
|
+
try {
|
|
356
|
+
await execFileAsync(WINDOWS_POWERSHELL, ["-NoProfile", "-Command", scriptLines.join("\n")]);
|
|
357
|
+
console.log("Brought existing Unity to the front.");
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
console.warn(`Failed to bring Unity to front on Windows: ${message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function handleStaleLockfile(projectPath) {
|
|
365
|
+
const tempDirectoryPath = join(projectPath, TEMP_DIRECTORY_NAME);
|
|
366
|
+
const lockfilePath = join(tempDirectoryPath, UNITY_LOCKFILE_NAME);
|
|
367
|
+
if (!existsSync(lockfilePath)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
console.log(`UnityLockfile found without active Unity process: ${lockfilePath}`);
|
|
371
|
+
console.log("Assuming previous crash. Cleaning Temp directory and continuing launch.");
|
|
372
|
+
try {
|
|
373
|
+
await rm(tempDirectoryPath, { recursive: true, force: true });
|
|
374
|
+
console.log("Deleted Temp directory.");
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
378
|
+
console.warn(`Failed to delete Temp directory: ${message}`);
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
await rm(lockfilePath, { force: true });
|
|
382
|
+
console.log("Deleted UnityLockfile.");
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
386
|
+
console.warn(`Failed to delete UnityLockfile: ${message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function hasBuildTargetArg(unityArgs) {
|
|
390
|
+
for (const arg of unityArgs) {
|
|
391
|
+
if (arg === "-buildTarget") {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
if (arg.startsWith("-buildTarget=")) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const EXCLUDED_DIR_NAMES = new Set([
|
|
401
|
+
"library",
|
|
402
|
+
"temp",
|
|
403
|
+
"logs",
|
|
404
|
+
"obj",
|
|
405
|
+
".git",
|
|
406
|
+
"node_modules",
|
|
407
|
+
".idea",
|
|
408
|
+
".vscode",
|
|
409
|
+
".vs",
|
|
410
|
+
]);
|
|
411
|
+
function isUnityProjectRoot(candidateDir) {
|
|
412
|
+
const versionFile = join(candidateDir, "ProjectSettings", "ProjectVersion.txt");
|
|
413
|
+
const hasVersion = existsSync(versionFile);
|
|
414
|
+
if (!hasVersion) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const libraryDir = join(candidateDir, "Library");
|
|
418
|
+
return existsSync(libraryDir);
|
|
419
|
+
}
|
|
420
|
+
function listSubdirectoriesSorted(dir) {
|
|
421
|
+
let entries = [];
|
|
422
|
+
try {
|
|
423
|
+
const dirents = readdirSync(dir, { withFileTypes: true });
|
|
424
|
+
const subdirs = dirents
|
|
425
|
+
.filter((d) => d.isDirectory())
|
|
426
|
+
.map((d) => d.name)
|
|
427
|
+
.filter((name) => !EXCLUDED_DIR_NAMES.has(name.toLocaleLowerCase()));
|
|
428
|
+
subdirs.sort((a, b) => a.localeCompare(b));
|
|
429
|
+
entries = subdirs.map((name) => join(dir, name));
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Ignore directories we cannot read
|
|
433
|
+
entries = [];
|
|
434
|
+
}
|
|
435
|
+
return entries;
|
|
436
|
+
}
|
|
437
|
+
function findUnityProjectBfs(rootDir, maxDepth) {
|
|
438
|
+
const queue = [];
|
|
439
|
+
let rootCanonical;
|
|
440
|
+
try {
|
|
441
|
+
rootCanonical = realpathSync(rootDir);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
rootCanonical = rootDir;
|
|
445
|
+
}
|
|
446
|
+
queue.push({ dir: rootCanonical, depth: 0 });
|
|
447
|
+
const visited = new Set([toComparablePath(normalizePath(rootCanonical))]);
|
|
448
|
+
while (queue.length > 0) {
|
|
449
|
+
const current = queue.shift();
|
|
450
|
+
if (!current) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const { dir, depth } = current;
|
|
454
|
+
if (isUnityProjectRoot(dir)) {
|
|
455
|
+
return normalizePath(dir);
|
|
456
|
+
}
|
|
457
|
+
const canDescend = maxDepth === -1 || depth < maxDepth;
|
|
458
|
+
if (!canDescend) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const children = listSubdirectoriesSorted(dir);
|
|
462
|
+
for (const child of children) {
|
|
463
|
+
let childCanonical = child;
|
|
464
|
+
try {
|
|
465
|
+
const stat = lstatSync(child);
|
|
466
|
+
if (stat.isSymbolicLink()) {
|
|
467
|
+
try {
|
|
468
|
+
childCanonical = realpathSync(child);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Broken symlink: skip
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const key = toComparablePath(normalizePath(childCanonical));
|
|
480
|
+
if (visited.has(key)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
visited.add(key);
|
|
484
|
+
queue.push({ dir: childCanonical, depth: depth + 1 });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
function launch(opts) {
|
|
490
|
+
const { projectPath, platform, unityArgs } = opts;
|
|
491
|
+
const unityVersion = getUnityVersion(projectPath);
|
|
492
|
+
const unityPath = getUnityPath(unityVersion);
|
|
493
|
+
console.log(`Detected Unity version: ${unityVersion}`);
|
|
494
|
+
if (!existsSync(unityPath)) {
|
|
495
|
+
console.error(`Error: Unity ${unityVersion} not found at ${unityPath}`);
|
|
496
|
+
console.error("Please install Unity through Unity Hub.");
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
console.log("Opening Unity...");
|
|
500
|
+
console.log(`Project Path: ${projectPath}`);
|
|
501
|
+
const args = ["-projectPath", projectPath];
|
|
502
|
+
const unityArgsContainBuildTarget = hasBuildTargetArg(unityArgs);
|
|
503
|
+
if (platform && platform.length > 0 && !unityArgsContainBuildTarget) {
|
|
504
|
+
args.push("-buildTarget", platform);
|
|
505
|
+
}
|
|
506
|
+
if (unityArgs.length > 0) {
|
|
507
|
+
args.push(...unityArgs);
|
|
508
|
+
}
|
|
509
|
+
const child = spawn(unityPath, args, { stdio: "ignore", detached: true });
|
|
510
|
+
child.unref();
|
|
511
|
+
}
|
|
512
|
+
async function main() {
|
|
513
|
+
const options = parseArgs(process.argv);
|
|
514
|
+
let resolvedProjectPath = options.projectPath;
|
|
515
|
+
if (!resolvedProjectPath) {
|
|
516
|
+
const searchRoot = process.cwd();
|
|
517
|
+
const depthInfo = options.searchMaxDepth === -1 ? "unlimited" : String(options.searchMaxDepth);
|
|
518
|
+
console.log(`No PROJECT_PATH provided. Searching under ${searchRoot} (max-depth: ${depthInfo})...`);
|
|
519
|
+
const found = findUnityProjectBfs(searchRoot, options.searchMaxDepth);
|
|
520
|
+
if (!found) {
|
|
521
|
+
console.error(`Error: Unity project not found under ${searchRoot}.`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
console.log(`Selected project: ${found}`);
|
|
526
|
+
resolvedProjectPath = found;
|
|
527
|
+
}
|
|
528
|
+
ensureProjectPath(resolvedProjectPath);
|
|
529
|
+
const runningProcess = await findRunningUnityProcess(resolvedProjectPath);
|
|
530
|
+
if (runningProcess) {
|
|
531
|
+
console.log(`Unity process already running for project: ${resolvedProjectPath} (PID: ${runningProcess.pid})`);
|
|
532
|
+
await focusUnityProcess(runningProcess.pid);
|
|
533
|
+
process.exit(0);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
await handleStaleLockfile(resolvedProjectPath);
|
|
537
|
+
const resolved = {
|
|
538
|
+
projectPath: resolvedProjectPath,
|
|
539
|
+
platform: options.platform,
|
|
540
|
+
unityArgs: options.unityArgs,
|
|
541
|
+
};
|
|
542
|
+
launch(resolved);
|
|
543
|
+
// Best-effort update of Unity Hub's lastModified timestamp.
|
|
544
|
+
try {
|
|
545
|
+
await updateLastModifiedIfExists(resolvedProjectPath, new Date());
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
549
|
+
console.warn(`Failed to update Unity Hub lastModified: ${message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
main().catch((error) => {
|
|
553
|
+
console.error(error);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
});
|
package/dist/unityHub.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const resolveUnityHubProjectFiles = () => {
|
|
4
|
+
if (process.platform === "darwin") {
|
|
5
|
+
const home = process.env.HOME;
|
|
6
|
+
if (!home) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const base = join(home, "Library", "Application Support", "UnityHub");
|
|
10
|
+
return [join(base, "projects-v1.json"), join(base, "projects.json")];
|
|
11
|
+
}
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
const appData = process.env.APPDATA;
|
|
14
|
+
if (!appData) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const base = join(appData, "UnityHub");
|
|
18
|
+
return [join(base, "projects-v1.json"), join(base, "projects.json")];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
};
|
|
22
|
+
const removeTrailingSeparators = (target) => {
|
|
23
|
+
let trimmed = target;
|
|
24
|
+
while (trimmed.length > 1 && (trimmed.endsWith("/") || trimmed.endsWith("\\"))) {
|
|
25
|
+
trimmed = trimmed.slice(0, -1);
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
};
|
|
29
|
+
const normalizePath = (target) => {
|
|
30
|
+
const resolvedPath = resolve(target);
|
|
31
|
+
return removeTrailingSeparators(resolvedPath);
|
|
32
|
+
};
|
|
33
|
+
const toComparablePath = (value) => {
|
|
34
|
+
return value.replace(/\\/g, "/").toLocaleLowerCase();
|
|
35
|
+
};
|
|
36
|
+
const pathsEqual = (left, right) => {
|
|
37
|
+
return toComparablePath(normalizePath(left)) === toComparablePath(normalizePath(right));
|
|
38
|
+
};
|
|
39
|
+
export const updateLastModifiedIfExists = async (projectPath, when) => {
|
|
40
|
+
const candidates = resolveUnityHubProjectFiles();
|
|
41
|
+
if (candidates.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Try primary then fallback only if read/parse fails
|
|
45
|
+
for (const path of candidates) {
|
|
46
|
+
let content;
|
|
47
|
+
let json;
|
|
48
|
+
try {
|
|
49
|
+
content = await readFile(path, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Try next candidate on read error
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
json = JSON.parse(content);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Try next candidate on parse error
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!json.data) {
|
|
63
|
+
// If file is readable but has no data, do not attempt fallback
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const projectKey = Object.keys(json.data).find((key) => {
|
|
67
|
+
const entryPath = json.data?.[key]?.path;
|
|
68
|
+
return entryPath ? pathsEqual(entryPath, projectPath) : false;
|
|
69
|
+
});
|
|
70
|
+
if (!projectKey) {
|
|
71
|
+
// Project not registered in Hub; do nothing
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const original = json.data[projectKey];
|
|
75
|
+
if (!original) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
json.data[projectKey] = {
|
|
79
|
+
...original,
|
|
80
|
+
lastModified: when.getTime(),
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
await writeFile(path, JSON.stringify(json, undefined, 2), "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Swallow write errors per requirement to not crash CLI
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launch-unity",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Open a Unity project with the matching Editor version (macOS/Windows)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/launch.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"launch-unity": "dist/launch.js"
|
|
9
|
-
"quit-unity": "dist/quit.js"
|
|
8
|
+
"launch-unity": "dist/launch.js"
|
|
10
9
|
},
|
|
11
10
|
"scripts": {
|
|
12
11
|
"build": "tsc -p tsconfig.json",
|