@tengx5383/aitool-sync-cli 0.1.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.
Files changed (153) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/README.md +203 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +87 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/add.d.ts +2 -0
  8. package/dist/commands/add.d.ts.map +1 -0
  9. package/dist/commands/add.js +24 -0
  10. package/dist/commands/add.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +2 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +89 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/init.d.ts +7 -0
  16. package/dist/commands/init.d.ts.map +1 -0
  17. package/dist/commands/init.js +105 -0
  18. package/dist/commands/init.js.map +1 -0
  19. package/dist/commands/list.d.ts +2 -0
  20. package/dist/commands/list.d.ts.map +1 -0
  21. package/dist/commands/list.js +22 -0
  22. package/dist/commands/list.js.map +1 -0
  23. package/dist/commands/pull.d.ts +6 -0
  24. package/dist/commands/pull.d.ts.map +1 -0
  25. package/dist/commands/pull.js +52 -0
  26. package/dist/commands/pull.js.map +1 -0
  27. package/dist/commands/push.d.ts +6 -0
  28. package/dist/commands/push.d.ts.map +1 -0
  29. package/dist/commands/push.js +60 -0
  30. package/dist/commands/push.js.map +1 -0
  31. package/dist/commands/remove.d.ts +2 -0
  32. package/dist/commands/remove.d.ts.map +1 -0
  33. package/dist/commands/remove.js +20 -0
  34. package/dist/commands/remove.js.map +1 -0
  35. package/dist/commands/status.d.ts +2 -0
  36. package/dist/commands/status.d.ts.map +1 -0
  37. package/dist/commands/status.js +46 -0
  38. package/dist/commands/status.js.map +1 -0
  39. package/dist/config/config-file.d.ts +22 -0
  40. package/dist/config/config-file.d.ts.map +1 -0
  41. package/dist/config/config-file.js +86 -0
  42. package/dist/config/config-file.js.map +1 -0
  43. package/dist/config/schema.d.ts +101 -0
  44. package/dist/config/schema.d.ts.map +1 -0
  45. package/dist/config/schema.js +24 -0
  46. package/dist/config/schema.js.map +1 -0
  47. package/dist/core/backup.d.ts +8 -0
  48. package/dist/core/backup.d.ts.map +1 -0
  49. package/dist/core/backup.js +50 -0
  50. package/dist/core/backup.js.map +1 -0
  51. package/dist/core/checksum.d.ts +14 -0
  52. package/dist/core/checksum.d.ts.map +1 -0
  53. package/dist/core/checksum.js +23 -0
  54. package/dist/core/checksum.js.map +1 -0
  55. package/dist/core/exclusions.d.ts +8 -0
  56. package/dist/core/exclusions.d.ts.map +1 -0
  57. package/dist/core/exclusions.js +29 -0
  58. package/dist/core/exclusions.js.map +1 -0
  59. package/dist/core/file-collector.d.ts +16 -0
  60. package/dist/core/file-collector.d.ts.map +1 -0
  61. package/dist/core/file-collector.js +104 -0
  62. package/dist/core/file-collector.js.map +1 -0
  63. package/dist/core/manifest.d.ts +18 -0
  64. package/dist/core/manifest.d.ts.map +1 -0
  65. package/dist/core/manifest.js +42 -0
  66. package/dist/core/manifest.js.map +1 -0
  67. package/dist/core/path-expander.d.ts +6 -0
  68. package/dist/core/path-expander.d.ts.map +1 -0
  69. package/dist/core/path-expander.js +25 -0
  70. package/dist/core/path-expander.js.map +1 -0
  71. package/dist/core/sync-engine.d.ts +24 -0
  72. package/dist/core/sync-engine.d.ts.map +1 -0
  73. package/dist/core/sync-engine.js +206 -0
  74. package/dist/core/sync-engine.js.map +1 -0
  75. package/dist/core/sync-package.d.ts +12 -0
  76. package/dist/core/sync-package.d.ts.map +1 -0
  77. package/dist/core/sync-package.js +95 -0
  78. package/dist/core/sync-package.js.map +1 -0
  79. package/dist/crypto/encryptor.d.ts +15 -0
  80. package/dist/crypto/encryptor.d.ts.map +1 -0
  81. package/dist/crypto/encryptor.js +77 -0
  82. package/dist/crypto/encryptor.js.map +1 -0
  83. package/dist/crypto/key-resolver.d.ts +28 -0
  84. package/dist/crypto/key-resolver.d.ts.map +1 -0
  85. package/dist/crypto/key-resolver.js +62 -0
  86. package/dist/crypto/key-resolver.js.map +1 -0
  87. package/dist/gist/client.d.ts +18 -0
  88. package/dist/gist/client.d.ts.map +1 -0
  89. package/dist/gist/client.js +58 -0
  90. package/dist/gist/client.js.map +1 -0
  91. package/dist/gist/mock-client.d.ts +19 -0
  92. package/dist/gist/mock-client.d.ts.map +1 -0
  93. package/dist/gist/mock-client.js +61 -0
  94. package/dist/gist/mock-client.js.map +1 -0
  95. package/dist/index.d.ts +3 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +3 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/presets/claude-code.d.ts +3 -0
  100. package/dist/presets/claude-code.d.ts.map +1 -0
  101. package/dist/presets/claude-code.js +27 -0
  102. package/dist/presets/claude-code.js.map +1 -0
  103. package/dist/presets/hermes.d.ts +3 -0
  104. package/dist/presets/hermes.d.ts.map +1 -0
  105. package/dist/presets/hermes.js +27 -0
  106. package/dist/presets/hermes.js.map +1 -0
  107. package/dist/presets/index.d.ts +15 -0
  108. package/dist/presets/index.d.ts.map +1 -0
  109. package/dist/presets/index.js +42 -0
  110. package/dist/presets/index.js.map +1 -0
  111. package/dist/presets/openclaw.d.ts +3 -0
  112. package/dist/presets/openclaw.d.ts.map +1 -0
  113. package/dist/presets/openclaw.js +27 -0
  114. package/dist/presets/openclaw.js.map +1 -0
  115. package/dist/types/config.d.ts +45 -0
  116. package/dist/types/config.d.ts.map +1 -0
  117. package/dist/types/config.js +2 -0
  118. package/dist/types/config.js.map +1 -0
  119. package/dist/types/gist.d.ts +25 -0
  120. package/dist/types/gist.d.ts.map +1 -0
  121. package/dist/types/gist.js +2 -0
  122. package/dist/types/gist.js.map +1 -0
  123. package/dist/types/index.d.ts +4 -0
  124. package/dist/types/index.d.ts.map +1 -0
  125. package/dist/types/index.js +2 -0
  126. package/dist/types/index.js.map +1 -0
  127. package/dist/types/sync.d.ts +78 -0
  128. package/dist/types/sync.d.ts.map +1 -0
  129. package/dist/types/sync.js +2 -0
  130. package/dist/types/sync.js.map +1 -0
  131. package/dist/utils/diff.d.ts +7 -0
  132. package/dist/utils/diff.d.ts.map +1 -0
  133. package/dist/utils/diff.js +99 -0
  134. package/dist/utils/diff.js.map +1 -0
  135. package/dist/utils/env.d.ts +13 -0
  136. package/dist/utils/env.d.ts.map +1 -0
  137. package/dist/utils/env.js +29 -0
  138. package/dist/utils/env.js.map +1 -0
  139. package/dist/utils/logger.d.ts +14 -0
  140. package/dist/utils/logger.d.ts.map +1 -0
  141. package/dist/utils/logger.js +39 -0
  142. package/dist/utils/logger.js.map +1 -0
  143. package/dist/utils/table.d.ts +5 -0
  144. package/dist/utils/table.d.ts.map +1 -0
  145. package/dist/utils/table.js +59 -0
  146. package/dist/utils/table.js.map +1 -0
  147. package/package.json +41 -0
  148. package/tests/integration/encryption-flow.test.ts +142 -0
  149. package/tests/integration/init-flow.test.ts +69 -0
  150. package/tests/integration/push-pull-flow.test.ts +183 -0
  151. package/tests/integration/status-flow.test.ts +149 -0
  152. package/tsconfig.json +19 -0
  153. package/vitest.config.ts +10 -0
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createDefaultConfig, readConfig, writeConfig } from '../../src/config/config-file.js';
6
+ import { getPreset, getAllPresets } from '../../src/presets/index.js';
7
+
8
+ describe('init flow', () => {
9
+ let tmpDir: string;
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aitoolsync-test-'));
13
+ process.chdir(tmpDir);
14
+ });
15
+
16
+ afterEach(() => {
17
+ fs.rmSync(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it('creates a default config file', () => {
21
+ const config = createDefaultConfig();
22
+ writeConfig(config, tmpDir);
23
+
24
+ const configPath = path.join(tmpDir, '.aitoolsync.json');
25
+ expect(fs.existsSync(configPath)).toBe(true);
26
+
27
+ const loaded = readConfig(configPath);
28
+ expect(loaded).not.toBeNull();
29
+ expect(loaded!.version).toBe(1);
30
+ expect(loaded!.gistId).toBeNull();
31
+ expect(loaded!.syncPaths).toEqual([]);
32
+ });
33
+
34
+ it('reads and validates config file', () => {
35
+ const config = createDefaultConfig();
36
+ config.gistId = 'test-gist-123';
37
+ writeConfig(config, tmpDir);
38
+
39
+ const loaded = readConfig();
40
+ expect(loaded).not.toBeNull();
41
+ expect(loaded!.gistId).toBe('test-gist-123');
42
+ });
43
+
44
+ it('returns null for non-existent config', () => {
45
+ const loaded = readConfig();
46
+ expect(loaded).toBeNull();
47
+ });
48
+
49
+ it('has all presets defined', () => {
50
+ const presets = getAllPresets();
51
+ expect(presets.length).toBe(3);
52
+ expect(presets.map((p) => p.id)).toContain('claude-code');
53
+ expect(presets.map((p) => p.id)).toContain('hermes');
54
+ expect(presets.map((p) => p.id)).toContain('openclaw');
55
+ });
56
+
57
+ it('can get preset by ID', () => {
58
+ const preset = getPreset('claude-code');
59
+ expect(preset).not.toBeUndefined();
60
+ expect(preset!.name).toBe('Claude Code');
61
+ expect(preset!.defaultPaths.length).toBeGreaterThan(0);
62
+ });
63
+
64
+ it('includes default exclusions in new config', () => {
65
+ const config = createDefaultConfig();
66
+ expect(config.exclusions.some((e) => e.pattern === '**/node_modules/**')).toBe(true);
67
+ expect(config.exclusions.some((e) => e.pattern === '**/.git/**')).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createDefaultConfig, writeConfig, readConfig } from '../../src/config/config-file.js';
6
+ import { pushSync, pullSync } from '../../src/core/sync-engine.js';
7
+ import { buildSyncPackage, parseSyncPackage } from '../../src/core/sync-package.js';
8
+ import { createManifest, serializeManifest, parseManifest } from '../../src/core/manifest.js';
9
+ import { MockGistClient } from '../../src/gist/mock-client.js';
10
+ import type { AppConfig, Manifest, ManifestEntry } from '../../src/types/index.js';
11
+
12
+ function createTestDir(base: string): string {
13
+ const dir = path.join(base, 'my-tools');
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ version: 1 }));
16
+ fs.writeFileSync(path.join(dir, 'secrets.env'), 'SECRET=abc');
17
+ return dir;
18
+ }
19
+
20
+ describe('push-pull flow', () => {
21
+ let tmpDir: string;
22
+ let mockClient: MockGistClient;
23
+
24
+ beforeEach(() => {
25
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aitoolsync-pushpull-'));
26
+ process.chdir(tmpDir);
27
+ mockClient = new MockGistClient();
28
+ });
29
+
30
+ afterEach(() => {
31
+ fs.rmSync(tmpDir, { recursive: true, force: true });
32
+ });
33
+
34
+ it('pushSync creates a Gist and uploads package', async () => {
35
+ const testDir = createTestDir(tmpDir);
36
+ const config = createDefaultConfig();
37
+ config.syncPaths = [{ name: 'my-tools', path: testDir, enabled: true }];
38
+
39
+ // Create Gist first
40
+ const gist = await mockClient.createGist('aitoolsync', {
41
+ 'aitoolsync-package.json': { content: '{}' },
42
+ });
43
+ config.gistId = gist.gistId;
44
+ writeConfig(config);
45
+
46
+ const result = await pushSync(config, mockClient, false);
47
+ expect(result.success).toBe(true);
48
+ expect(result.filesUploaded).toBeGreaterThan(0);
49
+
50
+ // Verify Gist contains the package
51
+ const fetched = await mockClient.getGist(gist.gistId);
52
+ const pkgFile = fetched.files.find((f) => f.filename === 'aitoolsync-package.json');
53
+ expect(pkgFile).toBeDefined();
54
+
55
+ const pkg = parseSyncPackage(pkgFile!.content);
56
+ expect(pkg).not.toBeNull();
57
+ expect(pkg!.files.length).toBeGreaterThan(0);
58
+ expect(pkg!.manifest.entries.length).toBeGreaterThan(0);
59
+ });
60
+
61
+ it('pushSync detects conflicts and fails without force', async () => {
62
+ const testDir = createTestDir(tmpDir);
63
+ const config = createDefaultConfig();
64
+ config.syncPaths = [{ name: 'my-tools', path: testDir, enabled: true }];
65
+
66
+ const gist = await mockClient.createGist('aitoolsync', {
67
+ 'aitoolsync-package.json': { content: '{}' },
68
+ });
69
+ config.gistId = gist.gistId;
70
+
71
+ // Set lastKnownManifest to a different version (third checksum)
72
+ const baseManifest = createManifest([
73
+ { relativePath: 'config.json', checksum: 'base-hash', size: 100, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
74
+ ]);
75
+ config.lastKnownManifest = serializeManifest(baseManifest);
76
+ writeConfig(config);
77
+
78
+ // Create a remote package with different content
79
+ const remoteManifest = createManifest([
80
+ { relativePath: 'config.json', checksum: 'different-hash', size: 100, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
81
+ ]);
82
+ const remotePkg = await buildSyncPackage(remoteManifest, config);
83
+ await mockClient.updateGist(gist.gistId, {
84
+ 'aitoolsync-package.json': { content: JSON.stringify(remotePkg) },
85
+ });
86
+
87
+ // Modify local file to be different from both
88
+ fs.writeFileSync(path.join(testDir, 'config.json'), JSON.stringify({ version: 2, changed: true }));
89
+
90
+ const result = await pushSync(config, mockClient, false);
91
+ expect(result.success).toBe(false);
92
+ expect(result.errors.some((e) => e.includes('Conflicts'))).toBe(true);
93
+ });
94
+
95
+ it('pushSync --force overrides conflicts', async () => {
96
+ const testDir = createTestDir(tmpDir);
97
+ const config = createDefaultConfig();
98
+ config.syncPaths = [{ name: 'my-tools', path: testDir, enabled: true }];
99
+
100
+ const gist = await mockClient.createGist('aitoolsync', {
101
+ 'aitoolsync-package.json': { content: '{}' },
102
+ });
103
+ config.gistId = gist.gistId;
104
+
105
+ // Create remote with different content
106
+ const remoteManifest = createManifest([
107
+ { relativePath: 'config.json', checksum: 'remotehash', size: 100, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
108
+ ]);
109
+ // Set lastKnownManifest to something else to create a three-way conflict
110
+ const baseManifest = createManifest([
111
+ { relativePath: 'config.json', checksum: 'basehash', size: 100, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
112
+ ]);
113
+ config.lastKnownManifest = serializeManifest(baseManifest);
114
+ writeConfig(config);
115
+
116
+ // Write remote package
117
+ const remotePkg = await buildSyncPackage(remoteManifest, config);
118
+ await mockClient.updateGist(gist.gistId, {
119
+ 'aitoolsync-package.json': { content: JSON.stringify(remotePkg) },
120
+ });
121
+
122
+ // Modify local
123
+ fs.writeFileSync(path.join(testDir, 'config.json'), JSON.stringify({ forced: true }));
124
+
125
+ const result = await pushSync(config, mockClient, true);
126
+ expect(result.success).toBe(true);
127
+ expect(result.action).toBe('force-push');
128
+ });
129
+
130
+ it('pullSync downloads files and writes them locally', async () => {
131
+ const testDir = createTestDir(tmpDir);
132
+ const config = createDefaultConfig();
133
+ config.syncPaths = [{ name: 'my-tools', path: testDir, enabled: true }];
134
+
135
+ const gist = await mockClient.createGist('aitoolsync', {
136
+ 'aitoolsync-package.json': { content: '{}' },
137
+ });
138
+ config.gistId = gist.gistId;
139
+
140
+ // Push local first
141
+ const pushResult = await pushSync(config, mockClient, false);
142
+ expect(pushResult.success).toBe(true);
143
+
144
+ // Now modify local file
145
+ fs.writeFileSync(path.join(testDir, 'config.json'), JSON.stringify({ version: 999 }));
146
+
147
+ // Pull should overwrite with remote version
148
+ const result = await pullSync(config, mockClient, false);
149
+ expect(result.success).toBe(true);
150
+ expect(result.filesDownloaded).toBeGreaterThan(0);
151
+
152
+ // Verify file was restored from remote
153
+ const content = JSON.parse(fs.readFileSync(path.join(testDir, 'config.json'), 'utf-8'));
154
+ expect(content.version).toBe(1); // original version from push
155
+ });
156
+
157
+ it('sync package round-trips correctly', async () => {
158
+ const testDir = createTestDir(tmpDir);
159
+ const config = createDefaultConfig();
160
+ config.syncPaths = [{ name: 'my-tools', path: testDir, enabled: true }];
161
+
162
+ const manifest = createManifest([
163
+ { relativePath: 'config.json', checksum: 'abc', size: 100, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
164
+ { relativePath: 'secrets.env', checksum: 'def', size: 200, modifiedAt: new Date().toISOString(), sourceName: 'my-tools', encrypted: false, encryptionPattern: null },
165
+ ]);
166
+
167
+ const pkg = await buildSyncPackage(manifest, config);
168
+ expect(pkg.version).toBe(1);
169
+ expect(pkg.files.length).toBe(2);
170
+
171
+ // Verify content is base64 encoded
172
+ const json = JSON.stringify(pkg);
173
+ const parsed = parseSyncPackage(json);
174
+ expect(parsed).not.toBeNull();
175
+ expect(parsed!.files.length).toBe(2);
176
+
177
+ // Decode and verify file content exists (not all files are JSON)
178
+ for (const file of parsed!.files) {
179
+ const decoded = Buffer.from(file.content, 'base64').toString('utf-8');
180
+ expect(decoded.length).toBeGreaterThan(0);
181
+ }
182
+ });
183
+ });
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { createDefaultConfig, writeConfig, readConfig } from '../../src/config/config-file.js';
6
+ import { buildLocalManifest, computeDiff } from '../../src/core/sync-engine.js';
7
+ import { createManifest, serializeManifest } from '../../src/core/manifest.js';
8
+ import { diffManifests } from '../../src/utils/diff.js';
9
+ import type { AppConfig, Manifest, ManifestEntry } from '../../src/types/index.js';
10
+
11
+ function makeEntry(overrides: Partial<ManifestEntry> & { relativePath: string }): ManifestEntry {
12
+ return {
13
+ checksum: 'abc123',
14
+ size: 100,
15
+ modifiedAt: new Date().toISOString(),
16
+ sourceName: 'test',
17
+ encrypted: false,
18
+ encryptionPattern: null,
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ describe('status flow', () => {
24
+ let tmpDir: string;
25
+
26
+ beforeEach(() => {
27
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aitoolsync-status-'));
28
+ process.chdir(tmpDir);
29
+ });
30
+
31
+ afterEach(() => {
32
+ fs.rmSync(tmpDir, { recursive: true, force: true });
33
+ });
34
+
35
+ it('computes diff: no changes', () => {
36
+ const local = createManifest([
37
+ makeEntry({ relativePath: 'file1.txt', checksum: 'aaa' }),
38
+ makeEntry({ relativePath: 'file2.txt', checksum: 'bbb' }),
39
+ ]);
40
+ const remote = createManifest([
41
+ makeEntry({ relativePath: 'file1.txt', checksum: 'aaa' }),
42
+ makeEntry({ relativePath: 'file2.txt', checksum: 'bbb' }),
43
+ ]);
44
+
45
+ const diff = diffManifests(local, remote, null);
46
+ expect(diff.hasChanges).toBe(false);
47
+ expect(diff.hasConflicts).toBe(false);
48
+ expect(diff.added).toHaveLength(0);
49
+ expect(diff.removed).toHaveLength(0);
50
+ expect(diff.modified).toHaveLength(0);
51
+ });
52
+
53
+ it('detects added files', () => {
54
+ const local = createManifest([
55
+ makeEntry({ relativePath: 'old.txt', checksum: 'aaa' }),
56
+ makeEntry({ relativePath: 'new.txt', checksum: 'bbb' }),
57
+ ]);
58
+ const remote = createManifest([
59
+ makeEntry({ relativePath: 'old.txt', checksum: 'aaa' }),
60
+ ]);
61
+
62
+ const diff = diffManifests(local, remote, null);
63
+ expect(diff.hasChanges).toBe(true);
64
+ expect(diff.added).toHaveLength(1);
65
+ expect(diff.added[0].relativePath).toBe('new.txt');
66
+ });
67
+
68
+ it('detects removed files', () => {
69
+ const local = createManifest([
70
+ makeEntry({ relativePath: 'old.txt', checksum: 'aaa' }),
71
+ ]);
72
+ const remote = createManifest([
73
+ makeEntry({ relativePath: 'old.txt', checksum: 'aaa' }),
74
+ makeEntry({ relativePath: 'gone.txt', checksum: 'bbb' }),
75
+ ]);
76
+
77
+ const diff = diffManifests(local, remote, null);
78
+ expect(diff.removed).toHaveLength(1);
79
+ expect(diff.removed[0].relativePath).toBe('gone.txt');
80
+ });
81
+
82
+ it('detects modified files', () => {
83
+ const local = createManifest([
84
+ makeEntry({ relativePath: 'file.txt', checksum: 'newhash' }),
85
+ ]);
86
+ const remote = createManifest([
87
+ makeEntry({ relativePath: 'file.txt', checksum: 'oldhash' }),
88
+ ]);
89
+ const lastKnown = createManifest([
90
+ makeEntry({ relativePath: 'file.txt', checksum: 'oldhash' }),
91
+ ]);
92
+
93
+ // Only local changed (checksums differ; lastKnown matches remote)
94
+ const diff = diffManifests(local, remote, lastKnown);
95
+ expect(diff.modified).toHaveLength(1);
96
+ expect(diff.modified[0].relativePath).toBe('file.txt');
97
+ expect(diff.hasConflicts).toBe(false);
98
+ });
99
+
100
+ it('detects conflicts with three-way merge', () => {
101
+ const local = createManifest([
102
+ makeEntry({ relativePath: 'file.txt', checksum: 'localhash' }),
103
+ ]);
104
+ const remote = createManifest([
105
+ makeEntry({ relativePath: 'file.txt', checksum: 'remotehash' }),
106
+ ]);
107
+ const lastKnown = createManifest([
108
+ makeEntry({ relativePath: 'file.txt', checksum: 'basehash' }),
109
+ ]);
110
+
111
+ const diff = diffManifests(local, remote, lastKnown);
112
+ expect(diff.hasConflicts).toBe(true);
113
+ expect(diff.conflicts).toHaveLength(1);
114
+ expect(diff.conflicts[0].relativePath).toBe('file.txt');
115
+ });
116
+
117
+ it('collects files from a real directory', () => {
118
+ // Create a test directory structure
119
+ const testDir = path.join(tmpDir, 'test-config');
120
+ fs.mkdirSync(testDir, { recursive: true });
121
+ fs.writeFileSync(path.join(testDir, 'settings.json'), JSON.stringify({ key: 'value' }));
122
+ fs.writeFileSync(path.join(testDir, 'mcp.json'), JSON.stringify({ mcp: true }));
123
+
124
+ const config = createDefaultConfig();
125
+ config.syncPaths = [{ name: 'test', path: testDir, enabled: true }];
126
+
127
+ const manifest = buildLocalManifest(config);
128
+ expect(manifest.entries.length).toBe(2);
129
+ const paths = manifest.entries.map((e) => e.relativePath);
130
+ expect(paths).toContain('settings.json');
131
+ expect(paths).toContain('mcp.json');
132
+ });
133
+
134
+ it('excludes files matching exclusion rules', () => {
135
+ const testDir = path.join(tmpDir, 'excluded-config');
136
+ fs.mkdirSync(path.join(testDir, 'node_modules'), { recursive: true });
137
+ fs.mkdirSync(path.join(testDir, 'src'), { recursive: true });
138
+ fs.writeFileSync(path.join(testDir, 'src', 'main.ts'), 'code');
139
+ fs.writeFileSync(path.join(testDir, 'node_modules', 'package.json'), '{}');
140
+
141
+ const config = createDefaultConfig();
142
+ config.syncPaths = [{ name: 'excl', path: testDir, enabled: true }];
143
+
144
+ const manifest = buildLocalManifest(config);
145
+ const paths = manifest.entries.map((e) => e.relativePath);
146
+ expect(paths).toContain('src/main.ts');
147
+ expect(paths).not.toContain('node_modules/package.json');
148
+ });
149
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src"],
18
+ "exclude": ["node_modules", "dist", "tests"]
19
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ testTimeout: 10000,
9
+ },
10
+ });