@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
@@ -1,62 +1,25 @@
1
- import { getBaseDir, stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
- import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken, loadSesskey } from "../lib/token.js";
5
- import path from "node:path";
6
- import fs from "node:fs";
1
+ import { stripHtmlTags, getOutputFormat, formatTimestamp } from "../lib/utils.js";
2
+ import { getEnrolledCoursesApi, getForumsApi, getForumDiscussionsApi, getDiscussionPostsApi, addForumDiscussionApi, addForumPostApi, deleteForumPostApi, resolveForumId } from "../lib/moodle.js";
3
+ import { createApiContext } from "../lib/auth.js";
4
+ import { formatAndOutput } from "../index.js";
7
5
  export function registerForumsCommand(program) {
8
6
  const forumsCmd = program.command("forums");
9
7
  forumsCmd.description("Forum operations");
10
- // Pure API context - no browser required (fast!)
11
- async function createApiContext(options, command) {
12
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
13
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
14
- const silent = outputFormat === "json" && !opts.verbose;
15
- const log = createLogger(opts.verbose, silent, outputFormat);
16
- const baseDir = getBaseDir();
17
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
18
- // Check if session exists
19
- if (!fs.existsSync(sessionPath)) {
20
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
21
- log.info(`Session 預期位置: ${sessionPath}`);
22
- return null;
23
- }
24
- // Try to load WS token
25
- const wsToken = loadWsToken(sessionPath);
26
- if (!wsToken) {
27
- console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
28
- return null;
29
- }
30
- // Try to load sesskey from cache
31
- const sesskey = loadSesskey(sessionPath) || undefined;
32
- return {
33
- log,
34
- session: {
35
- wsToken,
36
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
37
- sesskey,
38
- },
39
- };
40
- }
41
- forumsCmd
42
- .command("list")
43
- .description("List forums from in-progress courses")
44
- .option("--output <format>", "Output format: json|csv|table|silent")
45
- .action(async (options, command) => {
46
- const apiContext = await createApiContext(options, command);
8
+ async function listForums(classification) {
9
+ const apiContext = await createApiContext({});
47
10
  if (!apiContext) {
48
11
  process.exitCode = 1;
49
12
  return;
50
13
  }
51
14
  const courses = await getEnrolledCoursesApi(apiContext.session, {
52
- classification: "inprogress",
15
+ classification,
53
16
  });
54
- // Get forums via WS API (no browser needed!)
55
17
  const courseIds = courses.map(c => c.id);
56
18
  const wsForums = await getForumsApi(apiContext.session, courseIds);
19
+ const courseMap = new Map(courses.map(c => [c.id, c]));
57
20
  const allForums = [];
58
21
  for (const wsForum of wsForums) {
59
- const course = courses.find(c => c.id === wsForum.courseid);
22
+ const course = courseMap.get(wsForum.courseid);
60
23
  if (course) {
61
24
  allForums.push({
62
25
  course_id: wsForum.courseid,
@@ -66,62 +29,22 @@ export function registerForumsCommand(program) {
66
29
  forum_id: wsForum.id,
67
30
  name: wsForum.name,
68
31
  timemodified: wsForum.timemodified,
69
- // url: `https://ilearning.cycu.edu.tw/mod/forum/view.php?id=${wsForum.cmid}`,
70
32
  });
71
33
  }
72
34
  }
73
- console.log(JSON.stringify({
74
- status: "success",
75
- timestamp: new Date().toISOString(),
76
- total_courses: courses.length,
77
- total_forums: allForums.length,
78
- }));
79
- for (const forum of allForums) {
80
- console.log(JSON.stringify(forum));
81
- }
82
- });
35
+ formatAndOutput(allForums, "json", apiContext.log, { status: "success", timestamp: new Date().toISOString(), total_courses: courses.length, total_forums: allForums.length });
36
+ }
37
+ forumsCmd
38
+ .command("list")
39
+ .description("List forums from in-progress courses")
40
+ .action(() => listForums("inprogress"));
83
41
  forumsCmd
84
42
  .command("list-all")
85
43
  .description("List all forums across all courses")
86
44
  .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
87
- .option("--output <format>", "Output format: json|csv|table|silent")
88
- .action(async (options, command) => {
89
- const apiContext = await createApiContext(options, command);
90
- if (!apiContext) {
91
- process.exitCode = 1;
92
- return;
93
- }
45
+ .action(async (options) => {
94
46
  const classification = options.level === "all" ? undefined : "inprogress";
95
- const courses = await getEnrolledCoursesApi(apiContext.session, {
96
- classification,
97
- });
98
- // Get forums via WS API (no browser needed!)
99
- const courseIds = courses.map(c => c.id);
100
- const wsForums = await getForumsApi(apiContext.session, courseIds);
101
- const allForums = [];
102
- for (const wsForum of wsForums) {
103
- const course = courses.find(c => c.id === wsForum.courseid);
104
- if (course) {
105
- allForums.push({
106
- course_id: wsForum.courseid,
107
- course_name: course.fullname,
108
- intro: wsForum.intro,
109
- cmid: wsForum.cmid.toString(),
110
- forum_id: wsForum.id,
111
- name: wsForum.name,
112
- timemodified: wsForum.timemodified,
113
- });
114
- }
115
- }
116
- console.log(JSON.stringify({
117
- status: "success",
118
- timestamp: new Date().toISOString(),
119
- total_courses: courses.length,
120
- total_forums: allForums.length,
121
- }));
122
- for (const forum of allForums) {
123
- console.log(JSON.stringify(forum));
124
- }
47
+ await listForums(classification);
125
48
  });
126
49
  forumsCmd
127
50
  .command("discussions")
@@ -129,51 +52,29 @@ export function registerForumsCommand(program) {
129
52
  .argument("<forum-id>", "Forum ID")
130
53
  .option("--output <format>", "Output format: json|csv|table|silent")
131
54
  .action(async (forumId, options, command) => {
55
+ const output = getOutputFormat(command);
132
56
  const apiContext = await createApiContext(options, command);
133
57
  if (!apiContext) {
134
58
  process.exitCode = 1;
135
59
  return;
136
60
  }
137
- // Get courses via WS API
138
- const courses = await getEnrolledCoursesApi(apiContext.session, {
139
- classification: "inprogress",
140
- });
141
- // Get forums via WS API
142
- const courseIds = courses.map(c => c.id);
143
- const wsForums = await getForumsApi(apiContext.session, courseIds);
144
- // Find forum by cmid or instance ID
145
- const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
146
- if (!targetForum) {
147
- console.log(JSON.stringify({ status: "error", error: "Forum not found" }));
61
+ const resolved = await resolveForumId(apiContext.session, forumId);
62
+ if (!resolved) {
63
+ apiContext.log.error("Forum not found");
148
64
  process.exitCode = 1;
149
65
  return;
150
66
  }
151
- const course = courses.find(c => c.id === targetForum.courseid);
152
- // Get discussions via WS API
153
- const discussions = await getForumDiscussionsApi(apiContext.session, targetForum.id);
154
- // Output NDJSON: one line per discussion entry for stream-friendly parsing
155
- const meta = {
156
- status: "success",
157
- timestamp: new Date().toISOString(),
158
- forum_id: targetForum.id,
159
- forum_name: targetForum.name,
160
- forum_intro: targetForum.intro,
161
- course_id: course?.id,
162
- course_name: course?.fullname,
163
- total_discussions: discussions.length,
164
- };
165
- console.log(JSON.stringify(meta));
166
- for (const d of discussions) {
167
- console.log(JSON.stringify({
168
- id: d.id,
169
- name: d.name,
170
- user_id: d.userId,
171
- time_modified: d.timeModified,
172
- post_count: d.postCount,
173
- unread: d.unread,
174
- message: stripHtmlTags(d.message || ""),
175
- }));
176
- }
67
+ const discussions = await getForumDiscussionsApi(apiContext.session, resolved.forumId);
68
+ const items = discussions.map(d => ({
69
+ id: d.id,
70
+ name: d.name,
71
+ user_id: d.userId,
72
+ time_modified: d.timeModified,
73
+ post_count: d.postCount,
74
+ unread: d.unread,
75
+ message: stripHtmlTags(d.message || ""),
76
+ }));
77
+ formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), forum_id: resolved.forumId, forum_name: resolved.name ?? null, course_id: resolved.courseid ?? null, total_discussions: discussions.length });
177
78
  });
178
79
  forumsCmd
179
80
  .command("posts")
@@ -188,38 +89,17 @@ export function registerForumsCommand(program) {
188
89
  return;
189
90
  }
190
91
  const posts = await getDiscussionPostsApi(apiContext.session, parseInt(discussionId, 10));
191
- if (output === "json") {
192
- const result = {
193
- status: "success",
194
- timestamp: new Date().toISOString(),
195
- discussion_id: discussionId,
196
- posts: posts.map(p => ({
197
- id: p.id,
198
- subject: p.subject,
199
- author: p.author,
200
- author_id: p.authorId,
201
- created: formatTimestamp(p.created),
202
- modified: formatTimestamp(p.modified),
203
- message: p.message,
204
- unread: p.unread,
205
- })),
206
- summary: {
207
- total_posts: posts.length,
208
- },
209
- };
210
- console.log(JSON.stringify(result));
211
- }
212
- else if (output === "table") {
213
- console.log(`Discussion ${discussionId} - ${posts.length} posts`);
214
- console.log("Use --output json to see full post content");
215
- const tablePosts = posts.map(p => ({
216
- id: p.id,
217
- subject: p.subject.substring(0, 50) + (p.subject.length > 50 ? "..." : ""),
218
- author: p.author,
219
- created: new Date(p.created * 1000).toLocaleString(),
220
- }));
221
- console.table(tablePosts);
222
- }
92
+ const items = posts.map(p => ({
93
+ id: p.id,
94
+ subject: p.subject,
95
+ author: p.author,
96
+ author_id: p.authorId,
97
+ created: formatTimestamp(p.created),
98
+ modified: formatTimestamp(p.modified),
99
+ message: p.message,
100
+ unread: p.unread,
101
+ }));
102
+ formatAndOutput(items, output, apiContext.log, { status: "success", timestamp: new Date().toISOString(), discussion_id: discussionId, total_posts: posts.length });
223
103
  });
224
104
  forumsCmd
225
105
  .command("post")
@@ -236,22 +116,14 @@ export function registerForumsCommand(program) {
236
116
  return;
237
117
  }
238
118
  const { log, session } = apiContext;
239
- // Get courses to find the forum
240
- const courses = await getEnrolledCoursesApi(session, {
241
- classification: "inprogress",
242
- });
243
- const courseIds = courses.map(c => c.id);
244
- const wsForums = await getForumsApi(session, courseIds);
245
- // Find forum by cmid or instance ID
246
- const targetForum = wsForums.find(f => f.cmid.toString() === forumId || f.id === parseInt(forumId, 10));
247
- if (!targetForum) {
119
+ const resolved = await resolveForumId(session, forumId);
120
+ if (!resolved) {
248
121
  log.error(`Forum not found: ${forumId}`);
249
122
  process.exitCode = 1;
250
123
  return;
251
124
  }
252
- const course = courses.find(c => c.id === targetForum.courseid);
253
- log.info(`Posting to forum: ${targetForum.name} (${course?.fullname})`);
254
- const result = await addForumDiscussionApi(session, targetForum.id, subject, message);
125
+ log.info(`Posting to forum: ${resolved.name ?? forumId}`);
126
+ const result = await addForumDiscussionApi(session, resolved.forumId, subject, message);
255
127
  if (result.success) {
256
128
  log.success(`✓ Discussion posted successfully!`);
257
129
  log.info(` Discussion ID: ${result.discussionId}`);
@@ -1 +1 @@
1
- {"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsI5D"}
1
+ {"version":3,"file":"grades.d.ts","sourceRoot":"","sources":["../../../src/src/commands/grades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+F5D"}
@@ -1,45 +1,10 @@
1
- import { getBaseDir } from "../lib/utils.js";
2
1
  import { getEnrolledCoursesApi, getCourseGradesApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { loadWsToken } from "../lib/token.js";
2
+ import { createApiContext } from "../lib/auth.js";
5
3
  import { formatAndOutput } from "../index.js";
6
- import path from "node:path";
7
- import fs from "node:fs";
4
+ import { getOutputFormat } from "../lib/utils.js";
8
5
  export function registerGradesCommand(program) {
9
6
  const gradesCmd = program.command("grades");
10
7
  gradesCmd.description("Grade operations");
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
  gradesCmd
44
9
  .command("summary")
45
10
  .description("Show grade summary across all courses")
@@ -52,9 +17,13 @@ export function registerGradesCommand(program) {
52
17
  return;
53
18
  }
54
19
  const courses = await getEnrolledCoursesApi(apiContext.session);
20
+ const gradeResults = await Promise.allSettled(courses.map(course => getCourseGradesApi(apiContext.session, course.id)
21
+ .then(grades => ({ course, grades }))));
55
22
  const gradeSummaries = [];
56
- for (const course of courses) {
57
- const grades = await getCourseGradesApi(apiContext.session, course.id);
23
+ for (const result of gradeResults) {
24
+ if (result.status !== "fulfilled")
25
+ continue;
26
+ const { course, grades } = result.value;
58
27
  gradeSummaries.push({
59
28
  courseId: course.id,
60
29
  courseName: course.fullname,
@@ -64,19 +33,13 @@ export function registerGradesCommand(program) {
64
33
  totalUsers: grades.totalUsers,
65
34
  });
66
35
  }
67
- // Calculate overall statistics
68
36
  const gradedCourses = gradeSummaries.filter(g => g.grade !== undefined && g.grade !== null && g.grade !== "-");
69
37
  const averageRank = gradeSummaries
70
38
  .filter(g => g.rank !== undefined && g.rank !== null)
71
39
  .reduce((sum, g) => sum + (g.rank || 0), 0) /
72
40
  (gradeSummaries.filter(g => g.rank !== undefined && g.rank !== null).length || 1);
73
- const summaryData = {
74
- total_courses: courses.length,
75
- graded_courses: gradedCourses.length,
76
- average_rank: averageRank.toFixed(1),
77
- grades: gradeSummaries,
78
- };
79
- formatAndOutput(summaryData, output, apiContext.log);
41
+ apiContext.log.info(`Total: ${courses.length} courses, ${gradedCourses.length} graded, avg rank: ${averageRank.toFixed(1)}`);
42
+ formatAndOutput(gradeSummaries, output, apiContext.log);
80
43
  });
81
44
  gradesCmd
82
45
  .command("course")
@@ -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;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAid/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;AA4BpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwd/D"}
@@ -91,6 +91,7 @@ export function registerMaterialsCommand(program) {
91
91
  .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
92
92
  .option("--output <format>", "Output format: json|csv|table|silent")
93
93
  .action(async (options, command) => {
94
+ const output = getOutputFormat(command);
94
95
  const apiContext = await createApiContext(options, command);
95
96
  if (!apiContext) {
96
97
  process.exitCode = 1;
@@ -100,10 +101,8 @@ export function registerMaterialsCommand(program) {
100
101
  const courses = await getEnrolledCoursesApi(apiContext.session, {
101
102
  classification,
102
103
  });
103
- // Get materials via WS API (no browser needed!)
104
104
  const courseIds = courses.map(c => c.id);
105
105
  const apiResources = await getResourcesByCoursesApi(apiContext.session, courseIds);
106
- // Build a map of courseId -> course for quick lookup
107
106
  const courseMap = new Map(courses.map(c => [c.id, c]));
108
107
  const allMaterials = [];
109
108
  for (const resource of apiResources) {
@@ -122,31 +121,27 @@ export function registerMaterialsCommand(program) {
122
121
  });
123
122
  }
124
123
  }
125
- const output = {
124
+ const items = allMaterials.map(m => ({
125
+ course_id: m.course_id,
126
+ course_name: m.course_name,
127
+ id: m.cmid,
128
+ name: m.name,
129
+ type: m.modType,
130
+ mimetype: m.mimetype,
131
+ filesize: m.filesize,
132
+ modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
133
+ url: m.url,
134
+ }));
135
+ formatAndOutput(items, output, apiContext.log, {
126
136
  status: "success",
127
137
  timestamp: new Date().toISOString(),
128
- level: options.level,
129
- materials: allMaterials.map(m => ({
130
- course_id: m.course_id,
131
- course_name: m.course_name,
132
- id: m.cmid,
133
- name: m.name,
134
- type: m.modType,
135
- mimetype: m.mimetype,
136
- filesize: m.filesize,
137
- modified: m.modified ? new Date(m.modified * 1000).toISOString() : null,
138
- url: m.url,
139
- })),
140
- summary: {
141
- total_courses: courses.length,
142
- total_materials: allMaterials.length,
143
- by_type: allMaterials.reduce((acc, m) => {
144
- acc[m.modType] = (acc[m.modType] || 0) + 1;
145
- return acc;
146
- }, {}),
147
- },
148
- };
149
- console.log(JSON.stringify(output));
138
+ total_courses: courses.length,
139
+ total_materials: allMaterials.length,
140
+ by_type: allMaterials.reduce((acc, m) => {
141
+ acc[m.modType] = (acc[m.modType] || 0) + 1;
142
+ return acc;
143
+ }, {}),
144
+ });
150
145
  });
151
146
  materialsCmd
152
147
  .command("download")
@@ -177,24 +172,21 @@ export function registerMaterialsCommand(program) {
177
172
  downloadedFiles.push(result);
178
173
  }
179
174
  }
180
- const output = {
175
+ const items = downloadedFiles.map(f => ({
176
+ filename: f.filename,
177
+ path: f.path,
178
+ size: f.size,
179
+ course_id: f.course_id,
180
+ course_name: f.course_name,
181
+ }));
182
+ formatAndOutput(items, "json", log, {
181
183
  status: "success",
182
184
  timestamp: new Date().toISOString(),
183
- downloaded_files: downloadedFiles.map(f => ({
184
- filename: f.filename,
185
- path: f.path,
186
- size: f.size,
187
- course_id: f.course_id,
188
- course_name: f.course_name,
189
- })),
190
- summary: {
191
- total_materials: materials.length,
192
- downloaded: downloadedFiles.length,
193
- skipped: materials.length - downloadedFiles.length,
194
- total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
195
- },
196
- };
197
- console.log(JSON.stringify(output));
185
+ total_materials: materials.length,
186
+ downloaded: downloadedFiles.length,
187
+ skipped: materials.length - downloadedFiles.length,
188
+ total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
189
+ });
198
190
  });
199
191
  materialsCmd
200
192
  .command("download-all")
@@ -222,25 +214,22 @@ export function registerMaterialsCommand(program) {
222
214
  downloadedFiles.push(result);
223
215
  }
224
216
  }
225
- const output = {
217
+ const items = downloadedFiles.map(f => ({
218
+ filename: f.filename,
219
+ path: f.path,
220
+ size: f.size,
221
+ course_id: f.course_id,
222
+ course_name: f.course_name,
223
+ }));
224
+ formatAndOutput(items, "json", log, {
226
225
  status: "success",
227
226
  timestamp: new Date().toISOString(),
228
- downloaded_files: downloadedFiles.map(f => ({
229
- filename: f.filename,
230
- path: f.path,
231
- size: f.size,
232
- course_id: f.course_id,
233
- course_name: f.course_name,
234
- })),
235
- summary: {
236
- total_courses: courses.length,
237
- total_materials: allMaterials.length,
238
- downloaded: downloadedFiles.length,
239
- skipped: allMaterials.length - downloadedFiles.length,
240
- total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
241
- },
242
- };
243
- console.log(JSON.stringify(output));
227
+ total_courses: courses.length,
228
+ total_materials: allMaterials.length,
229
+ downloaded: downloadedFiles.length,
230
+ skipped: allMaterials.length - downloadedFiles.length,
231
+ total_size: downloadedFiles.reduce((sum, f) => sum + f.size, 0),
232
+ });
244
233
  });
245
234
  materialsCmd
246
235
  .command("complete")
@@ -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;AAyEpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoQ7D"}
1
+ {"version":3,"file":"quizzes.d.ts","sourceRoot":"","sources":["../../../src/src/commands/quizzes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsEpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2N7D"}
@@ -1,10 +1,7 @@
1
- import { getBaseDir, formatTimestamp } from "../lib/utils.js";
1
+ import { formatTimestamp, getOutputFormat } from "../lib/utils.js";
2
2
  import { getEnrolledCoursesApi, getQuizzesByCoursesApi, startQuizAttemptApi, getQuizAttemptDataApi, getAllQuizAttemptDataApi, processQuizAttemptApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
3
+ import { createApiContext } from "../lib/auth.js";
4
4
  import { formatAndOutput } from "../index.js";
5
- import { loadWsToken } from "../lib/token.js";
6
- import path from "node:path";
7
- import fs from "node:fs";
8
5
  function stripHtmlKeepLines(html) {
9
6
  return html
10
7
  .replace(/<br\s*\/?>/gi, "\n")
@@ -59,38 +56,6 @@ function parseQuizQuestions(questions) {
59
56
  export function registerQuizzesCommand(program) {
60
57
  const quizzesCmd = program.command("quizzes");
61
58
  quizzesCmd.description("Quiz operations");
62
- function getOutputFormat(command) {
63
- const opts = command.optsWithGlobals();
64
- return opts.output || "json";
65
- }
66
- // Pure API context - no browser required (fast!)
67
- async function createApiContext(options, command) {
68
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
69
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
70
- const silent = outputFormat === "json" && !opts.verbose;
71
- const log = createLogger(opts.verbose, silent, outputFormat);
72
- const baseDir = getBaseDir();
73
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
74
- // Check if session exists
75
- if (!fs.existsSync(sessionPath)) {
76
- log.error("未找到登入 session。請先執行 'openape login' 進行登入。");
77
- log.info(`Session 預期位置: ${sessionPath}`);
78
- return null;
79
- }
80
- // Try to load WS token
81
- const wsToken = loadWsToken(sessionPath);
82
- if (!wsToken) {
83
- log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
84
- return null;
85
- }
86
- return {
87
- log,
88
- session: {
89
- wsToken,
90
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
91
- },
92
- };
93
- }
94
59
  quizzesCmd
95
60
  .command("list")
96
61
  .description("List incomplete quizzes in a course")
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import { fileURLToPath } from "node:url";
4
5
  const SKILL_NAME = "openape";
5
6
  const GITHUB_RAW_URL = `https://raw.githubusercontent.com/mo7yw4ng/openape/refs/heads/main/skills/${SKILL_NAME}/SKILL.md`;
6
7
  /**
@@ -18,11 +19,10 @@ const PLATFORMS = {
18
19
  async function readSkillContent() {
19
20
  // Try local path first (relative to this file's location)
20
21
  try {
21
- const base = path.dirname(new URL(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).pathname);
22
- const normalized = process.platform === "win32" ? base.replace(/^\//, "") : base;
22
+ const base = path.dirname(fileURLToPath(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
23
23
  // When running from source: src/commands/ → ../../skills/openape/SKILL.md
24
24
  // When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
25
- const localPath = path.resolve(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
25
+ const localPath = path.resolve(base, "..", "..", "skills", SKILL_NAME, "SKILL.md");
26
26
  return await fs.promises.readFile(localPath, "utf-8");
27
27
  }
28
28
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2D5D"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/src/commands/upload.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwD5D"}
@@ -20,19 +20,16 @@ export function registerUploadCommand(program) {
20
20
  process.exitCode = 1;
21
21
  return;
22
22
  }
23
- // Resolve file path
24
23
  const resolvedPath = path.resolve(filePath);
25
- // Check if file exists
24
+ let stats;
26
25
  try {
27
- await fs.access(resolvedPath);
26
+ stats = await fs.stat(resolvedPath);
28
27
  }
29
28
  catch {
30
29
  apiContext.log.error(`檔案不存在: ${filePath}`);
31
30
  process.exitCode = 1;
32
31
  return;
33
32
  }
34
- // Get file size
35
- const stats = await fs.stat(resolvedPath);
36
33
  const fileSizeKB = formatFileSize(stats.size);
37
34
  apiContext.log.info(`上傳檔案: ${path.basename(resolvedPath)} (${fileSizeKB} KB)`);
38
35
  // Upload file
@@ -1 +1 @@
1
- {"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgY5D"}
1
+ {"version":3,"file":"videos.d.ts","sourceRoot":"","sources":["../../../src/src/commands/videos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgT5D"}