@frontman-ai/astro 0.1.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.
Files changed (36) hide show
  1. package/README.md +53 -0
  2. package/dist/cli.js +2744 -0
  3. package/package.json +66 -0
  4. package/src/FrontmanAstro.res +20 -0
  5. package/src/FrontmanAstro.res.mjs +36 -0
  6. package/src/FrontmanAstro__AstroBindings.res +46 -0
  7. package/src/FrontmanAstro__AstroBindings.res.mjs +2 -0
  8. package/src/FrontmanAstro__Config.res +85 -0
  9. package/src/FrontmanAstro__Config.res.mjs +44 -0
  10. package/src/FrontmanAstro__Integration.res +35 -0
  11. package/src/FrontmanAstro__Integration.res.mjs +36 -0
  12. package/src/FrontmanAstro__Middleware.res +149 -0
  13. package/src/FrontmanAstro__Middleware.res.mjs +141 -0
  14. package/src/FrontmanAstro__Server.res +196 -0
  15. package/src/FrontmanAstro__Server.res.mjs +241 -0
  16. package/src/FrontmanAstro__ToolRegistry.res +21 -0
  17. package/src/FrontmanAstro__ToolRegistry.res.mjs +41 -0
  18. package/src/FrontmanAstro__ToolbarApp.res +50 -0
  19. package/src/FrontmanAstro__ToolbarApp.res.mjs +39 -0
  20. package/src/cli/FrontmanAstro__Cli.res +126 -0
  21. package/src/cli/FrontmanAstro__Cli.res.mjs +180 -0
  22. package/src/cli/FrontmanAstro__Cli__AutoEdit.res +300 -0
  23. package/src/cli/FrontmanAstro__Cli__AutoEdit.res.mjs +266 -0
  24. package/src/cli/FrontmanAstro__Cli__Detect.res +298 -0
  25. package/src/cli/FrontmanAstro__Cli__Detect.res.mjs +345 -0
  26. package/src/cli/FrontmanAstro__Cli__Files.res +244 -0
  27. package/src/cli/FrontmanAstro__Cli__Files.res.mjs +321 -0
  28. package/src/cli/FrontmanAstro__Cli__Install.res +224 -0
  29. package/src/cli/FrontmanAstro__Cli__Install.res.mjs +194 -0
  30. package/src/cli/FrontmanAstro__Cli__Style.res +22 -0
  31. package/src/cli/FrontmanAstro__Cli__Style.res.mjs +61 -0
  32. package/src/cli/FrontmanAstro__Cli__Templates.res +226 -0
  33. package/src/cli/FrontmanAstro__Cli__Templates.res.mjs +237 -0
  34. package/src/cli/cli.mjs +3 -0
  35. package/src/tools/FrontmanAstro__Tool__GetPages.res +164 -0
  36. package/src/tools/FrontmanAstro__Tool__GetPages.res.mjs +180 -0
