create-universal-ai-context 2.2.2 → 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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Continue Adapter
3
+ *
4
+ * Generates .continue/config.json file for Continue extension
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: 'continue',
17
+ displayName: 'Continue',
18
+ description: 'Configuration file for Continue VS Code/JetBrains extension',
19
+ outputType: 'single-file',
20
+ outputPath: '.continue/config.json'
21
+ };
22
+
23
+ /**
24
+ * Get output path for Continue config file
25
+ * @param {string} projectRoot - Project root directory
26
+ * @returns {string} Output file path
27
+ */
28
+ function getOutputPath(projectRoot) {
29
+ return path.join(projectRoot, '.continue', 'config.json');
30
+ }
31
+
32
+ /**
33
+ * Check if Continue output already exists
34
+ * @param {string} projectRoot - Project root directory
35
+ * @returns {boolean}
36
+ */
37
+ function exists(projectRoot) {
38
+ const configPath = getOutputPath(projectRoot);
39
+ const continueDir = path.join(projectRoot, '.continue');
40
+ return fs.existsSync(configPath) || fs.existsSync(continueDir);
41
+ }
42
+
43
+ /**
44
+ * Generate Continue config 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 configPath = getOutputPath(projectRoot);
60
+
61
+ // Check if file exists and is custom (not managed by us)
62
+ if (fs.existsSync(configPath) && !config.force) {
63
+ if (!isManagedFile(configPath)) {
64
+ result.errors.push({
65
+ message: '.continue/config.json 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, 'continue');
75
+
76
+ // Render template
77
+ const content = renderTemplateByName('continue-config', context);
78
+
79
+ // Create .continue directory if it doesn't exist
80
+ const continueDir = path.dirname(configPath);
81
+ if (!fs.existsSync(continueDir)) {
82
+ fs.mkdirSync(continueDir, { recursive: true });
83
+ }
84
+
85
+ // Write output file
86
+ fs.writeFileSync(configPath, content, 'utf-8');
87
+
88
+ result.success = true;
89
+ result.files.push({
90
+ path: configPath,
91
+ relativePath: '.continue/config.json',
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 Continue output
106
+ * @param {string} projectRoot - Project root directory
107
+ * @returns {object} Validation result
108
+ */
109
+ function validate(projectRoot) {
110
+ const issues = [];
111
+ const configPath = getOutputPath(projectRoot);
112
+
113
+ if (!fs.existsSync(configPath)) {
114
+ issues.push({ file: '.continue/config.json', error: 'not found' });
115
+ } else {
116
+ const content = fs.readFileSync(configPath, 'utf-8');
117
+ const placeholderMatch = content.match(/\{\{[A-Z_]+\}\}/g);
118
+ if (placeholderMatch && placeholderMatch.length > 0) {
119
+ issues.push({
120
+ file: '.continue/config.json',
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
+ };
@@ -7,6 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { renderTemplateByName, buildContext } = require('../template-renderer');
10
+ const { isManagedFile } = require('../template-coordination');
10
11
 
11
12
  /**
12
13
  * Adapter metadata
@@ -53,14 +54,27 @@ async function generate(analysis, config, projectRoot) {
53
54
  };
54
55
 
55
56
  try {
57
+ const outputPath = getOutputPath(projectRoot);
58
+
59
+ // Check if file exists and is custom (not managed by us)
60
+ if (fs.existsSync(outputPath) && !config.force) {
61
+ if (!isManagedFile(outputPath)) {
62
+ result.errors.push({
63
+ message: '.github/copilot-instructions.md exists and appears to be custom. Use --force to overwrite.',
64
+ code: 'EXISTS_CUSTOM',
65
+ severity: 'error'
66
+ });
67
+ return result;
68
+ }
69
+ }
70
+
56
71
  // Build context from analysis
57
- const context = buildContext(analysis, config);
72
+ const context = buildContext(analysis, config, 'copilot');
58
73
 
59
74
  // Render template
60
75
  const content = renderTemplateByName('copilot', context);
61
76
 
62
77
  // Ensure .github directory exists
63
- const outputPath = getOutputPath(projectRoot);
64
78
  const outputDir = path.dirname(outputPath);
65
79
  if (!fs.existsSync(outputDir)) {
66
80
  fs.mkdirSync(outputDir, { recursive: true });
@@ -8,6 +8,9 @@ const claude = require('./claude');
8
8
  const copilot = require('./copilot');
9
9
  const cline = require('./cline');
10
10
  const antigravity = require('./antigravity');
11
+ const windsurf = require('./windsurf');
12
+ const aider = require('./aider');
13
+ const continueAdapter = require('./continue');
11
14
 
12
15
  /**
13
16
  * All available adapters
@@ -16,7 +19,10 @@ const adapters = {
16
19
  claude,
17
20
  copilot,
18
21
  cline,
19
- antigravity
22
+ antigravity,
23
+ windsurf,
24
+ aider,
25
+ continue: continueAdapter
20
26
  };
21
27
 
22
28
  /**
@@ -65,5 +71,8 @@ module.exports = {
65
71
  claude,
66
72
  copilot,
67
73
  cline,
68
- antigravity
74
+ antigravity,
75
+ windsurf,
76
+ aider,
77
+ continue: continueAdapter
69
78
  };
@@ -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
+ };
@@ -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) {