@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.
Files changed (103) hide show
  1. package/README.md +25 -8
  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 +62 -0
  6. package/dist/cli/agent.d.ts.map +1 -1
  7. package/dist/cli/doctor.d.ts +2 -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/index.d.ts +7 -1
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +21810 -0
  14. package/dist/cli/init-from-mcp.d.ts +17 -1
  15. package/dist/cli/init-from-mcp.d.ts.map +1 -1
  16. package/dist/cli/install.d.ts +1 -0
  17. package/dist/cli/install.d.ts.map +1 -1
  18. package/dist/cli/lint.d.ts +3 -1
  19. package/dist/cli/lint.d.ts.map +1 -1
  20. package/dist/cli/mcp-proxy.d.ts.map +1 -1
  21. package/dist/cli/migrate.d.ts.map +1 -1
  22. package/dist/cli/primitive-summary.d.ts +14 -0
  23. package/dist/cli/primitive-summary.d.ts.map +1 -0
  24. package/dist/cli/prompt.d.ts +1 -1
  25. package/dist/cli/publish.d.ts +6 -1
  26. package/dist/cli/publish.d.ts.map +1 -1
  27. package/dist/cli/sync-from-mcp.d.ts.map +1 -1
  28. package/dist/cli/verify-install.d.ts +25 -0
  29. package/dist/cli/verify-install.d.ts.map +1 -0
  30. package/dist/commands.d.ts +10 -0
  31. package/dist/commands.d.ts.map +1 -0
  32. package/dist/compiler-intent.d.ts +165 -0
  33. package/dist/compiler-intent.d.ts.map +1 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/delegation.d.ts +11 -0
  36. package/dist/delegation.d.ts.map +1 -0
  37. package/dist/generators/amp/index.d.ts.map +1 -1
  38. package/dist/generators/base.d.ts +5 -0
  39. package/dist/generators/base.d.ts.map +1 -1
  40. package/dist/generators/claude-code/index.d.ts.map +1 -1
  41. package/dist/generators/cline/index.d.ts.map +1 -1
  42. package/dist/generators/codex/index.d.ts +4 -0
  43. package/dist/generators/codex/index.d.ts.map +1 -1
  44. package/dist/generators/cursor/index.d.ts +1 -0
  45. package/dist/generators/cursor/index.d.ts.map +1 -1
  46. package/dist/generators/gemini-cli/index.d.ts.map +1 -1
  47. package/dist/generators/github-copilot/index.d.ts.map +1 -1
  48. package/dist/generators/opencode/index.d.ts +1 -0
  49. package/dist/generators/opencode/index.d.ts.map +1 -1
  50. package/dist/generators/openhands/index.d.ts.map +1 -1
  51. package/dist/generators/roo-code/index.d.ts.map +1 -1
  52. package/dist/generators/shared/claude-family.d.ts.map +1 -1
  53. package/dist/generators/warp/index.d.ts.map +1 -1
  54. package/dist/index.d.ts +4 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +5371 -553
  57. package/dist/schema.d.ts +91 -42
  58. package/dist/schema.d.ts.map +1 -1
  59. package/dist/text-files.d.ts +5 -0
  60. package/dist/text-files.d.ts.map +1 -0
  61. package/dist/validation/platform-rules.d.ts +15 -1
  62. package/dist/validation/platform-rules.d.ts.map +1 -1
  63. package/package.json +15 -13
  64. package/src/cli/agent.ts +0 -1455
  65. package/src/cli/dev.ts +0 -112
  66. package/src/cli/doctor.ts +0 -987
  67. package/src/cli/eval.ts +0 -470
  68. package/src/cli/index.ts +0 -2933
  69. package/src/cli/init-from-mcp.ts +0 -2115
  70. package/src/cli/install.ts +0 -860
  71. package/src/cli/lint.ts +0 -1249
  72. package/src/cli/mcp-proxy.ts +0 -322
  73. package/src/cli/migrate.ts +0 -867
  74. package/src/cli/prompt.ts +0 -82
  75. package/src/cli/publish.ts +0 -401
  76. package/src/cli/runtime.ts +0 -86
  77. package/src/cli/sync-from-mcp.ts +0 -586
  78. package/src/cli/test.ts +0 -142
  79. package/src/compatibility/matrix.ts +0 -149
  80. package/src/config/define.ts +0 -20
  81. package/src/config/load.ts +0 -74
  82. package/src/generators/amp/index.ts +0 -63
  83. package/src/generators/base.ts +0 -188
  84. package/src/generators/claude-code/index.ts +0 -172
  85. package/src/generators/cline/index.ts +0 -35
  86. package/src/generators/codex/index.ts +0 -143
  87. package/src/generators/cursor/index.ts +0 -158
  88. package/src/generators/gemini-cli/index.ts +0 -83
  89. package/src/generators/github-copilot/index.ts +0 -32
  90. package/src/generators/hooks-warning.ts +0 -51
  91. package/src/generators/index.ts +0 -71
  92. package/src/generators/opencode/index.ts +0 -526
  93. package/src/generators/openhands/index.ts +0 -32
  94. package/src/generators/roo-code/index.ts +0 -35
  95. package/src/generators/shared/claude-family.ts +0 -215
  96. package/src/generators/warp/index.ts +0 -32
  97. package/src/hook-events.ts +0 -33
  98. package/src/index.ts +0 -34
  99. package/src/mcp/introspect.ts +0 -1107
  100. package/src/permissions.ts +0 -260
  101. package/src/schema.ts +0 -312
  102. package/src/user-config.ts +0 -177
  103. package/src/validation/platform-rules.ts +0 -686
