@qmuse/qmuse-cli 1.0.9 → 1.0.10-dev.1
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 +14 -0
- package/dist/agent-skill.js +257 -0
- package/dist/app/account.js +23 -0
- package/dist/app/api.js +51 -0
- package/dist/app/binding.js +48 -0
- package/dist/app/chat.js +291 -0
- package/dist/app/cloud.js +28 -0
- package/dist/app/create.js +358 -0
- package/dist/app/data.js +104 -0
- package/dist/app/fn.js +25 -0
- package/dist/app/release.js +105 -0
- package/dist/app/sandbox.js +32 -0
- package/dist/app/secret.js +30 -0
- package/dist/app/sse.js +80 -0
- package/dist/app/version.js +12 -0
- package/dist/cli.js +2210 -139
- package/dist/client.js +79 -7
- package/dist/errors.js +143 -0
- package/dist/login.js +2 -0
- package/dist/telemetry.js +229 -0
- package/dist/tracker.js +130 -0
- package/dist/version-gate.js +202 -0
- package/package.json +4 -2
- package/skills/qmuse-cli/SKILL.md +30 -0
- package/skills/qmuse-cli/references/cloud.md +32 -0
- package/skills/qmuse-cli/references/core-flows.md +93 -0
package/README.md
CHANGED
|
@@ -154,6 +154,20 @@ qmuse import configure <jobId> --cloud-mode EXTERNAL \
|
|
|
154
154
|
|
|
155
155
|
本地等待中断不会取消任务。回到原项目目录,使用 `status <jobId> --wait` 继续等待;任务提示需要补充云服务配置时,使用 `configure`。
|
|
156
156
|
|
|
157
|
+
## 应用创建与管理
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
qmuse app create web --file ./需求.md # 统一创建接口:一次请求创建应用并开始首轮生成
|
|
161
|
+
qmuse app list --space-id <spaceId> --keyword 关键词
|
|
162
|
+
qmuse app info <appId>
|
|
163
|
+
qmuse app use <appId> # 绑定应用到当前目录(.qmuse/app.json)
|
|
164
|
+
qmuse app current / unlink
|
|
165
|
+
qmuse app update <appId> --title 新标题
|
|
166
|
+
qmuse app delete <appId> --yes # 不可恢复
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
退出码:成功/恢复身份 0;等待输入 2;失败 1(应用已保留);本地中断 130。
|
|
170
|
+
|
|
157
171
|
## 本地云联调
|
|
158
172
|
|
|
159
173
|
需要 Node.js 20.19+(或满足项目 Vite 的版本要求)、已安装依赖的 QMuse 模板、一次成功导入和 READY 云后端。首次需要云服务时,先生成最小业务代码与真实 Schema 并导入,再启动:
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 官方 CLI Skill(S01):指南内容随 CLI 包内分发,默认离线安装。
|
|
3
|
+
* install:复制包内 skills/qmuse-cli/* 到目标(保护用户已有修改),写安装清单;
|
|
4
|
+
* status:报告安装目标、版本、来源与文件状态(T01.11 记录 skill 事件)。
|
|
5
|
+
*/
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { QMUSE_CLI_VERSION } from './version.js';
|
|
12
|
+
const MANIFEST_FILE = '.qmuse-agent-skill-install.json';
|
|
13
|
+
/** 包内指南目录:dist/agent-skill.js 与 src/agent-skill.ts 均位于包根下一级。 */
|
|
14
|
+
export function packagedSkillRoot() {
|
|
15
|
+
return fileURLToPath(new URL('../skills/qmuse-cli/', import.meta.url));
|
|
16
|
+
}
|
|
17
|
+
export function defaultTargetForAgent(agent) {
|
|
18
|
+
if (agent === 'codex')
|
|
19
|
+
return path.join(homedir(), '.codex', 'skills');
|
|
20
|
+
if (agent === 'claude')
|
|
21
|
+
return path.join(homedir(), '.claude', 'skills');
|
|
22
|
+
return path.join(homedir(), '.qwen', 'skills');
|
|
23
|
+
}
|
|
24
|
+
export function manifestPath(targetRoot) {
|
|
25
|
+
return path.join(targetRoot, 'qmuse-cli', MANIFEST_FILE);
|
|
26
|
+
}
|
|
27
|
+
function fileSha256(content) {
|
|
28
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
29
|
+
}
|
|
30
|
+
/** 包内指南清单:相对路径 → sha256。 */
|
|
31
|
+
export function packagedFiles() {
|
|
32
|
+
const root = packagedSkillRoot();
|
|
33
|
+
const files = new Map();
|
|
34
|
+
if (!existsSync(root))
|
|
35
|
+
return files;
|
|
36
|
+
const walk = (dir) => {
|
|
37
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
38
|
+
const full = path.join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
walk(full);
|
|
41
|
+
else if (entry.isFile()) {
|
|
42
|
+
files.set(path.relative(root, full).split(path.sep).join('/'), fileSha256(readFileSync(full, 'utf8')));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
walk(root);
|
|
47
|
+
return files;
|
|
48
|
+
}
|
|
49
|
+
export function readInstallManifest(targetRoot) {
|
|
50
|
+
const file = manifestPath(targetRoot);
|
|
51
|
+
if (!existsSync(file))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
55
|
+
return parsed?.skill === 'qmuse-cli' && parsed.schemaVersion === 1 ? parsed : undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function skillStatus(targetRoot) {
|
|
62
|
+
const packaged = packagedFiles();
|
|
63
|
+
const report = {
|
|
64
|
+
packagedVersion: QMUSE_CLI_VERSION,
|
|
65
|
+
packagedAvailable: packaged.size > 0 && packaged.has('SKILL.md'),
|
|
66
|
+
packagedFiles: packaged.size,
|
|
67
|
+
...(packaged.has('SKILL.md') ? { packagedSkillSha256: packaged.get('SKILL.md') } : {}),
|
|
68
|
+
};
|
|
69
|
+
const manifest = readInstallManifest(targetRoot);
|
|
70
|
+
const installDir = path.join(targetRoot, 'qmuse-cli');
|
|
71
|
+
const present = new Set();
|
|
72
|
+
if (existsSync(installDir)) {
|
|
73
|
+
const walk = (dir) => {
|
|
74
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const full = path.join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory())
|
|
77
|
+
walk(full);
|
|
78
|
+
else if (entry.isFile())
|
|
79
|
+
present.add(path.relative(installDir, full).split(path.sep).join('/'));
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
walk(installDir);
|
|
83
|
+
}
|
|
84
|
+
let modifiedFiles = [];
|
|
85
|
+
for (const [relative, digest] of packaged) {
|
|
86
|
+
const file = path.join(installDir, relative);
|
|
87
|
+
if (!existsSync(file))
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
if (fileSha256(readFileSync(file, 'utf8')) !== digest)
|
|
91
|
+
modifiedFiles.push(relative);
|
|
92
|
+
}
|
|
93
|
+
catch { /* 不可读视为已修改 */ }
|
|
94
|
+
}
|
|
95
|
+
modifiedFiles = modifiedFiles.filter(name => name !== MANIFEST_FILE);
|
|
96
|
+
report.installed = manifest || present.size > 0 ? {
|
|
97
|
+
address: installDir,
|
|
98
|
+
agent: manifest?.agent,
|
|
99
|
+
cliVersion: manifest?.cliVersion,
|
|
100
|
+
matchesPackagedVersion: manifest?.cliVersion === QMUSE_CLI_VERSION,
|
|
101
|
+
installedAt: manifest?.installedAt,
|
|
102
|
+
presentFiles: present.size,
|
|
103
|
+
expectedFiles: packaged.size,
|
|
104
|
+
modifiedFiles,
|
|
105
|
+
} : undefined;
|
|
106
|
+
return report;
|
|
107
|
+
}
|
|
108
|
+
/** 安装:检查包内必要文件与版本元数据;目标已有内容不同且未 --force 时拒绝(保护用户修改)。 */
|
|
109
|
+
export function installAgentSkill(options) {
|
|
110
|
+
const packaged = packagedFiles();
|
|
111
|
+
if (!packaged.has('SKILL.md')) {
|
|
112
|
+
throw new Error(`CLI 包内缺少官方指南文件(SKILL.md):${packagedSkillRoot()}`);
|
|
113
|
+
}
|
|
114
|
+
const installDir = path.join(options.targetRoot, 'qmuse-cli');
|
|
115
|
+
const previous = readInstallManifest(options.targetRoot);
|
|
116
|
+
// 已装文件与包内不一致时分两类:用户确实改过(保护)/上游内容变更(应刷新)。
|
|
117
|
+
const userModified = [];
|
|
118
|
+
const upstreamChanged = [];
|
|
119
|
+
if (existsSync(installDir)) {
|
|
120
|
+
for (const [relative, digest] of packaged) {
|
|
121
|
+
const file = path.join(installDir, relative);
|
|
122
|
+
if (!existsSync(file))
|
|
123
|
+
continue;
|
|
124
|
+
let installed;
|
|
125
|
+
try {
|
|
126
|
+
installed = fileSha256(readFileSync(file, 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
installed = undefined;
|
|
130
|
+
}
|
|
131
|
+
if (installed === digest)
|
|
132
|
+
continue;
|
|
133
|
+
const baseline = previous?.installedDigests?.[relative];
|
|
134
|
+
// 无摘要基线(老版本安装)时按用户修改处理,保持保守语义。
|
|
135
|
+
if (baseline && installed === baseline)
|
|
136
|
+
upstreamChanged.push(relative);
|
|
137
|
+
else
|
|
138
|
+
userModified.push(relative);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const sameVersion = previous?.cliVersion === QMUSE_CLI_VERSION;
|
|
142
|
+
if (options.force !== true) {
|
|
143
|
+
if (sameVersion && userModified.length === 0 && upstreamChanged.length === 0) {
|
|
144
|
+
// 已安装内容与包完全一致:幂等返回,不重写文件。
|
|
145
|
+
const manifest = previous;
|
|
146
|
+
return { address: installDir, installedFiles: [...packaged.keys()], manifest, replaced: false };
|
|
147
|
+
}
|
|
148
|
+
if (userModified.length > 0) {
|
|
149
|
+
throw new Error(`检测到用户修改过的指南文件(${userModified.join(', ')})。保留修改请跳过安装;确需覆盖请加 --force。`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
mkdirSync(options.targetRoot, { recursive: true });
|
|
153
|
+
if (options.force === true) {
|
|
154
|
+
// 强制覆盖:仅移除“包内文件 ∪ 上次安装清单”范围的文件(用户自加文件一律保留),最后剪掉空目录。
|
|
155
|
+
const managed = new Set([...packaged.keys(), MANIFEST_FILE, ...(previous?.installedFiles ?? [])]);
|
|
156
|
+
if (existsSync(installDir)) {
|
|
157
|
+
const rm = (dir) => {
|
|
158
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
159
|
+
const full = path.join(dir, entry.name);
|
|
160
|
+
const rel = path.relative(installDir, full).split(path.sep).join('/');
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
rm(full);
|
|
163
|
+
if (readdirSync(full).length === 0)
|
|
164
|
+
rmSync(full, { recursive: true, force: true }); // 仅剪空目录(递归以兼容目录删除)
|
|
165
|
+
}
|
|
166
|
+
else if (managed.has(rel)) {
|
|
167
|
+
rmSync(full, { force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
rm(installDir);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (previous?.installedFiles?.length) {
|
|
175
|
+
// 升级残留清理:删“上次装的、新包不再提供且未被用户改动核对需要”的文件(无旧 digest,直接按清单删)。
|
|
176
|
+
for (const staleFile of previous.installedFiles) {
|
|
177
|
+
if (packaged.has(staleFile))
|
|
178
|
+
continue; // 新包仍提供 → cpSync 覆盖
|
|
179
|
+
if (staleFile === MANIFEST_FILE)
|
|
180
|
+
continue; // 清单本体重写
|
|
181
|
+
const target = path.join(installDir, staleFile);
|
|
182
|
+
try {
|
|
183
|
+
if (existsSync(target))
|
|
184
|
+
rmSync(target, { force: true });
|
|
185
|
+
}
|
|
186
|
+
catch { /* 清理尽力而为 */ }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
cpSync(packagedSkillRoot(), installDir, { recursive: true });
|
|
190
|
+
const manifest = {
|
|
191
|
+
schemaVersion: 1,
|
|
192
|
+
skill: 'qmuse-cli',
|
|
193
|
+
cliVersion: QMUSE_CLI_VERSION,
|
|
194
|
+
...(options.agent ? { agent: options.agent } : {}),
|
|
195
|
+
source: 'packaged',
|
|
196
|
+
installedAt: new Date().toISOString(),
|
|
197
|
+
};
|
|
198
|
+
const installedFiles = [...packaged.keys(), MANIFEST_FILE];
|
|
199
|
+
const installedDigests = Object.fromEntries(packaged.entries());
|
|
200
|
+
writeFileSync(path.join(installDir, MANIFEST_FILE), `${JSON.stringify({ ...manifest, installedFiles, installedDigests }, null, 2)}\n`, 'utf8');
|
|
201
|
+
// 安装自检:必要文件必须存在且版本元数据可读。
|
|
202
|
+
for (const required of ['SKILL.md', 'references/cloud.md', 'references/core-flows.md']) {
|
|
203
|
+
const file = path.join(installDir, required);
|
|
204
|
+
if (!existsSync(file) || !statSync(file).isFile()) {
|
|
205
|
+
throw new Error(`安装自检失败:缺少 ${required}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const persisted = readInstallManifest(options.targetRoot);
|
|
209
|
+
if (!persisted || !persisted.installedFiles)
|
|
210
|
+
throw new Error('安装自检失败:版本元数据或文件清单不可读');
|
|
211
|
+
return { address: installDir, installedFiles, manifest: persisted, replaced: true };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 升级 CLI 后同步「已存在的」官方指南安装 —— 不创建新目标,不覆盖用户修改。
|
|
215
|
+
* 供 `qmuse upgrade` 复用:只处理探测得到的既有安装,冲突跳过而非报错。
|
|
216
|
+
*/
|
|
217
|
+
export function syncInstalledSkills(options = {}) {
|
|
218
|
+
const candidates = [];
|
|
219
|
+
const agentRoots = options.agentRoots ?? {};
|
|
220
|
+
for (const agent of options.agents ?? ['codex', 'claude']) {
|
|
221
|
+
candidates.push({ root: agentRoots[agent] ?? defaultTargetForAgent(agent), agent });
|
|
222
|
+
}
|
|
223
|
+
for (const root of options.extraTargets ?? [])
|
|
224
|
+
candidates.push({ root });
|
|
225
|
+
const results = [];
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
for (const candidate of candidates) {
|
|
228
|
+
const installDir = path.join(candidate.root, 'qmuse-cli');
|
|
229
|
+
if (!existsSync(installDir) || seen.has(installDir))
|
|
230
|
+
continue;
|
|
231
|
+
seen.add(installDir);
|
|
232
|
+
const manifest = readInstallManifest(candidate.root);
|
|
233
|
+
const base = {
|
|
234
|
+
address: installDir,
|
|
235
|
+
...(candidate.agent ?? manifest?.agent ? { agent: candidate.agent ?? manifest?.agent } : {}),
|
|
236
|
+
};
|
|
237
|
+
if (!manifest) {
|
|
238
|
+
// 没有版本元数据:当作非本工具安装,不触碰。
|
|
239
|
+
results.push({ ...base, outcome: 'skipped-user-modified', detail: '缺少安装元数据,未改动' });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = installAgentSkill({ targetRoot: candidate.root, agent: candidate.agent ?? manifest.agent });
|
|
244
|
+
results.push({ ...base, outcome: result.replaced ? 'refreshed' : 'up-to-date' });
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
+
// 用户改过指南:保留用户版本,不阻断升级。
|
|
249
|
+
results.push({
|
|
250
|
+
...base,
|
|
251
|
+
outcome: /用户修改过/u.test(message) ? 'skipped-user-modified' : 'failed',
|
|
252
|
+
detail: message.slice(0, 200),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账户域(C29):Point/Benefit 现有 SPACE 只读契约。
|
|
3
|
+
* (套餐/订阅由 PC 承载,无独立 CLI 可用接口——清单相应项标注依赖。)
|
|
4
|
+
*/
|
|
5
|
+
import { apiCall } from '../client.js';
|
|
6
|
+
export function pointBalance(spaceId, cfg) {
|
|
7
|
+
return apiCall('GET', `/api/point/balance?spaceId=${encodeURIComponent(spaceId)}`, undefined, cfg);
|
|
8
|
+
}
|
|
9
|
+
export function pointRecords(spaceId, query = {}, cfg) {
|
|
10
|
+
return apiCall('POST', '/api/point/records', { spaceId, ...query }, cfg);
|
|
11
|
+
}
|
|
12
|
+
export function pointConsumeStats(spaceId, query = {}, cfg) {
|
|
13
|
+
const { pageIndex, pageSize, ...rest } = query;
|
|
14
|
+
return apiCall('POST', '/api/point/records/consume-stats', {
|
|
15
|
+
spaceId,
|
|
16
|
+
...(rest),
|
|
17
|
+
...(pageIndex ? { pageIndex: Number(pageIndex) } : {}),
|
|
18
|
+
...(pageSize ? { pageSize: Number(pageSize) } : {}),
|
|
19
|
+
}, cfg);
|
|
20
|
+
}
|
|
21
|
+
export function benefitEntitlements(spaceId, cfg) {
|
|
22
|
+
return apiCall('GET', `/api/benefit/entitlements?spaceId=${encodeURIComponent(spaceId)}`, undefined, cfg);
|
|
23
|
+
}
|
package/dist/app/api.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用域接口(C03):基于 musepub AppController 现有契约。
|
|
3
|
+
*
|
|
4
|
+
* - POST /api/app/queryAppListBySpaceId 分页查询空间应用列表
|
|
5
|
+
* - POST /api/app/queryAppInfo 查询应用详情
|
|
6
|
+
* - POST /api/app/update 白名单字段更新
|
|
7
|
+
* - POST /api/app/delete 删除应用
|
|
8
|
+
*/
|
|
9
|
+
import { apiCall } from '../client.js';
|
|
10
|
+
function cleanOptional(value) {
|
|
11
|
+
const trimmed = value?.trim?.();
|
|
12
|
+
return trimmed ? trimmed : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function queryAppListBySpaceId(spaceId, query, cfg) {
|
|
15
|
+
const body = {
|
|
16
|
+
spaceId,
|
|
17
|
+
pageIndex: query.pageIndex ?? 1,
|
|
18
|
+
pageSize: query.pageSize ?? 20,
|
|
19
|
+
};
|
|
20
|
+
const keyword = cleanOptional(query.searchKeyword);
|
|
21
|
+
if (keyword)
|
|
22
|
+
body.searchKeyword = keyword;
|
|
23
|
+
const appType = cleanOptional(query.appType);
|
|
24
|
+
if (appType)
|
|
25
|
+
body.appType = appType;
|
|
26
|
+
const publishStatus = cleanOptional(query.publishStatus);
|
|
27
|
+
if (publishStatus)
|
|
28
|
+
body.publishStatus = publishStatus;
|
|
29
|
+
return apiCall('POST', '/api/app/queryAppListBySpaceId', body, cfg);
|
|
30
|
+
}
|
|
31
|
+
export function queryAppInfo(appId, cfg) {
|
|
32
|
+
return apiCall('POST', '/api/app/queryAppInfo', { appId }, cfg);
|
|
33
|
+
}
|
|
34
|
+
export async function updateApp(appId, patch, cfg) {
|
|
35
|
+
const body = { appId };
|
|
36
|
+
if (cleanOptional(patch.title))
|
|
37
|
+
body.title = patch.title.trim();
|
|
38
|
+
if (patch.description !== undefined)
|
|
39
|
+
body.description = patch.description;
|
|
40
|
+
if (patch.requireLogin !== undefined)
|
|
41
|
+
body.requireLogin = patch.requireLogin;
|
|
42
|
+
if (cleanOptional(patch.defaultDevice))
|
|
43
|
+
body.defaultDevice = patch.defaultDevice.trim();
|
|
44
|
+
if (Object.keys(body).length === 1) {
|
|
45
|
+
throw new Error('app update 需要至少一个待修改字段:--title / --description / --require-login / --default-device');
|
|
46
|
+
}
|
|
47
|
+
return apiCall('POST', '/api/app/update', body, cfg);
|
|
48
|
+
}
|
|
49
|
+
export function deleteApp(appId, cfg) {
|
|
50
|
+
return apiCall('POST', '/api/app/delete', { appId }, cfg);
|
|
51
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地应用绑定(C03.06/C03.07/C03.08)。
|
|
3
|
+
*
|
|
4
|
+
* 独立的轻量绑定文件 `.qmuse/app.json`:记录当前目录绑定的已有应用,
|
|
5
|
+
* 供 `qmuse app use / current / unlink` 及后续 dev/publish 命令读取。
|
|
6
|
+
* 与导入链路的 `.qmuse/project.json` 互不干扰:导入绑定继续由
|
|
7
|
+
* import/project.ts 管理(含 sourceHash/jobId 等导入语义字段)。
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
export const APP_BINDING_FILE = join('.qmuse', 'app.json');
|
|
12
|
+
export function loadAppBinding(rootDir = process.cwd()) {
|
|
13
|
+
const path = join(rootDir, APP_BINDING_FILE);
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
19
|
+
&& parsed.schemaVersion === 1
|
|
20
|
+
&& typeof parsed.appId === 'string'
|
|
21
|
+
&& parsed.appId.trim()) {
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function saveAppBinding(binding, rootDir = process.cwd()) {
|
|
31
|
+
const path = join(rootDir, APP_BINDING_FILE);
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const value = {
|
|
34
|
+
...binding,
|
|
35
|
+
appId: binding.appId.trim(),
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
boundAt: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
40
|
+
return path;
|
|
41
|
+
}
|
|
42
|
+
export function clearAppBinding(rootDir = process.cwd()) {
|
|
43
|
+
const path = join(rootDir, APP_BINDING_FILE);
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
return false;
|
|
46
|
+
unlinkSync(path);
|
|
47
|
+
return true;
|
|
48
|
+
}
|