@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.
@@ -0,0 +1,452 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { logger } from '../../../core/src/index.js';
4
+ import pc from 'picocolors';
5
+
6
+ /**
7
+ * Init command - create new component project
8
+ */
9
+ export async function initCommand(directory) {
10
+ const projectPath = join(process.cwd(), directory);
11
+
12
+ logger.info(`Initializing Webmate project in ${pc.cyan(projectPath)}`);
13
+
14
+ // Create directories
15
+ const dirs = [
16
+ 'components',
17
+ 'tokens',
18
+ 'styles'
19
+ ];
20
+
21
+ for (const dir of dirs) {
22
+ const dirPath = join(projectPath, dir);
23
+ if (!existsSync(dirPath)) {
24
+ mkdirSync(dirPath, { recursive: true });
25
+ logger.success(`Created ${dir}/`);
26
+ }
27
+ }
28
+
29
+ // Create wm.config.js
30
+ const configPath = join(projectPath, 'wm.config.js');
31
+ if (!existsSync(configPath)) {
32
+ const configContent = `export default {
33
+ // Component configuration
34
+ components: {
35
+ path: './components',
36
+ styles: ['./tokens/tokens.css', './styles/base.css'],
37
+ fonts: [],
38
+ islands: {
39
+ path: './islands',
40
+ framework: 'lit' // or 'vanilla'
41
+ }
42
+ },
43
+
44
+ // Preview server
45
+ preview: {
46
+ port: 5173,
47
+ theme: 'light',
48
+ viewport: {
49
+ width: 1440,
50
+ height: 900
51
+ },
52
+ backgrounds: ['#ffffff', '#f5f5f5', '#000000']
53
+ },
54
+
55
+ // Build output
56
+ output: {
57
+ dir: './dist',
58
+ format: 'esm',
59
+ minify: false
60
+ }
61
+ };
62
+ `;
63
+ writeFileSync(configPath, configContent, 'utf8');
64
+ logger.success('Created wm.config.js');
65
+ }
66
+
67
+ // Create tokens.css
68
+ const tokensPath = join(projectPath, 'tokens/tokens.css');
69
+ if (!existsSync(tokensPath)) {
70
+ const tokensContent = `/**
71
+ * Design Tokens - Custom Overrides
72
+ *
73
+ * IMPORTANT: The Webmate CMS provides base design tokens automatically.
74
+ * This file is for ADDITIONAL or OVERRIDING tokens specific to your components.
75
+ *
76
+ * CMS-provided tokens include:
77
+ * - Colors (primary, secondary, text colors)
78
+ * - Typography (font families, sizes, line heights)
79
+ * - Spacing (consistent spacing scale)
80
+ * - Border radius, shadows, transitions
81
+ *
82
+ * Use this file to:
83
+ * 1. Add component-specific tokens
84
+ * 2. Override CMS tokens for specific use cases
85
+ * 3. Define custom animations or effects
86
+ */
87
+
88
+ :root {
89
+ /* Example: Component-specific color variations */
90
+ /* --my-component-accent: #705ef0; */
91
+
92
+ /* Example: Custom spacing for specific layouts */
93
+ /* --my-grid-gap: 2rem; */
94
+
95
+ /* Example: Component-specific animation */
96
+ /* --my-transition-fast: 150ms ease-in-out; */
97
+ }
98
+ `;
99
+ writeFileSync(tokensPath, tokensContent, 'utf8');
100
+ logger.success('Created tokens/tokens.css');
101
+ }
102
+
103
+ // Create base.css
104
+ const basePath = join(projectPath, 'styles/base.css');
105
+ if (!existsSync(basePath)) {
106
+ const baseContent = `* {
107
+ box-sizing: border-box;
108
+ margin: 0;
109
+ padding: 0;
110
+ }
111
+
112
+ body {
113
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
114
+ font-size: var(--wm-font-size-base);
115
+ color: var(--wm-color-text-primary);
116
+ line-height: 1.6;
117
+ }
118
+ `;
119
+ writeFileSync(basePath, baseContent, 'utf8');
120
+ logger.success('Created styles/base.css');
121
+ }
122
+
123
+ // Create ExampleSimple component (directory structure)
124
+ const exampleSimpleDir = join(projectPath, 'components/ExampleSimple');
125
+ if (!existsSync(exampleSimpleDir)) {
126
+ mkdirSync(exampleSimpleDir, { recursive: true });
127
+
128
+ // component.html
129
+ const componentHtmlPath = join(exampleSimpleDir, 'component.html');
130
+ const componentHtmlContent = `<!-- Simple Example Component -->
131
+ <!-- This is a basic HTML-only component using Tailwind CSS classes -->
132
+
133
+ <div class="p-6 bg-white rounded-lg shadow-md">
134
+ <h2 class="text-2xl font-semibold mb-4">
135
+ {{title}}
136
+ </h2>
137
+
138
+ <p class="text-gray-600 mb-4">
139
+ This is a simple HTML component. It uses Tailwind CSS classes for styling.
140
+ Edit this file to customize your component.
141
+ </p>
142
+
143
+ <button class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
144
+ Get Started
145
+ </button>
146
+ </div>
147
+ `;
148
+ writeFileSync(componentHtmlPath, componentHtmlContent, 'utf8');
149
+
150
+ // component.json
151
+ const componentJsonPath = join(exampleSimpleDir, 'component.json');
152
+ const componentJsonContent = `{
153
+ "name": "ExampleSimple",
154
+ "version": "1.0.0",
155
+ "description": "A simple example component using Tailwind CSS",
156
+ "category": "examples",
157
+ "props": {
158
+ "title": {
159
+ "type": "string",
160
+ "label": "Title",
161
+ "default": "Welcome to Webmate!",
162
+ "description": "Main heading text"
163
+ }
164
+ }
165
+ }
166
+ `;
167
+ writeFileSync(componentJsonPath, componentJsonContent, 'utf8');
168
+
169
+ logger.success('Created components/ExampleSimple/');
170
+ }
171
+
172
+ // Create ExampleExtended component (directory structure with island)
173
+ const exampleExtendedDir = join(projectPath, 'components/ExampleExtended');
174
+ if (!existsSync(exampleExtendedDir)) {
175
+ mkdirSync(exampleExtendedDir, { recursive: true });
176
+
177
+ // component.html
178
+ const componentHtmlPath = join(exampleExtendedDir, 'component.html');
179
+ const componentHtmlContent = `<!-- Extended Example Component with Island -->
180
+ <!-- This component demonstrates the full feature set including interactive islands -->
181
+
182
+ <div class="p-8 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl shadow-lg">
183
+ <div class="mb-6">
184
+ <h2 class="text-3xl font-bold text-gray-900 mb-2">
185
+ {{title}}
186
+ </h2>
187
+ <p class="text-gray-600">
188
+ {{subtitle}}
189
+ </p>
190
+ </div>
191
+
192
+ <!-- Interactive Island (Vanilla JS) -->
193
+ <!-- Islands can also use: React, Svelte, Vue, Preact, Alpine.js, or Lit -->
194
+ <div
195
+ data-island="counter"
196
+ data-island-props='{"initialCount": 0, "label": "Clicks"}'
197
+ class="p-6 bg-white rounded-lg border-2 border-blue-200"
198
+ >
199
+ <p class="text-sm text-gray-500 mb-4">
200
+ 💡 This counter is powered by an interactive island.
201
+ Islands enable client-side JavaScript while keeping the rest of the page static.
202
+ </p>
203
+ <div id="counter-display" class="text-center">
204
+ <div class="text-4xl font-bold text-blue-600 mb-2">0</div>
205
+ <div class="text-sm text-gray-600 mb-4">Clicks</div>
206
+ <button
207
+ id="increment-btn"
208
+ class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
209
+ >
210
+ Increment
211
+ </button>
212
+ </div>
213
+ </div>
214
+
215
+ <div class="mt-6 p-4 bg-blue-100 rounded-lg">
216
+ <p class="text-sm text-blue-900">
217
+ <strong>Supported Island Frameworks:</strong>
218
+ Vanilla JS, React, Svelte, Vue, Preact, Alpine.js, Lit
219
+ </p>
220
+ </div>
221
+ </div>
222
+ `;
223
+ writeFileSync(componentHtmlPath, componentHtmlContent, 'utf8');
224
+
225
+ // component.json
226
+ const componentJsonPath = join(exampleExtendedDir, 'component.json');
227
+ const componentJsonContent = `{
228
+ "name": "ExampleExtended",
229
+ "version": "1.0.0",
230
+ "description": "Extended example component with interactive island",
231
+ "category": "examples",
232
+ "props": {
233
+ "title": {
234
+ "type": "string",
235
+ "default": "Interactive Component",
236
+ "description": "Component heading text"
237
+ },
238
+ "subtitle": {
239
+ "type": "string",
240
+ "default": "This component includes an interactive island for client-side interactivity.",
241
+ "description": "Component subtitle text"
242
+ }
243
+ },
244
+ "islands": [
245
+ {
246
+ "name": "counter",
247
+ "file": "islands/counter.js",
248
+ "framework": "vanilla",
249
+ "props": {
250
+ "initialCount": {
251
+ "type": "number",
252
+ "default": 0,
253
+ "description": "Starting count value"
254
+ },
255
+ "label": {
256
+ "type": "string",
257
+ "default": "Clicks",
258
+ "description": "Label for the counter display"
259
+ }
260
+ }
261
+ }
262
+ ]
263
+ }
264
+ `;
265
+ writeFileSync(componentJsonPath, componentJsonContent, 'utf8');
266
+
267
+ // islands/counter.js (Vanilla JS Island)
268
+ const islandsDir = join(exampleExtendedDir, 'islands');
269
+ mkdirSync(islandsDir, { recursive: true });
270
+ const counterIslandPath = join(islandsDir, 'counter.js');
271
+ const counterIslandContent = `/**
272
+ * Counter Island - Vanilla JavaScript
273
+ *
274
+ * This is an example of a client-side interactive island.
275
+ * Islands are lazy-loaded and hydrated only when needed.
276
+ *
277
+ * You can also use:
278
+ * - React (.jsx)
279
+ * - Svelte (.svelte)
280
+ * - Vue (.vue)
281
+ * - Preact (.jsx with preact)
282
+ * - Alpine.js (inline in HTML)
283
+ * - Lit (.js with lit-element)
284
+ */
285
+
286
+ export default class CounterIsland {
287
+ constructor(element, props) {
288
+ this.element = element;
289
+ this.props = props;
290
+ this.count = props.initialCount || 0;
291
+ this.label = props.label || 'Count';
292
+ }
293
+
294
+ init() {
295
+ // Get DOM elements
296
+ this.display = this.element.querySelector('#counter-display > div:first-child');
297
+ this.labelElement = this.element.querySelector('#counter-display > div:nth-child(2)');
298
+ this.button = this.element.querySelector('#increment-btn');
299
+
300
+ // Set initial label
301
+ if (this.labelElement) {
302
+ this.labelElement.textContent = this.label;
303
+ }
304
+
305
+ // Add event listener
306
+ if (this.button) {
307
+ this.button.addEventListener('click', () => this.increment());
308
+ }
309
+
310
+ // Update display
311
+ this.updateDisplay();
312
+ }
313
+
314
+ increment() {
315
+ this.count++;
316
+ this.updateDisplay();
317
+
318
+ // Optional: Add animation
319
+ this.display?.classList.add('scale-110');
320
+ setTimeout(() => {
321
+ this.display?.classList.remove('scale-110');
322
+ }, 200);
323
+ }
324
+
325
+ updateDisplay() {
326
+ if (this.display) {
327
+ this.display.textContent = this.count;
328
+ }
329
+ }
330
+
331
+ // Cleanup when island is destroyed
332
+ destroy() {
333
+ if (this.button) {
334
+ this.button.removeEventListener('click', this.increment);
335
+ }
336
+ }
337
+ }
338
+ `;
339
+ writeFileSync(counterIslandPath, counterIslandContent, 'utf8');
340
+
341
+ logger.success('Created components/ExampleExtended/ (with island)');
342
+ }
343
+
344
+ // Create .gitignore
345
+ const gitignorePath = join(projectPath, '.gitignore');
346
+ if (!existsSync(gitignorePath)) {
347
+ const gitignoreContent = `node_modules/
348
+ dist/
349
+ .DS_Store
350
+ *.log
351
+ `;
352
+ writeFileSync(gitignorePath, gitignoreContent, 'utf8');
353
+ logger.success('Created .gitignore');
354
+ }
355
+
356
+ // Create README
357
+ const readmePath = join(projectPath, 'README.md');
358
+ if (!existsSync(readmePath)) {
359
+ const readmeContent = `# Webmate Components
360
+
361
+ HTML-first component collection built with Webmate CLI.
362
+
363
+ ## Getting Started
364
+
365
+ ### Development
366
+ \`\`\`bash
367
+ wm dev
368
+ \`\`\`
369
+
370
+ Start the preview server to see your components live. The preview updates automatically when you save changes.
371
+
372
+ ### Build
373
+ \`\`\`bash
374
+ wm build
375
+ \`\`\`
376
+
377
+ Build components for production deployment. Generates optimized HTML and JavaScript bundles.
378
+
379
+ ### Deploy to CMS
380
+ \`\`\`bash
381
+ export CMS_TOKEN="your-token"
382
+ wm push
383
+ \`\`\`
384
+
385
+ ## Component Types
386
+
387
+ ### Simple Components (Single HTML File)
388
+ Perfect for static content and basic layouts. See \`components/ExampleSimple.html\` for an example.
389
+
390
+ ### Extended Components (Directory Structure)
391
+ For complex components with interactive islands, assets, and metadata. See \`components/ExampleExtended/\` for an example.
392
+
393
+ ## Interactive Islands
394
+
395
+ Add client-side interactivity with Islands Architecture:
396
+
397
+ \`\`\`html
398
+ <div
399
+ data-island="my-island"
400
+ data-island-props='{"key": "value"}'
401
+ >
402
+ <!-- Your HTML content -->
403
+ </div>
404
+ \`\`\`
405
+
406
+ ### Supported Frameworks
407
+ - Vanilla JavaScript
408
+ - React
409
+ - Svelte
410
+ - Vue
411
+ - Preact
412
+ - Alpine.js
413
+ - Lit
414
+
415
+ ### Generate a New Component with Island
416
+ \`\`\`bash
417
+ wm g component MyComponent --islands --template vanilla
418
+ \`\`\`
419
+
420
+ ## Styling
421
+
422
+ Components use Tailwind CSS for styling. Design tokens from the CMS are automatically available.
423
+
424
+ Custom tokens can be added in \`tokens/tokens.css\`.
425
+
426
+ ## Documentation
427
+
428
+ - [Component Architecture](https://docs.webmate.io/components)
429
+ - [Islands Architecture](https://docs.webmate.io/islands)
430
+ - [CLI Reference](https://docs.webmate.io/cli)
431
+ `;
432
+ writeFileSync(readmePath, readmeContent, 'utf8');
433
+ logger.success('Created README.md');
434
+ }
435
+
436
+ console.log(`
437
+ ${pc.green('✨ Project initialized successfully!')}
438
+
439
+ ${pc.bold('Next steps:')}
440
+ ${pc.cyan('cd ' + directory)}
441
+ ${pc.cyan('wm dev')} # Start preview server
442
+ ${pc.cyan('wm build')} # Build for production
443
+ ${pc.cyan('wm push')} # Deploy to CMS
444
+
445
+ ${pc.bold('Example components created:')}
446
+ ${pc.gray('components/ExampleSimple/')} # Basic HTML component
447
+ ${pc.gray('components/ExampleExtended/')} # Component with interactive island
448
+
449
+ ${pc.gray('Edit wm.config.js to configure your project')}
450
+ ${pc.gray('Design tokens in tokens/tokens.css can override CMS defaults')}
451
+ `);
452
+ }
@@ -0,0 +1,193 @@
1
+ import { input, password, select } from '@inquirer/prompts';
2
+ import { logger } from '../../../core/src/index.js';
3
+ import { saveAuth, loadAuth, getCmsBaseUrl } from '../utils/auth.js';
4
+ import pc from 'picocolors';
5
+
6
+ /**
7
+ * Custom fetch wrapper that accepts self-signed certificates for localhost
8
+ */
9
+ async function secureFetch(url, options = {}) {
10
+ const isLocalhost = url.includes('localhost') || url.includes('127.0.0.1');
11
+
12
+ if (isLocalhost) {
13
+ // Temporarily disable TLS verification for localhost
14
+ const originalValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
15
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
16
+
17
+ try {
18
+ return await fetch(url, options);
19
+ } finally {
20
+ // Restore original value
21
+ if (originalValue !== undefined) {
22
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalValue;
23
+ } else {
24
+ delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
25
+ }
26
+ }
27
+ }
28
+
29
+ // For non-localhost, use regular fetch
30
+ return fetch(url, options);
31
+ }
32
+
33
+ /**
34
+ * Login command - Interactive login flow
35
+ */
36
+ export async function loginCommand(options = {}) {
37
+ console.log('');
38
+ logger.info(pc.bold('Webmate CLI Login'));
39
+ console.log('');
40
+
41
+ try {
42
+ // Check if already logged in
43
+ const existingAuth = loadAuth();
44
+ if (existingAuth?.user) {
45
+ const shouldLogout = await select({
46
+ message: `Bereits eingeloggt als ${pc.cyan(existingAuth.user.email)}. Neu anmelden?`,
47
+ choices: [
48
+ { name: 'Nein, weiter mit aktuellem Login', value: false },
49
+ { name: 'Ja, neu anmelden', value: true }
50
+ ]
51
+ });
52
+
53
+ if (!shouldLogout) {
54
+ logger.info('Login abgebrochen');
55
+ return;
56
+ }
57
+ }
58
+
59
+ // Get base URL (optional, default to localhost for dev)
60
+ // Note: CLI API routes are under app.localhost (account subdomain)
61
+ const baseUrl = options.url || await input({
62
+ message: 'Webmate Base URL:',
63
+ default: 'https://app.localhost:3029',
64
+ validate: (value) => {
65
+ try {
66
+ new URL(value);
67
+ return true;
68
+ } catch {
69
+ return 'Bitte gültige URL eingeben';
70
+ }
71
+ }
72
+ });
73
+
74
+ // Get credentials
75
+ const email = await input({
76
+ message: 'E-Mail:',
77
+ validate: (value) => {
78
+ if (!value || !value.includes('@')) {
79
+ return 'Bitte gültige E-Mail-Adresse eingeben';
80
+ }
81
+ return true;
82
+ }
83
+ });
84
+
85
+ const pwd = await password({
86
+ message: 'Passwort:',
87
+ mask: '*'
88
+ });
89
+
90
+ console.log('');
91
+ logger.info('Authentifiziere...');
92
+
93
+ // Login API call
94
+ const loginUrl = `${baseUrl}/api/cli/auth/login`;
95
+ const loginResponse = await secureFetch(loginUrl, {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ email, password: pwd })
99
+ });
100
+
101
+ if (!loginResponse.ok) {
102
+ const error = await loginResponse.json();
103
+ throw new Error(error.error || 'Login fehlgeschlagen');
104
+ }
105
+
106
+ const loginData = await loginResponse.json();
107
+ logger.success(`Eingeloggt als ${pc.cyan(loginData.user.email)}`);
108
+
109
+ // Get list of tenants
110
+ console.log('');
111
+ logger.info('Lade Projekte...');
112
+
113
+ const tenantsUrl = `${baseUrl}/api/cli/tenants?userId=${loginData.user.id}`;
114
+ const tenantsResponse = await secureFetch(tenantsUrl);
115
+
116
+ if (!tenantsResponse.ok) {
117
+ throw new Error('Fehler beim Laden der Projekte');
118
+ }
119
+
120
+ const tenantsData = await tenantsResponse.json();
121
+
122
+ if (!tenantsData.tenants || tenantsData.tenants.length === 0) {
123
+ logger.warning('Keine Projekte gefunden. Bitte erstelle zuerst ein Projekt im CMS.');
124
+ return;
125
+ }
126
+
127
+ // Let user select tenant
128
+ console.log('');
129
+ const selectedTenantId = await select({
130
+ message: 'Wähle ein Projekt:',
131
+ choices: tenantsData.tenants.map(t => ({
132
+ name: `${t.name} (${t.subdomain})`,
133
+ value: t.id,
134
+ description: `Erstellt am ${new Date(t.createdAt).toLocaleDateString('de-DE')}`
135
+ }))
136
+ });
137
+
138
+ const selectedTenant = tenantsData.tenants.find(t => t.id === selectedTenantId);
139
+
140
+ // Generate API token for selected tenant
141
+ console.log('');
142
+ logger.info('Generiere API-Token...');
143
+
144
+ const tokenUrl = `${baseUrl}/api/cli/tenants`;
145
+ const tokenResponse = await secureFetch(tokenUrl, {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({
149
+ userId: loginData.user.id,
150
+ tenantId: selectedTenantId
151
+ })
152
+ });
153
+
154
+ if (!tokenResponse.ok) {
155
+ throw new Error('Fehler beim Generieren des API-Tokens');
156
+ }
157
+
158
+ const tokenData = await tokenResponse.json();
159
+
160
+ // Save auth data
161
+ saveAuth({
162
+ user: loginData.user,
163
+ tenant: selectedTenant,
164
+ apiToken: tokenData.apiToken,
165
+ baseUrl,
166
+ loginAt: new Date().toISOString()
167
+ });
168
+
169
+ console.log('');
170
+ logger.success(pc.green('✨ Login erfolgreich!'));
171
+ console.log('');
172
+ console.log(` ${pc.bold('Projekt:')} ${pc.cyan(selectedTenant.name)}`);
173
+ console.log(` ${pc.bold('Subdomain:')} ${pc.cyan(selectedTenant.subdomain)}`);
174
+ console.log('');
175
+ console.log(`${pc.gray('Du kannst jetzt')} ${pc.cyan('wm dev')} ${pc.gray('und')} ${pc.cyan('wm push')} ${pc.gray('verwenden.')}`);
176
+ console.log('');
177
+
178
+ } catch (error) {
179
+ console.log('');
180
+ logger.error(`Login fehlgeschlagen: ${error.message}`);
181
+
182
+ // Detaillierte Fehlerausgabe für Debugging
183
+ if (error.cause) {
184
+ logger.error(`Ursache: ${error.cause.message || error.cause}`);
185
+ }
186
+ if (error.stack) {
187
+ console.error('\nStack trace:');
188
+ console.error(error.stack);
189
+ }
190
+
191
+ process.exit(1);
192
+ }
193
+ }
@@ -0,0 +1,20 @@
1
+ import { clearAuth } from '../utils/auth.js';
2
+ import { logger } from '../../../core/src/index.js';
3
+ import pc from 'picocolors';
4
+
5
+ /**
6
+ * Logout command - removes stored authentication
7
+ */
8
+ export async function logout(options = {}) {
9
+ try {
10
+ // Clear authentication
11
+ clearAuth();
12
+
13
+ logger.success('Successfully logged out');
14
+ logger.info('Run ' + pc.cyan('wm login') + ' to log in again');
15
+
16
+ } catch (error) {
17
+ logger.error(`Logout failed: ${error.message}`);
18
+ process.exit(1);
19
+ }
20
+ }