brightspace-mcp-server 5.0.5 → 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.
- package/build/update.js +108 -41
- 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";
|
|
@@ -28,10 +28,50 @@ function run(cmd, opts) {
|
|
|
28
28
|
}).trim();
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
|
+
// tsc reports compile errors on stdout and npm adds its summary on
|
|
32
|
+
// stderr, so a failure keeps both.
|
|
31
33
|
const error = err;
|
|
32
|
-
|
|
34
|
+
const output = [error.stdout, error.stderr].filter((s) => s?.trim()).join("\n");
|
|
35
|
+
throw new Error(output || error.message || "Command failed");
|
|
33
36
|
}
|
|
34
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
|
+
];
|
|
35
75
|
function getVersion() {
|
|
36
76
|
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
|
|
37
77
|
return pkg.version || "unknown";
|
|
@@ -50,6 +90,12 @@ export function samePath(a, b) {
|
|
|
50
90
|
};
|
|
51
91
|
return canonical(a) === canonical(b);
|
|
52
92
|
}
|
|
93
|
+
/** Stop before anything is modified, saying why and what to do. */
|
|
94
|
+
function refuse(reason, remedy) {
|
|
95
|
+
console.error(red(` Error: ${reason}`));
|
|
96
|
+
console.error(` ${remedy}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
53
99
|
const STABLE_VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
54
100
|
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
55
101
|
function versionTriple(version) {
|
|
@@ -112,12 +158,6 @@ export function chooseRelease(registry, nodeVersion) {
|
|
|
112
158
|
}
|
|
113
159
|
return { ok: true, version, commit: gitHead };
|
|
114
160
|
}
|
|
115
|
-
function fail(...lines) {
|
|
116
|
-
console.error(red(` Error: ${lines[0]}`));
|
|
117
|
-
for (const line of lines.slice(1))
|
|
118
|
-
console.error(` ${line}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
161
|
export function main() {
|
|
122
162
|
console.log("");
|
|
123
163
|
console.log(bold("=== Brightspace MCP Server — Update ==="));
|
|
@@ -149,12 +189,15 @@ export function main() {
|
|
|
149
189
|
const currentVersion = getVersion();
|
|
150
190
|
console.log(` Current version: ${bold(currentVersion)}`);
|
|
151
191
|
console.log("");
|
|
152
|
-
//
|
|
192
|
+
// The update fast-forwards the checked-out branch to origin/main, so it only
|
|
193
|
+
// runs where that cannot touch local work: a clean working tree on main.
|
|
153
194
|
const status = run("git status --porcelain", { silent: true });
|
|
154
195
|
if (status) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
196
|
+
refuse("You have uncommitted changes.", "Commit or stash them, then run the update again.");
|
|
197
|
+
}
|
|
198
|
+
const branch = run("git branch --show-current", { silent: true });
|
|
199
|
+
if (branch !== "main") {
|
|
200
|
+
refuse(branch ? `You are on branch '${branch}', not main.` : "HEAD is detached, not on main.", "Run 'git switch main', then run the update again.");
|
|
158
201
|
}
|
|
159
202
|
// Resolve the latest published release: the checkout follows what npm
|
|
160
203
|
// ships, never the unreleased tip of main.
|
|
@@ -164,12 +207,12 @@ export function main() {
|
|
|
164
207
|
registry = JSON.parse(run("npm view brightspace-mcp-server@latest version gitHead engines --json", { silent: true }));
|
|
165
208
|
}
|
|
166
209
|
catch {
|
|
167
|
-
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."]);
|
|
168
211
|
return;
|
|
169
212
|
}
|
|
170
213
|
const release = chooseRelease(registry, process.version);
|
|
171
214
|
if (!release.ok) {
|
|
172
|
-
fail("No release this checkout can safely update to.", release.reason);
|
|
215
|
+
fail("No release this checkout can safely update to.", undefined, [release.reason]);
|
|
173
216
|
return;
|
|
174
217
|
}
|
|
175
218
|
const { version: targetVersion, commit } = release;
|
|
@@ -178,8 +221,8 @@ export function main() {
|
|
|
178
221
|
try {
|
|
179
222
|
run("git fetch origin main", { silent: true });
|
|
180
223
|
}
|
|
181
|
-
catch {
|
|
182
|
-
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."]);
|
|
183
226
|
return;
|
|
184
227
|
}
|
|
185
228
|
try {
|
|
@@ -189,53 +232,77 @@ export function main() {
|
|
|
189
232
|
throw new Error("version mismatch");
|
|
190
233
|
}
|
|
191
234
|
catch {
|
|
192
|
-
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."]);
|
|
193
236
|
return;
|
|
194
237
|
}
|
|
195
|
-
//
|
|
238
|
+
// Local commits on main that origin/main lacks would need a merge or rebase.
|
|
239
|
+
const aheadCount = run("git rev-list --count origin/main..HEAD", { silent: true });
|
|
240
|
+
if (aheadCount !== "0") {
|
|
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.");
|
|
242
|
+
}
|
|
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);
|
|
196
248
|
const behindCount = run(`git rev-list --count HEAD..${commit}`, { silent: true });
|
|
197
|
-
if (behindCount === "0") {
|
|
249
|
+
if (behindCount === "0" && !unfinished) {
|
|
198
250
|
console.log("");
|
|
199
251
|
console.log(green(" Already up to date!"));
|
|
200
252
|
console.log("");
|
|
201
253
|
return;
|
|
202
254
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
|
|
206
|
-
console.log("");
|
|
207
|
-
const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
|
|
208
|
-
for (const line of log.split("\n")) {
|
|
209
|
-
console.log(` ${dim("•")} ${line}`);
|
|
210
|
-
}
|
|
211
|
-
console.log("");
|
|
212
|
-
// Move to the release
|
|
213
|
-
console.log(dim(` Updating to ${targetVersion}...`));
|
|
214
|
-
try {
|
|
215
|
-
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."));
|
|
216
257
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}
|
|
220
283
|
}
|
|
221
284
|
// Install dependencies
|
|
222
285
|
console.log(dim(" Installing dependencies..."));
|
|
223
286
|
try {
|
|
224
287
|
run("npm install", { silent: true });
|
|
225
288
|
}
|
|
226
|
-
catch {
|
|
227
|
-
|
|
228
|
-
|
|
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;
|
|
229
295
|
}
|
|
230
296
|
// Build
|
|
231
297
|
console.log(dim(" Building..."));
|
|
232
298
|
try {
|
|
233
299
|
run("npm run build", { silent: true });
|
|
234
300
|
}
|
|
235
|
-
catch {
|
|
236
|
-
|
|
237
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
fail("Build failed.", err, RETRY_HINT);
|
|
303
|
+
return;
|
|
238
304
|
}
|
|
305
|
+
rmSync(marker, { force: true });
|
|
239
306
|
// Show new version
|
|
240
307
|
const newVersion = getVersion();
|
|
241
308
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brightspace-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
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",
|