@dezkareid/ai-context-sync 1.0.0 → 1.2.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 CHANGED
@@ -8,6 +8,7 @@ A CLI utility to synchronize AI agent context files across different providers,
8
8
  - **Multi-provider support**:
9
9
  - **Claude**: Generates/updates `CLAUDE.md`.
10
10
  - **Gemini**: Configures `.gemini/settings.json` to use `AGENTS.md`.
11
+ - **Gemini Markdown**: Generates/updates `GEMINI.md`.
11
12
  - **Plugin Architecture**: Easily extendable to support other AI agents.
12
13
 
13
14
  ## Installation
@@ -23,15 +24,32 @@ pnpm install @dezkareid/ai-context-sync
23
24
  Run the sync command in your project root (where `AGENTS.md` is located):
24
25
 
25
26
  ```bash
26
- npx ai-context-sync sync
27
+ npx @dezkareid/ai-context-sync sync
27
28
  ```
28
29
 
29
- Or specify a directory:
30
+ You can select the strategy using the `--strategy` (or `-s`) option:
30
31
 
31
32
  ```bash
32
- npx ai-context-sync sync --dir ./my-package
33
+ npx @dezkareid/ai-context-sync sync --strategy claude
34
+ npx @dezkareid/ai-context-sync sync --strategy gemini
35
+ npx @dezkareid/ai-context-sync sync --strategy all
36
+ npx @dezkareid/ai-context-sync sync --strategy "claude, gemini"
33
37
  ```
34
38
 
39
+ If no strategy is provided, an interactive checkbox menu will appear to let you toggle which strategies to run.
40
+
41
+ ### Configuration
42
+
43
+ The tool can save your selected strategies in a `.ai-context-configrc` file in the project root. This avoids being prompted every time you run the command.
44
+
45
+ To bypass reading or creating this configuration file, use the `--skip-config` flag:
46
+
47
+ ```bash
48
+ npx @dezkareid/ai-context-sync sync --skip-config
49
+ ```
50
+
51
+ ### Directory option
52
+
35
53
  ## How it works
36
54
 
37
55
  1. The tool looks for an `AGENTS.md` file in the target directory.
package/dist/constants.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AGENTS_FILENAME = void 0;
3
+ exports.CONFIG_FILENAME = exports.AGENTS_FILENAME = void 0;
4
4
  exports.AGENTS_FILENAME = 'AGENTS.md';
5
+ exports.CONFIG_FILENAME = '.ai-context-configrc';
package/dist/engine.js CHANGED
@@ -9,21 +9,41 @@ const path_1 = __importDefault(require("path"));
9
9
  const constants_js_1 = require("./constants.js");
10
10
  const claude_js_1 = require("./strategies/claude.js");
11
11
  const gemini_js_1 = require("./strategies/gemini.js");
12
+ const gemini_md_js_1 = require("./strategies/gemini-md.js");
12
13
  class SyncEngine {
13
- strategies = [];
14
- constructor() {
15
- this.strategies.push(new claude_js_1.ClaudeStrategy());
16
- this.strategies.push(new gemini_js_1.GeminiStrategy());
17
- }
18
- async sync(projectRoot) {
14
+ allStrategies = [
15
+ new claude_js_1.ClaudeStrategy(),
16
+ new gemini_js_1.GeminiStrategy(),
17
+ new gemini_md_js_1.GeminiMdStrategy()
18
+ ];
19
+ async sync(projectRoot, selectedStrategies, targetDir) {
19
20
  const agentsPath = path_1.default.join(projectRoot, constants_js_1.AGENTS_FILENAME);
21
+ const outputDir = targetDir ?? projectRoot;
20
22
  if (!(await fs_extra_1.default.pathExists(agentsPath))) {
21
23
  throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
22
24
  }
23
25
  const context = await fs_extra_1.default.readFile(agentsPath, 'utf-8');
24
- for (const strategy of this.strategies) {
26
+ let strategiesToRun;
27
+ if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
28
+ strategiesToRun = this.allStrategies;
29
+ }
30
+ else {
31
+ const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
32
+ const normalizedList = selectedList.map(s => s.toLowerCase());
33
+ if (normalizedList.includes('all') || normalizedList.includes('both')) {
34
+ strategiesToRun = this.allStrategies;
35
+ }
36
+ else {
37
+ strategiesToRun = this.allStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
38
+ }
39
+ }
40
+ const availableNames = this.allStrategies.map(s => s.name).join(', ');
41
+ if (strategiesToRun.length === 0 && selectedStrategies) {
42
+ throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
43
+ }
44
+ for (const strategy of strategiesToRun) {
25
45
  console.log(`Syncing for ${strategy.name}...`);
26
- await strategy.sync(context, projectRoot);
46
+ await strategy.sync(context, projectRoot, outputDir);
27
47
  }
28
48
  }
29
49
  }
@@ -37,4 +37,114 @@ const os_1 = __importDefault(require("os"));
37
37
  const settings = await fs_extra_1.default.readJson(settingsPath);
38
38
  (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
39
39
  });
