claudekit-cli 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +3 -3
- package/CHANGELOG.md +16 -0
- package/biome.json +1 -1
- package/bun.lock +44 -429
- package/dist/index.js +87 -39
- package/package.json +9 -10
- package/src/commands/new.ts +27 -7
- package/src/index.ts +5 -9
- package/src/lib/download.ts +93 -12
- package/src/types.ts +1 -0
- package/src/version.json +3 -0
- package/test-integration/demo/.mcp.json +13 -0
- package/test-integration/demo/.repomixignore +15 -0
- package/test-integration/demo/CLAUDE.md +34 -0
- package/tests/integration/cli.test.ts +252 -0
- package/tests/lib/download.test.ts +230 -8
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { rm } from "node:fs/promises";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { DownloadManager } from "../../src/lib/download.js";
|
|
5
6
|
import { DownloadError, ExtractionError } from "../../src/types.js";
|
|
6
7
|
|
|
7
8
|
describe("DownloadManager", () => {
|
|
8
9
|
let manager: DownloadManager;
|
|
10
|
+
let testDir: string;
|
|
9
11
|
|
|
10
|
-
beforeEach(() => {
|
|
12
|
+
beforeEach(async () => {
|
|
11
13
|
manager = new DownloadManager();
|
|
14
|
+
testDir = join(process.cwd(), "test-temp", `test-${Date.now()}`);
|
|
15
|
+
await mkdir(testDir, { recursive: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (existsSync(testDir)) {
|
|
20
|
+
await rm(testDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
12
22
|
});
|
|
13
23
|
|
|
14
24
|
describe("constructor", () => {
|
|
@@ -46,6 +56,224 @@ describe("DownloadManager", () => {
|
|
|
46
56
|
});
|
|
47
57
|
});
|
|
48
58
|
|
|
59
|
+
describe("validateExtraction", () => {
|
|
60
|
+
test("should throw error for empty directory", async () => {
|
|
61
|
+
const emptyDir = join(testDir, "empty");
|
|
62
|
+
await mkdir(emptyDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
await expect(manager.validateExtraction(emptyDir)).rejects.toThrow(ExtractionError);
|
|
65
|
+
await expect(manager.validateExtraction(emptyDir)).rejects.toThrow(
|
|
66
|
+
"Extraction resulted in no files",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("should pass validation for directory with .claude and CLAUDE.md", async () => {
|
|
71
|
+
const validDir = join(testDir, "valid");
|
|
72
|
+
await mkdir(join(validDir, ".claude"), { recursive: true });
|
|
73
|
+
await writeFile(join(validDir, ".claude", "config.json"), "{}");
|
|
74
|
+
await writeFile(join(validDir, "CLAUDE.md"), "# Test");
|
|
75
|
+
|
|
76
|
+
// Should not throw
|
|
77
|
+
await manager.validateExtraction(validDir);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("should warn but not fail for directory with files but missing critical paths", async () => {
|
|
81
|
+
const partialDir = join(testDir, "partial");
|
|
82
|
+
await mkdir(partialDir, { recursive: true });
|
|
83
|
+
await writeFile(join(partialDir, "README.md"), "# Test");
|
|
84
|
+
|
|
85
|
+
// Should not throw but will log warnings
|
|
86
|
+
await manager.validateExtraction(partialDir);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("should throw error for non-existent directory", async () => {
|
|
90
|
+
const nonExistentDir = join(testDir, "does-not-exist");
|
|
91
|
+
|
|
92
|
+
await expect(manager.validateExtraction(nonExistentDir)).rejects.toThrow();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("wrapper directory detection", () => {
|
|
97
|
+
test("should detect version wrapper with v prefix", () => {
|
|
98
|
+
// Access private method via any type casting for testing
|
|
99
|
+
const isWrapper = (manager as any).isWrapperDirectory("project-v1.0.0");
|
|
100
|
+
expect(isWrapper).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should detect version wrapper without v prefix", () => {
|
|
104
|
+
const isWrapper = (manager as any).isWrapperDirectory("project-1.0.0");
|
|
105
|
+
expect(isWrapper).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("should detect commit hash wrapper", () => {
|
|
109
|
+
const isWrapper = (manager as any).isWrapperDirectory("project-abc1234");
|
|
110
|
+
expect(isWrapper).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("should detect prerelease version wrapper", () => {
|
|
114
|
+
const isWrapper = (manager as any).isWrapperDirectory("project-v1.0.0-alpha");
|
|
115
|
+
expect(isWrapper).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("should detect beta version wrapper", () => {
|
|
119
|
+
const isWrapper = (manager as any).isWrapperDirectory("project-v2.0.0-beta.1");
|
|
120
|
+
expect(isWrapper).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("should detect rc version wrapper", () => {
|
|
124
|
+
const isWrapper = (manager as any).isWrapperDirectory("repo-v3.0.0-rc.5");
|
|
125
|
+
expect(isWrapper).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("should not detect .claude as wrapper", () => {
|
|
129
|
+
const isWrapper = (manager as any).isWrapperDirectory(".claude");
|
|
130
|
+
expect(isWrapper).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("should not detect src as wrapper", () => {
|
|
134
|
+
const isWrapper = (manager as any).isWrapperDirectory("src");
|
|
135
|
+
expect(isWrapper).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("should not detect docs as wrapper", () => {
|
|
139
|
+
const isWrapper = (manager as any).isWrapperDirectory("docs");
|
|
140
|
+
expect(isWrapper).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should not detect node_modules as wrapper", () => {
|
|
144
|
+
const isWrapper = (manager as any).isWrapperDirectory("node_modules");
|
|
145
|
+
expect(isWrapper).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("path safety validation", () => {
|
|
150
|
+
test("should allow safe relative paths", () => {
|
|
151
|
+
const basePath = join(testDir, "base");
|
|
152
|
+
const targetPath = join(testDir, "base", "subdir", "file.txt");
|
|
153
|
+
const isSafe = (manager as any).isPathSafe(basePath, targetPath);
|
|
154
|
+
expect(isSafe).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("should block path traversal attempts with ..", () => {
|
|
158
|
+
const basePath = join(testDir, "base");
|
|
159
|
+
const targetPath = join(testDir, "outside", "file.txt");
|
|
160
|
+
const isSafe = (manager as any).isPathSafe(basePath, targetPath);
|
|
161
|
+
expect(isSafe).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("should block absolute path attempts", () => {
|
|
165
|
+
const basePath = join(testDir, "base");
|
|
166
|
+
const targetPath = "/etc/passwd";
|
|
167
|
+
const isSafe = (manager as any).isPathSafe(basePath, targetPath);
|
|
168
|
+
expect(isSafe).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("should allow same directory", () => {
|
|
172
|
+
const basePath = join(testDir, "base");
|
|
173
|
+
const targetPath = join(testDir, "base");
|
|
174
|
+
const isSafe = (manager as any).isPathSafe(basePath, targetPath);
|
|
175
|
+
expect(isSafe).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("archive bomb protection", () => {
|
|
180
|
+
test("should track extraction size", () => {
|
|
181
|
+
const manager = new DownloadManager();
|
|
182
|
+
|
|
183
|
+
// Add some file sizes
|
|
184
|
+
(manager as any).checkExtractionSize(100 * 1024 * 1024); // 100MB
|
|
185
|
+
expect((manager as any).totalExtractedSize).toBe(100 * 1024 * 1024);
|
|
186
|
+
|
|
187
|
+
(manager as any).checkExtractionSize(200 * 1024 * 1024); // 200MB more
|
|
188
|
+
expect((manager as any).totalExtractedSize).toBe(300 * 1024 * 1024);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("should throw error when size exceeds limit", () => {
|
|
192
|
+
const manager = new DownloadManager();
|
|
193
|
+
|
|
194
|
+
expect(() => {
|
|
195
|
+
(manager as any).checkExtractionSize(600 * 1024 * 1024); // 600MB
|
|
196
|
+
}).toThrow(ExtractionError);
|
|
197
|
+
|
|
198
|
+
expect(() => {
|
|
199
|
+
(manager as any).checkExtractionSize(600 * 1024 * 1024); // 600MB
|
|
200
|
+
}).toThrow("Archive exceeds maximum extraction size");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("should allow extraction within limit", () => {
|
|
204
|
+
const manager = new DownloadManager();
|
|
205
|
+
|
|
206
|
+
expect(() => {
|
|
207
|
+
(manager as any).checkExtractionSize(400 * 1024 * 1024); // 400MB
|
|
208
|
+
}).not.toThrow();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("should reset extraction size", () => {
|
|
212
|
+
const manager = new DownloadManager();
|
|
213
|
+
|
|
214
|
+
(manager as any).checkExtractionSize(300 * 1024 * 1024); // 300MB
|
|
215
|
+
expect((manager as any).totalExtractedSize).toBe(300 * 1024 * 1024);
|
|
216
|
+
|
|
217
|
+
(manager as any).resetExtractionSize();
|
|
218
|
+
expect((manager as any).totalExtractedSize).toBe(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("file exclusion", () => {
|
|
223
|
+
test("should exclude .git directory", () => {
|
|
224
|
+
const shouldExclude = (manager as any).shouldExclude(".git");
|
|
225
|
+
expect(shouldExclude).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("should exclude .git/** files", () => {
|
|
229
|
+
const shouldExclude = (manager as any).shouldExclude(".git/config");
|
|
230
|
+
expect(shouldExclude).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("should exclude node_modules", () => {
|
|
234
|
+
const shouldExclude = (manager as any).shouldExclude("node_modules");
|
|
235
|
+
expect(shouldExclude).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("should exclude .DS_Store", () => {
|
|
239
|
+
const shouldExclude = (manager as any).shouldExclude(".DS_Store");
|
|
240
|
+
expect(shouldExclude).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("should not exclude normal files", () => {
|
|
244
|
+
const shouldExclude = (manager as any).shouldExclude("src/index.ts");
|
|
245
|
+
expect(shouldExclude).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("should not exclude .claude directory", () => {
|
|
249
|
+
const shouldExclude = (manager as any).shouldExclude(".claude");
|
|
250
|
+
expect(shouldExclude).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("archive type detection", () => {
|
|
255
|
+
test("should detect .tar.gz archive", () => {
|
|
256
|
+
const type = (manager as any).detectArchiveType("project-v1.0.0.tar.gz");
|
|
257
|
+
expect(type).toBe("tar.gz");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should detect .tgz archive", () => {
|
|
261
|
+
const type = (manager as any).detectArchiveType("project-v1.0.0.tgz");
|
|
262
|
+
expect(type).toBe("tar.gz");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("should detect .zip archive", () => {
|
|
266
|
+
const type = (manager as any).detectArchiveType("project-v1.0.0.zip");
|
|
267
|
+
expect(type).toBe("zip");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("should throw error for unknown archive type", () => {
|
|
271
|
+
expect(() => {
|
|
272
|
+
(manager as any).detectArchiveType("project-v1.0.0.rar");
|
|
273
|
+
}).toThrow(ExtractionError);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
49
277
|
describe("error classes", () => {
|
|
50
278
|
test("DownloadError should store message", () => {
|
|
51
279
|
const error = new DownloadError("Download failed");
|
|
@@ -61,10 +289,4 @@ describe("DownloadManager", () => {
|
|
|
61
289
|
expect(error.name).toBe("ExtractionError");
|
|
62
290
|
});
|
|
63
291
|
});
|
|
64
|
-
|
|
65
|
-
// Note: Testing actual download and extraction would require:
|
|
66
|
-
// 1. Mock GitHub API responses
|
|
67
|
-
// 2. Test fixture archives
|
|
68
|
-
// 3. Network mocking
|
|
69
|
-
// These are integration tests that would be better suited for e2e testing
|
|
70
292
|
});
|