@webmate-studio/cli 0.1.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/bin/wm.js +102 -0
- package/package.json +37 -0
- package/src/commands/build.js +113 -0
- package/src/commands/dev.js +29 -0
- package/src/commands/generate.js +579 -0
- package/src/commands/info.js +49 -0
- package/src/commands/init.js +452 -0
- package/src/commands/login.js +193 -0
- package/src/commands/logout.js +20 -0
- package/src/commands/prop.js +286 -0
- package/src/commands/push.js +275 -0
- package/src/commands/switch.js +131 -0
- package/src/index.js +4 -0
- package/src/templates/islands/alpine.js +44 -0
- package/src/templates/islands/lit.js +90 -0
- package/src/templates/islands/preact.jsx +52 -0
- package/src/templates/islands/react.jsx +50 -0
- package/src/templates/islands/svelte-component.svelte +36 -0
- package/src/templates/islands/svelte.js +31 -0
- package/src/templates/islands/vanilla.js +71 -0
- package/src/templates/islands/vue.js +65 -0
- package/src/utils/auth.js +125 -0
- package/src/utils/bundler.js +163 -0
- package/src/utils/config.js +103 -0
- package/src/utils/semver.js +76 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { loadConfig } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a new component or island
|
|
9
|
+
*/
|
|
10
|
+
export async function generate(type, name, options = {}) {
|
|
11
|
+
// Handle --list-templates flag
|
|
12
|
+
if (options.listTemplates) {
|
|
13
|
+
listAvailableTemplates();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(pc.cyan('\nšØ Webmate Component Generator\n'));
|
|
18
|
+
|
|
19
|
+
// Resolve type aliases
|
|
20
|
+
const typeAliases = {
|
|
21
|
+
c: 'component',
|
|
22
|
+
comp: 'component'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (type && typeAliases[type]) {
|
|
26
|
+
type = typeAliases[type];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only component type supported (islands are part of components now)
|
|
30
|
+
if (!type) {
|
|
31
|
+
type = 'component';
|
|
32
|
+
} else if (type !== 'component') {
|
|
33
|
+
console.log(pc.red(`\nā Unknown type: ${type}\n`));
|
|
34
|
+
console.log(pc.dim('Valid type: component (c, comp)\n'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create component
|
|
39
|
+
await createComponent(name, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new HTML component
|
|
44
|
+
*/
|
|
45
|
+
async function createComponent(name, options) {
|
|
46
|
+
const config = await loadConfig();
|
|
47
|
+
const componentsDir = config.components.path;
|
|
48
|
+
|
|
49
|
+
// Ask for component name if not provided
|
|
50
|
+
if (!name) {
|
|
51
|
+
name = await input({
|
|
52
|
+
message: 'Component name (PascalCase):',
|
|
53
|
+
validate: (value) => {
|
|
54
|
+
if (!value) return 'Component name is required';
|
|
55
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(value)) {
|
|
56
|
+
return 'Component name must be in PascalCase (e.g., MyComponent)';
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate component name
|
|
64
|
+
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
65
|
+
console.log(
|
|
66
|
+
pc.red(
|
|
67
|
+
`\nā Invalid component name: ${name}\nComponent names must be in PascalCase (e.g., MyComponent)\n`
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ALWAYS create directory-based components (consistent structure)
|
|
74
|
+
const componentPath = path.join(componentsDir, name);
|
|
75
|
+
|
|
76
|
+
// Check if component already exists
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(componentPath);
|
|
79
|
+
console.log(pc.red(`\nā Component already exists: ${componentPath}\n`));
|
|
80
|
+
return;
|
|
81
|
+
} catch {
|
|
82
|
+
// Component doesn't exist, continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Skip wizard if requested
|
|
86
|
+
if (options.skipWizard) {
|
|
87
|
+
// Use flags if provided, otherwise defaults
|
|
88
|
+
const islandFramework = options.template || (options.islands ? 'vanilla' : null);
|
|
89
|
+
await generateDirectoryComponent(componentPath, name, options, {
|
|
90
|
+
description: `${name} component`,
|
|
91
|
+
category: 'General',
|
|
92
|
+
icon: 'mdi:puzzle',
|
|
93
|
+
props: {},
|
|
94
|
+
islandFramework
|
|
95
|
+
});
|
|
96
|
+
console.log(pc.green(`\nā Component created: ${componentPath}\n`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Interactive wizard
|
|
101
|
+
console.log(pc.dim('\nLet\'s configure your component:\n'));
|
|
102
|
+
|
|
103
|
+
const description = await input({
|
|
104
|
+
message: 'Component description:',
|
|
105
|
+
default: `${name} component`
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const category = await select({
|
|
109
|
+
message: 'Component category:',
|
|
110
|
+
choices: [
|
|
111
|
+
{ name: 'Layout', value: 'Layout' },
|
|
112
|
+
{ name: 'Navigation', value: 'Navigation' },
|
|
113
|
+
{ name: 'Forms', value: 'Forms' },
|
|
114
|
+
{ name: 'Content', value: 'Content' },
|
|
115
|
+
{ name: 'Media', value: 'Media' },
|
|
116
|
+
{ name: 'General', value: 'General' }
|
|
117
|
+
],
|
|
118
|
+
default: 'General'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const icon = await input({
|
|
122
|
+
message: 'Iconify icon (e.g., mdi:puzzle):',
|
|
123
|
+
default: 'mdi:puzzle'
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Ask if component should have an interactive island
|
|
127
|
+
const addIsland = await confirm({
|
|
128
|
+
message: 'Add interactive island?',
|
|
129
|
+
default: false
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
let islandFramework = null;
|
|
133
|
+
if (addIsland) {
|
|
134
|
+
islandFramework = await select({
|
|
135
|
+
message: 'Island framework:',
|
|
136
|
+
choices: [
|
|
137
|
+
{ name: 'Vanilla JS (0kb runtime)', value: 'vanilla' },
|
|
138
|
+
{ name: 'Svelte (Compiled, ~2kb)', value: 'svelte' },
|
|
139
|
+
{ name: 'Preact (React-like, ~4kb)', value: 'preact' },
|
|
140
|
+
{ name: 'Lit (Web Components, ~8kb)', value: 'lit' },
|
|
141
|
+
{ name: 'Alpine.js (HTML-first, ~15kb)', value: 'alpine' },
|
|
142
|
+
{ name: 'Vue (~35kb)', value: 'vue' },
|
|
143
|
+
{ name: 'React (~45kb)', value: 'react' }
|
|
144
|
+
],
|
|
145
|
+
default: 'vanilla'
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ask for props
|
|
150
|
+
const props = {};
|
|
151
|
+
let addMoreProps = true;
|
|
152
|
+
|
|
153
|
+
console.log(pc.dim('\nAdd component properties (Enter = skip, adds default "title" prop):\n'));
|
|
154
|
+
|
|
155
|
+
while (addMoreProps) {
|
|
156
|
+
const addProp = await confirm({
|
|
157
|
+
message: 'Add a property?',
|
|
158
|
+
default: false
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!addProp) {
|
|
162
|
+
addMoreProps = false;
|
|
163
|
+
// Add default title prop if no props were added
|
|
164
|
+
if (Object.keys(props).length === 0) {
|
|
165
|
+
props.title = {
|
|
166
|
+
type: 'string',
|
|
167
|
+
label: 'Title',
|
|
168
|
+
default: 'Beispiel Titel',
|
|
169
|
+
description: 'Titel der Komponente'
|
|
170
|
+
};
|
|
171
|
+
console.log(pc.dim(' ⹠Default "title" prop hinzugefügt\n'));
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const propName = await input({
|
|
177
|
+
message: 'Property name (camelCase):',
|
|
178
|
+
validate: (value) => {
|
|
179
|
+
if (!value) return 'Property name is required';
|
|
180
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(value)) {
|
|
181
|
+
return 'Property name must be in camelCase (e.g., myProperty)';
|
|
182
|
+
}
|
|
183
|
+
if (props[value]) return 'Property already exists';
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const propType = await select({
|
|
189
|
+
message: 'Property type:',
|
|
190
|
+
choices: [
|
|
191
|
+
{ name: 'String (text input)', value: 'string' },
|
|
192
|
+
{ name: 'Boolean (checkbox)', value: 'boolean' },
|
|
193
|
+
{ name: 'Number (number input)', value: 'number' },
|
|
194
|
+
{ name: 'Select (dropdown)', value: 'select' },
|
|
195
|
+
{ name: 'Color (color picker)', value: 'color' },
|
|
196
|
+
{ name: 'Image (image upload)', value: 'image' },
|
|
197
|
+
{ name: 'Rich Text (WYSIWYG editor)', value: 'richtext' }
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const propLabel = await input({
|
|
202
|
+
message: 'Property label (display name):',
|
|
203
|
+
default: propName.charAt(0).toUpperCase() + propName.slice(1).replace(/([A-Z])/g, ' $1')
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const propDescription = await input({
|
|
207
|
+
message: 'Property description (optional):',
|
|
208
|
+
default: ''
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let propDefault = '';
|
|
212
|
+
let propOptions = null;
|
|
213
|
+
let propMin = null;
|
|
214
|
+
let propMax = null;
|
|
215
|
+
|
|
216
|
+
// Type-specific configuration
|
|
217
|
+
if (propType === 'string') {
|
|
218
|
+
propDefault = await input({
|
|
219
|
+
message: 'Default value:',
|
|
220
|
+
default: ''
|
|
221
|
+
});
|
|
222
|
+
} else if (propType === 'boolean') {
|
|
223
|
+
const defaultBool = await confirm({
|
|
224
|
+
message: 'Default value:',
|
|
225
|
+
default: false
|
|
226
|
+
});
|
|
227
|
+
propDefault = defaultBool;
|
|
228
|
+
} else if (propType === 'number') {
|
|
229
|
+
propDefault = await input({
|
|
230
|
+
message: 'Default value:',
|
|
231
|
+
default: '0',
|
|
232
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
233
|
+
});
|
|
234
|
+
propDefault = Number(propDefault);
|
|
235
|
+
|
|
236
|
+
const hasMin = await confirm({
|
|
237
|
+
message: 'Set minimum value?',
|
|
238
|
+
default: false
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (hasMin) {
|
|
242
|
+
propMin = await input({
|
|
243
|
+
message: 'Minimum value:',
|
|
244
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
245
|
+
});
|
|
246
|
+
propMin = Number(propMin);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasMax = await confirm({
|
|
250
|
+
message: 'Set maximum value?',
|
|
251
|
+
default: false
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (hasMax) {
|
|
255
|
+
propMax = await input({
|
|
256
|
+
message: 'Maximum value:',
|
|
257
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
258
|
+
});
|
|
259
|
+
propMax = Number(propMax);
|
|
260
|
+
}
|
|
261
|
+
} else if (propType === 'select') {
|
|
262
|
+
const optionsInput = await input({
|
|
263
|
+
message: 'Options (comma-separated):',
|
|
264
|
+
validate: (v) => (v ? true : 'At least one option required')
|
|
265
|
+
});
|
|
266
|
+
propOptions = optionsInput.split(',').map((o) => o.trim());
|
|
267
|
+
propDefault = propOptions[0];
|
|
268
|
+
} else if (propType === 'color') {
|
|
269
|
+
propDefault = await input({
|
|
270
|
+
message: 'Default color (hex):',
|
|
271
|
+
default: '#000000',
|
|
272
|
+
validate: (v) => (/^#[0-9A-Fa-f]{6}$/.test(v) ? true : 'Must be hex color (e.g., #ff0000)')
|
|
273
|
+
});
|
|
274
|
+
} else if (propType === 'richtext') {
|
|
275
|
+
propDefault = '<p></p>';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Build prop definition
|
|
279
|
+
props[propName] = {
|
|
280
|
+
type: propType,
|
|
281
|
+
label: propLabel,
|
|
282
|
+
default: propDefault
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (propDescription) props[propName].description = propDescription;
|
|
286
|
+
if (propOptions) props[propName].options = propOptions;
|
|
287
|
+
if (propMin !== null) props[propName].min = propMin;
|
|
288
|
+
if (propMax !== null) props[propName].max = propMax;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Generate component directory
|
|
292
|
+
await generateDirectoryComponent(componentPath, name, options, {
|
|
293
|
+
description,
|
|
294
|
+
category,
|
|
295
|
+
icon,
|
|
296
|
+
props,
|
|
297
|
+
islandFramework
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
console.log(pc.green(`\nā Component created: ${componentPath}`));
|
|
301
|
+
console.log(pc.dim(`\nNext steps:`));
|
|
302
|
+
console.log(pc.dim(` 1. Edit component.html in ${name}/ directory`));
|
|
303
|
+
if (islandFramework) {
|
|
304
|
+
console.log(pc.dim(` 2. Add island logic in islands/ directory`));
|
|
305
|
+
console.log(pc.dim(` 3. Test with: wm dev`));
|
|
306
|
+
} else {
|
|
307
|
+
console.log(pc.dim(` 2. Test with: wm dev`));
|
|
308
|
+
}
|
|
309
|
+
console.log('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate directory-based component with component.json
|
|
314
|
+
*/
|
|
315
|
+
async function generateDirectoryComponent(componentDir, name, options, config = {}) {
|
|
316
|
+
const {
|
|
317
|
+
description = `${name} component`,
|
|
318
|
+
category = 'General',
|
|
319
|
+
props = {},
|
|
320
|
+
islandFramework = null
|
|
321
|
+
} = config;
|
|
322
|
+
|
|
323
|
+
// Create component directory
|
|
324
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
325
|
+
|
|
326
|
+
// Create component.json (metadata + props + islands config)
|
|
327
|
+
const componentJson = {
|
|
328
|
+
name,
|
|
329
|
+
version: '1.0.0',
|
|
330
|
+
description,
|
|
331
|
+
category
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Add props if any
|
|
335
|
+
if (Object.keys(props).length > 0) {
|
|
336
|
+
componentJson.props = props;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Add islands config if needed
|
|
340
|
+
if (islandFramework) {
|
|
341
|
+
// Use .jsx extension for React/Preact, .js for others
|
|
342
|
+
const fileExtension = (islandFramework === 'react' || islandFramework === 'preact') ? '.jsx' : '.js';
|
|
343
|
+
|
|
344
|
+
componentJson.islands = [
|
|
345
|
+
{
|
|
346
|
+
name: toKebabCase(name),
|
|
347
|
+
file: `islands/${toKebabCase(name)}${fileExtension}`,
|
|
348
|
+
framework: islandFramework,
|
|
349
|
+
props: {}
|
|
350
|
+
}
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await fs.writeFile(
|
|
355
|
+
path.join(componentDir, 'component.json'),
|
|
356
|
+
JSON.stringify(componentJson, null, 2),
|
|
357
|
+
'utf-8'
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Generate component.html (just HTML, no script blocks)
|
|
361
|
+
let exampleHtml = '';
|
|
362
|
+
|
|
363
|
+
if (islandFramework) {
|
|
364
|
+
// Island component: demonstriere title prop
|
|
365
|
+
const hasTitle = props.title;
|
|
366
|
+
const islandPropsJson = hasTitle ? `{"title": "{{title}}"}` : '{}';
|
|
367
|
+
|
|
368
|
+
exampleHtml = `\t<div
|
|
369
|
+
data-island="${toKebabCase(name)}"
|
|
370
|
+
data-island-props='${islandPropsJson}'
|
|
371
|
+
class="p-6 bg-white rounded-lg border border-gray-200"
|
|
372
|
+
>
|
|
373
|
+
<!-- Island wird hier initialisiert -->
|
|
374
|
+
</div>`;
|
|
375
|
+
} else {
|
|
376
|
+
// Normale component: zeige title an
|
|
377
|
+
const hasTitle = props.title;
|
|
378
|
+
if (hasTitle) {
|
|
379
|
+
exampleHtml = `\t<div class="p-6 bg-white rounded-lg border border-gray-200">
|
|
380
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-4">{{title}}</h2>
|
|
381
|
+
<p class="text-gray-600">Dein Component-Inhalt hier</p>
|
|
382
|
+
</div>`;
|
|
383
|
+
} else {
|
|
384
|
+
exampleHtml = `\t<div class="p-6 bg-white rounded-lg border border-gray-200">
|
|
385
|
+
<p class="text-gray-600">Dein Component-Inhalt hier</p>
|
|
386
|
+
</div>`;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const componentHtml = `<!-- ${name} Component -->
|
|
391
|
+
<div class="${toKebabCase(name)}">
|
|
392
|
+
${exampleHtml}
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<style>
|
|
396
|
+
/* Custom CSS falls benƶtigt */
|
|
397
|
+
.${toKebabCase(name)} {
|
|
398
|
+
/* Beispiel für Custom Styles */
|
|
399
|
+
}
|
|
400
|
+
</style>
|
|
401
|
+
`;
|
|
402
|
+
|
|
403
|
+
await fs.writeFile(path.join(componentDir, 'component.html'), componentHtml, 'utf-8');
|
|
404
|
+
|
|
405
|
+
// Create islands directory if needed
|
|
406
|
+
if (islandFramework) {
|
|
407
|
+
const islandsDir = path.join(componentDir, 'islands');
|
|
408
|
+
await fs.mkdir(islandsDir, { recursive: true });
|
|
409
|
+
|
|
410
|
+
// Determine file extension based on framework
|
|
411
|
+
const fileExtension = (islandFramework === 'react' || islandFramework === 'preact') ? '.jsx' : '.js';
|
|
412
|
+
const templateExtension = (islandFramework === 'react' || islandFramework === 'preact') ? '.jsx' : '.js';
|
|
413
|
+
|
|
414
|
+
// Copy template file
|
|
415
|
+
const templateFile =
|
|
416
|
+
islandFramework === 'svelte'
|
|
417
|
+
? path.join(
|
|
418
|
+
import.meta.dirname,
|
|
419
|
+
'../templates/islands/svelte.js'
|
|
420
|
+
)
|
|
421
|
+
: path.join(
|
|
422
|
+
import.meta.dirname,
|
|
423
|
+
`../templates/islands/${islandFramework}${templateExtension}`
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
let templateContent = await fs.readFile(templateFile, 'utf-8');
|
|
427
|
+
|
|
428
|
+
// Replace placeholders
|
|
429
|
+
templateContent = templateContent
|
|
430
|
+
.replace(/\{\{NAME\}\}/g, name)
|
|
431
|
+
.replace(/\{\{KEBAB_NAME\}\}/g, toKebabCase(name));
|
|
432
|
+
|
|
433
|
+
await fs.writeFile(
|
|
434
|
+
path.join(islandsDir, `${toKebabCase(name)}${fileExtension}`),
|
|
435
|
+
templateContent,
|
|
436
|
+
'utf-8'
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// If Svelte, also copy the .svelte component
|
|
440
|
+
if (islandFramework === 'svelte') {
|
|
441
|
+
const svelteComponentTemplate = path.join(
|
|
442
|
+
import.meta.dirname,
|
|
443
|
+
'../templates/islands/svelte-component.svelte'
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
let svelteContent = await fs.readFile(svelteComponentTemplate, 'utf-8');
|
|
447
|
+
svelteContent = svelteContent
|
|
448
|
+
.replace(/\{\{NAME\}\}/g, name)
|
|
449
|
+
.replace(/\{\{KEBAB_NAME\}\}/g, toKebabCase(name));
|
|
450
|
+
|
|
451
|
+
await fs.writeFile(path.join(islandsDir, `${name}.svelte`), svelteContent, 'utf-8');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Create assets directory if needed
|
|
456
|
+
if (options.assets) {
|
|
457
|
+
await fs.mkdir(path.join(componentDir, 'assets'), { recursive: true });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Convert PascalCase to kebab-case
|
|
463
|
+
*/
|
|
464
|
+
function toKebabCase(str) {
|
|
465
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* List available island templates
|
|
470
|
+
*/
|
|
471
|
+
function listAvailableTemplates() {
|
|
472
|
+
console.log(pc.cyan('\nš Available Island Templates\n'));
|
|
473
|
+
|
|
474
|
+
const templates = [
|
|
475
|
+
{
|
|
476
|
+
name: 'vanilla',
|
|
477
|
+
runtime: '0kb',
|
|
478
|
+
compiled: 'ā
',
|
|
479
|
+
description: 'Pure JavaScript - No dependencies'
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: 'lit',
|
|
483
|
+
runtime: '~8kb',
|
|
484
|
+
compiled: 'ā',
|
|
485
|
+
description: 'Web Components - Standard-based'
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'preact',
|
|
489
|
+
runtime: '~4kb',
|
|
490
|
+
compiled: 'ā',
|
|
491
|
+
description: 'React-like - Smallest alternative'
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: 'alpine',
|
|
495
|
+
runtime: '~15kb',
|
|
496
|
+
compiled: 'ā',
|
|
497
|
+
description: 'HTML-first - Like Vue directives'
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'react',
|
|
501
|
+
runtime: '~45kb',
|
|
502
|
+
compiled: 'ā',
|
|
503
|
+
description: 'React - Largest ecosystem'
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'svelte',
|
|
507
|
+
runtime: '~2kb*',
|
|
508
|
+
compiled: 'ā
',
|
|
509
|
+
description: 'Svelte - Compiles to Vanilla JS'
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
console.log(
|
|
514
|
+
pc.dim(
|
|
515
|
+
'āāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā¬āāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'
|
|
516
|
+
)
|
|
517
|
+
);
|
|
518
|
+
console.log(
|
|
519
|
+
pc.dim('ā') +
|
|
520
|
+
pc.bold(' Template ') +
|
|
521
|
+
pc.dim('ā') +
|
|
522
|
+
pc.bold(' Runtime Size ') +
|
|
523
|
+
pc.dim('ā') +
|
|
524
|
+
pc.bold(' Compiled? ') +
|
|
525
|
+
pc.dim('ā') +
|
|
526
|
+
pc.bold(' Description ') +
|
|
527
|
+
pc.dim('ā')
|
|
528
|
+
);
|
|
529
|
+
console.log(
|
|
530
|
+
pc.dim(
|
|
531
|
+
'āāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāā¼āāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤'
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
templates.forEach((template) => {
|
|
536
|
+
const nameCol = template.name.padEnd(11);
|
|
537
|
+
const runtimeCol = template.runtime.padEnd(12);
|
|
538
|
+
const compiledCol = template.compiled.padEnd(9);
|
|
539
|
+
const descCol = template.description.padEnd(35);
|
|
540
|
+
|
|
541
|
+
console.log(
|
|
542
|
+
pc.dim('ā ') +
|
|
543
|
+
pc.cyan(nameCol) +
|
|
544
|
+
pc.dim(' ā ') +
|
|
545
|
+
pc.green(runtimeCol) +
|
|
546
|
+
pc.dim(' ā ') +
|
|
547
|
+
compiledCol +
|
|
548
|
+
pc.dim(' ā ') +
|
|
549
|
+
descCol +
|
|
550
|
+
pc.dim(' ā')
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
console.log(
|
|
555
|
+
pc.dim(
|
|
556
|
+
'āāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā“āāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
console.log(pc.dim('\n*Svelte runtime is minimal because most logic compiles at build-time\n'));
|
|
561
|
+
|
|
562
|
+
console.log(pc.bold('Usage Examples:\n'));
|
|
563
|
+
console.log(pc.dim(' # Generate component with vanilla JavaScript island'));
|
|
564
|
+
console.log(pc.cyan(' wm generate component Counter --islands\n'));
|
|
565
|
+
|
|
566
|
+
console.log(pc.dim(' # Generate component with React island'));
|
|
567
|
+
console.log(pc.cyan(' wm generate component ProductList --islands --template=react\n'));
|
|
568
|
+
|
|
569
|
+
console.log(pc.dim(' # Generate component with Svelte island and assets'));
|
|
570
|
+
console.log(pc.cyan(' wm generate component ImageGallery --islands --template=svelte --assets\n'));
|
|
571
|
+
|
|
572
|
+
console.log(pc.bold('Next Steps:\n'));
|
|
573
|
+
console.log(pc.dim(' 1. Choose a template based on your needs'));
|
|
574
|
+
console.log(pc.dim(' 2. Generate component: ') + pc.cyan('wm g component Name --islands --template=<name>'));
|
|
575
|
+
console.log(pc.dim(' 3. Install dependencies: ') + pc.cyan('cd Name && npm install'));
|
|
576
|
+
console.log(pc.dim(' 4. Edit island code in islands/ directory'));
|
|
577
|
+
console.log(pc.dim(' 5. Build and test: ') + pc.cyan('wm build && wm push'));
|
|
578
|
+
console.log('');
|
|
579
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { loadAuth } from '../utils/auth.js';
|
|
2
|
+
import { logger } from '../../../core/src/index.js';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Info command - Show current project information
|
|
7
|
+
*/
|
|
8
|
+
export async function info(options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
const auth = loadAuth();
|
|
11
|
+
|
|
12
|
+
if (!auth) {
|
|
13
|
+
logger.error('Not logged in. Run ' + pc.cyan('wm login') + ' first');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
showProjectInfo(auth);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
logger.error(`Info command failed: ${error.message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Show current project information
|
|
26
|
+
*/
|
|
27
|
+
function showProjectInfo(auth) {
|
|
28
|
+
console.log();
|
|
29
|
+
logger.info(pc.bold('Current Project'));
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
// Build CMS URL from base URL and tenant subdomain
|
|
33
|
+
let cmsUrl = 'Not set';
|
|
34
|
+
if (auth.baseUrl && auth.tenant?.subdomain) {
|
|
35
|
+
try {
|
|
36
|
+
const baseUrl = new URL(auth.baseUrl);
|
|
37
|
+
const subdomain = auth.tenant.subdomain;
|
|
38
|
+
cmsUrl = `${baseUrl.protocol}//${subdomain}.cms.${baseUrl.host}`;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
cmsUrl = auth.baseUrl;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(pc.gray(' CMS URL: ') + pc.cyan(cmsUrl));
|
|
45
|
+
console.log(pc.gray(' User: ') + pc.cyan(auth.user?.email || 'Not set'));
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(pc.gray(' To switch project, run: ') + pc.cyan('wm switch'));
|
|
48
|
+
console.log();
|
|
49
|
+
}
|