jetic-cli 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "jetic-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "AI-Native API Behavior Testing & Discovery Platform — CLI and Local Studio Dashboard",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "jetic": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist"
11
+ ],
9
12
  "scripts": {
10
- "build": "tsc && node -e \"try { require('fs').cpSync('../dashboard/dist', './dist/dashboard', {recursive: true, force: true}); console.log('Bundled dashboard into dist/dashboard'); } catch (e) { console.warn('Could not bundle dashboard dist:', e.message); }\"",
13
+ "build": "tsup && node -e \"try { require('fs').cpSync('../dashboard/dist', './dist/dashboard', {recursive: true, force: true}); console.log('Bundled dashboard into dist/dashboard'); } catch (e) { console.warn('Could not bundle dashboard dist:', e.message); }\"",
11
14
  "clean": "rm -rf dist"
12
15
  },
13
16
  "keywords": [
@@ -26,6 +29,7 @@
26
29
  ],
27
30
  "author": "Jetic Labs",
28
31
  "license": "ISC",
32
+ "homepage": "https://jetic.online",
29
33
  "repository": {
30
34
  "type": "git",
31
35
  "url": "git+https://github.com/jeticlabs/jetic.git",
@@ -37,12 +41,6 @@
37
41
  "dependencies": {
38
42
  "@ai-sdk/openai": "^4.0.41",
39
43
  "@faker-js/faker": "^9.0.0",
40
- "@jetic/core": "workspace:*",
41
- "@jetic/dashboard": "workspace:*",
42
- "@jetic/memory": "workspace:*",
43
- "@jetic/model": "workspace:*",
44
- "@jetic/scanner": "workspace:*",
45
- "@jetic/simulator": "workspace:*",
46
44
  "@openrouter/ai-sdk-provider": "^3.0.0",
47
45
  "ai": "^7.0.65",
48
46
  "commander": "^12.0.0",
@@ -1,36 +0,0 @@
1
- import { Command } from 'commander';
2
- import { loadConfig, saveConfig } from '@jetic/core';
3
-
4
- export const configCommand = new Command('config')
5
- .description('Manage Jetic configuration')
6
- .action(() => {
7
- const config = loadConfig();
8
-
9
- console.log('\n\x1b[1mJetic Configuration\x1b[0m\n');
10
- const tableData = [
11
- { Key: 'Project Root', Value: config.projectRoot },
12
- { Key: 'Jetic Directory', Value: config.jeticDir },
13
- { Key: 'AI Provider', Value: config.ai?.provider || 'Not set' },
14
- { Key: 'AI Model', Value: config.ai?.model || 'Not set' },
15
- { Key: 'API Key Env Var', Value: config.ai?.apiKeyEnvVar || 'Not set' }
16
- ];
17
-
18
- console.table(tableData, ['Key', 'Value']);
19
- });
20
-
21
- configCommand
22
- .command('ai')
23
- .description('Configure the AI provider and model')
24
- .requiredOption('-p, --provider <provider>', 'AI Provider (e.g. openai)')
25
- .requiredOption('-m, --model <model>', 'Model name (e.g. gpt-4o)')
26
- .requiredOption('-k, --key-env <envvar>', 'Environment variable containing the API key (e.g. OPENAI_API_KEY)')
27
- .action((options) => {
28
- const config = loadConfig();
29
- config.ai = {
30
- provider: options.provider,
31
- model: options.model,
32
- apiKeyEnvVar: options.keyEnv
33
- };
34
- saveConfig(config);
35
- console.log('AI configuration saved successfully.');
36
- });
@@ -1,451 +0,0 @@
1
- import { Command } from 'commander';
2
- import express from 'express';
3
- import path from 'path';
4
- import fs from 'fs';
5
- import { JeticMemory } from '@jetic/memory';
6
-
7
- export const devCommand = new Command('dev')
8
- .description('Start the Jetic local dashboard')
9
- .option('-p, --port <number>', 'Port to run the dashboard on', '8787')
10
- .action(async (options) => {
11
- const port = parseInt(options.port, 10);
12
- const app = express();
13
-
14
- app.use(express.json());
15
-
16
- // Allow dashboard dev server to call the API during development
17
- app.use((_req, res, next) => {
18
- res.setHeader('Access-Control-Allow-Origin', '*');
19
- res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
20
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
21
- if (_req.method === 'OPTIONS') return res.sendStatus(204);
22
- next();
23
- });
24
-
25
- // ─── Model API ───────────────────────────────────────────────────────
26
- app.get('/api/model', (_req, res) => {
27
- const modelPath = path.join(process.cwd(), '.jetic', 'model.json');
28
- try {
29
- if (!fs.existsSync(modelPath)) return res.json(null);
30
- res.json(JSON.parse(fs.readFileSync(modelPath, 'utf8')));
31
- } catch (err: any) {
32
- res.status(500).json({ error: err.message });
33
- }
34
- });
35
-
36
- app.put('/api/model/endpoint/:id', (req, res) => {
37
- const modelPath = path.join(process.cwd(), '.jetic', 'model.json');
38
- try {
39
- if (!fs.existsSync(modelPath)) return res.status(404).json({ error: 'model.json not found' });
40
- const model = JSON.parse(fs.readFileSync(modelPath, 'utf8'));
41
- const idx = model.endpoints.findIndex((e: any) => e.id === req.params.id);
42
- if (idx === -1) return res.status(404).json({ error: 'Endpoint not found' });
43
- model.endpoints[idx] = { ...model.endpoints[idx], ...req.body };
44
- fs.writeFileSync(modelPath, JSON.stringify(model, null, 2), 'utf8');
45
- res.json({ ok: true, endpoint: model.endpoints[idx] });
46
- } catch (err: any) {
47
- res.status(500).json({ error: err.message });
48
- }
49
- });
50
-
51
- app.post('/api/model/scan', async (_req, res) => {
52
- try {
53
- const { loadConfig, writeJsonSync, ensureDirSync } = await import('@jetic/core');
54
- const { ExpressScanner } = await import('@jetic/scanner');
55
- const config = loadConfig();
56
- ensureDirSync(config.jeticDir);
57
- const scanner = new ExpressScanner(config);
58
- const model = await scanner.scan();
59
- const modelPath = path.join(config.jeticDir, 'model.json');
60
- writeJsonSync(modelPath, model);
61
- res.json({ ok: true, endpointCount: model.endpoints.length, model });
62
- } catch (err: any) {
63
- res.status(500).json({ error: err.message });
64
- }
65
- });
66
-
67
- // ─── Source viewer ────────────────────────────────────────────────────
68
- app.get('/api/model/source', (req, res) => {
69
- const file = req.query.file as string;
70
- const line = parseInt(req.query.line as string, 10) || 1;
71
- if (!file) return res.status(400).json({ error: 'file is required' });
72
- try {
73
- // Resolve relative to cwd
74
- const resolved = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
75
- if (!fs.existsSync(resolved)) {
76
- return res.status(404).json({ error: `File not found: ${resolved}` });
77
- }
78
- const content = fs.readFileSync(resolved, 'utf8');
79
- const lines = content.split('\n');
80
- // Return ~40 lines centred on the handler line
81
- const start = Math.max(0, line - 5);
82
- const end = Math.min(lines.length, line + 35);
83
- const snippet = lines.slice(start, end).join('\n');
84
- res.json({ source: snippet, startLine: start + 1, totalLines: lines.length, file: resolved });
85
- } catch (err: any) {
86
- res.status(500).json({ error: err.message });
87
- }
88
- });
89
-
90
- // ─── Related files ────────────────────────────────────────────────────
91
- app.get('/api/model/related', (req, res) => {
92
- const id = req.query.id as string;
93
- if (!id) return res.status(400).json({ error: 'id is required' });
94
- try {
95
- const modelPath = path.join(process.cwd(), '.jetic', 'model.json');
96
- if (!fs.existsSync(modelPath)) return res.json({ files: [] });
97
- const model = JSON.parse(fs.readFileSync(modelPath, 'utf8'));
98
- const endpoint = (model.endpoints || []).find((e: any) => e.id === id);
99
- if (!endpoint || !endpoint.source?.file) return res.json({ files: [] });
100
-
101
- const sourceFile = path.isAbsolute(endpoint.source.file)
102
- ? endpoint.source.file
103
- : path.join(process.cwd(), endpoint.source.file);
104
-
105
- if (!fs.existsSync(sourceFile)) return res.json({ files: [] });
106
-
107
- // Parse import/require statements from the source file
108
- const content = fs.readFileSync(sourceFile, 'utf8');
109
- const importRegex = /(?:import\s+.*?from\s+['"](.+?)['"]|require\(['"](.+?)['"]\))/g;
110
- const relatedFiles: string[] = [];
111
- const sourceDir = path.dirname(sourceFile);
112
- let match: RegExpExecArray | null;
113
-
114
- while ((match = importRegex.exec(content)) !== null) {
115
- const importPath = match[1] || match[2];
116
- if (!importPath || importPath.startsWith('@') || !importPath.startsWith('.')) continue;
117
- const exts = ['', '.ts', '.js', '.tsx', '.jsx'];
118
- for (const ext of exts) {
119
- const candidate = path.resolve(sourceDir, importPath + ext);
120
- if (fs.existsSync(candidate)) {
121
- // Return path relative to cwd
122
- relatedFiles.push(path.relative(process.cwd(), candidate));
123
- break;
124
- }
125
- }
126
- }
127
-
128
- // Also include the source file itself if not already there
129
- const relSelf = path.relative(process.cwd(), sourceFile);
130
- const files = [relSelf, ...relatedFiles.filter(f => f !== relSelf)];
131
- res.json({ files });
132
- } catch (err: any) {
133
- res.status(500).json({ error: err.message });
134
- }
135
- });
136
-
137
-
138
- // ─── Workflows API ───────────────────────────────────────────────────
139
- app.get('/api/workflows', (_req, res) => {
140
- const jeticDir = path.join(process.cwd(), '.jetic');
141
- try {
142
- if (!fs.existsSync(jeticDir)) return res.json([]);
143
- const files = fs.readdirSync(jeticDir).filter(f => f.endsWith('.json') && f !== 'model.json' && f !== 'memory.json');
144
- const workflows: any[] = [];
145
- for (const file of files) {
146
- try {
147
- const data = JSON.parse(fs.readFileSync(path.join(jeticDir, file), 'utf8'));
148
- if (data.steps && Array.isArray(data.steps)) workflows.push({ _file: file, ...data });
149
- } catch {}
150
- }
151
- res.json(workflows);
152
- } catch (err: any) {
153
- res.status(500).json({ error: err.message });
154
- }
155
- });
156
-
157
- // ─── Workflow run – Server-Sent Events stream ─────────────────────────
158
- app.post('/api/workflows/run', async (req, res) => {
159
- const { file } = req.body as { file: string };
160
- if (!file) { return res.status(400).json({ error: 'file is required' }); }
161
-
162
- const jeticDir = path.join(process.cwd(), '.jetic');
163
- const wfPath = path.join(jeticDir, file);
164
- if (!fs.existsSync(wfPath)) { return res.status(404).json({ error: 'workflow file not found' }); }
165
-
166
- let workflow: any;
167
- try { workflow = JSON.parse(fs.readFileSync(wfPath, 'utf8')); }
168
- catch (e: any) { return res.status(400).json({ error: e.message }); }
169
-
170
- // ── Pre-load async imports BEFORE opening the SSE stream so that
171
- // the async yield doesn't trigger req 'close' before the loop runs ──
172
- const { JeticMemory: Mem } = await import('@jetic/memory');
173
- const { faker } = await import('@faker-js/faker');
174
-
175
- // SSE headers – opened AFTER imports so aborted stays false
176
- res.setHeader('Content-Type', 'text/event-stream');
177
- res.setHeader('Cache-Control', 'no-cache');
178
- res.setHeader('Connection', 'keep-alive');
179
- res.flushHeaders();
180
-
181
- const send = (type: string, data: any) => {
182
- res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
183
- };
184
-
185
- // Resolve base URL from model
186
- let baseUrl = 'http://localhost:4000';
187
- const modelPath = path.join(jeticDir, 'model.json');
188
- if (fs.existsSync(modelPath)) {
189
- try {
190
- const model = JSON.parse(fs.readFileSync(modelPath, 'utf8'));
191
- const localEnv = (model.environments || []).find((e: any) => e.name === 'local');
192
- if (localEnv) baseUrl = localEnv.baseUrl;
193
- } catch {}
194
- }
195
-
196
- send('start', { name: workflow.name, totalSteps: workflow.steps.length, baseUrl });
197
-
198
- // Register close handler AFTER flushing start — any close before this is irrelevant
199
- let aborted = false;
200
- req.on('close', () => { aborted = true; });
201
-
202
- // ── inline the minimal executor (avoids importing CLI internals) ──
203
- function deepGet(obj: any, dotPath: string): any {
204
- return dotPath.replace(/\[(\d+)\]/g, '.$1').split('.').reduce((o, k) => o?.[k], obj);
205
- }
206
-
207
- async function resolveTemplate(value: string): Promise<string> {
208
- return value.replace(/\{\{([^}]+)\}\}/g, (_, expr) => {
209
- expr = expr.trim();
210
- if (expr.startsWith('faker.')) {
211
- try {
212
- let fn: any = faker;
213
- for (const part of expr.split('.').slice(1)) fn = fn[part];
214
- return typeof fn === 'function' ? String(fn()) : String(fn);
215
- } catch { return expr; }
216
- }
217
- return _; // memory refs handled below via async
218
- });
219
- // Note: memory template refs ({{scope:key}}) require async — handled separately
220
- }
221
-
222
- async function resolveMemoryTemplates(value: string): Promise<string> {
223
- const matches = [...value.matchAll(/\{\{([^}]+)\}\}/g)];
224
- let result = value;
225
- for (const [placeholder, expr] of matches.map(m => [m[0], m[1].trim()])) {
226
- if (expr.includes(':') && !expr.startsWith('faker.')) {
227
- const [scope, key] = expr.split(':', 2);
228
- const mem = new Mem({ scope });
229
- const val = await mem.get(key);
230
- result = result.replace(placeholder, val != null ? String(val) : '');
231
- }
232
- }
233
- return result;
234
- }
235
-
236
- async function resolveStr(v: string): Promise<string> {
237
- let s = await resolveTemplate(v);
238
- s = await resolveMemoryTemplates(s);
239
- return s;
240
- }
241
-
242
- async function executeStep(step: any): Promise<any> {
243
- const startTime = Date.now();
244
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
245
- const bodyExtra: Record<string, any> = {};
246
-
247
- // Resolve inject
248
- for (const [target, memKeyOrTpl] of Object.entries(step.inject ?? {}) as [string, string][]) {
249
- const strVal = memKeyOrTpl.includes('{{')
250
- ? await resolveStr(memKeyOrTpl)
251
- : await (async () => {
252
- const [scope, key] = memKeyOrTpl.includes(':') ? memKeyOrTpl.split(':', 2) : ['workflow', memKeyOrTpl];
253
- const val = await new Mem({ scope }).get(key);
254
- return val != null ? String(val) : '';
255
- })();
256
-
257
- if (!strVal) continue;
258
- if (target.startsWith('body:')) bodyExtra[target.slice(5)] = strVal;
259
- else headers[target.startsWith('header:') ? target.slice(7) : target] = strVal;
260
- }
261
-
262
- // Resolve body templates
263
- const resolvedBody: Record<string, any> = {};
264
- for (const [k, v] of Object.entries(step.body ?? {})) {
265
- resolvedBody[k] = typeof v === 'string' ? await resolveStr(v) : v;
266
- }
267
-
268
- const requestBody = { ...bodyExtra, ...resolvedBody };
269
-
270
- // captureInput
271
- for (const [memKey, bodyField] of Object.entries(step.captureInput ?? {}) as [string, string][]) {
272
- const val = requestBody[bodyField];
273
- if (val != null) {
274
- const [scope, key] = memKey.includes(':') ? memKey.split(':', 2) : ['workflow', memKey];
275
- await new Mem({ scope }).set(key, val);
276
- }
277
- }
278
-
279
- // Resolve path params
280
- let resolvedPath = step.path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_: string, p: string) =>
281
- requestBody[p] !== undefined ? String(requestBody[p]) : `:${p}`
282
- );
283
- // Also resolve {{}} in path
284
- resolvedPath = await resolveStr(resolvedPath);
285
-
286
- const finalUrl = `${baseUrl.replace(/\/$/, '')}${resolvedPath}`;
287
- const expectedStatus = step.expectStatus ?? 200;
288
-
289
- try {
290
- const isBodyMethod = !['GET', 'HEAD'].includes(step.method.toUpperCase());
291
- const fetchOpts: RequestInit = { method: step.method.toUpperCase(), headers };
292
- if (isBodyMethod && Object.keys(requestBody).length > 0) {
293
- fetchOpts.body = JSON.stringify(requestBody);
294
- } else if (!isBodyMethod && Object.keys(requestBody).length > 0) {
295
- const params = new URLSearchParams();
296
- for (const [k, v] of Object.entries(requestBody)) if (v != null) params.set(k, String(v));
297
- // Don't append query params for now — keep simple
298
- }
299
-
300
- const response = await fetch(finalUrl, fetchOpts);
301
- const durationMs = Date.now() - startTime;
302
-
303
- let responseBody: any = null;
304
- const ct = response.headers.get('content-type') ?? '';
305
- if (ct.includes('application/json')) {
306
- try { responseBody = await response.json(); } catch {}
307
- } else {
308
- responseBody = await response.text();
309
- }
310
-
311
- const passed = response.status === expectedStatus ||
312
- (response.status >= 200 && response.status < 300 && expectedStatus >= 200 && expectedStatus < 300);
313
-
314
- // Capture
315
- const captured: string[] = [];
316
- if (passed) {
317
- for (const [memKey, responsePath] of Object.entries(step.capture ?? {}) as [string, string][]) {
318
- const val = deepGet(responseBody, responsePath);
319
- if (val != null) {
320
- const [scope, key] = memKey.includes(':') ? memKey.split(':', 2) : ['workflow', memKey];
321
- await new Mem({ scope }).set(key, val);
322
- captured.push(`${memKey} ← ${responsePath}`);
323
- }
324
- }
325
- }
326
-
327
- return { status: response.status, passed, durationMs, captured, injected: headers, responseBody, error: null };
328
- } catch (err: any) {
329
- return { status: 0, passed: false, durationMs: Date.now() - startTime, captured: [], injected: headers, responseBody: null, error: err.message };
330
- }
331
- }
332
-
333
- // ── Execute steps, emitting SSE events ──
334
- let passed = 0; let failed = 0;
335
- for (let i = 0; i < workflow.steps.length; i++) {
336
- if (aborted) break;
337
- const step = workflow.steps[i];
338
- send('step_start', { index: i, step: { name: step.name, method: step.method, path: step.path } });
339
-
340
- const result = await executeStep(step);
341
-
342
- if (result.passed) passed++; else failed++;
343
- send('step_result', {
344
- index: i,
345
- step: { name: step.name, method: step.method, path: step.path, description: step.description },
346
- status: result.status,
347
- passed: result.passed,
348
- durationMs: result.durationMs,
349
- captured: result.captured,
350
- injected: result.injected,
351
- responseBody: result.responseBody,
352
- error: result.error,
353
- });
354
-
355
- // Only abort the chain if the step explicitly failed AND continueOnFailure is not set
356
- // AND the failure is a hard error (not just a bad status code)
357
- if (!result.passed && !step.continueOnFailure && result.error) {
358
- send('aborted', { index: i, reason: result.error ?? `Status ${result.status} ≠ ${step.expectStatus ?? 200}` });
359
- res.end();
360
- return;
361
- }
362
- }
363
-
364
- send('done', { passed, failed, total: workflow.steps.length });
365
- res.end();
366
- });
367
-
368
- // Memory API
369
- app.get('/api/memory', (_req, res) => {
370
- try {
371
- const allMemory = JeticMemory.getAllMemory();
372
- const entries: Array<{ key: string; value: any }> = [];
373
- for (const scope in allMemory) {
374
- for (const key in allMemory[scope]) {
375
- entries.push({ key: `${scope}:${key}`, value: allMemory[scope][key] });
376
- }
377
- }
378
- res.json(entries);
379
- } catch (err: any) {
380
- res.status(500).json({ error: err.message });
381
- }
382
- });
383
-
384
- app.post('/api/memory', async (req, res) => {
385
- const { key: rawKey, value } = req.body as { key: string; value: string };
386
- if (!rawKey || value === undefined) {
387
- return res.status(400).json({ error: 'key and value are required' });
388
- }
389
- const parts = rawKey.split(':');
390
- const scope = parts.length > 1 ? parts[0] : 'global';
391
- const key = parts.length > 1 ? parts.slice(1).join(':') : parts[0];
392
- const memory = new JeticMemory({ scope });
393
- await memory.set(key, value);
394
- res.json({ ok: true });
395
- });
396
-
397
- app.delete('/api/memory', async (req, res) => {
398
- const { key: rawKey } = req.body as { key: string };
399
- if (!rawKey) {
400
- return res.status(400).json({ error: 'key is required' });
401
- }
402
- const parts = rawKey.split(':');
403
- const scope = parts.length > 1 ? parts[0] : 'global';
404
- const key = parts.length > 1 ? parts.slice(1).join(':') : parts[0];
405
- const memory = new JeticMemory({ scope });
406
- await memory.delete(key);
407
- res.json({ ok: true });
408
- });
409
-
410
- // Locate the dashboard static files (bundled in dist/dashboard or resolved via workspace)
411
- try {
412
- let dashboardDistPath = path.join(__dirname, 'dashboard');
413
- if (!fs.existsSync(dashboardDistPath)) {
414
- dashboardDistPath = path.join(__dirname, '..', 'dashboard');
415
- }
416
- if (!fs.existsSync(dashboardDistPath)) {
417
- try {
418
- const dashboardPackagePath = require.resolve('@jetic/dashboard/package.json');
419
- dashboardDistPath = path.join(path.dirname(dashboardPackagePath), 'dist');
420
- } catch {}
421
- }
422
-
423
- if (!fs.existsSync(dashboardDistPath)) {
424
- console.warn(`Dashboard build not found at ${dashboardDistPath}. Please build the dashboard first.`);
425
- }
426
-
427
- // Serve static files
428
- app.use(express.static(dashboardDistPath));
429
-
430
- // SPA fallback
431
- app.get('*', (_req, res) => {
432
- const indexPath = path.join(dashboardDistPath, 'index.html');
433
- if (fs.existsSync(indexPath)) {
434
- res.sendFile(indexPath);
435
- } else {
436
- res.status(404).send('Dashboard not built yet.');
437
- }
438
- });
439
- } catch (e) {
440
- console.error('Could not find dashboard static files. Ensure it is built.', e);
441
- }
442
-
443
- app.listen(port, () => {
444
- const url = `http://localhost:${port}`;
445
- console.log(`\n🚀 Jetic Studio is running at ${url}\n`);
446
-
447
- const { exec } = require('child_process');
448
- const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open');
449
- exec(`${start} ${url}`);
450
- });
451
- });
@@ -1,64 +0,0 @@
1
- import { Command } from 'commander';
2
- import { loadConfig, saveConfig, ensureDirSync } from '@jetic/core';
3
- import * as readline from 'readline/promises';
4
- import { stdin as input, stdout as output } from 'process';
5
-
6
- export const initCommand = new Command('init')
7
- .description('Initialize Jetic in the current directory')
8
- .action(async () => {
9
- const config = loadConfig();
10
- ensureDirSync(config.jeticDir);
11
-
12
- console.log('\x1b[36mWelcome to Jetic Initialization\x1b[0m\n');
13
-
14
- const rl = readline.createInterface({ input, output });
15
-
16
- const providerInput = await rl.question('AI Provider [default: openrouter]: ');
17
- const modelInput = await rl.question('Model name [default: meta-llama/llama-3.1-8b-instruct]: ');
18
- const apiKeyInput = await rl.question('API Key: ');
19
-
20
- rl.close();
21
-
22
- const provider = providerInput.trim() || 'openrouter';
23
- const envVarName = `${provider.toUpperCase()}_API_KEY`;
24
-
25
- config.ai = {
26
- provider: provider,
27
- model: modelInput.trim() || 'meta-llama/llama-3.1-8b-instruct',
28
- apiKeyEnvVar: envVarName
29
- };
30
- saveConfig(config);
31
-
32
- if (apiKeyInput.trim()) {
33
- const fs = require('fs');
34
- const envPath = require('path').join(process.cwd(), '.env');
35
- const envContent = `\n${envVarName}=${apiKeyInput.trim()}\n`;
36
- fs.appendFileSync(envPath, envContent);
37
- console.log(`\n\x1b[32m✓\x1b[0m Saved API key to .env file as ${envVarName}`);
38
- console.log(`(Alternatively, you can run: set ${envVarName}=${apiKeyInput.trim()})\n`);
39
- }
40
-
41
- const banner = `
42
- \x1b[36m
43
- __ __ _ _____
44
- \\ \\ / /__| | ___ ___ _ __ ___ ___ |_ _|__
45
- \\ \\ /\\ / / _ \\ |/ __/ _ \\| '_ \` _ \\ / _ \\ | |/ _ \\
46
- \\ V V / __/ | (_| (_) | | | | | | __/ | | (_) |
47
- \\_/\\_/ \\___|_|\\___\\___/|_| |_| |_|\\___| |_|\\___/
48
-
49
- _ _____ _____ ___ ___
50
- | | ____|_ _|_ _/ __|
51
- _ | | _| | | | | |
52
- | |_| | |___ | | | | |__
53
- \\___/|_____| |_| |___\\___|
54
- \x1b[0m`;
55
-
56
- console.log(banner);
57
- console.log(`\x1b[32m✓\x1b[0m Successfully initialized Jetic in \x1b[1m${config.jeticDir}\x1b[0m\n`);
58
- console.log('Available Commands:');
59
- console.log(' \x1b[36mjetic init\x1b[0m Initialize Jetic in the current directory');
60
- console.log(' \x1b[36mjetic scan\x1b[0m Scan the project for API routes and generate behavioral model');
61
- console.log(' \x1b[36mjetic inspect\x1b[0m Inspect discovered endpoints and details');
62
- console.log(' \x1b[36mjetic config\x1b[0m Manage Jetic AI provider and settings');
63
- console.log('');
64
- });
@@ -1,47 +0,0 @@
1
- import { Command } from 'commander';
2
- import { loadConfig, readJsonSync } from '@jetic/core';
3
- import { BehavioralModel } from '@jetic/model';
4
- import * as path from 'path';
5
-
6
- export const inspectCommand = new Command('inspect')
7
- .description('Inspect the discovered API model')
8
- .action(() => {
9
- const config = loadConfig();
10
- const modelPath = path.join(config.jeticDir, 'model.json');
11
- const model = readJsonSync<BehavioralModel>(modelPath);
12
-
13
- if (!model) {
14
- console.error('No behavioral model found. Run `jetic scan` first.');
15
- return;
16
- }
17
-
18
- console.log(`Model Version: ${model.version}`);
19
- console.log(`Endpoints (${model.endpoints.length}):\n`);
20
-
21
- for (const ep of model.endpoints) {
22
- console.log(`${ep.method} ${ep.path}`);
23
- }
24
- });
25
-
26
- inspectCommand
27
- .command('endpoint <method> <path>')
28
- .description('Inspect a specific endpoint')
29
- .action((method, urlPath) => {
30
- const config = loadConfig();
31
- const modelPath = path.join(config.jeticDir, 'model.json');
32
- const model = readJsonSync<BehavioralModel>(modelPath);
33
-
34
- if (!model) return;
35
-
36
- const ep = model.endpoints.find(
37
- e => e.method.toLowerCase() === method.toLowerCase() && e.path === urlPath
38
- );
39
-
40
- if (!ep) {
41
- console.error('Endpoint not found');
42
- return;
43
- }
44
-
45
- console.log(`${ep.method} ${ep.path}\n`);
46
- console.log(`Source:\n ${ep.source.file}:${ep.source.line}`);
47
- });