atoo-studio 0.0.2 → 0.0.4

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 (73) hide show
  1. package/README.github.md +8 -2
  2. package/README.md +7 -1
  3. package/README.npm.md +7 -1
  4. package/dist/src/config.d.ts +1 -2
  5. package/dist/src/config.js +1 -2
  6. package/dist/src/handlers/projects.js +61 -2
  7. package/dist/src/services/file-search.d.ts +59 -0
  8. package/dist/src/services/file-search.js +269 -0
  9. package/dist/src/services/git-ops.d.ts +8 -0
  10. package/dist/src/services/git-ops.js +33 -4
  11. package/dist/src/services/remote-git-ops.d.ts +4 -0
  12. package/dist/src/services/remote-git-ops.js +7 -4
  13. package/dist/src/services/ssh-manager.js +1 -2
  14. package/frontend/dist/assets/{_basePickBy-B-LibQ4-.js → _basePickBy-DVcFyvUy.js} +1 -1
  15. package/frontend/dist/assets/{_baseUniq-CprifHap.js → _baseUniq-BlaoUomR.js} +1 -1
  16. package/frontend/dist/assets/architecture-PBZL5I3N-BExHjBcm.js +1 -0
  17. package/frontend/dist/assets/{architectureDiagram-2XIMDMQ5-DiHPxX4j.js → architectureDiagram-2XIMDMQ5-BNx6-oUQ.js} +1 -1
  18. package/frontend/dist/assets/{blockDiagram-WCTKOSBZ-C40u_hLo.js → blockDiagram-WCTKOSBZ-B4T4UcRa.js} +1 -1
  19. package/frontend/dist/assets/{chunk-7E7YKBS2-7zRaOLjj.js → chunk-7E7YKBS2-NeXbzmbu.js} +1 -1
  20. package/frontend/dist/assets/{chunk-C72U2L5F-_JbQPbLN.js → chunk-C72U2L5F-B8CSWPSJ.js} +1 -1
  21. package/frontend/dist/assets/{chunk-EGIJ26TM-B--aFyPw.js → chunk-EGIJ26TM-CIih76L7.js} +1 -1
  22. package/frontend/dist/assets/{chunk-L3YUKLVL-C-DkZTMr.js → chunk-L3YUKLVL-ki-YiEHS.js} +1 -1
  23. package/frontend/dist/assets/{chunk-NQ4KR5QH-Bpu9FsM7.js → chunk-NQ4KR5QH-CotGwnjL.js} +1 -1
  24. package/frontend/dist/assets/{chunk-OZEHJAEY-CNNiJtG0.js → chunk-OZEHJAEY-BpNr8Rjz.js} +1 -1
  25. package/frontend/dist/assets/{chunk-R5LLSJPH-CHQzVVOV.js → chunk-R5LLSJPH-BijepqoK.js} +1 -1
  26. package/frontend/dist/assets/{chunk-WL4C6EOR-BNFU6IIi.js → chunk-WL4C6EOR-pSm7LSvh.js} +1 -1
  27. package/frontend/dist/assets/{chunk-XIRO2GV7-98T93G85.js → chunk-XIRO2GV7-DwBvqLWa.js} +1 -1
  28. package/frontend/dist/assets/{chunk-XZSTWKYB-BcW3cyNp.js → chunk-XZSTWKYB-CBqjoIm6.js} +1 -1
  29. package/frontend/dist/assets/{classDiagram-VBA2DB6C-DikXzgcD.js → classDiagram-VBA2DB6C-BFFlfFWc.js} +1 -1
  30. package/frontend/dist/assets/{classDiagram-v2-RAHNMMFH-D7E3tQUK.js → classDiagram-v2-RAHNMMFH-Zm4JHiZG.js} +1 -1
  31. package/frontend/dist/assets/clone-DTaWrNz3.js +1 -0
  32. package/frontend/dist/assets/{dagre-DH4bgZO7.js → dagre-DwFVI7Jd.js} +1 -1
  33. package/frontend/dist/assets/{dagre-KLK3FWXG-DNSqDkwT.js → dagre-KLK3FWXG-HvRFWYud.js} +1 -1
  34. package/frontend/dist/assets/{diagram-E7M64L7V-RqPNT5Vs.js → diagram-E7M64L7V-CQyHprzu.js} +1 -1
  35. package/frontend/dist/assets/{diagram-IFDJBPK2-B-5NRyaE.js → diagram-IFDJBPK2-CqkOPpOt.js} +1 -1
  36. package/frontend/dist/assets/{diagram-P4PSJMXO-BrP69Hk0.js → diagram-P4PSJMXO-OpXCvbKg.js} +1 -1
  37. package/frontend/dist/assets/{erDiagram-INFDFZHY-BYiB9NYg.js → erDiagram-INFDFZHY-eQioynMC.js} +1 -1
  38. package/frontend/dist/assets/{flowDiagram-PKNHOUZH-Cwq47rsR.js → flowDiagram-PKNHOUZH-hx7shOit.js} +1 -1
  39. package/frontend/dist/assets/gitGraph-HDMCJU4V-CcSU6vzN.js +1 -0
  40. package/frontend/dist/assets/{gitGraphDiagram-K3NZZRJ6-BaUxboNc.js → gitGraphDiagram-K3NZZRJ6-BUC-llGK.js} +1 -1
  41. package/frontend/dist/assets/{graphlib-kEFlkt3U.js → graphlib-ChWzv2kY.js} +1 -1
  42. package/frontend/dist/assets/index-B71Ng0qa.css +1 -0
  43. package/frontend/dist/assets/index-B8EBg0hm.js +157 -0
  44. package/frontend/dist/assets/info-3K5VOQVL-B3MsAb_d.js +1 -0
  45. package/frontend/dist/assets/infoDiagram-LFFYTUFH-BMFFvbZ3.js +2 -0
  46. package/frontend/dist/assets/{ishikawaDiagram-PHBUUO56-Bld4two_.js → ishikawaDiagram-PHBUUO56-B80zFWUg.js} +1 -1
  47. package/frontend/dist/assets/{kanban-definition-K7BYSVSG-DpgsZmpG.js → kanban-definition-K7BYSVSG-wvVdN2XJ.js} +1 -1
  48. package/frontend/dist/assets/{mermaid-parser.core-DAeTodBQ.js → mermaid-parser.core-fDWeedvo.js} +2 -2
  49. package/frontend/dist/assets/{mindmap-definition-YRQLILUH-CoNlFyVl.js → mindmap-definition-YRQLILUH-COI6teLb.js} +1 -1
  50. package/frontend/dist/assets/packet-RMMSAZCW-AtnYULi6.js +1 -0
  51. package/frontend/dist/assets/pie-UPGHQEXC-Cm4Nnlh1.js +1 -0
  52. package/frontend/dist/assets/{pieDiagram-SKSYHLDU-CM_hbCcn.js → pieDiagram-SKSYHLDU-bjwLIsJh.js} +1 -1
  53. package/frontend/dist/assets/radar-KQ55EAFF-D5ioamgd.js +1 -0
  54. package/frontend/dist/assets/{requirementDiagram-Z7DCOOCP-CorP7L7F.js → requirementDiagram-Z7DCOOCP-3UYkNoFC.js} +1 -1
  55. package/frontend/dist/assets/{sequenceDiagram-2WXFIKYE-JMqJSFq6.js → sequenceDiagram-2WXFIKYE-DCXcXQoE.js} +1 -1
  56. package/frontend/dist/assets/{stateDiagram-RAJIS63D-CgFfENdy.js → stateDiagram-RAJIS63D-DnpcJhHB.js} +1 -1
  57. package/frontend/dist/assets/{stateDiagram-v2-FVOUBMTO-C4Hh2P-U.js → stateDiagram-v2-FVOUBMTO-Db77rn81.js} +1 -1
  58. package/frontend/dist/assets/treemap-KZPCXAKY-BJS2z0TG.js +1 -0
  59. package/frontend/dist/assets/{vennDiagram-LZ73GAT5-DygS4Zzd.js → vennDiagram-LZ73GAT5-DpZs8KCD.js} +1 -1
  60. package/frontend/dist/assets/{xychartDiagram-JWTSCODW-D6wY1Jwd.js → xychartDiagram-JWTSCODW-CFVB_zoy.js} +1 -1
  61. package/frontend/dist/index.html +2 -2
  62. package/package.json +1 -1
  63. package/frontend/dist/assets/architecture-PBZL5I3N-hvVXGhqd.js +0 -1
  64. package/frontend/dist/assets/clone-mOXuZa7C.js +0 -1
  65. package/frontend/dist/assets/gitGraph-HDMCJU4V-COlTQ7bA.js +0 -1
  66. package/frontend/dist/assets/index-DMLxes_u.js +0 -157
  67. package/frontend/dist/assets/index-DmzeqkB1.css +0 -1
  68. package/frontend/dist/assets/info-3K5VOQVL-DBtHyA4C.js +0 -1
  69. package/frontend/dist/assets/infoDiagram-LFFYTUFH-yBXLgMPI.js +0 -2
  70. package/frontend/dist/assets/packet-RMMSAZCW-FF6-Tmai.js +0 -1
  71. package/frontend/dist/assets/pie-UPGHQEXC-CFvXY2o-.js +0 -1
  72. package/frontend/dist/assets/radar-KQ55EAFF-MPZu7SdX.js +0 -1
  73. package/frontend/dist/assets/treemap-KZPCXAKY-qb1Pl9la.js +0 -1
package/README.github.md CHANGED
@@ -165,6 +165,12 @@ npx atoo-studio
165
165
 
166
166
  Then open `https://localhost:3010` in your browser.
167
167
 
168
+ To use a different port, set the `ATOO_PORT` environment variable:
169
+
170
+ ```bash
171
+ ATOO_PORT=4000 npx atoo-studio
172
+ ```
173
+
168
174
  ### Docker
169
175
 
170
176
  ```bash
@@ -193,13 +199,13 @@ Run one of the setup scripts on your Proxmox host:
193
199
  **LXC container** — lightweight, 2 CPU / 2 GB RAM / 20 GB disk
194
200
 
195
201
  ```bash
196
- bash -c "$(curl -fsSL https://raw.githubusercontent.com/atooai/atoo-studio/master/proxmox/lxc.sh)"
202
+ bash -c "$(curl -fsSL https://raw.githubusercontent.com/atooai/atoo-studio/main/proxmox/lxc.sh)"
197
203
  ```
198
204
 
199
205
  **VM** — stronger isolation, CUSE/serial support, 4 CPU / 4 GB RAM / 50 GB disk
200
206
 
201
207
  ```bash
202
- bash -c "$(curl -fsSL https://raw.githubusercontent.com/atooai/atoo-studio/master/proxmox/vm.sh)"
208
+ bash -c "$(curl -fsSL https://raw.githubusercontent.com/atooai/atoo-studio/main/proxmox/vm.sh)"
203
209
  ```
204
210
 
205
211
  Both scripts prompt for container or VM ID, hostname, storage, and resources with sensible defaults.
package/README.md CHANGED
@@ -14,6 +14,12 @@ npx atoo-studio
14
14
 
15
15
  Then open `https://localhost:3010` in your browser.
16
16
 
17
+ To use a different port:
18
+
19
+ ```bash
20
+ ATOO_PORT=4000 npx atoo-studio
21
+ ```
22
+
17
23
  ## Install Globally
18
24
 
19
25
  ```bash
@@ -100,7 +106,7 @@ Atoo Studio exposes MCP tools for agent integration:
100
106
 
101
107
  - **Docker**: `ghcr.io/atooai/atoo-studio`
102
108
  - **LXC/LXD**: Download from [GitHub Releases](https://github.com/atooai/atoo-studio/releases)
103
- - **Proxmox**: One-command [LXC](https://github.com/atooai/atoo-studio/tree/master/proxmox) and [VM](https://github.com/atooai/atoo-studio/tree/master/proxmox) scripts
109
+ - **Proxmox**: One-command [LXC](https://github.com/atooai/atoo-studio/tree/main/proxmox) and [VM](https://github.com/atooai/atoo-studio/tree/main/proxmox) scripts
104
110
 
105
111
  ## Links
106
112
 
package/README.npm.md CHANGED
@@ -14,6 +14,12 @@ npx atoo-studio
14
14
 
15
15
  Then open `https://localhost:3010` in your browser.
