@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,44 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Alpine.js interactive component
4
+ */
5
+ import Alpine from 'alpinejs';
6
+
7
+ export default class {{NAME}}Island {
8
+ constructor(element, props = {}) {
9
+ this.element = element;
10
+
11
+ // Title aus props oder default
12
+ const title = props.title || '{{NAME}} Component';
13
+
14
+ // Create Alpine component HTML
15
+ const html = `
16
+ <div x-data="{ count: 0, doubled: 0 }" x-init="doubled = count * 2" class="p-6 bg-white rounded-lg border border-gray-200">
17
+ <h3 class="text-lg font-semibold mb-4">${title}</h3>
18
+ <p class="mb-2">Count: <span x-text="count"></span></p>
19
+ <p class="mb-4 text-gray-600">Doubled: <span x-text="doubled"></span></p>
20
+ <div class="flex gap-2">
21
+ <button @click="count--; doubled = count * 2" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">-</button>
22
+ <button @click="count++; doubled = count * 2" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">+</button>
23
+ </div>
24
+ </div>
25
+ `;
26
+
27
+ // Insert HTML into element
28
+ element.innerHTML = html;
29
+
30
+ // Initialize Alpine on this element if not already started
31
+ if (!window.Alpine) {
32
+ window.Alpine = Alpine;
33
+ Alpine.start();
34
+ } else {
35
+ // Initialize Alpine for this specific element
36
+ Alpine.initTree(element);
37
+ }
38
+ }
39
+
40
+ destroy() {
41
+ // Alpine cleanup happens automatically
42
+ this.element.innerHTML = '';
43
+ }
44
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Lit Web Component (without Shadow DOM for Tailwind support)
4
+ */
5
+ import { LitElement, html } from 'lit';
6
+
7
+ class {{NAME}}Component extends LitElement {
8
+ static properties = {
9
+ title: { type: String },
10
+ count: { type: Number }
11
+ };
12
+
13
+ // Disable Shadow DOM to allow Tailwind classes to work
14
+ createRenderRoot() {
15
+ return this;
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this.title = '{{NAME}} Component';
21
+ this.count = 0;
22
+ }
23
+
24
+ increment() {
25
+ this.count++;
26
+ }
27
+
28
+ render() {
29
+ return html`
30
+ <div class="p-6 bg-white rounded-lg border border-gray-200">
31
+ <h3 class="text-lg font-semibold mb-4">${this.title}</h3>
32
+ <p class="mb-2">Count: ${this.count}</p>
33
+ <p class="mb-4 text-gray-600">Doubled: ${this.count * 2}</p>
34
+ <div class="flex gap-2">
35
+ <button @click=${() => this.count--} class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
36
+ -
37
+ </button>
38
+ <button @click=${this.increment} class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
39
+ +
40
+ </button>
41
+ </div>
42
+ </div>
43
+ `;
44
+ }
45
+ }
46
+
47
+ // For HMR: Use timestamp-based name to allow re-registration
48
+ const componentName = '{{KEBAB_NAME}}-component';
49
+ const timestamp = Date.now();
50
+ const uniqueComponentName = `${componentName}-${timestamp}`;
51
+
52
+ // Register custom element with unique name for HMR
53
+ if (!customElements.get(uniqueComponentName)) {
54
+ customElements.define(uniqueComponentName, {{NAME}}Component);
55
+ }
56
+
57
+ export default class {{NAME}}Island {
58
+ constructor(element, props = {}) {
59
+ this.element = element;
60
+
61
+ // Replace element content with Lit component (use unique name)
62
+ const component = document.createElement(uniqueComponentName);
63
+
64
+ // Transfer any data-attributes
65
+ for (const attr of element.attributes) {
66
+ if (attr.name.startsWith('data-')) {
67
+ component.setAttribute(attr.name, attr.value);
68
+ }
69
+ }
70
+
71
+ // Set initial props
72
+ Object.assign(component, props);
73
+
74
+ element.appendChild(component);
75
+ this.component = component;
76
+ }
77
+
78
+ destroy() {
79
+ // Remove component from DOM and clear element
80
+ if (this.component) {
81
+ if (this.component.parentNode) {
82
+ this.component.parentNode.removeChild(this.component);
83
+ }
84
+ }
85
+ // Clear element content for clean re-initialization
86
+ if (this.element) {
87
+ this.element.innerHTML = '';
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Preact interactive component
4
+ */
5
+ import { render } from 'preact';
6
+ import { useState } from 'preact/hooks';
7
+
8
+ function {{NAME}}Component({ title = '{{NAME}} Component' }) {
9
+ const [count, setCount] = useState(0);
10
+ const doubled = count * 2;
11
+
12
+ return (
13
+ <div className="p-6 bg-white rounded-lg border border-gray-200">
14
+ <h3 className="text-lg font-semibold mb-4">{title}</h3>
15
+ <p className="mb-2">Count: {count}</p>
16
+ <p className="mb-4 text-gray-600">Doubled: {doubled}</p>
17
+ <div className="flex gap-2">
18
+ <button
19
+ onClick={() => setCount(count - 1)}
20
+ className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
21
+ >
22
+ -
23
+ </button>
24
+ <button
25
+ onClick={() => setCount(count + 1)}
26
+ className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
27
+ >
28
+ +
29
+ </button>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export default class {{NAME}}Island {
36
+ constructor(element, props = {}) {
37
+ this.element = element;
38
+
39
+ // Get initial data from data-attributes
40
+ const initialData = JSON.parse(element.dataset.initialData || '{}');
41
+
42
+ // Merge props
43
+ const allProps = { ...initialData, ...props };
44
+
45
+ render(<{{NAME}}Component {...allProps} />, element);
46
+ }
47
+
48
+ destroy() {
49
+ // Preact will cleanup automatically
50
+ render(null, this.element);
51
+ }
52
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * React interactive component
4
+ */
5
+ import React, { useState } from 'react';
6
+ import { createRoot } from 'react-dom/client';
7
+
8
+ function {{NAME}}Component({ title = '{{NAME}} Component' }) {
9
+ const [count, setCount] = useState(0);
10
+ const doubled = count * 2;
11
+
12
+ return (
13
+ <div className="p-6 bg-white rounded-lg border border-gray-200">
14
+ <h3 className="text-lg font-semibold mb-4">{title}</h3>
15
+ <p className="mb-2">Count: {count}</p>
16
+ <p className="mb-4 text-gray-600">Doubled: {doubled}</p>
17
+ <div className="flex gap-2">
18
+ <button
19
+ onClick={() => setCount(count - 1)}
20
+ className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
21
+ >
22
+ -
23
+ </button>
24
+ <button
25
+ onClick={() => setCount(count + 1)}
26
+ className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
27
+ >
28
+ +
29
+ </button>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export default class {{NAME}}Island {
36
+ constructor(element, props = {}) {
37
+ this.element = element;
38
+ this.root = createRoot(element);
39
+
40
+ // Get initial data from data-attributes
41
+ const initialData = JSON.parse(element.dataset.initialData || '{}');
42
+ const allProps = { ...initialData, ...props };
43
+
44
+ this.root.render(<{{NAME}}Component {...allProps} />);
45
+ }
46
+
47
+ destroy() {
48
+ this.root?.unmount();
49
+ }
50
+ }
@@ -0,0 +1,36 @@
1
+ <script>
2
+ // Props
3
+ let { title = '{{NAME}} Component' } = $props();
4
+
5
+ // Svelte 5 - Reactive state with $state() rune
6
+ let count = $state(0);
7
+
8
+ // Derived state with $derived() rune
9
+ let doubled = $derived(count * 2);
10
+
11
+ function increment() {
12
+ count += 1;
13
+ }
14
+
15
+ function decrement() {
16
+ count -= 1;
17
+ }
18
+ </script>
19
+
20
+ <div class="p-6 bg-white rounded-lg border border-gray-200">
21
+ <h3 class="text-lg font-semibold mb-4">{title}</h3>
22
+ <p class="mb-2">Count: {count}</p>
23
+ <p class="mb-4 text-gray-600">Doubled: {doubled}</p>
24
+ <div class="flex gap-2">
25
+ <button onclick={decrement} class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
26
+ -
27
+ </button>
28
+ <button onclick={increment} class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
29
+ +
30
+ </button>
31
+ </div>
32
+ </div>
33
+
34
+ <style>
35
+ /* Custom CSS falls benötigt */
36
+ </style>
@@ -0,0 +1,31 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Svelte 5 interactive component
4
+ */
5
+ import Component from './{{NAME}}.svelte';
6
+ import { mount, unmount } from 'svelte';
7
+
8
+ export default class {{NAME}}Island {
9
+ constructor(element, props = {}) {
10
+ this.element = element;
11
+
12
+ // Get initial props from data-attributes
13
+ const initialData = JSON.parse(element.dataset.initialData || '{}');
14
+
15
+ // Merge props from constructor and data attributes
16
+ const allProps = { ...initialData, ...props };
17
+
18
+ // Svelte 5: Use mount() instead of new Component()
19
+ this.component = mount(Component, {
20
+ target: element,
21
+ props: allProps
22
+ });
23
+ }
24
+
25
+ destroy() {
26
+ // Svelte 5: Use unmount() instead of $destroy()
27
+ if (this.component) {
28
+ unmount(this.component);
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Vanilla JavaScript interactive component
4
+ */
5
+ export default class {{NAME}}Island {
6
+ constructor(element, props = {}) {
7
+ this.element = element;
8
+ this.props = props;
9
+ this.count = 0;
10
+ this.init();
11
+ }
12
+
13
+ init() {
14
+ // Title aus props oder default
15
+ const title = this.props.title || '{{NAME}} Component';
16
+
17
+ // Create HTML structure
18
+ this.element.innerHTML = `
19
+ <div class="p-6 bg-white rounded-lg border border-gray-200">
20
+ <h3 class="text-lg font-semibold mb-4">${title}</h3>
21
+ <p class="mb-2">Count: <span class="count-display">0</span></p>
22
+ <p class="mb-4 text-gray-600">Doubled: <span class="doubled-display">0</span></p>
23
+ <div class="flex gap-2">
24
+ <button class="decrement-btn px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
25
+ -
26
+ </button>
27
+ <button class="increment-btn px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
28
+ +
29
+ </button>
30
+ </div>
31
+ </div>
32
+ `;
33
+
34
+ // Get references to DOM elements
35
+ this.countDisplay = this.element.querySelector('.count-display');
36
+ this.doubledDisplay = this.element.querySelector('.doubled-display');
37
+ this.incrementBtn = this.element.querySelector('.increment-btn');
38
+ this.decrementBtn = this.element.querySelector('.decrement-btn');
39
+
40
+ // Add event listeners
41
+ this.incrementBtn.addEventListener('click', () => this.increment());
42
+ this.decrementBtn.addEventListener('click', () => this.decrement());
43
+
44
+ console.log('{{NAME}} Island initialized');
45
+ }
46
+
47
+ increment() {
48
+ this.count++;
49
+ this.updateDisplay();
50
+ }
51
+
52
+ decrement() {
53
+ this.count--;
54
+ this.updateDisplay();
55
+ }
56
+
57
+ updateDisplay() {
58
+ this.countDisplay.textContent = this.count;
59
+ this.doubledDisplay.textContent = this.count * 2;
60
+ }
61
+
62
+ destroy() {
63
+ // Remove event listeners
64
+ if (this.incrementBtn) {
65
+ this.incrementBtn.removeEventListener('click', this.increment);
66
+ }
67
+ if (this.decrementBtn) {
68
+ this.decrementBtn.removeEventListener('click', this.decrement);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * {{NAME}} Island
3
+ * Vue interactive component
4
+ */
5
+ import { createApp } from 'vue';
6
+
7
+ const {{NAME}}Component = {
8
+ props: {
9
+ title: {
10
+ type: String,
11
+ default: '{{NAME}} Component'
12
+ }
13
+ },
14
+ data() {
15
+ return {
16
+ count: 0
17
+ };
18
+ },
19
+ computed: {
20
+ doubled() {
21
+ return this.count * 2;
22
+ }
23
+ },
24
+ methods: {
25
+ increment() {
26
+ this.count++;
27
+ },
28
+ decrement() {
29
+ this.count--;
30
+ }
31
+ },
32
+ template: `
33
+ <div class="p-6 bg-white rounded-lg border border-gray-200">
34
+ <h3 class="text-lg font-semibold mb-4">{{ title }}</h3>
35
+ <p class="mb-2">Count: {{ count }}</p>
36
+ <p class="mb-4 text-gray-600">Doubled: {{ doubled }}</p>
37
+ <div class="flex gap-2">
38
+ <button @click="decrement" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">-</button>
39
+ <button @click="increment" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">+</button>
40
+ </div>
41
+ </div>
42
+ `
43
+ };
44
+
45
+ export default class {{NAME}}Island {
46
+ constructor(element, props = {}) {
47
+ this.element = element;
48
+
49
+ // Get initial data from data-attributes
50
+ const initialData = JSON.parse(element.dataset.initialData || '{}');
51
+
52
+ // Merge props
53
+ const allProps = { ...initialData, ...props };
54
+
55
+ // Create Vue app
56
+ this.app = createApp({{NAME}}Component, allProps);
57
+ this.instance = this.app.mount(element);
58
+ }
59
+
60
+ destroy() {
61
+ if (this.app) {
62
+ this.app.unmount();
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ /**
6
+ * Auth Helper für CLI
7
+ * Speichert Login-Credentials in ~/.webmate/auth.json
8
+ */
9
+
10
+ const AUTH_DIR = join(homedir(), '.webmate');
11
+ const AUTH_FILE = join(AUTH_DIR, 'auth.json');
12
+
13
+ /**
14
+ * Lädt gespeicherte Auth-Daten
15
+ */
16
+ export function loadAuth() {
17
+ if (!existsSync(AUTH_FILE)) {
18
+ return null;
19
+ }
20
+
21
+ try {
22
+ const content = readFileSync(AUTH_FILE, 'utf8');
23
+ if (!content || content.trim() === '') {
24
+ // Empty file - remove it
25
+ try {
26
+ unlinkSync(AUTH_FILE);
27
+ } catch (e) {
28
+ // Ignore cleanup errors
29
+ }
30
+ return null;
31
+ }
32
+ return JSON.parse(content);
33
+ } catch (error) {
34
+ // Corrupted auth file - remove it
35
+ try {
36
+ unlinkSync(AUTH_FILE);
37
+ } catch (e) {
38
+ // Ignore cleanup errors
39
+ }
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Speichert Auth-Daten
46
+ */
47
+ export function saveAuth(authData) {
48
+ if (!existsSync(AUTH_DIR)) {
49
+ mkdirSync(AUTH_DIR, { recursive: true });
50
+ }
51
+
52
+ writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf8');
53
+ }
54
+
55
+ /**
56
+ * Löscht gespeicherte Auth-Daten
57
+ */
58
+ export function clearAuth() {
59
+ if (existsSync(AUTH_FILE)) {
60
+ writeFileSync(AUTH_FILE, '', 'utf8');
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Prüft ob User eingeloggt ist
66
+ */
67
+ export function isLoggedIn() {
68
+ const auth = loadAuth();
69
+ return auth && auth.user && auth.apiToken;
70
+ }
71
+
72
+ /**
73
+ * Gibt aktuellen User zurück
74
+ */
75
+ export function getCurrentUser() {
76
+ const auth = loadAuth();
77
+ return auth?.user || null;
78
+ }
79
+
80
+ /**
81
+ * Gibt API Token zurück
82
+ */
83
+ export function getApiToken() {
84
+ const auth = loadAuth();
85
+ return auth?.apiToken || null;
86
+ }
87
+
88
+ /**
89
+ * Gibt aktuellen Tenant zurück
90
+ */
91
+ export function getCurrentTenant() {
92
+ const auth = loadAuth();
93
+ return auth?.tenant || null;
94
+ }
95
+
96
+ /**
97
+ * Gibt CMS Base URL zurück
98
+ */
99
+ export function getCmsBaseUrl() {
100
+ const auth = loadAuth();
101
+ if (!auth?.baseUrl) {
102
+ return 'https://app.localhost:3029'; // Default für Development
103
+ }
104
+ return auth.baseUrl;
105
+ }
106
+
107
+ /**
108
+ * Gibt Tenant CMS URL zurück
109
+ */
110
+ export function getTenantCmsUrl() {
111
+ const tenant = getCurrentTenant();
112
+ const baseUrl = getCmsBaseUrl();
113
+
114
+ if (!tenant) {
115
+ return null;
116
+ }
117
+
118
+ // Extrahiere Domain aus Base URL (z.B. "app.localhost:3029" → "localhost:3029")
119
+ const url = new URL(baseUrl);
120
+ const parts = url.host.split('.');
121
+ const domain = parts.slice(1).join('.'); // Entferne "app" Subdomain
122
+
123
+ // Baue Tenant CMS URL
124
+ return `${url.protocol}//${tenant.subdomain}.cms.${domain}`;
125
+ }