@qmuse/qmuse-cli 1.0.6 → 1.0.7
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 +13 -0
- package/dist/cli.js +48 -2
- package/dist/cloud.js +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,19 @@ qmuse import ./app --cloud-mode QMUSE \
|
|
|
75
75
|
|
|
76
76
|
先用 `cloud init` 检查当前空间的云服务权益,导入时使用 `--confirm-cloud-service` 确认云资源计划。外部项目切换到 QMuse 云服务时,还需添加 `--acknowledge-empty-cloud-data`,确认外部已有数据不会自动迁移。
|
|
77
77
|
|
|
78
|
+
云管理命令要求先完成 `qmuse login`,`--app-id` 可显式指定应用,省略时读取项目绑定。
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
qmuse cloud set-secret TOKEN --stdin --app-id <appId> --cloud-env dev
|
|
82
|
+
qmuse cloud schema preflight --candidate ./schema-candidate.json --app-id <appId>
|
|
83
|
+
qmuse cloud schema export --app-id <appId>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`set-secret --stdin` 从标准输入接收键名完全匹配的 JSON,如 `{"TOKEN":"实际密钥值"}`;请由可信密钥工具提供,避免在参数、源码或版本库保存值。命令只输出名称和保存结果;部分保存失败会返回非零退出码。更新已停用密钥会保留停用状态,返回 `updated-disabled` / `SECRET_DISABLED` 和非零退出码,表示值已保存但密钥仍不可用;需要在密钥管理面板启用。密钥保存后的云端异步同步状态在面板确认。`--cloud-env dev|prod` 是云资源环境,`--env prod|pre|local` 是平台接口环境。
|
|
87
|
+
|
|
88
|
+
`schema preflight` 比较完整候选与 READY 基线,返回检查结果和差异;检查不会写 schema 或建表,未通过时命令返回非零退出码。`schema export` 输出开发环境已应用的 READY schema,失败导入后它可能比本地文件更新;核对基线后再恢复本地文件。平台仍采用 append-only 演进规则,不能用导出来的旧版本删除已创建的表。
|
|
89
|
+
|
|
90
|
+
|
|
78
91
|
### 恢复任务
|
|
79
92
|
|
|
80
93
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as login from './login.js';
|
|
|
12
12
|
import { prompt } from './login.js';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
15
16
|
import { createImportArchive } from './import/archive.js';
|
|
16
17
|
import { configureAppImport, getAppOpenUrl, getImportStatusUrl, queryAppImportStatus, startAppImport, uploadImportArchive, } from './import/api.js';
|
|
17
18
|
import { pollAppImport } from './import/poll.js';
|
|
@@ -19,7 +20,7 @@ import { preflightImport } from './import/preflight.js';
|
|
|
19
20
|
import { resolveUpdateBase } from './import/update.js';
|
|
20
21
|
import { clearPendingImport, completeManagedImport, createExternalProject, loadManagedProject, saveManagedProject, resolveProjectEnvironment, setPendingImport, } from './import/project.js';
|
|
21
22
|
import { clearImportFailureDiagnostic, writeImportFailureDiagnostic, importRetryGuidance, } from './import/diagnostic.js';
|
|
22
|
-
import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE } from './cloud.js';
|
|
23
|
+
import { checkCloudInit, checkCloudEntitlement, CLOUD_INIT_SUCCESS_MESSAGE, resolveCloudAppId, preflightCloudSchema, exportCloudSchema, parseCloudSecrets, setCloudSecrets } from './cloud.js';
|
|
23
24
|
import { configurationGuidance } from './import/guidance.js';
|
|
24
25
|
import { QMUSE_CLI_VERSION } from './version.js';
|
|
25
26
|
import { startUpdateNotice } from './update-notice.js';
|
|
@@ -30,6 +31,7 @@ const BOOL_FLAGS = new Set([
|
|
|
30
31
|
'--log', '--no-open', '--wait', '--force',
|
|
31
32
|
'--acknowledge-empty-cloud-data',
|
|
32
33
|
'--confirm-cloud-service',
|
|
34
|
+
'--stdin',
|
|
33
35
|
]);
|
|
34
36
|
function parseArgs(argv) {
|
|
35
37
|
const command = [];
|
|
@@ -576,8 +578,48 @@ async function cmdImport(command, flags) {
|
|
|
576
578
|
}
|
|
577
579
|
async function cmdCloud(command, flags) {
|
|
578
580
|
const subcommand = command[1];
|
|
581
|
+
if (subcommand === 'set-secret' || subcommand === 'schema') {
|
|
582
|
+
const cfg = resolveCfg(flags);
|
|
583
|
+
requireAuth(cfg);
|
|
584
|
+
const appId = await resolveCloudAppId(process.cwd(), cfg, str(flags, '--env'), str(flags, '--app-id'));
|
|
585
|
+
if (subcommand === 'set-secret') {
|
|
586
|
+
if (flags['--stdin'] !== true || !command[2])
|
|
587
|
+
fail('用法: qmuse cloud set-secret KEY1,KEY2 --stdin [--cloud-env dev|prod];通过标准输入传入 JSON,不在命令参数中传入密钥值');
|
|
588
|
+
const cloudEnv = str(flags, '--cloud-env') || 'dev';
|
|
589
|
+
if (cloudEnv !== 'dev' && cloudEnv !== 'prod')
|
|
590
|
+
fail('--cloud-env 仅支持 dev / prod');
|
|
591
|
+
if (process.stdin.isTTY)
|
|
592
|
+
fail('请通过管道向标准输入提供密钥 JSON,不要在终端直接粘贴密钥值');
|
|
593
|
+
let input = '';
|
|
594
|
+
process.stdin.setEncoding('utf8');
|
|
595
|
+
for await (const chunk of process.stdin) {
|
|
596
|
+
input += chunk;
|
|
597
|
+
if (input.length > 500_000)
|
|
598
|
+
fail('密钥输入过大');
|
|
599
|
+
}
|
|
600
|
+
const result = await setCloudSecrets(appId, parseCloudSecrets(command[2], input), cfg, cloudEnv);
|
|
601
|
+
output(result, flags);
|
|
602
|
+
if (!result.success)
|
|
603
|
+
process.exitCode = 1;
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (command[2] === 'export') {
|
|
607
|
+
const result = await exportCloudSchema(appId, cfg);
|
|
608
|
+
output(flags['--json'] === true ? result : result.schema, flags);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (command[2] !== 'preflight' || !str(flags, '--candidate'))
|
|
612
|
+
fail('用法: qmuse cloud schema preflight --candidate <schema.json>;或 qmuse cloud schema export');
|
|
613
|
+
const candidate = str(flags, '--candidate');
|
|
614
|
+
const candidatePath = candidate.replace(/^\$HOME(?=\/|$)/u, homedir());
|
|
615
|
+
const result = await preflightCloudSchema(appId, readFileSync(path.resolve(candidatePath), 'utf8'), cfg);
|
|
616
|
+
output(result, flags);
|
|
617
|
+
if (result.status !== 'passed')
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
579
621
|
if (subcommand !== 'init') {
|
|
580
|
-
fail(`未知 cloud 子命令: ${subcommand ?? '(空)'}。可用: init`);
|
|
622
|
+
fail(`未知 cloud 子命令: ${subcommand ?? '(空)'}。可用: init、set-secret、schema preflight、schema export`);
|
|
581
623
|
}
|
|
582
624
|
if (!str(flags, '-d')?.trim()) {
|
|
583
625
|
fail('用法: qmuse cloud init -d "<应用功能描述>"');
|
|
@@ -804,6 +846,10 @@ const HELP = `QMuse CLI —— 将本地 React 项目导入 QMuse
|
|
|
804
846
|
config [get|path] 查看配置
|
|
805
847
|
|
|
806
848
|
cloud init -d "<应用功能描述>" 校验当前空间的 QMuse 云服务权益
|
|
849
|
+
cloud set-secret KEY1,KEY2 --stdin 从标准输入 JSON 保存密钥,不输出密钥值
|
|
850
|
+
[--app-id <id>] [--cloud-env dev|prod]
|
|
851
|
+
cloud schema preflight --candidate <schema.json> 只读校验候选与云端 READY 基线
|
|
852
|
+
cloud schema export [--app-id <id>] 输出当前开发环境的 READY schema
|
|
807
853
|
dev [directory] [--port <port>] 连接已导入应用的开发云资源并启动本地 Vite
|
|
808
854
|
|
|
809
855
|
import <directory> [--space-id <id>] 导入本地 React 仓库
|
package/dist/cloud.js
CHANGED
|
@@ -2,6 +2,74 @@ import { apiCall } from './client.js';
|
|
|
2
2
|
import { loadManagedProject } from './import/project.js';
|
|
3
3
|
export const CLOUD_INIT_SUCCESS_MESSAGE = 'Cloud service entitlement verified successfully. Continue implementing the requested ' +
|
|
4
4
|
'cloud features by following the cloud service skill instructions.';
|
|
5
|
+
export async function resolveCloudAppId(rootDir, cfg, explicitEnvironment, appId) {
|
|
6
|
+
if (appId?.trim())
|
|
7
|
+
return appId.trim();
|
|
8
|
+
const project = await loadManagedProject(rootDir, cfg, explicitEnvironment);
|
|
9
|
+
if (!project?.target?.appId)
|
|
10
|
+
throw new Error('当前项目尚未绑定应用,请使用 --app-id 指定诊断中的应用 ID');
|
|
11
|
+
return project.target.appId;
|
|
12
|
+
}
|
|
13
|
+
export async function preflightCloudSchema(appId, schemaContent, cfg) {
|
|
14
|
+
if (schemaContent.length > 2_000_000)
|
|
15
|
+
throw new Error('候选 schema 不能超过 2,000,000 个字符');
|
|
16
|
+
try {
|
|
17
|
+
JSON.parse(schemaContent);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('候选 schema 不是合法 JSON');
|
|
21
|
+
}
|
|
22
|
+
const result = await apiCall('POST', '/api/appwrite/schema/preflight', { appId, schemaContent }, cfg);
|
|
23
|
+
if (!result.ok || !result.data)
|
|
24
|
+
throw new Error(`schema 预检失败:${result.errorCode || `HTTP ${result.status}`}`);
|
|
25
|
+
return result.data;
|
|
26
|
+
}
|
|
27
|
+
export async function exportCloudSchema(appId, cfg) {
|
|
28
|
+
const result = await apiCall('POST', '/api/appwrite/schema/baseline', { appId }, cfg);
|
|
29
|
+
if (!result.ok || !result.data)
|
|
30
|
+
throw new Error(`读取 READY schema 失败:${result.errorCode || `HTTP ${result.status}`}`);
|
|
31
|
+
return result.data;
|
|
32
|
+
}
|
|
33
|
+
export function parseCloudSecrets(names, input) {
|
|
34
|
+
const keys = names.split(',').map(key => key.trim());
|
|
35
|
+
if (keys.length === 0 || keys.length > 50 || new Set(keys).size !== keys.length || keys.some(key => !/^[A-Za-z][A-Za-z0-9_]{0,254}$/u.test(key))) {
|
|
36
|
+
throw new Error('密钥名称必须为不重复的名称列表,以逗号分隔;不要在命令参数中传入密钥值');
|
|
37
|
+
}
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(input);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('标准输入必须是包含密钥名和值的 JSON 对象');
|
|
44
|
+
}
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
46
|
+
throw new Error('标准输入必须是 JSON 对象');
|
|
47
|
+
const entries = Object.entries(value);
|
|
48
|
+
if (entries.length !== keys.length || entries.some(([key, item]) => !keys.includes(key) || typeof item !== 'string' || item.length === 0 || item.length > 8192)) {
|
|
49
|
+
throw new Error('标准输入必须包含且仅包含指定密钥,每个值为 1..8192 个字符的字符串');
|
|
50
|
+
}
|
|
51
|
+
return Object.fromEntries(entries);
|
|
52
|
+
}
|
|
53
|
+
export async function setCloudSecrets(appId, values, cfg, env = 'dev') {
|
|
54
|
+
const listed = await apiCall('POST', '/api/appwrite/secrets/list', { appId, env }, cfg);
|
|
55
|
+
if (!listed.ok || !listed.data)
|
|
56
|
+
throw new Error(`读取密钥列表失败:${listed.errorCode || `HTTP ${listed.status}`}`);
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const [key, value] of Object.entries(values)) {
|
|
59
|
+
const existing = listed.data.secrets.find(secret => secret.key === key);
|
|
60
|
+
const response = await apiCall('POST', `/api/appwrite/secrets/${existing ? 'update-value' : 'create'}`, existing ? { appId, env, secretId: existing.id, value } : { appId, env, key, value }, cfg);
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
results.push({ key, status: 'failed', errorCode: response.errorCode || `HTTP_${response.status}` });
|
|
63
|
+
}
|
|
64
|
+
else if (existing?.status === 'DISABLED') {
|
|
65
|
+
results.push({ key, status: 'updated-disabled', errorCode: 'SECRET_DISABLED' });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
results.push({ key, status: existing ? 'updated' : 'created' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { success: results.every(result => result.status === 'updated' || result.status === 'created'), appId, env, results };
|
|
72
|
+
}
|
|
5
73
|
function entitlementError(snapshot) {
|
|
6
74
|
if (snapshot.reason === 'CLOUD_SERVICE_LIMIT_EXCEEDED') {
|
|
7
75
|
const usage = snapshot.appLimit === undefined
|