kirohub 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +56 -13
- package/dist/cli.js +145 -4
- package/dist/commands/add.js +44 -15
- package/dist/commands/add.test.js +67 -0
- package/dist/commands/remove.js +101 -0
- package/dist/commands/search.js +112 -13
- package/dist/commands/search.test.js +115 -0
- package/dist/utils/banner.js +37 -0
- package/package.json +9 -5
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -2,35 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
Install and discover Kiro resources from the command line.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx kirohub
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Interactive Mode
|
|
12
|
+
|
|
13
|
+
Run without arguments to launch the interactive menu:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx kirohub
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This provides a guided experience to search, install, list, and remove resources with type and category filters.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
### Search
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
26
|
npx kirohub search <query>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
- `-t, --type <type>` — Filter by type: `Steering`, `Hook`, `Power`, `Prompt`, `Agent`, `Skill`
|
|
31
|
+
- `-c, --category <cat>` — Filter by category: `aws`, `frontend`, `backend`, `api-design`, `documentation`, `devops`, `security`, `testing`, `database`, `ai-ml`, `architecture`, `developer-tools`, `typescript`, `python`, `java`, `rust`, `golang`, `csharp`
|
|
32
|
+
- `-l, --limit <n>` — Limit results (default: 10)
|
|
33
|
+
- `--json` — Output as JSON for scripting
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx kirohub search terraform
|
|
39
|
+
npx kirohub search aws --type Steering
|
|
40
|
+
npx kirohub search react --category frontend --limit 20
|
|
41
|
+
npx kirohub search api --type Hook --category backend --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Add
|
|
45
|
+
|
|
46
|
+
```bash
|
|
9
47
|
npx kirohub add <owner/resource>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
- `-y, --yes` — Skip confirmation prompt
|
|
52
|
+
|
|
53
|
+
### List
|
|
54
|
+
|
|
55
|
+
```bash
|
|
10
56
|
npx kirohub list
|
|
11
57
|
```
|
|
12
58
|
|
|
13
|
-
|
|
59
|
+
Shows installed resources in your `.kiro/` directory.
|
|
60
|
+
|
|
61
|
+
### Remove
|
|
14
62
|
|
|
15
63
|
```bash
|
|
16
|
-
npx kirohub
|
|
17
|
-
npx kirohub add owner/my-steering
|
|
18
|
-
npx kirohub add kirodotdev/power-builder
|
|
64
|
+
npx kirohub remove [resource]
|
|
19
65
|
```
|
|
20
66
|
|
|
67
|
+
Options:
|
|
68
|
+
- `-y, --yes` — Skip confirmation prompt
|
|
69
|
+
|
|
70
|
+
If no resource is specified, shows an interactive picker.
|
|
71
|
+
|
|
21
72
|
## Publish (Maintainers)
|
|
22
73
|
|
|
23
74
|
```bash
|
|
24
75
|
cd packages/cli
|
|
25
76
|
npm install
|
|
26
77
|
npm run build
|
|
27
|
-
node dist/cli.js --help
|
|
28
|
-
npm login
|
|
29
78
|
npm publish --access public
|
|
30
79
|
```
|
|
31
|
-
|
|
32
|
-
Test the published CLI:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx kirohub --help
|
|
36
|
-
```
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
3
4
|
import { addResource } from './commands/add.js';
|
|
4
5
|
import { listResources } from './commands/list.js';
|
|
5
|
-
import { searchResources } from './commands/search.js';
|
|
6
|
+
import { searchResources, fetchResources } from './commands/search.js';
|
|
7
|
+
import { removeResource } from './commands/remove.js';
|
|
8
|
+
import { showBanner, showLogo } from './utils/banner.js';
|
|
6
9
|
const program = new Command();
|
|
7
10
|
program
|
|
8
11
|
.name('kirohub')
|
|
@@ -11,8 +14,12 @@ program
|
|
|
11
14
|
program
|
|
12
15
|
.command('add <resource>')
|
|
13
16
|
.description('Install a Kiro resource (e.g., owner/resource-name)')
|
|
17
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
14
18
|
.option('--no-telemetry', 'Disable telemetry')
|
|
15
|
-
.action(
|
|
19
|
+
.action((resource, options) => {
|
|
20
|
+
showLogo();
|
|
21
|
+
addResource(resource, options);
|
|
22
|
+
});
|
|
16
23
|
program
|
|
17
24
|
.command('list')
|
|
18
25
|
.description('List installed resources')
|
|
@@ -20,6 +27,140 @@ program
|
|
|
20
27
|
program
|
|
21
28
|
.command('search <query>')
|
|
22
29
|
.description('Search available resources')
|
|
23
|
-
.option('-t, --type <type>', 'Filter by type')
|
|
30
|
+
.option('-t, --type <type>', 'Filter by type (Steering, Hook, Power, Prompt, Agent, Skill)')
|
|
31
|
+
.option('-c, --category <category>', 'Filter by category (aws, frontend, backend, etc.)')
|
|
32
|
+
.option('-l, --limit <n>', 'Limit results (default: 10)')
|
|
33
|
+
.option('--json', 'Output as JSON (for scripting)')
|
|
24
34
|
.action(searchResources);
|
|
25
|
-
program
|
|
35
|
+
program
|
|
36
|
+
.command('remove [resource]')
|
|
37
|
+
.description('Remove an installed resource')
|
|
38
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
39
|
+
.action((resource, options) => {
|
|
40
|
+
showLogo();
|
|
41
|
+
removeResource(resource, options);
|
|
42
|
+
});
|
|
43
|
+
async function interactiveMenu() {
|
|
44
|
+
showBanner();
|
|
45
|
+
while (true) {
|
|
46
|
+
const action = await p.select({
|
|
47
|
+
message: 'What would you like to do?',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'search', label: 'Search resources', hint: 'find and install from hub' },
|
|
50
|
+
{ value: 'add', label: 'Add a resource', hint: 'install by slug' },
|
|
51
|
+
{ value: 'list', label: 'List installed', hint: 'show local resources' },
|
|
52
|
+
{ value: 'remove', label: 'Remove a resource', hint: 'uninstall' },
|
|
53
|
+
{ value: 'exit', label: 'Exit' },
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
if (p.isCancel(action) || action === 'exit') {
|
|
57
|
+
p.outro('Goodbye!');
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
console.log();
|
|
61
|
+
if (action === 'search') {
|
|
62
|
+
const query = await p.text({
|
|
63
|
+
message: 'Search query:',
|
|
64
|
+
placeholder: 'e.g., aws, terraform, react',
|
|
65
|
+
});
|
|
66
|
+
if (p.isCancel(query))
|
|
67
|
+
continue;
|
|
68
|
+
const filterType = await p.select({
|
|
69
|
+
message: 'Filter by type?',
|
|
70
|
+
options: [
|
|
71
|
+
{ value: '', label: 'All types' },
|
|
72
|
+
{ value: 'Steering', label: 'Steering', hint: 'context files' },
|
|
73
|
+
{ value: 'Hook', label: 'Hook', hint: 'event triggers' },
|
|
74
|
+
{ value: 'Power', label: 'Power', hint: 'bundled packages' },
|
|
75
|
+
{ value: 'Prompt', label: 'Prompt', hint: 'CLI prompts' },
|
|
76
|
+
{ value: 'Agent', label: 'Agent', hint: 'agent configs' },
|
|
77
|
+
{ value: 'Skill', label: 'Skill', hint: 'agent skills' },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
if (p.isCancel(filterType))
|
|
81
|
+
continue;
|
|
82
|
+
const filterCategory = await p.select({
|
|
83
|
+
message: 'Filter by category?',
|
|
84
|
+
options: [
|
|
85
|
+
{ value: '', label: 'All categories' },
|
|
86
|
+
{ value: 'aws', label: 'AWS' },
|
|
87
|
+
{ value: 'frontend', label: 'Frontend' },
|
|
88
|
+
{ value: 'backend', label: 'Backend' },
|
|
89
|
+
{ value: 'api-design', label: 'API Design' },
|
|
90
|
+
{ value: 'documentation', label: 'Documentation' },
|
|
91
|
+
{ value: 'devops', label: 'DevOps' },
|
|
92
|
+
{ value: 'security', label: 'Security' },
|
|
93
|
+
{ value: 'testing', label: 'Testing' },
|
|
94
|
+
{ value: 'database', label: 'Database' },
|
|
95
|
+
{ value: 'ai-ml', label: 'AI/ML' },
|
|
96
|
+
{ value: 'architecture', label: 'Architecture' },
|
|
97
|
+
{ value: 'developer-tools', label: 'Developer Tools' },
|
|
98
|
+
{ value: 'typescript', label: 'TypeScript' },
|
|
99
|
+
{ value: 'python', label: 'Python' },
|
|
100
|
+
{ value: 'java', label: 'Java' },
|
|
101
|
+
{ value: 'rust', label: 'Rust' },
|
|
102
|
+
{ value: 'golang', label: 'Go' },
|
|
103
|
+
{ value: 'csharp', label: 'C#' },
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
if (p.isCancel(filterCategory))
|
|
107
|
+
continue;
|
|
108
|
+
const s = p.spinner();
|
|
109
|
+
s.start('Searching...');
|
|
110
|
+
try {
|
|
111
|
+
const results = await fetchResources(query, {
|
|
112
|
+
type: filterType || undefined,
|
|
113
|
+
category: filterCategory || undefined,
|
|
114
|
+
limit: 20,
|
|
115
|
+
});
|
|
116
|
+
s.stop(`Found ${results.length} resources`);
|
|
117
|
+
if (results.length === 0) {
|
|
118
|
+
p.log.warn('No resources found.');
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const selected = await p.select({
|
|
122
|
+
message: 'Select to install (or esc to cancel)',
|
|
123
|
+
options: results.map(r => ({
|
|
124
|
+
value: r.slug,
|
|
125
|
+
label: `${r.slug}${r.overallScore ? ` [${r.overallScore}]` : ''}`,
|
|
126
|
+
hint: r.type,
|
|
127
|
+
})),
|
|
128
|
+
});
|
|
129
|
+
if (p.isCancel(selected))
|
|
130
|
+
continue;
|
|
131
|
+
await addResource(selected, { yes: true });
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
s.stop(`Search failed: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (action === 'add') {
|
|
138
|
+
const slug = await p.text({
|
|
139
|
+
message: 'Resource slug:',
|
|
140
|
+
placeholder: 'e.g., owner/resource-name',
|
|
141
|
+
});
|
|
142
|
+
if (p.isCancel(slug))
|
|
143
|
+
continue;
|
|
144
|
+
await addResource(slug, {});
|
|
145
|
+
}
|
|
146
|
+
else if (action === 'list') {
|
|
147
|
+
await listResources();
|
|
148
|
+
}
|
|
149
|
+
else if (action === 'remove') {
|
|
150
|
+
await removeResource(undefined, {});
|
|
151
|
+
}
|
|
152
|
+
console.log();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Show interactive menu when no command provided
|
|
156
|
+
if (process.argv.length <= 2) {
|
|
157
|
+
if (process.stdout.isTTY) {
|
|
158
|
+
interactiveMenu();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
showBanner();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
program.parse();
|
|
166
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join, dirname, basename } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
5
|
import { trackResourceInstall, trackCommand } from '../utils/telemetry.js';
|
|
6
6
|
const KIROHUB_API = 'https://kirohub.dev/resource-map.json';
|
|
7
|
+
function isInteractive() {
|
|
8
|
+
return process.stdout.isTTY === true;
|
|
9
|
+
}
|
|
7
10
|
export async function addResource(resourceSlug, options) {
|
|
8
|
-
const
|
|
11
|
+
const skipPrompts = options.yes || !isInteractive();
|
|
12
|
+
const s = p.spinner();
|
|
13
|
+
s.start('Fetching resource...');
|
|
9
14
|
try {
|
|
10
15
|
// Fetch resource map
|
|
11
16
|
const response = await fetch(KIROHUB_API);
|
|
@@ -16,22 +21,46 @@ export async function addResource(resourceSlug, options) {
|
|
|
16
21
|
// Find resource by slug
|
|
17
22
|
const resource = resources.find(r => r.slug === resourceSlug);
|
|
18
23
|
if (!resource) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
s.stop(`Resource "${resourceSlug}" not found`);
|
|
25
|
+
if (isInteractive()) {
|
|
26
|
+
console.log(chalk.gray('Available resources:'));
|
|
27
|
+
resources.slice(0, 5).forEach(r => {
|
|
28
|
+
console.log(chalk.gray(` ${r.slug} (${r.type})`));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
24
31
|
return;
|
|
25
32
|
}
|
|
26
|
-
|
|
33
|
+
// Determine installation path
|
|
34
|
+
const installPath = getInstallPath(resource);
|
|
35
|
+
s.stop('Found resource');
|
|
36
|
+
// Check if file exists and prompt for confirmation
|
|
37
|
+
if (existsSync(installPath) && !skipPrompts) {
|
|
38
|
+
const overwrite = await p.confirm({
|
|
39
|
+
message: `File already exists at ${installPath}. Overwrite?`,
|
|
40
|
+
});
|
|
41
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
42
|
+
p.cancel('Cancelled.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Prompt for confirmation if not using -y flag
|
|
47
|
+
if (!skipPrompts && !existsSync(installPath)) {
|
|
48
|
+
p.note(`${resource.name} (${resource.type})\n→ ${installPath}`, 'Resource');
|
|
49
|
+
const proceed = await p.confirm({
|
|
50
|
+
message: 'Install this resource?',
|
|
51
|
+
});
|
|
52
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
53
|
+
p.cancel('Cancelled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
s.start(`Installing ${resource.name}...`);
|
|
27
58
|
// Fetch file content
|
|
28
59
|
const fileResponse = await fetch(resource.githubRawUrl);
|
|
29
60
|
if (!fileResponse.ok) {
|
|
30
61
|
throw new Error(`Failed to fetch resource file: ${fileResponse.status} ${fileResponse.statusText}`);
|
|
31
62
|
}
|
|
32
63
|
const content = await fileResponse.text();
|
|
33
|
-
// Determine installation path
|
|
34
|
-
const installPath = getInstallPath(resource);
|
|
35
64
|
// Create directory if needed
|
|
36
65
|
mkdirSync(dirname(installPath), { recursive: true });
|
|
37
66
|
// Write file
|
|
@@ -41,13 +70,13 @@ export async function addResource(resourceSlug, options) {
|
|
|
41
70
|
trackResourceInstall(resource.slug, resource.type);
|
|
42
71
|
trackCommand('add');
|
|
43
72
|
}
|
|
44
|
-
|
|
73
|
+
s.stop(`Installed ${resource.name} to ${installPath}`);
|
|
45
74
|
}
|
|
46
75
|
catch (error) {
|
|
47
|
-
|
|
76
|
+
s.stop(`Failed: ${error.message}`);
|
|
48
77
|
}
|
|
49
78
|
}
|
|
50
|
-
function getInstallPath(resource) {
|
|
79
|
+
export function getInstallPath(resource) {
|
|
51
80
|
const cwd = process.cwd();
|
|
52
81
|
const safeName = sanitizeName(resource.name || resource.slug);
|
|
53
82
|
const safeSlug = sanitizeName(resource.slug);
|
|
@@ -69,7 +98,7 @@ function getInstallPath(resource) {
|
|
|
69
98
|
return join(cwd, '.kiro', 'resources', safeName);
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
|
-
function sanitizeName(value) {
|
|
101
|
+
export function sanitizeName(value) {
|
|
73
102
|
return value
|
|
74
103
|
.toLowerCase()
|
|
75
104
|
.trim()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { getInstallPath, sanitizeName } from './add.js';
|
|
4
|
+
describe('sanitizeName', () => {
|
|
5
|
+
it('lowercases and trims', () => {
|
|
6
|
+
expect(sanitizeName(' Hello World ')).toBe('hello-world');
|
|
7
|
+
});
|
|
8
|
+
it('replaces special chars with hyphens', () => {
|
|
9
|
+
expect(sanitizeName('my@resource!name')).toBe('my-resource-name');
|
|
10
|
+
});
|
|
11
|
+
it('collapses multiple hyphens', () => {
|
|
12
|
+
expect(sanitizeName('a---b')).toBe('a-b');
|
|
13
|
+
});
|
|
14
|
+
it('strips leading/trailing slashes', () => {
|
|
15
|
+
expect(sanitizeName('/owner/repo/')).toBe('owner/repo');
|
|
16
|
+
});
|
|
17
|
+
it('preserves dots, underscores, slashes', () => {
|
|
18
|
+
expect(sanitizeName('my_file.md')).toBe('my_file.md');
|
|
19
|
+
expect(sanitizeName('owner/repo')).toBe('owner/repo');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('getInstallPath', () => {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const makeResource = (overrides) => ({
|
|
25
|
+
id: '1',
|
|
26
|
+
name: 'Test Resource',
|
|
27
|
+
slug: 'owner/test-resource',
|
|
28
|
+
type: 'Steering',
|
|
29
|
+
url: '',
|
|
30
|
+
githubRepo: 'owner/repo',
|
|
31
|
+
githubFile: 'architecture.md',
|
|
32
|
+
githubRawUrl: '',
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
it('puts steering files in .kiro/steering/', () => {
|
|
36
|
+
const result = getInstallPath(makeResource({ type: 'Steering', githubFile: 'architecture.md' }));
|
|
37
|
+
expect(result).toBe(join(cwd, '.kiro', 'steering', 'architecture.md'));
|
|
38
|
+
});
|
|
39
|
+
it('puts hooks in .kiro/hooks/', () => {
|
|
40
|
+
const result = getInstallPath(makeResource({ type: 'Hook', githubFile: 'on-save.kiro.hook' }));
|
|
41
|
+
expect(result).toBe(join(cwd, '.kiro', 'hooks', 'on-save.kiro.hook'));
|
|
42
|
+
});
|
|
43
|
+
it('puts powers in .kiro/powers/<slug>/', () => {
|
|
44
|
+
const result = getInstallPath(makeResource({ type: 'Power', githubFile: 'power.json', slug: 'owner/my-power' }));
|
|
45
|
+
expect(result).toBe(join(cwd, '.kiro', 'powers', 'owner/my-power', 'power.json'));
|
|
46
|
+
});
|
|
47
|
+
it('puts prompts in .kiro/prompts/', () => {
|
|
48
|
+
const result = getInstallPath(makeResource({ type: 'Prompt', githubFile: 'deploy.md' }));
|
|
49
|
+
expect(result).toBe(join(cwd, '.kiro', 'prompts', 'deploy.md'));
|
|
50
|
+
});
|
|
51
|
+
it('puts agents in .kiro/agents/', () => {
|
|
52
|
+
const result = getInstallPath(makeResource({ type: 'Agent', githubFile: 'reviewer.json' }));
|
|
53
|
+
expect(result).toBe(join(cwd, '.kiro', 'agents', 'reviewer.json'));
|
|
54
|
+
});
|
|
55
|
+
it('puts skills in .kiro/skills/<slug>/', () => {
|
|
56
|
+
const result = getInstallPath(makeResource({ type: 'Skill', githubFile: 'SKILL.md', slug: 'owner/my-skill' }));
|
|
57
|
+
expect(result).toBe(join(cwd, '.kiro', 'skills', 'owner/my-skill', 'SKILL.md'));
|
|
58
|
+
});
|
|
59
|
+
it('falls back to sanitized name when no githubFile', () => {
|
|
60
|
+
const result = getInstallPath(makeResource({ type: 'Steering', githubFile: '', name: 'My Resource' }));
|
|
61
|
+
expect(result).toBe(join(cwd, '.kiro', 'steering', 'my-resource.md'));
|
|
62
|
+
});
|
|
63
|
+
it('uses default path for unknown type', () => {
|
|
64
|
+
const result = getInstallPath(makeResource({ type: 'Unknown', githubFile: '' }));
|
|
65
|
+
expect(result).toContain(join('.kiro', 'resources'));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readdirSync, rmSync, statSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { trackCommand } from '../utils/telemetry.js';
|
|
6
|
+
const KIRO_DIRS = [
|
|
7
|
+
{ path: '.kiro/steering', type: 'Steering' },
|
|
8
|
+
{ path: '.kiro/hooks', type: 'Hook' },
|
|
9
|
+
{ path: '.kiro/powers', type: 'Power' },
|
|
10
|
+
{ path: '.kiro/prompts', type: 'Prompt' },
|
|
11
|
+
{ path: '.kiro/agents', type: 'Agent' },
|
|
12
|
+
{ path: '.kiro/skills', type: 'Skill' },
|
|
13
|
+
];
|
|
14
|
+
function isInteractive() {
|
|
15
|
+
return process.stdout.isTTY === true;
|
|
16
|
+
}
|
|
17
|
+
function getInstalledResources() {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const resources = [];
|
|
20
|
+
for (const dir of KIRO_DIRS) {
|
|
21
|
+
const fullPath = join(cwd, dir.path);
|
|
22
|
+
if (!existsSync(fullPath))
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
const entries = readdirSync(fullPath);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (entry.startsWith('.'))
|
|
28
|
+
continue;
|
|
29
|
+
const entryPath = join(fullPath, entry);
|
|
30
|
+
const stat = statSync(entryPath);
|
|
31
|
+
resources.push({
|
|
32
|
+
name: entry.replace(/\.(md|kiro\.hook|json)$/, ''),
|
|
33
|
+
path: entryPath,
|
|
34
|
+
type: dir.type,
|
|
35
|
+
isDir: stat.isDirectory(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Directory doesn't exist or can't be read
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return resources;
|
|
44
|
+
}
|
|
45
|
+
export async function removeResource(resourceName, options) {
|
|
46
|
+
const skipPrompts = options.yes || !isInteractive();
|
|
47
|
+
const resources = getInstalledResources();
|
|
48
|
+
if (resources.length === 0) {
|
|
49
|
+
p.log.warn('No installed resources found.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// If no resource specified and interactive, show selection
|
|
53
|
+
if (!resourceName) {
|
|
54
|
+
if (!isInteractive()) {
|
|
55
|
+
p.log.error('Resource name required in non-interactive mode.');
|
|
56
|
+
console.log(chalk.gray('Usage: kirohub remove <resource-name> -y'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const selected = await p.select({
|
|
60
|
+
message: 'Select resource to remove',
|
|
61
|
+
options: resources.map(r => ({
|
|
62
|
+
value: r.name,
|
|
63
|
+
label: r.name,
|
|
64
|
+
hint: r.type,
|
|
65
|
+
})),
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(selected)) {
|
|
68
|
+
p.cancel('Cancelled.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resourceName = selected;
|
|
72
|
+
}
|
|
73
|
+
// Find the resource
|
|
74
|
+
const resource = resources.find(r => r.name.toLowerCase() === resourceName.toLowerCase());
|
|
75
|
+
if (!resource) {
|
|
76
|
+
p.log.error(`Resource "${resourceName}" not found.`);
|
|
77
|
+
console.log(chalk.gray('Use `kirohub list` to see installed resources.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Confirm removal
|
|
81
|
+
if (!skipPrompts) {
|
|
82
|
+
p.note(`${resource.name} (${resource.type})\n${resource.path}`, 'Resource');
|
|
83
|
+
const proceed = await p.confirm({
|
|
84
|
+
message: 'Remove this resource?',
|
|
85
|
+
});
|
|
86
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
87
|
+
p.cancel('Cancelled.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const s = p.spinner();
|
|
92
|
+
s.start(`Removing ${resource.name}...`);
|
|
93
|
+
try {
|
|
94
|
+
rmSync(resource.path, { recursive: true });
|
|
95
|
+
trackCommand('remove');
|
|
96
|
+
s.stop(`Removed ${resource.name}`);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
s.stop(`Failed: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,39 +1,138 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { trackCommand } from '../utils/telemetry.js';
|
|
3
|
-
const
|
|
3
|
+
const KIROHUB_BASE = 'https://kirohub.dev';
|
|
4
|
+
export const TYPES = ['Steering', 'Hook', 'Power', 'Prompt', 'Agent', 'Skill'];
|
|
5
|
+
export const CATEGORIES = [
|
|
6
|
+
'aws', 'frontend', 'backend', 'api-design', 'documentation', 'devops',
|
|
7
|
+
'security', 'testing', 'database', 'ai-ml', 'architecture',
|
|
8
|
+
'developer-tools', 'typescript', 'python', 'java', 'rust', 'golang', 'csharp',
|
|
9
|
+
];
|
|
10
|
+
export function getApiUrl(options) {
|
|
11
|
+
// Use specific map if filtering by type or category
|
|
12
|
+
if (options.type) {
|
|
13
|
+
const typeMatch = TYPES.find(t => t.toLowerCase() === options.type.toLowerCase());
|
|
14
|
+
if (typeMatch)
|
|
15
|
+
return `${KIROHUB_BASE}/resource-map-${typeMatch.toLowerCase()}.json`;
|
|
16
|
+
}
|
|
17
|
+
if (options.category) {
|
|
18
|
+
const catMatch = CATEGORIES.find(c => c === options.category.toLowerCase());
|
|
19
|
+
if (catMatch)
|
|
20
|
+
return `${KIROHUB_BASE}/resource-map-category-${catMatch}.json`;
|
|
21
|
+
}
|
|
22
|
+
return `${KIROHUB_BASE}/resource-map.json`;
|
|
23
|
+
}
|
|
24
|
+
export async function fetchResources(query, options) {
|
|
25
|
+
const apiUrl = getApiUrl(options);
|
|
26
|
+
const response = await fetch(apiUrl);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Failed to fetch resource map: ${response.status} ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
let resources = await response.json();
|
|
31
|
+
if (options.type && options.category) {
|
|
32
|
+
const cat = options.category.toLowerCase();
|
|
33
|
+
resources = resources.filter(r => r.categories?.includes(cat));
|
|
34
|
+
}
|
|
35
|
+
const results = resources.filter(r => r.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
36
|
+
r.slug.toLowerCase().includes(query.toLowerCase()));
|
|
37
|
+
results.sort((a, b) => (b.overallScore ?? 0) - (a.overallScore ?? 0));
|
|
38
|
+
const limit = options.limit ? Math.max(1, parseInt(options.limit, 10) || 10) : 10;
|
|
39
|
+
return results.slice(0, limit);
|
|
40
|
+
}
|
|
4
41
|
export async function searchResources(query, options) {
|
|
5
42
|
try {
|
|
6
43
|
trackCommand('search');
|
|
7
|
-
|
|
44
|
+
// Validate type filter
|
|
45
|
+
if (options.type) {
|
|
46
|
+
const typeMatch = TYPES.find(t => t.toLowerCase() === options.type.toLowerCase());
|
|
47
|
+
if (!typeMatch) {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify({ error: `Unknown type: ${options.type}`, availableTypes: TYPES }));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log(chalk.yellow(`Unknown type "${options.type}". Available: ${TYPES.join(', ')}`));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Validate category filter
|
|
58
|
+
if (options.category) {
|
|
59
|
+
const catMatch = CATEGORIES.find(c => c === options.category.toLowerCase());
|
|
60
|
+
if (!catMatch) {
|
|
61
|
+
if (options.json) {
|
|
62
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}`, availableCategories: CATEGORIES }));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(chalk.yellow(`Unknown category "${options.category}".`));
|
|
66
|
+
console.log(chalk.gray(`Available: ${CATEGORIES.join(', ')}`));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const apiUrl = getApiUrl(options);
|
|
72
|
+
const response = await fetch(apiUrl);
|
|
8
73
|
if (!response.ok) {
|
|
9
74
|
throw new Error(`Failed to fetch resource map: ${response.status} ${response.statusText}`);
|
|
10
75
|
}
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
76
|
+
let resources = await response.json();
|
|
77
|
+
// Client-side category filter when type map was fetched (type takes URL priority)
|
|
78
|
+
if (options.type && options.category) {
|
|
79
|
+
const cat = options.category.toLowerCase();
|
|
80
|
+
resources = resources.filter(r => r.categories?.includes(cat));
|
|
16
81
|
}
|
|
17
82
|
// Search by name/slug
|
|
18
|
-
const results =
|
|
83
|
+
const results = resources.filter(r => r.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
19
84
|
r.slug.toLowerCase().includes(query.toLowerCase()));
|
|
85
|
+
// Sort by score if available
|
|
86
|
+
results.sort((a, b) => (b.overallScore ?? 0) - (a.overallScore ?? 0));
|
|
87
|
+
// Apply limit
|
|
88
|
+
const limit = Math.max(1, parseInt(options.limit, 10) || 10);
|
|
89
|
+
const limited = results.slice(0, limit);
|
|
90
|
+
// JSON output mode
|
|
91
|
+
if (options.json) {
|
|
92
|
+
console.log(JSON.stringify({
|
|
93
|
+
query,
|
|
94
|
+
total: results.length,
|
|
95
|
+
results: limited.map(r => ({
|
|
96
|
+
slug: r.slug,
|
|
97
|
+
name: r.name,
|
|
98
|
+
type: r.type,
|
|
99
|
+
categories: r.categories,
|
|
100
|
+
score: r.overallScore,
|
|
101
|
+
install: `npx kirohub add ${r.slug}`,
|
|
102
|
+
})),
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
20
106
|
if (results.length === 0) {
|
|
21
107
|
console.log(chalk.yellow(`No resources found for "${query}"`));
|
|
108
|
+
if (options.type)
|
|
109
|
+
console.log(chalk.gray(` type: ${options.type}`));
|
|
110
|
+
if (options.category)
|
|
111
|
+
console.log(chalk.gray(` category: ${options.category}`));
|
|
22
112
|
return;
|
|
23
113
|
}
|
|
24
114
|
console.log(chalk.bold(`Found ${results.length} resources:`));
|
|
25
115
|
console.log();
|
|
26
|
-
|
|
27
|
-
|
|
116
|
+
limited.forEach(resource => {
|
|
117
|
+
const score = resource.overallScore ? chalk.gray(` [${resource.overallScore}]`) : '';
|
|
118
|
+
console.log(chalk.green(`${resource.slug}`) + score);
|
|
28
119
|
console.log(chalk.gray(` Type: ${resource.type}`));
|
|
120
|
+
if (resource.categories?.length) {
|
|
121
|
+
console.log(chalk.gray(` Categories: ${resource.categories.join(', ')}`));
|
|
122
|
+
}
|
|
29
123
|
console.log(chalk.gray(` Install: npx kirohub add ${resource.slug}`));
|
|
30
124
|
console.log();
|
|
31
125
|
});
|
|
32
|
-
if (results.length >
|
|
33
|
-
console.log(chalk.gray(`... and ${results.length -
|
|
126
|
+
if (results.length > limit) {
|
|
127
|
+
console.log(chalk.gray(`... and ${results.length - limit} more (use --limit to show more)`));
|
|
34
128
|
}
|
|
35
129
|
}
|
|
36
130
|
catch (error) {
|
|
37
|
-
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.error(chalk.red(`Search failed: ${error.message}`));
|
|
136
|
+
}
|
|
38
137
|
}
|
|
39
138
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { getApiUrl, TYPES, CATEGORIES } from './search.js';
|
|
3
|
+
describe('getApiUrl', () => {
|
|
4
|
+
const base = 'https://kirohub.dev';
|
|
5
|
+
it('returns resource-map.json with no filters', () => {
|
|
6
|
+
expect(getApiUrl({})).toBe(`${base}/resource-map.json`);
|
|
7
|
+
});
|
|
8
|
+
it('returns type-specific map for type filter', () => {
|
|
9
|
+
expect(getApiUrl({ type: 'Steering' })).toBe(`${base}/steering.json`);
|
|
10
|
+
expect(getApiUrl({ type: 'Hook' })).toBe(`${base}/hook.json`);
|
|
11
|
+
expect(getApiUrl({ type: 'Power' })).toBe(`${base}/power.json`);
|
|
12
|
+
});
|
|
13
|
+
it('is case-insensitive for type', () => {
|
|
14
|
+
expect(getApiUrl({ type: 'steering' })).toBe(`${base}/steering.json`);
|
|
15
|
+
expect(getApiUrl({ type: 'HOOK' })).toBe(`${base}/hook.json`);
|
|
16
|
+
});
|
|
17
|
+
it('returns category-specific map for category filter', () => {
|
|
18
|
+
expect(getApiUrl({ category: 'aws' })).toBe(`${base}/aws.json`);
|
|
19
|
+
expect(getApiUrl({ category: 'frontend' })).toBe(`${base}/frontend.json`);
|
|
20
|
+
});
|
|
21
|
+
it('type takes priority over category for URL', () => {
|
|
22
|
+
expect(getApiUrl({ type: 'Steering', category: 'aws' })).toBe(`${base}/steering.json`);
|
|
23
|
+
});
|
|
24
|
+
it('falls back to resource-map.json for unknown type', () => {
|
|
25
|
+
expect(getApiUrl({ type: 'Unknown' })).toBe(`${base}/resource-map.json`);
|
|
26
|
+
});
|
|
27
|
+
it('falls back to resource-map.json for unknown category', () => {
|
|
28
|
+
expect(getApiUrl({ category: 'nonexistent' })).toBe(`${base}/resource-map.json`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('searchResources JSON output', () => {
|
|
32
|
+
const mockResources = [
|
|
33
|
+
{ id: '1', name: 'AWS Lambda Steering', slug: 'user/aws-lambda', type: 'Steering', url: '', categories: ['aws', 'backend'], overallScore: 90 },
|
|
34
|
+
{ id: '2', name: 'React Hooks Guide', slug: 'user/react-hooks', type: 'Steering', url: '', categories: ['frontend'], overallScore: 85 },
|
|
35
|
+
{ id: '3', name: 'AWS DynamoDB Hook', slug: 'user/aws-dynamo', type: 'Hook', url: '', categories: ['aws', 'database'], overallScore: 80 },
|
|
36
|
+
];
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
vi.resetModules();
|
|
40
|
+
});
|
|
41
|
+
it('filters by query and outputs JSON', async () => {
|
|
42
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: () => Promise.resolve(mockResources),
|
|
45
|
+
}));
|
|
46
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
47
|
+
const { searchResources } = await import('./search.js');
|
|
48
|
+
await searchResources('aws', { json: true });
|
|
49
|
+
const output = JSON.parse(spy.mock.calls[0][0]);
|
|
50
|
+
expect(output.total).toBe(2);
|
|
51
|
+
expect(output.results[0].slug).toBe('user/aws-lambda');
|
|
52
|
+
expect(output.results[1].slug).toBe('user/aws-dynamo');
|
|
53
|
+
});
|
|
54
|
+
it('applies limit', async () => {
|
|
55
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
56
|
+
ok: true,
|
|
57
|
+
json: () => Promise.resolve(mockResources),
|
|
58
|
+
}));
|
|
59
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
60
|
+
const { searchResources } = await import('./search.js');
|
|
61
|
+
await searchResources('', { json: true, limit: '1' });
|
|
62
|
+
const output = JSON.parse(spy.mock.calls[0][0]);
|
|
63
|
+
expect(output.total).toBe(3);
|
|
64
|
+
expect(output.results).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
it('defaults limit to 10 for invalid value', async () => {
|
|
67
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: () => Promise.resolve(mockResources),
|
|
70
|
+
}));
|
|
71
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
72
|
+
const { searchResources } = await import('./search.js');
|
|
73
|
+
await searchResources('', { json: true, limit: 'foo' });
|
|
74
|
+
const output = JSON.parse(spy.mock.calls[0][0]);
|
|
75
|
+
expect(output.results).toHaveLength(3); // all 3, since default limit 10 > 3
|
|
76
|
+
});
|
|
77
|
+
it('sorts by score descending', async () => {
|
|
78
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
79
|
+
ok: true,
|
|
80
|
+
json: () => Promise.resolve(mockResources),
|
|
81
|
+
}));
|
|
82
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
83
|
+
const { searchResources } = await import('./search.js');
|
|
84
|
+
await searchResources('', { json: true });
|
|
85
|
+
const output = JSON.parse(spy.mock.calls[0][0]);
|
|
86
|
+
const scores = output.results.map((r) => r.score);
|
|
87
|
+
expect(scores).toEqual([90, 85, 80]);
|
|
88
|
+
});
|
|
89
|
+
it('applies client-side category filter when both type and category set', async () => {
|
|
90
|
+
// When fetching steering.json, only Steering resources come back
|
|
91
|
+
const steeringOnly = mockResources.filter(r => r.type === 'Steering');
|
|
92
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
93
|
+
ok: true,
|
|
94
|
+
json: () => Promise.resolve(steeringOnly),
|
|
95
|
+
}));
|
|
96
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
97
|
+
const { searchResources } = await import('./search.js');
|
|
98
|
+
await searchResources('', { json: true, type: 'Steering', category: 'aws' });
|
|
99
|
+
const output = JSON.parse(spy.mock.calls[0][0]);
|
|
100
|
+
expect(output.total).toBe(1);
|
|
101
|
+
expect(output.results[0].slug).toBe('user/aws-lambda');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('constants', () => {
|
|
105
|
+
it('has 6 types', () => {
|
|
106
|
+
expect(TYPES).toHaveLength(6);
|
|
107
|
+
expect(TYPES).toContain('Steering');
|
|
108
|
+
expect(TYPES).toContain('Skill');
|
|
109
|
+
});
|
|
110
|
+
it('has 18 categories', () => {
|
|
111
|
+
expect(CATEGORIES).toHaveLength(18);
|
|
112
|
+
expect(CATEGORIES).toContain('aws');
|
|
113
|
+
expect(CATEGORIES).toContain('csharp');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
const RESET = '\x1b[0m';
|
|
3
|
+
const DIM = '\x1b[38;5;102m';
|
|
4
|
+
const TEXT = '\x1b[38;5;145m';
|
|
5
|
+
const LOGO_LINES = [
|
|
6
|
+
'██╗ ██╗██╗██████╗ ██████╗ ██╗ ██╗██╗ ██╗██████╗ ',
|
|
7
|
+
'██║ ██╔╝██║██╔══██╗██╔═══██╗ ██║ ██║██║ ██║██╔══██╗',
|
|
8
|
+
'█████╔╝ ██║██████╔╝██║ ██║ ███████║██║ ██║██████╔╝',
|
|
9
|
+
'██╔═██╗ ██║██╔══██╗██║ ██║ ██╔══██║██║ ██║██╔══██╗',
|
|
10
|
+
'██║ ██╗██║██║ ██║╚██████╔╝ ██║ ██║╚██████╔╝██████╔╝',
|
|
11
|
+
'╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
|
|
12
|
+
];
|
|
13
|
+
const GRAYS = [
|
|
14
|
+
'\x1b[38;5;250m',
|
|
15
|
+
'\x1b[38;5;248m',
|
|
16
|
+
'\x1b[38;5;245m',
|
|
17
|
+
'\x1b[38;5;243m',
|
|
18
|
+
'\x1b[38;5;240m',
|
|
19
|
+
'\x1b[38;5;238m',
|
|
20
|
+
];
|
|
21
|
+
export function showLogo() {
|
|
22
|
+
console.log();
|
|
23
|
+
LOGO_LINES.forEach((line, i) => {
|
|
24
|
+
console.log(`${GRAYS[i]}${line}${RESET}`);
|
|
25
|
+
});
|
|
26
|
+
console.log();
|
|
27
|
+
}
|
|
28
|
+
export function showBanner() {
|
|
29
|
+
showLogo();
|
|
30
|
+
p.intro('The Kiro resource hub');
|
|
31
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx kirohub add ${DIM}<slug>${RESET} ${DIM}Install a resource${RESET}`);
|
|
32
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx kirohub list${RESET} ${DIM}List installed resources${RESET}`);
|
|
33
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx kirohub search ${DIM}<query>${RESET} ${DIM}Search resources${RESET}`);
|
|
34
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx kirohub remove${RESET} ${DIM}Remove resources${RESET}`);
|
|
35
|
+
console.log();
|
|
36
|
+
p.outro(`Discover more at ${TEXT}https://kirohub.dev/${RESET}`);
|
|
37
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kirohub",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI for installing Kiro resources from KiroHub",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc -p tsconfig.json",
|
|
12
12
|
"dev": "tsx src/cli.ts",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
13
15
|
"smoke": "node dist/cli.js --help",
|
|
14
16
|
"postinstall": "node postinstall.cjs",
|
|
15
17
|
"prepublishOnly": "npm run build"
|
|
@@ -26,19 +28,21 @@
|
|
|
26
28
|
"author": "Kiro Community",
|
|
27
29
|
"license": "MIT",
|
|
28
30
|
"dependencies": {
|
|
29
|
-
"
|
|
31
|
+
"@clack/prompts": "^1.0.1",
|
|
30
32
|
"chalk": "^5.3.0",
|
|
31
|
-
"
|
|
33
|
+
"commander": "^12.0.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@types/node": "^22.0.0",
|
|
35
37
|
"tsx": "^4.0.0",
|
|
36
|
-
"typescript": "^5.8.2"
|
|
38
|
+
"typescript": "^5.8.2",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
37
40
|
},
|
|
38
41
|
"files": [
|
|
39
42
|
"dist/**/*",
|
|
40
43
|
"postinstall.cjs",
|
|
41
|
-
"README.md"
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md"
|
|
42
46
|
],
|
|
43
47
|
"engines": {
|
|
44
48
|
"node": ">=18"
|