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,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Background Sync Daemon
|
|
5
|
+
*
|
|
6
|
+
* Keeps workflow state in sync when multiple agents work on different branches.
|
|
7
|
+
* Watches .workflow/state/ for changes and handles branch switching.
|
|
8
|
+
*
|
|
9
|
+
* Part of Phase 6: Team & Integrations
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* flow sync-daemon start Start the daemon
|
|
13
|
+
* flow sync-daemon stop Stop the daemon
|
|
14
|
+
* flow sync-daemon status Check daemon status
|
|
15
|
+
* flow sync-daemon restart Restart the daemon
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync, spawn } = require('child_process');
|
|
21
|
+
const {
|
|
22
|
+
PROJECT_ROOT,
|
|
23
|
+
STATE_DIR,
|
|
24
|
+
parseFlags,
|
|
25
|
+
color,
|
|
26
|
+
info,
|
|
27
|
+
warn,
|
|
28
|
+
error,
|
|
29
|
+
success,
|
|
30
|
+
fileExists,
|
|
31
|
+
safeJsonParse,
|
|
32
|
+
getConfig,
|
|
33
|
+
printHeader
|
|
34
|
+
} = require('./flow-utils');
|
|
35
|
+
|
|
36
|
+
// ============================================================
|
|
37
|
+
// Constants
|
|
38
|
+
// ============================================================
|
|
39
|
+
|
|
40
|
+
const PID_FILE = path.join(STATE_DIR, 'sync-daemon.pid');
|
|
41
|
+
const LOG_FILE = path.join(STATE_DIR, 'sync-daemon.log');
|
|
42
|
+
const HEARTBEAT_FILE = path.join(STATE_DIR, 'sync-daemon.heartbeat');
|
|
43
|
+
const SYNC_STATE_FILE = path.join(STATE_DIR, 'sync-state.json');
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CONFIG = {
|
|
46
|
+
enabled: false,
|
|
47
|
+
watchPaths: ['.workflow/state/'],
|
|
48
|
+
syncOnBranchSwitch: true,
|
|
49
|
+
heartbeatIntervalMs: 30000,
|
|
50
|
+
debounceMs: 1000,
|
|
51
|
+
maxLogSizeBytes: 1024 * 1024 // 1MB
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Configuration
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get sync daemon configuration
|
|
60
|
+
*/
|
|
61
|
+
function getSyncConfig() {
|
|
62
|
+
const config = getConfig();
|
|
63
|
+
return {
|
|
64
|
+
...DEFAULT_CONFIG,
|
|
65
|
+
...(config?.syncDaemon || {})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================
|
|
70
|
+
// Daemon Management
|
|
71
|
+
// ============================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if daemon is running
|
|
75
|
+
*/
|
|
76
|
+
function isDaemonRunning() {
|
|
77
|
+
if (!fileExists(PID_FILE)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
83
|
+
// Check if process exists
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
|
|
86
|
+
// Check heartbeat freshness (2.5x interval for clearer threshold)
|
|
87
|
+
if (fileExists(HEARTBEAT_FILE)) {
|
|
88
|
+
try {
|
|
89
|
+
const heartbeat = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf-8'));
|
|
90
|
+
if (heartbeat?.timestamp) {
|
|
91
|
+
const age = Date.now() - new Date(heartbeat.timestamp).getTime();
|
|
92
|
+
const config = getSyncConfig();
|
|
93
|
+
const threshold = config.heartbeatIntervalMs * 2.5;
|
|
94
|
+
|
|
95
|
+
if (!isNaN(age) && age > threshold) {
|
|
96
|
+
warn('Daemon heartbeat stale, may be unresponsive');
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (parseError) {
|
|
101
|
+
warn('Failed to parse heartbeat file: ' + parseError.message);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Process doesn't exist
|
|
109
|
+
cleanupPidFile();
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get daemon status
|
|
116
|
+
*/
|
|
117
|
+
function getDaemonStatus() {
|
|
118
|
+
const running = isDaemonRunning();
|
|
119
|
+
let pid = null;
|
|
120
|
+
let heartbeat = null;
|
|
121
|
+
let currentBranch = null;
|
|
122
|
+
|
|
123
|
+
if (fileExists(PID_FILE)) {
|
|
124
|
+
pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (fileExists(HEARTBEAT_FILE)) {
|
|
128
|
+
try {
|
|
129
|
+
heartbeat = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf-8'));
|
|
130
|
+
} catch {
|
|
131
|
+
heartbeat = null; // Invalid JSON, treat as no heartbeat
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
currentBranch = execSync('git branch --show-current', {
|
|
137
|
+
encoding: 'utf-8',
|
|
138
|
+
cwd: PROJECT_ROOT
|
|
139
|
+
}).trim();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
currentBranch = 'unknown';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const syncState = safeJsonParse(SYNC_STATE_FILE) || {};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
running,
|
|
148
|
+
pid,
|
|
149
|
+
heartbeat,
|
|
150
|
+
currentBranch,
|
|
151
|
+
lastSync: syncState.lastSync,
|
|
152
|
+
syncedBranches: syncState.branches || {},
|
|
153
|
+
config: getSyncConfig()
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Cleanup stale PID file
|
|
159
|
+
*/
|
|
160
|
+
function cleanupPidFile() {
|
|
161
|
+
if (fileExists(PID_FILE)) {
|
|
162
|
+
fs.unlinkSync(PID_FILE);
|
|
163
|
+
}
|
|
164
|
+
if (fileExists(HEARTBEAT_FILE)) {
|
|
165
|
+
fs.unlinkSync(HEARTBEAT_FILE);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Filter environment variables for daemon (security: only pass necessary vars)
|
|
171
|
+
*/
|
|
172
|
+
function getSafeEnv() {
|
|
173
|
+
const safeVars = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'TERM', 'NODE_ENV'];
|
|
174
|
+
const env = { WOGI_DAEMON: '1' };
|
|
175
|
+
for (const key of safeVars) {
|
|
176
|
+
if (process.env[key]) {
|
|
177
|
+
env[key] = process.env[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return env;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Start the daemon
|
|
185
|
+
*/
|
|
186
|
+
function startDaemon() {
|
|
187
|
+
if (isDaemonRunning()) {
|
|
188
|
+
warn('Daemon is already running');
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const config = getSyncConfig();
|
|
193
|
+
if (!config.enabled) {
|
|
194
|
+
warn('Sync daemon is disabled in config');
|
|
195
|
+
info('Enable with: flow config set syncDaemon.enabled true');
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Start daemon as detached process with filtered environment
|
|
200
|
+
const daemon = spawn('node', [__filename, '--daemon'], {
|
|
201
|
+
detached: true,
|
|
202
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
203
|
+
cwd: PROJECT_ROOT,
|
|
204
|
+
env: getSafeEnv()
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
daemon.unref();
|
|
208
|
+
|
|
209
|
+
// Write PID file
|
|
210
|
+
fs.writeFileSync(PID_FILE, daemon.pid.toString());
|
|
211
|
+
|
|
212
|
+
// Verify daemon started by waiting briefly and checking heartbeat
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (!fileExists(HEARTBEAT_FILE)) {
|
|
215
|
+
warn('Daemon may have failed to start - no heartbeat file yet');
|
|
216
|
+
warn('Check log file for errors: ' + LOG_FILE);
|
|
217
|
+
}
|
|
218
|
+
}, 500);
|
|
219
|
+
|
|
220
|
+
success(`Daemon started (PID: ${daemon.pid})`);
|
|
221
|
+
info(`Log file: ${LOG_FILE}`);
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Stop the daemon
|
|
228
|
+
*/
|
|
229
|
+
function stopDaemon() {
|
|
230
|
+
if (!fileExists(PID_FILE)) {
|
|
231
|
+
info('Daemon is not running');
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
|
|
237
|
+
process.kill(pid, 'SIGTERM');
|
|
238
|
+
cleanupPidFile();
|
|
239
|
+
success('Daemon stopped');
|
|
240
|
+
return true;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
cleanupPidFile();
|
|
243
|
+
info('Daemon was not running (cleaned up stale PID)');
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// Daemon Process
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Log message to file with proper error handling
|
|
254
|
+
*/
|
|
255
|
+
function log(level, message) {
|
|
256
|
+
const timestamp = new Date().toISOString();
|
|
257
|
+
const line = `${timestamp} [${level}] ${message}\n`;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
// Rotate log if too large
|
|
261
|
+
const config = getSyncConfig();
|
|
262
|
+
if (fileExists(LOG_FILE)) {
|
|
263
|
+
try {
|
|
264
|
+
const stats = fs.statSync(LOG_FILE);
|
|
265
|
+
if (stats.size > config.maxLogSizeBytes) {
|
|
266
|
+
const backupPath = LOG_FILE + '.old';
|
|
267
|
+
try {
|
|
268
|
+
if (fileExists(backupPath)) fs.unlinkSync(backupPath);
|
|
269
|
+
fs.renameSync(LOG_FILE, backupPath);
|
|
270
|
+
} catch (rotateError) {
|
|
271
|
+
// Log rotation failed, continue anyway
|
|
272
|
+
console.error(console.error('Log rotation failed:', rotateError.message));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (statError) {
|
|
276
|
+
// Stat failed, continue anyway
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fs.appendFileSync(LOG_FILE, line);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// Silently fail to avoid infinite loops if logging fails
|
|
283
|
+
console.error(console.error('Failed to write log:', err.message));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Update heartbeat
|
|
289
|
+
*/
|
|
290
|
+
function updateHeartbeat() {
|
|
291
|
+
const status = {
|
|
292
|
+
timestamp: new Date().toISOString(),
|
|
293
|
+
pid: process.pid,
|
|
294
|
+
branch: getCurrentBranch(),
|
|
295
|
+
uptime: process.uptime()
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify(status, null, 2));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get current git branch
|
|
303
|
+
*/
|
|
304
|
+
function getCurrentBranch() {
|
|
305
|
+
try {
|
|
306
|
+
return execSync('git branch --show-current', {
|
|
307
|
+
encoding: 'utf-8',
|
|
308
|
+
cwd: PROJECT_ROOT
|
|
309
|
+
}).trim();
|
|
310
|
+
} catch (err) {
|
|
311
|
+
return 'unknown';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Detect branch switch
|
|
317
|
+
*/
|
|
318
|
+
let lastBranch = null;
|
|
319
|
+
function detectBranchSwitch() {
|
|
320
|
+
const currentBranch = getCurrentBranch();
|
|
321
|
+
|
|
322
|
+
if (lastBranch && lastBranch !== currentBranch) {
|
|
323
|
+
log('INFO', `Branch switched: ${lastBranch} -> ${currentBranch}`);
|
|
324
|
+
handleBranchSwitch(lastBranch, currentBranch);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
lastBranch = currentBranch;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Handle branch switch
|
|
332
|
+
*/
|
|
333
|
+
function handleBranchSwitch(fromBranch, toBranch) {
|
|
334
|
+
const config = getSyncConfig();
|
|
335
|
+
|
|
336
|
+
if (!config.syncOnBranchSwitch) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Save state for current branch
|
|
341
|
+
saveBranchState(fromBranch);
|
|
342
|
+
|
|
343
|
+
// Load state for new branch
|
|
344
|
+
loadBranchState(toBranch);
|
|
345
|
+
|
|
346
|
+
// Invalidate caches
|
|
347
|
+
invalidateCaches();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Validate branch name format (security: prevent JSON key injection)
|
|
352
|
+
*/
|
|
353
|
+
function isValidBranchName(branch) {
|
|
354
|
+
// Allow alphanumeric, dots, dashes, underscores, slashes (max 255 chars)
|
|
355
|
+
return branch &&
|
|
356
|
+
typeof branch === 'string' &&
|
|
357
|
+
branch.length <= 255 &&
|
|
358
|
+
/^[a-zA-Z0-9._/-]+$/.test(branch);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Save state for a branch
|
|
363
|
+
*/
|
|
364
|
+
function saveBranchState(branch) {
|
|
365
|
+
// Validate branch name before using as JSON key
|
|
366
|
+
if (!isValidBranchName(branch)) {
|
|
367
|
+
log('WARN', `Invalid branch name format, skipping save: ${branch?.slice(0, 50)}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const syncState = safeJsonParse(SYNC_STATE_FILE) || { branches: {} };
|
|
372
|
+
|
|
373
|
+
syncState.branches[branch] = {
|
|
374
|
+
savedAt: new Date().toISOString(),
|
|
375
|
+
ready: safeJsonParse(path.join(STATE_DIR, 'ready.json')),
|
|
376
|
+
progress: fileExists(path.join(STATE_DIR, 'progress.md'))
|
|
377
|
+
? fs.readFileSync(path.join(STATE_DIR, 'progress.md'), 'utf-8').slice(0, 1000)
|
|
378
|
+
: null
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(syncState, null, 2));
|
|
382
|
+
log('INFO', `Saved state for branch: ${branch}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Load state for a branch
|
|
387
|
+
*/
|
|
388
|
+
function loadBranchState(branch) {
|
|
389
|
+
const syncState = safeJsonParse(SYNC_STATE_FILE);
|
|
390
|
+
|
|
391
|
+
if (!syncState?.branches?.[branch]) {
|
|
392
|
+
log('INFO', `No saved state for branch: ${branch}`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const branchState = syncState.branches[branch];
|
|
397
|
+
|
|
398
|
+
// Optionally restore ready.json (be careful not to overwrite work)
|
|
399
|
+
// For now, just log that we could restore
|
|
400
|
+
log('INFO', `Found saved state for branch: ${branch} (from ${branchState.savedAt})`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Invalidate caches
|
|
405
|
+
*/
|
|
406
|
+
function invalidateCaches() {
|
|
407
|
+
const cacheFiles = [
|
|
408
|
+
'jira-cache.json',
|
|
409
|
+
'linear-cache.json',
|
|
410
|
+
'component-index.json'
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
for (const file of cacheFiles) {
|
|
414
|
+
const cachePath = path.join(STATE_DIR, file);
|
|
415
|
+
if (fileExists(cachePath)) {
|
|
416
|
+
fs.unlinkSync(cachePath);
|
|
417
|
+
log('DEBUG', `Invalidated cache: ${file}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* File watcher callback (debounced)
|
|
424
|
+
*/
|
|
425
|
+
let debounceTimer = null;
|
|
426
|
+
function onFileChange(eventType, filename) {
|
|
427
|
+
const config = getSyncConfig();
|
|
428
|
+
|
|
429
|
+
// Debounce rapid changes
|
|
430
|
+
if (debounceTimer) {
|
|
431
|
+
clearTimeout(debounceTimer);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
debounceTimer = setTimeout(() => {
|
|
435
|
+
log('DEBUG', `File changed: ${filename} (${eventType})`);
|
|
436
|
+
updateSyncState(filename);
|
|
437
|
+
}, config.debounceMs);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Update sync state after file change
|
|
442
|
+
*/
|
|
443
|
+
function updateSyncState(filename) {
|
|
444
|
+
const syncState = safeJsonParse(SYNC_STATE_FILE) || { branches: {} };
|
|
445
|
+
|
|
446
|
+
syncState.lastSync = new Date().toISOString();
|
|
447
|
+
syncState.lastFile = filename;
|
|
448
|
+
|
|
449
|
+
fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(syncState, null, 2));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Run the daemon
|
|
454
|
+
*/
|
|
455
|
+
function runDaemon() {
|
|
456
|
+
const config = getSyncConfig();
|
|
457
|
+
|
|
458
|
+
log('INFO', `Daemon started (PID: ${process.pid})`);
|
|
459
|
+
log('INFO', `Watch paths: ${config.watchPaths.join(', ')}`);
|
|
460
|
+
log('INFO', `Heartbeat interval: ${config.heartbeatIntervalMs}ms`);
|
|
461
|
+
|
|
462
|
+
// Initial state
|
|
463
|
+
lastBranch = getCurrentBranch();
|
|
464
|
+
log('INFO', `Current branch: ${lastBranch}`);
|
|
465
|
+
|
|
466
|
+
// Set up file watcher
|
|
467
|
+
const watchers = [];
|
|
468
|
+
for (const watchPath of config.watchPaths) {
|
|
469
|
+
const fullPath = path.join(PROJECT_ROOT, watchPath);
|
|
470
|
+
|
|
471
|
+
if (!fs.existsSync(fullPath)) {
|
|
472
|
+
log('WARN', `Watch path does not exist: ${watchPath}`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const watcher = fs.watch(fullPath, { recursive: true }, onFileChange);
|
|
478
|
+
watchers.push(watcher);
|
|
479
|
+
log('INFO', `Watching: ${watchPath}`);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
log('ERROR', `Failed to watch ${watchPath}: ${err.message}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Heartbeat interval
|
|
486
|
+
const heartbeatInterval = setInterval(() => {
|
|
487
|
+
updateHeartbeat();
|
|
488
|
+
detectBranchSwitch();
|
|
489
|
+
}, config.heartbeatIntervalMs);
|
|
490
|
+
|
|
491
|
+
// Initial heartbeat
|
|
492
|
+
updateHeartbeat();
|
|
493
|
+
|
|
494
|
+
// Handle signals
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
log('INFO', 'Daemon stopping...');
|
|
497
|
+
clearInterval(heartbeatInterval);
|
|
498
|
+
for (const watcher of watchers) {
|
|
499
|
+
watcher.close();
|
|
500
|
+
}
|
|
501
|
+
cleanupPidFile();
|
|
502
|
+
log('INFO', 'Daemon stopped');
|
|
503
|
+
process.exit(0);
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
process.on('SIGTERM', cleanup);
|
|
507
|
+
process.on('SIGINT', cleanup);
|
|
508
|
+
|
|
509
|
+
log('INFO', 'Daemon running. Send SIGTERM to stop.');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ============================================================
|
|
513
|
+
// CLI Output
|
|
514
|
+
// ============================================================
|
|
515
|
+
|
|
516
|
+
function printStatus(status) {
|
|
517
|
+
printHeader('SYNC DAEMON STATUS');
|
|
518
|
+
|
|
519
|
+
console.log(` ${color('dim', 'Running:')} ${status.running ? color('green', 'Yes') : color('red', 'No')}`);
|
|
520
|
+
console.log(` ${color('dim', 'PID:')} ${status.pid || 'N/A'}`);
|
|
521
|
+
console.log(` ${color('dim', 'Current branch:')} ${color('cyan', status.currentBranch)}`);
|
|
522
|
+
|
|
523
|
+
if (status.heartbeat) {
|
|
524
|
+
const age = Date.now() - new Date(status.heartbeat.timestamp).getTime();
|
|
525
|
+
const ageStr = age < 60000 ? `${Math.floor(age / 1000)}s ago` : `${Math.floor(age / 60000)}m ago`;
|
|
526
|
+
console.log(` ${color('dim', 'Last heartbeat:')} ${ageStr}`);
|
|
527
|
+
console.log(` ${color('dim', 'Uptime:')} ${Math.floor(status.heartbeat.uptime / 60)}m`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (status.lastSync) {
|
|
531
|
+
console.log(` ${color('dim', 'Last sync:')} ${status.lastSync}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
console.log(`\n ${color('dim', 'Configuration:')}`);
|
|
535
|
+
console.log(` ${color('dim', ' Enabled:')} ${status.config.enabled ? 'Yes' : 'No'}`);
|
|
536
|
+
console.log(` ${color('dim', ' Watch paths:')} ${status.config.watchPaths.join(', ')}`);
|
|
537
|
+
console.log(` ${color('dim', ' Branch sync:')} ${status.config.syncOnBranchSwitch ? 'Yes' : 'No'}`);
|
|
538
|
+
|
|
539
|
+
const branchCount = Object.keys(status.syncedBranches).length;
|
|
540
|
+
if (branchCount > 0) {
|
|
541
|
+
console.log(`\n ${color('dim', `Synced branches: ${branchCount}`)}`);
|
|
542
|
+
for (const [branch, data] of Object.entries(status.syncedBranches)) {
|
|
543
|
+
console.log(` - ${branch} (${data.savedAt})`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
console.log('');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ============================================================
|
|
551
|
+
// CLI Entry Point
|
|
552
|
+
// ============================================================
|
|
553
|
+
|
|
554
|
+
function showHelp() {
|
|
555
|
+
console.log(`
|
|
556
|
+
Wogi Flow - Background Sync Daemon
|
|
557
|
+
|
|
558
|
+
Keep workflow state in sync across branches and agents.
|
|
559
|
+
|
|
560
|
+
Usage:
|
|
561
|
+
flow sync-daemon start Start the daemon
|
|
562
|
+
flow sync-daemon stop Stop the daemon
|
|
563
|
+
flow sync-daemon status Check daemon status
|
|
564
|
+
flow sync-daemon restart Restart the daemon
|
|
565
|
+
|
|
566
|
+
Options:
|
|
567
|
+
--json Output as JSON
|
|
568
|
+
--help, -h Show this help
|
|
569
|
+
|
|
570
|
+
Configuration:
|
|
571
|
+
Add to .workflow/config.json:
|
|
572
|
+
{
|
|
573
|
+
"syncDaemon": {
|
|
574
|
+
"enabled": true,
|
|
575
|
+
"watchPaths": [".workflow/state/"],
|
|
576
|
+
"syncOnBranchSwitch": true,
|
|
577
|
+
"heartbeatIntervalMs": 30000
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
Features:
|
|
582
|
+
- Watches .workflow/state/ for file changes
|
|
583
|
+
- Detects branch switches and saves/restores state
|
|
584
|
+
- Invalidates caches on branch switch
|
|
585
|
+
- Logs activity to .workflow/state/sync-daemon.log
|
|
586
|
+
`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function main() {
|
|
590
|
+
const args = process.argv.slice(2);
|
|
591
|
+
const { flags, positional } = parseFlags(args);
|
|
592
|
+
|
|
593
|
+
// Check if running as daemon
|
|
594
|
+
if (flags.daemon || process.env.WOGI_DAEMON === '1') {
|
|
595
|
+
runDaemon();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (flags.help || flags.h) {
|
|
600
|
+
showHelp();
|
|
601
|
+
process.exit(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const command = positional[0] || 'status';
|
|
605
|
+
|
|
606
|
+
switch (command) {
|
|
607
|
+
case 'start':
|
|
608
|
+
startDaemon();
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'stop':
|
|
612
|
+
stopDaemon();
|
|
613
|
+
break;
|
|
614
|
+
|
|
615
|
+
case 'restart':
|
|
616
|
+
stopDaemon();
|
|
617
|
+
setTimeout(() => startDaemon(), 500);
|
|
618
|
+
break;
|
|
619
|
+
|
|
620
|
+
case 'status': {
|
|
621
|
+
const status = getDaemonStatus();
|
|
622
|
+
if (flags.json) {
|
|
623
|
+
console.log(JSON.stringify(status, null, 2));
|
|
624
|
+
} else {
|
|
625
|
+
printStatus(status);
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
default:
|
|
631
|
+
error(`Unknown command: ${command}`);
|
|
632
|
+
showHelp();
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ============================================================
|
|
638
|
+
// Exports
|
|
639
|
+
// ============================================================
|
|
640
|
+
|
|
641
|
+
module.exports = {
|
|
642
|
+
getSyncConfig,
|
|
643
|
+
isDaemonRunning,
|
|
644
|
+
getDaemonStatus,
|
|
645
|
+
startDaemon,
|
|
646
|
+
stopDaemon
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
if (require.main === module) {
|
|
650
|
+
main().catch(err => {
|
|
651
|
+
error(err.message);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
});
|
|
654
|
+
}
|