qiaoya-community-cli 1.1.11

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 ADDED
@@ -0,0 +1,159 @@
1
+ # qiaoya-cli
2
+
3
+ 敲鸭社区 Go 版 agent skill 安装器与公开信息 runtime。
4
+
5
+ 目标体验:
6
+
7
+ ```bash
8
+ # 推荐:已安装 Node.js / npm
9
+ npx qiaoya-community-cli@latest install
10
+ ```
11
+
12
+ ```bash
13
+ # macOS / Linux,无需 Node.js
14
+ curl -fsSL https://code.xhyovo.cn/install | sh
15
+ ```
16
+
17
+ ```powershell
18
+ # Windows PowerShell,无需 Node.js
19
+ irm https://code.xhyovo.cn/install.ps1 | iex
20
+ ```
21
+
22
+ 用户执行一行命令后,安装器会把敲鸭社区能力写入常用 AI agent 的 skill/rules 目录。之后用户直接在 Codex、Claude Code、Cursor、Windsurf、OpenClaw 里对话即可了解社区。
23
+
24
+ ## 主链路
25
+
26
+ `qiaoya` 是一个单文件 Go binary,同时负责:
27
+
28
+ - 安装 skill/rules:`qiaoya install`
29
+ - 自检:`qiaoya doctor`
30
+ - 卸载:`qiaoya uninstall`
31
+ - 提示更新:`qiaoya update`
32
+ - 浏览器授权登录:`qiaoya auth login`
33
+ - 查询公开信息:`qiaoya --json public overview`
34
+ - 查询课程:`qiaoya --json public courses`
35
+ - 查询 AI 日报:`qiaoya --json ai-news today`
36
+
37
+ 推荐入口是 `npx qiaoya-community-cli@latest install`。如果用户没有 Node、npm 或 npx,则使用 macOS/Linux 的 `curl | sh` 或 Windows PowerShell 的 `irm | iex` 零依赖安装脚本。脚本只依赖系统自带 shell、curl 和 SHA256 校验命令。
38
+
39
+ ## 支持的 Agent
40
+
41
+ 默认安装:
42
+
43
+ ```bash
44
+ qiaoya install --agents auto
45
+ ```
46
+
47
+ 写入位置:
48
+
49
+ ```text
50
+ Codex ~/.codex/skills/qiaoya/SKILL.md
51
+ Claude Code ~/.claude/skills/qiaoya/SKILL.md
52
+ OpenClaw ~/.openclaw/skills/qiaoya/SKILL.md
53
+ Cursor <project>/.cursor/rules/qiaoya.mdc
54
+ Windsurf ~/.codeium/windsurf/memories/global_rules.md
55
+ Windsurf <project>/.windsurf/rules/qiaoya.md
56
+ ```
57
+
58
+ Cursor 是项目级 rules。Windsurf 会写入全局规则;如果检测到项目目录,也会同时写入工作区规则。若要显式安装项目规则:
59
+
60
+ ```bash
61
+ qiaoya install --agents cursor,windsurf --project-dir /path/to/project
62
+ ```
63
+
64
+ ## 安装脚本
65
+
66
+ 推荐:
67
+
68
+ ```bash
69
+ npx qiaoya-community-cli@latest install
70
+ ```
71
+
72
+ macOS / Linux:
73
+
74
+ ```bash
75
+ curl -fsSL https://code.xhyovo.cn/install | sh
76
+ ```
77
+
78
+ Windows PowerShell:
79
+
80
+ ```powershell
81
+ irm https://code.xhyovo.cn/install.ps1 | iex
82
+ ```
83
+
84
+ 脚本会:
85
+
86
+ 1. 识别系统和 CPU 架构
87
+ 2. 下载对应 `qiaoya-*` 单文件 binary
88
+ 3. 下载并校验 `checksums.txt`
89
+ 4. 运行 `qiaoya install --agents auto`
90
+ 5. 把 runtime 固定安装到 `~/.qiaoya/bin/qiaoya`
91
+
92
+ `npx` 入口会下载同一组 release binary 和 `checksums.txt`,校验通过后把参数转交给临时 `qiaoya` runtime;实际安装后的 runtime 仍固定在 `~/.qiaoya/bin/qiaoya`。
93
+
94
+ 可选环境变量:
95
+
96
+ ```bash
97
+ QIAOYA_RELEASE_BASE_URL=https://github.com/xhyqaq/qiaoya-cli/releases/latest/download
98
+ QIAOYA_AGENTS=codex,claude,cursor,windsurf,openclaw
99
+ QIAOYA_PROJECT_DIR=/path/to/project
100
+ ```
101
+
102
+ ## 登录设计
103
+
104
+ 登录态能力走浏览器授权,不让 AI 或 CLI 直接接触用户密码。
105
+
106
+ 1. CLI 执行 `qiaoya auth login`
107
+ 2. CLI 在本机随机打开一个临时 callback 端口,例如 `http://127.0.0.1:<port>/callback`
108
+ 3. CLI 生成 `state`、PKCE `code_verifier/code_challenge`
109
+ 4. CLI 打开敲鸭现有授权链接,例如 `https://code.xhyovo.cn/api/public/oauth2/authorize?...`
110
+ 5. 用户在浏览器里使用已有敲鸭账号登录;如果已经登录,直接进入授权确认页
111
+ 6. 用户确认授权后,网站把一次性授权码重定向回本机 callback
112
+ 7. CLI 校验 `state`,再用授权码和 `code_verifier` 换取短期、低权限 token,并存到本机安全位置
113
+
114
+ 常用命令:
115
+
116
+ ```bash
117
+ qiaoya auth login
118
+ qiaoya auth status
119
+ qiaoya auth logout
120
+ ```
121
+
122
+ Access Token 默认短期有效,Refresh Token 默认长期有效。CLI 会在登录态命令执行前自动刷新 Access Token;只有 Refresh Token 也失效或被撤销时,用户才需要重新执行 `qiaoya auth login`。
123
+
124
+ 无论哪种方案,skill 都不应该要求邮箱、密码、token 或 Cookie。登录态能力默认按 scope 授权,并对写操作继续要求用户确认。
125
+
126
+ 社区项目已经有 `/oauth2/authorize` 授权页和 `/api/public/oauth2/token` 令牌端点。`qiaoya-cli` 注册为 public OAuth2 client,使用 `client_authentication_method=none + PKCE`,CLI 不内置 `client_secret`。CLI 本机 callback 使用随机端口,因此后端 redirect URI 校验只允许 `127.0.0.1` 或 `localhost` 加固定 path,端口允许变化。
127
+
128
+ ## 开发
129
+
130
+ ```bash
131
+ go test ./...
132
+ go vet ./...
133
+ go build -o /tmp/qiaoya ./cmd/qiaoya
134
+ /tmp/qiaoya --help
135
+ /tmp/qiaoya install --dry-run --agents all --project-dir "$PWD"
136
+ ```
137
+
138
+ ## Release
139
+
140
+ 推送 `v*` tag 或手动触发 `.github/workflows/release-binaries.yml` 会构建:
141
+
142
+ ```text
143
+ qiaoya-darwin-arm64
144
+ qiaoya-darwin-amd64
145
+ qiaoya-linux-amd64
146
+ qiaoya-linux-arm64
147
+ qiaoya-windows-amd64.exe
148
+ qiaoya-windows-arm64.exe
149
+ checksums.txt
150
+ ```
151
+
152
+ 安装脚本默认从 GitHub Release `latest/download` 下载这些资产。后续如果要使用自有域名或 CDN,只需要让 `https://code.xhyovo.cn/install` 返回 `scripts/install.sh`,并把 release asset 反代或同步到稳定地址。
153
+
154
+ npm wrapper 从仓库根目录发布:
155
+
156
+ ```bash
157
+ npm test
158
+ npm publish
159
+ ```
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { run } = require("../lib/installer");
5
+
6
+ run(process.argv.slice(2)).catch((error) => {
7
+ const message = error && error.message ? error.message : String(error);
8
+ console.error(`qiaoya-community-cli: ${message}`);
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const http = require("http");
6
+ const https = require("https");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { spawnSync } = require("child_process");
10
+ const { fileURLToPath } = require("url");
11
+
12
+ const DEFAULT_RELEASE_BASE_URL = "https://code.xhyovo.cn/downloads/qiaoya/latest";
13
+
14
+ function assetName(platform = process.platform, arch = process.arch) {
15
+ let osName;
16
+ switch (platform) {
17
+ case "darwin":
18
+ osName = "darwin";
19
+ break;
20
+ case "linux":
21
+ osName = "linux";
22
+ break;
23
+ case "win32":
24
+ osName = "windows";
25
+ break;
26
+ default:
27
+ throw new Error(`暂不支持当前系统: ${platform}`);
28
+ }
29
+
30
+ let cpu;
31
+ switch (arch) {
32
+ case "x64":
33
+ case "amd64":
34
+ cpu = "amd64";
35
+ break;
36
+ case "arm64":
37
+ case "aarch64":
38
+ cpu = "arm64";
39
+ break;
40
+ default:
41
+ throw new Error(`暂不支持当前架构: ${arch}`);
42
+ }
43
+
44
+ const suffix = osName === "windows" ? ".exe" : "";
45
+ return `qiaoya-${osName}-${cpu}${suffix}`;
46
+ }
47
+
48
+ function joinURL(base, segment) {
49
+ return `${String(base).replace(/\/+$/, "")}/${segment}`;
50
+ }
51
+
52
+ function readLocalFile(url) {
53
+ return fs.promises.readFile(fileURLToPath(url));
54
+ }
55
+
56
+ function fetchURL(url, redirects = 0) {
57
+ const parsed = new URL(url);
58
+ if (parsed.protocol === "file:") {
59
+ return readLocalFile(parsed);
60
+ }
61
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
62
+ throw new Error(`不支持的下载协议: ${parsed.protocol}`);
63
+ }
64
+ if (redirects > 5) {
65
+ throw new Error(`下载重定向过多: ${url}`);
66
+ }
67
+
68
+ const client = parsed.protocol === "https:" ? https : http;
69
+ return new Promise((resolve, reject) => {
70
+ const request = client.get(parsed, (response) => {
71
+ const statusCode = response.statusCode || 0;
72
+ const location = response.headers.location;
73
+ if (statusCode >= 300 && statusCode < 400 && location) {
74
+ response.resume();
75
+ const next = new URL(location, parsed).toString();
76
+ fetchURL(next, redirects + 1).then(resolve, reject);
77
+ return;
78
+ }
79
+ if (statusCode < 200 || statusCode >= 300) {
80
+ response.resume();
81
+ reject(new Error(`下载失败 ${url}: HTTP ${statusCode}`));
82
+ return;
83
+ }
84
+ const chunks = [];
85
+ response.on("data", (chunk) => chunks.push(chunk));
86
+ response.on("end", () => resolve(Buffer.concat(chunks)));
87
+ });
88
+ request.on("error", reject);
89
+ request.setTimeout(30000, () => {
90
+ request.destroy(new Error(`下载超时: ${url}`));
91
+ });
92
+ });
93
+ }
94
+
95
+ function expectedChecksum(checksums, name) {
96
+ for (const rawLine of String(checksums).split(/\r?\n/)) {
97
+ const line = rawLine.trim();
98
+ if (!line) {
99
+ continue;
100
+ }
101
+ const parts = line.split(/\s+/);
102
+ if (parts.length >= 2 && parts[1] === name) {
103
+ return parts[0].toLowerCase();
104
+ }
105
+ }
106
+ throw new Error(`checksums.txt 中未找到 ${name}`);
107
+ }
108
+
109
+ function verifyChecksum(buffer, checksums, name) {
110
+ const expected = expectedChecksum(checksums, name);
111
+ const actual = crypto.createHash("sha256").update(buffer).digest("hex");
112
+ if (actual !== expected) {
113
+ throw new Error(`SHA256 校验失败: ${name}`);
114
+ }
115
+ }
116
+
117
+ async function downloadRuntime(options = {}) {
118
+ const releaseBaseURL = options.releaseBaseURL || process.env.QIAOYA_RELEASE_BASE_URL || DEFAULT_RELEASE_BASE_URL;
119
+ const name = options.assetName || assetName(options.platform, options.arch);
120
+ const [binary, checksums] = await Promise.all([
121
+ fetchURL(joinURL(releaseBaseURL, name)),
122
+ fetchURL(joinURL(releaseBaseURL, "checksums.txt")),
123
+ ]);
124
+ verifyChecksum(binary, checksums.toString("utf8"), name);
125
+ return { name, binary };
126
+ }
127
+
128
+ async function prepareRuntime(options = {}) {
129
+ const runtime = await downloadRuntime(options);
130
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "qiaoya-npx-"));
131
+ const binaryPath = path.join(tempDir, process.platform === "win32" ? "qiaoya.exe" : "qiaoya");
132
+ await fs.promises.writeFile(binaryPath, runtime.binary, { mode: 0o755 });
133
+ if (process.platform !== "win32") {
134
+ await fs.promises.chmod(binaryPath, 0o755);
135
+ }
136
+ return { binaryPath, tempDir, assetName: runtime.name };
137
+ }
138
+
139
+ function cleanup(tempDir) {
140
+ if (!tempDir) {
141
+ return;
142
+ }
143
+ fs.rmSync(tempDir, { recursive: true, force: true });
144
+ }
145
+
146
+ async function run(args) {
147
+ const { binaryPath, tempDir, assetName: name } = await prepareRuntime();
148
+ try {
149
+ console.error(`下载 qiaoya: ${name}`);
150
+ const result = spawnSync(binaryPath, args, { stdio: "inherit" });
151
+ if (result.error) {
152
+ throw result.error;
153
+ }
154
+ process.exitCode = typeof result.status === "number" ? result.status : 1;
155
+ } finally {
156
+ cleanup(tempDir);
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ DEFAULT_RELEASE_BASE_URL,
162
+ assetName,
163
+ expectedChecksum,
164
+ fetchURL,
165
+ joinURL,
166
+ prepareRuntime,
167
+ run,
168
+ verifyChecksum,
169
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "qiaoya-community-cli",
3
+ "version": "1.1.11",
4
+ "description": "Cross-platform npx installer for the Qiaoya Community agent skill runtime.",
5
+ "bin": {
6
+ "qiaoya-community-cli": "npm/bin/qiaoya-community-cli.js"
7
+ },
8
+ "files": [
9
+ "npm/bin",
10
+ "npm/lib",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "npm run test:npm",
15
+ "test:npm": "node --test npm/test/*.test.js",
16
+ "pack:check": "npm pack --dry-run"
17
+ },
18
+ "keywords": [
19
+ "qiaoya",
20
+ "agent",
21
+ "skill",
22
+ "codex",
23
+ "claude"
24
+ ],
25
+ "author": "xhyovo",
26
+ "license": "MIT",
27
+ "homepage": "https://code.xhyovo.cn",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/xhyqaq/qiaoya-cli.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/xhyqaq/qiaoya-cli/issues"
34
+ },
35
+ "engines": {
36
+ "node": ">=16"
37
+ }
38
+ }