unity-mcp-cli 0.83.1 → 0.84.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.
@@ -0,0 +1,261 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { createHash, randomUUID } from 'crypto';
7
+ import { execFileSync } from 'child_process';
8
+ import { MACHINE_STORE_DIR_NAME } from './machine-credentials.js';
9
+ import { resolveHostRid } from './rid.js';
10
+ import { DEFAULT_SERVER_VERSION, SERVER_EXECUTABLE_NAME, SERVER_RELEASE_REPO, serverReleaseTag, } from './server-version.js';
11
+ /**
12
+ * The CLI's MANAGED server directory — the machine-shared home the `install-plugin --with-server`
13
+ * download lands in, and the directory `configure --agent` proxies to. Lives beside the shared
14
+ * machine credential store (`~/.ai-game-dev/`) so the server binary is fetched once per machine,
15
+ * not once per project, and is NEVER on PATH (design 06/09: the binary lives in the CLI's managed
16
+ * dir). Layout mirrors the plugin's `Library/mcp-server/<rid>/` — one folder per RID + a `version`
17
+ * marker.
18
+ */
19
+ export function managedServerRootDir(homeDir = os.homedir()) {
20
+ return path.join(homeDir, MACHINE_STORE_DIR_NAME, 'server');
21
+ }
22
+ export function managedServerDir(rid, homeDir = os.homedir()) {
23
+ return path.join(managedServerRootDir(homeDir), rid);
24
+ }
25
+ /** The server executable file name for a RID (`.exe` suffix on Windows RIDs). */
26
+ export function managedServerBinaryName(rid) {
27
+ return rid.startsWith('win-') ? `${SERVER_EXECUTABLE_NAME}.exe` : SERVER_EXECUTABLE_NAME;
28
+ }
29
+ export function managedServerBinaryPath(rid, homeDir = os.homedir()) {
30
+ return path.join(managedServerDir(rid, homeDir), managedServerBinaryName(rid));
31
+ }
32
+ /** Absolute path of the `version` marker recording which server version is staged for a RID. */
33
+ export function managedServerVersionPath(rid, homeDir = os.homedir()) {
34
+ return path.join(managedServerDir(rid, homeDir), 'version');
35
+ }
36
+ /** The release-asset zip NAME for a RID (`gamedev-mcp-server-<rid>.zip`) — the SHA256SUMS key. */
37
+ export function serverZipName(rid) {
38
+ return `${SERVER_EXECUTABLE_NAME}-${rid}.zip`;
39
+ }
40
+ /** The GitHub release download URL of the per-RID server zip, pinned to `version`. */
41
+ export function serverZipUrl(rid, version) {
42
+ return `https://github.com/${SERVER_RELEASE_REPO}/releases/download/${serverReleaseTag(version)}/${serverZipName(rid)}`;
43
+ }
44
+ /** The GitHub release download URL of the `SHA256SUMS` integrity manifest, pinned to `version`. */
45
+ export function serverShaSumsUrl(version) {
46
+ return `https://github.com/${SERVER_RELEASE_REPO}/releases/download/${serverReleaseTag(version)}/SHA256SUMS`;
47
+ }
48
+ /**
49
+ * Look up the expected SHA-256 (lowercase hex) for `fileName` in a `SHA256SUMS` manifest.
50
+ * Accepts the canonical `<hex> <name>` line shape and the `<hex> *<name>` binary-mode variant.
51
+ * Returns null when no line names `fileName` (exact match — mirrors the C# exact-key Ordinal
52
+ * lookup), so a missing entry fails closed at the call site.
53
+ */
54
+ export function parseSha256Sums(manifest, fileName) {
55
+ for (const rawLine of manifest.split(/\r?\n/)) {
56
+ const line = rawLine.trim();
57
+ if (!line)
58
+ continue;
59
+ const match = line.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
60
+ if (!match)
61
+ continue;
62
+ const [, hash, name] = match;
63
+ if (name.trim() === fileName)
64
+ return hash.toLowerCase();
65
+ }
66
+ return null;
67
+ }
68
+ /** SHA-256 of a buffer as lowercase hex. */
69
+ export function sha256Hex(buffer) {
70
+ return createHash('sha256').update(buffer).digest('hex');
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Archive extraction (runtime side; not exercised by the mocked unit gate)
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Extract a `.zip` into `destDir` using the best available platform archive tool. Node ships no
77
+ * zip reader, so this shells out: bsdtar (`tar -xf`, present on Windows 10+ and macOS) and `unzip`
78
+ * (POSIX) cover every supported host, with a PowerShell `Expand-Archive` fallback on Windows. The
79
+ * caller verifies the extracted binary exists afterwards, so a silent no-op tool is still caught.
80
+ */
81
+ export function extractZip(zipPath, destDir) {
82
+ fs.mkdirSync(destDir, { recursive: true });
83
+ const strategies = process.platform === 'win32'
84
+ ? [
85
+ { cmd: 'tar', args: ['-xf', zipPath, '-C', destDir] },
86
+ {
87
+ cmd: 'powershell.exe',
88
+ args: [
89
+ '-NoProfile',
90
+ '-NonInteractive',
91
+ '-Command',
92
+ `Expand-Archive -LiteralPath ${JSON.stringify(zipPath)} -DestinationPath ${JSON.stringify(destDir)} -Force`,
93
+ ],
94
+ },
95
+ ]
96
+ : [
97
+ { cmd: 'unzip', args: ['-o', zipPath, '-d', destDir] },
98
+ { cmd: 'tar', args: ['-xf', zipPath, '-C', destDir] },
99
+ ];
100
+ let lastError;
101
+ for (const strategy of strategies) {
102
+ try {
103
+ execFileSync(strategy.cmd, strategy.args, { stdio: 'ignore' });
104
+ return;
105
+ }
106
+ catch (err) {
107
+ lastError = err;
108
+ }
109
+ }
110
+ throw new Error(`Failed to extract ${zipPath} into ${destDir}: no working archive tool ` +
111
+ `(tried ${strategies.map((s) => s.cmd).join(', ')}). ` +
112
+ (lastError instanceof Error ? lastError.message : String(lastError)));
113
+ }
114
+ /** Shallow-search (root, then one level of subdirs) for `binaryName`; returns its directory. */
115
+ function locateBinaryDir(rootDir, binaryName) {
116
+ if (fs.existsSync(path.join(rootDir, binaryName)))
117
+ return rootDir;
118
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
119
+ if (!entry.isDirectory())
120
+ continue;
121
+ const candidate = path.join(rootDir, entry.name, binaryName);
122
+ if (fs.existsSync(candidate))
123
+ return path.join(rootDir, entry.name);
124
+ }
125
+ return null;
126
+ }
127
+ function copyDirContents(srcDir, destDir) {
128
+ fs.mkdirSync(destDir, { recursive: true });
129
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
130
+ const src = path.join(srcDir, entry.name);
131
+ const dest = path.join(destDir, entry.name);
132
+ if (entry.isDirectory()) {
133
+ copyDirContents(src, dest);
134
+ }
135
+ else {
136
+ fs.copyFileSync(src, dest);
137
+ }
138
+ }
139
+ }
140
+ async function fetchBytes(url, doFetch) {
141
+ const response = await doFetch(url);
142
+ if (!response.ok) {
143
+ throw new Error(`Download failed (HTTP ${response.status}) for ${url}`);
144
+ }
145
+ return Buffer.from(await response.arrayBuffer());
146
+ }
147
+ async function fetchText(url, doFetch) {
148
+ const response = await doFetch(url);
149
+ if (!response.ok) {
150
+ throw new Error(`Download failed (HTTP ${response.status}) for ${url}`);
151
+ }
152
+ return response.text();
153
+ }
154
+ /**
155
+ * Download (or copy from `--server-source`), verify against the release SHA256SUMS (fail-closed),
156
+ * extract, and atomically publish the pinned GameDev-MCP-Server binary for the host RID into the
157
+ * CLI's managed directory. Never launches the binary. Returns the published path.
158
+ */
159
+ export async function downloadServerBinary(opts = {}) {
160
+ const rid = opts.rid ?? resolveHostRid();
161
+ const version = opts.version ?? DEFAULT_SERVER_VERSION;
162
+ const homeDir = opts.homeDir ?? os.homedir();
163
+ const doFetch = opts.fetchImpl ?? fetch;
164
+ const extract = opts.extractImpl ?? extractZip;
165
+ const report = opts.onProgress ?? (() => { });
166
+ const zipName = serverZipName(rid);
167
+ // 1. Obtain the zip bytes.
168
+ let zipBytes;
169
+ let verified;
170
+ if (opts.source) {
171
+ if (fs.existsSync(opts.source)) {
172
+ report(`Using local server source: ${opts.source}`);
173
+ zipBytes = fs.readFileSync(opts.source);
174
+ }
175
+ else {
176
+ report(`Downloading server from source URL: ${opts.source}`);
177
+ zipBytes = await fetchBytes(opts.source, doFetch);
178
+ }
179
+ verified = false; // explicit-trust override — no release SHA256SUMS to verify against
180
+ }
181
+ else {
182
+ const zipUrl = serverZipUrl(rid, version);
183
+ report(`Downloading ${zipName} (v${version})...`);
184
+ zipBytes = await fetchBytes(zipUrl, doFetch);
185
+ // FAIL-CLOSED INTEGRITY GATE (verify-before-extract). The zip is UNTRUSTED until its SHA-256
186
+ // matches the release's SHA256SUMS entry for THIS RID. A missing/mismatched/unfetchable
187
+ // manifest aborts WITHOUT extracting — an unverified binary must never be published.
188
+ const sumsUrl = serverShaSumsUrl(version);
189
+ report('Verifying checksum against release SHA256SUMS...');
190
+ const manifest = await fetchText(sumsUrl, doFetch);
191
+ const expected = parseSha256Sums(manifest, zipName);
192
+ if (!expected) {
193
+ throw new Error(`Integrity check failed: no SHA256SUMS entry for ${zipName} in the v${version} release. ` +
194
+ `Refusing to install an unverified server binary.`);
195
+ }
196
+ const actual = sha256Hex(zipBytes);
197
+ if (actual !== expected) {
198
+ throw new Error(`Integrity check FAILED for ${zipName}: expected ${expected}, got ${actual}. ` +
199
+ `Refusing to install a tampered or corrupt server binary.`);
200
+ }
201
+ verified = true;
202
+ }
203
+ // 2. Stage into a same-root temp dir, extract, locate the binary.
204
+ const rootDir = managedServerRootDir(homeDir);
205
+ fs.mkdirSync(rootDir, { recursive: true });
206
+ const stagingDir = path.join(rootDir, `.staging-${rid}-${randomUUID()}`);
207
+ const tempZip = path.join(rootDir, `.${SERVER_EXECUTABLE_NAME}-${rid}-${randomUUID()}.zip`);
208
+ try {
209
+ fs.writeFileSync(tempZip, zipBytes);
210
+ fs.mkdirSync(stagingDir, { recursive: true });
211
+ report('Extracting server archive...');
212
+ extract(tempZip, stagingDir);
213
+ const binaryName = managedServerBinaryName(rid);
214
+ const binaryDir = locateBinaryDir(stagingDir, binaryName);
215
+ if (!binaryDir) {
216
+ throw new Error(`Extracted archive did not contain '${binaryName}'. The '${zipName}' asset layout may have changed.`);
217
+ }
218
+ // 3. Publish: replace the per-RID cache folder with the staged payload, write the version
219
+ // marker, and set the exec bit on POSIX so the payload is launch-ready.
220
+ const destDir = managedServerDir(rid, homeDir);
221
+ fs.rmSync(destDir, { recursive: true, force: true });
222
+ copyDirContents(binaryDir, destDir);
223
+ const binaryPath = managedServerBinaryPath(rid, homeDir);
224
+ if (process.platform !== 'win32') {
225
+ try {
226
+ fs.chmodSync(binaryPath, 0o755);
227
+ }
228
+ catch {
229
+ /* best effort */
230
+ }
231
+ }
232
+ fs.writeFileSync(managedServerVersionPath(rid, homeDir), version);
233
+ report(`Server binary ready: ${binaryPath}`);
234
+ return { rid, version, binaryPath, verified };
235
+ }
236
+ finally {
237
+ fs.rmSync(tempZip, { force: true });
238
+ fs.rmSync(stagingDir, { recursive: true, force: true });
239
+ }
240
+ }
241
+ /**
242
+ * Proxy `configure --agent <id>` to the managed GameDev-MCP-Server binary's `configure`
243
+ * subcommand (design 06/09 Phase 3) so the shared C# configurator registry — with the derived
244
+ * `port=` + `project=` pin — is reachable from the terminal. The binary derives the pin/port from
245
+ * its working directory, so we run it with `cwd` = the resolved project path. When no managed
246
+ * binary is installed, throws a clear, actionable error pointing at `install-plugin --with-server`.
247
+ */
248
+ export function proxyConfigure(opts) {
249
+ const rid = opts.rid ?? resolveHostRid();
250
+ const binaryPath = managedServerBinaryPath(rid, opts.homeDir);
251
+ if (!fs.existsSync(binaryPath)) {
252
+ throw new Error(`No managed GameDev-MCP-Server binary found at ${binaryPath}. ` +
253
+ `Run 'unity-mcp-cli install-plugin --with-server' first to download it.`);
254
+ }
255
+ const args = ['configure', '--agent', opts.agentId, ...(opts.url ? ['--url', opts.url] : [])];
256
+ const cwd = path.resolve(opts.projectPath);
257
+ const run = opts.runImpl ?? ((bin, a, workDir) => execFileSync(bin, a, { cwd: workDir, stdio: 'inherit' }));
258
+ run(binaryPath, args, cwd);
259
+ return { binaryPath, args, cwd };
260
+ }
261
+ //# sourceMappingURL=managed-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-server.js","sourceRoot":"","sources":["../../src/utils/managed-server.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kDAAkD;AAElD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAY,MAAM,UAAU,CAAC;AACpD,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,UAAkB,EAAE,CAAC,OAAO,EAAE;IACjE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,UAAkB,EAAE,CAAC,OAAO,EAAE;IAC1E,OAAO,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;AACvD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,uBAAuB,CAAC,GAAW;IACjD,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,sBAAsB,MAAM,CAAC,CAAC,CAAC,sBAAsB,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,GAAW,EAAE,UAAkB,EAAE,CAAC,OAAO,EAAE;IACjF,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC,CAAC;AACjF,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,wBAAwB,CAAC,GAAW,EAAE,UAAkB,EAAE,CAAC,OAAO,EAAE;IAClF,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC;AAC9D,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,OAAO,GAAG,sBAAsB,IAAI,GAAG,MAAM,CAAC;AAChD,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,OAAe;IACvD,OAAO,sBAAsB,mBAAmB,sBAAsB,gBAAgB,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;AAC1H,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,sBAAsB,mBAAmB,sBAAsB,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC;AAC/G,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,QAAgB;IAChE,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAC1D,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;QAC7B,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC1D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4CAA4C;AAC5C,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,OAAe;IACzD,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3C,MAAM,UAAU,GACd,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC1B,CAAC,CAAC;YACE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;YACrD;gBACE,GAAG,EAAE,gBAAgB;gBACrB,IAAI,EAAE;oBACJ,YAAY;oBACZ,iBAAiB;oBACjB,UAAU;oBACV,+BAA+B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS;iBAC5G;aACF;SACF;QACH,CAAC,CAAC;YACE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;YACtD,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;SACtD,CAAC;IAER,IAAI,SAAkB,CAAC;IACvB,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,GAAG,GAAG,CAAC;QAClB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,qBAAqB,OAAO,SAAS,OAAO,4BAA4B;QACtE,UAAU,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QACtD,CAAC,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CACvE,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,SAAS,eAAe,CAAC,OAAe,EAAE,UAAkB;IAC1D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC;IAClE,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACrE,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,MAAc,EAAE,OAAe;IACtD,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QACpE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAqCD,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,OAAqB;IAC1D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,OAAqB;IACzD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAA8B,EAAE;IAEhC,MAAM,GAAG,GAAiB,IAAI,CAAC,GAAG,IAAI,cAAc,EAAE,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,sBAAsB,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,IAAI,UAAU,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAEnC,2BAA2B;IAC3B,IAAI,QAAgB,CAAC;IACrB,IAAI,QAAiB,CAAC;IACtB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,8BAA8B,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACpD,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,uCAAuC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7D,QAAQ,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpD,CAAC;QACD,QAAQ,GAAG,KAAK,CAAC,CAAC,oEAAoE;IACxF,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,eAAe,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC;QAClD,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAE7C,6FAA6F;QAC7F,wFAAwF;QACxF,qFAAqF;QACrF,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,CAAC,kDAAkD,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CACb,mDAAmD,OAAO,YAAY,OAAO,YAAY;gBACvF,kDAAkD,CACrD,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CACb,8BAA8B,OAAO,cAAc,QAAQ,SAAS,MAAM,IAAI;gBAC5E,0DAA0D,CAC7D,CAAC;QACJ,CAAC;QACD,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,kEAAkE;IAClE,MAAM,OAAO,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAC9C,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,UAAU,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,sBAAsB,IAAI,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC,CAAC;IAC5F,IAAI,CAAC;QACH,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpC,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,MAAM,CAAC,8BAA8B,CAAC,CAAC;QACvC,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAE7B,MAAM,UAAU,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAC;QAChD,MAAM,SAAS,GAAG,eAAe,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC1D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,sCAAsC,UAAU,WAAW,OAAO,kCAAkC,CACrG,CAAC;QACJ,CAAC;QAED,0FAA0F;QAC1F,2EAA2E;QAC3E,MAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC/C,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAEpC,MAAM,UAAU,GAAG,uBAAuB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACzD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,iBAAiB;YACnB,CAAC;QACH,CAAC;QACD,EAAE,CAAC,aAAa,CAAC,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;QAElE,MAAM,CAAC,wBAAwB,UAAU,EAAE,CAAC,CAAC;QAC7C,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IAChD,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpC,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC;AAuBD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAA2B;IACxD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,cAAc,EAAE,CAAC;IACzC,MAAM,UAAU,GAAG,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC9D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,iDAAiD,UAAU,IAAI;YAC7D,wEAAwE,CAC3E,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3C,MAAM,GAAG,GACP,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAClG,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3B,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACnC,CAAC"}
@@ -1,6 +1,19 @@
1
+ /**
2
+ * The exact string that is UTF-8/SHA-256 hashed: the project root with trailing directory
3
+ * separators trimmed, then lowercased with the ToLowerInvariant-matching rules above.
4
+ * Exposed so the golden-vector parity test can reproduce the pre-hash string.
5
+ */
6
+ export declare function normalizeProjectRoot(projectRoot: string): string;
7
+ /**
8
+ * The routing pin: the first 4 bytes of the SHA-256 of the normalized project root as 8
9
+ * lowercase hex characters. Byte-for-byte the C# ProjectIdentity.DerivePin.
10
+ */
11
+ export declare function deriveProjectPin(dir: string): string;
1
12
  /**
2
13
  * Generate a deterministic port from a directory path.
3
- * Ports the C# UnityMcpPlugin.GeneratePortFromDirectory() logic.
4
- * SHA256 hash of lowercased directory → first 4 bytes as uint32 → modulo 10000 + 20000.
14
+ * Ports the canonical C# ProjectIdentity derivation (DerivePort), which is itself byte-for-byte
15
+ * the shipped Unity UnityMcpPlugin.GeneratePortFromDirectory() logic:
16
+ * SHA256 of the normalized (trailing-separator-trimmed, ToLowerInvariant) directory → first
17
+ * 4 bytes as a little-endian uint32 → modulo 10000 + 20000 (range 20000-29999).
5
18
  */
