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,631 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Linear Integration
|
|
5
|
+
*
|
|
6
|
+
* Sync tasks between Wogi Flow and Linear. Supports:
|
|
7
|
+
* - Listing assigned Linear issues
|
|
8
|
+
* - Importing issues to ready.json
|
|
9
|
+
* - Syncing completed tasks back to Linear
|
|
10
|
+
*
|
|
11
|
+
* Part of Phase 6: Team & Integrations
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* flow linear list List assigned issues
|
|
15
|
+
* flow linear sync Import issues to ready.json
|
|
16
|
+
* flow linear push Push completed tasks to Linear
|
|
17
|
+
* flow linear config Show/set Linear configuration
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { HttpClient } = require('./flow-http-client');
|
|
23
|
+
const { TIMEOUTS } = require('./flow-constants');
|
|
24
|
+
const {
|
|
25
|
+
PROJECT_ROOT,
|
|
26
|
+
STATE_DIR,
|
|
27
|
+
parseFlags,
|
|
28
|
+
color,
|
|
29
|
+
info,
|
|
30
|
+
warn,
|
|
31
|
+
error,
|
|
32
|
+
success,
|
|
33
|
+
fileExists,
|
|
34
|
+
safeJsonParse,
|
|
35
|
+
getConfig,
|
|
36
|
+
setConfigValue,
|
|
37
|
+
resolveConfigValue,
|
|
38
|
+
printHeader,
|
|
39
|
+
generateTaskId,
|
|
40
|
+
writeJson
|
|
41
|
+
} = require('./flow-utils');
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// Constants
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
const READY_PATH = path.join(STATE_DIR, 'ready.json');
|
|
48
|
+
const LINEAR_CACHE_PATH = path.join(STATE_DIR, 'linear-cache.json');
|
|
49
|
+
const CACHE_TTL_MS = TIMEOUTS.CACHE_TTL;
|
|
50
|
+
const LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// Configuration
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get Linear configuration from config.json
|
|
58
|
+
*/
|
|
59
|
+
function getLinearConfig() {
|
|
60
|
+
const config = getConfig();
|
|
61
|
+
const linearConfig = config?.integrations?.linear || {};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
enabled: linearConfig.enabled || false,
|
|
65
|
+
apiKey: resolveConfigValue(linearConfig.apiKey),
|
|
66
|
+
teamId: linearConfig.teamId || null,
|
|
67
|
+
teamKey: linearConfig.teamKey || null,
|
|
68
|
+
syncStatuses: linearConfig.syncStatuses || {
|
|
69
|
+
ready: ['Backlog', 'Todo', 'Triage'],
|
|
70
|
+
inProgress: ['In Progress', 'In Review'],
|
|
71
|
+
completed: ['Done', 'Canceled']
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================
|
|
77
|
+
// Linear GraphQL Client
|
|
78
|
+
// ============================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Make authenticated Linear GraphQL request using shared HttpClient
|
|
82
|
+
*/
|
|
83
|
+
async function linearRequest(query, variables = {}) {
|
|
84
|
+
const config = getLinearConfig();
|
|
85
|
+
|
|
86
|
+
if (!config.apiKey) {
|
|
87
|
+
throw new Error('Linear API key not configured. Run: flow linear config');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const client = new HttpClient(LINEAR_API_URL, {
|
|
91
|
+
headers: {
|
|
92
|
+
'Authorization': config.apiKey
|
|
93
|
+
},
|
|
94
|
+
timeout: 30000
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const response = await client.post('/', { query, variables });
|
|
98
|
+
|
|
99
|
+
if (response.data?.errors) {
|
|
100
|
+
throw new Error(response.data.errors[0].message);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response.data?.data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// Issue Operations
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetch assigned issues from Linear
|
|
112
|
+
*/
|
|
113
|
+
async function fetchIssues() {
|
|
114
|
+
const config = getLinearConfig();
|
|
115
|
+
|
|
116
|
+
const query = `
|
|
117
|
+
query AssignedIssues($teamId: String) {
|
|
118
|
+
viewer {
|
|
119
|
+
assignedIssues(
|
|
120
|
+
filter: {
|
|
121
|
+
state: { type: { nin: ["completed", "canceled"] } }
|
|
122
|
+
${config.teamId ? 'team: { id: { eq: $teamId } }' : ''}
|
|
123
|
+
}
|
|
124
|
+
orderBy: priority
|
|
125
|
+
first: 50
|
|
126
|
+
) {
|
|
127
|
+
nodes {
|
|
128
|
+
id
|
|
129
|
+
identifier
|
|
130
|
+
title
|
|
131
|
+
description
|
|
132
|
+
priority
|
|
133
|
+
priorityLabel
|
|
134
|
+
state {
|
|
135
|
+
name
|
|
136
|
+
type
|
|
137
|
+
}
|
|
138
|
+
team {
|
|
139
|
+
key
|
|
140
|
+
name
|
|
141
|
+
}
|
|
142
|
+
createdAt
|
|
143
|
+
updatedAt
|
|
144
|
+
url
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const variables = config.teamId ? { teamId: config.teamId } : {};
|
|
152
|
+
const result = await linearRequest(query, variables);
|
|
153
|
+
|
|
154
|
+
return result.viewer.assignedIssues.nodes.map(issue => ({
|
|
155
|
+
id: issue.id,
|
|
156
|
+
identifier: issue.identifier,
|
|
157
|
+
title: issue.title,
|
|
158
|
+
description: issue.description || '',
|
|
159
|
+
priority: issue.priority,
|
|
160
|
+
priorityLabel: issue.priorityLabel,
|
|
161
|
+
status: issue.state.name,
|
|
162
|
+
statusType: issue.state.type,
|
|
163
|
+
team: issue.team.key,
|
|
164
|
+
teamName: issue.team.name,
|
|
165
|
+
createdAt: issue.createdAt,
|
|
166
|
+
updatedAt: issue.updatedAt,
|
|
167
|
+
url: issue.url
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get cached issues or fetch fresh
|
|
173
|
+
*/
|
|
174
|
+
async function getIssues(forceRefresh = false) {
|
|
175
|
+
if (!forceRefresh && fileExists(LINEAR_CACHE_PATH)) {
|
|
176
|
+
const cache = safeJsonParse(LINEAR_CACHE_PATH);
|
|
177
|
+
// Validate cache has issues array and valid timestamp
|
|
178
|
+
if (cache?.issues && cache?.fetchedAt) {
|
|
179
|
+
const fetchTime = new Date(cache.fetchedAt).getTime();
|
|
180
|
+
// Check for valid date and within TTL
|
|
181
|
+
if (!isNaN(fetchTime) && Date.now() - fetchTime < CACHE_TTL_MS) {
|
|
182
|
+
return cache.issues;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const issues = await fetchIssues();
|
|
188
|
+
|
|
189
|
+
// Save to cache using writeJson for atomic writes
|
|
190
|
+
writeJson(LINEAR_CACHE_PATH, {
|
|
191
|
+
fetchedAt: new Date().toISOString(),
|
|
192
|
+
issues
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Update issue state in Linear
|
|
200
|
+
*/
|
|
201
|
+
async function updateIssueState(issueId, stateName) {
|
|
202
|
+
// First, get available states for the issue's team
|
|
203
|
+
const stateQuery = `
|
|
204
|
+
query GetIssueStates($issueId: String!) {
|
|
205
|
+
issue(id: $issueId) {
|
|
206
|
+
team {
|
|
207
|
+
states {
|
|
208
|
+
nodes {
|
|
209
|
+
id
|
|
210
|
+
name
|
|
211
|
+
type
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
const stateResult = await linearRequest(stateQuery, { issueId });
|
|
220
|
+
const states = stateResult.issue.team.states.nodes;
|
|
221
|
+
|
|
222
|
+
const targetState = states.find(s =>
|
|
223
|
+
s.name.toLowerCase() === stateName.toLowerCase() ||
|
|
224
|
+
s.type.toLowerCase() === stateName.toLowerCase()
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!targetState) {
|
|
228
|
+
throw new Error(`State "${stateName}" not found`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Update the issue
|
|
232
|
+
const updateQuery = `
|
|
233
|
+
mutation UpdateIssue($issueId: String!, $stateId: String!) {
|
|
234
|
+
issueUpdate(id: $issueId, input: { stateId: $stateId }) {
|
|
235
|
+
success
|
|
236
|
+
issue {
|
|
237
|
+
identifier
|
|
238
|
+
state { name }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
const updateResult = await linearRequest(updateQuery, {
|
|
245
|
+
issueId,
|
|
246
|
+
stateId: targetState.id
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: updateResult.issueUpdate.success,
|
|
251
|
+
identifier: updateResult.issueUpdate.issue.identifier,
|
|
252
|
+
newState: updateResult.issueUpdate.issue.state.name
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add comment to issue
|
|
258
|
+
*/
|
|
259
|
+
async function addComment(issueId, body) {
|
|
260
|
+
const query = `
|
|
261
|
+
mutation AddComment($issueId: String!, $body: String!) {
|
|
262
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
263
|
+
success
|
|
264
|
+
comment {
|
|
265
|
+
id
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
return linearRequest(query, { issueId, body });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================
|
|
275
|
+
// Sync Operations
|
|
276
|
+
// ============================================================
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Import Linear issues to ready.json
|
|
280
|
+
*/
|
|
281
|
+
async function syncToReady() {
|
|
282
|
+
const config = getLinearConfig();
|
|
283
|
+
const issues = await getIssues(true);
|
|
284
|
+
|
|
285
|
+
// Load current ready.json
|
|
286
|
+
const ready = safeJsonParse(READY_PATH) || {
|
|
287
|
+
ready: [],
|
|
288
|
+
inProgress: [],
|
|
289
|
+
blocked: [],
|
|
290
|
+
recentlyCompleted: []
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Map existing tasks by external ID
|
|
294
|
+
const existingByExternal = new Map();
|
|
295
|
+
for (const task of [...ready.ready, ...ready.inProgress, ...ready.blocked]) {
|
|
296
|
+
if (task.externalId) {
|
|
297
|
+
existingByExternal.set(task.externalId, task);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const imported = [];
|
|
302
|
+
const updated = [];
|
|
303
|
+
|
|
304
|
+
for (const issue of issues) {
|
|
305
|
+
const externalId = `linear:${issue.identifier}`;
|
|
306
|
+
const existing = existingByExternal.get(externalId);
|
|
307
|
+
|
|
308
|
+
// Determine status category
|
|
309
|
+
let targetList = 'ready';
|
|
310
|
+
if (config.syncStatuses.inProgress.includes(issue.status)) {
|
|
311
|
+
targetList = 'inProgress';
|
|
312
|
+
} else if (issue.statusType === 'completed' || issue.statusType === 'canceled') {
|
|
313
|
+
continue; // Skip completed
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (existing) {
|
|
317
|
+
// Update existing task
|
|
318
|
+
existing.title = issue.title;
|
|
319
|
+
existing.priority = mapPriority(issue.priority);
|
|
320
|
+
existing.updatedAt = new Date().toISOString();
|
|
321
|
+
updated.push(issue.identifier);
|
|
322
|
+
} else {
|
|
323
|
+
// Create new task using standard task ID generator
|
|
324
|
+
const taskId = generateTaskId();
|
|
325
|
+
const task = {
|
|
326
|
+
id: taskId,
|
|
327
|
+
externalId,
|
|
328
|
+
externalUrl: issue.url,
|
|
329
|
+
title: issue.title,
|
|
330
|
+
type: 'story',
|
|
331
|
+
feature: 'general',
|
|
332
|
+
status: targetList === 'ready' ? 'ready' : 'in_progress',
|
|
333
|
+
priority: mapPriority(issue.priority),
|
|
334
|
+
source: 'linear',
|
|
335
|
+
importedAt: new Date().toISOString()
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
ready[targetList].push(task);
|
|
339
|
+
imported.push(issue.identifier);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Save ready.json using atomic write
|
|
344
|
+
ready.lastUpdated = new Date().toISOString();
|
|
345
|
+
writeJson(READY_PATH, ready);
|
|
346
|
+
|
|
347
|
+
return { imported, updated };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Push completed tasks back to Linear
|
|
352
|
+
*/
|
|
353
|
+
async function pushCompleted() {
|
|
354
|
+
const ready = safeJsonParse(READY_PATH);
|
|
355
|
+
|
|
356
|
+
if (!ready || !ready.recentlyCompleted) {
|
|
357
|
+
return { pushed: [] };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const pushed = [];
|
|
361
|
+
|
|
362
|
+
for (const task of ready.recentlyCompleted) {
|
|
363
|
+
if (!task.externalId || !task.externalId.startsWith('linear:')) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Need to get the issue ID from the identifier
|
|
368
|
+
const identifier = task.externalId.replace('linear:', '');
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
// Use proper GraphQL variables to prevent injection
|
|
372
|
+
const searchQuery = `
|
|
373
|
+
query SearchIssue($identifier: String!) {
|
|
374
|
+
issues(filter: { identifier: { eq: $identifier } }) {
|
|
375
|
+
nodes { id }
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
|
|
380
|
+
const searchResult = await linearRequest(searchQuery, { identifier });
|
|
381
|
+
const issueId = searchResult.issues?.nodes?.[0]?.id;
|
|
382
|
+
|
|
383
|
+
if (!issueId) {
|
|
384
|
+
warn(`Could not find issue ${identifier}`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await updateIssueState(issueId, 'Done');
|
|
389
|
+
await addComment(issueId, `Completed via Wogi Flow at ${task.completedAt}`);
|
|
390
|
+
pushed.push(identifier);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
warn(`Failed to update ${identifier}: ${err.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { pushed };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Map Linear priority (0-4) to Wogi Flow priority
|
|
401
|
+
*/
|
|
402
|
+
function mapPriority(linearPriority) {
|
|
403
|
+
// Linear: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
404
|
+
const mapping = {
|
|
405
|
+
0: 'P2',
|
|
406
|
+
1: 'P0',
|
|
407
|
+
2: 'P1',
|
|
408
|
+
3: 'P2',
|
|
409
|
+
4: 'P3'
|
|
410
|
+
};
|
|
411
|
+
return mapping[linearPriority] || 'P2';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ============================================================
|
|
415
|
+
// CLI Output
|
|
416
|
+
// ============================================================
|
|
417
|
+
|
|
418
|
+
function printIssues(issues) {
|
|
419
|
+
if (issues.length === 0) {
|
|
420
|
+
info('No issues found');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
printHeader('LINEAR ISSUES');
|
|
425
|
+
|
|
426
|
+
for (const issue of issues) {
|
|
427
|
+
const statusColor = getStatusColor(issue.statusType);
|
|
428
|
+
const priorityIcon = getPriorityIcon(issue.priority);
|
|
429
|
+
|
|
430
|
+
console.log(`\n ${color('purple', issue.identifier)} ${priorityIcon}`);
|
|
431
|
+
console.log(` ${issue.title}`);
|
|
432
|
+
console.log(` ${color(statusColor, issue.status)} • ${color('dim', issue.teamName)}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(`\n ${color('dim', `Total: ${issues.length} issues`)}\n`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getStatusColor(statusType) {
|
|
439
|
+
const colors = {
|
|
440
|
+
'backlog': 'dim',
|
|
441
|
+
'unstarted': 'blue',
|
|
442
|
+
'started': 'yellow',
|
|
443
|
+
'completed': 'green',
|
|
444
|
+
'canceled': 'dim'
|
|
445
|
+
};
|
|
446
|
+
return colors[statusType] || 'blue';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function getPriorityIcon(priority) {
|
|
450
|
+
const icons = {
|
|
451
|
+
0: '',
|
|
452
|
+
1: color('red', '!!!'),
|
|
453
|
+
2: color('red', '!!'),
|
|
454
|
+
3: color('yellow', '!'),
|
|
455
|
+
4: color('dim', '-')
|
|
456
|
+
};
|
|
457
|
+
return icons[priority] || '';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function printConfig(config) {
|
|
461
|
+
printHeader('LINEAR CONFIGURATION');
|
|
462
|
+
|
|
463
|
+
console.log(` ${color('dim', 'Enabled:')} ${config.enabled ? color('green', 'Yes') : color('red', 'No')}`);
|
|
464
|
+
console.log(` ${color('dim', 'API Key:')} ${config.apiKey ? color('green', 'Set') : color('yellow', 'Not set')}`);
|
|
465
|
+
console.log(` ${color('dim', 'Team ID:')} ${config.teamId || color('dim', 'All teams')}`);
|
|
466
|
+
console.log(` ${color('dim', 'Team Key:')} ${config.teamKey || color('dim', 'Not set')}`);
|
|
467
|
+
|
|
468
|
+
console.log(`\nTo configure, add to .workflow/config.json:`);
|
|
469
|
+
console.log(` "integrations": {`);
|
|
470
|
+
console.log(` "linear": {`);
|
|
471
|
+
console.log(` "enabled": true,`);
|
|
472
|
+
console.log(` "apiKey": "{env:LINEAR_API_KEY}",`);
|
|
473
|
+
console.log(` "teamId": "your-team-id"`);
|
|
474
|
+
console.log(` }`);
|
|
475
|
+
console.log(` }`);
|
|
476
|
+
console.log('');
|
|
477
|
+
console.log('Get your API key at: https://linear.app/settings/api');
|
|
478
|
+
console.log('');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ============================================================
|
|
482
|
+
// CLI Entry Point
|
|
483
|
+
// ============================================================
|
|
484
|
+
|
|
485
|
+
function showHelp() {
|
|
486
|
+
console.log(`
|
|
487
|
+
Wogi Flow - Linear Integration
|
|
488
|
+
|
|
489
|
+
Sync tasks between Wogi Flow and Linear.
|
|
490
|
+
|
|
491
|
+
Usage:
|
|
492
|
+
flow linear list List assigned issues
|
|
493
|
+
flow linear list --refresh Force refresh from Linear
|
|
494
|
+
flow linear sync Import issues to ready.json
|
|
495
|
+
flow linear push Push completed tasks to Linear
|
|
496
|
+
flow linear config Show Linear configuration
|
|
497
|
+
|
|
498
|
+
Options:
|
|
499
|
+
--refresh Force refresh from API (bypass cache)
|
|
500
|
+
--json Output as JSON
|
|
501
|
+
--help, -h Show this help
|
|
502
|
+
|
|
503
|
+
Configuration:
|
|
504
|
+
Add to .workflow/config.json:
|
|
505
|
+
{
|
|
506
|
+
"integrations": {
|
|
507
|
+
"linear": {
|
|
508
|
+
"enabled": true,
|
|
509
|
+
"apiKey": "{env:LINEAR_API_KEY}",
|
|
510
|
+
"teamId": "optional-team-id"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
Get your API key at: https://linear.app/settings/api
|
|
516
|
+
`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function main() {
|
|
520
|
+
const args = process.argv.slice(2);
|
|
521
|
+
const { flags, positional } = parseFlags(args);
|
|
522
|
+
const command = positional[0] || 'list';
|
|
523
|
+
|
|
524
|
+
if (flags.help || flags.h) {
|
|
525
|
+
showHelp();
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const config = getLinearConfig();
|
|
530
|
+
|
|
531
|
+
switch (command) {
|
|
532
|
+
case 'list': {
|
|
533
|
+
if (!config.enabled) {
|
|
534
|
+
warn('Linear integration is not enabled');
|
|
535
|
+
printConfig(config);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
info('Fetching Linear issues...');
|
|
540
|
+
const issues = await getIssues(flags.refresh);
|
|
541
|
+
|
|
542
|
+
if (flags.json) {
|
|
543
|
+
console.log(JSON.stringify(issues, null, 2));
|
|
544
|
+
} else {
|
|
545
|
+
printIssues(issues);
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case 'sync': {
|
|
551
|
+
if (!config.enabled) {
|
|
552
|
+
error('Linear integration is not enabled');
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
info('Syncing Linear issues to ready.json...');
|
|
557
|
+
const result = await syncToReady();
|
|
558
|
+
|
|
559
|
+
if (flags.json) {
|
|
560
|
+
console.log(JSON.stringify(result, null, 2));
|
|
561
|
+
} else {
|
|
562
|
+
if (result.imported.length > 0) {
|
|
563
|
+
success(`Imported ${result.imported.length} issues: ${result.imported.join(', ')}`);
|
|
564
|
+
}
|
|
565
|
+
if (result.updated.length > 0) {
|
|
566
|
+
info(`Updated ${result.updated.length} issues: ${result.updated.join(', ')}`);
|
|
567
|
+
}
|
|
568
|
+
if (result.imported.length === 0 && result.updated.length === 0) {
|
|
569
|
+
info('No changes needed');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
case 'push': {
|
|
576
|
+
if (!config.enabled) {
|
|
577
|
+
error('Linear integration is not enabled');
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
info('Pushing completed tasks to Linear...');
|
|
582
|
+
const result = await pushCompleted();
|
|
583
|
+
|
|
584
|
+
if (flags.json) {
|
|
585
|
+
console.log(JSON.stringify(result, null, 2));
|
|
586
|
+
} else {
|
|
587
|
+
if (result.pushed.length > 0) {
|
|
588
|
+
success(`Pushed ${result.pushed.length} tasks: ${result.pushed.join(', ')}`);
|
|
589
|
+
} else {
|
|
590
|
+
info('No completed Linear tasks to push');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case 'config': {
|
|
597
|
+
if (flags.json) {
|
|
598
|
+
console.log(JSON.stringify(config, null, 2));
|
|
599
|
+
} else {
|
|
600
|
+
printConfig(config);
|
|
601
|
+
}
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
default:
|
|
606
|
+
error(`Unknown command: ${command}`);
|
|
607
|
+
showHelp();
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================================
|
|
613
|
+
// Exports
|
|
614
|
+
// ============================================================
|
|
615
|
+
|
|
616
|
+
module.exports = {
|
|
617
|
+
getLinearConfig,
|
|
618
|
+
fetchIssues,
|
|
619
|
+
getIssues,
|
|
620
|
+
syncToReady,
|
|
621
|
+
pushCompleted,
|
|
622
|
+
updateIssueState,
|
|
623
|
+
addComment
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
if (require.main === module) {
|
|
627
|
+
main().catch(err => {
|
|
628
|
+
error(err.message);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
});
|
|
631
|
+
}
|