shellbase 0.1.2 → 0.1.4
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 +7 -0
- package/dist/auth.js +3 -21
- package/dist/config.js +0 -1
- package/dist/devices.js +18 -23
- package/dist/session.js +27 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,13 @@ shellbase start --dir ~/projects/my-app --name "내 노트북"
|
|
|
38
38
|
`--auto-approve` 를 쓰면 폰에서 접속할 때마다 물어보지 않고 바로 승인해요. 편하지만, 내 계정에
|
|
39
39
|
로그인할 수 있는 사람은 누구나 바로 입력할 수 있게 되니 신뢰할 수 있는 환경에서만 쓰세요.
|
|
40
40
|
|
|
41
|
+
### 같은 컴퓨터에서 여러 개 동시에 켜기
|
|
42
|
+
|
|
43
|
+
터미널을 새로 열어서 다른 폴더로 `shellbase start` 를 한 번 더 실행하면, 별개의 세션으로 등록돼서
|
|
44
|
+
폰 목록에 폴더별로 따로 떠요(예: `내PC · project-a`, `내PC · project-b`). 프로젝트마다 세션을 하나씩
|
|
45
|
+
켜두고 폰에서 왔다갔다 접속할 수 있어요. `Ctrl+C` 로 정상 종료하면 그 세션은 목록에서 자동으로
|
|
46
|
+
지워지고, 강제 종료 등으로 남아있는 세션은 폰 목록에서 휴지통 아이콘으로 직접 지울 수 있어요.
|
|
47
|
+
|
|
41
48
|
## 참고
|
|
42
49
|
|
|
43
50
|
- `node-pty` 를 사용해서 설치 시 네이티브 모듈을 컴파일합니다. macOS는 Xcode Command Line Tools,
|
package/dist/auth.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
2
|
import { ConnectBase } from 'connectbase-client';
|
|
5
|
-
import { APP_ID, PUBLIC_KEY, CONFIG_DIR, CREDENTIALS_PATH
|
|
6
|
-
import { generateFrameKey } from './crypto.js';
|
|
3
|
+
import { APP_ID, PUBLIC_KEY, CONFIG_DIR, CREDENTIALS_PATH } from './config.js';
|
|
7
4
|
import { loginWithGoogle } from './google-auth.js';
|
|
8
5
|
function ensureConfigDir() {
|
|
9
6
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
@@ -20,23 +17,6 @@ function loadCredentials() {
|
|
|
20
17
|
return null;
|
|
21
18
|
}
|
|
22
19
|
}
|
|
23
|
-
export function loadOrCreateDevice() {
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(fs.readFileSync(DEVICE_PATH, 'utf8'));
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// realtime userId 는 UUID 형태를 "인증된 멤버 전용"으로 취급해서 거부하므로,
|
|
29
|
-
// 기기 식별자는 접두사를 붙여 UUID 로 보이지 않게 만든다.
|
|
30
|
-
const identity = {
|
|
31
|
-
deviceId: `desktop-${crypto.randomUUID()}`,
|
|
32
|
-
deviceName: os.hostname(),
|
|
33
|
-
frameKey: generateFrameKey(),
|
|
34
|
-
};
|
|
35
|
-
ensureConfigDir();
|
|
36
|
-
fs.writeFileSync(DEVICE_PATH, JSON.stringify(identity, null, 2), { mode: 0o600 });
|
|
37
|
-
return identity;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
20
|
function createClient(onTokens) {
|
|
41
21
|
return new ConnectBase({
|
|
42
22
|
appId: APP_ID,
|
|
@@ -74,6 +54,8 @@ export async function whoami() {
|
|
|
74
54
|
const { cb, memberId } = await requireClient();
|
|
75
55
|
const me = await cb.auth.getMe();
|
|
76
56
|
console.log(`${me.nickname}${me.email ? ` <${me.email}>` : ''} (member_id: ${memberId})`);
|
|
57
|
+
const autoApprove = me.custom_data?.auto_approve === true;
|
|
58
|
+
console.log(`계정 자동 승인 설정: ${autoApprove ? '켜짐 (웹에서 껐다켤 수 있어요)' : '꺼짐'}`);
|
|
77
59
|
}
|
|
78
60
|
export async function requireClient() {
|
|
79
61
|
const creds = loadCredentials();
|
package/dist/config.js
CHANGED
|
@@ -11,4 +11,3 @@ export const CLI_AUTH_SESSIONS_TABLE_ID = '01a00aa0-802d-7730-88ba-b831b7405188'
|
|
|
11
11
|
export const WEB_LOGIN_URL = 'https://shellbase.web.connectbase.world/login';
|
|
12
12
|
export const CONFIG_DIR = path.join(os.homedir(), '.shellbase');
|
|
13
13
|
export const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');
|
|
14
|
-
export const DEVICE_PATH = path.join(CONFIG_DIR, 'device.json');
|
package/dist/devices.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import { DEVICES_TABLE_ID } from './config.js';
|
|
2
2
|
export async function registerDevice(cb, params) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// deviceId는 `shellbase start` 를 실행할 때마다 새로 발급되므로(세션 단위 식별자),
|
|
4
|
+
// 기존 row 를 찾아 덮어쓸 필요 없이 매번 새 row 를 만든다.
|
|
5
|
+
const created = await cb.database.createData(DEVICES_TABLE_ID, {
|
|
6
|
+
data: {
|
|
7
|
+
owner_id: params.ownerId,
|
|
8
|
+
device_id: params.deviceId,
|
|
9
|
+
device_name: params.deviceName,
|
|
10
|
+
status: 'online',
|
|
11
|
+
cwd: params.cwd,
|
|
12
|
+
frame_key: params.frameKey,
|
|
13
|
+
favorite_dirs: params.favoriteDirs,
|
|
14
|
+
last_seen_at: new Date().toISOString(),
|
|
15
|
+
},
|
|
6
16
|
});
|
|
7
|
-
const payload = {
|
|
8
|
-
owner_id: params.ownerId,
|
|
9
|
-
device_id: params.deviceId,
|
|
10
|
-
device_name: params.deviceName,
|
|
11
|
-
status: 'online',
|
|
12
|
-
cwd: params.cwd,
|
|
13
|
-
frame_key: params.frameKey,
|
|
14
|
-
favorite_dirs: params.favoriteDirs,
|
|
15
|
-
last_seen_at: new Date().toISOString(),
|
|
16
|
-
};
|
|
17
|
-
const existing = rows[0];
|
|
18
|
-
if (existing) {
|
|
19
|
-
await cb.database.updateData(DEVICES_TABLE_ID, existing.id, { data: payload });
|
|
20
|
-
return existing.id;
|
|
21
|
-
}
|
|
22
|
-
const created = await cb.database.createData(DEVICES_TABLE_ID, { data: payload });
|
|
23
17
|
return created.id;
|
|
24
18
|
}
|
|
25
19
|
export async function heartbeatDevice(cb, rowId) {
|
|
@@ -27,8 +21,9 @@ export async function heartbeatDevice(cb, rowId) {
|
|
|
27
21
|
data: { status: 'online', last_seen_at: new Date().toISOString() },
|
|
28
22
|
});
|
|
29
23
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
// 세션이 정상 종료되면(Ctrl+C, 셸 종료 등) 목록에서 완전히 지워서 안 쓰는 항목이 쌓이지 않게 한다.
|
|
25
|
+
// (예전엔 컴퓨터당 기기 하나였어서 "오프라인" 표시만 하고 남겨뒀지만, 이제 세션 하나당 새 row 라서
|
|
26
|
+
// 끝난 세션을 그대로 남기면 목록이 계속 늘어나기만 함.)
|
|
27
|
+
export async function unregisterDevice(cb, rowId) {
|
|
28
|
+
await cb.database.deleteData(DEVICES_TABLE_ID, rowId);
|
|
34
29
|
}
|
package/dist/session.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
2
4
|
import * as pty from 'node-pty';
|
|
3
|
-
import { requireClient
|
|
5
|
+
import { requireClient } from './auth.js';
|
|
4
6
|
import { TERMINAL_CATEGORY } from './config.js';
|
|
5
7
|
import { startSleepGuard, stopSleepGuard } from './sleep-guard.js';
|
|
6
|
-
import { encryptFrame, decryptFrame } from './crypto.js';
|
|
7
|
-
import { registerDevice, heartbeatDevice,
|
|
8
|
+
import { encryptFrame, decryptFrame, generateFrameKey } from './crypto.js';
|
|
9
|
+
import { registerDevice, heartbeatDevice, unregisterDevice } from './devices.js';
|
|
8
10
|
import { promptApproval } from './prompt.js';
|
|
9
11
|
import { touchRecentDir } from './recent-dirs.js';
|
|
10
12
|
const HEARTBEAT_MS = 20_000;
|
|
@@ -17,12 +19,27 @@ function resolveShell() {
|
|
|
17
19
|
}
|
|
18
20
|
export async function startSession(options) {
|
|
19
21
|
const { cb, memberId } = await requireClient();
|
|
20
|
-
const device = loadOrCreateDevice();
|
|
21
|
-
const deviceName = options.name ?? device.deviceName;
|
|
22
22
|
const cwd = options.dir ? path.resolve(options.dir) : process.cwd();
|
|
23
|
+
// 폴더 이름을 기본 이름에 넣어서, 같은 컴퓨터에서 여러 개 띄워도 폰 목록에서 구분되게 한다.
|
|
24
|
+
const defaultName = cwd === os.homedir() ? os.hostname() : `${os.hostname()} · ${path.basename(cwd)}`;
|
|
25
|
+
const deviceName = options.name ?? defaultName;
|
|
26
|
+
// realtime userId 는 UUID 형태를 "인증된 멤버 전용"으로 취급해서 거부하므로, 세션 식별자는
|
|
27
|
+
// 접두사를 붙여 UUID 로 보이지 않게 만든다. `shellbase start` 를 실행할 때마다 새로 발급 —
|
|
28
|
+
// 예전엔 컴퓨터당 하나로 고정돼있어서, 같은 컴퓨터에서 두 개를 동시에 띄우면 프레임이 서로
|
|
29
|
+
// 섞였음(둘 다 같은 deviceId 를 썼기 때문). 이제 세션(프로세스)마다 독립된 식별자를 가짐.
|
|
30
|
+
const device = {
|
|
31
|
+
deviceId: `desktop-${crypto.randomUUID()}`,
|
|
32
|
+
deviceName,
|
|
33
|
+
frameKey: generateFrameKey(),
|
|
34
|
+
};
|
|
35
|
+
// 계정 설정(웹의 "새 세션 자동 승인" 토글, custom_data.auto_approve)이 켜져 있으면
|
|
36
|
+
// --auto-approve 를 안 줘도 자동 승인. 계정 단위라 이 계정으로 로그인한 모든 컴퓨터에 적용됨.
|
|
37
|
+
const me = await cb.auth.getMe();
|
|
38
|
+
const accountAutoApprove = me.custom_data?.auto_approve === true;
|
|
39
|
+
const autoApprove = options.autoApprove === true || accountAutoApprove;
|
|
23
40
|
console.log(`"${deviceName}" 세션을 시작해요 (작업 폴더: ${cwd})`);
|
|
24
|
-
if (
|
|
25
|
-
console.log('
|
|
41
|
+
if (autoApprove) {
|
|
42
|
+
console.log(`⚠️ 자동 승인이 켜져 있어요${accountAutoApprove ? ' (계정 설정)' : ' (--auto-approve)'} — 접속 요청을 묻지 않고 자동 승인해요. 내 계정에 들어올 수 있는 사람은 누구나 곧바로 입력할 수 있어요.`);
|
|
26
43
|
}
|
|
27
44
|
const deviceRowId = await registerDevice(cb, {
|
|
28
45
|
ownerId: memberId,
|
|
@@ -92,7 +109,7 @@ export async function startSession(options) {
|
|
|
92
109
|
if (approvalInFlight)
|
|
93
110
|
return;
|
|
94
111
|
approvalInFlight = true;
|
|
95
|
-
const approval =
|
|
112
|
+
const approval = autoApprove
|
|
96
113
|
? Promise.resolve(true)
|
|
97
114
|
: promptApproval('\n📱 모바일에서 접속을 요청했어요. 입력을 허용할까요? (y/N, 30초 내 미응답 시 자동 거부): ', APPROVAL_TIMEOUT_MS);
|
|
98
115
|
approval
|
|
@@ -136,10 +153,10 @@ export async function startSession(options) {
|
|
|
136
153
|
// 이미 끊겼으면 무시
|
|
137
154
|
}
|
|
138
155
|
try {
|
|
139
|
-
await
|
|
156
|
+
await unregisterDevice(cb, deviceRowId);
|
|
140
157
|
}
|
|
141
158
|
catch {
|
|
142
|
-
//
|
|
159
|
+
// 삭제 실패해도 종료는 계속 진행 — 폰 목록에 남아있으면 나중에 수동으로 지울 수 있음
|
|
143
160
|
}
|
|
144
161
|
cb.realtime.disconnect();
|
|
145
162
|
ptyProcess.kill();
|