@qmuse/qmuse-cli 1.0.3-dev.2 → 1.0.3-dev.4

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 CHANGED
@@ -21,6 +21,7 @@ import { clearPendingImport, completeManagedImport, loadManagedProject, saveMana
21
21
  import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, } from './import/diagnostic.js';
22
22
  import { checkCloudInit, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
23
23
  import { QMUSE_CLI_VERSION } from './version.js';
24
+ import { startUpdateNotice } from './update-notice.js';
24
25
  import { startDevelopment } from './dev/index.js';
25
26
  // ---------------- 参数解析 ----------------
26
27
  const BOOL_FLAGS = new Set([
@@ -727,6 +728,11 @@ async function main() {
727
728
  return;
728
729
  }
729
730
  const root = command[0];
731
+ const finishUpdateNotice = startUpdateNotice({
732
+ enabled: root === 'import' && process.stderr.isTTY === true && flags['--json'] !== true,
733
+ notify: message => process.stderr.write(`${message}\n`),
734
+ });
735
+ process.once('exit', finishUpdateNotice);
730
736
  try {
731
737
  switch (root) {
732
738
  case 'whoami':
@@ -761,8 +767,13 @@ async function main() {
761
767
  }
762
768
  }
763
769
  catch (err) {
770
+ finishUpdateNotice();
764
771
  const msg = err instanceof Error ? err.message : String(err);
765
772
  fail(msg);
766
773
  }
774
+ finally {
775
+ finishUpdateNotice();
776
+ process.off('exit', finishUpdateNotice);
777
+ }
767
778
  }
768
779
  main();
@@ -1,7 +1,9 @@
1
1
  import { openAsBlob } from 'node:fs';
2
2
  import { apiCall, authHeaders, buildUrl, requestSourceHeaders, } from '../client.js';
3
+ import { assertImportArchiveSize } from './limits.js';
3
4
  export async function uploadImportArchive(input, cfg) {
4
5
  const archive = await openAsBlob(input.filePath, { type: 'application/zip' });
6
+ assertImportArchiveSize(archive.size);
5
7
  const headers = authHeaders(cfg);
6
8
  headers['Content-Type'] = 'application/zip';
7
9
  headers['Content-Length'] = String(archive.size);
@@ -18,20 +20,51 @@ export async function uploadImportArchive(input, cfg) {
18
20
  catch (error) {
19
21
  throw new Error(`上传源码失败:${error instanceof Error ? error.message : String(error)}`);
20
22
  }
21
- let result;
23
+ const contentType = response.headers.get('content-type')?.split(';')[0].trim();
24
+ const traceId = response.headers.get('x-trace-id') ?? response.headers.get('trace-id');
25
+ const details = [
26
+ `HTTP ${response.status}`,
27
+ `源码包 ${(archive.size / 1024 / 1024).toFixed(2)} MiB(${archive.size} 字节)`,
28
+ contentType && /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/iu.test(contentType)
29
+ ? `响应类型 ${contentType}` : undefined,
30
+ traceId && /^[a-z0-9_-]{1,128}$/iu.test(traceId) ? `traceId=${traceId}` : undefined,
31
+ ].filter(Boolean).join(',');
32
+ if (response.status === 413) {
33
+ await response.body?.cancel().catch(() => undefined);
34
+ throw new Error(`上传源码失败:源码包超过上传入口大小限制(${details})。请将不参与运行的生成产物加入 .gitignore 后重试;如需上传完整源码包,请联系平台核对上传限制。`);
35
+ }
36
+ let payload;
22
37
  try {
23
- result = await response.json();
38
+ payload = await response.json();
24
39
  }
25
40
  catch {
26
- throw new Error('上传源码失败:服务端响应格式错误');
41
+ throw new Error(`上传源码失败:无法读取有效的 JSON 响应(${details}),请检查服务或网关响应后重试`);
42
+ }
43
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
44
+ throw new Error(`上传源码失败:服务端响应结构错误(${details})`);
27
45
  }
28
- if (!response.ok || result.success !== true || !result.data?.sourceToken) {
29
- throw new Error(`上传源码失败:${result.errorMessage ?? result.errorCode ?? `HTTP ${response.status}`}`);
46
+ const result = payload;
47
+ if (!response.ok || result.success !== true) {
48
+ const reason = typeof result.errorMessage === 'string' ? result.errorMessage
49
+ : typeof result.errorCode === 'string' ? result.errorCode : '服务端拒绝上传';
50
+ throw new Error(`上传源码失败:${reason}(${details})`);
51
+ }
52
+ if (typeof result.data?.sourceToken !== 'string' || !result.data.sourceToken.trim()) {
53
+ throw new Error(`上传源码失败:服务端响应缺少有效上传凭证(${details})`);
30
54
  }
31
55
  return result.data.sourceToken;
32
56
  }
33
- export function startAppImport(body, cfg) {
34
- return apiCall('POST', '/api/app-import/start', body, cfg);
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;
35
68
  }
36
69
  export function queryAppImportStatus(jobId, cfg) {
37
70
  return apiCall('POST', '/api/app-import/status', { jobId }, cfg);
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { ZipFile } from 'yazl';
6
6
  import { createSourceManifest, SOURCE_MANIFEST_PATH } from './source-manifest.js';
7
- const MAX_ARCHIVE_BYTES = 500 * 1024 * 1024;
7
+ import { assertImportArchiveSize } from './limits.js';
8
8
  const STABLE_ZIP_MTIME = new Date('1980-01-01T00:00:00.000Z');
9
9
  export async function createImportArchive(preflight) {
10
10
  const temporaryDirectory = await mkdtemp(path.join(tmpdir(), 'qmuse-import-'));
@@ -36,8 +36,7 @@ export async function createImportArchive(preflight) {
36
36
  const sizeBytes = (await stat(filePath)).size;
37
37
  if (sizeBytes <= 0)
38
38
  throw new Error('生成的源码压缩包为空');
39
- if (sizeBytes > MAX_ARCHIVE_BYTES)
40
- throw new Error('源码压缩包超过 500MB');
39
+ assertImportArchiveSize(sizeBytes);
41
40
  return {
42
41
  filePath,
43
42
  fileName,
@@ -0,0 +1,6 @@
1
+ export const MAX_IMPORT_ARCHIVE_BYTES = 10 * 1024 * 1024;
2
+ export function assertImportArchiveSize(sizeBytes) {
3
+ if (sizeBytes > MAX_IMPORT_ARCHIVE_BYTES) {
4
+ throw new Error(`源码压缩包超过 10 MiB 上传上限:当前 ${(sizeBytes / 1024 / 1024).toFixed(2)} MiB(${sizeBytes} 字节)。请将不参与运行的生成产物加入 .gitignore 后重试`);
5
+ }
6
+ }
@@ -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.2",
3
+ "version": "1.0.3-dev.4",
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",