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/README.md +49 -11
- package/bin/account.js +1 -1
- package/bin/api_tools.js +2 -2
- package/bin/cli.js +5 -1
- package/bin/connect.js +732 -68
- package/bin/hook.js +16 -4
- package/bin/integrations.js +46 -25
- package/bin/runner.js +946 -0
- package/bin/shared.js +65 -6
- package/dist/server.js +30 -9
- package/package.json +3 -3
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" -
|
|
60
|
-
printf '[preman] checks skipped
|
|
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:
|
|
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 = []) {
|
package/bin/integrations.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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);
|