premanmcp 0.12.0 → 0.13.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.
package/bin/desktop.js CHANGED
@@ -13,7 +13,16 @@
13
13
 
14
14
  import { spawn, spawnSync } from "node:child_process";
15
15
  import { createHash } from "node:crypto";
16
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
+ import {
17
+ chmodSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ mkdtempSync,
21
+ readFileSync,
22
+ readdirSync,
23
+ rmSync,
24
+ writeFileSync,
25
+ } from "node:fs";
17
26
  import os from "node:os";
18
27
  import path from "node:path";
19
28
 
@@ -76,16 +85,36 @@ export function writeDesktopSession(creds) {
76
85
  *
77
86
  * The session is written before the app is launched so a cold start finds it on
78
87
  * first read, rather than racing a window that is already loading.
88
+ *
89
+ * Whether it was taken up is then waited for rather than assumed: the app
90
+ * deletes the file as it reads it, and an app too old to know about the file at
91
+ * all would otherwise be reported as signed in while the customer looks at a
92
+ * login screen. The file is left behind on timeout -- it expires on its own, and
93
+ * a slow first launch can still find it.
79
94
  */
80
- export function openDesktopSignedIn(creds, { destination = "/Applications" } = {}) {
95
+ export async function openDesktopSignedIn(
96
+ creds,
97
+ { destination = "/Applications", waitMs = 12_000, sleep = defaultSleep } = {}
98
+ ) {
81
99
  if (!desktopAppInstalled(destination)) {
82
100
  return { state: "not-installed" };
83
101
  }
84
102
  const handoff = writeDesktopSession(creds);
85
103
  spawn("open", ["-a", "PreMan"], { stdio: "ignore", detached: true }).unref();
86
- return { state: handoff.state === "written" ? "opened-signed-in" : "opened", handoff: handoff.state };
104
+ if (handoff.state !== "written") return { state: "opened", handoff: handoff.state };
105
+
106
+ const deadline = Date.now() + waitMs;
107
+ while (Date.now() < deadline) {
108
+ await sleep(500);
109
+ if (!existsSync(DESKTOP_SESSION_FILE)) {
110
+ return { state: "opened-signed-in", handoff: handoff.state };
111
+ }
112
+ }
113
+ return { state: "opened-not-adopted", handoff: handoff.state };
87
114
  }
88
115
 
116
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
117
+
89
118
  /**
90
119
  * Open a Playground session URL in PreMan.app on macOS when it is installed.
91
120
  * Windows/Linux (and Mac without the app) get the website URL printed instead.
@@ -212,6 +241,34 @@ function mountedVolume(hdiutilOutput) {
212
241
  return match[1].trim();
213
242
  }
214
243
 
244
+ /**
245
+ * Attach the disk image somewhere we named, rather than wherever it landed.
246
+ *
247
+ * Asking for the mountpoint is what makes this unambiguous. Reading it back out
248
+ * of `hdiutil info` is not a substitute: that lists every image attached on the
249
+ * machine, so an unrelated volume -- an iOS restore image, someone else's dmg --
250
+ * could be searched for PreMan.app instead of the download.
251
+ */
252
+ function attachDiskImage(dmgPath, mountPoint) {
253
+ mkdirSync(mountPoint, { recursive: true });
254
+ try {
255
+ run("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet", "-readonly", "-mountpoint", mountPoint]);
256
+ return mountPoint;
257
+ } catch {
258
+ // Images that refuse an explicit mountpoint still report where they went, so
259
+ // fall back to this attach's own output -- never to the global image list.
260
+ return mountedVolume(run("hdiutil", ["attach", dmgPath, "-nobrowse", "-readonly"]));
261
+ }
262
+ }
263
+
264
+ /** The bundle to copy out, found by name and then by extension. */
265
+ function appInsideVolume(volume) {
266
+ const named = path.join(volume, APP_NAME);
267
+ if (existsSync(named)) return named;
268
+ const bundle = readdirSync(volume).find((entry) => entry.endsWith(".app"));
269
+ return bundle ? path.join(volume, bundle) : null;
270
+ }
271
+
215
272
  export async function installDesktopCommand(commandArgs = []) {
216
273
  const args = makeArgs(commandArgs);
217
274
 
@@ -266,12 +323,11 @@ export async function installDesktopCommand(commandArgs = []) {
266
323
  process.stdout.write(" checksum unavailable for this release; skipping verification\n");
267
324
  }
268
325
 
269
- const attach = run("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet", "-readonly"]);
270
- mounted = mountedVolume(attach || run("hdiutil", ["info"]));
326
+ mounted = attachDiskImage(dmgPath, path.join(workDir, "mnt"));
271
327
 
272
- const source = path.join(mounted, APP_NAME);
273
- if (!existsSync(source)) {
274
- throw new Error(`${APP_NAME} not found inside the disk image at ${mounted}`);
328
+ const source = appInsideVolume(mounted);
329
+ if (!source) {
330
+ throw new Error(`no application bundle found inside the disk image at ${mounted}`);
275
331
  }
276
332
  const target = path.join(destination, APP_NAME);
277
333
  if (existsSync(target)) rmSync(target, { recursive: true, force: true });
@@ -379,7 +379,8 @@ export async function onboardCommand(
379
379
  process.stdout.write("Sign in there with the account you just used.\n");
380
380
  return;
381
381
  }
382
- const opened = openDesktopSignedIn(creds);
382
+ process.stdout.write("Opening PreMan\u2026\n");
383
+ const opened = await openDesktopSignedIn(creds);
383
384
  if (opened.state === "opened-signed-in") {
384
385
  process.stdout.write("Opened PreMan, signed in as this account.\n");
385
386
  } else if (opened.state === "not-installed") {
@@ -387,25 +388,24 @@ export async function onboardCommand(
387
388
  "PreMan is not in /Applications yet \u2014 open it once installed and sign in.\n"
388
389
  );
389
390
  } else {
390
- // The app is there but the session could not be handed over, so say so
391
- // rather than let the customer wonder why they are at a login screen.
391
+ // Either the session could not be handed over or this app is too old to
392
+ // take it up. Say so rather than let the customer wonder why they are
393
+ // looking at a login screen.
392
394
  process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
393
395
  }
394
396
  },
395
397
  },
