kiroo 0.2.0 → 0.3.4
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/bin/kiroo.js +49 -4
- package/package.json +1 -1
- package/src/executor.js +42 -0
- package/src/import.js +113 -0
- package/src/stats.js +73 -0
- package/src/storage.js +12 -1
package/bin/kiroo.js
CHANGED
|
@@ -8,14 +8,16 @@ import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.j
|
|
|
8
8
|
import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
|
|
9
9
|
// import { showGraph } from '../src/graph.js';
|
|
10
10
|
import { initProject } from '../src/init.js';
|
|
11
|
-
|
|
11
|
+
import { showStats } from '../src/stats.js';
|
|
12
|
+
import { handleImport } from '../src/import.js';
|
|
13
|
+
import { clearAllInteractions } from '../src/storage.js';
|
|
12
14
|
|
|
13
15
|
const program = new Command();
|
|
14
16
|
|
|
15
17
|
program
|
|
16
18
|
.name('kiroo')
|
|
17
19
|
.description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
|
|
18
|
-
.version('0.
|
|
20
|
+
.version('0.3.4');
|
|
19
21
|
|
|
20
22
|
// Init command
|
|
21
23
|
program
|
|
@@ -58,6 +60,28 @@ program
|
|
|
58
60
|
await replayInteraction(id);
|
|
59
61
|
});
|
|
60
62
|
|
|
63
|
+
// Clear command
|
|
64
|
+
program
|
|
65
|
+
.command('clear')
|
|
66
|
+
.description('Clear all stored interaction history')
|
|
67
|
+
.option('-f, --force', 'Force clear without confirmation')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
if (!options.force) {
|
|
70
|
+
const inquirer = (await import('inquirer')).default;
|
|
71
|
+
const { confirm } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'confirm',
|
|
75
|
+
message: chalk.red('Are you sure you want to clear all history?'),
|
|
76
|
+
default: false
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
if (!confirm) return;
|
|
80
|
+
}
|
|
81
|
+
clearAllInteractions();
|
|
82
|
+
console.log(chalk.green('\n ✨ History cleared successfully.\n'));
|
|
83
|
+
});
|
|
84
|
+
|
|
61
85
|
// Environment commands
|
|
62
86
|
const env = program.command('env').description('Environment management');
|
|
63
87
|
|
|
@@ -105,6 +129,29 @@ snapshot
|
|
|
105
129
|
await compareSnapshots(tag1, tag2);
|
|
106
130
|
});
|
|
107
131
|
|
|
132
|
+
// Import command
|
|
133
|
+
program
|
|
134
|
+
.command('import')
|
|
135
|
+
.description('Import a request from a cURL command (opens editor if no command is provided)')
|
|
136
|
+
.allowUnknownOption()
|
|
137
|
+
.action(async (_, command) => {
|
|
138
|
+
if (command.args.length > 0) {
|
|
139
|
+
// Pass tokens directly to handleImport
|
|
140
|
+
await handleImport(command.args);
|
|
141
|
+
} else {
|
|
142
|
+
const inquirer = (await import('inquirer')).default;
|
|
143
|
+
const response = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'editor',
|
|
146
|
+
name: 'curl',
|
|
147
|
+
message: 'Paste your cURL command here (opens your default editor):',
|
|
148
|
+
validate: (input) => input.trim().length > 0 || 'Please enter a cURL command'
|
|
149
|
+
}
|
|
150
|
+
]);
|
|
151
|
+
await handleImport(response.curl);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
108
155
|
// Graph command
|
|
109
156
|
/*
|
|
110
157
|
program
|
|
@@ -116,14 +163,12 @@ program
|
|
|
116
163
|
*/
|
|
117
164
|
|
|
118
165
|
// Stats command
|
|
119
|
-
/*
|
|
120
166
|
program
|
|
121
167
|
.command('stats')
|
|
122
168
|
.description('Show usage statistics')
|
|
123
169
|
.action(async () => {
|
|
124
170
|
await showStats();
|
|
125
171
|
});
|
|
126
|
-
*/
|
|
127
172
|
|
|
128
173
|
// Error handling
|
|
129
174
|
program.exitOverride();
|
package/package.json
CHANGED
package/src/executor.js
CHANGED
|
@@ -55,6 +55,48 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
55
55
|
// Apply replacements to URL
|
|
56
56
|
url = applyEnvReplacements(url, currentEnvVars);
|
|
57
57
|
|
|
58
|
+
// Auto-BaseURL logic
|
|
59
|
+
if (currentEnvVars.baseUrl) {
|
|
60
|
+
let isRelative = false;
|
|
61
|
+
let pathPart = url;
|
|
62
|
+
|
|
63
|
+
// 1. Direct relative path
|
|
64
|
+
if (url.startsWith('/')) {
|
|
65
|
+
isRelative = true;
|
|
66
|
+
}
|
|
67
|
+
// 2. Windows Git Bash conversion: Detect "C:/..." style paths with no protocol
|
|
68
|
+
else if (process.platform === 'win32' && /^[a-zA-Z]:[/\\]/.test(url) && !url.includes('://')) {
|
|
69
|
+
isRelative = true;
|
|
70
|
+
// Extract the part after the drive letter and potential Git Bash root
|
|
71
|
+
// We look for common markers or just the first segment that looks like a path
|
|
72
|
+
// Most reliable for Git Bash: The user's path is at the end.
|
|
73
|
+
// We'll try to find the /api, /v1, etc., or just fallback to the full path after the first few segments
|
|
74
|
+
const segments = url.split(/[/\\]/);
|
|
75
|
+
const apiIdx = segments.findIndex(s => s === 'api' || s === 'v1' || s === 'v2');
|
|
76
|
+
if (apiIdx !== -1) {
|
|
77
|
+
pathPart = '/' + segments.slice(apiIdx).join('/');
|
|
78
|
+
} else {
|
|
79
|
+
// Fallback: If we can't find a marker, it's hard to guess safely,
|
|
80
|
+
// but we can try to strip the drive letter and first few segments
|
|
81
|
+
// In Git Bash, it's usually C:/Program Files/Git/api...
|
|
82
|
+
// Let's at least strip the drive letter root
|
|
83
|
+
pathPart = url.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
|
|
84
|
+
if (!pathPart.startsWith('/')) pathPart = '/' + pathPart;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 3. No leading slash but doesn't look like a host (no dots, no protocol)
|
|
88
|
+
else if (!url.includes('://') && !url.includes('.') && !url.includes(':') && !url.startsWith('localhost')) {
|
|
89
|
+
isRelative = true;
|
|
90
|
+
pathPart = '/' + url;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isRelative) {
|
|
94
|
+
const baseUrl = currentEnvVars.baseUrl;
|
|
95
|
+
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
96
|
+
url = normalizedBaseUrl + (pathPart.startsWith('/') ? pathPart : '/' + pathPart);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
58
100
|
// Parse headers
|
|
59
101
|
const headers = {};
|
|
60
102
|
if (options.header) {
|
package/src/import.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { executeRequest } from './executor.js';
|
|
3
|
+
|
|
4
|
+
export async function handleImport(input) {
|
|
5
|
+
try {
|
|
6
|
+
let tokens = [];
|
|
7
|
+
if (Array.isArray(input)) {
|
|
8
|
+
tokens = input;
|
|
9
|
+
} else {
|
|
10
|
+
tokens = stringToTokens(input);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parsed = parseCurlTokens(tokens);
|
|
14
|
+
|
|
15
|
+
console.log(chalk.cyan('\n 📥 Parsed cURL Command:'));
|
|
16
|
+
console.log(chalk.gray(` Method: `), chalk.white(parsed.method));
|
|
17
|
+
console.log(chalk.gray(` URL: `), chalk.white(parsed.url));
|
|
18
|
+
console.log(chalk.gray(` Headers:`), chalk.white(Object.keys(parsed.headers).length));
|
|
19
|
+
if (parsed.body) {
|
|
20
|
+
console.log(chalk.gray(` Body: `), chalk.white('Present'));
|
|
21
|
+
}
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
// Convert parsed object to options format for executeRequest
|
|
25
|
+
const options = {
|
|
26
|
+
header: Object.entries(parsed.headers).map(([k, v]) => `${k}: ${v}`),
|
|
27
|
+
data: parsed.body
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await executeRequest(parsed.method, parsed.url, options);
|
|
31
|
+
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(chalk.red('\n ✗ Import failed:'), error.message, '\n');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stringToTokens(curlString) {
|
|
38
|
+
// Clean up shell artifacts (Windows ^, backslashes for line continuation)
|
|
39
|
+
let cleanStr = curlString.replace(/\^/g, '').replace(/\\\n/g, ' ').trim();
|
|
40
|
+
|
|
41
|
+
const tokens = [];
|
|
42
|
+
let currentToken = '';
|
|
43
|
+
let inQuotes = false;
|
|
44
|
+
let quoteChar = '';
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < cleanStr.length; i++) {
|
|
47
|
+
const char = cleanStr[i];
|
|
48
|
+
if ((char === "'" || char === '"') && (i === 0 || cleanStr[i - 1] !== '\\')) {
|
|
49
|
+
if (inQuotes && char === quoteChar) {
|
|
50
|
+
inQuotes = false;
|
|
51
|
+
tokens.push(currentToken);
|
|
52
|
+
currentToken = '';
|
|
53
|
+
quoteChar = '';
|
|
54
|
+
} else if (!inQuotes) {
|
|
55
|
+
inQuotes = true;
|
|
56
|
+
quoteChar = char;
|
|
57
|
+
} else {
|
|
58
|
+
currentToken += char;
|
|
59
|
+
}
|
|
60
|
+
} else if (char === ' ' && !inQuotes) {
|
|
61
|
+
if (currentToken) {
|
|
62
|
+
tokens.push(currentToken);
|
|
63
|
+
currentToken = '';
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
currentToken += char;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (currentToken) tokens.push(currentToken);
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseCurlTokens(tokens) {
|
|
74
|
+
const result = {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
url: '',
|
|
77
|
+
headers: {},
|
|
78
|
+
body: ''
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
82
|
+
const token = tokens[i].trim();
|
|
83
|
+
if (!token) continue;
|
|
84
|
+
|
|
85
|
+
if (token === '-X' || token === '--request') {
|
|
86
|
+
result.method = tokens[++i].toUpperCase();
|
|
87
|
+
} else if (token === '-H' || token === '--header') {
|
|
88
|
+
const headerStr = tokens[++i];
|
|
89
|
+
if (!headerStr) continue;
|
|
90
|
+
const colonIdx = headerStr.indexOf(':');
|
|
91
|
+
if (colonIdx !== -1) {
|
|
92
|
+
const key = headerStr.substring(0, colonIdx).trim();
|
|
93
|
+
const value = headerStr.substring(colonIdx + 1).trim();
|
|
94
|
+
result.headers[key] = value;
|
|
95
|
+
}
|
|
96
|
+
} else if (token === '-d' || token === '--data' || token === '--data-raw' || token === '--data-binary') {
|
|
97
|
+
result.body = tokens[++i];
|
|
98
|
+
if (result.method === 'GET') result.method = 'POST';
|
|
99
|
+
} else if (token.includes('://') || (token.startsWith('localhost') || token.startsWith('127.0.0.1'))) {
|
|
100
|
+
if (!result.url) result.url = token;
|
|
101
|
+
} else if (token === 'curl' || token.startsWith('-')) {
|
|
102
|
+
continue;
|
|
103
|
+
} else {
|
|
104
|
+
if (!result.url) result.url = token;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.url) {
|
|
109
|
+
result.url = result.url.replace(/^["']|["']$/g, '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
package/src/stats.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { getAllInteractions } from './storage.js';
|
|
4
|
+
|
|
5
|
+
export function showStats() {
|
|
6
|
+
const interactions = getAllInteractions();
|
|
7
|
+
|
|
8
|
+
if (interactions.length === 0) {
|
|
9
|
+
console.log(chalk.yellow('\n No interactions found to analyze.'));
|
|
10
|
+
console.log(chalk.gray(' Run some requests first to see the magic! ✨\n'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(chalk.cyan.bold('\n 📊 Kiroo Analytics Dashboard\n'));
|
|
15
|
+
|
|
16
|
+
// 1. General Metrics
|
|
17
|
+
const total = interactions.length;
|
|
18
|
+
const successes = interactions.filter(i => i.response.status >= 200 && i.response.status < 300).length;
|
|
19
|
+
const successRate = ((successes / total) * 100).toFixed(1);
|
|
20
|
+
const avgDuration = (interactions.reduce((acc, i) => acc + i.metadata.duration_ms, 0) / total).toFixed(0);
|
|
21
|
+
|
|
22
|
+
const generalTable = new Table();
|
|
23
|
+
generalTable.push(
|
|
24
|
+
{ [chalk.white('Total Requests')]: chalk.cyan(total) },
|
|
25
|
+
{ [chalk.white('Success Rate')]: successRate >= 80 ? chalk.green(successRate + '%') : chalk.yellow(successRate + '%') },
|
|
26
|
+
{ [chalk.white('Avg. Duration')]: chalk.white(avgDuration + 'ms') }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
console.log(chalk.white.bold(' ● General Performance'));
|
|
30
|
+
console.log(generalTable.toString());
|
|
31
|
+
|
|
32
|
+
// 2. Method Distribution
|
|
33
|
+
const methods = {};
|
|
34
|
+
interactions.forEach(i => {
|
|
35
|
+
methods[i.request.method] = (methods[i.request.method] || 0) + 1;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const methodTable = new Table({
|
|
39
|
+
head: ['Method', 'Count', 'Percentage'].map(h => chalk.cyan(h)),
|
|
40
|
+
colWidths: [15, 10, 15]
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
Object.entries(methods).forEach(([method, count]) => {
|
|
44
|
+
const percentage = ((count / total) * 100).toFixed(1) + '%';
|
|
45
|
+
methodTable.push([chalk.white(method), chalk.white(count), chalk.gray(percentage)]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(chalk.white.bold('\n ● Request Distribution'));
|
|
49
|
+
console.log(methodTable.toString());
|
|
50
|
+
|
|
51
|
+
// 3. Slowest Endpoints
|
|
52
|
+
const slowTable = new Table({
|
|
53
|
+
head: ['URL', 'Status', 'Duration'].map(h => chalk.cyan(h)),
|
|
54
|
+
colWidths: [45, 10, 15]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const sortedBySlowest = [...interactions].sort((a, b) => b.metadata.duration_ms - a.metadata.duration_ms);
|
|
58
|
+
const topSlow = sortedBySlowest.slice(0, 5);
|
|
59
|
+
|
|
60
|
+
topSlow.forEach(i => {
|
|
61
|
+
const url = i.request.url.length > 42 ? i.request.url.substring(0, 39) + '...' : i.request.url;
|
|
62
|
+
const statusColor = i.response.status < 400 ? chalk.green : chalk.red;
|
|
63
|
+
slowTable.push([
|
|
64
|
+
chalk.gray(url),
|
|
65
|
+
statusColor(i.response.status),
|
|
66
|
+
chalk.red(i.metadata.duration_ms + 'ms')
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(chalk.white.bold('\n ● Top 5 Slowest Endpoints (Bottlenecks)'));
|
|
71
|
+
console.log(slowTable.toString());
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
package/src/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
const KIROO_DIR = '.kiroo';
|
|
@@ -112,6 +112,17 @@ export function getAllSnapshots() {
|
|
|
112
112
|
.map(f => f.replace('.json', ''));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export function clearAllInteractions() {
|
|
116
|
+
ensureKirooDir();
|
|
117
|
+
if (existsSync(INTERACTIONS_DIR)) {
|
|
118
|
+
const files = readdirSync(INTERACTIONS_DIR);
|
|
119
|
+
files.forEach(f => {
|
|
120
|
+
const filepath = join(INTERACTIONS_DIR, f);
|
|
121
|
+
rmSync(filepath, { force: true });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
115
126
|
export function loadEnv() {
|
|
116
127
|
ensureKirooDir();
|
|
117
128
|
const data = readFileSync(ENV_FILE, 'utf8');
|