create-forge-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ export type PluginType = 'ui' | 'backend' | 'fullstack';
2
+ export interface PluginNames {
3
+ /** Short plugin ID, consistent with backend's derivePluginId(). e.g. "pagerduty" */
4
+ pluginId: string;
5
+ /** Full npm package name. e.g. "@myorg/forge-plugin-pagerduty" */
6
+ packageName: string;
7
+ /** Output directory name. e.g. "forge-plugin-pagerduty" */
8
+ dirName: string;
9
+ /** PascalCase for component/class names. e.g. "Pagerduty" | "MyPlugin" */
10
+ pascalName: string;
11
+ /** camelCase for variable names. e.g. "pagerduty" | "myPlugin" */
12
+ camelName: string;
13
+ /** Human-readable title. e.g. "Pagerduty" | "My Plugin" */
14
+ title: string;
15
+ /** Optional org scope. e.g. "@myorg" (with @) */
16
+ org?: string;
17
+ }
18
+ /**
19
+ * Derives all naming variants from a short plugin identifier.
20
+ *
21
+ * @param pluginId - Short ID, e.g. "pagerduty" or "my-custom-plugin". No scope, no prefix.
22
+ * @param org - Optional npm scope with @, e.g. "@myorg"
23
+ *
24
+ * Convention: pluginId MUST be kebab-case, lowercase, no special chars.
25
+ */
26
+ export declare function deriveNames(pluginId: string, org?: string): PluginNames;
27
+ //# sourceMappingURL=names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,CAAC;AAExD,MAAM,WAAW,WAAW;IAC1B,oFAAoF;IACpF,QAAQ,EAAK,MAAM,CAAC;IACpB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,OAAO,EAAM,MAAM,CAAC;IACpB,0EAA0E;IAC1E,UAAU,EAAG,MAAM,CAAC;IACpB,kEAAkE;IAClE,SAAS,EAAI,MAAM,CAAC;IACpB,2DAA2D;IAC3D,KAAK,EAAQ,MAAM,CAAC;IACpB,iDAAiD;IACjD,GAAG,CAAC,EAAS,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,CAmCvE"}
package/dist/names.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Derives all naming variants from a short plugin identifier.
3
+ *
4
+ * @param pluginId - Short ID, e.g. "pagerduty" or "my-custom-plugin". No scope, no prefix.
5
+ * @param org - Optional npm scope with @, e.g. "@myorg"
6
+ *
7
+ * Convention: pluginId MUST be kebab-case, lowercase, no special chars.
8
+ */
9
+ export function deriveNames(pluginId, org) {
10
+ if (!/^[a-z][a-z0-9-]*$/.test(pluginId)) {
11
+ throw new Error(`Invalid plugin ID "${pluginId}". Must be lowercase kebab-case (e.g. "pagerduty", "my-plugin").`);
12
+ }
13
+ if (org && !/^@[a-z][a-z0-9-]*$/.test(org)) {
14
+ throw new Error(`Invalid org scope "${org}". Must start with @ and be lowercase (e.g. "@myorg").`);
15
+ }
16
+ const packageName = org
17
+ ? `${org}/forge-plugin-${pluginId}`
18
+ : `forge-plugin-${pluginId}`;
19
+ const dirName = `forge-plugin-${pluginId}`;
20
+ const parts = pluginId.split('-');
21
+ const pascalName = parts
22
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
23
+ .join('');
24
+ const camelName = (parts[0] ?? '') +
25
+ parts
26
+ .slice(1)
27
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
28
+ .join('');
29
+ const title = parts
30
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
31
+ .join(' ');
32
+ return { pluginId, packageName, dirName, pascalName, camelName, title, org };
33
+ }
34
+ //# sourceMappingURL=names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"names.js","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAmBA;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB,EAAE,GAAY;IACxD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CACb,sBAAsB,QAAQ,kEAAkE,CACjG,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,sBAAsB,GAAG,wDAAwD,CAClF,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,GAAG;QACrB,CAAC,CAAC,GAAG,GAAG,iBAAiB,QAAQ,EAAE;QACnC,CAAC,CAAC,gBAAgB,QAAQ,EAAE,CAAC;IAE/B,MAAM,OAAO,GAAG,gBAAgB,QAAQ,EAAE,CAAC;IAE3C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,KAAK;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,EAAE,CAAC,CAAC;IAEZ,MAAM,SAAS,GACb,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK;aACF,KAAK,CAAC,CAAC,CAAC;aACR,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;aAClD,IAAI,CAAC,EAAE,CAAC,CAAC;IAEd,MAAM,KAAK,GAAG,KAAK;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAClD,IAAI,CAAC,GAAG,CAAC,CAAC;IAEb,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { PluginNames, PluginType } from './names.js';
2
+ export interface ScaffoldResult {
3
+ targetDir: string;
4
+ filesCreated: string[];
5
+ }
6
+ /**
7
+ * Writes all generated files for the plugin to the target directory.
8
+ * Throws if the target directory already exists.
9
+ */
10
+ export declare function scaffoldPlugin(names: PluginNames, type: PluginType, parentDir: string): Promise<ScaffoldResult>;
11
+ //# sourceMappingURL=scaffolder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffolder.d.ts","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAQ1D,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAK,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAM,WAAW,EACtB,IAAI,EAAO,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC,CAgDzB"}
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { genManifest, genPackageJson, genTsConfig, genReadme, genUiIndex, genUiTab, genBackendIndex, genBackendAction, genBackendRoutes, genFullstackUiIndex, genFullstackCard, genFullstackBackendIndex, } from './templates.js';
4
+ /**
5
+ * Writes all generated files for the plugin to the target directory.
6
+ * Throws if the target directory already exists.
7
+ */
8
+ export async function scaffoldPlugin(names, type, parentDir) {
9
+ const targetDir = path.join(parentDir, names.dirName);
10
+ const filesCreated = [];
11
+ // Guard: refuse to overwrite existing directory
12
+ try {
13
+ await fs.access(targetDir);
14
+ throw new Error(`Directory "${names.dirName}" already exists in ${parentDir}. ` +
15
+ `Remove it first or choose a different plugin ID.`);
16
+ }
17
+ catch (err) {
18
+ if (err.code !== 'ENOENT')
19
+ throw err;
20
+ }
21
+ async function write(relPath, content) {
22
+ const absPath = path.join(targetDir, relPath);
23
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
24
+ await fs.writeFile(absPath, content, 'utf8');
25
+ filesCreated.push(relPath);
26
+ }
27
+ // ── Shared files ────────────────────────────────────────────────────────────
28
+ await write('forgeportal-plugin.json', genManifest(names, type));
29
+ await write('package.json', genPackageJson(names, type));
30
+ await write('tsconfig.json', genTsConfig(type));
31
+ await write('README.md', genReadme(names, type));
32
+ // ── Type-specific files ─────────────────────────────────────────────────────
33
+ if (type === 'ui') {
34
+ await write('src/index.ts', genUiIndex(names));
35
+ await write(`src/${names.pascalName}Tab.tsx`, genUiTab(names));
36
+ }
37
+ else if (type === 'backend') {
38
+ await write('src/index.ts', genBackendIndex(names));
39
+ await write(`src/actions/${names.camelName}Action.ts`, genBackendAction(names));
40
+ await write('src/routes.ts', genBackendRoutes(names));
41
+ }
42
+ else if (type === 'fullstack') {
43
+ await write('src/ui/index.ts', genFullstackUiIndex(names));
44
+ await write(`src/ui/${names.pascalName}Card.tsx`, genFullstackCard(names));
45
+ await write('src/backend/index.ts', genFullstackBackendIndex(names));
46
+ await write(`src/backend/actions/${names.camelName}Action.ts`, genBackendAction(names));
47
+ await write('src/backend/routes.ts', genBackendRoutes(names));
48
+ await write('src/index.ts', `// Re-export entry points for both plugin types\nexport * from './ui/index.js';\nexport * from './backend/index.js';\n`);
49
+ }
50
+ return { targetDir, filesCreated };
51
+ }
52
+ //# sourceMappingURL=scaffolder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffolder.js","sourceRoot":"","sources":["../src/scaffolder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { PluginNames, PluginType } from './names.js';
2
+ export declare function genManifest(names: PluginNames, type: PluginType): string;
3
+ export declare function genPackageJson(names: PluginNames, type: PluginType): string;
4
+ export declare function genTsConfig(type: PluginType): string;
5
+ export declare function genReadme(names: PluginNames, type: PluginType): string;
6
+ export declare function genUiIndex(names: PluginNames): string;
7
+ export declare function genUiTab(names: PluginNames): string;
8
+ export declare function genBackendIndex(names: PluginNames): string;
9
+ export declare function genBackendAction(names: PluginNames): string;
10
+ export declare function genBackendRoutes(names: PluginNames): string;
11
+ export declare function genFullstackUiIndex(names: PluginNames): string;
12
+ export declare function genFullstackCard(names: PluginNames): string;
13
+ export declare function genFullstackBackendIndex(names: PluginNames): string;
14
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAgCxE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAmD3E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6BpD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAwEtE;AAID,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAmBrD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAiCnD;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA6B1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAuD3D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA4B3D;AAID,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAa9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA4C3D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CA0BnE"}
@@ -0,0 +1,425 @@
1
+ // ─── Shared templates ────────────────────────────────────────────────────────
2
+ export function genManifest(names, type) {
3
+ const capabilities = {};
4
+ if (type === 'ui' || type === 'fullstack') {
5
+ capabilities['ui'] = { entityTabs: [`${names.pluginId}-tab`] };
6
+ }
7
+ if (type === 'backend' || type === 'fullstack') {
8
+ capabilities['backend'] = {
9
+ routes: ['/status'],
10
+ actionProviders: [`${names.pluginId}.myAction@v1`],
11
+ };
12
+ }
13
+ return JSON.stringify({
14
+ name: names.packageName,
15
+ version: '1.0.0',
16
+ forgeportal: {
17
+ engineVersion: '^1.0.0',
18
+ type,
19
+ capabilities,
20
+ config: {
21
+ apiEndpoint: {
22
+ type: 'string',
23
+ description: `${names.title} API endpoint`,
24
+ required: false,
25
+ },
26
+ },
27
+ },
28
+ }, null, 2);
29
+ }
30
+ export function genPackageJson(names, type) {
31
+ const hasUi = type === 'ui' || type === 'fullstack';
32
+ const hasBackend = type === 'backend' || type === 'fullstack';
33
+ const peerDeps = {
34
+ '@forgeportal/plugin-sdk': '^1.0.0',
35
+ };
36
+ const devDeps = {
37
+ '@forgeportal/plugin-sdk': '^1.0.0',
38
+ '@types/node': '^22',
39
+ typescript: '^5',
40
+ };
41
+ if (hasUi) {
42
+ peerDeps['react'] = '>=19';
43
+ devDeps['@types/react'] = '^19';
44
+ devDeps['react'] = '^19';
45
+ }
46
+ if (hasBackend) {
47
+ devDeps['fastify'] = '^5';
48
+ devDeps['@types/node'] = '^22';
49
+ }
50
+ const exports = {
51
+ '.': { types: './dist/index.d.ts', import: './dist/index.js' },
52
+ };
53
+ if (type === 'fullstack') {
54
+ exports['./ui'] = { types: './dist/ui/index.d.ts', import: './dist/ui/index.js' };
55
+ exports['./backend'] = { types: './dist/backend/index.d.ts', import: './dist/backend/index.js' };
56
+ }
57
+ return JSON.stringify({
58
+ name: names.packageName,
59
+ version: '1.0.0',
60
+ description: `ForgePortal ${type} plugin — ${names.title}`,
61
+ type: 'module',
62
+ main: 'dist/index.js',
63
+ types: 'dist/index.d.ts',
64
+ exports,
65
+ scripts: {
66
+ build: 'tsc',
67
+ dev: 'tsc --watch',
68
+ lint: 'eslint src/',
69
+ },
70
+ peerDependencies: peerDeps,
71
+ devDependencies: devDeps,
72
+ }, null, 2);
73
+ }
74
+ export function genTsConfig(type) {
75
+ const hasUi = type === 'ui' || type === 'fullstack';
76
+ const compilerOptions = {
77
+ target: 'ES2022',
78
+ module: 'NodeNext',
79
+ moduleResolution: 'NodeNext',
80
+ strict: true,
81
+ esModuleInterop: true,
82
+ skipLibCheck: true,
83
+ declaration: true,
84
+ declarationMap: true,
85
+ sourceMap: true,
86
+ outDir: 'dist',
87
+ rootDir: 'src',
88
+ };
89
+ if (hasUi) {
90
+ compilerOptions['jsx'] = 'react-jsx';
91
+ }
92
+ return JSON.stringify({
93
+ compilerOptions,
94
+ include: ['src'],
95
+ exclude: ['node_modules', 'dist'],
96
+ }, null, 2);
97
+ }
98
+ export function genReadme(names, type) {
99
+ const uiSection = type === 'ui' || type === 'fullstack'
100
+ ? `**UI** — add to \`apps/ui/src/plugins/index.ts\`:
101
+ \`\`\`typescript
102
+ import { registerPlugin } from '${names.packageName}${type === 'fullstack' ? '/ui' : ''}';
103
+ registerPluginById('${names.pluginId}', registerPlugin);
104
+ \`\`\``
105
+ : '';
106
+ const backendSection = type === 'backend' || type === 'fullstack'
107
+ ? `**Backend** — add to \`forgeportal.yaml\`:
108
+ \`\`\`yaml
109
+ pluginPackages:
110
+ packages:
111
+ - "${names.packageName}"
112
+ \`\`\``
113
+ : '';
114
+ return `# ${names.packageName}
115
+
116
+ > ForgePortal ${type} plugin — ${names.title}
117
+
118
+ ## Overview
119
+
120
+ This plugin was generated with [create-forge-plugin](https://github.com/your-org/forgeportal).
121
+
122
+ ## Plugin Type
123
+
124
+ **${type}** — ${type === 'ui' ? 'Provides UI components (entity tabs, cards, routes).' :
125
+ type === 'backend' ? 'Provides backend routes and action providers.' :
126
+ 'Provides both UI components and backend capabilities.'}
127
+
128
+ ## Installation
129
+
130
+ In your ForgePortal monorepo:
131
+
132
+ \`\`\`bash
133
+ pnpm add ${names.packageName}
134
+ \`\`\`
135
+
136
+ Then register the plugin:
137
+
138
+ ${uiSection}
139
+
140
+ ${backendSection}
141
+
142
+ ## Configuration
143
+
144
+ Add to \`forgeportal.yaml\`:
145
+
146
+ \`\`\`yaml
147
+ plugins:
148
+ ${names.pluginId}:
149
+ enabled: true
150
+ config:
151
+ apiEndpoint: "https://your-service.example.com"
152
+ \`\`\`
153
+
154
+ ## Development
155
+
156
+ \`\`\`bash
157
+ pnpm install
158
+ pnpm build # compile TypeScript
159
+ pnpm dev # watch mode
160
+ \`\`\`
161
+
162
+ ## License
163
+
164
+ MIT
165
+ `;
166
+ }
167
+ // ─── UI plugin templates ─────────────────────────────────────────────────────
168
+ export function genUiIndex(names) {
169
+ return `import type { ForgePluginSDK } from '@forgeportal/plugin-sdk';
170
+ import { ${names.pascalName}Tab } from './${names.pascalName}Tab.js';
171
+
172
+ /**
173
+ * Register this plugin's UI capabilities with ForgePortal.
174
+ * Called at UI startup by apps/ui/src/plugins/index.ts.
175
+ *
176
+ * To register: registerPluginById('${names.pluginId}', registerPlugin)
177
+ */
178
+ export function registerPlugin(sdk: ForgePluginSDK): void {
179
+ sdk.registerEntityTab({
180
+ id: '${names.pluginId}-tab',
181
+ title: '${names.title}',
182
+ component: ${names.pascalName}Tab,
183
+ appliesTo: { kinds: ['service'] }, // Adjust to your target entity kinds
184
+ });
185
+ }
186
+ `;
187
+ }
188
+ export function genUiTab(names) {
189
+ return `import { useEntity, useConfig } from '@forgeportal/plugin-sdk/react';
190
+
191
+ /**
192
+ * ${names.title} entity tab.
193
+ * Rendered inside the entity detail page for entities of matching kinds.
194
+ *
195
+ * useEntity() — access the current catalog entity
196
+ * useConfig<T>(key) — access plugin config from forgeportal.yaml
197
+ */
198
+ export function ${names.pascalName}Tab() {
199
+ const { entity } = useEntity();
200
+ const apiEndpoint = useConfig<string>('apiEndpoint');
201
+
202
+ return (
203
+ <div className="p-4">
204
+ <h3 className="text-lg font-semibold mb-3">${names.title}</h3>
205
+
206
+ <div className="rounded-lg border border-gray-200 p-4 text-sm text-gray-600">
207
+ <p>Entity: <strong>{entity.name}</strong> ({entity.kind})</p>
208
+ {apiEndpoint && (
209
+ <p className="mt-1 text-xs text-gray-400">API: {apiEndpoint}</p>
210
+ )}
211
+ </div>
212
+
213
+ {/* TODO: Implement your plugin UI here */}
214
+ <p className="mt-4 text-sm text-gray-400 italic">
215
+ This is a generated placeholder — implement ${names.title} content above.
216
+ </p>
217
+ </div>
218
+ );
219
+ }
220
+ `;
221
+ }
222
+ // ─── Backend plugin templates ─────────────────────────────────────────────────
223
+ export function genBackendIndex(names) {
224
+ return `import type { ForgeBackendPluginSDK } from '@forgeportal/plugin-sdk';
225
+ import { ${names.camelName}Action } from './actions/${names.camelName}Action.js';
226
+
227
+ /**
228
+ * Register this plugin's backend capabilities with ForgePortal.
229
+ * Called at API server startup by apps/api/src/plugins/plugin-loader.ts.
230
+ *
231
+ * To register: add "${names.packageName}" to pluginPackages.packages in forgeportal.yaml
232
+ */
233
+ export function registerBackendPlugin(sdk: ForgeBackendPluginSDK): void {
234
+ // Register action providers (available in action runner and templates)
235
+ sdk.registerActionProvider(${names.camelName}Action);
236
+
237
+ // Register backend routes (mounted at /api/v1/plugins/${names.pluginId}/)
238
+ sdk.registerBackendRoute({
239
+ path: '/status',
240
+ async handler(fastify) {
241
+ fastify.get('/', async () => ({
242
+ status: 'ok',
243
+ plugin: '${names.pluginId}',
244
+ }));
245
+
246
+ // TODO: Add more routes here
247
+ // Example: fastify.get('/data/:entityId', async (request) => { ... });
248
+ },
249
+ });
250
+ }
251
+ `;
252
+ }
253
+ export function genBackendAction(names) {
254
+ return `import type { ActionProvider } from '@forgeportal/plugin-sdk';
255
+
256
+ /**
257
+ * ${names.title} action provider.
258
+ * Registered as action ID: "${names.pluginId}.myAction@v1"
259
+ *
260
+ * Available in:
261
+ * - Action runner (POST /api/v1/actions/run)
262
+ * - Template steps (action: "${names.pluginId}.myAction@v1")
263
+ */
264
+ export const ${names.camelName}Action: ActionProvider = {
265
+ id: '${names.pluginId}.myAction',
266
+ version: 'v1',
267
+ schema: {
268
+ input: {
269
+ type: 'object',
270
+ properties: {
271
+ message: {
272
+ type: 'string',
273
+ title: 'Message',
274
+ description: 'The message to process',
275
+ },
276
+ },
277
+ required: ['message'],
278
+ },
279
+ output: {
280
+ type: 'object',
281
+ properties: {
282
+ result: { type: 'string', description: 'Action result' },
283
+ },
284
+ },
285
+ },
286
+
287
+ async handler(ctx, input) {
288
+ ctx.logger.info('Running ${names.pluginId}.myAction', { input });
289
+ await ctx.log('info', \`Processing: \${String(input['message'])}\`);
290
+
291
+ // Available context:
292
+ // ctx.config.get<string>('apiEndpoint') — plugin config
293
+ // ctx.scm.getFile(repoUrl, path) — SCM file access (read-only)
294
+ // ctx.db.query(sql, params) — DB read-only queries
295
+ // ctx.acquireRepoLock(repoUrl) — prevent concurrent SCM writes
296
+
297
+ // TODO: Implement your action logic here
298
+ const result = \`Processed: \${String(input['message'])}\`;
299
+
300
+ return {
301
+ status: 'success',
302
+ outputs: { result },
303
+ links: [],
304
+ };
305
+ },
306
+ };
307
+ `;
308
+ }
309
+ export function genBackendRoutes(names) {
310
+ return `import type { FastifyInstance } from 'fastify';
311
+
312
+ /**
313
+ * Backend routes for the ${names.title} plugin.
314
+ * Mounted at: /api/v1/plugins/${names.pluginId}/
315
+ *
316
+ * Import and use this in registerBackendPlugin:
317
+ * sdk.registerBackendRoute({ path: '/...', handler: registerRoutes });
318
+ */
319
+ export async function registerRoutes(fastify: FastifyInstance): Promise<void> {
320
+ // GET /api/v1/plugins/${names.pluginId}/status
321
+ fastify.get('/status', async () => ({
322
+ status: 'ok',
323
+ plugin: '${names.pluginId}',
324
+ }));
325
+
326
+ // TODO: Add your plugin routes here
327
+ // All routes in this function are automatically scoped to /api/v1/plugins/${names.pluginId}/
328
+ //
329
+ // Example:
330
+ // fastify.get('/data/:entityId', async (request) => {
331
+ // const { entityId } = request.params as { entityId: string };
332
+ // // fetch from your service ...
333
+ // return { entityId, data: [...] };
334
+ // });
335
+ }
336
+ `;
337
+ }
338
+ // ─── Fullstack plugin templates ───────────────────────────────────────────────
339
+ export function genFullstackUiIndex(names) {
340
+ return `import type { ForgePluginSDK } from '@forgeportal/plugin-sdk';
341
+ import { ${names.pascalName}Card } from './${names.pascalName}Card.js';
342
+
343
+ export function registerPlugin(sdk: ForgePluginSDK): void {
344
+ sdk.registerEntityCard({
345
+ id: '${names.pluginId}-card',
346
+ title: '${names.title}',
347
+ component: ${names.pascalName}Card,
348
+ appliesTo: { kinds: ['service'] },
349
+ });
350
+ }
351
+ `;
352
+ }
353
+ export function genFullstackCard(names) {
354
+ return `import { useEntity, useApi } from '@forgeportal/plugin-sdk/react';
355
+
356
+ interface ${names.pascalName}Data {
357
+ // TODO: Define the shape of data returned by your backend route
358
+ message: string;
359
+ }
360
+
361
+ /**
362
+ * ${names.title} entity card.
363
+ * Fetches data from the backend route: GET /api/v1/plugins/${names.pluginId}/data/:entityId
364
+ */
365
+ export function ${names.pascalName}Card() {
366
+ const { entity } = useEntity();
367
+ const { data, isPending, isError } = useApi<${names.pascalName}Data>(
368
+ \`/api/v1/plugins/${names.pluginId}/data/\${entity.id}\`
369
+ );
370
+
371
+ if (isPending) {
372
+ return (
373
+ <div className="p-3 text-sm text-gray-400 animate-pulse">
374
+ Loading ${names.title} data\u2026
375
+ </div>
376
+ );
377
+ }
378
+
379
+ if (isError || !data) {
380
+ return (
381
+ <div className="p-3 text-sm text-red-500">
382
+ Failed to load ${names.title} data.
383
+ </div>
384
+ );
385
+ }
386
+
387
+ return (
388
+ <div className="p-3 text-sm text-gray-700">
389
+ {/* TODO: Render your plugin data here */}
390
+ <pre className="text-xs bg-gray-50 rounded p-2 overflow-auto">
391
+ {JSON.stringify(data, null, 2)}
392
+ </pre>
393
+ </div>
394
+ );
395
+ }
396
+ `;
397
+ }
398
+ export function genFullstackBackendIndex(names) {
399
+ return `import type { ForgeBackendPluginSDK } from '@forgeportal/plugin-sdk';
400
+
401
+ export function registerBackendPlugin(sdk: ForgeBackendPluginSDK): void {
402
+ sdk.registerBackendRoute({
403
+ path: '/',
404
+ async handler(fastify) {
405
+ // GET /api/v1/plugins/${names.pluginId}/data/:entityId
406
+ fastify.get('/data/:entityId', async (request) => {
407
+ const { entityId } = request.params as { entityId: string };
408
+
409
+ // TODO: Fetch data from your external service
410
+ return {
411
+ entityId,
412
+ message: 'Hello from ${names.title} plugin!',
413
+ };
414
+ });
415
+
416
+ fastify.get('/status', async () => ({
417
+ status: 'ok',
418
+ plugin: '${names.pluginId}',
419
+ }));
420
+ },
421
+ });
422
+ }
423
+ `;
424
+ }
425
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "create-forge-plugin",
3
+ "version": "1.0.0",
4
+ "description": "Scaffolder CLI for ForgePortal plugins — generate a new plugin in under 2 minutes.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-forge-plugin": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "dependencies": {
19
+ "@clack/prompts": "^0.9.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22",
23
+ "tsx": "*",
24
+ "vitest": "*"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "keywords": [
30
+ "forgeportal",
31
+ "plugin",
32
+ "scaffolder",
33
+ "cli",
34
+ "idp"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "dev": "tsx src/index.ts",
39
+ "test": "vitest run",
40
+ "lint": "eslint src/",
41
+ "clean": "rm -rf dist"
42
+ }
43
+ }