@zibby/core 0.1.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 +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Code Node - Generate code implementation with optional commit/push
|
|
3
|
+
* Used by: analysisGraph (preview mode), implementationGraph (commit mode)
|
|
4
|
+
* Also generates PR title and description (Why/How format) for use when creating PRs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import Handlebars from 'handlebars';
|
|
11
|
+
import { invokeAgent } from '@zibby/core';
|
|
12
|
+
import { generatePRMeta } from './services/prMetaService.js';
|
|
13
|
+
import { adfToText } from '../../../src/utils/adf-converter.js';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
|
|
16
|
+
const CodeImplementationOutputSchema = z.object({
|
|
17
|
+
success: z.boolean(),
|
|
18
|
+
codeImplementation: z.object({
|
|
19
|
+
branchName: z.string().optional(),
|
|
20
|
+
committedRepos: z.array(z.string()).optional(),
|
|
21
|
+
diff: z.string(),
|
|
22
|
+
diffStat: z.string(),
|
|
23
|
+
changedFiles: z.array(z.string()),
|
|
24
|
+
fileContents: z.array(z.object({
|
|
25
|
+
path: z.string(),
|
|
26
|
+
content: z.string()
|
|
27
|
+
})),
|
|
28
|
+
prTitle: z.string().optional(),
|
|
29
|
+
prDescription: z.string().optional(),
|
|
30
|
+
metrics: z.object({
|
|
31
|
+
filesChanged: z.number(),
|
|
32
|
+
additions: z.number(),
|
|
33
|
+
deletions: z.number(),
|
|
34
|
+
netChange: z.number()
|
|
35
|
+
}),
|
|
36
|
+
commitMessage: z.string().optional(),
|
|
37
|
+
agentOutput: z.string(),
|
|
38
|
+
committed: z.boolean(),
|
|
39
|
+
timestamp: z.string()
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Factory function to create code generation node
|
|
45
|
+
* @param {Object} options - Configuration options
|
|
46
|
+
* @param {boolean} options.commitAndPush - If true, creates branch, commits, and pushes
|
|
47
|
+
* @param {string} options.nodeName - Name for the node (default: 'generate_code')
|
|
48
|
+
*/
|
|
49
|
+
export function createCodeGenerationNode(options = {}) {
|
|
50
|
+
const {
|
|
51
|
+
commitAndPush = false,
|
|
52
|
+
nodeName = 'generate_code'
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
name: nodeName,
|
|
57
|
+
outputSchema: CodeImplementationOutputSchema,
|
|
58
|
+
execute: async (state) => {
|
|
59
|
+
const mode = commitAndPush ? 'implementing' : 'generating preview of';
|
|
60
|
+
console.log(`\n💻 ${commitAndPush ? 'Implementing' : 'Generating'} code implementation...`);
|
|
61
|
+
|
|
62
|
+
const { workspace, ticketContext, repos, promptsDir, model, nodeConfigs = {} } = state;
|
|
63
|
+
const aiModel = model || ticketContext.model || 'auto';
|
|
64
|
+
const _nodeConfig = nodeConfigs[nodeName] || {};
|
|
65
|
+
const analysis = state.analyze_ticket?.analysis;
|
|
66
|
+
|
|
67
|
+
// Build implementation prompt from template
|
|
68
|
+
const implementPrompt = buildImplementationPrompt(
|
|
69
|
+
promptsDir,
|
|
70
|
+
ticketContext,
|
|
71
|
+
analysis,
|
|
72
|
+
commitAndPush,
|
|
73
|
+
repos,
|
|
74
|
+
workspace
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(`🚀 Running AI Agent to ${mode} changes with model: ${aiModel}...`);
|
|
78
|
+
|
|
79
|
+
const implementOutput = await invokeAgent(implementPrompt, { state, model: aiModel });
|
|
80
|
+
|
|
81
|
+
let branchName = null;
|
|
82
|
+
let commitMessage = null;
|
|
83
|
+
const committedRepos = [];
|
|
84
|
+
|
|
85
|
+
// If commit mode, create branch and commit in every repo that has changes
|
|
86
|
+
if (commitAndPush) {
|
|
87
|
+
const reposToCommit = repos && repos.length > 0 ? repos : [];
|
|
88
|
+
if (reposToCommit.length === 0) {
|
|
89
|
+
throw new Error('No repositories configured for commit/push');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
branchName = `feature/${ticketContext.ticketKey.toLowerCase()}-${generateShortId()}`;
|
|
93
|
+
const commitDesc = typeof ticketContext.description === 'object'
|
|
94
|
+
? adfToText(ticketContext.description)
|
|
95
|
+
: (ticketContext.description || 'No description');
|
|
96
|
+
commitMessage = `${ticketContext.ticketKey}: ${ticketContext.summary}\n\n${commitDesc}\n\nImplemented by Zibby Agent`;
|
|
97
|
+
|
|
98
|
+
for (const repo of reposToCommit) {
|
|
99
|
+
const repoPath = join(workspace, repo.name);
|
|
100
|
+
|
|
101
|
+
// Check if this repo has any changes
|
|
102
|
+
const status = await execCommand('git', ['status', '--porcelain'], repoPath);
|
|
103
|
+
if (!status.trim()) {
|
|
104
|
+
console.log(`⏭️ No changes in ${repo.name}, skipping`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`📊 Committing changes in ${repo.name}...`);
|
|
109
|
+
|
|
110
|
+
await execCommand('git', ['checkout', '-b', branchName], repoPath);
|
|
111
|
+
await execCommand('git', ['add', '.'], repoPath);
|
|
112
|
+
await execCommand('git', ['commit', '-m', commitMessage], repoPath);
|
|
113
|
+
|
|
114
|
+
// Set remote URL with token for push
|
|
115
|
+
const githubToken = state.githubToken;
|
|
116
|
+
const gitlabToken = process.env.GITLAB_TOKEN || '';
|
|
117
|
+
const gitlabUrl = process.env.GITLAB_URL || '';
|
|
118
|
+
|
|
119
|
+
if (repo.provider === 'gitlab' && gitlabToken && gitlabUrl) {
|
|
120
|
+
try {
|
|
121
|
+
const gitlabHost = new URL(gitlabUrl).host;
|
|
122
|
+
const remoteUrl = repo.url.replace(`https://${gitlabHost}`, `https://oauth2:${gitlabToken}@${gitlabHost}`);
|
|
123
|
+
await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.warn(`⚠️ Failed to set GitLab remote for ${repo.name}: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
} else if (githubToken && repo.url?.includes('github.com')) {
|
|
128
|
+
const remoteUrl = repo.url.replace('https://github.com', `https://x-access-token:${githubToken}@github.com`);
|
|
129
|
+
await execCommand('git', ['remote', 'set-url', 'origin', remoteUrl], repoPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`📤 Pushing ${repo.name} to remote...`);
|
|
133
|
+
await execCommand('git', ['push', '-u', 'origin', branchName], repoPath);
|
|
134
|
+
committedRepos.push(repo.name);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (committedRepos.length === 0) {
|
|
138
|
+
console.warn('⚠️ No repos had changes to commit');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('📊 Capturing code changes...');
|
|
143
|
+
|
|
144
|
+
// Capture git diff from ALL repos (multi-repo support)
|
|
145
|
+
// - Commit mode: compare last commit to previous (HEAD~1)
|
|
146
|
+
// - Preview mode: show all uncommitted changes (staged + unstaged)
|
|
147
|
+
const allDiffs = [];
|
|
148
|
+
const allDiffStats = [];
|
|
149
|
+
const allChangedFiles = [];
|
|
150
|
+
|
|
151
|
+
const reposToCheck = repos && repos.length > 0
|
|
152
|
+
? repos
|
|
153
|
+
: [{ name: '.' }];
|
|
154
|
+
|
|
155
|
+
for (const repo of reposToCheck) {
|
|
156
|
+
const currentRepoPath = repo.name === '.'
|
|
157
|
+
? workspace
|
|
158
|
+
: join(workspace, repo.name);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
let repoDiff, repoDiffStat, repoChangedFiles;
|
|
162
|
+
|
|
163
|
+
if (commitAndPush) {
|
|
164
|
+
repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD~1'], currentRepoPath);
|
|
165
|
+
repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD~1'], currentRepoPath);
|
|
166
|
+
const diffNameOutput = await execCommand('git', ['diff', '--name-only', 'HEAD~1'], currentRepoPath);
|
|
167
|
+
repoChangedFiles = diffNameOutput.split('\n').filter(f => f.trim());
|
|
168
|
+
} else {
|
|
169
|
+
repoDiff = await execCommand('git', ['diff', '-U10', 'HEAD'], currentRepoPath);
|
|
170
|
+
repoDiffStat = await execCommand('git', ['diff', '--stat', 'HEAD'], currentRepoPath);
|
|
171
|
+
|
|
172
|
+
const modifiedOutput = await execCommand('git', ['diff', '--name-only', 'HEAD'], currentRepoPath);
|
|
173
|
+
const modifiedFiles = modifiedOutput.split('\n').filter(f => f.trim());
|
|
174
|
+
const untrackedOutput = await execCommand('git', ['ls-files', '--others', '--exclude-standard'], currentRepoPath);
|
|
175
|
+
const untrackedFiles = untrackedOutput.split('\n').filter(f => f.trim());
|
|
176
|
+
repoChangedFiles = [...new Set([...modifiedFiles, ...untrackedFiles])];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const prefix = repos && repos.length > 1 ? `${repo.name}/` : '';
|
|
180
|
+
|
|
181
|
+
if (repoDiff.trim()) {
|
|
182
|
+
allDiffs.push(`# ${repo.name}\n${repoDiff}`);
|
|
183
|
+
}
|
|
184
|
+
if (repoDiffStat.trim()) {
|
|
185
|
+
allDiffStats.push(`# ${repo.name}\n${repoDiffStat}`);
|
|
186
|
+
}
|
|
187
|
+
if (repoChangedFiles.length > 0) {
|
|
188
|
+
allChangedFiles.push(...repoChangedFiles.map(f => `${prefix}${f}`));
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.warn(`⚠️ Could not capture diff for repo ${repo.name}:`, err.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const diff = allDiffs.join('\n\n');
|
|
196
|
+
const diffStat = allDiffStats.join('\n');
|
|
197
|
+
const changedFiles = allChangedFiles;
|
|
198
|
+
|
|
199
|
+
// Store file contents for expandable diff view
|
|
200
|
+
// Only store for reasonable number of files to avoid huge payloads
|
|
201
|
+
const fileContents = [];
|
|
202
|
+
if (changedFiles.length <= 20) {
|
|
203
|
+
for (const filePath of changedFiles) {
|
|
204
|
+
try {
|
|
205
|
+
let fullPath;
|
|
206
|
+
if (repos && repos.length > 1) {
|
|
207
|
+
fullPath = resolve(workspace, filePath);
|
|
208
|
+
} else {
|
|
209
|
+
const repoDir = repos?.[0] ? resolve(workspace, repos[0].name) : workspace;
|
|
210
|
+
fullPath = resolve(repoDir, filePath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!fullPath.startsWith(resolve(workspace))) continue;
|
|
214
|
+
if (existsSync(fullPath)) {
|
|
215
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
216
|
+
if (content.length < 100000) {
|
|
217
|
+
fileContents.push({ path: filePath, content });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (_err) {
|
|
221
|
+
// Skip files we can't read
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Parse diff for metrics
|
|
227
|
+
const additions = (diff.match(/^\+[^+]/gm) || []).length;
|
|
228
|
+
const deletions = (diff.match(/^-[^-]/gm) || []).length;
|
|
229
|
+
const filesChanged = changedFiles.length;
|
|
230
|
+
|
|
231
|
+
// Generate PR title and description (Why/How format) when we have changes
|
|
232
|
+
let prTitle = null;
|
|
233
|
+
let prDescription = null;
|
|
234
|
+
if (diff.trim() && ticketContext?.ticketKey) {
|
|
235
|
+
console.log('📝 Generating PR title and description...');
|
|
236
|
+
try {
|
|
237
|
+
const prMeta = await generatePRMeta({
|
|
238
|
+
ticketContext,
|
|
239
|
+
changedFiles,
|
|
240
|
+
diffStat,
|
|
241
|
+
state,
|
|
242
|
+
model: aiModel
|
|
243
|
+
}).catch(err => {
|
|
244
|
+
console.error(' ⚠️ Promise rejection in generatePRMeta:', err.message);
|
|
245
|
+
throw err;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(' 🔍 generatePRMeta returned:', typeof prMeta, prMeta ? Object.keys(prMeta) : 'null/undefined');
|
|
249
|
+
|
|
250
|
+
if (prMeta && typeof prMeta === 'object') {
|
|
251
|
+
prTitle = prMeta.prTitle;
|
|
252
|
+
prDescription = prMeta.prDescription;
|
|
253
|
+
console.log(` ✅ PR title: ${prTitle}`);
|
|
254
|
+
console.log(` ✅ PR description preview: ${prDescription?.substring(0, 100)}...`);
|
|
255
|
+
} else {
|
|
256
|
+
console.error(' ⚠️ generatePRMeta returned invalid result:', prMeta);
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error(' ❌ Failed to generate PR meta:', err.message);
|
|
260
|
+
console.error(' Error details:', err.stack);
|
|
261
|
+
} finally {
|
|
262
|
+
console.log(' ✅ PR meta generation attempt complete');
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
console.log('📝 Skipping PR meta generation (no diff or no ticketKey)');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(`✅ Code ${commitAndPush ? 'implementation' : 'generation'} complete`);
|
|
269
|
+
if (branchName) {
|
|
270
|
+
console.log(` 📁 Branch: ${branchName}`);
|
|
271
|
+
if (committedRepos.length > 0) {
|
|
272
|
+
console.log(` 📦 Committed repos: ${committedRepos.join(', ')}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
console.log(` 📁 Files changed: ${filesChanged}`);
|
|
276
|
+
console.log(` ➕ Additions: ${additions} lines`);
|
|
277
|
+
console.log(` ➖ Deletions: ${deletions} lines`);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
codeImplementation: {
|
|
282
|
+
...(branchName && { branchName }),
|
|
283
|
+
...(committedRepos.length > 0 && { committedRepos }),
|
|
284
|
+
diff,
|
|
285
|
+
diffStat,
|
|
286
|
+
changedFiles,
|
|
287
|
+
fileContents,
|
|
288
|
+
...(prTitle && { prTitle }),
|
|
289
|
+
...(prDescription && { prDescription }),
|
|
290
|
+
metrics: {
|
|
291
|
+
filesChanged,
|
|
292
|
+
additions,
|
|
293
|
+
deletions,
|
|
294
|
+
netChange: additions - deletions
|
|
295
|
+
},
|
|
296
|
+
...(commitMessage && { commitMessage }),
|
|
297
|
+
agentOutput: implementOutput,
|
|
298
|
+
committed: commitAndPush,
|
|
299
|
+
timestamp: new Date().toISOString()
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Convenience exports for common use cases
|
|
307
|
+
export const generateCodeNode = createCodeGenerationNode({
|
|
308
|
+
commitAndPush: false,
|
|
309
|
+
nodeName: 'generate_code'
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export const implementCodeNode = createCodeGenerationNode({
|
|
313
|
+
commitAndPush: true,
|
|
314
|
+
nodeName: 'implement_code'
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
function buildImplementationPrompt(promptsDir, ticketContext, analysis, isCommitting, repos, workspace) {
|
|
318
|
+
const templatePath = join(promptsDir, 'generate-code.md');
|
|
319
|
+
if (!existsSync(templatePath)) {
|
|
320
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const templateContent = readFileSync(templatePath, 'utf-8');
|
|
324
|
+
const template = Handlebars.compile(templateContent);
|
|
325
|
+
|
|
326
|
+
let descriptionText = ticketContext.description || 'No description provided';
|
|
327
|
+
console.log(`🔍 [generateCode] description type: ${typeof descriptionText}, value: ${typeof descriptionText === 'object' ? JSON.stringify(descriptionText).slice(0, 200) : String(descriptionText).slice(0, 200)}`);
|
|
328
|
+
if (typeof descriptionText === 'object') {
|
|
329
|
+
descriptionText = adfToText(descriptionText);
|
|
330
|
+
console.log(`🔍 [generateCode] Converted ADF to text (${descriptionText.length} chars): ${descriptionText.slice(0, 200)}...`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let planContext = null;
|
|
334
|
+
if (analysis) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed = typeof analysis.raw === 'string' ? JSON.parse(analysis.raw) : analysis.raw;
|
|
337
|
+
const inner = parsed?.analysis?.structured || parsed;
|
|
338
|
+
planContext = inner?.implementationPlan || null;
|
|
339
|
+
} catch (_) {}
|
|
340
|
+
if (!planContext) {
|
|
341
|
+
planContext = typeof analysis.raw === 'string' ? analysis.raw.slice(0, 3000) : null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Build repo metadata for the template
|
|
346
|
+
const repositories = (repos && repos.length > 0) ? repos.map(r => {
|
|
347
|
+
const info = detectRepoLanguage(workspace ? join(workspace, r.name) : null);
|
|
348
|
+
return { name: r.name, ...info };
|
|
349
|
+
}) : null;
|
|
350
|
+
|
|
351
|
+
return template({
|
|
352
|
+
ticketKey: ticketContext.ticketKey,
|
|
353
|
+
ticketSummary: ticketContext.summary,
|
|
354
|
+
ticketDescription: descriptionText,
|
|
355
|
+
acceptanceCriteria: ticketContext.acceptanceCriteria || null,
|
|
356
|
+
analysisContext: planContext,
|
|
357
|
+
repositories,
|
|
358
|
+
isCommitting
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function detectRepoLanguage(repoPath) {
|
|
363
|
+
if (!repoPath) return { language: 'Unknown', framework: null };
|
|
364
|
+
try {
|
|
365
|
+
if (existsSync(join(repoPath, 'package.json'))) {
|
|
366
|
+
const pkg = JSON.parse(readFileSync(join(repoPath, 'package.json'), 'utf-8'));
|
|
367
|
+
const framework = pkg.dependencies?.react ? 'React'
|
|
368
|
+
: pkg.dependencies?.next ? 'Next.js'
|
|
369
|
+
: pkg.dependencies?.vue ? 'Vue'
|
|
370
|
+
: pkg.dependencies?.express ? 'Express'
|
|
371
|
+
: null;
|
|
372
|
+
return { language: 'JavaScript/TypeScript', framework };
|
|
373
|
+
}
|
|
374
|
+
if (existsSync(join(repoPath, 'pom.xml')) || existsSync(join(repoPath, 'build.gradle'))) {
|
|
375
|
+
return { language: 'Java', framework: null };
|
|
376
|
+
}
|
|
377
|
+
if (existsSync(join(repoPath, 'requirements.txt')) || existsSync(join(repoPath, 'setup.py'))) {
|
|
378
|
+
return { language: 'Python', framework: null };
|
|
379
|
+
}
|
|
380
|
+
} catch (_) {}
|
|
381
|
+
return { language: 'Unknown', framework: null };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function generateShortId() {
|
|
385
|
+
return Math.random().toString(36).substring(2, 6);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Safe spawn wrapper — takes an explicit args array, never uses shell: true.
|
|
390
|
+
* @param {string} bin - executable (e.g. 'git')
|
|
391
|
+
* @param {string[]} args - argument list
|
|
392
|
+
* @param {string} cwd - working directory
|
|
393
|
+
*/
|
|
394
|
+
async function execCommand(bin, args, cwd) {
|
|
395
|
+
return new Promise((res, reject) => {
|
|
396
|
+
const proc = spawn(bin, args, { cwd, shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
397
|
+
|
|
398
|
+
let stdout = '';
|
|
399
|
+
let stderr = '';
|
|
400
|
+
|
|
401
|
+
proc.stdout.on('data', (data) => {
|
|
402
|
+
const output = data.toString();
|
|
403
|
+
stdout += output;
|
|
404
|
+
console.log(output.trimEnd());
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
proc.stderr.on('data', (data) => {
|
|
408
|
+
const output = data.toString();
|
|
409
|
+
stderr += output;
|
|
410
|
+
console.log(output.trimEnd());
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
proc.on('close', (code) => {
|
|
414
|
+
if (code !== 0) {
|
|
415
|
+
reject(new Error(`Command failed (exit ${code}): ${bin} ${args.join(' ')}`));
|
|
416
|
+
} else {
|
|
417
|
+
res(stdout || stderr || '');
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
proc.on('error', (err) => {
|
|
422
|
+
reject(new Error(`Command error: ${bin} - ${err.message}`));
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|