@wealthfolio/addon-dev-tools 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/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @wealthfolio/addon-dev-tools
2
+
3
+ Development tools for Wealthfolio addons including hot reload server and CLI.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @wealthfolio/addon-dev-tools
9
+ ```
10
+
11
+ ## CLI Commands
12
+
13
+ ### Create New Addon
14
+ ```bash
15
+ wealthfolio create my-awesome-addon
16
+ ```
17
+
18
+ ### Start Development Server
19
+ ```bash
20
+ # In your addon directory
21
+ wealthfolio dev
22
+ ```
23
+
24
+ ### Build Addon
25
+ ```bash
26
+ wealthfolio build
27
+ ```
28
+
29
+ ### Package for Distribution
30
+ ```bash
31
+ wealthfolio package
32
+ ```
33
+
34
+ ### Test Setup
35
+ ```bash
36
+ wealthfolio test
37
+ ```
38
+
39
+ ## Development Server
40
+
41
+ The development server provides:
42
+ - Hot reload functionality
43
+ - File watching
44
+ - Auto-building
45
+ - Health check endpoints
46
+
47
+ ### API Endpoints
48
+
49
+ - `GET /health` - Health check
50
+ - `GET /status` - Addon status and last modified time
51
+ - `GET /manifest.json` - Addon manifest
52
+ - `GET /addon.js` - Built addon code
53
+ - `GET /files` - List of built files
54
+ - `GET /test` - Test connectivity
55
+
56
+ ## Usage in Addon Projects
57
+
58
+ Add to your addon's `package.json`:
59
+
60
+ ```json
61
+ {
62
+ "scripts": {
63
+ "dev:server": "wealthfolio dev"
64
+ },
65
+ "devDependencies": {
66
+ "@wealthfolio/addon-dev-tools": "^1.0.0"
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Architecture
72
+
73
+ This package is separate from `@wealthfolio/addon-sdk` to:
74
+ - Keep the SDK lightweight for production
75
+ - Avoid unnecessary dependencies in addon bundles
76
+ - Provide optional development tooling
77
+
78
+ ## License
79
+
80
+ MIT
package/cli.js ADDED
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wealthfolio Addon CLI
5
+ *
6
+ * Command-line tool for developing, building, and managing addons
7
+ */
8
+
9
+ const { program } = require('commander');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { exec, spawn } = require('child_process');
13
+ const { promisify } = require('util');
14
+ const readline = require('node:readline/promises');
15
+ const { stdin, stdout } = require('node:process');
16
+ const { AddonScaffold } = require('./scaffold');
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ // Colors for console output
21
+ const colors = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[34m',
28
+ magenta: '\x1b[35m',
29
+ cyan: '\x1b[36m'
30
+ };
31
+
32
+ function log(message, color = colors.reset) {
33
+ console.log(`${color}${message}${colors.reset}`);
34
+ }
35
+
36
+ function error(message) {
37
+ log(`❌ ${message}`, colors.red);
38
+ }
39
+
40
+ function success(message) {
41
+ log(`✅ ${message}`, colors.green);
42
+ }
43
+
44
+ function info(message) {
45
+ log(`ℹ️ ${message}`, colors.blue);
46
+ }
47
+
48
+ function warn(message) {
49
+ log(`⚠️ ${message}`, colors.yellow);
50
+ }
51
+
52
+ // Initialize scaffold service
53
+ const scaffold = new AddonScaffold();
54
+
55
+ // Command: create
56
+ async function createAddon(name, options) {
57
+ try {
58
+ info(`Creating new addon: ${name}`);
59
+
60
+ // Prepare configuration
61
+ const config = {
62
+ name,
63
+ description: options.description,
64
+ author: options.author
65
+ };
66
+
67
+ // Validate configuration
68
+ const validationErrors = scaffold.validateConfig(config);
69
+ if (validationErrors.length > 0) {
70
+ error('Configuration errors:');
71
+ validationErrors.forEach(err => error(` - ${err}`));
72
+ return;
73
+ }
74
+
75
+ // Interactive prompts for missing information
76
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
77
+ if (interactive && (!config.description || !config.author)) {
78
+ const rl = readline.createInterface({ input: stdin, output: stdout });
79
+ try {
80
+ if (!config.description) {
81
+ const defaultDesc = `A Wealthfolio addon for ${name}`;
82
+ const answer = (await rl.question(`Description [${defaultDesc}]: `)).trim();
83
+ config.description = answer.length > 0 ? answer : defaultDesc;
84
+ }
85
+ if (!config.author) {
86
+ const defaultAuthor = 'Anonymous';
87
+ const answer = (await rl.question(`Author [${defaultAuthor}]: `)).trim();
88
+ config.author = answer.length > 0 ? answer : defaultAuthor;
89
+ }
90
+ } finally {
91
+ rl.close();
92
+ }
93
+ }
94
+
95
+ // Set defaults for non-interactive mode
96
+ if (!config.description) {
97
+ config.description = `A Wealthfolio addon for ${name}`;
98
+ }
99
+ if (!config.author) {
100
+ config.author = 'Anonymous';
101
+ }
102
+
103
+ const addonId = name.toLowerCase().replace(/[^a-z0-9]/g, '-');
104
+ const addonDir = path.resolve(process.cwd(), addonId);
105
+
106
+ // Check if directory already exists
107
+ if (fs.existsSync(addonDir)) {
108
+ error(`Directory ${addonId} already exists`);
109
+ return;
110
+ }
111
+
112
+ // Add current date for changelog
113
+ const currentDate = new Date().toISOString().split('T')[0];
114
+ config.currentDate = currentDate;
115
+
116
+ // Create addon using scaffold service
117
+ const result = await scaffold.createAddon(config, addonDir);
118
+
119
+ success(`Addon ${name} created successfully!`);
120
+ info(`Directory: ${result.addonDir}`);
121
+ info(`Addon ID: ${result.addonId}`);
122
+ info(`Package name: ${result.packageName}`);
123
+ info(`Structure created:`);
124
+ info(` ├── src/`);
125
+ info(` │ ├── addon.tsx # Main addon entry point`);
126
+ info(` │ ├── components/ # React components`);
127
+ info(` │ ├── hooks/ # React hooks`);
128
+ info(` │ ├── pages/ # Addon pages`);
129
+ info(` │ ├── lib/ # Utility functions and shared logic`);
130
+ info(` │ └── types/ # Type definitions`);
131
+ info(` ├── dist/ # Built files (generated)`);
132
+ info(` ├── manifest.json # Addon metadata and permissions`);
133
+ info(` ├── package.json # NPM package configuration`);
134
+ info(` ├── vite.config.ts # Build configuration`);
135
+ info(` ├── tsconfig.json # TypeScript configuration`);
136
+ info(` ├── CHANGELOG.md # Version history and release notes`);
137
+ info(` └── README.md # Documentation`);
138
+ info(`Next steps:`);
139
+ info(` 1. cd ${addonId}`);
140
+ info(` 2. pnpm install`);
141
+ info(` 3. pnpm run dev:server`);
142
+
143
+ } catch (err) {
144
+ error(`Failed to create addon: ${err.message}`);
145
+ }
146
+ }
147
+
148
+ // Command: dev
149
+ async function startDev(port = 3001) {
150
+ try {
151
+ const manifestPath = path.resolve(process.cwd(), 'manifest.json');
152
+
153
+ if (!fs.existsSync(manifestPath)) {
154
+ error('No manifest.json found. Are you in an addon directory?');
155
+ return;
156
+ }
157
+
158
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
159
+
160
+ info(`Starting development server for ${manifest.name}`);
161
+ info(`Server will run on http://localhost:${port}`);
162
+
163
+ // Start the development server
164
+ const devServerPath = path.resolve(__dirname, 'dev-server.js');
165
+ const child = spawn('node', [devServerPath, process.cwd(), port.toString()], {
166
+ stdio: 'inherit'
167
+ });
168
+
169
+ // Handle cleanup on exit
170
+ process.on('SIGINT', () => {
171
+ child.kill('SIGINT');
172
+ process.exit(0);
173
+ });
174
+
175
+ child.on('exit', (code) => {
176
+ process.exit(code);
177
+ });
178
+
179
+ } catch (err) {
180
+ error(`Failed to start development server: ${err.message}`);
181
+ }
182
+ }
183
+
184
+ // Command: build
185
+ async function buildAddon() {
186
+ try {
187
+ info('Building addon...');
188
+
189
+ const packageJsonPath = path.resolve(process.cwd(), 'package.json');
190
+ if (!fs.existsSync(packageJsonPath)) {
191
+ error('No package.json found. Are you in an addon directory?');
192
+ return;
193
+ }
194
+
195
+ await execAsync('pnpm run build');
196
+ success('Addon built successfully!');
197
+
198
+ } catch (err) {
199
+ error(`Build failed: ${err.message}`);
200
+ }
201
+ }
202
+
203
+ // Command: package
204
+ async function packageAddon() {
205
+ try {
206
+ info('Packaging addon...');
207
+
208
+ // Build first
209
+ await buildAddon();
210
+
211
+ // Create package
212
+ await execAsync('pnpm run package');
213
+ success('Addon packaged successfully!');
214
+
215
+ } catch (err) {
216
+ error(`Packaging failed: ${err.message}`);
217
+ }
218
+ }
219
+
220
+ // Command: test
221
+ async function testSetup() {
222
+ try {
223
+ info('Testing addon development setup...');
224
+
225
+ // Check if manifest exists
226
+ const manifestPath = path.resolve(process.cwd(), 'manifest.json');
227
+ if (!fs.existsSync(manifestPath)) {
228
+ error('❌ No manifest.json found. Are you in an addon directory?');
229
+ return;
230
+ }
231
+
232
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
233
+ success(`✅ Found manifest for: ${manifest.name}`);
234
+
235
+ // Check if dist exists
236
+ const distPath = path.resolve(process.cwd(), 'dist');
237
+ if (!fs.existsSync(distPath)) {
238
+ warn('⚠️ No dist directory found. Run `pnpm run build` first.');
239
+ } else {
240
+ success('✅ Dist directory exists');
241
+ }
242
+
243
+ // Check if dev server is running
244
+ try {
245
+ const response = await fetch('http://localhost:3001/test');
246
+ if (response.ok) {
247
+ const data = await response.json();
248
+ success('✅ Development server is running');
249
+ info(` Server message: ${data.message}`);
250
+ }
251
+ } catch (error) {
252
+ warn('⚠️ Development server not running on port 3001');
253
+ info(' Start it with: pnpm run dev:server');
254
+ }
255
+
256
+ info('\nNext steps:');
257
+ info('1. Start dev server: pnpm run dev:server');
258
+ info('2. Start main app in dev mode');
259
+ info('3. Check console: discoverAddons()');
260
+
261
+ } catch (err) {
262
+ error(`Test failed: ${err.message}`);
263
+ }
264
+ }
265
+
266
+ // Command: install
267
+ async function installAddon(zipPath) {
268
+ try {
269
+ info(`Installing addon from ${zipPath}`);
270
+
271
+ // This would integrate with the main app's addon installation
272
+ warn('Install command not yet implemented. Use the main app to install.');
273
+
274
+ } catch (err) {
275
+ error(`Installation failed: ${err.message}`);
276
+ }
277
+ }
278
+
279
+ // CLI Setup
280
+ program
281
+ .name('wealthfolio')
282
+ .description('Wealthfolio Addon Development CLI')
283
+ .version('1.0.0');
284
+
285
+ program
286
+ .command('create <name>')
287
+ .description('Create a new addon')
288
+ .option('-d, --description <desc>', 'Addon description')
289
+ .option('-a, --author <author>', 'Addon author')
290
+ .action(createAddon);
291
+
292
+ program
293
+ .command('dev')
294
+ .description('Start development server')
295
+ .option('-p, --port <port>', 'Port number', '3001')
296
+ .action((options) => startDev(parseInt(options.port)));
297
+
298
+ program
299
+ .command('build')
300
+ .description('Build the addon')
301
+ .action(buildAddon);
302
+
303
+ program
304
+ .command('package')
305
+ .description('Package the addon for distribution')
306
+ .action(packageAddon);
307
+
308
+ program
309
+ .command('test')
310
+ .description('Test addon development setup')
311
+ .action(testSetup);
312
+
313
+ program
314
+ .command('install <zip>')
315
+ .description('Install an addon from zip file')
316
+ .action(installAddon);
317
+
318
+ program.parse();
package/dev-server.js ADDED
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+
4
+ /**
5
+ * Addon Development Server
6
+ *
7
+ * A simple development server for hot reloading addons during development.
8
+ * This server watches for file changes and provides a hot reload endpoint.
9
+ */
10
+
11
+ const express = require('express');
12
+ const cors = require('cors');
13
+ const chokidar = require('chokidar');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const { exec } = require('child_process');
17
+ const { promisify } = require('util');
18
+
19
+ const execAsync = promisify(exec);
20
+
21
+ class AddonDevServer {
22
+ constructor(config) {
23
+ this.config = config;
24
+ this.app = express();
25
+ this.lastModified = new Date();
26
+ this.buildInProgress = false;
27
+ this.viteWatcher = null;
28
+
29
+ this.setupMiddleware();
30
+ this.setupRoutes();
31
+ this.setupFileWatcher();
32
+ this.startViteWatcher();
33
+ }
34
+
35
+ setupMiddleware() {
36
+ this.app.use(cors({
37
+ origin: ['http://localhost:1420', 'http://localhost:3000'],
38
+ credentials: true
39
+ }));
40
+ this.app.use(express.static(this.config.addonPath));
41
+ }
42
+
43
+ setupRoutes() {
44
+ // Health check endpoint
45
+ this.app.get('/health', (req, res) => {
46
+ res.json({
47
+ status: 'ok',
48
+ timestamp: new Date().toISOString(),
49
+ addonPath: this.config.addonPath
50
+ });
51
+ });
52
+
53
+ // Addon status endpoint
54
+ this.app.get('/status', (req, res) => {
55
+ res.json({
56
+ lastModified: this.lastModified.toISOString(),
57
+ buildInProgress: this.buildInProgress,
58
+ files: this.getFileList()
59
+ });
60
+ });
61
+
62
+ // Serve addon manifest
63
+ this.app.get('/manifest.json', (req, res) => {
64
+ try {
65
+ const manifestPath = path.resolve(this.config.manifestPath);
66
+ if (fs.existsSync(manifestPath)) {
67
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
68
+ res.json(manifest);
69
+ } else {
70
+ res.status(404).json({ error: 'Manifest not found' });
71
+ }
72
+ } catch (error) {
73
+ res.status(500).json({ error: 'Failed to read manifest' });
74
+ }
75
+ });
76
+
77
+ // Serve addon code
78
+ this.app.get('/addon.js', async (req, res) => {
79
+ try {
80
+ const addonFile = path.resolve(this.config.addonPath, 'dist/addon.js');
81
+ console.log(`📦 Serving addon.js from: ${addonFile}`);
82
+
83
+ // Wait for file to exist (with timeout)
84
+ const fileExists = await this.waitForFile(addonFile, 3000);
85
+
86
+ if (fileExists) {
87
+ const code = fs.readFileSync(addonFile, 'utf-8');
88
+ res.type('application/javascript').send(code);
89
+ } else {
90
+ console.error(`❌ Addon file not found at: ${addonFile}`);
91
+ res.status(404).json({ error: 'Addon file not found. Run build first.', path: addonFile });
92
+ }
93
+ } catch (error) {
94
+ console.error(`❌ Error serving addon.js:`, error);
95
+ res.status(500).json({ error: 'Failed to read addon file', details: error.message });
96
+ }
97
+ });
98
+
99
+ // Hot reload endpoint
100
+ this.app.get('/reload', (req, res) => {
101
+ res.json({
102
+ message: 'Reload triggered',
103
+ timestamp: new Date().toISOString()
104
+ });
105
+
106
+ // Trigger rebuild if configured
107
+ if (this.config.buildCommand) {
108
+ this.triggerBuild();
109
+ }
110
+ });
111
+
112
+ // File listing for debugging
113
+ this.app.get('/files', (req, res) => {
114
+ res.json({
115
+ files: this.getFileList(),
116
+ watchPaths: this.config.watchPaths
117
+ });
118
+ });
119
+
120
+ // Test endpoint for connectivity
121
+ this.app.get('/test', (req, res) => {
122
+ res.json({
123
+ message: 'Addon development server is working!',
124
+ addonPath: this.config.addonPath,
125
+ timestamp: new Date().toISOString(),
126
+ manifest: this.getManifestInfo()
127
+ });
128
+ });
129
+
130
+ // Debug endpoint for troubleshooting
131
+ this.app.get('/debug', (req, res) => {
132
+ const addonFile = path.resolve(this.config.addonPath, 'dist/addon.js');
133
+ res.json({
134
+ lastModified: this.lastModified.toISOString(),
135
+ buildInProgress: this.buildInProgress,
136
+ files: this.getFileList(),
137
+ watchPaths: this.config.watchPaths,
138
+ viteWatcherRunning: this.viteWatcher !== null,
139
+ addonFile: {
140
+ path: addonFile,
141
+ exists: fs.existsSync(addonFile),
142
+ size: fs.existsSync(addonFile) ? fs.statSync(addonFile).size : 0
143
+ },
144
+ config: {
145
+ port: this.config.port,
146
+ buildCommand: this.config.buildCommand
147
+ }
148
+ });
149
+ });
150
+
151
+ // Simple ping endpoint
152
+ this.app.get('/ping', (req, res) => {
153
+ res.json({ message: 'pong', timestamp: new Date().toISOString() });
154
+ });
155
+ }
156
+
157
+ setupFileWatcher() {
158
+ const watcher = chokidar.watch(this.config.watchPaths, {
159
+ ignored: /node_modules|\.git/,
160
+ persistent: true,
161
+ ignoreInitial: true
162
+ });
163
+
164
+ watcher.on('change', (filePath) => {
165
+ console.log(`📝 File changed: ${filePath}`);
166
+ // Don't trigger manual build since Vite is already watching
167
+ // Just update the timestamp for status endpoint
168
+ this.lastModified = new Date();
169
+ });
170
+
171
+ watcher.on('add', (filePath) => {
172
+ console.log(`➕ File added: ${filePath}`);
173
+ this.lastModified = new Date();
174
+ });
175
+
176
+ watcher.on('unlink', (filePath) => {
177
+ console.log(`➖ File removed: ${filePath}`);
178
+ this.lastModified = new Date();
179
+ });
180
+
181
+ console.log(`👀 Watching files: ${this.config.watchPaths.join(', ')}`);
182
+ }
183
+
184
+ async triggerBuild() {
185
+ if (this.buildInProgress || !this.config.buildCommand) return;
186
+
187
+ this.buildInProgress = true;
188
+ console.log(`🔨 Building addon with: ${this.config.buildCommand}`);
189
+
190
+ try {
191
+ await execAsync(this.config.buildCommand, {
192
+ cwd: this.config.addonPath
193
+ });
194
+
195
+ console.log('✅ Build completed successfully');
196
+ this.lastModified = new Date();
197
+ } catch (error) {
198
+ console.error('❌ Build failed:', error);
199
+ } finally {
200
+ this.buildInProgress = false;
201
+ }
202
+ }
203
+
204
+ getFileList() {
205
+ try {
206
+ const distPath = path.resolve(this.config.addonPath, 'dist');
207
+ if (fs.existsSync(distPath)) {
208
+ return fs.readdirSync(distPath).map(file => `dist/${file}`);
209
+ }
210
+ return [];
211
+ } catch (error) {
212
+ return [];
213
+ }
214
+ }
215
+
216
+ getManifestInfo() {
217
+ try {
218
+ const manifestPath = path.resolve(this.config.manifestPath);
219
+ if (fs.existsSync(manifestPath)) {
220
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
221
+ }
222
+ return null;
223
+ } catch (error) {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Wait for a file to exist with timeout
230
+ */
231
+ async waitForFile(filePath, timeout = 3000) {
232
+ const startTime = Date.now();
233
+ const checkInterval = 100;
234
+
235
+ while (Date.now() - startTime < timeout) {
236
+ if (fs.existsSync(filePath)) {
237
+ // Additional check to ensure file is fully written
238
+ try {
239
+ const stats = fs.statSync(filePath);
240
+ if (stats.size > 0) {
241
+ return true;
242
+ }
243
+ } catch (err) {
244
+ // File might be in the process of being written
245
+ }
246
+ }
247
+
248
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
249
+ }
250
+
251
+ return false;
252
+ }
253
+
254
+ startViteWatcher() {
255
+ if (!this.config.buildCommand) return;
256
+
257
+ console.log('🔨 Starting Vite in watch mode...');
258
+
259
+ // Start vite build in watch mode
260
+ const { spawn } = require('child_process');
261
+ this.viteWatcher = spawn('npm', ['run', 'dev'], {
262
+ cwd: this.config.addonPath,
263
+ stdio: ['ignore', 'pipe', 'pipe']
264
+ });
265
+
266
+ this.viteWatcher.stdout.on('data', (data) => {
267
+ const output = data.toString();
268
+ console.log(`Vite output: ${output.trim()}`);
269
+
270
+ if (output.includes('build started')) {
271
+ this.buildInProgress = true;
272
+ }
273
+
274
+ if (output.includes('built in')) {
275
+ console.log(`✅ Vite rebuild completed`);
276
+ this.lastModified = new Date();
277
+ this.buildInProgress = false;
278
+ }
279
+
280
+ if (output.includes('watching for file changes')) {
281
+ console.log(`✅ Vite watcher ready`);
282
+ this.buildInProgress = false;
283
+ }
284
+ });
285
+
286
+ this.viteWatcher.stderr.on('data', (data) => {
287
+ console.error(`Vite error: ${data}`);
288
+ });
289
+
290
+ this.viteWatcher.on('close', (code) => {
291
+ if (code !== 0) {
292
+ console.error(`Vite watcher exited with code ${code}`);
293
+ }
294
+ });
295
+ }
296
+
297
+ start() {
298
+ this.app.listen(this.config.port, () => {
299
+ console.log(`🚀 Addon dev server running on http://localhost:${this.config.port}`);
300
+ console.log(`📁 Serving from: ${this.config.addonPath}`);
301
+ console.log(`📋 Manifest: ${this.config.manifestPath}`);
302
+ console.log(`👀 Watching files: ${this.config.watchPaths.join(', ')}`);
303
+
304
+ if (this.config.buildCommand) {
305
+ console.log(`🔨 Build command: ${this.config.buildCommand}`);
306
+ }
307
+ });
308
+
309
+ // Handle graceful shutdown
310
+ process.on('SIGINT', () => {
311
+ this.stop();
312
+ process.exit(0);
313
+ });
314
+
315
+ process.on('SIGTERM', () => {
316
+ this.stop();
317
+ process.exit(0);
318
+ });
319
+ }
320
+
321
+ stop() {
322
+ console.log('🛑 Shutting down dev server...');
323
+
324
+ if (this.viteWatcher) {
325
+ this.viteWatcher.kill('SIGTERM');
326
+ this.viteWatcher = null;
327
+ }
328
+ }
329
+ }
330
+
331
+ // CLI interface
332
+ function main() {
333
+ const args = process.argv.slice(2);
334
+ const addonPath = args[0] || process.cwd();
335
+ const port = parseInt(args[1]) || 3001;
336
+
337
+ const config = {
338
+ port,
339
+ addonPath: path.resolve(addonPath),
340
+ manifestPath: path.resolve(addonPath, 'manifest.json'),
341
+ buildCommand: 'npm run build',
342
+ watchPaths: [
343
+ path.resolve(addonPath, 'src'),
344
+ path.resolve(addonPath, 'manifest.json')
345
+ ]
346
+ };
347
+
348
+ // Check if addon directory exists
349
+ if (!fs.existsSync(config.addonPath)) {
350
+ console.error(`❌ Addon directory not found: ${config.addonPath}`);
351
+ process.exit(1);
352
+ }
353
+
354
+ // Check if manifest exists
355
+ if (!fs.existsSync(config.manifestPath)) {
356
+ console.error(`❌ Manifest not found: ${config.manifestPath}`);
357
+ process.exit(1);
358
+ }
359
+
360
+ const server = new AddonDevServer(config);
361
+ server.start();
362
+ }
363
+
364
+ // Export for use as a module
365
+ module.exports = { AddonDevServer };
366
+
367
+ // Run if called directly
368
+ if (require.main === module) {
369
+ main();
370
+ }
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ AddonDevServer: require('./dev-server').AddonDevServer
3
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@wealthfolio/addon-dev-tools",
3
+ "version": "1.0.0",
4
+ "description": "Development tools for Wealthfolio addons - hot reload server and CLI",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "wealthfolio": "cli.js"
8
+ },
9
+ "keywords": [
10
+ "wealthfolio",
11
+ "addon",
12
+ "development",
13
+ "cli",
14
+ "hot-reload"
15
+ ],
16
+ "author": "Wealthfolio Team",
17
+
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/afadil/wealthfolio.git",
22
+ "directory": "packages/addon-dev-tools"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/afadil/wealthfolio/issues"
26
+ },
27
+ "homepage": "https://github.com/afadil/wealthfolio/tree/main/packages/addon-dev-tools#readme",
28
+ "files": [
29
+ "cli.js",
30
+ "dev-server.js",
31
+ "index.js",
32
+ "scaffold.js",
33
+ "templates/",
34
+ "README.md"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "test": "echo \"No tests yet\" && exit 0"
41
+ },
42
+ "dependencies": {
43
+ "commander": "^14.0.0",
44
+ "express": "^4.21.2",
45
+ "cors": "^2.8.5",
46
+ "chokidar": "^4.0.3"
47
+ },
48
+ "engines": {
49
+ "node": ">=20.0.0"
50
+ }
51
+ }
package/scaffold.js ADDED
@@ -0,0 +1,163 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Scaffold service for creating new addons from templates
6
+ */
7
+ class AddonScaffold {
8
+ constructor() {
9
+ this.templatesDir = path.join(__dirname, 'templates');
10
+ }
11
+
12
+ /**
13
+ * Get all available templates
14
+ */
15
+ getAvailableTemplates() {
16
+ if (!fs.existsSync(this.templatesDir)) {
17
+ throw new Error('Templates directory not found');
18
+ }
19
+
20
+ return fs.readdirSync(this.templatesDir)
21
+ .filter(file => file.endsWith('.template'))
22
+ .map(file => file.replace('.template', ''));
23
+ }
24
+
25
+ /**
26
+ * Load a template file
27
+ */
28
+ loadTemplate(templateName) {
29
+ const templatePath = path.join(this.templatesDir, `${templateName}.template`);
30
+
31
+ if (!fs.existsSync(templatePath)) {
32
+ throw new Error(`Template ${templateName} not found`);
33
+ }
34
+
35
+ return fs.readFileSync(templatePath, 'utf-8');
36
+ }
37
+
38
+ /**
39
+ * Replace template variables with actual values
40
+ */
41
+ processTemplate(content, replacements) {
42
+ let result = content;
43
+ for (const [key, value] of Object.entries(replacements)) {
44
+ const pattern = new RegExp(`{{${key}}}`, 'g');
45
+ result = result.replace(pattern, value);
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Generate replacements object from addon config
52
+ */
53
+ generateReplacements(config) {
54
+ const addonId = config.name.toLowerCase().replace(/[^a-z0-9]/g, '-');
55
+ const packageName = `wealthfolio-${addonId}-addon`;
56
+ const componentName = config.name.replace(/[^a-zA-Z0-9]/g, '');
57
+
58
+ return {
59
+ addonId,
60
+ addonName: config.name,
61
+ packageName,
62
+ componentName,
63
+ description: config.description || `A Wealthfolio addon for ${config.name}`,
64
+ author: config.author || 'Anonymous'
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Create addon structure
70
+ */
71
+ async createAddon(config, targetDir) {
72
+ const replacements = this.generateReplacements(config);
73
+
74
+ // Create directory structure
75
+ if (!fs.existsSync(targetDir)) {
76
+ fs.mkdirSync(targetDir, { recursive: true });
77
+ }
78
+
79
+ // Create src directory and subdirectories
80
+ const srcDir = path.join(targetDir, 'src');
81
+ const srcSubDirs = ['components', 'hooks', 'pages', 'lib', 'types'];
82
+
83
+ if (!fs.existsSync(srcDir)) {
84
+ fs.mkdirSync(srcDir);
85
+ }
86
+
87
+ // Create all subdirectories
88
+ srcSubDirs.forEach(subDir => {
89
+ const subDirPath = path.join(srcDir, subDir);
90
+ if (!fs.existsSync(subDirPath)) {
91
+ fs.mkdirSync(subDirPath, { recursive: true });
92
+ }
93
+ });
94
+
95
+ // Template file mappings
96
+ const fileTemplates = [
97
+ { template: 'manifest.json', output: 'manifest.json' },
98
+ { template: 'package.json', output: 'package.json' },
99
+ { template: 'vite.config.ts', output: 'vite.config.ts' },
100
+ { template: 'tsconfig.json', output: 'tsconfig.json' },
101
+ { template: 'README.md', output: 'README.md' },
102
+ { template: 'CHANGELOG.md', output: 'CHANGELOG.md' },
103
+ { template: 'addon.tsx', output: 'src/addon.tsx' },
104
+ { template: 'components-index.ts', output: 'src/components/index.ts' },
105
+ { template: 'hooks-index.ts', output: 'src/hooks/index.ts' },
106
+ { template: 'pages-index.ts', output: 'src/pages/index.ts' },
107
+ { template: 'lib-index.ts', output: 'src/lib/index.ts' },
108
+ { template: 'types-index.ts', output: 'src/types/index.ts' }
109
+ ];
110
+
111
+ // Process and write each template
112
+ for (const { template, output } of fileTemplates) {
113
+ try {
114
+ const templateContent = this.loadTemplate(template);
115
+ const processedContent = this.processTemplate(templateContent, replacements);
116
+ const outputPath = path.join(targetDir, output);
117
+
118
+ // Ensure directory exists
119
+ const outputDir = path.dirname(outputPath);
120
+ if (!fs.existsSync(outputDir)) {
121
+ fs.mkdirSync(outputDir, { recursive: true });
122
+ }
123
+
124
+ fs.writeFileSync(outputPath, processedContent);
125
+ } catch (error) {
126
+ throw new Error(`Failed to process template ${template}: ${error.message}`);
127
+ }
128
+ }
129
+
130
+ return {
131
+ addonDir: targetDir,
132
+ addonId: replacements.addonId,
133
+ packageName: replacements.packageName
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Validate addon configuration
139
+ */
140
+ validateConfig(config) {
141
+ const errors = [];
142
+
143
+ if (!config.name || typeof config.name !== 'string') {
144
+ errors.push('Addon name is required and must be a string');
145
+ }
146
+
147
+ if (config.name && config.name.trim().length === 0) {
148
+ errors.push('Addon name cannot be empty');
149
+ }
150
+
151
+ if (config.description && typeof config.description !== 'string') {
152
+ errors.push('Description must be a string');
153
+ }
154
+
155
+ if (config.author && typeof config.author !== 'string') {
156
+ errors.push('Author must be a string');
157
+ }
158
+
159
+ return errors;
160
+ }
161
+ }
162
+
163
+ module.exports = { AddonScaffold };
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to the {{name}} addon will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial addon structure and setup
12
+
13
+ ### Changed
14
+
15
+ ### Deprecated
16
+
17
+ ### Removed
18
+
19
+ ### Fixed
20
+
21
+ ### Security
22
+
23
+ ## [1.0.0] - {{currentDate}}
24
+
25
+ ### Added
26
+ - Initial release of {{name}} addon
27
+ - Basic addon functionality and core features
28
+ - Integration with Wealthfolio addon SDK v1.0.0
29
+ - Sidebar navigation integration for easy access
30
+ - Responsive design for all screen sizes
31
+
32
+ ### Features
33
+ - {{description}}
34
+ - User-friendly interface
35
+ - Compatible with Wealthfolio platform
36
+
37
+ ### Permissions
38
+ - UI components access for sidebar and routing
@@ -0,0 +1,27 @@
1
+ # {{addonName}}
2
+
3
+ {{description}}
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ npm install
10
+
11
+ # Start development server
12
+ npm run dev:server
13
+
14
+ # Build for production
15
+ npm run build
16
+
17
+ # Package addon
18
+ npm run bundle
19
+ ```
20
+
21
+ ## Features
22
+
23
+ - Add your features here
24
+
25
+ ## License
26
+
27
+ MIT
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import type { AddonContext } from '@wealthfolio/addon-sdk';
3
+ import { Card, CardContent, Icons } from '@wealthfolio/ui';
4
+
5
+ function AddonExample({ ctx }: { ctx: AddonContext }) {
6
+ return (
7
+ <div className="p-6">
8
+ <Card>
9
+ <CardContent className="p-6">
10
+ <h1 className="text-2xl font-semibold mb-2">{{addonName}}</h1>
11
+ <p className="text-muted-foreground">
12
+ Welcome to your new Wealthfolio addon! Start building amazing features.
13
+ </p>
14
+ </CardContent>
15
+ </Card>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ export default function enable(ctx: AddonContext) {
21
+ // Add a sidebar item
22
+ const sidebarItem = ctx.sidebar.addItem({
23
+ id: '{{addonId}}',
24
+ label: '{{addonName}}',
25
+ icon: <Icons.Blocks className="h-5 w-5" />,
26
+ route: '/addon/{{addonId}}',
27
+ order: 100,
28
+ });
29
+
30
+ // Add a route
31
+ const Wrapper = () => <AddonExample ctx={ctx} />;
32
+ ctx.router.add({
33
+ path: '/addon/{{addonId}}',
34
+ component: React.lazy(() => Promise.resolve({ default: Wrapper })),
35
+ });
36
+
37
+ // Cleanup on disable
38
+ ctx.onDisable(() => {
39
+ try {
40
+ sidebarItem.remove();
41
+ } catch (err) {
42
+ ctx.api.logger.error('Failed to remove sidebar item:', err);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,3 @@
1
+ // Export your components here
2
+ // Example:
3
+ // export { default as MyComponent } from './MyComponent';
@@ -0,0 +1,3 @@
1
+ // Export your custom hooks here
2
+ // Example:
3
+ // export { useMyHook } from './useMyHook';
@@ -0,0 +1,5 @@
1
+ // Export your utility functions and shared logic here
2
+ // Example:
3
+ // export { formatCurrency } from './currency';
4
+ // export { calculateReturns } from './calculations';
5
+ // export { validateInput } from './validation';
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "{{addonId}}",
3
+ "name": "{{addonName}}",
4
+ "version": "1.0.0",
5
+ "description": "{{description}}",
6
+ "author": "{{author}}",
7
+ "main": "dist/addon.js",
8
+ "sdkVersion": "1.0.0",
9
+ "enabled": true,
10
+ "permissions": [
11
+ {
12
+ "category": "ui",
13
+ "functions": ["sidebar.addItem"],
14
+ "purpose": "Add navigation items to the sidebar"
15
+ }
16
+ ],
17
+ "keywords": ["wealthfolio", "addon"],
18
+ "license": "MIT"
19
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "version": "1.0.0",
4
+ "description": "{{description}}",
5
+ "type": "module",
6
+ "main": "dist/addon.js",
7
+ "keywords": ["wealthfolio", "addon"],
8
+ "license": "MIT",
9
+ "scripts": {
10
+ "build": "vite build",
11
+ "dev": "vite build --watch",
12
+ "dev:server": "wealthfolio dev",
13
+ "clean": "rm -rf dist",
14
+ "package": "mkdir -p dist && zip -r dist/$npm_package_name-$npm_package_version.zip manifest.json dist/ assets/ README.md",
15
+ "bundle": "pnpm clean && pnpm build && pnpm package",
16
+ "lint": "tsc --noEmit",
17
+ "type-check": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@wealthfolio/addon-sdk": "^1.0.0",
21
+ "@wealthfolio/ui": "^1.0.0",
22
+ "react": "^18.3.1",
23
+ "react-dom": "^18.3.1"
24
+ },
25
+ "devDependencies": {
26
+ "@wealthfolio/addon-dev-tools": "^1.0.0",
27
+ "@types/node": "^22.14.0",
28
+ "@types/react": "^18.3.11",
29
+ "@types/react-dom": "^18.3.0",
30
+ "@vitejs/plugin-react": "^4.4.1",
31
+ "rollup-plugin-external-globals": "^0.13.0",
32
+ "typescript": "^5.8.3",
33
+ "vite": "^6.2.7"
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+ // Export your addon pages here
2
+ // Example:
3
+ // export { default as SettingsPage } from './SettingsPage';
4
+ // export { default as DashboardPage } from './DashboardPage';
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "jsx": "react-jsx",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "types": ["node", "react", "react-dom"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,10 @@
1
+ // Export your type definitions here
2
+ // Example:
3
+ // export interface AddonConfig {
4
+ // enabled: boolean;
5
+ // settings: Record<string, any>;
6
+ // }
7
+ //
8
+ // export type AddonPageProps = {
9
+ // onSettingsChange: (settings: Record<string, any>) => void;
10
+ // };
@@ -0,0 +1,40 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import externalGlobals from 'rollup-plugin-external-globals';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ define: {
8
+ 'process.env.NODE_ENV': JSON.stringify('production'),
9
+ },
10
+ build: {
11
+ lib: {
12
+ entry: 'src/addon.tsx',
13
+ fileName: () => 'addon.js',
14
+ formats: ['es'],
15
+ },
16
+ rollupOptions: {
17
+ external: ['react', 'react-dom'],
18
+ plugins: [
19
+ externalGlobals({
20
+ react: 'React',
21
+ 'react-dom': 'ReactDOM'
22
+ })
23
+ ],
24
+ output: {
25
+ globals: {
26
+ react: 'React',
27
+ 'react-dom': 'ReactDOM',
28
+ },
29
+ },
30
+ },
31
+ outDir: 'dist',
32
+ minify: false,
33
+ sourcemap: false,
34
+ watch: {
35
+ // Watch mode options for better hot reloading
36
+ include: ['src/**'],
37
+ exclude: ['node_modules/**', 'dist/**']
38
+ }
39
+ },
40
+ });