@qcplay/cli 1.0.0

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/bin/qcplay.js ADDED
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import path from "path";
6
+ import { spawn } from "child_process";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const QCPLAY_DIR = path.join(os.homedir(), ".qcplay");
13
+ const AUTH_FILE = path.join(QCPLAY_DIR, "auth.json");
14
+ const CONFIG_FILE = path.join(QCPLAY_DIR, "config.json");
15
+ const SKILLS_DIR = path.join(QCPLAY_DIR, "skills");
16
+
17
+ const colorsEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
18
+
19
+ const chalk = {
20
+ red: text => colorize("\x1b[31m", text),
21
+ green: text => colorize("\x1b[32m", text),
22
+ yellow: text => colorize("\x1b[33m", text),
23
+ cyan: text => colorize("\x1b[36m", text),
24
+ gray: text => colorize("\x1b[90m", text)
25
+ };
26
+
27
+ function colorize(code, text) {
28
+ if (!colorsEnabled) {
29
+ return String(text);
30
+ }
31
+
32
+ return `${code}${text}\x1b[0m`;
33
+ }
34
+
35
+ function printRootHelp() {
36
+ console.log(`QCPlay CLI
37
+
38
+ Usage:
39
+ qcplay-cli install
40
+ qcplay-cli auth [login|status|logout]
41
+ qcplay-cli article
42
+ qcplay-cli article init [file]
43
+ qcplay-cli article publish <file> [--env <env>] [--url <url>] [--dry-run]
44
+ qcplay-cli www-article-list.store <file> [--env <env>] [--url <url>] [--dry-run]
45
+ qcplay-cli features
46
+ qcplay-cli where
47
+
48
+ Options:
49
+ -h, --help Show help
50
+ -V, --version Show version
51
+ `);
52
+ }
53
+
54
+ function printAuthHelp() {
55
+ console.log("Usage:");
56
+ console.log(" qcplay-cli auth");
57
+ console.log(" qcplay-cli auth status");
58
+ console.log(" qcplay-cli auth logout");
59
+ }
60
+
61
+ function printArticleHelp() {
62
+ console.log(`Usage:
63
+ qcplay-cli article
64
+ qcplay-cli article init [file]
65
+ qcplay-cli article publish <file> [--env <env>] [--url <url>] [--dry-run]`);
66
+ }
67
+
68
+ function getVendorDir() {
69
+ if (process.platform === "win32") {
70
+ return path.resolve(__dirname, "../vendor/win32");
71
+ }
72
+
73
+ if (process.platform === "darwin") {
74
+ return path.resolve(__dirname, "../vendor/darwin");
75
+ }
76
+
77
+ if (process.platform === "linux") {
78
+ return path.resolve(__dirname, "../vendor/linux");
79
+ }
80
+
81
+ throw new Error(`Unsupported platform: ${process.platform}`);
82
+ }
83
+
84
+ function getExePath(name) {
85
+ const vendorDir = getVendorDir();
86
+ return process.platform === "win32" ? path.join(vendorDir, `${name}.exe`) : path.join(vendorDir, name);
87
+ }
88
+
89
+ function runNative(name, args = []) {
90
+ return new Promise((resolve, reject) => {
91
+ const exePath = getExePath(name);
92
+
93
+ if (!fs.existsSync(exePath)) {
94
+ reject(new Error(`执行程序不存在: ${exePath}`));
95
+ return;
96
+ }
97
+
98
+ const child = spawn(exePath, args, {
99
+ stdio: "inherit",
100
+ shell: false
101
+ });
102
+
103
+ child.on("error", err => {
104
+ reject(err);
105
+ });
106
+
107
+ child.on("exit", code => {
108
+ if (code === 0) {
109
+ resolve();
110
+ } else {
111
+ reject(new Error(`${name} 执行失败,退出码: ${code}`));
112
+ }
113
+ });
114
+ });
115
+ }
116
+
117
+ async function pathExists(targetPath) {
118
+ try {
119
+ await fs.promises.access(targetPath);
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ async function ensureDir(targetPath) {
127
+ await fs.promises.mkdir(targetPath, { recursive: true });
128
+ }
129
+
130
+ async function copyRecursive(sourcePath, targetPath) {
131
+ const sourceStat = await fs.promises.stat(sourcePath);
132
+
133
+ if (sourceStat.isDirectory()) {
134
+ await ensureDir(targetPath);
135
+ const entries = await fs.promises.readdir(sourcePath);
136
+ for (const entry of entries) {
137
+ await copyRecursive(path.join(sourcePath, entry), path.join(targetPath, entry));
138
+ }
139
+ return;
140
+ }
141
+
142
+ await ensureDir(path.dirname(targetPath));
143
+ await fs.promises.copyFile(sourcePath, targetPath);
144
+ }
145
+
146
+ async function writeJson(filePath, value) {
147
+ await ensureDir(path.dirname(filePath));
148
+ await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
149
+ }
150
+
151
+ async function readJson(filePath) {
152
+ const raw = await fs.promises.readFile(filePath, "utf8");
153
+ return JSON.parse(raw);
154
+ }
155
+
156
+ async function removeFile(filePath) {
157
+ await fs.promises.rm(filePath, { force: true });
158
+ }
159
+
160
+ async function ensureLocalDirs() {
161
+ await ensureDir(QCPLAY_DIR);
162
+ await ensureDir(SKILLS_DIR);
163
+ }
164
+
165
+ async function installSkills() {
166
+ const sourceDir = path.resolve(__dirname, "../templates/skills");
167
+
168
+ if (!(await pathExists(sourceDir))) {
169
+ return false;
170
+ }
171
+
172
+ await copyRecursive(sourceDir, SKILLS_DIR);
173
+ return true;
174
+ }
175
+
176
+ async function saveConfig() {
177
+ await writeJson(CONFIG_FILE, {
178
+ cli: "qcplay-cli",
179
+ version: "1.0.0",
180
+ auth_file: AUTH_FILE,
181
+ skills_dir: SKILLS_DIR,
182
+ configured_at: Date.now()
183
+ });
184
+ }
185
+
186
+ async function getLoginStatus() {
187
+ if (!(await pathExists(AUTH_FILE))) {
188
+ return {
189
+ ok: false,
190
+ reason: `认证文件不存在: ${AUTH_FILE}`
191
+ };
192
+ }
193
+
194
+ let auth;
195
+
196
+ try {
197
+ auth = await readJson(AUTH_FILE);
198
+ } catch (err) {
199
+ return {
200
+ ok: false,
201
+ reason: `认证文件不是合法 JSON: ${err.message}`
202
+ };
203
+ }
204
+
205
+ const accessToken =
206
+ auth.access_token ||
207
+ auth.accessToken ||
208
+ auth.token ||
209
+ auth.data?.access_token ||
210
+ auth.data?.accessToken;
211
+
212
+ if (!accessToken) {
213
+ return {
214
+ ok: false,
215
+ reason: "认证文件中没有找到 access_token"
216
+ };
217
+ }
218
+
219
+ let expiresAt =
220
+ auth.expires_at ||
221
+ auth.expiresAt ||
222
+ auth.data?.expires_at ||
223
+ auth.data?.expiresAt;
224
+
225
+ if (expiresAt) {
226
+ expiresAt = Number(expiresAt);
227
+
228
+ if (expiresAt > 0 && expiresAt < 1000000000000) {
229
+ expiresAt = expiresAt * 1000;
230
+ }
231
+
232
+ if (Date.now() >= expiresAt) {
233
+ return {
234
+ ok: false,
235
+ reason: `登录状态已过期: ${new Date(expiresAt).toLocaleString()}`
236
+ };
237
+ }
238
+ }
239
+
240
+ return {
241
+ ok: true,
242
+ auth
243
+ };
244
+ }
245
+
246
+ function printFeatures() {
247
+ console.log("");
248
+ console.log(chalk.green("你现在可以使用以下功能:"));
249
+ console.log("");
250
+
251
+ console.log(chalk.cyan("1. 登录认证"));
252
+ console.log(" qcplay-cli auth");
253
+ console.log("");
254
+
255
+ console.log(chalk.cyan("4. 发布官网文章"));
256
+ console.log(" qcplay-cli www-article-list.store article.md");
257
+ console.log(chalk.gray(" 文章标题、分类、缩略图、标签等信息写在 article.md 顶部 Front Matter 中。"));
258
+ console.log("");
259
+
260
+ console.log(chalk.cyan("5. 查看本地配置目录"));
261
+ console.log(" qcplay-cli where");
262
+ console.log("");
263
+ }
264
+
265
+ function printWhere() {
266
+ console.log(QCPLAY_DIR);
267
+ }
268
+
269
+ async function installCommand() {
270
+ console.log("");
271
+ console.log(chalk.cyan("正在安装 QCPlay CLI..."));
272
+ console.log("");
273
+
274
+ await ensureLocalDirs();
275
+ console.log(chalk.green("✔ 本地目录已创建"));
276
+
277
+ const skillsInstalled = await installSkills();
278
+ if (skillsInstalled) {
279
+ console.log(chalk.green("✔ Skills 已安装"));
280
+ } else {
281
+ console.log(chalk.yellow("! 未找到 Skills 模板,已跳过"));
282
+ }
283
+
284
+ await saveConfig();
285
+ console.log(chalk.green("✔ 配置文件已生成"));
286
+
287
+ console.log("");
288
+ console.log(chalk.cyan("正在打开登录窗口..."));
289
+ console.log(chalk.gray("请在浏览器中完成账号密码登录。"));
290
+ console.log("");
291
+
292
+ await runNative("qcplay-auth", ["login"]);
293
+
294
+ const loginStatus = await getLoginStatus();
295
+ if (!loginStatus.ok) {
296
+ throw new Error(`登录流程结束,但未检测到有效认证文件。\n原因: ${loginStatus.reason}`);
297
+ }
298
+
299
+ console.log("");
300
+ console.log(chalk.green("✔ 登录成功"));
301
+ console.log(`认证文件: ${chalk.gray(AUTH_FILE)}`);
302
+
303
+ console.log("");
304
+ console.log(chalk.green("✔ QCPlay CLI 安装完成"));
305
+
306
+ printFeatures();
307
+ }
308
+
309
+ async function authCommand(action) {
310
+ if (!action || action === "login") {
311
+ console.log("");
312
+ console.log(chalk.cyan("正在打开 QCPlay 登录窗口..."));
313
+ console.log("");
314
+
315
+ await runNative("qcplay-auth", ["login"]);
316
+
317
+ const loginStatus = await getLoginStatus();
318
+ if (!loginStatus.ok) {
319
+ throw new Error(`登录失败。\n原因: ${loginStatus.reason}`);
320
+ }
321
+
322
+ console.log("");
323
+ console.log(chalk.green("✔ 登录成功"));
324
+ console.log(`认证文件: ${chalk.gray(AUTH_FILE)}`);
325
+ return;
326
+ }
327
+
328
+ if (action === "status") {
329
+ const loginStatus = await getLoginStatus();
330
+
331
+ if (!loginStatus.ok) {
332
+ console.log(chalk.yellow("未登录或登录无效"));
333
+ console.log(`原因: ${loginStatus.reason}`);
334
+ console.log("");
335
+ console.log("请执行:");
336
+ console.log(" qcplay-cli auth");
337
+ return;
338
+ }
339
+
340
+ console.log(chalk.green("已登录"));
341
+ console.log(`认证文件: ${AUTH_FILE}`);
342
+ return;
343
+ }
344
+
345
+ if (action === "logout") {
346
+ await removeFile(AUTH_FILE);
347
+ console.log(chalk.green("已退出登录"));
348
+ return;
349
+ }
350
+
351
+ printAuthHelp();
352
+ }
353
+
354
+ function parsePublishOptions(args) {
355
+ const options = {
356
+ env: undefined,
357
+ url: undefined,
358
+ dryRun: false
359
+ };
360
+ const positional = [];
361
+
362
+ for (let index = 0; index < args.length; index += 1) {
363
+ const current = args[index];
364
+
365
+ if (current === "--dry-run") {
366
+ options.dryRun = true;
367
+ continue;
368
+ }
369
+
370
+ if (current === "--env" || current === "--url") {
371
+ const next = args[index + 1];
372
+ if (!next || next.startsWith("--")) {
373
+ throw new Error(`${current} 缺少参数值`);
374
+ }
375
+
376
+ if (current === "--env") {
377
+ options.env = next;
378
+ } else {
379
+ options.url = next;
380
+ }
381
+
382
+ index += 1;
383
+ continue;
384
+ }
385
+
386
+ if (current.startsWith("--")) {
387
+ throw new Error(`未知参数: ${current}`);
388
+ }
389
+
390
+ positional.push(current);
391
+ }
392
+
393
+ return {
394
+ file: positional[0],
395
+ options
396
+ };
397
+ }
398
+
399
+ async function publishArticleCommand(file, options) {
400
+ if (!file) {
401
+ throw new Error("缺少文章文件,例如:qcplay-cli www-article-list.store article.md");
402
+ }
403
+
404
+ const articleFile = path.resolve(process.cwd(), file);
405
+ if (!(await pathExists(articleFile))) {
406
+ throw new Error(`文章文件不存在: ${articleFile}`);
407
+ }
408
+
409
+ const loginStatus = await getLoginStatus();
410
+ if (!loginStatus.ok) {
411
+ throw new Error(`未检测到有效登录状态。\n原因: ${loginStatus.reason}\n请先执行:qcplay-cli auth`);
412
+ }
413
+
414
+ const args = ["--content-file", articleFile];
415
+
416
+ if (options.env) {
417
+ args.push("--env", options.env);
418
+ }
419
+
420
+ if (options.url) {
421
+ args.push("--url", options.url);
422
+ }
423
+
424
+ if (options.dryRun) {
425
+ args.push("--dry-run");
426
+ }
427
+
428
+ console.log("");
429
+ console.log(chalk.cyan("正在发布官网文章..."));
430
+ console.log(chalk.gray(`文章文件: ${articleFile}`));
431
+ console.log("");
432
+
433
+ await runNative("publish-article", args);
434
+ }
435
+
436
+ async function initArticleTemplate(file = "article.md") {
437
+ const sourceFile = path.resolve(__dirname, "../templates/skills/qcplay-publish-article/article/article.md");
438
+ const targetFile = path.resolve(process.cwd(), file);
439
+
440
+ if (!(await pathExists(sourceFile))) {
441
+ throw new Error(`文章模板不存在: ${sourceFile}`);
442
+ }
443
+
444
+ if (await pathExists(targetFile)) {
445
+ throw new Error(`文件已存在: ${targetFile}`);
446
+ }
447
+
448
+ await copyRecursive(sourceFile, targetFile);
449
+
450
+ console.log(chalk.green("文章模板已创建:"));
451
+ console.log(targetFile);
452
+ console.log("");
453
+ console.log("请编辑该文件后执行:");
454
+ console.log("");
455
+ console.log(` qcplay-cli www-article-list.store ${file}`);
456
+ console.log("");
457
+ }
458
+
459
+ function printArticleTemplate() {
460
+ console.log(`
461
+ ${chalk.cyan("QCPlay 官网文章发布模板")}
462
+
463
+ 请创建一个 Markdown 文件,例如:
464
+
465
+ article.md
466
+
467
+ 文件内容参考:
468
+
469
+ ${chalk.gray("--------------------------------------------------")}
470
+ ---
471
+ article_title: 文章标题
472
+ thumbnail: https://example.com/thumbnail.png
473
+ move_thumbnail: https://example.com/mobile-thumbnail.png
474
+ article_excerpt: 文章描述,可以为空
475
+ article_url: article-url-slug
476
+ origin: QCPlay
477
+ status: "1"
478
+ cate_id: "2"
479
+ video_link: ""
480
+ is_hot: "0"
481
+ is_index: "0"
482
+ release_time: "2026-07-09"
483
+ area: "1"
484
+ sort: "100"
485
+ game_id: "39"
486
+ index_pc_img: ""
487
+ index_move_img: ""
488
+ ---
489
+
490
+ # 文章标题
491
+
492
+ 这里填写文章正文内容。
493
+
494
+ ## 一、文章小标题
495
+
496
+ 这里填写正文内容。
497
+
498
+ ## 二、内容说明
499
+
500
+ 这里继续填写正文。
501
+
502
+ ## 三、总结
503
+
504
+ 这里填写文章结尾内容。
505
+ ${chalk.gray("--------------------------------------------------")}
506
+
507
+ ${chalk.cyan("发布命令:")}
508
+
509
+ qcplay-cli www-article-list.store article.md
510
+
511
+ ${chalk.cyan("参数说明:")}
512
+
513
+ article_title 文章标题,必填
514
+ thumbnail 缩略图,必填
515
+ move_thumbnail 移动端缩略图,可为空
516
+ article_excerpt 文章描述,可为空
517
+ article_url 文章链接,建议英文 slug
518
+ origin 作者来源
519
+ status 1 发布,0 未发布
520
+ cate_id 分类 ID
521
+ video_link 视频链接,可为空
522
+ is_hot 1 热门,0 非热门
523
+ is_index 1 推荐,0 不推荐
524
+ release_time 发布时间,格式:年-月-日
525
+ area 所属区域
526
+ sort 排序,数字
527
+ game_id 游戏 ID,最强蜗牛是 39
528
+ index_pc_img 推荐 PC 端缩略图,可为空
529
+ index_move_img 推荐移动端缩略图,可为空
530
+
531
+ ${chalk.cyan("分类 ID:")}
532
+
533
+ 综合 2
534
+ 视频中心 8
535
+ 活动 3
536
+ 游戏攻略 7
537
+ 萌新入门 9
538
+ 萌新入门-攻略专区 25
539
+ 高手进阶 26
540
+ 活动攻略 27
541
+ 视频攻略 29
542
+
543
+ ${chalk.cyan("区域 ID:")}
544
+
545
+ PC 1
546
+ 资料站 3
547
+ 公益网站 4
548
+
549
+ ${chalk.cyan("游戏 ID:")}
550
+
551
+ 最强蜗牛 39
552
+
553
+ ${chalk.yellow("注意:")}
554
+
555
+ 1. article_content 不需要写在 Front Matter 中。
556
+ 2. Markdown 正文会自动作为 article_content。
557
+ 3. is_index2 默认 0,不需要填写。
558
+ 4. type 默认 1,不需要填写。
559
+ 5. 发布前请先登录:
560
+
561
+ qcplay-cli auth
562
+
563
+ `);
564
+ }
565
+
566
+ async function runWithErrorBanner(title, action) {
567
+ try {
568
+ await action();
569
+ } catch (err) {
570
+ console.error("");
571
+ console.error(chalk.red(`${title}:`));
572
+ console.error(err.message || err);
573
+ process.exit(1);
574
+ }
575
+ }
576
+
577
+ async function main() {
578
+ const args = process.argv.slice(2);
579
+ const [command, subcommand, ...rest] = args;
580
+
581
+ if (!command || command === "-h" || command === "--help") {
582
+ printRootHelp();
583
+ return;
584
+ }
585
+
586
+ if (command === "-V" || command === "--version") {
587
+ console.log("1.0.0");
588
+ return;
589
+ }
590
+
591
+ if (command === "install") {
592
+ await runWithErrorBanner("安装失败", installCommand);
593
+ return;
594
+ }
595
+
596
+ if (command === "auth") {
597
+ if (subcommand === "-h" || subcommand === "--help") {
598
+ printAuthHelp();
599
+ return;
600
+ }
601
+
602
+ await runWithErrorBanner("认证失败", () => authCommand(subcommand));
603
+ return;
604
+ }
605
+
606
+ if (command === "article") {
607
+ if (!subcommand || subcommand === "-h" || subcommand === "--help") {
608
+ if (!subcommand) {
609
+ printArticleTemplate();
610
+ } else {
611
+ printArticleHelp();
612
+ }
613
+ return;
614
+ }
615
+
616
+ if (subcommand === "init") {
617
+ const file = rest[0] || "article.md";
618
+ await runWithErrorBanner("创建模板失败", () => initArticleTemplate(file));
619
+ return;
620
+ }
621
+
622
+ if (subcommand === "publish") {
623
+ const { file, options } = parsePublishOptions(rest);
624
+ await runWithErrorBanner("发布失败", () => publishArticleCommand(file, options));
625
+ return;
626
+ }
627
+
628
+ printArticleHelp();
629
+ process.exit(1);
630
+ }
631
+
632
+ if (command === "www-article-list.store") {
633
+ const parsed = parsePublishOptions([subcommand, ...rest].filter(Boolean));
634
+ await runWithErrorBanner("发布失败", () => publishArticleCommand(parsed.file, parsed.options));
635
+ return;
636
+ }
637
+
638
+ if (command === "features") {
639
+ printFeatures();
640
+ return;
641
+ }
642
+
643
+ if (command === "where") {
644
+ printWhere();
645
+ return;
646
+ }
647
+
648
+ printRootHelp();
649
+ process.exit(1);
650
+ }
651
+
652
+ await main();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@qcplay/cli",
3
+ "version": "1.0.0",
4
+ "description": "QCPlay CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "qcplay": "./bin/qcplay.js",
8
+ "qcplay-cli": "./bin/qcplay.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "vendor",
13
+ "templates",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org/"
19
+ },
20
+ "engines": {
21
+ "node": ">=16 <17",
22
+ "npm": ">=8"
23
+ },
24
+ "scripts": {
25
+ "start": "node bin/qcplay.js",
26
+ "test:auth": "node bin/qcplay.js auth",
27
+ "test:publish": "node bin/qcplay.js www-article-list.store article.md",
28
+ "pack:check": "npm pack --dry-run"
29
+ },
30
+ "license": "MIT"
31
+ }