create-tx5dr-plugin 1.1.0 → 1.2.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 +130 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +732 -18
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# create-tx5dr-plugin
|
|
2
|
+
|
|
3
|
+
Scaffold a new [TX-5DR](https://github.com/boybook/tx-5dr) plugin project.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-tx5dr-plugin [name] [options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Options
|
|
12
|
+
|
|
13
|
+
| Flag | Description | Default |
|
|
14
|
+
|------|-------------|---------|
|
|
15
|
+
| `--type <utility\|strategy>` | Plugin type | `utility` |
|
|
16
|
+
| `--lang <ts\|js>` | Language | `ts` |
|
|
17
|
+
| `--template <basic\|ui-vanilla\|ui-react\|ui-vue>` | Project template | `basic` |
|
|
18
|
+
| `--help, -h` | Show help | |
|
|
19
|
+
|
|
20
|
+
### Templates
|
|
21
|
+
|
|
22
|
+
| Template | Description |
|
|
23
|
+
|----------|-------------|
|
|
24
|
+
| `basic` | Server-side plugin only (no UI) |
|
|
25
|
+
| `ui-vanilla` | Plugin with vanilla HTML/JS/CSS UI page |
|
|
26
|
+
| `ui-react` | Plugin with React + Vite UI page |
|
|
27
|
+
| `ui-vue` | Plugin with Vue + Vite UI page |
|
|
28
|
+
|
|
29
|
+
### Examples
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Interactive mode (prompts for all options)
|
|
33
|
+
npx create-tx5dr-plugin
|
|
34
|
+
|
|
35
|
+
# Basic utility plugin
|
|
36
|
+
npx create-tx5dr-plugin my-plugin
|
|
37
|
+
|
|
38
|
+
# Strategy plugin
|
|
39
|
+
npx create-tx5dr-plugin my-strategy --type strategy
|
|
40
|
+
|
|
41
|
+
# Plugin with React UI
|
|
42
|
+
npx create-tx5dr-plugin my-plugin --template ui-react
|
|
43
|
+
|
|
44
|
+
# Plugin with Vue UI
|
|
45
|
+
npx create-tx5dr-plugin my-plugin --template ui-vue
|
|
46
|
+
|
|
47
|
+
# Vanilla UI (no build step for UI files)
|
|
48
|
+
npx create-tx5dr-plugin my-plugin --template ui-vanilla
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Generated Structure
|
|
52
|
+
|
|
53
|
+
### Basic (no UI)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
my-plugin/
|
|
57
|
+
├── package.json
|
|
58
|
+
├── tsconfig.json
|
|
59
|
+
├── .gitignore
|
|
60
|
+
├── scripts/
|
|
61
|
+
│ └── link.mjs # Symlink to TX-5DR plugins dir
|
|
62
|
+
└── src/
|
|
63
|
+
├── index.ts
|
|
64
|
+
├── locales/
|
|
65
|
+
└── __tests__/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### React / Vue UI
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
my-plugin/
|
|
72
|
+
├── package.json # Includes vite + framework deps
|
|
73
|
+
├── tsconfig.json # Server-side TS config
|
|
74
|
+
├── scripts/
|
|
75
|
+
│ └── link.mjs
|
|
76
|
+
├── src/
|
|
77
|
+
│ ├── index.ts # Plugin definition with UI pages
|
|
78
|
+
│ ├── locales/
|
|
79
|
+
│ └── __tests__/
|
|
80
|
+
├── ui/ # Vite project
|
|
81
|
+
│ ├── vite.config.ts # MPA build config
|
|
82
|
+
│ ├── tsconfig.json # Frontend TS (includes bridge types)
|
|
83
|
+
│ ├── settings.html # HTML entry
|
|
84
|
+
│ └── src/
|
|
85
|
+
│ ├── main.tsx / main.ts # Framework entry
|
|
86
|
+
│ ├── App.tsx / App.vue # Main component
|
|
87
|
+
│ └── App.css # Uses --tx5dr-* CSS tokens
|
|
88
|
+
└── dist/ # Build output
|
|
89
|
+
├── index.js # Server-side plugin
|
|
90
|
+
└── ui/
|
|
91
|
+
└── settings.html # Built iframe page
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development Workflow
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cd my-plugin
|
|
98
|
+
npm install
|
|
99
|
+
npm run build # Build everything (server + UI)
|
|
100
|
+
npm run link # Symlink dist/ to TX-5DR plugins dir
|
|
101
|
+
|
|
102
|
+
# Development (two terminals):
|
|
103
|
+
npm run dev:server # Watch server-side TypeScript
|
|
104
|
+
npm run dev:ui # Watch UI with Vite (React/Vue only)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### How `npm run link` works
|
|
108
|
+
|
|
109
|
+
The `scripts/link.mjs` script:
|
|
110
|
+
1. Detects the TX-5DR data directory (platform-specific, or `TX5DR_DATA_DIR` env)
|
|
111
|
+
2. Creates a symlink from `dist/` to `{dataDir}/plugins/{plugin-name}`
|
|
112
|
+
3. Creates a `.hotreload` marker so TX-5DR dev server auto-reloads on changes
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run link # Create symlink
|
|
116
|
+
npm run link -- --unlink # Remove symlink
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Hot Reload
|
|
120
|
+
|
|
121
|
+
In development mode (`NODE_ENV !== 'production'`), the TX-5DR server watches plugin directories that contain a `.hotreload` marker file. When files change, the plugin is automatically reloaded after a short debounce.
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
- [Plugin System Guide](https://github.com/boybook/tx-5dr/blob/main/docs/plugin-system.md)
|
|
126
|
+
- [@tx5dr/plugin-api](https://www.npmjs.com/package/@tx5dr/plugin-api) — TypeScript types and testing utilities
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
* npx create-tx5dr-plugin # Interactive
|
|
7
7
|
* npx create-tx5dr-plugin my-plugin # Name only, prompts for rest
|
|
8
8
|
* npx create-tx5dr-plugin my-plugin --type utility # Non-interactive
|
|
9
|
-
* npx create-tx5dr-plugin my-plugin --
|
|
9
|
+
* npx create-tx5dr-plugin my-plugin --template ui-react
|
|
10
10
|
*/
|
|
11
11
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* npx create-tx5dr-plugin # Interactive
|
|
7
7
|
* npx create-tx5dr-plugin my-plugin # Name only, prompts for rest
|
|
8
8
|
* npx create-tx5dr-plugin my-plugin --type utility # Non-interactive
|
|
9
|
-
* npx create-tx5dr-plugin my-plugin --
|
|
9
|
+
* npx create-tx5dr-plugin my-plugin --template ui-react
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { join, resolve } from 'node:path';
|
|
13
13
|
import { createInterface } from 'node:readline';
|
|
14
|
+
const VALID_TEMPLATES = ['basic', 'ui-vanilla', 'ui-react', 'ui-vue'];
|
|
14
15
|
// ===== CLI argument parsing =====
|
|
15
16
|
function parseArgs() {
|
|
16
17
|
const args = process.argv.slice(2);
|
|
@@ -30,6 +31,12 @@ function parseArgs() {
|
|
|
30
31
|
config.lang = value;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
else if (arg === '--template' && i + 1 < args.length) {
|
|
35
|
+
const value = args[++i];
|
|
36
|
+
if (VALID_TEMPLATES.includes(value)) {
|
|
37
|
+
config.template = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
33
40
|
else if (arg === '--help' || arg === '-h') {
|
|
34
41
|
printUsage();
|
|
35
42
|
process.exit(0);
|
|
@@ -49,14 +56,22 @@ function printUsage() {
|
|
|
49
56
|
npx create-tx5dr-plugin [name] [options]
|
|
50
57
|
|
|
51
58
|
Options:
|
|
52
|
-
--type <utility|strategy>
|
|
53
|
-
--lang <ts|js>
|
|
54
|
-
--
|
|
59
|
+
--type <utility|strategy> Plugin type (default: utility)
|
|
60
|
+
--lang <ts|js> Language (default: ts)
|
|
61
|
+
--template <basic|ui-vanilla|ui-react|ui-vue> Template (default: basic)
|
|
62
|
+
--help, -h Show this help message
|
|
63
|
+
|
|
64
|
+
Templates:
|
|
65
|
+
basic Server-side plugin only (no UI)
|
|
66
|
+
ui-vanilla Plugin with vanilla HTML/JS/CSS UI page
|
|
67
|
+
ui-react Plugin with React + Vite UI page
|
|
68
|
+
ui-vue Plugin with Vue + Vite UI page
|
|
55
69
|
|
|
56
70
|
Examples:
|
|
57
|
-
npx create-tx5dr-plugin my-
|
|
58
|
-
npx create-tx5dr-plugin my-
|
|
59
|
-
npx create-tx5dr-plugin my-
|
|
71
|
+
npx create-tx5dr-plugin my-plugin
|
|
72
|
+
npx create-tx5dr-plugin my-plugin --type strategy
|
|
73
|
+
npx create-tx5dr-plugin my-plugin --template ui-react
|
|
74
|
+
npx create-tx5dr-plugin my-plugin --template ui-vue --type utility
|
|
60
75
|
`);
|
|
61
76
|
}
|
|
62
77
|
// ===== Interactive prompts =====
|
|
@@ -83,28 +98,61 @@ async function promptConfig(partial) {
|
|
|
83
98
|
const answer = await prompt(rl, 'Language (ts/js) [ts]: ');
|
|
84
99
|
lang = answer === 'js' ? 'js' : 'ts';
|
|
85
100
|
}
|
|
86
|
-
|
|
101
|
+
let template = partial.template;
|
|
102
|
+
if (!template) {
|
|
103
|
+
const answer = await prompt(rl, 'Template (basic/ui-vanilla/ui-react/ui-vue) [basic]: ');
|
|
104
|
+
template = VALID_TEMPLATES.includes(answer) ? answer : 'basic';
|
|
105
|
+
}
|
|
106
|
+
return { name, type, lang, template };
|
|
87
107
|
}
|
|
88
108
|
finally {
|
|
89
109
|
rl.close();
|
|
90
110
|
}
|
|
91
111
|
}
|
|
92
|
-
// =====
|
|
112
|
+
// ===== Helpers =====
|
|
113
|
+
function hasUI(config) {
|
|
114
|
+
return config.template !== 'basic';
|
|
115
|
+
}
|
|
116
|
+
function hasVite(config) {
|
|
117
|
+
return config.template === 'ui-react' || config.template === 'ui-vue';
|
|
118
|
+
}
|
|
119
|
+
// ===== Core template generation =====
|
|
93
120
|
function generatePackageJson(config) {
|
|
94
|
-
const deps = {};
|
|
95
121
|
const devDeps = {
|
|
96
122
|
'@tx5dr/plugin-api': 'latest',
|
|
97
123
|
};
|
|
98
124
|
if (config.lang === 'ts') {
|
|
99
125
|
devDeps['typescript'] = '^5.0.0';
|
|
100
126
|
devDeps['vitest'] = '^1.0.0';
|
|
101
|
-
|
|
127
|
+
}
|
|
128
|
+
if (hasVite(config)) {
|
|
129
|
+
devDeps['vite'] = '^6.0.0';
|
|
130
|
+
if (config.template === 'ui-react') {
|
|
131
|
+
devDeps['react'] = '^19.0.0';
|
|
132
|
+
devDeps['react-dom'] = '^19.0.0';
|
|
133
|
+
devDeps['@vitejs/plugin-react'] = '^4.0.0';
|
|
134
|
+
devDeps['@types/react'] = '^19.0.0';
|
|
135
|
+
devDeps['@types/react-dom'] = '^19.0.0';
|
|
136
|
+
}
|
|
137
|
+
else if (config.template === 'ui-vue') {
|
|
138
|
+
devDeps['vue'] = '^3.5.0';
|
|
139
|
+
devDeps['@vitejs/plugin-vue'] = '^5.0.0';
|
|
140
|
+
}
|
|
102
141
|
}
|
|
103
142
|
const scripts = {};
|
|
104
143
|
if (config.lang === 'ts') {
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
if (hasVite(config)) {
|
|
145
|
+
scripts['build'] = 'tsc && npm run build:ui';
|
|
146
|
+
scripts['build:ui'] = 'vite build --config ui/vite.config.ts';
|
|
147
|
+
scripts['dev:server'] = 'tsc --watch';
|
|
148
|
+
scripts['dev:ui'] = 'vite build --watch --config ui/vite.config.ts';
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
scripts['build'] = 'tsc';
|
|
152
|
+
scripts['dev'] = 'tsc --watch';
|
|
153
|
+
}
|
|
107
154
|
scripts['test'] = 'vitest run';
|
|
155
|
+
scripts['link'] = 'node scripts/link.mjs';
|
|
108
156
|
}
|
|
109
157
|
const pkg = {
|
|
110
158
|
name: config.name,
|
|
@@ -116,9 +164,6 @@ function generatePackageJson(config) {
|
|
|
116
164
|
scripts,
|
|
117
165
|
devDependencies: devDeps,
|
|
118
166
|
};
|
|
119
|
-
if (Object.keys(deps).length > 0) {
|
|
120
|
-
pkg.dependencies = deps;
|
|
121
|
-
}
|
|
122
167
|
return JSON.stringify(pkg, null, 2) + '\n';
|
|
123
168
|
}
|
|
124
169
|
function generateTsConfig() {
|
|
@@ -153,6 +198,7 @@ function generateLocaleEn(config) {
|
|
|
153
198
|
pluginDescription: `${config.name} plugin`,
|
|
154
199
|
}, null, 2) + '\n';
|
|
155
200
|
}
|
|
201
|
+
// ===== Server-side plugin definition templates =====
|
|
156
202
|
function generateTsUtilityPlugin(config) {
|
|
157
203
|
return `import type {
|
|
158
204
|
PluginDefinition,
|
|
@@ -295,6 +341,78 @@ export const locales: Record<string, Record<string, string>> = {
|
|
|
295
341
|
en: enLocale,
|
|
296
342
|
};
|
|
297
343
|
|
|
344
|
+
export default plugin;
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
function generateTsUtilityPluginWithUI(config) {
|
|
348
|
+
return `import type {
|
|
349
|
+
PluginDefinition,
|
|
350
|
+
PluginContext,
|
|
351
|
+
ParsedFT8Message,
|
|
352
|
+
SlotInfo,
|
|
353
|
+
} from '@tx5dr/plugin-api';
|
|
354
|
+
import zhLocale from './locales/zh.json' with { type: 'json' };
|
|
355
|
+
import enLocale from './locales/en.json' with { type: 'json' };
|
|
356
|
+
|
|
357
|
+
export const plugin: PluginDefinition = {
|
|
358
|
+
name: '${config.name}',
|
|
359
|
+
version: '0.1.0',
|
|
360
|
+
type: 'utility',
|
|
361
|
+
description: 'pluginDescription',
|
|
362
|
+
|
|
363
|
+
settings: {
|
|
364
|
+
// Define your plugin settings here
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
ui: {
|
|
368
|
+
dir: 'ui',
|
|
369
|
+
pages: [
|
|
370
|
+
{
|
|
371
|
+
id: 'settings',
|
|
372
|
+
entry: 'settings.html',
|
|
373
|
+
title: 'settingsPage',
|
|
374
|
+
accessScope: 'admin',
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
onLoad(ctx: PluginContext): void {
|
|
380
|
+
ctx.ui.registerPageHandler({
|
|
381
|
+
async onMessage(pageId, action, data) {
|
|
382
|
+
if (action === 'getSettings') {
|
|
383
|
+
return {
|
|
384
|
+
message: ctx.store.global.get<string>('message', ''),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (action === 'saveSettings') {
|
|
388
|
+
const { message } = data as { message: string };
|
|
389
|
+
ctx.store.global.set('message', message);
|
|
390
|
+
ctx.log.info('Settings saved', { message });
|
|
391
|
+
return { ok: true };
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
hooks: {
|
|
399
|
+
onSlotStart(slotInfo: SlotInfo, messages: ParsedFT8Message[], ctx: PluginContext): void {
|
|
400
|
+
ctx.log.debug('Slot started', { slotId: slotInfo.id, messageCount: messages.length });
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
onDecode(messages: ParsedFT8Message[], ctx: PluginContext): void {
|
|
404
|
+
for (const msg of messages) {
|
|
405
|
+
ctx.log.debug('Decoded message', { raw: msg.rawMessage, snr: msg.snr });
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
export const locales: Record<string, Record<string, string>> = {
|
|
412
|
+
zh: zhLocale,
|
|
413
|
+
en: enLocale,
|
|
414
|
+
};
|
|
415
|
+
|
|
298
416
|
export default plugin;
|
|
299
417
|
`;
|
|
300
418
|
}
|
|
@@ -326,6 +444,63 @@ export const plugin = {
|
|
|
326
444
|
export default plugin;
|
|
327
445
|
`;
|
|
328
446
|
}
|
|
447
|
+
function generateJsUtilityPluginWithUI(config) {
|
|
448
|
+
return `/** @type {import('@tx5dr/plugin-api').PluginDefinition} */
|
|
449
|
+
export const plugin = {
|
|
450
|
+
name: '${config.name}',
|
|
451
|
+
version: '0.1.0',
|
|
452
|
+
type: 'utility',
|
|
453
|
+
description: 'pluginDescription',
|
|
454
|
+
|
|
455
|
+
settings: {},
|
|
456
|
+
|
|
457
|
+
ui: {
|
|
458
|
+
dir: 'ui',
|
|
459
|
+
pages: [
|
|
460
|
+
{
|
|
461
|
+
id: 'settings',
|
|
462
|
+
entry: 'settings.html',
|
|
463
|
+
title: 'settingsPage',
|
|
464
|
+
accessScope: 'admin',
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
onLoad(ctx) {
|
|
470
|
+
ctx.ui.registerPageHandler({
|
|
471
|
+
async onMessage(pageId, action, data) {
|
|
472
|
+
if (action === 'getSettings') {
|
|
473
|
+
return {
|
|
474
|
+
message: ctx.store.global.get('message', ''),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (action === 'saveSettings') {
|
|
478
|
+
ctx.store.global.set('message', data.message);
|
|
479
|
+
ctx.log.info('Settings saved', { message: data.message });
|
|
480
|
+
return { ok: true };
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
hooks: {
|
|
488
|
+
onSlotStart(slotInfo, messages, ctx) {
|
|
489
|
+
ctx.log.debug('Slot started', { slotId: slotInfo.id, messageCount: messages.length });
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
onDecode(messages, ctx) {
|
|
493
|
+
for (const msg of messages) {
|
|
494
|
+
ctx.log.debug('Decoded message', { raw: msg.rawMessage, snr: msg.snr });
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export default plugin;
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
// ===== Test templates =====
|
|
329
504
|
function generateTsTest(config) {
|
|
330
505
|
if (config.type === 'strategy') {
|
|
331
506
|
return `import { describe, it, expect } from 'vitest';
|
|
@@ -400,6 +575,490 @@ describe('${config.name}', () => {
|
|
|
400
575
|
});
|
|
401
576
|
`;
|
|
402
577
|
}
|
|
578
|
+
// ===== Vanilla UI templates =====
|
|
579
|
+
function generateVanillaUIHtml(config) {
|
|
580
|
+
return `<!DOCTYPE html>
|
|
581
|
+
<html>
|
|
582
|
+
<head>
|
|
583
|
+
<meta charset="UTF-8">
|
|
584
|
+
<link rel="stylesheet" href="settings.css">
|
|
585
|
+
</head>
|
|
586
|
+
<body>
|
|
587
|
+
<div class="container">
|
|
588
|
+
<h2 class="title">${config.name} Settings</h2>
|
|
589
|
+
|
|
590
|
+
<div class="section">
|
|
591
|
+
<label class="label">Message</label>
|
|
592
|
+
<input type="text" class="input" id="messageInput" placeholder="Enter a message">
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<div class="section">
|
|
596
|
+
<button class="btn btn-primary" id="saveBtn">Save</button>
|
|
597
|
+
<span class="status" id="status"></span>
|
|
598
|
+
</div>
|
|
599
|
+
|
|
600
|
+
<div class="section">
|
|
601
|
+
<p class="text-secondary" id="currentValue"></p>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
<script src="settings.js"></script>
|
|
605
|
+
</body>
|
|
606
|
+
</html>
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
function generateVanillaUIJs() {
|
|
610
|
+
return `/// <reference types="@tx5dr/plugin-api/bridge" />
|
|
611
|
+
(function () {
|
|
612
|
+
'use strict';
|
|
613
|
+
|
|
614
|
+
var bridge = window.tx5dr;
|
|
615
|
+
var messageInput = document.getElementById('messageInput');
|
|
616
|
+
var saveBtn = document.getElementById('saveBtn');
|
|
617
|
+
var status = document.getElementById('status');
|
|
618
|
+
var currentValue = document.getElementById('currentValue');
|
|
619
|
+
|
|
620
|
+
// Load saved value on page open
|
|
621
|
+
bridge.invoke('getSettings').then(function (settings) {
|
|
622
|
+
if (settings && settings.message) {
|
|
623
|
+
messageInput.value = settings.message;
|
|
624
|
+
currentValue.textContent = 'Current: ' + settings.message;
|
|
625
|
+
}
|
|
626
|
+
}).catch(function () {
|
|
627
|
+
// Use defaults
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Save button handler
|
|
631
|
+
saveBtn.addEventListener('click', function () {
|
|
632
|
+
var message = messageInput.value.trim();
|
|
633
|
+
if (!message) return;
|
|
634
|
+
|
|
635
|
+
saveBtn.disabled = true;
|
|
636
|
+
status.textContent = 'Saving...';
|
|
637
|
+
|
|
638
|
+
bridge.invoke('saveSettings', { message: message }).then(function () {
|
|
639
|
+
status.textContent = 'Saved!';
|
|
640
|
+
currentValue.textContent = 'Current: ' + message;
|
|
641
|
+
setTimeout(function () { status.textContent = ''; }, 2000);
|
|
642
|
+
}).catch(function (err) {
|
|
643
|
+
status.textContent = 'Error: ' + err.message;
|
|
644
|
+
}).finally(function () {
|
|
645
|
+
saveBtn.disabled = false;
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Listen for server push updates
|
|
650
|
+
bridge.onPush('settingsUpdated', function (data) {
|
|
651
|
+
if (data && data.message) {
|
|
652
|
+
messageInput.value = data.message;
|
|
653
|
+
currentValue.textContent = 'Current: ' + data.message;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Auto-resize iframe
|
|
658
|
+
var resizeObserver = new ResizeObserver(function () {
|
|
659
|
+
var h = document.body.scrollHeight;
|
|
660
|
+
if (h > 0) bridge.resize(h);
|
|
661
|
+
});
|
|
662
|
+
resizeObserver.observe(document.body);
|
|
663
|
+
bridge.resize(document.body.scrollHeight);
|
|
664
|
+
})();
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
function generateUICss() {
|
|
668
|
+
return `/* Plugin UI styles — using TX-5DR Design Tokens */
|
|
669
|
+
.container {
|
|
670
|
+
padding: var(--tx5dr-spacing-lg);
|
|
671
|
+
font-family: var(--tx5dr-font);
|
|
672
|
+
color: var(--tx5dr-text);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.title {
|
|
676
|
+
font-size: var(--tx5dr-font-size-lg);
|
|
677
|
+
font-weight: 600;
|
|
678
|
+
margin-bottom: var(--tx5dr-spacing-lg);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.section {
|
|
682
|
+
margin-bottom: var(--tx5dr-spacing-md);
|
|
683
|
+
display: flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
gap: var(--tx5dr-spacing-sm);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.label {
|
|
689
|
+
font-size: var(--tx5dr-font-size-sm);
|
|
690
|
+
color: var(--tx5dr-text-secondary);
|
|
691
|
+
min-width: 70px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.input {
|
|
695
|
+
flex: 1;
|
|
696
|
+
padding: var(--tx5dr-spacing-sm) var(--tx5dr-spacing-md);
|
|
697
|
+
background: var(--tx5dr-bg-content);
|
|
698
|
+
border: 1px solid var(--tx5dr-border);
|
|
699
|
+
border-radius: var(--tx5dr-radius-sm);
|
|
700
|
+
color: var(--tx5dr-text);
|
|
701
|
+
font-size: var(--tx5dr-font-size-md);
|
|
702
|
+
font-family: var(--tx5dr-font);
|
|
703
|
+
outline: none;
|
|
704
|
+
transition: border-color 0.2s;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.input:focus {
|
|
708
|
+
border-color: var(--tx5dr-primary);
|
|
709
|
+
box-shadow: 0 0 0 2px var(--tx5dr-focus-ring);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.btn {
|
|
713
|
+
padding: var(--tx5dr-spacing-sm) var(--tx5dr-spacing-lg);
|
|
714
|
+
border: 1px solid var(--tx5dr-border);
|
|
715
|
+
border-radius: var(--tx5dr-radius-sm);
|
|
716
|
+
background: var(--tx5dr-bg-content);
|
|
717
|
+
color: var(--tx5dr-text);
|
|
718
|
+
font-size: var(--tx5dr-font-size-sm);
|
|
719
|
+
font-family: var(--tx5dr-font);
|
|
720
|
+
cursor: pointer;
|
|
721
|
+
transition: background 0.15s;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.btn:hover {
|
|
725
|
+
background: var(--tx5dr-bg-hover);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.btn:disabled {
|
|
729
|
+
opacity: 0.5;
|
|
730
|
+
cursor: not-allowed;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.btn-primary {
|
|
734
|
+
background: var(--tx5dr-primary);
|
|
735
|
+
border-color: var(--tx5dr-primary);
|
|
736
|
+
color: #fff;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.btn-primary:hover {
|
|
740
|
+
background: var(--tx5dr-primary-hover);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.status {
|
|
744
|
+
font-size: var(--tx5dr-font-size-sm);
|
|
745
|
+
color: var(--tx5dr-text-secondary);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.text-secondary {
|
|
749
|
+
font-size: var(--tx5dr-font-size-sm);
|
|
750
|
+
color: var(--tx5dr-text-secondary);
|
|
751
|
+
}
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
function generateVanillaJsConfig() {
|
|
755
|
+
return JSON.stringify({
|
|
756
|
+
compilerOptions: {
|
|
757
|
+
types: ['@tx5dr/plugin-api/bridge'],
|
|
758
|
+
},
|
|
759
|
+
}, null, 2) + '\n';
|
|
760
|
+
}
|
|
761
|
+
// ===== Vite-based UI templates (React / Vue) =====
|
|
762
|
+
function generateViteConfig(config) {
|
|
763
|
+
const pluginImport = config.template === 'ui-react'
|
|
764
|
+
? "import react from '@vitejs/plugin-react';"
|
|
765
|
+
: "import vue from '@vitejs/plugin-vue';";
|
|
766
|
+
const pluginUsage = config.template === 'ui-react' ? 'react()' : 'vue()';
|
|
767
|
+
return `import { defineConfig } from 'vite';
|
|
768
|
+
${pluginImport}
|
|
769
|
+
import { resolve } from 'path';
|
|
770
|
+
|
|
771
|
+
export default defineConfig({
|
|
772
|
+
plugins: [${pluginUsage}],
|
|
773
|
+
root: import.meta.dirname,
|
|
774
|
+
build: {
|
|
775
|
+
outDir: '../dist/ui',
|
|
776
|
+
emptyOutDir: true,
|
|
777
|
+
rollupOptions: {
|
|
778
|
+
input: {
|
|
779
|
+
settings: resolve(import.meta.dirname, 'settings.html'),
|
|
780
|
+
// Add more entries here for multi-page plugins:
|
|
781
|
+
// dashboard: resolve(import.meta.dirname, 'dashboard.html'),
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
`;
|
|
787
|
+
}
|
|
788
|
+
function generateUITsConfig(config) {
|
|
789
|
+
const jsx = config.template === 'ui-react' ? { jsx: 'react-jsx' } : {};
|
|
790
|
+
return JSON.stringify({
|
|
791
|
+
compilerOptions: {
|
|
792
|
+
target: 'ES2021',
|
|
793
|
+
module: 'ESNext',
|
|
794
|
+
moduleResolution: 'bundler',
|
|
795
|
+
strict: true,
|
|
796
|
+
esModuleInterop: true,
|
|
797
|
+
skipLibCheck: true,
|
|
798
|
+
types: ['@tx5dr/plugin-api/bridge'],
|
|
799
|
+
...jsx,
|
|
800
|
+
},
|
|
801
|
+
include: ['src'],
|
|
802
|
+
}, null, 2) + '\n';
|
|
803
|
+
}
|
|
804
|
+
function generateFrameworkEntryHtml(config) {
|
|
805
|
+
const ext = config.template === 'ui-react' ? 'tsx' : 'ts';
|
|
806
|
+
return `<!DOCTYPE html>
|
|
807
|
+
<html>
|
|
808
|
+
<head>
|
|
809
|
+
<meta charset="UTF-8">
|
|
810
|
+
</head>
|
|
811
|
+
<body>
|
|
812
|
+
<div id="app"></div>
|
|
813
|
+
<script type="module" src="./src/main.${ext}"></script>
|
|
814
|
+
</body>
|
|
815
|
+
</html>
|
|
816
|
+
`;
|
|
817
|
+
}
|
|
818
|
+
function generateReactMain() {
|
|
819
|
+
return `import React from 'react';
|
|
820
|
+
import { createRoot } from 'react-dom/client';
|
|
821
|
+
import { App } from './App';
|
|
822
|
+
|
|
823
|
+
const root = createRoot(document.getElementById('app')!);
|
|
824
|
+
root.render(<App />);
|
|
825
|
+
|
|
826
|
+
// Auto-resize iframe to fit content
|
|
827
|
+
const observer = new ResizeObserver(() => {
|
|
828
|
+
const h = document.body.scrollHeight;
|
|
829
|
+
if (h > 0) tx5dr.resize(h);
|
|
830
|
+
});
|
|
831
|
+
observer.observe(document.body);
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
function generateReactApp(config) {
|
|
835
|
+
return `import React, { useCallback, useEffect, useState } from 'react';
|
|
836
|
+
import './App.css';
|
|
837
|
+
|
|
838
|
+
export function App() {
|
|
839
|
+
const [message, setMessage] = useState('');
|
|
840
|
+
const [saved, setSaved] = useState('');
|
|
841
|
+
const [status, setStatus] = useState('');
|
|
842
|
+
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
tx5dr.invoke('getSettings').then((settings: any) => {
|
|
845
|
+
if (settings?.message) {
|
|
846
|
+
setMessage(settings.message);
|
|
847
|
+
setSaved(settings.message);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// Listen for push updates from the server
|
|
852
|
+
const handleUpdate = (data: any) => {
|
|
853
|
+
if (data?.message) {
|
|
854
|
+
setMessage(data.message);
|
|
855
|
+
setSaved(data.message);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
tx5dr.onPush('settingsUpdated', handleUpdate);
|
|
859
|
+
return () => tx5dr.offPush('settingsUpdated', handleUpdate);
|
|
860
|
+
}, []);
|
|
861
|
+
|
|
862
|
+
const handleSave = useCallback(async () => {
|
|
863
|
+
if (!message.trim()) return;
|
|
864
|
+
setStatus('Saving...');
|
|
865
|
+
try {
|
|
866
|
+
await tx5dr.invoke('saveSettings', { message });
|
|
867
|
+
setSaved(message);
|
|
868
|
+
setStatus('Saved!');
|
|
869
|
+
setTimeout(() => setStatus(''), 2000);
|
|
870
|
+
} catch (err: any) {
|
|
871
|
+
setStatus('Error: ' + err.message);
|
|
872
|
+
}
|
|
873
|
+
}, [message]);
|
|
874
|
+
|
|
875
|
+
return (
|
|
876
|
+
<div className="container">
|
|
877
|
+
<h2 className="title">${config.name} Settings</h2>
|
|
878
|
+
|
|
879
|
+
<div className="section">
|
|
880
|
+
<label className="label">Message</label>
|
|
881
|
+
<input
|
|
882
|
+
className="input"
|
|
883
|
+
value={message}
|
|
884
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
885
|
+
placeholder="Enter a message"
|
|
886
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
|
887
|
+
/>
|
|
888
|
+
</div>
|
|
889
|
+
|
|
890
|
+
<div className="section">
|
|
891
|
+
<button className="btn btn-primary" onClick={handleSave}>
|
|
892
|
+
Save
|
|
893
|
+
</button>
|
|
894
|
+
{status && <span className="status">{status}</span>}
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
{saved && (
|
|
898
|
+
<div className="section">
|
|
899
|
+
<p className="text-secondary">Current: {saved}</p>
|
|
900
|
+
</div>
|
|
901
|
+
)}
|
|
902
|
+
</div>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
`;
|
|
906
|
+
}
|
|
907
|
+
function generateVueMain() {
|
|
908
|
+
return `import { createApp } from 'vue';
|
|
909
|
+
import App from './App.vue';
|
|
910
|
+
|
|
911
|
+
createApp(App).mount('#app');
|
|
912
|
+
|
|
913
|
+
// Auto-resize iframe to fit content
|
|
914
|
+
const observer = new ResizeObserver(() => {
|
|
915
|
+
const h = document.body.scrollHeight;
|
|
916
|
+
if (h > 0) tx5dr.resize(h);
|
|
917
|
+
});
|
|
918
|
+
observer.observe(document.body);
|
|
919
|
+
`;
|
|
920
|
+
}
|
|
921
|
+
function generateVueApp(config) {
|
|
922
|
+
return `<script setup lang="ts">
|
|
923
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
924
|
+
import './App.css';
|
|
925
|
+
|
|
926
|
+
const message = ref('');
|
|
927
|
+
const saved = ref('');
|
|
928
|
+
const status = ref('');
|
|
929
|
+
|
|
930
|
+
onMounted(async () => {
|
|
931
|
+
const settings = await tx5dr.invoke('getSettings') as any;
|
|
932
|
+
if (settings?.message) {
|
|
933
|
+
message.value = settings.message;
|
|
934
|
+
saved.value = settings.message;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
tx5dr.onPush('settingsUpdated', handleUpdate);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
onUnmounted(() => {
|
|
941
|
+
tx5dr.offPush('settingsUpdated', handleUpdate);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
function handleUpdate(data: any) {
|
|
945
|
+
if (data?.message) {
|
|
946
|
+
message.value = data.message;
|
|
947
|
+
saved.value = data.message;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function handleSave() {
|
|
952
|
+
if (!message.value.trim()) return;
|
|
953
|
+
status.value = 'Saving...';
|
|
954
|
+
try {
|
|
955
|
+
await tx5dr.invoke('saveSettings', { message: message.value });
|
|
956
|
+
saved.value = message.value;
|
|
957
|
+
status.value = 'Saved!';
|
|
958
|
+
setTimeout(() => { status.value = ''; }, 2000);
|
|
959
|
+
} catch (err: any) {
|
|
960
|
+
status.value = 'Error: ' + err.message;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
</script>
|
|
964
|
+
|
|
965
|
+
<template>
|
|
966
|
+
<div class="container">
|
|
967
|
+
<h2 class="title">${config.name} Settings</h2>
|
|
968
|
+
|
|
969
|
+
<div class="section">
|
|
970
|
+
<label class="label">Message</label>
|
|
971
|
+
<input
|
|
972
|
+
class="input"
|
|
973
|
+
v-model="message"
|
|
974
|
+
placeholder="Enter a message"
|
|
975
|
+
@keydown.enter="handleSave"
|
|
976
|
+
/>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
<div class="section">
|
|
980
|
+
<button class="btn btn-primary" @click="handleSave">Save</button>
|
|
981
|
+
<span v-if="status" class="status">{{ status }}</span>
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
<div v-if="saved" class="section">
|
|
985
|
+
<p class="text-secondary">Current: {{ saved }}</p>
|
|
986
|
+
</div>
|
|
987
|
+
</div>
|
|
988
|
+
</template>
|
|
989
|
+
`;
|
|
990
|
+
}
|
|
991
|
+
// ===== Link script =====
|
|
992
|
+
function generateLinkScript(pluginName) {
|
|
993
|
+
return `#!/usr/bin/env node
|
|
994
|
+
// Symlink this plugin's dist/ to the TX-5DR plugins directory.
|
|
995
|
+
// Usage: node scripts/link.mjs (create link)
|
|
996
|
+
// node scripts/link.mjs --unlink (remove link)
|
|
997
|
+
|
|
998
|
+
import { symlinkSync, unlinkSync, existsSync, mkdirSync, writeFileSync, lstatSync } from 'node:fs';
|
|
999
|
+
import { join, resolve } from 'node:path';
|
|
1000
|
+
import { homedir, platform } from 'node:os';
|
|
1001
|
+
|
|
1002
|
+
const PLUGIN_NAME = '${pluginName}';
|
|
1003
|
+
|
|
1004
|
+
function getDataDir() {
|
|
1005
|
+
if (process.env.TX5DR_DATA_DIR) return process.env.TX5DR_DATA_DIR;
|
|
1006
|
+
const home = homedir();
|
|
1007
|
+
switch (platform()) {
|
|
1008
|
+
case 'darwin':
|
|
1009
|
+
return join(home, 'Library', 'Application Support', 'TX-5DR');
|
|
1010
|
+
case 'win32':
|
|
1011
|
+
return join(process.env.LOCALAPPDATA || join(home, 'AppData', 'Local'), 'TX-5DR');
|
|
1012
|
+
default:
|
|
1013
|
+
return join(process.env.XDG_DATA_HOME || join(home, '.local', 'share'), 'TX-5DR');
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const unlink = process.argv.includes('--unlink');
|
|
1018
|
+
const pluginsDir = join(getDataDir(), 'plugins');
|
|
1019
|
+
const target = resolve('dist');
|
|
1020
|
+
const linkPath = join(pluginsDir, PLUGIN_NAME);
|
|
1021
|
+
|
|
1022
|
+
if (unlink) {
|
|
1023
|
+
if (existsSync(linkPath)) {
|
|
1024
|
+
unlinkSync(linkPath);
|
|
1025
|
+
console.log('Unlinked: ' + linkPath);
|
|
1026
|
+
} else {
|
|
1027
|
+
console.log('No link found at: ' + linkPath);
|
|
1028
|
+
}
|
|
1029
|
+
process.exit(0);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (!existsSync(target)) {
|
|
1033
|
+
console.error('Error: dist/ not found. Run "npm run build" first.');
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (existsSync(linkPath)) {
|
|
1038
|
+
try {
|
|
1039
|
+
const stat = lstatSync(linkPath);
|
|
1040
|
+
if (stat.isSymbolicLink()) {
|
|
1041
|
+
console.log('Already linked: ' + linkPath + ' -> ' + target);
|
|
1042
|
+
process.exit(0);
|
|
1043
|
+
}
|
|
1044
|
+
} catch { /* continue */ }
|
|
1045
|
+
console.error('Error: ' + linkPath + ' already exists and is not a symlink.');
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
1050
|
+
symlinkSync(target, linkPath, 'junction');
|
|
1051
|
+
|
|
1052
|
+
// Create .hotreload marker so the TX-5DR dev server auto-reloads on changes.
|
|
1053
|
+
const hotreloadPath = join(target, '.hotreload');
|
|
1054
|
+
if (!existsSync(hotreloadPath)) {
|
|
1055
|
+
writeFileSync(hotreloadPath, '', 'utf-8');
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
console.log('Linked: ' + linkPath + ' -> ' + target);
|
|
1059
|
+
console.log('Created .hotreload marker for dev auto-reload.');
|
|
1060
|
+
`;
|
|
1061
|
+
}
|
|
403
1062
|
// ===== File generation =====
|
|
404
1063
|
function generateFiles(config) {
|
|
405
1064
|
const files = new Map();
|
|
@@ -410,6 +1069,9 @@ function generateFiles(config) {
|
|
|
410
1069
|
if (config.type === 'strategy') {
|
|
411
1070
|
files.set('src/index.ts', generateTsStrategyPlugin(config));
|
|
412
1071
|
}
|
|
1072
|
+
else if (hasUI(config)) {
|
|
1073
|
+
files.set('src/index.ts', generateTsUtilityPluginWithUI(config));
|
|
1074
|
+
}
|
|
413
1075
|
else {
|
|
414
1076
|
files.set('src/index.ts', generateTsUtilityPlugin(config));
|
|
415
1077
|
}
|
|
@@ -418,7 +1080,41 @@ function generateFiles(config) {
|
|
|
418
1080
|
files.set('src/__tests__/plugin.test.ts', generateTsTest(config));
|
|
419
1081
|
}
|
|
420
1082
|
else {
|
|
421
|
-
|
|
1083
|
+
if (hasUI(config)) {
|
|
1084
|
+
files.set('index.js', generateJsUtilityPluginWithUI(config));
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
files.set('index.js', generateJsUtilityPlugin(config));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Vanilla UI template
|
|
1091
|
+
if (config.template === 'ui-vanilla') {
|
|
1092
|
+
files.set('ui/jsconfig.json', generateVanillaJsConfig());
|
|
1093
|
+
files.set('ui/settings.html', generateVanillaUIHtml(config));
|
|
1094
|
+
files.set('ui/settings.css', generateUICss());
|
|
1095
|
+
files.set('ui/settings.js', generateVanillaUIJs());
|
|
1096
|
+
}
|
|
1097
|
+
// React UI template
|
|
1098
|
+
if (config.template === 'ui-react') {
|
|
1099
|
+
files.set('ui/vite.config.ts', generateViteConfig(config));
|
|
1100
|
+
files.set('ui/tsconfig.json', generateUITsConfig(config));
|
|
1101
|
+
files.set('ui/settings.html', generateFrameworkEntryHtml(config));
|
|
1102
|
+
files.set('ui/src/main.tsx', generateReactMain());
|
|
1103
|
+
files.set('ui/src/App.tsx', generateReactApp(config));
|
|
1104
|
+
files.set('ui/src/App.css', generateUICss());
|
|
1105
|
+
}
|
|
1106
|
+
// Vue UI template
|
|
1107
|
+
if (config.template === 'ui-vue') {
|
|
1108
|
+
files.set('ui/vite.config.ts', generateViteConfig(config));
|
|
1109
|
+
files.set('ui/tsconfig.json', generateUITsConfig(config));
|
|
1110
|
+
files.set('ui/settings.html', generateFrameworkEntryHtml(config));
|
|
1111
|
+
files.set('ui/src/main.ts', generateVueMain());
|
|
1112
|
+
files.set('ui/src/App.vue', generateVueApp(config));
|
|
1113
|
+
files.set('ui/src/App.css', generateUICss());
|
|
1114
|
+
}
|
|
1115
|
+
// Link script for all templates with TS
|
|
1116
|
+
if (config.lang === 'ts') {
|
|
1117
|
+
files.set('scripts/link.mjs', generateLinkScript(config.name));
|
|
422
1118
|
}
|
|
423
1119
|
return files;
|
|
424
1120
|
}
|
|
@@ -439,6 +1135,7 @@ async function main() {
|
|
|
439
1135
|
console.log(`Creating TX-5DR plugin: ${config.name}`);
|
|
440
1136
|
console.log(` Type: ${config.type}`);
|
|
441
1137
|
console.log(` Language: ${config.lang === 'ts' ? 'TypeScript' : 'JavaScript'}`);
|
|
1138
|
+
console.log(` Template: ${config.template}`);
|
|
442
1139
|
console.log(` Directory: ${targetDir}`);
|
|
443
1140
|
console.log();
|
|
444
1141
|
const files = generateFiles(config);
|
|
@@ -457,7 +1154,24 @@ async function main() {
|
|
|
457
1154
|
console.log(' npm test');
|
|
458
1155
|
}
|
|
459
1156
|
console.log();
|
|
460
|
-
|
|
1157
|
+
if (config.lang === 'ts') {
|
|
1158
|
+
console.log('Link to TX-5DR for development:');
|
|
1159
|
+
console.log(' npm run link');
|
|
1160
|
+
console.log();
|
|
1161
|
+
}
|
|
1162
|
+
if (hasVite(config)) {
|
|
1163
|
+
console.log('UI development (run in two terminals):');
|
|
1164
|
+
console.log(' npm run dev:server # Watch server-side TypeScript');
|
|
1165
|
+
console.log(' npm run dev:ui # Watch UI with Vite');
|
|
1166
|
+
}
|
|
1167
|
+
else if (hasUI(config)) {
|
|
1168
|
+
console.log('UI files are in the ui/ directory.');
|
|
1169
|
+
console.log('Edit HTML/JS/CSS directly — the Bridge SDK is auto-injected by the host.');
|
|
1170
|
+
}
|
|
1171
|
+
if (hasUI(config) || hasVite(config)) {
|
|
1172
|
+
console.log();
|
|
1173
|
+
console.log('Docs: https://github.com/boybook/tx-5dr/blob/main/docs/plugin-system.md');
|
|
1174
|
+
}
|
|
461
1175
|
}
|
|
462
1176
|
main().catch((err) => {
|
|
463
1177
|
console.error(err);
|