aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/cli.mjs
blob: b67f4d342265c537c266d8e2a27ccabd08b67121 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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; });