@peterseibel/bhs-content
v0.8.2
Published
Shared client library and CLI for the bhs-cs content, question-bank, and grading APIs.
Downloads
3,069
Readme
@peterseibel/bhs-content
Shared client library and CLI for the bhs-cs server APIs: content-file sync, course calendars and lesson plans, the question bank and quizzes, student-work reads for grading, and grades write-back. Used by bhs-cs's push scripts, the lesson-planning app, and the grading scripts.
This is a repo-internal workspace package, not published to npm: it
lives and versions with the server so client and API move together, and the
apps link it through the npm workspace symlinks (buildless TypeScript — its
exports point at src/*.ts).
Requires Node >= 20 (uses global fetch).
Library
import { BhsContentClient, contentSha } from '@peterseibel/bhs-content';
const client = new BhsContentClient({
server: 'https://bhs-cs.example.com',
apiKey: process.env.BHS_CS_API_KEY,
});Every method sends Authorization: Bearer <apiKey>. Non-2xx responses throw
an Error carrying status (the HTTP status) and body (the response body
text). Generic verbs get(url), post(url, body?, contentType?),
put(url, body?, contentType?), and delete(url) are available alongside the
typed methods; bodies are JSON-encoded unless another content type is given.
Content files
The server stores pushable content under a content root mirroring the serving
roots views/, public/, static/, and standards/. Paths are always
relative to that root.
contentManifest(prefix?)→{ files: { path: { sha256, size, mtime } } }(mtime is epoch ms).getContentFile(path)→Bufferof the raw bytes.putContentFile(path, bytes)→{ ok, path, sha256, size }.deleteContentFile(path).batchContent(tarballBuffer)— POST a gzipped tar of files; the tar may include a special.bhs-content/delete.jsonentry (a JSON array of paths) which the server processes as deletions. Returns{ ok, written: [...], deleted: [...] }.diffContent(localDir, { prefix? })→{ added, changed, removed, unchanged }(arrays of paths) vs the server manifest.syncContent(localDir, { prefix?, delete?, dryRun? })— the manifest-diff push. WalkslocalDir(whose layout mirrors the roots, e.g.localDir/views/...,localDir/static/...), sha256s each file, diffs against the server manifest, and pushes all additions and changes as a single batch tarball. Withdelete: trueit also removes server files missing locally (limited toprefixwhen given). Returns{ pushed: [...], deleted: [...], unchanged: n }; withdryRun: trueit returns the plan without pushing.
buildContentTarball(localDir, files, deletions?) (exported) builds the batch
tarball by hand if you need it.
Calendar and lesson plans
putCalendar(courseId, calendar)/getCalendar(courseId)— calendar is{ units: [ { title, weeks?, children: [ { title, type?, days?, children: [ { title } ] } ] } ] }.lessonPlanManifest(courseId)→{ plans: { lessonName: contentSha } }.createLessonPlan(data),getLessonPlans(courseId),getLessonPlan(courseId, lessonName),deleteLessonPlan(courseId, lessonName).
Question bank and quizzes
bankManifest()→{ questions: { uuid: contentSha } }.putQuestion(uuid, question)— question is{ label?, bodyHtml, answerSpec, sourcePath?, contentSha, tags: [...] }; answerSpec is{ choices: [{html,text,correct}...], explanation?, allowedForms?, ordered? }— one list in authored/display order, correctness a per-choice flag.getQuestion(uuid),deleteQuestion(uuid),questionsByTag(tags)(tag intersection).putQuiz(courseId, kind, name, quiz, { force? })— quiz is{ title, questions: [ { uuid, form, options? } ] }(array order is display order);forcepermits removing or form-changing questions that already have answers.getQuiz(...),deleteQuiz(..., { force? }).syncQuestions(questions)— array of{ uuid, ...question }; diffs against the bank manifest by contentSha and pushes only new or changed questions. Returns{ pushed, unchanged }.
Work reads (grading)
classRoster(classId), courseRoster(courseId),
journalEntries(courseId, { promptId?, assignmentId? }),
quizGrid(courseId, kind, name, { classId?, saves? }), codingConfig(url),
assignments(), assignment(id), reflectionGradeData(id?),
completedSpeedruns(), startedSpeedruns(), studentGithubHandles(),
grades().
Grades write-back
postGrades(grades)— array of{ userId, assignmentId, standard, score, grade }.replaceAssignmentGrades(assignmentId, { grades, excused? })— scoped replace for one assignment;excusedis[{ userId, reason }].clearAssignmentGrades(assignmentId),postGradedSpeedruns(list),gradeJournalEntry(entryId, { ok, words?, lateExcuse? }).
Course administration
putCourse(courseId, { title, active? }).
Content shas
contentSha(objOrBuffer) implements the convention used everywhere: sha256
hex of the raw bytes for Buffers and strings (content files), or of the
canonical JSON serialization — JSON.stringify with keys sorted recursively
(canonicalJSON) — for objects (questions, lesson plans).
objectContentSha(obj) is the same minus any uuid/contentSha bookkeeping
fields, which is how the CLI and syncQuestions compute shas for payloads
that carry their own sha.
CLI
npx bhs-content [--server <url>] [--key <apiKey>] [--target <name>] <command>(Registry installs get the bhs-content and push-content bins from the
compiled dist/. Inside the monorepo workspace — which is buildless, no
dist/ — run the sources directly instead:
node bhs-content/src/bin/bhs-content.ts / node
bhs-content/src/bin/push-content.mjs.)
push-content is the separate zero-dep overlay mirror used by the
content-publisher repos (bhs-cs-content, bhs-cs-bhsawesome); see the header
of src/bin/push-content.mjs for its usage.
Server and key resolve in order: explicit --server/--key flags, the named
--target entry in ./.bhs-content.json, then the BHS_CS_SERVER /
BHS_CS_API_KEY environment variables.
.bhs-content.json (in the directory you run the CLI from) maps target names
to server configs:
{
"local": { "server": "http://localhost:3000", "apiKey": "dev-key" },
"prod": { "server": "https://bhs-cs.example.com", "apiKey": "..." }
}Don't commit real keys; keep the file out of version control or keep prod keys in the environment instead.
Commands
# Content sync (dir mirrors views/, public/, static/, standards/)
bhs-content push <dir> [--prefix <p>] [--delete] [--dry-run]
bhs-content status <dir> [--prefix <p>] # summary counts
bhs-content diff <dir> [--prefix <p>] # per-file A/M/D lines
# Lesson plans
bhs-content lesson-plans manifest <course>
bhs-content lesson-plans push <file.json> # computes contentSha from canonical JSON
bhs-content lesson-plans delete <course> <lesson>
# Question bank (file basename or uuid field is the uuid)
bhs-content questions push <file-or-dir>
# Quizzes
bhs-content quiz push <course> <kind> <name> <file.json> [--force]
bhs-content quiz get <course> <kind> <name>
# Grades
bhs-content grades push <file.json> # array → PUT /api/grades
bhs-content grades replace <assignmentId> <file.json> # scoped replace
# Calendar
bhs-content calendar push <course> <file.json>Development
npm install
npm test # node test/smoke.js — no network needed