@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.
Files changed (81) hide show
  1. package/README.md +37 -9
  2. package/esm/deno.js +1 -1
  3. package/esm/src/commands/announcements.d.ts.map +1 -1
  4. package/esm/src/commands/announcements.js +13 -14
  5. package/esm/src/commands/assignments.d.ts +3 -0
  6. package/esm/src/commands/assignments.d.ts.map +1 -0
  7. package/esm/src/commands/assignments.js +230 -0
  8. package/esm/src/commands/auth.d.ts +1 -1
  9. package/esm/src/commands/auth.d.ts.map +1 -1
  10. package/esm/src/commands/auth.js +24 -8
  11. package/esm/src/commands/calendar.d.ts.map +1 -1
  12. package/esm/src/commands/calendar.js +17 -18
  13. package/esm/src/commands/courses.js +3 -3
  14. package/esm/src/commands/forums.d.ts.map +1 -1
  15. package/esm/src/commands/forums.js +132 -45
  16. package/esm/src/commands/materials.d.ts.map +1 -1
  17. package/esm/src/commands/materials.js +176 -48
  18. package/esm/src/commands/quizzes.d.ts.map +1 -1
  19. package/esm/src/commands/quizzes.js +165 -65
  20. package/esm/src/commands/skills.d.ts.map +1 -1
  21. package/esm/src/commands/skills.js +4 -8
  22. package/esm/src/commands/upload.d.ts +3 -0
  23. package/esm/src/commands/upload.d.ts.map +1 -0
  24. package/esm/src/commands/upload.js +58 -0
  25. package/esm/src/commands/videos.d.ts.map +1 -1
  26. package/esm/src/commands/videos.js +113 -79
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +14 -3
  29. package/esm/src/lib/auth.d.ts +23 -1
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +36 -3
  32. package/esm/src/lib/moodle.d.ts +226 -2
  33. package/esm/src/lib/moodle.d.ts.map +1 -1
  34. package/esm/src/lib/moodle.js +648 -37
  35. package/esm/src/lib/types.d.ts +82 -164
  36. package/esm/src/lib/types.d.ts.map +1 -1
  37. package/esm/src/lib/types.js +1 -0
  38. package/esm/src/lib/utils.d.ts +40 -0
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +82 -4
  41. package/package.json +1 -2
  42. package/script/deno.js +1 -1
  43. package/script/src/commands/announcements.d.ts.map +1 -1
  44. package/script/src/commands/announcements.js +12 -13
  45. package/script/src/commands/assignments.d.ts +3 -0
  46. package/script/src/commands/assignments.d.ts.map +1 -0
  47. package/script/src/commands/assignments.js +269 -0
  48. package/script/src/commands/auth.d.ts +1 -1
  49. package/script/src/commands/auth.d.ts.map +1 -1
  50. package/script/src/commands/auth.js +24 -8
  51. package/script/src/commands/calendar.d.ts.map +1 -1
  52. package/script/src/commands/calendar.js +16 -17
  53. package/script/src/commands/courses.js +2 -2
  54. package/script/src/commands/forums.d.ts.map +1 -1
  55. package/script/src/commands/forums.js +132 -45
  56. package/script/src/commands/materials.d.ts.map +1 -1
  57. package/script/src/commands/materials.js +177 -49
  58. package/script/src/commands/quizzes.d.ts.map +1 -1
  59. package/script/src/commands/quizzes.js +163 -63
  60. package/script/src/commands/skills.d.ts.map +1 -1
  61. package/script/src/commands/skills.js +4 -8
  62. package/script/src/commands/upload.d.ts +3 -0
  63. package/script/src/commands/upload.d.ts.map +1 -0
  64. package/script/src/commands/upload.js +64 -0
  65. package/script/src/commands/videos.d.ts.map +1 -1
  66. package/script/src/commands/videos.js +114 -80
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +13 -2
  69. package/script/src/lib/auth.d.ts +23 -1
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +70 -3
  72. package/script/src/lib/moodle.d.ts +226 -2
  73. package/script/src/lib/moodle.d.ts.map +1 -1
  74. package/script/src/lib/moodle.js +663 -37
  75. package/script/src/lib/types.d.ts +82 -164
  76. package/script/src/lib/types.d.ts.map +1 -1
  77. package/script/src/lib/types.js +1 -0
  78. package/script/src/lib/utils.d.ts +40 -0
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +89 -3
  81. 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
- const result = {
79
+ console.log(JSON.stringify({
82
80
  status: "success",
83
81
  timestamp: new Date().toISOString(),
84
- forums: allForums,
85
- summary: {
86
- total_courses: courses.length,
87
- total_forums: allForums.length,
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
- url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
118
+ timemodified: wsForum.timemodified,
121
119
  });
122
120
  }
123
121
  }
124
- const result = {
122
+ console.log(JSON.stringify({
125
123
  status: "success",
126
124
  timestamp: new Date().toISOString(),
127
- forums: allForums,
128
- summary: {
129
- total_courses: courses.length,
130
- total_forums: allForums.length,
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
- const result = {
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
- discussions: discussions.map(d => {
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(result));
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: new Date(p.created * 1000).toISOString(),
215
- modified: new Date(p.modified * 1000).toISOString(),
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;AA+BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAka/D"}
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 token_js_1 = require("../lib/token.js");
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 baseDir = (0, utils_js_1.getBaseDir)();
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
- if (!node_fs_1.default.existsSync(courseDir)) {
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.statSync(outputPath);
144
- log.success(` Downloaded: ${filename} (${(stats.size / 1024).toFixed(1)} KB)`);
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;AAYpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuL7D"}
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"}