@@ -0,0 +1,126 @@
1
+ // CLI entry point for frontman-astro
2
+ // Usage: npx @frontman-ai/astro install --server <host>
3
+
4
+ module Process = FrontmanBindings.Process
5
+ module Install = FrontmanAstro__Cli__Install
6
+
7
+ // Parse command line arguments (simple implementation without external deps)
8
+ type parsedArgs = {
9
+ command: option<string>,
10
+ server: option<string>,
11
+ prefix: option<string>,
12
+ dryRun: bool,
13
+ skipDeps: bool,
14
+ help: bool,
15
+ }
16
+
17
+ let helpText = `
18
+ Frontman Astro CLI
19
+
20
+ Usage:
21
+ frontman-ai-astro <command> [options]
22
+
23
+ Commands:
24
+ install Install Frontman in an Astro project
25
+
26
+ Options:
27
+ --server <host> Frontman server host (default: api.frontman.sh)
28
+ --prefix <path> Target directory (default: current directory)
29
+ --dry-run Preview changes without writing files
30
+ --skip-deps Skip dependency installation
31
+ --help Show this help message
32
+
33
+ Examples:
34
+ npx @frontman-ai/astro install
35
+ npx @frontman-ai/astro install --server frontman.company.com
36
+ npx @frontman-ai/astro install --dry-run
37
+ `
38
+
39
+ // Simple argument parser
40
+ let parseArgs = (argv: array<string>): parsedArgs => {
41
+ // Skip node and script path (argv[0] and argv[1])
42
+ let args = argv->Array.slice(~start=2, ~end=Array.length(argv))
43
+
44
+ let rec parse = (
45
+ ~remaining: array<string>,
46
+ ~result: parsedArgs,
47
+ ): parsedArgs => {
48
+ switch remaining->Array.get(0) {
49
+ | None => result
50
+ | Some(arg) =>
51
+ let rest = remaining->Array.slice(~start=1, ~end=Array.length(remaining))
52
+
53
+ switch arg {
54
+ | "install" => parse(~remaining=rest, ~result={...result, command: Some("install")})
55
+ | "--server" =>
56
+ let value = rest->Array.get(0)
57
+ let nextRest = rest->Array.slice(~start=1, ~end=Array.length(rest))
58
+ parse(~remaining=nextRest, ~result={...result, server: value})
59
+ | "--prefix" =>
60
+ let value = rest->Array.get(0)
61
+ let nextRest = rest->Array.slice(~start=1, ~end=Array.length(rest))
62
+ parse(~remaining=nextRest, ~result={...result, prefix: value})
63
+ | "--dry-run" => parse(~remaining=rest, ~result={...result, dryRun: true})
64
+ | "--skip-deps" => parse(~remaining=rest, ~result={...result, skipDeps: true})
65
+ | "--help" | "-h" => parse(~remaining=rest, ~result={...result, help: true})
66
+ | _ => parse(~remaining=rest, ~result)
67
+ }
68
+ }
69
+ }
70
+
71
+ parse(
72
+ ~remaining=args,
73
+ ~result={
74
+ command: None,
75
+ server: None,
76
+ prefix: None,
77
+ dryRun: false,
78
+ skipDeps: false,
79
+ help: false,
80
+ },
81
+ )
82
+ }
83
+
84
+ // Main entry point
85
+ let main = async () => {
86
+ let args = parseArgs(Process.argv)
87
+
88
+ switch args.help {
89
+ | true =>
90
+ Console.log(helpText)
91
+ Process.exit(0)
92
+ | false => ()
93
+ }
94
+
95
+ switch args.command {
96
+ | Some("install") =>
97
+ let server = switch args.server {
98
+ | Some(s) => s
99
+ | None => "api.frontman.sh"
100
+ }
101
+ let result = await Install.run({
102
+ server,
103
+ prefix: args.prefix,
104
+ dryRun: args.dryRun,
105
+ skipDeps: args.skipDeps,
106
+ })
107
+
108
+ switch result {
109
+ | Install.Success => Process.exit(0)
110
+ | Install.PartialSuccess(_) => Process.exit(0) // Still success, just with manual steps
111
+ | Install.Failure(_) => Process.exit(1)
112
+ }
113
+
114
+ | Some(cmd) =>
115
+ Console.error(`Unknown command: ${cmd}`)
116
+ Console.log(helpText)
117
+ Process.exit(1)
118
+
119
+ | None =>
120
+ Console.log(helpText)
121
+ Process.exit(0)
122
+ }
123
+ }
124
+
125
+ // Run main
126
+ main()->ignore
@@ -0,0 +1,180 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Process from "process";
4
+ import * as FrontmanAstro__Cli__Install$FrontmanAiAstro from "./FrontmanAstro__Cli__Install.res.mjs";
5
+
6
+ let helpText = `
7
+ Frontman Astro CLI
8
+
9
+ Usage:
10
+ frontman-ai-astro <command> [options]
11
+
12
+ Commands:
13
+ install Install Frontman in an Astro project
14
+
15
+ Options:
16
+ --server <host> Frontman server host (default: api.frontman.sh)
17
+ --prefix <path> Target directory (default: current directory)
18
+ --dry-run Preview changes without writing files
19
+ --skip-deps Skip dependency installation
20
+ --help Show this help message
21
+
22
+ Examples:
23
+ npx @frontman-ai/astro install
24
+ npx @frontman-ai/astro install --server frontman.company.com
25
+ npx @frontman-ai/astro install --dry-run
26
+ `;
27
+
28
+ function parseArgs(argv) {
29
+ let args = argv.slice(2, argv.length);
30
+ let _remaining = args;
31
+ let _result = {
32
+ command: undefined,
33
+ server: undefined,
34
+ prefix: undefined,
35
+ dryRun: false,
36
+ skipDeps: false,
37
+ help: false
38
+ };
39
+ while (true) {
40
+ let result = _result;
41
+ let remaining = _remaining;
42
+ let arg = remaining[0];
43
+ if (arg === undefined) {
44
+ return result;
45
+ }
46
+ let rest = remaining.slice(1, remaining.length);
47
+ switch (arg) {
48
+ case "--dry-run" :
49
+ _result = {
50
+ command: result.command,
51
+ server: result.server,
52
+ prefix: result.prefix,
53
+ dryRun: true,
54
+ skipDeps: result.skipDeps,
55
+ help: result.help
56
+ };
57
+ _remaining = rest;
58
+ continue;
59
+ case "--prefix" :
60
+ let value = rest[0];
61
+ let nextRest = rest.slice(1, rest.length);
62
+ _result = {
63
+ command: result.command,
64
+ server: result.server,
65
+ prefix: value,
66
+ dryRun: result.dryRun,
67
+ skipDeps: result.skipDeps,
68
+ help: result.help
69
+ };
70
+ _remaining = nextRest;
71
+ continue;
72
+ case "--server" :
73
+ let value$1 = rest[0];
74
+ let nextRest$1 = rest.slice(1, rest.length);
75
+ _result = {
76
+ command: result.command,
77
+ server: value$1,
78
+ prefix: result.prefix,
79
+ dryRun: result.dryRun,
80
+ skipDeps: result.skipDeps,
81
+ help: result.help
82
+ };
83
+ _remaining = nextRest$1;
84
+ continue;
85
+ case "--skip-deps" :
86
+ _result = {
87
+ command: result.command,
88
+ server: result.server,
89
+ prefix: result.prefix,
90
+ dryRun: result.dryRun,
91
+ skipDeps: true,
92
+ help: result.help
93
+ };
94
+ _remaining = rest;
95
+ continue;
96
+ case "--help" :
97
+ case "-h" :
98
+ break;
99
+ case "install" :
100
+ _result = {
101
+ command: "install",
102
+ server: result.server,
103
+ prefix: result.prefix,
104
+ dryRun: result.dryRun,
105
+ skipDeps: result.skipDeps,
106
+ help: result.help
107
+ };
108
+ _remaining = rest;
109
+ continue;
110
+ default:
111
+ _remaining = rest;
112
+ continue;
113
+ }
114
+ _result = {
115
+ command: result.command,
116
+ server: result.server,
117
+ prefix: result.prefix,
118
+ dryRun: result.dryRun,
119
+ skipDeps: result.skipDeps,
120
+ help: true
121
+ };
122
+ _remaining = rest;
123
+ continue;
124
+ };
125
+ }
126
+
127
+ async function main() {
128
+ let args = parseArgs(process.argv);
129
+ if (args.help) {
130
+ console.log(helpText);
131
+ Process.exit(0);
132
+ }
133
+ let cmd = args.command;
134
+ if (cmd !== undefined) {
135
+ if (cmd === "install") {
136
+ let s = args.server;
137
+ let server = s !== undefined ? s : "api.frontman.sh";
138
+ let result = await FrontmanAstro__Cli__Install$FrontmanAiAstro.run({
139
+ server: server,
140
+ prefix: args.prefix,
141
+ dryRun: args.dryRun,
142
+ skipDeps: args.skipDeps
143
+ });
144
+ if (typeof result !== "object") {
145
+ Process.exit(0);
146
+ return;
147
+ }
148
+ if (result.TAG === "PartialSuccess") {
149
+ Process.exit(0);
150
+ return;
151
+ }
152
+ Process.exit(1);
153
+ return;
154
+ } else {
155
+ console.error(`Unknown command: ` + cmd);
156
+ console.log(helpText);
157
+ Process.exit(1);
158
+ return;
159
+ }
160
+ } else {
161
+ console.log(helpText);
162
+ Process.exit(0);
163
+ return;
164
+ }
165
+ }
166
+
167
+ main();
168
+
169
+ let Process$1;
170
+
171
+ let Install;
172
+
173
+ export {
174
+ Process$1 as Process,
175
+ Install,
176
+ helpText,
177
+ parseArgs,
178
+ main,
179
+ }
180
+ /* Not a pure module */
@@ -0,0 +1,300 @@
1
+ // AI-powered auto-edit for existing files during installation
2
+ // Uses OpenCode Zen API (free, no API key required) to merge Frontman into existing files
3
+
4
+ module Bindings = FrontmanBindings
5
+ module Fs = Bindings.Fs
6
+ module Readline = Bindings.Readline
7
+
8
+ module Templates = FrontmanAstro__Cli__Templates
9
+ module Style = FrontmanAstro__Cli__Style
10
+
11
+ type fileType =
12
+ | Config
13
+ | Middleware
14
+
15
+ // OpenCode Zen API configuration
16
+ let apiBaseUrl = "https://opencode.ai/zen/v1/chat/completions"
17
+ let apiKey = "public"
18
+
19
+ // Model fallback chain (all free on OpenCode Zen, no API key needed)
20
+ // Verified against https://opencode.ai/zen/v1/models
21
+ let models = ["gpt-5-nano", "big-pickle", "glm-4.7-free"]
22
+
23
+ // Build the system prompt for the LLM based on file type
24
+ let buildSystemPrompt = (~fileType: fileType, ~host: string): string => {
25
+ let (typeName, manualInstructions, referenceTemplate, rules) = switch fileType {
26
+ | Config => (
27
+ "astro.config.mjs",
28
+ Templates.ErrorMessages.configManualSetup("astro.config.mjs", host),
29
+ Templates.configTemplate(host),
30
+ `- Add the import for '@astrojs/node' at the top of the file
31
+ - Add the import for '@frontman-ai/astro/integration' at the top of the file
32
+ - Add frontmanIntegration() to the integrations array
33
+ - Add SSR dev mode config: ...(isProd ? {} : { output: 'server', adapter: node({ mode: 'standalone' }) })
34
+ - Add const isProd = process.env.NODE_ENV === 'production'; before defineConfig
35
+ - Preserve ALL existing integrations and configuration unchanged
36
+ - Do not remove or modify any existing imports or settings`,
37
+ )
38
+ | Middleware => (
39
+ "src/middleware.ts",
40
+ Templates.ErrorMessages.middlewareManualSetup("src/middleware.ts", host),
41
+ Templates.middlewareTemplate(host),
42
+ `- Add the import for '@frontman-ai/astro' (createMiddleware, makeConfig) at the top of the file
43
+ - Add the import for 'astro:middleware' (defineMiddleware, sequence) at the top of the file
44
+ - Create a Frontman middleware instance with makeConfig({ host: '${host}' })
45
+ - Create a defineMiddleware wrapper for the Frontman handler
46
+ - Use sequence() to combine the Frontman middleware with the existing onRequest handler
47
+ - The Frontman middleware should come FIRST in the sequence
48
+ - Preserve ALL existing middleware functionality unchanged
49
+ - Do not remove or modify any existing imports or middleware logic`,
50
+ )
51
+ }
52
+
53
+ `You are a code editor. Modify an Astro ${typeName} file to integrate Frontman.
54
+
55
+ ## What to add
56
+ ${manualInstructions}
57
+
58
+ ## Reference template (for a fresh file without any existing code):
59
+
60
+ ${referenceTemplate}
61
+
62
+ ## Rules
63
+ ${rules}
64
+ - Return ONLY the complete file contents. No markdown fences, no explanations, no comments about changes.`
65
+ }
66
+
67
+ // Build the user message with the existing file content
68
+ let buildUserMessage = (~existingContent: string): string => {
69
+ `Here is the existing file to modify:
70
+
71
+ ${existingContent}`
72
+ }
73
+
74
+ // Per-model timeout in milliseconds (30 seconds)
75
+ let requestTimeoutMs = 30_000
76
+
77
+ // Raw JS fetch implementation for Node.js (avoids webapi module dependency)
78
+ let fetchChatCompletion: (
79
+ ~url: string,
80
+ ~apiKey: string,
81
+ ~model: string,
82
+ ~systemPrompt: string,
83
+ ~userMessage: string,
84
+ ~timeoutMs: int,
85
+ ) => promise<result<string, string>> = %raw(`
86
+ async function(url, apiKey, model, systemPrompt, userMessage, timeoutMs) {
87
+ try {
88
+ const response = await fetch(url, {
89
+ method: "POST",
90
+ signal: AbortSignal.timeout(timeoutMs),
91
+ headers: {
92
+ "Content-Type": "application/json",
93
+ "Authorization": "Bearer " + apiKey,
94
+ },
95
+ body: JSON.stringify({
96
+ model: model,
97
+ temperature: 0,
98
+ messages: [
99
+ { role: "system", content: systemPrompt },
100
+ { role: "user", content: userMessage },
101
+ ],
102
+ }),
103
+ });
104
+
105
+ if (!response.ok) {
106
+ return { TAG: "Error", _0: "HTTP " + response.status + ": " + response.statusText };
107
+ }
108
+
109
+ const json = await response.json();
110
+ const content = json?.choices?.[0]?.message?.content?.trim();
111
+ if (!content) {
112
+ return { TAG: "Error", _0: "Empty response from model" };
113
+ }
114
+ return { TAG: "Ok", _0: content };
115
+ } catch (err) {
116
+ if (err?.name === "TimeoutError") {
117
+ return { TAG: "Error", _0: "Request timed out after " + (timeoutMs / 1000) + "s" };
118
+ }
119
+ return { TAG: "Error", _0: "Request failed: " + (err?.message || "Unknown error") };
120
+ }
121
+ }
122
+ `)
123
+
124
+ // Call a single model
125
+ let callModel = async (
126
+ ~model: string,
127
+ ~systemPrompt: string,
128
+ ~userMessage: string,
129
+ ): result<string, string> => {
130
+ await fetchChatCompletion(
131
+ ~url=apiBaseUrl,
132
+ ~apiKey,
133
+ ~model,
134
+ ~systemPrompt,
135
+ ~userMessage,
136
+ ~timeoutMs=requestTimeoutMs,
137
+ )
138
+ }
139
+
140
+ // Strip markdown fences if the LLM wraps the response in them
141
+ let stripMarkdownFences = (content: string): string => {
142
+ let lines = content->String.split("\n")
143
+ let len = lines->Array.length
144
+
145
+ // Check if first line is a markdown fence
146
+ let firstLine = lines->Array.get(0)->Option.getOr("")
147
+ let startsWithFence = firstLine->String.startsWith("```")
148
+
149
+ switch startsWithFence {
150
+ | false => content
151
+ | true =>
152
+ // Find last line that is a closing fence
153
+ let lastLine = lines->Array.get(len - 1)->Option.getOr("")
154
+ let endsWithFence = lastLine->String.trim == "```"
155
+
156
+ let endIdx = switch endsWithFence {
157
+ | true => len - 1
158
+ | false => len
159
+ }
160
+
161
+ lines
162
+ ->Array.slice(~start=1, ~end=endIdx)
163
+ ->Array.join("\n")
164
+ }
165
+ }
166
+
167
+ // Validate that the LLM output contains required Frontman imports/config
168
+ let validateOutput = (~content: string, ~fileType: fileType): bool => {
169
+ switch fileType {
170
+ | Config =>
171
+ content->String.includes("frontmanIntegration") &&
172
+ content->String.includes("@frontman-ai/astro") &&
173
+ content->String.includes("defineConfig")
174
+ | Middleware =>
175
+ content->String.includes("@frontman-ai/astro") &&
176
+ content->String.includes("createMiddleware") &&
177
+ content->String.includes("makeConfig") &&
178
+ content->String.includes("onRequest")
179
+ }
180
+ }
181
+
182
+ // Call LLM with model fallback chain
183
+ let callLLM = async (
184
+ ~existingContent: string,
185
+ ~fileType: fileType,
186
+ ~host: string,
187
+ ): result<string, string> => {
188
+ let systemPrompt = buildSystemPrompt(~fileType, ~host)
189
+ let userMessage = buildUserMessage(~existingContent)
190
+
191
+ let rec tryModels = async (remaining: array<string>, errors: array<string>) => {
192
+ switch remaining->Array.get(0) {
193
+ | None =>
194
+ let allErrors = errors->Array.join("; ")
195
+ Error(`All models failed: ${allErrors}`)
196
+ | Some(model) =>
197
+ let rest = remaining->Array.slice(~start=1, ~end=Array.length(remaining))
198
+ Console.log(` ${Style.dim(`Trying model: ${model}...`)}`)
199
+
200
+ switch await callModel(~model, ~systemPrompt, ~userMessage) {
201
+ | Ok(rawContent) =>
202
+ let content = stripMarkdownFences(rawContent)
203
+ switch validateOutput(~content, ~fileType) {
204
+ | true => Ok(content)
205
+ | false =>
206
+ let err = `${model}: output validation failed (missing Frontman imports)`
207
+ Console.log(` ${Style.dim(err)}`)
208
+ await tryModels(rest, errors->Array.concat([err]))
209
+ }
210
+ | Error(err) =>
211
+ let errMsg = `${model}: ${err}`
212
+ Console.log(` ${Style.dim(errMsg)}`)
213
+ await tryModels(rest, errors->Array.concat([errMsg]))
214
+ }
215
+ }
216
+ }
217
+
218
+ await tryModels(models, [])
219
+ }
220
+
221
+ // Prompt user for auto-edit with privacy disclosure (batched for multiple files)
222
+ let promptUserForAutoEdit = async (~fileNames: array<string>): bool => {
223
+ // Skip prompt if not interactive (piped input)
224
+ switch Readline.isTTY() {
225
+ | false => false
226
+ | true =>
227
+ Console.log("")
228
+ switch fileNames->Array.length {
229
+ | 1 =>
230
+ let fileName = fileNames->Array.getUnsafe(0)
231
+ Console.log(
232
+ ` ${Style.warn} ${Style.bold(fileName)} exists but doesn't have Frontman configured.`,
233
+ )
234
+ | _ =>
235
+ Console.log(
236
+ ` ${Style.warn} The following files exist but don't have Frontman configured:`,
237
+ )
238
+ fileNames->Array.forEach(fileName => {
239
+ Console.log(` ${Style.purple("•")} ${Style.bold(fileName)}`)
240
+ })
241
+ }
242
+ Console.log(
243
+ ` ${Style.dim("Your file contents will be sent to a public LLM (OpenCode Zen).")}`,
244
+ )
245
+ Console.log("")
246
+
247
+ let answer = await Readline.question(` Auto-edit using AI? ${Style.dim("[Y/n]")} `)
248
+
249
+ // Ctrl+D (EOF) returns null — treat as decline (never auto-consent)
250
+ switch answer->Nullable.toOption {
251
+ | None => false
252
+ | Some(raw) =>
253
+ switch raw->String.trim->String.toLowerCase {
254
+ | "" | "y" | "yes" => true
255
+ | _ => false
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ // Result type re-exported for use in Files.res
262
+ type autoEditResult =
263
+ | AutoEdited(string)
264
+ | AutoEditFailed(string)
265
+
266
+ // Maximum file size (in bytes) to send to the LLM. Files larger than this
267
+ // are skipped to avoid excessive latency or request failures.
268
+ let maxFileSizeBytes = 50_000
269
+
270
+ // Main auto-edit function: call LLM, write file
271
+ let autoEditFile = async (
272
+ ~filePath: string,
273
+ ~fileName: string,
274
+ ~existingContent: string,
275
+ ~fileType: fileType,
276
+ ~host: string,
277
+ ): autoEditResult => {
278
+ // Guard: skip files that are too large for reliable LLM editing
279
+ let fileSize = existingContent->String.length
280
+ switch fileSize > maxFileSizeBytes {
281
+ | true =>
282
+ AutoEditFailed(
283
+ `${fileName} is too large (${(fileSize / 1000)->Int.toString}KB) for auto-edit — max ${(maxFileSizeBytes / 1000)->Int.toString}KB`,
284
+ )
285
+ | false =>
286
+ Console.log("")
287
+ Console.log(` ${Style.purple("⟳")} Merging Frontman into ${Style.bold(fileName)}...`)
288
+
289
+ switch await callLLM(~existingContent, ~fileType, ~host) {
290
+ | Ok(newContent) =>
291
+ try {
292
+ await Fs.Promises.writeFile(filePath, newContent)
293
+ AutoEdited(fileName)
294
+ } catch {
295
+ | _ => AutoEditFailed(`Failed to write ${fileName}`)
296
+ }
297
+ | Error(err) => AutoEditFailed(err)
298
+ }
299
+ }
300
+ }