@comfanion/workflow 4.36.5 → 4.36.7

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/bin/cli.js CHANGED
@@ -11,20 +11,22 @@ import { execSync } from 'child_process';
11
11
  import yaml from 'js-yaml';
12
12
 
13
13
  /**
14
- * Deep merge two objects. User values override defaults.
15
- * Arrays are replaced, not merged.
14
+ * Find top-level keys that exist in newObj but not in oldObj
16
15
  */
17
- function deepMerge(defaults, user) {
18
- const result = { ...defaults };
19
- for (const key of Object.keys(user)) {
20
- if (user[key] !== null && typeof user[key] === 'object' && !Array.isArray(user[key]) &&
21
- defaults[key] !== null && typeof defaults[key] === 'object' && !Array.isArray(defaults[key])) {
22
- result[key] = deepMerge(defaults[key], user[key]);
23
- } else {
24
- result[key] = user[key];
16
+ function findNewKeys(newObj, oldObj, prefix = '') {
17
+ const newKeys = [];
18
+ for (const key of Object.keys(newObj)) {
19
+ const fullKey = prefix ? `${prefix}.${key}` : key;
20
+ if (!(key in oldObj)) {
21
+ newKeys.push(fullKey);
22
+ } else if (
23
+ newObj[key] !== null && typeof newObj[key] === 'object' && !Array.isArray(newObj[key]) &&
24
+ oldObj[key] !== null && typeof oldObj[key] === 'object' && !Array.isArray(oldObj[key])
25
+ ) {
26
+ newKeys.push(...findNewKeys(newObj[key], oldObj[key], fullKey));
25
27
  }
26
28
  }
27
- return result;
29
+ return newKeys;
28
30
  }
29
31
 
30
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -278,6 +280,8 @@ program
278
280
  let hadVectorizer = false;
279
281
  let hadVectors = false;
280
282
 
283
+ let existingConfigContent = null;
284
+
281
285
  if (await fs.pathExists(targetDir)) {
282
286
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
283
287
  const backupDir = path.join(process.cwd(), `.opencode.backup-${timestamp}`);
@@ -286,6 +290,12 @@ program
286
290
  hadVectorizer = await fs.pathExists(vectorizerNodeModules);
287
291
  hadVectors = await fs.pathExists(vectorsDir);
288
292
 
293
+ // Read existing config.yaml for merge
294
+ const existingConfigPath = path.join(targetDir, 'config.yaml');
295
+ if (await fs.pathExists(existingConfigPath)) {
296
+ existingConfigContent = await fs.readFile(existingConfigPath, 'utf8');
297
+ }
298
+
289
299
  // Preserve vectorizer node_modules (100MB+, don't backup)
290
300
  if (hadVectorizer) {
291
301
  spinner.text = 'Preserving vectorizer dependencies...';
@@ -339,20 +349,31 @@ program
339
349
  // Update config.yaml with user values
340
350
  spinner.text = 'Configuring...';
341
351
  const configPath = path.join(targetDir, 'config.yaml');
342
- let configContent = await fs.readFile(configPath, 'utf8');
352
+ let configContent;
353
+
354
+ // If we had existing config, use it as base (preserves comments and formatting)
355
+ if (existingConfigContent) {
356
+ configContent = existingConfigContent;
357
+ // Update version to match new package
358
+ configContent = configContent.replace(/^version:\s*["']?[\d.]+["']?/m, `version: "${VERSION}"`);
359
+ console.log(chalk.green(' ✅ Restored your config (comments preserved)'));
360
+ } else {
361
+ configContent = await fs.readFile(configPath, 'utf8');
362
+ }
343
363
 
364
+ // Apply user's answers from prompts
344
365
  configContent = configContent
345
- .replace(/user_name: ".*"/, `user_name: "${config.user_name}"`)
346
- .replace(/communication_language: ".*"/, `communication_language: "${config.communication_language}"`)
347
- .replace(/project_name: ".*"/, `project_name: "${config.project_name}"`)
348
- .replace(/methodology: (tdd|stub)/, `methodology: ${config.methodology}`);
366
+ .replace(/user_name:\s*["']?[^"\n]*["']?/, `user_name: "${config.user_name}"`)
367
+ .replace(/communication_language:\s*["']?[^"\n]*["']?/, `communication_language: "${config.communication_language}"`)
368
+ .replace(/project_name:\s*["']?[^"\n]*["']?/, `project_name: "${config.project_name}"`)
369
+ .replace(/methodology:\s*(tdd|stub)/, `methodology: ${config.methodology}`);
349
370
 
350
371
  // Jira config
351
372
  if (config.jira_enabled) {
352
373
  configContent = configContent
353
374
  .replace(/enabled: false\s+# Jira/, `enabled: true # Jira`)
354
- .replace(/base_url: ".*"/, `base_url: "${config.jira_url}"`)
355
- .replace(/project_key: ".*"/, `project_key: "${config.jira_project}"`);
375
+ .replace(/base_url: .*/, `base_url: "${config.jira_url}"`)
376
+ .replace(/project_key: .*/, `project_key: "${config.jira_project}"`);
356
377
  }
357
378
 
358
379
  // Vectorizer config
@@ -590,33 +611,37 @@ program
590
611
  await fs.move(tempVectors, path.join(targetDir, 'vectors'), { overwrite: true });
591
612
  }
592
613
 
593
- // Merge config.yaml: new defaults + user overrides
594
- spinner.text = 'Merging config.yaml...';
614
+ // Restore user's config.yaml (preserves comments and formatting)
615
+ spinner.text = 'Restoring config.yaml...';
595
616
  try {
596
- const newConfigPath = path.join(targetDir, 'config.yaml');
597
- const newConfigContent = await fs.readFile(newConfigPath, 'utf8');
617
+ // Update version in user's config to match new package
618
+ let restoredConfig = configBackup.replace(
619
+ /^version:\s*["']?[\d.]+["']?/m,
620
+ `version: "${VERSION}"`
621
+ );
622
+ await fs.writeFile(configPath, restoredConfig);
623
+ console.log(chalk.green(' ✅ config.yaml restored (your settings + comments preserved)'));
598
624
 
599
- // Parse both configs
600
- const newConfig = yaml.load(newConfigContent) || {};
601
- const userConfig = yaml.load(configBackup) || {};
602
-
603
- // Deep merge: defaults from new config, overridden by user values
604
- const mergedConfig = deepMerge(newConfig, userConfig);
605
-
606
- // Dump back to YAML with nice formatting
607
- const mergedContent = yaml.dump(mergedConfig, {
608
- indent: 2,
609
- lineWidth: 120,
610
- noRefs: true,
611
- sortKeys: false
612
- });
613
-
614
- await fs.writeFile(configPath, mergedContent);
615
- console.log(chalk.green(' ✅ config.yaml merged (your settings preserved, new options added)'));
625
+ // Check if new template has options that user doesn't have
626
+ const newConfigPath = path.join(OPENCODE_SRC, 'config.yaml');
627
+ if (await fs.pathExists(newConfigPath)) {
628
+ const newConfig = yaml.load(await fs.readFile(newConfigPath, 'utf8')) || {};
629
+ const userConfig = yaml.load(configBackup) || {};
630
+ const newKeys = findNewKeys(newConfig, userConfig);
631
+ if (newKeys.length > 0) {
632
+ console.log(chalk.yellow(` 💡 New config options available: ${newKeys.slice(0, 3).join(', ')}${newKeys.length > 3 ? '...' : ''}`));
633
+ console.log(chalk.gray(' Check .opencode.backup-*/config.yaml.new for full template'));
634
+ // Save new template for reference
635
+ await fs.writeFile(
636
+ path.join(process.cwd(), `.opencode.backup-${timestamp}`, 'config.yaml.new'),
637
+ await fs.readFile(newConfigPath, 'utf8')
638
+ );
639
+ }
640
+ }
616
641
  } catch (e) {
617
- // Fallback: just restore user's config if merge fails
642
+ // Fallback: just restore user's config
618
643
  await fs.writeFile(configPath, configBackup);
619
- console.log(chalk.yellow(' ⚠️ config.yaml restored (merge failed, using your original)'));
644
+ console.log(chalk.yellow(' ⚠️ config.yaml restored'));
620
645
  }
621
646
 
622
647
  // Install plugin dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfanion/workflow",
3
- "version": "4.36.5",
3
+ "version": "4.36.7",
4
4
  "description": "Initialize OpenCode Workflow system for AI-assisted development with semantic code search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "3.0.0",
3
- "buildDate": "2026-01-24T15:01:58.449Z",
3
+ "buildDate": "2026-01-24T15:08:08.592Z",
4
4
  "files": [
5
5
  "config.yaml",
6
6
  "FLOW.yaml",