@hyperdrive.bot/cli 1.0.2

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 (127) hide show
  1. package/README.md +1598 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +5 -0
  6. package/dist/commands/account/add.d.ts +16 -0
  7. package/dist/commands/account/add.js +185 -0
  8. package/dist/commands/account/list.d.ts +6 -0
  9. package/dist/commands/account/list.js +37 -0
  10. package/dist/commands/account/remove.d.ts +11 -0
  11. package/dist/commands/account/remove.js +57 -0
  12. package/dist/commands/auth/login.d.ts +16 -0
  13. package/dist/commands/auth/login.js +178 -0
  14. package/dist/commands/auth/logout.d.ts +6 -0
  15. package/dist/commands/auth/logout.js +39 -0
  16. package/dist/commands/auth/refresh.d.ts +6 -0
  17. package/dist/commands/auth/refresh.js +66 -0
  18. package/dist/commands/auth/status.d.ts +6 -0
  19. package/dist/commands/auth/status.js +63 -0
  20. package/dist/commands/ci/account/create.d.ts +16 -0
  21. package/dist/commands/ci/account/create.js +158 -0
  22. package/dist/commands/ci/account/delete.d.ts +14 -0
  23. package/dist/commands/ci/account/delete.js +88 -0
  24. package/dist/commands/ci/account/list.d.ts +10 -0
  25. package/dist/commands/ci/account/list.js +65 -0
  26. package/dist/commands/config/get.d.ts +9 -0
  27. package/dist/commands/config/get.js +37 -0
  28. package/dist/commands/config/set.d.ts +10 -0
  29. package/dist/commands/config/set.js +48 -0
  30. package/dist/commands/config/show.d.ts +6 -0
  31. package/dist/commands/config/show.js +10 -0
  32. package/dist/commands/deployment/create.d.ts +30 -0
  33. package/dist/commands/deployment/create.js +188 -0
  34. package/dist/commands/deployment/get.d.ts +13 -0
  35. package/dist/commands/deployment/get.js +101 -0
  36. package/dist/commands/deployment/launch.d.ts +15 -0
  37. package/dist/commands/deployment/launch.js +105 -0
  38. package/dist/commands/deployment/list.d.ts +11 -0
  39. package/dist/commands/deployment/list.js +91 -0
  40. package/dist/commands/domain/current.d.ts +6 -0
  41. package/dist/commands/domain/current.js +18 -0
  42. package/dist/commands/domain/list.d.ts +6 -0
  43. package/dist/commands/domain/list.js +42 -0
  44. package/dist/commands/domain/switch.d.ts +9 -0
  45. package/dist/commands/domain/switch.js +40 -0
  46. package/dist/commands/example.d.ts +13 -0
  47. package/dist/commands/example.js +24 -0
  48. package/dist/commands/git/connect.d.ts +10 -0
  49. package/dist/commands/git/connect.js +56 -0
  50. package/dist/commands/git/disconnect.d.ts +11 -0
  51. package/dist/commands/git/disconnect.js +93 -0
  52. package/dist/commands/git/list.d.ts +10 -0
  53. package/dist/commands/git/list.js +53 -0
  54. package/dist/commands/git/sync.d.ts +18 -0
  55. package/dist/commands/git/sync.js +235 -0
  56. package/dist/commands/init.d.ts +188 -0
  57. package/dist/commands/init.js +817 -0
  58. package/dist/commands/jira/connect.d.ts +9 -0
  59. package/dist/commands/jira/connect.js +141 -0
  60. package/dist/commands/jira/status.d.ts +9 -0
  61. package/dist/commands/jira/status.js +118 -0
  62. package/dist/commands/module/analyze.d.ts +29 -0
  63. package/dist/commands/module/analyze.js +201 -0
  64. package/dist/commands/module/create.d.ts +42 -0
  65. package/dist/commands/module/create.js +498 -0
  66. package/dist/commands/module/destroy.d.ts +11 -0
  67. package/dist/commands/module/destroy.js +77 -0
  68. package/dist/commands/module/get.d.ts +10 -0
  69. package/dist/commands/module/get.js +43 -0
  70. package/dist/commands/module/link.d.ts +15 -0
  71. package/dist/commands/module/link.js +175 -0
  72. package/dist/commands/module/list.d.ts +9 -0
  73. package/dist/commands/module/list.js +51 -0
  74. package/dist/commands/module/reanalyze.d.ts +30 -0
  75. package/dist/commands/module/reanalyze.js +206 -0
  76. package/dist/commands/module/update.d.ts +27 -0
  77. package/dist/commands/module/update.js +102 -0
  78. package/dist/commands/parameter/add.d.ts +15 -0
  79. package/dist/commands/parameter/add.js +99 -0
  80. package/dist/commands/parameter/backfill.d.ts +12 -0
  81. package/dist/commands/parameter/backfill.js +113 -0
  82. package/dist/commands/parameter/clear.d.ts +14 -0
  83. package/dist/commands/parameter/clear.js +95 -0
  84. package/dist/commands/parameter/list.d.ts +14 -0
  85. package/dist/commands/parameter/list.js +92 -0
  86. package/dist/commands/parameter/pull.d.ts +14 -0
  87. package/dist/commands/parameter/pull.js +124 -0
  88. package/dist/commands/parameter/remove.d.ts +15 -0
  89. package/dist/commands/parameter/remove.js +90 -0
  90. package/dist/commands/parameter/sync.d.ts +14 -0
  91. package/dist/commands/parameter/sync.js +153 -0
  92. package/dist/commands/parameter/update.d.ts +15 -0
  93. package/dist/commands/parameter/update.js +100 -0
  94. package/dist/commands/stage/create.d.ts +28 -0
  95. package/dist/commands/stage/create.js +312 -0
  96. package/dist/commands/stage/list.d.ts +9 -0
  97. package/dist/commands/stage/list.js +63 -0
  98. package/dist/commands/test-api.d.ts +9 -0
  99. package/dist/commands/test-api.js +40 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +1 -0
  102. package/dist/services/auth-service.d.ts +84 -0
  103. package/dist/services/auth-service.js +240 -0
  104. package/dist/services/git.d.ts +46 -0
  105. package/dist/services/git.js +409 -0
  106. package/dist/services/hyperdrive-sigv4.d.ts +449 -0
  107. package/dist/services/hyperdrive-sigv4.js +375 -0
  108. package/dist/services/hyperdrive.d.ts +87 -0
  109. package/dist/services/hyperdrive.js +108 -0
  110. package/dist/services/log-tailer.d.ts +95 -0
  111. package/dist/services/log-tailer.js +242 -0
  112. package/dist/services/tenant-service.d.ts +106 -0
  113. package/dist/services/tenant-service.js +332 -0
  114. package/dist/utils/account-flow.d.ts +74 -0
  115. package/dist/utils/account-flow.js +228 -0
  116. package/dist/utils/auth-flow.d.ts +146 -0
  117. package/dist/utils/auth-flow.js +477 -0
  118. package/dist/utils/git-flow.d.ts +72 -0
  119. package/dist/utils/git-flow.js +232 -0
  120. package/dist/utils/jira-flow.d.ts +71 -0
  121. package/dist/utils/jira-flow.js +120 -0
  122. package/dist/utils/summary-display.d.ts +59 -0
  123. package/dist/utils/summary-display.js +140 -0
  124. package/dist/utils/validation.d.ts +15 -0
  125. package/dist/utils/validation.js +32 -0
  126. package/oclif.manifest.json +2819 -0
  127. package/package.json +112 -0
