@mo7yw4ng/openape 1.0.5 → 2.0.3

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 (152) hide show
  1. package/bin/openape +29 -0
  2. package/bin/openape.js +29 -0
  3. package/package.json +22 -28
  4. package/LICENSE +0 -21
  5. package/README.md +0 -135
  6. package/esm/_dnt.polyfills.d.ts +0 -101
  7. package/esm/_dnt.polyfills.d.ts.map +0 -1
  8. package/esm/_dnt.polyfills.js +0 -127
  9. package/esm/_dnt.shims.d.ts +0 -6
  10. package/esm/_dnt.shims.d.ts.map +0 -1
  11. package/esm/_dnt.shims.js +0 -61
  12. package/esm/deno.d.ts +0 -25
  13. package/esm/deno.d.ts.map +0 -1
  14. package/esm/deno.js +0 -23
  15. package/esm/package.json +0 -3
  16. package/esm/src/commands/announcements.d.ts +0 -3
  17. package/esm/src/commands/announcements.d.ts.map +0 -1
  18. package/esm/src/commands/announcements.js +0 -134
  19. package/esm/src/commands/assignments.d.ts +0 -3
  20. package/esm/src/commands/assignments.d.ts.map +0 -1
  21. package/esm/src/commands/assignments.js +0 -230
  22. package/esm/src/commands/auth.d.ts +0 -3
  23. package/esm/src/commands/auth.d.ts.map +0 -1
  24. package/esm/src/commands/auth.js +0 -290
  25. package/esm/src/commands/calendar.d.ts +0 -3
  26. package/esm/src/commands/calendar.d.ts.map +0 -1
  27. package/esm/src/commands/calendar.js +0 -179
  28. package/esm/src/commands/courses.d.ts +0 -3
  29. package/esm/src/commands/courses.d.ts.map +0 -1
  30. package/esm/src/commands/courses.js +0 -348
  31. package/esm/src/commands/forums.d.ts +0 -3
  32. package/esm/src/commands/forums.d.ts.map +0 -1
  33. package/esm/src/commands/forums.js +0 -318
  34. package/esm/src/commands/grades.d.ts +0 -3
  35. package/esm/src/commands/grades.d.ts.map +0 -1
  36. package/esm/src/commands/grades.js +0 -121
  37. package/esm/src/commands/materials.d.ts +0 -3
  38. package/esm/src/commands/materials.d.ts.map +0 -1
  39. package/esm/src/commands/materials.js +0 -413
  40. package/esm/src/commands/quizzes.d.ts +0 -3
  41. package/esm/src/commands/quizzes.d.ts.map +0 -1
  42. package/esm/src/commands/quizzes.js +0 -271
  43. package/esm/src/commands/skills.d.ts +0 -3
  44. package/esm/src/commands/skills.d.ts.map +0 -1
  45. package/esm/src/commands/skills.js +0 -106
  46. package/esm/src/commands/upload.d.ts +0 -3
  47. package/esm/src/commands/upload.d.ts.map +0 -1
  48. package/esm/src/commands/upload.js +0 -58
  49. package/esm/src/commands/videos.d.ts +0 -3
  50. package/esm/src/commands/videos.d.ts.map +0 -1
  51. package/esm/src/commands/videos.js +0 -336
  52. package/esm/src/index.d.ts +0 -27
  53. package/esm/src/index.d.ts.map +0 -1
  54. package/esm/src/index.js +0 -160
  55. package/esm/src/lib/auth.d.ts +0 -47
  56. package/esm/src/lib/auth.d.ts.map +0 -1
  57. package/esm/src/lib/auth.js +0 -227
  58. package/esm/src/lib/config.d.ts +0 -6
  59. package/esm/src/lib/config.d.ts.map +0 -1
  60. package/esm/src/lib/config.js +0 -36
  61. package/esm/src/lib/logger.d.ts +0 -3
  62. package/esm/src/lib/logger.d.ts.map +0 -1
  63. package/esm/src/lib/logger.js +0 -27
  64. package/esm/src/lib/moodle.d.ts +0 -433
  65. package/esm/src/lib/moodle.d.ts.map +0 -1
  66. package/esm/src/lib/moodle.js +0 -1318
  67. package/esm/src/lib/session.d.ts +0 -8
  68. package/esm/src/lib/session.d.ts.map +0 -1
  69. package/esm/src/lib/session.js +0 -42
  70. package/esm/src/lib/token.d.ts +0 -38
  71. package/esm/src/lib/token.d.ts.map +0 -1
  72. package/esm/src/lib/token.js +0 -178
  73. package/esm/src/lib/types.d.ts +0 -189
  74. package/esm/src/lib/types.d.ts.map +0 -1
  75. package/esm/src/lib/types.js +0 -2
  76. package/esm/src/lib/utils.d.ts +0 -57
  77. package/esm/src/lib/utils.d.ts.map +0 -1
  78. package/esm/src/lib/utils.js +0 -129
  79. package/script/_dnt.polyfills.d.ts +0 -101
  80. package/script/_dnt.polyfills.d.ts.map +0 -1
  81. package/script/_dnt.polyfills.js +0 -130
  82. package/script/_dnt.shims.d.ts +0 -6
  83. package/script/_dnt.shims.d.ts.map +0 -1
  84. package/script/_dnt.shims.js +0 -65
  85. package/script/deno.d.ts +0 -25
  86. package/script/deno.d.ts.map +0 -1
  87. package/script/deno.js +0 -25
  88. package/script/package.json +0 -3
  89. package/script/src/commands/announcements.d.ts +0 -3
  90. package/script/src/commands/announcements.d.ts.map +0 -1
  91. package/script/src/commands/announcements.js +0 -140
  92. package/script/src/commands/assignments.d.ts +0 -3
  93. package/script/src/commands/assignments.d.ts.map +0 -1
  94. package/script/src/commands/assignments.js +0 -269
  95. package/script/src/commands/auth.d.ts +0 -3
  96. package/script/src/commands/auth.d.ts.map +0 -1
  97. package/script/src/commands/auth.js +0 -296
  98. package/script/src/commands/calendar.d.ts +0 -3
  99. package/script/src/commands/calendar.d.ts.map +0 -1
  100. package/script/src/commands/calendar.js +0 -185
  101. package/script/src/commands/courses.d.ts +0 -3
  102. package/script/src/commands/courses.d.ts.map +0 -1
  103. package/script/src/commands/courses.js +0 -354
  104. package/script/src/commands/forums.d.ts +0 -3
  105. package/script/src/commands/forums.d.ts.map +0 -1
  106. package/script/src/commands/forums.js +0 -324
  107. package/script/src/commands/grades.d.ts +0 -3
  108. package/script/src/commands/grades.d.ts.map +0 -1
  109. package/script/src/commands/grades.js +0 -127
  110. package/script/src/commands/materials.d.ts +0 -3
  111. package/script/src/commands/materials.d.ts.map +0 -1
  112. package/script/src/commands/materials.js +0 -419
  113. package/script/src/commands/quizzes.d.ts +0 -3
  114. package/script/src/commands/quizzes.d.ts.map +0 -1
  115. package/script/src/commands/quizzes.js +0 -277
  116. package/script/src/commands/skills.d.ts +0 -3
  117. package/script/src/commands/skills.d.ts.map +0 -1
  118. package/script/src/commands/skills.js +0 -112
  119. package/script/src/commands/upload.d.ts +0 -3
  120. package/script/src/commands/upload.d.ts.map +0 -1
  121. package/script/src/commands/upload.js +0 -64
  122. package/script/src/commands/videos.d.ts +0 -3
  123. package/script/src/commands/videos.d.ts.map +0 -1
  124. package/script/src/commands/videos.js +0 -342
  125. package/script/src/index.d.ts +0 -27
  126. package/script/src/index.d.ts.map +0 -1
  127. package/script/src/index.js +0 -167
  128. package/script/src/lib/auth.d.ts +0 -47
  129. package/script/src/lib/auth.d.ts.map +0 -1
  130. package/script/src/lib/auth.js +0 -269
  131. package/script/src/lib/config.d.ts +0 -6
  132. package/script/src/lib/config.d.ts.map +0 -1
  133. package/script/src/lib/config.js +0 -42
  134. package/script/src/lib/logger.d.ts +0 -3
  135. package/script/src/lib/logger.d.ts.map +0 -1
  136. package/script/src/lib/logger.js +0 -30
  137. package/script/src/lib/moodle.d.ts +0 -433
  138. package/script/src/lib/moodle.d.ts.map +0 -1
  139. package/script/src/lib/moodle.js +0 -1389
  140. package/script/src/lib/session.d.ts +0 -8
  141. package/script/src/lib/session.d.ts.map +0 -1
  142. package/script/src/lib/session.js +0 -45
  143. package/script/src/lib/token.d.ts +0 -38
  144. package/script/src/lib/token.d.ts.map +0 -1
  145. package/script/src/lib/token.js +0 -189
  146. package/script/src/lib/types.d.ts +0 -189
  147. package/script/src/lib/types.d.ts.map +0 -1
  148. package/script/src/lib/types.js +0 -3
  149. package/script/src/lib/utils.d.ts +0 -57
  150. package/script/src/lib/utils.d.ts.map +0 -1
  151. package/script/src/lib/utils.js +0 -175
  152. package/skills/openape/SKILL.md +0 -115