40
+ (0, vitest_1.it)('should run only Claude strategy when selected', async () => {
41
+ const engine = new engine_js_1.SyncEngine();
42
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
43
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
44
+ await engine.sync(tempDir, 'claude');
45
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
46
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
47
+ const geminiDir = path_1.default.join(tempDir, '.gemini');
48
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(geminiDir)).toBe(false);
49
+ });
50
+ (0, vitest_1.it)('should run only Gemini strategy when selected', async () => {
51
+ const engine = new engine_js_1.SyncEngine();
52
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
53
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
54
+ await engine.sync(tempDir, 'gemini');
55
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
56
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(false);
57
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
58
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
59
+ });
60
+ (0, vitest_1.it)('should run Gemini Markdown strategy when selected', async () => {
61
+ const engine = new engine_js_1.SyncEngine();
62
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
63
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
64
+ await engine.sync(tempDir, 'gemini-md');
65
+ const geminiMdPath = path_1.default.join(tempDir, 'GEMINI.md');
66
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(geminiMdPath)).toBe(true);
67
+ const stats = await fs_extra_1.default.lstat(geminiMdPath);
68
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
69
+ });
70
+ (0, vitest_1.it)('should run multiple selected strategies from array', async () => {
71
+ const engine = new engine_js_1.SyncEngine();
72
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
73
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
74
+ await engine.sync(tempDir, ['claude', 'gemini']);
75
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, 'CLAUDE.md'))).toBe(true);
76
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, '.gemini', 'settings.json'))).toBe(true);
77
+ });
78
+ (0, vitest_1.it)('should run all strategies when "all" is selected', async () => {
79
+ const engine = new engine_js_1.SyncEngine();
80
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
81
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
82
+ await engine.sync(tempDir, 'all');
83
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, 'CLAUDE.md'))).toBe(true);
84
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(path_1.default.join(tempDir, '.gemini', 'settings.json'))).toBe(true);
85
+ });
86
+ (0, vitest_1.it)('should throw error for invalid strategy', async () => {
87
+ const engine = new engine_js_1.SyncEngine();
88
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
89
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
90
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'invalid')).rejects.toThrow('No valid strategies found for: invalid. Available strategies: claude, gemini, gemini-md');
91
+ });
92
+ (0, vitest_1.describe)('targetDir option', () => {
93
+ let targetDir;
94
+ (0, vitest_1.beforeEach)(async () => {
95
+ targetDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'sync-engine-target-'));
96
+ });
97
+ (0, vitest_1.afterEach)(async () => {
98
+ await fs_extra_1.default.remove(targetDir);
99
+ });
100
+ (0, vitest_1.it)('should write synced files to targetDir instead of projectRoot', async () => {
101
+ const engine = new engine_js_1.SyncEngine();
102
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
103
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
104
+ await engine.sync(tempDir, 'claude', targetDir);
105
+ // File should be in targetDir
106
+ const claudeInTarget = path_1.default.join(targetDir, 'CLAUDE.md');
107
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInTarget)).toBe(true);
108
+ // File should NOT be in projectRoot (tempDir)
109
+ const claudeInSource = path_1.default.join(tempDir, 'CLAUDE.md');
110
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInSource)).toBe(false);
111
+ });
112
+ (0, vitest_1.it)('should create symlink in targetDir pointing back to projectRoot AGENTS.md', async () => {
113
+ const engine = new engine_js_1.SyncEngine();
114
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
115
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
116
+ await engine.sync(tempDir, 'claude', targetDir);
117
+ const claudePath = path_1.default.join(targetDir, 'CLAUDE.md');
118
+ const stats = await fs_extra_1.default.lstat(claudePath);
119
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
120
+ // The symlink should resolve to the AGENTS.md in projectRoot
121
+ const resolvedPath = await fs_extra_1.default.realpath(claudePath);
122
+ const expectedPath = await fs_extra_1.default.realpath(agentsPath);
123
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
124
+ });
125
+ (0, vitest_1.it)('should write .gemini/settings.json to targetDir when targetDir is specified', async () => {
126
+ const engine = new engine_js_1.SyncEngine();
127
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
128
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
129
+ await engine.sync(tempDir, 'gemini', targetDir);
130
+ // Settings should be in targetDir
131
+ const settingsInTarget = path_1.default.join(targetDir, '.gemini', 'settings.json');
132
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsInTarget)).toBe(true);
133
+ // Settings should NOT be in projectRoot
134
+ const settingsInSource = path_1.default.join(tempDir, '.gemini', 'settings.json');
135
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsInSource)).toBe(false);
136
+ // The stored path should be relative from targetDir to AGENTS.md
137
+ const settings = await fs_extra_1.default.readJson(settingsInTarget);
138
+ const expectedRelPath = path_1.default.relative(targetDir, agentsPath);
139
+ (0, vitest_1.expect)(settings.context.fileName).toContain(expectedRelPath);
140
+ });
141
+ (0, vitest_1.it)('should use projectRoot as targetDir when targetDir is not specified', async () => {
142
+ const engine = new engine_js_1.SyncEngine();
143
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
144
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
145
+ await engine.sync(tempDir, 'claude');
146
+ const claudeInSource = path_1.default.join(tempDir, 'CLAUDE.md');
147
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInSource)).toBe(true);
148
+ });
149
+ });
40
150
  });
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const engine_js_1 = require("./engine.js");
9
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");
10
13
  const program = new commander_1.Command();
