ai-nexus 1.3.20 → 1.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/README.ko.md +7 -28
- package/README.md +7 -28
- package/bin/ai-rules.cjs +0 -2
- package/dist/commands/doctor.js +4 -5
- package/dist/commands/init-interactive.js +22 -53
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +18 -26
- package/dist/commands/update.js +219 -119
- package/dist/types.d.ts +1 -1
- package/dist/utils/files.d.ts +0 -1
- package/dist/utils/files.js +0 -7
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -199,35 +199,13 @@ description: 이 룰을 로드할 시점 (시맨틱 라우터가 사용)
|
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
202
|
-
##
|
|
202
|
+
## 업데이트 & 로컬 우선
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
npx ai-nexus install
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
- 룰이 소스에 링크 → `update`로 즉시 동기화
|
|
211
|
-
- 룰을 직접 수정 불가 (소스에서 수정)
|
|
212
|
-
|
|
213
|
-
### Copy
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
npx ai-nexus install --copy
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
- 룰이 독립적인 복사본
|
|
220
|
-
- 로컬에서 자유롭게 수정 가능
|
|
221
|
-
- `update`는 새 파일만 추가, 기존 파일 덮어쓰지 않음
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 로컬 우선
|
|
226
|
-
|
|
227
|
-
사용자의 커스터마이징은 항상 안전합니다:
|
|
204
|
+
룰은 독립적인 복사본으로 설치됩니다. 사용자의 커스터마이징은 항상 안전합니다:
|
|
228
205
|
|
|
229
206
|
- **기존 파일은 절대 덮어쓰지 않음** (install, update 모두)
|
|
230
207
|
- 소스에서 새 파일만 추가
|
|
208
|
+
- `npx ai-nexus update`로 최신 패키지의 새 룰을 동기화
|
|
231
209
|
- `--force`로 강제 덮어쓰기 (백업 먼저!)
|
|
232
210
|
|
|
233
211
|
```bash
|
|
@@ -238,6 +216,8 @@ npx ai-nexus update
|
|
|
238
216
|
npx ai-nexus update --force
|
|
239
217
|
```
|
|
240
218
|
|
|
219
|
+
> **symlink 모드에서 마이그레이션?** `npx ai-nexus update`만 실행하면 symlink이 자동으로 복사본으로 변환됩니다.
|
|
220
|
+
|
|
241
221
|
---
|
|
242
222
|
|
|
243
223
|
## 디렉토리 구조
|
|
@@ -251,8 +231,8 @@ npx ai-nexus update --force
|
|
|
251
231
|
.claude/ # Claude Code
|
|
252
232
|
├── hooks/semantic-router.cjs
|
|
253
233
|
├── settings.json
|
|
254
|
-
├── rules/
|
|
255
|
-
└── commands/
|
|
234
|
+
├── rules/ # .ai-nexus/config/rules에서 복사
|
|
235
|
+
└── commands/ # .ai-nexus/config/commands에서 복사
|
|
256
236
|
|
|
257
237
|
.cursor/rules/ # Cursor (.mdc 파일)
|
|
258
238
|
├── essential.mdc
|
|
@@ -272,7 +252,6 @@ npx ai-nexus install
|
|
|
272
252
|
# 선택: Claude Code, Cursor
|
|
273
253
|
# 선택: rules, commands, hooks, settings
|
|
274
254
|
# 템플릿: React/Next.js
|
|
275
|
-
# 모드: symlink
|
|
276
255
|
```
|
|
277
256
|
|
|
278
257
|
### 팀 설정
|
package/README.md
CHANGED
|
@@ -199,35 +199,13 @@ Your rule content...
|
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
202
|
-
##
|
|
202
|
+
## Update & Local Priority
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
npx ai-nexus install
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
- Rules link to source → `update` syncs instantly
|
|
211
|
-
- Cannot edit rules directly (edit source instead)
|
|
212
|
-
|
|
213
|
-
### Copy
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
npx ai-nexus install --copy
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
- Rules are independent copies
|
|
220
|
-
- Can edit locally
|
|
221
|
-
- `update` only adds new files, never overwrites
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## Local Priority
|
|
226
|
-
|
|
227
|
-
Your customizations are always safe:
|
|
204
|
+
Rules are installed as independent copies. Your customizations are always safe:
|
|
228
205
|
|
|
229
206
|
- **Existing files are never overwritten** during install or update
|
|
230
207
|
- Only new files from source are added
|
|
208
|
+
- `npx ai-nexus update` syncs new rules from the latest package
|
|
231
209
|
- Use `--force` to override (backup first!)
|
|
232
210
|
|
|
233
211
|
```bash
|
|
@@ -238,6 +216,8 @@ npx ai-nexus update
|
|
|
238
216
|
npx ai-nexus update --force
|
|
239
217
|
```
|
|
240
218
|
|
|
219
|
+
> **Migrating from symlink mode?** Just run `npx ai-nexus update` — symlinks are automatically converted to copies.
|
|
220
|
+
|
|
241
221
|
---
|
|
242
222
|
|
|
243
223
|
## Directory Structure
|
|
@@ -251,8 +231,8 @@ npx ai-nexus update --force
|
|
|
251
231
|
.claude/ # Claude Code
|
|
252
232
|
├── hooks/semantic-router.cjs
|
|
253
233
|
├── settings.json
|
|
254
|
-
├── rules/
|
|
255
|
-
└── commands/
|
|
234
|
+
├── rules/ # Copied from .ai-nexus/config/rules
|
|
235
|
+
└── commands/ # Copied from .ai-nexus/config/commands
|
|
256
236
|
|
|
257
237
|
.cursor/rules/ # Cursor (.mdc files)
|
|
258
238
|
├── essential.mdc
|
|
@@ -272,7 +252,6 @@ npx ai-nexus install
|
|
|
272
252
|
# Select: Claude Code, Cursor
|
|
273
253
|
# Select: rules, commands, hooks, settings
|
|
274
254
|
# Template: React/Next.js
|
|
275
|
-
# Mode: symlink
|
|
276
255
|
```
|
|
277
256
|
|
|
278
257
|
### Team Setup
|
package/bin/ai-rules.cjs
CHANGED
|
@@ -16,7 +16,6 @@ program
|
|
|
16
16
|
.command('init')
|
|
17
17
|
.description('Initialize rules in current project (.claude/, .codex/)')
|
|
18
18
|
.option('--rules <url>', 'Git repository URL for rules (e.g., github.com/org/my-rules)')
|
|
19
|
-
.option('--copy', 'Copy files instead of symlink')
|
|
20
19
|
.option('-q, --quick', 'Quick mode (skip interactive wizard)')
|
|
21
20
|
.option('-y, --yes', 'Skip prompts and use defaults')
|
|
22
21
|
.action(async (options) => {
|
|
@@ -33,7 +32,6 @@ program
|
|
|
33
32
|
.command('install')
|
|
34
33
|
.description('Install rules globally (~/.claude/, ~/.codex/)')
|
|
35
34
|
.option('--rules <url>', 'Git repository URL for rules')
|
|
36
|
-
.option('--copy', 'Copy files instead of symlink')
|
|
37
35
|
.option('-q, --quick', 'Quick mode (skip interactive wizard)')
|
|
38
36
|
.action(async (options) => {
|
|
39
37
|
if (options.quick || options.rules) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -15,15 +15,14 @@ export async function doctor() {
|
|
|
15
15
|
status: 'ok',
|
|
16
16
|
message: `Found ${install.scope} installation at ${install.configPath}`,
|
|
17
17
|
});
|
|
18
|
-
// Check mode
|
|
19
18
|
const metaPath = path.join(install.configPath, 'meta.json');
|
|
20
19
|
if (fs.existsSync(metaPath)) {
|
|
21
20
|
try {
|
|
22
|
-
|
|
21
|
+
JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
23
22
|
results.push({
|
|
24
|
-
name: '
|
|
23
|
+
name: 'Metadata',
|
|
25
24
|
status: 'ok',
|
|
26
|
-
message:
|
|
25
|
+
message: 'meta.json valid',
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
catch {
|
|
@@ -84,7 +83,7 @@ export async function doctor() {
|
|
|
84
83
|
name: dir.label,
|
|
85
84
|
status: 'warn',
|
|
86
85
|
message: `Missing: ${missing.join(', ')}`,
|
|
87
|
-
fix: 'Run: ai-nexus install
|
|
86
|
+
fix: 'Run: ai-nexus install',
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
89
|
}
|
|
@@ -5,7 +5,7 @@ import { createRequire } from 'module';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import ora from 'ora';
|
|
8
|
-
import { getTargetDir, getConfigPath, ensureDir,
|
|
8
|
+
import { getTargetDir, getConfigPath, ensureDir, scanDir, computeFileHashes, aggregateToAgentsMd, } from '../utils/files.js';
|
|
9
9
|
import { scanConfigDir } from '../utils/config-scanner.js';
|
|
10
10
|
// Convert .md to .mdc format for Cursor
|
|
11
11
|
function convertToMdc(content, filename) {
|
|
@@ -135,30 +135,11 @@ export async function initInteractive() {
|
|
|
135
135
|
choices: TEMPLATES,
|
|
136
136
|
},
|
|
137
137
|
]);
|
|
138
|
-
// Step 6:
|
|
139
|
-
const { method } = await inquirer.prompt([
|
|
140
|
-
{
|
|
141
|
-
type: 'list',
|
|
142
|
-
name: 'method',
|
|
143
|
-
message: 'Select install method',
|
|
144
|
-
choices: [
|
|
145
|
-
{
|
|
146
|
-
name: '🔗 symlink (auto-update via ai-nexus update)',
|
|
147
|
-
value: 'symlink',
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: '📄 copy (independent copy)',
|
|
151
|
-
value: 'copy',
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
},
|
|
155
|
-
]);
|
|
156
|
-
// Step 7: Confirmation
|
|
138
|
+
// Step 6: Confirmation
|
|
157
139
|
console.log(chalk.cyan('\n📋 Installation Summary\n'));
|
|
158
140
|
console.log(chalk.gray('─'.repeat(40)));
|
|
159
141
|
console.log(` Scope: ${scope === 'global' ? 'Global (~/)' : 'Project (./)'}`);
|
|
160
142
|
console.log(` Tools: ${tools.join(', ')}`);
|
|
161
|
-
console.log(` Method: ${method === 'symlink' ? 'symlink' : 'copy'}`);
|
|
162
143
|
console.log(` Template: ${template || 'None'}`);
|
|
163
144
|
console.log(` Categories:`);
|
|
164
145
|
let totalFiles = 0;
|
|
@@ -190,7 +171,6 @@ export async function initInteractive() {
|
|
|
190
171
|
categories,
|
|
191
172
|
selectedFiles,
|
|
192
173
|
template,
|
|
193
|
-
method,
|
|
194
174
|
});
|
|
195
175
|
spinner.succeed('Installation complete!');
|
|
196
176
|
}
|
|
@@ -212,16 +192,12 @@ export async function initInteractive() {
|
|
|
212
192
|
if (tools.includes('cursor')) {
|
|
213
193
|
console.log(` Cursor: ${path.join(targetDir, '.cursor/rules')}`);
|
|
214
194
|
}
|
|
215
|
-
console.log(` Mode: ${method}`);
|
|
216
195
|
if (template) {
|
|
217
196
|
console.log(` Template: ${template}`);
|
|
218
197
|
}
|
|
219
198
|
console.log(chalk.gray('─'.repeat(40)));
|
|
220
199
|
// Next steps
|
|
221
200
|
console.log(chalk.cyan('\n📋 Next Steps:\n'));
|
|
222
|
-
if (method === 'symlink') {
|
|
223
|
-
console.log(chalk.white(' 1. Run "ai-nexus update" to sync latest rules'));
|
|
224
|
-
}
|
|
225
201
|
const hasApiKey = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
226
202
|
if (!hasApiKey && tools.includes('claude') && categories.includes('hooks')) {
|
|
227
203
|
console.log(chalk.white(' 2. Enable AI-powered rule selection (optional):'));
|
|
@@ -235,7 +211,7 @@ export async function initInteractive() {
|
|
|
235
211
|
console.log();
|
|
236
212
|
}
|
|
237
213
|
async function install(selections) {
|
|
238
|
-
const { scope, tools, categories, selectedFiles, template
|
|
214
|
+
const { scope, tools, categories, selectedFiles, template } = selections;
|
|
239
215
|
const targetDir = getTargetDir(scope);
|
|
240
216
|
const aiRulesDir = getConfigPath(scope);
|
|
241
217
|
const configDir = path.join(aiRulesDir, 'config');
|
|
@@ -298,42 +274,36 @@ async function install(selections) {
|
|
|
298
274
|
const targetPath = path.join(toolDir, category);
|
|
299
275
|
if (!fs.existsSync(sourceDir))
|
|
300
276
|
continue;
|
|
301
|
-
// Local priority: skip if directory exists
|
|
277
|
+
// Local priority: skip if directory already exists
|
|
302
278
|
if (fs.existsSync(targetPath)) {
|
|
303
279
|
try {
|
|
304
280
|
const stat = fs.lstatSync(targetPath);
|
|
305
|
-
if (
|
|
281
|
+
if (stat.isSymbolicLink()) {
|
|
282
|
+
// Remove existing symlink to replace with copy
|
|
283
|
+
fs.unlinkSync(targetPath);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
306
286
|
// Existing directory - only add new files
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
fs.writeFileSync(dest, content);
|
|
314
|
-
}
|
|
287
|
+
const files = scanDir(sourceDir);
|
|
288
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
289
|
+
const dest = path.join(targetPath, rel);
|
|
290
|
+
if (!fs.existsSync(dest)) {
|
|
291
|
+
ensureDir(path.dirname(dest));
|
|
292
|
+
fs.writeFileSync(dest, content);
|
|
315
293
|
}
|
|
316
294
|
}
|
|
317
295
|
continue;
|
|
318
296
|
}
|
|
319
|
-
// Symlink - remove and recreate
|
|
320
|
-
fs.unlinkSync(targetPath);
|
|
321
297
|
}
|
|
322
298
|
catch {
|
|
323
299
|
// Ignore errors
|
|
324
300
|
}
|
|
325
301
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const files = scanDir(sourceDir);
|
|
332
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
333
|
-
const dest = path.join(targetPath, rel);
|
|
334
|
-
ensureDir(path.dirname(dest));
|
|
335
|
-
fs.writeFileSync(dest, content);
|
|
336
|
-
}
|
|
302
|
+
const files = scanDir(sourceDir);
|
|
303
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
304
|
+
const dest = path.join(targetPath, rel);
|
|
305
|
+
ensureDir(path.dirname(dest));
|
|
306
|
+
fs.writeFileSync(dest, content);
|
|
337
307
|
}
|
|
338
308
|
}
|
|
339
309
|
// Install hooks for Claude Code
|
|
@@ -386,16 +356,15 @@ async function install(selections) {
|
|
|
386
356
|
}
|
|
387
357
|
}
|
|
388
358
|
}
|
|
389
|
-
// Save metadata
|
|
359
|
+
// Save metadata
|
|
390
360
|
const claudeDir = path.join(targetDir, '.claude');
|
|
391
361
|
const meta = {
|
|
392
362
|
version: require(path.join(PACKAGE_ROOT, 'package.json')).version,
|
|
393
|
-
mode: method,
|
|
394
363
|
tools,
|
|
395
364
|
template,
|
|
396
365
|
sources: [{ name: 'builtin', type: 'builtin' }],
|
|
397
366
|
selectedFiles,
|
|
398
|
-
|
|
367
|
+
fileHashes: computeFileHashes(claudeDir),
|
|
399
368
|
createdAt: new Date().toISOString(),
|
|
400
369
|
updatedAt: new Date().toISOString(),
|
|
401
370
|
};
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -3,17 +3,16 @@ import path from 'path';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import { getTargetDir, getConfigPath, ensureDir,
|
|
6
|
+
import { getTargetDir, getConfigPath, ensureDir, scanDir, computeFileHashes, } from '../utils/files.js';
|
|
7
7
|
import { cloneRepo, getRepoName, normalizeGitUrl } from '../utils/git.js';
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
export async function init(options) {
|
|
12
|
-
const { scope, rules: rulesUrl
|
|
12
|
+
const { scope, rules: rulesUrl } = options;
|
|
13
13
|
const targetDir = getTargetDir(scope);
|
|
14
14
|
const aiRulesDir = getConfigPath(scope);
|
|
15
15
|
const configDir = path.join(aiRulesDir, 'config');
|
|
16
|
-
const mode = copyMode ? 'copy' : 'symlink';
|
|
17
16
|
console.log(chalk.bold(`\n ai-nexus ${scope === 'global' ? 'global' : 'project'} setup\n`));
|
|
18
17
|
// Create .ai-nexus directory
|
|
19
18
|
ensureDir(aiRulesDir);
|
|
@@ -66,35 +65,30 @@ export async function init(options) {
|
|
|
66
65
|
if (!fs.existsSync(sourceDir))
|
|
67
66
|
continue;
|
|
68
67
|
const fileCount = countFiles(sourceDir);
|
|
69
|
-
// Local priority: skip if directory exists
|
|
68
|
+
// Local priority: skip if directory already exists
|
|
70
69
|
if (fs.existsSync(targetPath)) {
|
|
71
70
|
try {
|
|
72
71
|
const stat = fs.lstatSync(targetPath);
|
|
73
|
-
if (
|
|
72
|
+
if (stat.isSymbolicLink()) {
|
|
73
|
+
// Remove existing symlink to replace with copy
|
|
74
|
+
fs.unlinkSync(targetPath);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
74
77
|
results.push({ name: category, action: 'skipped', fileCount });
|
|
75
78
|
continue;
|
|
76
79
|
}
|
|
77
|
-
// Remove existing symlink to recreate
|
|
78
|
-
fs.unlinkSync(targetPath);
|
|
79
80
|
}
|
|
80
81
|
catch {
|
|
81
82
|
// Ignore errors
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// Copy mode
|
|
90
|
-
const files = scanDir(sourceDir);
|
|
91
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
92
|
-
const dest = path.join(targetPath, rel);
|
|
93
|
-
ensureDir(path.dirname(dest));
|
|
94
|
-
fs.writeFileSync(dest, content);
|
|
95
|
-
}
|
|
96
|
-
results.push({ name: category, action: 'copied', fileCount });
|
|
85
|
+
const files = scanDir(sourceDir);
|
|
86
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
87
|
+
const dest = path.join(targetPath, rel);
|
|
88
|
+
ensureDir(path.dirname(dest));
|
|
89
|
+
fs.writeFileSync(dest, content);
|
|
97
90
|
}
|
|
91
|
+
results.push({ name: category, action: 'copied', fileCount });
|
|
98
92
|
}
|
|
99
93
|
// Install hooks
|
|
100
94
|
const hooksSourceDir = path.join(configDir, 'hooks');
|
|
@@ -132,22 +126,20 @@ export async function init(options) {
|
|
|
132
126
|
// Save metadata
|
|
133
127
|
const meta = {
|
|
134
128
|
version: require(path.join(PACKAGE_ROOT, 'package.json')).version,
|
|
135
|
-
mode,
|
|
136
129
|
sources,
|
|
137
|
-
|
|
130
|
+
fileHashes: computeFileHashes(claudeDir),
|
|
138
131
|
createdAt: new Date().toISOString(),
|
|
139
132
|
updatedAt: new Date().toISOString(),
|
|
140
133
|
};
|
|
141
134
|
fs.writeFileSync(path.join(aiRulesDir, 'meta.json'), JSON.stringify(meta, null, 2));
|
|
142
135
|
// Print structured summary
|
|
143
|
-
printSummary(claudeDir,
|
|
136
|
+
printSummary(claudeDir, results);
|
|
144
137
|
}
|
|
145
|
-
function printSummary(claudeDir,
|
|
138
|
+
function printSummary(claudeDir, results) {
|
|
146
139
|
const installed = results.filter(r => r.action !== 'skipped');
|
|
147
140
|
const skipped = results.filter(r => r.action === 'skipped');
|
|
148
141
|
console.log(chalk.green.bold('\n Setup complete!\n'));
|
|
149
|
-
console.log(` Location: ${claudeDir}`);
|
|
150
|
-
console.log(` Mode: ${mode}\n`);
|
|
142
|
+
console.log(` Location: ${claudeDir}\n`);
|
|
151
143
|
// Installed items
|
|
152
144
|
if (installed.length > 0) {
|
|
153
145
|
console.log(chalk.bold(' Installed:'));
|
package/dist/commands/update.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import inquirer from 'inquirer';
|
|
6
7
|
import { detectInstall, scanDir, compareConfigs, ensureDir, computeFileHashes, aggregateToAgentsMd } from '../utils/files.js';
|
|
7
8
|
import crypto from 'crypto';
|
|
8
9
|
import { updateRepo } from '../utils/git.js';
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '../..');
|
|
9
12
|
export async function update(options = {}) {
|
|
10
13
|
const install = detectInstall();
|
|
11
14
|
if (!install) {
|
|
@@ -23,10 +26,40 @@ export async function update(options = {}) {
|
|
|
23
26
|
process.exit(1);
|
|
24
27
|
}
|
|
25
28
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
26
|
-
console.log(` Mode: ${meta.mode}`);
|
|
27
29
|
console.log(` Sources: ${meta.sources.map(s => s.name).join(', ')}\n`);
|
|
28
|
-
|
|
30
|
+
const configDir = path.join(configPath, 'config');
|
|
31
|
+
const targetDir = scope === 'global' ? os.homedir() : process.cwd();
|
|
32
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
33
|
+
// Migrate from symlink if needed
|
|
34
|
+
if (meta.mode === 'symlink') {
|
|
35
|
+
migrateFromSymlink(claudeDir, meta, metaPath);
|
|
36
|
+
}
|
|
29
37
|
let hasChanges = false;
|
|
38
|
+
// Step 1: Sync new builtin files to .ai-nexus/config/
|
|
39
|
+
const builtinConfigDir = path.join(PACKAGE_ROOT, 'config');
|
|
40
|
+
const categories = ['rules', 'commands', 'skills', 'agents', 'contexts', 'hooks'];
|
|
41
|
+
let builtinAdded = 0;
|
|
42
|
+
for (const category of categories) {
|
|
43
|
+
const srcDir = path.join(builtinConfigDir, category);
|
|
44
|
+
if (!fs.existsSync(srcDir))
|
|
45
|
+
continue;
|
|
46
|
+
const destDir = path.join(configDir, category);
|
|
47
|
+
ensureDir(destDir);
|
|
48
|
+
const srcFiles = scanDir(srcDir);
|
|
49
|
+
for (const [rel, content] of Object.entries(srcFiles)) {
|
|
50
|
+
const dest = path.join(destDir, rel);
|
|
51
|
+
if (!fs.existsSync(dest)) {
|
|
52
|
+
ensureDir(path.dirname(dest));
|
|
53
|
+
fs.writeFileSync(dest, content);
|
|
54
|
+
builtinAdded++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (builtinAdded > 0) {
|
|
59
|
+
console.log(chalk.green(` + ${builtinAdded} new builtin files synced`));
|
|
60
|
+
hasChanges = true;
|
|
61
|
+
}
|
|
62
|
+
// Step 2: Update external sources and re-merge new files
|
|
30
63
|
const sourcesDir = path.join(configPath, 'sources');
|
|
31
64
|
for (const source of meta.sources) {
|
|
32
65
|
if (source.type === 'external' && source.url) {
|
|
@@ -41,32 +74,47 @@ export async function update(options = {}) {
|
|
|
41
74
|
else {
|
|
42
75
|
console.log(` - Already up to date`);
|
|
43
76
|
}
|
|
77
|
+
// Re-merge new files from external source to config
|
|
78
|
+
const externalConfigDir = fs.existsSync(path.join(repoPath, 'config'))
|
|
79
|
+
? path.join(repoPath, 'config')
|
|
80
|
+
: repoPath;
|
|
81
|
+
let extAdded = 0;
|
|
82
|
+
for (const category of categories) {
|
|
83
|
+
const srcCat = path.join(externalConfigDir, category);
|
|
84
|
+
if (!fs.existsSync(srcCat))
|
|
85
|
+
continue;
|
|
86
|
+
const destCat = path.join(configDir, category);
|
|
87
|
+
ensureDir(destCat);
|
|
88
|
+
const srcFiles = scanDir(srcCat);
|
|
89
|
+
for (const [rel, content] of Object.entries(srcFiles)) {
|
|
90
|
+
const prefixed = `${source.name}-${path.basename(rel)}`;
|
|
91
|
+
const destRel = path.join(path.dirname(rel), prefixed);
|
|
92
|
+
const dest = path.join(destCat, destRel === prefixed ? prefixed : destRel);
|
|
93
|
+
if (!fs.existsSync(dest)) {
|
|
94
|
+
ensureDir(path.dirname(dest));
|
|
95
|
+
fs.writeFileSync(dest, content);
|
|
96
|
+
extAdded++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (extAdded > 0) {
|
|
101
|
+
console.log(chalk.green(` + ${extAdded} new files from ${source.name}`));
|
|
102
|
+
hasChanges = true;
|
|
103
|
+
}
|
|
44
104
|
}
|
|
45
105
|
}
|
|
46
106
|
}
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (meta.mode === 'symlink') {
|
|
54
|
-
const hasExternal = meta.sources.some(s => s.type === 'external');
|
|
55
|
-
if (!hasExternal && !hasChanges) {
|
|
56
|
-
console.log(chalk.gray(' Symlink mode with built-in rules only.'));
|
|
57
|
-
console.log(chalk.gray(' Built-in rules update when you run:'));
|
|
58
|
-
console.log(chalk.bold(' npm update -g ai-nexus\n'));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (meta.mode === 'copy') {
|
|
62
|
-
// Copy mode: compare and update files
|
|
63
|
-
const sourceFiles = scanDir(configDir);
|
|
64
|
-
const installedFiles = scanDir(claudeDir);
|
|
65
|
-
const diff = compareConfigs(sourceFiles, installedFiles);
|
|
66
|
-
if (diff.added.length === 0 && diff.modified.length === 0 && diff.removed.length === 0) {
|
|
107
|
+
// Step 3: Compare .ai-nexus/config vs .claude/ and sync
|
|
108
|
+
const sourceFiles = scanDir(configDir);
|
|
109
|
+
const installedFiles = scanDir(claudeDir);
|
|
110
|
+
const diff = compareConfigs(sourceFiles, installedFiles);
|
|
111
|
+
if (diff.added.length === 0 && diff.modified.length === 0 && diff.removed.length === 0) {
|
|
112
|
+
if (!hasChanges) {
|
|
67
113
|
console.log('\n✅ Already up to date!\n');
|
|
68
114
|
return;
|
|
69
115
|
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
70
118
|
console.log('\n Changes detected:');
|
|
71
119
|
if (diff.added.length > 0)
|
|
72
120
|
console.log(chalk.green(` + ${diff.added.length} new files`));
|
|
@@ -74,113 +122,128 @@ export async function update(options = {}) {
|
|
|
74
122
|
console.log(chalk.yellow(` ~ ${diff.modified.length} modified files`));
|
|
75
123
|
if (diff.removed.length > 0)
|
|
76
124
|
console.log(chalk.red(` - ${diff.removed.length} removed in source`));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
filesToUpdate = diff.modified;
|
|
102
|
-
}
|
|
125
|
+
}
|
|
126
|
+
// Determine which files to update
|
|
127
|
+
const filesToAdd = diff.added;
|
|
128
|
+
let filesToUpdate;
|
|
129
|
+
let filesToRemove = [];
|
|
130
|
+
if (options.force) {
|
|
131
|
+
const userEdited = detectUserEdits(diff.modified, claudeDir, meta.fileHashes);
|
|
132
|
+
if (userEdited.length > 0) {
|
|
133
|
+
console.log(chalk.red(`\n WARNING: ${userEdited.length} file(s) have local edits that will be overwritten:`));
|
|
134
|
+
for (const f of userEdited) {
|
|
135
|
+
console.log(chalk.red(` - ${f}`));
|
|
136
|
+
}
|
|
137
|
+
const { proceed } = await inquirer.prompt([
|
|
138
|
+
{
|
|
139
|
+
type: 'confirm',
|
|
140
|
+
name: 'proceed',
|
|
141
|
+
message: 'Overwrite these user-edited files?',
|
|
142
|
+
default: false,
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
if (!proceed) {
|
|
146
|
+
filesToUpdate = diff.modified.filter(f => !userEdited.includes(f));
|
|
103
147
|
}
|
|
104
148
|
else {
|
|
105
149
|
filesToUpdate = diff.modified;
|
|
106
150
|
}
|
|
107
|
-
filesToRemove = diff.removed;
|
|
108
151
|
}
|
|
109
|
-
else
|
|
110
|
-
|
|
111
|
-
filesToUpdate = [];
|
|
112
|
-
filesToRemove = [];
|
|
152
|
+
else {
|
|
153
|
+
filesToUpdate = diff.modified;
|
|
113
154
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
155
|
+
filesToRemove = diff.removed;
|
|
156
|
+
}
|
|
157
|
+
else if (options.addOnly) {
|
|
158
|
+
filesToUpdate = [];
|
|
159
|
+
filesToRemove = [];
|
|
160
|
+
}
|
|
161
|
+
else if (options.interactive && diff.modified.length > 0) {
|
|
162
|
+
console.log(chalk.cyan('\n Modified files (choose which to overwrite):\n'));
|
|
163
|
+
const { selectedFiles } = await inquirer.prompt([
|
|
164
|
+
{
|
|
165
|
+
type: 'checkbox',
|
|
166
|
+
name: 'selectedFiles',
|
|
167
|
+
message: 'Select files to overwrite',
|
|
168
|
+
choices: diff.modified.map(f => ({
|
|
169
|
+
name: f,
|
|
170
|
+
value: f,
|
|
171
|
+
checked: false,
|
|
172
|
+
})),
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
filesToUpdate = selectedFiles;
|
|
176
|
+
if (diff.removed.length > 0) {
|
|
177
|
+
const { removeFiles } = await inquirer.prompt([
|
|
118
178
|
{
|
|
119
|
-
type: '
|
|
120
|
-
name: '
|
|
121
|
-
message:
|
|
122
|
-
|
|
123
|
-
name: f,
|
|
124
|
-
value: f,
|
|
125
|
-
checked: false,
|
|
126
|
-
})),
|
|
179
|
+
type: 'confirm',
|
|
180
|
+
name: 'removeFiles',
|
|
181
|
+
message: `Remove ${diff.removed.length} files that no longer exist in source?`,
|
|
182
|
+
default: false,
|
|
127
183
|
},
|
|
128
184
|
]);
|
|
129
|
-
|
|
130
|
-
if (diff.removed.length > 0) {
|
|
131
|
-
const { removeFiles } = await inquirer.prompt([
|
|
132
|
-
{
|
|
133
|
-
type: 'confirm',
|
|
134
|
-
name: 'removeFiles',
|
|
135
|
-
message: `Remove ${diff.removed.length} files that no longer exist in source?`,
|
|
136
|
-
default: false,
|
|
137
|
-
},
|
|
138
|
-
]);
|
|
139
|
-
filesToRemove = removeFiles ? diff.removed : [];
|
|
140
|
-
}
|
|
185
|
+
filesToRemove = removeFiles ? diff.removed : [];
|
|
141
186
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(chalk.gray(`\n Skipping ${diff.modified.length} modified files (use --force to overwrite)`));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// Apply changes
|
|
152
|
-
let addedCount = 0;
|
|
153
|
-
let updatedCount = 0;
|
|
154
|
-
let removedCount = 0;
|
|
155
|
-
for (const rel of filesToAdd) {
|
|
156
|
-
const src = path.join(configDir, rel);
|
|
157
|
-
const dest = path.join(claudeDir, rel);
|
|
158
|
-
ensureDir(path.dirname(dest));
|
|
159
|
-
fs.copyFileSync(src, dest);
|
|
160
|
-
addedCount++;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
filesToUpdate = [];
|
|
190
|
+
filesToRemove = [];
|
|
191
|
+
if (diff.modified.length > 0) {
|
|
192
|
+
console.log(chalk.gray(`\n Skipping ${diff.modified.length} modified files (use --force to overwrite)`));
|
|
161
193
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
}
|
|
195
|
+
// Apply changes
|
|
196
|
+
let addedCount = 0;
|
|
197
|
+
let updatedCount = 0;
|
|
198
|
+
let removedCount = 0;
|
|
199
|
+
for (const rel of filesToAdd) {
|
|
200
|
+
const src = path.join(configDir, rel);
|
|
201
|
+
const dest = path.join(claudeDir, rel);
|
|
202
|
+
ensureDir(path.dirname(dest));
|
|
203
|
+
fs.copyFileSync(src, dest);
|
|
204
|
+
addedCount++;
|
|
205
|
+
}
|
|
206
|
+
for (const rel of filesToUpdate) {
|
|
207
|
+
const src = path.join(configDir, rel);
|
|
208
|
+
const dest = path.join(claudeDir, rel);
|
|
209
|
+
ensureDir(path.dirname(dest));
|
|
210
|
+
fs.copyFileSync(src, dest);
|
|
211
|
+
updatedCount++;
|
|
212
|
+
}
|
|
213
|
+
for (const rel of filesToRemove) {
|
|
214
|
+
const dest = path.join(claudeDir, rel);
|
|
215
|
+
if (fs.existsSync(dest)) {
|
|
216
|
+
fs.unlinkSync(dest);
|
|
217
|
+
removedCount++;
|
|
168
218
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
219
|
+
}
|
|
220
|
+
if (addedCount > 0 || updatedCount > 0 || removedCount > 0) {
|
|
221
|
+
console.log('\n Applied:');
|
|
222
|
+
if (addedCount > 0)
|
|
223
|
+
console.log(chalk.green(` + ${addedCount} files added`));
|
|
224
|
+
if (updatedCount > 0)
|
|
225
|
+
console.log(chalk.yellow(` ~ ${updatedCount} files updated`));
|
|
226
|
+
if (removedCount > 0)
|
|
227
|
+
console.log(chalk.red(` - ${removedCount} files removed`));
|
|
228
|
+
hasChanges = true;
|
|
229
|
+
}
|
|
230
|
+
// Step 4: Sync hooks to .claude/hooks/ (new files only)
|
|
231
|
+
const hooksConfigDir = path.join(configDir, 'hooks');
|
|
232
|
+
const hooksTargetDir = path.join(claudeDir, 'hooks');
|
|
233
|
+
if (fs.existsSync(hooksConfigDir)) {
|
|
234
|
+
ensureDir(hooksTargetDir);
|
|
235
|
+
let hooksAdded = 0;
|
|
236
|
+
const hookFiles = scanDir(hooksConfigDir);
|
|
237
|
+
for (const [rel, content] of Object.entries(hookFiles)) {
|
|
238
|
+
const dest = path.join(hooksTargetDir, rel);
|
|
239
|
+
if (!fs.existsSync(dest)) {
|
|
240
|
+
ensureDir(path.dirname(dest));
|
|
241
|
+
fs.writeFileSync(dest, content);
|
|
242
|
+
hooksAdded++;
|
|
174
243
|
}
|
|
175
244
|
}
|
|
176
|
-
if (
|
|
177
|
-
console.log(
|
|
178
|
-
if (addedCount > 0)
|
|
179
|
-
console.log(chalk.green(` + ${addedCount} files added`));
|
|
180
|
-
if (updatedCount > 0)
|
|
181
|
-
console.log(chalk.yellow(` ~ ${updatedCount} files updated`));
|
|
182
|
-
if (removedCount > 0)
|
|
183
|
-
console.log(chalk.red(` - ${removedCount} files removed`));
|
|
245
|
+
if (hooksAdded > 0) {
|
|
246
|
+
console.log(chalk.green(` + ${hooksAdded} new hooks added`));
|
|
184
247
|
hasChanges = true;
|
|
185
248
|
}
|
|
186
249
|
}
|
|
@@ -210,11 +273,10 @@ export async function update(options = {}) {
|
|
|
210
273
|
hasChanges = true;
|
|
211
274
|
}
|
|
212
275
|
}
|
|
213
|
-
// Update metadata
|
|
276
|
+
// Update metadata
|
|
214
277
|
meta.updatedAt = new Date().toISOString();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
278
|
+
delete meta.mode; // remove deprecated field
|
|
279
|
+
meta.fileHashes = computeFileHashes(claudeDir);
|
|
218
280
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
219
281
|
if (hasChanges) {
|
|
220
282
|
console.log('\n✅ Update complete!\n');
|
|
@@ -223,6 +285,44 @@ export async function update(options = {}) {
|
|
|
223
285
|
console.log('\n✅ Already up to date!\n');
|
|
224
286
|
}
|
|
225
287
|
}
|
|
288
|
+
function migrateFromSymlink(claudeDir, meta, metaPath) {
|
|
289
|
+
console.log(chalk.yellow(' ⚡ Migrating from symlink to copy mode...\n'));
|
|
290
|
+
const categories = ['rules', 'commands', 'skills', 'agents', 'contexts'];
|
|
291
|
+
let migrated = 0;
|
|
292
|
+
for (const category of categories) {
|
|
293
|
+
const targetPath = path.join(claudeDir, category);
|
|
294
|
+
if (!fs.existsSync(targetPath))
|
|
295
|
+
continue;
|
|
296
|
+
try {
|
|
297
|
+
const stat = fs.lstatSync(targetPath);
|
|
298
|
+
if (!stat.isSymbolicLink())
|
|
299
|
+
continue;
|
|
300
|
+
// Read files through symlink before removing it
|
|
301
|
+
const files = scanDir(targetPath);
|
|
302
|
+
// Remove symlink
|
|
303
|
+
fs.unlinkSync(targetPath);
|
|
304
|
+
// Create directory and copy files
|
|
305
|
+
ensureDir(targetPath);
|
|
306
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
307
|
+
const dest = path.join(targetPath, rel);
|
|
308
|
+
ensureDir(path.dirname(dest));
|
|
309
|
+
fs.writeFileSync(dest, content);
|
|
310
|
+
}
|
|
311
|
+
migrated++;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Skip on error
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Update meta
|
|
318
|
+
delete meta.mode;
|
|
319
|
+
meta.fileHashes = computeFileHashes(claudeDir);
|
|
320
|
+
meta.updatedAt = new Date().toISOString();
|
|
321
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
322
|
+
if (migrated > 0) {
|
|
323
|
+
console.log(chalk.green(` ✓ Migrated ${migrated} symlinks to copies\n`));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
226
326
|
function detectUserEdits(modifiedFiles, claudeDir, savedHashes) {
|
|
227
327
|
if (!savedHashes)
|
|
228
328
|
return [];
|
package/dist/types.d.ts
CHANGED
package/dist/utils/files.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export declare function getTargetDir(scope: 'project' | 'global'): string;
|
|
|
14
14
|
export declare function getConfigPath(scope: 'project' | 'global'): string;
|
|
15
15
|
export declare function ensureDir(dir: string): void;
|
|
16
16
|
export declare function copyFile(src: string, dest: string): void;
|
|
17
|
-
export declare function createSymlink(target: string, linkPath: string): void;
|
|
18
17
|
/**
|
|
19
18
|
* Aggregate rule files into a single AGENTS.md content string.
|
|
20
19
|
* If selectedFiles is provided, only those files are included.
|
package/dist/utils/files.js
CHANGED
|
@@ -76,13 +76,6 @@ export function copyFile(src, dest) {
|
|
|
76
76
|
ensureDir(path.dirname(dest));
|
|
77
77
|
fs.copyFileSync(src, dest);
|
|
78
78
|
}
|
|
79
|
-
export function createSymlink(target, linkPath) {
|
|
80
|
-
ensureDir(path.dirname(linkPath));
|
|
81
|
-
if (fs.existsSync(linkPath)) {
|
|
82
|
-
fs.rmSync(linkPath, { recursive: true });
|
|
83
|
-
}
|
|
84
|
-
fs.symlinkSync(target, linkPath);
|
|
85
|
-
}
|
|
86
79
|
const AGENTS_CATEGORIES = ['rules', 'commands', 'skills', 'agents', 'contexts'];
|
|
87
80
|
const CATEGORY_LABELS = {
|
|
88
81
|
rules: 'Rules',
|