kiroo 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yash Pouranik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ <div align="center">
2
+ <img src="./kiroo_banner.png" alt="Kiroo Banner" width="100%">
3
+
4
+ # đŸĻ KIROO
5
+ ### **Version Control for API Interactions**
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-green.svg)](https://nodejs.org/)
9
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
10
+
11
+ **Record, Replay, Snapshot, and Diff your APIs just like Git handles code.**
12
+
13
+ [Installation](#-installation) â€ĸ [Quick Start](#-quick-start) â€ĸ [Key Features](#-core-capabilities) â€ĸ [Why Kiroo?](#-why-kiroo)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## 📖 Introduction
20
+
21
+ Kiroo treats your API requests and responses as **first-class versionable artifacts**.
22
+
23
+ Ever had a production bug that worked fine on your machine? Ever refactored a backend only to find out you broke a critical field 3 days later? Kiroo solves this by letting you **store API interactions in your repository**.
24
+
25
+ Every interaction is a structured, reproducibility-focused JSON file that lives in your `.kiroo/` directory.
26
+
27
+ ---
28
+
29
+ ## ✨ Core Capabilities
30
+
31
+ ### 🔴 **Auto-Recording**
32
+ Every request made through Kiroo is automatically saved. No more manual exports from Postman.
33
+ ```bash
34
+ kiroo post {{baseUrl}}/users -d "name=Yash email=yash@example.com"
35
+ ```
36
+
37
+ ### 🔄 **Replay Engine**
38
+ Re-run any past interaction instantly and see if the backend behavior has changed.
39
+ ```bash
40
+ kiroo replay <interaction-id>
41
+ ```
42
+
43
+ ### 🌍 **Smart Environments & Variables**
44
+ Stop copy-pasting tokens. Chain requests together dynamically.
45
+ ```bash
46
+ # Save a token from login
47
+ kiroo post /login --save token=data.accessToken
48
+
49
+ # Use it in the next request
50
+ kiroo get /profile -H "Authorization: Bearer {{token}}"
51
+ ```
52
+
53
+ ### 📸 **Snapshot System & Diff Engine**
54
+ Capture the "Status Quo" of your API and detect **Breaking Changes** during refactors.
55
+ ```bash
56
+ # Before refactor
57
+ kiroo snapshot save v1-stable
58
+
59
+ # After refactor
60
+ kiroo snapshot compare v1-stable current
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 🚀 Quick Start
66
+
67
+ ### 1. Installation
68
+ ```bash
69
+ # Clone the repo
70
+ git clone https://github.com/yash-pouranik/kiroo.git
71
+ cd kiroo
72
+
73
+ # Install and link
74
+ npm install
75
+ npm link
76
+ ```
77
+
78
+ ### 2. Initialize
79
+ ```bash
80
+ kiroo init
81
+ ```
82
+
83
+ ### 3. Basic Request
84
+ ```bash
85
+ kiroo env set baseUrl http://localhost:3000
86
+ kiroo get {{baseUrl}}/health
87
+ ```
88
+
89
+ ---
90
+
91
+ ## đŸ› ī¸ Advanced Workflows
92
+
93
+ ### Nested Data Support
94
+ Kiroo's shorthand parser understands nested objects and arrays:
95
+ ```bash
96
+ kiroo put /products/1 -d "reviews[0].stars=5 metadata.isFeatured=true"
97
+ ```
98
+
99
+ ### Managing Environments
100
+ ```bash
101
+ kiroo env use prod
102
+ kiroo env list
103
+ ```
104
+
105
+ ---
106
+
107
+ ## đŸŽ¯ Comparison
108
+
109
+ | Feature | Postman / Insomnia | Bruno | **Kiroo** |
110
+ | :--- | :---: | :---: | :---: |
111
+ | **CLI-First** | ❌ | âš ī¸ | ✅ |
112
+ | **Git-Native** | ❌ | ✅ | ✅ |
113
+ | **Auto-Recording** | ❌ | ❌ | ✅ |
114
+ | **Built-in Replay** | ❌ | ❌ | ✅ |
115
+ | **Variable Chaining** | âš ī¸ | âš ī¸ | ✅ |
116
+
117
+ ---
118
+
119
+ ## 📜 License
120
+
121
+ Distributed under the MIT License. See `LICENSE` for more information.
122
+
123
+ ---
124
+
125
+ <div align="center">
126
+ Built with â¤ī¸ for Developers by <a href="https://github.com/yash-pouranik">Yash Pouranik</a>
127
+ </div>
package/bin/kiroo.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { executeRequest } from '../src/executor.js';
6
+ import { listInteractions, replayInteraction } from '../src/replay.js';
7
+ import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
8
+ import { setEnv, setVar, deleteVar, listEnv } from '../src/env.js';
9
+ // import { showGraph } from '../src/graph.js';
10
+ import { initProject } from '../src/init.js';
11
+ // import { showStats } from '../src/stats.js';
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('kiroo')
17
+ .description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
18
+ .version('0.2.0');
19
+
20
+ // Init command
21
+ program
22
+ .command('init')
23
+ .description('Initialize Kiroo in current directory')
24
+ .action(async () => {
25
+ await initProject();
26
+ });
27
+ // sk_live_p7BWJjsYlKmauBOjiEeiLRuu4DokkBWsgYne_E6osTo
28
+
29
+ // HTTP methods as commands
30
+ ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
31
+ program
32
+ .command(`${method.toLowerCase()} <url>`)
33
+ .alias(method)
34
+ .description(`Execute ${method} request and store interaction`)
35
+ .option('-H, --header <headers...>', 'Headers (key:value)')
36
+ .option('-d, --data <data>', 'Request body (JSON string or key=value pairs)')
37
+ .option('-s, --save <pairs...>', 'Extract values from response to env (key=path.to.data)')
38
+ .action(async (url, options) => {
39
+ await executeRequest(method, url, options);
40
+ });
41
+ });
42
+
43
+ // List interactions
44
+ program
45
+ .command('list')
46
+ .description('List all stored interactions')
47
+ .option('-n, --limit <number>', 'Number of interactions to show', '10')
48
+ .option('-o, --offset <number>', 'Number of interactions to skip', '0')
49
+ .action(async (options) => {
50
+ await listInteractions(options);
51
+ });
52
+
53
+ // Replay interaction
54
+ program
55
+ .command('replay <id>')
56
+ .description('Replay a stored interaction')
57
+ .action(async (id) => {
58
+ await replayInteraction(id);
59
+ });
60
+
61
+ // Environment commands
62
+ const env = program.command('env').description('Environment management');
63
+
64
+ env
65
+ .command('use <name>')
66
+ .description('Switch to a specific environment')
67
+ .action((name) => setEnv(name));
68
+
69
+ env
70
+ .command('list')
71
+ .description('List environments and variables')
72
+ .action(() => listEnv());
73
+
74
+ env
75
+ .command('set <key> <value>')
76
+ .description('Set a variable in current environment')
77
+ .action((key, value) => setVar(key, value));
78
+
79
+ env
80
+ .command('rm <key>')
81
+ .description('Remove a variable from current environment')
82
+ .action((key) => deleteVar(key));
83
+
84
+ // Snapshot commands
85
+ const snapshot = program.command('snapshot').description('Snapshot management');
86
+
87
+ snapshot
88
+ .command('save <tag>')
89
+ .description('Save current state as snapshot')
90
+ .action(async (tag) => {
91
+ await saveSnapshot(tag);
92
+ });
93
+
94
+ snapshot
95
+ .command('list')
96
+ .description('List all snapshots')
97
+ .action(async () => {
98
+ await listSnapshots();
99
+ });
100
+
101
+ snapshot
102
+ .command('compare <tag1> <tag2>')
103
+ .description('Compare two snapshots')
104
+ .action(async (tag1, tag2) => {
105
+ await compareSnapshots(tag1, tag2);
106
+ });
107
+
108
+ // Graph command
109
+ /*
110
+ program
111
+ .command('graph')
112
+ .description('Show API dependency graph')
113
+ .action(async () => {
114
+ await showGraph();
115
+ });
116
+ */
117
+
118
+ // Stats command
119
+ /*
120
+ program
121
+ .command('stats')
122
+ .description('Show usage statistics')
123
+ .action(async () => {
124
+ await showStats();
125
+ });
126
+ */
127
+
128
+ // Error handling
129
+ program.exitOverride();
130
+
131
+ try {
132
+ await program.parseAsync(process.argv);
133
+ } catch (err) {
134
+ if (err.code === 'commander.help' || err.message === '(outputHelp)') {
135
+ // Help was requested, exit normally
136
+ process.exit(0);
137
+ } else if (err.code === 'commander.unknownCommand') {
138
+ console.error(chalk.red(`\n ✗ Unknown command: ${err.message}\n`));
139
+ console.log(chalk.gray(' Run'), chalk.white('kiroo --help'), chalk.gray('for usage information.\n'));
140
+ process.exit(1);
141
+ } else {
142
+ console.error(chalk.red('\n ✗ Error:'), err.message, `(${err.code})`, '\n');
143
+ process.exit(1);
144
+ }
145
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "kiroo",
3
+ "version": "0.2.0",
4
+ "description": "Git for API interactions. Record, replay, snapshot, and diff your APIs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "kiroo": "./bin/kiroo.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "dev": "node bin/kiroo.js",
17
+ "test": "node --test test.js"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/yash-pouranik/kiroo.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/yash-pouranik/kiroo/issues"
25
+ },
26
+ "homepage": "https://github.com/yash-pouranik/kiroo#readme",
27
+ "keywords": [
28
+ "api",
29
+ "testing",
30
+ "cli",
31
+ "replay",
32
+ "snapshot",
33
+ "diff",
34
+ "git-native",
35
+ "developer-tools"
36
+ ],
37
+ "author": "Yash Pouranik",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "axios": "^1.6.7",
41
+ "chalk": "^5.3.0",
42
+ "cli-table3": "^0.6.3",
43
+ "commander": "^12.0.0",
44
+ "inquirer": "^9.2.15",
45
+ "js-yaml": "^4.1.0",
46
+ "ora": "^8.0.1"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ }
51
+ }
package/src/env.js ADDED
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { loadEnv, saveEnv } from './storage.js';
4
+
5
+ export function listEnv() {
6
+ const env = loadEnv();
7
+
8
+ console.log(chalk.cyan(`\n 🌍 Environments:`));
9
+ Object.keys(env.environments).forEach(name => {
10
+ const activeMarker = name === env.current ? chalk.green(' (active)') : '';
11
+ console.log(` - ${chalk.white(name)}${activeMarker}`);
12
+ });
13
+
14
+ const currentVars = env.environments[env.current];
15
+ if (Object.keys(currentVars).length > 0) {
16
+ console.log(chalk.cyan(`\n đŸ“Ļ Variables in '${env.current}':`));
17
+ const table = new Table({
18
+ head: [chalk.cyan('Key'), chalk.cyan('Value')],
19
+ colWidths: [20, 40]
20
+ });
21
+
22
+ Object.entries(currentVars).forEach(([k, v]) => {
23
+ table.push([chalk.white(k), chalk.gray(String(v))]);
24
+ });
25
+ console.log(table.toString());
26
+ } else {
27
+ console.log(chalk.gray(`\n No variables set in '${env.current}' environment.`));
28
+ }
29
+ console.log('');
30
+ }
31
+
32
+ export function setEnv(name) {
33
+ const env = loadEnv();
34
+ if (name === 'list') {
35
+ return listEnv();
36
+ }
37
+
38
+ if (!env.environments[name]) {
39
+ env.environments[name] = {};
40
+ console.log(chalk.green(` ✨ Created new environment:`), chalk.white(name));
41
+ }
42
+
43
+ env.current = name;
44
+ saveEnv(env);
45
+ console.log(chalk.green(` 🔄 Switched to environment:`), chalk.white(name));
46
+ }
47
+
48
+ export function setVar(key, value) {
49
+ const env = loadEnv();
50
+ env.environments[env.current][key] = value;
51
+ saveEnv(env);
52
+ console.log(chalk.green(` ✅ Set ${key}=${value} in`), chalk.white(env.current));
53
+ }
54
+
55
+ export function deleteVar(key) {
56
+ const env = loadEnv();
57
+ if (env.environments[env.current][key] !== undefined) {
58
+ delete env.environments[env.current][key];
59
+ saveEnv(env);
60
+ console.log(chalk.green(` đŸ—‘ī¸ Deleted variable:`), chalk.white(key));
61
+ } else {
62
+ console.log(chalk.yellow(` âš ī¸ Variable '${key}' not found in environment '${env.current}'`));
63
+ }
64
+ }
@@ -0,0 +1,176 @@
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { saveInteraction, loadEnv, saveEnv } from './storage.js';
5
+ import { formatResponse } from './formatter.js';
6
+
7
+ function applyEnvReplacements(data, envVars) {
8
+ if (typeof data === 'string') {
9
+ return data.replace(/\{\{(.+?)\}\}/g, (match, key) => {
10
+ return envVars[key] !== undefined ? envVars[key] : match;
11
+ });
12
+ }
13
+ if (typeof data === 'object' && data !== null) {
14
+ const newData = Array.isArray(data) ? [] : {};
15
+ for (const key in data) {
16
+ newData[key] = applyEnvReplacements(data[key], envVars);
17
+ }
18
+ return newData;
19
+ }
20
+ return data;
21
+ }
22
+
23
+ function setDeep(obj, path, value) {
24
+ const keys = path.split(/[.[\]]+/).filter(Boolean);
25
+ let current = obj;
26
+
27
+ for (let i = 0; i < keys.length; i++) {
28
+ const key = keys[i];
29
+ const isLast = i === keys.length - 1;
30
+
31
+ if (isLast) {
32
+ current[key] = value;
33
+ } else {
34
+ // Check if next key looks like a number (array index)
35
+ const nextKey = keys[i + 1];
36
+ const isNextNumber = !isNaN(nextKey);
37
+
38
+ if (!current[key]) {
39
+ current[key] = isNextNumber ? [] : {};
40
+ }
41
+ current = current[key];
42
+ }
43
+ }
44
+ }
45
+
46
+ function getDeep(obj, path) {
47
+ const keys = path.split(/[.[\]]+/).filter(Boolean);
48
+ return keys.reduce((acc, key) => acc && acc[key], obj);
49
+ }
50
+
51
+ export async function executeRequest(method, url, options = {}) {
52
+ const env = loadEnv();
53
+ const currentEnvVars = env.environments[env.current] || {};
54
+
55
+ // Apply replacements to URL
56
+ url = applyEnvReplacements(url, currentEnvVars);
57
+
58
+ // Parse headers
59
+ const headers = {};
60
+ if (options.header) {
61
+ options.header.forEach(h => {
62
+ const [key, ...valueParts] = h.split(':');
63
+ const headerValue = valueParts.join(':').trim();
64
+ headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars);
65
+ });
66
+ }
67
+
68
+ // Parse body
69
+ let body;
70
+ if (options.data) {
71
+ let rawData = options.data;
72
+ // Apply replacements to raw data string before parsing
73
+ rawData = applyEnvReplacements(rawData, currentEnvVars);
74
+
75
+ try {
76
+ body = JSON.parse(rawData);
77
+ } catch {
78
+ body = {};
79
+ // Improved shorthand parser to handle quoted strings and nested objects
80
+ const pairs = rawData.match(/(\\.|[^ ])+/g) || [];
81
+
82
+ pairs.forEach(pair => {
83
+ const [key, ...valueParts] = pair.split('=');
84
+ let value = valueParts.join('=');
85
+
86
+ if (key && value !== undefined) {
87
+ let parsedValue;
88
+ // Check for quoted strings
89
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
90
+ parsedValue = value.slice(1, -1);
91
+ } else {
92
+ parsedValue = value;
93
+ if (value === 'true') parsedValue = true;
94
+ else if (value === 'false') parsedValue = false;
95
+ else if (!isNaN(value) && value.trim() !== '') parsedValue = Number(value);
96
+ }
97
+
98
+ setDeep(body, key, parsedValue);
99
+ }
100
+ });
101
+ }
102
+ }
103
+
104
+ // Ensure URL has protocol
105
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
106
+ url = 'https://' + url;
107
+ }
108
+
109
+ const spinner = ora('Sending request...').start();
110
+ const startTime = Date.now();
111
+
112
+ try {
113
+ const response = await axios({
114
+ method: method.toLowerCase(),
115
+ url,
116
+ headers,
117
+ data: body,
118
+ validateStatus: () => true,
119
+ });
120
+
121
+ const duration = Date.now() - startTime;
122
+ spinner.succeed(chalk.green(`${response.status} ${response.statusText}`) + chalk.gray(` (${duration}ms)`));
123
+
124
+ // Format and display response
125
+ console.log(formatResponse(response));
126
+
127
+ // Handle --save option
128
+ if (options.save) {
129
+ const saves = Array.isArray(options.save) ? options.save : [options.save];
130
+ saves.forEach(s => {
131
+ const [envKey, responsePath] = s.split('=');
132
+ if (envKey && responsePath) {
133
+ const val = getDeep(response, responsePath);
134
+ if (val !== undefined) {
135
+ env.environments[env.current][envKey] = val;
136
+ console.log(chalk.cyan(` ✨ Saved to env:`), chalk.white(`${envKey}=${val}`));
137
+ } else {
138
+ console.log(chalk.yellow(` âš ī¸ Could not find path '${responsePath}' in response`));
139
+ }
140
+ }
141
+ });
142
+ saveEnv(env);
143
+ }
144
+
145
+ // Save interaction
146
+ const interactionId = await saveInteraction({
147
+ method,
148
+ url,
149
+ headers,
150
+ body,
151
+ response: {
152
+ status: response.status,
153
+ statusText: response.statusText,
154
+ headers: response.headers,
155
+ data: response.data,
156
+ },
157
+ duration,
158
+ });
159
+
160
+ console.log(chalk.gray('\n 💾 Interaction saved:'), chalk.white(interactionId));
161
+
162
+ } catch (error) {
163
+ const duration = Date.now() - startTime;
164
+ spinner.fail(chalk.red('Request failed'));
165
+
166
+ if (error.code === 'ENOTFOUND') {
167
+ console.error(chalk.red('\n ✗ Host not found:'), url);
168
+ } else if (error.code === 'ECONNREFUSED') {
169
+ console.error(chalk.red('\n ✗ Connection refused:'), url);
170
+ } else {
171
+ console.error(chalk.red('\n ✗ Error:'), error.message, '\n');
172
+ }
173
+
174
+ process.exit(1);
175
+ }
176
+ }
@@ -0,0 +1,54 @@
1
+ import chalk from 'chalk';
2
+
3
+ export function formatResponse(response) {
4
+ const lines = [];
5
+
6
+ // Status
7
+ const statusColor = response.status >= 200 && response.status < 300
8
+ ? 'green'
9
+ : response.status >= 400
10
+ ? 'red'
11
+ : 'yellow';
12
+
13
+ lines.push('');
14
+ lines.push(chalk[statusColor].bold(` ${response.status} ${response.statusText}`));
15
+ lines.push('');
16
+
17
+ // Headers (selected)
18
+ const importantHeaders = ['content-type', 'content-length', 'set-cookie'];
19
+ const headers = Object.entries(response.headers)
20
+ .filter(([key]) => importantHeaders.includes(key.toLowerCase()))
21
+ .slice(0, 3);
22
+
23
+ if (headers.length > 0) {
24
+ lines.push(chalk.gray(' Headers:'));
25
+ headers.forEach(([key, value]) => {
26
+ const displayValue = typeof value === 'string' && value.length > 50
27
+ ? value.substring(0, 50) + '...'
28
+ : value;
29
+ lines.push(chalk.gray(` ${key}:`), chalk.white(displayValue));
30
+ });
31
+ lines.push('');
32
+ }
33
+
34
+ // Body
35
+ if (response.data) {
36
+ lines.push(chalk.gray(' Response:'));
37
+
38
+ if (typeof response.data === 'object') {
39
+ // Pretty print JSON
40
+ const json = JSON.stringify(response.data, null, 2);
41
+ json.split('\n').forEach(line => {
42
+ lines.push(chalk.cyan(` ${line}`));
43
+ });
44
+ } else {
45
+ // Plain text
46
+ const text = String(response.data);
47
+ const preview = text.length > 500 ? text.substring(0, 500) + '...' : text;
48
+ lines.push(chalk.white(` ${preview}`));
49
+ }
50
+ }
51
+
52
+ lines.push('');
53
+ return lines.join('\n');
54
+ }
package/src/init.js ADDED
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import { writeFileSync, existsSync } from 'fs';
4
+ import { ensureKirooDir } from './storage.js';
5
+
6
+ export async function initProject() {
7
+ console.log('');
8
+ console.log(chalk.cyan.bold(' 🚀 Welcome to Kiroo'));
9
+ console.log(chalk.gray(' Git for API interactions\n'));
10
+
11
+ if (existsSync('.kiroo')) {
12
+ console.log(chalk.yellow(' âš ī¸ Kiroo is already initialized in this directory.\n'));
13
+ return;
14
+ }
15
+
16
+ const answers = await inquirer.prompt([
17
+ {
18
+ type: 'input',
19
+ name: 'projectName',
20
+ message: 'Project name:',
21
+ default: 'my-api-project',
22
+ },
23
+ {
24
+ type: 'input',
25
+ name: 'baseUrl',
26
+ message: 'Base URL (optional):',
27
+ default: '',
28
+ },
29
+ ]);
30
+
31
+ ensureKirooDir();
32
+
33
+ const config = {
34
+ projectName: answers.projectName,
35
+ baseUrl: answers.baseUrl,
36
+ createdAt: new Date().toISOString(),
37
+ };
38
+
39
+ writeFileSync('.kiroo/config.json', JSON.stringify(config, null, 2));
40
+
41
+ console.log('');
42
+ console.log(chalk.green(' ✅ Kiroo initialized successfully!'));
43
+ console.log('');
44
+ console.log(chalk.gray(' Next steps:'));
45
+ console.log(chalk.white(' kiroo POST https://api.example.com/login email=test@test.com'));
46
+ console.log(chalk.white(' kiroo list'));
47
+ console.log(chalk.white(' kiroo snapshot save initial\n'));
48
+ }
package/src/replay.js ADDED
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import axios from 'axios';
4
+ import { getAllInteractions, loadInteraction } from './storage.js';
5
+ import { formatResponse } from './formatter.js';
6
+
7
+ export async function listInteractions(options) {
8
+ const interactions = getAllInteractions();
9
+ const limit = parseInt(options.limit) || 10;
10
+ const offset = parseInt(options.offset) || 0;
11
+
12
+ if (interactions.length === 0) {
13
+ console.log(chalk.yellow('\n No interactions found.'));
14
+ console.log(chalk.gray(' Run a request first: '), chalk.white('kiroo POST https://api.example.com/endpoint\n'));
15
+ return;
16
+ }
17
+
18
+ const table = new Table({
19
+ head: ['ID', 'Method', 'URL', 'Status', 'Duration'].map(h => chalk.cyan(h)),
20
+ colWidths: [26, 8, 45, 8, 12],
21
+ });
22
+
23
+ const page = interactions.slice(offset, offset + limit);
24
+
25
+ page.forEach(int => {
26
+ const statusColor = int.response.status >= 200 && int.response.status < 300
27
+ ? chalk.green
28
+ : int.response.status >= 400
29
+ ? chalk.red
30
+ : chalk.yellow;
31
+
32
+ table.push([
33
+ chalk.white(int.id),
34
+ chalk.white(int.request.method),
35
+ chalk.gray(int.request.url.substring(0, 42) + (int.request.url.length > 42 ? '...' : '')),
36
+ statusColor(int.response.status),
37
+ chalk.gray(int.metadata.duration_ms + 'ms'),
38
+ ]);
39
+ });
40
+
41
+ console.log('');
42
+ console.log(table.toString());
43
+ console.log('');
44
+
45
+ const start = offset + 1;
46
+ const end = Math.min(offset + limit, interactions.length);
47
+
48
+ console.log(chalk.gray(` Showing ${start}-${end} of ${interactions.length} interactions`));
49
+ if (interactions.length > offset + limit) {
50
+ console.log(chalk.gray(` Next page: `), chalk.white(`kiroo list --offset ${offset + limit}\n`));
51
+ } else {
52
+ console.log('');
53
+ }
54
+ console.log(chalk.gray(' Replay: '), chalk.white('kiroo replay <id>\n'));
55
+ }
56
+
57
+ export async function replayInteraction(id) {
58
+ try {
59
+ const interaction = loadInteraction(id);
60
+
61
+ console.log(chalk.cyan(`\n 🔄 Replaying interaction:`), chalk.white(id));
62
+ console.log(chalk.gray(` ${interaction.request.method} ${interaction.request.url}\n`));
63
+
64
+ const startTime = Date.now();
65
+ const response = await axios({
66
+ method: interaction.request.method.toLowerCase(),
67
+ url: interaction.request.url,
68
+ headers: interaction.request.headers,
69
+ data: interaction.request.body,
70
+ validateStatus: () => true,
71
+ });
72
+ const duration = Date.now() - startTime;
73
+
74
+ console.log(formatResponse(response));
75
+
76
+ // Simple comparison
77
+ console.log(chalk.cyan(' 📊 Comparison with stored response:'));
78
+
79
+ if (response.status === interaction.response.status) {
80
+ console.log(chalk.green(' ✓ Status matches:'), chalk.white(response.status));
81
+ } else {
82
+ console.log(chalk.red(' ✗ Status changed:'), chalk.gray(interaction.response.status), chalk.white('→'), chalk.red(response.status));
83
+ }
84
+
85
+ const timeDiff = duration - interaction.metadata.duration_ms;
86
+ const timeColor = timeDiff > 0 ? chalk.yellow : chalk.green;
87
+ console.log(chalk.gray(' ⏱ Duration:'), chalk.white(`${duration}ms`), timeColor(`(${timeDiff > 0 ? '+' : ''}${timeDiff}ms)`));
88
+ console.log('');
89
+
90
+ } catch (error) {
91
+ console.error(chalk.red('\n ✗ Replay failed:'), error.message, '\n');
92
+ process.exit(1);
93
+ }
94
+ }
@@ -0,0 +1,127 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { getAllInteractions, saveSnapshotData, getAllSnapshots, loadSnapshotData } from './storage.js';
4
+
5
+ export async function saveSnapshot(tag) {
6
+ const interactions = getAllInteractions();
7
+
8
+ if (interactions.length === 0) {
9
+ console.log(chalk.yellow('\n No interactions to snapshot.'));
10
+ console.log(chalk.gray(' Run some requests first.\n'));
11
+ return;
12
+ }
13
+
14
+ const snapshotData = {
15
+ tag,
16
+ timestamp: new Date().toISOString(),
17
+ interactions: interactions.map(int => ({
18
+ id: int.id,
19
+ method: int.request.method,
20
+ url: int.request.url,
21
+ request: int.request,
22
+ response: {
23
+ status: int.response.status,
24
+ body: int.response.data
25
+ },
26
+ metadata: int.metadata
27
+ }))
28
+ };
29
+
30
+ saveSnapshotData(tag, snapshotData);
31
+
32
+ console.log(chalk.green(`\n 📸 Snapshot saved:`), chalk.white(tag));
33
+ console.log(chalk.gray(` Contains ${interactions.length} interactions\n`));
34
+ }
35
+
36
+ export async function listSnapshots() {
37
+ const snapshots = getAllSnapshots();
38
+
39
+ if (snapshots.length === 0) {
40
+ console.log(chalk.yellow('\n No snapshots found.'));
41
+ console.log(chalk.gray(' Save one with: '), chalk.white('kiroo snapshot save <tag>\n'));
42
+ return;
43
+ }
44
+
45
+ console.log(chalk.cyan('\n 📸 Available Snapshots:'));
46
+ snapshots.forEach(tag => {
47
+ console.log(` - ${chalk.white(tag)}`);
48
+ });
49
+ console.log('');
50
+ }
51
+
52
+ export async function compareSnapshots(tag1, tag2) {
53
+ try {
54
+ const s1 = loadSnapshotData(tag1);
55
+ const s2 = loadSnapshotData(tag2);
56
+
57
+ console.log(chalk.cyan(`\n 🔍 Comparing Snapshots:`), chalk.white(tag1), chalk.gray('vs'), chalk.white(tag2));
58
+
59
+ const results = [];
60
+ let breakingChanges = 0;
61
+
62
+ // Simplistic comparison: match by URL and Method
63
+ s2.interactions.forEach(int2 => {
64
+ const int1 = s1.interactions.find(i => i.url === int2.url && i.method === int2.method);
65
+
66
+ if (!int1) {
67
+ results.push({
68
+ type: 'NEW',
69
+ method: int2.method,
70
+ url: int2.url,
71
+ msg: chalk.blue('New interaction added')
72
+ });
73
+ return;
74
+ }
75
+
76
+ const diffs = [];
77
+
78
+ // Compare status
79
+ if (int1.response.status !== int2.response.status) {
80
+ diffs.push(`Status: ${chalk.gray(int1.response.status)} → ${chalk.red(int2.response.status)}`);
81
+ breakingChanges++;
82
+ }
83
+
84
+ // Deep field comparison (very basic for MVP)
85
+ if (typeof int1.response.body === 'object' && typeof int2.response.body === 'object' && int1.response.body !== null && int2.response.body !== null) {
86
+ const keys1 = Object.keys(int1.response.body);
87
+ const keys2 = Object.keys(int2.response.body);
88
+
89
+ const removed = keys1.filter(k => !keys2.includes(k));
90
+ if (removed.length > 0) {
91
+ diffs.push(`Fields removed: ${chalk.red(removed.join(', '))}`);
92
+ breakingChanges++;
93
+ }
94
+ }
95
+
96
+ if (diffs.length > 0) {
97
+ results.push({
98
+ type: 'CHANGE',
99
+ method: int2.method,
100
+ url: int2.url,
101
+ msg: diffs.join('\n ')
102
+ });
103
+ }
104
+ });
105
+
106
+ if (results.length === 0) {
107
+ console.log(chalk.green('\n ✅ No differences detected. Your API is stable!\n'));
108
+ } else {
109
+ console.log('');
110
+ results.forEach(res => {
111
+ const symbol = res.type === 'NEW' ? chalk.blue('+') : chalk.yellow('âš ī¸');
112
+ console.log(` ${symbol} ${chalk.white(res.method)} ${chalk.gray(res.url)}`);
113
+ console.log(` ${res.msg}`);
114
+ });
115
+
116
+ if (breakingChanges > 0) {
117
+ console.log(chalk.red(`\n 🚨 Detected ${breakingChanges} potential breaking changes!\n`));
118
+ } else {
119
+ console.log(chalk.blue('\n â„šī¸ Non-breaking changes detected.\n'));
120
+ }
121
+ }
122
+
123
+ } catch (error) {
124
+ console.error(chalk.red('\n ✗ Comparison failed:'), error.message, '\n');
125
+ process.exit(1);
126
+ }
127
+ }
package/src/storage.js ADDED
@@ -0,0 +1,124 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const KIROO_DIR = '.kiroo';
5
+ const INTERACTIONS_DIR = join(KIROO_DIR, 'interactions');
6
+ const SNAPSHOTS_DIR = join(KIROO_DIR, 'snapshots');
7
+ const ENV_FILE = join(KIROO_DIR, 'env.json');
8
+
9
+ export function ensureKirooDir() {
10
+ if (!existsSync(KIROO_DIR)) {
11
+ mkdirSync(KIROO_DIR);
12
+ }
13
+ if (!existsSync(INTERACTIONS_DIR)) {
14
+ mkdirSync(INTERACTIONS_DIR);
15
+ }
16
+ if (!existsSync(SNAPSHOTS_DIR)) {
17
+ mkdirSync(SNAPSHOTS_DIR);
18
+ }
19
+ if (!existsSync(ENV_FILE)) {
20
+ writeFileSync(ENV_FILE, JSON.stringify({ current: 'default', environments: { default: {} } }, null, 2));
21
+ }
22
+ }
23
+
24
+ export async function saveInteraction(interaction) {
25
+ ensureKirooDir();
26
+
27
+ const timestamp = new Date().toISOString();
28
+ const id = timestamp.replace(/[:.]/g, '-');
29
+
30
+ const interactionData = {
31
+ id,
32
+ timestamp,
33
+ request: {
34
+ method: interaction.method,
35
+ url: interaction.url,
36
+ headers: interaction.headers,
37
+ body: interaction.body,
38
+ },
39
+ response: interaction.response,
40
+ metadata: {
41
+ duration_ms: interaction.duration,
42
+ },
43
+ };
44
+
45
+ const filename = `${id}.json`;
46
+ const filepath = join(INTERACTIONS_DIR, filename);
47
+
48
+ writeFileSync(filepath, JSON.stringify(interactionData, null, 2));
49
+
50
+ return id;
51
+ }
52
+
53
+ export function loadInteraction(id) {
54
+ const filepath = join(INTERACTIONS_DIR, `${id}.json`);
55
+
56
+ if (!existsSync(filepath)) {
57
+ throw new Error(`Interaction not found: ${id}`);
58
+ }
59
+
60
+ const data = readFileSync(filepath, 'utf8');
61
+ return JSON.parse(data);
62
+ }
63
+
64
+ export function getAllInteractions() {
65
+ ensureKirooDir();
66
+
67
+ if (!existsSync(INTERACTIONS_DIR)) {
68
+ return [];
69
+ }
70
+
71
+ const files = readdirSync(INTERACTIONS_DIR)
72
+ .filter(f => f.endsWith('.json'))
73
+ .sort()
74
+ .reverse(); // Most recent first
75
+
76
+ return files.map(f => {
77
+ const filepath = join(INTERACTIONS_DIR, f);
78
+ const data = readFileSync(filepath, 'utf8');
79
+ return JSON.parse(data);
80
+ });
81
+ }
82
+
83
+ export function saveSnapshotData(tag, data) {
84
+ ensureKirooDir();
85
+
86
+ const filename = `${tag}.json`;
87
+ const filepath = join(SNAPSHOTS_DIR, filename);
88
+
89
+ writeFileSync(filepath, JSON.stringify(data, null, 2));
90
+ }
91
+
92
+ export function loadSnapshotData(tag) {
93
+ const filepath = join(SNAPSHOTS_DIR, `${tag}.json`);
94
+
95
+ if (!existsSync(filepath)) {
96
+ throw new Error(`Snapshot not found: ${tag}`);
97
+ }
98
+
99
+ const data = readFileSync(filepath, 'utf8');
100
+ return JSON.parse(data);
101
+ }
102
+
103
+ export function getAllSnapshots() {
104
+ ensureKirooDir();
105
+
106
+ if (!existsSync(SNAPSHOTS_DIR)) {
107
+ return [];
108
+ }
109
+
110
+ return readdirSync(SNAPSHOTS_DIR)
111
+ .filter(f => f.endsWith('.json'))
112
+ .map(f => f.replace('.json', ''));
113
+ }
114
+
115
+ export function loadEnv() {
116
+ ensureKirooDir();
117
+ const data = readFileSync(ENV_FILE, 'utf8');
118
+ return JSON.parse(data);
119
+ }
120
+
121
+ export function saveEnv(data) {
122
+ ensureKirooDir();
123
+ writeFileSync(ENV_FILE, JSON.stringify(data, null, 2));
124
+ }