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,218 @@
1
+ /**
2
+ * Project directory management with symlink support
3
+ * .spck-editor/ is a regular directory with .spck-editor/config symlinked to ~/.spck-editor/projects/{id}
4
+ * This prevents accidental git commits of secrets while avoiding cross-device link errors
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import * as crypto from 'crypto';
11
+ import { t } from '../i18n/index.js';
12
+
13
+ const PROJECT_DIR_NAME = '.spck-editor';
14
+ const CONFIG_SUBDIR_NAME = 'config';
15
+
16
+ /**
17
+ * Get the home base directory for projects
18
+ * Called at runtime instead of module load time to avoid issues in test environments
19
+ */
20
+ function getHomeBaseDir(): string {
21
+ return path.join(os.homedir(), '.spck-editor', 'projects');
22
+ }
23
+
24
+ /**
25
+ * Generate a consistent project ID from the project root path
26
+ */
27
+ export function generateProjectId(projectRoot: string): string {
28
+ // Resolve to absolute path and normalize
29
+ const absolutePath = path.resolve(projectRoot);
30
+
31
+ // Generate SHA256 hash of the absolute path
32
+ const hash = crypto.createHash('sha256').update(absolutePath).digest('hex');
33
+
34
+ // Use first 16 characters for readability (still unique enough)
35
+ return hash.substring(0, 16);
36
+ }
37
+
38
+ /**
39
+ * Get the home directory location for a project's data
40
+ */
41
+ export function getProjectDataPath(projectRoot: string): string {
42
+ const projectId = generateProjectId(projectRoot);
43
+ return path.join(getHomeBaseDir(), projectId);
44
+ }
45
+
46
+ /**
47
+ * Get the .spck-editor directory path in the project
48
+ */
49
+ export function getProjectDirPath(projectRoot: string): string {
50
+ return path.join(projectRoot, PROJECT_DIR_NAME);
51
+ }
52
+
53
+ /**
54
+ * Get the config symlink path (.spck-editor/config)
55
+ */
56
+ export function getConfigSymlinkPath(projectRoot: string): string {
57
+ return path.join(projectRoot, PROJECT_DIR_NAME, CONFIG_SUBDIR_NAME);
58
+ }
59
+
60
+ /**
61
+ * @deprecated Use getConfigSymlinkPath() instead
62
+ * Legacy compatibility - returns config symlink path
63
+ */
64
+ export function getProjectSymlinkPath(projectRoot: string): string {
65
+ return getConfigSymlinkPath(projectRoot);
66
+ }
67
+
68
+ /**
69
+ * Check if the project directory exists and is properly set up
70
+ */
71
+ export function isProjectDirSetup(projectRoot: string): boolean {
72
+ const projectDir = getProjectDirPath(projectRoot);
73
+ const configSymlink = getConfigSymlinkPath(projectRoot);
74
+ const dataPath = getProjectDataPath(projectRoot);
75
+
76
+ // Check if .spck-editor directory exists
77
+ if (!fs.existsSync(projectDir)) {
78
+ return false;
79
+ }
80
+
81
+ // Check if .spck-editor is a directory (not old-style symlink)
82
+ try {
83
+ const stats = fs.lstatSync(projectDir);
84
+ if (stats.isSymbolicLink()) {
85
+ // Old structure - needs migration
86
+ return false;
87
+ }
88
+ if (!stats.isDirectory()) {
89
+ return false;
90
+ }
91
+ } catch (error) {
92
+ return false;
93
+ }
94
+
95
+ // Check if config symlink exists
96
+ if (!fs.existsSync(configSymlink)) {
97
+ return false;
98
+ }
99
+
100
+ // Check if config is a symlink
101
+ try {
102
+ const stats = fs.lstatSync(configSymlink);
103
+ if (!stats.isSymbolicLink()) {
104
+ return false;
105
+ }
106
+ } catch (error) {
107
+ return false;
108
+ }
109
+
110
+ // Check if data directory exists
111
+ if (!fs.existsSync(dataPath)) {
112
+ return false;
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * Setup the project directory with config symlink
120
+ * Creates ~/.spck-editor/projects/{project_id}/ and symlinks .spck-editor/config to it
121
+ */
122
+ export function setupProjectDir(projectRoot: string): void {
123
+ const projectDir = getProjectDirPath(projectRoot);
124
+ const configSymlink = getConfigSymlinkPath(projectRoot);
125
+ const dataPath = getProjectDataPath(projectRoot);
126
+ const homeBaseDir = getHomeBaseDir();
127
+
128
+ // Ensure home base directory exists
129
+ if (!fs.existsSync(homeBaseDir)) {
130
+ fs.mkdirSync(homeBaseDir, { recursive: true, mode: 0o700 });
131
+ }
132
+
133
+ // Ensure project data directory exists
134
+ if (!fs.existsSync(dataPath)) {
135
+ fs.mkdirSync(dataPath, { recursive: true, mode: 0o700 });
136
+ }
137
+
138
+ // Create .spck-editor directory if it doesn't exist
139
+ if (!fs.existsSync(projectDir)) {
140
+ fs.mkdirSync(projectDir, { mode: 0o700 });
141
+ }
142
+
143
+ // Create subdirectories (.tmp, .trash, logs)
144
+ const subdirs = ['.tmp', '.trash', 'logs'];
145
+ for (const subdir of subdirs) {
146
+ const subdirPath = path.join(projectDir, subdir);
147
+ if (!fs.existsSync(subdirPath)) {
148
+ fs.mkdirSync(subdirPath, { mode: 0o700 });
149
+ }
150
+ }
151
+
152
+ // Create config symlink if it doesn't exist
153
+ if (!fs.existsSync(configSymlink)) {
154
+ fs.symlinkSync(dataPath, configSymlink, 'dir');
155
+ } else {
156
+ // Verify existing symlink points to correct location
157
+ const stats = fs.lstatSync(configSymlink);
158
+ if (stats.isSymbolicLink()) {
159
+ const target = fs.readlinkSync(configSymlink);
160
+ if (target !== dataPath) {
161
+ // Points to wrong location - remove and recreate
162
+ fs.unlinkSync(configSymlink);
163
+ fs.symlinkSync(dataPath, configSymlink, 'dir');
164
+ }
165
+ }
166
+ }
167
+
168
+ console.log(`✅ ${t('projectDir.configured')}`);
169
+ console.log(` ${t('projectDir.directory', { path: projectDir })}`);
170
+ console.log(` ${t('projectDir.configLink', { symlink: configSymlink, dataPath })}\n`);
171
+ }
172
+
173
+ /**
174
+ * Ensure project directory is set up, creating if needed
175
+ */
176
+ export function ensureProjectDir(projectRoot: string): void {
177
+ if (!isProjectDirSetup(projectRoot)) {
178
+ setupProjectDir(projectRoot);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Get the absolute path to a file within the project directory
184
+ * Files go in .spck-editor/{filename} (local) or .spck-editor/config/{filename} (symlinked)
185
+ * Config files (like connection-settings.json) go in the config subdirectory
186
+ */
187
+ export function getProjectFilePath(projectRoot: string, filename: string): string {
188
+ const projectDir = getProjectDirPath(projectRoot);
189
+
190
+ // Config files go in the symlinked config directory
191
+ const configFiles = ['connection-settings.json', '.credentials.json', 'spck-cli.config.json'];
192
+ if (configFiles.includes(filename)) {
193
+ const configSymlink = getConfigSymlinkPath(projectRoot);
194
+ return path.join(configSymlink, filename);
195
+ }
196
+
197
+ // All other files go in the local .spck-editor directory
198
+ return path.join(projectDir, filename);
199
+ }
200
+
201
+ /**
202
+ * Remove project directory and all associated data
203
+ * WARNING: This deletes the data directory in home and the local .spck-editor directory
204
+ */
205
+ export function removeProjectDir(projectRoot: string): void {
206
+ const projectDir = getProjectDirPath(projectRoot);
207
+ const dataPath = getProjectDataPath(projectRoot);
208
+
209
+ // Remove .spck-editor directory if exists (includes config symlink and local files)
210
+ if (fs.existsSync(projectDir)) {
211
+ fs.rmSync(projectDir, { recursive: true, force: true });
212
+ }
213
+
214
+ // Remove data directory in home if exists
215
+ if (fs.existsSync(dataPath)) {
216
+ fs.rmSync(dataPath, { recursive: true, force: true });
217
+ }
218
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Ripgrep wrapper utility
3
+ *
4
+ * Uses system-installed ripgrep (rg) if available
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+
9
+ /**
10
+ * Get the ripgrep command name for the current platform
11
+ */
12
+ export function getRipgrepCommand(): string {
13
+ // On Windows, try both 'rg.exe' and 'rg'
14
+ // On Unix-like systems, use 'rg'
15
+ return process.platform === 'win32' ? 'rg.exe' : 'rg';
16
+ }
17
+
18
+ /**
19
+ * Check if ripgrep is available on the system PATH
20
+ */
21
+ export async function isRipgrepAvailable(): Promise<boolean> {
22
+ const rgCommand = getRipgrepCommand();
23
+
24
+ // Try to execute ripgrep --version to verify it works
25
+ return new Promise((resolve) => {
26
+ try {
27
+ const proc = spawn(rgCommand, ['--version'], {
28
+ cwd: process.cwd(),
29
+ stdio: 'ignore',
30
+ shell: false
31
+ });
32
+
33
+ proc.on('error', () => resolve(false));
34
+ proc.on('close', (code) => resolve(code === 0));
35
+ } catch (err) {
36
+ resolve(false)
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Execute ripgrep search (buffered - waits for completion)
43
+ */
44
+ export async function executeRipgrep(
45
+ path: string,
46
+ args: string[],
47
+ options?: {
48
+ timeout?: number;
49
+ }
50
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
51
+ const rgCommand = getRipgrepCommand();
52
+
53
+ return new Promise((resolve, reject) => {
54
+ const proc = spawn(rgCommand, args, {
55
+ cwd: path,
56
+ stdio: 'pipe',
57
+ shell: false
58
+ });
59
+
60
+ let stdout = '';
61
+ let stderr = '';
62
+ let killed = false;
63
+
64
+ // Set timeout if specified
65
+ const timeout = options?.timeout;
66
+ let timeoutId: NodeJS.Timeout | undefined;
67
+ if (timeout) {
68
+ timeoutId = setTimeout(() => {
69
+ killed = true;
70
+ proc.kill();
71
+ reject(new Error(`Ripgrep execution timed out after ${timeout}ms`));
72
+ }, timeout);
73
+ }
74
+
75
+ proc.stdout?.on('data', (data) => {
76
+ stdout += data.toString();
77
+ });
78
+
79
+ proc.stderr?.on('data', (data) => {
80
+ stderr += data.toString();
81
+ });
82
+
83
+ proc.on('error', (error) => {
84
+ if (timeoutId) clearTimeout(timeoutId);
85
+ if (!killed) {
86
+ reject(error);
87
+ }
88
+ });
89
+
90
+ proc.on('close', (code) => {
91
+ if (timeoutId) clearTimeout(timeoutId);
92
+ if (!killed) {
93
+ resolve({
94
+ stdout,
95
+ stderr,
96
+ exitCode: code ?? -1
97
+ });
98
+ }
99
+ });
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Execute ripgrep search with streaming output
105
+ * Calls onLine for each line of output as it arrives
106
+ */
107
+ export async function executeRipgrepStream(
108
+ path: string,
109
+ args: string[],
110
+ options: {
111
+ timeout?: number;
112
+ onLine: (line: string) => void;
113
+ }
114
+ ): Promise<{ exitCode: number }> {
115
+ const rgCommand = getRipgrepCommand();
116
+
117
+ return new Promise((resolve, reject) => {
118
+ const proc = spawn(rgCommand, args, {
119
+ cwd: path,
120
+ stdio: 'pipe',
121
+ shell: false
122
+ });
123
+
124
+ let killed = false;
125
+ let buffer = '';
126
+
127
+ // Set timeout if specified
128
+ const timeout = options.timeout;
129
+ let timeoutId: NodeJS.Timeout | undefined;
130
+ if (timeout) {
131
+ timeoutId = setTimeout(() => {
132
+ killed = true;
133
+ proc.kill();
134
+ reject(new Error(`Ripgrep execution timed out after ${timeout}ms`));
135
+ }, timeout);
136
+ }
137
+
138
+ // Process stdout line by line as it arrives
139
+ proc.stdout?.on('data', (data) => {
140
+ buffer += data.toString();
141
+ const lines = buffer.split('\n');
142
+
143
+ // Keep the last incomplete line in the buffer
144
+ buffer = lines.pop() || '';
145
+
146
+ // Process complete lines
147
+ for (const line of lines) {
148
+ if (line.trim()) {
149
+ options.onLine(line);
150
+ }
151
+ }
152
+ });
153
+
154
+ proc.stderr?.on('data', (_data) => {
155
+ // Ignore stderr for now
156
+ });
157
+
158
+ proc.on('error', (error) => {
159
+ if (timeoutId) clearTimeout(timeoutId);
160
+ if (!killed) {
161
+ reject(error);
162
+ }
163
+ });
164
+
165
+ proc.on('close', (code) => {
166
+ if (timeoutId) clearTimeout(timeoutId);
167
+
168
+ // Process any remaining buffered data
169
+ if (buffer.trim()) {
170
+ options.onLine(buffer);
171
+ }
172
+
173
+ if (!killed) {
174
+ resolve({
175
+ exitCode: code ?? -1
176
+ });
177
+ }
178
+ });
179
+ });
180
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Tool detection for git and ripgrep
3
+ * Checks if required tools are installed and displays warnings
4
+ */
5
+
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import { ToolDetectionResult } from '../types.js';
9
+ import { t } from '../i18n/index.js';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * Check if a command is available
15
+ */
16
+ async function isCommandAvailable(command: string): Promise<boolean> {
17
+ try {
18
+ await execAsync(`${command} --version`);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Detect available tools (git and ripgrep)
27
+ */
28
+ export async function detectTools(options?: {
29
+ disableGit?: boolean;
30
+ disableRipgrep?: boolean;
31
+ }): Promise<ToolDetectionResult> {
32
+ console.log(`\n=== ${t('tools.title')} ===\n`);
33
+
34
+ const result: ToolDetectionResult = {
35
+ git: false,
36
+ ripgrep: false
37
+ };
38
+
39
+ // Check Git (unless force-disabled for development)
40
+ if (options?.disableGit) {
41
+ console.log(`⚠️ ${t('tools.gitForceDisabled')}`);
42
+ } else {
43
+ result.git = await isCommandAvailable('git');
44
+
45
+ if (result.git) {
46
+ try {
47
+ const { stdout } = await execAsync('git --version');
48
+ console.log(`✅ ${t('tools.gitDetected', { version: stdout.trim() })}`);
49
+ } catch {
50
+ console.log(`✅ ${t('tools.gitDetectedShort')}`);
51
+ }
52
+ } else {
53
+ console.warn(`⚠️ ${t('tools.gitNotDetected')}`);
54
+ console.warn(` ${t('tools.gitDisabledHint')}`);
55
+ console.warn(` ${t('tools.gitInstallHint')}`);
56
+ console.warn(` ${t('tools.gitInstallUrl')}\n`);
57
+ }
58
+ }
59
+
60
+ // Check Ripgrep (unless force-disabled for development)
61
+ if (options?.disableRipgrep) {
62
+ console.log(`⚠️ ${t('tools.ripgrepForceDisabled')}`);
63
+ } else {
64
+ result.ripgrep = await isCommandAvailable('rg');
65
+
66
+ if (result.ripgrep) {
67
+ try {
68
+ const { stdout } = await execAsync('rg --version');
69
+ const firstLine = stdout.split('\n')[0];
70
+ console.log(`✅ ${t('tools.ripgrepDetected', { version: firstLine })}`);
71
+ } catch {
72
+ console.log(`✅ ${t('tools.ripgrepDetectedShort')}`);
73
+ }
74
+ } else {
75
+ console.warn(`⚠️ ${t('tools.ripgrepNotDetected')}`);
76
+ console.warn(` ${t('tools.ripgrepDisabledHint')}`);
77
+ console.warn(` ${t('tools.ripgrepInstallHint')}`);
78
+ console.warn(` ${t('tools.ripgrepInstallUrl')}\n`);
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Display feature summary based on detected tools
87
+ */
88
+ export function displayFeatureSummary(
89
+ tools: ToolDetectionResult,
90
+ terminalEnabled: boolean,
91
+ userAuthEnabled?: boolean,
92
+ browserProxyEnabled?: boolean
93
+ ): void {
94
+ console.log(`\n=== ${t('features.title')} ===\n`);
95
+
96
+ const features: string[] = [];
97
+
98
+ // Always available
99
+ features.push(`✅ ${t('features.filesystem')}`);
100
+
101
+ // Conditional features
102
+ if (tools.git) {
103
+ features.push(`✅ ${t('features.gitEnabled')}`);
104
+ } else {
105
+ features.push(`❌ ${t('features.gitDisabled')}`);
106
+ }
107
+
108
+ if (tools.ripgrep) {
109
+ features.push(`✅ ${t('features.searchFast')}`);
110
+ } else {
111
+ features.push(`⚠️ ${t('features.searchBasic')}`);
112
+ }
113
+
114
+ if (terminalEnabled) {
115
+ features.push(`✅ ${t('features.terminalEnabled')}`);
116
+ } else {
117
+ features.push(`❌ ${t('features.terminalDisabled')}`);
118
+ }
119
+
120
+ if (browserProxyEnabled !== false) {
121
+ features.push(`✅ ${t('features.browserProxyEnabled')}`);
122
+ } else {
123
+ features.push(`❌ ${t('features.browserProxyDisabled')}`);
124
+ }
125
+
126
+ features.forEach(feature => console.log(` ${feature}`));
127
+
128
+ // Display authentication mode
129
+ console.log(`\n=== ${t('features.securityTitle')} ===\n`);
130
+ if (userAuthEnabled) {
131
+ console.log(` 🔐 ${t('features.userAuthEnabled')}`);
132
+ console.log(` → ${t('features.userAuthEnabledHint1')}`);
133
+ console.log(` → ${t('features.userAuthEnabledHint2')}`);
134
+ console.log(` → ${t('features.userAuthEnabledHint3')}\n`);
135
+ } else {
136
+ console.log(` 🔓 ${t('features.userAuthDisabled')}`);
137
+ console.log(` → ${t('features.userAuthDisabledHint1')}`);
138
+ console.log(` → ${t('features.userAuthDisabledHint2')}`);
139
+ console.log(` → ${t('features.userAuthDisabledHint3')}\n`);
140
+ }
141
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * File system watcher using chokidar
3
+ */
4
+
5
+ import * as chokidar from 'chokidar';
6
+ import { EventEmitter } from 'events';
7
+
8
+ export class FileWatcher extends EventEmitter {
9
+ private watcher: chokidar.FSWatcher;
10
+
11
+ constructor(rootPath: string, ignorePatterns: string[]) {
12
+ super();
13
+
14
+ this.watcher = chokidar.watch(rootPath, {
15
+ ignored: ignorePatterns,
16
+ persistent: true,
17
+ ignoreInitial: true,
18
+ awaitWriteFinish: {
19
+ stabilityThreshold: 100,
20
+ pollInterval: 50,
21
+ },
22
+ });
23
+
24
+ this.watcher.on('change', (path, stats) => {
25
+ this.emit('change', path, stats?.mtimeMs);
26
+ });
27
+
28
+ this.watcher.on('unlink', (path) => {
29
+ this.emit('removed', path);
30
+ });
31
+
32
+ this.watcher.on('add', (path, stats) => {
33
+ this.emit('added', path, stats?.mtimeMs);
34
+ });
35
+
36
+ this.watcher.on('unlinkDir', (path) => {
37
+ this.emit('removed', path);
38
+ });
39
+
40
+ this.watcher.on('addDir', (path) => {
41
+ this.emit('added', path);
42
+ });
43
+
44
+ this.watcher.on('error', (error) => {
45
+ console.error('File watcher error:', error);
46
+ this.emit('error', error);
47
+ });
48
+ }
49
+
50
+ close(): void {
51
+ this.watcher.close();
52
+ }
53
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2021",
4
+ "module": "ES2020",
5
+ "lib": ["ES2021"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node",
17
+ "types": []
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": [
21
+ "node_modules",
22
+ "dist"
23
+ ]
24
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ testTimeout: 10000,
7
+ server: {
8
+ deps: {
9
+ inline: ['default-shell', 'os-locale'],
10
+ },
11
+ },
12
+ coverage: {
13
+ include: ['src/**/*.ts'],
14
+ exclude: ['src/**/*.d.ts', 'src/**/__tests__/**'],
15
+ reportsDirectory: 'coverage',
16
+ reporter: ['text', 'lcov', 'html'],
17
+ },
18
+ },
19
+ });