@involvex/ext-cli 1.0.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/.github/FUNDING.yml +9 -0
- package/.superpowers/sdd/progress.md +15 -0
- package/.superpowers/sdd/task-1-brief.md +89 -0
- package/.superpowers/sdd/task-1-report.md +52 -0
- package/.superpowers/sdd/task-2-brief.md +113 -0
- package/.superpowers/sdd/task-2-report.md +27 -0
- package/.superpowers/sdd/task-3-brief.md +123 -0
- package/.superpowers/sdd/task-3-report.md +26 -0
- package/.superpowers/sdd/task-4-brief.md +85 -0
- package/.superpowers/sdd/task-4-report.md +33 -0
- package/.superpowers/sdd/task-5-brief.md +122 -0
- package/.superpowers/sdd/task-5-report.md +22 -0
- package/.superpowers/sdd/task-6-brief.md +96 -0
- package/.superpowers/sdd/task-6-report.md +24 -0
- package/.superpowers/sdd/task-7-brief.md +100 -0
- package/.superpowers/sdd/task-7-report.md +30 -0
- package/AGENTS.md +264 -0
- package/README.md +177 -0
- package/biome.json +25 -0
- package/docs/superpowers/plans/2026-07-05-ext-cli.md +1115 -0
- package/package.json +34 -0
- package/src/cli.ts +209 -0
- package/src/crx-parser.ts +35 -0
- package/src/downloader.ts +61 -0
- package/src/extractor.ts +34 -0
- package/src/index.ts +16 -0
- package/src/launcher.ts +107 -0
- package/src/packer.ts +127 -0
- package/src/search.ts +115 -0
- package/src/url-parser.ts +33 -0
- package/test/crx-parser.test.ts +48 -0
- package/test/extractor.test.ts +49 -0
- package/test/packer.test.ts +44 -0
- package/test/url-parser.test.ts +44 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Task 5: Extractor Module
|
|
2
|
+
|
|
3
|
+
**Files:**
|
|
4
|
+
- Create: `src/extractor.ts`
|
|
5
|
+
- Create: `test/extractor.test.ts`
|
|
6
|
+
|
|
7
|
+
**Interfaces:**
|
|
8
|
+
- Consumes: `extractZipFromCrx` from `src/crx-parser.ts`
|
|
9
|
+
- Produces: `extractCrx(crxPath: string, outputDir?: string): Promise<string>` — returns path to extracted directory
|
|
10
|
+
|
|
11
|
+
- [ ] **Step 1: Write failing tests**
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// test/extractor.test.ts
|
|
15
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
16
|
+
import { extractCrx } from "../src/extractor";
|
|
17
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
|
|
20
|
+
const TEST_DIR = join(import.meta.dir, "__test_extract__");
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("extractCrx extracts a CRX file to directory", async () => {
|
|
31
|
+
const zipPath = join(TEST_DIR, "test.zip");
|
|
32
|
+
|
|
33
|
+
const extDir = join(TEST_DIR, "ext_src");
|
|
34
|
+
mkdirSync(extDir, { recursive: true });
|
|
35
|
+
writeFileSync(join(extDir, "manifest.json"), JSON.stringify({ name: "test", version: "1.0" }));
|
|
36
|
+
|
|
37
|
+
const proc = Bun.spawn(["powershell", "-Command", `Compress-Archive -Path '${extDir}\\*' -DestinationPath '${zipPath}' -Force`]);
|
|
38
|
+
await proc.exited;
|
|
39
|
+
|
|
40
|
+
const zipBuf = Bun.file(zipPath);
|
|
41
|
+
const zipBytes = Buffer.from(await zipBuf.arrayBuffer());
|
|
42
|
+
|
|
43
|
+
const crxHeader = Buffer.alloc(12);
|
|
44
|
+
crxHeader.write("Cr24", 0);
|
|
45
|
+
crxHeader.writeUInt32LE(3, 4);
|
|
46
|
+
crxHeader.writeUInt32LE(4, 8);
|
|
47
|
+
|
|
48
|
+
const dummyProto = Buffer.alloc(4);
|
|
49
|
+
const crxBuffer = Buffer.concat([crxHeader, dummyProto, zipBytes]);
|
|
50
|
+
|
|
51
|
+
const crxPath = join(TEST_DIR, "test.crx");
|
|
52
|
+
writeFileSync(crxPath, crxBuffer);
|
|
53
|
+
|
|
54
|
+
const outputDir = join(TEST_DIR, "extracted");
|
|
55
|
+
const result = await extractCrx(crxPath, outputDir);
|
|
56
|
+
|
|
57
|
+
expect(existsSync(join(result, "manifest.json"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun test test/extractor.test.ts
|
|
65
|
+
```
|
|
66
|
+
Expected: FAIL — `extractCrx` not found
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 3: Implement extractor.ts**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/extractor.ts
|
|
72
|
+
import { readFile, mkdir, writeFile } from "fs/promises";
|
|
73
|
+
import { join, basename } from "path";
|
|
74
|
+
import { extractZipFromCrx } from "./crx-parser";
|
|
75
|
+
|
|
76
|
+
export async function extractCrx(
|
|
77
|
+
crxPath: string,
|
|
78
|
+
outputDir?: string
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
const crxBuffer = await readFile(crxPath);
|
|
81
|
+
const zipBuffer = extractZipFromCrx(crxBuffer);
|
|
82
|
+
|
|
83
|
+
const baseName = basename(crxPath, ".crx");
|
|
84
|
+
const destDir = outputDir ?? join(".", baseName);
|
|
85
|
+
await mkdir(destDir, { recursive: true });
|
|
86
|
+
|
|
87
|
+
const tempZip = join(destDir, "__temp__.zip");
|
|
88
|
+
await writeFile(tempZip, zipBuffer);
|
|
89
|
+
|
|
90
|
+
const proc = Bun.spawn([
|
|
91
|
+
"powershell",
|
|
92
|
+
"-Command",
|
|
93
|
+
`Expand-Archive -Path '${tempZip}' -DestinationPath '${destDir}' -Force`,
|
|
94
|
+
]);
|
|
95
|
+
const exitCode = await proc.exited;
|
|
96
|
+
|
|
97
|
+
if (exitCode !== 0) {
|
|
98
|
+
const stderr = await new Response(proc.stderr).text();
|
|
99
|
+
throw new Error(`Failed to extract ZIP: ${stderr}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { unlink } = await import("fs/promises");
|
|
103
|
+
await unlink(tempZip);
|
|
104
|
+
|
|
105
|
+
console.log(`Extracted to: ${destDir}`);
|
|
106
|
+
return destDir;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
bun test test/extractor.test.ts
|
|
114
|
+
```
|
|
115
|
+
Expected: PASS
|
|
116
|
+
|
|
117
|
+
- [ ] **Step 5: Commit**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git add src/extractor.ts test/extractor.test.ts
|
|
121
|
+
git commit -m "feat: add CRX extractor that unzips CRX files to directories"
|
|
122
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Task 5: Extractor Module — Report
|
|
2
|
+
|
|
3
|
+
## Files Created
|
|
4
|
+
- `src/extractor.ts` — CRX extractor using `extractZipFromCrx` from crx-parser
|
|
5
|
+
- `test/extractor.test.ts` — Test that builds a CRX, extracts it, and verifies manifest.json exists
|
|
6
|
+
|
|
7
|
+
## Implementation Notes
|
|
8
|
+
- Uses `Bun.spawn` with PowerShell `Expand-Archive` to unzip the extracted ZIP buffer
|
|
9
|
+
- Creates a temp zip in the destination dir, extracts, then cleans up the temp file
|
|
10
|
+
- Default output dir falls back to `./<crx-basename>` if no `outputDir` provided
|
|
11
|
+
- The test requires `--timeout 30000` to account for PowerShell startup overhead on Windows
|
|
12
|
+
|
|
13
|
+
## Test Results
|
|
14
|
+
```
|
|
15
|
+
12 pass, 0 fail across 4 test files
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commit
|
|
19
|
+
- `7c57087` — feat: add CRX extractor that unzips CRX files to directories
|
|
20
|
+
|
|
21
|
+
## Concerns
|
|
22
|
+
- The default bun test timeout (5s) is insufficient for PowerShell processes on Windows; the `bun.lock` changed (likely from the test run). Tests must be run with `--timeout 30000`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Task 6: Packer Module
|
|
2
|
+
|
|
3
|
+
**Files:**
|
|
4
|
+
- Create: `src/packer.ts`
|
|
5
|
+
- Create: `test/packer.test.ts`
|
|
6
|
+
|
|
7
|
+
**Interfaces:**
|
|
8
|
+
- Consumes: none
|
|
9
|
+
- Produces: `packCrx(dirPath: string, outputPath?: string): Promise<string>` — returns path to packed .crx file
|
|
10
|
+
|
|
11
|
+
- [ ] **Step 1: Write failing tests**
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// test/packer.test.ts
|
|
15
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
16
|
+
import { packCrx } from "../src/packer";
|
|
17
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
|
|
20
|
+
const TEST_DIR = join(import.meta.dir, "__test_pack__");
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("packCrx creates a CRX file from directory", async () => {
|
|
31
|
+
const extDir = join(TEST_DIR, "my-ext");
|
|
32
|
+
mkdirSync(extDir, { recursive: true });
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(extDir, "manifest.json"),
|
|
35
|
+
JSON.stringify({ manifest_version: 3, name: "Test Ext", version: "1.0" })
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const crxPath = join(TEST_DIR, "output.crx");
|
|
39
|
+
const result = await packCrx(extDir, crxPath);
|
|
40
|
+
|
|
41
|
+
expect(existsSync(result)).toBe(true);
|
|
42
|
+
|
|
43
|
+
const buf = readFileSync(result);
|
|
44
|
+
expect(buf.toString("ascii", 0, 4)).toBe("Cr24");
|
|
45
|
+
expect(buf.readUInt32LE(4)).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("packCrx generates .pem key if none exists", async () => {
|
|
49
|
+
const extDir = join(TEST_DIR, "my-ext2");
|
|
50
|
+
mkdirSync(extDir, { recursive: true });
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(extDir, "manifest.json"),
|
|
53
|
+
JSON.stringify({ manifest_version: 3, name: "Test", version: "1.0" })
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await packCrx(extDir, join(TEST_DIR, "out.crx"));
|
|
57
|
+
expect(existsSync(join(extDir, "key.pem"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun test test/packer.test.ts
|
|
65
|
+
```
|
|
66
|
+
Expected: FAIL — `packCrx` not found
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 3: Implement packer.ts**
|
|
69
|
+
|
|
70
|
+
The packer must:
|
|
71
|
+
1. Read manifest.json from the directory to validate it's an extension
|
|
72
|
+
2. Load existing key.pem or generate a new 2048-bit RSA key using node-forge
|
|
73
|
+
3. Create a ZIP archive from the directory contents (use PowerShell Compress-Archive on Windows)
|
|
74
|
+
4. Build a CRX3 header with the public key and a SHA256 signature
|
|
75
|
+
5. Assemble: magic "Cr24" + version(3) + headerLen + header + zipData
|
|
76
|
+
6. Write to output path and return it
|
|
77
|
+
|
|
78
|
+
Key implementation details:
|
|
79
|
+
- Use `node-forge` for RSA key generation and signing
|
|
80
|
+
- CRX3 protobuf header format: field 2 (sha256_with_rsa) contains AsymmetricKeyProof with field 1 (public_key DER) and field 2 (signature)
|
|
81
|
+
- Extension ID is computed as first 16 bytes of SHA256(public_key DER), base-16 encoded with alphabet "abcdefghijklmnop"
|
|
82
|
+
- ZIP creation: use `Bun.spawn` with PowerShell `Compress-Archive` on Windows
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bun test test/packer.test.ts
|
|
88
|
+
```
|
|
89
|
+
Expected: PASS
|
|
90
|
+
|
|
91
|
+
- [ ] **Step 5: Commit**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git add src/packer.ts test/packer.test.ts
|
|
95
|
+
git commit -m "feat: add CRX3 packer with auto key generation"
|
|
96
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Task 6: Packer Module — Report
|
|
2
|
+
|
|
3
|
+
## Status: DONE
|
|
4
|
+
|
|
5
|
+
## Commits
|
|
6
|
+
- `8d6308d` — feat: add CRX3 packer with auto key generation
|
|
7
|
+
|
|
8
|
+
## Test Summary
|
|
9
|
+
2/2 tests pass. Tests verify CRX file creation with correct "Cr24" magic and version 3, and that key.pem is auto-generated when missing.
|
|
10
|
+
|
|
11
|
+
## Implementation Summary
|
|
12
|
+
|
|
13
|
+
Created `src/packer.ts` implementing `packCrx(dirPath, outputPath?)`:
|
|
14
|
+
1. Validates manifest.json exists in the extension directory
|
|
15
|
+
2. Loads existing key.pem or generates new 2048-bit RSA key pair via node-forge
|
|
16
|
+
3. Extracts public key DER from private key for extension ID computation and CRX header
|
|
17
|
+
4. Computes extension ID: SHA256(publicKey DER), first 16 bytes, alphabet "abcdefghijklmnop"
|
|
18
|
+
5. Creates ZIP archive via PowerShell `Compress-Archive` (Windows)
|
|
19
|
+
6. Signs: SHA256(crxId + zipData) with RSA private key
|
|
20
|
+
7. Builds CRX3 protobuf header manually using varint encoding
|
|
21
|
+
8. Assembles final CRX: "Cr24" + version(3) + headerLen + header + zipData
|
|
22
|
+
|
|
23
|
+
## Concerns
|
|
24
|
+
None — tests pass cleanly, implementation is self-contained with no external dependencies beyond node-forge and child_process.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Task 7: CLI Entry Point
|
|
2
|
+
|
|
3
|
+
**Files:**
|
|
4
|
+
- Modify: `src/cli.ts`
|
|
5
|
+
|
|
6
|
+
**Interfaces:**
|
|
7
|
+
- Consumes: `downloadExtension` from `src/downloader.ts`, `extractCrx` from `src/extractor.ts`, `packCrx` from `src/packer.ts`
|
|
8
|
+
- Produces: working CLI binary with get, extract, and pack commands
|
|
9
|
+
|
|
10
|
+
- [ ] **Step 1: Implement full CLI**
|
|
11
|
+
|
|
12
|
+
Replace the contents of `src/cli.ts` with:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
#!/usr/bin/env bun
|
|
16
|
+
|
|
17
|
+
import { Command } from "commander";
|
|
18
|
+
import { downloadExtension } from "./downloader";
|
|
19
|
+
import { extractCrx } from "./extractor";
|
|
20
|
+
import { packCrx } from "./packer";
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name("ext-cli")
|
|
26
|
+
.description("Chrome/Edge Extension Manager - download, extract, and pack extensions")
|
|
27
|
+
.version("1.0.0");
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command("get")
|
|
31
|
+
.description("Download an extension from Chrome Web Store or Edge Add-ons")
|
|
32
|
+
.argument("<url-or-id>", "Store URL or 32-char extension ID")
|
|
33
|
+
.option("-d, --dir <directory>", "Output directory", ".")
|
|
34
|
+
.option("-e, --extract", "Extract the CRX after downloading")
|
|
35
|
+
.action(async (urlOrId: string, opts: { dir: string; extract: boolean }) => {
|
|
36
|
+
try {
|
|
37
|
+
const crxPath = await downloadExtension(urlOrId, opts.dir);
|
|
38
|
+
|
|
39
|
+
if (opts.extract) {
|
|
40
|
+
const extDir = crxPath.replace(/\.crx$/, "");
|
|
41
|
+
await extractCrx(crxPath, extDir);
|
|
42
|
+
console.log(`\nDone! Extension extracted to: ${extDir}`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`\nDone! CRX saved to: ${crxPath}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command("extract")
|
|
54
|
+
.description("Extract a CRX file to a directory")
|
|
55
|
+
.argument("<crx-file>", "Path to .crx file")
|
|
56
|
+
.option("-d, --dir <directory>", "Output directory")
|
|
57
|
+
.action(async (crxFile: string, opts: { dir?: string }) => {
|
|
58
|
+
try {
|
|
59
|
+
const outputDir = opts.dir ?? crxFile.replace(/\.crx$/, "");
|
|
60
|
+
await extractCrx(crxFile, outputDir);
|
|
61
|
+
console.log(`\nDone! Extracted to: ${outputDir}`);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command("pack")
|
|
70
|
+
.description("Pack a directory into a CRX file")
|
|
71
|
+
.argument("<directory>", "Extension directory containing manifest.json")
|
|
72
|
+
.option("-o, --output <path>", "Output CRX file path")
|
|
73
|
+
.action(async (directory: string, opts: { output?: string }) => {
|
|
74
|
+
try {
|
|
75
|
+
const crxPath = await packCrx(directory, opts.output);
|
|
76
|
+
console.log(`\nDone! Packed to: ${crxPath}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Error: ${error instanceof Error ? error.message : error}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program.parse();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 2: Test CLI manually**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bun run src/cli.ts --help
|
|
90
|
+
bun run src/cli.ts get --help
|
|
91
|
+
bun run src/cli.ts extract --help
|
|
92
|
+
bun run src/cli.ts pack --help
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- [ ] **Step 3: Commit**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git add src/cli.ts
|
|
99
|
+
git commit -m "feat: implement ext-cli with get, extract, and pack commands"
|
|
100
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Task 7: CLI Entry Point — Report
|
|
2
|
+
|
|
3
|
+
## What was done
|
|
4
|
+
|
|
5
|
+
Replaced the placeholder `src/cli.ts` with the full commander-based CLI implementation.
|
|
6
|
+
|
|
7
|
+
## Commands implemented
|
|
8
|
+
|
|
9
|
+
| Command | Description | Key options |
|
|
10
|
+
|---------|-------------|-------------|
|
|
11
|
+
| `get <url-or-id>` | Download extension from Chrome Web Store or Edge Add-ons | `-d/--dir`, `-e/--extract` |
|
|
12
|
+
| `extract <crx-file>` | Extract a CRX file to a directory | `-d/--dir` |
|
|
13
|
+
| `pack <directory>` | Pack a directory into a CRX file | `-o/--output` |
|
|
14
|
+
|
|
15
|
+
## Test results
|
|
16
|
+
|
|
17
|
+
- `bun run src/cli.ts --help` — Shows all 3 commands correctly
|
|
18
|
+
- `bun run src/cli.ts get --help` — Shows argument and options
|
|
19
|
+
- `bun run src/cli.ts extract --help` — Shows argument and options
|
|
20
|
+
- `bun run src/cli.ts pack --help` — Shows argument and options
|
|
21
|
+
|
|
22
|
+
All four help commands passed.
|
|
23
|
+
|
|
24
|
+
## Commit
|
|
25
|
+
|
|
26
|
+
- `aaaf063` — `feat: implement ext-cli with get, extract, and pack commands`
|
|
27
|
+
|
|
28
|
+
## Concerns
|
|
29
|
+
|
|
30
|
+
None. The implementation matches the brief exactly.
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Agents.md — chrome-ext-manager (ext-cli)
|
|
2
|
+
|
|
3
|
+
This document provides instructions for AI agents working on this codebase.
|
|
4
|
+
It covers commands, technologies, project structure, and best practices.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Project Overview
|
|
9
|
+
|
|
10
|
+
`ext-cli` is a CLI tool for downloading, extracting, packing, and searching
|
|
11
|
+
Chrome/Edge browser extensions. It parses CRX v2/v3 files, communicates with
|
|
12
|
+
the Chrome Web Store and Edge Add-ons APIs, and can build standalone binaries.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Technologies
|
|
17
|
+
|
|
18
|
+
| Category | Technology | Notes |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Runtime | **Bun** >= 1.3.0 | Required — do not use Node.js, npm, yarn, or pnpm |
|
|
21
|
+
| Language | **TypeScript** (strict mode) | ESNext target, bundler module resolution |
|
|
22
|
+
| CLI Framework | **commander** ^15.0.0 | Argument parsing and help generation |
|
|
23
|
+
| Crypto / Keys | **node-forge** ^1.4.0 | RSA key generation and CRX signing |
|
|
24
|
+
| Browser Launcher | **chromium-edge-launcher** ^2.0.1 | Launch Edge with remote debugging |
|
|
25
|
+
| Linter / Formatter | **Biome** ^2.5.2 | Replaces ESLint + Prettier |
|
|
26
|
+
| Testing | **bun:test** | Built-in test runner with `describe`, `test`, `expect` |
|
|
27
|
+
| Shell | **PowerShell** | All shell commands must be PowerShell-compatible (Windows) |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Project Structure
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
chrome-ext-manager/
|
|
35
|
+
├── src/
|
|
36
|
+
│ ├── cli.ts # CLI entry point (commander)
|
|
37
|
+
│ ├── index.ts # Public API exports
|
|
38
|
+
│ ├── url-parser.ts # Parse store URLs → { id, store }
|
|
39
|
+
│ ├── crx-parser.ts # CRX v2/v3 binary header parsing
|
|
40
|
+
│ ├── downloader.ts # Fetch CRX from Chrome/Edge stores
|
|
41
|
+
│ ├── extractor.ts # Extract CRX → directory
|
|
42
|
+
│ ├── packer.ts # Directory → signed CRX3 file
|
|
43
|
+
│ ├── search.ts # Search Chrome Web Store & Edge Add-ons
|
|
44
|
+
│ └── launcher.ts # Launch Edge with extensions
|
|
45
|
+
├── test/
|
|
46
|
+
│ ├── crx-parser.test.ts
|
|
47
|
+
│ ├── extractor.test.ts
|
|
48
|
+
│ ├── packer.test.ts
|
|
49
|
+
│ └── url-parser.test.ts
|
|
50
|
+
├── docs/superpowers/plans/
|
|
51
|
+
├── biome.json
|
|
52
|
+
├── tsconfig.json
|
|
53
|
+
├── package.json
|
|
54
|
+
└── bun.lock
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Module Responsibilities
|
|
58
|
+
|
|
59
|
+
| Module | Input | Output | Key Function |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| `url-parser.ts` | Store URL or bare ID | `StoreInfo { id, store }` | `parseStoreUrl()` |
|
|
62
|
+
| `crx-parser.ts` | CRX `Buffer` | `CrxInfo { version, headerLength, zipOffset }` | `parseCrxHeader()`, `extractZipFromCrx()` |
|
|
63
|
+
| `downloader.ts` | URL or ID + output dir | Path to saved `.crx` file | `downloadExtension()` |
|
|
64
|
+
| `extractor.ts` | `.crx` file path + output dir | Path to extracted directory | `extractCrx()` |
|
|
65
|
+
| `packer.ts` | Extension directory + optional output path | Path to packed `.crx` file | `packCrx()` |
|
|
66
|
+
| `search.ts` | Search query | `SearchResult[]` | `searchExtensions()` |
|
|
67
|
+
| `launcher.ts` | Extension path + options | `LaunchedEdge { port, pid, kill }` | `launch()`, `killAllEdges()`, `getEdgeInstallationPath()` |
|
|
68
|
+
| `cli.ts` | CLI arguments | Console output / exit code | Commander program |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Useful Commands
|
|
73
|
+
|
|
74
|
+
### Setup
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun install # Install all dependencies
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bun run src/cli.ts search "ublock" # Run CLI directly
|
|
84
|
+
bun run src/cli.ts get <url-or-id> -e # Download + extract
|
|
85
|
+
bun run src/cli.ts extract <file.crx> # Extract CRX
|
|
86
|
+
bun run src/cli.ts pack <directory> # Pack directory to CRX
|
|
87
|
+
bun run src/cli.ts launch --extension <dir> # Launch Edge with extension
|
|
88
|
+
bun run src/cli.ts test <url-or-id> # Download + extract + launch
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Quality Checks
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bun run typecheck # TypeScript type checking (tsc --noEmit)
|
|
95
|
+
bun run lint # Biome lint check (src/ + test/)
|
|
96
|
+
bun run lint:fix # Biome lint with auto-fix
|
|
97
|
+
bun run format # Biome format (src/ + test/)
|
|
98
|
+
bun run check # Format + lint:fix + typecheck (all-in-one)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Testing
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bun test # Run all tests
|
|
105
|
+
bun test test/url-parser.test.ts # Run a single test file
|
|
106
|
+
bun test --filter "parseStoreUrl" # Run tests matching a name
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Build
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bun run build # Builds dist/ext-cli.exe (runs prebuild check first)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Prebuild Pipeline
|
|
116
|
+
|
|
117
|
+
The `prebuild` script runs `bun run check` (format → lint:fix → typecheck)
|
|
118
|
+
before any build. This means the build will fail if code quality checks fail.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Architecture Notes
|
|
123
|
+
|
|
124
|
+
### CRX Format
|
|
125
|
+
|
|
126
|
+
- CRX files start with the magic bytes `Cr24`
|
|
127
|
+
- Version is stored as a 32-bit LE integer at offset 4 (2 or 3)
|
|
128
|
+
- A header length (32-bit LE) follows at offset 8
|
|
129
|
+
- The ZIP payload starts at offset `12 + headerLength`
|
|
130
|
+
- CRX3 adds a protobuf `AsymmetricKeyProof` with the public key and signature
|
|
131
|
+
|
|
132
|
+
### Download Flow
|
|
133
|
+
|
|
134
|
+
1. `parseStoreUrl()` resolves the URL/ID to a `StoreInfo` object
|
|
135
|
+
2. A download URL is constructed per store (Chrome or Edge)
|
|
136
|
+
3. The response is fetched with a browser-like User-Agent
|
|
137
|
+
4. The CRX magic bytes are verified before writing to disk
|
|
138
|
+
|
|
139
|
+
### Extract Flow
|
|
140
|
+
|
|
141
|
+
1. The CRX file is read into a `Buffer`
|
|
142
|
+
2. `extractZipFromCrx()` slices the buffer to get the ZIP payload
|
|
143
|
+
3. A temp ZIP is written, then extracted via PowerShell `Expand-Archive`
|
|
144
|
+
4. The temp ZIP is cleaned up
|
|
145
|
+
|
|
146
|
+
### Pack Flow
|
|
147
|
+
|
|
148
|
+
1. A `key.pem` is loaded or generated (2048-bit RSA via `node-forge`)
|
|
149
|
+
2. The extension directory is zipped via PowerShell `Compress-Archive`
|
|
150
|
+
3. A CRX3 header is built: protobuf-encoded `AsymmetricKeyProof` with the public key and a SHA-256-based RSA signature
|
|
151
|
+
4. The final CRX is assembled: magic + version + header length + header + ZIP data
|
|
152
|
+
|
|
153
|
+
### Key Implementation Details
|
|
154
|
+
|
|
155
|
+
- **`safeBinaryEncode()`** in `packer.ts` avoids stack overflow by encoding buffers in 8KB chunks instead of using `String.fromCharCode.apply()` on the full buffer
|
|
156
|
+
- **`computeExtensionId()`** hashes the DER public key with SHA-256, takes the first 16 bytes, and maps each byte to a letter in the alphabet `a-p` (base-16 encoding, not base-26)
|
|
157
|
+
- **Protobuf encoding** is done manually with `encodeVarint()` and `encodeField()` helpers — no protobuf library is used at runtime
|
|
158
|
+
|
|
159
|
+
### Launch Flow
|
|
160
|
+
|
|
161
|
+
1. `launch()` resolves extension path (auto-extracts `.crx` to temp directory if needed)
|
|
162
|
+
2. Constructs Edge flags: `--load-extension=<path>`, `--headless=new` (if headless), `--remote-debugging-port`
|
|
163
|
+
3. Spawns Edge via `chromium-edge-launcher` with fresh user data directory
|
|
164
|
+
4. Returns `LaunchedEdge` with port, PID, and `kill()` function for cleanup
|
|
165
|
+
5. Temp extension directory cleaned up on `kill()` if auto-extracted from CRX
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Best Practices & Guidelines
|
|
170
|
+
|
|
171
|
+
### Runtime & Package Management
|
|
172
|
+
|
|
173
|
+
- **Always use `bun`** — never npm, yarn, or pnpm
|
|
174
|
+
- Lock file is `bun.lock` — commit it when dependencies change
|
|
175
|
+
- Dev dependencies: `@biomejs/biome`, `@types/bun`, `@types/node`, `@types/node-forge`
|
|
176
|
+
|
|
177
|
+
### Shell Commands
|
|
178
|
+
|
|
179
|
+
- **Always use PowerShell-compatible commands** — this project targets Windows
|
|
180
|
+
- Use `dir` or `Get-ChildItem` instead of `ls -la`
|
|
181
|
+
- Use `Remove-Item -Recurse` instead of `rm -rf`
|
|
182
|
+
- Use `Copy-Item` instead of `cp`, `Move-Item` instead of `mv`
|
|
183
|
+
- Use `Get-Content` instead of `cat` for reading files
|
|
184
|
+
- When spawning shell commands in code (e.g., in `extractor.ts` and `packer.ts`), use `powershell -Command "..."` with proper quoting
|
|
185
|
+
|
|
186
|
+
### Code Style (Biome)
|
|
187
|
+
|
|
188
|
+
- Indent with **2 spaces** (no tabs)
|
|
189
|
+
- Max line width: **100 characters**
|
|
190
|
+
- Use **double quotes** for strings
|
|
191
|
+
- Always use **semicolons**
|
|
192
|
+
- Warn on unused variables and imports (`noUnusedVariables`, `noUnusedImports`)
|
|
193
|
+
|
|
194
|
+
### Error Handling
|
|
195
|
+
|
|
196
|
+
- CLI commands wrap action handlers in `try/catch` and call `process.exit(1)` on error
|
|
197
|
+
- Error messages use `error instanceof Error ? error.message : error` pattern
|
|
198
|
+
- Network errors include HTTP status codes in messages
|
|
199
|
+
- CRX validation checks magic bytes and throws descriptive errors before writing files
|
|
200
|
+
|
|
201
|
+
### Testing
|
|
202
|
+
|
|
203
|
+
- Tests use `bun:test` (import `describe`, `test`, `expect` from `"bun:test"`)
|
|
204
|
+
- Use `beforeEach` / `afterEach` for setup/teardown with temp directories
|
|
205
|
+
- Clean up temp directories with `rmSync(dir, { recursive: true, force: true })`
|
|
206
|
+
- Tests construct synthetic CRX files in-memory or from real zips
|
|
207
|
+
- No external fixtures are committed — tests are self-contained
|
|
208
|
+
|
|
209
|
+
### TypeScript
|
|
210
|
+
|
|
211
|
+
- Strict mode is enabled — all types must be explicit or inferrable
|
|
212
|
+
- Target ESNext with bundler module resolution
|
|
213
|
+
- Use `node:` prefix for Node.js built-in imports (e.g., `node:fs/promises`, `node:path`)
|
|
214
|
+
- Avoid `any` — use `unknown` and narrow with type guards
|
|
215
|
+
- Export public API through `src/index.ts`
|
|
216
|
+
|
|
217
|
+
### Security
|
|
218
|
+
|
|
219
|
+
- **Never commit `.pem` key files** — they are auto-generated per extension directory
|
|
220
|
+
- Validate downloaded CRX files by checking magic bytes before writing
|
|
221
|
+
- Do not trust external input (URLs, file contents) without validation
|
|
222
|
+
- The `.gitignore` already excludes `.env` files and build artifacts
|
|
223
|
+
|
|
224
|
+
### Commit Conventions
|
|
225
|
+
|
|
226
|
+
Use conventional commits:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
feat: add new feature
|
|
230
|
+
fix: correct a bug
|
|
231
|
+
test: add or update tests
|
|
232
|
+
refactor: improve code structure
|
|
233
|
+
docs: update documentation
|
|
234
|
+
chore: maintenance tasks
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Module Boundaries
|
|
238
|
+
|
|
239
|
+
- Each module has a single responsibility
|
|
240
|
+
- Dependencies flow downward: `cli.ts` → `downloader` / `extractor` / `packer` / `search` → `url-parser` / `crx-parser`
|
|
241
|
+
- `crx-parser.ts` and `url-parser.ts` are leaf modules with no internal dependencies
|
|
242
|
+
- `search.ts` is independent — it scrapes store HTML and does not use the downloader
|
|
243
|
+
|
|
244
|
+
### Network Requests
|
|
245
|
+
|
|
246
|
+
- User-Agent is set to Chrome 131 to avoid blocks from store servers
|
|
247
|
+
- `redirect: "follow"` is used for CRX downloads (stores redirect to CDN)
|
|
248
|
+
- HTML scraping is used for search (stores do not provide public APIs)
|
|
249
|
+
- Errors from non-200 responses are thrown immediately
|
|
250
|
+
|
|
251
|
+
### Adding New Features
|
|
252
|
+
|
|
253
|
+
1. Write tests first (TDD encouraged)
|
|
254
|
+
2. Run `bun test` to verify they fail
|
|
255
|
+
3. Implement the feature
|
|
256
|
+
4. Run `bun run check` to verify formatting, linting, and types
|
|
257
|
+
5. Run `bun test` again to verify all tests pass
|
|
258
|
+
6. Commit with a conventional commit message
|
|
259
|
+
|
|
260
|
+
### File Naming
|
|
261
|
+
|
|
262
|
+
- Source files: `kebab-case.ts` (e.g., `url-parser.ts`, `crx-parser.ts`)
|
|
263
|
+
- Test files: `<module>.test.ts` in the `test/` directory
|
|
264
|
+
- No barrel files beyond `src/index.ts`
|