shellbase 0.9.1 → 0.10.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/README.md +15 -0
- package/dist/agent.js +60 -6
- package/dist/devices.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,6 +117,7 @@ shellbase start --no-restore # 복구하지 않고 이 폴더 세션 하나
|
|
|
117
117
|
| 하고 싶은 것 | 폰에서 하는 법 |
|
|
118
118
|
|---|---|
|
|
119
119
|
| 세션 하나 더 열기 | 목록 화면의 **`+ 새 세션`**, 또는 터미널 화면의 기기 이름(▾) → **`+ 이 컴퓨터에서 새 세션 열기`** (열리면 그 세션으로 바로 넘어가요) |
|
|
120
|
+
| 잠깐 쓸 터미널 열기 | 터미널 화면 위쪽의 **`+ 임시 터미널`** — 지금 폴더에 하나 더 띄워요 (목록에 안 남고, 나가면 사라져요) |
|
|
120
121
|
| 세션 갈아타기 | 터미널 화면 위쪽의 **기기 이름(▾)** 탭 → 원하는 세션 선택 |
|
|
121
122
|
| 세션 끄기 | 목록의 **전원 아이콘**, 또는 터미널 화면의 기기 이름(▾) → **이 세션 끄기** |
|
|
122
123
|
| 화면만 닫기 | `← 목록` 으로 나가기 — 컴퓨터의 세션은 계속 살아있어요 |
|
|
@@ -132,6 +133,20 @@ shellbase start --no-restore # 복구하지 않고 이 폴더 세션 하나
|
|
|
132
133
|
종료돼요(그 대신 다음 실행 때 자동 복구됩니다). 세션을 하나만 끄고 싶으면 폰에서 그 세션의 `✕` 를
|
|
133
134
|
쓰세요 — 나머지 세션은 그대로 유지돼요.
|
|
134
135
|
|
|
136
|
+
### 임시 터미널 (0.10.0+)
|
|
137
|
+
|
|
138
|
+
Claude Code 가 화면을 꽉 채우고 있을 때 `git status` 같은 걸 잠깐 쳐보려고 터미널을 하나 더 열곤 하는데,
|
|
139
|
+
그렇게 연 터미널이 목록에 계속 쌓이는 게 문제였어요. 터미널 화면 위쪽의 **`+ 임시 터미널`** 로 연 것은
|
|
140
|
+
**한 번 쓰고 버리는** 터미널이에요.
|
|
141
|
+
|
|
142
|
+
- 세션 목록에도, 상단 칩 줄에도 **안 보여요** (지금 보고 있는 동안만 칩으로 보입니다)
|
|
143
|
+
- 그 화면에서 나가면 **스스로 종료**돼요 — 다만 **방금까지 뭔가 돌고 있었으면 끄지 않고 목록에 남깁니다**
|
|
144
|
+
(하던 일이 조용히 사라지면 안 되니까요)
|
|
145
|
+
- 컴퓨터를 재시작해도 **되살아나지 않아요** (정식 세션만 복구 대상)
|
|
146
|
+
- 브라우저를 그냥 닫아버린 경우엔, 아무도 안 보고 화면도 조용한 채로 **5분**이 지나면 컴퓨터가 알아서 정리해요
|
|
147
|
+
- 계속 쓰고 싶어지면 **`임시 · 계속 쓰기`** 를 누르세요 — 정식 세션이 되고 이름의 `(임시)` 표시도 떨어져요
|
|
148
|
+
- 임시 터미널에는 **Claude 자동 시작이 적용되지 않아요** (명령 하나 치려고 여는 것이라 빈 터미널로 열립니다)
|
|
149
|
+
|
|
135
150
|
### 파일 보기·편집
|
|
136
151
|
|
|
137
152
|
`📁 폴더 찾기` 목록에는 폴더와 함께 **파일**도 나와요. 파일을 탭하면 편집기가 열리고, 고친 뒤 `저장` 을
|
package/dist/agent.js
CHANGED
|
@@ -12,7 +12,7 @@ import { requireClient } from './auth.js';
|
|
|
12
12
|
import { TERMINAL_CATEGORY, CONTROL_TOKEN, SESSION_ENV } from './config.js';
|
|
13
13
|
import { startSleepGuard, stopSleepGuard } from './sleep-guard.js';
|
|
14
14
|
import { encryptFrame, decryptFrame, generateFrameKey } from './crypto.js';
|
|
15
|
-
import { registerDevice, heartbeatDevice, unregisterDevice, listOnlineNames, renameDevice, markSessionDone, } from './devices.js';
|
|
15
|
+
import { registerDevice, heartbeatDevice, unregisterDevice, listOnlineNames, renameDevice, markSessionDone, keepDevice, } from './devices.js';
|
|
16
16
|
import { promptApproval } from './prompt.js';
|
|
17
17
|
import { touchRecentDir } from './recent-dirs.js';
|
|
18
18
|
import { listDirs } from './browse.js';
|
|
@@ -23,6 +23,11 @@ import { IMAGE_CHUNK_TTL_MS, pruneImages, saveImage } from './images.js';
|
|
|
23
23
|
import { watchDoneNotes } from './notify.js';
|
|
24
24
|
import { hookAsked, hookState, installHook, rememberHookAsked } from './claude-hook.js';
|
|
25
25
|
const HEARTBEAT_MS = 20_000;
|
|
26
|
+
// 임시 터미널 정리 — 보는 사람도 없고(폰에서 아무 신호도 없고) 화면도 조용한 채로 이만큼 지나면 닫는다.
|
|
27
|
+
// 폰은 보고 있는 동안 10초마다 신호(ping)를 보내므로, 앱을 닫거나 다른 세션으로 넘어가면 신호가 끊긴다.
|
|
28
|
+
// 5분이나 두는 이유: 폰 화면이 잠기면 타이머가 멈춰서 신호도 잠깐 끊기기 때문 (잠깐 자리를 비운 것과 구분).
|
|
29
|
+
const TEMP_ORPHAN_MS = 5 * 60 * 1000;
|
|
30
|
+
const TEMP_SWEEP_MS = 60 * 1000;
|
|
26
31
|
// 올린 사진 정리 주기 — 보관 기한이 하루라, 오래 켜둔 컴퓨터에서도 반나절 안에 치워진다
|
|
27
32
|
const IMAGE_SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
28
33
|
const APPROVAL_TIMEOUT_MS = 30_000;
|
|
@@ -157,7 +162,8 @@ export async function runAgent(options) {
|
|
|
157
162
|
}
|
|
158
163
|
function persistOpenSessions() {
|
|
159
164
|
saveOpenSessions([...sessions.values()]
|
|
160
|
-
|
|
165
|
+
// 임시 터미널은 재시작 후 되살리지 않는다 — 한 번 쓰고 버리는 것이므로
|
|
166
|
+
.filter((session) => !session.closing && !session.temporary)
|
|
161
167
|
.map((session) => ({ dir: session.cwd, name: session.name })));
|
|
162
168
|
}
|
|
163
169
|
const send = async (frame) => {
|
|
@@ -322,7 +328,8 @@ export async function runAgent(options) {
|
|
|
322
328
|
console.error('접속 승인 처리 실패:', err.message);
|
|
323
329
|
});
|
|
324
330
|
}
|
|
325
|
-
async function createSession(dir, explicitName) {
|
|
331
|
+
async function createSession(dir, explicitName, opts = {}) {
|
|
332
|
+
const temporary = opts.temporary === true;
|
|
326
333
|
const cwd = path.resolve(dir);
|
|
327
334
|
if (sessions.size >= MAX_SESSIONS) {
|
|
328
335
|
throw new Error(`세션은 한 번에 최대 ${MAX_SESSIONS}개까지 열 수 있어요.`);
|
|
@@ -341,7 +348,9 @@ export async function runAgent(options) {
|
|
|
341
348
|
const session = {
|
|
342
349
|
deviceId,
|
|
343
350
|
frameKey: generateFrameKey(),
|
|
344
|
-
|
|
351
|
+
// 임시 터미널은 이름만 봐도 알 수 있게 (임시) 를 붙인다 — 목록에는 안 뜨지만
|
|
352
|
+
// 그 터미널을 보고 있는 동안 상단 칩에는 이 이름이 그대로 보인다
|
|
353
|
+
name: explicitName ?? (temporary ? `${defaultName(cwd)} (임시)` : defaultName(cwd)),
|
|
345
354
|
cwd,
|
|
346
355
|
rowId: '',
|
|
347
356
|
pty: pty.spawn(shell, [], {
|
|
@@ -377,6 +386,8 @@ export async function runAgent(options) {
|
|
|
377
386
|
idleTimer: null,
|
|
378
387
|
announcedFor: 0,
|
|
379
388
|
hookSeen: false,
|
|
389
|
+
temporary,
|
|
390
|
+
viewerSeenAt: Date.now(),
|
|
380
391
|
};
|
|
381
392
|
session.screen.loadAddon(session.serializer);
|
|
382
393
|
session.pty.onData((data) => {
|
|
@@ -401,6 +412,7 @@ export async function runAgent(options) {
|
|
|
401
412
|
cwd,
|
|
402
413
|
frameKey: session.frameKey,
|
|
403
414
|
favoriteDirs: touchRecentDir(cwd),
|
|
415
|
+
temporary,
|
|
404
416
|
});
|
|
405
417
|
}
|
|
406
418
|
catch (err) {
|
|
@@ -454,6 +466,7 @@ export async function runAgent(options) {
|
|
|
454
466
|
shuttingDown = true;
|
|
455
467
|
clearInterval(heartbeat);
|
|
456
468
|
clearInterval(imageSweep);
|
|
469
|
+
clearInterval(tempSweep);
|
|
457
470
|
stopNotes();
|
|
458
471
|
for (const session of [...sessions.values()]) {
|
|
459
472
|
session.closing = true;
|
|
@@ -515,6 +528,8 @@ export async function runAgent(options) {
|
|
|
515
528
|
const session = sessions.get(frame.to);
|
|
516
529
|
if (!session || session.closing)
|
|
517
530
|
return;
|
|
531
|
+
// 폰에서 온 프레임 = 누군가 이 세션을 보고 있다는 뜻 (임시 터미널 정리 판단에 쓴다)
|
|
532
|
+
session.viewerSeenAt = Date.now();
|
|
518
533
|
if (frame.kind === 'connect_request') {
|
|
519
534
|
// 화면 보기는 승인 여부와 무관하게 항상 되므로, 승인을 묻기 전에 지금 화면부터 그대로 복원해준다
|
|
520
535
|
void sendRestore(session);
|
|
@@ -542,6 +557,27 @@ export async function runAgent(options) {
|
|
|
542
557
|
void closeSession(session, { deliberate: true });
|
|
543
558
|
return;
|
|
544
559
|
}
|
|
560
|
+
// 임시 터미널을 정식 세션으로 — 목록에 나타나고, 재시작해도 되살아나고, 스스로 정리되지 않는다
|
|
561
|
+
if (frame.kind === 'keep_session') {
|
|
562
|
+
if (!readControl(frame.data, session.frameKey))
|
|
563
|
+
return;
|
|
564
|
+
if (!session.temporary)
|
|
565
|
+
return;
|
|
566
|
+
const before = session.name;
|
|
567
|
+
session.temporary = false;
|
|
568
|
+
session.name = session.name.replace(/ \(임시\)$/, '');
|
|
569
|
+
keepDevice(cb, session.rowId, session.name)
|
|
570
|
+
.then(() => {
|
|
571
|
+
persistOpenSessions();
|
|
572
|
+
console.log(`📱 "${before}" 를 계속 쓰기로 바꿨어요 — 이제 목록에 남습니다.`);
|
|
573
|
+
})
|
|
574
|
+
.catch((err) => {
|
|
575
|
+
session.temporary = true;
|
|
576
|
+
session.name = before;
|
|
577
|
+
console.error('계속 쓰기 전환 실패:', err.message);
|
|
578
|
+
});
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
545
581
|
// 폰에서 세션 이름 바꾸기 — 컴퓨터를 가리키는 앞부분은 유지해서 목록의 컴퓨터 묶음이 깨지지 않게 한다
|
|
546
582
|
if (frame.kind === 'rename') {
|
|
547
583
|
const control = readControl(frame.data, session.frameKey);
|
|
@@ -757,10 +793,12 @@ export async function runAgent(options) {
|
|
|
757
793
|
if (!control || typeof control.dir !== 'string')
|
|
758
794
|
return;
|
|
759
795
|
const dir = control.dir;
|
|
796
|
+
// 폰의 「+ 터미널」 은 임시로, 「여기서 새 세션」 은 정식 세션으로 연다
|
|
797
|
+
const temporary = control.temporary === true;
|
|
760
798
|
void (async () => {
|
|
761
799
|
try {
|
|
762
|
-
const created = await createSession(dir);
|
|
763
|
-
console.log(`\n📱 폰 요청으로
|
|
800
|
+
const created = await createSession(dir, undefined, { temporary });
|
|
801
|
+
console.log(`\n📱 폰 요청으로 ${temporary ? '임시 ' : ''}세션을 열었어요: "${created.name}" (${created.cwd})`);
|
|
764
802
|
}
|
|
765
803
|
catch (err) {
|
|
766
804
|
const reason = err.message;
|
|
@@ -796,6 +834,22 @@ export async function runAgent(options) {
|
|
|
796
834
|
}
|
|
797
835
|
}
|
|
798
836
|
});
|
|
837
|
+
// 임시 터미널 청소 — 폰이 창을 닫거나 앱이 죽어서 "끄기" 요청이 못 온 경우를 위한 안전망.
|
|
838
|
+
// (평소에는 폰이 그 터미널을 떠나는 순간 바로 끄기 요청을 보낸다)
|
|
839
|
+
const tempSweep = setInterval(() => {
|
|
840
|
+
const now = Date.now();
|
|
841
|
+
for (const session of sessions.values()) {
|
|
842
|
+
if (!session.temporary || session.closing)
|
|
843
|
+
continue;
|
|
844
|
+
if (now - session.viewerSeenAt < TEMP_ORPHAN_MS)
|
|
845
|
+
continue;
|
|
846
|
+
// 아무도 안 봐도 뭔가 돌고 있으면(출력이 계속 나오면) 끝날 때까지 기다린다
|
|
847
|
+
if (session.lastOutputAt && now - session.lastOutputAt < TEMP_ORPHAN_MS)
|
|
848
|
+
continue;
|
|
849
|
+
console.log(`🧹 임시 터미널 "${session.name}" 을(를) 아무도 안 봐서 정리해요.`);
|
|
850
|
+
void closeSession(session, { deliberate: true });
|
|
851
|
+
}
|
|
852
|
+
}, TEMP_SWEEP_MS);
|
|
799
853
|
const heartbeat = setInterval(() => {
|
|
800
854
|
for (const session of sessions.values()) {
|
|
801
855
|
if (session.closing || !session.rowId)
|
package/dist/devices.js
CHANGED
|
@@ -11,6 +11,7 @@ export async function registerDevice(cb, params) {
|
|
|
11
11
|
cwd: params.cwd,
|
|
12
12
|
frame_key: params.frameKey,
|
|
13
13
|
favorite_dirs: params.favoriteDirs,
|
|
14
|
+
temporary: params.temporary === true,
|
|
14
15
|
last_seen_at: new Date().toISOString(),
|
|
15
16
|
},
|
|
16
17
|
});
|
|
@@ -47,6 +48,13 @@ export async function listOnlineNames(cb) {
|
|
|
47
48
|
}
|
|
48
49
|
return names;
|
|
49
50
|
}
|
|
51
|
+
// 임시 터미널을 "계속 쓰기" 로 바꿀 때 — 목록에 정식으로 남기고 이름에서 (임시) 표시를 뗀다.
|
|
52
|
+
// 반대 방향(정식 → 임시)은 없다: 이미 쓰고 있던 터미널이 조용히 사라지는 건 사고이기 때문.
|
|
53
|
+
export async function keepDevice(cb, rowId, deviceName) {
|
|
54
|
+
await cb.database.updateData(DEVICES_TABLE_ID, rowId, {
|
|
55
|
+
data: { temporary: false, device_name: deviceName },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
50
58
|
// 폰에서 세션 이름을 바꿀 때 — 목록 row 의 표시 이름만 갱신한다 (세션 자체는 그대로 유지)
|
|
51
59
|
export async function renameDevice(cb, rowId, deviceName) {
|
|
52
60
|
await cb.database.updateData(DEVICES_TABLE_ID, rowId, { data: { device_name: deviceName } });
|