unity-open-mcp 1.2.3 → 1.4.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/README.md +34 -2
- package/dist/agent-identity.d.ts +12 -0
- package/dist/agent-identity.js +2 -0
- package/dist/agent-identity.js.map +1 -1
- package/dist/batch-spawn.d.ts +39 -0
- package/dist/batch-spawn.js +287 -69
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/build-capabilities.js +2 -0
- package/dist/capabilities/build-capabilities.js.map +1 -1
- package/dist/capabilities/discovery.d.ts +51 -0
- package/dist/capabilities/discovery.js +87 -0
- package/dist/capabilities/discovery.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +6 -0
- package/dist/capabilities/lifecycle.js +17 -1
- package/dist/capabilities/lifecycle.js.map +1 -1
- package/dist/capabilities/tool-groups.js +1 -0
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/child-supervision.d.ts +6 -0
- package/dist/child-supervision.js +12 -0
- package/dist/child-supervision.js.map +1 -1
- package/dist/cli/args.d.ts +24 -2
- package/dist/cli/args.js +97 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/cli.js +42 -3
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help-text.d.ts +1 -0
- package/dist/cli/help-text.js +83 -3
- package/dist/cli/help-text.js.map +1 -1
- package/dist/cli/json-guards.d.ts +2 -0
- package/dist/cli/json-guards.js +5 -0
- package/dist/cli/json-guards.js.map +1 -0
- package/dist/cli/setup-command.d.ts +116 -0
- package/dist/cli/setup-command.js +696 -0
- package/dist/cli/setup-command.js.map +1 -0
- package/dist/cli/update-command.d.ts +30 -0
- package/dist/cli/update-command.js +271 -0
- package/dist/cli/update-command.js.map +1 -0
- package/dist/compile-verify.d.ts +3 -0
- package/dist/compile-verify.js +4 -0
- package/dist/compile-verify.js.map +1 -1
- package/dist/constants.d.ts +10 -2
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/dialog-dismiss.d.ts +108 -12
- package/dist/dialog-dismiss.js +294 -109
- package/dist/dialog-dismiss.js.map +1 -1
- package/dist/dialog-policy.d.ts +55 -9
- package/dist/dialog-policy.js +85 -15
- package/dist/dialog-policy.js.map +1 -1
- package/dist/editor-search.d.ts +2 -0
- package/dist/editor-search.js +8 -0
- package/dist/editor-search.js.map +1 -0
- package/dist/gate-error.d.ts +3 -0
- package/dist/gate-error.js.map +1 -1
- package/dist/index.js +56 -1
- package/dist/index.js.map +1 -1
- package/dist/instance-discovery.d.ts +16 -0
- package/dist/instance-discovery.js +45 -0
- package/dist/instance-discovery.js.map +1 -1
- package/dist/jobs/adapters.d.ts +15 -0
- package/dist/jobs/adapters.js +147 -0
- package/dist/jobs/adapters.js.map +1 -0
- package/dist/jobs/handler.d.ts +3 -0
- package/dist/jobs/handler.js +29 -0
- package/dist/jobs/handler.js.map +1 -0
- package/dist/jobs/job-manager.d.ts +123 -0
- package/dist/jobs/job-manager.js +290 -0
- package/dist/jobs/job-manager.js.map +1 -0
- package/dist/live-client.d.ts +50 -2
- package/dist/live-client.js +221 -26
- package/dist/live-client.js.map +1 -1
- package/dist/local-tools.d.ts +17 -0
- package/dist/local-tools.js +47 -0
- package/dist/local-tools.js.map +1 -0
- package/dist/project-path.d.ts +99 -0
- package/dist/project-path.js +171 -0
- package/dist/project-path.js.map +1 -0
- package/dist/release-lookup.d.ts +32 -0
- package/dist/release-lookup.js +147 -0
- package/dist/release-lookup.js.map +1 -0
- package/dist/routers.d.ts +17 -3
- package/dist/routers.js +29 -8
- package/dist/routers.js.map +1 -1
- package/dist/running-unity.d.ts +21 -0
- package/dist/running-unity.js +50 -0
- package/dist/running-unity.js.map +1 -1
- package/dist/schema-defaults.d.ts +4 -3
- package/dist/schema-defaults.js +20 -6
- package/dist/schema-defaults.js.map +1 -1
- package/dist/server-instructions.js +7 -1
- package/dist/server-instructions.js.map +1 -1
- package/dist/server.d.ts +10 -3
- package/dist/server.js +24 -25
- package/dist/server.js.map +1 -1
- package/dist/setup/codex-toml.d.ts +23 -0
- package/dist/setup/codex-toml.js +514 -0
- package/dist/setup/codex-toml.js.map +1 -0
- package/dist/setup/portable-config.d.ts +59 -0
- package/dist/setup/portable-config.js +241 -0
- package/dist/setup/portable-config.js.map +1 -0
- package/dist/skill/SKILL.md +68 -0
- package/dist/skill/client-paths.js +2 -4
- package/dist/skill/client-paths.js.map +1 -1
- package/dist/skill/copy-references.d.ts +2 -0
- package/dist/skill/copy-references.js +16 -0
- package/dist/skill/copy-references.js.map +1 -0
- package/dist/skill/generate-skill.js +5 -3
- package/dist/skill/generate-skill.js.map +1 -1
- package/dist/skill/references/batch-and-gates.md +142 -0
- package/dist/skill/references/compile-and-safe-mode.md +163 -0
- package/dist/skill/references/discovery-and-groups.md +30 -0
- package/dist/skill/references/routing-and-lifecycle.md +62 -0
- package/dist/skill/references/senses-and-tests.md +41 -0
- package/dist/skill/references/yaml-and-offline-work.md +33 -0
- package/dist/templates/mcp-wrapper.sh +35 -0
- package/dist/tool-contract.d.ts +8 -0
- package/dist/tool-contract.js +253 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-router.d.ts +62 -7
- package/dist/tool-router.js +455 -17
- package/dist/tool-router.js.map +1 -1
- package/dist/tool-session-state.d.ts +6 -0
- package/dist/tool-session-state.js +14 -0
- package/dist/tool-session-state.js.map +1 -1
- package/dist/tools/agent-capabilities.js +9 -0
- package/dist/tools/agent-capabilities.js.map +1 -1
- package/dist/tools/batch-execute.js +17 -14
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/bridge-status.js +4 -5
- package/dist/tools/bridge-status.js.map +1 -1
- package/dist/tools/compile-check.js +6 -4
- package/dist/tools/compile-check.js.map +1 -1
- package/dist/tools/component-get.js +2 -2
- package/dist/tools/component-get.js.map +1 -1
- package/dist/tools/component-modify.js +4 -4
- package/dist/tools/component-modify.js.map +1 -1
- package/dist/tools/execute-csharp.js +8 -4
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/gameobject-modify.js +2 -1
- package/dist/tools/gameobject-modify.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +10 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-key.js +12 -14
- package/dist/tools/inputsim-key.js.map +1 -1
- package/dist/tools/inputsim-pointer.js +3 -3
- package/dist/tools/inputsim-pointer.js.map +1 -1
- package/dist/tools/inputsim-step.js +7 -8
- package/dist/tools/inputsim-step.js.map +1 -1
- package/dist/tools/inputsim-touch.js +9 -10
- package/dist/tools/inputsim-touch.js.map +1 -1
- package/dist/tools/jobs.d.ts +40 -0
- package/dist/tools/jobs.js +18 -0
- package/dist/tools/jobs.js.map +1 -0
- package/dist/tools/manage-tools.js +10 -5
- package/dist/tools/manage-tools.js.map +1 -1
- package/dist/tools/project-commands.d.ts +40 -0
- package/dist/tools/project-commands.js +23 -0
- package/dist/tools/project-commands.js.map +1 -0
- package/dist/tools/read-compile-errors.js +14 -93
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/scan-paths.js +4 -4
- package/dist/tools/scan-paths.js.map +1 -1
- package/dist/tools/schema-fragments.d.ts +15 -3
- package/dist/tools/schema-fragments.js +23 -13
- package/dist/tools/schema-fragments.js.map +1 -1
- package/dist/tools/upgrade.d.ts +40 -0
- package/dist/tools/upgrade.js +55 -0
- package/dist/tools/upgrade.js.map +1 -0
- package/dist/unity-install-discovery.d.ts +24 -0
- package/dist/unity-install-discovery.js +113 -4
- package/dist/unity-install-discovery.js.map +1 -1
- package/package.json +4 -3
- package/templates/mcp-wrapper.sh +35 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import { copySkillReferences } from "../skill/copy-references.js";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { clientSkillRelativePath, knownClientKeys, resolveTemplateSkillPath, } from "../skill/client-paths.js";
|
|
8
|
+
import { PROJECT_PATH_ENV_VAR } from "../constants.js";
|
|
9
|
+
import { validateUnityProjectRoot } from "../project-path.js";
|
|
10
|
+
import { isRecord } from "./json-guards.js";
|
|
11
|
+
import { CodexTomlError, mergeCodexServer, renderCodexServer } from "../setup/codex-toml.js";
|
|
12
|
+
import { portableEnv, portableServerArgs, portableSupportFor, renderWrapperScript, wrapperRelativePath, } from "../setup/portable-config.js";
|
|
13
|
+
const BRIDGE_PACKAGE = "com.alexeyperov.unity-open-mcp-bridge";
|
|
14
|
+
const VERIFY_PACKAGE = "com.alexeyperov.unity-open-mcp-verify";
|
|
15
|
+
const REPOSITORY_URL = "https://github.com/AlexeyPerov/unity-open-mcp.git";
|
|
16
|
+
const SERVER_KEY = "unity-open-mcp";
|
|
17
|
+
/**
|
|
18
|
+
* Clients `setup` can write a config for. The skill key follows
|
|
19
|
+
* `mcpClientMapping` in skills/client-paths.json (Codex installs into the
|
|
20
|
+
* shared `.agents/skills`); a unit test keeps the two in step.
|
|
21
|
+
*/
|
|
22
|
+
export const SETUP_CLIENTS = [
|
|
23
|
+
{ id: "cursor", configPath: ".cursor/mcp.json", format: "mcpServers", skillKey: "cursor", catalogId: "cursor" },
|
|
24
|
+
{ id: "claude", configPath: ".mcp.json", format: "mcpServers", skillKey: "claude", catalogId: "claude-code" },
|
|
25
|
+
{ id: "zcode", configPath: ".zcode/config.json", format: "zcode", skillKey: "agents", catalogId: "zcode" },
|
|
26
|
+
{ id: "vscode", configPath: ".vscode/mcp.json", format: "vscode", skillKey: "github", catalogId: "vscode-copilot" },
|
|
27
|
+
{ id: "codex", configPath: ".codex/config.toml", format: "codex", skillKey: "agents", catalogId: "codex" },
|
|
28
|
+
{ id: "opencode", configPath: "opencode.json", format: "opencode", skillKey: "opencode", catalogId: "opencode" },
|
|
29
|
+
{ id: "agents", configPath: ".mcp.json", format: "mcpServers", skillKey: "agents", catalogId: "agents" },
|
|
30
|
+
];
|
|
31
|
+
/** How many folders above the workspace setup looks for a repository root. */
|
|
32
|
+
const REPO_DETECT_DEPTH = 4;
|
|
33
|
+
class SetupError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
exitCode;
|
|
36
|
+
constructor(code, message, exitCode) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.exitCode = exitCode;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function runSetupCommand(opts) {
|
|
43
|
+
try {
|
|
44
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
45
|
+
const client = validateClient(opts.client);
|
|
46
|
+
const { project, source } = resolveSetupProject(opts, cwd);
|
|
47
|
+
const pins = packagePins(opts.version);
|
|
48
|
+
const placement = resolvePlacement(project, client, opts, cwd);
|
|
49
|
+
const warnings = [...placement.warnings];
|
|
50
|
+
const envProject = (opts.env ?? process.env)[PROJECT_PATH_ENV_VAR]?.trim();
|
|
51
|
+
if (envProject && !samePath(resolve(cwd, envProject), project)) {
|
|
52
|
+
warnings.push(`${PROJECT_PATH_ENV_VAR} is set to ${envProject} in this shell; setup ignores it and configures ${project}.`);
|
|
53
|
+
}
|
|
54
|
+
const manifestPath = join(project, "Packages", "manifest.json");
|
|
55
|
+
const manifest = await readJsonObject(manifestPath, false);
|
|
56
|
+
const dependencies = manifest.dependencies;
|
|
57
|
+
if (!isRecord(dependencies)) {
|
|
58
|
+
throw new SetupError("invalid_manifest", `${manifestPath} must contain a JSON object at 'dependencies'.`, 1);
|
|
59
|
+
}
|
|
60
|
+
dependencies[BRIDGE_PACKAGE] = pins.bridge;
|
|
61
|
+
dependencies[VERIFY_PACKAGE] = pins.verify;
|
|
62
|
+
// Client config and the agent skill live where the AI client is opened —
|
|
63
|
+
// the workspace root, which equals the Unity root for a plain layout.
|
|
64
|
+
const clientConfig = await planClientConfig(client, project, pins.npm, placement);
|
|
65
|
+
const configPath = clientConfig.path;
|
|
66
|
+
const entry = clientConfig.entry;
|
|
67
|
+
const wrapperPath = placement.emitWrapper
|
|
68
|
+
? join(placement.workspace, ...wrapperRelativePath(placement.layout).split("/"))
|
|
69
|
+
: null;
|
|
70
|
+
const wrapperBody = placement.emitWrapper
|
|
71
|
+
? renderWrapperScript({
|
|
72
|
+
version: opts.version,
|
|
73
|
+
layout: placement.layout,
|
|
74
|
+
unitySubpath: placement.unitySubpath,
|
|
75
|
+
})
|
|
76
|
+
: null;
|
|
77
|
+
let skillBytes = null;
|
|
78
|
+
let skillPath = null;
|
|
79
|
+
if (!opts.skipSkill) {
|
|
80
|
+
skillPath = join(placement.workspace, clientSkillRelativePath(client.skillKey));
|
|
81
|
+
const source = opts.skillSourcePath ?? resolveBundledSkillPath();
|
|
82
|
+
if (!source) {
|
|
83
|
+
throw new SetupError("skill_not_bundled", "The packaged core skill could not be located. Reinstall unity-open-mcp and retry.", 1);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
skillBytes = await readFile(source);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw ioError(`Could not read bundled skill at ${source}`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!opts.dryRun) {
|
|
93
|
+
await writeJson(manifestPath, manifest);
|
|
94
|
+
await writeText(configPath, clientConfig.body);
|
|
95
|
+
if (skillBytes && skillPath) {
|
|
96
|
+
try {
|
|
97
|
+
await mkdir(dirname(skillPath), { recursive: true });
|
|
98
|
+
await writeFile(skillPath, skillBytes);
|
|
99
|
+
await copySkillReferences(opts.skillSourcePath ?? resolveBundledSkillPath(), skillPath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw ioError(`Could not write skill to ${skillPath}`, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (wrapperPath && wrapperBody !== null) {
|
|
106
|
+
try {
|
|
107
|
+
await mkdir(dirname(wrapperPath), { recursive: true });
|
|
108
|
+
await writeFile(wrapperPath, wrapperBody, { mode: 0o755 });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw ioError(`Could not write wrapper script to ${wrapperPath}`, error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const report = {
|
|
116
|
+
version: opts.version,
|
|
117
|
+
project,
|
|
118
|
+
projectSource: source,
|
|
119
|
+
client: client.id,
|
|
120
|
+
dryRun: opts.dryRun,
|
|
121
|
+
workspace: placement.workspace,
|
|
122
|
+
layout: placement.layout,
|
|
123
|
+
unitySubpath: placement.unitySubpath,
|
|
124
|
+
portable: placement.portable,
|
|
125
|
+
configStrategy: placement.strategy,
|
|
126
|
+
wrapper: {
|
|
127
|
+
path: wrapperPath,
|
|
128
|
+
written: !opts.dryRun && wrapperPath !== null,
|
|
129
|
+
},
|
|
130
|
+
manifest: {
|
|
131
|
+
path: manifestPath,
|
|
132
|
+
written: !opts.dryRun,
|
|
133
|
+
dependencies: {
|
|
134
|
+
[BRIDGE_PACKAGE]: pins.bridge,
|
|
135
|
+
[VERIFY_PACKAGE]: pins.verify,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
mcpConfig: {
|
|
139
|
+
path: configPath,
|
|
140
|
+
written: !opts.dryRun,
|
|
141
|
+
entry,
|
|
142
|
+
},
|
|
143
|
+
skill: {
|
|
144
|
+
skipped: opts.skipSkill,
|
|
145
|
+
path: skillPath,
|
|
146
|
+
written: !opts.dryRun && !opts.skipSkill,
|
|
147
|
+
bytes: skillBytes?.byteLength ?? 0,
|
|
148
|
+
lines: skillBytes ? countLines(skillBytes) : 0,
|
|
149
|
+
},
|
|
150
|
+
warnings,
|
|
151
|
+
userAction: [
|
|
152
|
+
`Open Unity with ${project} and wait for compilation to finish.`,
|
|
153
|
+
"Restart the MCP / AI client so it reloads the updated configuration.",
|
|
154
|
+
...(client.format === "codex"
|
|
155
|
+
? [`Trust ${placement.workspace} in Codex — it reads .codex/config.toml only for a trusted project.`]
|
|
156
|
+
: []),
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
return { exitCode: 0, json: report, human: formatSetupReport(report) };
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const setupError = error instanceof SetupError
|
|
163
|
+
? error
|
|
164
|
+
: new SetupError("setup_failed", error instanceof Error ? error.message : String(error), 1);
|
|
165
|
+
const report = {
|
|
166
|
+
version: opts.version,
|
|
167
|
+
project: opts.projectPath ?? null,
|
|
168
|
+
client: opts.client ?? null,
|
|
169
|
+
dryRun: opts.dryRun,
|
|
170
|
+
error: { code: setupError.code, message: setupError.message },
|
|
171
|
+
warnings: [],
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
exitCode: setupError.exitCode,
|
|
175
|
+
json: report,
|
|
176
|
+
human: `Setup failed: ${setupError.message}`,
|
|
177
|
+
errorLabel: setupError.code,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The Unity project this run configures. First match wins:
|
|
183
|
+
*
|
|
184
|
+
* 1. `--project <path>` — absolute, or relative to the working directory.
|
|
185
|
+
* 2. `--workspace <path>` (+ `--unity-subpath <rel>`) — the Unity folder
|
|
186
|
+
* under an explicit repository root.
|
|
187
|
+
* 3. `--unity-subpath <rel>` — a monorepo, run from the repository root.
|
|
188
|
+
* 4. the working directory itself.
|
|
189
|
+
*
|
|
190
|
+
* `UNITY_PROJECT_PATH` is deliberately not an input: an agent's shell can
|
|
191
|
+
* carry a stale value from another project, and setup writes files.
|
|
192
|
+
*/
|
|
193
|
+
function resolveSetupProject(opts, cwd) {
|
|
194
|
+
const subpath = normalizeSubpath(opts.unitySubpath);
|
|
195
|
+
const flag = opts.projectPath?.trim();
|
|
196
|
+
const workspace = opts.workspacePath?.trim();
|
|
197
|
+
let project;
|
|
198
|
+
let source;
|
|
199
|
+
if (flag) {
|
|
200
|
+
project = resolve(cwd, flag);
|
|
201
|
+
source = "flag";
|
|
202
|
+
}
|
|
203
|
+
else if (workspace) {
|
|
204
|
+
project = resolve(cwd, workspace, ...subpath.split("/").filter(Boolean));
|
|
205
|
+
source = "workspace";
|
|
206
|
+
}
|
|
207
|
+
else if (subpath) {
|
|
208
|
+
project = resolve(cwd, ...subpath.split("/"));
|
|
209
|
+
source = "cwd+subpath";
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
project = resolve(cwd);
|
|
213
|
+
source = "cwd";
|
|
214
|
+
}
|
|
215
|
+
const validation = validateUnityProjectRoot(project);
|
|
216
|
+
if (!validation.valid) {
|
|
217
|
+
const missing = validation.missing.map((m) => `${m}/`).join(", ");
|
|
218
|
+
throw new SetupError("not_unity_project", `${project} is not a Unity project root: missing ${missing}. ${notUnityProjectHint(project, source)}`, 2);
|
|
219
|
+
}
|
|
220
|
+
return { project, source };
|
|
221
|
+
}
|
|
222
|
+
/** Point at a Unity project just below the folder that missed, when there is one. */
|
|
223
|
+
function notUnityProjectHint(searched, source) {
|
|
224
|
+
const found = findUnityProjectsBelow(searched, 2);
|
|
225
|
+
if (found.length > 0) {
|
|
226
|
+
const shown = found.slice(0, 3);
|
|
227
|
+
if (source === "cwd") {
|
|
228
|
+
return `Found a Unity project at ${shown.map((rel) => `${rel}/`).join(", ")} — run again with --unity-subpath ${quoteArg(shown[0])}.`;
|
|
229
|
+
}
|
|
230
|
+
return `Found a Unity project at ${shown.map((rel) => join(searched, rel)).join(", ")} — pass that folder instead.`;
|
|
231
|
+
}
|
|
232
|
+
return source === "flag"
|
|
233
|
+
? "Pass the folder that contains Assets/, Packages/, and ProjectSettings/."
|
|
234
|
+
: "Run setup in the folder that contains Assets/, Packages/, and ProjectSettings/, or pass --project <path>.";
|
|
235
|
+
}
|
|
236
|
+
/** Folders that never hold a Unity project worth suggesting. */
|
|
237
|
+
const SEARCH_PRUNED = new Set([
|
|
238
|
+
"node_modules", "Library", "Temp", "Logs", "Build", "Builds", "obj", "bin", "dist",
|
|
239
|
+
]);
|
|
240
|
+
/** Unity roots up to `depth` levels below `root`, as POSIX relative paths. */
|
|
241
|
+
function findUnityProjectsBelow(root, depth) {
|
|
242
|
+
const found = [];
|
|
243
|
+
const walk = (dir, rel, level) => {
|
|
244
|
+
if (level > depth)
|
|
245
|
+
return;
|
|
246
|
+
let entries;
|
|
247
|
+
try {
|
|
248
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (const e of entries) {
|
|
254
|
+
if (!e.isDirectory() || e.name.startsWith(".") || SEARCH_PRUNED.has(e.name))
|
|
255
|
+
continue;
|
|
256
|
+
const child = join(dir, e.name);
|
|
257
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
258
|
+
if (validateUnityProjectRoot(child).valid)
|
|
259
|
+
found.push(childRel);
|
|
260
|
+
else
|
|
261
|
+
walk(child, childRel, level + 1);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
walk(root, "", 1);
|
|
265
|
+
return found.sort();
|
|
266
|
+
}
|
|
267
|
+
function validateClient(input) {
|
|
268
|
+
const known = SETUP_CLIENTS.map((c) => c.id);
|
|
269
|
+
if (!input) {
|
|
270
|
+
throw new SetupError("missing_client", `setup requires --client <id>. Known ids: ${known.join(", ")}.`, 2);
|
|
271
|
+
}
|
|
272
|
+
const spec = SETUP_CLIENTS.find((c) => c.id === input);
|
|
273
|
+
if (spec)
|
|
274
|
+
return spec;
|
|
275
|
+
if (knownClientKeys().includes(input)) {
|
|
276
|
+
throw new SetupError("unsupported_client_config", `Client '${input}' has a skill path but no setup config writer. Configure MCP manually; skill-only is not enough. Setup config writers: ${known.join(", ")}.`, 2);
|
|
277
|
+
}
|
|
278
|
+
throw new SetupError("unknown_client", `Unknown client '${input}'. Known ids: ${known.join(", ")}.`, 2);
|
|
279
|
+
}
|
|
280
|
+
function resolvePlacement(project, client, opts, cwd) {
|
|
281
|
+
const warnings = [];
|
|
282
|
+
const requestedSubpath = normalizeSubpath(opts.unitySubpath);
|
|
283
|
+
let workspace;
|
|
284
|
+
if (opts.workspacePath?.trim()) {
|
|
285
|
+
workspace = resolve(cwd, opts.workspacePath.trim());
|
|
286
|
+
}
|
|
287
|
+
else if (requestedSubpath) {
|
|
288
|
+
// Derive the workspace by stripping the subpath off the Unity root, so
|
|
289
|
+
// `--project /abs/repo/Client --unity-subpath Client` needs no --workspace.
|
|
290
|
+
workspace = resolve(project, ...requestedSubpath.split("/").map(() => ".."));
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
workspace = project;
|
|
294
|
+
}
|
|
295
|
+
const derived = relativeSubpath(workspace, project);
|
|
296
|
+
if (derived === undefined) {
|
|
297
|
+
throw new SetupError("project_outside_workspace", `--project ${project} is not inside --workspace ${workspace}.`, 2);
|
|
298
|
+
}
|
|
299
|
+
if (requestedSubpath && requestedSubpath !== derived) {
|
|
300
|
+
throw new SetupError("subpath_mismatch", `--unity-subpath '${requestedSubpath}' does not match the Unity root under the workspace ('${derived || "."}').`, 2);
|
|
301
|
+
}
|
|
302
|
+
const unitySubpath = derived;
|
|
303
|
+
const layout = opts.layout ?? (unitySubpath ? "monorepo" : "unity-root");
|
|
304
|
+
if (layout === "monorepo" && !unitySubpath) {
|
|
305
|
+
throw new SetupError("missing_unity_subpath", "--layout monorepo requires --unity-subpath <relative-path> (or --workspace <repo root>).", 2);
|
|
306
|
+
}
|
|
307
|
+
if (layout === "unity-root" && unitySubpath) {
|
|
308
|
+
throw new SetupError("layout_conflict", `--layout unity-root conflicts with a Unity project at '${unitySubpath}' below the workspace; use --layout monorepo.`, 2);
|
|
309
|
+
}
|
|
310
|
+
const support = portableSupportFor(client.catalogId);
|
|
311
|
+
const repoRoot = findRepoRoot(workspace, opts.homeDir ?? homedir());
|
|
312
|
+
// Portable by default wherever the config is likely to be committed: a
|
|
313
|
+
// monorepo, or a workspace that is itself a repository root — the same
|
|
314
|
+
// default the bridge window and the Hub wizard use.
|
|
315
|
+
let portable = opts.portable ?? (layout === "monorepo" || repoRoot === workspace);
|
|
316
|
+
if (portable && support?.strategy === "absolute") {
|
|
317
|
+
warnings.push(`Client '${client.id}' has no portable configuration form; writing the absolute path.`);
|
|
318
|
+
portable = false;
|
|
319
|
+
}
|
|
320
|
+
if (portable && !support) {
|
|
321
|
+
warnings.push(`Client '${client.id}' is not in the portable-config catalog; writing the absolute path.`);
|
|
322
|
+
portable = false;
|
|
323
|
+
}
|
|
324
|
+
if (portable && support?.strategy === "wrapper" && (opts.platform ?? process.platform) === "win32") {
|
|
325
|
+
if (opts.portable === undefined) {
|
|
326
|
+
warnings.push(`The portable ${client.id} entry runs a bash wrapper script, which Windows cannot start without Git Bash or WSL; writing the absolute path. Pass --portable to write the wrapper anyway.`);
|
|
327
|
+
portable = false;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
warnings.push(`The ${client.id} wrapper is a bash script: on Windows it needs Git Bash or WSL on PATH.`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// The Unity project sits inside a repository whose root is above the
|
|
334
|
+
// workspace: an AI client opened on that root never reads this config.
|
|
335
|
+
if (repoRoot && repoRoot !== workspace && !opts.workspacePath?.trim()) {
|
|
336
|
+
const rel = relativeSubpath(repoRoot, project);
|
|
337
|
+
if (rel) {
|
|
338
|
+
warnings.push(`${project} is inside the repository ${repoRoot}. An AI client opened on ${repoRoot} does not read ${join(workspace, ...client.configPath.split("/"))}. ` +
|
|
339
|
+
`To configure the repository instead, run from ${repoRoot}: npx -y unity-open-mcp@${opts.version} setup --client ${client.id} --unity-subpath ${quoteArg(rel)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const strategy = portable && support ? support.strategy : "absolute";
|
|
343
|
+
const emitWrapper = (portable && strategy === "wrapper") || opts.wrapper === true;
|
|
344
|
+
return {
|
|
345
|
+
workspace,
|
|
346
|
+
layout,
|
|
347
|
+
unitySubpath,
|
|
348
|
+
portable,
|
|
349
|
+
strategy,
|
|
350
|
+
support,
|
|
351
|
+
emitWrapper,
|
|
352
|
+
warnings,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** `Client`, `Client/`, `.\Client` → `Client`; empty/`.` → "". */
|
|
356
|
+
function normalizeSubpath(input) {
|
|
357
|
+
if (!input)
|
|
358
|
+
return "";
|
|
359
|
+
const posix = input.split("\\").join("/");
|
|
360
|
+
const segments = posix
|
|
361
|
+
.split("/")
|
|
362
|
+
.map((segment) => segment.trim())
|
|
363
|
+
.filter((segment) => segment.length > 0 && segment !== ".");
|
|
364
|
+
if (segments.some((segment) => segment === "..")) {
|
|
365
|
+
throw new SetupError("invalid_unity_subpath", `--unity-subpath must stay inside the workspace (received '${input}').`, 2);
|
|
366
|
+
}
|
|
367
|
+
return segments.join("/");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* POSIX-separated path from `workspace` down to `project`, "" when they are the
|
|
371
|
+
* same folder, `undefined` when `project` is outside `workspace`.
|
|
372
|
+
*/
|
|
373
|
+
function relativeSubpath(workspace, project) {
|
|
374
|
+
const rel = relative(workspace, project);
|
|
375
|
+
if (rel === "")
|
|
376
|
+
return "";
|
|
377
|
+
if (rel.startsWith("..") || isAbsolute(rel))
|
|
378
|
+
return undefined;
|
|
379
|
+
return rel.split(sep).join("/");
|
|
380
|
+
}
|
|
381
|
+
export function packagePins(version) {
|
|
382
|
+
return {
|
|
383
|
+
npm: `unity-open-mcp@${version}`,
|
|
384
|
+
bridge: `${REPOSITORY_URL}?path=packages/bridge#bridge-v${version}`,
|
|
385
|
+
verify: `${REPOSITORY_URL}?path=packages/verify#verify-v${version}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Nearest folder at or above `start` (up to {@link REPO_DETECT_DEPTH} levels)
|
|
390
|
+
* that holds a `.git` entry, stopping before the home directory — a dotfiles
|
|
391
|
+
* repository in `$HOME` must not make every project look committed.
|
|
392
|
+
*/
|
|
393
|
+
function findRepoRoot(start, home) {
|
|
394
|
+
const boundary = home ? resolve(home) : "";
|
|
395
|
+
let dir = resolve(start);
|
|
396
|
+
for (let level = 0; level <= REPO_DETECT_DEPTH; level++) {
|
|
397
|
+
if (boundary && samePath(dir, boundary))
|
|
398
|
+
return undefined;
|
|
399
|
+
if (existsSync(join(dir, ".git")))
|
|
400
|
+
return dir;
|
|
401
|
+
const parent = dirname(dir);
|
|
402
|
+
if (parent === dir)
|
|
403
|
+
return undefined;
|
|
404
|
+
dir = parent;
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
async function planClientConfig(client, project, npmPin, placement) {
|
|
409
|
+
const path = join(placement.workspace, ...client.configPath.split("/"));
|
|
410
|
+
const launch = launchFields(project, npmPin, placement);
|
|
411
|
+
if (client.format === "codex") {
|
|
412
|
+
const codexEntry = {
|
|
413
|
+
command: launch.command,
|
|
414
|
+
args: launch.args,
|
|
415
|
+
env: launch.env,
|
|
416
|
+
removeEnv: launch.env[PROJECT_PATH_ENV_VAR] === undefined ? [PROJECT_PATH_ENV_VAR] : [],
|
|
417
|
+
};
|
|
418
|
+
const raw = (await readTextIfExists(path)) ?? "";
|
|
419
|
+
let body;
|
|
420
|
+
try {
|
|
421
|
+
body = mergeCodexServer(raw, SERVER_KEY, codexEntry);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
if (!(error instanceof CodexTomlError))
|
|
425
|
+
throw error;
|
|
426
|
+
throw new SetupError(error.code, `${path}: ${error.message} Add this entry by hand:\n${renderCodexServer(SERVER_KEY, codexEntry)}`, 1);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
path,
|
|
430
|
+
entry: { enabled: true, command: launch.command, args: launch.args, env: launch.env },
|
|
431
|
+
body,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const raw = await readTextIfExists(path);
|
|
435
|
+
let config = {};
|
|
436
|
+
if (raw !== undefined) {
|
|
437
|
+
try {
|
|
438
|
+
config = parseJsonObject(raw);
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (isJsonc(raw)) {
|
|
442
|
+
const snippet = {};
|
|
443
|
+
mergeClientConfig(snippet, client, launch);
|
|
444
|
+
throw new SetupError("jsonc_config", `${path} has comments or trailing commas. setup rewrites the file as plain JSON and would drop them, so it leaves the file unchanged. Add this entry by hand:\n${JSON.stringify(snippet, null, 2)}`, 1);
|
|
445
|
+
}
|
|
446
|
+
throw new SetupError("invalid_json", `${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const entry = mergeClientConfig(config, client, launch);
|
|
450
|
+
return { path, entry, body: `${JSON.stringify(config, null, 2)}\n` };
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Merge the `unity-open-mcp` entry into an existing client config, preserving
|
|
454
|
+
* sibling servers and any extra keys the user added.
|
|
455
|
+
*
|
|
456
|
+
* A portable re-run REPLACES the absolute `UNITY_PROJECT_PATH` left by an
|
|
457
|
+
* earlier absolute run (and vice versa), because leaving a stale machine path
|
|
458
|
+
* behind would silently keep winning over `--project-from-cwd`.
|
|
459
|
+
*/
|
|
460
|
+
function mergeClientConfig(config, client, launch) {
|
|
461
|
+
if (client.format === "opencode") {
|
|
462
|
+
const mcp = ensureObject(config, "mcp");
|
|
463
|
+
const previous = isRecord(mcp[SERVER_KEY]) ? mcp[SERVER_KEY] : {};
|
|
464
|
+
const previousEnvironment = isRecord(previous.environment)
|
|
465
|
+
? previous.environment
|
|
466
|
+
: {};
|
|
467
|
+
const entry = {
|
|
468
|
+
...previous,
|
|
469
|
+
type: "local",
|
|
470
|
+
command: [launch.command, ...launch.args],
|
|
471
|
+
enabled: true,
|
|
472
|
+
environment: mergeProjectEnv(previousEnvironment, launch.env),
|
|
473
|
+
};
|
|
474
|
+
mcp[SERVER_KEY] = entry;
|
|
475
|
+
return entry;
|
|
476
|
+
}
|
|
477
|
+
// VS Code (and Visual Studio) read `servers`, ZCode `mcp.servers`; both
|
|
478
|
+
// carry an explicit transport.
|
|
479
|
+
const typed = client.format === "vscode" || client.format === "zcode";
|
|
480
|
+
const servers = client.format === "zcode"
|
|
481
|
+
? ensureObject(ensureObject(config, "mcp"), "servers")
|
|
482
|
+
: ensureObject(config, client.format === "vscode" ? "servers" : "mcpServers");
|
|
483
|
+
const previous = isRecord(servers[SERVER_KEY]) ? servers[SERVER_KEY] : {};
|
|
484
|
+
const previousEnv = isRecord(previous.env) ? previous.env : {};
|
|
485
|
+
const entry = {
|
|
486
|
+
...(typed ? { type: "stdio" } : {}),
|
|
487
|
+
...previous,
|
|
488
|
+
command: launch.command,
|
|
489
|
+
args: launch.args,
|
|
490
|
+
env: mergeProjectEnv(previousEnv, launch.env),
|
|
491
|
+
};
|
|
492
|
+
servers[SERVER_KEY] = entry;
|
|
493
|
+
return entry;
|
|
494
|
+
}
|
|
495
|
+
function launchFields(project, npmPin, placement) {
|
|
496
|
+
if (!placement.portable || !placement.support) {
|
|
497
|
+
return {
|
|
498
|
+
command: "npx",
|
|
499
|
+
args: ["-y", npmPin],
|
|
500
|
+
env: { [PROJECT_PATH_ENV_VAR]: project },
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const target = {
|
|
504
|
+
layout: placement.layout,
|
|
505
|
+
unitySubpath: placement.unitySubpath,
|
|
506
|
+
npmPin,
|
|
507
|
+
};
|
|
508
|
+
if (placement.strategy === "wrapper") {
|
|
509
|
+
return {
|
|
510
|
+
command: "bash",
|
|
511
|
+
args: [wrapperRelativePath(placement.layout)],
|
|
512
|
+
env: {},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
command: "npx",
|
|
517
|
+
args: portableServerArgs(placement.strategy, target),
|
|
518
|
+
env: portableEnv(placement.support, target),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Keep every env key the user added, but let this run own
|
|
523
|
+
* `UNITY_PROJECT_PATH`: set it for absolute/interpolated strategies, drop it
|
|
524
|
+
* for the strategies that resolve the path at runtime.
|
|
525
|
+
*/
|
|
526
|
+
function mergeProjectEnv(previous, next) {
|
|
527
|
+
const merged = { ...previous, ...next };
|
|
528
|
+
if (next[PROJECT_PATH_ENV_VAR] === undefined)
|
|
529
|
+
delete merged[PROJECT_PATH_ENV_VAR];
|
|
530
|
+
return merged;
|
|
531
|
+
}
|
|
532
|
+
function ensureObject(parent, key) {
|
|
533
|
+
if (parent[key] === undefined)
|
|
534
|
+
parent[key] = {};
|
|
535
|
+
if (!isRecord(parent[key])) {
|
|
536
|
+
throw new SetupError("invalid_client_config", `Client config key '${key}' must be a JSON object.`, 1);
|
|
537
|
+
}
|
|
538
|
+
return parent[key];
|
|
539
|
+
}
|
|
540
|
+
async function readJsonObject(path, allowMissing) {
|
|
541
|
+
let raw;
|
|
542
|
+
try {
|
|
543
|
+
raw = await readFile(path, "utf8");
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (allowMissing && isNodeError(error) && error.code === "ENOENT")
|
|
547
|
+
return {};
|
|
548
|
+
throw ioError(`Could not read ${path}`, error);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const parsed = JSON.parse(raw);
|
|
552
|
+
if (!isRecord(parsed)) {
|
|
553
|
+
throw new Error("top level must be a JSON object");
|
|
554
|
+
}
|
|
555
|
+
return parsed;
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
throw new SetupError("invalid_json", `${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function writeJson(path, value) {
|
|
562
|
+
try {
|
|
563
|
+
await mkdir(dirname(path), { recursive: true });
|
|
564
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
throw ioError(`Could not write ${path}`, error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function writeText(path, body) {
|
|
571
|
+
try {
|
|
572
|
+
await mkdir(dirname(path), { recursive: true });
|
|
573
|
+
await writeFile(path, body, "utf8");
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
throw ioError(`Could not write ${path}`, error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async function readTextIfExists(path) {
|
|
580
|
+
try {
|
|
581
|
+
return await readFile(path, "utf8");
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
585
|
+
return undefined;
|
|
586
|
+
throw ioError(`Could not read ${path}`, error);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function parseJsonObject(raw) {
|
|
590
|
+
const parsed = JSON.parse(raw);
|
|
591
|
+
if (!isRecord(parsed))
|
|
592
|
+
throw new Error("top level must be a JSON object");
|
|
593
|
+
return parsed;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* True when `raw` only parses once comments and trailing commas are removed —
|
|
597
|
+
* the JSONC dialect VS Code allows in `.vscode/mcp.json`.
|
|
598
|
+
*/
|
|
599
|
+
function isJsonc(raw) {
|
|
600
|
+
let out = "";
|
|
601
|
+
let inString = false;
|
|
602
|
+
for (let i = 0; i < raw.length; i++) {
|
|
603
|
+
const ch = raw[i];
|
|
604
|
+
if (inString) {
|
|
605
|
+
out += ch;
|
|
606
|
+
if (ch === "\\")
|
|
607
|
+
out += raw[++i] ?? "";
|
|
608
|
+
else if (ch === '"')
|
|
609
|
+
inString = false;
|
|
610
|
+
}
|
|
611
|
+
else if (ch === '"') {
|
|
612
|
+
inString = true;
|
|
613
|
+
out += ch;
|
|
614
|
+
}
|
|
615
|
+
else if (raw.startsWith("//", i)) {
|
|
616
|
+
while (i < raw.length && raw[i] !== "\n")
|
|
617
|
+
i++;
|
|
618
|
+
out += "\n";
|
|
619
|
+
}
|
|
620
|
+
else if (raw.startsWith("/*", i)) {
|
|
621
|
+
const close = raw.indexOf("*/", i + 2);
|
|
622
|
+
i = close < 0 ? raw.length : close + 1;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
out += ch;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
parseJsonObject(out.replace(/,(\s*[}\]])/g, "$1"));
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/** Path equality after resolution; case folds on Windows and macOS. */
|
|
637
|
+
function samePath(a, b) {
|
|
638
|
+
const norm = (p) => {
|
|
639
|
+
const r = resolve(p);
|
|
640
|
+
return process.platform === "win32" || process.platform === "darwin" ? r.toLowerCase() : r;
|
|
641
|
+
};
|
|
642
|
+
return norm(a) === norm(b);
|
|
643
|
+
}
|
|
644
|
+
/** Shell-quote an argument that contains whitespace. */
|
|
645
|
+
function quoteArg(value) {
|
|
646
|
+
return /\s/.test(value) ? `"${value}"` : value;
|
|
647
|
+
}
|
|
648
|
+
function resolveBundledSkillPath() {
|
|
649
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
650
|
+
// Published package: dist/cli/setup-command.js -> dist/skill/SKILL.md.
|
|
651
|
+
const packaged = join(moduleDir, "..", "skill", "SKILL.md");
|
|
652
|
+
// Source-checkout fallback supports direct development invocations.
|
|
653
|
+
return resolveTemplateSkillPath() ?? packaged;
|
|
654
|
+
}
|
|
655
|
+
function formatSetupReport(report) {
|
|
656
|
+
const mode = report.dryRun ? "DRY RUN — no files changed" : "Setup complete";
|
|
657
|
+
const skill = report.skill.skipped
|
|
658
|
+
? "skipped (--skip-skill)"
|
|
659
|
+
: `${report.skill.path} (${report.skill.bytes} bytes, ${report.skill.lines} lines)`;
|
|
660
|
+
const lines = [
|
|
661
|
+
mode,
|
|
662
|
+
`VERSION: ${report.version}`,
|
|
663
|
+
`Project: ${report.project} (from ${report.projectSource})`,
|
|
664
|
+
`Client: ${report.client}`,
|
|
665
|
+
`UPM bridge: ${report.manifest.dependencies[BRIDGE_PACKAGE]}`,
|
|
666
|
+
`UPM verify: ${report.manifest.dependencies[VERIFY_PACKAGE]}`,
|
|
667
|
+
`MCP config: ${report.mcpConfig.path}`,
|
|
668
|
+
`Skill: ${skill}`,
|
|
669
|
+
"Domain packages: not installed",
|
|
670
|
+
];
|
|
671
|
+
if (report.layout === "monorepo" || report.portable) {
|
|
672
|
+
lines.push(`Workspace: ${report.workspace}`, `Layout: ${report.layout}${report.unitySubpath ? ` (Unity at ${report.unitySubpath}/)` : ""}`);
|
|
673
|
+
}
|
|
674
|
+
lines.push(`Config: ${report.portable ? `portable — safe to commit (${report.configStrategy})` : "absolute path — this machine only"}`);
|
|
675
|
+
if (report.wrapper.path)
|
|
676
|
+
lines.push(`Wrapper: ${report.wrapper.path}`);
|
|
677
|
+
for (const warning of report.warnings)
|
|
678
|
+
lines.push(`Warning: ${warning}`);
|
|
679
|
+
lines.push("", "USER ACTION:", ...report.userAction.map((action, i) => `${i + 1}. ${action}`));
|
|
680
|
+
return lines.join("\n");
|
|
681
|
+
}
|
|
682
|
+
function countLines(bytes) {
|
|
683
|
+
if (bytes.byteLength === 0)
|
|
684
|
+
return 0;
|
|
685
|
+
const text = bytes.toString("utf8");
|
|
686
|
+
const newlines = (text.match(/\n/g) ?? []).length;
|
|
687
|
+
return text.endsWith("\n") ? newlines : newlines + 1;
|
|
688
|
+
}
|
|
689
|
+
function isNodeError(error) {
|
|
690
|
+
return error instanceof Error && "code" in error;
|
|
691
|
+
}
|
|
692
|
+
function ioError(context, error) {
|
|
693
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
694
|
+
return new SetupError("io_error", `${context}: ${detail}`, 1);
|
|
695
|
+
}
|
|
696
|
+
//# sourceMappingURL=setup-command.js.map
|