@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 +170 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +758 -0
- package/package.json +41 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|