toolaby 1.4.3 → 1.4.4

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 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, STALE_FILES, BUILD_COPY_NOTE } from './lib/project.js';
36
36
 
37
- const VERSION = '1.4.3';
37
+ const VERSION = '1.4.4';
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()].replaceAll('__APP_POPUP__', appPopup ?? '').replaceAll('__APP_SIDE_PANEL__', appSidePanel ?? ''));
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,23 @@ 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
- for (const f of ['manifest.json', ...CODE_FILES, 'background.js', 'popup.html', 'popup.css', 'popup.js', 'README.md']) write(f, plan.files[f].replaceAll('__APP_POPUP__', '').replaceAll('__APP_SIDE_PANEL__', ''));
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
+ const wanted = [...CODE_FILES, ...scaffold];
263
+ const absent = wanted.filter((f) => typeof plan.files[f] !== 'string');
264
+ 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}`);
265
+ // Both surfaces, and the panel drawn inside them: no page of the Wall's stands in front, so both names stay empty.
266
+ for (const f of wanted) write(f, fillPages(plan.files[f]));
267
+ // The manifest names the side panel only now that the page is beside it — an extension whose side_panel points at a
268
+ // missing file does not load at all, so a Wall that stops serving the page must not leave the key behind either.
269
+ const wroteSidePanel = wanted.includes('sidepanel.html');
270
+ write('manifest.json', JSON.stringify(scaffoldManifest(JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8')), { sidePanel: wroteSidePanel }), null, 2) + '\n');
260
271
  for (const s of [16, 48, 128]) {
261
272
  const res = await fetch(`${wall}/boilerplate/icons/icon${s}.png`);
262
273
  write(join('icons', `icon${s}.png`), Buffer.from(await res.arrayBuffer()));
263
274
  }
264
- out(`\n ${plan.name} → ${dir}\n\n 1. chrome://extensions → Developer mode → Load unpacked → ${dir}\n 2. Click the icon: the popup shows what the Free plan holds; Buy opens ${plan.site}/tools/${slug}\n\n Edit background.js: your work goes behind toolaby.gate().\n`);
275
+ out(`\n ${plan.name} → ${dir}\n\n 1. chrome://extensions → Developer mode → Load unpacked → ${dir}\n 2. Click the icon: the popup shows what the Free plan holds; Buy opens ${plan.site}/tools/${slug}\n${wroteSidePanel ? " 3. Open side panel, in that popup: the same page at the panel's width (README.md says how to keep only one)\n" : ''}\n Edit background.js: your work goes behind toolaby.gate().\n`);
265
276
  }
266
277
 
267
278
  // ── webhooks: the catalogue, and a delivery on demand ───────────────────────────────────────────────────────────────
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 appSidePanel = declaredPanel && declaredPanel !== WALL_SIDE_PANEL ? declaredPanel : declaredPanel === WALL_SIDE_PANEL ? (o.rememberedSidePanel || null) : null;
55
- if (appSidePanel && declaredPanel !== WALL_SIDE_PANEL) {
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 ${appSidePanel}: the same as the popup, at the panel's width`);
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,26 @@ 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
+ /**
23
+ * The scaffold's manifest, once it is known which pages were really written. Chrome refuses to load an extension
24
+ * whose `side_panel.default_path` names a file that is not there — the whole extension, not just the panel — so the
25
+ * minimal extension ships a manifest that names only its popup, and whatever writes the side panel adds it here.
26
+ */
27
+ export function scaffoldManifest(manifest, wrote) {
28
+ if (!wrote.sidePanel)
29
+ return manifest;
30
+ const permissions = Array.isArray(manifest.permissions) ? [...manifest.permissions] : [];
31
+ if (!permissions.includes('sidePanel'))
32
+ permissions.push('sidePanel');
33
+ return { ...manifest, permissions, side_panel: { default_path: 'sidepanel.html' } };
34
+ }
35
+ /**
36
+ * The last two placeholders in a handed-out file — the extension's own popup and side panel, which only the
37
+ * writer knows, since it is the one that read the manifest. Everything else is filled where the files are built
38
+ * (wiring/files.ts). Every hand-out goes through this: the Wire button, the command, and the zip on the Set up
39
+ * page, which once shipped `appSidePanel: '__APP_SIDE_PANEL__'` because it filled the popup's name and not this one.
40
+ */
41
+ export const fillPages = (text, pages = {}) => text.replaceAll('__APP_POPUP__', pages.appPopup ?? '').replaceAll('__APP_SIDE_PANEL__', pages.appSidePanel ?? '');
22
42
  /** The import of toolaby.js from a file at `from`, both relative to the root. */
23
43
  export function importPath(from, codeDir = '') {
24
44
  const depth = from.split('/').length - 1;
@@ -109,7 +129,8 @@ export function detectProject(o) {
109
129
  // A plain extension with a build (webpack, Rollup, Parcel, esbuild…) is wired the same way — the manifest is a file and
110
130
  // the files go beside it — but only what the build copies reaches the output, so that is said.
111
131
  const declared = typeof manifest?.action?.default_popup === 'string' ? manifest.action.default_popup : null;
112
- const declaredPanel = typeof manifest?.side_panel?.default_path === 'string' ? manifest.side_panel.default_path : null;
132
+ // Chrome's side_panel, else Firefox's sidebar_action: the same page, two spellings.
133
+ 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
134
  const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker : null;
114
135
  const workerFile = named && has(named) ? named : 'background.js';
115
136
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
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": {