wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Jira Integration
|
|
5
|
+
*
|
|
6
|
+
* Sync tasks between Wogi Flow and Jira. Supports:
|
|
7
|
+
* - Listing assigned Jira issues
|
|
8
|
+
* - Importing issues to ready.json
|
|
9
|
+
* - Syncing completed tasks back to Jira
|
|
10
|
+
*
|
|
11
|
+
* Part of Phase 6: Team & Integrations
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* flow jira list List assigned issues
|
|
15
|
+
* flow jira sync Import issues to ready.json
|
|
16
|
+
* flow jira push Push completed tasks to Jira
|
|
17
|
+
* flow jira config Show/set Jira configuration
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { HttpClient } = require('./flow-http-client');
|
|
24
|
+
const { TIMEOUTS } = require('./flow-constants');
|
|
25
|
+
const {
|
|
26
|
+
PROJECT_ROOT,
|
|
27
|
+
STATE_DIR,
|
|
28
|
+
parseFlags,
|
|
29
|
+
color,
|
|
30
|
+
info,
|
|
31
|
+
warn,
|
|
32
|
+
error,
|
|
33
|
+
success,
|
|
34
|
+
fileExists,
|
|
35
|
+
safeJsonParse,
|
|
36
|
+
getConfig,
|
|
37
|
+
setConfigValue,
|
|
38
|
+
resolveConfigValue,
|
|
39
|
+
printHeader,
|
|
40
|
+
generateTaskId,
|
|
41
|
+
writeJson,
|
|
42
|
+
getReadyData
|
|
43
|
+
} = require('./flow-utils');
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// Constants
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
const READY_PATH = path.join(STATE_DIR, 'ready.json');
|
|
50
|
+
const JIRA_CACHE_PATH = path.join(STATE_DIR, 'jira-cache.json');
|
|
51
|
+
const CACHE_TTL_MS = TIMEOUTS.CACHE_TTL;
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// Configuration
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Jira configuration from config.json
|
|
59
|
+
*/
|
|
60
|
+
function getJiraConfig() {
|
|
61
|
+
const config = getConfig();
|
|
62
|
+
const jiraConfig = config?.integrations?.jira || {};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
enabled: jiraConfig.enabled || false,
|
|
66
|
+
baseUrl: jiraConfig.baseUrl || null,
|
|
67
|
+
projectKey: jiraConfig.projectKey || null,
|
|
68
|
+
email: jiraConfig.email || null,
|
|
69
|
+
apiToken: resolveConfigValue(jiraConfig.apiToken),
|
|
70
|
+
jqlFilter: jiraConfig.jqlFilter || 'assignee = currentUser() AND status != Done ORDER BY priority DESC',
|
|
71
|
+
syncStatuses: jiraConfig.syncStatuses || {
|
|
72
|
+
ready: ['To Do', 'Open', 'Backlog'],
|
|
73
|
+
inProgress: ['In Progress', 'In Review'],
|
|
74
|
+
completed: ['Done', 'Closed', 'Resolved']
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save Jira configuration (async with locking)
|
|
81
|
+
*/
|
|
82
|
+
async function saveJiraConfig(jiraConfig) {
|
|
83
|
+
const config = getConfig();
|
|
84
|
+
const currentJira = config?.integrations?.jira || {};
|
|
85
|
+
await setConfigValue('integrations.jira', {
|
|
86
|
+
...currentJira,
|
|
87
|
+
...jiraConfig
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// Jira API Client
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Make authenticated Jira API request using shared HttpClient
|
|
97
|
+
*/
|
|
98
|
+
async function jiraRequest(method, endpoint, body = null) {
|
|
99
|
+
const config = getJiraConfig();
|
|
100
|
+
|
|
101
|
+
if (!config.baseUrl || !config.email || !config.apiToken) {
|
|
102
|
+
throw new Error('Jira not configured. Run: flow jira config');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const auth = Buffer.from(`${config.email}:${config.apiToken}`).toString('base64');
|
|
106
|
+
const client = new HttpClient(config.baseUrl, {
|
|
107
|
+
headers: {
|
|
108
|
+
'Authorization': `Basic ${auth}`,
|
|
109
|
+
'Accept': 'application/json'
|
|
110
|
+
},
|
|
111
|
+
timeout: 30000
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const response = await client.request(method, endpoint, body);
|
|
115
|
+
|
|
116
|
+
if (response.status >= 400) {
|
|
117
|
+
throw new Error(`Jira API error ${response.status}: ${JSON.stringify(response.data)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return response.data;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// Issue Operations
|
|
125
|
+
// ============================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch issues from Jira using JQL
|
|
129
|
+
*/
|
|
130
|
+
async function fetchIssues(jql = null) {
|
|
131
|
+
const config = getJiraConfig();
|
|
132
|
+
const query = jql || config.jqlFilter;
|
|
133
|
+
|
|
134
|
+
const searchUrl = `/rest/api/3/search?jql=${encodeURIComponent(query)}&fields=summary,status,priority,assignee,created,updated,description`;
|
|
135
|
+
|
|
136
|
+
const result = await jiraRequest('GET', searchUrl);
|
|
137
|
+
|
|
138
|
+
return result.issues.map(issue => ({
|
|
139
|
+
key: issue.key,
|
|
140
|
+
id: issue.id,
|
|
141
|
+
summary: issue.fields.summary,
|
|
142
|
+
description: issue.fields.description?.content?.[0]?.content?.[0]?.text || '',
|
|
143
|
+
status: issue.fields.status?.name,
|
|
144
|
+
priority: issue.fields.priority?.name,
|
|
145
|
+
assignee: issue.fields.assignee?.displayName,
|
|
146
|
+
created: issue.fields.created,
|
|
147
|
+
updated: issue.fields.updated,
|
|
148
|
+
url: `${config.baseUrl}/browse/${issue.key}`
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get cached issues or fetch fresh
|
|
154
|
+
*/
|
|
155
|
+
async function getIssues(forceRefresh = false) {
|
|
156
|
+
if (!forceRefresh && fileExists(JIRA_CACHE_PATH)) {
|
|
157
|
+
const cache = safeJsonParse(JIRA_CACHE_PATH);
|
|
158
|
+
// Validate cache has issues array and valid timestamp
|
|
159
|
+
if (cache?.issues && cache?.fetchedAt) {
|
|
160
|
+
const fetchTime = new Date(cache.fetchedAt).getTime();
|
|
161
|
+
// Check for valid date and within TTL
|
|
162
|
+
if (!isNaN(fetchTime) && Date.now() - fetchTime < CACHE_TTL_MS) {
|
|
163
|
+
return cache.issues;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const issues = await fetchIssues();
|
|
169
|
+
|
|
170
|
+
// Save to cache using writeJson for atomic writes
|
|
171
|
+
writeJson(JIRA_CACHE_PATH, {
|
|
172
|
+
fetchedAt: new Date().toISOString(),
|
|
173
|
+
issues
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return issues;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Update issue status in Jira
|
|
181
|
+
*/
|
|
182
|
+
async function updateIssueStatus(issueKey, statusName) {
|
|
183
|
+
// Get available transitions
|
|
184
|
+
const transitions = await jiraRequest('GET', `/rest/api/3/issue/${issueKey}/transitions`);
|
|
185
|
+
|
|
186
|
+
const transition = transitions.transitions.find(t =>
|
|
187
|
+
t.name.toLowerCase() === statusName.toLowerCase() ||
|
|
188
|
+
t.to.name.toLowerCase() === statusName.toLowerCase()
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!transition) {
|
|
192
|
+
throw new Error(`Transition to "${statusName}" not available for ${issueKey}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await jiraRequest('POST', `/rest/api/3/issue/${issueKey}/transitions`, {
|
|
196
|
+
transition: { id: transition.id }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { success: true, issueKey, newStatus: statusName };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Add comment to issue
|
|
204
|
+
*/
|
|
205
|
+
async function addComment(issueKey, comment) {
|
|
206
|
+
await jiraRequest('POST', `/rest/api/3/issue/${issueKey}/comment`, {
|
|
207
|
+
body: {
|
|
208
|
+
type: 'doc',
|
|
209
|
+
version: 1,
|
|
210
|
+
content: [{
|
|
211
|
+
type: 'paragraph',
|
|
212
|
+
content: [{ type: 'text', text: comment }]
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================
|
|
219
|
+
// Sync Operations
|
|
220
|
+
// ============================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Import Jira issues to ready.json
|
|
224
|
+
*/
|
|
225
|
+
async function syncToReady() {
|
|
226
|
+
const config = getJiraConfig();
|
|
227
|
+
const issues = await getIssues(true);
|
|
228
|
+
|
|
229
|
+
// Load current ready.json
|
|
230
|
+
const ready = safeJsonParse(READY_PATH) || {
|
|
231
|
+
ready: [],
|
|
232
|
+
inProgress: [],
|
|
233
|
+
blocked: [],
|
|
234
|
+
recentlyCompleted: []
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Map existing tasks by external ID
|
|
238
|
+
const existingByExternal = new Map();
|
|
239
|
+
for (const task of [...ready.ready, ...ready.inProgress, ...ready.blocked]) {
|
|
240
|
+
if (task.externalId) {
|
|
241
|
+
existingByExternal.set(task.externalId, task);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const imported = [];
|
|
246
|
+
const updated = [];
|
|
247
|
+
|
|
248
|
+
for (const issue of issues) {
|
|
249
|
+
const externalId = `jira:${issue.key}`;
|
|
250
|
+
const existing = existingByExternal.get(externalId);
|
|
251
|
+
|
|
252
|
+
// Determine status category
|
|
253
|
+
let targetList = 'ready';
|
|
254
|
+
if (config.syncStatuses.inProgress.includes(issue.status)) {
|
|
255
|
+
targetList = 'inProgress';
|
|
256
|
+
} else if (config.syncStatuses.completed.includes(issue.status)) {
|
|
257
|
+
continue; // Skip completed
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (existing) {
|
|
261
|
+
// Update existing task
|
|
262
|
+
existing.title = issue.summary;
|
|
263
|
+
existing.priority = mapPriority(issue.priority);
|
|
264
|
+
existing.updatedAt = new Date().toISOString();
|
|
265
|
+
updated.push(issue.key);
|
|
266
|
+
} else {
|
|
267
|
+
// Create new task using standard task ID generator
|
|
268
|
+
const taskId = generateTaskId();
|
|
269
|
+
const task = {
|
|
270
|
+
id: taskId,
|
|
271
|
+
externalId,
|
|
272
|
+
externalUrl: issue.url,
|
|
273
|
+
title: issue.summary,
|
|
274
|
+
type: 'story',
|
|
275
|
+
feature: 'general',
|
|
276
|
+
status: targetList === 'ready' ? 'ready' : 'in_progress',
|
|
277
|
+
priority: mapPriority(issue.priority),
|
|
278
|
+
source: 'jira',
|
|
279
|
+
importedAt: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
ready[targetList].push(task);
|
|
283
|
+
imported.push(issue.key);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Save ready.json using atomic write
|
|
288
|
+
ready.lastUpdated = new Date().toISOString();
|
|
289
|
+
writeJson(READY_PATH, ready);
|
|
290
|
+
|
|
291
|
+
return { imported, updated };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Push completed tasks back to Jira
|
|
296
|
+
*/
|
|
297
|
+
async function pushCompleted() {
|
|
298
|
+
const config = getJiraConfig();
|
|
299
|
+
const ready = safeJsonParse(READY_PATH);
|
|
300
|
+
|
|
301
|
+
if (!ready || !ready.recentlyCompleted) {
|
|
302
|
+
return { pushed: [] };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Validate completed statuses array exists
|
|
306
|
+
const completedStatuses = config.syncStatuses?.completed || [];
|
|
307
|
+
if (completedStatuses.length === 0) {
|
|
308
|
+
warn('No completed statuses configured, using default "Done"');
|
|
309
|
+
}
|
|
310
|
+
const targetStatus = completedStatuses[0] || 'Done';
|
|
311
|
+
|
|
312
|
+
const pushed = [];
|
|
313
|
+
|
|
314
|
+
for (const task of ready.recentlyCompleted) {
|
|
315
|
+
// Validate external ID format
|
|
316
|
+
if (!task.externalId || !task.externalId.startsWith('jira:')) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const issueKey = task.externalId.replace('jira:', '');
|
|
321
|
+
// Validate issue key format (PROJECT-123)
|
|
322
|
+
if (!/^[A-Z][A-Z0-9]+-\d+$/i.test(issueKey)) {
|
|
323
|
+
warn(`Invalid Jira issue key format: ${issueKey}`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await updateIssueStatus(issueKey, targetStatus);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
warn(`Failed to update status for ${issueKey}: ${err.message}`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await addComment(issueKey, `Completed via Wogi Flow at ${task.completedAt}`);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
warn(`Failed to add comment to ${issueKey}: ${err.message}`);
|
|
338
|
+
// Status was updated, so still count as pushed
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
pushed.push(issueKey);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { pushed };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Map Jira priority to Wogi Flow priority
|
|
349
|
+
*/
|
|
350
|
+
function mapPriority(jiraPriority) {
|
|
351
|
+
const mapping = {
|
|
352
|
+
'Highest': 'P0',
|
|
353
|
+
'High': 'P1',
|
|
354
|
+
'Medium': 'P2',
|
|
355
|
+
'Low': 'P3',
|
|
356
|
+
'Lowest': 'P4'
|
|
357
|
+
};
|
|
358
|
+
return mapping[jiraPriority] || 'P2';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================
|
|
362
|
+
// CLI Output
|
|
363
|
+
// ============================================================
|
|
364
|
+
|
|
365
|
+
function printIssues(issues) {
|
|
366
|
+
if (issues.length === 0) {
|
|
367
|
+
info('No issues found');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
printHeader('JIRA ISSUES');
|
|
372
|
+
|
|
373
|
+
for (const issue of issues) {
|
|
374
|
+
const statusColor = getStatusColor(issue.status);
|
|
375
|
+
const priorityIcon = getPriorityIcon(issue.priority);
|
|
376
|
+
|
|
377
|
+
console.log(`\n ${color('cyan', issue.key)} ${priorityIcon}`);
|
|
378
|
+
console.log(` ${issue.summary}`);
|
|
379
|
+
console.log(` ${color(statusColor, issue.status)} • ${color('dim', issue.assignee || 'Unassigned')}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`\n ${color('dim', `Total: ${issues.length} issues`)}\n`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getStatusColor(status) {
|
|
386
|
+
const lower = status?.toLowerCase() || '';
|
|
387
|
+
if (lower.includes('done') || lower.includes('closed')) return 'green';
|
|
388
|
+
if (lower.includes('progress')) return 'yellow';
|
|
389
|
+
if (lower.includes('blocked')) return 'red';
|
|
390
|
+
return 'blue';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getPriorityIcon(priority) {
|
|
394
|
+
const icons = {
|
|
395
|
+
'Highest': color('red', '!!!'),
|
|
396
|
+
'High': color('red', '!!'),
|
|
397
|
+
'Medium': color('yellow', '!'),
|
|
398
|
+
'Low': color('dim', '-'),
|
|
399
|
+
'Lowest': color('dim', '--')
|
|
400
|
+
};
|
|
401
|
+
return icons[priority] || '';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function printConfig(config) {
|
|
405
|
+
printHeader('JIRA CONFIGURATION');
|
|
406
|
+
|
|
407
|
+
console.log(` ${color('dim', 'Enabled:')} ${config.enabled ? color('green', 'Yes') : color('red', 'No')}`);
|
|
408
|
+
console.log(` ${color('dim', 'Base URL:')} ${config.baseUrl || color('yellow', 'Not set')}`);
|
|
409
|
+
console.log(` ${color('dim', 'Project:')} ${config.projectKey || color('yellow', 'Not set')}`);
|
|
410
|
+
console.log(` ${color('dim', 'Email:')} ${config.email || color('yellow', 'Not set')}`);
|
|
411
|
+
console.log(` ${color('dim', 'API Token:')} ${config.apiToken ? color('green', 'Set') : color('yellow', 'Not set')}`);
|
|
412
|
+
|
|
413
|
+
console.log(`\n ${color('dim', 'JQL Filter:')}`);
|
|
414
|
+
console.log(` ${config.jqlFilter}`);
|
|
415
|
+
|
|
416
|
+
console.log(`\nTo configure, add to .workflow/config.json:`);
|
|
417
|
+
console.log(` "integrations": {`);
|
|
418
|
+
console.log(` "jira": {`);
|
|
419
|
+
console.log(` "enabled": true,`);
|
|
420
|
+
console.log(` "baseUrl": "https://your-company.atlassian.net",`);
|
|
421
|
+
console.log(` "projectKey": "PROJ",`);
|
|
422
|
+
console.log(` "email": "you@company.com",`);
|
|
423
|
+
console.log(` "apiToken": "{env:JIRA_API_TOKEN}"`);
|
|
424
|
+
console.log(` }`);
|
|
425
|
+
console.log(` }`);
|
|
426
|
+
console.log('');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ============================================================
|
|
430
|
+
// CLI Entry Point
|
|
431
|
+
// ============================================================
|
|
432
|
+
|
|
433
|
+
function showHelp() {
|
|
434
|
+
console.log(`
|
|
435
|
+
Wogi Flow - Jira Integration
|
|
436
|
+
|
|
437
|
+
Sync tasks between Wogi Flow and Jira.
|
|
438
|
+
|
|
439
|
+
Usage:
|
|
440
|
+
flow jira list List assigned issues
|
|
441
|
+
flow jira list --refresh Force refresh from Jira
|
|
442
|
+
flow jira sync Import issues to ready.json
|
|
443
|
+
flow jira push Push completed tasks to Jira
|
|
444
|
+
flow jira config Show Jira configuration
|
|
445
|
+
|
|
446
|
+
Options:
|
|
447
|
+
--refresh Force refresh from API (bypass cache)
|
|
448
|
+
--json Output as JSON
|
|
449
|
+
--help, -h Show this help
|
|
450
|
+
|
|
451
|
+
Configuration:
|
|
452
|
+
Add to .workflow/config.json:
|
|
453
|
+
{
|
|
454
|
+
"integrations": {
|
|
455
|
+
"jira": {
|
|
456
|
+
"enabled": true,
|
|
457
|
+
"baseUrl": "https://company.atlassian.net",
|
|
458
|
+
"projectKey": "PROJ",
|
|
459
|
+
"email": "user@company.com",
|
|
460
|
+
"apiToken": "{env:JIRA_API_TOKEN}"
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function main() {
|
|
468
|
+
const args = process.argv.slice(2);
|
|
469
|
+
const { flags, positional } = parseFlags(args);
|
|
470
|
+
const command = positional[0] || 'list';
|
|
471
|
+
|
|
472
|
+
if (flags.help || flags.h) {
|
|
473
|
+
showHelp();
|
|
474
|
+
process.exit(0);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const config = getJiraConfig();
|
|
478
|
+
|
|
479
|
+
switch (command) {
|
|
480
|
+
case 'list': {
|
|
481
|
+
if (!config.enabled) {
|
|
482
|
+
warn('Jira integration is not enabled');
|
|
483
|
+
printConfig(config);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
info('Fetching Jira issues...');
|
|
488
|
+
const issues = await getIssues(flags.refresh);
|
|
489
|
+
|
|
490
|
+
if (flags.json) {
|
|
491
|
+
console.log(JSON.stringify(issues, null, 2));
|
|
492
|
+
} else {
|
|
493
|
+
printIssues(issues);
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case 'sync': {
|
|
499
|
+
if (!config.enabled) {
|
|
500
|
+
error('Jira integration is not enabled');
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
info('Syncing Jira issues to ready.json...');
|
|
505
|
+
const result = await syncToReady();
|
|
506
|
+
|
|
507
|
+
if (flags.json) {
|
|
508
|
+
console.log(JSON.stringify(result, null, 2));
|
|
509
|
+
} else {
|
|
510
|
+
if (result.imported.length > 0) {
|
|
511
|
+
success(`Imported ${result.imported.length} issues: ${result.imported.join(', ')}`);
|
|
512
|
+
}
|
|
513
|
+
if (result.updated.length > 0) {
|
|
514
|
+
info(`Updated ${result.updated.length} issues: ${result.updated.join(', ')}`);
|
|
515
|
+
}
|
|
516
|
+
if (result.imported.length === 0 && result.updated.length === 0) {
|
|
517
|
+
info('No changes needed');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
case 'push': {
|
|
524
|
+
if (!config.enabled) {
|
|
525
|
+
error('Jira integration is not enabled');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
info('Pushing completed tasks to Jira...');
|
|
530
|
+
const result = await pushCompleted();
|
|
531
|
+
|
|
532
|
+
if (flags.json) {
|
|
533
|
+
console.log(JSON.stringify(result, null, 2));
|
|
534
|
+
} else {
|
|
535
|
+
if (result.pushed.length > 0) {
|
|
536
|
+
success(`Pushed ${result.pushed.length} tasks: ${result.pushed.join(', ')}`);
|
|
537
|
+
} else {
|
|
538
|
+
info('No completed Jira tasks to push');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
case 'config': {
|
|
545
|
+
if (flags.json) {
|
|
546
|
+
console.log(JSON.stringify(config, null, 2));
|
|
547
|
+
} else {
|
|
548
|
+
printConfig(config);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
default:
|
|
554
|
+
error(`Unknown command: ${command}`);
|
|
555
|
+
showHelp();
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ============================================================
|
|
561
|
+
// Exports
|
|
562
|
+
// ============================================================
|
|
563
|
+
|
|
564
|
+
module.exports = {
|
|
565
|
+
getJiraConfig,
|
|
566
|
+
fetchIssues,
|
|
567
|
+
getIssues,
|
|
568
|
+
syncToReady,
|
|
569
|
+
pushCompleted,
|
|
570
|
+
updateIssueStatus,
|
|
571
|
+
addComment
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
if (require.main === module) {
|
|
575
|
+
main().catch(err => {
|
|
576
|
+
error(err.message);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
});
|
|
579
|
+
}
|