toolaby 1.4.5 → 1.5.0
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 +2 -0
- package/bin.mjs +111 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ If the extension is built with WXT, CRXJS or Plasmo, the command puts the same f
|
|
|
24
24
|
|
|
25
25
|
**`toolaby create <tool-id>`** makes a new, minimal extension in a folder named after the tool, ready for *Load unpacked*.
|
|
26
26
|
|
|
27
|
+
**`toolaby pack [folder]`** makes the zip for the Chrome Web Store from your built extension: the manifest's `key` left out (the store refuses one it did not issue), and refused if the build carries a Test key or points at `localhost`. Nothing is sent anywhere.
|
|
28
|
+
|
|
27
29
|
**`toolaby tools`** lists your tools and their ids. **`toolaby whoami`** says who you are signed in as. **`toolaby logout`** signs the command out and revokes its session on the server.
|
|
28
30
|
|
|
29
31
|
## Where things live
|
package/bin.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// toolaby wire <tool-id> wire the extension in this folder to your tool — plain, WXT, CRXJS or Plasmo
|
|
6
6
|
// toolaby create <tool-id> make a new, minimal extension for your tool in <tool-id>/
|
|
7
7
|
// toolaby tools your tools, with the ids the commands want
|
|
8
|
+
// toolaby pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's key left out
|
|
8
9
|
// toolaby webhooks events the six events, with an example of each
|
|
9
10
|
// toolaby webhooks trigger <event> send an example delivery: to your registered endpoints, or with --to straight to a local server
|
|
10
11
|
// toolaby webhooks endpoints the workspace's endpoints
|
|
@@ -27,14 +28,15 @@
|
|
|
27
28
|
// platform's own (wall/src/packages/wiring), carried here transpiled in lib/,
|
|
28
29
|
// so this and the dashboard's Wire button never disagree.
|
|
29
30
|
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, statSync, chmodSync, unlinkSync } from 'node:fs';
|
|
30
|
-
import { join, resolve, dirname } from 'node:path';
|
|
31
|
+
import { join, resolve, dirname, relative, basename } from 'node:path';
|
|
32
|
+
import { deflateRawSync } from 'node:zlib';
|
|
31
33
|
import { homedir } from 'node:os';
|
|
32
34
|
import { spawn } from 'node:child_process';
|
|
33
35
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
34
36
|
import { wireManifest, WORKER_LINES } from './lib/manifest.js';
|
|
35
37
|
import { detectProject, CODE_FILES, filesToWrite, fillPages, scaffoldManifest, scaffoldFilesFor, surfacesOf, hasPopup, hasSidePanel, SURFACES, STALE_FILES, BUILD_COPY_NOTE } from './lib/project.js';
|
|
36
38
|
|
|
37
|
-
const VERSION = '1.
|
|
39
|
+
const VERSION = '1.5.0';
|
|
38
40
|
const CLIENT_ID = 'toolaby-cli';
|
|
39
41
|
const argv = process.argv.slice(2);
|
|
40
42
|
// `--version` and `--help` are commands of their own; every other command is the first bare word.
|
|
@@ -364,6 +366,110 @@ async function webhooks() {
|
|
|
364
366
|
fail('usage: toolaby webhooks events | trigger <event> [--to <url>] | endpoints | add <url> | remove <id>');
|
|
365
367
|
}
|
|
366
368
|
|
|
369
|
+
// ── pack: the zip the Chrome Web Store takes ──────────────────────────────────────────────────────────────────────────
|
|
370
|
+
// The built extension, zipped, with the manifest's `key` left out: the store refuses a key in a new item's first
|
|
371
|
+
// package and any key but the item's own in an update, and it keeps the id it gave the item either way — the Wall
|
|
372
|
+
// admits that id once the listing's address is on the tool. It refuses a build that would sell on Test (a tk_test_
|
|
373
|
+
// or tk_dev_ key inside) or that points at this machine (localhost in the manifest), the two mistakes a store build
|
|
374
|
+
// can ship with. Nothing is sent anywhere: it reads the folder and writes one file.
|
|
375
|
+
const BUILD_DIRS = ['.', '.output/chrome-mv3', 'dist', 'build/chrome-mv3-prod', 'build'];
|
|
376
|
+
const TOOL_KEY = /tk_(test|live|dev)_[A-Za-z0-9_-]{20,}/g;
|
|
377
|
+
const LOCAL = /^(https?|wss?):\/\/(localhost|127\.0\.0\.1|\[::1\]|[^/]*\.localhost)(:\d+)?\//i;
|
|
378
|
+
|
|
379
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; });
|
|
380
|
+
const crc32 = (buf) => { let c = 0xffffffff; for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; };
|
|
381
|
+
|
|
382
|
+
/** A zip of [name, bytes] pairs: deflated where that is smaller, stored otherwise, every entry dated 1 Jan 1980 so the same build packs to the same bytes. */
|
|
383
|
+
function zipOf(entries) {
|
|
384
|
+
const locals = []; const centrals = []; let offset = 0;
|
|
385
|
+
for (const [name, data] of entries) {
|
|
386
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
387
|
+
const deflated = deflateRawSync(data, { level: 9 });
|
|
388
|
+
const method = deflated.length < data.length ? 8 : 0;
|
|
389
|
+
const body = method === 8 ? deflated : data;
|
|
390
|
+
const crc = crc32(data);
|
|
391
|
+
const head = Buffer.alloc(30);
|
|
392
|
+
head.writeUInt32LE(0x04034b50, 0); head.writeUInt16LE(20, 4); head.writeUInt16LE(0x0800, 6); head.writeUInt16LE(method, 8);
|
|
393
|
+
head.writeUInt16LE(0, 10); head.writeUInt16LE(0x21, 12); head.writeUInt32LE(crc, 14); head.writeUInt32LE(body.length, 18);
|
|
394
|
+
head.writeUInt32LE(data.length, 22); head.writeUInt16LE(nameBuf.length, 26); head.writeUInt16LE(0, 28);
|
|
395
|
+
const central = Buffer.alloc(46);
|
|
396
|
+
central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0x0800, 8);
|
|
397
|
+
central.writeUInt16LE(method, 10); central.writeUInt16LE(0, 12); central.writeUInt16LE(0x21, 14); central.writeUInt32LE(crc, 16);
|
|
398
|
+
central.writeUInt32LE(body.length, 20); central.writeUInt32LE(data.length, 24); central.writeUInt16LE(nameBuf.length, 28);
|
|
399
|
+
central.writeUInt32LE(offset, 42);
|
|
400
|
+
locals.push(head, nameBuf, body); centrals.push(central, nameBuf);
|
|
401
|
+
offset += head.length + nameBuf.length + body.length;
|
|
402
|
+
}
|
|
403
|
+
const cd = Buffer.concat(centrals);
|
|
404
|
+
const end = Buffer.alloc(22);
|
|
405
|
+
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
|
|
406
|
+
end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16);
|
|
407
|
+
return Buffer.concat([...locals, cd, end]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function filesIn(root) {
|
|
411
|
+
const found = [];
|
|
412
|
+
const walk = (dir) => {
|
|
413
|
+
for (const name of readdirSync(dir).sort()) {
|
|
414
|
+
if (name.startsWith('.') || name === 'node_modules' || name.endsWith('.zip')) continue;
|
|
415
|
+
const p = join(dir, name);
|
|
416
|
+
if (statSync(p).isDirectory()) walk(p); else found.push(p);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
walk(root);
|
|
420
|
+
return found;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function pack() {
|
|
424
|
+
const given = positional[0];
|
|
425
|
+
const folder = given ? resolve(given) : BUILD_DIRS.map((d) => resolve(d)).find((d) => existsSync(join(d, 'manifest.json')));
|
|
426
|
+
if (!folder || !existsSync(join(folder, 'manifest.json'))) fail(given ? `No manifest.json in ${given}.` : `No manifest.json here or in ${BUILD_DIRS.slice(1).join(', ')}. Build first, or name the folder: toolaby pack <folder>.`);
|
|
427
|
+
let manifest;
|
|
428
|
+
try { manifest = JSON.parse(readFileSync(join(folder, 'manifest.json'), 'utf8')); } catch (e) { fail(`manifest.json does not parse: ${e.message}`); }
|
|
429
|
+
if (!manifest.version) fail('manifest.json has no "version".');
|
|
430
|
+
const files = filesIn(folder);
|
|
431
|
+
const problems = [];
|
|
432
|
+
|
|
433
|
+
// Which side the build sells on: the tool keys inside it, wherever the bundler put them.
|
|
434
|
+
const modes = new Set(); const sites = new Set();
|
|
435
|
+
for (const f of files) {
|
|
436
|
+
if (!/\.(m?js|html|json)$/.test(f)) continue;
|
|
437
|
+
for (const [k, mode] of readFileSync(f, 'utf8').matchAll(TOOL_KEY)) {
|
|
438
|
+
modes.add(mode);
|
|
439
|
+
try { const facts = JSON.parse(Buffer.from(k.split('_')[2], 'base64url').toString('utf8')); if (typeof facts.s === 'string') sites.add(facts.s.replace(/\/$/, '')); } catch { /* not a key after all */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if ((modes.has('test') || modes.has('dev')) && !flag('--allow-test')) problems.push(`It carries a ${[...modes].filter((m) => m !== 'live').map((m) => `tk_${m}_`).join(' and ')} key: it would sell on ${modes.has('test') ? 'Test, with test cards' : 'a development Wall'}. Run \`npx toolaby wire <tool-id> --live --force\`, build again, then pack. (--allow-test packs it anyway, for a build you will not publish.)`);
|
|
443
|
+
if (modes.size === 0) out('\n No tool key found in the build: it is not wired to the Wall, or the key is somewhere this cannot read. Packing anyway.');
|
|
444
|
+
|
|
445
|
+
// Addresses of this machine have no place in a store build.
|
|
446
|
+
const local = [...(manifest.host_permissions ?? []), ...(manifest.externally_connectable?.matches ?? [])].filter((m) => typeof m === 'string' && LOCAL.test(m));
|
|
447
|
+
if (local.length && !flag('--allow-local')) problems.push(`The manifest points at this machine: ${local.join(', ')}. A store build has no localhost in host_permissions or externally_connectable. (--allow-local packs it anyway.)`);
|
|
448
|
+
|
|
449
|
+
// The Wall the key names must be reachable from the extension, or sign-in and the purchase handoff fail.
|
|
450
|
+
for (const site of sites) {
|
|
451
|
+
const hosts = manifest.host_permissions ?? []; const ec = manifest.externally_connectable?.matches ?? [];
|
|
452
|
+
if (!hosts.includes(`${site}/*`) || !ec.includes(`${site}/*`)) problems.push(`The manifest does not name ${site}/* in both host_permissions and externally_connectable: sign-in and the purchase would not reach the extension. \`npx toolaby wire <tool-id> --live --force\` adds them.`);
|
|
453
|
+
}
|
|
454
|
+
if (problems.length) fail(`Not packed:\n\n${problems.map((p) => ` · ${p}`).join('\n\n')}`);
|
|
455
|
+
|
|
456
|
+
const hadKey = typeof manifest.key === 'string';
|
|
457
|
+
const { key: _key, ...forStore } = manifest;
|
|
458
|
+
const entries = files.map((f) => {
|
|
459
|
+
const name = relative(folder, f).split('\\').join('/');
|
|
460
|
+
return [name, name === 'manifest.json' ? Buffer.from(JSON.stringify(forStore, null, 2) + '\n') : readFileSync(f)];
|
|
461
|
+
});
|
|
462
|
+
// Named after the extension, or after its folder when the name is a __MSG_…__ placeholder.
|
|
463
|
+
const named = typeof manifest.name === 'string' && !manifest.name.startsWith('__MSG_') ? manifest.name : basename(folder);
|
|
464
|
+
const safe = named.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'extension';
|
|
465
|
+
const target = resolve(opt('--out') ?? `${safe}-${manifest.version}.zip`);
|
|
466
|
+
writeFileSync(target, zipOf(entries));
|
|
467
|
+
const kb = (statSync(target).size / 1024).toFixed(0);
|
|
468
|
+
out(`\n ${relative(process.cwd(), target) || target} · ${entries.length} files, ${kb} KB · version ${manifest.version}${modes.has('live') ? ' · Live key' : ''}`);
|
|
469
|
+
if (hadKey) out(' The manifest\'s "key" is left out: the store keeps the id it gives the item. Your unpacked copy keeps the key and its own id; the Wall admits both once the listing\'s address is on the tool.');
|
|
470
|
+
out(' Upload it at https://chrome.google.com/webstore/devconsole — a new item, or Package → Upload new package.\n');
|
|
471
|
+
}
|
|
472
|
+
|
|
367
473
|
function help() {
|
|
368
474
|
out(`
|
|
369
475
|
toolaby — the Toolaby command line (${VERSION})
|
|
@@ -375,6 +481,8 @@ function help() {
|
|
|
375
481
|
toolaby create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
|
|
376
482
|
--surfaces both|popup|side-panel|none what it opens (default: both)
|
|
377
483
|
toolaby tools your tools, with their ids
|
|
484
|
+
toolaby pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
|
|
485
|
+
refuses a Test key or localhost inside --out <file.zip>
|
|
378
486
|
toolaby webhooks events the six events, with an example of each
|
|
379
487
|
toolaby webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
|
|
380
488
|
toolaby webhooks endpoints your endpoints; add <url> registers one (secret shown once), remove <id> removes one
|
|
@@ -385,7 +493,7 @@ function help() {
|
|
|
385
493
|
`);
|
|
386
494
|
}
|
|
387
495
|
|
|
388
|
-
const commands = { login: async () => { await login(); }, logout, whoami, tools, wire, upgrade: wire, create, webhooks, help, '--help': help, '-h': help, '--version': () => out(VERSION), '-v': () => out(VERSION) };
|
|
496
|
+
const commands = { login: async () => { await login(); }, logout, whoami, tools, wire, upgrade: wire, create, webhooks, pack, help, '--help': help, '-h': help, '--version': () => out(VERSION), '-v': () => out(VERSION) };
|
|
389
497
|
const run = commands[command];
|
|
390
498
|
if (!run) { console.error(`\n Unknown command "${command}".`); help(); process.exit(1); }
|
|
391
499
|
Promise.resolve().then(run).catch((e) => fail(e.message));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolaby",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "The Toolaby command line: sign in once, then wire any Chrome extension to your tool on the Wall — sign-in, the Free plan, licences and subscriptions — or start a new one.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|