@mo7yw4ng/openape 1.0.2 → 1.0.4
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.
- package/README.md +37 -9
- package/esm/deno.js +1 -1
- package/esm/src/commands/announcements.d.ts.map +1 -1
- package/esm/src/commands/announcements.js +13 -14
- package/esm/src/commands/assignments.d.ts +3 -0
- package/esm/src/commands/assignments.d.ts.map +1 -0
- package/esm/src/commands/assignments.js +230 -0
- package/esm/src/commands/auth.d.ts +1 -1
- package/esm/src/commands/auth.d.ts.map +1 -1
- package/esm/src/commands/auth.js +24 -8
- package/esm/src/commands/calendar.d.ts.map +1 -1
- package/esm/src/commands/calendar.js +17 -18
- package/esm/src/commands/courses.js +3 -3
- package/esm/src/commands/forums.d.ts.map +1 -1
- package/esm/src/commands/forums.js +132 -45
- package/esm/src/commands/materials.d.ts.map +1 -1
- package/esm/src/commands/materials.js +176 -48
- package/esm/src/commands/quizzes.d.ts.map +1 -1
- package/esm/src/commands/quizzes.js +165 -65
- package/esm/src/commands/skills.d.ts.map +1 -1
- package/esm/src/commands/skills.js +4 -8
- package/esm/src/commands/upload.d.ts +3 -0
- package/esm/src/commands/upload.d.ts.map +1 -0
- package/esm/src/commands/upload.js +58 -0
- package/esm/src/commands/videos.d.ts.map +1 -1
- package/esm/src/commands/videos.js +113 -79
- package/esm/src/index.d.ts.map +1 -1
- package/esm/src/index.js +14 -3
- package/esm/src/lib/auth.d.ts +23 -1
- package/esm/src/lib/auth.d.ts.map +1 -1
- package/esm/src/lib/auth.js +36 -3
- package/esm/src/lib/moodle.d.ts +226 -2
- package/esm/src/lib/moodle.d.ts.map +1 -1
- package/esm/src/lib/moodle.js +648 -37
- package/esm/src/lib/types.d.ts +82 -164
- package/esm/src/lib/types.d.ts.map +1 -1
- package/esm/src/lib/types.js +1 -0
- package/esm/src/lib/utils.d.ts +40 -0
- package/esm/src/lib/utils.d.ts.map +1 -1
- package/esm/src/lib/utils.js +82 -4
- package/package.json +1 -2
- package/script/deno.js +1 -1
- package/script/src/commands/announcements.d.ts.map +1 -1
- package/script/src/commands/announcements.js +12 -13
- package/script/src/commands/assignments.d.ts +3 -0
- package/script/src/commands/assignments.d.ts.map +1 -0
- package/script/src/commands/assignments.js +269 -0
- package/script/src/commands/auth.d.ts +1 -1
- package/script/src/commands/auth.d.ts.map +1 -1
- package/script/src/commands/auth.js +24 -8
- package/script/src/commands/calendar.d.ts.map +1 -1
- package/script/src/commands/calendar.js +16 -17
- package/script/src/commands/courses.js +2 -2
- package/script/src/commands/forums.d.ts.map +1 -1
- package/script/src/commands/forums.js +132 -45
- package/script/src/commands/materials.d.ts.map +1 -1
- package/script/src/commands/materials.js +177 -49
- package/script/src/commands/quizzes.d.ts.map +1 -1
- package/script/src/commands/quizzes.js +163 -63
- package/script/src/commands/skills.d.ts.map +1 -1
- package/script/src/commands/skills.js +4 -8
- package/script/src/commands/upload.d.ts +3 -0
- package/script/src/commands/upload.d.ts.map +1 -0
- package/script/src/commands/upload.js +64 -0
- package/script/src/commands/videos.d.ts.map +1 -1
- package/script/src/commands/videos.js +114 -80
- package/script/src/index.d.ts.map +1 -1
- package/script/src/index.js +13 -2
- package/script/src/lib/auth.d.ts +23 -1
- package/script/src/lib/auth.d.ts.map +1 -1
- package/script/src/lib/auth.js +70 -3
- package/script/src/lib/moodle.d.ts +226 -2
- package/script/src/lib/moodle.d.ts.map +1 -1
- package/script/src/lib/moodle.js +663 -37
- package/script/src/lib/types.d.ts +82 -164
- package/script/src/lib/types.d.ts.map +1 -1
- package/script/src/lib/types.js +1 -0
- package/script/src/lib/utils.d.ts +40 -0
- package/script/src/lib/utils.d.ts.map +1 -1
- package/script/src/lib/utils.js +89 -3
- package/skills/openape/SKILL.md +73 -291
|
@@ -1,13 +1,46 @@
|
|
|
1
|
-
import { getBaseDir } from "../lib/utils.js";
|
|
2
|
-
import { getEnrolledCoursesApi, getQuizzesByCoursesApi } from "../lib/moodle.js";
|
|
1
|
+
import { getBaseDir, formatTimestamp } from "../lib/utils.js";
|
|
2
|
+
import { getEnrolledCoursesApi, getQuizzesByCoursesApi, startQuizAttemptApi, getQuizAttemptDataApi, processQuizAttemptApi } from "../lib/moodle.js";
|
|
3
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
4
|
import { formatAndOutput } from "../index.js";
|
|
8
5
|
import { loadWsToken } from "../lib/token.js";
|
|
9
6
|
import path from "node:path";
|
|
10
7
|
import fs from "node:fs";
|
|
8
|
+
function stripHtmlKeepLines(html) {
|
|
9
|
+
return html
|
|
10
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
11
|
+
.replace(/<\/p>/gi, "\n")
|
|
12
|
+
.replace(/<[^>]+>/g, "")
|
|
13
|
+
.replace(/ /g, " ")
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
function parseQuestionHtml(html) {
|
|
21
|
+
const qtextMatch = html.match(/<div class="qtext">([\s\S]*?)<\/div>\s*<\/div>/);
|
|
22
|
+
const text = stripHtmlKeepLines(qtextMatch?.[1] ?? "");
|
|
23
|
+
const options = [];
|
|
24
|
+
const optionRegex = /data-region="answer-label">([\s\S]*?)<\/div>\s*<\/div>/g;
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = optionRegex.exec(html)) !== null) {
|
|
27
|
+
options.push(stripHtmlKeepLines(match[1]));
|
|
28
|
+
}
|
|
29
|
+
return { text, options };
|
|
30
|
+
}
|
|
31
|
+
function parseSavedAnswer(html) {
|
|
32
|
+
const radioChecked = html.match(/<input type="radio"[^>]*value="(\d+)"[^>]*checked="checked"/);
|
|
33
|
+
if (radioChecked && radioChecked[1] !== "-1")
|
|
34
|
+
return radioChecked[1];
|
|
35
|
+
const checkboxChecked = [...html.matchAll(/<input type="checkbox"[^>]*name="[^"]*choice(\d+)"[^>]*checked="checked"/g)];
|
|
36
|
+
if (checkboxChecked.length > 0)
|
|
37
|
+
return checkboxChecked.map(m => m[1]);
|
|
38
|
+
// Match <input> with both name="*_answer" and type="text" in any attribute order
|
|
39
|
+
const textMatch = html.match(/<input[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*(?:name="[^"]*:_answer"|type="text")[^>]*value="([^"]*)"/);
|
|
40
|
+
if (textMatch && textMatch[1] !== "")
|
|
41
|
+
return textMatch[1];
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
11
44
|
export function registerQuizzesCommand(program) {
|
|
12
45
|
const quizzesCmd = program.command("quizzes");
|
|
13
46
|
quizzesCmd.description("Quiz operations");
|
|
@@ -43,45 +76,11 @@ export function registerQuizzesCommand(program) {
|
|
|
43
76
|
},
|
|
44
77
|
};
|
|
45
78
|
}
|
|
46
|
-
// Helper function to create session context (for open command only)
|
|
47
|
-
async function createSessionContext(options, command) {
|
|
48
|
-
const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
|
|
49
|
-
const outputFormat = getOutputFormat(command || { optsWithGlobals: () => ({ output: "json" }) });
|
|
50
|
-
const silent = outputFormat === "json" && !opts.verbose;
|
|
51
|
-
const log = createLogger(opts.verbose, silent);
|
|
52
|
-
const baseDir = getBaseDir();
|
|
53
|
-
const sessionPath = path.resolve(baseDir, ".auth", "storage-state.json");
|
|
54
|
-
if (!fs.existsSync(sessionPath)) {
|
|
55
|
-
log.error("未找到登入 session。請先執行 'openape auth login' 進行登入。");
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const config = {
|
|
59
|
-
username: "",
|
|
60
|
-
password: "",
|
|
61
|
-
courseUrl: "",
|
|
62
|
-
moodleBaseUrl: "https://ilearning.cycu.edu.tw",
|
|
63
|
-
headless: !options.headed,
|
|
64
|
-
slowMo: 0,
|
|
65
|
-
authStatePath: sessionPath,
|
|
66
|
-
ollamaBaseUrl: "",
|
|
67
|
-
};
|
|
68
|
-
log.info("啟動瀏覽器...");
|
|
69
|
-
const { browser, context, page } = await launchAuthenticated(config, log);
|
|
70
|
-
try {
|
|
71
|
-
const session = await extractSessionInfo(page, config, log);
|
|
72
|
-
return { log, page, session, browser, context };
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
await context.close();
|
|
76
|
-
await browser.close();
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
79
|
quizzesCmd
|
|
81
80
|
.command("list")
|
|
82
|
-
.description("List quizzes in a course")
|
|
81
|
+
.description("List incomplete quizzes in a course")
|
|
83
82
|
.argument("<course-id>", "Course ID")
|
|
84
|
-
.option("--
|
|
83
|
+
.option("--all", "Include completed quizzes")
|
|
85
84
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
86
85
|
.action(async (courseId, options, command) => {
|
|
87
86
|
const output = getOutputFormat(command);
|
|
@@ -91,16 +90,19 @@ export function registerQuizzesCommand(program) {
|
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
92
|
const quizzes = await getQuizzesByCoursesApi(apiContext.session, [parseInt(courseId, 10)]);
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
// Default: only show incomplete quizzes
|
|
94
|
+
const filtered = options.all ? quizzes : quizzes.filter(q => !q.isComplete);
|
|
95
|
+
const formattedQuizzes = filtered.map(({ courseId, ...q }) => ({
|
|
96
|
+
...q,
|
|
97
|
+
timeClose: q.timeClose ? formatTimestamp(q.timeClose) : null,
|
|
98
|
+
}));
|
|
99
|
+
formatAndOutput(formattedQuizzes, output, apiContext.log);
|
|
99
100
|
});
|
|
100
101
|
quizzesCmd
|
|
101
102
|
.command("list-all")
|
|
102
|
-
.description("List all
|
|
103
|
+
.description("List all incomplete quizzes across all courses")
|
|
103
104
|
.option("--level <type>", "Course level: in_progress (default) | all", "in_progress")
|
|
105
|
+
.option("--all", "Include completed quizzes")
|
|
104
106
|
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
105
107
|
.action(async (options, command) => {
|
|
106
108
|
const output = getOutputFormat(command);
|
|
@@ -121,13 +123,17 @@ export function registerQuizzesCommand(program) {
|
|
|
121
123
|
const allQuizzes = [];
|
|
122
124
|
for (const q of apiQuizzes) {
|
|
123
125
|
const course = courseMap.get(q.courseId);
|
|
124
|
-
if (course) {
|
|
126
|
+
if (course && (options.all || !q.isComplete)) {
|
|
125
127
|
allQuizzes.push({
|
|
126
128
|
courseName: course.fullname,
|
|
129
|
+
courseId: q.courseId,
|
|
127
130
|
name: q.name,
|
|
128
131
|
url: q.url,
|
|
129
|
-
|
|
132
|
+
quizid: q.quizid,
|
|
130
133
|
isComplete: q.isComplete,
|
|
134
|
+
attemptsUsed: q.attemptsUsed,
|
|
135
|
+
maxAttempts: q.maxAttempts,
|
|
136
|
+
timeClose: q.timeClose ? formatTimestamp(q.timeClose) : null,
|
|
131
137
|
});
|
|
132
138
|
}
|
|
133
139
|
}
|
|
@@ -135,26 +141,120 @@ export function registerQuizzesCommand(program) {
|
|
|
135
141
|
formatAndOutput(allQuizzes, output, apiContext.log);
|
|
136
142
|
});
|
|
137
143
|
quizzesCmd
|
|
138
|
-
.command("
|
|
139
|
-
.description("
|
|
140
|
-
.argument("<quiz-
|
|
141
|
-
.option("--
|
|
142
|
-
.action(async (
|
|
143
|
-
const
|
|
144
|
-
|
|
144
|
+
.command("start")
|
|
145
|
+
.description("Start a new quiz attempt")
|
|
146
|
+
.argument("<quiz-id>", "Quiz ID")
|
|
147
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
148
|
+
.action(async (quizCmid, options, command) => {
|
|
149
|
+
const output = getOutputFormat(command);
|
|
150
|
+
const apiContext = await createApiContext(options, command);
|
|
151
|
+
if (!apiContext) {
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = await startQuizAttemptApi(apiContext.session, quizCmid);
|
|
157
|
+
const outputData = [{
|
|
158
|
+
attemptId: result.attempt.attemptid,
|
|
159
|
+
quizId: result.attempt.quizid,
|
|
160
|
+
state: result.attempt.state,
|
|
161
|
+
timeStart: formatTimestamp(result.attempt.timestart),
|
|
162
|
+
timeFinish: result.attempt.timefinish
|
|
163
|
+
? formatTimestamp(result.attempt.timefinish)
|
|
164
|
+
: null,
|
|
165
|
+
isPreview: result.attempt.preview,
|
|
166
|
+
}];
|
|
167
|
+
apiContext.log.success(`Quiz attempt ${result.attempt.attemptid} started.`);
|
|
168
|
+
formatAndOutput(outputData, output, apiContext.log);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
apiContext.log.error(`Failed to start quiz attempt: ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
quizzesCmd
|
|
176
|
+
.command("info")
|
|
177
|
+
.description("Get quiz attempt data and questions")
|
|
178
|
+
.argument("<attempt-id>", "Quiz attempt ID")
|
|
179
|
+
.option("--page <number>", "Page number", "0")
|
|
180
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
181
|
+
.action(async (attemptId, options, command) => {
|
|
182
|
+
const output = getOutputFormat(command);
|
|
183
|
+
const apiContext = await createApiContext(options, command);
|
|
184
|
+
if (!apiContext) {
|
|
145
185
|
process.exitCode = 1;
|
|
146
186
|
return;
|
|
147
187
|
}
|
|
148
|
-
const { log, page, browser, context: browserContext } = context;
|
|
149
188
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
const data = await getQuizAttemptDataApi(apiContext.session, parseInt(attemptId), parseInt(options.page));
|
|
190
|
+
const questions = Object.values(data.questions).map((q) => {
|
|
191
|
+
const parsed = parseQuestionHtml(q.html ?? "");
|
|
192
|
+
const savedAnswer = parseSavedAnswer(q.html ?? "");
|
|
193
|
+
return {
|
|
194
|
+
number: q.questionnumber ?? q.slot,
|
|
195
|
+
type: q.type,
|
|
196
|
+
status: q.status,
|
|
197
|
+
stateclass: q.stateclass,
|
|
198
|
+
savedAnswer,
|
|
199
|
+
question: parsed.text,
|
|
200
|
+
options: parsed.options,
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
const outputData = [{
|
|
204
|
+
attemptId: data.attempt.attemptid,
|
|
205
|
+
quizId: data.attempt.quizid,
|
|
206
|
+
state: data.attempt.state,
|
|
207
|
+
totalQuestions: questions.length,
|
|
208
|
+
questions,
|
|
209
|
+
}];
|
|
210
|
+
apiContext.log.success(`Retrieved attempt ${data.attempt.attemptid}`);
|
|
211
|
+
formatAndOutput(outputData, output, apiContext.log);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
apiContext.log.error(`Failed to get attempt data: ${error instanceof Error ? error.message : String(error)}`);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
quizzesCmd
|
|
219
|
+
.command("save")
|
|
220
|
+
.description("Save answers for a quiz attempt")
|
|
221
|
+
.argument("<attempt-id>", "Quiz attempt ID")
|
|
222
|
+
.argument("<answers>", "Answers JSON: [{slot:1,answer:\"0\"}] multichoice=number, multichoices=\"0,2\", shortanswer=\"text\"")
|
|
223
|
+
.option("--submit", "Submit the attempt after saving")
|
|
224
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
225
|
+
.action(async (attemptId, answersJson, options, command) => {
|
|
226
|
+
const output = getOutputFormat(command);
|
|
227
|
+
const apiContext = await createApiContext(options, command);
|
|
228
|
+
if (!apiContext) {
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let answers;
|
|
233
|
+
try {
|
|
234
|
+
answers = JSON.parse(answersJson);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
apiContext.log.error("Invalid answers JSON. Expected format: [{\"slot\":1,\"answer\":\"0\"},...]");
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
// Get attempt data to find uniqueid and sequencecheck values
|
|
243
|
+
const attemptData = await getQuizAttemptDataApi(apiContext.session, parseInt(attemptId), 0);
|
|
244
|
+
const uniqueId = attemptData.attempt.uniqueid ?? attemptData.attempt.attemptid;
|
|
245
|
+
const sequenceChecks = new Map();
|
|
246
|
+
for (const q of Object.values(attemptData.questions)) {
|
|
247
|
+
if (q.sequencecheck !== undefined) {
|
|
248
|
+
sequenceChecks.set(q.slot, q.sequencecheck);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const result = await processQuizAttemptApi(apiContext.session, parseInt(attemptId), uniqueId, answers, sequenceChecks, !!options.submit);
|
|
252
|
+
apiContext.log.success(`Attempt ${attemptId} state: ${result.state}`);
|
|
253
|
+
formatAndOutput([result], output, apiContext.log);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
apiContext.log.error(`Failed to submit attempt: ${error instanceof Error ? error.message : String(error)}`);
|
|
257
|
+
process.exitCode = 1;
|
|
158
258
|
}
|
|
159
259
|
});
|
|
160
260
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -23,12 +23,10 @@ async function readSkillContent() {
|
|
|
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
25
|
const localPath = path.resolve(normalized, "..", "..", "skills", SKILL_NAME, "SKILL.md");
|
|
26
|
-
|
|
27
|
-
return fs.readFileSync(localPath, "utf-8");
|
|
28
|
-
}
|
|
26
|
+
return await fs.promises.readFile(localPath, "utf-8");
|
|
29
27
|
}
|
|
30
28
|
catch {
|
|
31
|
-
// import.meta.url may be unavailable in some environments
|
|
29
|
+
// import.meta.url may be unavailable in some environments, or file doesn't exist
|
|
32
30
|
}
|
|
33
31
|
// Fallback: fetch from GitHub
|
|
34
32
|
const res = await fetch(GITHUB_RAW_URL, { headers: { "User-Agent": "openape-cli" } });
|
|
@@ -81,10 +79,8 @@ export function registerSkillsCommand(program) {
|
|
|
81
79
|
for (const target of targets) {
|
|
82
80
|
console.log(`Installing to ${target.name} (${target.path})...`);
|
|
83
81
|
const destDir = path.join(target.path, SKILL_NAME);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
fs.writeFileSync(path.join(destDir, "SKILL.md"), content, "utf-8");
|
|
82
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
83
|
+
await fs.promises.writeFile(path.join(destDir, "SKILL.md"), content, "utf-8");
|
|
88
84
|
console.log(` \x1b[32m✔\x1b[0m ${SKILL_NAME} installed!`);
|
|
89
85
|
}
|
|
90
86
|
console.log("\nDone!");
|
|
@@ -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,58 @@
|
|
|
1
|
+
import { getOutputFormat, formatFileSize } from "../lib/utils.js";
|
|
2
|
+
import { uploadFileApi } from "../lib/moodle.js";
|
|
3
|
+
import { createApiContext } from "../lib/auth.js";
|
|
4
|
+
import { formatAndOutput } from "../index.js";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
export function registerUploadCommand(program) {
|
|
8
|
+
const uploadCmd = program.command("upload");
|
|
9
|
+
uploadCmd.description("Upload files to Moodle draft area");
|
|
10
|
+
uploadCmd
|
|
11
|
+
.command("file")
|
|
12
|
+
.description("Upload a file to Moodle draft area")
|
|
13
|
+
.argument("<file-path>", "Path to the file to upload")
|
|
14
|
+
.option("--filename <name>", "Custom filename (default: original filename)")
|
|
15
|
+
.option("--output <format>", "Output format: json|csv|table|silent")
|
|
16
|
+
.action(async (filePath, options, command) => {
|
|
17
|
+
const output = getOutputFormat(command);
|
|
18
|
+
const apiContext = await createApiContext(options, command);
|
|
19
|
+
if (!apiContext) {
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Resolve file path
|
|
24
|
+
const resolvedPath = path.resolve(filePath);
|
|
25
|
+
// Check if file exists
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(resolvedPath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
apiContext.log.error(`檔案不存在: ${filePath}`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Get file size
|
|
35
|
+
const stats = await fs.stat(resolvedPath);
|
|
36
|
+
const fileSizeKB = formatFileSize(stats.size);
|
|
37
|
+
apiContext.log.info(`上傳檔案: ${path.basename(resolvedPath)} (${fileSizeKB} KB)`);
|
|
38
|
+
// Upload file
|
|
39
|
+
const result = await uploadFileApi(apiContext.session, resolvedPath, {
|
|
40
|
+
filename: options.filename,
|
|
41
|
+
});
|
|
42
|
+
if (!result.success) {
|
|
43
|
+
apiContext.log.error(`上傳失敗: ${result.error}`);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
apiContext.log.info(`✓ 上傳成功!Draft ID: ${result.draftId}`);
|
|
48
|
+
const uploadResult = {
|
|
49
|
+
success: true,
|
|
50
|
+
draft_id: result.draftId,
|
|
51
|
+
filename: path.basename(resolvedPath),
|
|
52
|
+
filesize: stats.size,
|
|
53
|
+
filesize_kb: formatFileSize(stats.size),
|
|
54
|
+
message: "Use this draft ID for assignment submission or forum posts",
|
|
55
|
+
};
|
|
56
|
+
formatAndOutput(uploadResult, output, apiContext.log);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -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,
|
|
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"}
|