@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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/bin/qcplay.js +199 -8
  3. 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 readPackageVersion() {
96
+ function readPackageMetadata() {
97
97
  try {
98
98
  const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
99
- return packageJson.version || "0.0.0";
99
+ return {
100
+ name: packageJson.name || "@qcplay/cli",
101
+ version: packageJson.version || "0.0.0"
102
+ };
100
103
  } catch {
101
- return "0.0.0";
104
+ return {
105
+ name: "@qcplay/cli",
106
+ version: "0.0.0"
107
+ };
102
108
  }
103
109
  }
104
110
 
105
- const PACKAGE_VERSION = readPackageVersion();
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 false;
305
+ return {
306
+ available: false,
307
+ added: [],
308
+ updated: []
309
+ };
244
310
  }
245
311
 
246
- await copyRecursive(sourceDir, SKILLS_DIR);
247
- return true;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qcplay/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "QCPlay CLI",
5
5
  "type": "module",
6
6
  "bin": {