pdlab-cli 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/index.js +476 -0
  4. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 shadowcz007
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # pdlab
2
+
3
+ [1836 开源共建平台](https://github.com/shadowcz007/problem-driven-lab)的命令行工具。
4
+
5
+ 它是平台现有 REST API 的**薄封装**——没有任何绕过后端的旁路,权限、审核、积分规则一律由服务端判定。
6
+ 你在网页上是什么角色,在命令行里就是什么角色。
7
+
8
+ ```bash
9
+ npm install -g pdlab-cli
10
+ ```
11
+
12
+ 需要 Node 18 或更高(用到了全局 `fetch`)。
13
+
14
+ > 包名是 `pdlab-cli`,但装完之后敲的命令是 `pdlab`。
15
+ > (包名叫 `pdlab` 会被 npm 的防仿冒过滤判定为与已有包 `lab` 过于相似,无法发布。)
16
+
17
+ ## 快速开始
18
+
19
+ ```bash
20
+ # 首次使用需要告诉它服务器地址,之后会记住
21
+ pdlab login --api-url https://your-1836-instance.example.com
22
+ ```
23
+
24
+ `login` 会打印一个 8 位授权码并打开浏览器。你在网页上确认「以 XX 身份授权 pdlab 命令行工具」之后,
25
+ 终端会自动拿到凭证。整个过程不需要在命令行里输入密码。
26
+
27
+ ```bash
28
+ pdlab whoami # 看看当前是谁
29
+ pdlab project list # 列出项目
30
+ ```
31
+
32
+ ## 命令
33
+
34
+ | 命令 | 说明 |
35
+ |---|---|
36
+ | `pdlab login` | 浏览器授权登录。凭证长期有效,直到被撤销 |
37
+ | `pdlab register` | 提交注册申请。注册后需管理员审核,**不会**自动登录 |
38
+ | `pdlab logout` | 清除本地凭证(不撤销服务端的,见下方「撤销凭证」) |
39
+ | `pdlab whoami` | 查看当前登录账号 |
40
+ | `pdlab config get` / `set api-url <url>` | 查看/修改本地配置 |
41
+ | `pdlab project list \| get \| create \| update \| delete` | 项目的增删改查 |
42
+ | `pdlab post list \| get \| create \| update \| delete` | 内容的增删改查 |
43
+
44
+ 任何命令加 `--help` 看具体参数。
45
+
46
+ ### 例子
47
+
48
+ ```bash
49
+ # 按状态筛选项目
50
+ pdlab project list --status active
51
+
52
+ # 创建项目(需要项目创建权限,即「1836发起人」角色)
53
+ pdlab project create --title "钠电储能原型" --description "第一阶段验证"
54
+
55
+ # 往自己参与的项目发布内容(共建者发布后进待审队列)
56
+ pdlab post create \
57
+ --project-id <项目 id> \
58
+ --title "第三周进展" \
59
+ --body "本周完成了……" \
60
+ --media-links "https://example.com/a.pdf,https://example.com/b.mp4"
61
+
62
+ # 只看某个项目下的内容
63
+ pdlab post list --project-id <项目 id>
64
+ ```
65
+
66
+ ## 配置
67
+
68
+ 配置存在 `~/.pdlab/config.json`(目录 `0700`、文件 `0600`):
69
+
70
+ ```json
71
+ { "apiUrl": "https://your-1836-instance.example.com", "token": "pdlab_…" }
72
+ ```
73
+
74
+ API 地址的解析顺序:`--api-url` 参数 > `PDLAB_API_URL` 环境变量 > 配置文件。
75
+ 在 CI 里可以只给环境变量,不落配置文件。
76
+
77
+ ## 凭证与安全
78
+
79
+ - **凭证怎么来的**:走设备码授权——CLI 生成一次性 code,你在浏览器里确认后服务端才签发。
80
+ code 十分钟内有效、只能兑换一次。
81
+ - **存的是什么**:服务端只保存凭证的 SHA-256 哈希,明文只在签发那一刻交付给 CLI 一次。
82
+ 丢了就只能重新 `pdlab login`,任何人都无法从服务端把它读回来。
83
+ - **撤销凭证**:`pdlab logout` 只清本地文件,服务端的凭证仍然有效。
84
+ 要彻底作废请到网页的**个人中心 → API Token**,那里能看到每个凭证的签发时间、最近使用时间,并逐个撤销。
85
+ - **注意**:每执行一次 `pdlab login` 都会**新签发一个**凭证,旧的不会自动失效。
86
+ 换机器、换终端之后记得回上面那个页面清理。
87
+ - 账号被禁用时,它名下的所有凭证会立刻失去写权限。
88
+
89
+ ## 许可证
90
+
91
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,476 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_fs2 = require("fs");
28
+ var import_path2 = require("path");
29
+ var import_commander = require("commander");
30
+
31
+ // src/commands/login.ts
32
+ var import_open = __toESM(require("open"));
33
+
34
+ // src/config.ts
35
+ var import_fs = require("fs");
36
+ var import_os = require("os");
37
+ var import_path = require("path");
38
+ var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".pdlab");
39
+ var CONFIG_FILE = (0, import_path.join)(CONFIG_DIR, "config.json");
40
+ function readConfig() {
41
+ if (!(0, import_fs.existsSync)(CONFIG_FILE)) return {};
42
+ try {
43
+ return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+ function writeConfig(config) {
49
+ if (!(0, import_fs.existsSync)(CONFIG_DIR)) (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 448 });
50
+ (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
51
+ }
52
+ function updateConfig(patch) {
53
+ const next = { ...readConfig(), ...patch };
54
+ writeConfig(next);
55
+ return next;
56
+ }
57
+ function resolveApiUrl(flagValue) {
58
+ const apiUrl = flagValue ?? process.env.PDLAB_API_URL ?? readConfig().apiUrl;
59
+ if (!apiUrl) {
60
+ console.error("\u672A\u914D\u7F6E API \u5730\u5740\u3002\u8BF7\u5148\u8FD0\u884C `pdlab login` / `pdlab register`\uFF0C\u6216\u8BBE\u7F6E PDLAB_API_URL \u73AF\u5883\u53D8\u91CF\u3002");
61
+ process.exit(1);
62
+ }
63
+ return apiUrl.replace(/\/+$/, "");
64
+ }
65
+ function requireToken() {
66
+ const token = readConfig().token;
67
+ if (!token) {
68
+ console.error("\u5C1A\u672A\u767B\u5F55\u3002\u8BF7\u5148\u8FD0\u884C `pdlab login`\u3002");
69
+ process.exit(1);
70
+ }
71
+ return token;
72
+ }
73
+
74
+ // src/api-client.ts
75
+ var ApiRequestError = class extends Error {
76
+ constructor(message, status, details) {
77
+ super(message);
78
+ this.status = status;
79
+ this.details = details;
80
+ }
81
+ status;
82
+ details;
83
+ };
84
+ async function apiRequest(path, options = {}) {
85
+ const apiUrl = resolveApiUrl(options.apiUrl);
86
+ const url = new URL(path.replace(/^\/+/, ""), apiUrl + "/");
87
+ if (options.query) {
88
+ for (const [key, value] of Object.entries(options.query)) {
89
+ if (value !== void 0) url.searchParams.set(key, value);
90
+ }
91
+ }
92
+ const headers = {};
93
+ if (options.token) headers.Authorization = `Bearer ${options.token}`;
94
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
95
+ let res;
96
+ try {
97
+ res = await fetch(url, {
98
+ method: options.method ?? "GET",
99
+ headers,
100
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
101
+ });
102
+ } catch (cause) {
103
+ const reason = cause instanceof Error ? cause.message : String(cause);
104
+ throw new ApiRequestError(`\u65E0\u6CD5\u8FDE\u63A5\u5230 ${apiUrl}\uFF08${reason}\uFF09\u3002\u8BF7\u68C0\u67E5\u5730\u5740\u662F\u5426\u6B63\u786E\u3001\u670D\u52A1\u662F\u5426\u53EF\u8BBF\u95EE\u3002`, 0);
105
+ }
106
+ const text = await res.text();
107
+ let data = {};
108
+ if (text) {
109
+ try {
110
+ data = JSON.parse(text);
111
+ } catch {
112
+ const snippet = text.trim().slice(0, 120).replace(/\s+/g, " ");
113
+ throw new ApiRequestError(
114
+ `\u670D\u52A1\u7AEF\u8FD4\u56DE\u4E86\u975E JSON \u54CD\u5E94\uFF08HTTP ${res.status}\uFF09\uFF1A${snippet}${text.length > 120 ? "\u2026" : ""}`,
115
+ res.status
116
+ );
117
+ }
118
+ }
119
+ if (!res.ok) {
120
+ const body = data;
121
+ throw new ApiRequestError(body.error ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${res.status}\uFF09`, res.status, body.details);
122
+ }
123
+ return data;
124
+ }
125
+
126
+ // src/poll.ts
127
+ var POLL_INTERVAL_MS = 2e3;
128
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["consumed", "registered", "denied", "expired"]);
129
+ async function pollCliAuth(code, apiUrl) {
130
+ for (; ; ) {
131
+ const result = await apiRequest("/api/cli/auth/poll", {
132
+ query: { code },
133
+ apiUrl
134
+ });
135
+ if (result.status === "approved" || TERMINAL_STATUSES.has(result.status)) return result;
136
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
137
+ }
138
+ }
139
+
140
+ // src/commands/login.ts
141
+ function registerLoginCommand(program2) {
142
+ program2.command("login").description("\u5728\u6D4F\u89C8\u5668\u91CC\u5B8C\u6210\u4E00\u6B21\u6027\u6388\u6743\uFF0C\u767B\u5F55 pdlab CLI\uFF08token \u957F\u671F\u6709\u6548\uFF0C\u76F4\u5230\u88AB\u64A4\u9500\uFF09").option("--api-url <url>", "API \u670D\u52A1\u5668\u5730\u5740\uFF0C\u672A\u914D\u7F6E\u8FC7\u65F6\u5FC5\u987B\u63D0\u4F9B\u4E00\u6B21").action(async (opts) => {
143
+ const apiUrl = resolveApiUrl(opts.apiUrl);
144
+ const start = await apiRequest("/api/cli/auth/start", {
145
+ method: "POST",
146
+ body: { mode: "login" },
147
+ apiUrl
148
+ });
149
+ console.log(`\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u6388\u6743\uFF1A${start.verifyUrl}`);
150
+ try {
151
+ await (0, import_open.default)(start.verifyUrl);
152
+ } catch {
153
+ console.log("\uFF08\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u4E0A\u9762\u7684\u94FE\u63A5\uFF09");
154
+ }
155
+ console.log("\u7B49\u5F85\u6388\u6743\u4E2D\u2026");
156
+ const result = await pollCliAuth(start.code, apiUrl);
157
+ if (result.status === "approved" && result.token) {
158
+ updateConfig({ token: result.token, apiUrl });
159
+ console.log("\u767B\u5F55\u6210\u529F\u3002");
160
+ return;
161
+ }
162
+ if (result.status === "expired") {
163
+ console.error("\u6388\u6743\u8BF7\u6C42\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C pdlab login\u3002");
164
+ } else {
165
+ console.error(`\u767B\u5F55\u672A\u5B8C\u6210\uFF08\u72B6\u6001\uFF1A${result.status}\uFF09\u3002`);
166
+ }
167
+ process.exit(1);
168
+ });
169
+ }
170
+
171
+ // src/commands/register.ts
172
+ var import_open2 = __toESM(require("open"));
173
+ function registerRegisterCommand(program2) {
174
+ program2.command("register").description("\u5728\u6D4F\u89C8\u5668\u91CC\u5B8C\u6210\u6CE8\u518C\u7533\u8BF7\uFF08\u6CE8\u518C\u540E\u9700\u7BA1\u7406\u5458\u5BA1\u6838\uFF0CCLI \u4E0D\u4F1A\u81EA\u52A8\u767B\u5F55\uFF09").option("--api-url <url>", "API \u670D\u52A1\u5668\u5730\u5740\uFF0C\u672A\u914D\u7F6E\u8FC7\u65F6\u5FC5\u987B\u63D0\u4F9B\u4E00\u6B21").action(async (opts) => {
175
+ const apiUrl = resolveApiUrl(opts.apiUrl);
176
+ const start = await apiRequest("/api/cli/auth/start", {
177
+ method: "POST",
178
+ body: { mode: "register" },
179
+ apiUrl
180
+ });
181
+ console.log(`\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u6CE8\u518C\uFF1A${start.verifyUrl}`);
182
+ try {
183
+ await (0, import_open2.default)(start.verifyUrl);
184
+ } catch {
185
+ console.log("\uFF08\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u4E0A\u9762\u7684\u94FE\u63A5\uFF09");
186
+ }
187
+ console.log("\u7B49\u5F85\u6CE8\u518C\u5B8C\u6210\u2026");
188
+ const result = await pollCliAuth(start.code, apiUrl);
189
+ if (result.status === "registered") {
190
+ console.log("\u6CE8\u518C\u7533\u8BF7\u5DF2\u63D0\u4EA4\uFF0C\u8BF7\u7B49\u5F85\u7BA1\u7406\u5458\u5BA1\u6838\u3002\u5BA1\u6838\u901A\u8FC7\u540E\u8FD0\u884C `pdlab login` \u767B\u5F55\u3002");
191
+ return;
192
+ }
193
+ if (result.status === "expired") {
194
+ console.error("\u6CE8\u518C\u8BF7\u6C42\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C pdlab register\u3002");
195
+ } else {
196
+ console.error(`\u6CE8\u518C\u672A\u5B8C\u6210\uFF08\u72B6\u6001\uFF1A${result.status}\uFF09\u3002`);
197
+ }
198
+ process.exit(1);
199
+ });
200
+ }
201
+
202
+ // src/commands/logout.ts
203
+ function registerLogoutCommand(program2) {
204
+ program2.command("logout").description("\u6E05\u9664\u672C\u5730\u4FDD\u5B58\u7684\u767B\u5F55\u72B6\u6001\uFF08\u4E0D\u64A4\u9500\u670D\u52A1\u7AEF token\uFF0C\u64A4\u9500\u8BF7\u5230\u7F51\u9875\u300C\u4E2A\u4EBA\u4E2D\u5FC3 \u2192 API Token\u300D\uFF09").action(() => {
205
+ updateConfig({ token: void 0 });
206
+ console.log("\u5DF2\u9000\u51FA\u767B\u5F55\u3002\n\u6CE8\u610F\uFF1A\u670D\u52A1\u7AEF\u7684 token \u4ECD\u7136\u6709\u6548\uFF0C\u5982\u9700\u5F7B\u5E95\u4F5C\u5E9F\u8BF7\u5230\u7F51\u9875\u300C\u4E2A\u4EBA\u4E2D\u5FC3 \u2192 API Token\u300D\u64A4\u9500\u3002");
207
+ });
208
+ }
209
+
210
+ // src/commands/whoami.ts
211
+ function registerWhoamiCommand(program2) {
212
+ program2.command("whoami").description("\u67E5\u770B\u5F53\u524D\u767B\u5F55\u7684\u8D26\u53F7").action(async () => {
213
+ const token = requireToken();
214
+ const { user } = await apiRequest("/api/auth/me", { token });
215
+ if (!user) {
216
+ console.error("token \u65E0\u6548\u6216\u5DF2\u88AB\u64A4\u9500\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C pdlab login\u3002");
217
+ process.exit(1);
218
+ }
219
+ console.log(`${user.username} <${user.email}>\uFF08${user.status}\uFF0C${user.roleLabels.join("\u3001") || "\u65E0\u89D2\u8272"}\uFF09`);
220
+ });
221
+ }
222
+
223
+ // src/commands/config-cmd.ts
224
+ function registerConfigCommand(program2) {
225
+ const config = program2.command("config").description("\u67E5\u770B/\u4FEE\u6539\u672C\u5730\u914D\u7F6E\uFF08~/.pdlab/config.json\uFF09");
226
+ config.command("get").description("\u6253\u5370\u5F53\u524D\u914D\u7F6E").action(() => {
227
+ const { token, ...rest } = readConfig();
228
+ console.log(JSON.stringify({ ...rest, token: token ? "***" : void 0 }, null, 2));
229
+ });
230
+ config.command("set <key> <value>").description("\u8BBE\u7F6E\u4E00\u9879\u914D\u7F6E\uFF0C\u76EE\u524D\u652F\u6301\uFF1Aapi-url").action((key, value) => {
231
+ if (key !== "api-url") {
232
+ console.error(`\u4E0D\u652F\u6301\u7684\u914D\u7F6E\u9879\uFF1A${key}\uFF08\u76EE\u524D\u53EA\u652F\u6301 api-url\uFF09`);
233
+ process.exit(1);
234
+ }
235
+ updateConfig({ apiUrl: value });
236
+ console.log(`\u5DF2\u5C06 api-url \u8BBE\u7F6E\u4E3A ${value}`);
237
+ });
238
+ }
239
+
240
+ // ../shared-schemas/src/safe-url.ts
241
+ var import_zod = require("zod");
242
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
243
+ function isSafeHttpUrl(raw) {
244
+ let url;
245
+ try {
246
+ url = new URL(raw);
247
+ } catch {
248
+ return false;
249
+ }
250
+ return ALLOWED_PROTOCOLS.has(url.protocol);
251
+ }
252
+ var safeHttpUrl = import_zod.z.string().trim().max(2048, "\u94FE\u63A5\u8FC7\u957F").refine(isSafeHttpUrl, "\u53EA\u652F\u6301 http/https \u5F00\u5934\u7684\u94FE\u63A5");
253
+
254
+ // ../shared-schemas/src/projects.ts
255
+ var import_zod2 = require("zod");
256
+ var PROJECT_STATUS_VALUES = ["planning", "active", "review", "completed"];
257
+ var projectCreateSchema = import_zod2.z.object({
258
+ title: import_zod2.z.string().trim().min(1, "\u9879\u76EE\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A").max(200).describe("\u9879\u76EE\u6807\u9898"),
259
+ description: import_zod2.z.string().max(2e4).optional().describe("\u9879\u76EE\u63CF\u8FF0"),
260
+ coverImage: safeHttpUrl.optional().describe("\u5C01\u9762\u56FE http/https \u94FE\u63A5"),
261
+ commercialType: import_zod2.z.string().max(100).optional().describe("\u5546\u4E1A\u5C5E\u6027"),
262
+ status: import_zod2.z.enum(PROJECT_STATUS_VALUES).optional().describe("\u9879\u76EE\u72B6\u6001\uFF1Aplanning/active/review/completed"),
263
+ progress: import_zod2.z.number().int().min(0).max(100).optional().describe("\u9879\u76EE\u8FDB\u5EA6\u767E\u5206\u6BD4 0-100"),
264
+ taxonomyIds: import_zod2.z.array(import_zod2.z.string().min(1)).max(50).optional().default([]).describe("\u5206\u7C7B\u6807\u7B7E id \u5217\u8868")
265
+ });
266
+ var projectUpdateSchema = import_zod2.z.object({
267
+ title: import_zod2.z.string().trim().min(1, "\u9879\u76EE\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A").max(200).optional().describe("\u9879\u76EE\u6807\u9898"),
268
+ description: import_zod2.z.string().max(2e4).nullable().optional().describe("\u9879\u76EE\u63CF\u8FF0"),
269
+ coverImage: safeHttpUrl.nullable().optional().describe("\u5C01\u9762\u56FE http/https \u94FE\u63A5"),
270
+ commercialType: import_zod2.z.string().max(100).nullable().optional().describe("\u5546\u4E1A\u5C5E\u6027"),
271
+ progress: import_zod2.z.number().int().min(0).max(100).optional().describe("\u9879\u76EE\u8FDB\u5EA6\u767E\u5206\u6BD4 0-100"),
272
+ status: import_zod2.z.enum(PROJECT_STATUS_VALUES).optional().describe("\u9879\u76EE\u72B6\u6001\uFF1Aplanning/active/review/completed"),
273
+ taxonomyIds: import_zod2.z.array(import_zod2.z.string().min(1)).max(50).optional().describe("\u5206\u7C7B\u6807\u7B7E id \u5217\u8868\uFF08\u5168\u91CF\u66FF\u6362\uFF09")
274
+ }).refine((v) => Object.keys(v).length > 0, "\u6CA1\u6709\u9700\u8981\u66F4\u65B0\u7684\u5B57\u6BB5");
275
+
276
+ // ../shared-schemas/src/posts.ts
277
+ var import_zod3 = require("zod");
278
+ var postCreateSchema = import_zod3.z.object({
279
+ title: import_zod3.z.string().min(1).describe("\u5185\u5BB9\u6807\u9898"),
280
+ body: import_zod3.z.string().min(1).describe("\u5185\u5BB9\u6B63\u6587"),
281
+ projectId: import_zod3.z.string().describe("\u6240\u5C5E\u9879\u76EE id"),
282
+ mediaLinks: import_zod3.z.array(safeHttpUrl).max(20, "\u5A92\u4F53\u94FE\u63A5\u6700\u591A 20 \u6761").optional().default([]).describe("\u5A92\u4F53\u94FE\u63A5\u5217\u8868")
283
+ });
284
+ var postUpdateSchema = import_zod3.z.object({
285
+ title: import_zod3.z.string().trim().min(1, "\u6807\u9898\u4E0D\u80FD\u4E3A\u7A7A").max(200).describe("\u5185\u5BB9\u6807\u9898"),
286
+ body: import_zod3.z.string().trim().min(1, "\u6B63\u6587\u4E0D\u80FD\u4E3A\u7A7A").max(5e4).describe("\u5185\u5BB9\u6B63\u6587"),
287
+ mediaLinks: import_zod3.z.array(safeHttpUrl).max(20).optional().default([]).describe("\u5A92\u4F53\u94FE\u63A5\u5217\u8868")
288
+ });
289
+ var postReviewSchema = import_zod3.z.object({
290
+ action: import_zod3.z.enum(["approve", "reject"]).describe("\u5BA1\u6838\u7ED3\u679C\uFF1Aapprove/reject"),
291
+ reviewComment: import_zod3.z.string().trim().max(500, "\u5BA1\u6838\u610F\u89C1\u6700\u591A 500 \u5B57").optional().describe("\u5BA1\u6838\u610F\u89C1\uFF08\u9A73\u56DE\u65F6\u5FC5\u586B\uFF09")
292
+ });
293
+
294
+ // src/errors.ts
295
+ var import_zod4 = require("zod");
296
+ function printValidationError(error, examples) {
297
+ console.error("\u53C2\u6570\u9519\u8BEF\uFF1A");
298
+ for (const issue of error.issues) {
299
+ const path = issue.path.length > 0 ? issue.path.join(".") : "(\u6839)";
300
+ console.error(` - ${path}: ${issue.message}`);
301
+ }
302
+ printExamples(examples);
303
+ }
304
+ function printExamples(examples) {
305
+ if (examples.length === 0) return;
306
+ console.error("\n\u793A\u4F8B\uFF1A");
307
+ for (const example of examples) {
308
+ console.error(` $ ${example.command}`);
309
+ if (example.note) console.error(` ${example.note}`);
310
+ }
311
+ }
312
+ function printApiError(error) {
313
+ console.error(`\u9519\u8BEF\uFF1A${error.message}`);
314
+ if (error.details && typeof error.details === "object") {
315
+ const flat = error.details;
316
+ for (const [field, messages] of Object.entries(flat.fieldErrors ?? {})) {
317
+ for (const message of messages) console.error(` - ${field}: ${message}`);
318
+ }
319
+ for (const message of flat.formErrors ?? []) console.error(` - ${message}`);
320
+ }
321
+ }
322
+ function handleCommandError(error, examples) {
323
+ if (error instanceof import_zod4.ZodError) {
324
+ printValidationError(error, examples);
325
+ } else if (error instanceof ApiRequestError) {
326
+ printApiError(error);
327
+ } else {
328
+ console.error(error);
329
+ }
330
+ process.exit(1);
331
+ }
332
+
333
+ // src/commands/project.ts
334
+ var CREATE_EXAMPLES = [
335
+ {
336
+ command: 'pdlab project create --title "\u94A0\u7535\u56DE\u6536\u5DE5\u827A\u4F18\u5316" --description "..." --status planning --taxonomy-ids tax_1,tax_2',
337
+ note: "title \u5FC5\u586B\uFF0C\u5176\u4F59\u5B57\u6BB5\u53EF\u7701\u7565"
338
+ }
339
+ ];
340
+ var UPDATE_EXAMPLES = [
341
+ { command: "pdlab project update proj_123 --progress 60 --status active", note: "\u53EA\u4F20\u8981\u6539\u7684\u5B57\u6BB5" }
342
+ ];
343
+ function toBody(opts) {
344
+ const body = {};
345
+ if (opts.title !== void 0) body.title = opts.title;
346
+ if (opts.description !== void 0) body.description = opts.description;
347
+ if (opts.coverImage !== void 0) body.coverImage = opts.coverImage;
348
+ if (opts.commercialType !== void 0) body.commercialType = opts.commercialType;
349
+ if (opts.status !== void 0) body.status = opts.status;
350
+ if (opts.progress !== void 0) body.progress = Number(opts.progress);
351
+ if (opts.taxonomyIds !== void 0) {
352
+ body.taxonomyIds = opts.taxonomyIds.split(",").map((id) => id.trim()).filter(Boolean);
353
+ }
354
+ return body;
355
+ }
356
+ function withProjectFlags(cmd) {
357
+ return cmd.option("--title <title>", "\u9879\u76EE\u6807\u9898").option("--description <description>", "\u9879\u76EE\u63CF\u8FF0").option("--cover-image <url>", "\u5C01\u9762\u56FE http/https \u94FE\u63A5").option("--commercial-type <type>", "\u5546\u4E1A\u5C5E\u6027").option("--status <status>", "planning | active | review | completed").option("--progress <percent>", "\u8FDB\u5EA6 0-100").option("--taxonomy-ids <ids>", "\u9017\u53F7\u5206\u9694\u7684\u5206\u7C7B\u6807\u7B7E id \u5217\u8868");
358
+ }
359
+ function registerProjectCommand(program2) {
360
+ const project = program2.command("project").description("\u9879\u76EE\u7684\u589E\u5220\u6539\u67E5");
361
+ project.command("list").description("\u5217\u51FA\u9879\u76EE").option("--status <status>", "\u6309\u72B6\u6001\u7B5B\u9009\uFF1Aplanning | active | review | completed").action(async (opts) => {
362
+ const data = await apiRequest("/api/projects", { query: { status: opts.status } });
363
+ console.log(JSON.stringify(data, null, 2));
364
+ });
365
+ project.command("get <id>").description("\u67E5\u770B\u9879\u76EE\u8BE6\u60C5").action(async (id) => {
366
+ const data = await apiRequest(`/api/projects/${id}`);
367
+ console.log(JSON.stringify(data, null, 2));
368
+ });
369
+ withProjectFlags(project.command("create").description("\u521B\u5EFA\u9879\u76EE\uFF08\u9700\u8981\u9879\u76EE\u521B\u5EFA\u6743\u9650\uFF09")).action(
370
+ async (opts) => {
371
+ const token = requireToken();
372
+ try {
373
+ const body = projectCreateSchema.parse(toBody(opts));
374
+ const data = await apiRequest("/api/projects", { method: "POST", body, token });
375
+ console.log(JSON.stringify(data, null, 2));
376
+ } catch (error) {
377
+ handleCommandError(error, CREATE_EXAMPLES);
378
+ }
379
+ }
380
+ );
381
+ withProjectFlags(project.command("update <id>").description("\u66F4\u65B0\u9879\u76EE")).action(
382
+ async (id, opts) => {
383
+ const token = requireToken();
384
+ try {
385
+ const body = projectUpdateSchema.parse(toBody(opts));
386
+ const data = await apiRequest(`/api/projects/${id}`, { method: "PATCH", body, token });
387
+ console.log(JSON.stringify(data, null, 2));
388
+ } catch (error) {
389
+ handleCommandError(error, UPDATE_EXAMPLES);
390
+ }
391
+ }
392
+ );
393
+ project.command("delete <id>").description("\u5220\u9664\u9879\u76EE\uFF08\u9700\u8981\u662F\u9879\u76EE\u53D1\u8D77\u4EBA\uFF0C\u4E14\u9879\u76EE\u4E0B\u6CA1\u6709\u5185\u5BB9/\u79EF\u5206\u8BB0\u5F55\uFF09").action(async (id) => {
394
+ const token = requireToken();
395
+ const data = await apiRequest(`/api/projects/${id}`, { method: "DELETE", token });
396
+ console.log(JSON.stringify(data, null, 2));
397
+ });
398
+ }
399
+
400
+ // src/commands/post.ts
401
+ var CREATE_EXAMPLES2 = [
402
+ {
403
+ command: 'pdlab post create --title "\u7B2C\u4E00\u5468\u8FDB\u5C55" --body "..." --project-id proj_123 --media-links https://example.com/a.png',
404
+ note: "title / body / project-id \u5FC5\u586B"
405
+ }
406
+ ];
407
+ var UPDATE_EXAMPLES2 = [{ command: 'pdlab post update post_123 --title "..." --body "..."', note: "title \u548C body \u90FD\u5FC5\u987B\u4F20" }];
408
+ function toMediaLinks(value) {
409
+ if (value === void 0) return void 0;
410
+ return value.split(",").map((url) => url.trim()).filter(Boolean);
411
+ }
412
+ function registerPostCommand(program2) {
413
+ const post = program2.command("post").description("\u5185\u5BB9\uFF08\u5E16\u5B50\uFF09\u7684\u589E\u5220\u6539\u67E5");
414
+ post.command("list").description("\u5217\u51FA\u5185\u5BB9").option("--project-id <id>", "\u6309\u9879\u76EE\u7B5B\u9009").option("--status <status>", "pending | approved | rejected\uFF08\u67E5\u770B\u975E approved \u9700\u8981\u5BA1\u6838\u6743\u9650\uFF09").action(async (opts) => {
415
+ const data = await apiRequest("/api/posts", { query: { projectId: opts.projectId, status: opts.status } });
416
+ console.log(JSON.stringify(data, null, 2));
417
+ });
418
+ post.command("get <id>").description("\u67E5\u770B\u5185\u5BB9\u8BE6\u60C5").action(async (id) => {
419
+ const data = await apiRequest(`/api/posts/${id}`);
420
+ console.log(JSON.stringify(data, null, 2));
421
+ });
422
+ post.command("create").description("\u53D1\u5E03\u5185\u5BB9\uFF08\u9700\u8981\u5185\u5BB9\u521B\u5EFA\u6743\u9650\uFF0C\u4E14\u662F\u9879\u76EE\u53D1\u8D77\u4EBA\u6216\u5DF2\u62A5\u540D\u5171\u5EFA\uFF09").option("--title <title>", "\u5185\u5BB9\u6807\u9898").option("--body <body>", "\u5185\u5BB9\u6B63\u6587").option("--project-id <id>", "\u6240\u5C5E\u9879\u76EE id").option("--media-links <urls>", "\u9017\u53F7\u5206\u9694\u7684\u5A92\u4F53\u94FE\u63A5").action(async (opts) => {
423
+ const token = requireToken();
424
+ try {
425
+ const body = postCreateSchema.parse({
426
+ title: opts.title,
427
+ body: opts.body,
428
+ projectId: opts.projectId,
429
+ mediaLinks: toMediaLinks(opts.mediaLinks)
430
+ });
431
+ const data = await apiRequest("/api/posts", { method: "POST", body, token });
432
+ console.log(JSON.stringify(data, null, 2));
433
+ } catch (error) {
434
+ handleCommandError(error, CREATE_EXAMPLES2);
435
+ }
436
+ });
437
+ post.command("update <id>").description("\u7F16\u8F91\u5185\u5BB9\uFF08\u4F5C\u8005\u672C\u4EBA\uFF0C\u6216\u6709\u7F16\u8F91\u4EFB\u610F\u5185\u5BB9\u7684\u6743\u9650\uFF09").option("--title <title>", "\u5185\u5BB9\u6807\u9898").option("--body <body>", "\u5185\u5BB9\u6B63\u6587").option("--media-links <urls>", "\u9017\u53F7\u5206\u9694\u7684\u5A92\u4F53\u94FE\u63A5").action(async (id, opts) => {
438
+ const token = requireToken();
439
+ try {
440
+ const body = postUpdateSchema.parse({
441
+ title: opts.title,
442
+ body: opts.body,
443
+ mediaLinks: toMediaLinks(opts.mediaLinks)
444
+ });
445
+ const data = await apiRequest(`/api/posts/${id}`, { method: "PATCH", body, token });
446
+ console.log(JSON.stringify(data, null, 2));
447
+ } catch (error) {
448
+ handleCommandError(error, UPDATE_EXAMPLES2);
449
+ }
450
+ });
451
+ post.command("delete <id>").description("\u5220\u9664\u5185\u5BB9\uFF08\u4F5C\u8005\u672C\u4EBA\uFF0C\u6216\u6709\u5220\u9664\u4EFB\u610F\u5185\u5BB9\u7684\u6743\u9650\uFF09").action(async (id) => {
452
+ const token = requireToken();
453
+ const data = await apiRequest(`/api/posts/${id}`, { method: "DELETE", token });
454
+ console.log(JSON.stringify(data, null, 2));
455
+ });
456
+ }
457
+
458
+ // src/index.ts
459
+ var pkg = JSON.parse((0, import_fs2.readFileSync)((0, import_path2.join)(__dirname, "../package.json"), "utf8"));
460
+ var program = new import_commander.Command();
461
+ program.name("pdlab").description("1836 \u5F00\u6E90\u5171\u5EFA\u5E73\u53F0\u547D\u4EE4\u884C\u5DE5\u5177\u2014\u2014\u662F\u73B0\u6709 REST API \u7684\u8584\u5C01\u88C5").version(pkg.version);
462
+ registerLoginCommand(program);
463
+ registerRegisterCommand(program);
464
+ registerLogoutCommand(program);
465
+ registerWhoamiCommand(program);
466
+ registerConfigCommand(program);
467
+ registerProjectCommand(program);
468
+ registerPostCommand(program);
469
+ program.parseAsync(process.argv).catch((error) => {
470
+ if (error instanceof ApiRequestError) {
471
+ console.error(`\u9519\u8BEF\uFF1A${error.message}`);
472
+ } else {
473
+ console.error(error);
474
+ }
475
+ process.exit(1);
476
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "pdlab-cli",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "1836 开源共建平台(problem-driven-lab)的命令行工具:项目、内容的增删改查,浏览器授权登录",
6
+ "keywords": [
7
+ "cli",
8
+ "1836",
9
+ "problem-driven-lab",
10
+ "pdlab"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://github.com/shadowcz007/problem-driven-lab/tree/main/packages/cli#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/shadowcz007/problem-driven-lab.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/shadowcz007/problem-driven-lab/issues"
21
+ },
22
+ "bin": {
23
+ "pdlab": "dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsx src/index.ts"
39
+ },
40
+ "dependencies": {
41
+ "commander": "^14.0.0",
42
+ "open": "^10.1.0",
43
+ "zod": "^4.5.4"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.13.3",
47
+ "tsup": "^8.3.5",
48
+ "tsx": "^4.23.13",
49
+ "typescript": "^5",
50
+ "@pdlab/shared-schemas": "0.1.0"
51
+ }
52
+ }