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
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive setup wizard for spck-cli
|
|
3
|
+
* Configures CLI to connect to proxy server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as readline from 'readline';
|
|
7
|
+
import { ServerConfig } from '../types.js';
|
|
8
|
+
import { saveConfig, createDefaultConfig } from '../config/config.js';
|
|
9
|
+
import { ensureProjectDir } from '../utils/project-dir.js';
|
|
10
|
+
import { gitignoreExists, isSpckEditorIgnored, addSpckEditorToGitignore } from '../utils/gitignore.js';
|
|
11
|
+
import { t } from '../i18n/index.js';
|
|
12
|
+
|
|
13
|
+
const USER_AUTH_DOCS_URL = 'https://spck.io/docs/cli#user-authentication';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create readline interface
|
|
17
|
+
*/
|
|
18
|
+
function createPrompt(): readline.Interface {
|
|
19
|
+
return readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prompt user for input
|
|
27
|
+
*/
|
|
28
|
+
function question(rl: readline.Interface, query: string): Promise<string> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
rl.question(query, (answer) => {
|
|
31
|
+
resolve(answer);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ask yes/no question
|
|
38
|
+
*/
|
|
39
|
+
async function questionYesNo(
|
|
40
|
+
rl: readline.Interface,
|
|
41
|
+
query: string,
|
|
42
|
+
defaultValue: boolean
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const answer = await question(rl, query);
|
|
45
|
+
const normalized = answer.trim().toLowerCase();
|
|
46
|
+
|
|
47
|
+
if (normalized === '') {
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return normalized === 'y' || normalized === 'yes';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run the setup wizard
|
|
56
|
+
*/
|
|
57
|
+
export async function runSetup(configPath?: string): Promise<ServerConfig> {
|
|
58
|
+
const rl = createPrompt();
|
|
59
|
+
|
|
60
|
+
console.log('\n' + '='.repeat(60));
|
|
61
|
+
console.log(' ' + t('setup.title'));
|
|
62
|
+
console.log('='.repeat(60) + '\n');
|
|
63
|
+
|
|
64
|
+
console.log(t('setup.description1'));
|
|
65
|
+
console.log(t('setup.description2'));
|
|
66
|
+
console.log(t('setup.description3') + '\n');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Step 1: Root directory
|
|
70
|
+
console.log('--- ' + t('setup.projectConfig') + ' ---\n');
|
|
71
|
+
|
|
72
|
+
const root = await question(
|
|
73
|
+
rl,
|
|
74
|
+
t('setup.rootDirPrompt', { default: process.cwd() })
|
|
75
|
+
);
|
|
76
|
+
const rootPath = root.trim() || process.cwd();
|
|
77
|
+
|
|
78
|
+
// Step 1.5: Server name (for QR code identification)
|
|
79
|
+
const defaultConfig = createDefaultConfig();
|
|
80
|
+
|
|
81
|
+
// Step 2: Terminal service
|
|
82
|
+
console.log('\n--- ' + t('setup.terminalConfig') + ' ---\n');
|
|
83
|
+
console.log(t('setup.terminalDescription'));
|
|
84
|
+
|
|
85
|
+
const terminalEnabled = await questionYesNo(
|
|
86
|
+
rl,
|
|
87
|
+
t('setup.terminalPrompt'),
|
|
88
|
+
true
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let maxBufferedLines = 5000;
|
|
92
|
+
let maxTerminals = 10;
|
|
93
|
+
|
|
94
|
+
// Step 4: Advanced terminal configuration
|
|
95
|
+
if (terminalEnabled) {
|
|
96
|
+
const advancedTerminal = await questionYesNo(
|
|
97
|
+
rl,
|
|
98
|
+
'\n' + t('setup.advancedTerminalPrompt'),
|
|
99
|
+
false
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (advancedTerminal) {
|
|
103
|
+
console.log('');
|
|
104
|
+
const bufferInput = await question(
|
|
105
|
+
rl,
|
|
106
|
+
t('setup.maxBufferPrompt', { default: String(maxBufferedLines) })
|
|
107
|
+
);
|
|
108
|
+
maxBufferedLines = parseInt(bufferInput) || 5000;
|
|
109
|
+
|
|
110
|
+
console.log(' ' + t('setup.maxBufferHint') + '\n');
|
|
111
|
+
|
|
112
|
+
const maxTermInput = await question(
|
|
113
|
+
rl,
|
|
114
|
+
t('setup.maxTerminalsPrompt', { default: String(maxTerminals) })
|
|
115
|
+
);
|
|
116
|
+
maxTerminals = parseInt(maxTermInput) || 10;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Step 5: Browser proxy configuration
|
|
121
|
+
console.log('\n--- ' + t('setup.browserProxyConfig') + ' ---\n');
|
|
122
|
+
console.log(t('setup.browserProxyDescription') + '\n');
|
|
123
|
+
|
|
124
|
+
const browserProxyEnabled = await questionYesNo(
|
|
125
|
+
rl,
|
|
126
|
+
t('setup.browserProxyPrompt'),
|
|
127
|
+
true
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Step 6: Security configuration
|
|
131
|
+
console.log('\n--- ' + t('setup.securityConfig') + ' ---\n');
|
|
132
|
+
console.log(t('setup.securityDescription1'));
|
|
133
|
+
console.log(t('setup.securityDescription2'));
|
|
134
|
+
console.log(t('setup.securityDescription3'));
|
|
135
|
+
console.log(t('setup.securityDocsHint', { url: USER_AUTH_DOCS_URL }) + '\n');
|
|
136
|
+
|
|
137
|
+
const userAuthEnabled = await questionYesNo(
|
|
138
|
+
rl,
|
|
139
|
+
t('setup.securityPrompt'),
|
|
140
|
+
false
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Step 6: .gitignore configuration (advanced)
|
|
144
|
+
let shouldAddToGitignore = false;
|
|
145
|
+
|
|
146
|
+
if (gitignoreExists(rootPath)) {
|
|
147
|
+
if (!isSpckEditorIgnored(rootPath)) {
|
|
148
|
+
console.log('\n--- ' + t('setup.gitConfig') + ' ---\n');
|
|
149
|
+
console.log(t('setup.gitignoreDetected'));
|
|
150
|
+
console.log(t('setup.gitignoreRecommend1'));
|
|
151
|
+
console.log(t('setup.gitignoreRecommend2') + '\n');
|
|
152
|
+
|
|
153
|
+
shouldAddToGitignore = await questionYesNo(
|
|
154
|
+
rl,
|
|
155
|
+
t('setup.gitignorePrompt'),
|
|
156
|
+
true
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
rl.close();
|
|
162
|
+
|
|
163
|
+
// Create configuration
|
|
164
|
+
const config: ServerConfig = {
|
|
165
|
+
version: 1,
|
|
166
|
+
root: rootPath,
|
|
167
|
+
name: defaultConfig.name,
|
|
168
|
+
terminal: {
|
|
169
|
+
enabled: terminalEnabled,
|
|
170
|
+
maxBufferedLines,
|
|
171
|
+
maxTerminals
|
|
172
|
+
},
|
|
173
|
+
security: {
|
|
174
|
+
userAuthenticationEnabled: userAuthEnabled
|
|
175
|
+
},
|
|
176
|
+
browserProxy: {
|
|
177
|
+
enabled: browserProxyEnabled
|
|
178
|
+
},
|
|
179
|
+
filesystem: {
|
|
180
|
+
maxFileSize: '10MB',
|
|
181
|
+
watchIgnorePatterns: [
|
|
182
|
+
'**/.git/**',
|
|
183
|
+
'**/.spck-editor/**',
|
|
184
|
+
'**/node_modules/**',
|
|
185
|
+
'**/*.log',
|
|
186
|
+
'**/.DS_Store',
|
|
187
|
+
'**/dist/**',
|
|
188
|
+
'**/build/**'
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Ensure project directory exists (creates symlink)
|
|
194
|
+
ensureProjectDir(config.root);
|
|
195
|
+
|
|
196
|
+
// Save configuration
|
|
197
|
+
saveConfig(config, configPath);
|
|
198
|
+
|
|
199
|
+
// Add to .gitignore if requested
|
|
200
|
+
if (shouldAddToGitignore) {
|
|
201
|
+
try {
|
|
202
|
+
addSpckEditorToGitignore(config.root);
|
|
203
|
+
console.log('\n✅ ' + t('setup.gitignoreAdded'));
|
|
204
|
+
} catch (error: any) {
|
|
205
|
+
console.warn('\n⚠️ ' + t('setup.gitignoreFailed', { message: error.message }));
|
|
206
|
+
console.warn(' ' + t('setup.gitignoreManualHint'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('\n' + '='.repeat(60));
|
|
211
|
+
console.log('✅ ' + t('config.saved'));
|
|
212
|
+
console.log('='.repeat(60) + '\n');
|
|
213
|
+
|
|
214
|
+
displayConfigSummary(config);
|
|
215
|
+
|
|
216
|
+
return config;
|
|
217
|
+
|
|
218
|
+
} catch (error: any) {
|
|
219
|
+
rl.close();
|
|
220
|
+
console.error('\n❌ ' + t('setup.setupFailed', { message: error.message }));
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Display configuration summary
|
|
227
|
+
*/
|
|
228
|
+
function displayConfigSummary(config: ServerConfig): void {
|
|
229
|
+
console.log(t('setup.configSummary'));
|
|
230
|
+
console.log(' ' + t('setup.summaryName', { name: config.name || t('setup.summaryNameNotSet') }));
|
|
231
|
+
console.log(' ' + t('setup.summaryRoot', { root: config.root }));
|
|
232
|
+
console.log(' ' + t('setup.summaryTerminal', { status: config.terminal.enabled ? t('setup.summaryEnabled') : t('setup.summaryDisabled') }));
|
|
233
|
+
|
|
234
|
+
if (config.terminal.enabled) {
|
|
235
|
+
console.log(' ' + t('setup.summaryMaxBuffer', { value: String(config.terminal.maxBufferedLines) }));
|
|
236
|
+
console.log(' ' + t('setup.summaryMaxProcesses', { value: String(config.terminal.maxTerminals) }));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(' ' + t('setup.summaryUserAuth', { status: config.security.userAuthenticationEnabled ? t('setup.summaryEnabled') : t('setup.summaryDisabled') }));
|
|
240
|
+
console.log(' ' + t('setup.summaryBrowserProxy', { status: config.browserProxy?.enabled !== false ? t('setup.summaryEnabled') : t('setup.summaryDisabled') }));
|
|
241
|
+
console.log('');
|
|
242
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for spck-cli server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Minimal Socket interface for our needs (not dependent on socket.io)
|
|
6
|
+
export interface SocketInterface {
|
|
7
|
+
id: string;
|
|
8
|
+
emit(event: string, data?: any): boolean;
|
|
9
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
10
|
+
off(event: string, listener: (...args: any[]) => void): this;
|
|
11
|
+
broadcast: {
|
|
12
|
+
emit(event: string, data?: any): boolean;
|
|
13
|
+
};
|
|
14
|
+
data: {
|
|
15
|
+
uid: string; // CLI user ID (from Firebase auth)
|
|
16
|
+
deviceId: string; // Mobile device ID (identifies the specific device)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Server Configuration
|
|
21
|
+
export interface ServerConfig {
|
|
22
|
+
version: number;
|
|
23
|
+
root: string;
|
|
24
|
+
name?: string; // Optional: Friendly name for QR code identification
|
|
25
|
+
|
|
26
|
+
terminal: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
maxBufferedLines: number;
|
|
29
|
+
maxTerminals: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
security: {
|
|
33
|
+
userAuthenticationEnabled: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
filesystem: {
|
|
37
|
+
maxFileSize: string;
|
|
38
|
+
watchIgnorePatterns: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
browserProxy?: {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Connection Settings (stored in .spck-editor/config/connection-settings.json)
|
|
47
|
+
export interface ConnectionSettings {
|
|
48
|
+
serverToken: string;
|
|
49
|
+
serverTokenExpiry: number;
|
|
50
|
+
clientId: string;
|
|
51
|
+
secret: string;
|
|
52
|
+
userId: string;
|
|
53
|
+
connectedAt: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Stored credentials (persisted to ~/.spck-editor/.credentials.json)
|
|
57
|
+
// Only refreshToken and userId are persisted - firebaseToken is generated on demand
|
|
58
|
+
export interface StoredCredentials {
|
|
59
|
+
refreshToken: string;
|
|
60
|
+
userId: string;
|
|
61
|
+
proxyServerUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Global config (persisted to ~/.spck-editor/global.config)
|
|
65
|
+
export interface GlobalConfig {
|
|
66
|
+
knownDeviceIds: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Firebase Credentials (runtime - includes ephemeral ID token)
|
|
70
|
+
export interface FirebaseCredentials {
|
|
71
|
+
firebaseToken: string;
|
|
72
|
+
firebaseTokenExpiry: number;
|
|
73
|
+
refreshToken: string;
|
|
74
|
+
userId: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// JSON-RPC 2.0 Types
|
|
78
|
+
export interface JSONRPCRequest {
|
|
79
|
+
jsonrpc: '2.0';
|
|
80
|
+
method: string;
|
|
81
|
+
params?: any;
|
|
82
|
+
id?: number | string;
|
|
83
|
+
timestamp?: number;
|
|
84
|
+
hmac: string;
|
|
85
|
+
nonce: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface JSONRPCResponse {
|
|
89
|
+
jsonrpc: '2.0';
|
|
90
|
+
result?: any;
|
|
91
|
+
error?: JSONRPCError;
|
|
92
|
+
id: number | string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface JSONRPCError {
|
|
96
|
+
code: number;
|
|
97
|
+
message: string;
|
|
98
|
+
data?: any;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface JSONRPCNotification {
|
|
102
|
+
jsonrpc: '2.0';
|
|
103
|
+
method: string;
|
|
104
|
+
params?: any;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// JWT Payload
|
|
108
|
+
export interface JWTPayload {
|
|
109
|
+
aud: string;
|
|
110
|
+
iat: number;
|
|
111
|
+
exp: number;
|
|
112
|
+
iss: string;
|
|
113
|
+
sub: string;
|
|
114
|
+
[key: string]: any;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Socket with user data (extends our minimal socket interface)
|
|
118
|
+
export interface AuthenticatedSocket extends SocketInterface {}
|
|
119
|
+
|
|
120
|
+
// Error codes (JSON-RPC 2.0 + custom)
|
|
121
|
+
export enum ErrorCode {
|
|
122
|
+
// Standard JSON-RPC 2.0
|
|
123
|
+
PARSE_ERROR = -32700,
|
|
124
|
+
INVALID_REQUEST = -32600,
|
|
125
|
+
METHOD_NOT_FOUND = -32601,
|
|
126
|
+
INVALID_PARAMS = -32602,
|
|
127
|
+
INTERNAL_ERROR = -32603,
|
|
128
|
+
|
|
129
|
+
// Authentication & Security
|
|
130
|
+
AUTHENTICATION_FAILED = -32001,
|
|
131
|
+
JWT_EXPIRED = -32002,
|
|
132
|
+
HMAC_VALIDATION_FAILED = -32003,
|
|
133
|
+
PERMISSION_DENIED = -32005,
|
|
134
|
+
|
|
135
|
+
// Filesystem
|
|
136
|
+
FILE_NOT_FOUND = -32004,
|
|
137
|
+
WRITE_CONFLICT = -32006,
|
|
138
|
+
INVALID_PATH = -32007,
|
|
139
|
+
FILE_TOO_LARGE = -32031,
|
|
140
|
+
INVALID_ENCODING = -32032,
|
|
141
|
+
DELTA_PATCH_FAILED = -32033,
|
|
142
|
+
|
|
143
|
+
// Git
|
|
144
|
+
GIT_OPERATION_FAILED = -32010,
|
|
145
|
+
INVALID_OID = -32011,
|
|
146
|
+
REPOSITORY_NOT_FOUND = -32012,
|
|
147
|
+
|
|
148
|
+
// Terminal
|
|
149
|
+
TERMINAL_NOT_FOUND = -32020,
|
|
150
|
+
TERMINAL_LIMIT_EXCEEDED = -32021,
|
|
151
|
+
TERMINAL_PROCESS_EXITED = -32022,
|
|
152
|
+
|
|
153
|
+
// Browser Proxy
|
|
154
|
+
BROWSER_PROXY_REQUEST_FAILED = -32050,
|
|
155
|
+
|
|
156
|
+
// General
|
|
157
|
+
OPERATION_TIMEOUT = -32030,
|
|
158
|
+
UID_NOT_AUTHORIZED = -32040,
|
|
159
|
+
FEATURE_DISABLED = -32041,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Helper to create JSON-RPC error
|
|
163
|
+
export function createRPCError(
|
|
164
|
+
code: ErrorCode,
|
|
165
|
+
message: string,
|
|
166
|
+
data?: any
|
|
167
|
+
): JSONRPCError {
|
|
168
|
+
return { code, message, data };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Proxy Protocol Messages
|
|
172
|
+
|
|
173
|
+
// Handshake protocol message types
|
|
174
|
+
export type HandshakeMessageType =
|
|
175
|
+
| 'auth'
|
|
176
|
+
| 'auth_result'
|
|
177
|
+
| 'request_user_verification'
|
|
178
|
+
| 'user_verification'
|
|
179
|
+
| 'protocol_info'
|
|
180
|
+
| 'protocol_selected'
|
|
181
|
+
| 'connected';
|
|
182
|
+
|
|
183
|
+
// Client authentication message (JWT signed with shared secret)
|
|
184
|
+
export interface ClientAuthMessage {
|
|
185
|
+
type: 'auth';
|
|
186
|
+
jwt: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Auth result message (CLI -> Client)
|
|
190
|
+
export interface AuthResultMessage {
|
|
191
|
+
type: 'auth_result';
|
|
192
|
+
success: boolean;
|
|
193
|
+
error?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// User verification request (CLI -> Client)
|
|
197
|
+
export interface UserVerificationRequestMessage {
|
|
198
|
+
type: 'request_user_verification';
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// User verification response (Client -> CLI)
|
|
203
|
+
export interface UserVerificationMessage {
|
|
204
|
+
type: 'user_verification';
|
|
205
|
+
firebaseToken: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Protocol info message (CLI -> Client)
|
|
209
|
+
export interface ProtocolInfoMessage {
|
|
210
|
+
type: 'protocol_info';
|
|
211
|
+
minVersion: number;
|
|
212
|
+
maxVersion: number;
|
|
213
|
+
features: {
|
|
214
|
+
terminal: boolean;
|
|
215
|
+
git: boolean;
|
|
216
|
+
fastSearch: boolean;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Protocol selected message (Client -> CLI)
|
|
221
|
+
export interface ProtocolSelectedMessage {
|
|
222
|
+
type: 'protocol_selected';
|
|
223
|
+
version: number;
|
|
224
|
+
ready: boolean;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Connection established message (CLI -> Client)
|
|
228
|
+
export interface ConnectedMessage {
|
|
229
|
+
type: 'connected';
|
|
230
|
+
message: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Union type for all handshake messages
|
|
234
|
+
export type HandshakeMessage =
|
|
235
|
+
| ClientAuthMessage
|
|
236
|
+
| AuthResultMessage
|
|
237
|
+
| UserVerificationRequestMessage
|
|
238
|
+
| UserVerificationMessage
|
|
239
|
+
| ProtocolInfoMessage
|
|
240
|
+
| ProtocolSelectedMessage
|
|
241
|
+
| ConnectedMessage;
|
|
242
|
+
|
|
243
|
+
// Proxy server events (from server to CLI)
|
|
244
|
+
export interface FreeTierInfo {
|
|
245
|
+
dailyLimitSeconds: number;
|
|
246
|
+
usedSeconds: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface ProxyAuthenticatedEvent {
|
|
250
|
+
token: string;
|
|
251
|
+
clientId: string;
|
|
252
|
+
userId: string;
|
|
253
|
+
expiresAt: number;
|
|
254
|
+
freeTierInfo?: FreeTierInfo | null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ProxyClientConnectingEvent {
|
|
258
|
+
connectionId: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export interface ProxyMultipleConnectionEvent {
|
|
262
|
+
existingConnections: string[];
|
|
263
|
+
newConnectionId: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export interface ProxyClientMessageEvent {
|
|
267
|
+
connectionId: string;
|
|
268
|
+
data: any;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface ProxyClientDisconnectedEvent {
|
|
272
|
+
connectionId: string;
|
|
273
|
+
reason?: string;
|
|
274
|
+
resetTime?: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface ProxyErrorEvent {
|
|
278
|
+
code: string;
|
|
279
|
+
message: string;
|
|
280
|
+
[key: string]: any;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Tool detection result
|
|
284
|
+
export interface ToolDetectionResult {
|
|
285
|
+
git: boolean;
|
|
286
|
+
ripgrep: boolean;
|
|
287
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
/**
|
|
3
|
+
* Tests for .gitignore utilities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import {
|
|
10
|
+
gitignoreExists,
|
|
11
|
+
isSpckEditorIgnored,
|
|
12
|
+
addSpckEditorToGitignore,
|
|
13
|
+
getGitignorePath,
|
|
14
|
+
} from '../gitignore';
|
|
15
|
+
|
|
16
|
+
describe('gitignore utilities', () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Create a temporary directory for each test
|
|
21
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spck-gitignore-test-'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
// Clean up temporary directory
|
|
26
|
+
if (fs.existsSync(tempDir)) {
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('gitignoreExists', () => {
|
|
32
|
+
it('should return false when .gitignore does not exist', () => {
|
|
33
|
+
expect(gitignoreExists(tempDir)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return true when .gitignore exists', () => {
|
|
37
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
38
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\n', 'utf8');
|
|
39
|
+
|
|
40
|
+
expect(gitignoreExists(tempDir)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isSpckEditorIgnored', () => {
|
|
45
|
+
it('should return false when .gitignore does not exist', () => {
|
|
46
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return false when .spck-editor/ is not in .gitignore', () => {
|
|
50
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
51
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\ndist/\n', 'utf8');
|
|
52
|
+
|
|
53
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return true when .spck-editor/ is in .gitignore', () => {
|
|
57
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
58
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\n.spck-editor/\ndist/\n', 'utf8');
|
|
59
|
+
|
|
60
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return false when only .spck-editor (without slash) is in .gitignore', () => {
|
|
64
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
65
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\n.spck-editor\ndist/\n', 'utf8');
|
|
66
|
+
|
|
67
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should ignore comments in .gitignore', () => {
|
|
71
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
gitignorePath,
|
|
74
|
+
'# This is a comment\nnode_modules/\n# .spck-editor/\ndist/\n',
|
|
75
|
+
'utf8'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should ignore empty lines in .gitignore', () => {
|
|
82
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
83
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\n\n\ndist/\n', 'utf8');
|
|
84
|
+
|
|
85
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle .gitignore with only whitespace lines', () => {
|
|
89
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
90
|
+
fs.writeFileSync(gitignorePath, ' \n\t\n\n', 'utf8');
|
|
91
|
+
|
|
92
|
+
expect(isSpckEditorIgnored(tempDir)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('addSpckEditorToGitignore', () => {
|
|
97
|
+
it('should create .gitignore with .spck-editor/ when file does not exist', () => {
|
|
98
|
+
addSpckEditorToGitignore(tempDir);
|
|
99
|
+
|
|
100
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
101
|
+
expect(fs.existsSync(gitignorePath)).toBe(true);
|
|
102
|
+
|
|
103
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
104
|
+
expect(content).toContain('.spck-editor/');
|
|
105
|
+
expect(content).toContain('# Spck CLI project data');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should append .spck-editor/ to existing .gitignore', () => {
|
|
109
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
110
|
+
fs.writeFileSync(gitignorePath, 'node_modules/\ndist/\n', 'utf8');
|
|
111
|
+
|
|
112
|
+
addSpckEditorToGitignore(tempDir);
|
|
113
|
+
|
|
114
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
115
|
+
expect(content).toContain('node_modules/');
|
|
116
|
+
expect(content).toContain('dist/');
|
|
117
|
+
expect(content).toContain('.spck-editor/');
|
|
118
|
+
expect(content).toContain('# Spck CLI project data');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should add newline before comment if .gitignore does not end with newline', () => {
|
|
122
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
123
|
+
fs.writeFileSync(gitignorePath, 'node_modules/', 'utf8'); // No trailing newline
|
|
124
|
+
|
|
125
|
+
addSpckEditorToGitignore(tempDir);
|
|
126
|
+
|
|
127
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
128
|
+
expect(content).toBe('node_modules/\n\n# Spck CLI project data\n.spck-editor/\n');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should not add .spck-editor/ if already present', () => {
|
|
132
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
133
|
+
const initialContent = 'node_modules/\n.spck-editor/\ndist/\n';
|
|
134
|
+
fs.writeFileSync(gitignorePath, initialContent, 'utf8');
|
|
135
|
+
|
|
136
|
+
addSpckEditorToGitignore(tempDir);
|
|
137
|
+
|
|
138
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
139
|
+
// Content should be unchanged
|
|
140
|
+
expect(content).toBe(initialContent);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should add .spck-editor/ even if .spck-editor (without slash) is present', () => {
|
|
144
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
145
|
+
const initialContent = 'node_modules/\n.spck-editor\ndist/\n';
|
|
146
|
+
fs.writeFileSync(gitignorePath, initialContent, 'utf8');
|
|
147
|
+
|
|
148
|
+
addSpckEditorToGitignore(tempDir);
|
|
149
|
+
|
|
150
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
151
|
+
// Should add the pattern since .spck-editor (without slash) is different from .spck-editor/
|
|
152
|
+
expect(content).toContain('.spck-editor/');
|
|
153
|
+
expect(content).toContain('# Spck CLI project data');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle empty .gitignore file', () => {
|
|
157
|
+
const gitignorePath = path.join(tempDir, '.gitignore');
|
|
158
|
+
fs.writeFileSync(gitignorePath, '', 'utf8');
|
|
159
|
+
|
|
160
|
+
addSpckEditorToGitignore(tempDir);
|
|
161
|
+
|
|
162
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
163
|
+
expect(content).toContain('.spck-editor/');
|
|
164
|
+
expect(content).toContain('# Spck CLI project data');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('getGitignorePath', () => {
|
|
169
|
+
it('should return correct .gitignore path', () => {
|
|
170
|
+
const expected = path.join(tempDir, '.gitignore');
|
|
171
|
+
expect(getGitignorePath(tempDir)).toBe(expected);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|