@orchid-labs/pluxx 0.1.0 → 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.
Files changed (110) hide show
  1. package/README.md +110 -515
  2. package/bin/pluxx.js +19 -28
  3. package/dist/agents.d.ts +16 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/cli/agent.d.ts +69 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +3 -0
  8. package/dist/cli/doctor.d.ts.map +1 -1
  9. package/dist/cli/entry.d.ts +2 -0
  10. package/dist/cli/entry.d.ts.map +1 -0
  11. package/dist/cli/eval.d.ts +22 -0
  12. package/dist/cli/eval.d.ts.map +1 -0
  13. package/dist/cli/index.d.ts +26 -3
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +21810 -0
  16. package/dist/cli/init-from-mcp.d.ts +34 -3
  17. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  18. package/dist/cli/install.d.ts +3 -0
  19. package/dist/cli/install.d.ts.map +1 -1
  20. package/dist/cli/lint.d.ts +7 -1
  21. package/dist/cli/lint.d.ts.map +1 -1
  22. package/dist/cli/mcp-proxy.d.ts +10 -0
  23. package/dist/cli/mcp-proxy.d.ts.map +1 -0
  24. package/dist/cli/migrate.d.ts.map +1 -1
  25. package/dist/cli/primitive-summary.d.ts +14 -0
  26. package/dist/cli/primitive-summary.d.ts.map +1 -0
  27. package/dist/cli/prompt.d.ts +1 -1
  28. package/dist/cli/publish.d.ts +6 -1
  29. package/dist/cli/publish.d.ts.map +1 -1
  30. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  31. package/dist/cli/test.d.ts +2 -0
  32. package/dist/cli/test.d.ts.map +1 -1
  33. package/dist/cli/verify-install.d.ts +25 -0
  34. package/dist/cli/verify-install.d.ts.map +1 -0
  35. package/dist/commands.d.ts +10 -0
  36. package/dist/commands.d.ts.map +1 -0
  37. package/dist/compiler-intent.d.ts +165 -0
  38. package/dist/compiler-intent.d.ts.map +1 -0
  39. package/dist/config/load.d.ts.map +1 -1
  40. package/dist/delegation.d.ts +11 -0
  41. package/dist/delegation.d.ts.map +1 -0
  42. package/dist/generators/amp/index.d.ts.map +1 -1
  43. package/dist/generators/base.d.ts +5 -0
  44. package/dist/generators/base.d.ts.map +1 -1
  45. package/dist/generators/claude-code/index.d.ts +2 -0
  46. package/dist/generators/claude-code/index.d.ts.map +1 -1
  47. package/dist/generators/cline/index.d.ts.map +1 -1
  48. package/dist/generators/codex/index.d.ts +5 -0
  49. package/dist/generators/codex/index.d.ts.map +1 -1
  50. package/dist/generators/cursor/index.d.ts +1 -0
  51. package/dist/generators/cursor/index.d.ts.map +1 -1
  52. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  53. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  54. package/dist/generators/opencode/index.d.ts +1 -0
  55. package/dist/generators/opencode/index.d.ts.map +1 -1
  56. package/dist/generators/openhands/index.d.ts.map +1 -1
  57. package/dist/generators/roo-code/index.d.ts.map +1 -1
  58. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  59. package/dist/generators/warp/index.d.ts.map +1 -1
  60. package/dist/index.d.ts +4 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +5464 -548
  63. package/dist/mcp/introspect.d.ts +43 -1
  64. package/dist/mcp/introspect.d.ts.map +1 -1
  65. package/dist/permissions.d.ts.map +1 -1
  66. package/dist/schema.d.ts +91 -42
  67. package/dist/schema.d.ts.map +1 -1
  68. package/dist/text-files.d.ts +5 -0
  69. package/dist/text-files.d.ts.map +1 -0
  70. package/dist/validation/platform-rules.d.ts +35 -1
  71. package/dist/validation/platform-rules.d.ts.map +1 -1
  72. package/package.json +16 -14
  73. package/src/cli/agent.ts +0 -1030
  74. package/src/cli/dev.ts +0 -112
  75. package/src/cli/doctor.ts +0 -588
  76. package/src/cli/index.ts +0 -2414
  77. package/src/cli/init-from-mcp.ts +0 -1611
  78. package/src/cli/install.ts +0 -698
  79. package/src/cli/lint.ts +0 -1219
  80. package/src/cli/migrate.ts +0 -614
  81. package/src/cli/prompt.ts +0 -82
  82. package/src/cli/publish.ts +0 -401
  83. package/src/cli/runtime.ts +0 -86
  84. package/src/cli/sync-from-mcp.ts +0 -563
  85. package/src/cli/test.ts +0 -134
  86. package/src/compatibility/matrix.ts +0 -149
  87. package/src/config/define.ts +0 -20
  88. package/src/config/load.ts +0 -74
  89. package/src/generators/amp/index.ts +0 -63
  90. package/src/generators/base.ts +0 -188
  91. package/src/generators/claude-code/index.ts +0 -29
  92. package/src/generators/cline/index.ts +0 -35
  93. package/src/generators/codex/index.ts +0 -120
  94. package/src/generators/cursor/index.ts +0 -158
  95. package/src/generators/gemini-cli/index.ts +0 -83
  96. package/src/generators/github-copilot/index.ts +0 -32
  97. package/src/generators/hooks-warning.ts +0 -51
  98. package/src/generators/index.ts +0 -71
  99. package/src/generators/opencode/index.ts +0 -526
  100. package/src/generators/openhands/index.ts +0 -32
  101. package/src/generators/roo-code/index.ts +0 -35
  102. package/src/generators/shared/claude-family.ts +0 -215
  103. package/src/generators/warp/index.ts +0 -32
  104. package/src/hook-events.ts +0 -33
  105. package/src/index.ts +0 -23
  106. package/src/mcp/introspect.ts +0 -834
  107. package/src/permissions.ts +0 -258
  108. package/src/schema.ts +0 -312
  109. package/src/user-config.ts +0 -177
  110. package/src/validation/platform-rules.ts +0 -565
package/src/cli/dev.ts DELETED
@@ -1,112 +0,0 @@
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
- }
package/src/cli/doctor.ts DELETED
@@ -1,588 +0,0 @@
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
- }