kiro-mobile-bridge 1.0.7 → 1.0.10
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/README.md +16 -24
- package/package.json +1 -1
- package/src/public/index.html +1414 -1628
- package/src/routes/api.js +539 -0
- package/src/server.js +287 -2593
- package/src/services/cdp.js +210 -0
- package/src/services/click.js +533 -0
- package/src/services/message.js +214 -0
- package/src/services/snapshot.js +370 -0
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +34 -0
- package/src/utils/network.js +64 -0
- package/src/utils/security.js +160 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Routes - REST endpoints for mobile client
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { injectMessage } from '../services/message.js';
|
|
9
|
+
import { clickElement } from '../services/click.js';
|
|
10
|
+
import {
|
|
11
|
+
validatePathWithinRoot,
|
|
12
|
+
sanitizeClickInfo,
|
|
13
|
+
validateMessage,
|
|
14
|
+
sanitizeFilePath
|
|
15
|
+
} from '../utils/security.js';
|
|
16
|
+
import {
|
|
17
|
+
CODE_EXTENSIONS,
|
|
18
|
+
EXTENSION_TO_LANGUAGE,
|
|
19
|
+
getLanguageFromExtension,
|
|
20
|
+
isCodeFile,
|
|
21
|
+
MAX_FILE_SEARCH_DEPTH,
|
|
22
|
+
MAX_WORKSPACE_DEPTH
|
|
23
|
+
} from '../utils/constants.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create API router
|
|
27
|
+
* @param {Map} cascades - Cascade connections map
|
|
28
|
+
* @param {object} mainWindowCDP - Main window CDP connection
|
|
29
|
+
* @returns {Router} - Express router
|
|
30
|
+
*/
|
|
31
|
+
export function createApiRouter(cascades, mainWindowCDP) {
|
|
32
|
+
const router = Router();
|
|
33
|
+
|
|
34
|
+
// GET /snapshot/:id - Get HTML snapshot for a cascade
|
|
35
|
+
router.get('/snapshot/:id', (req, res) => {
|
|
36
|
+
const cascade = cascades.get(req.params.id);
|
|
37
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
38
|
+
if (!cascade.snapshot) return res.status(404).json({ error: 'No snapshot available' });
|
|
39
|
+
res.json(cascade.snapshot);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// GET /styles/:id - Get CSS for a cascade
|
|
43
|
+
router.get('/styles/:id', (req, res) => {
|
|
44
|
+
const cascade = cascades.get(req.params.id);
|
|
45
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
46
|
+
if (!cascade.css) return res.status(404).json({ error: 'No styles available' });
|
|
47
|
+
res.type('text/css').send(cascade.css);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// GET /editor/:id - Get editor snapshot
|
|
51
|
+
router.get('/editor/:id', (req, res) => {
|
|
52
|
+
const cascade = cascades.get(req.params.id);
|
|
53
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
54
|
+
if (!cascade.editor?.hasContent) return res.status(404).json({ error: 'No editor content available' });
|
|
55
|
+
res.json(cascade.editor);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// POST /send/:id - Send message to chat
|
|
59
|
+
router.post('/send/:id', async (req, res) => {
|
|
60
|
+
const cascade = cascades.get(req.params.id);
|
|
61
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
62
|
+
|
|
63
|
+
const { message } = req.body;
|
|
64
|
+
|
|
65
|
+
// Validate message input
|
|
66
|
+
const validation = validateMessage(message);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
return res.status(400).json({ error: validation.error });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!cascade.cdp) return res.status(503).json({ error: 'CDP connection not available' });
|
|
72
|
+
|
|
73
|
+
// Log message length only, not content (security)
|
|
74
|
+
console.log(`[Send] Message to ${req.params.id}: ${message.length} chars`);
|
|
75
|
+
const result = await injectMessage(cascade.cdp, message);
|
|
76
|
+
|
|
77
|
+
if (result.success) {
|
|
78
|
+
res.json({ success: true, method: result.method });
|
|
79
|
+
} else {
|
|
80
|
+
res.status(500).json({ success: false, error: result.error });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// POST /click/:id - Click UI element
|
|
85
|
+
router.post('/click/:id', async (req, res) => {
|
|
86
|
+
const cascade = cascades.get(req.params.id);
|
|
87
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
88
|
+
if (!cascade.cdp?.rootContextId) return res.status(503).json({ error: 'CDP not available' });
|
|
89
|
+
|
|
90
|
+
// Validate and sanitize click info
|
|
91
|
+
const { valid, sanitized, error } = sanitizeClickInfo(req.body);
|
|
92
|
+
if (!valid) {
|
|
93
|
+
return res.status(400).json({ error: error || 'Invalid click info' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`[Click] ${sanitized.text?.substring(0, 30) || sanitized.ariaLabel || sanitized.tag || 'element'}`);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await clickElement(cascade.cdp, sanitized);
|
|
100
|
+
res.json(result);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[Click] Error:', err.message);
|
|
103
|
+
res.status(500).json({ success: false, error: err.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// GET /debug-model/:id - Debug model selector structure
|
|
108
|
+
router.get('/debug-model/:id', async (req, res) => {
|
|
109
|
+
const cascade = cascades.get(req.params.id);
|
|
110
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
111
|
+
if (!cascade.cdp?.rootContextId) return res.status(503).json({ error: 'CDP not available' });
|
|
112
|
+
|
|
113
|
+
const script = `(function() {
|
|
114
|
+
let targetDoc = document;
|
|
115
|
+
const activeFrame = document.getElementById('active-frame');
|
|
116
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
117
|
+
|
|
118
|
+
const results = {
|
|
119
|
+
dropdownOpen: false,
|
|
120
|
+
dropdownItems: [],
|
|
121
|
+
dropdownContainer: null,
|
|
122
|
+
autopilotToggle: null
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Check if dropdown is open
|
|
126
|
+
const openDropdown = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"], [class*="dropdown-content"], [role="listbox"], [role="menu"]');
|
|
127
|
+
if (openDropdown && openDropdown.offsetParent !== null) {
|
|
128
|
+
results.dropdownOpen = true;
|
|
129
|
+
results.dropdownContainer = {
|
|
130
|
+
tag: openDropdown.tagName,
|
|
131
|
+
className: (openDropdown.className || '').substring(0, 100),
|
|
132
|
+
role: openDropdown.getAttribute('role'),
|
|
133
|
+
innerHTML: openDropdown.innerHTML.substring(0, 1000)
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Find all items in the dropdown
|
|
137
|
+
const items = openDropdown.querySelectorAll('.kiro-dropdown-item, [role="option"], [role="menuitem"], [class*="dropdown-item"], [class*="menu-item"], > div, > button');
|
|
138
|
+
items.forEach(item => {
|
|
139
|
+
results.dropdownItems.push({
|
|
140
|
+
tag: item.tagName,
|
|
141
|
+
className: (item.className || '').substring(0, 80),
|
|
142
|
+
text: (item.textContent || '').substring(0, 100),
|
|
143
|
+
role: item.getAttribute('role'),
|
|
144
|
+
dataValue: item.getAttribute('data-value'),
|
|
145
|
+
onclick: !!item.onclick,
|
|
146
|
+
cursor: window.getComputedStyle(item).cursor
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Find Autopilot toggle
|
|
152
|
+
const autopilotElements = targetDoc.querySelectorAll('[class*="toggle"], input[type="checkbox"]');
|
|
153
|
+
for (const el of autopilotElements) {
|
|
154
|
+
const parent = el.closest('.kiro-toggle-switch, [class*="toggle"]');
|
|
155
|
+
if (parent) {
|
|
156
|
+
const label = parent.querySelector('label');
|
|
157
|
+
const labelText = label ? label.textContent.toLowerCase() : '';
|
|
158
|
+
if (labelText.includes('autopilot') || labelText.includes('auto')) {
|
|
159
|
+
results.autopilotToggle = {
|
|
160
|
+
toggleElement: {
|
|
161
|
+
tag: el.tagName,
|
|
162
|
+
type: el.type,
|
|
163
|
+
className: (el.className || '').substring(0, 80),
|
|
164
|
+
checked: el.checked,
|
|
165
|
+
id: el.id
|
|
166
|
+
},
|
|
167
|
+
parentElement: {
|
|
168
|
+
tag: parent.tagName,
|
|
169
|
+
className: (parent.className || '').substring(0, 80),
|
|
170
|
+
onclick: !!parent.onclick
|
|
171
|
+
},
|
|
172
|
+
labelText: labelText
|
|
173
|
+
};
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return results;
|
|
180
|
+
})()`;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await cascade.cdp.call('Runtime.evaluate', {
|
|
184
|
+
expression: script,
|
|
185
|
+
contextId: cascade.cdp.rootContextId,
|
|
186
|
+
returnByValue: true
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const data = result.result?.value;
|
|
190
|
+
console.log('[Debug] UI structure:', JSON.stringify(data, null, 2));
|
|
191
|
+
res.json(data);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
res.status(500).json({ error: err.message });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// POST /readFile/:id - Read file from filesystem
|
|
198
|
+
router.post('/readFile/:id', async (req, res) => {
|
|
199
|
+
const cascade = cascades.get(req.params.id);
|
|
200
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
201
|
+
|
|
202
|
+
const { filePath } = req.body;
|
|
203
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
204
|
+
return res.status(400).json({ error: 'filePath is required and must be a string' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sanitize the file path
|
|
208
|
+
const sanitizedPath = sanitizeFilePath(filePath);
|
|
209
|
+
if (!sanitizedPath) {
|
|
210
|
+
return res.status(400).json({ error: 'Invalid file path' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`[ReadFile] Request for: ${sanitizedPath}`);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// Get workspace root
|
|
217
|
+
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
|
|
218
|
+
|
|
219
|
+
// SECURITY: Validate path is within workspace root
|
|
220
|
+
const pathValidation = validatePathWithinRoot(sanitizedPath, workspaceRoot);
|
|
221
|
+
|
|
222
|
+
let content = null;
|
|
223
|
+
let foundPath = null;
|
|
224
|
+
|
|
225
|
+
if (pathValidation.valid) {
|
|
226
|
+
// Try the validated path first
|
|
227
|
+
try {
|
|
228
|
+
content = await fs.readFile(pathValidation.resolvedPath, 'utf-8');
|
|
229
|
+
foundPath = pathValidation.resolvedPath;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// File doesn't exist at validated path, try searching
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If not found, search within workspace only
|
|
236
|
+
if (!content) {
|
|
237
|
+
const fileName = path.basename(sanitizedPath);
|
|
238
|
+
foundPath = await findFileRecursive(workspaceRoot, fileName, MAX_FILE_SEARCH_DEPTH);
|
|
239
|
+
|
|
240
|
+
if (foundPath) {
|
|
241
|
+
// Validate the found path is still within workspace
|
|
242
|
+
const foundValidation = validatePathWithinRoot(foundPath, workspaceRoot);
|
|
243
|
+
if (foundValidation.valid) {
|
|
244
|
+
content = await fs.readFile(foundPath, 'utf-8');
|
|
245
|
+
} else {
|
|
246
|
+
console.warn(`[ReadFile] Found file outside workspace: ${foundPath}`);
|
|
247
|
+
foundPath = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!content) {
|
|
253
|
+
return res.status(404).json({ error: 'File not found within workspace' });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const language = getLanguageFromExtension(sanitizedPath);
|
|
257
|
+
|
|
258
|
+
res.json({
|
|
259
|
+
content,
|
|
260
|
+
fileName: path.basename(sanitizedPath),
|
|
261
|
+
fullPath: foundPath,
|
|
262
|
+
language,
|
|
263
|
+
lineCount: content.split('\n').length,
|
|
264
|
+
hasContent: true
|
|
265
|
+
});
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error('[ReadFile] Error:', err.message);
|
|
268
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// GET /files/:id - List workspace files
|
|
273
|
+
router.get('/files/:id', async (req, res) => {
|
|
274
|
+
const cascade = cascades.get(req.params.id);
|
|
275
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
|
|
279
|
+
const files = await collectWorkspaceFiles(workspaceRoot);
|
|
280
|
+
|
|
281
|
+
console.log(`[Files] Found ${files.length} files in ${workspaceRoot}`);
|
|
282
|
+
res.json({ files, workspaceRoot });
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error('[Files] Error:', err.message);
|
|
285
|
+
res.status(500).json({ error: err.message });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// GET /tasks/:id - List task files from .kiro/specs
|
|
290
|
+
router.get('/tasks/:id', async (req, res) => {
|
|
291
|
+
const cascade = cascades.get(req.params.id);
|
|
292
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
|
|
296
|
+
const kiroSpecsPath = path.join(workspaceRoot, '.kiro', 'specs');
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await fs.access(kiroSpecsPath);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return res.json({ tasks: [], workspaceRoot });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tasks = [];
|
|
305
|
+
const specDirs = await fs.readdir(kiroSpecsPath, { withFileTypes: true });
|
|
306
|
+
|
|
307
|
+
for (const dir of specDirs) {
|
|
308
|
+
if (!dir.isDirectory()) continue;
|
|
309
|
+
const tasksFilePath = path.join(kiroSpecsPath, dir.name, 'tasks.md');
|
|
310
|
+
try {
|
|
311
|
+
const content = await fs.readFile(tasksFilePath, 'utf-8');
|
|
312
|
+
tasks.push({ name: dir.name, path: `.kiro/specs/${dir.name}/tasks.md`, content });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
// Task file doesn't exist, skip
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
319
|
+
console.log(`[Tasks] Found ${tasks.length} task files`);
|
|
320
|
+
res.json({ tasks, workspaceRoot });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error('[Tasks] Error:', err.message);
|
|
323
|
+
res.status(500).json({ error: err.message });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// POST /open-spec/:id - Open spec in Kiro
|
|
328
|
+
router.post('/open-spec/:id', async (req, res) => {
|
|
329
|
+
const cascade = cascades.get(req.params.id);
|
|
330
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
331
|
+
|
|
332
|
+
const { specName } = req.body;
|
|
333
|
+
if (!specName || typeof specName !== 'string') {
|
|
334
|
+
return res.status(400).json({ error: 'specName is required and must be a string' });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Sanitize spec name (alphanumeric, hyphens, underscores only)
|
|
338
|
+
const sanitizedSpecName = specName.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 100);
|
|
339
|
+
if (!sanitizedSpecName) {
|
|
340
|
+
return res.status(400).json({ error: 'Invalid spec name' });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const cdp = cascade.cdp;
|
|
344
|
+
if (!cdp?.rootContextId) return res.status(503).json({ error: 'CDP not connected' });
|
|
345
|
+
|
|
346
|
+
console.log(`[OpenSpec] Opening ${sanitizedSpecName}`);
|
|
347
|
+
|
|
348
|
+
// Try to click on spec in sidebar
|
|
349
|
+
const script = `(function() {
|
|
350
|
+
let targetDoc = document;
|
|
351
|
+
const activeFrame = document.getElementById('active-frame');
|
|
352
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
353
|
+
|
|
354
|
+
const specName = '${sanitizedSpecName}';
|
|
355
|
+
const allElements = targetDoc.querySelectorAll('*');
|
|
356
|
+
|
|
357
|
+
for (const el of allElements) {
|
|
358
|
+
const text = (el.textContent || '').trim();
|
|
359
|
+
if (text === specName || text.includes(specName)) {
|
|
360
|
+
if (el.offsetParent !== null && el.textContent.length < 100) {
|
|
361
|
+
el.click();
|
|
362
|
+
return { success: true, method: 'click-spec-name' };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { success: false, error: 'Spec not found in UI' };
|
|
367
|
+
})()`;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
371
|
+
expression: script,
|
|
372
|
+
contextId: cdp.rootContextId,
|
|
373
|
+
returnByValue: true
|
|
374
|
+
});
|
|
375
|
+
res.json(result.result?.value || { success: false });
|
|
376
|
+
} catch (err) {
|
|
377
|
+
res.status(500).json({ success: false, error: err.message });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return router;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get workspace root from VS Code window title
|
|
386
|
+
* Uses environment-based paths instead of hardcoded values
|
|
387
|
+
* @param {object} mainWindowCDP - Main window CDP connection
|
|
388
|
+
* @returns {Promise<string|null>}
|
|
389
|
+
*/
|
|
390
|
+
async function getWorkspaceRoot(mainWindowCDP) {
|
|
391
|
+
if (!mainWindowCDP.connection?.rootContextId) return null;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const result = await mainWindowCDP.connection.call('Runtime.evaluate', {
|
|
395
|
+
expression: 'document.title',
|
|
396
|
+
contextId: mainWindowCDP.connection.rootContextId,
|
|
397
|
+
returnByValue: true
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const title = result.result?.value || '';
|
|
401
|
+
const parts = title.split(' - ');
|
|
402
|
+
if (parts.length >= 2) {
|
|
403
|
+
const folderName = parts[parts.length - 2].trim();
|
|
404
|
+
const homeDir = os.homedir();
|
|
405
|
+
|
|
406
|
+
// Build possible roots from environment, not hardcoded paths
|
|
407
|
+
const possibleRoots = [
|
|
408
|
+
process.cwd(),
|
|
409
|
+
path.join(homeDir, folderName),
|
|
410
|
+
path.join(homeDir, 'projects', folderName),
|
|
411
|
+
path.join(homeDir, 'dev', folderName),
|
|
412
|
+
path.join(homeDir, 'workspace', folderName),
|
|
413
|
+
path.join(homeDir, 'code', folderName)
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
// Add Windows-specific paths if on Windows
|
|
417
|
+
if (process.platform === 'win32') {
|
|
418
|
+
const drives = ['C:', 'D:', 'E:'];
|
|
419
|
+
for (const drive of drives) {
|
|
420
|
+
possibleRoots.push(
|
|
421
|
+
path.join(drive, 'dev', folderName),
|
|
422
|
+
path.join(drive, 'projects', folderName),
|
|
423
|
+
path.join(drive, 'workspace', folderName)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const root of possibleRoots) {
|
|
429
|
+
try {
|
|
430
|
+
const stat = await fs.stat(root);
|
|
431
|
+
if (stat.isDirectory()) {
|
|
432
|
+
const hasKiro = await fs.access(path.join(root, '.kiro')).then(() => true).catch(() => false);
|
|
433
|
+
const hasPackage = await fs.access(path.join(root, 'package.json')).then(() => true).catch(() => false);
|
|
434
|
+
const hasGit = await fs.access(path.join(root, '.git')).then(() => true).catch(() => false);
|
|
435
|
+
if (hasKiro || hasPackage || hasGit) return root;
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
// Path doesn't exist, continue
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error('[getWorkspaceRoot] Error:', e.message);
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Find file recursively within a directory
|
|
450
|
+
* @param {string} dir - Directory to search
|
|
451
|
+
* @param {string} fileName - File name to find
|
|
452
|
+
* @param {number} maxDepth - Maximum search depth
|
|
453
|
+
* @param {number} currentDepth - Current depth (internal)
|
|
454
|
+
* @returns {Promise<string|null>}
|
|
455
|
+
*/
|
|
456
|
+
async function findFileRecursive(dir, fileName, maxDepth = MAX_FILE_SEARCH_DEPTH, currentDepth = 0) {
|
|
457
|
+
if (currentDepth > maxDepth) return null;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
461
|
+
|
|
462
|
+
// Check files first
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
if (entry.isFile() && entry.name === fileName) {
|
|
465
|
+
return path.join(dir, entry.name);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Then recurse into directories
|
|
470
|
+
for (const entry of entries) {
|
|
471
|
+
if (entry.isDirectory() &&
|
|
472
|
+
(!entry.name.startsWith('.') || entry.name === '.kiro') &&
|
|
473
|
+
entry.name !== 'node_modules' &&
|
|
474
|
+
entry.name !== 'dist' &&
|
|
475
|
+
entry.name !== 'build' &&
|
|
476
|
+
entry.name !== '.git') {
|
|
477
|
+
const found = await findFileRecursive(
|
|
478
|
+
path.join(dir, entry.name),
|
|
479
|
+
fileName,
|
|
480
|
+
maxDepth,
|
|
481
|
+
currentDepth + 1
|
|
482
|
+
);
|
|
483
|
+
if (found) return found;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
// Directory not accessible, skip
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Collect workspace files for file tree
|
|
494
|
+
* @param {string} workspaceRoot - Workspace root directory
|
|
495
|
+
* @returns {Promise<Array<{name: string, path: string, language: string}>>}
|
|
496
|
+
*/
|
|
497
|
+
async function collectWorkspaceFiles(workspaceRoot) {
|
|
498
|
+
const files = [];
|
|
499
|
+
|
|
500
|
+
async function collect(dir, relativePath = '', depth = 0) {
|
|
501
|
+
if (depth > MAX_WORKSPACE_DEPTH) return;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
505
|
+
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
// Skip hidden files (except .kiro and .github), node_modules, and build directories
|
|
508
|
+
if ((entry.name.startsWith('.') && entry.name !== '.kiro' && entry.name !== '.github') ||
|
|
509
|
+
entry.name === 'node_modules' ||
|
|
510
|
+
entry.name === 'dist' ||
|
|
511
|
+
entry.name === 'build' ||
|
|
512
|
+
entry.name === '__pycache__') {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const entryPath = path.join(dir, entry.name);
|
|
517
|
+
const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
518
|
+
|
|
519
|
+
if (entry.isFile()) {
|
|
520
|
+
if (isCodeFile(entry.name)) {
|
|
521
|
+
files.push({
|
|
522
|
+
name: entry.name,
|
|
523
|
+
path: entryRelative,
|
|
524
|
+
language: getLanguageFromExtension(entry.name)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
} else if (entry.isDirectory()) {
|
|
528
|
+
await collect(entryPath, entryRelative, depth + 1);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
// Directory not accessible, skip
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
await collect(workspaceRoot);
|
|
537
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
538
|
+
return files;
|
|
539
|
+
}
|