premanmcp 0.15.1 → 0.15.2

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 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
- `--yes` deliberately does *not* install the desktop app: that step's default is no, because
118
- it downloads a hundred-odd megabytes and writes to `/Applications`. Run `install-desktop`
119
- when you want it.
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
@@ -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 the default rather than a blanket yes, which matters for
78
- * exactly one step: the desktop app defaults to no because it downloads a
79
- * hundred-odd megabytes and writes to /Applications. A blanket yes made
80
- * `preman connect --yes` do that unattended, which is not what anyone means by
81
- * "do not ask me questions" -- they get `preman install-desktop` for that.
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
- * Why the desktop app should not be asked about here, or null to go ahead.
396
+ * An opt-out for runs that cannot pass a flag.
411
397
  *
412
- * Split out from the offer so a caller that runs its own prompt loop can decide
413
- * whether the question is worth asking *before* asking it. Both of these were
414
- * bugs when the check lived after the prompt: a Linux user got a macOS-only
415
- * question, and someone who already had the app got asked to download it again.
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 offer is withheld where there is no
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, the default and the wording, not the install itself.
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
- export function desktopSkipped() {
446
- process.stdout.write(`${MARK.skip()} Skipped. Install later: ${cliInvocation()} install-desktop\n`);
447
- }
448
-
449
- /** Offer the desktop app. Optional by design the terminal flow is complete without it. */
450
- async function offerDesktop(args, { assumeYes } = {}) {
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 offer, and a step that
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, { assumeYes });
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, do not offer the desktop app
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
- async function download(url, destination) {
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
- const buffer = Buffer.from(await resp.arrayBuffer());
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 bytes = await download(url, dmgPath);
339
- process.stdout.write(` ${(bytes / 1024 / 1024).toFixed(1)} MB\n`);
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) {
@@ -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. One step needs that today and it is the expensive one: the desktop app
374
- * defaults to no because it downloads a hundred-odd megabytes and writes to
375
- * /Applications, and `onboard --yes` used to do exactly that, unattended, to
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
- * one thing here nobody needs.
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, desktopQuestion, desktopSkipped, discoverEndpoints, runDesktopInstall } =
421
- await import("./connect/guide.js");
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, defaulted to no, and only where there is something to install. The
565
- // guards run out here rather than inside the step so a Linux user is not
566
- // asked a macOS question and someone who already has the app is not asked to
567
- // download it again — both of which happened when the check came after.
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: desktopQuestion(args),
580
- // `--desktop` is how an unattended run opts in, since `--yes` gives every
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
@@ -697,16 +694,17 @@ export const INTEGRATIONS_HELP = `
697
694
  Setup options:
698
695
  preman onboard Sign in, map this repo's endpoints, connect it for
699
696
  pull requests, and check the endpoints you touch on
700
- every git push. Offers the desktop app at the end
697
+ every git push. Installs the desktop app at the end
701
698
  preman aws Connect an AWS account and stream a log group
702
699
  preman github Install the PreMan GitHub App
703
700
  preman slack Add PreMan to a Slack workspace
704
701
 
705
- --yes Take each step's default without prompting. The
706
- desktop app defaults to no, so --yes never
707
- downloads it (onboard)
708
- --desktop Install the desktop app without asking (onboard)
709
- --no-desktop Do not offer the desktop app at all (onboard)
702
+ --yes Take each step's default without prompting (onboard)
703
+ --desktop Install the desktop app even where there is no macOS
704
+ build, to print the download link (onboard)
705
+ --no-desktop Skip the desktop app, which otherwise installs
706
+ (onboard)
707
+ PREMAN_NO_DESKTOP=1 The same, for runs that cannot pass a flag
710
708
  b at any onboard prompt Go back to the previous step
711
709
  --account <id> AWS account id, skips the prompt
712
710
  --region <region> AWS region for log groups. Defaults to us-east-1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {