shellbase 0.1.8 → 0.1.10
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 +1 -1
- package/dist/browse.js +48 -0
- package/dist/session.js +18 -1
- package/dist/spawner.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ shellbase start --dir ~/projects/my-app --name "내 노트북"
|
|
|
53
53
|
|
|
54
54
|
| 하고 싶은 것 | 폰에서 하는 법 |
|
|
55
55
|
|---|---|
|
|
56
|
-
| 세션 하나 더 열기 | 목록 화면의 **`+ 새
|
|
56
|
+
| 세션 하나 더 열기 | 목록 화면의 **`+ 새 세션`**, 또는 터미널 화면의 기기 이름(▾) → **`+ 이 컴퓨터에서 새 세션 열기`** (열리면 그 세션으로 바로 넘어가요) |
|
|
57
57
|
| 세션 갈아타기 | 터미널 화면 위쪽의 **기기 이름(▾)** 탭 → 원하는 세션 선택 |
|
|
58
58
|
| 세션 끄기 | 목록의 **전원 아이콘**, 또는 터미널 화면의 기기 이름(▾) → **이 세션 끄기** |
|
|
59
59
|
| 화면만 닫기 | `← 목록` 으로 나가기 — 컴퓨터의 세션은 계속 살아있어요 |
|
package/dist/browse.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
// 폰에서 폴더를 탭해서 파고들며 고를 수 있게, 요청받은 경로의 "하위 폴더 목록"만 돌려준다.
|
|
5
|
+
// 파일 이름은 보내지 않음 — 세션을 열 위치를 고르는 용도이고, 목록이 길어질 이유도 없음.
|
|
6
|
+
const MAX_ENTRIES = 300;
|
|
7
|
+
export function listDirs(dir) {
|
|
8
|
+
const target = path.resolve(dir);
|
|
9
|
+
const up = path.dirname(target);
|
|
10
|
+
const base = {
|
|
11
|
+
dir: target,
|
|
12
|
+
parent: up === target ? null : up,
|
|
13
|
+
home: os.homedir(),
|
|
14
|
+
entries: [],
|
|
15
|
+
};
|
|
16
|
+
let items;
|
|
17
|
+
try {
|
|
18
|
+
items = fs.readdirSync(target, { withFileTypes: true });
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return { ...base, error: `폴더를 열 수 없어요: ${err.message}` };
|
|
22
|
+
}
|
|
23
|
+
const names = items
|
|
24
|
+
.filter((entry) => {
|
|
25
|
+
// 숨김 폴더(.git, .cache 등)는 제외 — 목록이 길어지고 세션을 열 곳도 아님
|
|
26
|
+
if (entry.name.startsWith('.'))
|
|
27
|
+
return false;
|
|
28
|
+
if (entry.isDirectory())
|
|
29
|
+
return true;
|
|
30
|
+
// 심볼릭 링크는 가리키는 대상이 폴더일 때만 포함
|
|
31
|
+
if (entry.isSymbolicLink()) {
|
|
32
|
+
try {
|
|
33
|
+
return fs.statSync(path.join(target, entry.name)).isDirectory();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
})
|
|
41
|
+
.map((entry) => entry.name)
|
|
42
|
+
.sort((a, b) => a.localeCompare(b));
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
entries: names.slice(0, MAX_ENTRIES),
|
|
46
|
+
truncated: names.length > MAX_ENTRIES,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/session.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerDevice, heartbeatDevice, unregisterDevice } from './devices.js'
|
|
|
10
10
|
import { promptApproval } from './prompt.js';
|
|
11
11
|
import { touchRecentDir } from './recent-dirs.js';
|
|
12
12
|
import { spawnSession } from './spawner.js';
|
|
13
|
+
import { listDirs } from './browse.js';
|
|
13
14
|
const HEARTBEAT_MS = 20_000;
|
|
14
15
|
const APPROVAL_TIMEOUT_MS = 30_000;
|
|
15
16
|
const SCROLLBACK_MAX = 16_000;
|
|
@@ -150,12 +151,28 @@ export async function startSession(options) {
|
|
|
150
151
|
void shutdown();
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
154
|
+
// 폰의 폴더 탐색기 — 요청받은 경로의 하위 폴더 목록을 암호화해서 돌려준다 (세션 열 곳 고르기용)
|
|
155
|
+
if (frame.kind === 'list_dirs') {
|
|
156
|
+
const control = readControl(frame.data, device.frameKey);
|
|
157
|
+
if (!control)
|
|
158
|
+
return;
|
|
159
|
+
const listing = listDirs(typeof control.dir === 'string' && control.dir ? control.dir : cwd);
|
|
160
|
+
channel
|
|
161
|
+
.send({
|
|
162
|
+
kind: 'dirs',
|
|
163
|
+
to: device.deviceId,
|
|
164
|
+
data: encryptFrame(JSON.stringify(listing), device.frameKey),
|
|
165
|
+
}, { includeSelf: false })
|
|
166
|
+
.catch((err) => console.error('폴더 목록 전송 실패:', err.message));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
153
169
|
// 폰에서 "새 세션 열기" — 이 세션이 CLI 를 한 번 더 띄워주고, 새 세션은 폰 목록에 따로 나타난다.
|
|
154
170
|
if (frame.kind === 'new_session') {
|
|
155
171
|
const control = readControl(frame.data, device.frameKey);
|
|
156
172
|
if (!control || typeof control.dir !== 'string')
|
|
157
173
|
return;
|
|
158
|
-
|
|
174
|
+
// 이름 앞부분(컴퓨터를 가리키는 부분)만 물려줘서 "GB10 (Docker) · shellbase" 처럼 나오게 함
|
|
175
|
+
const result = spawnSession(control.dir, deviceName.split(' · ')[0]);
|
|
159
176
|
if (result.ok) {
|
|
160
177
|
console.log(`\n📱 폰 요청으로 새 세션을 열었어요 (${control.dir})`);
|
|
161
178
|
return;
|
package/dist/spawner.js
CHANGED
|
@@ -11,7 +11,10 @@ function resolveEntry() {
|
|
|
11
11
|
return current;
|
|
12
12
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// nameLabel: 폰 목록에서 부모 세션과 같은 컴퓨터임을 알아보게 물려받는 이름 앞부분.
|
|
15
|
+
// 도커 컨테이너 안에서는 os.hostname() 이 컨테이너 ID(예: 7a54a499a8f3)로 나와서 기본 이름이
|
|
16
|
+
// "7a54a499a8f3 · shellbase" 처럼 알아보기 어려워짐 — 그래서 부모 이름을 물려준다.
|
|
17
|
+
export function spawnSession(dir, nameLabel) {
|
|
15
18
|
const target = path.resolve(dir);
|
|
16
19
|
let stat;
|
|
17
20
|
try {
|
|
@@ -26,7 +29,10 @@ export function spawnSession(dir) {
|
|
|
26
29
|
// 폰에서 띄우는 세션은 승인 물음에 대답할 터미널이 없다(stdio: 'ignore') — 그대로 두면 30초 뒤
|
|
27
30
|
// 자동 거부돼서 화면만 보이고 타이핑이 안 되는 세션이 된다. 요청을 보낸 쪽은 이미 이 세션의
|
|
28
31
|
// 암호화 키(주인만 읽을 수 있는 devices row)를 가지고 있음이 확인된 상태라 자동 승인으로 띄운다.
|
|
29
|
-
const
|
|
32
|
+
const args = [resolveEntry(), 'start', '--dir', target, '--auto-approve'];
|
|
33
|
+
if (nameLabel)
|
|
34
|
+
args.push('--name', `${nameLabel} · ${path.basename(target)}`);
|
|
35
|
+
const child = spawn(process.execPath, args, {
|
|
30
36
|
cwd: target,
|
|
31
37
|
env: process.env,
|
|
32
38
|
// 부모 세션을 Ctrl+C 로 껐을 때 폰에서 따로 만든 세션까지 같이 죽지 않도록 독립 프로세스로 띄움
|