@askewly/design 0.6.4 → 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 +36 -34
- package/data/askewly.css +56 -56
- package/data/assets.json +1 -1
- package/data/knowledge.json +1 -1
- package/data/recipes.json +1 -1
- package/dist/data.js +25 -6
- package/dist/index.js +121 -69
- package/dist/local.js +76 -0
- package/dist/mascot.js +25 -25
- package/dist/site.js +11 -43
- package/package.json +50 -50
package/dist/data.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// M110 앞단 — 레포 자산 조회
|
|
1
|
+
// M110 앞단 — 레포 자산 조회 통합. 정식 이름 `find`, `data recall` 은 영구 별칭(cs724).
|
|
2
2
|
// design-intake.md §5단계와 askewly-design SKILL §2.7 이 약속해 온 `askewly data recall`
|
|
3
3
|
// 이 유령 명령이었다(이 기기 어디에도 `data` 커맨드가 없었다) — 그 약속을 실재하게 만든다.
|
|
4
4
|
//
|
|
5
5
|
// 번들된 두 축(terms · recipes) + knowledge 축을 한 번에 훑는다. methodology/ 는
|
|
6
6
|
// build-data.mjs 가 애초에 번들하지 않으므로(knowledge/*.md 만 읽는다) 여기서도 다루지
|
|
7
7
|
// 않는다 — 새 데이터 로더를 만들지 않고 기존 load.ts 만 재사용한다는 이 step 의 원칙.
|
|
8
|
-
//
|
|
9
|
-
// 로 보완한다(design-intake.md 가 그 경계를 함께 적는다).
|
|
8
|
+
// 여러 축을 넓게 찾는 일은 `askda recall` 소관이다(design-intake.md 가 그 경계를 함께 적는다).
|
|
10
9
|
import { loadKnowledge, loadRecipes, loadTerms, searchKnowledge, searchTerms, } from "./load.js";
|
|
11
|
-
|
|
10
|
+
import { assetUrl, loadAssets, searchAssets } from "./assets.js";
|
|
11
|
+
export const RECALL_AXES = ["terms", "recipes", "assets", "knowledge"];
|
|
12
12
|
function summarize(text, max = 120) {
|
|
13
13
|
if (!text)
|
|
14
14
|
return "";
|
|
@@ -46,6 +46,7 @@ function termHit(term) {
|
|
|
46
46
|
id: term.id,
|
|
47
47
|
path: `docs/ui-vocabulary/terms.yml#${term.id}`,
|
|
48
48
|
summary: summarize(term.one_liner ?? term.en?.name ?? term.id),
|
|
49
|
+
next: `terms show ${term.id}`,
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
function recipeHit(recipe) {
|
|
@@ -54,35 +55,53 @@ function recipeHit(recipe) {
|
|
|
54
55
|
id: recipe.id,
|
|
55
56
|
path: recipe.source_path,
|
|
56
57
|
summary: summarize(`${recipe.name ? `${recipe.name} (${recipe.pattern_group ?? "-"})` : recipe.id}${recipe.status ? ` [${recipe.status}]` : ""}`),
|
|
58
|
+
next: `recipes show ${recipe.id}`,
|
|
57
59
|
status: recipe.status,
|
|
58
60
|
};
|
|
59
61
|
}
|
|
62
|
+
function assetHit(asset) {
|
|
63
|
+
return {
|
|
64
|
+
axis: "assets",
|
|
65
|
+
id: asset.id,
|
|
66
|
+
path: assetUrl(asset),
|
|
67
|
+
summary: summarize(`${asset.definition} [${asset.install ? "installable" : "recipe only"}]`),
|
|
68
|
+
next: `assets show ${asset.id}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
60
71
|
function knowledgeHit(doc) {
|
|
61
72
|
return {
|
|
62
73
|
axis: "knowledge",
|
|
63
74
|
id: doc.id,
|
|
64
75
|
path: doc.source_path,
|
|
65
76
|
summary: summarize(doc.description),
|
|
77
|
+
next: `knowledge show ${doc.id}`,
|
|
66
78
|
};
|
|
67
79
|
}
|
|
68
80
|
/**
|
|
69
|
-
* Search terms + recipes + knowledge in one pass and return path + one-line
|
|
81
|
+
* Search terms + recipes + assets + knowledge in one pass and return path + one-line
|
|
70
82
|
* summary per hit. This is an index, not the source text — the caller must
|
|
71
83
|
* still open `path` and read the body before judging.
|
|
72
84
|
*/
|
|
73
85
|
export function recall(query, limit) {
|
|
74
86
|
const terms = searchTerms(loadTerms(), query).map(termHit);
|
|
75
87
|
const recipes = searchRecipes(loadRecipes(), query).map(recipeHit);
|
|
88
|
+
// Most assets share an id with a recipe; keep one row so duplicates don't eat the --limit window.
|
|
89
|
+
const recipeIds = new Set(recipes.map((hit) => hit.id));
|
|
90
|
+
const assets = searchAssets(loadAssets(), query)
|
|
91
|
+
.filter((asset) => !recipeIds.has(asset.id))
|
|
92
|
+
.map(assetHit);
|
|
76
93
|
const knowledge = searchKnowledge(loadKnowledge(), query).map(knowledgeHit);
|
|
77
94
|
// Interleave by axis (round-robin) so a query that happens to match many
|
|
78
95
|
// terms doesn't crowd recipes/knowledge out of a small --limit window.
|
|
79
96
|
const merged = [];
|
|
80
|
-
const maxLen = Math.max(terms.length, recipes.length, knowledge.length);
|
|
97
|
+
const maxLen = Math.max(terms.length, recipes.length, assets.length, knowledge.length);
|
|
81
98
|
for (let i = 0; i < maxLen; i++) {
|
|
82
99
|
if (terms[i])
|
|
83
100
|
merged.push(terms[i]);
|
|
84
101
|
if (recipes[i])
|
|
85
102
|
merged.push(recipes[i]);
|
|
103
|
+
if (assets[i])
|
|
104
|
+
merged.push(assets[i]);
|
|
86
105
|
if (knowledge[i])
|
|
87
106
|
merged.push(knowledge[i]);
|
|
88
107
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,14 @@ 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 {
|
|
13
|
+
import { siteNextStep, SiteError } from "./site.js";
|
|
14
|
+
import { localRepo, LocalToolError, siteFind, siteResolve, siteGet, siteSlugs } from "./local.js";
|
|
14
15
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
16
|
+
// Set only by the local `askde` shim; npm and npx users never see the repo-only commands.
|
|
17
|
+
const repo = localRepo();
|
|
15
18
|
const program = new Command();
|
|
16
19
|
program
|
|
17
|
-
.name("askewly-design")
|
|
20
|
+
.name(repo ? "askde" : "askewly-design")
|
|
18
21
|
.description("Query the Askewly Design system: UI vocabulary terms, design tokens, and component recipes.")
|
|
19
22
|
.version(pkg.version);
|
|
20
23
|
function fail(message) {
|
|
@@ -388,37 +391,40 @@ program
|
|
|
388
391
|
const color = options.color && Boolean(process.stdout.isTTY);
|
|
389
392
|
console.log(renderMascot(pose, { color }));
|
|
390
393
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
}
|
|
394
|
+
function addFindCommand(parent, name) {
|
|
395
|
+
parent
|
|
396
|
+
.command(name)
|
|
397
|
+
.argument("<query>", "text to match across terms / recipes / assets / knowledge")
|
|
398
|
+
.option("--limit <n>", "max results", "5")
|
|
399
|
+
.option("--json", "machine-readable output")
|
|
400
|
+
.description("search terms + recipes + assets + knowledge in one pass — an index of paths, not the source text")
|
|
401
|
+
.action((query, options) => {
|
|
402
|
+
const limit = Number(options.limit) || 5;
|
|
403
|
+
const hits = recall(query, limit);
|
|
404
|
+
if (options.json) {
|
|
405
|
+
console.log(JSON.stringify({
|
|
406
|
+
note: "index only — read `path` before judging",
|
|
407
|
+
axes: RECALL_AXES,
|
|
408
|
+
query,
|
|
409
|
+
hits,
|
|
410
|
+
}, null, 2));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
console.log("색인이지 본문이 아니다 — 후보 경로를 받은 뒤 판단은 본문을 읽고 한다.");
|
|
414
|
+
if (hits.length === 0) {
|
|
415
|
+
console.log(`no matches for "${query}" across axes: ${RECALL_AXES.join(", ")} (methodology/ is not bundled here)`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const hit of hits) {
|
|
419
|
+
console.log(`[${hit.axis}] ${hit.id} — ${hit.path}`);
|
|
420
|
+
console.log(` ${hit.summary}`);
|
|
421
|
+
console.log(` next: ${hit.next}`);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
addFindCommand(program, "find");
|
|
426
|
+
// Permanent alias: 0.5.1+ users and published docs call `data recall`.
|
|
427
|
+
addFindCommand(program.command("data").description("alias of `find`, kept for existing callers"), "recall");
|
|
422
428
|
// step-11 (M141): 자산별 md(공개 URL https://ui.askewly.com/llms/assets/<id>.md) 를 번들에서 오프라인으로 읽는다.
|
|
423
429
|
const assetsCmd = program.command("assets").description("registry + recipe assets (one md per asset: definition, usage rules, install command)");
|
|
424
430
|
assetsCmd
|
|
@@ -469,41 +475,87 @@ program
|
|
|
469
475
|
console.log(buildPrompt(assetOrFail(id)));
|
|
470
476
|
});
|
|
471
477
|
// M141 Ruling 11: 재현 사이트 구조 묶음(코드·텍스트 그대로, 이미지는 단색 빈 자리, 영상·폰트 제외)을 받는다.
|
|
472
|
-
const siteCmd = program
|
|
473
|
-
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
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}
|
|
501
518
|
${siteNextStep(target)}`);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
});
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (error instanceof LocalToolError || error instanceof SiteError)
|
|
522
|
+
fail(error.message);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
siteCmd
|
|
527
|
+
.command("find")
|
|
528
|
+
.argument("<query>", "element or site, e.g. menu")
|
|
529
|
+
.option("--limit <n>", "max results", "5")
|
|
530
|
+
.description("local: search approved recreated-site modules in this repo")
|
|
531
|
+
.action((query, options) => {
|
|
532
|
+
const limit = Number(options.limit);
|
|
533
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
534
|
+
fail("--limit must be a positive integer");
|
|
535
|
+
try {
|
|
536
|
+
process.exitCode = siteFind(repo, query, limit);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof LocalToolError)
|
|
540
|
+
fail(error.message);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
siteCmd
|
|
545
|
+
.command("resolve")
|
|
546
|
+
.argument("<reference>", "copied site pointer or URL")
|
|
547
|
+
.option("--file <path>", "save this manifest file under tmp/site-references after verifying its hash")
|
|
548
|
+
.option("--local", "read the same revision from the local snapshot")
|
|
549
|
+
.description("local: verify a copied site pointer and optionally fetch one pinned source file")
|
|
550
|
+
.action((reference, options) => {
|
|
551
|
+
try {
|
|
552
|
+
process.exitCode = siteResolve(repo, reference, options);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (error instanceof LocalToolError)
|
|
556
|
+
fail(error.message);
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
509
561
|
await program.parseAsync();
|
package/dist/local.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Repo-only commands for the local `askde` shim (cs724). They run the existing scripts, so the
|
|
2
|
+
// repo keeps one source of truth, and they never appear for npm users.
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { assertEmptyTarget, extractSiteZip, suggestSites } from "./site.js";
|
|
8
|
+
export class LocalToolError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export function localRepo() {
|
|
11
|
+
return process.env.ASKDE_REPO?.trim() || null;
|
|
12
|
+
}
|
|
13
|
+
function runRepoTool(repo, command, script, args) {
|
|
14
|
+
const file = path.join(repo, script);
|
|
15
|
+
if (!existsSync(file))
|
|
16
|
+
throw new LocalToolError(`${script} not found under ASKDE_REPO=${repo}`);
|
|
17
|
+
const result = spawnSync(command, [file, ...args], {
|
|
18
|
+
cwd: repo,
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: { ...process.env, PYTHONUTF8: "1" },
|
|
21
|
+
});
|
|
22
|
+
if (result.error)
|
|
23
|
+
throw new LocalToolError(`could not run ${command}: ${result.error.message}`);
|
|
24
|
+
return result.status ?? 1;
|
|
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
|
+
}
|
|
64
|
+
export function siteFind(repo, query, limit) {
|
|
65
|
+
const python = process.platform === "win32" ? "python" : "python3";
|
|
66
|
+
// `--` keeps a query like "--approve" from reaching catalog.py's write options.
|
|
67
|
+
return runRepoTool(repo, python, "scripts/recreate/catalog.py", ["--limit", String(limit), "--", query]);
|
|
68
|
+
}
|
|
69
|
+
export function siteResolve(repo, reference, options) {
|
|
70
|
+
const args = [reference];
|
|
71
|
+
if (options.local)
|
|
72
|
+
args.push("--local");
|
|
73
|
+
if (options.file)
|
|
74
|
+
args.push("--file", options.file);
|
|
75
|
+
return runRepoTool(repo, process.execPath, "scripts/recreate/resolve-site-reference.mjs", args);
|
|
76
|
+
}
|
package/dist/mascot.js
CHANGED
|
@@ -4,34 +4,34 @@
|
|
|
4
4
|
// 사용 규칙(정본 §3): 라벤더 #B298DC 단색 고정 · 등폭 전제 · 기계 소비 출력 금지.
|
|
5
5
|
export const MASCOT_POSES = ["lying", "standing", "sleepy", "mini"];
|
|
6
6
|
const ART = {
|
|
7
|
-
lying: ` ▄██▄ ▄██▄
|
|
8
|
-
█ ██ █ ██
|
|
9
|
-
▄██████████████████▄
|
|
10
|
-
█▄ █████████████████████
|
|
11
|
-
▀█▄ ████ ████ █████████
|
|
12
|
-
▀██▄████ ████ █████████
|
|
13
|
-
▀███████████████████▀
|
|
7
|
+
lying: ` ▄██▄ ▄██▄
|
|
8
|
+
█ ██ █ ██
|
|
9
|
+
▄██████████████████▄
|
|
10
|
+
█▄ █████████████████████
|
|
11
|
+
▀█▄ ████ ████ █████████
|
|
12
|
+
▀██▄████ ████ █████████
|
|
13
|
+
▀███████████████████▀
|
|
14
14
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀`,
|
|
15
|
-
standing: ` ▄██▄ ▄██▄
|
|
16
|
-
█ ██ █ ██
|
|
17
|
-
▄██████████████▄
|
|
18
|
-
███ ████ █████
|
|
19
|
-
███ ████ █████
|
|
20
|
-
████████████████
|
|
21
|
-
████████████████
|
|
22
|
-
▀████▀▀▀████▀▀██
|
|
15
|
+
standing: ` ▄██▄ ▄██▄
|
|
16
|
+
█ ██ █ ██
|
|
17
|
+
▄██████████████▄
|
|
18
|
+
███ ████ █████
|
|
19
|
+
███ ████ █████
|
|
20
|
+
████████████████
|
|
21
|
+
████████████████
|
|
22
|
+
▀████▀▀▀████▀▀██
|
|
23
23
|
████ ████ ▀███▄▄▄`,
|
|
24
|
-
sleepy: ` ▄██▄ ▄██▄
|
|
25
|
-
█ ██ █ ██
|
|
26
|
-
▄██████████████████▄
|
|
27
|
-
█▄ █████████████████████
|
|
28
|
-
▀█▄ █████████████████████
|
|
29
|
-
▀██▄███ ██ ████████
|
|
30
|
-
▀███████████████████▀
|
|
24
|
+
sleepy: ` ▄██▄ ▄██▄
|
|
25
|
+
█ ██ █ ██
|
|
26
|
+
▄██████████████████▄
|
|
27
|
+
█▄ █████████████████████
|
|
28
|
+
▀█▄ █████████████████████
|
|
29
|
+
▀██▄███ ██ ████████
|
|
30
|
+
▀███████████████████▀
|
|
31
31
|
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀`,
|
|
32
|
-
mini: ` ▄██▄▄▄▄██▄
|
|
33
|
-
██████████
|
|
34
|
-
██ ██ ██
|
|
32
|
+
mini: ` ▄██▄▄▄▄██▄
|
|
33
|
+
██████████
|
|
34
|
+
██ ██ ██
|
|
35
35
|
▀████████▀`,
|
|
36
36
|
};
|
|
37
37
|
/** askewly.lavender #B298DC — ANSI 24-bit. */
|
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,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": "1.0.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
|
+
}
|