lumia-plugin 0.1.4 → 0.1.6

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 CHANGED
@@ -1,13 +1,13 @@
1
1
  # Lumia Stream Plugin CLI
2
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.
3
+ The `lumia-plugin` package bundles the command-line tools for creating, building, and validating Lumia Stream plugins without pulling the full SDK into your runtime dependencies.
4
4
 
5
5
  ## Commands
6
6
 
7
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>
8
+ npx lumia-plugin create <directory>
9
+ npx lumia-plugin build [--dir <path>] [--out <file>]
10
+ npx lumia-plugin validate <plugin-directory>
11
11
  ```
12
12
 
13
13
  Run any command with `--help` to see detailed options.
@@ -0,0 +1,243 @@
1
+ # Getting Started with Showcase Plugin Template
2
+
3
+ Welcome to your new Lumia Stream plugin! This guide will help you understand the plugin structure and start building your custom functionality.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ your-plugin/
9
+ ├── manifest.json # Plugin metadata and configuration
10
+ ├── main.js # Your plugin's main code
11
+ ├── package.json # npm dependencies (optional)
12
+ └── README.md # Plugin documentation
13
+ ```
14
+
15
+ ## Understanding the Files
16
+
17
+ ### 📋 manifest.json
18
+
19
+ This file defines your plugin's metadata and capabilities:
20
+
21
+ - **id**: Unique identifier (kebab-case, e.g., "my-awesome-plugin")
22
+ - **name**: Display name shown to users
23
+ - **description**: Short description for the plugin marketplace
24
+ - **main**: Entry point file (usually "main.js")
25
+ - **config**: Defines settings, actions, and variables
26
+
27
+ **Key sections:**
28
+ - `settings`: User-configurable options for your plugin
29
+ - `actions`: Functions users can trigger (e.g., send a message, change color)
30
+ - `variables`: Data your plugin exposes to Lumia Stream
31
+
32
+ ### 🔧 main.js
33
+
34
+ Your plugin's logic lives here. The main class extends the `Plugin` base class:
35
+
36
+ ```javascript
37
+ class YourPlugin extends Plugin {
38
+ constructor(props) {
39
+ super(props);
40
+ // Initialize your plugin
41
+ }
42
+
43
+ async onload() {
44
+ // Called when plugin is loaded
45
+ }
46
+
47
+ async onsettingsupdate() {
48
+ // Called when settings change
49
+ }
50
+
51
+ async onunload() {
52
+ // Called when plugin is unloaded
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Common Tasks
58
+
59
+ ### Adding a New Setting
60
+
61
+ 1. Edit `manifest.json` in the `config.settings` array:
62
+
63
+ ```json
64
+ {
65
+ "key": "myNewSetting",
66
+ "label": "My New Setting",
67
+ "type": "text",
68
+ "defaultValue": "default value",
69
+ "required": false
70
+ }
71
+ ```
72
+
73
+ 2. Access in `main.js`:
74
+
75
+ ```javascript
76
+ const value = this.getSetting('myNewSetting');
77
+ ```
78
+
79
+ ### Adding a New Action
80
+
81
+ 1. Define in `manifest.json` under `config.actions`:
82
+
83
+ ```json
84
+ {
85
+ "type": "my_custom_action",
86
+ "label": "Do Something Cool",
87
+ "fields": [
88
+ {
89
+ "key": "message",
90
+ "label": "Message",
91
+ "type": "text",
92
+ "required": true
93
+ }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ 2. Handle in `main.js`:
99
+
100
+ ```javascript
101
+ async onAction(action) {
102
+ if (action.type === 'my_custom_action') {
103
+ const message = action.fields.message;
104
+ // Your logic here
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### Creating Variables
110
+
111
+ Variables let other Lumia features access your plugin's data:
112
+
113
+ 1. Define in `manifest.json` under `config.variables`:
114
+
115
+ ```json
116
+ {
117
+ "name": "my_variable",
118
+ "key": "myVariable",
119
+ "origin": "your-plugin-id",
120
+ "type": "string",
121
+ "example": "Sample value"
122
+ }
123
+ ```
124
+
125
+ 2. Update in `main.js`:
126
+
127
+ ```javascript
128
+ this.setVariable('myVariable', 'new value');
129
+ ```
130
+
131
+ ## Testing Your Plugin
132
+
133
+ 1. **Install dependencies** (if using package.json):
134
+ ```bash
135
+ npm install
136
+ ```
137
+
138
+ 2. **Load in Lumia Stream**:
139
+ - Open Lumia Stream
140
+ - Go to Plugins section
141
+ - Load your plugin directory
142
+ - Enable your plugin
143
+
144
+ 3. **Check logs**:
145
+ ```javascript
146
+ this.log('Debug message');
147
+ this.error('Error message');
148
+ ```
149
+
150
+ ## Next Steps
151
+
152
+ - [ ] Customize `manifest.json` with your plugin details
153
+ - [ ] Update the plugin name and description
154
+ - [ ] Add your custom settings
155
+ - [ ] Implement your actions
156
+ - [ ] Test your plugin in Lumia Stream
157
+ - [ ] Write tests (optional)
158
+ - [ ] Update README.md with usage instructions
159
+
160
+ ## API Reference
161
+
162
+ ### Plugin Methods
163
+
164
+ - `this.getSetting(key)` - Get a setting value
165
+ - `this.setVariable(key, value)` - Update a variable
166
+ - `this.log(message)` - Log info message
167
+ - `this.error(message)` - Log error message
168
+ - `this.sendAlert(type, data)` - Trigger an alert
169
+
170
+ ### Lifecycle Hooks
171
+
172
+ - `onload()` - Plugin initialization
173
+ - `onunload()` - Cleanup when plugin is disabled
174
+ - `onsettingsupdate()` - Respond to setting changes
175
+ - `onAction(action)` - Handle triggered actions
176
+
177
+ ## Common Patterns
178
+
179
+ ### Making HTTP Requests
180
+
181
+ ```javascript
182
+ const response = await fetch('https://api.example.com/data');
183
+ const data = await response.json();
184
+ ```
185
+
186
+ ### Using Timers
187
+
188
+ ```javascript
189
+ async onload() {
190
+ this.interval = setInterval(() => {
191
+ // Do something periodically
192
+ }, 5000);
193
+ }
194
+
195
+ async onunload() {
196
+ if (this.interval) {
197
+ clearInterval(this.interval);
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Error Handling
203
+
204
+ ```javascript
205
+ try {
206
+ // Your code
207
+ } catch (error) {
208
+ this.error(`Something went wrong: ${error.message}`);
209
+ }
210
+ ```
211
+
212
+ ## Troubleshooting
213
+
214
+ **Plugin not loading?**
215
+ - Check manifest.json syntax (use a JSON validator)
216
+ - Ensure the `main` field points to the correct file
217
+ - Check Lumia Stream logs for errors
218
+
219
+ **Settings not showing?**
220
+ - Verify the `settings` array in manifest.json
221
+ - Ensure required fields are filled
222
+
223
+ **Actions not working?**
224
+ - Check the action type matches your handler
225
+ - Verify field keys match what you're accessing
226
+
227
+ ## Resources
228
+
229
+ - [Lumia Stream Documentation](https://docs.lumiastream.com)
230
+ - [Plugin API Reference](https://docs.lumiastream.com/plugins/api)
231
+ - [Example Plugins](https://github.com/lumiastream/plugins)
232
+ - [Community Discord](https://discord.gg/lumiastream)
233
+
234
+ ## Need Help?
235
+
236
+ - Join the Lumia Stream Discord community
237
+ - Check the documentation
238
+ - Review example plugins
239
+ - Ask questions in the developer channel
240
+
241
+ ---
242
+
243
+ Happy coding! 🚀
@@ -11,7 +11,7 @@ This template demonstrates a handful of common Lumia Stream plugin capabilities:
11
11
  Use the CLI to copy and customise the template:
12
12
 
13
13
  ```
14
- npx @lumiastream/plugin-cli create my-plugin
14
+ npx lumia-plugin create my-plugin
15
15
  ```
16
16
 
17
17
  After scaffolding you can tailor the manifest, code, and README to match your idea.
@@ -1,143 +1,150 @@
1
- const { Plugin } = require('@lumiastream/plugin-sdk');
1
+ const { Plugin } = require("@lumiastream/plugin");
2
2
 
3
3
  const VARIABLE_NAMES = {
4
- lastMessage: 'last_message',
5
- lastAlertColor: 'last_alert_color',
4
+ lastMessage: "last_message",
5
+ lastAlertColor: "last_alert_color",
6
6
  };
7
7
 
8
8
  const DEFAULTS = {
9
- welcomeMessage: 'Hello from Showcase Plugin!',
10
- color: '#00c2ff',
11
- alertDuration: 5,
9
+ welcomeMessage: "Hello from Showcase Plugin!",
10
+ color: "#00c2ff",
11
+ alertDuration: 5,
12
12
  };
13
13
 
14
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
- }
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 (
36
+ settings?.welcomeMessage &&
37
+ settings.welcomeMessage !== previous?.welcomeMessage
38
+ ) {
39
+ await this._rememberMessage(settings.welcomeMessage);
40
+ }
41
+
42
+ if (settings?.autoAlert === "load" && previous?.autoAlert !== "load") {
43
+ await this._log("Auto alert configured to fire on load");
44
+ }
45
+ }
46
+
47
+ async actions(config = {}) {
48
+ const actions = Array.isArray(config.actions) ? config.actions : [];
49
+ for (const action of actions) {
50
+ switch (action?.type) {
51
+ case "log_message":
52
+ await this._handleLogMessage(action.data);
53
+ break;
54
+ case "update_variable":
55
+ await this._handleUpdateVariable(action.data);
56
+ break;
57
+ case "trigger_alert":
58
+ await this._triggerSampleAlert(action.data);
59
+ break;
60
+ default:
61
+ await this._log(
62
+ `Unknown action type: ${action?.type ?? "undefined"}`
63
+ );
64
+ }
65
+ }
66
+ }
67
+
68
+ _tag() {
69
+ return `[${this.manifest?.id ?? "showcase-plugin"}]`;
70
+ }
71
+
72
+ _currentMessage() {
73
+ return (
74
+ this.settings?.welcomeMessage ||
75
+ `Hello from ${this.manifest?.name ?? "Showcase Plugin"}!`
76
+ );
77
+ }
78
+
79
+ async _log(message, severity = "info") {
80
+ const prefix = this._tag();
81
+ const decorated =
82
+ severity === "warn"
83
+ ? `${prefix} ⚠️ ${message}`
84
+ : severity === "error"
85
+ ? `${prefix} ❌ ${message}`
86
+ : `${prefix} ${message}`;
87
+
88
+ await this.lumia.addLog(decorated);
89
+ }
90
+
91
+ async _rememberMessage(value) {
92
+ await this.lumia.setVariable(VARIABLE_NAMES.lastMessage, value);
93
+ }
94
+
95
+ async _handleLogMessage(data = {}) {
96
+ const message = data?.message || this._currentMessage();
97
+ const severity = data?.severity || "info";
98
+
99
+ await this._log(message, severity);
100
+
101
+ if (typeof this.lumia.showToast === "function") {
102
+ await this.lumia.showToast({
103
+ message: `${this.manifest?.name ?? "Plugin"}: ${message}`,
104
+ time: 4,
105
+ });
106
+ }
107
+
108
+ if (this.settings.autoAlert === "after-log") {
109
+ await this._triggerSampleAlert({
110
+ color: this.settings.favoriteColor,
111
+ duration: DEFAULTS.alertDuration,
112
+ });
113
+ }
114
+ }
115
+
116
+ async _handleUpdateVariable(data = {}) {
117
+ const value = data?.value ?? new Date().toISOString();
118
+ await this._rememberMessage(value);
119
+ await this._log(`Stored variable value: ${value}`);
120
+ }
121
+
122
+ async _triggerSampleAlert(data = {}) {
123
+ const color = data?.color || this.settings?.favoriteColor || DEFAULTS.color;
124
+ const duration = Number(data?.duration) || DEFAULTS.alertDuration;
125
+
126
+ try {
127
+ const success = await this.lumia.triggerAlert({
128
+ alert: "sample_light",
129
+ extraSettings: { color, duration },
130
+ });
131
+
132
+ if (!success) {
133
+ await this._log("Sample alert reported failure", "warn");
134
+ return;
135
+ }
136
+
137
+ await this.lumia.setVariable(VARIABLE_NAMES.lastAlertColor, color);
138
+ await this._log(
139
+ `Triggered sample alert with color ${color} for ${duration}s`
140
+ );
141
+ } catch (error) {
142
+ await this._log(
143
+ `Failed to trigger sample alert: ${error.message ?? error}`,
144
+ "error"
145
+ );
146
+ }
147
+ }
141
148
  }
142
149
 
143
150
  module.exports = ShowcasePluginTemplate;
@@ -1,10 +1,10 @@
1
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
- }
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": "^0.1.0"
9
+ }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumia-plugin",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Command-line tools for creating, building, and validating Lumia Stream plugins.",
5
5
  "bin": {
6
6
  "lumia-plugin": "scripts/cli.js"
@@ -43,7 +43,7 @@ function parseArgs() {
43
43
  function printHelp() {
44
44
  console.log(`Build a .lumiaplugin archive for distribution.
45
45
 
46
- Usage: npx @lumiastream/plugin-cli build [options]
46
+ Usage: npx lumia-plugin build [options]
47
47
 
48
48
  Options:
49
49
  --dir, -d Plugin directory (defaults to cwd)
@@ -5,207 +5,264 @@ const fs = require("fs");
5
5
  const TEMPLATE_DIR = path.resolve(__dirname, "..", "examples", "base-plugin");
6
6
 
7
7
  function toKebabCase(value) {
8
- return (
9
- value
10
- .trim()
11
- .toLowerCase()
12
- .replace(/[^a-z0-9]+/g, "-")
13
- .replace(/^-+|-+$/g, "") || "my-plugin"
14
- );
8
+ return (
9
+ value
10
+ .trim()
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, "-")
13
+ .replace(/^-+|-+$/g, "") || "my-plugin"
14
+ );
15
15
  }
16
16
 
17
17
  function toDisplayName(id) {
18
- return id
19
- .split("-")
20
- .filter(Boolean)
21
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
22
- .join(" ");
18
+ return id
19
+ .split("-")
20
+ .filter(Boolean)
21
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
22
+ .join(" ");
23
23
  }
24
24
 
25
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`);
26
+ console.log(
27
+ `Scaffold a new Lumia Stream plugin directory using the showcase template.\n\nUsage: npx lumia-plugin create [target-directory]\n\nExamples:\n npx lumia-plugin create ./plugins/my-plugin\n npx lumia-plugin create my-awesome-plugin\n`
28
+ );
27
29
  }
28
30
 
29
31
  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
- }
32
+ try {
33
+ const stats = await fs.promises.stat(targetDir);
34
+ if (!stats.isDirectory()) {
35
+ throw new Error(
36
+ `Path '${path.basename(
37
+ targetDir
38
+ )}' exists and is not a directory.\n Tip: Choose a different name or remove the existing file.`
39
+ );
40
+ }
41
+ const contents = await fs.promises.readdir(targetDir);
42
+ if (contents.length > 0) {
43
+ throw new Error(
44
+ `Directory '${path.basename(
45
+ targetDir
46
+ )}' already exists and is not empty.\n Tip: Use a different name or remove existing files first.`
47
+ );
48
+ }
49
+ } catch (error) {
50
+ if (error.code === "ENOENT") {
51
+ await fs.promises.mkdir(targetDir, { recursive: true });
52
+ return;
53
+ }
54
+ throw error;
55
+ }
46
56
  }
47
57
 
48
58
  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
+ const stats = await fs.promises.stat(src);
60
+ if (stats.isDirectory()) {
61
+ await fs.promises.mkdir(dest, { recursive: true });
62
+ const entries = await fs.promises.readdir(src);
63
+ for (const entry of entries) {
64
+ await copyTemplate(path.join(src, entry), path.join(dest, entry));
65
+ }
66
+ } else if (stats.isFile()) {
67
+ await fs.promises.copyFile(src, dest);
68
+ }
59
69
  }
60
70
 
61
71
  function ensureArray(value) {
62
- return Array.isArray(value) ? value : [];
72
+ return Array.isArray(value) ? value : [];
63
73
  }
64
74
 
65
75
  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));
