syncme-mcp 1.0.1 → 1.0.2
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/README.md +6 -4
- package/approve.html +2 -12
- package/approve.js +26 -65
- package/background.js +3 -3
- package/manifest.json +5 -2
- package/options.html +16 -0
- package/options.js +35 -0
- package/package.json +2 -1
- package/syncme.js +7 -4
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
copies your logins for a site from your normal chrome into a separate chrome that ai agents and scripts can drive. so jev, browser-use or playwright can use your accounts without touching your real browser.
|
|
4
4
|
|
|
5
|
+
syncme comes in two parts: the [chrome extension](https://chromewebstore.google.com/detail/dcgmgkdgmbjgnchjdmokimppnhfinpfp) and [syncme-mcp](https://www.npmjs.com/package/syncme-mcp), the npm bridge it talks to. the source for both is on [github](https://github.com/levinkin/syncme).
|
|
6
|
+
|
|
5
7
|
## how it works
|
|
6
8
|
|
|
7
9
|
1. when you're on a site you want, click the syncme icon and it asks if you want to copy it over
|
|
@@ -18,14 +20,14 @@ needs chrome and node 20 or newer. works on mac and windows.
|
|
|
18
20
|
|
|
19
21
|
there's nothing to install: the extension talks to the bridge over a websocket on 127.0.0.1, and any command starts the bridge itself.
|
|
20
22
|
|
|
21
|
-
1. install the extension
|
|
22
|
-
2. click the syncme icon and use one of the connect buttons, or run `npx -y syncme-mcp setup` to wire up claude code, codex, cursor, windsurf and claude desktop
|
|
23
|
+
1. install the extension from the [chrome web store](https://chromewebstore.google.com/detail/dcgmgkdgmbjgnchjdmokimppnhfinpfp), or load this folder unpacked from `chrome://extensions` with developer mode on
|
|
24
|
+
2. click the syncme icon, hit the gear, and use one of the connect buttons, or run `npx -y syncme-mcp setup` to wire up claude code, codex, cursor, windsurf and claude desktop
|
|
23
25
|
|
|
24
26
|
## commands
|
|
25
27
|
|
|
26
28
|
```
|
|
27
29
|
npx -y syncme-mcp open # start the ai browser, prints {"cdp": "http://127.0.0.1:<port>"}
|
|
28
|
-
npx -y syncme-mcp sync https://example.com # copy one site, asks you in chrome first
|
|
30
|
+
npx -y syncme-mcp sync https://example.com # copy one site, asks you in chrome first unless it's saved or yolo is on
|
|
29
31
|
npx -y syncme-mcp stop # close the ai browser
|
|
30
32
|
npx -y syncme-mcp shutdown # close it and stop the bridge
|
|
31
33
|
npx -y syncme-mcp setup # add the mcp server to the ai apps you have installed
|
|
@@ -46,7 +48,7 @@ await (await browser.contexts()[0].newPage()).goto('https://example.com');
|
|
|
46
48
|
|
|
47
49
|
## mcp
|
|
48
50
|
|
|
49
|
-
you can also hook it up as an mcp server so your ai can ask for a site itself. it calls `copy_site` with something like `https://github.com`, you get a popup in chrome asking if it can have that login, and if you say yes it copies it and gets back the cdp url. there's also `open_browser` and `close_browser`.
|
|
51
|
+
you can also hook it up as an mcp server so your ai can ask for a site itself. it calls `copy_site` with something like `https://github.com`, you get a popup in chrome asking if it can have that login, and if you say yes it copies it and gets back the cdp url. turn on yolo mode in the syncme options (the gear in the popup) to skip that popup and let your ai copy any site it asks for. there's also `open_browser` and `close_browser`.
|
|
50
52
|
|
|
51
53
|
```
|
|
52
54
|
claude mcp add --scope user syncme -- npx -y syncme-mcp
|
package/approve.html
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en"><meta charset="utf-8"><title>syncme</title>
|
|
3
|
-
<style
|
|
3
|
+
<style>:root{color-scheme:light dark}body{font:15px system-ui;width:340px;margin:16px}button{padding:8px 14px}#question{font-weight:600}footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #8884;padding-top:10px}#options{background:none;border:none;padding:2px;font-size:20px;line-height:1;cursor:pointer;opacity:.7}#options:hover{opacity:1}</style>
|
|
4
4
|
<p id="question"></p>
|
|
5
|
-
<button id="copy">Copy</button> <button id="deny" hidden>No</button>
|
|
6
5
|
<p id="status" role="status"></p>
|
|
7
|
-
<
|
|
8
|
-
<label><input type="checkbox" id="auto"> Copy all saved sites when the AI browser starts</label>
|
|
9
|
-
<h2>Saved sites</h2>
|
|
10
|
-
<ul id="approved"></ul>
|
|
11
|
-
<h2>Connect your ai</h2>
|
|
12
|
-
<p id="waiting" hidden>connect an ai app first.</p>
|
|
13
|
-
<p id="pairing" hidden><span></span> <button id="repair">Re-pair</button></p>
|
|
14
|
-
<p><a id="cursor" hidden>Add to Cursor</a> <a id="vscode" hidden>Add to VS Code</a></p>
|
|
15
|
-
<button id="claude">Copy Claude Code command</button> <button id="setup">Copy setup command</button>
|
|
16
|
-
</div>
|
|
6
|
+
<footer><span><button id="copy">Copy</button> <button id="deny" hidden>No</button></span><button id="options" title="Options" aria-label="Options">⚙︎</button></footer>
|
|
17
7
|
<script src="approve.js"></script></html>
|
package/approve.js
CHANGED
|
@@ -1,76 +1,37 @@
|
|
|
1
1
|
const status = document.querySelector('#status');
|
|
2
2
|
const question = document.querySelector('#question');
|
|
3
3
|
const copyButton = document.querySelector('#copy');
|
|
4
|
-
const auto = document.querySelector('#auto');
|
|
5
4
|
let site;
|
|
6
|
-
async function render() {
|
|
7
|
-
const {approved = []} = await chrome.storage.local.get('approved');
|
|
8
|
-
const list = document.querySelector('#approved');
|
|
9
|
-
list.replaceChildren();
|
|
10
|
-
for (const value of approved) {
|
|
11
|
-
const li = document.createElement('li');
|
|
12
|
-
li.append(document.createTextNode(new URL(value).host));
|
|
13
|
-
const button = document.createElement('button');
|
|
14
|
-
button.textContent = 'Remove';
|
|
15
|
-
button.onclick = async () => {
|
|
16
|
-
const {approved = []} = await chrome.storage.local.get('approved');
|
|
17
|
-
await chrome.storage.local.set({approved: approved.filter(x => x !== value)});
|
|
18
|
-
status.textContent = 'Removed. The AI browser keeps cookies it already has.';
|
|
19
|
-
await render();
|
|
20
|
-
};
|
|
21
|
-
li.append(button); list.append(li);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
5
|
const requested = new URLSearchParams(location.search).get('site');
|
|
25
6
|
if (requested) {
|
|
26
|
-
question.textContent = `Your AI
|
|
27
|
-
document.querySelector('#
|
|
7
|
+
question.textContent = `Your AI needs your ${new URL(requested).host} login to continue. Copy it?`;
|
|
8
|
+
document.querySelector('#options').hidden = true;
|
|
28
9
|
const deny = document.querySelector('#deny');
|
|
29
10
|
deny.hidden = false;
|
|
30
11
|
copyButton.onclick = () => chrome.runtime.sendMessage({op: 'allow', answer: true});
|
|
31
12
|
deny.onclick = () => chrome.runtime.sendMessage({op: 'allow', answer: false});
|
|
32
|
-
} else
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
copyButton.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
auto.onchange = () => chrome.storage.local.set({autoSync: auto.checked});
|
|
57
|
-
const server = {name: 'syncme', command: 'npx', args: ['-y', 'syncme-mcp']};
|
|
58
|
-
const links = {
|
|
59
|
-
cursor: 'cursor://anysphere.cursor-deeplink/mcp/install?name=syncme&config=' + btoa(JSON.stringify({command: server.command, args: server.args})),
|
|
60
|
-
vscode: 'vscode:mcp/install?' + encodeURIComponent(JSON.stringify(server)),
|
|
61
|
-
};
|
|
62
|
-
for (const [id, href] of Object.entries(links)) {
|
|
63
|
-
const link = document.querySelector('#' + id);
|
|
64
|
-
link.href = href;
|
|
65
|
-
link.hidden = false;
|
|
13
|
+
} else {
|
|
14
|
+
chrome.tabs.query({active: true, currentWindow: true}).then(([tab]) => {
|
|
15
|
+
const u = tab?.url ? new URL(tab.url) : null;
|
|
16
|
+
if (u && ['http:', 'https:'].includes(u.protocol)) {
|
|
17
|
+
site = u.origin;
|
|
18
|
+
question.textContent = `Copy ${u.host} to your AI browser?`;
|
|
19
|
+
} else {
|
|
20
|
+
question.textContent = 'Open a website to copy it.';
|
|
21
|
+
copyButton.hidden = true;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
copyButton.onclick = async () => {
|
|
25
|
+
copyButton.disabled = true;
|
|
26
|
+
status.textContent = 'Copying...';
|
|
27
|
+
try {
|
|
28
|
+
const result = await chrome.runtime.sendMessage({op: 'copy', origin: site});
|
|
29
|
+
status.textContent = result?.ok ? `Copied ${result.count} cookies.` : result?.error === 'Busy' ? 'Busy, try again in a moment.' : result?.error === 'Not connected' ? 'Connect an ai app first.' : 'Copy failed.';
|
|
30
|
+
} catch (_) {
|
|
31
|
+
status.textContent = 'Copy failed.';
|
|
32
|
+
} finally {
|
|
33
|
+
copyButton.disabled = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
document.querySelector('#options').onclick = () => chrome.runtime.openOptionsPage();
|
|
66
37
|
}
|
|
67
|
-
document.querySelector('#claude').onclick = () => navigator.clipboard.writeText('claude mcp add syncme -- npx -y syncme-mcp');
|
|
68
|
-
document.querySelector('#setup').onclick = () => navigator.clipboard.writeText('npx -y syncme-mcp setup');
|
|
69
|
-
chrome.storage.session.get(['connected', 'pairing']).then(({connected, pairing}) => {
|
|
70
|
-
document.querySelector('#waiting').hidden = connected === true || Boolean(pairing);
|
|
71
|
-
const box = document.querySelector('#pairing');
|
|
72
|
-
box.hidden = !pairing;
|
|
73
|
-
box.firstChild.textContent = pairing === 'lost' ? 'syncme lost its pairing with this extension.' : 'syncme is paired with a different extension. Run npx -y syncme-mcp unpair, then hit Re-pair.';
|
|
74
|
-
});
|
|
75
|
-
document.querySelector('#repair').onclick = () => { chrome.runtime.sendMessage({op: 'repair'}); document.querySelector('#pairing').hidden = true; };
|
|
76
|
-
render();
|
package/background.js
CHANGED
|
@@ -59,19 +59,19 @@ function connect() {
|
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
if (message.op === 'copied') { pending.get(message.id)?.(message); pending.delete(message.id); return; }
|
|
62
|
-
const {approved = [], autoSync = false} = await chrome.storage.local.get(['approved', 'autoSync']);
|
|
62
|
+
const {approved = [], autoSync = false, yolo = false} = await chrome.storage.local.get(['approved', 'autoSync', 'yolo']);
|
|
63
63
|
if (message.op === 'list') { send({id: message.id, origins: autoSync ? approved : []}); return; }
|
|
64
64
|
let stage = 'request';
|
|
65
65
|
try {
|
|
66
66
|
if (message.op !== 'sync' || typeof message.id !== 'string') throw Error('Invalid request');
|
|
67
67
|
const u = origin(message.origin);
|
|
68
68
|
if (!approved.includes(u.origin)) {
|
|
69
|
-
if (!await allow(u.origin)) throw Error('Origin not approved');
|
|
69
|
+
if (!yolo && !await allow(u.origin)) throw Error('Origin not approved');
|
|
70
70
|
const {approved: now = []} = await chrome.storage.local.get('approved');
|
|
71
71
|
await chrome.storage.local.set({approved: [...new Set([...now, u.origin])]});
|
|
72
72
|
}
|
|
73
73
|
stage = 'cookies';
|
|
74
|
-
const cookies = (await chrome.cookies.getAll({
|
|
74
|
+
const cookies = (await chrome.cookies.getAll({})).filter(c => (c.hostOnly ? c.domain === u.hostname : ('.' + u.hostname).endsWith(c.domain)) && !c.partitionKey && (!c.secure || u.protocol === 'https:'));
|
|
75
75
|
send({id: message.id, origin: u.origin, cookies});
|
|
76
76
|
} catch (_) { try { send({id: message.id, error: stage === 'cookies' ? 'Cookie API refused' : 'Origin unavailable or not approved'}); } catch (_) {} }
|
|
77
77
|
};
|
package/manifest.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"48": "icons/48.png",
|
|
10
10
|
"128": "icons/128.png"
|
|
11
11
|
},
|
|
12
|
-
"key": "
|
|
12
|
+
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7Ya2I48aElXGpM5ww2OWgBOa/qtRPz8dFJ80ny2yVo8QbNt2hFwfl9KpHOC93Ik/rgzDRQStH7X37kFV9TedCv+vIq5oYe9GOCzLtYRZZzQI1P8zF1znXzuWsTusWaXpvkKAo0stzRBSlEFI166kUeEHhPw3TreKQl2kFy0IT9ht9pHBkwKS77Yy4NbNogkdZPPBuAjB1OdL/Rl9epUtagtCHuBYemMLhdaa8UJxDynGLxuqeIsxK8kXtb4WTWeFjA13mlNSiGNXb8bIcu0HOYV1d9m/bVaosENQEuAJHw8NRmhbSjQKp7/2Dflphb50ZoE9yiazAhIZ4DGcCQt/QIDAQAB",
|
|
13
13
|
"permissions": [
|
|
14
14
|
"alarms",
|
|
15
15
|
"cookies",
|
|
@@ -29,5 +29,8 @@
|
|
|
29
29
|
"32": "icons/32.png"
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
|
-
"
|
|
32
|
+
"options_ui": {
|
|
33
|
+
"page": "options.html",
|
|
34
|
+
"open_in_tab": false
|
|
35
|
+
}
|
|
33
36
|
}
|
package/options.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><meta charset="utf-8"><title>syncme options</title>
|
|
3
|
+
<style>:root{color-scheme:light dark}body{font:14px system-ui;min-width:420px;margin:16px}h2{font-size:15px;margin:20px 0 8px}label{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:14px 0 2px;font-weight:500}.note{opacity:.7;font-size:12px;margin:0 0 10px}ul{padding:0;list-style:none}li{display:flex;justify-content:space-between;align-items:center;margin:6px 0}li button{padding:2px 8px}</style>
|
|
4
|
+
<h2>Options</h2>
|
|
5
|
+
<label for="auto">Copy saved sites on start <input type="checkbox" id="auto"></label>
|
|
6
|
+
<p class="note">Copies every saved site when the AI browser starts.</p>
|
|
7
|
+
<label for="yolo">Yolo mode <input type="checkbox" id="yolo"></label>
|
|
8
|
+
<p class="note">Copies any site your AI asks for without a popup. Installing syncme already means you're ok with that.</p>
|
|
9
|
+
<h2>Saved sites</h2>
|
|
10
|
+
<ul id="approved"></ul>
|
|
11
|
+
<p id="status" class="note" role="status"></p>
|
|
12
|
+
<h2>Connect your AI</h2>
|
|
13
|
+
<p id="waiting" class="note" hidden>Connect an AI app first.</p>
|
|
14
|
+
<p id="pairing" class="note" hidden><span></span> <button id="repair">Re-pair</button></p>
|
|
15
|
+
<button id="claude">Copy Claude Code command</button> <button id="setup">Copy setup command</button>
|
|
16
|
+
<script src="options.js"></script></html>
|
package/options.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const status = document.querySelector('#status');
|
|
2
|
+
async function render() {
|
|
3
|
+
const {approved = []} = await chrome.storage.local.get('approved');
|
|
4
|
+
const list = document.querySelector('#approved');
|
|
5
|
+
list.replaceChildren();
|
|
6
|
+
for (const value of approved) {
|
|
7
|
+
const li = document.createElement('li');
|
|
8
|
+
li.append(document.createTextNode(new URL(value).host));
|
|
9
|
+
const button = document.createElement('button');
|
|
10
|
+
button.textContent = 'Remove';
|
|
11
|
+
button.onclick = async () => {
|
|
12
|
+
const {approved = []} = await chrome.storage.local.get('approved');
|
|
13
|
+
await chrome.storage.local.set({approved: approved.filter(x => x !== value)});
|
|
14
|
+
status.textContent = 'Removed. The AI browser keeps cookies it already has.';
|
|
15
|
+
await render();
|
|
16
|
+
};
|
|
17
|
+
li.append(button); list.append(li);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const [id, key] of [['auto', 'autoSync'], ['yolo', 'yolo']]) {
|
|
21
|
+
const box = document.querySelector('#' + id);
|
|
22
|
+
chrome.storage.local.get(key).then(stored => { box.checked = stored[key] === true; });
|
|
23
|
+
box.onchange = () => chrome.storage.local.set({[key]: box.checked});
|
|
24
|
+
}
|
|
25
|
+
document.querySelector('#claude').onclick = () => navigator.clipboard.writeText('claude mcp add syncme -- npx -y syncme-mcp');
|
|
26
|
+
document.querySelector('#setup').onclick = () => navigator.clipboard.writeText('npx -y syncme-mcp setup');
|
|
27
|
+
chrome.storage.session.get(['connected', 'pairing']).then(({connected, pairing}) => {
|
|
28
|
+
document.querySelector('#waiting').hidden = connected === true || Boolean(pairing);
|
|
29
|
+
const box = document.querySelector('#pairing');
|
|
30
|
+
box.hidden = !pairing;
|
|
31
|
+
box.firstChild.textContent = pairing === 'lost' ? 'syncme lost its pairing with this extension.' : 'syncme is paired with a different extension. Run npx -y syncme-mcp unpair, then hit Re-pair.';
|
|
32
|
+
});
|
|
33
|
+
document.querySelector('#repair').onclick = () => { chrome.runtime.sendMessage({op: 'repair'}); document.querySelector('#pairing').hidden = true; };
|
|
34
|
+
chrome.storage.onChanged.addListener(changes => { if (changes.approved) render(); });
|
|
35
|
+
render();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syncme-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "copy your logins for a site into a separate chrome that your ai agents and scripts can use",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"playwright-core": "^1.49.0",
|
|
14
|
+
"tldts": "^7.4.19",
|
|
14
15
|
"ws": "^8.18.0"
|
|
15
16
|
},
|
|
16
17
|
"license": "PolyForm-Noncommercial-1.0.0",
|
package/syncme.js
CHANGED
|
@@ -7,6 +7,7 @@ import {dirname, join} from 'node:path';
|
|
|
7
7
|
import {createInterface} from 'node:readline';
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {chromium} from 'playwright-core';
|
|
10
|
+
import {getDomain} from 'tldts';
|
|
10
11
|
import {WebSocket, WebSocketServer} from 'ws';
|
|
11
12
|
|
|
12
13
|
const ROOT = dirname(fileURLToPath(import.meta.url));
|
|
@@ -115,13 +116,15 @@ function origin(value) {
|
|
|
115
116
|
|
|
116
117
|
function cookiesFor(site, cookies) {
|
|
117
118
|
if (!Array.isArray(cookies) || cookies.length > 3000) throw rejected('cookies');
|
|
119
|
+
const registrable = getDomain(site.hostname, {allowPrivateDomains: true});
|
|
118
120
|
const result = [];
|
|
119
121
|
for (const c of cookies) {
|
|
120
|
-
if (!c || typeof c !== 'object' || typeof c.domain !== 'string' || c.
|
|
122
|
+
if (!c || typeof c !== 'object' || typeof c.domain !== 'string' || c.partitionKey) throw rejected('cookies');
|
|
121
123
|
for (const field of ['name', 'value', 'domain', 'path']) {
|
|
122
124
|
if (typeof c[field] !== 'string' || c[field].length > 16384) throw rejected('cookies');
|
|
123
125
|
}
|
|
124
126
|
if (!c.path.startsWith('/') || ['secure', 'httpOnly', 'hostOnly', 'session'].some(field => typeof c[field] !== 'boolean')) throw rejected('cookies');
|
|
127
|
+
if (c.hostOnly ? c.domain !== site.hostname : !registrable || !c.domain.startsWith('.') || c.domain.length <= registrable.length || !('.' + site.hostname).endsWith(c.domain)) throw rejected('cookies');
|
|
125
128
|
if (c.secure && site.protocol !== 'https:') throw rejected('cookies');
|
|
126
129
|
const item = {name: c.name, value: c.value, path: c.path, secure: c.secure, httpOnly: c.httpOnly};
|
|
127
130
|
item.domain = c.hostOnly ? site.hostname : c.domain;
|
|
@@ -260,7 +263,7 @@ async function serve() {
|
|
|
260
263
|
if (response.origin !== value || response.error) throw rejected('bridge');
|
|
261
264
|
const cookies = cookiesFor(site, response.cookies);
|
|
262
265
|
for (const cookie of await context.cookies()) {
|
|
263
|
-
if (cookie.domain.
|
|
266
|
+
if ((cookie.domain === site.hostname || cookie.domain.startsWith('.') && ('.' + site.hostname).endsWith(cookie.domain)) && (site.protocol === 'https:' || !cookie.secure)) await context.clearCookies({name: cookie.name, domain: cookie.domain, path: cookie.path});
|
|
264
267
|
}
|
|
265
268
|
await context.addCookies(cookies);
|
|
266
269
|
count += cookies.length;
|
|
@@ -386,12 +389,12 @@ function pack() {
|
|
|
386
389
|
try {
|
|
387
390
|
const {key, ...store} = MANIFEST;
|
|
388
391
|
writeFileSync(join(stage, 'manifest.json'), JSON.stringify(store, null, 2));
|
|
389
|
-
for (const name of ['background.js', 'approve.html', 'approve.js']) copyFileSync(join(ROOT, name), join(stage, name));
|
|
392
|
+
for (const name of ['background.js', 'approve.html', 'approve.js', 'options.html', 'options.js']) copyFileSync(join(ROOT, name), join(stage, name));
|
|
390
393
|
cpSync(join(ROOT, 'icons'), join(stage, 'icons'), {recursive: true});
|
|
391
394
|
rmSync(zip, {force: true});
|
|
392
395
|
const result = process.platform === 'win32'
|
|
393
396
|
? spawnSync('powershell', ['-NoProfile', '-Command', `Compress-Archive -Path '${join(stage, '*')}' -DestinationPath '${zip}' -Force`], {stdio: 'ignore'})
|
|
394
|
-
: spawnSync('zip', ['-r', '-X', '-q', zip, 'manifest.json', 'background.js', 'approve.html', 'approve.js', 'icons'], {cwd: stage, stdio: 'ignore'});
|
|
397
|
+
: spawnSync('zip', ['-r', '-X', '-q', zip, 'manifest.json', 'background.js', 'approve.html', 'approve.js', 'options.html', 'options.js', 'icons'], {cwd: stage, stdio: 'ignore'});
|
|
395
398
|
if (result.status !== 0) throw Error('Pack failed');
|
|
396
399
|
} finally {
|
|
397
400
|
rmSync(stage, {recursive: true, force: true});
|