moodle-cli 0.7.0-alpha.3 → 0.7.0-alpha.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 +1 -1
- package/dist/moodle.js +222 -95
- package/dist/worker/worker.js +47 -3
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -174,7 +174,7 @@ moodle mcp bridge
|
|
|
174
174
|
|
|
175
175
|
The default client connection uses `moodle mcp bridge`, which keeps the Bearer token out of client configuration. Use `moodle mcp connect CLIENT --mode remote` for clients that support authenticated remote MCP headers.
|
|
176
176
|
|
|
177
|
-
Alpha version `0.7.0-alpha.
|
|
177
|
+
Alpha version `0.7.0-alpha.4` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
|
|
178
178
|
|
|
179
179
|
### Configuration
|
|
180
180
|
|
package/dist/moodle.js
CHANGED
|
@@ -98,7 +98,7 @@ var MoodleAPIError = class extends CliError {
|
|
|
98
98
|
moodleErrorCode;
|
|
99
99
|
constructor(message, moodleErrorCode) {
|
|
100
100
|
const auth = isLoginErrorCode(moodleErrorCode);
|
|
101
|
-
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") ||
|
|
101
|
+
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /\bHTTP 404\b/.test(message);
|
|
102
102
|
super(auth ? "auth" : notFound ? "not_found" : "upstream", message, auth ? "Run `moodle auth login`." : void 0);
|
|
103
103
|
this.moodleErrorCode = moodleErrorCode;
|
|
104
104
|
}
|
|
@@ -898,6 +898,32 @@ function htmlText(value, baseUrl) {
|
|
|
898
898
|
|
|
899
899
|
// src/scraper.ts
|
|
900
900
|
import { parse as parse2 } from "node-html-parser";
|
|
901
|
+
function parseMoodleErrorHtml(html) {
|
|
902
|
+
const root = parse2(html);
|
|
903
|
+
const messageNode = first(root, [
|
|
904
|
+
".errormessage",
|
|
905
|
+
".alert-danger .alert-message",
|
|
906
|
+
"[data-region='error-message']",
|
|
907
|
+
".alert-danger[role='alert']",
|
|
908
|
+
".alert-danger"
|
|
909
|
+
]);
|
|
910
|
+
if (!messageNode) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
const messageRoot = parse2(messageNode.toString());
|
|
914
|
+
for (const unwanted of messageRoot.querySelectorAll(
|
|
915
|
+
"button, .close, .errorcode, .stacktrace, .debuginfo, .backtrace, a.alert-link, a[href*='/error/']"
|
|
916
|
+
)) {
|
|
917
|
+
unwanted.remove();
|
|
918
|
+
}
|
|
919
|
+
const message = cleanText(messageRoot.textContent);
|
|
920
|
+
if (!message) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
const errorCodeText = cleanNodeText(root.querySelector(".errorcode"));
|
|
924
|
+
const errorCode = errorCodeText.match(/^error\s+code\s*:\s*([a-z][a-z0-9_]*)\s*$/iu)?.[1] ?? moodleDocsErrorCode(root);
|
|
925
|
+
return { message, ...errorCode ? { code: errorCode } : {} };
|
|
926
|
+
}
|
|
901
927
|
function parsePageContext(html, baseUrl) {
|
|
902
928
|
const root = parse2(html);
|
|
903
929
|
const config = parseMoodleConfig(html);
|
|
@@ -1329,6 +1355,16 @@ function selectedGroupName(root, groupId) {
|
|
|
1329
1355
|
}
|
|
1330
1356
|
return "";
|
|
1331
1357
|
}
|
|
1358
|
+
function moodleDocsErrorCode(root) {
|
|
1359
|
+
for (const link2 of root.querySelectorAll("a[href*='/error/']")) {
|
|
1360
|
+
const href = link2.getAttribute("href") ?? "";
|
|
1361
|
+
const code = href.match(/\/error\/[^/]+\/([a-z][a-z0-9_]*)/iu)?.[1];
|
|
1362
|
+
if (code) {
|
|
1363
|
+
return code;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return void 0;
|
|
1367
|
+
}
|
|
1332
1368
|
function cleanNodeText(node) {
|
|
1333
1369
|
return cleanText(node?.textContent ?? "");
|
|
1334
1370
|
}
|
|
@@ -1818,7 +1854,7 @@ var MoodleClientCoreApiError = class extends MoodleClientCoreError {
|
|
|
1818
1854
|
moodleErrorCode;
|
|
1819
1855
|
constructor(message, moodleErrorCode) {
|
|
1820
1856
|
const auth = isLoginErrorCode2(moodleErrorCode);
|
|
1821
|
-
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") ||
|
|
1857
|
+
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /\bHTTP 404\b/.test(message);
|
|
1822
1858
|
super(
|
|
1823
1859
|
auth ? "auth" : notFound ? "not_found" : "upstream",
|
|
1824
1860
|
message,
|
|
@@ -2249,7 +2285,15 @@ var MoodleClientCore = class {
|
|
|
2249
2285
|
throw this.errors.api("Session expired", "servicerequireslogin");
|
|
2250
2286
|
}
|
|
2251
2287
|
if (!response.ok) {
|
|
2252
|
-
|
|
2288
|
+
const context = `HTTP ${response.status} loading ${safeUrl(url)}`;
|
|
2289
|
+
const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
2290
|
+
if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
|
|
2291
|
+
const moodleError = await response.text().then(parseMoodleErrorHtml).catch(() => null);
|
|
2292
|
+
if (moodleError) {
|
|
2293
|
+
throw this.errors.api(`${moodleError.message} (${context})`, moodleError.code);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
throw this.errors.api(context);
|
|
2253
2297
|
}
|
|
2254
2298
|
return response;
|
|
2255
2299
|
}
|
|
@@ -2715,84 +2759,163 @@ function isMissingFileError2(error) {
|
|
|
2715
2759
|
return isRecord5(error) && error.code === "ENOENT";
|
|
2716
2760
|
}
|
|
2717
2761
|
|
|
2762
|
+
// src/terminal-table.ts
|
|
2763
|
+
import Table from "tty-table";
|
|
2764
|
+
var DEFAULT_WIDTH = 120;
|
|
2765
|
+
var MIN_TABLE_WIDTH = 40;
|
|
2766
|
+
function renderTerminalTable(columns, rows, options = {}) {
|
|
2767
|
+
const tableWidth = Math.max(MIN_TABLE_WIDTH, options.width ?? process.stdout.columns ?? DEFAULT_WIDTH);
|
|
2768
|
+
const tableColumns = columns.map((column, index) => ({
|
|
2769
|
+
align: "left",
|
|
2770
|
+
alias: sanitizeTerminalText(column.label),
|
|
2771
|
+
headerAlign: "left",
|
|
2772
|
+
value: `column_${index}`
|
|
2773
|
+
}));
|
|
2774
|
+
const data = rows.map((row) => Object.fromEntries(
|
|
2775
|
+
tableColumns.map((column, index) => [column.value, sanitizeTerminalText(row[index] ?? "")])
|
|
2776
|
+
));
|
|
2777
|
+
const tableOptions = {
|
|
2778
|
+
COLUMNS: tableWidth,
|
|
2779
|
+
compact: true,
|
|
2780
|
+
marginLeft: 0,
|
|
2781
|
+
marginTop: 0,
|
|
2782
|
+
width: String(tableWidth)
|
|
2783
|
+
};
|
|
2784
|
+
const output = Table(tableColumns, data, tableOptions).render();
|
|
2785
|
+
return [options.title ? sanitizeTerminalText(options.title) : "", output].filter(Boolean).join("\n");
|
|
2786
|
+
}
|
|
2787
|
+
function renderKeyValueTable(rows, options = {}) {
|
|
2788
|
+
const present = rows.filter(([, value]) => value !== "");
|
|
2789
|
+
if (present.length === 0) {
|
|
2790
|
+
return options.title ? `${options.title}
|
|
2791
|
+
No details` : "No details";
|
|
2792
|
+
}
|
|
2793
|
+
const columns = [
|
|
2794
|
+
{ label: "Field" },
|
|
2795
|
+
{ label: "Value" }
|
|
2796
|
+
];
|
|
2797
|
+
return renderTerminalTable(columns, present, options);
|
|
2798
|
+
}
|
|
2799
|
+
function sanitizeTerminalText(value) {
|
|
2800
|
+
return value.replace(/\r\n?/gu, "\n").replace(/\t/gu, " ").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "");
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2718
2803
|
// src/formatters.ts
|
|
2719
2804
|
function formatUser(user) {
|
|
2720
|
-
return
|
|
2805
|
+
return renderKeyValueTable([
|
|
2721
2806
|
["User", user.fullname],
|
|
2722
2807
|
["Username", user.username],
|
|
2723
2808
|
["User ID", String(user.userid)],
|
|
2724
2809
|
["Site", user.sitename],
|
|
2725
2810
|
["URL", user.siteurl],
|
|
2726
2811
|
["Language", user.lang ?? ""]
|
|
2727
|
-
]);
|
|
2812
|
+
], { title: "User" });
|
|
2728
2813
|
}
|
|
2729
2814
|
function formatCourses(courses) {
|
|
2730
|
-
return
|
|
2815
|
+
return renderTerminalTable(
|
|
2816
|
+
[
|
|
2817
|
+
{ label: "ID" },
|
|
2818
|
+
{ label: "Short Name" },
|
|
2819
|
+
{ label: "Full Name" }
|
|
2820
|
+
],
|
|
2821
|
+
courses.map((course) => [String(course.id), course.shortname, course.fullname]),
|
|
2822
|
+
{ title: "Enrolled Units" }
|
|
2823
|
+
);
|
|
2731
2824
|
}
|
|
2732
2825
|
function formatCourseSections(sections) {
|
|
2733
2826
|
const lines = ["Course"];
|
|
2734
|
-
for (const section of sections) {
|
|
2735
|
-
|
|
2827
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
2828
|
+
const sectionLast = sectionIndex === sections.length - 1;
|
|
2829
|
+
lines.push(`${sectionLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500"} ${section.name || `Section ${section.section}`}${section.visible ? "" : " (hidden)"}`);
|
|
2736
2830
|
if (!section.activities.length) {
|
|
2737
|
-
lines.push(" No activities
|
|
2831
|
+
lines.push(`${sectionLast ? " " : "\u2502 "}\u2514\u2500\u2500 No activities`);
|
|
2738
2832
|
continue;
|
|
2739
2833
|
}
|
|
2740
|
-
for (const activity of section.activities) {
|
|
2741
|
-
|
|
2834
|
+
for (const [activityIndex, activity] of section.activities.entries()) {
|
|
2835
|
+
const activityPrefix = activityIndex === section.activities.length - 1 ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
|
|
2836
|
+
lines.push(`${sectionLast ? " " : "\u2502 "}${activityPrefix} ${activity.name}${activity.visible ? "" : " (hidden)"} (${activity.modname})`);
|
|
2742
2837
|
}
|
|
2743
2838
|
}
|
|
2744
|
-
return lines.join("\n");
|
|
2839
|
+
return sanitizeTerminalText(lines.join("\n"));
|
|
2745
2840
|
}
|
|
2746
2841
|
function formatActivityList(value) {
|
|
2747
2842
|
const activities = Array.isArray(value) && value[0] && "activities" in value[0] ? value.flatMap((section) => section.activities) : value;
|
|
2748
|
-
return
|
|
2843
|
+
return renderTerminalTable(
|
|
2844
|
+
[
|
|
2845
|
+
{ label: "ID" },
|
|
2846
|
+
{ label: "Type" },
|
|
2847
|
+
{ label: "Name" }
|
|
2848
|
+
],
|
|
2849
|
+
activities.map((activity) => [String(activity.id), activity.modname, activity.name]),
|
|
2850
|
+
{ title: "Activities" }
|
|
2851
|
+
);
|
|
2749
2852
|
}
|
|
2750
2853
|
function formatTodo(items) {
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2854
|
+
const columns = [
|
|
2855
|
+
{ label: "Due" },
|
|
2856
|
+
{ label: "Unit" },
|
|
2857
|
+
{ label: "Activity" },
|
|
2858
|
+
{ label: "Type" },
|
|
2859
|
+
{ label: "Action" }
|
|
2860
|
+
];
|
|
2861
|
+
const rows = items.length ? items.map((item) => [
|
|
2862
|
+
`${item.overdue ? "Overdue \xB7 " : ""}${formatTimestamp(item.due_at)}`,
|
|
2863
|
+
`${item.course_name}${item.course_progress === void 0 ? "" : ` (${item.course_progress}%)`}`,
|
|
2864
|
+
item.activity_name || item.name,
|
|
2865
|
+
item.modname || item.event_type,
|
|
2866
|
+
item.actionable ? item.action_name : ""
|
|
2867
|
+
]) : [["No upcoming items", "", "", "", ""]];
|
|
2868
|
+
return renderTerminalTable(columns, rows, { title: "Todo" });
|
|
2764
2869
|
}
|
|
2765
2870
|
function formatAlerts(alerts) {
|
|
2766
|
-
const
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
];
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2871
|
+
const summary = renderKeyValueTable([
|
|
2872
|
+
["Notifications", String(alerts.notification_count)],
|
|
2873
|
+
["Unread notifications", String(alerts.unread_notification_count)],
|
|
2874
|
+
["Direct messages", String(alerts.direct_message_count)],
|
|
2875
|
+
["Unread direct messages", String(alerts.unread_direct_message_count)]
|
|
2876
|
+
], { title: "Alerts" });
|
|
2877
|
+
if (!alerts.notifications.length) return summary;
|
|
2878
|
+
const notifications = renderTerminalTable(
|
|
2879
|
+
[
|
|
2880
|
+
{ label: "When" },
|
|
2881
|
+
{ label: "Subject" }
|
|
2882
|
+
],
|
|
2883
|
+
alerts.notifications.map((notification) => [
|
|
2884
|
+
notification.created_pretty || formatTimestamp(notification.created_at),
|
|
2885
|
+
notification.short_subject || notification.subject
|
|
2886
|
+
]),
|
|
2887
|
+
{ title: "Notifications" }
|
|
2888
|
+
);
|
|
2889
|
+
return `${summary}
|
|
2890
|
+
|
|
2891
|
+
${notifications}`;
|
|
2776
2892
|
}
|
|
2777
2893
|
function formatGrades(grades) {
|
|
2778
|
-
return
|
|
2779
|
-
[
|
|
2780
|
-
|
|
2781
|
-
|
|
2894
|
+
return renderTerminalTable(
|
|
2895
|
+
[
|
|
2896
|
+
{ label: "Item" },
|
|
2897
|
+
{ label: "Grade" },
|
|
2898
|
+
{ label: "Range" },
|
|
2899
|
+
{ label: "Percent" },
|
|
2900
|
+
{ label: "Feedback" }
|
|
2901
|
+
],
|
|
2902
|
+
grades.items.map((item) => [item.name, item.grade, item.range, item.percentage, item.feedback]),
|
|
2903
|
+
{ title: grades.course_name ? `Grades \xB7 ${grades.course_name}` : "Grades" }
|
|
2904
|
+
);
|
|
2782
2905
|
}
|
|
2783
2906
|
function formatActivityDetail(activity) {
|
|
2784
2907
|
const rows = Object.entries(activity).filter(([, value]) => value !== "" && value !== void 0 && !(Array.isArray(value) && value.length === 0)).map(([key, value]) => [key, Array.isArray(value) ? value.join("\n") : String(value)]);
|
|
2785
|
-
return
|
|
2908
|
+
return renderKeyValueTable(rows, { title: "Activity" });
|
|
2786
2909
|
}
|
|
2787
2910
|
function formatDownloadReceipt(receipt) {
|
|
2788
|
-
return
|
|
2911
|
+
return renderKeyValueTable([
|
|
2789
2912
|
["File", receipt.file_path],
|
|
2790
2913
|
["Filename", receipt.filename],
|
|
2791
2914
|
["Bytes", String(receipt.bytes_written)],
|
|
2792
2915
|
["Content type", receipt.content_type],
|
|
2793
2916
|
["Source", receipt.source_url],
|
|
2794
2917
|
["Final URL", receipt.final_url]
|
|
2795
|
-
]);
|
|
2918
|
+
], { title: "Download" });
|
|
2796
2919
|
}
|
|
2797
2920
|
function formatForumDiscussion(discussion, options = {}) {
|
|
2798
2921
|
const lines = [`Discussion: ${discussion.id}`];
|
|
@@ -2810,13 +2933,13 @@ function formatForumDiscussion(discussion, options = {}) {
|
|
|
2810
2933
|
}
|
|
2811
2934
|
if (!discussion.posts.length) {
|
|
2812
2935
|
lines.push("", "No posts");
|
|
2813
|
-
return lines.join("\n");
|
|
2936
|
+
return sanitizeTerminalText(lines.join("\n"));
|
|
2814
2937
|
}
|
|
2815
2938
|
for (const post of discussion.posts) {
|
|
2816
2939
|
const marker = options.highlightPostId === post.id ? "*" : "-";
|
|
2817
2940
|
lines.push("", `${marker} Post ${post.id}`);
|
|
2818
2941
|
lines.push(` Author: ${post.author.fullname || "-"}`);
|
|
2819
|
-
lines.push(` When: ${post.created_pretty || (post.time_created
|
|
2942
|
+
lines.push(` When: ${post.created_pretty || formatTimestamp(post.time_created)}`);
|
|
2820
2943
|
if (post.subject) {
|
|
2821
2944
|
lines.push(` Subject: ${post.subject}`);
|
|
2822
2945
|
}
|
|
@@ -2835,58 +2958,68 @@ function formatForumDiscussion(discussion, options = {}) {
|
|
|
2835
2958
|
lines.push(` Images: ${post.image_urls.length}`);
|
|
2836
2959
|
}
|
|
2837
2960
|
}
|
|
2838
|
-
return lines.join("\n");
|
|
2961
|
+
return sanitizeTerminalText(lines.join("\n"));
|
|
2839
2962
|
}
|
|
2840
2963
|
function formatForumDiscussionRefs(forumCmid, refs) {
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2964
|
+
return renderTerminalTable(
|
|
2965
|
+
[
|
|
2966
|
+
{ label: "ID" },
|
|
2967
|
+
{ label: "Subject" },
|
|
2968
|
+
{ label: "Group" },
|
|
2969
|
+
{ label: "URL" }
|
|
2970
|
+
],
|
|
2971
|
+
refs.length ? refs.map((ref) => [String(ref.id), ref.subject, ref.group_name, ref.url]) : [["No discussions", "", "", ""]],
|
|
2972
|
+
{ title: `Forum ${forumCmid} \xB7 Discussions` }
|
|
2973
|
+
);
|
|
2850
2974
|
}
|
|
2851
2975
|
function formatForumActivities(forums) {
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2976
|
+
return renderTerminalTable(
|
|
2977
|
+
[
|
|
2978
|
+
{ label: "ID" },
|
|
2979
|
+
{ label: "Forum" },
|
|
2980
|
+
{ label: "Unit" },
|
|
2981
|
+
{ label: "URL" }
|
|
2982
|
+
],
|
|
2983
|
+
forums.length ? forums.map((forum) => [String(forum.id), forum.name, forum.course_name, forum.url]) : [["No forums", "", "", ""]],
|
|
2984
|
+
{ title: "Forums" }
|
|
2985
|
+
);
|
|
2859
2986
|
}
|
|
2860
2987
|
function formatForumSearchHits(hits) {
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2988
|
+
return renderTerminalTable(
|
|
2989
|
+
[
|
|
2990
|
+
{ label: "Discussion" },
|
|
2991
|
+
{ label: "Post" },
|
|
2992
|
+
{ label: "Unit" },
|
|
2993
|
+
{ label: "Forum" },
|
|
2994
|
+
{ label: "Subject" },
|
|
2995
|
+
{ label: "Author" },
|
|
2996
|
+
{ label: "Match" },
|
|
2997
|
+
{ label: "Snippet" },
|
|
2998
|
+
{ label: "URL" }
|
|
2999
|
+
],
|
|
3000
|
+
hits.length ? hits.map((hit) => [
|
|
3001
|
+
String(hit.discussion_id),
|
|
3002
|
+
String(hit.post_id),
|
|
3003
|
+
hit.course_name,
|
|
3004
|
+
hit.forum_name,
|
|
3005
|
+
hit.discussion_subject,
|
|
3006
|
+
hit.author_name,
|
|
3007
|
+
hit.matched_in,
|
|
3008
|
+
hit.snippet || hit.discussion_subject,
|
|
3009
|
+
hit.url
|
|
3010
|
+
]) : [["No matches", "", "", "", "", "", "", "", ""]],
|
|
3011
|
+
{ title: "Forum Search" }
|
|
3012
|
+
);
|
|
2880
3013
|
}
|
|
2881
3014
|
function formatAuthStatus(status) {
|
|
2882
|
-
return
|
|
3015
|
+
return renderKeyValueTable([
|
|
2883
3016
|
["Site", status.base_url],
|
|
2884
3017
|
["Cached session", status.session_cached ? "yes" : "no"],
|
|
2885
3018
|
["Cache age", status.cache_age_minutes === null ? "" : `${status.cache_age_minutes} min`],
|
|
2886
3019
|
["Session alive", status.session_alive === null ? status.session_cached ? "unknown" : "" : status.session_alive ? "yes" : "no"],
|
|
2887
3020
|
["Server timeout in", formatDuration(status.session_time_remaining_seconds)],
|
|
2888
3021
|
["Keepalive agent", status.keepalive_installed ? `installed (${status.keepalive_plist_path})` : "not installed"]
|
|
2889
|
-
]);
|
|
3022
|
+
], { title: "Authentication" });
|
|
2890
3023
|
}
|
|
2891
3024
|
function formatKeepaliveResult(result) {
|
|
2892
3025
|
switch (result.status) {
|
|
@@ -2919,17 +3052,11 @@ function preview(value, maxLen = 100) {
|
|
|
2919
3052
|
const cleaned = value.split(/\s+/).filter(Boolean).join(" ");
|
|
2920
3053
|
return cleaned.length <= maxLen ? cleaned : `${cleaned.slice(0, maxLen - 1)}\u2026`;
|
|
2921
3054
|
}
|
|
2922
|
-
function
|
|
2923
|
-
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
}
|
|
2927
|
-
function formatColumns(rows) {
|
|
2928
|
-
if (!rows.length) {
|
|
2929
|
-
return "";
|
|
2930
|
-
}
|
|
2931
|
-
const widths = rows[0].map((_, index) => Math.max(...rows.map((row) => (row[index] ?? "").length)));
|
|
2932
|
-
return rows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()).join("\n");
|
|
3055
|
+
function formatTimestamp(value) {
|
|
3056
|
+
if (value <= 0) return "-";
|
|
3057
|
+
const date = new Date(value * 1e3);
|
|
3058
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
3059
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
2933
3060
|
}
|
|
2934
3061
|
|
|
2935
3062
|
// src/download.ts
|
|
@@ -3700,7 +3827,7 @@ function escapeXml(value) {
|
|
|
3700
3827
|
}
|
|
3701
3828
|
|
|
3702
3829
|
// src/version.ts
|
|
3703
|
-
var VERSION = "0.7.0-alpha.
|
|
3830
|
+
var VERSION = "0.7.0-alpha.4";
|
|
3704
3831
|
|
|
3705
3832
|
// src/forum.ts
|
|
3706
3833
|
function parseDiscussionReference(value) {
|
package/dist/worker/worker.js
CHANGED
|
@@ -5996,7 +5996,7 @@ function problemResponse(status, code, title, detail, headers) {
|
|
|
5996
5996
|
}
|
|
5997
5997
|
|
|
5998
5998
|
// src/version.ts
|
|
5999
|
-
var VERSION = "0.7.0-alpha.
|
|
5999
|
+
var VERSION = "0.7.0-alpha.4";
|
|
6000
6000
|
|
|
6001
6001
|
// src/worker/http.ts
|
|
6002
6002
|
var HEALTH_PATH = "/healthz";
|
|
@@ -21154,6 +21154,32 @@ function htmlText(value, baseUrl) {
|
|
|
21154
21154
|
|
|
21155
21155
|
// src/scraper.ts
|
|
21156
21156
|
var import_node_html_parser2 = __toESM(require_dist(), 1);
|
|
21157
|
+
function parseMoodleErrorHtml(html) {
|
|
21158
|
+
const root = (0, import_node_html_parser2.parse)(html);
|
|
21159
|
+
const messageNode = first(root, [
|
|
21160
|
+
".errormessage",
|
|
21161
|
+
".alert-danger .alert-message",
|
|
21162
|
+
"[data-region='error-message']",
|
|
21163
|
+
".alert-danger[role='alert']",
|
|
21164
|
+
".alert-danger"
|
|
21165
|
+
]);
|
|
21166
|
+
if (!messageNode) {
|
|
21167
|
+
return null;
|
|
21168
|
+
}
|
|
21169
|
+
const messageRoot = (0, import_node_html_parser2.parse)(messageNode.toString());
|
|
21170
|
+
for (const unwanted of messageRoot.querySelectorAll(
|
|
21171
|
+
"button, .close, .errorcode, .stacktrace, .debuginfo, .backtrace, a.alert-link, a[href*='/error/']"
|
|
21172
|
+
)) {
|
|
21173
|
+
unwanted.remove();
|
|
21174
|
+
}
|
|
21175
|
+
const message = cleanText(messageRoot.textContent);
|
|
21176
|
+
if (!message) {
|
|
21177
|
+
return null;
|
|
21178
|
+
}
|
|
21179
|
+
const errorCodeText = cleanNodeText(root.querySelector(".errorcode"));
|
|
21180
|
+
const errorCode = errorCodeText.match(/^error\s+code\s*:\s*([a-z][a-z0-9_]*)\s*$/iu)?.[1] ?? moodleDocsErrorCode(root);
|
|
21181
|
+
return { message, ...errorCode ? { code: errorCode } : {} };
|
|
21182
|
+
}
|
|
21157
21183
|
function parsePageContext(html, baseUrl) {
|
|
21158
21184
|
const root = (0, import_node_html_parser2.parse)(html);
|
|
21159
21185
|
const config2 = parseMoodleConfig(html);
|
|
@@ -21585,6 +21611,16 @@ function selectedGroupName(root, groupId) {
|
|
|
21585
21611
|
}
|
|
21586
21612
|
return "";
|
|
21587
21613
|
}
|
|
21614
|
+
function moodleDocsErrorCode(root) {
|
|
21615
|
+
for (const link of root.querySelectorAll("a[href*='/error/']")) {
|
|
21616
|
+
const href = link.getAttribute("href") ?? "";
|
|
21617
|
+
const code = href.match(/\/error\/[^/]+\/([a-z][a-z0-9_]*)/iu)?.[1];
|
|
21618
|
+
if (code) {
|
|
21619
|
+
return code;
|
|
21620
|
+
}
|
|
21621
|
+
}
|
|
21622
|
+
return void 0;
|
|
21623
|
+
}
|
|
21588
21624
|
function cleanNodeText(node) {
|
|
21589
21625
|
return cleanText(node?.textContent ?? "");
|
|
21590
21626
|
}
|
|
@@ -22074,7 +22110,7 @@ var MoodleClientCoreApiError = class extends MoodleClientCoreError {
|
|
|
22074
22110
|
moodleErrorCode;
|
|
22075
22111
|
constructor(message, moodleErrorCode) {
|
|
22076
22112
|
const auth = isLoginErrorCode(moodleErrorCode);
|
|
22077
|
-
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") ||
|
|
22113
|
+
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /\bHTTP 404\b/.test(message);
|
|
22078
22114
|
super(
|
|
22079
22115
|
auth ? "auth" : notFound ? "not_found" : "upstream",
|
|
22080
22116
|
message,
|
|
@@ -22505,7 +22541,15 @@ var MoodleClientCore = class {
|
|
|
22505
22541
|
throw this.errors.api("Session expired", "servicerequireslogin");
|
|
22506
22542
|
}
|
|
22507
22543
|
if (!response.ok) {
|
|
22508
|
-
|
|
22544
|
+
const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
|
|
22545
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
22546
|
+
if (contentType.includes("text/html") || contentType.includes("application/xhtml+xml")) {
|
|
22547
|
+
const moodleError = await response.text().then(parseMoodleErrorHtml).catch(() => null);
|
|
22548
|
+
if (moodleError) {
|
|
22549
|
+
throw this.errors.api(`${moodleError.message} (${context})`, moodleError.code);
|
|
22550
|
+
}
|
|
22551
|
+
}
|
|
22552
|
+
throw this.errors.api(context);
|
|
22509
22553
|
}
|
|
22510
22554
|
return response;
|
|
22511
22555
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moodle-cli",
|
|
3
|
-
"version": "0.7.0-alpha.
|
|
3
|
+
"version": "0.7.0-alpha.4",
|
|
4
4
|
"description": "Terminal-first CLI for Moodle LMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@steipete/sweet-cookie": "^0.4.0",
|
|
36
36
|
"commander": "^13.1.0",
|
|
37
37
|
"node-html-parser": "^7.0.1",
|
|
38
|
+
"tty-table": "^5.0.0",
|
|
38
39
|
"wrangler": "^4.120.0",
|
|
39
40
|
"yaml": "^2.8.0",
|
|
40
41
|
"zod": "^4.0.5"
|