76
+ const raw = await fs.promises.readFile(manifestPath, "utf8");
77
+ const manifest = JSON.parse(raw);
78
+
79
+ manifest.id = pluginId;
80
+ manifest.name = displayName;
81
+ manifest.description = manifest.description?.trim()
82
+ ? manifest.description
83
+ : `Describe what ${displayName} does.`;
84
+ manifest.longDescription = manifest.longDescription || "";
85
+ manifest.repository = manifest.repository || "";
86
+ manifest.website = manifest.website || "";
87
+ manifest.email = manifest.email || "";
88
+ manifest.icon = manifest.icon || "";
89
+ manifest.screenshots = ensureArray(manifest.screenshots);
90
+ manifest.changelog = manifest.changelog || "";
91
+ manifest.keywords = ensureArray(manifest.keywords);
92
+
93
+ if (!manifest.keywords.includes(pluginId)) {
94
+ manifest.keywords.push(pluginId);
95
+ }
96
+
97
+ if (!manifest.main) {
98
+ manifest.main = "main.js";
99
+ }
100
+
101
+ if (manifest.config && typeof manifest.config === "object") {
102
+ const { settings, actions, variables } = manifest.config;
103
+
104
+ const welcomeSetting = ensureArray(settings).find(
105
+ (setting) => setting?.key === "welcomeMessage"
106
+ );
107
+ if (welcomeSetting) {
108
+ welcomeSetting.defaultValue = `Hello from ${displayName}!`;
109
+ }
110
+
111
+ const logAction = ensureArray(actions).find(
112
+ (action) => action?.type === "log_message"
113
+ );
114
+ if (logAction && Array.isArray(logAction.fields)) {
115
+ const messageField = logAction.fields.find(
116
+ (field) => field?.key === "message"
117
+ );
118
+ if (messageField) {
119
+ messageField.defaultValue = `Hello from ${displayName}!`;
120
+ }
121
+ }
122
+
123
+ for (const variable of ensureArray(variables)) {
124
+ if (!variable || typeof variable !== "object") continue;
125
+ if (variable.origin) {
126
+ variable.origin = pluginId;
127
+ }
128
+ if (variable.name === "last_message" && "example" in variable) {
129
+ variable.example = `Hello from ${displayName}!`;
130
+ }
131
+ }
132
+ }
133
+
134
+ await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
123
135
  }
