@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,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
|
+
}
|