@mo7yw4ng/openape 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -9
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +13 -14
- package/esm/src/commands/assignments.d.ts +3 -0
- package/esm/src/commands/assignments.d.ts.map +1 -0
- package/esm/src/commands/assignments.js +230 -0
- package/esm/src/commands/auth.d.ts +1 -1
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -8
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +17 -18
- package/esm/src/commands/courses.js +3 -3
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +132 -45
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +176 -48
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +165 -65
- package/esm/src/commands/skills.d.ts.map +1 -1
- package/esm/src/commands/skills.js +4 -8
- package/esm/src/commands/upload.d.ts +3 -0
- package/esm/src/commands/upload.d.ts.map +1 -0
- package/esm/src/commands/upload.js +58 -0
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +113 -79
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +14 -3
- package/esm/src/lib/auth.d.ts +23 -1
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +36 -3
- package/esm/src/lib/moodle.d.ts +226 -2
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +648 -37
- package/esm/src/lib/types.d.ts +82 -164
- package/esm/src/lib/types.d.ts.map +1 -1
- package/esm/src/lib/types.js +1 -0
- package/esm/src/lib/utils.d.ts +40 -0
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +82 -4
- package/package.json +1 -2
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +12 -13
- package/script/src/commands/assignments.d.ts +3 -0
- package/script/src/commands/assignments.d.ts.map +1 -0
- package/script/src/commands/assignments.js +269 -0
- package/script/src/commands/auth.d.ts +1 -1
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -8
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +16 -17
- package/script/src/commands/courses.js +2 -2
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +132 -45
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +177 -49
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +163 -63
- package/script/src/commands/skills.d.ts.map +1 -1
- package/script/src/commands/skills.js +4 -8
- package/script/src/commands/upload.d.ts +3 -0
- package/script/src/commands/upload.d.ts.map +1 -0
- package/script/src/commands/upload.js +64 -0
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +114 -80
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +13 -2
- package/script/src/lib/auth.d.ts +23 -1
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +70 -3
- package/script/src/lib/moodle.d.ts +226 -2
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +663 -37
- package/script/src/lib/types.d.ts +82 -164
- package/script/src/lib/types.d.ts.map +1 -1
- package/script/src/lib/types.js +1 -0
- package/script/src/lib/utils.d.ts +40 -0
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +89 -3
- package/skills/openape/SKILL.md +73 -291
|
@@ -13,14 +13,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
13
13
|
function registerForumsCommand(program) {
|
|
14
14
|
const forumsCmd = program.command("forums");
|
|
15
15
|
forumsCmd.description("Forum operations");
|
|
16
|
-
function getOutputFormat(command) {
|
|
17
|
-
const opts = command.optsWithGlobals();
|
|
18
|
-
return opts.output || "json";
|
|
19
|
-
}
|
|
20
16
|
// Pure API context - no browser required (fast!)
|
|
21
17
|
async function createApiContext(options, command) {
|
|
22
18
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
23
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
19
|
+
const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
24
20
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
25
21
|
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
26
22
|
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
@@ -71,23 +67,24 @@ function registerForumsCommand(program) {
|
|
|
71
67
|
allForums.push({
|
|
72
68
|
course_id: wsForum.courseid,
|
|
73
69
|
course_name: course.fullname,
|
|
70
|
+
intro: wsForum.intro,
|
|
74
71
|
cmid: wsForum.cmid.toString(),
|
|
75
72
|
forum_id: wsForum.id,
|
|
76
73
|
name: wsForum.name,
|
|
74
|
+
timemodified: wsForum.timemodified,
|
|
77
75
|
// url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
|
|
78
76
|
});
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
|
-
|
|
79
|
+
console.log(JSON.stringify({
|
|
82
80
|
status: "success",
|
|
83
81
|
timestamp: new Date().toISOString(),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
console.log(JSON.stringify(result));
|
|
82
|
+
total_courses: courses.length,
|
|
83
|
+
total_forums: allForums.length,
|
|
84
|
+
}));
|
|
85
|
+
for (const forum of allForums) {
|
|
86
|
+
console.log(JSON.stringify(forum));
|
|
87
|
+
}
|
|
91
88
|
});
|
|
92
89
|
forumsCmd
|
|
93
90
|
.command("list-all")
|
|
@@ -114,30 +111,29 @@ function registerForumsCommand(program) {
|
|
|
114
111
|
allForums.push({
|
|
115
112
|
course_id: wsForum.courseid,
|
|
116
113
|
course_name: course.fullname,
|
|
114
|
+
intro: wsForum.intro,
|
|
117
115
|
cmid: wsForum.cmid.toString(),
|
|
118
116
|
forum_id: wsForum.id,
|
|
119
117
|
name: wsForum.name,
|
|
120
|
-
|
|
118
|
+
timemodified: wsForum.timemodified,
|
|
121
119
|
});
|
|
122
120
|
}
|
|
123
121
|
}
|
|
124
|
-
|
|
122
|
+
console.log(JSON.stringify({
|
|
125
123
|
status: "success",
|
|
126
124
|
timestamp: new Date().toISOString(),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
console.log(JSON.stringify(result));
|
|
125
|
+
total_courses: courses.length,
|
|
126
|
+
total_forums: allForums.length,
|
|
127
|
+
}));
|
|
128
|
+
for (const forum of allForums) {
|
|
129
|
+
console.log(JSON.stringify(forum));
|
|
130
|
+
}
|
|
134
131
|
});
|
|
135
132
|
forumsCmd
|
|
136
133
|
.command("discussions")
|
|
137
134
|
.description("List discussions in a forum (use forum ID)")
|
|
138
135
|
.argument("<forum-id>", "Forum ID")
|
|
139
136
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
140
|
-
.option("--msg", "Include message content in response")
|
|
141
137
|
.action(async (forumId, options, command) => {
|
|
142
138
|
const apiContext = await createApiContext(options, command);
|
|
143
139
|
if (!apiContext) {
|
|
@@ -161,32 +157,29 @@ function registerForumsCommand(program) {
|
|
|
161
157
|
const course = courses.find(c => c.id === targetForum.courseid);
|
|
162
158
|
// Get discussions via WS API
|
|
163
159
|
const discussions = await (0, moodle_js_1.getForumDiscussionsApi)(apiContext.session, targetForum.id);
|
|
164
|
-
|
|
160
|
+
// Output NDJSON: one line per discussion entry for stream-friendly parsing
|
|
161
|
+
const meta = {
|
|
165
162
|
status: "success",
|
|
166
163
|
timestamp: new Date().toISOString(),
|
|
167
164
|
forum_id: targetForum.id,
|
|
168
165
|
forum_name: targetForum.name,
|
|
166
|
+
forum_intro: targetForum.intro,
|
|
169
167
|
course_id: course?.id,
|
|
170
168
|
course_name: course?.fullname,
|
|
171
|
-
|
|
172
|
-
const discussion = {
|
|
173
|
-
id: d.id,
|
|
174
|
-
name: d.name,
|
|
175
|
-
user_id: d.userId,
|
|
176
|
-
time_modified: d.timeModified,
|
|
177
|
-
post_count: d.postCount,
|
|
178
|
-
unread: d.unread,
|
|
179
|
-
};
|
|
180
|
-
if (options.msg) {
|
|
181
|
-
discussion.message = (0, utils_js_1.stripHtmlTags)(d.message || "");
|
|
182
|
-
}
|
|
183
|
-
return discussion;
|
|
184
|
-
}),
|
|
185
|
-
summary: {
|
|
186
|
-
total_discussions: discussions.length,
|
|
187
|
-
},
|
|
169
|
+
total_discussions: discussions.length,
|
|
188
170
|
};
|
|
189
|
-
console.log(JSON.stringify(
|
|
171
|
+
console.log(JSON.stringify(meta));
|
|
172
|
+
for (const d of discussions) {
|
|
173
|
+
console.log(JSON.stringify({
|
|
174
|
+
id: d.id,
|
|
175
|
+
name: d.name,
|
|
176
|
+
user_id: d.userId,
|
|
177
|
+
time_modified: d.timeModified,
|
|
178
|
+
post_count: d.postCount,
|
|
179
|
+
unread: d.unread,
|
|
180
|
+
message: (0, utils_js_1.stripHtmlTags)(d.message || ""),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
190
183
|
});
|
|
191
184
|
forumsCmd
|
|
192
185
|
.command("posts")
|
|
@@ -194,7 +187,7 @@ function registerForumsCommand(program) {
|
|
|
194
187
|
.argument("<discussion-id>", "Discussion ID")
|
|
195
188
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
196
189
|
.action(async (discussionId, options, command) => {
|
|
197
|
-
const output = getOutputFormat(command);
|
|
190
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
198
191
|
const apiContext = await createApiContext(options, command);
|
|
199
192
|
if (!apiContext) {
|
|
200
193
|
process.exitCode = 1;
|
|
@@ -211,8 +204,8 @@ function registerForumsCommand(program) {
|
|
|
211
204
|
subject: p.subject,
|
|
212
205
|
author: p.author,
|
|
213
206
|
author_id: p.authorId,
|
|
214
|
-
created:
|
|
215
|
-
modified:
|
|
207
|
+
created: (0, utils_js_1.formatTimestamp)(p.created),
|
|
208
|
+
modified: (0, utils_js_1.formatTimestamp)(p.modified),
|
|
216
209
|
message: p.message,
|
|
217
210
|
unread: p.unread,
|
|
218
211
|
})),
|
|
@@ -234,4 +227,98 @@ function registerForumsCommand(program) {
|
|
|
234
227
|
console.table(tablePosts);
|
|
235
228
|
}
|
|
236
229
|
});
|
|
230
|
+
forumsCmd
|
|
231
|
+
.command("post")
|
|
232
|
+
.description("Post a new discussion to a forum")
|
|
233
|
+
.argument("<forum-id>", "Forum ID")
|
|
234
|
+
.argument("<subject>", "Discussion subject")
|
|
235
|
+
.argument("<message>", "Discussion message")
|
|
236
|
+
.option("--subscribe", "Subscribe to the discussion", false)
|
|
237
|
+
.option("--pin", "Pin the discussion", false)
|
|
238
|
+
.action(async (forumId, subject, message, options, command) => {
|
|
239
|
+
const apiContext = await createApiContext(options, command);
|
|
240
|
+
if (!apiContext) {
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const { log, session } = apiContext;
|
|
245
|
+
// Get courses to find the forum
|
|
246
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(session, {
|
|
247
|
+
classification: "inprogress",
|
|
248
|
+
});
|
|
249
|
+
const courseIds = courses.map(c => c.id);
|
|
250
|
+
const wsForums = await (0, moodle_js_1.getForumsApi)(session, courseIds);
|
|
251
|
+
// Find forum by cmid or instance ID
|
|
252
|
+
const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
|
|
253
|
+
if (!targetForum) {
|
|
254
|
+
log.error(`Forum not found: ${forumId}`);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const course = courses.find(c => c.id === targetForum.courseid);
|
|
259
|
+
log.info(`Posting to forum: ${targetForum.name} (${course?.fullname})`);
|
|
260
|
+
const result = await (0, moodle_js_1.addForumDiscussionApi)(session, targetForum.id, subject, message);
|
|
261
|
+
if (result.success) {
|
|
262
|
+
log.success(`✓ Discussion posted successfully!`);
|
|
263
|
+
log.info(` Discussion ID: ${result.discussionId}`);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
log.error(`✗ Failed to post discussion: ${result.error}`);
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
forumsCmd
|
|
271
|
+
.command("reply")
|
|
272
|
+
.description("Reply to a discussion post")
|
|
273
|
+
.argument("<post-id>", "Parent post ID to reply to")
|
|
274
|
+
.argument("<subject>", "Reply subject")
|
|
275
|
+
.argument("<message>", "Reply message")
|
|
276
|
+
.option("--attachment-id <id>", "Draft file ID for attachment")
|
|
277
|
+
.option("--inline-attachment-id <id>", "Draft file ID for inline attachment")
|
|
278
|
+
.action(async (postId, subject, message, options, command) => {
|
|
279
|
+
const apiContext = await createApiContext(options, command);
|
|
280
|
+
if (!apiContext) {
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const { log, session } = apiContext;
|
|
285
|
+
log.info(`Replying to post: ${postId}`);
|
|
286
|
+
log.info(` Subject: ${subject}`);
|
|
287
|
+
log.info(` Message: ${message}`);
|
|
288
|
+
if (options.attachmentId) {
|
|
289
|
+
log.info(` Attachment ID: ${options.attachmentId}`);
|
|
290
|
+
}
|
|
291
|
+
const result = await (0, moodle_js_1.addForumPostApi)(session, parseInt(postId, 10), subject, message, {
|
|
292
|
+
attachmentId: options.attachmentId ? parseInt(options.attachmentId, 10) : undefined,
|
|
293
|
+
inlineAttachmentId: options.inlineAttachmentId ? parseInt(options.inlineAttachmentId, 10) : undefined,
|
|
294
|
+
});
|
|
295
|
+
if (result.success) {
|
|
296
|
+
log.success(`✓ Reply posted successfully!`);
|
|
297
|
+
log.info(` Post ID: ${result.postId}`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
log.error(`✗ Failed to post reply: ${result.error}`);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
forumsCmd
|
|
305
|
+
.command("delete")
|
|
306
|
+
.description("Delete a forum post or discussion (by post ID)")
|
|
307
|
+
.argument("<post-id>", "Post ID to delete (deletes entire discussion if it's the first post)")
|
|
308
|
+
.action(async (postId, options, command) => {
|
|
309
|
+
const apiContext = await createApiContext(options, command);
|
|
310
|
+
if (!apiContext) {
|
|
311
|
+
process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const { log, session } = apiContext;
|
|
315
|
+
const result = await (0, moodle_js_1.deleteForumPostApi)(session, parseInt(postId, 10));
|
|
316
|
+
if (result.success) {
|
|
317
|
+
log.success(`✓ Post ${postId} deleted successfully!`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
log.error(`✗ Failed to delete post: ${result.error}`);
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
237
324
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"materials.d.ts","sourceRoot":"","sources":["../../../src/src/commands/materials.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8iB/D"}
|
|
@@ -10,53 +10,19 @@ const logger_js_1 = require("../lib/logger.js");
|
|
|
10
10
|
const auth_js_1 = require("../lib/auth.js");
|
|
11
11
|
const session_js_1 = require("../lib/session.js");
|
|
12
12
|
const auth_js_2 = require("../lib/auth.js");
|
|
13
|
-
const
|
|
13
|
+
const index_js_1 = require("../index.js");
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
16
|
function registerMaterialsCommand(program) {
|
|
17
17
|
const materialsCmd = program.command("materials");
|
|
18
18
|
materialsCmd.description("Material/resource operations");
|
|
19
|
-
// Helper to get output format from global or local options
|
|
20
|
-
function getOutputFormat(command) {
|
|
21
|
-
const opts = command.optsWithGlobals();
|
|
22
|
-
return opts.output || "json";
|
|
23
|
-
}
|
|
24
|
-
// Pure API context - no browser required (fast!)
|
|
25
|
-
async function createApiContext(options, command) {
|
|
26
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
27
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
28
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
29
|
-
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
30
|
-
const baseDir = (0, utils_js_1.getBaseDir)();
|
|
31
|
-
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
32
|
-
// Check if session exists
|
|
33
|
-
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
34
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
35
|
-
log.info(`Session 預期位置: ${sessionPath}`);
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
// Try to load WS token
|
|
39
|
-
const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
|
|
40
|
-
if (!wsToken) {
|
|
41
|
-
log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
log,
|
|
46
|
-
session: {
|
|
47
|
-
wsToken,
|
|
48
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
19
|
// Helper function to create session context (for download commands)
|
|
53
20
|
async function createSessionContext(options, command) {
|
|
54
21
|
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
55
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
22
|
+
const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
56
23
|
const silent = outputFormat === "json" && !opts.verbose;
|
|
57
24
|
const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
|
|
58
|
-
const
|
|
59
|
-
const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
|
|
25
|
+
const sessionPath = (0, utils_js_1.getSessionPath)();
|
|
60
26
|
if (!node_fs_1.default.existsSync(sessionPath)) {
|
|
61
27
|
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
62
28
|
return null;
|
|
@@ -83,10 +49,6 @@ function registerMaterialsCommand(program) {
|
|
|
83
49
|
throw err;
|
|
84
50
|
}
|
|
85
51
|
}
|
|
86
|
-
// Helper to sanitize filenames
|
|
87
|
-
function sanitizeFilename(name) {
|
|
88
|
-
return name.replace(/[<>:"/\\|?*]/g, "_").replace(/\s+/g, "_");
|
|
89
|
-
}
|
|
90
52
|
// Helper to download a single resource
|
|
91
53
|
async function downloadResource(page, resource, outputDir, log) {
|
|
92
54
|
try {
|
|
@@ -96,10 +58,8 @@ function registerMaterialsCommand(program) {
|
|
|
96
58
|
return null;
|
|
97
59
|
}
|
|
98
60
|
// Create course directory
|
|
99
|
-
const courseDir = node_path_1.default.join(outputDir, sanitizeFilename(resource.course_name));
|
|
100
|
-
|
|
101
|
-
node_fs_1.default.mkdirSync(courseDir, { recursive: true });
|
|
102
|
-
}
|
|
61
|
+
const courseDir = node_path_1.default.join(outputDir, (0, utils_js_1.sanitizeFilename)(resource.course_name));
|
|
62
|
+
await node_fs_1.default.promises.mkdir(courseDir, { recursive: true });
|
|
103
63
|
// Navigate to resource page
|
|
104
64
|
log.debug(` Downloading: ${resource.name}`);
|
|
105
65
|
await page.goto(resource.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
@@ -114,7 +74,7 @@ function registerMaterialsCommand(program) {
|
|
|
114
74
|
// Extract filename from URL or use resource name
|
|
115
75
|
const urlObj = new URL(downloadUrl);
|
|
116
76
|
const filenameParam = urlObj.searchParams.get("filename");
|
|
117
|
-
let filename = filenameParam || sanitizeFilename(resource.name);
|
|
77
|
+
let filename = filenameParam || (0, utils_js_1.sanitizeFilename)(resource.name);
|
|
118
78
|
// Add extension if missing
|
|
119
79
|
if (resource.mimetype && !node_path_1.default.extname(filename)) {
|
|
120
80
|
const extMap = {
|
|
@@ -140,8 +100,8 @@ function registerMaterialsCommand(program) {
|
|
|
140
100
|
const download = await downloadPromise;
|
|
141
101
|
// Save file
|
|
142
102
|
await download.saveAs(outputPath);
|
|
143
|
-
const stats = node_fs_1.default.
|
|
144
|
-
log.success(` Downloaded: ${filename} (${(stats.size
|
|
103
|
+
const stats = await node_fs_1.default.promises.stat(outputPath);
|
|
104
|
+
log.success(` Downloaded: ${filename} (${(0, utils_js_1.formatFileSize)(stats.size, 1)} KB)`);
|
|
145
105
|
return {
|
|
146
106
|
filename,
|
|
147
107
|
path: outputPath,
|
|
@@ -161,7 +121,7 @@ function registerMaterialsCommand(program) {
|
|
|
161
121
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
162
122
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
163
123
|
.action(async (options, command) => {
|
|
164
|
-
const apiContext = await createApiContext(options, command);
|
|
124
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
165
125
|
if (!apiContext) {
|
|
166
126
|
process.exitCode = 1;
|
|
167
127
|
return;
|
|
@@ -365,4 +325,172 @@ function registerMaterialsCommand(program) {
|
|
|
365
325
|
await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
|
|
366
326
|
}
|
|
367
327
|
});
|
|
328
|
+
materialsCmd
|
|
329
|
+
.command("complete")
|
|
330
|
+
.description("Mark all incomplete resources (non-video) as complete in a course")
|
|
331
|
+
.argument("<course-id>", "Course ID")
|
|
332
|
+
.option("--dry-run", "Show what would be marked complete without doing it")
|
|
333
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
334
|
+
.action(async (courseId, options, command) => {
|
|
335
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
336
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
337
|
+
if (!apiContext) {
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const { log, session } = apiContext;
|
|
343
|
+
// Get user ID
|
|
344
|
+
const siteInfo = await (0, moodle_js_1.getSiteInfoApi)(session);
|
|
345
|
+
// Get completion status for all activities in the course
|
|
346
|
+
const completionData = await (0, moodle_js_1.moodleApiCall)(session, "core_completion_get_activities_completion_status", { courseid: parseInt(courseId, 10), userid: siteInfo.userid });
|
|
347
|
+
if (!completionData?.statuses) {
|
|
348
|
+
log.info("No activities found in this course.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Filter for resources (non-video) that have completion enabled but are not complete
|
|
352
|
+
const incompleteResources = completionData.statuses.filter((status) => {
|
|
353
|
+
// Only resources, not supervideo
|
|
354
|
+
if (status.modname === "supervideo")
|
|
355
|
+
return false;
|
|
356
|
+
// Must have completion enabled
|
|
357
|
+
if (!status.hascompletion)
|
|
358
|
+
return false;
|
|
359
|
+
// Must be incomplete
|
|
360
|
+
if (status.isoverallcomplete)
|
|
361
|
+
return false;
|
|
362
|
+
return true;
|
|
363
|
+
});
|
|
364
|
+
if (incompleteResources.length === 0) {
|
|
365
|
+
log.info("All resources are already complete (or no resources with completion tracking).");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
log.info(`Found ${incompleteResources.length} incomplete resources to complete:`);
|
|
369
|
+
for (const resource of incompleteResources) {
|
|
370
|
+
log.info(` - ${resource.name} (cmid: ${resource.cmid})`);
|
|
371
|
+
}
|
|
372
|
+
if (options.dryRun) {
|
|
373
|
+
log.info("\n[Dry run] No changes made.");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// Mark each resource as complete
|
|
377
|
+
const results = [];
|
|
378
|
+
for (const resource of incompleteResources) {
|
|
379
|
+
const success = await (0, moodle_js_1.updateActivityCompletionStatusManually)(session, resource.cmid, true);
|
|
380
|
+
results.push({
|
|
381
|
+
cmid: resource.cmid,
|
|
382
|
+
name: resource.name,
|
|
383
|
+
success,
|
|
384
|
+
});
|
|
385
|
+
if (success) {
|
|
386
|
+
log.success(` ✓ Completed: ${resource.name}`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
log.error(` ✗ Failed: ${resource.name}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const completed = results.filter(r => r.success).length;
|
|
393
|
+
const failed = results.filter(r => !r.success).length;
|
|
394
|
+
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
395
|
+
if (output !== "silent") {
|
|
396
|
+
(0, index_js_1.formatAndOutput)(results, output, log);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
apiContext.log.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
materialsCmd
|
|
405
|
+
.command("complete-all")
|
|
406
|
+
.description("Mark all incomplete resources (non-video) as complete across all in-progress courses")
|
|
407
|
+
.option("--dry-run", "Show what would be marked complete without doing it")
|
|
408
|
+
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
409
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
410
|
+
.action(async (options, command) => {
|
|
411
|
+
const output = (0, utils_js_1.getOutputFormat)(command);
|
|
412
|
+
const apiContext = await (0, auth_js_1.createApiContext)(options, command);
|
|
413
|
+
if (!apiContext) {
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const { log, session } = apiContext;
|
|
419
|
+
// Get user ID
|
|
420
|
+
const siteInfo = await (0, moodle_js_1.getSiteInfoApi)(session);
|
|
421
|
+
// Get all courses
|
|
422
|
+
const classification = options.level === "all" ? undefined : "inprogress";
|
|
423
|
+
const courses = await (0, moodle_js_1.getEnrolledCoursesApi)(session, { classification });
|
|
424
|
+
log.info(`Scanning ${courses.length} courses for incomplete resources...`);
|
|
425
|
+
const allResults = [];
|
|
426
|
+
let totalIncomplete = 0;
|
|
427
|
+
for (const course of courses) {
|
|
428
|
+
try {
|
|
429
|
+
// Get completion status for all activities in the course
|
|
430
|
+
const completionData = await (0, moodle_js_1.moodleApiCall)(session, "core_completion_get_activities_completion_status", { courseid: course.id, userid: siteInfo.userid });
|
|
431
|
+
if (!completionData?.statuses)
|
|
432
|
+
continue;
|
|
433
|
+
// Filter for resources (non-video) that have completion enabled but are not complete
|
|
434
|
+
const incompleteResources = completionData.statuses.filter((status) => {
|
|
435
|
+
if (status.modname === "supervideo")
|
|
436
|
+
return false;
|
|
437
|
+
if (!status.hascompletion)
|
|
438
|
+
return false;
|
|
439
|
+
if (status.isoverallcomplete)
|
|
440
|
+
return false;
|
|
441
|
+
return true;
|
|
442
|
+
});
|
|
443
|
+
if (incompleteResources.length > 0) {
|
|
444
|
+
log.info(`\n${course.fullname}: ${incompleteResources.length} incomplete resources`);
|
|
445
|
+
totalIncomplete += incompleteResources.length;
|
|
446
|
+
if (options.dryRun) {
|
|
447
|
+
for (const resource of incompleteResources) {
|
|
448
|
+
log.info(` - ${resource.name} (cmid: ${resource.cmid})`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
for (const resource of incompleteResources) {
|
|
453
|
+
const success = await (0, moodle_js_1.updateActivityCompletionStatusManually)(session, resource.cmid, true);
|
|
454
|
+
allResults.push({
|
|
455
|
+
courseId: course.id,
|
|
456
|
+
courseName: course.fullname,
|
|
457
|
+
cmid: resource.cmid,
|
|
458
|
+
name: resource.name,
|
|
459
|
+
success,
|
|
460
|
+
});
|
|
461
|
+
if (success) {
|
|
462
|
+
log.success(` ✓ Completed: ${resource.name}`);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
log.error(` ✗ Failed: ${resource.name}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
log.warn(`Failed to process course ${course.fullname}: ${e}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (totalIncomplete === 0) {
|
|
476
|
+
log.info("\nAll resources are already complete (or no resources with completion tracking).");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (options.dryRun) {
|
|
480
|
+
log.info(`\n[Dry run] Found ${totalIncomplete} incomplete resources across ${courses.length} courses.`);
|
|
481
|
+
log.info("Run without --dry-run to mark them as complete.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const completed = allResults.filter(r => r.success).length;
|
|
485
|
+
const failed = allResults.filter(r => !r.success).length;
|
|
486
|
+
log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
|
|
487
|
+
if (output !== "silent") {
|
|
488
|
+
(0, index_js_1.formatAndOutput)(allResults, output, log);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
apiContext.log.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
493
|
+
process.exitCode = 1;
|
|
494
|
+
}
|
|
495
|
+
});
|
|
368
496
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwDpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0Q7D"}
|