@zibby/cli 0.1.5
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 +926 -0
- package/bin/zibby.js +266 -0
- package/package.json +65 -0
- package/src/auth/cli-login.js +406 -0
- package/src/commands/analyze-graph.js +334 -0
- package/src/commands/ci-setup.js +65 -0
- package/src/commands/implement.js +664 -0
- package/src/commands/init.js +736 -0
- package/src/commands/list-projects.js +78 -0
- package/src/commands/memory.js +171 -0
- package/src/commands/run.js +926 -0
- package/src/commands/setup-scripts.js +101 -0
- package/src/commands/upload.js +163 -0
- package/src/commands/video.js +30 -0
- package/src/commands/workflow.js +369 -0
- package/src/config/config.js +117 -0
- package/src/config/environments.js +145 -0
- package/src/utils/execution-context.js +25 -0
- package/src/utils/progress-reporter.js +155 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analyze Command with Graph Architecture
|
|
5
|
+
*
|
|
6
|
+
* Uses multi-node graph workflow for comprehensive analysis:
|
|
7
|
+
* - Ticket analysis
|
|
8
|
+
* - Code implementation generation (with git diff)
|
|
9
|
+
* - Test case generation
|
|
10
|
+
* - Complexity assessment
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { dirname, join, resolve } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { readFileSync, existsSync } from 'fs';
|
|
16
|
+
import { compileGraph, validateGraphConfig } from '@zibby/core/framework/graph-compiler.js';
|
|
17
|
+
import { WorkflowGraph } from '@zibby/core/framework/graph.js';
|
|
18
|
+
import { buildAnalysisGraph } from '@zibby/core/templates/code-analysis/graph.js';
|
|
19
|
+
import { analysisStateSchema } from '@zibby/core/templates/code-analysis/state.js';
|
|
20
|
+
import '@zibby/core/templates/register-nodes.js';
|
|
21
|
+
import { fetchExecutionContext } from '../utils/execution-context.js';
|
|
22
|
+
import { reportProgress, reportArtifact, reportFinalStatus } from '../utils/progress-reporter.js';
|
|
23
|
+
import { writeMcpConfig } from '@zibby/core/utils/mcp-config-writer.js';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Node execution middleware - Captures logs and reports progress
|
|
30
|
+
* This is configured OUTSIDE the framework and passed in
|
|
31
|
+
*/
|
|
32
|
+
// Maps node names ā how to extract their artifact from the node result
|
|
33
|
+
const NODE_ARTIFACT_MAP = {
|
|
34
|
+
analyze_ticket: (result) => ({
|
|
35
|
+
key: 'analysis',
|
|
36
|
+
value: { raw: result.raw, structured: result.output }
|
|
37
|
+
}),
|
|
38
|
+
generate_code: (result) => ({
|
|
39
|
+
key: 'codeImplementation',
|
|
40
|
+
value: result.output?.codeImplementation
|
|
41
|
+
}),
|
|
42
|
+
generate_test_cases: (result) => ({
|
|
43
|
+
key: 'tests',
|
|
44
|
+
value: result.output?.tests
|
|
45
|
+
}),
|
|
46
|
+
finalize: (result) => ({
|
|
47
|
+
key: 'report',
|
|
48
|
+
value: result.output?.report
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function createLogCapturingMiddleware(reportProgressFn, reportArtifactFn) {
|
|
53
|
+
return async function nodeMiddleware(nodeName, executeNode, state) {
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
const logBuffer = [];
|
|
56
|
+
let lastSentLogs = '';
|
|
57
|
+
|
|
58
|
+
const originalLog = console.log;
|
|
59
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
60
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
61
|
+
|
|
62
|
+
// Flag to prevent double-capture: console.log internally calls process.stdout.write
|
|
63
|
+
let insideConsoleLog = false;
|
|
64
|
+
|
|
65
|
+
console.log = (...args) => {
|
|
66
|
+
const message = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
67
|
+
logBuffer.push(message);
|
|
68
|
+
insideConsoleLog = true;
|
|
69
|
+
originalLog(...args);
|
|
70
|
+
insideConsoleLog = false;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Capture direct process.stdout.write (agent streaming output) but skip console.log echoes
|
|
74
|
+
let stdoutLineBuffer = '';
|
|
75
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
76
|
+
if (!insideConsoleLog) {
|
|
77
|
+
const text = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
78
|
+
stdoutLineBuffer += text;
|
|
79
|
+
const lines = stdoutLineBuffer.split('\n');
|
|
80
|
+
stdoutLineBuffer = lines.pop() || '';
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (trimmed) {
|
|
84
|
+
logBuffer.push(trimmed);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return originalStdoutWrite(chunk, encoding, callback);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
originalLog(`[Middleware] Started capturing logs for ${nodeName}`);
|
|
92
|
+
|
|
93
|
+
let timerCleared = false;
|
|
94
|
+
const liveLogInterval = setInterval(() => {
|
|
95
|
+
if (timerCleared) return;
|
|
96
|
+
|
|
97
|
+
const currentLogs = logBuffer.join('\n');
|
|
98
|
+
if (currentLogs !== lastSentLogs && currentLogs.length > 0) {
|
|
99
|
+
lastSentLogs = currentLogs;
|
|
100
|
+
// Write middleware status to stderr to avoid interleaving with agent streaming on stdout
|
|
101
|
+
originalStderrWrite(`š” [Middleware] Sending live update for ${nodeName}: ${currentLogs.length} chars, ${logBuffer.length} lines\n`);
|
|
102
|
+
reportProgressFn(nodeName, 'in_progress', currentLogs, state).catch((err) => {
|
|
103
|
+
originalStderrWrite(`ā ļø [Middleware] Failed to send live update: ${err.message}\n`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}, 500);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await reportProgressFn(nodeName, 'in_progress', '', state);
|
|
110
|
+
|
|
111
|
+
const result = await executeNode();
|
|
112
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
113
|
+
|
|
114
|
+
timerCleared = true;
|
|
115
|
+
clearInterval(liveLogInterval);
|
|
116
|
+
await new Promise(resolveFn => setImmediate(resolveFn));
|
|
117
|
+
console.log = originalLog;
|
|
118
|
+
process.stdout.write = originalStdoutWrite;
|
|
119
|
+
// Flush any remaining partial line
|
|
120
|
+
if (stdoutLineBuffer.trim()) {
|
|
121
|
+
logBuffer.push(stdoutLineBuffer.trim());
|
|
122
|
+
stdoutLineBuffer = '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const finalLogs = logBuffer.join('\n');
|
|
126
|
+
originalStderrWrite(`š” [Middleware] Sending final update for ${nodeName}: ${finalLogs.length} chars, ${logBuffer.length} total lines captured\n`);
|
|
127
|
+
if (result.success) {
|
|
128
|
+
await reportProgressFn(nodeName, 'success', finalLogs || `Completed in ${duration}s`, state);
|
|
129
|
+
|
|
130
|
+
// Send artifact for this node immediately (survives even if later nodes fail)
|
|
131
|
+
const extractor = NODE_ARTIFACT_MAP[nodeName];
|
|
132
|
+
if (extractor) {
|
|
133
|
+
const { key, value } = extractor(result);
|
|
134
|
+
if (value) {
|
|
135
|
+
await reportArtifactFn(state, key, value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
await reportProgressFn(nodeName, 'failed', `${finalLogs }\n\nError: ${result.error}`, state);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
timerCleared = true;
|
|
145
|
+
clearInterval(liveLogInterval);
|
|
146
|
+
await new Promise(resolveFn => setImmediate(resolveFn));
|
|
147
|
+
console.log = originalLog;
|
|
148
|
+
process.stdout.write = originalStdoutWrite;
|
|
149
|
+
|
|
150
|
+
const errorLogs = `${logBuffer.join('\n') }\n\nError: ${error.message}`;
|
|
151
|
+
await reportProgressFn(nodeName, 'failed', errorLogs, state);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Main analyze command
|
|
159
|
+
*/
|
|
160
|
+
export async function analyzeCommand(options) {
|
|
161
|
+
const {
|
|
162
|
+
EXECUTION_ID,
|
|
163
|
+
TICKET_KEY,
|
|
164
|
+
PROJECT_ID,
|
|
165
|
+
REPOS,
|
|
166
|
+
PROGRESS_QUEUE_URL,
|
|
167
|
+
PROGRESS_API_URL,
|
|
168
|
+
SQS_AUTH_TOKEN,
|
|
169
|
+
PROJECT_API_TOKEN,
|
|
170
|
+
GITHUB_TOKEN,
|
|
171
|
+
MODEL
|
|
172
|
+
} = process.env;
|
|
173
|
+
|
|
174
|
+
// Validate required env vars
|
|
175
|
+
if (!EXECUTION_ID || !TICKET_KEY || !PROJECT_ID) {
|
|
176
|
+
console.error('ā Missing required environment variables');
|
|
177
|
+
console.error(' Required: EXECUTION_ID, TICKET_KEY, PROJECT_ID');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fetch large data (ticketContext, nodeConfigs) from DynamoDB instead of env vars
|
|
182
|
+
// This avoids the ECS 8192-char container overrides limit
|
|
183
|
+
const execCtx = await fetchExecutionContext(EXECUTION_ID, PROJECT_ID);
|
|
184
|
+
const ticketContext = execCtx.ticketContext;
|
|
185
|
+
let nodeConfigs = execCtx.nodeConfigs || {};
|
|
186
|
+
const repos = REPOS ? JSON.parse(REPOS) : execCtx.repos;
|
|
187
|
+
const workspace = process.env.WORKSPACE || '/workspace';
|
|
188
|
+
const promptsDir = join(dirname(dirname(dirname(__dirname))), 'core', 'templates', 'code-analysis', 'prompts');
|
|
189
|
+
|
|
190
|
+
console.log('\nš Zibby Analysis (Graph Mode)');
|
|
191
|
+
console.log('ā'.repeat(60));
|
|
192
|
+
console.log(`Ticket: ${TICKET_KEY}`);
|
|
193
|
+
console.log(`Repositories: ${repos.length}`);
|
|
194
|
+
console.log(`Workspace: ${workspace}`);
|
|
195
|
+
console.log(`AI Model: ${MODEL || 'auto'}`);
|
|
196
|
+
console.log('ā'.repeat(60));
|
|
197
|
+
|
|
198
|
+
// Compile graph from: --workflow flag (local file) > S3 config > default
|
|
199
|
+
const logMiddleware = createLogCapturingMiddleware(reportProgress, reportArtifact);
|
|
200
|
+
let graphConfig;
|
|
201
|
+
let graphSource;
|
|
202
|
+
|
|
203
|
+
let jsWorkflowModule = null;
|
|
204
|
+
|
|
205
|
+
if (options?.workflow) {
|
|
206
|
+
const workflowPath = resolve(process.cwd(), options.workflow);
|
|
207
|
+
if (!existsSync(workflowPath)) {
|
|
208
|
+
console.error(`ā Workflow file not found: ${workflowPath}`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (workflowPath.endsWith('.js') || workflowPath.endsWith('.mjs')) {
|
|
213
|
+
try {
|
|
214
|
+
const { pathToFileURL } = await import('url');
|
|
215
|
+
jsWorkflowModule = await import(pathToFileURL(workflowPath).href);
|
|
216
|
+
graphSource = `local JS module (${workflowPath})`;
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(`ā Failed to load workflow JS module: ${err.message}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
try {
|
|
223
|
+
const fileContent = JSON.parse(readFileSync(workflowPath, 'utf-8'));
|
|
224
|
+
const { _meta, ...config } = fileContent;
|
|
225
|
+
graphConfig = config;
|
|
226
|
+
graphSource = `local file (${workflowPath})`;
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`ā Failed to parse workflow file: ${err.message}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const validation = validateGraphConfig(graphConfig);
|
|
232
|
+
if (!validation.valid) {
|
|
233
|
+
console.error('ā Invalid workflow file:');
|
|
234
|
+
validation.errors.forEach(e => console.error(` - ${e}`));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else if (execCtx.graphConfig) {
|
|
239
|
+
graphConfig = execCtx.graphConfig;
|
|
240
|
+
graphSource = 'custom (from project workflow)';
|
|
241
|
+
} else {
|
|
242
|
+
const graph = new WorkflowGraph();
|
|
243
|
+
buildAnalysisGraph(graph);
|
|
244
|
+
graphConfig = graph.serialize();
|
|
245
|
+
graphSource = 'default';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let graph;
|
|
249
|
+
|
|
250
|
+
if (jsWorkflowModule) {
|
|
251
|
+
const jsNodeConfigs = jsWorkflowModule.nodeConfigs || {};
|
|
252
|
+
const mergedNodeConfigs = { ...jsNodeConfigs, ...nodeConfigs };
|
|
253
|
+
graph = jsWorkflowModule.buildGraph({ nodeMiddleware: logMiddleware });
|
|
254
|
+
console.log(`š Graph source: ${graphSource}`);
|
|
255
|
+
console.log(` Nodes: ${graph.nodes.size}`);
|
|
256
|
+
nodeConfigs = mergedNodeConfigs;
|
|
257
|
+
} else {
|
|
258
|
+
if (nodeConfigs && Object.keys(nodeConfigs).length > 0) {
|
|
259
|
+
const base = graphConfig.nodeConfigs || {};
|
|
260
|
+
const merged = { ...base };
|
|
261
|
+
for (const [nodeId, overrides] of Object.entries(nodeConfigs)) {
|
|
262
|
+
merged[nodeId] = { ...base[nodeId], ...overrides };
|
|
263
|
+
}
|
|
264
|
+
graphConfig.nodeConfigs = merged;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(`š Graph source: ${graphSource}`);
|
|
268
|
+
console.log(` Nodes: ${graphConfig.nodes?.length || 0}`);
|
|
269
|
+
console.log(` Edges: ${graphConfig.edges?.length || 0}`);
|
|
270
|
+
|
|
271
|
+
graph = compileGraph(graphConfig, { nodeMiddleware: logMiddleware, stateSchema: analysisStateSchema });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Write MCP config for any tools required by custom nodes (jira, slack_notify, github)
|
|
275
|
+
// Must be done before graph.run() so Cursor agent has MCP servers configured
|
|
276
|
+
writeMcpConfig(nodeConfigs);
|
|
277
|
+
|
|
278
|
+
// Execute graph
|
|
279
|
+
const initialState = {
|
|
280
|
+
EXECUTION_ID,
|
|
281
|
+
PROGRESS_QUEUE_URL,
|
|
282
|
+
PROGRESS_API_URL,
|
|
283
|
+
SQS_AUTH_TOKEN,
|
|
284
|
+
PROJECT_API_TOKEN,
|
|
285
|
+
workspace,
|
|
286
|
+
repos,
|
|
287
|
+
ticketContext,
|
|
288
|
+
promptsDir,
|
|
289
|
+
githubToken: GITHUB_TOKEN,
|
|
290
|
+
model: MODEL,
|
|
291
|
+
nodeConfigs
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const result = await graph.run(null, initialState);
|
|
296
|
+
|
|
297
|
+
// Artifacts are already sent per-node by the middleware (NODE_ARTIFACT_MAP).
|
|
298
|
+
// Here we only determine the final status and send it.
|
|
299
|
+
const finalState = result.state;
|
|
300
|
+
const validation = finalState.analyze_ticket_output?.validation
|
|
301
|
+
|| finalState.analyze_ticket_output?.analysis?.structured?.validation;
|
|
302
|
+
let finalStatus = 'completed';
|
|
303
|
+
if (validation && !validation.canProceed) {
|
|
304
|
+
finalStatus = validation.status === 'insufficient_context'
|
|
305
|
+
? 'insufficient_context'
|
|
306
|
+
: 'blocked';
|
|
307
|
+
}
|
|
308
|
+
console.log(`\nš Validation: canProceed=${validation?.canProceed}, status=${validation?.status}, finalStatus=${finalStatus}`);
|
|
309
|
+
|
|
310
|
+
console.log(`\nš Sending final status: ${finalStatus}`);
|
|
311
|
+
await reportFinalStatus(initialState, { status: finalStatus });
|
|
312
|
+
|
|
313
|
+
console.log('\nā
Analysis completed successfully');
|
|
314
|
+
process.exit(0);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
console.error('\nā Analysis failed:', error.message);
|
|
317
|
+
|
|
318
|
+
if (EXECUTION_ID) {
|
|
319
|
+
try {
|
|
320
|
+
console.log(`š” Reporting failure...`);
|
|
321
|
+
await reportFinalStatus(initialState, { status: 'failed', error: error.message });
|
|
322
|
+
} catch (_apiError) {
|
|
323
|
+
console.error('ā ļø Failed to report error');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Execute if run directly
|
|
332
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
333
|
+
analyzeCommand();
|
|
334
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { patchCursorAgentForCI, checkCursorAgentPatched, getApprovalKeys, saveApprovalKeys } from '@zibby/core';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
export async function ciSetupCommand(options) {
|
|
7
|
+
console.log(chalk.bold.cyan('\nš§ Setting up CI/CD for Cursor Agent\n'));
|
|
8
|
+
console.log(chalk.gray('ā'.repeat(50)));
|
|
9
|
+
|
|
10
|
+
const check = checkCursorAgentPatched();
|
|
11
|
+
|
|
12
|
+
if (!check.installed) {
|
|
13
|
+
console.log(chalk.red('\nā cursor-agent is not installed!\n'));
|
|
14
|
+
console.log(chalk.white('To install:'));
|
|
15
|
+
console.log(chalk.gray(' curl https://cursor.com/install -fsS | bash\n'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (check.patched) {
|
|
20
|
+
console.log(chalk.green('ā
cursor-agent is already patched for CI/CD\n'));
|
|
21
|
+
} else {
|
|
22
|
+
const spinner = ora('Patching cursor-agent...').start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await patchCursorAgentForCI();
|
|
26
|
+
spinner.succeed('cursor-agent patched successfully');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
spinner.fail('Failed to patch cursor-agent');
|
|
29
|
+
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.getKeys) {
|
|
35
|
+
const spinner = ora('Getting approval keys...').start();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await getApprovalKeys(resolve(process.cwd()));
|
|
39
|
+
spinner.succeed('Approval keys retrieved');
|
|
40
|
+
|
|
41
|
+
console.log(chalk.cyan('\nš Approval Keys:\n'));
|
|
42
|
+
Object.entries(result.keys).forEach(([name, key]) => {
|
|
43
|
+
console.log(chalk.white(` ${name}: ${chalk.gray(key)}`));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (options.save) {
|
|
47
|
+
saveApprovalKeys(resolve(process.cwd()), result.keys);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.gray('\nTo save these keys, run:'));
|
|
50
|
+
console.log(chalk.white(' zibby ci-setup --get-keys --save\n'));
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
spinner.fail('Failed to get approval keys');
|
|
54
|
+
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.green('\nā
CI/CD setup complete!\n'));
|
|
60
|
+
console.log(chalk.cyan('Next steps:'));
|
|
61
|
+
console.log(chalk.white(' 1. Get approval keys: zibby ci-setup --get-keys'));
|
|
62
|
+
console.log(chalk.white(' 2. Save to project: zibby ci-setup --get-keys --save'));
|
|
63
|
+
console.log(chalk.white(' 3. Use in CI: zibby run <spec> --agent=cursor --auto-approve\n'));
|
|
64
|
+
}
|
|
65
|
+
|