@webmate-studio/cli 0.3.62 → 0.4.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/README.md +222 -0
- package/bin/wm.mjs +208 -0
- package/package.json +5 -1
- package/src/commands/build.js +335 -0
- package/src/commands/clone.js +414 -0
- package/src/commands/components.js +101 -0
- package/src/commands/core.js +1039 -0
- package/src/commands/doctor.js +192 -0
- package/src/commands/install.js +312 -0
- package/src/commands/login.js +158 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/projects.js +91 -0
- package/src/commands/pull.js +192 -0
- package/src/commands/push.js +231 -0
- package/src/commands/reset.js +118 -0
- package/src/commands/status.js +118 -0
- package/src/commands/versions.js +130 -0
- package/src/commands/whoami.js +64 -0
- package/src/utils/api-client.js +131 -0
- package/src/utils/auth-resolver.js +145 -0
- package/src/utils/auth-storage.js +104 -0
- package/src/utils/component-files.js +195 -0
- package/src/utils/device-flow.js +111 -0
- package/src/utils/git-snapshot.js +63 -0
- package/src/utils/tenant-api.js +103 -0
- package/src/utils/webmate-meta.js +75 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
3
|
+
import { confirm } from '@inquirer/prompts';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { logger } from '@webmate-studio/core';
|
|
7
|
+
import { apiFetch, ApiError } from '../utils/api-client.js';
|
|
8
|
+
import { resolveAuth } from '../utils/auth-resolver.js';
|
|
9
|
+
import { writeMeta } from '../utils/webmate-meta.js';
|
|
10
|
+
import {
|
|
11
|
+
writeComponentFiles,
|
|
12
|
+
computeFileHashesFromMap
|
|
13
|
+
} from '../utils/component-files.js';
|
|
14
|
+
import { autoSnapshot } from '../utils/git-snapshot.js';
|
|
15
|
+
|
|
16
|
+
function slugify(value) {
|
|
17
|
+
return (value || '')
|
|
18
|
+
.toString()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
21
|
+
.replace(/^-+|-+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_WM_CONFIG = `export default {
|
|
25
|
+
components: {
|
|
26
|
+
path: './components',
|
|
27
|
+
styles: ['./tokens/tokens.css', './styles/base.css'],
|
|
28
|
+
fonts: [],
|
|
29
|
+
islands: {
|
|
30
|
+
path: './islands',
|
|
31
|
+
framework: 'lit'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
preview: {
|
|
35
|
+
port: 5173,
|
|
36
|
+
theme: 'light',
|
|
37
|
+
viewport: { width: 1440, height: 900 }
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
function parseSource(spec) {
|
|
43
|
+
if (!spec) return null;
|
|
44
|
+
const at = spec.lastIndexOf('@');
|
|
45
|
+
if (at < 0) return { sourceComponentId: spec, sourceVersion: null };
|
|
46
|
+
return {
|
|
47
|
+
sourceComponentId: spec.slice(0, at),
|
|
48
|
+
sourceVersion: spec.slice(at + 1) || null
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveTargetDir(arg) {
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
return arg ? resolve(cwd, arg) : cwd;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function resolveSourceVersionId({ sourceComponentId, sourceVersion }) {
|
|
58
|
+
if (!sourceVersion || sourceVersion === 'latest') return null;
|
|
59
|
+
if (/^cm[0-9a-z]{20,}$/i.test(sourceVersion)) return sourceVersion;
|
|
60
|
+
const list = await apiFetch(
|
|
61
|
+
`/api/organization/components/${sourceComponentId}/versions`
|
|
62
|
+
);
|
|
63
|
+
const versions = list?.versions ?? [];
|
|
64
|
+
const match = versions.find((v) => v.version === sourceVersion);
|
|
65
|
+
if (!match) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Version "${sourceVersion}" not found among ${versions.length} versions of ${sourceComponentId}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return match.id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function cloneCommand(projectArgs, options = {}) {
|
|
74
|
+
// `wm clone foo bar baz` arrives as ['foo', 'bar', 'baz'] thanks to the
|
|
75
|
+
// variadic positional. Join with spaces so project names with whitespace
|
|
76
|
+
// work without forcing the user to quote them.
|
|
77
|
+
const projectArg = Array.isArray(projectArgs) ? projectArgs.join(' ').trim() : (projectArgs ?? '').trim();
|
|
78
|
+
|
|
79
|
+
// Two modes share the verb:
|
|
80
|
+
// * `wm clone <project>` — project bootstrap (this file, below)
|
|
81
|
+
// * `wm clone --from <uuid>` — single-component clone (legacy, server-side)
|
|
82
|
+
// We can't use both at once.
|
|
83
|
+
if (projectArg && options.from) {
|
|
84
|
+
logger.error('Cannot combine positional <project> with --from. Use one mode at a time.');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
if (!projectArg && !options.from) {
|
|
88
|
+
logger.error('Either a project name/id or --from <component-uuid>[@<version>] is required.');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (projectArg) {
|
|
93
|
+
return cloneProjectCommand(projectArg, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return cloneComponentCommand(options);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function cloneComponentCommand(options = {}) {
|
|
100
|
+
const parsed = parseSource(options.from);
|
|
101
|
+
if (!parsed?.sourceComponentId) {
|
|
102
|
+
logger.error(`Invalid --from value: ${options.from}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const targetDir = resolveTargetDir(options.to);
|
|
107
|
+
|
|
108
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.force) {
|
|
109
|
+
const proceed = await confirm({
|
|
110
|
+
message: `Target ${pc.cyan(targetDir)} is not empty. Continue and overwrite?`,
|
|
111
|
+
default: false
|
|
112
|
+
});
|
|
113
|
+
if (!proceed) {
|
|
114
|
+
logger.info('Clone cancelled.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let sourceVersionId;
|
|
120
|
+
try {
|
|
121
|
+
sourceVersionId = await resolveSourceVersionId({
|
|
122
|
+
sourceComponentId: parsed.sourceComponentId,
|
|
123
|
+
sourceVersion: parsed.sourceVersion
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err instanceof ApiError) {
|
|
127
|
+
logger.error(`Could not list source versions (HTTP ${err.status}): ${err.message}`);
|
|
128
|
+
} else {
|
|
129
|
+
logger.error(err.message);
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const spinner = ora('Cloning component on server…').start();
|
|
135
|
+
let result;
|
|
136
|
+
try {
|
|
137
|
+
const body = {
|
|
138
|
+
sourceComponentId: parsed.sourceComponentId
|
|
139
|
+
};
|
|
140
|
+
if (sourceVersionId) body.sourceVersionId = sourceVersionId;
|
|
141
|
+
if (options.targetOrg) body.targetOrganizationSlug = options.targetOrg;
|
|
142
|
+
if (options.targetOrgId) body.targetOrganizationId = options.targetOrgId;
|
|
143
|
+
if (options.name) body.targetName = options.name;
|
|
144
|
+
if (options.category) body.targetCategory = options.category;
|
|
145
|
+
|
|
146
|
+
result = await apiFetch('/api/organization/components/clone', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
body
|
|
149
|
+
});
|
|
150
|
+
spinner.succeed(
|
|
151
|
+
`Cloned to new component ${pc.cyan(result.newComponentId)} ` +
|
|
152
|
+
`${pc.dim('(org ' + result.newOrganizationId + ')')}`
|
|
153
|
+
);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (err instanceof ApiError) {
|
|
156
|
+
spinner.fail(`Clone failed (HTTP ${err.status}): ${err.message}`);
|
|
157
|
+
} else {
|
|
158
|
+
spinner.fail(err.message);
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const files = result?.files ?? {};
|
|
164
|
+
if (!Object.keys(files).length) {
|
|
165
|
+
logger.warn('Server returned no files. Aborting before touching disk.');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
mkdirSync(targetDir, { recursive: true });
|
|
170
|
+
writeComponentFiles(targetDir, files, { clean: !options.merge });
|
|
171
|
+
|
|
172
|
+
const fileHashes = computeFileHashesFromMap(files);
|
|
173
|
+
const cloneAuth = resolveAuth();
|
|
174
|
+
const meta = writeMeta(targetDir, {
|
|
175
|
+
componentId: result.newComponentId,
|
|
176
|
+
baseVersion: null,
|
|
177
|
+
version: null,
|
|
178
|
+
pulledAt: new Date().toISOString(),
|
|
179
|
+
fileHashes,
|
|
180
|
+
originRepositoryId: cloneAuth?.repositoryId ?? null
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
logger.success(`Wrote ${Object.keys(files).length} file(s) to ${pc.dim(targetDir)}`);
|
|
184
|
+
console.log(` New component ID: ${pc.cyan(result.newComponentId)}`);
|
|
185
|
+
console.log(` Target org: ${pc.dim(result.newOrganizationId)}`);
|
|
186
|
+
console.log(` Source: ${pc.dim(result.source.componentId + '@' + result.source.version)}`);
|
|
187
|
+
console.log(` Manifest: ${pc.dim(meta && targetDir + '/.webmate.json')}`);
|
|
188
|
+
|
|
189
|
+
const snap = await autoSnapshot(
|
|
190
|
+
targetDir,
|
|
191
|
+
`wm clone from ${result.source.componentId}@${result.source.version}`,
|
|
192
|
+
{ skip: options.noGit }
|
|
193
|
+
);
|
|
194
|
+
if (snap.committed) {
|
|
195
|
+
console.log(` Git snapshot: ${pc.dim(snap.sha)}`);
|
|
196
|
+
} else if (snap.skipped && snap.reason === 'no-git') {
|
|
197
|
+
console.log(` Git snapshot: ${pc.dim('skipped (no .git)')}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log();
|
|
201
|
+
console.log(pc.bold('Next step:') + ' run ' + pc.cyan(`wm push ${basename(targetDir)} --force`));
|
|
202
|
+
console.log(pc.dim(' (--force is needed for the very first push because there is no baseVersion yet)'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Walk up from `start` (inclusive) toward the filesystem root. Returns the
|
|
206
|
+
// first ancestor that looks like an existing webmate workspace (has a
|
|
207
|
+
// wm.config.js or a workspace-shaped .webmate/config.json), or null if
|
|
208
|
+
// there is none. Used to stop `wm clone <project>` from creating a
|
|
209
|
+
// sub-workspace inside an existing one — which is the classic footgun
|
|
210
|
+
// that pollutes the parent's components/ tree with another full workspace.
|
|
211
|
+
//
|
|
212
|
+
// Workspace vs. global config: `~/.webmate/config.json` is also created by
|
|
213
|
+
// the CLI for global state (just { cmsUrl: ... }) and must not be treated
|
|
214
|
+
// as a workspace marker — otherwise running `wm clone` from $HOME refuses
|
|
215
|
+
// with a misleading "you're inside a workspace" error. We disambiguate by
|
|
216
|
+
// peeking at the file: only configs that carry a `repositoryId` (or, for
|
|
217
|
+
// freshly initialised but not-yet-bound workspaces, a `componentsPath`)
|
|
218
|
+
// are real workspaces.
|
|
219
|
+
function isWorkspaceConfigFile(path) {
|
|
220
|
+
try {
|
|
221
|
+
const raw = readFileSync(path, 'utf-8');
|
|
222
|
+
const data = JSON.parse(raw);
|
|
223
|
+
if (!data || typeof data !== 'object') return false;
|
|
224
|
+
return Boolean(data.repositoryId || data.componentsPath);
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function findEnclosingWorkspace(start) {
|
|
230
|
+
let dir = start;
|
|
231
|
+
while (true) {
|
|
232
|
+
if (existsSync(join(dir, 'wm.config.js'))) return dir;
|
|
233
|
+
const cfg = join(dir, '.webmate', 'config.json');
|
|
234
|
+
if (existsSync(cfg) && isWorkspaceConfigFile(cfg)) return dir;
|
|
235
|
+
const parent = dirname(dir);
|
|
236
|
+
if (parent === dir) return null;
|
|
237
|
+
dir = parent;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function cloneProjectCommand(projectArg, options = {}) {
|
|
242
|
+
// Refuse to nest workspaces. If the user invokes `wm clone <project>`
|
|
243
|
+
// from inside an existing workspace (or anywhere under its tree —
|
|
244
|
+
// most commonly inside its components/ folder), the resulting clone
|
|
245
|
+
// would land as a sub-workspace and every parent-walk component
|
|
246
|
+
// scanner would happily fold its contents into the outer workspace.
|
|
247
|
+
// We prefer a hard stop with a clear pointer to a safer location
|
|
248
|
+
// over the silent disaster.
|
|
249
|
+
const enclosing = findEnclosingWorkspace(process.cwd());
|
|
250
|
+
if (enclosing && !options.force) {
|
|
251
|
+
logger.error(
|
|
252
|
+
`Refusing to clone into a nested workspace.\n` +
|
|
253
|
+
` Current directory is inside the workspace at ${pc.cyan(enclosing)}.\n` +
|
|
254
|
+
` Run \`wm clone\` from a folder that is not itself a workspace — typically the parent:\n` +
|
|
255
|
+
` ${pc.dim('cd ' + dirname(enclosing))}\n` +
|
|
256
|
+
` ${pc.cyan('wm clone ' + projectArg)}\n` +
|
|
257
|
+
` Or pass ${pc.bold('--force')} if you really mean to nest.`
|
|
258
|
+
);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 1. Resolve the project (= ComponentRepository) — accept id or name.
|
|
263
|
+
const lookup = ora(`Looking up project ${pc.cyan(projectArg)}…`).start();
|
|
264
|
+
let repo;
|
|
265
|
+
try {
|
|
266
|
+
const res = await apiFetch('/api/organization/repositories');
|
|
267
|
+
const repos = Array.isArray(res?.repositories) ? res.repositories : [];
|
|
268
|
+
repo = repos.find(r => r.id === projectArg || r.name === projectArg);
|
|
269
|
+
if (!repo) {
|
|
270
|
+
lookup.fail(`Project "${projectArg}" not found in your organization.`);
|
|
271
|
+
console.log(pc.dim(' Available projects:'));
|
|
272
|
+
for (const r of repos) {
|
|
273
|
+
console.log(` ${pc.cyan(r.name)} ${pc.dim('(' + r.id + ')')}`);
|
|
274
|
+
}
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
lookup.succeed(`Project ${pc.cyan(repo.name)} ${pc.dim('(' + repo.id + ')')}`);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err instanceof ApiError) {
|
|
280
|
+
lookup.fail(`Could not list projects (HTTP ${err.status}): ${err.message}`);
|
|
281
|
+
} else {
|
|
282
|
+
lookup.fail(err.message);
|
|
283
|
+
}
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 2. Decide target directory.
|
|
288
|
+
const dirName = options.to ? options.to : slugify(repo.name);
|
|
289
|
+
const targetDir = resolve(process.cwd(), dirName);
|
|
290
|
+
|
|
291
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.force) {
|
|
292
|
+
const proceed = await confirm({
|
|
293
|
+
message: `Target ${pc.cyan(targetDir)} is not empty. Continue and overwrite?`,
|
|
294
|
+
default: false
|
|
295
|
+
});
|
|
296
|
+
if (!proceed) {
|
|
297
|
+
logger.info('Clone cancelled.');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 3. List components in this repo (filter client-side from the org list).
|
|
303
|
+
const compsSpinner = ora('Listing components…').start();
|
|
304
|
+
let components;
|
|
305
|
+
try {
|
|
306
|
+
const res = await apiFetch('/api/organization/components');
|
|
307
|
+
const all = Array.isArray(res?.components) ? res.components : [];
|
|
308
|
+
components = all.filter(c => c.repository?.id === repo.id || c.repositoryId === repo.id);
|
|
309
|
+
compsSpinner.succeed(
|
|
310
|
+
`Found ${pc.cyan(components.length)} component(s) in ${pc.cyan(repo.name)}`
|
|
311
|
+
);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err instanceof ApiError) {
|
|
314
|
+
compsSpinner.fail(`Could not list components (HTTP ${err.status}): ${err.message}`);
|
|
315
|
+
} else {
|
|
316
|
+
compsSpinner.fail(err.message);
|
|
317
|
+
}
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 4. Lay out the workspace skeleton.
|
|
322
|
+
mkdirSync(targetDir, { recursive: true });
|
|
323
|
+
mkdirSync(join(targetDir, 'components'), { recursive: true });
|
|
324
|
+
|
|
325
|
+
const configPath = join(targetDir, 'wm.config.js');
|
|
326
|
+
if (!existsSync(configPath)) {
|
|
327
|
+
writeFileSync(configPath, DEFAULT_WM_CONFIG, 'utf-8');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const auth = resolveAuth();
|
|
331
|
+
const dotWebmateDir = join(targetDir, '.webmate');
|
|
332
|
+
mkdirSync(dotWebmateDir, { recursive: true });
|
|
333
|
+
const workspaceConfig = {
|
|
334
|
+
organizationId: auth?.organizationId ?? null,
|
|
335
|
+
organizationSlug: auth?.organizationSlug ?? null,
|
|
336
|
+
organizationName: auth?.organizationName ?? null,
|
|
337
|
+
repositoryId: repo.id,
|
|
338
|
+
repositoryName: repo.name,
|
|
339
|
+
clonedAt: new Date().toISOString()
|
|
340
|
+
};
|
|
341
|
+
writeFileSync(
|
|
342
|
+
join(dotWebmateDir, 'config.json'),
|
|
343
|
+
JSON.stringify(workspaceConfig, null, 2) + '\n',
|
|
344
|
+
'utf-8'
|
|
345
|
+
);
|
|
346
|
+
logger.success(`Workspace scaffolded at ${pc.dim(targetDir)}`);
|
|
347
|
+
|
|
348
|
+
// 5. Pull source for every published component.
|
|
349
|
+
let pulled = 0;
|
|
350
|
+
let skipped = 0;
|
|
351
|
+
for (const comp of components) {
|
|
352
|
+
const versionId =
|
|
353
|
+
comp.latestVersion && typeof comp.latestVersion === 'object'
|
|
354
|
+
? comp.latestVersion.id
|
|
355
|
+
: null;
|
|
356
|
+
const versionLabel =
|
|
357
|
+
comp.latestVersion && typeof comp.latestVersion === 'object'
|
|
358
|
+
? comp.latestVersion.version
|
|
359
|
+
: typeof comp.latestVersion === 'string'
|
|
360
|
+
? comp.latestVersion
|
|
361
|
+
: null;
|
|
362
|
+
const compSlug = slugify(comp.name) || slugify(comp.displayName) || comp.id.slice(0, 8);
|
|
363
|
+
const compDir = join(targetDir, 'components', compSlug);
|
|
364
|
+
|
|
365
|
+
if (!versionId) {
|
|
366
|
+
logger.warn(` · ${comp.name}: no published version — skipped`);
|
|
367
|
+
skipped++;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const pullSpinner = ora(`Pulling ${comp.name} ${pc.dim('v' + versionLabel)}…`).start();
|
|
372
|
+
try {
|
|
373
|
+
const source = await apiFetch(
|
|
374
|
+
`/api/organization/components/${comp.id}/versions/${versionId}/source`
|
|
375
|
+
);
|
|
376
|
+
const files = source?.files ?? {};
|
|
377
|
+
if (!Object.keys(files).length) {
|
|
378
|
+
pullSpinner.warn(`${comp.name}: empty source bundle — skipped`);
|
|
379
|
+
skipped++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
mkdirSync(compDir, { recursive: true });
|
|
383
|
+
writeComponentFiles(compDir, files, { clean: true });
|
|
384
|
+
const fileHashes = computeFileHashesFromMap(files);
|
|
385
|
+
writeMeta(compDir, {
|
|
386
|
+
componentId: comp.id,
|
|
387
|
+
baseVersion: versionId,
|
|
388
|
+
version: versionLabel,
|
|
389
|
+
pulledAt: new Date().toISOString(),
|
|
390
|
+
fileHashes,
|
|
391
|
+
originRepositoryId: repo.id
|
|
392
|
+
});
|
|
393
|
+
pullSpinner.succeed(`${comp.name} ${pc.dim('v' + versionLabel)}`);
|
|
394
|
+
pulled++;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (err instanceof ApiError) {
|
|
397
|
+
pullSpinner.fail(`${comp.name}: HTTP ${err.status} — ${err.message}`);
|
|
398
|
+
} else {
|
|
399
|
+
pullSpinner.fail(`${comp.name}: ${err.message}`);
|
|
400
|
+
}
|
|
401
|
+
skipped++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 6. Recap + next steps.
|
|
406
|
+
console.log();
|
|
407
|
+
logger.success(
|
|
408
|
+
`Cloned project ${pc.cyan(repo.name)} — ${pulled} component(s) pulled, ${skipped} skipped`
|
|
409
|
+
);
|
|
410
|
+
console.log();
|
|
411
|
+
console.log(pc.bold('Next steps:'));
|
|
412
|
+
console.log(` ${pc.cyan('cd ' + dirName)}`);
|
|
413
|
+
console.log(` ${pc.cyan('wm dev')}`);
|
|
414
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { logger } from '@webmate-studio/core';
|
|
4
|
+
import { apiFetch, ApiError } from '../utils/api-client.js';
|
|
5
|
+
import { tenantApiFetch, resolveTenantSubdomain } from '../utils/tenant-api.js';
|
|
6
|
+
|
|
7
|
+
function buildQuery(opts) {
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
if (opts.org) params.set('orgSlug', opts.org);
|
|
10
|
+
if (opts.orgId) params.set('orgId', opts.orgId);
|
|
11
|
+
if (opts.category) params.set('category', opts.category);
|
|
12
|
+
if (opts.search) params.set('search', opts.search);
|
|
13
|
+
const qs = params.toString();
|
|
14
|
+
return qs ? `?${qs}` : '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function truncate(value, n) {
|
|
18
|
+
const s = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
19
|
+
if (!s) return '';
|
|
20
|
+
if (s.length <= n) return s;
|
|
21
|
+
return s.slice(0, n - 1) + '…';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function latestVersionLabel(c) {
|
|
25
|
+
const lv = c.latestVersion;
|
|
26
|
+
if (lv && typeof lv === 'object') return lv.version ?? '—';
|
|
27
|
+
if (typeof lv === 'string') return lv;
|
|
28
|
+
return '—';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function componentsListCommand(options = {}) {
|
|
32
|
+
const query = buildQuery(options);
|
|
33
|
+
const spinner = ora('Fetching components…').start();
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
// Tenant-Modus: --org wird ignoriert (Components leben nun pro Website),
|
|
37
|
+
// dafür liest der Endpoint die Website aus dem Auth-Tenant-Kontext.
|
|
38
|
+
// Legacy: org-weite Liste mit ?orgSlug=… wie bisher.
|
|
39
|
+
const sub = resolveTenantSubdomain(null);
|
|
40
|
+
if (sub) {
|
|
41
|
+
response = await tenantApiFetch(`/api/tenant-components`, {}, { tenantSubdomain: sub });
|
|
42
|
+
} else {
|
|
43
|
+
response = await apiFetch(`/api/organization/components${query}`);
|
|
44
|
+
}
|
|
45
|
+
spinner.stop();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err instanceof ApiError) {
|
|
48
|
+
spinner.fail(`Fetch failed (HTTP ${err.status}): ${err.message}`);
|
|
49
|
+
} else {
|
|
50
|
+
spinner.fail(err.message);
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const components =
|
|
56
|
+
response?.components ??
|
|
57
|
+
response?.data ??
|
|
58
|
+
(Array.isArray(response) ? response : []);
|
|
59
|
+
|
|
60
|
+
if (!Array.isArray(components) || components.length === 0) {
|
|
61
|
+
logger.info(
|
|
62
|
+
options.org
|
|
63
|
+
? `No components in organization "${options.org}".`
|
|
64
|
+
: 'No components in current organization.'
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const orgLabel = options.org ?? pc.dim('(current org)');
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(pc.bold(`${components.length} component(s) in ${orgLabel}`));
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
const ID_W = 36;
|
|
75
|
+
const NAME_W = 30;
|
|
76
|
+
const CAT_W = 18;
|
|
77
|
+
const VER_W = 12;
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
pc.dim(
|
|
81
|
+
`${'UUID'.padEnd(ID_W)} ${'Name'.padEnd(NAME_W)} ${'Category'.padEnd(CAT_W)} ${'Latest'.padEnd(VER_W)}`
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
console.log(
|
|
85
|
+
pc.dim('-'.repeat(ID_W) + ' ' + '-'.repeat(NAME_W) + ' ' + '-'.repeat(CAT_W) + ' ' + '-'.repeat(VER_W))
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
for (const c of components) {
|
|
89
|
+
const id = c.id ?? '';
|
|
90
|
+
const name = truncate(c.displayName ?? c.name ?? '', NAME_W);
|
|
91
|
+
const cat = truncate(c.category ?? '', CAT_W);
|
|
92
|
+
const ver = truncate(latestVersionLabel(c), VER_W);
|
|
93
|
+
const dep = c.deprecated ? pc.yellow(' (deprecated)') : '';
|
|
94
|
+
console.log(
|
|
95
|
+
`${pc.dim(String(id).padEnd(ID_W))} ${name.padEnd(NAME_W)} ${pc.dim(cat.padEnd(CAT_W))} ${pc.cyan(ver.padEnd(VER_W))}${dep}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(pc.dim(` wm clone --from <uuid>[@<ver>] --to <dir>`));
|
|
101
|
+
}
|