unity-mcp-cli 0.66.0 → 0.67.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +73 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +11 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/bootstrap-local.d.ts +17 -0
  7. package/dist/commands/bootstrap-local.js +83 -0
  8. package/dist/commands/bootstrap-local.js.map +1 -0
  9. package/dist/commands/close.d.ts +56 -0
  10. package/dist/commands/close.js +186 -0
  11. package/dist/commands/close.js.map +1 -0
  12. package/dist/commands/configure.js +37 -45
  13. package/dist/commands/configure.js.map +1 -1
  14. package/dist/commands/install-plugin.js +41 -23
  15. package/dist/commands/install-plugin.js.map +1 -1
  16. package/dist/commands/remove-plugin.js +21 -9
  17. package/dist/commands/remove-plugin.js.map +1 -1
  18. package/dist/commands/setup-mcp.js +32 -81
  19. package/dist/commands/setup-mcp.js.map +1 -1
  20. package/dist/index.js +4 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/configure.d.ts +10 -0
  23. package/dist/lib/configure.js +85 -0
  24. package/dist/lib/configure.js.map +1 -0
  25. package/dist/lib/install-plugin.d.ts +8 -0
  26. package/dist/lib/install-plugin.js +68 -0
  27. package/dist/lib/install-plugin.js.map +1 -0
  28. package/dist/lib/logger.d.ts +7 -0
  29. package/dist/lib/logger.js +18 -0
  30. package/dist/lib/logger.js.map +1 -0
  31. package/dist/lib/progress.d.ts +7 -0
  32. package/dist/lib/progress.js +16 -0
  33. package/dist/lib/progress.js.map +1 -0
  34. package/dist/lib/remove-plugin.d.ts +7 -0
  35. package/dist/lib/remove-plugin.js +51 -0
  36. package/dist/lib/remove-plugin.js.map +1 -0
  37. package/dist/lib/setup-mcp.d.ts +12 -0
  38. package/dist/lib/setup-mcp.js +141 -0
  39. package/dist/lib/setup-mcp.js.map +1 -0
  40. package/dist/lib/types.d.ts +132 -0
  41. package/dist/lib/types.js +8 -0
  42. package/dist/lib/types.js.map +1 -0
  43. package/dist/lib/validation.d.ts +37 -0
  44. package/dist/lib/validation.js +50 -0
  45. package/dist/lib/validation.js.map +1 -0
  46. package/dist/lib.d.ts +5 -0
  47. package/dist/lib.js +18 -0
  48. package/dist/lib.js.map +1 -0
  49. package/dist/utils/manifest.d.ts +31 -3
  50. package/dist/utils/manifest.js +27 -11
  51. package/dist/utils/manifest.js.map +1 -1
  52. package/dist/utils/unity-shutdown.d.ts +87 -0
  53. package/dist/utils/unity-shutdown.js +241 -0
  54. package/dist/utils/unity-shutdown.js.map +1 -0
  55. package/package.json +20 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to `unity-mcp-cli` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.67.0] - 2026-04-21
