@mo7yw4ng/openape 1.0.5 → 1.0.6

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 (77) hide show
  1. package/README.md +11 -8
  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 +22 -85
  5. package/esm/src/commands/assignments.d.ts.map +1 -1
  6. package/esm/src/commands/assignments.js +2 -3
  7. package/esm/src/commands/calendar.d.ts.map +1 -1
  8. package/esm/src/commands/calendar.js +32 -84
  9. package/esm/src/commands/courses.d.ts.map +1 -1
  10. package/esm/src/commands/courses.js +2 -38
  11. package/esm/src/commands/forums.d.ts.map +1 -1
  12. package/esm/src/commands/forums.js +47 -175
  13. package/esm/src/commands/grades.d.ts.map +1 -1
  14. package/esm/src/commands/grades.js +10 -47
  15. package/esm/src/commands/materials.d.ts.map +1 -1
  16. package/esm/src/commands/materials.js +47 -58
  17. package/esm/src/commands/quizzes.d.ts.map +1 -1
  18. package/esm/src/commands/quizzes.js +2 -37
  19. package/esm/src/commands/skills.js +3 -3
  20. package/esm/src/commands/upload.d.ts.map +1 -1
  21. package/esm/src/commands/upload.js +2 -5
  22. package/esm/src/commands/videos.d.ts.map +1 -1
  23. package/esm/src/commands/videos.js +6 -76
  24. package/esm/src/index.d.ts +2 -1
  25. package/esm/src/index.d.ts.map +1 -1
  26. package/esm/src/index.js +5 -1
  27. package/esm/src/lib/auth.d.ts +21 -2
  28. package/esm/src/lib/auth.d.ts.map +1 -1
  29. package/esm/src/lib/auth.js +78 -19
  30. package/esm/src/lib/logger.d.ts +2 -2
  31. package/esm/src/lib/logger.d.ts.map +1 -1
  32. package/esm/src/lib/logger.js +1 -2
  33. package/esm/src/lib/moodle.d.ts +14 -0
  34. package/esm/src/lib/moodle.d.ts.map +1 -1
  35. package/esm/src/lib/moodle.js +35 -0
  36. package/esm/src/lib/utils.d.ts +3 -8
  37. package/esm/src/lib/utils.d.ts.map +1 -1
  38. package/esm/src/lib/utils.js +3 -10
  39. package/package.json +1 -1
  40. package/script/deno.js +1 -1
  41. package/script/src/commands/announcements.d.ts.map +1 -1
  42. package/script/src/commands/announcements.js +23 -89
  43. package/script/src/commands/assignments.d.ts.map +1 -1
  44. package/script/src/commands/assignments.js +2 -3
  45. package/script/src/commands/calendar.d.ts.map +1 -1
  46. package/script/src/commands/calendar.js +33 -85
  47. package/script/src/commands/courses.d.ts.map +1 -1
  48. package/script/src/commands/courses.js +9 -48
  49. package/script/src/commands/forums.d.ts.map +1 -1
  50. package/script/src/commands/forums.js +50 -181
  51. package/script/src/commands/grades.d.ts.map +1 -1
  52. package/script/src/commands/grades.js +14 -54
  53. package/script/src/commands/materials.d.ts.map +1 -1
  54. package/script/src/commands/materials.js +47 -58
  55. package/script/src/commands/quizzes.d.ts.map +1 -1
  56. package/script/src/commands/quizzes.js +11 -49
  57. package/script/src/commands/skills.js +3 -3
  58. package/script/src/commands/upload.d.ts.map +1 -1
  59. package/script/src/commands/upload.js +2 -5
  60. package/script/src/commands/videos.d.ts.map +1 -1
  61. package/script/src/commands/videos.js +11 -81
  62. package/script/src/index.d.ts +2 -1
  63. package/script/src/index.d.ts.map +1 -1
  64. package/script/src/index.js +5 -1
  65. package/script/src/lib/auth.d.ts +21 -2
  66. package/script/src/lib/auth.d.ts.map +1 -1
  67. package/script/src/lib/auth.js +83 -56
  68. package/script/src/lib/logger.d.ts +2 -2
  69. package/script/src/lib/logger.d.ts.map +1 -1
  70. package/script/src/lib/logger.js +1 -2
  71. package/script/src/lib/moodle.d.ts +14 -0
  72. package/script/src/lib/moodle.d.ts.map +1 -1
  73. package/script/src/lib/moodle.js +36 -0
  74. package/script/src/lib/utils.d.ts +3 -8
  75. package/script/src/lib/utils.d.ts.map +1 -1
  76. package/script/src/lib/utils.js +3 -11
  77. package/skills/openape/SKILL.md +6 -6
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  <h1 align="center">
2
2
  <img src="assets/logo.svg" width="150" /><br/>
