From 1cd7b693ab93f14e85664ceb2cdc7ca26dde0816 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Sat, 12 Sep 2026 22:14:22 -0700 Subject: [PATCH] Release OpenScript 1.0.3 --- AGENTS.md | 9 +++++ manifest.json | 2 +- package-lock.json | 4 +- package.json | 3 +- scripts/publish.js | 84 ++++++++++++++++++++++++++++++++++++++++ src/popup.js | 22 ++++++++--- src/utils/parser.js | 7 ++++ src/utils/userScripts.js | 6 ++- src/version.js | 2 +- tests/parser.test.js | 29 +++++++++++++- 10 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 AGENTS.md create mode 100644 scripts/publish.js diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..068a32e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Chrome Web Store Instructions + +The following environment variables are configured on the host system: +- `CHROME_CLIENT_ID` +- `CHROME_CLIENT_SECRET` +- `CHROME_REFRESH_TOKEN` + +## Chrome Extension Details +- **Extension ID:** `dkelmgdchndagjemmodhkphdikhpnfol` \ No newline at end of file diff --git a/manifest.json b/manifest.json index fcd5520..b8e43df 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "minimum_chrome_version": "138", "name": "OpenScript", - "version": "1.0.2", + "version": "1.0.3", "description": "A lightweight user script manager for modern browsers", "action": { "default_popup": "src/popup.html", diff --git a/package-lock.json b/package-lock.json index 4c567e3..5722014 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscript", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscript", - "version": "1.0.2", + "version": "1.0.3", "dependencies": { "lucide": "^0.475.0" }, diff --git a/package.json b/package.json index 1782eef..eddace8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openscript", - "version": "1.0.2", + "version": "1.0.3", "private": true, "type": "module", "scripts": { @@ -9,6 +9,7 @@ "build:icons": "node scripts/generate-icons.js", "screenshot": "node scripts/make-screenshot.js", "zip": "node scripts/build-zip.js", + "publish": "node scripts/publish.js", "test": "node --test" }, "dependencies": { diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 0000000..fc68339 --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,84 @@ +import fs from 'fs'; + +const EXTENSION_ID = 'dkelmgdchndagjemmodhkphdikhpnfol'; +const { CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN } = process.env; + +if (!CHROME_CLIENT_ID || !CHROME_CLIENT_SECRET || !CHROME_REFRESH_TOKEN) { + console.error('Missing Chrome Web Store credentials in environment variables.'); + process.exit(1); +} + +const zipPath = 'openscript.zip'; +if (!fs.existsSync(zipPath)) { + console.error(`Zip file not found: ${zipPath}`); + process.exit(1); +} + +async function publish() { + console.log('1. Refreshing access token...'); + const tokenRes = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CHROME_CLIENT_ID, + client_secret: CHROME_CLIENT_SECRET, + refresh_token: CHROME_REFRESH_TOKEN, + grant_type: 'refresh_token', + }), + }); + + const tokenData = await tokenRes.json(); + if (!tokenRes.ok) { + console.error('Failed to obtain access token:', tokenData); + process.exit(1); + } + + const accessToken = tokenData.access_token; + console.log('✓ Access token obtained.'); + + console.log(`2. Uploading ${zipPath} to Chrome Web Store (${EXTENSION_ID})...`); + const zipBuffer = fs.readFileSync(zipPath); + const uploadRes = await fetch( + `https://www.googleapis.com/upload/chromewebstore/v1.1/items/${EXTENSION_ID}`, + { + method: 'PUT', + headers: { + Authorization: `Bearer ${accessToken}`, + 'x-goog-api-version': '2', + }, + body: zipBuffer, + } + ); + + const uploadData = await uploadRes.json(); + if (!uploadRes.ok || uploadData.uploadState !== 'SUCCESS') { + console.error('Upload failed:', uploadData); + process.exit(1); + } + console.log('✓ Upload successful:', uploadData.uploadState); + + console.log('3. Publishing new version to Chrome Web Store...'); + const publishRes = await fetch( + `https://www.googleapis.com/chromewebstore/v1.1/items/${EXTENSION_ID}/publish`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'x-goog-api-version': '2', + 'Content-Length': '0', + }, + } + ); + + const publishData = await publishRes.json(); + if (!publishRes.ok || (publishData.status && !publishData.status.includes('OK'))) { + console.error('Publish failed:', publishData); + process.exit(1); + } + console.log('✓ Publish response:', publishData); +} + +publish().catch(err => { + console.error('Error during publish:', err); + process.exit(1); +}); diff --git a/src/popup.js b/src/popup.js index 80f8665..2e96710 100644 --- a/src/popup.js +++ b/src/popup.js @@ -1,7 +1,7 @@ import { getScripts, saveScripts, getSecrets, saveSecrets, garbageCollectScriptStorage, } from './utils/storage.js'; -import { parseMeta, getBoilerplate } from './utils/parser.js'; +import { parseMeta, getBoilerplate, getMetaRunAt } from './utils/parser.js'; import { isUserScriptsAvailable } from './utils/userScripts.js'; import { renderIcons, icon } from './utils/icons.js'; import { VERSION } from './version.js'; @@ -44,6 +44,7 @@ const init = async () => { state.scripts = scripts.map(s => ({ ...s, version: parseMeta(s.code || '').version, + runAt: getMetaRunAt(s.code || '') || s.runAt || 'document_idle', })); state.secrets = secrets; render(); @@ -92,7 +93,7 @@ const saveCurrentScript = async () => { requires: meta.requires, requireCache: existing?.requireCache || {}, storageToken: existing?.storageToken, - runAt: $('#run-at-select')?.value || meta.runAt || 'document_idle', + runAt: getMetaRunAt(code) || $('#run-at-select')?.value || 'document_idle', code, enabled: existing ? existing.enabled : true, updatedAt: Date.now(), @@ -252,7 +253,7 @@ const renderScriptList = () => { const renderEditor = () => { const script = state.scripts.find(s => s.id === state.editingId); const code = script ? script.code : getBoilerplate(); - const runAt = script?.runAt || 'document_idle'; + const runAt = getMetaRunAt(code) || script?.runAt || 'document_idle'; return `
@@ -458,12 +459,23 @@ const bindEvents = () => { $('#btn-cancel-edit')?.addEventListener('click', () => setTab('list')); $('#btn-reset-boilerplate')?.addEventListener('click', () => { const el = $('#editor-code'); - if (el && confirm('Reset code to default boilerplate?')) el.value = getBoilerplate(); + if (el && confirm('Reset code to default boilerplate?')) { + el.value = getBoilerplate(); + const sel = $('#run-at-select'); + const script = state.scripts.find(s => s.id === state.editingId); + if (sel) sel.value = script?.runAt || 'document_idle'; + } }); - // Tab key indent in editor + // Tab key indent & metadata sync in editor const textarea = $('#editor-code'); if (textarea) { + textarea.addEventListener('input', () => { + const metaRunAt = getMetaRunAt(textarea.value); + const sel = $('#run-at-select'); + if (sel && metaRunAt) sel.value = metaRunAt; + }); + textarea.addEventListener('keydown', e => { if (e.key === 'Tab') { e.preventDefault(); diff --git a/src/utils/parser.js b/src/utils/parser.js index 548c3bf..f4c01eb 100644 --- a/src/utils/parser.js +++ b/src/utils/parser.js @@ -2,6 +2,13 @@ const SINGLE_KEYS = new Set(['name', 'version', 'description', 'run-at']); const MULTI_KEYS = new Set(['match', 'require']); +const RUN_AT_OPTIONS = new Set(['document_start', 'document_end', 'document_idle']); + +export const getMetaRunAt = code => { + const block = code.match(/\/\/ ==UserScript==([\s\S]*?)\/\/ ==\/UserScript==/)?.[1] || ''; + const val = block.match(/\/\/\s*@run-at\s+([\w-]+)/i)?.[1]?.toLowerCase().replace('-', '_'); + return RUN_AT_OPTIONS.has(val) ? val : null; +}; export const parseMeta = code => { const block = code.match(/\/\/ ==UserScript==([\s\S]*?)\/\/ ==\/UserScript==/)?.[1] || ''; diff --git a/src/utils/userScripts.js b/src/utils/userScripts.js index cbd0af9..5f2ab3b 100644 --- a/src/utils/userScripts.js +++ b/src/utils/userScripts.js @@ -1,5 +1,5 @@ import { getScripts, saveScripts, getSecrets } from './storage.js'; -import { normalizeMatch, parseMeta } from './parser.js'; +import { normalizeMatch, parseMeta, getMetaRunAt } from './parser.js'; import { VERSION } from '../version.js'; const STORAGE_MESSAGE = 'OPEN_SCRIPT_STORAGE'; @@ -91,14 +91,16 @@ export const syncUserScripts = async ({ refreshRequires = false } = {}) => { scripts = scripts.map(script => { const requires = parseMeta(script.code || '').requires; const storageToken = script.storageToken || crypto.randomUUID(); + const runAt = getMetaRunAt(script.code || '') || script.runAt || 'document_idle'; if (requires.some(url => script.requireCache?.[url] === undefined)) missingCache = true; if (storageToken === script.storageToken && + runAt === script.runAt && JSON.stringify(requires) === JSON.stringify(script.requires || [])) return script; changed = true; const requireCache = Object.fromEntries(requires.flatMap(url => script.requireCache && Object.hasOwn(script.requireCache, url) ? [[url, script.requireCache[url]]] : [] )); - return { ...script, requires, requireCache, storageToken }; + return { ...script, runAt, requires, requireCache, storageToken }; }); let warnings = []; diff --git a/src/version.js b/src/version.js index 926d3f2..30cbd1a 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const VERSION = '1.0.2'; +export const VERSION = '1.0.3'; diff --git a/tests/parser.test.js b/tests/parser.test.js index cb906c4..a8e3b14 100644 --- a/tests/parser.test.js +++ b/tests/parser.test.js @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { parseMeta, normalizeMatch, getBoilerplate } from '../src/utils/parser.js'; +import { parseMeta, normalizeMatch, getBoilerplate, getMetaRunAt } from '../src/utils/parser.js'; import { buildScriptCode, refreshRequireCaches, syncUserScripts, wrapScriptCode, } from '../src/utils/userScripts.js'; @@ -39,6 +39,33 @@ test('parseMeta falls back to defaults when fields are missing', () => { assert.deepEqual(meta.requires, []); }); +test('getMetaRunAt parses valid @run-at directives and ignores invalid or absent ones', () => { + assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document-start\n// ==/UserScript=='), 'document_start'); + assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document_end\n// ==/UserScript=='), 'document_end'); + assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at document-idle\n// ==/UserScript=='), 'document_idle'); + assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at DOCUMENT-START\n// ==/UserScript=='), 'document_start'); + assert.equal(getMetaRunAt('// ==UserScript==\n// @name Test\n// ==/UserScript=='), null); + assert.equal(getMetaRunAt('// @run-at document-start outside header'), null); + assert.equal(getMetaRunAt('// ==UserScript==\n// @run-at invalid-timing\n// ==/UserScript=='), null); +}); + +test('metadata run-at header overrides selector and falls back when absent', () => { + const resolveRunAt = (code, selectorVal, storedVal) => + getMetaRunAt(code) || selectorVal || storedVal || 'document_idle'; + + const codeWithHeader = '// ==UserScript==\n// @run-at document-start\n// ==/UserScript=='; + const codeWithoutHeader = '// ==UserScript==\n// @name Script\n// ==/UserScript=='; + + // First time creation: metadata overrides selector + assert.equal(resolveRunAt(codeWithHeader, 'document_idle', undefined), 'document_start'); + // First time creation: no metadata uses selector + assert.equal(resolveRunAt(codeWithoutHeader, 'document_end', undefined), 'document_end'); + // Opening to edit: metadata overrides stored value + assert.equal(resolveRunAt(codeWithHeader, null, 'document_idle'), 'document_start'); + // Opening to edit: no metadata falls back to stored value + assert.equal(resolveRunAt(codeWithoutHeader, null, 'document_end'), 'document_end'); +}); + test('normalizeMatch formats URL patterns for Chrome userScripts API', () => { assert.equal(normalizeMatch('https://example.com'), 'https://example.com/*'); assert.equal(normalizeMatch('example.com/*'), '*://example.com/*');