premanmcp 1.0.3 → 1.0.5
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/bin/hook.js +61 -5
- package/bin/shared.js +42 -6
- package/package.json +2 -2
package/bin/hook.js
CHANGED
|
@@ -139,6 +139,57 @@ function isGeneratedInvocation(invocation) {
|
|
|
139
139
|
return invocation === "preman" || /^npm exec -y premanmcp@\S+ --$/.test(invocation);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The release a generated invocation pins, or "" when it does not pin one.
|
|
144
|
+
*
|
|
145
|
+
* The bare `preman` form resolves at push time, and `@latest` is whatever we
|
|
146
|
+
* ship next, so neither can be out of date. Only an explicit version can be.
|
|
147
|
+
*/
|
|
148
|
+
export function pinnedRelease(invocation) {
|
|
149
|
+
const match = /^npm exec -y premanmcp@(\S+) --$/.exec(String(invocation || "").trim());
|
|
150
|
+
const version = match ? match[1] : "";
|
|
151
|
+
return version === "latest" ? "" : version;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Is `left` an earlier release than `right`?
|
|
156
|
+
*
|
|
157
|
+
* Anything this cannot read as three numbers compares as not-older, so a
|
|
158
|
+
* prerelease tag or a mirror's own numbering is left alone rather than being
|
|
159
|
+
* rewritten on a guess.
|
|
160
|
+
*/
|
|
161
|
+
export function isOlderRelease(left, right) {
|
|
162
|
+
const parse = (value) => String(value).split(".").map((part) => Number.parseInt(part, 10));
|
|
163
|
+
const a = parse(left);
|
|
164
|
+
const b = parse(right);
|
|
165
|
+
if (a.length !== 3 || b.length !== 3) return false;
|
|
166
|
+
if ([...a, ...b].some((part) => !Number.isFinite(part))) return false;
|
|
167
|
+
for (let index = 0; index < 3; index += 1) {
|
|
168
|
+
if (a[index] !== b[index]) return a[index] < b[index];
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Does this hook run a PreMan older than the one asking?
|
|
175
|
+
*
|
|
176
|
+
* A pinned hook keeps working long after it stops being current -- it answers
|
|
177
|
+
* `help`, it runs `verify`, it just does all of it as the version that wrote
|
|
178
|
+
* it. Found in the wild: a hook still on 0.13.0 while 1.0.4 was installed,
|
|
179
|
+
* printing an error message whose formatting had been fixed several releases
|
|
180
|
+
* earlier, with nothing anywhere to say why.
|
|
181
|
+
*
|
|
182
|
+
* Only ever true when we would be moving forward. An older CLI run for some
|
|
183
|
+
* other reason must not drag a newer hook back with it.
|
|
184
|
+
*/
|
|
185
|
+
export function hookIsBehind(invocation) {
|
|
186
|
+
if (declaredInvocation()) return false;
|
|
187
|
+
const pinned = pinnedRelease(invocation);
|
|
188
|
+
const running = packageVersion();
|
|
189
|
+
if (!pinned || !running) return false;
|
|
190
|
+
return isOlderRelease(pinned, running);
|
|
191
|
+
}
|
|
192
|
+
|
|
142
193
|
function hookBody(invocation) {
|
|
143
194
|
// `exec` is deliberately absent: we want the wrapper to survive the CLI exiting
|
|
144
195
|
// non-zero and still exit 0 itself.
|
|
@@ -319,9 +370,11 @@ function rememberHookCheck(cwd, now, fields) {
|
|
|
319
370
|
* PreMan is running here -- any command, the MCP server starting -- is taken as
|
|
320
371
|
* the moment to check.
|
|
321
372
|
*
|
|
322
|
-
* Deliberately narrow. It only ever rewrites a hook we wrote
|
|
323
|
-
*
|
|
324
|
-
*
|
|
373
|
+
* Deliberately narrow. It only ever rewrites a hook we wrote: an absent hook is
|
|
374
|
+
* not installed behind the user, and a foreign hook is not touched. A working
|
|
375
|
+
* hook is rewritten only when it pins a release older than the one running --
|
|
376
|
+
* see `hookIsBehind` -- because "it answers" and "it is the CLI we ship" stopped
|
|
377
|
+
* being the same question once a pin could outlive several releases.
|
|
325
378
|
*/
|
|
326
379
|
export function repairDeadHook({ cwd = process.cwd(), now = Date.now(), force = false } = {}) {
|
|
327
380
|
const remember = (fields) => rememberHookCheck(cwd, now, fields);
|
|
@@ -337,11 +390,14 @@ export function repairDeadHook({ cwd = process.cwd(), now = Date.now(), force =
|
|
|
337
390
|
return remember({ action: "not-a-repository" });
|
|
338
391
|
}
|
|
339
392
|
if (status.state !== "installed") return remember({ action: status.state });
|
|
340
|
-
|
|
393
|
+
const behind = hookIsBehind(status.invocation);
|
|
394
|
+
if (status.works && !behind) {
|
|
395
|
+
return remember({ action: "healthy", invocation: status.invocation });
|
|
396
|
+
}
|
|
341
397
|
|
|
342
398
|
const result = installHook(makeArgs([]));
|
|
343
399
|
return remember({
|
|
344
|
-
action: result.action === "updated" ? "repaired" : result.action,
|
|
400
|
+
action: result.action === "updated" ? (behind ? "upgraded" : "repaired") : result.action,
|
|
345
401
|
invocation: result.invocation || "",
|
|
346
402
|
replaced: status.invocation,
|
|
347
403
|
});
|
package/bin/shared.js
CHANGED
|
@@ -482,16 +482,52 @@ export async function callBackendJson(
|
|
|
482
482
|
* instead" reached a customer as `410 [object Object]` -- a remedy the response
|
|
483
483
|
* carried and the screen never showed.
|
|
484
484
|
*/
|
|
485
|
+
/**
|
|
486
|
+
* A readable reason for a backend failure, always as a string.
|
|
487
|
+
*
|
|
488
|
+
* FastAPI answers a rejected request with an object and a validation error with
|
|
489
|
+
* an array of them, and every caller interpolates what comes back into a line
|
|
490
|
+
* somebody reads. The nested cases used to be handed back as they arrived --
|
|
491
|
+
* `detail.message` being itself an object was returned as an object -- so the
|
|
492
|
+
* line printed "[object Object]", which is the exact failure this function
|
|
493
|
+
* exists to prevent, reintroduced one level further down.
|
|
494
|
+
*/
|
|
485
495
|
export function describeFailure(result, fallback = "backend error") {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
496
|
+
return readDetail(result?.detail ?? result?.message ?? result?.raw) || fallback;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** The first thing in `detail` a person could read, however deeply it is wrapped. */
|
|
500
|
+
function readDetail(detail, depth = 0) {
|
|
501
|
+
if (typeof detail === "string") return detail.trim();
|
|
502
|
+
if (typeof detail === "number" || typeof detail === "boolean") return String(detail);
|
|
503
|
+
if (Array.isArray(detail)) {
|
|
504
|
+
return detail
|
|
505
|
+
.map((item) => readDetail(item, depth + 1))
|
|
506
|
+
.filter(Boolean)
|
|
507
|
+
.join("; ");
|
|
490
508
|
}
|
|
491
509
|
if (detail && typeof detail === "object") {
|
|
492
|
-
|
|
510
|
+
// Bounded, because these arrive from somewhere else and a structure that
|
|
511
|
+
// refers to itself must not take the CLI down on its way to a skip message.
|
|
512
|
+
if (depth < 3) {
|
|
513
|
+
for (const key of ["msg", "message", "detail", "error", "description", "code"]) {
|
|
514
|
+
const nested = readDetail(detail[key], depth + 1);
|
|
515
|
+
if (nested) return nested;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const json = JSON.stringify(detail);
|
|
520
|
+
// An empty object describes nothing. Printing "{}" as the reason a push
|
|
521
|
+
// was skipped is the same unhelpfulness as "[object Object]" in nicer
|
|
522
|
+
// punctuation; the caller's fallback at least names the kind of failure.
|
|
523
|
+
return json === "{}" || json === "[]" ? "" : json;
|
|
524
|
+
} catch {
|
|
525
|
+
// Circular, or otherwise not describable. The caller's fallback is better
|
|
526
|
+
// than throwing from the thing that was explaining a failure.
|
|
527
|
+
return "";
|
|
528
|
+
}
|
|
493
529
|
}
|
|
494
|
-
return
|
|
530
|
+
return "";
|
|
495
531
|
}
|
|
496
532
|
|
|
497
533
|
export function assertOk(result, action) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "npm run build && npm run test:proxy",
|
|
16
16
|
"test:proxy": "node --test scripts/smoke-proxy.mjs",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
|
-
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs",
|
|
18
|
+
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs",
|
|
19
19
|
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|