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 +15 -10
- package/lib/manifest.js +14 -4
- package/lib/project.js +19 -7
- package/package.json +1 -1
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
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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;
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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" },
|