premanmcp 0.15.2 → 0.16.0

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 +133 -5
  2. package/package.json +1 -1
@@ -374,6 +374,106 @@ export async function slackCommand(args) {
374
374
  * that asks defaults to yes today; the desktop app, which used to be the
375
375
  * exception, no longer asks at all.
376
376
  */
377
+ /**
378
+ * Ask, once, before PreMan writes into an SDK repository or a docs site.
379
+ *
380
+ * Two questions rather than one. Agreeing to a generated client library is not
381
+ * agreeing to have your documentation site rewritten (a docs release replaces
382
+ * mkdocs.yml), and a single answer used to buy both.
383
+ *
384
+ * Says nothing when there is nothing to ask: no connected repository, no
385
+ * detected target, or an answer already on file. A walk that re-asks a question
386
+ * it has already had answered teaches people that answering buys them nothing,
387
+ * which is the whole reason this moved out of the delivery run.
388
+ */
389
+ async function offerReleaseTargets(args, checkout, { assumeYes }) {
390
+ const integrationId = checkout?.match?.integration_id;
391
+ if (!integrationId) return { state: "skipped" };
392
+
393
+ const token = resolveApiKey(args);
394
+ const path = `/sdk/integrations/${integrationId}/distribution`;
395
+ let detected = null;
396
+ try {
397
+ detected = await callBackendJson(args, "GET", path, { token });
398
+ } catch {
399
+ // Detection costs one root listing per repository in the installation. It
400
+ // is the least important thing in this walk and never worth failing setup
401
+ // over.
402
+ return { state: "skipped" };
403
+ }
404
+
405
+ if (detected?.confirmed) {
406
+ process.stdout.write(`${MARK.ok()} Release targets already answered.\n`);
407
+ return;
408
+ }
409
+
410
+ const sdkRepo = detected?.sdk_repo || "";
411
+ const docsRepo = detected?.docs_repo || "";
412
+ if (!sdkRepo && !docsRepo) return { state: "skipped" };
413
+
414
+ const body = {};
415
+ const decided = [];
416
+ for (const [repo, field, flag, question] of [
417
+ [
418
+ sdkRepo,
419
+ "sdk_repo",
420
+ "publish_sdk",
421
+ `${sdkRepo} looks like your client library. Open pull requests there with generated SDKs?`,
422
+ ],
423
+ [
424
+ docsRepo,
425
+ "docs_repo",
426
+ "publish_docs",
427
+ // The wider blast radius is named in the question, because it is the part
428
+ // somebody would resent discovering afterwards.
429
+ `${docsRepo} looks like your docs site. Open pull requests there? Releases rewrite mkdocs.yml.`,
430
+ ],
431
+ ]) {
432
+ if (!repo) continue;
433
+ const answer = await askRelease(question, assumeYes);
434
+ if (answer === null) continue;
435
+ body[field] = repo;
436
+ body[flag] = answer;
437
+ decided.push([repo, answer]);
438
+ }
439
+ if (!decided.length) return { state: "skipped" };
440
+
441
+ await callBackendJson(args, "PUT", path, { token, json: body });
442
+
443
+ for (const [repo, allowed] of decided) {
444
+ // A no is reported as recorded, not as a step that failed. It is an answer,
445
+ // and PreMan will not come back to it.
446
+ if (allowed) process.stdout.write(`${MARK.ok()} Publishing to ${repo}.\n`);
447
+ else
448
+ process.stdout.write(
449
+ `${MARK.skip()} Not publishing to ${repo}. PreMan will not ask again.\n`
450
+ );
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Yes, no, or nobody answered.
456
+ *
457
+ * Three outcomes rather than two, and the third is the whole reason this does
458
+ * not use `confirm`. That helper reads a closed stdin as a no, which is right
459
+ * for a step that then simply does less work. A no here is *recorded*, and a
460
+ * recorded no is never asked again, so reading silence as refusal would retire
461
+ * a question the customer never saw.
462
+ *
463
+ * The same argument applies to --yes: accepting defaults on someone's behalf is
464
+ * fine for a hook or a download, and is not a way to acquire write access to a
465
+ * repository nobody mentioned. So an unattended run leaves both questions open.
466
+ *
467
+ * The default is no, for the reason silence means no everywhere else PreMan
468
+ * writes to somebody's repository.
469
+ */
470
+ async function askRelease(question, assumeYes) {
471
+ if (assumeYes) return null;
472
+ const answer = (await promptText(`${question} [y/N]: `)).trim().toLowerCase();
473
+ if (answer === "") return null;
474
+ return answer === "y" || answer === "yes";
475
+ }
476
+
377
477
  async function askStep(question, { assumeYes, canGoBack, defaultYes = true }) {
378
478
  const fallback = defaultYes ? "yes" : "no";
379
479
  if (assumeYes) return fallback;
@@ -525,6 +625,15 @@ export async function onboardCommand(
525
625
  });
526
626
  }
527
627
 
628
+ // The only questions in this walk about repositories *other* than the one
629
+ // being connected. They are asked here, while PreMan has just read the
630
+ // installation and can name what it found, rather than weeks later from
631
+ // inside a delivery run, on a screen most people never opened.
632
+ steps.push({
633
+ name: "Releases",
634
+ run: () => offerReleaseTargets(args, checkout, { assumeYes }),
635
+ });
636
+
528
637
  steps.push({
529
638
  name: "Push checks",
530
639
  question: "Check your endpoints on every git push in this repo?",
@@ -615,7 +724,9 @@ export async function onboardCommand(
615
724
  // why they are looking at a login screen.
616
725
  process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
617
726
  }
618
- return installed;
727
+ // Whether the app is on screen decides what the last line of the walk
728
+ // should say, and only this closure knows.
729
+ return { ...installed, appOpened: opened.state !== "not-installed" };
619
730
  } finally {
620
731
  stopOpening?.();
621
732
  }
@@ -665,10 +776,18 @@ export async function onboardCommand(
665
776
  }
666
777
 
667
778
  try {
668
- // A step that caught its own failure and already explained it says so, and
669
- // is not summarised as done; anything else that returns is taken as done.
779
+ // A step that caught its own failure and already explained it says so; a step
780
+ // with nothing to do says that; anything else that returns is done.
781
+ //
782
+ // Translated rather than spread over, because the summary knows three words
783
+ // and a step may report any of its own. Spreading put `Desktop app: undefined`
784
+ // at the end of a successful install: the install reports state "installed",
785
+ // which is none of the three, so it printed as a failure with no detail.
670
786
  const reported = await step.run();
671
- outcome.set(step.name, { state: "done", ...(reported || {}) });
787
+ const state =
788
+ { failed: "failed", skipped: "skipped", unsupported: "skipped" }[reported?.state] ||
789
+ "done";
790
+ outcome.set(step.name, { ...(reported || {}), state });
672
791
  } catch (err) {
673
792
  // Report and carry on: a failed Slack install must not cost the customer
674
793
  // the AWS connection they just finished.
@@ -687,7 +806,16 @@ export async function onboardCommand(
687
806
  else if (result.state === "skipped") process.stdout.write(`${MARK.skip()} ${step.name} (skipped)\n`);
688
807
  else process.stdout.write(`${MARK.fail()} ${step.name}: ${result.detail}\n`);
689
808
  }
690
- process.stdout.write(`\nOpen ${frontendUrl(args)} to see your logs and endpoints.\n`);
809
+ // The app is on screen by the time this prints, so telling them to go and open
810
+ // a website is a step backwards. The website still earns its mention -- it is
811
+ // the way in from a machine that has no app, and the account is the same one --
812
+ // but as an alternative, not as the next thing to do.
813
+ const appOpened = outcome.get("Desktop app")?.appOpened === true;
814
+ process.stdout.write(
815
+ appOpened
816
+ ? `\nPreMan is open. You can also sign in at ${frontendUrl(args)}.\n`
817
+ : `\nOpen ${frontendUrl(args)} to see your logs and endpoints.\n`
818
+ );
691
819
  }
692
820
 
693
821
  export const INTEGRATIONS_HELP = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {