@wcs-edu-os/cli 0.0.0-stage → 0.1.1
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/LICENSE +661 -0
- package/README.md +73 -2
- package/dist/index.js +1446 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -6
package/dist/index.js
ADDED
|
@@ -0,0 +1,1446 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { stdin, stdout, stderr } from "process";
|
|
5
|
+
|
|
6
|
+
// src/program.ts
|
|
7
|
+
import { Command, CommanderError, Option } from "commander";
|
|
8
|
+
|
|
9
|
+
// ../core/src/registry.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var intOption = (min, max) => z.coerce.number().int().min(min).max(max);
|
|
12
|
+
var idSchema = z.string().min(1).describe("\u8D44\u6E90 ID\uFF0C\u59CB\u7EC8\u6309\u5B57\u7B26\u4E32\u5904\u7406");
|
|
13
|
+
var emptySchema = z.object({}).strict();
|
|
14
|
+
var anyOutput = z.unknown();
|
|
15
|
+
var requiredText = z.string().trim().min(1);
|
|
16
|
+
var nonNegativeDecimal = z.string().regex(/^\d+(?:\.\d+)?$/, "\u5FC5\u987B\u662F\u975E\u8D1F\u5341\u8FDB\u5236\u5B57\u7B26\u4E32");
|
|
17
|
+
var date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "\u65E5\u671F\u683C\u5F0F\u5FC5\u987B\u4E3A yyyy-MM-dd");
|
|
18
|
+
var dateTime = z.string().regex(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, "\u65F6\u95F4\u683C\u5F0F\u5FC5\u987B\u4E3A yyyy-MM-dd HH:mm:ss");
|
|
19
|
+
var binary = z.number().int().min(0).max(1);
|
|
20
|
+
var writeInputSchema = z.object({ input: requiredText }).strict();
|
|
21
|
+
var writeOptions = [{ flags: "--input <path>", description: "JSON \u8F93\u5165\u6587\u4EF6\uFF0C- \u8868\u793A stdin" }];
|
|
22
|
+
var pageOptions = [
|
|
23
|
+
{ flags: "--page <number>", description: "\u8D77\u59CB\u9875\u7801", defaultValue: 1 },
|
|
24
|
+
{ flags: "--size <number>", description: "\u6BCF\u9875\u6570\u91CF", defaultValue: 20 },
|
|
25
|
+
{ flags: "--max-items <number>", description: "\u8DE8\u9875\u6700\u591A\u62C9\u53D6\u6761\u6570", defaultValue: 100 },
|
|
26
|
+
{ flags: "--filter <key=value...>", description: "\u900F\u4F20\u540E\u7AEF\u7B5B\u9009\u5B57\u6BB5\uFF0C\u53EF\u91CD\u590D", collect: true },
|
|
27
|
+
{ flags: "--name <name>", description: "\u540D\u79F0\u7B5B\u9009" },
|
|
28
|
+
{ flags: "--keyword <keyword>", description: "\u5173\u952E\u5B57\u7B5B\u9009" },
|
|
29
|
+
{ flags: "--mobile <mobile>", description: "\u624B\u673A\u53F7\u7B5B\u9009" },
|
|
30
|
+
{ flags: "--status-id <statusId>", description: "\u72B6\u6001\u7B5B\u9009" },
|
|
31
|
+
{ flags: "--user-id <userId>", description: "\u7528\u6237 ID" },
|
|
32
|
+
{ flags: "--course-id <courseId>", description: "\u8BFE\u7A0B ID" },
|
|
33
|
+
{ flags: "--task-id <taskId>", description: "\u4EFB\u52A1 ID" }
|
|
34
|
+
];
|
|
35
|
+
var pageSchema = z.object({
|
|
36
|
+
page: intOption(1, 1e6).default(1),
|
|
37
|
+
size: intOption(1, 500).default(20),
|
|
38
|
+
maxItems: intOption(1, 1e5).default(100),
|
|
39
|
+
filter: z.array(z.string()).default([]),
|
|
40
|
+
name: z.string().optional(),
|
|
41
|
+
keyword: z.string().optional(),
|
|
42
|
+
mobile: z.string().optional(),
|
|
43
|
+
statusId: z.string().optional(),
|
|
44
|
+
userId: idSchema.optional(),
|
|
45
|
+
courseId: idSchema.optional(),
|
|
46
|
+
taskId: idSchema.optional()
|
|
47
|
+
});
|
|
48
|
+
var idInput = z.object({ id: idSchema });
|
|
49
|
+
var courseInput = z.object({ courseId: idSchema });
|
|
50
|
+
var defaultTable = [
|
|
51
|
+
{ key: "id", label: "ID" },
|
|
52
|
+
{ key: "name", label: "\u540D\u79F0" },
|
|
53
|
+
{ key: "statusId", label: "\u72B6\u6001" }
|
|
54
|
+
];
|
|
55
|
+
function endpoint(method, path, controller, requestDto, responseDto, pagination = false) {
|
|
56
|
+
return { method, path, controller, requestDto, responseDto, pagination };
|
|
57
|
+
}
|
|
58
|
+
function spec(value2) {
|
|
59
|
+
return {
|
|
60
|
+
...value2,
|
|
61
|
+
risk: "R0",
|
|
62
|
+
readOnly: true,
|
|
63
|
+
supportsDryRun: false,
|
|
64
|
+
supportsIdempotency: false,
|
|
65
|
+
sideEffects: [],
|
|
66
|
+
rollback: null,
|
|
67
|
+
implemented: true,
|
|
68
|
+
outputSchema: anyOutput
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function writeSpec(value2) {
|
|
72
|
+
return {
|
|
73
|
+
...value2,
|
|
74
|
+
risk: "R1",
|
|
75
|
+
readOnly: false,
|
|
76
|
+
supportsDryRun: true,
|
|
77
|
+
supportsIdempotency: true,
|
|
78
|
+
inputSchema: writeInputSchema,
|
|
79
|
+
options: writeOptions,
|
|
80
|
+
implemented: true,
|
|
81
|
+
outputSchema: anyOutput
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
var userCreatePayload = z.object({
|
|
85
|
+
mobile: requiredText.max(11),
|
|
86
|
+
mobilePwd: requiredText,
|
|
87
|
+
userSex: z.number().int().min(1).max(3),
|
|
88
|
+
userHead: requiredText,
|
|
89
|
+
remark: z.string().max(200).optional(),
|
|
90
|
+
userAge: date.optional(),
|
|
91
|
+
school: z.string().optional(),
|
|
92
|
+
realName: z.string().optional(),
|
|
93
|
+
nickname: z.string().optional(),
|
|
94
|
+
tagIds: z.array(idSchema).optional()
|
|
95
|
+
}).strict();
|
|
96
|
+
var userUpdatePayload = z.object({
|
|
97
|
+
id: idSchema,
|
|
98
|
+
userSex: z.number().int().min(1).max(3).optional(),
|
|
99
|
+
nickname: z.string().optional(),
|
|
100
|
+
realName: z.string().max(20).optional(),
|
|
101
|
+
idCard: z.string().max(18).optional(),
|
|
102
|
+
faceImg: z.string().optional(),
|
|
103
|
+
faceError: z.number().int().min(0).optional(),
|
|
104
|
+
faceStatus: z.boolean().optional(),
|
|
105
|
+
userAge: date.nullable(),
|
|
106
|
+
school: z.string().optional(),
|
|
107
|
+
userHead: z.string().optional(),
|
|
108
|
+
remark: z.string().max(200).optional(),
|
|
109
|
+
tagIds: z.array(idSchema)
|
|
110
|
+
}).strict();
|
|
111
|
+
function coursePayload(includeId) {
|
|
112
|
+
return z.object({
|
|
113
|
+
...includeId ? { id: idSchema } : {},
|
|
114
|
+
lecturerId: idSchema,
|
|
115
|
+
categoryId: idSchema,
|
|
116
|
+
courseName: requiredText.max(120),
|
|
117
|
+
courseLogo: requiredText,
|
|
118
|
+
introduce: z.string().optional(),
|
|
119
|
+
countVirtualStudy: z.number().int().min(0).optional(),
|
|
120
|
+
countVirtualBuy: z.number().int().min(0).optional(),
|
|
121
|
+
linePrice: nonNegativeDecimal,
|
|
122
|
+
price: nonNegativeDecimal,
|
|
123
|
+
publishStatus: binary,
|
|
124
|
+
speedDouble: binary,
|
|
125
|
+
speedDrag: binary,
|
|
126
|
+
checkIn: binary,
|
|
127
|
+
studyMode: z.number().int().min(1).max(2),
|
|
128
|
+
validityDateType: z.number().int().min(1).max(3),
|
|
129
|
+
days: z.number().int().min(1).optional(),
|
|
130
|
+
beginTime: dateTime.optional(),
|
|
131
|
+
endTime: dateTime.optional(),
|
|
132
|
+
courseSell: z.boolean().optional(),
|
|
133
|
+
certSet: z.boolean().optional(),
|
|
134
|
+
certStudyProgress: nonNegativeDecimal.optional(),
|
|
135
|
+
certId: idSchema.optional()
|
|
136
|
+
}).strict().superRefine(validateValidity);
|
|
137
|
+
}
|
|
138
|
+
function validateValidity(payload, context) {
|
|
139
|
+
if (payload.validityDateType === 2 && payload.days === void 0) {
|
|
140
|
+
context.addIssue({ code: "custom", path: ["days"], message: "\u6709\u6548\u671F\u7C7B\u578B\u4E3A 2 \u65F6\u5FC5\u987B\u63D0\u4F9B days" });
|
|
141
|
+
}
|
|
142
|
+
if (payload.validityDateType === 3) {
|
|
143
|
+
if (!payload.beginTime) context.addIssue({ code: "custom", path: ["beginTime"], message: "\u6709\u6548\u671F\u7C7B\u578B\u4E3A 3 \u65F6\u5FC5\u987B\u63D0\u4F9B beginTime" });
|
|
144
|
+
if (!payload.endTime) context.addIssue({ code: "custom", path: ["endTime"], message: "\u6709\u6548\u671F\u7C7B\u578B\u4E3A 3 \u65F6\u5FC5\u987B\u63D0\u4F9B endTime" });
|
|
145
|
+
if (typeof payload.beginTime === "string" && typeof payload.endTime === "string" && payload.beginTime > payload.endTime) {
|
|
146
|
+
context.addIssue({ code: "custom", path: ["endTime"], message: "endTime \u4E0D\u80FD\u65E9\u4E8E beginTime" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var noArgs = [];
|
|
151
|
+
function adminPage(name, segments, description, path, controller, requestDto, responseDto, method = "GET") {
|
|
152
|
+
return spec({
|
|
153
|
+
name,
|
|
154
|
+
segments,
|
|
155
|
+
description,
|
|
156
|
+
permissionHint: null,
|
|
157
|
+
kind: "api",
|
|
158
|
+
arguments: noArgs,
|
|
159
|
+
options: pageOptions,
|
|
160
|
+
inputSchema: pageSchema,
|
|
161
|
+
endpoints: [endpoint(method, path, controller, requestDto, responseDto, true)],
|
|
162
|
+
table: defaultTable
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function adminGet(name, segments, description, path, controller, requestDto, responseDto) {
|
|
166
|
+
return spec({
|
|
167
|
+
name,
|
|
168
|
+
segments,
|
|
169
|
+
description,
|
|
170
|
+
permissionHint: null,
|
|
171
|
+
kind: "api",
|
|
172
|
+
arguments: [{ name: "id", description: "\u8D44\u6E90 ID", required: true }],
|
|
173
|
+
options: [],
|
|
174
|
+
inputSchema: idInput,
|
|
175
|
+
endpoints: [endpoint("GET", path, controller, requestDto, responseDto)],
|
|
176
|
+
table: []
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
var commandRegistry = [
|
|
180
|
+
spec({ name: "version", segments: ["version"], description: "\u663E\u793A CLI \u7248\u672C", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: [] }),
|
|
181
|
+
spec({ name: "guide", segments: ["guide"], description: "\u67E5\u770B\u5B89\u88C5\u3001\u8BA4\u8BC1\u548C\u4E1A\u52A1\u64CD\u4F5C\u5F15\u5BFC", permissionHint: null, kind: "local", arguments: [{ name: "topic", description: "\u4E3B\u9898\uFF1Agetting-started|course|exam|write|all", required: false }], options: [], inputSchema: z.object({ topic: z.enum(["getting-started", "course", "exam", "write", "all"]).default("all") }), endpoints: [], table: [{ key: "title", label: "\u4E3B\u9898" }, { key: "summary", label: "\u8BF4\u660E" }] }),
|
|
182
|
+
spec({ name: "capabilities", segments: ["capabilities"], description: "\u5217\u51FA\u547D\u4EE4\u53CA\u5F53\u524D\u8C03\u7528\u80FD\u529B", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: defaultTable }),
|
|
183
|
+
spec({ name: "schema", segments: ["schema"], description: "\u8F93\u51FA\u6307\u5B9A\u547D\u4EE4\u7684 Zod JSON Schema", permissionHint: null, kind: "local", arguments: [{ name: "command", description: "\u547D\u4EE4\u540D\uFF08\u70B9\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF09", required: true }], options: [], inputSchema: z.object({ command: z.string().min(1) }), endpoints: [], table: [] }),
|
|
184
|
+
spec({ name: "doctor", segments: ["doctor"], description: "\u68C0\u67E5\u8FD0\u884C\u73AF\u5883\u3001\u914D\u7F6E\u3001\u51ED\u636E\u548C\u540E\u7AEF\u8FDE\u901A\u6027", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: [{ key: "check", label: "\u68C0\u67E5\u9879" }, { key: "status", label: "\u72B6\u6001" }, { key: "message", label: "\u8BF4\u660E" }] }),
|
|
185
|
+
spec({ name: "auth.status", segments: ["auth", "status"], description: "\u68C0\u67E5\u672C\u5730\u8BA4\u8BC1\u72B6\u6001\uFF0C\u4E0D\u8BBF\u95EE\u7F51\u7EDC", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: [] }),
|
|
186
|
+
spec({ name: "auth.whoami", segments: ["auth", "whoami"], description: "\u83B7\u53D6\u5F53\u524D\u7ED1\u5B9A\u7BA1\u7406\u5458\u4FE1\u606F", permissionHint: null, kind: "api", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [endpoint("GET", "/base/admin/login/user", "AdminLoginController", "-", "AdminLoginUserResp")], table: [] }),
|
|
187
|
+
spec({ name: "auth.test", segments: ["auth", "test"], description: "\u7B7E\u53D1\u5E76\u9A8C\u8BC1 CLI token", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [endpoint("POST", "/base/api/cli/token", "ApiCliTokenController", "OAuth client_credentials", "ApiCliTokenResp")], table: [] }),
|
|
188
|
+
spec({ name: "auth.credential-info", segments: ["auth", "credential-info"], description: "\u663E\u793A\u8131\u654F\u51ED\u636E\u4E0E token \u4FE1\u606F", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: [] }),
|
|
189
|
+
spec({ name: "profile.list", segments: ["profile", "list"], description: "\u5217\u51FA Profile", permissionHint: null, kind: "local", arguments: noArgs, options: [], inputSchema: emptySchema, endpoints: [], table: [{ key: "name", label: "\u540D\u79F0" }, { key: "baseUrl", label: "Base URL" }, { key: "current", label: "\u5F53\u524D" }] }),
|
|
190
|
+
spec({ name: "profile.get", segments: ["profile", "get"], description: "\u67E5\u770B Profile", permissionHint: null, kind: "local", arguments: [{ name: "name", description: "Profile \u540D\u79F0", required: false }], options: [], inputSchema: z.object({ name: z.string().optional() }), endpoints: [], table: [] }),
|
|
191
|
+
spec({ name: "profile.use", segments: ["profile", "use"], description: "\u5207\u6362 Profile\uFF0C\u53EF\u540C\u65F6\u521B\u5EFA\u6216\u66F4\u65B0 base URL", permissionHint: null, kind: "local", arguments: [{ name: "name", description: "Profile \u540D\u79F0", required: true }], options: [{ flags: "--base-url <url>", description: "\u521B\u5EFA\u6216\u66F4\u65B0 Profile \u7684 base URL" }], inputSchema: z.object({ name: z.string().min(1), baseUrl: z.string().url().optional() }), endpoints: [], table: [] }),
|
|
192
|
+
spec({ name: "user.list", segments: ["user", "list"], description: "\u5206\u9875\u5217\u51FA\u7528\u6237", permissionHint: "\u5019\u9009: user:view", kind: "api", arguments: noArgs, options: pageOptions, inputSchema: pageSchema, endpoints: [endpoint("POST", "/base/admin/users/page", "AdminUsersController", "AdminUsersPageReq", "Page<AdminUsersPageResp>", true)], table: [{ key: "id", label: "ID" }, { key: "realName", label: "\u59D3\u540D" }, { key: "mobile", label: "\u624B\u673A\u53F7" }, { key: "statusId", label: "\u72B6\u6001" }] }),
|
|
193
|
+
spec({ name: "user.get", segments: ["user", "get"], description: "\u67E5\u770B\u7528\u6237", permissionHint: "\u5019\u9009: user:view", kind: "api", arguments: [{ name: "id", description: "\u7528\u6237 ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/base/admin/users/view", "AdminUsersController", "id", "AdminUsersViewResp")], table: [] }),
|
|
194
|
+
spec({ name: "user.situation", segments: ["user", "situation"], description: "\u67E5\u770B\u7528\u6237\u5185\u5BB9\u6982\u51B5", permissionHint: "\u5019\u9009: user:record", kind: "api", arguments: [{ name: "id", description: "\u7528\u6237 ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/base/admin/users/situation", "AdminUsersController", "id", "AdminUsersSituationResp")], table: [] }),
|
|
195
|
+
spec({ name: "user.account", segments: ["user", "account"], description: "\u67E5\u770B\u7528\u6237\u8D26\u6237", permissionHint: "\u5019\u9009: user:record", kind: "api", arguments: [{ name: "id", description: "\u7528\u6237 ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/base/admin/users/account", "AdminUsersController", "id", "AdminUsersViewAccountResp")], table: [] }),
|
|
196
|
+
writeSpec({ name: "user.create", segments: ["user", "create"], description: "\u521B\u5EFA\u7528\u6237", permissionHint: "user:add", kind: "api", arguments: noArgs, payloadSchema: userCreatePayload, endpoints: [endpoint("POST", "/base/admin/users/save", "AdminUsersController", "AdminUsersSaveReq", "String")], table: [], sideEffects: ["\u521B\u5EFA\u7528\u6237\u53CA\u5173\u8054\u6807\u7B7E", "\u4FDD\u5B58\u52A0\u5BC6\u540E\u7684\u767B\u5F55\u5BC6\u7801"], rollback: "\u53EF\u901A\u8FC7\u7BA1\u7406\u7AEF\u5220\u9664\u672A\u4EA7\u751F\u4E1A\u52A1\u8BB0\u5F55\u7684\u7528\u6237" }),
|
|
197
|
+
writeSpec({ name: "user.update", segments: ["user", "update"], description: "\u5B8C\u6574\u66F4\u65B0\u7528\u6237\u8D44\u6599", permissionHint: "user:edit", kind: "api", arguments: noArgs, payloadSchema: userUpdatePayload, endpoints: [endpoint("PUT", "/base/admin/users/edit", "AdminUsersController", "AdminUsersEditReq", "String")], table: [], sideEffects: ["\u66F4\u65B0\u7528\u6237\u8D44\u6599", "\u66FF\u6362\u5168\u90E8\u7528\u6237\u6807\u7B7E", "\u6309 userAge \u663E\u5F0F\u503C\u66F4\u65B0\u6216\u6E05\u7A7A\u51FA\u751F\u65E5\u671F"], rollback: "\u9700\u4F7F\u7528\u53D8\u66F4\u524D\u6570\u636E\u518D\u6B21\u6267\u884C update" }),
|
|
198
|
+
spec({ name: "course.list", segments: ["course", "list"], description: "\u5206\u9875\u5217\u51FA\u8BFE\u7A0B", permissionHint: "\u5019\u9009: course:view", kind: "api", arguments: noArgs, options: pageOptions, inputSchema: pageSchema, endpoints: [endpoint("POST", "/resource/admin/course/page", "AdminCourseController", "AdminCoursePageReq", "Page<AdminCoursePageResp>", true)], table: defaultTable }),
|
|
199
|
+
spec({ name: "course.get", segments: ["course", "get"], description: "\u67E5\u770B\u8BFE\u7A0B", permissionHint: "\u5019\u9009: course:view", kind: "api", arguments: [{ name: "id", description: "\u8BFE\u7A0B ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/resource/admin/course/view", "AdminCourseController", "id", "AdminCourseViewResp")], table: [] }),
|
|
200
|
+
writeSpec({ name: "course.create", segments: ["course", "create"], description: "\u521B\u5EFA\u8BFE\u7A0B", permissionHint: "course:save", kind: "api", arguments: noArgs, payloadSchema: coursePayload(false), endpoints: [endpoint("POST", "/resource/admin/course/save", "AdminCourseController", "AdminCourseSaveReq", "String")], table: [], sideEffects: ["\u521B\u5EFA\u8BFE\u7A0B\u8BB0\u5F55"], rollback: "\u53EF\u901A\u8FC7\u7BA1\u7406\u7AEF\u5220\u9664\u672A\u4EA7\u751F\u5173\u8054\u6570\u636E\u7684\u8BFE\u7A0B" }),
|
|
201
|
+
writeSpec({ name: "course.update", segments: ["course", "update"], description: "\u5B8C\u6574\u66F4\u65B0\u8BFE\u7A0B", permissionHint: "course:edit", kind: "api", arguments: noArgs, payloadSchema: coursePayload(true), endpoints: [endpoint("PUT", "/resource/admin/course/edit", "AdminCourseController", "AdminCourseEditReq", "String")], table: [], sideEffects: ["\u66F4\u65B0\u8BFE\u7A0B\u914D\u7F6E\u3001\u4EF7\u683C\u4E0E\u6709\u6548\u671F"], rollback: "\u9700\u4F7F\u7528\u53D8\u66F4\u524D\u6570\u636E\u518D\u6B21\u6267\u884C update" }),
|
|
202
|
+
spec({ name: "course.chapter.list", segments: ["course", "chapter", "list"], description: "\u5217\u51FA\u8BFE\u7A0B\u7AE0\u8282", permissionHint: "\u5019\u9009: course:chapter:period", kind: "api", arguments: [{ name: "courseId", description: "\u8BFE\u7A0B ID", required: true }], options: [], inputSchema: courseInput, endpoints: [endpoint("GET", "/resource/admin/course/chapter/list", "AdminCourseChapterController", "courseId", "List<AdminCourseChapterListResp>")], table: defaultTable }),
|
|
203
|
+
spec({ name: "course.chapter.get", segments: ["course", "chapter", "get"], description: "\u67E5\u770B\u8BFE\u7A0B\u7AE0\u8282", permissionHint: "\u5019\u9009: course:chapter:period", kind: "api", arguments: [{ name: "id", description: "\u7AE0\u8282 ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/resource/admin/course/chapter/view", "AdminCourseChapterController", "id", "AdminCourseChapterViewResp")], table: [] }),
|
|
204
|
+
spec({ name: "course-study.list", segments: ["course-study", "list"], description: "\u5206\u9875\u5217\u51FA\u8BFE\u7A0B\u5B66\u4E60\u8BB0\u5F55", permissionHint: "\u5019\u9009: user:course", kind: "api", arguments: noArgs, options: pageOptions, inputSchema: pageSchema, endpoints: [endpoint("GET", "/resource/admin/course/user/study/page", "AdminCourseUserStudyController", "AdminCourseUserStudyPageReq", "Page<AdminCourseUserStudyPageResp>", true)], table: [{ key: "id", label: "ID" }, { key: "userId", label: "\u7528\u6237 ID" }, { key: "courseId", label: "\u8BFE\u7A0B ID" }, { key: "studyProgress", label: "\u8FDB\u5EA6" }] }),
|
|
205
|
+
spec({ name: "course-study.get", segments: ["course-study", "get"], description: "\u67E5\u770B\u8BFE\u7A0B\u5B66\u4E60\u8BB0\u5F55", permissionHint: "\u5019\u9009: user:course", kind: "api", arguments: [{ name: "id", description: "\u5B66\u4E60\u8BB0\u5F55 ID", required: true }], options: [], inputSchema: idInput, endpoints: [endpoint("GET", "/resource/admin/course/user/study/view", "AdminCourseUserStudyController", "id", "AdminCourseUserStudyViewResp")], table: [] }),
|
|
206
|
+
spec({ name: "statistics.dashboard", segments: ["statistics", "dashboard"], description: "\u5E76\u884C\u83B7\u53D6\u7BA1\u7406\u7AEF\u4EEA\u8868\u76D8\u7EDF\u8BA1", permissionHint: "\u5019\u9009: dashboard:view", kind: "dashboard", arguments: noArgs, options: [{ flags: "--dates <days>", description: "\u8D8B\u52BF\u7A97\u53E3\uFF0C\u6CBF\u7528 Admin dates \u53C2\u6570", defaultValue: -14 }], inputSchema: z.object({ dates: z.coerce.number().int().min(-3650).max(3650).default(-14) }), endpoints: [
|
|
207
|
+
endpoint("GET", "/base/admin/dashboard/stat/data", "AdminDashboardController", "-", "AdminDashboardDataResp"),
|
|
208
|
+
endpoint("GET", "/base/admin/dashboard/stat/login", "AdminDashboardController", "dates", "AdminDashboardLoginResp"),
|
|
209
|
+
endpoint("GET", "/base/admin/dashboard/stat/order", "AdminDashboardController", "dates", "AdminDashboardOrderResp")
|
|
210
|
+
], table: [] }),
|
|
211
|
+
// Admin menu read-only coverage. Write operations remain gated until their DTO and risk contract are added.
|
|
212
|
+
adminPage("category.list", ["category", "list"], "\u5206\u9875\u5217\u51FA\u8BFE\u7A0B\u5206\u7C7B", "/resource/admin/category/page", "AdminCategoryController", "AdminCategoryPageReq", "Page<AdminCategoryPageResp>"),
|
|
213
|
+
adminGet("category.get", ["category", "get"], "\u67E5\u770B\u8BFE\u7A0B\u5206\u7C7B", "/resource/admin/category/view", "AdminCategoryController", "id", "AdminCategoryViewResp"),
|
|
214
|
+
adminPage("lecturer.list", ["lecturer", "list"], "\u5206\u9875\u5217\u51FA\u8BB2\u5E08", "/base/admin/lecturer/page", "AdminLecturerController", "AdminLecturerPageReq", "Page<AdminLecturerPageResp>"),
|
|
215
|
+
adminGet("lecturer.get", ["lecturer", "get"], "\u67E5\u770B\u8BB2\u5E08", "/base/admin/lecturer/view", "AdminLecturerController", "id", "AdminLecturerViewResp"),
|
|
216
|
+
adminPage("question.list", ["question", "list"], "\u5206\u9875\u5217\u51FA\u8BD5\u9898", "/resource/admin/question/page", "AdminQuestionController", "AdminQuestionPageReq", "Page<AdminQuestionPageResp>", "POST"),
|
|
217
|
+
adminGet("question.get", ["question", "get"], "\u67E5\u770B\u8BD5\u9898", "/resource/admin/question/view", "AdminQuestionController", "id", "AdminQuestionViewResp"),
|
|
218
|
+
adminPage("paper.list", ["paper", "list"], "\u5206\u9875\u5217\u51FA\u8BD5\u5377", "/resource/admin/paper/page", "AdminPaperController", "AdminPaperPageReq", "Page<AdminPaperPageResp>"),
|
|
219
|
+
adminGet("paper.get", ["paper", "get"], "\u67E5\u770B\u8BD5\u5377", "/resource/admin/paper/view", "AdminPaperController", "id", "AdminPaperViewResp"),
|
|
220
|
+
adminPage("exam.list", ["exam", "list"], "\u5206\u9875\u5217\u51FA\u8003\u8BD5", "/resource/admin/exam/page", "AdminExamController", "AdminExamPageReq", "Page<AdminExamPageResp>", "POST"),
|
|
221
|
+
adminGet("exam.get", ["exam", "get"], "\u67E5\u770B\u8003\u8BD5", "/resource/admin/exam/view", "AdminExamController", "id", "AdminExamViewResp"),
|
|
222
|
+
adminPage("material.list", ["material", "list"], "\u5206\u9875\u5217\u51FA\u7D20\u6750", "/resource/admin/material/page", "AdminMaterialController", "AdminMaterialPageReq", "Page<AdminMaterialPageResp>", "POST"),
|
|
223
|
+
adminGet("material.get", ["material", "get"], "\u67E5\u770B\u7D20\u6750", "/resource/admin/material/view", "AdminMaterialController", "id", "AdminMaterialViewResp")
|
|
224
|
+
];
|
|
225
|
+
var notImplementedCapabilities = [
|
|
226
|
+
{ name: "statistics.course", reason: "\u7F3A\u5C11\u7A33\u5B9A\u7EDF\u4E00\u540E\u7AEF\u5951\u7EA6", risk: "R0", readOnly: true },
|
|
227
|
+
{ name: "statistics.task", reason: "\u7F3A\u5C11\u7A33\u5B9A\u7EDF\u4E00\u540E\u7AEF\u5951\u7EA6", risk: "R0", readOnly: true },
|
|
228
|
+
{ name: "statistics.user", reason: "\u7F3A\u5C11\u7A33\u5B9A\u7EDF\u4E00\u540E\u7AEF\u5951\u7EA6", risk: "R0", readOnly: true },
|
|
229
|
+
{ name: "job.status", reason: "\u7F3A\u5C11\u7EDF\u4E00\u4EFB\u52A1\u72B6\u6001\u6A21\u578B", risk: "R0", readOnly: true },
|
|
230
|
+
{ name: "job.wait", reason: "\u7F3A\u5C11\u7EDF\u4E00\u4EFB\u52A1\u72B6\u6001\u6A21\u578B", risk: "R0", readOnly: true },
|
|
231
|
+
{ name: "training-task.assign", reason: "R2 \u6279\u91CF\u6307\u6D3E\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R2", readOnly: false },
|
|
232
|
+
{ name: "user.enable", reason: "R2 \u72B6\u6001\u64CD\u4F5C\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R2", readOnly: false },
|
|
233
|
+
{ name: "user.disable", reason: "R2 \u72B6\u6001\u64CD\u4F5C\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R2", readOnly: false },
|
|
234
|
+
{ name: "course-study.import", reason: "R2 \u5BFC\u5165\u4EFB\u52A1\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R2", readOnly: false },
|
|
235
|
+
{ name: "course.publish", reason: "R2 \u53D1\u5E03\u64CD\u4F5C\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R2", readOnly: false },
|
|
236
|
+
{ name: "resource.delete", reason: "R3 \u5220\u9664\u64CD\u4F5C\u4E0D\u5728\u672C\u9636\u6BB5\u8303\u56F4", risk: "R3", readOnly: false }
|
|
237
|
+
];
|
|
238
|
+
function findCommand(name) {
|
|
239
|
+
const normalized = name.trim().replace(/\s+/g, ".");
|
|
240
|
+
return commandRegistry.find((item) => item.name === normalized);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ../core/src/capabilities.ts
|
|
244
|
+
function getCapabilities(apis) {
|
|
245
|
+
return [
|
|
246
|
+
...commandRegistry.map((spec2) => {
|
|
247
|
+
const endpointPaths = spec2.endpoints.map((item) => item.path);
|
|
248
|
+
const callable = endpointPaths.length === 0 || !apis ? endpointPaths.length === 0 ? true : null : endpointPaths.every((path) => apis.some((api) => matchesApi(api, path)));
|
|
249
|
+
return {
|
|
250
|
+
name: spec2.name,
|
|
251
|
+
description: spec2.description,
|
|
252
|
+
risk: spec2.risk,
|
|
253
|
+
readOnly: spec2.readOnly,
|
|
254
|
+
implemented: true,
|
|
255
|
+
authenticated: Boolean(apis),
|
|
256
|
+
callable,
|
|
257
|
+
endpoints: endpointPaths,
|
|
258
|
+
permissionHint: spec2.permissionHint
|
|
259
|
+
};
|
|
260
|
+
}),
|
|
261
|
+
...notImplementedCapabilities.map((item) => ({
|
|
262
|
+
...item,
|
|
263
|
+
implemented: false,
|
|
264
|
+
callable: false,
|
|
265
|
+
endpoints: []
|
|
266
|
+
}))
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
function matchesApi(pattern, path) {
|
|
270
|
+
if (pattern === path) return true;
|
|
271
|
+
try {
|
|
272
|
+
return new RegExp(`^(?:${pattern})$`).test(path);
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function buildTree(items) {
|
|
278
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
279
|
+
const roots = [];
|
|
280
|
+
for (const value2 of items) {
|
|
281
|
+
if (!isRecord(value2) || value2.id === void 0) continue;
|
|
282
|
+
nodes.set(String(value2.id), { ...value2, children: [] });
|
|
283
|
+
}
|
|
284
|
+
for (const node of nodes.values()) {
|
|
285
|
+
const parentId = node.parentId === void 0 || node.parentId === null ? "0" : String(node.parentId);
|
|
286
|
+
const parent = nodes.get(parentId);
|
|
287
|
+
if (parent && String(node.id) !== parentId) parent.children?.push(node);
|
|
288
|
+
else roots.push(node);
|
|
289
|
+
}
|
|
290
|
+
return roots;
|
|
291
|
+
}
|
|
292
|
+
function isRecord(value2) {
|
|
293
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ../core/src/client.ts
|
|
297
|
+
import { z as z2 } from "zod";
|
|
298
|
+
|
|
299
|
+
// ../core/src/errors.ts
|
|
300
|
+
var ExitCode = {
|
|
301
|
+
SUCCESS: 0,
|
|
302
|
+
VALIDATION: 2,
|
|
303
|
+
AUTH: 3,
|
|
304
|
+
PERMISSION: 4,
|
|
305
|
+
NOT_FOUND: 5,
|
|
306
|
+
BUSINESS: 6,
|
|
307
|
+
RATE_LIMIT: 7,
|
|
308
|
+
SERVER: 8,
|
|
309
|
+
NETWORK: 9,
|
|
310
|
+
CONFIRMATION: 10,
|
|
311
|
+
PARTIAL: 11,
|
|
312
|
+
CANCELLED: 130
|
|
313
|
+
};
|
|
314
|
+
var CliError = class extends Error {
|
|
315
|
+
constructor(code, message, exitCode2, retryable = false, details = {}, suggestions = [], options) {
|
|
316
|
+
super(message, options);
|
|
317
|
+
this.code = code;
|
|
318
|
+
this.exitCode = exitCode2;
|
|
319
|
+
this.retryable = retryable;
|
|
320
|
+
this.details = details;
|
|
321
|
+
this.suggestions = suggestions;
|
|
322
|
+
this.name = "CliError";
|
|
323
|
+
}
|
|
324
|
+
code;
|
|
325
|
+
exitCode;
|
|
326
|
+
retryable;
|
|
327
|
+
details;
|
|
328
|
+
suggestions;
|
|
329
|
+
toJSON() {
|
|
330
|
+
return {
|
|
331
|
+
code: this.code,
|
|
332
|
+
message: this.message,
|
|
333
|
+
retryable: this.retryable,
|
|
334
|
+
details: this.details,
|
|
335
|
+
suggestions: this.suggestions
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function mapHttpError(status, data) {
|
|
340
|
+
const body = isRecord2(data) ? data : {};
|
|
341
|
+
const oauthError = typeof body.error === "string" ? body.error : void 0;
|
|
342
|
+
if (status === 401 && oauthError === "invalid_client") {
|
|
343
|
+
return new CliError("AUTH_INVALID_CLIENT", "client authentication failed", ExitCode.AUTH, false);
|
|
344
|
+
}
|
|
345
|
+
if (status === 404) {
|
|
346
|
+
return new CliError("RESOURCE_NOT_FOUND", "\u8BF7\u6C42\u7684\u8D44\u6E90\u6216\u63A5\u53E3\u4E0D\u5B58\u5728", ExitCode.NOT_FOUND, false, { httpStatus: status });
|
|
347
|
+
}
|
|
348
|
+
if (status === 429) {
|
|
349
|
+
return new CliError("RATE_LIMITED", "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41", ExitCode.RATE_LIMIT, true, { httpStatus: status });
|
|
350
|
+
}
|
|
351
|
+
if (status >= 500) {
|
|
352
|
+
return new CliError("BACKEND_SERVICE_ERROR", "\u540E\u7AEF\u670D\u52A1\u5F02\u5E38", ExitCode.SERVER, true, { httpStatus: status });
|
|
353
|
+
}
|
|
354
|
+
return new CliError("HTTP_ERROR", `HTTP \u8BF7\u6C42\u5931\u8D25 (${status})`, ExitCode.BUSINESS, false, { httpStatus: status });
|
|
355
|
+
}
|
|
356
|
+
function mapBackendError(code, message) {
|
|
357
|
+
const details = { backendCode: code };
|
|
358
|
+
if ([301, 302, 303, 304].includes(code)) {
|
|
359
|
+
return new CliError("AUTH_REQUIRED", message, ExitCode.AUTH, false, details);
|
|
360
|
+
}
|
|
361
|
+
if ([305, 306].includes(code)) {
|
|
362
|
+
return new CliError("ADMIN_PERMISSION_DENIED", message, ExitCode.PERMISSION, false, details);
|
|
363
|
+
}
|
|
364
|
+
if (code === 999) {
|
|
365
|
+
return new CliError("BACKEND_BUSINESS_ERROR", message, ExitCode.BUSINESS, false, details);
|
|
366
|
+
}
|
|
367
|
+
return new CliError("BACKEND_BUSINESS_ERROR", message, ExitCode.BUSINESS, false, details);
|
|
368
|
+
}
|
|
369
|
+
function toCliError(error) {
|
|
370
|
+
if (error instanceof CliError) return error;
|
|
371
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
372
|
+
return new CliError("NETWORK_TIMEOUT", "\u8BF7\u6C42\u8D85\u65F6", ExitCode.NETWORK, true, {}, [], { cause: error });
|
|
373
|
+
}
|
|
374
|
+
const message = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
|
|
375
|
+
return new CliError("NETWORK_ERROR", message, ExitCode.NETWORK, true, {}, [], {
|
|
376
|
+
cause: error instanceof Error ? error : void 0
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function isRecord2(value2) {
|
|
380
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ../core/src/http.ts
|
|
384
|
+
import { request } from "undici";
|
|
385
|
+
|
|
386
|
+
// ../core/src/redact.ts
|
|
387
|
+
var SENSITIVE_KEYS = /^(token|access[_-]?token|authorization|password|passwd|secret|appsecret|app_secret|privatekey|private_key|mobile|idcard|id_card|email)$/i;
|
|
388
|
+
function redact(value2) {
|
|
389
|
+
return redactValue(value2, /* @__PURE__ */ new WeakSet());
|
|
390
|
+
}
|
|
391
|
+
function redactValue(value2, seen) {
|
|
392
|
+
if (Array.isArray(value2)) return value2.map((item) => redactValue(item, seen));
|
|
393
|
+
if (typeof value2 !== "object" || value2 === null) return value2;
|
|
394
|
+
if (seen.has(value2)) return "[Circular]";
|
|
395
|
+
seen.add(value2);
|
|
396
|
+
const output = {};
|
|
397
|
+
for (const [key, child] of Object.entries(value2)) {
|
|
398
|
+
output[key] = SENSITIVE_KEYS.test(key) ? mask(child) : redactValue(child, seen);
|
|
399
|
+
}
|
|
400
|
+
return output;
|
|
401
|
+
}
|
|
402
|
+
function mask(value2) {
|
|
403
|
+
const text = String(value2 ?? "");
|
|
404
|
+
if (text.length <= 4) return "[REDACTED]";
|
|
405
|
+
return `${text.slice(0, 2)}***${text.slice(-2)}`;
|
|
406
|
+
}
|
|
407
|
+
function maskIdentifier(value2) {
|
|
408
|
+
if (!value2) return "";
|
|
409
|
+
if (value2.length <= 6) return `${value2.slice(0, 1)}***`;
|
|
410
|
+
return `${value2.slice(0, 3)}***${value2.slice(-3)}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ../core/src/http.ts
|
|
414
|
+
var USER_AGENT = "@wcs-edu-os/cli/0.1.1";
|
|
415
|
+
var HttpClient = class {
|
|
416
|
+
constructor(baseUrl) {
|
|
417
|
+
this.baseUrl = baseUrl;
|
|
418
|
+
}
|
|
419
|
+
baseUrl;
|
|
420
|
+
async send(options) {
|
|
421
|
+
const maxAttempts = options.readOnly ? 3 : 1;
|
|
422
|
+
let lastError;
|
|
423
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
424
|
+
try {
|
|
425
|
+
const response = await this.sendOnce(options);
|
|
426
|
+
if (isRetryableStatus(response.status) && attempt < maxAttempts) {
|
|
427
|
+
options.onDiagnostic?.(`\u8BF7\u6C42\u8FD4\u56DE ${response.status}\uFF0C\u51C6\u5907\u7B2C ${attempt} \u6B21\u91CD\u8BD5`, redact({ path: options.path }));
|
|
428
|
+
await delay(50 * attempt);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (response.status < 200 || response.status >= 300) throw mapHttpError(response.status, response.data);
|
|
432
|
+
return response;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof CliError) {
|
|
435
|
+
if (!error.retryable || attempt >= maxAttempts) throw error;
|
|
436
|
+
} else if (attempt >= maxAttempts || !options.readOnly) {
|
|
437
|
+
throw toCliError(error);
|
|
438
|
+
}
|
|
439
|
+
lastError = error;
|
|
440
|
+
options.onDiagnostic?.(`\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25\uFF0C\u51C6\u5907\u7B2C ${attempt} \u6B21\u91CD\u8BD5`, redact({ path: options.path, error }));
|
|
441
|
+
await delay(50 * attempt);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
throw toCliError(lastError);
|
|
445
|
+
}
|
|
446
|
+
async sendOnce(options) {
|
|
447
|
+
const url = new URL(options.path, `${this.baseUrl}/`);
|
|
448
|
+
for (const [key, value2] of Object.entries(options.query ?? {})) {
|
|
449
|
+
if (value2 === void 0 || value2 === null || value2 === "") continue;
|
|
450
|
+
if (Array.isArray(value2)) value2.forEach((item) => url.searchParams.append(key, String(item)));
|
|
451
|
+
else url.searchParams.set(key, String(value2));
|
|
452
|
+
}
|
|
453
|
+
const headers = {
|
|
454
|
+
accept: "application/json",
|
|
455
|
+
"user-agent": USER_AGENT,
|
|
456
|
+
"x-request-id": options.requestId
|
|
457
|
+
};
|
|
458
|
+
if (options.token) headers.token = options.token;
|
|
459
|
+
if (options.idempotencyKey) headers["x-idempotency-key"] = options.idempotencyKey;
|
|
460
|
+
if (options.basic) {
|
|
461
|
+
headers.authorization = `Basic ${Buffer.from(`${options.basic.appKey}:${options.basic.appSecret}`, "utf8").toString("base64")}`;
|
|
462
|
+
}
|
|
463
|
+
let body;
|
|
464
|
+
if (options.form) {
|
|
465
|
+
headers["content-type"] = "application/x-www-form-urlencoded";
|
|
466
|
+
body = new URLSearchParams(options.form).toString();
|
|
467
|
+
} else if (options.body !== void 0) {
|
|
468
|
+
headers["content-type"] = "application/json";
|
|
469
|
+
body = JSON.stringify(options.body);
|
|
470
|
+
}
|
|
471
|
+
let response;
|
|
472
|
+
try {
|
|
473
|
+
response = await request(url, {
|
|
474
|
+
method: options.method,
|
|
475
|
+
headers,
|
|
476
|
+
body,
|
|
477
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
throw toCliError(error);
|
|
481
|
+
}
|
|
482
|
+
const text = await response.body.text();
|
|
483
|
+
let data = null;
|
|
484
|
+
if (text) {
|
|
485
|
+
try {
|
|
486
|
+
data = JSON.parse(text);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw new CliError("BACKEND_INVALID_RESPONSE", "\u540E\u7AEF\u8FD4\u56DE\u4E86\u65E0\u6548 JSON", ExitCode.SERVER, false, {
|
|
489
|
+
httpStatus: response.statusCode
|
|
490
|
+
}, [], { cause: error instanceof Error ? error : void 0 });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
status: response.statusCode,
|
|
495
|
+
headers: response.headers,
|
|
496
|
+
data
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
function isRetryableStatus(status) {
|
|
501
|
+
return status === 429 || status >= 500;
|
|
502
|
+
}
|
|
503
|
+
function delay(ms) {
|
|
504
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ../core/src/client.ts
|
|
508
|
+
var tokenSchema = z2.object({
|
|
509
|
+
access_token: z2.string().min(1),
|
|
510
|
+
token_type: z2.string().min(1),
|
|
511
|
+
expires_in: z2.number().int().positive(),
|
|
512
|
+
user_id: z2.string().min(1),
|
|
513
|
+
apis: z2.array(z2.string())
|
|
514
|
+
});
|
|
515
|
+
var WcsClient = class {
|
|
516
|
+
constructor(baseUrl, appKey, tokenCache, timeoutMs, onDiagnostic) {
|
|
517
|
+
this.baseUrl = baseUrl;
|
|
518
|
+
this.appKey = appKey;
|
|
519
|
+
this.tokenCache = tokenCache;
|
|
520
|
+
this.timeoutMs = timeoutMs;
|
|
521
|
+
this.onDiagnostic = onDiagnostic;
|
|
522
|
+
this.http = new HttpClient(baseUrl);
|
|
523
|
+
}
|
|
524
|
+
baseUrl;
|
|
525
|
+
appKey;
|
|
526
|
+
tokenCache;
|
|
527
|
+
timeoutMs;
|
|
528
|
+
onDiagnostic;
|
|
529
|
+
http;
|
|
530
|
+
async token(requestId2, appSecret, force = false) {
|
|
531
|
+
if (!force) {
|
|
532
|
+
const cached = await this.tokenCache.get(this.baseUrl, this.appKey);
|
|
533
|
+
if (cached) return cached;
|
|
534
|
+
}
|
|
535
|
+
if (!appSecret) {
|
|
536
|
+
throw new CliError("APP_SECRET_MISSING", "\u672A\u63D0\u4F9B appSecret\uFF0C\u4E14\u6CA1\u6709\u53EF\u7528 token \u7F13\u5B58", ExitCode.AUTH, false, {}, [
|
|
537
|
+
"\u4F7F\u7528 --secret-stdin \u4ECE\u6807\u51C6\u8F93\u5165\u63D0\u4F9B",
|
|
538
|
+
"\u6216\u8BBE\u7F6E WCS_EDU_APP_SECRET"
|
|
539
|
+
]);
|
|
540
|
+
}
|
|
541
|
+
const response = await this.http.send({
|
|
542
|
+
method: "POST",
|
|
543
|
+
path: "/base/api/cli/token",
|
|
544
|
+
requestId: requestId2,
|
|
545
|
+
timeoutMs: this.timeoutMs,
|
|
546
|
+
readOnly: false,
|
|
547
|
+
basic: { appKey: this.appKey, appSecret },
|
|
548
|
+
form: { grant_type: "client_credentials" },
|
|
549
|
+
onDiagnostic: this.onDiagnostic
|
|
550
|
+
});
|
|
551
|
+
const parsed = tokenSchema.safeParse(response.data);
|
|
552
|
+
if (!parsed.success) {
|
|
553
|
+
throw new CliError("TOKEN_RESPONSE_INVALID", "token \u54CD\u5E94\u4E0D\u7B26\u5408\u534F\u8BAE", ExitCode.SERVER, false, {
|
|
554
|
+
issues: parsed.error.issues
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return this.tokenCache.set(this.baseUrl, this.appKey, parsed.data);
|
|
558
|
+
}
|
|
559
|
+
async result(spec2, endpointIndex, requestId2, token, input, idempotencyKey) {
|
|
560
|
+
const endpoint2 = spec2.endpoints[endpointIndex];
|
|
561
|
+
if (!endpoint2) throw new CliError("COMMAND_CONTRACT_ERROR", "\u547D\u4EE4\u7F3A\u5C11 endpoint", ExitCode.SERVER);
|
|
562
|
+
const payload = buildBackendPayload(input);
|
|
563
|
+
const response = await this.http.send({
|
|
564
|
+
method: endpoint2.method,
|
|
565
|
+
path: endpoint2.path,
|
|
566
|
+
requestId: requestId2,
|
|
567
|
+
timeoutMs: this.timeoutMs,
|
|
568
|
+
readOnly: spec2.readOnly,
|
|
569
|
+
token,
|
|
570
|
+
query: endpoint2.method === "GET" ? payload : void 0,
|
|
571
|
+
body: endpoint2.method === "GET" ? void 0 : payload,
|
|
572
|
+
idempotencyKey,
|
|
573
|
+
onDiagnostic: this.onDiagnostic
|
|
574
|
+
});
|
|
575
|
+
return normalizeBackendResult(response.data);
|
|
576
|
+
}
|
|
577
|
+
async pagedResult(spec2, requestId2, token, input) {
|
|
578
|
+
const startPage = Number(input.page ?? 1);
|
|
579
|
+
const size = Number(input.size ?? 20);
|
|
580
|
+
const maxItems = Number(input.maxItems ?? 100);
|
|
581
|
+
const items = [];
|
|
582
|
+
let current = startPage;
|
|
583
|
+
let finalPage = null;
|
|
584
|
+
while (items.length < maxItems) {
|
|
585
|
+
const result = await this.result(spec2, 0, requestId2, token, { ...input, page: current, size });
|
|
586
|
+
const pageItems = extractItems(result.data);
|
|
587
|
+
items.push(...pageItems.slice(0, maxItems - items.length));
|
|
588
|
+
finalPage = result.page;
|
|
589
|
+
if (!result.page?.hasMore || pageItems.length === 0 || items.length >= maxItems) break;
|
|
590
|
+
current += 1;
|
|
591
|
+
}
|
|
592
|
+
const total = finalPage?.total ?? items.length;
|
|
593
|
+
return {
|
|
594
|
+
data: { items },
|
|
595
|
+
page: { number: startPage, size, total, hasMore: startPage * size < total && items.length < total }
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function normalizeBackendResult(value2) {
|
|
600
|
+
if (!isRecord3(value2) || typeof value2.code !== "number") {
|
|
601
|
+
throw new CliError("BACKEND_INVALID_RESPONSE", "\u540E\u7AEF\u54CD\u5E94\u7F3A\u5C11 Result.code", ExitCode.SERVER);
|
|
602
|
+
}
|
|
603
|
+
if (value2.code !== 200) {
|
|
604
|
+
throw mapBackendError(value2.code, typeof value2.msg === "string" ? value2.msg : "\u540E\u7AEF\u4E1A\u52A1\u9519\u8BEF");
|
|
605
|
+
}
|
|
606
|
+
const data = value2.data;
|
|
607
|
+
if (isPage(data)) {
|
|
608
|
+
const page = {
|
|
609
|
+
number: numberOr(data.pageCurrent, 1),
|
|
610
|
+
size: numberOr(data.pageSize, 20),
|
|
611
|
+
total: numberOr(data.totalCount, 0),
|
|
612
|
+
hasMore: numberOr(data.pageCurrent, 1) < numberOr(data.totalPage, 0)
|
|
613
|
+
};
|
|
614
|
+
return { data: { items: Array.isArray(data.list) ? data.list : [] }, page };
|
|
615
|
+
}
|
|
616
|
+
return { data, page: null };
|
|
617
|
+
}
|
|
618
|
+
function buildBackendPayload(input) {
|
|
619
|
+
const output = {};
|
|
620
|
+
const filters = Array.isArray(input.filter) ? input.filter : [];
|
|
621
|
+
for (const filter of filters) {
|
|
622
|
+
const separator = String(filter).indexOf("=");
|
|
623
|
+
if (separator <= 0) {
|
|
624
|
+
throw new CliError("FILTER_INVALID", `\u7B5B\u9009\u6761\u4EF6\u683C\u5F0F\u9519\u8BEF: ${filter}`, ExitCode.VALIDATION);
|
|
625
|
+
}
|
|
626
|
+
output[String(filter).slice(0, separator)] = String(filter).slice(separator + 1);
|
|
627
|
+
}
|
|
628
|
+
for (const [key, value2] of Object.entries(input)) {
|
|
629
|
+
if (["filter", "maxItems"].includes(key) || value2 === void 0 || value2 === "") continue;
|
|
630
|
+
if (key === "page") output.pageCurrent = value2;
|
|
631
|
+
else if (key === "size") output.pageSize = value2;
|
|
632
|
+
else output[key] = value2;
|
|
633
|
+
}
|
|
634
|
+
return output;
|
|
635
|
+
}
|
|
636
|
+
function extractItems(data) {
|
|
637
|
+
if (isRecord3(data) && Array.isArray(data.items)) return data.items;
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
function isPage(value2) {
|
|
641
|
+
return isRecord3(value2) && ("list" in value2 || "totalCount" in value2) && "pageCurrent" in value2;
|
|
642
|
+
}
|
|
643
|
+
function isRecord3(value2) {
|
|
644
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
645
|
+
}
|
|
646
|
+
function numberOr(value2, fallback) {
|
|
647
|
+
const number = Number(value2);
|
|
648
|
+
return Number.isFinite(number) ? number : fallback;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ../core/src/confirmation-store.ts
|
|
652
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
653
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
654
|
+
import { join as join2 } from "path";
|
|
655
|
+
|
|
656
|
+
// ../core/src/token-cache.ts
|
|
657
|
+
import { createHash } from "crypto";
|
|
658
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
659
|
+
import { homedir } from "os";
|
|
660
|
+
import { join } from "path";
|
|
661
|
+
function getCacheDir(env = process.env) {
|
|
662
|
+
if (env.WCS_EDU_CACHE_DIR) return env.WCS_EDU_CACHE_DIR;
|
|
663
|
+
if (process.platform === "win32") return join(env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "wcs-edu-os-cli");
|
|
664
|
+
if (process.platform === "darwin") return join(homedir(), "Library", "Caches", "wcs-edu-os-cli");
|
|
665
|
+
return join(env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "wcs-edu-os-cli");
|
|
666
|
+
}
|
|
667
|
+
function credentialFingerprint(baseUrl, appKey) {
|
|
668
|
+
return createHash("sha256").update(`${baseUrl}\0${appKey}`).digest("hex").slice(0, 16);
|
|
669
|
+
}
|
|
670
|
+
var TokenCache = class {
|
|
671
|
+
constructor(env = process.env) {
|
|
672
|
+
this.env = env;
|
|
673
|
+
}
|
|
674
|
+
env;
|
|
675
|
+
path(baseUrl, appKey) {
|
|
676
|
+
return join(getCacheDir(this.env), `token-${credentialFingerprint(baseUrl, appKey)}.json`);
|
|
677
|
+
}
|
|
678
|
+
async get(baseUrl, appKey, now = Date.now()) {
|
|
679
|
+
const path = this.path(baseUrl, appKey);
|
|
680
|
+
try {
|
|
681
|
+
const token = JSON.parse(await readFile(path, "utf8"));
|
|
682
|
+
if (!token.access_token || token.expiresAt <= now + 5e3) {
|
|
683
|
+
await rm(path, { force: true });
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
return token;
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (error instanceof SyntaxError) await rm(path, { force: true });
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async set(baseUrl, appKey, token, now = Date.now()) {
|
|
693
|
+
const dir = getCacheDir(this.env);
|
|
694
|
+
const path = this.path(baseUrl, appKey);
|
|
695
|
+
const cached = { ...token, expiresAt: now + token.expires_in * 1e3 };
|
|
696
|
+
await mkdir(dir, { recursive: true });
|
|
697
|
+
await writeFile(path, JSON.stringify(cached), { encoding: "utf8", mode: 384 });
|
|
698
|
+
if (process.platform !== "win32") await chmod(path, 384);
|
|
699
|
+
return cached;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// ../core/src/confirmation-store.ts
|
|
704
|
+
var CONFIRMATION_TTL_MS = 10 * 60 * 1e3;
|
|
705
|
+
var ConfirmationStore = class {
|
|
706
|
+
constructor(env = process.env) {
|
|
707
|
+
this.env = env;
|
|
708
|
+
}
|
|
709
|
+
env;
|
|
710
|
+
async create(binding, now = Date.now()) {
|
|
711
|
+
const confirmationCode = randomBytes(32).toString("base64url");
|
|
712
|
+
const record = this.bind(binding, now + CONFIRMATION_TTL_MS);
|
|
713
|
+
const path = this.path(confirmationCode);
|
|
714
|
+
await mkdir2(this.directory(), { recursive: true });
|
|
715
|
+
await writeFile2(path, JSON.stringify(record), { encoding: "utf8", mode: 384 });
|
|
716
|
+
if (process.platform !== "win32") await chmod2(path, 384);
|
|
717
|
+
return { confirmationCode, record };
|
|
718
|
+
}
|
|
719
|
+
async verify(code, binding, now = Date.now()) {
|
|
720
|
+
const path = this.path(code);
|
|
721
|
+
let record;
|
|
722
|
+
try {
|
|
723
|
+
record = JSON.parse(await readFile2(path, "utf8"));
|
|
724
|
+
} catch (error) {
|
|
725
|
+
throw new CliError("CONFIRMATION_INVALID", "\u786E\u8BA4\u4EE4\u724C\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548", ExitCode.CONFIRMATION, false, {}, [], {
|
|
726
|
+
cause: error instanceof Error ? error : void 0
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (record.expiresAt <= now) {
|
|
730
|
+
await rm2(path, { force: true });
|
|
731
|
+
throw new CliError("CONFIRMATION_EXPIRED", "\u786E\u8BA4\u4EE4\u724C\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C dry-run", ExitCode.CONFIRMATION);
|
|
732
|
+
}
|
|
733
|
+
const expected = this.bind(binding, record.expiresAt);
|
|
734
|
+
const mismatches = ["command", "environmentFingerprint", "userId", "payloadHash", "endpoint", "risk"].filter((key) => record[key] !== expected[key]);
|
|
735
|
+
if (mismatches.length > 0) {
|
|
736
|
+
throw new CliError("CONFIRMATION_MISMATCH", "\u8BF7\u6C42\u53C2\u6570\u6216\u6267\u884C\u8EAB\u4EFD\u4E0E dry-run \u4E0D\u4E00\u81F4", ExitCode.CONFIRMATION, false, {
|
|
737
|
+
mismatches
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
return record;
|
|
741
|
+
}
|
|
742
|
+
async consume(code) {
|
|
743
|
+
await rm2(this.path(code), { force: true });
|
|
744
|
+
}
|
|
745
|
+
bind(binding, expiresAt) {
|
|
746
|
+
return {
|
|
747
|
+
command: binding.command,
|
|
748
|
+
environmentFingerprint: hash(binding.baseUrl),
|
|
749
|
+
userId: binding.userId,
|
|
750
|
+
payloadHash: hash(stableStringify(binding.payload)),
|
|
751
|
+
endpoint: binding.endpoint,
|
|
752
|
+
risk: binding.risk,
|
|
753
|
+
sideEffects: binding.sideEffects,
|
|
754
|
+
rollback: binding.rollback,
|
|
755
|
+
expiresAt
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
directory() {
|
|
759
|
+
return join2(getCacheDir(this.env), "confirmations");
|
|
760
|
+
}
|
|
761
|
+
path(code) {
|
|
762
|
+
return join2(this.directory(), `${hash(code)}.json`);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function stableStringify(value2) {
|
|
766
|
+
return JSON.stringify(sortValue(value2));
|
|
767
|
+
}
|
|
768
|
+
function sortValue(value2) {
|
|
769
|
+
if (Array.isArray(value2)) return value2.map(sortValue);
|
|
770
|
+
if (typeof value2 !== "object" || value2 === null) return value2;
|
|
771
|
+
return Object.fromEntries(Object.entries(value2).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, sortValue(child)]));
|
|
772
|
+
}
|
|
773
|
+
function hash(value2) {
|
|
774
|
+
return createHash2("sha256").update(value2).digest("hex");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ../core/src/credentials.ts
|
|
778
|
+
import { emitKeypressEvents } from "readline";
|
|
779
|
+
import { createInterface } from "readline/promises";
|
|
780
|
+
import { stdin as defaultStdin, stderr as defaultStderr } from "process";
|
|
781
|
+
async function resolveAppSecret(options = {}) {
|
|
782
|
+
const env = options.env ?? process.env;
|
|
783
|
+
const input = options.stdin ?? defaultStdin;
|
|
784
|
+
if (options.secretStdin) {
|
|
785
|
+
const value2 = (await readAll(input)).trimEnd();
|
|
786
|
+
if (!value2) throw new CliError("APP_SECRET_MISSING", "\u6807\u51C6\u8F93\u5165\u4E2D\u6CA1\u6709 appSecret", ExitCode.VALIDATION);
|
|
787
|
+
return { value: value2, source: "stdin" };
|
|
788
|
+
}
|
|
789
|
+
if (options.interactive && "isTTY" in input && input.isTTY) {
|
|
790
|
+
const output = options.stderr ?? defaultStderr;
|
|
791
|
+
const value2 = "setRawMode" in input && typeof input.setRawMode === "function" ? await readHidden(input, output) : await readVisibleFallback(input, output);
|
|
792
|
+
if (value2) return { value: value2, source: "prompt" };
|
|
793
|
+
}
|
|
794
|
+
if (env.WCS_EDU_APP_SECRET) return { value: env.WCS_EDU_APP_SECRET, source: "environment" };
|
|
795
|
+
return { source: "none" };
|
|
796
|
+
}
|
|
797
|
+
async function readHidden(input, output) {
|
|
798
|
+
output.write("App Secret: ");
|
|
799
|
+
emitKeypressEvents(input);
|
|
800
|
+
const wasRaw = input.isRaw ?? false;
|
|
801
|
+
input.setRawMode(true);
|
|
802
|
+
input.resume();
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
let value2 = "";
|
|
805
|
+
const cleanup = () => {
|
|
806
|
+
input.off("keypress", onKeypress);
|
|
807
|
+
input.setRawMode(wasRaw);
|
|
808
|
+
input.pause();
|
|
809
|
+
output.write("\n");
|
|
810
|
+
};
|
|
811
|
+
const onKeypress = (text, key) => {
|
|
812
|
+
if (key.ctrl && key.name === "c") {
|
|
813
|
+
cleanup();
|
|
814
|
+
reject(new CliError("USER_CANCELLED", "\u7528\u6237\u53D6\u6D88\u8F93\u5165", ExitCode.CANCELLED));
|
|
815
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
816
|
+
cleanup();
|
|
817
|
+
resolve(value2);
|
|
818
|
+
} else if (key.name === "backspace") {
|
|
819
|
+
value2 = value2.slice(0, -1);
|
|
820
|
+
} else if (!key.ctrl && !key.meta && text) {
|
|
821
|
+
value2 += text;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
input.on("keypress", onKeypress);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async function readVisibleFallback(input, output) {
|
|
828
|
+
const rl = createInterface({ input, output, terminal: true });
|
|
829
|
+
const value2 = await rl.question("App Secret: ");
|
|
830
|
+
rl.close();
|
|
831
|
+
return value2;
|
|
832
|
+
}
|
|
833
|
+
async function readAll(stream) {
|
|
834
|
+
const chunks = [];
|
|
835
|
+
for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
836
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ../core/src/json-input.ts
|
|
840
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
841
|
+
var MAX_INPUT_BYTES = 1024 * 1024;
|
|
842
|
+
async function loadJsonInput(path, stdin2) {
|
|
843
|
+
let text;
|
|
844
|
+
try {
|
|
845
|
+
text = path === "-" ? await readStream(stdin2) : await readFile3(path, "utf8");
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error instanceof CliError) throw error;
|
|
848
|
+
throw new CliError("INPUT_READ_FAILED", `\u65E0\u6CD5\u8BFB\u53D6\u8F93\u5165: ${path}`, ExitCode.VALIDATION, false, {}, [], {
|
|
849
|
+
cause: error instanceof Error ? error : void 0
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (Buffer.byteLength(text, "utf8") > MAX_INPUT_BYTES) {
|
|
853
|
+
throw new CliError("INPUT_TOO_LARGE", "\u8F93\u5165 JSON \u4E0D\u80FD\u8D85\u8FC7 1 MiB", ExitCode.VALIDATION);
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
return JSON.parse(text);
|
|
857
|
+
} catch (error) {
|
|
858
|
+
throw new CliError("INPUT_JSON_INVALID", "\u8F93\u5165\u4E0D\u662F\u6709\u6548 JSON", ExitCode.VALIDATION, false, {}, [], {
|
|
859
|
+
cause: error instanceof Error ? error : void 0
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async function readStream(stream) {
|
|
864
|
+
const chunks = [];
|
|
865
|
+
let size = 0;
|
|
866
|
+
for await (const chunk of stream) {
|
|
867
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
868
|
+
size += buffer.length;
|
|
869
|
+
if (size > MAX_INPUT_BYTES) {
|
|
870
|
+
throw new CliError("INPUT_TOO_LARGE", "\u8F93\u5165 JSON \u4E0D\u80FD\u8D85\u8FC7 1 MiB", ExitCode.VALIDATION);
|
|
871
|
+
}
|
|
872
|
+
chunks.push(buffer);
|
|
873
|
+
}
|
|
874
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ../core/src/output.ts
|
|
878
|
+
import { randomUUID } from "crypto";
|
|
879
|
+
function requestId(value2) {
|
|
880
|
+
return value2?.trim() || randomUUID();
|
|
881
|
+
}
|
|
882
|
+
function successEnvelope(command, id, result, meta, warnings = []) {
|
|
883
|
+
return redact({
|
|
884
|
+
schemaVersion: "1.0",
|
|
885
|
+
ok: true,
|
|
886
|
+
command,
|
|
887
|
+
requestId: id,
|
|
888
|
+
data: result.data,
|
|
889
|
+
page: result.page,
|
|
890
|
+
warnings,
|
|
891
|
+
meta,
|
|
892
|
+
error: null
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
function failureEnvelope(command, id, error, meta = {}) {
|
|
896
|
+
const cliError = toCliError(error);
|
|
897
|
+
return {
|
|
898
|
+
error: cliError,
|
|
899
|
+
envelope: redact({
|
|
900
|
+
schemaVersion: "1.0",
|
|
901
|
+
ok: false,
|
|
902
|
+
command,
|
|
903
|
+
requestId: id,
|
|
904
|
+
data: null,
|
|
905
|
+
page: null,
|
|
906
|
+
warnings: [],
|
|
907
|
+
meta,
|
|
908
|
+
error: cliError.toJSON()
|
|
909
|
+
})
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ../core/src/profiles.ts
|
|
914
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
915
|
+
import { homedir as homedir2 } from "os";
|
|
916
|
+
import { join as join3 } from "path";
|
|
917
|
+
import YAML from "yaml";
|
|
918
|
+
var EMPTY_PROFILES = { profiles: {} };
|
|
919
|
+
function getConfigDir(env = process.env) {
|
|
920
|
+
if (env.WCS_EDU_CONFIG_DIR) return env.WCS_EDU_CONFIG_DIR;
|
|
921
|
+
if (process.platform === "win32") return join3(env.APPDATA ?? join3(homedir2(), "AppData", "Roaming"), "wcs-edu-os-cli");
|
|
922
|
+
if (process.platform === "darwin") return join3(homedir2(), "Library", "Application Support", "wcs-edu-os-cli");
|
|
923
|
+
return join3(env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config"), "wcs-edu-os-cli");
|
|
924
|
+
}
|
|
925
|
+
function getProfilePath(env = process.env) {
|
|
926
|
+
return join3(getConfigDir(env), "profiles.yml");
|
|
927
|
+
}
|
|
928
|
+
async function loadProfiles(env = process.env) {
|
|
929
|
+
try {
|
|
930
|
+
const parsed = YAML.parse(await readFile4(getProfilePath(env), "utf8"));
|
|
931
|
+
if (!parsed || typeof parsed !== "object") return structuredClone(EMPTY_PROFILES);
|
|
932
|
+
const value2 = parsed;
|
|
933
|
+
return { currentProfile: value2.currentProfile, profiles: value2.profiles ?? {} };
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (isNotFound(error)) return structuredClone(EMPTY_PROFILES);
|
|
936
|
+
throw new CliError("PROFILE_READ_FAILED", "\u65E0\u6CD5\u8BFB\u53D6 Profile \u914D\u7F6E", ExitCode.VALIDATION, false, {}, [], {
|
|
937
|
+
cause: error instanceof Error ? error : void 0
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async function saveProfiles(config, env = process.env) {
|
|
942
|
+
const dir = getConfigDir(env);
|
|
943
|
+
const path = getProfilePath(env);
|
|
944
|
+
await mkdir3(dir, { recursive: true });
|
|
945
|
+
await writeFile3(path, YAML.stringify(config), { encoding: "utf8", mode: 384 });
|
|
946
|
+
if (process.platform !== "win32") await chmod3(path, 384);
|
|
947
|
+
}
|
|
948
|
+
async function useProfile(name, baseUrl, env = process.env) {
|
|
949
|
+
const config = await loadProfiles(env);
|
|
950
|
+
if (baseUrl) config.profiles[name] = { baseUrl: normalizeBaseUrl(baseUrl) };
|
|
951
|
+
if (!config.profiles[name]) {
|
|
952
|
+
throw new CliError("PROFILE_NOT_FOUND", `Profile \u4E0D\u5B58\u5728: ${name}`, ExitCode.VALIDATION);
|
|
953
|
+
}
|
|
954
|
+
config.currentProfile = name;
|
|
955
|
+
await saveProfiles(config, env);
|
|
956
|
+
return config;
|
|
957
|
+
}
|
|
958
|
+
async function resolveProfile(explicitName, env = process.env) {
|
|
959
|
+
const config = await loadProfiles(env);
|
|
960
|
+
const name = explicitName ?? env.WCS_EDU_PROFILE ?? config.currentProfile ?? "default";
|
|
961
|
+
const source = explicitName ? "argument" : env.WCS_EDU_PROFILE ? "environment" : config.currentProfile ? "current" : "default";
|
|
962
|
+
const baseUrl = env.WCS_EDU_BASE_URL ?? config.profiles[name]?.baseUrl;
|
|
963
|
+
if (!baseUrl) {
|
|
964
|
+
throw new CliError("BASE_URL_MISSING", "\u672A\u914D\u7F6E WCS EDU OS base URL", ExitCode.VALIDATION, false, { profile: name }, [
|
|
965
|
+
"\u8BBE\u7F6E WCS_EDU_BASE_URL",
|
|
966
|
+
`\u6216\u6267\u884C profile use ${name} --base-url <url>`
|
|
967
|
+
]);
|
|
968
|
+
}
|
|
969
|
+
return { name, baseUrl: normalizeBaseUrl(baseUrl), source };
|
|
970
|
+
}
|
|
971
|
+
function normalizeBaseUrl(value2) {
|
|
972
|
+
let url;
|
|
973
|
+
try {
|
|
974
|
+
url = new URL(value2);
|
|
975
|
+
} catch {
|
|
976
|
+
throw new CliError("BASE_URL_INVALID", "base URL \u683C\u5F0F\u65E0\u6548", ExitCode.VALIDATION);
|
|
977
|
+
}
|
|
978
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
979
|
+
throw new CliError("BASE_URL_INVALID", "base URL \u53EA\u652F\u6301 http \u6216 https", ExitCode.VALIDATION);
|
|
980
|
+
}
|
|
981
|
+
return url.toString().replace(/\/$/, "");
|
|
982
|
+
}
|
|
983
|
+
function isNotFound(error) {
|
|
984
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/runner.ts
|
|
988
|
+
import { createHash as createHash3 } from "crypto";
|
|
989
|
+
import { z as z3 } from "zod";
|
|
990
|
+
import Table from "cli-table3";
|
|
991
|
+
var VERSION = "0.1.1";
|
|
992
|
+
async function runCommand(spec2, rawInput, globals, io) {
|
|
993
|
+
const id = requestId(globals.requestId);
|
|
994
|
+
const started = Date.now();
|
|
995
|
+
let profileName;
|
|
996
|
+
try {
|
|
997
|
+
const parsed = spec2.inputSchema.safeParse(rawInput);
|
|
998
|
+
if (!parsed.success) {
|
|
999
|
+
throw new CliError("VALIDATION_ERROR", "\u547D\u4EE4\u53C2\u6570\u6821\u9A8C\u5931\u8D25", ExitCode.VALIDATION, false, {
|
|
1000
|
+
issues: parsed.error.issues
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const input = parsed.data;
|
|
1004
|
+
const result = await execute(spec2, input, globals, io, id, (name) => {
|
|
1005
|
+
profileName = name;
|
|
1006
|
+
});
|
|
1007
|
+
const envelope = successEnvelope(spec2.name, id, result, {
|
|
1008
|
+
profile: profileName ?? null,
|
|
1009
|
+
durationMs: Date.now() - started
|
|
1010
|
+
});
|
|
1011
|
+
writeSuccess(envelope, chooseOutput(globals.output, io.stdout.isTTY), spec2, io.stdout);
|
|
1012
|
+
return { exitCode: !spec2.readOnly && globals.dryRun ? ExitCode.CONFIRMATION : 0, envelope };
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
const failed = failureEnvelope(spec2.name, id, error, {
|
|
1015
|
+
profile: profileName ?? null,
|
|
1016
|
+
durationMs: Date.now() - started
|
|
1017
|
+
});
|
|
1018
|
+
io.stdout.write(`${JSON.stringify(failed.envelope)}
|
|
1019
|
+
`);
|
|
1020
|
+
if (globals.debug) io.stderr.write(`${JSON.stringify(redact({ error }))}
|
|
1021
|
+
`);
|
|
1022
|
+
return { exitCode: failed.error.exitCode, envelope: failed.envelope };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async function execute(spec2, input, globals, io, id, setProfile) {
|
|
1026
|
+
if (spec2.name === "version") return value({ version: VERSION, node: process.version });
|
|
1027
|
+
if (spec2.name === "guide") return value(buildGuide(String(input.topic ?? "all")));
|
|
1028
|
+
if (spec2.name.startsWith("profile.")) return executeProfile(spec2.name, input, globals, io, setProfile);
|
|
1029
|
+
if (spec2.name === "schema") {
|
|
1030
|
+
const target = findCommand(String(input.command));
|
|
1031
|
+
if (!target) throw new CliError("COMMAND_NOT_FOUND", `\u672A\u77E5\u547D\u4EE4: ${input.command}`, ExitCode.VALIDATION);
|
|
1032
|
+
return value({
|
|
1033
|
+
command: target.name,
|
|
1034
|
+
input: z3.toJSONSchema(target.payloadSchema ?? target.inputSchema),
|
|
1035
|
+
output: z3.toJSONSchema(target.outputSchema),
|
|
1036
|
+
endpoints: target.endpoints,
|
|
1037
|
+
risk: target.risk,
|
|
1038
|
+
readOnly: target.readOnly,
|
|
1039
|
+
supportsDryRun: target.supportsDryRun,
|
|
1040
|
+
supportsIdempotency: target.supportsIdempotency
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
if (spec2.name === "auth.status") return authStatus(globals, io, setProfile);
|
|
1044
|
+
if (spec2.name === "auth.credential-info") return credentialInfo(globals, io, setProfile);
|
|
1045
|
+
if (spec2.name === "doctor") return doctor(globals, io, id, setProfile);
|
|
1046
|
+
if (spec2.name === "capabilities") return capabilities(globals, io, id, setProfile);
|
|
1047
|
+
if (!spec2.readOnly && globals.dryRun && globals.confirmToken) {
|
|
1048
|
+
throw new CliError("CONFIRMATION_OPTION_CONFLICT", "dry-run \u4E0D\u80FD\u540C\u65F6\u63D0\u4F9B --confirm-token", ExitCode.VALIDATION);
|
|
1049
|
+
}
|
|
1050
|
+
if (!spec2.readOnly && !globals.dryRun && !globals.confirmToken) {
|
|
1051
|
+
throw new CliError("CONFIRMATION_REQUIRED", "\u5199\u64CD\u4F5C\u5FC5\u987B\u5148\u6267\u884C --dry-run\uFF0C\u518D\u63D0\u4F9B --confirm-token", ExitCode.CONFIRMATION);
|
|
1052
|
+
}
|
|
1053
|
+
if (!spec2.readOnly && !globals.dryRun) {
|
|
1054
|
+
const idempotencyKey = globals.idempotencyKey?.trim();
|
|
1055
|
+
if (!idempotencyKey || idempotencyKey.length > 128) {
|
|
1056
|
+
throw new CliError("IDEMPOTENCY_KEY_REQUIRED", "\u5199\u64CD\u4F5C\u5FC5\u987B\u63D0\u4F9B 1-128 \u5B57\u7B26\u7684 --idempotency-key", ExitCode.CONFIRMATION);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (!spec2.readOnly && input.input === "-" && globals.secretStdin) {
|
|
1060
|
+
throw new CliError("STDIN_CONFLICT", "--input - \u4E0E --secret-stdin \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528", ExitCode.VALIDATION);
|
|
1061
|
+
}
|
|
1062
|
+
const context = await createContext(globals, io, setProfile);
|
|
1063
|
+
const token = await context.client.token(id, context.secret.value, spec2.name === "auth.test");
|
|
1064
|
+
if (spec2.name === "auth.test") {
|
|
1065
|
+
const whoami = findCommand("auth.whoami");
|
|
1066
|
+
if (!whoami) throw new CliError("COMMAND_CONTRACT_ERROR", "\u7F3A\u5C11 auth.whoami \u5951\u7EA6", ExitCode.SERVER);
|
|
1067
|
+
await context.client.result(whoami, 0, id, token.access_token, {});
|
|
1068
|
+
return value({ valid: true, userId: token.user_id, expiresAt: token.expiresAt, apiCount: token.apis.length });
|
|
1069
|
+
}
|
|
1070
|
+
if (spec2.name === "statistics.dashboard") return dashboard(spec2, input, context.client, token.access_token, id);
|
|
1071
|
+
if (!spec2.readOnly) {
|
|
1072
|
+
return executeWrite(spec2, input, globals, io, context.client, context.profile.baseUrl, token, id);
|
|
1073
|
+
}
|
|
1074
|
+
const endpoint2 = spec2.endpoints[0];
|
|
1075
|
+
const result = endpoint2?.pagination ? await context.client.pagedResult(spec2, id, token.access_token, input) : await context.client.result(spec2, 0, id, token.access_token, input);
|
|
1076
|
+
if (spec2.name === "department.tree") {
|
|
1077
|
+
return value(buildTree(Array.isArray(result.data) ? result.data : []));
|
|
1078
|
+
}
|
|
1079
|
+
if (spec2.name === "auth.whoami") {
|
|
1080
|
+
return value({ user: result.data, permissions: { count: token.apis.length, apis: token.apis } });
|
|
1081
|
+
}
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
async function executeWrite(spec2, input, globals, io, client, baseUrl, token, requestIdValue) {
|
|
1085
|
+
if (!spec2.payloadSchema || !spec2.supportsDryRun || !spec2.supportsIdempotency) {
|
|
1086
|
+
throw new CliError("COMMAND_CONTRACT_ERROR", "\u5199\u547D\u4EE4\u7F3A\u5C11\u5B89\u5168\u534F\u8BAE\u58F0\u660E", ExitCode.SERVER);
|
|
1087
|
+
}
|
|
1088
|
+
const inputPath = String(input.input);
|
|
1089
|
+
const payloadResult = spec2.payloadSchema.safeParse(await loadJsonInput(inputPath, io.stdin));
|
|
1090
|
+
if (!payloadResult.success) {
|
|
1091
|
+
throw new CliError("PAYLOAD_VALIDATION_ERROR", "\u8F93\u5165 JSON \u4E0D\u7B26\u5408\u547D\u4EE4\u5951\u7EA6", ExitCode.VALIDATION, false, {
|
|
1092
|
+
issues: payloadResult.error.issues
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const payload = payloadResult.data;
|
|
1096
|
+
const endpoint2 = spec2.endpoints[0];
|
|
1097
|
+
if (!endpoint2) throw new CliError("COMMAND_CONTRACT_ERROR", "\u5199\u547D\u4EE4\u7F3A\u5C11 endpoint", ExitCode.SERVER);
|
|
1098
|
+
const binding = {
|
|
1099
|
+
command: spec2.name,
|
|
1100
|
+
baseUrl,
|
|
1101
|
+
userId: token.user_id,
|
|
1102
|
+
payload,
|
|
1103
|
+
endpoint: endpoint2.path,
|
|
1104
|
+
risk: spec2.risk,
|
|
1105
|
+
sideEffects: spec2.sideEffects,
|
|
1106
|
+
rollback: spec2.rollback
|
|
1107
|
+
};
|
|
1108
|
+
const confirmations = new ConfirmationStore(io.env);
|
|
1109
|
+
if (globals.dryRun) {
|
|
1110
|
+
const plan = await confirmations.create(binding);
|
|
1111
|
+
return value({
|
|
1112
|
+
dryRun: true,
|
|
1113
|
+
confirmationCode: plan.confirmationCode,
|
|
1114
|
+
expiresAt: plan.record.expiresAt,
|
|
1115
|
+
endpoint: endpoint2.path,
|
|
1116
|
+
method: endpoint2.method,
|
|
1117
|
+
risk: spec2.risk,
|
|
1118
|
+
sideEffects: spec2.sideEffects,
|
|
1119
|
+
rollback: spec2.rollback,
|
|
1120
|
+
payloadHash: plan.record.payloadHash
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const confirmationToken = globals.confirmToken;
|
|
1124
|
+
const idempotencyKey = globals.idempotencyKey?.trim();
|
|
1125
|
+
await confirmations.verify(confirmationToken, binding);
|
|
1126
|
+
const result = await client.result(spec2, 0, requestIdValue, token.access_token, payload, idempotencyKey);
|
|
1127
|
+
await confirmations.consume(confirmationToken);
|
|
1128
|
+
return result;
|
|
1129
|
+
}
|
|
1130
|
+
async function executeProfile(name, input, globals, io, setProfile) {
|
|
1131
|
+
const config = await loadProfiles(io.env);
|
|
1132
|
+
if (name === "profile.list") {
|
|
1133
|
+
return value(Object.entries(config.profiles).map(([profileName2, profile]) => ({
|
|
1134
|
+
name: profileName2,
|
|
1135
|
+
baseUrl: profile.baseUrl,
|
|
1136
|
+
current: config.currentProfile === profileName2
|
|
1137
|
+
})));
|
|
1138
|
+
}
|
|
1139
|
+
if (name === "profile.get") {
|
|
1140
|
+
const profileName2 = String(input.name ?? globals.profile ?? io.env.WCS_EDU_PROFILE ?? config.currentProfile ?? "default");
|
|
1141
|
+
const profile = config.profiles[profileName2];
|
|
1142
|
+
if (!profile) throw new CliError("PROFILE_NOT_FOUND", `Profile \u4E0D\u5B58\u5728: ${profileName2}`, ExitCode.VALIDATION);
|
|
1143
|
+
setProfile(profileName2);
|
|
1144
|
+
return value({ name: profileName2, ...profile, current: config.currentProfile === profileName2 });
|
|
1145
|
+
}
|
|
1146
|
+
const profileName = String(input.name);
|
|
1147
|
+
const updated = await useProfile(profileName, input.baseUrl ? String(input.baseUrl) : void 0, io.env);
|
|
1148
|
+
setProfile(profileName);
|
|
1149
|
+
return value({ name: profileName, baseUrl: updated.profiles[profileName]?.baseUrl, current: true });
|
|
1150
|
+
}
|
|
1151
|
+
async function authStatus(globals, io, setProfile) {
|
|
1152
|
+
const profiles = await loadProfiles(io.env);
|
|
1153
|
+
const profileName = globals.profile ?? io.env.WCS_EDU_PROFILE ?? profiles.currentProfile ?? "default";
|
|
1154
|
+
setProfile(profileName);
|
|
1155
|
+
const baseUrl = io.env.WCS_EDU_BASE_URL ?? profiles.profiles[profileName]?.baseUrl;
|
|
1156
|
+
const appKey = io.env.WCS_EDU_APP_KEY;
|
|
1157
|
+
const token = baseUrl && appKey ? await new TokenCache(io.env).get(baseUrl.replace(/\/$/, ""), appKey) : null;
|
|
1158
|
+
return value({
|
|
1159
|
+
profile: profileName,
|
|
1160
|
+
configPath: getProfilePath(io.env),
|
|
1161
|
+
baseUrlConfigured: Boolean(baseUrl),
|
|
1162
|
+
appKeyConfigured: Boolean(appKey),
|
|
1163
|
+
credentialSource: globals.secretStdin ? "stdin" : io.env.WCS_EDU_APP_SECRET ? "environment" : io.stdin.isTTY ? "prompt" : "none",
|
|
1164
|
+
tokenCached: Boolean(token),
|
|
1165
|
+
tokenExpiresAt: token?.expiresAt ?? null
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
async function credentialInfo(globals, io, setProfile) {
|
|
1169
|
+
const profile = await resolveProfile(globals.profile, io.env);
|
|
1170
|
+
setProfile(profile.name);
|
|
1171
|
+
const appKey = io.env.WCS_EDU_APP_KEY;
|
|
1172
|
+
const token = appKey ? await new TokenCache(io.env).get(profile.baseUrl, appKey) : null;
|
|
1173
|
+
return value({
|
|
1174
|
+
profile: profile.name,
|
|
1175
|
+
baseUrl: profile.baseUrl,
|
|
1176
|
+
appKey: appKey ? maskIdentifier(appKey) : null,
|
|
1177
|
+
appKeyFingerprint: appKey ? credentialFingerprint(profile.baseUrl, appKey) : null,
|
|
1178
|
+
credentialSource: globals.secretStdin ? "stdin" : io.env.WCS_EDU_APP_SECRET ? "environment" : io.stdin.isTTY ? "prompt" : "none",
|
|
1179
|
+
userId: token?.user_id ?? null,
|
|
1180
|
+
tokenExpiresAt: token?.expiresAt ?? null
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
async function capabilities(globals, io, id, setProfile) {
|
|
1184
|
+
try {
|
|
1185
|
+
const context = await createContext(globals, io, setProfile, false);
|
|
1186
|
+
const cached = await context.cache.get(context.profile.baseUrl, context.appKey);
|
|
1187
|
+
if (cached) return value(getCapabilities(cached.apis));
|
|
1188
|
+
if (context.secret.value) {
|
|
1189
|
+
const token = await context.client.token(id, context.secret.value);
|
|
1190
|
+
return value(getCapabilities(token.apis));
|
|
1191
|
+
}
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
return value(getCapabilities());
|
|
1195
|
+
}
|
|
1196
|
+
async function doctor(globals, io, id, setProfile) {
|
|
1197
|
+
const checks = [];
|
|
1198
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1199
|
+
checks.push({ check: "node", status: nodeMajor >= 22 ? "pass" : "fail", message: process.version });
|
|
1200
|
+
checks.push({ check: "config", status: "pass", message: getConfigDir(io.env) });
|
|
1201
|
+
let context;
|
|
1202
|
+
try {
|
|
1203
|
+
context = await createContext(globals, io, setProfile, false);
|
|
1204
|
+
checks.push({ check: "profile", status: "pass", message: context.profile.name });
|
|
1205
|
+
checks.push({ check: "baseUrl", status: "pass", message: context.profile.baseUrl });
|
|
1206
|
+
checks.push({ check: "credentials", status: context.secret.value ? "pass" : "skip", message: context.secret.source });
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
checks.push({ check: "profile/baseUrl", status: "fail", message: error instanceof Error ? error.message : String(error) });
|
|
1209
|
+
}
|
|
1210
|
+
if (context) {
|
|
1211
|
+
try {
|
|
1212
|
+
const token = await context.client.token(id, context.secret.value);
|
|
1213
|
+
checks.push({ check: "token", status: "pass", message: `userId=${token.user_id}` });
|
|
1214
|
+
const whoami = findCommand("auth.whoami");
|
|
1215
|
+
if (!whoami) throw new Error("auth.whoami contract missing");
|
|
1216
|
+
await context.client.result(whoami, 0, id, token.access_token, {});
|
|
1217
|
+
checks.push({ check: "currentUser", status: "pass", message: "/base/admin/login/user" });
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
checks.push({ check: "token/currentUser", status: context.secret.value ? "fail" : "skip", message: error instanceof Error ? error.message : String(error) });
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
checks.push({ check: "token/currentUser", status: "skip", message: "\u7F3A\u5C11\u53EF\u7528\u914D\u7F6E" });
|
|
1223
|
+
}
|
|
1224
|
+
return value({ items: checks, healthy: checks.every((item) => item.status !== "fail") });
|
|
1225
|
+
}
|
|
1226
|
+
async function dashboard(spec2, input, client, token, id) {
|
|
1227
|
+
const calls = spec2.endpoints.map((endpoint2, index) => client.result(
|
|
1228
|
+
spec2,
|
|
1229
|
+
index,
|
|
1230
|
+
id,
|
|
1231
|
+
token,
|
|
1232
|
+
endpoint2.path.endsWith("/data") ? {} : { dates: input.dates }
|
|
1233
|
+
));
|
|
1234
|
+
const settled = await Promise.allSettled(calls);
|
|
1235
|
+
const failed = settled.flatMap((result, index) => result.status === "rejected" ? [{ endpoint: spec2.endpoints[index]?.path, error: result.reason instanceof Error ? result.reason.message : String(result.reason) }] : []);
|
|
1236
|
+
if (failed.length > 0) {
|
|
1237
|
+
throw new CliError("DASHBOARD_REQUEST_FAILED", "\u4EEA\u8868\u76D8\u5B50\u8BF7\u6C42\u5931\u8D25", ExitCode.SERVER, false, { failedEndpoints: failed });
|
|
1238
|
+
}
|
|
1239
|
+
const values = settled.map((result) => result.status === "fulfilled" ? result.value.data : null);
|
|
1240
|
+
return value({ data: values[0], login: values[1], order: values[2] });
|
|
1241
|
+
}
|
|
1242
|
+
async function createContext(globals, io, setProfile, allowPrompt = true) {
|
|
1243
|
+
const profile = await resolveProfile(globals.profile, io.env);
|
|
1244
|
+
setProfile(profile.name);
|
|
1245
|
+
const appKey = io.env.WCS_EDU_APP_KEY;
|
|
1246
|
+
if (!appKey) throw new CliError("APP_KEY_MISSING", "\u672A\u8BBE\u7F6E WCS_EDU_APP_KEY", ExitCode.AUTH);
|
|
1247
|
+
const secret = await resolveAppSecret({
|
|
1248
|
+
secretStdin: globals.secretStdin,
|
|
1249
|
+
interactive: allowPrompt && !globals.nonInteractive,
|
|
1250
|
+
env: io.env,
|
|
1251
|
+
stdin: io.stdin,
|
|
1252
|
+
stderr: io.stderr
|
|
1253
|
+
});
|
|
1254
|
+
const cache = new TokenCache(io.env);
|
|
1255
|
+
const timeoutMs = Number(globals.timeout ?? 1e4);
|
|
1256
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
1257
|
+
throw new CliError("TIMEOUT_INVALID", "timeout \u5FC5\u987B\u662F\u6B63\u6574\u6570", ExitCode.VALIDATION);
|
|
1258
|
+
}
|
|
1259
|
+
const diagnostic = globals.debug ? (message, context) => io.stderr.write(`${message} ${JSON.stringify(redact(context))}
|
|
1260
|
+
`) : void 0;
|
|
1261
|
+
return { profile, appKey, secret, cache, client: new WcsClient(profile.baseUrl, appKey, cache, timeoutMs, diagnostic) };
|
|
1262
|
+
}
|
|
1263
|
+
function value(data) {
|
|
1264
|
+
return { data, page: null };
|
|
1265
|
+
}
|
|
1266
|
+
function buildGuide(topic) {
|
|
1267
|
+
const guides = {
|
|
1268
|
+
"getting-started": {
|
|
1269
|
+
title: "\u9996\u6B21\u5B89\u88C5\u4E0E\u8FDE\u63A5",
|
|
1270
|
+
summary: "\u5B89\u88C5 CLI\u3001\u914D\u7F6E\u7F51\u5173\u548C\u51ED\u636E\uFF0C\u7136\u540E\u7528 doctor \u9A8C\u8BC1\u8FDE\u63A5\u3002",
|
|
1271
|
+
commands: [
|
|
1272
|
+
"npm install -g @wcs-edu-os/cli",
|
|
1273
|
+
"wcs-edu-os version",
|
|
1274
|
+
"wcs-edu-os profile use test --base-url https://edu.example.com",
|
|
1275
|
+
"wcs-edu-os doctor --output table",
|
|
1276
|
+
"wcs-edu-os auth test --non-interactive --output json"
|
|
1277
|
+
],
|
|
1278
|
+
environment: ["WCS_EDU_BASE_URL", "WCS_EDU_APP_KEY", "WCS_EDU_APP_SECRET \u6216 --secret-stdin"],
|
|
1279
|
+
prompt: "\u8BF7\u5148\u8FD0\u884C wcs-edu-os doctor\uFF1B\u5982\u679C\u901A\u8FC7\uFF0C\u5E2E\u6211\u67E5\u770B\u5F53\u524D\u7BA1\u7406\u5458\u8EAB\u4EFD\u548C\u53EF\u7528\u6743\u9650\u3002"
|
|
1280
|
+
},
|
|
1281
|
+
course: {
|
|
1282
|
+
title: "\u8BFE\u7A0B\u7BA1\u7406\u6D41\u7A0B",
|
|
1283
|
+
summary: "\u5148\u67E5\u5206\u7C7B\u548C\u8BB2\u5E08\uFF0C\u518D\u67E5\u8BE2\u8BFE\u7A0B\u3001\u7AE0\u8282\uFF1B\u521B\u5EFA\u6216\u66F4\u65B0\u5FC5\u987B\u5148 dry-run\u3002",
|
|
1284
|
+
commands: [
|
|
1285
|
+
"wcs-edu-os category list --output table",
|
|
1286
|
+
"wcs-edu-os lecturer list --output table",
|
|
1287
|
+
"wcs-edu-os course list --keyword \u5173\u952E\u8BCD --output table",
|
|
1288
|
+
"wcs-edu-os course get <courseId> --output json",
|
|
1289
|
+
"wcs-edu-os course chapter list <courseId> --output json",
|
|
1290
|
+
"wcs-edu-os course create --input course.json --dry-run --output json"
|
|
1291
|
+
],
|
|
1292
|
+
prompt: "\u8BF7\u5148\u5217\u51FA\u8BFE\u7A0B\u5206\u7C7B\u3001\u8BB2\u5E08\u548C\u8BFE\u7A0B\uFF0C\u627E\u5230\u76EE\u6807\u8BFE\u7A0B\u540E\u5C55\u793A\u8BE6\u60C5\u4E0E\u7AE0\u8282\uFF1B\u4EFB\u4F55\u5199\u64CD\u4F5C\u5148\u7ED9\u51FA dry-run \u8BA1\u5212\uFF0C\u4E0D\u8981\u76F4\u63A5\u63D0\u4EA4\u3002"
|
|
1293
|
+
},
|
|
1294
|
+
exam: {
|
|
1295
|
+
title: "\u9898\u5E93\u4E0E\u8003\u8BD5\u67E5\u8BE2",
|
|
1296
|
+
summary: "\u6309\u8BD5\u9898\u3001\u8BD5\u5377\u3001\u8003\u8BD5\u4E09\u5C42\u67E5\u8BE2\uFF0C\u5148\u5217\u51FA\u8D44\u6E90\u518D\u67E5\u770B\u8BE6\u60C5\u3002",
|
|
1297
|
+
commands: [
|
|
1298
|
+
"wcs-edu-os question list --output table",
|
|
1299
|
+
"wcs-edu-os paper list --output table",
|
|
1300
|
+
"wcs-edu-os exam list --output table",
|
|
1301
|
+
"wcs-edu-os exam get <examId> --output json"
|
|
1302
|
+
],
|
|
1303
|
+
prompt: "\u8BF7\u5148\u5217\u51FA\u53EF\u7528\u8BD5\u5377\u548C\u8003\u8BD5\uFF0C\u6309\u540D\u79F0\u6216\u72B6\u6001\u7B5B\u9009\uFF1B\u627E\u5230\u76EE\u6807\u540E\u67E5\u8BE2\u8BE6\u60C5\uFF0C\u5E76\u8BF4\u660E\u5F53\u524D CLI \u4E0D\u652F\u6301\u53D1\u5E03\u3001\u5220\u9664\u548C\u6279\u91CF\u6307\u6D3E\u3002"
|
|
1304
|
+
},
|
|
1305
|
+
write: {
|
|
1306
|
+
title: "\u5B89\u5168\u5199\u64CD\u4F5C",
|
|
1307
|
+
summary: "\u521B\u5EFA\u548C\u66F4\u65B0\u53EA\u63A5\u53D7 JSON \u8F93\u5165\uFF0C\u5FC5\u987B\u7ECF\u8FC7 dry-run\u3001\u786E\u8BA4\u4EE4\u724C\u548C\u5E42\u7B49\u952E\u3002",
|
|
1308
|
+
commands: [
|
|
1309
|
+
"wcs-edu-os course update --input course.json --dry-run --output json",
|
|
1310
|
+
"wcs-edu-os course update --input course.json --confirm-token <token> --idempotency-key <unique-key> --output json"
|
|
1311
|
+
],
|
|
1312
|
+
prompt: "\u8BF7\u68C0\u67E5\u8FD9\u4EFD JSON \u7684\u5B57\u6BB5\u548C\u76EE\u6807\u73AF\u5883\uFF0C\u5148\u6267\u884C dry-run\uFF0C\u89E3\u91CA\u526F\u4F5C\u7528\u3001\u56DE\u6EDA\u65B9\u5F0F\u548C\u786E\u8BA4\u7801\u6709\u6548\u671F\uFF0C\u7B49\u5F85\u6211\u786E\u8BA4\u540E\u518D\u63D0\u4EA4\u3002"
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
if (topic === "all") return { topics: Object.values(guides), hint: "\u4F7F\u7528 wcs-edu-os guide <topic> \u67E5\u770B\u5355\u4E2A\u4E3B\u9898\u3002" };
|
|
1316
|
+
return guides[topic] ?? guides["getting-started"];
|
|
1317
|
+
}
|
|
1318
|
+
function chooseOutput(explicit, isTTY) {
|
|
1319
|
+
return explicit ?? (isTTY ? "table" : "json");
|
|
1320
|
+
}
|
|
1321
|
+
function writeSuccess(envelope, format, spec2, stdout2) {
|
|
1322
|
+
if (format === "jsonl") {
|
|
1323
|
+
stdout2.write(`${JSON.stringify(envelope)}
|
|
1324
|
+
`);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (format === "json") {
|
|
1328
|
+
stdout2.write(`${JSON.stringify(envelope, null, 2)}
|
|
1329
|
+
`);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
stdout2.write(`${renderTable(envelope.data, spec2)}
|
|
1333
|
+
`);
|
|
1334
|
+
}
|
|
1335
|
+
function renderTable(data, spec2) {
|
|
1336
|
+
const items = extractRows(data);
|
|
1337
|
+
if (items.length === 0) return "(\u65E0\u6570\u636E)";
|
|
1338
|
+
const columns = spec2.table.length > 0 ? spec2.table : inferColumns(items[0]);
|
|
1339
|
+
const table = new Table({ head: columns.map((column) => column.label), style: { head: [], border: [] } });
|
|
1340
|
+
for (const row of items) table.push(columns.map((column) => formatCell(row[column.key])));
|
|
1341
|
+
return table.toString();
|
|
1342
|
+
}
|
|
1343
|
+
function extractRows(data) {
|
|
1344
|
+
if (Array.isArray(data)) return data.filter(isRecord4);
|
|
1345
|
+
if (isRecord4(data) && Array.isArray(data.items)) return data.items.filter(isRecord4);
|
|
1346
|
+
if (isRecord4(data)) return Object.entries(data).map(([key, val]) => ({ key, value: val }));
|
|
1347
|
+
return [{ value: data }];
|
|
1348
|
+
}
|
|
1349
|
+
function inferColumns(row) {
|
|
1350
|
+
if (!row) return [];
|
|
1351
|
+
return Object.keys(row).slice(0, 8).map((key) => ({ key, label: key }));
|
|
1352
|
+
}
|
|
1353
|
+
function formatCell(value2) {
|
|
1354
|
+
if (value2 === null || value2 === void 0) return "";
|
|
1355
|
+
if (typeof value2 === "object") return JSON.stringify(value2);
|
|
1356
|
+
return String(value2);
|
|
1357
|
+
}
|
|
1358
|
+
function isRecord4(value2) {
|
|
1359
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/program.ts
|
|
1363
|
+
function createProgram(io) {
|
|
1364
|
+
const program = new Command();
|
|
1365
|
+
program.name("wcs-edu-os").description("WCS EDU OS \u5F00\u6E90\u7BA1\u7406\u5458\u547D\u4EE4\u884C\u5DE5\u5177").addHelpText("after", `
|
|
1366
|
+
\u9996\u6B21\u4F7F\u7528\uFF1A
|
|
1367
|
+
npm install -g @wcs-edu-os/cli
|
|
1368
|
+
wcs-edu-os guide getting-started
|
|
1369
|
+
wcs-edu-os doctor
|
|
1370
|
+
|
|
1371
|
+
\u5E38\u7528\u4E3B\u9898\uFF1Awcs-edu-os guide course | wcs-edu-os guide exam | wcs-edu-os guide write
|
|
1372
|
+
`).showHelpAfterError().exitOverride().option("--profile <name>", "\u9009\u62E9 Profile").option("--output <format>", "\u8F93\u51FA\u683C\u5F0F: json|jsonl|table").option("--non-interactive", "\u7981\u7528\u4EA4\u4E92\u63D0\u793A").option("--secret-stdin", "\u4ECE\u6807\u51C6\u8F93\u5165\u8BFB\u53D6 appSecret").option("--request-id <id>", "\u8BF7\u6C42\u94FE\u8DEF\u6807\u8BC6").option("--timeout <ms>", "\u8BF7\u6C42\u8D85\u65F6\u6BEB\u79D2\u6570", "10000").option("--no-color", "\u7981\u7528 ANSI \u989C\u8272").option("--debug", "\u8F93\u51FA\u8131\u654F\u8BCA\u65AD\u4FE1\u606F\u5230 stderr");
|
|
1373
|
+
program.option("--dry-run", "\u4EC5\u6821\u9A8C\u5199\u8BA1\u5212\u5E76\u751F\u6210\u786E\u8BA4\u4EE4\u724C").option("--confirm-token <token>", "\u6267\u884C dry-run \u751F\u6210\u7684\u786E\u8BA4\u4EE4\u724C").option("--idempotency-key <key>", "\u5199\u8BF7\u6C42\u5E42\u7B49\u952E");
|
|
1374
|
+
for (const spec2 of commandRegistry) registerSpec(program, spec2, io);
|
|
1375
|
+
return program;
|
|
1376
|
+
}
|
|
1377
|
+
async function runProgram(argv, io) {
|
|
1378
|
+
const program = createProgram(io);
|
|
1379
|
+
try {
|
|
1380
|
+
await program.parseAsync(argv, { from: "user" });
|
|
1381
|
+
return Number(process.exitCode ?? 0);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (error instanceof CommanderError && error.exitCode === 0) return 0;
|
|
1384
|
+
const id = requestId();
|
|
1385
|
+
const cliError = error instanceof CommanderError ? new CliError("VALIDATION_ERROR", error.message, ExitCode.VALIDATION) : error;
|
|
1386
|
+
const failed = failureEnvelope("cli", id, cliError);
|
|
1387
|
+
io.stdout.write(`${JSON.stringify(failed.envelope)}
|
|
1388
|
+
`);
|
|
1389
|
+
return failed.error.exitCode;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function registerSpec(program, spec2, io) {
|
|
1393
|
+
let parent = program;
|
|
1394
|
+
for (let index = 0; index < spec2.segments.length - 1; index += 1) {
|
|
1395
|
+
const segment = spec2.segments[index];
|
|
1396
|
+
let child = parent.commands.find((command) => command.name() === segment);
|
|
1397
|
+
if (!child) {
|
|
1398
|
+
child = new Command(segment).exitOverride();
|
|
1399
|
+
parent.addCommand(child);
|
|
1400
|
+
}
|
|
1401
|
+
parent = child;
|
|
1402
|
+
}
|
|
1403
|
+
const leaf = new Command(spec2.segments.at(-1)).description(spec2.description).exitOverride();
|
|
1404
|
+
leaf.addHelpText("after", `
|
|
1405
|
+
\u793A\u4F8B\uFF1Awcs-edu-os ${spec2.segments.join(" ")} --output json
|
|
1406
|
+
`);
|
|
1407
|
+
for (const argument of spec2.arguments) {
|
|
1408
|
+
leaf.argument(argument.required ? `<${argument.name}>` : `[${argument.name}]`, argument.description);
|
|
1409
|
+
}
|
|
1410
|
+
for (const option of spec2.options) addOption(leaf, option);
|
|
1411
|
+
leaf.action(async (...values) => {
|
|
1412
|
+
const command = values.at(-1);
|
|
1413
|
+
const positional = values.slice(0, spec2.arguments.length);
|
|
1414
|
+
const input = { ...command.opts() };
|
|
1415
|
+
spec2.arguments.forEach((argument, index) => {
|
|
1416
|
+
if (positional[index] !== void 0) input[argument.name] = positional[index];
|
|
1417
|
+
});
|
|
1418
|
+
const globals = command.optsWithGlobals();
|
|
1419
|
+
if (globals.output && !["json", "jsonl", "table"].includes(globals.output)) {
|
|
1420
|
+
throw new CliError("OUTPUT_INVALID", `\u4E0D\u652F\u6301\u7684\u8F93\u51FA\u683C\u5F0F: ${globals.output}`, ExitCode.VALIDATION);
|
|
1421
|
+
}
|
|
1422
|
+
const result = await runCommand(spec2, input, globals, io);
|
|
1423
|
+
process.exitCode = result.exitCode;
|
|
1424
|
+
});
|
|
1425
|
+
parent.addCommand(leaf);
|
|
1426
|
+
}
|
|
1427
|
+
function addOption(command, spec2) {
|
|
1428
|
+
const option = new Option(spec2.flags, spec2.description);
|
|
1429
|
+
if (spec2.collect) {
|
|
1430
|
+
option.argParser((value2, previous = []) => previous.concat(value2));
|
|
1431
|
+
option.default([]);
|
|
1432
|
+
} else if (spec2.defaultValue !== void 0) {
|
|
1433
|
+
option.default(spec2.defaultValue);
|
|
1434
|
+
}
|
|
1435
|
+
command.addOption(option);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/index.ts
|
|
1439
|
+
var exitCode = await runProgram(process.argv.slice(2), {
|
|
1440
|
+
stdin,
|
|
1441
|
+
stdout,
|
|
1442
|
+
stderr,
|
|
1443
|
+
env: process.env
|
|
1444
|
+
});
|
|
1445
|
+
process.exitCode = exitCode;
|
|
1446
|
+
//# sourceMappingURL=index.js.map
|