@namzu/sdk 35.0.0 → 36.0.0
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 +38 -0
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +56 -8
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +1 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +50 -10
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +7 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +3 -3
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +31 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +10 -5
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +14 -4
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/sandbox/file-walk-program.d.ts +3 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -0
- package/dist/sandbox/file-walk-program.js +94 -0
- package/dist/sandbox/file-walk-program.js.map +1 -0
- package/dist/sandbox/file-walk.d.ts +12 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -0
- package/dist/sandbox/file-walk.js +429 -0
- package/dist/sandbox/file-walk.js.map +1 -0
- package/dist/sandbox/index.d.ts +2 -0
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +1 -0
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +9 -0
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +3 -2
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +29 -15
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +215 -59
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +2 -1
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +95 -103
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +155 -111
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/ls.js +2 -2
- package/dist/tools/builtins/ls.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +20 -0
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/package.json +6 -1
- package/src/provider/token-budget.ts +52 -8
- package/src/public-runtime.ts +2 -1
- package/src/runtime/query/executor.ts +54 -14
- package/src/runtime/query/index.ts +8 -0
- package/src/runtime/query/iteration/index.ts +29 -9
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/stream-turn.ts +9 -5
- package/src/runtime/query/prompt.ts +21 -4
- package/src/sandbox/file-walk-program.ts +93 -0
- package/src/sandbox/file-walk.ts +463 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/provider/local.ts +13 -0
- package/src/scheduler/completion-inbox.ts +29 -19
- package/src/tools/builtins/bash.ts +230 -58
- package/src/tools/builtins/glob.ts +104 -122
- package/src/tools/builtins/grep.ts +166 -132
- package/src/tools/builtins/ls.ts +2 -2
- package/src/types/sandbox/index.ts +21 -0
package/src/public-runtime.ts
CHANGED
|
@@ -649,8 +649,9 @@ export {
|
|
|
649
649
|
LocalSandboxProvider,
|
|
650
650
|
missingIsolation,
|
|
651
651
|
SandboxProviderFactory,
|
|
652
|
+
walkFilesViaExec,
|
|
652
653
|
} from './sandbox/index.js'
|
|
653
|
-
export type { LocalSandboxProviderOptions } from './sandbox/index.js'
|
|
654
|
+
export type { LocalSandboxProviderOptions, SandboxFileWalkExec } from './sandbox/index.js'
|
|
654
655
|
|
|
655
656
|
// The classified provider-failure surface: a driver states what went wrong
|
|
656
657
|
// first-hand, and the run boundary reads it to choose between a pause and a
|
|
@@ -1279,6 +1279,9 @@ export class ToolExecutor {
|
|
|
1279
1279
|
settleProgress: () => Promise<void>,
|
|
1280
1280
|
preparedCall?: PreparedDirectCall,
|
|
1281
1281
|
): Promise<ToolCallOutcome> {
|
|
1282
|
+
if (this.config.abortSignal.aborted) {
|
|
1283
|
+
return this.recordCancelledBeforeExecution(toolCall.id, toolCall.function.name, {})
|
|
1284
|
+
}
|
|
1282
1285
|
if (preparedCall?.kind === 'synthetic') {
|
|
1283
1286
|
return this.recordSyntheticPreparation(preparedCall)
|
|
1284
1287
|
}
|
|
@@ -1376,7 +1379,15 @@ export class ToolExecutor {
|
|
|
1376
1379
|
|
|
1377
1380
|
input = resolved.input
|
|
1378
1381
|
|
|
1379
|
-
|
|
1382
|
+
let preOutcome: PreToolHookOutcome
|
|
1383
|
+
try {
|
|
1384
|
+
preOutcome = await this.runPreToolHook(toolName, input)
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
if (!this.config.abortSignal.aborted) throw error
|
|
1387
|
+
// A later call's interrupted preparation must not reject the batch
|
|
1388
|
+
// that already holds an earlier call's settled side-effect receipt.
|
|
1389
|
+
return this.recordCancelledBeforeExecution(toolCall.id, toolName, input)
|
|
1390
|
+
}
|
|
1380
1391
|
if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
|
|
1381
1392
|
return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome)
|
|
1382
1393
|
}
|
|
@@ -1483,6 +1494,7 @@ export class ToolExecutor {
|
|
|
1483
1494
|
...this.config.toolRetryBackoff,
|
|
1484
1495
|
}
|
|
1485
1496
|
for (let attempt = 1; ; attempt++) {
|
|
1497
|
+
if (this.config.abortSignal.aborted) break
|
|
1486
1498
|
// A missing file will not appear on the second attempt; burning
|
|
1487
1499
|
// the budget on it only delays the error the model needs to see.
|
|
1488
1500
|
const toolWants = !result.success && result.retryable === true
|
|
@@ -1606,7 +1618,7 @@ export class ToolExecutor {
|
|
|
1606
1618
|
|
|
1607
1619
|
if (this.workingStateManager) {
|
|
1608
1620
|
extractFromToolResult(this.workingStateManager, toolName, output, effectiveIsError)
|
|
1609
|
-
for (const pin of result.workingState ?? []) {
|
|
1621
|
+
for (const pin of this.config.abortSignal.aborted ? [] : (result.workingState ?? [])) {
|
|
1610
1622
|
this.workingStateManager.pin(pin.key, pin.text, toolName)
|
|
1611
1623
|
}
|
|
1612
1624
|
}
|
|
@@ -1623,7 +1635,7 @@ export class ToolExecutor {
|
|
|
1623
1635
|
[NAMZU.RUN_ID]: this.config.runId,
|
|
1624
1636
|
[GENAI.TOOL_NAME]: toolName,
|
|
1625
1637
|
'namzu.duration_ms': durationMs,
|
|
1626
|
-
'exception.message': result.error ?? 'unknown',
|
|
1638
|
+
'exception.message': postOverride ? output : (result.error ?? 'unknown'),
|
|
1627
1639
|
})
|
|
1628
1640
|
}
|
|
1629
1641
|
|
|
@@ -2369,17 +2381,33 @@ export class ToolExecutor {
|
|
|
2369
2381
|
toolResult: ToolResult,
|
|
2370
2382
|
): Promise<{ override: PostToolOverride | null; retry: boolean }> {
|
|
2371
2383
|
if (!this.config.pluginManager) return { override: null, retry: false }
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2384
|
+
let results: PluginHookResult[]
|
|
2385
|
+
try {
|
|
2386
|
+
results = await this.config.pluginManager.executeHooks(
|
|
2387
|
+
'post_tool_use',
|
|
2388
|
+
{
|
|
2389
|
+
runId: this.config.runId,
|
|
2390
|
+
toolName,
|
|
2391
|
+
toolInput: input,
|
|
2392
|
+
toolResult,
|
|
2393
|
+
signal: this.config.abortSignal,
|
|
2394
|
+
},
|
|
2395
|
+
this.emitEvent,
|
|
2396
|
+
)
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
if (!this.config.abortSignal.aborted) throw error
|
|
2399
|
+
// The tool has already returned. Cancellation of a later redaction
|
|
2400
|
+
// cannot erase that execution or turn it into a safe-to-repeat call.
|
|
2401
|
+
// Withhold both channels until their post-tool policy has completed.
|
|
2402
|
+
const outcome = toolResult.success
|
|
2403
|
+
? 'reported success before cancellation. The call already executed.'
|
|
2404
|
+
: 'reported failure while cancellation was in progress. Side effects are possible.'
|
|
2405
|
+
const output = `Tool "${toolName}" ${outcome} Output withheld because post-tool review was interrupted. Inspect external state before any retry.`
|
|
2406
|
+
return {
|
|
2407
|
+
override: { output, content: output, isError: !toolResult.success },
|
|
2408
|
+
retry: false,
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2383
2411
|
let override: PostToolOverride | null = null
|
|
2384
2412
|
let retry = false
|
|
2385
2413
|
for (const result of results) {
|
|
@@ -2484,6 +2512,18 @@ export class ToolExecutor {
|
|
|
2484
2512
|
return { toolCallId: toolCall.id, toolName, output, isError: true }
|
|
2485
2513
|
}
|
|
2486
2514
|
|
|
2515
|
+
private recordCancelledBeforeExecution(
|
|
2516
|
+
toolCallId: string,
|
|
2517
|
+
toolName: string,
|
|
2518
|
+
input: unknown,
|
|
2519
|
+
): Promise<ToolCallOutcome> {
|
|
2520
|
+
const reason = abortReasonText(this.config.abortSignal.reason)
|
|
2521
|
+
return this.recordSyntheticHookOutcome(toolCallId, toolName, input, {
|
|
2522
|
+
kind: 'error',
|
|
2523
|
+
output: `Tool "${toolName}" was not started because the run was cancelled${reason ? `: ${reason}` : '.'}`,
|
|
2524
|
+
})
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2487
2527
|
private async recordSyntheticHookOutcome(
|
|
2488
2528
|
toolCallId: string,
|
|
2489
2529
|
toolName: string,
|
|
@@ -671,6 +671,13 @@ export interface QueryParams {
|
|
|
671
671
|
* started. See `BaseAgentConfig.inboundMessages` for what it closes.
|
|
672
672
|
*/
|
|
673
673
|
inboundMessages?: () => import('../../types/message/index.js').Message[]
|
|
674
|
+
/**
|
|
675
|
+
* Wake a background-task hold when operator input is available, without draining it.
|
|
676
|
+
* Resolve immediately if input is already pending. Otherwise wait for its arrival;
|
|
677
|
+
* the supplied signal ends the wait and must release any listeners. Messages are
|
|
678
|
+
* still consumed only through `inboundMessages` at a provider-valid boundary.
|
|
679
|
+
*/
|
|
680
|
+
waitForInbound?: (signal: AbortSignal) => Promise<void>
|
|
674
681
|
|
|
675
682
|
/**
|
|
676
683
|
* Live project policy for this run. Unlike `inboundMessages`, snapshot
|
|
@@ -1963,6 +1970,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
|
|
|
1963
1970
|
...(params.repeatCallAdvisory === false ? {} : { repeatCalls: new RepeatCallTracker() }),
|
|
1964
1971
|
compactionConfig,
|
|
1965
1972
|
...(params.inboundMessages ? { inboundMessages: params.inboundMessages } : {}),
|
|
1973
|
+
...(params.waitForInbound ? { waitForInbound: params.waitForInbound } : {}),
|
|
1966
1974
|
...(params.projectInstructionContext
|
|
1967
1975
|
? { projectInstructionContext: params.projectInstructionContext }
|
|
1968
1976
|
: {}),
|
|
@@ -1450,9 +1450,9 @@ export class IterationOrchestrator {
|
|
|
1450
1450
|
/**
|
|
1451
1451
|
* Hold the run open for a worker that has not finished, and deliver it.
|
|
1452
1452
|
*
|
|
1453
|
-
* Returns whether a completion
|
|
1454
|
-
* caller continues
|
|
1455
|
-
*
|
|
1453
|
+
* Returns whether a completion or operator message entered the transcript —
|
|
1454
|
+
* the caller continues on `true`, so the model gets a turn to respond.
|
|
1455
|
+
* That turn is the entire justification for waiting, which
|
|
1456
1456
|
* is why only the exits that can still take one call this.
|
|
1457
1457
|
*
|
|
1458
1458
|
* Bounded by `settleGraceMs` and by `maxIterations`, so a worker that never
|
|
@@ -1473,14 +1473,34 @@ export class IterationOrchestrator {
|
|
|
1473
1473
|
[NAMZU.ITERATION]: iterationNum,
|
|
1474
1474
|
'namzu.runtime.grace_ms': graceMs,
|
|
1475
1475
|
})
|
|
1476
|
-
|
|
1476
|
+
// User input releases this wait without cancelling any child. Both waits
|
|
1477
|
+
// share a disposable signal so the losing arrival listener cannot leak.
|
|
1478
|
+
const waiting = new AbortController()
|
|
1479
|
+
const runSignal = this.ctx.abortController.signal
|
|
1480
|
+
const cancelWait = () => waiting.abort(runSignal.reason)
|
|
1481
|
+
runSignal.addEventListener('abort', cancelWait, { once: true })
|
|
1482
|
+
if (runSignal.aborted) cancelWait()
|
|
1483
|
+
try {
|
|
1484
|
+
await Promise.race([
|
|
1485
|
+
this.ctx.completionInbox.waitForArrival(graceMs, waiting.signal),
|
|
1486
|
+
...(this.ctx.waitForInbound ? [this.ctx.waitForInbound(waiting.signal)] : []),
|
|
1487
|
+
])
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
if (!runSignal.aborted) throw error
|
|
1490
|
+
} finally {
|
|
1491
|
+
waiting.abort()
|
|
1492
|
+
runSignal.removeEventListener('abort', cancelWait)
|
|
1493
|
+
}
|
|
1494
|
+
runSignal.throwIfAborted()
|
|
1477
1495
|
|
|
1478
1496
|
const arrived = this.ctx.completionInbox.drain()
|
|
1479
|
-
if (arrived.length
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1497
|
+
if (arrived.length > 0) {
|
|
1498
|
+
this.ctx.runMgr.pushMessage(
|
|
1499
|
+
createRuntimeContextMessage(formatCompletionNotification(arrived), 'task-completion'),
|
|
1500
|
+
)
|
|
1501
|
+
}
|
|
1502
|
+
const inbound = this.deliverInbound()
|
|
1503
|
+
if (arrived.length === 0 && inbound === 0) return false
|
|
1484
1504
|
await this.ctx.emitEvent({
|
|
1485
1505
|
type: 'iteration_completed',
|
|
1486
1506
|
runId: this.ctx.runMgr.id,
|
|
@@ -188,6 +188,8 @@ export interface IterationContext {
|
|
|
188
188
|
* after tool results and before the next turn.
|
|
189
189
|
*/
|
|
190
190
|
readonly inboundMessages?: () => readonly import('../../../../types/message/index.js').Message[]
|
|
191
|
+
/** Observes pending input without draining it; abort releases the waiter. */
|
|
192
|
+
readonly waitForInbound?: (signal: AbortSignal) => Promise<void>
|
|
191
193
|
|
|
192
194
|
/** Live project policy; separate from human inbound continuation. */
|
|
193
195
|
readonly projectInstructionContext?: ProjectInstructionContext
|
|
@@ -518,11 +518,15 @@ export async function* streamProviderTurn(
|
|
|
518
518
|
streamCause = err
|
|
519
519
|
} finally {
|
|
520
520
|
if (onAbort) signal?.removeEventListener('abort', onAbort)
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
521
|
+
// Request cleanup on every exit, but never wait for a blocked provider's
|
|
522
|
+
// pending next(). An async generator queues return() behind that pull;
|
|
523
|
+
// awaiting it would undo the cancellation race above. Provider metering
|
|
524
|
+
// independently records cancellation and any already-issued late receipt.
|
|
525
|
+
try {
|
|
526
|
+
void it.return?.().catch(() => {})
|
|
527
|
+
} catch {
|
|
528
|
+
// Cleanup cannot replace the original stream outcome.
|
|
529
|
+
}
|
|
526
530
|
}
|
|
527
531
|
|
|
528
532
|
// Flush any tool buckets the provider failed to close (no toolCallEnd
|
|
@@ -41,7 +41,8 @@ export interface PromptBuilderConfig {
|
|
|
41
41
|
contributions?: PromptContributionRegistry
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function buildEnvContext(workingDirectory: string,
|
|
44
|
+
function buildEnvContext(workingDirectory: string, config: PromptBuilderConfig): string {
|
|
45
|
+
const { runtimeContext, tools, allowedTools } = config
|
|
45
46
|
const lines = [
|
|
46
47
|
`<env>
|
|
47
48
|
Working directory: ${workingDirectory}
|
|
@@ -69,7 +70,23 @@ Platform: ${process.platform}`,
|
|
|
69
70
|
|
|
70
71
|
lines.push(`</env>
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
Resolve relative paths against the working directory above; use supplied absolute paths as given.`)
|
|
74
|
+
|
|
75
|
+
if (hasFilesystemTools(tools, allowedTools)) {
|
|
76
|
+
lines.push(
|
|
77
|
+
'When file contents are needed, read known file paths directly. Discover unknown paths with available filesystem tools using targeted directories and patterns.',
|
|
78
|
+
)
|
|
79
|
+
const canGlob =
|
|
80
|
+
tools.has('glob') &&
|
|
81
|
+
(!allowedTools || allowedTools.includes('glob')) &&
|
|
82
|
+
tools.getAvailability('glob') === 'active'
|
|
83
|
+
lines.push(
|
|
84
|
+
canGlob
|
|
85
|
+
? "For an immediate directory overview, use glob with pattern '*' or an available directory listing tool. Use '**' only when recursive discovery is needed."
|
|
86
|
+
: 'For an immediate directory overview, use an available directory listing tool.',
|
|
87
|
+
"Keep discovery within the task's scope; do not recursively inventory unrelated home directories or cache trees.",
|
|
88
|
+
)
|
|
89
|
+
}
|
|
73
90
|
|
|
74
91
|
return lines.join('\n')
|
|
75
92
|
}
|
|
@@ -224,7 +241,7 @@ export class PromptBuilder {
|
|
|
224
241
|
hasFilesystemTools(this.config.tools, this.config.allowedTools) ||
|
|
225
242
|
Boolean(this.config.runtimeContext)
|
|
226
243
|
if (shouldIncludeEnv) {
|
|
227
|
-
parts.push(buildEnvContext(workingDirectory, this.config
|
|
244
|
+
parts.push(buildEnvContext(workingDirectory, this.config))
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
|
|
@@ -290,7 +307,7 @@ export class PromptBuilder {
|
|
|
290
307
|
hasFilesystemTools(this.config.tools, this.config.allowedTools) ||
|
|
291
308
|
Boolean(this.config.runtimeContext)
|
|
292
309
|
if (shouldIncludeEnv) {
|
|
293
|
-
dynamicParts.push(buildEnvContext(workingDirectory, this.config
|
|
310
|
+
dynamicParts.push(buildEnvContext(workingDirectory, this.config))
|
|
294
311
|
}
|
|
295
312
|
}
|
|
296
313
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Stable guest code: host bundlers must never rewrite a function we serialize. */
|
|
2
|
+
export const FILE_WALK_PROGRAM = String.raw`
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const plan = JSON.parse(process.argv[1]);
|
|
6
|
+
const missing = error => ['ENOENT', 'ENOTDIR'].includes(error.code);
|
|
7
|
+
const write = async value => {
|
|
8
|
+
if (!process.stdout.write(JSON.stringify(value) + '\n')) {
|
|
9
|
+
await new Promise(resolve => process.stdout.once('drain', resolve));
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const visit = count => {
|
|
13
|
+
if (count > plan.maxVisitedEntries) {
|
|
14
|
+
throw Object.assign(new Error('File search stopped after examining ' + plan.maxVisitedEntries + ' entries; narrow its root or pattern.'), {code:'ERR_FILE_WALK_LIMIT'});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const patterns = plan.patterns.map(set => set.map(part =>
|
|
18
|
+
typeof part === 'object' && part !== null ? new RegExp(part.source, part.flags + 's') : part));
|
|
19
|
+
const canDescend = relative => patterns.some(pattern => {
|
|
20
|
+
const closure = states => {
|
|
21
|
+
for (const index of states) if (pattern[index] === null) states.add(index + 1);
|
|
22
|
+
return states;
|
|
23
|
+
};
|
|
24
|
+
let states = closure(new Set([0]));
|
|
25
|
+
for (const name of relative.split('/')) {
|
|
26
|
+
const next = new Set();
|
|
27
|
+
for (const index of states) {
|
|
28
|
+
const segment = pattern[index];
|
|
29
|
+
if (segment === null) {
|
|
30
|
+
if (plan.includeHidden || !name.startsWith('.')) next.add(index);
|
|
31
|
+
} else if (typeof segment === 'string' ? segment === name : segment?.test(name)) next.add(index + 1);
|
|
32
|
+
}
|
|
33
|
+
states = closure(next);
|
|
34
|
+
if (states.size === 0) return false;
|
|
35
|
+
}
|
|
36
|
+
return [...states].some(index => index < pattern.length);
|
|
37
|
+
});
|
|
38
|
+
async function run() {
|
|
39
|
+
const stack = [];
|
|
40
|
+
let visited = 0;
|
|
41
|
+
let emitted = 0;
|
|
42
|
+
const matchers = plan.expressions.map(({source, flags}) => new RegExp(source, flags + 's'));
|
|
43
|
+
const maxDepth = plan.maxDepth === null ? Infinity : plan.maxDepth;
|
|
44
|
+
const open = async (absolute, relative, depth) => {
|
|
45
|
+
try { stack.push({dir: await fs.opendir(absolute), relative, depth}); }
|
|
46
|
+
catch (error) { if (!missing(error)) throw error; }
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
if (plan.patterns.length === 0) return;
|
|
50
|
+
let canonicalRoot;
|
|
51
|
+
try { canonicalRoot = await fs.realpath(plan.root); }
|
|
52
|
+
catch (error) { if (missing(error)) return; throw error; }
|
|
53
|
+
if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
|
|
54
|
+
let start = plan.root;
|
|
55
|
+
for (const part of plan.prefix) {
|
|
56
|
+
start = path.join(start, part);
|
|
57
|
+
let info;
|
|
58
|
+
try { info = await fs.lstat(start); }
|
|
59
|
+
catch (error) { if (missing(error)) return; throw error; }
|
|
60
|
+
visit(++visited);
|
|
61
|
+
if (!info.isDirectory()) return;
|
|
62
|
+
}
|
|
63
|
+
if (plan.prefix.length >= maxDepth) return;
|
|
64
|
+
await open(start, plan.prefix.join('/'), plan.prefix.length);
|
|
65
|
+
while (stack.length) {
|
|
66
|
+
const frame = stack[stack.length - 1];
|
|
67
|
+
const entry = await frame.dir.read();
|
|
68
|
+
if (!entry) { stack.pop(); await frame.dir.close(); continue; }
|
|
69
|
+
visit(++visited);
|
|
70
|
+
const relative = frame.relative ? frame.relative + '/' + entry.name : entry.name;
|
|
71
|
+
const absolute = path.join(plan.root, relative);
|
|
72
|
+
const depth = frame.depth + 1;
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (depth < maxDepth && canDescend(relative)) await open(absolute, relative, depth);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!entry.isFile() || !matchers.some(matcher => matcher.test(relative))) continue;
|
|
78
|
+
let info;
|
|
79
|
+
try { info = await fs.lstat(absolute); }
|
|
80
|
+
catch (error) { if (missing(error)) continue; throw error; }
|
|
81
|
+
if (!info.isFile()) continue;
|
|
82
|
+
await write({type:'entry', path:absolute, size:info.size});
|
|
83
|
+
if (++emitted >= plan.maxEntries) return;
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
for (const {dir} of stack.reverse()) await dir.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
run().then(
|
|
90
|
+
() => write({type:'done'}),
|
|
91
|
+
async error => { await write({type:'error', message:error.message, code:error.code}); process.exitCode = 1; },
|
|
92
|
+
);
|
|
93
|
+
`
|