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,977 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Team Collaboration Module
|
|
5
|
+
*
|
|
6
|
+
* Manages team features including:
|
|
7
|
+
* - Team login/logout with invite codes
|
|
8
|
+
* - Setup selection from team configurations
|
|
9
|
+
* - Knowledge sync with AWS backend
|
|
10
|
+
* - Proposal management with offline queue
|
|
11
|
+
*
|
|
12
|
+
* Part of v1.8.0 Team Collaboration
|
|
13
|
+
*
|
|
14
|
+
* Note: Team features require a subscription and hosted backend.
|
|
15
|
+
* Without subscription, features gracefully degrade to local-only mode.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const {
|
|
22
|
+
getConfig,
|
|
23
|
+
saveConfig,
|
|
24
|
+
STATE_DIR,
|
|
25
|
+
colors,
|
|
26
|
+
color,
|
|
27
|
+
success,
|
|
28
|
+
warn,
|
|
29
|
+
error,
|
|
30
|
+
info,
|
|
31
|
+
printHeader,
|
|
32
|
+
fileExists,
|
|
33
|
+
readFile,
|
|
34
|
+
writeFile
|
|
35
|
+
} = require('./flow-utils');
|
|
36
|
+
|
|
37
|
+
// Use shared database for proposals
|
|
38
|
+
const memoryDb = require('./flow-memory-db');
|
|
39
|
+
|
|
40
|
+
// Decisions file path
|
|
41
|
+
const DECISIONS_PATH = path.join(STATE_DIR, 'decisions.md');
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// Constants
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
const TEAM_STATE_FILE = path.join(STATE_DIR, 'team-state.json');
|
|
48
|
+
const OFFLINE_QUEUE_FILE = path.join(STATE_DIR, 'offline-queue.json');
|
|
49
|
+
const DEFAULT_BACKEND_URL = 'https://api.wogi-flow.com';
|
|
50
|
+
|
|
51
|
+
// Token refresh threshold (refresh 5 minutes before expiry)
|
|
52
|
+
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Team State Management
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get current team state
|
|
60
|
+
*/
|
|
61
|
+
function getTeamState() {
|
|
62
|
+
if (!fileExists(TEAM_STATE_FILE)) {
|
|
63
|
+
return {
|
|
64
|
+
loggedIn: false,
|
|
65
|
+
teamId: null,
|
|
66
|
+
userId: null,
|
|
67
|
+
teamName: null,
|
|
68
|
+
setupId: null,
|
|
69
|
+
setupName: null,
|
|
70
|
+
lastSync: null,
|
|
71
|
+
accessToken: null,
|
|
72
|
+
refreshToken: null,
|
|
73
|
+
tokenExpiresAt: null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const state = JSON.parse(readFile(TEAM_STATE_FILE));
|
|
79
|
+
// Decrypt tokens if encrypted
|
|
80
|
+
if (state.accessToken && state.encrypted) {
|
|
81
|
+
state.accessToken = decryptToken(state.accessToken);
|
|
82
|
+
state.refreshToken = decryptToken(state.refreshToken);
|
|
83
|
+
}
|
|
84
|
+
return state;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return { loggedIn: false };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Save team state (with encrypted tokens)
|
|
92
|
+
*/
|
|
93
|
+
function saveTeamState(state) {
|
|
94
|
+
const stateToSave = { ...state };
|
|
95
|
+
|
|
96
|
+
// Encrypt tokens before saving
|
|
97
|
+
if (stateToSave.accessToken) {
|
|
98
|
+
stateToSave.accessToken = encryptToken(stateToSave.accessToken);
|
|
99
|
+
stateToSave.refreshToken = encryptToken(stateToSave.refreshToken);
|
|
100
|
+
stateToSave.encrypted = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
writeFile(TEAM_STATE_FILE, JSON.stringify(stateToSave, null, 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Simple token encryption (better than plain text)
|
|
108
|
+
*/
|
|
109
|
+
function encryptToken(token) {
|
|
110
|
+
if (!token) return null;
|
|
111
|
+
const key = crypto.createHash('sha256').update(getMachineId()).digest();
|
|
112
|
+
const iv = crypto.randomBytes(16);
|
|
113
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
114
|
+
let encrypted = cipher.update(token, 'utf8', 'hex');
|
|
115
|
+
encrypted += cipher.final('hex');
|
|
116
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function decryptToken(encrypted) {
|
|
120
|
+
if (!encrypted) return null;
|
|
121
|
+
try {
|
|
122
|
+
const key = crypto.createHash('sha256').update(getMachineId()).digest();
|
|
123
|
+
const parts = encrypted.split(':');
|
|
124
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
125
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
126
|
+
let decrypted = decipher.update(parts[1], 'hex', 'utf8');
|
|
127
|
+
decrypted += decipher.final('utf8');
|
|
128
|
+
return decrypted;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getMachineId() {
|
|
135
|
+
// Use a combination of machine-specific values
|
|
136
|
+
return process.env.USER + process.env.HOME + __dirname;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if team features are enabled and configured
|
|
141
|
+
*/
|
|
142
|
+
function isTeamEnabled() {
|
|
143
|
+
const config = getConfig();
|
|
144
|
+
return config.team?.enabled === true && config.team?.teamId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get backend URL from config or default
|
|
149
|
+
*/
|
|
150
|
+
function getBackendUrl() {
|
|
151
|
+
const config = getConfig();
|
|
152
|
+
return config.team?.backendUrl || DEFAULT_BACKEND_URL;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================
|
|
156
|
+
// Offline Queue
|
|
157
|
+
// ============================================================
|
|
158
|
+
|
|
159
|
+
function getOfflineQueue() {
|
|
160
|
+
if (!fileExists(OFFLINE_QUEUE_FILE)) return [];
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(readFile(OFFLINE_QUEUE_FILE));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function saveOfflineQueue(queue) {
|
|
169
|
+
writeFile(OFFLINE_QUEUE_FILE, JSON.stringify(queue, null, 2));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addToOfflineQueue(operation) {
|
|
173
|
+
const queue = getOfflineQueue();
|
|
174
|
+
queue.push({
|
|
175
|
+
...operation,
|
|
176
|
+
queuedAt: new Date().toISOString(),
|
|
177
|
+
retries: 0
|
|
178
|
+
});
|
|
179
|
+
saveOfflineQueue(queue);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function processOfflineQueue() {
|
|
183
|
+
const queue = getOfflineQueue();
|
|
184
|
+
if (queue.length === 0) return { processed: 0, failed: 0 };
|
|
185
|
+
|
|
186
|
+
let processed = 0;
|
|
187
|
+
let failed = 0;
|
|
188
|
+
const remaining = [];
|
|
189
|
+
|
|
190
|
+
for (const item of queue) {
|
|
191
|
+
try {
|
|
192
|
+
const result = await executeQueuedOperation(item);
|
|
193
|
+
if (result.success) {
|
|
194
|
+
processed++;
|
|
195
|
+
} else if (item.retries < 3) {
|
|
196
|
+
remaining.push({ ...item, retries: item.retries + 1 });
|
|
197
|
+
failed++;
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (item.retries < 3) {
|
|
201
|
+
remaining.push({ ...item, retries: item.retries + 1 });
|
|
202
|
+
}
|
|
203
|
+
failed++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
saveOfflineQueue(remaining);
|
|
208
|
+
return { processed, failed, remaining: remaining.length };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function executeQueuedOperation(item) {
|
|
212
|
+
switch (item.type) {
|
|
213
|
+
case 'proposal':
|
|
214
|
+
return await apiRequest(`/teams/${item.teamId}/proposals`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
body: JSON.stringify(item.data)
|
|
217
|
+
});
|
|
218
|
+
case 'knowledge':
|
|
219
|
+
return await apiRequest(`/teams/${item.teamId}/knowledge`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
body: JSON.stringify(item.data)
|
|
222
|
+
});
|
|
223
|
+
default:
|
|
224
|
+
return { success: false, error: 'Unknown operation type' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================
|
|
229
|
+
// API Client (with JWT refresh and offline support)
|
|
230
|
+
// ============================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Ensure valid access token, refresh if needed
|
|
234
|
+
*/
|
|
235
|
+
async function ensureValidToken() {
|
|
236
|
+
const state = getTeamState();
|
|
237
|
+
|
|
238
|
+
if (!state.accessToken || !state.refreshToken) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if token is about to expire
|
|
243
|
+
const expiresAt = state.tokenExpiresAt ? new Date(state.tokenExpiresAt).getTime() : 0;
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
|
|
246
|
+
if (now > expiresAt - TOKEN_REFRESH_THRESHOLD) {
|
|
247
|
+
// Token expired or about to expire, refresh it
|
|
248
|
+
const backendUrl = getBackendUrl();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch(`${backendUrl}/auth/refresh`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify({ refreshToken: state.refreshToken })
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (response.ok) {
|
|
258
|
+
const data = await response.json();
|
|
259
|
+
state.accessToken = data.accessToken;
|
|
260
|
+
state.tokenExpiresAt = new Date(now + (data.expiresIn || 3600) * 1000).toISOString();
|
|
261
|
+
saveTeamState(state);
|
|
262
|
+
return state.accessToken;
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Token refresh failed, return existing token and hope for the best
|
|
266
|
+
console.error('Token refresh failed:', err.message);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return state.accessToken;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Make authenticated request to backend
|
|
275
|
+
*/
|
|
276
|
+
async function apiRequest(endpoint, options = {}) {
|
|
277
|
+
const backendUrl = getBackendUrl();
|
|
278
|
+
const url = `${backendUrl}${endpoint}`;
|
|
279
|
+
|
|
280
|
+
// Get valid token (will refresh if needed)
|
|
281
|
+
const token = await ensureValidToken();
|
|
282
|
+
|
|
283
|
+
const headers = {
|
|
284
|
+
'Content-Type': 'application/json',
|
|
285
|
+
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
|
286
|
+
...options.headers
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(url, {
|
|
291
|
+
...options,
|
|
292
|
+
headers
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (response.status === 401) {
|
|
296
|
+
return { error: 'unauthorized', message: 'Token expired or invalid' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (response.status === 403) {
|
|
300
|
+
return { error: 'forbidden', message: 'Access denied' };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
const data = await response.json().catch(() => ({}));
|
|
305
|
+
return { error: 'api_error', message: data.error || response.statusText, status: response.status };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return await response.json();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
// Network error - add to offline queue if it's a write operation
|
|
311
|
+
if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method)) {
|
|
312
|
+
// Extract teamId from endpoint
|
|
313
|
+
const teamIdMatch = endpoint.match(/\/teams\/([^/]+)/);
|
|
314
|
+
if (teamIdMatch && options.body) {
|
|
315
|
+
addToOfflineQueue({
|
|
316
|
+
type: inferOperationType(endpoint),
|
|
317
|
+
teamId: teamIdMatch[1],
|
|
318
|
+
endpoint,
|
|
319
|
+
data: JSON.parse(options.body)
|
|
320
|
+
});
|
|
321
|
+
return { error: 'queued', message: 'Operation queued for sync when online' };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { error: 'network', message: `Backend unavailable: ${err.message}` };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function inferOperationType(endpoint) {
|
|
329
|
+
if (endpoint.includes('/proposals')) return 'proposal';
|
|
330
|
+
if (endpoint.includes('/knowledge')) return 'knowledge';
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================
|
|
335
|
+
// Auto-Apply Approved Proposals (v1.8.0)
|
|
336
|
+
// ============================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Apply approved team proposals to local decisions.md
|
|
340
|
+
*/
|
|
341
|
+
async function applyApprovedProposals(approvedProposals) {
|
|
342
|
+
if (!approvedProposals || approvedProposals.length === 0) return { applied: 0 };
|
|
343
|
+
|
|
344
|
+
// Load current decisions.md
|
|
345
|
+
let decisionsContent = '';
|
|
346
|
+
if (fileExists(DECISIONS_PATH)) {
|
|
347
|
+
decisionsContent = readFile(DECISIONS_PATH);
|
|
348
|
+
} else {
|
|
349
|
+
decisionsContent = '# Decisions\n\nProject coding rules and patterns.\n\n';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let applied = 0;
|
|
353
|
+
let currentContent = decisionsContent;
|
|
354
|
+
|
|
355
|
+
for (const proposal of approvedProposals) {
|
|
356
|
+
// Check if already in decisions.md
|
|
357
|
+
if (isRuleInDecisions(proposal.rule, currentContent)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Format and add to decisions.md
|
|
362
|
+
const formatted = formatProposalForDecisions(proposal);
|
|
363
|
+
currentContent = appendToDecisions(formatted, currentContent);
|
|
364
|
+
applied++;
|
|
365
|
+
|
|
366
|
+
// Mark as applied in local memory
|
|
367
|
+
try {
|
|
368
|
+
await memoryDb.markFactPromoted(`proposal:${proposal.id}`, 'decisions.md');
|
|
369
|
+
} catch (err) {
|
|
370
|
+
// Ignore if fact doesn't exist locally
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (applied > 0) {
|
|
375
|
+
writeFile(DECISIONS_PATH, currentContent);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { applied };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if rule is already in decisions.md
|
|
383
|
+
*/
|
|
384
|
+
function isRuleInDecisions(rule, content) {
|
|
385
|
+
const keywords = rule.split(/\s+/)
|
|
386
|
+
.filter(w => w.length > 4)
|
|
387
|
+
.slice(0, 5);
|
|
388
|
+
|
|
389
|
+
let matches = 0;
|
|
390
|
+
for (const keyword of keywords) {
|
|
391
|
+
if (content.toLowerCase().includes(keyword.toLowerCase())) {
|
|
392
|
+
matches++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return matches > keywords.length / 2;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Format proposal for decisions.md
|
|
401
|
+
*/
|
|
402
|
+
function formatProposalForDecisions(proposal) {
|
|
403
|
+
const sectionMap = {
|
|
404
|
+
'naming': 'Naming Conventions',
|
|
405
|
+
'pattern': 'Coding Patterns',
|
|
406
|
+
'architecture': 'Architecture Decisions',
|
|
407
|
+
'styling': 'Styling Rules',
|
|
408
|
+
'testing': 'Testing Conventions',
|
|
409
|
+
'error-handling': 'Error Handling',
|
|
410
|
+
'general': 'General Rules',
|
|
411
|
+
'api': 'API Patterns',
|
|
412
|
+
'component': 'Component Patterns'
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
section: sectionMap[proposal.category] || 'Team-Approved Rules',
|
|
417
|
+
rule: `- ${proposal.rule}`,
|
|
418
|
+
source: '(Team-approved)'
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Append formatted rule to decisions.md content
|
|
424
|
+
*/
|
|
425
|
+
function appendToDecisions(formatted, content) {
|
|
426
|
+
const lines = content.split('\n');
|
|
427
|
+
let sectionIndex = -1;
|
|
428
|
+
|
|
429
|
+
// Find the section
|
|
430
|
+
for (let i = 0; i < lines.length; i++) {
|
|
431
|
+
if (lines[i].includes(formatted.section) && lines[i].startsWith('#')) {
|
|
432
|
+
sectionIndex = i;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (sectionIndex === -1) {
|
|
438
|
+
// Section doesn't exist, append at end
|
|
439
|
+
return content.trim() + `\n\n## ${formatted.section}\n\n${formatted.rule} ${formatted.source}\n`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Find end of section (next heading or end of file)
|
|
443
|
+
let insertIndex = lines.length;
|
|
444
|
+
for (let i = sectionIndex + 1; i < lines.length; i++) {
|
|
445
|
+
if (lines[i].startsWith('#')) {
|
|
446
|
+
insertIndex = i;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Insert before next section
|
|
452
|
+
lines.splice(insertIndex, 0, `${formatted.rule} ${formatted.source}`);
|
|
453
|
+
|
|
454
|
+
return lines.join('\n');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================
|
|
458
|
+
// Team Commands
|
|
459
|
+
// ============================================================
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Login to team with invite code or credentials
|
|
463
|
+
*/
|
|
464
|
+
async function login(inviteCodeOrEmail, password) {
|
|
465
|
+
printHeader('Team Login');
|
|
466
|
+
|
|
467
|
+
if (!inviteCodeOrEmail) {
|
|
468
|
+
console.log('To join a team, you need an invite code from your team admin.');
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log('Options:');
|
|
471
|
+
console.log(' 1. Join with invite: ./scripts/flow team login <invite-code>');
|
|
472
|
+
console.log(' 2. Login with email: ./scripts/flow team login <email> <password>');
|
|
473
|
+
console.log('');
|
|
474
|
+
info('Team features require a subscription at https://wogi-flow.com');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const backendUrl = getBackendUrl();
|
|
479
|
+
|
|
480
|
+
// Determine if invite code or email login
|
|
481
|
+
const isEmail = inviteCodeOrEmail.includes('@');
|
|
482
|
+
let body;
|
|
483
|
+
|
|
484
|
+
if (isEmail) {
|
|
485
|
+
if (!password) {
|
|
486
|
+
error('Password required for email login');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
body = { email: inviteCodeOrEmail, password };
|
|
490
|
+
info('Logging in...');
|
|
491
|
+
} else {
|
|
492
|
+
// Prompt for email and password for invite code login
|
|
493
|
+
const readline = require('readline');
|
|
494
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
495
|
+
|
|
496
|
+
console.log('Setting up your account for this team...');
|
|
497
|
+
|
|
498
|
+
const email = await new Promise(resolve => rl.question('Email: ', resolve));
|
|
499
|
+
const pwd = await new Promise(resolve => {
|
|
500
|
+
process.stdout.write('Password: ');
|
|
501
|
+
// Note: In production, use a proper password input
|
|
502
|
+
rl.question('', resolve);
|
|
503
|
+
});
|
|
504
|
+
rl.close();
|
|
505
|
+
|
|
506
|
+
body = { inviteCode: inviteCodeOrEmail, email, password: pwd };
|
|
507
|
+
info('Validating invite and creating account...');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const response = await fetch(`${backendUrl}/auth/login`, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify(body)
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (!response.ok) {
|
|
518
|
+
const data = await response.json().catch(() => ({}));
|
|
519
|
+
if (response.status === 401) {
|
|
520
|
+
error(data.error || 'Invalid credentials or invite code');
|
|
521
|
+
} else {
|
|
522
|
+
error(`Login failed: ${data.error || response.statusText}`);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const result = await response.json();
|
|
528
|
+
|
|
529
|
+
// Save team state with tokens
|
|
530
|
+
const teamState = {
|
|
531
|
+
loggedIn: true,
|
|
532
|
+
teamId: result.teamId,
|
|
533
|
+
userId: result.userId,
|
|
534
|
+
teamName: result.teamName,
|
|
535
|
+
accessToken: result.accessToken,
|
|
536
|
+
refreshToken: result.refreshToken,
|
|
537
|
+
tokenExpiresAt: new Date(Date.now() + (result.expiresIn || 3600) * 1000).toISOString(),
|
|
538
|
+
setupId: null,
|
|
539
|
+
setupName: null,
|
|
540
|
+
lastSync: null
|
|
541
|
+
};
|
|
542
|
+
saveTeamState(teamState);
|
|
543
|
+
|
|
544
|
+
// Update config
|
|
545
|
+
const config = getConfig();
|
|
546
|
+
config.team = {
|
|
547
|
+
...config.team,
|
|
548
|
+
enabled: true,
|
|
549
|
+
teamId: result.teamId,
|
|
550
|
+
userId: result.userId,
|
|
551
|
+
backendUrl
|
|
552
|
+
};
|
|
553
|
+
saveConfig(config);
|
|
554
|
+
|
|
555
|
+
success(`Logged in to team: ${result.teamName || result.teamId}`);
|
|
556
|
+
|
|
557
|
+
// Trigger initial sync
|
|
558
|
+
console.log('');
|
|
559
|
+
info('Running initial sync...');
|
|
560
|
+
await sync({ silent: true });
|
|
561
|
+
success('Initial sync complete');
|
|
562
|
+
|
|
563
|
+
} catch (err) {
|
|
564
|
+
error(`Could not connect to team backend: ${err.message}`);
|
|
565
|
+
info('Check your internet connection or try again later.');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Logout from team
|
|
571
|
+
*/
|
|
572
|
+
async function logout() {
|
|
573
|
+
printHeader('Team Logout');
|
|
574
|
+
|
|
575
|
+
const state = getTeamState();
|
|
576
|
+
|
|
577
|
+
if (!state.loggedIn) {
|
|
578
|
+
warn('Not logged in to any team.');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const teamName = state.teamName || 'team';
|
|
583
|
+
|
|
584
|
+
// Clear team state
|
|
585
|
+
saveTeamState({
|
|
586
|
+
loggedIn: false,
|
|
587
|
+
teamId: null,
|
|
588
|
+
userId: null,
|
|
589
|
+
teamName: null,
|
|
590
|
+
setupId: null,
|
|
591
|
+
setupName: null,
|
|
592
|
+
lastSync: null,
|
|
593
|
+
accessToken: null,
|
|
594
|
+
refreshToken: null,
|
|
595
|
+
tokenExpiresAt: null
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Update config
|
|
599
|
+
const config = getConfig();
|
|
600
|
+
config.team = {
|
|
601
|
+
...config.team,
|
|
602
|
+
enabled: false
|
|
603
|
+
};
|
|
604
|
+
saveConfig(config);
|
|
605
|
+
|
|
606
|
+
success(`Logged out from ${teamName}`);
|
|
607
|
+
info('Local data preserved. Team features disabled.');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Sync with team backend
|
|
612
|
+
*/
|
|
613
|
+
async function sync(options = {}) {
|
|
614
|
+
if (!options.silent) printHeader('Team Sync');
|
|
615
|
+
|
|
616
|
+
const state = getTeamState();
|
|
617
|
+
|
|
618
|
+
if (!state.loggedIn) {
|
|
619
|
+
error('Not logged in to a team.');
|
|
620
|
+
return { pulled: 0, pushed: 0 };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const { silent = false } = options;
|
|
624
|
+
|
|
625
|
+
if (!silent) info('Syncing with team...');
|
|
626
|
+
|
|
627
|
+
// 1. Process offline queue first
|
|
628
|
+
const queueResult = await processOfflineQueue();
|
|
629
|
+
if (queueResult.processed > 0 && !silent) {
|
|
630
|
+
info(`Processed ${queueResult.processed} queued operations`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 2. Get unsynced proposals from local database
|
|
634
|
+
const localProposals = await memoryDb.getUnsyncedProposals();
|
|
635
|
+
let pushed = 0;
|
|
636
|
+
|
|
637
|
+
// Push local proposals
|
|
638
|
+
for (const proposal of localProposals) {
|
|
639
|
+
const result = await apiRequest(`/teams/${state.teamId}/proposals`, {
|
|
640
|
+
method: 'POST',
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
rule: proposal.rule,
|
|
643
|
+
category: proposal.category,
|
|
644
|
+
rationale: proposal.rationale,
|
|
645
|
+
sourceContext: proposal.source_context,
|
|
646
|
+
localId: proposal.id
|
|
647
|
+
})
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
if (!result.error || result.error === 'queued') {
|
|
651
|
+
await memoryDb.updateProposal(proposal.id, {
|
|
652
|
+
synced: true,
|
|
653
|
+
remoteId: result.id
|
|
654
|
+
});
|
|
655
|
+
pushed++;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 3. Pull new knowledge
|
|
660
|
+
const pullResult = await apiRequest(`/teams/${state.teamId}/knowledge?since=${state.lastSync || ''}`);
|
|
661
|
+
|
|
662
|
+
let pulled = 0;
|
|
663
|
+
if (!pullResult.error && pullResult.knowledge) {
|
|
664
|
+
for (const item of pullResult.knowledge) {
|
|
665
|
+
await memoryDb.storeFact({
|
|
666
|
+
fact: item.fact,
|
|
667
|
+
category: item.category,
|
|
668
|
+
scope: 'team',
|
|
669
|
+
model: item.modelSpecific,
|
|
670
|
+
sourceContext: `team:${state.teamId}`
|
|
671
|
+
});
|
|
672
|
+
pulled++;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 4. Pull approved proposals and auto-apply to decisions.md (v1.8.0)
|
|
677
|
+
let appliedProposals = 0;
|
|
678
|
+
const config = getConfig();
|
|
679
|
+
const autoApply = config.automaticPromotion?.autoApplyTeamApproved !== false;
|
|
680
|
+
|
|
681
|
+
if (autoApply) {
|
|
682
|
+
const approvedResult = await apiRequest(`/teams/${state.teamId}/proposals?status=approved&since=${state.lastSync || ''}`);
|
|
683
|
+
|
|
684
|
+
if (!approvedResult.error && approvedResult.proposals && approvedResult.proposals.length > 0) {
|
|
685
|
+
const applyResult = await applyApprovedProposals(approvedResult.proposals);
|
|
686
|
+
appliedProposals = applyResult.applied;
|
|
687
|
+
|
|
688
|
+
if (!silent && appliedProposals > 0) {
|
|
689
|
+
success(`Applied ${appliedProposals} team-approved rule(s) to decisions.md`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Update last sync time
|
|
695
|
+
state.lastSync = new Date().toISOString();
|
|
696
|
+
saveTeamState(state);
|
|
697
|
+
|
|
698
|
+
if (!silent) {
|
|
699
|
+
success(`Sync complete: ${pulled} pulled, ${pushed} pushed`);
|
|
700
|
+
if (appliedProposals > 0) {
|
|
701
|
+
info(`${appliedProposals} approved rule(s) added to decisions.md`);
|
|
702
|
+
}
|
|
703
|
+
if (queueResult.remaining > 0) {
|
|
704
|
+
warn(`${queueResult.remaining} operations still queued (will retry)`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return { pulled, pushed, appliedProposals };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Show/vote on team proposals
|
|
713
|
+
*/
|
|
714
|
+
async function proposals(action, proposalId, vote) {
|
|
715
|
+
printHeader('Team Proposals');
|
|
716
|
+
|
|
717
|
+
const state = getTeamState();
|
|
718
|
+
|
|
719
|
+
if (!state.loggedIn) {
|
|
720
|
+
error('Not logged in to a team.');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (action === 'vote' && proposalId && vote) {
|
|
725
|
+
if (!['approve', 'reject'].includes(vote)) {
|
|
726
|
+
error('Vote must be "approve" or "reject"');
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const result = await apiRequest(`/teams/${state.teamId}/proposals/${proposalId}/vote`, {
|
|
731
|
+
method: 'POST',
|
|
732
|
+
body: JSON.stringify({ vote, comment: '' })
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
if (result.error) {
|
|
736
|
+
if (result.error === 'queued') {
|
|
737
|
+
info('Vote queued for sync when online');
|
|
738
|
+
} else {
|
|
739
|
+
error(`Failed to vote: ${result.message}`);
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
success(`Vote recorded: ${vote} on proposal #${proposalId}`);
|
|
745
|
+
|
|
746
|
+
if (result.status === 'approved') {
|
|
747
|
+
success('Proposal approved and added to team knowledge!');
|
|
748
|
+
} else if (result.status === 'rejected') {
|
|
749
|
+
info('Proposal rejected');
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// List pending proposals
|
|
755
|
+
const result = await apiRequest(`/teams/${state.teamId}/proposals?status=pending`);
|
|
756
|
+
|
|
757
|
+
if (result.error) {
|
|
758
|
+
error(`Failed to fetch proposals: ${result.message}`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (!result.proposals || result.proposals.length === 0) {
|
|
763
|
+
info('No pending proposals.');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
console.log('');
|
|
768
|
+
console.log('Pending proposals:');
|
|
769
|
+
console.log('');
|
|
770
|
+
|
|
771
|
+
for (const p of result.proposals) {
|
|
772
|
+
const votes = p.votes || [];
|
|
773
|
+
const approvals = votes.filter(v => v.vote === 'approve').length;
|
|
774
|
+
const rejections = votes.filter(v => v.vote === 'reject').length;
|
|
775
|
+
|
|
776
|
+
console.log(` #${p.id} | ${p.category}`);
|
|
777
|
+
console.log(color('dim', ' ' + '─'.repeat(50)));
|
|
778
|
+
console.log(` Rule: "${p.rule}"`);
|
|
779
|
+
if (p.rationale) {
|
|
780
|
+
console.log(color('dim', ` Rationale: ${p.rationale}`));
|
|
781
|
+
}
|
|
782
|
+
console.log(` Votes: ${approvals} approve, ${rejections} reject`);
|
|
783
|
+
console.log('');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
info('Vote with: ./scripts/flow team proposals vote <id> <approve|reject>');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Generate invite code (admin only)
|
|
791
|
+
*/
|
|
792
|
+
async function invite(expiresInDays = 7) {
|
|
793
|
+
printHeader('Generate Invite');
|
|
794
|
+
|
|
795
|
+
const state = getTeamState();
|
|
796
|
+
|
|
797
|
+
if (!state.loggedIn) {
|
|
798
|
+
error('Not logged in to a team.');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const result = await apiRequest(`/teams/${state.teamId}/invites`, {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
body: JSON.stringify({ expiresInDays: parseInt(expiresInDays) || 7 })
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (result.error) {
|
|
808
|
+
if (result.error === 'forbidden') {
|
|
809
|
+
error('Only team admins can generate invite codes.');
|
|
810
|
+
} else {
|
|
811
|
+
error(`Failed to generate invite: ${result.message}`);
|
|
812
|
+
}
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
console.log('');
|
|
817
|
+
console.log('Invite code generated:');
|
|
818
|
+
console.log('');
|
|
819
|
+
console.log(` ${color('green', result.code)}`);
|
|
820
|
+
console.log('');
|
|
821
|
+
console.log(`Valid for: ${expiresInDays} days`);
|
|
822
|
+
if (result.url) {
|
|
823
|
+
console.log(`Join URL: ${result.url}`);
|
|
824
|
+
}
|
|
825
|
+
console.log('');
|
|
826
|
+
info('Share this code with team members to join.');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Show team status
|
|
831
|
+
*/
|
|
832
|
+
async function status() {
|
|
833
|
+
printHeader('Team Status');
|
|
834
|
+
|
|
835
|
+
const state = getTeamState();
|
|
836
|
+
const config = getConfig();
|
|
837
|
+
|
|
838
|
+
if (!state.loggedIn) {
|
|
839
|
+
console.log('Status: ' + color('yellow', 'Not logged in'));
|
|
840
|
+
console.log('');
|
|
841
|
+
info('Join a team: ./scripts/flow team login <invite-code>');
|
|
842
|
+
info('Learn more: https://wogi-flow.com/teams');
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
console.log('Status: ' + color('green', 'Logged in'));
|
|
847
|
+
console.log('');
|
|
848
|
+
console.log(`Team: ${state.teamName || state.teamId}`);
|
|
849
|
+
console.log(`Setup: ${state.setupName || 'None selected'}`);
|
|
850
|
+
console.log(`Last sync: ${state.lastSync || 'Never'}`);
|
|
851
|
+
console.log('');
|
|
852
|
+
|
|
853
|
+
// Check token status
|
|
854
|
+
const expiresAt = state.tokenExpiresAt ? new Date(state.tokenExpiresAt) : null;
|
|
855
|
+
if (expiresAt) {
|
|
856
|
+
const now = new Date();
|
|
857
|
+
if (expiresAt < now) {
|
|
858
|
+
warn('Access token expired - will refresh on next request');
|
|
859
|
+
} else {
|
|
860
|
+
const hours = Math.round((expiresAt - now) / (1000 * 60 * 60));
|
|
861
|
+
console.log(`Token: Valid for ~${hours} hours`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
console.log('');
|
|
865
|
+
|
|
866
|
+
// Show offline queue status
|
|
867
|
+
const queue = getOfflineQueue();
|
|
868
|
+
if (queue.length > 0) {
|
|
869
|
+
warn(`${queue.length} operation(s) queued for sync`);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Show stats from database
|
|
873
|
+
const stats = await memoryDb.getStats();
|
|
874
|
+
console.log('Local stats:');
|
|
875
|
+
console.log(` Facts: ${stats.facts.total}`);
|
|
876
|
+
console.log(` Proposals: ${stats.proposals.total} (${stats.proposals.pending} pending)`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ============================================================
|
|
880
|
+
// CLI
|
|
881
|
+
// ============================================================
|
|
882
|
+
|
|
883
|
+
function printUsage() {
|
|
884
|
+
console.log(`
|
|
885
|
+
Wogi Flow - Team Collaboration
|
|
886
|
+
|
|
887
|
+
Usage: ./scripts/flow team <command> [args]
|
|
888
|
+
|
|
889
|
+
Commands:
|
|
890
|
+
login <code> Join team with invite code
|
|
891
|
+
login <email> <pass> Login with email and password
|
|
892
|
+
logout Leave current team
|
|
893
|
+
sync Sync knowledge with team
|
|
894
|
+
proposals List pending proposals
|
|
895
|
+
proposals vote <id> <approve|reject>
|
|
896
|
+
Vote on a proposal
|
|
897
|
+
invite [days] Generate invite code (admin only)
|
|
898
|
+
status Show team status
|
|
899
|
+
|
|
900
|
+
Examples:
|
|
901
|
+
./scripts/flow team login ABC123XY
|
|
902
|
+
./scripts/flow team login user@example.com mypassword
|
|
903
|
+
./scripts/flow team sync
|
|
904
|
+
./scripts/flow team proposals vote prop_abc123 approve
|
|
905
|
+
./scripts/flow team invite 14
|
|
906
|
+
|
|
907
|
+
Team features require a subscription at https://wogi-flow.com
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function main() {
|
|
912
|
+
const args = process.argv.slice(2);
|
|
913
|
+
const command = args[0];
|
|
914
|
+
|
|
915
|
+
switch (command) {
|
|
916
|
+
case 'login':
|
|
917
|
+
await login(args[1], args[2]);
|
|
918
|
+
break;
|
|
919
|
+
|
|
920
|
+
case 'logout':
|
|
921
|
+
await logout();
|
|
922
|
+
break;
|
|
923
|
+
|
|
924
|
+
case 'sync':
|
|
925
|
+
await sync();
|
|
926
|
+
break;
|
|
927
|
+
|
|
928
|
+
case 'proposals':
|
|
929
|
+
await proposals(args[1], args[2], args[3]);
|
|
930
|
+
break;
|
|
931
|
+
|
|
932
|
+
case 'invite':
|
|
933
|
+
await invite(args[1]);
|
|
934
|
+
break;
|
|
935
|
+
|
|
936
|
+
case 'status':
|
|
937
|
+
await status();
|
|
938
|
+
break;
|
|
939
|
+
|
|
940
|
+
case '--help':
|
|
941
|
+
case '-h':
|
|
942
|
+
case 'help':
|
|
943
|
+
printUsage();
|
|
944
|
+
break;
|
|
945
|
+
|
|
946
|
+
default:
|
|
947
|
+
if (command) {
|
|
948
|
+
error(`Unknown command: ${command}`);
|
|
949
|
+
}
|
|
950
|
+
printUsage();
|
|
951
|
+
process.exit(command ? 1 : 0);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ============================================================
|
|
956
|
+
// Exports
|
|
957
|
+
// ============================================================
|
|
958
|
+
|
|
959
|
+
module.exports = {
|
|
960
|
+
getTeamState,
|
|
961
|
+
saveTeamState,
|
|
962
|
+
isTeamEnabled,
|
|
963
|
+
login,
|
|
964
|
+
logout,
|
|
965
|
+
sync,
|
|
966
|
+
proposals,
|
|
967
|
+
invite,
|
|
968
|
+
status,
|
|
969
|
+
processOfflineQueue
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
if (require.main === module) {
|
|
973
|
+
main().catch(e => {
|
|
974
|
+
error(`Error: ${err.message}`);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
});
|
|
977
|
+
}
|