@dezkareid/ai-context-sync 1.1.0 → 1.3.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
@@ -10,39 +10,50 @@ 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
12
  const gemini_md_js_1 = require("./strategies/gemini-md.js");
13
+ const other_js_1 = require("./strategies/other.js");
13
14
  class SyncEngine {
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) {
20
- const agentsPath = path_1.default.join(projectRoot, constants_js_1.AGENTS_FILENAME);
15
+ async sync(projectRoot, selectedStrategies, targetDir, fromFile, otherFiles) {
16
+ const sourceFile = fromFile ?? constants_js_1.AGENTS_FILENAME;
17
+ const agentsPath = path_1.default.join(projectRoot, sourceFile);
18
+ const outputDir = targetDir ?? projectRoot;
21
19
  if (!(await fs_extra_1.default.pathExists(agentsPath))) {
22
- throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
20
+ throw new Error(`${sourceFile} not found in ${projectRoot}`);
23
21
  }
24
22
  const context = await fs_extra_1.default.readFile(agentsPath, 'utf-8');
23
+ const builtInStrategies = [
24
+ new claude_js_1.ClaudeStrategy(fromFile),
25
+ new gemini_js_1.GeminiStrategy(fromFile),
26
+ new gemini_md_js_1.GeminiMdStrategy(fromFile)
27
+ ];
25
28
  let strategiesToRun;
26
29
  if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
27
- strategiesToRun = this.allStrategies;
30
+ strategiesToRun = builtInStrategies;
28
31
  }
29
32
  else {
30
33
  const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
31
34
  const normalizedList = selectedList.map(s => s.toLowerCase());
32
35
  if (normalizedList.includes('all') || normalizedList.includes('both')) {
33
- strategiesToRun = this.allStrategies;
36
+ strategiesToRun = builtInStrategies;
34
37
  }
35
38
  else {
36
- strategiesToRun = this.allStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
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
+ }
37
48
  }
38
49
  }
39
- const availableNames = this.allStrategies.map(s => s.name).join(', ');
50
+ const availableNames = [...builtInStrategies.map(s => s.name), 'other'].join(', ');
40
51
  if (strategiesToRun.length === 0 && selectedStrategies) {
41
52
  throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
42
53
  }
43
54
  for (const strategy of strategiesToRun) {
44
55
  console.log(`Syncing for ${strategy.name}...`);
45
- await strategy.sync(context, projectRoot);
56
+ await strategy.sync(context, projectRoot, outputDir);
46
57
  }
47
58
  }
48
59
  }
@@ -87,6 +87,139 @@ const os_1 = __importDefault(require("os"));
87
87
  const engine = new engine_js_1.SyncEngine();
88
88
  const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
