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.
@@ -89,6 +89,8 @@ program
89
89
  .option('--preserve-custom', 'Keep user customizations when merging (default: true)', true)
90
90
  .option('--update-refs', 'Auto-fix drifted line references')
91
91
  .option('--backup', 'Create backup before modifying existing files')
92
+ .option('-f, --force', 'Force overwrite of existing custom files (use with caution)')
93
+ .option('--fail-on-unreplaced', 'Error if any placeholders remain unreplaced')
92
94
  .action(async (projectName, options) => {
93
95
  console.log(banner);
94
96
 
@@ -118,7 +120,10 @@ program
118
120
  mode: options.mode,
119
121
  preserveCustom: options.preserveCustom,
120
122
  updateRefs: options.updateRefs,
121
- backup: options.backup
123
+ backup: options.backup,
124
+ force: options.force || false,
125
+ // Placeholder validation
126
+ failOnUnreplaced: options.failOnUnreplaced || false
122
127
  });
123
128
  } catch (error) {
124
129
  console.error(chalk.red('\n✖ Error:'), error.message);
@@ -137,6 +142,7 @@ program
137
142
  .option('-d, --dryRun', 'Show what would be done without making changes')
138
143
  .option('-v, --verbose', 'Show detailed output')
139
144
  .option('-p, --path <dir>', 'Project directory (defaults to current)', '.')
145
+ .option('-f, --force', 'Force overwrite of existing custom files (use with caution)')
140
146
  .action(async (options) => {
141
147
  console.log(banner);
142
148
 
@@ -169,7 +175,8 @@ program
169
175
  const config = {
170
176
  projectName: path.basename(projectRoot),
171
177
  aiTools,
172
- verbose: options.verbose
178
+ verbose: options.verbose,
179
+ force: options.force || false
173
180
  };
174
181
 
175
182
  if (options.dryRun) {
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Aider Adapter
3
+ *
4
+ * Generates .aider.conf.yml file for Aider AI pair-programmer
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: 'aider',
17
+ displayName: 'Aider',
18
+ description: 'Configuration file for Aider AI pair-programmer',
19
+ outputType: 'single-file',
20
+ outputPath: '.aider.conf.yml'
21
+ };
22
+
23
+ /**
24
+ * Get output path for Aider 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, '.aider.conf.yml');
30
+ }
31
+
32
+ /**
33
+ * Check if Aider output already exists
34
+ * @param {string} projectRoot - Project root directory
35
+ * @returns {boolean}
36
+ */
37
+ function exists(projectRoot) {
38
+ const configPath = getOutputPath(projectRoot);
39
+ return fs.existsSync(configPath);
40
+ }
41
+
42
+ /**
43
+ * Generate Aider config file
44
+ * @param {object} analysis - Analysis results from static analyzer
45
+ * @param {object} config - Configuration from CLI
46
+ * @param {string} projectRoot - Project root directory
47
+ * @returns {object} Generation result
48
+ */
49
+ async function generate(analysis, config, projectRoot) {
50
+ const result = {
51
+ success: false,
52
+ adapter: adapter.name,
53
+ files: [],
54
+ errors: []
55
+ };
56
+
57
+ try {
58
+ const configPath = getOutputPath(projectRoot);
59
+
60
+ // Check if file exists and is custom (not managed by us)
61
+ if (fs.existsSync(configPath) && !config.force) {
62
+ if (!isManagedFile(configPath)) {
63
+ result.errors.push({
64
+ message: '.aider.conf.yml exists and appears to be custom. Use --force to overwrite.',
65
+ code: 'EXISTS_CUSTOM',
66
+ severity: 'error'
67
+ });
68
+ return result;
69
+ }
70
+ }
71
+
72
+ // Build context from analysis
73
+ const context = buildContext(analysis, config, 'aider');
74
+
75
+ // Render template
76
+ const content = renderTemplateByName('aider-config', context);
77
+
78
+ // Write output file
79
+ fs.writeFileSync(configPath, content, 'utf-8');
80
+
81
+ result.success = true;
82
+ result.files.push({
83
+ path: configPath,
84
+ relativePath: '.aider.conf.yml',
85
+ size: content.length
86
+ });
87
+ } catch (error) {
88
+ result.errors.push({
89
+ message: error.message,
90
+ stack: error.stack
91
+ });
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Validate Aider output
99
+ * @param {string} projectRoot - Project root directory
100
+ * @returns {object} Validation result
101
+ */
102
+ function validate(projectRoot) {
103
+ const issues = [];
104
+ const configPath = getOutputPath(projectRoot);
105
+
106
+ if (!fs.existsSync(configPath)) {
107
+ issues.push({ file: '.aider.conf.yml', error: 'not found' });
108
+ } else {
109
+ const content = fs.readFileSync(configPath, 'utf-8');
110
+ const placeholderMatch = content.match(/\{\{[A-Z_]+\}\}/g);
111
+ if (placeholderMatch && placeholderMatch.length > 0) {
112
+ issues.push({
113
+ file: '.aider.conf.yml',
114
+ error: `Found ${placeholderMatch.length} unreplaced placeholders`
115
+ });
116
+ }
117
+ }
118
+
119
+ return {
120
+ valid: issues.filter(i => i.severity !== 'warning').length === 0,
121
+ issues
122
+ };
123
+ }
124
+
125
+ module.exports = {
126
+ ...adapter,
127
+ getOutputPath,
128
+ exists,
129
+ generate,
130
+ validate
131
+ };
@@ -7,6 +7,7 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { renderMultiFileTemplate, buildContext } = require('../template-renderer');
10
+ const { isManagedFile } = require('../template-coordination');
10
11
 
11
12
  /**
12
13
  * Adapter metadata
@@ -53,8 +54,23 @@ async function generate(analysis, config, projectRoot) {
53
54
  };
54
55
 
55
56
  try {
57
+ const outputDir = getOutputPath(projectRoot);
58
+
59
+ // Check if .agent/ directory exists and contains custom files
60
+ if (fs.existsSync(outputDir) && !config.force) {
61
+ const hasCustomFiles = checkForCustomFiles(outputDir);
62
+ if (hasCustomFiles) {
63
+ result.errors.push({
64
+ message: '.agent/ directory exists and contains custom files. Use --force to overwrite.',
65
+ code: 'EXISTS_CUSTOM',
66
+ severity: 'error'
67
+ });
68
+ return result;
69
+ }
70
+ }
71
+
56
72
  // Build context from analysis
57
- const context = buildContext(analysis, config);
73
+ const context = buildContext(analysis, config, 'antigravity');
58
74
 
59
75
  // Get template path
60
76
  const templatePath = path.join(__dirname, '..', '..', 'templates', 'handlebars', 'antigravity.hbs');
@@ -63,7 +79,6 @@ async function generate(analysis, config, projectRoot) {
63
79
  const files = renderMultiFileTemplate(templatePath, context);
64
80
 
65
81
  // Create output directory
66
- const outputDir = getOutputPath(projectRoot);
67
82
  if (!fs.existsSync(outputDir)) {
68
83
  fs.mkdirSync(outputDir, { recursive: true });
69
84
  }
@@ -99,6 +114,36 @@ async function generate(analysis, config, projectRoot) {
99
114
  return result;
100
115
  }
101
116
 
117
+ /**
118
+ * Check if directory contains custom (non-managed) files
119
+ * @param {string} dir - Directory to check
120
+ * @returns {boolean} True if custom files found
121
+ */
122
+ function checkForCustomFiles(dir) {
123
+ const walkDir = (currentDir, depth = 0) => {
124
+ if (depth > 10) return false;
125
+
126
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
127
+ for (const entry of entries) {
128
+ if (entry.isDirectory()) {
129
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
130
+ if (walkDir(path.join(currentDir, entry.name), depth + 1)) {
131
+ return true;
132
+ }
133
+ }
134
+ } else if (entry.name.endsWith('.md')) {
135
+ const filePath = path.join(currentDir, entry.name);
136
+ if (!isManagedFile(filePath)) {
137
+ return true;
138
+ }
139
+ }
140
+ }
141
+ return false;
142
+ };
143
+
144
+ return walkDir(dir);
145
+ }
146
+
102
147
  /**
103
148
  * Validate Antigravity output
104
149
  * @param {string} projectRoot - Project root directory
@@ -8,6 +8,8 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { renderTemplateByName, buildContext } = require('../template-renderer');
11
+ const { isManagedFile } = require('../template-coordination');
12
+ const { findCustomContentInClaude, migrateCustomContent } = require('../content-preservation');
11
13
 
12
14
  /**
13
15
  * Adapter metadata
@@ -56,19 +58,44 @@ async function generate(analysis, config, projectRoot) {
56
58
  };
57
59
 
58
60
  try {
59
- // 1. Generate AI_CONTEXT.md at project root (existing behavior)
60
- const context = buildContext(analysis, config);
61
- const content = renderTemplateByName('claude', context);
61
+ // 1. Generate AI_CONTEXT.md at project root
62
62
  const outputPath = getOutputPath(projectRoot);
63
- fs.writeFileSync(outputPath, content, 'utf-8');
64
- result.files.push({
65
- path: outputPath,
66
- relativePath: 'AI_CONTEXT.md',
67
- size: content.length
68
- });
69
63
 
70
- // 2. Generate .claude/ directory structure (NEW)
71
- const claudeDirResult = await generateClaudeDirectory(projectRoot, context, result);
64
+ // Check if file exists and is custom (not managed by us)
65
+ if (fs.existsSync(outputPath) && !config.force) {
66
+ if (!isManagedFile(outputPath)) {
67
+ result.errors.push({
68
+ message: 'AI_CONTEXT.md exists and appears to be custom. Use --force to overwrite.',
69
+ code: 'EXISTS_CUSTOM',
70
+ severity: 'error'
71
+ });
72
+ // Don't return early - still try to generate .claude/ directory
73
+ } else {
74
+ // File is managed by us, safe to overwrite
75
+ const context = buildContext(analysis, config, 'claude');
76
+ const content = renderTemplateByName('claude', context);
77
+ fs.writeFileSync(outputPath, content, 'utf-8');
78
+ result.files.push({
79
+ path: outputPath,
80
+ relativePath: 'AI_CONTEXT.md',
81
+ size: content.length
82
+ });
83
+ }
84
+ } else {
85
+ // File doesn't exist or force is enabled
86
+ const context = buildContext(analysis, config, 'claude');
87
+ const content = renderTemplateByName('claude', context);
88
+ fs.writeFileSync(outputPath, content, 'utf-8');
89
+ result.files.push({
90
+ path: outputPath,
91
+ relativePath: 'AI_CONTEXT.md',
92
+ size: content.length
93
+ });
94
+ }
95
+
96
+ // 2. Generate .claude/ directory structure
97
+ const context = buildContext(analysis, config, 'claude');
98
+ const claudeDirResult = await generateClaudeDirectory(projectRoot, context, config, result);
72
99
  if (claudeDirResult) {
73
100
  result.files.push(...claudeDirResult);
74
101
  }
@@ -90,28 +117,46 @@ async function generate(analysis, config, projectRoot) {
90
117
  * Generate .claude/ directory with symlinks to .ai-context/
91
118
  * @param {string} projectRoot - Project root directory
92
119
  * @param {object} context - Template context
120
+ * @param {object} config - Configuration from CLI
93
121
  * @param {object} result - Result object to track files/errors
94
122
  * @returns {Array} List of generated files
95
123
  */
96
- async function generateClaudeDirectory(projectRoot, context, result) {
124
+ async function generateClaudeDirectory(projectRoot, context, config, result) {
97
125
  const { copyDirectory } = require('../installer');
98
126
  const templatesDir = path.join(__dirname, '..', '..', 'templates', 'base');
99
127
  const aiContextDir = path.join(projectRoot, '.ai-context');
100
128
  const claudeDir = path.join(projectRoot, '.claude');
101
129
 
102
- // Don't overwrite existing .claude/ directory
103
- if (fs.existsSync(claudeDir)) {
104
- result.errors.push({
105
- message: '.claude/ directory already exists, skipping structure generation',
106
- code: 'EXISTS',
107
- severity: 'warning'
108
- });
109
- return [{
110
- path: claudeDir,
111
- relativePath: '.claude/',
112
- size: 0,
113
- skipped: true
114
- }];
130
+ // Check for existing .claude/ directory
131
+ if (fs.existsSync(claudeDir) && !config.force) {
132
+ // Check if it has custom files
133
+ const hasCustomFiles = checkForCustomFiles(claudeDir);
134
+ if (hasCustomFiles) {
135
+ // Migrate custom content before skipping
136
+ const customItems = findCustomContentInClaude(claudeDir);
137
+ if (customItems.length > 0) {
138
+ const migrated = migrateCustomContent(claudeDir, aiContextDir, customItems);
139
+ result.errors.push({
140
+ message: `Migrated ${migrated.length} custom items from .claude/ to .ai-context/custom/`,
141
+ code: 'MIGRATED_CUSTOM',
142
+ severity: 'info',
143
+ migratedItems: migrated
144
+ });
145
+ }
146
+
147
+ result.errors.push({
148
+ message: '.claude/ directory exists and contains custom files. Use --force to overwrite. Skipping directory generation.',
149
+ code: 'EXISTS_CUSTOM',
150
+ severity: 'warning'
151
+ });
152
+ return [{
153
+ path: claudeDir,
154
+ relativePath: '.claude/',
155
+ size: 0,
156
+ skipped: true
157
+ }];
158
+ }
159
+ // Directory exists but only has managed files, we can regenerate
115
160
  }
116
161
 
117
162
  try {
@@ -258,6 +303,36 @@ See \`AI_CONTEXT.md\` at project root for universal AI context (works with all t
258
303
  }
259
304
  }
260
305
 
306
+ /**
307
+ * Check if directory contains custom (non-managed) files
308
+ * @param {string} dir - Directory to check
309
+ * @returns {boolean} True if custom files found
310
+ */
311
+ function checkForCustomFiles(dir) {
312
+ const walkDir = (currentDir, depth = 0) => {
313
+ if (depth > 10) return false;
314
+
315
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
316
+ for (const entry of entries) {
317
+ if (entry.isDirectory()) {
318
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
319
+ if (walkDir(path.join(currentDir, entry.name), depth + 1)) {
320
+ return true;
321
+ }
322
+ }
323
+ } else if (entry.name.endsWith('.md')) {
324
+ const filePath = path.join(currentDir, entry.name);
325
+ if (!isManagedFile(filePath)) {
326
+ return true;
327
+ }
328
+ }
329
+ }
330
+ return false;
331
+ };
332
+
333
+ return walkDir(dir);
334
+ }
335
+
261
336
  /**
262
337
  * Validate Claude output
263
338
  * @param {string} projectRoot - Project root directory
@@ -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: '.clinerules 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, 'cline');
58
73
 
59
74
  // Render template
60
75
  const content = renderTemplateByName('cline', context);
61
76
 
62
77
  // Write output file
63
- const outputPath = getOutputPath(projectRoot);
64
78
  fs.writeFileSync(outputPath, content, 'utf-8');
65
79
 
66
80
  result.success = true;
@@ -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
  };