spck 0.3.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.
Files changed (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. package/vitest.config.ts +19 -0
package/src/index.ts ADDED
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Spck Networking CLI - Proxy Mode Entry Point
3
+ * Connects to proxy server for remote filesystem, git, and terminal access
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import yargs from 'yargs';
8
+ import { hideBin } from 'yargs/helpers';
9
+ import jwt from 'jsonwebtoken';
10
+ import { loadConfig, ConfigNotFoundError } from './config/config.js';
11
+ import {
12
+ loadCredentials,
13
+ loadConnectionSettings,
14
+ isServerTokenExpired,
15
+ clearCredentials,
16
+ clearConnectionSettings,
17
+ getCredentialsPath,
18
+ getConnectionSettingsPath,
19
+ loadServerPreference,
20
+ saveServerPreference,
21
+ } from './config/credentials.js';
22
+ import { fetchServerList, selectBestServer, displayServerPings, getDefaultServerList } from './config/server-selection.js';
23
+ import { authenticateWithFirebase, getValidFirebaseToken, abortCurrentAuth } from './connection/firebase-auth.js';
24
+ import { runSetup } from './setup/wizard.js';
25
+ import { detectTools, displayFeatureSummary } from './utils/tool-detection.js';
26
+ import { ensureProjectDir } from './utils/project-dir.js';
27
+ import { ProxyClient } from './proxy/ProxyClient.js';
28
+ import { RPCRouter } from './rpc/router.js';
29
+ import { ServerConfig, FirebaseCredentials, StoredCredentials } from './types.js';
30
+ import { t, detectLocale, setLocale } from './i18n/index.js';
31
+
32
+ let proxyClient: ProxyClient | null = null;
33
+
34
+ /**
35
+ * Start the proxy client
36
+ */
37
+ export async function startProxyClient(
38
+ configPath?: string,
39
+ options?: {
40
+ disableGit?: boolean;
41
+ disableRipgrep?: boolean;
42
+ serverOverride?: string;
43
+ }
44
+ ): Promise<void> {
45
+ console.log('\n' + '='.repeat(60));
46
+ console.log(' ' + t('app.title'));
47
+ console.log('='.repeat(60) + '\n');
48
+
49
+ try {
50
+ // Step 0: Ensure project directory is set up (creates symlink)
51
+ ensureProjectDir(process.cwd());
52
+
53
+ // Step 1: Load or create configuration
54
+ let config: ServerConfig;
55
+
56
+ try {
57
+ config = loadConfig(configPath);
58
+ console.log('āœ… ' + t('config.loaded') + '\n');
59
+ } catch (error: any) {
60
+ if (error instanceof ConfigNotFoundError) {
61
+ // Run setup wizard for missing config
62
+ console.log(t('config.notFound') + '\n');
63
+ config = await runSetup(configPath);
64
+ } else if (error.code === 'CORRUPTED' || error instanceof SyntaxError) {
65
+ // Config file is corrupted - trigger setup wizard
66
+ console.warn('āš ļø ' + t('config.corrupted'));
67
+ console.warn(' ' + t('config.corruptedRunSetup') + '\n');
68
+ config = await runSetup(configPath);
69
+ } else {
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ // Step 2: Authenticate with Firebase
75
+ let storedCredentials: StoredCredentials | null = null;
76
+ let credentials: FirebaseCredentials;
77
+
78
+ try {
79
+ storedCredentials = loadCredentials();
80
+ } catch (error: any) {
81
+ if (error.code === 'CORRUPTED') {
82
+ // Credentials file is corrupted - trigger re-authentication
83
+ storedCredentials = null; // Will trigger re-auth below
84
+ } else {
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ if (!storedCredentials) {
90
+ // No stored credentials - full authentication required
91
+ credentials = await authenticateWithFirebase();
92
+ } else {
93
+ // Have stored credentials - generate fresh ID token using refresh token
94
+ credentials = await getValidFirebaseToken(storedCredentials);
95
+ console.log('āœ… ' + t('auth.credentialsLoaded'));
96
+ console.log(` ${t('auth.userId', { userId: credentials.userId })}\n`);
97
+ }
98
+
99
+ // Step 3: Validate root directory
100
+ const fs = await import('fs');
101
+ if (!fs.existsSync(config.root)) {
102
+ console.error(`\nāŒ ${t('errors.rootNotFound', { path: config.root })}\n`);
103
+ console.error(t('errors.rootNotFoundHint'));
104
+ console.error(' spck --setup\n');
105
+ process.exit(1);
106
+ }
107
+
108
+ // Step 4: Detect tools
109
+ const tools = await detectTools({
110
+ disableGit: options?.disableGit,
111
+ disableRipgrep: options?.disableRipgrep,
112
+ });
113
+
114
+ // Step 5: Initialize RPC Router
115
+ RPCRouter.initialize(config.root, config, tools);
116
+
117
+ // Step 6: Check connection settings
118
+ let connectionSettings = null;
119
+ let needsReconnect = false;
120
+
121
+ try {
122
+ connectionSettings = loadConnectionSettings();
123
+ } catch (error: any) {
124
+ if (error.code === 'CORRUPTED') {
125
+ // Connection settings corrupted - will reconnect with Firebase credentials
126
+ console.warn('āš ļø ' + t('connection.settingsCorrupted') + '\n');
127
+ connectionSettings = null;
128
+ needsReconnect = true;
129
+ } else {
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ if (!connectionSettings) {
135
+ if (!needsReconnect) {
136
+ console.log(t('connection.noExisting') + '\n');
137
+ }
138
+ needsReconnect = true;
139
+ } else if (isServerTokenExpired(connectionSettings)) {
140
+ console.log('āš ļø ' + t('connection.tokenExpired') + '\n');
141
+ needsReconnect = true;
142
+ } else {
143
+ console.log('āœ… ' + t('connection.existingFound'));
144
+ console.log(` ${t('connection.connectedAt', { date: new Date(connectionSettings.connectedAt).toLocaleString() })}\n`);
145
+ }
146
+
147
+ // Step 7: Display feature summary
148
+ displayFeatureSummary(tools, config.terminal.enabled, config.security.userAuthenticationEnabled, config.browserProxy?.enabled ?? true);
149
+
150
+ // Step 8: Select relay server
151
+ let proxyServerUrl: string;
152
+
153
+ if (options?.serverOverride) {
154
+ // CLI --server flag overrides everything
155
+ proxyServerUrl = options.serverOverride;
156
+ saveServerPreference(proxyServerUrl);
157
+ console.log(`āœ… ${t('server.usingOverride', { url: proxyServerUrl })}\n`);
158
+ } else {
159
+ // Check saved preference
160
+ const savedServer = loadServerPreference();
161
+ if (savedServer) {
162
+ proxyServerUrl = savedServer;
163
+ console.log(`āœ… ${t('server.usingSaved', { url: proxyServerUrl })}\n`);
164
+ } else {
165
+ // Auto-select best server by ping
166
+ try {
167
+ console.log('🌐 ' + t('server.selectingBest'));
168
+ const servers = await fetchServerList();
169
+ await displayServerPings(servers);
170
+ const best = await selectBestServer(servers);
171
+ if (best.ping !== Infinity) {
172
+ proxyServerUrl = best.server.url;
173
+ saveServerPreference(proxyServerUrl);
174
+ const label = best.server.label.en || best.server.url;
175
+ console.log(`āœ… ${t('server.selected', { label, url: proxyServerUrl, ping: best.ping })}\n`);
176
+ } else {
177
+ // All servers unreachable — use first server from hardcoded list
178
+ proxyServerUrl = getDefaultServerList()[0].url;
179
+ console.warn(`āš ļø ${t('server.allUnreachable', { url: proxyServerUrl })}\n`);
180
+ }
181
+ } catch (error: any) {
182
+ // Fetch/ping failed — use first server from hardcoded list
183
+ proxyServerUrl = getDefaultServerList()[0].url;
184
+ console.warn(`āš ļø ${t('server.failedSelect', { message: error.message })}`);
185
+ console.warn(` ${t('server.usingDefault', { url: proxyServerUrl })}\n`);
186
+ }
187
+ }
188
+ }
189
+
190
+ // Step 9: Create and connect ProxyClient
191
+ proxyClient = new ProxyClient({
192
+ config,
193
+ firebaseToken: credentials.firebaseToken,
194
+ userId: credentials.userId,
195
+ tools,
196
+ existingConnectionSettings: connectionSettings || undefined,
197
+ proxyServerUrl,
198
+ });
199
+
200
+ await proxyClient.connect();
201
+
202
+ } catch (error: any) {
203
+ // Handle specific error cases with helpful messages
204
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
205
+ // Permission error
206
+ console.error('\nāŒ ' + t('errors.permissionError') + '\n');
207
+ console.error(`${t('errors.permissionPath', { path: error.path || 'unknown' })}`);
208
+ console.error(`${t('errors.permissionOperation', { operation: error.operation || 'file operation' })}\n`);
209
+ console.error(t('errors.permissionFix'));
210
+ console.error(' ' + t('errors.permissionFixCmd1'));
211
+ console.error(' ' + t('errors.permissionFixCmd2') + '\n');
212
+ console.error(t('errors.permissionFixHint') + '\n');
213
+ process.exit(1);
214
+ } else if (error.code === 'ENOSPC') {
215
+ // Disk full error
216
+ console.error('\nāŒ ' + t('errors.diskFull') + '\n');
217
+ console.error(`${t('errors.permissionPath', { path: error.path || 'unknown' })}`);
218
+ console.error(`${t('errors.permissionOperation', { operation: error.operation || 'file operation' })}\n`);
219
+ console.error(t('errors.diskFullHint') + '\n');
220
+ process.exit(1);
221
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
222
+ // Network/proxy connection error
223
+ console.error('\nāŒ ' + t('errors.cannotConnect') + '\n');
224
+ console.error(`${t('errors.cannotConnectError', { message: error.message })}\n`);
225
+ console.error(t('errors.cannotConnectCauses'));
226
+ console.error(' ' + t('errors.cannotConnectCause1'));
227
+ console.error(' ' + t('errors.cannotConnectCause2'));
228
+ console.error(' ' + t('errors.cannotConnectCause3') + '\n');
229
+ console.error(t('errors.cannotConnectHint') + '\n');
230
+ process.exit(1);
231
+ } else {
232
+ // Generic error
233
+ console.error('\nāŒ ' + t('errors.failedToStart', { message: error.message }));
234
+
235
+ if (error.stack) {
236
+ console.error('\nStack trace:');
237
+ console.error(error.stack);
238
+ }
239
+
240
+ process.exit(1);
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Logout - clear credentials and connection settings
247
+ */
248
+ export async function logout(): Promise<void> {
249
+ console.log('\n=== ' + t('logout.title') + ' ===\n');
250
+
251
+ let clearedSomething = false;
252
+
253
+ // Clear user credentials
254
+ const credentialsPath = getCredentialsPath();
255
+ if (fs.existsSync(credentialsPath)) {
256
+ clearCredentials();
257
+ console.log('āœ… ' + t('logout.clearedCredentials'));
258
+ console.log(` ${t('logout.removed', { path: credentialsPath })}`);
259
+ clearedSomething = true;
260
+ }
261
+
262
+ // Clear connection settings
263
+ const settingsPath = getConnectionSettingsPath();
264
+ if (fs.existsSync(settingsPath)) {
265
+ clearConnectionSettings();
266
+ console.log('āœ… ' + t('logout.clearedSettings'));
267
+ console.log(` ${t('logout.removed', { path: settingsPath })}`);
268
+ clearedSomething = true;
269
+ }
270
+
271
+ if (!clearedSomething) {
272
+ console.log('ā„¹ļø ' + t('logout.noCredentials'));
273
+ console.log(' ' + t('logout.notLoggedIn') + '\n');
274
+ } else {
275
+ console.log('\n✨ ' + t('logout.success') + '\n');
276
+ console.log(t('logout.runAgain') + '\n');
277
+ }
278
+
279
+ process.exit(0);
280
+ }
281
+
282
+ /**
283
+ * Show account information - email and subscription status
284
+ */
285
+ export async function showAccountInfo(): Promise<void> {
286
+ console.log('\n' + '='.repeat(60));
287
+ console.log(' ' + t('account.title'));
288
+ console.log('='.repeat(60) + '\n');
289
+
290
+ try {
291
+ // Load stored credentials
292
+ let storedCredentials: StoredCredentials | null = null;
293
+ try {
294
+ storedCredentials = loadCredentials();
295
+ } catch (error: any) {
296
+ if (error.code === 'CORRUPTED') {
297
+ console.error('āŒ ' + t('account.credentialsCorrupted'));
298
+ console.error(' ' + t('account.credentialsCorruptedHint1'));
299
+ console.error(' ' + t('account.credentialsCorruptedHint2') + '\n');
300
+ process.exit(1);
301
+ }
302
+ throw error;
303
+ }
304
+
305
+ if (!storedCredentials) {
306
+ console.log('ā„¹ļø ' + t('account.notLoggedIn'));
307
+ console.log(' ' + t('account.notLoggedInHint1'));
308
+ console.log(' ' + t('account.notLoggedInHint2') + '\n');
309
+ process.exit(0);
310
+ }
311
+
312
+ // Get fresh Firebase token
313
+ console.log('šŸ”„ ' + t('account.fetching') + '\n');
314
+ const credentials = await getValidFirebaseToken(storedCredentials);
315
+
316
+ // Decode JWT to extract user information
317
+ const decoded: any = jwt.decode(credentials.firebaseToken);
318
+
319
+ if (!decoded) {
320
+ console.error('āŒ ' + t('account.decodeFailed') + '\n');
321
+ process.exit(1);
322
+ }
323
+
324
+ console.log('āœ… ' + t('account.loggedIn') + '\n');
325
+ console.log(` ${t('account.userId', { userId: credentials.userId })}`);
326
+
327
+ // Extract email from JWT claims if available
328
+ if (decoded.email) {
329
+ console.log(` ${t('account.email', { email: decoded.email })}`);
330
+ if (decoded.email_verified !== undefined) {
331
+ console.log(` ${t('account.verified', { status: decoded.email_verified ? t('account.yes') : t('account.no') })}`);
332
+ }
333
+ }
334
+
335
+ // Show token expiry
336
+ if (decoded.exp) {
337
+ const expiryDate = new Date(decoded.exp * 1000);
338
+ const now = new Date();
339
+ const timeLeft = expiryDate.getTime() - now.getTime();
340
+ const hoursLeft = Math.floor(timeLeft / (1000 * 60 * 60));
341
+ const minutesLeft = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
342
+
343
+ console.log(`\n ${t('account.tokenExpires', { date: expiryDate.toLocaleString() })}`);
344
+ if (timeLeft > 0) {
345
+ console.log(` ${t('account.timeRemaining', { hours: hoursLeft, minutes: minutesLeft })}`);
346
+ }
347
+ }
348
+
349
+ // Check for subscription information in JWT claims
350
+ if (decoded.subscription || decoded.premium || decoded.plan) {
351
+ console.log('\nšŸ“‹ ' + t('account.subscription'));
352
+ if (decoded.subscription) {
353
+ console.log(` ${t('account.status', { status: decoded.subscription })}`);
354
+ }
355
+ if (decoded.plan) {
356
+ console.log(` ${t('account.plan', { plan: decoded.plan })}`);
357
+ }
358
+ if (decoded.premium !== undefined) {
359
+ console.log(` ${t('account.premium', { status: decoded.premium ? t('account.yes') : t('account.no') })}`);
360
+ }
361
+ }
362
+
363
+ console.log('\n' + '='.repeat(60) + '\n');
364
+
365
+ process.exit(0);
366
+
367
+ } catch (error: any) {
368
+ console.error('\nāŒ ' + t('account.fetchFailed') + '\n');
369
+ console.error(` ${t('account.fetchFailedError', { message: error.message })}\n`);
370
+
371
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
372
+ console.error(' ' + t('account.permissionDenied'));
373
+ console.error(' ' + t('account.permissionHint1') + '\n');
374
+ console.error(' ' + t('account.permissionHint2') + '\n');
375
+ } else if (error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
376
+ console.error(' ' + t('account.networkError'));
377
+ console.error(' ' + t('account.networkHint') + '\n');
378
+ }
379
+
380
+ process.exit(1);
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Setup graceful shutdown
386
+ */
387
+ function setupGracefulShutdown(): void {
388
+ const shutdown = async (signal: string) => {
389
+ console.log(`\n\n${t('setup.received', { signal })}`);
390
+
391
+ // Abort any pending authentication (cancels in-flight fetch + closes callback server)
392
+ abortCurrentAuth();
393
+
394
+ if (proxyClient) {
395
+ try {
396
+ await proxyClient.disconnect();
397
+ } catch (error: any) {
398
+ console.error(t('errors.shutdownError', { message: error.message }));
399
+ }
400
+ }
401
+
402
+ console.log(t('app.goodbye') + ' šŸ‘‹\n');
403
+ process.exit(0);
404
+ };
405
+
406
+ process.on('SIGINT', () => shutdown('SIGINT'));
407
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
408
+
409
+ // Handle uncaught errors
410
+ process.on('uncaughtException', (error) => {
411
+ console.error('\nāŒ ' + t('errors.uncaughtException', { message: error.message }));
412
+ console.error(error.stack);
413
+ process.exit(1);
414
+ });
415
+
416
+ process.on('unhandledRejection', (reason: any) => {
417
+ console.error('\nāŒ ' + t('errors.unhandledRejection', { message: reason?.message || reason }));
418
+ if (reason?.stack) {
419
+ console.error(reason.stack);
420
+ }
421
+ process.exit(1);
422
+ });
423
+ }
424
+
425
+ /**
426
+ * Main CLI entry point - parse arguments and run appropriate command
427
+ */
428
+ export async function main(): Promise<void> {
429
+ setupGracefulShutdown();
430
+ detectLocale();
431
+
432
+ const argv = yargs(hideBin(process.argv))
433
+ .usage('Usage: $0 [options]')
434
+ .example('$0', 'Start the proxy client with default settings')
435
+ .example('$0 --setup', 'Run the interactive setup wizard')
436
+ .example('$0 --account', 'Show current account email and subscription status')
437
+ .example('$0 --logout', 'Logout and clear all credentials')
438
+ .example('$0 -c /path/to/config.json', 'Use a custom configuration file')
439
+ .option('config', {
440
+ alias: 'c',
441
+ type: 'string',
442
+ description: 'Path to configuration file (default: .spck-editor/config/spck-cli.config.json)',
443
+ })
444
+ .option('setup', {
445
+ type: 'boolean',
446
+ description: 'Run interactive setup wizard',
447
+ default: false,
448
+ })
449
+ .option('account', {
450
+ type: 'boolean',
451
+ description: 'Show account information (email and subscription status)',
452
+ default: false,
453
+ })
454
+ .option('logout', {
455
+ type: 'boolean',
456
+ description: 'Logout and clear all credentials and connection settings',
457
+ default: false,
458
+ })
459
+ .option('locale', {
460
+ type: 'string',
461
+ description: 'Set locale for CLI output (e.g., en, es, fr, ja, ko, pt, zh-Hans)',
462
+ })
463
+ .option('port', {
464
+ alias: 'p',
465
+ type: 'number',
466
+ description: 'Server port (overrides config)',
467
+ })
468
+ .option('root', {
469
+ alias: 'r',
470
+ type: 'string',
471
+ description: 'Root directory to serve (overrides config)',
472
+ })
473
+ .option('server', {
474
+ alias: 's',
475
+ type: 'string',
476
+ description: 'Proxy server URL override (e.g., cli-na-1.spck.io)',
477
+ })
478
+ // Hidden development flags (not documented)
479
+ .option('__internal_disable_ripgrep', {
480
+ type: 'boolean',
481
+ hidden: true,
482
+ default: false,
483
+ })
484
+ .option('__internal_disable_git', {
485
+ type: 'boolean',
486
+ hidden: true,
487
+ default: false,
488
+ })
489
+ .help()
490
+ .alias('help', 'h')
491
+ .version()
492
+ .alias('version', 'v')
493
+ .strict()
494
+ .fail((msg, err, _yargs) => {
495
+ if (err) throw err; // Preserve stack trace for actual errors
496
+ console.error('\nāŒ ' + t('errors.cliError', { message: msg }));
497
+ console.error('\n' + t('errors.cliErrorHint') + '\n');
498
+ process.exit(1);
499
+ })
500
+ .epilogue(
501
+ 'For more information, visit: https://github.com/spck-io/spck\n\n' +
502
+ 'Configuration:\n' +
503
+ ' User credentials: ~/.spck-editor/.credentials.json\n' +
504
+ ' Project data: ~/.spck-editor/projects/{project_id}/\n' +
505
+ ' Project directory: .spck-editor/ (contains local files and config symlink)\n' +
506
+ ' Config symlink: .spck-editor/config -> ~/.spck-editor/projects/{project_id}/\n\n' +
507
+ 'Authentication:\n' +
508
+ ' The CLI uses Firebase authentication to securely connect to the proxy server.\n' +
509
+ ' You will be prompted to authenticate on first run or when credentials expire.\n' +
510
+ ' Use --logout to clear credentials and connection settings.'
511
+ )
512
+ .parseSync();
513
+
514
+ // Apply --locale if provided
515
+ if (argv.locale) {
516
+ setLocale(argv.locale as string);
517
+ }
518
+
519
+ // Execute the appropriate command
520
+ if (argv.account) {
521
+ await showAccountInfo();
522
+ } else if (argv.logout) {
523
+ await logout();
524
+ } else if (argv.setup) {
525
+ await runSetup(argv.config as string | undefined);
526
+ process.exit(0);
527
+ } else {
528
+ await startProxyClient(argv.config as string | undefined, {
529
+ disableGit: argv.__internal_disable_git as boolean | undefined,
530
+ disableRipgrep: argv.__internal_disable_ripgrep as boolean | undefined,
531
+ serverOverride: argv.server as string | undefined,
532
+ });
533
+ }
534
+ }
535
+
536
+ // Auto-run if executed directly (e.g., via npm start or node dist/index.js)
537
+ if (import.meta.url === `file://${process.argv[1]}`) {
538
+ main().catch((error: any) => {
539
+ console.error(t('errors.cliError', { message: error.message }));
540
+ process.exit(1);
541
+ });
542
+ }