@novedu/cli 0.7.0 → 0.8.0

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.
Files changed (2) hide show
  1. package/dist/main.js +140 -7
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -6,11 +6,11 @@ import { createServer } from "node:http";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
9
+ import { readFile } from "node:fs/promises";
9
10
  import { fileURLToPath, pathToFileURL } from "node:url";
10
11
  import Handlebars from "handlebars";
11
12
  import { parse } from "yaml";
12
13
  import { z } from "zod";
13
- import { readFile } from "node:fs/promises";
14
14
  //#region src/auth.ts
15
15
  const DEFAULT_TENANT_ID = "91fc072c-edef-4f97-bdc5-cfb67718ae3a";
16
16
  const DEFAULT_CLIENT_ID = "4d44fc4b-0434-4981-9765-62e2074ceecb";
@@ -185,6 +185,143 @@ function displayName(result) {
185
185
  return result.account?.name ?? result.account?.username ?? "(unknown account)";
186
186
  }
187
187
  //#endregion
188
+ //#region src/server-url.ts
189
+ const DEFAULT_SERVER = "https://novedu-chat-mvp-at.azurewebsites.net";
190
+ function resolveServerUrl(cliOption) {
191
+ return cliOption || process.env.NOVEDU_SERVER || DEFAULT_SERVER;
192
+ }
193
+ //#endregion
194
+ //#region src/api.ts
195
+ /** Pretty-prints a success payload to stdout. */
196
+ function printJson(value) {
197
+ console.log(JSON.stringify(value, null, 2));
198
+ }
199
+ /** Prints a failure payload as JSON to stderr and marks the process failed. */
200
+ function failJson(value) {
201
+ console.error(JSON.stringify(value, null, 2));
202
+ process.exitCode = 1;
203
+ }
204
+ /**
205
+ * Performs one authenticated API request and prints the outcome per the JSON
206
+ * contract. Server error bodies (`{ message }` — incl. the generic 401/403 —
207
+ * or `{ errors }`) are passed through VERBATIM to stderr — the server's
208
+ * structured validation detail is the CLI's error message. No client-side
209
+ * pre-validation: the server runs the identical pipeline; offline checking is
210
+ * the `validate` command's job.
211
+ */
212
+ async function runApiRequest(options) {
213
+ let token;
214
+ try {
215
+ token = await getAccessToken();
216
+ } catch (error) {
217
+ if (error instanceof NotSignedInError) {
218
+ failJson({ message: error.message });
219
+ return;
220
+ }
221
+ throw error;
222
+ }
223
+ const server = resolveServerUrl(options.server);
224
+ let response;
225
+ try {
226
+ response = await fetch(new URL(options.path, server), {
227
+ method: options.method ?? "GET",
228
+ headers: {
229
+ authorization: `Bearer ${token}`,
230
+ ...options.body === void 0 ? {} : { "content-type": "application/json" }
231
+ },
232
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
233
+ });
234
+ } catch (error) {
235
+ failJson({ message: `Could not reach ${server}: ${error instanceof Error ? error.message : error}` });
236
+ return;
237
+ }
238
+ let payload;
239
+ try {
240
+ payload = await response.json();
241
+ } catch {
242
+ payload = void 0;
243
+ }
244
+ if (!response.ok) {
245
+ failJson(payload ?? { message: `${server} rejected the request: HTTP ${response.status}` });
246
+ return;
247
+ }
248
+ printJson(payload ?? null);
249
+ }
250
+ //#endregion
251
+ //#region src/commands/codes.ts
252
+ const SERVER_OPTION$1 = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
253
+ function registerCodes(program) {
254
+ const codes = program.command("codes").description("Manage activity codes on the Novedu server");
255
+ codes.command("create").description("Create a code for an activity YAML (validated server-side before storing)").requiredOption("--module <module>", "activity module: tutor, quiz, writing or coding").requiredOption("--file <url>", "public http(s) URL of the activity YAML").option("--start <iso>", "window start, ISO 8601 with explicit offset (e.g. 2026-07-07T08:00:00Z)").option("--end <iso>", "window end, ISO 8601 with explicit offset").option("--note <text>", "note shown in the codes list").option("--llm-provider <provider>", "LLM override provider (\"SCCH\" or \"Azure Foundry\"; needs --llm-model)").option("--llm-model <model>", "LLM override model id (needs --llm-provider)").option(...SERVER_OPTION$1).action(async (options) => {
256
+ await runApiRequest({
257
+ server: options.server,
258
+ path: "/api/codes",
259
+ method: "POST",
260
+ body: {
261
+ module: options.module,
262
+ fileUrl: options.file,
263
+ ...options.start === void 0 ? {} : { validFrom: options.start },
264
+ ...options.end === void 0 ? {} : { validUntil: options.end },
265
+ ...options.note === void 0 ? {} : { note: options.note },
266
+ ...options.llmProvider === void 0 && options.llmModel === void 0 ? {} : { llm: {
267
+ provider: options.llmProvider ?? "",
268
+ model: options.llmModel ?? ""
269
+ } }
270
+ }
271
+ });
272
+ });
273
+ codes.command("list").description("List codes (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over note/code").option("--module <module>", "only codes for one activity module").option("--all", "include codes created by other teachers").option(...SERVER_OPTION$1).action(async (options) => {
274
+ const params = new URLSearchParams();
275
+ if (options.search) params.set("q", options.search);
276
+ if (options.module) params.set("module", options.module);
277
+ if (options.all) params.set("mine", "0");
278
+ const query = params.toString();
279
+ await runApiRequest({
280
+ server: options.server,
281
+ path: `/api/codes${query ? `?${query}` : ""}`
282
+ });
283
+ });
284
+ }
285
+ //#endregion
286
+ //#region src/commands/files.ts
287
+ const SERVER_OPTION = ["--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)"];
288
+ async function readStdin() {
289
+ const chunks = [];
290
+ for await (const chunk of process.stdin) chunks.push(chunk);
291
+ return Buffer.concat(chunks).toString("utf8");
292
+ }
293
+ function registerFiles(program) {
294
+ const files = program.command("files").description("Manage app-hosted YAML files on the Novedu server");
295
+ files.command("upload <name>").description("Create or update an app-hosted YAML file from --file or stdin (validated server-side)").option("--kind <kind>", "file kind (tutor, fragment, quiz, writing, coding) — required when creating").option("--file <path>", "read the YAML from this path instead of stdin").option(...SERVER_OPTION).action(async (name, options) => {
296
+ let content;
297
+ try {
298
+ content = options.file === void 0 ? await readStdin() : await readFile(options.file, "utf8");
299
+ } catch (error) {
300
+ failJson({ message: `Could not read ${options.file}: ${error instanceof Error ? error.message : error}` });
301
+ return;
302
+ }
303
+ await runApiRequest({
304
+ server: options.server,
305
+ path: `/api/files/${encodeURIComponent(name)}`,
306
+ method: "PUT",
307
+ body: {
308
+ ...options.kind === void 0 ? {} : { kind: options.kind },
309
+ content
310
+ }
311
+ });
312
+ });
313
+ files.command("list").description("List app-hosted YAML files (defaults to only your own, like the web list)").option("--search <q>", "contains-filter over name/title/description").option("--all", "include files last written by other teachers").option(...SERVER_OPTION).action(async (options) => {
314
+ const params = new URLSearchParams();
315
+ if (options.search) params.set("q", options.search);
316
+ if (options.all) params.set("mine", "0");
317
+ const query = params.toString();
318
+ await runApiRequest({
319
+ server: options.server,
320
+ path: `/api/files${query ? `?${query}` : ""}`
321
+ });
322
+ });
323
+ }
324
+ //#endregion
188
325
  //#region src/commands/login.ts
