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/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
|
+
}
|