kasaframe 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/NOTICE +146 -0
- package/README.md +77 -2
- package/app/package.json +12 -0
- package/app/server/engine.ts +415 -0
- package/app/server/main.ts +205 -0
- package/app/web/env.d.ts +2 -0
- package/app/web/icons.ts +31 -0
- package/app/web/index.html +21 -0
- package/app/web/main.ts +1013 -0
- package/app/web/style.css +430 -0
- package/bin/hooks.mjs +51 -0
- package/bin/kf.mjs +14 -0
- package/docs/AGENTS.md +238 -0
- package/music/README.md +13 -0
- package/package.json +44 -4
- package/packages/adapters/package.json +36 -0
- package/packages/adapters/src/css.ts +11 -0
- package/packages/adapters/src/gsap.ts +26 -0
- package/packages/adapters/src/index.ts +9 -0
- package/packages/adapters/src/lottie.ts +22 -0
- package/packages/adapters/src/media.ts +59 -0
- package/packages/adapters/src/page.ts +229 -0
- package/packages/adapters/src/project.ts +214 -0
- package/packages/adapters/src/react.ts +36 -0
- package/packages/adapters/src/svelte.ts +22 -0
- package/packages/adapters/src/three.ts +18 -0
- package/packages/adapters/src/vue.ts +27 -0
- package/packages/check/package.json +19 -0
- package/packages/check/src/check.ts +273 -0
- package/packages/check/src/cli.ts +70 -0
- package/packages/check/src/image.ts +146 -0
- package/packages/check/src/index.ts +9 -0
- package/packages/check/src/ocr.swift +37 -0
- package/packages/check/src/probe.ts +328 -0
- package/packages/check/src/report.ts +44 -0
- package/packages/check/src/rules/assets.ts +42 -0
- package/packages/check/src/rules/beats.ts +212 -0
- package/packages/check/src/rules/camera.ts +217 -0
- package/packages/check/src/rules/captions.ts +49 -0
- package/packages/check/src/rules/flash.ts +235 -0
- package/packages/check/src/rules/frames.ts +135 -0
- package/packages/check/src/rules/layout.ts +257 -0
- package/packages/check/src/rules/page.ts +87 -0
- package/packages/check/src/rules/runs.ts +20 -0
- package/packages/check/src/rules/text.ts +326 -0
- package/packages/check/src/scan.ts +240 -0
- package/packages/check/src/serve.ts +67 -0
- package/packages/check/src/types.ts +80 -0
- package/packages/check/src/video.ts +137 -0
- package/packages/cli/package.json +29 -0
- package/packages/cli/src/blur.ts +68 -0
- package/packages/cli/src/browser.ts +203 -0
- package/packages/cli/src/command.ts +44 -0
- package/packages/cli/src/commands/app.ts +25 -0
- package/packages/cli/src/commands/beats.ts +36 -0
- package/packages/cli/src/commands/blender.ts +270 -0
- package/packages/cli/src/commands/check.ts +25 -0
- package/packages/cli/src/commands/doctor.ts +22 -0
- package/packages/cli/src/commands/gen.ts +173 -0
- package/packages/cli/src/commands/look.ts +39 -0
- package/packages/cli/src/commands/lyrics.ts +111 -0
- package/packages/cli/src/commands/make.ts +542 -0
- package/packages/cli/src/commands/new.ts +176 -0
- package/packages/cli/src/commands/preview.ts +43 -0
- package/packages/cli/src/commands/render.ts +216 -0
- package/packages/cli/src/commands/setup.ts +63 -0
- package/packages/cli/src/commands/sheet.ts +114 -0
- package/packages/cli/src/commands/skill.ts +43 -0
- package/packages/cli/src/commands/still.ts +40 -0
- package/packages/cli/src/commands/theme.ts +85 -0
- package/packages/cli/src/commands/transitions.ts +40 -0
- package/packages/cli/src/deps.ts +122 -0
- package/packages/cli/src/ffmpeg.ts +81 -0
- package/packages/cli/src/index.ts +8 -0
- package/packages/cli/src/kf.ts +53 -0
- package/packages/cli/src/lock.ts +72 -0
- package/packages/cli/src/out.ts +13 -0
- package/packages/cli/src/serve.ts +124 -0
- package/packages/clock/package.json +14 -0
- package/packages/clock/src/index.ts +25 -0
- package/packages/clock/src/install.ts +351 -0
- package/packages/clock/src/types.ts +45 -0
- package/packages/core/package.json +9 -0
- package/packages/core/src/beats.ts +283 -0
- package/packages/core/src/ease.ts +87 -0
- package/packages/core/src/frame.ts +99 -0
- package/packages/core/src/index.ts +27 -0
- package/packages/core/src/project.ts +146 -0
- package/packages/core/src/rand.ts +56 -0
- package/packages/core/src/spring.ts +52 -0
- package/packages/core/src/style.ts +100 -0
- package/packages/core/src/time.ts +88 -0
- package/packages/gen/PROVIDERS.md +55 -0
- package/packages/gen/package.json +9 -0
- package/packages/gen/src/generate.ts +204 -0
- package/packages/gen/src/image/og.ts +133 -0
- package/packages/gen/src/index.ts +18 -0
- package/packages/gen/src/keys.ts +60 -0
- package/packages/gen/src/media.ts +150 -0
- package/packages/gen/src/plan.ts +106 -0
- package/packages/gen/src/registry.ts +51 -0
- package/packages/gen/src/seam.ts +103 -0
- package/packages/gen/src/types.ts +86 -0
- package/packages/gen/src/video/fal.ts +140 -0
- package/packages/gen/src/video/h3.ts +107 -0
- package/packages/gen/src/video/http.ts +44 -0
- package/packages/gen/src/video/ltx.ts +60 -0
- package/packages/gen/src/video/minimax.ts +55 -0
- package/packages/gen/src/video/veo.ts +71 -0
- package/packages/jizura/package.json +16 -0
- package/packages/jizura/src/deny.ts +73 -0
- package/packages/jizura/src/index.ts +7 -0
- package/packages/jizura/src/install.ts +226 -0
- package/packages/jizura/src/layer.ts +282 -0
- package/packages/jizura/src/lyrics.ts +81 -0
- package/packages/jizura/src/pin.ts +5 -0
- package/packages/jizura/src/vite.ts +36 -0
- package/packages/motion/README.md +142 -0
- package/packages/motion/package.json +29 -0
- package/packages/motion/src/camera.ts +97 -0
- package/packages/motion/src/captions.ts +479 -0
- package/packages/motion/src/enter.ts +128 -0
- package/packages/motion/src/hangul.ts +129 -0
- package/packages/motion/src/impact.ts +100 -0
- package/packages/motion/src/index.ts +17 -0
- package/packages/motion/src/moves/brushWipe.ts +90 -0
- package/packages/motion/src/moves/cloneArray.ts +111 -0
- package/packages/motion/src/moves/curtains.ts +81 -0
- package/packages/motion/src/moves/echoStack.ts +70 -0
- package/packages/motion/src/moves/fit.ts +34 -0
- package/packages/motion/src/moves/flood.ts +61 -0
- package/packages/motion/src/moves/ghostStack.ts +79 -0
- package/packages/motion/src/moves/gridFloor.ts +100 -0
- package/packages/motion/src/moves/imageSwarm.ts +109 -0
- package/packages/motion/src/moves/index.ts +19 -0
- package/packages/motion/src/moves/irisBlades.ts +113 -0
- package/packages/motion/src/moves/karaokeLine.ts +94 -0
- package/packages/motion/src/moves/ledBoard.ts +94 -0
- package/packages/motion/src/moves/liquidTab.ts +72 -0
- package/packages/motion/src/moves/markerWrite.ts +63 -0
- package/packages/motion/src/moves/morphBox.ts +96 -0
- package/packages/motion/src/moves/neonSign.ts +77 -0
- package/packages/motion/src/moves/panelPop.ts +78 -0
- package/packages/motion/src/moves/paperTear.ts +75 -0
- package/packages/motion/src/moves/path.ts +64 -0
- package/packages/motion/src/moves/pathText.ts +107 -0
- package/packages/motion/src/moves/pointSphere.ts +109 -0
- package/packages/motion/src/moves/ransomLetters.ts +73 -0
- package/packages/motion/src/moves/shapeMorph.ts +143 -0
- package/packages/motion/src/moves/slamWords.ts +89 -0
- package/packages/motion/src/moves/sparkTrail.ts +114 -0
- package/packages/motion/src/moves/speedLines.ts +91 -0
- package/packages/motion/src/moves/splitFlap.ts +147 -0
- package/packages/motion/src/moves/stepWords.ts +117 -0
- package/packages/motion/src/moves/strikeOut.ts +98 -0
- package/packages/motion/src/moves/sunburst.ts +58 -0
- package/packages/motion/src/moves/tickerRows.ts +87 -0
- package/packages/motion/src/moves/tokenPicker.ts +114 -0
- package/packages/motion/src/moves/truchet.ts +85 -0
- package/packages/motion/src/moves/wordWall.ts +76 -0
- package/packages/motion/src/moves/worldRoll.ts +31 -0
- package/packages/motion/src/particles.ts +102 -0
- package/packages/motion/src/react/index.tsx +21 -0
- package/packages/motion/src/react/parts.tsx +431 -0
- package/packages/motion/src/react/time.tsx +97 -0
- package/packages/motion/src/text.ts +103 -0
- package/packages/motion/src/transition.ts +82 -0
- package/packages/motion/src/transition3d/cards.ts +65 -0
- package/packages/motion/src/transition3d/chain.ts +84 -0
- package/packages/motion/src/transition3d/dive.ts +181 -0
- package/packages/motion/src/transition3d/fold.ts +110 -0
- package/packages/motion/src/transition3d/gl.ts +219 -0
- package/packages/motion/src/transition3d/index.ts +228 -0
- package/packages/motion/src/transition3d/live.ts +70 -0
- package/packages/motion/src/transition3d/portal.ts +115 -0
- package/packages/motion/src/transition3d/react.tsx +29 -0
- package/packages/motion/src/transition3d/rig.ts +502 -0
- package/packages/motion/src/transition3d/shatter.ts +101 -0
- package/packages/motion/src/transition3d/whip.ts +65 -0
- package/packages/motion/src/transition3d/world.ts +683 -0
- package/packages/motion/src/transition3d/zoomOut.ts +114 -0
- package/packages/motion/src/transitions2d/catalog.ts +127 -0
- package/packages/motion/src/transitions2d/css.d.ts +2 -0
- package/packages/motion/src/transitions2d/cult-ui/LICENSE.md +9 -0
- package/packages/motion/src/transitions2d/cult-ui/RollingNumber.tsx +43 -0
- package/packages/motion/src/transitions2d/gl-transitions/LICENSE +25 -0
- package/packages/motion/src/transitions2d/gl-transitions/shaders.ts +1931 -0
- package/packages/motion/src/transitions2d/index.ts +2 -0
- package/packages/motion/src/transitions2d/kokonutui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.css +16 -0
- package/packages/motion/src/transitions2d/kokonutui/MatrixText.tsx +94 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.css +15 -0
- package/packages/motion/src/transitions2d/magicui/HyperText.tsx +115 -0
- package/packages/motion/src/transitions2d/magicui/LICENSE.md +21 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.css +25 -0
- package/packages/motion/src/transitions2d/magicui/MorphingText.tsx +130 -0
- package/packages/motion/src/transitions2d/magicui/WordRotate.tsx +42 -0
- package/packages/motion/src/transitions2d/react.ts +13 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.css +39 -0
- package/packages/motion/src/transitions2d/skiper-ui/ClipDiv.tsx +32 -0
- package/packages/motion/src/transitions2d/smoothui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/smoothui/SharedAxisY.tsx +50 -0
- package/packages/motion/src/transitions2d/smoothui/TextMorph.tsx +90 -0
- package/packages/motion/src/transitions2d/smoothui/shaders.ts +223 -0
- package/packages/motion/src/transitions2d/unlumen/LICENSE +21 -0
- package/packages/motion/src/transitions2d/unlumen/ScrambleText.tsx +114 -0
- package/packages/motion/src/transitions2d/vengeance-ui/LICENSE +21 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.css +69 -0
- package/packages/motion/src/transitions2d/vengeance-ui/MorphText.tsx +45 -0
- package/packages/motion/src/type.ts +131 -0
- package/packages/motion/src/ui.ts +172 -0
- package/packages/plan/package.json +15 -0
- package/packages/plan/src/brief.ts +254 -0
- package/packages/plan/src/captions.ts +75 -0
- package/packages/plan/src/detect.ts +93 -0
- package/packages/plan/src/index.ts +23 -0
- package/packages/plan/src/layout.ts +176 -0
- package/packages/plan/src/llm.ts +110 -0
- package/packages/plan/src/look.ts +220 -0
- package/packages/plan/src/plan.ts +125 -0
- package/packages/plan/src/request.ts +55 -0
- package/packages/plan/src/studio.ts +53 -0
- package/packages/plan/src/templates.ts +42 -0
- package/packages/plan/src/themes.ts +48 -0
- package/packages/plan/src/types.ts +106 -0
- package/packages/studio/package.json +12 -0
- package/packages/studio/src/index.ts +1 -0
- package/packages/studio/src/plugin.ts +236 -0
- package/packages/studio/src/ui/main.ts +503 -0
- package/packages/studio/src/ui/studio.css +165 -0
- package/packages/studio/src/ui/timeline.ts +317 -0
- package/packages/themes/kit/THEME-MAKE.md +68 -0
- package/packages/themes/kit/THEME.md +30 -0
- package/packages/themes/kit/briefs/og-intro-15s/brief.json +16 -0
- package/packages/themes/kit/briefs/og-intro-15s/facts.ts +29 -0
- package/packages/themes/kit/briefs/og-intro-15s/logo.svg +12 -0
- package/packages/themes/kit/effects.ts +5 -0
- package/packages/themes/kit/sample/index.html +11 -0
- package/packages/themes/kit/sample/main.ts +9 -0
- package/packages/themes/kit/sample/scenes/scene.ts +15 -0
- package/packages/themes/package.json +16 -0
- package/packages/themes/src/factory.ts +296 -0
- package/packages/themes/src/fx.ts +14 -0
- package/packages/themes/src/index.ts +105 -0
- package/packages/themes/src/stage3d.ts +177 -0
- package/packages/themes/src/types.ts +58 -0
- package/presets/kawaii-pastel.json +94 -0
- package/presets/neutral-dark.json +82 -0
- package/presets/og.json +98 -0
- package/skills/kasaframe/SKILL.md +32 -0
- package/templates/_shared/project/index.html +11 -0
- package/templates/_shared/project/lib/react.ts +22 -0
- package/templates/_shared/project/lib/stage.ts +272 -0
- package/templates/_shared/project/main.ts +9 -0
- package/templates/_shared/scenes/default.ts +18 -0
- package/templates/_shared/scenes/drop.ts +30 -0
- package/templates/_shared/scenes/end.ts +23 -0
- package/templates/_shared/scenes/hook.ts +16 -0
- package/templates/_shared/scenes/seam.ts +41 -0
- package/templates/_shared/scenes/step.ts +40 -0
- package/templates/ad/preview.png +0 -0
- package/templates/ad/template.json +132 -0
- package/templates/demo/preview.png +0 -0
- package/templates/demo/template.json +130 -0
- package/templates/explainer/preview.png +0 -0
- package/templates/explainer/template.json +142 -0
- package/templates/launch/preview.png +0 -0
- package/templates/launch/template.json +130 -0
- package/templates/mv/preview.png +0 -0
- package/templates/mv/scenes/default.ts +95 -0
- package/templates/mv/template.json +176 -0
- package/templates/promo/preview.png +0 -0
- package/templates/promo/template.json +144 -0
- package/templates/shorts/preview.png +0 -0
- package/templates/shorts/scenes/hook.ts +34 -0
- package/templates/shorts/template.json +127 -0
- package/templates/square/preview.png +0 -0
- package/templates/square/template.json +113 -0
- package/themes/debug-hud/THEME.md +39 -0
- package/themes/debug-hud/effects.ts +389 -0
- package/themes/debug-hud/sample/assets/og/logo.svg +12 -0
- package/themes/debug-hud/sample/beats.json +1 -0
- package/themes/debug-hud/sample/index.html +11 -0
- package/themes/debug-hud/sample/kf.json +59 -0
- package/themes/debug-hud/sample/lib/hudkit.ts +49 -0
- package/themes/debug-hud/sample/lib/og.ts +29 -0
- package/themes/debug-hud/sample/main.ts +9 -0
- package/themes/debug-hud/sample/scenes/end.ts +62 -0
- package/themes/debug-hud/sample/scenes/gather.ts +67 -0
- package/themes/debug-hud/sample/scenes/models.ts +70 -0
- package/themes/debug-hud/sample/scenes/price.ts +79 -0
- package/themes/debug-hud/sample/style.json +91 -0
- package/themes/debug-hud/still.png +0 -0
- package/themes/debug-hud/style.json +91 -0
- package/themes/debug-hud/theme.json +68 -0
- package/themes/og-measure/THEME.md +47 -0
- package/themes/og-measure/effects.ts +432 -0
- package/themes/og-measure/sample/assets/brand/og-logo-full.svg +12 -0
- package/themes/og-measure/sample/assets/brand/og-logo.svg +12 -0
- package/themes/og-measure/sample/assets/data/measure.json +353 -0
- package/themes/og-measure/sample/beats.json +1 -0
- package/themes/og-measure/sample/index.html +11 -0
- package/themes/og-measure/sample/kf.json +66 -0
- package/themes/og-measure/sample/lib/data.ts +62 -0
- package/themes/og-measure/sample/main.ts +9 -0
- package/themes/og-measure/sample/measure.ts +78 -0
- package/themes/og-measure/sample/scenes/end.ts +47 -0
- package/themes/og-measure/sample/scenes/gather.ts +51 -0
- package/themes/og-measure/sample/scenes/models.ts +34 -0
- package/themes/og-measure/sample/scenes/one.ts +39 -0
- package/themes/og-measure/sample/scenes/price.ts +56 -0
- package/themes/og-measure/sample/style.json +97 -0
- package/themes/og-measure/still.png +0 -0
- package/themes/og-measure/style.json +97 -0
- package/themes/og-measure/theme.json +79 -0
- package/themes/riso-zine/THEME.md +43 -0
- package/themes/riso-zine/effects.ts +321 -0
- package/themes/riso-zine/sample/assets/og/logo.svg +12 -0
- package/themes/riso-zine/sample/beats.json +1 -0
- package/themes/riso-zine/sample/index.html +11 -0
- package/themes/riso-zine/sample/kf.json +59 -0
- package/themes/riso-zine/sample/lib/og.ts +29 -0
- package/themes/riso-zine/sample/lib/shot.ts +86 -0
- package/themes/riso-zine/sample/lib/zine.ts +42 -0
- package/themes/riso-zine/sample/main.ts +9 -0
- package/themes/riso-zine/sample/scenes/end.ts +61 -0
- package/themes/riso-zine/sample/scenes/gather.ts +62 -0
- package/themes/riso-zine/sample/scenes/models.ts +52 -0
- package/themes/riso-zine/sample/scenes/price.ts +82 -0
- package/themes/riso-zine/sample/style.json +99 -0
- package/themes/riso-zine/still.png +0 -0
- package/themes/riso-zine/style.json +99 -0
- package/themes/riso-zine/theme.json +75 -0
- package/tools/beats/beats.py +372 -0
- package/tools/beats/requirements.txt +3 -0
- package/tools/lyrics/align.py +164 -0
- package/tools/lyrics/requirements.txt +9 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import type { Command } from "../command.ts";
|
|
3
|
+
import { num, parseFlags } from "../command.ts";
|
|
4
|
+
import { openSession } from "../browser.ts";
|
|
5
|
+
import { fmtTime, outPath } from "../out.ts";
|
|
6
|
+
import { blurOpts, captureBlurred, rgbaToPng } from "../blur.ts";
|
|
7
|
+
|
|
8
|
+
const cmd: Command = {
|
|
9
|
+
summary: "한 시각을 PNG 한 장으로",
|
|
10
|
+
usage: `kf still <폴더> --t <초> [--out 파일.png]
|
|
11
|
+
--t 찍을 시각(초). --frame N 으로 프레임 번호를 줘도 된다
|
|
12
|
+
--out 기본 <폴더>/out/still-<t>.png
|
|
13
|
+
--motion-blur N --shutter 180 렌더와 같은 모션 블러(하위 프레임 N 장 평균)로 한 장
|
|
14
|
+
step 모드 페이지는 0 초부터 그 시각까지 돌린 뒤 찍는다.`,
|
|
15
|
+
async run(argv) {
|
|
16
|
+
const { args, opts } = parseFlags(argv);
|
|
17
|
+
const dir = args[0];
|
|
18
|
+
if (!dir) {
|
|
19
|
+
console.error("kf still: 폴더를 줘라 — kf still <폴더> --t 12.3");
|
|
20
|
+
return 2;
|
|
21
|
+
}
|
|
22
|
+
const s = await openSession(dir);
|
|
23
|
+
try {
|
|
24
|
+
const { fps } = s.info.meta;
|
|
25
|
+
const t = opts.frame !== undefined ? num(opts.frame, 0) / fps : num(opts.t, 0);
|
|
26
|
+
const file = outPath(dir, opts.out, `still-${fmtTime(t)}.png`);
|
|
27
|
+
const blur = blurOpts(opts);
|
|
28
|
+
if (blur && s.info.mode === "seek") writeFileSync(file, rgbaToPng(await captureBlurred(s.first, s.info, t, blur)));
|
|
29
|
+
else {
|
|
30
|
+
await s.first.seek(t);
|
|
31
|
+
writeFileSync(file, await s.first.capture());
|
|
32
|
+
}
|
|
33
|
+
console.log(file);
|
|
34
|
+
return 0;
|
|
35
|
+
} finally {
|
|
36
|
+
await s.close();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default cmd;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { listThemes, themesDir } from "@kasaframe/themes";
|
|
4
|
+
import { briefs, review, scaffold, sessionPrompt, setStatus, staticCheck } from "@kasaframe/themes/factory";
|
|
5
|
+
import { type Command, num, parseFlags } from "../command.ts";
|
|
6
|
+
|
|
7
|
+
const kfBin = new URL("../kf.ts", import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
const cmd: Command = {
|
|
10
|
+
summary: "테마 공장 — 참고 자료로 테마 팩 뼈대·지시서, 계약 검사, 견본 review, 사람 OK 뒤 approve",
|
|
11
|
+
usage: `kf theme list
|
|
12
|
+
kf theme new <id> --ref <그림·영상·폴더·URL> [--ref …] [--name 이름] [--like <기존 테마>] [--3d] [--brief ${briefs().join("|")}] [--force]
|
|
13
|
+
kf theme check <id> [--no-page] 계약·빈칸 검사 + kf check sample
|
|
14
|
+
kf theme review <id> [--tabs 2] 굽기 → mp4 검사 → 장면 모음·대표 장면 → 공유 폴더 → status review
|
|
15
|
+
kf theme approve <id> 사람이 견본을 보고 OK 한 뒤에만
|
|
16
|
+
|
|
17
|
+
new 는 themes/<id>/ 에 계약 파일 뼈대·refs/·브리프대로 자른 곡과 박·장면 넷 뼈대·THEME-MAKE.md(지시서)를 짓고,
|
|
18
|
+
그 지시서를 받을 세션을 여는 명령을 알려 준다. 테마는 그 세션이 만든다.`,
|
|
19
|
+
async run(argv) {
|
|
20
|
+
const [sub, ...rest] = argv;
|
|
21
|
+
const { args, opts } = parseFlags(rest);
|
|
22
|
+
const id = args[0];
|
|
23
|
+
if (sub === "list" || !sub) {
|
|
24
|
+
const all = listThemes();
|
|
25
|
+
if (!all.length) console.log("(테마 없음)");
|
|
26
|
+
for (const t of all) console.log(`${t.id.padEnd(18)} ${t.status.padEnd(9)} ${String(t.approvedAt ?? "").padEnd(11)} 효과 ${String(t.effects.length).padStart(2)} · ${t.metaphor}`);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (!id) {
|
|
30
|
+
console.error(`kf theme ${sub}: 테마 id 가 필요하다. kf theme --help`);
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
if (sub === "new") {
|
|
34
|
+
// --ref 는 여러 번 — parseFlags 는 마지막 하나만 남기니 따로 모은다
|
|
35
|
+
const refs = rest.flatMap((a, i) => (a === "--ref" && rest[i + 1] ? [rest[i + 1]] : a.startsWith("--ref=") ? [a.slice(6)] : []));
|
|
36
|
+
try {
|
|
37
|
+
const r = scaffold(id, {
|
|
38
|
+
refs,
|
|
39
|
+
name: typeof opts.name === "string" ? opts.name : undefined,
|
|
40
|
+
like: typeof opts.like === "string" ? opts.like : undefined,
|
|
41
|
+
brief: typeof opts.brief === "string" ? opts.brief : undefined,
|
|
42
|
+
kind: opts["3d"] ? "3d" : "flat",
|
|
43
|
+
force: !!opts.force,
|
|
44
|
+
});
|
|
45
|
+
console.log(`${r.dir}/ — 참고 자료 ${r.refs.length}개${r.urls.length ? ` · 주소 ${r.urls.length}개` : ""}`);
|
|
46
|
+
console.log(`지시서: ${r.make}`);
|
|
47
|
+
console.log(`\n세션 열기(레포 뿌리에서):\n claude --model opus "${sessionPrompt(id)}"`);
|
|
48
|
+
return 0;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`kf theme new: ${(e as Error).message}`);
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (sub === "check") {
|
|
55
|
+
const bad = await staticCheck(id);
|
|
56
|
+
for (const b of bad) console.log(`[오류] ${b}`);
|
|
57
|
+
if (!bad.length) console.log("계약·빈칸: 통과");
|
|
58
|
+
let page = 0;
|
|
59
|
+
if (!opts["no-page"]) page = spawnSync(process.execPath, [kfBin, "check", join(themesDir, id, "sample"), "--tabs", String(num(opts.tabs, 2))], { stdio: "inherit" }).status ?? 1;
|
|
60
|
+
return bad.length || page ? 1 : 0;
|
|
61
|
+
}
|
|
62
|
+
if (sub === "review") {
|
|
63
|
+
const bad = await staticCheck(id);
|
|
64
|
+
if (bad.length) {
|
|
65
|
+
for (const b of bad) console.log(`[오류] ${b}`);
|
|
66
|
+
console.error("kf theme review: 계약·빈칸 오류부터 — kf theme check");
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const r = review(id, { tabs: num(opts.tabs, 2), brief: typeof opts.brief === "string" ? opts.brief : undefined });
|
|
70
|
+
for (const n of r.notes) console.log(n);
|
|
71
|
+
if (!r.ok) return 1;
|
|
72
|
+
console.log(`status review · 공유 폴더 ${r.share}`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (sub === "approve") {
|
|
76
|
+
const m = setStatus(id, "approved");
|
|
77
|
+
console.log(`${id} approved ${m.approvedAt} — 사람이 견본을 보고 OK 한 테마만`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
console.error(`kf theme: 모르는 하위 명령 "${sub}". kf theme --help`);
|
|
81
|
+
return 2;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default cmd;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from "../command.ts";
|
|
2
|
+
import { parseFlags } from "../command.ts";
|
|
3
|
+
import { CATALOG, type TransitionEntry } from "@kasaframe/motion/transitions2d";
|
|
4
|
+
|
|
5
|
+
const USE: Record<TransitionEntry["use"], string> = { transition3d: "이름으로", react: "React 부품", vendor: "원본만" };
|
|
6
|
+
|
|
7
|
+
const cmd: Command = {
|
|
8
|
+
summary: "전환 목록 — 이름·출처·라이선스·seek/step",
|
|
9
|
+
usage: `kf transitions [찾을 말] [--source 출처] [--clock seek|step] [--use transition3d|react|vendor] [--json]
|
|
10
|
+
이름으로(transition3d) 인 것은 장면에서 transition3d(root, ctx, { kind: "<이름>" }) 로 부른다 — 예 gl:crosswarp · smooth:sdf-circle · dive
|
|
11
|
+
React 부품은 @kasaframe/motion/transitions2d/react, 원본만은 geono-hub vendor/transitions/ 에 있다
|
|
12
|
+
step 은 자기 시계(rAF·타이머)라 kf.json 의 mode 를 "step" 으로`,
|
|
13
|
+
async run(argv) {
|
|
14
|
+
const { args, opts } = parseFlags(argv);
|
|
15
|
+
const has = (v: string, q: unknown) => typeof q !== "string" || v.toLowerCase().includes(q.toLowerCase());
|
|
16
|
+
const list = CATALOG.filter(
|
|
17
|
+
(e) =>
|
|
18
|
+
has(e.source, opts.source) &&
|
|
19
|
+
(typeof opts.clock !== "string" || e.clock === opts.clock) &&
|
|
20
|
+
(typeof opts.use !== "string" || e.use === opts.use) &&
|
|
21
|
+
args.every((q) => has(`${e.name} ${e.note ?? ""} ${e.author ?? ""}`, q)),
|
|
22
|
+
);
|
|
23
|
+
if (opts.json) {
|
|
24
|
+
console.log(JSON.stringify(list, null, 2));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const count = (k: (e: TransitionEntry) => string) => [...list.reduce((m, e) => m.set(k(e), (m.get(k(e)) ?? 0) + 1), new Map<string, number>())].map(([k, n]) => `${k} ${n}`).join(" · ");
|
|
28
|
+
console.log(`${list.length}개 — ${count((e) => e.source)}`);
|
|
29
|
+
console.log(`시계 ${count((e) => e.clock)} · ${count((e) => USE[e.use])}\n`);
|
|
30
|
+
const w = Math.max(...list.map((e) => e.name.length), 4) + 2;
|
|
31
|
+
const sw = Math.max(...list.map((e) => e.source.length), 4) + 2;
|
|
32
|
+
const lw = Math.max(...list.map((e) => e.license.length), 4) + 2;
|
|
33
|
+
for (const e of list) {
|
|
34
|
+
const extra = [e.component && `<${e.component}>`, e.author, e.note].filter(Boolean).join(" — ");
|
|
35
|
+
console.log(`${e.name.padEnd(w)}${e.source.padEnd(sw)}${e.license.padEnd(lw)}${e.clock.padEnd(6)}${USE[e.use].padEnd(9)}${extra}`);
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export default cmd;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir, platform } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { hasKey, whereToPut } from "@kasaframe/gen";
|
|
8
|
+
import { jizuraCheck } from "@kasaframe/jizura/install";
|
|
9
|
+
import { launch } from "./browser.ts";
|
|
10
|
+
import { lyricsCheck } from "./commands/lyrics.ts";
|
|
11
|
+
|
|
12
|
+
// kf doctor·kf setup 이 같이 보는 바깥 도구들 — npm 이 못 까는 것(ffmpeg·파이썬 꾸러미·크롬 본체)만.
|
|
13
|
+
|
|
14
|
+
export const requirements = fileURLToPath(new URL("../../../tools/beats/requirements.txt", import.meta.url));
|
|
15
|
+
// 요즘 파이썬(Homebrew·데비안)은 시스템에 pip 설치를 막는다(PEP 668) — kf setup 은 쓰임마다 venv 를 따로 둔다
|
|
16
|
+
export const pyVenv = (name: string): string => join(homedir(), ".cache", "kasaframe", "py", name);
|
|
17
|
+
export const venvPython = (dir: string): string => join(dir, platform() === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
18
|
+
export const beatsVenv = pyVenv("beats");
|
|
19
|
+
|
|
20
|
+
export function python(): string {
|
|
21
|
+
return process.env.KF_PYTHON ?? (existsSync(venvPython(beatsVenv)) ? venvPython(beatsVenv) : "python3");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// npx playwright 는 다른 판의 playwright 를 집을 수 있다 — 크롬 판이 이 패키지가 쓰는 playwright 와 맞아야 뜬다
|
|
25
|
+
export const playwrightCli = (): string => join(dirname(createRequire(import.meta.url).resolve("playwright/package.json")), "cli.js");
|
|
26
|
+
|
|
27
|
+
export interface Check {
|
|
28
|
+
name: string;
|
|
29
|
+
ok: boolean;
|
|
30
|
+
/** 없으면 렌더가 안 된다 */
|
|
31
|
+
required: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
fix?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const run = (cmd: string, args: string[]) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
37
|
+
|
|
38
|
+
function installHint(pkg: { brew: string; apt: string; url: string }): string {
|
|
39
|
+
if (platform() === "darwin") return `brew install ${pkg.brew}`;
|
|
40
|
+
if (platform() === "linux") return `sudo apt-get install -y ${pkg.apt} (데비안·우분투 — 다른 배포판은 그 패키지 관리자로)`;
|
|
41
|
+
return pkg.url;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function node(): Check {
|
|
45
|
+
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
46
|
+
const ok = major > 22 || (major === 22 && minor >= 18);
|
|
47
|
+
return { name: "Node 22.18 이상", ok, required: true, detail: process.version, fix: ok ? undefined : "https://nodejs.org 에서 22 LTS 이상" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ffmpeg(): Check {
|
|
51
|
+
const r = run("ffmpeg", ["-hide_banner", "-version"]);
|
|
52
|
+
const p = run("ffprobe", ["-hide_banner", "-version"]);
|
|
53
|
+
const ok = r.status === 0 && p.status === 0;
|
|
54
|
+
return {
|
|
55
|
+
name: "ffmpeg·ffprobe",
|
|
56
|
+
ok,
|
|
57
|
+
required: true,
|
|
58
|
+
detail: ok ? (/version (\S+)/.exec(r.stdout)?.[1] ?? "있음") : "없음 — MP4 로 굽기·검사에 쓴다",
|
|
59
|
+
fix: ok ? undefined : installHint({ brew: "ffmpeg", apt: "ffmpeg", url: "https://ffmpeg.org/download.html" }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function chrome(): Promise<Check> {
|
|
64
|
+
try {
|
|
65
|
+
const b = await launch();
|
|
66
|
+
const v = b.version();
|
|
67
|
+
await b.close();
|
|
68
|
+
return { name: "크롬(Playwright)", ok: true, required: true, detail: v };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const first = String(err instanceof Error ? err.message : err).split("\n")[0];
|
|
71
|
+
return { name: "크롬(Playwright)", ok: false, required: true, detail: `못 띄웠다 — ${first.slice(0, 120)}`, fix: `kf setup chrome (같은 일: node ${playwrightCli()} install chromium${platform() === "linux" ? " · 리눅스는 sudo 로 install-deps chromium 도" : ""})` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function librosa(): Check {
|
|
76
|
+
const py = python();
|
|
77
|
+
const r = run(py, ["-c", "import librosa; print(librosa.__version__)"]);
|
|
78
|
+
const ok = r.status === 0;
|
|
79
|
+
return {
|
|
80
|
+
name: "python3 + librosa",
|
|
81
|
+
ok,
|
|
82
|
+
required: false,
|
|
83
|
+
detail: ok ? `${py} · librosa ${r.stdout.trim()}` : `${py} 에 없다 — 곡에 박자를 맞출 때(kf beats·--music)만 쓴다`,
|
|
84
|
+
fix: ok ? undefined : `kf setup beats (같은 일: python3 -m venv ${beatsVenv} && ${venvPython(beatsVenv)} -m pip install -r ${requirements})`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ogKey(): Check {
|
|
89
|
+
const ok = hasKey("opengateway");
|
|
90
|
+
return {
|
|
91
|
+
name: "OG 키",
|
|
92
|
+
ok,
|
|
93
|
+
required: false,
|
|
94
|
+
detail: ok ? "있음 — 그림 생성(kf gen)·kf make OG 모드" : "없음 — 그림 생성(kf gen)과 kf make OG 모드만 못 쓴다. 장면·렌더·검사는 키 없이 돈다",
|
|
95
|
+
fix: ok ? undefined : whereToPut("opengateway"),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 글꼴은 CDN(jsdelivr)에서 받지만, 망이 막힌 리눅스 상자에서는 한글이 두부로 나온다
|
|
100
|
+
function koreanFont(): Check | null {
|
|
101
|
+
if (platform() !== "linux") return null;
|
|
102
|
+
const r = run("fc-list", [":lang=ko", "family"]);
|
|
103
|
+
const ok = r.status === 0 && r.stdout.trim().length > 0;
|
|
104
|
+
return { name: "한글 글꼴", ok, required: false, detail: ok ? r.stdout.trim().split("\n")[0] : "없음 — 글꼴 CDN 이 막히면 한글이 두부로 나온다", fix: ok ? undefined : "sudo apt-get install -y fonts-noto-cjk" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 점검을 더할 땐 여기 한 줄 — kf doctor 와 kf setup 끝 표가 같이 쓴다
|
|
108
|
+
export async function checks(): Promise<Check[]> {
|
|
109
|
+
return [node(), ffmpeg(), await chrome(), librosa(), jizuraCheck(), lyricsCheck(), ogKey(), koreanFont()].filter((c): c is Check => c !== null);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 한글은 터미널에서 두 칸을 먹는다 — padEnd 로는 열이 어긋난다
|
|
113
|
+
const cols = (s: string) => [...s].reduce((n, ch) => n + (/[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af]/.test(ch) ? 2 : 1), 0);
|
|
114
|
+
const pad = (s: string, n: number) => s + " ".repeat(Math.max(0, n - cols(s)));
|
|
115
|
+
|
|
116
|
+
export function report(list: Check[]): void {
|
|
117
|
+
const w = Math.max(...list.map((c) => cols(c.name))) + 2;
|
|
118
|
+
for (const c of list) {
|
|
119
|
+
console.log(` ${pad(c.ok ? "ok" : c.required ? "없음" : "선택", 4)} ${pad(c.name, w)}${c.detail}`);
|
|
120
|
+
if (c.fix) console.log(` ${pad("", 4 + 2 + w)}→ ${c.fix}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
|
|
4
|
+
export type VideoFormat = "h264" | "prores";
|
|
5
|
+
|
|
6
|
+
export interface Encoder {
|
|
7
|
+
/** PNG, raw 로 만들었으면 RGBA 날것 */
|
|
8
|
+
write(frame: Buffer): Promise<void>;
|
|
9
|
+
end(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function run(args: string[], label: string): { proc: ChildProcess; done: Promise<void> } {
|
|
13
|
+
const proc = spawn("ffmpeg", ["-hide_banner", "-loglevel", "error", "-y", ...args], {
|
|
14
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
let err = "";
|
|
17
|
+
proc.stderr!.on("data", (d) => (err += d));
|
|
18
|
+
const done = new Promise<void>((resolve, reject) => {
|
|
19
|
+
proc.on("error", (e) => reject(new Error(`${label}: ffmpeg 를 못 띄웠다 (${e.message})`)));
|
|
20
|
+
proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${label}: ffmpeg 종료 ${code}\n${err.trim()}`))));
|
|
21
|
+
});
|
|
22
|
+
return { proc, done };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// RGB→YUV 를 BT.709 로 바꾸고 그렇다고 적는다. 안 적으면 ffmpeg 는 BT.601 로 바꾸는데 브라우저·유튜브는 HD 를 709 로 풀어
|
|
26
|
+
// 색이 어긋난다(#20C040 이 (20,168,61) 로 — 초록이 눌린다)
|
|
27
|
+
const BT709 = ["-vf", "scale=out_color_matrix=bt709:out_range=tv,setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709", "-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"];
|
|
28
|
+
|
|
29
|
+
const CODEC: Record<VideoFormat, string[]> = {
|
|
30
|
+
h264: ["-c:v", "libx264", "-preset", "medium", "-crf", "16", "-pix_fmt", "yuv420p", ...BT709, "-movflags", "+faststart"],
|
|
31
|
+
prores: ["-c:v", "prores_ks", "-profile:v", "3", "-pix_fmt", "yuv422p10le", ...BT709, "-vendor", "apl0"],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// PNG(또는 raw 를 주면 RGBA 날것 — 모션 블러가 평균한 프레임)를 stdin 으로 흘려 구간 파일 하나를 만든다. 쓰기가 밀리면 기다려 메모리가 차지 않게 한다.
|
|
35
|
+
export function encoder(out: string, fps: number, format: VideoFormat, crf?: number, raw?: { width: number; height: number }): Encoder {
|
|
36
|
+
const codec = [...CODEC[format]];
|
|
37
|
+
if (crf !== undefined && format === "h264") codec[codec.indexOf("-crf") + 1] = String(crf);
|
|
38
|
+
const input = raw
|
|
39
|
+
? ["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${raw.width}x${raw.height}`, "-framerate", String(fps), "-i", "-"]
|
|
40
|
+
: ["-f", "image2pipe", "-framerate", String(fps), "-c:v", "png", "-i", "-"];
|
|
41
|
+
const { proc, done } = run([...input, ...codec, "-r", String(fps), out], out);
|
|
42
|
+
const stdin = proc.stdin!;
|
|
43
|
+
let failed: Error | null = null;
|
|
44
|
+
done.catch((e) => (failed = e));
|
|
45
|
+
return {
|
|
46
|
+
async write(frame) {
|
|
47
|
+
if (failed) throw failed;
|
|
48
|
+
if (!stdin.write(frame)) await Promise.race([once(stdin, "drain"), done]);
|
|
49
|
+
},
|
|
50
|
+
async end() {
|
|
51
|
+
stdin.end();
|
|
52
|
+
await done;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 구간들을 다시 인코딩하지 않고 잇고, 곡이 있으면 그 구간만큼 잘라 얹는다.
|
|
58
|
+
export async function concat(o: {
|
|
59
|
+
list: string;
|
|
60
|
+
out: string;
|
|
61
|
+
format: VideoFormat;
|
|
62
|
+
music?: string;
|
|
63
|
+
from: number;
|
|
64
|
+
dur: number;
|
|
65
|
+
}): Promise<void> {
|
|
66
|
+
const args = ["-f", "concat", "-safe", "0", "-i", o.list];
|
|
67
|
+
if (o.music) args.push("-ss", o.from.toFixed(6), "-i", o.music);
|
|
68
|
+
args.push("-map", "0:v:0");
|
|
69
|
+
if (o.music) {
|
|
70
|
+
args.push("-map", "1:a:0", ...(o.format === "prores" ? ["-c:a", "pcm_s16le"] : ["-c:a", "aac", "-b:a", "256k"]));
|
|
71
|
+
args.push("-af", `apad`, "-t", o.dur.toFixed(6));
|
|
72
|
+
}
|
|
73
|
+
args.push("-c:v", "copy");
|
|
74
|
+
// concat 이 구간 파일의 primaries·trc 태그를 떨군다 — 다시 인코딩 없이 비트스트림에 709 를 다시 적는다
|
|
75
|
+
args.push("-bsf:v", o.format === "h264" ? "h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1" : "prores_metadata=color_primaries=bt709:color_trc=bt709:colorspace=bt709");
|
|
76
|
+
args.push("-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709");
|
|
77
|
+
if (o.format === "h264") args.push("-movflags", "+faststart");
|
|
78
|
+
args.push(o.out);
|
|
79
|
+
const { done } = run(args, o.out);
|
|
80
|
+
await done;
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { Command, Flags } from "./command.ts";
|
|
2
|
+
export { parseFlags, num } from "./command.ts";
|
|
3
|
+
export { openProject, repoRoot } from "./serve.ts";
|
|
4
|
+
export type { OpenProject, OpenProjectOptions } from "./serve.ts";
|
|
5
|
+
export { openPage, openSession, launch, Tab } from "./browser.ts";
|
|
6
|
+
export type { OpenPage, PageInfo, PageMeta, Session, SessionOptions } from "./browser.ts";
|
|
7
|
+
export { blurOpts, captureBlurred, rgbaToPng, subTimes } from "./blur.ts";
|
|
8
|
+
export type { BlurOpts } from "./blur.ts";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { Command } from "./command.ts";
|
|
5
|
+
|
|
6
|
+
const commandsDir = fileURLToPath(new URL("./commands/", import.meta.url));
|
|
7
|
+
|
|
8
|
+
function available(): string[] {
|
|
9
|
+
return readdirSync(commandsDir)
|
|
10
|
+
.filter((f) => f.endsWith(".ts") && !f.endsWith(".test.ts"))
|
|
11
|
+
.map((f) => f.slice(0, -3))
|
|
12
|
+
.sort();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function load(name: string): Promise<Command> {
|
|
16
|
+
const mod = await import(new URL(`./commands/${name}.ts`, import.meta.url).href);
|
|
17
|
+
return mod.default as Command;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function help(): Promise<void> {
|
|
21
|
+
console.log("kf <명령> [인자]\n");
|
|
22
|
+
for (const name of available()) {
|
|
23
|
+
const cmd = await load(name).catch(() => null);
|
|
24
|
+
console.log(` ${name.padEnd(10)} ${cmd?.summary ?? ""}`);
|
|
25
|
+
}
|
|
26
|
+
console.log("\nkf <명령> --help 로 인자를 본다.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main(argv: string[]): Promise<number> {
|
|
30
|
+
const [name, ...rest] = argv;
|
|
31
|
+
if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
32
|
+
await help();
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (!available().includes(name)) {
|
|
36
|
+
console.error(`kf: 모르는 명령 "${name}". 있는 명령: ${available().join(" ")}`);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const cmd = await load(name);
|
|
40
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
41
|
+
console.log(`kf ${name} — ${cmd.summary}\n\n${cmd.usage ?? ""}`.trimEnd());
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
return (await cmd.run(rest)) ?? 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
main(process.argv.slice(2)).then(
|
|
48
|
+
(code) => process.exit(code),
|
|
49
|
+
(err) => {
|
|
50
|
+
console.error(err instanceof Error ? (err.stack ?? err.message) : err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { installed, repoRoot } from "./serve.ts";
|
|
5
|
+
|
|
6
|
+
const LOCK = installed ? join(tmpdir(), "kasaframe-render-lock") : join(repoRoot, ".render-lock");
|
|
7
|
+
|
|
8
|
+
function alive(pid: number): boolean {
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function owner(): { pid: number; dir: string; started: string } | null {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(join(LOCK, "owner.json"), "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 최종 렌더는 레포 전체에서 하나씩 — mkdir 이 원자적이라 먼저 만든 쪽만 이긴다.
|
|
26
|
+
export async function acquireRenderLock(dir: string, waitSec: number): Promise<() => void> {
|
|
27
|
+
const until = Date.now() + waitSec * 1000;
|
|
28
|
+
let told = false;
|
|
29
|
+
for (;;) {
|
|
30
|
+
try {
|
|
31
|
+
mkdirSync(LOCK);
|
|
32
|
+
writeFileSync(join(LOCK, "owner.json"), JSON.stringify({ pid: process.pid, dir, started: new Date().toISOString() }));
|
|
33
|
+
let held = true;
|
|
34
|
+
const release = () => {
|
|
35
|
+
if (!held) return;
|
|
36
|
+
held = false;
|
|
37
|
+
if (owner()?.pid === process.pid) rmSync(LOCK, { recursive: true, force: true });
|
|
38
|
+
};
|
|
39
|
+
process.once("exit", release);
|
|
40
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
41
|
+
process.once(sig, () => {
|
|
42
|
+
release();
|
|
43
|
+
process.exit(130);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return release;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
49
|
+
}
|
|
50
|
+
const o = owner();
|
|
51
|
+
// 남이 mkdir 하고 owner.json 을 쓰기 전 찰나일 수 있으니, 주인 없는 잠금은 10초 지난 것만 치운다.
|
|
52
|
+
const age = (() => {
|
|
53
|
+
try {
|
|
54
|
+
return Date.now() - statSync(LOCK).mtimeMs;
|
|
55
|
+
} catch {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
if ((o && !alive(o.pid)) || (!o && age > 10_000)) {
|
|
60
|
+
rmSync(LOCK, { recursive: true, force: true });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (Date.now() > until) {
|
|
64
|
+
throw new Error(`렌더 잠금을 못 얻었다 — ${o ? `pid ${o.pid} 가 ${o.dir} 를 ${o.started} 부터 굽는 중` : LOCK}`);
|
|
65
|
+
}
|
|
66
|
+
if (!told) {
|
|
67
|
+
console.error(`렌더 잠금 기다리는 중${o ? ` (pid ${o.pid}: ${o.dir})` : ""} — 최대 ${waitSec}초`);
|
|
68
|
+
told = true;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
// 결과물은 기본으로 프로젝트의 out/ 에 둔다.
|
|
5
|
+
export function outPath(dir: string, given: string | boolean | undefined, fallback: string): string {
|
|
6
|
+
const p = typeof given === "string" ? resolve(given) : join(resolve(dir), "out", fallback);
|
|
7
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
8
|
+
return p;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function fmtTime(t: number): string {
|
|
12
|
+
return t.toFixed(3).replace(/\.?0+$/, "") || "0";
|
|
13
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { resolve, join, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createServer, type Plugin, type ViteDevServer } from "vite";
|
|
5
|
+
import { clockScript, type ClockOptions } from "@kasaframe/clock";
|
|
6
|
+
import { jizuraServe } from "@kasaframe/jizura/vite";
|
|
7
|
+
|
|
8
|
+
export const repoRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
|
9
|
+
// npm 으로 깐 패키지 안에서 도는가 — 그 자리는 쓸 수 없을 수 있어 잠금·작업실 폴더를 밖에 둔다
|
|
10
|
+
export const installed = repoRoot.includes(`${sep}node_modules${sep}`);
|
|
11
|
+
|
|
12
|
+
export interface OpenProjectOptions {
|
|
13
|
+
// true 면 index.html 맨 앞에 가상 시계를 넣는다. Playwright 로 열 땐 addInitScript 가 따로 넣으니 끈다.
|
|
14
|
+
clock?: boolean | ClockOptions;
|
|
15
|
+
port?: number;
|
|
16
|
+
// 미리보기처럼 저장하면 반영돼야 하는 쪽만 켠다. 렌더·검사는 끈다.
|
|
17
|
+
hmr?: boolean;
|
|
18
|
+
// 엔진 플러그인 뒤에 붙는다. 미리보기가 studio 화면을 / 에 내는 데 쓴다.
|
|
19
|
+
plugins?: Plugin[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OpenProject {
|
|
23
|
+
url: string;
|
|
24
|
+
dir: string;
|
|
25
|
+
server: ViteDevServer;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 레포 밖 프로젝트가 import 하는 @kasaframe/*·react 같은 맨 이름을 프로젝트에서 못 찾으면 이 레포에서 찾는다.
|
|
30
|
+
function repoFallback(): Plugin {
|
|
31
|
+
const anchor = join(repoRoot, "package.json");
|
|
32
|
+
return {
|
|
33
|
+
name: "kf-repo-fallback",
|
|
34
|
+
enforce: "pre",
|
|
35
|
+
async resolveId(id, importer, opts) {
|
|
36
|
+
if (/^[./\0]/.test(id) || id.includes(":")) return null;
|
|
37
|
+
// 설치된 패키지에는 워크스페이스 링크가 없다 — Node 가 푸는 자리(레포는 링크, 패키지는 bin/hooks.mjs)로 보낸다
|
|
38
|
+
if (id.startsWith("@kasaframe/")) {
|
|
39
|
+
const file = kasaframeFile(id);
|
|
40
|
+
return file ? this.resolve(file, importer, { ...opts, skipSelf: true }) : null;
|
|
41
|
+
}
|
|
42
|
+
if (!importer || importer.startsWith(repoRoot)) return null;
|
|
43
|
+
const own = await this.resolve(id, importer, { ...opts, skipSelf: true });
|
|
44
|
+
if (own) return own;
|
|
45
|
+
return this.resolve(id, anchor, { ...opts, skipSelf: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function kasaframeFile(id: string): string | null {
|
|
51
|
+
try {
|
|
52
|
+
return fileURLToPath(import.meta.resolve(id));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const kasaframePackages = (): string[] =>
|
|
59
|
+
readdirSync(join(repoRoot, "packages")).filter((p) => existsSync(join(repoRoot, "packages", p, "package.json"))).map((p) => `@kasaframe/${p}`);
|
|
60
|
+
|
|
61
|
+
function browserEntries(): string[] {
|
|
62
|
+
return [
|
|
63
|
+
"packages/adapters/src/react.ts",
|
|
64
|
+
"packages/motion/src/**/*.{ts,tsx}",
|
|
65
|
+
"packages/themes/src/fx.ts",
|
|
66
|
+
"packages/themes/src/stage3d.ts",
|
|
67
|
+
"themes/*/effects.ts",
|
|
68
|
+
].map((p) => join(repoRoot, p));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function injectClock(opts: ClockOptions): Plugin {
|
|
72
|
+
return {
|
|
73
|
+
name: "kf-clock",
|
|
74
|
+
async transformIndexHtml() {
|
|
75
|
+
return [{ tag: "script", children: await clockScript(opts), injectTo: "head-prepend" }];
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function openProject(dir: string, o: OpenProjectOptions = {}): Promise<OpenProject> {
|
|
81
|
+
if (!existsSync(join(resolve(dir), "index.html"))) {
|
|
82
|
+
throw new Error(`${resolve(dir)} 에 index.html 이 없다 — kf new 로 만든 프로젝트 폴더를 줘라`);
|
|
83
|
+
}
|
|
84
|
+
// Vite 는 importer 를 실제 경로로 준다(/tmp → /private/tmp). 경로 비교가 어긋나지 않게 처음부터 실제 경로로.
|
|
85
|
+
const root = realpathSync(resolve(dir));
|
|
86
|
+
const plugins: Plugin[] = [repoFallback(), jizuraServe()];
|
|
87
|
+
if (o.clock) plugins.push(injectClock(o.clock === true ? {} : o.clock));
|
|
88
|
+
plugins.push(...(o.plugins ?? []));
|
|
89
|
+
const server = await createServer({
|
|
90
|
+
root,
|
|
91
|
+
configFile: existsSync(join(root, "vite.config.ts")) ? undefined : false,
|
|
92
|
+
logLevel: "warn",
|
|
93
|
+
clearScreen: false,
|
|
94
|
+
plugins,
|
|
95
|
+
esbuild: { jsx: "automatic" },
|
|
96
|
+
define: {
|
|
97
|
+
__VUE_OPTIONS_API__: "true",
|
|
98
|
+
__VUE_PROD_DEVTOOLS__: "false",
|
|
99
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false",
|
|
100
|
+
},
|
|
101
|
+
// 레포 밖 컴포넌트가 자기 node_modules 의 React 를 끌고 오면 훅이 깨진다 — 한 벌로 묶는다.
|
|
102
|
+
resolve: { dedupe: ["react", "react-dom"] },
|
|
103
|
+
// 렌더는 HMR 이 꺼져 있어, 실행 중에 새 의존성이 나오면 다시 묶은 판으로 갈아탈 수 없다(CJS 가 그대로 가서 named export 가 빠진다).
|
|
104
|
+
// 그래서 처음 훑을 때 packages/ 의 브라우저 쪽 소스까지 같이 훑게 한다. 이 스캔은 repoFallback 을 거쳐 레포 밖 루트에서도 풀린다.
|
|
105
|
+
// @kasaframe/* 는 미리 묶지 않고 소스 그대로 낸다 — 설치된 패키지에선 node_modules 안이라 안 빼면 따로 묶여 한 모듈이 두 벌 된다.
|
|
106
|
+
// 빼면 훑기가 그 안으로 안 들어가므로 위 진입점이 그 몫을 한다.
|
|
107
|
+
optimizeDeps: {
|
|
108
|
+
entries: ["**/*.html", ...browserEntries()],
|
|
109
|
+
exclude: kasaframePackages(),
|
|
110
|
+
},
|
|
111
|
+
server: {
|
|
112
|
+
host: "127.0.0.1",
|
|
113
|
+
port: o.port ?? 5100,
|
|
114
|
+
strictPort: false,
|
|
115
|
+
hmr: o.hmr ?? false,
|
|
116
|
+
// 이 맥에서 폴더 단위 fs.watch(FSEvents)가 이벤트를 하나도 안 준다(파일 단위 kqueue 만 산다). 프로젝트는 작으니 폴링이 확실하다.
|
|
117
|
+
watch: o.hmr ? { usePolling: true, interval: 150 } : null,
|
|
118
|
+
fs: { allow: [root, repoRoot] },
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await server.listen();
|
|
122
|
+
const url = server.resolvedUrls?.local[0] ?? `http://127.0.0.1:${server.config.server.port}/`;
|
|
123
|
+
return { url, dir: root, server, close: () => server.close() };
|
|
124
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kasaframe/clock",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./types": "./src/types.ts",
|
|
9
|
+
"./install": "./src/install.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"esbuild": "^0.28.2"
|
|
13
|
+
}
|
|
14
|
+
}
|