brightspace-mcp-server 5.0.4 → 5.0.6

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 +126 -19
  2. package/package.json +1 -1
package/build/update.js CHANGED
@@ -15,7 +15,6 @@ 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`;
19
18
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
20
19
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
21
20
  const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -50,6 +49,80 @@ export function samePath(a, b) {
50
49
  };
51
50
  return canonical(a) === canonical(b);
52
51
  }
52
+ /** Stop before anything is modified, saying why and what to do. */
53
+ function refuse(reason, remedy) {
54
+ console.error(red(` Error: ${reason}`));
55
+ console.error(` ${remedy}`);
56
+ process.exit(1);
57
+ }
58
+ const STABLE_VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
59
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
60
+ function versionTriple(version) {
61
+ const m = STABLE_VERSION.exec(version.trim().replace(/^v/, ""));
62
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
63
+ }
64
+ function compareTriples(a, b) {
65
+ for (let i = 0; i < 3; i++) {
66
+ if (a[i] !== b[i])
67
+ return a[i] - b[i];
68
+ }
69
+ return 0;
70
+ }
71
+ /**
72
+ * Whether `nodeVersion` meets an `engines.node` range. Only plain
73
+ * comparator sets (">=20.0.0", ">=20 <23") are understood; anything else
74
+ * yields null so the caller refuses instead of guessing.
75
+ */
76
+ function satisfiesNodeRange(range, nodeVersion) {
77
+ const have = versionTriple(nodeVersion);
78
+ const comparators = range.trim().split(/\s+/);
79
+ if (!have || comparators.length === 0)
80
+ return null;
81
+ for (const comparator of comparators) {
82
+ const m = /^(>=|<=|>|<|=)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(comparator);
83
+ if (!m)
84
+ return null;
85
+ const want = [Number(m[2]), Number(m[3] ?? 0), Number(m[4] ?? 0)];
86
+ const cmp = compareTriples(have, want);
87
+ const ok = { ">=": cmp >= 0, "<=": cmp <= 0, ">": cmp > 0, "<": cmp < 0, "=": cmp === 0 }[m[1] ?? "="];
88
+ if (!ok)
89
+ return false;
90
+ }
91
+ return true;
92
+ }
93
+ /**
94
+ * Decide which commit a checkout should move to, from what npm reports as the
95
+ * `latest` release. Only a stable version, published from a known commit, whose
96
+ * Node.js requirement this runtime meets, is a target; anything else is refused
97
+ * so the update fails closed.
98
+ */
99
+ export function chooseRelease(registry, nodeVersion) {
100
+ const info = (registry ?? {});
101
+ const { version, gitHead } = info;
102
+ if (typeof version !== "string" || !versionTriple(version)) {
103
+ return { ok: false, reason: "npm did not report a stable release version." };
104
+ }
105
+ if (typeof gitHead !== "string" || !COMMIT_SHA.test(gitHead)) {
106
+ return { ok: false, reason: `npm does not record which commit ${version} was published from.` };
107
+ }
108
+ const nodeRange = info.engines?.node;
109
+ if (nodeRange !== undefined) {
110
+ const satisfied = typeof nodeRange === "string" ? satisfiesNodeRange(nodeRange, nodeVersion) : null;
111
+ if (satisfied === null) {
112
+ return { ok: false, reason: `Cannot check ${version}'s Node.js requirement (${String(nodeRange)}).` };
113
+ }
114
+ if (!satisfied) {
115
+ return { ok: false, reason: `${version} requires Node.js ${nodeRange}; this is Node.js ${nodeVersion}.` };
116
+ }
117
+ }
118
+ return { ok: true, version, commit: gitHead };
119
+ }
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
+ }
53
126
  export function main() {
54
127
  console.log("");
55
128
  console.log(bold("=== Brightspace MCP Server — Update ==="));
@@ -81,25 +154,59 @@ export function main() {
81
154
  const currentVersion = getVersion();
82
155
  console.log(` Current version: ${bold(currentVersion)}`);
83
156
  console.log("");
84
- // Check for uncommitted changes
157
+ // The update fast-forwards the checked-out branch to origin/main, so it only
158
+ // runs where that cannot touch local work: a clean working tree on main.
85
159
  const status = run("git status --porcelain", { silent: true });
86
160
  if (status) {
87
- console.log(yellow(" Warning: You have uncommitted changes."));
88
- console.log(dim(" The update will proceed, but your local changes may conflict."));
89
- console.log("");
161
+ refuse("You have uncommitted changes.", "Commit or stash them, then run the update again.");
90
162
  }
91
- // Fetch latest from remote
92
- console.log(dim(" Fetching latest changes..."));
163
+ const branch = run("git branch --show-current", { silent: true });
164
+ if (branch !== "main") {
165
+ refuse(branch ? `You are on branch '${branch}', not main.` : "HEAD is detached, not on main.", "Run 'git switch main', then run the update again.");
166
+ }
167
+ // Resolve the latest published release: the checkout follows what npm
168
+ // ships, never the unreleased tip of main.
169
+ console.log(dim(" Checking the latest release on npm..."));
170
+ let registry;
171
+ try {
172
+ registry = JSON.parse(run("npm view brightspace-mcp-server@latest version gitHead engines --json", { silent: true }));
173
+ }
174
+ catch {
175
+ fail("Could not read the latest release from npm.", "Check your network connection and try again.");
176
+ return;
177
+ }
178
+ const release = chooseRelease(registry, process.version);
179
+ if (!release.ok) {
180
+ fail("No release this checkout can safely update to.", release.reason);
181
+ return;
182
+ }
183
+ const { version: targetVersion, commit } = release;
184
+ // Fetch, then confirm the published commit really is that release on main.
185
+ console.log(dim(` Fetching release ${targetVersion}...`));
93
186
  try {
94
187
  run("git fetch origin main", { silent: true });
95
188
  }
96
189
  catch {
97
- console.error(red(" Error: Failed to fetch from remote."));
98
- console.error(" Check your network connection and try again.");
99
- process.exit(1);
190
+ fail("Failed to fetch from remote.", "Check your network connection and try again.");
191
+ return;
192
+ }
193
+ try {
194
+ run(`git merge-base --is-ancestor ${commit} origin/main`, { silent: true });
195
+ const released = JSON.parse(run(`git show ${commit}:package.json`, { silent: true }));
196
+ if (released.version !== targetVersion)
197
+ throw new Error("version mismatch");
198
+ }
199
+ catch {
200
+ fail(`Could not verify release ${targetVersion} (commit ${commit.slice(0, 7)}) on origin/main.`, "Nothing was changed.");
201
+ return;
202
+ }
203
+ // Local commits on main that origin/main lacks would need a merge or rebase.
204
+ const aheadCount = run("git rev-list --count origin/main..HEAD", { silent: true });
205
+ if (aheadCount !== "0") {
206
+ 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.");
100
207
  }
101
208
  // Check if we're behind
102
- const behindCount = run("git rev-list --count HEAD..origin/main", { silent: true });
209
+ const behindCount = run(`git rev-list --count HEAD..${commit}`, { silent: true });
103
210
  if (behindCount === "0") {
104
211
  console.log("");
105
212
  console.log(green(" Already up to date!"));
@@ -108,22 +215,22 @@ export function main() {
108
215
  }
109
216
  // Show what's new
110
217
  console.log("");
111
- console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"}:`));
218
+ console.log(bold(` ${behindCount} new commit${behindCount === "1" ? "" : "s"} in ${targetVersion}:`));
112
219
  console.log("");
113
- const log = run("git log HEAD..origin/main --oneline", { silent: true });
220
+ const log = run(`git log HEAD..${commit} --oneline`, { silent: true });
114
221
  for (const line of log.split("\n")) {
115
222
  console.log(` ${dim("•")} ${line}`);
116
223
  }
117
224
  console.log("");
118
- // Pull changes
119
- console.log(dim(" Pulling changes..."));
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}...`));
120
228
  try {
121
- run("git pull origin main", { silent: true });
229
+ run(`git merge --ff-only ${commit}`, { silent: true });
122
230
  }
123
231
  catch {
124
- console.error(red(" Error: Failed to pull changes."));
125
- console.error(" You may have merge conflicts. Resolve them manually and try again.");
126
- process.exit(1);
232
+ fail(`Could not fast-forward to ${targetVersion}.`, "Nothing was changed. Check 'git status' and try again.");
233
+ return;
127
234
  }
128
235
  // Install dependencies
129
236
  console.log(dim(" Installing dependencies..."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "5.0.4",
3
+ "version": "5.0.6",
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",