@mo7yw4ng/openape 1.0.4 → 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 (81) 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/auth.d.ts.map +1 -1
  8. package/esm/src/commands/auth.js +24 -14
  9. package/esm/src/commands/calendar.d.ts.map +1 -1
  10. package/esm/src/commands/calendar.js +32 -84
  11. package/esm/src/commands/courses.d.ts.map +1 -1
  12. package/esm/src/commands/courses.js +2 -38
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +47 -175
  15. package/esm/src/commands/grades.d.ts.map +1 -1
  16. package/esm/src/commands/grades.js +10 -47
  17. package/esm/src/commands/materials.d.ts.map +1 -1
  18. package/esm/src/commands/materials.js +135 -223
  19. package/esm/src/commands/quizzes.d.ts.map +1 -1
  20. package/esm/src/commands/quizzes.js +32 -56
  21. package/esm/src/commands/skills.js +3 -3
  22. package/esm/src/commands/upload.d.ts.map +1 -1
  23. package/esm/src/commands/upload.js +2 -5
  24. package/esm/src/commands/videos.d.ts.map +1 -1
  25. package/esm/src/commands/videos.js +6 -76
  26. package/esm/src/index.d.ts +2 -1
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +5 -1
  29. package/esm/src/lib/auth.d.ts +21 -2
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +79 -20
  32. package/esm/src/lib/logger.d.ts +2 -2
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +6 -4
  35. package/esm/src/lib/moodle.d.ts +18 -0
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +54 -2
  38. package/esm/src/lib/utils.d.ts +3 -8
  39. package/esm/src/lib/utils.d.ts.map +1 -1
  40. package/esm/src/lib/utils.js +3 -10
  41. package/package.json +1 -1
  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 +23 -89
  45. package/script/src/commands/assignments.d.ts.map +1 -1
  46. package/script/src/commands/assignments.js +2 -3
  47. package/script/src/commands/auth.d.ts.map +1 -1
  48. package/script/src/commands/auth.js +24 -14
  49. package/script/src/commands/calendar.d.ts.map +1 -1
  50. package/script/src/commands/calendar.js +33 -85
  51. package/script/src/commands/courses.d.ts.map +1 -1
  52. package/script/src/commands/courses.js +9 -48
  53. package/script/src/commands/forums.d.ts.map +1 -1
  54. package/script/src/commands/forums.js +50 -181
  55. package/script/src/commands/grades.d.ts.map +1 -1
  56. package/script/src/commands/grades.js +14 -54
  57. package/script/src/commands/materials.d.ts.map +1 -1
  58. package/script/src/commands/materials.js +132 -220
  59. package/script/src/commands/quizzes.d.ts.map +1 -1
  60. package/script/src/commands/quizzes.js +40 -67
  61. package/script/src/commands/skills.js +3 -3
  62. package/script/src/commands/upload.d.ts.map +1 -1
  63. package/script/src/commands/upload.js +2 -5
  64. package/script/src/commands/videos.d.ts.map +1 -1
  65. package/script/src/commands/videos.js +11 -81
  66. package/script/src/index.d.ts +2 -1
  67. package/script/src/index.d.ts.map +1 -1
  68. package/script/src/index.js +5 -1
  69. package/script/src/lib/auth.d.ts +21 -2
  70. package/script/src/lib/auth.d.ts.map +1 -1
  71. package/script/src/lib/auth.js +83 -56
  72. package/script/src/lib/logger.d.ts +2 -2
  73. package/script/src/lib/logger.d.ts.map +1 -1
  74. package/script/src/lib/logger.js +6 -4
  75. package/script/src/lib/moodle.d.ts +18 -0
  76. package/script/src/lib/moodle.d.ts.map +1 -1
  77. package/script/src/lib/moodle.js +56 -2
  78. package/script/src/lib/utils.d.ts +3 -8
  79. package/script/src/lib/utils.d.ts.map +1 -1
  80. package/script/src/lib/utils.js +3 -11
  81. package/skills/openape/SKILL.md +10 -8
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.4",
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);
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
- log.error("未找到登入 session。請先執行 'openape auth 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
- log.error("未找到 WS token。請先執行 'openape auth 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":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuRtD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/commands/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkStD"}
@@ -129,31 +129,41 @@ export function registerCommand(program) {
129
129
  catch {
130
130
  // Ignore sesskey extraction errors
131
131
  }
132
- // Try to acquire WS token
132
+ // Acquire WS token
133
+ let wsToken;
133
134
  try {
134
- const config = {
135
- username: "",
136
- password: "",
137
- courseUrl: "",
138
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
139
- headless: false,
140
- slowMo: 0,
141
- authStatePath: sessionPath,
142
- ollamaBaseUrl: "",
143
- };
144
- const wsToken = await acquireWsToken(page, config, log);
135
+ wsToken = await acquireWsToken(page, { moodleBaseUrl: "https://ilearning.cycu.edu.tw" }, log);
145
136
  saveWsToken(sessionPath, wsToken);
146
137
  }
147
138
  catch {
148
139
  // WS token is optional, ignore errors
149
140
  }
150
141
  const stats = fs.statSync(sessionPath);
142
+ // Get user info via WS API
143
+ let user;
144
+ try {
145
+ if (wsToken) {
146
+ const siteInfo = await getSiteInfoApi({
147
+ wsToken,
148
+ moodleBaseUrl: "https://ilearning.cycu.edu.tw",
149
+ });
150
+ user = {
151
+ userid: siteInfo.userid,
152
+ username: siteInfo.username,
153
+ fullname: siteInfo.fullname,
154
+ };
155
+ }
156
+ }
157
+ catch {
158
+ // Ignore
159
+ }
151
160
  const result = {
152
161
  status: "success",
153
162
  message: "Login successful",
154
163
  session_path: sessionPath,
155
164
  session_size: stats.size,
156
- updated: true
165
+ updated: true,
166
+ ...(user ? { user } : {}),
157
167
  };
158
168
  console.log(JSON.stringify(result, null, 2));
159
169
  }
@@ -247,7 +257,7 @@ export function registerCommand(program) {
247
257
  status: "error",
248
258
  error: "Session not found",
249
259
  session_path: sessionPath,
250
- hint: "Run 'openape auth login' first"
260
+ hint: "Run 'openape login' first"
251
261
  };
252
262
  console.log(JSON.stringify(result, null, 2));
253
263
  }
@@ -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);
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
- log.error("未找到登入 session。請先執行 'openape auth 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
- log.error("未找到 WS token。請先執行 'openape auth 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);
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
- log.error("未找到登入 session。請先執行 'openape auth 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
- log.error("未找到 WS token。請先執行 'openape auth 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"}