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,91 @@
1
+ /**
2
+ * .gitignore file management utilities
3
+ * Handles checking and updating .gitignore files
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+
9
+ const DIR_PATTERN = '.spck-editor/';
10
+
11
+ /**
12
+ * Check if .gitignore exists in a directory
13
+ */
14
+ export function gitignoreExists(directory: string): boolean {
15
+ const gitignorePath = path.join(directory, '.gitignore');
16
+ return fs.existsSync(gitignorePath);
17
+ }
18
+
19
+ /**
20
+ * Check if .gitignore contains the .spck-editor pattern
21
+ * Returns true if the pattern is found (exact match)
22
+ */
23
+ export function isSpckEditorIgnored(directory: string): boolean {
24
+ const gitignorePath = path.join(directory, '.gitignore');
25
+
26
+ if (!fs.existsSync(gitignorePath)) {
27
+ return false;
28
+ }
29
+
30
+ try {
31
+ const content = fs.readFileSync(gitignorePath, 'utf8');
32
+ const lines = content.split('\n');
33
+
34
+ // Check if any line contains .spck-editor/ (ignoring comments and whitespace)
35
+ for (const line of lines) {
36
+ const trimmed = line.trim();
37
+ // Skip empty lines and comments
38
+ if (trimmed === '' || trimmed.startsWith('#')) {
39
+ continue;
40
+ }
41
+ // Check if line matches the directory pattern
42
+ if (trimmed === DIR_PATTERN) {
43
+ return true;
44
+ }
45
+ }
46
+
47
+ return false;
48
+ } catch (error) {
49
+ // If we can't read the file, assume it's not ignored
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Add .spck-editor/ to .gitignore
56
+ * Creates .gitignore if it doesn't exist
57
+ * Appends the pattern if not already present
58
+ */
59
+ export function addSpckEditorToGitignore(directory: string): void {
60
+ const gitignorePath = path.join(directory, '.gitignore');
61
+
62
+ // Check if already ignored
63
+ if (isSpckEditorIgnored(directory)) {
64
+ return; // Already present, nothing to do
65
+ }
66
+
67
+ let content = '';
68
+
69
+ if (fs.existsSync(gitignorePath)) {
70
+ // Read existing content
71
+ content = fs.readFileSync(gitignorePath, 'utf8');
72
+
73
+ // Ensure content ends with newline
74
+ if (content.length > 0 && !content.endsWith('\n')) {
75
+ content += '\n';
76
+ }
77
+ }
78
+
79
+ // Add comment and pattern
80
+ const addition = `\n# Spck CLI project data\n${DIR_PATTERN}\n`;
81
+
82
+ // Write back to file
83
+ fs.writeFileSync(gitignorePath, content + addition, 'utf8');
84
+ }
85
+
86
+ /**
87
+ * Get the full path to .gitignore in a directory
88
+ */
89
+ export function getGitignorePath(directory: string): string {
90
+ return path.join(directory, '.gitignore');
91
+ }
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Simple, readable logging utility for network operations
3
+ */
4
+
5
+ import chalk from 'chalk';
6
+ import * as path from 'path';
7
+ import * as fs from 'fs';
8
+
9
+ const LOG_RETENTION_DAYS = 30;
10
+ let logDirInitialized = false;
11
+ let cleanupScheduled = false;
12
+
13
+ /**
14
+ * Get log directory path (lazy - doesn't create it)
15
+ */
16
+ function getLogDir(): string {
17
+ return path.join(process.cwd(), '.spck-editor', 'logs');
18
+ }
19
+
20
+ /**
21
+ * Ensure log directory exists (lazy initialization)
22
+ * Called only when actually writing logs
23
+ */
24
+ function ensureLogDirectory(): void {
25
+ if (logDirInitialized) {
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const logDir = getLogDir();
31
+
32
+ // Check if .spck-editor exists and is accessible
33
+ const spckEditorDir = path.join(process.cwd(), '.spck-editor');
34
+ if (!fs.existsSync(spckEditorDir)) {
35
+ // .spck-editor directory not set up yet - skip logging to file
36
+ return;
37
+ }
38
+
39
+ // Create logs subdirectory if needed
40
+ if (!fs.existsSync(logDir)) {
41
+ fs.mkdirSync(logDir, { recursive: true });
42
+ }
43
+
44
+ logDirInitialized = true;
45
+
46
+ // Schedule cleanup only once, after first successful initialization
47
+ if (!cleanupScheduled) {
48
+ cleanupScheduled = true;
49
+ // Run cleanup after a short delay (not immediately on import)
50
+ setTimeout(() => {
51
+ cleanOldLogs();
52
+ setInterval(cleanOldLogs, 24 * 60 * 60 * 1000).unref();
53
+ }, 1000).unref();
54
+ }
55
+ } catch (error) {
56
+ // Silently fail if we can't create log directory
57
+ // Logging will just go to console only
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get current log file path with date
63
+ */
64
+ function getCurrentLogFile(): string {
65
+ const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
66
+ return path.join(getLogDir(), `spck-cli-${date}.log`);
67
+ }
68
+
69
+ /**
70
+ * Clean up old log files (retention policy)
71
+ */
72
+ function cleanOldLogs(): void {
73
+ try {
74
+ const logDir = getLogDir();
75
+ if (!fs.existsSync(logDir)) {
76
+ return;
77
+ }
78
+
79
+ const files = fs.readdirSync(logDir);
80
+ const now = Date.now();
81
+ const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
82
+
83
+ for (const file of files) {
84
+ if (file.startsWith('spck-cli-') && file.endsWith('.log')) {
85
+ const filePath = path.join(logDir, file);
86
+ const stats = fs.statSync(filePath);
87
+
88
+ if (now - stats.mtimeMs > retentionMs) {
89
+ fs.unlinkSync(filePath);
90
+ console.log(chalk.gray(`[Logger] Deleted old log file: ${file}`));
91
+ }
92
+ }
93
+ }
94
+ } catch (error) {
95
+ // Silently fail cleanup errors
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Format timestamp for display (compact format for files)
101
+ * Format: MM-DD HH:MM:SS
102
+ */
103
+ function formatTime(): string {
104
+ const now = new Date();
105
+ const month = String(now.getMonth() + 1).padStart(2, '0');
106
+ const day = String(now.getDate()).padStart(2, '0');
107
+ const hours = String(now.getHours()).padStart(2, '0');
108
+ const minutes = String(now.getMinutes()).padStart(2, '0');
109
+ const seconds = String(now.getSeconds()).padStart(2, '0');
110
+ return `${month}-${day} ${hours}:${minutes}:${seconds}`;
111
+ }
112
+
113
+ /**
114
+ * Format timestamp for terminal display (compact format)
115
+ */
116
+ function formatTimeCompact(): string {
117
+ const now = new Date();
118
+ // Format: HH:MM:SS
119
+ return now.toTimeString().substring(0, 8);
120
+ }
121
+
122
+ /**
123
+ * Format UID for display (truncate if needed)
124
+ */
125
+ function formatUid(uid: string, maxLen: number = 12): string {
126
+ if (uid.length <= maxLen) return uid;
127
+ return uid.substring(0, maxLen - 3) + '...';
128
+ }
129
+
130
+ /**
131
+ * Write log entry to file
132
+ */
133
+ function writeToFile(message: string): void {
134
+ try {
135
+ // Lazy initialization - only create log directory when actually writing
136
+ ensureLogDirectory();
137
+
138
+ if (!logDirInitialized) {
139
+ // Log directory couldn't be initialized, skip file logging
140
+ return;
141
+ }
142
+
143
+ const logFile = getCurrentLogFile();
144
+ const timestamp = formatTime();
145
+ fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
146
+ } catch (error) {
147
+ // Silently fail file writes to not disrupt service
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Format path for display (truncate if too long)
153
+ */
154
+ function formatPath(p: string | undefined, maxLen: number = 50): string {
155
+ if (!p) return '';
156
+ if (p.length <= maxLen) return p;
157
+ return '...' + p.substring(p.length - maxLen + 3);
158
+ }
159
+
160
+ /**
161
+ * Format byte count as human-readable size string
162
+ */
163
+ function formatBytes(bytes: number): string {
164
+ if (bytes < 1024) return `${bytes} B`;
165
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
166
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
167
+ }
168
+
169
+ // Debounce state for browser proxy console output
170
+ const BROWSER_PROXY_DEBOUNCE_MS = 1000;
171
+ let _browserDebounceTimer: ReturnType<typeof setTimeout> | null = null;
172
+ let _browserSuccessCount = 0;
173
+ let _browserTotalBytes = 0;
174
+ let _browserLastUid = '';
175
+
176
+ function flushBrowserProxyLog(): void {
177
+ if (_browserSuccessCount === 0) return;
178
+ const timestamp = chalk.gray(formatTimeCompact());
179
+ const uidStr = chalk.gray(formatUid(_browserLastUid));
180
+ const sizeStr = chalk.gray(formatBytes(_browserTotalBytes));
181
+ const countStr = chalk.white(String(_browserSuccessCount));
182
+ console.log(`${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.blueBright('BROWSER')} ${countStr} files fetched (${sizeStr})`);
183
+ _browserSuccessCount = 0;
184
+ _browserTotalBytes = 0;
185
+ _browserLastUid = '';
186
+ _browserDebounceTimer = null;
187
+ }
188
+
189
+ /**
190
+ * Log a filesystem read operation
191
+ */
192
+ export function logFsRead(
193
+ method: string,
194
+ params: {
195
+ path?: string;
196
+ src?: string;
197
+ target?: string;
198
+ oldpath?: string;
199
+ [key: string]: any;
200
+ },
201
+ uid: string,
202
+ success: boolean,
203
+ error?: any,
204
+ metadata?: Record<string, any>
205
+ ): void {
206
+ const filepath = params.path || params.src || params.oldpath;
207
+ const displayPath = formatPath(filepath);
208
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
209
+ const timestamp = chalk.gray(formatTimeCompact());
210
+ const uidStr = chalk.gray(formatUid(uid));
211
+
212
+ if (success) {
213
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.cyan('FS')} ${chalk.white(method.padEnd(12))} ${chalk.gray(displayPath)}${metaStr}`;
214
+ console.log(msg);
215
+ writeToFile(`[INFO] FS READ ${method} ${filepath} uid=${uid} success=true${metaStr}`);
216
+ } else {
217
+ const errMsg = error?.message || String(error);
218
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.cyan('FS')} ${chalk.white(method.padEnd(12))} ${chalk.gray(displayPath)} ${chalk.red(errMsg)}`;
219
+ console.log(msg);
220
+ writeToFile(`[ERROR] FS READ ${method} ${filepath} uid=${uid} success=false error="${errMsg}"`);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Log a filesystem write operation
226
+ */
227
+ export function logFsWrite(
228
+ method: string,
229
+ params: {
230
+ path?: string;
231
+ src?: string;
232
+ target?: string;
233
+ oldpath?: string;
234
+ [key: string]: any;
235
+ },
236
+ uid: string,
237
+ success: boolean,
238
+ error?: any,
239
+ metadata?: Record<string, any>
240
+ ): void {
241
+ const filepath = params.path || params.src || params.target || params.oldpath;
242
+ const displayPath = formatPath(filepath);
243
+ const srcTarget = params.src && params.target
244
+ ? `${formatPath(params.src, 25)} → ${formatPath(params.target, 25)}`
245
+ : displayPath;
246
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
247
+ const timestamp = chalk.gray(formatTimeCompact());
248
+ const uidStr = chalk.gray(formatUid(uid));
249
+
250
+ if (success) {
251
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.yellow('FS')} ${chalk.white(method.padEnd(12))} ${chalk.gray(srcTarget)}${metaStr}`;
252
+ console.log(msg);
253
+ writeToFile(`[INFO] FS WRITE ${method} ${filepath} uid=${uid} success=true${metaStr}`);
254
+ } else {
255
+ const errMsg = error?.message || String(error);
256
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.yellow('FS')} ${chalk.white(method.padEnd(12))} ${chalk.gray(srcTarget)} ${chalk.red(errMsg)}`;
257
+ console.log(msg);
258
+ writeToFile(`[ERROR] FS WRITE ${method} ${filepath} uid=${uid} success=false error="${errMsg}"`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Log a git read operation
264
+ */
265
+ export function logGitRead(
266
+ method: string,
267
+ params: {
268
+ dir?: string;
269
+ [key: string]: any;
270
+ },
271
+ uid: string,
272
+ success: boolean,
273
+ error?: any,
274
+ metadata?: Record<string, any>
275
+ ): void {
276
+ const dir = formatPath(params.dir);
277
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
278
+ const timestamp = chalk.gray(formatTimeCompact());
279
+ const uidStr = chalk.gray(formatUid(uid));
280
+
281
+ if (success) {
282
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.magenta('GIT')} ${chalk.white(method.padEnd(12))} ${chalk.gray(dir)}${metaStr}`;
283
+ console.log(msg);
284
+ writeToFile(`[INFO] GIT READ ${method} dir=${params.dir} uid=${uid} success=true${metaStr}`);
285
+ } else {
286
+ const errMsg = error?.message || String(error);
287
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.magenta('GIT')} ${chalk.white(method.padEnd(12))} ${chalk.gray(dir)} ${chalk.red(errMsg)}`;
288
+ console.log(msg);
289
+ writeToFile(`[ERROR] GIT READ ${method} dir=${params.dir} uid=${uid} success=false error="${errMsg}"`);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Log a git write operation
295
+ */
296
+ export function logGitWrite(
297
+ method: string,
298
+ params: {
299
+ dir?: string;
300
+ message?: string;
301
+ filepaths?: string[];
302
+ ref?: string;
303
+ [key: string]: any;
304
+ },
305
+ uid: string,
306
+ success: boolean,
307
+ error?: any,
308
+ metadata?: Record<string, any>
309
+ ): void {
310
+ const dir = formatPath(params.dir);
311
+ const details = [];
312
+ if (params.message) details.push(`msg="${params.message.substring(0, 30)}${params.message.length > 30 ? '...' : ''}"`);
313
+ if (params.filepaths?.length) details.push(`files=${params.filepaths.length}`);
314
+ if (params.ref) details.push(`ref=${params.ref}`);
315
+ const detailStr = details.length ? ` ${chalk.gray(details.join(' '))}` : '';
316
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
317
+ const timestamp = chalk.gray(formatTimeCompact());
318
+ const uidStr = chalk.gray(formatUid(uid));
319
+
320
+ if (success) {
321
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.yellow('GIT')} ${chalk.white(method.padEnd(12))} ${chalk.gray(dir)}${detailStr}${metaStr}`;
322
+ console.log(msg);
323
+ writeToFile(`[INFO] GIT WRITE ${method} dir=${params.dir} uid=${uid} success=true${detailStr}${metaStr}`);
324
+ } else {
325
+ const errMsg = error?.message || String(error);
326
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.yellow('GIT')} ${chalk.white(method.padEnd(12))} ${chalk.gray(dir)} ${chalk.red(errMsg)}`;
327
+ console.log(msg);
328
+ writeToFile(`[ERROR] GIT WRITE ${method} dir=${params.dir} uid=${uid} success=false error="${errMsg}"`);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Log a terminal read operation
334
+ */
335
+ export function logTerminalRead(
336
+ method: string,
337
+ params: {
338
+ terminalId?: string;
339
+ [key: string]: any;
340
+ },
341
+ uid: string,
342
+ success: boolean,
343
+ error?: any,
344
+ metadata?: Record<string, any>
345
+ ): void {
346
+ const termId = params.terminalId || 'all';
347
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
348
+ const timestamp = chalk.gray(formatTimeCompact());
349
+ const uidStr = chalk.gray(formatUid(uid));
350
+
351
+ if (success) {
352
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.blue('TERM')} ${chalk.white(method.padEnd(12))} ${chalk.gray(termId)}${metaStr}`;
353
+ console.log(msg);
354
+ writeToFile(`[INFO] TERMINAL READ ${method} terminalId=${termId} uid=${uid} success=true${metaStr}`);
355
+ } else {
356
+ const errMsg = error?.message || String(error);
357
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.blue('TERM')} ${chalk.white(method.padEnd(12))} ${chalk.gray(termId)} ${chalk.red(errMsg)}`;
358
+ console.log(msg);
359
+ writeToFile(`[ERROR] TERMINAL READ ${method} terminalId=${termId} uid=${uid} success=false error="${errMsg}"`);
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Log a terminal write operation
365
+ */
366
+ export function logTerminalWrite(
367
+ method: string,
368
+ params: {
369
+ terminalId?: string;
370
+ data?: string;
371
+ cols?: number;
372
+ rows?: number;
373
+ [key: string]: any;
374
+ },
375
+ uid: string,
376
+ success: boolean,
377
+ error?: any,
378
+ metadata?: Record<string, any>
379
+ ): void {
380
+ const termId = params.terminalId || metadata?.terminalId || 'new';
381
+ const details = [];
382
+ if (params.cols && params.rows) details.push(`${params.cols}x${params.rows}`);
383
+ if (params.data) details.push(`${params.data.length}b`);
384
+ const detailStr = details.length ? ` ${chalk.gray(details.join(' '))}` : '';
385
+ const metaStr = metadata && !metadata.terminalId ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
386
+ const timestamp = chalk.gray(formatTimeCompact());
387
+ const uidStr = chalk.gray(formatUid(uid));
388
+
389
+ if (success) {
390
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.yellow('TERM')} ${chalk.white(method.padEnd(12))} ${chalk.gray(termId)}${detailStr}${metaStr}`;
391
+ console.log(msg);
392
+ writeToFile(`[INFO] TERMINAL WRITE ${method} terminalId=${termId} uid=${uid} success=true${detailStr}${metaStr}`);
393
+ } else {
394
+ const errMsg = error?.message || String(error);
395
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.yellow('TERM')} ${chalk.white(method.padEnd(12))} ${chalk.gray(termId)} ${chalk.red(errMsg)}`;
396
+ console.log(msg);
397
+ writeToFile(`[ERROR] TERMINAL WRITE ${method} terminalId=${termId} uid=${uid} success=false error="${errMsg}"`);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Log a search operation
403
+ */
404
+ export function logSearchRead(
405
+ method: string,
406
+ params: {
407
+ path?: string;
408
+ searchTerm?: string;
409
+ [key: string]: any;
410
+ },
411
+ uid: string,
412
+ success: boolean,
413
+ error?: any,
414
+ metadata?: Record<string, any>
415
+ ): void {
416
+ const filepath = formatPath(params.path);
417
+ const searchTerm = params.searchTerm ? `"${params.searchTerm.substring(0, 30)}${params.searchTerm.length > 30 ? '...' : ''}'"` : '';
418
+ const details = [];
419
+ if (searchTerm) details.push(searchTerm);
420
+ if (metadata?.matches !== undefined) details.push(`matches=${metadata.matches}`);
421
+ if (metadata?.method) details.push(metadata.method);
422
+ const detailStr = details.length ? ` ${chalk.gray(details.join(' '))}` : '';
423
+ const metaStr = metadata && !metadata.matches && !metadata.method ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
424
+ const timestamp = chalk.gray(formatTimeCompact());
425
+ const uidStr = chalk.gray(formatUid(uid));
426
+
427
+ if (success) {
428
+ const msg = `${timestamp} ${uidStr} ${chalk.green('✓')} ${chalk.green('SEARCH')} ${chalk.white(method.padEnd(12))} ${chalk.gray(filepath)}${detailStr}${metaStr}`;
429
+ console.log(msg);
430
+ writeToFile(`[INFO] SEARCH ${method} ${params.path} searchTerm="${params.searchTerm}" uid=${uid} success=true${detailStr}${metaStr}`);
431
+ } else {
432
+ const errMsg = error?.message || String(error);
433
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.green('SEARCH')} ${chalk.white(method.padEnd(12))} ${chalk.gray(filepath)} ${chalk.red(errMsg)}`;
434
+ console.log(msg);
435
+ writeToFile(`[ERROR] SEARCH ${method} ${params.path} searchTerm="${params.searchTerm}" uid=${uid} success=false error="${errMsg}"`);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Log an authentication event
441
+ */
442
+ export function logAuth(
443
+ event: string,
444
+ details: Record<string, any>,
445
+ level: 'info' | 'warn' | 'error' = 'info'
446
+ ): void {
447
+ const timestamp = chalk.gray(formatTimeCompact());
448
+ const deviceId = details.deviceId ? chalk.gray(formatUid(details.deviceId)) : '';
449
+ const userId = details.userId ? chalk.gray(`user=${details.userId}`) : '';
450
+ const metaStr = Object.entries(details)
451
+ .filter(([key]) => key !== 'deviceId' && key !== 'userId')
452
+ .map(([key, val]) => `${key}=${val}`)
453
+ .join(' ');
454
+
455
+ let symbol: string;
456
+ let color: (str: string) => string;
457
+ let logLevel: string;
458
+
459
+ if (level === 'error') {
460
+ symbol = chalk.red('✗');
461
+ color = chalk.red;
462
+ logLevel = 'ERROR';
463
+ } else if (level === 'warn') {
464
+ symbol = chalk.yellow('⚠');
465
+ color = chalk.yellow;
466
+ logLevel = 'WARN';
467
+ } else {
468
+ symbol = chalk.green('✓');
469
+ color = chalk.green;
470
+ logLevel = 'INFO';
471
+ }
472
+
473
+ const msg = `${timestamp} ${deviceId} ${userId} ${symbol} ${color('AUTH')} ${chalk.white(event.padEnd(20))} ${chalk.gray(metaStr)}`;
474
+ console.log(msg);
475
+ writeToFile(`[${logLevel}] AUTH ${event} ${metaStr}`);
476
+ }
477
+
478
+ /**
479
+ * Log connection events (client connecting, authenticated, disconnected)
480
+ */
481
+ export function logConnection(
482
+ event: 'connecting' | 'authenticated' | 'auth_failed' | 'disconnected' | 'ready',
483
+ deviceId?: string,
484
+ metadata?: Record<string, any>
485
+ ): void {
486
+ const timestamp = chalk.gray(formatTimeCompact());
487
+ const deviceStr = deviceId ? chalk.gray(formatUid(deviceId)) : chalk.gray('...');
488
+ const metaStr = metadata ? ` ${chalk.gray(JSON.stringify(metadata))}` : '';
489
+
490
+ let symbol: string;
491
+ let color: (str: string) => string;
492
+ let logLevel: string;
493
+
494
+ switch (event) {
495
+ case 'connecting':
496
+ symbol = '🔌';
497
+ color = chalk.blue;
498
+ logLevel = 'INFO';
499
+ break;
500
+ case 'authenticated':
501
+ symbol = chalk.green('✓');
502
+ color = chalk.green;
503
+ logLevel = 'INFO';
504
+ break;
505
+ case 'auth_failed':
506
+ symbol = chalk.red('✗');
507
+ color = chalk.red;
508
+ logLevel = 'ERROR';
509
+ break;
510
+ case 'disconnected':
511
+ symbol = '🔌';
512
+ color = chalk.gray;
513
+ logLevel = 'INFO';
514
+ break;
515
+ case 'ready':
516
+ symbol = '🎉';
517
+ color = chalk.green;
518
+ logLevel = 'INFO';
519
+ break;
520
+ default:
521
+ symbol = 'ℹ';
522
+ color = chalk.gray;
523
+ logLevel = 'INFO';
524
+ }
525
+
526
+ const msg = `${timestamp} ${deviceStr} ${symbol} ${color('CONN')} ${chalk.white(event.padEnd(15))}${metaStr}`;
527
+ console.log(msg);
528
+ writeToFile(`[${logLevel}] CONN ${event} deviceId=${deviceId || 'unknown'}${metaStr}`);
529
+ }
530
+
531
+ /**
532
+ * Log a browser proxy request
533
+ */
534
+ export function logBrowserProxy(
535
+ params: {
536
+ url?: string;
537
+ method?: string;
538
+ requestId?: string;
539
+ [key: string]: any;
540
+ },
541
+ uid: string,
542
+ success: boolean,
543
+ error?: any,
544
+ metadata?: Record<string, any>
545
+ ): void {
546
+ const httpMethod = (params.method || 'GET').toUpperCase();
547
+
548
+ if (success) {
549
+ writeToFile(`[INFO] BROWSER PROXY ${httpMethod} ${params.url} uid=${uid} success=true status=${metadata?.status}`);
550
+ // Accumulate and debounce console output into a single summary line
551
+ _browserSuccessCount++;
552
+ _browserTotalBytes += metadata?.size ?? 0;
553
+ _browserLastUid = uid;
554
+ if (_browserDebounceTimer) clearTimeout(_browserDebounceTimer);
555
+ _browserDebounceTimer = setTimeout(flushBrowserProxyLog, BROWSER_PROXY_DEBOUNCE_MS);
556
+ } else {
557
+ const displayUrl = formatPath(params.url, 60);
558
+ const timestamp = chalk.gray(formatTimeCompact());
559
+ const uidStr = chalk.gray(formatUid(uid));
560
+ const errMsg = error?.message || String(error);
561
+ const msg = `${timestamp} ${uidStr} ${chalk.red('✗')} ${chalk.blueBright('BROWSER')} ${chalk.white(httpMethod.padEnd(7))} ${chalk.gray(displayUrl)} ${chalk.red(errMsg)}`;
562
+ console.log(msg);
563
+ writeToFile(`[ERROR] BROWSER PROXY ${httpMethod} ${params.url} uid=${uid} success=false error="${errMsg}"`);
564
+ }
565
+ }
566
+
567
+ export default {
568
+ logFsRead,
569
+ logFsWrite,
570
+ logGitRead,
571
+ logGitWrite,
572
+ logTerminalRead,
573
+ logTerminalWrite,
574
+ logSearchRead,
575
+ logBrowserProxy,
576
+ logAuth,
577
+ logConnection,
578
+ };