6
19
  export declare function generatePortFromDirectory(dir: string): number;
@@ -2,16 +2,71 @@ import { createHash } from 'crypto';
2
2
  const MIN_PORT = 20000;
3
3
  const MAX_PORT = 29999;
4
4
  const PORT_RANGE = MAX_PORT - MIN_PORT + 1;
5
+ // Routing pin = first 4 bytes of the hash rendered as 8 lowercase hex chars.
6
+ const PIN_BYTES = 4;
7
+ // Characters where JS String.prototype.toLowerCase() diverges from .NET
8
+ // string.ToLowerInvariant(). ToLowerInvariant is the canonical origin of the
9
+ // ProjectIdentity derivation (see MCP-Plugin-dotnet ProjectIdentity.GoldenVectors.json),
10
+ // so the TS port must reproduce it byte-for-byte. Each entry maps a code point to the
11
+ // value ToLowerInvariant produces:
12
+ // U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE - ToLowerInvariant leaves it unchanged
13
+ // (no case fold), whereas toLowerCase() lowers it to U+0069 U+0307 (i + COMBINING DOT ABOVE).
14
+ const INVARIANT_LOWER_OVERRIDES = {
15
+ 'İ': 'İ',
16
+ };
17
+ /**
18
+ * Lowercase a string the way .NET string.ToLowerInvariant() does: a simple,
19
+ * culture-independent, per-code-point mapping (no context-sensitive rules such as the
20
+ * Greek final-sigma or the Turkish-i special cases). We lower each code point on its own
21
+ * and apply INVARIANT_LOWER_OVERRIDES for the few points where JS disagrees with .NET.
22
+ */
23
+ function toLowerInvariant(value) {
24
+ let out = '';
25
+ for (const ch of value) {
26
+ out += INVARIANT_LOWER_OVERRIDES[ch] ?? ch.toLowerCase();
27
+ }
28
+ return out;
29
+ }
30
+ /**
31
+ * Trim trailing directory separators ('/' and '\\') so '/a/b' and '/a/b/' are the same
32
+ * project. Never trims below length 1 (matches ProjectIdentity.TrimTrailingSeparators).
33
+ * Separators are NOT converted — 'C:\\a' and 'C:/a' remain distinct and hash differently.
34
+ */
35
+ function trimTrailingSeparators(pathStr) {
36
+ let end = pathStr.length;
37
+ while (end > 1 && (pathStr[end - 1] === '/' || pathStr[end - 1] === '\\')) {
38
+ end--;
39
+ }
40
+ return end === pathStr.length ? pathStr : pathStr.slice(0, end);
41
+ }
42
+ /**
43
+ * The exact string that is UTF-8/SHA-256 hashed: the project root with trailing directory
44
+ * separators trimmed, then lowercased with the ToLowerInvariant-matching rules above.
45
+ * Exposed so the golden-vector parity test can reproduce the pre-hash string.
46
+ */
47
+ export function normalizeProjectRoot(projectRoot) {
48
+ return toLowerInvariant(trimTrailingSeparators(projectRoot));
49
+ }
50
+ function hashOf(projectRoot) {
51
+ return createHash('sha256').update(normalizeProjectRoot(projectRoot), 'utf-8').digest();
52
+ }
53
+ /**
54
+ * The routing pin: the first 4 bytes of the SHA-256 of the normalized project root as 8
55
+ * lowercase hex characters. Byte-for-byte the C# ProjectIdentity.DerivePin.
56
+ */
57
+ export function deriveProjectPin(dir) {
58
+ return hashOf(dir).subarray(0, PIN_BYTES).toString('hex');
59
+ }
5
60
  /**
6
61
  * Generate a deterministic port from a directory path.
7
- * Ports the C# UnityMcpPlugin.GeneratePortFromDirectory() logic.
8
- * SHA256 hash of lowercased directory → first 4 bytes as uint32 → modulo 10000 + 20000.
62
+ * Ports the canonical C# ProjectIdentity derivation (DerivePort), which is itself byte-for-byte
63
+ * the shipped Unity UnityMcpPlugin.GeneratePortFromDirectory() logic:
64
+ * SHA256 of the normalized (trailing-separator-trimmed, ToLowerInvariant) directory → first
65
+ * 4 bytes as a little-endian uint32 → modulo 10000 + 20000 (range 20000-29999).
9
66
  */
