lumia-plugin 0.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lumia Stream
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # Lumia Stream Plugin CLI
2
+
3
+ The `@lumiastream/plugin-cli` package bundles the command-line tools for creating, building, and validating Lumia Stream plugins without pulling the full SDK into your runtime dependencies.
4
+
5
+ ## Commands
6
+
7
+ ```
8
+ npx @lumiastream/plugin-cli create <directory>
9
+ npx @lumiastream/plugin-cli build [--dir <path>] [--out <file>]
10
+ npx @lumiastream/plugin-cli validate <plugin-directory>
11
+ ```
12
+
13
+ Run any command with `--help` to see detailed options.
14
+
15
+ ## Template
16
+
17
+ The CLI ships with a showcase template that demonstrates logging, settings, variables, and alert handling. It mirrors the starter plugin available in this repository so the CLI can scaffold it anywhere.
18
+
19
+ ## License
20
+
21
+ MIT
@@ -0,0 +1,17 @@
1
+ # Showcase Plugin Template
2
+
3
+ This template demonstrates a handful of common Lumia Stream plugin capabilities:
4
+
5
+ - Logs lifecycle events and recent actions
6
+ - Stores and updates variables that other Lumia features can consume
7
+ - Responds to custom actions for logging, variable updates, and alert triggering
8
+ - Triggers a sample alert effect using configurable colors and duration
9
+ - Shows how to react to setting changes inside `onsettingsupdate`
10
+
11
+ Use the CLI to copy and customise the template:
12
+
13
+ ```
14
+ npx @lumiastream/plugin-cli create my-plugin
15
+ ```
16
+
17
+ After scaffolding you can tailor the manifest, code, and README to match your idea.
@@ -0,0 +1,143 @@
1
+ const { Plugin } = require('@lumiastream/plugin-sdk');
2
+
3
+ const VARIABLE_NAMES = {
4
+ lastMessage: 'last_message',
5
+ lastAlertColor: 'last_alert_color',
6
+ };
7
+
8
+ const DEFAULTS = {
9
+ welcomeMessage: 'Hello from Showcase Plugin!',
10
+ color: '#00c2ff',
11
+ alertDuration: 5,
12
+ };
13
+
14
+ class ShowcasePluginTemplate extends Plugin {
15
+ async onload() {
16
+ const message = this._currentMessage();
17
+ await this._log('Plugin loaded');
18
+ await this._rememberMessage(message);
19
+
20
+ if (this.settings.autoAlert === 'load') {
21
+ await this._triggerSampleAlert({
22
+ color: this.settings.favoriteColor,
23
+ duration: DEFAULTS.alertDuration,
24
+ });
25
+ }
26
+ }
27
+
28
+ async onunload() {
29
+ await this._log('Plugin unloaded');
30
+ }
31
+
32
+ async onsettingsupdate(settings, previous = {}) {
33
+ await this._log('Settings updated');
34
+
35
+ if (settings?.welcomeMessage && settings.welcomeMessage !== previous?.welcomeMessage) {
36
+ await this._rememberMessage(settings.welcomeMessage);
37
+ }
38
+
39
+ if (settings?.autoAlert === 'load' && previous?.autoAlert !== 'load') {
40
+ await this._log('Auto alert configured to fire on load');
41
+ }
42
+ }
43
+
44
+ async actions(config = {}) {
45
+ const actions = Array.isArray(config.actions) ? config.actions : [];
46
+ for (const action of actions) {
47
+ switch (action?.type) {
48
+ case 'log_message':
49
+ await this._handleLogMessage(action.data);
50
+ break;
51
+ case 'update_variable':
52
+ await this._handleUpdateVariable(action.data);
53
+ break;
54
+ case 'trigger_alert':
55
+ await this._triggerSampleAlert(action.data);
56
+ break;
57
+ default:
58
+ await this._log(`Unknown action type: ${action?.type ?? 'undefined'}`);
59
+ }
60
+ }
61
+ }
62
+
63
+ _tag() {
64
+ return `[${this.manifest?.id ?? 'showcase-plugin'}]`;
65
+ }
66
+
67
+ _currentMessage() {
68
+ return (
69
+ this.settings?.welcomeMessage ||
70
+ `Hello from ${this.manifest?.name ?? 'Showcase Plugin'}!`
71
+ );
72
+ }
73
+
74
+ async _log(message, severity = 'info') {
75
+ const prefix = this._tag();
76
+ const decorated =
77
+ severity === 'warn'
78
+ ? `${prefix} ⚠️ ${message}`
79
+ : severity === 'error'
80
+ ? `${prefix} ❌ ${message}`
81
+ : `${prefix} ${message}`;
82
+
83
+ await this.lumia.addLog(decorated);
84
+ }
85
+
86
+ async _rememberMessage(value) {
87
+ await this.lumia.setVariable(VARIABLE_NAMES.lastMessage, value);
88
+ }
89
+
90
+ async _handleLogMessage(data = {}) {
91
+ const message = data?.message || this._currentMessage();
92
+ const severity = data?.severity || 'info';
93
+
94
+ await this._log(message, severity);
95
+
96
+ if (typeof this.lumia.showToast === 'function') {
97
+ await this.lumia.showToast({
98
+ message: `${this.manifest?.name ?? 'Plugin'}: ${message}`,
99
+ time: 4,
100
+ });
101
+ }
102
+
103
+ if (this.settings.autoAlert === 'after-log') {
104
+ await this._triggerSampleAlert({
105
+ color: this.settings.favoriteColor,
106
+ duration: DEFAULTS.alertDuration,
107
+ });
108
+ }
109
+ }
110
+
111
+ async _handleUpdateVariable(data = {}) {
112
+ const value = data?.value ?? new Date().toISOString();
113
+ await this._rememberMessage(value);
114
+ await this._log(`Stored variable value: ${value}`);
115
+ }
116
+
117
+ async _triggerSampleAlert(data = {}) {
118
+ const color =
119
+ data?.color ||
120
+ this.settings?.favoriteColor ||
121
+ DEFAULTS.color;
122
+ const duration = Number(data?.duration) || DEFAULTS.alertDuration;
123
+
124
+ try {
125
+ const success = await this.lumia.triggerAlert({
126
+ alert: 'sample_light',
127
+ extraSettings: { color, duration },
128
+ });
129
+
130
+ if (!success) {
131
+ await this._log('Sample alert reported failure', 'warn');
132
+ return;
133
+ }
134
+
135
+ await this.lumia.setVariable(VARIABLE_NAMES.lastAlertColor, color);
136
+ await this._log(`Triggered sample alert with color ${color} for ${duration}s`);
137
+ } catch (error) {
138
+ await this._log(`Failed to trigger sample alert: ${error.message ?? error}`, 'error');
139
+ }
140
+ }
141
+ }
142
+
143
+ module.exports = ShowcasePluginTemplate;
@@ -0,0 +1,151 @@
1
+ {
2
+ "id": "showcase-plugin",
3
+ "name": "Showcase Plugin",
4
+ "version": "1.0.0",
5
+ "author": "LumiaStream",
6
+ "email": "",
7
+ "website": "",
8
+ "repository": "",
9
+ "description": "Sample plugin that demonstrates Lumia Stream logging, variables, alerts, and settings.",
10
+ "longDescription": "",
11
+ "license": "MIT",
12
+ "lumiaVersion": "^9.0.0",
13
+ "keywords": ["sample", "demo", "lumia"],
14
+ "category": "examples",
15
+ "icon": "",
16
+ "screenshots": [],
17
+ "changelog": "",
18
+ "config": {
19
+ "settings": [
20
+ {
21
+ "key": "welcomeMessage",
22
+ "label": "Welcome Message",
23
+ "type": "text",
24
+ "defaultValue": "Hello from Showcase Plugin!",
25
+ "helperText": "Shown when the plugin loads and stored in the sample variable."
26
+ },
27
+ {
28
+ "key": "favoriteColor",
29
+ "label": "Favorite Color",
30
+ "type": "color",
31
+ "defaultValue": "#00c2ff",
32
+ "helperText": "Used when triggering the sample light alert."
33
+ },
34
+ {
35
+ "key": "autoAlert",
36
+ "label": "Trigger Sample Alert",
37
+ "type": "select",
38
+ "defaultValue": "never",
39
+ "options": [
40
+ { "label": "Never", "value": "never" },
41
+ { "label": "On Load", "value": "load" },
42
+ { "label": "After Log Action", "value": "after-log" }
43
+ ],
44
+ "helperText": "Automatically fire the sample alert at different times."
45
+ }
46
+ ],
47
+ "actions": [
48
+ {
49
+ "type": "log_message",
50
+ "label": "Log Message",
51
+ "description": "Write a formatted message to the Lumia log panel and optionally trigger the sample alert.",
52
+ "fields": [
53
+ {
54
+ "key": "message",
55
+ "label": "Message",
56
+ "type": "text",
57
+ "defaultValue": "Hello from Showcase Plugin!"
58
+ },
59
+ {
60
+ "key": "severity",
61
+ "label": "Severity",
62
+ "type": "select",
63
+ "defaultValue": "info",
64
+ "options": [
65
+ { "label": "Info", "value": "info" },
66
+ { "label": "Warning", "value": "warn" },
67
+ { "label": "Error", "value": "error" }
68
+ ]
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "type": "update_variable",
74
+ "label": "Update Variable",
75
+ "description": "Persist a value into the sample Lumia variable.",
76
+ "fields": [
77
+ {
78
+ "key": "value",
79
+ "label": "Value",
80
+ "type": "text",
81
+ "defaultValue": "Triggered from an action"
82
+ }
83
+ ]
84
+ },
85
+ {
86
+ "type": "trigger_alert",
87
+ "label": "Trigger Sample Alert",
88
+ "description": "Fire the sample alert with optional overrides.",
89
+ "fields": [
90
+ {
91
+ "key": "color",
92
+ "label": "Color",
93
+ "type": "color",
94
+ "defaultValue": "#ff5f5f"
95
+ },
96
+ {
97
+ "key": "duration",
98
+ "label": "Duration (seconds)",
99
+ "type": "number",
100
+ "defaultValue": 5,
101
+ "min": 1,
102
+ "max": 60
103
+ }
104
+ ]
105
+ }
106
+ ],
107
+ "variables": [
108
+ {
109
+ "name": "last_message",
110
+ "system": false,
111
+ "origin": "showcase-plugin",
112
+ "allowedPlaces": ["automations", "overlays"],
113
+ "description": "Stores the most recent message handled by the plugin.",
114
+ "value": "",
115
+ "example": "Hello from Showcase Plugin!"
116
+ },
117
+ {
118
+ "name": "last_alert_color",
119
+ "system": false,
120
+ "origin": "showcase-plugin",
121
+ "allowedPlaces": ["automations", "overlays"],
122
+ "description": "Tracks the color used by the latest sample alert.",
123
+ "value": "",
124
+ "example": "#00c2ff"
125
+ }
126
+ ],
127
+ "alerts": [
128
+ {
129
+ "id": "sample_light",
130
+ "label": "Sample Light Alert",
131
+ "description": "Change lights to the selected color for a few seconds.",
132
+ "fields": [
133
+ {
134
+ "key": "color",
135
+ "label": "Color",
136
+ "type": "color",
137
+ "defaultValue": "#00c2ff"
138
+ },
139
+ {
140
+ "key": "duration",
141
+ "label": "Duration (seconds)",
142
+ "type": "number",
143
+ "defaultValue": 5,
144
+ "min": 1,
145
+ "max": 60
146
+ }
147
+ ]
148
+ }
149
+ ]
150
+ }
151
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "lumia-showcase-plugin-template",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Internal template illustrating logging, variables, actions, and alerts for Lumia Stream plugins.",
6
+ "main": "main.js",
7
+ "dependencies": {
8
+ "@lumiastream/plugin-sdk": "^0.1.0"
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "lumia-plugin",
3
+ "version": "0.1.4",
4
+ "description": "Command-line tools for creating, building, and validating Lumia Stream plugins.",
5
+ "bin": {
6
+ "lumia-plugin": "scripts/cli.js"
7
+ },
8
+ "files": [
9
+ "scripts",
10
+ "examples/base-plugin",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "keywords": [
15
+ "lumia",
16
+ "stream",
17
+ "plugin",
18
+ "cli"
19
+ ],
20
+ "author": "Lumia Stream",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "jszip": "3.10.1"
24
+ }
25
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const {
5
+ readManifest,
6
+ validateManifest,
7
+ collectFiles,
8
+ createPluginArchive,
9
+ } = require("./utils");
10
+
11
+ function parseArgs() {
12
+ const args = process.argv.slice(2);
13
+ const options = { dir: process.cwd(), outFile: null };
14
+ while (args.length) {
15
+ const arg = args.shift();
16
+ switch (arg) {
17
+ case "--dir":
18
+ case "-d":
19
+ options.dir = path.resolve(args.shift() || ".");
20
+ break;
21
+ case "--out":
22
+ case "-o":
23
+ options.outFile = path.resolve(args.shift() || "");
24
+ break;
25
+ case "--help":
26
+ case "-h":
27
+ printHelp();
28
+ process.exit(0);
29
+ default:
30
+ // treat as directory if first positional
31
+ if (!options._positional) {
32
+ options.dir = path.resolve(arg);
33
+ options._positional = true;
34
+ } else {
35
+ console.warn(`Ignoring unknown argument: ${arg}`);
36
+ }
37
+ break;
38
+ }
39
+ }
40
+ return options;
41
+ }
42
+
43
+ function printHelp() {
44
+ console.log(`Build a .lumiaplugin archive for distribution.
45
+
46
+ Usage: npx @lumiastream/plugin-cli build [options]
47
+
48
+ Options:
49
+ --dir, -d Plugin directory (defaults to cwd)
50
+ --out, -o Output file path (defaults to ./<id>-<version>.lumiaplugin)
51
+ --help, -h Show this help message
52
+ `);
53
+ }
54
+
55
+ async function main() {
56
+ const options = parseArgs();
57
+ const pluginDir = options.dir;
58
+
59
+ try {
60
+ await fs.promises.access(pluginDir, fs.constants.R_OK);
61
+ } catch (error) {
62
+ console.error(`✖ Unable to access plugin directory: ${pluginDir}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ try {
67
+ const { manifest } = await readManifest(pluginDir);
68
+ const errors = validateManifest(manifest);
69
+ if (errors.length) {
70
+ console.error("✖ Plugin manifest validation failed:");
71
+ errors.forEach((err) => console.error(` • ${err}`));
72
+ process.exit(1);
73
+ }
74
+
75
+ const files = await collectFiles(pluginDir);
76
+ if (!files.length) {
77
+ console.error(
78
+ "✖ No files found to package (did you point to the plugin root?)"
79
+ );
80
+ process.exit(1);
81
+ }
82
+
83
+ const filename = `${manifest.id}-${manifest.version}.lumiaplugin`;
84
+ const outputPath = options.outFile
85
+ ? options.outFile
86
+ : path.resolve(filename);
87
+ await createPluginArchive(pluginDir, outputPath, files);
88
+
89
+ console.log("✔ Plugin package created");
90
+ console.log(` - output: ${outputPath}`);
91
+ console.log(` - files: ${files.length}`);
92
+ } catch (error) {
93
+ console.error(`✖ Build failed: ${error.message}`);
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ main();
package/scripts/cli.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require("child_process");
3
+ const path = require("path");
4
+
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+
8
+ const commands = {
9
+ create: "create-plugin.js",
10
+ build: "build-plugin.js",
11
+ validate: "validate-plugin.js",
12
+ };
13
+
14
+ const scriptArgs = args.slice(1);
15
+
16
+ if (!command || !commands[command]) {
17
+ console.log(`Lumia Stream Plugin CLI
18
+
19
+ Usage: lumia-plugin <command> [options]
20
+
21
+ Commands:
22
+ create [directory] Create a new plugin from template
23
+ build [directory] Build a plugin into .lumiaplugin package
24
+ validate [file] Validate a .lumiaplugin package
25
+
26
+ Examples:
27
+ lumia-plugin create my-plugin
28
+ lumia-plugin build ./my-plugin
29
+ lumia-plugin validate my-plugin.lumiaplugin
30
+ `);
31
+ process.exit(command ? 1 : 0);
32
+ }
33
+
34
+ const scriptPath = path.join(__dirname, commands[command]);
35
+
36
+ const child = spawn("node", [scriptPath, ...scriptArgs], {
37
+ stdio: "inherit",
38
+ cwd: process.cwd(),
39
+ });
40
+
41
+ child.on("exit", (code) => {
42
+ process.exit(code || 0);
43
+ });
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ const TEMPLATE_DIR = path.resolve(__dirname, "..", "examples", "base-plugin");
6
+
7
+ function toKebabCase(value) {
8
+ return (
9
+ value
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, "-")
13
+ .replace(/^-+|-+$/g, "") || "my-plugin"
14
+ );
15
+ }
16
+
17
+ function toDisplayName(id) {
18
+ return id
19
+ .split("-")
20
+ .filter(Boolean)
21
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
22
+ .join(" ");
23
+ }
24
+
25
+ function printHelp() {
26
+ console.log(`Scaffold a new Lumia Stream plugin directory using the showcase template.\n\nUsage: npx @lumiastream/plugin-cli create [target-directory]\n\nExamples:\n npx @lumiastream/plugin-cli create ./plugins/my-plugin\n npx @lumiastream/plugin-cli create my-awesome-plugin\n`);
27
+ }
28
+
29
+ async function ensureEmptyDir(targetDir) {
30
+ try {
31
+ const stats = await fs.promises.stat(targetDir);
32
+ if (!stats.isDirectory()) {
33
+ throw new Error("Target path exists and is not a directory");
34
+ }
35
+ const contents = await fs.promises.readdir(targetDir);
36
+ if (contents.length > 0) {
37
+ throw new Error("Target directory must be empty");
38
+ }
39
+ } catch (error) {
40
+ if (error.code === "ENOENT") {
41
+ await fs.promises.mkdir(targetDir, { recursive: true });
42
+ return;
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ async function copyTemplate(src, dest) {
49
+ const stats = await fs.promises.stat(src);
50
+ if (stats.isDirectory()) {
51
+ await fs.promises.mkdir(dest, { recursive: true });
52
+ const entries = await fs.promises.readdir(src);
53
+ for (const entry of entries) {
54
+ await copyTemplate(path.join(src, entry), path.join(dest, entry));
55
+ }
56
+ } else if (stats.isFile()) {
57
+ await fs.promises.copyFile(src, dest);
58
+ }
59
+ }
60
+
61
+ function ensureArray(value) {
62
+ return Array.isArray(value) ? value : [];
63
+ }
64
+
65
+ async function updateManifest(manifestPath, pluginId, displayName) {
66
+ const raw = await fs.promises.readFile(manifestPath, "utf8");
67
+ const manifest = JSON.parse(raw);
68
+
69
+ manifest.id = pluginId;
70
+ manifest.name = displayName;
71
+ manifest.description = manifest.description?.trim()
72
+ ? manifest.description
73
+ : `Describe what ${displayName} does.`;
74
+ manifest.longDescription = manifest.longDescription || "";
75
+ manifest.repository = manifest.repository || "";
76
+ manifest.website = manifest.website || "";
77
+ manifest.email = manifest.email || "";
78
+ manifest.icon = manifest.icon || "";
79
+ manifest.screenshots = ensureArray(manifest.screenshots);
80
+ manifest.changelog = manifest.changelog || "";
81
+ manifest.keywords = ensureArray(manifest.keywords);
82
+
83
+ if (!manifest.keywords.includes(pluginId)) {
84
+ manifest.keywords.push(pluginId);
85
+ }
86
+
87
+ if (!manifest.main) {
88
+ manifest.main = "main.js";
89
+ }
90
+
91
+ if (manifest.config && typeof manifest.config === "object") {
92
+ const { settings, actions, variables } = manifest.config;
93
+
94
+ const welcomeSetting = ensureArray(settings).find(
95
+ (setting) => setting?.key === "welcomeMessage"
96
+ );
97
+ if (welcomeSetting) {
98
+ welcomeSetting.defaultValue = `Hello from ${displayName}!`;
99
+ }
100
+
101
+ const logAction = ensureArray(actions).find(
102
+ (action) => action?.type === "log_message"
103
+ );
104
+ if (logAction && Array.isArray(logAction.fields)) {
105
+ const messageField = logAction.fields.find((field) => field?.key === "message");
106
+ if (messageField) {
107
+ messageField.defaultValue = `Hello from ${displayName}!`;
108
+ }
109
+ }
110
+
111
+ for (const variable of ensureArray(variables)) {
112
+ if (!variable || typeof variable !== "object") continue;
113
+ if (variable.origin) {
114
+ variable.origin = pluginId;
115
+ }
116
+ if (variable.name === "last_message" && "example" in variable) {
117
+ variable.example = `Hello from ${displayName}!`;
118
+ }
119
+ }
120
+ }
121
+
122
+ await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
123
+ }
124
+
125
+ async function updateMain(mainPath, pluginId, className, displayName) {
126
+ let source = await fs.promises.readFile(mainPath, "utf8");
127
+ source = source.replace(
128
+ /class\s+ShowcasePluginTemplate\s+extends\s+Plugin/,
129
+ `class ${className} extends Plugin`
130
+ );
131
+ source = source.replace(
132
+ /module\.exports\s*=\s*ShowcasePluginTemplate;/,
133
+ `module.exports = ${className};`
134
+ );
135
+ source = source.replace(/Hello from Showcase Plugin!/g, `Hello from ${displayName}!`);
136
+ source = source.replace(/Showcase Plugin/g, displayName);
137
+ source = source.replace(/showcase-plugin/g, pluginId);
138
+ await fs.promises.writeFile(mainPath, source);
139
+ }
140
+
141
+ async function updatePackageJson(packagePath, pluginId, displayName) {
142
+ if (!fs.existsSync(packagePath)) return;
143
+ const pkg = JSON.parse(await fs.promises.readFile(packagePath, "utf8"));
144
+ pkg.name = `lumia-plugin-${pluginId}`;
145
+ pkg.description = pkg.description || `${displayName} for Lumia Stream`;
146
+ await fs.promises.writeFile(packagePath, JSON.stringify(pkg, null, 2));
147
+ }
148
+
149
+ async function updateReadme(readmePath, displayName) {
150
+ if (!fs.existsSync(readmePath)) return;
151
+ let content = await fs.promises.readFile(readmePath, "utf8");
152
+ content = content.replace(/^# .*$/m, `# ${displayName}`);
153
+ await fs.promises.writeFile(readmePath, content);
154
+ }
155
+
156
+ async function main() {
157
+ const args = process.argv.slice(2);
158
+ if (args.includes("--help") || args.includes("-h")) {
159
+ printHelp();
160
+ return;
161
+ }
162
+
163
+ if (!fs.existsSync(TEMPLATE_DIR)) {
164
+ console.error("✖ Template directory not found:", TEMPLATE_DIR);
165
+ process.exit(1);
166
+ }
167
+
168
+ const targetDir = path.resolve(args[0] || "my-plugin");
169
+ const pluginId = toKebabCase(path.basename(targetDir));
170
+ const displayName = toDisplayName(pluginId) || "My Plugin";
171
+ const className = displayName.replace(/[^a-zA-Z0-9]/g, "") || "MyPlugin";
172
+
173
+ try {
174
+ await ensureEmptyDir(targetDir);
175
+ await copyTemplate(TEMPLATE_DIR, targetDir);
176
+
177
+ await updateManifest(
178
+ path.join(targetDir, "manifest.json"),
179
+ pluginId,
180
+ displayName
181
+ );
182
+ await updateMain(
183
+ path.join(targetDir, "main.js"),
184
+ pluginId,
185
+ className,
186
+ displayName
187
+ );
188
+ await updatePackageJson(
189
+ path.join(targetDir, "package.json"),
190
+ pluginId,
191
+ displayName
192
+ );
193
+ await updateReadme(path.join(targetDir, "README.md"), displayName);
194
+
195
+ console.log("✔ Plugin scaffold created");
196
+ console.log(` - directory: ${targetDir}`);
197
+ console.log(" - manifest.json");
198
+ console.log(" - main.js");
199
+ if (fs.existsSync(path.join(targetDir, "README.md"))) {
200
+ console.log(" - README.md");
201
+ }
202
+ if (fs.existsSync(path.join(targetDir, "package.json"))) {
203
+ console.log(" - package.json");
204
+ }
205
+ } catch (error) {
206
+ console.error(`✖ Failed to create plugin: ${error.message}`);
207
+ process.exit(1);
208
+ }
209
+ }
210
+
211
+ main();
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const JSZip = require('jszip');
4
+
5
+ const DEFAULT_IGNORE = new Set([
6
+ '.git',
7
+ '.DS_Store',
8
+ 'Thumbs.db',
9
+ 'package-lock.json',
10
+ 'yarn.lock',
11
+ '.npmrc',
12
+ '.gitignore',
13
+ ]);
14
+
15
+ function toPosix(p) {
16
+ return p.split(path.sep).join('/');
17
+ }
18
+
19
+ async function readManifest(pluginDir) {
20
+ const manifestPath = path.join(pluginDir, 'manifest.json');
21
+ const raw = await fs.promises.readFile(manifestPath, 'utf8');
22
+ return { manifest: JSON.parse(raw), manifestPath };
23
+ }
24
+
25
+ function validateManifest(manifest) {
26
+ const errors = [];
27
+ const requiredStringFields = ['id', 'name', 'version', 'author', 'description', 'license', 'lumiaVersion'];
28
+ for (const field of requiredStringFields) {
29
+ const value = manifest[field];
30
+ if (typeof value !== 'string' || value.trim().length === 0) {
31
+ errors.push(`Missing or invalid manifest field: ${field}`);
32
+ }
33
+ }
34
+
35
+ if (!manifest.category || (typeof manifest.category !== 'string' && !Array.isArray(manifest.category))) {
36
+ errors.push('Manifest must declare a category string');
37
+ }
38
+
39
+ if (!manifest.config || typeof manifest.config !== 'object') {
40
+ errors.push('Manifest config must be an object');
41
+ }
42
+
43
+ if (manifest.config) {
44
+ if (manifest.config.settings && !Array.isArray(manifest.config.settings)) {
45
+ errors.push('config.settings must be an array when provided');
46
+ }
47
+ if (manifest.config.actions && !Array.isArray(manifest.config.actions)) {
48
+ errors.push('config.actions must be an array when provided');
49
+ }
50
+ if (manifest.config.variables && !Array.isArray(manifest.config.variables)) {
51
+ errors.push('config.variables must be an array when provided');
52
+ }
53
+ if (manifest.config.alerts && !Array.isArray(manifest.config.alerts)) {
54
+ errors.push('config.alerts must be an array when provided');
55
+ }
56
+ }
57
+
58
+ return errors;
59
+ }
60
+
61
+ async function collectFiles(pluginDir, ignore = DEFAULT_IGNORE) {
62
+ const entries = [];
63
+
64
+ async function walk(currentDir) {
65
+ const list = await fs.promises.readdir(currentDir, { withFileTypes: true });
66
+ for (const entry of list) {
67
+ if (ignore.has(entry.name)) continue;
68
+ const absolute = path.join(currentDir, entry.name);
69
+ const relative = path.relative(pluginDir, absolute);
70
+ if (!relative || relative.startsWith('..')) continue;
71
+
72
+ // Skip bundled Lumia packages in node_modules
73
+ if (relative === 'node_modules/@lumiastream'
74
+ || relative.startsWith('node_modules/@lumiastream/plugin-sdk')
75
+ || relative.startsWith('node_modules/@lumiastream/plugin-cli')) {
76
+ continue;
77
+ }
78
+
79
+ if (entry.isDirectory()) {
80
+ await walk(absolute);
81
+ } else if (entry.isFile()) {
82
+ entries.push({ absolute, relative: toPosix(relative) });
83
+ }
84
+ }
85
+ }
86
+
87
+ await walk(pluginDir);
88
+ return entries;
89
+ }
90
+
91
+ async function createPluginArchive(pluginDir, outputFile, files) {
92
+ const zip = new JSZip();
93
+ for (const file of files) {
94
+ const data = await fs.promises.readFile(file.absolute);
95
+ zip.file(file.relative, data);
96
+ }
97
+ const content = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 9 } });
98
+ await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
99
+ await fs.promises.writeFile(outputFile, content);
100
+ return outputFile;
101
+ }
102
+
103
+ module.exports = {
104
+ readManifest,
105
+ validateManifest,
106
+ collectFiles,
107
+ createPluginArchive,
108
+ DEFAULT_IGNORE,
109
+ };
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { readManifest, validateManifest } = require('./utils');
5
+
6
+ async function main() {
7
+ const target = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
8
+
9
+ try {
10
+ await fs.promises.access(target, fs.constants.R_OK);
11
+ } catch (error) {
12
+ console.error(`✖ Unable to access plugin directory: ${target}`);
13
+ process.exit(1);
14
+ }
15
+
16
+ try {
17
+ const { manifest, manifestPath } = await readManifest(target);
18
+ const errors = validateManifest(manifest);
19
+
20
+ const mainFile = manifest.main || 'main.js';
21
+ const mainPath = path.join(target, mainFile);
22
+ if (!fs.existsSync(mainPath)) {
23
+ errors.push(`Main file not found: ${mainFile}`);
24
+ }
25
+
26
+ if (manifest.config?.actions) {
27
+ manifest.config.actions.forEach((action, index) => {
28
+ if (!action.type) {
29
+ errors.push(`config.actions[${index}] is missing required field "type"`);
30
+ }
31
+ if (!Array.isArray(action.fields)) {
32
+ errors.push(`config.actions[${index}].fields must be an array`);
33
+ }
34
+ });
35
+ }
36
+
37
+ if (manifest.config?.settings) {
38
+ manifest.config.settings.forEach((setting, index) => {
39
+ if (!setting.key) {
40
+ errors.push(`config.settings[${index}] is missing required field "key"`);
41
+ }
42
+ if (!setting.type) {
43
+ errors.push(`config.settings[${index}] is missing required field "type"`);
44
+ }
45
+ });
46
+ }
47
+
48
+ if (errors.length) {
49
+ console.error('✖ Plugin validation failed:');
50
+ errors.forEach((err) => console.error(` • ${err}`));
51
+ process.exit(1);
52
+ }
53
+
54
+ console.log('✔ Plugin manifest passed validation');
55
+ console.log(` - manifest: ${manifestPath}`);
56
+ console.log(` - main: ${mainFile}`);
57
+ } catch (error) {
58
+ console.error(`✖ Validation error: ${error.message}`);
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ main();