124
136
 
125
137
  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);
138
+ let source = await fs.promises.readFile(mainPath, "utf8");
139
+ source = source.replace(
140
+ /class\s+ShowcasePluginTemplate\s+extends\s+Plugin/,
141
+ `class ${className} extends Plugin`
142
+ );
143
+ source = source.replace(
144
+ /module\.exports\s*=\s*ShowcasePluginTemplate;/,
145
+ `module.exports = ${className};`
146
+ );
147
+ source = source.replace(
148
+ /Hello from Showcase Plugin!/g,
149
+ `Hello from ${displayName}!`
150
+ );
151
+ source = source.replace(/Showcase Plugin/g, displayName);
152
+ source = source.replace(/showcase-plugin/g, pluginId);
153
+ await fs.promises.writeFile(mainPath, source);
139
154
  }
140
155
 
141
156
  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));
157
+ if (!fs.existsSync(packagePath)) return;
158
+ const pkg = JSON.parse(await fs.promises.readFile(packagePath, "utf8"));
159
+ pkg.name = `lumia-plugin-${pluginId}`;
160
+ pkg.description = pkg.description || `${displayName} for Lumia Stream`;
161
+ await fs.promises.writeFile(packagePath, JSON.stringify(pkg, null, 2));
147
162
  }
148
163
 
