create-omniflow-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # create-omniflow-plugin
2
+
3
+ Interactive CLI scaffolder for [OmniFlow](https://github.com/agnistack/omniflow) plugins.
4
+
5
+ Generates a ready-to-build Gradle project with a Java ingestor, optional Next.js micro UI, and all the wiring needed to upload the plugin as a JAR to a running OmniFlow backend.
6
+
7
+ ## Usage
8
+
9
+ Run directly with `npx` (no install needed):
10
+
11
+ ```sh
12
+ npx create-omniflow-plugin
13
+ ```
14
+
15
+ Or with `pnpm`/`yarn`:
16
+
17
+ ```sh
18
+ pnpm dlx create-omniflow-plugin
19
+ yarn dlx create-omniflow-plugin
20
+ ```
21
+
22
+ The CLI will ask a few questions and scaffold a new directory named after your plugin ID.
23
+
24
+ ### Prompts
25
+
26
+ | Prompt | Description |
27
+ |---|---|
28
+ | Plugin ID | Lowercase, hyphen-separated identifier (e.g. `gradle-build-scan`) |
29
+ | Display name | Human-readable name shown in the OmniFlow UI |
30
+ | Description | Short description of what the plugin ingests |
31
+ | Author | Your name or organisation |
32
+ | Java base package | Root Java package (e.g. `io.github.acme.plugins.myingestor`) |
33
+ | Ingestor type key | Used in API paths — `/api/ingest/{type}` |
34
+ | Include Next.js UI? | Whether to scaffold a micro frontend |
35
+ | OmniFlow plugin-api version | Version of `omniflow-plugin-api` to depend on |
36
+ | OmniFlow API base URL | Backend URL for local UI dev (e.g. `http://localhost:8080`) |
37
+
38
+ ## What gets generated
39
+
40
+ ```
41
+ <plugin-id>/
42
+ ├── build.gradle # Gradle build — Java + optional node-gradle plugin
43
+ ├── settings.gradle
44
+ ├── gradlew / gradlew.bat # Wrapper stubs (run `gradle wrapper` for the real ones)
45
+ ├── src/main/java/…/
46
+ │ ├── <Name>Plugin.java # OmniflowPlugin implementation
47
+ │ └── <Name>Ingestor.java # PluginIngestor implementation
48
+ └── ui/ # Only when "Include Next.js UI?" = yes
49
+ ├── package.json
50
+ ├── tsconfig.json
51
+ ├── next.config.ts
52
+ ├── tailwind.config.ts
53
+ ├── postcss.config.mjs
54
+ ├── eslint.config.mjs
55
+ ├── .env.local
56
+ ├── app/
57
+ │ ├── globals.css
58
+ │ ├── layout.tsx
59
+ │ └── page.tsx
60
+ ├── components/
61
+ │ └── ThemeSync.tsx
62
+ └── lib/
63
+ └── api.ts
64
+ ```
65
+
66
+ ## Build
67
+
68
+ ### Prerequisites
69
+
70
+ - **Java 25+** and **Gradle** on your `PATH`
71
+ - **Node.js 22+** (only required if you included the UI — node-gradle will download it automatically during `./gradlew jar`)
72
+
73
+ ### Build the JAR
74
+
75
+ ```sh
76
+ cd <plugin-id>
77
+ gradle wrapper # generate the real Gradle wrapper (one-time)
78
+ ./gradlew jar
79
+ ```
80
+
81
+ The fat JAR is written to `build/libs/<plugin-id>-1.0.0.jar`. It includes all runtime dependencies and, if you chose to include a UI, the compiled Next.js static export embedded as resources.
82
+
83
+ To skip the UI build during development:
84
+
85
+ ```sh
86
+ ./gradlew jar -PskipUi
87
+ ```
88
+
89
+ ## Upload to OmniFlow
90
+
91
+ ```sh
92
+ curl -X POST http://localhost:8080/api/plugins/upload \
93
+ -b "JSESSIONID=<your-session>" \
94
+ -F "file=@build/libs/<plugin-id>-1.0.0.jar"
95
+ ```
96
+
97
+ Replace `http://localhost:8080` and the session cookie with your actual backend URL and credentials.
98
+
99
+ ## Develop the UI locally
100
+
101
+ When you include a Next.js UI you can run it standalone against a live OmniFlow backend — no JAR build required:
102
+
103
+ ```sh
104
+ cd <plugin-id>/ui
105
+ npm install
106
+ npm run dev # starts on http://localhost:3000
107
+ ```
108
+
109
+ The `NEXT_PUBLIC_API_URL` variable in `.env.local` controls which OmniFlow backend the UI talks to.
110
+
111
+ ---
112
+
113
+ ## Contributing to `create-omniflow-plugin`
114
+
115
+ ### Prerequisites
116
+
117
+ - Node.js 18+
118
+ - npm / pnpm
119
+
120
+ ### Install dependencies
121
+
122
+ ```sh
123
+ npm install
124
+ ```
125
+
126
+ ### Build
127
+
128
+ ```sh
129
+ npm run build
130
+ ```
131
+
132
+ Compiled output lands in `dist/`.
133
+
134
+ ### Watch mode (rebuild on save)
135
+
136
+ ```sh
137
+ npm run dev
138
+ ```
139
+
140
+ ### Test locally
141
+
142
+ ```sh
143
+ node dist/index.js
144
+ ```
145
+
146
+ Or link it globally so `create-omniflow-plugin` works in any directory:
147
+
148
+ ```sh
149
+ npm link
150
+ create-omniflow-plugin
151
+ ```
152
+
153
+ ## Publish to npm
154
+
155
+ 1. Bump the version in `package.json`.
156
+ 2. Build:
157
+ ```sh
158
+ npm run build
159
+ ```
160
+ 3. Publish:
161
+ ```sh
162
+ npm publish --access public
163
+ ```
164
+
165
+ The `files` field in `package.json` ensures only `dist/` is included in the published package.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,683 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p from "@clack/prompts";
5
+ import pc from "picocolors";
6
+
7
+ // src/scaffold.ts
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ // src/templates/java.ts
12
+ function toPascalCase(id) {
13
+ return id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
14
+ }
15
+ function javaTemplates(a) {
16
+ const className = toPascalCase(a.pluginId);
17
+ const pkgPath = a.javaPackage.replace(/\./g, "/");
18
+ return {
19
+ // ── Gradle wrapper stub (real wrapper needs `gradle wrapper` to generate) ──
20
+ "gradlew": gradlew(),
21
+ "gradlew.bat": gradlewBat(),
22
+ "settings.gradle": settingsGradle(a),
23
+ "build.gradle": buildGradle(a, className),
24
+ // ── Java sources ──────────────────────────────────────────────────────────
25
+ [`src/main/java/${pkgPath}/${className}Plugin.java`]: pluginClass(a, className),
26
+ [`src/main/java/${pkgPath}/${className}Ingestor.java`]: ingestorClass(a, className),
27
+ // ── CI / ingestor scripts ─────────────────────────────────────────────────
28
+ "scripts/ingest.sh": ingestScript(a),
29
+ "scripts/upload-plugin.sh": uploadPluginScript(a)
30
+ };
31
+ }
32
+ function settingsGradle(a) {
33
+ return `rootProject.name = '${a.pluginId}'
34
+ `;
35
+ }
36
+ function buildGradle(a, className) {
37
+ const uiBlock = a.hasUi ? `
38
+ import com.github.gradle.node.npm.task.NpmTask
39
+
40
+ node {
41
+ version = '22.14.0'
42
+ npmVersion = '10.9.2'
43
+ download = true
44
+ nodeProjectDir = file("\${projectDir}/ui")
45
+ }
46
+
47
+ def uiOutDir = file("\${projectDir}/ui/out")
48
+ def uiResourceDir = file("\${projectDir}/src/main/resources/static-ui")
49
+ def skipUi = { project.hasProperty('skipUi') }
50
+
51
+ tasks.named('nodeSetup') { onlyIf { !skipUi() } }
52
+ tasks.named('npmSetup') { onlyIf { !skipUi() } }
53
+ tasks.named('npmInstall') { onlyIf { !skipUi() } }
54
+
55
+ tasks.register('buildUi', NpmTask) {
56
+ dependsOn tasks.named('npmInstall')
57
+ onlyIf { !skipUi() }
58
+ args = ['run', 'build']
59
+ environment = ['NEXT_BUILD_FOR_JAR': '1']
60
+ inputs.dir("\${projectDir}/ui/app")
61
+ inputs.dir("\${projectDir}/ui/components")
62
+ inputs.dir("\${projectDir}/ui/lib")
63
+ inputs.file("\${projectDir}/ui/next.config.ts")
64
+ outputs.dir(uiOutDir)
65
+ }
66
+
67
+ tasks.register('copyUiAssets', Copy) {
68
+ dependsOn tasks.named('buildUi')
69
+ onlyIf { !skipUi() }
70
+ from uiOutDir
71
+ into uiResourceDir
72
+ }
73
+
74
+ processResources.dependsOn tasks.named('copyUiAssets')
75
+
76
+ clean {
77
+ delete uiResourceDir
78
+ }
79
+ ` : "";
80
+ const plugins = a.hasUi ? ` id 'java'
81
+ id 'com.github.node-gradle.node' version '7.1.0'` : ` id 'java'`;
82
+ return `${a.hasUi ? "import com.github.gradle.node.npm.task.NpmTask\n\n" : ""}plugins {
83
+ ${plugins}
84
+ }
85
+
86
+ group = 'io.github.omniflow.plugins'
87
+ version = '1.0.0'
88
+ description = '${a.description}'
89
+
90
+ java {
91
+ toolchain {
92
+ languageVersion = JavaLanguageVersion.of(25)
93
+ }
94
+ }
95
+
96
+ repositories {
97
+ mavenCentral()
98
+ mavenLocal()
99
+ }
100
+
101
+ dependencies {
102
+ compileOnly 'io.github.agnistack:omniflow-plugin-api:${a.omniflowVersion}'
103
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
104
+ }
105
+ ${uiBlock}
106
+ jar {
107
+ archiveBaseName = '${a.pluginId}'
108
+ from {
109
+ configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
110
+ }
111
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
112
+ manifest {
113
+ attributes(
114
+ 'Plugin-Id' : '${a.pluginId}',
115
+ 'Plugin-Version': project.version,
116
+ 'Plugin-Class' : '${a.javaPackage}.${className}Plugin'
117
+ )
118
+ }
119
+ }
120
+ `;
121
+ }
122
+ function pluginClass(a, className) {
123
+ return `package ${a.javaPackage};
124
+
125
+ import io.github.agnistack.omniflow.pluginapi.*;
126
+ import java.util.List;
127
+
128
+ public class ${className}Plugin implements OmniflowPlugin {
129
+
130
+ @Override
131
+ public PluginMetadata metadata() {
132
+ return PluginMetadata.builder()
133
+ .id("${a.pluginId}")
134
+ .name("${a.pluginName}")
135
+ .version("1.0.0")
136
+ .description("${a.description}")
137
+ .author("${a.author}")
138
+ .build();
139
+ }
140
+
141
+ @Override
142
+ public List<PluginIngestor<?>> ingestors() {
143
+ return List.of(new ${className}Ingestor());
144
+ }
145
+
146
+ @Override
147
+ public List<PluginAction> actions() {
148
+ return List.of();
149
+ }
150
+
151
+ @Override
152
+ public boolean hasUi() {
153
+ return ${a.hasUi};
154
+ }
155
+
156
+ @Override
157
+ public void onLoad(PluginContext context) {
158
+ // Called when the plugin is loaded \u2014 register schemas, etc.
159
+ }
160
+
161
+ @Override
162
+ public void onUnload() {
163
+ // Called when the plugin is unloaded \u2014 release resources.
164
+ }
165
+ }
166
+ `;
167
+ }
168
+ function ingestorClass(a, className) {
169
+ return `package ${a.javaPackage};
170
+
171
+ import com.fasterxml.jackson.databind.ObjectMapper;
172
+ import io.github.agnistack.omniflow.pluginapi.*;
173
+ import java.io.InputStream;
174
+ import java.time.Instant;
175
+ import java.util.Map;
176
+ import java.util.UUID;
177
+
178
+ public class ${className}Ingestor implements PluginIngestor<PluginDataRecord> {
179
+
180
+ private static final ObjectMapper MAPPER = new ObjectMapper();
181
+
182
+ @Override
183
+ public String getType() {
184
+ return "${a.ingestorType}";
185
+ }
186
+
187
+ @Override
188
+ public PluginDataRecord ingest(InputStream data) throws Exception {
189
+ // TODO: parse your data format here
190
+ @SuppressWarnings("unchecked")
191
+ Map<String, Object> parsed = MAPPER.readValue(data, Map.class);
192
+
193
+ return PluginDataRecord.builder()
194
+ .id(UUID.randomUUID().toString())
195
+ .type(getType())
196
+ .timestamp(Instant.now())
197
+ .fields(parsed)
198
+ .build();
199
+ }
200
+ }
201
+ `;
202
+ }
203
+ function gradlew() {
204
+ return `#!/bin/sh
205
+ # Run: gradle wrapper \u2014 to generate the real Gradle wrapper files
206
+ # Or install Gradle and run: gradle <task>
207
+ exec gradle "$@"
208
+ `;
209
+ }
210
+ function gradlewBat() {
211
+ return `@rem Run: gradle wrapper \u2014 to generate the real Gradle wrapper files
212
+ @rem Or install Gradle and run: gradle <task>
213
+ @gradle %*
214
+ `;
215
+ }
216
+ function ingestScript(a) {
217
+ return `#!/usr/bin/env bash
218
+ # Ingest a JSON file into OmniFlow as type "${a.ingestorType}".
219
+ #
220
+ # Usage:
221
+ # ./scripts/ingest.sh data.json
222
+ # ./scripts/ingest.sh data.json https://omniflow.example.com
223
+ #
224
+ # Set OMNIFLOW_API_KEY in your environment or CI secrets.
225
+
226
+ set -euo pipefail
227
+
228
+ FILE=\${1:?Usage: $0 <data.json> [api-url]}
229
+ API_URL=\${2:-${a.apiUrl}}
230
+ API_KEY=\${OMNIFLOW_API_KEY:?Set OMNIFLOW_API_KEY environment variable}
231
+
232
+ echo "Ingesting \${FILE} as type '${a.ingestorType}'..."
233
+
234
+ HTTP_STATUS=$(curl -s -o /tmp/ingest_response.json -w "%{http_code}" \\
235
+ -X POST "\${API_URL}/api/ingest/${a.ingestorType}" \\
236
+ -H "X-Api-Key: \${API_KEY}" \\
237
+ -H "Content-Type: application/json" \\
238
+ --data-binary "@\${FILE}")
239
+
240
+ if [ "\${HTTP_STATUS}" -ge 200 ] && [ "\${HTTP_STATUS}" -lt 300 ]; then
241
+ echo "Success (\${HTTP_STATUS}):"
242
+ cat /tmp/ingest_response.json
243
+ else
244
+ echo "Failed (\${HTTP_STATUS}):"
245
+ cat /tmp/ingest_response.json
246
+ exit 1
247
+ fi
248
+ `;
249
+ }
250
+ function uploadPluginScript(a) {
251
+ return `#!/usr/bin/env bash
252
+ # Build and upload the plugin JAR to a running OmniFlow backend.
253
+ #
254
+ # Usage:
255
+ # ./scripts/upload-plugin.sh
256
+ # ./scripts/upload-plugin.sh https://omniflow.example.com
257
+ #
258
+ # Set OMNIFLOW_API_KEY in your environment or CI secrets.
259
+
260
+ set -euo pipefail
261
+
262
+ API_URL=\${1:-${a.apiUrl}}
263
+ API_KEY=\${OMNIFLOW_API_KEY:?Set OMNIFLOW_API_KEY environment variable}
264
+ JAR="build/libs/${a.pluginId}-1.0.0.jar"
265
+
266
+ echo "Building JAR..."
267
+ ./gradlew jar
268
+
269
+ echo "Uploading \${JAR} to \${API_URL}..."
270
+
271
+ HTTP_STATUS=$(curl -s -o /tmp/upload_response.json -w "%{http_code}" \\
272
+ -X POST "\${API_URL}/api/plugins/upload" \\
273
+ -H "X-Api-Key: \${API_KEY}" \\
274
+ -F "file=@\${JAR}")
275
+
276
+ if [ "\${HTTP_STATUS}" -ge 200 ] && [ "\${HTTP_STATUS}" -lt 300 ]; then
277
+ echo "Plugin uploaded successfully (\${HTTP_STATUS}):"
278
+ cat /tmp/upload_response.json
279
+ else
280
+ echo "Upload failed (\${HTTP_STATUS}):"
281
+ cat /tmp/upload_response.json
282
+ exit 1
283
+ fi
284
+ `;
285
+ }
286
+
287
+ // src/templates/ui.ts
288
+ function uiTemplates(a) {
289
+ return {
290
+ "ui/package.json": uiPackageJson(a),
291
+ "ui/tsconfig.json": uiTsConfig(),
292
+ "ui/next.config.ts": uiNextConfig(a),
293
+ "ui/tailwind.config.ts": uiTailwindConfig(),
294
+ "ui/postcss.config.mjs": uiPostcss(),
295
+ "ui/eslint.config.mjs": uiEslintConfig(),
296
+ "ui/.env.local": uiEnvLocal(a),
297
+ "ui/app/globals.css": uiGlobalsCss(),
298
+ "ui/app/layout.tsx": uiLayout(a),
299
+ "ui/app/page.tsx": uiPage(a),
300
+ "ui/components/ThemeSync.tsx": uiThemeSync(),
301
+ "ui/lib/api.ts": uiApi(a)
302
+ };
303
+ }
304
+ function uiPackageJson(a) {
305
+ return JSON.stringify(
306
+ {
307
+ name: `${a.pluginId}-ui`,
308
+ version: "1.0.0",
309
+ private: true,
310
+ scripts: {
311
+ dev: "next dev",
312
+ build: "next build",
313
+ start: "next start",
314
+ lint: "next lint"
315
+ },
316
+ dependencies: {
317
+ "@omniflow/ui": "latest",
318
+ next: "16.2.1",
319
+ react: "19.2.4",
320
+ "react-dom": "19.2.4",
321
+ swr: "^2.3.3"
322
+ },
323
+ devDependencies: {
324
+ "@types/node": "^22",
325
+ "@types/react": "19.2.14",
326
+ "@types/react-dom": "19.2.3",
327
+ autoprefixer: "^10.4.21",
328
+ eslint: "^9.39.4",
329
+ "eslint-config-next": "^16.2.1",
330
+ postcss: "^8.5.3",
331
+ tailwindcss: "^3.4.17",
332
+ typescript: "^5"
333
+ },
334
+ overrides: {
335
+ "@types/react": "19.2.14",
336
+ "@types/react-dom": "19.2.3"
337
+ }
338
+ },
339
+ null,
340
+ 2
341
+ ) + "\n";
342
+ }
343
+ function uiTsConfig() {
344
+ return JSON.stringify(
345
+ {
346
+ compilerOptions: {
347
+ target: "ES2017",
348
+ lib: ["dom", "dom.iterable", "esnext"],
349
+ allowJs: true,
350
+ skipLibCheck: true,
351
+ strict: true,
352
+ noEmit: true,
353
+ esModuleInterop: true,
354
+ module: "esnext",
355
+ moduleResolution: "bundler",
356
+ resolveJsonModule: true,
357
+ isolatedModules: true,
358
+ jsx: "preserve",
359
+ incremental: true,
360
+ plugins: [{ name: "next" }],
361
+ paths: { "@/*": ["./*"] }
362
+ },
363
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
364
+ exclude: ["node_modules"]
365
+ },
366
+ null,
367
+ 2
368
+ ) + "\n";
369
+ }
370
+ function uiNextConfig(a) {
371
+ return `import type { NextConfig } from 'next';
372
+
373
+ const forJar = process.env.NEXT_BUILD_FOR_JAR === '1';
374
+
375
+ const config: NextConfig = {
376
+ transpilePackages: ['@omniflow/ui'],
377
+ output: 'export',
378
+ // basePath and assetPrefix match the host's /api/plugins/{id}/ui/** route.
379
+ basePath: forJar ? '/api/plugins/${a.pluginId}/ui' : '',
380
+ assetPrefix: forJar ? '/api/plugins/${a.pluginId}/ui' : '',
381
+ images: { unoptimized: true },
382
+ trailingSlash: false,
383
+ };
384
+
385
+ export default config;
386
+ `;
387
+ }
388
+ function uiTailwindConfig() {
389
+ return `import type { Config } from 'tailwindcss';
390
+
391
+ const config: Config = {
392
+ content: [
393
+ './app/**/*.{ts,tsx}',
394
+ './components/**/*.{ts,tsx}',
395
+ './lib/**/*.{ts,tsx}',
396
+ './node_modules/@omniflow/ui/src/**/*.{ts,tsx}',
397
+ ],
398
+ darkMode: 'class',
399
+ theme: { extend: {} },
400
+ plugins: [],
401
+ };
402
+
403
+ export default config;
404
+ `;
405
+ }
406
+ function uiPostcss() {
407
+ return `const config = { plugins: { tailwindcss: {}, autoprefixer: {} } };
408
+ export default config;
409
+ `;
410
+ }
411
+ function uiEslintConfig() {
412
+ return `import nextCoreWebVitals from 'eslint-config-next/core-web-vitals';
413
+ import nextTypescript from 'eslint-config-next/typescript';
414
+
415
+ const eslintConfig = [
416
+ ...nextCoreWebVitals,
417
+ ...nextTypescript,
418
+ {
419
+ ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
420
+ },
421
+ ];
422
+
423
+ export default eslintConfig;
424
+ `;
425
+ }
426
+ function uiEnvLocal(a) {
427
+ return `# OmniFlow backend URL \u2014 used by the UI when running outside the JAR (npm run dev)
428
+ NEXT_PUBLIC_API_URL=${a.apiUrl}
429
+ `;
430
+ }
431
+ function uiGlobalsCss() {
432
+ return `@tailwind base;
433
+ @tailwind components;
434
+ @tailwind utilities;
435
+
436
+ :root {
437
+ color-scheme: light;
438
+ }
439
+
440
+ html.dark {
441
+ color-scheme: dark;
442
+ }
443
+
444
+ body {
445
+ @apply bg-gray-50 text-gray-900 dark:bg-slate-950 dark:text-slate-300;
446
+ }
447
+ `;
448
+ }
449
+ function uiLayout(a) {
450
+ return `import type { Metadata } from 'next';
451
+ import './globals.css';
452
+ import ThemeSync from '@/components/ThemeSync';
453
+
454
+ export const metadata: Metadata = { title: '${a.pluginName} \u2014 OmniFlow' };
455
+
456
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
457
+ return (
458
+ <html lang="en" suppressHydrationWarning>
459
+ <head>
460
+ <script dangerouslySetInnerHTML={{ __html: \`(function(){var p=new URLSearchParams(location.search).get('theme'),t=p||sessionStorage.getItem('omniflow-plugin-theme'),s=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';if(p)sessionStorage.setItem('omniflow-plugin-theme',p);if((t||s)==='dark')document.documentElement.classList.add('dark')})()\` }} />
461
+ </head>
462
+ <body className="min-h-screen flex flex-col">
463
+ <ThemeSync />
464
+ <header className="bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-800 px-6 py-3 flex items-center gap-3 flex-shrink-0">
465
+ <span className="text-sm font-bold text-gray-900 dark:text-slate-100">${a.pluginName}</span>
466
+ <span className="text-xs bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-900 px-2 py-0.5 rounded-full">OmniFlow Plugin</span>
467
+ </header>
468
+ <main className="flex-1 p-6">{children}</main>
469
+ </body>
470
+ </html>
471
+ );
472
+ }
473
+ `;
474
+ }
475
+ function uiPage(a) {
476
+ return `'use client';
477
+
478
+ import useSWR from 'swr';
479
+ import { EmptyState, cls } from '@omniflow/ui';
480
+ import { fetchRecords, type PluginRecord } from '@/lib/api';
481
+
482
+ export default function HomePage() {
483
+ const { data: records, isLoading } = useSWR<PluginRecord[]>(
484
+ 'records',
485
+ () => fetchRecords(50),
486
+ { refreshInterval: 30_000 },
487
+ );
488
+
489
+ if (isLoading) return <p className="text-gray-500 dark:text-slate-500 text-sm">Loading\u2026</p>;
490
+ if (!records?.length) return (
491
+ <EmptyState
492
+ title="No ${a.pluginName} data ingested yet."
493
+ description="POST data to /api/ingest/${a.ingestorType} to get started."
494
+ />
495
+ );
496
+
497
+ return (
498
+ <div className="max-w-4xl space-y-6">
499
+ {/* Records table */}
500
+ <div className={\`\${cls.card} p-4\`}>
501
+ <p className={\`\${cls.heading} mb-3\`}>Records</p>
502
+ <div className="overflow-x-auto">
503
+ <table className="w-full text-sm">
504
+ <thead>
505
+ <tr className={cls.table.header}>
506
+ <th className="text-left py-2 px-2">ID</th>
507
+ <th className="text-left py-2 px-2">Timestamp</th>
508
+ </tr>
509
+ </thead>
510
+ <tbody>
511
+ {records.map(r => (
512
+ <tr key={r.id} className={cls.table.row}>
513
+ <td className="py-1.5 px-2 font-mono text-gray-500 dark:text-slate-400">{r.id}</td>
514
+ <td className="py-1.5 px-2 text-gray-500 dark:text-slate-400">
515
+ {new Date(r.timestamp).toLocaleString()}
516
+ </td>
517
+ </tr>
518
+ ))}
519
+ </tbody>
520
+ </table>
521
+ </div>
522
+ </div>
523
+ </div>
524
+ );
525
+ }
526
+ `;
527
+ }
528
+ function uiThemeSync() {
529
+ return `'use client';
530
+
531
+ import { useEffect } from 'react';
532
+
533
+ export default function ThemeSync() {
534
+ useEffect(() => {
535
+ const param = new URLSearchParams(window.location.search).get('theme');
536
+ const stored = sessionStorage.getItem('omniflow-plugin-theme');
537
+ const theme = param ?? stored;
538
+ if (param) sessionStorage.setItem('omniflow-plugin-theme', param);
539
+ if (theme === 'dark') document.documentElement.classList.add('dark');
540
+ else if (theme === 'light') document.documentElement.classList.remove('dark');
541
+ else document.documentElement.classList.toggle('dark', window.matchMedia('(prefers-color-scheme: dark)').matches);
542
+ }, []);
543
+ return null;
544
+ }
545
+ `;
546
+ }
547
+ function uiApi(a) {
548
+ return `// The OmniFlow backend URL. Defaults to same origin in production (assets
549
+ // are served by the OmniFlow host at /api/plugins/${a.pluginId}/ui).
550
+ const BASE = process.env.NEXT_PUBLIC_API_URL ?? '';
551
+
552
+ async function get<T>(path: string): Promise<T> {
553
+ const res = await fetch(BASE + path);
554
+ if (!res.ok) throw new Error(\`\${res.status} \${res.statusText}\`);
555
+ return res.json() as Promise<T>;
556
+ }
557
+
558
+ // \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
559
+
560
+ // Extend PluginFields to match your ingestor's output shape.
561
+ export interface PluginFields {
562
+ [key: string]: unknown;
563
+ }
564
+
565
+ export interface PluginRecord {
566
+ id: string;
567
+ type: string;
568
+ timestamp: string;
569
+ fields: PluginFields;
570
+ }
571
+
572
+ // \u2500\u2500 API calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
573
+
574
+ export const fetchRecords = (limit = 50) =>
575
+ get<PluginRecord[]>(\`/api/analytics/builds?type=${a.ingestorType}&limit=\${limit}\`);
576
+ `;
577
+ }
578
+
579
+ // src/scaffold.ts
580
+ async function scaffold(answers) {
581
+ const outDir = path.resolve(process.cwd(), answers.pluginId);
582
+ const files = javaTemplates(answers);
583
+ if (answers.hasUi) {
584
+ Object.assign(files, uiTemplates(answers));
585
+ }
586
+ for (const [relPath, content] of Object.entries(files)) {
587
+ const abs = path.join(outDir, relPath);
588
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
589
+ fs.writeFileSync(abs, content, "utf8");
590
+ }
591
+ for (const script of ["gradlew", "scripts/ingest.sh", "scripts/upload-plugin.sh"]) {
592
+ const abs = path.join(outDir, script);
593
+ if (fs.existsSync(abs)) fs.chmodSync(abs, 493);
594
+ }
595
+ return answers.pluginId;
596
+ }
597
+
598
+ // src/index.ts
599
+ function toPascalCase2(id) {
600
+ return id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
601
+ }
602
+ async function main() {
603
+ console.log("");
604
+ p.intro(pc.bgMagenta(pc.white(" create-omniflow-plugin ")));
605
+ const answers = await p.group(
606
+ {
607
+ pluginId: () => p.text({
608
+ message: "Plugin ID",
609
+ placeholder: "my-ingestor",
610
+ validate: (v) => /^[a-z][a-z0-9-]*$/.test(v) ? void 0 : "Use lowercase letters, numbers and hyphens only"
611
+ }),
612
+ pluginName: ({ results }) => p.text({
613
+ message: "Plugin display name",
614
+ initialValue: toPascalCase2(results.pluginId ?? "").replace(/([A-Z])/g, " $1").trim()
615
+ }),
616
+ description: () => p.text({
617
+ message: "Description",
618
+ placeholder: "Ingests ... data into OmniFlow"
619
+ }),
620
+ author: () => p.text({
621
+ message: "Author",
622
+ placeholder: "Your Name"
623
+ }),
624
+ javaPackage: ({ results }) => p.text({
625
+ message: "Java base package",
626
+ initialValue: `io.github.omniflow.plugins.${(results.pluginId ?? "").replace(/-/g, "")}`
627
+ }),
628
+ ingestorType: ({ results }) => p.text({
629
+ message: "Ingestor type key (used in API paths, e.g. /api/ingest/{type})",
630
+ initialValue: results.pluginId
631
+ }),
632
+ hasUi: () => p.confirm({
633
+ message: "Include a Next.js micro UI?",
634
+ initialValue: true
635
+ }),
636
+ omniflowVersion: () => p.text({
637
+ message: "OmniFlow plugin-api version to depend on",
638
+ initialValue: "1.0.0"
639
+ }),
640
+ apiUrl: () => p.text({
641
+ message: "OmniFlow API base URL (for local dev)",
642
+ initialValue: "http://localhost:8080"
643
+ })
644
+ },
645
+ {
646
+ onCancel: () => {
647
+ p.cancel("Cancelled.");
648
+ process.exit(0);
649
+ }
650
+ }
651
+ );
652
+ const spinner2 = p.spinner();
653
+ spinner2.start("Scaffolding plugin\u2026");
654
+ const outDir = await scaffold(answers);
655
+ spinner2.stop("Done!");
656
+ const hasUi = answers.hasUi;
657
+ p.note(
658
+ [
659
+ `cd ${outDir}`,
660
+ "",
661
+ "# Build the JAR (includes UI if enabled):",
662
+ `./gradlew jar`,
663
+ "",
664
+ "# Upload to a running OmniFlow backend:",
665
+ `curl -X POST ${answers.apiUrl}/api/plugins/upload \\`,
666
+ ` -H "X-Api-Key: <your-api-key>" \\`,
667
+ ` -F "file=@build/libs/${answers.pluginId}-1.0.0.jar"`,
668
+ "",
669
+ "# Ingest data (see scripts/ingest.sh for a ready-made script):",
670
+ `curl -X POST ${answers.apiUrl}/api/ingest/${answers.ingestorType} \\`,
671
+ ` -H "X-Api-Key: <your-api-key>" \\`,
672
+ ` -H "Content-Type: application/json" \\`,
673
+ ` -d @data.json`,
674
+ ...hasUi ? ["", "# Dev UI standalone (no JAR needed):", "cd ui && npm install && npm run dev"] : []
675
+ ].join("\n"),
676
+ "Next steps"
677
+ );
678
+ p.outro(pc.green("Plugin scaffolded successfully!"));
679
+ }
680
+ main().catch((err) => {
681
+ console.error(pc.red("Error: ") + String(err));
682
+ process.exit(1);
683
+ });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-omniflow-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new OmniFlow plugin — Java ingestor/action + optional Next.js micro UI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-omniflow-plugin": "./dist/index.js"
9
+ },
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format esm --dts --clean",
13
+ "dev": "tsup src/index.ts --format esm --watch",
14
+ "start": "node dist/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^0.9.0",
18
+ "picocolors": "^1.1.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22",
22
+ "tsup": "^8",
23
+ "typescript": "^5"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }