@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,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
+ }