@mo7yw4ng/openape 1.0.3 → 1.0.5

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 (87) hide show
  1. package/README.md +30 -5
  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 +16 -17
  5. package/esm/src/commands/assignments.d.ts +3 -0
  6. package/esm/src/commands/assignments.d.ts.map +1 -0
  7. package/esm/src/commands/assignments.js +230 -0
  8. package/esm/src/commands/auth.d.ts.map +1 -1
  9. package/esm/src/commands/auth.js +45 -15
  10. package/esm/src/commands/calendar.d.ts.map +1 -1
  11. package/esm/src/commands/calendar.js +20 -21
  12. package/esm/src/commands/courses.js +6 -6
  13. package/esm/src/commands/forums.d.ts.map +1 -1
  14. package/esm/src/commands/forums.js +128 -36
  15. package/esm/src/commands/grades.js +3 -3
  16. package/esm/src/commands/materials.d.ts.map +1 -1
  17. package/esm/src/commands/materials.js +115 -224
  18. package/esm/src/commands/quizzes.d.ts.map +1 -1
  19. package/esm/src/commands/quizzes.js +179 -68
  20. package/esm/src/commands/skills.d.ts.map +1 -1
  21. package/esm/src/commands/skills.js +4 -8
  22. package/esm/src/commands/upload.d.ts +3 -0
  23. package/esm/src/commands/upload.d.ts.map +1 -0
  24. package/esm/src/commands/upload.js +58 -0
  25. package/esm/src/commands/videos.d.ts.map +1 -1
  26. package/esm/src/commands/videos.js +10 -9
  27. package/esm/src/index.d.ts.map +1 -1
  28. package/esm/src/index.js +12 -1
  29. package/esm/src/lib/auth.d.ts +23 -1
  30. package/esm/src/lib/auth.d.ts.map +1 -1
  31. package/esm/src/lib/auth.js +36 -3
  32. package/esm/src/lib/logger.d.ts +1 -1
  33. package/esm/src/lib/logger.d.ts.map +1 -1
  34. package/esm/src/lib/logger.js +7 -4
  35. package/esm/src/lib/moodle.d.ts +183 -1
  36. package/esm/src/lib/moodle.d.ts.map +1 -1
  37. package/esm/src/lib/moodle.js +498 -13
  38. package/esm/src/lib/types.d.ts +81 -164
  39. package/esm/src/lib/types.d.ts.map +1 -1
  40. package/esm/src/lib/types.js +1 -0
  41. package/esm/src/lib/utils.d.ts +20 -0
  42. package/esm/src/lib/utils.d.ts.map +1 -1
  43. package/esm/src/lib/utils.js +48 -1
  44. package/package.json +1 -1
  45. package/script/deno.js +1 -1
  46. package/script/src/commands/announcements.d.ts.map +1 -1
  47. package/script/src/commands/announcements.js +15 -16
  48. package/script/src/commands/assignments.d.ts +3 -0
  49. package/script/src/commands/assignments.d.ts.map +1 -0
  50. package/script/src/commands/assignments.js +269 -0
  51. package/script/src/commands/auth.d.ts.map +1 -1
  52. package/script/src/commands/auth.js +44 -14
  53. package/script/src/commands/calendar.d.ts.map +1 -1
  54. package/script/src/commands/calendar.js +19 -20
  55. package/script/src/commands/courses.js +5 -5
  56. package/script/src/commands/forums.d.ts.map +1 -1
  57. package/script/src/commands/forums.js +128 -36
  58. package/script/src/commands/grades.js +3 -3
  59. package/script/src/commands/materials.d.ts.map +1 -1
  60. package/script/src/commands/materials.js +115 -224
  61. package/script/src/commands/quizzes.d.ts.map +1 -1
  62. package/script/src/commands/quizzes.js +177 -66
  63. package/script/src/commands/skills.d.ts.map +1 -1
  64. package/script/src/commands/skills.js +4 -8
  65. package/script/src/commands/upload.d.ts +3 -0
  66. package/script/src/commands/upload.d.ts.map +1 -0
  67. package/script/src/commands/upload.js +64 -0
  68. package/script/src/commands/videos.d.ts.map +1 -1
  69. package/script/src/commands/videos.js +10 -9
  70. package/script/src/index.d.ts.map +1 -1
  71. package/script/src/index.js +12 -1
  72. package/script/src/lib/auth.d.ts +23 -1
  73. package/script/src/lib/auth.d.ts.map +1 -1
  74. package/script/src/lib/auth.js +70 -3
  75. package/script/src/lib/logger.d.ts +1 -1
  76. package/script/src/lib/logger.d.ts.map +1 -1
  77. package/script/src/lib/logger.js +7 -4
  78. package/script/src/lib/moodle.d.ts +183 -1
  79. package/script/src/lib/moodle.d.ts.map +1 -1
  80. package/script/src/lib/moodle.js +511 -13
  81. package/script/src/lib/types.d.ts +81 -164
  82. package/script/src/lib/types.d.ts.map +1 -1
  83. package/script/src/lib/types.js +1 -0
  84. package/script/src/lib/utils.d.ts +20 -0
  85. package/script/src/lib/utils.d.ts.map +1 -1
  86. package/script/src/lib/utils.js +52 -0
  87. package/skills/openape/SKILL.md +74 -270
