devforgeai 1.0.7 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devforgeai",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
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 };
|