@qcplay/cli 1.0.5 → 1.0.6
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 +1 -0
- package/bin/qcplay.js +199 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- 认证默认请求线上接口:`https://cli.qcg.ink`
|
|
8
8
|
- 发布默认请求线上接口:`https://cli.qcg.ink`
|
|
9
9
|
- 登录默认打开线上页面:`https://cli.qcg.ink/auth.html`
|
|
10
|
+
- `qcplay-cli update` 会更新 npm 全局包,并同步 `~/.agents/skills`
|
|
10
11
|
- 本地调试时可直接使用 `--local`
|
|
11
12
|
- `--local` 默认指向 `http://127.0.0.1:8787`
|
|
12
13
|
- `qcplay-cli auth --local` 会打开仓库里的本地页面 `web/auth.html`
|
package/bin/qcplay.js
CHANGED
|
@@ -93,16 +93,24 @@ const AGENTS_DIR = path.join(os.homedir(), ".agents");
|
|
|
93
93
|
const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
|
|
94
94
|
const SKILLS_DIR = path.join(AGENTS_DIR, "skills");
|
|
95
95
|
|
|
96
|
-
function
|
|
96
|
+
function readPackageMetadata() {
|
|
97
97
|
try {
|
|
98
98
|
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
|
|
99
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
name: packageJson.name || "@qcplay/cli",
|
|
101
|
+
version: packageJson.version || "0.0.0"
|
|
102
|
+
};
|
|
100
103
|
} catch {
|
|
101
|
-
return
|
|
104
|
+
return {
|
|
105
|
+
name: "@qcplay/cli",
|
|
106
|
+
version: "0.0.0"
|
|
107
|
+
};
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
const
|
|
111
|
+
const PACKAGE_METADATA = readPackageMetadata();
|
|
112
|
+
const PACKAGE_NAME = PACKAGE_METADATA.name;
|
|
113
|
+
const PACKAGE_VERSION = PACKAGE_METADATA.version;
|
|
106
114
|
const colorsEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
107
115
|
|
|
108
116
|
const chalk = {
|
|
@@ -126,6 +134,7 @@ function printRootHelp() {
|
|
|
126
134
|
|
|
127
135
|
Usage:
|
|
128
136
|
qcplay-cli install [--local]
|
|
137
|
+
qcplay-cli update
|
|
129
138
|
qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]
|
|
130
139
|
qcplay-cli auth permissions [--local] [--key <key>] [--json]
|
|
131
140
|
qcplay-cli article
|
|
@@ -141,6 +150,11 @@ Options:
|
|
|
141
150
|
`);
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
function printUpdateHelp() {
|
|
154
|
+
console.log("Usage:");
|
|
155
|
+
console.log(" qcplay-cli update");
|
|
156
|
+
}
|
|
157
|
+
|
|
144
158
|
function printAuthHelp() {
|
|
145
159
|
console.log("Usage:");
|
|
146
160
|
console.log(" qcplay-cli auth [login|status|logout] [--local] [--backend <url>] [--auth-page <url>]");
|
|
@@ -236,15 +250,70 @@ async function ensureLocalDirs() {
|
|
|
236
250
|
await ensureDir(SKILLS_DIR);
|
|
237
251
|
}
|
|
238
252
|
|
|
253
|
+
async function filesAreEqual(sourcePath, targetPath) {
|
|
254
|
+
try {
|
|
255
|
+
const [sourceContent, targetContent] = await Promise.all([
|
|
256
|
+
fs.promises.readFile(sourcePath),
|
|
257
|
+
fs.promises.readFile(targetPath)
|
|
258
|
+
]);
|
|
259
|
+
return sourceContent.equals(targetContent);
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function syncDirectoryChanges(sourceDir, targetDir, rootDir = sourceDir, changes = { added: [], updated: [] }) {
|
|
266
|
+
await ensureDir(targetDir);
|
|
267
|
+
const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
|
|
268
|
+
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
271
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
272
|
+
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
await syncDirectoryChanges(sourcePath, targetPath, rootDir, changes);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!entry.isFile()) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const relativePath = path.relative(rootDir, sourcePath);
|
|
283
|
+
if (!(await pathExists(targetPath))) {
|
|
284
|
+
await ensureDir(path.dirname(targetPath));
|
|
285
|
+
await fs.promises.copyFile(sourcePath, targetPath);
|
|
286
|
+
changes.added.push(relativePath);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (await filesAreEqual(sourcePath, targetPath)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await fs.promises.copyFile(sourcePath, targetPath);
|
|
295
|
+
changes.updated.push(relativePath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return changes;
|
|
299
|
+
}
|
|
300
|
+
|
|
239
301
|
async function installSkills() {
|
|
240
302
|
const sourceDir = path.resolve(__dirname, "../templates/skills");
|
|
241
303
|
|
|
242
304
|
if (!(await pathExists(sourceDir))) {
|
|
243
|
-
return
|
|
305
|
+
return {
|
|
306
|
+
available: false,
|
|
307
|
+
added: [],
|
|
308
|
+
updated: []
|
|
309
|
+
};
|
|
244
310
|
}
|
|
245
311
|
|
|
246
|
-
await
|
|
247
|
-
return
|
|
312
|
+
const changes = await syncDirectoryChanges(sourceDir, SKILLS_DIR);
|
|
313
|
+
return {
|
|
314
|
+
available: true,
|
|
315
|
+
...changes
|
|
316
|
+
};
|
|
248
317
|
}
|
|
249
318
|
|
|
250
319
|
async function saveConfig(authBackendUrl, publishBackendUrl, authPageUrl, localMode = false) {
|
|
@@ -264,6 +333,19 @@ async function loadConfig() {
|
|
|
264
333
|
return readJson(CONFIG_FILE);
|
|
265
334
|
}
|
|
266
335
|
|
|
336
|
+
async function updateStoredConfigVersion(version) {
|
|
337
|
+
const config = await loadConfig();
|
|
338
|
+
if (Object.keys(config).length === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
await writeJson(CONFIG_FILE, {
|
|
343
|
+
...config,
|
|
344
|
+
version,
|
|
345
|
+
updated_at: Date.now()
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
267
349
|
function normalizeText(value) {
|
|
268
350
|
return String(value ?? "").trim();
|
|
269
351
|
}
|
|
@@ -489,6 +571,28 @@ function openBrowser(targetUrl) {
|
|
|
489
571
|
});
|
|
490
572
|
}
|
|
491
573
|
|
|
574
|
+
function getNpmCommand() {
|
|
575
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function runCommand(command, args) {
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
580
|
+
const child = spawn(command, args, {
|
|
581
|
+
stdio: "inherit"
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
child.on("error", reject);
|
|
585
|
+
child.on("close", code => {
|
|
586
|
+
if (code === 0) {
|
|
587
|
+
resolve();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
reject(new Error(`命令执行失败 (${code}): ${command} ${args.join(" ")}`));
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
492
596
|
function delay(ms) {
|
|
493
597
|
return new Promise(resolve => {
|
|
494
598
|
setTimeout(resolve, ms);
|
|
@@ -675,6 +779,28 @@ function printPermissionTree(items) {
|
|
|
675
779
|
}
|
|
676
780
|
}
|
|
677
781
|
|
|
782
|
+
function formatAccountSummary(data = {}) {
|
|
783
|
+
const id = Number(data.id || 0);
|
|
784
|
+
const name = normalizeText(data.name) || "未知账号";
|
|
785
|
+
const type = normalizeText(data.type);
|
|
786
|
+
const status = normalizeText(data.status);
|
|
787
|
+
const parts = [name];
|
|
788
|
+
|
|
789
|
+
if (id > 0) {
|
|
790
|
+
parts.push(`id=${id}`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (type) {
|
|
794
|
+
parts.push(`type=${type}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (status) {
|
|
798
|
+
parts.push(`status=${status}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return parts.join(" | ");
|
|
802
|
+
}
|
|
803
|
+
|
|
678
804
|
async function installCommand(options) {
|
|
679
805
|
const config = await loadConfig();
|
|
680
806
|
const localMode = isLocalMode(options, config);
|
|
@@ -690,7 +816,7 @@ async function installCommand(options) {
|
|
|
690
816
|
console.log(chalk.green("✔ 本地目录已创建"));
|
|
691
817
|
|
|
692
818
|
const skillsInstalled = await installSkills();
|
|
693
|
-
if (skillsInstalled) {
|
|
819
|
+
if (skillsInstalled.available) {
|
|
694
820
|
console.log(chalk.green("✔ Skills 已安装"));
|
|
695
821
|
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
696
822
|
} else {
|
|
@@ -708,6 +834,56 @@ async function installCommand(options) {
|
|
|
708
834
|
console.log("");
|
|
709
835
|
}
|
|
710
836
|
|
|
837
|
+
function printSkillsSyncResult(result) {
|
|
838
|
+
if (!result.available) {
|
|
839
|
+
console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const addedCount = result.added.length;
|
|
844
|
+
const updatedCount = result.updated.length;
|
|
845
|
+
if (addedCount === 0 && updatedCount === 0) {
|
|
846
|
+
console.log(chalk.green("✔ Skills 已检查,无新增或变更"));
|
|
847
|
+
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const parts = [];
|
|
852
|
+
if (addedCount > 0) {
|
|
853
|
+
parts.push(`新增 ${addedCount} 个文件`);
|
|
854
|
+
}
|
|
855
|
+
if (updatedCount > 0) {
|
|
856
|
+
parts.push(`更新 ${updatedCount} 个文件`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
console.log(chalk.green(`✔ Skills 已同步(${parts.join(",")})`));
|
|
860
|
+
console.log(`Skills 目录: ${chalk.gray(SKILLS_DIR)}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function updateCommand() {
|
|
864
|
+
console.log("");
|
|
865
|
+
console.log(chalk.cyan("正在更新 QCPlay CLI..."));
|
|
866
|
+
console.log(chalk.gray(`当前版本: ${PACKAGE_VERSION}`));
|
|
867
|
+
console.log("");
|
|
868
|
+
|
|
869
|
+
await ensureLocalDirs();
|
|
870
|
+
await runCommand(getNpmCommand(), ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
871
|
+
|
|
872
|
+
const latestPackage = readPackageMetadata();
|
|
873
|
+
await updateStoredConfigVersion(latestPackage.version);
|
|
874
|
+
const skillsInstalled = await installSkills();
|
|
875
|
+
|
|
876
|
+
console.log("");
|
|
877
|
+
console.log(chalk.green("CLI 更新完成"));
|
|
878
|
+
if (latestPackage.version !== PACKAGE_VERSION) {
|
|
879
|
+
console.log(chalk.gray(`版本: ${PACKAGE_VERSION} -> ${latestPackage.version}`));
|
|
880
|
+
} else {
|
|
881
|
+
console.log(chalk.gray(`版本: ${latestPackage.version}`));
|
|
882
|
+
}
|
|
883
|
+
printSkillsSyncResult(skillsInstalled);
|
|
884
|
+
console.log("");
|
|
885
|
+
}
|
|
886
|
+
|
|
711
887
|
async function authCommand(rawArgs = []) {
|
|
712
888
|
const parsed = parseBackendOptions(rawArgs);
|
|
713
889
|
const options = parsed.options;
|
|
@@ -734,7 +910,11 @@ async function authCommand(rawArgs = []) {
|
|
|
734
910
|
} catch {}
|
|
735
911
|
|
|
736
912
|
await waitForLoginSuccessSequence(backendUrl, previousSequence);
|
|
913
|
+
const statusResponse = await requestJson("GET", backendUrl, "/api/auth/status");
|
|
737
914
|
console.log(chalk.green("登录成功"));
|
|
915
|
+
if (statusResponse.data?.logged_in) {
|
|
916
|
+
console.log(chalk.gray(`当前账号: ${formatAccountSummary(statusResponse.data)}`));
|
|
917
|
+
}
|
|
738
918
|
return;
|
|
739
919
|
}
|
|
740
920
|
|
|
@@ -742,6 +922,7 @@ async function authCommand(rawArgs = []) {
|
|
|
742
922
|
const response = await requestJson("GET", backendUrl, "/api/auth/status");
|
|
743
923
|
if (response.data?.logged_in) {
|
|
744
924
|
console.log(chalk.green("已登录"));
|
|
925
|
+
console.log(chalk.gray(`当前账号: ${formatAccountSummary(response.data)}`));
|
|
745
926
|
return;
|
|
746
927
|
}
|
|
747
928
|
|
|
@@ -1189,6 +1370,16 @@ async function main() {
|
|
|
1189
1370
|
return;
|
|
1190
1371
|
}
|
|
1191
1372
|
|
|
1373
|
+
if (command === "update") {
|
|
1374
|
+
if (subcommand === "-h" || subcommand === "--help") {
|
|
1375
|
+
printUpdateHelp();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
await runWithErrorBanner("更新失败", () => updateCommand());
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1192
1383
|
if (command === "auth") {
|
|
1193
1384
|
if (subcommand === "-h" || subcommand === "--help") {
|
|
1194
1385
|
printAuthHelp();
|