@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.
@@ -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`