devforgeai 1.0.6 → 1.0.8

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/devforgeai.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devforgeai",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "DevForgeAI is a spec-driven development framework designed to enable AI-assisted software development with zero technical debt through automated validation, architectural constraints enforcement, and test-driven development workflows.",
5
5
  "keywords": [
6
6
  "ai",
File without changes
File without changes
File without changes
File without changes
@@ -204,9 +204,14 @@ async function action(directory, opts) {
204
204
  ide: ['claude-code'],
205
205
  });
206
206
 
207
- // IDE setup
207
+ // IDE setup (merge settings on update, overwrite on reinstall)
208
208
  const claudeCode = new ClaudeCodeIDE();
209
- await claudeCode.setup(installDir, copier);
209
+ const ideResult = await claudeCode.setup(installDir, copier, {
210
+ reinstall: !opts._updateMode,
211
+ });
212
+ if (!opts.quiet && ideResult.message) {
213
+ formatter.info(ideResult.message);
214
+ }
210
215
 
211
216
  // Phase 9: Summary
212
217
  if (!opts.quiet) {
@@ -1,23 +1,38 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const fsp = fs.promises;
1
4
  const { BaseIDE } = require('./base');
5
+ const { SettingsMerger } = require('../settings-merger');
2
6
 
3
7
  class ClaudeCodeIDE extends BaseIDE {
4
8
  constructor() {
5
9
  super({ name: 'claude-code', displayName: 'Claude Code' });
6
10
  }
7
11
 
8
- async setup(targetRoot, copier) {
9
- // Claude Code uses .claude/ directory which is already handled
10
- // by the core-framework, agents, skills, commands, hooks components.
11
- // This integration ensures the directory structure is correct
12
- // and any Claude Code-specific config is in place.
12
+ async setup(targetRoot, copier, options = {}) {
13
+ const merger = new SettingsMerger(targetRoot);
13
14
 
14
- // No additional steps needed - all .claude/ content
15
- // is handled by component copier
16
- return { success: true, message: 'Claude Code integration ready' };
15
+ // Load template settings from the package source
16
+ const templatePath = path.join(copier.sourceRoot, 'src', 'claude', 'settings.json');
17
+ let templateSettings;
18
+ try {
19
+ const raw = await fsp.readFile(templatePath, 'utf8');
20
+ templateSettings = JSON.parse(raw);
21
+ } catch (err) {
22
+ return { success: false, message: `Could not read template settings: ${err.message}` };
23
+ }
24
+
25
+ const mode = options.reinstall ? 'overwrite' : 'merge';
26
+ const result = await merger.install(templateSettings, { mode });
27
+
28
+ return {
29
+ success: true,
30
+ message: `Claude Code settings ${result.action}${result.backupCreated ? ' (backup created)' : ''}`,
31
+ };
17
32
  }
18
33
 
19
34
  describe() {
20
- return 'Claude Code — .claude/ directory with agents, skills, commands, rules, hooks';
35
+ return 'Claude Code — .claude/ directory with agents, skills, commands, rules, hooks, settings';
21
36
  }
22
37
  }
23
38
 
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const fsp = fs.promises;
5
+ const path = require('path');
6
+
7
+ class SettingsMerger {
8
+ constructor(targetRoot) {
9
+ if (!targetRoot) {
10
+ throw new Error('SettingsMerger requires targetRoot');
11
+ }
12
+ this.targetRoot = path.resolve(targetRoot);
13
+ this.settingsPath = path.join(this.targetRoot, '.claude', 'settings.json');
14
+ this.backupPath = path.join(this.targetRoot, '.claude', 'settings.json.bak');
15
+ }
16
+
17
+ /**
18
+ * Install settings.json into target project.
19
+ * @param {object} templateSettings - The DevForgeAI template settings object
20
+ * @param {object} options - { mode: 'merge' | 'overwrite' }
21
+ * @returns {object} { action: string, backupCreated: boolean }
22
+ */
23
+ async install(templateSettings, options = {}) {
24
+ const mode = options.mode || 'merge';
25
+
26
+ await fsp.mkdir(path.dirname(this.settingsPath), { recursive: true });
27
+
28
+ let existing = null;
29
+ try {
30
+ const raw = await fsp.readFile(this.settingsPath, 'utf8');
31
+ existing = JSON.parse(raw);
32
+ } catch {
33
+ // No existing file or invalid JSON — treat as fresh install
34
+ }
35
+
36
+ if (!existing || mode === 'overwrite') {
37
+ await fsp.writeFile(this.settingsPath, JSON.stringify(templateSettings, null, 2) + '\n', 'utf8');
38
+ return { action: existing ? 'overwritten' : 'created', backupCreated: false };
39
+ }
40
+
41
+ // Merge mode: backup first, then deep merge
42
+ await this.backup(existing);
43
+ const merged = this.merge(existing, templateSettings);
44
+ await fsp.writeFile(this.settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
45
+ return { action: 'merged', backupCreated: true };
46
+ }
47
+
48
+ /**
49
+ * Deep merge template into existing settings.
50
+ * Existing values are preserved; template fills gaps.
51
+ */
52
+ merge(existing, template) {
53
+ const result = JSON.parse(JSON.stringify(existing));
54
+
55
+ // Merge permissions
56
+ if (template.permissions) {
57
+ result.permissions = this.mergePermissions(
58
+ result.permissions || {},
59
+ template.permissions
60
+ );
61
+ }
62
+
63
+ // Merge hooks
64
+ if (template.hooks) {
65
+ result.hooks = this.mergeHooks(
66
+ result.hooks || {},
67
+ template.hooks
68
+ );
69
+ }
70
+
71
+ // Set statusLine if missing
72
+ if (template.statusLine && !result.statusLine) {
73
+ result.statusLine = template.statusLine;
74
+ }
75
+
76
+ // Set includeCoAuthoredBy if missing
77
+ if ('includeCoAuthoredBy' in template && !('includeCoAuthoredBy' in result)) {
78
+ result.includeCoAuthoredBy = template.includeCoAuthoredBy;
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Merge permission arrays: union of allow/ask/deny with deduplication.
86
+ */
87
+ mergePermissions(existing, incoming) {
88
+ const result = JSON.parse(JSON.stringify(existing));
89
+
90
+ if (incoming.defaultMode && !result.defaultMode) {
91
+ result.defaultMode = incoming.defaultMode;
92
+ }
93
+
94
+ for (const key of ['allow', 'ask', 'deny']) {
95
+ if (incoming[key]) {
96
+ const existingSet = new Set(result[key] || []);
97
+ for (const item of incoming[key]) {
98
+ existingSet.add(item);
99
+ }
100
+ result[key] = [...existingSet];
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Merge hooks by event name. Within each event, deduplicate by command path.
109
+ */
110
+ mergeHooks(existing, incoming) {
111
+ const result = JSON.parse(JSON.stringify(existing));
112
+
113
+ for (const [eventName, incomingEntries] of Object.entries(incoming)) {
114
+ if (!result[eventName]) {
115
+ // Event doesn't exist — add all entries
116
+ result[eventName] = incomingEntries;
117
+ continue;
118
+ }
119
+
120
+ // Event exists — deduplicate by command path
121
+ for (const incomingEntry of incomingEntries) {
122
+ const isDuplicate = result[eventName].some(existingEntry =>
123
+ this._hookEntriesMatch(existingEntry, incomingEntry)
124
+ );
125
+ if (!isDuplicate) {
126
+ result[eventName].push(incomingEntry);
127
+ }
128
+ }
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Check if two hook entries match (same matcher + same command paths).
136
+ */
137
+ _hookEntriesMatch(a, b) {
138
+ // Different matchers = different entries
139
+ const matcherA = a.matcher || '';
140
+ const matcherB = b.matcher || '';
141
+ if (matcherA !== matcherB) return false;
142
+
143
+ // Compare hook commands
144
+ const cmdsA = (a.hooks || []).map(h => h.command).sort();
145
+ const cmdsB = (b.hooks || []).map(h => h.command).sort();
146
+
147
+ if (cmdsA.length !== cmdsB.length) return false;
148
+ return cmdsA.every((cmd, i) => cmd === cmdsB[i]);
149
+ }
150
+
151
+ /**
152
+ * Backup existing settings.json.
153
+ */
154
+ async backup(existingObj) {
155
+ const content = JSON.stringify(existingObj, null, 2) + '\n';
156
+ await fsp.writeFile(this.backupPath, content, 'utf8');
157
+ }
158
+ }
159
+
160
+ module.exports = { SettingsMerger };