incur 0.4.8 → 0.4.9
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/dist/Cli.d.ts +65 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +294 -66
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +2 -1
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js +52 -13
- package/dist/Completions.js.map +1 -1
- package/dist/Formatter.d.ts.map +1 -1
- package/dist/Formatter.js +6 -5
- package/dist/Formatter.js.map +1 -1
- package/dist/Help.d.ts +5 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +19 -5
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +21 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +25 -8
- package/dist/Mcp.js.map +1 -1
- package/dist/Parser.d.ts +14 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +127 -1
- package/dist/Parser.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Skillgen.d.ts.map +1 -1
- package/dist/Skillgen.js +4 -38
- package/dist/Skillgen.js.map +1 -1
- package/dist/SyncMcp.d.ts.map +1 -1
- package/dist/SyncMcp.js +75 -8
- package/dist/SyncMcp.js.map +1 -1
- package/dist/SyncSkills.d.ts +9 -0
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +13 -74
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.d.ts +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/internal/command.d.ts +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +6 -5
- package/dist/internal/command.js.map +1 -1
- package/dist/internal/cta.d.ts +22 -0
- package/dist/internal/cta.d.ts.map +1 -0
- package/dist/internal/cta.js +25 -0
- package/dist/internal/cta.js.map +1 -0
- package/dist/internal/json.d.ts +5 -0
- package/dist/internal/json.d.ts.map +1 -0
- package/dist/internal/json.js +13 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.d.ts.map +1 -0
- package/dist/internal/yaml.js +20 -0
- package/dist/internal/yaml.js.map +1 -0
- package/dist/middleware.d.ts +8 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test-d.ts +71 -0
- package/src/Cli.test.ts +1164 -65
- package/src/Cli.ts +456 -101
- package/src/Completions.test.ts +84 -0
- package/src/Completions.ts +48 -9
- package/src/Formatter.test.ts +17 -0
- package/src/Formatter.ts +7 -5
- package/src/Help.test.ts +31 -0
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +193 -0
- package/src/Mcp.ts +49 -8
- package/src/Parser.test.ts +173 -0
- package/src/Parser.ts +132 -1
- package/src/Skill.test.ts +6 -0
- package/src/Skill.ts +8 -5
- package/src/Skillgen.test.ts +55 -6
- package/src/Skillgen.ts +9 -35
- package/src/SyncMcp.test.ts +89 -0
- package/src/SyncMcp.ts +72 -8
- package/src/SyncSkills.test.ts +78 -1
- package/src/SyncSkills.ts +20 -78
- package/src/bun-compile.test.ts +5 -0
- package/src/e2e.test.ts +162 -8
- package/src/internal/command.ts +8 -4
- package/src/internal/cta.ts +58 -0
- package/src/internal/json.ts +16 -0
- package/src/internal/yaml.ts +23 -0
- package/src/lazy-imports.test.ts +94 -0
- package/src/middleware.ts +12 -3
package/src/SyncMcp.test.ts
CHANGED
|
@@ -123,6 +123,95 @@ test('register with agents: ["amp"] skips add-mcp', async () => {
|
|
|
123
123
|
expect(result.agents).toEqual(['Amp'])
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
test('register handles quoted command paths with spaces', async () => {
|
|
127
|
+
const { execFile } = await import('node:child_process')
|
|
128
|
+
vi.mocked(execFile).mockClear()
|
|
129
|
+
|
|
130
|
+
const result = await register('my-cli', {
|
|
131
|
+
command: '"/path/to my/cli" --mcp',
|
|
132
|
+
agents: ['amp'],
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(result.agents).toEqual(['Amp'])
|
|
136
|
+
|
|
137
|
+
const configPath = join(fakeHome!, '.config', 'amp', 'settings.json')
|
|
138
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
139
|
+
expect(config['amp.mcpServers']['my-cli']).toEqual({
|
|
140
|
+
command: '/path/to my/cli',
|
|
141
|
+
args: ['--mcp'],
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('register uses bare name for global binary installs', async () => {
|
|
146
|
+
process.argv[1] = '/usr/local/bin/my-cli'
|
|
147
|
+
|
|
148
|
+
const { execFile } = await import('node:child_process')
|
|
149
|
+
vi.mocked(execFile).mockClear()
|
|
150
|
+
|
|
151
|
+
const result = await register('my-cli', { agents: ['amp'] })
|
|
152
|
+
|
|
153
|
+
expect(result.command).toBe('my-cli --mcp')
|
|
154
|
+
|
|
155
|
+
const configPath = join(fakeHome!, '.config', 'amp', 'settings.json')
|
|
156
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'))
|
|
157
|
+
expect(config['amp.mcpServers']['my-cli']).toEqual({
|
|
158
|
+
command: 'my-cli',
|
|
159
|
+
args: ['--mcp'],
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('register uses runner for source entrypoints outside node_modules', async () => {
|
|
164
|
+
process.argv[1] = join(tmp, 'dist', 'bin.js')
|
|
165
|
+
|
|
166
|
+
const { execFile } = await import('node:child_process')
|
|
167
|
+
vi.mocked(execFile).mockClear()
|
|
168
|
+
|
|
169
|
+
const result = await register('my-cli', { agents: ['amp'] })
|
|
170
|
+
|
|
171
|
+
expect(result.command).toMatch(/^(npx|pnpx|bunx)\s/)
|
|
172
|
+
expect(result.command).toContain('my-cli --mcp')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('register uses bare name for global package entrypoints under node_modules', async () => {
|
|
176
|
+
process.argv[1] = join(tmp, 'global', 'node_modules', 'my-cli', 'dist', 'bin.js')
|
|
177
|
+
|
|
178
|
+
const { execFile } = await import('node:child_process')
|
|
179
|
+
vi.mocked(execFile).mockClear()
|
|
180
|
+
|
|
181
|
+
const result = await register('my-cli', { agents: ['amp'] })
|
|
182
|
+
|
|
183
|
+
expect(result.command).toBe('my-cli --mcp')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('register uses runner for project dev dependency entrypoints under node_modules', async () => {
|
|
187
|
+
writeFileSync(
|
|
188
|
+
join(tmp, 'package.json'),
|
|
189
|
+
JSON.stringify({ devDependencies: { 'my-cli': '1.0.0' } }),
|
|
190
|
+
)
|
|
191
|
+
process.argv[1] = join(tmp, 'node_modules', 'my-cli', 'dist', 'bin.js')
|
|
192
|
+
|
|
193
|
+
const { execFile } = await import('node:child_process')
|
|
194
|
+
vi.mocked(execFile).mockClear()
|
|
195
|
+
|
|
196
|
+
const result = await register('my-cli', { agents: ['amp'] })
|
|
197
|
+
|
|
198
|
+
expect(result.command).toMatch(/^(npx|pnpx|bunx)\s/)
|
|
199
|
+
expect(result.command).toContain('my-cli --mcp')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('register uses runner for node_modules installs', async () => {
|
|
203
|
+
process.argv[1] = join(tmp, 'node_modules', '.bin', 'my-cli')
|
|
204
|
+
|
|
205
|
+
const { execFile } = await import('node:child_process')
|
|
206
|
+
vi.mocked(execFile).mockClear()
|
|
207
|
+
|
|
208
|
+
const result = await register('my-cli', { agents: ['amp'] })
|
|
209
|
+
|
|
210
|
+
// Should include a runner prefix (npx, pnpx, or bunx)
|
|
211
|
+
expect(result.command).toMatch(/^(npx|pnpx|bunx)\s/)
|
|
212
|
+
expect(result.command).toContain('--mcp')
|
|
213
|
+
})
|
|
214
|
+
|
|
126
215
|
test('register writes amp config to existing settings', async () => {
|
|
127
216
|
const configDir = join(fakeHome!, '.config', 'amp')
|
|
128
217
|
mkdirSync(configDir, { recursive: true })
|
package/src/SyncMcp.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function register(
|
|
|
11
11
|
options: register.Options = {},
|
|
12
12
|
): Promise<register.Result> {
|
|
13
13
|
const runner = detectRunner()
|
|
14
|
-
const command = options.command ??
|
|
14
|
+
const command = options.command ?? defaultCommand(name, runner)
|
|
15
15
|
const targetAgents = options.agents ?? []
|
|
16
16
|
const ampOnly = targetAgents.length === 1 && targetAgents[0] === 'amp'
|
|
17
17
|
|
|
@@ -64,7 +64,7 @@ function registerAmp(name: string, command: string): boolean {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const [cmd, ...args] = command
|
|
67
|
+
const [cmd, ...args] = splitCommand(command)
|
|
68
68
|
if (!cmd) return false
|
|
69
69
|
|
|
70
70
|
const servers: Record<string, any> = config['amp.mcpServers'] ?? {}
|
|
@@ -98,16 +98,56 @@ export declare namespace register {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/** @internal
|
|
102
|
-
|
|
101
|
+
/** @internal Builds the default MCP command for the current launch mode. */
|
|
102
|
+
function defaultCommand(name: string, runner: string): string {
|
|
103
|
+
return shouldUseBareCommand(name)
|
|
104
|
+
? `${name} --mcp`
|
|
105
|
+
: `${runner} ${detectPackageSpecifier(name)} --mcp`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @internal Returns node_modules path details for the current entrypoint. */
|
|
109
|
+
function nodeModulesInfo(): { entry: string; root: string } | null {
|
|
110
|
+
const normalized = process.argv[1]?.replace(/\\/g, '/')
|
|
111
|
+
const match = normalized?.match(/^(.+)\/node_modules\/(.+)$/)
|
|
112
|
+
if (!match) return null
|
|
113
|
+
return { root: match[1]!, entry: match[2]! }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @internal Uses the bare command only when the binary is expected on PATH. */
|
|
117
|
+
function shouldUseBareCommand(name: string): boolean {
|
|
103
118
|
const bin = process.argv[1]
|
|
104
|
-
if (!bin) return
|
|
119
|
+
if (!bin) return false
|
|
120
|
+
|
|
121
|
+
const info = nodeModulesInfo()
|
|
122
|
+
if (info) return !info.entry.startsWith('.bin/') && !packageDependsOn(info.root, name)
|
|
105
123
|
|
|
106
|
-
const
|
|
107
|
-
|
|
124
|
+
const file = bin.replace(/\\/g, '/').split('/').pop()
|
|
125
|
+
return file === name || file === `${name}.cmd` || file === `${name}.ps1`
|
|
126
|
+
}
|
|
108
127
|
|
|
128
|
+
/** @internal Checks whether the entrypoint came from a project dependency install. */
|
|
129
|
+
function packageDependsOn(root: string, name: string): boolean {
|
|
109
130
|
try {
|
|
110
|
-
const pkg = JSON.parse(readFileSync(join(
|
|
131
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8'))
|
|
132
|
+
const dependencyFields = [
|
|
133
|
+
pkg.dependencies,
|
|
134
|
+
pkg.devDependencies,
|
|
135
|
+
pkg.optionalDependencies,
|
|
136
|
+
pkg.peerDependencies,
|
|
137
|
+
]
|
|
138
|
+
return dependencyFields.some((dependencies) => name in (dependencies ?? {}))
|
|
139
|
+
} catch {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @internal Detects the package specifier used to run this CLI (handles dlx/npx URL and version installs). */
|
|
145
|
+
export function detectPackageSpecifier(name: string): string {
|
|
146
|
+
const info = nodeModulesInfo()
|
|
147
|
+
if (!info) return name
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const pkg = JSON.parse(readFileSync(join(info.root, 'package.json'), 'utf-8'))
|
|
111
151
|
const deps = pkg.dependencies ?? {}
|
|
112
152
|
const spec = deps[name]
|
|
113
153
|
if (!spec || Object.keys(deps).length !== 1) return name
|
|
@@ -119,6 +159,30 @@ export function detectPackageSpecifier(name: string): string {
|
|
|
119
159
|
return name
|
|
120
160
|
}
|
|
121
161
|
|
|
162
|
+
/** Splits a command string into tokens, respecting single and double quotes. */
|
|
163
|
+
function splitCommand(input: string): string[] {
|
|
164
|
+
const tokens: string[] = []
|
|
165
|
+
let current = ''
|
|
166
|
+
let quote: string | null = null
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < input.length; i++) {
|
|
169
|
+
const ch = input[i]!
|
|
170
|
+
if (quote) {
|
|
171
|
+
if (ch === quote) quote = null
|
|
172
|
+
else current += ch
|
|
173
|
+
} else if (ch === '"' || ch === "'") {
|
|
174
|
+
quote = ch
|
|
175
|
+
} else if (ch === ' ') {
|
|
176
|
+
if (current) tokens.push(current)
|
|
177
|
+
current = ''
|
|
178
|
+
} else {
|
|
179
|
+
current += ch
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (current) tokens.push(current)
|
|
183
|
+
return tokens
|
|
184
|
+
}
|
|
185
|
+
|
|
122
186
|
/** Promisified execFile with stderr in error message. */
|
|
123
187
|
function exec(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
124
188
|
return new Promise((resolve, reject) => {
|
package/src/SyncSkills.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cli, SyncSkills } from 'incur'
|
|
1
|
+
import { Cli, SyncSkills, z } from 'incur'
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -161,6 +161,45 @@ test('installed SKILL.md contains frontmatter', async () => {
|
|
|
161
161
|
rmSync(tmp, { recursive: true, force: true })
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
+
test('installed SKILL.md marks destructive commands', async () => {
|
|
165
|
+
const tmp = join(tmpdir(), `clac-destructive-test-${Date.now()}`)
|
|
166
|
+
mkdirSync(tmp, { recursive: true })
|
|
167
|
+
|
|
168
|
+
const cli = Cli.create('my-tool')
|
|
169
|
+
cli.command('destroy', {
|
|
170
|
+
description: 'Destroy data',
|
|
171
|
+
destructive: true,
|
|
172
|
+
hint: 'Deletes all data.',
|
|
173
|
+
run: () => ({}),
|
|
174
|
+
})
|
|
175
|
+
cli.command('status', {
|
|
176
|
+
description: 'Check status',
|
|
177
|
+
hint: 'Shows current status.',
|
|
178
|
+
run: () => ({}),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const commands = Cli.toCommands.get(cli)!
|
|
182
|
+
const installDir = join(tmp, 'install')
|
|
183
|
+
mkdirSync(join(installDir, '.agents', 'skills'), { recursive: true })
|
|
184
|
+
|
|
185
|
+
const result = await SyncSkills.sync('my-tool', commands, {
|
|
186
|
+
depth: 0,
|
|
187
|
+
global: false,
|
|
188
|
+
cwd: installDir,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const content = readFileSync(join(result.paths[0]!, 'SKILL.md'), 'utf8')
|
|
192
|
+
expect(content).toContain(
|
|
193
|
+
'Deletes all data. Confirm with the user before executing this destructive command.',
|
|
194
|
+
)
|
|
195
|
+
expect(content).toContain('Shows current status.')
|
|
196
|
+
expect(content).not.toContain(
|
|
197
|
+
'Shows current status. Confirm with the user before executing this destructive command.',
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
201
|
+
})
|
|
202
|
+
|
|
164
203
|
test('sync returns unquoted descriptions from YAML frontmatter', async () => {
|
|
165
204
|
const tmp = join(tmpdir(), `clac-quoted-description-test-${Date.now()}`)
|
|
166
205
|
mkdirSync(tmp, { recursive: true })
|
|
@@ -288,6 +327,44 @@ test('list includes root command skill', async () => {
|
|
|
288
327
|
expect(names).toContain('test-ping')
|
|
289
328
|
})
|
|
290
329
|
|
|
330
|
+
test('sync uses CLI skill projection for aliases, fetch gateways, examples, and output', async () => {
|
|
331
|
+
const tmp = join(tmpdir(), `clac-sync-drift-test-${Date.now()}`)
|
|
332
|
+
mkdirSync(tmp, { recursive: true })
|
|
333
|
+
|
|
334
|
+
const cli = Cli.create('tool')
|
|
335
|
+
.command('real', {
|
|
336
|
+
description: 'Real command',
|
|
337
|
+
aliases: ['r'],
|
|
338
|
+
options: z.object({ dryRun: z.boolean().default(false) }),
|
|
339
|
+
output: z.object({ value: z.string() }),
|
|
340
|
+
examples: [{ options: { dryRun: true }, description: 'Preview' }],
|
|
341
|
+
run: () => ({ value: 'ok' }),
|
|
342
|
+
})
|
|
343
|
+
.command('api', { description: 'Raw API', fetch: () => new Response('{}') })
|
|
344
|
+
|
|
345
|
+
const commands = Cli.toCommands.get(cli)!
|
|
346
|
+
const listed = await SyncSkills.list('tool', commands)
|
|
347
|
+
const names = listed.map((skill) => skill.name)
|
|
348
|
+
expect(names).toContain('tool-api')
|
|
349
|
+
expect(names).toContain('tool-real')
|
|
350
|
+
expect(names).not.toContain('tool-r')
|
|
351
|
+
|
|
352
|
+
const installDir = join(tmp, 'install')
|
|
353
|
+
mkdirSync(join(installDir, '.agents', 'skills'), { recursive: true })
|
|
354
|
+
const synced = await SyncSkills.sync('tool', commands, {
|
|
355
|
+
depth: 0,
|
|
356
|
+
global: false,
|
|
357
|
+
cwd: installDir,
|
|
358
|
+
})
|
|
359
|
+
const content = readFileSync(join(synced.paths[0]!, 'SKILL.md'), 'utf8')
|
|
360
|
+
expect(content).toContain('Preview')
|
|
361
|
+
expect(content).toContain('## Output')
|
|
362
|
+
expect(content).toContain('Fetch gateway. Pass path segments')
|
|
363
|
+
expect(content).not.toMatch(/^# tool r$/m)
|
|
364
|
+
|
|
365
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
366
|
+
})
|
|
367
|
+
|
|
291
368
|
test('list results are sorted alphabetically', async () => {
|
|
292
369
|
const cli = Cli.create('test')
|
|
293
370
|
cli.command('zebra', { description: 'Z command', run: () => ({}) })
|
package/src/SyncSkills.ts
CHANGED
|
@@ -2,10 +2,11 @@ import fsSync from 'node:fs'
|
|
|
2
2
|
import fs from 'node:fs/promises'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { parse as yamlParse } from 'yaml'
|
|
6
5
|
|
|
7
|
-
import {
|
|
6
|
+
import { collectSkillCommands, parseSkillFrontmatter } from './Cli.js'
|
|
8
7
|
import * as Agents from './internal/agents.js'
|
|
8
|
+
import * as Yaml from './internal/yaml.js'
|
|
9
|
+
import type * as Mcp from './Mcp.js'
|
|
9
10
|
import * as Skill from './Skill.js'
|
|
10
11
|
|
|
11
12
|
/** Generates skill files from a command map and installs them natively. */
|
|
@@ -17,9 +18,12 @@ export async function sync(
|
|
|
17
18
|
const { depth = 1, description, global = true } = options
|
|
18
19
|
const cwd = options.cwd ?? (global ? resolvePackageRoot() : process.cwd())
|
|
19
20
|
|
|
21
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, `parseFrontmatter`).
|
|
22
|
+
await Yaml.load()
|
|
23
|
+
|
|
20
24
|
const groups = new Map<string, string>()
|
|
21
25
|
if (description) groups.set(name, description)
|
|
22
|
-
const entries =
|
|
26
|
+
const entries = collectSkillCommands(commands, [], groups, options.rootCommand)
|
|
23
27
|
const files = Skill.split(name, entries, depth, groups)
|
|
24
28
|
|
|
25
29
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `incur-skills-${name}-`))
|
|
@@ -31,7 +35,7 @@ export async function sync(
|
|
|
31
35
|
: path.join(tmpDir, 'SKILL.md')
|
|
32
36
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
33
37
|
await fs.writeFile(filePath, `${file.content}\n`)
|
|
34
|
-
const meta =
|
|
38
|
+
const meta = parseSkillFrontmatter(file.content)
|
|
35
39
|
skills.push({ name: meta.name ?? (file.dir || name), description: meta.description })
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -42,7 +46,7 @@ export async function sync(
|
|
|
42
46
|
for await (const match of fs.glob(globPattern, { cwd })) {
|
|
43
47
|
try {
|
|
44
48
|
const content = await fs.readFile(path.resolve(cwd, match), 'utf8')
|
|
45
|
-
const meta =
|
|
49
|
+
const meta = parseSkillFrontmatter(content)
|
|
46
50
|
const skillName =
|
|
47
51
|
pattern === '_root' ? (meta.name ?? name) : path.basename(path.dirname(match))
|
|
48
52
|
const dest = path.join(tmpDir, skillName, 'SKILL.md')
|
|
@@ -68,7 +72,7 @@ export async function sync(
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// Write skills hash + names for staleness detection
|
|
71
|
-
const hashEntries =
|
|
75
|
+
const hashEntries = collectSkillCommands(commands, [], new Map(), options.rootCommand)
|
|
72
76
|
writeMeta(
|
|
73
77
|
name,
|
|
74
78
|
Skill.hash(hashEntries),
|
|
@@ -100,8 +104,10 @@ export declare namespace sync {
|
|
|
100
104
|
| {
|
|
101
105
|
description?: string | undefined
|
|
102
106
|
args?: any
|
|
107
|
+
destructive?: boolean | undefined
|
|
103
108
|
env?: any
|
|
104
109
|
hint?: string | undefined
|
|
110
|
+
mcp?: { annotations?: Mcp.ToolAnnotations | undefined } | undefined
|
|
105
111
|
options?: any
|
|
106
112
|
output?: any
|
|
107
113
|
examples?: any[] | undefined
|
|
@@ -137,16 +143,19 @@ export async function list(
|
|
|
137
143
|
const { depth = 1, description } = options
|
|
138
144
|
const cwd = options.cwd ?? process.cwd()
|
|
139
145
|
|
|
146
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, `parseFrontmatter`).
|
|
147
|
+
await Yaml.load()
|
|
148
|
+
|
|
140
149
|
const groups = new Map<string, string>()
|
|
141
150
|
if (description) groups.set(name, description)
|
|
142
|
-
const entries =
|
|
151
|
+
const entries = collectSkillCommands(commands, [], groups, options.rootCommand)
|
|
143
152
|
const files = Skill.split(name, entries, depth, groups)
|
|
144
153
|
|
|
145
154
|
const skills: list.Skill[] = []
|
|
146
155
|
const installed = readInstalledSkills(name, { cwd })
|
|
147
156
|
|
|
148
157
|
for (const file of files) {
|
|
149
|
-
const meta =
|
|
158
|
+
const meta = parseSkillFrontmatter(file.content)
|
|
150
159
|
const skillName = meta.name ?? (file.dir || name)
|
|
151
160
|
skills.push({
|
|
152
161
|
name: skillName,
|
|
@@ -162,7 +171,7 @@ export async function list(
|
|
|
162
171
|
for await (const match of fs.glob(globPattern, { cwd })) {
|
|
163
172
|
try {
|
|
164
173
|
const content = await fs.readFile(path.resolve(cwd, match), 'utf8')
|
|
165
|
-
const meta =
|
|
174
|
+
const meta = parseSkillFrontmatter(content)
|
|
166
175
|
const skillName =
|
|
167
176
|
pattern === '_root' ? (meta.name ?? name) : path.basename(path.dirname(match))
|
|
168
177
|
if (!skills.some((s) => s.name === skillName)) {
|
|
@@ -204,8 +213,10 @@ export declare namespace list {
|
|
|
204
213
|
| {
|
|
205
214
|
description?: string | undefined
|
|
206
215
|
args?: any
|
|
216
|
+
destructive?: boolean | undefined
|
|
207
217
|
env?: any
|
|
208
218
|
hint?: string | undefined
|
|
219
|
+
mcp?: { annotations?: Mcp.ToolAnnotations | undefined } | undefined
|
|
209
220
|
options?: any
|
|
210
221
|
output?: any
|
|
211
222
|
examples?: any[] | undefined
|
|
@@ -223,75 +234,6 @@ export declare namespace list {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
|
|
226
|
-
/** Recursively collects leaf commands as `Skill.CommandInfo`. */
|
|
227
|
-
function collectEntries(
|
|
228
|
-
commands: Map<string, any>,
|
|
229
|
-
prefix: string[],
|
|
230
|
-
groups: Map<string, string> = new Map(),
|
|
231
|
-
rootCommand?:
|
|
232
|
-
| {
|
|
233
|
-
description?: string | undefined
|
|
234
|
-
args?: any
|
|
235
|
-
env?: any
|
|
236
|
-
hint?: string | undefined
|
|
237
|
-
options?: any
|
|
238
|
-
output?: any
|
|
239
|
-
examples?: any[] | undefined
|
|
240
|
-
}
|
|
241
|
-
| undefined,
|
|
242
|
-
): Skill.CommandInfo[] {
|
|
243
|
-
const result: Skill.CommandInfo[] = []
|
|
244
|
-
if (rootCommand) {
|
|
245
|
-
const cmd: Skill.CommandInfo = {}
|
|
246
|
-
if (rootCommand.description) cmd.description = rootCommand.description
|
|
247
|
-
if (rootCommand.args) cmd.args = rootCommand.args
|
|
248
|
-
if (rootCommand.env) cmd.env = rootCommand.env
|
|
249
|
-
if (rootCommand.hint) cmd.hint = rootCommand.hint
|
|
250
|
-
if (rootCommand.options) cmd.options = rootCommand.options
|
|
251
|
-
if (rootCommand.output) cmd.output = rootCommand.output
|
|
252
|
-
const examples = formatExamples(rootCommand.examples)
|
|
253
|
-
if (examples) cmd.examples = examples
|
|
254
|
-
result.push(cmd)
|
|
255
|
-
}
|
|
256
|
-
for (const [name, entry] of commands) {
|
|
257
|
-
const entryPath = [...prefix, name]
|
|
258
|
-
if ('_group' in entry && entry._group) {
|
|
259
|
-
if (entry.description) groups.set(entryPath.join(' '), entry.description)
|
|
260
|
-
result.push(...collectEntries(entry.commands, entryPath, groups))
|
|
261
|
-
} else {
|
|
262
|
-
const cmd: Skill.CommandInfo = { name: entryPath.join(' ') }
|
|
263
|
-
if (entry.description) cmd.description = entry.description
|
|
264
|
-
if (entry.args) cmd.args = entry.args
|
|
265
|
-
if (entry.env) cmd.env = entry.env
|
|
266
|
-
if (entry.hint) cmd.hint = entry.hint
|
|
267
|
-
if (entry.options) cmd.options = entry.options
|
|
268
|
-
if (entry.output) cmd.output = entry.output
|
|
269
|
-
const examples = formatExamples(entry.examples)
|
|
270
|
-
if (examples) {
|
|
271
|
-
const cmdName = entryPath.join(' ')
|
|
272
|
-
cmd.examples = examples.map((e) => ({
|
|
273
|
-
...e,
|
|
274
|
-
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
275
|
-
}))
|
|
276
|
-
}
|
|
277
|
-
result.push(cmd)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function parseFrontmatter(content: string): {
|
|
284
|
-
description?: string | undefined
|
|
285
|
-
name?: string | undefined
|
|
286
|
-
} {
|
|
287
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
288
|
-
if (!match) return {}
|
|
289
|
-
|
|
290
|
-
const meta = yamlParse(match[1]!)
|
|
291
|
-
if (!meta || typeof meta !== 'object') return {}
|
|
292
|
-
return meta as { description?: string | undefined; name?: string | undefined }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
237
|
/** Resolves the package root from the executing bin script (`process.argv[1]`). Walks up from the bin's directory looking for `package.json`. Falls back to `process.cwd()`. */
|
|
296
238
|
function resolvePackageRoot(): string {
|
|
297
239
|
const bin = process.argv[1]
|
package/src/bun-compile.test.ts
CHANGED
|
@@ -70,6 +70,11 @@ cli.serve()
|
|
|
70
70
|
expect(stdout).toContain('result: HELLO')
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
test('runs command with --format yaml (lazy yaml import)', async () => {
|
|
74
|
+
const { stdout } = await exec(bin, ['ping', '--format', 'yaml'])
|
|
75
|
+
expect(stdout).toContain('pong: true')
|
|
76
|
+
})
|
|
77
|
+
|
|
73
78
|
test('shows help', async () => {
|
|
74
79
|
const { stdout } = await exec(bin, ['--help'])
|
|
75
80
|
expect(stdout).toContain('test-cli')
|