9
+
10
+ ### Added
11
+
12
+ - **Library API.** The package now exposes its core commands as a typed,
13
+ side-effect-free library alongside the existing CLI binary.
14
+ Consumers can do:
15
+
16
+ ```ts
17
+ import { installPlugin, removePlugin, configure, setupMcp } from 'unity-mcp-cli';
18
+ ```
19
+
20
+ Available functions:
21
+ - `installPlugin(opts)` — install the Unity-MCP plugin into a Unity project
22
+ - `removePlugin(opts)` — remove the Unity-MCP plugin from a Unity project
23
+ - `configure(opts)` — enable/disable MCP tools, prompts, resources
24
+ - `setupMcp(opts)` — write an agent MCP config file
25
+ - `listAgentIds()` — list every supported agent id
26
+
27
+ All functions return a typed `{ success, ... }` result (never throw past the
28
+ public boundary) and accept an optional `onProgress(event)` callback that
29
+ fires for `start`, `dependencies-resolved`, `manifest-patched`, and
30
+ `done` phases.
31
+
32
+ - **`package.json` `exports` field.** The package now ships with a
33
+ conditional-exports map:
34
+ - `"."` — the library entry (`dist/lib.js`)
35
+ - `"./cli"` — the CLI entry (`dist/cli.js`), identical behaviour to
36
+ the `unity-mcp-cli` binary
37
+
38
+ ### Changed
39
+
40
+ - Command handlers (`install-plugin`, `remove-plugin`, `configure`,
41
+ `setup-mcp`) are now thin Commander wrappers that delegate to the
42
+ library functions. Terminal output and exit codes are unchanged from
43
+ the user's perspective.
44
+
45
+ - `utils/manifest.ts` functions now accept an optional `logger` argument
46
+ so the library can run silently while the CLI preserves its chalk-
47
+ styled output. `addPluginToManifest` and `removePluginFromManifest`
48
+ also return structured result objects; return values are purely
49
+ additive and the existing call sites are unaffected.
50
+
51
+ ### Notes
52
+
53
+ - No breaking changes to the CLI binary. `unity-mcp-cli install-plugin
54
+ ./MyProject` and friends still work exactly as before.
55
+ - The library entry point has no top-level side effects: importing
56
+ `unity-mcp-cli` never writes to stdout/stderr and never parses argv.
57
+ An `import 'unity-mcp-cli'` or dynamic `import('unity-mcp-cli')`
58
+ call is safe to do anywhere.
59
+
60
+ [0.67.0]: https://github.com/IvanMurzak/Unity-MCP/releases/tag/cli-0.67.0
package/README.md CHANGED
@@ -96,6 +96,7 @@ npx unity-mcp-cli install-plugin /path/to/unity/project
96
96
  - [`install-plugin`](#install-plugin)
97
97
  - [`install-unity`](#install-unity)
98
98
  - [`open`](#open)
99
+ - [`close`](#close)
99
100
  - [`run-tool`](#run-tool)
100
101
  - [`wait-for-ready`](#wait-for-ready)
101
102
  - [`setup-mcp`](#setup-mcp)
@@ -283,6 +284,48 @@ unity-mcp-cli open ./MyGame \
283
284
 
284
285
  ![AI Game Developer — Unity SKILLS and MCP](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/promo/hazzard-divider.svg?raw=true)
285
286
 
287
+ ## `close`
288
+
289
+ Gracefully terminate the Unity Editor instance running for a given project path. Symmetric counterpart of [`open`](#open) — for scripted workflows (CI agents, pipeline executors, integration test fixtures) that need a clean tear-down without resorting to OS-level process kills.
290
+
291
+ ```bash
292
+ unity-mcp-cli close ./MyGame
293
+ ```
294
+
295
+ | Option | Required | Description |
296
+ |---|---|---|
297
+ | `[path]` | No | Path to the Unity project (positional, defaults to current directory) |
298
+ | `--timeout <seconds>` | No | Polite-quit timeout in seconds (default: 30) |
299
+ | `--force` | No | Hard-kill the Editor if it does not exit within `--timeout` |
300
+
301
+ **How it works:**
302
+
303
+ 1. Resolves the running Editor's PID by reading `<project>/Temp/UnityLockfile` (4-byte little-endian uint32) and cross-checking against process enumeration to handle stale lock files.
304
+ 2. Sends a polite-quit signal — `SIGTERM` on Linux/macOS, `taskkill` (no `/F`) on Windows — letting Unity finish autosave / asset-import.
305
+ 3. Polls every 250ms until the process exits or `--timeout` elapses.
306
+ 4. If the timeout expires AND `--force` is set, falls back to `SIGKILL` / `taskkill /F`.
307
+ 5. Idempotent — closing an already-closed Editor (or a project whose Editor was never running) exits 0 with `no running Editor for project at <path>`.
308
+ 6. Refuses to act on any path that is not a Unity project root (`ProjectSettings/ProjectVersion.txt` must exist) — protects against accidental kill-all-Unity-on-host invocations.
309
+
310
+ > **Windows headless caveat:** the polite-quit step uses `taskkill` (no `/F`), which delivers `WM_CLOSE`. That message only reaches processes owning a top-level window on the **same desktop/session** as the CLI. If Unity was launched by a Windows service in session 0 (or any other non-interactive desktop), the polite-quit will be silently dropped, the `--timeout` will elapse, and `--force` becomes the only path that brings the Editor down. Plan accordingly in headless CI runners.
311
+
312
+ **Example — close, fall back to force after 60s:**
313
+
314
+ ```bash
315
+ unity-mcp-cli close ./MyGame --timeout 60 --force
316
+ ```
317
+
318
+ **Example — clean tear-down at the end of an automation script:**
319
+
320
+ ```bash
321
+ unity-mcp-cli open ./MyGame
322
+ unity-mcp-cli wait-for-ready ./MyGame
323
+ unity-mcp-cli run-tool tests-run ./MyGame --input '{"testMode":"EditMode"}'
324
+ unity-mcp-cli close ./MyGame
325
+ ```
326
+
327
+ ![AI Game Developer — Unity SKILLS and MCP](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/promo/hazzard-divider.svg?raw=true)
328
+
286
329
  ## `run-tool`
287
330
 
288
331
  Execute an MCP tool directly via the HTTP API. The server URL and authorization token are **automatically resolved** from the project's config file (`UserSettings/AI-Game-Developer-Config.json`), based on the current connection mode (Custom or Cloud).
@@ -557,3 +600,33 @@ Commands that manage editors or create projects use the **Unity Hub CLI** (`--he
557
600
  > For the full Unity-MCP project documentation, see the [main README](https://github.com/IvanMurzak/Unity-MCP/blob/main/README.md).
558
601
 
559
602
  ![AI Game Developer — Unity SKILLS and MCP](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/promo/hazzard-divider.svg?raw=true)
603
+
604
+ # Library API (v0.67.0+)
605
+
606
+ In addition to the CLI binary, `unity-mcp-cli` exposes its core commands as a typed, side-effect-free library so other Node.js / TypeScript tools can embed the same install / configure flow without shelling out.
607
+
608
+ ```ts
609
+ import { installPlugin, removePlugin, configure, setupMcp } from 'unity-mcp-cli';
610
+
611
+ const result = await installPlugin({
612
+ unityProjectPath: './MyUnityProject',
613
+ // version: '0.67.0', // optional — defaults to latest from OpenUPM
614
+ onProgress: (event) => {
615
+ // phase is one of: 'start' | 'dependencies-resolved' | 'manifest-patched' | 'done'
616
+ console.log(event.phase, event.message);
617
+ },
618
+ });
619
+
620
+ if (!result.success) {
621
+ console.error('Install failed:', result.error?.message);
622
+ return;
623
+ }
624
+
625
+ console.log(`Installed v${result.installedVersion}`);
626
+ for (const warning of result.warnings) console.warn(warning);
627
+ for (const step of result.nextSteps) console.log(step);
628
+ ```
629
+
630
+ Each function returns a typed `{ success, ... }` result object; errors are never thrown past the public boundary. The library entry has no top-level side effects — `import 'unity-mcp-cli'` never parses argv and never writes to stdout or stderr.
631
+
632
+ See [`CHANGELOG.md`](CHANGELOG.md#0670---2026-04-21) for the full list of exported functions and types.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ import './index.js';
package/dist/cli.js ADDED
@@ -0,0 +1,11 @@
1
+ // CLI entry for `unity-mcp-cli`, exported at the `./cli` subpath.
2
+ //
3
+ // Importing this file runs the Commander-based CLI exactly as the
4
+ // `unity-mcp-cli` binary does — it parses argv, dispatches subcommands,
5
+ // and exits the process.
6
+ //
7
+ // Consumers that want the programmatic, side-effect-free surface
8
+ // should import the package root (`import { installPlugin } from
9
+ // "unity-mcp-cli"`) — NOT this file.
10
+ import './index.js';
11
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,wEAAwE;AACxE,yBAAyB;AACzB,EAAE;AACF,iEAAiE;AACjE,iEAAiE;AACjE,qCAAqC;AAErC,OAAO,YAAY,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { Command } from 'commander';
2
+ import { type UnityConnectionConfig } from '../utils/config.js';
3
+ /**
4
+ * Produce a new config object with local-mode fields pinned to `url` / `token`.
5
+ * Preserves all other keys from `current` (immutable update — does not mutate `current`).
6
+ *
7
+ * The plugin-side ConnectionMode enum only accepts "Custom" (local) or "Cloud".
8
+ * We intentionally normalize any integer (0 → Custom, 1 → Cloud) representation
9
+ * to the canonical string "Custom" so the on-disk file is explicit.
10
+ */
11
+ export declare function pinLocalModeConfig(current: Readonly<UnityConnectionConfig>, url: string, token: string): UnityConnectionConfig;
12
+ /**
13
+ * Returns true iff `current` already reflects local mode with matching url + token.
14
+ * Used to make bootstrap-local a no-op when nothing would change on disk.
15
+ */
16
+ export declare function isAlreadyPinned(current: Readonly<UnityConnectionConfig>, url: string, token: string): boolean;
17
+ export declare const bootstrapLocalCommand: Command;
@@ -0,0 +1,83 @@
1
+ // Copyright (c) 2025 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ import { Command } from 'commander';
4
+ import * as path from 'path';
5
+ import * as fs from 'fs';
6
+ import * as ui from '../utils/ui.js';
7
+ import { verbose } from '../utils/ui.js';
8
+ import { resolveAndValidateProjectPath } from '../utils/connection.js';
9
+ import { readConfig, writeConfig, } from '../utils/config.js';
10
+ /**
11
+ * Produce a new config object with local-mode fields pinned to `url` / `token`.
12
+ * Preserves all other keys from `current` (immutable update — does not mutate `current`).
13
+ *
14
+ * The plugin-side ConnectionMode enum only accepts "Custom" (local) or "Cloud".
15
+ * We intentionally normalize any integer (0 → Custom, 1 → Cloud) representation
16
+ * to the canonical string "Custom" so the on-disk file is explicit.
17
+ */
18
+ export function pinLocalModeConfig(current, url, token) {
19
+ return {
20
+ ...current,
21
+ connectionMode: 'Custom',
22
+ host: url,
23
+ token,
24
+ };
25
+ }
26
+ /**
27
+ * Returns true iff `current` already reflects local mode with matching url + token.
28
+ * Used to make bootstrap-local a no-op when nothing would change on disk.
29
+ */
30
+ export function isAlreadyPinned(current, url, token) {
31
+ // Accept both "Custom" (string) and 0 (legacy int) as already-local.
32
+ // Anything else (including "Cloud", 1, or unset) means we still need to pin.
33
+ const mode = current.connectionMode;
34
+ if (mode !== 'Custom' && mode !== 0)
35
+ return false;
36
+ return current.host === url && current.token === token;
37
+ }
38
+ export const bootstrapLocalCommand = new Command('bootstrap-local')
39
+ .description('Pin a Unity project\'s MCP config to local mode with the given URL/token. Idempotent.')
40
+ .argument('[path]', 'Unity project path (defaults to cwd)')
41
+ .option('--path <path>', 'Unity project path (defaults to cwd)')
42
+ .requiredOption('--url <url>', 'Local MCP server URL to pin (e.g. http://localhost:5140/)')
43
+ .requiredOption('--token <token>', 'Local MCP bearer token to pin')
44
+ .option('--dry-run', 'Report what would change without writing to disk')
45
+ .action(async (positionalPath, options) => {
46
+ const projectPath = resolveAndValidateProjectPath(positionalPath, options);
47
+ // commander's requiredOption guarantees url/token are defined before the action runs.
48
+ const url = options.url.replace(/\/$/, '');
49
+ const token = options.token;
50
+ verbose(`Project path: ${projectPath}`);
51
+ verbose(`Target URL: ${url}`);
52
+ verbose(`Target token: ${'*'.repeat(Math.min(token.length, 8))}... (length ${token.length})`);
53
+ const configPath = path.join(projectPath, 'UserSettings', 'AI-Game-Developer-Config.json');
54
+ const existedBefore = fs.existsSync(configPath);
55
+ // Use readConfig (not getOrCreateConfig) so --dry-run truly has no side effects.
56
+ // When the file doesn't exist we pin onto an empty base; this mirrors a fresh
57
+ // worktree where the plugin has not yet written its default Cloud config.
58
+ const current = readConfig(projectPath) ?? {};
59
+ if (isAlreadyPinned(current, url, token)) {
60
+ ui.success(`MCP config already pinned to local mode (${url}). No changes written.`);
61
+ return;
62
+ }
63
+ const next = pinLocalModeConfig(current, url, token);
64
+ if (options.dryRun) {
65
+ ui.heading('bootstrap-local (dry run)');
66
+ ui.label('Config file', configPath);
67
+ ui.label('connectionMode', `${String(current.connectionMode ?? 'unset')} → Custom`);
68
+ ui.label('host', `${String(current.host ?? 'unset')} → ${url}`);
69
+ ui.label('token', `${current.token ? '<previous>' : '<unset>'} → <supplied>`);
70
+ ui.info('No changes written (--dry-run).');
71
+ return;
72
+ }
73
+ writeConfig(projectPath, next);
74
+ ui.heading('bootstrap-local');
75
+ ui.label('Config file', configPath);
76
+ ui.label('connectionMode', 'Custom');
77
+ ui.label('host', url);
78
+ ui.label('token', '<supplied>');
79
+ ui.success(existedBefore
80
+ ? 'Config updated to pin local mode.'
81
+ : 'Config created and pinned to local mode.');
82
+ });
83
+ //# sourceMappingURL=bootstrap-local.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-local.js","sourceRoot":"","sources":["../../src/commands/bootstrap-local.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { Command } from 'commander';
2
+ import { type SupportedPlatform } from '../utils/unity-shutdown.js';
3
+ export interface CloseOptions {
4
+ timeout?: string;
5
+ force?: boolean;
6
+ }
7
+ /**
8
+ * Resolve the project path argument to an absolute, canonical path.
9
+ *
10
+ * Mirrors the convention used by `open` / `wait-for-ready` — accepts a
11
+ * positional argument or `process.cwd()` fallback, normalises symlinks where
12
+ * possible, and trims any trailing path separator. Exported for unit tests.
13
+ */
14
+ export declare function resolveCloseProjectPath(positionalPath: string | undefined, cwd: string): string;
15
+ /**
16
+ * Returns true when `<projectPath>/ProjectSettings/ProjectVersion.txt` exists.
17
+ *
18
+ * The issue's acceptance criteria require refusing to act on any path that
19
+ * does not look like a Unity project root (`ProjectVersion.txt` is the
20
+ * canonical marker). This is intentionally narrower than the `open`
21
+ * subcommand's check — the goal here is to defend against accidental
22
+ * "kill all Unity processes on host" invocations, so a positive ID of
23
+ * "this is a Unity project" is required.
24
+ *
25
+ * Exported for unit tests.
26
+ */
27
+ export declare function isUnityProjectRoot(projectPath: string): boolean;
28
+ /**
29
+ * Parse a positive-integer `--timeout` value (seconds). Returns the parsed
30
+ * value, or `null` when the input is not a valid positive integer.
31
+ *
32
+ * The `undefined` branch falls back to the default for ergonomic
33
+ * direct-call use (e.g., unit tests). The action handler never reaches it
34
+ * because commander always materialises the configured default string.
35
+ *
36
+ * Exported for unit tests.
37
+ */
38
+ export declare function parseTimeoutSeconds(raw: string | undefined): number | null;
39
+ /**
40
+ * Resolve the editor PID for a given project path.
41
+ *
42
+ * Reads `<project>/Temp/UnityLockfile` (4 bytes LE uint32) and enumerates
43
+ * Unity processes by command line, then prefers the lockfile PID when it
44
+ * is alive and matches the enumerated process for this project (handles
45
+ * stale lockfiles); otherwise uses the enumerated PID.
46
+ *
47
+ * Symlink/realpath handling: `resolveCloseProjectPath` canonicalises via
48
+ * `realpathSync` while `findUnityProcess` only `path.resolve`s the cmdline
49
+ * `-projectPath` argument. If Unity was launched via a symlink, the first
50
+ * lookup misses. We retry against the un-canonicalised form so a symlinked
51
+ * launch path doesn't silently degrade close to a no-op.
52
+ *
53
+ * Returns `null` when no live editor matches the project. Exported for tests.
54
+ */
55
+ export declare function resolveEditorPid(projectPath: string, platform?: SupportedPlatform): number | null;
56
+ export declare const closeCommand: Command;
@@ -0,0 +1,186 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { platform as nodePlatform } from 'os';
5
+ import * as ui from '../utils/ui.js';
6
+ import { verbose } from '../utils/ui.js';
7
+ import { findUnityProcess } from '../utils/unity-process.js';
8
+ import { readLockfilePid, isProcessAlive, sendGracefulShutdown, sendForceKill, waitForExit, } from '../utils/unity-shutdown.js';
9
+ const DEFAULT_TIMEOUT_SECONDS = 30;
10
+ /**
11
+ * Resolve the project path argument to an absolute, canonical path.
12
+ *
13
+ * Mirrors the convention used by `open` / `wait-for-ready` — accepts a
14
+ * positional argument or `process.cwd()` fallback, normalises symlinks where
15
+ * possible, and trims any trailing path separator. Exported for unit tests.
16
+ */
17
+ export function resolveCloseProjectPath(positionalPath, cwd) {
18
+ const explicit = positionalPath ?? cwd;
19
+ let resolved = path.resolve(explicit);
20
+ // realpathSync collapses symlinks, ".." segments, and trailing separators
21
+ // when the path exists. When it does not, fall through to the resolved
22
+ // form so the caller can produce a "path does not exist" error.
23
+ try {
24
+ resolved = fs.realpathSync(resolved);
25
+ }
26
+ catch {
27
+ // Path may not exist — let the caller decide what to do.
28
+ }
29
+ return resolved;
30
+ }
31
+ /**
32
+ * Returns true when `<projectPath>/ProjectSettings/ProjectVersion.txt` exists.
33
+ *
34
+ * The issue's acceptance criteria require refusing to act on any path that
35
+ * does not look like a Unity project root (`ProjectVersion.txt` is the
36
+ * canonical marker). This is intentionally narrower than the `open`
37
+ * subcommand's check — the goal here is to defend against accidental
38
+ * "kill all Unity processes on host" invocations, so a positive ID of
39
+ * "this is a Unity project" is required.
40
+ *
41
+ * Exported for unit tests.
42
+ */
43
+ export function isUnityProjectRoot(projectPath) {
44
+ return fs.existsSync(path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt'));
45
+ }
46
+ /**
47
+ * Parse a positive-integer `--timeout` value (seconds). Returns the parsed
48
+ * value, or `null` when the input is not a valid positive integer.
49
+ *
50
+ * The `undefined` branch falls back to the default for ergonomic
51
+ * direct-call use (e.g., unit tests). The action handler never reaches it
52
+ * because commander always materialises the configured default string.
53
+ *
54
+ * Exported for unit tests.
55
+ */
56
+ export function parseTimeoutSeconds(raw) {
57
+ if (raw === undefined)
58
+ return DEFAULT_TIMEOUT_SECONDS;
59
+ const parsed = Number(raw);
60
+ if (!Number.isInteger(parsed) || parsed <= 0)
61
+ return null;
62
+ return parsed;
63
+ }
64
+ /**
65
+ * Resolve the editor PID for a given project path.
66
+ *
67
+ * Reads `<project>/Temp/UnityLockfile` (4 bytes LE uint32) and enumerates
68
+ * Unity processes by command line, then prefers the lockfile PID when it
69
+ * is alive and matches the enumerated process for this project (handles
70
+ * stale lockfiles); otherwise uses the enumerated PID.
71
+ *
72
+ * Symlink/realpath handling: `resolveCloseProjectPath` canonicalises via
73
+ * `realpathSync` while `findUnityProcess` only `path.resolve`s the cmdline
74
+ * `-projectPath` argument. If Unity was launched via a symlink, the first
75
+ * lookup misses. We retry against the un-canonicalised form so a symlinked
76
+ * launch path doesn't silently degrade close to a no-op.
77
+ *
78
+ * Returns `null` when no live editor matches the project. Exported for tests.
79
+ */
80
+ export function resolveEditorPid(projectPath, platform = nodePlatform()) {
81
+ const lockPid = readLockfilePid(projectPath);
82
+ // Enumerate Unity processes once. `findUnityProcess` shells out (3s timeout
83
+ // on Windows via PowerShell+CIM, 5s on POSIX via `ps`), so calling it twice
84
+ // on the stale-lockfile path used to roughly double close latency.
85
+ let proc = findUnityProcess(projectPath);
86
+ // Symlink fallback: if the canonical realpath form found no process, retry
87
+ // with `path.resolve` only (no realpath collapse) — that matches what
88
+ // `findUnityProcess` derives from the cmdline `-projectPath` argument.
89
+ if (!proc) {
90
+ const resolvedNoRealpath = path.resolve(projectPath);
91
+ if (resolvedNoRealpath !== projectPath) {
92
+ proc = findUnityProcess(resolvedNoRealpath);
93
+ }
94
+ }
95
+ if (lockPid !== null && isProcessAlive(lockPid, platform)) {
96
+ if (proc && proc.pid === lockPid) {
97
+ verbose(`Lockfile PID ${lockPid} confirmed via process enumeration`);
98
+ return lockPid;
99
+ }
100
+ // Lockfile alive but no matching enumerated process — the lockfile may
101
+ // still be authoritative if the asymmetry is purely realpath/symlink
102
+ // related (process listed under symlink path while we queried by realpath).
103
+ // Trust the live lockfile PID in that case rather than discarding it.
104
+ if (!proc) {
105
+ verbose(`Lockfile PID ${lockPid} alive but no enumerated match — using lockfile PID (likely symlink/path mismatch)`);
106
+ return lockPid;
107
+ }
108
+ verbose(`Lockfile PID ${lockPid} did not match enumerated Unity process for project — treating as stale`);
109
+ }
110
+ return proc ? proc.pid : null;
111
+ }
112
+ export const closeCommand = new Command('close')
113
+ .description('Gracefully terminate the Unity Editor instance running for a given project path')
114
+ .argument('[path]', 'Path to the Unity project (defaults to current directory)')
115
+ .option('--timeout <seconds>', `Polite-quit timeout in seconds (default: ${DEFAULT_TIMEOUT_SECONDS})`, String(DEFAULT_TIMEOUT_SECONDS))
116
+ .option('--force', 'Hard-kill the Editor if it does not exit within --timeout')
117
+ .action(async (positionalPath, options) => {
118
+ const projectPath = resolveCloseProjectPath(positionalPath, process.cwd());
119
+ verbose(`close invoked for project: ${projectPath}`);
120
+ if (!fs.existsSync(projectPath)) {
121
+ ui.error(`Project path does not exist: ${projectPath}`);
122
+ process.exit(1);
123
+ }
124
+ if (!isUnityProjectRoot(projectPath)) {
125
+ ui.error(`Not a Unity project root: ${projectPath}`);
126
+ process.exit(1);
127
+ }
128
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
129
+ if (timeoutSeconds === null) {
130
+ ui.error(`Invalid --timeout value: "${options.timeout}". Must be a positive integer (seconds).`);
131
+ process.exit(1);
132
+ }
133
+ const platform = nodePlatform();
134
+ const pid = resolveEditorPid(projectPath, platform);
135
+ if (pid === null) {
136
+ ui.success(`no running Editor for project at ${projectPath}`);
137
+ process.exit(0);
138
+ }
139
+ ui.heading('Closing Unity Editor');
140
+ ui.label('Project', projectPath);
141
+ ui.label('PID', String(pid));
142
+ ui.label('Timeout', `${timeoutSeconds}s`);
143
+ ui.label('Force', options.force ? 'yes' : 'no');
144
+ ui.divider();
145
+ // TOCTOU window: between resolveEditorPid returning and the signal call
146
+ // below, the original Unity process could exit and the OS could reuse
147
+ // the same numeric PID for an unrelated process (more likely on POSIX,
148
+ // where PIDs cycle aggressively). We accept the residual risk — the
149
+ // lockfile cross-check above already narrows it, holding a lock across
150
+ // a spawned Editor's lifetime is impractical, and the signals we send
151
+ // (SIGTERM / WM_CLOSE) are best-effort, not destructive.
152
+ const spinner = ui.startSpinner(`Sending polite-quit to PID ${pid}...`);
153
+ const sent = sendGracefulShutdown(pid, platform);
154
+ if (!sent) {
155
+ spinner.error(`Failed to deliver polite-quit signal to PID ${pid}`);
156
+ process.exit(1);
157
+ }
158
+ spinner.text = `Waiting up to ${timeoutSeconds}s for PID ${pid} to exit...`;
159
+ const timeoutMs = timeoutSeconds * 1000;
160
+ const exited = await waitForExit(pid, timeoutMs, platform);
161
+ if (exited) {
162
+ spinner.success(`Unity Editor (PID ${pid}) exited cleanly`);
163
+ process.exit(0);
164
+ }
165
+ if (!options.force) {
166
+ spinner.error(`Unity Editor (PID ${pid}) did not exit within ${timeoutSeconds}s. ` +
167
+ `Re-run with --force to hard-kill, or close it manually.`);
168
+ process.exit(1);
169
+ }
170
+ spinner.text = `Force-killing PID ${pid}...`;
171
+ const killed = sendForceKill(pid, platform);
172
+ if (!killed) {
173
+ spinner.error(`Failed to force-kill PID ${pid}`);
174
+ process.exit(1);
175
+ }
176
+ // Brief secondary wait so we report a true terminal state — the OS may
177
+ // need a moment to reap the process even after SIGKILL / `taskkill /F`.
178
+ const reaped = await waitForExit(pid, 5000, platform);
179
+ if (reaped) {
180
+ spinner.success(`Unity Editor (PID ${pid}) force-killed`);
181
+ process.exit(0);
182
+ }
183
+ spinner.error(`Unity Editor (PID ${pid}) is still alive after force-kill`);
184
+ process.exit(1);
185
+ });
186
+ //# sourceMappingURL=close.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close.js","sourceRoot":"","sources":["../../src/commands/close.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,WAAW,GAEZ,MAAM,4BAA4B,CAAC;AAOpC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAEnC;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,cAAkC,EAAE,GAAW;IACrF,MAAM,QAAQ,GAAG,cAAc,IAAI,GAAG,CAAC;IACvC,IAAI,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEtC,0EAA0E;IAC1E,uEAAuE;IACvE,gEAAgE;IAChE,IAAI,CAAC;QACH,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,yDAAyD;IAC3D,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IACpD,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,EAAE,oBAAoB,CAAC,CAAC,CAAC;AACxF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAuB;IACzD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,uBAAuB,CAAC;IACtD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1D,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,WAA8B,YAAY,EAAuB;IACrH,MAAM,OAAO,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAC7C,4EAA4E;IAC5E,4EAA4E;IAC5E,mEAAmE;IACnE,IAAI,IAAI,GAAG,gBAAgB,CAAC,WAAW,CAAC,CAAC;IAEzC,2EAA2E;IAC3E,sEAAsE;IACtE,uEAAuE;IACvE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACrD,IAAI,kBAAkB,KAAK,WAAW,EAAE,CAAC;YACvC,IAAI,GAAG,gBAAgB,CAAC,kBAAkB,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,IAAI,OAAO,KAAK,IAAI,IAAI,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;QAC1D,IAAI,IAAI,IAAI,IAAI,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YACjC,OAAO,CAAC,gBAAgB,OAAO,oCAAoC,CAAC,CAAC;YACrE,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,uEAAuE;QACvE,qEAAqE;QACrE,4EAA4E;QAC5E,sEAAsE;QACtE,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,gBAAgB,OAAO,oFAAoF,CAAC,CAAC;YACrH,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,CAAC,gBAAgB,OAAO,yEAAyE,CAAC,CAAC;IAC5G,CAAC;IAED,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;KAC7C,WAAW,CAAC,iFAAiF,CAAC;KAC9F,QAAQ,CAAC,QAAQ,EAAE,2DAA2D,CAAC;KAC/E,MAAM,CAAC,qBAAqB,EAAE,4CAA4C,uBAAuB,GAAG,EAAE,MAAM,CAAC,uBAAuB,CAAC,CAAC;KACtI,MAAM,CAAC,SAAS,EAAE,2DAA2D,CAAC;KAC9E,MAAM,CAAC,KAAK,EAAE,cAAkC,EAAE,OAAqB,EAAE,EAAE;IAC1E,MAAM,WAAW,GAAG,uBAAuB,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3E,OAAO,CAAC,8BAA8B,WAAW,EAAE,CAAC,CAAC;IAErD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,KAAK,CAAC,gCAAgC,WAAW,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC;QACrC,EAAE,CAAC,KAAK,CAAC,6BAA6B,WAAW,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,cAAc,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC5B,EAAE,CAAC,KAAK,CAAC,6BAA6B,OAAO,CAAC,OAAO,0CAA0C,CAAC,CAAC;QACjG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,EAAuB,CAAC;IACrD,MAAM,GAAG,GAAG,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAEpD,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,EAAE,CAAC,OAAO,CAAC,oCAAoC,WAAW,EAAE,CAAC,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACnC,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IACjC,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,cAAc,GAAG,CAAC,CAAC;IAC1C,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChD,EAAE,CAAC,OAAO,EAAE,CAAC;IAEb,wEAAwE;IACxE,sEAAsE;IACtE,uEAAuE;IACvE,oEAAoE;IACpE,uEAAuE;IACvE,sEAAsE;IACtE,yDAAyD;IACzD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,8BAA8B,GAAG,KAAK,CAAC,CAAC;IACxE,MAAM,IAAI,GAAG,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CAAC,KAAK,CAAC,+CAA+C,GAAG,EAAE,CAAC,CAAC;QACpE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,GAAG,iBAAiB,cAAc,aAAa,GAAG,aAAa,CAAC;IAC5E,MAAM,SAAS,GAAG,cAAc,GAAG,IAAI,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;IAE3D,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,OAAO,CAAC,qBAAqB,GAAG,kBAAkB,CAAC,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,KAAK,CACX,qBAAqB,GAAG,yBAAyB,cAAc,KAAK;YAClE,yDAAyD,CAC5D,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,IAAI,GAAG,qBAAqB,GAAG,KAAK,CAAC;IAC7C,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,4BAA4B,GAAG,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,uEAAuE;IACvE,wEAAwE;IACxE,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACtD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,OAAO,CAAC,qBAAqB,GAAG,gBAAgB,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,qBAAqB,GAAG,mCAAmC,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -1,12 +1,21 @@
1
1
  import { Command } from 'commander';
2
2
  import * as path from 'path';
3
- import * as fs from 'fs';
4
- import { getOrCreateConfig, writeConfig, updateFeatures } from '../utils/config.js';
5
3
  import * as ui from '../utils/ui.js';
6
4
  import { verbose } from '../utils/ui.js';
5
+ import { configure } from '../lib/configure.js';
7
6
  function parseCommaSeparated(value) {
8
7
  return value.split(',').map((s) => s.trim()).filter(Boolean);
9
8
  }
9
+ function buildAction(enable, disable, enableAll, disableAll) {
10
+ if (!enable && !disable && !enableAll && !disableAll)
11
+ return undefined;
12
+ return {
13
+ enableNames: enable,
14
+ disableNames: disable,
15
+ enableAll,
16
+ disableAll,
17
+ };
18
+ }
10
19
  export const configureCommand = new Command('configure')
11
20
  .description('Configure MCP tools, prompts, and resources in AI-Game-Developer-Config.json')
12
21
  .argument('[path]', 'Path to the Unity project')
@@ -31,63 +40,46 @@ export const configureCommand = new Command('configure')
31
40
  process.exit(1);
32
41
  }
33
42
  const projectPath = path.resolve(resolvedPath);
34
- if (!fs.existsSync(projectPath)) {
35
- ui.error(`Project path does not exist: ${projectPath}`);
43
+ verbose(`Loading config for project: ${projectPath}`);
44
+ // `--list` must be read-only: previously this command short-
45
+ // circuited before applying any enable/disable flags. Preserve
46
+ // that semantics by suppressing the mutating actions whenever
47
+ // `options.list` is set, regardless of other flags.
48
+ const toolsAction = buildAction(options.enableTools, options.disableTools, options.enableAllTools, options.disableAllTools);
49
+ const promptsAction = buildAction(options.enablePrompts, options.disablePrompts, options.enableAllPrompts, options.disableAllPrompts);
50
+ const resourcesAction = buildAction(options.enableResources, options.disableResources, options.enableAllResources, options.disableAllResources);
51
+ const result = await configure({
52
+ unityProjectPath: projectPath,
53
+ tools: options.list ? undefined : toolsAction,
54
+ prompts: options.list ? undefined : promptsAction,
55
+ resources: options.list ? undefined : resourcesAction,
56
+ });
57
+ if (!result.success) {
58
+ ui.error(result.error?.message ?? 'Unknown error');
36
59
  process.exit(1);
37
60
  }
38
- verbose(`Loading config for project: ${projectPath}`);
39
- const config = getOrCreateConfig(projectPath);
40
- if (options.list) {
61
+ if (options.list && result.snapshot) {
41
62
  ui.heading('Current configuration');
42
- ui.label('Host', config.host ?? 'not set');
43
- ui.label('Keep Connected', String(config.keepConnected ?? false));
44
- ui.label('Transport', config.transportMethod ?? 'streamableHttp');
45
- ui.label('Auth', config.authOption ?? 'none');
63
+ ui.label('Host', result.snapshot.host ?? 'not set');
64
+ ui.label('Keep Connected', String(result.snapshot.keepConnected ?? false));
65
+ ui.label('Transport', result.snapshot.transportMethod ?? 'streamableHttp');
66
+ ui.label('Auth', result.snapshot.authOption ?? 'none');
46
67
  const printFeatures = (featureLabel, features) => {
47
- if (!features || features.length === 0) {
48
- ui.heading(featureLabel);
68
+ ui.heading(featureLabel);
69
+ if (features.length === 0) {
49
70
  ui.info('(none configured - all enabled by default)');
50
71
  return;
51
72
  }
52
- ui.heading(featureLabel);
53
73
  for (const f of features) {
54
74
  ui.featureRow(f.name, f.enabled);
55
75
  }
56
76
  };
57
- printFeatures('Tools', config.tools);
58
- printFeatures('Prompts', config.prompts);
59
- printFeatures('Resources', config.resources);
77
+ printFeatures('Tools', result.snapshot.tools);
78
+ printFeatures('Prompts', result.snapshot.prompts);
79
+ printFeatures('Resources', result.snapshot.resources);
60
80
  return;
61
81
  }
62
- // Apply tool changes
63
- if (options.enableTools || options.disableTools || options.enableAllTools || options.disableAllTools) {
64
- updateFeatures(config, 'tools', {
65
- enableNames: options.enableTools,
66
- disableNames: options.disableTools,
67
- enableAll: options.enableAllTools,
68
- disableAll: options.disableAllTools,
69
- });
70
- }
71
- // Apply prompt changes
72
- if (options.enablePrompts || options.disablePrompts || options.enableAllPrompts || options.disableAllPrompts) {
73
- updateFeatures(config, 'prompts', {
74
- enableNames: options.enablePrompts,
75
- disableNames: options.disablePrompts,
76
- enableAll: options.enableAllPrompts,
77
- disableAll: options.disableAllPrompts,
78
- });
79
- }
80
- // Apply resource changes
81
- if (options.enableResources || options.disableResources || options.enableAllResources || options.disableAllResources) {
82
- updateFeatures(config, 'resources', {
83
- enableNames: options.enableResources,
84
- disableNames: options.disableResources,
85
- enableAll: options.enableAllResources,
86
- disableAll: options.disableAllResources,
87
- });
88
- }
89
82
  verbose('Writing updated configuration');
90
- writeConfig(projectPath, config);
91
83
  ui.success('Configuration updated successfully.');
92
84
  });
93
85
  //# sourceMappingURL=configure.js.map