@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,286 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import glob from 'glob';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Add a property to a component
|
|
10
|
+
* wm prop [filename]
|
|
11
|
+
*/
|
|
12
|
+
export async function prop(filename) {
|
|
13
|
+
console.log(pc.cyan('\n📝 Add Property to Component\n'));
|
|
14
|
+
|
|
15
|
+
let targetFile;
|
|
16
|
+
|
|
17
|
+
// Case 1: Filename provided
|
|
18
|
+
if (filename) {
|
|
19
|
+
if (!existsSync(filename)) {
|
|
20
|
+
console.log(pc.red(`\n❌ File not found: ${filename}\n`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
targetFile = filename;
|
|
24
|
+
} else {
|
|
25
|
+
// Case 2: No filename - auto-detect
|
|
26
|
+
targetFile = await detectComponentFile();
|
|
27
|
+
if (!targetFile) return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(pc.dim(`Target: ${targetFile}\n`));
|
|
31
|
+
|
|
32
|
+
// Read and parse component
|
|
33
|
+
const content = readFileSync(targetFile, 'utf-8');
|
|
34
|
+
const { metadata, props, propsScriptMatch } = parseComponent(content);
|
|
35
|
+
|
|
36
|
+
if (!propsScriptMatch) {
|
|
37
|
+
console.log(pc.yellow('⚠️ No <script wm:props> found in component.'));
|
|
38
|
+
const shouldCreate = await confirm({
|
|
39
|
+
message: 'Create wm:props section?',
|
|
40
|
+
default: true
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!shouldCreate) {
|
|
44
|
+
console.log(pc.dim('\nAborted.\n'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Show existing props
|
|
50
|
+
if (Object.keys(props).length > 0) {
|
|
51
|
+
console.log(pc.dim('Existing props:'));
|
|
52
|
+
Object.keys(props).forEach((key) => {
|
|
53
|
+
console.log(pc.dim(` - ${key} (${props[key].type})`));
|
|
54
|
+
});
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wizard for new prop (reuse logic from generate.js)
|
|
59
|
+
const propName = await input({
|
|
60
|
+
message: 'Property name (camelCase):',
|
|
61
|
+
validate: (value) => {
|
|
62
|
+
if (!value) return 'Property name is required';
|
|
63
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(value)) {
|
|
64
|
+
return 'Property name must be in camelCase (e.g., myProperty)';
|
|
65
|
+
}
|
|
66
|
+
if (props[value]) return 'Property already exists';
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const propType = await select({
|
|
72
|
+
message: 'Property type:',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'String (text input)', value: 'string' },
|
|
75
|
+
{ name: 'Boolean (checkbox)', value: 'boolean' },
|
|
76
|
+
{ name: 'Number (number input)', value: 'number' },
|
|
77
|
+
{ name: 'Select (dropdown)', value: 'select' },
|
|
78
|
+
{ name: 'Color (color picker)', value: 'color' },
|
|
79
|
+
{ name: 'Image (image upload)', value: 'image' },
|
|
80
|
+
{ name: 'Rich Text (WYSIWYG editor)', value: 'richtext' }
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const propLabel = await input({
|
|
85
|
+
message: 'Property label (display name):',
|
|
86
|
+
default: propName.charAt(0).toUpperCase() + propName.slice(1).replace(/([A-Z])/g, ' $1')
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const propDescription = await input({
|
|
90
|
+
message: 'Property description (optional):',
|
|
91
|
+
default: ''
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let propDefault = '';
|
|
95
|
+
let propOptions = null;
|
|
96
|
+
let propMin = null;
|
|
97
|
+
let propMax = null;
|
|
98
|
+
|
|
99
|
+
// Type-specific configuration
|
|
100
|
+
if (propType === 'string') {
|
|
101
|
+
propDefault = await input({
|
|
102
|
+
message: 'Default value:',
|
|
103
|
+
default: ''
|
|
104
|
+
});
|
|
105
|
+
} else if (propType === 'boolean') {
|
|
106
|
+
const defaultBool = await confirm({
|
|
107
|
+
message: 'Default value:',
|
|
108
|
+
default: false
|
|
109
|
+
});
|
|
110
|
+
propDefault = defaultBool;
|
|
111
|
+
} else if (propType === 'number') {
|
|
112
|
+
propDefault = await input({
|
|
113
|
+
message: 'Default value:',
|
|
114
|
+
default: '0',
|
|
115
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
116
|
+
});
|
|
117
|
+
propDefault = Number(propDefault);
|
|
118
|
+
|
|
119
|
+
const hasMin = await confirm({
|
|
120
|
+
message: 'Set minimum value?',
|
|
121
|
+
default: false
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (hasMin) {
|
|
125
|
+
propMin = await input({
|
|
126
|
+
message: 'Minimum value:',
|
|
127
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
128
|
+
});
|
|
129
|
+
propMin = Number(propMin);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const hasMax = await confirm({
|
|
133
|
+
message: 'Set maximum value?',
|
|
134
|
+
default: false
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (hasMax) {
|
|
138
|
+
propMax = await input({
|
|
139
|
+
message: 'Maximum value:',
|
|
140
|
+
validate: (v) => (!isNaN(Number(v)) ? true : 'Must be a number')
|
|
141
|
+
});
|
|
142
|
+
propMax = Number(propMax);
|
|
143
|
+
}
|
|
144
|
+
} else if (propType === 'select') {
|
|
145
|
+
const optionsInput = await input({
|
|
146
|
+
message: 'Options (comma-separated):',
|
|
147
|
+
validate: (v) => (v ? true : 'At least one option required')
|
|
148
|
+
});
|
|
149
|
+
propOptions = optionsInput.split(',').map((o) => o.trim());
|
|
150
|
+
propDefault = propOptions[0];
|
|
151
|
+
} else if (propType === 'color') {
|
|
152
|
+
propDefault = await input({
|
|
153
|
+
message: 'Default color (hex):',
|
|
154
|
+
default: '#000000',
|
|
155
|
+
validate: (v) => (/^#[0-9A-Fa-f]{6}$/.test(v) ? true : 'Must be hex color (e.g., #ff0000)')
|
|
156
|
+
});
|
|
157
|
+
} else if (propType === 'richtext') {
|
|
158
|
+
propDefault = '<p></p>';
|
|
159
|
+
} else if (propType === 'image') {
|
|
160
|
+
propDefault = '';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build new prop
|
|
164
|
+
const newProp = {
|
|
165
|
+
type: propType,
|
|
166
|
+
label: propLabel,
|
|
167
|
+
default: propDefault
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (propDescription) newProp.description = propDescription;
|
|
171
|
+
if (propOptions) newProp.options = propOptions;
|
|
172
|
+
if (propMin !== null) newProp.min = propMin;
|
|
173
|
+
if (propMax !== null) newProp.max = propMax;
|
|
174
|
+
|
|
175
|
+
// Add to props object
|
|
176
|
+
props[propName] = newProp;
|
|
177
|
+
|
|
178
|
+
// Update file content
|
|
179
|
+
let newContent;
|
|
180
|
+
if (propsScriptMatch) {
|
|
181
|
+
// Replace existing wm:props script
|
|
182
|
+
const propsJson = JSON.stringify(props, null, 2);
|
|
183
|
+
const newPropsScript = `<script type="application/json" wm:props>\n${propsJson}\n</script>`;
|
|
184
|
+
newContent = content.replace(propsScriptMatch[0], newPropsScript);
|
|
185
|
+
} else {
|
|
186
|
+
// Insert new wm:props script after wm:description (if exists) or at beginning
|
|
187
|
+
const descMatch = content.match(
|
|
188
|
+
/<script[^>]*wm:description[^>]*>[\s\S]*?<\/script>/
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const propsJson = JSON.stringify(props, null, 2);
|
|
192
|
+
const newPropsScript = `\n<!-- Component Props -->\n<script type="application/json" wm:props>\n${propsJson}\n</script>\n`;
|
|
193
|
+
|
|
194
|
+
if (descMatch) {
|
|
195
|
+
// Insert after description script
|
|
196
|
+
const insertPos = descMatch.index + descMatch[0].length;
|
|
197
|
+
newContent = content.slice(0, insertPos) + newPropsScript + content.slice(insertPos);
|
|
198
|
+
} else {
|
|
199
|
+
// Insert at beginning
|
|
200
|
+
newContent = newPropsScript + content;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Write back
|
|
205
|
+
await fs.writeFile(targetFile, newContent, 'utf-8');
|
|
206
|
+
|
|
207
|
+
console.log(pc.green(`\n✓ Property "${propName}" added to ${targetFile}`));
|
|
208
|
+
console.log(pc.dim(`\nUse in template: {{${propName}}}\n`));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Auto-detect component file
|
|
213
|
+
*/
|
|
214
|
+
async function detectComponentFile() {
|
|
215
|
+
const htmlFiles = glob.sync('*.html');
|
|
216
|
+
|
|
217
|
+
if (htmlFiles.length === 0) {
|
|
218
|
+
// No files in current dir - check components/ dir
|
|
219
|
+
const componentFiles = glob.sync('components/**/*.html');
|
|
220
|
+
|
|
221
|
+
if (componentFiles.length === 0) {
|
|
222
|
+
console.log(pc.red('\n❌ No .html component files found\n'));
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Show selector for all components
|
|
227
|
+
const selected = await select({
|
|
228
|
+
message: 'Which component?',
|
|
229
|
+
choices: componentFiles.map((f) => ({
|
|
230
|
+
name: f,
|
|
231
|
+
value: f
|
|
232
|
+
}))
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return selected;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (htmlFiles.length === 1) {
|
|
239
|
+
// Exactly one file - use it
|
|
240
|
+
return htmlFiles[0];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Multiple files - ask
|
|
244
|
+
const selected = await select({
|
|
245
|
+
message: 'Which component?',
|
|
246
|
+
choices: htmlFiles.map((f) => ({
|
|
247
|
+
name: f,
|
|
248
|
+
value: f
|
|
249
|
+
}))
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return selected;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parse component to extract existing props
|
|
257
|
+
*/
|
|
258
|
+
function parseComponent(content) {
|
|
259
|
+
// Extract wm:description
|
|
260
|
+
const descMatch = content.match(/<script[^>]*wm:description[^>]*>([\s\S]*?)<\/script>/);
|
|
261
|
+
let metadata = {};
|
|
262
|
+
if (descMatch) {
|
|
263
|
+
try {
|
|
264
|
+
metadata = JSON.parse(descMatch[1].trim());
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// Ignore parse errors
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Extract wm:props
|
|
271
|
+
const propsMatch = content.match(/<script[^>]*wm:props[^>]*>([\s\S]*?)<\/script>/);
|
|
272
|
+
let props = {};
|
|
273
|
+
if (propsMatch) {
|
|
274
|
+
try {
|
|
275
|
+
props = JSON.parse(propsMatch[1].trim());
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.warn(pc.yellow('⚠️ Could not parse existing props (invalid JSON)'));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
metadata,
|
|
283
|
+
props,
|
|
284
|
+
propsScriptMatch: propsMatch
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { uploadComponents, logger } from '../../../core/src/index.js';
|
|
2
|
+
import { loadAuth, getTenantCmsUrl, getApiToken, isLoggedIn } from '../utils/auth.js';
|
|
3
|
+
import { loadConfig, updateConfigVersion } from '../utils/config.js';
|
|
4
|
+
import { incrementPatch, incrementMinor, incrementMajor } from '../utils/semver.js';
|
|
5
|
+
import { build as buildComponents } from '../../../builder/src/index.js';
|
|
6
|
+
import { existsSync, readFileSync, statSync, readdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { select } from '@inquirer/prompts';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load design tokens from CMS
|
|
14
|
+
*/
|
|
15
|
+
async function loadDesignTokens() {
|
|
16
|
+
try {
|
|
17
|
+
const auth = loadAuth();
|
|
18
|
+
if (!auth || !auth.apiToken) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cmsUrl = getTenantCmsUrl();
|
|
23
|
+
if (!cmsUrl) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Disable SSL verification for localhost
|
|
28
|
+
const isLocalhost = cmsUrl.includes('localhost') || cmsUrl.includes('127.0.0.1');
|
|
29
|
+
if (isLocalhost) {
|
|
30
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const response = await fetch(`${cmsUrl}/api/design-tokens`, {
|
|
34
|
+
headers: {
|
|
35
|
+
'x-api-token': auth.apiToken
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
|
|
45
|
+
// Restore SSL verification
|
|
46
|
+
if (isLocalhost) {
|
|
47
|
+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data.tokens || null;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if build is stale (source files newer than build)
|
|
58
|
+
*/
|
|
59
|
+
function isBuildStale(config) {
|
|
60
|
+
const distDir = config.output.dir;
|
|
61
|
+
const componentsDir = config.components.path;
|
|
62
|
+
|
|
63
|
+
const manifestPath = join(distDir, 'manifest.json');
|
|
64
|
+
if (!existsSync(manifestPath)) {
|
|
65
|
+
return true; // No build exists
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const manifestStat = statSync(manifestPath);
|
|
70
|
+
const manifestTime = manifestStat.mtimeMs;
|
|
71
|
+
|
|
72
|
+
// Check if any component file is newer than manifest
|
|
73
|
+
const componentFiles = readdirSync(componentsDir, { recursive: true, withFileTypes: true });
|
|
74
|
+
for (const file of componentFiles) {
|
|
75
|
+
if (file.isFile() && (file.name.endsWith('.html') || file.name.endsWith('.js'))) {
|
|
76
|
+
const filePath = join(file.path, file.name);
|
|
77
|
+
const fileStat = statSync(filePath);
|
|
78
|
+
if (fileStat.mtimeMs > manifestTime) {
|
|
79
|
+
return true; // Source file is newer
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false; // Build is up to date
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return true; // On error, assume stale
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Push command - upload built components to CMS
|
|
92
|
+
* Auto-builds unless --no-build is specified
|
|
93
|
+
*/
|
|
94
|
+
export async function pushCommand(options) {
|
|
95
|
+
const config = await loadConfig();
|
|
96
|
+
const distDir = config.output.dir;
|
|
97
|
+
const manifestPath = join(distDir, 'manifest.json');
|
|
98
|
+
|
|
99
|
+
// Auto-build unless --no-build is specified
|
|
100
|
+
// Commander sets build: false when --no-build is used
|
|
101
|
+
if (options.build === false) {
|
|
102
|
+
// --no-build flag: Check if build exists
|
|
103
|
+
if (!existsSync(manifestPath)) {
|
|
104
|
+
logger.error('No build found and --no-build specified.');
|
|
105
|
+
logger.info('Run `wm build` first or remove --no-build flag.');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
logger.info('Using existing build (--no-build).');
|
|
109
|
+
} else {
|
|
110
|
+
// Auto-build mode (default)
|
|
111
|
+
const buildExists = existsSync(manifestPath);
|
|
112
|
+
const buildStale = buildExists ? isBuildStale(config) : true;
|
|
113
|
+
|
|
114
|
+
if (!buildExists) {
|
|
115
|
+
logger.info('No build found. Building components...');
|
|
116
|
+
} else if (buildStale) {
|
|
117
|
+
logger.info('Source files changed. Rebuilding components...');
|
|
118
|
+
} else {
|
|
119
|
+
logger.info('Using existing build (up to date).');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!buildExists || buildStale) {
|
|
123
|
+
const buildSpinner = ora('Building components...').start();
|
|
124
|
+
try {
|
|
125
|
+
await buildComponents({
|
|
126
|
+
outputDir: config.output.dir,
|
|
127
|
+
minify: config.output.minify || false,
|
|
128
|
+
designTokens: await loadDesignTokens()
|
|
129
|
+
});
|
|
130
|
+
buildSpinner.succeed('Build complete!');
|
|
131
|
+
} catch (error) {
|
|
132
|
+
buildSpinner.fail('Build failed');
|
|
133
|
+
logger.error(error.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
140
|
+
|
|
141
|
+
// Get target and token - prefer auth over config
|
|
142
|
+
let target;
|
|
143
|
+
let token;
|
|
144
|
+
|
|
145
|
+
// Use auth credentials if logged in (priority!)
|
|
146
|
+
if (isLoggedIn()) {
|
|
147
|
+
const auth = loadAuth();
|
|
148
|
+
target = options.target || getTenantCmsUrl();
|
|
149
|
+
token = options.token || getApiToken();
|
|
150
|
+
|
|
151
|
+
console.log('');
|
|
152
|
+
logger.info(`Using logged in project: ${pc.cyan(auth.tenant.name)}`);
|
|
153
|
+
logger.info(`Target: ${pc.cyan(target)}`);
|
|
154
|
+
} else {
|
|
155
|
+
// Not logged in - require explicit options
|
|
156
|
+
target = options.target;
|
|
157
|
+
token = options.token || process.env.CMS_TOKEN;
|
|
158
|
+
|
|
159
|
+
if (!target) {
|
|
160
|
+
logger.error('Not logged in and no target URL specified.');
|
|
161
|
+
logger.info('Either run `wm login` first or use --target option');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!token) {
|
|
166
|
+
logger.error('Not authenticated.');
|
|
167
|
+
logger.info('Run `wm login` first or set CMS_TOKEN env variable');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Determine new version
|
|
173
|
+
const currentVersion = config.version || '0.1.0';
|
|
174
|
+
let newVersion;
|
|
175
|
+
let shouldUpdateConfig = false;
|
|
176
|
+
|
|
177
|
+
if (options.force) {
|
|
178
|
+
// Force mode: Keep current version, overwrite
|
|
179
|
+
newVersion = currentVersion;
|
|
180
|
+
logger.info(`Version: ${pc.cyan(currentVersion)} ${pc.dim('(force overwrite)')}`);
|
|
181
|
+
} else if (options.patch) {
|
|
182
|
+
// Explicit patch increment
|
|
183
|
+
newVersion = incrementPatch(currentVersion);
|
|
184
|
+
shouldUpdateConfig = true;
|
|
185
|
+
logger.info(`Version: ${pc.cyan(currentVersion)} → ${pc.cyan(newVersion)} ${pc.dim('(patch)')}`);
|
|
186
|
+
} else if (options.minor) {
|
|
187
|
+
// Explicit minor increment
|
|
188
|
+
newVersion = incrementMinor(currentVersion);
|
|
189
|
+
shouldUpdateConfig = true;
|
|
190
|
+
logger.info(`Version: ${pc.cyan(currentVersion)} → ${pc.cyan(newVersion)} ${pc.dim('(minor)')}`);
|
|
191
|
+
} else if (options.major) {
|
|
192
|
+
// Explicit major increment
|
|
193
|
+
newVersion = incrementMajor(currentVersion);
|
|
194
|
+
shouldUpdateConfig = true;
|
|
195
|
+
logger.info(`Version: ${pc.cyan(currentVersion)} → ${pc.cyan(newVersion)} ${pc.dim('(major)')}`);
|
|
196
|
+
} else {
|
|
197
|
+
// Interactive prompt
|
|
198
|
+
console.log('');
|
|
199
|
+
const choice = await select({
|
|
200
|
+
message: `Current version: ${pc.cyan(currentVersion)}. How do you want to publish?`,
|
|
201
|
+
choices: [
|
|
202
|
+
{
|
|
203
|
+
name: `Patch (${pc.cyan(incrementPatch(currentVersion))}) - Bug fixes, small changes`,
|
|
204
|
+
value: 'patch',
|
|
205
|
+
description: 'Backwards compatible bug fixes'
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: `Minor (${pc.cyan(incrementMinor(currentVersion))}) - New features, backwards compatible`,
|
|
209
|
+
value: 'minor',
|
|
210
|
+
description: 'New functionality, backwards compatible'
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: `Major (${pc.cyan(incrementMajor(currentVersion))}) - Breaking changes`,
|
|
214
|
+
value: 'major',
|
|
215
|
+
description: 'Breaking changes, incompatible with previous versions'
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: `Force (${pc.cyan(currentVersion)}) - Overwrite current version ${pc.dim('(development only)')}`,
|
|
219
|
+
value: 'force',
|
|
220
|
+
description: 'Overwrite existing version without incrementing'
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (choice === 'patch') {
|
|
226
|
+
newVersion = incrementPatch(currentVersion);
|
|
227
|
+
shouldUpdateConfig = true;
|
|
228
|
+
} else if (choice === 'minor') {
|
|
229
|
+
newVersion = incrementMinor(currentVersion);
|
|
230
|
+
shouldUpdateConfig = true;
|
|
231
|
+
} else if (choice === 'major') {
|
|
232
|
+
newVersion = incrementMajor(currentVersion);
|
|
233
|
+
shouldUpdateConfig = true;
|
|
234
|
+
} else if (choice === 'force') {
|
|
235
|
+
newVersion = currentVersion;
|
|
236
|
+
shouldUpdateConfig = false;
|
|
237
|
+
options.force = true; // Enable force mode
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('');
|
|
241
|
+
logger.info(`Publishing version: ${pc.cyan(newVersion)}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Update manifest with new version
|
|
245
|
+
manifest.version = newVersion;
|
|
246
|
+
|
|
247
|
+
const spinner = ora(`Uploading to ${target}...`).start();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await uploadComponents(distDir, target, token, {
|
|
251
|
+
force: options.force || false,
|
|
252
|
+
version: newVersion
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
spinner.succeed('Upload complete!');
|
|
256
|
+
|
|
257
|
+
// Update config file with new version (only if not force mode)
|
|
258
|
+
if (shouldUpdateConfig) {
|
|
259
|
+
try {
|
|
260
|
+
updateConfigVersion(newVersion);
|
|
261
|
+
logger.success(`Updated wm.config.js with version ${pc.cyan(newVersion)}`);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
logger.warn(`Could not update config file: ${err.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.success(`\n✅ Uploaded ${manifest.components.length} components to ${target}`);
|
|
268
|
+
logger.info(`Version: ${pc.cyan(newVersion)}`);
|
|
269
|
+
logger.info('Components are now available in your CMS.');
|
|
270
|
+
} catch (error) {
|
|
271
|
+
spinner.fail('Upload failed');
|
|
272
|
+
logger.error(error.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { loadAuth, saveAuth } from '../utils/auth.js';
|
|
2
|
+
import { logger } from '../../../core/src/index.js';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { select } from '@inquirer/prompts';
|
|
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
|
+
const originalValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
14
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return await fetch(url, options);
|
|
18
|
+
} finally {
|
|
19
|
+
if (originalValue !== undefined) {
|
|
20
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalValue;
|
|
21
|
+
} else {
|
|
22
|
+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return fetch(url, options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Switch command - Switch to a different tenant/project
|
|
32
|
+
*/
|
|
33
|
+
export async function switchCommand(options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
const auth = loadAuth();
|
|
36
|
+
|
|
37
|
+
if (!auth) {
|
|
38
|
+
logger.error('Not logged in. Run ' + pc.cyan('wm login') + ' first');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log();
|
|
43
|
+
logger.info(pc.bold('Switch Project'));
|
|
44
|
+
console.log();
|
|
45
|
+
|
|
46
|
+
// Load available tenants for this user
|
|
47
|
+
logger.info('Loading available projects...');
|
|
48
|
+
|
|
49
|
+
const tenantsUrl = `${auth.baseUrl}/api/cli/tenants?userId=${auth.user.id}`;
|
|
50
|
+
const tenantsResponse = await secureFetch(tenantsUrl);
|
|
51
|
+
|
|
52
|
+
if (!tenantsResponse.ok) {
|
|
53
|
+
throw new Error('Failed to load projects from CMS');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const tenantsData = await tenantsResponse.json();
|
|
57
|
+
|
|
58
|
+
if (!tenantsData.tenants || tenantsData.tenants.length === 0) {
|
|
59
|
+
logger.error('No projects found');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Show current project
|
|
64
|
+
const currentTenant = auth.tenant;
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(pc.gray(' Current: ') + pc.cyan(`${currentTenant.name} (${currentTenant.subdomain})`));
|
|
67
|
+
console.log();
|
|
68
|
+
|
|
69
|
+
// Let user select a different tenant
|
|
70
|
+
const selectedTenantId = await select({
|
|
71
|
+
message: 'Switch to:',
|
|
72
|
+
choices: tenantsData.tenants.map(t => ({
|
|
73
|
+
name: `${t.name} (${t.subdomain})`,
|
|
74
|
+
value: t.id,
|
|
75
|
+
description: t.id === currentTenant.id ? 'Current project' : `Created ${new Date(t.createdAt).toLocaleDateString('de-DE')}`
|
|
76
|
+
}))
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const selectedTenant = tenantsData.tenants.find(t => t.id === selectedTenantId);
|
|
80
|
+
|
|
81
|
+
// If same tenant, no need to switch
|
|
82
|
+
if (selectedTenant.id === currentTenant.id) {
|
|
83
|
+
console.log();
|
|
84
|
+
logger.info('Already using this project');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Generate new API token for selected tenant
|
|
89
|
+
console.log();
|
|
90
|
+
logger.info('Generating API token...');
|
|
91
|
+
|
|
92
|
+
const tokenUrl = `${auth.baseUrl}/api/cli/tenants`;
|
|
93
|
+
const tokenResponse = await secureFetch(tokenUrl, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
userId: auth.user.id,
|
|
98
|
+
tenantId: selectedTenantId
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!tokenResponse.ok) {
|
|
103
|
+
throw new Error('Failed to generate API token');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tokenData = await tokenResponse.json();
|
|
107
|
+
|
|
108
|
+
// Update auth with new tenant and token
|
|
109
|
+
const newAuth = {
|
|
110
|
+
...auth,
|
|
111
|
+
tenant: selectedTenant,
|
|
112
|
+
apiToken: tokenData.apiToken
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
saveAuth(newAuth);
|
|
116
|
+
|
|
117
|
+
console.log();
|
|
118
|
+
logger.success(pc.green('✨ Switched to project: ') + pc.cyan(selectedTenant.name));
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(` ${pc.bold('Subdomain:')} ${pc.cyan(selectedTenant.subdomain)}`);
|
|
121
|
+
console.log(` ${pc.bold('CMS URL:')} ${pc.cyan(`${new URL(auth.baseUrl).protocol}//${selectedTenant.subdomain}.cms.${new URL(auth.baseUrl).host}`)}`);
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(pc.yellow('⚠ ') + pc.gray('If you have ') + pc.cyan('wm dev') + pc.gray(' running, please restart it to load the new project.'));
|
|
124
|
+
console.log();
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.log();
|
|
128
|
+
logger.error(`Switch failed: ${error.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/index.js
ADDED