brightspace-mcp-server 5.0.6 → 5.0.7

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/build/update.js +91 -38
  2. package/package.json +1 -1
package/build/update.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * https://github.com/rohanmuppa/brightspace-mcp-server
8
8
  */
9
9
  import { execSync } from "node:child_process";
10
- import { readFileSync, realpathSync } from "node:fs";
10
+ import { existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import dotenv from "dotenv";
@@ -15,6 +15,7 @@ dotenv.config({ quiet: true });
15
15
  // ANSI color helpers
16
16
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
17
17
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
18
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
18
19
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
19
20
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
20
21
  const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -27,10 +28,50 @@ function run(cmd, opts) {
27
28
  }).trim();
28
29
  }
29
30
  catch (err) {
31
+ // tsc reports compile errors on stdout and npm adds its summary on
32
+ // stderr, so a failure keeps both.
30
33
  const error = err;
31
- throw new Error(error.stderr || error.message || "Command failed");
34
+ const output = [error.stdout, error.stderr].filter((s) => s?.trim()).join("\n");
35
+ throw new Error(output || error.message || "Command failed");
32
36
  }
33
37
  }
38
+ /** How many trailing lines of a failed command's output are worth showing. */
39
+ const FAILURE_TAIL_LINES = 20;
40
+ /**
41
+ * The end of a failed command's output, fit for a terminal: escape sequences
42
+ * and control characters stripped, blank lines dropped, long logs cut to the
43
+ * last few lines (where npm and tsc put the actual error).
44
+ */
45
+ export function failureDetail(err) {
46
+ if (err === undefined || err === null)
47
+ return [];
48
+ const text = err instanceof Error ? err.message : String(err);
49
+ const lines = text
50
+ .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "")
51
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "")
52
+ .split("\n")
53
+ .map((line) => line.trimEnd())
54
+ .filter((line) => line.trim() !== "");
55
+ return lines.slice(-FAILURE_TAIL_LINES);
56
+ }
57
+ /** Print a failed step with its cause and how to recover, then exit. */
58
+ function fail(summary, err, recovery) {
59
+ console.error(red(` Error: ${summary}`));
60
+ const detail = failureDetail(err);
61
+ if (detail.length > 0) {
62
+ console.error("");
63
+ for (const line of detail)
64
+ console.error(dim(` ${line}`));
65
+ console.error("");
66
+ }
67
+ for (const line of recovery)
68
+ console.error(` ${line}`);
69
+ process.exit(1);
70
+ }
71
+ const RETRY_HINT = [
72
+ "The update is not finished. Fix the problem above, then run `npm run update` again;",
73
+ "it will redo the install and build even though the new code is already pulled.",
74
+ ];
34
75
  function getVersion() {
35
76
  const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
36
77
  return pkg.version || "unknown";
@@ -117,12 +158,6 @@ export function chooseRelease(registry, nodeVersion) {
117
158
  }
118
159
  return { ok: true, version, commit: gitHead };
119
160
  }
