opencastle 0.26.1 → 0.27.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/README.md +7 -1
- package/bin/cli.mjs +10 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { statSync, realpathSync, lstatSync, writeFileSync, rmSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { normalize, join, resolve } from 'node:path'
|
|
4
|
+
import type { Task } from '../types.js'
|
|
5
|
+
|
|
6
|
+
// ── Path normalization ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a file path for partition comparison.
|
|
10
|
+
* - Rejects glob patterns (* or ?)
|
|
11
|
+
* - Strips leading ./ and /
|
|
12
|
+
* - Replaces backslashes with forward slashes
|
|
13
|
+
* - Resolves . and .. via path.normalize()
|
|
14
|
+
* - Preserves trailing slash for directories
|
|
15
|
+
*/
|
|
16
|
+
export function normalizePath(p: string): string {
|
|
17
|
+
if (p.includes('*') || p.includes('?')) {
|
|
18
|
+
throw new Error(`Glob patterns are not allowed in file paths: "${p}"`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Record whether the path indicates a directory (trailing slash)
|
|
22
|
+
const hasTrailingSlash = p.endsWith('/') || p.endsWith('\\')
|
|
23
|
+
|
|
24
|
+
// Normalize separators to forward slash
|
|
25
|
+
let result = p.replace(/\\/g, '/')
|
|
26
|
+
|
|
27
|
+
// Strip trailing slashes before further processing
|
|
28
|
+
result = result.replace(/\/+$/, '')
|
|
29
|
+
|
|
30
|
+
// Strip leading './' (may be multiple, e.g. '././')
|
|
31
|
+
result = result.replace(/^(\.\/)+/, '')
|
|
32
|
+
|
|
33
|
+
// Strip leading '/'
|
|
34
|
+
result = result.replace(/^\/+/, '')
|
|
35
|
+
|
|
36
|
+
// Reject any .. path segment — even those that would not escape the root.
|
|
37
|
+
// All usage of .. is rejected for safety, not just escaping traversals.
|
|
38
|
+
if (/(^|\/)\.\.(\/|$)/.test(result)) {
|
|
39
|
+
throw new Error(`Path traversal detected: "${p}" resolves to a path containing ".." segments`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve '.' and '..' segments
|
|
43
|
+
result = normalize(result).replace(/\\/g, '/')
|
|
44
|
+
|
|
45
|
+
// normalize can introduce leading './' (e.g. for '.') — strip it again
|
|
46
|
+
result = result.replace(/^(\.\/)+/, '')
|
|
47
|
+
result = result.replace(/^\/+/, '')
|
|
48
|
+
|
|
49
|
+
// Restore trailing slash for directories (but not when result is '.' or empty)
|
|
50
|
+
if (hasTrailingSlash && result !== '.' && result !== '') {
|
|
51
|
+
result += '/'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Overlap detection ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns true if path a and path b overlap (exact match or prefix containment).
|
|
61
|
+
* Example: 'src/auth/' overlaps 'src/auth/service.ts' in both directions.
|
|
62
|
+
*/
|
|
63
|
+
export function pathsOverlap(a: string, b: string): boolean {
|
|
64
|
+
if (a === b) return true
|
|
65
|
+
// Treat each path as a potential directory prefix
|
|
66
|
+
const aDir = a.endsWith('/') ? a : a + '/'
|
|
67
|
+
const bDir = b.endsWith('/') ? b : b + '/'
|
|
68
|
+
return b.startsWith(aDir) || a.startsWith(bDir)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Partition validation ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface PartitionConflict {
|
|
74
|
+
phase: number
|
|
75
|
+
taskA: string
|
|
76
|
+
taskB: string
|
|
77
|
+
overlapping: string[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface PartitionValidationResult {
|
|
81
|
+
valid: boolean
|
|
82
|
+
conflicts: PartitionConflict[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate that tasks within the same parallel phase do not have overlapping file partitions.
|
|
87
|
+
* Tasks in different phases (sequential) are allowed to share files.
|
|
88
|
+
*/
|
|
89
|
+
export function validateFilePartitions(
|
|
90
|
+
_tasks: Task[],
|
|
91
|
+
phases: Task[][],
|
|
92
|
+
): PartitionValidationResult {
|
|
93
|
+
const isCaseSensitive = determineFsCaseSensitivity()
|
|
94
|
+
const conflicts: PartitionConflict[] = []
|
|
95
|
+
|
|
96
|
+
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
97
|
+
const phaseTasks = phases[phaseIdx]
|
|
98
|
+
for (let i = 0; i < phaseTasks.length; i++) {
|
|
99
|
+
for (let j = i + 1; j < phaseTasks.length; j++) {
|
|
100
|
+
const taskA = phaseTasks[i]
|
|
101
|
+
const taskB = phaseTasks[j]
|
|
102
|
+
|
|
103
|
+
// Empty files arrays are not partitioned — skip
|
|
104
|
+
if (!taskA.files.length || !taskB.files.length) continue
|
|
105
|
+
|
|
106
|
+
const normalizedA = taskA.files.map(normalizePath)
|
|
107
|
+
const normalizedB = taskB.files.map(normalizePath)
|
|
108
|
+
const overlapping: string[] = []
|
|
109
|
+
|
|
110
|
+
for (const fileA of normalizedA) {
|
|
111
|
+
for (const fileB of normalizedB) {
|
|
112
|
+
const directOverlap = pathsOverlap(fileA, fileB)
|
|
113
|
+
// On case-insensitive filesystems, also check lowercased paths
|
|
114
|
+
const ciOverlap =
|
|
115
|
+
!isCaseSensitive && pathsOverlap(fileA.toLowerCase(), fileB.toLowerCase())
|
|
116
|
+
if ((directOverlap || ciOverlap) && !overlapping.includes(fileA)) {
|
|
117
|
+
overlapping.push(fileA)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (overlapping.length > 0) {
|
|
123
|
+
conflicts.push({ phase: phaseIdx, taskA: taskA.id, taskB: taskB.id, overlapping })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { valid: conflicts.length === 0, conflicts }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Filesystem case-sensitivity probe ────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Probe whether the filesystem is case-sensitive by creating a mixed-case temp file
|
|
136
|
+
* and checking if the lowercase path resolves to the same inode.
|
|
137
|
+
*
|
|
138
|
+
* Uses realpathSync per LES-003: on macOS, os.tmpdir() returns /var/... which is a
|
|
139
|
+
* symlink to /private/var/... — realpathSync resolves this to the canonical path.
|
|
140
|
+
*
|
|
141
|
+
* Returns true if case-sensitive (git-compatible default), false if case-insensitive.
|
|
142
|
+
*/
|
|
143
|
+
export function determineFsCaseSensitivity(): boolean {
|
|
144
|
+
const base = realpathSync(tmpdir())
|
|
145
|
+
const mixedCase = join(base, `OpenCastle_CaseSensitivity_${Date.now()}`)
|
|
146
|
+
const lowerCase = mixedCase.toLowerCase()
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
writeFileSync(mixedCase, '')
|
|
150
|
+
try {
|
|
151
|
+
const statMixed = statSync(mixedCase)
|
|
152
|
+
const statLower = statSync(lowerCase)
|
|
153
|
+
// Same inode → same file → case-insensitive
|
|
154
|
+
return statMixed.ino !== statLower.ino
|
|
155
|
+
} catch {
|
|
156
|
+
// stat(lowerCase) threw → file not found at lowercase path → case-sensitive
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
try { rmSync(mixedCase) } catch { /* ignore cleanup errors */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Symlink security scan ─────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Before task execution: scan each file in the task's files[] partition.
|
|
168
|
+
* If any resolved symlink target escapes the basePath directory, throw symlink_escape.
|
|
169
|
+
*/
|
|
170
|
+
export function scanSymlinks(files: string[], basePath: string): void {
|
|
171
|
+
const realBase = realpathSync(resolve(basePath))
|
|
172
|
+
|
|
173
|
+
for (const file of files) {
|
|
174
|
+
const absPath = join(realBase, normalizePath(file))
|
|
175
|
+
let stat: ReturnType<typeof lstatSync>
|
|
176
|
+
try {
|
|
177
|
+
stat = lstatSync(absPath)
|
|
178
|
+
} catch {
|
|
179
|
+
continue // file doesn't exist yet — skip
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (stat.isSymbolicLink()) {
|
|
183
|
+
let realTarget: string
|
|
184
|
+
try {
|
|
185
|
+
realTarget = realpathSync(absPath)
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error(`symlink_escape: symlink at "${file}" could not be resolved`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`symlink_escape: "${file}" is a symlink that resolves outside the partition`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* After task execution: scan files[] in the worktree for new symlinks that escape the partition.
|
|
201
|
+
* Throws symlink_escape_post_task if any symlink target is outside worktreePath.
|
|
202
|
+
*/
|
|
203
|
+
export function scanNewSymlinks(worktreePath: string, files: string[]): void {
|
|
204
|
+
const realBase = realpathSync(resolve(worktreePath))
|
|
205
|
+
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const absPath = join(realBase, normalizePath(file))
|
|
208
|
+
let stat: ReturnType<typeof lstatSync>
|
|
209
|
+
try {
|
|
210
|
+
stat = lstatSync(absPath)
|
|
211
|
+
} catch {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (stat.isSymbolicLink()) {
|
|
216
|
+
let realTarget: string
|
|
217
|
+
try {
|
|
218
|
+
realTarget = realpathSync(absPath)
|
|
219
|
+
} catch {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`symlink_escape_post_task: "${file}" is a new symlink that cannot be resolved`,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!realTarget.startsWith(realBase + '/') && realTarget !== realBase) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`symlink_escape_post_task: "${file}" is a new symlink that escapes the partition`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -92,6 +92,8 @@ function makeEngineFactory(runResults: ConvoyResult[]) {
|
|
|
92
92
|
return {
|
|
93
93
|
run: vi.fn().mockResolvedValue(result),
|
|
94
94
|
resume: vi.fn().mockResolvedValue(makeConvoyResult()),
|
|
95
|
+
retryFailed: vi.fn(),
|
|
96
|
+
injectTask: vi.fn(),
|
|
95
97
|
}
|
|
96
98
|
})
|
|
97
99
|
}
|
|
@@ -443,6 +445,8 @@ describe('pipeline record persistence', () => {
|
|
|
443
445
|
return makeConvoyResult()
|
|
444
446
|
}),
|
|
445
447
|
resume: vi.fn(),
|
|
448
|
+
retryFailed: vi.fn(),
|
|
449
|
+
injectTask: vi.fn(),
|
|
446
450
|
}))
|
|
447
451
|
|
|
448
452
|
const result = await createPipelineOrchestrator({
|
|
@@ -653,6 +657,8 @@ describe('pipeline resume', () => {
|
|
|
653
657
|
const mockEngine: ConvoyEngine = {
|
|
654
658
|
run: vi.fn().mockResolvedValue(makeConvoyResult()),
|
|
655
659
|
resume: vi.fn().mockResolvedValue(resumedResult),
|
|
660
|
+
retryFailed: vi.fn(),
|
|
661
|
+
injectTask: vi.fn(),
|
|
656
662
|
}
|
|
657
663
|
const factory = vi.fn().mockReturnValue(mockEngine)
|
|
658
664
|
|