create-onin-plugin 1.7.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,55 @@
1
+ # create-onin-plugin
2
+
3
+ CLI for bootstrapping Onin plugins with a marketplace-safe release layout.
4
+
5
+ ## Usage
6
+
7
+ Published usage:
8
+
9
+ ```bash
10
+ npx create-onin-plugin my-plugin
11
+ ```
12
+
13
+ Monorepo usage:
14
+
15
+ ```bash
16
+ pnpm create:plugin my-plugin
17
+ ```
18
+
19
+ Direct package execution inside the repo:
20
+
21
+ ```bash
22
+ pnpm --filter create-onin-plugin start my-plugin
23
+ ```
24
+
25
+ Interactive mode:
26
+
27
+ ```bash
28
+ npx create-onin-plugin
29
+ ```
30
+
31
+ ## Current template
32
+
33
+ The first version ships a single `svelte-view` template with:
34
+
35
+ - `src/main.ts`
36
+ - `src/lifecycle.ts`
37
+ - `vite.lifecycle.config.ts`
38
+ - `pnpm build`
39
+ - `pnpm pack:plugin`
40
+
41
+ ## Generated project
42
+
43
+ The generated plugin includes:
44
+
45
+ - `src/main.ts` for the UI entry
46
+ - `src/lifecycle.ts` for settings, commands, and startup initialization
47
+ - `manifest.json` wired to `dist/lifecycle.js`
48
+ - `pnpm pack:plugin` to create `plugin.zip`
49
+
50
+ The release zip contains:
51
+
52
+ - `manifest.json`
53
+ - `icon.svg`
54
+ - `dist/index.html`
55
+ - `dist/lifecycle.js`
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-onin-plugin",
3
+ "version": "1.7.0",
4
+ "private": false,
5
+ "description": "CLI for scaffolding Onin plugins with lifecycle and release packaging built in.",
6
+ "license": "GPL-3.0",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "create-onin-plugin": "./src/cli.js"
13
+ },
14
+ "files": [
15
+ "README.md",
16
+ "src",
17
+ "templates"
18
+ ],
19
+ "scripts": {
20
+ "start": "node ./src/cli.js"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ }
25
+ }
package/src/cli.js ADDED
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { stdin as input, stdout as output } from "node:process";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
8
+
9
+ const TEMPLATE_NAME = "svelte-view";
10
+ const SUPPORTED_TEMPLATES = [TEMPLATE_NAME];
11
+ const CLI_DIR = dirname(fileURLToPath(import.meta.url));
12
+ const TEMPLATE_DIR = resolve(
13
+ CLI_DIR,
14
+ "../templates",
15
+ TEMPLATE_NAME,
16
+ );
17
+
18
+ function parseArgs(argv) {
19
+ const options = {
20
+ targetDir: undefined,
21
+ pluginName: undefined,
22
+ pluginId: undefined,
23
+ withSettings: undefined,
24
+ yes: false,
25
+ template: TEMPLATE_NAME,
26
+ };
27
+
28
+ for (let i = 0; i < argv.length; i += 1) {
29
+ const arg = argv[i];
30
+
31
+ if (arg === "--") {
32
+ continue;
33
+ }
34
+
35
+ if (!arg.startsWith("--") && !options.targetDir) {
36
+ options.targetDir = arg;
37
+ continue;
38
+ }
39
+
40
+ if (arg === "--template") {
41
+ options.template = argv[i + 1] ?? TEMPLATE_NAME;
42
+ i += 1;
43
+ continue;
44
+ }
45
+
46
+ if (arg === "--plugin-name") {
47
+ options.pluginName = argv[i + 1];
48
+ i += 1;
49
+ continue;
50
+ }
51
+
52
+ if (arg === "--plugin-id") {
53
+ options.pluginId = argv[i + 1];
54
+ i += 1;
55
+ continue;
56
+ }
57
+
58
+ if (arg === "--yes") {
59
+ options.yes = true;
60
+ continue;
61
+ }
62
+
63
+ if (arg === "--with-settings") {
64
+ options.withSettings = true;
65
+ continue;
66
+ }
67
+
68
+ if (arg === "--no-with-settings") {
69
+ options.withSettings = false;
70
+ continue;
71
+ }
72
+ }
73
+
74
+ return options;
75
+ }
76
+
77
+ function printHelp() {
78
+ console.log("create-onin-plugin");
79
+ console.log("");
80
+ console.log("Usage:");
81
+ console.log(" create-onin-plugin [target-dir] [options]");
82
+ console.log("");
83
+ console.log("Options:");
84
+ console.log(" --template <name> Template to use (default: svelte-view)");
85
+ console.log(" --plugin-name <name> Plugin display name");
86
+ console.log(" --plugin-id <id> Plugin manifest id");
87
+ console.log(" --with-settings Include settings schema example");
88
+ console.log(" --no-with-settings Skip settings schema example");
89
+ console.log(" --yes Use defaults for missing answers");
90
+ console.log(" --help Show this help message");
91
+ }
92
+
93
+ function toTitleCase(value) {
94
+ return value
95
+ .split(/[-_.\s]+/)
96
+ .filter(Boolean)
97
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
98
+ .join(" ");
99
+ }
100
+
101
+ function slugify(value) {
102
+ return value
103
+ .trim()
104
+ .toLowerCase()
105
+ .replace(/[^a-z0-9.-]+/g, "-")
106
+ .replace(/^-+|-+$/g, "")
107
+ .replace(/-{2,}/g, "-");
108
+ }
109
+
110
+ function isValidPluginId(value) {
111
+ return /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(value) && !value.includes("..");
112
+ }
113
+
114
+ function isValidPackageName(value) {
115
+ return /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(value);
116
+ }
117
+
118
+ async function isDirectoryEmpty(dir) {
119
+ const entries = await readdir(dir);
120
+ return entries.length === 0;
121
+ }
122
+
123
+ async function ensureTargetDirectory(targetDir) {
124
+ try {
125
+ const targetStat = await stat(targetDir);
126
+ if (!targetStat.isDirectory()) {
127
+ throw new Error(
128
+ `Target path exists and is not a directory: ${targetDir}\nChoose a new directory name and try again.`,
129
+ );
130
+ }
131
+
132
+ if (!(await isDirectoryEmpty(targetDir))) {
133
+ throw new Error(
134
+ `Target directory is not empty: ${targetDir}\nUse an empty directory or choose a new project name.`,
135
+ );
136
+ }
137
+ } catch (error) {
138
+ if (error && error.code === "ENOENT") {
139
+ await mkdir(targetDir, { recursive: true });
140
+ return;
141
+ }
142
+
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ function renderTemplate(content, context) {
148
+ return content
149
+ .replaceAll("__PLUGIN_NAME__", context.pluginName)
150
+ .replaceAll("__PLUGIN_ID__", context.pluginId)
151
+ .replaceAll("__PACKAGE_NAME__", context.packageName)
152
+ .replaceAll("__PLUGIN_DESCRIPTION__", context.pluginDescription)
153
+ .replaceAll("__SETTINGS_BLOCK__", context.settingsBlock)
154
+ .replaceAll("__SETTINGS_IMPORT__", context.settingsImport)
155
+ .replaceAll("__SETTINGS_NOTE__", context.settingsNote)
156
+ .replaceAll("__KEYWORD__", context.keyword);
157
+ }
158
+
159
+ async function copyTemplateDir(sourceDir, targetDir, context) {
160
+ const entries = await readdir(sourceDir, { withFileTypes: true });
161
+
162
+ for (const entry of entries) {
163
+ const sourcePath = join(sourceDir, entry.name);
164
+ const outputName = entry.name.endsWith(".tpl")
165
+ ? entry.name.slice(0, -4)
166
+ : entry.name;
167
+ const targetPath = join(targetDir, outputName);
168
+
169
+ if (entry.isDirectory()) {
170
+ await mkdir(targetPath, { recursive: true });
171
+ await copyTemplateDir(sourcePath, targetPath, context);
172
+ continue;
173
+ }
174
+
175
+ if (entry.name.endsWith(".tpl")) {
176
+ const content = await readFile(sourcePath, "utf8");
177
+ await writeFile(targetPath, renderTemplate(content, context), "utf8");
178
+ continue;
179
+ }
180
+
181
+ await copyFile(sourcePath, targetPath);
182
+ }
183
+ }
184
+
185
+ function buildSettingsBlock(withSettings) {
186
+ if (!withSettings) {
187
+ return " // Add settings.useSettingsSchema(...) here when your plugin needs configurable options.\n";
188
+ }
189
+
190
+ return ` await settings.useSettingsSchema([
191
+ {
192
+ key: "accentColor",
193
+ label: "Accent Color",
194
+ type: "color",
195
+ defaultValue: "#111827",
196
+ description: "Example plugin setting registered during lifecycle onLoad.",
197
+ },
198
+ ]);
199
+ `;
200
+ }
201
+
202
+ async function promptForMissingOptions(initialOptions) {
203
+ if (initialOptions.yes) {
204
+ const targetDir = initialOptions.targetDir || "my-onin-plugin";
205
+ const packageName = slugify(basename(targetDir));
206
+ const pluginName =
207
+ initialOptions.pluginName || toTitleCase(packageName) || "My Onin Plugin";
208
+ const pluginId =
209
+ initialOptions.pluginId || `com.example.${packageName || "my-onin-plugin"}`;
210
+
211
+ return {
212
+ targetDir,
213
+ pluginName,
214
+ pluginId,
215
+ withSettings: initialOptions.withSettings ?? true,
216
+ };
217
+ }
218
+
219
+ const rl = createInterface({ input, output });
220
+
221
+ try {
222
+ const targetDirInput =
223
+ initialOptions.targetDir ||
224
+ (await rl.question("Project directory name: ")).trim();
225
+ const targetDir = targetDirInput || "my-onin-plugin";
226
+ const packageName = slugify(basename(targetDir));
227
+
228
+ const pluginNameInput =
229
+ initialOptions.pluginName ||
230
+ (await rl.question(
231
+ `Plugin name (${toTitleCase(packageName) || "My Onin Plugin"}): `,
232
+ )).trim();
233
+ const pluginName = pluginNameInput || toTitleCase(packageName) || "My Onin Plugin";
234
+
235
+ const defaultPluginId = `com.example.${packageName || "my-onin-plugin"}`;
236
+ const pluginIdInput =
237
+ initialOptions.pluginId ||
238
+ (await rl.question(`Plugin ID (${defaultPluginId}): `)).trim();
239
+ const pluginId = pluginIdInput || defaultPluginId;
240
+
241
+ let withSettings = initialOptions.withSettings;
242
+ if (withSettings === undefined) {
243
+ const answer = (
244
+ await rl.question("Include settings schema example? (Y/n): ")
245
+ ).trim().toLowerCase();
246
+ withSettings = answer !== "n";
247
+ }
248
+
249
+ return {
250
+ targetDir,
251
+ pluginName,
252
+ pluginId,
253
+ withSettings,
254
+ };
255
+ } finally {
256
+ rl.close();
257
+ }
258
+ }
259
+
260
+ function printNextSteps(targetDir) {
261
+ console.log("");
262
+ console.log("Project created.");
263
+ console.log("");
264
+ console.log(` cd ${targetDir}`);
265
+ console.log(" pnpm install");
266
+ console.log(" pnpm dev");
267
+ console.log("");
268
+ console.log("To build release artifacts:");
269
+ console.log(" pnpm build");
270
+ console.log(" pnpm pack:plugin");
271
+ console.log("");
272
+ console.log("To load the plugin in Onin:");
273
+ console.log(" Open Settings -> Plugins -> Import Local Plugin");
274
+ }
275
+
276
+ async function main() {
277
+ const options = parseArgs(process.argv.slice(2));
278
+
279
+ if (process.argv.includes("--help")) {
280
+ printHelp();
281
+ return;
282
+ }
283
+
284
+ if (!SUPPORTED_TEMPLATES.includes(options.template)) {
285
+ console.error(
286
+ `Unsupported template: ${options.template}\nSupported templates: ${SUPPORTED_TEMPLATES.join(", ")}`,
287
+ );
288
+ process.exitCode = 1;
289
+ return;
290
+ }
291
+
292
+ const answers = await promptForMissingOptions(options);
293
+ const targetDir = resolve(process.cwd(), answers.targetDir);
294
+ const packageName = slugify(basename(targetDir));
295
+
296
+ if (!isValidPackageName(packageName)) {
297
+ console.error(
298
+ `Invalid project directory name: ${packageName}\nUse lowercase letters, numbers, dots, or hyphens only.`,
299
+ );
300
+ process.exitCode = 1;
301
+ return;
302
+ }
303
+
304
+ if (!isValidPluginId(answers.pluginId)) {
305
+ console.error(
306
+ `Invalid plugin ID: ${answers.pluginId}\nUse lowercase letters, numbers, dots, and hyphens only.`,
307
+ );
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+
312
+ await ensureTargetDirectory(targetDir);
313
+
314
+ const context = {
315
+ packageName,
316
+ pluginName: answers.pluginName,
317
+ pluginId: answers.pluginId,
318
+ pluginDescription: `${answers.pluginName} plugin for Onin`,
319
+ keyword: packageName.split(".").pop() || packageName,
320
+ settingsImport: answers.withSettings ? ", settings" : "",
321
+ settingsBlock: buildSettingsBlock(answers.withSettings),
322
+ settingsNote: answers.withSettings
323
+ ? "This template includes a sample settings schema registered from lifecycle.ts."
324
+ : "This template omits settings schema. Add it later in src/lifecycle.ts if needed.",
325
+ };
326
+
327
+ await copyTemplateDir(TEMPLATE_DIR, targetDir, context);
328
+ printNextSteps(answers.targetDir);
329
+ }
330
+
331
+ main().catch((error) => {
332
+ console.error(error instanceof Error ? error.message : String(error));
333
+ process.exitCode = 1;
334
+ });
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .svelte-kit
4
+ plugin.zip
@@ -0,0 +1,30 @@
1
+ # __PLUGIN_NAME__
2
+
3
+ __PLUGIN_DESCRIPTION__
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev
10
+ ```
11
+
12
+ ## Build
13
+
14
+ ```bash
15
+ pnpm build
16
+ ```
17
+
18
+ ## Pack
19
+
20
+ ```bash
21
+ pnpm pack:plugin
22
+ ```
23
+
24
+ `pnpm pack:plugin` creates a `plugin.zip` that includes:
25
+
26
+ - `manifest.json`
27
+ - `icon.svg`
28
+ - `dist/`
29
+
30
+ __SETTINGS_NOTE__
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
2
+ <rect width="128" height="128" rx="28" fill="#111827" />
3
+ <path
4
+ d="M33 88V40h18l13 27 13-27h18v48H81V62L69 86H59L47 62v26H33Z"
5
+ fill="#F9FAFB"
6
+ />
7
+ </svg>
@@ -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>__PLUGIN_NAME__</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,24 @@
1
+ {
2
+ "id": "__PLUGIN_ID__",
3
+ "name": "__PLUGIN_NAME__",
4
+ "version": "0.1.0",
5
+ "description": "__PLUGIN_DESCRIPTION__",
6
+ "entry": "dist/index.html",
7
+ "icon": "icon.svg",
8
+ "type": "webview",
9
+ "display_mode": "inline",
10
+ "lifecycle": "dist/lifecycle.js",
11
+ "commands": [
12
+ {
13
+ "code": "open",
14
+ "name": "Open __PLUGIN_NAME__",
15
+ "description": "Open the plugin UI",
16
+ "keywords": [
17
+ {
18
+ "name": "__KEYWORD__",
19
+ "type": "prefix"
20
+ }
21
+ ]
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build:index": "vite build",
9
+ "build:lifecycle": "vite build --config vite.lifecycle.config.ts",
10
+ "build": "npm run build:index && npm run build:lifecycle",
11
+ "pack:plugin": "npm run build && bestzip plugin.zip manifest.json icon.svg dist"
12
+ },
13
+ "dependencies": {
14
+ "onin-sdk": "^1.6.0",
15
+ "svelte": "^5.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
19
+ "bestzip": "^2.2.1",
20
+ "typescript": "^5.5.0",
21
+ "vite": "^7.3.1"
22
+ }
23
+ }
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ export let pluginName = "__PLUGIN_NAME__";
3
+ export let pluginId = "__PLUGIN_ID__";
4
+ </script>
5
+
6
+ <main class="shell">
7
+ <section class="hero">
8
+ <p class="eyebrow">Onin Plugin</p>
9
+ <h1>{pluginName}</h1>
10
+ <p class="lede">
11
+ This starter includes a dedicated lifecycle build, release pack command,
12
+ and a manifest wired for marketplace-safe output.
13
+ </p>
14
+ </section>
15
+
16
+ <section class="card">
17
+ <h2>What is ready</h2>
18
+ <ul>
19
+ <li>Vite app build to <code>dist/</code></li>
20
+ <li>Standalone <code>lifecycle.js</code> build</li>
21
+ <li><code>pnpm pack</code> for release zip creation</li>
22
+ <li>Manifest and lifecycle path already aligned</li>
23
+ </ul>
24
+ </section>
25
+
26
+ <section class="card">
27
+ <h2>Plugin ID</h2>
28
+ <code>{pluginId}</code>
29
+ </section>
30
+ </main>
31
+
32
+ <style>
33
+ :global(body) {
34
+ margin: 0;
35
+ font-family:
36
+ "IBM Plex Sans", "Segoe UI", sans-serif;
37
+ background:
38
+ radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 35%),
39
+ linear-gradient(180deg, #f7f7f5 0%, #eceae3 100%);
40
+ color: #161616;
41
+ }
42
+
43
+ code {
44
+ font-family:
45
+ "IBM Plex Mono", "Cascadia Code", monospace;
46
+ }
47
+
48
+ .shell {
49
+ min-height: 100vh;
50
+ padding: 28px;
51
+ display: grid;
52
+ gap: 18px;
53
+ align-content: start;
54
+ }
55
+
56
+ .hero,
57
+ .card {
58
+ background: rgba(255, 255, 255, 0.78);
59
+ border: 1px solid rgba(22, 22, 22, 0.08);
60
+ border-radius: 24px;
61
+ padding: 24px;
62
+ box-shadow: 0 18px 50px rgba(22, 22, 22, 0.08);
63
+ backdrop-filter: blur(14px);
64
+ }
65
+
66
+ .eyebrow {
67
+ margin: 0 0 8px;
68
+ font-size: 12px;
69
+ letter-spacing: 0.16em;
70
+ text-transform: uppercase;
71
+ color: #5f6368;
72
+ }
73
+
74
+ h1,
75
+ h2,
76
+ p,
77
+ ul {
78
+ margin: 0;
79
+ }
80
+
81
+ h1 {
82
+ font-size: 32px;
83
+ line-height: 1.05;
84
+ }
85
+
86
+ h2 {
87
+ font-size: 18px;
88
+ margin-bottom: 12px;
89
+ }
90
+
91
+ .lede {
92
+ margin-top: 12px;
93
+ max-width: 48ch;
94
+ color: #4b5563;
95
+ line-height: 1.6;
96
+ }
97
+
98
+ ul {
99
+ padding-left: 18px;
100
+ color: #374151;
101
+ line-height: 1.7;
102
+ }
103
+
104
+ @media (max-width: 640px) {
105
+ .shell {
106
+ padding: 16px;
107
+ }
108
+
109
+ h1 {
110
+ font-size: 28px;
111
+ }
112
+ }
113
+ </style>
@@ -0,0 +1,13 @@
1
+ import { command, lifecycle__SETTINGS_IMPORT__ } from "onin-sdk";
2
+
3
+ lifecycle.onLoad(async () => {
4
+ __SETTINGS_BLOCK__ await command.handle(async (code) => {
5
+ if (code === "open") {
6
+ return {
7
+ ok: true,
8
+ };
9
+ }
10
+
11
+ return null;
12
+ });
13
+ });
@@ -0,0 +1,12 @@
1
+ import { mount } from "svelte";
2
+ import App from "./App.svelte";
3
+
4
+ const app = mount(App, {
5
+ target: document.getElementById("app")!,
6
+ props: {
7
+ pluginName: "__PLUGIN_NAME__",
8
+ pluginId: "__PLUGIN_ID__",
9
+ },
10
+ });
11
+
12
+ export default app;
@@ -0,0 +1,2 @@
1
+ export default {
2
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "Node",
7
+ "strict": true,
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "types": ["svelte"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts", "vite.lifecycle.config.ts"]
15
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+
4
+ export default defineConfig({
5
+ plugins: [svelte()],
6
+ base: "./",
7
+ server: {
8
+ port: 5173,
9
+ cors: true,
10
+ },
11
+ });
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "vite";
2
+ import { resolve } from "path";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ outDir: "dist",
7
+ emptyOutDir: false,
8
+ lib: {
9
+ entry: resolve(__dirname, "src/lifecycle.ts"),
10
+ formats: ["es"],
11
+ fileName: () => "lifecycle.js",
12
+ },
13
+ rollupOptions: {
14
+ external: [],
15
+ output: {
16
+ inlineDynamicImports: true,
17
+ },
18
+ },
19
+ },
20
+ });