3
- OpenApe CLI (Unofficial)
3
+ OpenApe CLI (Unofficial)<br/>
4
+ <a href="https://www.npmjs.com/package/@mo7yw4ng/openape"><img src="https://img.shields.io/npm/v/@mo7yw4ng/openape" alt="npm version" /></a>
5
+ <a href="https://jsr.io/@openape/openape"><img src="https://jsr.io/badges/@openape/openape" alt="JSR" /></a>
6
+ <a href="https://www.npmjs.com/package/@mo7yw4ng/openape"><img src="https://img.shields.io/npm/dm/@mo7yw4ng/openape" alt="npm downloads" /></a>
4
7
  </h1>
5
8
 
6
9
  中原大學 [i-Learning](https://ilearning.cycu.edu.tw/) (Moodle) 平台自動化命令列工具 (CLI),幫助你快速查詢課程、觀看影片、下載教材。
@@ -53,8 +56,8 @@ openape courses syllabus <id> # 顯示課程大綱
53
56
  ```bash
54
57
  openape videos list <course-id> # 列出課程中的影片
55
58
  openape videos complete <id> # 標記特定影片為已觀看
56
- openape videos complete-all <id> # 影片批次完成
57
- openape videos download <id> # 下載影片
59
+ openape videos complete-all # 影片批次完成
60
+ openape videos download <id> # 下載影片 (支援 --output-dir)
58
61
  ```
59
62
 
60
63
  ### 測驗與教材 (Quizzes & Materials)
@@ -65,8 +68,8 @@ openape quizzes start <quiz-id> # 開始測驗
65
68
  openape quizzes info <attempt-id> # 查看測驗題目
66
69
  openape quizzes save <attempt-id> '<answers>' # 儲存測驗答案
67
70
  openape materials list-all # 列出所有可下載教材
68
- openape materials download <id> # 下載指定教材
69
- openape materials download-all # 批次下載教材
71
+ openape materials download <id> # 下載指定教材 (支援 --output-dir)
72
+ openape materials download-all # 批次下載教材 (支援 --output-dir, --level)
70
73
  openape materials complete <id> # 標記教材為已完成
71
74
  openape materials complete-all # 批次標記教材為已完成
72
75
  ```
@@ -79,13 +82,13 @@ openape forums list # 列出進行中課程的討論區
79
82
  openape forums list-all # 列出所有討論區
80
83
  openape forums discussions <forum-id> # 列出討論區中的討論串
81
84
  openape forums posts <discussion-id> # 列出討論串中的貼文
82
- openape forums reply <post-id> <subject> <message> # 回覆貼文
85
+ openape forums reply <post-id> <subject> <message> # 回覆貼文 (支援 --attachment-id)
83
86
  openape forums post <forum-id> <subject> <message> # 發起新討論
84
87
  openape forums delete <post-id> # 刪除討論貼文
85
88
  openape announcements list-all # 列出所有公告
86
89
  openape announcements read <id> # 閱讀特定公告
87
- openape calendar events # 查詢行事曆事件
88
- openape calendar export # 匯出事件
90
+ openape calendar events # 查詢行事曆事件 (支援 --upcoming, --days, --course)
91
+ openape calendar export # 匯出事件 (支援 --output, --days)
89
92
  ```
90
93
 
91
94
  ### 作業與檔案上傳 (Assignments & Upload)
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@openape/openape",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "CLI tool for CYCU i-Learning platform",
5
5
  "license": "MIT",
6
6
  "exports": "./src/index.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoJnE"}
