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.
- package/bin/integrations.js +199 -22
- package/package.json +1 -1
package/bin/integrations.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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";
|