cluttry 1.0.3 → 1.5.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/CHANGELOG.md +71 -0
- package/README.md +203 -300
- package/dist/commands/completions.d.ts +16 -0
- package/dist/commands/completions.d.ts.map +1 -0
- package/dist/commands/completions.js +46 -0
- package/dist/commands/completions.js.map +1 -0
- package/dist/commands/explain-copy.d.ts +11 -0
- package/dist/commands/explain-copy.d.ts.map +1 -0
- package/dist/commands/explain-copy.js +34 -0
- package/dist/commands/explain-copy.js.map +1 -0
- package/dist/commands/finish.d.ts +20 -0
- package/dist/commands/finish.d.ts.map +1 -0
- package/dist/commands/finish.js +817 -0
- package/dist/commands/finish.js.map +1 -0
- package/dist/commands/gc.d.ts +22 -0
- package/dist/commands/gc.d.ts.map +1 -0
- package/dist/commands/gc.js +163 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/open.d.ts +15 -1
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +96 -17
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/resume.d.ts +21 -0
- package/dist/commands/resume.d.ts.map +1 -0
- package/dist/commands/resume.js +106 -0
- package/dist/commands/resume.js.map +1 -0
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +6 -14
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/spawn.d.ts +3 -0
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +182 -19
- package/dist/commands/spawn.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -9
- package/dist/index.js.map +1 -1
- package/dist/lib/completions.d.ts +35 -0
- package/dist/lib/completions.d.ts.map +1 -0
- package/dist/lib/completions.js +368 -0
- package/dist/lib/completions.js.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +2 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +43 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +251 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/git.d.ts +17 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +78 -10
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/safety.d.ts +79 -0
- package/dist/lib/safety.d.ts.map +1 -0
- package/dist/lib/safety.js +133 -0
- package/dist/lib/safety.js.map +1 -0
- package/dist/lib/secrets.d.ts +29 -0
- package/dist/lib/secrets.d.ts.map +1 -1
- package/dist/lib/secrets.js +115 -0
- package/dist/lib/secrets.js.map +1 -1
- package/dist/lib/session.d.ts +93 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +254 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/types.d.ts +6 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -7
- package/src/commands/doctor.ts +0 -222
- package/src/commands/init.ts +0 -120
- package/src/commands/list.ts +0 -133
- package/src/commands/open.ts +0 -78
- package/src/commands/prune.ts +0 -36
- package/src/commands/rm.ts +0 -125
- package/src/commands/shell.ts +0 -99
- package/src/commands/spawn.ts +0 -169
- package/src/index.ts +0 -123
- package/src/lib/config.ts +0 -120
- package/src/lib/git.ts +0 -243
- package/src/lib/output.ts +0 -102
- package/src/lib/paths.ts +0 -108
- package/src/lib/secrets.ts +0 -193
- package/src/lib/types.ts +0 -69
- package/tests/config.test.ts +0 -102
- package/tests/paths.test.ts +0 -155
- package/tests/secrets.test.ts +0 -150
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -15
package/tests/config.test.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for configuration management
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { mergeConfig } from '../src/lib/config.js';
|
|
7
|
-
import type { CryConfig, CryLocalConfig } from '../src/lib/types.js';
|
|
8
|
-
|
|
9
|
-
describe('mergeConfig', () => {
|
|
10
|
-
const baseConfig: CryConfig = {
|
|
11
|
-
defaultMode: 'copy',
|
|
12
|
-
include: ['.env', '.env.local'],
|
|
13
|
-
hooks: {
|
|
14
|
-
postCreate: ['npm install'],
|
|
15
|
-
},
|
|
16
|
-
agentCommand: 'claude',
|
|
17
|
-
worktreeBaseDir: '/default/base',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
it('returns defaults when both configs are null', () => {
|
|
21
|
-
const result = mergeConfig(null, null);
|
|
22
|
-
expect(result.defaultMode).toBe('copy');
|
|
23
|
-
expect(result.include).toContain('.env');
|
|
24
|
-
expect(result.agentCommand).toBe('claude');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('uses base config values when no local config', () => {
|
|
28
|
-
const result = mergeConfig(baseConfig, null);
|
|
29
|
-
expect(result.defaultMode).toBe('copy');
|
|
30
|
-
expect(result.include).toEqual(['.env', '.env.local']);
|
|
31
|
-
expect(result.hooks.postCreate).toEqual(['npm install']);
|
|
32
|
-
expect(result.agentCommand).toBe('claude');
|
|
33
|
-
expect(result.worktreeBaseDir).toBe('/default/base');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('merges include arrays from both configs', () => {
|
|
37
|
-
const localConfig: CryLocalConfig = {
|
|
38
|
-
include: ['credentials.json', '.secrets'],
|
|
39
|
-
};
|
|
40
|
-
const result = mergeConfig(baseConfig, localConfig);
|
|
41
|
-
expect(result.include).toEqual(['.env', '.env.local', 'credentials.json', '.secrets']);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('local worktreeBaseDir overrides base', () => {
|
|
45
|
-
const localConfig: CryLocalConfig = {
|
|
46
|
-
worktreeBaseDir: '/custom/local/path',
|
|
47
|
-
};
|
|
48
|
-
const result = mergeConfig(baseConfig, localConfig);
|
|
49
|
-
expect(result.worktreeBaseDir).toBe('/custom/local/path');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('local agentCommand overrides base', () => {
|
|
53
|
-
const localConfig: CryLocalConfig = {
|
|
54
|
-
agentCommand: 'cursor',
|
|
55
|
-
};
|
|
56
|
-
const result = mergeConfig(baseConfig, localConfig);
|
|
57
|
-
expect(result.agentCommand).toBe('cursor');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('merges postCreate hooks from both configs', () => {
|
|
61
|
-
const localConfig: CryLocalConfig = {
|
|
62
|
-
hooks: {
|
|
63
|
-
postCreate: ['npm run dev', 'code .'],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
const result = mergeConfig(baseConfig, localConfig);
|
|
67
|
-
expect(result.hooks.postCreate).toEqual(['npm install', 'npm run dev', 'code .']);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('handles empty include arrays', () => {
|
|
71
|
-
const config: CryConfig = {
|
|
72
|
-
defaultMode: 'none',
|
|
73
|
-
include: [],
|
|
74
|
-
};
|
|
75
|
-
const localConfig: CryLocalConfig = {
|
|
76
|
-
include: [],
|
|
77
|
-
};
|
|
78
|
-
const result = mergeConfig(config, localConfig);
|
|
79
|
-
expect(result.include).toEqual([]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('handles missing hooks in configs', () => {
|
|
83
|
-
const config: CryConfig = {
|
|
84
|
-
defaultMode: 'symlink',
|
|
85
|
-
include: ['.env'],
|
|
86
|
-
};
|
|
87
|
-
const result = mergeConfig(config, null);
|
|
88
|
-
expect(result.hooks.postCreate).toEqual([]);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('preserves defaultMode from base config', () => {
|
|
92
|
-
const config: CryConfig = {
|
|
93
|
-
defaultMode: 'symlink',
|
|
94
|
-
include: [],
|
|
95
|
-
};
|
|
96
|
-
const localConfig: CryLocalConfig = {
|
|
97
|
-
include: ['.env'],
|
|
98
|
-
};
|
|
99
|
-
const result = mergeConfig(config, localConfig);
|
|
100
|
-
expect(result.defaultMode).toBe('symlink');
|
|
101
|
-
});
|
|
102
|
-
});
|
package/tests/paths.test.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for path utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from 'vitest';
|
|
6
|
-
import { sanitizeBranchName, getDefaultWorktreePath, resolveBranchOrPath } from '../src/lib/paths.js';
|
|
7
|
-
|
|
8
|
-
describe('sanitizeBranchName', () => {
|
|
9
|
-
it('replaces forward slashes with dashes', () => {
|
|
10
|
-
// Slashes become dashes (collapsed from double-dashes)
|
|
11
|
-
expect(sanitizeBranchName('feature/add-login')).toBe('feature-add-login');
|
|
12
|
-
expect(sanitizeBranchName('fix/bug/123')).toBe('fix-bug-123');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('replaces Windows-forbidden characters', () => {
|
|
16
|
-
expect(sanitizeBranchName('test<>:"|?*\\')).toBe('test');
|
|
17
|
-
expect(sanitizeBranchName('branch:name')).toBe('branch-name');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('replaces whitespace with dashes', () => {
|
|
21
|
-
expect(sanitizeBranchName('my branch name')).toBe('my-branch-name');
|
|
22
|
-
expect(sanitizeBranchName('tabs\there')).toBe('tabs-here');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('removes leading and trailing dots', () => {
|
|
26
|
-
expect(sanitizeBranchName('.hidden')).toBe('hidden');
|
|
27
|
-
expect(sanitizeBranchName('trailing.')).toBe('trailing');
|
|
28
|
-
expect(sanitizeBranchName('...dots...')).toBe('dots');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('collapses multiple dashes', () => {
|
|
32
|
-
expect(sanitizeBranchName('a--b---c')).toBe('a-b-c');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('removes leading and trailing dashes', () => {
|
|
36
|
-
expect(sanitizeBranchName('-start')).toBe('start');
|
|
37
|
-
expect(sanitizeBranchName('end-')).toBe('end');
|
|
38
|
-
expect(sanitizeBranchName('---middle---')).toBe('middle');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('handles complex branch names', () => {
|
|
42
|
-
expect(sanitizeBranchName('feature/user-auth/oauth2.0')).toBe('feature-user-auth-oauth2.0');
|
|
43
|
-
// Note: # is allowed in branch names and preserved
|
|
44
|
-
expect(sanitizeBranchName('fix/issue#123')).toBe('fix-issue#123');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('handles simple branch names unchanged', () => {
|
|
48
|
-
expect(sanitizeBranchName('main')).toBe('main');
|
|
49
|
-
expect(sanitizeBranchName('develop')).toBe('develop');
|
|
50
|
-
expect(sanitizeBranchName('my-feature')).toBe('my-feature');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('getDefaultWorktreePath', () => {
|
|
55
|
-
const repoRoot = '/home/user/myrepo';
|
|
56
|
-
|
|
57
|
-
it('returns explicit path when provided', () => {
|
|
58
|
-
const result = getDefaultWorktreePath(repoRoot, 'feature', {
|
|
59
|
-
explicitPath: '/custom/path',
|
|
60
|
-
});
|
|
61
|
-
expect(result).toBe('/custom/path');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('resolves relative explicit path against CWD', () => {
|
|
65
|
-
const result = getDefaultWorktreePath(repoRoot, 'feature', {
|
|
66
|
-
explicitPath: './relative/path',
|
|
67
|
-
});
|
|
68
|
-
expect(result).toContain('relative/path');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('uses base directory with repo name and sanitized branch', () => {
|
|
72
|
-
const result = getDefaultWorktreePath(repoRoot, 'feature/test', {
|
|
73
|
-
baseDir: '/worktrees',
|
|
74
|
-
repoName: 'myrepo',
|
|
75
|
-
});
|
|
76
|
-
expect(result).toBe('/worktrees/myrepo/feature-test');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('derives repo name from repoRoot if not provided', () => {
|
|
80
|
-
const result = getDefaultWorktreePath(repoRoot, 'main', {
|
|
81
|
-
baseDir: '/worktrees',
|
|
82
|
-
});
|
|
83
|
-
expect(result).toBe('/worktrees/myrepo/main');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('resolves relative base directory against repo root', () => {
|
|
87
|
-
const result = getDefaultWorktreePath(repoRoot, 'feature', {
|
|
88
|
-
baseDir: '../sibling',
|
|
89
|
-
repoName: 'myrepo',
|
|
90
|
-
});
|
|
91
|
-
expect(result).toContain('myrepo/feature');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('defaults to .worktrees/<branch> inside repo', () => {
|
|
95
|
-
const result = getDefaultWorktreePath(repoRoot, 'my-branch');
|
|
96
|
-
expect(result).toBe('/home/user/myrepo/.worktrees/my-branch');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('sanitizes branch names in default path', () => {
|
|
100
|
-
const result = getDefaultWorktreePath(repoRoot, 'feature/login');
|
|
101
|
-
expect(result).toBe('/home/user/myrepo/.worktrees/feature-login');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('resolveBranchOrPath', () => {
|
|
106
|
-
const repoRoot = '/home/user/myrepo';
|
|
107
|
-
const worktrees = [
|
|
108
|
-
{ branch: 'main', path: '/home/user/myrepo' },
|
|
109
|
-
{ branch: 'feature-login', path: '/home/user/myrepo/.worktrees/feature-login' },
|
|
110
|
-
{ branch: null, path: '/home/user/myrepo/.worktrees/detached' },
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
it('finds worktree by exact branch name', () => {
|
|
114
|
-
const result = resolveBranchOrPath('feature-login', worktrees, repoRoot);
|
|
115
|
-
expect(result).toEqual({
|
|
116
|
-
branch: 'feature-login',
|
|
117
|
-
path: '/home/user/myrepo/.worktrees/feature-login',
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('finds worktree by absolute path', () => {
|
|
122
|
-
const result = resolveBranchOrPath('/home/user/myrepo/.worktrees/feature-login', worktrees, repoRoot);
|
|
123
|
-
expect(result).toEqual({
|
|
124
|
-
branch: 'feature-login',
|
|
125
|
-
path: '/home/user/myrepo/.worktrees/feature-login',
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('finds worktree by partial path suffix', () => {
|
|
130
|
-
const result = resolveBranchOrPath('feature-login', worktrees, repoRoot);
|
|
131
|
-
expect(result).not.toBeNull();
|
|
132
|
-
expect(result?.branch).toBe('feature-login');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('returns null when no match found', () => {
|
|
136
|
-
const result = resolveBranchOrPath('nonexistent', worktrees, repoRoot);
|
|
137
|
-
expect(result).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('handles detached worktrees', () => {
|
|
141
|
-
const result = resolveBranchOrPath('/home/user/myrepo/.worktrees/detached', worktrees, repoRoot);
|
|
142
|
-
expect(result).toEqual({
|
|
143
|
-
branch: null,
|
|
144
|
-
path: '/home/user/myrepo/.worktrees/detached',
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('handles main worktree', () => {
|
|
149
|
-
const result = resolveBranchOrPath('main', worktrees, repoRoot);
|
|
150
|
-
expect(result).toEqual({
|
|
151
|
-
branch: 'main',
|
|
152
|
-
path: '/home/user/myrepo',
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
});
|
package/tests/secrets.test.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for secrets/file safety module
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
-
import { checkFileSafety } from '../src/lib/secrets.js';
|
|
7
|
-
import * as fs from 'node:fs';
|
|
8
|
-
import * as git from '../src/lib/git.js';
|
|
9
|
-
|
|
10
|
-
// Mock the modules
|
|
11
|
-
vi.mock('node:fs', () => ({
|
|
12
|
-
existsSync: vi.fn(),
|
|
13
|
-
copyFileSync: vi.fn(),
|
|
14
|
-
symlinkSync: vi.fn(),
|
|
15
|
-
mkdirSync: vi.fn(),
|
|
16
|
-
statSync: vi.fn(),
|
|
17
|
-
readdirSync: vi.fn(),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock('../src/lib/git.js', () => ({
|
|
21
|
-
isTracked: vi.fn(),
|
|
22
|
-
isIgnored: vi.fn(),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
describe('checkFileSafety', () => {
|
|
26
|
-
const repoRoot = '/home/user/myrepo';
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
vi.resetAllMocks();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
vi.restoreAllMocks();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns unsafe result when file does not exist', () => {
|
|
37
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
38
|
-
|
|
39
|
-
const result = checkFileSafety('.env', repoRoot);
|
|
40
|
-
|
|
41
|
-
expect(result.safe).toBe(false);
|
|
42
|
-
expect(result.exists).toBe(false);
|
|
43
|
-
expect(result.reason).toBe('File does not exist');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('returns unsafe result when file is tracked by git', () => {
|
|
47
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
48
|
-
vi.mocked(git.isTracked).mockReturnValue(true);
|
|
49
|
-
|
|
50
|
-
const result = checkFileSafety('.env', repoRoot);
|
|
51
|
-
|
|
52
|
-
expect(result.safe).toBe(false);
|
|
53
|
-
expect(result.exists).toBe(true);
|
|
54
|
-
expect(result.isTracked).toBe(true);
|
|
55
|
-
expect(result.reason).toBe('File is tracked by git (would be committed)');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('returns unsafe result when file is not ignored by git', () => {
|
|
59
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
60
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
61
|
-
vi.mocked(git.isIgnored).mockReturnValue(false);
|
|
62
|
-
|
|
63
|
-
const result = checkFileSafety('.env', repoRoot);
|
|
64
|
-
|
|
65
|
-
expect(result.safe).toBe(false);
|
|
66
|
-
expect(result.exists).toBe(true);
|
|
67
|
-
expect(result.isTracked).toBe(false);
|
|
68
|
-
expect(result.isIgnored).toBe(false);
|
|
69
|
-
expect(result.reason).toBe('File is not ignored by git (could be accidentally committed)');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns safe result when file exists, is not tracked, and is ignored', () => {
|
|
73
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
74
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
75
|
-
vi.mocked(git.isIgnored).mockReturnValue(true);
|
|
76
|
-
|
|
77
|
-
const result = checkFileSafety('.env', repoRoot);
|
|
78
|
-
|
|
79
|
-
expect(result.safe).toBe(true);
|
|
80
|
-
expect(result.exists).toBe(true);
|
|
81
|
-
expect(result.isTracked).toBe(false);
|
|
82
|
-
expect(result.isIgnored).toBe(true);
|
|
83
|
-
expect(result.reason).toBeUndefined();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('handles absolute paths correctly', () => {
|
|
87
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
88
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
89
|
-
vi.mocked(git.isIgnored).mockReturnValue(true);
|
|
90
|
-
|
|
91
|
-
const result = checkFileSafety('/home/user/myrepo/.env', repoRoot);
|
|
92
|
-
|
|
93
|
-
expect(result.path).toBe('.env');
|
|
94
|
-
expect(result.safe).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('handles nested paths correctly', () => {
|
|
98
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
99
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
100
|
-
vi.mocked(git.isIgnored).mockReturnValue(true);
|
|
101
|
-
|
|
102
|
-
const result = checkFileSafety('config/secrets/oauth.json', repoRoot);
|
|
103
|
-
|
|
104
|
-
expect(result.path).toBe('config/secrets/oauth.json');
|
|
105
|
-
expect(result.safe).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('safety enforcement rules', () => {
|
|
110
|
-
const repoRoot = '/home/user/myrepo';
|
|
111
|
-
|
|
112
|
-
beforeEach(() => {
|
|
113
|
-
vi.resetAllMocks();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('NEVER allows tracked files to be copied/symlinked', () => {
|
|
117
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
118
|
-
vi.mocked(git.isTracked).mockReturnValue(true);
|
|
119
|
-
vi.mocked(git.isIgnored).mockReturnValue(false); // Even if somehow ignored
|
|
120
|
-
|
|
121
|
-
const result = checkFileSafety('src/config.ts', repoRoot);
|
|
122
|
-
|
|
123
|
-
expect(result.safe).toBe(false);
|
|
124
|
-
expect(result.reason).toContain('tracked');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('ONLY allows files that are explicitly ignored', () => {
|
|
128
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
129
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
130
|
-
vi.mocked(git.isIgnored).mockReturnValue(false);
|
|
131
|
-
|
|
132
|
-
const result = checkFileSafety('random-file.txt', repoRoot);
|
|
133
|
-
|
|
134
|
-
expect(result.safe).toBe(false);
|
|
135
|
-
expect(result.reason).toContain('not ignored');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('correctly identifies safe secret files', () => {
|
|
139
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
140
|
-
vi.mocked(git.isTracked).mockReturnValue(false);
|
|
141
|
-
vi.mocked(git.isIgnored).mockReturnValue(true);
|
|
142
|
-
|
|
143
|
-
const secretFiles = ['.env', '.env.local', 'config/oauth.json', '.secrets/api-key'];
|
|
144
|
-
|
|
145
|
-
for (const file of secretFiles) {
|
|
146
|
-
const result = checkFileSafety(file, repoRoot);
|
|
147
|
-
expect(result.safe).toBe(true);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
-
}
|
package/vitest.config.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
coverage: {
|
|
9
|
-
provider: 'v8',
|
|
10
|
-
reporter: ['text', 'json', 'html'],
|
|
11
|
-
include: ['src/**/*.ts'],
|
|
12
|
-
exclude: ['src/index.ts'],
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
});
|