kirohub 1.0.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/README.md +36 -0
- package/dist/cli.js +25 -0
- package/dist/commands/add.js +79 -0
- package/dist/commands/list.js +37 -0
- package/dist/commands/search.js +39 -0
- package/dist/utils/telemetry.js +52 -0
- package/package.json +38 -0
- package/postinstall.cjs +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# KiroHub CLI
|
|
2
|
+
|
|
3
|
+
Install and discover Kiro resources from the command line.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx kirohub search <query>
|
|
9
|
+
npx kirohub add <owner/resource>
|
|
10
|
+
npx kirohub list
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx kirohub search steering
|
|
17
|
+
npx kirohub add owner/my-steering
|
|
18
|
+
npx kirohub add kirodotdev/power-builder
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Publish (Maintainers)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd packages/cli
|
|
25
|
+
npm install
|
|
26
|
+
npm run build
|
|
27
|
+
node dist/cli.js --help
|
|
28
|
+
npm login
|
|
29
|
+
npm publish --access public
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Test the published CLI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx kirohub --help
|
|
36
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { addResource } from './commands/add.js';
|
|
4
|
+
import { listResources } from './commands/list.js';
|
|
5
|
+
import { searchResources } from './commands/search.js';
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('kirohub')
|
|
9
|
+
.description('CLI for installing Kiro resources from KiroHub')
|
|
10
|
+
.version('1.0.0');
|
|
11
|
+
program
|
|
12
|
+
.command('add <resource>')
|
|
13
|
+
.description('Install a Kiro resource (e.g., owner/resource-name)')
|
|
14
|
+
.option('--no-telemetry', 'Disable telemetry')
|
|
15
|
+
.action(addResource);
|
|
16
|
+
program
|
|
17
|
+
.command('list')
|
|
18
|
+
.description('List installed resources')
|
|
19
|
+
.action(listResources);
|
|
20
|
+
program
|
|
21
|
+
.command('search <query>')
|
|
22
|
+
.description('Search available resources')
|
|
23
|
+
.option('-t, --type <type>', 'Filter by type')
|
|
24
|
+
.action(searchResources);
|
|
25
|
+
program.parse();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname, basename } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { trackResourceInstall, trackCommand } from '../utils/telemetry.js';
|
|
6
|
+
const KIROHUB_API = 'https://kirohub.dev/resource-map.json';
|
|
7
|
+
export async function addResource(resourceSlug, options) {
|
|
8
|
+
const spinner = ora('Fetching resource...').start();
|
|
9
|
+
try {
|
|
10
|
+
// Fetch resource map
|
|
11
|
+
const response = await fetch(KIROHUB_API);
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`Failed to fetch resource map: ${response.status} ${response.statusText}`);
|
|
14
|
+
}
|
|
15
|
+
const resources = await response.json();
|
|
16
|
+
// Find resource by slug
|
|
17
|
+
const resource = resources.find(r => r.slug === resourceSlug);
|
|
18
|
+
if (!resource) {
|
|
19
|
+
spinner.fail(`Resource "${resourceSlug}" not found`);
|
|
20
|
+
console.log(chalk.gray('Available resources:'));
|
|
21
|
+
resources.slice(0, 5).forEach(r => {
|
|
22
|
+
console.log(chalk.gray(` ${r.slug} (${r.type})`));
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
spinner.text = `Installing ${resource.name}...`;
|
|
27
|
+
// Fetch file content
|
|
28
|
+
const fileResponse = await fetch(resource.githubRawUrl);
|
|
29
|
+
if (!fileResponse.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch resource file: ${fileResponse.status} ${fileResponse.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const content = await fileResponse.text();
|
|
33
|
+
// Determine installation path
|
|
34
|
+
const installPath = getInstallPath(resource);
|
|
35
|
+
// Create directory if needed
|
|
36
|
+
mkdirSync(dirname(installPath), { recursive: true });
|
|
37
|
+
// Write file
|
|
38
|
+
writeFileSync(installPath, content);
|
|
39
|
+
// Track telemetry
|
|
40
|
+
if (!options.noTelemetry) {
|
|
41
|
+
trackResourceInstall(resource.slug, resource.type);
|
|
42
|
+
trackCommand('add');
|
|
43
|
+
}
|
|
44
|
+
spinner.succeed(`Installed ${chalk.green(resource.name)} to ${chalk.gray(installPath)}`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
spinner.fail(`Failed to install resource: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getInstallPath(resource) {
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const safeName = sanitizeName(resource.name || resource.slug);
|
|
53
|
+
const safeSlug = sanitizeName(resource.slug);
|
|
54
|
+
const fileNameFromRepo = basename(resource.githubFile || '');
|
|
55
|
+
switch (resource.type.toLowerCase()) {
|
|
56
|
+
case 'steering':
|
|
57
|
+
return join(cwd, '.kiro', 'steering', fileNameFromRepo || `${safeName}.md`);
|
|
58
|
+
case 'hook':
|
|
59
|
+
return join(cwd, '.kiro', 'hooks', fileNameFromRepo || `${safeName}.kiro.hook`);
|
|
60
|
+
case 'power':
|
|
61
|
+
return join(cwd, '.kiro', 'powers', safeSlug || safeName, fileNameFromRepo || 'POWER.md');
|
|
62
|
+
case 'prompt':
|
|
63
|
+
return join(cwd, '.kiro', 'prompts', fileNameFromRepo || `${safeName}.md`);
|
|
64
|
+
case 'agent':
|
|
65
|
+
return join(cwd, '.kiro', 'agents', fileNameFromRepo || `${safeName}.json`);
|
|
66
|
+
case 'skill':
|
|
67
|
+
return join(cwd, '.kiro', 'skills', safeSlug || safeName, fileNameFromRepo || 'SKILL.md');
|
|
68
|
+
default:
|
|
69
|
+
return join(cwd, '.kiro', 'resources', safeName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function sanitizeName(value) {
|
|
73
|
+
return value
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/[^a-z0-9-_./]+/g, "-")
|
|
77
|
+
.replace(/-+/g, "-")
|
|
78
|
+
.replace(/^\/+|\/+$/g, "");
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { trackCommand } from '../utils/telemetry.js';
|
|
5
|
+
export async function listResources() {
|
|
6
|
+
trackCommand('list');
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const kiroDir = join(cwd, '.kiro');
|
|
9
|
+
if (!existsSync(kiroDir)) {
|
|
10
|
+
console.log(chalk.yellow('No .kiro directory found. Install resources first.'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(chalk.bold('Installed Kiro resources:'));
|
|
14
|
+
console.log();
|
|
15
|
+
const categories = [
|
|
16
|
+
{ name: 'Steering', dir: 'steering', ext: '.md' },
|
|
17
|
+
{ name: 'Hooks', dir: 'hooks', ext: '.kiro.hook' },
|
|
18
|
+
{ name: 'Powers', dir: 'powers', ext: null },
|
|
19
|
+
{ name: 'Prompts', dir: 'prompts', ext: '.md' },
|
|
20
|
+
{ name: 'Agents', dir: 'agents', ext: '.json' },
|
|
21
|
+
{ name: 'Skills', dir: 'skills', ext: null }
|
|
22
|
+
];
|
|
23
|
+
categories.forEach(category => {
|
|
24
|
+
const categoryDir = join(kiroDir, category.dir);
|
|
25
|
+
if (!existsSync(categoryDir))
|
|
26
|
+
return;
|
|
27
|
+
const items = readdirSync(categoryDir);
|
|
28
|
+
if (items.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
console.log(chalk.green(`${category.name}:`));
|
|
31
|
+
items.forEach(item => {
|
|
32
|
+
const name = category.ext ? item.replace(category.ext, '') : item;
|
|
33
|
+
console.log(chalk.gray(` ${name}`));
|
|
34
|
+
});
|
|
35
|
+
console.log();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { trackCommand } from '../utils/telemetry.js';
|
|
3
|
+
const KIROHUB_API = 'https://kirohub.dev/resource-map.json';
|
|
4
|
+
export async function searchResources(query, options) {
|
|
5
|
+
try {
|
|
6
|
+
trackCommand('search');
|
|
7
|
+
const response = await fetch(KIROHUB_API);
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
throw new Error(`Failed to fetch resource map: ${response.status} ${response.statusText}`);
|
|
10
|
+
}
|
|
11
|
+
const resources = await response.json();
|
|
12
|
+
// Filter by type if specified
|
|
13
|
+
let filtered = resources;
|
|
14
|
+
if (options.type) {
|
|
15
|
+
filtered = resources.filter(r => r.type.toLowerCase() === options.type.toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
// Search by name/slug
|
|
18
|
+
const results = filtered.filter(r => r.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
19
|
+
r.slug.toLowerCase().includes(query.toLowerCase()));
|
|
20
|
+
if (results.length === 0) {
|
|
21
|
+
console.log(chalk.yellow(`No resources found for "${query}"`));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(chalk.bold(`Found ${results.length} resources:`));
|
|
25
|
+
console.log();
|
|
26
|
+
results.slice(0, 10).forEach(resource => {
|
|
27
|
+
console.log(chalk.green(`${resource.slug}`));
|
|
28
|
+
console.log(chalk.gray(` Type: ${resource.type}`));
|
|
29
|
+
console.log(chalk.gray(` Install: npx kirohub add ${resource.slug}`));
|
|
30
|
+
console.log();
|
|
31
|
+
});
|
|
32
|
+
if (results.length > 10) {
|
|
33
|
+
console.log(chalk.gray(`... and ${results.length - 10} more`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error(chalk.red(`Search failed: ${error.message}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Telemetry endpoint — update this after deploying the Lambda Function URL
|
|
2
|
+
// IMPORTANT: Replace this URL with the actual Lambda Function URL before publishing to npm
|
|
3
|
+
const TELEMETRY_ENDPOINT = 'https://kirohub.dev/api/telemetry';
|
|
4
|
+
const TELEMETRY_TIMEOUT = 1500;
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const { version: CLI_VERSION } = require('../../package.json');
|
|
8
|
+
function isTelemetryDisabled() {
|
|
9
|
+
return (process.env.DISABLE_TELEMETRY === '1' ||
|
|
10
|
+
process.env.KIROHUB_DISABLE_TELEMETRY === '1' ||
|
|
11
|
+
process.env.NO_TELEMETRY === '1');
|
|
12
|
+
}
|
|
13
|
+
export async function sendTelemetry(payload) {
|
|
14
|
+
if (isTelemetryDisabled())
|
|
15
|
+
return;
|
|
16
|
+
if (typeof fetch !== 'function')
|
|
17
|
+
return;
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT);
|
|
20
|
+
try {
|
|
21
|
+
await fetch(TELEMETRY_ENDPOINT, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify(payload),
|
|
25
|
+
signal: controller.signal,
|
|
26
|
+
keepalive: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Ignore telemetry failures
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function trackResourceInstall(resourceSlug, resourceType) {
|
|
37
|
+
await sendTelemetry({
|
|
38
|
+
eventType: 'resource_install',
|
|
39
|
+
resourceSlug,
|
|
40
|
+
resourceType,
|
|
41
|
+
cliVersion: CLI_VERSION,
|
|
42
|
+
source: 'cli',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function trackCommand(command) {
|
|
46
|
+
await sendTelemetry({
|
|
47
|
+
eventType: 'command_usage',
|
|
48
|
+
command,
|
|
49
|
+
cliVersion: CLI_VERSION,
|
|
50
|
+
source: 'cli',
|
|
51
|
+
});
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kirohub",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for installing Kiro resources from KiroHub",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kirohub": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"smoke": "node dist/cli.js --help",
|
|
14
|
+
"postinstall": "node postinstall.cjs",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["kiro", "ai", "cli", "resources", "steering", "hooks", "powers"],
|
|
18
|
+
"author": "Kiro Community",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"ora": "^8.0.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"tsx": "^4.0.0",
|
|
28
|
+
"typescript": "^5.8.2"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/**/*",
|
|
32
|
+
"postinstall.cjs",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/postinstall.cjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// TODO: Update with actual Lambda Function URL after deployment
|
|
2
|
+
// IMPORTANT: Replace this URL with the actual Lambda Function URL before publishing to npm
|
|
3
|
+
const TELEMETRY_ENDPOINT = "https://kirohub.dev/api/telemetry";
|
|
4
|
+
|
|
5
|
+
function isTelemetryDisabled() {
|
|
6
|
+
return (
|
|
7
|
+
process.env.DISABLE_TELEMETRY === "1" ||
|
|
8
|
+
process.env.KIROHUB_DISABLE_TELEMETRY === "1" ||
|
|
9
|
+
process.env.NO_TELEMETRY === "1"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function detectInstallSource(userAgent) {
|
|
14
|
+
const value = (userAgent || "").toLowerCase();
|
|
15
|
+
if (value.includes("npx")) return "npx";
|
|
16
|
+
if (value.includes("npm")) return "npm";
|
|
17
|
+
if (value.includes("yarn")) return "yarn";
|
|
18
|
+
if (value.includes("pnpm")) return "pnpm";
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function sendInstallTelemetry() {
|
|
23
|
+
if (isTelemetryDisabled()) return;
|
|
24
|
+
if (typeof fetch !== "function") return;
|
|
25
|
+
|
|
26
|
+
const payload = {
|
|
27
|
+
eventType: "cli_install",
|
|
28
|
+
cliVersion: process.env.npm_package_version || "unknown",
|
|
29
|
+
source: detectInstallSource(process.env.npm_config_user_agent),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await fetch(TELEMETRY_ENDPOINT, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify(payload),
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
keepalive: true,
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore telemetry failures
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
sendInstallTelemetry();
|