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.
@@ -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,24 +58,51 @@ 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
  }
75
102
 
76
- result.success = result.errors.length === 0 || result.errors.some(e => e.code === 'EXISTS');
103
+ // Success if no actual errors (warnings and info are OK)
104
+ result.success = result.errors.length === 0 ||
105
+ result.errors.every(e => e.code === 'EXISTS' || e.severity === 'info' || e.severity === 'warning');
77
106
  } catch (error) {
78
107
  result.errors.push({
79
108
  message: error.message,
@@ -85,59 +114,101 @@ async function generate(analysis, config, projectRoot) {
85
114
  }
86
115
 
87
116
  /**
88
- * Generate .claude/ directory with full structure
117
+ * Generate .claude/ directory with symlinks to .ai-context/
89
118
  * @param {string} projectRoot - Project root directory
90
119
  * @param {object} context - Template context
120
+ * @param {object} config - Configuration from CLI
91
121
  * @param {object} result - Result object to track files/errors
92
122
  * @returns {Array} List of generated files
93
123
  */
94
- async function generateClaudeDirectory(projectRoot, context, result) {
124
+ async function generateClaudeDirectory(projectRoot, context, config, result) {
95
125
  const { copyDirectory } = require('../installer');
96
126
  const templatesDir = path.join(__dirname, '..', '..', 'templates', 'base');
127
+ const aiContextDir = path.join(projectRoot, '.ai-context');
97
128
  const claudeDir = path.join(projectRoot, '.claude');
98
129
 
99
- // Don't overwrite existing .claude/ directory
100
- if (fs.existsSync(claudeDir)) {
101
- result.errors.push({
102
- message: '.claude/ directory already exists, skipping structure generation',
103
- code: 'EXISTS',
104
- severity: 'warning'
105
- });
106
- return [{
107
- path: claudeDir,
108
- relativePath: '.claude/',
109
- size: 0,
110
- skipped: true
111
- }];
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
112
160
  }
113
161
 
114
162
  try {
115
163
  // Create .claude/ directory
116
164
  fs.mkdirSync(claudeDir, { recursive: true });
117
165
 
118
- // Copy relevant subdirectories from templates/base to .claude/
119
- const subdirsToCopy = [
166
+ // Subdirectories to symlink from .ai-context/
167
+ const subdirsToLink = [
120
168
  'agents',
121
169
  'commands',
122
170
  'indexes',
123
171
  'context',
124
172
  'schemas',
125
- 'standards'
173
+ 'standards',
174
+ 'tools'
126
175
  ];
127
176
 
128
- // Only copy tools if explicitly enabled
129
- if (context.features?.tools !== false) {
130
- subdirsToCopy.push('tools');
131
- }
132
-
177
+ let linksCreated = 0;
133
178
  let filesCopied = 0;
134
- for (const subdir of subdirsToCopy) {
135
- const srcPath = path.join(templatesDir, subdir);
136
- if (fs.existsSync(srcPath)) {
137
- const destPath = path.join(claudeDir, subdir);
138
- fs.mkdirSync(destPath, { recursive: true });
139
- const count = await copyDirectory(srcPath, destPath);
140
- filesCopied += count;
179
+
180
+ for (const subdir of subdirsToLink) {
181
+ const srcPath = path.join(aiContextDir, subdir);
182
+ const destPath = path.join(claudeDir, subdir);
183
+
184
+ // Skip if source doesn't exist
185
+ if (!fs.existsSync(srcPath)) {
186
+ continue;
187
+ }
188
+
189
+ // Try to create symlink, fallback to copy
190
+ try {
191
+ // Remove dest if it exists (shouldn't, but safety)
192
+ if (fs.existsSync(destPath)) {
193
+ if (fs.lstatSync(destPath).isSymbolicLink()) {
194
+ fs.unlinkSync(destPath);
195
+ } else {
196
+ // Existing directory, skip
197
+ continue;
198
+ }
199
+ }
200
+
201
+ // Create symlink
202
+ fs.symlinkSync(srcPath, destPath, 'junction');
203
+ linksCreated++;
204
+ } catch (symlinkError) {
205
+ // Symlink failed (likely Windows permissions or filesystem limitation)
206
+ // Fallback: copy directory contents
207
+ if (!fs.existsSync(destPath)) {
208
+ fs.mkdirSync(destPath, { recursive: true });
209
+ const count = await copyDirectory(srcPath, destPath);
210
+ filesCopied += count;
211
+ }
141
212
  }
142
213
  }
143
214
 
@@ -145,7 +216,7 @@ async function generateClaudeDirectory(projectRoot, context, result) {
145
216
  const settingsPath = path.join(claudeDir, 'settings.json');
146
217
  const settings = {
147
218
  '$schema': './schemas/settings.schema.json',
148
- version: '2.1.0',
219
+ version: '2.2.2',
149
220
  project: {
150
221
  name: context.project?.name || 'Project',
151
222
  tech_stack: context.project?.tech_stack || 'Not detected'
@@ -170,7 +241,26 @@ async function generateClaudeDirectory(projectRoot, context, result) {
170
241
  const readmePath = path.join(claudeDir, 'README.md');
171
242
  const readme = `# .claude Configuration - ${context.project?.name || 'Project'}
172
243
 
173
- This directory contains Claude Code-specific context engineering files.
244
+ This directory provides Claude Code with auto-discovered commands, agents, and configuration.
245
+
246
+ ## Architecture
247
+
248
+ This directory uses **symlinks** to \`../.ai-context/\` for all shared content:
249
+
250
+ \`\`\`
251
+ .claude/
252
+ ├── agents → ../.ai-context/agents/
253
+ ├── commands → ../.ai-context/commands/
254
+ ├── indexes → ../.ai-context/indexes/
255
+ ├── context → ../.ai-context/context/
256
+ ├── schemas → ../.ai-context/schemas/
257
+ ├── standards → ../.ai-context/standards/
258
+ ├── tools → ../.ai-context/tools/
259
+ ├── settings.json (this file - Claude-specific)
260
+ └── README.md (this file)
261
+ \`\`\`
262
+
263
+ **Single source of truth:** All content lives in \`.ai-context/\`. Edit there, not here.
174
264
 
175
265
  ## Quick Start
176
266
 
@@ -182,22 +272,26 @@ This directory contains Claude Code-specific context engineering files.
182
272
 
183
273
  See \`AI_CONTEXT.md\` at project root for universal AI context (works with all tools).
184
274
 
185
- ## Claude-Specific Files
186
-
187
- - **agents/** - Specialized agents for different tasks
188
- - **commands/** - Custom slash commands
189
- - **indexes/** - 3-level navigation system
190
- - **context/** - Workflow documentation
191
-
192
- *Generated by create-universal-ai-context v${context.version || '2.1.0'}*
275
+ *Generated by create-universal-ai-context v${context.version || '2.3.0'}*
193
276
  `;
194
277
  fs.writeFileSync(readmePath, readme);
195
278
  filesCopied++;
196
279
 
280
+ // Add info message about symlink approach
281
+ if (linksCreated > 0) {
282
+ result.errors.push({
283
+ message: `Created ${linksCreated} symlinks from .claude/ to .ai-context/ (single source of truth)`,
284
+ code: 'SYMLINKS_CREATED',
285
+ severity: 'info'
286
+ });
287
+ }
288
+
197
289
  return [{
198
290
  path: claudeDir,
199
291
  relativePath: '.claude/',
200
- size: filesCopied
292
+ size: filesCopied,
293
+ symlinks: linksCreated,
294
+ details: `${linksCreated} symlinks, ${filesCopied} files`
201
295
  }];
202
296
 
203
297
  } catch (error) {
@@ -209,6 +303,36 @@ See \`AI_CONTEXT.md\` at project root for universal AI context (works with all t
209
303
  }
210
304
  }
211
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
+
212
336
  /**
213
337
  * Validate Claude output
214
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;