@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.
- package/README.md +53 -0
- package/dist/cli.js +2744 -0
- package/package.json +66 -0
- package/src/FrontmanAstro.res +20 -0
- package/src/FrontmanAstro.res.mjs +36 -0
- package/src/FrontmanAstro__AstroBindings.res +46 -0
- package/src/FrontmanAstro__AstroBindings.res.mjs +2 -0
- package/src/FrontmanAstro__Config.res +85 -0
- package/src/FrontmanAstro__Config.res.mjs +44 -0
- package/src/FrontmanAstro__Integration.res +35 -0
- package/src/FrontmanAstro__Integration.res.mjs +36 -0
- package/src/FrontmanAstro__Middleware.res +149 -0
- package/src/FrontmanAstro__Middleware.res.mjs +141 -0
- package/src/FrontmanAstro__Server.res +196 -0
- package/src/FrontmanAstro__Server.res.mjs +241 -0
- package/src/FrontmanAstro__ToolRegistry.res +21 -0
- package/src/FrontmanAstro__ToolRegistry.res.mjs +41 -0
- package/src/FrontmanAstro__ToolbarApp.res +50 -0
- package/src/FrontmanAstro__ToolbarApp.res.mjs +39 -0
- package/src/cli/FrontmanAstro__Cli.res +126 -0
- package/src/cli/FrontmanAstro__Cli.res.mjs +180 -0
- package/src/cli/FrontmanAstro__Cli__AutoEdit.res +300 -0
- package/src/cli/FrontmanAstro__Cli__AutoEdit.res.mjs +266 -0
- package/src/cli/FrontmanAstro__Cli__Detect.res +298 -0
- package/src/cli/FrontmanAstro__Cli__Detect.res.mjs +345 -0
- package/src/cli/FrontmanAstro__Cli__Files.res +244 -0
- package/src/cli/FrontmanAstro__Cli__Files.res.mjs +321 -0
- package/src/cli/FrontmanAstro__Cli__Install.res +224 -0
- package/src/cli/FrontmanAstro__Cli__Install.res.mjs +194 -0
- package/src/cli/FrontmanAstro__Cli__Style.res +22 -0
- package/src/cli/FrontmanAstro__Cli__Style.res.mjs +61 -0
- package/src/cli/FrontmanAstro__Cli__Templates.res +226 -0
- package/src/cli/FrontmanAstro__Cli__Templates.res.mjs +237 -0
- package/src/cli/cli.mjs +3 -0
- package/src/tools/FrontmanAstro__Tool__GetPages.res +164 -0
- package/src/tools/FrontmanAstro__Tool__GetPages.res.mjs +180 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Fs from "fs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Readline$FrontmanBindings from "@frontman/bindings/src/Readline.res.mjs";
|
|
6
|
+
import * as FrontmanAstro__Cli__Style$FrontmanAiAstro from "./FrontmanAstro__Cli__Style.res.mjs";
|
|
7
|
+
import * as FrontmanAstro__Cli__Templates$FrontmanAiAstro from "./FrontmanAstro__Cli__Templates.res.mjs";
|
|
8
|
+
|
|
9
|
+
let apiBaseUrl = "https://opencode.ai/zen/v1/chat/completions";
|
|
10
|
+
|
|
11
|
+
let apiKey = "public";
|
|
12
|
+
|
|
13
|
+
let models = [
|
|
14
|
+
"gpt-5-nano",
|
|
15
|
+
"big-pickle",
|
|
16
|
+
"glm-4.7-free"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function buildSystemPrompt(fileType, host) {
|
|
20
|
+
let match;
|
|
21
|
+
match = fileType === "Config" ? [
|
|
22
|
+
"astro.config.mjs",
|
|
23
|
+
FrontmanAstro__Cli__Templates$FrontmanAiAstro.ErrorMessages.configManualSetup("astro.config.mjs", host),
|
|
24
|
+
FrontmanAstro__Cli__Templates$FrontmanAiAstro.configTemplate(host),
|
|
25
|
+
`- Add the import for '@astrojs/node' at the top of the file
|
|
26
|
+
- Add the import for '@frontman-ai/astro/integration' at the top of the file
|
|
27
|
+
- Add frontmanIntegration() to the integrations array
|
|
28
|
+
- Add SSR dev mode config: ...(isProd ? {} : { output: 'server', adapter: node({ mode: 'standalone' }) })
|
|
29
|
+
- Add const isProd = process.env.NODE_ENV === 'production'; before defineConfig
|
|
30
|
+
- Preserve ALL existing integrations and configuration unchanged
|
|
31
|
+
- Do not remove or modify any existing imports or settings`
|
|
32
|
+
] : [
|
|
33
|
+
"src/middleware.ts",
|
|
34
|
+
FrontmanAstro__Cli__Templates$FrontmanAiAstro.ErrorMessages.middlewareManualSetup("src/middleware.ts", host),
|
|
35
|
+
FrontmanAstro__Cli__Templates$FrontmanAiAstro.middlewareTemplate(host),
|
|
36
|
+
`- Add the import for '@frontman-ai/astro' (createMiddleware, makeConfig) at the top of the file
|
|
37
|
+
- Add the import for 'astro:middleware' (defineMiddleware, sequence) at the top of the file
|
|
38
|
+
- Create a Frontman middleware instance with makeConfig({ host: '` + host + `' })
|
|
39
|
+
- Create a defineMiddleware wrapper for the Frontman handler
|
|
40
|
+
- Use sequence() to combine the Frontman middleware with the existing onRequest handler
|
|
41
|
+
- The Frontman middleware should come FIRST in the sequence
|
|
42
|
+
- Preserve ALL existing middleware functionality unchanged
|
|
43
|
+
- Do not remove or modify any existing imports or middleware logic`
|
|
44
|
+
];
|
|
45
|
+
return `You are a code editor. Modify an Astro ` + match[0] + ` file to integrate Frontman.
|
|
46
|
+
|
|
47
|
+
## What to add
|
|
48
|
+
` + match[1] + `
|
|
49
|
+
|
|
50
|
+
## Reference template (for a fresh file without any existing code):
|
|
51
|
+
|
|
52
|
+
` + match[2] + `
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
` + match[3] + `
|
|
56
|
+
- Return ONLY the complete file contents. No markdown fences, no explanations, no comments about changes.`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildUserMessage(existingContent) {
|
|
60
|
+
return `Here is the existing file to modify:
|
|
61
|
+
|
|
62
|
+
` + existingContent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let fetchChatCompletion = (async function(url, apiKey, model, systemPrompt, userMessage, timeoutMs) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"Authorization": "Bearer " + apiKey,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
model: model,
|
|
76
|
+
temperature: 0,
|
|
77
|
+
messages: [
|
|
78
|
+
{ role: "system", content: systemPrompt },
|
|
79
|
+
{ role: "user", content: userMessage },
|
|
80
|
+
],
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return { TAG: "Error", _0: "HTTP " + response.status + ": " + response.statusText };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const json = await response.json();
|
|
89
|
+
const content = json?.choices?.[0]?.message?.content?.trim();
|
|
90
|
+
if (!content) {
|
|
91
|
+
return { TAG: "Error", _0: "Empty response from model" };
|
|
92
|
+
}
|
|
93
|
+
return { TAG: "Ok", _0: content };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err?.name === "TimeoutError") {
|
|
96
|
+
return { TAG: "Error", _0: "Request timed out after " + (timeoutMs / 1000) + "s" };
|
|
97
|
+
}
|
|
98
|
+
return { TAG: "Error", _0: "Request failed: " + (err?.message || "Unknown error") };
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function callModel(model, systemPrompt, userMessage) {
|
|
103
|
+
return await fetchChatCompletion(apiBaseUrl, apiKey, model, systemPrompt, userMessage, 30000);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripMarkdownFences(content) {
|
|
107
|
+
let lines = content.split("\n");
|
|
108
|
+
let len = lines.length;
|
|
109
|
+
let firstLine = Stdlib_Option.getOr(lines[0], "");
|
|
110
|
+
let startsWithFence = firstLine.startsWith("```");
|
|
111
|
+
if (!startsWithFence) {
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
let lastLine = Stdlib_Option.getOr(lines[len - 1 | 0], "");
|
|
115
|
+
let endsWithFence = lastLine.trim() === "```";
|
|
116
|
+
let endIdx = endsWithFence ? len - 1 | 0 : len;
|
|
117
|
+
return lines.slice(1, endIdx).join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validateOutput(content, fileType) {
|
|
121
|
+
if (fileType === "Config") {
|
|
122
|
+
if (content.includes("frontmanIntegration") && content.includes("@frontman-ai/astro")) {
|
|
123
|
+
return content.includes("defineConfig");
|
|
124
|
+
} else {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
} else if (content.includes("@frontman-ai/astro") && content.includes("createMiddleware") && content.includes("makeConfig")) {
|
|
128
|
+
return content.includes("onRequest");
|
|
129
|
+
} else {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function callLLM(existingContent, fileType, host) {
|
|
135
|
+
let systemPrompt = buildSystemPrompt(fileType, host);
|
|
136
|
+
let userMessage = buildUserMessage(existingContent);
|
|
137
|
+
let tryModels = async (remaining, errors) => {
|
|
138
|
+
let model = remaining[0];
|
|
139
|
+
if (model !== undefined) {
|
|
140
|
+
let rest = remaining.slice(1, remaining.length);
|
|
141
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.dim(`Trying model: ` + model + `...`));
|
|
142
|
+
let rawContent = await callModel(model, systemPrompt, userMessage);
|
|
143
|
+
if (rawContent.TAG === "Ok") {
|
|
144
|
+
let content = stripMarkdownFences(rawContent._0);
|
|
145
|
+
if (validateOutput(content, fileType)) {
|
|
146
|
+
return {
|
|
147
|
+
TAG: "Ok",
|
|
148
|
+
_0: content
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
let err = model + `: output validation failed (missing Frontman imports)`;
|
|
152
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.dim(err));
|
|
153
|
+
return await tryModels(rest, errors.concat([err]));
|
|
154
|
+
}
|
|
155
|
+
let errMsg = model + `: ` + rawContent._0;
|
|
156
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.dim(errMsg));
|
|
157
|
+
return await tryModels(rest, errors.concat([errMsg]));
|
|
158
|
+
}
|
|
159
|
+
let allErrors = errors.join("; ");
|
|
160
|
+
return {
|
|
161
|
+
TAG: "Error",
|
|
162
|
+
_0: `All models failed: ` + allErrors
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
return await tryModels(models, []);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function promptUserForAutoEdit(fileNames) {
|
|
169
|
+
if (!Readline$FrontmanBindings.isTTY()) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
console.log("");
|
|
173
|
+
let match = fileNames.length;
|
|
174
|
+
if (match !== 1) {
|
|
175
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.warn + ` The following files exist but don't have Frontman configured:`);
|
|
176
|
+
fileNames.forEach(fileName => {
|
|
177
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.purple("•") + ` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.bold(fileName));
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
let fileName = fileNames[0];
|
|
181
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.warn + ` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.bold(fileName) + ` exists but doesn't have Frontman configured.`);
|
|
182
|
+
}
|
|
183
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.dim("Your file contents will be sent to a public LLM (OpenCode Zen)."));
|
|
184
|
+
console.log("");
|
|
185
|
+
let answer = await Readline$FrontmanBindings.question(` Auto-edit using AI? ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.dim("[Y/n]") + ` `);
|
|
186
|
+
if (answer == null) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
let match$1 = answer.trim().toLowerCase();
|
|
190
|
+
switch (match$1) {
|
|
191
|
+
case "" :
|
|
192
|
+
case "y" :
|
|
193
|
+
case "yes" :
|
|
194
|
+
return true;
|
|
195
|
+
default:
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function autoEditFile(filePath, fileName, existingContent, fileType, host) {
|
|
201
|
+
let fileSize = existingContent.length;
|
|
202
|
+
if (fileSize > 50000) {
|
|
203
|
+
return {
|
|
204
|
+
TAG: "AutoEditFailed",
|
|
205
|
+
_0: fileName + ` is too large (` + (fileSize / 1000 | 0).toString() + `KB) for auto-edit — max ` + (50).toString() + `KB`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(` ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.purple("⟳") + ` Merging Frontman into ` + FrontmanAstro__Cli__Style$FrontmanAiAstro.bold(fileName) + `...`);
|
|
210
|
+
let newContent = await callLLM(existingContent, fileType, host);
|
|
211
|
+
if (newContent.TAG !== "Ok") {
|
|
212
|
+
return {
|
|
213
|
+
TAG: "AutoEditFailed",
|
|
214
|
+
_0: newContent._0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await Fs.promises.writeFile(filePath, newContent._0, "utf8");
|
|
219
|
+
return {
|
|
220
|
+
TAG: "AutoEdited",
|
|
221
|
+
_0: fileName
|
|
222
|
+
};
|
|
223
|
+
} catch (exn) {
|
|
224
|
+
return {
|
|
225
|
+
TAG: "AutoEditFailed",
|
|
226
|
+
_0: `Failed to write ` + fileName
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let Bindings;
|
|
232
|
+
|
|
233
|
+
let Fs$1;
|
|
234
|
+
|
|
235
|
+
let Readline;
|
|
236
|
+
|
|
237
|
+
let Templates;
|
|
238
|
+
|
|
239
|
+
let Style;
|
|
240
|
+
|
|
241
|
+
let requestTimeoutMs = 30000;
|
|
242
|
+
|
|
243
|
+
let maxFileSizeBytes = 50000;
|
|
244
|
+
|
|
245
|
+
export {
|
|
246
|
+
Bindings,
|
|
247
|
+
Fs$1 as Fs,
|
|
248
|
+
Readline,
|
|
249
|
+
Templates,
|
|
250
|
+
Style,
|
|
251
|
+
apiBaseUrl,
|
|
252
|
+
apiKey,
|
|
253
|
+
models,
|
|
254
|
+
buildSystemPrompt,
|
|
255
|
+
buildUserMessage,
|
|
256
|
+
requestTimeoutMs,
|
|
257
|
+
fetchChatCompletion,
|
|
258
|
+
callModel,
|
|
259
|
+
stripMarkdownFences,
|
|
260
|
+
validateOutput,
|
|
261
|
+
callLLM,
|
|
262
|
+
promptUserForAutoEdit,
|
|
263
|
+
maxFileSizeBytes,
|
|
264
|
+
autoEditFile,
|
|
265
|
+
}
|
|
266
|
+
/* fetchChatCompletion Not a pure module */
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Detection module for Astro project analysis
|
|
2
|
+
module Bindings = FrontmanBindings
|
|
3
|
+
module Fs = Bindings.Fs
|
|
4
|
+
module Path = Bindings.Path
|
|
5
|
+
|
|
6
|
+
type astroVersion = {
|
|
7
|
+
major: int,
|
|
8
|
+
minor: int,
|
|
9
|
+
raw: string,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type packageManager =
|
|
13
|
+
| Npm
|
|
14
|
+
| Yarn
|
|
15
|
+
| Pnpm
|
|
16
|
+
| Bun
|
|
17
|
+
| Deno
|
|
18
|
+
|
|
19
|
+
type existingFile =
|
|
20
|
+
| NotFound
|
|
21
|
+
| HasFrontman({host: string})
|
|
22
|
+
| NeedsManualEdit
|
|
23
|
+
|
|
24
|
+
type projectInfo = {
|
|
25
|
+
astroVersion: option<astroVersion>,
|
|
26
|
+
config: existingFile,
|
|
27
|
+
middleware: existingFile,
|
|
28
|
+
configFileName: string,
|
|
29
|
+
middlewareFileName: string,
|
|
30
|
+
packageManager: packageManager,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if a file exists
|
|
34
|
+
let fileExists = async (path: string): bool => {
|
|
35
|
+
try {
|
|
36
|
+
await Fs.Promises.access(path)
|
|
37
|
+
true
|
|
38
|
+
} catch {
|
|
39
|
+
| _ => false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read file content safely
|
|
44
|
+
let readFile = async (path: string): option<string> => {
|
|
45
|
+
try {
|
|
46
|
+
let content = await Fs.Promises.readFile(path)
|
|
47
|
+
Some(content)
|
|
48
|
+
} catch {
|
|
49
|
+
| _ => None
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Schema for the minimal astro package.json we need
|
|
54
|
+
@schema
|
|
55
|
+
type astroPkgJson = {version: string}
|
|
56
|
+
|
|
57
|
+
// Detect Astro version from node_modules
|
|
58
|
+
let detectAstroVersion = async (projectDir: string): option<astroVersion> => {
|
|
59
|
+
let astroPkgPath = Path.join([projectDir, "node_modules", "astro", "package.json"])
|
|
60
|
+
|
|
61
|
+
switch await readFile(astroPkgPath) {
|
|
62
|
+
| None => None
|
|
63
|
+
| Some(content) =>
|
|
64
|
+
try {
|
|
65
|
+
let pkg = S.parseJsonStringOrThrow(content, astroPkgJsonSchema)
|
|
66
|
+
let version = pkg.version
|
|
67
|
+
// Parse version like "5.0.0" or "5.1.0-beta.1"
|
|
68
|
+
let parts = version->String.split(".")
|
|
69
|
+
switch (parts->Array.get(0), parts->Array.get(1)) {
|
|
70
|
+
| (Some(majorStr), Some(minorStr)) =>
|
|
71
|
+
let major = majorStr->Int.fromString->Option.getOrThrow
|
|
72
|
+
// Handle minor with potential suffixes like "0-beta"
|
|
73
|
+
let minorClean = minorStr->String.split("-")->Array.getUnsafe(0)
|
|
74
|
+
let minor = minorClean->Int.fromString->Option.getOrThrow
|
|
75
|
+
Some({major, minor, raw: version})
|
|
76
|
+
| _ => None
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
| _ => None
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Detect which astro config file variant exists
|
|
85
|
+
// Checks: astro.config.mjs, astro.config.ts, astro.config.mts, astro.config.js
|
|
86
|
+
let detectConfigFile = async (projectDir: string): option<string> => {
|
|
87
|
+
let variants = ["astro.config.mjs", "astro.config.ts", "astro.config.mts", "astro.config.js"]
|
|
88
|
+
|
|
89
|
+
let rec check = async (remaining: array<string>) => {
|
|
90
|
+
switch remaining->Array.get(0) {
|
|
91
|
+
| None => None
|
|
92
|
+
| Some(name) =>
|
|
93
|
+
let path = Path.join([projectDir, name])
|
|
94
|
+
switch await fileExists(path) {
|
|
95
|
+
| true => Some(name)
|
|
96
|
+
| false =>
|
|
97
|
+
let rest = remaining->Array.slice(~start=1, ~end=Array.length(remaining))
|
|
98
|
+
await check(rest)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await check(variants)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pattern to detect @frontman-ai/astro (or legacy @frontman/frontman-astro) import
|
|
107
|
+
let frontmanImportPattern = %re("/@frontman-ai\/astro|@frontman\/frontman-astro|frontman-astro\/integration/")
|
|
108
|
+
|
|
109
|
+
// Pattern to extract host from makeConfig or createMiddleware config
|
|
110
|
+
let hostPattern = %re("/host:\s*['\"]([^'\"]+)['\"]/")
|
|
111
|
+
|
|
112
|
+
// Analyze an existing file for Frontman configuration
|
|
113
|
+
let analyzeFile = async (filePath: string): existingFile => {
|
|
114
|
+
switch await readFile(filePath) {
|
|
115
|
+
| None => NotFound
|
|
116
|
+
| Some(content) =>
|
|
117
|
+
// Check if it imports @frontman-ai/astro (or legacy @frontman/frontman-astro)
|
|
118
|
+
switch frontmanImportPattern->RegExp.test(content) {
|
|
119
|
+
| true =>
|
|
120
|
+
// Try to extract the host
|
|
121
|
+
switch hostPattern->RegExp.exec(content) {
|
|
122
|
+
| Some(result) =>
|
|
123
|
+
let maybeHost =
|
|
124
|
+
result
|
|
125
|
+
->RegExp.Result.matches
|
|
126
|
+
->Array.get(0) // First capture group after slice(1)
|
|
127
|
+
->Option.flatMap(x => x)
|
|
128
|
+
switch maybeHost {
|
|
129
|
+
| Some(host) => HasFrontman({host: host})
|
|
130
|
+
| None => HasFrontman({host: ""})
|
|
131
|
+
}
|
|
132
|
+
| None => HasFrontman({host: ""})
|
|
133
|
+
}
|
|
134
|
+
| false => NeedsManualEdit
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Detect package manager from lock files
|
|
140
|
+
// Checks current directory and parent directories (for monorepo setups)
|
|
141
|
+
let detectPackageManager = async (projectDir: string): packageManager => {
|
|
142
|
+
// Check a directory for lock files, in priority order
|
|
143
|
+
let checkDir = async (dir: string): option<packageManager> => {
|
|
144
|
+
let lockFiles = [
|
|
145
|
+
(Path.join([dir, "bun.lockb"]), Bun),
|
|
146
|
+
(Path.join([dir, "bun.lock"]), Bun),
|
|
147
|
+
(Path.join([dir, "deno.lock"]), Deno),
|
|
148
|
+
(Path.join([dir, "pnpm-lock.yaml"]), Pnpm),
|
|
149
|
+
(Path.join([dir, "yarn.lock"]), Yarn),
|
|
150
|
+
(Path.join([dir, "package-lock.json"]), Npm),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
let rec check = async (remaining: array<(string, packageManager)>) => {
|
|
154
|
+
switch remaining->Array.get(0) {
|
|
155
|
+
| None => None
|
|
156
|
+
| Some((path, pm)) =>
|
|
157
|
+
switch await fileExists(path) {
|
|
158
|
+
| true => Some(pm)
|
|
159
|
+
| false =>
|
|
160
|
+
let rest = remaining->Array.slice(~start=1, ~end=Array.length(remaining))
|
|
161
|
+
await check(rest)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await check(lockFiles)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check project dir, then parent, then grandparent (monorepo support)
|
|
170
|
+
let dirsToCheck = {
|
|
171
|
+
let parentDir = Path.dirname(projectDir)
|
|
172
|
+
let grandparentDir = Path.dirname(parentDir)
|
|
173
|
+
[projectDir, parentDir, grandparentDir]->Array.filter(d => d != projectDir || d == projectDir)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Deduplicate (e.g. at filesystem root, dirname("/") == "/")
|
|
177
|
+
let uniqueDirs = {
|
|
178
|
+
let seen = Dict.make()
|
|
179
|
+
dirsToCheck->Array.filter(d => {
|
|
180
|
+
switch seen->Dict.get(d) {
|
|
181
|
+
| Some(_) => false
|
|
182
|
+
| None =>
|
|
183
|
+
seen->Dict.set(d, true)
|
|
184
|
+
true
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let rec tryDirs = async (remaining: array<string>) => {
|
|
190
|
+
switch remaining->Array.get(0) {
|
|
191
|
+
| None => Npm // Default to npm
|
|
192
|
+
| Some(dir) =>
|
|
193
|
+
switch await checkDir(dir) {
|
|
194
|
+
| Some(pm) => pm
|
|
195
|
+
| None =>
|
|
196
|
+
let rest = remaining->Array.slice(~start=1, ~end=Array.length(remaining))
|
|
197
|
+
await tryDirs(rest)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await tryDirs(uniqueDirs)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if package.json exists (validates this is a project root)
|
|
206
|
+
let hasPackageJson = async (projectDir: string): bool => {
|
|
207
|
+
await fileExists(Path.join([projectDir, "package.json"]))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Main detection function
|
|
211
|
+
let detect = async (projectDir: string): result<projectInfo, string> => {
|
|
212
|
+
// First verify this is a project directory
|
|
213
|
+
let hasPackage = await hasPackageJson(projectDir)
|
|
214
|
+
switch hasPackage {
|
|
215
|
+
| false => Error("No package.json found. Please run from your Astro project root.")
|
|
216
|
+
| true =>
|
|
217
|
+
// Detect Astro version
|
|
218
|
+
let astroVersion = await detectAstroVersion(projectDir)
|
|
219
|
+
|
|
220
|
+
switch astroVersion {
|
|
221
|
+
| None =>
|
|
222
|
+
Error(
|
|
223
|
+
"Could not find Astro in node_modules. Please run 'npm install' first or verify this is an Astro project.",
|
|
224
|
+
)
|
|
225
|
+
| Some(_) =>
|
|
226
|
+
// Detect config file
|
|
227
|
+
let configFileName = switch await detectConfigFile(projectDir) {
|
|
228
|
+
| Some(name) => name
|
|
229
|
+
| None => "astro.config.mjs" // Default for creation
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let configPath = Path.join([projectDir, configFileName])
|
|
233
|
+
let config = await analyzeFile(configPath)
|
|
234
|
+
|
|
235
|
+
// Detect middleware file (check src/middleware.ts and src/middleware.js)
|
|
236
|
+
let middlewareTsPath = Path.join([projectDir, "src", "middleware.ts"])
|
|
237
|
+
let middlewareJsPath = Path.join([projectDir, "src", "middleware.js"])
|
|
238
|
+
|
|
239
|
+
let (middleware, middlewareFileName) = switch await fileExists(middlewareTsPath) {
|
|
240
|
+
| true => (await analyzeFile(middlewareTsPath), "src/middleware.ts")
|
|
241
|
+
| false =>
|
|
242
|
+
switch await fileExists(middlewareJsPath) {
|
|
243
|
+
| true => (await analyzeFile(middlewareJsPath), "src/middleware.js")
|
|
244
|
+
| false => (NotFound, "src/middleware.ts") // Default for creation
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Detect package manager
|
|
249
|
+
let packageManager = await detectPackageManager(projectDir)
|
|
250
|
+
|
|
251
|
+
Ok({
|
|
252
|
+
astroVersion,
|
|
253
|
+
config,
|
|
254
|
+
middleware,
|
|
255
|
+
configFileName,
|
|
256
|
+
middlewareFileName,
|
|
257
|
+
packageManager,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Get package manager command
|
|
264
|
+
// Uses npx to ensure the package manager is available even if not in PATH
|
|
265
|
+
let getPackageManagerCommand = (pm: packageManager): string => {
|
|
266
|
+
switch pm {
|
|
267
|
+
| Npm => "npm"
|
|
268
|
+
| Yarn => "npx yarn"
|
|
269
|
+
| Pnpm => "npx pnpm"
|
|
270
|
+
| Bun => "bun"
|
|
271
|
+
| Deno => "deno"
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Get the dev server command for display in success messages
|
|
276
|
+
let getDevCommand = (pm: packageManager): string => {
|
|
277
|
+
switch pm {
|
|
278
|
+
| Npm => "npm run dev"
|
|
279
|
+
| Yarn => "yarn dev"
|
|
280
|
+
| Pnpm => "pnpm dev"
|
|
281
|
+
| Bun => "bun dev"
|
|
282
|
+
| Deno => "deno task dev"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Get install command args for each package manager
|
|
287
|
+
let getInstallArgs = (pm: packageManager, ~isDev: bool=false): array<string> => {
|
|
288
|
+
switch (pm, isDev) {
|
|
289
|
+
| (Npm, true) => ["install", "-D"]
|
|
290
|
+
| (Npm, false) => ["install"]
|
|
291
|
+
| (Yarn, true) => ["add", "-D"]
|
|
292
|
+
| (Yarn, false) => ["add"]
|
|
293
|
+
| (Pnpm, true) => ["add", "--save-dev"]
|
|
294
|
+
| (Pnpm, false) => ["add"]
|
|
295
|
+
| (Bun, true) | (Deno, true) => ["add", "--dev"]
|
|
296
|
+
| (Bun, false) | (Deno, false) => ["add"]
|
|
297
|
+
}
|
|
298
|
+
}
|