@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,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
@@ -0,0 +1,4 @@
1
+ export { buildCommand } from './commands/build.js';
2
+ export { pushCommand } from './commands/push.js';
3
+ export { devCommand } from './commands/dev.js';
4
+ export { initCommand } from './commands/init.js';