@yojahny/wp-design-library 0.1.0 → 0.2.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/Dockerfile +4 -0
- package/README.md +50 -1
- package/package.json +27 -4
- package/src/cli/check.mjs +2 -1
- package/src/index/embed.mjs +16 -2
- package/src/index/query.mjs +4 -4
- package/src/ingest/measure.mjs +6 -1
- package/src/mcp/server.mjs +3 -1
- package/src/mcp/tools.mjs +2 -2
- package/tests/checks/frames-dense.sh +1 -0
- package/tests/checks/index-degrade.sh +1 -1
- package/tests/checks/measure.sh +5 -1
- package/tests/checks/optional-deps.sh +55 -0
- package/tests/run.sh +9 -3
package/Dockerfile
CHANGED
|
@@ -14,6 +14,10 @@ RUN if [ "$WITH_CHROME" = "1" ] && [ "$(dpkg --print-architecture)" = "amd64" ];
|
|
|
14
14
|
WORKDIR /app
|
|
15
15
|
COPY package*.json ./
|
|
16
16
|
RUN npm ci --omit=dev
|
|
17
|
+
# The vector arm and URL measurement are optional peers (npm ci skips an optional
|
|
18
|
+
# peer by design); the hosted image wants both, so install them explicitly at the
|
|
19
|
+
# pins package.json declares.
|
|
20
|
+
RUN npm install --no-save --no-audit --no-fund @huggingface/transformers@4.2.0 playwright-core@1.63.0
|
|
17
21
|
COPY . .
|
|
18
22
|
# Vector-search models are not baked into the image: they land on the /data volume
|
|
19
23
|
# under models/ and are fetched at container start when LIBRARY_FETCH_MODELS=1.
|
package/README.md
CHANGED
|
@@ -24,9 +24,28 @@ only for `own` or `open` sources; the licence gate refuses anything else.
|
|
|
24
24
|
```bash
|
|
25
25
|
git clone git@github.com:yojahny55/wp-design-library.git
|
|
26
26
|
cd wp-design-library && npm install
|
|
27
|
-
npm test #
|
|
27
|
+
npm test # 25 checks, all should PASS (measure.sh SKIPs without a system Chrome)
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Install: core vs full
|
|
31
|
+
|
|
32
|
+
A plain `npm install` (or the plugin's `npx @yojahny/wp-design-library serve`) gives you
|
|
33
|
+
keyword search, ingest from recordings, the gallery, and every MCP tool — everything except
|
|
34
|
+
the vector arm of `search`/`similar` and live-URL measurement (`add https://…`, `refresh`).
|
|
35
|
+
Those two features sit behind `@huggingface/transformers` and `playwright-core`, listed as
|
|
36
|
+
optional `peerDependencies` rather than regular dependencies precisely so the `npx` path (what
|
|
37
|
+
the plugin actually runs) stays a core-only install. Measured on a fresh npm cache, a cold
|
|
38
|
+
install is 57 MB and takes 6 s, against 736 MB and 48 s when both are regular dependencies;
|
|
39
|
+
513 MB of the difference is `onnxruntime-node`, which transformers pulls in.
|
|
40
|
+
Add them yourself when you want both arms:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @huggingface/transformers@4.2.0 playwright-core@1.63.0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Without them, `search` degrades to the `fts` arm alone (`skipped` names the missing package),
|
|
47
|
+
and `add`/`refresh` refuse a URL input with a named reason instead of crashing.
|
|
48
|
+
|
|
30
49
|
## Adding an entry
|
|
31
50
|
|
|
32
51
|
Adding is two steps by design: the tool does the mechanical part, a Claude
|
|
@@ -348,6 +367,36 @@ The container runs as `node` (uid 1000), not root: `docker/entrypoint.sh` starts
|
|
|
348
367
|
root only to `chown` an existing (possibly root-owned) `/data` volume, then drops to
|
|
349
368
|
`node` via `setpriv` before exec'ing the server.
|
|
350
369
|
|
|
370
|
+
## Releasing
|
|
371
|
+
|
|
372
|
+
1. Bump `version` in `package.json`.
|
|
373
|
+
2. Tag the release: `git tag v0.2.0` (the workflow triggers on any `v*` tag).
|
|
374
|
+
3. Push the tag: `git push origin v0.2.0`.
|
|
375
|
+
4. `.github/workflows/publish.yml` runs on that push: checks out, installs with
|
|
376
|
+
`npm ci`, runs `npm test`, then `npm publish`. Trusted publishing means no npm
|
|
377
|
+
token lives in this repo — the registry trusts the GitHub Actions run itself via
|
|
378
|
+
OIDC (`id-token: write`).
|
|
379
|
+
|
|
380
|
+
One-time setup on npmjs.com, before the first tagged release:
|
|
381
|
+
|
|
382
|
+
1. Open the package's settings page on npmjs.com.
|
|
383
|
+
2. Under **Trusted Publisher**, add a publisher.
|
|
384
|
+
3. Choose **GitHub Actions** as the provider.
|
|
385
|
+
4. Organisation: `yojahny55`.
|
|
386
|
+
5. Repository: `wp-design-library`.
|
|
387
|
+
6. Workflow filename: `publish.yml` (the filename only, not the path under
|
|
388
|
+
`.github/workflows/`).
|
|
389
|
+
7. Environment: leave empty (the workflow does not define one).
|
|
390
|
+
|
|
391
|
+
Trusted publisher configurations created now default to **staged** publishing rather
|
|
392
|
+
than a direct `npm publish`. Either flip the allowed-actions toggle on the trusted
|
|
393
|
+
publisher to permit direct publishing (matches the workflow above as written), or
|
|
394
|
+
change the workflow's last step to `npm stage publish` and approve each release by
|
|
395
|
+
hand in the npm UI. Provenance is not generated for this release, because this repo
|
|
396
|
+
is private — provenance requires a public repository. After the first successful
|
|
397
|
+
publish, publishing access can be tightened to require 2FA and disallow tokens
|
|
398
|
+
without breaking this workflow, since it never uses a token.
|
|
399
|
+
|
|
351
400
|
## Known ceilings
|
|
352
401
|
|
|
353
402
|
- **Chrome runs without its own sandbox inside the container.** Docker's default
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yojahny/wp-design-library",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Design reference corpus and MCP server for claude-wp-builder",
|
|
5
|
+
"repository": { "type": "git", "url": "git+https://github.com/yojahny55/wp-design-library.git" },
|
|
6
|
+
"homepage": "https://github.com/yojahny55/wp-design-library#readme",
|
|
7
|
+
"bugs": { "url": "https://github.com/yojahny55/wp-design-library/issues" },
|
|
5
8
|
"type": "module",
|
|
6
9
|
"bin": { "library": "bin/library.mjs" },
|
|
7
10
|
"engines": { "node": ">=22" },
|
|
@@ -12,15 +15,35 @@
|
|
|
12
15
|
"serve": "node bin/library.mjs serve"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
|
-
"@huggingface/transformers": "4.2.0",
|
|
16
18
|
"@modelcontextprotocol/sdk": "^1.20.0",
|
|
17
19
|
"better-sqlite3": "^12.2.0",
|
|
18
20
|
"js-yaml": "^4.1.0",
|
|
19
|
-
"playwright-core": "1.63.0",
|
|
20
21
|
"sharp": "^0.34.3",
|
|
21
22
|
"sqlite-vec": "0.1.9",
|
|
22
23
|
"zod": "^3.25.0"
|
|
23
24
|
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@huggingface/transformers": "4.2.0",
|
|
27
|
+
"playwright-core": "1.63.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@huggingface/transformers": { "optional": true },
|
|
31
|
+
"playwright-core": { "optional": true }
|
|
32
|
+
},
|
|
24
33
|
"license": "MIT",
|
|
25
|
-
"private": false
|
|
34
|
+
"private": false,
|
|
35
|
+
"files": [
|
|
36
|
+
".dockerignore",
|
|
37
|
+
".env.example",
|
|
38
|
+
"Dockerfile",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md",
|
|
41
|
+
"bin/",
|
|
42
|
+
"docker-compose.yml",
|
|
43
|
+
"docker/",
|
|
44
|
+
"entries/",
|
|
45
|
+
"src/",
|
|
46
|
+
"tests/",
|
|
47
|
+
"vocab.yaml"
|
|
48
|
+
]
|
|
26
49
|
}
|
package/src/cli/check.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import { fetchModels, modelDirs } from '../index/embed.mjs';
|
|
|
7
7
|
export async function run(args = []) {
|
|
8
8
|
const p = paths();
|
|
9
9
|
if (args.includes('--fetch-models')) {
|
|
10
|
-
await fetchModels(p.models);
|
|
10
|
+
const r = await fetchModels(p.models);
|
|
11
|
+
if (r?.refused) { process.stdout.write(JSON.stringify(r) + '\n'); return 1; }
|
|
11
12
|
const d = modelDirs(p.models);
|
|
12
13
|
process.stdout.write(`model: ${d.text}\nmodel: ${d.image}\n`);
|
|
13
14
|
return 0;
|
package/src/index/embed.mjs
CHANGED
|
@@ -19,6 +19,12 @@ export function modelDirs(models) {
|
|
|
19
19
|
return { text: path.join(models, TEXT_MODEL), image: path.join(models, IMAGE_MODEL) };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Resolvable without importing/executing the module, so it works as a cheap, sync
|
|
23
|
+
// "is it installed" probe for the optional peer.
|
|
24
|
+
function packageAvailable(specifier) {
|
|
25
|
+
try { import.meta.resolve(specifier); return true; } catch { return false; }
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
export async function getEmbedder({ models, mode = process.env.LIBRARY_EMBED } = {}) {
|
|
23
29
|
if (mode === 'off') return null;
|
|
24
30
|
if (mode === 'stub') {
|
|
@@ -34,7 +40,9 @@ export async function getEmbedder({ models, mode = process.env.LIBRARY_EMBED } =
|
|
|
34
40
|
}
|
|
35
41
|
const d = modelDirs(models);
|
|
36
42
|
if (!fs.existsSync(d.text) || !fs.existsSync(d.image)) return null; // caller reads absentReason()
|
|
37
|
-
|
|
43
|
+
let tf;
|
|
44
|
+
try { tf = await import('@huggingface/transformers'); }
|
|
45
|
+
catch { return null; } // package not installed; caller reads absentReason()
|
|
38
46
|
tf.env.cacheDir = models; tf.env.allowRemoteModels = process.env.LIBRARY_FETCH_MODELS === '1';
|
|
39
47
|
const textPipe = await tf.pipeline('feature-extraction', TEXT_MODEL);
|
|
40
48
|
const proc = await tf.AutoProcessor.from_pretrained(IMAGE_MODEL);
|
|
@@ -55,13 +63,19 @@ export function absentReason(models, mode = process.env.LIBRARY_EMBED) {
|
|
|
55
63
|
// An explicit opt-out is the reason on its own; do not let a stale/present
|
|
56
64
|
// model dir on disk relabel it, and do not touch the filesystem to find out.
|
|
57
65
|
if (mode === 'off') return 'LIBRARY_EMBED=off';
|
|
66
|
+
// The package is a smaller, more fundamental blocker than an unfetched model
|
|
67
|
+
// directory, and it's checked first so a host missing the peer gets told that,
|
|
68
|
+
// not a misleading "models absent" (a fresh host has neither).
|
|
69
|
+
if (!packageAvailable('@huggingface/transformers')) return '@huggingface/transformers is not installed';
|
|
58
70
|
const d = modelDirs(models);
|
|
59
71
|
const missing = Object.entries(d).filter(([, p]) => !fs.existsSync(p)).map(([k]) => k);
|
|
60
72
|
return missing.length ? `models absent: ${missing.join(', ')} under ${models}` : null;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
export async function fetchModels(models) {
|
|
64
|
-
|
|
76
|
+
let tf;
|
|
77
|
+
try { tf = await import('@huggingface/transformers'); }
|
|
78
|
+
catch { return { refused: '@huggingface/transformers is not installed' }; }
|
|
65
79
|
tf.env.cacheDir = models; tf.env.allowRemoteModels = true;
|
|
66
80
|
await tf.pipeline('feature-extraction', TEXT_MODEL);
|
|
67
81
|
await tf.AutoProcessor.from_pretrained(IMAGE_MODEL);
|
package/src/index/query.mjs
CHANGED
|
@@ -20,7 +20,7 @@ function filterSql(filters = {}, col = 'slug') {
|
|
|
20
20
|
|
|
21
21
|
const RRF_K = 60;
|
|
22
22
|
|
|
23
|
-
export async function search(db, { query = '', filters = {}, limit = 10, embedder = null }) {
|
|
23
|
+
export async function search(db, { query = '', filters = {}, limit = 10, embedder = null, embedderReason = null }) {
|
|
24
24
|
const { where, params } = filterSql(filters, 'e.slug');
|
|
25
25
|
if (!query.trim()) {
|
|
26
26
|
const { where, params } = filterSql(filters);
|
|
@@ -39,7 +39,7 @@ export async function search(db, { query = '', filters = {}, limit = 10, embedde
|
|
|
39
39
|
return { arms: ['fts'], skipped: [{ arm: 'vec', reason }], hits: rows };
|
|
40
40
|
};
|
|
41
41
|
if (!(db.vec && embedder)) {
|
|
42
|
-
return ftsOnly(db.vec ? 'no embedder' : 'sqlite-vec not loaded');
|
|
42
|
+
return ftsOnly(db.vec ? (embedderReason ?? 'no embedder') : 'sqlite-vec not loaded');
|
|
43
43
|
}
|
|
44
44
|
let vec;
|
|
45
45
|
try {
|
|
@@ -65,9 +65,9 @@ export async function search(db, { query = '', filters = {}, limit = 10, embedde
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export async function similar(db, { slug, image, limit = 10, embedder = null }) {
|
|
68
|
+
export async function similar(db, { slug, image, limit = 10, embedder = null, embedderReason = null }) {
|
|
69
69
|
if (!(db.vec && embedder)) {
|
|
70
|
-
return { refused: `similar needs the image arm: ${db.vec ? 'no embedder' : 'sqlite-vec not loaded'}` };
|
|
70
|
+
return { refused: `similar needs the image arm: ${db.vec ? (embedderReason ?? 'no embedder') : 'sqlite-vec not loaded'}` };
|
|
71
71
|
}
|
|
72
72
|
let vec;
|
|
73
73
|
if (slug) {
|
package/src/ingest/measure.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { chromium } from 'playwright-core';
|
|
4
3
|
|
|
5
4
|
const VIEWPORT_WIDE = { width: 1440, height: 900 };
|
|
6
5
|
const VIEWPORT_NARROW = { width: 390, height: 844 };
|
|
@@ -87,6 +86,12 @@ function collect() {
|
|
|
87
86
|
// comes back as { ok: false, error }, with whatever partial output (frames,
|
|
88
87
|
// screenshots) had already been captured still attached.
|
|
89
88
|
export async function measure(url, outDir, { chrome = process.env.CHROME_PATH, record = false, stops = 12 } = {}) {
|
|
89
|
+
let chromium;
|
|
90
|
+
try {
|
|
91
|
+
({ chromium } = await import('playwright-core'));
|
|
92
|
+
} catch {
|
|
93
|
+
return { ok: false, error: 'playwright-core is not installed; install it for URL measurement' };
|
|
94
|
+
}
|
|
90
95
|
if (!chrome || !fs.existsSync(chrome)) {
|
|
91
96
|
return { ok: false, error: 'chrome not available: CHROME_PATH not set or not found' };
|
|
92
97
|
}
|
package/src/mcp/server.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { paths } from '../paths.mjs';
|
|
4
4
|
import { loadVocab } from '../vocab.mjs';
|
|
5
|
-
import { getEmbedder } from '../index/embed.mjs';
|
|
5
|
+
import { getEmbedder, absentReason } from '../index/embed.mjs';
|
|
6
6
|
import { registerPrompts } from './prompts.mjs';
|
|
7
7
|
import { registerResources } from './resources.mjs';
|
|
8
8
|
import { makeTools, registerTools } from './tools.mjs';
|
|
@@ -13,6 +13,8 @@ export async function buildServer(opts = {}) {
|
|
|
13
13
|
const ctx = { paths: p, vocab: loadVocab(p.vocab), transport, models: p.models };
|
|
14
14
|
// http passes its own cached embedder in (one per process, not one per request); stdio resolves its own.
|
|
15
15
|
ctx.embedder = 'embedder' in opts ? opts.embedder : await getEmbedder({ models: ctx.models });
|
|
16
|
+
// Names why, when it's null: off / package not installed / models absent are distinct.
|
|
17
|
+
ctx.embedderReason = ctx.embedder ? null : absentReason(ctx.models);
|
|
16
18
|
const server = new McpServer({ name: 'wp-design-library', version: p.version });
|
|
17
19
|
registerTools(server, makeTools(ctx));
|
|
18
20
|
registerPrompts(server, ctx);
|
package/src/mcp/tools.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export function makeTools(ctx) {
|
|
|
70
70
|
}),
|
|
71
71
|
handler: withDb(async (db, a) => {
|
|
72
72
|
const { filters, unknown } = resolveFilters(ctx.vocab, a.filters ?? {});
|
|
73
|
-
const result = await search(db, { ...a, filters, embedder: ctx.embedder });
|
|
73
|
+
const result = await search(db, { ...a, filters, embedder: ctx.embedder, embedderReason: ctx.embedderReason });
|
|
74
74
|
return unknown.length ? { ...result, unknown } : result;
|
|
75
75
|
}),
|
|
76
76
|
},
|
|
@@ -97,7 +97,7 @@ export function makeTools(ctx) {
|
|
|
97
97
|
const refusal = confineToInbox(ctx, a.image);
|
|
98
98
|
if (refusal) return { refused: refusal };
|
|
99
99
|
}
|
|
100
|
-
return similar(db, { ...a, embedder: ctx.embedder });
|
|
100
|
+
return similar(db, { ...a, embedder: ctx.embedder, embedderReason: ctx.embedderReason });
|
|
101
101
|
}),
|
|
102
102
|
},
|
|
103
103
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -eu
|
|
3
3
|
cd "$(dirname "$0")/../.."
|
|
4
|
+
command -v ffmpeg >/dev/null || { echo "SKIP: ffmpeg not installed"; exit 0; }
|
|
4
5
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
5
6
|
mkdir -p "$tmp/f" "$tmp/data/entries"
|
|
6
7
|
node --input-type=module -e 'import sharp from "sharp"; for (let i = 0; i < 60; i++) await sharp({create:{width:40,height:20,channels:3,background:{r:i*4,g:0,b:0}}}).png().toFile(process.argv[1] + "/" + i + ".png")' "$tmp/f"
|
|
@@ -6,7 +6,7 @@ mkdir -p "$tmp/entries"; cp -r tests/fixtures/entry-ok "$tmp/entries/entry-ok"
|
|
|
6
6
|
# models absent → fts only, skip reported, exit 0
|
|
7
7
|
out=$(LIBRARY_DATA="$tmp" LIBRARY_EMBED= node bin/library.mjs index 2>&1)
|
|
8
8
|
echo "$out" | grep -qE "arms: fts([^,]|$)" || { echo "FAIL: arms without models; got: $out"; exit 1; }
|
|
9
|
-
echo "$out" | grep -
|
|
9
|
+
echo "$out" | grep -qE "embeddings: skipped \((models absent|@huggingface/transformers is not installed)" || { echo "FAIL: skip line; got: $out"; exit 1; }
|
|
10
10
|
# stub → both arms, vectors stored with the right dims
|
|
11
11
|
out=$(LIBRARY_DATA="$tmp" LIBRARY_EMBED=stub node bin/library.mjs index 2>&1)
|
|
12
12
|
echo "$out" | grep -q "arms: fts,vec" || { echo "FAIL: arms with stub; got: $out"; exit 1; }
|
package/tests/checks/measure.sh
CHANGED
|
@@ -3,6 +3,8 @@ set -eu
|
|
|
3
3
|
cd "$(dirname "$0")/../.."
|
|
4
4
|
[ -n "${CHROME_PATH:-}" ] || CHROME_PATH=$(command -v google-chrome || command -v chromium || true)
|
|
5
5
|
[ -n "$CHROME_PATH" ] || { echo "SKIP: no chrome/chromium found"; exit 0; }
|
|
6
|
+
node --input-type=module -e 'await import("playwright-core")' 2>/dev/null \
|
|
7
|
+
|| { echo "SKIP: playwright-core not installed"; exit 0; }
|
|
6
8
|
export CHROME_PATH
|
|
7
9
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
8
10
|
mkdir -p "$tmp/entries"
|
|
@@ -51,7 +53,9 @@ if (strip.height < single.height * 2) { console.error('FAIL: strip height ' + st
|
|
|
51
53
|
# refresh re-measures and rewrites captured.at
|
|
52
54
|
out=$(LIBRARY_DATA="$tmp" node bin/library.mjs refresh fx)
|
|
53
55
|
echo "$out" | grep -q '"ok":true' || { echo "FAIL: refresh did not succeed"; echo "$out"; exit 1; }
|
|
54
|
-
|
|
56
|
+
# UTC, because measure.mjs writes `new Date().toISOString()`. Using local `date`
|
|
57
|
+
# here fails every evening west of Greenwich, on a correct entry.
|
|
58
|
+
today=$(date -u +%Y-%m-%d)
|
|
55
59
|
grep -Eq "^ at: '?$today'?\$" "$tmp/entries/fx/entry.md" || { echo "FAIL: captured.at not rewritten to today"; cat "$tmp/entries/fx/entry.md"; exit 1; }
|
|
56
60
|
|
|
57
61
|
# add without --record: strip is just the 1440 screenshot, method is measured (not both)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -eu
|
|
3
|
+
cd "$(dirname "$0")/../.."
|
|
4
|
+
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
5
|
+
mkdir -p "$tmp/entries"; cp -r tests/fixtures/entry-ok "$tmp/entries/entry-ok"
|
|
6
|
+
|
|
7
|
+
# A sandbox that genuinely lacks the two optional peers: copy the code, then symlink
|
|
8
|
+
# every real node_modules entry except @huggingface/transformers and playwright-core.
|
|
9
|
+
# Scoped packages are symlinked individually under @scope/, not the scope directory
|
|
10
|
+
# itself — symlinking the scope dir would drag the excluded package back in.
|
|
11
|
+
sandbox="$tmp/sandbox"
|
|
12
|
+
mkdir -p "$sandbox/node_modules"
|
|
13
|
+
cp -r bin src package.json vocab.yaml "$sandbox/"
|
|
14
|
+
for entry in node_modules/*; do
|
|
15
|
+
name=$(basename "$entry")
|
|
16
|
+
case "$name" in
|
|
17
|
+
@*|playwright-core) continue ;;
|
|
18
|
+
esac
|
|
19
|
+
ln -s "$PWD/$entry" "$sandbox/node_modules/$name"
|
|
20
|
+
done
|
|
21
|
+
for scope in node_modules/@*; do
|
|
22
|
+
sname=$(basename "$scope")
|
|
23
|
+
mkdir -p "$sandbox/node_modules/$sname"
|
|
24
|
+
for pkg in "$scope"/*; do
|
|
25
|
+
pname=$(basename "$pkg")
|
|
26
|
+
[ "$sname/$pname" = "@huggingface/transformers" ] && continue
|
|
27
|
+
ln -s "$PWD/$pkg" "$sandbox/node_modules/$sname/$pname"
|
|
28
|
+
done
|
|
29
|
+
done
|
|
30
|
+
[ ! -e "$sandbox/node_modules/playwright-core" ] || { echo "FAIL: sandbox still has playwright-core"; exit 1; }
|
|
31
|
+
[ ! -e "$sandbox/node_modules/@huggingface/transformers" ] || { echo "FAIL: sandbox still has @huggingface/transformers"; exit 1; }
|
|
32
|
+
|
|
33
|
+
# search over stdio: fts only, skip names the transformers package
|
|
34
|
+
{
|
|
35
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}'
|
|
36
|
+
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
37
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search","arguments":{"query":"parallax"}}}'
|
|
38
|
+
sleep 1
|
|
39
|
+
} | LIBRARY_DATA="$tmp" LIBRARY_SEED="$tmp/noseed" timeout 10 node "$sandbox/bin/library.mjs" serve >"$tmp/out" 2>"$tmp/err" || true
|
|
40
|
+
grep -q '"arms":\["fts"\]' "$tmp/out" || { echo "FAIL: arms not fts-only; got:"; cat "$tmp/out"; exit 1; }
|
|
41
|
+
grep -q '"skipped":\[{"arm":"vec","reason":"@huggingface/transformers is not installed"}\]' "$tmp/out" \
|
|
42
|
+
|| { echo "FAIL: skipped reason did not name the transformers package; got:"; cat "$tmp/out"; exit 1; }
|
|
43
|
+
|
|
44
|
+
# add <https url>: refused, naming playwright-core
|
|
45
|
+
out=$(LIBRARY_DATA="$tmp" node "$sandbox/bin/library.mjs" add https://example.com --slug px-url --title "Px Url" --source own --license x) || true
|
|
46
|
+
echo "$out" | grep -q '"refused"' || { echo "FAIL: https add did not refuse; got: $out"; exit 1; }
|
|
47
|
+
echo "$out" | grep -q "playwright-core is not installed" || { echo "FAIL: refusal did not name playwright-core; got: $out"; exit 1; }
|
|
48
|
+
|
|
49
|
+
# add <local recording>: still works — the regression a static playwright-core import
|
|
50
|
+
# in measure.mjs would cause, since add.mjs imports measure.mjs unconditionally
|
|
51
|
+
out=$(LIBRARY_DATA="$tmp" node "$sandbox/bin/library.mjs" add "$PWD/tests/fixtures/three-frame.webp" --slug px-local --title "Px Local" --source own --license x)
|
|
52
|
+
echo "$out" | grep -q '"blanks"' || { echo "FAIL: local-file add did not draft; got: $out"; exit 1; }
|
|
53
|
+
[ -f "$tmp/entries/px-local/strip.png" ] || { echo "FAIL: strip.png missing for local-file add"; exit 1; }
|
|
54
|
+
|
|
55
|
+
echo PASS
|
package/tests/run.sh
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -u
|
|
3
3
|
cd "$(dirname "$0")/.."
|
|
4
|
-
pass=0; fail=0
|
|
4
|
+
pass=0; fail=0; skip=0
|
|
5
|
+
# A check that cannot run its subject exits 0 after printing SKIP. Counting those as
|
|
6
|
+
# PASS would let a runner missing every tool report a clean suite, which is how three
|
|
7
|
+
# absent-dependency failures reached the publish workflow unnoticed.
|
|
5
8
|
for f in tests/checks/*.sh; do
|
|
6
|
-
if out=$(bash "$f" 2>&1); then
|
|
9
|
+
if out=$(bash "$f" 2>&1); then
|
|
10
|
+
if printf '%s' "$out" | grep -q '^SKIP'; then
|
|
11
|
+
skip=$((skip+1)); echo "SKIP $f ($(printf '%s' "$out" | grep -m1 '^SKIP' | cut -c7-))"
|
|
12
|
+
else pass=$((pass+1)); echo "PASS $f"; fi
|
|
7
13
|
else fail=$((fail+1)); echo "FAIL $f"; echo "$out" | tail -20; fi
|
|
8
14
|
done
|
|
9
|
-
echo "PASS=$pass FAIL=$fail"
|
|
15
|
+
echo "PASS=$pass SKIP=$skip FAIL=$fail"
|
|
10
16
|
[ "$fail" -eq 0 ]
|