@@ -1,336 +0,0 @@
1
- import { getBaseDir, getOutputFormat, sanitizeFilename } from "../lib/utils.js";
2
- import { getEnrolledCoursesApi, getSupervideosInCourse, getSupervideosInCourseApi, getVideoMetadata, completeVideoApi, downloadVideo, getIncompleteVideosApi } from "../lib/moodle.js";
3
- import { createLogger } from "../lib/logger.js";
4
- import { launchAuthenticated } from "../lib/auth.js";
5
- import { extractSessionInfo } from "../lib/session.js";
6
- import { closeBrowserSafely } from "../lib/auth.js";
7
- import { formatAndOutput } from "../index.js";
8
- import { loadWsToken } from "../lib/token.js";
9
- import path from "node:path";
10
- import fs from "node:fs";
11
- export function registerVideosCommand(program) {
12
- const videosCmd = program.command("videos");
13
- videosCmd.description("Video progress operations");
14
- // Pure API context - no browser required (fast!)
15
- async function createApiContext(options, command) {
16
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
17
- // Don't silence logs for commands that don't have explicit output format control
18
- const outputFormat = command && command.optsWithGlobals ? getOutputFormat(command) : "table";
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
- // Helper function to create session context (for browser-only commands)
44
- async function createSessionContext(options, command) {
45
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
46
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
47
- const silent = outputFormat === "json" && !opts.verbose;
48
- const log = createLogger(opts.verbose, silent, outputFormat);
49
- const baseDir = getBaseDir();
50
- const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
51
- if (!fs.existsSync(sessionPath)) {
52
- console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
53
- return null;
54
- }
55
- const config = {
56
- username: "",
57
- password: "",
58
- courseUrl: "",
59
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
60
- headless: !options.headed,
61
- slowMo: 0,
62
- authStatePath: sessionPath,
63
- ollamaBaseUrl: "",
64
- };
65
- log.info("啟動瀏覽器...");
66
- const { browser, context, page } = await launchAuthenticated(config, log);
67
- try {
68
- const session = await extractSessionInfo(page, config, log);
69
- return { log, page, session, browser, context };
70
- }
71
- catch (err) {
72
- await context.close();
73
- await browser.close();
74
- throw err;
75
- }
76
- }
77
- videosCmd
78
- .command("list")
79
- .description("List videos in a course")
80
- .argument("<course-id>", "Course ID")
81
- .option("--incomplete-only", "Show only incomplete videos")
82
- .option("--output <format>", "Output format: json|csv|table|silent")
83
- .action(async (courseId, options, command) => {
84
- const output = getOutputFormat(command);
85
- const apiContext = await createApiContext(options, command);
86
- if (!apiContext) {
87
- process.exitCode = 1;
88
- return;
89
- }
90
- let videos = await getSupervideosInCourseApi(apiContext.session, parseInt(courseId, 10));
91
- // Filter for incomplete videos if requested
92
- if (options.incompleteOnly) {
93
- videos = videos.filter(v => !v.isComplete);
94
- }
95
- formatAndOutput(videos, output, apiContext.log);
96
- });
97
- videosCmd
98
- .command("complete")
99
- .description("Complete videos in a course (uses API for list & completion, browser for metadata)")
100
- .argument("<course-id>", "Course ID")
101
- .option("--dry-run", "Discover videos but don't complete them")
102
- .option("--output <format>", "Output format: json|csv|table|silent")
103
- .action(async (courseId, options, command) => {
104
- const output = getOutputFormat(command);
105
- // Get API context for getting incomplete videos and completion
106
- const apiContext = await createApiContext(options, command);
107
- if (!apiContext) {
108
- process.exitCode = 1;
109
- return;
110
- }
111
- // Get incomplete videos via API (fast, no browser needed)
112
- const incompleteVideos = await getIncompleteVideosApi(apiContext.session, parseInt(courseId, 10));
113
- if (incompleteVideos.length === 0) {
114
- apiContext.log.info("所有影片已完成(或無影片)。");
115
- return;
116
- }
117
- apiContext.log.info(`找到 ${incompleteVideos.length} 部未完成影片`);
118
- // Dry-run: show videos without needing browser
119
- if (options.dryRun) {
120
- const results = incompleteVideos.map(v => ({ name: v.name, success: true }));
121
- for (const video of incompleteVideos) {
122
- apiContext.log.info(` [試執行] ${video.name}`);
123
- }
124
- apiContext.log.info(`\n執行結果: ${results.length} 影片將被完成`);
125
- if (output !== "silent") {
126
- formatAndOutput(results, output, apiContext.log);
127
- }
128
- return;
129
- }
130
- // Need browser only for getting viewId and duration (not needed for dry-run)
131
- const context = await createSessionContext(options, command);
132
- if (!context) {
133
- process.exitCode = 1;
134
- return;
135
- }
136
- const { log, page, browser, context: browserContext } = context;
137
- try {
138
- const results = [];
139
- for (const sv of incompleteVideos) {
140
- log.info(`處理中: ${sv.name}`);
141
- try {
142
- const video = await getVideoMetadata(page, sv.url, log);
143
- // Use WS API for completion
144
- const success = await completeVideoApi(apiContext.session, { ...video, cmid: sv.cmid.toString() });
145
- if (success) {
146
- log.success(` 已完成!`);
147
- results.push({ name: sv.name, success: true });
148
- }
149
- else {
150
- log.error(` 失敗。`);
151
- results.push({ name: sv.name, success: false, error: "Failed to complete" });
152
- }
153
- }
154
- catch (err) {
155
- const msg = err instanceof Error ? err.message : String(err);
156
- log.error(` 錯誤: ${msg}`);
157
- results.push({ name: sv.name, success: false, error: msg });
158
- }
159
- }
160
- const completed = results.filter(r => r.success).length;
161
- const failed = results.filter(r => !r.success).length;
162
- log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
163
- if (output !== "silent") {
164
- formatAndOutput(results, output, log);
165
- }
166
- }
167
- finally {
168
- await closeBrowserSafely(browser, browserContext);
169
- }
170
- });
171
- videosCmd
172
- .command("complete-all")
173
- .description("Complete all incomplete videos across all courses (uses API for list & completion, browser for metadata)")
174
- .option("--dry-run", "Discover videos but don't complete them")
175
- .option("--output <format>", "Output format: json|csv|table|silent")
176
- .action(async (options, command) => {
177
- const output = getOutputFormat(command);
178
- // Get API context for getting incomplete videos and completion
179
- const apiContext = await createApiContext(options, command);
180
- if (!apiContext) {
181
- process.exitCode = 1;
182
- return;
183
- }
184
- // Get all courses via API
185
- const classification = undefined; // all courses
186
- const courses = await getEnrolledCoursesApi(apiContext.session, { classification });
187
- apiContext.log.info(`掃描 ${courses.length} 個課程...`);
188
- // Collect all incomplete videos across all courses using flatMap for cleaner code
189
- const allIncompleteVideos = (await Promise.allSettled(courses.map(async (course) => {
190
- try {
191
- const videos = await getIncompleteVideosApi(apiContext.session, course.id);
192
- return videos.map((video) => ({
193
- courseId: course.id,
194
- courseName: course.fullname,
195
- cmid: video.cmid,
196
- name: video.name,
197
- url: video.url,
198
- }));
199
- }
200
- catch (e) {
201
- apiContext.log.warn(`無法取得課程 ${course.fullname} 的影片: ${e}`);
202
- return [];
203
- }
204
- })))
205
- .filter((result) => result.status === "fulfilled")
206
- .flatMap((result) => result.status === "fulfilled" ? result.value : []);
207
- if (allIncompleteVideos.length === 0) {
208
- apiContext.log.info("所有影片已完成(或無影片)。");
209
- return;
210
- }
211
- apiContext.log.info(`找到 ${allIncompleteVideos.length} 部未完成影片`);
212
- // Dry-run: show videos without needing browser
213
- if (options.dryRun) {
214
- for (const video of allIncompleteVideos) {
215
- apiContext.log.info(` [試執行] [${video.courseName}] ${video.name}`);
216
- }
217
- apiContext.log.info("\n===== 執行結果 =====");
218
- apiContext.log.info(`掃描課程數: ${courses.length}`);
219
- apiContext.log.info(`找到未完成影片: ${allIncompleteVideos.length}`);
220
- apiContext.log.info(`執行影片數: ${allIncompleteVideos.length} (試執行)`);
221
- return;
222
- }
223
- // Need browser only for getting viewId and duration (not needed for dry-run)
224
- const context = await createSessionContext(options, command);
225
- if (!context) {
226
- process.exitCode = 1;
227
- return;
228
- }
229
- const { log, page, browser, context: browserContext } = context;
230
- try {
231
- const allResults = [];
232
- let totalCompleted = 0;
233
- let totalFailed = 0;
234
- for (const video of allIncompleteVideos) {
235
- log.info(`處理中: [${video.courseName}] ${video.name}`);
236
- try {
237
- const metadata = await getVideoMetadata(page, video.url, log);
238
- // Use WS API for completion
239
- const success = await completeVideoApi(apiContext.session, { ...metadata, cmid: video.cmid.toString() });
240
- if (success) {
241
- log.success(` 已完成!`);
242
- allResults.push({ courseName: video.courseName, name: video.name, success: true });
243
- totalCompleted++;
244
- }
245
- else {
246
- log.error(` 失敗。`);
247
- allResults.push({ courseName: video.courseName, name: video.name, success: false, error: "Failed to complete" });
248
- totalFailed++;
249
- }
250
- }
251
- catch (err) {
252
- const msg = err instanceof Error ? err.message : String(err);
253
- log.error(` 錯誤: ${msg}`);
254
- allResults.push({ courseName: video.courseName, name: video.name, success: false, error: msg });
255
- totalFailed++;
256
- }
257
- }
258
- log.info("\n===== 執行結果 =====");
259
- log.info(`掃描課程數: ${courses.length}`);
260
- log.info(`找到未完成影片: ${allIncompleteVideos.length}`);
261
- log.info(`執行影片數: ${totalCompleted}`);
262
- if (totalFailed > 0)
263
- log.warn(`失敗影片數: ${totalFailed}`);
264
- if (output !== "silent") {
265
- formatAndOutput(allResults, output, log);
266
- }
267
- }
268
- finally {
269
- await closeBrowserSafely(browser, browserContext);
270
- }
271
- });
272
- videosCmd
273
- .command("download")
274
- .description("Download videos from a course (requires browser)")
275
- .argument("<course-id>", "Course ID")
276
- .option("--output-dir <path>", "Output directory", "./downloads/videos")
277
- .option("--incomplete-only", "Download only incomplete videos")
278
- .action(async (courseId, options, command) => {
279
- const context = await createSessionContext(options, command);
280
- if (!context) {
281
- process.exitCode = 1;
282
- return;
283
- }
284
- const { log, page, session, browser, context: browserContext } = context;
285
- try {
286
- const videos = await getSupervideosInCourse(page, session, parseInt(courseId, 10), log, {
287
- incompleteOnly: options.incompleteOnly,
288
- });
289
- log.info(`找到 ${videos.length} 個影片`);
290
- const baseDir = getBaseDir();
291
- const outputDir = path.resolve(baseDir, options.outputDir);
292
- fs.mkdirSync(outputDir, { recursive: true });
293
- const downloaded = [];
294
- for (const video of videos) {
295
- const filename = sanitizeFilename(video.name) + ".mp4";
296
- const outputPath = path.join(outputDir, filename);
297
- log.info(`處理中: ${video.name}`);
298
- try {
299
- const metadata = await getVideoMetadata(page, video.url, log);
300
- const result = await downloadVideo(page, metadata, outputPath, log);
301
- if (result.success) {
302
- log.success(` 已下載: ${result.path}`);
303
- downloaded.push({ name: video.name, path: result.path, success: true, type: result.type });
304
- }
305
- else {
306
- log.warn(` 失敗: ${result.error}`);
307
- downloaded.push({ name: video.name, path: "", success: false, error: result.error, type: result.type });
308
- }
309
- }
310
- catch (err) {
311
- const msg = err instanceof Error ? err.message : String(err);
312
- log.error(` 錯誤: ${msg}`);
313
- downloaded.push({ name: video.name, path: "", success: false, error: msg });
314
- }
315
- }
316
- const completed = downloaded.filter(d => d.success).length;
317
- const failed = downloaded.filter(d => !d.success).length;
318
- log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
319
- console.log(JSON.stringify({
320
- status: "success",
321
- timestamp: new Date().toISOString(),
322
- course_id: courseId,
323
- output_dir: outputDir,
324
- total_videos: videos.length,
325
- downloaded: completed,
326
- failed,
327
- }));
328
- for (const v of downloaded) {
329
- console.log(JSON.stringify(v));
330
- }
331
- }
332
- finally {
333
- await closeBrowserSafely(browser, browserContext);
334
- }
335
- });
336
- }
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- import "../_dnt.polyfills.js";
3
- import { createLogger } from "./lib/logger.js";
4
- import type { AppConfig, Logger, SessionInfo, OutputFormat } from "./lib/types.js";
5
- /**
6
- * Create a session context for commands that need authentication.
7
- */
8
- export declare function createSessionContext(options: {
9
- config?: string;
10
- session?: string;
11
- verbose?: boolean;
12
- silent?: boolean;
13
- headed?: boolean;
14
- interactive?: boolean;
15
- }): Promise<{
16
- config: AppConfig;
17
- log: Logger;
18
- page: import("playwright-core").Page;
19
- session: SessionInfo;
20
- } | null>;
21
- /**
22
- * Helper to output formatted data.
23
- * For JSON output (agent mode), exits immediately after output.
24
- */
25
- export declare function formatAndOutput<T extends Record<string, unknown>>(data: T | T[], format: OutputFormat, log: Logger): void;
26
- export { createLogger, type AppConfig, type Logger, type SessionInfo, type OutputFormat };
27
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/src/index.ts"],"names":[],"mappings":";AACA,OAAO,sBAAsB,CAAC;AAO9B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAwEnF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GACA,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBhH;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,EACb,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,GACV,IAAI,CAyBN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
package/esm/src/index.js DELETED
@@ -1,160 +0,0 @@
1
- #!/usr/bin/env node
2
- import "../_dnt.polyfills.js";
3
- import { getBaseDir } from "./lib/utils.js";
4
- import { Command } from "commander";
5
- import { loadConfig } from "./lib/config.js";
6
- import { launchAuthenticated } from "./lib/auth.js";
7
- import { extractSessionInfo } from "./lib/session.js";
8
- import { createLogger } from "./lib/logger.js";
9
- import denoJson from "../deno.js";
10
- // Import command handlers
11
- import { registerCoursesCommand } from "./commands/courses.js";
12
- import { registerVideosCommand } from "./commands/videos.js";
13
- import { registerQuizzesCommand } from "./commands/quizzes.js";
14
- import { registerCommand } from "./commands/auth.js";
15
- import { registerMaterialsCommand } from "./commands/materials.js";
16
- import { registerGradesCommand } from "./commands/grades.js";
17
- import { registerForumsCommand } from "./commands/forums.js";
18
- import { registerAnnouncementsCommand } from "./commands/announcements.js";
19
- import { registerCalendarCommand } from "./commands/calendar.js";
20
- import { registerSkillsCommand } from "./commands/skills.js";
21
- import { registerAssignmentsCommand } from "./commands/assignments.js";
22
- import { registerUploadCommand } from "./commands/upload.js";
23
- const program = new Command();
24
- program
25
- .name("openape")
26
- .description(denoJson.description)
27
- .version(denoJson.version);
28
- // Global options
29
- program
30
- .option("--config <path>", "Custom config file path")
31
- .option("--session <path>", "Session file path", ".auth/storage-state.json")
32
- .option("--output <format>", "Output format: json|csv|table|silent", "json")
33
- .option("--verbose", "Enable debug logging")
34
- .option("--silent", "Suppress all log output (JSON only)")
35
- .option("--headed", "Run browser in visible mode");
36
- // Register subcommands
37
- registerCommand(program);
38
- registerCoursesCommand(program);
39
- registerVideosCommand(program);
40
- registerQuizzesCommand(program);
41
- registerMaterialsCommand(program);
42
- registerGradesCommand(program);
43
- registerForumsCommand(program);
44
- registerAnnouncementsCommand(program);
45
- registerCalendarCommand(program);
46
- registerSkillsCommand(program);
47
- registerAssignmentsCommand(program);
48
- registerUploadCommand(program);
49
- /**
50
- * Load configuration and authenticate, returning the context for commands.
51
- */
52
- async function createCommandContext(options) {
53
- const log = createLogger(options.verbose, options.silent);
54
- const baseDir = getBaseDir();
55
- const config = loadConfig(baseDir);
56
- // Apply CLI overrides
57
- if (options.headed)
58
- config.headless = false;
59
- if (options.session)
60
- config.authStatePath = options.session;
61
- return { config, log };
62
- }
63
- /**
64
- * Create a session context for commands that need authentication.
65
- */
66
- export async function createSessionContext(options) {
67
- const context = await createCommandContext(options);
68
- if (!context)
69
- return null;
70
- const { config, log } = context;
71
- log.info("啟動瀏覽器...");
72
- const { browser, context: browserContext, page, wsToken } = await launchAuthenticated(config, log);
73
- try {
74
- const session = await extractSessionInfo(page, config, log, wsToken);
75
- // Keep the browser context alive for the duration of the command
76
- // Note: Caller is responsible for closing the browser
77
- return { config, log, page, session };
78
- }
79
- catch (err) {
80
- await browserContext.close();
81
- await browser.close();
82
- throw err;
83
- }
84
- }
85
- /**
86
- * Helper to output formatted data.
87
- * For JSON output (agent mode), exits immediately after output.
88
- */
89
- export function formatAndOutput(data, format, log) {
90
- if (format === "json") {
91
- if (Array.isArray(data)) {
92
- for (const item of data) {
93
- console.log(JSON.stringify(item));
94
- }
95
- }
96
- else {
97
- console.log(JSON.stringify(data));
98
- }
99
- // Exit immediately for AI agent - no need to wait for browser cleanup
100
- process.exit(0);
101
- }
102
- else if (format === "csv") {
103
- const arr = Array.isArray(data) ? data : [data];
104
- if (arr.length === 0)
105
- return;
106
- const fields = Object.keys(arr[0]);
107
- console.log(formatAsCsv(arr, fields));
108
- }
109
- else if (format === "table") {
110
- const arr = Array.isArray(data) ? data : [data];
111
- if (arr.length === 0) {
112
- console.log("No data");
113
- return;
114
- }
115
- console.log(formatAsTable(arr));
116
- }
117
- // "silent" produces no output
118
- }
119
- function formatAsCsv(data, fields) {
120
- const headers = fields.join(",");
121
- const rows = data.map((item) => {
122
- return fields.map((field) => {
123
- const value = item[field];
124
- if (value === null || value === undefined)
125
- return "";
126
- if (typeof value === "string") {
127
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
128
- return `"${value.replace(/"/g, '""')}"`;
129
- }
130
- return value;
131
- }
132
- return String(value);
133
- }).join(",");
134
- });
135
- return [headers, ...rows].join("\n");
136
- }
137
- function formatAsTable(data) {
138
- const allFields = Array.from(new Set(data.flatMap((item) => Object.keys(item))));
139
- const widths = {};
140
- allFields.forEach((field) => {
141
- widths[field] = Math.max(field.length, ...data.map((item) => String(item[field] ?? "").length)) + 2;
142
- });
143
- const header = allFields.map((f) => f.padEnd(widths[f])).join(" | ");
144
- const separator = allFields.map((f) => "-".repeat(widths[f] - 1)).join("-+-");
145
- const rows = data.map((item) => {
146
- return allFields.map((f) => String(item[f] ?? "").padEnd(widths[f])).join(" | ");
147
- });
148
- return [header, separator, ...rows].join("\n");
149
- }
150
- // Export utilities for commands
151
- export { createLogger };
152
- // Run the program
153
- if (globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).main) {
154
- // If no subcommand provided, show help
155
- const args = process.argv.slice(2);
156
- if (args.length === 0) {
157
- program.help();
158
- }
159
- program.parse();
160
- }
@@ -1,47 +0,0 @@
1
- import { type Browser, type BrowserContext, type Page } from "playwright-core";
2
- import type { AppConfig, Logger, OutputFormat } from "./types.js";
3
- /**
4
- * Find a Chromium-based browser executable on Windows.
5
- * Priority: Edge → Chrome → Brave
6
- */
7
- export declare function findEdgePath(): string;
8
- /**
9
- * Launch a browser and return an authenticated context.
10
- * Tries to restore a saved session first; falls back to fresh OAuth login.
11
- * Also acquires Moodle Web Service Token for API calls.
12
- */
13
- export declare function launchAuthenticated(config: AppConfig, log: Logger): Promise<{
14
- browser: Browser;
15
- context: BrowserContext;
16
- page: Page;
17
- wsToken?: string;
18
- }>;
19
- /**
20
- * Safely close browser and context with timeout.
21
- * Designed for AI agent usage - no human interaction needed.
22
- * If noWait is true, initiates cleanup but doesn't wait for completion.
23
- *
24
- * Note: Closes sequentially (context first, then browser) to avoid libuv
25
- * assertion failures on Windows when handles are closed concurrently.
26
- */
27
- export declare function closeBrowserSafely(browser: Browser, context?: BrowserContext, timeoutMs?: number, noWait?: boolean): Promise<void>;
28
- /**
29
- * Create API context for WS token operations (no browser required).
30
- * Returns null if session is invalid or WS token is missing.
31
- */
32
- export declare function createApiContext(options: {
33
- verbose?: boolean;
34
- headed?: boolean;
35
- }, command?: {
36
- optsWithGlobals(): {
37
- output?: OutputFormat;
38
- verbose?: boolean;
39
- };
40
- }): Promise<{
41
- log: Logger;
42
- session: {
43
- wsToken: string;
44
- moodleBaseUrl: string;
45
- };
46
- } | null>;
47
- //# sourceMappingURL=auth.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/src/lib/auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACzF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA4DtF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,cAAc,EACxB,SAAS,GAAE,MAAa,EACxB,MAAM,GAAE,OAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAoHD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAChD,OAAO,CAAC,EAAE;IAAE,eAAe,IAAI;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAC5E,OAAO,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD,GAAG,IAAI,CAAC,CA0BR"}