@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,345 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Fs from "fs";
5
+ import * as Nodepath from "node:path";
6
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+
9
+ async function fileExists(path) {
10
+ try {
11
+ await Fs.promises.access(path);
12
+ return true;
13
+ } catch (exn) {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ async function readFile(path) {
19
+ try {
20
+ return await Fs.promises.readFile(path, "utf8");
21
+ } catch (exn) {
22
+ return;
23
+ }
24
+ }
25
+
26
+ let astroPkgJsonSchema = S.schema(s => ({
27
+ version: s.m(S.string)
28
+ }));
29
+
30
+ async function detectAstroVersion(projectDir) {
31
+ let astroPkgPath = Nodepath.join(projectDir, "node_modules", "astro", "package.json");
32
+ let content = await readFile(astroPkgPath);
33
+ if (content === undefined) {
34
+ return;
35
+ }
36
+ try {
37
+ let pkg = S.parseJsonStringOrThrow(content, astroPkgJsonSchema);
38
+ let version = pkg.version;
39
+ let parts = version.split(".");
40
+ let match = parts[0];
41
+ let match$1 = parts[1];
42
+ if (match === undefined) {
43
+ return;
44
+ }
45
+ if (match$1 === undefined) {
46
+ return;
47
+ }
48
+ let major = Stdlib_Option.getOrThrow(Stdlib_Int.fromString(match, undefined), undefined);
49
+ let minorClean = match$1.split("-")[0];
50
+ let minor = Stdlib_Option.getOrThrow(Stdlib_Int.fromString(minorClean, undefined), undefined);
51
+ return {
52
+ major: major,
53
+ minor: minor,
54
+ raw: version
55
+ };
56
+ } catch (exn) {
57
+ return;
58
+ }
59
+ }
60
+
61
+ async function detectConfigFile(projectDir) {
62
+ let variants = [
63
+ "astro.config.mjs",
64
+ "astro.config.ts",
65
+ "astro.config.mts",
66
+ "astro.config.js"
67
+ ];
68
+ let check = async remaining => {
69
+ let name = remaining[0];
70
+ if (name === undefined) {
71
+ return;
72
+ }
73
+ let path = Nodepath.join(projectDir, name);
74
+ if (await fileExists(path)) {
75
+ return name;
76
+ }
77
+ let rest = remaining.slice(1, remaining.length);
78
+ return await check(rest);
79
+ };
80
+ return await check(variants);
81
+ }
82
+
83
+ let frontmanImportPattern = /@frontman-ai\/astro|@frontman\/frontman-astro|frontman-astro\/integration/;
84
+
85
+ let hostPattern = /host:\s*['\"]([^'\"]+)['\"]/;
86
+
87
+ async function analyzeFile(filePath) {
88
+ let content = await readFile(filePath);
89
+ if (content === undefined) {
90
+ return "NotFound";
91
+ }
92
+ if (!frontmanImportPattern.test(content)) {
93
+ return "NeedsManualEdit";
94
+ }
95
+ let result = hostPattern.exec(content);
96
+ if (result == null) {
97
+ return {
98
+ TAG: "HasFrontman",
99
+ host: ""
100
+ };
101
+ }
102
+ let maybeHost = Stdlib_Option.flatMap(result.slice(1)[0], x => x);
103
+ if (maybeHost !== undefined) {
104
+ return {
105
+ TAG: "HasFrontman",
106
+ host: maybeHost
107
+ };
108
+ } else {
109
+ return {
110
+ TAG: "HasFrontman",
111
+ host: ""
112
+ };
113
+ }
114
+ }
115
+
116
+ async function detectPackageManager(projectDir) {
117
+ let checkDir = async dir => {
118
+ let lockFiles = [
119
+ [
120
+ Nodepath.join(dir, "bun.lockb"),
121
+ "Bun"
122
+ ],
123
+ [
124
+ Nodepath.join(dir, "bun.lock"),
125
+ "Bun"
126
+ ],
127
+ [
128
+ Nodepath.join(dir, "deno.lock"),
129
+ "Deno"
130
+ ],
131
+ [
132
+ Nodepath.join(dir, "pnpm-lock.yaml"),
133
+ "Pnpm"
134
+ ],
135
+ [
136
+ Nodepath.join(dir, "yarn.lock"),
137
+ "Yarn"
138
+ ],
139
+ [
140
+ Nodepath.join(dir, "package-lock.json"),
141
+ "Npm"
142
+ ]
143
+ ];
144
+ let check = async remaining => {
145
+ let match = remaining[0];
146
+ if (match === undefined) {
147
+ return;
148
+ }
149
+ if (await fileExists(match[0])) {
150
+ return match[1];
151
+ }
152
+ let rest = remaining.slice(1, remaining.length);
153
+ return await check(rest);
154
+ };
155
+ return await check(lockFiles);
156
+ };
157
+ let parentDir = Nodepath.dirname(projectDir);
158
+ let grandparentDir = Nodepath.dirname(parentDir);
159
+ let dirsToCheck = [
160
+ projectDir,
161
+ parentDir,
162
+ grandparentDir
163
+ ].filter(d => {
164
+ if (d !== projectDir) {
165
+ return true;
166
+ } else {
167
+ return d === projectDir;
168
+ }
169
+ });
170
+ let seen = {};
171
+ let uniqueDirs = dirsToCheck.filter(d => {
172
+ let match = seen[d];
173
+ if (match !== undefined) {
174
+ return false;
175
+ } else {
176
+ seen[d] = true;
177
+ return true;
178
+ }
179
+ });
180
+ let tryDirs = async remaining => {
181
+ let dir = remaining[0];
182
+ if (dir === undefined) {
183
+ return "Npm";
184
+ }
185
+ let pm = await checkDir(dir);
186
+ if (pm !== undefined) {
187
+ return pm;
188
+ }
189
+ let rest = remaining.slice(1, remaining.length);
190
+ return await tryDirs(rest);
191
+ };
192
+ return await tryDirs(uniqueDirs);
193
+ }
194
+
195
+ async function hasPackageJson(projectDir) {
196
+ return await fileExists(Nodepath.join(projectDir, "package.json"));
197
+ }
198
+
199
+ async function detect(projectDir) {
200
+ let hasPackage = await hasPackageJson(projectDir);
201
+ if (!hasPackage) {
202
+ return {
203
+ TAG: "Error",
204
+ _0: "No package.json found. Please run from your Astro project root."
205
+ };
206
+ }
207
+ let astroVersion = await detectAstroVersion(projectDir);
208
+ if (astroVersion === undefined) {
209
+ return {
210
+ TAG: "Error",
211
+ _0: "Could not find Astro in node_modules. Please run 'npm install' first or verify this is an Astro project."
212
+ };
213
+ }
214
+ let name = await detectConfigFile(projectDir);
215
+ let configFileName = name !== undefined ? name : "astro.config.mjs";
216
+ let configPath = Nodepath.join(projectDir, configFileName);
217
+ let config = await analyzeFile(configPath);
218
+ let middlewareTsPath = Nodepath.join(projectDir, "src", "middleware.ts");
219
+ let middlewareJsPath = Nodepath.join(projectDir, "src", "middleware.js");
220
+ let match = await fileExists(middlewareTsPath) ? [
221
+ await analyzeFile(middlewareTsPath),
222
+ "src/middleware.ts"
223
+ ] : (
224
+ await fileExists(middlewareJsPath) ? [
225
+ await analyzeFile(middlewareJsPath),
226
+ "src/middleware.js"
227
+ ] : [
228
+ "NotFound",
229
+ "src/middleware.ts"
230
+ ]
231
+ );
232
+ let packageManager = await detectPackageManager(projectDir);
233
+ return {
234
+ TAG: "Ok",
235
+ _0: {
236
+ astroVersion: astroVersion,
237
+ config: config,
238
+ middleware: match[0],
239
+ configFileName: configFileName,
240
+ middlewareFileName: match[1],
241
+ packageManager: packageManager
242
+ }
243
+ };
244
+ }
245
+
246
+ function getPackageManagerCommand(pm) {
247
+ switch (pm) {
248
+ case "Npm" :
249
+ return "npm";
250
+ case "Yarn" :
251
+ return "npx yarn";
252
+ case "Pnpm" :
253
+ return "npx pnpm";
254
+ case "Bun" :
255
+ return "bun";
256
+ case "Deno" :
257
+ return "deno";
258
+ }
259
+ }
260
+
261
+ function getDevCommand(pm) {
262
+ switch (pm) {
263
+ case "Npm" :
264
+ return "npm run dev";
265
+ case "Yarn" :
266
+ return "yarn dev";
267
+ case "Pnpm" :
268
+ return "pnpm dev";
269
+ case "Bun" :
270
+ return "bun dev";
271
+ case "Deno" :
272
+ return "deno task dev";
273
+ }
274
+ }
275
+
276
+ function getInstallArgs(pm, isDevOpt) {
277
+ let isDev = isDevOpt !== undefined ? isDevOpt : false;
278
+ switch (pm) {
279
+ case "Npm" :
280
+ if (isDev) {
281
+ return [
282
+ "install",
283
+ "-D"
284
+ ];
285
+ } else {
286
+ return ["install"];
287
+ }
288
+ case "Yarn" :
289
+ if (isDev) {
290
+ return [
291
+ "add",
292
+ "-D"
293
+ ];
294
+ } else {
295
+ return ["add"];
296
+ }
297
+ case "Pnpm" :
298
+ if (isDev) {
299
+ return [
300
+ "add",
301
+ "--save-dev"
302
+ ];
303
+ } else {
304
+ return ["add"];
305
+ }
306
+ case "Bun" :
307
+ case "Deno" :
308
+ break;
309
+ }
310
+ if (isDev) {
311
+ return [
312
+ "add",
313
+ "--dev"
314
+ ];
315
+ } else {
316
+ return ["add"];
317
+ }
318
+ }
319
+
320
+ let Bindings;
321
+
322
+ let Fs$1;
323
+
324
+ let Path;
325
+
326
+ export {
327
+ Bindings,
328
+ Fs$1 as Fs,
329
+ Path,
330
+ fileExists,
331
+ readFile,
332
+ astroPkgJsonSchema,
333
+ detectAstroVersion,
334
+ detectConfigFile,
335
+ frontmanImportPattern,
336
+ hostPattern,
337
+ analyzeFile,
338
+ detectPackageManager,
339
+ hasPackageJson,
340
+ detect,
341
+ getPackageManagerCommand,
342
+ getDevCommand,
343
+ getInstallArgs,
344
+ }
345
+ /* astroPkgJsonSchema Not a pure module */
@@ -0,0 +1,244 @@
1
+ // File operations module for CLI installer
2
+ module Bindings = FrontmanBindings
3
+ module Fs = Bindings.Fs
4
+ module Path = Bindings.Path
5
+
6
+ module Detect = FrontmanAstro__Cli__Detect
7
+ module Templates = FrontmanAstro__Cli__Templates
8
+ module AutoEdit = FrontmanAstro__Cli__AutoEdit
9
+ module Style = FrontmanAstro__Cli__Style
10
+
11
+ // Result type for file operations
12
+ type fileResult =
13
+ | Created(string)
14
+ | Updated({fileName: string, oldHost: string, newHost: string})
15
+ | Skipped(string)
16
+ | ManualEditRequired({fileName: string, details: string})
17
+ | AutoEdited(string)
18
+
19
+ // Pattern to match and replace host in existing file
20
+ let hostPattern = %re("/host:\s*['\"]([^'\"]+)['\"]/")
21
+
22
+ // Escape special replacement patterns ($1, $&, etc.) in a string used as
23
+ // the replacement argument to String.replaceRegExp
24
+ let escapeReplacement: string => string = %raw(`
25
+ function(str) { return str.replace(/\$/g, '$$$$'); }
26
+ `)
27
+
28
+ // Update host in existing file content
29
+ let updateHostInContent = (content: string, newHost: string): string => {
30
+ let safeHost = escapeReplacement(newHost)
31
+ content->String.replaceRegExp(hostPattern, `host: '${safeHost}'`)
32
+ }
33
+
34
+ // Read file content
35
+ let readFile = async (path: string): option<string> => {
36
+ try {
37
+ let content = await Fs.Promises.readFile(path)
38
+ Some(content)
39
+ } catch {
40
+ | _ => None
41
+ }
42
+ }
43
+
44
+ // Write file content
45
+ let writeFile = async (path: string, content: string): result<unit, string> => {
46
+ try {
47
+ await Fs.Promises.writeFile(path, content)
48
+ Ok()
49
+ } catch {
50
+ | _ => Error(`Failed to write ${path}`)
51
+ }
52
+ }
53
+
54
+ // Info about a file that needs auto-editing (collected before prompting)
55
+ type pendingAutoEdit = {
56
+ filePath: string,
57
+ fileName: string,
58
+ fileType: AutoEdit.fileType,
59
+ manualDetails: string,
60
+ }
61
+
62
+ // Handle the NeedsManualEdit case — when autoEdit is true, perform the edit;
63
+ // when false, return manual instructions
64
+ let handleNeedsManualEdit = async (
65
+ ~filePath: string,
66
+ ~fileName: string,
67
+ ~host: string,
68
+ ~fileType: AutoEdit.fileType,
69
+ ~dryRun: bool,
70
+ ~autoEdit: bool,
71
+ ~manualDetails: string,
72
+ ): result<fileResult, string> => {
73
+ switch dryRun {
74
+ | true => Ok(ManualEditRequired({fileName, details: manualDetails}))
75
+ | false =>
76
+ switch autoEdit {
77
+ | false => Ok(ManualEditRequired({fileName, details: manualDetails}))
78
+ | true =>
79
+ switch await readFile(filePath) {
80
+ | None => Ok(ManualEditRequired({fileName, details: manualDetails}))
81
+ | Some(existingContent) =>
82
+ switch await AutoEdit.autoEditFile(
83
+ ~filePath,
84
+ ~fileName,
85
+ ~existingContent,
86
+ ~fileType,
87
+ ~host,
88
+ ) {
89
+ | AutoEdit.AutoEdited(name) => Ok(AutoEdited(name))
90
+ | AutoEdit.AutoEditFailed(err) =>
91
+ Console.log(Templates.SuccessMessages.autoEditFailed(fileName, err))
92
+ Console.log(` Falling back to manual instructions.`)
93
+ Ok(ManualEditRequired({fileName, details: manualDetails}))
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ // Check if a file handler would need auto-editing (without prompting)
101
+ let getPendingAutoEdit = (
102
+ ~existingFile: Detect.existingFile,
103
+ ~filePath: string,
104
+ ~fileName: string,
105
+ ~fileType: AutoEdit.fileType,
106
+ ~manualDetails: string,
107
+ ): option<pendingAutoEdit> => {
108
+ switch existingFile {
109
+ | NeedsManualEdit => Some({filePath, fileName, fileType, manualDetails})
110
+ | NotFound | HasFrontman(_) => None
111
+ }
112
+ }
113
+
114
+ // Handle astro.config.mjs file
115
+ let handleConfig = async (
116
+ ~projectDir: string,
117
+ ~host: string,
118
+ ~configFileName: string,
119
+ ~existingFile: Detect.existingFile,
120
+ ~dryRun: bool,
121
+ ~autoEdit: bool=false,
122
+ ): result<fileResult, string> => {
123
+ let filePath = Path.join([projectDir, configFileName])
124
+ let fileName = configFileName
125
+
126
+ switch existingFile {
127
+ | NotFound =>
128
+ switch dryRun {
129
+ | true => Ok(Created(fileName))
130
+ | false =>
131
+ let content = Templates.configTemplate(host)
132
+ switch await writeFile(filePath, content) {
133
+ | Ok() => Ok(Created(fileName))
134
+ | Error(e) => Error(e)
135
+ }
136
+ }
137
+
138
+ | HasFrontman({host: existingHost}) =>
139
+ // Config files don't contain host themselves (host is in middleware),
140
+ // so an empty existingHost means Frontman is already configured — skip.
141
+ switch existingHost == host || existingHost == "" {
142
+ | true => Ok(Skipped(fileName))
143
+ | false =>
144
+ switch dryRun {
145
+ | true => Ok(Updated({fileName, oldHost: existingHost, newHost: host}))
146
+ | false =>
147
+ switch await readFile(filePath) {
148
+ | None => Error(`Failed to read ${fileName}`)
149
+ | Some(content) =>
150
+ let newContent = updateHostInContent(content, host)
151
+ switch await writeFile(filePath, newContent) {
152
+ | Ok() => Ok(Updated({fileName, oldHost: existingHost, newHost: host}))
153
+ | Error(e) => Error(e)
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ | NeedsManualEdit =>
160
+ await handleNeedsManualEdit(
161
+ ~filePath,
162
+ ~fileName,
163
+ ~host,
164
+ ~fileType=AutoEdit.Config,
165
+ ~dryRun,
166
+ ~autoEdit,
167
+ ~manualDetails=Templates.ManualInstructions.config(fileName, host),
168
+ )
169
+ }
170
+ }
171
+
172
+ // Handle src/middleware.ts (or .js) file
173
+ let handleMiddleware = async (
174
+ ~projectDir: string,
175
+ ~host: string,
176
+ ~middlewareFileName: string,
177
+ ~existingFile: Detect.existingFile,
178
+ ~dryRun: bool,
179
+ ~autoEdit: bool=false,
180
+ ): result<fileResult, string> => {
181
+ let filePath = Path.join([projectDir, middlewareFileName])
182
+ let fileName = middlewareFileName
183
+
184
+ switch existingFile {
185
+ | NotFound =>
186
+ switch dryRun {
187
+ | true => Ok(Created(fileName))
188
+ | false =>
189
+ // Ensure src/ directory exists
190
+ let srcDir = Path.join([projectDir, "src"])
191
+ let _ = await Fs.Promises.mkdir(srcDir, {recursive: true})
192
+ let content = Templates.middlewareTemplate(host)
193
+ switch await writeFile(filePath, content) {
194
+ | Ok() => Ok(Created(fileName))
195
+ | Error(e) => Error(e)
196
+ }
197
+ }
198
+
199
+ | HasFrontman({host: existingHost}) =>
200
+ // When analyzeFile can't extract a host (e.g. host: process.env.FRONTMAN_HOST),
201
+ // it returns HasFrontman({host: ""}). Treat empty host as already configured
202
+ // to avoid rewriting the file unchanged with a misleading "Updated" message.
203
+ switch existingHost == host || existingHost == "" {
204
+ | true => Ok(Skipped(fileName))
205
+ | false =>
206
+ switch dryRun {
207
+ | true => Ok(Updated({fileName, oldHost: existingHost, newHost: host}))
208
+ | false =>
209
+ switch await readFile(filePath) {
210
+ | None => Error(`Failed to read ${fileName}`)
211
+ | Some(content) =>
212
+ let newContent = updateHostInContent(content, host)
213
+ switch await writeFile(filePath, newContent) {
214
+ | Ok() => Ok(Updated({fileName, oldHost: existingHost, newHost: host}))
215
+ | Error(e) => Error(e)
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ | NeedsManualEdit =>
222
+ await handleNeedsManualEdit(
223
+ ~filePath,
224
+ ~fileName,
225
+ ~host,
226
+ ~fileType=AutoEdit.Middleware,
227
+ ~dryRun,
228
+ ~autoEdit,
229
+ ~manualDetails=Templates.ManualInstructions.middleware(fileName, host),
230
+ )
231
+ }
232
+ }
233
+
234
+ // Format file result for display (short one-liner)
235
+ let formatResult = (result: fileResult): string => {
236
+ switch result {
237
+ | Created(fileName) => Templates.SuccessMessages.fileCreated(fileName)
238
+ | Updated({fileName, oldHost, newHost}) =>
239
+ Templates.SuccessMessages.hostUpdated(fileName, oldHost, newHost)
240
+ | Skipped(fileName) => Templates.SuccessMessages.fileSkipped(fileName)
241
+ | ManualEditRequired({fileName, _}) => Templates.SuccessMessages.manualEditRequired(fileName)
242
+ | AutoEdited(fileName) => Templates.SuccessMessages.fileAutoEdited(fileName)
243
+ }
244
+ }