@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.
- package/dist/main.js +140 -7
- 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.
|
|
4
|
-
"description": "Command-line companion for the Novedu chat app. Validates tutor, fragment, quiz, writing and coding YAML definitions
|
|
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",
|