unity-hub-cli 0.1.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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +783 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Masamichi Hatayama
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# UnityHub CLI
|
|
2
|
+
|
|
3
|
+
A CLI tool that reads Unity Hub's `projects-v1.json`, displays projects in an Ink-based TUI, allows navigation with arrow keys/`j`/`k`, and launches Unity Editor by pressing Enter.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- macOS
|
|
8
|
+
- Node.js 20+
|
|
9
|
+
- Unity Hub (with `~/Library/Application Support/UnityHub/projects-v1.json` present)
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Development
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Build
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Run
|
|
27
|
+
|
|
28
|
+
After building, `dist/index.js` will be generated. You can also run it directly via npx.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx unity-hub-cli
|
|
32
|
+
# or
|
|
33
|
+
node dist/index.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
By default, the project list uses the Git repository root folder name when available.
|
|
37
|
+
|
|
38
|
+
### CLI Options
|
|
39
|
+
|
|
40
|
+
- `--no-git-root-name`: Display Unity project titles instead of Git repository root folder names.
|
|
41
|
+
- `--hide-branch`: Hide the Git branch column.
|
|
42
|
+
- `--hide-path`: Hide the project path column.
|
|
43
|
+
|
|
44
|
+
## Release Automation
|
|
45
|
+
|
|
46
|
+
Version and release management is automated using release-please and GitHub Actions.
|
|
47
|
+
|
|
48
|
+
- `.github/workflows/release-please.yml` runs on push to `main` or manual trigger
|
|
49
|
+
- The action references `release-please-config.json` and `.release-please-manifest.json` to create release PRs and tags
|
|
50
|
+
- When a PR is merged, GitHub Releases and changelog are automatically updated
|
|
51
|
+
- **npm publish is automated with provenance** for supply chain security
|
|
52
|
+
|
|
53
|
+
### Initial Setup Notes
|
|
54
|
+
|
|
55
|
+
- If existing releases are present, set the latest release commit in `bootstrap-sha` of `release-please-config.json`
|
|
56
|
+
- The workflow uses GitHub's `GITHUB_TOKEN` and operates with `contents`/`pull-requests` permissions
|
|
57
|
+
|
|
58
|
+
### Manual Execution
|
|
59
|
+
|
|
60
|
+
You can manually trigger the `release-please` workflow from the Actions tab by selecting `Run workflow`
|
|
61
|
+
|
|
62
|
+
## Security
|
|
63
|
+
|
|
64
|
+
This package implements multiple security measures to protect against supply chain attacks:
|
|
65
|
+
|
|
66
|
+
1. **Automated Publishing with Provenance**: All npm releases are published via GitHub Actions with `--provenance` flag, providing cryptographic proof of the build environment
|
|
67
|
+
2. **Minimal Dependencies**: Only 2 runtime dependencies (`ink` and `react`), both from highly trusted sources
|
|
68
|
+
3. **Locked Dependencies**: `package-lock.json` is committed to ensure reproducible builds
|
|
69
|
+
4. **Regular Security Audits**: Dependencies are regularly checked with `npm audit`
|
|
70
|
+
|
|
71
|
+
### Verifying Package Authenticity
|
|
72
|
+
|
|
73
|
+
You can verify the authenticity of published packages:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Check provenance information
|
|
77
|
+
npm view unity-hub-cli --json | jq .dist.attestations
|
|
78
|
+
|
|
79
|
+
# Verify package integrity
|
|
80
|
+
npm audit signatures
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Controls
|
|
84
|
+
|
|
85
|
+
- Arrow keys / `j` / `k`: Navigate selection
|
|
86
|
+
- `o`: Launch selected project in Unity
|
|
87
|
+
- Ctrl + C (twice): Exit
|
|
88
|
+
|
|
89
|
+
The display includes Git branch (if present), Unity version, project path, and last modified time (`lastModified`).
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
|
|
7
|
+
// src/application/usecases.ts
|
|
8
|
+
var ListProjectsUseCase = class {
|
|
9
|
+
constructor(unityHubProjectsReader, gitRepositoryInfoReader, unityProjectOptionsReader) {
|
|
10
|
+
this.unityHubProjectsReader = unityHubProjectsReader;
|
|
11
|
+
this.gitRepositoryInfoReader = gitRepositoryInfoReader;
|
|
12
|
+
this.unityProjectOptionsReader = unityProjectOptionsReader;
|
|
13
|
+
}
|
|
14
|
+
async execute() {
|
|
15
|
+
const projects = await this.unityHubProjectsReader.listProjects();
|
|
16
|
+
const repositoryInfoResults = await Promise.allSettled(
|
|
17
|
+
projects.map((project) => this.gitRepositoryInfoReader.readRepositoryInfo(project.path))
|
|
18
|
+
);
|
|
19
|
+
return projects.map((project, index) => {
|
|
20
|
+
const repositoryResult = repositoryInfoResults[index];
|
|
21
|
+
if (repositoryResult.status === "fulfilled") {
|
|
22
|
+
return { project, repository: repositoryResult.value ?? void 0 };
|
|
23
|
+
}
|
|
24
|
+
return { project };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var LaunchCancelledError = class extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super("Launch cancelled by user");
|
|
31
|
+
this.name = "LaunchCancelledError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var LaunchProjectUseCase = class {
|
|
35
|
+
constructor(editorPathResolver, processLauncher, unityHubProjectsReader, unityProjectOptionsReader, unityProcessLockChecker) {
|
|
36
|
+
this.editorPathResolver = editorPathResolver;
|
|
37
|
+
this.processLauncher = processLauncher;
|
|
38
|
+
this.unityHubProjectsReader = unityHubProjectsReader;
|
|
39
|
+
this.unityProjectOptionsReader = unityProjectOptionsReader;
|
|
40
|
+
this.unityProcessLockChecker = unityProcessLockChecker;
|
|
41
|
+
}
|
|
42
|
+
async execute(project) {
|
|
43
|
+
const lockDecision = await this.unityProcessLockChecker.check(project.path);
|
|
44
|
+
if (lockDecision === "skip") {
|
|
45
|
+
throw new LaunchCancelledError();
|
|
46
|
+
}
|
|
47
|
+
const editorPath = await this.editorPathResolver.resolve(project.version);
|
|
48
|
+
const extraArgs = await this.unityProjectOptionsReader.readCliArgs(project.path);
|
|
49
|
+
const launchArgs = ["-projectPath", project.path, ...extraArgs];
|
|
50
|
+
await this.processLauncher.launch(editorPath, launchArgs, {
|
|
51
|
+
detached: true
|
|
52
|
+
});
|
|
53
|
+
await this.unityHubProjectsReader.updateLastModified(project.path, /* @__PURE__ */ new Date());
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/infrastructure/editor.ts
|
|
58
|
+
import { constants } from "fs";
|
|
59
|
+
import { access } from "fs/promises";
|
|
60
|
+
import { join } from "path";
|
|
61
|
+
var UNITY_EDITOR_BASE = "/Applications/Unity/Hub/Editor";
|
|
62
|
+
var UNITY_BINARY_PATH = "Unity.app/Contents/MacOS/Unity";
|
|
63
|
+
var MacEditorPathResolver = class {
|
|
64
|
+
async resolve(version) {
|
|
65
|
+
const editorPath = join(UNITY_EDITOR_BASE, version.value, UNITY_BINARY_PATH);
|
|
66
|
+
try {
|
|
67
|
+
await access(editorPath, constants.X_OK);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`\u5BFE\u5FDC\u3059\u308BUnity Editor\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${version.value}\uFF09`);
|
|
70
|
+
}
|
|
71
|
+
return editorPath;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/infrastructure/git.ts
|
|
76
|
+
import { readFile, stat } from "fs/promises";
|
|
77
|
+
import { dirname, join as join2, resolve } from "path";
|
|
78
|
+
var HEAD_FILE = "HEAD";
|
|
79
|
+
var GIT_DIR = ".git";
|
|
80
|
+
var MAX_ASCENT = 50;
|
|
81
|
+
var isDirectory = async (path) => {
|
|
82
|
+
try {
|
|
83
|
+
const stats = await stat(path);
|
|
84
|
+
return stats.isDirectory();
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var isFile = async (path) => {
|
|
90
|
+
try {
|
|
91
|
+
const stats = await stat(path);
|
|
92
|
+
return stats.isFile();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var findGitDir = async (start) => {
|
|
98
|
+
let current = resolve(start);
|
|
99
|
+
for (let depth = 0; depth < MAX_ASCENT; depth += 1) {
|
|
100
|
+
const candidate = join2(current, GIT_DIR);
|
|
101
|
+
if (await isDirectory(candidate)) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
if (await isFile(candidate)) {
|
|
105
|
+
const content = await readFile(candidate, "utf8");
|
|
106
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
107
|
+
if (match) {
|
|
108
|
+
const gitdirPath = match[1]?.trim();
|
|
109
|
+
if (gitdirPath) {
|
|
110
|
+
const target = gitdirPath.startsWith("/") ? gitdirPath : resolve(current, gitdirPath);
|
|
111
|
+
return target;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const parent = dirname(current);
|
|
116
|
+
if (parent === current) {
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
current = parent;
|
|
120
|
+
}
|
|
121
|
+
return void 0;
|
|
122
|
+
};
|
|
123
|
+
var parseHead = (content) => {
|
|
124
|
+
const trimmed = content.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
if (trimmed.startsWith("ref:")) {
|
|
129
|
+
const refName = trimmed.split(" ")[1]?.trim();
|
|
130
|
+
if (refName?.startsWith("refs/heads/")) {
|
|
131
|
+
return { type: "branch", name: refName.replace("refs/heads/", "") };
|
|
132
|
+
}
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
return { type: "detached", sha: trimmed.slice(0, 7) };
|
|
136
|
+
};
|
|
137
|
+
var GitRepositoryInfoReader = class {
|
|
138
|
+
async readRepositoryInfo(projectPath) {
|
|
139
|
+
const gitDir = await findGitDir(projectPath);
|
|
140
|
+
if (!gitDir) {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const headPath = join2(gitDir, HEAD_FILE);
|
|
145
|
+
const content = await readFile(headPath, "utf8");
|
|
146
|
+
const branch = parseHead(content);
|
|
147
|
+
const root = dirname(gitDir);
|
|
148
|
+
return { branch, root };
|
|
149
|
+
} catch {
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/infrastructure/process.ts
|
|
156
|
+
import { spawn } from "child_process";
|
|
157
|
+
var NodeProcessLauncher = class {
|
|
158
|
+
async launch(command, args, options) {
|
|
159
|
+
const detached = options?.detached ?? false;
|
|
160
|
+
await new Promise((resolve2, reject) => {
|
|
161
|
+
const child = spawn(command, args, {
|
|
162
|
+
detached,
|
|
163
|
+
stdio: "ignore"
|
|
164
|
+
});
|
|
165
|
+
const handleError = (error) => {
|
|
166
|
+
child.off("spawn", handleSpawn);
|
|
167
|
+
reject(error);
|
|
168
|
+
};
|
|
169
|
+
const handleSpawn = () => {
|
|
170
|
+
child.off("error", handleError);
|
|
171
|
+
child.unref();
|
|
172
|
+
resolve2();
|
|
173
|
+
};
|
|
174
|
+
child.once("error", handleError);
|
|
175
|
+
child.once("spawn", handleSpawn);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/infrastructure/unityhub.ts
|
|
181
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
182
|
+
import { basename } from "path";
|
|
183
|
+
var HUB_PROJECTS_PATH = `${process.env.HOME ?? ""}/Library/Application Support/UnityHub/projects-v1.json`;
|
|
184
|
+
var schemaVersion = "v1";
|
|
185
|
+
var toUnityProject = (entry) => {
|
|
186
|
+
const safePath = entry.path;
|
|
187
|
+
if (!safePath) {
|
|
188
|
+
throw new Error("Unity Hub entry is missing project path");
|
|
189
|
+
}
|
|
190
|
+
const version = entry.version;
|
|
191
|
+
if (!version) {
|
|
192
|
+
throw new Error(`Unity Hub entry ${safePath} is missing version`);
|
|
193
|
+
}
|
|
194
|
+
const lastModified = typeof entry.lastModified === "number" ? new Date(entry.lastModified) : void 0;
|
|
195
|
+
return {
|
|
196
|
+
id: safePath,
|
|
197
|
+
title: entry.title?.trim() || basename(safePath),
|
|
198
|
+
path: safePath,
|
|
199
|
+
version: { value: version },
|
|
200
|
+
lastModified,
|
|
201
|
+
favorite: entry.isFavorite === true
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
var normalizeValue = (value) => value.toLocaleLowerCase();
|
|
205
|
+
var sortByFavoriteThenLastModified = (projects) => {
|
|
206
|
+
return [...projects].sort((a, b) => {
|
|
207
|
+
const favoriteRankA = a.favorite ? 0 : 1;
|
|
208
|
+
const favoriteRankB = b.favorite ? 0 : 1;
|
|
209
|
+
if (favoriteRankA !== favoriteRankB) {
|
|
210
|
+
return favoriteRankA - favoriteRankB;
|
|
211
|
+
}
|
|
212
|
+
const fallbackTime = 0;
|
|
213
|
+
const timeA = a.lastModified?.getTime() ?? fallbackTime;
|
|
214
|
+
const timeB = b.lastModified?.getTime() ?? fallbackTime;
|
|
215
|
+
if (timeA === timeB) {
|
|
216
|
+
const titleA = normalizeValue(a.title);
|
|
217
|
+
const titleB = normalizeValue(b.title);
|
|
218
|
+
if (titleA === titleB) {
|
|
219
|
+
return normalizeValue(a.path).localeCompare(normalizeValue(b.path));
|
|
220
|
+
}
|
|
221
|
+
return titleA.localeCompare(titleB);
|
|
222
|
+
}
|
|
223
|
+
return timeB - timeA;
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
var UnityHubProjectsReader = class {
|
|
227
|
+
async listProjects() {
|
|
228
|
+
let content;
|
|
229
|
+
try {
|
|
230
|
+
content = await readFile2(HUB_PROJECTS_PATH, "utf8");
|
|
231
|
+
} catch {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${HUB_PROJECTS_PATH}\uFF09`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
let json;
|
|
237
|
+
try {
|
|
238
|
+
json = JSON.parse(content);
|
|
239
|
+
} catch {
|
|
240
|
+
throw new Error("Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\uFF08\u6A29\u9650/\u5F62\u5F0F\u30A8\u30E9\u30FC\uFF09");
|
|
241
|
+
}
|
|
242
|
+
if (json.schema_version && json.schema_version !== schemaVersion) {
|
|
243
|
+
throw new Error(`\u672A\u5BFE\u5FDC\u306Eschema_version\u3067\u3059\uFF08${json.schema_version}\uFF09`);
|
|
244
|
+
}
|
|
245
|
+
const entries = Object.values(json.data ?? {});
|
|
246
|
+
if (entries.length === 0) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const projects = entries.map(toUnityProject);
|
|
250
|
+
return sortByFavoriteThenLastModified(projects);
|
|
251
|
+
}
|
|
252
|
+
async updateLastModified(projectPath, date) {
|
|
253
|
+
let content;
|
|
254
|
+
try {
|
|
255
|
+
content = await readFile2(HUB_PROJECTS_PATH, "utf8");
|
|
256
|
+
} catch {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\uFF08${HUB_PROJECTS_PATH}\uFF09`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
let json;
|
|
262
|
+
try {
|
|
263
|
+
json = JSON.parse(content);
|
|
264
|
+
} catch {
|
|
265
|
+
throw new Error("Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u4E00\u89A7\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\uFF08\u6A29\u9650/\u5F62\u5F0F\u30A8\u30E9\u30FC\uFF09");
|
|
266
|
+
}
|
|
267
|
+
if (!json.data) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const projectKey = Object.keys(json.data).find((key) => json.data?.[key]?.path === projectPath);
|
|
271
|
+
if (!projectKey) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const original = json.data[projectKey];
|
|
275
|
+
if (!original) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
json.data[projectKey] = {
|
|
279
|
+
...original,
|
|
280
|
+
lastModified: date.getTime()
|
|
281
|
+
};
|
|
282
|
+
await writeFile(HUB_PROJECTS_PATH, JSON.stringify(json, void 0, 2), "utf8");
|
|
283
|
+
}
|
|
284
|
+
async readCliArgs(projectPath) {
|
|
285
|
+
const infoPath = `${process.env.HOME ?? ""}/Library/Application Support/UnityHub/projectsInfo.json`;
|
|
286
|
+
let content;
|
|
287
|
+
try {
|
|
288
|
+
content = await readFile2(infoPath, "utf8");
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
let json;
|
|
293
|
+
try {
|
|
294
|
+
json = JSON.parse(content);
|
|
295
|
+
} catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
const entry = json[projectPath];
|
|
299
|
+
if (!entry?.cliArgs) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
const tokens = entry.cliArgs.match(/(?:"[^"]*"|'[^']*'|[^\s"']+)/g);
|
|
303
|
+
if (!tokens) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
return tokens.map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/infrastructure/unityLock.ts
|
|
311
|
+
import { constants as constants2, createReadStream, createWriteStream } from "fs";
|
|
312
|
+
import { access as access2, rm } from "fs/promises";
|
|
313
|
+
import { join as join3 } from "path";
|
|
314
|
+
import readline from "readline";
|
|
315
|
+
var RAW_PROMPT_MESSAGE = "Delete UnityLockfile and continue? Type 'y' to continue; anything else aborts: ";
|
|
316
|
+
var isRawModeSupported = () => {
|
|
317
|
+
const stdin = process.stdin;
|
|
318
|
+
return Boolean(stdin?.isTTY && typeof stdin.setRawMode === "function" && process.stdout.isTTY);
|
|
319
|
+
};
|
|
320
|
+
var createPromptInterface = () => {
|
|
321
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
322
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
323
|
+
const close = () => rl.close();
|
|
324
|
+
return { rl, close };
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
if (process.platform === "win32") {
|
|
328
|
+
const inCandidates = ["\\\\.\\CONIN$", "CONIN$"];
|
|
329
|
+
const outCandidates = ["\\\\.\\CONOUT$", "CONOUT$"];
|
|
330
|
+
for (const inPath of inCandidates) {
|
|
331
|
+
for (const outPath of outCandidates) {
|
|
332
|
+
try {
|
|
333
|
+
const input = createReadStream(inPath);
|
|
334
|
+
const output = createWriteStream(outPath);
|
|
335
|
+
const rl = readline.createInterface({ input, output });
|
|
336
|
+
const close = () => {
|
|
337
|
+
rl.close();
|
|
338
|
+
input.destroy();
|
|
339
|
+
output.end();
|
|
340
|
+
};
|
|
341
|
+
return { rl, close };
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
const input = createReadStream("/dev/tty");
|
|
349
|
+
const output = createWriteStream("/dev/tty");
|
|
350
|
+
const rl = readline.createInterface({ input, output });
|
|
351
|
+
const close = () => {
|
|
352
|
+
rl.close();
|
|
353
|
+
input.destroy();
|
|
354
|
+
output.end();
|
|
355
|
+
};
|
|
356
|
+
return { rl, close };
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
return void 0;
|
|
362
|
+
};
|
|
363
|
+
var promptYesNoSingleKey = async () => {
|
|
364
|
+
const stdin = process.stdin;
|
|
365
|
+
const supportsRaw = isRawModeSupported();
|
|
366
|
+
const previousRaw = supportsRaw ? stdin.isRaw === true : false;
|
|
367
|
+
const wasPaused = stdin.isPaused();
|
|
368
|
+
return await new Promise((resolve2) => {
|
|
369
|
+
const cleanup = () => {
|
|
370
|
+
stdin.removeListener("data", handleData);
|
|
371
|
+
if (wasPaused) {
|
|
372
|
+
stdin.pause();
|
|
373
|
+
}
|
|
374
|
+
if (supportsRaw) {
|
|
375
|
+
stdin.setRawMode(previousRaw);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const handleData = (data) => {
|
|
379
|
+
const char = data.toString();
|
|
380
|
+
const firstByte = data[0] ?? 0;
|
|
381
|
+
let result = false;
|
|
382
|
+
if (char === "y") {
|
|
383
|
+
result = true;
|
|
384
|
+
} else if (char === "n" || char === "N" || firstByte === 3 || firstByte === 27 || firstByte === 13) {
|
|
385
|
+
result = false;
|
|
386
|
+
} else {
|
|
387
|
+
result = false;
|
|
388
|
+
}
|
|
389
|
+
process.stdout.write("\n");
|
|
390
|
+
cleanup();
|
|
391
|
+
resolve2(result);
|
|
392
|
+
};
|
|
393
|
+
process.stdout.write(RAW_PROMPT_MESSAGE);
|
|
394
|
+
if (supportsRaw) {
|
|
395
|
+
stdin.setRawMode(true);
|
|
396
|
+
}
|
|
397
|
+
if (wasPaused) {
|
|
398
|
+
stdin.resume();
|
|
399
|
+
}
|
|
400
|
+
stdin.once("data", handleData);
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
var promptYesNoLine = async () => {
|
|
404
|
+
const prompt = createPromptInterface();
|
|
405
|
+
if (!prompt) {
|
|
406
|
+
console.error("UnityLockfile exists. No interactive console available for confirmation.");
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const confirmed = await new Promise((resolve2) => {
|
|
410
|
+
prompt.rl.question(RAW_PROMPT_MESSAGE, (answer) => {
|
|
411
|
+
resolve2(answer.trim() === "y");
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
prompt.close();
|
|
415
|
+
return confirmed;
|
|
416
|
+
};
|
|
417
|
+
var pathExists = async (target) => {
|
|
418
|
+
try {
|
|
419
|
+
await access2(target, constants2.F_OK);
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var UnityLockChecker = class {
|
|
426
|
+
async check(projectPath) {
|
|
427
|
+
const lockfilePath = join3(projectPath, "Temp", "UnityLockfile");
|
|
428
|
+
const hasLockfile = await pathExists(lockfilePath);
|
|
429
|
+
if (!hasLockfile) {
|
|
430
|
+
return "allow";
|
|
431
|
+
}
|
|
432
|
+
console.log(`UnityLockfile found: ${lockfilePath}`);
|
|
433
|
+
console.log("Another Unity process may be using this project.");
|
|
434
|
+
const confirmed = isRawModeSupported() ? await promptYesNoSingleKey() : await promptYesNoLine();
|
|
435
|
+
if (!confirmed) {
|
|
436
|
+
console.log("Aborted by user.");
|
|
437
|
+
return "skip";
|
|
438
|
+
}
|
|
439
|
+
await rm(lockfilePath, { force: true });
|
|
440
|
+
console.log("Deleted UnityLockfile. Continuing launch.");
|
|
441
|
+
return "allow";
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// src/presentation/App.tsx
|
|
446
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
447
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
448
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
449
|
+
var extractRootFolder = (repository) => {
|
|
450
|
+
if (!repository?.root) {
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
const segments = repository.root.split("/").filter((segment) => segment.length > 0);
|
|
454
|
+
if (segments.length === 0) {
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
return segments[segments.length - 1];
|
|
458
|
+
};
|
|
459
|
+
var formatProjectName = (project, repository, useGitRootName) => {
|
|
460
|
+
if (!useGitRootName) {
|
|
461
|
+
return `${project.title} (${project.version.value})`;
|
|
462
|
+
}
|
|
463
|
+
const rootFolder = extractRootFolder(repository);
|
|
464
|
+
if (!rootFolder) {
|
|
465
|
+
return `${project.title} (${project.version.value})`;
|
|
466
|
+
}
|
|
467
|
+
return `${rootFolder} (${project.version.value})`;
|
|
468
|
+
};
|
|
469
|
+
var formatBranch = (branch) => {
|
|
470
|
+
if (!branch) {
|
|
471
|
+
return "-";
|
|
472
|
+
}
|
|
473
|
+
if (branch.type === "branch") {
|
|
474
|
+
return branch.name;
|
|
475
|
+
}
|
|
476
|
+
return `detached@${branch.sha}`;
|
|
477
|
+
};
|
|
478
|
+
var homeDirectory = process.env.HOME ?? "";
|
|
479
|
+
var homePrefix = homeDirectory ? `${homeDirectory}/` : "";
|
|
480
|
+
var minimumVisibleProjectCount = 4;
|
|
481
|
+
var defaultHintMessage = "Move with arrows or j/k \xB7 Launch with o \xB7 Exit with Ctrl+C twice";
|
|
482
|
+
var PROJECT_COLOR = "#abd8e7";
|
|
483
|
+
var BRANCH_COLOR = "#e3839c";
|
|
484
|
+
var PATH_COLOR = "#719bd8";
|
|
485
|
+
var shortenHomePath = (targetPath) => {
|
|
486
|
+
if (!homeDirectory) {
|
|
487
|
+
return targetPath;
|
|
488
|
+
}
|
|
489
|
+
if (targetPath === homeDirectory) {
|
|
490
|
+
return "~";
|
|
491
|
+
}
|
|
492
|
+
if (homePrefix && targetPath.startsWith(homePrefix)) {
|
|
493
|
+
return `~/${targetPath.slice(homePrefix.length)}`;
|
|
494
|
+
}
|
|
495
|
+
return targetPath;
|
|
496
|
+
};
|
|
497
|
+
var App = ({
|
|
498
|
+
projects,
|
|
499
|
+
onLaunch,
|
|
500
|
+
useGitRootName = true,
|
|
501
|
+
showBranch = true,
|
|
502
|
+
showPath = true
|
|
503
|
+
}) => {
|
|
504
|
+
const { exit } = useApp();
|
|
505
|
+
const { stdout } = useStdout();
|
|
506
|
+
const [visibleCount, setVisibleCount] = useState(minimumVisibleProjectCount);
|
|
507
|
+
const [index, setIndex] = useState(0);
|
|
508
|
+
const [hint, setHint] = useState("Move with j/k \xB7 Launch with o \xB7 Exit with Ctrl+C twice");
|
|
509
|
+
const [pendingExit, setPendingExit] = useState(false);
|
|
510
|
+
const linesPerProject = (showBranch ? 1 : 0) + (showPath ? 1 : 0) + 2;
|
|
511
|
+
const sortedProjects = useMemo(() => {
|
|
512
|
+
const fallbackTime = 0;
|
|
513
|
+
const toSortKey = (view) => {
|
|
514
|
+
if (useGitRootName) {
|
|
515
|
+
const rootName = extractRootFolder(view.repository);
|
|
516
|
+
if (rootName) {
|
|
517
|
+
return rootName.toLocaleLowerCase();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return view.project.title.toLocaleLowerCase();
|
|
521
|
+
};
|
|
522
|
+
const toTieBreaker = (view) => view.project.path.toLocaleLowerCase();
|
|
523
|
+
return [...projects].sort((a, b) => {
|
|
524
|
+
if (a.project.favorite !== b.project.favorite) {
|
|
525
|
+
return a.project.favorite ? -1 : 1;
|
|
526
|
+
}
|
|
527
|
+
const timeA = a.project.lastModified?.getTime() ?? fallbackTime;
|
|
528
|
+
const timeB = b.project.lastModified?.getTime() ?? fallbackTime;
|
|
529
|
+
if (timeA !== timeB) {
|
|
530
|
+
return timeB - timeA;
|
|
531
|
+
}
|
|
532
|
+
const keyA = toSortKey(a);
|
|
533
|
+
const keyB = toSortKey(b);
|
|
534
|
+
if (keyA === keyB) {
|
|
535
|
+
return toTieBreaker(a).localeCompare(toTieBreaker(b));
|
|
536
|
+
}
|
|
537
|
+
return keyA.localeCompare(keyB);
|
|
538
|
+
});
|
|
539
|
+
}, [projects, useGitRootName]);
|
|
540
|
+
useEffect(() => {
|
|
541
|
+
const handleSigint = () => {
|
|
542
|
+
if (!pendingExit) {
|
|
543
|
+
setPendingExit(true);
|
|
544
|
+
setHint("Press Ctrl+C again to exit");
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
setPendingExit(false);
|
|
547
|
+
setHint(defaultHintMessage);
|
|
548
|
+
}, 2e3);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
exit();
|
|
552
|
+
};
|
|
553
|
+
process.on("SIGINT", handleSigint);
|
|
554
|
+
return () => {
|
|
555
|
+
process.off("SIGINT", handleSigint);
|
|
556
|
+
};
|
|
557
|
+
}, [exit, pendingExit]);
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
const updateVisibleCount = () => {
|
|
560
|
+
if (!stdout || typeof stdout.columns !== "number" || typeof stdout.rows !== "number") {
|
|
561
|
+
setVisibleCount(minimumVisibleProjectCount);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const reservedRows = 6;
|
|
565
|
+
const availableRows = stdout.rows - reservedRows;
|
|
566
|
+
const rowsPerProject = Math.max(linesPerProject, 1);
|
|
567
|
+
const calculatedCount = Math.max(
|
|
568
|
+
minimumVisibleProjectCount,
|
|
569
|
+
Math.floor(availableRows / rowsPerProject)
|
|
570
|
+
);
|
|
571
|
+
setVisibleCount(calculatedCount);
|
|
572
|
+
};
|
|
573
|
+
updateVisibleCount();
|
|
574
|
+
stdout?.on("resize", updateVisibleCount);
|
|
575
|
+
return () => {
|
|
576
|
+
stdout?.off("resize", updateVisibleCount);
|
|
577
|
+
};
|
|
578
|
+
}, [linesPerProject, stdout]);
|
|
579
|
+
const move = useCallback(
|
|
580
|
+
(delta) => {
|
|
581
|
+
setIndex((prev) => {
|
|
582
|
+
if (sortedProjects.length === 0) {
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
let next = prev + delta;
|
|
586
|
+
if (next < 0) {
|
|
587
|
+
next = 0;
|
|
588
|
+
}
|
|
589
|
+
if (next >= sortedProjects.length) {
|
|
590
|
+
next = sortedProjects.length - 1;
|
|
591
|
+
}
|
|
592
|
+
return next;
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
[sortedProjects.length]
|
|
596
|
+
);
|
|
597
|
+
const launchSelected = useCallback(async () => {
|
|
598
|
+
const project = sortedProjects[index]?.project;
|
|
599
|
+
if (!project) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
await onLaunch(project);
|
|
604
|
+
setHint(`Launched Unity: ${project.title}`);
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
setHint(defaultHintMessage);
|
|
607
|
+
}, 2e3);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (error instanceof LaunchCancelledError) {
|
|
610
|
+
setHint("Launch cancelled");
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
setHint(defaultHintMessage);
|
|
613
|
+
}, 3e3);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
setHint(`Failed to launch: ${message}`);
|
|
618
|
+
setTimeout(() => {
|
|
619
|
+
setHint(defaultHintMessage);
|
|
620
|
+
}, 3e3);
|
|
621
|
+
}
|
|
622
|
+
}, [index, onLaunch, sortedProjects]);
|
|
623
|
+
useInput((input, key) => {
|
|
624
|
+
if (input === "j" || key.downArrow) {
|
|
625
|
+
move(1);
|
|
626
|
+
}
|
|
627
|
+
if (input === "k" || key.upArrow) {
|
|
628
|
+
move(-1);
|
|
629
|
+
}
|
|
630
|
+
if (input === "o") {
|
|
631
|
+
void launchSelected();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
const { startIndex, visibleProjects } = useMemo(() => {
|
|
635
|
+
const limit = Math.max(minimumVisibleProjectCount, visibleCount);
|
|
636
|
+
if (sortedProjects.length <= limit) {
|
|
637
|
+
return {
|
|
638
|
+
startIndex: 0,
|
|
639
|
+
endIndex: sortedProjects.length,
|
|
640
|
+
visibleProjects: sortedProjects
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const halfWindow = Math.floor(limit / 2);
|
|
644
|
+
let start = index - halfWindow;
|
|
645
|
+
let end = index + halfWindow + limit % 2;
|
|
646
|
+
if (start < 0) {
|
|
647
|
+
start = 0;
|
|
648
|
+
end = limit;
|
|
649
|
+
}
|
|
650
|
+
if (end > sortedProjects.length) {
|
|
651
|
+
end = sortedProjects.length;
|
|
652
|
+
start = Math.max(0, end - limit);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
startIndex: start,
|
|
656
|
+
visibleProjects: sortedProjects.slice(start, end)
|
|
657
|
+
};
|
|
658
|
+
}, [index, sortedProjects, visibleCount]);
|
|
659
|
+
const scrollbarChars = useMemo(() => {
|
|
660
|
+
const totalProjects = projects.length;
|
|
661
|
+
const totalLines = totalProjects * linesPerProject;
|
|
662
|
+
const windowProjects = visibleProjects.length;
|
|
663
|
+
const visibleLines = windowProjects * linesPerProject;
|
|
664
|
+
if (totalLines === 0 || visibleLines === 0) {
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
if (totalLines <= visibleLines) {
|
|
668
|
+
return Array.from({ length: visibleLines }, () => "\u2588");
|
|
669
|
+
}
|
|
670
|
+
const trackLength = visibleLines;
|
|
671
|
+
const sliderSize = Math.max(1, Math.round(visibleLines / totalLines * trackLength));
|
|
672
|
+
const maxSliderStart = Math.max(0, trackLength - sliderSize);
|
|
673
|
+
const topLine = startIndex * linesPerProject;
|
|
674
|
+
const denominator = Math.max(1, totalLines - visibleLines);
|
|
675
|
+
const sliderStart = Math.min(
|
|
676
|
+
maxSliderStart,
|
|
677
|
+
Math.round(topLine / denominator * maxSliderStart)
|
|
678
|
+
);
|
|
679
|
+
return Array.from({ length: trackLength }, (_, position) => {
|
|
680
|
+
if (position >= sliderStart && position < sliderStart + sliderSize) {
|
|
681
|
+
return "\u2588";
|
|
682
|
+
}
|
|
683
|
+
return "|";
|
|
684
|
+
});
|
|
685
|
+
}, [linesPerProject, projects.length, startIndex, visibleProjects]);
|
|
686
|
+
const rows = useMemo(() => {
|
|
687
|
+
return visibleProjects.map(({ project, repository }, offset) => {
|
|
688
|
+
const rowIndex = startIndex + offset;
|
|
689
|
+
const isSelected = rowIndex === index;
|
|
690
|
+
const arrow = isSelected ? ">" : " ";
|
|
691
|
+
const titleLine = formatProjectName(project, repository, useGitRootName);
|
|
692
|
+
const pathLine = shortenHomePath(project.path);
|
|
693
|
+
const branchLine = formatBranch(repository?.branch);
|
|
694
|
+
const baseScrollbarIndex = offset * linesPerProject;
|
|
695
|
+
const titleScrollbar = scrollbarChars[baseScrollbarIndex] ?? " ";
|
|
696
|
+
const branchScrollbar = showBranch ? scrollbarChars[baseScrollbarIndex + 1] ?? " " : " ";
|
|
697
|
+
const pathScrollbar = showPath ? scrollbarChars[baseScrollbarIndex + 1 + (showBranch ? 1 : 0)] ?? " " : " ";
|
|
698
|
+
const spacerScrollbar = scrollbarChars[baseScrollbarIndex + linesPerProject - 1] ?? " ";
|
|
699
|
+
const versionStart = titleLine.indexOf("(");
|
|
700
|
+
const versionText = versionStart >= 0 ? titleLine.slice(versionStart) : "";
|
|
701
|
+
const nameText = versionStart >= 0 ? titleLine.slice(0, versionStart).trimEnd() : titleLine;
|
|
702
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
|
|
703
|
+
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [
|
|
704
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
705
|
+
/* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PROJECT_COLOR, bold: true, children: [
|
|
706
|
+
arrow,
|
|
707
|
+
" ",
|
|
708
|
+
nameText
|
|
709
|
+
] }),
|
|
710
|
+
versionText ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
|
|
711
|
+
" ",
|
|
712
|
+
versionText
|
|
713
|
+
] }) : null
|
|
714
|
+
] }),
|
|
715
|
+
showBranch ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : BRANCH_COLOR, children: [
|
|
716
|
+
" ",
|
|
717
|
+
branchLine
|
|
718
|
+
] }) : null,
|
|
719
|
+
showPath ? /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : PATH_COLOR, children: [
|
|
720
|
+
" ",
|
|
721
|
+
pathLine
|
|
722
|
+
] }) : null,
|
|
723
|
+
/* @__PURE__ */ jsx(Text, { children: " " })
|
|
724
|
+
] }),
|
|
725
|
+
/* @__PURE__ */ jsxs(Box, { marginLeft: 1, width: 1, flexDirection: "column", alignItems: "center", children: [
|
|
726
|
+
/* @__PURE__ */ jsx(Text, { children: titleScrollbar }),
|
|
727
|
+
showBranch ? /* @__PURE__ */ jsx(Text, { children: branchScrollbar }) : null,
|
|
728
|
+
showPath ? /* @__PURE__ */ jsx(Text, { children: pathScrollbar }) : null,
|
|
729
|
+
/* @__PURE__ */ jsx(Text, { children: spacerScrollbar })
|
|
730
|
+
] })
|
|
731
|
+
] }, project.id);
|
|
732
|
+
});
|
|
733
|
+
}, [index, scrollbarChars, showBranch, showPath, startIndex, useGitRootName, visibleProjects]);
|
|
734
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
735
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", children: rows.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "Unity Hub\u306E\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F" }) : rows }),
|
|
736
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: hint }) })
|
|
737
|
+
] });
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/index.tsx
|
|
741
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
742
|
+
var bootstrap = async () => {
|
|
743
|
+
const unityHubReader = new UnityHubProjectsReader();
|
|
744
|
+
const gitRepositoryInfoReader = new GitRepositoryInfoReader();
|
|
745
|
+
const listProjectsUseCase = new ListProjectsUseCase(
|
|
746
|
+
unityHubReader,
|
|
747
|
+
gitRepositoryInfoReader,
|
|
748
|
+
unityHubReader
|
|
749
|
+
);
|
|
750
|
+
const editorPathResolver = new MacEditorPathResolver();
|
|
751
|
+
const processLauncher = new NodeProcessLauncher();
|
|
752
|
+
const lockChecker = new UnityLockChecker();
|
|
753
|
+
const launchProjectUseCase = new LaunchProjectUseCase(
|
|
754
|
+
editorPathResolver,
|
|
755
|
+
processLauncher,
|
|
756
|
+
unityHubReader,
|
|
757
|
+
unityHubReader,
|
|
758
|
+
lockChecker
|
|
759
|
+
);
|
|
760
|
+
const useGitRootName = !process2.argv.includes("--no-git-root-name");
|
|
761
|
+
const showBranch = !process2.argv.includes("--hide-branch");
|
|
762
|
+
const showPath = !process2.argv.includes("--hide-path");
|
|
763
|
+
try {
|
|
764
|
+
const projects = await listProjectsUseCase.execute();
|
|
765
|
+
render(
|
|
766
|
+
/* @__PURE__ */ jsx2(
|
|
767
|
+
App,
|
|
768
|
+
{
|
|
769
|
+
projects,
|
|
770
|
+
onLaunch: (project) => launchProjectUseCase.execute(project),
|
|
771
|
+
useGitRootName,
|
|
772
|
+
showBranch,
|
|
773
|
+
showPath
|
|
774
|
+
}
|
|
775
|
+
)
|
|
776
|
+
);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
779
|
+
console.error(message);
|
|
780
|
+
process2.exitCode = 1;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
await bootstrap();
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "unity-hub-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI tool that reads Unity Hub's projects and launches Unity Editor with an interactive TUI",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"dev": "tsx src/index.ts",
|
|
9
|
+
"build": "tsup",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
12
|
+
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"typecheck": "tsc -noEmit"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/hatayama/UnityHubCli.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"unity",
|
|
22
|
+
"unity-hub",
|
|
23
|
+
"cli",
|
|
24
|
+
"tui",
|
|
25
|
+
"ink",
|
|
26
|
+
"terminal",
|
|
27
|
+
"interactive"
|
|
28
|
+
],
|
|
29
|
+
"author": "masamichi hatayama",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"type": "module",
|
|
32
|
+
"bin": {
|
|
33
|
+
"unity-hub-cli": "dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"ink": "^4.4.1",
|
|
40
|
+
"react": "^18.3.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.19.20",
|
|
44
|
+
"@types/react": "^18.3.26",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
46
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
47
|
+
"eslint": "^8.57.0",
|
|
48
|
+
"eslint-config-prettier": "^9.1.2",
|
|
49
|
+
"eslint-import-resolver-typescript": "^3.10.1",
|
|
50
|
+
"eslint-plugin-import": "^2.32.0",
|
|
51
|
+
"prettier": "^3.6.2",
|
|
52
|
+
"tsup": "^8.5.0",
|
|
53
|
+
"tsx": "^4.20.6",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
}
|
|
56
|
+
}
|