package/src/cli/test.ts DELETED
@@ -1,142 +0,0 @@
1
- import { existsSync } from 'fs'
2
- import { resolve } from 'path'
3
- import { loadConfig } from '../config/load'
4
- import { build } from '../generators'
5
- import { lintProject, type LintResult } from './lint'
6
- import { runEvalSuite, type EvalReport } from './eval'
7
- import type { TargetPlatform } from '../schema'
8
-
9
- export interface TestRunOptions {
10
- rootDir?: string
11
- targets?: TargetPlatform[]
12
- }
13
-
14
- export interface TestSmokeCheck {
15
- platform: TargetPlatform
16
- requiredPath: string
17
- ok: boolean
18
- }
19
-
20
- export interface TestRunResult {
21
- ok: boolean
22
- config: {
23
- ok: boolean
24
- name?: string
25
- version?: string
26
- error?: string
27
- }
28
- lint?: LintResult
29
- eval?: EvalReport
30
- build?: {
31
- ok: boolean
32
- outDir: string
33
- targets: TargetPlatform[]
34
- }
35
- smoke?: {
36
- ok: boolean
37
- checks: TestSmokeCheck[]
38
- }
39
- }
40
-
41
- const SMOKE_PATHS: Record<TargetPlatform, string> = {
42
- 'claude-code': '.claude-plugin/plugin.json',
43
- cursor: '.cursor-plugin/plugin.json',
44
- codex: '.codex-plugin/plugin.json',
45
- opencode: 'package.json',
46
- 'github-copilot': '.claude-plugin/plugin.json',
47
- openhands: '.plugin/plugin.json',
48
- warp: 'AGENTS.md',
49
- 'gemini-cli': 'gemini-extension.json',
50
- 'roo-code': '.roo/mcp.json',
51
- cline: '.cline/mcp.json',
52
- amp: '.amp/settings.json',
53
- }
54
-
55
- export async function runTestSuite(options: TestRunOptions = {}): Promise<TestRunResult> {
56
- const rootDir = options.rootDir ?? process.cwd()
57
-
58
- try {
59
- const config = await loadConfig(rootDir)
60
- const targets = options.targets ?? config.targets
61
- const lint = await lintProject(rootDir, { targets })
62
-
63
- if (lint.errors > 0) {
64
- return {
65
- ok: false,
66
- config: {
67
- ok: true,
68
- name: config.name,
69
- version: config.version,
70
- },
71
- lint,
72
- }
73
- }
74
-
75
- await build(config, rootDir, { targets })
76
- const evalReport = await runEvalSuite({ rootDir })
77
-
78
- const checks = targets.map((platform) => {
79
- const requiredPath = SMOKE_PATHS[platform]
80
- const ok = existsSync(resolve(rootDir, config.outDir, platform, requiredPath))
81
- return { platform, requiredPath, ok }
82
- })
83
-
84
- return {
85
- ok: checks.every((check) => check.ok) && evalReport.errors === 0,
86
- config: {
87
- ok: true,
88
- name: config.name,
89
- version: config.version,
90
- },
91
- lint,
92
- eval: evalReport,
93
- build: {
94
- ok: true,
95
- outDir: config.outDir,
96
- targets,
97
- },
98
- smoke: {
99
- ok: checks.every((check) => check.ok),
100
- checks,
101
- },
102
- }
103
- } catch (error) {
104
- return {
105
- ok: false,
106
- config: {
107
- ok: false,
108
- error: error instanceof Error ? error.message : String(error),
109
- },
110
- }
111
- }
112
- }
113
-
114
- export function printTestResult(result: TestRunResult): void {
115
- if (!result.config.ok) {
116
- console.error(`Config: ${result.config.error}`)
117
- return
118
- }
119
-
120
- console.log(`Config: ${result.config.name}@${result.config.version}`)
121
-
122
- if (result.lint) {
123
- console.log(`Lint: ${result.lint.errors} error(s), ${result.lint.warnings} warning(s)`)
124
- }
125
-
126
- if (result.eval) {
127
- console.log(`Eval: ${result.eval.errors} error(s), ${result.eval.warnings} warning(s), ${result.eval.infos} info message(s)`)
128
- }
129
-
130
- if (result.build) {
131
- console.log(`Build: ${result.build.targets.join(', ')} -> ${result.build.outDir}`)
132
- }
133
-
134
- if (result.smoke) {
135
- for (const check of result.smoke.checks) {
136
- const prefix = check.ok ? 'PASS' : 'FAIL'
137
- console.log(`${prefix} ${check.platform}: ${check.requiredPath}`)
138
- }
139
- }
140
-
141
- console.log(result.ok ? 'pluxx test passed.' : 'pluxx test failed.')
142
- }
@@ -1,149 +0,0 @@
1
- import type { TargetPlatform } from '../schema'
2
- import { PLATFORM_VALIDATION_RULES, type PlatformRules } from '../validation/platform-rules'
3
-
4
- export interface PlatformCompatibilityRow {
5
- platform: TargetPlatform
6
- status: 'Primary' | 'Beta'
7
- docsBasis: string
8
- manifest: string
9
- hooks: string
10
- mcp: string
11
- instructions: string
12
- verification: string
13
- }
14
-
15
- const ALL_TARGET_PLATFORMS: TargetPlatform[] = [
16
- 'claude-code',
17
- 'cursor',
18
- 'codex',
19
- 'opencode',
20
- 'github-copilot',
21
- 'openhands',
22
- 'warp',
23
- 'gemini-cli',
24
- 'roo-code',
25
- 'cline',
26
- 'amp',
27
- ]
28
-
29
- const PRIMARY_TARGETS = new Set<TargetPlatform>(['claude-code', 'cursor', 'codex', 'opencode'])
30
- const RELEASE_SMOKE_TARGETS = new Set<TargetPlatform>(['claude-code', 'cursor', 'codex', 'opencode'])
31
- const GENERATOR_FIXTURE_TARGETS = new Set<TargetPlatform>(ALL_TARGET_PLATFORMS)
32
-
33
- const INHERITED_RULES: Partial<Record<TargetPlatform, keyof typeof PLATFORM_VALIDATION_RULES>> = {
34
- 'github-copilot': 'claude-code',
35
- }
36
-
37
- function getRuleEntry(platform: TargetPlatform): {
38
- rules: PlatformRules
39
- docsBasis: string
40
- } {
41
- const directRule = PLATFORM_VALIDATION_RULES[platform as keyof typeof PLATFORM_VALIDATION_RULES]
42
- if (directRule) {
43
- return {
44
- rules: directRule,
45
- docsBasis: 'Official docs audited',
46
- }
47
- }
48
-
49
- const inheritedPlatform = INHERITED_RULES[platform]
50
- if (!inheritedPlatform) {
51
- throw new Error(`No platform rules available for ${platform}`)
52
- }
53
-
54
- return {
55
- rules: PLATFORM_VALIDATION_RULES[inheritedPlatform],
56
- docsBasis: `Inherits ${humanizePlatformName(inheritedPlatform)} packaging`,
57
- }
58
- }
59
-
60
- const PLATFORM_LABELS: Record<TargetPlatform, string> = {
61
- 'claude-code': 'Claude Code',
62
- cursor: 'Cursor',
63
- codex: 'Codex',
64
- opencode: 'OpenCode',
65
- 'github-copilot': 'GitHub Copilot',
66
- openhands: 'OpenHands',
67
- warp: 'Warp',
68
- 'gemini-cli': 'Gemini CLI',
69
- 'roo-code': 'Roo Code',
70
- cline: 'Cline',
71
- amp: 'AMP',
72
- }
73
-
74
- function humanizePlatformName(platform: TargetPlatform): string {
75
- return PLATFORM_LABELS[platform]
76
- }
77
-
78
- function formatFiles(files: string[], emptyLabel: string): string {
79
- return files.length > 0 ? files.join(', ') : emptyLabel
80
- }
81
-
82
- function getVerificationLabel(platform: TargetPlatform): string {
83
- const labels: string[] = []
84
-
85
- if (RELEASE_SMOKE_TARGETS.has(platform)) {
86
- labels.push('Release smoke')
87
- }
88
-
89
- if (GENERATOR_FIXTURE_TARGETS.has(platform)) {
90
- labels.push('Generator fixture')
91
- }
92
-
93
- return labels.join(' + ')
94
- }
95
-
96
- export function getPlatformCompatibilityMatrix(): PlatformCompatibilityRow[] {
97
- return ALL_TARGET_PLATFORMS.map((platform) => {
98
- const { rules, docsBasis } = getRuleEntry(platform)
99
-
100
- return {
101
- platform,
102
- status: PRIMARY_TARGETS.has(platform) ? 'Primary' : 'Beta',
103
- docsBasis,
104
- manifest: formatFiles(rules.manifest.files, 'No dedicated manifest'),
105
- hooks: rules.hooks.supported
106
- ? formatFiles(rules.hooks.files, 'Supported')
107
- : 'No documented standalone hook surface',
108
- mcp: formatFiles(rules.mcp.files, 'None documented'),
109
- instructions: formatFiles(rules.instructions.files, 'None documented'),
110
- verification: getVerificationLabel(platform),
111
- }
112
- })
113
- }
114
-
115
- export function renderCompatibilityMatrixMarkdown(): string {
116
- const rows = getPlatformCompatibilityMatrix()
117
-
118
- const lines = [
119
- '<!-- Generated by `bun scripts/generate-compatibility-matrix.ts`. Do not edit by hand. -->',
120
- '# Compatibility Matrix',
121
- '',
122
- 'This matrix is generated from the repo-owned platform rules and verification surface.',
123
- '',
124
- 'Verification labels:',
125
- '- `Release smoke`: exercised end to end by the real CLI against the real example plugins on the core four.',
126
- '- `Generator fixture`: covered by the repo build/test fixture layer.',
127
- '',
128
- '| Platform | Status | Docs Basis | Manifest | Hooks | MCP | Instructions | Verification |',
129
- '|----------|--------|------------|----------|-------|-----|--------------|--------------|',
130
- ]
131
-
132
- for (const row of rows) {
133
- lines.push(
134
- `| ${humanizePlatformName(row.platform)} | ${row.status} | ${row.docsBasis} | ${row.manifest} | ${row.hooks} | ${row.mcp} | ${row.instructions} | ${row.verification} |`,
135
- )
136
- }
137
-
138
- lines.push(
139
- '',
140
- '## Notes',
141
- '',
142
- '- The prime-time launch path remains Claude Code, Cursor, Codex, and OpenCode.',
143
- '- GitHub Copilot currently inherits the Claude Code packaging model in Pluxx rather than using its own separately audited plugin surface.',
144
- '- Beta platforms are generated and fixture-tested, but they do not yet have the same release-smoke coverage as the core four.',
145
- '',
146
- )
147
-
148
- return `${lines.join('\n')}\n`
149
- }
@@ -1,20 +0,0 @@
1
- import { type PluginConfig, PluginConfigSchema } from '../schema'
2
-
3
- /**
4
- * Define a plugin configuration with full type checking and validation.
5
- *
6
- * Usage in pluxx.config.ts:
7
- * ```ts
8
- * import { definePlugin } from 'pluxx'
9
- *
10
- * export default definePlugin({
11
- * name: 'my-plugin',
12
- * description: 'My awesome plugin',
13
- * author: { name: 'Me' },
14
- * // ...
15
- * })
16
- * ```
17
- */
18
- export function definePlugin(config: PluginConfig): PluginConfig {
19
- return PluginConfigSchema.parse(config)
20
- }
@@ -1,74 +0,0 @@
1
- import { resolve, extname, dirname } from 'path'
2
- import { existsSync } from 'fs'
3
- import { rm } from 'fs/promises'
4
- import { fileURLToPath, pathToFileURL } from 'url'
5
- import { PluginConfigSchema, type PluginConfig } from '../schema'
6
-
7
- export const CONFIG_FILES = [
8
- 'pluxx.config.ts',
9
- 'pluxx.config.js',
10
- 'pluxx.config.json',
11
- ]
12
-
13
- function getRuntimePackageEntry(): string {
14
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
15
- const distEntry = resolve(packageRoot, 'dist', 'index.js')
16
-
17
- if (existsSync(distEntry)) {
18
- return distEntry
19
- }
20
-
21
- return resolve(packageRoot, 'src', 'index.ts')
22
- }
23
-
24
- async function importConfigModule(filepath: string): Promise<unknown> {
25
- const ext = extname(filepath)
26
- const runtimeEntryUrl = pathToFileURL(getRuntimePackageEntry()).href
27
- const source = await Bun.file(filepath).text()
28
- const rewritten = source
29
- .replace(/from\s+(['"])pluxx\1/g, `from ${JSON.stringify(runtimeEntryUrl)}`)
30
- .replace(/import\s+(['"])pluxx\1/g, `import ${JSON.stringify(runtimeEntryUrl)}`)
31
-
32
- const transpiler = new Bun.Transpiler({
33
- loader: ext === '.ts' ? 'ts' : 'js',
34
- })
35
-
36
- const tempFile = resolve(
37
- dirname(filepath),
38
- `.pluxx-load-config-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`,
39
- )
40
- await Bun.write(tempFile, transpiler.transformSync(rewritten))
41
-
42
- try {
43
- const mod = await import(`${pathToFileURL(tempFile).href}?t=${Date.now()}`)
44
- return mod.default ?? mod
45
- } finally {
46
- await rm(tempFile, { force: true })
47
- }
48
- }
49
-
50
- /**
51
- * Load and validate a pluxx config from the given directory.
52
- */
53
- export async function loadConfig(dir: string = process.cwd()): Promise<PluginConfig> {
54
- for (const filename of CONFIG_FILES) {
55
- const filepath = resolve(dir, filename)
56
- if (!existsSync(filepath)) continue
57
-
58
- const ext = extname(filename)
59
-
60
- if (ext === '.ts' || ext === '.js') {
61
- const raw = await importConfigModule(filepath)
62
- return PluginConfigSchema.parse(raw)
63
- }
64
-
65
- if (ext === '.json') {
66
- const text = await Bun.file(filepath).text()
67
- return PluginConfigSchema.parse(JSON.parse(text))
68
- }
69
- }
70
-
71
- throw new Error(
72
- `No pluxx config found in ${dir}. Expected one of: ${CONFIG_FILES.join(', ')}`
73
- )
74
- }
@@ -1,63 +0,0 @@
1
- import { existsSync } from 'fs'
2
- import { Generator } from '../base'
3
- import { warnDroppedHookFields } from '../hooks-warning'
4
- import type { TargetPlatform } from '../../schema'
5
-
6
- /**
7
- * Amp uses AGENT.md for instructions, .amp/settings.json for hooks,
8
- * MCP config, and skills/ for skills. No plugin manifest.
9
- */
10
- export class AmpGenerator extends Generator {
11
- readonly platform: TargetPlatform = 'amp'
12
-
13
- async generate(): Promise<void> {
14
- await Promise.all([
15
- this.generateAgentMd(),
16
- this.generateSettings(),
17
- this.generateMcpConfig('mcp.json'),
18
- ])
19
-
20
- this.copySkills()
21
- this.copyScripts()
22
- }
23
-
24
- private async generateAgentMd(): Promise<void> {
25
- if (!this.config.instructions) return
26
- const srcPath = this.resolveConfigPath(this.config.instructions, 'instructions')
27
- if (!existsSync(srcPath)) return
28
-
29
- const content = await Bun.file(srcPath).text()
30
-
31
- const agentMd = [
32
- `# ${this.config.brand?.displayName ?? this.config.name}`,
33
- '',
34
- this.config.brand?.shortDescription ?? this.config.description,
35
- '',
36
- content,
37
- ].join('\n')
38
-
39
- await this.writeFile('AGENT.md', agentMd)
40
- }
41
-
42
- private async generateSettings(): Promise<void> {
43
- if (!this.config.hooks) return
44
-
45
- // Amp hooks use a post-execute format
46
- const hooks: Record<string, unknown[]> = {}
47
-
48
- for (const [event, entries] of Object.entries(this.config.hooks)) {
49
- if (!entries) continue
50
- warnDroppedHookFields(this.platform, event, entries)
51
- const commandEntries = entries.filter(entry => entry.type !== 'prompt' && entry.command)
52
- if (commandEntries.length === 0) continue
53
- hooks[event] = commandEntries.map(entry => ({
54
- type: 'post-execute',
55
- command: entry.command!.replace('${PLUGIN_ROOT}', '.'),
56
- ...(entry.timeout ? { timeout: entry.timeout } : {}),
57
- }))
58
- }
59
-
60
- await this.writeJson('.amp/settings.json', { hooks })
61
- }
62
-
63
- }
@@ -1,188 +0,0 @@
1
- import { resolve, join, relative } from 'path'
2
- import { mkdirSync, existsSync, cpSync } from 'fs'
3
- import type { PluginConfig, TargetPlatform, McpServer } from '../schema'
4
-
5
- type McpRemoteServer = Exclude<McpServer, { transport: 'stdio' }>
6
-
7
- interface McpConfigOptions {
8
- includeDefaultAuthHeaders?: boolean
9
- transformRemoteEntry?: (context: {
10
- name: string
11
- server: McpRemoteServer
12
- entry: Record<string, unknown>
13
- }) => Record<string, unknown>
14
- }
15
-
16
- export abstract class Generator {
17
- abstract readonly platform: TargetPlatform
18
-
19
- constructor(
20
- protected config: PluginConfig,
21
- protected rootDir: string,
22
- ) {}
23
-
24
- /** The output directory for this platform */
25
- get outDir(): string {
26
- return resolve(this.rootDir, this.config.outDir, this.platform)
27
- }
28
-
29
- /** Generate all platform-specific files */
30
- abstract generate(): Promise<void>
31
-
32
- /** Write a file to the output directory */
33
- protected async writeFile(relativePath: string, content: string): Promise<void> {
34
- const filepath = join(this.outDir, relativePath)
35
- const dir = filepath.substring(0, filepath.lastIndexOf('/'))
36
- mkdirSync(dir, { recursive: true })
37
- await Bun.write(filepath, content)
38
- }
39
-
40
- /** Write JSON to the output directory */
41
- protected async writeJson(relativePath: string, data: unknown): Promise<void> {
42
- await this.writeFile(relativePath, JSON.stringify(data, null, 2) + '\n')
43
- }
44
-
45
- /** Copy a directory from source to output */
46
- protected copyDir(srcRelative: string, destRelative: string, configKey: string): void {
47
- const src = this.resolveConfigPath(srcRelative, configKey)
48
- if (!existsSync(src)) return
49
- const dest = join(this.outDir, destRelative)
50
- mkdirSync(dest, { recursive: true })
51
- cpSync(src, dest, { recursive: true })
52
- }
53
-
54
- /** Resolve a user-configured path and ensure it stays within the project root. */
55
- protected resolveConfigPath(configPath: string, configKey: string): string {
56
- const resolvedPath = resolve(this.rootDir, configPath)
57
- const rel = relative(this.rootDir, resolvedPath)
58
- if (rel.startsWith('..')) {
59
- throw new Error(
60
- `${configKey} path "${configPath}" resolves outside the project root.`
61
- )
62
- }
63
- return resolvedPath
64
- }
65
-
66
- /** Copy skills directory, applying any platform-specific frontmatter */
67
- protected copySkills(): void {
68
- this.copyDir(this.config.skills, 'skills/', 'skills')
69
- }
70
-
71
- /** Copy commands directory if it exists */
72
- protected copyCommands(): void {
73
- if (this.config.commands) {
74
- this.copyDir(this.config.commands, 'commands/', 'commands')
75
- }
76
- }
77
-
78
- /** Copy agents directory if it exists */
79
- protected copyAgents(): void {
80
- if (this.config.agents) {
81
- this.copyDir(this.config.agents, 'agents/', 'agents')
82
- }
83
- }
84
-
85
- /** Copy scripts directory if it exists */
86
- protected copyScripts(): void {
87
- if (this.config.scripts) {
88
- this.copyDir(this.config.scripts, 'scripts/', 'scripts')
89
- }
90
- }
91
-
92
- /** Copy assets directory if it exists */
93
- protected copyAssets(): void {
94
- if (this.config.assets) {
95
- this.copyDir(this.config.assets, 'assets/', 'assets')
96
- }
97
- }
98
-
99
- /** Build canonical MCP server configs for target-specific output shaping. */
100
- protected buildMcpServers(options: McpConfigOptions = {}): Record<string, unknown> | undefined {
101
- if (!this.config.mcp) return undefined
102
-
103
- const {
104
- includeDefaultAuthHeaders = true,
105
- transformRemoteEntry,
106
- } = options
107
-
108
- const mcpServers: Record<string, unknown> = {}
109
-
110
- for (const [name, server] of Object.entries(this.config.mcp)) {
111
- if (server.transport === 'stdio') {
112
- mcpServers[name] = {
113
- command: server.command,
114
- args: server.args ?? [],
115
- env: server.env ?? {},
116
- }
117
- continue
118
- }
119
-
120
- const remoteServer: McpRemoteServer = server
121
-
122
- let entry: Record<string, unknown> = {
123
- url: remoteServer.url,
124
- }
125
-
126
- if (includeDefaultAuthHeaders) {
127
- const headers = this.getMcpAuthHeaders(remoteServer)
128
- if (headers) {
129
- entry.headers = headers
130
- }
131
- }
132
-
133
- if (transformRemoteEntry) {
134
- entry = transformRemoteEntry({ name, server: remoteServer, entry })
135
- }
136
-
137
- mcpServers[name] = entry
138
- }
139
-
140
- return mcpServers
141
- }
142
-
143
- protected getMcpAuthMode(): 'inline' | 'platform' {
144
- if (this.platform === 'claude-code') {
145
- return this.config.platforms?.['claude-code']?.mcpAuth ?? 'inline'
146
- }
147
-
148
- if (this.platform === 'cursor') {
149
- return this.config.platforms?.cursor?.mcpAuth ?? 'inline'
150
- }
151
-
152
- return 'inline'
153
- }
154
-
155
- /** Generate MCP config JSON in the common `{ mcpServers }` shape. */
156
- protected async generateMcpConfig(relativePath: string, options: McpConfigOptions = {}): Promise<void> {
157
- const mcpServers = this.buildMcpServers(options)
158
- if (!mcpServers) return
159
- await this.writeJson(relativePath, { mcpServers })
160
- }
161
-
162
- private getMcpAuthHeaders(server: McpRemoteServer): Record<string, string> | undefined {
163
- if (this.getMcpAuthMode() === 'platform' || server.auth?.type === 'platform') {
164
- return undefined
165
- }
166
-
167
- if (server.auth?.type === 'bearer' && server.auth.envVar) {
168
- return {
169
- Authorization: `Bearer ${this.getEnvVarReference(server.auth.envVar)}`,
170
- }
171
- }
172
-
173
- if (server.auth?.type === 'header' && server.auth.envVar) {
174
- return {
175
- [server.auth.headerName]: server.auth.headerTemplate.replace(
176
- '${value}',
177
- this.getEnvVarReference(server.auth.envVar),
178
- ),
179
- }
180
- }
181
-
182
- return undefined
183
- }
184
-
185
- private getEnvVarReference(envVar: string): string {
186
- return `\${${envVar}}`
187
- }
188
- }