@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,9 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [involvex]
4
+ custom:
5
+ [
6
+ https://buymeacoffee.com/involvex,
7
+ https://paypal.me/involvex,
8
+ https://rewards.bing.com/welcome?rh=14525F68&ref=rafsrchae&form=ML2XE3&OCID=ML2XE3&PUBL=RewardsDO&CREA=ML2XE3,
9
+ ]
@@ -0,0 +1,15 @@
1
+ # Progress Ledger
2
+
3
+ ## Task Status
4
+
5
+ | Task | Status | Commits | Review |
6
+ |------|--------|---------|--------|
7
+ | 1: Project Scaffolding | complete | 1a2dfd4 | skip (scaffolding) |
8
+ | 2: URL Parser | complete | 0664478 | 5/5 tests pass |
9
+ | 3: CRX Parser | complete | 372bb8f | 4/4 tests pass |
10
+ | 4: Downloader | complete | 1ee51fe | compiles clean |
11
+ | 5: Extractor | complete | 7c57087 | 12/12 pass (timeout concern noted) |
12
+ | 6: Packer | complete | 8d6308d | 2/2 tests pass |
13
+ | 7: CLI Entry Point | complete | aaaf063 | all help commands pass |
14
+ | 8: E2E Testing | complete | e8a2b3a | download, extract, pack all verified |
15
+ | 9: Build & Release | complete | b65a1ba | ext-cli.exe builds and runs |
@@ -0,0 +1,89 @@
1
+ # Task 1: Project Scaffolding
2
+
3
+ **Files:**
4
+ - Create: `package.json`
5
+ - Create: `tsconfig.json`
6
+ - Create: `src/cli.ts` (minimal)
7
+ - Create: `src/index.ts` (minimal)
8
+
9
+ **Interfaces:**
10
+ - Consumes: none
11
+ - Produces: project structure ready for development
12
+
13
+ - [ ] **Step 1: Initialize Bun project**
14
+
15
+ ```bash
16
+ cd D:\repos\chrome-ext-manager
17
+ bun init -y
18
+ ```
19
+
20
+ - [ ] **Step 2: Install dependencies**
21
+
22
+ ```bash
23
+ bun add commander node-forge
24
+ bun add -d @types/node @types/bun typescript
25
+ ```
26
+
27
+ - [ ] **Step 3: Create tsconfig.json**
28
+
29
+ ```json
30
+ {
31
+ "compilerOptions": {
32
+ "target": "ESNext",
33
+ "module": "ESNext",
34
+ "moduleResolution": "bundler",
35
+ "strict": true,
36
+ "esModuleInterop": true,
37
+ "skipLibCheck": true,
38
+ "outDir": "dist",
39
+ "rootDir": "src",
40
+ "declaration": true,
41
+ "resolveJsonModule": true,
42
+ "types": ["bun-types"]
43
+ },
44
+ "include": ["src/**/*"],
45
+ "exclude": ["node_modules", "dist", "test"]
46
+ }
47
+ ```
48
+
49
+ - [ ] **Step 4: Create minimal src/cli.ts**
50
+
51
+ ```typescript
52
+ #!/usr/bin/env bun
53
+
54
+ console.log("ext-cli - Chrome/Edge Extension Manager");
55
+ ```
56
+
57
+ - [ ] **Step 5: Create minimal src/index.ts**
58
+
59
+ ```typescript
60
+ export { downloadExtension } from "./downloader";
61
+ export { extractCrx } from "./extractor";
62
+ export { packCrx } from "./packer";
63
+ ```
64
+
65
+ - [ ] **Step 6: Add scripts to package.json**
66
+
67
+ Add to `package.json`:
68
+ ```json
69
+ {
70
+ "name": "ext-cli",
71
+ "version": "1.0.0",
72
+ "type": "module",
73
+ "bin": {
74
+ "ext-cli": "./src/cli.ts"
75
+ },
76
+ "scripts": {
77
+ "build": "bun build src/cli.ts --compile --outfile ext-cli",
78
+ "dev": "bun run src/cli.ts",
79
+ "test": "bun test"
80
+ }
81
+ }
82
+ ```
83
+
84
+ - [ ] **Step 7: Commit**
85
+
86
+ ```bash
87
+ git add -A
88
+ git commit -m "feat: scaffold ext-cli project with bun, typescript, commander"
89
+ ```
@@ -0,0 +1,52 @@
1
+ # Task 1 Report: Project Scaffolding
2
+
3
+ ## What I Implemented
4
+
5
+ Completed all 7 steps from the task brief:
6
+
7
+ 1. **Bun project initialization** - Ran `bun init -y` to create base project structure
8
+ 2. **Dependencies installed** - Added commander, node-forge (runtime) and @types/node, @types/bun, typescript (dev)
9
+ 3. **tsconfig.json created** - Configured with ESNext target, bundler module resolution, strict mode, and bun-types
10
+ 4. **src/cli.ts created** - Minimal CLI entry point that prints "ext-cli - Chrome/Edge Extension Manager"
11
+ 5. **src/index.ts created** - Placeholder exports for downloader, extractor, and packer modules
12
+ 6. **package.json updated** - Added name "ext-cli", version "1.0.0", type "module", bin entry, and scripts (build, dev, test)
13
+ 7. **Committed** - All changes committed with conventional commit message
14
+
15
+ ## Test Results
16
+
17
+ ```bash
18
+ $ bun run src/cli.ts
19
+ ext-cli - Chrome/Edge Extension Manager
20
+
21
+ $ bun run dev
22
+ $ bun run src/cli.ts
23
+ ext-cli - Chrome/Edge Extension Manager
24
+ ```
25
+
26
+ Both direct execution and npm script work correctly.
27
+
28
+ ## Files Changed
29
+
30
+ | File | Status |
31
+ |------|--------|
32
+ | package.json | Created |
33
+ | tsconfig.json | Created |
34
+ | src/cli.ts | Created |
35
+ | src/index.ts | Created |
36
+ | bun.lock | Created |
37
+ | .gitignore | Created |
38
+ | README.md | Created |
39
+
40
+ ## Self-Review Findings
41
+
42
+ - ✅ All files created in correct locations (root for config, src/ for source)
43
+ - ✅ CLI prints expected message
44
+ - ✅ package.json has correct scripts and bin entry
45
+ - ✅ tsconfig.json configured for Bun development
46
+ - ✅ No issues found - implementation matches spec exactly
47
+
48
+ ## Commit
49
+
50
+ ```
51
+ 1a2dfd4 feat: scaffold ext-cli project with bun, typescript, commander
52
+ ```
@@ -0,0 +1,113 @@
1
+ # Task 2: URL Parser Module
2
+
3
+ **Files:**
4
+ - Create: `src/url-parser.ts`
5
+ - Create: `test/url-parser.test.ts`
6
+
7
+ **Interfaces:**
8
+ - Consumes: none
9
+ - Produces: `parseStoreUrl(url: string): StoreInfo` where `StoreInfo = { id: string; store: 'chrome' | 'edge' }`
10
+
11
+ - [ ] **Step 1: Write failing tests**
12
+
13
+ ```typescript
14
+ // test/url-parser.test.ts
15
+ import { describe, test, expect } from "bun:test";
16
+ import { parseStoreUrl } from "../src/url-parser";
17
+
18
+ describe("parseStoreUrl", () => {
19
+ test("parses Chrome Web Store URL", () => {
20
+ const result = parseStoreUrl(
21
+ "https://chromewebstore.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm"
22
+ );
23
+ expect(result).toEqual({ id: "cjpalhdlnbpafiamejdnhcphjbkeiagm", store: "chrome" });
24
+ });
25
+
26
+ test("parses Chrome URL with query params", () => {
27
+ const result = parseStoreUrl(
28
+ "https://chromewebstore.google.com/detail/ext-name/abcdef1234567890abcdef12?hl=en"
29
+ );
30
+ expect(result).toEqual({ id: "abcdef1234567890abcdef12", store: "chrome" });
31
+ });
32
+
33
+ test("parses Edge Add-ons URL", () => {
34
+ const result = parseStoreUrl(
35
+ "https://microsoftedge.microsoft.com/addons/detail/ublock-origin/odfafepnkmbhccpbejgmiehpchacaeak"
36
+ );
37
+ expect(result).toEqual({ id: "odfafepnkmbhccpbejgmiehpchacaeak", store: "edge" });
38
+ });
39
+
40
+ test("parses bare 32-char extension ID as chrome", () => {
41
+ const result = parseStoreUrl("cjpalhdlnbpafiamejdnhcphjbkeiagm");
42
+ expect(result).toEqual({ id: "cjpalhdlnbpafiamejdnhcphjbkeiagm", store: "chrome" });
43
+ });
44
+
45
+ test("throws on invalid input", () => {
46
+ expect(() => parseStoreUrl("")).toThrow();
47
+ expect(() => parseStoreUrl("not-a-valid-id!")).toThrow();
48
+ });
49
+ });
50
+ ```
51
+
52
+ - [ ] **Step 2: Run tests to verify they fail**
53
+
54
+ ```bash
55
+ bun test test/url-parser.test.ts
56
+ ```
57
+ Expected: FAIL — `parseStoreUrl` not found
58
+
59
+ - [ ] **Step 3: Implement url-parser.ts**
60
+
61
+ ```typescript
62
+ // src/url-parser.ts
63
+
64
+ export interface StoreInfo {
65
+ id: string;
66
+ store: "chrome" | "edge";
67
+ }
68
+
69
+ const CHROME_URL_RE =
70
+ /chromewebstore\.google\.com\/webstore\/detail\/[^/]+\/([a-z]{32})/i;
71
+ const EDGE_URL_RE =
72
+ /microsoftedge\.microsoft\.com\/addons\/detail\/([a-z]{32})/i;
73
+ const BARE_ID_RE = /^[a-z]{32}$/i;
74
+
75
+ export function parseStoreUrl(input: string): StoreInfo {
76
+ const trimmed = input.trim();
77
+
78
+ // Try Chrome Web Store URL
79
+ const chromeMatch = trimmed.match(CHROME_URL_RE);
80
+ if (chromeMatch) {
81
+ return { id: chromeMatch[1].toLowerCase(), store: "chrome" };
82
+ }
83
+
84
+ // Try Edge Add-ons URL
85
+ const edgeMatch = trimmed.match(EDGE_URL_RE);
86
+ if (edgeMatch) {
87
+ return { id: edgeMatch[1].toLowerCase(), store: "edge" };
88
+ }
89
+
90
+ // Try bare extension ID (32 lowercase letters)
91
+ if (BARE_ID_RE.test(trimmed)) {
92
+ return { id: trimmed.toLowerCase(), store: "chrome" };
93
+ }
94
+
95
+ throw new Error(
96
+ `Invalid extension URL or ID: "${input}". Expected a Chrome Web Store URL, Edge Add-ons URL, or a 32-character extension ID.`
97
+ );
98
+ }
99
+ ```
100
+
101
+ - [ ] **Step 4: Run tests to verify they pass**
102
+
103
+ ```bash
104
+ bun test test/url-parser.test.ts
105
+ ```
106
+ Expected: ALL PASS
107
+
108
+ - [ ] **Step 5: Commit**
109
+
110
+ ```bash
111
+ git add src/url-parser.ts test/url-parser.test.ts
112
+ git commit -m "feat: add URL parser for Chrome/Edge store URLs and bare IDs"
113
+ ```
@@ -0,0 +1,27 @@
1
+ # Task 2: URL Parser Module - Report
2
+
3
+ ## Summary
4
+ Implemented `parseStoreUrl` function that converts Chrome/Edge store URLs and bare extension IDs into structured `StoreInfo` objects.
5
+
6
+ ## Files Created
7
+ - `src/url-parser.ts` - URL parser implementation
8
+ - `test/url-parser.test.ts` - Test suite (5 tests)
9
+
10
+ ## Implementation Notes
11
+ The brief's regex patterns had minor mismatches with the test cases:
12
+ 1. Chrome URL regex expected `/webstore/detail/` but test used `/detail/` - fixed with `(?:webstore\/)?`
13
+ 2. Edge URL regex expected ID immediately after `/addons/detail/` but test had extension name in between - added `[^/]+\/`
14
+ 3. Test used 24-char alphanumeric ID, not strictly 32 lowercase - changed `[a-z]{32}` to `[a-z0-9]+`
15
+
16
+ Code follows clean TypeScript conventions, no external dependencies, minimal footprint.
17
+
18
+ ## Test Results
19
+ 5/5 tests pass:
20
+ - ✓ Parses Chrome Web Store URL
21
+ - ✓ Parses Chrome URL with query params
22
+ - ✓ Parses Edge Add-ons URL
23
+ - ✓ Parses bare 32-char extension ID
24
+ - ✓ Throws on invalid input
25
+
26
+ ## Commit
27
+ `0664478` - feat: add URL parser for Chrome/Edge store URLs and bare IDs
@@ -0,0 +1,123 @@
1
+ # Task 3: CRX Parser Module
2
+
3
+ **Files:**
4
+ - Create: `src/crx-parser.ts`
5
+ - Create: `test/crx-parser.test.ts`
6
+
7
+ **Interfaces:**
8
+ - Consumes: none
9
+ - Produces: `parseCrxHeader(buffer: Buffer): CrxInfo` and `extractZipFromCrx(buffer: Buffer): Buffer`
10
+
11
+ - [ ] **Step 1: Write failing tests**
12
+
13
+ ```typescript
14
+ // test/crx-parser.test.ts
15
+ import { describe, test, expect } from "bun:test";
16
+ import { parseCrxHeader, extractZipFromCrx } from "../src/crx-parser";
17
+
18
+ describe("CRX Parser", () => {
19
+ test("parseCrxHeader reads CRX3 header", () => {
20
+ const header = Buffer.alloc(16);
21
+ header.write("Cr24", 0);
22
+ header.writeUInt32LE(3, 4);
23
+ header.writeUInt32LE(4, 8);
24
+ header.writeUInt32LE(0, 12);
25
+
26
+ const info = parseCrxHeader(header);
27
+ expect(info.version).toBe(3);
28
+ expect(info.headerLength).toBe(4);
29
+ expect(info.zipOffset).toBe(16);
30
+ });
31
+
32
+ test("parseCrxHeader reads CRX2 header", () => {
33
+ const header = Buffer.alloc(16);
34
+ header.write("Cr24", 0);
35
+ header.writeUInt32LE(2, 4);
36
+ header.writeUInt32LE(4, 8);
37
+
38
+ const info = parseCrxHeader(header);
39
+ expect(info.version).toBe(2);
40
+ });
41
+
42
+ test("parseCrxHeader throws on invalid magic", () => {
43
+ const bad = Buffer.from("NOTACRX");
44
+ expect(() => parseCrxHeader(bad)).toThrow("Not a CRX file");
45
+ });
46
+
47
+ test("extractZipFromCrx returns zip bytes after header", () => {
48
+ const fixed = Buffer.alloc(12);
49
+ fixed.write("Cr24", 0);
50
+ fixed.writeUInt32LE(3, 4);
51
+ fixed.writeUInt32LE(4, 8);
52
+
53
+ const protoHeader = Buffer.alloc(4);
54
+ const zipData = Buffer.from("PKZIP_DATA_HERE");
55
+
56
+ const crx = Buffer.concat([fixed, protoHeader, zipData]);
57
+ const zip = extractZipFromCrx(crx);
58
+
59
+ expect(zip.equals(zipData)).toBe(true);
60
+ });
61
+ });
62
+ ```
63
+
64
+ - [ ] **Step 2: Run tests to verify they fail**
65
+
66
+ ```bash
67
+ bun test test/crx-parser.test.ts
68
+ ```
69
+ Expected: FAIL — `parseCrxHeader` not found
70
+
71
+ - [ ] **Step 3: Implement crx-parser.ts**
72
+
73
+ ```typescript
74
+ // src/crx-parser.ts
75
+
76
+ export interface CrxInfo {
77
+ version: number;
78
+ headerLength: number;
79
+ zipOffset: number;
80
+ }
81
+
82
+ const CRX_MAGIC = "Cr24";
83
+
84
+ export function parseCrxHeader(buffer: Buffer): CrxInfo {
85
+ if (buffer.length < 12) {
86
+ throw new Error("Not a CRX file: buffer too small");
87
+ }
88
+
89
+ const magic = buffer.toString("ascii", 0, 4);
90
+ if (magic !== CRX_MAGIC) {
91
+ throw new Error(`Not a CRX file: invalid magic "${magic}" (expected "${CRX_MAGIC}")`);
92
+ }
93
+
94
+ const version = buffer.readUInt32LE(4);
95
+ if (version !== 2 && version !== 3) {
96
+ throw new Error(`Unsupported CRX version: ${version}`);
97
+ }
98
+
99
+ const headerLength = buffer.readUInt32LE(8);
100
+ const zipOffset = 12 + headerLength;
101
+
102
+ return { version, headerLength, zipOffset };
103
+ }
104
+
105
+ export function extractZipFromCrx(buffer: Buffer): Buffer {
106
+ const info = parseCrxHeader(buffer);
107
+ return buffer.subarray(info.zipOffset);
108
+ }
109
+ ```
110
+
111
+ - [ ] **Step 4: Run tests to verify they pass**
112
+
113
+ ```bash
114
+ bun test test/crx-parser.test.ts
115
+ ```
116
+ Expected: ALL PASS
117
+
118
+ - [ ] **Step 5: Commit**
119
+
120
+ ```bash
121
+ git add src/crx-parser.ts test/crx-parser.test.ts
122
+ git commit -m "feat: add CRX v2/v3 parser for header reading and ZIP extraction"
123
+ ```
@@ -0,0 +1,26 @@
1
+ # Task 3 Report: CRX Parser Module
2
+
3
+ ## Status: DONE
4
+
5
+ ## Files Created
6
+ - `src/crx-parser.ts` — CRX v2/v3 binary parser with `parseCrxHeader` and `extractZipFromCrx`
7
+ - `test/crx-parser.test.ts` — 4 tests covering CRX3 header, CRX2 header, invalid magic, and ZIP extraction
8
+
9
+ ## Test Summary
10
+ 4/4 pass. Covers: CRX3 header parsing, CRX2 header parsing, invalid magic rejection, ZIP payload extraction.
11
+
12
+ ## Implementation Notes
13
+ - Reads fixed 12-byte header: 4-byte magic (`Cr24`), 4-byte version, 4-byte header length
14
+ - Validates magic bytes and version (only 2 and 3 supported)
15
+ - `zipOffset = 12 + headerLength` — ZIP data starts immediately after the CRX header
16
+ - `extractZipFromCrx` uses `subarray` (zero-copy view) for efficiency
17
+ - Matches Chrome CRX format spec: the header-length field covers everything after the fixed 12 bytes (extension headers in CRX3, pubkey+sig in CRX2)
18
+
19
+ ## Self-Review
20
+ - Code is minimal and correct per the brief
21
+ - No edge cases missed — buffer-too-small check prevents out-of-bounds reads
22
+ - Error messages are descriptive for debugging
23
+ - Interface `CrxInfo` is clean and sufficient for downstream consumers (extractor in Task 5)
24
+
25
+ ## Concerns
26
+ None.
@@ -0,0 +1,85 @@
1
+ # Task 4: Downloader Module
2
+
3
+ **Files:**
4
+ - Create: `src/downloader.ts`
5
+
6
+ **Interfaces:**
7
+ - Consumes: `parseStoreUrl` from `src/url-parser.ts`
8
+ - Produces: `downloadExtension(urlOrId: string, outputDir?: string): Promise<string>` — returns path to saved .crx file
9
+
10
+ - [ ] **Step 1: Implement downloader.ts**
11
+
12
+ ```typescript
13
+ // src/downloader.ts
14
+ import { parseStoreUrl } from "./url-parser";
15
+ import { mkdir, writeFile } from "fs/promises";
16
+ import { join } from "path";
17
+
18
+ const CHROME_CRX_URL =
19
+ "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0&acceptformat=crx2,crx3&x=id%3D{ID}%26uc";
20
+
21
+ const EDGE_CRX_URL =
22
+ "https://edge.microsoft.com/extensionwebstorebase/v1/crx?response=redirect&x=id%3D{ID}%26installsource%3Dondemand%26uc";
23
+
24
+ function buildDownloadUrl(id: string, store: "chrome" | "edge"): string {
25
+ const template = store === "chrome" ? CHROME_CRX_URL : EDGE_CRX_URL;
26
+ return template.replace("{ID}", id);
27
+ }
28
+
29
+ /**
30
+ * Download a CRX file from the Chrome Web Store or Edge Add-ons.
31
+ * Accepts a full store URL or a bare 32-char extension ID.
32
+ * Returns the path to the saved .crx file.
33
+ */
34
+ export async function downloadExtension(
35
+ urlOrId: string,
36
+ outputDir: string = "."
37
+ ): Promise<string> {
38
+ const { id, store } = parseStoreUrl(urlOrId);
39
+ const downloadUrl = buildDownloadUrl(id, store);
40
+
41
+ console.log(`Downloading ${store} extension: ${id}`);
42
+ console.log(`URL: ${downloadUrl}`);
43
+
44
+ const response = await fetch(downloadUrl, {
45
+ headers: {
46
+ "User-Agent":
47
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
48
+ },
49
+ redirect: "follow",
50
+ });
51
+
52
+ if (!response.ok) {
53
+ throw new Error(
54
+ `Download failed: HTTP ${response.status} ${response.statusText}`
55
+ );
56
+ }
57
+
58
+ const arrayBuffer = await response.arrayBuffer();
59
+ const buffer = Buffer.from(arrayBuffer);
60
+
61
+ // Verify it's a valid CRX file
62
+ const magic = buffer.toString("ascii", 0, 4);
63
+ if (magic !== "Cr24") {
64
+ throw new Error(
65
+ `Invalid CRX response: got "${magic}" instead of "Cr24". The extension may not exist or the store may have changed.`
66
+ );
67
+ }
68
+
69
+ // Ensure output directory exists
70
+ await mkdir(outputDir, { recursive: true });
71
+
72
+ const outputPath = join(outputDir, `${id}.crx`);
73
+ await writeFile(outputPath, buffer);
74
+
75
+ console.log(`Saved: ${outputPath} (${(buffer.length / 1024).toFixed(1)} KB)`);
76
+ return outputPath;
77
+ }
78
+ ```
79
+
80
+ - [ ] **Step 2: Commit**
81
+
82
+ ```bash
83
+ git add src/downloader.ts
84
+ git commit -m "feat: add downloader for Chrome Web Store and Edge Add-ons CRX files"
85
+ ```
@@ -0,0 +1,33 @@
1
+ # Task 4: Downloader Module — Report
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+ `src/downloader.ts` — a function that downloads CRX files from the Chrome Web Store or Edge Add-ons.
7
+
8
+ ## API
9
+ ```typescript
10
+ export async function downloadExtension(
11
+ urlOrId: string, // full store URL or bare 32-char extension ID
12
+ outputDir: string = "." // directory to save the .crx file
13
+ ): Promise<string> // returns path to saved .crx file
14
+ ```
15
+
16
+ ## Key design decisions
17
+ - Followed the brief exactly — no deviations.
18
+ - Uses `parseStoreUrl` from Task 2's URL parser to resolve input to `{ id, store }`.
19
+ - Template-based URL construction for Chrome and Edge CRX endpoints.
20
+ - Sets a browser-like `User-Agent` header to avoid store blocks.
21
+ - Validates CRX magic bytes (`Cr24`) before writing to disk.
22
+ - Creates output directory recursively if it doesn't exist.
23
+ - Logs download progress and file size.
24
+
25
+ ## Verification
26
+ - `bun run src/cli.ts` — still works (imports are clean).
27
+ - No type errors detected.
28
+
29
+ ## Commit
30
+ `1ee51fe` — feat: add downloader for Chrome Web Store and Edge Add-ons CRX files
31
+
32
+ ## Concerns
33
+ None. Implementation matches the brief exactly.