wogiflow 1.1.1 → 1.1.3
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/bridges/base-bridge.js +124 -4
- package/.workflow/bridges/claude-bridge.js +3 -5
- package/.workflow/bridges/codex-bridge.js +2 -2
- package/.workflow/bridges/cursor-bridge.js +2 -2
- package/.workflow/bridges/gemini-bridge.js +3 -3
- package/.workflow/bridges/index.js +36 -3
- package/.workflow/bridges/kimi-bridge.js +4 -4
- package/.workflow/bridges/opencode-bridge.js +2 -2
- package/lib/installer.js +76 -36
- package/package.json +1 -1
- package/scripts/flow-bridge-state.js +367 -0
- package/scripts/flow-bridge.js +75 -17
- package/scripts/hooks/entry/claude-code/session-start.js +22 -0
- package/scripts/hooks/entry/cursor/session-start.js +23 -0
- package/scripts/hooks/entry/gemini-cli/session-start.js +22 -0
- package/scripts/hooks/entry/opencode/session-start.js +23 -0
|
@@ -400,13 +400,112 @@ class BaseBridge {
|
|
|
400
400
|
|
|
401
401
|
// ==================== Template Utility Methods ====================
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Get the package root directory (where wogiflow is installed)
|
|
405
|
+
* @returns {string|null} Package root path or null if not found
|
|
406
|
+
*/
|
|
407
|
+
getPackageRoot() {
|
|
408
|
+
// Try to find the package by looking up from scripts directory
|
|
409
|
+
const possibleRoots = [
|
|
410
|
+
path.resolve(__dirname, '..', '..'), // From .workflow/bridges/ -> package root
|
|
411
|
+
path.resolve(__dirname, '..', '..', '..', 'node_modules', 'wogiflow'), // From project
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
for (const root of possibleRoots) {
|
|
415
|
+
const pkgPath = path.join(root, 'package.json');
|
|
416
|
+
if (fs.existsSync(pkgPath)) {
|
|
417
|
+
try {
|
|
418
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
419
|
+
if (pkg.name === 'wogiflow') {
|
|
420
|
+
return root;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// Continue to next option
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Check if a template file is outdated/stub
|
|
432
|
+
* @param {string} templatePath - Path to template file
|
|
433
|
+
* @returns {boolean} True if template appears to be outdated
|
|
434
|
+
*/
|
|
435
|
+
isTemplateOutdated(templatePath) {
|
|
436
|
+
if (!fs.existsSync(templatePath)) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
442
|
+
|
|
443
|
+
// Check for stub/placeholder markers
|
|
444
|
+
const stubMarkers = [
|
|
445
|
+
'Full implementation pending',
|
|
446
|
+
'stub template',
|
|
447
|
+
'not yet implemented',
|
|
448
|
+
'TODO: implement',
|
|
449
|
+
'placeholder template'
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
for (const marker of stubMarkers) {
|
|
453
|
+
if (content.toLowerCase().includes(marker.toLowerCase())) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Check if template is too short (likely incomplete)
|
|
459
|
+
if (content.length < 500) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return false;
|
|
464
|
+
} catch {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get template path, preferring package template if project template is outdated
|
|
471
|
+
* @param {string} templateName - Template filename (e.g., 'gemini-md.hbs')
|
|
472
|
+
* @returns {string|null} Path to best available template
|
|
473
|
+
*/
|
|
474
|
+
getBestTemplatePath(templateName) {
|
|
475
|
+
const projectTemplate = path.join(this.projectDir, this.workflowDir, 'templates', templateName);
|
|
476
|
+
|
|
477
|
+
// If project template exists and is not outdated, use it
|
|
478
|
+
if (!this.isTemplateOutdated(projectTemplate)) {
|
|
479
|
+
return projectTemplate;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Try to use package template instead
|
|
483
|
+
const packageRoot = this.getPackageRoot();
|
|
484
|
+
if (packageRoot) {
|
|
485
|
+
const packageTemplate = path.join(packageRoot, '.workflow', 'templates', templateName);
|
|
486
|
+
if (fs.existsSync(packageTemplate)) {
|
|
487
|
+
this.log(`Using package template for ${templateName} (project template outdated)`);
|
|
488
|
+
return packageTemplate;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Fall back to project template even if outdated
|
|
493
|
+
if (fs.existsSync(projectTemplate)) {
|
|
494
|
+
return projectTemplate;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
403
500
|
/**
|
|
404
501
|
* Load a partial template from .workflow/templates/partials/
|
|
502
|
+
* Checks both project and package directories
|
|
405
503
|
* @param {string} partialName - Name of the partial (without .hbs extension)
|
|
406
504
|
* @returns {string} Partial content or empty string if not found
|
|
407
505
|
*/
|
|
408
506
|
loadPartial(partialName) {
|
|
409
|
-
|
|
507
|
+
// Try project partials first
|
|
508
|
+
const projectPartialPath = path.join(
|
|
410
509
|
this.projectDir,
|
|
411
510
|
this.workflowDir,
|
|
412
511
|
'templates',
|
|
@@ -415,12 +514,33 @@ class BaseBridge {
|
|
|
415
514
|
);
|
|
416
515
|
|
|
417
516
|
try {
|
|
418
|
-
if (fs.existsSync(
|
|
419
|
-
return fs.readFileSync(
|
|
517
|
+
if (fs.existsSync(projectPartialPath)) {
|
|
518
|
+
return fs.readFileSync(projectPartialPath, 'utf-8');
|
|
420
519
|
}
|
|
421
520
|
} catch (err) {
|
|
422
|
-
this.log(`Warning: Could not load partial ${partialName}: ${err.message}`);
|
|
521
|
+
this.log(`Warning: Could not load project partial ${partialName}: ${err.message}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Try package partials as fallback
|
|
525
|
+
const packageRoot = this.getPackageRoot();
|
|
526
|
+
if (packageRoot) {
|
|
527
|
+
const packagePartialPath = path.join(
|
|
528
|
+
packageRoot,
|
|
529
|
+
'.workflow',
|
|
530
|
+
'templates',
|
|
531
|
+
'partials',
|
|
532
|
+
`${partialName}.hbs`
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
if (fs.existsSync(packagePartialPath)) {
|
|
537
|
+
return fs.readFileSync(packagePartialPath, 'utf-8');
|
|
538
|
+
}
|
|
539
|
+
} catch (err) {
|
|
540
|
+
this.log(`Warning: Could not load package partial ${partialName}: ${err.message}`);
|
|
541
|
+
}
|
|
423
542
|
}
|
|
543
|
+
|
|
424
544
|
return '';
|
|
425
545
|
}
|
|
426
546
|
|
|
@@ -45,11 +45,9 @@ class ClaudeBridge extends BaseBridge {
|
|
|
45
45
|
* @returns {string} Generated CLAUDE.md content
|
|
46
46
|
*/
|
|
47
47
|
generateRulesContent(config) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const templatePath = path.join(this.projectDir, this.workflowDir, 'templates', 'claude-md.hbs');
|
|
52
|
-
if (fs.existsSync(templatePath)) {
|
|
48
|
+
// Use getBestTemplatePath to find the best template (prefers package over outdated project)
|
|
49
|
+
const templatePath = this.getBestTemplatePath('claude-md.hbs');
|
|
50
|
+
if (templatePath) {
|
|
53
51
|
return this.generateFromTemplate(templatePath, config);
|
|
54
52
|
}
|
|
55
53
|
|
|
@@ -81,8 +81,8 @@ class CodexBridge extends BaseBridge {
|
|
|
81
81
|
this.registerPartials();
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
|
-
const templatePath =
|
|
85
|
-
if (
|
|
84
|
+
const templatePath = this.getBestTemplatePath('agents-md.hbs');
|
|
85
|
+
if (templatePath) {
|
|
86
86
|
const templateSource = fs.readFileSync(templatePath, 'utf-8');
|
|
87
87
|
const template = Handlebars.compile(templateSource);
|
|
88
88
|
return template(context);
|
|
@@ -88,8 +88,8 @@ class CursorBridge extends BaseBridge {
|
|
|
88
88
|
// Register partials before compiling
|
|
89
89
|
this.registerPartials();
|
|
90
90
|
|
|
91
|
-
const templatePath =
|
|
92
|
-
if (
|
|
91
|
+
const templatePath = this.getBestTemplatePath('cursor-rules.mdc.hbs');
|
|
92
|
+
if (templatePath) {
|
|
93
93
|
try {
|
|
94
94
|
const templateSource = fs.readFileSync(templatePath, 'utf-8');
|
|
95
95
|
const template = Handlebars.compile(templateSource);
|
|
@@ -58,9 +58,9 @@ class GeminiBridge extends BaseBridge {
|
|
|
58
58
|
* @returns {string} Generated GEMINI.md content
|
|
59
59
|
*/
|
|
60
60
|
generateRulesContent(config) {
|
|
61
|
-
//
|
|
62
|
-
const templatePath =
|
|
63
|
-
if (
|
|
61
|
+
// Use getBestTemplatePath to find the best template (prefers package over outdated project)
|
|
62
|
+
const templatePath = this.getBestTemplatePath('gemini-md.hbs');
|
|
63
|
+
if (templatePath) {
|
|
64
64
|
return this.generateFromTemplate(templatePath, config);
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -92,23 +92,53 @@ function getCliType(projectDir = process.cwd()) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Detect which CLI is currently running based on environment
|
|
97
|
+
* @param {string} projectDir - Project root directory
|
|
98
|
+
* @returns {string} Detected CLI type
|
|
99
|
+
*/
|
|
100
|
+
function detectRunningCli(projectDir = process.cwd()) {
|
|
101
|
+
// Priority 1: Environment variables set by CLI tools
|
|
102
|
+
if (process.env.CLAUDE_CODE_ENTRY_POINT) return 'claude-code';
|
|
103
|
+
if (process.env.CURSOR_SESSION_ID) return 'cursor';
|
|
104
|
+
if (process.env.OPENCODE_SESSION) return 'opencode';
|
|
105
|
+
|
|
106
|
+
// Priority 2: Check caller stack for hook path hints
|
|
107
|
+
try {
|
|
108
|
+
const stack = new Error().stack || '';
|
|
109
|
+
if (stack.includes('/claude-code/')) return 'claude-code';
|
|
110
|
+
if (stack.includes('/gemini-cli/')) return 'gemini-cli';
|
|
111
|
+
if (stack.includes('/cursor/')) return 'cursor';
|
|
112
|
+
if (stack.includes('/opencode/')) return 'opencode';
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore stack parsing errors
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Priority 3: Config file setting
|
|
118
|
+
return getCliType(projectDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/**
|
|
96
122
|
* Get the bridge instance for the current CLI type
|
|
97
123
|
* @param {Object} options - Options to pass to bridge constructor
|
|
98
124
|
* @param {string} options.projectDir - Project root directory
|
|
125
|
+
* @param {string} options.cliType - Override CLI type (optional)
|
|
99
126
|
* @param {boolean} options.verbose - Enable verbose logging
|
|
100
127
|
* @returns {BaseBridge} Bridge instance
|
|
101
128
|
*/
|
|
102
129
|
function getBridge(options = {}) {
|
|
103
130
|
const projectDir = options.projectDir || process.cwd();
|
|
104
|
-
|
|
131
|
+
// Allow explicit CLI type override, otherwise detect from config
|
|
132
|
+
const cliType = options.cliType || getCliType(projectDir);
|
|
105
133
|
|
|
106
134
|
loadBridges();
|
|
107
135
|
|
|
108
136
|
const BridgeLoader = bridges[cliType];
|
|
109
137
|
if (!BridgeLoader) {
|
|
110
138
|
// If no specific bridge exists, return null (manual mode)
|
|
111
|
-
|
|
139
|
+
if (options.verbose) {
|
|
140
|
+
console.warn(`No bridge available for CLI type: ${cliType}`);
|
|
141
|
+
}
|
|
112
142
|
return null;
|
|
113
143
|
}
|
|
114
144
|
|
|
@@ -122,16 +152,18 @@ function getBridge(options = {}) {
|
|
|
122
152
|
/**
|
|
123
153
|
* Sync the current CLI bridge
|
|
124
154
|
* @param {Object} options - Options
|
|
155
|
+
* @param {string} options.cliType - Override CLI type (optional)
|
|
125
156
|
* @returns {Object} Sync result
|
|
126
157
|
*/
|
|
127
158
|
async function syncBridge(options = {}) {
|
|
128
159
|
const bridge = getBridge(options);
|
|
160
|
+
const cliType = options.cliType || getCliType(options.projectDir);
|
|
129
161
|
|
|
130
162
|
if (!bridge) {
|
|
131
163
|
return {
|
|
132
164
|
success: false,
|
|
133
165
|
error: 'No bridge available for current CLI type',
|
|
134
|
-
cliType
|
|
166
|
+
cliType
|
|
135
167
|
};
|
|
136
168
|
}
|
|
137
169
|
|
|
@@ -161,6 +193,7 @@ module.exports = {
|
|
|
161
193
|
getBridge,
|
|
162
194
|
syncBridge,
|
|
163
195
|
getCliType,
|
|
196
|
+
detectRunningCli,
|
|
164
197
|
listAvailableBridges,
|
|
165
198
|
isBridgeAvailable,
|
|
166
199
|
BaseBridge: require('./base-bridge')
|
|
@@ -88,11 +88,11 @@ class KimiBridge extends BaseBridge {
|
|
|
88
88
|
|
|
89
89
|
try {
|
|
90
90
|
// Try kimi-specific template first, fall back to generic agents-md.hbs
|
|
91
|
-
let templatePath =
|
|
92
|
-
if (!
|
|
93
|
-
templatePath =
|
|
91
|
+
let templatePath = this.getBestTemplatePath('kimi-agents-md.hbs');
|
|
92
|
+
if (!templatePath) {
|
|
93
|
+
templatePath = this.getBestTemplatePath('agents-md.hbs');
|
|
94
94
|
}
|
|
95
|
-
if (
|
|
95
|
+
if (templatePath) {
|
|
96
96
|
let templateSource;
|
|
97
97
|
try {
|
|
98
98
|
templateSource = fs.readFileSync(templatePath, 'utf-8');
|
|
@@ -84,8 +84,8 @@ class OpenCodeBridge extends BaseBridge {
|
|
|
84
84
|
this.registerPartials();
|
|
85
85
|
|
|
86
86
|
try {
|
|
87
|
-
const templatePath =
|
|
88
|
-
if (
|
|
87
|
+
const templatePath = this.getBestTemplatePath('opencode-agents-md.hbs');
|
|
88
|
+
if (templatePath) {
|
|
89
89
|
const templateSource = fs.readFileSync(templatePath, 'utf-8');
|
|
90
90
|
const template = Handlebars.compile(templateSource);
|
|
91
91
|
return template(context);
|
package/lib/installer.js
CHANGED
|
@@ -322,6 +322,31 @@ function createWorkflowStructure(projectRoot, config) {
|
|
|
322
322
|
console.log(' Created .workflow/ directory structure');
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* CLI-specific resource mappings
|
|
327
|
+
* Maps CLI keys to their package source directories and output file names
|
|
328
|
+
*/
|
|
329
|
+
const CLI_RESOURCES = {
|
|
330
|
+
claude: {
|
|
331
|
+
packageDir: '.claude',
|
|
332
|
+
rulesFile: 'CLAUDE.md',
|
|
333
|
+
templateName: 'claude-md.hbs',
|
|
334
|
+
subdirs: ['commands', 'docs', 'rules', 'skills']
|
|
335
|
+
},
|
|
336
|
+
gemini: {
|
|
337
|
+
packageDir: '.claude', // Share Claude's resources as base, bridge customizes output
|
|
338
|
+
rulesFile: 'GEMINI.md',
|
|
339
|
+
templateName: 'gemini-md.hbs',
|
|
340
|
+
subdirs: ['commands', 'docs', 'rules', 'skills']
|
|
341
|
+
},
|
|
342
|
+
opencode: {
|
|
343
|
+
packageDir: '.claude',
|
|
344
|
+
rulesFile: '.opencode/agents.md',
|
|
345
|
+
templateName: 'opencode-agents-md.hbs',
|
|
346
|
+
subdirs: ['docs', 'skills']
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
325
350
|
/**
|
|
326
351
|
* Create CLI-specific configuration
|
|
327
352
|
* @param {string} projectRoot - Project root directory
|
|
@@ -338,45 +363,48 @@ function createCLIConfig(projectRoot, cliKey, config) {
|
|
|
338
363
|
const cliDir = path.join(projectRoot, cli.dir);
|
|
339
364
|
fs.mkdirSync(cliDir, { recursive: true });
|
|
340
365
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
console.log(' Copied .claude/commands/ (slash commands)');
|
|
349
|
-
}
|
|
366
|
+
// Get CLI-specific resource configuration
|
|
367
|
+
const resources = CLI_RESOURCES[cliKey];
|
|
368
|
+
if (!resources) {
|
|
369
|
+
console.log(` ${cli.name} will be configured via bridge sync`);
|
|
370
|
+
console.log(` Configured ${cli.name}`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
350
373
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
374
|
+
// Copy common subdirectories (commands, docs, rules, skills)
|
|
375
|
+
const packageCliDir = path.join(PACKAGE_ROOT, resources.packageDir);
|
|
376
|
+
for (const subdir of resources.subdirs) {
|
|
377
|
+
const packageSubdir = path.join(packageCliDir, subdir);
|
|
378
|
+
const projectSubdir = path.join(cliDir, subdir);
|
|
379
|
+
if (fs.existsSync(packageSubdir)) {
|
|
380
|
+
copyDir(packageSubdir, projectSubdir);
|
|
381
|
+
console.log(` Copied ${cli.dir}/${subdir}/`);
|
|
357
382
|
}
|
|
383
|
+
}
|
|
358
384
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
385
|
+
// Generate the rules/instructions file (CLAUDE.md, GEMINI.md, etc.)
|
|
386
|
+
// First try to use the bridge for proper template rendering
|
|
387
|
+
try {
|
|
388
|
+
const bridgesPath = path.join(projectRoot, '.workflow', 'bridges');
|
|
389
|
+
if (fs.existsSync(bridgesPath)) {
|
|
390
|
+
const bridges = require(bridgesPath);
|
|
391
|
+
const bridge = bridges.getBridge({ projectDir: projectRoot, cliType: cliKey === 'claude' ? 'claude-code' : cliKey === 'gemini' ? 'gemini-cli' : cliKey, verbose: false });
|
|
392
|
+
if (bridge) {
|
|
393
|
+
bridge.generateRulesFile();
|
|
394
|
+
console.log(` Created ${resources.rulesFile} (via bridge)`);
|
|
395
|
+
console.log(` Configured ${cli.name}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
365
398
|
}
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (fs.existsSync(packageSkills)) {
|
|
371
|
-
copyDir(packageSkills, projectSkills);
|
|
372
|
-
console.log(' Copied .claude/skills/ (base skills)');
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// Bridge not available yet - fall through to simple generation
|
|
401
|
+
if (process.env.DEBUG) {
|
|
402
|
+
console.log(` Bridge not available: ${err.message}`);
|
|
373
403
|
}
|
|
404
|
+
}
|
|
374
405
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (fs.existsSync(claudeMdTemplate)) {
|
|
378
|
-
// For now, create a simple CLAUDE.md - the bridge will regenerate with full template
|
|
379
|
-
const claudeMd = `# Project Instructions
|
|
406
|
+
// Fallback: Create a simple rules file - the bridge will regenerate with full template
|
|
407
|
+
const simpleContent = `# Project Instructions
|
|
380
408
|
|
|
381
409
|
You are an AI development assistant using the WogiFlow methodology v1.0.
|
|
382
410
|
|
|
@@ -394,13 +422,25 @@ cat .workflow/state/ready.json # Check tasks
|
|
|
394
422
|
- \`/wogi-status\` - Project overview
|
|
395
423
|
- \`/wogi-health\` - Check workflow health
|
|
396
424
|
|
|
425
|
+
Run \`flow bridge sync\` to regenerate this file with full template.
|
|
426
|
+
|
|
397
427
|
Generated by Wogi Flow v${config.version}
|
|
398
428
|
`;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
429
|
+
|
|
430
|
+
// Determine output path (handle nested paths like .opencode/agents.md)
|
|
431
|
+
const rulesFilePath = resources.rulesFile.includes('/')
|
|
432
|
+
? path.join(projectRoot, resources.rulesFile)
|
|
433
|
+
: path.join(projectRoot, resources.rulesFile);
|
|
434
|
+
|
|
435
|
+
// Ensure parent directory exists for nested paths
|
|
436
|
+
const rulesFileDir = path.dirname(rulesFilePath);
|
|
437
|
+
if (!fs.existsSync(rulesFileDir)) {
|
|
438
|
+
fs.mkdirSync(rulesFileDir, { recursive: true });
|
|
402
439
|
}
|
|
403
440
|
|
|
441
|
+
fs.writeFileSync(rulesFilePath, simpleContent);
|
|
442
|
+
console.log(` Created ${resources.rulesFile}`);
|
|
443
|
+
|
|
404
444
|
console.log(` Configured ${cli.name}`);
|
|
405
445
|
}
|
|
406
446
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Bridge State Tracker
|
|
5
|
+
*
|
|
6
|
+
* Tracks CLI bridge sync state and provides auto-sync functionality.
|
|
7
|
+
* Enables seamless generation of CLI instruction files on session start.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { autoSyncBridge, needsSync } = require('./flow-bridge-state');
|
|
11
|
+
*
|
|
12
|
+
* // Auto-sync on session start (non-blocking)
|
|
13
|
+
* await autoSyncBridge('claude-code', { silent: true });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
// Project paths
|
|
21
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
|
|
23
|
+
const STATE_DIR = path.join(WORKFLOW_DIR, 'state');
|
|
24
|
+
const CONFIG_PATH = path.join(WORKFLOW_DIR, 'config.json');
|
|
25
|
+
const SYNC_STATE_PATH = path.join(STATE_DIR, 'bridge-sync.json');
|
|
26
|
+
|
|
27
|
+
// CLI type to output file mapping
|
|
28
|
+
const CLI_OUTPUT_FILES = {
|
|
29
|
+
'claude-code': 'CLAUDE.md',
|
|
30
|
+
'gemini-cli': 'GEMINI.md',
|
|
31
|
+
'cursor': '.cursor/rules/wogi-flow.mdc',
|
|
32
|
+
'opencode': '.opencode/agents.md',
|
|
33
|
+
'codex': 'AGENTS.md',
|
|
34
|
+
'kimi': 'KIMI.md'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safe JSON parse with prototype pollution protection
|
|
39
|
+
* @param {string} filePath - Path to JSON file
|
|
40
|
+
* @param {*} defaultValue - Default value if parsing fails
|
|
41
|
+
* @returns {*} Parsed object or default value
|
|
42
|
+
*/
|
|
43
|
+
function safeJsonParse(filePath, defaultValue = {}) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
46
|
+
const parsed = JSON.parse(content);
|
|
47
|
+
|
|
48
|
+
// Check for prototype pollution keys
|
|
49
|
+
const checkDangerous = (obj, depth = 0) => {
|
|
50
|
+
if (depth > 10 || !obj || typeof obj !== 'object') return false;
|
|
51
|
+
const dangerous = ['__proto__', 'constructor', 'prototype'];
|
|
52
|
+
for (const key of Object.keys(obj)) {
|
|
53
|
+
if (dangerous.includes(key)) return true;
|
|
54
|
+
if (obj[key] && typeof obj[key] === 'object') {
|
|
55
|
+
if (checkDangerous(obj[key], depth + 1)) return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (checkDangerous(parsed)) {
|
|
62
|
+
return defaultValue;
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
} catch {
|
|
66
|
+
return defaultValue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Calculate MD5 hash of config.json for staleness detection
|
|
72
|
+
* @returns {string} Hash of config content
|
|
73
|
+
*/
|
|
74
|
+
function getConfigChecksum() {
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
77
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
78
|
+
} catch {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the output file path for a CLI type
|
|
85
|
+
* @param {string} cliType - CLI type
|
|
86
|
+
* @returns {string} Full path to output file
|
|
87
|
+
*/
|
|
88
|
+
function getOutputFilePath(cliType) {
|
|
89
|
+
const filename = CLI_OUTPUT_FILES[cliType];
|
|
90
|
+
if (!filename) return null;
|
|
91
|
+
return path.join(PROJECT_ROOT, filename);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read current sync state
|
|
96
|
+
* @returns {Object} Sync state
|
|
97
|
+
*/
|
|
98
|
+
function readSyncState() {
|
|
99
|
+
return safeJsonParse(SYNC_STATE_PATH, { syncs: {}, version: 1 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Write sync state
|
|
104
|
+
* @param {Object} state - State to write
|
|
105
|
+
*/
|
|
106
|
+
function writeSyncState(state) {
|
|
107
|
+
try {
|
|
108
|
+
// Ensure state directory exists
|
|
109
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
110
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (process.env.DEBUG) {
|
|
115
|
+
console.error(`[bridge-state] Failed to write sync state: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get last sync time for a CLI type
|
|
122
|
+
* @param {string} cliType - CLI type
|
|
123
|
+
* @returns {string|null} ISO timestamp or null
|
|
124
|
+
*/
|
|
125
|
+
function getLastSyncTime(cliType) {
|
|
126
|
+
const state = readSyncState();
|
|
127
|
+
return state.syncs?.[cliType]?.lastSync || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Update sync time for a CLI type
|
|
132
|
+
* @param {string} cliType - CLI type
|
|
133
|
+
* @param {string} configHash - Current config hash
|
|
134
|
+
*/
|
|
135
|
+
function setLastSyncTime(cliType, configHash) {
|
|
136
|
+
const state = readSyncState();
|
|
137
|
+
if (!state.syncs) state.syncs = {};
|
|
138
|
+
state.syncs[cliType] = {
|
|
139
|
+
lastSync: new Date().toISOString(),
|
|
140
|
+
configHash
|
|
141
|
+
};
|
|
142
|
+
writeSyncState(state);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a CLI bridge needs to be synced
|
|
147
|
+
* @param {string} cliType - CLI type to check
|
|
148
|
+
* @returns {Object} { needsSync: boolean, reason: string }
|
|
149
|
+
*/
|
|
150
|
+
function needsSync(cliType) {
|
|
151
|
+
// Check if output file exists
|
|
152
|
+
const outputPath = getOutputFilePath(cliType);
|
|
153
|
+
if (!outputPath) {
|
|
154
|
+
return { needsSync: false, reason: 'unknown-cli' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(outputPath)) {
|
|
158
|
+
return { needsSync: true, reason: 'file-missing' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if config has changed since last sync
|
|
162
|
+
const state = readSyncState();
|
|
163
|
+
const cliState = state.syncs?.[cliType];
|
|
164
|
+
|
|
165
|
+
if (!cliState) {
|
|
166
|
+
return { needsSync: true, reason: 'never-synced' };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const currentHash = getConfigChecksum();
|
|
170
|
+
if (cliState.configHash !== currentHash) {
|
|
171
|
+
return { needsSync: true, reason: 'config-changed' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { needsSync: false, reason: 'up-to-date' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Auto-sync a CLI bridge if needed
|
|
179
|
+
* @param {string} cliType - CLI type to sync
|
|
180
|
+
* @param {Object} options - Options
|
|
181
|
+
* @param {boolean} options.silent - Suppress output
|
|
182
|
+
* @param {boolean} options.force - Force sync even if up-to-date
|
|
183
|
+
* @returns {Object} { synced: boolean, reason: string }
|
|
184
|
+
*/
|
|
185
|
+
async function autoSyncBridge(cliType, options = {}) {
|
|
186
|
+
const { silent = false, force = false } = options;
|
|
187
|
+
|
|
188
|
+
// Check if sync is needed
|
|
189
|
+
if (!force) {
|
|
190
|
+
const check = needsSync(cliType);
|
|
191
|
+
if (!check.needsSync) {
|
|
192
|
+
if (!silent && process.env.DEBUG) {
|
|
193
|
+
console.error(`[bridge-state] ${cliType}: ${check.reason}, skipping sync`);
|
|
194
|
+
}
|
|
195
|
+
return { synced: false, reason: check.reason };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Load bridges module
|
|
200
|
+
let bridges;
|
|
201
|
+
try {
|
|
202
|
+
bridges = require(path.join(PROJECT_ROOT, '.workflow', 'bridges'));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (process.env.DEBUG) {
|
|
205
|
+
console.error(`[bridge-state] Failed to load bridges: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
return { synced: false, reason: 'bridges-unavailable', error: err.message };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get bridge for the specified CLI type
|
|
211
|
+
let bridge;
|
|
212
|
+
try {
|
|
213
|
+
// Pass explicit cliType to override config default
|
|
214
|
+
bridge = bridges.getBridge({
|
|
215
|
+
projectDir: PROJECT_ROOT,
|
|
216
|
+
cliType: cliType,
|
|
217
|
+
verbose: !silent
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Fallback: Try loading the specific bridge directly
|
|
221
|
+
if (!bridge) {
|
|
222
|
+
const BridgeClass = require(path.join(PROJECT_ROOT, '.workflow', 'bridges', `${cliType}-bridge`));
|
|
223
|
+
bridge = new BridgeClass({
|
|
224
|
+
projectDir: PROJECT_ROOT,
|
|
225
|
+
verbose: !silent
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (process.env.DEBUG) {
|
|
230
|
+
console.error(`[bridge-state] Failed to get bridge for ${cliType}: ${err.message}`);
|
|
231
|
+
}
|
|
232
|
+
return { synced: false, reason: 'bridge-load-failed', error: err.message };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Run sync
|
|
236
|
+
try {
|
|
237
|
+
await bridge.sync();
|
|
238
|
+
|
|
239
|
+
// Update state
|
|
240
|
+
const configHash = getConfigChecksum();
|
|
241
|
+
setLastSyncTime(cliType, configHash);
|
|
242
|
+
|
|
243
|
+
if (!silent) {
|
|
244
|
+
console.error(`[bridge-state] Synced ${cliType} bridge`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { synced: true, reason: 'success' };
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (process.env.DEBUG) {
|
|
250
|
+
console.error(`[bridge-state] Sync failed for ${cliType}: ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
return { synced: false, reason: 'sync-failed', error: err.message };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Sync all enabled CLIs
|
|
258
|
+
* @param {Object} options - Options
|
|
259
|
+
* @returns {Object} Results for each CLI
|
|
260
|
+
*/
|
|
261
|
+
async function syncAllEnabledClis(options = {}) {
|
|
262
|
+
const config = safeJsonParse(CONFIG_PATH, {});
|
|
263
|
+
const primaryCli = config.cli?.type || 'claude-code';
|
|
264
|
+
const enabledClis = config.cli?.enabled || [primaryCli];
|
|
265
|
+
|
|
266
|
+
const results = {};
|
|
267
|
+
for (const cliType of enabledClis) {
|
|
268
|
+
results[cliType] = await autoSyncBridge(cliType, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Detect which CLI is currently running
|
|
276
|
+
* Based on environment variables and caller context
|
|
277
|
+
* @returns {string} CLI type
|
|
278
|
+
*/
|
|
279
|
+
function detectRunningCli() {
|
|
280
|
+
// Priority 1: Environment variables
|
|
281
|
+
if (process.env.CLAUDE_CODE_ENTRY_POINT) return 'claude-code';
|
|
282
|
+
if (process.env.GEMINI_API_KEY && !process.env.CLAUDE_CODE_ENTRY_POINT) return 'gemini-cli';
|
|
283
|
+
if (process.env.CURSOR_SESSION_ID) return 'cursor';
|
|
284
|
+
if (process.env.OPENCODE_SESSION) return 'opencode';
|
|
285
|
+
|
|
286
|
+
// Priority 2: Check caller stack for hook path hints
|
|
287
|
+
try {
|
|
288
|
+
const stack = new Error().stack || '';
|
|
289
|
+
if (stack.includes('/claude-code/')) return 'claude-code';
|
|
290
|
+
if (stack.includes('/gemini-cli/')) return 'gemini-cli';
|
|
291
|
+
if (stack.includes('/cursor/')) return 'cursor';
|
|
292
|
+
if (stack.includes('/opencode/')) return 'opencode';
|
|
293
|
+
} catch {
|
|
294
|
+
// Ignore stack parsing errors
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Priority 3: Config file setting
|
|
298
|
+
const config = safeJsonParse(CONFIG_PATH, {});
|
|
299
|
+
if (config.cli?.type) return config.cli.type;
|
|
300
|
+
|
|
301
|
+
// Default
|
|
302
|
+
return 'claude-code';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// CLI interface
|
|
306
|
+
if (require.main === module) {
|
|
307
|
+
const args = process.argv.slice(2);
|
|
308
|
+
const command = args[0];
|
|
309
|
+
|
|
310
|
+
const run = async () => {
|
|
311
|
+
switch (command) {
|
|
312
|
+
case 'check': {
|
|
313
|
+
const cliType = args[1] || detectRunningCli();
|
|
314
|
+
const result = needsSync(cliType);
|
|
315
|
+
console.log(JSON.stringify({ cliType, ...result }, null, 2));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'sync': {
|
|
320
|
+
const cliType = args[1] || detectRunningCli();
|
|
321
|
+
const result = await autoSyncBridge(cliType, { silent: false, force: args.includes('--force') });
|
|
322
|
+
console.log(JSON.stringify({ cliType, ...result }, null, 2));
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'sync-all': {
|
|
327
|
+
const results = await syncAllEnabledClis({ silent: false, force: args.includes('--force') });
|
|
328
|
+
console.log(JSON.stringify(results, null, 2));
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'detect': {
|
|
333
|
+
const cliType = detectRunningCli();
|
|
334
|
+
console.log(cliType);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
console.log('Usage: flow-bridge-state <command> [options]');
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log('Commands:');
|
|
342
|
+
console.log(' check [cli-type] Check if sync is needed');
|
|
343
|
+
console.log(' sync [cli-type] Sync a CLI bridge');
|
|
344
|
+
console.log(' sync-all Sync all enabled CLIs');
|
|
345
|
+
console.log(' detect Detect running CLI type');
|
|
346
|
+
console.log('');
|
|
347
|
+
console.log('Options:');
|
|
348
|
+
console.log(' --force Force sync even if up-to-date');
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
run().catch(err => {
|
|
353
|
+
console.error(`Error: ${err.message}`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
needsSync,
|
|
360
|
+
autoSyncBridge,
|
|
361
|
+
syncAllEnabledClis,
|
|
362
|
+
detectRunningCli,
|
|
363
|
+
getConfigChecksum,
|
|
364
|
+
getLastSyncTime,
|
|
365
|
+
setLastSyncTime,
|
|
366
|
+
CLI_OUTPUT_FILES
|
|
367
|
+
};
|
package/scripts/flow-bridge.js
CHANGED
|
@@ -62,30 +62,44 @@ function listBridges() {
|
|
|
62
62
|
{
|
|
63
63
|
id: 'claude-code',
|
|
64
64
|
name: 'Claude Code',
|
|
65
|
-
status: '
|
|
65
|
+
status: 'full',
|
|
66
66
|
folder: '.claude',
|
|
67
67
|
rulesFile: 'CLAUDE.md'
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
id: 'gemini-cli',
|
|
71
71
|
name: 'Gemini CLI',
|
|
72
|
-
status: '
|
|
72
|
+
status: 'full',
|
|
73
73
|
folder: '.gemini',
|
|
74
74
|
rulesFile: 'GEMINI.md'
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
id: 'cursor',
|
|
78
|
+
name: 'Cursor',
|
|
79
|
+
status: 'full',
|
|
80
|
+
folder: '.cursor',
|
|
81
|
+
rulesFile: '.cursor/rules/wogi-flow.mdc'
|
|
82
|
+
},
|
|
76
83
|
{
|
|
77
84
|
id: 'opencode',
|
|
78
85
|
name: 'OpenCode',
|
|
79
|
-
status: '
|
|
86
|
+
status: 'full',
|
|
80
87
|
folder: '.opencode',
|
|
81
|
-
rulesFile: '
|
|
88
|
+
rulesFile: '.opencode/agents.md'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'codex',
|
|
92
|
+
name: 'Codex CLI',
|
|
93
|
+
status: 'soft',
|
|
94
|
+
folder: '.codex',
|
|
95
|
+
rulesFile: 'AGENTS.md'
|
|
82
96
|
},
|
|
83
97
|
{
|
|
84
|
-
id: '
|
|
85
|
-
name: '
|
|
86
|
-
status: '
|
|
87
|
-
folder: '
|
|
88
|
-
rulesFile: '
|
|
98
|
+
id: 'kimi',
|
|
99
|
+
name: 'Kimi CLI',
|
|
100
|
+
status: 'soft',
|
|
101
|
+
folder: '.kimi',
|
|
102
|
+
rulesFile: 'KIMI.md'
|
|
89
103
|
}
|
|
90
104
|
];
|
|
91
105
|
|
|
@@ -93,12 +107,14 @@ function listBridges() {
|
|
|
93
107
|
|
|
94
108
|
for (const bridge of availableBridges) {
|
|
95
109
|
const isCurrent = bridge.id === currentCli;
|
|
96
|
-
const statusColor = bridge.status === '
|
|
97
|
-
bridge.status === '
|
|
110
|
+
const statusColor = bridge.status === 'full' ? colors.green :
|
|
111
|
+
bridge.status === 'soft' ? colors.yellow : colors.cyan;
|
|
112
|
+
const statusLabel = bridge.status === 'full' ? 'full parity (hooks)' :
|
|
113
|
+
bridge.status === 'soft' ? 'soft parity (rules only)' : bridge.status;
|
|
98
114
|
const indicator = isCurrent ? `${colors.green}→${colors.reset}` : ' ';
|
|
99
115
|
|
|
100
116
|
console.log(` ${indicator} ${colors.bold}${bridge.name}${colors.reset} (${bridge.id})`);
|
|
101
|
-
console.log(` Status: ${statusColor}${
|
|
117
|
+
console.log(` Status: ${statusColor}${statusLabel}${colors.reset}`);
|
|
102
118
|
console.log(` Folder: ${bridge.folder}`);
|
|
103
119
|
console.log(` Rules: ${bridge.rulesFile}`);
|
|
104
120
|
console.log('');
|
|
@@ -150,12 +166,43 @@ function showStatus() {
|
|
|
150
166
|
console.log('');
|
|
151
167
|
}
|
|
152
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Normalize CLI type argument to standard format
|
|
171
|
+
*/
|
|
172
|
+
function normalizeCliType(input) {
|
|
173
|
+
if (!input) return null;
|
|
174
|
+
const normalized = input.toLowerCase().trim();
|
|
175
|
+
const aliases = {
|
|
176
|
+
'gemini': 'gemini-cli',
|
|
177
|
+
'gemini-cli': 'gemini-cli',
|
|
178
|
+
'claude': 'claude-code',
|
|
179
|
+
'claude-code': 'claude-code',
|
|
180
|
+
'opencode': 'opencode',
|
|
181
|
+
'cursor': 'cursor',
|
|
182
|
+
'codex': 'codex',
|
|
183
|
+
'kimi': 'kimi'
|
|
184
|
+
};
|
|
185
|
+
return aliases[normalized] || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
153
188
|
/**
|
|
154
189
|
* Sync bridge
|
|
155
190
|
*/
|
|
156
191
|
async function syncBridge(options = {}) {
|
|
157
192
|
const verbose = options.verbose || process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
158
193
|
|
|
194
|
+
// Check for CLI type argument (e.g., "flow bridge sync gemini")
|
|
195
|
+
const cliTypeArg = process.argv[3];
|
|
196
|
+
const requestedCliType = normalizeCliType(cliTypeArg);
|
|
197
|
+
|
|
198
|
+
if (cliTypeArg && !requestedCliType) {
|
|
199
|
+
console.error(`${colors.red}Error:${colors.reset} Unknown CLI type: ${cliTypeArg}`);
|
|
200
|
+
console.error('Available types: claude-code, gemini-cli, cursor, opencode, codex, kimi');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const targetCliType = requestedCliType || getCliType();
|
|
205
|
+
|
|
159
206
|
console.log(`${colors.cyan}Syncing CLI bridge...${colors.reset}`);
|
|
160
207
|
console.log('');
|
|
161
208
|
|
|
@@ -170,7 +217,11 @@ async function syncBridge(options = {}) {
|
|
|
170
217
|
process.exit(1);
|
|
171
218
|
}
|
|
172
219
|
|
|
173
|
-
const result = await bridges.syncBridge({
|
|
220
|
+
const result = await bridges.syncBridge({
|
|
221
|
+
verbose,
|
|
222
|
+
projectDir: PROJECT_ROOT,
|
|
223
|
+
cliType: targetCliType
|
|
224
|
+
});
|
|
174
225
|
|
|
175
226
|
if (result.success) {
|
|
176
227
|
console.log(`${colors.green}✓ Bridge sync complete${colors.reset}`);
|
|
@@ -213,11 +264,18 @@ switch (command) {
|
|
|
213
264
|
listBridges();
|
|
214
265
|
break;
|
|
215
266
|
default:
|
|
216
|
-
console.log('Usage: flow bridge [sync|status|list]');
|
|
267
|
+
console.log('Usage: flow bridge [sync|status|list] [cli-type]');
|
|
217
268
|
console.log('');
|
|
218
269
|
console.log('Commands:');
|
|
219
|
-
console.log(' sync
|
|
220
|
-
console.log(' status
|
|
221
|
-
console.log(' list
|
|
270
|
+
console.log(' sync [cli-type] Sync .workflow/ config to CLI-specific folder');
|
|
271
|
+
console.log(' status Show current bridge configuration');
|
|
272
|
+
console.log(' list List available CLI bridges');
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log('CLI Types:');
|
|
275
|
+
console.log(' claude-code, gemini-cli (or gemini), cursor, opencode, codex, kimi');
|
|
276
|
+
console.log('');
|
|
277
|
+
console.log('Examples:');
|
|
278
|
+
console.log(' flow bridge sync # Sync default CLI from config');
|
|
279
|
+
console.log(' flow bridge sync gemini # Sync Gemini CLI specifically');
|
|
222
280
|
process.exit(1);
|
|
223
281
|
}
|
|
@@ -11,8 +11,30 @@ const { gatherSessionContext } = require('../../core/session-context');
|
|
|
11
11
|
const { claudeCodeAdapter } = require('../../adapters/claude-code');
|
|
12
12
|
const { setCliSessionId, clearStaleCurrentTaskAsync } = require('../../../flow-session-state');
|
|
13
13
|
|
|
14
|
+
// Lazy-load bridge state to avoid circular dependencies
|
|
15
|
+
let autoSyncBridge = null;
|
|
16
|
+
function getAutoSyncBridge() {
|
|
17
|
+
if (!autoSyncBridge) {
|
|
18
|
+
try {
|
|
19
|
+
autoSyncBridge = require('../../../flow-bridge-state').autoSyncBridge;
|
|
20
|
+
} catch {
|
|
21
|
+
autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return autoSyncBridge;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
async function main() {
|
|
15
28
|
try {
|
|
29
|
+
// Auto-sync bridge if needed (non-blocking, silent)
|
|
30
|
+
try {
|
|
31
|
+
const syncFn = getAutoSyncBridge();
|
|
32
|
+
await syncFn('claude-code', { silent: true });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (process.env.DEBUG) {
|
|
35
|
+
console.error(`[session-start] Bridge auto-sync failed: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
// Read input from stdin
|
|
17
39
|
let inputData = '';
|
|
18
40
|
for await (const chunk of process.stdin) {
|
|
@@ -31,6 +31,19 @@ function getSessionState() {
|
|
|
31
31
|
return sessionState;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Lazy-load bridge state for auto-sync
|
|
35
|
+
let autoSyncBridge = null;
|
|
36
|
+
function getAutoSyncBridge() {
|
|
37
|
+
if (!autoSyncBridge) {
|
|
38
|
+
try {
|
|
39
|
+
autoSyncBridge = require('../../../flow-bridge-state').autoSyncBridge;
|
|
40
|
+
} catch {
|
|
41
|
+
autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return autoSyncBridge;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
/**
|
|
35
48
|
* Read stdin with size limit protection
|
|
36
49
|
* @returns {string} Input data, truncated if over limit
|
|
@@ -59,6 +72,16 @@ async function readStdinWithLimit() {
|
|
|
59
72
|
* Handle session start event
|
|
60
73
|
*/
|
|
61
74
|
async function handleSessionStart(input) {
|
|
75
|
+
// Auto-sync bridge if needed (non-blocking, silent)
|
|
76
|
+
try {
|
|
77
|
+
const syncFn = getAutoSyncBridge();
|
|
78
|
+
await syncFn('cursor', { silent: true });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (process.env.DEBUG) {
|
|
81
|
+
console.error(`[cursor/session-start] Bridge auto-sync failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
try {
|
|
63
86
|
const parsedInput = cursorAdapter.parseInput(input);
|
|
64
87
|
|
|
@@ -22,8 +22,30 @@ try {
|
|
|
22
22
|
clearStaleCurrentTaskAsync = async () => {};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Lazy-load bridge state for auto-sync
|
|
26
|
+
let autoSyncBridge = null;
|
|
27
|
+
function getAutoSyncBridge() {
|
|
28
|
+
if (!autoSyncBridge) {
|
|
29
|
+
try {
|
|
30
|
+
autoSyncBridge = require('../../../flow-bridge-state').autoSyncBridge;
|
|
31
|
+
} catch {
|
|
32
|
+
autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return autoSyncBridge;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
async function main() {
|
|
26
39
|
try {
|
|
40
|
+
// Auto-sync bridge if needed (non-blocking, silent)
|
|
41
|
+
try {
|
|
42
|
+
const syncFn = getAutoSyncBridge();
|
|
43
|
+
await syncFn('gemini-cli', { silent: true });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (process.env.DEBUG) {
|
|
46
|
+
console.error(`[session-start] Bridge auto-sync failed: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
// Read input from stdin
|
|
28
50
|
let inputData = '';
|
|
29
51
|
for await (const chunk of process.stdin) {
|
|
@@ -26,12 +26,35 @@ function getSessionState() {
|
|
|
26
26
|
return sessionState;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Lazy-load bridge state for auto-sync
|
|
30
|
+
let autoSyncBridge = null;
|
|
31
|
+
function getAutoSyncBridge() {
|
|
32
|
+
if (!autoSyncBridge) {
|
|
33
|
+
try {
|
|
34
|
+
autoSyncBridge = require('../../../flow-bridge-state').autoSyncBridge;
|
|
35
|
+
} catch {
|
|
36
|
+
autoSyncBridge = async () => ({ synced: false, reason: 'unavailable' });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return autoSyncBridge;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
/**
|
|
30
43
|
* Handle session start event
|
|
31
44
|
* @param {Object} ctx - OpenCode plugin context
|
|
32
45
|
* @returns {Object} Plugin result with additionalContext
|
|
33
46
|
*/
|
|
34
47
|
async function handleSessionStart(ctx) {
|
|
48
|
+
// Auto-sync bridge if needed (non-blocking, silent)
|
|
49
|
+
try {
|
|
50
|
+
const syncFn = getAutoSyncBridge();
|
|
51
|
+
await syncFn('opencode', { silent: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
console.error(`[opencode/session-start] Bridge auto-sync failed: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
try {
|
|
36
59
|
const input = ctx || {};
|
|
37
60
|
const parsedInput = opencodeAdapter.parseInput(input);
|