diff --git a/.gitignore b/.gitignore index 0bbe0d1..5bedb54 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ dev-dist extension-release.zip *.zip tools-app-extension-*.zip +tools-app-extension-*.crx + +# Extension keys and builds +*.pem +*.crx +scripts/*.pub diff --git a/package.json b/package.json index 0338457..c285249 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "tools-app", "private": true, - "version": "0.6.16", + "version": "0.6.17", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", + "pack-extension": "node scripts/pack_crx.js", "postinstall": "mkdir -p public/wasm && cp node_modules/zxing-wasm/dist/reader/zxing_reader.wasm public/wasm/" }, "dependencies": { diff --git a/scripts/build_extension.py b/scripts/build_extension.py index 222e994..bc27de5 100644 --- a/scripts/build_extension.py +++ b/scripts/build_extension.py @@ -6,9 +6,14 @@ import zipfile # Configuration SOURCE_DIR = "extension" BUILD_DIR = "dist-extension" -OUTPUT_ZIP = "extension-release.zip" MANIFEST_FILE = "manifest.json" +# Read version to create dynamic zip name +with open(os.path.join(SOURCE_DIR, MANIFEST_FILE), "r") as f: + source_manifest = json.load(f) +version = source_manifest.get("version", "unknown") +OUTPUT_ZIP = f"tools-app-extension-v{version}.zip" + # Remove build directory if exists if os.path.exists(BUILD_DIR): shutil.rmtree(BUILD_DIR) @@ -27,7 +32,7 @@ print("Removing localhost from manifest...") # Filter host_permissions if "host_permissions" in manifest: manifest["host_permissions"] = [ - perm for perm in manifest["host_permissions"] + perm for perm in manifest["host_permissions"] if "localhost" not in perm ] @@ -36,7 +41,7 @@ if "content_scripts" in manifest: for script in manifest["content_scripts"]: if "matches" in script: script["matches"] = [ - match for match in script["matches"] + match for match in script["matches"] if "localhost" not in match ] @@ -55,4 +60,4 @@ with zipfile.ZipFile(OUTPUT_ZIP, "w", zipfile.ZIP_DEFLATED) as zipf: # Cleanup shutil.rmtree(BUILD_DIR) -print(f"Done! {OUTPUT_ZIP} is ready for upload to Chrome Web Store.") \ No newline at end of file +print(f"Done! {OUTPUT_ZIP} is ready for upload to Chrome Web Store.") diff --git a/scripts/pack_crx.js b/scripts/pack_crx.js new file mode 100644 index 0000000..48fc050 --- /dev/null +++ b/scripts/pack_crx.js @@ -0,0 +1,110 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +function findKey() { + // 1. Check if key provided via CLI + if (process.argv[3]) return process.argv[3]; + + // 2. Check local project directory (gitignored) + const localKey = path.join(process.cwd(), 'scripts', 'key.pem'); + if (fs.existsSync(localKey)) return localKey; + + // 3. Check common SSH key locations in ~/.ssh + const sshDir = path.join(process.env.HOME, '.ssh'); + const commonKeys = ['id_rsa', 'id_ecdsa', 'id_ed25519']; + + for (const keyName of commonKeys) { + const fullPath = path.join(sshDir, keyName); + if (fs.existsSync(fullPath)) return fullPath; + } + + return null; +} + +const KEY_PATH = findKey(); +const INPUT_SOURCE = process.argv[2] || path.join(process.cwd(), 'extension'); +const TEMP_DIR = path.join(process.cwd(), 'temp_extension_build'); + +function pack() { + console.log('šŸ“¦ Packing extension to CRX...'); + + if (!KEY_PATH) { + console.error('āŒ Error: No private key found.'); + console.log('Tried local scripts/key.pem and common ~/.ssh/id_* keys.'); + console.log('You can provide a path manually: npm run pack-extension '); + process.exit(1); + } + + let extensionDir = INPUT_SOURCE; + let isTemp = false; + + try { + // If input is a zip file, unzip it first + if (INPUT_SOURCE.endsWith('.zip')) { + console.log('🤐 Unzipping extension...'); + if (fs.existsSync(TEMP_DIR)) fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEMP_DIR); + execSync(`unzip -o "${INPUT_SOURCE}" -d "${TEMP_DIR}"`, { stdio: 'pipe' }); + extensionDir = TEMP_DIR; + isTemp = true; + } + + // Resolve actual extension directory (handle subdirs in zip) + if (!fs.existsSync(path.join(extensionDir, 'manifest.json'))) { + const subdirs = fs.readdirSync(extensionDir).filter(f => fs.statSync(path.join(extensionDir, f)).isDirectory()); + if (subdirs.length === 1 && fs.existsSync(path.join(extensionDir, subdirs[0], 'manifest.json'))) { + extensionDir = path.join(extensionDir, subdirs[0]); + console.log(`šŸ“‚ Found manifest in subdirectory: ${extensionDir}`); + } else { + console.error(`āŒ Error: manifest.json not found in ${extensionDir}`); + process.exit(1); + } + } + + // Determine output filename + let outputName; + if (INPUT_SOURCE.endsWith('.zip')) { + outputName = path.basename(INPUT_SOURCE).replace('.zip', '.crx'); + } else { + const manifest = JSON.parse(fs.readFileSync(path.join(extensionDir, 'manifest.json'), 'utf8')); + outputName = `tools-app-extension-v${manifest.version}.crx`; + } + const outputFull = path.join(process.cwd(), outputName); + + // Get version for logging + const manifest = JSON.parse(fs.readFileSync(path.join(extensionDir, 'manifest.json'), 'utf8')); + const version = manifest.version; + + console.log(`šŸ”‘ Using key: ${KEY_PATH}`); + console.log(`šŸ“‚ Source: ${extensionDir} (v${version})`); + console.log(`šŸš€ Running crx3 via npx...`); + + // Command: npx -y crx3 --key --crx + execSync(`npx -y crx3 --key "${KEY_PATH}" --crx "${outputFull}" "${extensionDir}"`, { + stdio: 'inherit' + }); + + // Cleanup any file that crx3 might have created automatically based on temp dir name + const unintendedFile = extensionDir + '.crx'; + if (fs.existsSync(unintendedFile) && unintendedFile !== outputFull) { + fs.unlinkSync(unintendedFile); + } + + console.log(`\nāœ… Success! Extension packed to: ${outputFull}`); + } catch (error) { + console.error('\nāŒ Failed to pack extension.'); + if (error.message.includes('algorithm')) { + console.error('āš ļø Note: Chrome CRX format requires RSA or ECDSA (P-256) keys.'); + } else { + console.error('Error details:', error.message); + } + process.exit(1); + } finally { + if (isTemp && fs.existsSync(TEMP_DIR)) { + fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + } + } +} + +pack();