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.
- package/.oxlintrc.json +49 -0
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/bin/cli.js +20 -0
- package/bin/validate-cwd.js +41 -0
- package/dist/config/__tests__/config.test.d.ts +2 -0
- package/dist/config/__tests__/config.test.js +262 -0
- package/dist/config/__tests__/credentials.test.d.ts +2 -0
- package/dist/config/__tests__/credentials.test.js +360 -0
- package/dist/config/config.d.ts +33 -0
- package/dist/config/config.js +185 -0
- package/dist/config/credentials.d.ts +75 -0
- package/dist/config/credentials.js +259 -0
- package/dist/config/server-selection.d.ts +40 -0
- package/dist/config/server-selection.js +130 -0
- package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
- package/dist/connection/__tests__/firebase-auth.test.js +96 -0
- package/dist/connection/__tests__/hmac.test.d.ts +2 -0
- package/dist/connection/__tests__/hmac.test.js +372 -0
- package/dist/connection/auth.d.ts +13 -0
- package/dist/connection/auth.js +91 -0
- package/dist/connection/firebase-auth.d.ts +40 -0
- package/dist/connection/firebase-auth.js +429 -0
- package/dist/connection/hmac.d.ts +24 -0
- package/dist/connection/hmac.js +109 -0
- package/dist/i18n/index.d.ts +25 -0
- package/dist/i18n/index.js +101 -0
- package/dist/i18n/locales/en.json +313 -0
- package/dist/i18n/locales/es.json +302 -0
- package/dist/i18n/locales/fr.json +302 -0
- package/dist/i18n/locales/id.json +302 -0
- package/dist/i18n/locales/ja.json +302 -0
- package/dist/i18n/locales/ko.json +302 -0
- package/dist/i18n/locales/locales/en.json +309 -0
- package/dist/i18n/locales/locales/es.json +302 -0
- package/dist/i18n/locales/locales/fr.json +302 -0
- package/dist/i18n/locales/locales/id.json +302 -0
- package/dist/i18n/locales/locales/ja.json +302 -0
- package/dist/i18n/locales/locales/ko.json +302 -0
- package/dist/i18n/locales/locales/pt.json +302 -0
- package/dist/i18n/locales/locales/zh-Hans.json +302 -0
- package/dist/i18n/locales/pt.json +302 -0
- package/dist/i18n/locales/zh-Hans.json +302 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +493 -0
- package/dist/proxy/ProxyClient.d.ts +125 -0
- package/dist/proxy/ProxyClient.js +781 -0
- package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
- package/dist/proxy/ProxySocketWrapper.js +98 -0
- package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
- package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
- package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
- package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
- package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
- package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
- package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
- package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
- package/dist/proxy/chunking.d.ts +53 -0
- package/dist/proxy/chunking.js +127 -0
- package/dist/proxy/handshake-validation.d.ts +21 -0
- package/dist/proxy/handshake-validation.js +49 -0
- package/dist/rpc/__tests__/router.test.d.ts +2 -0
- package/dist/rpc/__tests__/router.test.js +262 -0
- package/dist/rpc/router.d.ts +37 -0
- package/dist/rpc/router.js +132 -0
- package/dist/services/BrowserProxyService.d.ts +13 -0
- package/dist/services/BrowserProxyService.js +139 -0
- package/dist/services/FilesystemService.d.ts +99 -0
- package/dist/services/FilesystemService.js +742 -0
- package/dist/services/GitService.d.ts +243 -0
- package/dist/services/GitService.js +1439 -0
- package/dist/services/SearchService.d.ts +93 -0
- package/dist/services/SearchService.js +670 -0
- package/dist/services/TerminalService.d.ts +62 -0
- package/dist/services/TerminalService.js +337 -0
- package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
- package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
- package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
- package/dist/services/__tests__/FilesystemService.test.js +609 -0
- package/dist/services/__tests__/GitService.test.d.ts +2 -0
- package/dist/services/__tests__/GitService.test.js +953 -0
- package/dist/services/__tests__/SearchService.test.d.ts +2 -0
- package/dist/services/__tests__/SearchService.test.js +384 -0
- package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
- package/dist/services/__tests__/TerminalService.test.js +513 -0
- package/dist/setup/wizard.d.ts +10 -0
- package/dist/setup/wizard.js +172 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.js +44 -0
- package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
- package/dist/utils/__tests__/gitignore.test.js +127 -0
- package/dist/utils/gitignore.d.ts +24 -0
- package/dist/utils/gitignore.js +77 -0
- package/dist/utils/logger.d.ts +96 -0
- package/dist/utils/logger.js +456 -0
- package/dist/utils/project-dir.d.ts +51 -0
- package/dist/utils/project-dir.js +191 -0
- package/dist/utils/ripgrep.d.ts +34 -0
- package/dist/utils/ripgrep.js +148 -0
- package/dist/utils/tool-detection.d.ts +17 -0
- package/dist/utils/tool-detection.js +126 -0
- package/dist/watcher/FileWatcher.d.ts +10 -0
- package/dist/watcher/FileWatcher.js +42 -0
- package/package.json +70 -0
- package/src/config/__tests__/config.test.ts +318 -0
- package/src/config/__tests__/credentials.test.ts +494 -0
- package/src/config/config.ts +206 -0
- package/src/config/credentials.ts +302 -0
- package/src/config/server-selection.ts +150 -0
- package/src/connection/__tests__/firebase-auth.test.ts +121 -0
- package/src/connection/__tests__/hmac.test.ts +509 -0
- package/src/connection/auth.ts +140 -0
- package/src/connection/firebase-auth.ts +504 -0
- package/src/connection/hmac.ts +139 -0
- package/src/i18n/index.ts +119 -0
- package/src/i18n/locales/en.json +313 -0
- package/src/i18n/locales/es.json +302 -0
- package/src/i18n/locales/fr.json +302 -0
- package/src/i18n/locales/id.json +302 -0
- package/src/i18n/locales/ja.json +302 -0
- package/src/i18n/locales/ko.json +302 -0
- package/src/i18n/locales/pt.json +302 -0
- package/src/i18n/locales/zh-Hans.json +302 -0
- package/src/index.ts +542 -0
- package/src/proxy/ProxyClient.ts +968 -0
- package/src/proxy/ProxySocketWrapper.ts +113 -0
- package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
- package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
- package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
- package/src/proxy/chunking.ts +162 -0
- package/src/proxy/handshake-validation.ts +64 -0
- package/src/rpc/__tests__/router.test.ts +400 -0
- package/src/rpc/router.ts +183 -0
- package/src/services/BrowserProxyService.ts +179 -0
- package/src/services/FilesystemService.ts +841 -0
- package/src/services/GitService.ts +1639 -0
- package/src/services/SearchService.ts +809 -0
- package/src/services/TerminalService.ts +413 -0
- package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
- package/src/services/__tests__/FilesystemService.test.ts +1002 -0
- package/src/services/__tests__/GitService.test.ts +1552 -0
- package/src/services/__tests__/SearchService.test.ts +484 -0
- package/src/services/__tests__/TerminalService.test.ts +702 -0
- package/src/setup/wizard.ts +242 -0
- package/src/types/fossil-delta.d.ts +4 -0
- package/src/types.ts +287 -0
- package/src/utils/__tests__/gitignore.test.ts +174 -0
- package/src/utils/gitignore.ts +91 -0
- package/src/utils/logger.ts +578 -0
- package/src/utils/project-dir.ts +218 -0
- package/src/utils/ripgrep.ts +180 -0
- package/src/utils/tool-detection.ts +141 -0
- package/src/watcher/FileWatcher.ts +53 -0
- package/tsconfig.json +24 -0
- 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
|