@askewly/design 0.7.0 → 1.0.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 +1 -1
- package/dist/index.js +49 -39
- package/dist/local.js +41 -1
- package/dist/site.js +11 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx @askewly/design verify src
|
|
|
20
20
|
| `recipes list` / `recipes show <id>` | Agent-ready implementation contracts (200+ recipes). |
|
|
21
21
|
| `knowledge list` / `search <query>` / `show <id>` | Design knowledge rules: decision tables and judgment gates. |
|
|
22
22
|
| `assets search <query>` / `assets show <id>` | One md per asset (registry + recipes): definition, usage rules, install command. Bundled, works offline. `show` on an unknown id exits `1` with 3 closest ids (0.6.0). |
|
|
23
|
-
| `site list` / `site get
|
|
23
|
+
| `site list` / `site get` | No longer available. Whole-site bundles are not published since 2026-11-06; each site's design guide and notes are at https://ui.askewly.com/sites. |
|
|
24
24
|
| `prompt <id>` | Paste-ready agent prompt: asset md URL + install command + usage rules + one-line instruction (0.6.0). |
|
|
25
25
|
|
|
26
26
|
## Why `verify` exists
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@ 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
12
|
import { assetUrl, buildPrompt, loadAssets, searchAssets, suggestAssets } from "./assets.js";
|
|
13
|
-
import {
|
|
14
|
-
import { localRepo, LocalToolError, siteFind, siteResolve } from "./local.js";
|
|
13
|
+
import { siteNextStep, SiteError } from "./site.js";
|
|
14
|
+
import { localRepo, LocalToolError, siteFind, siteResolve, siteGet, siteSlugs } from "./local.js";
|
|
15
15
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
16
16
|
// Set only by the local `askde` shim; npm and npx users never see the repo-only commands.
|
|
17
17
|
const repo = localRepo();
|
|
@@ -475,44 +475,54 @@ program
|
|
|
475
475
|
console.log(buildPrompt(assetOrFail(id)));
|
|
476
476
|
});
|
|
477
477
|
// M141 Ruling 11: 재현 사이트 구조 묶음(코드·텍스트 그대로, 이미지는 단색 빈 자리, 영상·폰트 제외)을 받는다.
|
|
478
|
-
const siteCmd = program
|
|
479
|
-
|
|
480
|
-
.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (error instanceof SiteError)
|
|
493
|
-
fail(error.message);
|
|
494
|
-
throw error;
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
siteCmd
|
|
498
|
-
.command("get")
|
|
499
|
-
.argument("<slug>", "site slug, e.g. stripe-home")
|
|
500
|
-
.argument("[dir]", "target dir (default ./<slug>); must be empty or missing")
|
|
501
|
-
.description("download a site bundle, verify sha256, extract into [dir] (never overwrites)")
|
|
502
|
-
.action(async (slug, dir) => {
|
|
503
|
-
try {
|
|
504
|
-
const target = dir ?? slug;
|
|
505
|
-
const n = await getSite(slug, target);
|
|
506
|
-
console.log(`${n} files -> ${target}
|
|
507
|
-
${siteNextStep(target)}`);
|
|
508
|
-
}
|
|
509
|
-
catch (error) {
|
|
510
|
-
if (error instanceof SiteError)
|
|
511
|
-
fail(error.message);
|
|
512
|
-
throw error;
|
|
513
|
-
}
|
|
514
|
-
});
|
|
478
|
+
const siteCmd = program
|
|
479
|
+
.command("site")
|
|
480
|
+
.description(repo ? "local: recreated sites in this repo" : "whole-site bundles are no longer published");
|
|
481
|
+
const SITES_NOT_PUBLISHED = `Whole-site bundles are no longer published.
|
|
482
|
+
See https://ui.askewly.com/sites for each site's design guide and notes, and use \`askewly-design find "<element>"\` for published parts.`;
|
|
483
|
+
if (!repo) {
|
|
484
|
+
siteCmd.command("list").description("no longer available").action(() => fail(SITES_NOT_PUBLISHED));
|
|
485
|
+
siteCmd
|
|
486
|
+
.command("get")
|
|
487
|
+
.argument("[slug]")
|
|
488
|
+
.argument("[dir]")
|
|
489
|
+
.description("no longer available")
|
|
490
|
+
.action(() => fail(SITES_NOT_PUBLISHED));
|
|
491
|
+
}
|
|
515
492
|
if (repo) {
|
|
493
|
+
siteCmd
|
|
494
|
+
.command("list")
|
|
495
|
+
.option("--json", "machine-readable output")
|
|
496
|
+
.description("local: list recreated sites in this repo")
|
|
497
|
+
.action((options) => {
|
|
498
|
+
try {
|
|
499
|
+
const slugs = siteSlugs(repo);
|
|
500
|
+
console.log(options.json ? JSON.stringify(slugs, null, 2) : slugs.join("\n"));
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
if (error instanceof LocalToolError)
|
|
504
|
+
fail(error.message);
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
siteCmd
|
|
509
|
+
.command("get")
|
|
510
|
+
.argument("<slug>", "site slug, e.g. stripe-home")
|
|
511
|
+
.argument("[dir]", "target dir (default ./<slug>); must be empty or missing")
|
|
512
|
+
.description("local: pack a site from this repo and extract it into [dir] (never overwrites)")
|
|
513
|
+
.action((slug, dir) => {
|
|
514
|
+
try {
|
|
515
|
+
const target = dir ?? slug;
|
|
516
|
+
const n = siteGet(repo, slug, target);
|
|
517
|
+
console.log(`${n} files -> ${target}
|
|
518
|
+
${siteNextStep(target)}`);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (error instanceof LocalToolError || error instanceof SiteError)
|
|
522
|
+
fail(error.message);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
516
526
|
siteCmd
|
|
517
527
|
.command("find")
|
|
518
528
|
.argument("<query>", "element or site, e.g. menu")
|
package/dist/local.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Repo-only commands for the local `askde` shim (cs724). They run the existing scripts, so the
|
|
2
2
|
// repo keeps one source of truth, and they never appear for npm users.
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
7
|
+
import { assertEmptyTarget, extractSiteZip, suggestSites } from "./site.js";
|
|
6
8
|
export class LocalToolError extends Error {
|
|
7
9
|
}
|
|
8
10
|
export function localRepo() {
|
|
@@ -21,6 +23,44 @@ function runRepoTool(repo, command, script, args) {
|
|
|
21
23
|
throw new LocalToolError(`could not run ${command}: ${result.error.message}`);
|
|
22
24
|
return result.status ?? 1;
|
|
23
25
|
}
|
|
26
|
+
const PACK_SCRIPT = "scripts/recreate/pack-site-bundles.mjs";
|
|
27
|
+
function runPack(repo, args) {
|
|
28
|
+
const file = path.join(repo, PACK_SCRIPT);
|
|
29
|
+
if (!existsSync(file))
|
|
30
|
+
throw new LocalToolError(`${PACK_SCRIPT} not found under ASKDE_REPO=${repo}`);
|
|
31
|
+
const result = spawnSync(process.execPath, [file, ...args], {
|
|
32
|
+
cwd: repo,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
35
|
+
env: { ...process.env, PYTHONUTF8: "1" },
|
|
36
|
+
});
|
|
37
|
+
if (result.error)
|
|
38
|
+
throw new LocalToolError(`could not run ${PACK_SCRIPT}: ${result.error.message}`);
|
|
39
|
+
if (result.status !== 0)
|
|
40
|
+
throw new LocalToolError(result.stderr.trim() || `${PACK_SCRIPT} exited ${result.status}`);
|
|
41
|
+
return result.stdout;
|
|
42
|
+
}
|
|
43
|
+
export function siteSlugs(repo) {
|
|
44
|
+
return runPack(repo, ["--list"]).split(/\r?\n/).filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
/** Packs one site from the repo into a throwaway folder and extracts it. Nothing leaves this machine. */
|
|
47
|
+
export function siteGet(repo, slug, dir) {
|
|
48
|
+
const slugs = siteSlugs(repo);
|
|
49
|
+
if (!slugs.includes(slug)) {
|
|
50
|
+
const near = suggestSites(slugs, slug).map((s) => ` - ${s}`);
|
|
51
|
+
throw new LocalToolError(`unknown site "${slug}" — closest:\n${near.join("\n")}\nor: askde site list`);
|
|
52
|
+
}
|
|
53
|
+
assertEmptyTarget(dir);
|
|
54
|
+
const out = mkdtempSync(path.join(tmpdir(), "askde-site-"));
|
|
55
|
+
try {
|
|
56
|
+
runPack(repo, ["--out", out, slug]);
|
|
57
|
+
const [entry] = JSON.parse(readFileSync(path.join(out, "index.json"), "utf8"));
|
|
58
|
+
return extractSiteZip(readFileSync(path.join(out, entry.file)), dir);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(out, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
24
64
|
export function siteFind(repo, query, limit) {
|
|
25
65
|
const python = process.platform === "win32" ? "python" : "python3";
|
|
26
66
|
// `--` keeps a query like "--approve" from reaching catalog.py's write options.
|
package/dist/site.js
CHANGED
|
@@ -1,32 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 재현 사이트 구조 묶음의 압축 해제 — 로컬 `askde site get` 전용(2026-11-06 공개 배포 폐지).
|
|
2
2
|
// 묶음 = scripts/recreate/pack-site-bundles.mjs 산출물(코드·텍스트 그대로, 이미지는 단색 빈 자리, 영상·폰트 제외).
|
|
3
3
|
// 압축 해제는 node 내장 zlib 만 쓴다(새 의존성 없음).
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
4
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
5
|
import path from "node:path";
|
|
7
6
|
import { inflateRawSync } from "node:zlib";
|
|
8
|
-
export const SITE_BUNDLE_BASE_URL = "https://bundles.askewly.com";
|
|
9
7
|
export class SiteError extends Error {
|
|
10
8
|
}
|
|
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
9
|
function editDistance(a, b) {
|
|
31
10
|
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
32
11
|
for (let i = 1; i <= a.length; i++) {
|
|
@@ -41,12 +20,12 @@ function editDistance(a, b) {
|
|
|
41
20
|
return row[b.length];
|
|
42
21
|
}
|
|
43
22
|
/** 없는 slug 에 대한 근접 후보 — assets.ts suggestAssets 와 같은 규칙(부분 문자열 우선, 편집 거리). */
|
|
44
|
-
export function suggestSites(
|
|
23
|
+
export function suggestSites(slugs, slug, n = 3) {
|
|
45
24
|
const needle = slug.toLowerCase();
|
|
46
|
-
return
|
|
47
|
-
.map((
|
|
48
|
-
const a =
|
|
49
|
-
return { slug:
|
|
25
|
+
return slugs
|
|
26
|
+
.map((candidate) => {
|
|
27
|
+
const a = candidate.toLowerCase();
|
|
28
|
+
return { slug: candidate, key: (a.includes(needle) || needle.includes(a) ? 0 : 1000) + editDistance(a, needle) };
|
|
50
29
|
})
|
|
51
30
|
.sort((x, y) => x.key - y.key || x.slug.localeCompare(y.slug))
|
|
52
31
|
.slice(0, n)
|
|
@@ -85,24 +64,13 @@ export function siteNextStep(dir) {
|
|
|
85
64
|
}
|
|
86
65
|
return `read ${path.join(dir, "ASKEWLY-BUNDLE.md")} for what was left out and how to run it`;
|
|
87
66
|
}
|
|
88
|
-
export
|
|
89
|
-
return fetchSiteIndex(base);
|
|
90
|
-
}
|
|
91
|
-
/** 묶음을 받아 sha256 대조 후 dir 에 푼다. 비어 있지 않은 폴더·해시 불일치·없는 slug 는 SiteError. 반환 = 푼 파일 수. */
|
|
92
|
-
export async function getSite(slug, dir, base = bundleBaseUrl()) {
|
|
93
|
-
const index = await fetchSiteIndex(base);
|
|
94
|
-
const entry = index.find((e) => e.slug === slug);
|
|
95
|
-
if (!entry) {
|
|
96
|
-
const near = suggestSites(index, slug).map((s) => ` - ${s}`);
|
|
97
|
-
throw new SiteError(`unknown site "${slug}" — closest:\n${near.join("\n")}\nor: askewly-design site list`);
|
|
98
|
-
}
|
|
67
|
+
export function assertEmptyTarget(dir) {
|
|
99
68
|
if (existsSync(dir) && readdirSync(dir).length > 0)
|
|
100
69
|
throw new SiteError(`${dir} is not empty — refusing to overwrite`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
throw new SiteError(`sha256 mismatch for ${file} (expected ${entry.sha256}, got ${sha})`);
|
|
70
|
+
}
|
|
71
|
+
/** zip 을 dir 에 푼다. 비어 있지 않은 폴더·경로 탈출은 SiteError 이고 아무것도 쓰지 않는다. 반환 = 푼 파일 수. */
|
|
72
|
+
export function extractSiteZip(zip, dir) {
|
|
73
|
+
assertEmptyTarget(dir);
|
|
106
74
|
const files = readZip(zip);
|
|
107
75
|
const root = path.resolve(dir);
|
|
108
76
|
for (const f of files) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askewly/design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://ui.askewly.com",
|