@dezkareid/ai-context-sync 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.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 to update provider-specific files:
58
- - **Claude**: Creates a symbolic link `CLAUDE.md` pointing to `AGENTS.md`.
59
- - **Gemini**: Updates `.gemini/settings.json` to include `AGENTS.md` in the `context.fileName` list.
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/engine.js CHANGED
@@ -10,35 +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
- class SyncEngine {
14
- allStrategies = [
15
- new claude_js_1.ClaudeStrategy(),
16
- new gemini_js_1.GeminiStrategy(),
17
- new gemini_md_js_1.GeminiMdStrategy()
13
+ const other_js_1 = require("./strategies/other.js");
14
+ function buildBuiltInStrategies(fromFile) {
15
+ return [
16
+ new claude_js_1.ClaudeStrategy(fromFile),
17
+ new gemini_js_1.GeminiStrategy(fromFile),
18
+ new gemini_md_js_1.GeminiMdStrategy(fromFile)
18
19
  ];
19
- async sync(projectRoot, selectedStrategies, targetDir) {
20
- const agentsPath = path_1.default.join(projectRoot, constants_js_1.AGENTS_FILENAME);
20
+ }
21
+ function resolveStrategiesToRun(selectedStrategies, builtInStrategies, otherFiles, fromFile) {
22
+ if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
23
+ return builtInStrategies;
24
+ }
25
+ const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
26
+ const normalizedList = selectedList.map(s => s.toLowerCase());
27
+ let strategies;
28
+ if (normalizedList.includes('all') || normalizedList.includes('both')) {
29
+ strategies = [...builtInStrategies];
30
+ }
31
+ else {
32
+ strategies = builtInStrategies.filter(s => normalizedList.includes(s.name.toLowerCase()));
33
+ }
34
+ if (normalizedList.includes('other')) {
35
+ if (!otherFiles || otherFiles.length === 0) {
36
+ throw new Error('Strategy "other" requires otherFiles to be specified.');
37
+ }
38
+ for (const filename of otherFiles) {
39
+ strategies.push(new other_js_1.OtherStrategy(filename, fromFile));
40
+ }
41
+ }
42
+ return strategies;
43
+ }
44
+ class SyncEngine {
45
+ async sync(projectRoot, selectedStrategies, targetDir, fromFile, otherFiles) {
46
+ const sourceFile = fromFile ?? constants_js_1.AGENTS_FILENAME;
47
+ const agentsPath = path_1.default.join(projectRoot, sourceFile);
21
48
  const outputDir = targetDir ?? projectRoot;
22
49
  if (!(await fs_extra_1.default.pathExists(agentsPath))) {
23
- throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
50
+ throw new Error(`${sourceFile} not found in ${projectRoot}`);
24
51
  }
25
52
  const context = await fs_extra_1.default.readFile(agentsPath, 'utf-8');
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(', ');
53
+ const builtInStrategies = buildBuiltInStrategies(fromFile);
54
+ const strategiesToRun = resolveStrategiesToRun(selectedStrategies, builtInStrategies, otherFiles, fromFile);
41
55
  if (strategiesToRun.length === 0 && selectedStrategies) {
56
+ const availableNames = [...builtInStrategies.map(s => s.name), 'other'].join(', ');
42
57
  throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
43
58
  }
44
59
  for (const strategy of strategiesToRun) {
package/dist/index.js CHANGED
@@ -4,81 +4,273 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.readConfig = readConfig;
8
+ exports.resolveProjectConfig = resolveProjectConfig;
9
+ exports.addProject = addProject;
10
+ exports.createProgram = createProgram;
7
11
  const commander_1 = require("commander");
8
12
  const engine_js_1 = require("./engine.js");
9
13
  const path_1 = __importDefault(require("path"));
10
14
  const inquirer_1 = __importDefault(require("inquirer"));
11
15
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
16
  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('--skip-config', 'Avoid reading/creating the config file', false)
25
- .action(async (options) => {
17
+ async function readConfig(configPath) {
26
18
  try {
27
- const projectRoot = path_1.default.resolve(options.dir);
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
19
+ return await fs_extra_1.default.readJson(configPath);
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ async function promptStrategies() {
26
+ const answers = await inquirer_1.default.prompt([
27
+ {
28
+ type: 'checkbox',
29
+ name: 'strategies',
30
+ message: 'Select the AI context files to sync:',
31
+ choices: [
32
+ { name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
33
+ { name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
34
+ { name: 'Gemini Markdown (GEMINI.md)', value: 'gemini-md', checked: true },
35
+ { name: 'Other (custom files)', value: 'other', checked: false }
36
+ ],
37
+ validate: (answer) => {
38
+ if (answer.length < 1) {
39
+ return 'You must choose at least one strategy.';
42
40
  }
41
+ return true;
43
42
  }
44
43
  }
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;
44
+ ]);
45
+ return answers.strategies;
46
+ }
47
+ async function promptOtherFiles() {
48
+ const filesAnswer = await inquirer_1.default.prompt([
49
+ {
50
+ type: 'input',
51
+ name: 'otherFiles',
52
+ message: 'Enter custom file name(s) to create as symlinks (comma-separated):',
53
+ validate: (v) => v.trim().length > 0 || 'At least one filename is required.'
54
+ }
55
+ ]);
56
+ return filesAnswer.otherFiles.split(',').map((s) => s.trim()).filter(Boolean);
57
+ }
58
+ async function resolveStrategy(strategyOption, otherFiles) {
59
+ if (!strategyOption) {
60
+ const selected = await promptStrategies();
61
+ let resolved = [...(otherFiles ?? [])];
62
+ if (selected.includes('other') && resolved.length === 0) {
63
+ resolved = await promptOtherFiles();
66
64
  }
67
- else if (typeof strategy === 'string') {
68
- strategy = strategy.split(',').map(s => s.trim());
65
+ return { strategy: selected, otherFiles: resolved.length > 0 ? resolved : otherFiles };
66
+ }
67
+ const strategy = typeof strategyOption === 'string'
68
+ ? strategyOption.split(',').map(s => s.trim())
69
+ : strategyOption;
70
+ return { strategy, otherFiles };
71
+ }
72
+ async function applyConfig(options, configPath) {
73
+ let strategy = options.strategy;
74
+ let otherFiles;
75
+ let fromFile;
76
+ let config = {};
77
+ if (!options.skipConfig && await fs_extra_1.default.pathExists(configPath)) {
78
+ config = await readConfig(configPath);
79
+ if (!strategy && config.strategies) {
80
+ strategy = config.strategies;
69
81
  }
70
- // 3. Save strategy to config if not skipping
82
+ otherFiles = config.otherFiles;
83
+ fromFile = config.from;
84
+ }
85
+ return { config, strategy, otherFiles, fromFile };
86
+ }
87
+ function resolveFromFile(optionFrom, configFromFile) {
88
+ if (!optionFrom)
89
+ return configFromFile;
90
+ if (path_1.default.isAbsolute(optionFrom)) {
91
+ throw new Error('--from must be a relative path, not an absolute path.');
92
+ }
93
+ return optionFrom;
94
+ }
95
+ function mergeOtherFiles(optionFiles, configOtherFiles) {
96
+ if (!optionFiles)
97
+ return configOtherFiles;
98
+ const flagFiles = optionFiles.split(',').map((s) => s.trim()).filter(Boolean);
99
+ return [...new Set([...(configOtherFiles ?? []), ...flagFiles])];
100
+ }
101
+ async function resolveProjectConfig(projectPath, rootConfig, overrides) {
102
+ const configPath = path_1.default.join(projectPath, constants_js_1.CONFIG_FILENAME);
103
+ const localConfig = await readConfig(configPath);
104
+ const strategy = overrides?.strategies ?? localConfig.strategies ?? rootConfig.strategies;
105
+ const otherFiles = overrides?.otherFiles ?? localConfig.otherFiles ?? rootConfig.otherFiles;
106
+ const fromFile = overrides?.from ?? localConfig.from ?? rootConfig.from;
107
+ const strategyArray = typeof strategy === 'string'
108
+ ? strategy.split(',').map(s => s.trim())
109
+ : (strategy ?? []);
110
+ return {
111
+ strategy: strategyArray,
112
+ otherFiles,
113
+ fromFile
114
+ };
115
+ }
116
+ async function syncProject(projectRoot, strategy, targetDir, fromFile, otherFiles, projectName) {
117
+ if (strategy.includes('other') && (!otherFiles || otherFiles.length === 0)) {
118
+ throw new Error('Strategy "other" requires custom files to be defined.');
119
+ }
120
+ const engine = new engine_js_1.SyncEngine();
121
+ await engine.sync(projectRoot, strategy, targetDir, fromFile, otherFiles);
122
+ console.log(`[${projectName}] Successfully synchronized using "${strategy.join(', ')}"!`);
123
+ }
124
+ async function runRootSync(options, config, projectRoot, configPath, configResult) {
125
+ try {
126
+ const fromFile = resolveFromFile(options.from, configResult.fromFile);
127
+ const otherFiles = mergeOtherFiles(options.files, configResult.otherFiles);
128
+ const resolved = await resolveStrategy(configResult.strategy, otherFiles);
129
+ await syncProject(projectRoot, resolved.strategy, options.targetDir ? path_1.default.resolve(options.targetDir) : projectRoot, fromFile, resolved.otherFiles, 'root');
71
130
  if (!options.skipConfig) {
72
- await fs_extra_1.default.writeJson(configPath, { strategies: strategy }, { spaces: 2 });
131
+ const configData = { ...config, strategies: resolved.strategy };
132
+ if (resolved.otherFiles?.length)
133
+ configData.otherFiles = resolved.otherFiles;
134
+ if (fromFile)
135
+ configData.from = fromFile;
136
+ await fs_extra_1.default.writeJson(configPath, configData, { spaces: 2 });
73
137
  }
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}"!`);
138
+ return { name: 'root', success: true };
78
139
  }
79
140
  catch (error) {
80
- console.error(`Error: ${error.message}`);
81
- process.exit(1);
141
+ return { name: 'root', success: false, error: error instanceof Error ? error.message : String(error) };
142
+ }
143
+ }
144
+ async function runProjectsSync(config, projectRoot) {
145
+ const results = [];
146
+ if (!config.projects)
147
+ return results;
148
+ for (const [projectPath, projectOverrides] of Object.entries(config.projects)) {
149
+ try {
150
+ const absoluteProjectPath = path_1.default.resolve(projectRoot, projectPath);
151
+ if (!(await fs_extra_1.default.pathExists(absoluteProjectPath))) {
152
+ throw new Error(`Project path does not exist: ${projectPath}`);
153
+ }
154
+ const resolved = await resolveProjectConfig(absoluteProjectPath, config, projectOverrides);
155
+ await syncProject(absoluteProjectPath, resolved.strategy, absoluteProjectPath, resolved.fromFile, resolved.otherFiles, projectPath);
156
+ results.push({ name: projectPath, success: true });
157
+ }
158
+ catch (error) {
159
+ results.push({ name: projectPath, success: false, error: error instanceof Error ? error.message : String(error) });
160
+ }
161
+ }
162
+ return results;
163
+ }
164
+ function displaySummary(results) {
165
+ const failures = results.filter(r => !r.success);
166
+ if (failures.length > 0) {
167
+ console.warn('\n--- Synchronization Summary ---');
168
+ console.warn(`Success: ${results.length - failures.length}/${results.length}`);
169
+ console.warn('Failures:');
170
+ failures.forEach(f => console.warn(` - [${f.name}]: ${f.error}`));
171
+ if (failures.length === results.length) {
172
+ throw new Error('All synchronizations failed.');
173
+ }
174
+ }
175
+ else if (results.length > 0) {
176
+ console.log('\nAll projects synchronized successfully!');
177
+ }
178
+ }
179
+ async function runSync(options) {
180
+ const projectRoot = path_1.default.resolve(options.dir);
181
+ const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
182
+ const configResult = await applyConfig(options, configPath);
183
+ const results = [];
184
+ const rootResult = await runRootSync(options, configResult.config, projectRoot, configPath, configResult);
185
+ results.push(rootResult);
186
+ const projectResults = await runProjectsSync(configResult.config, projectRoot);
187
+ results.push(...projectResults);
188
+ displaySummary(results);
189
+ }
190
+ async function addProject(projectPath, options) {
191
+ const projectRoot = path_1.default.resolve(options.dir);
192
+ const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
193
+ const config = await readConfig(configPath);
194
+ const relativePath = path_1.default.isAbsolute(projectPath)
195
+ ? path_1.default.relative(projectRoot, projectPath)
196
+ : projectPath;
197
+ const absoluteProjectPath = path_1.default.resolve(projectRoot, relativePath);
198
+ if (!(await fs_extra_1.default.pathExists(absoluteProjectPath))) {
199
+ console.warn(`Warning: Project path does not exist: ${absoluteProjectPath}`);
82
200
  }
83
- });
84
- program.parse();
201
+ const otherFiles = options.files ? options.files.split(',').map(s => s.trim()) : undefined;
202
+ const strategies = options.strategy ? options.strategy.split(',').map(s => s.trim()) : undefined;
203
+ let resolvedStrategies = strategies;
204
+ let resolvedOtherFiles = otherFiles;
205
+ if (!strategies) {
206
+ const resolved = await resolveStrategy(undefined, undefined);
207
+ resolvedStrategies = resolved.strategy;
208
+ resolvedOtherFiles = resolved.otherFiles;
209
+ }
210
+ const projectConfig = {};
211
+ if (resolvedStrategies)
212
+ projectConfig.strategies = resolvedStrategies;
213
+ if (resolvedOtherFiles)
214
+ projectConfig.otherFiles = resolvedOtherFiles;
215
+ if (options.from)
216
+ projectConfig.from = options.from;
217
+ const newConfig = {
218
+ ...config,
219
+ projects: {
220
+ ...(config.projects || {}),
221
+ [relativePath]: projectConfig
222
+ }
223
+ };
224
+ await fs_extra_1.default.writeJson(configPath, newConfig, { spaces: 2 });
225
+ console.log(`Successfully added project "${relativePath}" to configuration.`);
226
+ }
227
+ function createProgram() {
228
+ const program = new commander_1.Command();
229
+ program
230
+ .name('ai-context-sync')
231
+ .description('Sync AI context files across different providers')
232
+ .version('1.0.0');
233
+ const projectCommand = program.command('project').description('Manage configured projects');
234
+ projectCommand
235
+ .command('add <path>')
236
+ .description('Add a new project to the configuration')
237
+ .option('-d, --dir <path>', 'Root project directory', process.cwd())
238
+ .option('-s, --strategy <strategy>', 'Sync strategy for this project')
239
+ .option('-f, --files <names>', 'Custom filenames for "other" strategy')
240
+ .option('--from <path>', 'Source file path for symlinks')
241
+ .action(async (projectPath, options) => {
242
+ try {
243
+ await addProject(projectPath, options);
244
+ }
245
+ catch (error) {
246
+ const message = error instanceof Error ? error.message : String(error);
247
+ console.error(`Error: ${message}`);
248
+ process.exit(1);
249
+ }
250
+ });
251
+ program
252
+ .command('sync')
253
+ .description('Synchronize context files from AGENTS.md')
254
+ .option('-d, --dir <path>', 'Project directory (where AGENTS.md lives)', process.cwd())
255
+ .option('-t, --target-dir <path>', 'Target directory where synced files will be written (defaults to --dir)')
256
+ .option('-s, --strategy <strategy>', 'Sync strategy (claude, gemini, all, or comma-separated list)')
257
+ .option('-f, --files <names>', 'Comma-separated custom filenames for "other" strategy')
258
+ .option('--from <path>', 'Source file path for symlinks (default: AGENTS.md)')
259
+ .option('--skip-config', 'Avoid reading/creating the config file', false)
260
+ .action(async (options) => {
261
+ try {
262
+ await runSync(options);
263
+ }
264
+ catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ console.error(`Error: ${message}`);
267
+ process.exit(1);
268
+ }
269
+ });
270
+ return program;
271
+ }
272
+ if (process.argv[1] && (process.argv[1].endsWith('dist/index.js') ||
273
+ process.argv[1].endsWith('bin/ai-context-sync') ||
274
+ (fs_extra_1.default.existsSync(path_1.default.resolve('dist/index.js')) && fs_extra_1.default.realpathSync(process.argv[1]) === fs_extra_1.default.realpathSync(path_1.default.resolve('dist/index.js'))))) {
275
+ createProgram().parse();
276
+ }
@@ -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,12 +9,16 @@ 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
+ fromFile;
13
+ constructor(fromFile = index_js_1.AGENTS_FILE) {
14
+ this.fromFile = fromFile;
15
+ }
12
16
  async sync(_context, projectRoot, targetDir) {
13
17
  const outputDir = targetDir ?? projectRoot;
14
18
  const geminiDir = path_1.default.join(outputDir, '.gemini');
15
19
  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);
20
+ // Compute the path to the source file relative to the target .gemini directory
21
+ const agentsAbsPath = path_1.default.join(projectRoot, this.fromFile);
18
22
  const agentsRelativePath = path_1.default.relative(outputDir, agentsAbsPath);
19
23
  await fs_extra_1.default.ensureDir(geminiDir);
20
24
  let settings = {};
@@ -24,7 +28,7 @@ class GeminiStrategy {
24
28
  try {
25
29
  settings = await fs_extra_1.default.readJson(settingsPath);
26
30
  }
27
- catch (e) {
31
+ catch {
28
32
  settings = {};
29
33
  }
30
34
  }
@@ -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;
@@ -8,11 +8,15 @@ 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
+ fromFile;
12
+ constructor(fromFile = index_js_1.AGENTS_FILE) {
13
+ this.fromFile = fromFile;
14
+ }
11
15
  async sync(_context, projectRoot, targetDir) {
12
16
  const outputDir = targetDir ?? projectRoot;
13
17
  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);
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);
16
20
  const symlinkTarget = path_1.default.relative(outputDir, agentsAbsPath);
17
21
  // Check if it exists and what type it is
18
22
  if (await fs_extra_1.default.pathExists(targetPath)) {
@@ -27,7 +31,7 @@ class SymlinkStrategy {
27
31
  // If it's a file or a link to somewhere else, remove it
28
32
  await fs_extra_1.default.remove(targetPath);
29
33
  }
30
- // Create symlink pointing to AGENTS.md
34
+ // Create symlink pointing to the source file
31
35
  await fs_extra_1.default.ensureSymlink(symlinkTarget, targetPath);
32
36
  }
33
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dezkareid/ai-context-sync",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI utility to synchronize AI agent context files from AGENTS.md",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -40,17 +40,22 @@
40
40
  "inquirer": "9.2.12"
41
41
  },
42
42
  "devDependencies": {
43
+ "@eslint/js": "9.39.2",
43
44
  "@types/fs-extra": "11.0.4",
44
45
  "@types/inquirer": "9.0.7",
45
46
  "@types/node": "25.0.10",
46
47
  "@vitest/coverage-v8": "4.0.18",
48
+ "eslint": "9.39.2",
47
49
  "typescript": "5.9.3",
48
- "vitest": "4.0.18"
50
+ "typescript-eslint": "8.57.0",
51
+ "vitest": "4.0.18",
52
+ "@dezkareid/eslint-config-ts-base": "^0.0.0"
49
53
  },
50
54
  "scripts": {
51
55
  "build": "tsc",
52
56
  "start": "node dist/index.js",
53
57
  "dev": "tsc -w",
58
+ "lint": "eslint .",
54
59
  "test": "vitest run --coverage"
55
60
  }
56
61
  }
@@ -1,150 +0,0 @@
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 engine_js_1 = require("./engine.js");
8
- const constants_js_1 = require("./constants.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)('SyncEngine', () => {
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(), 'sync-engine-test-'));
16
- });
17
- (0, vitest_1.afterEach)(async () => {
18
- await fs_extra_1.default.remove(tempDir);
19
- });
20
- (0, vitest_1.it)('should throw error if AGENTS.md is missing', async () => {
21
- const engine = new engine_js_1.SyncEngine();
22
- await (0, vitest_1.expect)(engine.sync(tempDir)).rejects.toThrow(`${constants_js_1.AGENTS_FILENAME} not found`);
23
- });
24
- (0, vitest_1.it)('should run all strategies when AGENTS.md exists', async () => {
25
- const engine = new engine_js_1.SyncEngine();
26
- const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
27
- const context = '# Agent Context';
28
- await fs_extra_1.default.writeFile(agentsPath, context);
29
- await engine.sync(tempDir);
30
- // Verify Claude
31
- const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
32
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(claudePath)).toBe(true);
33
- (0, vitest_1.expect)(await fs_extra_1.default.readFile(claudePath, 'utf-8')).toBe(context);
34
- // Verify Gemini
35
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
36
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
37
- const settings = await fs_extra_1.default.readJson(settingsPath);
38
- (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
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
- });
150
- });
@@ -1,31 +0,0 @@
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 claude_js_1 = require("./claude.js");
8
- const constants_js_1 = require("../constants.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)('ClaudeStrategy', () => {
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(), 'claude-strategy-test-'));
16
- });
17
- (0, vitest_1.afterEach)(async () => {
18
- await fs_extra_1.default.remove(tempDir);
19
- });
20
- (0, vitest_1.it)('should create a symbolic link CLAUDE.md pointing to AGENTS.md', async () => {
21
- const strategy = new claude_js_1.ClaudeStrategy();
22
- const agentsPath = path_1.default.join(tempDir, constants_js_1.AGENTS_FILENAME);
23
- await fs_extra_1.default.writeFile(agentsPath, '# Agents');
24
- await strategy.sync('# Agents', tempDir);
25
- const claudePath = path_1.default.join(tempDir, 'CLAUDE.md');
26
- const stats = await fs_extra_1.default.lstat(claudePath);
27
- (0, vitest_1.expect)(stats.isSymbolicLink()).toBe(true);
28
- const target = await fs_extra_1.default.readlink(claudePath);
29
- (0, vitest_1.expect)(target).toBe(constants_js_1.AGENTS_FILENAME);
30
- });
31
- });
@@ -1,31 +0,0 @@
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
- });
@@ -1,110 +0,0 @@
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_js_1 = require("./gemini.js");
8
- const constants_js_1 = require("../constants.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)('GeminiStrategy', () => {
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(), 'gemini-strategy-test-'));
16
- });
17
- (0, vitest_1.afterEach)(async () => {
18
- await fs_extra_1.default.remove(tempDir);
19
- });
20
- (0, vitest_1.it)('should create .gemini/settings.json if it does not exist', async () => {
21
- const strategy = new gemini_js_1.GeminiStrategy();
22
- await strategy.sync('context', tempDir);
23
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
24
- (0, vitest_1.expect)(await fs_extra_1.default.pathExists(settingsPath)).toBe(true);
25
- const settings = await fs_extra_1.default.readJson(settingsPath);
26
- (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
27
- });
28
- (0, vitest_1.it)('should update existing context.fileName array', async () => {
29
- const strategy = new gemini_js_1.GeminiStrategy();
30
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
31
- await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
32
- await fs_extra_1.default.writeJson(settingsPath, {
33
- context: {
34
- fileName: ['OTHER.md']
35
- }
36
- });
37
- await strategy.sync('context', tempDir);
38
- const settings = await fs_extra_1.default.readJson(settingsPath);
39
- (0, vitest_1.expect)(settings.context.fileName).toContain('OTHER.md');
40
- (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
41
- });
42
- (0, vitest_1.it)('should convert string fileName to array and add AGENTS.md', async () => {
43
- const strategy = new gemini_js_1.GeminiStrategy();
44
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
45
- await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
46
- await fs_extra_1.default.writeJson(settingsPath, {
47
- context: {
48
- fileName: 'OTHER.md'
49
- }
50
- });
51
- await strategy.sync('context', tempDir);
52
- const settings = await fs_extra_1.default.readJson(settingsPath);
53
- (0, vitest_1.expect)(Array.isArray(settings.context.fileName)).toBe(true);
54
- (0, vitest_1.expect)(settings.context.fileName).toContain('OTHER.md');
55
- (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
56
- });
57
- (0, vitest_1.it)('should not add AGENTS.md twice', async () => {
58
- const strategy = new gemini_js_1.GeminiStrategy();
59
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
60
- await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
61
- await fs_extra_1.default.writeJson(settingsPath, {
62
- context: {
63
- fileName: [constants_js_1.AGENTS_FILENAME]
64
- }
65
- });
66
- await strategy.sync('context', tempDir);
67
- const settings = await fs_extra_1.default.readJson(settingsPath);
68
- (0, vitest_1.expect)(settings.context.fileName.filter((f) => f === constants_js_1.AGENTS_FILENAME).length).toBe(1);
69
- });
70
- (0, vitest_1.it)('should not write to settings.json if AGENTS.md is already present', async () => {
71
- const strategy = new gemini_js_1.GeminiStrategy();
72
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
73
- await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
74
- await fs_extra_1.default.writeJson(settingsPath, {
75
- context: {
76
- fileName: [constants_js_1.AGENTS_FILENAME]
77
- }
78
- });
79
- const mtimeBefore = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
80
- // Wait a bit to ensure mtime would change if written
81
- await new Promise(resolve => setTimeout(resolve, 10));
82
- await strategy.sync('context', tempDir);
83
- const mtimeAfter = (await fs_extra_1.default.stat(settingsPath)).mtimeMs;
84
- (0, vitest_1.expect)(mtimeAfter).toBe(mtimeBefore);
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
- });
101
- (0, vitest_1.it)('should handle invalid JSON in settings.json by starting fresh', async () => {
102
- const strategy = new gemini_js_1.GeminiStrategy();
103
- const settingsPath = path_1.default.join(tempDir, '.gemini', 'settings.json');
104
- await fs_extra_1.default.ensureDir(path_1.default.dirname(settingsPath));
105
- await fs_extra_1.default.writeFile(settingsPath, 'not a json');
106
- await strategy.sync('context', tempDir);
107
- const settings = await fs_extra_1.default.readJson(settingsPath);
108
- (0, vitest_1.expect)(settings.context.fileName).toContain(constants_js_1.AGENTS_FILENAME);
109
- });
110
- });
@@ -1,101 +0,0 @@
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
- });