moodle-cli 0.9.1 → 0.9.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/dist/moodle.js CHANGED
@@ -3719,7 +3719,7 @@ import {
3719
3719
  colorEnabled as colorEnabled2,
3720
3720
  confirm,
3721
3721
  createProgram,
3722
- createTheme as createTheme3,
3722
+ createTheme as createTheme5,
3723
3723
  createUi as createUi3,
3724
3724
  detectAudience,
3725
3725
  examples,
@@ -7156,7 +7156,7 @@ async function readAll(input2) {
7156
7156
  }
7157
7157
 
7158
7158
  // src/version.ts
7159
- var VERSION = "0.9.1";
7159
+ var VERSION = "0.9.2";
7160
7160
 
7161
7161
  // src/forum.ts
7162
7162
  function parseDiscussionReference(value) {
@@ -7310,6 +7310,7 @@ function resolveTopLevelUrl(baseUrlOrOptions, targetValue, resolveCourseIdForUrl
7310
7310
  }
7311
7311
 
7312
7312
  // src/mcp/cli.ts
7313
+ import { createTheme as createTheme4 } from "@bunizao/cli-kit";
7313
7314
  import { createHash as createHash3 } from "crypto";
7314
7315
  import { readFile as readFile9 } from "fs/promises";
7315
7316
  import { homedir as homedir14 } from "os";
@@ -7934,6 +7935,8 @@ function isMissing3(error) {
7934
7935
  }
7935
7936
 
7936
7937
  // src/mcp/deployment/onboarding.ts
7938
+ import { createTheme as createTheme3 } from "@bunizao/cli-kit";
7939
+ var PLAIN = createTheme3(false);
7937
7940
  var ONBOARDING_STAGES = [
7938
7941
  { id: "validate_moodle_session", index: 1, label: "Validating Moodle session" },
7939
7942
  { id: "check_cloudflare_access", index: 2, label: "Checking Cloudflare access" },
@@ -8025,39 +8028,41 @@ var ONBOARDING_COPY = {
8025
8028
  "Wrangler will request Cloudflare authorization again."
8026
8029
  ].join("\n")
8027
8030
  };
8028
- function formatOnboardingStage(stageId, status) {
8031
+ function formatOnboardingStage(stageId, status, theme = PLAIN) {
8029
8032
  const stage = ONBOARDING_STAGES.find((item) => item.id === stageId);
8030
8033
  if (!stage) {
8031
8034
  throw new Error(`Unknown onboarding stage: ${stageId}`);
8032
8035
  }
8033
8036
  const line = `[${stage.index}/8] ${stage.label}`;
8034
- return status === "completed" ? `\u2713 ${line}` : line;
8037
+ return status === "completed" ? `${theme.tone("success", "\u2713")} ${line}` : line;
8035
8038
  }
8036
- function successfulDeploymentCopy(input2) {
8037
- const connectedClients = input2.clients.length ? input2.clients.map((client) => ` \u2713 ${client}`).join("\n") : " No supported clients detected";
8039
+ function successfulDeploymentCopy(input2, theme = PLAIN) {
8040
+ const connectedClients = input2.clients.length ? input2.clients.map((client) => ` ${theme.tone("success", "\u2713")} ${client}`).join("\n") : ` ${theme.dim("No supported clients detected")}`;
8041
+ const field = (label, value) => ` ${theme.dim(`${label}:`)} ${value}`;
8038
8042
  return [
8039
- "Moodle MCP is ready.",
8043
+ theme.tone("success", "Moodle MCP is ready."),
8040
8044
  "",
8041
- "Endpoint",
8042
- ` ${input2.endpoint}`,
8045
+ theme.subject("Endpoint"),
8046
+ // The one line a person copies into a client, so it reads as an identifier, not a footnote.
8047
+ ` ${theme.key(input2.endpoint)}`,
8043
8048
  "",
8044
- "Protocol",
8045
- " MCP 2026-07-28",
8046
- " Stateless legacy compatibility: 2025-11-25",
8049
+ theme.subject("Protocol"),
8050
+ ` ${theme.dim("MCP")} ${MODERN_PROTOCOL_VERSION}`,
8051
+ ` ${theme.dim(`Stateless legacy compatibility: ${LEGACY_PROTOCOL_VERSION}`)}`,
8047
8052
  "",
8048
- "Moodle",
8049
- ` Site: ${input2.moodleSite}`,
8050
- ` User: ${input2.moodleUser}`,
8051
- " Session: ready",
8053
+ theme.subject("Moodle"),
8054
+ field("Site", theme.target(input2.moodleSite)),
8055
+ field("User", input2.moodleUser),
8056
+ field("Session", theme.status("ready", { ready: "success" })),
8052
8057
  "",
8053
- "Renewal",
8054
- " Installed on this computer",
8055
- " Next check: within 30 minutes",
8058
+ theme.subject("Renewal"),
8059
+ ` ${theme.dim("Installed on this computer")}`,
8060
+ ` ${theme.dim("Next check: within 30 minutes")}`,
8056
8061
  "",
8057
- "Connected clients",
8062
+ theme.subject("Connected clients"),
8058
8063
  connectedClients,
8059
8064
  "",
8060
- "Run `moodle mcp status` at any time."
8065
+ `Run ${theme.key("moodle mcp status")} at any time.`
8061
8066
  ].join("\n");
8062
8067
  }
8063
8068
 
@@ -8367,7 +8372,8 @@ var ManagedMcpDeployment = class {
8367
8372
  clientsConnected: await this.dependencies.clients.inspect(profile),
8368
8373
  readiness: "unknown",
8369
8374
  readinessReasonCode: "NOT_DEPLOYED",
8370
- sessionRevision: null
8375
+ sessionRevision: null,
8376
+ recoveryActive: false
8371
8377
  };
8372
8378
  }
8373
8379
  const [worker, credentials, renewalInstalled, clientsConnected] = await Promise.all([
@@ -8396,7 +8402,8 @@ var ManagedMcpDeployment = class {
8396
8402
  clientsConnected,
8397
8403
  readiness,
8398
8404
  readinessReasonCode,
8399
- sessionRevision
8405
+ sessionRevision,
8406
+ recoveryActive: receipt.releaseDigest.endsWith("-recovery")
8400
8407
  };
8401
8408
  }
8402
8409
  async recover(profile) {
@@ -9136,28 +9143,25 @@ var FetchManagedWorkerClient = class {
9136
9143
  }
9137
9144
  await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "server/discover", {}, 1);
9138
9145
  await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/list", {}, 2);
9139
- const userResult = await this.mcpCall(
9140
- input2.endpoint,
9141
- input2.mcpAccessToken,
9142
- "tools/call",
9143
- { name: "get_user", arguments: {} },
9144
- 3
9145
- );
9146
- const moodleUser = mcpUserFullname(userResult);
9146
+ const homeResult = await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/call", { name: "home", arguments: {} }, 3);
9147
+ const moodleUser = smokeMoodleUser(homeResult);
9147
9148
  if (!moodleUser) {
9148
- throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP get_user check returned no Moodle user");
9149
+ throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP home check returned no Moodle user");
9149
9150
  }
9150
- const coursesResult = await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/call", { name: "list_courses", arguments: { limit: 1 } }, 4);
9151
- const courses = readableToolResult(coursesResult).courses;
9152
- if (!Array.isArray(courses)) throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP list_courses returned no course list");
9153
- if (courses.length) {
9154
- const first2 = courses[0];
9151
+ const unitsResult = await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/call", { name: "units", arguments: { limit: 1 } }, 4);
9152
+ const units = readableToolResult(unitsResult).units;
9153
+ if (!Array.isArray(units)) throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP units returned no unit list");
9154
+ if (units.length) {
9155
+ const first2 = units[0];
9155
9156
  if (!isRecord9(first2) || !Number.isSafeInteger(first2.id) || Number(first2.id) <= 0) {
9156
- throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP list_courses returned no usable course ID");
9157
+ throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP units returned no usable unit ID");
9158
+ }
9159
+ const detail = readableToolResult(await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/call", { name: "unit", arguments: { unit: first2.id } }, 5));
9160
+ if (!isRecord9(detail.unit) || detail.unit.id !== first2.id) {
9161
+ throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP unit lookup did not match the listed unit");
9157
9162
  }
9158
- const detail = readableToolResult(await this.mcpCall(input2.endpoint, input2.mcpAccessToken, "tools/call", { name: "get_course", arguments: { courseId: first2.id } }, 5));
9159
- if (!isRecord9(detail.course) || !isRecord9(detail.course.course) || detail.course.course.id !== first2.id || !Array.isArray(detail.course.sections)) {
9160
- throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP course lookup did not match the listed course");
9163
+ if (detail.sections !== void 0 && !Array.isArray(detail.sections)) {
9164
+ throw new DeploymentApplyError("MCP_SMOKE_FAILED", "MCP unit lookup returned an unusable section list");
9161
9165
  }
9162
9166
  }
9163
9167
  return { moodleUser };
@@ -9373,9 +9377,9 @@ function readableToolResult(result) {
9373
9377
  }
9374
9378
  throw new DeploymentApplyError("MCP_CONTENT_INCOMPLETE", "MCP text content is missing the complete structured result");
9375
9379
  }
9376
- function mcpUserFullname(result) {
9377
- const user = readableToolResult(result).user;
9378
- return isRecord9(user) && typeof user.fullname === "string" && user.fullname.trim() ? user.fullname.trim() : null;
9380
+ function smokeMoodleUser(result) {
9381
+ const home = readableToolResult(result).home;
9382
+ return isRecord9(home) && typeof home.name === "string" && home.name.trim() ? home.name.trim() : null;
9379
9383
  }
9380
9384
  function parseJsonOutput(output) {
9381
9385
  const candidates = [output.indexOf("{"), output.indexOf("[")].filter((index) => index >= 0).sort((a, b) => a - b);
@@ -9862,6 +9866,7 @@ var DefaultMcpCommandService = class {
9862
9866
  progressReporter;
9863
9867
  async deploy(input2) {
9864
9868
  await this.prepareToolchain(input2.yes);
9869
+ const theme = this.theme();
9865
9870
  const progress = this.progress();
9866
9871
  try {
9867
9872
  progress.begin("Reading Cloudflare account and deployment state");
@@ -9904,8 +9909,8 @@ var DefaultMcpCommandService = class {
9904
9909
  const events = [];
9905
9910
  for await (const event2 of deployment.apply(plan)) {
9906
9911
  events.push(event2);
9907
- if (event2.status === "started") progress.begin(formatOnboardingStage(event2.stageId, "pending"));
9908
- else if (event2.status === "completed") progress.end(formatOnboardingStage(event2.stageId, "completed"));
9912
+ if (event2.status === "started") progress.begin(formatOnboardingStage(event2.stageId, "pending", theme));
9913
+ else if (event2.status === "completed") progress.end(formatOnboardingStage(event2.stageId, "completed", theme));
9909
9914
  else progress.clear();
9910
9915
  }
9911
9916
  progress.begin("Reading deployment status");
@@ -9928,7 +9933,7 @@ var DefaultMcpCommandService = class {
9928
9933
  moodleSite: identity.moodleOrigin,
9929
9934
  moodleUser: events.find((event2) => event2.moodleUser)?.moodleUser ?? "Unknown Moodle user",
9930
9935
  clients: await this.connectedClientNames(identity.profile)
9931
- })
9936
+ }, this.theme())
9932
9937
  };
9933
9938
  }
9934
9939
  async planDeployment(deployment, initialIntent) {
@@ -10000,16 +10005,19 @@ var DefaultMcpCommandService = class {
10000
10005
  ...input2.verbose ? { serviceVersion: VERSION } : {},
10001
10006
  ...input2.logs ? { logs: { available: false, reason: "live_tail_required" } } : {}
10002
10007
  };
10008
+ const theme = this.theme();
10009
+ const row = (label, value) => `${theme.dim(`${label}:`)} ${theme.status(value, { pass: "success", warn: "warning", fail: "danger", unknown: "muted", "not deployed": "warning", missing: "warning", "not connected": "warning", stored: "success", installed: "success", connected: "success" })}`;
10003
10010
  return {
10004
10011
  data,
10005
10012
  text: [
10006
- `Moodle MCP: ${managed.readiness}`,
10007
- `Worker: ${managed.worker?.workerName ?? "not deployed"}`,
10008
- `Credentials: ${managed.credentialsStored ? "stored" : "missing"}`,
10009
- `Renewal: ${managed.renewalInstalled ? "installed" : "missing"}`,
10010
- `Clients: ${managed.clientsConnected ? "connected" : "not connected"}`,
10011
- ...updateAvailable ? ["Update: remote Worker is behind this CLI. Run `moodle mcp deploy` to update it."] : [],
10012
- ...input2.logs ? ["Logs: use a live sanitized tail from an interactive terminal"] : []
10013
+ row("Moodle MCP", managed.readiness),
10014
+ row("Worker", managed.worker?.workerName ?? "not deployed"),
10015
+ row("Credentials", managed.credentialsStored ? "stored" : "missing"),
10016
+ row("Renewal", managed.renewalInstalled ? "installed" : "missing"),
10017
+ row("Clients", managed.clientsConnected ? "connected" : "not connected"),
10018
+ ...managed.recoveryActive ? [`${theme.dim("Release:")} ${theme.tone("warning", "the recovery Worker is live; OAuth sign-in is disabled until")} ${theme.key("moodle mcp deploy")} ${theme.tone("warning", "succeeds.")}`] : [],
10019
+ ...updateAvailable ? [`${theme.dim("Update:")} remote Worker is behind this CLI. Run ${theme.key("moodle mcp deploy")} to update it.`] : [],
10020
+ ...input2.logs ? [`${theme.dim("Logs:")} use a live sanitized tail from an interactive terminal`] : []
10013
10021
  ].join("\n")
10014
10022
  };
10015
10023
  }
@@ -10431,6 +10439,9 @@ Selection: `)).trim());
10431
10439
  }
10432
10440
  // Progress belongs on stderr so that --json and --yaml keep stdout to themselves.
10433
10441
  // One shared reporter, so anything else that writes can stop the animation first.
10442
+ theme() {
10443
+ return createTheme4(this.options.color?.() ?? false);
10444
+ }
10434
10445
  progress() {
10435
10446
  this.progressReporter ??= createProgressReporter({ stream: this.options.stderr ?? process.stderr });
10436
10447
  return this.progressReporter;
@@ -10674,7 +10685,9 @@ ${tryLines(["moodle due", "moodle units", "moodle --help"])}`}
10674
10685
  stdin: io.stdin,
10675
10686
  stdout,
10676
10687
  stderr,
10677
- fetchImpl: io.fetchImpl
10688
+ fetchImpl: io.fetchImpl,
10689
+ // Progress goes to stderr and the summary to stdout, so both streams must want colour.
10690
+ color: () => colorEnabled2(stdout, io.env) && colorEnabled2(stderr, io.env) && program.opts().color !== false
10678
10691
  });
10679
10692
  return mcpService;
10680
10693
  };
@@ -10689,7 +10702,7 @@ ${tryLines(["moodle due", "moodle units", "moodle --help"])}`}
10689
10702
  env: io.env ?? process.env,
10690
10703
  format: outputFormat(program.opts(), stdout)
10691
10704
  }) === "human";
10692
- const theme = () => createTheme3(colorEnabled2(stderr, io.env) && program.opts().color !== false);
10705
+ const theme = () => createTheme5(colorEnabled2(stderr, io.env) && program.opts().color !== false);
10693
10706
  const signIn = async (baseUrl) => {
10694
10707
  const ui = createUi3({ input: io.stdin ?? process.stdin, output: stderr, interactive: true });
10695
10708
  const auth2 = { env: io.env, fetch: io.fetchImpl, homeDir: io.homeDir, captureMobileToken: true };
@@ -26452,7 +26452,7 @@ function stringValue(value) {
26452
26452
  }
26453
26453
 
26454
26454
  // src/version.ts
26455
- var VERSION = "0.9.1";
26455
+ var VERSION = "0.9.2";
26456
26456
 
26457
26457
  // src/worker/http.ts
26458
26458
  var HEALTH_PATH = "/healthz";
@@ -26452,7 +26452,7 @@ function stringValue(value) {
26452
26452
  }
26453
26453
 
26454
26454
  // src/version.ts
26455
- var VERSION = "0.9.1";
26455
+ var VERSION = "0.9.2";
26456
26456
 
26457
26457
  // src/worker/http.ts
26458
26458
  var HEALTH_PATH = "/healthz";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moodle-cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Terminal-first CLI for Moodle LMS",
5
5
  "license": "MIT",
6
6
  "type": "module",