kasaframe 0.1.4 → 0.1.5
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/NOTICE +298 -0
- package/README.md +16 -3
- package/docs/AGENTS.md +1 -1
- package/package.json +1 -1
- package/packages/cli/src/commands/doctor.ts +15 -2
- package/packages/cli/src/commands/fonts.ts +118 -0
- package/packages/cli/src/commands/update.ts +66 -0
- package/packages/cli/src/fonts.ts +405 -0
- package/packages/cli/src/kf.ts +5 -0
- package/packages/cli/src/update.ts +250 -0
- package/packages/plan/src/asks.ts +1 -1
- package/packages/plan/src/themes.ts +8 -2
- package/packages/themes/kit/THEME-MAKE.md +2 -2
- package/packages/themes/kit/fonts/display.json +3517 -0
- package/packages/themes/kit/fonts/sans.json +5155 -0
- package/packages/themes/kit/fonts/serif.json +5229 -0
- package/packages/themes/kit/fonts.json +243 -79
- package/packages/themes/package.json +3 -1
- package/packages/themes/src/fontfile.ts +183 -0
- package/packages/themes/src/fonts.ts +296 -0
- package/packages/themes/src/index.ts +10 -2
- package/packages/themes/src/look.ts +34 -8
- package/packages/themes/src/types.ts +5 -0
- package/packages/themes/src/vary.ts +35 -34
- package/presets/kawaii-pastel.json +2 -2
- package/skills/kasaframe/SKILL.md +1 -0
- package/themes/blueprint/sample/style.json +5 -5
- package/themes/blueprint/style.json +5 -5
- package/themes/blueprint/theme.json +3 -1
- package/themes/blueprint/variants/deep-rose-suit.json +1 -1
- package/themes/blueprint/variants/navy-coral-2.json +1 -1
- package/themes/blueprint/variants/navy-coral-suit.json +1 -1
- package/themes/blueprint/variants/navy-orange-2.json +1 -1
- package/themes/blueprint/variants/navy-orange-suit.json +1 -1
- package/themes/cartography/sample/style.json +2 -2
- package/themes/cartography/style.json +2 -2
- package/themes/cartography/theme.json +3 -1
- package/themes/cartography/variants/deep-plum.json +1 -1
- package/themes/cartography/variants/frost-teal.json +1 -1
- package/themes/cartography/variants/navy-green.json +1 -1
- package/themes/circuit-board/sample/style.json +2 -2
- package/themes/circuit-board/style.json +2 -2
- package/themes/circuit-board/theme.json +3 -1
- package/themes/circuit-board/variants/dusk-gold-pretendard.json +1 -1
- package/themes/circuit-board/variants/navy-gold-grain.json +1 -1
- package/themes/circuit-board/variants/wine-gold-pretendard.json +1 -1
- package/themes/clay-toy/sample/style.json +2 -2
- package/themes/clay-toy/style.json +2 -2
- package/themes/clay-toy/theme.json +3 -1
- package/themes/clay-toy/variants/blush-coral-grain.json +1 -1
- package/themes/clay-toy/variants/chalk-teal.json +1 -1
- package/themes/clay-toy/variants/frost-sky.json +1 -1
- package/themes/clay-toy/variants/frost-teal.json +1 -1
- package/themes/clay-toy/variants/lilac-plum-round.json +1 -1
- package/themes/clay-toy/variants/lilac-plum.json +1 -1
- package/themes/clay-toy/variants/sage-indigo.json +1 -1
- package/themes/conveyor-factory/sample/style.json +3 -3
- package/themes/conveyor-factory/style.json +3 -3
- package/themes/conveyor-factory/theme.json +3 -1
- package/themes/conveyor-factory/variants/cocoa-gold.json +1 -1
- package/themes/conveyor-factory/variants/navy-gold.json +1 -1
- package/themes/conveyor-factory/variants/slate-amber-2.json +1 -1
- package/themes/conveyor-factory/variants/slate-amber-suit.json +1 -1
- package/themes/conveyor-factory/variants/slate-gold-bold.json +1 -1
- package/themes/data-journal/theme.json +3 -1
- package/themes/data-journal/variants/deep-coral.json +1 -1
- package/themes/data-journal/variants/navy-lime.json +1 -1
- package/themes/data-journal/variants/wine-indigo.json +1 -1
- package/themes/debug-hud/sample/style.json +1 -1
- package/themes/debug-hud/style.json +1 -1
- package/themes/debug-hud/theme.json +2 -1
- package/themes/debug-hud/variants/cocoa-teal.json +2 -2
- package/themes/debug-hud/variants/deep-teal-scan.json +2 -2
- package/themes/debug-hud/variants/dusk-plum.json +2 -2
- package/themes/debug-hud/variants/forest-lime.json +2 -2
- package/themes/debug-hud/variants/slate-green.json +2 -2
- package/themes/debug-hud/variants/wine-gold.json +2 -2
- package/themes/depth-flight/sample/style.json +1 -1
- package/themes/depth-flight/style.json +1 -1
- package/themes/depth-flight/theme.json +3 -1
- package/themes/depth-flight/variants/cocoa-cyan.json +1 -1
- package/themes/depth-flight/variants/cocoa-violet.json +1 -1
- package/themes/depth-flight/variants/deep-sky.json +1 -1
- package/themes/depth-flight/variants/forest-blue.json +1 -1
- package/themes/depth-flight/variants/navy-sky-grain.json +1 -1
- package/themes/depth-flight/variants/navy-sky.json +1 -1
- package/themes/depth-flight/variants/night-teal.json +1 -1
- package/themes/depth-flight/variants/slate-indigo.json +1 -1
- package/themes/depth-flight/variants/slate-violet.json +1 -1
- package/themes/film-reel/sample/style.json +2 -2
- package/themes/film-reel/style.json +2 -2
- package/themes/film-reel/theme.json +3 -1
- package/themes/film-reel/variants/chalk-pink.json +1 -1
- package/themes/film-reel/variants/lilac-gold.json +1 -1
- package/themes/film-reel/variants/lilac-violet.json +1 -1
- package/themes/glass-light/sample/style.json +1 -1
- package/themes/glass-light/style.json +1 -1
- package/themes/glass-light/theme.json +3 -1
- package/themes/glass-light/variants/blush-pink.json +1 -1
- package/themes/glass-light/variants/blush-plum.json +1 -1
- package/themes/glass-light/variants/cream-violet.json +1 -1
- package/themes/glass-light/variants/frost-sky-suit.json +1 -1
- package/themes/glass-light/variants/frost-sky.json +1 -1
- package/themes/glass-light/variants/sage-green.json +1 -1
- package/themes/ink-wash/sample/style.json +3 -3
- package/themes/ink-wash/style.json +3 -3
- package/themes/ink-wash/theme.json +3 -1
- package/themes/iso-diorama/sample/style.json +1 -1
- package/themes/iso-diorama/style.json +1 -1
- package/themes/iso-diorama/theme.json +3 -1
- package/themes/iso-diorama/variants/chalk-plum.json +1 -1
- package/themes/iso-diorama/variants/cocoa-indigo.json +1 -1
- package/themes/iso-diorama/variants/dusk-blue.json +1 -1
- package/themes/iso-diorama/variants/dusk-coral.json +1 -1
- package/themes/iso-diorama/variants/dusk-jade.json +1 -1
- package/themes/iso-diorama/variants/forest-cyan.json +1 -1
- package/themes/iso-diorama/variants/frost-green.json +1 -1
- package/themes/iso-diorama/variants/frost-teal.json +1 -1
- package/themes/iso-diorama/variants/navy-amber.json +1 -1
- package/themes/isotype/sample/style.json +2 -2
- package/themes/isotype/style.json +2 -2
- package/themes/isotype/theme.json +2 -1
- package/themes/isotype/variants/blush-rose.json +1 -1
- package/themes/isotype/variants/chalk-coral.json +1 -1
- package/themes/isotype/variants/chalk-orange.json +1 -1
- package/themes/isotype/variants/frost-amber.json +1 -1
- package/themes/isotype/variants/frost-coral.json +1 -1
- package/themes/isotype/variants/frost-orange.json +1 -1
- package/themes/isotype/variants/sand-amber.json +1 -1
- package/themes/kawaii-sticker/sample/style.json +2 -2
- package/themes/kawaii-sticker/style.json +2 -2
- package/themes/kawaii-sticker/theme.json +3 -1
- package/themes/kawaii-sticker/variants/cream-gold.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-green.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-indigo.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-sky.json +1 -1
- package/themes/kawaii-sticker/variants/dusk-teal.json +1 -1
- package/themes/kawaii-sticker/variants/frost-blue.json +1 -1
- package/themes/kawaii-sticker/variants/frost-plum.json +1 -1
- package/themes/kawaii-sticker/variants/lilac-violet.json +1 -1
- package/themes/kawaii-sticker/variants/navy-gold.json +1 -1
- package/themes/kawaii-sticker/variants/navy-plum.json +1 -1
- package/themes/lab-notebook/sample/style.json +3 -3
- package/themes/lab-notebook/style.json +3 -3
- package/themes/lab-notebook/theme.json +2 -1
- package/themes/lab-notebook/variants/blush-orange.json +1 -1
- package/themes/lab-notebook/variants/chalk-coral.json +1 -1
- package/themes/lab-notebook/variants/chalk-orange.json +1 -1
- package/themes/lab-notebook/variants/chalk-rose.json +1 -1
- package/themes/lab-notebook/variants/lilac-rose.json +1 -1
- package/themes/lab-notebook/variants/sage-rose.json +1 -1
- package/themes/lab-notebook/variants/sand-orange.json +1 -1
- package/themes/lab-notebook/variants/snow-orange.json +1 -1
- package/themes/lab-notebook/variants/snow-rose.json +1 -1
- package/themes/manga-panel/sample/style.json +3 -3
- package/themes/manga-panel/style.json +3 -3
- package/themes/manga-panel/theme.json +3 -1
- package/themes/manga-panel/variants/chalk-cyan-dots.json +1 -1
- package/themes/manga-panel/variants/chalk-rose.json +1 -1
- package/themes/manga-panel/variants/snow-cyan.json +1 -1
- package/themes/manga-panel/variants/snow-magenta-dots.json +1 -1
- package/themes/marble-run/sample/style.json +2 -2
- package/themes/marble-run/style.json +2 -2
- package/themes/marble-run/theme.json +3 -1
- package/themes/marble-run/variants/slate-orange.json +1 -1
- package/themes/neon-sign/sample/style.json +2 -2
- package/themes/neon-sign/style.json +2 -2
- package/themes/neon-sign/theme.json +2 -1
- package/themes/neon-sign/variants/dusk-plum.json +1 -1
- package/themes/neon-sign/variants/forest-magenta.json +1 -1
- package/themes/neon-sign/variants/night-pink.json +1 -1
- package/themes/neon-sign/variants/night-violet.json +1 -1
- package/themes/neon-sign/variants/slate-violet.json +1 -1
- package/themes/newspaper/sample/style.json +2 -2
- package/themes/newspaper/style.json +2 -2
- package/themes/newspaper/theme.json +2 -1
- package/themes/newspaper/variants/blush-amber.json +1 -1
- package/themes/newspaper/variants/chalk-magenta.json +1 -1
- package/themes/newspaper/variants/frost-sky.json +1 -1
- package/themes/newspaper/variants/lilac-green.json +1 -1
- package/themes/newspaper/variants/sage-lime.json +1 -1
- package/themes/og-measure/THEME.md +32 -6
- package/themes/og-measure/effects.ts +219 -115
- package/themes/og-measure/sample/assets/brand/og-logo.svg +11 -11
- package/themes/og-measure/sample/lib/data.ts +6 -0
- package/themes/og-measure/sample/scenes/end.ts +14 -10
- package/themes/og-measure/sample/scenes/gather.ts +12 -1
- package/themes/og-measure/sample/scenes/models.ts +13 -2
- package/themes/og-measure/sample/scenes/one.ts +8 -6
- package/themes/og-measure/sample/scenes/price.ts +20 -3
- package/themes/og-measure/sample/style.json +30 -35
- package/themes/og-measure/slots.ts +25 -30
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +30 -35
- package/themes/og-measure/theme.json +64 -49
- package/themes/og-measure/variants/snow-blue.json +90 -0
- package/themes/orbit-system/sample/style.json +3 -3
- package/themes/orbit-system/style.json +3 -3
- package/themes/orbit-system/theme.json +3 -1
- package/themes/orbit-system/variants/cocoa-amber.json +1 -1
- package/themes/orbit-system/variants/cocoa-cyan.json +1 -1
- package/themes/orbit-system/variants/dusk-magenta.json +1 -1
- package/themes/orbit-system/variants/dusk-violet.json +1 -1
- package/themes/orbit-system/variants/forest-coral.json +1 -1
- package/themes/orbit-system/variants/wine-amber.json +1 -1
- package/themes/orbit-system/variants/wine-magenta.json +1 -1
- package/themes/origami-fold/sample/style.json +1 -1
- package/themes/origami-fold/style.json +1 -1
- package/themes/origami-fold/theme.json +3 -1
- package/themes/origami-fold/variants/deep-amber-sunflower.json +1 -1
- package/themes/origami-fold/variants/deep-orange.json +1 -1
- package/themes/origami-fold/variants/forest-coral.json +1 -1
- package/themes/origami-fold/variants/forest-pink.json +1 -1
- package/themes/origami-fold/variants/frost-amber-paper.json +1 -1
- package/themes/origami-fold/variants/sage-coral-sunflower.json +1 -1
- package/themes/origami-fold/variants/sage-coral.json +1 -1
- package/themes/origami-fold/variants/sage-pink-round.json +1 -1
- package/themes/origami-fold/variants/sage-pink.json +1 -1
- package/themes/paper-cutout/sample/style.json +2 -2
- package/themes/paper-cutout/style.json +2 -2
- package/themes/paper-cutout/theme.json +3 -1
- package/themes/paper-cutout/variants/blush-violet.json +1 -1
- package/themes/paper-cutout/variants/frost-lime.json +1 -1
- package/themes/paper-cutout/variants/lilac-gold.json +1 -1
- package/themes/paper-cutout/variants/sage-cyan.json +1 -1
- package/themes/paper-cutout/variants/sage-green.json +1 -1
- package/themes/paper-cutout/variants/sage-pink.json +1 -1
- package/themes/paper-cutout/variants/sand-blue.json +1 -1
- package/themes/pixel-quest/theme.json +2 -1
- package/themes/receipt/sample/style.json +2 -2
- package/themes/receipt/style.json +2 -2
- package/themes/riso-zine/sample/style.json +2 -2
- package/themes/riso-zine/style.json +2 -2
- package/themes/riso-zine/theme.json +3 -1
- package/themes/riso-zine/variants/chalk-lime.json +1 -1
- package/themes/riso-zine/variants/frost-green.json +1 -1
- package/themes/riso-zine/variants/lilac-plum.json +1 -1
- package/themes/riso-zine/variants/lilac-violet.json +1 -1
- package/themes/server-hall/sample/style.json +2 -2
- package/themes/server-hall/style.json +2 -2
- package/themes/server-hall/theme.json +3 -1
- package/themes/server-hall/variants/night-indigo.json +1 -1
- package/themes/server-hall/variants/wine-plum.json +1 -1
- package/themes/subway-map/sample/style.json +2 -2
- package/themes/subway-map/style.json +2 -2
- package/themes/subway-map/theme.json +3 -1
- package/themes/subway-map/variants/deep-green.json +1 -1
- package/themes/subway-map/variants/deep-teal.json +1 -1
- package/themes/subway-map/variants/frost-jade.json +1 -1
- package/themes/subway-map/variants/frost-teal.json +1 -1
- package/themes/subway-map/variants/slate-green.json +1 -1
- package/themes/subway-map/variants/snow-jade.json +1 -1
- package/themes/swiss-poster/sample/style.json +2 -2
- package/themes/swiss-poster/style.json +2 -2
- package/themes/swiss-poster/theme.json +3 -1
- package/themes/swiss-poster/variants/chalk-plum.json +1 -1
- package/themes/swiss-poster/variants/cocoa-cyan.json +1 -1
- package/themes/swiss-poster/variants/dusk-magenta.json +1 -1
- package/themes/swiss-poster/variants/frost-indigo.json +1 -1
- package/themes/swiss-poster/variants/slate-orange.json +1 -1
- package/themes/water-works/sample/style.json +1 -1
- package/themes/water-works/style.json +1 -1
- package/themes/water-works/theme.json +3 -1
- package/themes/water-works/variants/chalk-cyan.json +1 -1
- package/themes/water-works/variants/frost-indigo.json +1 -1
- package/themes/water-works/variants/sand-amber.json +1 -1
- package/themes/water-works/variants/sand-plum.json +1 -1
- package/themes/whiteboard/sample/style.json +2 -2
- package/themes/whiteboard/style.json +2 -2
- package/themes/whiteboard/theme.json +3 -1
- package/themes/whiteboard/variants/chalk-coral.json +1 -1
- package/themes/whiteboard/variants/cream-lime.json +1 -1
- package/themes/whiteboard/variants/frost-teal.json +1 -1
- package/themes/whiteboard/variants/lilac-indigo.json +1 -1
- package/themes/whiteboard/variants/sage-plum.json +1 -1
- package/themes/whiteboard/variants/snow-cyan.json +1 -1
- package/themes/whiteboard/variants/snow-green.json +1 -1
- package/themes/whiteboard/variants/snow-magenta.json +1 -1
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +0 -12
- package/themes/og-measure/variants/chalk-orange.json +0 -102
- package/themes/og-measure/variants/cocoa-amber.json +0 -138
- package/themes/og-measure/variants/cocoa-cyan.json +0 -138
- package/themes/og-measure/variants/cocoa-gold.json +0 -129
- package/themes/og-measure/variants/cream-lime.json +0 -137
- package/themes/og-measure/variants/deep-jade.json +0 -100
- package/themes/og-measure/variants/dusk-violet.json +0 -133
- package/themes/og-measure/variants/forest-cyan.json +0 -123
- package/themes/og-measure/variants/frost-amber.json +0 -129
- package/themes/og-measure/variants/frost-cyan.json +0 -133
- package/themes/og-measure/variants/lilac-plum.json +0 -127
- package/themes/og-measure/variants/lilac-violet.json +0 -139
- package/themes/og-measure/variants/sage-green.json +0 -137
- package/themes/og-measure/variants/wine-jade.json +0 -127
- package/themes/og-measure/variants/wine-plum.json +0 -127
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// 새 판 알림 — Codex CLI 처럼 확인은 떨어진 자식이 뒤에서 하고 결과를 캐시에 둔다. kf 는 네트워크를 기다리지 않고 지난 확인 결과로 묻거나 알린다.
|
|
2
|
+
// 여기는 모든 kf 명령 앞에서 불리니 무거운 것을 import 하지 않는다.
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
export const PACKAGE = "kasaframe";
|
|
11
|
+
const root = fileURLToPath(new URL("../../../", import.meta.url));
|
|
12
|
+
const versionAt = (dir: string): string => JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
|
|
13
|
+
export const current = versionAt(root);
|
|
14
|
+
// 몇 시간에 한 번 — 하루에 몇 번 켜도 레지스트리는 한두 번만 본다
|
|
15
|
+
export const CHECK_EVERY_MS = 6 * 3600_000;
|
|
16
|
+
|
|
17
|
+
export interface UpdateCache {
|
|
18
|
+
latest?: string;
|
|
19
|
+
checkedAt?: string;
|
|
20
|
+
/** 「이 판은 다시 묻지 않기」로 넘긴 판 — 더 새 판이 나오면 다시 묻는다 */
|
|
21
|
+
dismissed?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const cacheFile = (): string => join(process.env.KF_CACHE_DIR ?? join(homedir(), ".cache", "kasaframe"), "update.json");
|
|
25
|
+
|
|
26
|
+
export function readCache(): UpdateCache {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(cacheFile(), "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// kf 여럿이 한꺼번에 써도 반쯤 쓴 파일을 읽지 않게 다른 이름에 쓰고 옮긴다
|
|
35
|
+
export function writeCache(c: UpdateCache): void {
|
|
36
|
+
const file = cacheFile();
|
|
37
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
38
|
+
const tmp = `${file}.${process.pid}`;
|
|
39
|
+
writeFileSync(tmp, `${JSON.stringify(c)}\n`);
|
|
40
|
+
renameSync(tmp, file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SEMVER = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
44
|
+
|
|
45
|
+
/** latest 가 now 보다 새 정식 판인가 — 미리보기 판(prerelease)은 권하지 않는다 */
|
|
46
|
+
export function isNewer(latest: string, now: string): boolean {
|
|
47
|
+
const l = SEMVER.exec(latest.trim());
|
|
48
|
+
const c = SEMVER.exec(now.trim());
|
|
49
|
+
if (!l || !c || l[4]) return false;
|
|
50
|
+
for (let i = 1; i <= 3; i++) {
|
|
51
|
+
const d = Number(l[i]) - Number(c[i]);
|
|
52
|
+
if (d) return d > 0;
|
|
53
|
+
}
|
|
54
|
+
return !!c[4];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type Manager = "npm" | "pnpm" | "bun" | "yarn";
|
|
58
|
+
export interface Install {
|
|
59
|
+
manager: Manager;
|
|
60
|
+
/** npm 전역 자리 — 기본 자리가 아니면 올릴 때도 거기에 깔아야 이 kf 가 바뀐다 */
|
|
61
|
+
prefix?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 패키지가 놓인 자리로 어느 관리자의 전역 설치인가. 레포 체크아웃·npx 가 잠깐 받은 것·프로젝트 의존성은 null — 판은 그쪽이 정한다 */
|
|
65
|
+
export function installOf(dir: string, win = process.platform === "win32"): Install | null {
|
|
66
|
+
const p = dir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
67
|
+
if (!p.includes("/node_modules/")) return null;
|
|
68
|
+
if (p.includes("/_npx/")) return null;
|
|
69
|
+
if (p.includes("/.bun/install/global/")) return { manager: "bun" };
|
|
70
|
+
if (/\/pnpm\/global\//i.test(p)) return { manager: "pnpm" };
|
|
71
|
+
if (/\/yarn\/(data\/)?global\/node_modules\//i.test(p)) return { manager: "yarn" };
|
|
72
|
+
const npm = (win ? /^(.+)\/node_modules\/kasaframe$/ : /^(.+)\/lib\/node_modules\/kasaframe$/).exec(p);
|
|
73
|
+
return npm ? { manager: "npm", prefix: win ? npm[1].replace(/\//g, "\\") : npm[1] } : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function updateCommand(inst: Install, version: string, npmDefaultPrefix?: string): string[] {
|
|
77
|
+
const spec = `${PACKAGE}@${version}`;
|
|
78
|
+
if (inst.manager === "pnpm") return ["pnpm", "add", "-g", spec];
|
|
79
|
+
if (inst.manager === "bun") return ["bun", "add", "-g", spec];
|
|
80
|
+
if (inst.manager === "yarn") return ["yarn", "global", "add", spec];
|
|
81
|
+
const custom = inst.prefix && npmDefaultPrefix !== undefined && !samePath(inst.prefix, npmDefaultPrefix);
|
|
82
|
+
return ["npm", "i", "-g", spec, ...(custom ? ["--prefix", inst.prefix!] : [])];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function samePath(a: string, b: string): boolean {
|
|
86
|
+
const real = (p: string) => {
|
|
87
|
+
try {
|
|
88
|
+
return realpathSync(p);
|
|
89
|
+
} catch {
|
|
90
|
+
return p;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return real(a) === real(b);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const shell = process.platform === "win32";
|
|
97
|
+
|
|
98
|
+
function npmPrefix(): string | undefined {
|
|
99
|
+
const r = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8", shell, timeout: 10_000 });
|
|
100
|
+
return r.status === 0 ? r.stdout.trim() : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 이 kf 를 올리는 명령 — npm 이면 기본 전역 자리와 비교해야 해서 npm 을 한 번 부른다(묻거나 올릴 때만) */
|
|
104
|
+
export function commandFor(version: string): string[] | null {
|
|
105
|
+
const inst = installOf(root);
|
|
106
|
+
return inst && updateCommand(inst, version, inst.manager === "npm" && inst.prefix ? npmPrefix() : undefined);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 환경 변수 → npm 설정(사용자·전역·프로젝트 .npmrc) — 올리는 명령도 같은 npm 설정을 따른다
|
|
110
|
+
export function registry(env: NodeJS.ProcessEnv = process.env): string {
|
|
111
|
+
const set = env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY;
|
|
112
|
+
if (set) return set;
|
|
113
|
+
const r = spawnSync("npm", ["config", "get", "registry"], { encoding: "utf8", shell, timeout: 10_000 });
|
|
114
|
+
const got = r.status === 0 ? r.stdout.trim() : "";
|
|
115
|
+
return /^https?:\/\//.test(got) ? got : "https://registry.npmjs.org/";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function fetchLatest(timeoutMs: number): Promise<string> {
|
|
119
|
+
const url = `${registry().replace(/\/+$/, "")}/${PACKAGE}`;
|
|
120
|
+
const r = await fetch(url, {
|
|
121
|
+
headers: { accept: "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" },
|
|
122
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
123
|
+
});
|
|
124
|
+
if (!r.ok) throw new Error(`${url} → HTTP ${r.status}`);
|
|
125
|
+
const latest = ((await r.json()) as { "dist-tags"?: { latest?: unknown } })["dist-tags"]?.latest;
|
|
126
|
+
if (typeof latest !== "string") throw new Error(`${url} 에 dist-tags.latest 가 없다`);
|
|
127
|
+
return latest;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 레지스트리를 지금 보고 캐시에 적는다 — kf update·kf doctor 와 뒤에서 도는 자식이 쓴다 */
|
|
131
|
+
export async function refresh(timeoutMs = 8000): Promise<string> {
|
|
132
|
+
const latest = await fetchLatest(timeoutMs);
|
|
133
|
+
writeCache({ ...readCache(), latest, checkedAt: new Date().toISOString() });
|
|
134
|
+
return latest;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const on = (v: string | undefined) => !!v && v !== "0" && v.toLowerCase() !== "false";
|
|
138
|
+
|
|
139
|
+
export interface Decision {
|
|
140
|
+
show: "prompt" | "notice" | "none";
|
|
141
|
+
refresh: boolean;
|
|
142
|
+
latest?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function decide(i: { current: string; cache: UpdateCache; install: Install | null; tty: boolean; env: NodeJS.ProcessEnv; now: number }): Decision {
|
|
146
|
+
if (!i.install || on(i.env.KF_NO_UPDATE_CHECK) || on(i.env.CI)) return { show: "none", refresh: false };
|
|
147
|
+
// 시각이 없거나 미래(시계가 돌아갔다)면 지금 본다
|
|
148
|
+
const age = i.now - Date.parse(i.cache.checkedAt ?? "");
|
|
149
|
+
const refresh = !(age >= 0 && age < CHECK_EVERY_MS);
|
|
150
|
+
const latest = i.cache.latest;
|
|
151
|
+
if (!latest || !isNewer(latest, i.current) || i.cache.dismissed === latest) return { show: "none", refresh };
|
|
152
|
+
return { show: i.tty ? "prompt" : "notice", refresh, latest };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 뜻이 고정된 한 줄 — 에이전트가 이 꼴을 보고 kf update --yes 를 돌린다(skills/kasaframe/SKILL.md)
|
|
156
|
+
export const noticeLine = (latest: string, now = current): string => `[kasaframe] 새 판 ${latest} (지금 ${now}) — kf update`;
|
|
157
|
+
|
|
158
|
+
// 확인이 안 돼도(캐시 자리를 못 쓰거나 오프라인) kf 명령은 그대로 돈다
|
|
159
|
+
function refreshInBackground(): void {
|
|
160
|
+
try {
|
|
161
|
+
// 같이 뜬 kf 들이 저마다 확인을 띄우지 않게 시각부터 적는다 — 실패하면 다음 주기에 다시 본다
|
|
162
|
+
writeCache({ ...readCache(), checkedAt: new Date().toISOString() });
|
|
163
|
+
const code = `const m = await import(${JSON.stringify(import.meta.url)}); await m.refresh(15000).catch(() => {});`;
|
|
164
|
+
spawn(process.execPath, ["--input-type=module", "-e", code], { detached: true, stdio: "ignore", windowsHide: true })
|
|
165
|
+
.on("error", () => {})
|
|
166
|
+
.unref();
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 설치본 입구가 NODE_OPTIONS 에 넣은 형 지우기 훅 — 새 판으로 다시 띄울 땐 빼야 새 입구가 제 훅을 넣는다(pnpm 은 옛 판 자리를 지운다)
|
|
171
|
+
const hooksArg = `--import=${new URL("../../../bin/hooks.mjs", import.meta.url).href}`;
|
|
172
|
+
|
|
173
|
+
function install(cmd: string[]): { ok: boolean; why: string } {
|
|
174
|
+
const r = spawnSync(cmd[0], cmd.slice(1), { stdio: "inherit", shell });
|
|
175
|
+
if (r.error) return { ok: false, why: r.error.message };
|
|
176
|
+
return { ok: r.status === 0, why: r.signal ? `신호 ${r.signal}` : `종료 ${r.status}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 사람이 친 kf 입구(심볼릭 링크 그대로)가 이제 가리키는 판 */
|
|
180
|
+
function versionBehind(entry: string): string | null {
|
|
181
|
+
try {
|
|
182
|
+
return versionAt(join(dirname(realpathSync(entry)), ".."));
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 올리고, 이 kf 입구가 실제로 새 판을 가리키는지까지 본다 */
|
|
189
|
+
export function upgrade(cmd: string[], latest: string, entry = process.argv[1]): boolean {
|
|
190
|
+
process.stderr.write(`\n${cmd.join(" ")}\n`);
|
|
191
|
+
const r = install(cmd);
|
|
192
|
+
if (!r.ok) {
|
|
193
|
+
process.stderr.write(`\n올리지 못했다(${r.why}). 직접 올리려면: ${cmd.join(" ")}\n`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const now = versionBehind(entry);
|
|
197
|
+
if (now !== latest) {
|
|
198
|
+
process.stderr.write(`\n설치는 끝났는데 이 kf(${entry})는 ${now ?? "?"} 그대로다 — 다른 자리에 깔렸다. 직접: ${cmd.join(" ")}\n`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function ask(latest: string, cmd: string[]): Promise<"update" | "skip" | "dismiss"> {
|
|
205
|
+
// 묻는 동안 표준 출력이 터미널임은 확인했다 — 표준 오류가 파일로 가 있어도 보이게 표준 출력에 묻는다
|
|
206
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
207
|
+
rl.on("SIGINT", () => {
|
|
208
|
+
rl.close();
|
|
209
|
+
process.exit(130);
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
const a = (
|
|
213
|
+
await rl.question(
|
|
214
|
+
`\n새 판이 있다 · kasaframe ${current} → ${latest}\n 1) 지금 업데이트 (${cmd.join(" ")})\n 2) 건너뛰기\n 3) 이 판은 다시 묻지 않기\n번호(엔터 = 1): `,
|
|
215
|
+
)
|
|
216
|
+
).trim();
|
|
217
|
+
return a === "" || a === "1" ? "update" : a === "3" ? "dismiss" : "skip";
|
|
218
|
+
} catch {
|
|
219
|
+
return "skip";
|
|
220
|
+
} finally {
|
|
221
|
+
rl.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 명령 앞 관문. 터미널이면 보기 셋을 묻고, 아니면 표준 오류에 한 줄(표준 출력은 --json 을 읽는 도구가 있어 건드리지 않는다).
|
|
227
|
+
* 「지금 업데이트」가 되면 원래 명령을 새 판으로 다시 띄우고 그 종료 코드를, 아니면 null(지금 판으로 이어 간다)
|
|
228
|
+
*/
|
|
229
|
+
export async function updateGate(argv: string[]): Promise<number | null> {
|
|
230
|
+
const env = process.env;
|
|
231
|
+
const d = decide({ current, cache: readCache(), install: installOf(root), tty: !!(process.stdin.isTTY && process.stdout.isTTY), env, now: Date.now() });
|
|
232
|
+
// kf 가 띄우는 kf(make → render 따위)는 다시 묻거나 찍지 않는다
|
|
233
|
+
env.KF_NO_UPDATE_CHECK = "1";
|
|
234
|
+
if (d.refresh) refreshInBackground();
|
|
235
|
+
if (d.show === "notice") process.stderr.write(`${noticeLine(d.latest!)}\n`);
|
|
236
|
+
if (d.show !== "prompt") return null;
|
|
237
|
+
|
|
238
|
+
const cmd = commandFor(d.latest!)!;
|
|
239
|
+
const pick = await ask(d.latest!, cmd);
|
|
240
|
+
if (pick === "dismiss") writeCache({ ...readCache(), dismissed: d.latest });
|
|
241
|
+
if (pick !== "update") return null;
|
|
242
|
+
if (!upgrade(cmd, d.latest!)) {
|
|
243
|
+
process.stderr.write(`${current} 로 이어 간다\n\n`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
process.stderr.write(`\nkasaframe ${d.latest} 로 올렸다 — 이어서 kf ${argv.join(" ")}\n\n`);
|
|
247
|
+
const nodeOptions = (env.NODE_OPTIONS ?? "").split(" ").filter((t) => t && t !== hooksArg).join(" ");
|
|
248
|
+
const r = spawnSync(process.execPath, [process.argv[1], ...argv], { stdio: "inherit", env: { ...env, NODE_OPTIONS: nodeOptions } });
|
|
249
|
+
return r.status ?? (r.signal === "SIGINT" ? 130 : 1);
|
|
250
|
+
}
|
|
@@ -153,7 +153,7 @@ function voicePart(plan: Plan, caps: CapabilityReport): { q?: Question; auto?: s
|
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// 테마 — 시리즈가 정했거나 요청이 「리소 느낌」·「og-measure/
|
|
156
|
+
// 테마 — 시리즈가 정했거나 요청이 「리소 느낌」·「og-measure/snow-blue」처럼 짚었으면 묻지 않는다(kf new 의 auto 가 그대로 짚은 판을 쓴다).
|
|
157
157
|
// 보기는 「알아서」(= auto, 권장)와 요청·용도·분위기 낱말(사람이 준 한 마디·레퍼런스까지)에 맞는 approved 세계·변주 넷 — 수백 판을 다 늘어놓지 않는다.
|
|
158
158
|
// 사람이 보기 밖으로 분위기 한 마디나 그림·주소를 주면 그걸로 넷을 다시 고른다(kf make --theme-mood·--theme-ref)
|
|
159
159
|
export const THEME_PICKS = 4;
|
|
@@ -38,14 +38,20 @@ export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect
|
|
|
38
38
|
const lookOn = hasLook(want) || !!look.ref;
|
|
39
39
|
const pool = listThemes({ status: "approved", purpose, ...(aspect ? { aspect } : {}), variants: lookOn });
|
|
40
40
|
const ask = new Set(requestWords(text));
|
|
41
|
+
const tagsOf = (t: Theme) => (t.tags?.use ?? []).map((u) => u.toLowerCase());
|
|
42
|
+
// 드물게 적힌 낱말일수록 그 세계를 짚는다 — 「소개」처럼 여러 세계가 적은 낱말보다 「OpenGateway」처럼 한 세계만 적은 낱말이 무겁다
|
|
43
|
+
const tagShare = (w: string) => pool.filter((t) => tagsOf(t).some((u) => u.includes(w))).length / Math.max(1, pool.length);
|
|
41
44
|
const scored = pool.map((t) => {
|
|
42
45
|
const hay = `${t.name} ${t.metaphor} ${t.guide} ${(t.tags?.use ?? []).join(" ")} ${(t.tags?.mood ?? []).join(" ")} ${t.variant?.desc ?? ""}`.toLowerCase();
|
|
43
46
|
const hit = [...ask].filter((w) => hay.includes(w));
|
|
47
|
+
// 세계가 tags.use 에 일부러 적은 낱말은 본문에 우연히 나온 낱말보다 무겁다 — 공통 견본 브리프(「OpenGateway 소개」)의 낱말은 모든 세계 THEME.md 에 있어
|
|
48
|
+
// 본문 맞음만으로는 그 제품 전용 세계가 다른 세계와 동점이 된다
|
|
49
|
+
const tagged = hit.filter((w) => tagsOf(t).some((u) => u.includes(w)));
|
|
44
50
|
const l = lookOn ? lookScore(t, want, look.ref) : { score: 0, why: [] };
|
|
45
51
|
const used = use.filter((w) => (t.tags?.use ?? []).some((u) => u.includes(w)));
|
|
46
52
|
const fit = use.length ? (0.9 * used.length) / use.length : 0;
|
|
47
53
|
// 같은 점수면 세계 기본 판이 앞 — 바란 생김새가 없을 때 변주를 굳이 고르지 않게
|
|
48
|
-
return { t, score: hit.length + fit + l.score + (t.variant ? 0 : 0.3), hit, used, why: l.why, tie: tieKey(text, t.id) };
|
|
54
|
+
return { t, score: hit.length + tagged.reduce((s, w) => s + 0.5 * (1 - tagShare(w)), 0) + fit + l.score + (t.variant ? 0 : 0.3), hit, used, why: l.why, tie: tieKey(text, t.id) };
|
|
49
55
|
});
|
|
50
56
|
scored.sort((a, b) => b.score - a.score || a.tie - b.tie || a.t.id.localeCompare(b.t.id));
|
|
51
57
|
const picked: typeof scored = [];
|
|
@@ -72,7 +78,7 @@ export function suggestThemes(request: string, purpose: Purpose, aspect?: Aspect
|
|
|
72
78
|
// 사람이 짚은 것이라 review 까지 받는다(draft 는 빼고)
|
|
73
79
|
export function themeFromRequest(request: string, purpose?: Purpose): ThemePick | null {
|
|
74
80
|
const t = request.toLowerCase();
|
|
75
|
-
// 변주를 이름으로 짚었으면(「og-measure/
|
|
81
|
+
// 변주를 이름으로 짚었으면(「og-measure/snow-blue 로」) 그 판
|
|
76
82
|
const full = /([a-z0-9-]+)\/([a-z0-9-]+)/.exec(t);
|
|
77
83
|
if (full) {
|
|
78
84
|
try {
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
## 3. 비유·스타일 — `THEME.md` 앞부분 · `style.json` · `theme.json`
|
|
24
24
|
|
|
25
25
|
- 비유 한 줄: 「무엇이 무엇처럼 되며 화면이 찍힌다/드러난다」. 내용(정보가 화면에 오르는 방식)에서 나온 개념이어야 한다 — 장식 이름이 아니라 동사가 있는 문장.
|
|
26
|
-
- `style.json`: 프리셋과 같은 KfStyle 꼴 통째, `name` = `{{id}}`. 잉크 2–3색 + 그 어두운 단(`colors`), 바탕 `bg`, 글 `ink`, 강조 `accent`. 글꼴 셋(`sans`·`display`·`mono`)은 **한글이 서는 글꼴로 끝나게**(Pretendard 를 마지막에), `faces` 는
|
|
26
|
+
- `style.json`: 프리셋과 같은 KfStyle 꼴 통째, `name` = `{{id}}`. 잉크 2–3색 + 그 어두운 단(`colors`), 바탕 `bg`, 글 `ink`, 강조 `accent`. 글꼴 셋(`sans`·`display`·`mono`)은 **한글이 서는 글꼴로 끝나게**(Pretendard 를 마지막에), `faces` 는 **판을 박은 한 파일 주소**(jsdelivr `gh/google/fonts@<커밋 40자>/ofl/<이름>/<파일>.ttf` 나 `npm/pretendard@1.3.9/.../PretendardVariable.woff2` — `@main`·`@latest`·`@5` 처럼 바뀌는 판은 같은 영상이 다르게 구워진다). 구글 폰트 CSS(unicode-range 조각)는 글이 투명하게 찍히니 쓰지 않는다. 글꼴은 상업·영상·웹 임베딩이 허용된 OFL·Apache 만, 한 굵기뿐인 글꼴을 더 굵게 부르면 가짜 굵기가 된다 — 짝 목록에 있는 글꼴이면 `kf fonts list`·`kf fonts sheet` 에서 고르는 게 가장 빠르다. `theme` 칸에 `inks`·`texture`·`movement` 를 글로.
|
|
27
27
|
- `theme.json`: `name`·`metaphor`·`purposes`·`aspect`. status 는 draft 그대로.
|
|
28
|
-
- `theme.json` 의 `vary`·`tags`(SPEC 9 「변주」): 이 세계가 approved 되면 `kf theme vary {{id}}` 가 겉모습만 바꾼 판 15개를 뽑는다. 비유를 깨지 않는 축과 범위만 적는다 — 밝기(`scheme`), 강조 색상 범위(`hue`, OKLCH 도), 글꼴 짝(`fonts` — `packages/themes/kit/fonts.json`
|
|
28
|
+
- `theme.json` 의 `vary`·`tags`(SPEC 9 「변주」): 이 세계가 approved 되면 `kf theme vary {{id}}` 가 겉모습만 바꾼 판 15개를 뽑는다. 비유를 깨지 않는 축과 범위만 적는다 — 밝기(`scheme`), 강조 색상 범위(`hue`, OKLCH 도), 글꼴 짝(`fonts` — 짝 id·`tag:<태그>`·`tag:<태그>+<태그>`(둘 다 가진 짝 — 짝이 수백이라 결이 다른 무리가 딸려 오지 않게)·`base`. 목록은 `kf fonts list`(`packages/themes/kit/fonts.json`·`kit/fonts/<무리>.json`), 견본은 `kf fonts sheet`), 질감(`texture`), 선 굵기(`stroke` — 슬롯 `trace` 가 `slotkit.traceCanvas` 를 쓰면 실린다), 모서리(`radius`), 빠르기(`tempo`), 밀도(`density` — 효과가 `ctx.style.look?.density` 를 읽을 때만). 금지에 적은 것과 부딪히는 축은 빼고(「밝은 바탕 금지」면 `scheme: ["dark"]`), 재료처럼 색상이 뜻인 색 열쇠는 `fixedHue`, 늘 같은 색은 `keep`. 브랜드 가이드가 라이트·다크 두 벌을 따로 정해 뒀으면 다른 밝기 한 벌을 `palettes.<light|dark>` 에(그 밝기 판은 뒤집어 옮기지 않고 그 벌에서 시작한다). `tags` 는 `{ mood, light, use, dims }` — `use` 에는 그 세계가 맞는 제품·주제 이름도(공통 견본 브리프 낱말은 모든 THEME.md 에 있어 `use` 가 고르기를 가른다). 효과·슬롯 색은 `var(--kf-*)`·`ctx.style.colors` 로 읽어야 변주 색이 실린다 — 색을 코드에 박지 않는다.
|
|
29
29
|
- 글자 대비는 찍힌 픽셀로 3:1 이상 — 옅은 잉크로 작은 글을 쓰지 않는다.
|
|
30
30
|
|
|
31
31
|
## 4. 효과 — `effects.ts` (5–10개)
|