1
+ {"version":3,"file":"announcements.d.ts","sourceRoot":"","sources":["../../../src/src/commands/announcements.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuFnE"}
@@ -1,45 +1,10 @@
1
- import { getBaseDir, formatTimestamp } from "../lib/utils.js";
1
+ import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
2
  import { getSiteInfoApi, getMessagesApi, getDiscussionPostsApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
5
- import path from "node:path";
6
- import fs from "node:fs";
3
+ import { createApiContext } from "../lib/auth.js";
4
+ import { formatAndOutput } from "../index.js";
7
5
  export function registerAnnouncementsCommand(program) {
8
6
  const announcementsCmd = program.command("announcements");
9
7
  announcementsCmd.description("Announcement operations");
10
- // Helper to get output format from global or local options
11
- function getOutputFormat(command) {
12
- const opts = command.optsWithGlobals();
13
- return opts.output || "json";
14
- }
15
- // Pure API context - no browser required (fast!)
16
- async function createApiContext(options, command) {
17
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
- const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent, outputFormat);
21
- const baseDir = getBaseDir();
22
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
- // Check if session exists
24
- if (!fs.existsSync(sessionPath)) {
25
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
26
- log.info(`Session 預期位置: ${sessionPath}`);
27
- return null;
28
- }
29
- // Try to load WS token
30
- const wsToken = loadWsToken(sessionPath);
31
- if (!wsToken) {
32
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
33
- return null;
34
- }
35
- return {
36
- log,
37
- session: {
38
- wsToken,
39
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
40
- },
41
- };
42
- }
43
8
  announcementsCmd
44
9
  .command("list-all")
45
10
  .description("List all announcements across all courses")
@@ -48,55 +13,31 @@ export function registerAnnouncementsCommand(program) {
48
13
  .option("--limit <n>", "Maximum number of announcements to show", "20")
49
14
  .option("--output <format>", "Output format: json|csv|table|silent")
50
15
  .action(async (options, command) => {
16
+ const output = getOutputFormat(command);
51
17
  const limit = parseInt(options.limit, 10);
52
18
  const apiContext = await createApiContext(options, command);
53
19
  if (!apiContext) {
54
20
  process.exitCode = 1;
55
21
  return;
56
22
  }
57
- // Get site info to retrieve userid
58
23
  const siteInfo = await getSiteInfoApi(apiContext.session);
59
- // Get messages for the current user
60
24
  const messages = await getMessagesApi(apiContext.session, siteInfo.userid, {
61
25
  limitnum: limit,
62
26
  });
63
- // Convert messages to announcement format
64
27
  const allAnnouncements = messages.map(m => ({
65
- course_id: 0, // Messages don't have courseId
28
+ course_id: 0,
66
29
  course_name: "Notifications",
67
30
  id: m.id,
68
31
  subject: m.subject,
69
32
  author: `User ${m.useridfrom}`,
70
- authorId: m.useridfrom,
71
- createdAt: m.timecreated,
72
- modifiedAt: m.timecreated,
73
- unread: false, // Messages API doesn't provide unread status
74
- forumId: 0,
33
+ author_id: m.useridfrom,
34
+ created_at: formatTimestamp(m.timecreated),
35
+ modified_at: formatTimestamp(m.timecreated),
36
+ unread: false,
75
37
  }));
76
- // Sort by created date (newest first)
77
- allAnnouncements.sort((a, b) => b.createdAt - a.createdAt);
78
- // Apply limit
79
- let filteredAnnouncements = allAnnouncements.slice(0, limit);
80
- console.log(JSON.stringify({
81
- status: "success",
82
- timestamp: new Date().toISOString(),
83
- level: options.level,
84
- total_announcements: allAnnouncements.length,
85
- shown: filteredAnnouncements.length,
86
- }));
87
- for (const a of filteredAnnouncements) {
88
- console.log(JSON.stringify({
89
- course_id: a.course_id,
90
- course_name: a.course_name,
91
- id: a.id,
92
- subject: a.subject,
93
- author: a.author,
94
- author_id: a.authorId,
95
- created_at: formatTimestamp(a.createdAt),
96
- modified_at: formatTimestamp(a.modifiedAt),
97
- unread: a.unread,
98
- }));
99
- }
38
+ allAnnouncements.sort((a, b) => b.created_at > a.created_at ? 1 : -1);
39
+ const shown = allAnnouncements.slice(0, limit);
40
+ formatAndOutput(shown, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_announcements: allAnnouncements.length, shown: shown.length });
100
41
  });
101
42
  announcementsCmd
102
43
  .command("read")
@@ -104,6 +45,7 @@ export function registerAnnouncementsCommand(program) {
104
45
  .argument("<announcement-id>", "Discussion ID of the announcement")
105
46
  .option("--output <format>", "Output format: json|csv|table|silent")
106
47
  .action(async (announcementId, options, command) => {
48
+ const output = getOutputFormat(command);
107
49
  const apiContext = await createApiContext(options, command);
108
50
  if (!apiContext) {
109
51
  process.exitCode = 1;
@@ -116,19 +58,14 @@ export function registerAnnouncementsCommand(program) {
116
58
  return;
117
59
  }
118
60
  const firstPost = posts[0];
119
- const output = {
120
- status: "success",
121
- timestamp: new Date().toISOString(),
122
- announcement: {
123
- id: announcementId,
124
- subject: firstPost.subject,
125
- author: firstPost.author,
126
- author_id: firstPost.authorId,
127
- created_at: formatTimestamp(firstPost.created),
128
- modified_at: formatTimestamp(firstPost.modified),
129
- message: firstPost.message,
130
- },
131
- };
132
- console.log(JSON.stringify(output));
61
+ formatAndOutput({
62
+ id: announcementId,
63
+ subject: firstPost.subject,
64
+ author: firstPost.author,
65
+ author_id: firstPost.authorId,
66
+ created_at: formatTimestamp(firstPost.created),
67
+ modified_at: formatTimestamp(firstPost.modified),
68
+ message: firstPost.message,
69
+ }, output, apiContext.log);
133
70
  });
134
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/commands/assignments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0PjE"}
1
+ {"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../../../src/src/commands/assignments.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyPjE"}
@@ -158,9 +158,9 @@ export function registerAssignmentsCommand(program) {
158
158
  // Upload file if --file option is provided
159
159
  if (options.file) {
160
160
  const resolvedPath = path.resolve(options.file);
161
- // Check if file exists
161
+ let stats;
162
162
  try {
163
- await fs.access(resolvedPath);
163
+ stats = await fs.stat(resolvedPath);
164
164
  }
165
165
  catch {
166
166
  const errorResult = {
@@ -171,7 +171,6 @@ export function registerAssignmentsCommand(program) {
171
171
  process.exitCode = 1;
172
172
  return;
173
173
  }
174
- const stats = await fs.stat(resolvedPath);
175
174
  const fileSizeKB = formatFileSize(stats.size);
176
175
  const uploadResult = await uploadFileApi(apiContext.session, resolvedPath);
177
176
  if (!uploadResult.success) {
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmM9D"}
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../../../src/src/commands/calendar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqJ9D"}
@@ -1,45 +1,11 @@
1
- import { getBaseDir, formatTimestamp } from "../lib/utils.js";
1
+ import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
2
  import { getEnrolledCoursesApi, getCalendarEventsApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
5
- import path from "node:path";
3
+ import { createApiContext } from "../lib/auth.js";
4
+ import { formatAndOutput } from "../index.js";
6
5
  import fs from "node:fs";
7
6
  export function registerCalendarCommand(program) {
8
7
  const calendarCmd = program.command("calendar");
9
8
  calendarCmd.description("Calendar operations");
10
- // Helper to get output format from global or local options
11
- function getOutputFormat(command) {
12
- const opts = command.optsWithGlobals();
13
- return opts.output || "json";
14
- }
15
- // Pure API context - no browser required (fast!)
16
- async function createApiContext(options, command) {
17
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
18
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
19
- const silent = outputFormat === "json" && !opts.verbose;
20
- const log = createLogger(opts.verbose, silent, outputFormat);
21
- const baseDir = getBaseDir();
22
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
23
- // Check if session exists
24
- if (!fs.existsSync(sessionPath)) {
25
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
26
- log.info(`Session 預期位置: ${sessionPath}`);
27
- return null;
28
- }
29
- // Try to load WS token
30
- const wsToken = loadWsToken(sessionPath);
31
- if (!wsToken) {
32
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
33
- return null;
34
- }
35
- return {
36
- log,
37
- session: {
38
- wsToken,
39
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
40
- },
41
- };
42
- }
43
9
  calendarCmd
44
10
  .command("events")
45
11
  .description("List calendar events")
@@ -48,6 +14,7 @@ export function registerCalendarCommand(program) {
48
14
  .option("--course <id>", "Filter by course ID")
49
15
  .option("--output <format>", "Output format: json|csv|table|silent")
50
16
  .action(async (options, command) => {
17
+ const output = getOutputFormat(command);
51
18
  const days = parseInt(options.days, 10);
52
19
  const apiContext = await createApiContext(options, command);
53
20
  if (!apiContext) {
@@ -55,12 +22,10 @@ export function registerCalendarCommand(program) {
55
22
  return;
56
23
  }
57
24
  const courses = await getEnrolledCoursesApi(apiContext.session);
58
- // Calculate time range
59
25
  const now = Math.floor(Date.now() / 1000);
60
26
  const endTime = now + (days * 24 * 60 * 60);
61
27
  let allEvents = [];
62
28
  if (options.course) {
63
- // Get events for specific course
64
29
  const courseId = parseInt(options.course, 10);
65
30
  const events = await getCalendarEventsApi(apiContext.session, {
66
31
  startTime: now,
@@ -69,29 +34,32 @@ export function registerCalendarCommand(program) {
69
34
  allEvents = events.filter(e => e.courseid === courseId);
70
35
  }
71
36
  else {
72
- // Get events for all courses
73
- for (const course of courses) {
74
- try {
75
- const events = await getCalendarEventsApi(apiContext.session, {
76
- courseId: course.id,
77
- startTime: now,
78
- endTime: endTime,
79
- });
80
- allEvents.push(...events);
81
- }
82
- catch (err) {
83
- apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
84
- }
37
+ const results = await Promise.allSettled(courses.map(course => getCalendarEventsApi(apiContext.session, {
38
+ courseId: course.id,
39
+ startTime: now,
40
+ endTime: endTime,
41
+ })));
42
+ for (const result of results) {
43
+ if (result.status === "fulfilled")
44
+ allEvents.push(...result.value);
85
45
  }
86
46
  }
87
- // Sort by start time
88
47
  allEvents.sort((a, b) => a.timestart - b.timestart);
89
- // Filter upcoming only if requested
90
48
  let filteredEvents = allEvents;
91
49
  if (options.upcoming) {
92
50
  filteredEvents = allEvents.filter(e => e.timestart > now);
93
51
  }
94
- console.log(JSON.stringify({
52
+ const items = filteredEvents.map(e => ({
53
+ id: e.id,
54
+ name: e.name,
55
+ description: e.description,
56
+ course_id: e.courseid,
57
+ event_type: e.eventtype,
58
+ start_time: formatTimestamp(e.timestart),
59
+ end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
60
+ location: e.location,
61
+ }));
62
+ formatAndOutput(items, output, apiContext.log, {
95
63
  status: "success",
96
64
  timestamp: new Date().toISOString(),
97
65
  total_events: allEvents.length,
@@ -100,19 +68,7 @@ export function registerCalendarCommand(program) {
100
68
  acc[e.eventtype] = (acc[e.eventtype] || 0) + 1;
101
69
  return acc;
102
70
  }, {}),
103
- }));
104
- for (const e of filteredEvents) {
105
- console.log(JSON.stringify({
106
- id: e.id,
107
- name: e.name,
108
- description: e.description,
109
- course_id: e.courseid,
110
- event_type: e.eventtype,
111
- start_time: formatTimestamp(e.timestart),
112
- end_time: e.timeduration ? formatTimestamp(e.timestart + Math.floor(e.timeduration / 1000)) : null,
113
- location: e.location,
114
- }));
115
- }
71
+ });
116
72
  });
117
73
  calendarCmd
118
74
  .command("export")
@@ -126,27 +82,20 @@ export function registerCalendarCommand(program) {
126
82
  return;
127
83
  }
128
84
  const courses = await getEnrolledCoursesApi(apiContext.session);
129
- // Calculate time range
130
85
  const now = Math.floor(Date.now() / 1000);
131
86
  const days = parseInt(options.days, 10);
132
87
  const endTime = now + (days * 24 * 60 * 60);
133
88
  const allEvents = [];
134
- for (const course of courses) {
135
- try {
136
- const events = await getCalendarEventsApi(apiContext.session, {
137
- courseId: course.id,
138
- startTime: now,
139
- endTime: endTime,
140
- });
141
- allEvents.push(...events);
142
- }
143
- catch (err) {
144
- apiContext.log.debug(`Failed to fetch calendar events for ${course.fullname}: ${err}`);
145
- }
89
+ const results = await Promise.allSettled(courses.map(course => getCalendarEventsApi(apiContext.session, {
90
+ courseId: course.id,
91
+ startTime: now,
92
+ endTime: endTime,
93
+ })));
94
+ for (const result of results) {
95
+ if (result.status === "fulfilled")
96
+ allEvents.push(...result.value);
146
97
  }
147
- // Sort by start time
148
98
  allEvents.sort((a, b) => a.timestart - b.timestart);
149
- // Export data
150
99
  const exportData = {
151
100
  exported_at: new Date().toISOString(),
152
101
  time_range: {
@@ -172,7 +121,6 @@ export function registerCalendarCommand(program) {
172
121
  }, {}),
173
122
  },
174
123
  };
175
- // Write to file
176
124
  fs.writeFileSync(options.output, JSON.stringify(exportData));
177
125
  apiContext.log.success(`Exported ${allEvents.length} events to ${options.output}`);
178
126
  });
@@ -1 +1 @@
1
- {"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/src/commands/courses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4Y7D"}
1
+ {"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/src/commands/courses.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkW7D"}
@@ -1,46 +1,10 @@
1
- import { getBaseDir, formatTimestamp } from "../lib/utils.js";
1
+ import { getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
2
  import { getEnrolledCoursesApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
3
+ import { createApiContext } from "../lib/auth.js";
5
4
  import { formatAndOutput } from "../index.js";
6
- import path from "node:path";
7
- import fs from "node:fs";
8
5
  export function registerCoursesCommand(program) {
9
6
  const coursesCmd = program.command("courses");
10
7
  coursesCmd.description("Course operations");
11
- // Helper to get output format from global or local options
12
- function getOutputFormat(command) {
13
- const opts = command.optsWithGlobals();
14
- return opts.output || "json";
15
- }
16
- // Pure API context - no browser required (fast!)
17
- async function createApiContext(options, command) {
18
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
19
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
20
- const silent = outputFormat === "json" && !opts.verbose;
21
- const log = createLogger(opts.verbose, silent, outputFormat);
22
- const baseDir = getBaseDir();
23
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
24
- // Check if session exists
25
- if (!fs.existsSync(sessionPath)) {
26
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
27
- log.info(`Session 預期位置: ${sessionPath}`);
28
- return null;
29
- }
30
- // Try to load WS token
31
- const wsToken = loadWsToken(sessionPath);
32
- if (!wsToken) {
33
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
34
- return null;
35
- }
36
- return {
37
- log,
38
- session: {
39
- wsToken,
40
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
41
- },
42
- };
43
- }
44
8
  coursesCmd
45
9
  .command("list")
46
10
  .description("List enrolled courses")
@@ -1 +1 @@
1
- {"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkX5D"}
1
+ {"version":3,"file":"forums.d.ts","sourceRoot":"","sources":["../../../src/src/commands/forums.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+O5D"}