149
164
  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);
165
+ if (!fs.existsSync(readmePath)) return;
166
+ let content = await fs.promises.readFile(readmePath, "utf8");
167
+ content = content.replace(/^# .*$/m, `# ${displayName}`);
168
+ await fs.promises.writeFile(readmePath, content);
169
+ }
170
+
171
+ async function updateGettingStarted(gettingStartedPath, displayName) {
172
+ if (!fs.existsSync(gettingStartedPath)) return;
173
+ let content = await fs.promises.readFile(gettingStartedPath, "utf8");
174
+ content = content.replace(
175
+ /^# Getting Started with .*$/m,
176
+ `# Getting Started with ${displayName}`
177
+ );
178
+ content = content.replace(/Showcase Plugin Template/g, displayName);
179
+ await fs.promises.writeFile(gettingStartedPath, content);
154
180
  }
155
181
 
156
182
  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
- }
183
+ const args = process.argv.slice(2);
184
+ if (args.includes("--help") || args.includes("-h")) {
185
+ printHelp();
186
+ return;
187
+ }
188
+
189
+ if (!fs.existsSync(TEMPLATE_DIR)) {
190
+ console.error("✖ Template directory not found:", TEMPLATE_DIR);
191
+ console.error(" Tip: Make sure lumia-plugin is properly installed.");
192
+ process.exit(1);
193
+ }
194
+
195
+ const targetDir = path.resolve(args[0] || "my-plugin");
196
+ const pluginId = toKebabCase(path.basename(targetDir));
197
+ const displayName = toDisplayName(pluginId) || "My Plugin";
198
+ const className = displayName.replace(/[^a-zA-Z0-9]/g, "") || "MyPlugin";
199
+
200
+ try {
201
+ console.log("⏳ Creating plugin scaffold...\n");
202
+
203
+ console.log(" 📁 Creating directory...");
204
+ await ensureEmptyDir(targetDir);
205
+
206
+ console.log(" 📋 Copying template files...");
207
+ await copyTemplate(TEMPLATE_DIR, targetDir);
208
+
209
+ console.log(" ✏️ Updating manifest.json...");
210
+ await updateManifest(
211
+ path.join(targetDir, "manifest.json"),
212
+ pluginId,
213
+ displayName
214
+ );
215
+
216
+ console.log(" ✏️ Updating main.js...");
217
+ await updateMain(
218
+ path.join(targetDir, "main.js"),
219
+ pluginId,
220
+ className,
221
+ displayName
222
+ );
223
+
224
+ console.log(" ✏️ Updating package.json...");
225
+ await updatePackageJson(
226
+ path.join(targetDir, "package.json"),
227
+ pluginId,
228
+ displayName
229
+ );
230
+
231
+ console.log(" ✏️ Updating README.md...");
232
+ await updateReadme(path.join(targetDir, "README.md"), displayName);
233
+
234
+ console.log(" ✏️ Updating GETTING_STARTED.md...");
235
+ await updateGettingStarted(
236
+ path.join(targetDir, "GETTING_STARTED.md"),
237
+ displayName
238
+ );
239
+
240
+ console.log("\n✅ Plugin scaffold created successfully!\n");
241
+ console.log("📦 Created files:");
242
+ console.log(` - ${targetDir}/`);
243
+ console.log(" - manifest.json");
244
+ console.log(" - main.js");
245
+ if (fs.existsSync(path.join(targetDir, "README.md"))) {
246
+ console.log(" - README.md");
247
+ }
248
+ if (fs.existsSync(path.join(targetDir, "GETTING_STARTED.md"))) {
249
+ console.log(" - GETTING_STARTED.md");
250
+ }
251
+ if (fs.existsSync(path.join(targetDir, "package.json"))) {
252
+ console.log(" - package.json");
253
+ }
254
+
255
+ console.log("\n🚀 Next steps:");
256
+ console.log(` 1. cd ${path.basename(targetDir)}`);
257
+ console.log(" 2. npm install");
258
+ console.log(" 3. Read GETTING_STARTED.md for a complete guide");
259
+ console.log(" 4. Edit manifest.json to configure your plugin");
260
+ console.log(" 5. Start coding in main.js");
261
+ console.log("\n📖 Learn more: https://docs.lumiastream.com");
262
+ } catch (error) {
263
+ console.error(`\n✖ Failed to create plugin: ${error.message}`);
264
+ process.exit(1);
265
+ }
209
266
  }
