@orchid-labs/pluxx 0.1.1 → 0.1.3
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 +25 -8
- package/bin/pluxx.js +19 -28
- package/dist/agents.d.ts +16 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/cli/agent.d.ts +62 -0
- package/dist/cli/agent.d.ts.map +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.d.ts.map +1 -0
- package/dist/cli/index.d.ts +7 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21810 -0
- package/dist/cli/init-from-mcp.d.ts +17 -1
- package/dist/cli/init-from-mcp.d.ts.map +1 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/lint.d.ts +3 -1
- package/dist/cli/lint.d.ts.map +1 -1
- package/dist/cli/mcp-proxy.d.ts.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/primitive-summary.d.ts +14 -0
- package/dist/cli/primitive-summary.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +1 -1
- package/dist/cli/publish.d.ts +6 -1
- package/dist/cli/publish.d.ts.map +1 -1
- package/dist/cli/sync-from-mcp.d.ts.map +1 -1
- package/dist/cli/verify-install.d.ts +25 -0
- package/dist/cli/verify-install.d.ts.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/compiler-intent.d.ts +165 -0
- package/dist/compiler-intent.d.ts.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/delegation.d.ts +11 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts.map +1 -1
- package/dist/generators/base.d.ts +5 -0
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/claude-code/index.d.ts.map +1 -1
- package/dist/generators/cline/index.d.ts.map +1 -1
- package/dist/generators/codex/index.d.ts +4 -0
- package/dist/generators/codex/index.d.ts.map +1 -1
- package/dist/generators/cursor/index.d.ts +1 -0
- package/dist/generators/cursor/index.d.ts.map +1 -1
- package/dist/generators/gemini-cli/index.d.ts.map +1 -1
- package/dist/generators/github-copilot/index.d.ts.map +1 -1
- package/dist/generators/opencode/index.d.ts +1 -0
- package/dist/generators/opencode/index.d.ts.map +1 -1
- package/dist/generators/openhands/index.d.ts.map +1 -1
- package/dist/generators/roo-code/index.d.ts.map +1 -1
- package/dist/generators/shared/claude-family.d.ts.map +1 -1
- package/dist/generators/warp/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5371 -553
- package/dist/schema.d.ts +91 -42
- package/dist/schema.d.ts.map +1 -1
- package/dist/text-files.d.ts +5 -0
- package/dist/text-files.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +15 -1
- package/dist/validation/platform-rules.d.ts.map +1 -1
- package/package.json +15 -13
- package/src/cli/agent.ts +0 -1455
- package/src/cli/dev.ts +0 -112
- package/src/cli/doctor.ts +0 -987
- package/src/cli/eval.ts +0 -470
- package/src/cli/index.ts +0 -2933
- package/src/cli/init-from-mcp.ts +0 -2115
- package/src/cli/install.ts +0 -860
- package/src/cli/lint.ts +0 -1249
- package/src/cli/mcp-proxy.ts +0 -322
- package/src/cli/migrate.ts +0 -867
- package/src/cli/prompt.ts +0 -82
- package/src/cli/publish.ts +0 -401
- package/src/cli/runtime.ts +0 -86
- package/src/cli/sync-from-mcp.ts +0 -586
- package/src/cli/test.ts +0 -142
- package/src/compatibility/matrix.ts +0 -149
- package/src/config/define.ts +0 -20
- package/src/config/load.ts +0 -74
- package/src/generators/amp/index.ts +0 -63
- package/src/generators/base.ts +0 -188
- package/src/generators/claude-code/index.ts +0 -172
- package/src/generators/cline/index.ts +0 -35
- package/src/generators/codex/index.ts +0 -143
- package/src/generators/cursor/index.ts +0 -158
- package/src/generators/gemini-cli/index.ts +0 -83
- package/src/generators/github-copilot/index.ts +0 -32
- package/src/generators/hooks-warning.ts +0 -51
- package/src/generators/index.ts +0 -71
- package/src/generators/opencode/index.ts +0 -526
- package/src/generators/openhands/index.ts +0 -32
- package/src/generators/roo-code/index.ts +0 -35
- package/src/generators/shared/claude-family.ts +0 -215
- package/src/generators/warp/index.ts +0 -32
- package/src/hook-events.ts +0 -33
- package/src/index.ts +0 -34
- package/src/mcp/introspect.ts +0 -1107
- package/src/permissions.ts +0 -260
- package/src/schema.ts +0 -312
- package/src/user-config.ts +0 -177
- package/src/validation/platform-rules.ts +0 -686
package/src/cli/doctor.ts
DELETED
|
@@ -1,987 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants, existsSync, lstatSync, readFileSync } from 'fs'
|
|
2
|
-
import { resolve } from 'path'
|
|
3
|
-
import { CONFIG_FILES, loadConfig } from '../config/load'
|
|
4
|
-
import { listHookCommands } from './install'
|
|
5
|
-
import { PLATFORM_LIMITS } from '../validation/platform-rules'
|
|
6
|
-
import type { McpServer, PluginConfig, TargetPlatform } from '../schema'
|
|
7
|
-
import { MCP_SCAFFOLD_METADATA_PATH, type McpScaffoldMetadata } from './init-from-mcp'
|
|
8
|
-
|
|
9
|
-
export type DoctorLevel = 'error' | 'warning' | 'info' | 'success'
|
|
10
|
-
|
|
11
|
-
export interface DoctorCheck {
|
|
12
|
-
level: DoctorLevel
|
|
13
|
-
code: string
|
|
14
|
-
title: string
|
|
15
|
-
detail: string
|
|
16
|
-
fix: string
|
|
17
|
-
path?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DoctorReport {
|
|
21
|
-
ok: boolean
|
|
22
|
-
errors: number
|
|
23
|
-
warnings: number
|
|
24
|
-
infos: number
|
|
25
|
-
checks: DoctorCheck[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const CORE_FOUR = new Set<TargetPlatform>(['claude-code', 'cursor', 'codex', 'opencode'])
|
|
29
|
-
const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
30
|
-
const GENERIC_TOOL_NAME_PATTERNS = [
|
|
31
|
-
/^tool[_-]?\d+$/i,
|
|
32
|
-
/^function[_-]?\d+$/i,
|
|
33
|
-
/^action[_-]?\d+$/i,
|
|
34
|
-
/^untitled/i,
|
|
35
|
-
/^mcp[-_]?tool/i,
|
|
36
|
-
]
|
|
37
|
-
const LOW_INFO_DESCRIPTION_PATTERNS = [
|
|
38
|
-
/^n\/?a$/i,
|
|
39
|
-
/^none$/i,
|
|
40
|
-
/^todo$/i,
|
|
41
|
-
/^tbd$/i,
|
|
42
|
-
/^description$/i,
|
|
43
|
-
/^no description provided\.?$/i,
|
|
44
|
-
]
|
|
45
|
-
const MATERIALIZED_ENV_MARKER = 'materialized required config'
|
|
46
|
-
|
|
47
|
-
type ConsumerPlatform = 'claude-code' | 'cursor' | 'codex' | 'opencode'
|
|
48
|
-
|
|
49
|
-
interface ConsumerBundleLayout {
|
|
50
|
-
kind: 'installed-platform'
|
|
51
|
-
platform: ConsumerPlatform
|
|
52
|
-
manifestPath: string
|
|
53
|
-
mcpConfigPath?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type ConsumerLayoutDetection =
|
|
57
|
-
| ConsumerBundleLayout
|
|
58
|
-
| { kind: 'source-project' }
|
|
59
|
-
| { kind: 'multi-target-dist' }
|
|
60
|
-
| { kind: 'unknown' }
|
|
61
|
-
|
|
62
|
-
function addCheck(checks: DoctorCheck[], check: DoctorCheck): void {
|
|
63
|
-
checks.push(check)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function summarizeChecks(checks: DoctorCheck[]): DoctorReport {
|
|
67
|
-
const errors = checks.filter((check) => check.level === 'error').length
|
|
68
|
-
const warnings = checks.filter((check) => check.level === 'warning').length
|
|
69
|
-
const infos = checks.filter((check) => check.level === 'info').length
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
ok: errors === 0,
|
|
73
|
-
errors,
|
|
74
|
-
warnings,
|
|
75
|
-
infos,
|
|
76
|
-
checks,
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function parseMajorVersion(version: string | undefined): number | null {
|
|
81
|
-
if (!version) return null
|
|
82
|
-
const match = version.match(/^(\d+)/)
|
|
83
|
-
return match ? Number(match[1]) : null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function checkReadablePath(
|
|
87
|
-
checks: DoctorCheck[],
|
|
88
|
-
rootDir: string,
|
|
89
|
-
label: string,
|
|
90
|
-
configuredPath: string | undefined,
|
|
91
|
-
required: boolean,
|
|
92
|
-
): void {
|
|
93
|
-
if (!configuredPath) {
|
|
94
|
-
if (required) {
|
|
95
|
-
addCheck(checks, {
|
|
96
|
-
level: 'error',
|
|
97
|
-
code: 'path-missing',
|
|
98
|
-
title: `${label} path missing`,
|
|
99
|
-
detail: `The config does not define a ${label.toLowerCase()} path.`,
|
|
100
|
-
fix: `Add a valid ${label.toLowerCase()} path to pluxx.config.ts.`,
|
|
101
|
-
path: 'pluxx.config.ts',
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const resolvedPath = resolve(rootDir, configuredPath)
|
|
108
|
-
if (!existsSync(resolvedPath)) {
|
|
109
|
-
addCheck(checks, {
|
|
110
|
-
level: 'error',
|
|
111
|
-
code: 'path-not-found',
|
|
112
|
-
title: `${label} path not found`,
|
|
113
|
-
detail: `Configured ${label.toLowerCase()} path does not exist: ${configuredPath}`,
|
|
114
|
-
fix: `Create ${configuredPath} or update the path in pluxx.config.ts.`,
|
|
115
|
-
path: configuredPath,
|
|
116
|
-
})
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
accessSync(resolvedPath, constants.R_OK)
|
|
122
|
-
addCheck(checks, {
|
|
123
|
-
level: 'success',
|
|
124
|
-
code: 'path-readable',
|
|
125
|
-
title: `${label} path readable`,
|
|
126
|
-
detail: `${configuredPath} is present and readable.`,
|
|
127
|
-
fix: 'No action needed.',
|
|
128
|
-
path: configuredPath,
|
|
129
|
-
})
|
|
130
|
-
} catch {
|
|
131
|
-
addCheck(checks, {
|
|
132
|
-
level: 'error',
|
|
133
|
-
code: 'path-unreadable',
|
|
134
|
-
title: `${label} path unreadable`,
|
|
135
|
-
detail: `Configured ${label.toLowerCase()} path is not readable: ${configuredPath}`,
|
|
136
|
-
fix: `Adjust permissions on ${configuredPath}.`,
|
|
137
|
-
path: configuredPath,
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function checkTargetPlatforms(checks: DoctorCheck[], config: PluginConfig): void {
|
|
143
|
-
const betaTargets = config.targets.filter((target) => !CORE_FOUR.has(target))
|
|
144
|
-
|
|
145
|
-
if (betaTargets.length === 0) {
|
|
146
|
-
addCheck(checks, {
|
|
147
|
-
level: 'success',
|
|
148
|
-
code: 'targets-core-four',
|
|
149
|
-
title: 'Core-four target set',
|
|
150
|
-
detail: `Configured targets stay within the primary launch path: ${config.targets.join(', ')}`,
|
|
151
|
-
fix: 'No action needed.',
|
|
152
|
-
path: 'pluxx.config.ts',
|
|
153
|
-
})
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
addCheck(checks, {
|
|
158
|
-
level: 'warning',
|
|
159
|
-
code: 'targets-beta',
|
|
160
|
-
title: 'Beta targets configured',
|
|
161
|
-
detail: `These targets are generated but less validated: ${betaTargets.join(', ')}`,
|
|
162
|
-
fix: 'Prefer claude-code, cursor, codex, and opencode for prime-time support.',
|
|
163
|
-
path: 'pluxx.config.ts',
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function checkMcpServer(checks: DoctorCheck[], serverName: string, server: McpServer): void {
|
|
168
|
-
const basePath = 'pluxx.config.ts'
|
|
169
|
-
|
|
170
|
-
if (server.transport === 'stdio') {
|
|
171
|
-
if (!server.command.trim()) {
|
|
172
|
-
addCheck(checks, {
|
|
173
|
-
level: 'error',
|
|
174
|
-
code: 'mcp-stdio-command-empty',
|
|
175
|
-
title: `MCP stdio command missing for ${serverName}`,
|
|
176
|
-
detail: `The stdio MCP server "${serverName}" does not define a command.`,
|
|
177
|
-
fix: `Set mcp.${serverName}.command to the executable used to start the server.`,
|
|
178
|
-
path: basePath,
|
|
179
|
-
})
|
|
180
|
-
} else {
|
|
181
|
-
addCheck(checks, {
|
|
182
|
-
level: 'success',
|
|
183
|
-
code: 'mcp-stdio-command',
|
|
184
|
-
title: `MCP stdio command configured for ${serverName}`,
|
|
185
|
-
detail: `The stdio MCP server "${serverName}" starts with "${server.command}".`,
|
|
186
|
-
fix: 'No action needed.',
|
|
187
|
-
path: basePath,
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const key of Object.keys(server.env ?? {})) {
|
|
192
|
-
if (!ENV_VAR_NAME.test(key)) {
|
|
193
|
-
addCheck(checks, {
|
|
194
|
-
level: 'warning',
|
|
195
|
-
code: 'mcp-env-key-invalid',
|
|
196
|
-
title: `Invalid env var name for ${serverName}`,
|
|
197
|
-
detail: `The env key "${key}" is not a shell-safe environment variable name.`,
|
|
198
|
-
fix: `Rename ${key} to a shell-safe env var such as ${key.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}.`,
|
|
199
|
-
path: basePath,
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
} else {
|
|
204
|
-
addCheck(checks, {
|
|
205
|
-
level: 'success',
|
|
206
|
-
code: 'mcp-remote-url',
|
|
207
|
-
title: `Remote MCP URL configured for ${serverName}`,
|
|
208
|
-
detail: `The ${server.transport.toUpperCase()} MCP server "${serverName}" points to ${server.url}.`,
|
|
209
|
-
fix: 'No action needed.',
|
|
210
|
-
path: basePath,
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (server.auth?.type && server.auth.type !== 'none') {
|
|
215
|
-
if (server.auth.type === 'platform') {
|
|
216
|
-
addCheck(checks, {
|
|
217
|
-
level: 'info',
|
|
218
|
-
code: 'mcp-auth-platform',
|
|
219
|
-
title: `Platform-managed auth declared for ${serverName}`,
|
|
220
|
-
detail: `${serverName} expects native platform auth at runtime (${server.auth.mode}).`,
|
|
221
|
-
fix: 'Complete the platform auth flow in Claude Code or Cursor before calling authenticated tools.',
|
|
222
|
-
path: basePath,
|
|
223
|
-
})
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!ENV_VAR_NAME.test(server.auth.envVar)) {
|
|
228
|
-
addCheck(checks, {
|
|
229
|
-
level: 'error',
|
|
230
|
-
code: 'mcp-auth-env-invalid',
|
|
231
|
-
title: `Invalid auth env var for ${serverName}`,
|
|
232
|
-
detail: `Auth env var "${server.auth.envVar}" is not a valid shell environment variable name.`,
|
|
233
|
-
fix: `Rename the env var for ${serverName} to a shell-safe name like ${server.auth.envVar.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}.`,
|
|
234
|
-
path: basePath,
|
|
235
|
-
})
|
|
236
|
-
} else {
|
|
237
|
-
addCheck(checks, {
|
|
238
|
-
level: 'info',
|
|
239
|
-
code: 'mcp-auth-env',
|
|
240
|
-
title: `Auth env var declared for ${serverName}`,
|
|
241
|
-
detail: `${serverName} expects ${server.auth.envVar} to be provided at runtime.`,
|
|
242
|
-
fix: `Export ${server.auth.envVar} before linting, building, or installing the generated plugin.`,
|
|
243
|
-
path: basePath,
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function checkMcpConfig(checks: DoctorCheck[], config: PluginConfig): void {
|
|
250
|
-
const servers = Object.entries(config.mcp ?? {})
|
|
251
|
-
if (servers.length === 0) {
|
|
252
|
-
addCheck(checks, {
|
|
253
|
-
level: 'info',
|
|
254
|
-
code: 'mcp-none-configured',
|
|
255
|
-
title: 'No MCP servers configured',
|
|
256
|
-
detail: 'This plugin does not currently define any MCP servers.',
|
|
257
|
-
fix: 'No action needed unless this plugin should wrap an MCP server.',
|
|
258
|
-
path: 'pluxx.config.ts',
|
|
259
|
-
})
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
for (const [serverName, server] of servers) {
|
|
264
|
-
checkMcpServer(checks, serverName, server)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function checkUserConfig(checks: DoctorCheck[], config: PluginConfig): void {
|
|
269
|
-
const entries = config.userConfig ?? []
|
|
270
|
-
if (entries.length === 0) {
|
|
271
|
-
addCheck(checks, {
|
|
272
|
-
level: 'info',
|
|
273
|
-
code: 'user-config-none',
|
|
274
|
-
title: 'No userConfig entries declared',
|
|
275
|
-
detail: 'This plugin does not currently declare install-time config entries.',
|
|
276
|
-
fix: 'No action needed unless this plugin needs install-time config or secret handling.',
|
|
277
|
-
path: 'pluxx.config.ts',
|
|
278
|
-
})
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const invalidEnvEntries = entries.filter((entry) => entry.envVar && !ENV_VAR_NAME.test(entry.envVar))
|
|
283
|
-
if (invalidEnvEntries.length > 0) {
|
|
284
|
-
addCheck(checks, {
|
|
285
|
-
level: 'warning',
|
|
286
|
-
code: 'user-config-env-invalid',
|
|
287
|
-
title: 'Invalid userConfig env var name',
|
|
288
|
-
detail: `${invalidEnvEntries.length} userConfig entr${invalidEnvEntries.length === 1 ? 'y' : 'ies'} use invalid env var names.`,
|
|
289
|
-
fix: 'Rename the env var to a shell-safe name and keep userConfig aligned with the install/runtime config contract.',
|
|
290
|
-
path: 'pluxx.config.ts',
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const secretEntriesWithoutEnv = entries.filter((entry) => entry.type === 'secret' && !entry.envVar)
|
|
295
|
-
if (secretEntriesWithoutEnv.length > 0) {
|
|
296
|
-
addCheck(checks, {
|
|
297
|
-
level: 'warning',
|
|
298
|
-
code: 'user-config-secret-missing-env',
|
|
299
|
-
title: 'Secret userConfig entries should declare envVar',
|
|
300
|
-
detail: `${secretEntriesWithoutEnv.length} secret userConfig entr${secretEntriesWithoutEnv.length === 1 ? 'y' : 'ies'} are missing envVar bindings.`,
|
|
301
|
-
fix: 'Bind secret entries to env vars so Pluxx can persist and validate install-time config safely.',
|
|
302
|
-
path: 'pluxx.config.ts',
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
addCheck(checks, {
|
|
307
|
-
level: 'info',
|
|
308
|
-
code: 'user-config-declared',
|
|
309
|
-
title: 'userConfig entries declared',
|
|
310
|
-
detail: `${entries.length} install-time config entr${entries.length === 1 ? 'y' : 'ies'} are declared.`,
|
|
311
|
-
fix: 'No action needed.',
|
|
312
|
-
path: 'pluxx.config.ts',
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function checkHookTrust(checks: DoctorCheck[], config: PluginConfig): void {
|
|
317
|
-
const commands = listHookCommands(config.hooks)
|
|
318
|
-
if (commands.length === 0) {
|
|
319
|
-
addCheck(checks, {
|
|
320
|
-
level: 'success',
|
|
321
|
-
code: 'hooks-no-commands',
|
|
322
|
-
title: 'No command hooks configured',
|
|
323
|
-
detail: 'This plugin does not define hook commands that execute shell code locally.',
|
|
324
|
-
fix: 'No action needed.',
|
|
325
|
-
path: 'pluxx.config.ts',
|
|
326
|
-
})
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
addCheck(checks, {
|
|
331
|
-
level: 'warning',
|
|
332
|
-
code: 'hooks-trust-required',
|
|
333
|
-
title: 'Hook commands require install trust',
|
|
334
|
-
detail: `This plugin defines local hook commands: ${commands.map((command) => `${command.event} -> ${command.command}`).join('; ')}`,
|
|
335
|
-
fix: 'Review the commands carefully. Users will need to opt in with pluxx install --trust.',
|
|
336
|
-
path: 'pluxx.config.ts',
|
|
337
|
-
})
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function isSafeManagedPath(path: string): boolean {
|
|
341
|
-
return path !== '' && !path.startsWith('/') && !path.includes('..')
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function formatSampleNames(values: string[]): string {
|
|
345
|
-
if (values.length === 0) return 'none'
|
|
346
|
-
const sample = values.slice(0, 3)
|
|
347
|
-
return values.length > sample.length
|
|
348
|
-
? `${sample.join(', ')} (+${values.length - sample.length} more)`
|
|
349
|
-
: sample.join(', ')
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function normalizeMetadataText(value: string): string {
|
|
353
|
-
return value
|
|
354
|
-
.trim()
|
|
355
|
-
.toLowerCase()
|
|
356
|
-
.replace(/\s+/g, ' ')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function isLowInfoDescription(description: string): boolean {
|
|
360
|
-
const normalizedDescription = normalizeMetadataText(description)
|
|
361
|
-
return LOW_INFO_DESCRIPTION_PATTERNS.some((pattern) => pattern.test(normalizedDescription))
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function checkMcpMetadataQuality(checks: DoctorCheck[], metadata: McpScaffoldMetadata): void {
|
|
365
|
-
const tools = metadata.tools ?? []
|
|
366
|
-
if (tools.length === 0) {
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const missingDescription = tools
|
|
371
|
-
.filter((tool) => !tool.description || tool.description.trim() === '')
|
|
372
|
-
.map((tool) => tool.name)
|
|
373
|
-
const lowInfoDescription = tools
|
|
374
|
-
.filter((tool) => {
|
|
375
|
-
const description = tool.description?.trim()
|
|
376
|
-
if (!description) return false
|
|
377
|
-
return isLowInfoDescription(description)
|
|
378
|
-
})
|
|
379
|
-
.map((tool) => tool.name)
|
|
380
|
-
const genericNames = tools
|
|
381
|
-
.filter((tool) => GENERIC_TOOL_NAME_PATTERNS.some((pattern) => pattern.test(tool.name.trim())))
|
|
382
|
-
.map((tool) => tool.name)
|
|
383
|
-
|
|
384
|
-
const findings: string[] = []
|
|
385
|
-
if (missingDescription.length > 0) {
|
|
386
|
-
findings.push(`missing descriptions: ${formatSampleNames(missingDescription)}`)
|
|
387
|
-
}
|
|
388
|
-
if (lowInfoDescription.length > 0) {
|
|
389
|
-
findings.push(`low-information descriptions: ${formatSampleNames(lowInfoDescription)}`)
|
|
390
|
-
}
|
|
391
|
-
if (genericNames.length > 0) {
|
|
392
|
-
findings.push(`generic tool names: ${formatSampleNames(genericNames)}`)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (findings.length === 0) {
|
|
396
|
-
addCheck(checks, {
|
|
397
|
-
level: 'success',
|
|
398
|
-
code: 'mcp-metadata-quality-ok',
|
|
399
|
-
title: 'MCP metadata quality looks strong',
|
|
400
|
-
detail: `Tool metadata quality checks passed for ${tools.length} tool(s).`,
|
|
401
|
-
fix: 'No action needed.',
|
|
402
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
403
|
-
})
|
|
404
|
-
return
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
addCheck(checks, {
|
|
408
|
-
level: 'warning',
|
|
409
|
-
code: 'mcp-metadata-quality-weak',
|
|
410
|
-
title: 'MCP metadata quality is weak in scaffold source',
|
|
411
|
-
detail: `Weak metadata signals detected across ${tools.length} tool(s): ${findings.join('; ')}`,
|
|
412
|
-
fix: 'Before publishing, run `pluxx agent run review` and refine generated sections with concrete tool descriptions and product-shaped naming.',
|
|
413
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
414
|
-
})
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function checkScaffoldMetadata(checks: DoctorCheck[], rootDir: string, config: PluginConfig): void {
|
|
418
|
-
const metadataPath = resolve(rootDir, MCP_SCAFFOLD_METADATA_PATH)
|
|
419
|
-
if (!existsSync(metadataPath)) {
|
|
420
|
-
addCheck(checks, {
|
|
421
|
-
level: 'info',
|
|
422
|
-
code: 'mcp-metadata-missing',
|
|
423
|
-
title: 'No MCP scaffold metadata found',
|
|
424
|
-
detail: `No ${MCP_SCAFFOLD_METADATA_PATH} file was found in this project.`,
|
|
425
|
-
fix: 'No action needed unless this project was created with pluxx init --from-mcp.',
|
|
426
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
427
|
-
})
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as McpScaffoldMetadata
|
|
433
|
-
const invalidManaged = metadata.managedFiles.filter((path) => !isSafeManagedPath(path))
|
|
434
|
-
if (metadata.version !== 1) {
|
|
435
|
-
addCheck(checks, {
|
|
436
|
-
level: 'warning',
|
|
437
|
-
code: 'mcp-metadata-version',
|
|
438
|
-
title: 'Unexpected MCP scaffold metadata version',
|
|
439
|
-
detail: `Found scaffold metadata version ${String(metadata.version)}.`,
|
|
440
|
-
fix: 'Re-run pluxx sync --from-mcp to refresh the scaffold metadata.',
|
|
441
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
442
|
-
})
|
|
443
|
-
} else if (invalidManaged.length > 0) {
|
|
444
|
-
addCheck(checks, {
|
|
445
|
-
level: 'error',
|
|
446
|
-
code: 'mcp-metadata-managed-paths',
|
|
447
|
-
title: 'Unsafe managed file paths in MCP metadata',
|
|
448
|
-
detail: `Managed file list contains unsafe relative paths: ${invalidManaged.join(', ')}`,
|
|
449
|
-
fix: 'Re-run pluxx init --from-mcp or pluxx sync --from-mcp to regenerate the metadata.',
|
|
450
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
451
|
-
})
|
|
452
|
-
} else {
|
|
453
|
-
addCheck(checks, {
|
|
454
|
-
level: 'success',
|
|
455
|
-
code: 'mcp-metadata-valid',
|
|
456
|
-
title: 'MCP scaffold metadata parsed successfully',
|
|
457
|
-
detail: `Managed scaffold metadata is present with ${metadata.managedFiles.length} managed files.`,
|
|
458
|
-
fix: 'No action needed.',
|
|
459
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (metadata.settings.pluginName !== config.name) {
|
|
464
|
-
addCheck(checks, {
|
|
465
|
-
level: 'warning',
|
|
466
|
-
code: 'mcp-metadata-plugin-mismatch',
|
|
467
|
-
title: 'MCP scaffold metadata name mismatch',
|
|
468
|
-
detail: `Metadata was generated for "${metadata.settings.pluginName}" but the config name is "${config.name}".`,
|
|
469
|
-
fix: 'If this plugin was renamed, run pluxx sync --from-mcp to refresh generated metadata.',
|
|
470
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
471
|
-
})
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
checkMcpMetadataQuality(checks, metadata)
|
|
475
|
-
} catch (error) {
|
|
476
|
-
addCheck(checks, {
|
|
477
|
-
level: 'error',
|
|
478
|
-
code: 'mcp-metadata-invalid',
|
|
479
|
-
title: 'MCP scaffold metadata is not parseable',
|
|
480
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
481
|
-
fix: `Repair or delete ${MCP_SCAFFOLD_METADATA_PATH}, then re-run pluxx init --from-mcp or pluxx sync --from-mcp.`,
|
|
482
|
-
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
483
|
-
})
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function detectConsumerLayout(rootDir: string): ConsumerLayoutDetection {
|
|
488
|
-
if (existsSync(resolve(rootDir, '.claude-plugin/plugin.json'))) {
|
|
489
|
-
return {
|
|
490
|
-
kind: 'installed-platform',
|
|
491
|
-
platform: 'claude-code',
|
|
492
|
-
manifestPath: '.claude-plugin/plugin.json',
|
|
493
|
-
mcpConfigPath: '.mcp.json',
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (existsSync(resolve(rootDir, '.cursor-plugin/plugin.json'))) {
|
|
498
|
-
return {
|
|
499
|
-
kind: 'installed-platform',
|
|
500
|
-
platform: 'cursor',
|
|
501
|
-
manifestPath: '.cursor-plugin/plugin.json',
|
|
502
|
-
mcpConfigPath: 'mcp.json',
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (existsSync(resolve(rootDir, '.codex-plugin/plugin.json'))) {
|
|
507
|
-
return {
|
|
508
|
-
kind: 'installed-platform',
|
|
509
|
-
platform: 'codex',
|
|
510
|
-
manifestPath: '.codex-plugin/plugin.json',
|
|
511
|
-
mcpConfigPath: '.mcp.json',
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const packagePath = resolve(rootDir, 'package.json')
|
|
516
|
-
const indexPath = resolve(rootDir, 'index.ts')
|
|
517
|
-
if (existsSync(packagePath) && existsSync(indexPath)) {
|
|
518
|
-
try {
|
|
519
|
-
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8')) as {
|
|
520
|
-
peerDependencies?: Record<string, string>
|
|
521
|
-
keywords?: string[]
|
|
522
|
-
}
|
|
523
|
-
if (pkg.peerDependencies?.['@opencode-ai/plugin'] || pkg.keywords?.includes('opencode-plugin')) {
|
|
524
|
-
return {
|
|
525
|
-
kind: 'installed-platform',
|
|
526
|
-
platform: 'opencode',
|
|
527
|
-
manifestPath: 'package.json',
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
} catch {
|
|
531
|
-
return {
|
|
532
|
-
kind: 'installed-platform',
|
|
533
|
-
platform: 'opencode',
|
|
534
|
-
manifestPath: 'package.json',
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (CONFIG_FILES.some((filename) => existsSync(resolve(rootDir, filename)))) {
|
|
540
|
-
return { kind: 'source-project' }
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (['claude-code', 'cursor', 'codex', 'opencode'].some((dir) => existsSync(resolve(rootDir, dir)))) {
|
|
544
|
-
return { kind: 'multi-target-dist' }
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return { kind: 'unknown' }
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function readJsonFile<T>(rootDir: string, relativePath: string): T {
|
|
551
|
-
return JSON.parse(readFileSync(resolve(rootDir, relativePath), 'utf-8')) as T
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function checkConsumerBundlePath(checks: DoctorCheck[], rootDir: string): void {
|
|
555
|
-
try {
|
|
556
|
-
accessSync(rootDir, constants.R_OK)
|
|
557
|
-
const details = lstatSync(rootDir)
|
|
558
|
-
addCheck(checks, {
|
|
559
|
-
level: 'success',
|
|
560
|
-
code: 'consumer-path-readable',
|
|
561
|
-
title: 'Consumer bundle path readable',
|
|
562
|
-
detail: `${rootDir} is present and readable${details.isSymbolicLink() ? ' (symlinked install)' : ''}.`,
|
|
563
|
-
fix: 'No action needed.',
|
|
564
|
-
path: rootDir,
|
|
565
|
-
})
|
|
566
|
-
} catch {
|
|
567
|
-
addCheck(checks, {
|
|
568
|
-
level: 'error',
|
|
569
|
-
code: 'consumer-path-unreadable',
|
|
570
|
-
title: 'Consumer bundle path unreadable',
|
|
571
|
-
detail: `The installed plugin path is not readable: ${rootDir}`,
|
|
572
|
-
fix: 'Fix the path or permissions and rerun pluxx doctor --consumer.',
|
|
573
|
-
path: rootDir,
|
|
574
|
-
})
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function checkConsumerManifest(checks: DoctorCheck[], rootDir: string, layout: ConsumerBundleLayout): void {
|
|
579
|
-
try {
|
|
580
|
-
const manifest = readJsonFile<Record<string, unknown>>(rootDir, layout.manifestPath)
|
|
581
|
-
const name = typeof manifest.name === 'string' && manifest.name.trim() !== ''
|
|
582
|
-
? manifest.name
|
|
583
|
-
: layout.platform
|
|
584
|
-
const version = typeof manifest.version === 'string' && manifest.version.trim() !== ''
|
|
585
|
-
? manifest.version
|
|
586
|
-
: 'unknown'
|
|
587
|
-
|
|
588
|
-
addCheck(checks, {
|
|
589
|
-
level: 'success',
|
|
590
|
-
code: 'consumer-manifest-valid',
|
|
591
|
-
title: 'Installed plugin manifest parsed successfully',
|
|
592
|
-
detail: `Detected ${name}@${version} for ${layout.platform}.`,
|
|
593
|
-
fix: 'No action needed.',
|
|
594
|
-
path: layout.manifestPath,
|
|
595
|
-
})
|
|
596
|
-
} catch (error) {
|
|
597
|
-
addCheck(checks, {
|
|
598
|
-
level: 'error',
|
|
599
|
-
code: 'consumer-manifest-invalid',
|
|
600
|
-
title: 'Installed plugin manifest is not parseable',
|
|
601
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
602
|
-
fix: 'Rebuild or reinstall this plugin bundle and rerun pluxx doctor --consumer.',
|
|
603
|
-
path: layout.manifestPath,
|
|
604
|
-
})
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function checkInstalledUserConfig(checks: DoctorCheck[], rootDir: string): void {
|
|
609
|
-
const userConfigPath = '.pluxx-user.json'
|
|
610
|
-
const resolvedPath = resolve(rootDir, userConfigPath)
|
|
611
|
-
if (!existsSync(resolvedPath)) {
|
|
612
|
-
addCheck(checks, {
|
|
613
|
-
level: 'info',
|
|
614
|
-
code: 'consumer-user-config-missing',
|
|
615
|
-
title: 'No local install config materialized',
|
|
616
|
-
detail: 'This bundle does not include a .pluxx-user.json file.',
|
|
617
|
-
fix: 'If tools require secrets or install-time config, reinstall the plugin and provide the requested values.',
|
|
618
|
-
path: userConfigPath,
|
|
619
|
-
})
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
try {
|
|
624
|
-
const payload = JSON.parse(readFileSync(resolvedPath, 'utf-8')) as {
|
|
625
|
-
values?: Record<string, unknown>
|
|
626
|
-
env?: Record<string, string>
|
|
627
|
-
}
|
|
628
|
-
const valueCount = Object.keys(payload.values ?? {}).length
|
|
629
|
-
const envCount = Object.keys(payload.env ?? {}).length
|
|
630
|
-
addCheck(checks, {
|
|
631
|
-
level: 'success',
|
|
632
|
-
code: 'consumer-user-config-valid',
|
|
633
|
-
title: 'Local install config parsed successfully',
|
|
634
|
-
detail: `.pluxx-user.json contains ${valueCount} saved value entr${valueCount === 1 ? 'y' : 'ies'} and ${envCount} env binding${envCount === 1 ? '' : 's'}.`,
|
|
635
|
-
fix: 'No action needed.',
|
|
636
|
-
path: userConfigPath,
|
|
637
|
-
})
|
|
638
|
-
} catch (error) {
|
|
639
|
-
addCheck(checks, {
|
|
640
|
-
level: 'error',
|
|
641
|
-
code: 'consumer-user-config-invalid',
|
|
642
|
-
title: 'Local install config is not parseable',
|
|
643
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
644
|
-
fix: 'Delete or repair .pluxx-user.json, then reinstall the plugin if needed.',
|
|
645
|
-
path: userConfigPath,
|
|
646
|
-
})
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function checkInstalledEnvValidation(checks: DoctorCheck[], rootDir: string): void {
|
|
651
|
-
const envScriptPath = 'scripts/check-env.sh'
|
|
652
|
-
const resolvedPath = resolve(rootDir, envScriptPath)
|
|
653
|
-
if (!existsSync(resolvedPath)) {
|
|
654
|
-
addCheck(checks, {
|
|
655
|
-
level: 'info',
|
|
656
|
-
code: 'consumer-env-script-missing',
|
|
657
|
-
title: 'No install-time env validation script found',
|
|
658
|
-
detail: 'This bundle does not ship a scripts/check-env.sh file.',
|
|
659
|
-
fix: 'No action needed unless this plugin is expected to validate runtime secrets on install.',
|
|
660
|
-
path: envScriptPath,
|
|
661
|
-
})
|
|
662
|
-
return
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
const content = readFileSync(resolvedPath, 'utf-8')
|
|
666
|
-
if (content.includes(MATERIALIZED_ENV_MARKER)) {
|
|
667
|
-
addCheck(checks, {
|
|
668
|
-
level: 'success',
|
|
669
|
-
code: 'consumer-env-script-materialized',
|
|
670
|
-
title: 'Install-time env validation was disabled after materialization',
|
|
671
|
-
detail: 'This local install already materialized required config, so the env validation hook is bypassed.',
|
|
672
|
-
fix: 'No action needed.',
|
|
673
|
-
path: envScriptPath,
|
|
674
|
-
})
|
|
675
|
-
return
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
addCheck(checks, {
|
|
679
|
-
level: 'warning',
|
|
680
|
-
code: 'consumer-env-script-active',
|
|
681
|
-
title: 'Install-time env validation is still active',
|
|
682
|
-
detail: 'This bundle still runs scripts/check-env.sh, which usually means required config was not materialized into the installed plugin.',
|
|
683
|
-
fix: 'If authenticated tools fail, reinstall the plugin and provide the requested userConfig values or required env vars.',
|
|
684
|
-
path: envScriptPath,
|
|
685
|
-
})
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function checkInstalledMcpConfig(checks: DoctorCheck[], rootDir: string, layout: ConsumerBundleLayout): void {
|
|
689
|
-
if (!layout.mcpConfigPath) {
|
|
690
|
-
addCheck(checks, {
|
|
691
|
-
level: 'info',
|
|
692
|
-
code: 'consumer-mcp-config-not-applicable',
|
|
693
|
-
title: 'No static MCP config file for this platform',
|
|
694
|
-
detail: `${layout.platform} builds runtime MCP wiring inside the plugin wrapper rather than a standalone JSON file.`,
|
|
695
|
-
fix: 'No action needed.',
|
|
696
|
-
path: layout.manifestPath,
|
|
697
|
-
})
|
|
698
|
-
return
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const resolvedPath = resolve(rootDir, layout.mcpConfigPath)
|
|
702
|
-
if (!existsSync(resolvedPath)) {
|
|
703
|
-
addCheck(checks, {
|
|
704
|
-
level: 'info',
|
|
705
|
-
code: 'consumer-mcp-config-missing',
|
|
706
|
-
title: 'No MCP config file emitted in this bundle',
|
|
707
|
-
detail: `This ${layout.platform} bundle does not include ${layout.mcpConfigPath}.`,
|
|
708
|
-
fix: 'No action needed unless this plugin should expose MCP servers on this platform.',
|
|
709
|
-
path: layout.mcpConfigPath,
|
|
710
|
-
})
|
|
711
|
-
return
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
try {
|
|
715
|
-
const payload = readJsonFile<{ mcpServers?: Record<string, Record<string, unknown>> }>(rootDir, layout.mcpConfigPath)
|
|
716
|
-
const servers = Object.values(payload.mcpServers ?? {})
|
|
717
|
-
addCheck(checks, {
|
|
718
|
-
level: 'success',
|
|
719
|
-
code: 'consumer-mcp-config-valid',
|
|
720
|
-
title: 'Installed MCP config parsed successfully',
|
|
721
|
-
detail: `${layout.mcpConfigPath} defines ${servers.length} MCP server${servers.length === 1 ? '' : 's'}.`,
|
|
722
|
-
fix: 'No action needed.',
|
|
723
|
-
path: layout.mcpConfigPath,
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
if (servers.length === 0) {
|
|
727
|
-
return
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const remoteEntries = servers.filter((server) => 'url' in server)
|
|
731
|
-
const stdioEntries = servers.filter((server) => 'command' in server)
|
|
732
|
-
const inlineHeaderEntries = servers.filter((server) => {
|
|
733
|
-
if ('headers' in server && server.headers && typeof server.headers === 'object') return true
|
|
734
|
-
if ('http_headers' in server && server.http_headers && typeof server.http_headers === 'object') return true
|
|
735
|
-
return false
|
|
736
|
-
})
|
|
737
|
-
|
|
738
|
-
if (stdioEntries.length > 0) {
|
|
739
|
-
addCheck(checks, {
|
|
740
|
-
level: 'info',
|
|
741
|
-
code: 'consumer-mcp-stdio',
|
|
742
|
-
title: 'Local MCP servers configured',
|
|
743
|
-
detail: `${stdioEntries.length} MCP server${stdioEntries.length === 1 ? '' : 's'} run via local stdio commands in this bundle.`,
|
|
744
|
-
fix: 'If tools fail, verify the bundled command or its runtime dependencies on this machine.',
|
|
745
|
-
path: layout.mcpConfigPath,
|
|
746
|
-
})
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (remoteEntries.length > 0 && inlineHeaderEntries.length > 0) {
|
|
750
|
-
addCheck(checks, {
|
|
751
|
-
level: 'success',
|
|
752
|
-
code: 'consumer-mcp-inline-auth',
|
|
753
|
-
title: 'Remote MCP auth was materialized into this install',
|
|
754
|
-
detail: `${inlineHeaderEntries.length} remote MCP server${inlineHeaderEntries.length === 1 ? '' : 's'} include inline auth headers in the installed bundle.`,
|
|
755
|
-
fix: 'No action needed.',
|
|
756
|
-
path: layout.mcpConfigPath,
|
|
757
|
-
})
|
|
758
|
-
return
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (remoteEntries.length > 0) {
|
|
762
|
-
const fix = layout.platform === 'claude-code' || layout.platform === 'cursor'
|
|
763
|
-
? 'If authenticated tools fail, complete the platform auth flow in the host or reinstall with any required userConfig values.'
|
|
764
|
-
: 'If authenticated tools fail, reinstall the plugin and provide any required userConfig or runtime env vars.'
|
|
765
|
-
addCheck(checks, {
|
|
766
|
-
level: 'info',
|
|
767
|
-
code: 'consumer-mcp-remote-auth-runtime',
|
|
768
|
-
title: 'Remote MCP auth is expected at runtime',
|
|
769
|
-
detail: `${remoteEntries.length} remote MCP server${remoteEntries.length === 1 ? '' : 's'} are configured without inline auth headers in this installed bundle.`,
|
|
770
|
-
fix,
|
|
771
|
-
path: layout.mcpConfigPath,
|
|
772
|
-
})
|
|
773
|
-
}
|
|
774
|
-
} catch (error) {
|
|
775
|
-
addCheck(checks, {
|
|
776
|
-
level: 'error',
|
|
777
|
-
code: 'consumer-mcp-config-invalid',
|
|
778
|
-
title: 'Installed MCP config is not parseable',
|
|
779
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
780
|
-
fix: 'Rebuild or reinstall this platform bundle and rerun pluxx doctor --consumer.',
|
|
781
|
-
path: layout.mcpConfigPath,
|
|
782
|
-
})
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
export async function doctorConsumer(rootDir: string = process.cwd()): Promise<DoctorReport> {
|
|
787
|
-
const checks: DoctorCheck[] = []
|
|
788
|
-
const bunVersion = process.versions.bun
|
|
789
|
-
const bunMajor = parseMajorVersion(bunVersion)
|
|
790
|
-
|
|
791
|
-
if (!bunVersion) {
|
|
792
|
-
addCheck(checks, {
|
|
793
|
-
level: 'error',
|
|
794
|
-
code: 'bun-missing',
|
|
795
|
-
title: 'Bun runtime not detected',
|
|
796
|
-
detail: 'pluxx currently requires Bun at runtime.',
|
|
797
|
-
fix: 'Install Bun from https://bun.sh and rerun pluxx doctor --consumer.',
|
|
798
|
-
})
|
|
799
|
-
} else if (bunMajor === null || bunMajor < 1) {
|
|
800
|
-
addCheck(checks, {
|
|
801
|
-
level: 'error',
|
|
802
|
-
code: 'bun-version-unsupported',
|
|
803
|
-
title: 'Unsupported Bun version',
|
|
804
|
-
detail: `Detected Bun ${bunVersion}. pluxx requires Bun >= 1.0.`,
|
|
805
|
-
fix: 'Upgrade Bun to a supported version and rerun pluxx doctor --consumer.',
|
|
806
|
-
})
|
|
807
|
-
} else {
|
|
808
|
-
addCheck(checks, {
|
|
809
|
-
level: 'success',
|
|
810
|
-
code: 'bun-version',
|
|
811
|
-
title: 'Supported Bun runtime detected',
|
|
812
|
-
detail: `Bun ${bunVersion} is available.`,
|
|
813
|
-
fix: 'No action needed.',
|
|
814
|
-
})
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
checkConsumerBundlePath(checks, rootDir)
|
|
818
|
-
const layout = detectConsumerLayout(rootDir)
|
|
819
|
-
|
|
820
|
-
if (layout.kind === 'source-project') {
|
|
821
|
-
addCheck(checks, {
|
|
822
|
-
level: 'error',
|
|
823
|
-
code: 'consumer-source-project',
|
|
824
|
-
title: 'Consumer doctor expects an installed or built platform bundle',
|
|
825
|
-
detail: `Found a pluxx source project at ${rootDir}, not a built platform directory.`,
|
|
826
|
-
fix: 'Run `pluxx doctor` in the source project, or run `pluxx doctor --consumer <dist/platform>` against an installed or built bundle.',
|
|
827
|
-
})
|
|
828
|
-
return summarizeChecks(checks)
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (layout.kind === 'multi-target-dist') {
|
|
832
|
-
addCheck(checks, {
|
|
833
|
-
level: 'error',
|
|
834
|
-
code: 'consumer-dist-root',
|
|
835
|
-
title: 'Consumer doctor expects one platform directory at a time',
|
|
836
|
-
detail: `Found a multi-target dist root at ${rootDir}.`,
|
|
837
|
-
fix: 'Point --consumer at one built platform directory such as dist/cursor or an installed plugin path.',
|
|
838
|
-
})
|
|
839
|
-
return summarizeChecks(checks)
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (layout.kind === 'unknown') {
|
|
843
|
-
addCheck(checks, {
|
|
844
|
-
level: 'error',
|
|
845
|
-
code: 'consumer-platform-unknown',
|
|
846
|
-
title: 'Could not detect an installed plugin layout',
|
|
847
|
-
detail: `No known installed plugin markers were found in ${rootDir}.`,
|
|
848
|
-
fix: 'Pass the root of a built platform bundle or installed plugin directory to pluxx doctor --consumer.',
|
|
849
|
-
})
|
|
850
|
-
return summarizeChecks(checks)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
addCheck(checks, {
|
|
854
|
-
level: 'success',
|
|
855
|
-
code: 'consumer-platform-detected',
|
|
856
|
-
title: 'Installed platform bundle detected',
|
|
857
|
-
detail: `Detected a ${layout.platform} plugin bundle.`,
|
|
858
|
-
fix: 'No action needed.',
|
|
859
|
-
path: layout.manifestPath,
|
|
860
|
-
})
|
|
861
|
-
|
|
862
|
-
checkConsumerManifest(checks, rootDir, layout)
|
|
863
|
-
checkInstalledUserConfig(checks, rootDir)
|
|
864
|
-
checkInstalledEnvValidation(checks, rootDir)
|
|
865
|
-
checkInstalledMcpConfig(checks, rootDir, layout)
|
|
866
|
-
|
|
867
|
-
return summarizeChecks(checks)
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
export async function doctorProject(rootDir: string = process.cwd()): Promise<DoctorReport> {
|
|
871
|
-
const checks: DoctorCheck[] = []
|
|
872
|
-
const bunVersion = process.versions.bun
|
|
873
|
-
const bunMajor = parseMajorVersion(bunVersion)
|
|
874
|
-
const configPath = CONFIG_FILES.find((filename) => existsSync(resolve(rootDir, filename)))
|
|
875
|
-
|
|
876
|
-
if (!bunVersion) {
|
|
877
|
-
addCheck(checks, {
|
|
878
|
-
level: 'error',
|
|
879
|
-
code: 'bun-missing',
|
|
880
|
-
title: 'Bun runtime not detected',
|
|
881
|
-
detail: 'pluxx currently requires Bun at runtime.',
|
|
882
|
-
fix: 'Install Bun from https://bun.sh and rerun pluxx doctor.',
|
|
883
|
-
})
|
|
884
|
-
} else if (bunMajor === null || bunMajor < 1) {
|
|
885
|
-
addCheck(checks, {
|
|
886
|
-
level: 'error',
|
|
887
|
-
code: 'bun-version-unsupported',
|
|
888
|
-
title: 'Unsupported Bun version',
|
|
889
|
-
detail: `Detected Bun ${bunVersion}. pluxx requires Bun >= 1.0.`,
|
|
890
|
-
fix: 'Upgrade Bun to a supported version and rerun pluxx doctor.',
|
|
891
|
-
})
|
|
892
|
-
} else {
|
|
893
|
-
addCheck(checks, {
|
|
894
|
-
level: 'success',
|
|
895
|
-
code: 'bun-version',
|
|
896
|
-
title: 'Supported Bun runtime detected',
|
|
897
|
-
detail: `Bun ${bunVersion} is available.`,
|
|
898
|
-
fix: 'No action needed.',
|
|
899
|
-
})
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (!configPath) {
|
|
903
|
-
addCheck(checks, {
|
|
904
|
-
level: 'error',
|
|
905
|
-
code: 'config-not-found',
|
|
906
|
-
title: 'pluxx config not found',
|
|
907
|
-
detail: `Expected one of ${CONFIG_FILES.join(', ')} in ${rootDir}.`,
|
|
908
|
-
fix: 'Create a pluxx.config.ts, pluxx.config.js, or pluxx.config.json file in the project root.',
|
|
909
|
-
})
|
|
910
|
-
return summarizeChecks(checks)
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
addCheck(checks, {
|
|
914
|
-
level: 'success',
|
|
915
|
-
code: 'config-found',
|
|
916
|
-
title: 'pluxx config found',
|
|
917
|
-
detail: `Detected ${configPath} in the project root.`,
|
|
918
|
-
fix: 'No action needed.',
|
|
919
|
-
path: configPath,
|
|
920
|
-
})
|
|
921
|
-
|
|
922
|
-
let config: PluginConfig
|
|
923
|
-
try {
|
|
924
|
-
config = await loadConfig(rootDir)
|
|
925
|
-
addCheck(checks, {
|
|
926
|
-
level: 'success',
|
|
927
|
-
code: 'config-valid',
|
|
928
|
-
title: 'Config parsed successfully',
|
|
929
|
-
detail: `Loaded ${config.name}@${config.version} for ${config.targets.length} target(s).`,
|
|
930
|
-
fix: 'No action needed.',
|
|
931
|
-
path: configPath,
|
|
932
|
-
})
|
|
933
|
-
} catch (error) {
|
|
934
|
-
addCheck(checks, {
|
|
935
|
-
level: 'error',
|
|
936
|
-
code: 'config-invalid',
|
|
937
|
-
title: 'Config could not be loaded',
|
|
938
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
939
|
-
fix: 'Fix the config error and rerun pluxx doctor.',
|
|
940
|
-
path: configPath,
|
|
941
|
-
})
|
|
942
|
-
return summarizeChecks(checks)
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
checkReadablePath(checks, rootDir, 'Skills', config.skills, true)
|
|
946
|
-
checkReadablePath(checks, rootDir, 'Instructions', config.instructions, false)
|
|
947
|
-
checkReadablePath(checks, rootDir, 'Agents', config.agents, false)
|
|
948
|
-
checkReadablePath(checks, rootDir, 'Commands', config.commands, false)
|
|
949
|
-
checkReadablePath(checks, rootDir, 'Scripts', config.scripts, false)
|
|
950
|
-
checkReadablePath(checks, rootDir, 'Assets', config.assets, false)
|
|
951
|
-
checkTargetPlatforms(checks, config)
|
|
952
|
-
checkMcpConfig(checks, config)
|
|
953
|
-
checkUserConfig(checks, config)
|
|
954
|
-
checkScaffoldMetadata(checks, rootDir, config)
|
|
955
|
-
checkHookTrust(checks, config)
|
|
956
|
-
|
|
957
|
-
for (const target of config.targets) {
|
|
958
|
-
const limits = PLATFORM_LIMITS[target]
|
|
959
|
-
if (!CORE_FOUR.has(target) && limits) {
|
|
960
|
-
addCheck(checks, {
|
|
961
|
-
level: 'info',
|
|
962
|
-
code: 'target-caveat',
|
|
963
|
-
title: `Platform caveat for ${target}`,
|
|
964
|
-
detail: `${target} is supported, but it is currently less validated than the core four targets.`,
|
|
965
|
-
fix: 'Use pluxx lint and pluxx test before relying on beta targets in production.',
|
|
966
|
-
path: 'pluxx.config.ts',
|
|
967
|
-
})
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
return summarizeChecks(checks)
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
export function printDoctorReport(report: DoctorReport): void {
|
|
975
|
-
for (const check of report.checks) {
|
|
976
|
-
const prefix = check.level.toUpperCase().padEnd(7, ' ')
|
|
977
|
-
const pathLabel = check.path ? ` [${check.path}]` : ''
|
|
978
|
-
console.log(`${prefix} ${check.code}${pathLabel} ${check.title}`)
|
|
979
|
-
console.log(` ${check.detail}`)
|
|
980
|
-
console.log(` Fix: ${check.fix}`)
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
console.log('')
|
|
984
|
-
console.log(
|
|
985
|
-
`Doctor summary: ${report.errors} error(s), ${report.warnings} warning(s), ${report.infos} info message(s)`,
|
|
986
|
-
)
|
|
987
|
-
}
|