89
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');
90
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'invalid')).rejects.toThrow('No valid strategies found for: invalid. Available strategies: claude, gemini, gemini-md, other');
91
+ });
92
+ (0, vitest_1.describe)('otherFiles option', () => {
93
+ (0, vitest_1.it)('should create a symlink for a single otherFile', async () => {
94
+ const engine = new engine_js_1.SyncEngine();
95
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
96
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
97
+ await engine.sync(tempDir, 'other', undefined, undefined, ['CURSOR.md']);
98
+ const cursorPath = path_1.default.join(tempDir, 'CURSOR.md');
99
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(cursorPath)).toBe(true);
100
+ const stats = await fs_extra_1.default.lstat(cursorPath);
101
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
102
+ });
103
+ (0, vitest_1.it)('should create symlinks for multiple otherFiles', async () => {
104
+ const engine = new engine_js_1.SyncEngine();
105
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
106
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
107
+ await engine.sync(tempDir, ['other'], undefined, undefined, ['CURSOR.md', 'COPILOT.md']);
108
+ const cursorPath = path_1.default.join(tempDir, 'CURSOR.md');
109
+ const copilotPath = path_1.default.join(tempDir, 'COPILOT.md');
110
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(cursorPath)).toBe(true);
111
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(copilotPath)).toBe(true);
112
+ });
113
+ (0, vitest_1.it)('should throw descriptive error when strategy is "other" but otherFiles is not provided', async () => {
114
+ const engine = new engine_js_1.SyncEngine();
115
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
116
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
117
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'other')).rejects.toThrow('Strategy "other" requires otherFiles to be specified.');
118
+ });
119
+ (0, vitest_1.it)('should throw descriptive error when strategy is "other" but otherFiles is empty', async () => {
120
+ const engine = new engine_js_1.SyncEngine();
121
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
122
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
123
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'other', undefined, undefined, [])).rejects.toThrow('Strategy "other" requires otherFiles to be specified.');
124
+ });
125
+ (0, vitest_1.it)('should run both built-in and custom strategies when combined', 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, ['claude', 'other'], undefined, undefined, ['CURSOR.md']);
130
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
131
+ const cursorPath = path_1.default.join(tempDir, 'CURSOR.md');
132
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
133
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(cursorPath)).toBe(true);
134
+ });
135
+ });
136
+ (0, vitest_1.describe)('fromFile option', () => {
137
+ (0, vitest_1.it)('should use a custom source file when fromFile is provided', async () => {
138
+ const engine = new engine_js_1.SyncEngine();
139
+ const customSource = 'MY_AGENTS.md';
140
+ const customSourcePath = path_1.default.join(tempDir, customSource);
141
+ await fs_extra_1.default.writeFile(customSourcePath, '# Custom Agent Context');
142
+ await engine.sync(tempDir, 'claude', undefined, customSource);
143
+ const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
144
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
145
+ const stats = await fs_extra_1.default.lstat(claudePath);
146
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
147
+ // Symlink should resolve to the custom source file
148
+ const resolvedPath = await fs_extra_1.default.realpath(claudePath);
149
+ const expectedPath = await fs_extra_1.default.realpath(customSourcePath);
150
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
151
+ });
152
+ (0, vitest_1.it)('should throw error when custom fromFile does not exist', async () => {
153
+ const engine = new engine_js_1.SyncEngine();
154
+ await (0, vitest_1.expect)(engine.sync(tempDir, 'claude', undefined, 'MISSING.md')).rejects.toThrow('MISSING.md not found in');
155
+ });
156
+ (0, vitest_1.it)('should use custom fromFile with other strategy', async () => {
157
+ const engine = new engine_js_1.SyncEngine();
158
+ const customSource = 'MY_AGENTS.md';
159
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, customSource), '# Custom Agent Context');
160
+ await engine.sync(tempDir, 'other', undefined, customSource, ['CURSOR.md']);
161
+ const cursorPath = path_1.default.join(tempDir, 'CURSOR.md');
162
+ const resolvedPath = await fs_extra_1.default.realpath(cursorPath);
163
+ const expectedPath = await fs_extra_1.default.realpath(path_1.default.join(tempDir, customSource));
164
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
165
+ });
166
+ });
167
+ (0, vitest_1.describe)('targetDir option', () => {
168
+ let targetDir;
169
+ (0, vitest_1.beforeEach)(async () => {
170
+ targetDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'sync-engine-target-'));
171
+ });
172
+ (0, vitest_1.afterEach)(async () => {
173
+ await fs_extra_1.default.remove(targetDir);
174
+ });
175
+ (0, vitest_1.it)('should write synced files to targetDir instead of projectRoot', async () => {
176
+ const engine = new engine_js_1.SyncEngine();
177
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
178
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
179
+ await engine.sync(tempDir, 'claude', targetDir);
180
+ // File should be in targetDir
181
+ const claudeInTarget = path_1.default.join(targetDir, 'CLAUDE.md');
182
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInTarget)).toBe(true);
183
+ // File should NOT be in projectRoot (tempDir)
184
+ const claudeInSource = path_1.default.join(tempDir, 'CLAUDE.md');
185
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInSource)).toBe(false);
186
+ });
187
+ (0, vitest_1.it)('should create symlink in targetDir pointing back to projectRoot AGENTS.md', async () => {
188
+ const engine = new engine_js_1.SyncEngine();
189
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
190
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
191
+ await engine.sync(tempDir, 'claude', targetDir);
192
+ const claudePath = path_1.default.join(targetDir, 'CLAUDE.md');
193
+ const stats = await fs_extra_1.default.lstat(claudePath);
194
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
195
+ // The symlink should resolve to the AGENTS.md in projectRoot
196
+ const resolvedPath = await fs_extra_1.default.realpath(claudePath);
197
+ const expectedPath = await fs_extra_1.default.realpath(agentsPath);
198
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
199
+ });
200
+ (0, vitest_1.it)('should write .gemini/settings.json to targetDir when targetDir is specified', async () => {
201
+ const engine = new engine_js_1.SyncEngine();
202
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
203
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
204
+ await engine.sync(tempDir, 'gemini', targetDir);
205
+ // Settings should be in targetDir
206
+ const settingsInTarget = path_1.default.join(targetDir, '.gemini', 'settings.json');
207
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsInTarget)).toBe(true);
208
+ // Settings should NOT be in projectRoot
209
+ const settingsInSource = path_1.default.join(tempDir, '.gemini', 'settings.json');
210
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsInSource)).toBe(false);
211
+ // The stored path should be relative from targetDir to AGENTS.md
212
+ const settings = await fs_extra_1.default.readJson(settingsInTarget);
213
+ const expectedRelPath = path_1.default.relative(targetDir, agentsPath);
214
+ (0, vitest_1.expect)(settings.context.fileName).toContain(expectedRelPath);
215
+ });
216
+ (0, vitest_1.it)('should use projectRoot as targetDir when targetDir is not specified', async () => {
217
+ const engine = new engine_js_1.SyncEngine();
218
+ const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
219
+ await fs_extra_1.default.writeFile(agentsPath, '# Agent Context');
220
+ await engine.sync(tempDir, 'claude');
221
+ const claudeInSource = path_1.default.join(tempDir, 'CLAUDE.md');
222
+ (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudeInSource)).toBe(true);
223
+ });
91
224
  });
