@cliphijack/santaclaude 0.5.0 → 0.6.0
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/package.json +1 -1
- package/santaclaude.js +31 -20
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -45,9 +45,23 @@ function spawnClaude(session, cmd, cwd) {
|
|
|
45
45
|
try { execFileSync('sh', ['-c', 'sleep 0.3']); } catch (e) {}
|
|
46
46
|
execFileSync('tmux', ['send-keys', '-t', session, 'Enter']);
|
|
47
47
|
}
|
|
48
|
-
//
|
|
49
|
-
function
|
|
50
|
-
|
|
48
|
+
// 한 세션(작업장) 안에 새 윈도우(탭)로 claude 실행 — 프로젝트별 분리
|
|
49
|
+
function spawnWindow(session, name, cmd, cwd) {
|
|
50
|
+
const a = ['new-window', '-t', session, '-n', name];
|
|
51
|
+
if (cwd) a.push('-c', cwd);
|
|
52
|
+
execFileSync('tmux', a);
|
|
53
|
+
try { execFileSync('sh', ['-c', 'sleep 1']); } catch (e) {}
|
|
54
|
+
execFileSync('tmux', ['send-keys', '-t', session + ':' + name, '-l', cmd]);
|
|
55
|
+
try { execFileSync('sh', ['-c', 'sleep 0.3']); } catch (e) {}
|
|
56
|
+
execFileSync('tmux', ['send-keys', '-t', session + ':' + name, 'Enter']);
|
|
57
|
+
}
|
|
58
|
+
function windowExists(session, name) {
|
|
59
|
+
try { return execFileSync('tmux', ['list-windows', '-t', session, '-F', '#{window_name}'], { encoding: 'utf8' }).trim().split('\n').indexOf(name) >= 0; }
|
|
60
|
+
catch (e) { return false; }
|
|
61
|
+
}
|
|
62
|
+
// 작업장(세션) 안의 윈도우(루돌프) 목록
|
|
63
|
+
function listWindows(session) {
|
|
64
|
+
try { return execFileSync('tmux', ['list-windows', '-t', session, '-F', '#{window_name}'], { encoding: 'utf8' }).trim().split('\n').filter(Boolean); }
|
|
51
65
|
catch (e) { return []; }
|
|
52
66
|
}
|
|
53
67
|
|
|
@@ -77,43 +91,40 @@ async function run(conf) {
|
|
|
77
91
|
console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
// 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
|
|
80
95
|
function runControl(cmd) {
|
|
81
96
|
try {
|
|
82
97
|
if (cmd.action === 'create') {
|
|
83
|
-
if (paneExists(
|
|
98
|
+
if (!paneExists(session)) spawnClaude(session, claudeCmd); // 작업장 세션 없으면 먼저 띄움
|
|
99
|
+
if (windowExists(session, cmd.name)) { console.log(` 🦌 "${cmd.name}" 탭 이미 있어 — 안 만듦`); return; }
|
|
84
100
|
let cwd = cmd.cwd ? String(cmd.cwd).trim() : '';
|
|
85
101
|
if (cwd) {
|
|
86
102
|
if (cwd === '~' || cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(1)); // ~ 확장
|
|
87
103
|
try { fs.mkdirSync(cwd, { recursive: true }); } // 폴더 없으면 새로 생성
|
|
88
104
|
catch (e) { console.warn(` ⚠️ 폴더 생성 실패(${cwd}): ${e.message} — 홈에서 띄움`); cwd = ''; }
|
|
89
105
|
}
|
|
90
|
-
|
|
91
|
-
console.log(` 🦌 새 루돌프 "${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''}
|
|
106
|
+
spawnWindow(session, cmd.name, claudeCmd, cwd || undefined);
|
|
107
|
+
console.log(` 🦌 새 루돌프 "${session}:${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''} 탭 추가 (tmux attach -t ${session} → Ctrl+b 창번호로 이동, 첫 로그인 1회)`);
|
|
92
108
|
} else if (cmd.action === 'kill') {
|
|
93
|
-
execFileSync('tmux', ['kill-
|
|
94
|
-
console.log(` 💤 루돌프 "${cmd.name}" 닫음`);
|
|
109
|
+
execFileSync('tmux', ['kill-window', '-t', session + ':' + cmd.name]);
|
|
110
|
+
console.log(` 💤 루돌프 "${session}:${cmd.name}" 탭 닫음`);
|
|
95
111
|
}
|
|
96
112
|
} catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
async function tick() {
|
|
100
116
|
try {
|
|
101
|
-
post(api, '/api/heartbeat', { token, pane, sessions:
|
|
117
|
+
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session) }).catch(() => {}); // 보고 = 작업장 안 윈도우(루돌프)들
|
|
102
118
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
103
119
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
104
120
|
for (const j of (d.jobs || [])) {
|
|
105
|
-
|
|
106
|
-
let tpane = (j.target && String(j.target).trim()) || pane;
|
|
107
|
-
if (!paneExists(tpane)) {
|
|
108
|
-
if (conf.spawn !== false && tpane !== pane) {
|
|
109
|
-
try { spawnClaude(String(tpane).split(':')[0], claudeCmd); console.log(` 🦌 대상 "${tpane}" 세션이 없어서 새로 띄웠어 (tmux attach 로 로그인 1회).`); }
|
|
110
|
-
catch (e) { tpane = pane; }
|
|
111
|
-
} else if (!paneExists(tpane)) { tpane = pane; }
|
|
112
|
-
}
|
|
113
|
-
if (!paneExists(tpane)) { if (!warned) { console.warn(` ⚠️ 주입할 세션 "${tpane}" 없음 — 보류`); warned = true; } continue; }
|
|
121
|
+
if (!paneExists(session)) { if (!warned) { console.warn(` ⚠️ 작업장 "${session}" 없음 — 보류`); warned = true; } continue; }
|
|
114
122
|
warned = false;
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
// 대상 윈도우 지정 → session:target, 없으면 기본 pane(활성창)
|
|
124
|
+
const tname = j.target && String(j.target).trim();
|
|
125
|
+
const tgt = (tname && windowExists(session, tname)) ? (session + ':' + tname) : pane;
|
|
126
|
+
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
127
|
+
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
117
128
|
}
|
|
118
129
|
} catch (e) { console.error('[폴링 오류]', e.message); }
|
|
119
130
|
}
|