@tengx5383/aitool-sync-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +4 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/doctor.d.ts.map +1 -1
  5. package/dist/commands/doctor.js +13 -16
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/init.d.ts +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +71 -26
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/list.js +1 -1
  12. package/dist/commands/list.js.map +1 -1
  13. package/dist/commands/pull.d.ts.map +1 -1
  14. package/dist/commands/pull.js +9 -10
  15. package/dist/commands/pull.js.map +1 -1
  16. package/dist/commands/push.d.ts.map +1 -1
  17. package/dist/commands/push.js +39 -22
  18. package/dist/commands/push.js.map +1 -1
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +15 -8
  21. package/dist/commands/status.js.map +1 -1
  22. package/dist/config/config-file.js +1 -1
  23. package/dist/config/config-file.js.map +1 -1
  24. package/dist/config/schema.d.ts +3 -3
  25. package/dist/config/schema.js +1 -1
  26. package/dist/config/schema.js.map +1 -1
  27. package/dist/core/backup.d.ts.map +1 -1
  28. package/dist/core/backup.js +41 -13
  29. package/dist/core/backup.js.map +1 -1
  30. package/dist/core/file-collector.js +70 -1
  31. package/dist/core/file-collector.js.map +1 -1
  32. package/dist/core/manifest.d.ts.map +1 -1
  33. package/dist/core/sync-engine.d.ts +7 -14
  34. package/dist/core/sync-engine.d.ts.map +1 -1
  35. package/dist/core/sync-engine.js +192 -140
  36. package/dist/core/sync-engine.js.map +1 -1
  37. package/dist/git/github-api.d.ts +14 -0
  38. package/dist/git/github-api.d.ts.map +1 -0
  39. package/dist/git/github-api.js +38 -0
  40. package/dist/git/github-api.js.map +1 -0
  41. package/dist/git/repo-client.d.ts +17 -0
  42. package/dist/git/repo-client.d.ts.map +1 -0
  43. package/dist/git/repo-client.js +40 -0
  44. package/dist/git/repo-client.js.map +1 -0
  45. package/dist/presets/claude-code.d.ts.map +1 -1
  46. package/dist/presets/claude-code.js +5 -0
  47. package/dist/presets/claude-code.js.map +1 -1
  48. package/dist/types/config.d.ts +2 -2
  49. package/dist/types/config.d.ts.map +1 -1
  50. package/dist/types/git.d.ts +5 -0
  51. package/dist/types/git.d.ts.map +1 -0
  52. package/dist/types/git.js +2 -0
  53. package/dist/types/git.js.map +1 -0
  54. package/dist/types/index.d.ts +2 -2
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/package.json +10 -3
  57. package/.claude/settings.local.json +0 -8
  58. package/tests/integration/encryption-flow.test.ts +0 -142
  59. package/tests/integration/init-flow.test.ts +0 -69
  60. package/tests/integration/push-pull-flow.test.ts +0 -183
  61. package/tests/integration/status-flow.test.ts +0 -149
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/types/git.ts"],"names":[],"mappings":""}
@@ -1,4 +1,4 @@
1
1
  export type { AppConfig, SyncPathEntry, ExclusionRule, EncryptionRule, ToolPresetId, ToolPreset, } from './config.js';
2
- export type { Manifest, ManifestEntry, SyncFile, SyncPackage, DiffEntry, ConflictEntry, SyncDiff, SyncResult, } from './sync.js';
3
- export type { GistFile, GistDescriptor, GistClientInterface, } from './gist.js';
2
+ export type { Manifest, ManifestEntry, DiffEntry, ConflictEntry, SyncDiff, SyncResult, } from './sync.js';
3
+ export type { RepoClientInterface, } from './git.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,QAAQ,EACR,aAAa,EACb,QAAQ,EACR,WAAW,EACX,SAAS,EACT,aAAa,EACb,QAAQ,EACR,UAAU,GACX,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,QAAQ,EACR,cAAc,EACd,mBAAmB,GACpB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,EACZ,UAAU,GACX,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,QAAQ,EACR,aAAa,EACb,SAAS,EACT,aAAa,EACb,QAAQ,EACR,UAAU,GACX,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,mBAAmB,GACpB,MAAM,UAAU,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tengx5383/aitool-sync-cli",
3
- "version": "0.1.0",
4
- "description": "CLI for syncing AI agent tool configs across devices via GitHub Gist",
3
+ "version": "0.2.0",
4
+ "description": "CLI for syncing AI agent tool configs across devices via Git repositories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -16,7 +16,13 @@
16
16
  "test:watch": "vitest",
