@qmuse/qmuse-cli 1.0.3-dev.3 → 1.0.3-dev.5
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 +2 -2
- package/dist/cli.js +84 -18
- package/dist/cloud.js +3 -0
- package/dist/dev/index.js +6 -0
- package/dist/import/api.js +11 -2
- package/dist/import/diagnostic.js +19 -1
- package/dist/import/guidance.js +35 -0
- package/dist/import/project.js +26 -10
- package/dist/update-notice.js +80 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ qmuse import /path/to/react-project --space-id <spaceId>
|
|
|
50
50
|
|
|
51
51
|
### 更新已有应用
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
没有关联应用时,首次导入会创建新应用并自动保存本地绑定。之后在同一项目目录执行 `qmuse import .`,即可更新对应应用。请保留 `.qmuse/project.json`,从 QMuse 下载且带有绑定的项目也使用相同命令更新。
|
|
54
54
|
|
|
55
55
|
平台已有新版本时,终端会询问是否用本地全部源码覆盖,不会自动合并平台修改。确认覆盖可执行 `qmuse import . --force`;脚本或 `--json` 模式须显式指定该参数。覆盖会在原应用创建新版本,仍需通过正常导入校验。
|
|
56
56
|
|
|
@@ -101,7 +101,7 @@ CLI 将本地 Runtime 的 `__MUSE__.domain` 设置为实际监听地址(如 `h
|
|
|
101
101
|
|
|
102
102
|
服务端必须先部署 `/api/appwrite/local-dev/context` 和 `/api/appwrite/local-dev/mutation-events`,再发布并安装新版 CLI;缺少接口时不会降级为模拟云服务。
|
|
103
103
|
|
|
104
|
-
页面改动热更新,Schema/函数改动继续执行 `qmuse import
|
|
104
|
+
页面改动热更新,Schema/函数改动继续执行 `qmuse import .`。CLI 启动期间设置 `QMUSE_HMR_GATE_ENABLED=false`,退出后恢复原值;项目的 `@qmuse/vite-config` 必须支持此开关,否则平台批量改码门禁仍会暂存本地修改。导入更新本地绑定后,连接会重新获取配置并刷新页面。切换登录账号或应用绑定需重启连接;退出 CLI 不会自动撤销浏览器已经建立的 AppBase Session。SDK 的匿名降级行为保持不变,验收必须检查实际 Session 用户,不能只看页面是否显示。
|
|
105
105
|
|
|
106
106
|
联调验收包括 CRUD、刷新读回、文件上传/预览、函数调用和权限拒绝;浏览器 Cookie 限制需在目标浏览器验证。`.muse-tmp`、`.qmuse` 和系统临时配置不进入导入包。
|
|
107
107
|
|
package/dist/cli.js
CHANGED
|
@@ -17,10 +17,12 @@ import { configureAppImport, getAppOpenUrl, getImportStatusUrl, queryAppImportSt
|
|
|
17
17
|
import { pollAppImport } from './import/poll.js';
|
|
18
18
|
import { preflightImport } from './import/preflight.js';
|
|
19
19
|
import { resolveUpdateBase } from './import/update.js';
|
|
20
|
-
import { clearPendingImport, completeManagedImport, loadManagedProject, saveManagedProject, setPendingImport, } from './import/project.js';
|
|
21
|
-
import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, } from './import/diagnostic.js';
|
|
22
|
-
import { checkCloudInit, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
|
|
20
|
+
import { clearPendingImport, completeManagedImport, createExternalProject, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
|
|
21
|
+
import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, importRetryGuidance, } from './import/diagnostic.js';
|
|
22
|
+
import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
|
|
23
|
+
import { configurationGuidance } from './import/guidance.js';
|
|
23
24
|
import { QMUSE_CLI_VERSION } from './version.js';
|
|
25
|
+
import { startUpdateNotice } from './update-notice.js';
|
|
24
26
|
import { startDevelopment } from './dev/index.js';
|
|
25
27
|
// ---------------- 参数解析 ----------------
|
|
26
28
|
const BOOL_FLAGS = new Set([
|
|
@@ -190,11 +192,14 @@ async function pollImportWithLogs(jobId, cfg, flags) {
|
|
|
190
192
|
process.removeListener('SIGINT', onInterrupt);
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
|
-
async function resolveImportConfiguration(status, cfg, flags) {
|
|
195
|
+
async function resolveImportConfiguration(status, cfg, flags, cloudTarget) {
|
|
194
196
|
if (status.stage === 'QMUSE_CLOUD_ADAPTATION_REQUIRED')
|
|
195
197
|
return undefined;
|
|
196
198
|
const interactive = process.stdin.isTTY === true && flags['--json'] !== true;
|
|
197
199
|
let cloudMode = parseImportCloudMode(str(flags, '--cloud-mode'));
|
|
200
|
+
if (cloudMode === 'AUTO' && ['QMUSE', 'EXTERNAL'].includes(status.cloudMode ?? '')) {
|
|
201
|
+
cloudMode = status.cloudMode;
|
|
202
|
+
}
|
|
198
203
|
if (cloudMode === 'AUTO') {
|
|
199
204
|
if (!interactive)
|
|
200
205
|
return undefined;
|
|
@@ -234,6 +239,8 @@ async function resolveImportConfiguration(status, cfg, flags) {
|
|
|
234
239
|
if (!acknowledgeEmptyCloudData)
|
|
235
240
|
return undefined;
|
|
236
241
|
}
|
|
242
|
+
if (cloudMode === 'QMUSE')
|
|
243
|
+
await checkCloudEntitlement(cloudTarget, cfg);
|
|
237
244
|
const configured = await configureAppImport({
|
|
238
245
|
jobId: status.jobId,
|
|
239
246
|
cloudMode,
|
|
@@ -250,23 +257,34 @@ async function resolveImportConfiguration(status, cfg, flags) {
|
|
|
250
257
|
}
|
|
251
258
|
async function finishImport(status, sourceHash, cfg, flags, managedProject, rootDir) {
|
|
252
259
|
if (status.status !== 'READY' || !status.appId || !status.versionId) {
|
|
260
|
+
const guidance = configurationGuidance(status, resolveProjectEnvironment(cfg, str(flags, '--env')), parseImportCloudMode(str(flags, '--cloud-mode')));
|
|
253
261
|
if (flags['--json'] === true) {
|
|
254
|
-
output({ success: false, ...status }, flags);
|
|
262
|
+
output({ success: false, ...status, ...(guidance ? { configurationRequired: guidance } : {}) }, flags);
|
|
255
263
|
}
|
|
256
264
|
else {
|
|
257
265
|
printImportStatus(status, flags);
|
|
258
266
|
if (status.status === 'FAILED')
|
|
259
267
|
log.error('导入失败');
|
|
260
|
-
else
|
|
261
|
-
log.warn(
|
|
268
|
+
else if (guidance) {
|
|
269
|
+
log.warn(guidance.message);
|
|
270
|
+
if (guidance.resumeCommand)
|
|
271
|
+
log.warn(`继续配置:${guidance.resumeCommand}`);
|
|
272
|
+
if (guidance.waitCommand)
|
|
273
|
+
log.warn(`配置后在原项目目录执行:${guidance.waitCommand}`);
|
|
274
|
+
}
|
|
262
275
|
}
|
|
263
276
|
process.exitCode = status.status === 'FAILED' ? 1 : 2;
|
|
264
277
|
if (status.status === 'FAILED' && rootDir) {
|
|
265
|
-
const
|
|
278
|
+
const diagnosticProject = managedProject ?? await loadManagedProject(rootDir, cfg, str(flags, '--env'));
|
|
279
|
+
const diagnosticPath = await writeImportFailureDiagnostic({ rootDir, status, project: diagnosticProject,
|
|
280
|
+
environment: resolveProjectEnvironment(cfg, str(flags, '--env')) });
|
|
266
281
|
if (flags['--json'] !== true) {
|
|
267
282
|
log.warn(`诊断已写入 ${diagnosticPath}`);
|
|
268
283
|
if (status.failureDiagnostic?.fixableByModel) {
|
|
269
|
-
|
|
284
|
+
const retry = importRetryGuidance(status, diagnosticProject);
|
|
285
|
+
log.warn(`请让当前大模型读取 ${diagnosticPath},修复源码。${retry.retryAdvice}`);
|
|
286
|
+
if (retry.retryCommand)
|
|
287
|
+
log.warn(retry.retryCommand);
|
|
270
288
|
}
|
|
271
289
|
}
|
|
272
290
|
}
|
|
@@ -368,27 +386,53 @@ async function cmdImport(command, flags) {
|
|
|
368
386
|
const mode = parseImportCloudMode(str(flags, '--cloud-mode'));
|
|
369
387
|
if (mode === 'AUTO')
|
|
370
388
|
fail('configure 必须指定 --cloud-mode EXTERNAL 或 QMUSE');
|
|
389
|
+
const current = await queryAppImportStatus(jobId, cfg);
|
|
390
|
+
if (!current.ok || !current.data)
|
|
391
|
+
throw new Error(`查询导入任务失败:${current.errorMessage ?? current.errorCode}`);
|
|
392
|
+
const guidance = configurationGuidance(current.data, resolveProjectEnvironment(cfg, str(flags, '--env')), mode);
|
|
393
|
+
if (mode === 'QMUSE' && guidance?.requiredFlags?.some(flag => flags[flag] !== true)) {
|
|
394
|
+
await finishImport(current.data, undefined, cfg, flags);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (mode === 'QMUSE') {
|
|
398
|
+
const project = await loadManagedProject(process.cwd(), cfg, str(flags, '--env'));
|
|
399
|
+
const pending = project?.manifest.pendingImports?.[project.environment];
|
|
400
|
+
const matchingProject = pending?.jobId === jobId ? project : undefined;
|
|
401
|
+
const appId = current.data.appId || matchingProject?.target?.appId;
|
|
402
|
+
const spaceId = pending?.jobId === jobId ? pending.spaceId : str(flags, '--space-id');
|
|
403
|
+
if (appId || spaceId)
|
|
404
|
+
await checkCloudEntitlement({ appId, spaceId: spaceId ?? '' }, cfg);
|
|
405
|
+
}
|
|
371
406
|
const result = await configureAppImport({
|
|
372
407
|
jobId,
|
|
373
408
|
cloudMode: mode,
|
|
374
409
|
...(mode === 'EXTERNAL'
|
|
375
410
|
? { publicConfig: readPublicConfig(str(flags, '--public-config')) }
|
|
376
|
-
:
|
|
377
|
-
|
|
378
|
-
|
|
411
|
+
: {
|
|
412
|
+
confirmCloudService: flags['--confirm-cloud-service'] === true,
|
|
413
|
+
acknowledgeEmptyCloudData: flags['--acknowledge-empty-cloud-data'] === true,
|
|
414
|
+
}),
|
|
379
415
|
}, cfg);
|
|
380
416
|
if (!result.ok || !result.data) {
|
|
381
417
|
fail(`提交导入配置失败:${result.errorMessage ?? result.errorCode ?? `HTTP ${result.status}`}`);
|
|
382
418
|
}
|
|
383
|
-
|
|
419
|
+
if (['WAITING_CLOUD_CONFIRMATION', 'NEEDS_CONFIGURATION', 'FAILED'].includes(result.data.status)) {
|
|
420
|
+
await finishImport(result.data, undefined, cfg, flags);
|
|
421
|
+
}
|
|
422
|
+
else
|
|
423
|
+
printImportStatus(result.data, flags);
|
|
384
424
|
return;
|
|
385
425
|
}
|
|
386
426
|
const directory = subcommand;
|
|
387
427
|
if (!directory)
|
|
388
428
|
fail('用法: qmuse import <directory> [--space-id <id>]');
|
|
389
429
|
const rootDir = path.resolve(directory);
|
|
390
|
-
await
|
|
391
|
-
|
|
430
|
+
const managedProject = await loadManagedProject(rootDir, cfg, str(flags, '--env')) ??
|
|
431
|
+
createExternalProject(rootDir, cfg, str(flags, '--env'));
|
|
432
|
+
const pendingImport = managedProject.manifest.pendingImports?.[managedProject.environment];
|
|
433
|
+
if (pendingImport) {
|
|
434
|
+
throw new Error(`当前目录有尚未完成的导入任务,请先执行 qmuse import status ${pendingImport.jobId} --wait --env ${managedProject.environment},避免重复创建应用`);
|
|
435
|
+
}
|
|
392
436
|
const explicitSpaceId = str(flags, '--space-id');
|
|
393
437
|
const operation = managedProject?.target ? 'UPDATE' : 'CREATE';
|
|
394
438
|
if (flags['--force'] === true && !managedProject?.target) {
|
|
@@ -434,6 +478,12 @@ async function cmdImport(command, flags) {
|
|
|
434
478
|
log.success('源码没有变化,无需创建新版本');
|
|
435
479
|
return;
|
|
436
480
|
}
|
|
481
|
+
if (cloudMode === 'QMUSE') {
|
|
482
|
+
const quota = await checkCloudEntitlement({ appId: managedProject.target?.appId, spaceId }, cfg);
|
|
483
|
+
if (flags['--json'] !== true && quota.appLimit !== undefined) {
|
|
484
|
+
log.info(`QMuse 云服务额度:已使用 ${quota.enabledAppCount ?? 0}/${quota.appLimit}${quota.alreadyEnabled ? '(当前应用已开通)' : ''}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
437
487
|
if (flags['--json'] !== true) {
|
|
438
488
|
log.success(`识别为 React 项目(${preflight.fileCount} 个文件)`);
|
|
439
489
|
if (flags['--log'] === true) {
|
|
@@ -448,6 +498,7 @@ async function cmdImport(command, flags) {
|
|
|
448
498
|
const archive = await createImportArchive(preflight);
|
|
449
499
|
let sourceToken;
|
|
450
500
|
try {
|
|
501
|
+
await saveManagedProject(managedProject);
|
|
451
502
|
sourceToken = await uploadImportArchive({
|
|
452
503
|
filePath: archive.filePath,
|
|
453
504
|
spaceId,
|
|
@@ -466,7 +517,7 @@ async function cmdImport(command, flags) {
|
|
|
466
517
|
sourceType: 'REACT_VITE',
|
|
467
518
|
cloudMode,
|
|
468
519
|
idempotencyKey: str(flags, '--idempotency-key'),
|
|
469
|
-
sourceScenario: managedProject
|
|
520
|
+
sourceScenario: managedProject.manifest.projectKind,
|
|
470
521
|
operation,
|
|
471
522
|
...(managedProject?.target
|
|
472
523
|
? {
|
|
@@ -494,7 +545,7 @@ async function cmdImport(command, flags) {
|
|
|
494
545
|
log.success(`已创建导入任务 ${jobId}`);
|
|
495
546
|
let polled = await pollImportWithLogs(jobId, cfg, flags);
|
|
496
547
|
while (polled.reason === 'CONFIGURATION_REQUIRED' && polled.status) {
|
|
497
|
-
const configured = await resolveImportConfiguration(polled.status, cfg, flags);
|
|
548
|
+
const configured = await resolveImportConfiguration(polled.status, cfg, flags, { appId: managedProject.target?.appId, spaceId });
|
|
498
549
|
if (!configured) {
|
|
499
550
|
await finishImport(polled.status, preflight.sourceHash, cfg, flags, managedProject, rootDir);
|
|
500
551
|
return;
|
|
@@ -727,6 +778,11 @@ async function main() {
|
|
|
727
778
|
return;
|
|
728
779
|
}
|
|
729
780
|
const root = command[0];
|
|
781
|
+
const finishUpdateNotice = startUpdateNotice({
|
|
782
|
+
enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
|
|
783
|
+
notify: message => process.stderr.write(`${message}\n`),
|
|
784
|
+
});
|
|
785
|
+
process.once('exit', finishUpdateNotice);
|
|
730
786
|
try {
|
|
731
787
|
switch (root) {
|
|
732
788
|
case 'whoami':
|
|
@@ -761,8 +817,18 @@ async function main() {
|
|
|
761
817
|
}
|
|
762
818
|
}
|
|
763
819
|
catch (err) {
|
|
820
|
+
finishUpdateNotice();
|
|
764
821
|
const msg = err instanceof Error ? err.message : String(err);
|
|
765
|
-
|
|
822
|
+
if (flags['--json'] === true) {
|
|
823
|
+
output({ success: false, message: msg }, flags);
|
|
824
|
+
process.exitCode = 1;
|
|
825
|
+
}
|
|
826
|
+
else
|
|
827
|
+
fail(msg);
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
finishUpdateNotice();
|
|
831
|
+
process.off('exit', finishUpdateNotice);
|
|
766
832
|
}
|
|
767
833
|
}
|
|
768
834
|
main();
|
package/dist/cloud.js
CHANGED
|
@@ -20,6 +20,9 @@ export async function checkCloudInit(rootDir, cfg, explicitEnvironment) {
|
|
|
20
20
|
if (!spaceId) {
|
|
21
21
|
throw new Error('请先执行 qmuse space use <spaceId>,或通过 --space-id 指定目标空间');
|
|
22
22
|
}
|
|
23
|
+
return checkCloudEntitlement({ appId, spaceId }, cfg);
|
|
24
|
+
}
|
|
25
|
+
export async function checkCloudEntitlement({ appId, spaceId }, cfg) {
|
|
23
26
|
const response = appId
|
|
24
27
|
? await apiCall('POST', '/api/appwrite/entitlement/check', { appId }, cfg)
|
|
25
28
|
: await apiCall('POST', '/api/appwrite/entitlement/space/check', { spaceId }, cfg);
|
package/dist/dev/index.js
CHANGED
|
@@ -26,6 +26,7 @@ export async function startDevelopment(options) {
|
|
|
26
26
|
const directory = await mkdtemp(path.join(tmpdir(), 'qmuse-local-dev-'));
|
|
27
27
|
const runtimePath = path.join(directory, 'runtime.json');
|
|
28
28
|
const previousPath = process.env.MUSE_RUNTIME_CONFIG_PATH;
|
|
29
|
+
const previousHmrGate = process.env.QMUSE_HMR_GATE_ENABLED;
|
|
29
30
|
let server;
|
|
30
31
|
let closed = false;
|
|
31
32
|
const manifestPath = path.join(root, '.qmuse/project.json');
|
|
@@ -58,11 +59,16 @@ export async function startDevelopment(options) {
|
|
|
58
59
|
delete process.env.MUSE_RUNTIME_CONFIG_PATH;
|
|
59
60
|
else
|
|
60
61
|
process.env.MUSE_RUNTIME_CONFIG_PATH = previousPath;
|
|
62
|
+
if (previousHmrGate === undefined)
|
|
63
|
+
delete process.env.QMUSE_HMR_GATE_ENABLED;
|
|
64
|
+
else
|
|
65
|
+
process.env.QMUSE_HMR_GATE_ENABLED = previousHmrGate;
|
|
61
66
|
}
|
|
62
67
|
};
|
|
63
68
|
try {
|
|
64
69
|
await writeRuntime();
|
|
65
70
|
process.env.MUSE_RUNTIME_CONFIG_PATH = runtimePath;
|
|
71
|
+
process.env.QMUSE_HMR_GATE_ENABLED = 'false';
|
|
66
72
|
const vite = await import(pathToFileURL(vitePath).href);
|
|
67
73
|
server = await vite.createServer({
|
|
68
74
|
root,
|
package/dist/import/api.js
CHANGED
|
@@ -54,8 +54,17 @@ export async function uploadImportArchive(input, cfg) {
|
|
|
54
54
|
}
|
|
55
55
|
return result.data.sourceToken;
|
|
56
56
|
}
|
|
57
|
-
export function startAppImport(body, cfg) {
|
|
58
|
-
|
|
57
|
+
export async function startAppImport(body, cfg) {
|
|
58
|
+
const result = await apiCall('POST', '/api/app-import/start', body, cfg);
|
|
59
|
+
if (!result.ok && result.errorCode === 'APP_IMPORT_VERSION_CONFLICT') {
|
|
60
|
+
const raw = result.raw;
|
|
61
|
+
const latest = raw?.error?.latestVersionId;
|
|
62
|
+
const version = raw?.error?.appId === body.targetAppId &&
|
|
63
|
+
typeof latest === 'string' && /^[A-Za-z0-9_-]{1,128}$/u.test(latest)
|
|
64
|
+
? `(当前平台版本 ${latest})` : '';
|
|
65
|
+
result.errorMessage = `应用已产生新版本${version}。请同步最新源码后重新导入;确认以本地源码覆盖时,重新执行 qmuse import . --force。`;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
59
68
|
}
|
|
60
69
|
export function queryAppImportStatus(jobId, cfg) {
|
|
61
70
|
return apiCall('POST', '/api/app-import/status', { jobId }, cfg);
|
|
@@ -2,6 +2,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export const QMUSE_IMPORT_DIAGNOSTIC_PATH = '.qmuse/last-import-error.json';
|
|
5
|
+
export function importRetryGuidance(status, project) {
|
|
6
|
+
const target = project?.target;
|
|
7
|
+
const pending = project?.manifest.pendingImports?.[project.environment];
|
|
8
|
+
const canRetryUpdate = target && status.operation !== 'CREATE' &&
|
|
9
|
+
(status.appId === target.appId || (!status.appId && pending?.jobId === status.jobId && pending.operation === 'UPDATE'));
|
|
10
|
+
return canRetryUpdate
|
|
11
|
+
? { retryCommand: `qmuse import . --env ${project.environment}`, retryAdvice: '在当前项目目录修复源码后执行,将更新已绑定的应用。' }
|
|
12
|
+
: { retryCommand: null, retryAdvice: '当前任务没有可确认的原应用绑定。请先核对任务是否已创建应用,并恢复对应环境的项目绑定;直接重新 import 可能创建新应用。' };
|
|
13
|
+
}
|
|
5
14
|
export async function writeImportFailureDiagnostic(input) {
|
|
6
15
|
if (input.status.status !== 'FAILED') {
|
|
7
16
|
throw new Error('只能为失败的导入任务写入诊断文件');
|
|
@@ -17,10 +26,18 @@ export async function writeImportFailureDiagnostic(input) {
|
|
|
17
26
|
message: input.status.message,
|
|
18
27
|
diagnostic: input.status.diagnostic,
|
|
19
28
|
failureDiagnostic: input.status.failureDiagnostic,
|
|
20
|
-
|
|
29
|
+
...importRetryGuidance(input.status, input.project),
|
|
30
|
+
environment: input.project?.environment ?? input.environment,
|
|
21
31
|
generatedAt: new Date().toISOString(),
|
|
22
32
|
};
|
|
33
|
+
const historyPath = path.join(input.rootDir, '.qmuse', 'import-errors', input.project?.environment ?? input.environment ?? 'unknown', `${encodeURIComponent(input.status.jobId)}.json`);
|
|
34
|
+
await mkdir(path.dirname(historyPath), { recursive: true });
|
|
35
|
+
const historyTemporaryPath = `${historyPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
23
36
|
try {
|
|
37
|
+
await writeFile(historyTemporaryPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
38
|
+
encoding: 'utf8', mode: 0o600, flag: 'wx',
|
|
39
|
+
});
|
|
40
|
+
await rename(historyTemporaryPath, historyPath);
|
|
24
41
|
await writeFile(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
25
42
|
encoding: 'utf8',
|
|
26
43
|
mode: 0o600,
|
|
@@ -29,6 +46,7 @@ export async function writeImportFailureDiagnostic(input) {
|
|
|
29
46
|
await rename(temporaryPath, filePath);
|
|
30
47
|
}
|
|
31
48
|
catch (error) {
|
|
49
|
+
await rm(historyTemporaryPath, { force: true });
|
|
32
50
|
await rm(temporaryPath, { force: true });
|
|
33
51
|
throw error;
|
|
34
52
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function configurationGuidance(status, environment, requestedMode = 'AUTO') {
|
|
2
|
+
if (!['WAITING_CLOUD_CONFIRMATION', 'NEEDS_CONFIGURATION'].includes(status.status))
|
|
3
|
+
return undefined;
|
|
4
|
+
if (status.stage === 'QMUSE_CLOUD_ADAPTATION_REQUIRED') {
|
|
5
|
+
return { message: '请先根据诊断完成源码的 QMuse 云服务适配,再重新导入。' };
|
|
6
|
+
}
|
|
7
|
+
const mode = requestedMode === 'AUTO' ? status.cloudMode : requestedMode;
|
|
8
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(status.jobId))
|
|
9
|
+
return { message: '任务标识格式异常,请联系平台支持。' };
|
|
10
|
+
const base = `qmuse import configure ${status.jobId} --env ${environment}`;
|
|
11
|
+
const waitCommand = `qmuse import status ${status.jobId} --wait --env ${environment} --json`;
|
|
12
|
+
if (mode === 'QMUSE') {
|
|
13
|
+
const flag = status.sourceScenario === 'QMUSE_TEMPLATE'
|
|
14
|
+
? '--confirm-cloud-service' : '--acknowledge-empty-cloud-data';
|
|
15
|
+
return {
|
|
16
|
+
message: `任务尚未完成,需要确认:模板使用 --confirm-cloud-service,外部项目使用 --acknowledge-empty-cloud-data。当前任务请使用 ${flag}。`,
|
|
17
|
+
requiredFlags: [flag],
|
|
18
|
+
resumeCommand: `${base} --cloud-mode QMUSE ${flag} --json`,
|
|
19
|
+
waitCommand,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (mode === 'EXTERNAL') {
|
|
23
|
+
return {
|
|
24
|
+
message: `请提供外部云服务所需的公开配置:${(status.requiredConfigKeys ?? []).join(', ') || '请参考任务诊断'}。`,
|
|
25
|
+
requiredConfigKeys: status.requiredConfigKeys ?? [],
|
|
26
|
+
resumeCommand: `${base} --cloud-mode EXTERNAL --public-config @public-config.json --json`,
|
|
27
|
+
waitCommand,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
message: '请先选择 --cloud-mode QMUSE 或 EXTERNAL;QMUSE 模板需 --confirm-cloud-service,外部项目需 --acknowledge-empty-cloud-data。',
|
|
32
|
+
requiredFlags: ['--cloud-mode'],
|
|
33
|
+
waitCommand,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/import/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export const QMUSE_PROJECT_PATH = '.qmuse/project.json';
|
|
5
5
|
function isRecord(value) {
|
|
@@ -73,20 +73,21 @@ function parseManifest(value) {
|
|
|
73
73
|
if (value.schemaVersion !== 1) {
|
|
74
74
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 schemaVersion 不受支持`);
|
|
75
75
|
}
|
|
76
|
-
if (value.projectKind !== 'QMUSE_TEMPLATE') {
|
|
77
|
-
throw new Error(`${QMUSE_PROJECT_PATH} 的 projectKind
|
|
76
|
+
if (value.projectKind !== 'QMUSE_TEMPLATE' && value.projectKind !== 'EXTERNAL_REPOSITORY') {
|
|
77
|
+
throw new Error(`${QMUSE_PROJECT_PATH} 的 projectKind 无效`);
|
|
78
78
|
}
|
|
79
79
|
if (value.appType !== 'GENERAL') {
|
|
80
80
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 appType 必须是 GENERAL`);
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const isTemplate = value.projectKind === 'QMUSE_TEMPLATE';
|
|
83
|
+
if (isTemplate && (!Number.isSafeInteger(value.packageVersion) || Number(value.packageVersion) < 1)) {
|
|
83
84
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 packageVersion 无效`);
|
|
84
85
|
}
|
|
85
|
-
if (!isRecord(value.skillVersions)) {
|
|
86
|
+
if (isTemplate && !isRecord(value.skillVersions)) {
|
|
86
87
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 skillVersions 必须是对象`);
|
|
87
88
|
}
|
|
88
89
|
const skillVersions = {};
|
|
89
|
-
for (const [name, version] of Object.entries(value.skillVersions)) {
|
|
90
|
+
for (const [name, version] of Object.entries(isTemplate ? value.skillVersions : {})) {
|
|
90
91
|
skillVersions[requireNonEmptyString(name, 'skillVersions key')] = requireNonEmptyString(version, `skillVersions.${name}`);
|
|
91
92
|
}
|
|
92
93
|
const defaultSpaceId = value.defaultSpaceId === undefined
|
|
@@ -94,11 +95,13 @@ function parseManifest(value) {
|
|
|
94
95
|
: requireNonEmptyString(value.defaultSpaceId, 'defaultSpaceId');
|
|
95
96
|
return {
|
|
96
97
|
schemaVersion: 1,
|
|
97
|
-
|
|
98
|
-
projectKind: 'QMUSE_TEMPLATE',
|
|
98
|
+
projectKind: value.projectKind,
|
|
99
99
|
appType: 'GENERAL',
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
...(isTemplate ? {
|
|
101
|
+
packageVersion: Number(value.packageVersion),
|
|
102
|
+
templateCommit: requireNonEmptyString(value.templateCommit, 'templateCommit'),
|
|
103
|
+
skillVersions,
|
|
104
|
+
} : {}),
|
|
102
105
|
...(defaultSpaceId ? { defaultSpaceId } : {}),
|
|
103
106
|
targets: parseEnvironmentMap(value.targets, 'targets', parseTarget),
|
|
104
107
|
...(value.pendingImports === undefined
|
|
@@ -150,6 +153,7 @@ export async function loadManagedProject(rootDir, cfg, explicitEnvironment) {
|
|
|
150
153
|
return { manifestPath, manifest, environment, target };
|
|
151
154
|
}
|
|
152
155
|
export async function saveManagedProject(context) {
|
|
156
|
+
await mkdir(path.dirname(context.manifestPath), { recursive: true });
|
|
153
157
|
const temporaryPath = `${context.manifestPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
154
158
|
const content = `${JSON.stringify(context.manifest, null, 2)}\n`;
|
|
155
159
|
try {
|
|
@@ -162,6 +166,18 @@ export async function saveManagedProject(context) {
|
|
|
162
166
|
throw error;
|
|
163
167
|
}
|
|
164
168
|
}
|
|
169
|
+
export function createExternalProject(rootDir, cfg, explicitEnvironment) {
|
|
170
|
+
return {
|
|
171
|
+
manifestPath: path.join(rootDir, QMUSE_PROJECT_PATH),
|
|
172
|
+
environment: resolveProjectEnvironment(cfg, explicitEnvironment),
|
|
173
|
+
manifest: {
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
projectKind: 'EXTERNAL_REPOSITORY',
|
|
176
|
+
appType: 'GENERAL',
|
|
177
|
+
targets: {},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
165
181
|
export function setPendingImport(context, pending) {
|
|
166
182
|
context.manifest.pendingImports = {
|
|
167
183
|
...context.manifest.pendingImports,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { QMUSE_CLI_DISTRIBUTION, QMUSE_CLI_VERSION } from './version.js';
|
|
6
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
export function updateMessage(current, latest, distribution) {
|
|
8
|
+
if (typeof latest !== 'string' || !semver.valid(latest) || semver.prerelease(latest) ||
|
|
9
|
+
!semver.valid(current) || !semver.gt(latest, current))
|
|
10
|
+
return;
|
|
11
|
+
const command = distribution === 'PUBLIC'
|
|
12
|
+
? 'npm install -g @qmuse/qmuse-cli@latest --registry=https://registry.npmjs.org/'
|
|
13
|
+
: 'tnpm install -g @alipay/qmuse-cli@latest';
|
|
14
|
+
return `qmuse-cli 有新版本 ${latest}(当前 ${current})。建议更新:${command}`;
|
|
15
|
+
}
|
|
16
|
+
// Check alongside the command; finishing a command never waits for the registry.
|
|
17
|
+
export function startUpdateNotice(options) {
|
|
18
|
+
if (!options.enabled)
|
|
19
|
+
return () => { };
|
|
20
|
+
const distribution = options.distribution ?? QMUSE_CLI_DISTRIBUTION;
|
|
21
|
+
const current = options.current ?? QMUSE_CLI_VERSION;
|
|
22
|
+
const directory = options.cacheDirectory ?? join(homedir(), '.cache', 'qmuse-cli');
|
|
23
|
+
const cachePath = join(directory, `update-${distribution.toLowerCase()}.json`);
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
let latest;
|
|
26
|
+
let finished = false;
|
|
27
|
+
let cached = false;
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(readFileSync(cachePath, 'utf8'));
|
|
30
|
+
const age = Date.now() - value.checkedAt;
|
|
31
|
+
if (Number.isFinite(age) && age >= 0 && age < CACHE_TTL_MS) {
|
|
32
|
+
latest = value.latest;
|
|
33
|
+
cached = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* Missing or corrupt cache must not affect the command. */ }
|
|
37
|
+
let timer;
|
|
38
|
+
if (!cached) {
|
|
39
|
+
timer = setTimeout(() => controller.abort(), 2000);
|
|
40
|
+
timer.unref();
|
|
41
|
+
const url = distribution === 'PUBLIC'
|
|
42
|
+
? 'https://registry.npmjs.org/@qmuse%2fqmuse-cli/latest'
|
|
43
|
+
: 'https://registry.antgroup-inc.cn/@alipay%2fqmuse-cli/latest';
|
|
44
|
+
void (async () => {
|
|
45
|
+
try {
|
|
46
|
+
const response = await (options.fetcher ?? fetch)(url, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
redirect: 'error',
|
|
49
|
+
headers: { Accept: 'application/json' },
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok)
|
|
52
|
+
return;
|
|
53
|
+
const metadata = await response.json();
|
|
54
|
+
if (finished || controller.signal.aborted || typeof metadata.version !== 'string' ||
|
|
55
|
+
!semver.valid(metadata.version) || semver.prerelease(metadata.version))
|
|
56
|
+
return;
|
|
57
|
+
latest = metadata.version;
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
60
|
+
writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now(), latest }), { mode: 0o600 });
|
|
61
|
+
}
|
|
62
|
+
catch { /* Cache is best effort. */ }
|
|
63
|
+
}
|
|
64
|
+
catch { /* Update checks never fail the user's command. */ }
|
|
65
|
+
finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
if (finished)
|
|
72
|
+
return;
|
|
73
|
+
finished = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
controller.abort();
|
|
76
|
+
const message = updateMessage(current, latest, distribution);
|
|
77
|
+
if (message)
|
|
78
|
+
options.notify(message);
|
|
79
|
+
};
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qmuse/qmuse-cli",
|
|
3
|
-
"version": "1.0.3-dev.
|
|
3
|
+
"version": "1.0.3-dev.5",
|
|
4
4
|
"description": "将本地 React 项目安全导入 QMuse 的命令行客户端",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@napi-rs/keyring": "2.0.0",
|
|
23
23
|
"ignore": "^7.0.8",
|
|
24
|
+
"semver": "7.7.4",
|
|
24
25
|
"yazl": "^3.3.1"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@playwright/test": "1.63.0",
|
|
35
36
|
"@types/node": "^22.10.0",
|
|
37
|
+
"@types/semver": "7.7.1",
|
|
36
38
|
"@types/yazl": "^3.3.1",
|
|
37
39
|
"tsx": "^4.19.2",
|
|
38
40
|
"typescript": "^5.9.3",
|