@a5gard/bifrost 1.0.1
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 +282 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1118 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk5 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/constants.ts
|
|
8
|
+
var PLATFORMS = {
|
|
9
|
+
"remix": {
|
|
10
|
+
name: "Remix",
|
|
11
|
+
templates: {
|
|
12
|
+
"remix-tutorial": {
|
|
13
|
+
name: "Tutorial",
|
|
14
|
+
repo: "8an3/remixv2/templates/remix-tutorial",
|
|
15
|
+
description: "Great for learning Remix"
|
|
16
|
+
},
|
|
17
|
+
"remix": {
|
|
18
|
+
name: "Default (TypeScript)",
|
|
19
|
+
repo: "8an3/remixv2/templates/remix",
|
|
20
|
+
description: "Standard Remix template"
|
|
21
|
+
},
|
|
22
|
+
"remix-javascript": {
|
|
23
|
+
name: "JavaScript",
|
|
24
|
+
repo: "8an3/remixv2/templates/remix-javascript",
|
|
25
|
+
description: "Remix w/ plain JavaScript"
|
|
26
|
+
},
|
|
27
|
+
"express": {
|
|
28
|
+
name: "Express Server",
|
|
29
|
+
repo: "8an3/remixv2/templates/express",
|
|
30
|
+
description: "Configure w/ Express.js"
|
|
31
|
+
},
|
|
32
|
+
"cloudflare-workers": {
|
|
33
|
+
name: "Cloudflare Workers",
|
|
34
|
+
repo: "8an3/remixv2/templates/cloudflare-workers",
|
|
35
|
+
description: "Optimized Remix app"
|
|
36
|
+
},
|
|
37
|
+
"cloudflare": {
|
|
38
|
+
name: "Cloudflare Pages",
|
|
39
|
+
repo: "8an3/remixv2/templates/cloudflare",
|
|
40
|
+
description: "Optimized for Cloudflare"
|
|
41
|
+
},
|
|
42
|
+
"spa": {
|
|
43
|
+
name: "Single Page App",
|
|
44
|
+
repo: "8an3/remixv2/templates/spa",
|
|
45
|
+
description: "Remix in SPA mode"
|
|
46
|
+
},
|
|
47
|
+
"classic-remix-compiler-remix": {
|
|
48
|
+
name: "Classic Remix Compiler",
|
|
49
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/remix",
|
|
50
|
+
description: "Original Remix compiler setup"
|
|
51
|
+
},
|
|
52
|
+
"classic-remix-compiler-arc": {
|
|
53
|
+
name: "Classic Remix Compiler Arc",
|
|
54
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/arc",
|
|
55
|
+
description: "Original Remix compiler setup"
|
|
56
|
+
},
|
|
57
|
+
"classic-remix-compiler-cloudflare-pages": {
|
|
58
|
+
name: "Classic Remix Compiler Cloudflare Pages",
|
|
59
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/cloudflare-pages",
|
|
60
|
+
description: "Original Remix compiler setup"
|
|
61
|
+
},
|
|
62
|
+
"classic-remix-compiler-cloudflare-workers": {
|
|
63
|
+
name: "Classic Remix Compiler Cloudflare Workers",
|
|
64
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/cloudflare-workers",
|
|
65
|
+
description: "Original Remix compiler setup"
|
|
66
|
+
},
|
|
67
|
+
"classic-remix-compiler-deno": {
|
|
68
|
+
name: "Classic Remix Compiler Deno",
|
|
69
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/deno",
|
|
70
|
+
description: "Original Remix compiler setup"
|
|
71
|
+
},
|
|
72
|
+
"classic-remix-compiler-fly": {
|
|
73
|
+
name: "Classic Remix Compiler Fly",
|
|
74
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/fly",
|
|
75
|
+
description: "Original Remix compiler setup"
|
|
76
|
+
},
|
|
77
|
+
"classic-remix-compiler-remix-javascript": {
|
|
78
|
+
name: "Classic Remix Compiler Remix Javascript",
|
|
79
|
+
repo: "8an3/remixv2/templates/classic-remix-compiler/remix-javascript",
|
|
80
|
+
description: "Original Remix compiler setup"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"cra": {
|
|
85
|
+
name: "Create React App",
|
|
86
|
+
repo: "facebook/create-react-app",
|
|
87
|
+
description: "Standard React application"
|
|
88
|
+
},
|
|
89
|
+
"vite-react": {
|
|
90
|
+
name: "Vite + React",
|
|
91
|
+
repo: "vitejs/vite/packages/create-vite/template-react-ts",
|
|
92
|
+
description: "Fast React development with Vite"
|
|
93
|
+
},
|
|
94
|
+
"nextjs": {
|
|
95
|
+
name: "Next.js",
|
|
96
|
+
repo: "vercel/next.js/examples/blog-starter",
|
|
97
|
+
description: "The React Framework for Production"
|
|
98
|
+
},
|
|
99
|
+
"vue": {
|
|
100
|
+
name: "Vue",
|
|
101
|
+
repo: "vuejs/create-vue",
|
|
102
|
+
description: "Progressive JavaScript Framework"
|
|
103
|
+
},
|
|
104
|
+
"svelte": {
|
|
105
|
+
name: "SvelteKit",
|
|
106
|
+
repo: "sveltejs/kit",
|
|
107
|
+
description: "Cybernetically enhanced web apps"
|
|
108
|
+
},
|
|
109
|
+
"astro": {
|
|
110
|
+
name: "Astro",
|
|
111
|
+
repo: "withastro/astro/examples/basics",
|
|
112
|
+
description: "Build faster websites"
|
|
113
|
+
},
|
|
114
|
+
"solid": {
|
|
115
|
+
name: "SolidStart",
|
|
116
|
+
repo: "solidjs/solid-start",
|
|
117
|
+
description: "Fine-grained reactive JavaScript"
|
|
118
|
+
},
|
|
119
|
+
"qwik": {
|
|
120
|
+
name: "Qwik",
|
|
121
|
+
repo: "BuilderIO/qwik",
|
|
122
|
+
description: "Instant-loading web apps"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
126
|
+
var TEMP_DIR_PREFIX = "bifrost-temp-";
|
|
127
|
+
|
|
128
|
+
// src/prompts.ts
|
|
129
|
+
import prompts from "prompts";
|
|
130
|
+
|
|
131
|
+
// src/utilts.ts
|
|
132
|
+
import fs from "fs-extra";
|
|
133
|
+
import validateNpmPackageName from "validate-npm-package-name";
|
|
134
|
+
function toValidPackageName(name) {
|
|
135
|
+
return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]/, "").replace(/[^a-z0-9-~]+/g, "-");
|
|
136
|
+
}
|
|
137
|
+
function parseStackReference(template) {
|
|
138
|
+
const parts = template.split("/");
|
|
139
|
+
if (parts.length !== 2) {
|
|
140
|
+
throw new Error("Stack must be in format: owner/repo");
|
|
141
|
+
}
|
|
142
|
+
return { owner: parts[0], repo: parts[1] };
|
|
143
|
+
}
|
|
144
|
+
async function directoryExists(dir) {
|
|
145
|
+
try {
|
|
146
|
+
const stats = await fs.stat(dir);
|
|
147
|
+
return stats.isDirectory();
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function isDirectoryEmpty(dir) {
|
|
153
|
+
const files = await fs.readdir(dir);
|
|
154
|
+
return files.length === 0;
|
|
155
|
+
}
|
|
156
|
+
function getPackageManagerCommand(pm, command) {
|
|
157
|
+
const commands = {
|
|
158
|
+
npm: { install: "npm install", run: "npm run" },
|
|
159
|
+
pnpm: { install: "pnpm install", run: "pnpm" },
|
|
160
|
+
yarn: { install: "yarn", run: "yarn" },
|
|
161
|
+
bun: { install: "bun install", run: "bun" }
|
|
162
|
+
};
|
|
163
|
+
return commands[pm][command];
|
|
164
|
+
}
|
|
165
|
+
async function detectPackageManager() {
|
|
166
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
167
|
+
if (!userAgent) return "bun";
|
|
168
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
169
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
170
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
171
|
+
if (userAgent.startsWith("npm")) return "npm";
|
|
172
|
+
return "bun";
|
|
173
|
+
}
|
|
174
|
+
function detectPlatformFromStack(template) {
|
|
175
|
+
const lowerStack = template.toLowerCase();
|
|
176
|
+
if (lowerStack.includes("remix")) return "remix";
|
|
177
|
+
if (lowerStack.includes("next")) return "nextjs";
|
|
178
|
+
if (lowerStack.includes("vite")) return "vite";
|
|
179
|
+
if (lowerStack.includes("vue")) return "vue";
|
|
180
|
+
if (lowerStack.includes("svelte")) return "svelte";
|
|
181
|
+
if (lowerStack.includes("astro")) return "astro";
|
|
182
|
+
if (lowerStack.includes("solid")) return "solid";
|
|
183
|
+
if (lowerStack.includes("qwik")) return "qwik";
|
|
184
|
+
if (lowerStack.includes("react") || lowerStack.includes("cra")) return "react";
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
function detectTagsFromStack(template) {
|
|
188
|
+
const tags = [];
|
|
189
|
+
const lowerStack = template.toLowerCase();
|
|
190
|
+
if (lowerStack.includes("typescript") || lowerStack.includes("-ts")) tags.push("typescript");
|
|
191
|
+
if (lowerStack.includes("javascript") || lowerStack.includes("-js")) tags.push("javascript");
|
|
192
|
+
if (lowerStack.includes("tailwind")) tags.push("tailwind");
|
|
193
|
+
if (lowerStack.includes("prisma")) tags.push("prisma");
|
|
194
|
+
if (lowerStack.includes("postgres")) tags.push("postgresql");
|
|
195
|
+
if (lowerStack.includes("sqlite")) tags.push("sqlite");
|
|
196
|
+
if (lowerStack.includes("mongo")) tags.push("mongodb");
|
|
197
|
+
if (lowerStack.includes("aws")) tags.push("aws");
|
|
198
|
+
if (lowerStack.includes("cloudflare")) tags.push("cloudflare");
|
|
199
|
+
if (lowerStack.includes("vercel")) tags.push("vercel");
|
|
200
|
+
if (lowerStack.includes("react")) tags.push("react");
|
|
201
|
+
return tags;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/prompts.ts
|
|
205
|
+
async function promptForMissingOptions(projectName, template, packageManager, install) {
|
|
206
|
+
const detectedPM = await detectPackageManager();
|
|
207
|
+
const questions = [];
|
|
208
|
+
if (!projectName) {
|
|
209
|
+
questions.push({
|
|
210
|
+
type: "text",
|
|
211
|
+
name: "projectName",
|
|
212
|
+
message: "What would you like to name your new project?",
|
|
213
|
+
initial: "my-bifrost-app",
|
|
214
|
+
validate: (value) => {
|
|
215
|
+
if (!value) return "Project name is required";
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (!template) {
|
|
221
|
+
questions.push({
|
|
222
|
+
type: "select",
|
|
223
|
+
name: "platform",
|
|
224
|
+
message: "Which platform would you like to use?",
|
|
225
|
+
choices: Object.entries(PLATFORMS).map(([key, platform]) => ({
|
|
226
|
+
title: platform.name,
|
|
227
|
+
value: key,
|
|
228
|
+
description: platform.description || ""
|
|
229
|
+
})),
|
|
230
|
+
initial: 0
|
|
231
|
+
});
|
|
232
|
+
questions.push({
|
|
233
|
+
type: (prev) => {
|
|
234
|
+
const platform = PLATFORMS[prev];
|
|
235
|
+
return platform.templates ? "select" : null;
|
|
236
|
+
},
|
|
237
|
+
name: "template",
|
|
238
|
+
message: "Select a template:",
|
|
239
|
+
choices: (prev) => {
|
|
240
|
+
const platform = PLATFORMS[prev];
|
|
241
|
+
if (!platform.templates) return [];
|
|
242
|
+
return Object.entries(platform.templates).map(([key, template2]) => ({
|
|
243
|
+
title: template2.name,
|
|
244
|
+
value: template2.repo,
|
|
245
|
+
description: template2.description
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
questions.push({
|
|
250
|
+
type: (prev, values) => {
|
|
251
|
+
const platformKey = values.platform;
|
|
252
|
+
return platformKey === "custom" ? "text" : null;
|
|
253
|
+
},
|
|
254
|
+
name: "customStack",
|
|
255
|
+
message: "Enter template (owner/repo):",
|
|
256
|
+
validate: (value) => {
|
|
257
|
+
if (!value || !value.includes("/")) {
|
|
258
|
+
return "Stack must be in format: owner/repo";
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (!packageManager) {
|
|
265
|
+
questions.push({
|
|
266
|
+
type: "select",
|
|
267
|
+
name: "packageManager",
|
|
268
|
+
message: "Which package manager do you prefer?",
|
|
269
|
+
choices: PACKAGE_MANAGERS.map((pm) => ({
|
|
270
|
+
title: pm,
|
|
271
|
+
value: pm,
|
|
272
|
+
selected: pm === detectedPM
|
|
273
|
+
})),
|
|
274
|
+
initial: PACKAGE_MANAGERS.indexOf(detectedPM)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
if (install === void 0) {
|
|
278
|
+
questions.push({
|
|
279
|
+
type: "confirm",
|
|
280
|
+
name: "install",
|
|
281
|
+
message: "Would you like to have the install command run once the project has initialized?",
|
|
282
|
+
initial: true
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
questions.push({
|
|
286
|
+
type: "confirm",
|
|
287
|
+
name: "gitPush",
|
|
288
|
+
message: "Would you like to auto create and push the first commit to GitHub?",
|
|
289
|
+
initial: false
|
|
290
|
+
});
|
|
291
|
+
questions.push({
|
|
292
|
+
type: "confirm",
|
|
293
|
+
name: "runWizard",
|
|
294
|
+
message: "Would you like to run the config.bifrost wizard?",
|
|
295
|
+
initial: false
|
|
296
|
+
});
|
|
297
|
+
questions.push({
|
|
298
|
+
type: "confirm",
|
|
299
|
+
name: "submitToRegistry",
|
|
300
|
+
message: "Would you like to submit your template to the bifrost registry?",
|
|
301
|
+
initial: false
|
|
302
|
+
});
|
|
303
|
+
const answers = await prompts(questions, {
|
|
304
|
+
onCancel: () => {
|
|
305
|
+
console.log("\nOperation cancelled");
|
|
306
|
+
process.exit(0);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
let finalStack = template;
|
|
310
|
+
if (!template) {
|
|
311
|
+
if (answers.platform === "custom") {
|
|
312
|
+
finalStack = answers.customStack;
|
|
313
|
+
} else if (answers.template) {
|
|
314
|
+
finalStack = answers.template;
|
|
315
|
+
} else {
|
|
316
|
+
const platform = PLATFORMS[answers.platform];
|
|
317
|
+
if (platform.repo) {
|
|
318
|
+
finalStack = platform.repo;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
projectName: projectName || answers.projectName,
|
|
324
|
+
template: finalStack,
|
|
325
|
+
packageManager: packageManager || answers.packageManager,
|
|
326
|
+
install: install !== void 0 ? install : answers.install,
|
|
327
|
+
gitPush: answers.gitPush,
|
|
328
|
+
runWizard: answers.runWizard,
|
|
329
|
+
submitToRegistry: answers.submitToRegistry
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/creator.ts
|
|
334
|
+
import fs5 from "fs-extra";
|
|
335
|
+
import path4 from "path";
|
|
336
|
+
import chalk2 from "chalk";
|
|
337
|
+
import ora2 from "ora";
|
|
338
|
+
|
|
339
|
+
// src/git.ts
|
|
340
|
+
import { execa } from "execa";
|
|
341
|
+
import fs2 from "fs-extra";
|
|
342
|
+
import path from "path";
|
|
343
|
+
import os from "os";
|
|
344
|
+
async function isGitInstalled() {
|
|
345
|
+
try {
|
|
346
|
+
await execa("git", ["--version"]);
|
|
347
|
+
return true;
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function cloneRepository(owner, repo, targetDir) {
|
|
353
|
+
const gitInstalled = await isGitInstalled();
|
|
354
|
+
if (!gitInstalled) {
|
|
355
|
+
throw new Error("Git is not installed. Please install Git and try again.");
|
|
356
|
+
}
|
|
357
|
+
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
358
|
+
const tempDir = path.join(os.tmpdir(), `${TEMP_DIR_PREFIX}${Date.now()}`);
|
|
359
|
+
try {
|
|
360
|
+
await execa("git", ["clone", "--depth", "1", repoUrl, tempDir]);
|
|
361
|
+
await fs2.remove(path.join(tempDir, ".git"));
|
|
362
|
+
await fs2.copy(tempDir, targetDir, { overwrite: true });
|
|
363
|
+
await fs2.remove(tempDir);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
await fs2.remove(tempDir).catch(() => {
|
|
366
|
+
});
|
|
367
|
+
if (error instanceof Error) {
|
|
368
|
+
if (error.message.includes("not found") || error.message.includes("not exist")) {
|
|
369
|
+
throw new Error(`Repository ${owner}/${repo} not found or inaccessible`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function initializeGitRepo(projectDir) {
|
|
376
|
+
try {
|
|
377
|
+
await execa("git", ["init"], { cwd: projectDir });
|
|
378
|
+
await execa("git", ["add", "."], { cwd: projectDir });
|
|
379
|
+
await execa("git", ["commit", "-m", "Initial commit from create-bifrost"], { cwd: projectDir });
|
|
380
|
+
} catch {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function pushToGitHub(projectDir) {
|
|
385
|
+
try {
|
|
386
|
+
const { stdout: remoteUrl } = await execa("git", ["remote", "get-url", "origin"], { cwd: projectDir });
|
|
387
|
+
if (!remoteUrl) {
|
|
388
|
+
throw new Error("No remote origin found");
|
|
389
|
+
}
|
|
390
|
+
await execa("git", ["push", "-u", "origin", "main"], { cwd: projectDir });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const mainExists = await execa("git", ["rev-parse", "--verify", "main"], { cwd: projectDir, reject: false });
|
|
393
|
+
const masterExists = await execa("git", ["rev-parse", "--verify", "master"], { cwd: projectDir, reject: false });
|
|
394
|
+
const branch = mainExists.exitCode === 0 ? "main" : masterExists.exitCode === 0 ? "master" : "main";
|
|
395
|
+
try {
|
|
396
|
+
await execa("git", ["push", "-u", "origin", branch], { cwd: projectDir });
|
|
397
|
+
} catch {
|
|
398
|
+
throw new Error("Failed to push to GitHub. Ensure you have a remote repository set up and proper permissions.");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/install.ts
|
|
404
|
+
import { execa as execa2 } from "execa";
|
|
405
|
+
async function installDependencies(projectDir, packageManager) {
|
|
406
|
+
const installCommand = getPackageManagerCommand(packageManager, "install");
|
|
407
|
+
const [cmd, ...args] = installCommand.split(" ");
|
|
408
|
+
await execa2(cmd, args, {
|
|
409
|
+
cwd: projectDir,
|
|
410
|
+
stdio: "inherit"
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
async function runPostInstallScripts(projectDir, packageManager, scripts) {
|
|
414
|
+
for (const script of scripts) {
|
|
415
|
+
const runCommand = getPackageManagerCommand(packageManager, "run");
|
|
416
|
+
const [cmd, ...baseArgs] = runCommand.split(" ");
|
|
417
|
+
const args = [...baseArgs, script];
|
|
418
|
+
try {
|
|
419
|
+
await execa2(cmd, args, {
|
|
420
|
+
cwd: projectDir,
|
|
421
|
+
stdio: "inherit"
|
|
422
|
+
});
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.warn(`Warning: Post-install script "${script}" failed`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/plugin.ts
|
|
430
|
+
import fs3 from "fs-extra";
|
|
431
|
+
import path2 from "path";
|
|
432
|
+
import os2 from "os";
|
|
433
|
+
import prompts2 from "prompts";
|
|
434
|
+
import chalk from "chalk";
|
|
435
|
+
import ora from "ora";
|
|
436
|
+
import { execa as execa3 } from "execa";
|
|
437
|
+
async function fetchPluginConfig(owner, repo) {
|
|
438
|
+
const tempDir = path2.join(os2.tmpdir(), `${TEMP_DIR_PREFIX}plugin-${Date.now()}`);
|
|
439
|
+
try {
|
|
440
|
+
await cloneRepository(owner, repo, tempDir);
|
|
441
|
+
const configPath = path2.join(tempDir, "plugin.bifrost");
|
|
442
|
+
if (!await fs3.pathExists(configPath)) {
|
|
443
|
+
throw new Error(`Plugin ${owner}/${repo} is missing plugin.bifrost configuration file`);
|
|
444
|
+
}
|
|
445
|
+
const config = await fs3.readJson(configPath);
|
|
446
|
+
await fs3.remove(tempDir);
|
|
447
|
+
return config;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
await fs3.remove(tempDir).catch(() => {
|
|
450
|
+
});
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function installPluginLibraries(projectDir, packageManager, libraries) {
|
|
455
|
+
if (libraries.length === 0) return;
|
|
456
|
+
const installCommand = getPackageManagerCommand(packageManager, "install");
|
|
457
|
+
const [cmd, ...baseArgs] = installCommand.split(" ");
|
|
458
|
+
const args = [...baseArgs, ...libraries];
|
|
459
|
+
await execa3(cmd, args, {
|
|
460
|
+
cwd: projectDir,
|
|
461
|
+
stdio: "inherit"
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
async function promptForFileLocation(fileName, suggestedLocation) {
|
|
465
|
+
const response = await prompts2({
|
|
466
|
+
type: "text",
|
|
467
|
+
name: "location",
|
|
468
|
+
message: `Location for ${chalk.cyan(fileName)}:`,
|
|
469
|
+
initial: suggestedLocation,
|
|
470
|
+
validate: (value) => {
|
|
471
|
+
if (!value) return "Location is required";
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
if (!response.location) {
|
|
476
|
+
throw new Error("Operation cancelled");
|
|
477
|
+
}
|
|
478
|
+
return response.location;
|
|
479
|
+
}
|
|
480
|
+
async function copyPluginFiles(projectDir, pluginTempDir, files) {
|
|
481
|
+
for (const file of files) {
|
|
482
|
+
const confirmedLocation = await promptForFileLocation(file.name, file.location);
|
|
483
|
+
const sourcePath = path2.join(pluginTempDir, "files", file.name);
|
|
484
|
+
const destPath = path2.join(projectDir, confirmedLocation);
|
|
485
|
+
if (!await fs3.pathExists(sourcePath)) {
|
|
486
|
+
console.warn(chalk.yellow(`Warning: File ${file.name} not found in plugin, skipping...`));
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
await fs3.ensureDir(path2.dirname(destPath));
|
|
490
|
+
await fs3.copy(sourcePath, destPath);
|
|
491
|
+
console.log(chalk.green(`\u2713 Copied ${file.name} to ${confirmedLocation}`));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function installPlugin(projectDir, packageManager, pluginReference) {
|
|
495
|
+
const { owner, repo } = parseStackReference(pluginReference);
|
|
496
|
+
console.log();
|
|
497
|
+
console.log(chalk.bold(`Installing plugin: ${chalk.cyan(pluginReference)}`));
|
|
498
|
+
console.log();
|
|
499
|
+
const configSpinner = ora("Fetching plugin configuration...").start();
|
|
500
|
+
let pluginConfig;
|
|
501
|
+
try {
|
|
502
|
+
pluginConfig = await fetchPluginConfig(owner, repo);
|
|
503
|
+
configSpinner.succeed(`Fetched configuration for ${chalk.cyan(pluginConfig.name)}`);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
configSpinner.fail("Failed to fetch plugin configuration");
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
console.log(chalk.gray(`Description: ${pluginConfig.description}`));
|
|
509
|
+
console.log(chalk.gray(`Platform: ${pluginConfig.platform}`));
|
|
510
|
+
console.log(chalk.gray(`Tags: ${pluginConfig.tags.join(", ")}`));
|
|
511
|
+
console.log();
|
|
512
|
+
if (pluginConfig.libraries && pluginConfig.libraries.length > 0) {
|
|
513
|
+
const libSpinner = ora("Installing plugin libraries...").start();
|
|
514
|
+
try {
|
|
515
|
+
await installPluginLibraries(projectDir, packageManager, pluginConfig.libraries);
|
|
516
|
+
libSpinner.succeed("Installed plugin libraries");
|
|
517
|
+
} catch (error) {
|
|
518
|
+
libSpinner.fail("Failed to install plugin libraries");
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (pluginConfig.files && pluginConfig.files.length > 0) {
|
|
523
|
+
console.log();
|
|
524
|
+
console.log(chalk.bold("Plugin files:"));
|
|
525
|
+
console.log();
|
|
526
|
+
const tempDir = path2.join(os2.tmpdir(), `${TEMP_DIR_PREFIX}plugin-${Date.now()}`);
|
|
527
|
+
try {
|
|
528
|
+
await cloneRepository(owner, repo, tempDir);
|
|
529
|
+
await copyPluginFiles(projectDir, tempDir, pluginConfig.files);
|
|
530
|
+
await fs3.remove(tempDir);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
await fs3.remove(tempDir).catch(() => {
|
|
533
|
+
});
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
console.log();
|
|
538
|
+
console.log(chalk.bold.green(`\u2713 Plugin ${pluginConfig.name} installed successfully!`));
|
|
539
|
+
console.log();
|
|
540
|
+
}
|
|
541
|
+
async function installPlugins(projectDir, packageManager, plugins) {
|
|
542
|
+
if (plugins.length === 0) return;
|
|
543
|
+
console.log();
|
|
544
|
+
console.log(chalk.bold(`Found ${plugins.length} plugin(s) to install`));
|
|
545
|
+
for (const plugin of plugins) {
|
|
546
|
+
try {
|
|
547
|
+
await installPlugin(projectDir, packageManager, plugin);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error(chalk.red(`Failed to install plugin ${plugin}:`), error instanceof Error ? error.message : "Unknown error");
|
|
550
|
+
console.log();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/packge-json.ts
|
|
556
|
+
import fs4 from "fs-extra";
|
|
557
|
+
import path3 from "path";
|
|
558
|
+
async function updatePackageJson(projectDir, projectName) {
|
|
559
|
+
const packageJsonPath = path3.join(projectDir, "package.json");
|
|
560
|
+
if (!await fs4.pathExists(packageJsonPath)) {
|
|
561
|
+
const defaultPackageJson = {
|
|
562
|
+
name: projectName,
|
|
563
|
+
version: "0.0.1",
|
|
564
|
+
private: true
|
|
565
|
+
};
|
|
566
|
+
await fs4.writeJson(packageJsonPath, defaultPackageJson, { spaces: 2 });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const packageJson = await fs4.readJson(packageJsonPath);
|
|
570
|
+
packageJson.name = projectName;
|
|
571
|
+
await fs4.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
572
|
+
}
|
|
573
|
+
async function readStackConfig(projectDir) {
|
|
574
|
+
const configPath = path3.join(projectDir, "config.bifrost");
|
|
575
|
+
if (!await fs4.pathExists(configPath)) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return await fs4.readJson(configPath);
|
|
579
|
+
}
|
|
580
|
+
async function createBifrostConfig(projectDir, projectName, template, platform, tags, existingConfig) {
|
|
581
|
+
const configPath = path3.join(projectDir, "config.bifrost");
|
|
582
|
+
if (await fs4.pathExists(configPath)) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const config = {
|
|
586
|
+
name: projectName,
|
|
587
|
+
description: existingConfig?.description || "",
|
|
588
|
+
platform: platform || existingConfig?.platform || "unknown",
|
|
589
|
+
github: template,
|
|
590
|
+
tags: tags || existingConfig?.tags || [],
|
|
591
|
+
postInstall: existingConfig?.postInstall || [],
|
|
592
|
+
plugins: existingConfig?.plugins || []
|
|
593
|
+
};
|
|
594
|
+
await fs4.writeJson(configPath, config, { spaces: 2 });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/creator.ts
|
|
598
|
+
async function createProject(context) {
|
|
599
|
+
const { projectName, template, packageManager, install, gitPush } = context;
|
|
600
|
+
const absolutePath = path4.resolve(projectName);
|
|
601
|
+
console.log();
|
|
602
|
+
console.log(chalk2.bold("Creating your Bifrost project..."));
|
|
603
|
+
console.log();
|
|
604
|
+
if (await directoryExists(absolutePath)) {
|
|
605
|
+
const isEmpty = await isDirectoryEmpty(absolutePath);
|
|
606
|
+
if (!isEmpty) {
|
|
607
|
+
throw new Error(`Directory ${projectName} already exists and is not empty`);
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
await fs5.ensureDir(absolutePath);
|
|
611
|
+
}
|
|
612
|
+
const { owner, repo } = parseStackReference(template);
|
|
613
|
+
const cloneSpinner = ora2(`Cloning ${chalk2.cyan(template)}...`).start();
|
|
614
|
+
try {
|
|
615
|
+
await cloneRepository(owner, repo, absolutePath);
|
|
616
|
+
cloneSpinner.succeed(`Cloned ${chalk2.cyan(template)}`);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
cloneSpinner.fail(`Failed to clone ${chalk2.cyan(template)}`);
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
const updateSpinner = ora2("Updating package.json...").start();
|
|
622
|
+
try {
|
|
623
|
+
await updatePackageJson(absolutePath, projectName);
|
|
624
|
+
updateSpinner.succeed("Updated package.json");
|
|
625
|
+
} catch (error) {
|
|
626
|
+
updateSpinner.fail("Failed to update package.json");
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
const stackConfig = await readStackConfig(absolutePath);
|
|
630
|
+
if (install) {
|
|
631
|
+
const installSpinner = ora2(`Installing dependencies with ${chalk2.cyan(packageManager)}...`).start();
|
|
632
|
+
try {
|
|
633
|
+
await installDependencies(absolutePath, packageManager);
|
|
634
|
+
installSpinner.succeed(`Installed dependencies with ${chalk2.cyan(packageManager)}`);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
installSpinner.fail("Failed to install dependencies");
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
if (stackConfig?.postInstall && Array.isArray(stackConfig.postInstall)) {
|
|
640
|
+
const postInstallSpinner = ora2("Running post-install scripts...").start();
|
|
641
|
+
try {
|
|
642
|
+
await runPostInstallScripts(absolutePath, packageManager, stackConfig.postInstall);
|
|
643
|
+
postInstallSpinner.succeed("Completed post-install scripts");
|
|
644
|
+
} catch (error) {
|
|
645
|
+
postInstallSpinner.warn("Some post-install scripts failed");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (stackConfig?.plugins && Array.isArray(stackConfig.plugins) && stackConfig.plugins.length > 0) {
|
|
649
|
+
try {
|
|
650
|
+
await installPlugins(absolutePath, packageManager, stackConfig.plugins);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error(chalk2.red("Some plugins failed to install"));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const gitSpinner = ora2("Initializing git repository...").start();
|
|
657
|
+
try {
|
|
658
|
+
await initializeGitRepo(absolutePath);
|
|
659
|
+
gitSpinner.succeed("Initialized git repository");
|
|
660
|
+
} catch {
|
|
661
|
+
gitSpinner.info("Skipped git initialization");
|
|
662
|
+
}
|
|
663
|
+
const configSpinner = ora2("Creating config.bifrost...").start();
|
|
664
|
+
try {
|
|
665
|
+
const platform = detectPlatformFromStack(template);
|
|
666
|
+
const tags = detectTagsFromStack(template);
|
|
667
|
+
await createBifrostConfig(absolutePath, projectName, template, platform, tags, stackConfig);
|
|
668
|
+
configSpinner.succeed("Created config.bifrost");
|
|
669
|
+
} catch (error) {
|
|
670
|
+
configSpinner.warn("Failed to create config.bifrost");
|
|
671
|
+
}
|
|
672
|
+
if (gitPush) {
|
|
673
|
+
const pushSpinner = ora2("Pushing to GitHub...").start();
|
|
674
|
+
try {
|
|
675
|
+
await pushToGitHub(absolutePath);
|
|
676
|
+
pushSpinner.succeed("Pushed to GitHub");
|
|
677
|
+
} catch (error) {
|
|
678
|
+
pushSpinner.warn("Failed to push to GitHub - you may need to set up a remote repository first");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
console.log();
|
|
682
|
+
console.log(chalk2.bold.green("\u2713 Project created successfully!"));
|
|
683
|
+
console.log();
|
|
684
|
+
console.log(chalk2.bold("Next steps:"));
|
|
685
|
+
console.log();
|
|
686
|
+
console.log(` ${chalk2.cyan("cd")} ${projectName}`);
|
|
687
|
+
if (!install) {
|
|
688
|
+
console.log(` ${chalk2.cyan(`${packageManager} install`)}`);
|
|
689
|
+
}
|
|
690
|
+
console.log(` ${chalk2.cyan(`${packageManager} ${packageManager === "npm" ? "run " : ""}dev`)}`);
|
|
691
|
+
console.log();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/wizard.ts
|
|
695
|
+
import fs6 from "fs-extra";
|
|
696
|
+
import path5 from "path";
|
|
697
|
+
import chalk3 from "chalk";
|
|
698
|
+
import prompts3 from "prompts";
|
|
699
|
+
import { execSync } from "child_process";
|
|
700
|
+
async function detectGitHubRepo() {
|
|
701
|
+
try {
|
|
702
|
+
const remote = execSync("git config --get remote.origin.url", { encoding: "utf-8" }).trim();
|
|
703
|
+
const match = remote.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
704
|
+
if (match) {
|
|
705
|
+
return match[1];
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const packageJsonPath = path5.join(process.cwd(), "package.json");
|
|
711
|
+
if (await fs6.pathExists(packageJsonPath)) {
|
|
712
|
+
const packageJson = await fs6.readJson(packageJsonPath);
|
|
713
|
+
if (packageJson.repository) {
|
|
714
|
+
const repoUrl = typeof packageJson.repository === "string" ? packageJson.repository : packageJson.repository.url;
|
|
715
|
+
const match = repoUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
716
|
+
if (match) {
|
|
717
|
+
return match[1];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
async function promptForGitHubRepo() {
|
|
726
|
+
console.log(chalk3.yellow("\n\u26A0 No GitHub repository detected"));
|
|
727
|
+
console.log(chalk3.gray("Please push your project and create a public repository\n"));
|
|
728
|
+
const { hasRepo } = await prompts3({
|
|
729
|
+
type: "confirm",
|
|
730
|
+
name: "hasRepo",
|
|
731
|
+
message: "Have you created a public GitHub repository?",
|
|
732
|
+
initial: false
|
|
733
|
+
});
|
|
734
|
+
if (!hasRepo) {
|
|
735
|
+
console.log(chalk3.red("\nPlease create a public GitHub repository first"));
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
const { repo } = await prompts3({
|
|
739
|
+
type: "text",
|
|
740
|
+
name: "repo",
|
|
741
|
+
message: "Enter your GitHub repository (owner/repo):",
|
|
742
|
+
validate: (value) => {
|
|
743
|
+
const pattern = /^[\w-]+\/[\w-]+$/;
|
|
744
|
+
return pattern.test(value) || "Invalid format. Use: owner/repo";
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
if (!repo) {
|
|
748
|
+
console.log(chalk3.red("\nRepository is required"));
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
return repo;
|
|
752
|
+
}
|
|
753
|
+
async function runConfigWizard() {
|
|
754
|
+
console.log(chalk3.blue.bold("\n\u{1F9D9} Config.bifrost Wizard\n"));
|
|
755
|
+
const configPath = path5.join(process.cwd(), "config.bifrost");
|
|
756
|
+
if (await fs6.pathExists(configPath)) {
|
|
757
|
+
const { overwrite } = await prompts3({
|
|
758
|
+
type: "confirm",
|
|
759
|
+
name: "overwrite",
|
|
760
|
+
message: "config.bifrost already exists. Overwrite?",
|
|
761
|
+
initial: false
|
|
762
|
+
});
|
|
763
|
+
if (!overwrite) {
|
|
764
|
+
const existingConfig = await fs6.readJson(configPath);
|
|
765
|
+
return existingConfig;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const detectedRepo = await detectGitHubRepo();
|
|
769
|
+
const responses = await prompts3([
|
|
770
|
+
{
|
|
771
|
+
type: "text",
|
|
772
|
+
name: "name",
|
|
773
|
+
message: "Template name:",
|
|
774
|
+
validate: (value) => value.trim().length > 0 || "Name is required"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
type: "text",
|
|
778
|
+
name: "description",
|
|
779
|
+
message: "Description:",
|
|
780
|
+
validate: (value) => value.trim().length > 0 || "Description is required"
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
type: "text",
|
|
784
|
+
name: "platform",
|
|
785
|
+
message: "Platform:",
|
|
786
|
+
initial: "remix",
|
|
787
|
+
validate: (value) => value.trim().length > 0 || "Platform is required"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
type: "text",
|
|
791
|
+
name: "github",
|
|
792
|
+
message: "GitHub repository (owner/repo):",
|
|
793
|
+
initial: detectedRepo || "",
|
|
794
|
+
validate: (value) => {
|
|
795
|
+
const pattern = /^[\w-]+\/[\w-]+$/;
|
|
796
|
+
return pattern.test(value) || "Invalid format. Use: owner/repo";
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
type: "text",
|
|
801
|
+
name: "tags",
|
|
802
|
+
message: "Tags (comma-separated):",
|
|
803
|
+
validate: (value) => value.trim().length > 0 || "At least one tag is required"
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
type: "text",
|
|
807
|
+
name: "postInstall",
|
|
808
|
+
message: "Post-install scripts (comma-separated npm script names):",
|
|
809
|
+
initial: ""
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
type: "text",
|
|
813
|
+
name: "plugins",
|
|
814
|
+
message: "Plugins to include (comma-separated owner/repo):",
|
|
815
|
+
initial: ""
|
|
816
|
+
}
|
|
817
|
+
]);
|
|
818
|
+
if (!responses.name) {
|
|
819
|
+
console.log(chalk3.red("\nWizard cancelled"));
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
if (!detectedRepo && !responses.github) {
|
|
823
|
+
responses.github = await promptForGitHubRepo();
|
|
824
|
+
}
|
|
825
|
+
const config = {
|
|
826
|
+
name: responses.name,
|
|
827
|
+
description: responses.description,
|
|
828
|
+
platform: responses.platform,
|
|
829
|
+
github: responses.github,
|
|
830
|
+
tags: responses.tags.split(",").map((t) => t.trim()).filter(Boolean),
|
|
831
|
+
postInstall: responses.postInstall ? responses.postInstall.split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
832
|
+
plugins: responses.plugins ? responses.plugins.split(",").map((p) => p.trim()).filter(Boolean) : []
|
|
833
|
+
};
|
|
834
|
+
await fs6.writeJson(configPath, config, { spaces: 2 });
|
|
835
|
+
console.log(chalk3.green("\n\u2705 config.bifrost created successfully!\n"));
|
|
836
|
+
console.log(chalk3.cyan("Configuration:"));
|
|
837
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
838
|
+
console.log(chalk3.white(JSON.stringify(config, null, 2)));
|
|
839
|
+
console.log(chalk3.gray("\u2500".repeat(50)));
|
|
840
|
+
return config;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/templateSubmitter.ts
|
|
844
|
+
import fs7 from "fs-extra";
|
|
845
|
+
import path6 from "path";
|
|
846
|
+
import chalk4 from "chalk";
|
|
847
|
+
import prompts4 from "prompts";
|
|
848
|
+
import { execSync as execSync2 } from "child_process";
|
|
849
|
+
var REGISTRY_REPO = "A5GARD/BIFROST";
|
|
850
|
+
var REGISTRY_FILE = "dist/registry.bifrost";
|
|
851
|
+
async function verifyPublicRepo(github) {
|
|
852
|
+
try {
|
|
853
|
+
const response = await fetch(`https://api.github.com/repos/${github}`);
|
|
854
|
+
if (!response.ok) return false;
|
|
855
|
+
const data = await response.json();
|
|
856
|
+
return !data.private;
|
|
857
|
+
} catch {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function submitTemplate() {
|
|
862
|
+
console.log(chalk4.blue.bold("\n\u{1F4E4} Submit Template to Registry\n"));
|
|
863
|
+
const configPath = path6.join(process.cwd(), "config.bifrost");
|
|
864
|
+
let config;
|
|
865
|
+
if (!await fs7.pathExists(configPath)) {
|
|
866
|
+
console.log(chalk4.yellow("\u26A0 config.bifrost not found\n"));
|
|
867
|
+
const { runWizard } = await prompts4({
|
|
868
|
+
type: "confirm",
|
|
869
|
+
name: "runWizard",
|
|
870
|
+
message: "Would you like to run the config wizard to create it?",
|
|
871
|
+
initial: true
|
|
872
|
+
});
|
|
873
|
+
if (!runWizard) {
|
|
874
|
+
console.log(chalk4.red("\nconfig.bifrost is required for submission"));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
config = await runConfigWizard();
|
|
878
|
+
} else {
|
|
879
|
+
config = await fs7.readJson(configPath);
|
|
880
|
+
}
|
|
881
|
+
console.log(chalk4.blue("\n\u{1F50D} Verifying repository..."));
|
|
882
|
+
const isPublic = await verifyPublicRepo(config.github);
|
|
883
|
+
if (!isPublic) {
|
|
884
|
+
console.log(chalk4.red("\n\u274C Repository must be public"));
|
|
885
|
+
console.log(chalk4.yellow("Please make your repository public before submitting"));
|
|
886
|
+
const { madePublic } = await prompts4({
|
|
887
|
+
type: "confirm",
|
|
888
|
+
name: "madePublic",
|
|
889
|
+
message: "Have you made the repository public?",
|
|
890
|
+
initial: false
|
|
891
|
+
});
|
|
892
|
+
if (!madePublic) {
|
|
893
|
+
console.log(chalk4.red("\nSubmission cancelled"));
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
const stillNotPublic = await verifyPublicRepo(config.github);
|
|
897
|
+
if (!stillNotPublic) {
|
|
898
|
+
console.log(chalk4.red("\n\u274C Repository is still not public"));
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
console.log(chalk4.cyan("\nTemplate Information:"));
|
|
903
|
+
console.log(chalk4.gray("\u2500".repeat(50)));
|
|
904
|
+
console.log(`Name: ${chalk4.white(config.name)}`);
|
|
905
|
+
console.log(`Description: ${chalk4.white(config.description)}`);
|
|
906
|
+
console.log(`Platform: ${chalk4.white(config.platform)}`);
|
|
907
|
+
console.log(`GitHub: ${chalk4.white(config.github)}`);
|
|
908
|
+
console.log(`Tags: ${chalk4.white(config.tags.join(", "))}`);
|
|
909
|
+
if (config.postInstall.length > 0) {
|
|
910
|
+
console.log(`Post-Install: ${chalk4.white(config.postInstall.join(", "))}`);
|
|
911
|
+
}
|
|
912
|
+
if (config.plugins.length > 0) {
|
|
913
|
+
console.log(`Plugins: ${chalk4.white(config.plugins.join(", "))}`);
|
|
914
|
+
}
|
|
915
|
+
console.log(chalk4.gray("\u2500".repeat(50)));
|
|
916
|
+
const { confirm } = await prompts4({
|
|
917
|
+
type: "confirm",
|
|
918
|
+
name: "confirm",
|
|
919
|
+
message: "Submit this template to the registry?",
|
|
920
|
+
initial: true
|
|
921
|
+
});
|
|
922
|
+
if (!confirm) {
|
|
923
|
+
console.log(chalk4.yellow("\nSubmission cancelled"));
|
|
924
|
+
process.exit(0);
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
const [owner, repo] = config.github.split("/");
|
|
928
|
+
const registryEntry = {
|
|
929
|
+
owner,
|
|
930
|
+
repo,
|
|
931
|
+
description: config.description,
|
|
932
|
+
platform: config.platform,
|
|
933
|
+
tags: config.tags
|
|
934
|
+
};
|
|
935
|
+
console.log(chalk4.blue("\n\u{1F504} Forking registry repository..."));
|
|
936
|
+
execSync2(`gh repo fork ${REGISTRY_REPO} --clone=false`, { stdio: "inherit" });
|
|
937
|
+
const username = execSync2("gh api user -q .login", { encoding: "utf-8" }).trim();
|
|
938
|
+
const forkRepo = `${username}/BIFROST`;
|
|
939
|
+
console.log(chalk4.blue("\u{1F4E5} Cloning forked repository..."));
|
|
940
|
+
const tempDir = path6.join(process.cwd(), ".bifrost-temp");
|
|
941
|
+
await fs7.ensureDir(tempDir);
|
|
942
|
+
execSync2(`gh repo clone ${forkRepo} ${tempDir}`, { stdio: "inherit" });
|
|
943
|
+
console.log(chalk4.blue("\u{1F4CB} Fetching current registry..."));
|
|
944
|
+
const registryUrl = `https://raw.githubusercontent.com/${REGISTRY_REPO}/main/${REGISTRY_FILE}`;
|
|
945
|
+
const registryResponse = await fetch(registryUrl);
|
|
946
|
+
let registry = [];
|
|
947
|
+
if (registryResponse.ok) {
|
|
948
|
+
registry = await registryResponse.json();
|
|
949
|
+
}
|
|
950
|
+
const registryPath = path6.join(tempDir, REGISTRY_FILE);
|
|
951
|
+
await fs7.ensureDir(path6.dirname(registryPath));
|
|
952
|
+
const existingIndex = registry.findIndex((t) => t.owner === owner && t.repo === repo);
|
|
953
|
+
if (existingIndex !== -1) {
|
|
954
|
+
console.log(chalk4.yellow("\n\u26A0 Template already exists in registry. Updating..."));
|
|
955
|
+
registry[existingIndex] = registryEntry;
|
|
956
|
+
} else {
|
|
957
|
+
registry.push(registryEntry);
|
|
958
|
+
}
|
|
959
|
+
await fs7.writeJson(registryPath, registry, { spaces: 2 });
|
|
960
|
+
console.log(chalk4.blue("\u{1F4BE} Committing changes..."));
|
|
961
|
+
process.chdir(tempDir);
|
|
962
|
+
execSync2("git add .", { stdio: "inherit" });
|
|
963
|
+
execSync2(`git commit -m "Add/Update template: ${config.name}"`, { stdio: "inherit" });
|
|
964
|
+
execSync2("git push", { stdio: "inherit" });
|
|
965
|
+
console.log(chalk4.blue("\u{1F500} Creating pull request..."));
|
|
966
|
+
const prUrl = execSync2(
|
|
967
|
+
`gh pr create --repo ${REGISTRY_REPO} --title "Add template: ${config.name}" --body "Submitting template ${config.name} to the registry.
|
|
968
|
+
|
|
969
|
+
Platform: ${config.platform}
|
|
970
|
+
Description: ${config.description}"`,
|
|
971
|
+
{ encoding: "utf-8" }
|
|
972
|
+
).trim();
|
|
973
|
+
process.chdir("..");
|
|
974
|
+
await fs7.remove(tempDir);
|
|
975
|
+
console.log(chalk4.green.bold("\n\u2728 Template submitted successfully!\n"));
|
|
976
|
+
console.log(chalk4.cyan("Pull Request:"), chalk4.white(prUrl));
|
|
977
|
+
console.log(chalk4.gray("\nYour template will be available once the PR is merged."));
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (error instanceof Error && error.message.includes("gh: command not found")) {
|
|
980
|
+
console.log(chalk4.red("\n\u274C GitHub CLI (gh) is not installed"));
|
|
981
|
+
console.log(chalk4.yellow("\nManual submission steps:"));
|
|
982
|
+
console.log(chalk4.gray(`1. Fork the repository: https://github.com/${REGISTRY_REPO}`));
|
|
983
|
+
console.log(chalk4.gray(`2. Clone your fork`));
|
|
984
|
+
console.log(chalk4.gray(`3. Add your template to ${REGISTRY_FILE}`));
|
|
985
|
+
console.log(chalk4.gray(`4. Commit and push changes`));
|
|
986
|
+
console.log(chalk4.gray(`5. Create a pull request`));
|
|
987
|
+
} else {
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/cli.ts
|
|
994
|
+
async function loadRegistry() {
|
|
995
|
+
const registryFile = Bun.file(new URL("../registry.bifrost", import.meta.url));
|
|
996
|
+
return await registryFile.json();
|
|
997
|
+
}
|
|
998
|
+
async function runCLI(argv) {
|
|
999
|
+
const DEFAULT_STACKS = await loadRegistry();
|
|
1000
|
+
const program = new Command();
|
|
1001
|
+
program.name("@a5gard/bifrost").description("Create a new project with platform-agnostic templates").version("1.0.0").argument("[projectName]", "The project name").option("-t, --template <owner/repo>", "The template to use (format: owner/repo)").option("-p, --pkg-mgr <pm>", `Package manager to use (${PACKAGE_MANAGERS.join(", ")})`).option("--no-install", "Skip dependency installation").option("--list-templates", "List all available community templates").option("--wizard", "Run config.bifrost wizard").option("--submit", "Submit template to bifrost registry").option("-h, --help", "Show help").action(async (projectName, options) => {
|
|
1002
|
+
if (options.help) {
|
|
1003
|
+
showHelp();
|
|
1004
|
+
process.exit(0);
|
|
1005
|
+
}
|
|
1006
|
+
if (options.listTemplates) {
|
|
1007
|
+
showTemplates(DEFAULT_STACKS);
|
|
1008
|
+
process.exit(0);
|
|
1009
|
+
}
|
|
1010
|
+
if (options.wizard) {
|
|
1011
|
+
await runConfigWizard();
|
|
1012
|
+
process.exit(0);
|
|
1013
|
+
}
|
|
1014
|
+
if (options.submit) {
|
|
1015
|
+
await submitTemplate();
|
|
1016
|
+
process.exit(0);
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
if (options.pkgMgr && !PACKAGE_MANAGERS.includes(options.pkgMgr)) {
|
|
1020
|
+
console.error(chalk5.red(`Invalid package manager. Must be one of: ${PACKAGE_MANAGERS.join(", ")}`));
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
let finalProjectName = projectName;
|
|
1024
|
+
let finalStack = options.template;
|
|
1025
|
+
let finalPackageManager = options.pkgMgr;
|
|
1026
|
+
let finalInstall = options.noInstall === false;
|
|
1027
|
+
const prompted = await promptForMissingOptions(
|
|
1028
|
+
finalProjectName,
|
|
1029
|
+
finalStack,
|
|
1030
|
+
finalPackageManager,
|
|
1031
|
+
finalInstall ? void 0 : false
|
|
1032
|
+
);
|
|
1033
|
+
finalProjectName = prompted.projectName;
|
|
1034
|
+
finalStack = prompted.template;
|
|
1035
|
+
finalPackageManager = prompted.packageManager;
|
|
1036
|
+
finalInstall = prompted.install;
|
|
1037
|
+
const gitPush = prompted.gitPush;
|
|
1038
|
+
const runWizard = prompted.runWizard;
|
|
1039
|
+
const submitToRegistry = prompted.submitToRegistry;
|
|
1040
|
+
const validProjectName = toValidPackageName(finalProjectName);
|
|
1041
|
+
await createProject({
|
|
1042
|
+
projectName: validProjectName,
|
|
1043
|
+
template: finalStack,
|
|
1044
|
+
packageManager: finalPackageManager,
|
|
1045
|
+
install: finalInstall,
|
|
1046
|
+
gitPush
|
|
1047
|
+
});
|
|
1048
|
+
if (runWizard) {
|
|
1049
|
+
await runConfigWizard();
|
|
1050
|
+
}
|
|
1051
|
+
if (submitToRegistry) {
|
|
1052
|
+
await submitTemplate();
|
|
1053
|
+
}
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
console.error();
|
|
1056
|
+
console.error(chalk5.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1057
|
+
console.error();
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
await program.parseAsync(argv);
|
|
1062
|
+
}
|
|
1063
|
+
function showHelp() {
|
|
1064
|
+
console.log(`
|
|
1065
|
+
${chalk5.bold("Usage:")}
|
|
1066
|
+
|
|
1067
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost")} ${chalk5.gray("<projectName> <...options>")}
|
|
1068
|
+
|
|
1069
|
+
${chalk5.bold("Examples:")}
|
|
1070
|
+
|
|
1071
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost")}
|
|
1072
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost my-app")}
|
|
1073
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost my-app --template remix-run/indie-template")}
|
|
1074
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost my-app -s owner/repo -p bun")}
|
|
1075
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost my-app -s owner/repo --no-install")}
|
|
1076
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost --list-templates")}
|
|
1077
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost --wizard")}
|
|
1078
|
+
${chalk5.cyan("$ bunx @a5gard/bifrost --submit")}
|
|
1079
|
+
|
|
1080
|
+
${chalk5.bold("Options:")}
|
|
1081
|
+
|
|
1082
|
+
${chalk5.cyan("--help, -h")} Print this help message
|
|
1083
|
+
${chalk5.cyan("--version, -V")} Print the CLI version
|
|
1084
|
+
${chalk5.cyan("--template, -s")} Stack to use (format: owner/repo)
|
|
1085
|
+
${chalk5.cyan("--pkg-mgr, -p")} Package manager (npm, pnpm, yarn, bun)
|
|
1086
|
+
${chalk5.cyan("--no-install")} Skip dependency installation
|
|
1087
|
+
${chalk5.cyan("--list-templates")} List all available community templates
|
|
1088
|
+
${chalk5.cyan("--wizard")} Run config.bifrost wizard
|
|
1089
|
+
${chalk5.cyan("--submit")} Submit template to bifrost registry
|
|
1090
|
+
`);
|
|
1091
|
+
}
|
|
1092
|
+
function showTemplates(DEFAULT_STACKS) {
|
|
1093
|
+
console.log();
|
|
1094
|
+
console.log(chalk5.bold("Available Community Templates"));
|
|
1095
|
+
console.log();
|
|
1096
|
+
const groupedByPlatform = DEFAULT_STACKS.reduce((acc, template) => {
|
|
1097
|
+
if (!acc[template.platform]) {
|
|
1098
|
+
acc[template.platform] = [];
|
|
1099
|
+
}
|
|
1100
|
+
acc[template.platform].push(template);
|
|
1101
|
+
return acc;
|
|
1102
|
+
}, {});
|
|
1103
|
+
Object.entries(groupedByPlatform).forEach(([platform, template]) => {
|
|
1104
|
+
console.log(chalk5.bold.cyan(`${platform.toUpperCase()}`));
|
|
1105
|
+
console.log();
|
|
1106
|
+
template.forEach((template2) => {
|
|
1107
|
+
console.log(` ${chalk5.green("\u203A")} ${chalk5.bold(`${template2.owner}/${template2.repo}`)}`);
|
|
1108
|
+
console.log(` ${chalk5.gray(template2.description)}`);
|
|
1109
|
+
console.log(` ${chalk5.gray(`Tags: ${template2.tags.join(", ")}`)}`);
|
|
1110
|
+
console.log();
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
console.log(chalk5.gray("Use any template with: ") + chalk5.cyan("bunx @a5gard/bifrost my-app --template owner/repo"));
|
|
1114
|
+
console.log();
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/index.ts
|
|
1118
|
+
runCLI(process.argv);
|