claudekit-cli 1.4.1 → 1.5.1
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/bin/ck-darwin-arm64 +0 -0
- package/bin/ck-darwin-x64 +0 -0
- package/bin/ck-linux-x64 +0 -0
- package/bin/ck-win32-x64.exe +0 -0
- package/bin/ck.js +62 -0
- package/package.json +6 -2
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/claude-code-review.yml +0 -57
- package/.github/workflows/claude.yml +0 -50
- package/.github/workflows/release.yml +0 -102
- package/.releaserc.json +0 -17
- package/.repomixignore +0 -15
- package/AGENTS.md +0 -217
- package/CHANGELOG.md +0 -95
- package/CLAUDE.md +0 -34
- package/biome.json +0 -28
- package/bun.lock +0 -863
- package/dist/index.js +0 -22511
- package/src/commands/new.ts +0 -185
- package/src/commands/update.ts +0 -174
- package/src/commands/version.ts +0 -135
- package/src/index.ts +0 -102
- package/src/lib/auth.ts +0 -157
- package/src/lib/download.ts +0 -689
- package/src/lib/github.ts +0 -230
- package/src/lib/merge.ts +0 -119
- package/src/lib/prompts.ts +0 -114
- package/src/types.ts +0 -178
- package/src/utils/config.ts +0 -87
- package/src/utils/file-scanner.ts +0 -134
- package/src/utils/logger.ts +0 -124
- package/src/utils/safe-prompts.ts +0 -44
- package/src/utils/safe-spinner.ts +0 -38
- package/src/version.json +0 -3
- package/tests/commands/version.test.ts +0 -297
- package/tests/integration/cli.test.ts +0 -252
- package/tests/lib/auth.test.ts +0 -116
- package/tests/lib/download.test.ts +0 -292
- package/tests/lib/github-download-priority.test.ts +0 -432
- package/tests/lib/github.test.ts +0 -52
- package/tests/lib/merge.test.ts +0 -267
- package/tests/lib/prompts.test.ts +0 -66
- package/tests/types.test.ts +0 -337
- package/tests/utils/config.test.ts +0 -263
- package/tests/utils/file-scanner.test.ts +0 -202
- package/tests/utils/logger.test.ts +0 -239
- package/tsconfig.json +0 -30
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import type { Config } from "../../src/types.js";
|
|
7
|
-
import { ConfigManager } from "../../src/utils/config.js";
|
|
8
|
-
|
|
9
|
-
const TEST_CONFIG_DIR = join(homedir(), ".claudekit-test");
|
|
10
|
-
const TEST_CONFIG_FILE = join(TEST_CONFIG_DIR, "config.json");
|
|
11
|
-
|
|
12
|
-
describe("ConfigManager", () => {
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
// Create test config directory
|
|
15
|
-
if (!existsSync(TEST_CONFIG_DIR)) {
|
|
16
|
-
await mkdir(TEST_CONFIG_DIR, { recursive: true });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Override config paths for testing
|
|
20
|
-
// Note: This is a simplified test - in production we'd need to mock the paths
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
// Clean up test config directory
|
|
25
|
-
if (existsSync(TEST_CONFIG_DIR)) {
|
|
26
|
-
await rm(TEST_CONFIG_DIR, { recursive: true, force: true });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Reset ConfigManager state
|
|
30
|
-
(ConfigManager as any).config = null;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("load", () => {
|
|
34
|
-
test("should return default config when no config file exists", async () => {
|
|
35
|
-
const config = await ConfigManager.load();
|
|
36
|
-
expect(config).toEqual({ github: {}, defaults: {} });
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("should load config from file when it exists", async () => {
|
|
40
|
-
const testConfig: Config = {
|
|
41
|
-
github: { token: "test-token" },
|
|
42
|
-
defaults: { kit: "engineer", dir: "./test" },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Write test config file (to actual location for this test)
|
|
46
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
47
|
-
const actualConfigFile = join(actualConfigDir, "config.json");
|
|
48
|
-
|
|
49
|
-
if (!existsSync(actualConfigDir)) {
|
|
50
|
-
await mkdir(actualConfigDir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
await writeFile(actualConfigFile, JSON.stringify(testConfig));
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const config = await ConfigManager.load();
|
|
56
|
-
expect(config.github?.token).toBe("test-token");
|
|
57
|
-
expect(config.defaults?.kit).toBe("engineer");
|
|
58
|
-
} finally {
|
|
59
|
-
// Cleanup
|
|
60
|
-
if (existsSync(actualConfigFile)) {
|
|
61
|
-
await rm(actualConfigFile);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("should return default config on invalid JSON", async () => {
|
|
67
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
68
|
-
const actualConfigFile = join(actualConfigDir, "config.json");
|
|
69
|
-
|
|
70
|
-
if (!existsSync(actualConfigDir)) {
|
|
71
|
-
await mkdir(actualConfigDir, { recursive: true });
|
|
72
|
-
}
|
|
73
|
-
await writeFile(actualConfigFile, "invalid json");
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const config = await ConfigManager.load();
|
|
77
|
-
expect(config).toEqual({ github: {}, defaults: {} });
|
|
78
|
-
} finally {
|
|
79
|
-
// Cleanup
|
|
80
|
-
if (existsSync(actualConfigFile)) {
|
|
81
|
-
await rm(actualConfigFile);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("should cache config after first load", async () => {
|
|
87
|
-
const config1 = await ConfigManager.load();
|
|
88
|
-
const config2 = await ConfigManager.load();
|
|
89
|
-
expect(config1).toBe(config2); // Same reference
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("save", () => {
|
|
94
|
-
test("should save valid config to file", async () => {
|
|
95
|
-
const testConfig: Config = {
|
|
96
|
-
github: { token: "test-token" },
|
|
97
|
-
defaults: { kit: "marketing", dir: "./projects" },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
await ConfigManager.save(testConfig);
|
|
101
|
-
|
|
102
|
-
// Verify file was created
|
|
103
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
104
|
-
expect(existsSync(actualConfigFile)).toBe(true);
|
|
105
|
-
|
|
106
|
-
// Cleanup
|
|
107
|
-
if (existsSync(actualConfigFile)) {
|
|
108
|
-
await rm(actualConfigFile);
|
|
109
|
-
}
|
|
110
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
111
|
-
if (existsSync(actualConfigDir)) {
|
|
112
|
-
await rm(actualConfigDir, { recursive: true });
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("should create config directory if it does not exist", async () => {
|
|
117
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
118
|
-
if (existsSync(actualConfigDir)) {
|
|
119
|
-
await rm(actualConfigDir, { recursive: true });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const testConfig: Config = { github: {}, defaults: {} };
|
|
123
|
-
await ConfigManager.save(testConfig);
|
|
124
|
-
|
|
125
|
-
expect(existsSync(actualConfigDir)).toBe(true);
|
|
126
|
-
|
|
127
|
-
// Cleanup
|
|
128
|
-
if (existsSync(actualConfigDir)) {
|
|
129
|
-
await rm(actualConfigDir, { recursive: true });
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("should throw error on invalid config", async () => {
|
|
134
|
-
const invalidConfig = {
|
|
135
|
-
github: { token: 123 }, // Invalid: should be string
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
await expect(ConfigManager.save(invalidConfig as any)).rejects.toThrow();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("should update cached config", async () => {
|
|
142
|
-
const testConfig: Config = {
|
|
143
|
-
github: { token: "new-token" },
|
|
144
|
-
defaults: {},
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
await ConfigManager.save(testConfig);
|
|
148
|
-
const loaded = await ConfigManager.get();
|
|
149
|
-
expect(loaded.github?.token).toBe("new-token");
|
|
150
|
-
|
|
151
|
-
// Cleanup
|
|
152
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
153
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
154
|
-
if (existsSync(actualConfigFile)) {
|
|
155
|
-
await rm(actualConfigFile);
|
|
156
|
-
}
|
|
157
|
-
if (existsSync(actualConfigDir)) {
|
|
158
|
-
await rm(actualConfigDir, { recursive: true });
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe("get", () => {
|
|
164
|
-
test("should return current config", async () => {
|
|
165
|
-
const config = await ConfigManager.get();
|
|
166
|
-
expect(config).toBeDefined();
|
|
167
|
-
expect(config).toHaveProperty("github");
|
|
168
|
-
expect(config).toHaveProperty("defaults");
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("set", () => {
|
|
173
|
-
test("should set nested config value", async () => {
|
|
174
|
-
await ConfigManager.set("github.token", "test-token-123");
|
|
175
|
-
const config = await ConfigManager.get();
|
|
176
|
-
expect(config.github?.token).toBe("test-token-123");
|
|
177
|
-
|
|
178
|
-
// Cleanup
|
|
179
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
180
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
181
|
-
if (existsSync(actualConfigFile)) {
|
|
182
|
-
await rm(actualConfigFile);
|
|
183
|
-
}
|
|
184
|
-
if (existsSync(actualConfigDir)) {
|
|
185
|
-
await rm(actualConfigDir, { recursive: true });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("should create nested objects if they do not exist", async () => {
|
|
190
|
-
await ConfigManager.set("defaults.kit", "engineer");
|
|
191
|
-
const config = await ConfigManager.get();
|
|
192
|
-
expect(config.defaults?.kit).toBe("engineer");
|
|
193
|
-
|
|
194
|
-
// Cleanup
|
|
195
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
196
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
197
|
-
if (existsSync(actualConfigFile)) {
|
|
198
|
-
await rm(actualConfigFile);
|
|
199
|
-
}
|
|
200
|
-
if (existsSync(actualConfigDir)) {
|
|
201
|
-
await rm(actualConfigDir, { recursive: true });
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("should handle multiple nested levels", async () => {
|
|
206
|
-
await ConfigManager.set("defaults.dir", "/test/path");
|
|
207
|
-
const config = await ConfigManager.get();
|
|
208
|
-
expect(config.defaults?.dir).toBe("/test/path");
|
|
209
|
-
|
|
210
|
-
// Cleanup
|
|
211
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
212
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
213
|
-
if (existsSync(actualConfigFile)) {
|
|
214
|
-
await rm(actualConfigFile);
|
|
215
|
-
}
|
|
216
|
-
if (existsSync(actualConfigDir)) {
|
|
217
|
-
await rm(actualConfigDir, { recursive: true });
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("getToken", () => {
|
|
223
|
-
test("should return token from config", async () => {
|
|
224
|
-
await ConfigManager.setToken("test-token-456");
|
|
225
|
-
const token = await ConfigManager.getToken();
|
|
226
|
-
expect(token).toBe("test-token-456");
|
|
227
|
-
|
|
228
|
-
// Cleanup
|
|
229
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
230
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
231
|
-
if (existsSync(actualConfigFile)) {
|
|
232
|
-
await rm(actualConfigFile);
|
|
233
|
-
}
|
|
234
|
-
if (existsSync(actualConfigDir)) {
|
|
235
|
-
await rm(actualConfigDir, { recursive: true });
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("should return undefined if no token is set", async () => {
|
|
240
|
-
(ConfigManager as any).config = null;
|
|
241
|
-
const token = await ConfigManager.getToken();
|
|
242
|
-
expect(token).toBeUndefined();
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe("setToken", () => {
|
|
247
|
-
test("should set token in config", async () => {
|
|
248
|
-
await ConfigManager.setToken("new-test-token");
|
|
249
|
-
const config = await ConfigManager.get();
|
|
250
|
-
expect(config.github?.token).toBe("new-test-token");
|
|
251
|
-
|
|
252
|
-
// Cleanup
|
|
253
|
-
const actualConfigFile = join(homedir(), ".claudekit", "config.json");
|
|
254
|
-
const actualConfigDir = join(homedir(), ".claudekit");
|
|
255
|
-
if (existsSync(actualConfigFile)) {
|
|
256
|
-
await rm(actualConfigFile);
|
|
257
|
-
}
|
|
258
|
-
if (existsSync(actualConfigDir)) {
|
|
259
|
-
await rm(actualConfigDir, { recursive: true });
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { mkdir, remove, writeFile } from "fs-extra";
|
|
4
|
-
import { FileScanner } from "../../src/utils/file-scanner.js";
|
|
5
|
-
|
|
6
|
-
describe("FileScanner", () => {
|
|
7
|
-
const testDir = join(__dirname, "..", "..", "temp-test-file-scanner");
|
|
8
|
-
const destDir = join(testDir, "dest");
|
|
9
|
-
const sourceDir = join(testDir, "source");
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
// Clean up and create test directories
|
|
13
|
-
await remove(testDir);
|
|
14
|
-
await mkdir(testDir, { recursive: true });
|
|
15
|
-
await mkdir(destDir, { recursive: true });
|
|
16
|
-
await mkdir(sourceDir, { recursive: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
// Clean up test directories
|
|
21
|
-
await remove(testDir);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("getFiles", () => {
|
|
25
|
-
test("should return empty array for non-existent directory", async () => {
|
|
26
|
-
const files = await FileScanner.getFiles(join(testDir, "non-existent"));
|
|
27
|
-
expect(files).toEqual([]);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("should return files from directory", async () => {
|
|
31
|
-
// Create test files
|
|
32
|
-
await writeFile(join(destDir, "file1.txt"), "content1");
|
|
33
|
-
await writeFile(join(destDir, "file2.txt"), "content2");
|
|
34
|
-
|
|
35
|
-
const files = await FileScanner.getFiles(destDir);
|
|
36
|
-
|
|
37
|
-
expect(files).toHaveLength(2);
|
|
38
|
-
expect(files).toContain("file1.txt");
|
|
39
|
-
expect(files).toContain("file2.txt");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("should recursively scan subdirectories", async () => {
|
|
43
|
-
// Create nested structure
|
|
44
|
-
await mkdir(join(destDir, "subdir"), { recursive: true });
|
|
45
|
-
await writeFile(join(destDir, "file1.txt"), "content1");
|
|
46
|
-
await writeFile(join(destDir, "subdir", "file2.txt"), "content2");
|
|
47
|
-
|
|
48
|
-
const files = await FileScanner.getFiles(destDir);
|
|
49
|
-
|
|
50
|
-
expect(files).toHaveLength(2);
|
|
51
|
-
expect(files).toContain("file1.txt");
|
|
52
|
-
expect(files).toContain("subdir/file2.txt");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("should handle empty directory", async () => {
|
|
56
|
-
const files = await FileScanner.getFiles(destDir);
|
|
57
|
-
expect(files).toEqual([]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("should handle deeply nested directories", async () => {
|
|
61
|
-
// Create deeply nested structure
|
|
62
|
-
const deepPath = join(destDir, "a", "b", "c", "d");
|
|
63
|
-
await mkdir(deepPath, { recursive: true });
|
|
64
|
-
await writeFile(join(deepPath, "deep.txt"), "deep content");
|
|
65
|
-
|
|
66
|
-
const files = await FileScanner.getFiles(destDir);
|
|
67
|
-
|
|
68
|
-
expect(files).toHaveLength(1);
|
|
69
|
-
expect(files).toContain("a/b/c/d/deep.txt");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("should return relative paths", async () => {
|
|
73
|
-
await mkdir(join(destDir, "subdir"), { recursive: true });
|
|
74
|
-
await writeFile(join(destDir, "subdir", "file.txt"), "content");
|
|
75
|
-
|
|
76
|
-
const files = await FileScanner.getFiles(destDir);
|
|
77
|
-
|
|
78
|
-
// Should return relative path, not absolute
|
|
79
|
-
expect(files[0]).toBe("subdir/file.txt");
|
|
80
|
-
expect(files[0]).not.toContain(destDir);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("findCustomFiles", () => {
|
|
85
|
-
test("should identify files in dest but not in source", async () => {
|
|
86
|
-
// Create .claude directories
|
|
87
|
-
const destClaudeDir = join(destDir, ".claude");
|
|
88
|
-
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
89
|
-
await mkdir(destClaudeDir, { recursive: true });
|
|
90
|
-
await mkdir(sourceClaudeDir, { recursive: true });
|
|
91
|
-
|
|
92
|
-
// Create files
|
|
93
|
-
await writeFile(join(destClaudeDir, "custom.md"), "custom content");
|
|
94
|
-
await writeFile(join(destClaudeDir, "standard.md"), "standard content");
|
|
95
|
-
await writeFile(join(sourceClaudeDir, "standard.md"), "standard content");
|
|
96
|
-
|
|
97
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
98
|
-
|
|
99
|
-
expect(customFiles).toHaveLength(1);
|
|
100
|
-
expect(customFiles).toContain(".claude/custom.md");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("should return empty array when no custom files exist", async () => {
|
|
104
|
-
// Create .claude directories
|
|
105
|
-
const destClaudeDir = join(destDir, ".claude");
|
|
106
|
-
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
107
|
-
await mkdir(destClaudeDir, { recursive: true });
|
|
108
|
-
await mkdir(sourceClaudeDir, { recursive: true });
|
|
109
|
-
|
|
110
|
-
// Create same files in both
|
|
111
|
-
await writeFile(join(destClaudeDir, "file1.md"), "content1");
|
|
112
|
-
await writeFile(join(sourceClaudeDir, "file1.md"), "content1");
|
|
113
|
-
|
|
114
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
115
|
-
|
|
116
|
-
expect(customFiles).toEqual([]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("should handle missing .claude in destination", async () => {
|
|
120
|
-
// Only create source .claude directory
|
|
121
|
-
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
122
|
-
await mkdir(sourceClaudeDir, { recursive: true });
|
|
123
|
-
await writeFile(join(sourceClaudeDir, "file1.md"), "content1");
|
|
124
|
-
|
|
125
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
126
|
-
|
|
127
|
-
expect(customFiles).toEqual([]);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("should handle missing .claude in source", async () => {
|
|
131
|
-
// Only create dest .claude directory
|
|
132
|
-
const destClaudeDir = join(destDir, ".claude");
|
|
133
|
-
await mkdir(destClaudeDir, { recursive: true });
|
|
134
|
-
await writeFile(join(destClaudeDir, "custom.md"), "custom content");
|
|
135
|
-
|
|
136
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
137
|
-
|
|
138
|
-
expect(customFiles).toHaveLength(1);
|
|
139
|
-
expect(customFiles).toContain(".claude/custom.md");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("should handle nested subdirectories", async () => {
|
|
143
|
-
// Create nested structure
|
|
144
|
-
const destNestedDir = join(destDir, ".claude", "commands");
|
|
145
|
-
const sourceNestedDir = join(sourceDir, ".claude", "commands");
|
|
146
|
-
await mkdir(destNestedDir, { recursive: true });
|
|
147
|
-
await mkdir(sourceNestedDir, { recursive: true });
|
|
148
|
-
|
|
149
|
-
// Create custom file in nested dir
|
|
150
|
-
await writeFile(join(destNestedDir, "custom-cmd.md"), "custom command");
|
|
151
|
-
await writeFile(join(destNestedDir, "standard-cmd.md"), "standard command");
|
|
152
|
-
await writeFile(join(sourceNestedDir, "standard-cmd.md"), "standard command");
|
|
153
|
-
|
|
154
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
155
|
-
|
|
156
|
-
expect(customFiles).toHaveLength(1);
|
|
157
|
-
expect(customFiles).toContain(".claude/commands/custom-cmd.md");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("should handle multiple custom files", async () => {
|
|
161
|
-
// Create .claude directories
|
|
162
|
-
const destClaudeDir = join(destDir, ".claude");
|
|
163
|
-
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
164
|
-
await mkdir(destClaudeDir, { recursive: true });
|
|
165
|
-
await mkdir(sourceClaudeDir, { recursive: true });
|
|
166
|
-
|
|
167
|
-
// Create multiple custom files
|
|
168
|
-
await writeFile(join(destClaudeDir, "custom1.md"), "custom1");
|
|
169
|
-
await writeFile(join(destClaudeDir, "custom2.md"), "custom2");
|
|
170
|
-
await writeFile(join(destClaudeDir, "custom3.md"), "custom3");
|
|
171
|
-
await writeFile(join(destClaudeDir, "standard.md"), "standard");
|
|
172
|
-
await writeFile(join(sourceClaudeDir, "standard.md"), "standard");
|
|
173
|
-
|
|
174
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
175
|
-
|
|
176
|
-
expect(customFiles).toHaveLength(3);
|
|
177
|
-
expect(customFiles).toContain(".claude/custom1.md");
|
|
178
|
-
expect(customFiles).toContain(".claude/custom2.md");
|
|
179
|
-
expect(customFiles).toContain(".claude/custom3.md");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("should handle files with special characters in names", async () => {
|
|
183
|
-
// Create .claude directories
|
|
184
|
-
const destClaudeDir = join(destDir, ".claude");
|
|
185
|
-
const sourceClaudeDir = join(sourceDir, ".claude");
|
|
186
|
-
await mkdir(destClaudeDir, { recursive: true });
|
|
187
|
-
await mkdir(sourceClaudeDir, { recursive: true });
|
|
188
|
-
|
|
189
|
-
// Create files with special characters
|
|
190
|
-
await writeFile(join(destClaudeDir, "file-with-dash.md"), "content");
|
|
191
|
-
await writeFile(join(destClaudeDir, "file_with_underscore.md"), "content");
|
|
192
|
-
await writeFile(join(destClaudeDir, "file.multiple.dots.md"), "content");
|
|
193
|
-
|
|
194
|
-
const customFiles = await FileScanner.findCustomFiles(destDir, sourceDir, ".claude");
|
|
195
|
-
|
|
196
|
-
expect(customFiles).toHaveLength(3);
|
|
197
|
-
expect(customFiles).toContain(".claude/file-with-dash.md");
|
|
198
|
-
expect(customFiles).toContain(".claude/file_with_underscore.md");
|
|
199
|
-
expect(customFiles).toContain(".claude/file.multiple.dots.md");
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
import { logger } from "../../src/utils/logger.js";
|
|
3
|
-
|
|
4
|
-
describe("Logger Utilities", () => {
|
|
5
|
-
let consoleLogSpy: any;
|
|
6
|
-
let consoleErrorSpy: any;
|
|
7
|
-
const originalDebug = process.env.DEBUG;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
// Reset logger state
|
|
11
|
-
logger.setVerbose(false);
|
|
12
|
-
logger.setLogFile(undefined);
|
|
13
|
-
|
|
14
|
-
consoleLogSpy = mock(() => {});
|
|
15
|
-
consoleErrorSpy = mock(() => {});
|
|
16
|
-
console.log = consoleLogSpy;
|
|
17
|
-
console.error = consoleErrorSpy;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
process.env.DEBUG = originalDebug;
|
|
22
|
-
consoleLogSpy.mockRestore?.();
|
|
23
|
-
consoleErrorSpy.mockRestore?.();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("info", () => {
|
|
27
|
-
test("should log info messages", () => {
|
|
28
|
-
logger.info("Test info message");
|
|
29
|
-
expect(consoleLogSpy).toHaveBeenCalled();
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("success", () => {
|
|
34
|
-
test("should log success messages", () => {
|
|
35
|
-
logger.success("Test success message");
|
|
36
|
-
expect(consoleLogSpy).toHaveBeenCalled();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe("warning", () => {
|
|
41
|
-
test("should log warning messages", () => {
|
|
42
|
-
logger.warning("Test warning message");
|
|
43
|
-
expect(consoleLogSpy).toHaveBeenCalled();
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("error", () => {
|
|
48
|
-
test("should log error messages", () => {
|
|
49
|
-
logger.error("Test error message");
|
|
50
|
-
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("debug", () => {
|
|
55
|
-
test("should log debug messages when DEBUG is set", () => {
|
|
56
|
-
process.env.DEBUG = "true";
|
|
57
|
-
logger.debug("Test debug message");
|
|
58
|
-
expect(consoleLogSpy).toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("should not log debug messages when DEBUG is not set", () => {
|
|
62
|
-
process.env.DEBUG = undefined;
|
|
63
|
-
logger.debug("Test debug message");
|
|
64
|
-
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("sanitize", () => {
|
|
69
|
-
test("should sanitize ghp_ tokens (36 chars)", () => {
|
|
70
|
-
const text = "Token: ghp_123456789012345678901234567890123456";
|
|
71
|
-
const sanitized = logger.sanitize(text);
|
|
72
|
-
expect(sanitized).toBe("Token: ghp_***");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("should sanitize github_pat_ tokens (82 chars)", () => {
|
|
76
|
-
// github_pat_ prefix + 82 alphanumeric/underscore characters (exact length)
|
|
77
|
-
const token =
|
|
78
|
-
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012";
|
|
79
|
-
const text = `Token: github_pat_${token}`;
|
|
80
|
-
const sanitized = logger.sanitize(text);
|
|
81
|
-
expect(sanitized).toBe("Token: github_pat_***");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("should sanitize gho_ tokens (36 chars)", () => {
|
|
85
|
-
const text = "Token: gho_123456789012345678901234567890123456";
|
|
86
|
-
const sanitized = logger.sanitize(text);
|
|
87
|
-
expect(sanitized).toBe("Token: gho_***");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("should sanitize ghu_ tokens (36 chars)", () => {
|
|
91
|
-
const text = "Token: ghu_123456789012345678901234567890123456";
|
|
92
|
-
const sanitized = logger.sanitize(text);
|
|
93
|
-
expect(sanitized).toBe("Token: ghu_***");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("should sanitize ghs_ tokens (36 chars)", () => {
|
|
97
|
-
const text = "Token: ghs_123456789012345678901234567890123456";
|
|
98
|
-
const sanitized = logger.sanitize(text);
|
|
99
|
-
expect(sanitized).toBe("Token: ghs_***");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("should sanitize ghr_ tokens (36 chars)", () => {
|
|
103
|
-
const text = "Token: ghr_123456789012345678901234567890123456";
|
|
104
|
-
const sanitized = logger.sanitize(text);
|
|
105
|
-
expect(sanitized).toBe("Token: ghr_***");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("should sanitize Bearer tokens", () => {
|
|
109
|
-
const text = "Authorization: Bearer abc123xyz-token_value";
|
|
110
|
-
const sanitized = logger.sanitize(text);
|
|
111
|
-
expect(sanitized).toBe("Authorization: Bearer ***");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("should sanitize query string tokens", () => {
|
|
115
|
-
const text = "https://api.example.com?token=secret123";
|
|
116
|
-
const sanitized = logger.sanitize(text);
|
|
117
|
-
expect(sanitized).toBe("https://api.example.com?token=***");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("should sanitize multiple tokens", () => {
|
|
121
|
-
const ghpToken = "123456789012345678901234567890123456";
|
|
122
|
-
const patToken =
|
|
123
|
-
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012";
|
|
124
|
-
const text = `Tokens: ghp_${ghpToken} and github_pat_${patToken}`;
|
|
125
|
-
const sanitized = logger.sanitize(text);
|
|
126
|
-
expect(sanitized).toBe("Tokens: ghp_*** and github_pat_***");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("should not modify text without tokens", () => {
|
|
130
|
-
const text = "No tokens here, just regular text";
|
|
131
|
-
const sanitized = logger.sanitize(text);
|
|
132
|
-
expect(sanitized).toBe(text);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("should handle empty string", () => {
|
|
136
|
-
const sanitized = logger.sanitize("");
|
|
137
|
-
expect(sanitized).toBe("");
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("verbose", () => {
|
|
142
|
-
beforeEach(() => {
|
|
143
|
-
logger.setVerbose(false);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("should not log when verbose is disabled", () => {
|
|
147
|
-
logger.verbose("Test verbose message");
|
|
148
|
-
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("should log to stderr when verbose is enabled", () => {
|
|
152
|
-
logger.setVerbose(true);
|
|
153
|
-
logger.verbose("Test verbose message");
|
|
154
|
-
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
155
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
156
|
-
expect(call).toContain("[VERBOSE]");
|
|
157
|
-
expect(call).toContain("Test verbose message");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("should include timestamp in verbose logs", () => {
|
|
161
|
-
logger.setVerbose(true);
|
|
162
|
-
logger.verbose("Test message");
|
|
163
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
164
|
-
expect(call).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("should sanitize sensitive data in verbose logs", () => {
|
|
168
|
-
logger.setVerbose(true);
|
|
169
|
-
logger.verbose("Token: ghp_123456789012345678901234567890123456");
|
|
170
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
171
|
-
expect(call).toContain("ghp_***");
|
|
172
|
-
expect(call).not.toContain("ghp_123456789012345678901234567890123456");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("should include context in verbose logs", () => {
|
|
176
|
-
logger.setVerbose(true);
|
|
177
|
-
logger.verbose("Test message", { key: "value", num: 123 });
|
|
178
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
179
|
-
expect(call).toContain('"key": "value"');
|
|
180
|
-
expect(call).toContain('"num": 123');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("should sanitize context strings", () => {
|
|
184
|
-
logger.setVerbose(true);
|
|
185
|
-
logger.verbose("Test message", {
|
|
186
|
-
token: "ghp_123456789012345678901234567890123456",
|
|
187
|
-
});
|
|
188
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
189
|
-
expect(call).toContain("ghp_***");
|
|
190
|
-
expect(call).not.toContain("ghp_123456789012345678901234567890123456");
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("should handle nested objects in context", () => {
|
|
194
|
-
logger.setVerbose(true);
|
|
195
|
-
logger.verbose("Test message", {
|
|
196
|
-
nested: { key: "value" },
|
|
197
|
-
});
|
|
198
|
-
const call = consoleErrorSpy.mock.calls[1][0];
|
|
199
|
-
expect(call).toContain('"nested"');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe("setVerbose", () => {
|
|
204
|
-
test("should enable verbose logging", () => {
|
|
205
|
-
logger.setVerbose(true);
|
|
206
|
-
expect(logger.isVerbose()).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("should disable verbose logging", () => {
|
|
210
|
-
logger.setVerbose(true);
|
|
211
|
-
logger.setVerbose(false);
|
|
212
|
-
expect(logger.isVerbose()).toBe(false);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("should log when enabling verbose", () => {
|
|
216
|
-
logger.setVerbose(true);
|
|
217
|
-
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
218
|
-
const call = consoleErrorSpy.mock.calls[0][0];
|
|
219
|
-
expect(call).toContain("Verbose logging enabled");
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe("isVerbose", () => {
|
|
224
|
-
test("should return false by default", () => {
|
|
225
|
-
expect(logger.isVerbose()).toBe(false);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("should return true when enabled", () => {
|
|
229
|
-
logger.setVerbose(true);
|
|
230
|
-
expect(logger.isVerbose()).toBe(true);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("should return false when disabled", () => {
|
|
234
|
-
logger.setVerbose(true);
|
|
235
|
-
logger.setVerbose(false);
|
|
236
|
-
expect(logger.isVerbose()).toBe(false);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
});
|