create-universal-ai-context 2.3.0 → 2.4.0
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/create-ai-context.js +9 -2
- package/lib/adapters/aider.js +131 -0
- package/lib/adapters/antigravity.js +47 -2
- package/lib/adapters/claude.js +100 -25
- package/lib/adapters/cline.js +16 -2
- package/lib/adapters/continue.js +138 -0
- package/lib/adapters/copilot.js +16 -2
- package/lib/adapters/index.js +11 -2
- package/lib/adapters/windsurf.js +138 -0
- package/lib/ai-orchestrator.js +2 -1
- package/lib/content-preservation.js +243 -0
- package/lib/index.js +11 -4
- package/lib/placeholder.js +82 -1
- package/lib/template-coordination.js +148 -0
- package/lib/template-renderer.js +15 -5
- package/lib/utils/fs-wrapper.js +79 -0
- package/lib/utils/path-utils.js +60 -0
- package/package.json +1 -1
- package/templates/handlebars/aider-config.hbs +80 -0
- package/templates/handlebars/antigravity.hbs +40 -0
- package/templates/handlebars/claude.hbs +1 -2
- package/templates/handlebars/cline.hbs +1 -2
- package/templates/handlebars/continue-config.hbs +116 -0
- package/templates/handlebars/copilot.hbs +1 -2
- package/templates/handlebars/partials/header.hbs +1 -1
- package/templates/handlebars/windsurf-rules.hbs +69 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf IDE Adapter
|
|
3
|
+
*
|
|
4
|
+
* Generates .windsurf/rules.md file for Windsurf IDE Cascade AI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { renderTemplateByName, buildContext } = require('../template-renderer');
|
|
10
|
+
const { isManagedFile } = require('../template-coordination');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adapter metadata
|
|
14
|
+
*/
|
|
15
|
+
const adapter = {
|
|
16
|
+
name: 'windsurf',
|
|
17
|
+
displayName: 'Windsurf IDE',
|
|
18
|
+
description: 'Project rules for Windsurf IDE Cascade AI',
|
|
19
|
+
outputType: 'single-file',
|
|
20
|
+
outputPath: '.windsurf/rules.md'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get output path for Windsurf rules file
|
|
25
|
+
* @param {string} projectRoot - Project root directory
|
|
26
|
+
* @returns {string} Output file path
|
|
27
|
+
*/
|
|
28
|
+
function getOutputPath(projectRoot) {
|
|
29
|
+
return path.join(projectRoot, '.windsurf', 'rules.md');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if Windsurf output already exists
|
|
34
|
+
* @param {string} projectRoot - Project root directory
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function exists(projectRoot) {
|
|
38
|
+
const rulesPath = getOutputPath(projectRoot);
|
|
39
|
+
const windsurfDir = path.join(projectRoot, '.windsurf');
|
|
40
|
+
return fs.existsSync(rulesPath) || fs.existsSync(windsurfDir);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate Windsurf rules file
|
|
45
|
+
* @param {object} analysis - Analysis results from static analyzer
|
|
46
|
+
* @param {object} config - Configuration from CLI
|
|
47
|
+
* @param {string} projectRoot - Project root directory
|
|
48
|
+
* @returns {object} Generation result
|
|
49
|
+
*/
|
|
50
|
+
async function generate(analysis, config, projectRoot) {
|
|
51
|
+
const result = {
|
|
52
|
+
success: false,
|
|
53
|
+
adapter: adapter.name,
|
|
54
|
+
files: [],
|
|
55
|
+
errors: []
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const rulesPath = getOutputPath(projectRoot);
|
|
60
|
+
|
|
61
|
+
// Check if file exists and is custom (not managed by us)
|
|
62
|
+
if (fs.existsSync(rulesPath) && !config.force) {
|
|
63
|
+
if (!isManagedFile(rulesPath)) {
|
|
64
|
+
result.errors.push({
|
|
65
|
+
message: '.windsurf/rules.md exists and appears to be custom. Use --force to overwrite.',
|
|
66
|
+
code: 'EXISTS_CUSTOM',
|
|
67
|
+
severity: 'error'
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build context from analysis
|
|
74
|
+
const context = buildContext(analysis, config, 'windsurf');
|
|
75
|
+
|
|
76
|
+
// Render template
|
|
77
|
+
const content = renderTemplateByName('windsurf-rules', context);
|
|
78
|
+
|
|
79
|
+
// Create .windsurf directory if it doesn't exist
|
|
80
|
+
const windsurfDir = path.dirname(rulesPath);
|
|
81
|
+
if (!fs.existsSync(windsurfDir)) {
|
|
82
|
+
fs.mkdirSync(windsurfDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Write output file
|
|
86
|
+
fs.writeFileSync(rulesPath, content, 'utf-8');
|
|
87
|
+
|
|
88
|
+
result.success = true;
|
|
89
|
+
result.files.push({
|
|
90
|
+
path: rulesPath,
|
|
91
|
+
relativePath: '.windsurf/rules.md',
|
|
92
|
+
size: content.length
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
result.errors.push({
|
|
96
|
+
message: error.message,
|
|
97
|
+
stack: error.stack
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate Windsurf output
|
|
106
|
+
* @param {string} projectRoot - Project root directory
|
|
107
|
+
* @returns {object} Validation result
|
|
108
|
+
*/
|
|
109
|
+
function validate(projectRoot) {
|
|
110
|
+
const issues = [];
|
|
111
|
+
const rulesPath = getOutputPath(projectRoot);
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(rulesPath)) {
|
|
114
|
+
issues.push({ file: '.windsurf/rules.md', error: 'not found' });
|
|
115
|
+
} else {
|
|
116
|
+
const content = fs.readFileSync(rulesPath, 'utf-8');
|
|
117
|
+
const placeholderMatch = content.match(/\{\{[A-Z_]+\}\}/g);
|
|
118
|
+
if (placeholderMatch && placeholderMatch.length > 0) {
|
|
119
|
+
issues.push({
|
|
120
|
+
file: '.windsurf/rules.md',
|
|
121
|
+
error: `Found ${placeholderMatch.length} unreplaced placeholders`
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
valid: issues.filter(i => i.severity !== 'warning').length === 0,
|
|
128
|
+
issues
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
...adapter,
|
|
134
|
+
getOutputPath,
|
|
135
|
+
exists,
|
|
136
|
+
generate,
|
|
137
|
+
validate
|
|
138
|
+
};
|
package/lib/ai-orchestrator.js
CHANGED
|
@@ -303,7 +303,8 @@ function getPackageVersion() {
|
|
|
303
303
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
304
304
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
305
305
|
return pkg.version || '1.0.0';
|
|
306
|
-
} catch {
|
|
306
|
+
} catch (error) {
|
|
307
|
+
// Silently fall back to default version
|
|
307
308
|
return '1.0.0';
|
|
308
309
|
}
|
|
309
310
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Preservation Module
|
|
3
|
+
* Handles migration and preservation of custom content during context generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Find custom content in .claude/ directory
|
|
11
|
+
* Custom content = files without "MANAGED BY" header
|
|
12
|
+
* @param {string} claudeDir - Path to .claude/ directory
|
|
13
|
+
* @returns {Array} List of custom content items
|
|
14
|
+
*/
|
|
15
|
+
function findCustomContentInClaude(claudeDir) {
|
|
16
|
+
const custom = [];
|
|
17
|
+
|
|
18
|
+
const walkDir = (dir, depth = 0) => {
|
|
19
|
+
// Limit depth to avoid infinite recursion
|
|
20
|
+
if (depth > 10) return;
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
// Skip node_modules and other common dirs
|
|
30
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
walkDir(path.join(dir, entry.name), depth + 1);
|
|
34
|
+
} else if (entry.name.endsWith('.md')) {
|
|
35
|
+
const filePath = path.join(dir, entry.name);
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
|
|
39
|
+
// Check if file has managed header
|
|
40
|
+
if (!content.includes('MANAGED BY CREATE-AI-CONTEXT') &&
|
|
41
|
+
!content.includes('Auto-generated by AI Context Engineering')) {
|
|
42
|
+
const relPath = path.relative(claudeDir, filePath);
|
|
43
|
+
custom.push({
|
|
44
|
+
path: relPath,
|
|
45
|
+
type: determineContentType(relPath),
|
|
46
|
+
content: content
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// Skip unreadable files
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
walkDir(claudeDir);
|
|
57
|
+
return custom;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Determine content type from path
|
|
62
|
+
* @param {string} relPath - Relative path from .claude/
|
|
63
|
+
* @returns {string} Content type
|
|
64
|
+
*/
|
|
65
|
+
function determineContentType(relPath) {
|
|
66
|
+
// Normalize path separators to forward slashes for cross-platform compatibility
|
|
67
|
+
const normalizedPath = relPath.replace(/\\/g, '/');
|
|
68
|
+
|
|
69
|
+
if (normalizedPath.startsWith('agents/')) return 'agent';
|
|
70
|
+
if (normalizedPath.startsWith('commands/')) return 'command';
|
|
71
|
+
if (normalizedPath.startsWith('context/')) return 'context';
|
|
72
|
+
if (normalizedPath.startsWith('workflows/')) return 'workflow';
|
|
73
|
+
if (normalizedPath.startsWith('schemas/')) return 'schema';
|
|
74
|
+
if (normalizedPath.startsWith('standards/')) return 'standard';
|
|
75
|
+
return 'other';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Migrate custom content to .ai-context/custom/
|
|
80
|
+
* @param {string} claudeDir - Source .claude/ directory
|
|
81
|
+
* @param {string} aiContextDir - Destination .ai-context/ directory
|
|
82
|
+
* @param {Array} customItems - Items to migrate
|
|
83
|
+
* @returns {Array} List of migrated items with paths
|
|
84
|
+
*/
|
|
85
|
+
function migrateCustomContent(claudeDir, aiContextDir, customItems) {
|
|
86
|
+
const customDir = path.join(aiContextDir, 'custom');
|
|
87
|
+
fs.mkdirSync(customDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
const migrated = [];
|
|
90
|
+
|
|
91
|
+
for (const item of customItems) {
|
|
92
|
+
const destPath = path.join(customDir, item.path);
|
|
93
|
+
const destDir = path.dirname(destPath);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
97
|
+
|
|
98
|
+
// Add preservation header
|
|
99
|
+
const preservedHeader = `<!--
|
|
100
|
+
PRESERVED FROM .claude/${item.path}
|
|
101
|
+
This file was migrated from .claude/ to .ai-context/custom/ to preserve custom content.
|
|
102
|
+
Original migration date: ${new Date().toISOString()}
|
|
103
|
+
-->
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(destPath, preservedHeader + item.content);
|
|
107
|
+
|
|
108
|
+
migrated.push({
|
|
109
|
+
original: `.claude/${item.path}`,
|
|
110
|
+
destination: `.ai-context/custom/${item.path}`,
|
|
111
|
+
type: item.type
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// Log error but continue with other files
|
|
115
|
+
console.warn(`Failed to migrate ${item.path}: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return migrated;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect custom content markers in a file
|
|
124
|
+
* @param {string} content - File content
|
|
125
|
+
* @returns {boolean} True if content has custom markers
|
|
126
|
+
*/
|
|
127
|
+
function detectCustomContent(content) {
|
|
128
|
+
// Check for absence of managed marker
|
|
129
|
+
if (!content || content.length === 0) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasManagedMarker = content.includes('MANAGED BY CREATE-AI-CONTEXT') ||
|
|
134
|
+
content.includes('Auto-generated by AI Context Engineering') ||
|
|
135
|
+
content.includes('Managed by create-ai-context') ||
|
|
136
|
+
content.includes('CREATE-AI-CONTEXT');
|
|
137
|
+
|
|
138
|
+
if (!hasManagedMarker) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for custom edit markers
|
|
143
|
+
if (content.includes('<!-- CUSTOM EDIT -->') ||
|
|
144
|
+
content.includes('# CUSTOM EDIT') ||
|
|
145
|
+
content.includes('<!-- USER CUSTOM -->')) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find custom files in .agent/ directory
|
|
154
|
+
* @param {string} agentDir - Path to .agent/ directory
|
|
155
|
+
* @returns {Array} List of custom file paths (relative)
|
|
156
|
+
*/
|
|
157
|
+
function findCustomFilesInAgent(agentDir) {
|
|
158
|
+
const custom = [];
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(agentDir)) {
|
|
161
|
+
return custom;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const walkDir = (dir, depth = 0) => {
|
|
165
|
+
if (depth > 10) return;
|
|
166
|
+
|
|
167
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
171
|
+
walkDir(path.join(dir, entry.name), depth + 1);
|
|
172
|
+
}
|
|
173
|
+
} else if (entry.name.endsWith('.md')) {
|
|
174
|
+
const filePath = path.join(dir, entry.name);
|
|
175
|
+
try {
|
|
176
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
177
|
+
|
|
178
|
+
if (!content.includes('Auto-generated by AI Context Engineering') &&
|
|
179
|
+
!content.includes('Auto-generated by AI Context Engineering v2')) {
|
|
180
|
+
const relPath = path.relative(agentDir, filePath);
|
|
181
|
+
custom.push(relPath);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// Skip unreadable files
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
walkDir(agentDir);
|
|
191
|
+
return custom;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Backup existing files before migration
|
|
196
|
+
* @param {string} filePath - File to backup
|
|
197
|
+
* @returns {string} Backup file path
|
|
198
|
+
*/
|
|
199
|
+
function backupFile(filePath) {
|
|
200
|
+
if (!fs.existsSync(filePath)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const backupPath = filePath + '.backup.' + Date.now();
|
|
205
|
+
try {
|
|
206
|
+
fs.copyFileSync(filePath, backupPath);
|
|
207
|
+
return backupPath;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.warn(`Failed to create backup of ${filePath}: ${err.message}`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Restore from backup
|
|
216
|
+
* @param {string} backupPath - Backup file path
|
|
217
|
+
* @returns {boolean} True if restored successfully
|
|
218
|
+
*/
|
|
219
|
+
function restoreFromBackup(backupPath) {
|
|
220
|
+
if (!fs.existsSync(backupPath)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const originalPath = backupPath.replace(/\.backup\.\d+$/, '');
|
|
225
|
+
try {
|
|
226
|
+
fs.copyFileSync(backupPath, originalPath);
|
|
227
|
+
fs.unlinkSync(backupPath);
|
|
228
|
+
return true;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn(`Failed to restore from backup ${backupPath}: ${err.message}`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
findCustomContentInClaude,
|
|
237
|
+
migrateCustomContent,
|
|
238
|
+
detectCustomContent,
|
|
239
|
+
findCustomFilesInAgent,
|
|
240
|
+
determineContentType,
|
|
241
|
+
backupFile,
|
|
242
|
+
restoreFromBackup
|
|
243
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -77,7 +77,11 @@ async function run(options = {}) {
|
|
|
77
77
|
mode = 'merge', // 'merge' | 'overwrite' | 'interactive'
|
|
78
78
|
preserveCustom = true,
|
|
79
79
|
updateRefs = false,
|
|
80
|
-
backup = false
|
|
80
|
+
backup = false,
|
|
81
|
+
// Force flag
|
|
82
|
+
force = false,
|
|
83
|
+
// Placeholder validation
|
|
84
|
+
failOnUnreplaced = false
|
|
81
85
|
} = options;
|
|
82
86
|
|
|
83
87
|
// Determine target directory
|
|
@@ -267,9 +271,11 @@ async function run(options = {}) {
|
|
|
267
271
|
const placeholdersReplaced = await replacePlaceholders(targetDir, {
|
|
268
272
|
...config,
|
|
269
273
|
techStack,
|
|
270
|
-
analysis
|
|
274
|
+
analysis,
|
|
275
|
+
failOnUnreplaced: config.failOnUnreplaced,
|
|
276
|
+
verbose: config.verbose
|
|
271
277
|
});
|
|
272
|
-
spinner.succeed(`Replaced ${placeholdersReplaced} placeholders`);
|
|
278
|
+
spinner.succeed(`Replaced ${placeholdersReplaced.totalReplaced} placeholders`);
|
|
273
279
|
|
|
274
280
|
// Phase 10: AI Orchestration (if in Claude Code environment)
|
|
275
281
|
if (env.mode === 'full-ai' || env.mode === 'hybrid') {
|
|
@@ -289,7 +295,8 @@ async function run(options = {}) {
|
|
|
289
295
|
try {
|
|
290
296
|
generationResults = await generateAllContexts(analysis, config, targetDir, {
|
|
291
297
|
aiTools: config.aiTools,
|
|
292
|
-
verbose: config.verbose
|
|
298
|
+
verbose: config.verbose,
|
|
299
|
+
force: config.force || false
|
|
293
300
|
});
|
|
294
301
|
const toolsGenerated = generationResults.generated.map(g => g.adapter).join(', ');
|
|
295
302
|
if (generationResults.success) {
|
package/lib/placeholder.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { glob } = require('glob');
|
|
10
|
+
const chalk = require('chalk');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Context directory and file names
|
|
@@ -463,7 +464,16 @@ function capitalize(str) {
|
|
|
463
464
|
/**
|
|
464
465
|
* Replace placeholders in all files in a directory
|
|
465
466
|
*/
|
|
467
|
+
/**
|
|
468
|
+
* Replace all placeholders in all files
|
|
469
|
+
* @param {string} targetDir - Target directory
|
|
470
|
+
* @param {object} config - Configuration options
|
|
471
|
+
* @param {boolean} config.failOnUnreplaced - Throw if placeholders remain
|
|
472
|
+
* @param {boolean} config.verbose - Log warnings for unreplaced placeholders
|
|
473
|
+
* @returns {object} Results { totalReplaced: number, unreplaced: Array, unreplacedCount: number }
|
|
474
|
+
*/
|
|
466
475
|
async function replacePlaceholders(targetDir, config = {}) {
|
|
476
|
+
const { failOnUnreplaced = false, verbose = false } = config;
|
|
467
477
|
const contextDir = path.join(targetDir, AI_CONTEXT_DIR);
|
|
468
478
|
const values = getDefaultValues(config, config.techStack || {}, config.analysis || {});
|
|
469
479
|
|
|
@@ -476,6 +486,7 @@ async function replacePlaceholders(targetDir, config = {}) {
|
|
|
476
486
|
});
|
|
477
487
|
|
|
478
488
|
let totalReplaced = 0;
|
|
489
|
+
const unreplacedDetails = [];
|
|
479
490
|
|
|
480
491
|
for (const filePath of files) {
|
|
481
492
|
try {
|
|
@@ -492,6 +503,10 @@ async function replacePlaceholders(targetDir, config = {}) {
|
|
|
492
503
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
493
504
|
totalReplaced++;
|
|
494
505
|
}
|
|
506
|
+
|
|
507
|
+
// Check for remaining placeholders
|
|
508
|
+
const remaining = findPlaceholdersInContent(content, filePath);
|
|
509
|
+
unreplacedDetails.push(...remaining);
|
|
495
510
|
} catch (error) {
|
|
496
511
|
// Skip files that can't be read
|
|
497
512
|
}
|
|
@@ -513,12 +528,78 @@ async function replacePlaceholders(targetDir, config = {}) {
|
|
|
513
528
|
fs.writeFileSync(aiContextPath, content, 'utf8');
|
|
514
529
|
totalReplaced++;
|
|
515
530
|
}
|
|
531
|
+
|
|
532
|
+
// Check for remaining placeholders
|
|
533
|
+
const remaining = findPlaceholdersInContent(content, aiContextPath);
|
|
534
|
+
unreplacedDetails.push(...remaining);
|
|
516
535
|
} catch (error) {
|
|
517
536
|
// Skip if can't read
|
|
518
537
|
}
|
|
519
538
|
}
|
|
520
539
|
|
|
521
|
-
|
|
540
|
+
// Deduplicate unreplaced items
|
|
541
|
+
const uniqueUnreplaced = deduplicateUnreplaced(unreplacedDetails);
|
|
542
|
+
const unreplacedCount = uniqueUnreplaced.length;
|
|
543
|
+
|
|
544
|
+
// Handle unreplaced placeholders
|
|
545
|
+
if (unreplacedCount > 0) {
|
|
546
|
+
const placeholders = uniqueUnreplaced.map(u => u.placeholder).join(', ');
|
|
547
|
+
const message = `${unreplacedCount} placeholder${unreplacedCount > 1 ? 's' : ''} not replaced: ${placeholders}`;
|
|
548
|
+
|
|
549
|
+
if (verbose) {
|
|
550
|
+
console.warn(chalk.yellow(`⚠ ${message}`));
|
|
551
|
+
uniqueUnreplaced.forEach(u => {
|
|
552
|
+
console.warn(chalk.gray(` - ${u.placeholder} in ${u.file}`));
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (failOnUnreplaced) {
|
|
557
|
+
throw new Error(message);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return { totalReplaced, unreplaced: uniqueUnreplaced, unreplacedCount };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Find placeholders in content (helper for replacePlaceholders)
|
|
566
|
+
* @param {string} content - File content
|
|
567
|
+
* @param {string} filePath - File path
|
|
568
|
+
* @returns {Array} List of unreplaced placeholder info
|
|
569
|
+
*/
|
|
570
|
+
function findPlaceholdersInContent(content, filePath) {
|
|
571
|
+
const placeholderPattern = /\{\{([A-Z_]+)\}\}/g;
|
|
572
|
+
const found = [];
|
|
573
|
+
let match;
|
|
574
|
+
|
|
575
|
+
while ((match = placeholderPattern.exec(content)) !== null) {
|
|
576
|
+
found.push({
|
|
577
|
+
placeholder: match[0],
|
|
578
|
+
name: match[1],
|
|
579
|
+
file: path.relative(process.cwd(), filePath),
|
|
580
|
+
index: match.index,
|
|
581
|
+
known: KNOWN_PLACEHOLDERS.hasOwnProperty(match[1])
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return found;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Deduplicate unreplaced placeholder list
|
|
590
|
+
* @param {Array} unreplaced - List of unreplaced items
|
|
591
|
+
* @returns {Array} Deduplicated list
|
|
592
|
+
*/
|
|
593
|
+
function deduplicateUnreplaced(unreplaced) {
|
|
594
|
+
const seen = new Set();
|
|
595
|
+
return unreplaced.filter(item => {
|
|
596
|
+
const key = `${item.placeholder}:${item.file}`;
|
|
597
|
+
if (seen.has(key)) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
seen.add(key);
|
|
601
|
+
return true;
|
|
602
|
+
});
|
|
522
603
|
}
|
|
523
604
|
|
|
524
605
|
/**
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Coordination Module
|
|
3
|
+
* Helpers for adding tool awareness to generated content
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get tool coordination header
|
|
11
|
+
* @param {string} toolName - Name of the tool (claude, copilot, cline, antigravity)
|
|
12
|
+
* @param {string} version - Version of create-ai-context
|
|
13
|
+
* @returns {string} Header comment for the tool
|
|
14
|
+
*/
|
|
15
|
+
function getToolCoordinationHeader(toolName, version) {
|
|
16
|
+
version = version || '2.3.0';
|
|
17
|
+
|
|
18
|
+
const newline = '\n';
|
|
19
|
+
const htmlHeaderBase = '<!-- ========================================= -->';
|
|
20
|
+
const htmlFooterBase = '<!-- ========================================= -->';
|
|
21
|
+
const hashHeaderBase = '# ==========================================';
|
|
22
|
+
const hashFooterBase = '# ==========================================';
|
|
23
|
+
|
|
24
|
+
const headers = {
|
|
25
|
+
claude: [
|
|
26
|
+
htmlHeaderBase,
|
|
27
|
+
'<!-- WARNING: CLAUDE CODE CONTEXT -->',
|
|
28
|
+
'<!-- Managed by create-ai-context v' + version + ' -->',
|
|
29
|
+
'<!-- Source: .ai-context/ directory -->',
|
|
30
|
+
htmlFooterBase
|
|
31
|
+
].join(newline),
|
|
32
|
+
|
|
33
|
+
copilot: [
|
|
34
|
+
htmlHeaderBase,
|
|
35
|
+
'<!-- WARNING: GITHUB COPILOT INSTRUCTIONS -->',
|
|
36
|
+
'<!-- Managed by create-ai-context v' + version + ' -->',
|
|
37
|
+
'<!-- Source: .ai-context/ directory -->',
|
|
38
|
+
htmlFooterBase
|
|
39
|
+
].join(newline),
|
|
40
|
+
|
|
41
|
+
cline: [
|
|
42
|
+
hashHeaderBase,
|
|
43
|
+
'# WARNING: CLINE RULES',
|
|
44
|
+
'# Managed by create-ai-context v' + version,
|
|
45
|
+
'# Source: .ai-context/ directory',
|
|
46
|
+
hashFooterBase
|
|
47
|
+
].join(newline),
|
|
48
|
+
|
|
49
|
+
antigravity: [
|
|
50
|
+
htmlHeaderBase,
|
|
51
|
+
'<!-- WARNING: ANTIGRAVITY CONTEXT -->',
|
|
52
|
+
'<!-- Managed by create-ai-context v' + version + ' -->',
|
|
53
|
+
'<!-- Source: .ai-context/ directory -->',
|
|
54
|
+
htmlFooterBase
|
|
55
|
+
].join(newline)
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return headers[toolName] || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get tool coordination footer
|
|
63
|
+
* @param {string} toolName - Name of the tool
|
|
64
|
+
* @returns {string} Footer content with cross-tool references
|
|
65
|
+
*/
|
|
66
|
+
function getToolCoordinationFooter(toolName) {
|
|
67
|
+
const footer = '---\n' +
|
|
68
|
+
'## Universal Context Directory\n\n' +
|
|
69
|
+
'This project uses **AI Context Engineering** for coordinated documentation across all AI tools.\n\n' +
|
|
70
|
+
'**Universal Source:** .ai-context/\n\n' +
|
|
71
|
+
'**Related Tool Contexts:**\n' +
|
|
72
|
+
'- Claude Code: `AI_CONTEXT.md`\n' +
|
|
73
|
+
'- GitHub Copilot: `.github/copilot-instructions.md`\n' +
|
|
74
|
+
'- Cline: `.clinerules`\n' +
|
|
75
|
+
'- Antigravity: `.agent/`\n\n' +
|
|
76
|
+
'**Regeneration Command:**\n' +
|
|
77
|
+
'```bash\n' +
|
|
78
|
+
'npx create-ai-context generate --ai ' + toolName + '\n```\n\n' +
|
|
79
|
+
'**To Modify Documentation:**\n' +
|
|
80
|
+
'Edit source files in .ai-context/ then regenerate.\n';
|
|
81
|
+
|
|
82
|
+
return footer;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if file is managed by create-ai-context
|
|
87
|
+
* @param {string} filePath - Path to file to check
|
|
88
|
+
* @returns {boolean} True if file is managed by create-ai-context
|
|
89
|
+
*/
|
|
90
|
+
function isManagedFile(filePath) {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(filePath)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
96
|
+
return content.includes('Managed by create-ai-context') ||
|
|
97
|
+
content.includes('CREATE-AI-CONTEXT') ||
|
|
98
|
+
content.includes('Auto-generated by AI Context Engineering');
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get universal context reference for templates
|
|
106
|
+
* @returns {object} Object with context directory info
|
|
107
|
+
*/
|
|
108
|
+
function getUniversalContextReference() {
|
|
109
|
+
return {
|
|
110
|
+
directory: '.ai-context',
|
|
111
|
+
description: 'Universal AI Context Engineering directory',
|
|
112
|
+
subdirectories: {
|
|
113
|
+
agents: '.ai-context/agents/',
|
|
114
|
+
commands: '.ai-context/commands/',
|
|
115
|
+
context: '.ai-context/context/',
|
|
116
|
+
indexes: '.ai-context/indexes/',
|
|
117
|
+
custom: '.ai-context/custom/',
|
|
118
|
+
tools: '.ai-context/tools/'
|
|
119
|
+
},
|
|
120
|
+
mainFile: 'AI_CONTEXT.md',
|
|
121
|
+
workflowIndex: '.ai-context/context/WORKFLOW_INDEX.md',
|
|
122
|
+
rpiPlan: '.ai-context/RPI_WORKFLOW_PLAN.md'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format tool coordination message for CLI output
|
|
128
|
+
* @param {string} toolName - Name of the tool
|
|
129
|
+
* @returns {string} Formatted message
|
|
130
|
+
*/
|
|
131
|
+
function formatCoordinationMessage(toolName) {
|
|
132
|
+
const messages = {
|
|
133
|
+
claude: 'Claude Code context generated. Symlinks created from .ai-context/',
|
|
134
|
+
copilot: 'Copilot instructions generated from .ai-context/ universal context',
|
|
135
|
+
cline: 'Cline rules generated from .ai-context/ universal context',
|
|
136
|
+
antigravity: 'Antigravity context generated from .ai-context/ universal context'
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return messages[toolName] || toolName + ' context generated';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
getToolCoordinationHeader,
|
|
144
|
+
getToolCoordinationFooter,
|
|
145
|
+
isManagedFile,
|
|
146
|
+
getUniversalContextReference,
|
|
147
|
+
formatCoordinationMessage
|
|
148
|
+
};
|