@paradigma-inc/flywheel 0.1.4 → 0.1.9
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 +129 -24
- package/package.json +10 -8
- package/skills/flywheel/SKILL.md +52 -0
- package/skills/flywheel/agents/openai.yaml +15 -0
- package/skills/flywheel/campaigns/participating-in-a-campaign.md +18 -0
- package/skills/flywheel/compute/credits-and-billing.md +11 -0
- package/skills/flywheel/compute/managed-compute.md +44 -0
- package/skills/flywheel/example-workflows/organizing-exploring-and-iterating-on-a-research-topic.md +255 -0
- package/skills/flywheel/example-workflows/reproducing-papers-on-a-budget.md +151 -0
- package/skills/flywheel/getting-started/account-access.md +8 -0
- package/skills/flywheel/getting-started/flywheel-quickstart.md +23 -0
- package/skills/flywheel/getting-started/flywheel-tutorial-overview.md +30 -0
- package/skills/flywheel/reference/experiment-design-protocol.md +200 -0
- package/skills/flywheel/reference/flywheel-mcp-tool-map.md +160 -0
- package/skills/flywheel/setting-up-flywheel/claude-code-cli-installation.md +16 -0
- package/skills/flywheel/setting-up-flywheel/codex-cli-installation.md +16 -0
- package/skills/flywheel/setting-up-flywheel/how-can-i-get-an-authorized-client_id-for-the-oauth-flow.md +50 -0
- package/skills/flywheel/setting-up-flywheel/installation-overview.md +26 -0
- package/skills/flywheel/setting-up-flywheel/other-hosts-installation.md +40 -0
- package/skills/flywheel/setting-up-flywheel/updating-flywheel-mcp.md +21 -0
- package/skills/flywheel/usage-and-workflows/using-local-hardware-with-flywheel.md +57 -0
- package/skills/flywheel/usage-and-workflows/what-to-do-with-flywheel.md +34 -0
- package/skills/flywheel/web-ui/flywheel-webui-map.md +28 -0
- package/skills/flywheel/web-ui/the-flywheel-web-ui.md +17 -0
- package/src/cli.mjs +508 -54
- package/src/mcp-writer.mjs +128 -3
- package/src/setup-auth.mjs +231 -27
- package/src/skill-installer.mjs +542 -0
package/src/mcp-writer.mjs
CHANGED
|
@@ -33,6 +33,77 @@ function normalizeLineEndings(text) {
|
|
|
33
33
|
return text.replace(/\r\n/g, "\n");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function stripTomlInlineComment(valueText) {
|
|
37
|
+
let inSingleQuotedString = false;
|
|
38
|
+
let inDoubleQuotedString = false;
|
|
39
|
+
let escapeNextChar = false;
|
|
40
|
+
|
|
41
|
+
for (let index = 0; index < valueText.length; index += 1) {
|
|
42
|
+
const char = valueText[index];
|
|
43
|
+
|
|
44
|
+
if (escapeNextChar) {
|
|
45
|
+
escapeNextChar = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (inDoubleQuotedString && char === "\\") {
|
|
50
|
+
escapeNextChar = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!inDoubleQuotedString && char === "'") {
|
|
55
|
+
inSingleQuotedString = !inSingleQuotedString;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!inSingleQuotedString && char === '"') {
|
|
60
|
+
inDoubleQuotedString = !inDoubleQuotedString;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!inSingleQuotedString && !inDoubleQuotedString && char === "#") {
|
|
65
|
+
return valueText.slice(0, index).trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return valueText.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseTomlStringValue(valueText) {
|
|
73
|
+
const stripped = stripTomlInlineComment(valueText);
|
|
74
|
+
if (!stripped) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (stripped.startsWith('"""') && stripped.endsWith('"""') && stripped.length >= 6) {
|
|
79
|
+
return stripped.slice(3, -3);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (stripped.startsWith("'''") && stripped.endsWith("'''") && stripped.length >= 6) {
|
|
83
|
+
return stripped.slice(3, -3);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (stripped.startsWith('"') && stripped.endsWith('"')) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(stripped);
|
|
89
|
+
} catch {
|
|
90
|
+
return stripped.slice(1, -1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (stripped.startsWith("'") && stripped.endsWith("'")) {
|
|
95
|
+
return stripped.slice(1, -1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return stripped;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseTomlSectionName(lineText) {
|
|
102
|
+
const withoutComment = stripTomlInlineComment(String(lineText || "").trim());
|
|
103
|
+
const sectionMatch = withoutComment.match(/^\[(.+)\]$/);
|
|
104
|
+
return sectionMatch ? sectionMatch[1] : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
36
107
|
export async function readJsonConfig(filePath) {
|
|
37
108
|
let raw;
|
|
38
109
|
try {
|
|
@@ -120,6 +191,15 @@ function readConfigSection(config, configKey) {
|
|
|
120
191
|
return current;
|
|
121
192
|
}
|
|
122
193
|
|
|
194
|
+
export function readNamedConfigEntry(config, configKey, serverName) {
|
|
195
|
+
const section = readConfigSection(config, configKey);
|
|
196
|
+
const entry = section[serverName];
|
|
197
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
123
203
|
function writeConfigSection(config, configKey, section) {
|
|
124
204
|
const pathParts = keyPath(configKey);
|
|
125
205
|
if (pathParts.length === 0) return config || {};
|
|
@@ -182,6 +262,52 @@ export async function readTomlServerExists(filePath, serverName) {
|
|
|
182
262
|
}
|
|
183
263
|
}
|
|
184
264
|
|
|
265
|
+
export async function readTomlServerEntry(filePath, serverName) {
|
|
266
|
+
let raw = "";
|
|
267
|
+
try {
|
|
268
|
+
raw = await readFile(filePath, "utf8");
|
|
269
|
+
} catch {
|
|
270
|
+
return { exists: false, url: null };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const normalized = normalizeLineEndings(raw);
|
|
274
|
+
const lines = normalized.split("\n");
|
|
275
|
+
const targetSection = `mcp_servers.${serverName}`;
|
|
276
|
+
let inTargetSection = false;
|
|
277
|
+
let exists = false;
|
|
278
|
+
let url = null;
|
|
279
|
+
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
const trimmed = line.trim();
|
|
282
|
+
const sectionName = parseTomlSectionName(trimmed);
|
|
283
|
+
|
|
284
|
+
if (sectionName) {
|
|
285
|
+
if (sectionName === targetSection) {
|
|
286
|
+
exists = true;
|
|
287
|
+
inTargetSection = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (exists && !sectionName.startsWith(`${targetSection}.`)) {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
inTargetSection = false;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!inTargetSection) continue;
|
|
300
|
+
|
|
301
|
+
const withoutComment = stripTomlInlineComment(trimmed);
|
|
302
|
+
const urlMatch = withoutComment.match(/^url\s*=\s*(.+)$/);
|
|
303
|
+
if (!urlMatch) continue;
|
|
304
|
+
|
|
305
|
+
url = parseTomlStringValue(urlMatch[1]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { exists, url };
|
|
309
|
+
}
|
|
310
|
+
|
|
185
311
|
export function buildTomlServerBlock(serverName, entry) {
|
|
186
312
|
const lines = [`[mcp_servers.${serverName}]`];
|
|
187
313
|
const headers =
|
|
@@ -242,10 +368,9 @@ function stripCodexTomlServerBlock({ tomlText, serverName }) {
|
|
|
242
368
|
|
|
243
369
|
for (const line of lines) {
|
|
244
370
|
const trimmed = line.trim();
|
|
245
|
-
const
|
|
371
|
+
const sectionName = parseTomlSectionName(trimmed);
|
|
246
372
|
|
|
247
|
-
if (
|
|
248
|
-
const sectionName = sectionMatch[1];
|
|
373
|
+
if (sectionName) {
|
|
249
374
|
const isTargetSection =
|
|
250
375
|
sectionName === `mcp_servers.${serverName}` ||
|
|
251
376
|
sectionName.startsWith(`mcp_servers.${serverName}.`);
|
package/src/setup-auth.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import http from "node:http";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
6
|
+
const MIN_POLL_DELAY_MS = 1000;
|
|
7
|
+
const MAX_POLL_DELAY_MS = 10_000;
|
|
6
8
|
|
|
7
9
|
function openUrlInBrowser(url) {
|
|
8
10
|
const platform = process.platform;
|
|
@@ -32,6 +34,39 @@ function renderCallbackPage({ ok, message }) {
|
|
|
32
34
|
</html>`;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
function resolveErrorMessage(payload, fallback) {
|
|
38
|
+
if (!payload || typeof payload !== "object") {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
const detail = payload.detail;
|
|
42
|
+
if (typeof detail === "string" && detail.trim()) {
|
|
43
|
+
return detail.trim();
|
|
44
|
+
}
|
|
45
|
+
if (detail && typeof detail === "object") {
|
|
46
|
+
const message = detail.message;
|
|
47
|
+
if (typeof message === "string" && message.trim()) {
|
|
48
|
+
return message.trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function parseJsonResponse(response) {
|
|
55
|
+
let payload = {};
|
|
56
|
+
try {
|
|
57
|
+
payload = await response.json();
|
|
58
|
+
} catch {
|
|
59
|
+
payload = {};
|
|
60
|
+
}
|
|
61
|
+
return payload;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nonEmptyString(value) {
|
|
65
|
+
if (typeof value !== "string") return null;
|
|
66
|
+
const candidate = value.trim();
|
|
67
|
+
return candidate ? candidate : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
35
70
|
async function redeemSetupExchangeToken({
|
|
36
71
|
baseUrl,
|
|
37
72
|
exchangeToken,
|
|
@@ -54,38 +89,163 @@ async function redeemSetupExchangeToken({
|
|
|
54
89
|
redirect_uri: redirectUri,
|
|
55
90
|
}),
|
|
56
91
|
});
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
payload = await response.json();
|
|
60
|
-
} catch {
|
|
61
|
-
payload = {};
|
|
62
|
-
}
|
|
92
|
+
const payload = await parseJsonResponse(response);
|
|
63
93
|
if (!response.ok) {
|
|
64
|
-
const detail =
|
|
94
|
+
const detail = resolveErrorMessage(payload, "Setup exchange redemption failed.");
|
|
65
95
|
throw new Error(detail);
|
|
66
96
|
}
|
|
67
|
-
const key = (payload?.key
|
|
97
|
+
const key = nonEmptyString(payload?.key);
|
|
68
98
|
if (!key) {
|
|
69
99
|
throw new Error("Setup exchange response missing API key.");
|
|
70
100
|
}
|
|
71
101
|
return { apiKey: key };
|
|
72
102
|
}
|
|
73
103
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
104
|
+
async function startSetupDeviceSession({
|
|
105
|
+
baseUrl,
|
|
106
|
+
keyName,
|
|
107
|
+
verificationBaseUrl,
|
|
108
|
+
}) {
|
|
109
|
+
const startUrl = new URL("/api/auth/mcp-api-keys/setup-device/start", baseUrl);
|
|
110
|
+
const response = await fetch(startUrl, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"content-type": "application/json",
|
|
114
|
+
"Idempotency-Key": `setup-device-start:${crypto.randomUUID()}`,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
name: keyName,
|
|
118
|
+
verification_base_url: verificationBaseUrl,
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
const payload = await parseJsonResponse(response);
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const detail = resolveErrorMessage(payload, "Failed to start setup device flow.");
|
|
124
|
+
throw new Error(detail);
|
|
77
125
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
126
|
+
|
|
127
|
+
const deviceCode = nonEmptyString(payload.device_code);
|
|
128
|
+
const userCode = nonEmptyString(payload.user_code);
|
|
129
|
+
const verificationUri = nonEmptyString(payload.verification_uri);
|
|
130
|
+
const verificationUriComplete = nonEmptyString(payload.verification_uri_complete);
|
|
131
|
+
const pollIntervalSeconds = Number(payload.poll_interval_seconds);
|
|
132
|
+
const expiresInSeconds = Number(payload.expires_in_seconds);
|
|
133
|
+
|
|
134
|
+
if (!deviceCode || !userCode || !verificationUri || !verificationUriComplete) {
|
|
135
|
+
throw new Error("Setup device session response missing required fields.");
|
|
81
136
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
137
|
+
if (
|
|
138
|
+
!Number.isFinite(pollIntervalSeconds) ||
|
|
139
|
+
pollIntervalSeconds <= 0 ||
|
|
140
|
+
!Number.isFinite(expiresInSeconds) ||
|
|
141
|
+
expiresInSeconds <= 0
|
|
142
|
+
) {
|
|
143
|
+
throw new Error("Setup device session response contained invalid timing values.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
deviceCode,
|
|
148
|
+
userCode,
|
|
149
|
+
verificationUri,
|
|
150
|
+
verificationUriComplete,
|
|
151
|
+
pollIntervalSeconds: Math.floor(pollIntervalSeconds),
|
|
152
|
+
expiresInSeconds: Math.floor(expiresInSeconds),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function pollSetupDeviceSession({ baseUrl, userCode, deviceCode }) {
|
|
157
|
+
const pollUrl = new URL("/api/auth/mcp-api-keys/setup-device/poll", baseUrl);
|
|
158
|
+
const response = await fetch(pollUrl, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"content-type": "application/json",
|
|
162
|
+
"Idempotency-Key": `setup-device-poll:${deviceCode}:${Date.now()}`,
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
user_code: userCode,
|
|
166
|
+
device_code: deviceCode,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
if (response.status === 404) {
|
|
170
|
+
return { status: "expired", pollIntervalSeconds: 0 };
|
|
171
|
+
}
|
|
172
|
+
const payload = await parseJsonResponse(response);
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
const detail = resolveErrorMessage(payload, "Setup device polling failed.");
|
|
175
|
+
throw new Error(detail);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const status = nonEmptyString(payload.status);
|
|
179
|
+
const pollIntervalSeconds = Number(payload.poll_interval_seconds);
|
|
180
|
+
if (!status || !Number.isFinite(pollIntervalSeconds) || pollIntervalSeconds <= 0) {
|
|
181
|
+
throw new Error("Setup device poll response missing required status fields.");
|
|
182
|
+
}
|
|
183
|
+
if (status === "pending") {
|
|
184
|
+
return {
|
|
185
|
+
status,
|
|
186
|
+
pollIntervalSeconds: Math.floor(pollIntervalSeconds),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (status === "approved") {
|
|
190
|
+
const key = nonEmptyString(payload.key);
|
|
191
|
+
if (!key) {
|
|
192
|
+
throw new Error("Setup device poll response missing API key.");
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
status,
|
|
196
|
+
pollIntervalSeconds: Math.floor(pollIntervalSeconds),
|
|
197
|
+
apiKey: key,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Unsupported setup device session status: ${status}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pollDelayMs({ pollIntervalSeconds, attempt }) {
|
|
204
|
+
const baseMs = Math.max(
|
|
205
|
+
MIN_POLL_DELAY_MS,
|
|
206
|
+
Math.floor(pollIntervalSeconds * 1000),
|
|
207
|
+
);
|
|
208
|
+
const rampMs = Math.min(5000, attempt * 250);
|
|
209
|
+
const jitterMs = Math.floor(Math.random() * 250);
|
|
210
|
+
return Math.min(MAX_POLL_DELAY_MS, baseMs + rampMs + jitterMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function defaultSleep(ms) {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
setTimeout(resolve, ms);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function launchBrowser(openUrl, url) {
|
|
220
|
+
try {
|
|
221
|
+
const child = openUrl(url);
|
|
222
|
+
if (child && typeof child.once === "function") {
|
|
223
|
+
child.once("error", () => {
|
|
224
|
+
// URL is printed to stdout; opener failures should not abort setup.
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (child && typeof child.unref === "function") {
|
|
228
|
+
child.unref();
|
|
86
229
|
}
|
|
230
|
+
} catch {
|
|
231
|
+
// User can still open URL manually.
|
|
87
232
|
}
|
|
88
|
-
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function resolveSetupAuthMode({ requestedMode, env = process.env }) {
|
|
236
|
+
const normalizedRequested = String(requestedMode || "auto")
|
|
237
|
+
.trim()
|
|
238
|
+
.toLowerCase();
|
|
239
|
+
|
|
240
|
+
if (normalizedRequested === "device" || normalizedRequested === "loopback") {
|
|
241
|
+
return normalizedRequested;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const isRemoteShell =
|
|
245
|
+
nonEmptyString(env.SSH_CONNECTION) ||
|
|
246
|
+
nonEmptyString(env.SSH_CLIENT) ||
|
|
247
|
+
nonEmptyString(env.SSH_TTY);
|
|
248
|
+
return isRemoteShell ? "device" : "loopback";
|
|
89
249
|
}
|
|
90
250
|
|
|
91
251
|
export async function acquireApiKeyViaBrowserBridge({
|
|
@@ -221,14 +381,7 @@ export async function acquireApiKeyViaBrowserBridge({
|
|
|
221
381
|
console.log("Opening browser for Flywheel login and key creation...");
|
|
222
382
|
console.log(`If it does not open, use this URL:\n${setupUrl.toString()}\n`);
|
|
223
383
|
|
|
224
|
-
|
|
225
|
-
const child = openUrl(setupUrl.toString());
|
|
226
|
-
if (child && typeof child.unref === "function") {
|
|
227
|
-
child.unref();
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
// User can still open URL manually.
|
|
231
|
-
}
|
|
384
|
+
launchBrowser(openUrl, setupUrl.toString());
|
|
232
385
|
});
|
|
233
386
|
|
|
234
387
|
timeout = setTimeout(() => {
|
|
@@ -236,3 +389,54 @@ export async function acquireApiKeyViaBrowserBridge({
|
|
|
236
389
|
}, timeoutMs);
|
|
237
390
|
});
|
|
238
391
|
}
|
|
392
|
+
|
|
393
|
+
export async function acquireApiKeyViaDeviceFlow({
|
|
394
|
+
baseUrl,
|
|
395
|
+
keyName,
|
|
396
|
+
verificationBaseUrl = baseUrl,
|
|
397
|
+
timeoutMs = DEFAULT_AUTH_TIMEOUT_MS,
|
|
398
|
+
openUrl = openUrlInBrowser,
|
|
399
|
+
sleep = defaultSleep,
|
|
400
|
+
}) {
|
|
401
|
+
const startedAt = Date.now();
|
|
402
|
+
const session = await startSetupDeviceSession({
|
|
403
|
+
baseUrl,
|
|
404
|
+
keyName,
|
|
405
|
+
verificationBaseUrl,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
console.log("Opening browser for Flywheel device authorization...");
|
|
409
|
+
console.log(`Approval URL:\n${session.verificationUriComplete}\n`);
|
|
410
|
+
console.log(`Code: ${session.userCode}`);
|
|
411
|
+
console.log(
|
|
412
|
+
"If the page does not open automatically, open the URL above and approve this code.\n",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
launchBrowser(openUrl, session.verificationUriComplete);
|
|
416
|
+
|
|
417
|
+
let attempt = 0;
|
|
418
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
419
|
+
// eslint-disable-next-line no-await-in-loop
|
|
420
|
+
const pollResult = await pollSetupDeviceSession({
|
|
421
|
+
baseUrl,
|
|
422
|
+
userCode: session.userCode,
|
|
423
|
+
deviceCode: session.deviceCode,
|
|
424
|
+
});
|
|
425
|
+
if (pollResult.status === "approved") {
|
|
426
|
+
return { apiKey: pollResult.apiKey };
|
|
427
|
+
}
|
|
428
|
+
if (pollResult.status === "expired") {
|
|
429
|
+
throw new Error("Setup device session expired. Please rerun setup.");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
attempt += 1;
|
|
433
|
+
const delayMs = pollDelayMs({
|
|
434
|
+
pollIntervalSeconds: pollResult.pollIntervalSeconds,
|
|
435
|
+
attempt,
|
|
436
|
+
});
|
|
437
|
+
// eslint-disable-next-line no-await-in-loop
|
|
438
|
+
await sleep(delayMs);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
throw new Error("Timed out waiting for device authorization.");
|
|
442
|
+
}
|