@online5880/opensession 0.1.0 → 0.1.1

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 CHANGED
@@ -4,18 +4,20 @@ MVP CLI for session continuity with Supabase.
4
4
 
5
5
  ## Commands
6
6
 
7
- - `init --url --anon-key [--project-key] [--actor]`
7
+ - `init [--project-key] [--actor]` (프롬프트로 URL/ANON KEY 입력)
8
8
  - `login --actor`
9
9
  - `start --project-key [--project-name] [--actor]`
10
10
  - `resume --session-id [--actor]`
11
11
  - `status [--project-key]`
12
+ - `sync --project <name>`
12
13
  - `log [--session-id] [--limit]`
13
14
 
14
15
  ## Quick start
15
16
 
16
17
  ```bash
17
18
  npm install
18
- node src/cli.js init --url "$SUPABASE_URL" --anon-key "$SUPABASE_ANON_KEY" --project-key demo --actor mane
19
+ node src/cli.js init --project-key demo --actor mane
20
+ node src/cli.js sync --project demo
19
21
  node src/cli.js start --project-key demo
20
22
  node src/cli.js status
21
23
  node src/cli.js log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@online5880/opensession",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Session continuity bridge CLI with Supabase backend",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { stdin as input, stdout as output } from 'node:process';
6
+ import { readFile } from 'node:fs/promises';
4
7
  import {
5
8
  appendEvent,
6
9
  ensureProject,
@@ -8,12 +11,55 @@ import {
8
11
  getSession,
9
12
  getSessionEvents,
10
13
  listActiveSessions,
11
- startSession
14
+ startSession,
15
+ validateConnection
12
16
  } from './supabase.js';
13
17
  import { getConfigPath, mergeConfig, readConfig, writeConfig } from './config.js';
14
18
 
15
19
  const program = new Command();
16
20
 
21
+ function formatError(error) {
22
+ if (error instanceof Error) {
23
+ return error.message;
24
+ }
25
+ if (error && typeof error === 'object') {
26
+ const message = error.message;
27
+ const details = error.details;
28
+ const hint = error.hint;
29
+ const code = error.code;
30
+ const parts = [message, details, hint, code].filter(Boolean);
31
+ if (parts.length > 0) {
32
+ return parts.join(' | ');
33
+ }
34
+ try {
35
+ return JSON.stringify(error);
36
+ } catch {
37
+ return 'Unknown object error';
38
+ }
39
+ }
40
+ return String(error);
41
+ }
42
+
43
+ async function readInitSecrets() {
44
+ if (input.isTTY) {
45
+ const rl = createInterface({ input, output });
46
+ const url = (await rl.question('Supabase URL을 입력하세요: ')).trim();
47
+ const anonKey = (await rl.question('Supabase ANON KEY를 입력하세요: ')).trim();
48
+ rl.close();
49
+ return { url, anonKey };
50
+ }
51
+
52
+ const raw = await readFile('/dev/stdin', 'utf8');
53
+ const lines = raw
54
+ .split(/\r?\n/)
55
+ .map((line) => line.trim())
56
+ .filter((line) => line.length > 0);
57
+ return {
58
+ url: lines[0] ?? '',
59
+ anonKey: lines[1] ?? ''
60
+ };
61
+ }
62
+
17
63
  program
18
64
  .name('opensession')
19
65
  .description('Session continuity bridge CLI for Supabase')
@@ -21,22 +67,34 @@ program
21
67
 
22
68
  program
23
69
  .command('init')
24
- .description('Initialize CLI config')
25
- .requiredOption('--url <url>', 'Supabase project URL')
26
- .requiredOption('--anon-key <anonKey>', 'Supabase anon key')
70
+ .description('Initialize CLI config with interactive prompt')
27
71
  .option('--project-key <projectKey>', 'Default project key')
28
72
  .option('--actor <actor>', 'Default actor/username')
29
73
  .action(async (options) => {
74
+ const { url, anonKey } = await readInitSecrets();
75
+
76
+ if (!url || !anonKey) {
77
+ throw new Error('Supabase URL과 ANON KEY는 필수입니다.');
78
+ }
79
+
30
80
  const current = await readConfig();
31
81
  const next = mergeConfig(current, {
32
- supabaseUrl: options.url,
33
- supabaseAnonKey: options.anonKey,
82
+ supabaseUrl: url,
83
+ supabaseAnonKey: anonKey,
34
84
  defaultProjectKey: options.projectKey ?? current.defaultProjectKey,
35
85
  actor: options.actor ?? current.actor
36
86
  });
37
87
 
38
88
  const path = await writeConfig(next);
39
- console.log(`Config saved: ${path}`);
89
+ console.log(`설정 저장 완료: ${path}`);
90
+ try {
91
+ await validateConnection(url, anonKey);
92
+ console.log('연결 검증: 성공');
93
+ } catch (error) {
94
+ const message = formatError(error);
95
+ console.log(`연결 검증: 실패 (${message})`);
96
+ process.exitCode = 1;
97
+ }
40
98
  });
41
99
 
42
100
  program
@@ -100,22 +158,35 @@ program
100
158
 
101
159
  program
102
160
  .command('status')
103
- .description('Show active sessions for a project')
161
+ .description('Show active sessions and latest sync status')
104
162
  .option('--project-key <projectKey>', 'Project key (defaults to configured project key)')
105
163
  .action(async (options) => {
106
164
  const config = await readConfig();
107
- const client = getClient(config);
108
- const projectKey = options.projectKey ?? config.defaultProjectKey;
165
+ const projectKey = options.projectKey ?? config.defaultProjectKey ?? config.syncStatus?.project;
166
+ const syncStatus = config.syncStatus ?? {};
109
167
 
110
168
  if (!projectKey) {
111
- throw new Error('Missing project key. Pass --project-key or run start first.');
169
+ throw new Error('Missing project key. Pass --project-key, sync --project, or run start first.');
112
170
  }
113
171
 
172
+ console.log(`Project: ${projectKey}`);
173
+ console.log(`Last sync: ${syncStatus.lastSyncAt ?? '-'}`);
174
+ console.log(`Sync project: ${syncStatus.project ?? '-'}`);
175
+ console.log(`Pending events: ${syncStatus.pendingEvents ?? 0}`);
176
+ console.log(`Recent sync error: ${syncStatus.lastError ?? '-'}`);
177
+
178
+ let client;
179
+ try {
180
+ client = getClient(config);
181
+ } catch (error) {
182
+ const message = formatError(error);
183
+ console.log(`원격 상태 확인 불가: ${message}`);
184
+ process.exitCode = 1;
185
+ return;
186
+ }
114
187
  const project = await ensureProject(client, projectKey, projectKey);
115
188
  const active = await listActiveSessions(client, project.id);
116
189
 
117
- console.log(`Project: ${project.project_key}`);
118
-
119
190
  if (active.length === 0) {
120
191
  console.log('No active sessions');
121
192
  return;
@@ -126,6 +197,47 @@ program
126
197
  }
127
198
  });
128
199
 
200
+ program
201
+ .command('sync')
202
+ .description('Sync local/remote session state for a project')
203
+ .requiredOption('--project <projectKey>', 'Project key')
204
+ .action(async (options) => {
205
+ const config = await readConfig();
206
+ const now = new Date().toISOString();
207
+
208
+ try {
209
+ const client = getClient(config);
210
+ const project = await ensureProject(client, options.project, options.project);
211
+ const active = await listActiveSessions(client, project.id);
212
+ const next = mergeConfig(config, {
213
+ defaultProjectKey: options.project,
214
+ syncStatus: {
215
+ lastSyncAt: now,
216
+ project: options.project,
217
+ pendingEvents: 0,
218
+ lastError: null
219
+ }
220
+ });
221
+ await writeConfig(next);
222
+ console.log(`동기화 완료: project=${options.project}`);
223
+ console.log(`active sessions=${active.length}`);
224
+ console.log(`pending events=0`);
225
+ } catch (error) {
226
+ const message = formatError(error);
227
+ const next = mergeConfig(config, {
228
+ syncStatus: {
229
+ lastSyncAt: now,
230
+ project: options.project,
231
+ pendingEvents: 0,
232
+ lastError: message
233
+ }
234
+ });
235
+ await writeConfig(next);
236
+ console.log(`동기화 실패: ${message}`);
237
+ process.exitCode = 1;
238
+ }
239
+ });
240
+
129
241
  program
130
242
  .command('log')
131
243
  .description('Show session event log')
@@ -163,7 +275,7 @@ async function main() {
163
275
  try {
164
276
  await program.parseAsync(process.argv);
165
277
  } catch (error) {
166
- const message = error instanceof Error ? error.message : String(error);
278
+ const message = formatError(error);
167
279
  console.error(message);
168
280
  process.exitCode = 1;
169
281
  }
package/src/supabase.js CHANGED
@@ -2,7 +2,7 @@ import { createClient } from '@supabase/supabase-js';
2
2
 
3
3
  export function getClient(config) {
4
4
  if (!config.supabaseUrl || !config.supabaseAnonKey) {
5
- throw new Error('Supabase is not configured. Run `opensession init --url <url> --anon-key <key>`.');
5
+ throw new Error('Supabase is not configured. Run `opensession init` first.');
6
6
  }
7
7
 
8
8
  return createClient(config.supabaseUrl, config.supabaseAnonKey, {
@@ -74,6 +74,16 @@ export async function appendEvent(client, sessionId, type, payload = {}) {
74
74
  return data;
75
75
  }
76
76
 
77
+ export async function validateConnection(supabaseUrl, supabaseAnonKey) {
78
+ const client = createClient(supabaseUrl, supabaseAnonKey, {
79
+ auth: { persistSession: false }
80
+ });
81
+ const { error } = await client.from('projects').select('id').limit(1);
82
+ if (error) {
83
+ throw error;
84
+ }
85
+ }
86
+
77
87
  export async function getSession(client, sessionId) {
78
88
  const { data, error } = await client
79
89
  .from('sessions')