premanmcp 1.0.1 → 1.0.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/bin/integrations.js +100 -58
- package/bin/verify.js +6 -2
- package/package.json +1 -1
package/bin/integrations.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describeCheckout, explainNoCheckout, resolveCheckout } from "./repo.js";
|
|
18
|
+
import { desktopAppInstalled, writeDesktopSession } from "./desktop.js";
|
|
18
19
|
import {
|
|
19
20
|
DEFAULT_BACKEND,
|
|
20
21
|
assertOk,
|
|
@@ -254,7 +255,7 @@ export async function awsCommand(args) {
|
|
|
254
255
|
// GitHub
|
|
255
256
|
// ---------------------------------------------------------------------------
|
|
256
257
|
|
|
257
|
-
export async function githubCommand(args) {
|
|
258
|
+
export async function githubCommand(args, { ensureDesktopApp = null } = {}) {
|
|
258
259
|
const token = requireKey(args);
|
|
259
260
|
|
|
260
261
|
// This route answers with a bare array; callBackendJson exposes it as `list`.
|
|
@@ -265,15 +266,24 @@ export async function githubCommand(args) {
|
|
|
265
266
|
|
|
266
267
|
const seen = new Set((await listRepos()).map((r) => r.id));
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// tab into the hosted dashboard
|
|
271
|
-
// environment owns the App's setup URL.
|
|
272
|
-
//
|
|
273
|
-
//
|
|
269
|
+
// The desktop return is the right ending for a run started in a terminal: the
|
|
270
|
+
// CLI is already polling for the installation, and the web return 302s the
|
|
271
|
+
// tab into the hosted dashboard — an app they did not ask for, on whichever
|
|
272
|
+
// environment owns the App's setup URL.
|
|
273
|
+
//
|
|
274
|
+
// What it must never be is unconditional. That page's only action is a
|
|
275
|
+
// preman:// link, and a machine with no app answers one with a system dialog
|
|
276
|
+
// reading "There is no application set to open the URL preman://open". That is
|
|
277
|
+
// where a first run actually ended, every time: the app installs at the end of
|
|
278
|
+
// this walk and GitHub is asked about in the middle, so by the time this link
|
|
279
|
+
// was minted there had never been an app. A caller may put one there first;
|
|
280
|
+
// without one, the web return is the only ending that leads anywhere.
|
|
281
|
+
const hasDesktopApp = ensureDesktopApp
|
|
282
|
+
? await ensureDesktopApp()
|
|
283
|
+
: desktopAppInstalled(args.value("--dest", "/Applications"));
|
|
274
284
|
const started = await callBackendJson(args, "POST", "/integrations/github/app/install", {
|
|
275
285
|
token,
|
|
276
|
-
json: { return_to: "desktop" },
|
|
286
|
+
json: { return_to: hasDesktopApp ? "desktop" : "web" },
|
|
277
287
|
});
|
|
278
288
|
assertOk(started, "start GitHub install");
|
|
279
289
|
|
|
@@ -590,6 +600,12 @@ async function askStep(question, { assumeYes, canGoBack, defaultYes = true }) {
|
|
|
590
600
|
* only step that asks nothing: by then every decision is made, and a download is
|
|
591
601
|
* something to leave running, not something to make someone watch first.
|
|
592
602
|
*
|
|
603
|
+
* With one exception, and it is not a preference. Connecting a repository ends
|
|
604
|
+
* with GitHub handing the browser back through a preman:// link, and a machine
|
|
605
|
+
* with no app answers that with a system dialog rather than a window. So saying
|
|
606
|
+
* yes to the repository pulls the download forward to just before it; saying no
|
|
607
|
+
* leaves the order exactly as described above.
|
|
608
|
+
*
|
|
593
609
|
* The steps themselves live in connect/guide.js, which is also what
|
|
594
610
|
* `preman connect --guide` runs. Two walks that asked the same questions in
|
|
595
611
|
* different words with different defaults was the thing worth deleting.
|
|
@@ -631,6 +647,43 @@ export async function onboardCommand(
|
|
|
631
647
|
// is said in one place rather than before PreMan has looked at the code.
|
|
632
648
|
const checkout = await resolveCheckout(args, resolveApiKey(args));
|
|
633
649
|
|
|
650
|
+
// What the app install did, once, shared by the step that needs an app to
|
|
651
|
+
// exist and the step that owns installing one.
|
|
652
|
+
//
|
|
653
|
+
// GitHub's callback ends on a page whose only action is a preman:// link, and
|
|
654
|
+
// the app is the last step of this walk — so a first run always handed GitHub
|
|
655
|
+
// back to a machine with nothing to open it with, and macOS said so in a
|
|
656
|
+
// dialog. The download moves in front of that step when it is needed and stays
|
|
657
|
+
// where it was when it is not: someone who declines the repository never
|
|
658
|
+
// reaches this, and still gets the app at the end.
|
|
659
|
+
//
|
|
660
|
+
// Memoised because it is a hundred megabytes and both callers want the same
|
|
661
|
+
// copy of it. Answers whether there is now an app on this machine, which is
|
|
662
|
+
// the only part either caller can act on.
|
|
663
|
+
let desktopInstall = null;
|
|
664
|
+
const installedNow = (result) => result?.state === "installed" || result?.state === "current";
|
|
665
|
+
const ensureDesktopApp = async ({ onInstalled } = {}) => {
|
|
666
|
+
const already = desktopPrecheck(args);
|
|
667
|
+
if (already?.state === "installed") return true;
|
|
668
|
+
// --no-desktop, or a platform with no build. Nothing to install here, and so
|
|
669
|
+
// nothing that could ever answer a preman:// link either.
|
|
670
|
+
if (already) return false;
|
|
671
|
+
if (!desktopInstall) {
|
|
672
|
+
process.stdout.write(
|
|
673
|
+
"Getting the PreMan app first, so GitHub has somewhere to hand you back to.\n"
|
|
674
|
+
);
|
|
675
|
+
desktopInstall = await runDesktopInstall(args, {
|
|
676
|
+
install: () => installDesktop([...commandArgs], onInstalled ? { onInstalled } : undefined),
|
|
677
|
+
});
|
|
678
|
+
// Handed the session the moment it lands, before anything can launch it.
|
|
679
|
+
// The app reads one only when it starts, and the callback's deep link is a
|
|
680
|
+
// start — without this it opens on a login screen for an account signed in
|
|
681
|
+
// ninety seconds ago.
|
|
682
|
+
if (installedNow(desktopInstall)) writeDesktopSession(creds);
|
|
683
|
+
}
|
|
684
|
+
return installedNow(desktopInstall);
|
|
685
|
+
};
|
|
686
|
+
|
|
634
687
|
const steps = [
|
|
635
688
|
{
|
|
636
689
|
// No question. Everything below is a decision about code PreMan has not
|
|
@@ -672,7 +725,7 @@ export async function onboardCommand(
|
|
|
672
725
|
return `Connect ${checkout.slug} so PreMan can open pull requests for the ${found} endpoint${found === 1 ? "" : "s"} it just mapped?`;
|
|
673
726
|
},
|
|
674
727
|
run: async () => {
|
|
675
|
-
await githubCommand(args);
|
|
728
|
+
await githubCommand(args, { ensureDesktopApp });
|
|
676
729
|
// Finishing the App install is not the same as this repository being
|
|
677
730
|
// connected: the install shares whichever repositories the customer
|
|
678
731
|
// picked, which may not include this one. Re-asking is the difference
|
|
@@ -783,66 +836,55 @@ export async function onboardCommand(
|
|
|
783
836
|
// and someone who already has the app does not watch it download again — both
|
|
784
837
|
// of which happened when the check came after.
|
|
785
838
|
const desktop = desktopPrecheck(args);
|
|
786
|
-
if (desktop?.state === "installed") {
|
|
839
|
+
if (desktop?.state === "installed" || !desktop) {
|
|
787
840
|
steps.push({
|
|
788
841
|
name: "Desktop app",
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
842
|
+
// No question. See offerDesktop in connect/guide.js for why the ask went
|
|
843
|
+
// away; `--no-desktop` is the way past it.
|
|
844
|
+
//
|
|
845
|
+
// Installing and launching used to be one thing here, which is why the one
|
|
846
|
+
// run with nothing to install was also the only run that opened nothing: it
|
|
847
|
+
// stopped at the sentence and sent someone to a website instead of the app
|
|
848
|
+
// already sitting on their machine. They are two things now, and the
|
|
849
|
+
// install may have happened further up the walk, so this step is the launch
|
|
850
|
+
// and ensureDesktopApp decides whether anything is left to download.
|
|
794
851
|
run: async () => {
|
|
795
|
-
|
|
796
|
-
|
|
852
|
+
// Said only when this walk did not just download it. After an install of
|
|
853
|
+
// our own, announcing that it is already installed is the walk telling
|
|
854
|
+
// somebody about their own last thirty seconds.
|
|
855
|
+
if (!desktopInstall && desktopPrecheck(args)?.state === "installed") {
|
|
856
|
+
process.stdout.write(`${MARK.ok()} PreMan desktop app already installed.\n`);
|
|
857
|
+
}
|
|
858
|
+
let stopOpening = null;
|
|
797
859
|
try {
|
|
860
|
+
const ready = await ensureDesktopApp({
|
|
861
|
+
onInstalled: () => {
|
|
862
|
+
stopOpening ??= showOpeningDesktop();
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
if (!ready) {
|
|
866
|
+
if (desktopInstall?.state === "unsupported") {
|
|
867
|
+
// Not a failure: the download link has already been printed, and
|
|
868
|
+
// the account this step exists to create is finished either way.
|
|
869
|
+
process.stdout.write("Sign in there with the account you just used.\n");
|
|
870
|
+
}
|
|
871
|
+
// A failed install already said so through runDesktopInstall, and its
|
|
872
|
+
// state is what keeps this out of the summary as a skip.
|
|
873
|
+
return desktopInstall || { state: "skipped" };
|
|
874
|
+
}
|
|
875
|
+
stopOpening ??= showOpeningDesktop();
|
|
798
876
|
return {
|
|
877
|
+
...(desktopInstall || {}),
|
|
878
|
+
// Onboarding installs *and* hands the fresh session over, so the app
|
|
879
|
+
// opens already signed in rather than on a login screen for the
|
|
880
|
+
// account created ninety seconds ago.
|
|
799
881
|
appOpened: await showDesktopSignedIn(openDesktopSignedIn, args, creds, stopOpening),
|
|
800
882
|
};
|
|
801
883
|
} finally {
|
|
802
|
-
stopOpening();
|
|
884
|
+
stopOpening?.();
|
|
803
885
|
}
|
|
804
886
|
},
|
|
805
887
|
});
|
|
806
|
-
} else if (!desktop) {
|
|
807
|
-
steps.push({
|
|
808
|
-
name: "Desktop app",
|
|
809
|
-
// No question. See offerDesktop in connect/guide.js for why the ask went
|
|
810
|
-
// away; `--no-desktop` is the way past it.
|
|
811
|
-
run: () =>
|
|
812
|
-
runDesktopInstall(args, {
|
|
813
|
-
// Onboarding installs *and* hands the fresh session over, so the app
|
|
814
|
-
// opens already signed in rather than on a login screen for the
|
|
815
|
-
// account created ninety seconds ago.
|
|
816
|
-
install: async () => {
|
|
817
|
-
let stopOpening = null;
|
|
818
|
-
try {
|
|
819
|
-
const installed = await installDesktop([...commandArgs], {
|
|
820
|
-
onInstalled: () => {
|
|
821
|
-
stopOpening ??= showOpeningDesktop();
|
|
822
|
-
},
|
|
823
|
-
});
|
|
824
|
-
if (installed?.state === "unsupported") {
|
|
825
|
-
// Not a failure: the download link has already been printed, and
|
|
826
|
-
// the account this step exists to create is finished either way.
|
|
827
|
-
process.stdout.write("Sign in there with the account you just used.\n");
|
|
828
|
-
return installed;
|
|
829
|
-
}
|
|
830
|
-
stopOpening ??= showOpeningDesktop();
|
|
831
|
-
return {
|
|
832
|
-
...installed,
|
|
833
|
-
appOpened: await showDesktopSignedIn(
|
|
834
|
-
openDesktopSignedIn,
|
|
835
|
-
args,
|
|
836
|
-
creds,
|
|
837
|
-
stopOpening
|
|
838
|
-
),
|
|
839
|
-
};
|
|
840
|
-
} finally {
|
|
841
|
-
stopOpening?.();
|
|
842
|
-
}
|
|
843
|
-
},
|
|
844
|
-
}),
|
|
845
|
-
});
|
|
846
888
|
}
|
|
847
889
|
|
|
848
890
|
// Outcome per step rather than three lists, so revisiting a step replaces its
|
package/bin/verify.js
CHANGED
|
@@ -27,7 +27,7 @@ import { changedFilesForPush, readChangedFiles, readPushRefsFromStdin, repoRoot
|
|
|
27
27
|
import { missingMountPrefix, resolveLocalTarget } from "./detect.js";
|
|
28
28
|
import { formatDashboardLink, formatPushLink } from "./link.js";
|
|
29
29
|
import { createReporter } from "./progress.js";
|
|
30
|
-
import { backendUrl, callBackendJson, cliInvocation, frontendUrl, makeArgs, nowMs, resolveApiKey } from "./shared.js";
|
|
30
|
+
import { backendUrl, callBackendJson, cliInvocation, describeFailure, frontendUrl, makeArgs, nowMs, resolveApiKey } from "./shared.js";
|
|
31
31
|
|
|
32
32
|
export const VERIFY_HELP = `
|
|
33
33
|
Verify options:
|
|
@@ -250,7 +250,11 @@ async function fetchInventory(args, token) {
|
|
|
250
250
|
if (!result.ok) {
|
|
251
251
|
return {
|
|
252
252
|
endpoints,
|
|
253
|
-
|
|
253
|
+
// FastAPI answers a rejected request with an object, and a validation
|
|
254
|
+
// error with an array of them. Interpolated straight into the skip
|
|
255
|
+
// line, both read as "[object Object]" -- which is what every push
|
|
256
|
+
// against a backend that said no has printed.
|
|
257
|
+
error: describeFailure(result, `status ${result.status_code}`),
|
|
254
258
|
};
|
|
255
259
|
}
|
|
256
260
|
|