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 +165 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +683 -0
- package/package.json +28 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|