@qmuse/qmuse-cli 1.0.7 → 1.0.8
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 +15 -0
- package/dist/cli.js +24 -2
- package/dist/create.js +151 -0
- package/dist/import/api.js +6 -0
- package/dist/import/project.js +20 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -36,6 +36,21 @@ qmuse space info
|
|
|
36
36
|
|
|
37
37
|
## 导入
|
|
38
38
|
|
|
39
|
+
### 从模板开始
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
qmuse login
|
|
43
|
+
qmuse create my-app
|
|
44
|
+
cd my-app
|
|
45
|
+
npm install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`create` 从 QMuse 模板下载接口获取代码并直接解压,包含 `AGENTS.md`、云服务 Skill 和 `.qmuse/project.json`。只需登录,无需先选择空间;目标目录必须不存在。命令不会自动安装依赖。开发完成后选择空间并执行 `qmuse import .`。
|
|
49
|
+
|
|
50
|
+
支持 `qmuse --env pre create my-app` 下载预发模板,以及 `--json` 输出项目目录和模板 commit。后续导入使用相同的 `--env`。
|
|
51
|
+
|
|
52
|
+
### 导入本地代码
|
|
53
|
+
|
|
39
54
|
```bash
|
|
40
55
|
qmuse import /path/to/react-project --space-id <spaceId>
|
|
41
56
|
```
|
package/dist/cli.js
CHANGED
|
@@ -14,17 +14,18 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { createImportArchive } from './import/archive.js';
|
|
17
|
-
import { configureAppImport, getAppOpenUrl, getImportStatusUrl, queryAppImportStatus, startAppImport, uploadImportArchive, } from './import/api.js';
|
|
17
|
+
import { configureAppImport, assertTemplateProtocolSupported, getAppOpenUrl, getImportStatusUrl, queryAppImportStatus, startAppImport, uploadImportArchive, } from './import/api.js';
|
|
18
18
|
import { pollAppImport } from './import/poll.js';
|
|
19
19
|
import { preflightImport } from './import/preflight.js';
|
|
20
20
|
import { resolveUpdateBase } from './import/update.js';
|
|
21
|
-
import { clearPendingImport, completeManagedImport, createExternalProject, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
|
|
21
|
+
import { clearPendingImport, completeManagedImport, createExternalProject, importArchitectureProtocol, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
|
|
22
22
|
import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, importRetryGuidance, } from './import/diagnostic.js';
|
|
23
23
|
import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE, resolveCloudAppId, preflightCloudSchema, exportCloudSchema, parseCloudSecrets, setCloudSecrets } from './cloud.js';
|
|
24
24
|
import { configurationGuidance } from './import/guidance.js';
|
|
25
25
|
import { QMUSE_CLI_VERSION } from './version.js';
|
|
26
26
|
import { startUpdateNotice } from './update-notice.js';
|
|
27
27
|
import { startDevelopment } from './dev/index.js';
|
|
28
|
+
import { createProject } from './create.js';
|
|
28
29
|
// ---------------- 参数解析 ----------------
|
|
29
30
|
const BOOL_FLAGS = new Set([
|
|
30
31
|
'--help', '-h', '--version', '-V', '--json', '--status',
|
|
@@ -441,6 +442,8 @@ async function cmdImport(command, flags) {
|
|
|
441
442
|
const rootDir = path.resolve(directory);
|
|
442
443
|
const managedProject = await loadManagedProject(rootDir, cfg, str(flags, '--env')) ??
|
|
443
444
|
createExternalProject(rootDir, cfg, str(flags, '--env'));
|
|
445
|
+
if (managedProject.manifest.schemaVersion === 2)
|
|
446
|
+
await assertTemplateProtocolSupported(cfg);
|
|
444
447
|
const pendingImport = managedProject.manifest.pendingImports?.[managedProject.environment];
|
|
445
448
|
if (pendingImport) {
|
|
446
449
|
throw new Error(`当前目录有尚未完成的导入任务,请先执行 qmuse import status ${pendingImport.jobId} --wait --env ${managedProject.environment},避免重复创建应用`);
|
|
@@ -522,6 +525,7 @@ async function cmdImport(command, flags) {
|
|
|
522
525
|
if (flags['--json'] !== true)
|
|
523
526
|
log.success('源代码上传完成');
|
|
524
527
|
const started = await startAppImport({
|
|
528
|
+
...importArchitectureProtocol(managedProject.manifest),
|
|
525
529
|
sourceToken,
|
|
526
530
|
spaceId,
|
|
527
531
|
title: str(flags, '--title')?.trim() || undefined,
|
|
@@ -827,6 +831,18 @@ async function cmdLogout(flags) {
|
|
|
827
831
|
}
|
|
828
832
|
}
|
|
829
833
|
// ---------------- 帮助 & 入口 ----------------
|
|
834
|
+
async function cmdCreate(command, flags) {
|
|
835
|
+
if (command.length !== 2)
|
|
836
|
+
fail('用法:qmuse create <directory>');
|
|
837
|
+
const result = await createProject(command[1], resolveCfg(flags));
|
|
838
|
+
if (flags['--json'] === true) {
|
|
839
|
+
output(result, flags);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
log.success(`模板已下载到 ${result.directory}`);
|
|
843
|
+
log.info('进入项目目录后执行:npm install');
|
|
844
|
+
log.info(`开发完成后执行:qmuse${str(flags, '--env') ? ` --env ${str(flags, '--env')}` : ''} import .`);
|
|
845
|
+
}
|
|
830
846
|
const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
|
|
831
847
|
|
|
832
848
|
用法:
|
|
@@ -845,6 +861,8 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
|
|
|
845
861
|
logout 清除保存的登录态和空间选择
|
|
846
862
|
config [get|path] 查看配置
|
|
847
863
|
|
|
864
|
+
create <directory> 下载 QMuse 模板并解压到新项目目录
|
|
865
|
+
|
|
848
866
|
cloud init -d "<应用功能描述>" 校验当前空间的 QMuse 云服务权益
|
|
849
867
|
cloud set-secret KEY1,KEY2 --stdin 从标准输入 JSON 保存密钥,不输出密钥值
|
|
850
868
|
[--app-id <id>] [--cloud-env dev|prod]
|
|
@@ -868,6 +886,7 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
|
|
|
868
886
|
qmuse login
|
|
869
887
|
qmuse space list
|
|
870
888
|
qmuse space use S640459450135
|
|
889
|
+
qmuse create my-app
|
|
871
890
|
qmuse import /path/to/react-vite-app
|
|
872
891
|
`;
|
|
873
892
|
async function main() {
|
|
@@ -905,6 +924,9 @@ async function main() {
|
|
|
905
924
|
case 'config':
|
|
906
925
|
await cmdConfig(command, flags);
|
|
907
926
|
return;
|
|
927
|
+
case 'create':
|
|
928
|
+
await cmdCreate(command, flags);
|
|
929
|
+
return;
|
|
908
930
|
case 'cloud':
|
|
909
931
|
await cmdCloud(command, flags);
|
|
910
932
|
return;
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { fromBufferPromise } from 'yauzl';
|
|
6
|
+
import { authHeaders, buildUrl, hasAuth } from './client.js';
|
|
7
|
+
import { loadManagedProject } from './import/project.js';
|
|
8
|
+
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
9
|
+
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
const MAX_FILES = 20_000;
|
|
11
|
+
async function readResponse(response, limit) {
|
|
12
|
+
if (!response.body)
|
|
13
|
+
throw new Error('模板下载响应为空');
|
|
14
|
+
const reader = response.body.getReader();
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
try {
|
|
18
|
+
if (Number(response.headers.get('content-length')) > limit) {
|
|
19
|
+
throw new Error('模板下载响应超过大小限制');
|
|
20
|
+
}
|
|
21
|
+
while (true) {
|
|
22
|
+
const { value, done } = await reader.read();
|
|
23
|
+
if (done)
|
|
24
|
+
break;
|
|
25
|
+
size += value.byteLength;
|
|
26
|
+
if (size > limit)
|
|
27
|
+
throw new Error('模板下载响应超过大小限制');
|
|
28
|
+
chunks.push(Buffer.from(value));
|
|
29
|
+
}
|
|
30
|
+
return Buffer.concat(chunks, size);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await reader.cancel().catch(() => { });
|
|
34
|
+
reader.releaseLock();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function downloadTemplate(projectName, cfg) {
|
|
38
|
+
const params = new URLSearchParams({ appType: 'GENERAL', projectName });
|
|
39
|
+
// 模板下载无需空间;不继承当前空间,避免触发无关的空间权限校验。
|
|
40
|
+
const headers = authHeaders({ ...cfg, spaceId: '' });
|
|
41
|
+
headers.Accept = 'application/zip';
|
|
42
|
+
delete headers['Content-Type'];
|
|
43
|
+
const response = await fetch(buildUrl(cfg, `/api/project-template/download?${params}`), {
|
|
44
|
+
headers,
|
|
45
|
+
redirect: 'error',
|
|
46
|
+
signal: AbortSignal.timeout(120_000),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok || response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/zip') {
|
|
49
|
+
const body = await readResponse(response, 64 * 1024);
|
|
50
|
+
let message = '';
|
|
51
|
+
try {
|
|
52
|
+
const error = JSON.parse(body.toString('utf8'));
|
|
53
|
+
if (typeof error.errorMessage === 'string')
|
|
54
|
+
message = error.errorMessage;
|
|
55
|
+
else if (typeof error.message === 'string')
|
|
56
|
+
message = error.message;
|
|
57
|
+
}
|
|
58
|
+
catch { /* HTML 登录页或代理错误页不作为模板落盘。 */ }
|
|
59
|
+
if (response.status === 401)
|
|
60
|
+
throw new Error('登录已失效,请先运行 `qmuse login`');
|
|
61
|
+
throw new Error(`模板下载失败(HTTP ${response.status})${message ? `:${message}` : ':服务端未返回 ZIP 模板'}`);
|
|
62
|
+
}
|
|
63
|
+
return readResponse(response, MAX_ARCHIVE_BYTES);
|
|
64
|
+
}
|
|
65
|
+
async function extractTemplate(archive, directory, projectName) {
|
|
66
|
+
const zip = await fromBufferPromise(archive, {
|
|
67
|
+
lazyEntries: true, strictFileNames: true, validateEntrySizes: true,
|
|
68
|
+
});
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
let totalBytes = 0;
|
|
71
|
+
let count = 0;
|
|
72
|
+
try {
|
|
73
|
+
if (zip.entryCount > MAX_FILES)
|
|
74
|
+
throw new Error('模板文件数超过限制');
|
|
75
|
+
for await (const entry of zip.eachEntry()) {
|
|
76
|
+
if (++count > MAX_FILES)
|
|
77
|
+
throw new Error('模板文件数超过限制');
|
|
78
|
+
const name = entry.fileName;
|
|
79
|
+
const isDirectory = name.endsWith('/');
|
|
80
|
+
const parts = name.replace(/\/$/u, '').split('/');
|
|
81
|
+
if (parts[0] !== projectName || parts.some(part => !part || part === '.' || part === '..' || /[\\:\x00-\x1f]/u.test(part)))
|
|
82
|
+
throw new Error(`模板包含不安全路径:${name}`);
|
|
83
|
+
const fileType = (entry.externalFileAttributes >>> 16) & 0o170000;
|
|
84
|
+
if (fileType && fileType !== (isDirectory ? 0o040000 : 0o100000)) {
|
|
85
|
+
throw new Error(`模板包含符号链接或特殊文件:${name}`);
|
|
86
|
+
}
|
|
87
|
+
const relative = parts.slice(1).join('/');
|
|
88
|
+
if (!relative) {
|
|
89
|
+
if (!isDirectory)
|
|
90
|
+
throw new Error(`模板路径不是项目文件:${name}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (seen.has(relative))
|
|
94
|
+
throw new Error(`模板包含重复路径:${name}`);
|
|
95
|
+
seen.add(relative);
|
|
96
|
+
if (entry.uncompressedSize > MAX_FILE_BYTES)
|
|
97
|
+
throw new Error(`模板文件过大:${name}`);
|
|
98
|
+
totalBytes += entry.uncompressedSize;
|
|
99
|
+
if (totalBytes > MAX_ARCHIVE_BYTES)
|
|
100
|
+
throw new Error('模板解压大小超过限制');
|
|
101
|
+
const destination = path.join(directory, relative);
|
|
102
|
+
if (isDirectory) {
|
|
103
|
+
await mkdir(destination, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
107
|
+
const stream = await zip.openReadStreamPromise(entry);
|
|
108
|
+
await pipeline(stream, createWriteStream(destination, { flags: 'wx', mode: 0o644 }));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
zip.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function createProject(directory, cfg) {
|
|
117
|
+
const rootDir = path.resolve(directory);
|
|
118
|
+
const projectName = path.basename(rootDir);
|
|
119
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u.test(projectName)) {
|
|
120
|
+
throw new Error('项目目录名称需以字母或数字开头,仅支持字母、数字、点、下划线和短横线,最多 64 个字符');
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await lstat(rootDir);
|
|
124
|
+
throw new Error(`目标目录已存在,请使用新目录:${rootDir}`);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error.code !== 'ENOENT')
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (!hasAuth(cfg))
|
|
131
|
+
throw new Error('当前环境未登录。请先运行 `qmuse login`。');
|
|
132
|
+
const archive = await downloadTemplate(projectName, cfg);
|
|
133
|
+
// mkdir 不覆盖已有目录,下载期间出现的同名目录也保留原样。
|
|
134
|
+
await mkdir(rootDir);
|
|
135
|
+
try {
|
|
136
|
+
await extractTemplate(archive, rootDir, projectName);
|
|
137
|
+
const project = await loadManagedProject(rootDir, cfg);
|
|
138
|
+
if (!project || project.manifest.projectKind !== 'QMUSE_TEMPLATE') {
|
|
139
|
+
throw new Error('模板缺少有效的 .qmuse/project.json');
|
|
140
|
+
}
|
|
141
|
+
for (const name of ['package.json', 'AGENTS.md', '.agents/skills/qmuse-cloud/SKILL.md']) {
|
|
142
|
+
if (!(await lstat(path.join(rootDir, name))).isFile())
|
|
143
|
+
throw new Error(`模板缺少文件:${name}`);
|
|
144
|
+
}
|
|
145
|
+
return { success: true, directory: rootDir, templateCommit: project.manifest.templateCommit };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
package/dist/import/api.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { openAsBlob } from 'node:fs';
|
|
2
2
|
import { apiCall, authHeaders, buildUrl, requestSourceHeaders, } from '../client.js';
|
|
3
3
|
import { assertImportArchiveSize } from './limits.js';
|
|
4
|
+
export async function assertTemplateProtocolSupported(cfg) {
|
|
5
|
+
const result = await apiCall('GET', '/api/app-import/protocol', undefined, cfg);
|
|
6
|
+
if (!result.ok || !result.data?.templateProtocolVersions?.includes(2)) {
|
|
7
|
+
throw new Error('当前平台尚不支持新模板架构协议,请等待服务端升级;未提交导入,不会降级为旧架构');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
4
10
|
export async function uploadImportArchive(input, cfg) {
|
|
5
11
|
const archive = await openAsBlob(input.filePath, { type: 'application/zip' });
|
|
6
12
|
assertImportArchiveSize(archive.size);
|
package/dist/import/project.js
CHANGED
|
@@ -70,8 +70,8 @@ function parseEnvironmentMap(value, field, parse) {
|
|
|
70
70
|
function parseManifest(value) {
|
|
71
71
|
if (!isRecord(value))
|
|
72
72
|
throw new Error(`${QMUSE_PROJECT_PATH} 必须是 JSON 对象`);
|
|
73
|
-
if (value.schemaVersion !== 1) {
|
|
74
|
-
throw new Error(`${QMUSE_PROJECT_PATH} 的 schemaVersion
|
|
73
|
+
if (value.schemaVersion !== 1 && value.schemaVersion !== 2) {
|
|
74
|
+
throw new Error(`${QMUSE_PROJECT_PATH} 的 schemaVersion 不受支持,请升级 QMuse CLI`);
|
|
75
75
|
}
|
|
76
76
|
if (value.projectKind !== 'QMUSE_TEMPLATE' && value.projectKind !== 'EXTERNAL_REPOSITORY') {
|
|
77
77
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 projectKind 无效`);
|
|
@@ -80,6 +80,14 @@ function parseManifest(value) {
|
|
|
80
80
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 appType 必须是 GENERAL`);
|
|
81
81
|
}
|
|
82
82
|
const isTemplate = value.projectKind === 'QMUSE_TEMPLATE';
|
|
83
|
+
if (value.schemaVersion === 2) {
|
|
84
|
+
if (!isTemplate || !['LEGACY_DIRECT_V1', 'HONO_BACKEND_V1'].includes(String(value.cloudArchitecture))) {
|
|
85
|
+
throw new Error(`${QMUSE_PROJECT_PATH} 的新模板协议必须声明有效的 cloudArchitecture`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (value.cloudArchitecture !== undefined) {
|
|
89
|
+
throw new Error(`${QMUSE_PROJECT_PATH} 的 cloudArchitecture 需要 schemaVersion 2,请使用新版模板`);
|
|
90
|
+
}
|
|
83
91
|
if (isTemplate && (!Number.isSafeInteger(value.packageVersion) || Number(value.packageVersion) < 1)) {
|
|
84
92
|
throw new Error(`${QMUSE_PROJECT_PATH} 的 packageVersion 无效`);
|
|
85
93
|
}
|
|
@@ -94,7 +102,8 @@ function parseManifest(value) {
|
|
|
94
102
|
? undefined
|
|
95
103
|
: requireNonEmptyString(value.defaultSpaceId, 'defaultSpaceId');
|
|
96
104
|
return {
|
|
97
|
-
schemaVersion:
|
|
105
|
+
schemaVersion: value.schemaVersion,
|
|
106
|
+
...(value.schemaVersion === 2 ? { cloudArchitecture: value.cloudArchitecture } : {}),
|
|
98
107
|
projectKind: value.projectKind,
|
|
99
108
|
appType: 'GENERAL',
|
|
100
109
|
...(isTemplate ? {
|
|
@@ -109,6 +118,14 @@ function parseManifest(value) {
|
|
|
109
118
|
: { pendingImports: parseEnvironmentMap(value.pendingImports, 'pendingImports', parsePendingImport) }),
|
|
110
119
|
};
|
|
111
120
|
}
|
|
121
|
+
/** Keep v1 requests byte-compatible; never infer architecture from installed dependencies. */
|
|
122
|
+
export function importArchitectureProtocol(manifest) {
|
|
123
|
+
if (manifest.schemaVersion === 1)
|
|
124
|
+
return {};
|
|
125
|
+
if (!manifest.cloudArchitecture)
|
|
126
|
+
throw new Error('新模板协议缺少 cloudArchitecture');
|
|
127
|
+
return { templateProtocolVersion: 2, cloudArchitecture: manifest.cloudArchitecture };
|
|
128
|
+
}
|
|
112
129
|
export function resolveProjectEnvironment(cfg, explicitEnvironment) {
|
|
113
130
|
if (explicitEnvironment)
|
|
114
131
|
return explicitEnvironment;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qmuse/qmuse-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "将本地 React 项目安全导入 QMuse 的命令行客户端",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"open": "^11.0.4",
|
|
25
25
|
"semver": "7.7.4",
|
|
26
26
|
"typescript": "^5.9.3",
|
|
27
|
+
"yauzl": "^3.4.0",
|
|
27
28
|
"yazl": "^3.3.1"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"@playwright/test": "1.63.0",
|
|
38
39
|
"@types/node": "^22.10.0",
|
|
39
40
|
"@types/semver": "7.7.1",
|
|
41
|
+
"@types/yauzl": "^3.4.0",
|
|
40
42
|
"@types/yazl": "^3.3.1",
|
|
41
43
|
"tsx": "^4.19.2",
|
|
42
44
|
"vite": "7.3.6"
|