396
- { name: "GitHub", question: "Connect GitHub?", run: () => githubCommand(args) },
397
- { name: "AWS logs", question: "Connect AWS?", run: () => awsCommand(args) },
398
- { name: "Slack", question: "Connect Slack?", run: () => slackCommand(args) },
398
+ // GitHub, AWS and Slack are deliberately not here. Starting out is an account
399
+ // and the app; each integration is its own command for whenever it is wanted.
399
400
  ];
400
401
 
401
402
  // Outcome per step rather than three lists, so revisiting a step replaces its
402
403
  // result instead of recording it twice.
403
404
  const outcome = new Map();
404
405
 
405
- // Indexed rather than for..of so "back" can move the cursor. Re-running a
406
- // step is safe: every one of these is idempotent on the backend -- an AWS
407
- // grant returns the existing link, and an App install that already happened
408
- // is detected rather than duplicated.
406
+ // Indexed rather than for..of so "back" can move the cursor. Re-running a step
407
+ // is safe: an install that already happened overwrites the same bundle rather
408
+ // than duplicating it.
409
409
  let i = 0;
410
410
  while (i < steps.length) {
411
411
  const step = steps[i];
@@ -451,9 +451,8 @@ export async function onboardCommand(
451
451
 
452
452
  export const INTEGRATIONS_HELP = `
453
453
  Setup options:
454
- preman onboard Create or sign in to an account, install the
455
- PreMan app signed in, then GitHub, AWS and Slack,
456
- one prompt per step
454
+ preman onboard Create or sign in to an account, then install the
455
+ PreMan app and open it signed in
457
456
  preman aws Connect an AWS account and stream a log group
458
457
  preman github Install the PreMan GitHub App
459
458
  preman slack Add PreMan to a Slack workspace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,9 +13,10 @@
13
13
  "dev": "tsx src/server.ts",
14
14
  "open-mcp-preview": "node scripts/emit-mcp-preview.mjs",
15
15
  "open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
16
- "test": "npm run build && npm run test:connect && npm run test:node",
16
+ "test": "npm run build && npm run test:connect && npm run test:node && npm run test:dmg",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
- "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs"
18
+ "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs",
19
+ "test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
19
20
  },
20
21
  "dependencies": {
21
22
  "@modelcontextprotocol/ext-apps": "^0.1.0",