mycelia-kernel-plugin 1.0.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/LICENSE +22 -0
- package/README.md +248 -0
- package/bin/cli.js +433 -0
- package/package.json +63 -0
- package/src/builder/context-resolver.js +62 -0
- package/src/builder/dependency-graph-cache.js +105 -0
- package/src/builder/dependency-graph.js +141 -0
- package/src/builder/facet-validator.js +43 -0
- package/src/builder/hook-processor.js +271 -0
- package/src/builder/index.js +13 -0
- package/src/builder/subsystem-builder.js +104 -0
- package/src/builder/utils.js +165 -0
- package/src/contract/contracts/hierarchy.contract.js +60 -0
- package/src/contract/contracts/index.js +17 -0
- package/src/contract/contracts/listeners.contract.js +66 -0
- package/src/contract/contracts/processor.contract.js +47 -0
- package/src/contract/contracts/queue.contract.js +58 -0
- package/src/contract/contracts/router.contract.js +53 -0
- package/src/contract/contracts/scheduler.contract.js +65 -0
- package/src/contract/contracts/server.contract.js +88 -0
- package/src/contract/contracts/speak.contract.js +50 -0
- package/src/contract/contracts/storage.contract.js +107 -0
- package/src/contract/contracts/websocket.contract.js +90 -0
- package/src/contract/facet-contract-registry.js +155 -0
- package/src/contract/facet-contract.js +136 -0
- package/src/contract/index.js +63 -0
- package/src/core/create-hook.js +63 -0
- package/src/core/facet.js +189 -0
- package/src/core/index.js +3 -0
- package/src/hooks/listeners/handler-group-manager.js +88 -0
- package/src/hooks/listeners/listener-manager-policies.js +229 -0
- package/src/hooks/listeners/listener-manager.js +668 -0
- package/src/hooks/listeners/listener-registry.js +176 -0
- package/src/hooks/listeners/listener-statistics.js +106 -0
- package/src/hooks/listeners/pattern-matcher.js +283 -0
- package/src/hooks/listeners/use-listeners.js +164 -0
- package/src/hooks/queue/bounded-queue.js +341 -0
- package/src/hooks/queue/circular-buffer.js +231 -0
- package/src/hooks/queue/subsystem-queue-manager.js +198 -0
- package/src/hooks/queue/use-queue.js +96 -0
- package/src/hooks/speak/use-speak.js +79 -0
- package/src/index.js +49 -0
- package/src/manager/facet-manager-transaction.js +45 -0
- package/src/manager/facet-manager.js +570 -0
- package/src/manager/index.js +3 -0
- package/src/system/base-subsystem.js +416 -0
- package/src/system/base-subsystem.utils.js +106 -0
- package/src/system/index.js +4 -0
- package/src/system/standalone-plugin-system.js +70 -0
- package/src/utils/debug-flag.js +34 -0
- package/src/utils/find-facet.js +30 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/semver.js +221 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mycelia Kernel Contributors
|
|
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.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Mycelia Plugin System
|
|
2
|
+
|
|
3
|
+
A sophisticated, dependency-aware plugin system with transaction safety and lifecycle management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Mycelia Plugin System is a standalone plugin architecture extracted from [Mycelia Kernel](https://github.com/lesfleursdelanuitdev/mycelia-kernel). It provides:
|
|
8
|
+
|
|
9
|
+
- **Hook-based composition** - Extend systems without modification
|
|
10
|
+
- **Dependency resolution** - Automatic topological sorting
|
|
11
|
+
- **Transaction safety** - Atomic installation with rollback
|
|
12
|
+
- **Lifecycle management** - Built-in initialization and disposal
|
|
13
|
+
- **Hot reloading** - Reload and extend plugins without full teardown
|
|
14
|
+
- **Facet contracts** - Runtime validation of plugin interfaces
|
|
15
|
+
- **Standalone mode** - Works without message system or other dependencies
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { StandalonePluginSystem, createHook, Facet } from 'mycelia-kernel-plugin';
|
|
21
|
+
|
|
22
|
+
// Create a hook
|
|
23
|
+
const useDatabase = createHook({
|
|
24
|
+
kind: 'database',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
attach: true,
|
|
27
|
+
source: import.meta.url,
|
|
28
|
+
fn: (ctx, api, subsystem) => {
|
|
29
|
+
const config = ctx.config?.database || {};
|
|
30
|
+
|
|
31
|
+
return new Facet('database', {
|
|
32
|
+
attach: true,
|
|
33
|
+
source: import.meta.url
|
|
34
|
+
})
|
|
35
|
+
.add({
|
|
36
|
+
async query(sql) {
|
|
37
|
+
// Database query implementation
|
|
38
|
+
return { rows: [] };
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async close() {
|
|
42
|
+
// Cleanup
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
.onInit(async ({ ctx }) => {
|
|
46
|
+
// Initialize database connection
|
|
47
|
+
})
|
|
48
|
+
.onDispose(async () => {
|
|
49
|
+
// Close database connection
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Create and use the system
|
|
55
|
+
const system = new StandalonePluginSystem('my-app', {
|
|
56
|
+
config: {
|
|
57
|
+
database: { host: 'localhost' }
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
system
|
|
62
|
+
.use(useDatabase)
|
|
63
|
+
.build();
|
|
64
|
+
|
|
65
|
+
// Use the plugin
|
|
66
|
+
const db = system.find('database');
|
|
67
|
+
await db.query('SELECT * FROM users');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install mycelia-kernel-plugin
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
### Hook System
|
|
79
|
+
Create composable plugins using the `createHook` factory:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const useCache = createHook({
|
|
83
|
+
kind: 'cache',
|
|
84
|
+
required: ['database'], // Dependencies
|
|
85
|
+
attach: true,
|
|
86
|
+
source: import.meta.url,
|
|
87
|
+
fn: (ctx, api, subsystem) => {
|
|
88
|
+
const db = subsystem.find('database');
|
|
89
|
+
|
|
90
|
+
return new Facet('cache', { attach: true })
|
|
91
|
+
.add({
|
|
92
|
+
async get(key) {
|
|
93
|
+
// Cache implementation
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Dependency Resolution
|
|
101
|
+
Dependencies are automatically resolved and initialized in the correct order:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
system
|
|
105
|
+
.use(useDatabase) // Will be initialized first
|
|
106
|
+
.use(useCache) // Will be initialized after database
|
|
107
|
+
.build();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Transaction Safety
|
|
111
|
+
If any plugin fails during initialization, all changes are rolled back:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
try {
|
|
115
|
+
await system
|
|
116
|
+
.use(useDatabase)
|
|
117
|
+
.use(useCache)
|
|
118
|
+
.build();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// System is in clean state - all plugins rolled back
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hot Reloading
|
|
125
|
+
Reload the system and add more plugins without full teardown:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// Initial build
|
|
129
|
+
await system.use(useDatabase).build();
|
|
130
|
+
|
|
131
|
+
// Hot reload - add more plugins
|
|
132
|
+
await system.reload();
|
|
133
|
+
await system.use(useCache).use(useAuth).build();
|
|
134
|
+
|
|
135
|
+
// All plugins (old + new) are now active
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `reload()` method:
|
|
139
|
+
- Disposes all facets and resets built state
|
|
140
|
+
- Preserves hooks and configuration
|
|
141
|
+
- Allows adding more hooks and rebuilding
|
|
142
|
+
- Perfect for development and hot-reload scenarios
|
|
143
|
+
|
|
144
|
+
### Facet Contracts
|
|
145
|
+
Validate plugin interfaces at build time:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
import { createFacetContract } from 'mycelia-kernel-plugin';
|
|
149
|
+
|
|
150
|
+
const databaseContract = createFacetContract({
|
|
151
|
+
name: 'database',
|
|
152
|
+
requiredMethods: ['query', 'close'],
|
|
153
|
+
requiredProperties: ['connection']
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Contract is automatically enforced during build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Architecture
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
StandalonePluginSystem
|
|
163
|
+
├── BaseSubsystem (base class)
|
|
164
|
+
├── SubsystemBuilder (build orchestrator)
|
|
165
|
+
├── FacetManager (plugin registry)
|
|
166
|
+
├── FacetContractRegistry (contract validation)
|
|
167
|
+
└── DependencyGraphCache (performance optimization)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## API Reference
|
|
171
|
+
|
|
172
|
+
### Core Classes
|
|
173
|
+
|
|
174
|
+
- **`StandalonePluginSystem`** - Main plugin system class
|
|
175
|
+
- **`BaseSubsystem`** - Base class for plugin containers
|
|
176
|
+
- **`SubsystemBuilder`** - Build orchestrator
|
|
177
|
+
- **`FacetManager`** - Plugin registry
|
|
178
|
+
- **`FacetContractRegistry`** - Contract validation
|
|
179
|
+
|
|
180
|
+
### Factory Functions
|
|
181
|
+
|
|
182
|
+
- **`createHook()`** - Create a plugin hook
|
|
183
|
+
- **`createFacetContract()`** - Create a facet contract
|
|
184
|
+
|
|
185
|
+
### Utilities
|
|
186
|
+
|
|
187
|
+
- **`createLogger()`** - Create a logger
|
|
188
|
+
- **`getDebugFlag()`** - Extract debug flag from config
|
|
189
|
+
|
|
190
|
+
## Documentation
|
|
191
|
+
|
|
192
|
+
Comprehensive documentation is available in the [`docs/`](./docs/) directory:
|
|
193
|
+
|
|
194
|
+
- **[Getting Started Guide](./docs/getting-started/README.md)** - Quick start with examples
|
|
195
|
+
- **[Hooks and Facets Overview](./docs/core-concepts/HOOKS-AND-FACETS-OVERVIEW.md)** - Core concepts
|
|
196
|
+
- **[Standalone Plugin System](./docs/standalone/STANDALONE-PLUGIN-SYSTEM.md)** - Complete usage guide
|
|
197
|
+
- **[Documentation Index](./docs/README.md)** - Full documentation index
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
See the `examples/` directory for:
|
|
202
|
+
- Basic plugin usage
|
|
203
|
+
- Plugins with dependencies
|
|
204
|
+
- Lifecycle management
|
|
205
|
+
- Contract validation
|
|
206
|
+
- Hot reloading
|
|
207
|
+
|
|
208
|
+
## CLI Tool
|
|
209
|
+
|
|
210
|
+
The package includes a CLI tool for scaffolding hooks, contracts, and projects:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Create a new hook
|
|
214
|
+
npx mycelia-kernel-plugin create hook database
|
|
215
|
+
|
|
216
|
+
# Create a new contract
|
|
217
|
+
npx mycelia-kernel-plugin create contract database
|
|
218
|
+
|
|
219
|
+
# Initialize a new project
|
|
220
|
+
npx mycelia-kernel-plugin init my-app
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Or install globally:
|
|
224
|
+
```bash
|
|
225
|
+
npm install -g mycelia-kernel-plugin
|
|
226
|
+
mycelia-kernel-plugin create hook database
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Testing
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm test
|
|
233
|
+
npm run test:watch
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT License - see [LICENSE](./LICENSE) for details.
|
|
239
|
+
|
|
240
|
+
## Links
|
|
241
|
+
|
|
242
|
+
- **GitHub:** https://github.com/lesfleursdelanuitdev/mycelia-kernel-plugin-system
|
|
243
|
+
- **Main Project:** https://github.com/lesfleursdelanuitdev/mycelia-kernel
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
Made with ❤️ by [@lesfleursdelanuitdev](https://github.com/lesfleursdelanuitdev)
|
|
248
|
+
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mycelia Plugin System CLI
|
|
5
|
+
*
|
|
6
|
+
* Command-line tool for scaffolding hooks, contracts, and project structure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Get command and arguments
|
|
18
|
+
const [,, command, ...args] = process.argv;
|
|
19
|
+
|
|
20
|
+
// Commands
|
|
21
|
+
const commands = {
|
|
22
|
+
'create': handleCreate,
|
|
23
|
+
'init': handleInit,
|
|
24
|
+
'help': showHelp,
|
|
25
|
+
'--help': showHelp,
|
|
26
|
+
'-h': showHelp
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Main entry point
|
|
30
|
+
if (!command || !commands[command]) {
|
|
31
|
+
showHelp();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
commands[command](args);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle 'create' command
|
|
39
|
+
*/
|
|
40
|
+
function handleCreate(args) {
|
|
41
|
+
if (args.length < 2) {
|
|
42
|
+
console.error('Error: Missing arguments');
|
|
43
|
+
console.error('Usage: mycelia-kernel-plugin create <type> <name>');
|
|
44
|
+
console.error('Types: hook, contract');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [type, name] = args;
|
|
49
|
+
|
|
50
|
+
if (type === 'hook') {
|
|
51
|
+
createHook(name);
|
|
52
|
+
} else if (type === 'contract') {
|
|
53
|
+
createContract(name);
|
|
54
|
+
} else {
|
|
55
|
+
console.error(`Error: Unknown type "${type}"`);
|
|
56
|
+
console.error('Types: hook, contract');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle 'init' command
|
|
63
|
+
*/
|
|
64
|
+
function handleInit(args) {
|
|
65
|
+
const projectName = args[0] || 'my-plugin-system';
|
|
66
|
+
initProject(projectName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a hook file
|
|
71
|
+
*/
|
|
72
|
+
function createHook(name) {
|
|
73
|
+
if (!name) {
|
|
74
|
+
console.error('Error: Hook name is required');
|
|
75
|
+
console.error('Usage: mycelia-kernel-plugin create hook <name>');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate name (kebab-case or camelCase)
|
|
80
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9-]/g, '');
|
|
81
|
+
const hookName = toCamelCase(sanitizedName, true); // useXxx
|
|
82
|
+
const kindName = toKebabCase(sanitizedName); // xxx
|
|
83
|
+
const className = toPascalCase(sanitizedName); // Xxx
|
|
84
|
+
|
|
85
|
+
// Determine output directory
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
let outputDir;
|
|
88
|
+
|
|
89
|
+
if (existsSync(join(cwd, 'src/hooks'))) {
|
|
90
|
+
outputDir = join(cwd, 'src/hooks');
|
|
91
|
+
} else if (existsSync(join(cwd, 'hooks'))) {
|
|
92
|
+
outputDir = join(cwd, 'hooks');
|
|
93
|
+
} else if (existsSync(join(cwd, 'src'))) {
|
|
94
|
+
outputDir = join(cwd, 'src');
|
|
95
|
+
mkdirSync(join(outputDir, 'hooks'), { recursive: true });
|
|
96
|
+
outputDir = join(outputDir, 'hooks');
|
|
97
|
+
} else {
|
|
98
|
+
mkdirSync(join(cwd, 'src', 'hooks'), { recursive: true });
|
|
99
|
+
outputDir = join(cwd, 'src', 'hooks');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!existsSync(outputDir)) {
|
|
103
|
+
mkdirSync(outputDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fileName = `use-${kindName}.js`;
|
|
107
|
+
const filePath = join(outputDir, fileName);
|
|
108
|
+
|
|
109
|
+
if (existsSync(filePath)) {
|
|
110
|
+
console.error(`Error: File already exists: ${filePath}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const template = `import { createHook, Facet } from 'mycelia-kernel-plugin';
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ${className} Hook
|
|
118
|
+
*
|
|
119
|
+
* ${sanitizedName} plugin hook.
|
|
120
|
+
*/
|
|
121
|
+
export const ${hookName} = createHook({
|
|
122
|
+
kind: '${kindName}',
|
|
123
|
+
version: '1.0.0',
|
|
124
|
+
attach: true,
|
|
125
|
+
source: import.meta.url,
|
|
126
|
+
fn: (ctx, api, subsystem) => {
|
|
127
|
+
const config = ctx.config?.${kindName} || {};
|
|
128
|
+
|
|
129
|
+
return new Facet('${kindName}', {
|
|
130
|
+
attach: true,
|
|
131
|
+
source: import.meta.url,
|
|
132
|
+
version: '1.0.0'
|
|
133
|
+
})
|
|
134
|
+
.add({
|
|
135
|
+
// Add your methods here
|
|
136
|
+
})
|
|
137
|
+
.onInit(async ({ ctx }) => {
|
|
138
|
+
// Initialization logic
|
|
139
|
+
if (ctx.debug) {
|
|
140
|
+
console.log('${className} plugin initialized');
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.onDispose(async () => {
|
|
144
|
+
// Cleanup logic
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
writeFileSync(filePath, template, 'utf8');
|
|
151
|
+
console.log(`✅ Created hook: ${filePath}`);
|
|
152
|
+
console.log(` Hook name: ${hookName}`);
|
|
153
|
+
console.log(` Facet kind: ${kindName}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a contract file
|
|
158
|
+
*/
|
|
159
|
+
function createContract(name) {
|
|
160
|
+
if (!name) {
|
|
161
|
+
console.error('Error: Contract name is required');
|
|
162
|
+
console.error('Usage: mycelia-kernel-plugin create contract <name>');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Validate name
|
|
167
|
+
const sanitizedName = name.replace(/[^a-zA-Z0-9-]/g, '');
|
|
168
|
+
const contractName = toCamelCase(sanitizedName); // xxxContract
|
|
169
|
+
const contractType = toKebabCase(sanitizedName); // xxx
|
|
170
|
+
|
|
171
|
+
// Determine output directory
|
|
172
|
+
const cwd = process.cwd();
|
|
173
|
+
let outputDir;
|
|
174
|
+
|
|
175
|
+
if (existsSync(join(cwd, 'src/contracts'))) {
|
|
176
|
+
outputDir = join(cwd, 'src/contracts');
|
|
177
|
+
} else if (existsSync(join(cwd, 'contracts'))) {
|
|
178
|
+
outputDir = join(cwd, 'contracts');
|
|
179
|
+
} else if (existsSync(join(cwd, 'src'))) {
|
|
180
|
+
outputDir = join(cwd, 'src');
|
|
181
|
+
mkdirSync(join(outputDir, 'contracts'), { recursive: true });
|
|
182
|
+
outputDir = join(outputDir, 'contracts');
|
|
183
|
+
} else {
|
|
184
|
+
mkdirSync(join(cwd, 'src', 'contracts'), { recursive: true });
|
|
185
|
+
outputDir = join(cwd, 'src', 'contracts');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!existsSync(outputDir)) {
|
|
189
|
+
mkdirSync(outputDir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const fileName = `${contractType}.contract.js`;
|
|
193
|
+
const filePath = join(outputDir, fileName);
|
|
194
|
+
|
|
195
|
+
if (existsSync(filePath)) {
|
|
196
|
+
console.error(`Error: File already exists: ${filePath}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const template = `import { createFacetContract } from 'mycelia-kernel-plugin';
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* ${sanitizedName} Contract
|
|
204
|
+
*
|
|
205
|
+
* Defines the contract for ${sanitizedName} facets.
|
|
206
|
+
*/
|
|
207
|
+
export const ${contractName}Contract = createFacetContract({
|
|
208
|
+
name: '${contractType}',
|
|
209
|
+
requiredMethods: [
|
|
210
|
+
// Add required method names here
|
|
211
|
+
],
|
|
212
|
+
requiredProperties: [
|
|
213
|
+
// Add required property names here
|
|
214
|
+
],
|
|
215
|
+
validate: (ctx, api, subsystem, facet) => {
|
|
216
|
+
// Custom validation logic
|
|
217
|
+
// Throw an error if validation fails
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Register with default registry (optional)
|
|
222
|
+
// import { defaultContractRegistry } from 'mycelia-kernel-plugin';
|
|
223
|
+
// defaultContractRegistry.register(${contractName}Contract);
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
writeFileSync(filePath, template, 'utf8');
|
|
227
|
+
console.log(`✅ Created contract: ${filePath}`);
|
|
228
|
+
console.log(` Contract name: ${contractName}Contract`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Initialize a new project
|
|
233
|
+
*/
|
|
234
|
+
function initProject(projectName) {
|
|
235
|
+
const projectDir = projectName;
|
|
236
|
+
|
|
237
|
+
if (existsSync(projectDir)) {
|
|
238
|
+
console.error(`Error: Directory already exists: ${projectDir}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`Creating project: ${projectName}...`);
|
|
243
|
+
|
|
244
|
+
// Create directory structure
|
|
245
|
+
mkdirSync(projectDir, { recursive: true });
|
|
246
|
+
mkdirSync(join(projectDir, 'src'), { recursive: true });
|
|
247
|
+
mkdirSync(join(projectDir, 'src/hooks'), { recursive: true });
|
|
248
|
+
mkdirSync(join(projectDir, 'src/contracts'), { recursive: true });
|
|
249
|
+
|
|
250
|
+
// Create package.json
|
|
251
|
+
const packageJson = {
|
|
252
|
+
name: projectName,
|
|
253
|
+
version: '1.0.0',
|
|
254
|
+
type: 'module',
|
|
255
|
+
main: 'src/index.js',
|
|
256
|
+
scripts: {
|
|
257
|
+
start: 'node src/index.js',
|
|
258
|
+
test: 'vitest run'
|
|
259
|
+
},
|
|
260
|
+
dependencies: {
|
|
261
|
+
'mycelia-kernel-plugin': '^1.0.0'
|
|
262
|
+
},
|
|
263
|
+
devDependencies: {
|
|
264
|
+
vitest: '^2.1.5'
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
writeFileSync(
|
|
269
|
+
join(projectDir, 'package.json'),
|
|
270
|
+
JSON.stringify(packageJson, null, 2),
|
|
271
|
+
'utf8'
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Create README.md
|
|
275
|
+
const readme = `# ${projectName}
|
|
276
|
+
|
|
277
|
+
A plugin system built with Mycelia Plugin System.
|
|
278
|
+
|
|
279
|
+
## Getting Started
|
|
280
|
+
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
npm install
|
|
283
|
+
npm start
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
## Creating Plugins
|
|
287
|
+
|
|
288
|
+
Use the CLI to scaffold new plugins:
|
|
289
|
+
|
|
290
|
+
\`\`\`bash
|
|
291
|
+
npx mycelia-kernel-plugin create hook my-plugin
|
|
292
|
+
\`\`\`
|
|
293
|
+
|
|
294
|
+
## Structure
|
|
295
|
+
|
|
296
|
+
- \`src/hooks/\` - Plugin hooks
|
|
297
|
+
- \`src/contracts/\` - Facet contracts
|
|
298
|
+
- \`src/index.js\` - Main entry point
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
writeFileSync(join(projectDir, 'README.md'), readme, 'utf8');
|
|
302
|
+
|
|
303
|
+
// Create example hook
|
|
304
|
+
const exampleHook = `import { createHook, Facet } from 'mycelia-kernel-plugin';
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Example Hook
|
|
308
|
+
*/
|
|
309
|
+
export const useExample = createHook({
|
|
310
|
+
kind: 'example',
|
|
311
|
+
version: '1.0.0',
|
|
312
|
+
attach: true,
|
|
313
|
+
source: import.meta.url,
|
|
314
|
+
fn: (ctx, api, subsystem) => {
|
|
315
|
+
return new Facet('example', {
|
|
316
|
+
attach: true,
|
|
317
|
+
source: import.meta.url,
|
|
318
|
+
version: '1.0.0'
|
|
319
|
+
})
|
|
320
|
+
.add({
|
|
321
|
+
greet(name) {
|
|
322
|
+
return \`Hello, \${name}!\`;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
writeFileSync(join(projectDir, 'src/hooks/use-example.js'), exampleHook, 'utf8');
|
|
330
|
+
|
|
331
|
+
// Create main index.js
|
|
332
|
+
const indexJs = `import { StandalonePluginSystem } from 'mycelia-kernel-plugin';
|
|
333
|
+
import { useExample } from './hooks/use-example.js';
|
|
334
|
+
|
|
335
|
+
async function main() {
|
|
336
|
+
const system = new StandalonePluginSystem('${projectName}', {
|
|
337
|
+
config: {},
|
|
338
|
+
debug: true
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await system
|
|
342
|
+
.use(useExample)
|
|
343
|
+
.build();
|
|
344
|
+
|
|
345
|
+
const example = system.find('example');
|
|
346
|
+
console.log(example.greet('World'));
|
|
347
|
+
|
|
348
|
+
await system.dispose();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
main().catch(console.error);
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
writeFileSync(join(projectDir, 'src/index.js'), indexJs, 'utf8');
|
|
355
|
+
|
|
356
|
+
// Create .gitignore
|
|
357
|
+
const gitignore = `node_modules/
|
|
358
|
+
*.log
|
|
359
|
+
.DS_Store
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
writeFileSync(join(projectDir, '.gitignore'), gitignore, 'utf8');
|
|
363
|
+
|
|
364
|
+
console.log(`✅ Project created: ${projectDir}`);
|
|
365
|
+
console.log(`\nNext steps:`);
|
|
366
|
+
console.log(` cd ${projectDir}`);
|
|
367
|
+
console.log(` npm install`);
|
|
368
|
+
console.log(` npm start`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Show help message
|
|
373
|
+
*/
|
|
374
|
+
function showHelp() {
|
|
375
|
+
const help = `
|
|
376
|
+
Mycelia Plugin System CLI
|
|
377
|
+
|
|
378
|
+
Usage:
|
|
379
|
+
mycelia-kernel-plugin <command> [options]
|
|
380
|
+
|
|
381
|
+
Commands:
|
|
382
|
+
create hook <name> Create a new hook file
|
|
383
|
+
create contract <name> Create a new contract file
|
|
384
|
+
init [name] Initialize a new project
|
|
385
|
+
help Show this help message
|
|
386
|
+
|
|
387
|
+
Examples:
|
|
388
|
+
mycelia-kernel-plugin create hook database
|
|
389
|
+
mycelia-kernel-plugin create contract database
|
|
390
|
+
mycelia-kernel-plugin init my-app
|
|
391
|
+
|
|
392
|
+
For more information, visit:
|
|
393
|
+
https://github.com/lesfleursdelanuitdev/mycelia-kernel-plugin-system
|
|
394
|
+
`;
|
|
395
|
+
console.log(help);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Convert string to camelCase (with use prefix for hooks)
|
|
400
|
+
*/
|
|
401
|
+
function toCamelCase(str, usePrefix = false) {
|
|
402
|
+
const parts = str.split('-').filter(Boolean);
|
|
403
|
+
const camel = parts.map((part, i) => {
|
|
404
|
+
if (i === 0 && !usePrefix) {
|
|
405
|
+
return part.toLowerCase();
|
|
406
|
+
}
|
|
407
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
408
|
+
}).join('');
|
|
409
|
+
return usePrefix ? `use${camel}` : camel;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Convert string to kebab-case
|
|
414
|
+
*/
|
|
415
|
+
function toKebabCase(str) {
|
|
416
|
+
return str
|
|
417
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
418
|
+
.toLowerCase()
|
|
419
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
420
|
+
.replace(/-+/g, '-')
|
|
421
|
+
.replace(/^-|-$/g, '');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Convert string to PascalCase
|
|
426
|
+
*/
|
|
427
|
+
function toPascalCase(str) {
|
|
428
|
+
const parts = str.split('-').filter(Boolean);
|
|
429
|
+
return parts.map(part =>
|
|
430
|
+
part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
431
|
+
).join('');
|
|
432
|
+
}
|
|
433
|
+
|