spck 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/.oxlintrc.json +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +631 -0
  4. package/bin/cli.js +20 -0
  5. package/bin/validate-cwd.js +41 -0
  6. package/dist/config/__tests__/config.test.d.ts +2 -0
  7. package/dist/config/__tests__/config.test.js +262 -0
  8. package/dist/config/__tests__/credentials.test.d.ts +2 -0
  9. package/dist/config/__tests__/credentials.test.js +360 -0
  10. package/dist/config/config.d.ts +33 -0
  11. package/dist/config/config.js +185 -0
  12. package/dist/config/credentials.d.ts +75 -0
  13. package/dist/config/credentials.js +259 -0
  14. package/dist/config/server-selection.d.ts +40 -0
  15. package/dist/config/server-selection.js +130 -0
  16. package/dist/connection/__tests__/firebase-auth.test.d.ts +2 -0
  17. package/dist/connection/__tests__/firebase-auth.test.js +96 -0
  18. package/dist/connection/__tests__/hmac.test.d.ts +2 -0
  19. package/dist/connection/__tests__/hmac.test.js +372 -0
  20. package/dist/connection/auth.d.ts +13 -0
  21. package/dist/connection/auth.js +91 -0
  22. package/dist/connection/firebase-auth.d.ts +40 -0
  23. package/dist/connection/firebase-auth.js +429 -0
  24. package/dist/connection/hmac.d.ts +24 -0
  25. package/dist/connection/hmac.js +109 -0
  26. package/dist/i18n/index.d.ts +25 -0
  27. package/dist/i18n/index.js +101 -0
  28. package/dist/i18n/locales/en.json +313 -0
  29. package/dist/i18n/locales/es.json +302 -0
  30. package/dist/i18n/locales/fr.json +302 -0
  31. package/dist/i18n/locales/id.json +302 -0
  32. package/dist/i18n/locales/ja.json +302 -0
  33. package/dist/i18n/locales/ko.json +302 -0
  34. package/dist/i18n/locales/locales/en.json +309 -0
  35. package/dist/i18n/locales/locales/es.json +302 -0
  36. package/dist/i18n/locales/locales/fr.json +302 -0
  37. package/dist/i18n/locales/locales/id.json +302 -0
  38. package/dist/i18n/locales/locales/ja.json +302 -0
  39. package/dist/i18n/locales/locales/ko.json +302 -0
  40. package/dist/i18n/locales/locales/pt.json +302 -0
  41. package/dist/i18n/locales/locales/zh-Hans.json +302 -0
  42. package/dist/i18n/locales/pt.json +302 -0
  43. package/dist/i18n/locales/zh-Hans.json +302 -0
  44. package/dist/index.d.ts +25 -0
  45. package/dist/index.js +493 -0
  46. package/dist/proxy/ProxyClient.d.ts +125 -0
  47. package/dist/proxy/ProxyClient.js +781 -0
  48. package/dist/proxy/ProxySocketWrapper.d.ts +43 -0
  49. package/dist/proxy/ProxySocketWrapper.js +98 -0
  50. package/dist/proxy/__tests__/ProxyClient.test.d.ts +2 -0
  51. package/dist/proxy/__tests__/ProxyClient.test.js +445 -0
  52. package/dist/proxy/__tests__/ProxySocketWrapper.test.d.ts +2 -0
  53. package/dist/proxy/__tests__/ProxySocketWrapper.test.js +190 -0
  54. package/dist/proxy/__tests__/handshake-validation.test.d.ts +2 -0
  55. package/dist/proxy/__tests__/handshake-validation.test.js +282 -0
  56. package/dist/proxy/__tests__/token-refresh-race.test.d.ts +14 -0
  57. package/dist/proxy/__tests__/token-refresh-race.test.js +173 -0
  58. package/dist/proxy/chunking.d.ts +53 -0
  59. package/dist/proxy/chunking.js +127 -0
  60. package/dist/proxy/handshake-validation.d.ts +21 -0
  61. package/dist/proxy/handshake-validation.js +49 -0
  62. package/dist/rpc/__tests__/router.test.d.ts +2 -0
  63. package/dist/rpc/__tests__/router.test.js +262 -0
  64. package/dist/rpc/router.d.ts +37 -0
  65. package/dist/rpc/router.js +132 -0
  66. package/dist/services/BrowserProxyService.d.ts +13 -0
  67. package/dist/services/BrowserProxyService.js +139 -0
  68. package/dist/services/FilesystemService.d.ts +99 -0
  69. package/dist/services/FilesystemService.js +742 -0
  70. package/dist/services/GitService.d.ts +243 -0
  71. package/dist/services/GitService.js +1439 -0
  72. package/dist/services/SearchService.d.ts +93 -0
  73. package/dist/services/SearchService.js +670 -0
  74. package/dist/services/TerminalService.d.ts +62 -0
  75. package/dist/services/TerminalService.js +337 -0
  76. package/dist/services/__tests__/BrowserProxyService.test.d.ts +2 -0
  77. package/dist/services/__tests__/BrowserProxyService.test.js +145 -0
  78. package/dist/services/__tests__/FilesystemService.test.d.ts +2 -0
  79. package/dist/services/__tests__/FilesystemService.test.js +609 -0
  80. package/dist/services/__tests__/GitService.test.d.ts +2 -0
  81. package/dist/services/__tests__/GitService.test.js +953 -0
  82. package/dist/services/__tests__/SearchService.test.d.ts +2 -0
  83. package/dist/services/__tests__/SearchService.test.js +384 -0
  84. package/dist/services/__tests__/TerminalService.test.d.ts +2 -0
  85. package/dist/services/__tests__/TerminalService.test.js +513 -0
  86. package/dist/setup/wizard.d.ts +10 -0
  87. package/dist/setup/wizard.js +172 -0
  88. package/dist/types.d.ts +196 -0
  89. package/dist/types.js +44 -0
  90. package/dist/utils/__tests__/gitignore.test.d.ts +2 -0
  91. package/dist/utils/__tests__/gitignore.test.js +127 -0
  92. package/dist/utils/gitignore.d.ts +24 -0
  93. package/dist/utils/gitignore.js +77 -0
  94. package/dist/utils/logger.d.ts +96 -0
  95. package/dist/utils/logger.js +456 -0
  96. package/dist/utils/project-dir.d.ts +51 -0
  97. package/dist/utils/project-dir.js +191 -0
  98. package/dist/utils/ripgrep.d.ts +34 -0
  99. package/dist/utils/ripgrep.js +148 -0
  100. package/dist/utils/tool-detection.d.ts +17 -0
  101. package/dist/utils/tool-detection.js +126 -0
  102. package/dist/watcher/FileWatcher.d.ts +10 -0
  103. package/dist/watcher/FileWatcher.js +42 -0
  104. package/package.json +70 -0
  105. package/src/config/__tests__/config.test.ts +318 -0
  106. package/src/config/__tests__/credentials.test.ts +494 -0
  107. package/src/config/config.ts +206 -0
  108. package/src/config/credentials.ts +302 -0
  109. package/src/config/server-selection.ts +150 -0
  110. package/src/connection/__tests__/firebase-auth.test.ts +121 -0
  111. package/src/connection/__tests__/hmac.test.ts +509 -0
  112. package/src/connection/auth.ts +140 -0
  113. package/src/connection/firebase-auth.ts +504 -0
  114. package/src/connection/hmac.ts +139 -0
  115. package/src/i18n/index.ts +119 -0
  116. package/src/i18n/locales/en.json +313 -0
  117. package/src/i18n/locales/es.json +302 -0
  118. package/src/i18n/locales/fr.json +302 -0
  119. package/src/i18n/locales/id.json +302 -0
  120. package/src/i18n/locales/ja.json +302 -0
  121. package/src/i18n/locales/ko.json +302 -0
  122. package/src/i18n/locales/pt.json +302 -0
  123. package/src/i18n/locales/zh-Hans.json +302 -0
  124. package/src/index.ts +542 -0
  125. package/src/proxy/ProxyClient.ts +968 -0
  126. package/src/proxy/ProxySocketWrapper.ts +113 -0
  127. package/src/proxy/__tests__/ProxyClient.test.ts +575 -0
  128. package/src/proxy/__tests__/ProxySocketWrapper.test.ts +251 -0
  129. package/src/proxy/__tests__/handshake-validation.test.ts +367 -0
  130. package/src/proxy/chunking.ts +162 -0
  131. package/src/proxy/handshake-validation.ts +64 -0
  132. package/src/rpc/__tests__/router.test.ts +400 -0
  133. package/src/rpc/router.ts +183 -0
  134. package/src/services/BrowserProxyService.ts +179 -0
  135. package/src/services/FilesystemService.ts +841 -0
  136. package/src/services/GitService.ts +1639 -0
  137. package/src/services/SearchService.ts +809 -0
  138. package/src/services/TerminalService.ts +413 -0
  139. package/src/services/__tests__/BrowserProxyService.test.ts +155 -0
  140. package/src/services/__tests__/FilesystemService.test.ts +1002 -0
  141. package/src/services/__tests__/GitService.test.ts +1552 -0
  142. package/src/services/__tests__/SearchService.test.ts +484 -0
  143. package/src/services/__tests__/TerminalService.test.ts +702 -0
  144. package/src/setup/wizard.ts +242 -0
  145. package/src/types/fossil-delta.d.ts +4 -0
  146. package/src/types.ts +287 -0
  147. package/src/utils/__tests__/gitignore.test.ts +174 -0
  148. package/src/utils/gitignore.ts +91 -0
  149. package/src/utils/logger.ts +578 -0
  150. package/src/utils/project-dir.ts +218 -0
  151. package/src/utils/ripgrep.ts +180 -0
  152. package/src/utils/tool-detection.ts +141 -0
  153. package/src/watcher/FileWatcher.ts +53 -0
  154. package/tsconfig.json +24 -0
  155. package/vitest.config.ts +19 -0
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'fossil-delta' {
2
+ export function create(origin: Buffer, target: Buffer): Buffer;
3
+ export function apply(origin: Buffer, delta: Buffer): Buffer;
4
+ }
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
+ });