@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 +11 -0
- package/dist/import/api.js +40 -7
- package/dist/import/archive.js +2 -3
- package/dist/import/limits.js +6 -0
- package/dist/update-notice.js +80 -0
- package/package.json +3 -1
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();
|
package/dist/import/api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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);
|
package/dist/import/archive.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|