@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.
- package/README.md +1598 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +3 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/account/add.d.ts +16 -0
- package/dist/commands/account/add.js +185 -0
- package/dist/commands/account/list.d.ts +6 -0
- package/dist/commands/account/list.js +37 -0
- package/dist/commands/account/remove.d.ts +11 -0
- package/dist/commands/account/remove.js +57 -0
- package/dist/commands/auth/login.d.ts +16 -0
- package/dist/commands/auth/login.js +178 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.js +39 -0
- package/dist/commands/auth/refresh.d.ts +6 -0
- package/dist/commands/auth/refresh.js +66 -0
- package/dist/commands/auth/status.d.ts +6 -0
- package/dist/commands/auth/status.js +63 -0
- package/dist/commands/ci/account/create.d.ts +16 -0
- package/dist/commands/ci/account/create.js +158 -0
- package/dist/commands/ci/account/delete.d.ts +14 -0
- package/dist/commands/ci/account/delete.js +88 -0
- package/dist/commands/ci/account/list.d.ts +10 -0
- package/dist/commands/ci/account/list.js +65 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +37 -0
- package/dist/commands/config/set.d.ts +10 -0
- package/dist/commands/config/set.js +48 -0
- package/dist/commands/config/show.d.ts +6 -0
- package/dist/commands/config/show.js +10 -0
- package/dist/commands/deployment/create.d.ts +30 -0
- package/dist/commands/deployment/create.js +188 -0
- package/dist/commands/deployment/get.d.ts +13 -0
- package/dist/commands/deployment/get.js +101 -0
- package/dist/commands/deployment/launch.d.ts +15 -0
- package/dist/commands/deployment/launch.js +105 -0
- package/dist/commands/deployment/list.d.ts +11 -0
- package/dist/commands/deployment/list.js +91 -0
- package/dist/commands/domain/current.d.ts +6 -0
- package/dist/commands/domain/current.js +18 -0
- package/dist/commands/domain/list.d.ts +6 -0
- package/dist/commands/domain/list.js +42 -0
- package/dist/commands/domain/switch.d.ts +9 -0
- package/dist/commands/domain/switch.js +40 -0
- package/dist/commands/example.d.ts +13 -0
- package/dist/commands/example.js +24 -0
- package/dist/commands/git/connect.d.ts +10 -0
- package/dist/commands/git/connect.js +56 -0
- package/dist/commands/git/disconnect.d.ts +11 -0
- package/dist/commands/git/disconnect.js +93 -0
- package/dist/commands/git/list.d.ts +10 -0
- package/dist/commands/git/list.js +53 -0
- package/dist/commands/git/sync.d.ts +18 -0
- package/dist/commands/git/sync.js +235 -0
- package/dist/commands/init.d.ts +188 -0
- package/dist/commands/init.js +817 -0
- package/dist/commands/jira/connect.d.ts +9 -0
- package/dist/commands/jira/connect.js +141 -0
- package/dist/commands/jira/status.d.ts +9 -0
- package/dist/commands/jira/status.js +118 -0
- package/dist/commands/module/analyze.d.ts +29 -0
- package/dist/commands/module/analyze.js +201 -0
- package/dist/commands/module/create.d.ts +42 -0
- package/dist/commands/module/create.js +498 -0
- package/dist/commands/module/destroy.d.ts +11 -0
- package/dist/commands/module/destroy.js +77 -0
- package/dist/commands/module/get.d.ts +10 -0
- package/dist/commands/module/get.js +43 -0
- package/dist/commands/module/link.d.ts +15 -0
- package/dist/commands/module/link.js +175 -0
- package/dist/commands/module/list.d.ts +9 -0
- package/dist/commands/module/list.js +51 -0
- package/dist/commands/module/reanalyze.d.ts +30 -0
- package/dist/commands/module/reanalyze.js +206 -0
- package/dist/commands/module/update.d.ts +27 -0
- package/dist/commands/module/update.js +102 -0
- package/dist/commands/parameter/add.d.ts +15 -0
- package/dist/commands/parameter/add.js +99 -0
- package/dist/commands/parameter/backfill.d.ts +12 -0
- package/dist/commands/parameter/backfill.js +113 -0
- package/dist/commands/parameter/clear.d.ts +14 -0
- package/dist/commands/parameter/clear.js +95 -0
- package/dist/commands/parameter/list.d.ts +14 -0
- package/dist/commands/parameter/list.js +92 -0
- package/dist/commands/parameter/pull.d.ts +14 -0
- package/dist/commands/parameter/pull.js +124 -0
- package/dist/commands/parameter/remove.d.ts +15 -0
- package/dist/commands/parameter/remove.js +90 -0
- package/dist/commands/parameter/sync.d.ts +14 -0
- package/dist/commands/parameter/sync.js +153 -0
- package/dist/commands/parameter/update.d.ts +15 -0
- package/dist/commands/parameter/update.js +100 -0
- package/dist/commands/stage/create.d.ts +28 -0
- package/dist/commands/stage/create.js +312 -0
- package/dist/commands/stage/list.d.ts +9 -0
- package/dist/commands/stage/list.js +63 -0
- package/dist/commands/test-api.d.ts +9 -0
- package/dist/commands/test-api.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/auth-service.d.ts +84 -0
- package/dist/services/auth-service.js +240 -0
- package/dist/services/git.d.ts +46 -0
- package/dist/services/git.js +409 -0
- package/dist/services/hyperdrive-sigv4.d.ts +449 -0
- package/dist/services/hyperdrive-sigv4.js +375 -0
- package/dist/services/hyperdrive.d.ts +87 -0
- package/dist/services/hyperdrive.js +108 -0
- package/dist/services/log-tailer.d.ts +95 -0
- package/dist/services/log-tailer.js +242 -0
- package/dist/services/tenant-service.d.ts +106 -0
- package/dist/services/tenant-service.js +332 -0
- package/dist/utils/account-flow.d.ts +74 -0
- package/dist/utils/account-flow.js +228 -0
- package/dist/utils/auth-flow.d.ts +146 -0
- package/dist/utils/auth-flow.js +477 -0
- package/dist/utils/git-flow.d.ts +72 -0
- package/dist/utils/git-flow.js +232 -0
- package/dist/utils/jira-flow.d.ts +71 -0
- package/dist/utils/jira-flow.js +120 -0
- package/dist/utils/summary-display.d.ts +59 -0
- package/dist/utils/summary-display.js +140 -0
- package/dist/utils/validation.d.ts +15 -0
- package/dist/utils/validation.js +32 -0
- package/oclif.manifest.json +2819 -0
- 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
|
+
}
|