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