@cliphijack/santaclaude 1.0.21 → 1.0.23

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/santaclaude.js +49 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -16,6 +16,49 @@ const fs = require('fs');
16
16
  const os = require('os');
17
17
  const path = require('path');
18
18
 
19
+ const VER = (() => { try { return require('./package.json').version; } catch (e) { return '0'; } })();
20
+ const VIA_NPX = __dirname.includes('_npx') || __dirname.includes('npm-cache') || process.env.SC_AUTOUP === '1';
21
+
22
+ function verGt(a, b) { const pa = String(a).split('.').map(Number), pb = String(b).split('.').map(Number); for (let i = 0; i < 3; i++) { if ((pa[i] || 0) > (pb[i] || 0)) return true; if ((pa[i] || 0) < (pb[i] || 0)) return false; } return false; }
23
+ const _TGT = path.join(os.homedir(), '.santaclaude-target'); // 워커가 지정한 목표 버전
24
+ const _GOOD = path.join(os.homedir(), '.santaclaude-good'); // 60초+ 산 마지막 안정버전(롤백용)
25
+ const _rd = (f) => { try { return fs.readFileSync(f, 'utf8').trim(); } catch (e) { return ''; } };
26
+
27
+ // ── 🔄 자동 업데이트 슈퍼바이저 (안전가드 포함) ──
28
+ // npx로 띄운 경우만: 실제 워커를 자식(@목표버전)으로 돌린다. 워커가 새 버전 감지하면 75로 종료 → 여기서 재시작.
29
+ // 안전판: ①새 버전이 계속 빨리 죽으면(크래시루프) 안정버전으로 자동 롤백 ②5연속 실패면 자동재시작 중단.
30
+ if (VIA_NPX && process.env.SC_SUP !== '1' && !process.argv.slice(2).some((a) => a === 'status' || a === 'connect')) {
31
+ const { spawnSync } = require('child_process');
32
+ console.log('🛷 SantaClaude — 자동 업데이트 켜짐 (크래시 롤백·서버 킬스위치 안전가드 포함)');
33
+ let quickFails = 0;
34
+ while (true) {
35
+ let spec = _rd(_TGT) || 'latest';
36
+ if (quickFails >= 3) { const g = _rd(_GOOD); if (g) { spec = g; console.log(` 🛟 새 버전이 반복해서 죽음 → 안정버전 ${g} 으로 고정`); } }
37
+ const t0 = Date.now();
38
+ const r = spawnSync('npx', ['-y', '@cliphijack/santaclaude@' + spec], { stdio: 'inherit', env: { ...process.env, SC_SUP: '1' } });
39
+ const lived = Date.now() - t0;
40
+ if (r.signal === 'SIGINT' || r.signal === 'SIGTERM') process.exit(0); // 사람이 Ctrl+C
41
+ if (lived > 60000) quickFails = 0; else quickFails++; // 60초+ 살았으면 정상
42
+ if (r.status === 75) { console.log(' ⬆️ 업데이트 적용 — 재시작'); continue; }
43
+ if (quickFails >= 5) { console.log(' ⛔ 반복 실패 — 자동 재시작 중단(수동 확인 필요). 마지막 코드 ' + r.status); process.exit(r.status == null ? 1 : r.status); }
44
+ console.log(` ⚠️ 커넥터 종료(코드 ${r.status}) — 6초 후 재시작`); spawnSync('sleep', ['6']);
45
+ }
46
+ }
47
+ // 워커(SC_SUP=1): 서버 버전 게이트 확인 → 새 목표면 목표버전 기록하고 75로 종료(슈퍼바이저가 재시작)
48
+ function checkUpdate() {
49
+ fetch(DEFAULT_API + '/api/cli-version', { headers: { accept: 'application/json' } })
50
+ .then((r) => r.json()).then((j) => {
51
+ if (!j || j.paused) return; // 🔴 킬스위치: paused면 업데이트 중단
52
+ const target = String(j.target || '');
53
+ if (!target || target === VER) return; // 목표=현재면 그대로 (target≠VER이면 업/다운 둘다 = 롤백 가능)
54
+ if (process.env.SC_SUP === '1') {
55
+ console.log(` ⬆️ 새 버전 ${target} (현재 ${VER}) — 업데이트 재시작…`);
56
+ try { fs.writeFileSync(_TGT, target); } catch (e) {}
57
+ setTimeout(() => process.exit(75), 800);
58
+ } else console.log(` ⬆️ 새 버전 ${target} 나옴 (현재 ${VER}) — 재시작 권장: npx -y @cliphijack/santaclaude@latest`);
59
+ }).catch(() => {});
60
+ }
61
+
19
62
  const CONF = path.join(os.homedir(), '.santaclaude.json');
