@a5gard/bifrost-plugin 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 ADDED
@@ -0,0 +1,170 @@
1
+ # bifrost-plugin
2
+
3
+ Plugin installer / wizard for bifrost projects.
4
+
5
+ ## Installing A Plugin
6
+
7
+ ### Interactive Mode
8
+
9
+ Once your project has completed its installation process, you may now cd into the newly created directory and run:
10
+
11
+ ```bash
12
+ bunx bifrost-plugin
13
+ ```
14
+
15
+ Entering interactive mode it will display the following options:
16
+ - List available plugins to install
17
+ - Plugin wizard ( guide in creating your own plugin )
18
+ - Submit Plugin
19
+
20
+ ## `List available plugins to install`
21
+
22
+ Running the following command will start plugin installation process:
23
+
24
+ ```bash
25
+ bunx bifrost-plugin list
26
+ ```
27
+
28
+ The installer will then obtain the list of available plugins to choose from the bifrost-plugin repo (owner `8an3`) from the file labeled `registry.bifrost`
29
+
30
+ ### Direct Installation
31
+
32
+ or you may use the supplied method
33
+
34
+ ```bash
35
+ bunx bifrost-plugin otp-auth-plugin
36
+ ```
37
+
38
+ Which will immediatly start the installation process, after scanning your projects config.bifrost to see if the platforms match for compatibility to ensure you are installing the correct plugin.
39
+
40
+ ## `Plugin wizard`
41
+ ### Creating your own plugin
42
+
43
+ Running the following command will start the create plugin wizard:
44
+
45
+ ```bash
46
+ bunx bifrost-plugin create
47
+ ```
48
+
49
+ Where it will then inquirer:
50
+ - name of plugin ( req )
51
+ - platform ( req )
52
+ - description ( req )
53
+ - tags you would like to have associated with your plugin
54
+ - will ask if you would like to supply the req. libraries now
55
+ - a placeholder will display the format to input the library names but will go as follows @remix-run/react, remix-auth, react
56
+ - auto push / create github repo
57
+
58
+ It will then create:
59
+ - create `files/` folder
60
+ - run `npm init`
61
+ - push to github
62
+ - create a readme containing a plugin guide and links to the site in order to submit your new plugin and discover others
63
+ - create `plugin.bifrost` configuration file, filing in all the fields that it had gotten from you during the setup process
64
+ - name
65
+ - description
66
+ - platform
67
+ - tags, if you completed this step
68
+ - libraries, if you completed this step
69
+ - github
70
+
71
+ Plugins are to be made with their own repo so as it can host all the required files for the plugin.
72
+ The repo is required to include a json config file labeled `plugin.bifrost` and a folder labeled `files` where it will host all the required files.
73
+ When installing a plugin it will prompt the user to either confirm the default supplied file location or the use can also edit the location to suite their use cases needs.
74
+
75
+ ### plugin.bifrost
76
+
77
+ ```json
78
+ {
79
+ "name": "otp-auth-plugin",
80
+ "description": "A custom one time password auth plugin for the remix platform",
81
+ "platform": "remix",
82
+ "github": "8an3/otp-auth-plugin",
83
+ "tags": ["remix-run", "auth", "one-time-password"],
84
+ "libraries": ["remix-auth-totp","remix-auth","@catalystsoftware/icons","@prisma/client","resend"],
85
+ "files": [
86
+ {
87
+ "name": "email.tsx",
88
+ "location": "app/components/catalyst-ui/utils/email.tsx"
89
+ },
90
+ {
91
+ "name": "client-auth.tsx",
92
+ "location": "app/components/catalyst-ui/utils/client-auth.tsx"
93
+ },
94
+ {
95
+ "name": "auth-session.ts",
96
+ "location": "app/components/catalyst-ui/utils/auth-session.ts"
97
+ },
98
+ {
99
+ "name": "prisma.ts",
100
+ "location": "app/components/catalyst-ui/utils/prisma.ts"
101
+ },
102
+ {
103
+ "name": "login.tsx",
104
+ "location": "app/routes/auth/login.tsx"
105
+ },
106
+ {
107
+ "name": "lougout.tsx",
108
+ "location": "app/routes/auth/lougout.tsx"
109
+ },
110
+ {
111
+ "name": "signup.tsx",
112
+ "location": "app/routes/auth/signup.tsx"
113
+ },
114
+ {
115
+ "name": "magic-link.tsx",
116
+ "location": "app/routes/auth/magic-link.tsx"
117
+ },
118
+ {
119
+ "name": "verify.tsx",
120
+ "location": "app/routes/auth/verify.tsx"
121
+ },
122
+ ],
123
+ "configs":[]
124
+ }
125
+ ```
126
+
127
+ ## `Submit Plugin`
128
+
129
+ Running the following command will start the submission process without the need of interactive mode:
130
+
131
+ ```bash
132
+ bunx bifrost-plugin submit
133
+ ```
134
+
135
+ Selecting this option will automate the submission process for you, adding your plugin to the libraries registry. Allowing you to share you plugin with others that will also be posted on the site to allow users to find it more easily.
136
+
137
+
138
+ ## Searching / Posting Templates and Plugins
139
+
140
+ Shortly a site will be available for use where you can search for templates and plugins.
141
+
142
+ Feature two tabs, both tabs will host a filtering section located to the left of the pages content and a search bar located at the top of each tabs section. Allowing you to filter by platform, tags, etc meanwhile the search bar will allow you to search for individual templates or plugins for you to use.
143
+
144
+ ### Templates
145
+
146
+ Each template result will display:
147
+ - name
148
+ - description
149
+ - platform
150
+ - command line to install the template
151
+ - tags
152
+ - any plugins that are to be included with the templates installation
153
+
154
+ ### Plugins
155
+
156
+ Each plugin result will display
157
+ - name
158
+ - description
159
+ - platform
160
+ - command line to install the plugin
161
+ - tags
162
+ - required libraries
163
+ - required files
164
+
165
+ ### Submitting
166
+
167
+ Whether its a template or plugin, you will have the ability to submit your own to be included with its respective registry, this step is not required or needed but will help in its overall discoverability.
168
+ All you have to do in order to submit is supply your templates or plugins config file once you start the submission process. The pages nav bar will host a `submit` button in order to start the process.
169
+
170
+ Upon submission the website will automatically update the relevant registry file and push the update to github to ensure the process is automated.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env bun
package/dist/index.js ADDED
@@ -0,0 +1,758 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import prompts5 from "prompts";
6
+ import chalk5 from "chalk";
7
+
8
+ // src/utils.ts
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ async function getProjectConfig() {
12
+ const configPath = path.join(process.cwd(), "config.bifrost");
13
+ if (!await fs.pathExists(configPath)) {
14
+ return null;
15
+ }
16
+ return await fs.readJson(configPath);
17
+ }
18
+ async function getRegistry() {
19
+ const registryPath = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "registry.bifrost");
20
+ return await fs.readJson(registryPath);
21
+ }
22
+ function validatePlatformCompatibility(projectPlatform, pluginPlatform) {
23
+ return projectPlatform === pluginPlatform;
24
+ }
25
+
26
+ // src/installer.ts
27
+ import fs3 from "fs-extra";
28
+ import path3 from "path";
29
+ import { execSync } from "child_process";
30
+ import ora from "ora";
31
+ import chalk2 from "chalk";
32
+ import prompts2 from "prompts";
33
+
34
+ // src/config-manager.ts
35
+ import fs2 from "fs-extra";
36
+ import path2 from "path";
37
+ import chalk from "chalk";
38
+ import prompts from "prompts";
39
+ async function processConfigFiles(pluginGithub, configs) {
40
+ for (const config of configs) {
41
+ const targetPath = path2.join(process.cwd(), config.targetFile);
42
+ const configUrl = `https://raw.githubusercontent.com/${pluginGithub}/main/files/${config.configSource}`;
43
+ const configResponse = await fetch(configUrl);
44
+ if (!configResponse.ok) {
45
+ throw new Error(`Failed to fetch config file ${config.configSource}: ${configResponse.statusText}`);
46
+ }
47
+ const configContent = await configResponse.text();
48
+ if (!await fs2.pathExists(targetPath)) {
49
+ console.log(chalk.yellow(`
50
+ Target file ${config.targetFile} does not exist. Skipping...`));
51
+ continue;
52
+ }
53
+ const existingContent = await fs2.readFile(targetPath, "utf-8");
54
+ if (await checkIfConfigExists(existingContent, configContent, config.targetFile)) {
55
+ console.log(chalk.green(`
56
+ \u2713 Configuration already exists in ${config.targetFile}. Skipping...`));
57
+ continue;
58
+ }
59
+ console.log(chalk.cyan(`
60
+ \u{1F4DD} Configuration needed for: ${config.targetFile}`));
61
+ console.log(chalk.gray("\u2500".repeat(50)));
62
+ console.log(configContent);
63
+ console.log(chalk.gray("\u2500".repeat(50)));
64
+ const { action } = await prompts({
65
+ type: "select",
66
+ name: "action",
67
+ message: `How would you like to handle ${config.targetFile}?`,
68
+ choices: [
69
+ { title: "Auto-apply changes", value: "auto" },
70
+ { title: "Copy to clipboard (manual)", value: "manual" },
71
+ { title: "Skip this configuration", value: "skip" }
72
+ ]
73
+ });
74
+ if (action === "skip") {
75
+ console.log(chalk.yellow(`Skipped ${config.targetFile}`));
76
+ continue;
77
+ }
78
+ if (action === "manual") {
79
+ console.log(chalk.blue(`
80
+ Please manually add the above configuration to ${config.targetFile}`));
81
+ continue;
82
+ }
83
+ if (action === "auto") {
84
+ await applyConfig(targetPath, existingContent, configContent, config);
85
+ console.log(chalk.green(`\u2713 Applied configuration to ${config.targetFile}`));
86
+ }
87
+ }
88
+ }
89
+ async function checkIfConfigExists(existingContent, newContent, targetFile) {
90
+ const extension = path2.extname(targetFile);
91
+ if (extension === ".json" || extension === ".jsonc") {
92
+ return checkJsonConfigExists(existingContent, newContent);
93
+ }
94
+ if (extension === ".env") {
95
+ return checkEnvConfigExists(existingContent, newContent);
96
+ }
97
+ const cleanNew = newContent.trim().replace(/\s+/g, " ");
98
+ const cleanExisting = existingContent.trim().replace(/\s+/g, " ");
99
+ return cleanExisting.includes(cleanNew);
100
+ }
101
+ function checkJsonConfigExists(existingContent, newContent) {
102
+ try {
103
+ const existing = JSON.parse(existingContent);
104
+ const newConfig = JSON.parse(newContent);
105
+ return deepIncludes(existing, newConfig);
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ function checkEnvConfigExists(existingContent, newContent) {
111
+ const existingLines = existingContent.split("\n").map((line) => line.trim());
112
+ const newLines = newContent.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
113
+ for (const newLine of newLines) {
114
+ const [key] = newLine.split("=");
115
+ const exists = existingLines.some((line) => line.startsWith(`${key}=`));
116
+ if (!exists) {
117
+ return false;
118
+ }
119
+ }
120
+ return true;
121
+ }
122
+ function deepIncludes(existing, newConfig) {
123
+ if (typeof newConfig !== "object" || newConfig === null) {
124
+ return existing === newConfig;
125
+ }
126
+ for (const key in newConfig) {
127
+ if (!(key in existing)) {
128
+ return false;
129
+ }
130
+ if (typeof newConfig[key] === "object" && newConfig[key] !== null) {
131
+ if (!deepIncludes(existing[key], newConfig[key])) {
132
+ return false;
133
+ }
134
+ } else if (Array.isArray(newConfig[key])) {
135
+ if (!Array.isArray(existing[key])) {
136
+ return false;
137
+ }
138
+ for (const item of newConfig[key]) {
139
+ if (!existing[key].includes(item)) {
140
+ return false;
141
+ }
142
+ }
143
+ } else if (existing[key] !== newConfig[key]) {
144
+ return false;
145
+ }
146
+ }
147
+ return true;
148
+ }
149
+ async function applyConfig(targetPath, existingContent, newContent, config) {
150
+ const extension = path2.extname(targetPath);
151
+ if (extension === ".json" || extension === ".jsonc") {
152
+ await applyJsonConfig(targetPath, existingContent, newContent);
153
+ return;
154
+ }
155
+ if (extension === ".env") {
156
+ await applyEnvConfig(targetPath, existingContent, newContent);
157
+ return;
158
+ }
159
+ if (config.insertType === "append") {
160
+ const updatedContent = existingContent + "\n\n" + newContent;
161
+ await fs2.writeFile(targetPath, updatedContent, "utf-8");
162
+ } else if (config.insertType === "replace") {
163
+ await fs2.writeFile(targetPath, newContent, "utf-8");
164
+ } else if (config.insertType === "merge") {
165
+ const updatedContent = existingContent + "\n\n" + newContent;
166
+ await fs2.writeFile(targetPath, updatedContent, "utf-8");
167
+ }
168
+ }
169
+ async function applyJsonConfig(targetPath, existingContent, newContent) {
170
+ const existing = JSON.parse(existingContent);
171
+ const newConfig = JSON.parse(newContent);
172
+ const merged = deepMerge(existing, newConfig);
173
+ await fs2.writeFile(targetPath, JSON.stringify(merged, null, 2), "utf-8");
174
+ }
175
+ async function applyEnvConfig(targetPath, existingContent, newContent) {
176
+ const existingLines = existingContent.split("\n");
177
+ const newLines = newContent.split("\n").filter((line) => line.trim() && !line.trim().startsWith("#"));
178
+ const existingKeys = new Set(
179
+ existingLines.filter((line) => line.includes("=")).map((line) => line.split("=")[0].trim())
180
+ );
181
+ const linesToAdd = newLines.filter((line) => {
182
+ const key = line.split("=")[0].trim();
183
+ return !existingKeys.has(key);
184
+ });
185
+ if (linesToAdd.length > 0) {
186
+ const updatedContent = existingContent + "\n\n" + linesToAdd.join("\n");
187
+ await fs2.writeFile(targetPath, updatedContent, "utf-8");
188
+ }
189
+ }
190
+ function deepMerge(target, source) {
191
+ const output = { ...target };
192
+ for (const key in source) {
193
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
194
+ if (key in target) {
195
+ output[key] = deepMerge(target[key], source[key]);
196
+ } else {
197
+ output[key] = source[key];
198
+ }
199
+ } else if (Array.isArray(source[key])) {
200
+ if (Array.isArray(target[key])) {
201
+ output[key] = [.../* @__PURE__ */ new Set([...target[key], ...source[key]])];
202
+ } else {
203
+ output[key] = source[key];
204
+ }
205
+ } else {
206
+ output[key] = source[key];
207
+ }
208
+ }
209
+ return output;
210
+ }
211
+
212
+ // src/installer.ts
213
+ async function installPlugin(pluginGithub, projectPlatform) {
214
+ const installedFiles = [];
215
+ const installedLibraries = [];
216
+ const modifiedConfigFiles = [];
217
+ let spinner = ora("Fetching plugin configuration...").start();
218
+ try {
219
+ const configUrl = `https://raw.githubusercontent.com/${pluginGithub}/main/plugin.bifrost`;
220
+ const configResponse = await fetch(configUrl);
221
+ if (!configResponse.ok) {
222
+ throw new Error(`Failed to fetch plugin configuration: ${configResponse.statusText}`);
223
+ }
224
+ const pluginConfig = await configResponse.json();
225
+ spinner.succeed("Plugin configuration fetched");
226
+ if (pluginConfig.platform !== projectPlatform) {
227
+ throw new Error(`Platform mismatch: Plugin is for ${pluginConfig.platform}, but project is ${projectPlatform}`);
228
+ }
229
+ spinner = ora("Installing plugin files...").start();
230
+ for (const file of pluginConfig.files) {
231
+ const shouldUseDefault = await prompts2({
232
+ type: "confirm",
233
+ name: "useDefault",
234
+ message: `Install ${file.name} to ${file.location}?`,
235
+ initial: true
236
+ });
237
+ let finalTargetPath = path3.join(process.cwd(), file.location);
238
+ if (!shouldUseDefault.useDefault) {
239
+ const customLocation = await prompts2({
240
+ type: "text",
241
+ name: "location",
242
+ message: `Enter custom location for ${file.name}:`,
243
+ initial: file.location
244
+ });
245
+ finalTargetPath = path3.join(process.cwd(), customLocation.location);
246
+ }
247
+ const fileUrl = `https://raw.githubusercontent.com/${pluginGithub}/main/files/${file.name}`;
248
+ const fileResponse = await fetch(fileUrl);
249
+ if (!fileResponse.ok) {
250
+ throw new Error(`Failed to fetch file ${file.name}: ${fileResponse.statusText}`);
251
+ }
252
+ const fileContent = await fileResponse.text();
253
+ await fs3.ensureDir(path3.dirname(finalTargetPath));
254
+ await fs3.writeFile(finalTargetPath, fileContent, "utf-8");
255
+ installedFiles.push(finalTargetPath);
256
+ }
257
+ spinner.succeed("Plugin files installed");
258
+ if (pluginConfig.configs && pluginConfig.configs.length > 0) {
259
+ spinner = ora("Processing configuration files...").start();
260
+ spinner.stop();
261
+ await processConfigFiles(pluginGithub, pluginConfig.configs);
262
+ console.log(chalk2.green("\n\u2713 Configuration files processed"));
263
+ }
264
+ const pkgManager = detectPackageManager();
265
+ if (pluginConfig.dependencies && pluginConfig.dependencies.length > 0) {
266
+ spinner = ora("Installing dependencies...").start();
267
+ const installCmd = pkgManager === "npm" ? "npm install" : pkgManager === "yarn" ? "yarn add" : pkgManager === "pnpm" ? "pnpm add" : "bun add";
268
+ execSync(`${installCmd} ${pluginConfig.dependencies.join(" ")}`, { stdio: "inherit" });
269
+ installedLibraries.push(...pluginConfig.dependencies);
270
+ spinner.succeed("Dependencies installed");
271
+ }
272
+ if (pluginConfig.devDependencies && pluginConfig.devDependencies.length > 0) {
273
+ spinner = ora("Installing dev dependencies...").start();
274
+ const installCmd = pkgManager === "npm" ? "npm install -D" : pkgManager === "yarn" ? "yarn add -D" : pkgManager === "pnpm" ? "pnpm add -D" : "bun add -D";
275
+ execSync(`${installCmd} ${pluginConfig.devDependencies.join(" ")}`, { stdio: "inherit" });
276
+ installedLibraries.push(...pluginConfig.devDependencies);
277
+ spinner.succeed("Dev dependencies installed");
278
+ }
279
+ console.log(chalk2.green("\n\u2713 Plugin installed successfully!"));
280
+ } catch (error) {
281
+ spinner.fail("Plugin installation failed");
282
+ console.log(chalk2.yellow("\nRolling back changes..."));
283
+ for (const filePath of installedFiles) {
284
+ try {
285
+ await fs3.remove(filePath);
286
+ } catch (e) {
287
+ console.error(chalk2.red(`Failed to remove ${filePath}`));
288
+ }
289
+ }
290
+ if (installedLibraries.length > 0) {
291
+ const pkgManager = detectPackageManager();
292
+ const uninstallCmd = pkgManager === "npm" ? "npm uninstall" : pkgManager === "yarn" ? "yarn remove" : pkgManager === "pnpm" ? "pnpm remove" : "bun remove";
293
+ try {
294
+ execSync(`${uninstallCmd} ${installedLibraries.join(" ")}`, { stdio: "inherit" });
295
+ } catch (e) {
296
+ console.error(chalk2.red("Failed to remove installed libraries"));
297
+ }
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+ function detectPackageManager() {
303
+ if (fs3.pathExistsSync("bun.lockb")) return "bun";
304
+ if (fs3.pathExistsSync("pnpm-lock.yaml")) return "pnpm";
305
+ if (fs3.pathExistsSync("yarn.lock")) return "yarn";
306
+ return "npm";
307
+ }
308
+
309
+ // src/creator.ts
310
+ import fs4 from "fs-extra";
311
+ import path4 from "path";
312
+ import chalk3 from "chalk";
313
+ import prompts3 from "prompts";
314
+ import { execSync as execSync2 } from "child_process";
315
+ async function createPlugin() {
316
+ console.log(chalk3.blue.bold("\n\u{1F680} Bifrost Plugin Creator\n"));
317
+ const answers = await prompts3([
318
+ {
319
+ type: "text",
320
+ name: "name",
321
+ message: "Plugin name:",
322
+ validate: (value) => value.length > 0 ? true : "Plugin name is required"
323
+ },
324
+ {
325
+ type: "select",
326
+ name: "platform",
327
+ message: "Select platform:",
328
+ choices: [
329
+ { title: "Remix", value: "remix" },
330
+ { title: "Next.js", value: "nextjs" },
331
+ { title: "Vite", value: "vite" },
332
+ { title: "Other", value: "other" }
333
+ ]
334
+ },
335
+ {
336
+ type: "text",
337
+ name: "description",
338
+ message: "Description:",
339
+ validate: (value) => value.length > 0 ? true : "Description is required"
340
+ },
341
+ {
342
+ type: "text",
343
+ name: "tags",
344
+ message: "Tags (comma-separated):",
345
+ initial: "",
346
+ format: (value) => value ? value.split(",").map((t) => t.trim()).filter(Boolean) : []
347
+ },
348
+ {
349
+ type: "confirm",
350
+ name: "addLibraries",
351
+ message: "Would you like to supply required libraries now?",
352
+ initial: false
353
+ }
354
+ ]);
355
+ if (!answers.name) {
356
+ console.log(chalk3.yellow("\nPlugin creation cancelled"));
357
+ process.exit(0);
358
+ }
359
+ let libraries = [];
360
+ if (answers.addLibraries) {
361
+ console.log(chalk3.gray("\nFormat: @remix-run/react, remix-auth, react"));
362
+ const { libraryInput } = await prompts3({
363
+ type: "text",
364
+ name: "libraryInput",
365
+ message: "Libraries:",
366
+ initial: ""
367
+ });
368
+ if (libraryInput) {
369
+ libraries = libraryInput.split(",").map((l) => l.trim()).filter(Boolean);
370
+ }
371
+ }
372
+ const { githubUsername } = await prompts3({
373
+ type: "text",
374
+ name: "githubUsername",
375
+ message: "GitHub username:",
376
+ validate: (value) => value.length > 0 ? true : "GitHub username is required"
377
+ });
378
+ if (!githubUsername) {
379
+ console.log(chalk3.yellow("\nPlugin creation cancelled"));
380
+ process.exit(0);
381
+ }
382
+ const { autoGithub } = await prompts3({
383
+ type: "confirm",
384
+ name: "autoGithub",
385
+ message: "Auto-create and push to GitHub?",
386
+ initial: true
387
+ });
388
+ const pluginDir = path4.join(process.cwd(), answers.name);
389
+ if (await fs4.pathExists(pluginDir)) {
390
+ console.error(chalk3.red(`
391
+ Error: Directory ${answers.name} already exists`));
392
+ process.exit(1);
393
+ }
394
+ console.log(chalk3.blue("\n\u{1F4E6} Creating plugin structure..."));
395
+ await fs4.ensureDir(pluginDir);
396
+ await fs4.ensureDir(path4.join(pluginDir, "files"));
397
+ const packageJson = {
398
+ name: answers.name,
399
+ version: "1.0.0",
400
+ description: answers.description,
401
+ main: "index.js",
402
+ type: "module",
403
+ keywords: answers.tags,
404
+ author: githubUsername,
405
+ license: "MIT"
406
+ };
407
+ await fs4.writeJson(path4.join(pluginDir, "package.json"), packageJson, { spaces: 2 });
408
+ const pluginConfig = {
409
+ name: answers.name,
410
+ description: answers.description,
411
+ platform: answers.platform,
412
+ github: `${githubUsername}/${answers.name}`,
413
+ tags: answers.tags,
414
+ libraries,
415
+ files: [],
416
+ configs: []
417
+ };
418
+ await fs4.writeJson(path4.join(pluginDir, "plugin.bifrost"), pluginConfig, { spaces: 2 });
419
+ const readme = generateReadme(pluginConfig, githubUsername);
420
+ await fs4.writeFile(path4.join(pluginDir, "README.md"), readme, "utf-8");
421
+ const gitignore = `node_modules/
422
+ .DS_Store
423
+ *.log
424
+ .env
425
+ .env.local
426
+ dist/
427
+ build/
428
+ `;
429
+ await fs4.writeFile(path4.join(pluginDir, ".gitignore"), gitignore, "utf-8");
430
+ console.log(chalk3.green("\u2713 Plugin structure created"));
431
+ if (autoGithub) {
432
+ console.log(chalk3.blue("\n\u{1F4E4} Setting up GitHub repository..."));
433
+ try {
434
+ process.chdir(pluginDir);
435
+ execSync2("git init", { stdio: "inherit" });
436
+ execSync2("git add .", { stdio: "inherit" });
437
+ execSync2('git commit -m "Initial commit: Plugin scaffold"', { stdio: "inherit" });
438
+ execSync2(`gh repo create ${answers.name} --public --source=. --remote=origin --push`, { stdio: "inherit" });
439
+ console.log(chalk3.green("\u2713 GitHub repository created and pushed"));
440
+ console.log(chalk3.cyan(`
441
+ \u{1F4CD} Repository: https://github.com/${githubUsername}/${answers.name}`));
442
+ } catch (error) {
443
+ console.log(chalk3.yellow("\n\u26A0 Could not auto-create GitHub repository"));
444
+ console.log(chalk3.gray("You can manually create and push:"));
445
+ console.log(chalk3.gray(` cd ${answers.name}`));
446
+ console.log(chalk3.gray(" git init"));
447
+ console.log(chalk3.gray(" git add ."));
448
+ console.log(chalk3.gray(' git commit -m "Initial commit"'));
449
+ console.log(chalk3.gray(` gh repo create ${answers.name} --public --source=. --remote=origin --push`));
450
+ }
451
+ } else {
452
+ console.log(chalk3.blue("\n\u{1F4DD} Manual GitHub setup:"));
453
+ console.log(chalk3.gray(` cd ${answers.name}`));
454
+ console.log(chalk3.gray(" git init"));
455
+ console.log(chalk3.gray(" git add ."));
456
+ console.log(chalk3.gray(' git commit -m "Initial commit"'));
457
+ console.log(chalk3.gray(` gh repo create ${answers.name} --public --source=. --remote=origin --push`));
458
+ }
459
+ console.log(chalk3.green.bold("\n\u2728 Plugin created successfully!\n"));
460
+ console.log(chalk3.cyan("Next steps:"));
461
+ console.log(chalk3.gray(` 1. Add your plugin files to ${answers.name}/files/`));
462
+ console.log(chalk3.gray(` 2. Update plugin.bifrost with file mappings`));
463
+ console.log(chalk3.gray(" 3. Submit to registry: https://bifrost-plugins.dev/submit"));
464
+ console.log();
465
+ }
466
+ function generateReadme(config, username) {
467
+ return `# ${config.name}
468
+
469
+ ${config.description}
470
+
471
+ ## Installation
472
+
473
+ \`\`\`bash
474
+ bunx bifrost-plugin ${config.name}
475
+ \`\`\`
476
+
477
+ ## Platform
478
+
479
+ This plugin is designed for **${config.platform}** projects.
480
+
481
+ ## Required Libraries
482
+
483
+ ${config.libraries.length > 0 ? config.libraries.map((lib) => `- \`${lib}\``).join("\n") : "No additional libraries required."}
484
+
485
+ ## Tags
486
+
487
+ ${config.tags.length > 0 ? config.tags.map((tag) => `\`${tag}\``).join(", ") : "No tags specified."}
488
+
489
+ ## Files
490
+
491
+ This plugin will add the following files to your project:
492
+
493
+ ${config.files.length > 0 ? config.files.map((file) => `- \`${file.location}\``).join("\n") : "File mappings to be configured."}
494
+
495
+ ## Configuration
496
+
497
+ Add your plugin files to the \`files/\` directory and update \`plugin.bifrost\` with the file mappings:
498
+
499
+ \`\`\`json
500
+ {
501
+ "files": [
502
+ {
503
+ "name": "your-file.tsx",
504
+ "location": "app/path/to/your-file.tsx"
505
+ }
506
+ ]
507
+ }
508
+ \`\`\`
509
+
510
+ ## Submit to Registry
511
+
512
+ Once your plugin is ready, submit it to the Bifrost Plugin Registry:
513
+
514
+ \u{1F517} [Submit Plugin](https://bifrost-plugins.dev/submit)
515
+
516
+ ## Development
517
+
518
+ 1. Add your plugin files to \`files/\` directory
519
+ 2. Update \`plugin.bifrost\` with file mappings and configurations
520
+ 3. Test installation in a bifrost project
521
+ 4. Push changes to GitHub
522
+ 5. Submit to registry
523
+
524
+ ## License
525
+
526
+ MIT \xA9 ${username}
527
+
528
+ ## Links
529
+
530
+ - [Bifrost Plugin Registry](https://bifrost-plugins.dev)
531
+ - [Plugin Documentation](https://bifrost-plugins.dev/docs)
532
+ - [Submit a Plugin](https://bifrost-plugins.dev/submit)
533
+ `;
534
+ }
535
+
536
+ // src/submitter.ts
537
+ import fs5 from "fs-extra";
538
+ import path5 from "path";
539
+ import chalk4 from "chalk";
540
+ import prompts4 from "prompts";
541
+ import { execSync as execSync3 } from "child_process";
542
+ var REGISTRY_REPO = "8an3/bifrost-plugin";
543
+ var REGISTRY_FILE = "dist/registry.bifrost";
544
+ async function submitPlugin() {
545
+ console.log(chalk4.blue.bold("\n\u{1F4E4} Submit Plugin to Registry\n"));
546
+ const pluginConfigPath = path5.join(process.cwd(), "plugin.bifrost");
547
+ if (!await fs5.pathExists(pluginConfigPath)) {
548
+ console.error(chalk4.red("Error: plugin.bifrost not found in current directory"));
549
+ console.log(chalk4.yellow("Make sure you are in your plugin directory"));
550
+ process.exit(1);
551
+ }
552
+ const pluginConfig = await fs5.readJson(pluginConfigPath);
553
+ console.log(chalk4.cyan("\nPlugin Information:"));
554
+ console.log(chalk4.gray("\u2500".repeat(50)));
555
+ console.log(`Name: ${chalk4.white(pluginConfig.name)}`);
556
+ console.log(`Description: ${chalk4.white(pluginConfig.description)}`);
557
+ console.log(`Platform: ${chalk4.white(pluginConfig.platform)}`);
558
+ console.log(`GitHub: ${chalk4.white(pluginConfig.github)}`);
559
+ console.log(`Tags: ${chalk4.white(pluginConfig.tags.join(", "))}`);
560
+ console.log(`Libraries: ${chalk4.white(pluginConfig.libraries.join(", "))}`);
561
+ console.log(chalk4.gray("\u2500".repeat(50)));
562
+ const { confirm } = await prompts4({
563
+ type: "confirm",
564
+ name: "confirm",
565
+ message: "Submit this plugin to the registry?",
566
+ initial: true
567
+ });
568
+ if (!confirm) {
569
+ console.log(chalk4.yellow("\nSubmission cancelled"));
570
+ process.exit(0);
571
+ }
572
+ try {
573
+ const registryEntry = {
574
+ name: pluginConfig.name,
575
+ description: pluginConfig.description,
576
+ platform: pluginConfig.platform,
577
+ github: pluginConfig.github,
578
+ tags: pluginConfig.tags
579
+ };
580
+ console.log(chalk4.blue("\n\u{1F504} Forking registry repository..."));
581
+ execSync3(`gh repo fork ${REGISTRY_REPO} --clone=false`, { stdio: "inherit" });
582
+ const username = execSync3("gh api user -q .login", { encoding: "utf-8" }).trim();
583
+ const forkRepo = `${username}/bifrost-plugin`;
584
+ console.log(chalk4.blue("\u{1F4E5} Cloning forked repository..."));
585
+ const tempDir = path5.join(process.cwd(), ".bifrost-temp");
586
+ await fs5.ensureDir(tempDir);
587
+ execSync3(`gh repo clone ${forkRepo} ${tempDir}`, { stdio: "inherit" });
588
+ console.log(chalk4.blue("\u{1F4CB} Fetching current registry..."));
589
+ const registryUrl = `https://raw.githubusercontent.com/${REGISTRY_REPO}/main/${REGISTRY_FILE}`;
590
+ const registryResponse = await fetch(registryUrl);
591
+ let registry = [];
592
+ if (registryResponse.ok) {
593
+ registry = await registryResponse.json();
594
+ }
595
+ const registryPath = path5.join(tempDir, REGISTRY_FILE);
596
+ await fs5.ensureDir(path5.dirname(registryPath));
597
+ const existingIndex = registry.findIndex((p) => p.name === pluginConfig.name);
598
+ if (existingIndex !== -1) {
599
+ console.log(chalk4.yellow("\n\u26A0 Plugin already exists in registry. Updating..."));
600
+ registry[existingIndex] = registryEntry;
601
+ } else {
602
+ registry.push(registryEntry);
603
+ }
604
+ await fs5.writeJson(registryPath, registry, { spaces: 2 });
605
+ console.log(chalk4.blue("\u{1F4BE} Committing changes..."));
606
+ process.chdir(tempDir);
607
+ execSync3("git add .", { stdio: "inherit" });
608
+ execSync3(`git commit -m "Add/Update plugin: ${pluginConfig.name}"`, { stdio: "inherit" });
609
+ execSync3("git push", { stdio: "inherit" });
610
+ console.log(chalk4.blue("\u{1F500} Creating pull request..."));
611
+ const prUrl = execSync3(
612
+ `gh pr create --repo ${REGISTRY_REPO} --title "Add plugin: ${pluginConfig.name}" --body "Submitting plugin ${pluginConfig.name} to the registry.
613
+
614
+ Platform: ${pluginConfig.platform}
615
+ Description: ${pluginConfig.description}"`,
616
+ { encoding: "utf-8" }
617
+ ).trim();
618
+ process.chdir("..");
619
+ await fs5.remove(tempDir);
620
+ console.log(chalk4.green.bold("\n\u2728 Plugin submitted successfully!\n"));
621
+ console.log(chalk4.cyan("Pull Request:"), chalk4.white(prUrl));
622
+ console.log(chalk4.gray("\nYour plugin will be available once the PR is merged."));
623
+ } catch (error) {
624
+ if (error instanceof Error && error.message.includes("gh: command not found")) {
625
+ console.log(chalk4.red("\n\u274C GitHub CLI (gh) is not installed"));
626
+ console.log(chalk4.yellow("\nManual submission steps:"));
627
+ console.log(chalk4.gray(`1. Fork the repository: https://github.com/${REGISTRY_REPO}`));
628
+ console.log(chalk4.gray(`2. Clone your fork`));
629
+ console.log(chalk4.gray(`3. Add your plugin to ${REGISTRY_FILE}`));
630
+ console.log(chalk4.gray(`4. Commit and push changes`));
631
+ console.log(chalk4.gray(`5. Create a pull request`));
632
+ } else {
633
+ throw error;
634
+ }
635
+ }
636
+ }
637
+
638
+ // src/index.ts
639
+ var program = new Command();
640
+ program.name("@a5gard/bifrost-plugin").description("Plugin installer for bifrost projects").version("1.0.0");
641
+ program.command("create").description("Create a new bifrost plugin").action(async () => {
642
+ try {
643
+ await createPlugin();
644
+ } catch (error) {
645
+ console.error(chalk5.red(`
646
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
647
+ process.exit(1);
648
+ }
649
+ });
650
+ program.command("list").description("List available plugins to install").action(async () => {
651
+ try {
652
+ await listPlugins();
653
+ } catch (error) {
654
+ console.error(chalk5.red(`
655
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
656
+ process.exit(1);
657
+ }
658
+ });
659
+ program.command("submit").description("Submit your plugin to the registry").action(async () => {
660
+ try {
661
+ await submitPlugin();
662
+ } catch (error) {
663
+ console.error(chalk5.red(`
664
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
665
+ process.exit(1);
666
+ }
667
+ });
668
+ program.argument("[plugin-name]", "Name of the plugin to install").action(async (pluginName) => {
669
+ try {
670
+ if (!pluginName) {
671
+ await interactiveMode();
672
+ return;
673
+ }
674
+ const projectConfig = await getProjectConfig();
675
+ if (!projectConfig) {
676
+ console.error(chalk5.red("Error: config.bifrost not found in current directory"));
677
+ console.log(chalk5.yellow("Make sure you are in a bifrost project directory"));
678
+ process.exit(1);
679
+ }
680
+ const registry = await getRegistry();
681
+ const plugin = registry.find((p) => p.name === pluginName);
682
+ if (!plugin) {
683
+ console.error(chalk5.red(`Error: Plugin "${pluginName}" not found in registry`));
684
+ process.exit(1);
685
+ }
686
+ if (!validatePlatformCompatibility(projectConfig.platform, plugin.platform)) {
687
+ console.error(chalk5.red(`Error: Plugin is for ${plugin.platform}, but your project is ${projectConfig.platform}`));
688
+ process.exit(1);
689
+ }
690
+ console.log(chalk5.blue(`Installing ${plugin.name}...`));
691
+ await installPlugin(plugin.github, projectConfig.platform);
692
+ } catch (error) {
693
+ console.error(chalk5.red(`
694
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
695
+ process.exit(1);
696
+ }
697
+ });
698
+ async function interactiveMode() {
699
+ console.log(chalk5.blue.bold("\n\u{1F309} bifrost Plugin Manager\n"));
700
+ const { action } = await prompts5({
701
+ type: "select",
702
+ name: "action",
703
+ message: "What would you like to do?",
704
+ choices: [
705
+ { title: "List available plugins to install", value: "list" },
706
+ { title: "Plugin wizard (create your own plugin)", value: "create" },
707
+ { title: "Submit plugin to registry", value: "submit" }
708
+ ]
709
+ });
710
+ if (!action) {
711
+ console.log(chalk5.yellow("\nCancelled"));
712
+ process.exit(0);
713
+ }
714
+ switch (action) {
715
+ case "list":
716
+ await listPlugins();
717
+ break;
718
+ case "create":
719
+ await createPlugin();
720
+ break;
721
+ case "submit":
722
+ await submitPlugin();
723
+ break;
724
+ }
725
+ }
726
+ async function listPlugins() {
727
+ const projectConfig = await getProjectConfig();
728
+ if (!projectConfig) {
729
+ console.error(chalk5.red("Error: config.bifrost not found in current directory"));
730
+ console.log(chalk5.yellow("Make sure you are in a bifrost project directory"));
731
+ process.exit(1);
732
+ }
733
+ const registry = await getRegistry();
734
+ const compatiblePlugins = registry.filter(
735
+ (p) => validatePlatformCompatibility(projectConfig.platform, p.platform)
736
+ );
737
+ if (compatiblePlugins.length === 0) {
738
+ console.log(chalk5.yellow(`No plugins available for platform: ${projectConfig.platform}`));
739
+ process.exit(0);
740
+ }
741
+ const { selectedPlugin } = await prompts5({
742
+ type: "select",
743
+ name: "selectedPlugin",
744
+ message: "Select a plugin to install:",
745
+ choices: compatiblePlugins.map((p) => ({
746
+ title: `${p.name} - ${p.description}`,
747
+ value: p
748
+ }))
749
+ });
750
+ if (!selectedPlugin) {
751
+ console.log(chalk5.yellow("Installation cancelled"));
752
+ process.exit(0);
753
+ }
754
+ console.log(chalk5.blue(`
755
+ Installing ${selectedPlugin.name}...`));
756
+ await installPlugin(selectedPlugin.github, projectConfig.platform);
757
+ }
758
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@a5gard/bifrost-plugin",
3
+ "version": "1.0.1",
4
+ "description": "Plugin installer / wizard for bifrost projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "bifrost-plugin": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "dev": "tsup src/index.ts --format esm --dts --watch",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "bifrost",
19
+ "plugin",
20
+ "cli"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "author": "8an3",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "commander": "^11.1.0",
29
+ "prompts": "^2.4.2",
30
+ "chalk": "^5.3.0",
31
+ "ora": "^8.0.1",
32
+ "fs-extra": "^11.2.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.11.5",
36
+ "@types/prompts": "^2.4.9",
37
+ "@types/fs-extra": "^11.0.4",
38
+ "typescript": "^5.3.3",
39
+ "tsup": "^8.0.1"
40
+ }
41
+ }