16
16
 
17
+ To use a different port:
18
+
19
+ ```bash
20
+ ATOO_PORT=4000 npx atoo-studio
21
+ ```
22
+
17
23
  ## Install Globally
18
24
 
19
25
  ```bash
@@ -100,7 +106,7 @@ Atoo Studio exposes MCP tools for agent integration:
100
106
 
101
107
  - **Docker**: `ghcr.io/atooai/atoo-studio`
102
108
  - **LXC/LXD**: Download from [GitHub Releases](https://github.com/atooai/atoo-studio/releases)
103
- - **Proxmox**: One-command [LXC](https://github.com/atooai/atoo-studio/tree/master/proxmox) and [VM](https://github.com/atooai/atoo-studio/tree/master/proxmox) scripts
109
+ - **Proxmox**: One-command [LXC](https://github.com/atooai/atoo-studio/tree/main/proxmox) and [VM](https://github.com/atooai/atoo-studio/tree/main/proxmox) scripts
104
110
 
105
111
  ## Links
106
112
 
@@ -1,6 +1,5 @@
1
1
  export declare const PROJECT_ROOT: string;
2
- export declare const PROXY_PORT = 8081;
3
- export declare const WEB_PORT = 3010;
2
+ export declare const WEB_PORT: number;
4
3
  export declare const CERTS_DIR: string;
5
4
  export declare const CA_CERT_PATH: string;
6
5
  export declare const CA_KEY_PATH: string;
@@ -5,8 +5,7 @@ const __config_dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  export const PROJECT_ROOT = __config_dirname.includes('/dist/src')
6
6
  ? path.resolve(__config_dirname, '../..') // dist/src/ → project root
7
7
  : path.resolve(__config_dirname, '..'); // src/ → project root
8
- export const PROXY_PORT = 8081;
9
- export const WEB_PORT = 3010;
8
+ export const WEB_PORT = parseInt(process.env.ATOO_PORT || '3010', 10);
10
9
  export const CERTS_DIR = path.join(PROJECT_ROOT, 'certs') + '/';
11
10
  export const CA_CERT_PATH = `${CERTS_DIR}ca.pem`;
12
11
  export const CA_KEY_PATH = `${CERTS_DIR}ca-key.pem`;
@@ -9,6 +9,7 @@ import { getRemoteFileTree, readRemoteFileContent } from '../services/remote-fs-
9
9
  import * as gitOps from '../services/git-ops.js';
10
10
  import * as remoteGitOps from '../services/remote-git-ops.js';
11
11
  import { sshManager } from '../services/ssh-manager.js';
12
+ import { searchFiles, replaceInFiles, replaceInSingleFile } from '../services/file-search.js';
12
13
  import { watchProject, unwatchProject, reconcileWorktrees } from '../services/project-watcher.js';
13
14
  export const projectsRouter = Router();
14
15
  // Helper: get project context (local or remote)
@@ -67,7 +68,10 @@ projectsRouter.get('/api/projects/:id/files', async (req, res) => {
67
68
  if (!ctx)
68
69
  return res.status(404).json({ error: 'Project not found' });
69
70
  try {
70
- const rootPath = req.query.rootPath || ctx.cwd;
71
+ const requestedRootPath = req.query.rootPath;
72
+ const rootPath = requestedRootPath
73
+ ? (path.isAbsolute(requestedRootPath) ? requestedRootPath : path.join(ctx.cwd, requestedRootPath))
74
+ : ctx.cwd;
71
75
  const showHidden = req.query.showHidden === 'true';
72
76
  const maxDepth = req.query.maxDepth != null ? parseInt(req.query.maxDepth, 10) : undefined;
73
77
  if (ctx.connectionId) {
@@ -78,7 +82,7 @@ projectsRouter.get('/api/projects/:id/files', async (req, res) => {
78
82
  const tree = await getFileTree(rootPath, 0, showHidden, maxDepth);
79
83
  res.json(tree);
80
84
  // Start watching when project root is first accessed (lazy — avoids watching all projects on startup)
81
- if (!req.query.rootPath && !ctx.connectionId) {
85
+ if (!requestedRootPath && !ctx.connectionId) {
82
86
  setImmediate(() => watchProject(req.params.id, ctx.cwd));
83
87
  }
84
88
  }
@@ -230,6 +234,61 @@ projectsRouter.post('/api/files/raw/search', (req, res) => {
230
234
  res.status(500).json({ error: err.message });
231
235
  }
232
236
  });
237
+ // Search in project files (content search)
238
+ projectsRouter.post('/api/projects/:id/search', async (req, res) => {
239
+ const ctx = getProjectContext(req.params.id);
240
+ if (!ctx)
241
+ return res.status(404).json({ error: 'Project not found' });
242
+ if (!req.body.query)
243
+ return res.status(400).json({ error: 'query is required' });
244
+ try {
245
+ const result = await searchFiles(ctx.cwd, { ...req.body, showHidden: req.body.showHidden });
246
+ res.json(result);
247
+ }
248
+ catch (err) {
249
+ if (err instanceof SyntaxError || err.message?.includes('Invalid regular expression')) {
250
+ return res.status(400).json({ error: `Invalid regex: ${err.message}` });
251
+ }
252
+ res.status(500).json({ error: err.message });
253
+ }
254
+ });
255
+ // Replace in project files
256
+ projectsRouter.post('/api/projects/:id/replace-all', async (req, res) => {
257
+ const ctx = getProjectContext(req.params.id);
258
+ if (!ctx)
259
+ return res.status(404).json({ error: 'Project not found' });
260
+ if (!req.body.query || req.body.replacement == null)
261
+ return res.status(400).json({ error: 'query and replacement are required' });
262
+ try {
263
+ const result = await replaceInFiles(ctx.cwd, { ...req.body, showHidden: req.body.showHidden });
264
+ res.json(result);
265
+ }
266
+ catch (err) {
267
+ if (err instanceof SyntaxError || err.message?.includes('Invalid regular expression')) {
268
+ return res.status(400).json({ error: `Invalid regex: ${err.message}` });
269
+ }
270
+ res.status(500).json({ error: err.message });
271
+ }
272
+ });
273
+ // Replace in a single file (optionally specific lines)
274
+ projectsRouter.post('/api/projects/:id/replace-in-file', async (req, res) => {
275
+ const ctx = getProjectContext(req.params.id);
276
+ if (!ctx)
277
+ return res.status(404).json({ error: 'Project not found' });
278
+ const { file, query, replacement, lines, ...opts } = req.body;
279
+ if (!file || !query || replacement == null)
280
+ return res.status(400).json({ error: 'file, query and replacement are required' });
281
+ try {
282
+ const result = await replaceInSingleFile(ctx.cwd, file, { query, replacement, lines, ...opts });
283
+ res.json(result);
284
+ }
285
+ catch (err) {
286
+ if (err.message?.includes('Invalid regular expression')) {
287
+ return res.status(400).json({ error: `Invalid regex: ${err.message}` });
288
+ }
289
+ res.status(500).json({ error: err.message });
290
+ }
291
+ });
233
292
  projectsRouter.put('/api/files', async (req, res) => {
234
293
  const { path: filePath, content, ssh_connection_id } = req.body;
235
294
  if (!filePath || typeof content !== 'string') {
@@ -0,0 +1,59 @@
1
+ export interface SearchMatch {
2
+ line: number;
3
+ column: number;
4
+ length: number;
5
+ lineContent: string;
6
+ }
7
+ export interface SearchFileResult {
8
+ file: string;
9
+ matches: SearchMatch[];
10
+ filenameMatch?: boolean;
11
+ }
12
+ export interface SearchRequest {
13
+ query: string;
14
+ isRegex?: boolean;
15
+ matchCase?: boolean;
16
+ matchWholeWord?: boolean;
17
+ includeFilenames?: boolean;
18
+ includeFilter?: string;
19
+ excludeFilter?: string;
20
+ includeFilterIsRegex?: boolean;
21
+ excludeFilterIsRegex?: boolean;
22
+ openFilesOnly?: string[];
23
+ showHidden?: boolean;
24
+ maxResults?: number;
25
+ }
26
+ export interface SearchResponse {
27
+ results: SearchFileResult[];
28
+ truncated: boolean;
29
+ totalFiles: number;
30
+ totalMatches: number;
31
+ }
32
+ export interface ReplaceRequest extends SearchRequest {
33
+ replacement: string;
34
+ preserveCase?: boolean;
35
+ renameFiles?: boolean;
36
+ }
37
+ export interface ReplaceResponse {
38
+ filesModified: number;
39
+ totalReplacements: number;
40
+ filesRenamed: number;
41
+ errors: Array<{
42
+ file: string;
43
+ error: string;
44
+ }>;
45
+ }
46
+ export declare function searchFiles(rootPath: string, req: SearchRequest): Promise<SearchResponse>;
47
+ export declare function replaceInFiles(rootPath: string, req: ReplaceRequest): Promise<ReplaceResponse>;
48
+ export declare function replaceInSingleFile(rootPath: string, relFile: string, req: {
49
+ query: string;
50
+ replacement: string;
51
+ isRegex?: boolean;
52
+ matchCase?: boolean;
53
+ matchWholeWord?: boolean;
54
+ preserveCase?: boolean;
55
+ lines?: number[];
56
+ }): Promise<{
57
+ replacements: number;
58
+ error?: string;
59
+ }>;
@@ -0,0 +1,269 @@
1
+ import fsp from 'fs/promises';
2
+ import path from 'path';
3
+ import { isBinaryFile } from './fs-browser.js';
4
+ const SKIP_NAMES = new Set(['.git', '.atoo-studio', 'node_modules', '.next', '.nuxt', 'dist', 'build', '__pycache__', '.venv', 'venv', '.cache', '.parcel-cache', 'coverage', '.svn', '.hg']);
5
+ const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
6
+ function buildSearchRegex(query, opts) {
7
+ let pattern = opts.isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
8
+ if (opts.matchWholeWord)
9
+ pattern = `\\b${pattern}\\b`;
10
+ const flags = opts.matchCase ? 'g' : 'gi';
11
+ return new RegExp(pattern, flags);
12
+ }
13
+ function globToRegex(glob) {
14
+ // Simple glob-to-regex: * → [^/]*, ** → .*, ? → .
15
+ return glob
16
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
17
+ .replace(/\*\*/g, '{{DOUBLESTAR}}')
18
+ .replace(/\*/g, '[^/]*')
19
+ .replace(/\?/g, '.')
20
+ .replace(/\{\{DOUBLESTAR\}\}/g, '.*');
21
+ }
22
+ function buildFilterRegex(filter, isRegex) {
23
+ if (!filter.trim())
24
+ return null;
25
+ if (isRegex) {
26
+ return new RegExp(filter, 'i');
27
+ }
28
+ // Comma-separated globs
29
+ const parts = filter.split(',').map(s => s.trim()).filter(Boolean);
30
+ if (parts.length === 0)
31
+ return null;
32
+ const regexParts = parts.map(globToRegex);
33
+ return new RegExp(`^(${regexParts.join('|')})$`, 'i');
34
+ }
35
+ function matchesFilter(relPath, filter) {
36
+ const fileName = path.basename(relPath);
37
+ // Match against both the full relative path and just the filename
38
+ return filter.test(relPath) || filter.test(fileName);
39
+ }
40
+ function preserveCaseReplace(match, replacement) {
41
+ if (match === match.toUpperCase())
42
+ return replacement.toUpperCase();
43
+ if (match === match.toLowerCase())
44
+ return replacement.toLowerCase();
45
+ if (match[0] === match[0].toUpperCase() && match.slice(1) === match.slice(1).toLowerCase()) {
46
+ return replacement[0].toUpperCase() + replacement.slice(1).toLowerCase();
47
+ }
48
+ return replacement;
49
+ }
50
+ async function walkFiles(dirPath, rootPath, showHidden, includeRe, excludeRe, openFilesSet, callback) {
51
+ let entries;
52
+ try {
53
+ entries = await fsp.readdir(dirPath, { withFileTypes: true });
54
+ }
55
+ catch {
56
+ return;
57
+ }
58
+ let fileCount = 0;
59
+ for (const entry of entries) {
60
+ if (!showHidden && SKIP_NAMES.has(entry.name))
61
+ continue;
62
+ const absPath = path.join(dirPath, entry.name);
63
+ const relPath = path.relative(rootPath, absPath);
64
+ if (entry.isDirectory()) {
65
+ await walkFiles(absPath, rootPath, showHidden, includeRe, excludeRe, openFilesSet, callback);
66
+ }
67
+ else if (entry.isFile() || entry.isSymbolicLink()) {
68
+ // Filter by open files
69
+ if (openFilesSet && !openFilesSet.has(absPath))
70
+ continue;
71
+ // Apply include/exclude filters
72
+ if (includeRe && !matchesFilter(relPath, includeRe))
73
+ continue;
74
+ if (excludeRe && matchesFilter(relPath, excludeRe))
75
+ continue;
76
+ // Skip binary files
77
+ if (isBinaryFile(absPath))
78
+ continue;
79
+ // Skip large files
80
+ try {
81
+ const stat = await fsp.stat(absPath);
82
+ if (stat.size > MAX_FILE_SIZE)
83
+ continue;
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ const shouldContinue = await callback(absPath, relPath);
89
+ if (!shouldContinue)
90
+ return;
91
+ // Yield every 50 files
92
+ fileCount++;
93
+ if (fileCount % 50 === 0) {
94
+ await new Promise(resolve => setImmediate(resolve));
95
+ }
96
+ }
97
+ }
98
+ }
99
+ export async function searchFiles(rootPath, req) {
100
+ const maxResults = req.maxResults || 500;
101
+ const regex = buildSearchRegex(req.query, req);
102
+ const includeRe = buildFilterRegex(req.includeFilter || '', req.includeFilterIsRegex || false);
103
+ const excludeRe = buildFilterRegex(req.excludeFilter || '', req.excludeFilterIsRegex || false);
104
+ const openFilesSet = req.openFilesOnly ? new Set(req.openFilesOnly.map(f => path.resolve(f))) : null;
105
+ const results = [];
106
+ let totalMatches = 0;
107
+ let totalFiles = 0;
108
+ let truncated = false;
109
+ await walkFiles(rootPath, rootPath, req.showHidden || false, includeRe, excludeRe, openFilesSet, async (absPath, relPath) => {
110
+ totalFiles++;
111
+ const fileResult = { file: relPath, matches: [] };
112
+ // Check filename match
113
+ if (req.includeFilenames) {
114
+ const fnRegex = buildSearchRegex(req.query, req);
115
+ if (fnRegex.test(relPath)) {
116
+ fileResult.filenameMatch = true;
117
+ }
118
+ }
119
+ // Search file contents
120
+ let content;
121
+ try {
122
+ content = await fsp.readFile(absPath, 'utf-8');
123
+ }
124
+ catch {
125
+ return true;
126
+ }
127
+ const lines = content.split('\n');
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const line = lines[i];
130
+ // Reset regex lastIndex for each line
131
+ const lineRegex = buildSearchRegex(req.query, req);
132
+ let m;
133
+ while ((m = lineRegex.exec(line)) !== null) {
134
+ fileResult.matches.push({
135
+ line: i + 1,
136
+ column: m.index,
137
+ length: m[0].length,
138
+ lineContent: line.length > 500 ? line.substring(0, 500) : line,
139
+ });
140
+ totalMatches++;
141
+ if (totalMatches >= maxResults) {
142
+ truncated = true;
143
+ break;
144
+ }
145
+ // Prevent infinite loop on zero-length matches
146
+ if (m[0].length === 0)
147
+ lineRegex.lastIndex++;
148
+ }
149
+ if (truncated)
150
+ break;
151
+ }
152
+ if (fileResult.matches.length > 0 || fileResult.filenameMatch) {
153
+ results.push(fileResult);
154
+ }
155
+ return !truncated;
156
+ });
157
+ return { results, truncated, totalFiles, totalMatches };
158
+ }
159
+ export async function replaceInFiles(rootPath, req) {
160
+ const regex = buildSearchRegex(req.query, req);
161
+ const includeRe = buildFilterRegex(req.includeFilter || '', req.includeFilterIsRegex || false);
162
+ const excludeRe = buildFilterRegex(req.excludeFilter || '', req.excludeFilterIsRegex || false);
163
+ const openFilesSet = req.openFilesOnly ? new Set(req.openFilesOnly.map(f => path.resolve(f))) : null;
164
+ let filesModified = 0;
165
+ let totalReplacements = 0;
166
+ let filesRenamed = 0;
167
+ const errors = [];
168
+ // Collect files to process (for rename, we need all paths first)
169
+ const filesToProcess = [];
170
+ await walkFiles(rootPath, rootPath, req.showHidden || false, includeRe, excludeRe, openFilesSet, async (absPath, relPath) => {
171
+ filesToProcess.push({ absPath, relPath });
172
+ return true;
173
+ });
174
+ // Replace contents
175
+ for (const { absPath, relPath } of filesToProcess) {
176
+ try {
177
+ const content = await fsp.readFile(absPath, 'utf-8');
178
+ const replaceRegex = buildSearchRegex(req.query, req);
179
+ let newContent;
180
+ let count = 0;
181
+ if (req.preserveCase) {
182
+ newContent = content.replace(replaceRegex, (match) => {
183
+ count++;
184
+ return preserveCaseReplace(match, req.replacement);
185
+ });
186
+ }
187
+ else {
188
+ newContent = content.replace(replaceRegex, () => {
189
+ count++;
190
+ return req.replacement;
191
+ });
192
+ }
193
+ if (count > 0) {
194
+ await fsp.writeFile(absPath, newContent, 'utf-8');
195
+ filesModified++;
196
+ totalReplacements += count;
197
+ }
198
+ }
199
+ catch (err) {
200
+ errors.push({ file: relPath, error: err.message });
201
+ }
202
+ }
203
+ // Rename files if requested
204
+ if (req.renameFiles && req.includeFilenames) {
205
+ for (const { absPath, relPath } of filesToProcess) {
206
+ const fileName = path.basename(absPath);
207
+ const renameRegex = buildSearchRegex(req.query, req);
208
+ let newName;
209
+ if (req.preserveCase) {
210
+ newName = fileName.replace(renameRegex, (match) => preserveCaseReplace(match, req.replacement));
211
+ }
212
+ else {
213
+ newName = fileName.replace(renameRegex, req.replacement);
214
+ }
215
+ if (newName !== fileName) {
216
+ const newPath = path.join(path.dirname(absPath), newName);
217
+ try {
218
+ await fsp.rename(absPath, newPath);
219
+ filesRenamed++;
220
+ }
221
+ catch (err) {
222
+ errors.push({ file: relPath, error: `Rename failed: ${err.message}` });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ return { filesModified, totalReplacements, filesRenamed, errors };
228
+ }
229
+ // Replace in a single file, optionally restricted to specific lines
230
+ export async function replaceInSingleFile(rootPath, relFile, req) {
231
+ const absPath = path.join(rootPath, relFile);
232
+ try {
233
+ const content = await fsp.readFile(absPath, 'utf-8');
234
+ const lineSet = req.lines ? new Set(req.lines) : null;
235
+ if (lineSet) {
236
+ // Replace only on specific lines
237
+ const lines = content.split('\n');
238
+ let count = 0;
239
+ for (let i = 0; i < lines.length; i++) {
240
+ if (!lineSet.has(i + 1))
241
+ continue; // lines are 1-based
242
+ const lineRegex = buildSearchRegex(req.query, req);
243
+ const newLine = req.preserveCase
244
+ ? lines[i].replace(lineRegex, (match) => { count++; return preserveCaseReplace(match, req.replacement); })
245
+ : lines[i].replace(lineRegex, () => { count++; return req.replacement; });
246
+ lines[i] = newLine;
247
+ }
248
+ if (count > 0) {
249
+ await fsp.writeFile(absPath, lines.join('\n'), 'utf-8');
250
+ }
251
+ return { replacements: count };
252
+ }
253
+ else {
254
+ // Replace all in file
255
+ const replaceRegex = buildSearchRegex(req.query, req);
256
+ let count = 0;
257
+ const newContent = req.preserveCase
258
+ ? content.replace(replaceRegex, (match) => { count++; return preserveCaseReplace(match, req.replacement); })
259
+ : content.replace(replaceRegex, () => { count++; return req.replacement; });
260
+ if (count > 0) {
261
+ await fsp.writeFile(absPath, newContent, 'utf-8');
262
+ }
263
+ return { replacements: count };
264
+ }
265
+ }
266
+ catch (err) {
267
+ return { replacements: 0, error: err.message };
268
+ }
269
+ }
@@ -8,6 +8,10 @@ export declare function gitStatus(cwd: string): Promise<{
8
8
  workTreeStatus: string;
9
9
  oldPath: string | undefined;
10
10
  }[]>;
11
+ export declare function parseRefs(decorate: string): {
12
+ type: "head" | "branch" | "tag" | "remote";
13
+ label: string;
14
+ }[];
11
15
  export declare function gitLog(cwd: string, branch?: string, count?: number): Promise<{
12
16
  hash: string;
13
17
  fullHash: string;
@@ -17,6 +21,10 @@ export declare function gitLog(cwd: string, branch?: string, count?: number): Pr
17
21
  date: string;
18
22
  files: never[];
19
23
  merge: boolean;
24
+ refs: {
25
+ type: "head" | "branch" | "tag" | "remote";
26
+ label: string;
27
+ }[];
20
28
  }[]>;
21
29
  export declare function gitCommitFiles(cwd: string, hash: string): Promise<({
22
30
  path: string;
@@ -90,9 +90,36 @@ export async function gitStatus(cwd) {
90
90
  }
91
91
  return entries;
92
92
  }
93
+ export function parseRefs(decorate) {
94
+ if (!decorate.trim())
95
+ return [];
96
+ const refs = [];
97
+ for (const part of decorate.split(',')) {
98
+ const token = part.trim();
99
+ if (!token)
100
+ continue;
101
+ if (token === 'HEAD') {
102
+ refs.push({ type: 'head', label: 'HEAD' });
103
+ }
104
+ else if (token.startsWith('HEAD -> ')) {
105
+ refs.push({ type: 'head', label: 'HEAD' });
106
+ refs.push({ type: 'branch', label: token.slice(8) });
107
+ }
108
+ else if (token.startsWith('tag: ')) {
109
+ refs.push({ type: 'tag', label: token.slice(5) });
110
+ }
111
+ else if (token.includes('/')) {
112
+ refs.push({ type: 'remote', label: token });
113
+ }
114
+ else {
115
+ refs.push({ type: 'branch', label: token });
116
+ }
117
+ }
118
+ return refs;
119
+ }
93
120
  export async function gitLog(cwd, branch, count = 30) {
94
121
  const SEP = '---GIT-LOG-SEP---';
95
- const args = ['log', `--format=${SEP}%n%H%n%h%n%an%n%ar%n%s%n%B`, '-n', String(count)];
122
+ const args = ['log', `--format=${SEP}%n%H%n%h%n%an%n%ar%n%D%n%s%n%B`, '-n', String(count)];
96
123
  if (branch)
97
124
  args.push(branch);
98
125
  const output = await git(args, cwd);
@@ -103,10 +130,12 @@ export async function gitLog(cwd, branch, count = 30) {
103
130
  const hash = lines[1] || '';
104
131
  const author = lines[2] || '';
105
132
  const date = lines[3] || '';
106
- const msg = lines[4] || '';
107
- const fullMessage = lines.slice(5).join('\n').trim() || msg;
133
+ const decorate = lines[4] || '';
134
+ const msg = lines[5] || '';
135
+ const fullMessage = lines.slice(6).join('\n').trim() || msg;
108
136
  const isMerge = msg.toLowerCase().startsWith('merge');
109
- return { hash, fullHash, msg, fullMessage, author, date, files: [], merge: isMerge };
137
+ const refs = parseRefs(decorate);
138
+ return { hash, fullHash, msg, fullMessage, author, date, files: [], merge: isMerge, refs };
110
139
  });
111
140
  }
112
141
  export async function gitCommitFiles(cwd, hash) {
@@ -17,6 +17,10 @@ export declare function gitLog(connId: string, cwd: string, branch?: string, cou
17
17
  date: string;
18
18
  files: never[];
19
19
  merge: boolean;
20
+ refs: {
21
+ type: "head" | "branch" | "tag" | "remote";
22
+ label: string;
23
+ }[];
20
24
  }[]>;
21
25
  export declare function gitCommitFiles(connId: string, cwd: string, hash: string): Promise<{
22
26
  path: string;
@@ -1,4 +1,5 @@
1
1
  import { sshManager } from './ssh-manager.js';
2
+ import { parseRefs } from './git-ops.js';
2
3
  function shellEscape(s) {
3
4
  return "'" + s.replace(/'/g, "'\\''") + "'";
4
5
  }
@@ -34,7 +35,7 @@ export async function gitStatus(connId, cwd) {
34
35
  }
35
36
  export async function gitLog(connId, cwd, branch, count = 30) {
36
37
  const SEP = '---GIT-LOG-SEP---';
37
- const args = ['log', `--format=${SEP}%n%H%n%h%n%an%n%ar%n%s%n%B`, '-n', String(count)];
38
+ const args = ['log', `--format=${SEP}%n%H%n%h%n%an%n%ar%n%D%n%s%n%B`, '-n', String(count)];
38
39
  if (branch)
39
40
  args.push(branch);
40
41
  const output = await git(connId, args, cwd);
@@ -45,10 +46,12 @@ export async function gitLog(connId, cwd, branch, count = 30) {
45
46
  const hash = lines[1] || '';
46
47
  const author = lines[2] || '';
47
48
  const date = lines[3] || '';
48
- const msg = lines[4] || '';
49
- const fullMessage = lines.slice(5).join('\n').trim() || msg;
49
+ const decorate = lines[4] || '';
50
+ const msg = lines[5] || '';
51
+ const fullMessage = lines.slice(6).join('\n').trim() || msg;
50
52
  const isMerge = msg.toLowerCase().startsWith('merge');
51
- return { hash, fullHash, msg, fullMessage, author, date, files: [], merge: isMerge };
53
+ const refs = parseRefs(decorate);
54
+ return { hash, fullHash, msg, fullMessage, author, date, files: [], merge: isMerge, refs };
52
55
  });
53
56
  }
54
57
  export async function gitCommitFiles(connId, cwd, hash) {
@@ -1,7 +1,7 @@
1
1
  import { Client } from 'ssh2';
2
2
  import fs from 'fs';
3
3
  import net from 'net';
4
- import { CA_CERT_PATH, PROXY_PORT } from '../config.js';
4
+ import { CA_CERT_PATH } from '../config.js';
5
5
  import { deobfuscate } from './obfuscation.js';
6
6
  class SshManager {
7
7
  connections = new Map();
@@ -46,7 +46,6 @@ class SshManager {
46
46
  try {
47
47
  // Setup reverse tunnels so remote claude can reach our proxy
48
48
  await this.setupReverseTunnel(config.id, 3000, 3010);
49
- await this.setupReverseTunnel(config.id, PROXY_PORT, PROXY_PORT);
50
49
  // Upload CA cert to remote
51
50
  await this.uploadCaCert(config.id);
52
51
  resolve();
@@ -1 +1 @@
1
- import{H as e,R as t,S as n,U as r,b as i,c as a,f as o,n as s,r as c,s as l,u,v as d}from"./_createAssigner-ByDUqGii.js";import{A as f,C as p,D as m,M as h,N as g,S as _,T as v,f as y,h as b,u as x,w as S}from"./_baseUniq-CprifHap.js";var C=/\s/;function w(e){for(var t=e.length;t--&&C.test(e.charAt(t)););return t}var ee=/^\s+/;function T(e){return e&&e.slice(0,w(e)+1).replace(ee,``)}var E=NaN,D=/^[-+]0x[0-9a-f]+$/i,O=/^0b[01]+$/i,k=/^0o[0-7]+$/i,A=parseInt;function j(t){if(typeof t==`number`)return t;if(g(t))return E;if(e(t)){var n=typeof t.valueOf==`function`?t.valueOf():t;t=e(n)?n+``:n}if(typeof t!=`string`)return t===0?t:+t;t=T(t);var r=O.test(t);return r||k.test(t)?A(t.slice(2),r?2:8):D.test(t)?E:+t}var M=1/0,N=17976931348623157e292;function P(e){return e?(e=j(e),e===M||e===-M?(e<0?-1:1)*N:e===e?e:0):e===0?e:0}function F(e){var t=P(e),n=t%1;return t===t?n?t-n:t:0}function I(e){return e!=null&&e.length?_(e,1):[]}var L=Object.prototype,R=L.hasOwnProperty,z=c(function(e,n){e=Object(e);var r=-1,i=n.length,o=i>2?n[2]:void 0;for(o&&s(n[0],n[1],o)&&(i=1);++r<i;)for(var c=n[r],l=a(c),u=-1,d=l.length;++u<d;){var f=l[u],p=e[f];(p===void 0||t(p,L[f])&&!R.call(e,f))&&(e[f]=c[f])}return e});function B(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}function V(e){return function(t,n,r){var i=Object(t);if(!d(t)){var a=y(n,3);t=m(t),n=function(e){return a(i[e],e,i)}}var o=e(t,n,r);return o>-1?i[a?t[o]:o]:void 0}}var H=Math.max;function U(e,t,n){var r=e==null?0:e.length;if(!r)return-1;var i=n==null?0:F(n);return i<0&&(i=H(r+i,0)),f(e,y(t,3),i)}var W=V(U);function G(e,t){var n=-1,r=d(e)?Array(e.length):[];return x(e,function(e,i,a){r[++n]=t(e,i,a)}),r}function K(e,t){return(i(e)?h:G)(e,y(t,3))}var q=Object.prototype.hasOwnProperty;function J(e,t){return e!=null&&q.call(e,t)}function Y(e,t){return e!=null&&b(e,t,J)}var X=`[object String]`;function Z(e){return typeof e==`string`||!i(e)&&n(e)&&r(e)==X}function Q(e,t){return e<t}function $(e,t,n){for(var r=-1,i=e.length;++r<i;){var a=e[r],o=t(a);if(o!=null&&(s===void 0?o===o&&!g(o):n(o,s)))var s=o,c=a}return c}function te(e){return e&&e.length?$(e,l,Q):void 0}function ne(t,n,r,i){if(!e(t))return t;n=v(n,t);for(var a=-1,s=n.length,c=s-1,l=t;l!=null&&++a<s;){var d=S(n[a]),f=r;if(d===`__proto__`||d===`constructor`||d===`prototype`)return t;if(a!=c){var p=l[d];f=i?i(p,d,l):void 0,f===void 0&&(f=e(p)?p:u(n[a+1])?[]:{})}o(l,d,f),l=l[d]}return t}function re(e,t,n){for(var r=-1,i=t.length,a={};++r<i;){var o=t[r],s=p(e,o);n(s,o)&&ne(a,v(o,e),s)}return a}export{Z as a,G as c,z as d,I as f,Q as i,W as l,P as m,te as n,Y as o,F as p,$ as r,K as s,re as t,B as u};
1
+ import{H as e,R as t,S as n,U as r,b as i,c as a,f as o,n as s,r as c,s as l,u,v as d}from"./_createAssigner-ByDUqGii.js";import{A as f,C as p,D as m,M as h,N as g,S as _,T as v,f as y,h as b,u as x,w as S}from"./_baseUniq-BlaoUomR.js";var C=/\s/;function w(e){for(var t=e.length;t--&&C.test(e.charAt(t)););return t}var ee=/^\s+/;function T(e){return e&&e.slice(0,w(e)+1).replace(ee,``)}var E=NaN,D=/^[-+]0x[0-9a-f]+$/i,O=/^0b[01]+$/i,k=/^0o[0-7]+$/i,A=parseInt;function j(t){if(typeof t==`number`)return t;if(g(t))return E;if(e(t)){var n=typeof t.valueOf==`function`?t.valueOf():t;t=e(n)?n+``:n}if(typeof t!=`string`)return t===0?t:+t;t=T(t);var r=O.test(t);return r||k.test(t)?A(t.slice(2),r?2:8):D.test(t)?E:+t}var M=1/0,N=17976931348623157e292;function P(e){return e?(e=j(e),e===M||e===-M?(e<0?-1:1)*N:e===e?e:0):e===0?e:0}function F(e){var t=P(e),n=t%1;return t===t?n?t-n:t:0}function I(e){return e!=null&&e.length?_(e,1):[]}var L=Object.prototype,R=L.hasOwnProperty,z=c(function(e,n){e=Object(e);var r=-1,i=n.length,o=i>2?n[2]:void 0;for(o&&s(n[0],n[1],o)&&(i=1);++r<i;)for(var c=n[r],l=a(c),u=-1,d=l.length;++u<d;){var f=l[u],p=e[f];(p===void 0||t(p,L[f])&&!R.call(e,f))&&(e[f]=c[f])}return e});function B(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}function V(e){return function(t,n,r){var i=Object(t);if(!d(t)){var a=y(n,3);t=m(t),n=function(e){return a(i[e],e,i)}}var o=e(t,n,r);return o>-1?i[a?t[o]:o]:void 0}}var H=Math.max;function U(e,t,n){var r=e==null?0:e.length;if(!r)return-1;var i=n==null?0:F(n);return i<0&&(i=H(r+i,0)),f(e,y(t,3),i)}var W=V(U);function G(e,t){var n=-1,r=d(e)?Array(e.length):[];return x(e,function(e,i,a){r[++n]=t(e,i,a)}),r}function K(e,t){return(i(e)?h:G)(e,y(t,3))}var q=Object.prototype.hasOwnProperty;function J(e,t){return e!=null&&q.call(e,t)}function Y(e,t){return e!=null&&b(e,t,J)}var X=`[object String]`;function Z(e){return typeof e==`string`||!i(e)&&n(e)&&r(e)==X}function Q(e,t){return e<t}function $(e,t,n){for(var r=-1,i=e.length;++r<i;){var a=e[r],o=t(a);if(o!=null&&(s===void 0?o===o&&!g(o):n(o,s)))var s=o,c=a}return c}function te(e){return e&&e.length?$(e,l,Q):void 0}function ne(t,n,r,i){if(!e(t))return t;n=v(n,t);for(var a=-1,s=n.length,c=s-1,l=t;l!=null&&++a<s;){var d=S(n[a]),f=r;if(d===`__proto__`||d===`constructor`||d===`prototype`)return t;if(a!=c){var p=l[d];f=i?i(p,d,l):void 0,f===void 0&&(f=e(p)?p:u(n[a+1])?[]:{})}o(l,d,f),l=l[d]}return t}function re(e,t,n){for(var r=-1,i=t.length,a={};++r<i;){var o=t[r],s=p(e,o);n(s,o)&&ne(a,v(o,e),s)}return a}export{Z as a,G as c,z as d,I as f,Q as i,W as l,P as m,te as n,Y as o,F as p,$ as r,K as s,re as t,B as u};