wogiflow 1.9.8 → 1.9.9

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/lib/installer.js CHANGED
@@ -532,6 +532,14 @@ function createWorkflowStructure(projectRoot, config) {
532
532
  });
533
533
  fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
534
534
 
535
+ // Sync .gitignore entries for enabled features
536
+ try {
537
+ const { syncGitignore } = require('../scripts/flow-gitignore');
538
+ syncGitignore(configContent);
539
+ } catch (err) {
540
+ // Non-blocking — gitignore sync should never fail installation
541
+ }
542
+
535
543
  // Create ready.json
536
544
  const readyPath = path.join(workflowDir, 'state', 'ready.json');
537
545
  const readyContent = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "1.9.8",
3
+ "version": "1.9.9",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -518,6 +518,14 @@ async function setConfigValue(configPath, newValue) {
518
518
  obj[parts[parts.length - 1]] = newValue;
519
519
  writeJson(PATHS.config, config);
520
520
  invalidateConfigCache();
521
+
522
+ // Auto-sync .gitignore when config changes enable features with runtime artifacts
523
+ try {
524
+ const { syncGitignore } = require('./flow-gitignore');
525
+ syncGitignore(config);
526
+ } catch (err) {
527
+ // Non-blocking — gitignore sync should never fail config writes
528
+ }
521
529
  } finally {
522
530
  if (release) release();
523
531
  }
@@ -548,6 +556,14 @@ function setConfigValueSync(configPath, newValue) {
548
556
  obj[parts[parts.length - 1]] = newValue;
549
557
  writeJson(PATHS.config, config);
550
558
  invalidateConfigCache();
559
+
560
+ // Auto-sync .gitignore when config changes enable features with runtime artifacts
561
+ try {
562
+ const { syncGitignore } = require('./flow-gitignore');
563
+ syncGitignore(config);
564
+ } catch (err) {
565
+ // Non-blocking — gitignore sync should never fail config writes
566
+ }
551
567
  }
552
568
 
