@askewly/design 0.5.1 → 0.6.1
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 +34 -31
- package/data/assets.json +1 -0
- package/data/knowledge.json +1 -1
- package/data/recipes.json +1 -1
- package/dist/assets.js +83 -0
- package/dist/index.js +89 -0
- package/dist/site.js +113 -0
- package/package.json +50 -50
package/dist/assets.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// step-11 (M141): 자산별 md 조회 — `assets search` · `assets show` · `prompt`.
|
|
2
|
+
// 데이터 = build-data.mjs 가 번들한 data/assets.json (public/llms/assets/*.md 를 그대로 실음).
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
export const ASSET_BASE_URL = "https://ui.askewly.com/llms/assets";
|
|
5
|
+
export function loadAssets() {
|
|
6
|
+
return JSON.parse(readFileSync(new URL("../data/assets.json", import.meta.url), "utf8"));
|
|
7
|
+
}
|
|
8
|
+
/** Case-insensitive substring search over id, title (high), keywords (mid), definition (low). */
|
|
9
|
+
export function searchAssets(assets, query) {
|
|
10
|
+
const needle = query.trim().toLowerCase();
|
|
11
|
+
if (!needle)
|
|
12
|
+
return [];
|
|
13
|
+
const scored = [];
|
|
14
|
+
for (const asset of assets) {
|
|
15
|
+
const id = asset.id.toLowerCase();
|
|
16
|
+
const title = asset.title.toLowerCase();
|
|
17
|
+
const keywords = asset.keywords.map((k) => String(k).toLowerCase());
|
|
18
|
+
let score = 0;
|
|
19
|
+
if (id === needle || title === needle)
|
|
20
|
+
score = 100;
|
|
21
|
+
else if (id.includes(needle) || title.includes(needle))
|
|
22
|
+
score = 60;
|
|
23
|
+
else if (keywords.some((k) => k === needle || k.includes(needle)))
|
|
24
|
+
score = 40;
|
|
25
|
+
else if (asset.definition.toLowerCase().includes(needle))
|
|
26
|
+
score = 20;
|
|
27
|
+
if (score > 0)
|
|
28
|
+
scored.push({ asset, score });
|
|
29
|
+
}
|
|
30
|
+
return scored.sort((a, b) => b.score - a.score || a.asset.id.localeCompare(b.asset.id)).map((s) => s.asset);
|
|
31
|
+
}
|
|
32
|
+
function editDistance(a, b) {
|
|
33
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
34
|
+
for (let i = 1; i <= a.length; i++) {
|
|
35
|
+
let prev = row[0];
|
|
36
|
+
row[0] = i;
|
|
37
|
+
for (let j = 1; j <= b.length; j++) {
|
|
38
|
+
const tmp = row[j];
|
|
39
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
40
|
+
prev = tmp;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return row[b.length];
|
|
44
|
+
}
|
|
45
|
+
/** 없는 id 에 대한 근접 후보 — 부분 문자열 일치 우선, 그다음 편집 거리. 항상 최대 n 개. */
|
|
46
|
+
export function suggestAssets(assets, id, n = 3) {
|
|
47
|
+
const needle = id.toLowerCase();
|
|
48
|
+
return assets
|
|
49
|
+
.map((asset) => {
|
|
50
|
+
const a = asset.id.toLowerCase();
|
|
51
|
+
const sub = a.includes(needle) || needle.includes(a) ? 0 : 1;
|
|
52
|
+
return { asset, key: sub * 1000 + editDistance(a, needle) };
|
|
53
|
+
})
|
|
54
|
+
.sort((x, y) => x.key - y.key || x.asset.id.localeCompare(y.asset.id))
|
|
55
|
+
.slice(0, n)
|
|
56
|
+
.map((s) => s.asset);
|
|
57
|
+
}
|
|
58
|
+
export function assetUrl(asset) {
|
|
59
|
+
return `${ASSET_BASE_URL}/${asset.id}.md`;
|
|
60
|
+
}
|
|
61
|
+
export function usageRules(asset) {
|
|
62
|
+
const match = asset.md.match(/^## 사용 규칙\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
|
|
63
|
+
return match ? match[1].trim() : null;
|
|
64
|
+
}
|
|
65
|
+
/** 에이전트에 그대로 붙여넣는 프롬프트. 설치 명령이 없는 자산은 recipe 를 읽고 짓게 한다. */
|
|
66
|
+
export function buildPrompt(asset) {
|
|
67
|
+
const lines = [`Use the Askewly Design asset "${asset.title}" (${asset.id}).`, "", `Definition: ${asset.definition}`, `Asset page (read first): ${assetUrl(asset)}`];
|
|
68
|
+
if (asset.install) {
|
|
69
|
+
lines.push(`Install: ${asset.install}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
lines.push(`Install: none (not in the registry). Build it from the recipe: ${asset.recipe_url ?? assetUrl(asset)}`);
|
|
73
|
+
}
|
|
74
|
+
const rules = usageRules(asset);
|
|
75
|
+
if (rules) {
|
|
76
|
+
lines.push("", "Usage rules:", rules);
|
|
77
|
+
}
|
|
78
|
+
const respect = rules ? ", and respect the usage rules above" : "";
|
|
79
|
+
lines.push("", asset.install
|
|
80
|
+
? `Instruction: read the asset page, run the install command and wire it to the project's tokens${respect}. Then run \`npx @askewly/design verify\`.`
|
|
81
|
+
: `Instruction: read the asset page and the recipe and build it with the project's tokens${respect}. Then run \`npx @askewly/design verify\`.`);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { MASCOT_POSES, renderMascot } from "./mascot.js";
|
|
|
9
9
|
import { verifyDir } from "./verify.js";
|
|
10
10
|
import { loadKnowledge, loadRecipes, loadTerms, loadTokens, loadTokensCss, searchKnowledge, searchTerms, tokensForTier, } from "./load.js";
|
|
11
11
|
import { recall, RECALL_AXES } from "./data.js";
|
|
12
|
+
import { assetUrl, buildPrompt, loadAssets, searchAssets, suggestAssets } from "./assets.js";
|
|
13
|
+
import { getSite, listSites, SiteError } from "./site.js";
|
|
12
14
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
15
|
const program = new Command();
|
|
14
16
|
program
|
|
@@ -363,4 +365,91 @@ data
|
|
|
363
365
|
console.log(` ${hit.summary}`);
|
|
364
366
|
}
|
|
365
367
|
});
|
|
368
|
+
// step-11 (M141): 자산별 md(공개 URL https://ui.askewly.com/llms/assets/<id>.md) 를 번들에서 오프라인으로 읽는다.
|
|
369
|
+
const assetsCmd = program.command("assets").description("registry + recipe assets (one md per asset: definition, usage rules, install command)");
|
|
370
|
+
assetsCmd
|
|
371
|
+
.command("search")
|
|
372
|
+
.argument("<query>", "text to match against id, name, pattern group, term refs, definition")
|
|
373
|
+
.option("--json", "machine-readable output")
|
|
374
|
+
.option("--limit <n>", "max results", "10")
|
|
375
|
+
.description("search assets — each line: id, one-line definition, installable or recipe only")
|
|
376
|
+
.action((query, options) => {
|
|
377
|
+
const results = searchAssets(loadAssets(), query).slice(0, Number(options.limit) || 10);
|
|
378
|
+
if (results.length === 0)
|
|
379
|
+
fail(`no assets match "${query}"`);
|
|
380
|
+
if (options.json) {
|
|
381
|
+
console.log(JSON.stringify(results.map((a) => ({ id: a.id, definition: a.definition, installable: a.install !== null, install: a.install, url: assetUrl(a) })), null, 2));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
for (const asset of results) {
|
|
385
|
+
console.log(`${asset.id} ${asset.definition} [${asset.install ? "installable" : "recipe only"}]`);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
function assetOrFail(id) {
|
|
389
|
+
const all = loadAssets();
|
|
390
|
+
const asset = all.find((a) => a.id === id);
|
|
391
|
+
if (!asset) {
|
|
392
|
+
const near = suggestAssets(all, id).map((a) => ` - ${a.id}`);
|
|
393
|
+
fail(`unknown asset "${id}" — closest:\n${near.join("\n")}\nor: askewly-design assets search <query>`);
|
|
394
|
+
}
|
|
395
|
+
return asset;
|
|
396
|
+
}
|
|
397
|
+
assetsCmd
|
|
398
|
+
.command("show")
|
|
399
|
+
.argument("<id>", "asset id, e.g. split-tone-hero")
|
|
400
|
+
.option("--json", "machine-readable output (metadata + md)")
|
|
401
|
+
.description("print the asset md (definition, usage rules, install command, recipe link)")
|
|
402
|
+
.action((id, options) => {
|
|
403
|
+
const asset = assetOrFail(id);
|
|
404
|
+
if (options.json) {
|
|
405
|
+
console.log(JSON.stringify(asset, null, 2));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
process.stdout.write(asset.md.endsWith("\n") ? asset.md : `${asset.md}\n`);
|
|
409
|
+
});
|
|
410
|
+
program
|
|
411
|
+
.command("prompt")
|
|
412
|
+
.argument("<id>", "asset id, e.g. split-tone-hero")
|
|
413
|
+
.description("print a paste-ready agent prompt: asset md URL + install command + usage rules + one-line instruction")
|
|
414
|
+
.action((id) => {
|
|
415
|
+
console.log(buildPrompt(assetOrFail(id)));
|
|
416
|
+
});
|
|
417
|
+
// M141 Ruling 11: 재현 사이트 구조 묶음(코드·텍스트 그대로, 이미지는 단색 빈 자리, 영상·폰트 제외)을 받는다.
|
|
418
|
+
const siteCmd = program.command("site").description("reproduced-site bundles: structure only (code, layout, motion, text); images are blank placeholders, no video or fonts");
|
|
419
|
+
siteCmd
|
|
420
|
+
.command("list")
|
|
421
|
+
.option("--json", "machine-readable output")
|
|
422
|
+
.description("list downloadable site bundles")
|
|
423
|
+
.action(async (options) => {
|
|
424
|
+
try {
|
|
425
|
+
const index = await listSites();
|
|
426
|
+
if (options.json)
|
|
427
|
+
return void console.log(JSON.stringify(index, null, 2));
|
|
428
|
+
for (const e of index)
|
|
429
|
+
console.log(`${e.slug} ${(e.bytes / 1024).toFixed(0)} KB ${e.files} files`);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (error instanceof SiteError)
|
|
433
|
+
fail(error.message);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
siteCmd
|
|
438
|
+
.command("get")
|
|
439
|
+
.argument("<slug>", "site slug, e.g. stripe-home")
|
|
440
|
+
.argument("[dir]", "target dir (default ./<slug>); must be empty or missing")
|
|
441
|
+
.description("download a site bundle, verify sha256, extract into [dir] (never overwrites)")
|
|
442
|
+
.action(async (slug, dir) => {
|
|
443
|
+
try {
|
|
444
|
+
const target = dir ?? slug;
|
|
445
|
+
const n = await getSite(slug, target);
|
|
446
|
+
console.log(`${n} files -> ${target}
|
|
447
|
+
read ${path.join(target, "ASKEWLY-BUNDLE.md")} for what was left out and how to run it`);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof SiteError)
|
|
451
|
+
fail(error.message);
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
366
455
|
program.parse();
|
package/dist/site.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// M141 Ruling 11: 재현 사이트 구조 묶음 — `site list` · `site get <slug> [dir]`.
|
|
2
|
+
// 묶음 = scripts/recreate/pack-site-bundles.mjs 산출물(코드·텍스트 그대로, 이미지는 단색 빈 자리, 영상·폰트 제외).
|
|
3
|
+
// 압축 해제는 node 내장 zlib 만 쓴다(새 의존성 없음).
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { inflateRawSync } from "node:zlib";
|
|
8
|
+
export const SITE_BUNDLE_BASE_URL = "https://bundles.askewly.com";
|
|
9
|
+
export class SiteError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export function bundleBaseUrl() {
|
|
12
|
+
return (process.env.ASKEWLY_SITE_BUNDLE_URL || SITE_BUNDLE_BASE_URL).replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
async function fetchBuffer(url) {
|
|
15
|
+
let res;
|
|
16
|
+
try {
|
|
17
|
+
res = await fetch(url);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new SiteError(`cannot reach ${url}: ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new SiteError(`GET ${url} -> HTTP ${res.status}`);
|
|
24
|
+
return Buffer.from(await res.arrayBuffer());
|
|
25
|
+
}
|
|
26
|
+
// 목록은 엣지 캐시를 건너뛴다 — zip 은 이름에 해시가 있어 캐시돼도 된다
|
|
27
|
+
export async function fetchSiteIndex(base = bundleBaseUrl()) {
|
|
28
|
+
return JSON.parse((await fetchBuffer(`${base}/index.json?v=${Date.now()}`)).toString("utf8"));
|
|
29
|
+
}
|
|
30
|
+
function editDistance(a, b) {
|
|
31
|
+
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
32
|
+
for (let i = 1; i <= a.length; i++) {
|
|
33
|
+
let prev = row[0];
|
|
34
|
+
row[0] = i;
|
|
35
|
+
for (let j = 1; j <= b.length; j++) {
|
|
36
|
+
const tmp = row[j];
|
|
37
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
38
|
+
prev = tmp;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return row[b.length];
|
|
42
|
+
}
|
|
43
|
+
/** 없는 slug 에 대한 근접 후보 — assets.ts suggestAssets 와 같은 규칙(부분 문자열 우선, 편집 거리). */
|
|
44
|
+
export function suggestSites(index, slug, n = 3) {
|
|
45
|
+
const needle = slug.toLowerCase();
|
|
46
|
+
return index
|
|
47
|
+
.map((e) => {
|
|
48
|
+
const a = e.slug.toLowerCase();
|
|
49
|
+
return { slug: e.slug, key: (a.includes(needle) || needle.includes(a) ? 0 : 1000) + editDistance(a, needle) };
|
|
50
|
+
})
|
|
51
|
+
.sort((x, y) => x.key - y.key || x.slug.localeCompare(y.slug))
|
|
52
|
+
.slice(0, n)
|
|
53
|
+
.map((s) => s.slug);
|
|
54
|
+
}
|
|
55
|
+
/** zip(중앙 디렉터리) → [{name, data}]. store(0)·deflate(8)만 지원. 경로 탈출 이름은 거부. */
|
|
56
|
+
export function readZip(buf) {
|
|
57
|
+
const eocd = buf.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
|
|
58
|
+
if (eocd < 0)
|
|
59
|
+
throw new SiteError("not a zip file");
|
|
60
|
+
let count = buf.readUInt16LE(eocd + 10);
|
|
61
|
+
let p = buf.readUInt32LE(eocd + 16);
|
|
62
|
+
const out = [];
|
|
63
|
+
while (count--) {
|
|
64
|
+
if (buf.readUInt32LE(p) !== 0x02014b50)
|
|
65
|
+
throw new SiteError("corrupt zip directory");
|
|
66
|
+
const method = buf.readUInt16LE(p + 10);
|
|
67
|
+
const csize = buf.readUInt32LE(p + 20);
|
|
68
|
+
const nl = buf.readUInt16LE(p + 28);
|
|
69
|
+
const el = buf.readUInt16LE(p + 30);
|
|
70
|
+
const cl = buf.readUInt16LE(p + 32);
|
|
71
|
+
const off = buf.readUInt32LE(p + 42);
|
|
72
|
+
const name = buf.toString("utf8", p + 46, p + 46 + nl);
|
|
73
|
+
const start = off + 30 + buf.readUInt16LE(off + 26) + buf.readUInt16LE(off + 28);
|
|
74
|
+
const raw = buf.subarray(start, start + csize);
|
|
75
|
+
if (method !== 0 && method !== 8)
|
|
76
|
+
throw new SiteError(`unsupported zip method ${method} for ${name}`);
|
|
77
|
+
out.push({ name, data: method === 8 ? inflateRawSync(raw) : Buffer.from(raw) });
|
|
78
|
+
p += 46 + nl + el + cl;
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
export async function listSites(base = bundleBaseUrl()) {
|
|
83
|
+
return fetchSiteIndex(base);
|
|
84
|
+
}
|
|
85
|
+
/** 묶음을 받아 sha256 대조 후 dir 에 푼다. 비어 있지 않은 폴더·해시 불일치·없는 slug 는 SiteError. 반환 = 푼 파일 수. */
|
|
86
|
+
export async function getSite(slug, dir, base = bundleBaseUrl()) {
|
|
87
|
+
const index = await fetchSiteIndex(base);
|
|
88
|
+
const entry = index.find((e) => e.slug === slug);
|
|
89
|
+
if (!entry) {
|
|
90
|
+
const near = suggestSites(index, slug).map((s) => ` - ${s}`);
|
|
91
|
+
throw new SiteError(`unknown site "${slug}" — closest:\n${near.join("\n")}\nor: askewly-design site list`);
|
|
92
|
+
}
|
|
93
|
+
if (existsSync(dir) && readdirSync(dir).length > 0)
|
|
94
|
+
throw new SiteError(`${dir} is not empty — refusing to overwrite`);
|
|
95
|
+
const file = entry.file ?? `${slug}.zip`;
|
|
96
|
+
const zip = await fetchBuffer(`${base}/${file}`);
|
|
97
|
+
const sha = createHash("sha256").update(zip).digest("hex");
|
|
98
|
+
if (sha !== entry.sha256)
|
|
99
|
+
throw new SiteError(`sha256 mismatch for ${file} (expected ${entry.sha256}, got ${sha})`);
|
|
100
|
+
const files = readZip(zip);
|
|
101
|
+
const root = path.resolve(dir);
|
|
102
|
+
for (const f of files) {
|
|
103
|
+
const dest = path.resolve(root, f.name);
|
|
104
|
+
if (!dest.startsWith(root + path.sep))
|
|
105
|
+
throw new SiteError(`unsafe path in zip: ${f.name}`);
|
|
106
|
+
}
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
const dest = path.resolve(root, f.name);
|
|
109
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
110
|
+
writeFileSync(dest, f.data);
|
|
111
|
+
}
|
|
112
|
+
return files.length;
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@askewly/design",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"homepage": "https://ui.askewly.com",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/lumatic2/ui-dictionary.git",
|
|
10
|
-
"directory": "packages/cli"
|
|
11
|
-
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
"design-system",
|
|
14
|
-
"design-tokens",
|
|
15
|
-
"linter",
|
|
16
|
-
"ui",
|
|
17
|
-
"coding-agent"
|
|
18
|
-
],
|
|
19
|
-
"type": "module",
|
|
20
|
-
"bin": {
|
|
21
|
-
"askewly-design": "./dist/index.js"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"dist",
|
|
25
|
-
"data",
|
|
26
|
-
"README.md",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
],
|
|
29
|
-
"publishConfig": {
|
|
30
|
-
"access": "public"
|
|
31
|
-
},
|
|
32
|
-
"engines": {
|
|
33
|
-
"node": ">=18"
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build:data": "node scripts/build-data.mjs",
|
|
37
|
-
"build": "npm run build:data && tsc",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"prepack": "npm run build"
|
|
40
|
-
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"commander": "^13.1.0"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/node": "^22.10.0",
|
|
46
|
-
"typescript": "^5.7.0",
|
|
47
|
-
"vitest": "^3.0.0",
|
|
48
|
-
"yaml": "^2.6.0"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@askewly/design",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://ui.askewly.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/lumatic2/ui-dictionary.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"design-system",
|
|
14
|
+
"design-tokens",
|
|
15
|
+
"linter",
|
|
16
|
+
"ui",
|
|
17
|
+
"coding-agent"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"askewly-design": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"data",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build:data": "node scripts/build-data.mjs",
|
|
37
|
+
"build": "npm run build:data && tsc",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"prepack": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"commander": "^13.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.10.0",
|
|
46
|
+
"typescript": "^5.7.0",
|
|
47
|
+
"vitest": "^3.0.0",
|
|
48
|
+
"yaml": "^2.6.0"
|
|
49
|
+
}
|
|
50
|
+
}
|