92
225
  });
package/dist/index.js CHANGED
@@ -18,29 +18,53 @@ 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)')
24
+ .option('-f, --files <names>', 'Comma-separated custom filenames for "other" strategy')
25
+ .option('--from <path>', 'Source file path for symlinks (default: AGENTS.md)')
23
26
  .option('--skip-config', 'Avoid reading/creating the config file', false)
24
27
  .action(async (options) => {
25
28
  try {
26
29
  const projectRoot = path_1.default.resolve(options.dir);
30
+ const targetDir = options.targetDir ? path_1.default.resolve(options.targetDir) : projectRoot;
27
31
  const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
28
32
  let strategy = options.strategy;
33
+ let otherFiles;
34
+ let fromFile;
29
35
  // 1. If no strategy provided, try to read from config
30
- if (!strategy && !options.skipConfig) {
36
+ if (!options.skipConfig) {
31
37
  if (await fs_extra_1.default.pathExists(configPath)) {
32
38
  try {
33
39
  const config = await fs_extra_1.default.readJson(configPath);
34
- if (config.strategies) {
40
+ if (!strategy && config.strategies) {
35
41
  strategy = config.strategies;
36
42
  }
43
+ if (config.otherFiles) {
44
+ otherFiles = config.otherFiles;
45
+ }
46
+ if (config.from) {
47
+ fromFile = config.from;
48
+ }
37
49
  }
38
50
  catch (e) {
39
51
  // Ignore corrupted config and proceed to prompt
40
52
  }
41
53
  }
42
54
  }
43
- // 2. If still no strategy, prompt user
55
+ // 2. Resolve --from flag (takes precedence over config)
56
+ if (options.from) {
57
+ if (path_1.default.isAbsolute(options.from)) {
58
+ throw new Error('--from must be a relative path, not an absolute path.');
59
+ }
60
+ fromFile = options.from;
61
+ }
62
+ // 3. Resolve --files flag (merge with config otherFiles)
63
+ if (options.files) {
64
+ const flagFiles = options.files.split(',').map((s) => s.trim()).filter(Boolean);
65
+ otherFiles = [...new Set([...(otherFiles ?? []), ...flagFiles])];
66
+ }
67
+ // 4. If still no strategy, prompt user
44
68
  if (!strategy) {
45
69
  const answers = await inquirer_1.default.prompt([
46
70
  {
@@ -50,7 +74,8 @@ program
50
74
  choices: [
51
75
  { name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
52
76
  { name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
53
- { name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true }
77
+ { name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true },
78
+ { name: 'Other (custom files)', value: 'other', checked: false }
54
79
  ],
55
80
  validate: (answer) => {
56
81
  if (answer.length < 1) {
@@ -61,16 +86,37 @@ program
61
86
  }
62
87
  ]);
63
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
+ }
64
101
  }
65
102
  else if (typeof strategy === 'string') {
66
103
  strategy = strategy.split(',').map(s => s.trim());
67
104
  }
68
- // 3. Save strategy to config if not skipping
105
+ // 5. Validate: if 'other' strategy selected but no otherFiles resolved
106
+ if (Array.isArray(strategy) && strategy.includes('other') && (!otherFiles || otherFiles.length === 0)) {
107
+ throw new Error('Strategy "other" requires --files or a saved "otherFiles" config entry.');
108
+ }
109
+ // 6. Save strategy to config if not skipping
69
110
  if (!options.skipConfig) {
70
- await fs_extra_1.default.writeJson(configPath, { strategies: strategy }, { spaces: 2 });
111
+ const configData = { strategies: strategy };
112
+ if (otherFiles?.length)
113
+ configData.otherFiles = otherFiles;
114
+ if (fromFile)
115
+ configData.from = fromFile;
116
+ await fs_extra_1.default.writeJson(configPath, configData, { spaces: 2 });
71
117
  }
72
118
  const engine = new engine_js_1.SyncEngine();
73
- await engine.sync(projectRoot, strategy);
119
+ await engine.sync(projectRoot, strategy, targetDir, fromFile, otherFiles);
74
120
  const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
75
121
  console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
76
122
  }
@@ -5,5 +5,8 @@ const symlink_js_1 = require("./symlink.js");
5
5
  class ClaudeStrategy extends symlink_js_1.SymlinkStrategy {
6
6
  name = 'claude';
7
7
  targetFilename = 'CLAUDE.md';
8
+ constructor(fromFile) {
9
+ super(fromFile);
10
+ }
8
11
  }
9
12
  exports.ClaudeStrategy = ClaudeStrategy;
@@ -5,5 +5,8 @@ const symlink_js_1 = require("./symlink.js");
5
5
  class GeminiMdStrategy extends symlink_js_1.SymlinkStrategy {
6
6
  name = 'gemini-md';
7
7
  targetFilename = 'GEMINI.md';
8
+ constructor(fromFile) {
9
+ super(fromFile);
10
+ }
8
11
  }
9
12
  exports.GeminiMdStrategy = GeminiMdStrategy;
@@ -9,9 +9,17 @@ 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
+ fromFile;
13
+ constructor(fromFile = index_js_1.AGENTS_FILE) {
14
+ this.fromFile = fromFile;
15
+ }
16
+ async sync(_context, projectRoot, targetDir) {
17
+ const outputDir = targetDir ?? projectRoot;
18
+ const geminiDir = path_1.default.join(outputDir, '.gemini');
14
19
  const settingsPath = path_1.default.join(geminiDir, 'settings.json');
20
+ // Compute the path to the source file relative to the target .gemini directory
21
+ const agentsAbsPath = path_1.default.join(projectRoot, this.fromFile);
22
+ const agentsRelativePath = path_1.default.relative(outputDir, agentsAbsPath);
15
23
  await fs_extra_1.default.ensureDir(geminiDir);
16
24
  let settings = {};
17
25
  let exists = false;
@@ -30,19 +38,19 @@ class GeminiStrategy {
30
38
  const currentFiles = settings.context.fileName;
31
39
  let modified = false;
32
40
  if (Array.isArray(currentFiles)) {
33
- if (!currentFiles.includes(index_js_1.AGENTS_FILE)) {
34
- settings.context.fileName = [...currentFiles, index_js_1.AGENTS_FILE];
41
+ if (!currentFiles.includes(agentsRelativePath)) {
42
+ settings.context.fileName = [...currentFiles, agentsRelativePath];
35
43
  modified = true;
36
44
  }
37
45
  }
38
46
  else if (typeof currentFiles === 'string') {
39
- if (currentFiles !== index_js_1.AGENTS_FILE) {
40
- settings.context.fileName = [currentFiles, index_js_1.AGENTS_FILE];
47
+ if (currentFiles !== agentsRelativePath) {
48
+ settings.context.fileName = [currentFiles, agentsRelativePath];
41
49
  modified = true;
42
50
  }
43
51
  }
44
52
  else {
45
- settings.context.fileName = [index_js_1.AGENTS_FILE];
53
+ settings.context.fileName = [agentsRelativePath];
46
54
  modified = true;
47
55
  }
48
56
  if (modified || !exists) {
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OtherStrategy = void 0;
4
+ const symlink_js_1 = require("./symlink.js");
5
+ class OtherStrategy extends symlink_js_1.SymlinkStrategy {
6
+ name = 'other';
7
+ targetFilename;
8
+ constructor(targetFilename, fromFile) {
9
+ super(fromFile);
10
+ this.targetFilename = targetFilename;
11
+ }
12
+ }
13
+ exports.OtherStrategy = OtherStrategy;
@@ -0,0 +1,58 @@
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 other_js_1 = require("./other.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)('OtherStrategy', () => {
13
+ let tempDir;
14
+ (0, vitest_1.beforeEach)(async () => {
15
+ tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'other-strategy-test-'));
16
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, index_js_1.AGENTS_FILE), '# Agents');
17
+ });
18
+ (0, vitest_1.afterEach)(async () => {
19
+ await fs_extra_1.default.remove(tempDir);
20
+ });
21
+ (0, vitest_1.it)('should create a symlink with the correct targetFilename', async () => {
22
+ const strategy = new other_js_1.OtherStrategy('CURSOR.md');
23
+ await strategy.sync('', tempDir);
24
+ const targetPath = path_1.default.join(tempDir, 'CURSOR.md');
25
+ const stats = await fs_extra_1.default.lstat(targetPath);
26
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
27
+ const target = await fs_extra_1.default.readlink(targetPath);
28
+ (0, vitest_1.expect)(target).toContain(index_js_1.AGENTS_FILE);
29
+ });
30
+ (0, vitest_1.it)('should use default AGENTS.md as source when fromFile is not provided', async () => {
31
+ const strategy = new other_js_1.OtherStrategy('COPILOT.md');
32
+ await strategy.sync('', tempDir);
33
+ const targetPath = path_1.default.join(tempDir, 'COPILOT.md');
34
+ const resolvedPath = await fs_extra_1.default.realpath(targetPath);
35
+ const expectedPath = await fs_extra_1.default.realpath(path_1.default.join(tempDir, index_js_1.AGENTS_FILE));
36
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
37
+ });
38
+ (0, vitest_1.it)('should use custom fromFile when provided', async () => {
39
+ const customSource = 'MY_AGENTS.md';
40
+ await fs_extra_1.default.writeFile(path_1.default.join(tempDir, customSource), '# Custom Agents');
41
+ const strategy = new other_js_1.OtherStrategy('CURSOR.md', customSource);
42
+ await strategy.sync('', tempDir);
43
+ const targetPath = path_1.default.join(tempDir, 'CURSOR.md');
44
+ const stats = await fs_extra_1.default.lstat(targetPath);
45
+ (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
46
+ const resolvedPath = await fs_extra_1.default.realpath(targetPath);
47
+ const expectedPath = await fs_extra_1.default.realpath(path_1.default.join(tempDir, customSource));
48
+ (0, vitest_1.expect)(resolvedPath).toBe(expectedPath);
49
+ });
50
+ (0, vitest_1.it)('should have name "other"', () => {
51
+ const strategy = new other_js_1.OtherStrategy('CURSOR.md');
52
+ (0, vitest_1.expect)(strategy.name).toBe('other');
53
+ });
54
+ (0, vitest_1.it)('should expose the targetFilename', () => {
55
+ const strategy = new other_js_1.OtherStrategy('CUSTOM_FILE.md');
56
+ (0, vitest_1.expect)(strategy.targetFilename).toBe('CUSTOM_FILE.md');
57
+ });
58
+ });
@@ -8,23 +8,31 @@ 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
+ fromFile;
12
+ constructor(fromFile = index_js_1.AGENTS_FILE) {
13
+ this.fromFile = fromFile;
14
+ }
15
+ async sync(_context, projectRoot, targetDir) {
16
+ const outputDir = targetDir ?? projectRoot;
17
+ const targetPath = path_1.default.join(outputDir, this.targetFilename);
18
+ // Compute the symlink value: relative path from outputDir to the source file in projectRoot
19
+ const agentsAbsPath = path_1.default.join(projectRoot, this.fromFile);
20
+ const symlinkTarget = path_1.default.relative(outputDir, agentsAbsPath);
13
21
  // Check if it exists and what type it is
14
22
  if (await fs_extra_1.default.pathExists(targetPath)) {
15
23
  const stats = await fs_extra_1.default.lstat(targetPath);
16
24
  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
25
+ const existingTarget = await fs_extra_1.default.readlink(targetPath);
26
+ if (existingTarget === symlinkTarget) {
27
+ // Already points to the correct location, nothing to do
20
28
  return;
21
29
  }
22
30
  }
23
31
  // If it's a file or a link to somewhere else, remove it
24
32
  await fs_extra_1.default.remove(targetPath);
25
33
  }
26
- // Create relative symlink
27
- await fs_extra_1.default.ensureSymlink(index_js_1.AGENTS_FILE, targetPath);
34
+ // Create symlink pointing to the source file
35
+ await fs_extra_1.default.ensureSymlink(symlinkTarget, targetPath);
28
36
  }
29
37
  }
30
38
  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.3.0",
4
4
  "description": "CLI utility to synchronize AI agent context files from AGENTS.md",
5
5
  "main": "dist/index.js",
6
6
  "bin": {