saeeol 1.2.1 → 1.2.3
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/bin/saeeol.cjs +187 -0
- package/npm/bin/saeeol +0 -0
- package/package.json +12 -12
- package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
- package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
- package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
- package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
- package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
- package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
- package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
- package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
- package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
- package/src/cli/cmd/tui/context/app/args.tsx +15 -0
- package/src/cli/cmd/tui/context/app/directory.ts +15 -0
- package/src/cli/cmd/tui/context/app/editor-zed.ts +281 -0
- package/src/cli/cmd/tui/context/app/editor.ts +425 -0
- package/src/cli/cmd/tui/context/app/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/app/project.tsx +109 -0
- package/src/cli/cmd/tui/context/app/route.tsx +67 -0
- package/src/cli/cmd/tui/context/app/sdk.tsx +142 -0
- package/src/cli/cmd/tui/context/app/sync.tsx +713 -0
- package/src/cli/cmd/tui/context/app/theme.tsx +307 -0
- package/src/cli/cmd/tui/context/app/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/context/args.tsx +1 -15
- package/src/cli/cmd/tui/context/directory.ts +1 -15
- package/src/cli/cmd/tui/context/editor-zed.ts +1 -281
- package/src/cli/cmd/tui/context/editor.ts +1 -425
- package/src/cli/cmd/tui/context/event.ts +1 -45
- package/src/cli/cmd/tui/context/exit.tsx +1 -67
- package/src/cli/cmd/tui/context/helper.tsx +1 -25
- package/src/cli/cmd/tui/context/keybind.tsx +1 -105
- package/src/cli/cmd/tui/context/kv.tsx +1 -76
- package/src/cli/cmd/tui/context/local.tsx +1 -478
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +1 -41
- package/src/cli/cmd/tui/context/project.tsx +1 -109
- package/src/cli/cmd/tui/context/prompt.tsx +1 -18
- package/src/cli/cmd/tui/context/route.tsx +1 -67
- package/src/cli/cmd/tui/context/runtime/event.ts +45 -0
- package/src/cli/cmd/tui/context/runtime/exit.tsx +67 -0
- package/src/cli/cmd/tui/context/runtime/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/runtime/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/runtime/local.tsx +478 -0
- package/src/cli/cmd/tui/context/runtime/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/sdk.tsx +1 -142
- package/src/cli/cmd/tui/context/session/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/sync.tsx +1 -713
- package/src/cli/cmd/tui/context/theme.tsx +1 -307
- package/src/cli/cmd/tui/context/tui-config.tsx +1 -9
- package/src/tool/apply_patch.ts +1 -334
- package/src/tool/bash.ts +1 -656
- package/src/tool/core/external-directory.ts +55 -0
- package/src/tool/core/invalid.ts +21 -0
- package/src/tool/core/recall.ts +164 -0
- package/src/tool/core/recall.txt +12 -0
- package/src/tool/core/schema.ts +16 -0
- package/src/tool/core/tool.ts +162 -0
- package/src/tool/core/truncate.ts +160 -0
- package/src/tool/core/truncation-dir.ts +4 -0
- package/src/tool/diagnostics.ts +1 -20
- package/src/tool/edit-replacers.ts +1 -288
- package/src/tool/edit-utils.ts +1 -86
- package/src/tool/edit.ts +1 -262
- package/src/tool/external-directory.ts +1 -55
- package/src/tool/file/apply_patch.ts +334 -0
- package/src/tool/file/apply_patch.txt +33 -0
- package/src/tool/file/bash.ts +656 -0
- package/src/tool/file/bash.txt +119 -0
- package/src/tool/file/edit-replacers.ts +288 -0
- package/src/tool/file/edit-utils.ts +86 -0
- package/src/tool/file/edit.ts +262 -0
- package/src/tool/file/edit.txt +10 -0
- package/src/tool/file/read.ts +389 -0
- package/src/tool/file/read.txt +14 -0
- package/src/tool/file/write.ts +114 -0
- package/src/tool/file/write.txt +8 -0
- package/src/tool/glob.ts +1 -115
- package/src/tool/grep.ts +1 -151
- package/src/tool/integration/diagnostics.ts +20 -0
- package/src/tool/integration/lsp.ts +113 -0
- package/src/tool/integration/lsp.txt +24 -0
- package/src/tool/integration/mcp-exa.ts +73 -0
- package/src/tool/integration/package.ts +168 -0
- package/src/tool/integration/registry.ts +375 -0
- package/src/tool/invalid.ts +1 -21
- package/src/tool/lsp.ts +1 -113
- package/src/tool/mcp-exa.ts +1 -73
- package/src/tool/package.ts +1 -168
- package/src/tool/plan.ts +1 -30
- package/src/tool/question.ts +1 -52
- package/src/tool/read.ts +1 -389
- package/src/tool/recall.ts +1 -164
- package/src/tool/registry.ts +1 -375
- package/src/tool/schema.ts +1 -16
- package/src/tool/search/glob.ts +115 -0
- package/src/tool/search/glob.txt +6 -0
- package/src/tool/search/grep.ts +151 -0
- package/src/tool/search/grep.txt +8 -0
- package/src/tool/search/warpgrep.ts +107 -0
- package/src/tool/search/warpgrep.txt +10 -0
- package/src/tool/search/webfetch.ts +202 -0
- package/src/tool/search/webfetch.txt +13 -0
- package/src/tool/search/websearch.ts +71 -0
- package/src/tool/search/websearch.txt +14 -0
- package/src/tool/skill.ts +1 -91
- package/src/tool/task.ts +1 -197
- package/src/tool/todo.ts +1 -62
- package/src/tool/tool.ts +1 -162
- package/src/tool/truncate.ts +1 -160
- package/src/tool/truncation-dir.ts +1 -4
- package/src/tool/warpgrep.ts +1 -107
- package/src/tool/webfetch.ts +1 -202
- package/src/tool/websearch.ts +1 -71
- package/src/tool/workflow/plan-enter.txt +14 -0
- package/src/tool/workflow/plan-exit.txt +13 -0
- package/src/tool/workflow/plan.ts +30 -0
- package/src/tool/workflow/question.ts +52 -0
- package/src/tool/workflow/question.txt +11 -0
- package/src/tool/workflow/skill.ts +91 -0
- package/src/tool/workflow/skill.txt +5 -0
- package/src/tool/workflow/task.ts +197 -0
- package/src/tool/workflow/task.txt +57 -0
- package/src/tool/workflow/todo.ts +62 -0
- package/src/tool/workflow/todowrite.txt +167 -0
- package/src/tool/write.ts +1 -114
|
@@ -1,288 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
|
|
4
|
-
|
|
5
|
-
export const SimpleReplacer: Replacer = function* (_content, find) {
|
|
6
|
-
yield find
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
|
10
|
-
const originalLines = content.split("\n")
|
|
11
|
-
const searchLines = find.split("\n")
|
|
12
|
-
if (searchLines[searchLines.length - 1] === "") {
|
|
13
|
-
searchLines.pop()
|
|
14
|
-
}
|
|
15
|
-
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
16
|
-
let matches = true
|
|
17
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
18
|
-
const originalTrimmed = originalLines[i + j].trim()
|
|
19
|
-
const searchTrimmed = searchLines[j].trim()
|
|
20
|
-
if (originalTrimmed !== searchTrimmed) {
|
|
21
|
-
matches = false
|
|
22
|
-
break
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (matches) {
|
|
26
|
-
let matchStartIndex = 0
|
|
27
|
-
for (let k = 0; k < i; k++) {
|
|
28
|
-
matchStartIndex += originalLines[k].length + 1
|
|
29
|
-
}
|
|
30
|
-
let matchEndIndex = matchStartIndex
|
|
31
|
-
for (let k = 0; k < searchLines.length; k++) {
|
|
32
|
-
matchEndIndex += originalLines[i + k].length
|
|
33
|
-
if (k < searchLines.length - 1) {
|
|
34
|
-
matchEndIndex += 1
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
|
43
|
-
const originalLines = content.split("\n")
|
|
44
|
-
const searchLines = find.split("\n")
|
|
45
|
-
if (searchLines.length < 3) return
|
|
46
|
-
if (searchLines[searchLines.length - 1] === "") {
|
|
47
|
-
searchLines.pop()
|
|
48
|
-
}
|
|
49
|
-
const firstLineSearch = searchLines[0].trim()
|
|
50
|
-
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
|
51
|
-
const searchBlockSize = searchLines.length
|
|
52
|
-
const candidates: Array<{ startLine: number; endLine: number }> = []
|
|
53
|
-
for (let i = 0; i < originalLines.length; i++) {
|
|
54
|
-
if (originalLines[i].trim() !== firstLineSearch) continue
|
|
55
|
-
for (let j = i + 2; j < originalLines.length; j++) {
|
|
56
|
-
if (originalLines[j].trim() === lastLineSearch) {
|
|
57
|
-
candidates.push({ startLine: i, endLine: j })
|
|
58
|
-
break
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (candidates.length === 0) return
|
|
63
|
-
if (candidates.length === 1) {
|
|
64
|
-
const { startLine, endLine } = candidates[0]
|
|
65
|
-
const actualBlockSize = endLine - startLine + 1
|
|
66
|
-
let similarity = 0
|
|
67
|
-
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2)
|
|
68
|
-
if (linesToCheck > 0) {
|
|
69
|
-
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
70
|
-
const originalLine = originalLines[startLine + j].trim()
|
|
71
|
-
const searchLine = searchLines[j].trim()
|
|
72
|
-
const maxLen = Math.max(originalLine.length, searchLine.length)
|
|
73
|
-
if (maxLen === 0) continue
|
|
74
|
-
const distance = levenshtein(originalLine, searchLine)
|
|
75
|
-
similarity += (1 - distance / maxLen) / linesToCheck
|
|
76
|
-
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) break
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
similarity = 1.0
|
|
80
|
-
}
|
|
81
|
-
if (similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD) {
|
|
82
|
-
let matchStartIndex = 0
|
|
83
|
-
for (let k = 0; k < startLine; k++) {
|
|
84
|
-
matchStartIndex += originalLines[k].length + 1
|
|
85
|
-
}
|
|
86
|
-
let matchEndIndex = matchStartIndex
|
|
87
|
-
for (let k = startLine; k <= endLine; k++) {
|
|
88
|
-
matchEndIndex += originalLines[k].length
|
|
89
|
-
if (k < endLine) matchEndIndex += 1
|
|
90
|
-
}
|
|
91
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
92
|
-
}
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
let bestMatch: { startLine: number; endLine: number } | null = null
|
|
96
|
-
let maxSimilarity = -1
|
|
97
|
-
for (const candidate of candidates) {
|
|
98
|
-
const { startLine, endLine } = candidate
|
|
99
|
-
const actualBlockSize = endLine - startLine + 1
|
|
100
|
-
let similarity = 0
|
|
101
|
-
const linesToCheck = Math.min(searchBlockSize - 2, actualBlockSize - 2)
|
|
102
|
-
if (linesToCheck > 0) {
|
|
103
|
-
for (let j = 1; j < searchBlockSize - 1 && j < actualBlockSize - 1; j++) {
|
|
104
|
-
const originalLine = originalLines[startLine + j].trim()
|
|
105
|
-
const searchLine = searchLines[j].trim()
|
|
106
|
-
const maxLen = Math.max(originalLine.length, searchLine.length)
|
|
107
|
-
if (maxLen === 0) continue
|
|
108
|
-
const distance = levenshtein(originalLine, searchLine)
|
|
109
|
-
similarity += 1 - distance / maxLen
|
|
110
|
-
}
|
|
111
|
-
similarity /= linesToCheck
|
|
112
|
-
} else {
|
|
113
|
-
similarity = 1.0
|
|
114
|
-
}
|
|
115
|
-
if (similarity > maxSimilarity) {
|
|
116
|
-
maxSimilarity = similarity
|
|
117
|
-
bestMatch = candidate
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (maxSimilarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD && bestMatch) {
|
|
121
|
-
const { startLine, endLine } = bestMatch
|
|
122
|
-
let matchStartIndex = 0
|
|
123
|
-
for (let k = 0; k < startLine; k++) {
|
|
124
|
-
matchStartIndex += originalLines[k].length + 1
|
|
125
|
-
}
|
|
126
|
-
let matchEndIndex = matchStartIndex
|
|
127
|
-
for (let k = startLine; k <= endLine; k++) {
|
|
128
|
-
matchEndIndex += originalLines[k].length
|
|
129
|
-
if (k < endLine) matchEndIndex += 1
|
|
130
|
-
}
|
|
131
|
-
yield content.substring(matchStartIndex, matchEndIndex)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const WhitespaceNormalizedReplacer: Replacer = function* (content, find) {
|
|
136
|
-
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
|
137
|
-
const normalizedFind = normalizeWhitespace(find)
|
|
138
|
-
const lines = content.split("\n")
|
|
139
|
-
for (let i = 0; i < lines.length; i++) {
|
|
140
|
-
const line = lines[i]
|
|
141
|
-
if (normalizeWhitespace(line) === normalizedFind) {
|
|
142
|
-
yield line
|
|
143
|
-
} else {
|
|
144
|
-
const normalizedLine = normalizeWhitespace(line)
|
|
145
|
-
if (normalizedLine.includes(normalizedFind)) {
|
|
146
|
-
const words = find.trim().split(/\s+/)
|
|
147
|
-
if (words.length > 0) {
|
|
148
|
-
const pattern = words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+")
|
|
149
|
-
try {
|
|
150
|
-
const regex = new RegExp(pattern)
|
|
151
|
-
const match = line.match(regex)
|
|
152
|
-
if (match) yield match[0]
|
|
153
|
-
} catch {
|
|
154
|
-
// Invalid regex pattern, skip
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const findLines = find.split("\n")
|
|
161
|
-
if (findLines.length > 1) {
|
|
162
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
163
|
-
const block = lines.slice(i, i + findLines.length)
|
|
164
|
-
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
|
|
165
|
-
yield block.join("\n")
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
|
172
|
-
const removeIndentation = (text: string) => {
|
|
173
|
-
const lines = text.split("\n")
|
|
174
|
-
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
|
175
|
-
if (nonEmptyLines.length === 0) return text
|
|
176
|
-
const minIndent = Math.min(
|
|
177
|
-
...nonEmptyLines.map((line) => {
|
|
178
|
-
const match = line.match(/^(\s*)/)
|
|
179
|
-
return match ? match[1].length : 0
|
|
180
|
-
}),
|
|
181
|
-
)
|
|
182
|
-
return lines.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent))).join("\n")
|
|
183
|
-
}
|
|
184
|
-
const normalizedFind = removeIndentation(find)
|
|
185
|
-
const contentLines = content.split("\n")
|
|
186
|
-
const findLines = find.split("\n")
|
|
187
|
-
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
188
|
-
const block = contentLines.slice(i, i + findLines.length).join("\n")
|
|
189
|
-
if (removeIndentation(block) === normalizedFind) {
|
|
190
|
-
yield block
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
|
196
|
-
const unescapeString = (str: string): string => {
|
|
197
|
-
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (_match, capturedChar: string) => {
|
|
198
|
-
switch (capturedChar) {
|
|
199
|
-
case "n": return "\n"
|
|
200
|
-
case "t": return "\t"
|
|
201
|
-
case "r": return "\r"
|
|
202
|
-
case "'": return "'"
|
|
203
|
-
case '"': return '"'
|
|
204
|
-
case "`": return "`"
|
|
205
|
-
case "\\": return "\\"
|
|
206
|
-
case "\n": return "\n"
|
|
207
|
-
case "$": return "$"
|
|
208
|
-
default: return capturedChar
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
const unescapedFind = unescapeString(find)
|
|
213
|
-
if (content.includes(unescapedFind)) {
|
|
214
|
-
yield unescapedFind
|
|
215
|
-
}
|
|
216
|
-
const lines = content.split("\n")
|
|
217
|
-
const findLines = unescapedFind.split("\n")
|
|
218
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
219
|
-
const block = lines.slice(i, i + findLines.length).join("\n")
|
|
220
|
-
const unescapedBlock = unescapeString(block)
|
|
221
|
-
if (unescapedBlock === unescapedFind) {
|
|
222
|
-
yield block
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
228
|
-
let startIndex = 0
|
|
229
|
-
while (true) {
|
|
230
|
-
const index = content.indexOf(find, startIndex)
|
|
231
|
-
if (index === -1) break
|
|
232
|
-
yield find
|
|
233
|
-
startIndex = index + find.length
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
|
238
|
-
const trimmedFind = find.trim()
|
|
239
|
-
if (trimmedFind === find) return
|
|
240
|
-
if (content.includes(trimmedFind)) {
|
|
241
|
-
yield trimmedFind
|
|
242
|
-
}
|
|
243
|
-
const lines = content.split("\n")
|
|
244
|
-
const findLines = find.split("\n")
|
|
245
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
246
|
-
const block = lines.slice(i, i + findLines.length).join("\n")
|
|
247
|
-
if (block.trim() === trimmedFind) {
|
|
248
|
-
yield block
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
254
|
-
const findLines = find.split("\n")
|
|
255
|
-
if (findLines.length < 3) return
|
|
256
|
-
if (findLines[findLines.length - 1] === "") {
|
|
257
|
-
findLines.pop()
|
|
258
|
-
}
|
|
259
|
-
const contentLines = content.split("\n")
|
|
260
|
-
const firstLine = findLines[0].trim()
|
|
261
|
-
const lastLine = findLines[findLines.length - 1].trim()
|
|
262
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
263
|
-
if (contentLines[i].trim() !== firstLine) continue
|
|
264
|
-
for (let j = i + 2; j < contentLines.length; j++) {
|
|
265
|
-
if (contentLines[j].trim() === lastLine) {
|
|
266
|
-
const blockLines = contentLines.slice(i, j + 1)
|
|
267
|
-
const block = blockLines.join("\n")
|
|
268
|
-
if (blockLines.length === findLines.length) {
|
|
269
|
-
let matchingLines = 0
|
|
270
|
-
let totalNonEmptyLines = 0
|
|
271
|
-
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
272
|
-
const blockLine = blockLines[k].trim()
|
|
273
|
-
const findLine = findLines[k].trim()
|
|
274
|
-
if (blockLine.length > 0 || findLine.length > 0) {
|
|
275
|
-
totalNonEmptyLines++
|
|
276
|
-
if (blockLine === findLine) matchingLines++
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (totalNonEmptyLines === 0 || matchingLines / totalNonEmptyLines >= 0.5) {
|
|
280
|
-
yield block
|
|
281
|
-
break
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
break
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
1
|
+
export * from "./file/edit-replacers"
|
package/src/tool/edit-utils.ts
CHANGED
|
@@ -1,86 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Snapshot } from "@/snapshot"
|
|
3
|
-
|
|
4
|
-
export const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
|
|
5
|
-
export const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
|
|
6
|
-
|
|
7
|
-
export function levenshtein(a: string, b: string): number {
|
|
8
|
-
if (a === "" || b === "") {
|
|
9
|
-
return Math.max(a.length, b.length)
|
|
10
|
-
}
|
|
11
|
-
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
12
|
-
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
13
|
-
)
|
|
14
|
-
for (let i = 1; i <= a.length; i++) {
|
|
15
|
-
for (let j = 1; j <= b.length; j++) {
|
|
16
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
17
|
-
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return matrix[a.length][b.length]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const MAX_DIFF_CONTENT = 500_000
|
|
24
|
-
export function buildFileDiff(file: string, before: string, after: string): Snapshot.FileDiff {
|
|
25
|
-
const tooLarge = before.length > MAX_DIFF_CONTENT || after.length > MAX_DIFF_CONTENT
|
|
26
|
-
let additions = 0
|
|
27
|
-
let deletions = 0
|
|
28
|
-
if (!tooLarge) {
|
|
29
|
-
for (const change of diffLines(before, after)) {
|
|
30
|
-
if (change.added) additions += change.count || 0
|
|
31
|
-
if (change.removed) deletions += change.count || 0
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
file,
|
|
36
|
-
patch: tooLarge ? "" : createTwoFilesPatch(file, file, before, after),
|
|
37
|
-
additions,
|
|
38
|
-
deletions,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function normalizeLineEndings(text: string): string {
|
|
43
|
-
return text.replaceAll("\r\n", "\n")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function detectLineEnding(text: string): "\n" | "\r\n" {
|
|
47
|
-
return text.includes("\r\n") ? "\r\n" : "\n"
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function convertToLineEnding(text: string, ending: "\n" | "\r\n"): string {
|
|
51
|
-
if (ending === "\n") return text
|
|
52
|
-
return text.replaceAll("\n", "\r\n")
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function trimDiff(diff: string): string {
|
|
56
|
-
const lines = diff.split("\n")
|
|
57
|
-
const contentLines = lines.filter(
|
|
58
|
-
(line) =>
|
|
59
|
-
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
|
60
|
-
!line.startsWith("---") &&
|
|
61
|
-
!line.startsWith("+++"),
|
|
62
|
-
)
|
|
63
|
-
if (contentLines.length === 0) return diff
|
|
64
|
-
let min = Infinity
|
|
65
|
-
for (const line of contentLines) {
|
|
66
|
-
const content = line.slice(1)
|
|
67
|
-
if (content.trim().length > 0) {
|
|
68
|
-
const match = content.match(/^(\s*)/)
|
|
69
|
-
if (match) min = Math.min(min, match[1].length)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (min === Infinity || min === 0) return diff
|
|
73
|
-
const trimmedLines = lines.map((line) => {
|
|
74
|
-
if (
|
|
75
|
-
(line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) &&
|
|
76
|
-
!line.startsWith("---") &&
|
|
77
|
-
!line.startsWith("+++")
|
|
78
|
-
) {
|
|
79
|
-
const prefix = line[0]
|
|
80
|
-
const content = line.slice(1)
|
|
81
|
-
return prefix + content.slice(min)
|
|
82
|
-
}
|
|
83
|
-
return line
|
|
84
|
-
})
|
|
85
|
-
return trimmedLines.join("\n")
|
|
86
|
-
}
|
|
1
|
+
export * from "./file/edit-utils"
|
package/src/tool/edit.ts
CHANGED
|
@@ -1,262 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
|
3
|
-
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
|
4
|
-
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
|
|
5
|
-
|
|
6
|
-
import * as path from "path"
|
|
7
|
-
import { Effect, Schema, Semaphore } from "effect"
|
|
8
|
-
import * as Tool from "./tool"
|
|
9
|
-
import { LSP } from "@/lsp/lsp"
|
|
10
|
-
import { createTwoFilesPatch } from "diff"
|
|
11
|
-
import DESCRIPTION from "./edit.txt"
|
|
12
|
-
import { File } from "../file"
|
|
13
|
-
import { FileWatcher } from "../file/watcher"
|
|
14
|
-
import { Bus } from "../bus"
|
|
15
|
-
import { Format } from "../format"
|
|
16
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
17
|
-
import { Snapshot } from "@/snapshot"
|
|
18
|
-
import { assertExternalDirectoryEffect } from "./external-directory"
|
|
19
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
20
|
-
import * as Bom from "@/util/bom"
|
|
21
|
-
import { filterDiagnostics } from "./diagnostics"
|
|
22
|
-
import { ConfigValidation } from "../overlay/config-validation"
|
|
23
|
-
import * as EncodedIO from "../overlay/tool/encoded-io"
|
|
24
|
-
import { buildFileDiff, normalizeLineEndings, detectLineEnding, convertToLineEnding, trimDiff } from "./edit-utils"
|
|
25
|
-
import {
|
|
26
|
-
SimpleReplacer,
|
|
27
|
-
LineTrimmedReplacer,
|
|
28
|
-
BlockAnchorReplacer,
|
|
29
|
-
WhitespaceNormalizedReplacer,
|
|
30
|
-
IndentationFlexibleReplacer,
|
|
31
|
-
EscapeNormalizedReplacer,
|
|
32
|
-
MultiOccurrenceReplacer,
|
|
33
|
-
TrimmedBoundaryReplacer,
|
|
34
|
-
ContextAwareReplacer,
|
|
35
|
-
} from "./edit-replacers"
|
|
36
|
-
|
|
37
|
-
export { buildFileDiff, trimDiff } from "./edit-utils"
|
|
38
|
-
export type { Replacer } from "./edit-replacers"
|
|
39
|
-
export {
|
|
40
|
-
SimpleReplacer,
|
|
41
|
-
LineTrimmedReplacer,
|
|
42
|
-
BlockAnchorReplacer,
|
|
43
|
-
WhitespaceNormalizedReplacer,
|
|
44
|
-
IndentationFlexibleReplacer,
|
|
45
|
-
EscapeNormalizedReplacer,
|
|
46
|
-
MultiOccurrenceReplacer,
|
|
47
|
-
TrimmedBoundaryReplacer,
|
|
48
|
-
ContextAwareReplacer,
|
|
49
|
-
} from "./edit-replacers"
|
|
50
|
-
|
|
51
|
-
const locks = new Map<string, Semaphore.Semaphore>()
|
|
52
|
-
|
|
53
|
-
function lock(filePath: string) {
|
|
54
|
-
const resolvedFilePath = AppFileSystem.resolve(filePath)
|
|
55
|
-
const hit = locks.get(resolvedFilePath)
|
|
56
|
-
if (hit) return hit
|
|
57
|
-
|
|
58
|
-
const next = Semaphore.makeUnsafe(1)
|
|
59
|
-
locks.set(resolvedFilePath, next)
|
|
60
|
-
return next
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export const Parameters = Schema.Struct({
|
|
64
|
-
filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
|
|
65
|
-
oldString: Schema.String.annotate({ description: "The text to replace" }),
|
|
66
|
-
newString: Schema.String.annotate({
|
|
67
|
-
description: "The text to replace it with (must be different from oldString)",
|
|
68
|
-
}),
|
|
69
|
-
replaceAll: Schema.optional(Schema.Boolean).annotate({
|
|
70
|
-
description: "Replace all occurrences of oldString (default false)",
|
|
71
|
-
}),
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
|
|
75
|
-
if (oldString === newString) {
|
|
76
|
-
throw new Error("No changes to apply: oldString and newString are identical.")
|
|
77
|
-
}
|
|
78
|
-
let notFound = true
|
|
79
|
-
for (const replacer of [
|
|
80
|
-
SimpleReplacer,
|
|
81
|
-
LineTrimmedReplacer,
|
|
82
|
-
BlockAnchorReplacer,
|
|
83
|
-
WhitespaceNormalizedReplacer,
|
|
84
|
-
IndentationFlexibleReplacer,
|
|
85
|
-
EscapeNormalizedReplacer,
|
|
86
|
-
TrimmedBoundaryReplacer,
|
|
87
|
-
ContextAwareReplacer,
|
|
88
|
-
MultiOccurrenceReplacer,
|
|
89
|
-
]) {
|
|
90
|
-
for (const search of replacer(content, oldString)) {
|
|
91
|
-
const index = content.indexOf(search)
|
|
92
|
-
if (index === -1) continue
|
|
93
|
-
notFound = false
|
|
94
|
-
if (replaceAll) {
|
|
95
|
-
return content.replaceAll(search, newString)
|
|
96
|
-
}
|
|
97
|
-
const lastIndex = content.lastIndexOf(search)
|
|
98
|
-
if (index !== lastIndex) continue
|
|
99
|
-
return content.substring(0, index) + newString + content.substring(index + search.length)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (notFound) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
"Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.",
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
throw new Error("Found multiple matches for oldString. Provide more surrounding context to make the match unique.")
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const EditTool = Tool.define(
|
|
111
|
-
"edit",
|
|
112
|
-
Effect.gen(function* () {
|
|
113
|
-
const lsp = yield* LSP.Service
|
|
114
|
-
const afs = yield* AppFileSystem.Service
|
|
115
|
-
const format = yield* Format.Service
|
|
116
|
-
const bus = yield* Bus.Service
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
description: DESCRIPTION,
|
|
120
|
-
parameters: Parameters,
|
|
121
|
-
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
|
122
|
-
Effect.gen(function* () {
|
|
123
|
-
if (!params.filePath) {
|
|
124
|
-
throw new Error("filePath is required")
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (params.oldString === params.newString) {
|
|
128
|
-
throw new Error("No changes to apply: oldString and newString are identical.")
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const instance = yield* InstanceState.context
|
|
132
|
-
const filePath = path.isAbsolute(params.filePath)
|
|
133
|
-
? params.filePath
|
|
134
|
-
: path.join(instance.directory, params.filePath)
|
|
135
|
-
yield* assertExternalDirectoryEffect(ctx, filePath)
|
|
136
|
-
|
|
137
|
-
let diff = ""
|
|
138
|
-
let contentOld = ""
|
|
139
|
-
let contentNew = ""
|
|
140
|
-
let cachedFilediff: Snapshot.FileDiff | undefined
|
|
141
|
-
yield* lock(filePath).withPermits(1)(
|
|
142
|
-
Effect.gen(function* () {
|
|
143
|
-
if (params.oldString === "") {
|
|
144
|
-
const existed = yield* afs.existsSafe(filePath)
|
|
145
|
-
// derive the BOM flag from the detected encoding label instead of the decoded text.
|
|
146
|
-
const pre = existed ? yield* EncodedIO.read(filePath) : { text: "", encoding: "utf-8" }
|
|
147
|
-
const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
|
|
148
|
-
const next = Bom.split(params.newString)
|
|
149
|
-
const desiredBom = source.bom || next.bom
|
|
150
|
-
contentOld = source.text
|
|
151
|
-
contentNew = next.text
|
|
152
|
-
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
|
153
|
-
cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
|
|
154
|
-
yield* ctx.ask({
|
|
155
|
-
permission: "edit",
|
|
156
|
-
patterns: [path.relative(instance.worktree, filePath)],
|
|
157
|
-
always: ["*"],
|
|
158
|
-
metadata: {
|
|
159
|
-
filepath: filePath,
|
|
160
|
-
diff,
|
|
161
|
-
filediff: cachedFilediff,
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
|
|
165
|
-
if (yield* format.file(filePath)) {
|
|
166
|
-
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
|
167
|
-
}
|
|
168
|
-
yield* bus.publish(File.Event.Edited, { file: filePath })
|
|
169
|
-
yield* bus.publish(FileWatcher.Event.Updated, {
|
|
170
|
-
file: filePath,
|
|
171
|
-
event: existed ? "change" : "add",
|
|
172
|
-
})
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
177
|
-
if (!info) throw new Error(`File ${filePath} not found`)
|
|
178
|
-
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
|
|
179
|
-
// derive the BOM flag from the detected encoding label instead of the decoded text.
|
|
180
|
-
const pre = yield* EncodedIO.read(filePath)
|
|
181
|
-
const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
|
|
182
|
-
contentOld = source.text
|
|
183
|
-
|
|
184
|
-
const ending = detectLineEnding(contentOld)
|
|
185
|
-
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
|
|
186
|
-
const replacement = convertToLineEnding(normalizeLineEndings(params.newString), ending)
|
|
187
|
-
|
|
188
|
-
const next = Bom.split(replace(contentOld, old, replacement, params.replaceAll))
|
|
189
|
-
const desiredBom = source.bom || next.bom
|
|
190
|
-
contentNew = next.text
|
|
191
|
-
|
|
192
|
-
diff = trimDiff(
|
|
193
|
-
createTwoFilesPatch(
|
|
194
|
-
filePath,
|
|
195
|
-
filePath,
|
|
196
|
-
normalizeLineEndings(contentOld),
|
|
197
|
-
normalizeLineEndings(contentNew),
|
|
198
|
-
),
|
|
199
|
-
)
|
|
200
|
-
cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
|
|
201
|
-
yield* ctx.ask({
|
|
202
|
-
permission: "edit",
|
|
203
|
-
patterns: [path.relative(instance.worktree, filePath)],
|
|
204
|
-
always: ["*"],
|
|
205
|
-
metadata: {
|
|
206
|
-
filepath: filePath,
|
|
207
|
-
diff,
|
|
208
|
-
filediff: cachedFilediff,
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
|
|
213
|
-
if (yield* format.file(filePath)) {
|
|
214
|
-
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
|
215
|
-
}
|
|
216
|
-
yield* bus.publish(File.Event.Edited, { file: filePath })
|
|
217
|
-
yield* bus.publish(FileWatcher.Event.Updated, {
|
|
218
|
-
file: filePath,
|
|
219
|
-
event: "change",
|
|
220
|
-
})
|
|
221
|
-
diff = trimDiff(
|
|
222
|
-
createTwoFilesPatch(
|
|
223
|
-
filePath,
|
|
224
|
-
filePath,
|
|
225
|
-
normalizeLineEndings(contentOld),
|
|
226
|
-
normalizeLineEndings(contentNew),
|
|
227
|
-
),
|
|
228
|
-
)
|
|
229
|
-
}).pipe(Effect.orDie),
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
const filediff: Snapshot.FileDiff = cachedFilediff ?? buildFileDiff(filePath, contentOld, contentNew)
|
|
233
|
-
|
|
234
|
-
yield* ctx.metadata({
|
|
235
|
-
metadata: {
|
|
236
|
-
diff,
|
|
237
|
-
filediff,
|
|
238
|
-
diagnostics: {},
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
let output = "Edit applied successfully."
|
|
243
|
-
yield* lsp.touchFile(filePath, "document")
|
|
244
|
-
const diagnostics = yield* lsp.diagnostics()
|
|
245
|
-
const normalizedFilePath = AppFileSystem.normalizePath(filePath)
|
|
246
|
-
const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
|
|
247
|
-
if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}`
|
|
248
|
-
output += yield* Effect.promise(() => ConfigValidation.check(filePath))
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
metadata: {
|
|
252
|
-
diagnostics: filterDiagnostics(diagnostics, [normalizedFilePath]),
|
|
253
|
-
diff,
|
|
254
|
-
filediff,
|
|
255
|
-
},
|
|
256
|
-
title: `${path.relative(instance.worktree, filePath)}`,
|
|
257
|
-
output,
|
|
258
|
-
}
|
|
259
|
-
}),
|
|
260
|
-
}
|
|
261
|
-
}),
|
|
262
|
-
)
|
|
1
|
+
export * from "./file/edit"
|
|
@@ -1,55 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import * as EffectLogger from "@saeeol/core/effect/logger"
|
|
4
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
5
|
-
import type * as Tool from "./tool"
|
|
6
|
-
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
7
|
-
|
|
8
|
-
type Kind = "file" | "directory"
|
|
9
|
-
|
|
10
|
-
type Options = {
|
|
11
|
-
bypass?: boolean
|
|
12
|
-
kind?: Kind
|
|
13
|
-
}
|
|
14
|
-
function root(dir: string) {
|
|
15
|
-
return path.parse(dir).root === dir
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function inside(dir: string, file: string) {
|
|
19
|
-
return !root(dir) && AppFileSystem.contains(dir, file)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const assertExternalDirectoryEffect = Effect.fn("Tool.assertExternalDirectory")(function* (
|
|
23
|
-
ctx: Tool.Context,
|
|
24
|
-
target?: string,
|
|
25
|
-
options?: Options,
|
|
26
|
-
) {
|
|
27
|
-
if (!target) return
|
|
28
|
-
|
|
29
|
-
if (options?.bypass) return
|
|
30
|
-
|
|
31
|
-
const ins = yield* InstanceState.context
|
|
32
|
-
const full = process.platform === "win32" ? AppFileSystem.normalizePath(target) : target
|
|
33
|
-
if (inside(ins.directory, full) || inside(ins.worktree, full)) return
|
|
34
|
-
|
|
35
|
-
const kind = options?.kind ?? "file"
|
|
36
|
-
const dir = kind === "directory" ? full : path.dirname(full)
|
|
37
|
-
const glob =
|
|
38
|
-
process.platform === "win32"
|
|
39
|
-
? AppFileSystem.normalizePathPattern(path.join(dir, "*"))
|
|
40
|
-
: path.join(dir, "*").replaceAll("\\", "/")
|
|
41
|
-
|
|
42
|
-
yield* ctx.ask({
|
|
43
|
-
permission: "external_directory",
|
|
44
|
-
patterns: [glob],
|
|
45
|
-
always: [glob],
|
|
46
|
-
metadata: {
|
|
47
|
-
filepath: full,
|
|
48
|
-
parentDir: dir,
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
export async function assertExternalDirectory(ctx: Tool.Context, target?: string, options?: Options) {
|
|
54
|
-
return Effect.runPromise(assertExternalDirectoryEffect(ctx, target, options).pipe(Effect.provide(EffectLogger.layer)))
|
|
55
|
-
}
|
|
1
|
+
export * from "./core/external-directory"
|