premanmcp 0.9.0 → 0.10.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/hook.js CHANGED
@@ -56,8 +56,10 @@ if [ -z "\${PREMAN_SKIP_HOOK}" ]; then
56
56
  if [ "$preman_status" -eq ${BLOCK_EXIT_CODE} ]; then
57
57
  exit ${BLOCK_EXIT_CODE}
58
58
  fi
59
- if [ "$preman_status" -ne 0 ]; then
60
- printf '[preman] checks skipped (%s)\\n' "advisory" >&2
59
+ if [ "$preman_status" -eq 127 ]; then
60
+ printf '[preman] the PreMan CLI is not on PATH; push checks skipped\\n' >&2
61
+ elif [ "$preman_status" -ne 0 ]; then
62
+ printf '[preman] checks skipped (advisory, exit %s)\\n' "$preman_status" >&2
61
63
  fi
62
64
  fi
63
65
  ${END_MARKER}
@@ -116,11 +118,21 @@ export function uninstallHook() {
116
118
  return { path: target, action: "removed" };
117
119
  }
118
120
 
121
+ /**
122
+ * `current` is what tells a caller an installed hook still needs rewriting.
123
+ *
124
+ * A hook is generated shell holding one invocation, and that invocation can go
125
+ * stale — the machine gained a `preman` that is not ours, or lost the one that
126
+ * was. "Installed" then means a file exists that calls the wrong thing on every
127
+ * push, so anything that skips work when the hook is present has to be able to
128
+ * tell the difference.
129
+ */
119
130
  export function hookStatus() {
120
131
  const target = hookPath();
121
- if (!existsSync(target)) return { path: target, state: "absent" };
132
+ if (!existsSync(target)) return { path: target, state: "absent", current: false };
122
133
  const existing = readFileSync(target, "utf8");
123
- return { path: target, state: isOurHook(existing) ? "installed" : "foreign" };
134
+ if (!isOurHook(existing)) return { path: target, state: "foreign", current: false };
135
+ return { path: target, state: "installed", current: existing === hookBody() };
124
136
  }
125
137
 
