incur 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -31
- package/SKILL.md +45 -11
- package/dist/Cli.d.ts +27 -0
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +80 -61
- package/dist/Cli.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +22 -9
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.js +4 -4
- package/dist/bin.js.map +1 -1
- package/dist/internal/agents.d.ts +7 -0
- package/dist/internal/agents.d.ts.map +1 -1
- package/dist/internal/agents.js +30 -2
- package/dist/internal/agents.js.map +1 -1
- package/examples/npm/cli.ts +8 -8
- package/package.json +1 -1
- package/src/Cli.test-d.ts +12 -12
- package/src/Cli.test.ts +446 -29
- package/src/Cli.ts +146 -82
- package/src/Mcp.test.ts +6 -6
- package/src/SyncSkills.ts +26 -9
- package/src/bin.ts +4 -4
- package/src/e2e.test.ts +93 -39
- package/src/internal/agents.ts +32 -2
package/src/e2e.test.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { Cli, Errors, Skill, Typegen, z } from 'incur'
|
|
|
2
2
|
|
|
3
3
|
let __mockSkillsHash: string | undefined
|
|
4
4
|
|
|
5
|
+
const originalIsTTY = process.stdout.isTTY
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
;(process.stdout as any).isTTY = false
|
|
8
|
+
})
|
|
9
|
+
afterAll(() => {
|
|
10
|
+
;(process.stdout as any).isTTY = originalIsTTY
|
|
11
|
+
})
|
|
12
|
+
|
|
5
13
|
vi.mock('./SyncSkills.js', async (importOriginal) => {
|
|
6
14
|
const actual = await importOriginal<typeof import('./SyncSkills.js')>()
|
|
7
15
|
return { ...actual, readHash: () => __mockSkillsHash }
|
|
@@ -56,6 +64,18 @@ describe('routing', () => {
|
|
|
56
64
|
test('unknown top-level command', async () => {
|
|
57
65
|
const { output, exitCode } = await serve(createApp(), ['nonexistent'])
|
|
58
66
|
expect(exitCode).toBe(1)
|
|
67
|
+
expect(output).toMatchInlineSnapshot(`
|
|
68
|
+
"code: COMMAND_NOT_FOUND
|
|
69
|
+
message: 'nonexistent' is not a command. See 'app --help' for a list of available commands.
|
|
70
|
+
"
|
|
71
|
+
`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('unknown top-level command shows human error in TTY', async () => {
|
|
75
|
+
;(process.stdout as any).isTTY = true
|
|
76
|
+
const { output, exitCode } = await serve(createApp(), ['nonexistent'])
|
|
77
|
+
;(process.stdout as any).isTTY = false
|
|
78
|
+
expect(exitCode).toBe(1)
|
|
59
79
|
expect(output).toMatchInlineSnapshot(`
|
|
60
80
|
"Error: 'nonexistent' is not a command. See 'app --help' for a list of available commands.
|
|
61
81
|
"
|
|
@@ -66,7 +86,8 @@ describe('routing', () => {
|
|
|
66
86
|
const { output, exitCode } = await serve(createApp(), ['auth', 'whoami'])
|
|
67
87
|
expect(exitCode).toBe(1)
|
|
68
88
|
expect(output).toMatchInlineSnapshot(`
|
|
69
|
-
"
|
|
89
|
+
"code: COMMAND_NOT_FOUND
|
|
90
|
+
message: 'whoami' is not a command. See 'app auth --help' for a list of available commands.
|
|
70
91
|
"
|
|
71
92
|
`)
|
|
72
93
|
})
|
|
@@ -75,7 +96,8 @@ describe('routing', () => {
|
|
|
75
96
|
const { output, exitCode } = await serve(createApp(), ['project', 'deploy', 'nope'])
|
|
76
97
|
expect(exitCode).toBe(1)
|
|
77
98
|
expect(output).toMatchInlineSnapshot(`
|
|
78
|
-
"
|
|
99
|
+
"code: COMMAND_NOT_FOUND
|
|
100
|
+
message: 'nope' is not a command. See 'app project deploy --help' for a list of available commands.
|
|
79
101
|
"
|
|
80
102
|
`)
|
|
81
103
|
})
|
|
@@ -163,12 +185,32 @@ describe('args and options', () => {
|
|
|
163
185
|
test('missing required arg fails validation', async () => {
|
|
164
186
|
const { output, exitCode } = await serve(createApp(), ['project', 'get'])
|
|
165
187
|
expect(exitCode).toBe(1)
|
|
188
|
+
expect(output).toContain('VALIDATION_ERROR')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('missing required arg shows human error in TTY', async () => {
|
|
192
|
+
;(process.stdout as any).isTTY = true
|
|
193
|
+
const { output, exitCode } = await serve(createApp(), ['project', 'get'])
|
|
194
|
+
;(process.stdout as any).isTTY = false
|
|
195
|
+
expect(exitCode).toBe(1)
|
|
166
196
|
expect(output).toContain('Error: missing required argument <id>')
|
|
167
197
|
})
|
|
168
198
|
|
|
169
199
|
test('unknown flag returns error', async () => {
|
|
170
200
|
const { output, exitCode } = await serve(createApp(), ['ping', '--unknown-flag'])
|
|
171
201
|
expect(exitCode).toBe(1)
|
|
202
|
+
expect(output).toMatchInlineSnapshot(`
|
|
203
|
+
"code: UNKNOWN
|
|
204
|
+
message: "Unknown flag: --unknown-flag"
|
|
205
|
+
"
|
|
206
|
+
`)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('unknown flag shows human error in TTY', async () => {
|
|
210
|
+
;(process.stdout as any).isTTY = true
|
|
211
|
+
const { output, exitCode } = await serve(createApp(), ['ping', '--unknown-flag'])
|
|
212
|
+
;(process.stdout as any).isTTY = false
|
|
213
|
+
expect(exitCode).toBe(1)
|
|
172
214
|
expect(output).toMatchInlineSnapshot(`
|
|
173
215
|
"Error: Unknown flag: --unknown-flag
|
|
174
216
|
"
|
|
@@ -348,8 +390,20 @@ describe('undefined output', () => {
|
|
|
348
390
|
})
|
|
349
391
|
|
|
350
392
|
describe('error handling', () => {
|
|
351
|
-
test('thrown Error shows
|
|
393
|
+
test('thrown Error shows structured error', async () => {
|
|
394
|
+
const { output, exitCode } = await serve(createApp(), ['explode'])
|
|
395
|
+
expect(exitCode).toBe(1)
|
|
396
|
+
expect(output).toMatchInlineSnapshot(`
|
|
397
|
+
"code: UNKNOWN
|
|
398
|
+
message: kaboom
|
|
399
|
+
"
|
|
400
|
+
`)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
test('thrown Error shows human error in TTY', async () => {
|
|
404
|
+
;(process.stdout as any).isTTY = true
|
|
352
405
|
const { output, exitCode } = await serve(createApp(), ['explode'])
|
|
406
|
+
;(process.stdout as any).isTTY = false
|
|
353
407
|
expect(exitCode).toBe(1)
|
|
354
408
|
expect(output).toMatchInlineSnapshot(`
|
|
355
409
|
"Error: kaboom
|
|
@@ -1192,8 +1246,8 @@ describe('root command with subcommands', () => {
|
|
|
1192
1246
|
const cli = Cli.create('tool', {
|
|
1193
1247
|
description: 'A tool with a default action',
|
|
1194
1248
|
args: z.object({ query: z.string().optional().describe('Search query') }),
|
|
1195
|
-
run(
|
|
1196
|
-
return { default: true, query: args.query ?? null }
|
|
1249
|
+
run(c) {
|
|
1250
|
+
return { default: true, query: c.args.query ?? null }
|
|
1197
1251
|
},
|
|
1198
1252
|
})
|
|
1199
1253
|
cli.command('info', {
|
|
@@ -1556,9 +1610,9 @@ function createApp() {
|
|
|
1556
1610
|
scopes: z.array(z.string()).default([]).describe('OAuth scopes'),
|
|
1557
1611
|
}),
|
|
1558
1612
|
alias: { hostname: 'h', web: 'w' },
|
|
1559
|
-
run(
|
|
1560
|
-
return ok(
|
|
1561
|
-
{ hostname: env.AUTH_HOST, scopes: options.scopes },
|
|
1613
|
+
run(c) {
|
|
1614
|
+
return c.ok(
|
|
1615
|
+
{ hostname: c.env.AUTH_HOST, scopes: c.options.scopes },
|
|
1562
1616
|
{
|
|
1563
1617
|
cta: {
|
|
1564
1618
|
description: 'Verify your session:',
|
|
@@ -1570,15 +1624,15 @@ function createApp() {
|
|
|
1570
1624
|
})
|
|
1571
1625
|
.command('logout', {
|
|
1572
1626
|
description: 'Log out of the service',
|
|
1573
|
-
run(
|
|
1574
|
-
return ok({ loggedOut: true })
|
|
1627
|
+
run(c) {
|
|
1628
|
+
return c.ok({ loggedOut: true })
|
|
1575
1629
|
},
|
|
1576
1630
|
})
|
|
1577
1631
|
.command('status', {
|
|
1578
1632
|
description: 'Show authentication status',
|
|
1579
1633
|
output: z.object({ loggedIn: z.boolean(), hostname: z.string(), user: z.string() }),
|
|
1580
|
-
run(
|
|
1581
|
-
return error({
|
|
1634
|
+
run(c) {
|
|
1635
|
+
return c.error({
|
|
1582
1636
|
code: 'NOT_AUTHENTICATED',
|
|
1583
1637
|
message: 'Not logged in',
|
|
1584
1638
|
retryable: false,
|
|
@@ -1607,12 +1661,12 @@ function createApp() {
|
|
|
1607
1661
|
),
|
|
1608
1662
|
total: z.number(),
|
|
1609
1663
|
}),
|
|
1610
|
-
run(
|
|
1664
|
+
run(c) {
|
|
1611
1665
|
const items = [
|
|
1612
1666
|
{ id: 'p1', name: 'Alpha', archived: false },
|
|
1613
1667
|
{ id: 'p2', name: 'Beta', archived: true },
|
|
1614
|
-
].filter((p) => options.archived || !p.archived)
|
|
1615
|
-
return ok(
|
|
1668
|
+
].filter((p) => c.options.archived || !p.archived)
|
|
1669
|
+
return c.ok(
|
|
1616
1670
|
{ items, total: items.length },
|
|
1617
1671
|
{
|
|
1618
1672
|
cta: {
|
|
@@ -1634,9 +1688,9 @@ function createApp() {
|
|
|
1634
1688
|
description: z.string(),
|
|
1635
1689
|
members: z.array(z.object({ userId: z.string(), role: z.string() })),
|
|
1636
1690
|
}),
|
|
1637
|
-
run(
|
|
1638
|
-
return ok({
|
|
1639
|
-
id: args.id,
|
|
1691
|
+
run(c) {
|
|
1692
|
+
return c.ok({
|
|
1693
|
+
id: c.args.id,
|
|
1640
1694
|
name: 'Alpha',
|
|
1641
1695
|
description: 'Main project',
|
|
1642
1696
|
members: [{ userId: 'u1', role: 'admin' }],
|
|
@@ -1653,13 +1707,13 @@ function createApp() {
|
|
|
1653
1707
|
alias: { description: 'd' },
|
|
1654
1708
|
|
|
1655
1709
|
output: z.object({ id: z.string(), url: z.string() }),
|
|
1656
|
-
run(
|
|
1657
|
-
return ok(
|
|
1710
|
+
run(c) {
|
|
1711
|
+
return c.ok(
|
|
1658
1712
|
{ id: 'p-new', url: 'https://example.com/projects/p-new' },
|
|
1659
1713
|
{
|
|
1660
1714
|
cta: {
|
|
1661
1715
|
commands: [
|
|
1662
|
-
{ command: 'project get p-new', description: `View "${args.name}"` },
|
|
1716
|
+
{ command: 'project get p-new', description: `View "${c.args.name}"` },
|
|
1663
1717
|
'project list',
|
|
1664
1718
|
],
|
|
1665
1719
|
},
|
|
@@ -1675,14 +1729,14 @@ function createApp() {
|
|
|
1675
1729
|
}),
|
|
1676
1730
|
alias: { force: 'f' },
|
|
1677
1731
|
|
|
1678
|
-
run(
|
|
1679
|
-
if (!options.force)
|
|
1732
|
+
run(c) {
|
|
1733
|
+
if (!c.options.force)
|
|
1680
1734
|
throw new Errors.IncurError({
|
|
1681
1735
|
code: 'CONFIRMATION_REQUIRED',
|
|
1682
|
-
message: `Use --force to delete project ${args.id}`,
|
|
1736
|
+
message: `Use --force to delete project ${c.args.id}`,
|
|
1683
1737
|
retryable: true,
|
|
1684
1738
|
})
|
|
1685
|
-
return { deleted: true, id: args.id }
|
|
1739
|
+
return { deleted: true, id: c.args.id }
|
|
1686
1740
|
},
|
|
1687
1741
|
})
|
|
1688
1742
|
|
|
@@ -1705,11 +1759,11 @@ function createApp() {
|
|
|
1705
1759
|
options: { branch: 'release', dryRun: true },
|
|
1706
1760
|
},
|
|
1707
1761
|
],
|
|
1708
|
-
run(
|
|
1709
|
-
return ok({
|
|
1762
|
+
run(c) {
|
|
1763
|
+
return c.ok({
|
|
1710
1764
|
deployId: 'd-123',
|
|
1711
|
-
url: `https://${args.env}.example.com`,
|
|
1712
|
-
status: options.dryRun ? 'dry-run' : 'pending',
|
|
1765
|
+
url: `https://${c.args.env}.example.com`,
|
|
1766
|
+
status: c.options.dryRun ? 'dry-run' : 'pending',
|
|
1713
1767
|
})
|
|
1714
1768
|
},
|
|
1715
1769
|
})
|
|
@@ -1718,16 +1772,16 @@ function createApp() {
|
|
|
1718
1772
|
args: z.object({ deployId: z.string().describe('Deployment ID') }),
|
|
1719
1773
|
|
|
1720
1774
|
output: z.object({ deployId: z.string(), status: z.string(), progress: z.number() }),
|
|
1721
|
-
run(
|
|
1722
|
-
return { deployId: args.deployId, status: 'running', progress: 75 }
|
|
1775
|
+
run(c) {
|
|
1776
|
+
return { deployId: c.args.deployId, status: 'running', progress: 75 }
|
|
1723
1777
|
},
|
|
1724
1778
|
})
|
|
1725
1779
|
.command('rollback', {
|
|
1726
1780
|
description: 'Rollback a deployment',
|
|
1727
1781
|
args: z.object({ deployId: z.string().describe('Deployment ID') }),
|
|
1728
1782
|
|
|
1729
|
-
run(
|
|
1730
|
-
return { rolledBack: true, deployId: args.deployId }
|
|
1783
|
+
run(c) {
|
|
1784
|
+
return { rolledBack: true, deployId: c.args.deployId }
|
|
1731
1785
|
},
|
|
1732
1786
|
})
|
|
1733
1787
|
|
|
@@ -1736,8 +1790,8 @@ function createApp() {
|
|
|
1736
1790
|
const config = Cli.create('config', {
|
|
1737
1791
|
description: 'Show current configuration',
|
|
1738
1792
|
args: z.object({ key: z.string().optional().describe('Config key to show') }),
|
|
1739
|
-
run(
|
|
1740
|
-
if (args.key) return { key: args.key, value: 'some-value' }
|
|
1793
|
+
run(c) {
|
|
1794
|
+
if (c.args.key) return { key: c.args.key, value: 'some-value' }
|
|
1741
1795
|
return { apiUrl: 'https://api.example.com', timeout: 30, debug: false }
|
|
1742
1796
|
},
|
|
1743
1797
|
})
|
|
@@ -1765,10 +1819,10 @@ function createApp() {
|
|
|
1765
1819
|
prefix: z.string().default('').describe('Prefix string'),
|
|
1766
1820
|
}),
|
|
1767
1821
|
alias: { upper: 'u', prefix: 'p' },
|
|
1768
|
-
run(
|
|
1769
|
-
const count = args.repeat ?? 1
|
|
1770
|
-
let msg = options.prefix ? `${options.prefix} ${args.message}` : args.message
|
|
1771
|
-
if (options.upper) msg = msg.toUpperCase()
|
|
1822
|
+
run(c) {
|
|
1823
|
+
const count = c.args.repeat ?? 1
|
|
1824
|
+
let msg = c.options.prefix ? `${c.options.prefix} ${c.args.message}` : c.args.message
|
|
1825
|
+
if (c.options.upper) msg = msg.toUpperCase()
|
|
1772
1826
|
return { result: Array(count).fill(msg) }
|
|
1773
1827
|
},
|
|
1774
1828
|
})
|
package/src/internal/agents.ts
CHANGED
|
@@ -79,7 +79,7 @@ export function install(
|
|
|
79
79
|
const canonicalDir = path.join(canonicalBase, skill.name)
|
|
80
80
|
|
|
81
81
|
// Copy to canonical location
|
|
82
|
-
|
|
82
|
+
rmForce(canonicalDir)
|
|
83
83
|
fs.mkdirSync(canonicalDir, { recursive: true })
|
|
84
84
|
if (skill.root)
|
|
85
85
|
fs.copyFileSync(path.join(skill.dir, 'SKILL.md'), path.join(canonicalDir, 'SKILL.md'))
|
|
@@ -96,7 +96,7 @@ export function install(
|
|
|
96
96
|
if (agentDir === canonicalDir) continue
|
|
97
97
|
|
|
98
98
|
try {
|
|
99
|
-
|
|
99
|
+
rmForce(agentDir)
|
|
100
100
|
fs.mkdirSync(path.dirname(agentDir), { recursive: true })
|
|
101
101
|
// Resolve through any existing symlinks in parent directories
|
|
102
102
|
const realLinkDir = resolveParent(path.dirname(agentDir))
|
|
@@ -142,6 +142,27 @@ export declare namespace install {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Removes a skill by name from the canonical location and all detected agent directories.
|
|
147
|
+
*/
|
|
148
|
+
export function remove(
|
|
149
|
+
skillName: string,
|
|
150
|
+
options: { global?: boolean | undefined; cwd?: string | undefined } = {},
|
|
151
|
+
) {
|
|
152
|
+
const isGlobal = options.global !== false
|
|
153
|
+
const cwd = options.cwd || process.cwd()
|
|
154
|
+
const base = isGlobal ? home : cwd
|
|
155
|
+
const canonicalDir = path.join(base, '.agents', 'skills', skillName)
|
|
156
|
+
rmForce(canonicalDir)
|
|
157
|
+
|
|
158
|
+
for (const agent of detect()) {
|
|
159
|
+
if (agent.universal) continue
|
|
160
|
+
const agentSkillsDir = isGlobal ? agent.globalSkillsDir : path.join(cwd, agent.projectSkillsDir)
|
|
161
|
+
const agentDir = path.join(agentSkillsDir, skillName)
|
|
162
|
+
rmForce(agentDir)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
145
166
|
/** Recursively discovers skill directories (those containing a `SKILL.md`). */
|
|
146
167
|
function discoverSkills(rootDir: string): { name: string; dir: string; root?: boolean }[] {
|
|
147
168
|
const results: { name: string; dir: string; root?: boolean }[] = []
|
|
@@ -190,6 +211,15 @@ function sanitizeName(name: string): string {
|
|
|
190
211
|
.slice(0, 255)
|
|
191
212
|
}
|
|
192
213
|
|
|
214
|
+
/** Removes a file, directory, or symlink (including broken symlinks). */
|
|
215
|
+
function rmForce(target: string) {
|
|
216
|
+
try {
|
|
217
|
+
const stat = fs.lstatSync(target)
|
|
218
|
+
if (stat.isSymbolicLink()) fs.unlinkSync(target)
|
|
219
|
+
else fs.rmSync(target, { recursive: true, force: true })
|
|
220
|
+
} catch {}
|
|
221
|
+
}
|
|
222
|
+
|
|
193
223
|
/** Resolves parent directories through symlinks. */
|
|
194
224
|
function resolveParent(dir: string): string {
|
|
195
225
|
try {
|