diff options
| author | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-24 17:14:54 +0100 |
| commit | b1ce1927022d374198441afafeb27199fc8a052a (patch) | |
| tree | 6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/cli.mjs | |
| parent | d34f9426d5317a1948f12f229439946728bc342a (diff) | |
| download | pure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz pure-b1ce1927022d374198441afafeb27199fc8a052a.zip | |
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/cli.mjs')
| -rw-r--r-- | prototypes/workbench/cli.mjs | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/prototypes/workbench/cli.mjs b/prototypes/workbench/cli.mjs new file mode 100644 index 0000000..b67f4d3 --- /dev/null +++ b/prototypes/workbench/cli.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node +import { readFile, writeFile } from 'node:fs/promises'; +import { createInterface } from 'node:readline'; +import { createRuntime } from './web/runtime.mjs'; +import { parseProject } from './web/project.mjs'; +import { remoteTarget, pushProject, pullProject } from './remote.mjs'; + +const help = `Pure authoring workbench · 같은 정의와 Zig 실행기 + load PATH .pure 파일을 적용 (같은 이름의 상태 보존) + down NAME 정의한 입력 누르기 + up NAME 정의한 입력 놓기 + point NAME PHASE X Y 좌표 입력 (down / move / up / cancel) + point-target NAME VIEW PHASE X Y 그룹의 대상에 좌표 입력 + change NAME VALUE 숫자 입력 전달 + frame [1..600] 시간 진행, 1프레임 = 1/60초 + show 상태와 도형 출력 + limits C H W 표시 캐시(0..32)·변경 기록(0..8)·작업량(0..4294967295) 설정 + forget [PART] 표시 기록 삭제: all, values, history (상태는 유지) + costs 마지막 표시 계산의 작업량·명령 수·캐시 적중 출력 + reset 현재 정의의 초기 상태로 + save PATH 적용한 정의를 저장 + save-project PATH 적용 코드와 현재 배치를 JSON으로 저장 + restore PATH 저장한 배치를 복원 (초안이 있으면 파일에 보존) + connect URL KEY 로컬 네이티브 저장 서버의 작업물에 연결 + push [REVISION] 현재 배치를 저장 (첫 생성은 push 0, 충돌 시 기존 값 유지) + pull 원격 배치를 검증·복원하고 revision 기억 + help / quit +브라우저와 별도 인스턴스입니다. 임의 JavaScript는 실행하지 않습니다.`; + +async function main() { + const args = process.argv.slice(2); + if (args.includes('--help')) { console.log(help); return; } + if (args.length > 1) throw new Error('사용법: node cli.mjs [project.pure]'); + const runtime = await createRuntime(await readFile(new URL('./web/core.wasm', import.meta.url))); + let savedDraft = null; + let remote = null, remoteRevision = null; + async function loadFile(path, restoreOnly = false) { + const content = await readFile(path, 'utf8'); + if (restoreOnly || path.toLowerCase().endsWith('.json')) { + const project = parseProject(content); + if (project.version === 2) { + savedDraft = runtime.restoreDocument(project).draft; + return; + } + if (restoreOnly) throw new Error('현재 배치가 포함된 version 2 파일이 필요합니다.'); + runtime.apply(project.source); + } else runtime.apply(content); + savedDraft = null; + } + if (args[0]) await loadFile(args[0]); else runtime.apply(''); + const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) }); + if (process.stdin.isTTY) console.log(help); + terminal.setPrompt('pure> '); + if (process.stdin.isTTY) terminal.prompt(); + for await (const line of terminal) { + try { + const match = /^(\S+)(?:\s+(.+))?$/.exec(line.trim()); + const name = match?.[1] || 'show'; + const arg = match?.[2]; + if (name === 'quit') { terminal.close(); break; } + if (name === 'help') console.log(help); + else if (name === 'costs' && !arg) console.log(JSON.stringify({ evaluation: runtime.evaluationStats() })); + else { + if (name === 'load' && arg) await loadFile(arg); + else if (name === 'restore' && arg) await loadFile(arg, true); + else if (name === 'connect' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 2) throw new Error('connect URL KEY 형식을 사용하세요.'); + remote = remoteTarget(parts[0], parts[1]); + remoteRevision = null; + console.log(JSON.stringify({ remote: remote.href })); + } else if (name === 'push') { + if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.'); + const expected = arg ?? remoteRevision; + if (expected === null) throw new Error('먼저 pull로 버전을 읽거나 push 0으로 새 작업물을 만드세요.'); + remoteRevision = await pushProject(remote, expected, runtime.exportDocument(savedDraft ?? undefined)); + console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, stored: true })); + } else if (name === 'pull' && !arg) { + if (!remote) throw new Error('먼저 connect URL KEY로 연결하세요.'); + const loaded = await pullProject(remote); + savedDraft = runtime.restoreDocument(loaded.project).draft; + remoteRevision = loaded.revision; + console.log(JSON.stringify({ remote: remote.href, revision: remoteRevision, restored: true })); + } + else if (name === 'save-project' && arg) await writeFile(arg, JSON.stringify(runtime.exportDocument(savedDraft ?? undefined), null, 2) + '\n', {flag:'wx'}); + else if (name === 'save' && arg) await writeFile(arg, runtime.exportProject().source, { flag: 'wx' }); + else if (name === 'down' && arg) runtime.down(arg, 'terminal'); + else if (name === 'up' && arg) runtime.up(arg, 'terminal'); + else if (name === 'point' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 4) throw new Error('point NAME PHASE X Y 형식을 사용하세요.'); + runtime.point(parts[0], parts[1], Number(parts[2]), Number(parts[3]), 'terminal'); + } else if (name === 'point-target' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 5) throw new Error('point-target NAME VIEW PHASE X Y 형식을 사용하세요.'); + runtime.point(parts[0], parts[2], Number(parts[3]), Number(parts[4]), 'terminal', parts[1]); + } else if (name === 'change' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 2) throw new Error('change NAME VALUE 형식을 사용하세요.'); + runtime.change(parts[0], Number(parts[1]), 'terminal'); + } else if (name === 'frame') { + if (arg !== undefined && !/^\d+$/.test(arg)) throw new Error('frame 1..600 정수를 입력하세요.'); + const count = arg === undefined ? 1 : Number(arg); + if (count < 1 || count > 600) throw new Error('frame 1..600 정수를 입력하세요.'); + runtime.frame(count); + } else if (name === 'reset' && !arg) runtime.reset(); + else if (name === 'limits' && arg) { + const parts = arg.split(/\s+/); + if (parts.length !== 3 || parts.some(part => !/^\d+$/.test(part))) + throw new Error('limits C H W 정수 세 개를 입력하세요.'); + const [cacheEntries, historyEntries, workLimit] = parts.map(Number); + runtime.configureEvaluation({ cacheEntries, historyEntries, workLimit }); + } else if (name === 'forget') runtime.forgetEvaluation(arg || 'all'); + else if (name !== 'show' || arg) throw new Error('명령 형식을 확인하세요. help로 안내를 볼 수 있습니다.'); + const result = runtime.snapshot(); + console.log(JSON.stringify(result)); + if (result.lastError && !process.stdin.isTTY) process.exitCode = 1; + } + } catch (error) { + console.error(`${error.line ? `${error.line}:${error.column || 1} ` : ''}${error.message}`); + if (!process.stdin.isTTY) process.exitCode = 1; + } + if (process.stdin.isTTY) terminal.prompt(); + } +} + +main().catch((error) => { console.error(error.message); process.exitCode = 1; }); |
