@kestra-io/create-artifact-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # `@kestra-io/create-artifact-sdk`
2
+
3
+ CLI scaffolder for Kestra plugin custom UIs.
4
+
5
+ ## Usage
6
+
7
+ Run from your plugin's root directory (the one containing `settings.gradle` or `settings.gradle.kts`):
8
+
9
+ ```sh
10
+ npm create @kestra-io/artifact-sdk
11
+ ```
12
+
13
+ Or from inside the `ui/` directory:
14
+
15
+ ```sh
16
+ cd my-plugin/ui
17
+ npm create @kestra-io/artifact-sdk
18
+ ```
19
+
20
+ The CLI will:
21
+
22
+ 1. **Detect your plugin** — reads `settings.gradle[.kts]` to find the plugin group id (e.g. `io.kestra.plugin.redis`).
23
+ 2. **Ask which task** you want to add a custom UI for (e.g. `list.ListPop`).
24
+ 3. **Ask which UI module** to customize:
25
+ - `topology-details` — panel shown when a task node is selected in the topology view
26
+ - `log-details` — panel shown in the log view for a task execution
27
+ - `metrics-details` — panel shown in the metrics tab for a task execution
28
+ - `trigger-details` — panel shown when a trigger node is selected
29
+ 4. **Show a summary** and ask for confirmation before writing anything.
30
+ 5. **Scaffold the `ui/` directory** with:
31
+ - `package.json` (with all required dependencies)
32
+ - `vite.config.ts` (pre-configured with your plugin id and task)
33
+ - `src/main.ts` and `src/App.vue` for local development
34
+ - `src/components/<ComponentName>.vue` — the starter component to edit
35
+ - `src/<ComponentName>.stories.ts` — a Storybook story
36
+ - `.storybook/main.ts` and `.storybook/preview.ts`
37
+ - `index.html`, `tsconfig.json`, `.gitignore`
38
+ 6. **Run `npm install`** (optional, prompted).
39
+
40
+ ## After scaffolding
41
+
42
+ ```sh
43
+ cd ui
44
+
45
+ # Local dev — renders the component in a Vite app
46
+ npm run dev
47
+
48
+ # Storybook — develop and document in isolation
49
+ npm run storybook
50
+
51
+ # Build the UI module for bundling with the plugin
52
+ npm run build
53
+ ```
54
+
55
+ ## How component names are derived
56
+
57
+ The component filename is built from the task type and the UI module:
58
+
59
+ | Task type | UI module | Component name |
60
+ | ------------------ | ------------------ | ----------------------------------- |
61
+ | `list.ListPop` | `topology-details` | `ListListPopTopologyDetails.vue` |
62
+ | `io.Get` | `log-details` | `IoGetLogDetails.vue` |
63
+ | `trigger.Schedule` | `trigger-details` | `TriggerScheduleTriggerDetails.vue` |
64
+
65
+ ## Requirements
66
+
67
+ - Node.js ≥ 18
68
+ - Must be run from a directory containing `settings.gradle` or `settings.gradle.kts` (or a direct child of one)
package/bin/create.js ADDED
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ // oxlint-disable no-console
3
+ import {
4
+ intro,
5
+ outro,
6
+ text,
7
+ select,
8
+ confirm,
9
+ spinner,
10
+ log,
11
+ cancel,
12
+ note,
13
+ isCancel,
14
+ } from "@clack/prompts";
15
+ import pc from "picocolors";
16
+ import path from "node:path";
17
+ import fs from "node:fs";
18
+
19
+ import { detectContext } from "../src/context.js";
20
+ import { UI_MODULES, deriveComponentName } from "../src/modules.js";
21
+ import {
22
+ scaffoldPackageJson,
23
+ scaffoldMainTs,
24
+ scaffoldAppVue,
25
+ scaffoldViteConfig,
26
+ scaffoldComponent,
27
+ scaffoldIndexHtml,
28
+ scaffoldTsConfig,
29
+ scaffoldStorybookMain,
30
+ scaffoldStorybookPreview,
31
+ scaffoldStory,
32
+ scaffoldGitignore,
33
+ } from "../src/scaffold.js";
34
+ import { npmInstall } from "../src/runner.js";
35
+ import { findKestraTasks } from "../src/find-kestra-tasks.js";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function bail(message) {
42
+ cancel(message);
43
+ process.exit(1);
44
+ }
45
+
46
+ function checkCancel(value) {
47
+ if (isCancel(value)) bail("Operation cancelled.");
48
+ return value;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Main
53
+ // ---------------------------------------------------------------------------
54
+
55
+ async function main() {
56
+ console.log(); // breathing room
57
+ intro(pc.bgCyan(pc.black(" create-artifact-sdk ")));
58
+
59
+ // ── 1. Detect project context ─────────────────────────────────────────────
60
+
61
+ const ctx = detectContext(process.cwd());
62
+
63
+ if (!ctx.pluginRoot) {
64
+ log.warn(
65
+ "Could not find a " +
66
+ pc.bold("settings.gradle") +
67
+ " or " +
68
+ pc.bold("settings.gradle.kts") +
69
+ " file in the current directory or its parent.",
70
+ );
71
+ log.info(
72
+ "Run this command from your plugin's root directory (or the " +
73
+ pc.bold("ui/") +
74
+ " directory inside it).",
75
+ );
76
+ bail("Plugin root not found.");
77
+ }
78
+
79
+ log.info("Detected plugin root: " + pc.cyan(ctx.pluginRoot));
80
+
81
+ // ── 2. Resolve / confirm plugin id ───────────────────────────────────────
82
+
83
+ let pluginId = ctx.pluginId;
84
+
85
+ if (!pluginId) {
86
+ log.warn("Could not deduce a plugin group id from " + pc.bold(path.basename(ctx.gradleFile)));
87
+ pluginId = checkCancel(
88
+ await text({
89
+ message: "Enter the plugin group id (e.g. io.kestra.plugin.redis):",
90
+ validate: (v) =>
91
+ v.includes(".") ? undefined : "Must be a dotted id like io.kestra.plugin.myPlugin",
92
+ }),
93
+ );
94
+ } else {
95
+ const confirmed = checkCancel(
96
+ await confirm({
97
+ message: `Plugin id detected as ${pc.cyan(pluginId)} — is that correct?`,
98
+ initialValue: true,
99
+ }),
100
+ );
101
+ if (!confirmed) {
102
+ pluginId = checkCancel(
103
+ await text({
104
+ message: "Enter the correct plugin group id:",
105
+ validate: (v) =>
106
+ v.includes(".") ? undefined : "Must be a dotted id like io.kestra.plugin.myPlugin",
107
+ }),
108
+ );
109
+ }
110
+ }
111
+
112
+ // ── 3. Task type ──────────────────────────────────────────────────────────
113
+
114
+ // get task list from plugin sources, if possible, and offer it as autocomplete options
115
+ let taskOptions = await findKestraTasks(path.join(ctx.pluginRoot, "src"));
116
+ const taskType = checkCancel(
117
+ await select({
118
+ message: "Which task do you want to add a custom UI for?",
119
+ options: taskOptions.map((task) => {
120
+ const taskShort = task.replace(`${ctx.pluginId}.`, "");
121
+ return {
122
+ value: taskShort,
123
+ label: taskShort,
124
+ };
125
+ }),
126
+ validate: (v) => {
127
+ if (!v.trim()) return "Task type is required.";
128
+ },
129
+ }),
130
+ );
131
+
132
+ // ── 4. UI module ──────────────────────────────────────────────────────────
133
+
134
+ const moduleValue = checkCancel(
135
+ await select({
136
+ message: "Which UI do you want to customize?",
137
+ options: Object.entries(UI_MODULES).map(([key, m]) => ({
138
+ value: key,
139
+ label: m.label,
140
+ hint: m.hint,
141
+ })),
142
+ }),
143
+ );
144
+
145
+ const mod = UI_MODULES[moduleValue];
146
+ const componentName = deriveComponentName(taskType, mod.componentSuffix);
147
+
148
+ // ── 5. Summary & confirmation ─────────────────────────────────────────────
149
+
150
+ const uiDir = ctx.uiDir;
151
+ const relUiDir = path.relative(process.cwd(), uiDir) || "ui";
152
+
153
+ note(
154
+ [
155
+ `Plugin id: ${pc.cyan(pluginId)}`,
156
+ `Task: ${pc.cyan(taskType)}`,
157
+ `UI module: ${pc.cyan(moduleValue)}`,
158
+ `Component: ${pc.cyan(componentName + ".vue")}`,
159
+ ``,
160
+ `${pc.bold("Will create / write:")}`,
161
+ ` ${relUiDir}/package.json`,
162
+ ` ${relUiDir}/index.html`,
163
+ ` ${relUiDir}/tsconfig.json`,
164
+ ` ${relUiDir}/vite.config.ts`,
165
+ ` ${relUiDir}/src/main.ts`,
166
+ ` ${relUiDir}/src/App.vue`,
167
+ ` ${relUiDir}/src/components/${componentName}.vue`,
168
+ ` ${relUiDir}/src/${componentName}.stories.ts`,
169
+ ` ${relUiDir}/.storybook/main.ts`,
170
+ ` ${relUiDir}/.storybook/preview.ts`,
171
+ ` ${relUiDir}/.gitignore`,
172
+ ctx.uiDirExists ? "" : ` ${relUiDir}/ ${pc.dim("(directory will be created)")}`,
173
+ ]
174
+ .filter((l) => l !== undefined)
175
+ .join("\n"),
176
+ "Here's what will happen",
177
+ );
178
+
179
+ const go = checkCancel(await confirm({ message: "Proceed?", initialValue: true }));
180
+ if (!go) bail("Cancelled.");
181
+
182
+ // ── 6. Scaffold files ─────────────────────────────────────────────────────
183
+
184
+ const s = spinner();
185
+ s.start("Scaffolding files…");
186
+
187
+ try {
188
+ fs.mkdirSync(uiDir, { recursive: true });
189
+
190
+ const npmPkgName = pluginId; // use the plugin group id as the npm package name
191
+
192
+ scaffoldPackageJson(uiDir, npmPkgName);
193
+ scaffoldIndexHtml(uiDir, npmPkgName);
194
+ scaffoldTsConfig(uiDir);
195
+ scaffoldViteConfig(
196
+ uiDir,
197
+ pluginId,
198
+ taskType,
199
+ moduleValue,
200
+ componentName,
201
+ mod.additionalProperties,
202
+ );
203
+ scaffoldMainTs(uiDir);
204
+ scaffoldAppVue(uiDir, componentName, moduleValue);
205
+ scaffoldComponent(uiDir, componentName, mod.starterFile);
206
+ scaffoldStorybookMain(uiDir);
207
+ scaffoldStorybookPreview(uiDir);
208
+ scaffoldStory(uiDir, componentName, moduleValue);
209
+ scaffoldGitignore(uiDir);
210
+
211
+ s.stop("Files scaffolded.");
212
+ } catch (err) {
213
+ s.stop("Scaffolding failed.");
214
+ bail(String(err));
215
+ }
216
+
217
+ // ── 7. npm install ────────────────────────────────────────────────────────
218
+
219
+ const doInstall = checkCancel(
220
+ await confirm({
221
+ message: `Run ${pc.bold("npm install")} in ${pc.cyan(relUiDir)}?`,
222
+ initialValue: true,
223
+ }),
224
+ );
225
+
226
+ if (doInstall) {
227
+ const s2 = spinner();
228
+ s2.start("Installing dependencies…");
229
+ const { success, output } = npmInstall(uiDir);
230
+ if (success) {
231
+ s2.stop("Dependencies installed.");
232
+ } else {
233
+ s2.stop(pc.yellow("npm install finished with warnings/errors."));
234
+ log.warn(output.slice(0, 600));
235
+ }
236
+ }
237
+
238
+ // ── 8. Done ───────────────────────────────────────────────────────────────
239
+
240
+ const cdCmd = ctx.pluginRoot !== process.cwd() ? `cd ${relUiDir} && ` : `cd ${relUiDir} && `;
241
+
242
+ outro(
243
+ pc.green("✔ Done!") +
244
+ "\n\n" +
245
+ [
246
+ "Next steps:",
247
+ ` ${pc.bold(cdCmd + "npm run dev")} — start Vite dev server`,
248
+ ` ${pc.bold(cdCmd + "npm run storybook")} — start Storybook`,
249
+ ` ${pc.bold(cdCmd + "npm run build")} — build the UI module`,
250
+ ].join("\n"),
251
+ );
252
+ }
253
+
254
+ main().catch((err) => {
255
+ console.error(pc.red("Unexpected error:"), err);
256
+ process.exit(1);
257
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@kestra-io/create-artifact-sdk",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a Kestra plugin custom UI package",
5
+ "keywords": [
6
+ "kestra",
7
+ "plugin",
8
+ "scaffold",
9
+ "ui"
10
+ ],
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/kestra-io/artifact-sdk.git"
15
+ },
16
+ "bin": {
17
+ "create-artifact-sdk": "./bin/create.js"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "src",
22
+ "templates"
23
+ ],
24
+ "type": "module",
25
+ "main": "index.js",
26
+ "scripts": {
27
+ "test": "node bin/create.js"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^1.2.0",
31
+ "@kestra-io/artifact-sdk": "*",
32
+ "picocolors": "^1.1.1"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ }
37
+ }
package/src/context.js ADDED
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Walk upward from `startDir` looking for a settings.gradle or settings.gradle.kts file.
6
+ * Returns { dir, file, content } or null if not found within `maxLevels`.
7
+ */
8
+ function findGradleSettings(startDir, maxLevels = 2) {
9
+ let dir = startDir;
10
+ for (let i = 0; i <= maxLevels; i++) {
11
+ for (const name of ["settings.gradle.kts", "settings.gradle"]) {
12
+ const candidate = path.join(dir, name);
13
+ if (fs.existsSync(candidate)) {
14
+ return { dir, file: candidate, content: fs.readFileSync(candidate, "utf8") };
15
+ }
16
+ }
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir) break; // filesystem root
19
+ dir = parent;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Extract the rootProject.name value from a Gradle settings file.
26
+ * Handles both:
27
+ * rootProject.name = "my-plugin" (Groovy)
28
+ * rootProject.name = "my-plugin" (KTS)
29
+ */
30
+ function extractPluginName(content) {
31
+ const match = content.match(/rootProject\.name\s*=\s*["']([^"']+)["']/);
32
+ return match ? match[1] : null;
33
+ }
34
+
35
+ /**
36
+ * Derive a Java-style plugin group id from the project name.
37
+ * e.g. "plugin-redis" → "io.kestra.plugin.redis"
38
+ * If the name already looks like a group id, return as-is.
39
+ */
40
+ function derivePluginId(name) {
41
+ if (name.includes(".")) return name;
42
+ // strip leading "plugin-" or "kestra-plugin-" if present
43
+ const stripped = name.replace(/^(kestra-)?plugin-/i, "");
44
+ return `io.kestra.plugin.${stripped}`;
45
+ }
46
+
47
+ export function detectContext(cwd = process.cwd()) {
48
+ const gradle = findGradleSettings(cwd);
49
+
50
+ let pluginRoot = null;
51
+ let pluginName = null;
52
+ let pluginId = null;
53
+ let uiDirExists = false;
54
+ let gradleFile = null;
55
+
56
+ if (gradle) {
57
+ pluginRoot = gradle.dir;
58
+ gradleFile = gradle.file;
59
+ pluginName = extractPluginName(gradle.content);
60
+ if (pluginName) {
61
+ pluginId = derivePluginId(pluginName);
62
+ }
63
+ uiDirExists = fs.existsSync(path.join(pluginRoot, "ui"));
64
+ }
65
+
66
+ return {
67
+ cwd,
68
+ pluginRoot, // absolute path to the directory containing settings.gradle
69
+ gradleFile, // absolute path to the settings.gradle file
70
+ pluginName, // raw name from rootProject.name, e.g. "plugin-redis"
71
+ pluginId, // derived group id, e.g. "io.kestra.plugin.redis"
72
+ uiDirExists, // whether ui/ already exists in pluginRoot
73
+ uiDir: pluginRoot ? path.join(pluginRoot, "ui") : null,
74
+ };
75
+ }
@@ -0,0 +1,97 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, extname } from "node:path";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Heuristics (mirrors the GitHub version)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const TASK_ANNOTATIONS = [/@Plugin\s*\(/, /@Schema\s*\(/];
9
+
10
+ const TASK_SUPERTYPES = [
11
+ /implements\s+Task\b/,
12
+ /extends\s+\w*(Task|Trigger|Condition)\b/,
13
+ /implements\s+RunnableTask/,
14
+ /implements\s+FlowableTask/,
15
+ /implements\s+TriggerInterface/,
16
+ ];
17
+
18
+ const EXCLUDE = [
19
+ /^\s*(public\s+)?abstract\s+class/m, // abstract classes
20
+ /^\s*public\s+interface\s+/m, // interfaces
21
+ /package\s+.*\.tests?[.;]/, // test packages
22
+ ];
23
+
24
+ function isTaskClass(source) {
25
+ if (EXCLUDE.some((re) => re.test(source))) return false;
26
+ return (
27
+ TASK_ANNOTATIONS.some((re) => re.test(source)) || TASK_SUPERTYPES.some((re) => re.test(source))
28
+ );
29
+ }
30
+
31
+ function extractFullClassName(source, filePath) {
32
+ const packageMatch = source.match(/^\s*package\s+([\w.]+)\s*;/m);
33
+ const pkg = packageMatch ? packageMatch[1] : null;
34
+ if (!pkg || !pkg.startsWith("io.kestra.plugin.")) return null;
35
+
36
+ const classMatch = source.match(/public\s+(?:final\s+)?class\s+(\w+)/);
37
+ const className = classMatch
38
+ ? classMatch[1]
39
+ : filePath
40
+ .split("/")
41
+ .pop()
42
+ .replace(/\.java$/, "");
43
+
44
+ return `${pkg}.${className}`;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Recursive .java file collector
49
+ // ---------------------------------------------------------------------------
50
+
51
+ async function collectJavaFiles(dir, results = []) {
52
+ const entries = await readdir(dir, { withFileTypes: true });
53
+ await Promise.all(
54
+ entries.map((entry) => {
55
+ const full = join(dir, entry.name);
56
+ if (entry.isDirectory()) return collectJavaFiles(full, results);
57
+ if (entry.isFile() && extname(entry.name) === ".java") results.push(full);
58
+ }),
59
+ );
60
+ return results;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Main exported function
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Scans a local Kestra plugin source tree and returns the class names of all
69
+ * concrete task / trigger / condition classes found.
70
+ *
71
+ * @param {string} srcRoot Path to the source root, e.g. "/path/to/plugin/src"
72
+ * or the deeper Java root like "…/src/main/java".
73
+ * The function accepts either — it will descend into
74
+ * src/main/java automatically if `srcRoot` points at
75
+ * the broader `src` directory.
76
+ * @returns {Promise<string[]>} Sorted array of fully-qualified class names (e.g. `io.kestra.plugin.jdbc.mysql.Query`).
77
+ */
78
+ export async function findKestraTasks(srcRoot) {
79
+ // Normalise: if the caller passed the broad `src` dir, step into main/java.
80
+ const javaRoot = srcRoot.endsWith("/src") ? join(srcRoot, "main", "java") : srcRoot;
81
+
82
+ const files = await collectJavaFiles(javaRoot);
83
+
84
+ const classNames = (
85
+ await Promise.all(
86
+ files.map(async (filePath) => {
87
+ const source = await readFile(filePath, "utf8");
88
+ if (!isTaskClass(source)) return null;
89
+ return extractFullClassName(source, filePath);
90
+ }),
91
+ )
92
+ )
93
+ .filter(Boolean)
94
+ .sort();
95
+
96
+ return classNames;
97
+ }
package/src/modules.js ADDED
@@ -0,0 +1,26 @@
1
+ import { UI_MODULES } from "@kestra-io/artifact-sdk";
2
+
3
+ /**
4
+ * Derive a PascalCase component name from a task type and a UI module.
5
+ *
6
+ * The task type is the short dotted form: e.g. "list.ListPop"
7
+ * The module suffix is e.g. "TopologyDetails"
8
+ *
9
+ * Steps:
10
+ * 1. Split on "." → ["list", "ListPop"]
11
+ * 2. PascalCase each segment → ["List", "ListPop"]
12
+ * 3. Join + append suffix → "ListListPopTopologyDetails"
13
+ */
14
+ export function deriveComponentName(taskType, moduleSuffix) {
15
+ const segments = taskType.split(".").map((s) => s.charAt(0).toUpperCase() + s.slice(1));
16
+ return segments.join("") + moduleSuffix;
17
+ }
18
+
19
+ /**
20
+ * Look up a UI_MODULES entry by its value string.
21
+ */
22
+ export function findModule(value) {
23
+ return UI_MODULES[value];
24
+ }
25
+
26
+ export { UI_MODULES };
package/src/runner.js ADDED
@@ -0,0 +1,20 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ /**
4
+ * Run `npm install` in the given directory.
5
+ * Returns { success, output }.
6
+ */
7
+ export function npmInstall(cwd) {
8
+ const result = spawnSync("npm", ["install"], {
9
+ cwd,
10
+ stdio: "pipe",
11
+ encoding: "utf8",
12
+ shell: process.platform === "win32",
13
+ });
14
+
15
+ const output = (result.stdout ?? "") + (result.stderr ?? "");
16
+ return {
17
+ success: result.status === 0,
18
+ output,
19
+ };
20
+ }
@@ -0,0 +1,194 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATES_DIR = path.resolve(__dirname, "../templates");
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Internal helpers
10
+ // ---------------------------------------------------------------------------
11
+
12
+ function write(filePath, content) {
13
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
14
+ fs.writeFileSync(filePath, content, "utf8");
15
+ }
16
+
17
+ function readStatic(name) {
18
+ return fs.readFileSync(path.join(TEMPLATES_DIR, "static", name), "utf8");
19
+ }
20
+
21
+ function readDynamic(name) {
22
+ return fs.readFileSync(path.join(TEMPLATES_DIR, "dynamic", name), "utf8");
23
+ }
24
+
25
+ function readStarter(name) {
26
+ // get the root of the starters directory, which is in @kestra-io/artifact-sdk
27
+ const startersDir = path.dirname(
28
+ require.resolve("@kestra-io/artifact-sdk/starters/placeholder.txt"),
29
+ );
30
+ return fs.readFileSync(path.join(startersDir, name), "utf8");
31
+ }
32
+
33
+ /**
34
+ * Replace all `{{key}}` placeholders in a template string with the
35
+ * corresponding values from the `vars` object.
36
+ */
37
+ function render(template, vars) {
38
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
39
+ if (!(key in vars)) throw new Error(`Template variable "{{${key}}}" has no value.`);
40
+ return vars[key];
41
+ });
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // package.json (structured data — kept in JS for easy maintenance)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export function scaffoldPackageJson(uiDir, pluginName) {
49
+ const pkg = {
50
+ name: pluginName,
51
+ version: "0.1.0",
52
+ private: true,
53
+ type: "module",
54
+ scripts: {
55
+ dev: "vite",
56
+ build: "vite build",
57
+ preview: "vite preview",
58
+ storybook: "storybook dev --port 6006",
59
+ "build-storybook": "storybook build",
60
+ },
61
+ dependencies: {
62
+ "@kestra-io/artifact-sdk": "latest",
63
+ vue: "^3.5.0",
64
+ },
65
+ devDependencies: {
66
+ "@chromatic-com/storybook": "^3.0.0",
67
+ "@storybook/addon-essentials": "^8.0.0",
68
+ "@storybook/addon-interactions": "^8.0.0",
69
+ "@storybook/addon-links": "^8.0.0",
70
+ "@storybook/blocks": "^8.0.0",
71
+ "@storybook/vue3": "^8.0.0",
72
+ "@storybook/vue3-vite": "^8.0.0",
73
+ "@vitejs/plugin-vue": "^5.0.0",
74
+ storybook: "^8.0.0",
75
+ typescript: "^5.0.0",
76
+ vite: "^6.0.0",
77
+ },
78
+ };
79
+ write(path.join(uiDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // tsconfig.json (structured data — kept in JS for easy maintenance)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export function scaffoldTsConfig(uiDir) {
87
+ const tsconfig = {
88
+ compilerOptions: {
89
+ target: "ES2020",
90
+ useDefineForClassFields: true,
91
+ module: "ESNext",
92
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
93
+ skipLibCheck: true,
94
+ moduleResolution: "bundler",
95
+ allowImportingTsExtensions: true,
96
+ resolveJsonModule: true,
97
+ isolatedModules: true,
98
+ noEmit: true,
99
+ jsx: "preserve",
100
+ strict: true,
101
+ },
102
+ include: ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
103
+ };
104
+ write(path.join(uiDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Static files — read verbatim from templates/static/
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export function scaffoldMainTs(uiDir) {
112
+ write(path.join(uiDir, "src", "main.ts"), readStatic("main.ts"));
113
+ }
114
+
115
+ export function scaffoldStorybookMain(uiDir) {
116
+ write(path.join(uiDir, ".storybook", "main.ts"), readStatic("storybook-main.ts"));
117
+ }
118
+
119
+ export function scaffoldStorybookPreview(uiDir) {
120
+ write(path.join(uiDir, ".storybook", "preview.ts"), readStatic("storybook-preview.ts"));
121
+ }
122
+
123
+ export function scaffoldGitignore(uiDir) {
124
+ write(path.join(uiDir, ".gitignore"), readStatic("gitignore"));
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Dynamic files — rendered from templates/dynamic/*.hbs
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export function scaffoldAppVue(uiDir, componentName, moduleValue) {
132
+ const content = render(readDynamic("App.vue.hbs"), {
133
+ componentName,
134
+ moduleValue,
135
+ });
136
+ write(path.join(uiDir, "src", "App.vue"), content);
137
+ }
138
+
139
+ export function scaffoldIndexHtml(uiDir, pluginName) {
140
+ const content = render(readDynamic("index.html.hbs"), { pluginName });
141
+ write(path.join(uiDir, "index.html"), content);
142
+ }
143
+
144
+ export function scaffoldViteConfig(
145
+ uiDir,
146
+ pluginId,
147
+ taskType,
148
+ moduleValue,
149
+ componentName,
150
+ additionalProperties,
151
+ ) {
152
+ const hasAdditional = Object.keys(additionalProperties).length > 0;
153
+ const additionalPropertiesStr = hasAdditional
154
+ ? "\n additionalProperties: " +
155
+ JSON.stringify(additionalProperties, null, 2).split("\n").join("\n ") +
156
+ ","
157
+ : "";
158
+
159
+ const content = render(readDynamic("vite.config.ts.hbs"), {
160
+ pluginId,
161
+ taskType,
162
+ moduleValue,
163
+ componentName,
164
+ additionalProperties: additionalPropertiesStr,
165
+ });
166
+ write(path.join(uiDir, "vite.config.ts"), content);
167
+ }
168
+
169
+ export function scaffoldStory(uiDir, componentName, moduleValue) {
170
+ const content = render(readDynamic("Component.stories.ts.hbs"), {
171
+ componentName,
172
+ moduleValue,
173
+ });
174
+ write(path.join(uiDir, "src", `${componentName}.stories.ts`), content);
175
+ }
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Component — copied from templates/starters/ with fallback
179
+ // ---------------------------------------------------------------------------
180
+
181
+ export function scaffoldComponent(uiDir, componentName, starterFile) {
182
+ let content;
183
+ try {
184
+ content = readStarter(starterFile);
185
+ } catch {
186
+ // Fallback used before @kestra-io/artifact-sdk is published
187
+ const uiType = starterFile.replace(".vue", "");
188
+ content = render(readDynamic("Component.fallback.vue.hbs"), {
189
+ componentName,
190
+ uiType,
191
+ });
192
+ }
193
+ write(path.join(uiDir, "src", "components", `${componentName}.vue`), content);
194
+ }
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import {{componentName}} from "./components/{{componentName}}.vue";
3
+
4
+ // Default props for local development — adjust to match your task's real output
5
+ const props = {
6
+ // TODO: populate with realistic sample data for the "{{moduleValue}}" UI
7
+ };
8
+ </script>
9
+
10
+ <template>
11
+ <div style="padding: 1rem; font-family: sans-serif">
12
+ <{{componentName}} v-bind="props" />
13
+ </div>
14
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ // TODO: define props matching the "{{uiType}}" contract
3
+ // from @kestra-io/artifact-sdk once the SDK is published.
4
+ defineProps<Record<string, unknown>>();
5
+ </script>
6
+
7
+ <template>
8
+ <div class="kestra-custom-ui">
9
+ <p>✏️ Customize this component for your task.</p>
10
+ </div>
11
+ </template>
12
+
13
+ <style scoped>
14
+ .kestra-custom-ui { padding: 1rem; }
15
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from "@storybook/vue3";
2
+ import {{componentName}} from "./components/{{componentName}}.vue";
3
+
4
+ const meta: Meta<typeof {{componentName}}> = {
5
+ title: "Plugin UI / {{moduleValue}} / {{componentName}}",
6
+ component: {{componentName}},
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ // TODO: define argTypes matching your component's props
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof {{componentName}}>;
15
+
16
+ export const Default: Story = {
17
+ args: {
18
+ // TODO: populate with realistic sample data
19
+ },
20
+ };
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{pluginName}} — Plugin UI Dev</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,13 @@
1
+ import { defaultViteConfig } from "@kestra-io/artifact-sdk";
2
+
3
+ export default defaultViteConfig({
4
+ plugin: "{{pluginId}}",
5
+ exposes: {
6
+ "{{taskType}}": [
7
+ {
8
+ uiModule: "{{moduleValue}}",
9
+ path: "./src/components/{{componentName}}.vue",{{additionalProperties}}
10
+ },
11
+ ],
12
+ },
13
+ });
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ storybook-static
4
+ *.local
@@ -0,0 +1,8 @@
1
+ import { createApp } from "vue";
2
+ import "@kestra-io/artifact-sdk/style.css";
3
+ import { initApp } from "@kestra-io/artifact-sdk";
4
+ import App from "./App.vue";
5
+
6
+ const appInstance = createApp(App);
7
+ initApp(appInstance);
8
+ appInstance.mount("#app");
@@ -0,0 +1,17 @@
1
+ import type { StorybookConfig } from "@storybook/vue3-vite";
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
5
+ addons: [
6
+ "@storybook/addon-links",
7
+ "@storybook/addon-essentials",
8
+ "@chromatic-com/storybook",
9
+ "@storybook/addon-interactions",
10
+ ],
11
+ framework: {
12
+ name: "@storybook/vue3-vite",
13
+ options: {},
14
+ },
15
+ };
16
+
17
+ export default config;
@@ -0,0 +1,21 @@
1
+ import type { Preview } from "@storybook/vue3";
2
+ import "@kestra-io/artifact-sdk/style.css";
3
+ import { setup } from "@storybook/vue3";
4
+ import { initApp } from "@kestra-io/artifact-sdk";
5
+
6
+ setup((app) => {
7
+ initApp(app);
8
+ });
9
+
10
+ const preview: Preview = {
11
+ parameters: {
12
+ controls: {
13
+ matchers: {
14
+ color: /(background|color)$/i,
15
+ date: /Date$/i,
16
+ },
17
+ },
18
+ },
19
+ };
20
+
21
+ export default preview;