toolaby 1.0.2 → 1.1.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/bin.mjs CHANGED
@@ -22,9 +22,9 @@ import { join, resolve, dirname } from 'node:path';
22
22
  import { homedir } from 'node:os';
23
23
  import { spawn } from 'node:child_process';
24
24
  import { wireManifest, WORKER_LINES } from './lib/manifest.js';
25
- import { detectProject, CODE_FILES, filesToWrite } from './lib/project.js';
25
+ import { detectProject, CODE_FILES, filesToWrite, BUILD_COPY_NOTE } from './lib/project.js';
26
26
 
27
- const VERSION = '1.0.2';
27
+ const VERSION = '1.1.0';
28
28
  const CLIENT_ID = 'toolaby-cli';
29
29
  const argv = process.argv.slice(2);
30
30
  // `--version` and `--help` are commands of their own; every other command is the first bare word.
@@ -155,10 +155,11 @@ const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8'))
155
155
  const isId = (s) => typeof s === 'string' && /^[a-z0-9][a-z0-9-]{1,118}$/.test(s);
156
156
 
157
157
  /** The tool's files from the Wall, as the signed-in developer: a tool of theirs, or a clear no. */
158
- async function filesFor(slug, scaffold) {
158
+ async function filesFor(slug, scaffold, manifestKey = null) {
159
159
  const token = await tokenOrLogin();
160
- const res = await api(`/api/cli/wiring/${slug}${scaffold ? '?scaffold=1' : ''}`, { token });
161
- if (res.status === 401) { out(' The stored session is no longer valid; signing in again.'); await login(); return filesFor(slug, scaffold); }
160
+ const q = new URLSearchParams(); if (scaffold) q.set('scaffold', '1'); if (manifestKey) q.set('manifestKey', manifestKey);
161
+ const res = await api(`/api/cli/wiring/${slug}${q.size ? `?${q}` : ''}`, { token });
162
+ if (res.status === 401) { out(' The stored session is no longer valid; signing in again.'); await login(); return filesFor(slug, scaffold, manifestKey); }
162
163
  if (res.status === 404) {
163
164
  // Say who the command is: the tool may well exist — in a workspace of another account of theirs.
164
165
  const me = await api('/api/cli/me', { token }).catch(() => null);
@@ -178,16 +179,19 @@ async function wire() {
178
179
  const has = (p) => tree.has(p.replace(/\/$/, ''));
179
180
  const manifest = has('manifest.json') ? readJson(join(dir, 'manifest.json')) : null;
180
181
  if (has('manifest.json') && !manifest) fail('manifest.json could not be read as JSON.');
181
- const plan = await filesFor(slug, false);
182
+ // A key the manifest already has is the identity the platform adopts; otherwise the platform's goes in.
183
+ const plan = await filesFor(slug, false, typeof manifest?.key === 'string' ? manifest.key : null);
182
184
  const site = plan.site;
185
+ const manifestKey = plan.identity?.manifestKey ?? null;
183
186
  const configSource = ['manifest.config.ts', 'manifest.config.js', 'manifest.ts', 'src/manifest.ts'].find(has);
184
- const project = detectProject({ has, packageJson: has('package.json') ? readJson(join(dir, 'package.json')) : null, manifest, manifestSource: configSource ? readFileSync(join(dir, configSource), 'utf8') : null, site });
185
- if (project.kind === 'unknown') fail(`${dir} has no manifest.json and is not a WXT, CRXJS or Plasmo project — run this inside your extension's folder, or point --into at it.`);
187
+ const project = detectProject({ has, packageJson: has('package.json') ? readJson(join(dir, 'package.json')) : null, manifest, manifestSource: configSource ? readFileSync(join(dir, configSource), 'utf8') : null, site, tree, manifestKey });
188
+ // Nothing recognised: nothing is written and nothing guessed. A manifest below the top is where to point --into; otherwise the Set up page's Do it myself has the files and the three things to paste.
189
+ if (project.kind === 'unknown') fail(`${dir} has no manifest.json at the top and is not a WXT, CRXJS or Plasmo project${project.foundManifest ? ` — there is one at ${project.foundManifest}: run this with --into ${dirname(project.foundManifest) || '.'}` : ''}.\n Nothing was written. For a layout of your own, the tool's Set up page → Do it myself has the files and the three things to paste.`);
186
190
 
187
191
  // The extension's own popup: the manifest's, or — wired before — the one the existing configuration remembers.
188
192
  const configPath = join(dir, `${project.codeDir}toolaby.config.js`);
189
193
  const remembered = existsSync(configPath) ? readFileSync(configPath, 'utf8').match(/appPopup:\s*'([^']*)'/)?.[1] || null : null;
190
- const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, has, remembered }) : null;
194
+ const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, has, remembered, manifestKey }) : null;
191
195
  const appPopup = wiring ? wiring.appPopup : project.wrapper ? (project.appPopup ?? remembered) : null;
192
196
  const targets = filesToWrite(project, appPopup);
193
197
  const present = targets.filter((f) => existsSync(join(dir, f)));
@@ -212,7 +216,8 @@ async function wire() {
212
216
  out(` Written: ${targets.join(', ')}${did.length ? `,\n ${did.join(',\n ')}` : ''}.\n`);
213
217
  for (const sn of project.snippets) out(` Add to ${sn.file} — yours to paste, the manifest being generated:\n\n${sn.code.split('\n').map((l) => ` ${l}`).join('\n')}\n`);
214
218
  if (ownWorker) out(` Your background, ${ownWorker.file}: add${ownWorker.classic ? ' — after giving "background" a "type": "module" in manifest.json (importScripts() is not available in a module worker)' : ''}:\n\n${project.workerLines.trimEnd().split('\n').map((l) => ` ${l}`).join('\n')}\n`);
215
- out(` Next: ${project.kind === 'plain' ? '' : 'build or start the dev server, then '}chrome://extensions → reload it. The tool's Set up page says "checked in" the moment it starts.`);
219
+ if (project.built) out(` ${BUILD_COPY_NOTE}\n`);
220
+ out(` Next: ${project.kind === 'plain' && !project.built ? '' : 'build or start the dev server, then '}chrome://extensions → reload it. The tool's Set up page says "checked in" the moment it starts.`);
216
221
  if (project.note) out(` ${project.note}\n`);
217
222
  else if (appPopup) out(` Your popup opens as before; what the Free plan holds (Pricing, in the dashboard) decides when the Wall's panel takes its place.\n`);
218
223
  else out(` No popup to stand in front of: count a use where your feature runs — the Set up page shows the line.\n`);
package/lib/manifest.js CHANGED
@@ -6,15 +6,25 @@
6
6
  * the storage permission; the platform's address as a host permission and
7
7
  * for externally_connectable (the site's sign-in and purchase handoffs); a
8
8
  * module service worker when the manifest names none, or names one that is
9
- * not there; and the Wall's popup in front of the extension's own, which
10
- * the configuration then remembers. Pure: given the manifest and what is
11
- * on disk, it says what to write.
9
+ * not there; the Wall's popup in front of the extension's own, which the
10
+ * configuration then remembers; and the extension's identity — the `key`
11
+ * that fixes its id, which the tool key is bound to (identity.ts) — unless
12
+ * the manifest has one of its own, which is then the identity the platform
13
+ * adopts. Pure: given the manifest and what is on disk, it says what to
14
+ * write.
12
15
  */
13
16
  export const WALL_POPUP = 'toolaby-popup.html';
14
17
  export const WORKER_LINES = "import { toolaby } from './toolaby.js';\n\ntoolaby.startBackground();\n";
15
18
  export function wireManifest(input, o) {
16
19
  const manifest = JSON.parse(JSON.stringify(input));
17
20
  const did = [];
21
+ // The identity: the manifest's own key stays (the platform adopts it); otherwise the platform's goes in, and the unpacked extension's id is known before it loads.
22
+ const own = typeof manifest.key === 'string' && manifest.key.trim() ? manifest.key.trim() : null;
23
+ if (!own && o.manifestKey) {
24
+ manifest.key = o.manifestKey;
25
+ did.push('a "key", so the extension keeps one id and the platform knows it');
26
+ }
27
+ const manifestKey = own ?? o.manifestKey ?? null;
18
28
  const add = (list, value) => { const arr = Array.isArray(list) ? [...list] : []; if (!arr.includes(value))
19
29
  arr.push(value); return arr; };
20
30
  if (!(Array.isArray(manifest.permissions) && manifest.permissions.includes('storage'))) {
@@ -48,5 +58,5 @@ export function wireManifest(input, o) {
48
58
  else {
49
59
  ownWorker = { file: named, classic: manifest.background?.type !== 'module' };
50
60
  }
51
- return { manifest, did, writeWorker, ownWorker, appPopup };
61
+ return { manifest, did, writeWorker, ownWorker, appPopup, manifestKey };
52
62
  }
package/lib/project.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // Generated from wall/src/packages/wiring/project.ts by wall/scripts/pack-create.mjs — edit there.
2
+ /** What to tell a developer whose build ships the extension — the Wire button, the command and the Set up page all say it in these words. */
3
+ export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies the platform\'s files to its output — toolaby.js, toolaby.config.js, toolaby-client.module.js, paywall.js, paywall.css and toolaby-popup.* — the way it copies your popup.html.';
2
4
  /** Always written, where the build imports from: the client, its config and types, and the panel a popup of the developer's own can draw. */
3
5
  export const CODE_FILES = ['toolaby.config.js', 'toolaby.js', 'toolaby-client.module.js', 'toolaby.d.ts', 'paywall.js', 'paywall.css'];
4
6
  /** The Wall's popup, where the build copies files as they are; it imports the panel beside it, so the panel goes there too when that is elsewhere. */
@@ -19,6 +21,9 @@ const dep = (pkg, name) => !!(pkg?.dependencies?.[name] || pkg?.devDependencies?
19
21
  const first = (has, candidates) => candidates.find(has) ?? null;
20
22
  export function detectProject(o) {
21
23
  const { has, packageJson: pkg, manifest, site } = o;
24
+ const keyLine = o.manifestKey ? `key: '${o.manifestKey}',` : null;
25
+ const scripts = pkg?.scripts ?? {};
26
+ const built = !!scripts.build || has('dist') || has('build');
22
27
  const wxt = dep(pkg, 'wxt') || !!first(has, ['wxt.config.ts', 'wxt.config.js', 'wxt.config.mts', 'wxt.config.mjs']);
23
28
  const plasmo = dep(pkg, 'plasmo');
24
29
  const crxjs = dep(pkg, '@crxjs/vite-plugin');
@@ -34,6 +39,7 @@ export function detectProject(o) {
34
39
  `// wxt.config.ts — what the Wall needs in the manifest${appPopup ? ', and its popup in front of yours' : ''}`,
35
40
  `export default defineConfig({`,
36
41
  ` manifest: {`,
42
+ ...(keyLine ? [` ${keyLine} // the extension's id, known to the platform`] : []),
37
43
  ` permissions: ['storage'],`,
38
44
  ` host_permissions: ['${site}/*'],`,
39
45
  ` externally_connectable: { matches: ['${site}/*'] },`,
@@ -49,7 +55,7 @@ export function detectProject(o) {
49
55
  `});`,
50
56
  ].join('\n');
51
57
  return {
52
- kind: 'wxt', label: 'WXT', codeDir: '', popupDir: `${src}public/`, manifestFile: null, wrapper: true, appPopup, worker,
58
+ kind: 'wxt', label: 'WXT', codeDir: '', popupDir: `${src}public/`, manifestFile: null, wrapper: true, appPopup, worker, built: false, foundManifest: null,
53
59
  createWorker: worker ? null : { file: workerFile, content: workerLines },
54
60
  workerLines, snippets: [{ file: first(has, ['wxt.config.ts', 'wxt.config.js', 'wxt.config.mts', 'wxt.config.mjs']) ?? 'wxt.config.ts', language: 'ts', code }],
55
61
  };
@@ -62,12 +68,12 @@ export function detectProject(o) {
62
68
  const worker = first(has, [`${src}background.ts`, `${src}background.js`, `${src}background/index.ts`, `${src}background/index.js`]);
63
69
  const workerFile = worker ?? `${src}background.ts`;
64
70
  const workerLines = `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`;
65
- const code = JSON.stringify({ manifest: { permissions: ['storage'], host_permissions: [`${site}/*`], externally_connectable: { matches: [`${site}/*`] } } }, null, 2).replace(/^\{\n|\n\}$/g, '');
71
+ const code = JSON.stringify({ manifest: { ...(o.manifestKey ? { key: o.manifestKey } : {}), permissions: ['storage'], host_permissions: [`${site}/*`], externally_connectable: { matches: [`${site}/*`] } } }, null, 2).replace(/^\{\n|\n\}$/g, '');
66
72
  return {
67
- kind: 'plasmo', label: 'Plasmo', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, worker,
73
+ kind: 'plasmo', label: 'Plasmo', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, worker, built: false, foundManifest: null,
68
74
  createWorker: worker ? null : { file: workerFile, content: workerLines },
69
75
  workerLines, snippets: [{ file: 'package.json', language: 'json', code }],
70
- note: 'Plasmo builds only the pages its manifest names, so the Wall\'s popup cannot stand in front of yours: ask the gate in your popup — the lines under Fine-tune.',
76
+ note: 'Plasmo builds only the pages its manifest names, so the Wall\'s popup cannot stand in front of yours: ask the gate in your popup — the lines are in the docs, under Plasmo.',
71
77
  };
72
78
  }
73
79
  if (crxjs && !has('manifest.json')) {
@@ -80,6 +86,7 @@ export function detectProject(o) {
80
86
  const workerLines = `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`;
81
87
  const code = [
82
88
  `// ${configFile ?? 'manifest.config.ts'} — add inside defineManifest({ … })`,
89
+ ...(keyLine && !/\bkey:/.test(source) ? [`${keyLine} // the extension's id, known to the platform`] : []),
83
90
  `permissions: ['storage'],`,
84
91
  `host_permissions: ['${site}/*'],`,
85
92
  `externally_connectable: { matches: ['${site}/*'] },`,
@@ -96,21 +103,26 @@ export function detectProject(o) {
96
103
  `});`,
97
104
  ].join('\n');
98
105
  return {
99
- kind: 'crxjs', label: 'CRXJS', codeDir: '', popupDir: '', manifestFile: null, wrapper: true, appPopup, worker: worker && has(worker) ? worker : null,
106
+ kind: 'crxjs', label: 'CRXJS', codeDir: '', popupDir: '', manifestFile: null, wrapper: true, appPopup, worker: worker && has(worker) ? worker : null, built: false, foundManifest: null,
100
107
  createWorker: worker && has(worker) ? null : { file: workerFile, content: workerLines },
101
108
  workerLines, snippets: [{ file: configFile ?? 'manifest.config.ts', language: 'ts', code }, ...(appPopup ? [{ file: viteFile, language: 'ts', code: vite }] : [])],
102
109
  };
103
110
  }
104
111
  if (has('manifest.json')) {
105
112
  // A plain extension, or CRXJS with a real manifest.json: the manifest is edited (wireManifest), everything at the root.
113
+ // A plain extension with a build (webpack, Rollup, Parcel, esbuild…) is wired the same way — the manifest is a file and
114
+ // the files go beside it — but only what the build copies reaches the output, so that is said.
106
115
  const declared = typeof manifest?.action?.default_popup === 'string' ? manifest.action.default_popup : null;
107
116
  const named = typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker : null;
108
117
  const workerFile = named && has(named) ? named : 'background.js';
109
118
  return {
110
- kind: crxjs ? 'crxjs' : 'plain', label: crxjs ? 'CRXJS' : 'a plain extension', codeDir: '', popupDir: '', manifestFile: 'manifest.json', wrapper: true,
119
+ kind: crxjs ? 'crxjs' : 'plain', label: crxjs ? 'CRXJS' : 'a plain extension', codeDir: '', popupDir: '', manifestFile: 'manifest.json', wrapper: true, built: crxjs ? false : built, foundManifest: null,
111
120
  appPopup: declared && declared !== 'toolaby-popup.html' ? declared : null, worker: named && has(named) ? named : null, createWorker: null,
112
121
  workerLines: `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`, snippets: [],
113
122
  };
114
123
  }
115
- return { kind: 'unknown', label: 'unknown', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, worker: null, createWorker: null, workerLines: '', snippets: [] };
124
+ // Nothing recognised. A manifest below the root — src/, public/, a package of a monorepo — is where the wiring should have
125
+ // been pointed, and is said; the shallowest one, since a fixture or a dependency may carry another.
126
+ const foundManifest = [...(o.tree ?? [])].filter((p) => p.endsWith('/manifest.json') && !p.includes('node_modules/')).sort((a, b) => a.split('/').length - b.split('/').length || a.localeCompare(b))[0] ?? null;
127
+ return { kind: 'unknown', label: 'unknown', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, worker: null, createWorker: null, workerLines: '', snippets: [], built, foundManifest };
116
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.0.2",
3
+ "version": "1.1.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": { "toolaby": "./bin.mjs" },