@dezkareid/ai-context-sync 1.1.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/dist/engine.js CHANGED
@@ -16,8 +16,9 @@ class SyncEngine {
16
16
  new gemini_js_1.GeminiStrategy(),
17
17
  new gemini_md_js_1.GeminiMdStrategy()
18
18
  ];
19
- async sync(projectRoot, selectedStrategies) {
19
+ async sync(projectRoot, selectedStrategies, targetDir) {
20
20
  const agentsPath = path_1.default.join(projectRoot, constants_js_1.AGENTS_FILENAME);
21
+ const outputDir = targetDir ?? projectRoot;
21
22
  if (!(await fs_extra_1.default.pathExists(agentsPath))) {
22
23
  throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
23
24
  }
@@ -42,7 +43,7 @@ class SyncEngine {
42
43
  }
43
44
  for (const strategy of strategiesToRun) {
44
45
  console.log(`Syncing for ${strategy.name}...`);
45
- await strategy.sync(context, projectRoot);
46
+ await strategy.sync(context, projectRoot, outputDir);
46
47
  }
47
48
  }
48
49
  }
@@ -89,4 +89,62 @@ const os_1 = __importDefault(require("os"));
89
89
  await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
90
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
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
+ });
92
150
  });
package/dist/index.js CHANGED
@@ -18,12 +18,14 @@ program
18
18
  program
19
19
  .command('sync')
20
20
  .description('Synchronize context files from AGENTS.md')
21
- .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)')
22
23
  .option('-s, --strategy <strategy>', 'Sync strategy (claude, gemini, all, or comma-separated list)')
23
24
  .option('--skip-config', 'Avoid reading/creating the config file', false)
24
25
  .action(async (options) => {
25
26
  try {
26
27
  const projectRoot = path_1.default.resolve(options.dir);
28
+ const targetDir = options.targetDir ? path_1.default.resolve(options.targetDir) : projectRoot;
27
29
  const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
28
30
  let strategy = options.strategy;
29
31
  // 1. If no strategy provided, try to read from config
@@ -70,7 +72,7 @@ program
70
72
  await fs_extra_1.default.writeJson(configPath, { strategies: strategy }, { spaces: 2 });
71
73
  }
72
74
  const engine = new engine_js_1.SyncEngine();
73
- await engine.sync(projectRoot, strategy);
75
+ await engine.sync(projectRoot, strategy, targetDir);
74
76
  const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
75
77
  console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
76
78
  }
@@ -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) {
@@ -8,23 +8,27 @@ const index_js_1 = require("./index.js");
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  class SymlinkStrategy {
11
- async sync(_context, projectRoot) {
12
- const targetPath = path_1.default.join(projectRoot, this.targetFilename);
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);
13
17
  // Check if it exists and what type it is
14
18
  if (await fs_extra_1.default.pathExists(targetPath)) {
15
19
  const stats = await fs_extra_1.default.lstat(targetPath);
16
20
  if (stats.isSymbolicLink()) {
17
- const target = await fs_extra_1.default.readlink(targetPath);
18
- if (target === index_js_1.AGENTS_FILE) {
19
- // Already points to AGENTS.md, nothing to do
21
+ const existingTarget = await fs_extra_1.default.readlink(targetPath);
22
+ if (existingTarget === symlinkTarget) {
23
+ // Already points to the correct location, nothing to do
20
24
  return;
21
25
  }
22
26
  }
23
27
  // If it's a file or a link to somewhere else, remove it
24
28
  await fs_extra_1.default.remove(targetPath);
25
29
  }
26
- // Create relative symlink
27
- await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, targetPath);
30
+ // Create symlink pointing to AGENTS.md
31
+ await fs_extra_1.default.ensureSymlink(symlinkTarget, targetPath);
28
32
  }
29
33
  }
30
34
  exports.SymlinkStrategy = SymlinkStrategy;
@@ -62,4 +62,40 @@ class TestSymlinkStrategy extends symlink_js_1.SymlinkStrategy {
62
62
  const target = await fs_extra_1.default.readlink(targetPath);
63
63
  (0, vitest_1.expect)(target).toBe(index_js_1.AGENTS_FILE);
64
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
+ });
65
101
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dezkareid/ai-context-sync",
3
- "version": "1.1.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": {