126
138
  export async function hookCommand(commandArgs = []) {
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  assertOk,
19
19
  callBackendJson,
20
+ cliInvocation,
20
21
  frontendUrl,
21
22
  openUrl,
22
23
  promptText,
@@ -41,9 +42,9 @@ function paint(code, text) {
41
42
  }
42
43
 
43
44
  export const MARK = {
44
- ok: () => paint("32", "?"),
45
- skip: () => paint("90", "?"),
46
- fail: () => paint("31", "?"),
45
+ ok: () => paint("32", "✓"),
46
+ skip: () => paint("90", "·"),
47
+ fail: () => paint("31", "✗"),
47
48
  };
48
49
 
49
50
  /** A finished step, marked so it can be found at a glance. */
@@ -74,8 +75,10 @@ export class Unrecoverable extends Error {}
74
75
  * Returns the truthy value from ``check``, or null on timeout. Ordinary
75
76
  * exceptions are swallowed and retried; :class:`Unrecoverable` stops the wait.
76
77
  */
77
- async function waitFor(label, check) {
78
- const deadline = Date.now() + POLL_TIMEOUT_MS;
78
+ async function waitFor(label, check, { hint = "", hintAfterMs = 60000 } = {}) {
79
+ const startedAt = Date.now();
80
+ const deadline = startedAt + POLL_TIMEOUT_MS;
81
+ let hinted = false;
79
82
  process.stdout.write(`Waiting for ${label}`);
80
83
  while (Date.now() < deadline) {
81
84
  try {
@@ -91,6 +94,12 @@ async function waitFor(label, check) {
91
94
  }
92
95
  /* keep waiting; the customer is elsewhere */
93
96
  }
97
+ // A minute of dots is indistinguishable from a hang, and the reason this
98
+ // waits forever is usually something the customer can act on.
99
+ if (hint && !hinted && Date.now() - startedAt > hintAfterMs) {
100
+ hinted = true;
101
+ process.stdout.write(`\n${hint}\nStill waiting`);
102
+ }
94
103
  process.stdout.write(".");
95
104
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
96
105
  }
@@ -101,7 +110,7 @@ async function waitFor(label, check) {
101
110
  function requireKey(args) {
102
111
  const apiKey = resolveApiKey(args);
103
112
  if (!apiKey) {
104
- throw new Error("Not signed in. Run 'preman login' first.");
113
+ throw new Error(`Not signed in. Run '${cliInvocation()} login' first.`);
105
114
  }
106
115
  return apiKey;
107
116
  }
@@ -156,7 +165,7 @@ export async function awsCommand(args) {
156
165
  }
157
166
 
158
167
  if (!verified) {
159
- process.stdout.write("Timed out. Re-run 'preman aws' once the stack finishes.\n");
168
+ process.stdout.write(`Timed out. Re-run '${cliInvocation()} aws' once the stack finishes.\n`);
160
169
  return;
161
170
  }
162
171
  connected(`AWS connected: ${verified.role_arn}`);
@@ -223,23 +232,35 @@ export async function githubCommand(args) {
223
232
  present(url, "install the PreMan GitHub App");
224
233
  process.stdout.write("Pick the repositories PreMan may read.\n");
225
234
 
226
- const done = await waitFor("the installation", async () => {
227
- // Installing the App and having repositories appear are two events: the
228
- // callback records the installation, and a refresh materialises the repos.
229
- // Polling the repo list alone waits for something that may never arrive on
230
- // its own.
231
- await callBackendJson(args, "POST", "/integrations/github/app/refresh", {
232
- token,
233
- json: {},
234
- });
235
- // Compare against what existed before, so a user who already had repos
236
- // connected is not told they are done the moment polling starts.
237
- const fresh = (await listRepos()).filter((r) => !seen.has(r.id));
238
- return fresh.length ? fresh : null;
239
- });
235
+ const done = await waitFor(
236
+ "the installation",
237
+ async () => {
238
+ // Installing the App and having repositories appear are two events: the
239
+ // callback records the installation, and a refresh materialises the repos.
240
+ // Polling the repo list alone waits for something that may never arrive on
241
+ // its own.
242
+ await callBackendJson(args, "POST", "/integrations/github/app/refresh", {
243
+ token,
244
+ json: {},
245
+ });
246
+ // Compare against what existed before, so a user who already had repos
247
+ // connected is not told they are done the moment polling starts.
248
+ const fresh = (await listRepos()).filter((r) => !seen.has(r.id));
249
+ return fresh.length ? fresh : null;
250
+ },
251
+ {
252
+ hint:
253
+ "PreMan has not heard from GitHub yet. Finish the install in the browser tab —\n" +
254
+ "pick at least one repository and confirm — and GitHub will send you back here.",
255
+ }
256
+ );
240
257
 
241
258
  if (!done) {
242
- process.stdout.write("Timed out. Re-run 'preman github' if the install finished.\n");
259
+ process.stdout.write(
260
+ `Timed out: GitHub never told PreMan about an installation.\n` +
261
+ ` - Check that the App is installed: https://github.com/settings/installations\n` +
262
+ ` - Then re-run '${cliInvocation()} github'.\n`
263
+ );
243
264
  return;
244
265
  }
245
266
  connected(`GitHub connected: ${done.length} repository(ies).`);
@@ -270,7 +291,7 @@ export async function slackCommand(args) {
270
291
  });
271
292
 
272
293
  if (!done) {
273
- process.stdout.write("Timed out. Re-run 'preman slack' if the install finished.\n");
294
+ process.stdout.write(`Timed out. Re-run '${cliInvocation()} slack' if the install finished.\n`);
274
295
  return;
275
296
  }
276
297
  connected(`Slack connected: ${done[0].team_name || done[0].id}`);
@@ -329,7 +350,7 @@ export async function onboardCommand(commandArgs, { makeArgs, authenticateTermin
329
350
  let i = 0;
330
351
  while (i < steps.length) {
331
352
  const step = steps[i];
332
- process.stdout.write(`\n?? ${step.name} ??\n`);
353
+ process.stdout.write(`\n── ${step.name} ──\n`);
333
354
 
334
355
  const choice = await askStep(step.question, { assumeYes, canGoBack: i > 0 });
335
356
  if (choice === "back") {
@@ -357,7 +378,7 @@ export async function onboardCommand(commandArgs, { makeArgs, authenticateTermin
357
378
  i += 1;
358
379
  }
359
380
 
360
- process.stdout.write("\n?? done ??\n");
381
+ process.stdout.write("\n── done ──\n");
361
382
  // One line per step, marked, so the outcome is scannable rather than prose.
362
383
  for (const step of steps) {
363
384
  const result = outcome.get(step.name);