20
63
  const DEFAULT_API = 'https://santaclaude.app';
21
64
 
@@ -334,7 +377,9 @@ async function run(conf) {
334
377
 
335
378
  cleanOldImages();
336
379
  try { execFileSync('tmux', ['set-option', '-g', 'history-limit', '50000'], { stdio: 'ignore' }); } catch (e) {} // 새로 띄우는 루돌프 pane은 스크롤백 5만줄(기존 pane은 유지)
337
- console.log(`🛷 SantaClaude 커넥터 가동 pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
380
+ setTimeout(() => { try { fs.writeFileSync(_GOOD, VER); } catch (e) {} }, 60000); // 60초+ 살면 버전을 안정버전으로 기록(롤백 기준)
381
+ setTimeout(checkUpdate, 90000); setInterval(checkUpdate, 3 * 3600 * 1000); // 자동 업데이트 체크: 시작 90초 후 + 3시간마다
382
+ console.log(`🛷 SantaClaude 커넥터 가동 v${VER} — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
338
383
  if (paneExists(pane)) {
339
384
  // 기존(고객) 세션엔 사람이 안 붙어있을 때만 리사이즈 — 직접 쓰고 있으면 화면 절대 안 건드림
340
385
  if (!clientsAttached(session)) { try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} }
@@ -443,11 +488,13 @@ async function run(conf) {
443
488
  execFileSync('tmux', ['send-keys', '-t', w, '-l', String(cmd.text || '')]);
444
489
  setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
445
490
  console.log(` ⌨️ 입력 → ${w}: ${String(cmd.text || '').slice(0, 50)}`);
491
+ setTimeout(sendScreen, 650); setTimeout(sendScreen, 1200); // 입력 직후 즉시 화면 푸시(diff-gated) — 타이핑→결과 1초 안
446
492
  } else if (cmd.action === 'key') {
447
493
  // 플로팅 컨트롤 — 특수키 그대로 전달 (Escape, C-c, Up 등)
448
494
  const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
449
495
  execFileSync('tmux', ['send-keys', '-t', w, String(cmd.key || '')]);
450
496
  console.log(` ⌨️ 키 → ${w}: ${cmd.key}`);
497
+ setTimeout(sendScreen, 300); setTimeout(sendScreen, 750);
451
498
  } else if (cmd.action === 'resize') {
452
499
  // 📐 창에맞춤 토글: fit=강제 reflow(사람 붙어있어도) / restore=window-size latest로 원복(내 터미널 따라가기)
453
500
  const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
@@ -583,7 +630,7 @@ async function run(conf) {
583
630
  let ws = null, hbIv = null, scrIv = null;
584
631
  function connectWS() {
585
632
  try { ws = new WebSocket(wsUrl); } catch (e) { console.error(' WebSocket 생성 실패:', e.message); setTimeout(connectWS, 5000); return; }
586
- ws.addEventListener('open', () => { console.log(' 🔌 클라우드 연결됨 (WebSocket).'); lastScr = ''; sendHb(); sendScreen(); clearInterval(hbIv); clearInterval(scrIv); hbIv = setInterval(sendHb, Math.max(every, 15000)); scrIv = setInterval(() => { sendScreen(); sendTxDelta(); }, 2500); });
633
+ ws.addEventListener('open', () => { console.log(' 🔌 클라우드 연결됨 (WebSocket).'); lastScr = ''; sendHb(); sendScreen(); clearInterval(hbIv); clearInterval(scrIv); hbIv = setInterval(sendHb, Math.max(every, 15000)); scrIv = setInterval(() => { sendScreen(); sendTxDelta(); }, 1000); });
587
634
  ws.addEventListener('message', (e) => { let m; try { m = JSON.parse(typeof e.data === 'string' ? e.data : e.data.toString()); } catch { return; } if (m.type === 'job') onJob(m); else if (m.type === 'ctl') runControl(m.cmd); });
588
635
  ws.addEventListener('close', () => { clearInterval(hbIv); clearInterval(scrIv); console.log(' 🔌 연결 끊김 — 5초 후 재연결'); setTimeout(connectWS, 5000); });
589
636
  ws.addEventListener('error', () => { try { ws.close(); } catch (e) {} });