17
17
  "prettier": "prettier --write ."
18
18
  },
19
- "keywords": ["cli", "sync", "ai-tools", "claude-code", "config"],
19
+ "keywords": [
20
+ "cli",
21
+ "sync",
22
+ "ai-tools",
23
+ "claude-code",
24
+ "config"
25
+ ],
20
26
  "license": "MIT",
21
27
  "dependencies": {
22
28
  "@octokit/rest": "^21.0.0",
@@ -27,6 +33,7 @@
27
33
  "inquirer": "^12.0.0",
28
34
  "micromatch": "^4.0.8",
29
35
  "ora": "^8.1.0",
36
+ "simple-git": "^3.36.0",
30
37
  "zod": "^3.23.0"
31
38
  },
32
39
  "devDependencies": {
@@ -1,8 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(node -e \"const p = require\\('./package-lock.json'\\); p.name = '@tengx5383/aitool-sync-cli'; p.packages[''].name = '@tengx5383/aitool-sync-cli'; require\\('fs'\\).writeFileSync\\('package-lock.json', JSON.stringify\\(p, null, 2\\) + '\\\\n'\\)\")",
5
- "Bash(npm run *)"
6
- ]
7
- }
8
- }
@@ -1,142 +0,0 @@
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 { encrypt, decrypt, isEncrypted } from '../../src/crypto/encryptor.js';
6
- import { setPassword, clearPassword, resolvePassword, encryptWithPassword, decryptWithPassword, hasPassword } from '../../src/crypto/key-resolver.js';
7
- import { createDefaultConfig, writeConfig } from '../../src/config/config-file.js';
8
- import { buildSyncPackage, parseSyncPackage } from '../../src/core/sync-package.js';
9
- import { createManifest, serializeManifest } from '../../src/core/manifest.js';
10
- import { pushSync, pullSync, writeRemoteFiles } from '../../src/core/sync-engine.js';
11
- import { MockGistClient } from '../../src/gist/mock-client.js';
12
- import type { AppConfig } from '../../src/types/index.js';
13
-
14
- const TEST_PASSWORD = 'test-secret-password-123';
15
-
16
- function createTestDir(base: string): string {
17
- const dir = path.join(base, 'encrypted-tools');
18
- fs.mkdirSync(dir, { recursive: true });
19
- fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify({ version: 1 }));
20
- fs.writeFileSync(path.join(dir, 'token.json'), JSON.stringify({ apiKey: 'secret-token-123' }));
21
- return dir;
22
- }
23
-
24
- describe('encryption flow', () => {
25
- let tmpDir: string;
26
- let mockClient: MockGistClient;
27
-
28
- beforeEach(() => {
29
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aitoolsync-crypto-'));
30
- process.chdir(tmpDir);
31
- mockClient = new MockGistClient();
32
- setPassword(TEST_PASSWORD);
33
- });
34
-
35
- afterEach(() => {
36
- fs.rmSync(tmpDir, { recursive: true, force: true });
37
- clearPassword();
38
- });
39
-
40
- it('encrypts and decrypts a string', () => {
41
- const plaintext = 'Hello, World!';
42
- const encrypted = encrypt(plaintext, TEST_PASSWORD);
43
- expect(encrypted).not.toBe(plaintext);
44
-
45
- const decrypted = decrypt(encrypted, TEST_PASSWORD);
46
- expect(decrypted).toBe(plaintext);
47
- });
48
-
49
- it('detects encrypted payloads', () => {
50
- const encrypted = encrypt('test data', TEST_PASSWORD);
51
- expect(isEncrypted(encrypted)).toBe(true);
52
- expect(isEncrypted('plain text')).toBe(false);
53
- });
54
-
55
- it('key resolver caches and resolves password', () => {
56
- clearPassword();
57
- expect(hasPassword()).toBe(false);
58
-
59
- setPassword('my-password');
60
- expect(hasPassword()).toBe(true);
61
- expect(resolvePassword()).toBe('my-password');
62
-
63
- clearPassword();
64
- expect(hasPassword()).toBe(false);
65
- });
66
-
67
- it('encrypt with password convenience function', () => {
68
- const plaintext = 'sensitive data';
69
- const encrypted = encryptWithPassword(plaintext);
70
- expect(encrypted).not.toBe(plaintext);
71
-
72
- const decrypted = decryptWithPassword(encrypted);
73
- expect(decrypted).toBe(plaintext);
74
- });
75
-
76
- it('push and pull with encrypted files', async () => {
77
- const testDir = createTestDir(tmpDir);
78
- const config = createDefaultConfig();
79
- config.syncPaths = [{ name: 'enc-tools', path: testDir, enabled: true }];
80
- config.encryption = [{ pattern: '**/token.json' }];
81
-
82
- const gist = await mockClient.createGist('aitoolsync', {
83
- 'aitoolsync-package.json': { content: '{}' },
84
- });
85
- config.gistId = gist.gistId;
86
- writeConfig(config);
87
-
88
- // Push with encryption
89
- const pushResult = await pushSync(config, mockClient, false, encryptWithPassword);
90
- expect(pushResult.success).toBe(true);
91
-
92
- // Verify Gist content is encrypted for token.json
93
- const fetched = await mockClient.getGist(gist.gistId);
94
- const pkgFile = fetched.files.find((f) => f.filename === 'aitoolsync-package.json');
95
- const pkg = parseSyncPackage(pkgFile!.content);
96
- const tokenFile = pkg!.files.find((f) => f.relativePath === 'token.json');
97
- expect(tokenFile!.encrypted).toBe(true);
98
- expect(tokenFile!.content).not.toContain('secret-token');
99
-
100
- const configFile = pkg!.files.find((f) => f.relativePath === 'config.json');
101
- expect(configFile!.encrypted).toBe(false);
102
- });
103
-
104
- it('pull and decrypt files', async () => {
105
- const testDir = createTestDir(tmpDir);
106
- const config = createDefaultConfig();
107
- config.syncPaths = [{ name: 'enc-tools', path: testDir, enabled: true }];
108
- config.encryption = [{ pattern: '**/token.json' }];
109
-
110
- const gist = await mockClient.createGist('aitoolsync', {
111
- 'aitoolsync-package.json': { content: '{}' },
112
- });
113
- config.gistId = gist.gistId;
114
- writeConfig(config);
115
-
116
- // Push with encryption first
117
- await pushSync(config, mockClient, false, encryptWithPassword);
118
-
119
- // Corrupt local files
120
- fs.writeFileSync(path.join(testDir, 'token.json'), 'CORRUPTED');
121
-
122
- // Pull and decrypt
123
- const pullResult = await pullSync(config, mockClient, false, decryptWithPassword);
124
- expect(pullResult.success).toBe(true);
125
-
126
- // Verify file was restored correctly
127
- const content = JSON.parse(fs.readFileSync(path.join(testDir, 'token.json'), 'utf-8'));
128
- expect(content.apiKey).toBe('secret-token-123');
129
- });
130
-
131
- it('fails to decrypt with wrong password', () => {
132
- const encrypted = encrypt('secret', TEST_PASSWORD);
133
- expect(() => decrypt(encrypted, 'wrong-password')).toThrow();
134
- });
135
-
136
- it('encrypted content does not leak plaintext', () => {
137
- const plaintext = 'super-secret-api-key-12345';
138
- const encrypted = encrypt(plaintext, TEST_PASSWORD);
139
- expect(encrypted).not.toContain('super-secret-api-key');
140
- expect(encrypted).not.toContain('12345');
141
- });
142
- });
@@ -1,69 +0,0 @@
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
- });
@@ -1,183 +0,0 @@
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
- });
@@ -1,149 +0,0 @@
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
- });