120
- function fail(...lines) {
121
- console.error(red(` Error: ${lines[0]}`));
122
- for (const line of lines.slice(1))
123
- console.error(` ${line}`);
124
- process.exit(1);
125
- }
126
161
  export function main() {
127
162
  console.log("");
128
163
  console.log(bold("=== Brightspace MCP Server — Update ==="));
@@ -172,12 +207,12 @@ export function main() {
172
207
  registry = JSON.parse(run("npm view brightspace-mcp-server@latest version gitHead engines --json", { silent: true }));
173
208
  }
174
209
  catch {
175
- fail("Could not read the latest release from npm.", "Check your network connection and try again.");
210
+ fail("Could not read the latest release from npm.", undefined, ["Check your network connection and try again."]);
176
211
  return;
177
212
  }
178
213
  const release = chooseRelease(registry, process.version);
179
214
  if (!release.ok) {
180
- fail("No release this checkout can safely update to.", release.reason);
215
+ fail("No release this checkout can safely update to.", undefined, [release.reason]);
181
216
  return;
182
217
  }
183
218
  const { version: targetVersion, commit } = release;
@@ -186,8 +221,8 @@ export function main() {
186
221
  try {
187
222
  run("git fetch origin main", { silent: true });
188
223
  }
189
- catch {
190
- fail("Failed to fetch from remote.", "Check your network connection and try again.");
224
+ catch (err) {
225
+ fail("Failed to fetch from remote.", err, ["Check your network connection and try again."]);
191
226
  return;
192
227
  }
193
228
  try {
@@ -197,7 +232,7 @@ export function main() {
197
232
  throw new Error("version mismatch");
198
233
  }
199
234
  catch {
200
- fail(`Could not verify release ${targetVersion} (commit ${commit.slice(0, 7)}) on origin/main.`, "Nothing was changed.");
235
+ fail(`Could not verify release ${targetVersion} (commit ${commit.slice(0, 7)}) on origin/main.`, undefined, ["Nothing was changed."]);
201
236
  return;
202
237
  }
203
238
  // Local commits on main that origin/main lacks would need a merge or rebase.
@@ -205,51 +240,69 @@ export function main() {
205
240
  if (aheadCount !== "0") {
206
241
  refuse(`Your main has ${aheadCount} commit${aheadCount === "1" ? "" : "s"} that origin/main does not.`, "Move them to another branch or reconcile main with origin/main yourself, then run the update again.");
207
242
  }
208
- // Check if we're behind
243
+ // A marker in the git directory records that the code was updated but the
244
+ // install or build has not yet succeeded. Without it a retry would see no
245
+ // new commits and report "Already up to date" over a broken build.
246
+ const marker = resolve(projectRoot, run("git rev-parse --git-path brightspace-update-incomplete", { silent: true }));
247
+ const unfinished = existsSync(marker);
209
248
  const behindCount = run(`git rev-list --count HEAD..${commit}`, { silent: true });
210
- if (behindCount === "0") {
249
+ if (behindCount === "0" && !unfinished) {
211
250
  console.log("");
212
251
  console.log(green(" Already up to date!"));
213
252
  console.log("");
214
253
  return;
215
254
  }
216
- // Show what's new
217
- console.log("");
218
- console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
219
- console.log("");
220
- const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
221
- for (const line of log.split("\n")) {
222
- console.log(` ${dim("•")} ${line}`);
223
- }
224
- console.log("");
225
- // Fast-forward only, to the verified release: never a merge or rebase,
226
- // whatever git's pull settings say, and no pull fallback if it fails.
227
- console.log(dim(` Updating to ${targetVersion}...`));
228
- try {
229
- run(`git merge --ff-only ${commit}`, { silent: true });
255
+ if (behindCount === "0") {
256
+ console.log(yellow(" The last update did not finish. Finishing it now."));
230
257
  }
231
- catch {
232
- fail(`Could not fast-forward to ${targetVersion}.`, "Nothing was changed. Check 'git status' and try again.");
233
- return;
258
+ else {
259
+ // Show what's new
260
+ console.log("");
261
+ console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
262
+ console.log("");
263
+ const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
264
+ for (const line of log.split("\n")) {
265
+ console.log(` ${dim("•")} ${line}`);
266
+ }
267
+ console.log("");
268
+ // Recorded before HEAD moves, and cleared only after the build succeeds.
269
+ writeFileSync(marker, `${new Date().toISOString()}\n`);
270
+ // Fast-forward only, to the verified release: never a merge or rebase,
271
+ // whatever git's pull settings say, and no pull fallback if it fails.
272
+ console.log(dim(` Updating to ${targetVersion}...`));
273
+ try {
274
+ run(`git merge --ff-only ${commit}`, { silent: true });
275
+ }
276
+ catch (err) {
277
+ // HEAD did not move, so this run left nothing unfinished.
278
+ if (!unfinished)
279
+ rmSync(marker, { force: true });
280
+ fail(`Could not fast-forward to ${targetVersion}.`, err, ["Nothing was changed. Check 'git status' and try again."]);
281
+ return;
282
+ }
234
283
  }
235
284
  // Install dependencies
236
285
  console.log(dim(" Installing dependencies..."));
237
286
  try {
238
287
  run("npm install", { silent: true });
239
288
  }
240
- catch {
241
- console.error(red(" Error: Failed to install dependencies."));
242
- process.exit(1);
289
+ catch (err) {
290
+ fail("Failed to install dependencies.", err, [
291
+ "If a file is locked (EBUSY/EPERM), close every MCP client using this server first.",
292
+ ...RETRY_HINT,
293
+ ]);
294
+ return;
243
295
  }
244
296
  // Build
245
297
  console.log(dim(" Building..."));
246
298
  try {
247
299
  run("npm run build", { silent: true });
248
300
  }
249
- catch {
250
- console.error(red(" Error: Build failed."));
251
- process.exit(1);
301
+ catch (err) {
302
+ fail("Build failed.", err, RETRY_HINT);
303
+ return;
252
304
  }
305
+ rmSync(marker, { force: true });
253
306
  // Show new version
254
307
  const newVersion = getVersion();
255
308
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "mcpName": "io.github.RohanMuppa/brightspace",
5
5
  "description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
6
6
  "type": "module",