toolaby 1.7.1 → 1.8.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 +6 -4
- package/bin.mjs +38 -35
- package/lib/check.js +2 -2
- package/lib/manifest.js +20 -6
- package/lib/project.js +18 -1
- package/lib/safe.js +10 -4
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
The Toolaby command line. It puts the Wall — sign-in, a Free plan, paid plans, licences and subscriptions — into a Chrome extension you already have, or makes a new one for you.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm i -g toolaby
|
|
7
|
-
toolaby wire <tool-id> # in your extension's folder, on Test; signs you in first if it must
|
|
8
|
-
toolaby wire <tool-id> --live # the same, on Live: the store build
|
|
6
|
+
npm i -g toolaby # once; or run every command through `npx -y toolaby@latest …`
|
|
7
|
+
toolaby-cli wire <tool-id> # in your extension's folder, on Test; signs you in first if it must
|
|
8
|
+
toolaby-cli wire <tool-id> --live # the same, on Live: the store build
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
The command is `toolaby-cli`: on Windows, the Command Prompt runs a `toolaby.js` in the current folder before a command named `toolaby`, and every wired folder has one. Below, `toolaby <command>` names each command.
|
|
12
|
+
|
|
11
13
|
In a repository you did not write, use the installed command: `npx` can run a `toolaby` the project carries, or fetch one from a registry the project's `.npmrc` names.
|
|
12
14
|
|
|
13
|
-
Every command acts on the test Wall (`
|
|
15
|
+
Every command acts on the test Wall (`toolaby.app/test`) unless told `--live` (`toolaby.app`), as Stripe's command line does. `toolaby tools` lists your ids on either side.
|
|
14
16
|
|
|
15
17
|
## What it does
|
|
16
18
|
|
package/bin.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// toolaby whoami · logout · help
|
|
18
18
|
//
|
|
19
19
|
// Test and live, as Stripe's command line has them: every command acts on
|
|
20
|
-
// the TEST Wall (
|
|
20
|
+
// the TEST Wall (toolaby.app/test) unless told `--live`, so the everyday
|
|
21
21
|
// command is the safe one and going live is a word you type on purpose.
|
|
22
22
|
// Signed in, the command acts as you and only on your tools, as Clerk's and
|
|
23
23
|
// Stripe's do: nothing is fetched for a tool your workspaces do not own. The
|
|
@@ -37,14 +37,14 @@ import { deflateRawSync } from 'node:zlib';
|
|
|
37
37
|
import { homedir } from 'node:os';
|
|
38
38
|
import { spawn } from 'node:child_process';
|
|
39
39
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
40
|
-
import { wireManifest, drawsItself, WORKER_LINES, wallWorkerSource, configValues, wrappedBy, namedWorker as workerIn, startsPlatform as startsWall, CLASSIC_CLIENT } from './lib/manifest.js';
|
|
40
|
+
import { wireManifest, sitePattern, drawsItself, WORKER_LINES, wallWorkerSource, configValues, wrappedBy, namedWorker as workerIn, startsPlatform as startsWall, CLASSIC_CLIENT } from './lib/manifest.js';
|
|
41
41
|
import { wiringSummary, popupSwapIn, gatesIn, ownCode } from './lib/summary.js';
|
|
42
42
|
import { detectProject, CODE_FILES, POPUP_FILES, SIDE_PANEL_FILES, filesToWrite, fillPages, scaffoldManifest, scaffoldFilesFor, surfacesOf, hasPopup, hasSidePanel, SURFACES, STALE_FILES, BUILD_COPY_NOTE, AGENT_FILES, AGENT_ENTRY_FILES } from './lib/project.js';
|
|
43
43
|
import { runMcp } from './lib/mcp.js';
|
|
44
44
|
import { runCheck } from './lib/check.js';
|
|
45
45
|
import { inside, signInPage, devicePolling, opener, privateKind, privateContent, privateCopies, googleKey, projectRoot, gitDirectory, gitObject, gitObjects, gitFile, notAnImage, unpacked, COMPRESSED, publicLinks, privateLinks, oneLine, fetchBounded, fetchHere, readBounded, readText, SCAFFOLD_FILES } from './lib/safe.js';
|
|
46
46
|
|
|
47
|
-
const VERSION = '1.
|
|
47
|
+
const VERSION = '1.8.0';
|
|
48
48
|
const CLIENT_ID = 'toolaby-cli';
|
|
49
49
|
const argv = process.argv.slice(2);
|
|
50
50
|
// `--version` and `--help` are commands of their own; every other command is the first bare word.
|
|
@@ -54,7 +54,8 @@ const positional = rest.filter((a) => !a.startsWith('--'));
|
|
|
54
54
|
const opt = (k, d = null) => { const i = rest.indexOf(k); return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : d; };
|
|
55
55
|
// `upgrade` is `wire --force`: the platform's files replaced with the current ones, the extension's own untouched.
|
|
56
56
|
const flag = (k) => rest.includes(k) || (k === '--force' && command === 'upgrade');
|
|
57
|
-
|
|
57
|
+
// Test shares Live's addresses under /test, as Stripe keeps both modes on its own hosts.
|
|
58
|
+
const WALLS = { test: 'https://toolaby.app/test', live: 'https://toolaby.app' };
|
|
58
59
|
const live = flag('--live');
|
|
59
60
|
// An address as fetch() reads it: a URL loses its tabs and line breaks when it is parsed, and kept in the name every
|
|
60
61
|
// message prints, a --wall or a TOOLABY_WALL with one in it drew lines of its own (25 Sep 2026).
|
|
@@ -167,11 +168,11 @@ async function login() {
|
|
|
167
168
|
}
|
|
168
169
|
const why = poll.json.error;
|
|
169
170
|
if (why === 'slow_down') every += 5000;
|
|
170
|
-
else if (why === 'expired_token') fail('The code expired before it was approved. Run `toolaby login` again.');
|
|
171
|
+
else if (why === 'expired_token') fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
|
|
171
172
|
else if (why === 'access_denied') fail('The sign-in was denied in the browser. Nothing was signed in.');
|
|
172
173
|
else if (why && why !== 'authorization_pending') fail(`The Wall answered "${oneLine(why)}"${poll.json.error_description ? `: ${oneLine(poll.json.error_description)}` : ''}.`);
|
|
173
174
|
}
|
|
174
|
-
fail('The code expired before it was approved. Run `toolaby login` again.');
|
|
175
|
+
fail('The code expired before it was approved. Run `npx -y toolaby@latest login` again.');
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
/** A token to act with: the stored one, else a sign-in right here — `wire` in a fresh terminal is one command, not two. */
|
|
@@ -183,9 +184,9 @@ async function tokenOrLogin() {
|
|
|
183
184
|
|
|
184
185
|
async function whoami() {
|
|
185
186
|
const token = storedToken();
|
|
186
|
-
if (!token) fail(`Not signed in to ${side} (${wall}). Run \`toolaby login${live ? ' --live' : ''}\`.${envElsewhere()}`);
|
|
187
|
+
if (!token) fail(`Not signed in to ${side} (${wall}). Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.${envElsewhere()}`);
|
|
187
188
|
const me = await api('/api/cli/me', { token });
|
|
188
|
-
if (me.status === 401) fail(`The session for ${wall} is no longer valid. Run \`toolaby login\`.`);
|
|
189
|
+
if (me.status === 401) fail(`The session for ${wall} is no longer valid. Run \`npx -y toolaby@latest login\`.`);
|
|
189
190
|
if (!me.ok) fail(`The Wall answered HTTP ${me.status}.`);
|
|
190
191
|
out(`\n ${oneLine(me.json.email)} · ${side} · ${wall}\n`);
|
|
191
192
|
for (const w of me.json.workspaces) out(` ${oneLine(w.name)} (${oneLine(w.slug)}, ${oneLine(w.role)}) ${oneLine(w.site)}`);
|
|
@@ -207,7 +208,7 @@ async function logout() {
|
|
|
207
208
|
async function tools() {
|
|
208
209
|
const token = await tokenOrLogin();
|
|
209
210
|
const res = await api('/api/cli/tools', { token });
|
|
210
|
-
if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`toolaby login${live ? ' --live' : ''}\`.`);
|
|
211
|
+
if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
|
|
211
212
|
if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
|
|
212
213
|
if (!res.json.tools.length) { out(`\n No tools on ${side} yet. Make one in the dashboard, then \`toolaby wire <tool-id>${live ? ' --live' : ''}\` in your extension's folder.\n`); return; }
|
|
213
214
|
out(`\n ${side}\n`);
|
|
@@ -276,7 +277,7 @@ async function filesFor(slug, scaffold, manifestKey = null, surfaces = null) {
|
|
|
276
277
|
|
|
277
278
|
async function wire() {
|
|
278
279
|
const slug = positional[0];
|
|
279
|
-
if (!isId(slug)) fail(command === 'upgrade' ? 'usage: toolaby upgrade <tool-id> [--into <folder>] — `toolaby tools` lists your ids' : 'usage: toolaby wire <tool-id> [--into <folder>] [--force] [--adopt-key] — `toolaby tools` lists your ids');
|
|
280
|
+
if (!isId(slug)) fail(command === 'upgrade' ? 'usage: npx -y toolaby@latest upgrade <tool-id> [--into <folder>] — `npx -y toolaby@latest tools` lists your ids' : 'usage: npx -y toolaby@latest wire <tool-id> [--into <folder>] [--force] [--adopt-key] — `npx -y toolaby@latest tools` lists your ids');
|
|
280
281
|
const dir = resolve(opt('--into') ?? '.');
|
|
281
282
|
if (!existsSync(dir)) fail(`${dir} does not exist`);
|
|
282
283
|
const tree = listTree(dir, 3);
|
|
@@ -371,7 +372,7 @@ async function wire() {
|
|
|
371
372
|
|
|
372
373
|
async function create() {
|
|
373
374
|
const slug = positional[0];
|
|
374
|
-
if (!isId(slug)) fail('usage: toolaby create <tool-id> [--dir <folder>] — `toolaby tools` lists your ids');
|
|
375
|
+
if (!isId(slug)) fail('usage: npx -y toolaby@latest create <tool-id> [--dir <folder>] — `npx -y toolaby@latest tools` lists your ids');
|
|
375
376
|
const dir = resolve(opt('--dir') ?? slug);
|
|
376
377
|
if (there(dir)) fail(`${dir} already exists`);
|
|
377
378
|
// Which surfaces this extension is made with, asked first: the files for coding agents name only what is written.
|
|
@@ -442,7 +443,7 @@ function devSecret() {
|
|
|
442
443
|
}
|
|
443
444
|
async function catalogue() {
|
|
444
445
|
const res = await api('/api/cli/webhooks/events', { token: await tokenOrLogin() });
|
|
445
|
-
if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`toolaby login${live ? ' --live' : ''}\`.`);
|
|
446
|
+
if (res.status === 401) fail(`The session for ${side} is no longer valid. Run \`npx -y toolaby@latest login${live ? ' --live' : ''}\`.`);
|
|
446
447
|
if (!res.ok) fail(`The Wall answered HTTP ${res.status}.`);
|
|
447
448
|
return res.json.events;
|
|
448
449
|
}
|
|
@@ -462,7 +463,7 @@ async function webhooks() {
|
|
|
462
463
|
}
|
|
463
464
|
if (sub === 'trigger') {
|
|
464
465
|
const type = positional[1];
|
|
465
|
-
if (!type) fail('usage: toolaby webhooks trigger <event> [--to <url>] [--secret <whsec_…>] — `toolaby webhooks events` lists them');
|
|
466
|
+
if (!type) fail('usage: npx -y toolaby@latest webhooks trigger <event> [--to <url>] [--secret <whsec_…>] — `npx -y toolaby@latest webhooks events` lists them');
|
|
466
467
|
const to = opt('--to');
|
|
467
468
|
if (!to) {
|
|
468
469
|
const res = await api('/api/cli/webhooks/trigger', { method: 'POST', body: { type, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
|
|
@@ -472,7 +473,7 @@ async function webhooks() {
|
|
|
472
473
|
}
|
|
473
474
|
const events = await catalogue();
|
|
474
475
|
const event = events.find((e) => e.type === type);
|
|
475
|
-
if (!event) fail(`No event "${type}". \`toolaby webhooks events\` lists them.`);
|
|
476
|
+
if (!event) fail(`No event "${type}". \`npx -y toolaby@latest webhooks events\` lists them.`);
|
|
476
477
|
const secret = opt('--secret') ?? devSecret();
|
|
477
478
|
if (!/^whsec_[A-Za-z0-9+/=]+$/.test(secret)) fail('--secret must be a Standard Webhooks secret: whsec_ followed by base64.');
|
|
478
479
|
const id = `msg_${randomBytes(12).toString('base64url')}`;
|
|
@@ -494,14 +495,14 @@ async function webhooks() {
|
|
|
494
495
|
const res = await api(`/api/cli/webhooks/endpoints${ws ? `?workspace=${encodeURIComponent(ws)}` : ''}`, { token: await tokenOrLogin() });
|
|
495
496
|
if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
|
|
496
497
|
out(`\n ${oneLine(res.json.workspace)} · ${side}\n`);
|
|
497
|
-
if (res.json.endpoints.length === 0) out(' None yet. `toolaby webhooks add <url>` registers one.');
|
|
498
|
+
if (res.json.endpoints.length === 0) out(' None yet. `npx -y toolaby@latest webhooks add <url>` registers one.');
|
|
498
499
|
for (const e of res.json.endpoints) out(` ${oneLine(e.id)} ${e.disabled ? 'paused ' : 'on '} ${oneLine(e.url)}${e.description ? ` — ${oneLine(e.description)}` : ''}${e.events.length ? ` (${oneLine(e.events.join(', '))})` : ''}`);
|
|
499
500
|
out();
|
|
500
501
|
return;
|
|
501
502
|
}
|
|
502
503
|
if (sub === 'add') {
|
|
503
504
|
const url = positional[1];
|
|
504
|
-
if (!url) fail('usage: toolaby webhooks add <url> [--description <text>] [--events a.b,c.d] [--workspace <slug>]');
|
|
505
|
+
if (!url) fail('usage: npx -y toolaby@latest webhooks add <url> [--description <text>] [--events a.b,c.d] [--workspace <slug>]');
|
|
505
506
|
const events = (opt('--events') ?? '').split(',').map((e) => e.trim()).filter(Boolean);
|
|
506
507
|
const res = await api('/api/cli/webhooks/endpoints', { method: 'POST', body: { url, description: opt('--description') ?? '', events, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
|
|
507
508
|
if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
|
|
@@ -510,13 +511,13 @@ async function webhooks() {
|
|
|
510
511
|
}
|
|
511
512
|
if (sub === 'remove') {
|
|
512
513
|
const id = positional[1];
|
|
513
|
-
if (!id) fail('usage: toolaby webhooks remove <ep_…> [--workspace <slug>] — `toolaby webhooks endpoints` lists them');
|
|
514
|
+
if (!id) fail('usage: npx -y toolaby@latest webhooks remove <ep_…> [--workspace <slug>] — `npx -y toolaby@latest webhooks endpoints` lists them');
|
|
514
515
|
const res = await api('/api/cli/webhooks/endpoints', { method: 'DELETE', body: { id, workspace: opt('--workspace') ?? undefined }, token: await tokenOrLogin() });
|
|
515
516
|
if (!res.ok) fail(oneLine(res.json.message ?? `The Wall answered HTTP ${res.status}.`));
|
|
516
517
|
out(`\n Removed ${oneLine(res.json.removed)} · ${oneLine(res.json.workspace)} · ${side}. Deliveries to it stop at once.\n`);
|
|
517
518
|
return;
|
|
518
519
|
}
|
|
519
|
-
fail('usage: toolaby webhooks events | trigger <event> [--to <url>] | endpoints | add <url> | remove <id>');
|
|
520
|
+
fail('usage: npx -y toolaby@latest webhooks events | trigger <event> [--to <url>] | endpoints | add <url> | remove <id>');
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
// ── pack: the zip the Chrome Web Store takes ──────────────────────────────────────────────────────────────────────────
|
|
@@ -732,7 +733,7 @@ function pack() {
|
|
|
732
733
|
// The Wall the key names must be reachable from the extension, or sign-in and the purchase handoff fail.
|
|
733
734
|
for (const site of sites) {
|
|
734
735
|
const hosts = manifest.host_permissions ?? []; const ec = manifest.externally_connectable?.matches ?? [];
|
|
735
|
-
if (!hosts.includes(
|
|
736
|
+
if (!hosts.includes(sitePattern(site)) || !ec.includes(sitePattern(site))) problems.push(`The manifest does not name ${oneLine(sitePattern(site))} in both host_permissions and externally_connectable: sign-in and the purchase would not reach the extension. \`npx -y toolaby@latest wire <tool-id> --live --force\` adds them.`);
|
|
736
737
|
}
|
|
737
738
|
if (problems.length) fail(`Not packed:\n\n${problems.map((p) => ` · ${p}`).join('\n\n')}`);
|
|
738
739
|
|
|
@@ -784,35 +785,37 @@ function help() {
|
|
|
784
785
|
out(`
|
|
785
786
|
toolaby — the Toolaby command line (${VERSION})
|
|
786
787
|
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
npx -y toolaby@latest <command>, or installed (npm i -g toolaby): toolaby-cli <command>
|
|
789
|
+
|
|
790
|
+
wire <tool-id> wire the extension in this folder to your tool, on Test
|
|
791
|
+
wire <tool-id> --live the same, on Live: the store build
|
|
789
792
|
--into <folder> another folder --force replace the platform's files
|
|
790
793
|
--own-popup keep your popup in front (the Wall's opens from toolaby.showPaywall())
|
|
791
|
-
|
|
792
|
-
|
|
794
|
+
upgrade <tool-id> the platform's files replaced with the current ones — the popup, the client — yours untouched
|
|
795
|
+
create <tool-id> a new, minimal extension for your tool, in <tool-id>/ --dir <folder>
|
|
793
796
|
--surfaces both|popup|side-panel|none what it opens (default: both)
|
|
794
|
-
|
|
795
|
-
|
|
797
|
+
tools your tools, with their ids
|
|
798
|
+
pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
|
|
796
799
|
refuses a Test key or localhost inside --out <file.zip>
|
|
797
800
|
leaves out keys, certificates, .env and credential files, source maps, secrets,
|
|
798
801
|
git's files, a build's copy of a private file (compressed, UTF-16 or base64),
|
|
799
802
|
an image that is none; refuses a build if one is a file the manifest names
|
|
800
803
|
--allow-private packs them
|
|
801
|
-
|
|
804
|
+
check [folder] whether the extension here is wired right: files, key, manifest, client, features;
|
|
802
805
|
--browser loads it in Chromium (needs Playwright in the project)
|
|
803
|
-
|
|
806
|
+
mcp the Wall for coding agents, an MCP server on stdio:
|
|
804
807
|
claude mcp add toolaby -- npx -y toolaby@latest mcp
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
808
|
+
webhooks events the six events, with an example of each
|
|
809
|
+
webhooks trigger <event> an example delivery to your endpoints; --to <url> sends it to a local server instead
|
|
810
|
+
webhooks endpoints your endpoints; add <url> registers one (secret shown once), remove <id> removes one
|
|
811
|
+
login · whoami · logout the session, per side; a command that needs one signs you in itself
|
|
809
812
|
|
|
810
|
-
Every command acts on Test (
|
|
813
|
+
Every command acts on Test (toolaby.app/test) unless told --live (toolaby.app).
|
|
811
814
|
--wall <url> or TOOLABY_WALL names any other Wall; TOOLABY_TOKEN a session, for CI —
|
|
812
|
-
sent to Live alone, or to the one Wall TOOLABY_TOKEN_WALL names (https://
|
|
815
|
+
sent to Live alone, or to the one Wall TOOLABY_TOKEN_WALL names (https://toolaby.app/test for Test).
|
|
813
816
|
|
|
814
|
-
|
|
815
|
-
|
|
817
|
+
In a repository you did not write, install it (npm i -g toolaby) and type toolaby-cli: npx can run a toolaby
|
|
818
|
+
the project carries, or fetch one from a registry its .npmrc names.
|
|
816
819
|
`);
|
|
817
820
|
}
|
|
818
821
|
|
package/lib/check.js
CHANGED
|
@@ -8,7 +8,7 @@ import { join, relative, resolve } from 'node:path';
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
10
|
import { oneLine, keySite, fetchBounded, readText } from './safe.js';
|
|
11
|
-
import { wrappedBy, namedWorker, startsPlatform as startsWall } from './manifest.js';
|
|
11
|
+
import { wrappedBy, namedWorker, startsPlatform as startsWall, sitePattern } from './manifest.js';
|
|
12
12
|
import { popupSwapIn, ownCode } from './summary.js';
|
|
13
13
|
|
|
14
14
|
const KEY = /tk_(test|live|dev)_[A-Za-z0-9_-]{20,}/;
|
|
@@ -118,7 +118,7 @@ export async function runCheck({ dir: given, out, api, token, detectProject, wir
|
|
|
118
118
|
else ok('The manifest has the permissions and host permissions the Wall needs.');
|
|
119
119
|
// The identity: without the `key`, each copy's id follows its folder, and once the extension is named the tool key refuses every copy not chosen.
|
|
120
120
|
if (typeof manifest.key !== 'string' && /manifestKey:\s*'[A-Za-z0-9+/]{100,}={0,2}'/.test(configText)) warn(`manifest.json has no "key": each copy's id then depends on its folder, and once your extension is named the tool key refuses any copy you did not choose on the Keys page. \`npx -y toolaby@latest upgrade ${key.slug}\` adds it.`);
|
|
121
|
-
} else warn(`The manifest is generated by ${project.label}: build, then check the built manifest has ${key.site}
|
|
121
|
+
} else warn(`The manifest is generated by ${project.label}: build, then check the built manifest has ${sitePattern(key.site)} in host_permissions and externally_connectable.`);
|
|
122
122
|
|
|
123
123
|
// 4b. The background starts the Wall: the popup and every page ask it everything, so without it nothing answers.
|
|
124
124
|
const upgrade = `\`npx -y toolaby@latest upgrade ${key.slug}\``;
|
package/lib/manifest.js
CHANGED
|
@@ -69,6 +69,19 @@ export const loadableName = (file) => {
|
|
|
69
69
|
};
|
|
70
70
|
/** The background the manifest names, as the folder spells it: "./background.js" is background.js. */
|
|
71
71
|
export const namedWorker = (manifest) => typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
|
|
72
|
+
/**
|
|
73
|
+
* The match pattern a manifest names the Wall by: the origin of the key's site, whatever path follows it. Test shares
|
|
74
|
+
* Live's hosts under /test (`https://northwind.toolaby.app/test`), so one pattern — `https://northwind.toolaby.app/*` —
|
|
75
|
+
* lets the same build reach either side, and a Live wiring's manifest is the one it always was.
|
|
76
|
+
*/
|
|
77
|
+
export function sitePattern(site) {
|
|
78
|
+
try {
|
|
79
|
+
return `${new URL(site).origin}/*`;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return `${site.replace(/\/+$/, '')}/*`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
72
85
|
/**
|
|
73
86
|
* Whether an earlier wiring left the extension's own pages drawing the panel: a configuration that says '' for both
|
|
74
87
|
* pages while the manifest names none of the Wall's. That is what `create` makes, and the "your popup first" shape
|
|
@@ -110,16 +123,17 @@ export function wireManifest(input, o) {
|
|
|
110
123
|
did.push('the "storage" permission');
|
|
111
124
|
added.push('storage');
|
|
112
125
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
const pattern = sitePattern(o.site);
|
|
127
|
+
if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(pattern))) {
|
|
128
|
+
manifest.host_permissions = add(manifest.host_permissions, pattern);
|
|
129
|
+
did.push(`a host permission for ${host}`);
|
|
116
130
|
if (!added.includes(host))
|
|
117
131
|
added.push(host);
|
|
118
132
|
}
|
|
119
133
|
const ec = manifest.externally_connectable ?? {};
|
|
120
|
-
if (!(Array.isArray(ec.matches) && ec.matches.includes(
|
|
121
|
-
manifest.externally_connectable = { ...ec, matches: add(ec.matches,
|
|
122
|
-
did.push(`externally_connectable for ${
|
|
134
|
+
if (!(Array.isArray(ec.matches) && ec.matches.includes(pattern))) {
|
|
135
|
+
manifest.externally_connectable = { ...ec, matches: add(ec.matches, pattern) };
|
|
136
|
+
did.push(`externally_connectable for ${host}`);
|
|
123
137
|
if (!added.includes(host))
|
|
124
138
|
added.push(host);
|
|
125
139
|
}
|
package/lib/project.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
// Generated from wall/src/packages/wiring/project.ts by wall/scripts/pack-create.mjs — edit there.
|
|
2
|
+
/**
|
|
3
|
+
* What kind of extension project a folder is, and where the platform's
|
|
4
|
+
* files go in it — the same files for every kind. A plain extension has
|
|
5
|
+
* its manifest.json at the root and everything beside it. A framework
|
|
6
|
+
* (WXT, Plasmo, CRXJS) generates the manifest at build time, bundles the
|
|
7
|
+
* background, and copies a folder of files as they are: the client goes
|
|
8
|
+
* where the build can import it, the Wall's popup — and its side panel,
|
|
9
|
+
* where the extension has one — where the build copies verbatim. What the manifest and the background need is the framework's
|
|
10
|
+
* own extension point where it has one — WXT loads a module from modules/
|
|
11
|
+
* by itself, so the wiring writes one and nothing is pasted — and, where it
|
|
12
|
+
* has none (Plasmo, CRXJS), a snippet for its config: shown, never written
|
|
13
|
+
* into someone's TypeScript. Pure:
|
|
14
|
+
* given what is in the folder, it says what to write and what to say.
|
|
15
|
+
* Shared by the browser (rb/WireExtension) and the terminal
|
|
16
|
+
* (`toolaby wire`, which carries a transpiled copy).
|
|
17
|
+
*/
|
|
18
|
+
import { sitePattern } from './manifest.js';
|
|
2
19
|
/** 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
20
|
export const BUILD_COPY_NOTE = 'A build ships this extension: make sure it copies every toolaby* file to its output — toolaby.js, toolaby.config.js, toolaby-popup.html and the rest — the way it copies your popup.html.';
|
|
4
21
|
/** Always written, where the build imports from: the platform as one file (the client and the popup UI), its config, its types. */
|
|
@@ -109,7 +126,7 @@ export function detectProject(o) {
|
|
|
109
126
|
const worker = first(has, [`${src}background.ts`, `${src}background.js`, `${src}background/index.ts`, `${src}background/index.js`]);
|
|
110
127
|
const workerFile = worker ?? `${src}background.ts`;
|
|
111
128
|
const workerLines = `import { toolaby } from '${importPath(workerFile)}';\n\ntoolaby.startBackground();\n`;
|
|
112
|
-
const code = JSON.stringify({ manifest: { ...(o.manifestKey ? { key: o.manifestKey } : {}), permissions: ['storage'], host_permissions: [
|
|
129
|
+
const code = JSON.stringify({ manifest: { ...(o.manifestKey ? { key: o.manifestKey } : {}), permissions: ['storage'], host_permissions: [sitePattern(site)], externally_connectable: { matches: [sitePattern(site)] } } }, null, 2).replace(/^\{\n|\n\}$/g, '');
|
|
113
130
|
return {
|
|
114
131
|
kind: 'plasmo', label: 'Plasmo', codeDir: '', popupDir: '', manifestFile: null, wrapper: false, appPopup: null, appSidePanel: null, worker, built: false, foundManifest: null,
|
|
115
132
|
createWorker: worker ? null : { file: workerFile, content: workerLines },
|
package/lib/safe.js
CHANGED
|
@@ -60,15 +60,17 @@ export const oneLine = (value) => String(value).replace(/\s+/g, ' ').slice(0, 50
|
|
|
60
60
|
const wallScheme = (url) => url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.test(url.hostname));
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* The Wall a tool key names, when `check` may ask it anything: an origin, https or on this machine,
|
|
64
|
-
*
|
|
63
|
+
* The Wall a tool key names, when `check` may ask it anything: an origin, https or on this machine, with nothing after
|
|
64
|
+
* it but Test's `/test`, where Test shares Live's hosts — no other path, no query, no credentials, no other scheme.
|
|
65
|
+
* Null otherwise. The key is read from the project's own toolaby.config.js, and a
|
|
65
66
|
* cloned project's key sent `check`'s requests wherever it named: a server inside the developer's network, any path on
|
|
66
67
|
* any host (25 Sep 2026).
|
|
67
68
|
*/
|
|
68
69
|
export function keySite(site) {
|
|
69
70
|
let url;
|
|
70
71
|
try { url = new URL(site); } catch { return null; }
|
|
71
|
-
|
|
72
|
+
const path = url.pathname === '/' ? '' : url.pathname;
|
|
73
|
+
return (path === '' || path === '/test') && `${url.origin}${path}` === site && wallScheme(url) ? site : null;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
@@ -673,13 +675,17 @@ function publicLinkEntries(root, packed = null) {
|
|
|
673
675
|
/**
|
|
674
676
|
* The page `login` opens: the one the Wall gave, when it is the Wall's own over https — or, for a Wall on this
|
|
675
677
|
* machine, a page on this machine. Anything else is null, and is not opened: an address elsewhere, a downgrade to
|
|
676
|
-
* http, or a scheme such as file: that opens whatever it names.
|
|
678
|
+
* http, or a scheme such as file: that opens whatever it names. A Wall under a path gives a page under it: Test at
|
|
679
|
+
* toolaby.app/test approves its codes at toolaby.app/test/…, and Live's page of the same name knows none of them (a
|
|
680
|
+
* root path resolved against Test's own base loses /test: 29 Sep 2026).
|
|
677
681
|
*/
|
|
678
682
|
export function signInPage(link, wall) {
|
|
679
683
|
if (typeof link !== 'string') return null;
|
|
680
684
|
let home; let page;
|
|
681
685
|
try { home = new URL(wall); page = new URL(link, home); } catch { return null; }
|
|
682
686
|
if (page.username || page.password) return null;
|
|
687
|
+
const base = home.pathname.replace(/\/+$/, '');
|
|
688
|
+
if (base && page.pathname !== base && !page.pathname.startsWith(`${base}/`)) return null;
|
|
683
689
|
if (page.protocol === 'https:' && page.origin === home.origin) return page.href;
|
|
684
690
|
const web = page.protocol === 'http:' || page.protocol === 'https:';
|
|
685
691
|
return web && LOOPBACK.test(home.hostname) && LOOPBACK.test(page.hostname) ? page.href : null;
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolaby",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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; check it, and give coding agents the Wall as an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"toolaby-cli": "bin.mjs"
|
|
8
|
-
"toolaby": "bin.mjs"
|
|
7
|
+
"toolaby-cli": "bin.mjs"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
10
|
"bin.mjs",
|