@@ -0,0 +1,409 @@
1
+ import { execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ export class GitService {
6
+ DEFAULT_EXCLUDES = [
7
+ // Always exclude these regardless of .gitignore
8
+ 'tmp',
9
+ 'temp'
10
+ ];
11
+ async branchExists(tmpProjectPath, branchName, remote) {
12
+ const result = await this.executeCommand(`git show-ref --verify --quiet refs/remotes/${remote}/${branchName}`, tmpProjectPath);
13
+ return result.success;
14
+ }
15
+ async checkoutBranch(tmpProjectPath, branchName, remote) {
16
+ // Clean untracked files before reset
17
+ const cleanResult = await this.executeCommand('git clean -fd', tmpProjectPath);
18
+ if (!cleanResult.success) {
19
+ return {
20
+ error: `Failed to clean untracked files before checkout: ${cleanResult.stderr || cleanResult.stdout || 'Unknown error'}`,
21
+ success: false
22
+ };
23
+ }
24
+ // First reset any local changes to ensure clean checkout
25
+ const resetResult = await this.executeCommand('git reset --hard HEAD', tmpProjectPath);
26
+ if (!resetResult.success) {
27
+ return {
28
+ error: `Failed to reset working directory before checkout: ${resetResult.stderr || resetResult.stdout || 'Unknown error'}`,
29
+ success: false
30
+ };
31
+ }
32
+ // First try to checkout existing local branch
33
+ let result = await this.executeCommand(`git checkout ${branchName}`, tmpProjectPath);
34
+ if (!result.success) {
35
+ // If local branch doesn't exist, create it from remote
36
+ result = await this.executeCommand(`git checkout -b ${branchName} ${remote}/${branchName}`, tmpProjectPath);
37
+ if (!result.success) {
38
+ return {
39
+ error: `Failed to checkout/create branch ${branchName}: ${result.stderr || result.stdout || 'Unknown error'}`,
40
+ success: false
41
+ };
42
+ }
43
+ }
44
+ return { success: true };
45
+ }
46
+ async cleanupTmpDir(tmpDir) {
47
+ try {
48
+ fs.rmSync(tmpDir, { force: true, recursive: true });
49
+ }
50
+ catch (error) {
51
+ // Ignore cleanup errors
52
+ console.warn(`Warning: Failed to cleanup temporary directory: ${tmpDir}`);
53
+ }
54
+ }
55
+ async copyProjectToTmp(sourcePath, tmpDir, verbose = false, progressCallback) {
56
+ // Get exclusion patterns from .gitignore
57
+ const excludePatterns = this.getExcludePatterns(sourcePath);
58
+ if (verbose) {
59
+ progressCallback?.('📂', 'cyan', `Found ${excludePatterns.length} exclusion patterns from .gitignore`);
60
+ console.log('Exclusion patterns:', excludePatterns);
61
+ }
62
+ // Try rsync first for better performance
63
+ if (await this.tryRsyncCopy(sourcePath, tmpDir, excludePatterns, verbose, progressCallback)) {
64
+ return;
65
+ }
66
+ // Fallback to manual copy with exclusions
67
+ progressCallback?.('📂', 'cyan', 'rsync not available, using manual copy with exclusions...');
68
+ await this.copyWithExclusions(sourcePath, tmpDir, excludePatterns, verbose, progressCallback);
69
+ }
70
+ async createTmpDir() {
71
+ const tmpDir = path.join(os.tmpdir(), `hyperdrive-sync-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
72
+ fs.mkdirSync(tmpDir, { recursive: true });
73
+ return tmpDir;
74
+ }
75
+ async fetchRemote(tmpProjectPath, remote) {
76
+ const result = await this.executeCommand(`git fetch ${remote}`, tmpProjectPath, 30000); // 30 second timeout
77
+ if (!result.success) {
78
+ throw new Error(`Failed to fetch from remote ${remote}: ${result.stderr}`);
79
+ }
80
+ }
81
+ async getAllRemoteBranches(tmpProjectPath, remote) {
82
+ const result = await this.executeCommand(`git branch -r`, tmpProjectPath);
83
+ if (!result.success) {
84
+ throw new Error(`Failed to get remote branches: ${result.stderr}`);
85
+ }
86
+ return result.stdout
87
+ .split('\n')
88
+ .map(branch => branch.trim())
89
+ .filter(branch => branch.startsWith(`${remote}/`) && !branch.includes('HEAD'))
90
+ .map(branch => branch.replace(`${remote}/`, ''))
91
+ .filter(branch => branch.length > 0);
92
+ }
93
+ async getCurrentBranch(tmpProjectPath) {
94
+ const result = await this.executeCommand('git branch --show-current', tmpProjectPath);
95
+ if (!result.success) {
96
+ throw new Error(`Failed to get current branch: ${result.stderr}`);
97
+ }
98
+ return result.stdout.trim();
99
+ }
100
+ async mergeBranch(tmpProjectPath, sourceBranch, remote, mergeStrategy) {
101
+ // Clean untracked files before merge
102
+ const cleanResult = await this.executeCommand('git clean -fd', tmpProjectPath);
103
+ if (!cleanResult.success) {
104
+ return { error: `Failed to clean untracked files before merge: ${cleanResult.stderr}`, hasConflicts: false, success: false };
105
+ }
106
+ // Reset to ensure clean state before merge
107
+ const resetResult = await this.executeCommand('git reset --hard HEAD', tmpProjectPath);
108
+ if (!resetResult.success) {
109
+ return { error: `Failed to reset before merge: ${resetResult.stderr}`, hasConflicts: false, success: false };
110
+ }
111
+ // First, ensure we have the latest source branch
112
+ const fetchResult = await this.executeCommand(`git fetch ${remote} ${sourceBranch}`, tmpProjectPath, 30000);
113
+ if (!fetchResult.success) {
114
+ return { error: `Failed to fetch ${sourceBranch}: ${fetchResult.stderr}`, hasConflicts: false, success: false };
115
+ }
116
+ // Attempt merge
117
+ const mergeCommand = mergeStrategy === 'no-ff'
118
+ ? `git merge --no-ff --allow-unrelated-histories ${remote}/${sourceBranch}`
119
+ : `git merge ${mergeStrategy} --allow-unrelated-histories ${remote}/${sourceBranch}`;
120
+ const result = await this.executeCommand(mergeCommand, tmpProjectPath, 30000);
121
+ if (!result.success) {
122
+ // Check if it's a merge conflict
123
+ const statusResult = await this.executeCommand('git status --porcelain', tmpProjectPath);
124
+ const hasConflicts = statusResult.stdout.includes('UU') ||
125
+ statusResult.stdout.includes('AA') ||
126
+ result.stderr.includes('CONFLICT');
127
+ // Abort the merge if there are conflicts
128
+ if (hasConflicts) {
129
+ await this.executeCommand('git merge --abort', tmpProjectPath);
130
+ }
131
+ return {
132
+ error: hasConflicts ? 'Merge conflicts detected' : result.stderr,
133
+ hasConflicts,
134
+ success: false
135
+ };
136
+ }
137
+ return { hasConflicts: false, success: true };
138
+ }
139
+ async pushBranch(tmpProjectPath, branchName, remote) {
140
+ const result = await this.executeCommand(`git push ${remote} ${branchName}`, tmpProjectPath, 30000);
141
+ if (!result.success) {
142
+ return {
143
+ error: `Failed to push branch ${branchName}: ${result.stderr || result.stdout || 'Unknown error'}`,
144
+ success: false
145
+ };
146
+ }
147
+ return { success: true };
148
+ }
149
+ async syncBranches(options, progressCallback) {
150
+ const results = [];
151
+ let tmpDir;
152
+ try {
153
+ progressCallback?.('🏗️', 'blue', 'Creating temporary workspace...');
154
+ // Create temporary directory
155
+ tmpDir = options.tmpDir || await this.createTmpDir();
156
+ progressCallback?.('📂', 'cyan', 'Copying project files (excluding .gitignore patterns)...');
157
+ // Copy current project to tmp directory
158
+ await this.copyProjectToTmp(process.cwd(), tmpDir, options.verbose, progressCallback);
159
+ const tmpProjectPath = path.join(tmpDir, 'project');
160
+ progressCallback?.('🌐', 'blue', 'Fetching remote branches...');
161
+ // Fetch all branches from remote
162
+ await this.fetchRemote(tmpProjectPath, options.remote);
163
+ progressCallback?.('🔍', 'yellow', 'Determining branches to sync...');
164
+ // Determine which branches to sync
165
+ let branchesToSync = [];
166
+ if (options.all) {
167
+ branchesToSync = await this.getAllRemoteBranches(tmpProjectPath, options.remote);
168
+ }
169
+ else if (options.targetBranches && options.targetBranches.length > 0) {
170
+ // Verify the target branches exist
171
+ for (const branch of options.targetBranches) {
172
+ const exists = await this.branchExists(tmpProjectPath, branch, options.remote);
173
+ if (!exists) {
174
+ return [{
175
+ branch,
176
+ error: `Branch ${branch} does not exist on remote ${options.remote}`,
177
+ success: false
178
+ }];
179
+ }
180
+ }
181
+ branchesToSync = options.targetBranches;
182
+ }
183
+ progressCallback?.('⚙️', 'magenta', `Processing ${branchesToSync.length} branches...`);
184
+ // Process each branch
185
+ for (const branch of branchesToSync) {
186
+ // Skip the source branch (no need to merge itself)
187
+ if (branch === options.sourceBranch) {
188
+ continue;
189
+ }
190
+ try {
191
+ progressCallback?.('🔄', 'cyan', `Syncing branch: ${branch}`);
192
+ // Checkout the target branch
193
+ const checkoutResult = await this.checkoutBranch(tmpProjectPath, branch, options.remote);
194
+ if (!checkoutResult.success) {
195
+ results.push({
196
+ branch,
197
+ error: checkoutResult.error,
198
+ success: false
199
+ });
200
+ continue;
201
+ }
202
+ progressCallback?.('🔀', 'yellow', `Merging branch: ${options.remote}/${options.sourceBranch} into ${branch}`);
203
+ // Merge source branch into target branch
204
+ const mergeResult = await this.mergeBranch(tmpProjectPath, options.sourceBranch, options.remote, options.mergeStrategy);
205
+ if (!mergeResult.success) {
206
+ progressCallback?.('❌', 'red', `Merge failed: ${mergeResult.error}`);
207
+ results.push({
208
+ branch,
209
+ conflicts: mergeResult.hasConflicts,
210
+ error: mergeResult.error,
211
+ success: false
212
+ });
213
+ continue;
214
+ }
215
+ progressCallback?.('⬆️', 'blue', ` Pushing branch: ${options.remote}/${branch}`);
216
+ // Pull latest changes from remote before pushing
217
+ progressCallback?.('⬇️', 'blue', ` Pulling latest changes for branch: ${branch}`);
218
+ const pullResult = await this.executeCommand(`git pull ${options.remote} ${branch}`, tmpProjectPath, 30000);
219
+ if (!pullResult.success) {
220
+ // If pull fails, it might be because the branch doesn't exist remotely yet, which is fine
221
+ // We'll try to push anyway and let the push command handle it
222
+ progressCallback?.('⚠️', 'yellow', ` Pull failed (branch might be new): ${pullResult.stderr}`);
223
+ }
224
+ // Push the merged branch
225
+ const pushResult = await this.pushBranch(tmpProjectPath, branch, options.remote);
226
+ if (!pushResult.success) {
227
+ results.push({
228
+ branch,
229
+ error: pushResult.error,
230
+ success: false
231
+ });
232
+ continue;
233
+ }
234
+ progressCallback?.('✅', 'green', `Branch ${branch} synced successfully`);
235
+ results.push({
236
+ branch,
237
+ success: true
238
+ });
239
+ }
240
+ catch (error) {
241
+ results.push({
242
+ branch,
243
+ error: error instanceof Error ? error.message : String(error),
244
+ success: false
245
+ });
246
+ }
247
+ }
248
+ }
249
+ finally {
250
+ progressCallback?.('🧹', 'gray', 'Cleaning up temporary files...');
251
+ // Cleanup temporary directory
252
+ if (tmpDir) {
253
+ await this.cleanupTmpDir(tmpDir);
254
+ }
255
+ }
256
+ return results;
257
+ }
258
+ async copyDirectoryRecursive(sourceDir, targetDir, excludeRegexes, verbose, progressCallback, basePath = '') {
259
+ // Get all items in source directory
260
+ const items = fs.readdirSync(sourceDir, { withFileTypes: true });
261
+ let copiedCount = 0;
262
+ let skippedCount = 0;
263
+ for (const item of items) {
264
+ const relativePath = path.join(basePath, item.name);
265
+ const isExcluded = excludeRegexes.some(regex => regex.test(relativePath) || regex.test(item.name));
266
+ if (isExcluded) {
267
+ skippedCount++;
268
+ if (verbose) {
269
+ console.log(`Skipped: ${relativePath}`);
270
+ }
271
+ continue;
272
+ }
273
+ const sourcePath_item = path.join(sourceDir, item.name);
274
+ const targetPath = path.join(targetDir, item.name);
275
+ if (item.isDirectory()) {
276
+ fs.mkdirSync(targetPath, { recursive: true });
277
+ await this.copyDirectoryRecursive(sourcePath_item, targetPath, excludeRegexes, verbose, progressCallback, relativePath);
278
+ }
279
+ else {
280
+ fs.copyFileSync(sourcePath_item, targetPath);
281
+ copiedCount++;
282
+ if (verbose) {
283
+ console.log(`Copied: ${relativePath}`);
284
+ }
285
+ }
286
+ }
287
+ if (verbose && basePath === '') {
288
+ progressCallback?.('📂', 'green', `Manual copy completed: ${copiedCount} files copied, ${skippedCount} files skipped`);
289
+ }
290
+ }
291
+ async copyWithExclusions(sourcePath, tmpDir, excludePatterns, verbose, progressCallback) {
292
+ const projectDir = path.join(tmpDir, 'project');
293
+ fs.mkdirSync(projectDir, { recursive: true });
294
+ if (verbose) {
295
+ progressCallback?.('📂', 'cyan', 'Starting manual file copy with exclusions...');
296
+ }
297
+ // Convert gitignore patterns to regex for matching
298
+ const excludeRegexes = excludePatterns.map(pattern => this.gitignorePatternToRegex(pattern));
299
+ await this.copyDirectoryRecursive(sourcePath, projectDir, excludeRegexes, verbose, progressCallback);
300
+ }
301
+ async executeCommand(command, cwd, timeout = 10000) {
302
+ try {
303
+ const result = execSync(command, {
304
+ cwd,
305
+ encoding: 'utf8',
306
+ env: {
307
+ ...process.env,
308
+ GIT_TERMINAL_PROMPT: '0' // Disable interactive prompts
309
+ },
310
+ stdio: 'pipe',
311
+ timeout
312
+ });
313
+ return { stderr: '', stdout: result, success: true };
314
+ }
315
+ catch (error) {
316
+ const execError = error;
317
+ // Handle timeout specifically
318
+ if (execError.code === 'ETIMEDOUT') {
319
+ return {
320
+ stderr: `Command timed out after ${timeout}ms: ${command}`,
321
+ stdout: '',
322
+ success: false
323
+ };
324
+ }
325
+ return {
326
+ stderr: execError.stderr || execError.message || '',
327
+ stdout: execError.stdout || '',
328
+ success: false
329
+ };
330
+ }
331
+ }
332
+ getExcludePatterns(sourcePath) {
333
+ const gitignorePath = path.join(sourcePath, '.gitignore');
334
+ const patterns = [...this.DEFAULT_EXCLUDES];
335
+ try {
336
+ if (fs.existsSync(gitignorePath)) {
337
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
338
+ const gitignorePatterns = this.parseGitignore(gitignoreContent);
339
+ patterns.push(...gitignorePatterns);
340
+ }
341
+ }
342
+ catch (error) {
343
+ console.warn(`Warning: Could not read .gitignore file: ${error instanceof Error ? error.message : String(error)}`);
344
+ }
345
+ return patterns;
346
+ }
347
+ gitignorePatternToRegex(pattern) {
348
+ // Escape special regex characters except for * and ?
349
+ let regexPattern = pattern
350
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
351
+ .replace(/\*/g, '.*')
352
+ .replace(/\?/g, '.');
353
+ // If pattern ends with /, it only matches directories
354
+ if (pattern.endsWith('/')) {
355
+ regexPattern = regexPattern.slice(0, -1) + '$';
356
+ }
357
+ // If pattern doesn't start with /, it can match at any level
358
+ if (!pattern.startsWith('/')) {
359
+ regexPattern = `(^|/)${regexPattern}`;
360
+ }
361
+ return new RegExp(regexPattern);
362
+ }
363
+ parseGitignore(content) {
364
+ return content
365
+ .split('\n')
366
+ .map(line => line.trim())
367
+ .filter(line => {
368
+ // Skip empty lines and comments
369
+ if (!line || line.startsWith('#'))
370
+ return false;
371
+ // Skip negation patterns for simplicity (lines starting with !)
372
+ if (line.startsWith('!'))
373
+ return false;
374
+ return true;
375
+ })
376
+ .map(line => {
377
+ // Remove leading slash for rsync compatibility
378
+ if (line.startsWith('/')) {
379
+ return line.substring(1);
380
+ }
381
+ return line;
382
+ });
383
+ }
384
+ async tryRsyncCopy(sourcePath, tmpDir, excludePatterns, verbose, progressCallback) {
385
+ try {
386
+ // Build rsync exclude patterns
387
+ const excludeArgs = excludePatterns.map(pattern => `--exclude='${pattern}'`).join(' ');
388
+ // Add rsync flags for verbose output if requested
389
+ const verboseFlag = verbose ? '-v' : '';
390
+ const command = `rsync -a${verboseFlag} ${excludeArgs} "${sourcePath}/" "${tmpDir}/project/"`;
391
+ if (verbose) {
392
+ progressCallback?.('⚡', 'cyan', `Executing: ${command}`);
393
+ }
394
+ const result = await this.executeCommand(command, process.cwd(), 60000); // 60 second timeout
395
+ if (verbose && result.stdout) {
396
+ const files = result.stdout.split('\n').filter(line => line.trim() && !line.endsWith('/'));
397
+ progressCallback?.('⚡', 'green', `Copied ${files.length} files using rsync`);
398
+ console.log('Files copied:', files);
399
+ }
400
+ return result.success;
401
+ }
402
+ catch (error) {
403
+ if (verbose) {
404
+ progressCallback?.('⚠️', 'yellow', `rsync failed: ${error instanceof Error ? error.message : String(error)}`);
405
+ }
406
+ return false;
407
+ }
408
+ }
409
+ }