@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.
@@ -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
+