@dezkareid/ai-context-sync 1.2.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,34 +10,44 @@ 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, targetDir) {
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);
21
18
  const outputDir = targetDir ?? projectRoot;
22
19
  if (!(await fs_extra_1.default.pathExists(agentsPath))) {
23
- throw new Error(`${constants_js_1.AGENTS_FILENAME} not found in ${projectRoot}`);
20
+ throw new Error(`${sourceFile} not found in ${projectRoot}`);
24
21
  }
25
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
+ ];
26
28
  let strategiesToRun;
27
29
  if (!selectedStrategies || (Array.isArray(selectedStrategies) && selectedStrategies.length === 0)) {
28
- strategiesToRun = this.allStrategies;
30
+ strategiesToRun = builtInStrategies;
29
31
  }
30
32
  else {
31
33
  const selectedList = Array.isArray(selectedStrategies) ? selectedStrategies : [selectedStrategies];
32
34
  const normalizedList = selectedList.map(s => s.toLowerCase());
33
35
  if (normalizedList.includes('all') || normalizedList.includes('both')) {
34
- strategiesToRun = this.allStrategies;
36
+ strategiesToRun = builtInStrategies;
35
37
  }
36
38
  else {
37
- 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
+ }
38
48
  }
39
49
  }
40
- const availableNames = this.allStrategies.map(s => s.name).join(', ');
50
+ const availableNames = [...builtInStrategies.map(s => s.name), 'other'].join(', ');
41
51
  if (strategiesToRun.length === 0 && selectedStrategies) {
42
52
  throw new Error(`No valid strategies found for: ${selectedStrategies}. Available strategies: ${availableNames}`);
43
53
  }
@@ -87,7 +87,82 @@ 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
+ });
91
166
  });
92
167
  (0, vitest_1.describe)('targetDir option', () => {
93
168
  let targetDir;
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ program
21
21
  .option('-d, --dir <path>', 'Project directory (where AGENTS.md lives)', process.cwd())
22
22
  .option('-t, --target-dir <path>', 'Target directory where synced files will be written (defaults to --dir)')
23
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)')
24
26
  .option('--skip-config', 'Avoid reading/creating the config file', false)
25
27
  .action(async (options) => {
26
28
  try {
@@ -28,21 +30,41 @@ program
28
30
  const targetDir = options.targetDir ? path_1.default.resolve(options.targetDir) : projectRoot;
29
31
  const configPath = path_1.default.join(projectRoot, constants_js_1.CONFIG_FILENAME);
30
32
  let strategy = options.strategy;
33
+ let otherFiles;
34
+ let fromFile;
31
35
  // 1. If no strategy provided, try to read from config
32
- if (!strategy && !options.skipConfig) {
36
+ if (!options.skipConfig) {
33
37
  if (await fs_extra_1.default.pathExists(configPath)) {
34
38
  try {
35
39
  const config = await fs_extra_1.default.readJson(configPath);
36
- if (config.strategies) {
40
+ if (!strategy && config.strategies) {
37
41
  strategy = config.strategies;
38
42
  }
43
+ if (config.otherFiles) {
44
+ otherFiles = config.otherFiles;
45
+ }
46
+ if (config.from) {
47
+ fromFile = config.from;
48
+ }
39
49
  }
40
50
  catch (e) {
41
51
  // Ignore corrupted config and proceed to prompt
42
52
  }
43
53
  }
44
54
  }
45
- // 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
46
68
  if (!strategy) {
47
69
  const answers = await inquirer_1.default.prompt([
48
70
  {
@@ -52,7 +74,8 @@ program
52
74
  choices: [
53
75
  { name: 'Claude (CLAUDE.md)', value: 'claude', checked: true },
54
76
  { name: 'Gemini (.gemini/settings.json)', value: 'gemini', checked: true },
55
- { 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 }
56
79
  ],
57
80
  validate: (answer) => {
58
81
  if (answer.length < 1) {
@@ -63,16 +86,37 @@ program
63
86
  }
64
87
  ]);
65
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
+ }
66
101
  }
67
102
  else if (typeof strategy === 'string') {
68
103
  strategy = strategy.split(',').map(s => s.trim());
69
104
  }
70
- // 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
71
110
  if (!options.skipConfig) {
72
- 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 });
73
117
  }
74
118
  const engine = new engine_js_1.SyncEngine();
75
- await engine.sync(projectRoot, strategy, targetDir);
119
+ await engine.sync(projectRoot, strategy, targetDir, fromFile, otherFiles);
76
120
  const strategyMsg = Array.isArray(strategy) ? strategy.join(', ') : strategy;
77
121
  console.log(`Successfully synchronized context files using "${strategyMsg}"!`);
78
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,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 = {};
@@ -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,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.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": {