10
67
  export function generatePortFromDirectory(dir) {
11
- const hash = createHash('sha256')
12
- .update(dir.toLowerCase())
13
- .digest();
14
- // Read first 4 bytes as little-endian int32, then treat as unsigned
68
+ const hash = hashOf(dir);
69
+ // Read first 4 bytes as little-endian int32, then treat as unsigned.
15
70
  const int32 = hash.readInt32LE(0);
16
71
  const uint32 = int32 >>> 0;
17
72
  return MIN_PORT + (uint32 % PORT_RANGE);
@@ -1 +1 @@
1
- {"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/utils/port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC;SAC9B,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;SACzB,MAAM,EAAE,CAAC;IAEZ,oEAAoE;IACpE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC;IAE3B,OAAO,QAAQ,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/utils/port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,QAAQ,GAAG,KAAK,CAAC;AACvB,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC;AAC3C,6EAA6E;AAC7E,MAAM,SAAS,GAAG,CAAC,CAAC;AAEpB,wEAAwE;AACxE,6EAA6E;AAC7E,yFAAyF;AACzF,sFAAsF;AACtF,mCAAmC;AACnC,wFAAwF;AACxF,gGAAgG;AAChG,MAAM,yBAAyB,GAA2B;IACxD,GAAG,EAAE,GAAG;CACT,CAAC;AAEF;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,KAAa;IACrC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;QACvB,GAAG,IAAI,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,SAAS,sBAAsB,CAAC,OAAe;IAC7C,IAAI,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IACzB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1E,GAAG,EAAE,CAAC;IACR,CAAC;IACD,OAAO,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAAmB;IACtD,OAAO,gBAAgB,CAAC,sBAAsB,CAAC,WAAW,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,MAAM,CAAC,WAAmB;IACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,oBAAoB,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAC1F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAEzB,qEAAqE;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,KAAK,KAAK,CAAC,CAAC;IAE3B,OAAO,QAAQ,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The tool-neutral, NON-SECRET, committable project marker at
3
+ * `<project>/.ai-game-dev/project.json` (design 06/09, D15). It records the enrolled server
4
+ * target (hosted vs local) and the optional user port override so ProjectIdentity resolution and
5
+ * every config writer (engine UI, CLIs, `configure`) agree on one source of truth. Credentials
6
+ * NEVER go here — those live only in the machine credential store (`credentials.json`).
7
+ */
8
+ export declare const PROJECT_MARKER_FILE = "project.json";
9
+ export interface ProjectMarker {
10
+ /** The server the project is enrolled against (hosted `https://ai-game.dev` or a local URL). */
11
+ serverTarget?: string;
12
+ /** User's explicit local-port override (wins over the deterministic derived port). */
13
+ portOverride?: number;
14
+ /** Unknown fields are preserved on read/merge for forward-compatibility. */
15
+ [key: string]: unknown;
16
+ }
17
+ export declare function projectMarkerDir(projectPath: string): string;
18
+ export declare function projectMarkerPath(projectPath: string): string;
19
+ /** Read the marker, or null when absent/unparsable. */
20
+ export declare function readProjectMarker(projectPath: string): ProjectMarker | null;
21
+ /**
22
+ * Merge `marker` into any existing marker and write it back (creating the `.ai-game-dev/`
23
+ * directory as needed). Idempotent for the same inputs; preserves pre-existing keys. Returns the
24
+ * absolute marker path.
25
+ */
26
+ export declare function writeProjectMarker(projectPath: string, marker: ProjectMarker): string;
@@ -0,0 +1,48 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { MACHINE_STORE_DIR_NAME } from './machine-credentials.js';
6
+ /**
7
+ * The tool-neutral, NON-SECRET, committable project marker at
8
+ * `<project>/.ai-game-dev/project.json` (design 06/09, D15). It records the enrolled server
9
+ * target (hosted vs local) and the optional user port override so ProjectIdentity resolution and
10
+ * every config writer (engine UI, CLIs, `configure`) agree on one source of truth. Credentials
11
+ * NEVER go here — those live only in the machine credential store (`credentials.json`).
12
+ */
13
+ export const PROJECT_MARKER_FILE = 'project.json';
14
+ export function projectMarkerDir(projectPath) {
15
+ return path.join(projectPath, MACHINE_STORE_DIR_NAME);
16
+ }
17
+ export function projectMarkerPath(projectPath) {
18
+ return path.join(projectMarkerDir(projectPath), PROJECT_MARKER_FILE);
19
+ }
20
+ /** Read the marker, or null when absent/unparsable. */
21
+ export function readProjectMarker(projectPath) {
22
+ const markerPath = projectMarkerPath(projectPath);
23
+ if (!fs.existsSync(markerPath))
24
+ return null;
25
+ try {
26
+ const parsed = JSON.parse(fs.readFileSync(markerPath, 'utf-8'));
27
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
28
+ return null;
29
+ return parsed;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /**
36
+ * Merge `marker` into any existing marker and write it back (creating the `.ai-game-dev/`
37
+ * directory as needed). Idempotent for the same inputs; preserves pre-existing keys. Returns the
38
+ * absolute marker path.
39
+ */
40
+ export function writeProjectMarker(projectPath, marker) {
41
+ const dir = projectMarkerDir(projectPath);
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ const merged = { ...(readProjectMarker(projectPath) ?? {}), ...marker };
44
+ const markerPath = projectMarkerPath(projectPath);
45
+ fs.writeFileSync(markerPath, JSON.stringify(merged, null, 2) + '\n');
46
+ return markerPath;
47
+ }
48
+ //# sourceMappingURL=project-marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-marker.js","sourceRoot":"","sources":["../../src/utils/project-marker.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kDAAkD;AAElD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAWlD,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,sBAAsB,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,OAAO,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC,CAAC;AACvE,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAClD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAY,CAAC;QAC3E,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;QAChF,OAAO,MAAuB,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,MAAqB;IAC3E,MAAM,GAAG,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAC1C,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,MAAM,GAAkB,EAAE,GAAG,CAAC,iBAAiB,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;IACvF,MAAM,UAAU,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;IAClD,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACrE,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Runtime Identifier (RID) detection for the shared GameDev-MCP-Server binary.
3
+ *
4
+ * The RID string `<os>-<arch>` (e.g. `win-x64`, `osx-arm64`) selects which
5
+ * `gamedev-mcp-server-<rid>.zip` release asset to download. Ported from the plugin's C#
6
+ * `McpServerManager.PlatformName` (`OperationSystem` + `-` + `CpuArch`) so the CLI and plugin
7
+ * resolve the same asset for the same host.
8
+ */
9
+ /** The exact set of RIDs published as `gamedev-mcp-server-<rid>.zip` on every server release. */
10
+ export declare const KNOWN_RIDS: readonly ["linux-arm64", "linux-x64", "osx-arm64", "osx-x64", "win-arm64", "win-x64", "win-x86"];
11
+ export type Rid = (typeof KNOWN_RIDS)[number];
12
+ /**
13
+ * Resolve the host RID (defaulting to the current process's platform/arch). Throws a clear,
14
+ * actionable error when the host has no published server build — a fail-closed guard so
15
+ * `install-plugin --with-server` never silently downloads the wrong asset (or a 404 page).
16
+ */
17
+ export declare function resolveHostRid(platform?: NodeJS.Platform, arch?: string): Rid;
@@ -0,0 +1,60 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ /**
4
+ * Runtime Identifier (RID) detection for the shared GameDev-MCP-Server binary.
5
+ *
6
+ * The RID string `<os>-<arch>` (e.g. `win-x64`, `osx-arm64`) selects which
7
+ * `gamedev-mcp-server-<rid>.zip` release asset to download. Ported from the plugin's C#
8
+ * `McpServerManager.PlatformName` (`OperationSystem` + `-` + `CpuArch`) so the CLI and plugin
9
+ * resolve the same asset for the same host.
10
+ */
11
+ /** The exact set of RIDs published as `gamedev-mcp-server-<rid>.zip` on every server release. */
12
+ export const KNOWN_RIDS = [
13
+ 'linux-arm64',
14
+ 'linux-x64',
15
+ 'osx-arm64',
16
+ 'osx-x64',
17
+ 'win-arm64',
18
+ 'win-x64',
19
+ 'win-x86',
20
+ ];
21
+ /** Map a Node `process.platform` value to the server's `OperationSystem` token, or null. */
22
+ function osToken(platform) {
23
+ if (platform === 'win32')
24
+ return 'win';
25
+ if (platform === 'darwin')
26
+ return 'osx';
27
+ if (platform === 'linux')
28
+ return 'linux';
29
+ return null;
30
+ }
31
+ /** Map a Node `process.arch` value to the server's `CpuArch` token, or null. */
32
+ function archToken(arch) {
33
+ if (arch === 'x64')
34
+ return 'x64';
35
+ if (arch === 'arm64')
36
+ return 'arm64';
37
+ if (arch === 'ia32')
38
+ return 'x86';
39
+ return null;
40
+ }
41
+ /**
42
+ * Resolve the host RID (defaulting to the current process's platform/arch). Throws a clear,
43
+ * actionable error when the host has no published server build — a fail-closed guard so
44
+ * `install-plugin --with-server` never silently downloads the wrong asset (or a 404 page).
45
+ */
46
+ export function resolveHostRid(platform = process.platform, arch = process.arch) {
47
+ const os = osToken(platform);
48
+ const cpu = archToken(arch);
49
+ if (!os || !cpu) {
50
+ throw new Error(`Unsupported host platform/architecture (${platform}/${arch}). ` +
51
+ `Supported GameDev-MCP-Server RIDs: ${KNOWN_RIDS.join(', ')}.`);
52
+ }
53
+ const rid = `${os}-${cpu}`;
54
+ if (!KNOWN_RIDS.includes(rid)) {
55
+ throw new Error(`No GameDev-MCP-Server build exists for host RID '${rid}' (${platform}/${arch}). ` +
56
+ `Supported RIDs: ${KNOWN_RIDS.join(', ')}.`);
57
+ }
58
+ return rid;
59
+ }
60
+ //# sourceMappingURL=rid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rid.js","sourceRoot":"","sources":["../../src/utils/rid.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kDAAkD;AAElD;;;;;;;GAOG;AAEH,iGAAiG;AACjG,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,aAAa;IACb,WAAW;IACX,WAAW;IACX,SAAS;IACT,WAAW;IACX,SAAS;IACT,SAAS;CACD,CAAC;AAIX,4FAA4F;AAC5F,SAAS,OAAO,CAAC,QAAyB;IACxC,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACzC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,gFAAgF;AAChF,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACjC,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,OAAO,CAAC;IACrC,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAClC,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,WAA4B,OAAO,CAAC,QAAQ,EAC5C,OAAe,OAAO,CAAC,IAAI;IAE3B,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,2CAA2C,QAAQ,IAAI,IAAI,KAAK;YAC9D,sCAAsC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACjE,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,EAAE,IAAI,GAAG,EAAE,CAAC;IAC3B,IAAI,CAAE,UAAgC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,oDAAoD,GAAG,MAAM,QAAQ,IAAI,IAAI,KAAK;YAChF,mBAAmB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC9C,CAAC;IACJ,CAAC;IACD,OAAO,GAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The pinned GameDev-MCP-Server version the CLI fetches by default with
3
+ * `install-plugin --with-server`.
4
+ *
5
+ * Kept in LOCKSTEP with the Unity plugin's `ServerVersion` constant in
6
+ * Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/McpServerManager.cs
7
+ * so the CLI-downloaded binary and the plugin-downloaded binary can never drift. A drift guard
8
+ * in `tests/server-version.test.ts` reads that C# constant and asserts the two are byte-equal.
9
+ * Override at runtime with `--server-version <v>`.
10
+ *
11
+ * The CLI mirrors the constant rather than reading the `.cs` at runtime because the published npm
12
+ * package ships no plugin sources — the plugin release cadence bumps both in the same PR, and the
13
+ * drift-guard test fails CI if a bump touches only one side.
14
+ */
15
+ export declare const DEFAULT_SERVER_VERSION = "9.0.0";
16
+ /** GitHub `owner/repo` that hosts the shared server's tagged releases + per-RID zips. */
17
+ export declare const SERVER_RELEASE_REPO = "IvanMurzak/GameDev-MCP-Server";
18
+ /** Base executable name (no extension) of the shared server binary. */
19
+ export declare const SERVER_EXECUTABLE_NAME = "gamedev-mcp-server";
20
+ /**
21
+ * The Git release TAG for a server version: the version with a leading `v` (e.g. `9.0.0` →
22
+ * `v9.0.0`). GameDev-MCP-Server tags every release `v<version>` and the per-RID zips + the
23
+ * `SHA256SUMS` manifest are attached to THAT tag — so the download path MUST use the v-prefixed
24
+ * tag (a bare-version path 404s). Already-v-prefixed input is passed through unchanged so a caller
25
+ * cannot accidentally double-prefix. Mirrors the C# `McpServerManager.ServerReleaseTag`.
26
+ */
27
+ export declare function serverReleaseTag(version: string): string;
@@ -0,0 +1,33 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ /**
4
+ * The pinned GameDev-MCP-Server version the CLI fetches by default with
5
+ * `install-plugin --with-server`.
6
+ *
7
+ * Kept in LOCKSTEP with the Unity plugin's `ServerVersion` constant in
8
+ * Unity-MCP-Plugin/Packages/com.ivanmurzak.unity.mcp/Editor/Scripts/McpServerManager.cs
9
+ * so the CLI-downloaded binary and the plugin-downloaded binary can never drift. A drift guard
10
+ * in `tests/server-version.test.ts` reads that C# constant and asserts the two are byte-equal.
11
+ * Override at runtime with `--server-version <v>`.
12
+ *
13
+ * The CLI mirrors the constant rather than reading the `.cs` at runtime because the published npm
14
+ * package ships no plugin sources — the plugin release cadence bumps both in the same PR, and the
15
+ * drift-guard test fails CI if a bump touches only one side.
16
+ */
17
+ export const DEFAULT_SERVER_VERSION = '9.0.0';
18
+ /** GitHub `owner/repo` that hosts the shared server's tagged releases + per-RID zips. */
19
+ export const SERVER_RELEASE_REPO = 'IvanMurzak/GameDev-MCP-Server';
20
+ /** Base executable name (no extension) of the shared server binary. */
21
+ export const SERVER_EXECUTABLE_NAME = 'gamedev-mcp-server';
22
+ /**
23
+ * The Git release TAG for a server version: the version with a leading `v` (e.g. `9.0.0` →
24
+ * `v9.0.0`). GameDev-MCP-Server tags every release `v<version>` and the per-RID zips + the
25
+ * `SHA256SUMS` manifest are attached to THAT tag — so the download path MUST use the v-prefixed
26
+ * tag (a bare-version path 404s). Already-v-prefixed input is passed through unchanged so a caller
27
+ * cannot accidentally double-prefix. Mirrors the C# `McpServerManager.ServerReleaseTag`.
28
+ */
29
+ export function serverReleaseTag(version) {
30
+ const v = (version ?? '').trim();
31
+ return v.startsWith('v') ? v : `v${v}`;
32
+ }
33
+ //# sourceMappingURL=server-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-version.js","sourceRoot":"","sources":["../../src/utils/server-version.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,kDAAkD;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,OAAO,CAAC;AAE9C,yFAAyF;AACzF,MAAM,CAAC,MAAM,mBAAmB,GAAG,+BAA+B,CAAC;AAEnE,uEAAuE;AACvE,MAAM,CAAC,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,OAAO,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AACzC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-mcp-cli",
3
- "version": "0.83.1",
3
+ "version": "0.84.0",
4
4
  "description": "Cross-platform CLI tool for AI Game Developer (Skills & MCP). Full AI develop and test loop. Efficient token usage, advanced tools. Creates Unity project, installs plugins, configures tools, and manages HTTP connection with Unity Editor and a game made with Unity. Works with Claude Code, Gemini, Copilot, Cursor and any other absolutely for free.",
5
5
  "type": "module",
6
6
  "main": "dist/lib.js",