553
569
  /**
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Gitignore Auto-Management
5
+ *
6
+ * Declarative mapping of config features to .gitignore entries.
7
+ * When a config change enables a feature that produces runtime artifacts,
8
+ * the relevant entries are automatically appended to .gitignore.
9
+ *
10
+ * Design:
11
+ * - Append-only: never removes existing entries
12
+ * - Idempotent: no duplicate entries on repeated calls
13
+ * - Grouped under "# WogiFlow runtime (auto-managed)" comment
14
+ * - Declarative: RUNTIME_ARTIFACT_MAP defines all mappings
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { getConfig } = require('./flow-config-loader');
22
+ const { PROJECT_ROOT } = require('./flow-paths');
23
+
24
+ // ============================================================================
25
+ // Declarative Config-to-Gitignore Mapping
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Each entry maps a config condition to gitignore patterns.
30
+ * - configPath: dot-notation path into config object
31
+ * - matchValue: value that triggers the entry (true for boolean, string for exact match)
32
+ * If matchValue is true, any truthy value triggers the entry.
33
+ * - patterns: gitignore entries to add when condition is met
34
+ * - description: human-readable description for health check output
35
+ */
36
+ const RUNTIME_ARTIFACT_MAP = [
37
+ {
38
+ configPath: 'testing.uiProvider',
39
+ matchValue: 'playwright-mcp',
40
+ patterns: ['.playwright-mcp/'],
41
+ description: 'Playwright MCP logs and screenshots'
42
+ },
43
+ {
44
+ configPath: 'testing.enabled',
45
+ matchValue: true,
46
+ patterns: ['.workflow/verifications/', '.workflow/tests/generated/'],
47
+ description: 'Test verification artifacts and generated tests'
48
+ },
49
+ {
50
+ configPath: 'webmcp.enabled',
51
+ matchValue: true,
52
+ patterns: ['.workflow/webmcp/'],
53
+ description: 'WebMCP tool definitions'
54
+ }
55
+ ];
56
+
57
+ const GITIGNORE_SECTION_HEADER = '# WogiFlow runtime (auto-managed)';
58
+
59
+ // ============================================================================
60
+ // Core Functions
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Get the value at a dot-notation path in an object.
65
+ * @param {Object} obj
66
+ * @param {string} dotPath - e.g. 'testing.uiProvider'
67
+ * @returns {*} Value at path, or undefined
68
+ */
69
+ function getNestedValue(obj, dotPath) {
70
+ const parts = dotPath.split('.');
71
+ let current = obj;
72
+ for (const part of parts) {
73
+ if (current == null || typeof current !== 'object') return undefined;
74
+ current = current[part];
75
+ }
76
+ return current;
77
+ }
78
+
79
+ /**
80
+ * Determine which gitignore entries are needed based on current config.
81
+ * @param {Object} [config] - Pre-loaded config (optional)
82
+ * @returns {string[]} Gitignore patterns that should exist
83
+ */
84
+ function getRequiredEntries(config) {
85
+ if (!config) config = getConfig();
86
+ const needed = [];
87
+
88
+ for (const mapping of RUNTIME_ARTIFACT_MAP) {
89
+ const value = getNestedValue(config, mapping.configPath);
90
+ let matches = false;
91
+
92
+ if (mapping.matchValue === true) {
93
+ matches = !!value;
94
+ } else {
95
+ matches = value === mapping.matchValue;
96
+ }
97
+
98
+ if (matches) {
99
+ needed.push(...mapping.patterns);
100
+ }
101
+ }
102
+
103
+ return needed;
104
+ }
105
+
106
+ /**
107
+ * Read the current .gitignore content.
108
+ * @returns {string} Content of .gitignore, or empty string if not found
109
+ */
110
+ function readGitignore() {
111
+ const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
112
+ try {
113
+ return fs.readFileSync(gitignorePath, 'utf-8');
114
+ } catch (err) {
115
+ return '';
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Parse existing gitignore entries (trimmed, non-empty, non-comment lines).
121
+ * @param {string} content - .gitignore content
122
+ * @returns {Set<string>} Set of existing entries
123
+ */
124
+ function parseExistingEntries(content) {
125
+ const entries = new Set();
126
+ for (const line of content.split('\n')) {
127
+ const trimmed = line.trim();
128
+ if (trimmed && !trimmed.startsWith('#')) {
129
+ entries.push ? entries.add(trimmed) : entries.add(trimmed);
130
+ }
131
+ }
132
+ return entries;
133
+ }
134
+
135
+ /**
136
+ * Sync .gitignore with config-required entries.
137
+ * Appends missing entries under the auto-managed section header.
138
+ * @param {Object} [config] - Pre-loaded config (optional)
139
+ * @returns {{ added: string[], alreadyPresent: string[] }} What was done
140
+ */
141
+ function syncGitignore(config) {
142
+ const required = getRequiredEntries(config);
143
+ if (required.length === 0) {
144
+ return { added: [], alreadyPresent: [] };
145
+ }
146
+
147
+ const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
148
+ const content = readGitignore();
149
+ const existing = parseExistingEntries(content);
150
+
151
+ const missing = required.filter(entry => !existing.has(entry));
152
+ const alreadyPresent = required.filter(entry => existing.has(entry));
153
+
154
+ if (missing.length === 0) {
155
+ return { added: [], alreadyPresent };
156
+ }
157
+
158
+ // Build the new section content
159
+ const hasSection = content.includes(GITIGNORE_SECTION_HEADER);
160
+ let newContent;
161
+
162
+ if (hasSection) {
163
+ // Append to existing section — find the section and add after it
164
+ const lines = content.split('\n');
165
+ const headerIdx = lines.findIndex(l => l.trim() === GITIGNORE_SECTION_HEADER);
166
+ // Find the end of the managed section (next blank line or next comment section)
167
+ let insertIdx = headerIdx + 1;
168
+ while (insertIdx < lines.length) {
169
+ const line = lines[insertIdx].trim();
170
+ if (line === '' || (line.startsWith('#') && line !== GITIGNORE_SECTION_HEADER)) {
171
+ break;
172
+ }
173
+ insertIdx++;
174
+ }
175
+ // Insert missing entries
176
+ lines.splice(insertIdx, 0, ...missing);
177
+ newContent = lines.join('\n');
178
+ } else {
179
+ // Create new section at end of file
180
+ const separator = content.endsWith('\n') ? '\n' : '\n\n';
181
+ newContent = content + separator + GITIGNORE_SECTION_HEADER + '\n' + missing.join('\n') + '\n';
182
+ }
183
+
184
+ fs.writeFileSync(gitignorePath, newContent, 'utf-8');
185
+
186
+ return { added: missing, alreadyPresent };
187
+ }
188
+
189
+ /**
190
+ * Check gitignore health — returns missing entries for /wogi-health.
191
+ * @param {Object} [config] - Pre-loaded config (optional)
192
+ * @returns {{ ok: boolean, missing: Array<{pattern: string, description: string}> }}
193
+ */
194
+ function checkGitignoreHealth(config) {
195
+ if (!config) config = getConfig();
196
+ const content = readGitignore();
197
+ const existing = parseExistingEntries(content);
198
+ const missing = [];
199
+
200
+ for (const mapping of RUNTIME_ARTIFACT_MAP) {
201
+ const value = getNestedValue(config, mapping.configPath);
202
+ let matches = false;
203
+
204
+ if (mapping.matchValue === true) {
205
+ matches = !!value;
206
+ } else {
207
+ matches = value === mapping.matchValue;
208
+ }
209
+
210
+ if (matches) {
211
+ for (const pattern of mapping.patterns) {
212
+ if (!existing.has(pattern)) {
213
+ missing.push({ pattern, description: mapping.description });
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ return { ok: missing.length === 0, missing };
220
+ }
221
+
222
+ // ============================================================================
223
+ // CLI
224
+ // ============================================================================
225
+
226
+ if (require.main === module) {
227
+ const args = process.argv.slice(2);
228
+ const command = args[0] || 'sync';
229
+
230
+ if (command === 'sync') {
231
+ const result = syncGitignore();
232
+ if (result.added.length > 0) {
233
+ console.log(`Added ${result.added.length} entries to .gitignore:`);
234
+ for (const entry of result.added) {
235
+ console.log(` + ${entry}`);
236
+ }
237
+ } else {
238
+ console.log('.gitignore is up to date');
239
+ }
240
+ } else if (command === 'check') {
241
+ const health = checkGitignoreHealth();
242
+ if (health.ok) {
243
+ console.log('.gitignore: all required entries present');
244
+ } else {
245
+ console.log(`Missing ${health.missing.length} .gitignore entries:`);
246
+ for (const m of health.missing) {
247
+ console.log(` - ${m.pattern} (${m.description})`);
248
+ }
249
+ console.log('\nRun: flow gitignore sync');
250
+ process.exit(1);
251
+ }
252
+ } else {
253
+ console.log('Usage: flow-gitignore.js [sync|check]');
254
+ }
255
+ }
256
+
257
+ module.exports = {
258
+ RUNTIME_ARTIFACT_MAP,
259
+ GITIGNORE_SECTION_HEADER,
260
+ getRequiredEntries,
261
+ syncGitignore,
262
+ checkGitignoreHealth
263
+ };
@@ -651,6 +651,26 @@ function main() {
651
651
  }
652
652
  }
653
653
 
654
+ // Check .gitignore sync
655
+ console.log('');
656
+ printSection('Checking .gitignore sync...');
657
+
658
+ try {
659
+ const { checkGitignoreHealth } = require('./flow-gitignore');
660
+ const gitignoreHealth = checkGitignoreHealth(config);
661
+ if (gitignoreHealth.ok) {
662
+ console.log(` ${color('green', '✓')} All required .gitignore entries present`);
663
+ } else {
664
+ for (const m of gitignoreHealth.missing) {
665
+ console.log(` ${color('yellow', '⚠')} Missing: ${m.pattern} (${m.description})`);
666
+ }
667
+ console.log(` ${color('yellow', '⚠')} Run: node scripts/flow-gitignore.js sync`);
668
+ warnings += gitignoreHealth.missing.length;
669
+ }
670
+ } catch (err) {
671
+ console.log(` ${color('yellow', '○')} Gitignore check unavailable`);
672
+ }
673
+
654
674
  // Check git status
655
675
  console.log('');
656
676
  printSection('Checking git status...');
@@ -2161,6 +2161,10 @@ module.exports = {
2161
2161
  // CLI Tool Detection (Claude Code 2.1.72+ compatibility)
2162
2162
  meetsVersion,
2163
2163
  getFdCommand,
2164
+
2165
+ // Gitignore Auto-Management (v1.9.8)
2166
+ get syncGitignore() { return require('./flow-gitignore').syncGitignore; },
2167
+ get checkGitignoreHealth() { return require('./flow-gitignore').checkGitignoreHealth; },
2164
2168
  };
2165
2169
 
2166
2170
  // ============================================================