@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/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { checkbox, select } from "@inquirer/prompts";
|
|
2
|
+
import { checkbox, confirm, select } from "@inquirer/prompts";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import pc from "picocolors";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { randomBytes } from "node:crypto";
|
|
7
8
|
|
|
@@ -16,8 +17,9 @@ import {
|
|
|
16
17
|
import {
|
|
17
18
|
appendTomlServer,
|
|
18
19
|
mergeServerEntry,
|
|
20
|
+
readNamedConfigEntry,
|
|
19
21
|
readJsonConfig,
|
|
20
|
-
|
|
22
|
+
readTomlServerEntry,
|
|
21
23
|
readYamlConfig,
|
|
22
24
|
removeCodexTomlServer,
|
|
23
25
|
removeJsonServerEntry,
|
|
@@ -25,10 +27,32 @@ import {
|
|
|
25
27
|
writeJsonConfig,
|
|
26
28
|
writeYamlConfig,
|
|
27
29
|
} from "./mcp-writer.mjs";
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
acquireApiKeyViaBrowserBridge,
|
|
32
|
+
acquireApiKeyViaDeviceFlow,
|
|
33
|
+
resolveSetupAuthMode,
|
|
34
|
+
} from "./setup-auth.mjs";
|
|
35
|
+
import {
|
|
36
|
+
cleanupInstalledSkillArtifactsForHosts,
|
|
37
|
+
formatSupportedInstallSkillHosts,
|
|
38
|
+
inspectInstalledSkillArtifactsForHosts,
|
|
39
|
+
installBundledSkillForHosts,
|
|
40
|
+
listBundledSkills,
|
|
41
|
+
partitionInstallSkillHosts,
|
|
42
|
+
resolveInstalledSkillDir,
|
|
43
|
+
resolvePackageRoot,
|
|
44
|
+
} from "./skill-installer.mjs";
|
|
29
45
|
|
|
46
|
+
const INSTALL_SKILL_BASE_URL = "https://flywheel.paradigma.inc";
|
|
30
47
|
const DEFAULT_BASE_URL =
|
|
31
|
-
process.env.FLYWHEEL_PUBLIC_BASE_URL ||
|
|
48
|
+
process.env.FLYWHEEL_PUBLIC_BASE_URL || INSTALL_SKILL_BASE_URL;
|
|
49
|
+
const NON_INTERACTIVE_SKILL_DECISION_ERROR =
|
|
50
|
+
"Non-interactive setup requires one of --install-skill or --skip-skill.";
|
|
51
|
+
const MUTUALLY_EXCLUSIVE_SKILL_FLAGS_ERROR =
|
|
52
|
+
"--install-skill and --skip-skill cannot be used together.";
|
|
53
|
+
const GUIDED_SKILL_DECISION_TEST_ENV =
|
|
54
|
+
"FLYWHEEL_GUIDED_SKILL_DECISION_FOR_TESTS";
|
|
55
|
+
const ENABLE_TEST_SEAMS_ENV = "FLYWHEEL_ENABLE_TEST_SEAMS";
|
|
32
56
|
|
|
33
57
|
const log = {
|
|
34
58
|
info: (message) => console.log(pc.cyan(message)),
|
|
@@ -71,6 +95,20 @@ function normalizeServerName(value) {
|
|
|
71
95
|
return normalized;
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
function normalizeAuthMode(value) {
|
|
99
|
+
const normalized = String(value || "auto")
|
|
100
|
+
.trim()
|
|
101
|
+
.toLowerCase();
|
|
102
|
+
if (
|
|
103
|
+
normalized === "auto" ||
|
|
104
|
+
normalized === "loopback" ||
|
|
105
|
+
normalized === "device"
|
|
106
|
+
) {
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Auth mode must be one of: auto, loopback, device.");
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
function selectedHostsFromOptions(options) {
|
|
75
113
|
const hosts = [];
|
|
76
114
|
if (options.claude) hosts.push("claude");
|
|
@@ -100,39 +138,122 @@ function mcpCandidatesForScope(host, scope) {
|
|
|
100
138
|
);
|
|
101
139
|
}
|
|
102
140
|
|
|
103
|
-
function
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Array.isArray(current) ||
|
|
111
|
-
!(part in current)
|
|
112
|
-
) {
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
const next = current[part];
|
|
116
|
-
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
117
|
-
return {};
|
|
118
|
-
}
|
|
119
|
-
current = next;
|
|
120
|
-
}
|
|
121
|
-
return current;
|
|
141
|
+
async function isAlreadyConfigured(hostName, scope, serverName) {
|
|
142
|
+
const existingState = await readExistingServerState(
|
|
143
|
+
hostName,
|
|
144
|
+
scope,
|
|
145
|
+
serverName,
|
|
146
|
+
);
|
|
147
|
+
return existingState.exists;
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
async function
|
|
150
|
+
async function readExistingServerState(hostName, scope, serverName) {
|
|
125
151
|
const host = getHost(hostName);
|
|
126
152
|
const mcpPath = await resolveMcpPath(mcpCandidatesForScope(host, scope));
|
|
153
|
+
|
|
127
154
|
if (host.mcp.configType === "toml") {
|
|
128
|
-
|
|
155
|
+
const { exists, url } = await readTomlServerEntry(mcpPath, serverName);
|
|
156
|
+
return { exists, url, mcpPath };
|
|
129
157
|
}
|
|
158
|
+
|
|
130
159
|
const existing =
|
|
131
160
|
host.mcp.configType === "yaml"
|
|
132
161
|
? await readYamlConfig(mcpPath)
|
|
133
162
|
: await readJsonConfig(mcpPath);
|
|
134
|
-
const
|
|
135
|
-
return
|
|
163
|
+
const entry = readNamedConfigEntry(existing, host.mcp.configKey, serverName);
|
|
164
|
+
return {
|
|
165
|
+
exists: entry !== null,
|
|
166
|
+
url: typeof entry?.url === "string" ? entry.url : null,
|
|
167
|
+
mcpPath,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function inspectInstallSkillHosts({
|
|
172
|
+
hostNames,
|
|
173
|
+
scope,
|
|
174
|
+
serverName,
|
|
175
|
+
serverUrl,
|
|
176
|
+
}) {
|
|
177
|
+
const hostStates = new Map();
|
|
178
|
+
const needsSetupHosts = [];
|
|
179
|
+
const driftHosts = [];
|
|
180
|
+
|
|
181
|
+
for (const hostName of hostNames) {
|
|
182
|
+
// eslint-disable-next-line no-await-in-loop
|
|
183
|
+
const existingState = await readExistingServerState(
|
|
184
|
+
hostName,
|
|
185
|
+
scope,
|
|
186
|
+
serverName,
|
|
187
|
+
);
|
|
188
|
+
if (!existingState.exists) {
|
|
189
|
+
needsSetupHosts.push(hostName);
|
|
190
|
+
hostStates.set(hostName, {
|
|
191
|
+
classification: "needs-setup",
|
|
192
|
+
mcpPath: existingState.mcpPath,
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (existingState.url === serverUrl) {
|
|
198
|
+
hostStates.set(hostName, {
|
|
199
|
+
classification: "matching",
|
|
200
|
+
mcpPath: existingState.mcpPath,
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
driftHosts.push({
|
|
206
|
+
hostName,
|
|
207
|
+
mcpPath: existingState.mcpPath,
|
|
208
|
+
existingUrl: existingState.url ?? "(missing URL)",
|
|
209
|
+
});
|
|
210
|
+
hostStates.set(hostName, {
|
|
211
|
+
classification: "drift",
|
|
212
|
+
mcpPath: existingState.mcpPath,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { hostStates, needsSetupHosts, driftHosts };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatInstallSkillDriftError({
|
|
220
|
+
hostName,
|
|
221
|
+
scope,
|
|
222
|
+
serverName,
|
|
223
|
+
existingUrl,
|
|
224
|
+
requestedUrl,
|
|
225
|
+
}) {
|
|
226
|
+
const host = getHost(hostName);
|
|
227
|
+
const scopeFlag = scope === "project" ? "--scope project" : "--scope global";
|
|
228
|
+
return `Flywheel MCP entry '${serverName}' for ${host.displayName} already points to ${existingUrl}, not ${requestedUrl}. Run 'flywheel uninstall ${scopeFlag} --${hostName} --name ${serverName}' first or choose a different --name.`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getGuidedSkillDecisionOverrideForTests() {
|
|
232
|
+
const testSeamsEnabled =
|
|
233
|
+
String(process.env[ENABLE_TEST_SEAMS_ENV] || "").trim() === "1";
|
|
234
|
+
if (!testSeamsEnabled) {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const normalizedGuidedDecisionOverride = String(
|
|
239
|
+
process.env[GUIDED_SKILL_DECISION_TEST_ENV] || "",
|
|
240
|
+
)
|
|
241
|
+
.trim()
|
|
242
|
+
.toLowerCase();
|
|
243
|
+
if (normalizedGuidedDecisionOverride.length === 0) {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
normalizedGuidedDecisionOverride === "accept" ||
|
|
248
|
+
normalizedGuidedDecisionOverride === "decline" ||
|
|
249
|
+
normalizedGuidedDecisionOverride === "abort"
|
|
250
|
+
) {
|
|
251
|
+
return normalizedGuidedDecisionOverride;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
throw new Error(
|
|
255
|
+
`${GUIDED_SKILL_DECISION_TEST_ENV} must be one of: accept, decline, abort.`,
|
|
256
|
+
);
|
|
136
257
|
}
|
|
137
258
|
|
|
138
259
|
async function promptHosts(scope, detected, serverName) {
|
|
@@ -171,12 +292,23 @@ async function promptHosts(scope, detected, serverName) {
|
|
|
171
292
|
}
|
|
172
293
|
}
|
|
173
294
|
|
|
174
|
-
async function resolveHosts(options, scope, serverName) {
|
|
295
|
+
async function resolveHosts(options, scope, serverName, allowPrompt = true) {
|
|
175
296
|
const explicit = selectedHostsFromOptions(options);
|
|
176
297
|
if (explicit.length > 0) return explicit;
|
|
177
298
|
|
|
178
299
|
const detected = await detectHosts(scope);
|
|
179
|
-
|
|
300
|
+
const promptDisabled =
|
|
301
|
+
options.yes || process.stdout.isTTY !== true || !allowPrompt;
|
|
302
|
+
if (detected.length > 0 && promptDisabled) {
|
|
303
|
+
return detected;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (promptDisabled) {
|
|
307
|
+
log.warn(
|
|
308
|
+
"No hosts were detected for prompt-free setup. Pass explicit host flags such as --codex.",
|
|
309
|
+
);
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
180
312
|
|
|
181
313
|
log.blank();
|
|
182
314
|
const selected = await promptHosts(scope, detected, serverName);
|
|
@@ -188,20 +320,43 @@ async function resolveHosts(options, scope, serverName) {
|
|
|
188
320
|
}
|
|
189
321
|
|
|
190
322
|
async function resolveApiKey(options, baseUrl) {
|
|
191
|
-
if (options
|
|
323
|
+
if (Object.hasOwn(options, "apiKey")) {
|
|
324
|
+
const normalizedApiKey = String(options.apiKey || "").trim();
|
|
325
|
+
if (normalizedApiKey.length === 0) {
|
|
326
|
+
throw new Error("--api-key cannot be empty.");
|
|
327
|
+
}
|
|
328
|
+
return normalizedApiKey;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const requestedAuthMode = normalizeAuthMode(options.authMode || "auto");
|
|
332
|
+
const resolvedAuthMode = resolveSetupAuthMode({
|
|
333
|
+
requestedMode: requestedAuthMode,
|
|
334
|
+
});
|
|
192
335
|
|
|
193
336
|
const spinner = ora("Configuring authentication...").start();
|
|
194
337
|
try {
|
|
195
338
|
const keyName = `flywheel-setup-${randomBytes(3).toString("hex")}`;
|
|
196
|
-
const authResult =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
339
|
+
const authResult =
|
|
340
|
+
resolvedAuthMode === "device"
|
|
341
|
+
? await acquireApiKeyViaDeviceFlow({
|
|
342
|
+
baseUrl,
|
|
343
|
+
keyName,
|
|
344
|
+
verificationBaseUrl: baseUrl,
|
|
345
|
+
})
|
|
346
|
+
: await acquireApiKeyViaBrowserBridge({
|
|
347
|
+
baseUrl,
|
|
348
|
+
keyName,
|
|
349
|
+
});
|
|
350
|
+
spinner.succeed(`Authenticated (${resolvedAuthMode})`);
|
|
201
351
|
return authResult.apiKey;
|
|
202
352
|
} catch (error) {
|
|
203
353
|
spinner.fail("Authentication failed");
|
|
204
|
-
|
|
354
|
+
const message =
|
|
355
|
+
error instanceof Error ? error.message : "Authentication failed.";
|
|
356
|
+
if (resolvedAuthMode === "loopback" && requestedAuthMode === "auto") {
|
|
357
|
+
throw new Error(`${message} Try --auth-mode device on remote shells.`);
|
|
358
|
+
}
|
|
359
|
+
throw new Error(message);
|
|
205
360
|
}
|
|
206
361
|
}
|
|
207
362
|
|
|
@@ -263,31 +418,319 @@ function printSetupResults(results, scope, serverUrl) {
|
|
|
263
418
|
log.blank();
|
|
264
419
|
}
|
|
265
420
|
|
|
421
|
+
async function promptGuidedSkillInstall(
|
|
422
|
+
bundledSkillNames,
|
|
423
|
+
unsupportedHosts,
|
|
424
|
+
guidedDecisionOverride = "",
|
|
425
|
+
) {
|
|
426
|
+
if (guidedDecisionOverride === "accept") {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (
|
|
430
|
+
guidedDecisionOverride === "decline" ||
|
|
431
|
+
guidedDecisionOverride === "abort"
|
|
432
|
+
) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const message = buildGuidedSkillInstallPrompt({
|
|
437
|
+
unsupportedHosts,
|
|
438
|
+
bundledSkillNames,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
return await confirm({
|
|
443
|
+
message,
|
|
444
|
+
default: true,
|
|
445
|
+
theme: CHECKBOX_THEME,
|
|
446
|
+
});
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function buildGuidedSkillInstallPrompt({
|
|
453
|
+
unsupportedHosts,
|
|
454
|
+
bundledSkillNames,
|
|
455
|
+
}) {
|
|
456
|
+
const usesPlural = bundledSkillNames.length > 1;
|
|
457
|
+
if (unsupportedHosts.length > 0) {
|
|
458
|
+
return usesPlural
|
|
459
|
+
? `Also install or refresh the bundled Flywheel skills for supported hosts? Unsupported hosts (${unsupportedHosts.join(", ")}) will remain MCP-only.`
|
|
460
|
+
: `Also install or refresh the bundled Flywheel skill for supported hosts? Unsupported hosts (${unsupportedHosts.join(", ")}) will remain MCP-only.`;
|
|
461
|
+
}
|
|
462
|
+
return usesPlural
|
|
463
|
+
? "Also install or refresh the bundled Flywheel skills?"
|
|
464
|
+
: "Also install or refresh the bundled Flywheel skill?";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function logBundledSkillInstallPlan({
|
|
468
|
+
bundledSkillNames,
|
|
469
|
+
projectRoot,
|
|
470
|
+
scope,
|
|
471
|
+
supportedHosts,
|
|
472
|
+
}) {
|
|
473
|
+
for (const hostName of supportedHosts) {
|
|
474
|
+
const hostDisplayName = getHost(hostName).displayName;
|
|
475
|
+
const resolvedSkillRoot = path.dirname(
|
|
476
|
+
resolveInstalledSkillDir(
|
|
477
|
+
projectRoot,
|
|
478
|
+
hostName,
|
|
479
|
+
scope,
|
|
480
|
+
bundledSkillNames[0],
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
log.plain(
|
|
484
|
+
`Installing bundled skills for ${hostDisplayName} (${scope}: ${resolvedSkillRoot}): ${bundledSkillNames.join(", ")}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function normalizeSkillDecisionMode(options) {
|
|
490
|
+
if (options.installSkill && options.skipSkill) {
|
|
491
|
+
throw new Error(MUTUALLY_EXCLUSIVE_SKILL_FLAGS_ERROR);
|
|
492
|
+
}
|
|
493
|
+
if (options.installSkill) return "install";
|
|
494
|
+
if (options.skipSkill) return "skip";
|
|
495
|
+
return "guided";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function discardInstallSkillCleanupState(cleanupState) {
|
|
499
|
+
if (!cleanupState?.backupRoot) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
await rm(cleanupState.backupRoot, { force: true, recursive: true });
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
266
506
|
async function runSetupCommand(options) {
|
|
507
|
+
const projectRoot = process.cwd();
|
|
508
|
+
const skillDecisionMode = normalizeSkillDecisionMode(options);
|
|
509
|
+
const guidedSkillDecisionOverride = getGuidedSkillDecisionOverrideForTests();
|
|
510
|
+
const nonInteractive = process.stdout.isTTY !== true;
|
|
511
|
+
const hasGuidedDecisionOverride = guidedSkillDecisionOverride.length > 0;
|
|
512
|
+
if (
|
|
513
|
+
nonInteractive &&
|
|
514
|
+
skillDecisionMode === "guided" &&
|
|
515
|
+
!hasGuidedDecisionOverride
|
|
516
|
+
) {
|
|
517
|
+
throw new Error(NON_INTERACTIVE_SKILL_DECISION_ERROR);
|
|
518
|
+
}
|
|
519
|
+
|
|
267
520
|
const scope = options.project ? "project" : "global";
|
|
268
521
|
const baseUrl = normalizeBaseUrl(options.baseUrl || DEFAULT_BASE_URL);
|
|
269
522
|
const serverUrl = `${baseUrl}/mcp-server`;
|
|
270
523
|
const serverName = normalizeServerName(options.name || SERVER_NAME);
|
|
271
|
-
const hosts = await resolveHosts(
|
|
524
|
+
const hosts = await resolveHosts(
|
|
525
|
+
options,
|
|
526
|
+
scope,
|
|
527
|
+
serverName,
|
|
528
|
+
skillDecisionMode === "guided",
|
|
529
|
+
);
|
|
272
530
|
if (hosts.length === 0) return;
|
|
273
531
|
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
532
|
+
const { supportedHosts, unsupportedHosts } =
|
|
533
|
+
partitionInstallSkillHosts(hosts);
|
|
534
|
+
const packageRoot = resolvePackageRoot();
|
|
535
|
+
const bundledSkillNames =
|
|
536
|
+
skillDecisionMode === "skip" || supportedHosts.length === 0
|
|
537
|
+
? []
|
|
538
|
+
: await listBundledSkills(packageRoot);
|
|
539
|
+
|
|
540
|
+
if (skillDecisionMode === "install" && supportedHosts.length === 0) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`--install-skill is not supported for: ${unsupportedHosts.join(", ")}. Supported hosts: ${formatSupportedInstallSkillHosts()}.`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
if (
|
|
546
|
+
skillDecisionMode !== "skip" &&
|
|
547
|
+
supportedHosts.length > 0 &&
|
|
548
|
+
bundledSkillNames.length === 0
|
|
549
|
+
) {
|
|
550
|
+
throw new Error(`No bundled skills found under ${path.join(packageRoot, "skills")}.`);
|
|
278
551
|
}
|
|
279
552
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
553
|
+
const {
|
|
554
|
+
hostStates = new Map(),
|
|
555
|
+
needsSetupHosts = [],
|
|
556
|
+
driftHosts = [],
|
|
557
|
+
} = await inspectInstallSkillHosts({
|
|
558
|
+
hostNames: hosts,
|
|
559
|
+
scope,
|
|
560
|
+
serverName,
|
|
561
|
+
serverUrl,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const installDriftHosts = driftHosts;
|
|
565
|
+
|
|
566
|
+
if (skillDecisionMode === "install" && installDriftHosts.length > 0) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
formatInstallSkillDriftError({
|
|
569
|
+
hostName: installDriftHosts[0].hostName,
|
|
570
|
+
scope,
|
|
571
|
+
serverName,
|
|
572
|
+
existingUrl: installDriftHosts[0].existingUrl,
|
|
573
|
+
requestedUrl: serverUrl,
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let cleanupState = null;
|
|
579
|
+
try {
|
|
580
|
+
if (skillDecisionMode === "install") {
|
|
581
|
+
logBundledSkillInstallPlan({
|
|
582
|
+
bundledSkillNames,
|
|
583
|
+
projectRoot,
|
|
584
|
+
scope,
|
|
585
|
+
supportedHosts,
|
|
586
|
+
});
|
|
587
|
+
cleanupState = await inspectInstalledSkillArtifactsForHosts({
|
|
588
|
+
projectRoot,
|
|
589
|
+
hostNames: supportedHosts,
|
|
590
|
+
scope,
|
|
591
|
+
skillNames: bundledSkillNames,
|
|
592
|
+
});
|
|
593
|
+
await installBundledSkillForHosts({
|
|
594
|
+
projectRoot,
|
|
595
|
+
hostNames: supportedHosts,
|
|
596
|
+
scope,
|
|
597
|
+
serverName,
|
|
598
|
+
serverUrl,
|
|
599
|
+
skillNames: bundledSkillNames,
|
|
600
|
+
});
|
|
601
|
+
if (unsupportedHosts.length > 0) {
|
|
602
|
+
log.plain(
|
|
603
|
+
`--install-skill skipped unsupported hosts (MCP-only): ${unsupportedHosts.join(", ")}.`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const hostsNeedingSetup = needsSetupHosts;
|
|
609
|
+
let apiKey = null;
|
|
610
|
+
if (hostsNeedingSetup.length > 0) {
|
|
611
|
+
apiKey = await resolveApiKey(options, baseUrl);
|
|
612
|
+
if (!apiKey) {
|
|
613
|
+
if (cleanupState) {
|
|
614
|
+
await cleanupInstalledSkillArtifactsForHosts({
|
|
615
|
+
projectRoot,
|
|
616
|
+
hostNames: supportedHosts,
|
|
617
|
+
priorState: cleanupState,
|
|
618
|
+
scope,
|
|
619
|
+
skillNames: bundledSkillNames,
|
|
620
|
+
});
|
|
621
|
+
cleanupState = null;
|
|
622
|
+
}
|
|
623
|
+
log.warn("Setup cancelled");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let setupResults = [];
|
|
629
|
+
const spinner = ora("Setting up Flywheel MCP...").start();
|
|
630
|
+
try {
|
|
631
|
+
const results = [];
|
|
632
|
+
for (const hostName of hosts) {
|
|
633
|
+
if (!needsSetupHosts.includes(hostName)) {
|
|
634
|
+
results.push({
|
|
635
|
+
host: getHost(hostName).displayName,
|
|
636
|
+
status: "already configured",
|
|
637
|
+
filePath: hostStates.get(hostName).mcpPath,
|
|
638
|
+
});
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
spinner.text = `Setting up ${getHost(hostName).displayName}...`;
|
|
643
|
+
// eslint-disable-next-line no-await-in-loop
|
|
644
|
+
results.push(
|
|
645
|
+
await setupHost(hostName, scope, serverUrl, apiKey, serverName),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
spinner.succeed("MCP setup complete");
|
|
649
|
+
setupResults = results;
|
|
650
|
+
} catch (error) {
|
|
651
|
+
spinner.fail("Setup failed");
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (skillDecisionMode !== "guided") {
|
|
656
|
+
const completedCleanupState = cleanupState;
|
|
657
|
+
cleanupState = null;
|
|
658
|
+
await discardInstallSkillCleanupState(completedCleanupState);
|
|
659
|
+
printSetupResults(setupResults, scope, serverUrl);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (supportedHosts.length === 0) {
|
|
664
|
+
log.plain(
|
|
665
|
+
`Bundled skill installation is unavailable for the selected hosts (${hosts.join(", ")}). Continuing with MCP-only setup.`,
|
|
666
|
+
);
|
|
667
|
+
printSetupResults(setupResults, scope, serverUrl);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const shouldInstallSkill = await promptGuidedSkillInstall(
|
|
672
|
+
bundledSkillNames,
|
|
673
|
+
unsupportedHosts,
|
|
674
|
+
guidedSkillDecisionOverride,
|
|
287
675
|
);
|
|
676
|
+
if (!shouldInstallSkill) {
|
|
677
|
+
printSetupResults(setupResults, scope, serverUrl);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (installDriftHosts.length > 0) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
formatInstallSkillDriftError({
|
|
684
|
+
hostName: installDriftHosts[0].hostName,
|
|
685
|
+
scope,
|
|
686
|
+
serverName,
|
|
687
|
+
existingUrl: installDriftHosts[0].existingUrl,
|
|
688
|
+
requestedUrl: serverUrl,
|
|
689
|
+
}),
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
logBundledSkillInstallPlan({
|
|
694
|
+
bundledSkillNames,
|
|
695
|
+
projectRoot,
|
|
696
|
+
scope,
|
|
697
|
+
supportedHosts,
|
|
698
|
+
});
|
|
699
|
+
cleanupState = await inspectInstalledSkillArtifactsForHosts({
|
|
700
|
+
projectRoot,
|
|
701
|
+
hostNames: supportedHosts,
|
|
702
|
+
scope,
|
|
703
|
+
skillNames: bundledSkillNames,
|
|
704
|
+
});
|
|
705
|
+
await installBundledSkillForHosts({
|
|
706
|
+
projectRoot,
|
|
707
|
+
hostNames: supportedHosts,
|
|
708
|
+
scope,
|
|
709
|
+
serverName,
|
|
710
|
+
serverUrl,
|
|
711
|
+
skillNames: bundledSkillNames,
|
|
712
|
+
});
|
|
713
|
+
if (unsupportedHosts.length > 0) {
|
|
714
|
+
log.plain(
|
|
715
|
+
`Guided skill install skipped unsupported hosts (MCP-only): ${unsupportedHosts.join(", ")}.`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const completedCleanupState = cleanupState;
|
|
719
|
+
cleanupState = null;
|
|
720
|
+
await discardInstallSkillCleanupState(completedCleanupState);
|
|
721
|
+
printSetupResults(setupResults, scope, serverUrl);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (cleanupState) {
|
|
724
|
+
await cleanupInstalledSkillArtifactsForHosts({
|
|
725
|
+
projectRoot,
|
|
726
|
+
hostNames: supportedHosts,
|
|
727
|
+
priorState: cleanupState,
|
|
728
|
+
scope,
|
|
729
|
+
skillNames: bundledSkillNames,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
288
733
|
}
|
|
289
|
-
spinner.succeed("Setup complete");
|
|
290
|
-
printSetupResults(results, scope, serverUrl);
|
|
291
734
|
}
|
|
292
735
|
|
|
293
736
|
function parseUninstallScope(value) {
|
|
@@ -496,11 +939,14 @@ function buildProgram() {
|
|
|
496
939
|
"after",
|
|
497
940
|
`
|
|
498
941
|
Examples:
|
|
499
|
-
${pc.green("npx @paradigma-inc/flywheel setup")}
|
|
500
|
-
${pc.green("npx @paradigma-inc/flywheel setup --
|
|
501
|
-
${pc.green("npx @paradigma-inc/flywheel setup --
|
|
942
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup")}
|
|
943
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup --install-skill")}
|
|
944
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup --skip-skill")}
|
|
945
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup --auth-mode device --codex --project")}
|
|
946
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup --codex --project")}
|
|
947
|
+
${pc.green("npx --yes @paradigma-inc/flywheel setup --yes --codex --claude --cursor")}
|
|
502
948
|
${pc.green(
|
|
503
|
-
"npx @paradigma-inc/flywheel uninstall --scope all --hosts codex,claude,opencode,cursor,pi-mono,hermes-agent,openclaw",
|
|
949
|
+
"npx --yes @paradigma-inc/flywheel uninstall --scope all --hosts codex,claude,opencode,cursor,pi-mono,hermes-agent,openclaw",
|
|
504
950
|
)}
|
|
505
951
|
`,
|
|
506
952
|
);
|
|
@@ -521,6 +967,14 @@ Examples:
|
|
|
521
967
|
)
|
|
522
968
|
.option("-y, --yes", "Skip host selection prompts")
|
|
523
969
|
.option("--api-key <key>", "Use API key authentication")
|
|
970
|
+
.option(
|
|
971
|
+
"--auth-mode <mode>",
|
|
972
|
+
"Authentication mode: auto | loopback | device (default: auto)",
|
|
973
|
+
normalizeAuthMode,
|
|
974
|
+
"auto",
|
|
975
|
+
)
|
|
976
|
+
.option("--install-skill", "Install or refresh the bundled Flywheel skills")
|
|
977
|
+
.option("--skip-skill", "Skip bundled skill installation (MCP-only setup)")
|
|
524
978
|
.option(
|
|
525
979
|
"--base-url <url>",
|
|
526
980
|
`Public Flywheel origin used for setup and MCP config (default: ${DEFAULT_BASE_URL})`,
|