@@ -7,13 +7,61 @@ exports.registerQuizzesCommand = registerQuizzesCommand;
7
7
  const utils_js_1 = require("../lib/utils.js");
8
8
  const moodle_js_1 = require("../lib/moodle.js");
9
9
  const logger_js_1 = require("../lib/logger.js");
10
- const auth_js_1 = require("../lib/auth.js");
11
- const session_js_1 = require("../lib/session.js");
12
- const auth_js_2 = require("../lib/auth.js");
13
10
  const index_js_1 = require("../index.js");
14
11
  const token_js_1 = require("../lib/token.js");
15
12
  const node_path_1 = __importDefault(require("node:path"));
16
13
  const node_fs_1 = __importDefault(require("node:fs"));
14
+ function stripHtmlKeepLines(html) {
15
+ return html
16
+ .replace(/<br\s*\/?>/gi, "\n")
17
+ .replace(/<\/p>/gi, "\n")
18
+ .replace(/<[^>]+>/g, "")
19
+ .replace(/&nbsp;/g, " ")
20
+ .replace(/&amp;/g, "&")
21
+ .replace(/&lt;/g, "<")
22
+ .replace(/&gt;/g, ">")
23
+ .replace(/\n{3,}/g, "\n\n")
24
+ .trim();
25
+ }
26
+ function parseQuestionHtml(html) {
27
+ const qtextMatch = html.match(/<div class="qtext">([\s\S]*?)<\/div>\s*<\/div>/);
28
+ const text = stripHtmlKeepLines(qtextMatch?.[1] ?? "");
29
+ const options = [];
30
+ const optionRegex = /data-region="answer-label">([\s\S]*?)<\/div>\s*<\/div>/g;
31
+ let match;
32
+ while ((match = optionRegex.exec(html)) !== null) {
33
+ options.push(stripHtmlKeepLines(match[1]));
34
+ }
35
+ return { text, options };
36
+ }
37
+ function parseSavedAnswer(html) {
38
+ const radioChecked = html.match(/<input type="radio"[^>]*value="(\d+)"[^>]*checked="checked"/);
39
+ if (radioChecked && radioChecked[1] !== "-1")
40
+ return radioChecked[1];
41
+ const checkboxChecked = [...html.matchAll(/<input type="checkbox"[^>]*name="[^"]*choice(\d+)"[^>]*checked="checked"/g)];
42
+ if (checkboxChecked.length > 0)
43
+ return checkboxChecked.map(m => m[1]);
44
+ // Match <input> with both name="*_answer" and type="text" in any attribute order
45
+ const textMatch = html.match(/<input[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*value="([^"]*)"/);
46
+ if (textMatch && textMatch[1] !== "")
47
+ return textMatch[1];
48
+ return null;
49
+ }
50
+ function parseQuizQuestions(questions) {
51
+ return Object.values(questions).map((q) => {
52
+ const parsed = parseQuestionHtml(q.html ?? "");
53
+ const savedAnswer = parseSavedAnswer(q.html ?? "");
54
+ return {
55
+ slot: q.slot,
56
+ type: q.type,
57
+ status: q.status,
58
+ stateclass: q.stateclass,
59
+ savedAnswer,
60
+ question: parsed.text,
61
+ options: parsed.options,
62
+ };
63
+ });
64
+ }
17
65
  function registerQuizzesCommand(program) {
18
66
  const quizzesCmd = program.command("quizzes");
19
67
  quizzesCmd.description("Quiz operations");
@@ -26,19 +74,19 @@ function registerQuizzesCommand(program) {
26
74
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
27
75
  const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
28
76
  const silent = outputFormat === "json" && !opts.verbose;
29
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
77
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
30
78
  const baseDir = (0, utils_js_1.getBaseDir)();
31
79
  const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
32
80
  // Check if session exists
33
81
  if (!node_fs_1.default.existsSync(sessionPath)) {
34
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
82
+ log.error("未找到登入 session。請先執行 'openape login' 進行登入。");
35
83
  log.info(`Session 預期位置: ${sessionPath}`);
36
84
  return null;
37
85
  }
38
86
  // Try to load WS token
39
87
  const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
40
88
  if (!wsToken) {
41
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
89
+ log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
42
90
  return null;
43
91
  }
44
92
  return {
@@ -49,45 +97,11 @@ function registerQuizzesCommand(program) {
49
97
  },
50
98
  };
51
99
  }
52
- // Helper function to create session context (for open command only)
53
- async function createSessionContext(options, command) {
54
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
55
- const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
56
- const silent = outputFormat === "json" && !opts.verbose;
57
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
58
- const baseDir = (0, utils_js_1.getBaseDir)();
59
- const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
60
- if (!node_fs_1.default.existsSync(sessionPath)) {
61
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
62
- return null;
63
- }
64
- const config = {
65
- username: "",
66
- password: "",
67
- courseUrl: "",
68
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
69
- headless: !options.headed,
70
- slowMo: 0,
71
- authStatePath: sessionPath,
72
- ollamaBaseUrl: "",
73
- };
74
- log.info("啟動瀏覽器...");
75
- const { browser, context, page } = await (0, auth_js_1.launchAuthenticated)(config, log);
76
- try {
77
- const session = await (0, session_js_1.extractSessionInfo)(page, config, log);
78
- return { log, page, session, browser, context };
79
- }
80
- catch (err) {
81
- await context.close();
82
- await browser.close();
83
- throw err;
84
- }
85
- }
86
100
  quizzesCmd
87
101
  .command("list")
88
- .description("List quizzes in a course")
102
+ .description("List incomplete quizzes in a course")
89
103
  .argument("<course-id>", "Course ID")
90
- .option("--available-only", "Show only available quizzes")
104
+ .option("--all", "Include completed quizzes")
91
105
  .option("--output <format>", "Output format: json|csv|table|silent")
92
106
  .action(async (courseId, options, command) => {
93
107
  const output = getOutputFormat(command);
@@ -97,16 +111,19 @@ function registerQuizzesCommand(program) {
97
111
  return;
98
112
  }
99
113
  const quizzes = await (0, moodle_js_1.getQuizzesByCoursesApi)(apiContext.session, [parseInt(courseId, 10)]);
100
- // Note: API doesn't provide completion status, so --available-only shows all
101
- if (options.availableOnly) {
102
- apiContext.log.warn("--available-only is not supported in API mode, showing all quizzes");
103
- }
104
- (0, index_js_1.formatAndOutput)(quizzes, output, apiContext.log);
114
+ // Default: only show incomplete quizzes
115
+ const filtered = options.all ? quizzes : quizzes.filter(q => !q.isComplete);
116
+ const formattedQuizzes = filtered.map(({ courseId, ...q }) => ({
117
+ ...q,
118
+ timeClose: q.timeClose ? (0, utils_js_1.formatTimestamp)(q.timeClose) : null,
119
+ }));
120
+ (0, index_js_1.formatAndOutput)(formattedQuizzes, output, apiContext.log);
105
121
  });
106
122
  quizzesCmd
107
123
  .command("list-all")
108
- .description("List all available quizzes across all courses")
124
+ .description("List all incomplete quizzes across all courses")
109
125
  .option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
126
+ .option("--all", "Include completed quizzes")
110
127
  .option("--output <format>", "Output format: json|csv|table|silent")
111
128
  .action(async (options, command) => {
112
129
  const output = getOutputFormat(command);
@@ -127,13 +144,17 @@ function registerQuizzesCommand(program) {
127
144
  const allQuizzes = [];
128
145
  for (const q of apiQuizzes) {
129
146
  const course = courseMap.get(q.courseId);
130
- if (course) {
147
+ if (course && (options.all || !q.isComplete)) {
131
148
  allQuizzes.push({
132
149
  courseName: course.fullname,
150
+ courseId: q.courseId,
133
151
  name: q.name,
134
152
  url: q.url,
135
- cmid: q.cmid,
153
+ quizid: q.quizid,
136
154
  isComplete: q.isComplete,
155
+ attemptsUsed: q.attemptsUsed,
156
+ maxAttempts: q.maxAttempts,
157
+ timeClose: q.timeClose ? (0, utils_js_1.formatTimestamp)(q.timeClose) : null,
137
158
  });
138
159
  }
139
160
  }
@@ -141,26 +162,116 @@ function registerQuizzesCommand(program) {
141
162
  (0, index_js_1.formatAndOutput)(allQuizzes, output, apiContext.log);
142
163
  });
143
164
  quizzesCmd
144
- .command("open")
145
- .description("Open a quiz URL in browser (manual mode)")
146
- .argument("<quiz-url>", "Quiz URL")
147
- .option("--headed", "Run browser in visible mode (default: true)")
148
- .action(async (quizUrl, options, command) => {
149
- const context = await createSessionContext({ ...options, headed: true }, command);
150
- if (!context) {
165
+ .command("start")
166
+ .description("Start a new quiz attempt")
167
+ .argument("<quiz-id>", "Quiz ID")
168
+ .option("--output <format>", "Output format: json|csv|table|silent")
169
+ .action(async (quizCmid, options, command) => {
170
+ const output = getOutputFormat(command);
171
+ const apiContext = await createApiContext(options, command);
172
+ if (!apiContext) {
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ try {
177
+ const result = await (0, moodle_js_1.startQuizAttemptApi)(apiContext.session, quizCmid);
178
+ apiContext.log.success(`Quiz attempt ${result.attempt.attemptid} started.`);
179
+ const attemptId = result.attempt.attemptid;
180
+ const data = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, attemptId);
181
+ const questions = parseQuizQuestions(data.questions);
182
+ const outputData = [{
183
+ attemptId,
184
+ quizId: result.attempt.quizid,
185
+ state: result.attempt.state,
186
+ timeStart: (0, utils_js_1.formatTimestamp)(result.attempt.timestart),
187
+ timeFinish: result.attempt.timefinish
188
+ ? (0, utils_js_1.formatTimestamp)(result.attempt.timefinish)
189
+ : null,
190
+ isPreview: result.attempt.preview,
191
+ totalQuestions: questions.length,
192
+ questions,
193
+ }];
194
+ (0, index_js_1.formatAndOutput)(outputData, output, apiContext.log);
195
+ }
196
+ catch (error) {
197
+ apiContext.log.error(`Failed to start quiz attempt: ${error instanceof Error ? error.message : String(error)}`);
198
+ process.exitCode = 1;
199
+ }
200
+ });
201
+ quizzesCmd
202
+ .command("info")
203
+ .description("Get quiz attempt data and questions")
204
+ .argument("<attempt-id>", "Quiz attempt ID")
205
+ .option("--page <number>", "Page number (-1 for all pages)", "-1")
206
+ .option("--output <format>", "Output format: json|csv|table|silent")
207
+ .action(async (attemptId, options, command) => {
208
+ const output = getOutputFormat(command);
209
+ const apiContext = await createApiContext(options, command);
210
+ if (!apiContext) {
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ try {
215
+ const pageNumber = parseInt(options.page);
216
+ const data = pageNumber === -1
217
+ ? await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId))
218
+ : await (0, moodle_js_1.getQuizAttemptDataApi)(apiContext.session, parseInt(attemptId), pageNumber);
219
+ const questions = parseQuizQuestions(data.questions);
220
+ const outputData = [{
221
+ attemptId: data.attempt.attemptid,
222
+ quizId: data.attempt.quizid,
223
+ state: data.attempt.state,
224
+ totalQuestions: questions.length,
225
+ questions,
226
+ }];
227
+ apiContext.log.success(`Retrieved attempt ${data.attempt.attemptid}`);
228
+ (0, index_js_1.formatAndOutput)(outputData, output, apiContext.log);
229
+ }
230
+ catch (error) {
231
+ apiContext.log.error(`Failed to get attempt data: ${error instanceof Error ? error.message : String(error)}`);
232
+ process.exitCode = 1;
233
+ }
234
+ });
235
+ quizzesCmd
236
+ .command("save")
237
+ .description("Save answers for a quiz attempt")
238
+ .argument("<attempt-id>", "Quiz attempt ID")
239
+ .argument("<answers>", "Answers JSON: [{slot:1,answer:\"0\"}] multichoice=number, multichoices=\"0,2\", shortanswer=\"text\"")
240
+ .option("--submit", "Submit the attempt after saving")
241
+ .option("--output <format>", "Output format: json|csv|table|silent")
242
+ .action(async (attemptId, answersJson, options, command) => {
243
+ const output = getOutputFormat(command);
244
+ const apiContext = await createApiContext(options, command);
245
+ if (!apiContext) {
151
246
  process.exitCode = 1;
152
247
  return;
153
248
  }
154
- const { log, page, browser, context: browserContext } = context;
249
+ let answers;
155
250
  try {
156
- log.info(`導航至測驗頁面: ${quizUrl}`);
157
- await page.goto(quizUrl, { waitUntil: "domcontentloaded" });
158
- log.info("瀏覽器已開啟,請手動完成測驗。");
159
- log.info(" Ctrl+C 關閉瀏覽器。");
160
- await new Promise(() => { });
161
- }
162
- finally {
163
- await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
251
+ answers = JSON.parse(answersJson);
252
+ }
253
+ catch {
254
+ apiContext.log.error("Invalid answers JSON. Expected format: [{\"slot\":1,\"answer\":\"0\"},...]");
255
+ process.exitCode = 1;
256
+ return;
257
+ }
258
+ try {
259
+ // Get attempt data to find uniqueid and sequencecheck values
260
+ const attemptData = await (0, moodle_js_1.getAllQuizAttemptDataApi)(apiContext.session, parseInt(attemptId));
261
+ const uniqueId = attemptData.attempt.uniqueid ?? attemptData.attempt.attemptid;
262
+ const sequenceChecks = new Map();
263
+ for (const q of Object.values(attemptData.questions)) {
264
+ if (q.sequencecheck !== undefined) {
265
+ sequenceChecks.set(q.slot, q.sequencecheck);
266
+ }
267
+ }
268
+ const result = await (0, moodle_js_1.processQuizAttemptApi)(apiContext.session, parseInt(attemptId), uniqueId, answers, sequenceChecks, !!options.submit);
269
+ apiContext.log.success(`Attempt ${attemptId} state: ${result.state}`);
270
+ (0, index_js_1.formatAndOutput)([result], output, apiContext.log);
271
+ }
272
+ catch (error) {
273
+ apiContext.log.error(`Failed to submit attempt: ${error instanceof Error ? error.message : String(error)}`);
274
+ process.exitCode = 1;
164
275
  }
165
276
  });
166
277
  }
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6CpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0E5D"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2CpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuE5D"}
@@ -29,12 +29,10 @@ async function readSkillContent() {
29
29
  // When running from source: src/commands/ → ../../skills/openape/SKILL.md
30
30
  // When bundled by dnt into build/: esm/commands/ or script/ → ../../skills/openape/SKILL.md
31
31
  const localPath = node_path_1.default.resolve(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
32
- if (node_fs_1.default.existsSync(localPath)) {
33
- return node_fs_1.default.readFileSync(localPath, "utf-8");
34
- }
32
+ return await node_fs_1.default.promises.readFile(localPath, "utf-8");
35
33
  }
36
34
  catch {
37
- // import.meta.url may be unavailable in some environments
35
+ // import.meta.url may be unavailable in some environments, or file doesn't exist
38
36
  }
39
37
  // Fallback: fetch from GitHub
40
38
  const res = await fetch(GITHUB_RAW_URL, { headers: { "User-Agent": "openape-cli" } });
@@ -87,10 +85,8 @@ function registerSkillsCommand(program) {
87
85
  for (const target of targets) {
88
86
  console.log(`Installing to ${target.name} (${target.path})...`);
89
87
  const destDir = node_path_1.default.join(target.path, SKILL_NAME);
90
- if (!node_fs_1.default.existsSync(destDir)) {
91
- node_fs_1.default.mkdirSync(destDir, { recursive: true });
92
- }
93
- node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, "SKILL.md"), content, "utf-8");
88
+ await node_fs_1.default.promises.mkdir(destDir, { recursive: true });
89
+ await node_fs_1.default.promises.writeFile(node_path_1.default.join(destDir, "SKILL.md"), content, "utf-8");
94
90
  console.log(` \x1b[32m✔\x1b[0m ${SKILL_NAME} installed!`);
95
91
  }
96
92
  console.log("\nDone!");
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerUploadCommand(program: Command): void;
3
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerUploadCommand = registerUploadCommand;
7
+ const utils_js_1 = require("../lib/utils.js");
8
+ const moodle_js_1 = require("../lib/moodle.js");
9
+ const auth_js_1 = require("../lib/auth.js");
10
+ const index_js_1 = require("../index.js");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ function registerUploadCommand(program) {
14
+ const uploadCmd = program.command("upload");
15
+ uploadCmd.description("Upload files to Moodle draft area");
16
+ uploadCmd
17
+ .command("file")
18
+ .description("Upload a file to Moodle draft area")
19
+ .argument("<file-path>", "Path to the file to upload")
20
+ .option("--filename <name>", "Custom filename (default: original filename)")
21
+ .option("--output <format>", "Output format: json|csv|table|silent")
22
+ .action(async (filePath, options, command) => {
23
+ const output = (0, utils_js_1.getOutputFormat)(command);
24
+ const apiContext = await (0, auth_js_1.createApiContext)(options, command);
25
+ if (!apiContext) {
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ // Resolve file path
30
+ const resolvedPath = node_path_1.default.resolve(filePath);
31
+ // Check if file exists
32
+ try {
33
+ await promises_1.default.access(resolvedPath);
34
+ }
35
+ catch {
36
+ apiContext.log.error(`檔案不存在: ${filePath}`);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ // Get file size
41
+ const stats = await promises_1.default.stat(resolvedPath);
42
+ const fileSizeKB = (0, utils_js_1.formatFileSize)(stats.size);
43
+ apiContext.log.info(`上傳檔案: ${node_path_1.default.basename(resolvedPath)} (${fileSizeKB} KB)`);
44
+ // Upload file
45
+ const result = await (0, moodle_js_1.uploadFileApi)(apiContext.session, resolvedPath, {
46
+ filename: options.filename,
47
+ });
48
+ if (!result.success) {
49
+ apiContext.log.error(`上傳失敗: ${result.error}`);
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ apiContext.log.info(`✓ 上傳成功!Draft ID: ${result.draftId}`);
54
+ const uploadResult = {
55
+ success: true,
56
+ draft_id: result.draftId,
57
+ filename: node_path_1.default.basename(resolvedPath),
58
+ filesize: stats.size,
59
+ filesize_kb: (0, utils_js_1.formatFileSize)(stats.size),
60
+ message: "Use this draft ID for assignment submission or forum posts",
61
+ };
62
+ (0, index_js_1.formatAndOutput)(uploadResult, output, apiContext.log);
63
+ });
64
+ }
@@ -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,CA+X5D"}
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"}
@@ -23,19 +23,19 @@ function registerVideosCommand(program) {
23
23
  // Don't silence logs for commands that don't have explicit output format control
24
24
  const outputFormat = command && command.optsWithGlobals ? (0, utils_js_1.getOutputFormat)(command) : "table";
25
25
  const silent = outputFormat === "json" && !opts.verbose;
26
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
26
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
27
27
  const baseDir = (0, utils_js_1.getBaseDir)();
28
28
  const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
29
29
  // Check if session exists
30
30
  if (!node_fs_1.default.existsSync(sessionPath)) {
31
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
31
+ console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
32
32
  log.info(`Session 預期位置: ${sessionPath}`);
33
33
  return null;
34
34
  }
35
35
  // Try to load WS token
36
36
  const wsToken = (0, token_js_1.loadWsToken)(sessionPath);
37
37
  if (!wsToken) {
38
- log.error("未找到 WS token。請先執行 'openape auth login' 進行登入。");
38
+ console.error("未找到 WS token。請先執行 'openape login' 進行登入。");
39
39
  return null;
40
40
  }
41
41
  return {
@@ -51,11 +51,11 @@ function registerVideosCommand(program) {
51
51
  const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
52
52
  const outputFormat = (0, utils_js_1.getOutputFormat)(command || { optsWithGlobals: () => ({ output: "json" }) });
53
53
  const silent = outputFormat === "json" && !opts.verbose;
54
- const log = (0, logger_js_1.createLogger)(opts.verbose, silent);
54
+ const log = (0, logger_js_1.createLogger)(opts.verbose, silent, outputFormat);
55
55
  const baseDir = (0, utils_js_1.getBaseDir)();
56
56
  const sessionPath = node_path_1.default.resolve(baseDir, ".auth", "storage-state.json");
57
57
  if (!node_fs_1.default.existsSync(sessionPath)) {
58
- log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
58
+ console.error("未找到登入 session。請先執行 'openape login' 進行登入。");
59
59
  return null;
60
60
  }
61
61
  const config = {
@@ -322,7 +322,7 @@ function registerVideosCommand(program) {
322
322
  const completed = downloaded.filter(d => d.success).length;
323
323
  const failed = downloaded.filter(d => !d.success).length;
324
324
  log.info(`\n執行結果: ${completed} 成功, ${failed} 失敗`);
325
- const output = {
325
+ console.log(JSON.stringify({
326
326
  status: "success",
327
327
  timestamp: new Date().toISOString(),
328
328
  course_id: courseId,
@@ -330,9 +330,10 @@ function registerVideosCommand(program) {
330
330
  total_videos: videos.length,
331
331
  downloaded: completed,
332
332
  failed,
333
- videos: downloaded,
334
- };
335
- console.log(JSON.stringify(output));
333
+ }));
334
+ for (const v of downloaded) {
335
+ console.log(JSON.stringify(v));
336
+ }
336
337
  }
337
338
  finally {
338
339
  await (0, auth_js_2.closeBrowserSafely)(browser, browserContext);
@@ -1 +1 @@
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;AAoEnF;;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,CAmBN;AA2CD,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC"}
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"}
@@ -27,6 +27,8 @@ const forums_js_1 = require("./commands/forums.js");
27
27
  const announcements_js_1 = require("./commands/announcements.js");
28
28
  const calendar_js_1 = require("./commands/calendar.js");
29
29
  const skills_js_1 = require("./commands/skills.js");
30
+ const assignments_js_1 = require("./commands/assignments.js");
31
+ const upload_js_1 = require("./commands/upload.js");
30
32
  const program = new commander_1.Command();
31
33
  program
32
34
  .name("openape")
@@ -51,6 +53,8 @@ program
51
53
  (0, announcements_js_1.registerAnnouncementsCommand)(program);
52
54
  (0, calendar_js_1.registerCalendarCommand)(program);
53
55
  (0, skills_js_1.registerSkillsCommand)(program);
56
+ (0, assignments_js_1.registerAssignmentsCommand)(program);
57
+ (0, upload_js_1.registerUploadCommand)(program);
54
58
  /**
55
59
  * Load configuration and authenticate, returning the context for commands.
56
60
  */
@@ -93,7 +97,14 @@ async function createSessionContext(options) {
93
97
  */
94
98
  function formatAndOutput(data, format, log) {
95
99
  if (format === "json") {
96
- console.log(JSON.stringify(data));
100
+ if (Array.isArray(data)) {
101
+ for (const item of data) {
102
+ console.log(JSON.stringify(item));
103
+ }
104
+ }
105
+ else {
106
+ console.log(JSON.stringify(data));
107
+ }
97
108
  // Exit immediately for AI agent - no need to wait for browser cleanup
98
109
  process.exit(0);
99
110
  }
@@ -1,5 +1,5 @@
1
1
  import { type Browser, type BrowserContext, type Page } from "playwright-core";
2
- import type { AppConfig, Logger } from "./types.js";
2
+ import type { AppConfig, Logger, OutputFormat } from "./types.js";
3
3
  /**
4
4
  * Find a Chromium-based browser executable on Windows.
5
5
  * Priority: Edge → Chrome → Brave
@@ -20,6 +20,28 @@ export declare function launchAuthenticated(config: AppConfig, log: Logger): Pro
20
20
  * Safely close browser and context with timeout.
21
21
  * Designed for AI agent usage - no human interaction needed.
22
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.
23
26
  */
24
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>;
25
47
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
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,MAAM,YAAY,CAAC;AAGpD;;;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;;;;GAIG;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,CA4Bf"}
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"}