@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 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 <slug> [dir]` | Download a reproduced-site bundle: structure only (code, layout, motion, text). Images are same-size blank placeholders; video, audio and fonts are left out (originals stay with their owners). Verifies sha256, refuses a non-empty dir, unknown slug exits `1` with closest slugs. Base URL override: `ASKEWLY_SITE_BUNDLE_URL`. |
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 { getSite, listSites, siteNextStep, SiteError } from "./site.js";
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.command("site").description("reproduced-site bundles: structure only (code, layout, motion, text); images are blank placeholders, no video or fonts");
479
- siteCmd
480
- .command("list")
481
- .option("--json", "machine-readable output")
482
- .description("list downloadable site bundles")
483
- .action(async (options) => {
484
- try {
485
- const index = await listSites();
486
- if (options.json)
487
- return void console.log(JSON.stringify(index, null, 2));
488
- for (const e of index)
489
- console.log(`${e.slug} ${(e.bytes / 1024).toFixed(0)} KB ${e.files} files`);
490
- }
491
- catch (error) {
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
- // M141 Ruling 11: 재현 사이트 구조 묶음 — `site list` · `site get <slug> [dir]`.
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(index, slug, n = 3) {
23
+ export function suggestSites(slugs, slug, n = 3) {
45
24
  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) };
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 async function listSites(base = bundleBaseUrl()) {
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
- const file = entry.file ?? `${slug}.zip`;
102
- const zip = await fetchBuffer(`${base}/${file}`);
103
- const sha = createHash("sha256").update(zip).digest("hex");
104
- if (sha !== entry.sha256)
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.7.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",