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 +21 -0
- package/README.md +127 -0
- package/bin/kiroo.js +145 -0
- package/package.json +51 -0
- package/src/env.js +64 -0
- package/src/executor.js +176 -0
- package/src/formatter.js +54 -0
- package/src/init.js +48 -0
- package/src/replay.js +94 -0
- package/src/snapshot.js +127 -0
- package/src/storage.js +124 -0
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](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
|
+
}
|
package/src/executor.js
ADDED
|
@@ -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
|
+
}
|
package/src/formatter.js
ADDED
|
@@ -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
|
+
}
|
package/src/snapshot.js
ADDED
|
@@ -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
|
+
}
|