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.
@@ -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
- // `return_to: "desktop"` because the person who ran this is in a terminal and
269
- // the CLI is already polling for the installation. The web return 302s the
270
- // tab into the hosted dashboard an app they did not ask for, on whichever
271
- // environment owns the App's setup URL. The desktop return finishes on a page
272
- // that offers PreMan and says the tab may be closed, which is all the browser
273
- // still has to do here.
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
- // Nothing to download, which is the point of this branch. It is not a
790
- // reason to stop at the sentence: the step exists to leave the customer
791
- // looking at PreMan, so the one run with nothing to install was also the
792
- // only run that opened nothing, and it closed by sending someone to a
793
- // website instead of the app already sitting on their machine.
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
- process.stdout.write(`${MARK.ok()} PreMan desktop app already installed.\n`);
796
- const stopOpening = showOpeningDesktop();
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
- error: result.detail || `status ${result.status_code}`,
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
5
5
  "type": "module",
6
6
  "bin": {