210
267
 
211
268
  main();
package/scripts/utils.js CHANGED
@@ -1,109 +1,129 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const JSZip = require('jszip');
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const JSZip = require("jszip");
4
4
 
5
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',
6
+ ".git",
7
+ ".DS_Store",
8
+ "Thumbs.db",
9
+ "package-lock.json",
10
+ "yarn.lock",
11
+ ".npmrc",
12
+ ".gitignore",
13
13
  ]);
14
14
 
15
15
  function toPosix(p) {
16
- return p.split(path.sep).join('/');
16
+ return p.split(path.sep).join("/");
17
17
  }
18
18
 
19
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 };
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
23
  }
24
24
 
25
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
- }
26
+ const errors = [];
27
+ const requiredStringFields = [
28
+ "id",
29
+ "name",
30
+ "version",
31
+ "author",
32
+ "description",
33
+ "license",
34
+ "lumiaVersion",
35
+ ];
36
+ for (const field of requiredStringFields) {
37
+ const value = manifest[field];
38
+ if (typeof value !== "string" || value.trim().length === 0) {
39
+ errors.push(`Missing or invalid manifest field: ${field}`);
40
+ }
41
+ }
34
42
 
35
- if (!manifest.category || (typeof manifest.category !== 'string' && !Array.isArray(manifest.category))) {
36
- errors.push('Manifest must declare a category string');
37
- }
43
+ if (
44
+ !manifest.category ||
45
+ (typeof manifest.category !== "string" && !Array.isArray(manifest.category))
46
+ ) {
47
+ errors.push("Manifest must declare a category string");
48
+ }
38
49
 
