@orchid-labs/pluxx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +574 -0
- package/bin/pluxx.js +37 -0
- package/dist/cli/agent.d.ts +90 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/dev.d.ts +2 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/doctor.d.ts +19 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/init-from-mcp.d.ts +145 -0
- package/dist/cli/init-from-mcp.d.ts.map +1 -0
- package/dist/cli/install.d.ts +56 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/lint.d.ts +18 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/migrate.d.ts +2 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/prompt.d.ts +20 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/publish.d.ts +70 -0
- package/dist/cli/publish.d.ts.map +1 -0
- package/dist/cli/runtime.d.ts +20 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/sync-from-mcp.d.ts +32 -0
- package/dist/cli/sync-from-mcp.d.ts.map +1 -0
- package/dist/cli/test.d.ts +33 -0
- package/dist/cli/test.d.ts.map +1 -0
- package/dist/compatibility/matrix.d.ts +14 -0
- package/dist/compatibility/matrix.d.ts.map +1 -0
- package/dist/config/define.d.ts +18 -0
- package/dist/config/define.d.ts.map +1 -0
- package/dist/config/load.d.ts +7 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/generators/amp/index.d.ts +13 -0
- package/dist/generators/amp/index.d.ts.map +1 -0
- package/dist/generators/base.d.ts +49 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/claude-code/index.d.ts +7 -0
- package/dist/generators/claude-code/index.d.ts.map +1 -0
- package/dist/generators/cline/index.d.ts +14 -0
- package/dist/generators/cline/index.d.ts.map +1 -0
- package/dist/generators/codex/index.d.ts +9 -0
- package/dist/generators/codex/index.d.ts.map +1 -0
- package/dist/generators/cursor/index.d.ts +11 -0
- package/dist/generators/cursor/index.d.ts.map +1 -0
- package/dist/generators/gemini-cli/index.d.ts +13 -0
- package/dist/generators/gemini-cli/index.d.ts.map +1 -0
- package/dist/generators/github-copilot/index.d.ts +11 -0
- package/dist/generators/github-copilot/index.d.ts.map +1 -0
- package/dist/generators/hooks-warning.d.ts +3 -0
- package/dist/generators/hooks-warning.d.ts.map +1 -0
- package/dist/generators/index.d.ts +11 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/opencode/index.d.ts +15 -0
- package/dist/generators/opencode/index.d.ts.map +1 -0
- package/dist/generators/openhands/index.d.ts +11 -0
- package/dist/generators/openhands/index.d.ts.map +1 -0
- package/dist/generators/roo-code/index.d.ts +14 -0
- package/dist/generators/roo-code/index.d.ts.map +1 -0
- package/dist/generators/shared/claude-family.d.ts +18 -0
- package/dist/generators/shared/claude-family.d.ts.map +1 -0
- package/dist/generators/warp/index.d.ts +13 -0
- package/dist/generators/warp/index.d.ts.map +1 -0
- package/dist/hook-events.d.ts +4 -0
- package/dist/hook-events.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5302 -0
- package/dist/mcp/introspect.d.ts +34 -0
- package/dist/mcp/introspect.d.ts.map +1 -0
- package/dist/permissions.d.ts +18 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/schema.d.ts +9457 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/user-config.d.ts +19 -0
- package/dist/user-config.d.ts.map +1 -0
- package/dist/validation/platform-rules.d.ts +64 -0
- package/dist/validation/platform-rules.d.ts.map +1 -0
- package/package.json +76 -0
- package/src/cli/agent.ts +1030 -0
- package/src/cli/dev.ts +112 -0
- package/src/cli/doctor.ts +588 -0
- package/src/cli/index.ts +2414 -0
- package/src/cli/init-from-mcp.ts +1611 -0
- package/src/cli/install.ts +698 -0
- package/src/cli/lint.ts +1219 -0
- package/src/cli/migrate.ts +614 -0
- package/src/cli/prompt.ts +82 -0
- package/src/cli/publish.ts +401 -0
- package/src/cli/runtime.ts +86 -0
- package/src/cli/sync-from-mcp.ts +563 -0
- package/src/cli/test.ts +134 -0
- package/src/compatibility/matrix.ts +149 -0
- package/src/config/define.ts +20 -0
- package/src/config/load.ts +74 -0
- package/src/generators/amp/index.ts +63 -0
- package/src/generators/base.ts +188 -0
- package/src/generators/claude-code/index.ts +29 -0
- package/src/generators/cline/index.ts +35 -0
- package/src/generators/codex/index.ts +120 -0
- package/src/generators/cursor/index.ts +158 -0
- package/src/generators/gemini-cli/index.ts +83 -0
- package/src/generators/github-copilot/index.ts +32 -0
- package/src/generators/hooks-warning.ts +51 -0
- package/src/generators/index.ts +71 -0
- package/src/generators/opencode/index.ts +526 -0
- package/src/generators/openhands/index.ts +32 -0
- package/src/generators/roo-code/index.ts +35 -0
- package/src/generators/shared/claude-family.ts +215 -0
- package/src/generators/warp/index.ts +32 -0
- package/src/hook-events.ts +33 -0
- package/src/index.ts +23 -0
- package/src/mcp/introspect.ts +834 -0
- package/src/permissions.ts +258 -0
- package/src/schema.ts +312 -0
- package/src/user-config.ts +177 -0
- package/src/validation/platform-rules.ts +565 -0
package/src/cli/dev.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from 'fs'
|
|
2
|
+
import { relative, resolve } from 'path'
|
|
3
|
+
import { loadConfig } from '../config/load'
|
|
4
|
+
import { build } from '../generators'
|
|
5
|
+
import type { TargetPlatform } from '../schema'
|
|
6
|
+
|
|
7
|
+
/** Patterns that trigger a rebuild when changed */
|
|
8
|
+
const WATCH_PATTERNS = [
|
|
9
|
+
/^pluxx\.config\.(ts|js|json)$/,
|
|
10
|
+
/^skills\//,
|
|
11
|
+
/^commands\//,
|
|
12
|
+
/^agents\//,
|
|
13
|
+
/^scripts\//,
|
|
14
|
+
/\.md$/,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function shouldRebuild(relativePath: string): boolean {
|
|
18
|
+
return WATCH_PATTERNS.some(pattern => pattern.test(relativePath))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runDev(args: string[]) {
|
|
22
|
+
const targetFlag = args.indexOf('--target')
|
|
23
|
+
let targets: TargetPlatform[] | undefined
|
|
24
|
+
|
|
25
|
+
if (targetFlag !== -1) {
|
|
26
|
+
targets = args.slice(targetFlag + 1).filter(a => !a.startsWith('-')) as TargetPlatform[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rootDir = process.cwd()
|
|
30
|
+
|
|
31
|
+
console.log('pluxx dev — watching for changes...')
|
|
32
|
+
console.log('')
|
|
33
|
+
|
|
34
|
+
// Run initial build
|
|
35
|
+
await runBuild(rootDir, targets)
|
|
36
|
+
|
|
37
|
+
// Debounce timer
|
|
38
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
39
|
+
let pendingFile: string | null = null
|
|
40
|
+
|
|
41
|
+
const watcher: FSWatcher = watch(rootDir, { recursive: true }, (_event, filename) => {
|
|
42
|
+
if (!filename) return
|
|
43
|
+
|
|
44
|
+
const rel = relative(rootDir, resolve(rootDir, filename))
|
|
45
|
+
|
|
46
|
+
// Skip output directory and hidden files
|
|
47
|
+
if (rel.startsWith('dist/') || rel.startsWith('.') || rel.includes('node_modules')) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!shouldRebuild(rel)) return
|
|
52
|
+
|
|
53
|
+
pendingFile = rel
|
|
54
|
+
|
|
55
|
+
if (debounceTimer) {
|
|
56
|
+
clearTimeout(debounceTimer)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
debounceTimer = setTimeout(async () => {
|
|
60
|
+
debounceTimer = null
|
|
61
|
+
const changed = pendingFile
|
|
62
|
+
pendingFile = null
|
|
63
|
+
|
|
64
|
+
console.clear()
|
|
65
|
+
console.log(`Change detected: ${changed}`)
|
|
66
|
+
console.log('')
|
|
67
|
+
|
|
68
|
+
await runBuild(rootDir, targets)
|
|
69
|
+
}, 300)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Keep process alive, clean up on exit
|
|
73
|
+
process.on('SIGINT', () => {
|
|
74
|
+
watcher.close()
|
|
75
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
76
|
+
console.log('\nStopped watching.')
|
|
77
|
+
process.exit(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
process.on('SIGTERM', () => {
|
|
81
|
+
watcher.close()
|
|
82
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
83
|
+
process.exit(0)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function runBuild(rootDir: string, targets?: TargetPlatform[]) {
|
|
88
|
+
const start = performance.now()
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const config = await loadConfig(rootDir)
|
|
92
|
+
const platforms = targets ?? config.targets
|
|
93
|
+
|
|
94
|
+
console.log(`Building for: ${platforms.join(', ')}`)
|
|
95
|
+
|
|
96
|
+
await build(config, rootDir, { targets })
|
|
97
|
+
|
|
98
|
+
const elapsed = (performance.now() - start).toFixed(0)
|
|
99
|
+
console.log(`Done in ${elapsed}ms — output in ${config.outDir}/`)
|
|
100
|
+
for (const platform of platforms) {
|
|
101
|
+
console.log(` ${config.outDir}/${platform}/`)
|
|
102
|
+
}
|
|
103
|
+
console.log('')
|
|
104
|
+
console.log('Watching for changes...')
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const elapsed = (performance.now() - start).toFixed(0)
|
|
107
|
+
console.error(`Build failed after ${elapsed}ms:`)
|
|
108
|
+
console.error(err instanceof Error ? err.message : err)
|
|
109
|
+
console.log('')
|
|
110
|
+
console.log('Watching for changes...')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, 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
|
+
|
|
46
|
+
function addCheck(checks: DoctorCheck[], check: DoctorCheck): void {
|
|
47
|
+
checks.push(check)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function summarizeChecks(checks: DoctorCheck[]): DoctorReport {
|
|
51
|
+
const errors = checks.filter((check) => check.level === 'error').length
|
|
52
|
+
const warnings = checks.filter((check) => check.level === 'warning').length
|
|
53
|
+
const infos = checks.filter((check) => check.level === 'info').length
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ok: errors === 0,
|
|
57
|
+
errors,
|
|
58
|
+
warnings,
|
|
59
|
+
infos,
|
|
60
|
+
checks,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseMajorVersion(version: string | undefined): number | null {
|
|
65
|
+
if (!version) return null
|
|
66
|
+
const match = version.match(/^(\d+)/)
|
|
67
|
+
return match ? Number(match[1]) : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkReadablePath(
|
|
71
|
+
checks: DoctorCheck[],
|
|
72
|
+
rootDir: string,
|
|
73
|
+
label: string,
|
|
74
|
+
configuredPath: string | undefined,
|
|
75
|
+
required: boolean,
|
|
76
|
+
): void {
|
|
77
|
+
if (!configuredPath) {
|
|
78
|
+
if (required) {
|
|
79
|
+
addCheck(checks, {
|
|
80
|
+
level: 'error',
|
|
81
|
+
code: 'path-missing',
|
|
82
|
+
title: `${label} path missing`,
|
|
83
|
+
detail: `The config does not define a ${label.toLowerCase()} path.`,
|
|
84
|
+
fix: `Add a valid ${label.toLowerCase()} path to pluxx.config.ts.`,
|
|
85
|
+
path: 'pluxx.config.ts',
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resolvedPath = resolve(rootDir, configuredPath)
|
|
92
|
+
if (!existsSync(resolvedPath)) {
|
|
93
|
+
addCheck(checks, {
|
|
94
|
+
level: 'error',
|
|
95
|
+
code: 'path-not-found',
|
|
96
|
+
title: `${label} path not found`,
|
|
97
|
+
detail: `Configured ${label.toLowerCase()} path does not exist: ${configuredPath}`,
|
|
98
|
+
fix: `Create ${configuredPath} or update the path in pluxx.config.ts.`,
|
|
99
|
+
path: configuredPath,
|
|
100
|
+
})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
accessSync(resolvedPath, constants.R_OK)
|
|
106
|
+
addCheck(checks, {
|
|
107
|
+
level: 'success',
|
|
108
|
+
code: 'path-readable',
|
|
109
|
+
title: `${label} path readable`,
|
|
110
|
+
detail: `${configuredPath} is present and readable.`,
|
|
111
|
+
fix: 'No action needed.',
|
|
112
|
+
path: configuredPath,
|
|
113
|
+
})
|
|
114
|
+
} catch {
|
|
115
|
+
addCheck(checks, {
|
|
116
|
+
level: 'error',
|
|
117
|
+
code: 'path-unreadable',
|
|
118
|
+
title: `${label} path unreadable`,
|
|
119
|
+
detail: `Configured ${label.toLowerCase()} path is not readable: ${configuredPath}`,
|
|
120
|
+
fix: `Adjust permissions on ${configuredPath}.`,
|
|
121
|
+
path: configuredPath,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkTargetPlatforms(checks: DoctorCheck[], config: PluginConfig): void {
|
|
127
|
+
const betaTargets = config.targets.filter((target) => !CORE_FOUR.has(target))
|
|
128
|
+
|
|
129
|
+
if (betaTargets.length === 0) {
|
|
130
|
+
addCheck(checks, {
|
|
131
|
+
level: 'success',
|
|
132
|
+
code: 'targets-core-four',
|
|
133
|
+
title: 'Core-four target set',
|
|
134
|
+
detail: `Configured targets stay within the primary launch path: ${config.targets.join(', ')}`,
|
|
135
|
+
fix: 'No action needed.',
|
|
136
|
+
path: 'pluxx.config.ts',
|
|
137
|
+
})
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
addCheck(checks, {
|
|
142
|
+
level: 'warning',
|
|
143
|
+
code: 'targets-beta',
|
|
144
|
+
title: 'Beta targets configured',
|
|
145
|
+
detail: `These targets are generated but less validated: ${betaTargets.join(', ')}`,
|
|
146
|
+
fix: 'Prefer claude-code, cursor, codex, and opencode for prime-time support.',
|
|
147
|
+
path: 'pluxx.config.ts',
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkMcpServer(checks: DoctorCheck[], serverName: string, server: McpServer): void {
|
|
152
|
+
const basePath = 'pluxx.config.ts'
|
|
153
|
+
|
|
154
|
+
if (server.transport === 'stdio') {
|
|
155
|
+
if (!server.command.trim()) {
|
|
156
|
+
addCheck(checks, {
|
|
157
|
+
level: 'error',
|
|
158
|
+
code: 'mcp-stdio-command-empty',
|
|
159
|
+
title: `MCP stdio command missing for ${serverName}`,
|
|
160
|
+
detail: `The stdio MCP server "${serverName}" does not define a command.`,
|
|
161
|
+
fix: `Set mcp.${serverName}.command to the executable used to start the server.`,
|
|
162
|
+
path: basePath,
|
|
163
|
+
})
|
|
164
|
+
} else {
|
|
165
|
+
addCheck(checks, {
|
|
166
|
+
level: 'success',
|
|
167
|
+
code: 'mcp-stdio-command',
|
|
168
|
+
title: `MCP stdio command configured for ${serverName}`,
|
|
169
|
+
detail: `The stdio MCP server "${serverName}" starts with "${server.command}".`,
|
|
170
|
+
fix: 'No action needed.',
|
|
171
|
+
path: basePath,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const key of Object.keys(server.env ?? {})) {
|
|
176
|
+
if (!ENV_VAR_NAME.test(key)) {
|
|
177
|
+
addCheck(checks, {
|
|
178
|
+
level: 'warning',
|
|
179
|
+
code: 'mcp-env-key-invalid',
|
|
180
|
+
title: `Invalid env var name for ${serverName}`,
|
|
181
|
+
detail: `The env key "${key}" is not a shell-safe environment variable name.`,
|
|
182
|
+
fix: `Rename ${key} to a shell-safe env var such as ${key.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}.`,
|
|
183
|
+
path: basePath,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
addCheck(checks, {
|
|
189
|
+
level: 'success',
|
|
190
|
+
code: 'mcp-remote-url',
|
|
191
|
+
title: `Remote MCP URL configured for ${serverName}`,
|
|
192
|
+
detail: `The ${server.transport.toUpperCase()} MCP server "${serverName}" points to ${server.url}.`,
|
|
193
|
+
fix: 'No action needed.',
|
|
194
|
+
path: basePath,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (server.auth?.type && server.auth.type !== 'none') {
|
|
199
|
+
if (server.auth.type === 'platform') {
|
|
200
|
+
addCheck(checks, {
|
|
201
|
+
level: 'info',
|
|
202
|
+
code: 'mcp-auth-platform',
|
|
203
|
+
title: `Platform-managed auth declared for ${serverName}`,
|
|
204
|
+
detail: `${serverName} expects native platform auth at runtime (${server.auth.mode}).`,
|
|
205
|
+
fix: 'Complete the platform auth flow in Claude Code or Cursor before calling authenticated tools.',
|
|
206
|
+
path: basePath,
|
|
207
|
+
})
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!ENV_VAR_NAME.test(server.auth.envVar)) {
|
|
212
|
+
addCheck(checks, {
|
|
213
|
+
level: 'error',
|
|
214
|
+
code: 'mcp-auth-env-invalid',
|
|
215
|
+
title: `Invalid auth env var for ${serverName}`,
|
|
216
|
+
detail: `Auth env var "${server.auth.envVar}" is not a valid shell environment variable name.`,
|
|
217
|
+
fix: `Rename the env var for ${serverName} to a shell-safe name like ${server.auth.envVar.replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}.`,
|
|
218
|
+
path: basePath,
|
|
219
|
+
})
|
|
220
|
+
} else {
|
|
221
|
+
addCheck(checks, {
|
|
222
|
+
level: 'info',
|
|
223
|
+
code: 'mcp-auth-env',
|
|
224
|
+
title: `Auth env var declared for ${serverName}`,
|
|
225
|
+
detail: `${serverName} expects ${server.auth.envVar} to be provided at runtime.`,
|
|
226
|
+
fix: `Export ${server.auth.envVar} before linting, building, or installing the generated plugin.`,
|
|
227
|
+
path: basePath,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function checkMcpConfig(checks: DoctorCheck[], config: PluginConfig): void {
|
|
234
|
+
const servers = Object.entries(config.mcp ?? {})
|
|
235
|
+
if (servers.length === 0) {
|
|
236
|
+
addCheck(checks, {
|
|
237
|
+
level: 'info',
|
|
238
|
+
code: 'mcp-none-configured',
|
|
239
|
+
title: 'No MCP servers configured',
|
|
240
|
+
detail: 'This plugin does not currently define any MCP servers.',
|
|
241
|
+
fix: 'No action needed unless this plugin should wrap an MCP server.',
|
|
242
|
+
path: 'pluxx.config.ts',
|
|
243
|
+
})
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [serverName, server] of servers) {
|
|
248
|
+
checkMcpServer(checks, serverName, server)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function checkUserConfig(checks: DoctorCheck[], config: PluginConfig): void {
|
|
253
|
+
const entries = config.userConfig ?? []
|
|
254
|
+
if (entries.length === 0) {
|
|
255
|
+
addCheck(checks, {
|
|
256
|
+
level: 'info',
|
|
257
|
+
code: 'user-config-none',
|
|
258
|
+
title: 'No userConfig entries declared',
|
|
259
|
+
detail: 'This plugin does not currently declare install-time config entries.',
|
|
260
|
+
fix: 'No action needed unless this plugin needs install-time config or secret handling.',
|
|
261
|
+
path: 'pluxx.config.ts',
|
|
262
|
+
})
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const invalidEnvEntries = entries.filter((entry) => entry.envVar && !ENV_VAR_NAME.test(entry.envVar))
|
|
267
|
+
if (invalidEnvEntries.length > 0) {
|
|
268
|
+
addCheck(checks, {
|
|
269
|
+
level: 'warning',
|
|
270
|
+
code: 'user-config-env-invalid',
|
|
271
|
+
title: 'Invalid userConfig env var name',
|
|
272
|
+
detail: `${invalidEnvEntries.length} userConfig entr${invalidEnvEntries.length === 1 ? 'y' : 'ies'} use invalid env var names.`,
|
|
273
|
+
fix: 'Rename the env var to a shell-safe name and keep userConfig aligned with the install/runtime config contract.',
|
|
274
|
+
path: 'pluxx.config.ts',
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const secretEntriesWithoutEnv = entries.filter((entry) => entry.type === 'secret' && !entry.envVar)
|
|
279
|
+
if (secretEntriesWithoutEnv.length > 0) {
|
|
280
|
+
addCheck(checks, {
|
|
281
|
+
level: 'warning',
|
|
282
|
+
code: 'user-config-secret-missing-env',
|
|
283
|
+
title: 'Secret userConfig entries should declare envVar',
|
|
284
|
+
detail: `${secretEntriesWithoutEnv.length} secret userConfig entr${secretEntriesWithoutEnv.length === 1 ? 'y' : 'ies'} are missing envVar bindings.`,
|
|
285
|
+
fix: 'Bind secret entries to env vars so Pluxx can persist and validate install-time config safely.',
|
|
286
|
+
path: 'pluxx.config.ts',
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
addCheck(checks, {
|
|
291
|
+
level: 'info',
|
|
292
|
+
code: 'user-config-declared',
|
|
293
|
+
title: 'userConfig entries declared',
|
|
294
|
+
detail: `${entries.length} install-time config entr${entries.length === 1 ? 'y' : 'ies'} are declared.`,
|
|
295
|
+
fix: 'No action needed.',
|
|
296
|
+
path: 'pluxx.config.ts',
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function checkHookTrust(checks: DoctorCheck[], config: PluginConfig): void {
|
|
301
|
+
const commands = listHookCommands(config.hooks)
|
|
302
|
+
if (commands.length === 0) {
|
|
303
|
+
addCheck(checks, {
|
|
304
|
+
level: 'success',
|
|
305
|
+
code: 'hooks-no-commands',
|
|
306
|
+
title: 'No command hooks configured',
|
|
307
|
+
detail: 'This plugin does not define hook commands that execute shell code locally.',
|
|
308
|
+
fix: 'No action needed.',
|
|
309
|
+
path: 'pluxx.config.ts',
|
|
310
|
+
})
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
addCheck(checks, {
|
|
315
|
+
level: 'warning',
|
|
316
|
+
code: 'hooks-trust-required',
|
|
317
|
+
title: 'Hook commands require install trust',
|
|
318
|
+
detail: `This plugin defines local hook commands: ${commands.map((command) => `${command.event} -> ${command.command}`).join('; ')}`,
|
|
319
|
+
fix: 'Review the commands carefully. Users will need to opt in with pluxx install --trust.',
|
|
320
|
+
path: 'pluxx.config.ts',
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isSafeManagedPath(path: string): boolean {
|
|
325
|
+
return path !== '' && !path.startsWith('/') && !path.includes('..')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatSampleNames(values: string[]): string {
|
|
329
|
+
if (values.length === 0) return 'none'
|
|
330
|
+
const sample = values.slice(0, 3)
|
|
331
|
+
return values.length > sample.length
|
|
332
|
+
? `${sample.join(', ')} (+${values.length - sample.length} more)`
|
|
333
|
+
: sample.join(', ')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function normalizeMetadataText(value: string): string {
|
|
337
|
+
return value
|
|
338
|
+
.trim()
|
|
339
|
+
.toLowerCase()
|
|
340
|
+
.replace(/\s+/g, ' ')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isLowInfoDescription(description: string): boolean {
|
|
344
|
+
const normalizedDescription = normalizeMetadataText(description)
|
|
345
|
+
return LOW_INFO_DESCRIPTION_PATTERNS.some((pattern) => pattern.test(normalizedDescription))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function checkMcpMetadataQuality(checks: DoctorCheck[], metadata: McpScaffoldMetadata): void {
|
|
349
|
+
const tools = metadata.tools ?? []
|
|
350
|
+
if (tools.length === 0) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const missingDescription = tools
|
|
355
|
+
.filter((tool) => !tool.description || tool.description.trim() === '')
|
|
356
|
+
.map((tool) => tool.name)
|
|
357
|
+
const lowInfoDescription = tools
|
|
358
|
+
.filter((tool) => {
|
|
359
|
+
const description = tool.description?.trim()
|
|
360
|
+
if (!description) return false
|
|
361
|
+
return isLowInfoDescription(description)
|
|
362
|
+
})
|
|
363
|
+
.map((tool) => tool.name)
|
|
364
|
+
const genericNames = tools
|
|
365
|
+
.filter((tool) => GENERIC_TOOL_NAME_PATTERNS.some((pattern) => pattern.test(tool.name.trim())))
|
|
366
|
+
.map((tool) => tool.name)
|
|
367
|
+
|
|
368
|
+
const findings: string[] = []
|
|
369
|
+
if (missingDescription.length > 0) {
|
|
370
|
+
findings.push(`missing descriptions: ${formatSampleNames(missingDescription)}`)
|
|
371
|
+
}
|
|
372
|
+
if (lowInfoDescription.length > 0) {
|
|
373
|
+
findings.push(`low-information descriptions: ${formatSampleNames(lowInfoDescription)}`)
|
|
374
|
+
}
|
|
375
|
+
if (genericNames.length > 0) {
|
|
376
|
+
findings.push(`generic tool names: ${formatSampleNames(genericNames)}`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (findings.length === 0) {
|
|
380
|
+
addCheck(checks, {
|
|
381
|
+
level: 'success',
|
|
382
|
+
code: 'mcp-metadata-quality-ok',
|
|
383
|
+
title: 'MCP metadata quality looks strong',
|
|
384
|
+
detail: `Tool metadata quality checks passed for ${tools.length} tool(s).`,
|
|
385
|
+
fix: 'No action needed.',
|
|
386
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
387
|
+
})
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
addCheck(checks, {
|
|
392
|
+
level: 'warning',
|
|
393
|
+
code: 'mcp-metadata-quality-weak',
|
|
394
|
+
title: 'MCP metadata quality is weak in scaffold source',
|
|
395
|
+
detail: `Weak metadata signals detected across ${tools.length} tool(s): ${findings.join('; ')}`,
|
|
396
|
+
fix: 'Before publishing, run `pluxx agent run review` and refine generated sections with concrete tool descriptions and product-shaped naming.',
|
|
397
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function checkScaffoldMetadata(checks: DoctorCheck[], rootDir: string, config: PluginConfig): void {
|
|
402
|
+
const metadataPath = resolve(rootDir, MCP_SCAFFOLD_METADATA_PATH)
|
|
403
|
+
if (!existsSync(metadataPath)) {
|
|
404
|
+
addCheck(checks, {
|
|
405
|
+
level: 'info',
|
|
406
|
+
code: 'mcp-metadata-missing',
|
|
407
|
+
title: 'No MCP scaffold metadata found',
|
|
408
|
+
detail: `No ${MCP_SCAFFOLD_METADATA_PATH} file was found in this project.`,
|
|
409
|
+
fix: 'No action needed unless this project was created with pluxx init --from-mcp.',
|
|
410
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
411
|
+
})
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) as McpScaffoldMetadata
|
|
417
|
+
const invalidManaged = metadata.managedFiles.filter((path) => !isSafeManagedPath(path))
|
|
418
|
+
if (metadata.version !== 1) {
|
|
419
|
+
addCheck(checks, {
|
|
420
|
+
level: 'warning',
|
|
421
|
+
code: 'mcp-metadata-version',
|
|
422
|
+
title: 'Unexpected MCP scaffold metadata version',
|
|
423
|
+
detail: `Found scaffold metadata version ${String(metadata.version)}.`,
|
|
424
|
+
fix: 'Re-run pluxx sync --from-mcp to refresh the scaffold metadata.',
|
|
425
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
426
|
+
})
|
|
427
|
+
} else if (invalidManaged.length > 0) {
|
|
428
|
+
addCheck(checks, {
|
|
429
|
+
level: 'error',
|
|
430
|
+
code: 'mcp-metadata-managed-paths',
|
|
431
|
+
title: 'Unsafe managed file paths in MCP metadata',
|
|
432
|
+
detail: `Managed file list contains unsafe relative paths: ${invalidManaged.join(', ')}`,
|
|
433
|
+
fix: 'Re-run pluxx init --from-mcp or pluxx sync --from-mcp to regenerate the metadata.',
|
|
434
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
435
|
+
})
|
|
436
|
+
} else {
|
|
437
|
+
addCheck(checks, {
|
|
438
|
+
level: 'success',
|
|
439
|
+
code: 'mcp-metadata-valid',
|
|
440
|
+
title: 'MCP scaffold metadata parsed successfully',
|
|
441
|
+
detail: `Managed scaffold metadata is present with ${metadata.managedFiles.length} managed files.`,
|
|
442
|
+
fix: 'No action needed.',
|
|
443
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (metadata.settings.pluginName !== config.name) {
|
|
448
|
+
addCheck(checks, {
|
|
449
|
+
level: 'warning',
|
|
450
|
+
code: 'mcp-metadata-plugin-mismatch',
|
|
451
|
+
title: 'MCP scaffold metadata name mismatch',
|
|
452
|
+
detail: `Metadata was generated for "${metadata.settings.pluginName}" but the config name is "${config.name}".`,
|
|
453
|
+
fix: 'If this plugin was renamed, run pluxx sync --from-mcp to refresh generated metadata.',
|
|
454
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
checkMcpMetadataQuality(checks, metadata)
|
|
459
|
+
} catch (error) {
|
|
460
|
+
addCheck(checks, {
|
|
461
|
+
level: 'error',
|
|
462
|
+
code: 'mcp-metadata-invalid',
|
|
463
|
+
title: 'MCP scaffold metadata is not parseable',
|
|
464
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
465
|
+
fix: `Repair or delete ${MCP_SCAFFOLD_METADATA_PATH}, then re-run pluxx init --from-mcp or pluxx sync --from-mcp.`,
|
|
466
|
+
path: MCP_SCAFFOLD_METADATA_PATH,
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function doctorProject(rootDir: string = process.cwd()): Promise<DoctorReport> {
|
|
472
|
+
const checks: DoctorCheck[] = []
|
|
473
|
+
const bunVersion = process.versions.bun
|
|
474
|
+
const bunMajor = parseMajorVersion(bunVersion)
|
|
475
|
+
const configPath = CONFIG_FILES.find((filename) => existsSync(resolve(rootDir, filename)))
|
|
476
|
+
|
|
477
|
+
if (!bunVersion) {
|
|
478
|
+
addCheck(checks, {
|
|
479
|
+
level: 'error',
|
|
480
|
+
code: 'bun-missing',
|
|
481
|
+
title: 'Bun runtime not detected',
|
|
482
|
+
detail: 'pluxx currently requires Bun at runtime.',
|
|
483
|
+
fix: 'Install Bun from https://bun.sh and rerun pluxx doctor.',
|
|
484
|
+
})
|
|
485
|
+
} else if (bunMajor === null || bunMajor < 1) {
|
|
486
|
+
addCheck(checks, {
|
|
487
|
+
level: 'error',
|
|
488
|
+
code: 'bun-version-unsupported',
|
|
489
|
+
title: 'Unsupported Bun version',
|
|
490
|
+
detail: `Detected Bun ${bunVersion}. pluxx requires Bun >= 1.0.`,
|
|
491
|
+
fix: 'Upgrade Bun to a supported version and rerun pluxx doctor.',
|
|
492
|
+
})
|
|
493
|
+
} else {
|
|
494
|
+
addCheck(checks, {
|
|
495
|
+
level: 'success',
|
|
496
|
+
code: 'bun-version',
|
|
497
|
+
title: 'Supported Bun runtime detected',
|
|
498
|
+
detail: `Bun ${bunVersion} is available.`,
|
|
499
|
+
fix: 'No action needed.',
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!configPath) {
|
|
504
|
+
addCheck(checks, {
|
|
505
|
+
level: 'error',
|
|
506
|
+
code: 'config-not-found',
|
|
507
|
+
title: 'pluxx config not found',
|
|
508
|
+
detail: `Expected one of ${CONFIG_FILES.join(', ')} in ${rootDir}.`,
|
|
509
|
+
fix: 'Create a pluxx.config.ts, pluxx.config.js, or pluxx.config.json file in the project root.',
|
|
510
|
+
})
|
|
511
|
+
return summarizeChecks(checks)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
addCheck(checks, {
|
|
515
|
+
level: 'success',
|
|
516
|
+
code: 'config-found',
|
|
517
|
+
title: 'pluxx config found',
|
|
518
|
+
detail: `Detected ${configPath} in the project root.`,
|
|
519
|
+
fix: 'No action needed.',
|
|
520
|
+
path: configPath,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
let config: PluginConfig
|
|
524
|
+
try {
|
|
525
|
+
config = await loadConfig(rootDir)
|
|
526
|
+
addCheck(checks, {
|
|
527
|
+
level: 'success',
|
|
528
|
+
code: 'config-valid',
|
|
529
|
+
title: 'Config parsed successfully',
|
|
530
|
+
detail: `Loaded ${config.name}@${config.version} for ${config.targets.length} target(s).`,
|
|
531
|
+
fix: 'No action needed.',
|
|
532
|
+
path: configPath,
|
|
533
|
+
})
|
|
534
|
+
} catch (error) {
|
|
535
|
+
addCheck(checks, {
|
|
536
|
+
level: 'error',
|
|
537
|
+
code: 'config-invalid',
|
|
538
|
+
title: 'Config could not be loaded',
|
|
539
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
540
|
+
fix: 'Fix the config error and rerun pluxx doctor.',
|
|
541
|
+
path: configPath,
|
|
542
|
+
})
|
|
543
|
+
return summarizeChecks(checks)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
checkReadablePath(checks, rootDir, 'Skills', config.skills, true)
|
|
547
|
+
checkReadablePath(checks, rootDir, 'Instructions', config.instructions, false)
|
|
548
|
+
checkReadablePath(checks, rootDir, 'Agents', config.agents, false)
|
|
549
|
+
checkReadablePath(checks, rootDir, 'Commands', config.commands, false)
|
|
550
|
+
checkReadablePath(checks, rootDir, 'Scripts', config.scripts, false)
|
|
551
|
+
checkReadablePath(checks, rootDir, 'Assets', config.assets, false)
|
|
552
|
+
checkTargetPlatforms(checks, config)
|
|
553
|
+
checkMcpConfig(checks, config)
|
|
554
|
+
checkUserConfig(checks, config)
|
|
555
|
+
checkScaffoldMetadata(checks, rootDir, config)
|
|
556
|
+
checkHookTrust(checks, config)
|
|
557
|
+
|
|
558
|
+
for (const target of config.targets) {
|
|
559
|
+
const limits = PLATFORM_LIMITS[target]
|
|
560
|
+
if (!CORE_FOUR.has(target) && limits) {
|
|
561
|
+
addCheck(checks, {
|
|
562
|
+
level: 'info',
|
|
563
|
+
code: 'target-caveat',
|
|
564
|
+
title: `Platform caveat for ${target}`,
|
|
565
|
+
detail: `${target} is supported, but it is currently less validated than the core four targets.`,
|
|
566
|
+
fix: 'Use pluxx lint and pluxx test before relying on beta targets in production.',
|
|
567
|
+
path: 'pluxx.config.ts',
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return summarizeChecks(checks)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function printDoctorReport(report: DoctorReport): void {
|
|
576
|
+
for (const check of report.checks) {
|
|
577
|
+
const prefix = check.level.toUpperCase().padEnd(7, ' ')
|
|
578
|
+
const pathLabel = check.path ? ` [${check.path}]` : ''
|
|
579
|
+
console.log(`${prefix} ${check.code}${pathLabel} ${check.title}`)
|
|
580
|
+
console.log(` ${check.detail}`)
|
|
581
|
+
console.log(` Fix: ${check.fix}`)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
console.log('')
|
|
585
|
+
console.log(
|
|
586
|
+
`Doctor summary: ${report.errors} error(s), ${report.warnings} warning(s), ${report.infos} info message(s)`,
|
|
587
|
+
)
|
|
588
|
+
}
|