toolaby 1.4.3 → 1.4.5
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/bin.mjs +22 -5
- package/lib/manifest.js +12 -3
- package/lib/project.js +36 -1
- package/package.json +1 -1
package/bin.mjs
CHANGED
|
@@ -32,9 +32,9 @@ import { homedir } from 'node:os';
|
|
|
32
32
|
import { spawn } from 'node:child_process';
|
|
33
33
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
34
34
|
import { wireManifest, WORKER_LINES } from './lib/manifest.js';
|
|
35
|
-
import { detectProject, CODE_FILES, filesToWrite, STALE_FILES, BUILD_COPY_NOTE } from './lib/project.js';
|
|
35
|
+
import { detectProject, CODE_FILES, filesToWrite, fillPages, scaffoldManifest, scaffoldFilesFor, surfacesOf, hasPopup, hasSidePanel, SURFACES, STALE_FILES, BUILD_COPY_NOTE } from './lib/project.js';
|
|
36
36
|
|
|
37
|
-
const VERSION = '1.4.
|
|
37
|
+
const VERSION = '1.4.5';
|
|
38
38
|
const CLIENT_ID = 'toolaby-cli';
|
|
39
39
|
const argv = process.argv.slice(2);
|
|
40
40
|
// `--version` and `--help` are commands of their own; every other command is the first bare word.
|
|
@@ -218,7 +218,7 @@ async function wire() {
|
|
|
218
218
|
// A command older than the Wall asks for files the Wall no longer hands out: said, with the way to the current one, rather than a crash.
|
|
219
219
|
const missing = targets.map((p) => p.split('/').pop()).filter((name) => typeof plan.files[name] !== 'string');
|
|
220
220
|
if (missing.length) fail(`This command (${VERSION}) is older than the Wall at ${wall}: it expects ${missing.join(', ')}, which the Wall no longer hands out.\n Update it: npm install -g toolaby@latest — or run the Wall's own copy: npx --yes ${wall}/toolaby.tgz ${command} ${slug}`);
|
|
221
|
-
for (const path of targets) write(path, plan.files[path.split('/').pop()]
|
|
221
|
+
for (const path of targets) write(path, fillPages(plan.files[path.split('/').pop()], { appPopup, appSidePanel }));
|
|
222
222
|
// What an earlier wiring wrote and this one does not, wherever it put them: taken away, so nothing stale is shipped.
|
|
223
223
|
const removed = [];
|
|
224
224
|
for (const at of new Set([project.codeDir, project.popupDir, 'public/', 'src/public/'])) for (const f of STALE_FILES) { const p = join(dir, `${at}${f}`); if (existsSync(p)) { unlinkSync(p); removed.push(`${at}${f}`); } }
|
|
@@ -256,12 +256,28 @@ async function create() {
|
|
|
256
256
|
if (existsSync(dir)) fail(`${dir} already exists`);
|
|
257
257
|
const plan = await filesFor(slug, true);
|
|
258
258
|
const write = (rel, content) => { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), content); };
|
|
259
|
-
|
|
259
|
+
// The scaffold's own files as the Wall names them (older Walls did not, hence the list): a Wall that adds one to the
|
|
260
|
+
// minimal extension is written by this command too, instead of a manifest naming a file that never arrived.
|
|
261
|
+
const scaffold = Array.isArray(plan.scaffold) && plan.scaffold.length ? plan.scaffold : ['manifest.json', 'background.js', 'popup.html', 'sidepanel.html', 'popup.css', 'popup.js', 'README.md'];
|
|
262
|
+
// Which surfaces this extension is made with: both (the default), one of them, or none at all for a tool that works
|
|
263
|
+
// from the background. Asked for once here, rather than written and then deleted by hand.
|
|
264
|
+
const asked = opt('--surfaces');
|
|
265
|
+
if (asked && !SURFACES.includes(asked)) fail(`--surfaces takes ${SURFACES.join(', ')} — not "${asked}".`);
|
|
266
|
+
const surfaces = surfacesOf(asked);
|
|
267
|
+
const wanted = [...CODE_FILES, ...scaffoldFilesFor(surfaces, scaffold)];
|
|
268
|
+
const absent = wanted.filter((f) => typeof plan.files[f] !== 'string');
|
|
269
|
+
if (absent.length) fail(`The Wall at ${wall} named ${absent.join(', ')} for a new extension but did not hand ${absent.length > 1 ? 'them' : 'it'} over.\n Nothing was written. Update this command: npm install -g toolaby@latest — or run the Wall's own copy: npx --yes ${wall}/toolaby.tgz create ${slug}`);
|
|
270
|
+
// Both surfaces, and the panel drawn inside them: no page of the Wall's stands in front, so both names stay empty.
|
|
271
|
+
for (const f of wanted) write(f, fillPages(plan.files[f]));
|
|
272
|
+
// The manifest names the side panel only now that the page is beside it — an extension whose side_panel points at a
|
|
273
|
+
// missing file does not load at all, so a Wall that stops serving the page must not leave the key behind either.
|
|
274
|
+
write('manifest.json', JSON.stringify(scaffoldManifest(JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8')), { sidePanel: hasSidePanel(surfaces), popup: hasPopup(surfaces) }), null, 2) + '\n');
|
|
260
275
|
for (const s of [16, 48, 128]) {
|
|
261
276
|
const res = await fetch(`${wall}/boilerplate/icons/icon${s}.png`);
|
|
262
277
|
write(join('icons', `icon${s}.png`), Buffer.from(await res.arrayBuffer()));
|
|
263
278
|
}
|
|
264
|
-
|
|
279
|
+
const clicking = { both: 'Click the icon: the popup shows what the Free plan holds. Open side panel, in it, is the same page at the panel\'s width', popup: 'Click the icon: the popup shows what the Free plan holds', 'side-panel': 'Click the icon: the side panel opens — it has no popup, so the click is the panel\'s', none: 'No popup and no side panel: nothing opens on a click. Your code says when to sell — toolaby.openPaymentPage()' }[surfaces];
|
|
280
|
+
out(`\n ${plan.name} → ${dir}${surfaces === 'both' ? '' : ` (--surfaces ${surfaces})`}\n\n 1. chrome://extensions → Developer mode → Load unpacked → ${dir}\n 2. ${clicking}; Buy opens ${plan.site}/tools/${slug}\n\n Edit background.js: your work goes behind toolaby.gate().\n Surfaces, and how to add or drop one later: ${plan.site}/docs/surfaces\n`);
|
|
265
281
|
}
|
|
266
282
|
|
|
267
283
|
// ── webhooks: the catalogue, and a delivery on demand ───────────────────────────────────────────────────────────────
|
|
@@ -357,6 +373,7 @@ function help() {
|
|
|
357
373
|
--into <folder> another folder --force replace the platform's files
|
|
358
374
|
toolaby upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
|
|
359
375
|
toolaby create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
|
|
376
|
+
--surfaces both|popup|side-panel|none what it opens (default: both)
|
|
360
377
|
toolaby tools your tools, with their ids
|
|
361
378
|
toolaby webhooks events the six events, with an example of each
|
|
362
379
|
toolaby webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
|
package/lib/manifest.js
CHANGED
|
@@ -50,13 +50,22 @@ export function wireManifest(input, o) {
|
|
|
50
50
|
did.push(`the Wall's popup in front of ${appPopup}: it opens on to yours while the device may, and shows the panel when it may not`);
|
|
51
51
|
}
|
|
52
52
|
// The extension's own side panel, the same way: the Wall's in front, with the permission a side panel needs.
|
|
53
|
+
// Chrome spells it side_panel.default_path, Firefox sidebar_action.default_panel; a manifest may carry both.
|
|
54
|
+
const theirs = (v) => (typeof v === 'string' && v !== WALL_SIDE_PANEL ? v : null);
|
|
53
55
|
const declaredPanel = typeof manifest.side_panel?.default_path === 'string' ? manifest.side_panel.default_path : null;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
+
const declaredSidebar = typeof manifest.sidebar_action?.default_panel === 'string' ? manifest.sidebar_action.default_panel : null;
|
|
57
|
+
const ours = declaredPanel === WALL_SIDE_PANEL || declaredSidebar === WALL_SIDE_PANEL;
|
|
58
|
+
const appSidePanel = theirs(declaredPanel) ?? theirs(declaredSidebar) ?? (ours ? o.rememberedSidePanel || null : null);
|
|
59
|
+
if (theirs(declaredPanel)) {
|
|
56
60
|
manifest.side_panel = { ...manifest.side_panel, default_path: WALL_SIDE_PANEL };
|
|
57
61
|
if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('sidePanel')))
|
|
58
62
|
manifest.permissions = add(manifest.permissions, 'sidePanel');
|
|
59
|
-
did.push(`the Wall's side panel in front of ${
|
|
63
|
+
did.push(`the Wall's side panel in front of ${declaredPanel}: the same as the popup, at the panel's width`);
|
|
64
|
+
}
|
|
65
|
+
// Firefox's sidebar, when it names the same page — one that names another is left alone rather than pointed elsewhere.
|
|
66
|
+
if (theirs(declaredSidebar) && (!declaredPanel || declaredSidebar === declaredPanel)) {
|
|
67
|
+
manifest.sidebar_action = { ...manifest.sidebar_action, default_panel: WALL_SIDE_PANEL };
|
|
68
|
+
did.push(`the Wall's side panel in front of ${declaredSidebar} in Firefox's sidebar_action too`);
|
|
60
69
|
}
|
|
61
70
|
const named = typeof manifest.background?.service_worker === 'string' && manifest.background.service_worker.trim() ? manifest.background.service_worker.trim() : null;
|
|
62
71
|
let writeWorker = null;
|
package/lib/project.js
CHANGED
|
@@ -19,6 +19,40 @@ export const filesToWrite = (p, appPopup, appSidePanel = null) => [
|
|
|
19
19
|
...(appSidePanel ? SIDE_PANEL_FILES.map((f) => `${p.popupDir}${f}`) : []),
|
|
20
20
|
...(p.extraFiles ?? []).map((f) => f.to),
|
|
21
21
|
];
|
|
22
|
+
export const SURFACES = ['both', 'popup', 'side-panel', 'none'];
|
|
23
|
+
export const surfacesOf = (v) => SURFACES.includes(v ?? '') ? v : 'both';
|
|
24
|
+
export const hasPopup = (s) => s === 'both' || s === 'popup';
|
|
25
|
+
export const hasSidePanel = (s) => s === 'both' || s === 'side-panel';
|
|
26
|
+
/** The scaffold's own files for those surfaces: the manifest and the background always, the pages as asked for. */
|
|
27
|
+
export const scaffoldFilesFor = (s, all) => all.filter((f) => (f === 'popup.html' ? hasPopup(s) : f === 'sidepanel.html' ? hasSidePanel(s) : f === 'popup.js' || f === 'popup.css' ? s !== 'none' : true));
|
|
28
|
+
/**
|
|
29
|
+
* The scaffold's manifest, once it is known which pages were really written. Chrome refuses to load an extension
|
|
30
|
+
* whose `side_panel.default_path` names a file that is not there — the whole extension, not just the panel — so the
|
|
31
|
+
* minimal extension ships a manifest that names only its popup, and whatever writes the pages settles it here: the
|
|
32
|
+
* side panel added with its page, the popup taken out when there is none (the icon stays; the click then opens the
|
|
33
|
+
* side panel, or nothing, which is what a background-only tool wants).
|
|
34
|
+
*/
|
|
35
|
+
export function scaffoldManifest(manifest, wrote) {
|
|
36
|
+
const out = { ...manifest };
|
|
37
|
+
if (wrote.popup === false) {
|
|
38
|
+
const action = { ...out.action };
|
|
39
|
+
delete action.default_popup;
|
|
40
|
+
out.action = action;
|
|
41
|
+
}
|
|
42
|
+
if (!wrote.sidePanel)
|
|
43
|
+
return out;
|
|
44
|
+
const permissions = Array.isArray(out.permissions) ? [...out.permissions] : [];
|
|
45
|
+
if (!permissions.includes('sidePanel'))
|
|
46
|
+
permissions.push('sidePanel');
|
|
47
|
+
return { ...out, permissions, side_panel: { default_path: 'sidepanel.html' } };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The last two placeholders in a handed-out file — the extension's own popup and side panel, which only the
|
|
51
|
+
* writer knows, since it is the one that read the manifest. Everything else is filled where the files are built
|
|
52
|
+
* (wiring/files.ts). Every hand-out goes through this: the Wire button, the command, and the zip on the Set up
|
|
53
|
+
* page, which once shipped `appSidePanel: '__APP_SIDE_PANEL__'` because it filled the popup's name and not this one.
|
|
54
|
+
*/
|
|
55
|
+
export const fillPages = (text, pages = {}) => text.replaceAll('__APP_POPUP__', pages.appPopup ?? '').replaceAll('__APP_SIDE_PANEL__', pages.appSidePanel ?? '');
|
|
22
56
|
/** The import of toolaby.js from a file at `from`, both relative to the root. */
|
|
23
57
|
export function importPath(from, codeDir = '') {
|
|
24
58
|
const depth = from.split('/').length - 1;
|
|
@@ -109,7 +143,8 @@ export function detectProject(o) {
|
|
|
109
143
|
// A plain extension with a build (webpack, Rollup, Parcel, esbuild…) is wired the same way — the manifest is a file and
|
|
110
144
|
// the files go beside it — but only what the build copies reaches the output, so that is said.
|
|
111
145
|
const declared = typeof manifest?.action?.default_popup === 'string' ? manifest.action.default_popup : null;
|
|
112
|
-
|
|
146
|
+
// Chrome's side_panel, else Firefox's sidebar_action: the same page, two spellings.
|
|
147
|
+
const declaredPanel = typeof manifest?.side_panel?.default_path === 'string' ? manifest.side_panel.default_path : typeof manifest?.sidebar_action?.default_panel === 'string' ? manifest.sidebar_action.default_panel : null;
|
|
113
148
|
const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker : null;
|
|
114
149
|
const workerFile = named && has(named) ? named : 'background.js';
|
|
115
150
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolaby",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
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": {
|