@paradigma-inc/flywheel 0.1.95 → 0.1.99
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 +22 -4
- package/package.json +2 -1
- package/skills/flywheel-llm-proof-paper/SKILL.md +156 -0
- package/skills/flywheel-llm-proof-paper/agents/openai.yaml +4 -0
- package/skills/flywheel-llm-proof-paper/references/checks.md +60 -0
- package/skills/flywheel-llm-proof-paper/scripts/__pycache__/check_paper.cpython-311.pyc +0 -0
- package/skills/flywheel-llm-proof-paper/scripts/check_paper.py +1065 -0
- package/src/cli.mjs +27 -3
- package/src/public-command-metadata.mjs +44 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.d.ts +21 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.js +68 -2
- package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-acquire.js +2 -8
- package/src/runtime/vendor/flywheel-cli-dist/commands/compute-acquire.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/feedback-create.js +4 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/feedback-create.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/compute.js +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/compute.js.map +1 -1
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js +16 -0
- package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js.map +1 -1
- package/src/runtime/vendor/manifest.json +1 -1
- package/src/setup/shared/prior-mode-detect.mjs +76 -9
- package/src/unified-cli.mjs +54 -16
- package/src/update/cache.mjs +124 -0
- package/src/update/command.mjs +438 -0
- package/src/update/install-state.mjs +135 -0
- package/src/update/refresh.mjs +393 -0
- package/src/update/registry.mjs +35 -0
- package/src/update/version.mjs +59 -0
- package/src/update/warning.mjs +109 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ALL_HOST_NAMES,
|
|
5
|
+
SERVER_NAME,
|
|
6
|
+
getHost as defaultGetHost,
|
|
7
|
+
isSkillOnlyHost,
|
|
8
|
+
} from "../agents.mjs";
|
|
9
|
+
import { resolveRuntimeConfigPath } from "../runtime/runtime-config.mjs";
|
|
10
|
+
import {
|
|
11
|
+
resolveFlywheelInstalledMarkerPath,
|
|
12
|
+
SUPPORTED_INSTALL_SKILL_HOSTS,
|
|
13
|
+
} from "../skill-installer.mjs";
|
|
14
|
+
import { findFlywheelMcpEntries as defaultFindFlywheelMcpEntries } from "../setup/shared/prior-mode-detect.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SERVER_URL = "https://flywheel.paradigma.inc/mcp-server";
|
|
17
|
+
|
|
18
|
+
async function pathExists(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
await access(filePath);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function baseUrlFromServerUrl(serverUrl) {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(serverUrl).origin;
|
|
30
|
+
} catch {
|
|
31
|
+
return "https://flywheel.paradigma.inc";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function originOf(url) {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(url).origin.toLowerCase();
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sortArtifacts(artifacts) {
|
|
44
|
+
return artifacts.sort((left, right) => {
|
|
45
|
+
const byHost = left.hostName.localeCompare(right.hostName);
|
|
46
|
+
if (byHost !== 0) return byHost;
|
|
47
|
+
const byScope = left.scope.localeCompare(right.scope);
|
|
48
|
+
if (byScope !== 0) return byScope;
|
|
49
|
+
return String(left.serverName || "").localeCompare(
|
|
50
|
+
String(right.serverName || ""),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function discoverInstalledFlywheelState({
|
|
56
|
+
cwd = process.cwd(),
|
|
57
|
+
env = process.env,
|
|
58
|
+
hostNames = ALL_HOST_NAMES,
|
|
59
|
+
scopes = ["project", "global"],
|
|
60
|
+
serverUrl = DEFAULT_SERVER_URL,
|
|
61
|
+
getHost = defaultGetHost,
|
|
62
|
+
findFlywheelMcpEntries = defaultFindFlywheelMcpEntries,
|
|
63
|
+
markerPathForHost = resolveFlywheelInstalledMarkerPath,
|
|
64
|
+
runtimeConfigPath = resolveRuntimeConfigPath({ env }),
|
|
65
|
+
pathExistsFn = pathExists,
|
|
66
|
+
} = {}) {
|
|
67
|
+
const targetOrigin = originOf(serverUrl);
|
|
68
|
+
const mcpArtifacts = [];
|
|
69
|
+
const cliArtifacts = [];
|
|
70
|
+
const seenCli = new Set();
|
|
71
|
+
const runtimeConfigExists = await pathExistsFn(runtimeConfigPath);
|
|
72
|
+
|
|
73
|
+
for (const hostName of hostNames) {
|
|
74
|
+
let host;
|
|
75
|
+
try {
|
|
76
|
+
host = getHost(hostName);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const scope of scopes) {
|
|
82
|
+
if (!isSkillOnlyHost(host)) {
|
|
83
|
+
// eslint-disable-next-line no-await-in-loop
|
|
84
|
+
const entries = await findFlywheelMcpEntries({
|
|
85
|
+
hostName,
|
|
86
|
+
scope,
|
|
87
|
+
cwd,
|
|
88
|
+
targetOrigin,
|
|
89
|
+
trustedServerNames: [SERVER_NAME],
|
|
90
|
+
getHost,
|
|
91
|
+
});
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
mcpArtifacts.push({
|
|
94
|
+
hostName,
|
|
95
|
+
scope,
|
|
96
|
+
serverName: entry.serverName,
|
|
97
|
+
serverUrl: entry.serverUrl,
|
|
98
|
+
baseUrl: baseUrlFromServerUrl(entry.serverUrl),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!SUPPORTED_INSTALL_SKILL_HOSTS.includes(hostName)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
let markerPath;
|
|
107
|
+
try {
|
|
108
|
+
markerPath = markerPathForHost(cwd, hostName, scope);
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// eslint-disable-next-line no-await-in-loop
|
|
113
|
+
const markerExists = await pathExistsFn(markerPath);
|
|
114
|
+
if (!markerExists) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const key = `${hostName}\0${scope}`;
|
|
118
|
+
if (seenCli.has(key)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
seenCli.add(key);
|
|
122
|
+
cliArtifacts.push({
|
|
123
|
+
hostName,
|
|
124
|
+
scope,
|
|
125
|
+
runtimeConfigExists,
|
|
126
|
+
markerPath,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
mcpArtifacts: sortArtifacts(mcpArtifacts),
|
|
133
|
+
cliArtifacts: sortArtifacts(cliArtifacts),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { load as loadYaml } from "js-yaml";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ALL_HOST_NAMES,
|
|
8
|
+
SERVER_NAME,
|
|
9
|
+
getHost,
|
|
10
|
+
isSkillOnlyHost,
|
|
11
|
+
} from "../agents.mjs";
|
|
12
|
+
import {
|
|
13
|
+
installBundledSkillForHosts as defaultInstallBundledSkillForHosts,
|
|
14
|
+
listBundledSkills as defaultListBundledSkills,
|
|
15
|
+
resolveInstalledSkillDir,
|
|
16
|
+
resolvePackageRoot,
|
|
17
|
+
SUPPORTED_INSTALL_SKILL_HOSTS,
|
|
18
|
+
} from "../skill-installer.mjs";
|
|
19
|
+
import { runMcpModeCommand as defaultRunMcpModeCommand } from "../setup/modes/mcp-command.mjs";
|
|
20
|
+
import { resolveFlywheelBinaryOnPath as defaultResolveFlywheelBinaryOnPath } from "../setup/modes/cli.mjs";
|
|
21
|
+
import { resolveSetupBaseUrl } from "../setup/base-url.mjs";
|
|
22
|
+
import { discoverInstalledFlywheelState } from "./install-state.mjs";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SERVER_URL = "https://flywheel.paradigma.inc/mcp-server";
|
|
25
|
+
const INSTALL_NEXT_STEP =
|
|
26
|
+
"curl -fsSL https://flywheel.paradigma.inc/install | sh";
|
|
27
|
+
const OPENAI_AGENT_METADATA_RELATIVE_PATH = path.join("agents", "openai.yaml");
|
|
28
|
+
|
|
29
|
+
function sortByHostScope(values) {
|
|
30
|
+
return [...values].sort((left, right) => {
|
|
31
|
+
const byHost = left.hostName.localeCompare(right.hostName);
|
|
32
|
+
if (byHost !== 0) return byHost;
|
|
33
|
+
return left.scope.localeCompare(right.scope);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeBaseUrl(value) {
|
|
38
|
+
const normalized = String(value || "").trim();
|
|
39
|
+
let parsedUrl;
|
|
40
|
+
try {
|
|
41
|
+
parsedUrl = new URL(normalized);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error("Base URL must be a valid absolute URL.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parsedUrl.pathname !== "/" || parsedUrl.search || parsedUrl.hash) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Base URL must be a public Flywheel origin without a path, query, or hash.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return parsedUrl.origin;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveDiscoveryServerUrl({ env }) {
|
|
56
|
+
return `${normalizeBaseUrl(resolveSetupBaseUrl({ env }))}/mcp-server`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readMcpTargetFromOpenAiMetadata(metadataRaw) {
|
|
60
|
+
const metadata = loadYaml(metadataRaw);
|
|
61
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const dependencies = metadata.dependencies;
|
|
65
|
+
if (
|
|
66
|
+
!dependencies ||
|
|
67
|
+
typeof dependencies !== "object" ||
|
|
68
|
+
Array.isArray(dependencies)
|
|
69
|
+
) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const tools = dependencies.tools;
|
|
73
|
+
if (!Array.isArray(tools)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
for (const tool of tools) {
|
|
77
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (tool.type !== "mcp") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const serverName =
|
|
84
|
+
typeof tool.value === "string" ? tool.value.trim() : "";
|
|
85
|
+
const serverUrl = typeof tool.url === "string" ? tool.url.trim() : "";
|
|
86
|
+
if (!serverName && !serverUrl) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
serverName: serverName || SERVER_NAME,
|
|
91
|
+
serverUrl: serverUrl || DEFAULT_SERVER_URL,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function readInstalledSkillMcpTarget({
|
|
98
|
+
artifact,
|
|
99
|
+
cwd,
|
|
100
|
+
skillNames,
|
|
101
|
+
}) {
|
|
102
|
+
for (const skillName of skillNames) {
|
|
103
|
+
const metadataPath = path.join(
|
|
104
|
+
resolveInstalledSkillDir(
|
|
105
|
+
cwd,
|
|
106
|
+
artifact.hostName,
|
|
107
|
+
artifact.scope,
|
|
108
|
+
skillName,
|
|
109
|
+
),
|
|
110
|
+
OPENAI_AGENT_METADATA_RELATIVE_PATH,
|
|
111
|
+
);
|
|
112
|
+
let metadataRaw;
|
|
113
|
+
try {
|
|
114
|
+
// eslint-disable-next-line no-await-in-loop
|
|
115
|
+
metadataRaw = await readFile(metadataPath, "utf8");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (["ENOENT", "ENOTDIR"].includes(error?.code)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
throw errorWithStep(
|
|
121
|
+
`cli-skill-target:${artifact.hostName}:${artifact.scope}`,
|
|
122
|
+
`Unable to read installed CLI skill MCP target at ${metadataPath}.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const target = readMcpTargetFromOpenAiMetadata(metadataRaw);
|
|
127
|
+
if (target) {
|
|
128
|
+
return target;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw errorWithStep(
|
|
133
|
+
`cli-skill-target:${artifact.hostName}:${artifact.scope}`,
|
|
134
|
+
`Unable to parse installed CLI skill MCP target at ${metadataPath}: ${message}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function resolveCliArtifactMcpTarget({
|
|
142
|
+
artifact,
|
|
143
|
+
cwd,
|
|
144
|
+
skillNames,
|
|
145
|
+
}) {
|
|
146
|
+
const target = await readInstalledSkillMcpTarget({
|
|
147
|
+
artifact,
|
|
148
|
+
cwd,
|
|
149
|
+
skillNames,
|
|
150
|
+
});
|
|
151
|
+
if (target) {
|
|
152
|
+
return target;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
serverName: SERVER_NAME,
|
|
156
|
+
serverUrl: DEFAULT_SERVER_URL,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hostScopeKey(artifact) {
|
|
161
|
+
return `${artifact.hostName}\0${artifact.scope}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function resolveMcpSkillTargets({ mcpArtifacts, cwd, skillNames }) {
|
|
165
|
+
const targets = new Map();
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
for (const artifact of mcpArtifacts) {
|
|
168
|
+
if (!SUPPORTED_INSTALL_SKILL_HOSTS.includes(artifact.hostName)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const key = hostScopeKey(artifact);
|
|
172
|
+
if (seen.has(key)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
seen.add(key);
|
|
176
|
+
// eslint-disable-next-line no-await-in-loop
|
|
177
|
+
const target = await readInstalledSkillMcpTarget({
|
|
178
|
+
artifact,
|
|
179
|
+
cwd,
|
|
180
|
+
skillNames,
|
|
181
|
+
});
|
|
182
|
+
if (target) {
|
|
183
|
+
targets.set(key, target);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return targets;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mcpSkillTargetMatchesArtifact(target, artifact) {
|
|
190
|
+
return (
|
|
191
|
+
target?.serverName === artifact.serverName &&
|
|
192
|
+
target?.serverUrl === artifact.serverUrl
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function groupCliArtifactsByScopeAndTarget({
|
|
197
|
+
cliArtifacts,
|
|
198
|
+
cwd,
|
|
199
|
+
skillNames,
|
|
200
|
+
}) {
|
|
201
|
+
const groups = new Map();
|
|
202
|
+
for (const artifact of cliArtifacts) {
|
|
203
|
+
// eslint-disable-next-line no-await-in-loop
|
|
204
|
+
const target = await resolveCliArtifactMcpTarget({
|
|
205
|
+
artifact,
|
|
206
|
+
cwd,
|
|
207
|
+
skillNames,
|
|
208
|
+
});
|
|
209
|
+
const key = JSON.stringify([
|
|
210
|
+
artifact.scope,
|
|
211
|
+
target.serverName,
|
|
212
|
+
target.serverUrl,
|
|
213
|
+
]);
|
|
214
|
+
if (!groups.has(key)) {
|
|
215
|
+
groups.set(key, {
|
|
216
|
+
scope: artifact.scope,
|
|
217
|
+
hostNames: [],
|
|
218
|
+
serverName: target.serverName,
|
|
219
|
+
serverUrl: target.serverUrl,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
groups.get(key).hostNames.push(artifact.hostName);
|
|
223
|
+
}
|
|
224
|
+
return [...groups.values()].map((group) => ({
|
|
225
|
+
...group,
|
|
226
|
+
hostNames: [...new Set(group.hostNames)].sort(),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function errorWithStep(stepName, message) {
|
|
231
|
+
const error = new Error(message);
|
|
232
|
+
error.stepName = stepName;
|
|
233
|
+
return error;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function cliArtifactsRequireBinaryPath(cliArtifacts) {
|
|
237
|
+
return cliArtifacts.some((artifact) => {
|
|
238
|
+
try {
|
|
239
|
+
return !isSkillOnlyHost(getHost(artifact.hostName));
|
|
240
|
+
} catch {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function renderRefreshPlan({
|
|
247
|
+
state,
|
|
248
|
+
prefix,
|
|
249
|
+
packageInstallCommand = `npm install -g --prefix ${prefix} @paradigma-inc/flywheel@latest`,
|
|
250
|
+
} = {}) {
|
|
251
|
+
const lines = [`Package update: ${packageInstallCommand}`];
|
|
252
|
+
const mcpArtifacts = sortByHostScope(state?.mcpArtifacts || []);
|
|
253
|
+
const cliArtifacts = sortByHostScope(state?.cliArtifacts || []);
|
|
254
|
+
|
|
255
|
+
for (const artifact of mcpArtifacts) {
|
|
256
|
+
lines.push(
|
|
257
|
+
`MCP refresh: ${artifact.hostName} ${artifact.scope} ${artifact.serverName} (${artifact.serverUrl})`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
for (const artifact of cliArtifacts) {
|
|
261
|
+
lines.push(`CLI skill refresh: ${artifact.hostName} ${artifact.scope}`);
|
|
262
|
+
}
|
|
263
|
+
if (mcpArtifacts.length === 0 && cliArtifacts.length === 0) {
|
|
264
|
+
lines.push(
|
|
265
|
+
`No local setup artifacts detected. Next step: ${INSTALL_NEXT_STEP}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return `${lines.join("\n")}\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function refreshExistingSetup({
|
|
272
|
+
cwd = process.cwd(),
|
|
273
|
+
env = process.env,
|
|
274
|
+
hostNames = ALL_HOST_NAMES,
|
|
275
|
+
scopes = ["project", "global"],
|
|
276
|
+
stdout = process.stdout,
|
|
277
|
+
stderr = process.stderr,
|
|
278
|
+
discoverInstalledState = discoverInstalledFlywheelState,
|
|
279
|
+
runMcpModeCommand = defaultRunMcpModeCommand,
|
|
280
|
+
installBundledSkillForHosts = defaultInstallBundledSkillForHosts,
|
|
281
|
+
listBundledSkills = defaultListBundledSkills,
|
|
282
|
+
resolveFlywheelBinaryOnPath = defaultResolveFlywheelBinaryOnPath,
|
|
283
|
+
} = {}) {
|
|
284
|
+
const state = await discoverInstalledState({
|
|
285
|
+
cwd,
|
|
286
|
+
env,
|
|
287
|
+
hostNames,
|
|
288
|
+
scopes,
|
|
289
|
+
serverUrl: resolveDiscoveryServerUrl({ env }),
|
|
290
|
+
});
|
|
291
|
+
const mcpArtifacts = sortByHostScope(state.mcpArtifacts || []);
|
|
292
|
+
const cliArtifacts = sortByHostScope(state.cliArtifacts || []);
|
|
293
|
+
|
|
294
|
+
if (mcpArtifacts.length === 0 && cliArtifacts.length === 0) {
|
|
295
|
+
stdout.write(
|
|
296
|
+
`No existing Flywheel setup artifacts were detected to refresh.\nNext step: ${INSTALL_NEXT_STEP}\n`,
|
|
297
|
+
);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const needsSkillNames =
|
|
302
|
+
cliArtifacts.length > 0 ||
|
|
303
|
+
mcpArtifacts.some((artifact) =>
|
|
304
|
+
SUPPORTED_INSTALL_SKILL_HOSTS.includes(artifact.hostName),
|
|
305
|
+
);
|
|
306
|
+
const skillNames = needsSkillNames
|
|
307
|
+
? await listBundledSkills(resolvePackageRoot())
|
|
308
|
+
: [];
|
|
309
|
+
let mcpSkillTargets = new Map();
|
|
310
|
+
if (skillNames.length > 0) {
|
|
311
|
+
mcpSkillTargets = await resolveMcpSkillTargets({
|
|
312
|
+
mcpArtifacts,
|
|
313
|
+
cwd,
|
|
314
|
+
skillNames,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const artifact of mcpArtifacts) {
|
|
319
|
+
const supportsSkillInstall = SUPPORTED_INSTALL_SKILL_HOSTS.includes(
|
|
320
|
+
artifact.hostName,
|
|
321
|
+
);
|
|
322
|
+
const shouldInstallSkill =
|
|
323
|
+
supportsSkillInstall &&
|
|
324
|
+
mcpSkillTargetMatchesArtifact(
|
|
325
|
+
mcpSkillTargets.get(hostScopeKey(artifact)),
|
|
326
|
+
artifact,
|
|
327
|
+
);
|
|
328
|
+
const options = {
|
|
329
|
+
[artifact.hostName]: true,
|
|
330
|
+
project: artifact.scope === "project",
|
|
331
|
+
name: artifact.serverName,
|
|
332
|
+
baseUrl: artifact.baseUrl,
|
|
333
|
+
yes: true,
|
|
334
|
+
...(shouldInstallSkill
|
|
335
|
+
? { installSkill: true }
|
|
336
|
+
: {
|
|
337
|
+
skipSkill: true,
|
|
338
|
+
...(!supportsSkillInstall
|
|
339
|
+
? { allowUnsupportedSkillInstall: true }
|
|
340
|
+
: {}),
|
|
341
|
+
}),
|
|
342
|
+
};
|
|
343
|
+
// Validate host names before dispatching so failures name the refresh step
|
|
344
|
+
// instead of surfacing as a generic object-shape error downstream.
|
|
345
|
+
getHost(artifact.hostName);
|
|
346
|
+
// eslint-disable-next-line no-await-in-loop
|
|
347
|
+
const result = await runMcpModeCommand(options, { force: true });
|
|
348
|
+
const exitCode = Number(result?.exitCode || 0);
|
|
349
|
+
if (exitCode !== 0) {
|
|
350
|
+
throw errorWithStep(
|
|
351
|
+
`mcp:${artifact.hostName}:${artifact.scope}:${artifact.serverName}`,
|
|
352
|
+
`MCP refresh failed for ${artifact.hostName} ${artifact.scope} ${artifact.serverName}.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (cliArtifacts.length > 0) {
|
|
358
|
+
if (skillNames.length === 0) {
|
|
359
|
+
throw errorWithStep("cli-skills", "No bundled Flywheel skills found.");
|
|
360
|
+
}
|
|
361
|
+
const cliGroups = await groupCliArtifactsByScopeAndTarget({
|
|
362
|
+
cliArtifacts,
|
|
363
|
+
cwd,
|
|
364
|
+
skillNames,
|
|
365
|
+
});
|
|
366
|
+
for (const group of cliGroups) {
|
|
367
|
+
// eslint-disable-next-line no-await-in-loop
|
|
368
|
+
await installBundledSkillForHosts({
|
|
369
|
+
projectRoot: cwd,
|
|
370
|
+
hostNames: group.hostNames,
|
|
371
|
+
scope: group.scope,
|
|
372
|
+
serverName: group.serverName,
|
|
373
|
+
serverUrl: group.serverUrl,
|
|
374
|
+
skillNames,
|
|
375
|
+
cliMode: true,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (cliArtifactsRequireBinaryPath(cliArtifacts)) {
|
|
379
|
+
const binaryPath = await resolveFlywheelBinaryOnPath({ env });
|
|
380
|
+
if (!binaryPath) {
|
|
381
|
+
throw errorWithStep(
|
|
382
|
+
"cli-binary-path",
|
|
383
|
+
"flywheel binary is not on PATH after CLI skill refresh.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
stderr.write(`Verified flywheel binary on PATH at ${binaryPath}.\n`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export { INSTALL_NEXT_STEP };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const FLYWHEEL_REGISTRY_URL =
|
|
2
|
+
"https://registry.npmjs.org/@paradigma-inc%2Fflywheel";
|
|
3
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 1000;
|
|
4
|
+
|
|
5
|
+
export async function fetchLatestVersionFromRegistry({
|
|
6
|
+
fetchImpl = globalThis.fetch,
|
|
7
|
+
registryUrl = FLYWHEEL_REGISTRY_URL,
|
|
8
|
+
timeoutMs = UPDATE_CHECK_TIMEOUT_MS,
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (typeof fetchImpl !== "function") {
|
|
11
|
+
throw new Error("Unable to check latest @paradigma-inc/flywheel version.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetchImpl(registryUrl, {
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
headers: { accept: "application/json" },
|
|
20
|
+
});
|
|
21
|
+
if (!response || response.ok !== true) {
|
|
22
|
+
throw new Error("registry request failed");
|
|
23
|
+
}
|
|
24
|
+
const payload = await response.json();
|
|
25
|
+
const latest = payload?.["dist-tags"]?.latest;
|
|
26
|
+
if (typeof latest !== "string" || latest.trim().length === 0) {
|
|
27
|
+
throw new Error("registry latest dist-tag missing");
|
|
28
|
+
}
|
|
29
|
+
return latest.trim();
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("Unable to check latest @paradigma-inc/flywheel version.");
|
|
32
|
+
} finally {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
|
|
3
|
+
export const FLYWHEEL_PACKAGE_NAME = "@paradigma-inc/flywheel";
|
|
4
|
+
|
|
5
|
+
export function normalizeVersion(value) {
|
|
6
|
+
const cleaned = semver.clean(String(value ?? "").trim());
|
|
7
|
+
if (!cleaned || !semver.valid(cleaned)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return cleaned;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isPrereleaseVersion(value) {
|
|
14
|
+
const normalized = normalizeVersion(value);
|
|
15
|
+
return normalized ? semver.prerelease(normalized) !== null : false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function compareInstalledToLatest({ currentVersion, latestVersion }) {
|
|
19
|
+
const current = normalizeVersion(currentVersion);
|
|
20
|
+
const latest = normalizeVersion(latestVersion);
|
|
21
|
+
if (!current || !latest) {
|
|
22
|
+
throw new Error(`Invalid ${FLYWHEEL_PACKAGE_NAME} version metadata.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (semver.gt(current, latest)) {
|
|
26
|
+
return {
|
|
27
|
+
currentVersion: current,
|
|
28
|
+
latestVersion: latest,
|
|
29
|
+
status: "installed version is newer than npm latest",
|
|
30
|
+
stale: false,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (semver.gt(latest, current)) {
|
|
35
|
+
const latestIsPrerelease = semver.prerelease(latest) !== null;
|
|
36
|
+
const currentIsPrerelease = semver.prerelease(current) !== null;
|
|
37
|
+
if (latestIsPrerelease && !currentIsPrerelease) {
|
|
38
|
+
return {
|
|
39
|
+
currentVersion: current,
|
|
40
|
+
latestVersion: latest,
|
|
41
|
+
status: "current",
|
|
42
|
+
stale: false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
currentVersion: current,
|
|
47
|
+
latestVersion: latest,
|
|
48
|
+
status: "update available",
|
|
49
|
+
stale: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
currentVersion: current,
|
|
55
|
+
latestVersion: latest,
|
|
56
|
+
status: "current",
|
|
57
|
+
stale: false,
|
|
58
|
+
};
|
|
59
|
+
}
|