premanmcp 0.15.3 → 0.16.1

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/bin/integrations.js +199 -22
  2. package/package.json +1 -1
@@ -366,6 +366,145 @@ export async function slackCommand(args) {
366
366
  // The guided run
367
367
  // ---------------------------------------------------------------------------
368
368
 
369
+ /**
370
+ * Ask, once, before PreMan writes into an SDK repository or a docs site.
371
+ *
372
+ * Two questions rather than one. Agreeing to a generated client library is not
373
+ * agreeing to have your documentation site rewritten (a docs release replaces
374
+ * mkdocs.yml), and a single answer used to buy both.
375
+ *
376
+ * Says nothing when there is nothing to ask: no connected repository, no
377
+ * detected target, or an answer already on file. A walk that re-asks a question
378
+ * it has already had answered teaches people that answering buys them nothing,
379
+ * which is the whole reason this moved out of the delivery run.
380
+ */
381
+ async function offerReleaseTargets(args, checkout, { assumeYes }) {
382
+ // SILENT is a step that did not happen rather than one that was skipped: no
383
+ // heading, and no line in the summary either. A repository with no SDK and no
384
+ // docs site has nothing to decide here, and saying so is worse than saying
385
+ // nothing.
386
+ const SILENT = { state: "skipped", silent: true };
387
+
388
+ const integrationId = checkout?.match?.integration_id;
389
+ if (!integrationId) return SILENT;
390
+
391
+ const token = resolveApiKey(args);
392
+ const path = `/sdk/integrations/${integrationId}/distribution`;
393
+ let detected = null;
394
+ try {
395
+ detected = await callBackendJson(args, "GET", path, { token });
396
+ } catch {
397
+ // Detection costs one root listing per repository in the installation. It
398
+ // is the least important thing in this walk and never worth failing setup
399
+ // over.
400
+ return SILENT;
401
+ }
402
+
403
+ const sdkRepo = detected?.sdk_repo || "";
404
+ const docsRepo = detected?.docs_repo || "";
405
+ if (!sdkRepo && !docsRepo) return SILENT;
406
+
407
+ banner("Releases");
408
+ if (detected?.confirmed) {
409
+ process.stdout.write(`${MARK.ok()} Release targets already answered.\n`);
410
+ return;
411
+ }
412
+
413
+ const body = {};
414
+ const decided = [];
415
+ for (const [repo, field, flag, question] of [
416
+ [
417
+ sdkRepo,
418
+ "sdk_repo",
419
+ "publish_sdk",
420
+ `${sdkRepo} looks like your client library. Open pull requests there with generated SDKs?`,
421
+ ],
422
+ [
423
+ docsRepo,
424
+ "docs_repo",
425
+ "publish_docs",
426
+ // The wider blast radius is named in the question, because it is the part
427
+ // somebody would resent discovering afterwards.
428
+ `${docsRepo} looks like your docs site. Open pull requests there? Releases rewrite mkdocs.yml.`,
429
+ ],
430
+ ]) {
431
+ if (!repo) continue;
432
+ const answer = await askRelease(question, assumeYes);
433
+ if (answer === null) continue;
434
+ body[field] = repo;
435
+ body[flag] = answer;
436
+ decided.push([repo, answer]);
437
+ }
438
+ if (!decided.length) return { state: "skipped" };
439
+
440
+ await callBackendJson(args, "PUT", path, { token, json: body });
441
+
442
+ for (const [repo, allowed] of decided) {
443
+ // A no is reported as recorded, not as a step that failed. It is an answer,
444
+ // and PreMan will not come back to it.
445
+ if (allowed) process.stdout.write(`${MARK.ok()} Publishing to ${repo}.\n`);
446
+ else
447
+ process.stdout.write(
448
+ `${MARK.skip()} Not publishing to ${repo}. PreMan will not ask again.\n`
449
+ );
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Yes, no, or nobody answered.
455
+ *
456
+ * Three outcomes rather than two, and the third is the whole reason this does
457
+ * not use `confirm`. That helper reads a closed stdin as a no, which is right
458
+ * for a step that then simply does less work. A no here is *recorded*, and a
459
+ * recorded no is never asked again, so reading silence as refusal would retire
460
+ * a question the customer never saw.
461
+ *
462
+ * The same argument applies to --yes: accepting defaults on someone's behalf is
463
+ * fine for a hook or a download, and is not a way to acquire write access to a
464
+ * repository nobody mentioned. So an unattended run leaves both questions open.
465
+ *
466
+ * The default is no, for the reason silence means no everywhere else PreMan
467
+ * writes to somebody's repository.
468
+ */
469
+ async function askRelease(question, assumeYes) {
470
+ if (assumeYes) return null;
471
+ const answer = (await promptText(`${question} [y/N]: `)).trim().toLowerCase();
472
+ if (answer === "") return null;
473
+ return answer === "y" || answer === "yes";
474
+ }
475
+
476
+ /**
477
+ * Put PreMan on screen, signed in as the account this walk just used.
478
+ *
479
+ * Returns whether the app is actually up, because that is what decides the last
480
+ * line of the walk and nothing downstream can work it out.
481
+ */
482
+ async function showDesktopSignedIn(openDesktopSignedIn, args, creds, stopOpening) {
483
+ // The same --dest the install honoured, or the launch would look for the app
484
+ // somewhere it was never copied to.
485
+ const destination = args.value("--dest", "/Applications");
486
+ const opened = await openDesktopSignedIn(creds, { destination });
487
+ stopOpening?.();
488
+ if (opened.state === "opened-signed-in") {
489
+ process.stdout.write("Opened PreMan, signed in as this account.\n");
490
+ } else if (opened.state === "not-installed") {
491
+ process.stdout.write(
492
+ `PreMan is not in ${destination} yet \u2014 open it once installed and sign in.\n`
493
+ );
494
+ } else {
495
+ // Either the session could not be handed over or this app is too old to take
496
+ // it up. Say so rather than let the customer wonder why they are looking at
497
+ // a login screen.
498
+ process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
499
+ }
500
+ return opened.state !== "not-installed";
501
+ }
502
+
503
+ /** One step heading, in the one format the whole walk uses. */
504
+ function banner(name) {
505
+ process.stdout.write(`\n── ${name} ──\n`);
506
+ }
507
+
369
508
  /**
370
509
  * "yes" | "no" | "back" -- back only offered once there is somewhere to go.
371
510
  *
@@ -499,6 +638,22 @@ export async function onboardCommand(
499
638
  process.stdout.write(
500
639
  `${checkout.slug} is still not shared. Add it at ${INSTALLATIONS_URL}.\n`
501
640
  );
641
+ // Nothing was connected, so this step did not finish. Returning nothing
642
+ // marks it done, which is how the walk came to print a tick directly
643
+ // under a line saying the repository is not shared — and why somebody
644
+ // would then open the app expecting to find GitHub set up and find
645
+ // "Continue with GitHub" instead. The sibling branch below already
646
+ // reports this correctly; this one simply never did.
647
+ return { state: "skipped" };
648
+ },
649
+ onSkip: () => {
650
+ // The one question in this walk that decides whether a fix from this
651
+ // code can ever land anywhere. Declining it in silence is how the gap
652
+ // stays invisible until a fix task finishes and produces nothing.
653
+ process.stdout.write(
654
+ `${MARK.skip()} ${checkout.slug} stays disconnected — fixes here cannot open pull requests.\n`
655
+ );
656
+ process.stdout.write(` Run '${cliInvocation()} github' here to change that.\n`);
502
657
  },
503
658
  });
504
659
  } else {
@@ -525,6 +680,19 @@ export async function onboardCommand(
525
680
  });
526
681
  }
527
682
 
683
+ // The only questions in this walk about repositories *other* than the one
684
+ // being connected. They are asked here, while PreMan has just read the
685
+ // installation and can name what it found, rather than weeks later from
686
+ // inside a delivery run, on a screen most people never opened.
687
+ steps.push({
688
+ name: "Releases",
689
+ // Prints its own heading, and only once it knows there is a question. A
690
+ // walk that draws an empty section has told the customer something is
691
+ // missing when nothing is.
692
+ ownBanner: true,
693
+ run: () => offerReleaseTargets(args, checkout, { assumeYes }),
694
+ });
695
+
528
696
  steps.push({
529
697
  name: "Push checks",
530
698
  question: "Check your endpoints on every git push in this repo?",
@@ -569,8 +737,21 @@ export async function onboardCommand(
569
737
  if (desktop?.state === "installed") {
570
738
  steps.push({
571
739
  name: "Desktop app",
572
- run: () => {
740
+ // Nothing to download, which is the point of this branch. It is not a
741
+ // reason to stop at the sentence: the step exists to leave the customer
742
+ // looking at PreMan, so the one run with nothing to install was also the
743
+ // only run that opened nothing, and it closed by sending someone to a
744
+ // website instead of the app already sitting on their machine.
745
+ run: async () => {
573
746
  process.stdout.write(`${MARK.ok()} PreMan desktop app already installed.\n`);
747
+ const stopOpening = showOpeningDesktop();
748
+ try {
749
+ return {
750
+ appOpened: await showDesktopSignedIn(openDesktopSignedIn, args, creds, stopOpening),
751
+ };
752
+ } finally {
753
+ stopOpening();
754
+ }
574
755
  },
575
756
  });
576
757
  } else if (!desktop) {
@@ -598,26 +779,15 @@ export async function onboardCommand(
598
779
  return installed;
599
780
  }
600
781
  stopOpening ??= showOpeningDesktop();
601
- // The same --dest the install honoured, or the launch would look
602
- // for the app somewhere it was never copied to.
603
- const destination = args.value("--dest", "/Applications");
604
- const opened = await openDesktopSignedIn(creds, { destination });
605
- stopOpening();
606
- if (opened.state === "opened-signed-in") {
607
- process.stdout.write("Opened PreMan, signed in as this account.\n");
608
- } else if (opened.state === "not-installed") {
609
- process.stdout.write(
610
- `PreMan is not in ${destination} yet \u2014 open it once installed and sign in.\n`
611
- );
612
- } else {
613
- // Either the session could not be handed over or this app is too
614
- // old to take it up. Say so rather than let the customer wonder
615
- // why they are looking at a login screen.
616
- process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
617
- }
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" };
782
+ return {
783
+ ...installed,
784
+ appOpened: await showDesktopSignedIn(
785
+ openDesktopSignedIn,
786
+ args,
787
+ creds,
788
+ stopOpening
789
+ ),
790
+ };
621
791
  } finally {
622
792
  stopOpening?.();
623
793
  }
@@ -636,7 +806,7 @@ export async function onboardCommand(
636
806
  let i = 0;
637
807
  while (i < steps.length) {
638
808
  const step = steps[i];
639
- process.stdout.write(`\n── ${step.name} ──\n`);
809
+ if (!step.ownBanner) banner(step.name);
640
810
 
641
811
  if (step.question) {
642
812
  // "back" goes to the last step that asked something, not simply to the
@@ -675,6 +845,13 @@ export async function onboardCommand(
675
845
  // at the end of a successful install: the install reports state "installed",
676
846
  // which is none of the three, so it printed as a failure with no detail.
677
847
  const reported = await step.run();
848
+ // A step that never announced itself does not belong in the summary
849
+ // either: a line reading "(skipped)" for work nobody was offered reads as
850
+ // something having gone wrong.
851
+ if (reported?.silent) {
852
+ i += 1;
853
+ continue;
854
+ }
678
855
  const state =
679
856
  { failed: "failed", skipped: "skipped", unsupported: "skipped" }[reported?.state] ||
680
857
  "done";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.15.3",
3
+ "version": "0.16.1",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {