premanmcp 0.15.1 → 0.15.3
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/README.md +8 -3
- package/bin/connect/guide.js +43 -44
- package/bin/connect.js +2 -2
- package/bin/desktop.js +61 -4
- package/bin/integrations.js +45 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,9 +114,14 @@ push testing alone, and `--no-guide` connects and nothing else. With `--guide`,
|
|
|
114
114
|
takes every step's default without asking and `--no-runner` / `--no-desktop` /
|
|
115
115
|
`--no-integrations` skip one each.
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
The desktop app is installed rather than offered. It was a question for a while, defaulted
|
|
118
|
+
to no on the grounds that nobody should get a 150MB download by pressing Enter, and what
|
|
119
|
+
that produced was a setup whose last act was to describe the app and decline to install
|
|
120
|
+
it. The build is Developer ID signed and notarized, it is checked against the sha512
|
|
121
|
+
published with the release before anything is copied into `/Applications`, and nothing
|
|
122
|
+
arrives through a browser so there is no quarantine flag and no Gatekeeper warning on
|
|
123
|
+
first launch. `--no-desktop`, or `PREMAN_NO_DESKTOP=1` where no flag can be passed, skips
|
|
124
|
+
it; `install-desktop` does it on its own.
|
|
120
125
|
|
|
121
126
|
In CI or any non-interactive shell, run `connect --agent <name> --api-key pm_live_…`.
|
|
122
127
|
Without `--agent` there is nothing to prompt on, so `connect` prints ready-to-paste
|
package/bin/connect/guide.js
CHANGED
|
@@ -74,11 +74,11 @@ export function whatIsLeftBlock(args) {
|
|
|
74
74
|
/**
|
|
75
75
|
* Ask, or take the step's own default when nobody can answer.
|
|
76
76
|
*
|
|
77
|
-
* `--yes` returns
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
77
|
+
* `--yes` returns each step's own default rather than a blanket yes, so a step
|
|
78
|
+
* that should not ride in on a keypress can say so. The desktop app used to be
|
|
79
|
+
* the only one that did, on the grounds that `--yes` should not mean 150MB into
|
|
80
|
+
* /Applications unattended; it does not ask at all now, and `PREMAN_NO_DESKTOP`
|
|
81
|
+
* or `--no-desktop` is how a run that does not want it says so.
|
|
82
82
|
*/
|
|
83
83
|
export async function confirm(question, { assumeYes = false, defaultYes = true } = {}) {
|
|
84
84
|
if (assumeYes) return defaultYes;
|
|
@@ -387,38 +387,38 @@ async function setUpRunner(args, agent, { assumeYes }) {
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
/**
|
|
391
|
-
* The question, word for word, wherever the desktop app is offered.
|
|
392
|
-
*
|
|
393
|
-
* Shared because both walks ask it and they must not drift: "install the
|
|
394
|
-
* desktop app?" got yes from people who thought they were agreeing to a menu
|
|
395
|
-
* bar icon, then spent two minutes watching an unexplained download. The size
|
|
396
|
-
* and the destination are the two facts that change the answer, and the last
|
|
397
|
-
* clause is there because the honest answer to "do I need this?" is no.
|
|
398
|
-
*/
|
|
399
|
-
export function desktopQuestion(args) {
|
|
400
|
-
const dest = args.value("--dest", "/Applications");
|
|
401
|
-
return `Download the PreMan desktop app? ~150MB, installs to ${dest} — optional, everything works without it`;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
390
|
/** Where this run would put the app, honouring the same --dest the install does. */
|
|
405
391
|
function desktopAppPath(args) {
|
|
406
392
|
return path.join(args.value("--dest", "/Applications"), "PreMan.app");
|
|
407
393
|
}
|
|
408
394
|
|
|
409
395
|
/**
|
|
410
|
-
*
|
|
396
|
+
* An opt-out for runs that cannot pass a flag.
|
|
411
397
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
398
|
+
* A bare `preman` with no arguments is a real entry point and now installs the
|
|
399
|
+
* app, which is right for a customer and wrong for a CI image or a suite that
|
|
400
|
+
* drives the walk end to end. `--no-desktop` cannot reach either of those, so
|
|
401
|
+
* they get the environment variable instead. Same spelling as PREMAN_NO_BROWSER
|
|
402
|
+
* and PREMAN_NO_AGENT, including that 0/false/no mean the opt-out is off.
|
|
403
|
+
*/
|
|
404
|
+
function noDesktopEnv() {
|
|
405
|
+
const optOut = (process.env.PREMAN_NO_DESKTOP || "").trim().toLowerCase();
|
|
406
|
+
return Boolean(optOut) && !["0", "false", "no"].includes(optOut);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Why the desktop app should not be installed here, or null to go ahead.
|
|
411
|
+
*
|
|
412
|
+
* Split out from the step so a caller running its own loop can decide whether
|
|
413
|
+
* the step is worth listing *before* listing it. Both of these were bugs when
|
|
414
|
+
* the check lived after: a Linux user got a macOS-only step, and someone who
|
|
415
|
+
* already had the app watched it download again.
|
|
416
416
|
*/
|
|
417
417
|
export function desktopPrecheck(args) {
|
|
418
|
-
if (args.has("--no-desktop")) return { state: "skipped" };
|
|
418
|
+
if (args.has("--no-desktop") || noDesktopEnv()) return { state: "skipped" };
|
|
419
419
|
// An explicit --desktop is honoured whatever the platform: someone who asked
|
|
420
420
|
// for the app by name is owed the macOS-only notice and the download link
|
|
421
|
-
// rather than silence. Only the unasked
|
|
421
|
+
// rather than silence. Only the unasked install is withheld where there is no
|
|
422
422
|
// build, because a step that announces itself to say "not for you" reads as
|
|
423
423
|
// something being broken.
|
|
424
424
|
if (process.platform !== "darwin" && !args.has("--desktop")) return { state: "unsupported" };
|
|
@@ -431,7 +431,7 @@ export function desktopPrecheck(args) {
|
|
|
431
431
|
*
|
|
432
432
|
* `install` is injectable because onboarding does more than this one does — it
|
|
433
433
|
* hands the fresh session to the app so it opens signed in — and the part worth
|
|
434
|
-
* sharing is the guards
|
|
434
|
+
* sharing is the guards and the wording, not the install itself.
|
|
435
435
|
*/
|
|
436
436
|
export async function runDesktopInstall(args, { install = null } = {}) {
|
|
437
437
|
try {
|
|
@@ -442,14 +442,22 @@ export async function runDesktopInstall(args, { install = null } = {}) {
|
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Install the desktop app. Not a question.
|
|
447
|
+
*
|
|
448
|
+
* It used to be one, defaulted to no, on the grounds that the download is large
|
|
449
|
+
* and nobody should get it by pressing Enter. What that produced was a setup
|
|
450
|
+
* whose last act was to describe a thing and decline to do it — the customer
|
|
451
|
+
* still had to go install the app, only now by hand and later. The download is
|
|
452
|
+
* the whole reason a terminal is better placed to do this than a download page:
|
|
453
|
+
* the build is Developer ID signed and notarized, it lands in /Applications
|
|
454
|
+
* verified against the checksum published with the release, and because nothing
|
|
455
|
+
* here came through a browser there is no quarantine flag and so no Gatekeeper
|
|
456
|
+
* warning on first launch. Opting out is `--no-desktop`.
|
|
457
|
+
*/
|
|
458
|
+
async function offerDesktop(args) {
|
|
451
459
|
const already = desktopPrecheck(args);
|
|
452
|
-
// Silent on Linux and Windows: there is no build to
|
|
460
|
+
// Silent on Linux and Windows: there is no build to install, and a step that
|
|
453
461
|
// announces itself only to say "not for you" reads as something being wrong.
|
|
454
462
|
if (already && already.state !== "installed") return already;
|
|
455
463
|
step("Desktop app");
|
|
@@ -457,15 +465,6 @@ async function offerDesktop(args, { assumeYes } = {}) {
|
|
|
457
465
|
process.stdout.write(`${MARK.ok()} PreMan desktop app already installed.\n`);
|
|
458
466
|
return already;
|
|
459
467
|
}
|
|
460
|
-
// Default no, unlike every other step here: this one downloads a hundred-odd
|
|
461
|
-
// megabytes and writes to /Applications, which nobody should get by pressing
|
|
462
|
-
// Enter to move past a prompt. `--desktop` is how an unattended run opts in,
|
|
463
|
-
// since `--yes` deliberately no longer does.
|
|
464
|
-
const wanted = args.has("--desktop");
|
|
465
|
-
if (!wanted && !(await confirm(desktopQuestion(args), { assumeYes, defaultYes: false }))) {
|
|
466
|
-
desktopSkipped();
|
|
467
|
-
return { state: "skipped" };
|
|
468
|
-
}
|
|
469
468
|
return runDesktopInstall(args);
|
|
470
469
|
}
|
|
471
470
|
|
|
@@ -605,7 +604,7 @@ export async function guidedFirstRun(args, agent, apiKey, serverName, { blockedH
|
|
|
605
604
|
step("Runner");
|
|
606
605
|
await setUpRunner(args, agent, { assumeYes });
|
|
607
606
|
|
|
608
|
-
await offerDesktop(args
|
|
607
|
+
await offerDesktop(args);
|
|
609
608
|
|
|
610
609
|
step("Integrations");
|
|
611
610
|
await connectIntegrations(args, apiKey, { assumeYes });
|
package/bin/connect.js
CHANGED
|
@@ -144,11 +144,11 @@ Connect options:
|
|
|
144
144
|
and the integration prompts after connecting
|
|
145
145
|
--no-guide Connect only: no push hook, no closing summary
|
|
146
146
|
--no-runner With --guide, do not pair this machine as a runner
|
|
147
|
-
--no-desktop With --guide,
|
|
147
|
+
--no-desktop With --guide, skip the desktop app, which otherwise
|
|
148
|
+
installs. PREMAN_NO_DESKTOP=1 does the same
|
|
148
149
|
--no-integrations With --guide, do not offer GitHub / AWS / Slack
|
|
149
150
|
--no-hook Do not install the git pre-push hook
|
|
150
151
|
--yes Take every step's default without prompting
|
|
151
|
-
(the desktop app defaults to no; install-desktop)
|
|
152
152
|
--print Print the config instead of writing it
|
|
153
153
|
`;
|
|
154
154
|
|
package/bin/desktop.js
CHANGED
|
@@ -243,13 +243,62 @@ async function expectedDigest(arch) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
const BYTES_PER_MB = 1024 * 1024;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A single line that rewrites itself, or nothing at all when not on a terminal.
|
|
250
|
+
*
|
|
251
|
+
* Onboarding no longer asks before installing the app, and a step nobody agreed
|
|
252
|
+
* to cannot also go quiet for ninety seconds: a still cursor during a 150 MB
|
|
253
|
+
* fetch is indistinguishable from a hang, and the reflex is Ctrl-C halfway
|
|
254
|
+
* through. Redirected output gets the summary line that already existed rather
|
|
255
|
+
* than a few hundred carriage returns, which is why this is TTY-only.
|
|
256
|
+
*/
|
|
257
|
+
function progressLine() {
|
|
258
|
+
if (!process.stdout.isTTY) return { tick: () => {}, clear: () => {} };
|
|
259
|
+
let lastDrawn = 0;
|
|
260
|
+
let width = 0;
|
|
261
|
+
const tick = (received, total) => {
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
// One redraw per chunk is thousands of writes for an effect the eye cannot
|
|
264
|
+
// follow. The final chunk always draws, so the line never stops short.
|
|
265
|
+
if (now - lastDrawn < 150 && received !== total) return;
|
|
266
|
+
lastDrawn = now;
|
|
267
|
+
const got = (received / BYTES_PER_MB).toFixed(1);
|
|
268
|
+
const text = total
|
|
269
|
+
? ` ${got} of ${(total / BYTES_PER_MB).toFixed(1)} MB ${Math.floor((received / total) * 100)}%`
|
|
270
|
+
: ` ${got} MB`;
|
|
271
|
+
// Padded to the widest line drawn so far, or a shorter one leaves the tail
|
|
272
|
+
// of its predecessor on screen.
|
|
273
|
+
width = Math.max(width, text.length);
|
|
274
|
+
process.stdout.write(`\r${text.padEnd(width)}`);
|
|
275
|
+
};
|
|
276
|
+
const clear = () => {
|
|
277
|
+
if (width) process.stdout.write(`\r${" ".repeat(width)}\r`);
|
|
278
|
+
};
|
|
279
|
+
return { tick, clear };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function download(url, destination, onProgress) {
|
|
247
283
|
const controller = new AbortController();
|
|
248
284
|
const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
|
|
249
285
|
try {
|
|
250
286
|
const resp = await fetch(url, { signal: controller.signal, redirect: "follow" });
|
|
251
287
|
if (!resp.ok) throw new Error(`download failed: ${resp.status} ${url}`);
|
|
252
|
-
|
|
288
|
+
// Read the body in chunks rather than as one arrayBuffer: the buffering is
|
|
289
|
+
// the same either way, but only this version can say how far along it is.
|
|
290
|
+
const total = Number(resp.headers.get("content-length")) || 0;
|
|
291
|
+
const reader = resp.body.getReader();
|
|
292
|
+
const chunks = [];
|
|
293
|
+
let received = 0;
|
|
294
|
+
for (;;) {
|
|
295
|
+
const { done, value } = await reader.read();
|
|
296
|
+
if (done) break;
|
|
297
|
+
chunks.push(value);
|
|
298
|
+
received += value.length;
|
|
299
|
+
onProgress?.(received, total);
|
|
300
|
+
}
|
|
301
|
+
const buffer = Buffer.concat(chunks);
|
|
253
302
|
writeFileSync(destination, buffer);
|
|
254
303
|
return buffer.length;
|
|
255
304
|
} finally {
|
|
@@ -335,8 +384,16 @@ export async function installDesktopCommand(commandArgs = [], { onInstalled } =
|
|
|
335
384
|
let mounted = null;
|
|
336
385
|
|
|
337
386
|
try {
|
|
338
|
-
const
|
|
339
|
-
|
|
387
|
+
const progress = progressLine();
|
|
388
|
+
let bytes;
|
|
389
|
+
try {
|
|
390
|
+
bytes = await download(url, dmgPath, progress.tick);
|
|
391
|
+
} finally {
|
|
392
|
+
// Cleared even when the download throws, or the error prints onto the
|
|
393
|
+
// half-drawn progress line.
|
|
394
|
+
progress.clear();
|
|
395
|
+
}
|
|
396
|
+
process.stdout.write(` ${(bytes / BYTES_PER_MB).toFixed(1)} MB\n`);
|
|
340
397
|
|
|
341
398
|
const expected = await expectedDigest(arch);
|
|
342
399
|
if (expected) {
|
package/bin/integrations.js
CHANGED
|
@@ -370,11 +370,9 @@ export async function slackCommand(args) {
|
|
|
370
370
|
* "yes" | "no" | "back" -- back only offered once there is somewhere to go.
|
|
371
371
|
*
|
|
372
372
|
* `--yes` takes each step's own default rather than answering yes to all of
|
|
373
|
-
* them
|
|
374
|
-
* defaults to
|
|
375
|
-
*
|
|
376
|
-
* someone who only meant "stop asking me questions". They have
|
|
377
|
-
* `preman install-desktop` when they want it.
|
|
373
|
+
* them, so a step that should not ride in on a keypress can say so. Every step
|
|
374
|
+
* that asks defaults to yes today; the desktop app, which used to be the
|
|
375
|
+
* exception, no longer asks at all.
|
|
378
376
|
*/
|
|
379
377
|
async function askStep(question, { assumeYes, canGoBack, defaultYes = true }) {
|
|
380
378
|
const fallback = defaultYes ? "yes" : "no";
|
|
@@ -401,7 +399,8 @@ async function askStep(question, { assumeYes, canGoBack, defaultYes = true }) {
|
|
|
401
399
|
* halfway had a desktop app pointed at an empty account. Endpoints come first
|
|
402
400
|
* now because they are the only step that shows what PreMan is for, and every
|
|
403
401
|
* ask after it can name what it just found. The app goes last because it is the
|
|
404
|
-
*
|
|
402
|
+
* only step that asks nothing: by then every decision is made, and a download is
|
|
403
|
+
* something to leave running, not something to make someone watch first.
|
|
405
404
|
*
|
|
406
405
|
* The steps themselves live in connect/guide.js, which is also what
|
|
407
406
|
* `preman connect --guide` runs. Two walks that asked the same questions in
|
|
@@ -417,8 +416,9 @@ export async function onboardCommand(
|
|
|
417
416
|
// Imported at call time, not at the top: guide.js imports this module for MARK
|
|
418
417
|
// and the three integration commands, so a static edge back would close the
|
|
419
418
|
// cycle. By the time anyone runs onboard, this module is fully evaluated.
|
|
420
|
-
const { desktopPrecheck,
|
|
421
|
-
|
|
419
|
+
const { desktopPrecheck, discoverEndpoints, runDesktopInstall } = await import(
|
|
420
|
+
"./connect/guide.js"
|
|
421
|
+
);
|
|
422
422
|
const { resolveAgentToDrive } = await import("./connect/agents.js");
|
|
423
423
|
|
|
424
424
|
process.stdout.write("PreMan setup\n\n");
|
|
@@ -561,10 +561,10 @@ export async function onboardCommand(
|
|
|
561
561
|
},
|
|
562
562
|
});
|
|
563
563
|
|
|
564
|
-
// Last,
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
564
|
+
// Last, and only where there is something to install. The guards run out here
|
|
565
|
+
// rather than inside the step so a Linux user does not get a macOS-only step
|
|
566
|
+
// and someone who already has the app does not watch it download again — both
|
|
567
|
+
// of which happened when the check came after.
|
|
568
568
|
const desktop = desktopPrecheck(args);
|
|
569
569
|
if (desktop?.state === "installed") {
|
|
570
570
|
steps.push({
|
|
@@ -576,11 +576,8 @@ export async function onboardCommand(
|
|
|
576
576
|
} else if (!desktop) {
|
|
577
577
|
steps.push({
|
|
578
578
|
name: "Desktop app",
|
|
579
|
-
question
|
|
580
|
-
// `--desktop` is
|
|
581
|
-
// step its own default and this step's default is no.
|
|
582
|
-
defaultYes: args.has("--desktop"),
|
|
583
|
-
onSkip: desktopSkipped,
|
|
579
|
+
// No question. See offerDesktop in connect/guide.js for why the ask went
|
|
580
|
+
// away; `--no-desktop` is the way past it.
|
|
584
581
|
run: () =>
|
|
585
582
|
runDesktopInstall(args, {
|
|
586
583
|
// Onboarding installs *and* hands the fresh session over, so the app
|
|
@@ -618,7 +615,9 @@ export async function onboardCommand(
|
|
|
618
615
|
// why they are looking at a login screen.
|
|
619
616
|
process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
|
|
620
617
|
}
|
|
621
|
-
|
|
618
|
+
// Whether the app is on screen decides what the last line of the walk
|
|
619
|
+
// should say, and only this closure knows.
|
|
620
|
+
return { ...installed, appOpened: opened.state !== "not-installed" };
|
|
622
621
|
} finally {
|
|
623
622
|
stopOpening?.();
|
|
624
623
|
}
|
|
@@ -668,10 +667,18 @@ export async function onboardCommand(
|
|
|
668
667
|
}
|
|
669
668
|
|
|
670
669
|
try {
|
|
671
|
-
// A step that caught its own failure and already explained it says so
|
|
672
|
-
//
|
|
670
|
+
// A step that caught its own failure and already explained it says so; a step
|
|
671
|
+
// with nothing to do says that; anything else that returns is done.
|
|
672
|
+
//
|
|
673
|
+
// Translated rather than spread over, because the summary knows three words
|
|
674
|
+
// and a step may report any of its own. Spreading put `Desktop app: undefined`
|
|
675
|
+
// at the end of a successful install: the install reports state "installed",
|
|
676
|
+
// which is none of the three, so it printed as a failure with no detail.
|
|
673
677
|
const reported = await step.run();
|
|
674
|
-
|
|
678
|
+
const state =
|
|
679
|
+
{ failed: "failed", skipped: "skipped", unsupported: "skipped" }[reported?.state] ||
|
|
680
|
+
"done";
|
|
681
|
+
outcome.set(step.name, { ...(reported || {}), state });
|
|
675
682
|
} catch (err) {
|
|
676
683
|
// Report and carry on: a failed Slack install must not cost the customer
|
|
677
684
|
// the AWS connection they just finished.
|
|
@@ -690,23 +697,33 @@ export async function onboardCommand(
|
|
|
690
697
|
else if (result.state === "skipped") process.stdout.write(`${MARK.skip()} ${step.name} (skipped)\n`);
|
|
691
698
|
else process.stdout.write(`${MARK.fail()} ${step.name}: ${result.detail}\n`);
|
|
692
699
|
}
|
|
693
|
-
|
|
700
|
+
// The app is on screen by the time this prints, so telling them to go and open
|
|
701
|
+
// a website is a step backwards. The website still earns its mention -- it is
|
|
702
|
+
// the way in from a machine that has no app, and the account is the same one --
|
|
703
|
+
// but as an alternative, not as the next thing to do.
|
|
704
|
+
const appOpened = outcome.get("Desktop app")?.appOpened === true;
|
|
705
|
+
process.stdout.write(
|
|
706
|
+
appOpened
|
|
707
|
+
? `\nPreMan is open. You can also sign in at ${frontendUrl(args)}.\n`
|
|
708
|
+
: `\nOpen ${frontendUrl(args)} to see your logs and endpoints.\n`
|
|
709
|
+
);
|
|
694
710
|
}
|
|
695
711
|
|
|
696
712
|
export const INTEGRATIONS_HELP = `
|
|
697
713
|
Setup options:
|
|
698
714
|
preman onboard Sign in, map this repo's endpoints, connect it for
|
|
699
715
|
pull requests, and check the endpoints you touch on
|
|
700
|
-
every git push.
|
|
716
|
+
every git push. Installs the desktop app at the end
|
|
701
717
|
preman aws Connect an AWS account and stream a log group
|
|
702
718
|
preman github Install the PreMan GitHub App
|
|
703
719
|
preman slack Add PreMan to a Slack workspace
|
|
704
720
|
|
|
705
|
-
--yes Take each step's default without prompting
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
--desktop
|
|
709
|
-
|
|
721
|
+
--yes Take each step's default without prompting (onboard)
|
|
722
|
+
--desktop Install the desktop app even where there is no macOS
|
|
723
|
+
build, to print the download link (onboard)
|
|
724
|
+
--no-desktop Skip the desktop app, which otherwise installs
|
|
725
|
+
(onboard)
|
|
726
|
+
PREMAN_NO_DESKTOP=1 The same, for runs that cannot pass a flag
|
|
710
727
|
b at any onboard prompt Go back to the previous step
|
|
711
728
|
--account <id> AWS account id, skips the prompt
|
|
712
729
|
--region <region> AWS region for log groups. Defaults to us-east-1
|