@lukeguo12210/canvas-cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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((resolve, reject) => {
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
- resolve(buffer.trim());
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/me.ts
979
- async function handleMeCommand(options) {
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
- const profile = await new ConfigStore().activeProfile();
982
- const client = new CanvasClient({
983
- baseUrl: profile.baseUrl,
984
- token: profile.token
985
- });
986
- const response = await client.get("/api/v1/users/self/profile");
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: true,
990
- data: response.data,
991
- meta: {
992
- ...response.meta,
993
- baseUrl: profile.baseUrl
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 0;
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
- if (subcommand !== "list") {
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.0";
2157
+ var VERSION = "0.0.3";
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
- Auth/config/me/courses/tabs are in progress. Review commands are planned next.
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,