189
326
  function registerLogin(program) {
190
327
  program.command("login").description("Sign in to Microsoft Entra ID (opens your browser)").option("--device-code", "sign in with the device code flow instead (for machines without a browser; the tenant must allow it)").addHelpText("after", `
@@ -1310,12 +1447,6 @@ function formatOutcome(outcome, source) {
1310
1447
  }
1311
1448
  }
1312
1449
  //#endregion
1313
- //#region src/server-url.ts
1314
- const DEFAULT_SERVER = "https://novedu-chat-mvp-at.azurewebsites.net";
1315
- function resolveServerUrl(cliOption) {
1316
- return cliOption || process.env.NOVEDU_SERVER || DEFAULT_SERVER;
1317
- }
1318
- //#endregion
1319
1450
  //#region src/commands/whoami.ts
1320
1451
  function registerWhoami(program) {
1321
1452
  program.command("whoami").description("Show who is signed in by calling the Novedu server's /api/me").option("--server <url>", "Novedu server base URL (defaults to the NOVEDU_SERVER env var, then production)").action(async (options) => {
@@ -1359,6 +1490,8 @@ registerValidate(program);
1359
1490
  registerLogin(program);
1360
1491
  registerLogout(program);
1361
1492
  registerWhoami(program);
1493
+ registerCodes(program);
1494
+ registerFiles(program);
1362
1495
  program.parseAsync().catch((err) => {
1363
1496
  console.error(err instanceof Error ? err.message : err);
1364
1497
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novedu/cli",
3
- "version": "0.7.0",
4
- "description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions (more commands to follow).",
3
+ "version": "0.8.0",
4
+ "description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions; signs in with Entra ID and manages codes and app-hosted files over the app's API.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",