@qmuse/qmuse-cli 1.0.0-dev.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/dist/cli.js ADDED
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * QMuse CLI —— 将本地 React 项目导入 QMuse。
4
+ *
5
+ * 用法见 `qmuse help`。
6
+ */
7
+ import { applyEnvironment, loadConfig, saveConfig, findConfigPath } from './config.js';
8
+ import { hasAuth } from './client.js';
9
+ import { log } from './logger.js';
10
+ import * as api from './api.js';
11
+ import * as login from './login.js';
12
+ import { prompt } from './login.js';
13
+ import { readFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { createImportArchive } from './import/archive.js';
16
+ import { configureAppImport, getAppOpenUrl, getImportStatusUrl, queryAppImportStatus, startAppImport, uploadImportArchive, } from './import/api.js';
17
+ import { pollAppImport } from './import/poll.js';
18
+ import { preflightImport } from './import/preflight.js';
19
+ import { clearPendingImport, completeManagedImport, loadManagedProject, saveManagedProject, setPendingImport, } from './import/project.js';
20
+ import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, } from './import/diagnostic.js';
21
+ import { checkCloudInit, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
22
+ import { QMUSE_CLI_VERSION } from './version.js';
23
+ // ---------------- 参数解析 ----------------
24
+ const BOOL_FLAGS = new Set([
25
+ '--help', '-h', '--version', '-V', '--json', '--status',
26
+ '--log', '--no-open', '--wait',
27
+ '--acknowledge-empty-cloud-data',
28
+ '--confirm-cloud-service',
29
+ ]);
30
+ function parseArgs(argv) {
31
+ const command = [];
32
+ const flags = {};
33
+ for (let i = 0; i < argv.length; i++) {
34
+ const tok = argv[i];
35
+ if (tok.startsWith('--') || tok.startsWith('-')) {
36
+ const eq = tok.indexOf('=');
37
+ if (eq >= 0) {
38
+ const key = tok.slice(0, eq);
39
+ flags[key] = tok.slice(eq + 1);
40
+ continue;
41
+ }
42
+ if (BOOL_FLAGS.has(tok)) {
43
+ flags[tok] = true;
44
+ continue;
45
+ }
46
+ const next = argv[i + 1];
47
+ if (next === undefined || next.startsWith('--')) {
48
+ flags[tok] = true;
49
+ }
50
+ else {
51
+ flags[tok] = next;
52
+ i++;
53
+ }
54
+ }
55
+ else {
56
+ command.push(tok);
57
+ }
58
+ }
59
+ return { command, flags };
60
+ }
61
+ const str = (f, k) => {
62
+ const v = f[k];
63
+ return typeof v === 'string' ? v : undefined;
64
+ };
65
+ const num = (f, key, def) => {
66
+ const v = str(f, key);
67
+ const n = v === undefined ? def : Number(v);
68
+ return Number.isFinite(n) ? n : def;
69
+ };
70
+ /** 仅允许使用内置环境,避免命令行参数把登录凭证重定向到任意地址。 */
71
+ function resolveCfg(flags) {
72
+ let cfg = loadConfig();
73
+ const environment = str(flags, '--env');
74
+ if (environment) {
75
+ if (!['prod', 'pre', 'local'].includes(environment)) {
76
+ fail('--env 仅支持 prod / pre / local');
77
+ }
78
+ cfg = applyEnvironment(cfg, environment);
79
+ }
80
+ // --space-id 覆盖当前空间
81
+ const sp = str(flags, '--space-id');
82
+ if (sp)
83
+ cfg.spaceId = sp;
84
+ return cfg;
85
+ }
86
+ /** 统一打印结果(code cat/diff 走纯文本,其余走 JSON)。 */
87
+ function output(value, flags) {
88
+ if (typeof value === 'string') {
89
+ process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
90
+ return;
91
+ }
92
+ const json = flags['--json'] === true ? JSON.stringify(value, null, 2) : JSON.stringify(value);
93
+ process.stdout.write(`${json}\n`);
94
+ }
95
+ function fail(message) {
96
+ log.error(message);
97
+ process.exit(1);
98
+ }
99
+ function requireAuth(cfg) {
100
+ if (!hasAuth(cfg)) {
101
+ fail('当前环境未登录。请先运行 `qmuse login`。');
102
+ }
103
+ }
104
+ function readPublicConfig(value) {
105
+ if (!value)
106
+ return {};
107
+ const raw = value.startsWith('@')
108
+ ? readFileSync(value.slice(1), 'utf8')
109
+ : value;
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(raw);
113
+ }
114
+ catch (error) {
115
+ throw new Error(`--public-config 不是有效 JSON:${error instanceof Error ? error.message : String(error)}`);
116
+ }
117
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
118
+ throw new Error('--public-config 必须是字符串键值对象');
119
+ }
120
+ const config = {};
121
+ for (const [key, item] of Object.entries(parsed)) {
122
+ if (typeof item !== 'string') {
123
+ throw new Error(`--public-config 的 ${key} 必须是字符串`);
124
+ }
125
+ config[key] = item;
126
+ }
127
+ return config;
128
+ }
129
+ function parseImportCloudMode(value) {
130
+ const normalized = (value || 'AUTO').toUpperCase();
131
+ if (!['AUTO', 'EXTERNAL', 'QMUSE'].includes(normalized)) {
132
+ throw new Error('--cloud-mode 仅支持 AUTO / EXTERNAL / QMUSE');
133
+ }
134
+ return normalized;
135
+ }
136
+ function printImportStatus(status, flags) {
137
+ if (flags['--json'] === true) {
138
+ output(status, flags);
139
+ return;
140
+ }
141
+ log.info(`${status.status}${status.stage ? ` / ${status.stage}` : ''}${status.message ? `:${status.message}` : ''}`);
142
+ if (status.failureDiagnostic) {
143
+ log.warn(`失败分类:${status.failureDiagnostic.category}`);
144
+ log.warn(status.failureDiagnostic.summary);
145
+ const items = Array.isArray(status.failureDiagnostic.items)
146
+ ? status.failureDiagnostic.items
147
+ : [];
148
+ for (const item of items.slice(0, 10)) {
149
+ const location = item.file
150
+ ? `${item.file}${item.line ? `:${item.line}${item.column ? `:${item.column}` : ''}` : ''}`
151
+ : '';
152
+ log.warn(`${location ? `${location} ` : ''}${item.code ? `${item.code} ` : ''}${item.message}`);
153
+ }
154
+ const mismatch = status.failureDiagnostic.sourceMismatch;
155
+ if (mismatch) {
156
+ log.warn(`源码指纹:期望 ${mismatch.expectedHash ?? '(未知)'},实际 ${mismatch.actualHash ?? '(未知)'}`);
157
+ for (const [label, paths] of [
158
+ ['缺少文件', mismatch.missingFiles],
159
+ ['多余文件', mismatch.unexpectedFiles],
160
+ ['内容变化', mismatch.changedFiles],
161
+ ]) {
162
+ if (paths.length)
163
+ log.warn(`${label}:${paths.join(', ')}`);
164
+ }
165
+ }
166
+ }
167
+ else if (status.diagnostic) {
168
+ log.warn(status.diagnostic);
169
+ }
170
+ }
171
+ async function pollImportWithLogs(jobId, cfg, flags) {
172
+ const abortController = new AbortController();
173
+ const onInterrupt = () => abortController.abort();
174
+ process.once('SIGINT', onInterrupt);
175
+ let lastStage = '';
176
+ try {
177
+ return await pollAppImport(() => queryAppImportStatus(jobId, cfg), {
178
+ signal: abortController.signal,
179
+ onStatus: status => {
180
+ if (flags['--json'] === true)
181
+ return;
182
+ const stage = `${status.status}:${status.stage ?? ''}`;
183
+ if (stage === lastStage)
184
+ return;
185
+ lastStage = stage;
186
+ log.info(status.message || status.stage || status.status);
187
+ },
188
+ });
189
+ }
190
+ finally {
191
+ process.removeListener('SIGINT', onInterrupt);
192
+ }
193
+ }
194
+ async function resolveImportConfiguration(status, cfg, flags) {
195
+ if (status.stage === 'QMUSE_CLOUD_ADAPTATION_REQUIRED')
196
+ return undefined;
197
+ const interactive = process.stdin.isTTY === true && flags['--json'] !== true;
198
+ let cloudMode = parseImportCloudMode(str(flags, '--cloud-mode'));
199
+ if (cloudMode === 'AUTO') {
200
+ if (!interactive)
201
+ return undefined;
202
+ const answer = (await prompt('检测到云服务依赖,保留外部服务还是使用 QMuse 云服务?[external/qmuse]:'))
203
+ .trim().toLowerCase();
204
+ cloudMode = answer === 'qmuse' ? 'QMUSE' : 'EXTERNAL';
205
+ }
206
+ const publicConfig = readPublicConfig(str(flags, '--public-config'));
207
+ if (cloudMode === 'EXTERNAL') {
208
+ for (const key of status.requiredConfigKeys ?? []) {
209
+ if (publicConfig[key]?.trim())
210
+ continue;
211
+ if (!interactive)
212
+ return undefined;
213
+ publicConfig[key] = await prompt(`请输入公开构建配置 ${key}:`);
214
+ if (!publicConfig[key]?.trim())
215
+ return undefined;
216
+ }
217
+ }
218
+ let acknowledgeEmptyCloudData = flags['--acknowledge-empty-cloud-data'] === true;
219
+ let confirmCloudService = flags['--confirm-cloud-service'] === true;
220
+ if (cloudMode === 'QMUSE' && status.sourceScenario === 'QMUSE_TEMPLATE') {
221
+ if (!confirmCloudService) {
222
+ if (!interactive)
223
+ return undefined;
224
+ const answer = await prompt('确认开通并同步源码声明的 QMuse 云服务资源?[y/N]:');
225
+ confirmCloudService = /^y(?:es)?$/iu.test(answer);
226
+ if (!confirmCloudService)
227
+ return undefined;
228
+ }
229
+ }
230
+ else if (cloudMode === 'QMUSE' && !acknowledgeEmptyCloudData) {
231
+ if (!interactive)
232
+ return undefined;
233
+ const answer = await prompt('确认不会自动迁移外部服务中的存量业务数据?[y/N]:');
234
+ acknowledgeEmptyCloudData = /^y(?:es)?$/iu.test(answer);
235
+ if (!acknowledgeEmptyCloudData)
236
+ return undefined;
237
+ }
238
+ const configured = await configureAppImport({
239
+ jobId: status.jobId,
240
+ cloudMode,
241
+ ...(cloudMode === 'EXTERNAL' ? { publicConfig } : {}),
242
+ ...(cloudMode === 'QMUSE' ? { acknowledgeEmptyCloudData } : {}),
243
+ ...(cloudMode === 'QMUSE' && confirmCloudService
244
+ ? { confirmCloudService }
245
+ : {}),
246
+ }, cfg);
247
+ if (!configured.ok || !configured.data) {
248
+ throw new Error(`提交导入配置失败:${configured.errorMessage ?? configured.errorCode ?? `HTTP ${configured.status}`}`);
249
+ }
250
+ return configured.data;
251
+ }
252
+ async function finishImport(status, sourceHash, cfg, flags, managedProject, rootDir) {
253
+ if (status.status !== 'READY' || !status.appId || !status.versionId) {
254
+ if (flags['--json'] === true) {
255
+ output({ success: false, ...status }, flags);
256
+ }
257
+ else {
258
+ printImportStatus(status, flags);
259
+ if (status.status === 'FAILED')
260
+ log.error('导入失败');
261
+ else
262
+ log.warn(`任务需要继续配置,可执行 qmuse import status ${status.jobId} --wait`);
263
+ }
264
+ process.exitCode = status.status === 'FAILED' ? 1 : 2;
265
+ if (status.status === 'FAILED' && rootDir) {
266
+ const diagnosticPath = await writeImportFailureDiagnostic({ rootDir, status });
267
+ if (flags['--json'] !== true) {
268
+ log.warn(`诊断已写入 ${diagnosticPath}`);
269
+ if (status.failureDiagnostic?.fixableByModel) {
270
+ log.warn(`请让当前大模型读取 ${diagnosticPath},修复源码后执行 qmuse import .`);
271
+ }
272
+ }
273
+ }
274
+ if (status.status === 'FAILED' && managedProject) {
275
+ clearPendingImport(managedProject, status.jobId);
276
+ await saveManagedProject(managedProject);
277
+ }
278
+ return;
279
+ }
280
+ const openUrl = getAppOpenUrl(cfg.webBaseUrl, status.appId);
281
+ const pending = managedProject?.manifest.pendingImports?.[managedProject.environment];
282
+ const completedSourceHash = sourceHash || pending?.sourceHash;
283
+ const completedSpaceId = pending?.spaceId || managedProject?.target?.spaceId || cfg.spaceId;
284
+ if (managedProject) {
285
+ if (!completedSourceHash || !completedSpaceId) {
286
+ throw new Error('导入已完成,但本地项目绑定缺少 sourceHash 或 spaceId');
287
+ }
288
+ completeManagedImport(managedProject, {
289
+ apiBaseUrl: cfg.baseUrl,
290
+ appId: status.appId,
291
+ jobId: status.jobId,
292
+ sourceHash: completedSourceHash,
293
+ spaceId: completedSpaceId,
294
+ versionId: status.versionId,
295
+ });
296
+ await saveManagedProject(managedProject);
297
+ }
298
+ if (rootDir)
299
+ await clearImportFailureDiagnostic(rootDir);
300
+ const result = {
301
+ success: true,
302
+ jobId: status.jobId,
303
+ appId: status.appId,
304
+ chatId: status.chatId,
305
+ sprintId: status.sprintId,
306
+ versionId: status.versionId,
307
+ openUrl,
308
+ sourceHash: completedSourceHash,
309
+ operation: status.operation || pending?.operation || (managedProject?.target ? 'UPDATE' : 'CREATE'),
310
+ templateCommit: status.templateCommit,
311
+ importerVersion: status.importerVersion,
312
+ assetCount: status.assetCount,
313
+ uploadedAssetCount: status.uploadedAssetCount,
314
+ bundledAssetCount: status.bundledAssetCount,
315
+ contentSafetyStatus: status.contentSafetyStatus,
316
+ cloudMode: status.cloudMode,
317
+ };
318
+ if (flags['--json'] === true)
319
+ output(result, flags);
320
+ else {
321
+ log.success('项目检查、内容安全、预览和发布产物均已通过');
322
+ log.success('导入成功');
323
+ log.raw(`${openUrl}\n`);
324
+ }
325
+ if (flags['--no-open'] !== true) {
326
+ const opened = await login.openBrowser(openUrl);
327
+ if (!opened && flags['--json'] !== true)
328
+ log.warn('无法自动打开浏览器');
329
+ }
330
+ }
331
+ async function cmdImport(command, flags) {
332
+ const cfg = resolveCfg(flags);
333
+ requireAuth(cfg);
334
+ const subcommand = command[1];
335
+ if (subcommand === 'status') {
336
+ const jobId = command[2];
337
+ if (!jobId)
338
+ fail('用法: qmuse import status <jobId> [--wait]');
339
+ if (flags['--wait'] === true) {
340
+ const candidateManagedProject = await loadManagedProject(process.cwd(), cfg, str(flags, '--env'));
341
+ const pendingImport = candidateManagedProject
342
+ ?.manifest.pendingImports?.[candidateManagedProject.environment];
343
+ if (pendingImport &&
344
+ pendingImport.jobId !== jobId) {
345
+ throw new Error('当前项目记录的待恢复任务与指定 jobId 不一致');
346
+ }
347
+ const managedProject = pendingImport ? candidateManagedProject : undefined;
348
+ const polled = await pollImportWithLogs(jobId, cfg, flags);
349
+ if (!polled.status) {
350
+ log.warn(`已停止本地等待,可继续执行 qmuse import status ${jobId} --wait`);
351
+ process.exitCode = 130;
352
+ return;
353
+ }
354
+ await finishImport(polled.status, undefined, cfg, flags, managedProject, process.cwd());
355
+ }
356
+ else {
357
+ const result = await queryAppImportStatus(jobId, cfg);
358
+ if (!result.ok || !result.data) {
359
+ fail(`查询导入任务失败:${result.errorMessage ?? result.errorCode ?? `HTTP ${result.status}`}`);
360
+ }
361
+ printImportStatus(result.data, flags);
362
+ }
363
+ return;
364
+ }
365
+ if (subcommand === 'configure') {
366
+ const jobId = command[2];
367
+ if (!jobId)
368
+ fail('用法: qmuse import configure <jobId> --cloud-mode EXTERNAL|QMUSE');
369
+ const mode = parseImportCloudMode(str(flags, '--cloud-mode'));
370
+ if (mode === 'AUTO')
371
+ fail('configure 必须指定 --cloud-mode EXTERNAL 或 QMUSE');
372
+ const result = await configureAppImport({
373
+ jobId,
374
+ cloudMode: mode,
375
+ ...(mode === 'EXTERNAL'
376
+ ? { publicConfig: readPublicConfig(str(flags, '--public-config')) }
377
+ : flags['--confirm-cloud-service'] === true
378
+ ? { confirmCloudService: true }
379
+ : { acknowledgeEmptyCloudData: flags['--acknowledge-empty-cloud-data'] === true }),
380
+ }, cfg);
381
+ if (!result.ok || !result.data) {
382
+ fail(`提交导入配置失败:${result.errorMessage ?? result.errorCode ?? `HTTP ${result.status}`}`);
383
+ }
384
+ printImportStatus(result.data, flags);
385
+ return;
386
+ }
387
+ const directory = subcommand;
388
+ if (!directory)
389
+ fail('用法: qmuse import <directory> [--space-id <id>]');
390
+ const rootDir = path.resolve(directory);
391
+ await clearImportFailureDiagnostic(rootDir);
392
+ const managedProject = await loadManagedProject(rootDir, cfg, str(flags, '--env'));
393
+ const explicitSpaceId = str(flags, '--space-id');
394
+ const operation = managedProject?.target ? 'UPDATE' : 'CREATE';
395
+ if (managedProject?.target &&
396
+ explicitSpaceId &&
397
+ explicitSpaceId !== managedProject.target.spaceId) {
398
+ throw new Error('更新导入不能切换目标空间;--space-id 必须与项目绑定一致');
399
+ }
400
+ const spaceId = explicitSpaceId ||
401
+ managedProject?.target?.spaceId ||
402
+ managedProject?.manifest.defaultSpaceId ||
403
+ cfg.spaceId;
404
+ if (!spaceId)
405
+ fail('请通过 --space-id 指定目标空间,或先执行 qmuse space use <spaceId>');
406
+ const cloudMode = parseImportCloudMode(str(flags, '--cloud-mode'));
407
+ if (flags['--json'] !== true)
408
+ log.info('正在只读预检本地仓库...');
409
+ const preflight = await preflightImport(directory);
410
+ if (managedProject?.target?.lastSourceHash === preflight.sourceHash) {
411
+ const result = {
412
+ success: true,
413
+ noChanges: true,
414
+ operation: 'UPDATE',
415
+ appId: managedProject.target.appId,
416
+ versionId: managedProject.target.lastVersionId,
417
+ sourceHash: preflight.sourceHash,
418
+ openUrl: getAppOpenUrl(cfg.webBaseUrl, managedProject.target.appId),
419
+ };
420
+ if (flags['--json'] === true)
421
+ output(result, flags);
422
+ else
423
+ log.success('源码没有变化,无需创建新版本');
424
+ return;
425
+ }
426
+ if (flags['--json'] !== true) {
427
+ log.success(`识别为 React 项目(${preflight.fileCount} 个文件)`);
428
+ if (flags['--log'] === true) {
429
+ log.dim(`源码 SHA-256:${preflight.sourceHash}`);
430
+ log.dim(`静态资源:${preflight.assets.length} 个`);
431
+ log.dim(`公开配置键:${preflight.envKeys.join(', ') || '(无)'}`);
432
+ log.dim(`云服务:${preflight.cloudProviders.join(', ') || '(无)'}`);
433
+ log.dim(`状态接口:${getImportStatusUrl(cfg)}`);
434
+ }
435
+ log.info('正在打包并上传源码...');
436
+ }
437
+ const archive = await createImportArchive(preflight);
438
+ let sourceToken;
439
+ try {
440
+ sourceToken = await uploadImportArchive({
441
+ filePath: archive.filePath,
442
+ spaceId,
443
+ }, cfg);
444
+ }
445
+ finally {
446
+ await archive.cleanup();
447
+ }
448
+ if (flags['--json'] !== true)
449
+ log.success('源代码上传完成');
450
+ const started = await startAppImport({
451
+ sourceToken,
452
+ spaceId,
453
+ title: str(flags, '--title') || preflight.title,
454
+ sourceHash: preflight.sourceHash,
455
+ sourceType: 'REACT_VITE',
456
+ cloudMode,
457
+ idempotencyKey: str(flags, '--idempotency-key'),
458
+ sourceScenario: managedProject ? 'QMUSE_TEMPLATE' : 'EXTERNAL_REPOSITORY',
459
+ operation,
460
+ ...(managedProject?.target
461
+ ? {
462
+ targetAppId: managedProject.target.appId,
463
+ baseVersionId: managedProject.target.lastVersionId,
464
+ }
465
+ : {}),
466
+ }, cfg);
467
+ if (!started.ok || !started.data?.jobId) {
468
+ throw new Error(`创建导入任务失败:${started.errorMessage ?? started.errorCode ?? `HTTP ${started.status}`}`);
469
+ }
470
+ const jobId = started.data.jobId;
471
+ if (managedProject) {
472
+ setPendingImport(managedProject, {
473
+ apiBaseUrl: cfg.baseUrl,
474
+ jobId,
475
+ operation,
476
+ sourceHash: preflight.sourceHash,
477
+ spaceId,
478
+ createdAt: new Date().toISOString(),
479
+ });
480
+ await saveManagedProject(managedProject);
481
+ }
482
+ if (flags['--json'] !== true)
483
+ log.success(`已创建导入任务 ${jobId}`);
484
+ let polled = await pollImportWithLogs(jobId, cfg, flags);
485
+ while (polled.reason === 'CONFIGURATION_REQUIRED' && polled.status) {
486
+ const configured = await resolveImportConfiguration(polled.status, cfg, flags);
487
+ if (!configured) {
488
+ await finishImport(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir);
489
+ return;
490
+ }
491
+ polled = await pollImportWithLogs(jobId, cfg, flags);
492
+ }
493
+ if (!polled.status) {
494
+ if (flags['--json'] === true) {
495
+ output({ success: false, interrupted: true, jobId }, flags);
496
+ }
497
+ else {
498
+ log.warn(`已停止本地等待,服务端任务仍会继续;使用 qmuse import status ${jobId} --wait 恢复查询`);
499
+ }
500
+ process.exitCode = 130;
501
+ return;
502
+ }
503
+ await finishImport(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir);
504
+ }
505
+ async function cmdCloud(command, flags) {
506
+ const subcommand = command[1];
507
+ if (subcommand !== 'init') {
508
+ fail(`未知 cloud 子命令: ${subcommand ?? '(空)'}。可用: init`);
509
+ }
510
+ if (!str(flags, '-d')?.trim()) {
511
+ fail('用法: qmuse cloud init -d "<应用功能描述>"');
512
+ }
513
+ const cfg = resolveCfg(flags);
514
+ requireAuth(cfg);
515
+ const result = await checkCloudInit(process.cwd(), cfg, str(flags, '--env'));
516
+ if (flags['--json'] === true) {
517
+ output({ success: true, ...result }, flags);
518
+ return;
519
+ }
520
+ process.stdout.write(`${CLOUD_INIT_SUCCESS_MESSAGE}\n`);
521
+ }
522
+ // ---------------- 命令实现 ----------------
523
+ async function cmdWhoami(flags) {
524
+ const cfg = resolveCfg(flags);
525
+ if (flags['--json'] === true) {
526
+ output({
527
+ apiBaseUrl: `${cfg.baseUrl}${cfg.apiPrefix || ''}`,
528
+ loggedIn: hasAuth(cfg),
529
+ ...(cfg.spaceId ? { spaceId: cfg.spaceId } : {}),
530
+ }, flags);
531
+ return;
532
+ }
533
+ log.info(`QMuse API: ${cfg.baseUrl}${cfg.apiPrefix || ''}`);
534
+ log.info(`登录状态 : ${hasAuth(cfg) ? '已登录' : '未登录'}`);
535
+ if (cfg.spaceId)
536
+ log.info(`当前空间 : ${cfg.spaceId}`);
537
+ }
538
+ async function cmdConfig(command, flags) {
539
+ const sub = command[1];
540
+ if (!sub || sub === 'get') {
541
+ const cfg = loadConfig();
542
+ const redacted = {
543
+ ...cfg,
544
+ sessionId: cfg.sessionId ? `<已设置,长度${cfg.sessionId.length}>` : '',
545
+ credentialOrigin: cfg.credentialOrigin || '',
546
+ };
547
+ output(redacted, flags);
548
+ return;
549
+ }
550
+ if (sub === 'path') {
551
+ output({ path: findConfigPath() ?? '(无配置文件,使用默认值)' }, flags);
552
+ return;
553
+ }
554
+ fail(`未知 config 子命令: ${sub}`);
555
+ }
556
+ async function cmdSpace(command, flags) {
557
+ const cfg = resolveCfg(flags);
558
+ const sub = command[1];
559
+ requireAuth(cfg);
560
+ if (sub === 'list') {
561
+ const r = await api.listSpaces(num(flags, '--page-index', 1), num(flags, '--page-size', 20), cfg);
562
+ if (!r.ok || !r.data)
563
+ fail(`查询空间失败: ${r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`}`);
564
+ if (flags['--json'] === true) {
565
+ output(r.data, flags);
566
+ return;
567
+ }
568
+ const list = r.data.list ?? [];
569
+ log.info(`共 ${r.data.total} 个空间:`);
570
+ list.forEach((sp, i) => {
571
+ const pin = sp.isPinned ? '★' : ' ';
572
+ log.raw(`${String(i + 1).padStart(2)} ${pin} ${sp.cnName} [${sp.spaceId}]${sp.role ? ' (' + sp.role + ')' : ''}\n`);
573
+ });
574
+ return;
575
+ }
576
+ if (sub === 'use' || sub === 'select') {
577
+ const spaceId = command[2] || str(flags, '--space-id');
578
+ if (!spaceId)
579
+ fail('用法: qmuse space use <spaceId>');
580
+ const next = { ...loadConfig(), spaceId };
581
+ saveConfig(next, true);
582
+ log.success(`已设为当前空间 spaceId=${spaceId}(后续请求带 x-space-id 头)`);
583
+ return;
584
+ }
585
+ if (sub === 'info') {
586
+ const r = await api.spaceInfo(cfg);
587
+ if (!r.ok)
588
+ fail(`查询当前空间失败: ${r.errorMessage ?? r.errorCode ?? `HTTP ${r.status}`}`);
589
+ output(r.data, flags);
590
+ return;
591
+ }
592
+ fail(`未知 space 子命令: ${sub ?? '(空)'}。可用: list / use <spaceId> / info`);
593
+ }
594
+ async function cmdLogin(flags) {
595
+ const cfg = resolveCfg(flags);
596
+ if (flags['--status'] === true) {
597
+ const out = await login.loginStatus(cfg);
598
+ if (out.success)
599
+ log.success(out.message ?? '会话有效');
600
+ else
601
+ log.error(out.message ?? '会话无效');
602
+ return;
603
+ }
604
+ log.info(`登录方式:browser 目标:${cfg.baseUrl}${cfg.apiPrefix || ''}`);
605
+ const out = await login.loginBrowser(cfg, {
606
+ logRequestDetails: flags['--log'] === true,
607
+ });
608
+ if (out.success) {
609
+ login.closePrompt();
610
+ log.success(`登录成功(${out.method})`);
611
+ if (flags['--json'] === true)
612
+ output({ success: true, method: out.method }, flags);
613
+ }
614
+ else {
615
+ login.closePrompt();
616
+ fail(out.message ?? '登录失败');
617
+ }
618
+ }
619
+ async function cmdLogout(flags) {
620
+ const cfg = resolveCfg(flags);
621
+ if (!(cfg.sessionId || '').trim()) {
622
+ log.warn('当前未配置登录态,无需退出');
623
+ return;
624
+ }
625
+ const cleared = { ...cfg, sessionId: '', credentialOrigin: '' };
626
+ const path = saveConfig(cleared, true);
627
+ log.success(`已清除登录态并写入 ${path}`);
628
+ }
629
+ // ---------------- 帮助 & 入口 ----------------
630
+ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
631
+
632
+ 用法:
633
+ qmuse [global flags] <command> [subcommand] [flags]
634
+
635
+ 全局 flags:
636
+ --env <env> 环境预设:prod / pre / local
637
+ --json 结果以 JSON 输出
638
+ --log 打印登录调试 URL 和临时 token(仅 browser 登录)
639
+
640
+ 全局 flags 可放在命令前或命令后
641
+
642
+ 命令:
643
+ whoami 查看当前环境、登录和空间状态
644
+ login [--status] [--log] [--json] 通过 QMuse 网页登录
645
+ logout 清除保存的登录态
646
+ config [get|path] 查看配置
647
+
648
+ cloud init -d "<应用功能描述>" 校验当前空间的 QMuse 云服务权益
649
+
650
+ import <directory> [--space-id <id>] 导入本地 React 仓库
651
+ [--title <title>] [--cloud-mode AUTO|EXTERNAL|QMUSE]
652
+ [--public-config '<json>'|@file] [--acknowledge-empty-cloud-data]
653
+ [--confirm-cloud-service]
654
+ [--no-open] [--json] [--log]
655
+ import status <jobId> [--wait] [--no-open] 查询或恢复等待导入任务
656
+ import configure <jobId> --cloud-mode <mode> 补充云服务配置后继续任务
657
+
658
+ space list 我加入的空间列表
659
+ space use <spaceId> 设为当前空间(后续带 x-space-id 头)
660
+ space info 当前空间信息
661
+
662
+ 典型流程:
663
+ qmuse login
664
+ qmuse space list
665
+ qmuse space use S640459450135
666
+ qmuse import /path/to/react-vite-app
667
+ `;
668
+ async function main() {
669
+ const argv = process.argv.slice(2);
670
+ if (argv.length === 0) {
671
+ process.stdout.write(HELP);
672
+ return;
673
+ }
674
+ const { command, flags } = parseArgs(argv);
675
+ if (flags['--help'] === true || flags['-h'] === true) {
676
+ process.stdout.write(HELP);
677
+ return;
678
+ }
679
+ if (flags['--version'] === true || flags['-V'] === true) {
680
+ process.stdout.write(`qmuse ${QMUSE_CLI_VERSION}\n`);
681
+ return;
682
+ }
683
+ const root = command[0];
684
+ try {
685
+ switch (root) {
686
+ case 'whoami':
687
+ await cmdWhoami(flags);
688
+ return;
689
+ case 'login':
690
+ await cmdLogin(flags);
691
+ return;
692
+ case 'logout':
693
+ await cmdLogout(flags);
694
+ return;
695
+ case 'config':
696
+ await cmdConfig(command, flags);
697
+ return;
698
+ case 'cloud':
699
+ await cmdCloud(command, flags);
700
+ return;
701
+ case 'import':
702
+ await cmdImport(command, flags);
703
+ return;
704
+ case 'space':
705
+ await cmdSpace(command, flags);
706
+ return;
707
+ case 'help':
708
+ process.stdout.write(HELP);
709
+ return;
710
+ default:
711
+ fail(`未知命令: ${root ?? '(空)'}。\n\n${HELP}`);
712
+ }
713
+ }
714
+ catch (err) {
715
+ const msg = err instanceof Error ? err.message : String(err);
716
+ fail(msg);
717
+ }
718
+ }
719
+ main();