@lukeguo12210/canvas-cli 0.0.1 → 0.0.2
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 +24 -20
- package/dist/bin/canvas.js +1185 -58
- package/dist/bin/canvas.js.map +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +11 -0
- package/skills/canvas-assignments/SKILL.md +6 -5
- package/skills/canvas-courses/SKILL.md +1 -1
- package/skills/canvas-files/SKILL.md +15 -6
- package/skills/canvas-modules/SKILL.md +5 -5
- package/skills/canvas-pages/SKILL.md +28 -0
- package/skills/canvas-review/SKILL.md +14 -5
- package/skills/canvas-shared/SKILL.md +39 -3
- package/skills/canvas-shared/references/auth.md +9 -0
package/dist/bin/canvas.js
CHANGED
|
@@ -231,6 +231,38 @@ var CanvasClient = class {
|
|
|
231
231
|
}
|
|
232
232
|
};
|
|
233
233
|
}
|
|
234
|
+
async download(url) {
|
|
235
|
+
const response = await this.fetchImpl(url, {
|
|
236
|
+
method: "GET",
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${this.token}`,
|
|
239
|
+
Accept: "*/*"
|
|
240
|
+
}
|
|
241
|
+
}).catch((error) => {
|
|
242
|
+
throw new CanvasCliError("CANVAS_NETWORK_ERROR", "Could not reach Canvas.", {
|
|
243
|
+
retryable: true,
|
|
244
|
+
cause: error
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new CanvasCliError(
|
|
249
|
+
mapStatusCode(response.status),
|
|
250
|
+
`Canvas download failed with status ${response.status}.`,
|
|
251
|
+
{ status: response.status, retryable: response.status === 429 || response.status >= 500 }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
data: new Uint8Array(await response.arrayBuffer()),
|
|
256
|
+
contentType: response.headers.get("content-type") ?? void 0,
|
|
257
|
+
filename: filenameFromContentDisposition(response.headers.get("content-disposition")),
|
|
258
|
+
meta: {
|
|
259
|
+
request: {
|
|
260
|
+
method: "GET",
|
|
261
|
+
url: redactSecrets(url)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
234
266
|
};
|
|
235
267
|
function normalizeBaseUrl(input2) {
|
|
236
268
|
const trimmed = input2.trim().replace(/\/+$/, "");
|
|
@@ -279,6 +311,17 @@ function mapStatusCode(status) {
|
|
|
279
311
|
if (status >= 500) return "CANVAS_SERVER_ERROR";
|
|
280
312
|
return "CANVAS_REQUEST_FAILED";
|
|
281
313
|
}
|
|
314
|
+
function filenameFromContentDisposition(header) {
|
|
315
|
+
if (!header) {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
const utf8Match = /filename\*=UTF-8''([^;]+)/i.exec(header);
|
|
319
|
+
if (utf8Match?.[1]) {
|
|
320
|
+
return decodeURIComponent(utf8Match[1]);
|
|
321
|
+
}
|
|
322
|
+
const match = /filename="?([^";]+)"?/i.exec(header);
|
|
323
|
+
return match?.[1];
|
|
324
|
+
}
|
|
282
325
|
|
|
283
326
|
// src/core/config-store.ts
|
|
284
327
|
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
@@ -380,7 +423,7 @@ async function promptHidden(prompt) {
|
|
|
380
423
|
io.close();
|
|
381
424
|
}
|
|
382
425
|
}
|
|
383
|
-
return new Promise((
|
|
426
|
+
return new Promise((resolve2, reject) => {
|
|
384
427
|
const stdin = process.stdin;
|
|
385
428
|
const onData = (char) => {
|
|
386
429
|
const value = char.toString("utf8");
|
|
@@ -389,7 +432,7 @@ async function promptHidden(prompt) {
|
|
|
389
432
|
stdin.pause();
|
|
390
433
|
stdin.off("data", onData);
|
|
391
434
|
process.stdout.write("\n");
|
|
392
|
-
|
|
435
|
+
resolve2(buffer.trim());
|
|
393
436
|
return;
|
|
394
437
|
}
|
|
395
438
|
if (value === "") {
|
|
@@ -687,40 +730,6 @@ async function validateToken(client) {
|
|
|
687
730
|
}
|
|
688
731
|
}
|
|
689
732
|
|
|
690
|
-
// src/commands/config.ts
|
|
691
|
-
async function handleConfigCommand(argv, options) {
|
|
692
|
-
const [subcommand] = argv;
|
|
693
|
-
if (subcommand !== "show") {
|
|
694
|
-
await writeOutput(
|
|
695
|
-
{
|
|
696
|
-
ok: false,
|
|
697
|
-
error: {
|
|
698
|
-
code: "UNKNOWN_COMMAND",
|
|
699
|
-
message: `Unknown config command: ${argv.join(" ")}`,
|
|
700
|
-
retryable: false
|
|
701
|
-
}
|
|
702
|
-
},
|
|
703
|
-
options
|
|
704
|
-
);
|
|
705
|
-
return 1;
|
|
706
|
-
}
|
|
707
|
-
const config = await new ConfigStore().readRedacted();
|
|
708
|
-
await writeOutput(
|
|
709
|
-
{
|
|
710
|
-
ok: true,
|
|
711
|
-
data: config ?? {
|
|
712
|
-
configured: false,
|
|
713
|
-
message: "No Canvas config found. Run canvas auth login."
|
|
714
|
-
},
|
|
715
|
-
meta: {
|
|
716
|
-
command: "config show"
|
|
717
|
-
}
|
|
718
|
-
},
|
|
719
|
-
options
|
|
720
|
-
);
|
|
721
|
-
return 0;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
733
|
// src/commands/shared.ts
|
|
725
734
|
async function activeCanvas() {
|
|
726
735
|
const profile = await new ConfigStore().activeProfile();
|
|
@@ -739,9 +748,24 @@ function flagValue(argv, flag) {
|
|
|
739
748
|
}
|
|
740
749
|
return argv[index + 1];
|
|
741
750
|
}
|
|
751
|
+
function requiredFlag(argv, flag, usage) {
|
|
752
|
+
const value = flagValue(argv, flag);
|
|
753
|
+
if (!value) {
|
|
754
|
+
throw new Error(usage);
|
|
755
|
+
}
|
|
756
|
+
return value;
|
|
757
|
+
}
|
|
742
758
|
function hasFlag(argv, flag) {
|
|
743
759
|
return argv.includes(flag);
|
|
744
760
|
}
|
|
761
|
+
function csvFlag(argv, flag) {
|
|
762
|
+
const raw = flagValue(argv, flag);
|
|
763
|
+
if (!raw) {
|
|
764
|
+
return void 0;
|
|
765
|
+
}
|
|
766
|
+
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
767
|
+
return values.length > 0 ? values : void 0;
|
|
768
|
+
}
|
|
745
769
|
function pageOptions(argv) {
|
|
746
770
|
const pageLimitRaw = flagValue(argv, "--page-limit");
|
|
747
771
|
return {
|
|
@@ -767,7 +791,15 @@ function positionalArgs(argv) {
|
|
|
767
791
|
"--enrollment-state",
|
|
768
792
|
"--state",
|
|
769
793
|
"--include",
|
|
770
|
-
"--params"
|
|
794
|
+
"--params",
|
|
795
|
+
"--bucket",
|
|
796
|
+
"--search",
|
|
797
|
+
"--order-by",
|
|
798
|
+
"--sort",
|
|
799
|
+
"--file-id",
|
|
800
|
+
"--folder-id",
|
|
801
|
+
"--group-id",
|
|
802
|
+
"--content-type"
|
|
771
803
|
]);
|
|
772
804
|
const values = [];
|
|
773
805
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -783,6 +815,313 @@ function positionalArgs(argv) {
|
|
|
783
815
|
return values;
|
|
784
816
|
}
|
|
785
817
|
|
|
818
|
+
// src/commands/api.ts
|
|
819
|
+
async function handleApiCommand(argv, options) {
|
|
820
|
+
const [subcommand] = argv;
|
|
821
|
+
try {
|
|
822
|
+
if (subcommand === "get") {
|
|
823
|
+
return await apiGet(argv.slice(1), options);
|
|
824
|
+
}
|
|
825
|
+
await writeOutput(
|
|
826
|
+
{
|
|
827
|
+
ok: false,
|
|
828
|
+
error: {
|
|
829
|
+
code: "UNKNOWN_COMMAND",
|
|
830
|
+
message: `Unknown api command: ${argv.join(" ")}`,
|
|
831
|
+
retryable: false
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
options
|
|
835
|
+
);
|
|
836
|
+
return 1;
|
|
837
|
+
} catch (error) {
|
|
838
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
839
|
+
return 1;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function apiGet(argv, options) {
|
|
843
|
+
const path2 = positionalArgs(argv)[0];
|
|
844
|
+
if (!path2) {
|
|
845
|
+
throw new Error("Usage: canvas api get /api/v1/<path> [--params '<json>'] [--page-all]");
|
|
846
|
+
}
|
|
847
|
+
if (!path2.startsWith("/api/v1/")) {
|
|
848
|
+
throw new CanvasCliError("INVALID_API_PATH", "Raw API paths must start with /api/v1/.");
|
|
849
|
+
}
|
|
850
|
+
const params = parseParams(flagValue(argv, "--params"));
|
|
851
|
+
const { client, profile } = await activeCanvas();
|
|
852
|
+
const response = await client.get(path2, {
|
|
853
|
+
query: params,
|
|
854
|
+
...pageOptions(argv)
|
|
855
|
+
});
|
|
856
|
+
await writeOutput(
|
|
857
|
+
{
|
|
858
|
+
ok: true,
|
|
859
|
+
data: response.data,
|
|
860
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
861
|
+
},
|
|
862
|
+
options
|
|
863
|
+
);
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
function parseParams(raw) {
|
|
867
|
+
if (!raw) {
|
|
868
|
+
return {};
|
|
869
|
+
}
|
|
870
|
+
const parsed = JSON.parse(raw);
|
|
871
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
872
|
+
throw new CanvasCliError("INVALID_PARAMS", "--params must be a JSON object.");
|
|
873
|
+
}
|
|
874
|
+
const params = {};
|
|
875
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
876
|
+
if (value === null) {
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (isQueryValue(value)) {
|
|
880
|
+
params[key] = value;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
throw new CanvasCliError(
|
|
884
|
+
"INVALID_PARAMS",
|
|
885
|
+
`Unsupported query value for ${key}. Use string, number, boolean, or arrays of those.`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
return params;
|
|
889
|
+
}
|
|
890
|
+
function isQueryValue(value) {
|
|
891
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
if (Array.isArray(value)) {
|
|
895
|
+
return value.every(
|
|
896
|
+
(item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
return value === void 0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/commands/assignments.ts
|
|
903
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
904
|
+
import { join } from "path";
|
|
905
|
+
async function handleAssignmentsCommand(argv, options) {
|
|
906
|
+
const [subcommand] = argv;
|
|
907
|
+
try {
|
|
908
|
+
if (subcommand === "list") {
|
|
909
|
+
return await listAssignments(argv.slice(1), options);
|
|
910
|
+
}
|
|
911
|
+
if (subcommand === "show") {
|
|
912
|
+
return await showAssignment(argv.slice(1), options);
|
|
913
|
+
}
|
|
914
|
+
if (subcommand === "export") {
|
|
915
|
+
return await exportAssignment(argv.slice(1), options);
|
|
916
|
+
}
|
|
917
|
+
await writeOutput(
|
|
918
|
+
{
|
|
919
|
+
ok: false,
|
|
920
|
+
error: {
|
|
921
|
+
code: "UNKNOWN_COMMAND",
|
|
922
|
+
message: `Unknown assignments command: ${argv.join(" ")}`,
|
|
923
|
+
retryable: false
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
options
|
|
927
|
+
);
|
|
928
|
+
return 1;
|
|
929
|
+
} catch (error) {
|
|
930
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
931
|
+
return 1;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async function listAssignments(argv, options) {
|
|
935
|
+
const courseId = requiredFlag(
|
|
936
|
+
argv,
|
|
937
|
+
"--course-id",
|
|
938
|
+
"Usage: canvas assignments list --course-id <course-id>"
|
|
939
|
+
);
|
|
940
|
+
const { client, profile } = await activeCanvas();
|
|
941
|
+
const response = await client.get(
|
|
942
|
+
`/api/v1/courses/${courseId}/assignments`,
|
|
943
|
+
{
|
|
944
|
+
query: assignmentListQuery(argv),
|
|
945
|
+
...pageOptions(argv)
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
await writeOutput(
|
|
949
|
+
{
|
|
950
|
+
ok: true,
|
|
951
|
+
data: response.data.map(normalizeAssignment),
|
|
952
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
953
|
+
},
|
|
954
|
+
options
|
|
955
|
+
);
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
async function showAssignment(argv, options) {
|
|
959
|
+
const courseId = requiredFlag(
|
|
960
|
+
argv,
|
|
961
|
+
"--course-id",
|
|
962
|
+
"Usage: canvas assignments show --course-id <course-id> --assignment-id <assignment-id>"
|
|
963
|
+
);
|
|
964
|
+
const assignmentId = flagValue(argv, "--assignment-id") ?? positionalArgs(argv)[0] ?? missing("Usage: canvas assignments show --course-id <course-id> --assignment-id <assignment-id>");
|
|
965
|
+
const { client, profile } = await activeCanvas();
|
|
966
|
+
const response = await client.get(
|
|
967
|
+
`/api/v1/courses/${courseId}/assignments/${assignmentId}`,
|
|
968
|
+
{ query: assignmentShowQuery(argv) }
|
|
969
|
+
);
|
|
970
|
+
await writeOutput(
|
|
971
|
+
{
|
|
972
|
+
ok: true,
|
|
973
|
+
data: normalizeAssignment(response.data),
|
|
974
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
975
|
+
},
|
|
976
|
+
options
|
|
977
|
+
);
|
|
978
|
+
return 0;
|
|
979
|
+
}
|
|
980
|
+
async function exportAssignment(argv, options) {
|
|
981
|
+
const courseId = requiredFlag(
|
|
982
|
+
argv,
|
|
983
|
+
"--course-id",
|
|
984
|
+
"Usage: canvas assignments export --course-id <course-id> --assignment-id <assignment-id> --out <dir>"
|
|
985
|
+
);
|
|
986
|
+
const assignmentId = flagValue(argv, "--assignment-id") ?? positionalArgs(argv)[0] ?? missing("Usage: canvas assignments export --course-id <course-id> --assignment-id <assignment-id> --out <dir>");
|
|
987
|
+
const outDir = requiredFlag(
|
|
988
|
+
argv,
|
|
989
|
+
"--out",
|
|
990
|
+
"Usage: canvas assignments export --course-id <course-id> --assignment-id <assignment-id> --out <dir>"
|
|
991
|
+
);
|
|
992
|
+
const { client, profile } = await activeCanvas();
|
|
993
|
+
const response = await client.get(
|
|
994
|
+
`/api/v1/courses/${courseId}/assignments/${assignmentId}`,
|
|
995
|
+
{ query: { "include[]": ["all_dates", "description", "submission"] } }
|
|
996
|
+
);
|
|
997
|
+
const assignment = normalizeAssignment(response.data);
|
|
998
|
+
await mkdir2(outDir, { recursive: true });
|
|
999
|
+
const jsonPath = join(outDir, `assignment-${assignment.id}.json`);
|
|
1000
|
+
const markdownPath = join(outDir, `assignment-${assignment.id}.md`);
|
|
1001
|
+
await writeFile2(jsonPath, `${JSON.stringify(assignment, null, 2)}
|
|
1002
|
+
`, "utf8");
|
|
1003
|
+
await writeFile2(markdownPath, assignmentMarkdown(assignment), "utf8");
|
|
1004
|
+
await writeOutput(
|
|
1005
|
+
{
|
|
1006
|
+
ok: true,
|
|
1007
|
+
data: {
|
|
1008
|
+
assignment,
|
|
1009
|
+
written: [
|
|
1010
|
+
{ path: jsonPath, kind: "assignment-json" },
|
|
1011
|
+
{ path: markdownPath, kind: "assignment-markdown" }
|
|
1012
|
+
]
|
|
1013
|
+
},
|
|
1014
|
+
meta: { baseUrl: profile.baseUrl }
|
|
1015
|
+
},
|
|
1016
|
+
options
|
|
1017
|
+
);
|
|
1018
|
+
return 0;
|
|
1019
|
+
}
|
|
1020
|
+
function assignmentListQuery(argv) {
|
|
1021
|
+
return {
|
|
1022
|
+
bucket: flagValue(argv, "--bucket"),
|
|
1023
|
+
search_term: flagValue(argv, "--search"),
|
|
1024
|
+
order_by: flagValue(argv, "--order-by"),
|
|
1025
|
+
"include[]": csvFlag(argv, "--include"),
|
|
1026
|
+
per_page: flagValue(argv, "--page-size")
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function assignmentShowQuery(argv) {
|
|
1030
|
+
return {
|
|
1031
|
+
"include[]": csvFlag(argv, "--include") ?? ["all_dates", "description"]
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function normalizeAssignment(assignment) {
|
|
1035
|
+
return {
|
|
1036
|
+
id: String(assignment.id),
|
|
1037
|
+
name: assignment.name,
|
|
1038
|
+
description: assignment.description,
|
|
1039
|
+
dueAt: assignment.due_at,
|
|
1040
|
+
unlockAt: assignment.unlock_at,
|
|
1041
|
+
lockAt: assignment.lock_at,
|
|
1042
|
+
pointsPossible: assignment.points_possible,
|
|
1043
|
+
gradingType: assignment.grading_type,
|
|
1044
|
+
submissionTypes: assignment.submission_types,
|
|
1045
|
+
allowedExtensions: assignment.allowed_extensions,
|
|
1046
|
+
htmlUrl: assignment.html_url,
|
|
1047
|
+
assignmentGroupId: assignment.assignment_group_id === void 0 ? void 0 : String(assignment.assignment_group_id),
|
|
1048
|
+
position: assignment.position,
|
|
1049
|
+
published: assignment.published,
|
|
1050
|
+
muted: assignment.muted,
|
|
1051
|
+
hasSubmittedSubmissions: assignment.has_submitted_submissions,
|
|
1052
|
+
lockedForUser: assignment.locked_for_user,
|
|
1053
|
+
lockExplanation: assignment.lock_explanation,
|
|
1054
|
+
needsGradingCount: assignment.needs_grading_count,
|
|
1055
|
+
allDates: assignment.all_dates,
|
|
1056
|
+
externalToolTagAttributes: assignment.external_tool_tag_attributes,
|
|
1057
|
+
rubric: assignment.rubric,
|
|
1058
|
+
attachments: assignment.attachments?.map(normalizeAttachment)
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function normalizeAttachment(attachment) {
|
|
1062
|
+
return {
|
|
1063
|
+
id: String(attachment.id),
|
|
1064
|
+
displayName: attachment.display_name,
|
|
1065
|
+
filename: attachment.filename,
|
|
1066
|
+
contentType: attachment.content_type,
|
|
1067
|
+
url: attachment.url,
|
|
1068
|
+
size: attachment.size
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function assignmentMarkdown(assignment) {
|
|
1072
|
+
const lines = [
|
|
1073
|
+
`# ${assignment.name ?? `Assignment ${assignment.id}`}`,
|
|
1074
|
+
"",
|
|
1075
|
+
`- id: ${assignment.id}`,
|
|
1076
|
+
`- due: ${assignment.dueAt ?? "none"}`,
|
|
1077
|
+
`- points: ${assignment.pointsPossible ?? "none"}`,
|
|
1078
|
+
`- html: ${assignment.htmlUrl ?? "none"}`,
|
|
1079
|
+
"",
|
|
1080
|
+
"## Description",
|
|
1081
|
+
"",
|
|
1082
|
+
assignment.description ?? ""
|
|
1083
|
+
];
|
|
1084
|
+
return `${lines.join("\n")}
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
function missing(message) {
|
|
1088
|
+
throw new Error(message);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/commands/config.ts
|
|
1092
|
+
async function handleConfigCommand(argv, options) {
|
|
1093
|
+
const [subcommand] = argv;
|
|
1094
|
+
if (subcommand !== "show") {
|
|
1095
|
+
await writeOutput(
|
|
1096
|
+
{
|
|
1097
|
+
ok: false,
|
|
1098
|
+
error: {
|
|
1099
|
+
code: "UNKNOWN_COMMAND",
|
|
1100
|
+
message: `Unknown config command: ${argv.join(" ")}`,
|
|
1101
|
+
retryable: false
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
options
|
|
1105
|
+
);
|
|
1106
|
+
return 1;
|
|
1107
|
+
}
|
|
1108
|
+
const config = await new ConfigStore().readRedacted();
|
|
1109
|
+
await writeOutput(
|
|
1110
|
+
{
|
|
1111
|
+
ok: true,
|
|
1112
|
+
data: config ?? {
|
|
1113
|
+
configured: false,
|
|
1114
|
+
message: "No Canvas config found. Run canvas auth login."
|
|
1115
|
+
},
|
|
1116
|
+
meta: {
|
|
1117
|
+
command: "config show"
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
options
|
|
1121
|
+
);
|
|
1122
|
+
return 0;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
786
1125
|
// src/commands/courses.ts
|
|
787
1126
|
async function handleCoursesCommand(argv, options) {
|
|
788
1127
|
const [subcommand] = argv;
|
|
@@ -975,37 +1314,681 @@ function normalizeCourse(course) {
|
|
|
975
1314
|
};
|
|
976
1315
|
}
|
|
977
1316
|
|
|
978
|
-
// src/commands/
|
|
979
|
-
|
|
1317
|
+
// src/commands/files.ts
|
|
1318
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
1319
|
+
import { basename, resolve } from "path";
|
|
1320
|
+
async function handleFilesCommand(argv, options) {
|
|
1321
|
+
const [subcommand] = argv;
|
|
980
1322
|
try {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1323
|
+
if (subcommand === "list") {
|
|
1324
|
+
return await listFiles(argv.slice(1), options);
|
|
1325
|
+
}
|
|
1326
|
+
if (subcommand === "show") {
|
|
1327
|
+
return await showFile(argv.slice(1), options);
|
|
1328
|
+
}
|
|
1329
|
+
if (subcommand === "download") {
|
|
1330
|
+
return await downloadFile(argv.slice(1), options);
|
|
1331
|
+
}
|
|
1332
|
+
if (subcommand === "download-linked") {
|
|
1333
|
+
return await downloadLinkedFiles(argv.slice(1), options);
|
|
1334
|
+
}
|
|
987
1335
|
await writeOutput(
|
|
988
1336
|
{
|
|
989
|
-
ok:
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1337
|
+
ok: false,
|
|
1338
|
+
error: {
|
|
1339
|
+
code: "UNKNOWN_COMMAND",
|
|
1340
|
+
message: `Unknown files command: ${argv.join(" ")}`,
|
|
1341
|
+
retryable: false
|
|
994
1342
|
}
|
|
995
1343
|
},
|
|
996
1344
|
options
|
|
997
1345
|
);
|
|
998
|
-
return
|
|
1346
|
+
return 1;
|
|
999
1347
|
} catch (error) {
|
|
1000
1348
|
await writeOutput(toErrorEnvelope(error), options);
|
|
1001
1349
|
return 1;
|
|
1002
1350
|
}
|
|
1003
1351
|
}
|
|
1004
|
-
|
|
1005
|
-
// src/commands/tabs.ts
|
|
1006
|
-
async function handleTabsCommand(argv, options) {
|
|
1352
|
+
async function handleFoldersCommand(argv, options) {
|
|
1007
1353
|
const [subcommand] = argv;
|
|
1008
|
-
|
|
1354
|
+
try {
|
|
1355
|
+
if (subcommand === "list") {
|
|
1356
|
+
return await listFolders(argv.slice(1), options);
|
|
1357
|
+
}
|
|
1358
|
+
if (subcommand === "path") {
|
|
1359
|
+
return await folderPath(argv.slice(1), options);
|
|
1360
|
+
}
|
|
1361
|
+
await writeOutput(
|
|
1362
|
+
{
|
|
1363
|
+
ok: false,
|
|
1364
|
+
error: {
|
|
1365
|
+
code: "UNKNOWN_COMMAND",
|
|
1366
|
+
message: `Unknown folders command: ${argv.join(" ")}`,
|
|
1367
|
+
retryable: false
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
options
|
|
1371
|
+
);
|
|
1372
|
+
return 1;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
1375
|
+
return 1;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
async function listFiles(argv, options) {
|
|
1379
|
+
const path2 = filesListPath(argv);
|
|
1380
|
+
const { client, profile } = await activeCanvas();
|
|
1381
|
+
const response = await client.get(path2, {
|
|
1382
|
+
query: filesListQuery(argv),
|
|
1383
|
+
...pageOptions(argv)
|
|
1384
|
+
});
|
|
1385
|
+
await writeOutput(
|
|
1386
|
+
{
|
|
1387
|
+
ok: true,
|
|
1388
|
+
data: response.data.map(normalizeFile),
|
|
1389
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1390
|
+
},
|
|
1391
|
+
options
|
|
1392
|
+
);
|
|
1393
|
+
return 0;
|
|
1394
|
+
}
|
|
1395
|
+
async function showFile(argv, options) {
|
|
1396
|
+
const fileId = flagValue(argv, "--file-id") ?? positionalArgs(argv)[0] ?? missing2("Usage: canvas files show <file-id>");
|
|
1397
|
+
const { client, profile } = await activeCanvas();
|
|
1398
|
+
const response = await client.get(`/api/v1/files/${fileId}`, {
|
|
1399
|
+
query: { "include[]": csvFlag(argv, "--include") }
|
|
1400
|
+
});
|
|
1401
|
+
await writeOutput(
|
|
1402
|
+
{
|
|
1403
|
+
ok: true,
|
|
1404
|
+
data: normalizeFile(response.data),
|
|
1405
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1406
|
+
},
|
|
1407
|
+
options
|
|
1408
|
+
);
|
|
1409
|
+
return 0;
|
|
1410
|
+
}
|
|
1411
|
+
async function downloadFile(argv, options) {
|
|
1412
|
+
const fileId = flagValue(argv, "--file-id") ?? positionalArgs(argv)[0] ?? missing2("Usage: canvas files download <file-id> --out <dir>");
|
|
1413
|
+
const outDir = requiredFlag(argv, "--out", "Usage: canvas files download <file-id> --out <dir>");
|
|
1414
|
+
const { client, profile } = await activeCanvas();
|
|
1415
|
+
const fileResponse = await client.get(`/api/v1/files/${fileId}`);
|
|
1416
|
+
const file = normalizeFile(fileResponse.data);
|
|
1417
|
+
if (!file.url) {
|
|
1418
|
+
throw new CanvasCliError("FILE_URL_UNAVAILABLE", "Canvas did not return a download URL for this file.");
|
|
1419
|
+
}
|
|
1420
|
+
const download = await client.download(file.url);
|
|
1421
|
+
const filename = safeFilename(download.filename ?? file.displayName ?? file.filename ?? `file-${file.id}`);
|
|
1422
|
+
const filePath = safeJoin(outDir, filename);
|
|
1423
|
+
await mkdir3(outDir, { recursive: true });
|
|
1424
|
+
await writeFile3(filePath, download.data);
|
|
1425
|
+
await writeOutput(
|
|
1426
|
+
{
|
|
1427
|
+
ok: true,
|
|
1428
|
+
data: {
|
|
1429
|
+
file,
|
|
1430
|
+
written: {
|
|
1431
|
+
path: filePath,
|
|
1432
|
+
bytes: download.data.byteLength,
|
|
1433
|
+
contentType: download.contentType
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
meta: { baseUrl: profile.baseUrl, download: download.meta }
|
|
1437
|
+
},
|
|
1438
|
+
options
|
|
1439
|
+
);
|
|
1440
|
+
return 0;
|
|
1441
|
+
}
|
|
1442
|
+
async function downloadLinkedFiles(argv, options) {
|
|
1443
|
+
const courseId = requiredFlag(
|
|
1444
|
+
argv,
|
|
1445
|
+
"--course-id",
|
|
1446
|
+
"Usage: canvas files download-linked --course-id <course-id> --out <dir>"
|
|
1447
|
+
);
|
|
1448
|
+
const outDir = requiredFlag(
|
|
1449
|
+
argv,
|
|
1450
|
+
"--out",
|
|
1451
|
+
"Usage: canvas files download-linked --course-id <course-id> --out <dir>"
|
|
1452
|
+
);
|
|
1453
|
+
const { client, profile } = await activeCanvas();
|
|
1454
|
+
const [moduleItems, assignments] = await Promise.all([
|
|
1455
|
+
client.get(
|
|
1456
|
+
`/api/v1/courses/${courseId}/modules/items`,
|
|
1457
|
+
{ query: { per_page: 100 }, pageAll: true }
|
|
1458
|
+
).catch(() => ({ data: [] })),
|
|
1459
|
+
client.get(`/api/v1/courses/${courseId}/assignments`, {
|
|
1460
|
+
query: { "include[]": ["description"], per_page: 100 },
|
|
1461
|
+
pageAll: true
|
|
1462
|
+
}).catch(() => ({ data: [] }))
|
|
1463
|
+
]);
|
|
1464
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1465
|
+
for (const item of moduleItems.data) {
|
|
1466
|
+
if (item.type === "File" && item.content_id !== void 0) {
|
|
1467
|
+
ids.add(String(item.content_id));
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
for (const assignment of assignments.data) {
|
|
1471
|
+
for (const attachment of assignment.attachments ?? []) {
|
|
1472
|
+
ids.add(String(attachment.id));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const written = [];
|
|
1476
|
+
for (const id of ids) {
|
|
1477
|
+
try {
|
|
1478
|
+
const fileResponse = await client.get(`/api/v1/files/${id}`);
|
|
1479
|
+
const file = normalizeFile(fileResponse.data);
|
|
1480
|
+
if (!file.url) {
|
|
1481
|
+
written.push({ id, error: "FILE_URL_UNAVAILABLE" });
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
const download = await client.download(file.url);
|
|
1485
|
+
const filename = safeFilename(download.filename ?? file.displayName ?? file.filename ?? `file-${id}`);
|
|
1486
|
+
const filePath = safeJoin(outDir, filename);
|
|
1487
|
+
await mkdir3(outDir, { recursive: true });
|
|
1488
|
+
await writeFile3(filePath, download.data);
|
|
1489
|
+
written.push({ id, path: filePath });
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1492
|
+
written.push({ id, error: message });
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
await writeOutput(
|
|
1496
|
+
{
|
|
1497
|
+
ok: true,
|
|
1498
|
+
data: { count: ids.size, written },
|
|
1499
|
+
meta: { baseUrl: profile.baseUrl }
|
|
1500
|
+
},
|
|
1501
|
+
options
|
|
1502
|
+
);
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
async function listFolders(argv, options) {
|
|
1506
|
+
const courseId = requiredFlag(
|
|
1507
|
+
argv,
|
|
1508
|
+
"--course-id",
|
|
1509
|
+
"Usage: canvas folders list --course-id <course-id>"
|
|
1510
|
+
);
|
|
1511
|
+
const { client, profile } = await activeCanvas();
|
|
1512
|
+
const response = await client.get(`/api/v1/courses/${courseId}/folders`, {
|
|
1513
|
+
query: { per_page: flagValue(argv, "--page-size") },
|
|
1514
|
+
...pageOptions(argv)
|
|
1515
|
+
});
|
|
1516
|
+
await writeOutput(
|
|
1517
|
+
{
|
|
1518
|
+
ok: true,
|
|
1519
|
+
data: response.data.map(normalizeFolder),
|
|
1520
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1521
|
+
},
|
|
1522
|
+
options
|
|
1523
|
+
);
|
|
1524
|
+
return 0;
|
|
1525
|
+
}
|
|
1526
|
+
async function folderPath(argv, options) {
|
|
1527
|
+
const courseId = requiredFlag(
|
|
1528
|
+
argv,
|
|
1529
|
+
"--course-id",
|
|
1530
|
+
"Usage: canvas folders path --course-id <course-id> --path <path>"
|
|
1531
|
+
);
|
|
1532
|
+
const folderPathValue = requiredFlag(
|
|
1533
|
+
argv,
|
|
1534
|
+
"--path",
|
|
1535
|
+
"Usage: canvas folders path --course-id <course-id> --path <path>"
|
|
1536
|
+
);
|
|
1537
|
+
const { client, profile } = await activeCanvas();
|
|
1538
|
+
const encodedPath = folderPathValue.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
1539
|
+
const response = await client.get(
|
|
1540
|
+
`/api/v1/courses/${courseId}/folders/by_path/${encodedPath}`
|
|
1541
|
+
);
|
|
1542
|
+
await writeOutput(
|
|
1543
|
+
{
|
|
1544
|
+
ok: true,
|
|
1545
|
+
data: response.data.map(normalizeFolder),
|
|
1546
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1547
|
+
},
|
|
1548
|
+
options
|
|
1549
|
+
);
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
1552
|
+
function filesListPath(argv) {
|
|
1553
|
+
const groupId = flagValue(argv, "--group-id");
|
|
1554
|
+
if (groupId) {
|
|
1555
|
+
return `/api/v1/groups/${groupId}/files`;
|
|
1556
|
+
}
|
|
1557
|
+
const folderId = flagValue(argv, "--folder-id");
|
|
1558
|
+
if (folderId) {
|
|
1559
|
+
return `/api/v1/folders/${folderId}/files`;
|
|
1560
|
+
}
|
|
1561
|
+
const courseId = requiredFlag(argv, "--course-id", "Usage: canvas files list --course-id <course-id>");
|
|
1562
|
+
return `/api/v1/courses/${courseId}/files`;
|
|
1563
|
+
}
|
|
1564
|
+
function filesListQuery(argv) {
|
|
1565
|
+
return {
|
|
1566
|
+
search_term: flagValue(argv, "--search"),
|
|
1567
|
+
sort: flagValue(argv, "--sort"),
|
|
1568
|
+
"content_types[]": csvFlag(argv, "--content-type"),
|
|
1569
|
+
"include[]": csvFlag(argv, "--include"),
|
|
1570
|
+
per_page: flagValue(argv, "--page-size")
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function normalizeFile(file) {
|
|
1574
|
+
return {
|
|
1575
|
+
id: String(file.id),
|
|
1576
|
+
uuid: file.uuid,
|
|
1577
|
+
folderId: file.folder_id === void 0 ? void 0 : String(file.folder_id),
|
|
1578
|
+
displayName: file.display_name,
|
|
1579
|
+
filename: file.filename,
|
|
1580
|
+
contentType: file.content_type,
|
|
1581
|
+
url: file.url,
|
|
1582
|
+
size: file.size,
|
|
1583
|
+
createdAt: file.created_at,
|
|
1584
|
+
updatedAt: file.updated_at,
|
|
1585
|
+
modifiedAt: file.modified_at,
|
|
1586
|
+
unlockAt: file.unlock_at,
|
|
1587
|
+
locked: file.locked,
|
|
1588
|
+
hidden: file.hidden,
|
|
1589
|
+
lockedForUser: file.locked_for_user,
|
|
1590
|
+
lockExplanation: file.lock_explanation,
|
|
1591
|
+
thumbnailUrl: file.thumbnail_url,
|
|
1592
|
+
previewUrl: file.preview_url,
|
|
1593
|
+
mimeClass: file.mime_class
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
function normalizeFolder(folder) {
|
|
1597
|
+
return {
|
|
1598
|
+
id: String(folder.id),
|
|
1599
|
+
name: folder.name,
|
|
1600
|
+
fullName: folder.full_name,
|
|
1601
|
+
contextId: folder.context_id === void 0 ? void 0 : String(folder.context_id),
|
|
1602
|
+
contextType: folder.context_type,
|
|
1603
|
+
parentFolderId: folder.parent_folder_id === void 0 ? void 0 : String(folder.parent_folder_id),
|
|
1604
|
+
filesCount: folder.files_count,
|
|
1605
|
+
foldersCount: folder.folders_count,
|
|
1606
|
+
position: folder.position,
|
|
1607
|
+
locked: folder.locked,
|
|
1608
|
+
hidden: folder.hidden,
|
|
1609
|
+
lockedForUser: folder.locked_for_user,
|
|
1610
|
+
forSubmissions: folder.for_submissions
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function safeFilename(input2) {
|
|
1614
|
+
const name = basename(input2).replace(/[/:\\]/g, "_").trim();
|
|
1615
|
+
return name || "canvas-file";
|
|
1616
|
+
}
|
|
1617
|
+
function safeJoin(outDir, filename) {
|
|
1618
|
+
const base = resolve(outDir);
|
|
1619
|
+
const target = resolve(base, filename);
|
|
1620
|
+
if (!target.startsWith(`${base}/`) && target !== base) {
|
|
1621
|
+
throw new CanvasCliError("INVALID_OUTPUT_PATH", "Refusing to write outside the output directory.");
|
|
1622
|
+
}
|
|
1623
|
+
return target;
|
|
1624
|
+
}
|
|
1625
|
+
function missing2(message) {
|
|
1626
|
+
throw new Error(message);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/commands/me.ts
|
|
1630
|
+
async function handleMeCommand(options) {
|
|
1631
|
+
try {
|
|
1632
|
+
const profile = await new ConfigStore().activeProfile();
|
|
1633
|
+
const client = new CanvasClient({
|
|
1634
|
+
baseUrl: profile.baseUrl,
|
|
1635
|
+
token: profile.token
|
|
1636
|
+
});
|
|
1637
|
+
const response = await client.get("/api/v1/users/self/profile");
|
|
1638
|
+
await writeOutput(
|
|
1639
|
+
{
|
|
1640
|
+
ok: true,
|
|
1641
|
+
data: response.data,
|
|
1642
|
+
meta: {
|
|
1643
|
+
...response.meta,
|
|
1644
|
+
baseUrl: profile.baseUrl
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
options
|
|
1648
|
+
);
|
|
1649
|
+
return 0;
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
1652
|
+
return 1;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/commands/modules.ts
|
|
1657
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
1658
|
+
import { join as join3 } from "path";
|
|
1659
|
+
async function handleModulesCommand(argv, options) {
|
|
1660
|
+
const [subcommand] = argv;
|
|
1661
|
+
try {
|
|
1662
|
+
if (subcommand === "list") {
|
|
1663
|
+
return await listModules(argv.slice(1), options);
|
|
1664
|
+
}
|
|
1665
|
+
if (subcommand === "items") {
|
|
1666
|
+
return await listModuleItems(argv.slice(1), options);
|
|
1667
|
+
}
|
|
1668
|
+
if (subcommand === "item") {
|
|
1669
|
+
return await showModuleItem(argv.slice(1), options);
|
|
1670
|
+
}
|
|
1671
|
+
if (subcommand === "export") {
|
|
1672
|
+
return await exportModule(argv.slice(1), options);
|
|
1673
|
+
}
|
|
1674
|
+
await writeOutput(
|
|
1675
|
+
{
|
|
1676
|
+
ok: false,
|
|
1677
|
+
error: {
|
|
1678
|
+
code: "UNKNOWN_COMMAND",
|
|
1679
|
+
message: `Unknown modules command: ${argv.join(" ")}`,
|
|
1680
|
+
retryable: false
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
options
|
|
1684
|
+
);
|
|
1685
|
+
return 1;
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
1688
|
+
return 1;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async function listModules(argv, options) {
|
|
1692
|
+
const courseId = requiredFlag(argv, "--course-id", "Usage: canvas modules list --course-id <course-id>");
|
|
1693
|
+
const { client, profile } = await activeCanvas();
|
|
1694
|
+
const response = await client.get(`/api/v1/courses/${courseId}/modules`, {
|
|
1695
|
+
query: moduleListQuery(argv),
|
|
1696
|
+
...pageOptions(argv)
|
|
1697
|
+
});
|
|
1698
|
+
await writeOutput(
|
|
1699
|
+
{
|
|
1700
|
+
ok: true,
|
|
1701
|
+
data: response.data.map(normalizeModule),
|
|
1702
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1703
|
+
},
|
|
1704
|
+
options
|
|
1705
|
+
);
|
|
1706
|
+
return 0;
|
|
1707
|
+
}
|
|
1708
|
+
async function listModuleItems(argv, options) {
|
|
1709
|
+
const courseId = requiredFlag(
|
|
1710
|
+
argv,
|
|
1711
|
+
"--course-id",
|
|
1712
|
+
"Usage: canvas modules items --course-id <course-id> --module-id <module-id>"
|
|
1713
|
+
);
|
|
1714
|
+
const moduleId = requiredFlag(
|
|
1715
|
+
argv,
|
|
1716
|
+
"--module-id",
|
|
1717
|
+
"Usage: canvas modules items --course-id <course-id> --module-id <module-id>"
|
|
1718
|
+
);
|
|
1719
|
+
const { client, profile } = await activeCanvas();
|
|
1720
|
+
const response = await client.get(
|
|
1721
|
+
`/api/v1/courses/${courseId}/modules/${moduleId}/items`,
|
|
1722
|
+
{
|
|
1723
|
+
query: moduleItemsQuery(argv),
|
|
1724
|
+
...pageOptions(argv)
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
await writeOutput(
|
|
1728
|
+
{
|
|
1729
|
+
ok: true,
|
|
1730
|
+
data: response.data.map(normalizeModuleItem),
|
|
1731
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1732
|
+
},
|
|
1733
|
+
options
|
|
1734
|
+
);
|
|
1735
|
+
return 0;
|
|
1736
|
+
}
|
|
1737
|
+
async function showModuleItem(argv, options) {
|
|
1738
|
+
const courseId = requiredFlag(
|
|
1739
|
+
argv,
|
|
1740
|
+
"--course-id",
|
|
1741
|
+
"Usage: canvas modules item --course-id <course-id> --module-id <module-id> --item-id <item-id>"
|
|
1742
|
+
);
|
|
1743
|
+
const moduleId = requiredFlag(
|
|
1744
|
+
argv,
|
|
1745
|
+
"--module-id",
|
|
1746
|
+
"Usage: canvas modules item --course-id <course-id> --module-id <module-id> --item-id <item-id>"
|
|
1747
|
+
);
|
|
1748
|
+
const itemId = flagValue(argv, "--item-id") ?? positionalArgs(argv)[0] ?? missing3("Usage: canvas modules item --course-id <course-id> --module-id <module-id> --item-id <item-id>");
|
|
1749
|
+
const { client, profile } = await activeCanvas();
|
|
1750
|
+
const response = await client.get(
|
|
1751
|
+
`/api/v1/courses/${courseId}/modules/${moduleId}/items/${itemId}`,
|
|
1752
|
+
{ query: moduleItemsQuery(argv) }
|
|
1753
|
+
);
|
|
1754
|
+
await writeOutput(
|
|
1755
|
+
{
|
|
1756
|
+
ok: true,
|
|
1757
|
+
data: normalizeModuleItem(response.data),
|
|
1758
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1759
|
+
},
|
|
1760
|
+
options
|
|
1761
|
+
);
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
async function exportModule(argv, options) {
|
|
1765
|
+
const courseId = requiredFlag(
|
|
1766
|
+
argv,
|
|
1767
|
+
"--course-id",
|
|
1768
|
+
"Usage: canvas modules export --course-id <course-id> --module-id <module-id> --out <dir>"
|
|
1769
|
+
);
|
|
1770
|
+
const moduleId = requiredFlag(
|
|
1771
|
+
argv,
|
|
1772
|
+
"--module-id",
|
|
1773
|
+
"Usage: canvas modules export --course-id <course-id> --module-id <module-id> --out <dir>"
|
|
1774
|
+
);
|
|
1775
|
+
const outDir = requiredFlag(
|
|
1776
|
+
argv,
|
|
1777
|
+
"--out",
|
|
1778
|
+
"Usage: canvas modules export --course-id <course-id> --module-id <module-id> --out <dir>"
|
|
1779
|
+
);
|
|
1780
|
+
const { client, profile } = await activeCanvas();
|
|
1781
|
+
const [moduleResponse, itemsResponse] = await Promise.all([
|
|
1782
|
+
client.get(`/api/v1/courses/${courseId}/modules/${moduleId}`),
|
|
1783
|
+
client.get(`/api/v1/courses/${courseId}/modules/${moduleId}/items`, {
|
|
1784
|
+
query: { "include[]": ["content_details"], per_page: 100 },
|
|
1785
|
+
pageAll: true
|
|
1786
|
+
})
|
|
1787
|
+
]);
|
|
1788
|
+
const moduleData = normalizeModule({ ...moduleResponse.data, items: itemsResponse.data });
|
|
1789
|
+
await mkdir4(outDir, { recursive: true });
|
|
1790
|
+
const filePath = join3(outDir, `module-${moduleData.id}.json`);
|
|
1791
|
+
await writeFile4(filePath, `${JSON.stringify(moduleData, null, 2)}
|
|
1792
|
+
`, "utf8");
|
|
1793
|
+
await writeOutput(
|
|
1794
|
+
{
|
|
1795
|
+
ok: true,
|
|
1796
|
+
data: { module: moduleData, written: [{ path: filePath, kind: "module-json" }] },
|
|
1797
|
+
meta: { baseUrl: profile.baseUrl }
|
|
1798
|
+
},
|
|
1799
|
+
options
|
|
1800
|
+
);
|
|
1801
|
+
return 0;
|
|
1802
|
+
}
|
|
1803
|
+
function moduleListQuery(argv) {
|
|
1804
|
+
return {
|
|
1805
|
+
"include[]": csvFlag(argv, "--include"),
|
|
1806
|
+
per_page: flagValue(argv, "--page-size")
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function moduleItemsQuery(argv) {
|
|
1810
|
+
return {
|
|
1811
|
+
"include[]": csvFlag(argv, "--include"),
|
|
1812
|
+
per_page: flagValue(argv, "--page-size")
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
function normalizeModule(module) {
|
|
1816
|
+
return {
|
|
1817
|
+
id: String(module.id),
|
|
1818
|
+
name: module.name,
|
|
1819
|
+
position: module.position,
|
|
1820
|
+
state: module.state,
|
|
1821
|
+
unlockAt: module.unlock_at,
|
|
1822
|
+
completedAt: module.completed_at,
|
|
1823
|
+
requireSequentialProgress: module.require_sequential_progress,
|
|
1824
|
+
publishFinalGrade: module.publish_final_grade,
|
|
1825
|
+
prerequisiteModuleIds: module.prerequisite_module_ids?.map(String),
|
|
1826
|
+
itemsCount: module.items_count,
|
|
1827
|
+
itemsUrl: module.items_url,
|
|
1828
|
+
items: module.items?.map(normalizeModuleItem)
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
function normalizeModuleItem(item) {
|
|
1832
|
+
return {
|
|
1833
|
+
id: String(item.id),
|
|
1834
|
+
moduleId: item.module_id === void 0 ? void 0 : String(item.module_id),
|
|
1835
|
+
title: item.title,
|
|
1836
|
+
type: item.type,
|
|
1837
|
+
contentId: item.content_id === void 0 ? void 0 : String(item.content_id),
|
|
1838
|
+
position: item.position,
|
|
1839
|
+
indent: item.indent,
|
|
1840
|
+
pageUrl: item.page_url,
|
|
1841
|
+
externalUrl: item.external_url,
|
|
1842
|
+
htmlUrl: item.html_url,
|
|
1843
|
+
apiUrl: item.url,
|
|
1844
|
+
newTab: item.new_tab,
|
|
1845
|
+
completionRequirement: item.completion_requirement,
|
|
1846
|
+
contentDetails: item.content_details
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
function missing3(message) {
|
|
1850
|
+
throw new Error(message);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/commands/pages.ts
|
|
1854
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
1855
|
+
import { join as join4 } from "path";
|
|
1856
|
+
async function handlePagesCommand(argv, options) {
|
|
1857
|
+
const [subcommand] = argv;
|
|
1858
|
+
try {
|
|
1859
|
+
if (subcommand === "list") {
|
|
1860
|
+
return await listPages(argv.slice(1), options);
|
|
1861
|
+
}
|
|
1862
|
+
if (subcommand === "show") {
|
|
1863
|
+
return await showPage(argv.slice(1), options);
|
|
1864
|
+
}
|
|
1865
|
+
if (subcommand === "export") {
|
|
1866
|
+
return await exportPage(argv.slice(1), options);
|
|
1867
|
+
}
|
|
1868
|
+
await writeOutput(
|
|
1869
|
+
{
|
|
1870
|
+
ok: false,
|
|
1871
|
+
error: {
|
|
1872
|
+
code: "UNKNOWN_COMMAND",
|
|
1873
|
+
message: `Unknown pages command: ${argv.join(" ")}`,
|
|
1874
|
+
retryable: false
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1877
|
+
options
|
|
1878
|
+
);
|
|
1879
|
+
return 1;
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
1882
|
+
return 1;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
async function listPages(argv, options) {
|
|
1886
|
+
const courseId = requiredFlag(argv, "--course-id", "Usage: canvas pages list --course-id <course-id>");
|
|
1887
|
+
const { client, profile } = await activeCanvas();
|
|
1888
|
+
const response = await client.get(`/api/v1/courses/${courseId}/pages`, {
|
|
1889
|
+
query: pagesListQuery(argv),
|
|
1890
|
+
...pageOptions(argv)
|
|
1891
|
+
});
|
|
1892
|
+
await writeOutput(
|
|
1893
|
+
{
|
|
1894
|
+
ok: true,
|
|
1895
|
+
data: response.data.map(normalizePage),
|
|
1896
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1897
|
+
},
|
|
1898
|
+
options
|
|
1899
|
+
);
|
|
1900
|
+
return 0;
|
|
1901
|
+
}
|
|
1902
|
+
async function showPage(argv, options) {
|
|
1903
|
+
const courseId = requiredFlag(argv, "--course-id", "Usage: canvas pages show --course-id <course-id> --page <url>");
|
|
1904
|
+
const pageUrl = flagValue(argv, "--page") ?? positionalArgs(argv)[0] ?? missing4("Usage: canvas pages show --course-id <course-id> --page <url>");
|
|
1905
|
+
const { client, profile } = await activeCanvas();
|
|
1906
|
+
const response = await client.get(
|
|
1907
|
+
`/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`
|
|
1908
|
+
);
|
|
1909
|
+
await writeOutput(
|
|
1910
|
+
{
|
|
1911
|
+
ok: true,
|
|
1912
|
+
data: normalizePage(response.data),
|
|
1913
|
+
meta: { ...response.meta, baseUrl: profile.baseUrl }
|
|
1914
|
+
},
|
|
1915
|
+
options
|
|
1916
|
+
);
|
|
1917
|
+
return 0;
|
|
1918
|
+
}
|
|
1919
|
+
async function exportPage(argv, options) {
|
|
1920
|
+
const courseId = requiredFlag(
|
|
1921
|
+
argv,
|
|
1922
|
+
"--course-id",
|
|
1923
|
+
"Usage: canvas pages export --course-id <course-id> --page <url> --out <dir>"
|
|
1924
|
+
);
|
|
1925
|
+
const pageUrl = flagValue(argv, "--page") ?? positionalArgs(argv)[0] ?? missing4("Usage: canvas pages export --course-id <course-id> --page <url> --out <dir>");
|
|
1926
|
+
const outDir = requiredFlag(argv, "--out", "Usage: canvas pages export --course-id <course-id> --page <url> --out <dir>");
|
|
1927
|
+
const { client, profile } = await activeCanvas();
|
|
1928
|
+
const response = await client.get(
|
|
1929
|
+
`/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`
|
|
1930
|
+
);
|
|
1931
|
+
const page = normalizePage(response.data);
|
|
1932
|
+
await mkdir5(outDir, { recursive: true });
|
|
1933
|
+
const jsonPath = join4(outDir, `${page.url}.json`);
|
|
1934
|
+
const htmlPath = join4(outDir, `${page.url}.html`);
|
|
1935
|
+
await writeFile5(jsonPath, `${JSON.stringify(page, null, 2)}
|
|
1936
|
+
`, "utf8");
|
|
1937
|
+
await writeFile5(htmlPath, page.body ?? "", "utf8");
|
|
1938
|
+
await writeOutput(
|
|
1939
|
+
{
|
|
1940
|
+
ok: true,
|
|
1941
|
+
data: {
|
|
1942
|
+
page,
|
|
1943
|
+
written: [
|
|
1944
|
+
{ path: jsonPath, kind: "page-json" },
|
|
1945
|
+
{ path: htmlPath, kind: "page-html" }
|
|
1946
|
+
]
|
|
1947
|
+
},
|
|
1948
|
+
meta: { baseUrl: profile.baseUrl }
|
|
1949
|
+
},
|
|
1950
|
+
options
|
|
1951
|
+
);
|
|
1952
|
+
return 0;
|
|
1953
|
+
}
|
|
1954
|
+
function pagesListQuery(argv) {
|
|
1955
|
+
return {
|
|
1956
|
+
sort: flagValue(argv, "--sort"),
|
|
1957
|
+
search_term: flagValue(argv, "--search"),
|
|
1958
|
+
per_page: flagValue(argv, "--page-size")
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function normalizePage(page) {
|
|
1962
|
+
return {
|
|
1963
|
+
id: page.page_id === void 0 ? void 0 : String(page.page_id),
|
|
1964
|
+
url: page.url,
|
|
1965
|
+
title: page.title,
|
|
1966
|
+
createdAt: page.created_at,
|
|
1967
|
+
updatedAt: page.updated_at,
|
|
1968
|
+
editingRoles: page.editing_roles,
|
|
1969
|
+
lastEditedBy: page.last_edited_by,
|
|
1970
|
+
body: page.body,
|
|
1971
|
+
published: page.published,
|
|
1972
|
+
hideFromStudents: page.hide_from_students,
|
|
1973
|
+
frontPage: page.front_page,
|
|
1974
|
+
htmlUrl: page.html_url,
|
|
1975
|
+
lockedForUser: page.locked_for_user,
|
|
1976
|
+
lockInfo: page.lock_info,
|
|
1977
|
+
lockExplanation: page.lock_explanation
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function missing4(message) {
|
|
1981
|
+
throw new Error(message);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// src/commands/review.ts
|
|
1985
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
1986
|
+
import { join as join5 } from "path";
|
|
1987
|
+
|
|
1988
|
+
// src/commands/tabs.ts
|
|
1989
|
+
async function handleTabsCommand(argv, options) {
|
|
1990
|
+
const [subcommand] = argv;
|
|
1991
|
+
if (subcommand !== "list") {
|
|
1009
1992
|
await writeOutput(
|
|
1010
1993
|
{
|
|
1011
1994
|
ok: false,
|
|
@@ -1056,8 +2039,122 @@ function normalizeTab(tab) {
|
|
|
1056
2039
|
};
|
|
1057
2040
|
}
|
|
1058
2041
|
|
|
2042
|
+
// src/commands/review.ts
|
|
2043
|
+
async function handleReviewCommand(argv, options) {
|
|
2044
|
+
const [subcommand] = argv;
|
|
2045
|
+
try {
|
|
2046
|
+
if (subcommand === "pack") {
|
|
2047
|
+
return await reviewPack(argv.slice(1), options);
|
|
2048
|
+
}
|
|
2049
|
+
await writeOutput(
|
|
2050
|
+
{
|
|
2051
|
+
ok: false,
|
|
2052
|
+
error: {
|
|
2053
|
+
code: "UNKNOWN_COMMAND",
|
|
2054
|
+
message: `Unknown review command: ${argv.join(" ")}`,
|
|
2055
|
+
retryable: false
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
options
|
|
2059
|
+
);
|
|
2060
|
+
return 1;
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
await writeOutput(toErrorEnvelope(error), options);
|
|
2063
|
+
return 1;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
async function reviewPack(argv, options) {
|
|
2067
|
+
const courseId = requiredFlag(
|
|
2068
|
+
argv,
|
|
2069
|
+
"--course-id",
|
|
2070
|
+
"Usage: canvas review pack --course-id <course-id> --out <dir>"
|
|
2071
|
+
);
|
|
2072
|
+
const outDir = requiredFlag(argv, "--out", "Usage: canvas review pack --course-id <course-id> --out <dir>");
|
|
2073
|
+
const includeAllFiles = hasFlag(argv, "--include-all-files");
|
|
2074
|
+
const { client, profile } = await activeCanvas();
|
|
2075
|
+
const [course, tabs, modules, assignments, pages, files] = await Promise.all([
|
|
2076
|
+
client.get(`/api/v1/courses/${courseId}`, {
|
|
2077
|
+
query: { "include[]": ["term", "course_image"] }
|
|
2078
|
+
}),
|
|
2079
|
+
client.get(`/api/v1/courses/${courseId}/tabs`).catch(() => ({ data: [] })),
|
|
2080
|
+
client.get(`/api/v1/courses/${courseId}/modules`, {
|
|
2081
|
+
query: { "include[]": ["items", "content_details"], per_page: 100 },
|
|
2082
|
+
pageAll: true
|
|
2083
|
+
}).catch(() => ({ data: [] })),
|
|
2084
|
+
client.get(`/api/v1/courses/${courseId}/assignments`, {
|
|
2085
|
+
query: { per_page: 100 },
|
|
2086
|
+
pageAll: true
|
|
2087
|
+
}).catch(() => ({ data: [] })),
|
|
2088
|
+
client.get(`/api/v1/courses/${courseId}/pages`, {
|
|
2089
|
+
query: { per_page: 100 },
|
|
2090
|
+
pageAll: true
|
|
2091
|
+
}).catch(() => ({ data: [] })),
|
|
2092
|
+
includeAllFiles ? client.get(`/api/v1/courses/${courseId}/files`, {
|
|
2093
|
+
query: { per_page: 100 },
|
|
2094
|
+
pageAll: true
|
|
2095
|
+
}).catch(() => ({ data: [] })) : Promise.resolve({ data: [] })
|
|
2096
|
+
]);
|
|
2097
|
+
const pack = {
|
|
2098
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2099
|
+
baseUrl: profile.baseUrl,
|
|
2100
|
+
course: normalizeCourse(course.data),
|
|
2101
|
+
tabs: tabs.data.map(normalizeTab),
|
|
2102
|
+
modules: modules.data.map(normalizeModule),
|
|
2103
|
+
assignments: assignments.data.map(normalizeAssignment),
|
|
2104
|
+
pages: pages.data.map(normalizePage),
|
|
2105
|
+
files: files.data.map(normalizeFile),
|
|
2106
|
+
notes: [
|
|
2107
|
+
"This review pack preserves Canvas IDs and visible course structure.",
|
|
2108
|
+
includeAllFiles ? "All visible course files metadata was included; file bytes are not downloaded by this command yet." : "All-files metadata is omitted by default. Re-run with --include-all-files to include visible file metadata."
|
|
2109
|
+
]
|
|
2110
|
+
};
|
|
2111
|
+
await mkdir6(outDir, { recursive: true });
|
|
2112
|
+
const manifestPath = join5(outDir, "manifest.json");
|
|
2113
|
+
const coursePath = join5(outDir, "course.json");
|
|
2114
|
+
const modulesPath = join5(outDir, "modules.json");
|
|
2115
|
+
const assignmentsPath = join5(outDir, "assignments.json");
|
|
2116
|
+
const pagesPath = join5(outDir, "pages.json");
|
|
2117
|
+
await Promise.all([
|
|
2118
|
+
writeFile6(manifestPath, `${JSON.stringify(pack, null, 2)}
|
|
2119
|
+
`, "utf8"),
|
|
2120
|
+
writeFile6(coursePath, `${JSON.stringify(pack.course, null, 2)}
|
|
2121
|
+
`, "utf8"),
|
|
2122
|
+
writeFile6(modulesPath, `${JSON.stringify(pack.modules, null, 2)}
|
|
2123
|
+
`, "utf8"),
|
|
2124
|
+
writeFile6(assignmentsPath, `${JSON.stringify(pack.assignments, null, 2)}
|
|
2125
|
+
`, "utf8"),
|
|
2126
|
+
writeFile6(pagesPath, `${JSON.stringify(pack.pages, null, 2)}
|
|
2127
|
+
`, "utf8")
|
|
2128
|
+
]);
|
|
2129
|
+
await writeOutput(
|
|
2130
|
+
{
|
|
2131
|
+
ok: true,
|
|
2132
|
+
data: {
|
|
2133
|
+
course: pack.course,
|
|
2134
|
+
counts: {
|
|
2135
|
+
tabs: pack.tabs.length,
|
|
2136
|
+
modules: pack.modules.length,
|
|
2137
|
+
assignments: pack.assignments.length,
|
|
2138
|
+
pages: pack.pages.length,
|
|
2139
|
+
files: pack.files.length
|
|
2140
|
+
},
|
|
2141
|
+
written: [
|
|
2142
|
+
{ path: manifestPath, kind: "manifest" },
|
|
2143
|
+
{ path: coursePath, kind: "course-json" },
|
|
2144
|
+
{ path: modulesPath, kind: "modules-json" },
|
|
2145
|
+
{ path: assignmentsPath, kind: "assignments-json" },
|
|
2146
|
+
{ path: pagesPath, kind: "pages-json" }
|
|
2147
|
+
]
|
|
2148
|
+
},
|
|
2149
|
+
meta: { baseUrl: profile.baseUrl }
|
|
2150
|
+
},
|
|
2151
|
+
options
|
|
2152
|
+
);
|
|
2153
|
+
return 0;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
1059
2156
|
// src/bin/canvas.ts
|
|
1060
|
-
var VERSION = "0.0.
|
|
2157
|
+
var VERSION = "0.0.2";
|
|
1061
2158
|
function helpText() {
|
|
1062
2159
|
return `canvas \u2014 Canvas LMS CLI for students and agents.
|
|
1063
2160
|
|
|
@@ -1070,9 +2167,17 @@ COMMANDS:
|
|
|
1070
2167
|
auth logout Remove local Canvas auth config
|
|
1071
2168
|
config show Show redacted local config
|
|
1072
2169
|
me Show current Canvas user profile
|
|
1073
|
-
context show Show cached post-login context
|
|
1074
2170
|
courses list List active Canvas courses
|
|
2171
|
+
courses overview Summarize course setup
|
|
2172
|
+
tabs list List course tabs
|
|
2173
|
+
modules list List course modules
|
|
2174
|
+
modules items List module items
|
|
2175
|
+
assignments list List course assignments
|
|
2176
|
+
pages list List course pages
|
|
2177
|
+
files list List course files
|
|
2178
|
+
folders list List course folders
|
|
1075
2179
|
review pack Create a local course review pack
|
|
2180
|
+
api get Raw read-only Canvas API GET
|
|
1076
2181
|
version Print CLI version
|
|
1077
2182
|
|
|
1078
2183
|
FLAGS:
|
|
@@ -1080,7 +2185,8 @@ FLAGS:
|
|
|
1080
2185
|
--format <fmt> Output format: json | pretty | table | ndjson
|
|
1081
2186
|
|
|
1082
2187
|
MVP STATUS:
|
|
1083
|
-
|
|
2188
|
+
Read-only student commands are available for auth, courses, tabs, modules,
|
|
2189
|
+
assignments, pages, files, folders, review packs, and raw GET.
|
|
1084
2190
|
`;
|
|
1085
2191
|
}
|
|
1086
2192
|
async function main(argv) {
|
|
@@ -1116,6 +2222,27 @@ async function main(argv) {
|
|
|
1116
2222
|
if (command === "tabs") {
|
|
1117
2223
|
return handleTabsCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
1118
2224
|
}
|
|
2225
|
+
if (command === "modules") {
|
|
2226
|
+
return handleModulesCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2227
|
+
}
|
|
2228
|
+
if (command === "assignments") {
|
|
2229
|
+
return handleAssignmentsCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2230
|
+
}
|
|
2231
|
+
if (command === "pages") {
|
|
2232
|
+
return handlePagesCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2233
|
+
}
|
|
2234
|
+
if (command === "files") {
|
|
2235
|
+
return handleFilesCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2236
|
+
}
|
|
2237
|
+
if (command === "folders") {
|
|
2238
|
+
return handleFoldersCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2239
|
+
}
|
|
2240
|
+
if (command === "review") {
|
|
2241
|
+
return handleReviewCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2242
|
+
}
|
|
2243
|
+
if (command === "api") {
|
|
2244
|
+
return handleApiCommand(parsed.argv.slice(1), { format: parsed.format });
|
|
2245
|
+
}
|
|
1119
2246
|
await writeOutput(
|
|
1120
2247
|
{
|
|
1121
2248
|
ok: false,
|