11
14
  program
12
15
  .name('ai-context-sync')
@@ -15,13 +18,63 @@ program
15
18
  program
16
19
  .command('sync')
17
20
  .description('Synchronize context files from AGENTS.md')
18
- .option('-d, --dir <path>', 'Project directory', process.cwd())
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('--skip-config', 'Avoid reading/creating the config file', false)
19
25
  .action(async (options) => {
20
26
  try {
21
- const engine = new engine_js_1.SyncEngine();
22
27
  const projectRoot = path_1.default.resolve(options.dir);
23
- await engine.sync(projectRoot);
24
- console.log('Successfully synchronized context files!');
28
+ const targetDir = options.targetDir ? path_1.default.resolve(options.targetDir) : projectRoot;
29
+ const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
30
+ let strategy = options.strategy;
31
+ // 1. If no strategy provided, try to read from config
32
+ if (!strategy && !options.skipConfig) {
33
+ if (await fs_extra_1.default.pathExists(configPath)) {
34
+ try {
35
+ const config = await fs_extra_1.default.readJson(configPath);
36
+ if (config.strategies) {
37
+ strategy = config.strategies;
38
+ }
39
+ }
40
+ catch (e) {
41
+ // Ignore corrupted config and proceed to prompt
42
+ }
43
+ }
44
+ }
45
+ // 2. If still no strategy, prompt user
46
+ if (!strategy) {
47
+ const answers = await inquirer_1.default.prompt([
48
+ {
49
+ type: 'checkbox',
50
+ name: 'strategies',
51
+ message: 'Select the AI context files to sync:',
52
+ choices: [
53
+ { name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
54
+ { name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
55
+ { name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true }
56
+ ],
57
+ validate: (answer) => {
58
+ if (answer.length < 1) {
59
+ return 'You must choose at least one strategy.';
60
+ }
61
+ return true;
62
+ }
63
+ }
64
+ ]);
65
+ strategy = answers.strategies;
66
+ }
67
+ else if (typeof strategy === 'string') {
68
+ strategy = strategy.split(',').map(s => s.trim());
69
+ }
70
+ // 3. Save strategy to config if not skipping
71
+ if (!options.skipConfig) {
72
+ await fs_extra_1.default.writeJson(configPath, { strategies: strategy }, { spaces: 2 });
73
+ }
74
+ const engine = new engine_js_1.SyncEngine();
75
+ await engine.sync(projectRoot, strategy, targetDir);
76
+ const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
77
+ console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
25
78
  }
26
79
  catch (error) {
27
80
  console.error(`Error: ${error.message}`);
@@ -1,31 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.ClaudeStrategy = 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 ClaudeStrategy {
4
+ const symlink_js_1 = require("./symlink.js");
5
+ class ClaudeStrategy extends symlink_js_1.SymlinkStrategy {
11
6
  name = 'claude';
12
- async sync(_context, projectRoot) {
13
- const claudePath = path_1.default.join(projectRoot, 'CLAUDE.md');
14
- // Check if it exists and what type it is
15
- if (await fs_extra_1.default.pathExists(claudePath)) {
16
- const stats = await fs_extra_1.default.lstat(claudePath);
17
- if (stats.isSymbolicLink()) {
18
- const target = await fs_extra_1.default.readlink(claudePath);
19
- if (target === index_js_1.AGENTS_FILE) {
20
- // Already points to AGENTS.md, nothing to do
21
- return;
22
- }
23
- }
24
- // If it's a file or a link to somewhere else, remove it
25
- await fs_extra_1.default.remove(claudePath);
26
- }
27
- // Create relative symlink
28
- await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, claudePath);
29
- }
7
+ targetFilename = 'CLAUDE.md';
30
8
  }
31
9
  exports.ClaudeStrategy = ClaudeStrategy;
@@ -28,26 +28,4 @@ const os_1 = __importDefault(require("os"));
28
28
  const target = await fs_extra_1.default.readlink(claudePath);
29
29
  (0, vitest_1.expect)(target).toBe(constants_js_1.AGENTS_FILENAME);
30
30
  });
31
- (0, vitest_1.it)('should replace existing file with a symbolic link', async () => {
32
- const strategy = new claude_js_1.ClaudeStrategy();
33
- const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
34
- await fs_extra_1.default.writeFile(claudePath, 'old content');
35
- await strategy.sync('# Agents', tempDir);
36
- const stats = await fs_extra_1.default.lstat(claudePath);
37
- (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
38
- });
39
- (0, vitest_1.it)('should not recreate the link if it already exists and points to AGENTS.md', async () => {
40
- const strategy = new claude_js_1.ClaudeStrategy();
41
- const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
42
- const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
43
- await fs_extra_1.default.writeFile(agentsPath, '# Agents');
44
- await fs_extra_1.default.ensureSymlink(constants_js_1.AGENTS_FILENAME, claudePath);
45
- const statsBefore = await fs_extra_1.default.lstat(claudePath);
46
- const mtimeBefore = statsBefore.mtimeMs;
47
- await new Promise(resolve => setTimeout(resolve, 10));
48
- await strategy.sync('# Agents', tempDir);
49
- const statsAfter = await fs_extra_1.default.lstat(claudePath);
50
- const mtimeAfter = statsAfter.mtimeMs;
51
- (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
52
- });
53
31
  });
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GeminiMdStrategy = void 0;
4
+ const symlink_js_1 = require("./symlink.js");
5
+ class GeminiMdStrategy extends symlink_js_1.SymlinkStrategy {
6
+ name = 'gemini-md';
7
+ targetFilename = 'GEMINI.md';
8
+ }
9
+ exports.GeminiMdStrategy = GeminiMdStrategy;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const gemini_md_js_1 = require("./gemini-md.js");
8
+ const index_js_1 = require("./index.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ (0, vitest_1.describe)('GeminiMdStrategy', () => {
13
+ let tempDir;
14
+ let strategy;
15
+ (0, vitest_1.beforeEach)(async () => {
16
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'gemini-md-strategy-test-'));
17
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, index_js_1.AGENTS_FILE), '# Agents');
18
+ strategy = new gemini_md_js_1.GeminiMdStrategy();
19
+ });
20
+ (0, vitest_1.afterEach)(async () => {
21
+ await fs_extra_1.default.remove(tempDir);
22
+ });
23
+ (0, vitest_1.it)('should create a symbolic link GEMINI.md pointing to AGENTS.md', async () => {
24
+ await strategy.sync('', tempDir);
25
+ const targetPath = path_1.default.join(tempDir, 'GEMINI.md');
26
+ const stats = await fs_extra_1.default.lstat(targetPath);
27
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
28
+ const target = await fs_extra_1.default.readlink(targetPath);
29
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
30
+ });
31
+ });
@@ -9,9 +9,13 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  class GeminiStrategy {
11
11
  name = 'gemini';
12
- async sync(_context, projectRoot) {
13
- const geminiDir = path_1.default.join(projectRoot, '.gemini');
12
+ async sync(_context, projectRoot, targetDir) {
13
+ const outputDir = targetDir ?? projectRoot;
14
+ const geminiDir = path_1.default.join(outputDir, '.gemini');
14
15
  const settingsPath = path_1.default.join(geminiDir, 'settings.json');
16
+ // Compute the path to AGENTS.md relative to the target .gemini directory
17
+ const agentsAbsPath = path_1.default.join(projectRoot, index_js_1.AGENTS_FILE);
18
+ const agentsRelativePath = path_1.default.relative(outputDir, agentsAbsPath);
15
19
  await fs_extra_1.default.ensureDir(geminiDir);
16
20
  let settings = {};
17
21
  let exists = false;
@@ -30,19 +34,19 @@ class GeminiStrategy {
30
34
  const currentFiles = settings.context.fileName;
31
35
  let modified = false;
32
36
  if (Array.isArray(currentFiles)) {
33
- if (!currentFiles.includes(index_js_1.AGENTS_FILE)) {
34
- settings.context.fileName = [...currentFiles, index_js_1.AGENTS_FILE];
37
+ if (!currentFiles.includes(agentsRelativePath)) {
38
+ settings.context.fileName = [...currentFiles, agentsRelativePath];
35
39
  modified = true;
36
40
  }
37
41
  }
38
42
  else if (typeof currentFiles === 'string') {
39
- if (currentFiles !== index_js_1.AGENTS_FILE) {
40
- settings.context.fileName = [currentFiles, index_js_1.AGENTS_FILE];
43
+ if (currentFiles !== agentsRelativePath) {
44
+ settings.context.fileName = [currentFiles, agentsRelativePath];
41
45
  modified = true;
42
46
  }
43
47
  }
44
48
  else {
45
- settings.context.fileName = [index_js_1.AGENTS_FILE];
49
+ settings.context.fileName = [agentsRelativePath];
46
50
  modified = true;
47
51
  }
48
52
  if (modified || !exists) {
@@ -83,6 +83,21 @@ const os_1 = __importDefault(require("os"));
83
83
  const mtimeAfter = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
84
84
  (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
85
85
  });
86
+ (0, vitest_1.it)('should not write to settings.json if fileName is already AGENTS.md as a string', async () => {
87
+ const strategy = new gemini_js_1.GeminiStrategy();
88
+ const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
89
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
90
+ await fs_extra_1.default.writeJson(settingsPath, {
91
+ context: {
92
+ fileName: constants_js_1.AGENTS_FILENAME
93
+ }
94
+ });
95
+ const mtimeBefore = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
96
+ await new Promise(resolve => setTimeout(resolve, 10));
97
+ await strategy.sync('context', tempDir);
98
+ const mtimeAfter = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
99
+ (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
100
+ });
86
101
  (0, vitest_1.it)('should handle invalid JSON in settings.json by starting fresh', async () => {
87
102
  const strategy = new gemini_js_1.GeminiStrategy();
88
103
  const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SymlinkStrategy = 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 SymlinkStrategy {
11
+ async sync(_context, projectRoot, targetDir) {
12
+ const outputDir = targetDir ?? projectRoot;
13
+ const targetPath = path_1.default.join(outputDir, this.targetFilename);
14
+ // Compute the symlink value: relative path from outputDir to the AGENTS.md in projectRoot
15
+ const agentsAbsPath = path_1.default.join(projectRoot, index_js_1.AGENTS_FILE);
16
+ const symlinkTarget = path_1.default.relative(outputDir, agentsAbsPath);
17
+ // Check if it exists and what type it is
18
+ if (await fs_extra_1.default.pathExists(targetPath)) {
19
+ const stats = await fs_extra_1.default.lstat(targetPath);
20
+ if (stats.isSymbolicLink()) {
21
+ const existingTarget = await fs_extra_1.default.readlink(targetPath);
22
+ if (existingTarget === symlinkTarget) {
23
+ // Already points to the correct location, nothing to do
24
+ return;
25
+ }
26
+ }
27
+ // If it's a file or a link to somewhere else, remove it
28
+ await fs_extra_1.default.remove(targetPath);
29
+ }
30
+ // Create symlink pointing to AGENTS.md
31
+ await fs_extra_1.default.ensureSymlink(symlinkTarget, targetPath);
32
+ }
33
+ }
34
+ exports.SymlinkStrategy = SymlinkStrategy;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const symlink_js_1 = require("./symlink.js");
8
+ const index_js_1 = require("./index.js");
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ class TestSymlinkStrategy extends symlink_js_1.SymlinkStrategy {
13
+ name = 'test';
14
+ targetFilename = 'TEST.md';
15
+ }
16
+ (0, vitest_1.describe)('SymlinkStrategy', () => {
17
+ let tempDir;
18
+ let strategy;
19
+ (0, vitest_1.beforeEach)(async () => {
20
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'symlink-strategy-test-'));
21
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, index_js_1.AGENTS_FILE), '# Agents');
22
+ strategy = new TestSymlinkStrategy();
23
+ });
24
+ (0, vitest_1.afterEach)(async () => {
25
+ await fs_extra_1.default.remove(tempDir);
26
+ });
27
+ (0, vitest_1.it)('should create a symlink to AGENTS.md', async () => {
28
+ await strategy.sync('', tempDir);
29
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
30
+ const stats = await fs_extra_1.default.lstat(targetPath);
31
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
32
+ const target = await fs_extra_1.default.readlink(targetPath);
33
+ // It might be absolute or relative depending on environment/implementation
34
+ (0, vitest_1.expect)(target).toContain(index_js_1.AGENTS_FILE);
35
+ });
36
+ (0, vitest_1.it)('should not recreate symlink if it already points to AGENTS.md', async () => {
37
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
38
+ await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, targetPath);
39
+ // Get initial mtime
40
+ const initialStats = await fs_extra_1.default.lstat(targetPath);
41
+ await strategy.sync('', tempDir);
42
+ const finalStats = await fs_extra_1.default.lstat(targetPath);
43
+ (0, vitest_1.expect)(finalStats.mtimeMs).toBe(initialStats.mtimeMs);
44
+ const target = await fs_extra_1.default.readlink(targetPath);
45
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
46
+ });
47
+ (0, vitest_1.it)('should recreate symlink if it points elsewhere', async () => {
48
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
49
+ const otherFile = path_1.default.join(tempDir, 'OTHER.md');
50
+ await fs_extra_1.default.writeFile(otherFile, 'other content');
51
+ await fs_extra_1.default.ensureSymlink('OTHER.md', targetPath);
52
+ await strategy.sync('', tempDir);
53
+ const target = await fs_extra_1.default.readlink(targetPath);
54
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
55
+ });
56
+ (0, vitest_1.it)('should replace existing file with a symlink', async () => {
57
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
58
+ await fs_extra_1.default.writeFile(targetPath, 'existing file');
59
+ await strategy.sync('', tempDir);
60
+ const stats = await fs_extra_1.default.lstat(targetPath);
61
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
62
+ const target = await fs_extra_1.default.readlink(targetPath);
63
+ (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
64
+ });
65
+ (0, vitest_1.describe)('targetDir option', () => {
66
+ let targetDir;
67
+ (0, vitest_1.beforeEach)(async () => {
68
+ targetDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'symlink-strategy-target-'));
69
+ });
70
+ (0, vitest_1.afterEach)(async () => {
71
+ await fs_extra_1.default.remove(targetDir);
72
+ });
73
+ (0, vitest_1.it)('should create symlink in targetDir pointing to AGENTS.md in projectRoot', async () => {
74
+ await strategy.sync('', tempDir, targetDir);
75
+ const targetPath = path_1.default.join(targetDir, 'TEST.md');
76
+ const stats = await fs_extra_1.default.lstat(targetPath);
77
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
78
+ // Symlink should resolve to the actual AGENTS.md in projectRoot
79
+ const resolvedPath = await fs_extra_1.default.realpath(targetPath);
80
+ const expectedPath = await fs_extra_1.default.realpath(path_1.default.join(tempDir, index_js_1.AGENTS_FILE));
81
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
82
+ });
83
+ (0, vitest_1.it)('should not recreate symlink if it already points to the correct location', async () => {
84
+ const targetPath = path_1.default.join(targetDir, 'TEST.md');
85
+ const agentsAbsPath = path_1.default.join(tempDir, index_js_1.AGENTS_FILE);
86
+ const symlinkTarget = path_1.default.relative(targetDir, agentsAbsPath);
87
+ await fs_extra_1.default.ensureSymlink(symlinkTarget, targetPath);
88
+ const initialStats = await fs_extra_1.default.lstat(targetPath);
89
+ await strategy.sync('', tempDir, targetDir);
90
+ const finalStats = await fs_extra_1.default.lstat(targetPath);
91
+ (0, vitest_1.expect)(finalStats.mtimeMs).toBe(initialStats.mtimeMs);
92
+ });
93
+ (0, vitest_1.it)('should write to projectRoot when targetDir is not provided', async () => {
94
+ await strategy.sync('', tempDir);
95
+ const targetPath = path_1.default.join(tempDir, 'TEST.md');
96
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(targetPath)).toBe(true);
97
+ const targetPathInTargetDir = path_1.default.join(targetDir, 'TEST.md');
98
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(targetPathInTargetDir)).toBe(false);
99
+ });
100
+ });
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dezkareid/ai-context-sync",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI utility to synchronize AI agent context files from AGENTS.md",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -36,10 +36,12 @@
36
36
  "dependencies": {
37
37
  "commander": "12.0.0",
38
38
  "fs-extra": "11.2.0",
39
- "globby": "14.0.1"
39
+ "globby": "14.0.1",
40
+ "inquirer": "9.2.12"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/fs-extra": "11.0.4",
44
+ "@types/inquirer": "9.0.7",
43
45
  "@types/node": "25.0.10",
44
46
  "@vitest/coverage-v8": "4.0.18",
45
47
  "typescript": "5.9.3",