kiroo 0.3.4 → 0.4.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/bin/kiroo.js +10 -2
- package/package.json +1 -1
- package/src/executor.js +16 -6
- package/src/graph.js +75 -0
- package/src/snapshot.js +16 -4
- package/src/storage.js +2 -0
package/bin/kiroo.js
CHANGED
|
@@ -6,7 +6,7 @@ import { executeRequest } from '../src/executor.js';
|
|
|
6
6
|
import { listInteractions, replayInteraction } from '../src/replay.js';
|
|
7
7
|
import { saveSnapshot, compareSnapshots, listSnapshots } from '../src/snapshot.js';
|
|
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
12
|
import { handleImport } from '../src/import.js';
|
|
@@ -17,7 +17,7 @@ const program = new Command();
|
|
|
17
17
|
program
|
|
18
18
|
.name('kiroo')
|
|
19
19
|
.description('Git for API interactions. Record, replay, snapshot, and diff your APIs.')
|
|
20
|
-
.version('0.
|
|
20
|
+
.version('0.4.0');
|
|
21
21
|
|
|
22
22
|
// Init command
|
|
23
23
|
program
|
|
@@ -129,6 +129,14 @@ snapshot
|
|
|
129
129
|
await compareSnapshots(tag1, tag2);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
// Graph command
|
|
133
|
+
program
|
|
134
|
+
.command('graph')
|
|
135
|
+
.description('Show visual dependency graph of API interactions')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
await showGraph();
|
|
138
|
+
});
|
|
139
|
+
|
|
132
140
|
// Import command
|
|
133
141
|
program
|
|
134
142
|
.command('import')
|
package/package.json
CHANGED
package/src/executor.js
CHANGED
|
@@ -4,16 +4,20 @@ import ora from 'ora';
|
|
|
4
4
|
import { saveInteraction, loadEnv, saveEnv } from './storage.js';
|
|
5
5
|
import { formatResponse } from './formatter.js';
|
|
6
6
|
|
|
7
|
-
function applyEnvReplacements(data, envVars) {
|
|
7
|
+
function applyEnvReplacements(data, envVars, usedKeys = null) {
|
|
8
8
|
if (typeof data === 'string') {
|
|
9
9
|
return data.replace(/\{\{(.+?)\}\}/g, (match, key) => {
|
|
10
|
-
|
|
10
|
+
if (envVars[key] !== undefined) {
|
|
11
|
+
if (usedKeys) usedKeys.add(key);
|
|
12
|
+
return envVars[key];
|
|
13
|
+
}
|
|
14
|
+
return match;
|
|
11
15
|
});
|
|
12
16
|
}
|
|
13
17
|
if (typeof data === 'object' && data !== null) {
|
|
14
18
|
const newData = Array.isArray(data) ? [] : {};
|
|
15
19
|
for (const key in data) {
|
|
16
|
-
newData[key] = applyEnvReplacements(data[key], envVars);
|
|
20
|
+
newData[key] = applyEnvReplacements(data[key], envVars, usedKeys);
|
|
17
21
|
}
|
|
18
22
|
return newData;
|
|
19
23
|
}
|
|
@@ -52,8 +56,11 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
52
56
|
const env = loadEnv();
|
|
53
57
|
const currentEnvVars = env.environments[env.current] || {};
|
|
54
58
|
|
|
59
|
+
const usedKeys = new Set();
|
|
60
|
+
const savedKeys = [];
|
|
61
|
+
|
|
55
62
|
// Apply replacements to URL
|
|
56
|
-
url = applyEnvReplacements(url, currentEnvVars);
|
|
63
|
+
url = applyEnvReplacements(url, currentEnvVars, usedKeys);
|
|
57
64
|
|
|
58
65
|
// Auto-BaseURL logic
|
|
59
66
|
if (currentEnvVars.baseUrl) {
|
|
@@ -103,7 +110,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
103
110
|
options.header.forEach(h => {
|
|
104
111
|
const [key, ...valueParts] = h.split(':');
|
|
105
112
|
const headerValue = valueParts.join(':').trim();
|
|
106
|
-
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars);
|
|
113
|
+
headers[key.trim()] = applyEnvReplacements(headerValue, currentEnvVars, usedKeys);
|
|
107
114
|
});
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -112,7 +119,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
112
119
|
if (options.data) {
|
|
113
120
|
let rawData = options.data;
|
|
114
121
|
// Apply replacements to raw data string before parsing
|
|
115
|
-
rawData = applyEnvReplacements(rawData, currentEnvVars);
|
|
122
|
+
rawData = applyEnvReplacements(rawData, currentEnvVars, usedKeys);
|
|
116
123
|
|
|
117
124
|
try {
|
|
118
125
|
body = JSON.parse(rawData);
|
|
@@ -175,6 +182,7 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
175
182
|
const val = getDeep(response, responsePath);
|
|
176
183
|
if (val !== undefined) {
|
|
177
184
|
env.environments[env.current][envKey] = val;
|
|
185
|
+
savedKeys.push(envKey);
|
|
178
186
|
console.log(chalk.cyan(` ✨ Saved to env:`), chalk.white(`${envKey}=${val}`));
|
|
179
187
|
} else {
|
|
180
188
|
console.log(chalk.yellow(` ⚠️ Could not find path '${responsePath}' in response`));
|
|
@@ -197,6 +205,8 @@ export async function executeRequest(method, url, options = {}) {
|
|
|
197
205
|
data: response.data,
|
|
198
206
|
},
|
|
199
207
|
duration,
|
|
208
|
+
saves: savedKeys,
|
|
209
|
+
uses: Array.from(usedKeys)
|
|
200
210
|
});
|
|
201
211
|
|
|
202
212
|
console.log(chalk.gray('\n 💾 Interaction saved:'), chalk.white(interactionId));
|
package/src/graph.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getAllInteractions } from './storage.js';
|
|
3
|
+
|
|
4
|
+
export async function showGraph() {
|
|
5
|
+
const interactions = getAllInteractions().reverse(); // Chronological order
|
|
6
|
+
|
|
7
|
+
if (interactions.length === 0) {
|
|
8
|
+
console.log(chalk.yellow('\n No interactions recorded yet.'));
|
|
9
|
+
console.log(chalk.gray(' Run some requests to see the dependency graph!\n'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 1. Map: Variable -> Provider Interaction (Method + Path)
|
|
14
|
+
const variableProviders = {};
|
|
15
|
+
|
|
16
|
+
// 2. Interaction nodes with their connections
|
|
17
|
+
const nodes = [];
|
|
18
|
+
|
|
19
|
+
const getPath = (urlStr) => {
|
|
20
|
+
try {
|
|
21
|
+
const urlObj = new URL(urlStr);
|
|
22
|
+
return urlObj.pathname;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interactions.forEach(int => {
|
|
29
|
+
const path = getPath(int.request.url);
|
|
30
|
+
const method = int.request.method;
|
|
31
|
+
const saves = int.metadata.saves || [];
|
|
32
|
+
const uses = int.metadata.uses || [];
|
|
33
|
+
|
|
34
|
+
// Track which variables this interaction provides
|
|
35
|
+
saves.forEach(v => {
|
|
36
|
+
variableProviders[v] = { method, path };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
nodes.push({ method, path, saves, uses });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan('\n 🕸️ API Dependency Graph:'));
|
|
43
|
+
console.log(chalk.gray(' (Shows how data flows between endpoints)\n'));
|
|
44
|
+
|
|
45
|
+
// Simple visualization
|
|
46
|
+
const seenPaths = new Set();
|
|
47
|
+
|
|
48
|
+
nodes.forEach((node, idx) => {
|
|
49
|
+
const nodeLabel = `${chalk.white(node.method)} ${chalk.gray(node.path)}`;
|
|
50
|
+
|
|
51
|
+
// Find dependencies based on 'uses'
|
|
52
|
+
const dependencies = node.uses.map(v => {
|
|
53
|
+
const provider = variableProviders[v];
|
|
54
|
+
return provider ? `[${v}] from ${provider.method} ${provider.path}` : null;
|
|
55
|
+
}).filter(Boolean);
|
|
56
|
+
|
|
57
|
+
// Render the node
|
|
58
|
+
if (dependencies.length > 0) {
|
|
59
|
+
console.log(` ${chalk.blue('⬇')} ${nodeLabel}`);
|
|
60
|
+
dependencies.forEach((dep, depIdx) => {
|
|
61
|
+
const branchToken = depIdx === dependencies.length - 1 ? '└─' : '├─';
|
|
62
|
+
console.log(` ${chalk.gray(branchToken)} ${chalk.yellow('uses')} ${dep}`);
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
console.log(` ${chalk.green('○')} ${nodeLabel}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.saves.length > 0) {
|
|
69
|
+
const saveToken = node.uses.length > 0 ? ' │' : ' ';
|
|
70
|
+
console.log(`${saveToken} ${chalk.magenta('↳')} ${chalk.gray('saves')} ${chalk.white(node.saves.join(', '))}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
});
|
|
75
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -59,15 +59,27 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
59
59
|
const results = [];
|
|
60
60
|
let breakingChanges = 0;
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// Helper to get path from URL string
|
|
63
|
+
const getPath = (urlStr) => {
|
|
64
|
+
try {
|
|
65
|
+
const urlObj = new URL(urlStr);
|
|
66
|
+
return urlObj.pathname;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// If it's already a path or invalid full URL, return as is
|
|
69
|
+
return urlStr.startsWith('http') ? urlStr : (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Domain-agnostic comparison: match by Path and Method
|
|
63
74
|
s2.interactions.forEach(int2 => {
|
|
64
|
-
const
|
|
75
|
+
const path2 = getPath(int2.url);
|
|
76
|
+
const int1 = s1.interactions.find(i => getPath(i.url) === path2 && i.method === int2.method);
|
|
65
77
|
|
|
66
78
|
if (!int1) {
|
|
67
79
|
results.push({
|
|
68
80
|
type: 'NEW',
|
|
69
81
|
method: int2.method,
|
|
70
|
-
url:
|
|
82
|
+
url: path2,
|
|
71
83
|
msg: chalk.blue('New interaction added')
|
|
72
84
|
});
|
|
73
85
|
return;
|
|
@@ -97,7 +109,7 @@ export async function compareSnapshots(tag1, tag2) {
|
|
|
97
109
|
results.push({
|
|
98
110
|
type: 'CHANGE',
|
|
99
111
|
method: int2.method,
|
|
100
|
-
url:
|
|
112
|
+
url: path2,
|
|
101
113
|
msg: diffs.join('\n ')
|
|
102
114
|
});
|
|
103
115
|
}
|
package/src/storage.js
CHANGED
|
@@ -39,6 +39,8 @@ export async function saveInteraction(interaction) {
|
|
|
39
39
|
response: interaction.response,
|
|
40
40
|
metadata: {
|
|
41
41
|
duration_ms: interaction.duration,
|
|
42
|
+
saves: interaction.saves || [], // Variables saved from this response
|
|
43
|
+
uses: interaction.uses || [], // Variables used in this request
|
|
42
44
|
},
|
|
43
45
|
};
|
|
44
46
|
|