agileflow 2.89.0 → 2.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/lib/file-cache.js +4 -3
- package/lib/progress.js +6 -4
- package/lib/validate.js +1 -2
- package/lib/yaml-utils.js +1 -5
- package/package.json +1 -1
- package/scripts/agileflow-welcome.js +42 -7
- package/scripts/batch-pmap-loop.js +21 -12
- package/scripts/obtain-context.js +3 -3
- package/scripts/precompact-context.sh +14 -0
- package/scripts/session-manager.js +16 -10
- package/scripts/test-session-boundary.js +2 -2
- package/tools/cli/commands/config.js +1 -5
- package/tools/cli/commands/setup.js +1 -5
- package/tools/cli/installers/ide/_base-ide.js +6 -16
- package/tools/cli/installers/ide/cursor.js +1 -0
- package/tools/cli/installers/ide/windsurf.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.89.1] - 2026-01-14
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Fix active_commands preservation after conversation compact
|
|
14
|
+
|
|
10
15
|
## [2.89.0] - 2026-01-14
|
|
11
16
|
|
|
12
17
|
### Added
|
package/lib/file-cache.js
CHANGED
|
@@ -130,9 +130,10 @@ class LRUCache {
|
|
|
130
130
|
...this.stats,
|
|
131
131
|
size: this.cache.size,
|
|
132
132
|
maxSize: this.maxSize,
|
|
133
|
-
hitRate:
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
hitRate:
|
|
134
|
+
this.stats.hits + this.stats.misses > 0
|
|
135
|
+
? ((this.stats.hits / (this.stats.hits + this.stats.misses)) * 100).toFixed(1) + '%'
|
|
136
|
+
: '0%',
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
|
package/lib/progress.js
CHANGED
|
@@ -168,7 +168,8 @@ class Spinner {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
const elapsed = this.startTime ? Date.now() - this.startTime : 0;
|
|
171
|
-
const suffix =
|
|
171
|
+
const suffix =
|
|
172
|
+
elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
|
|
172
173
|
|
|
173
174
|
console.log(`${color}${symbol}${c.reset} ${message}${suffix}`);
|
|
174
175
|
return this;
|
|
@@ -179,7 +180,7 @@ class Spinner {
|
|
|
179
180
|
* @returns {boolean}
|
|
180
181
|
*/
|
|
181
182
|
wasFast() {
|
|
182
|
-
return this.startTime &&
|
|
183
|
+
return this.startTime && Date.now() - this.startTime < DOHERTY_THRESHOLD_MS;
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
|
|
@@ -263,7 +264,7 @@ class ProgressBar {
|
|
|
263
264
|
return this;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
const percent = this.total > 0 ?
|
|
267
|
+
const percent = this.total > 0 ? current / this.total : 0;
|
|
267
268
|
const filled = Math.round(this.width * percent);
|
|
268
269
|
const empty = this.width - filled;
|
|
269
270
|
|
|
@@ -300,7 +301,8 @@ class ProgressBar {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
const elapsed = Date.now() - this.startTime;
|
|
303
|
-
const suffix =
|
|
304
|
+
const suffix =
|
|
305
|
+
elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
|
|
304
306
|
const msg = message || `${this.label} complete`;
|
|
305
307
|
|
|
306
308
|
console.log(`${c.green}✓${c.reset} ${msg} (${this.total} items)${suffix}`);
|
package/lib/validate.js
CHANGED
|
@@ -427,8 +427,7 @@ function validatePath(inputPath, baseDir, options = {}) {
|
|
|
427
427
|
? normalizedBase
|
|
428
428
|
: normalizedBase + path.sep;
|
|
429
429
|
|
|
430
|
-
const isWithinBase =
|
|
431
|
-
resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
|
|
430
|
+
const isWithinBase = resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
|
|
432
431
|
|
|
433
432
|
if (!isWithinBase) {
|
|
434
433
|
return {
|
package/lib/yaml-utils.js
CHANGED
|
@@ -53,11 +53,7 @@ function safeLoadAll(content, options = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const documents = [];
|
|
56
|
-
yaml.loadAll(
|
|
57
|
-
content,
|
|
58
|
-
(doc) => documents.push(doc),
|
|
59
|
-
{ schema: yaml.DEFAULT_SCHEMA, ...options }
|
|
60
|
-
);
|
|
56
|
+
yaml.loadAll(content, doc => documents.push(doc), { schema: yaml.DEFAULT_SCHEMA, ...options });
|
|
61
57
|
return documents;
|
|
62
58
|
}
|
|
63
59
|
|
package/package.json
CHANGED
|
@@ -274,7 +274,7 @@ function runArchival(rootDir, cache = null) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
function clearActiveCommands(rootDir, cache = null) {
|
|
277
|
-
const result = { ran: false, cleared: 0, commandNames: [] };
|
|
277
|
+
const result = { ran: false, cleared: 0, commandNames: [], preserved: false };
|
|
278
278
|
|
|
279
279
|
try {
|
|
280
280
|
const sessionStatePath = path.join(rootDir, 'docs/09-agents/session-state.json');
|
|
@@ -291,6 +291,31 @@ function clearActiveCommands(rootDir, cache = null) {
|
|
|
291
291
|
result.ran = true;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Check if PreCompact just ran (within last 30 seconds)
|
|
295
|
+
// If so, preserve active_commands instead of clearing them (post-compact session start)
|
|
296
|
+
if (state.last_precompact_at) {
|
|
297
|
+
const precompactTime = new Date(state.last_precompact_at).getTime();
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const secondsSincePrecompact = (now - precompactTime) / 1000;
|
|
300
|
+
|
|
301
|
+
if (secondsSincePrecompact < 30) {
|
|
302
|
+
// This is a post-compact session start - preserve active commands
|
|
303
|
+
result.preserved = true;
|
|
304
|
+
// Capture command names for display (but don't clear)
|
|
305
|
+
if (state.active_commands && state.active_commands.length > 0) {
|
|
306
|
+
for (const cmd of state.active_commands) {
|
|
307
|
+
if (cmd.name) result.commandNames.push(cmd.name);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Clear the precompact timestamp so next true session start will clear
|
|
311
|
+
delete state.last_precompact_at;
|
|
312
|
+
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
// Precompact was too long ago - clear as normal
|
|
316
|
+
delete state.last_precompact_at;
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
// Handle new array format (active_commands)
|
|
295
320
|
if (state.active_commands && state.active_commands.length > 0) {
|
|
296
321
|
result.cleared = state.active_commands.length;
|
|
@@ -1061,10 +1086,18 @@ function formatTable(
|
|
|
1061
1086
|
}
|
|
1062
1087
|
|
|
1063
1088
|
// Session cleanup
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1089
|
+
let sessionStatus, sessionColor;
|
|
1090
|
+
if (session.preserved) {
|
|
1091
|
+
sessionStatus = `preserved ${session.commandNames.length} command(s)`;
|
|
1092
|
+
sessionColor = c.mintGreen;
|
|
1093
|
+
} else if (session.cleared > 0) {
|
|
1094
|
+
sessionStatus = `cleared ${session.cleared} command(s)`;
|
|
1095
|
+
sessionColor = c.mintGreen;
|
|
1096
|
+
} else {
|
|
1097
|
+
sessionStatus = `clean`;
|
|
1098
|
+
sessionColor = c.dim;
|
|
1099
|
+
}
|
|
1100
|
+
lines.push(row('Session state', sessionStatus, c.lavender, sessionColor));
|
|
1068
1101
|
|
|
1069
1102
|
// PreCompact status with version check
|
|
1070
1103
|
if (precompact.configured && precompact.scriptExists) {
|
|
@@ -1269,12 +1302,14 @@ async function main() {
|
|
|
1269
1302
|
if (parallelSessions.cleaned > 0 && parallelSessions.cleanedSessions) {
|
|
1270
1303
|
console.log('');
|
|
1271
1304
|
console.log(`${c.amber}📋 Cleaned ${parallelSessions.cleaned} inactive session(s):${c.reset}`);
|
|
1272
|
-
parallelSessions.cleanedSessions.forEach(
|
|
1305
|
+
parallelSessions.cleanedSessions.forEach(sess => {
|
|
1273
1306
|
const name = sess.nickname ? `${sess.id} "${sess.nickname}"` : `Session ${sess.id}`;
|
|
1274
1307
|
const reason = sess.reason === 'pid_dead' ? 'process ended' : sess.reason;
|
|
1275
1308
|
console.log(` ${c.dim}└─ ${name} (${reason}, PID ${sess.pid})${c.reset}`);
|
|
1276
1309
|
});
|
|
1277
|
-
console.log(
|
|
1310
|
+
console.log(
|
|
1311
|
+
` ${c.slate}Sessions are cleaned when their Claude Code process is no longer running.${c.reset}`
|
|
1312
|
+
);
|
|
1278
1313
|
}
|
|
1279
1314
|
|
|
1280
1315
|
// Story claiming: cleanup stale claims and show warnings
|
|
@@ -46,11 +46,14 @@ function saveSessionState(rootDir, state) {
|
|
|
46
46
|
async function resolveGlob(pattern, rootDir) {
|
|
47
47
|
// Use bash globbing for pattern expansion
|
|
48
48
|
try {
|
|
49
|
-
const result = execSync(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const result = execSync(
|
|
50
|
+
`bash -c 'shopt -s nullglob; for f in ${pattern}; do echo "$f"; done'`,
|
|
51
|
+
{
|
|
52
|
+
cwd: rootDir,
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
54
57
|
const files = result
|
|
55
58
|
.split('\n')
|
|
56
59
|
.filter(f => f.trim())
|
|
@@ -218,7 +221,9 @@ function handleBatchLoop(rootDir) {
|
|
|
218
221
|
);
|
|
219
222
|
console.log('');
|
|
220
223
|
console.log(`${c.green}Pattern: ${loop.pattern}${c.reset}`);
|
|
221
|
-
console.log(
|
|
224
|
+
console.log(
|
|
225
|
+
`${c.dim}${summary.completed} items completed in ${iteration - 1} iterations${c.reset}`
|
|
226
|
+
);
|
|
222
227
|
return;
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -287,7 +292,9 @@ function handleBatchLoop(rootDir) {
|
|
|
287
292
|
console.log(`${c.cyan}━━━ Next Item ━━━${c.reset}`);
|
|
288
293
|
console.log(`${c.bold}${nextItem}${c.reset}`);
|
|
289
294
|
console.log('');
|
|
290
|
-
console.log(
|
|
295
|
+
console.log(
|
|
296
|
+
`${c.dim}Progress: ${summary.completed}/${summary.total} items complete${c.reset}`
|
|
297
|
+
);
|
|
291
298
|
console.log('');
|
|
292
299
|
console.log(`${c.brand}▶ Implement "${loop.action}" for this file${c.reset}`);
|
|
293
300
|
console.log(`${c.dim} Run tests when ready. Loop will validate and continue.${c.reset}`);
|
|
@@ -310,7 +317,9 @@ function handleBatchLoop(rootDir) {
|
|
|
310
317
|
console.log('');
|
|
311
318
|
console.log(`${c.green}Pattern: ${loop.pattern}${c.reset}`);
|
|
312
319
|
console.log(`${c.green}Action: ${loop.action}${c.reset}`);
|
|
313
|
-
console.log(
|
|
320
|
+
console.log(
|
|
321
|
+
`${c.dim}${summary.completed} items completed in ${iteration} iterations${c.reset}`
|
|
322
|
+
);
|
|
314
323
|
}
|
|
315
324
|
} else {
|
|
316
325
|
// Tests failed - continue iterating
|
|
@@ -353,9 +362,7 @@ async function handleInit(args, rootDir) {
|
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
const pattern = patternArg.split('=').slice(1).join('=').replace(/"/g, '');
|
|
356
|
-
const action = actionArg
|
|
357
|
-
? actionArg.split('=').slice(1).join('=').replace(/"/g, '')
|
|
358
|
-
: 'process';
|
|
365
|
+
const action = actionArg ? actionArg.split('=').slice(1).join('=').replace(/"/g, '') : 'process';
|
|
359
366
|
const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 50, 1, 200);
|
|
360
367
|
|
|
361
368
|
// Resolve glob pattern
|
|
@@ -432,7 +439,9 @@ function handleStatus(rootDir) {
|
|
|
432
439
|
console.log(` Pattern: ${loop.pattern}`);
|
|
433
440
|
console.log(` Action: ${loop.action}`);
|
|
434
441
|
console.log(` Current Item: ${loop.current_item || 'none'}`);
|
|
435
|
-
console.log(
|
|
442
|
+
console.log(
|
|
443
|
+
` Progress: ${summary.completed}/${summary.total} (${summary.in_progress} in progress)`
|
|
444
|
+
);
|
|
436
445
|
console.log(` Iteration: ${loop.iteration || 0}/${loop.max_iterations || 50}`);
|
|
437
446
|
}
|
|
438
447
|
|
|
@@ -461,10 +461,10 @@ function generateFullContent() {
|
|
|
461
461
|
'loop-mode': 'Autonomous epic execution (MODE=loop)',
|
|
462
462
|
'multi-session': 'Multi-session coordination detected',
|
|
463
463
|
'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
delegation: 'Expert spawning patterns (load when spawning)',
|
|
465
|
+
stuck: 'Research prompt guidance (load after 2 failures)',
|
|
466
466
|
'plan-mode': 'Planning workflow details (load when entering plan mode)',
|
|
467
|
-
|
|
467
|
+
tools: 'Tool usage guidance (load when needed)',
|
|
468
468
|
};
|
|
469
469
|
|
|
470
470
|
content += `\n${C.dim}Section meanings:${C.reset}\n`;
|
|
@@ -121,3 +121,17 @@ cat << EOF
|
|
|
121
121
|
3. Review docs/02-practices/ for implementation patterns
|
|
122
122
|
4. Check git log for recent changes
|
|
123
123
|
EOF
|
|
124
|
+
|
|
125
|
+
# Mark that PreCompact just ran - tells SessionStart to preserve active_commands
|
|
126
|
+
# This prevents the welcome script from clearing commands right after compact
|
|
127
|
+
if [ -f "docs/09-agents/session-state.json" ]; then
|
|
128
|
+
node -e "
|
|
129
|
+
const fs = require('fs');
|
|
130
|
+
const path = 'docs/09-agents/session-state.json';
|
|
131
|
+
try {
|
|
132
|
+
const state = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
133
|
+
state.last_precompact_at = new Date().toISOString();
|
|
134
|
+
fs.writeFileSync(path, JSON.stringify(state, null, 2) + '\n');
|
|
135
|
+
} catch (e) {}
|
|
136
|
+
" 2>/dev/null
|
|
137
|
+
fi
|
|
@@ -228,9 +228,8 @@ function registerSession(nickname = null, threadType = null) {
|
|
|
228
228
|
registry.next_id++;
|
|
229
229
|
|
|
230
230
|
const isMain = cwd === ROOT;
|
|
231
|
-
const detectedType =
|
|
232
|
-
? threadType
|
|
233
|
-
: detectThreadType(null, !isMain);
|
|
231
|
+
const detectedType =
|
|
232
|
+
threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
|
|
234
233
|
|
|
235
234
|
registry.sessions[sessionId] = {
|
|
236
235
|
path: cwd,
|
|
@@ -750,10 +749,10 @@ function getSessionPhase(session) {
|
|
|
750
749
|
|
|
751
750
|
// Count commits since branch diverged from main
|
|
752
751
|
const mainBranch = getMainBranch();
|
|
753
|
-
const commitCount = execSync(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
).trim();
|
|
752
|
+
const commitCount = execSync(`git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`, {
|
|
753
|
+
cwd: sessionPath,
|
|
754
|
+
encoding: 'utf8',
|
|
755
|
+
}).trim();
|
|
757
756
|
|
|
758
757
|
const commits = parseInt(commitCount, 10);
|
|
759
758
|
|
|
@@ -1037,7 +1036,9 @@ function main() {
|
|
|
1037
1036
|
if (nickname) registry.sessions[sessionId].nickname = nickname;
|
|
1038
1037
|
// Ensure thread_type exists (migration for old sessions)
|
|
1039
1038
|
if (!registry.sessions[sessionId].thread_type) {
|
|
1040
|
-
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
|
|
1039
|
+
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
|
|
1040
|
+
? 'base'
|
|
1041
|
+
: 'parallel';
|
|
1041
1042
|
}
|
|
1042
1043
|
writeLock(sessionId, pid);
|
|
1043
1044
|
} else {
|
|
@@ -1218,7 +1219,9 @@ function main() {
|
|
|
1218
1219
|
const sessionId = args[2];
|
|
1219
1220
|
const threadType = args[3];
|
|
1220
1221
|
if (!sessionId || !threadType) {
|
|
1221
|
-
console.log(
|
|
1222
|
+
console.log(
|
|
1223
|
+
JSON.stringify({ success: false, error: 'Usage: thread-type set <sessionId> <type>' })
|
|
1224
|
+
);
|
|
1222
1225
|
return;
|
|
1223
1226
|
}
|
|
1224
1227
|
const result = setSessionThreadType(sessionId, threadType);
|
|
@@ -1910,7 +1913,10 @@ function getSessionThreadType(sessionId = null) {
|
|
|
1910
1913
|
*/
|
|
1911
1914
|
function setSessionThreadType(sessionId, threadType) {
|
|
1912
1915
|
if (!THREAD_TYPES.includes(threadType)) {
|
|
1913
|
-
return {
|
|
1916
|
+
return {
|
|
1917
|
+
success: false,
|
|
1918
|
+
error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
|
|
1919
|
+
};
|
|
1914
1920
|
}
|
|
1915
1921
|
|
|
1916
1922
|
const registry = loadRegistry();
|
|
@@ -63,8 +63,8 @@ console.log(`File Being Edited: ${normalizedFile}`);
|
|
|
63
63
|
console.log('');
|
|
64
64
|
|
|
65
65
|
// Check if file is within active session path
|
|
66
|
-
const isInsideSession =
|
|
67
|
-
|
|
66
|
+
const isInsideSession =
|
|
67
|
+
normalizedFile.startsWith(normalizedActive + path.sep) || normalizedFile === normalizedActive;
|
|
68
68
|
|
|
69
69
|
if (isInsideSession) {
|
|
70
70
|
console.log('✅ ALLOWED - File is inside the active session directory');
|
|
@@ -130,11 +130,7 @@ async function handleGet(status, key) {
|
|
|
130
130
|
const handler = new ErrorHandler('config');
|
|
131
131
|
|
|
132
132
|
if (!key) {
|
|
133
|
-
handler.warning(
|
|
134
|
-
'Missing key',
|
|
135
|
-
'Provide a config key to get',
|
|
136
|
-
'npx agileflow config get <key>'
|
|
137
|
-
);
|
|
133
|
+
handler.warning('Missing key', 'Provide a config key to get', 'npx agileflow config get <key>');
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
const validKeys = ['userName', 'ides', 'agileflowFolder', 'docsFolder', 'version'];
|
|
@@ -103,11 +103,7 @@ module.exports = {
|
|
|
103
103
|
|
|
104
104
|
if (!coreResult.success) {
|
|
105
105
|
const handler = new ErrorHandler('setup');
|
|
106
|
-
handler.warning(
|
|
107
|
-
'Core setup failed',
|
|
108
|
-
'Check directory permissions',
|
|
109
|
-
'npx agileflow doctor'
|
|
110
|
-
);
|
|
106
|
+
handler.warning('Core setup failed', 'Check directory permissions', 'npx agileflow doctor');
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
success(`Installed ${coreResult.counts.agents} agents`);
|
|
@@ -195,11 +195,7 @@ class BaseIdeSetup {
|
|
|
195
195
|
`Permission denied: ${error.message}`
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
|
-
throw new CleanupError(
|
|
199
|
-
this.displayName,
|
|
200
|
-
agileflowPath,
|
|
201
|
-
error.message
|
|
202
|
-
);
|
|
198
|
+
throw new CleanupError(this.displayName, agileflowPath, error.message);
|
|
203
199
|
}
|
|
204
200
|
}
|
|
205
201
|
}
|
|
@@ -332,11 +328,7 @@ class BaseIdeSetup {
|
|
|
332
328
|
try {
|
|
333
329
|
content = this.injectDynamicContent(content, agileflowDir);
|
|
334
330
|
} catch (injectionError) {
|
|
335
|
-
throw new ContentInjectionError(
|
|
336
|
-
this.displayName,
|
|
337
|
-
sourcePath,
|
|
338
|
-
injectionError.message
|
|
339
|
-
);
|
|
331
|
+
throw new ContentInjectionError(this.displayName, sourcePath, injectionError.message);
|
|
340
332
|
}
|
|
341
333
|
}
|
|
342
334
|
|
|
@@ -350,12 +342,10 @@ class BaseIdeSetup {
|
|
|
350
342
|
if (error.name && error.name.includes('Error') && error.ideName) {
|
|
351
343
|
throw error;
|
|
352
344
|
}
|
|
353
|
-
throw new CommandInstallationError(
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ sourcePath, targetPath }
|
|
358
|
-
);
|
|
345
|
+
throw new CommandInstallationError(this.displayName, entry.name, error.message, {
|
|
346
|
+
sourcePath,
|
|
347
|
+
targetPath,
|
|
348
|
+
});
|
|
359
349
|
}
|
|
360
350
|
} else if (entry.isDirectory()) {
|
|
361
351
|
// Recursively process subdirectory
|