39
- if (!manifest.config || typeof manifest.config !== 'object') {
40
- errors.push('Manifest config must be an object');
41
- }
50
+ if (!manifest.config || typeof manifest.config !== "object") {
51
+ errors.push("Manifest config must be an object");
52
+ }
42
53
 
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
- }
54
+ if (manifest.config) {
55
+ if (manifest.config.settings && !Array.isArray(manifest.config.settings)) {
56
+ errors.push("config.settings must be an array when provided");
57
+ }
58
+ if (manifest.config.actions && !Array.isArray(manifest.config.actions)) {
59
+ errors.push("config.actions must be an array when provided");
60
+ }
61
+ if (
62
+ manifest.config.variables &&
63
+ !Array.isArray(manifest.config.variables)
64
+ ) {
65
+ errors.push("config.variables must be an array when provided");
66
+ }
67
+ if (manifest.config.alerts && !Array.isArray(manifest.config.alerts)) {
68
+ errors.push("config.alerts must be an array when provided");
69
+ }
70
+ }
57
71
 
58
- return errors;
72
+ return errors;
59
73
  }
60
74
 
61
75
  async function collectFiles(pluginDir, ignore = DEFAULT_IGNORE) {
62
- const entries = [];
76
+ const entries = [];
63
77
 
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;
78
+ async function walk(currentDir) {
79
+ const list = await fs.promises.readdir(currentDir, { withFileTypes: true });
80
+ for (const entry of list) {
81
+ if (ignore.has(entry.name)) continue;
82
+ const absolute = path.join(currentDir, entry.name);
83
+ const relative = path.relative(pluginDir, absolute);
84
+ if (!relative || relative.startsWith("..")) continue;
71
85
 
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
- }
86
+ // Skip bundled Lumia packages in node_modules
87
+ if (
88
+ relative === "node_modules/@lumiastream" ||
89
+ relative.startsWith("node_modules/@lumiastream/plugin") ||
90
+ relative.startsWith("node_modules/lumia-plugin")
91
+ ) {
92
+ continue;
93
+ }
78
94
 
79
- if (entry.isDirectory()) {
80
- await walk(absolute);
81
- } else if (entry.isFile()) {
82
- entries.push({ absolute, relative: toPosix(relative) });
83
- }
84
- }
85
- }
95
+ if (entry.isDirectory()) {
96
+ await walk(absolute);
97
+ } else if (entry.isFile()) {
98
+ entries.push({ absolute, relative: toPosix(relative) });
99
+ }
100
+ }
101
+ }
86
102
 
87
- await walk(pluginDir);
88
- return entries;
103
+ await walk(pluginDir);
104
+ return entries;
89
105
  }
90
106
 
91
107
  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;
108
+ const zip = new JSZip();
109
+ for (const file of files) {
110
+ const data = await fs.promises.readFile(file.absolute);
111
+ zip.file(file.relative, data);
112
+ }
113
+ const content = await zip.generateAsync({
114
+ type: "nodebuffer",
115
+ compression: "DEFLATE",
116
+ compressionOptions: { level: 9 },
117
+ });
118
+ await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
119
+ await fs.promises.writeFile(outputFile, content);
120
+ return outputFile;
101
121
  }
102
122
 
103
123
  module.exports = {
104
- readManifest,
105
- validateManifest,
106
- collectFiles,
107
- createPluginArchive,
108
- DEFAULT_IGNORE,
124
+ readManifest,
125
+ validateManifest,
126
+ collectFiles,
127
+ createPluginArchive,
128
+ DEFAULT_IGNORE,
109
129
  };