@dezkareid/ai-context-sync 1.3.0 → 1.5.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.md +51 -4
- package/dist/constants.js +2 -5
- package/dist/engine.js +45 -47
- package/dist/index.js +252 -113
- package/dist/strategies/claude.js +2 -6
- package/dist/strategies/gemini-md.js +2 -6
- package/dist/strategies/gemini.js +14 -21
- package/dist/strategies/index.js +2 -5
- package/dist/strategies/other.js +2 -6
- package/dist/strategies/symlink.js +13 -20
- package/package.json +8 -2
- package/dist/engine.test.js +0 -225
- package/dist/strategies/claude.test.js +0 -31
- package/dist/strategies/gemini-md.test.js +0 -31
- package/dist/strategies/gemini.test.js +0 -110
- package/dist/strategies/other.test.js +0 -58
- package/dist/strategies/symlink.test.js +0 -101
package/README.md
CHANGED
|
@@ -48,15 +48,62 @@ To bypass reading or creating this configuration file, use the `--skip-config` f
|
|
|
48
48
|
npx @dezkareid/ai-context-sync sync --skip-config
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Monorepo / Multi-project Sync
|
|
52
|
+
|
|
53
|
+
In complex projects or monorepos, you can configure multiple subdirectories to be synchronized in a single command. The tool will sequentially sync the root project and then each configured subdirectory.
|
|
54
|
+
|
|
55
|
+
#### Managing Projects
|
|
56
|
+
|
|
57
|
+
Use the `project` command to manage your configured projects:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Add a new project (interactive)
|
|
61
|
+
npx @dezkareid/ai-context-sync project add apps/web
|
|
62
|
+
|
|
63
|
+
# Add a project with specific strategies
|
|
64
|
+
npx @dezkareid/ai-context-sync project add packages/ui --strategy "claude, gemini"
|
|
65
|
+
|
|
66
|
+
# Add a project with custom files
|
|
67
|
+
npx @dezkareid/ai-context-sync project add packages/lib --strategy other --files "CUSTOM.md"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Configuration Structure
|
|
71
|
+
|
|
72
|
+
Configured projects are stored in the `.ai-context-configrc` file at the root:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"strategies": ["claude"],
|
|
77
|
+
"projects": {
|
|
78
|
+
"apps/web": {
|
|
79
|
+
"strategies": ["gemini"]
|
|
80
|
+
},
|
|
81
|
+
"packages/ui": {
|
|
82
|
+
"strategies": ["claude", "gemini"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
When you run `npx @dezkareid/ai-context-sync sync`, it will:
|
|
89
|
+
1. Sync the root using the top-level `strategies`.
|
|
90
|
+
2. Sync `apps/web` using the `gemini` strategy.
|
|
91
|
+
3. Sync `packages/ui` using `claude` and `gemini` strategies.
|
|
92
|
+
|
|
93
|
+
If a project doesn't define its own `strategies`, it will inherit the root's `strategies`.
|
|
94
|
+
|
|
95
|
+
|
|
51
96
|
### Directory option
|
|
52
97
|
|
|
98
|
+
The `-d, --dir` option allows you to specify where the root `AGENTS.md` and configuration file live. All project paths are resolved relative to this directory.
|
|
99
|
+
|
|
53
100
|
## How it works
|
|
54
101
|
|
|
55
|
-
1. The tool looks for an `AGENTS.md` file in the target directory.
|
|
102
|
+
1. The tool looks for an `AGENTS.md` file in the target directory (root and each configured project).
|
|
56
103
|
2. It reads the content of `AGENTS.md`.
|
|
57
|
-
3. It applies different strategies
|
|
58
|
-
|
|
59
|
-
|
|
104
|
+
3. It applies different strategies sequentially.
|
|
105
|
+
4. If a specific synchronization fails, it reports the error but continues with the next project (fail-soft).
|
|
106
|
+
5. A summary is displayed at the end if any errors occurred.
|
|
60
107
|
|
|
61
108
|
## License
|
|
62
109
|
|
package/dist/constants.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.CONFIG_FILENAME = exports.AGENTS_FILENAME = void 0;
|
|
4
|
-
exports.AGENTS_FILENAME = 'AGENTS.md';
|
|
5
|
-
exports.CONFIG_FILENAME = '.ai-context-configrc';
|
|
1
|
+
export const AGENTS_FILENAME = 'AGENTS.md';
|
|
2
|
+
export const CONFIG_FILENAME = '.ai-context-configrc';
|
package/dist/engine.js
CHANGED
|
@@ -1,54 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { AGENTS_FILENAME } from './constants.js';
|
|
4
|
+
import { ClaudeStrategy } from './strategies/claude.js';
|
|
5
|
+
import { GeminiStrategy } from './strategies/gemini.js';
|
|
6
|
+
import { GeminiMdStrategy } from './strategies/gemini-md.js';
|
|
7
|
+
import { OtherStrategy } from './strategies/other.js';
|
|
8
|
+
function buildBuiltInStrategies(fromFile) {
|
|
9
|
+
return [
|
|
10
|
+
new ClaudeStrategy(fromFile),
|
|
11
|
+
new GeminiStrategy(fromFile),
|
|
12
|
+
new GeminiMdStrategy(fromFile)
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
function resolveStrategiesToRun(selectedStrategies, builtInStrategies, otherFiles, fromFile) {
|
|
16
|
+
if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
|
|
17
|
+
return builtInStrategies;
|
|
18
|
+
}
|
|
19
|
+
const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
|
|
20
|
+
const normalizedList = selectedList.map(s => s.toLowerCase());
|
|
21
|
+
let strategies;
|
|
22
|
+
if (normalizedList.includes('all') || normalizedList.includes('both')) {
|
|
23
|
+
strategies = [...builtInStrategies];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
strategies = builtInStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
|
|
27
|
+
}
|
|
28
|
+
if (normalizedList.includes('other')) {
|
|
29
|
+
if (!otherFiles || otherFiles.length === 0) {
|
|
30
|
+
throw new Error('Strategy "other" requires otherFiles to be specified.');
|
|
31
|
+
}
|
|
32
|
+
for (const filename of otherFiles) {
|
|
33
|
+
strategies.push(new OtherStrategy(filename, fromFile));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return strategies;
|
|
37
|
+
}
|
|
38
|
+
export class SyncEngine {
|
|
15
39
|
async sync(projectRoot, selectedStrategies, targetDir, fromFile, otherFiles) {
|
|
16
|
-
const sourceFile = fromFile ??
|
|
17
|
-
const agentsPath =
|
|
40
|
+
const sourceFile = fromFile ?? AGENTS_FILENAME;
|
|
41
|
+
const agentsPath = path.join(projectRoot, sourceFile);
|
|
18
42
|
const outputDir = targetDir ?? projectRoot;
|
|
19
|
-
if (!(await
|
|
43
|
+
if (!(await fs.pathExists(agentsPath))) {
|
|
20
44
|
throw new Error(`${sourceFile} not found in ${projectRoot}`);
|
|
21
45
|
}
|
|
22
|
-
const context = await
|
|
23
|
-
const builtInStrategies =
|
|
24
|
-
|
|
25
|
-
new gemini_js_1.GeminiStrategy(fromFile),
|
|
26
|
-
new gemini_md_js_1.GeminiMdStrategy(fromFile)
|
|
27
|
-
];
|
|
28
|
-
let strategiesToRun;
|
|
29
|
-
if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
|
|
30
|
-
strategiesToRun = builtInStrategies;
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
|
|
34
|
-
const normalizedList = selectedList.map(s => s.toLowerCase());
|
|
35
|
-
if (normalizedList.includes('all') || normalizedList.includes('both')) {
|
|
36
|
-
strategiesToRun = builtInStrategies;
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
strategiesToRun = builtInStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
|
|
40
|
-
}
|
|
41
|
-
if (normalizedList.includes('other')) {
|
|
42
|
-
if (!otherFiles || otherFiles.length === 0) {
|
|
43
|
-
throw new Error('Strategy "other" requires otherFiles to be specified.');
|
|
44
|
-
}
|
|
45
|
-
for (const filename of otherFiles) {
|
|
46
|
-
strategiesToRun.push(new other_js_1.OtherStrategy(filename, fromFile));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const availableNames = [...builtInStrategies.map(s => s.name), 'other'].join(', ');
|
|
46
|
+
const context = await fs.readFile(agentsPath, 'utf-8');
|
|
47
|
+
const builtInStrategies = buildBuiltInStrategies(fromFile);
|
|
48
|
+
const strategiesToRun = resolveStrategiesToRun(selectedStrategies, builtInStrategies, otherFiles, fromFile);
|
|
51
49
|
if (strategiesToRun.length === 0 && selectedStrategies) {
|
|
50
|
+
const availableNames = [...builtInStrategies.map(s => s.name), 'other'].join(', ');
|
|
52
51
|
throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
|
|
53
52
|
}
|
|
54
53
|
for (const strategy of strategiesToRun) {
|
|
@@ -57,4 +56,3 @@ class SyncEngine {
|
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
|
-
exports.SyncEngine = SyncEngine;
|
package/dist/index.js
CHANGED
|
@@ -1,128 +1,267 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
|
-
const constants_js_1 = require("./constants.js");
|
|
13
|
-
const program = new commander_1.Command();
|
|
14
|
-
program
|
|
15
|
-
.name('ai-context-sync')
|
|
16
|
-
.description('Sync AI context files across different providers')
|
|
17
|
-
.version('1.0.0');
|
|
18
|
-
program
|
|
19
|
-
.command('sync')
|
|
20
|
-
.description('Synchronize context files from AGENTS.md')
|
|
21
|
-
.option('-d, --dir <path>', 'Project directory (where AGENTS.md lives)', process.cwd())
|
|
22
|
-
.option('-t, --target-dir <path>', 'Target directory where synced files will be written (defaults to --dir)')
|
|
23
|
-
.option('-s, --strategy <strategy>', 'Sync strategy (claude, gemini, all, or comma-separated list)')
|
|
24
|
-
.option('-f, --files <names>', 'Comma-separated custom filenames for "other" strategy')
|
|
25
|
-
.option('--from <path>', 'Source file path for symlinks (default: AGENTS.md)')
|
|
26
|
-
.option('--skip-config', 'Avoid reading/creating the config file', false)
|
|
27
|
-
.action(async (options) => {
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { SyncEngine } from './engine.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import { CONFIG_FILENAME } from './constants.js';
|
|
8
|
+
export async function readConfig(configPath) {
|
|
28
9
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
catch (e) {
|
|
51
|
-
// Ignore corrupted config and proceed to prompt
|
|
10
|
+
return await fs.readJson(configPath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function promptStrategies() {
|
|
17
|
+
const answers = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: 'checkbox',
|
|
20
|
+
name: 'strategies',
|
|
21
|
+
message: 'Select the AI context files to sync:',
|
|
22
|
+
choices: [
|
|
23
|
+
{ name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
|
|
24
|
+
{ name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
|
|
25
|
+
{ name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true },
|
|
26
|
+
{ name: 'Other (custom files)', value: 'other', checked: false }
|
|
27
|
+
],
|
|
28
|
+
validate: (answer) => {
|
|
29
|
+
if (answer.length < 1) {
|
|
30
|
+
return 'You must choose at least one strategy.';
|
|
52
31
|
}
|
|
32
|
+
return true;
|
|
53
33
|
}
|
|
54
34
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
otherFiles = [...new Set([...(otherFiles ?? []), ...flagFiles])];
|
|
66
|
-
}
|
|
67
|
-
// 4. If still no strategy, prompt user
|
|
68
|
-
if (!strategy) {
|
|
69
|
-
const answers = await inquirer_1.default.prompt([
|
|
70
|
-
{
|
|
71
|
-
type: 'checkbox',
|
|
72
|
-
name: 'strategies',
|
|
73
|
-
message: 'Select the AI context files to sync:',
|
|
74
|
-
choices: [
|
|
75
|
-
{ name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
|
|
76
|
-
{ name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
|
|
77
|
-
{ name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true },
|
|
78
|
-
{ name: 'Other (custom files)', value: 'other', checked: false }
|
|
79
|
-
],
|
|
80
|
-
validate: (answer) => {
|
|
81
|
-
if (answer.length < 1) {
|
|
82
|
-
return 'You must choose at least one strategy.';
|
|
83
|
-
}
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
]);
|
|
88
|
-
strategy = answers.strategies;
|
|
89
|
-
// Follow-up prompt for custom filenames when 'other' is selected and not already set
|
|
90
|
-
if (strategy.includes('other') && (!otherFiles || otherFiles.length === 0)) {
|
|
91
|
-
const filesAnswer = await inquirer_1.default.prompt([
|
|
92
|
-
{
|
|
93
|
-
type: 'input',
|
|
94
|
-
name: 'otherFiles',
|
|
95
|
-
message: 'Enter custom file name(s) to create as symlinks (comma-separated):',
|
|
96
|
-
validate: (v) => v.trim().length > 0 || 'At least one filename is required.'
|
|
97
|
-
}
|
|
98
|
-
]);
|
|
99
|
-
otherFiles = filesAnswer.otherFiles.split(',').map((s) => s.trim()).filter(Boolean);
|
|
100
|
-
}
|
|
35
|
+
]);
|
|
36
|
+
return answers.strategies;
|
|
37
|
+
}
|
|
38
|
+
async function promptOtherFiles() {
|
|
39
|
+
const filesAnswer = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: 'input',
|
|
42
|
+
name: 'otherFiles',
|
|
43
|
+
message: 'Enter custom file name(s) to create as symlinks (comma-separated):',
|
|
44
|
+
validate: (v) => v.trim().length > 0 || 'At least one filename is required.'
|
|
101
45
|
}
|
|
102
|
-
|
|
103
|
-
|
|
46
|
+
]);
|
|
47
|
+
return filesAnswer.otherFiles.split(',').map((s) => s.trim()).filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
async function resolveStrategy(strategyOption, otherFiles) {
|
|
50
|
+
if (!strategyOption) {
|
|
51
|
+
const selected = await promptStrategies();
|
|
52
|
+
let resolved = [...(otherFiles ?? [])];
|
|
53
|
+
if (selected.includes('other') && resolved.length === 0) {
|
|
54
|
+
resolved = await promptOtherFiles();
|
|
104
55
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
56
|
+
return { strategy: selected, otherFiles: resolved.length > 0 ? resolved : otherFiles };
|
|
57
|
+
}
|
|
58
|
+
const strategy = typeof strategyOption === 'string'
|
|
59
|
+
? strategyOption.split(',').map(s => s.trim())
|
|
60
|
+
: strategyOption;
|
|
61
|
+
return { strategy, otherFiles };
|
|
62
|
+
}
|
|
63
|
+
async function applyConfig(options, configPath) {
|
|
64
|
+
let strategy = options.strategy;
|
|
65
|
+
let otherFiles;
|
|
66
|
+
let fromFile;
|
|
67
|
+
let config = {};
|
|
68
|
+
if (!options.skipConfig && await fs.pathExists(configPath)) {
|
|
69
|
+
config = await readConfig(configPath);
|
|
70
|
+
if (!strategy && config.strategies) {
|
|
71
|
+
strategy = config.strategies;
|
|
108
72
|
}
|
|
109
|
-
|
|
73
|
+
otherFiles = config.otherFiles;
|
|
74
|
+
fromFile = config.from;
|
|
75
|
+
}
|
|
76
|
+
return { config, strategy, otherFiles, fromFile };
|
|
77
|
+
}
|
|
78
|
+
function resolveFromFile(optionFrom, configFromFile) {
|
|
79
|
+
if (!optionFrom)
|
|
80
|
+
return configFromFile;
|
|
81
|
+
if (path.isAbsolute(optionFrom)) {
|
|
82
|
+
throw new Error('--from must be a relative path, not an absolute path.');
|
|
83
|
+
}
|
|
84
|
+
return optionFrom;
|
|
85
|
+
}
|
|
86
|
+
function mergeOtherFiles(optionFiles, configOtherFiles) {
|
|
87
|
+
if (!optionFiles)
|
|
88
|
+
return configOtherFiles;
|
|
89
|
+
const flagFiles = optionFiles.split(',').map((s) => s.trim()).filter(Boolean);
|
|
90
|
+
return [...new Set([...(configOtherFiles ?? []), ...flagFiles])];
|
|
91
|
+
}
|
|
92
|
+
export async function resolveProjectConfig(projectPath, rootConfig, overrides) {
|
|
93
|
+
const configPath = path.join(projectPath, CONFIG_FILENAME);
|
|
94
|
+
const localConfig = await readConfig(configPath);
|
|
95
|
+
const strategy = overrides?.strategies ?? localConfig.strategies ?? rootConfig.strategies;
|
|
96
|
+
const otherFiles = overrides?.otherFiles ?? localConfig.otherFiles ?? rootConfig.otherFiles;
|
|
97
|
+
const fromFile = overrides?.from ?? localConfig.from ?? rootConfig.from;
|
|
98
|
+
const strategyArray = typeof strategy === 'string'
|
|
99
|
+
? strategy.split(',').map(s => s.trim())
|
|
100
|
+
: (strategy ?? []);
|
|
101
|
+
return {
|
|
102
|
+
strategy: strategyArray,
|
|
103
|
+
otherFiles,
|
|
104
|
+
fromFile
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function syncProject(projectRoot, strategy, targetDir, fromFile, otherFiles, projectName) {
|
|
108
|
+
if (strategy.includes('other') && (!otherFiles || otherFiles.length === 0)) {
|
|
109
|
+
throw new Error('Strategy "other" requires custom files to be defined.');
|
|
110
|
+
}
|
|
111
|
+
const engine = new SyncEngine();
|
|
112
|
+
await engine.sync(projectRoot, strategy, targetDir, fromFile, otherFiles);
|
|
113
|
+
console.log(`[${projectName}] Successfully synchronized using "${strategy.join(', ')}"!`);
|
|
114
|
+
}
|
|
115
|
+
async function runRootSync(options, config, projectRoot, configPath, configResult) {
|
|
116
|
+
try {
|
|
117
|
+
const fromFile = resolveFromFile(options.from, configResult.fromFile);
|
|
118
|
+
const otherFiles = mergeOtherFiles(options.files, configResult.otherFiles);
|
|
119
|
+
const resolved = await resolveStrategy(configResult.strategy, otherFiles);
|
|
120
|
+
await syncProject(projectRoot, resolved.strategy, options.targetDir ? path.resolve(options.targetDir) : projectRoot, fromFile, resolved.otherFiles, 'root');
|
|
110
121
|
if (!options.skipConfig) {
|
|
111
|
-
const configData = { strategies: strategy };
|
|
112
|
-
if (otherFiles?.length)
|
|
113
|
-
configData.otherFiles = otherFiles;
|
|
122
|
+
const configData = { ...config, strategies: resolved.strategy };
|
|
123
|
+
if (resolved.otherFiles?.length)
|
|
124
|
+
configData.otherFiles = resolved.otherFiles;
|
|
114
125
|
if (fromFile)
|
|
115
126
|
configData.from = fromFile;
|
|
116
|
-
await
|
|
127
|
+
await fs.writeJson(configPath, configData, { spaces: 2 });
|
|
117
128
|
}
|
|
118
|
-
|
|
119
|
-
await engine.sync(projectRoot, strategy, targetDir, fromFile, otherFiles);
|
|
120
|
-
const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
|
|
121
|
-
console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
|
|
129
|
+
return { name: 'root', success: true };
|
|
122
130
|
}
|
|
123
131
|
catch (error) {
|
|
124
|
-
|
|
125
|
-
process.exit(1);
|
|
132
|
+
return { name: 'root', success: false, error: error instanceof Error ? error.message : String(error) };
|
|
126
133
|
}
|
|
127
|
-
}
|
|
128
|
-
|
|
134
|
+
}
|
|
135
|
+
async function runProjectsSync(config, projectRoot) {
|
|
136
|
+
const results = [];
|
|
137
|
+
if (!config.projects)
|
|
138
|
+
return results;
|
|
139
|
+
for (const [projectPath, projectOverrides] of Object.entries(config.projects)) {
|
|
140
|
+
try {
|
|
141
|
+
const absoluteProjectPath = path.resolve(projectRoot, projectPath);
|
|
142
|
+
if (!(await fs.pathExists(absoluteProjectPath))) {
|
|
143
|
+
throw new Error(`Project path does not exist: ${projectPath}`);
|
|
144
|
+
}
|
|
145
|
+
const resolved = await resolveProjectConfig(absoluteProjectPath, config, projectOverrides);
|
|
146
|
+
await syncProject(absoluteProjectPath, resolved.strategy, absoluteProjectPath, resolved.fromFile, resolved.otherFiles, projectPath);
|
|
147
|
+
results.push({ name: projectPath, success: true });
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
results.push({ name: projectPath, success: false, error: error instanceof Error ? error.message : String(error) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
function displaySummary(results) {
|
|
156
|
+
const failures = results.filter(r => !r.success);
|
|
157
|
+
if (failures.length > 0) {
|
|
158
|
+
console.warn('\n--- Synchronization Summary ---');
|
|
159
|
+
console.warn(`Success: ${results.length - failures.length}/${results.length}`);
|
|
160
|
+
console.warn('Failures:');
|
|
161
|
+
failures.forEach(f => console.warn(` - [${f.name}]: ${f.error}`));
|
|
162
|
+
if (failures.length === results.length) {
|
|
163
|
+
throw new Error('All synchronizations failed.');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (results.length > 0) {
|
|
167
|
+
console.log('\nAll projects synchronized successfully!');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function runSync(options) {
|
|
171
|
+
const projectRoot = path.resolve(options.dir);
|
|
172
|
+
const configPath = path.join(projectRoot, CONFIG_FILENAME);
|
|
173
|
+
const configResult = await applyConfig(options, configPath);
|
|
174
|
+
const results = [];
|
|
175
|
+
const rootResult = await runRootSync(options, configResult.config, projectRoot, configPath, configResult);
|
|
176
|
+
results.push(rootResult);
|
|
177
|
+
const projectResults = await runProjectsSync(configResult.config, projectRoot);
|
|
178
|
+
results.push(...projectResults);
|
|
179
|
+
displaySummary(results);
|
|
180
|
+
}
|
|
181
|
+
export async function addProject(projectPath, options) {
|
|
182
|
+
const projectRoot = path.resolve(options.dir);
|
|
183
|
+
const configPath = path.join(projectRoot, CONFIG_FILENAME);
|
|
184
|
+
const config = await readConfig(configPath);
|
|
185
|
+
const relativePath = path.isAbsolute(projectPath)
|
|
186
|
+
? path.relative(projectRoot, projectPath)
|
|
187
|
+
: projectPath;
|
|
188
|
+
const absoluteProjectPath = path.resolve(projectRoot, relativePath);
|
|
189
|
+
if (!(await fs.pathExists(absoluteProjectPath))) {
|
|
190
|
+
console.warn(`Warning: Project path does not exist: ${absoluteProjectPath}`);
|
|
191
|
+
}
|
|
192
|
+
const otherFiles = options.files ? options.files.split(',').map(s => s.trim()) : undefined;
|
|
193
|
+
const strategies = options.strategy ? options.strategy.split(',').map(s => s.trim()) : undefined;
|
|
194
|
+
let resolvedStrategies = strategies;
|
|
195
|
+
let resolvedOtherFiles = otherFiles;
|
|
196
|
+
if (!strategies) {
|
|
197
|
+
const resolved = await resolveStrategy(undefined, undefined);
|
|
198
|
+
resolvedStrategies = resolved.strategy;
|
|
199
|
+
resolvedOtherFiles = resolved.otherFiles;
|
|
200
|
+
}
|
|
201
|
+
const projectConfig = {};
|
|
202
|
+
if (resolvedStrategies)
|
|
203
|
+
projectConfig.strategies = resolvedStrategies;
|
|
204
|
+
if (resolvedOtherFiles)
|
|
205
|
+
projectConfig.otherFiles = resolvedOtherFiles;
|
|
206
|
+
if (options.from)
|
|
207
|
+
projectConfig.from = options.from;
|
|
208
|
+
const newConfig = {
|
|
209
|
+
...config,
|
|
210
|
+
projects: {
|
|
211
|
+
...(config.projects || {}),
|
|
212
|
+
[relativePath]: projectConfig
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
await fs.writeJson(configPath, newConfig, { spaces: 2 });
|
|
216
|
+
console.log(`Successfully added project "${relativePath}" to configuration.`);
|
|
217
|
+
}
|
|
218
|
+
export function createProgram() {
|
|
219
|
+
const program = new Command();
|
|
220
|
+
program
|
|
221
|
+
.name('ai-context-sync')
|
|
222
|
+
.description('Sync AI context files across different providers')
|
|
223
|
+
.version('1.0.0');
|
|
224
|
+
const projectCommand = program.command('project').description('Manage configured projects');
|
|
225
|
+
projectCommand
|
|
226
|
+
.command('add <path>')
|
|
227
|
+
.description('Add a new project to the configuration')
|
|
228
|
+
.option('-d, --dir <path>', 'Root project directory', process.cwd())
|
|
229
|
+
.option('-s, --strategy <strategy>', 'Sync strategy for this project')
|
|
230
|
+
.option('-f, --files <names>', 'Custom filenames for "other" strategy')
|
|
231
|
+
.option('--from <path>', 'Source file path for symlinks')
|
|
232
|
+
.action(async (projectPath, options) => {
|
|
233
|
+
try {
|
|
234
|
+
await addProject(projectPath, options);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
console.error(`Error: ${message}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
program
|
|
243
|
+
.command('sync')
|
|
244
|
+
.description('Synchronize context files from AGENTS.md')
|
|
245
|
+
.option('-d, --dir <path>', 'Project directory (where AGENTS.md lives)', process.cwd())
|
|
246
|
+
.option('-t, --target-dir <path>', 'Target directory where synced files will be written (defaults to --dir)')
|
|
247
|
+
.option('-s, --strategy <strategy>', 'Sync strategy (claude, gemini, all, or comma-separated list)')
|
|
248
|
+
.option('-f, --files <names>', 'Comma-separated custom filenames for "other" strategy')
|
|
249
|
+
.option('--from <path>', 'Source file path for symlinks (default: AGENTS.md)')
|
|
250
|
+
.option('--skip-config', 'Avoid reading/creating the config file', false)
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
try {
|
|
253
|
+
await runSync(options);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
console.error(`Error: ${message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return program;
|
|
262
|
+
}
|
|
263
|
+
if (process.argv[1] && (process.argv[1].endsWith('dist/index.js') ||
|
|
264
|
+
process.argv[1].endsWith('bin/ai-context-sync') ||
|
|
265
|
+
(fs.existsSync(path.resolve('dist/index.js')) && fs.realpathSync(process.argv[1]) === fs.realpathSync(path.resolve('dist/index.js'))))) {
|
|
266
|
+
createProgram().parse();
|
|
267
|
+
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.ClaudeStrategy = void 0;
|
|
4
|
-
const symlink_js_1 = require("./symlink.js");
|
|
5
|
-
class ClaudeStrategy extends symlink_js_1.SymlinkStrategy {
|
|
1
|
+
import { SymlinkStrategy } from './symlink.js';
|
|
2
|
+
export class ClaudeStrategy extends SymlinkStrategy {
|
|
6
3
|
name = 'claude';
|
|
7
4
|
targetFilename = 'CLAUDE.md';
|
|
8
5
|
constructor(fromFile) {
|
|
9
6
|
super(fromFile);
|
|
10
7
|
}
|
|
11
8
|
}
|
|
12
|
-
exports.ClaudeStrategy = ClaudeStrategy;
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.GeminiMdStrategy = void 0;
|
|
4
|
-
const symlink_js_1 = require("./symlink.js");
|
|
5
|
-
class GeminiMdStrategy extends symlink_js_1.SymlinkStrategy {
|
|
1
|
+
import { SymlinkStrategy } from './symlink.js';
|
|
2
|
+
export class GeminiMdStrategy extends SymlinkStrategy {
|
|
6
3
|
name = 'gemini-md';
|
|
7
4
|
targetFilename = 'GEMINI.md';
|
|
8
5
|
constructor(fromFile) {
|
|
9
6
|
super(fromFile);
|
|
10
7
|
}
|
|
11
8
|
}
|
|
12
|
-
exports.GeminiMdStrategy = GeminiMdStrategy;
|
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.GeminiStrategy = void 0;
|
|
7
|
-
const index_js_1 = require("./index.js");
|
|
8
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
class GeminiStrategy {
|
|
1
|
+
import { AGENTS_FILE } from './index.js';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export class GeminiStrategy {
|
|
11
5
|
name = 'gemini';
|
|
12
6
|
fromFile;
|
|
13
|
-
constructor(fromFile =
|
|
7
|
+
constructor(fromFile = AGENTS_FILE) {
|
|
14
8
|
this.fromFile = fromFile;
|
|
15
9
|
}
|
|
16
10
|
async sync(_context, projectRoot, targetDir) {
|
|
17
11
|
const outputDir = targetDir ?? projectRoot;
|
|
18
|
-
const geminiDir =
|
|
19
|
-
const settingsPath =
|
|
12
|
+
const geminiDir = path.join(outputDir, '.gemini');
|
|
13
|
+
const settingsPath = path.join(geminiDir, 'settings.json');
|
|
20
14
|
// Compute the path to the source file relative to the target .gemini directory
|
|
21
|
-
const agentsAbsPath =
|
|
22
|
-
const agentsRelativePath =
|
|
23
|
-
await
|
|
15
|
+
const agentsAbsPath = path.join(projectRoot, this.fromFile);
|
|
16
|
+
const agentsRelativePath = path.relative(outputDir, agentsAbsPath);
|
|
17
|
+
await fs.ensureDir(geminiDir);
|
|
24
18
|
let settings = {};
|
|
25
19
|
let exists = false;
|
|
26
|
-
if (await
|
|
20
|
+
if (await fs.pathExists(settingsPath)) {
|
|
27
21
|
exists = true;
|
|
28
22
|
try {
|
|
29
|
-
settings = await
|
|
23
|
+
settings = await fs.readJson(settingsPath);
|
|
30
24
|
}
|
|
31
|
-
catch
|
|
25
|
+
catch {
|
|
32
26
|
settings = {};
|
|
33
27
|
}
|
|
34
28
|
}
|
|
@@ -54,8 +48,7 @@ class GeminiStrategy {
|
|
|
54
48
|
modified = true;
|
|
55
49
|
}
|
|
56
50
|
if (modified || !exists) {
|
|
57
|
-
await
|
|
51
|
+
await fs.writeJson(settingsPath, settings, { spaces: 2 });
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
}
|
|
61
|
-
exports.GeminiStrategy = GeminiStrategy;
|