@paradigma-inc/flywheel 0.1.93 → 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.
Files changed (34) hide show
  1. package/README.md +22 -4
  2. package/package.json +2 -1
  3. package/skills/flywheel-llm-proof-paper/SKILL.md +156 -0
  4. package/skills/flywheel-llm-proof-paper/agents/openai.yaml +4 -0
  5. package/skills/flywheel-llm-proof-paper/references/checks.md +60 -0
  6. package/skills/flywheel-llm-proof-paper/scripts/__pycache__/check_paper.cpython-311.pyc +0 -0
  7. package/skills/flywheel-llm-proof-paper/scripts/check_paper.py +1065 -0
  8. package/src/agents.mjs +5 -2
  9. package/src/cli.mjs +27 -3
  10. package/src/public-command-metadata.mjs +44 -0
  11. package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.d.ts +21 -0
  12. package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.js +68 -2
  13. package/src/runtime/vendor/flywheel-cli-dist/commands/_wait-polling.js.map +1 -1
  14. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-acquire.js +2 -8
  15. package/src/runtime/vendor/flywheel-cli-dist/commands/compute-acquire.js.map +1 -1
  16. package/src/runtime/vendor/flywheel-cli-dist/commands/feedback-create.js +4 -0
  17. package/src/runtime/vendor/flywheel-cli-dist/commands/feedback-create.js.map +1 -1
  18. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/compute.js +1 -1
  19. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/compute.js.map +1 -1
  20. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js +16 -0
  21. package/src/runtime/vendor/flywheel-cli-dist/commands/registry/resources.js.map +1 -1
  22. package/src/runtime/vendor/flywheel-cli-dist/generatedProductTelemetryContract.d.ts +1 -1
  23. package/src/runtime/vendor/flywheel-cli-dist/generatedProductTelemetryContract.js +3 -3
  24. package/src/runtime/vendor/flywheel-cli-dist/generatedProductTelemetryContract.js.map +1 -1
  25. package/src/runtime/vendor/manifest.json +1 -1
  26. package/src/setup/shared/prior-mode-detect.mjs +76 -9
  27. package/src/unified-cli.mjs +54 -16
  28. package/src/update/cache.mjs +124 -0
  29. package/src/update/command.mjs +438 -0
  30. package/src/update/install-state.mjs +135 -0
  31. package/src/update/refresh.mjs +393 -0
  32. package/src/update/registry.mjs +35 -0
  33. package/src/update/version.mjs +59 -0
  34. package/src/update/warning.mjs +109 -0
@@ -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
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ readFreshUpdateCache,
3
+ resolveUpdateCheckCachePath,
4
+ writeUpdateCacheAtomic,
5
+ } from "./cache.mjs";
6
+ import { fetchLatestVersionFromRegistry } from "./registry.mjs";
7
+ import {
8
+ compareInstalledToLatest,
9
+ FLYWHEEL_PACKAGE_NAME,
10
+ isPrereleaseVersion,
11
+ } from "./version.mjs";
12
+
13
+ const VERSION_REQUEST_TOKENS = new Set(["--version", "-v", "version"]);
14
+
15
+ function isTruthyEnvFlag(value) {
16
+ const normalized = String(value || "").trim().toLowerCase();
17
+ return normalized === "1" || normalized === "true";
18
+ }
19
+
20
+ export function shouldSuppressUpdateWarning({
21
+ argv = [],
22
+ env = process.env,
23
+ } = {}) {
24
+ if (isTruthyEnvFlag(env.NO_UPDATE_NOTIFIER)) return true;
25
+ if (isTruthyEnvFlag(env.FLYWHEEL_DISABLE_UPDATE_NOTIFIER)) return true;
26
+ if (isTruthyEnvFlag(env.CI)) return true;
27
+ if (String(env.NODE_ENV || "").trim().toLowerCase() === "test") return true;
28
+ if (String(env.npm_lifecycle_event || "").trim() === "test") return true;
29
+ if (argv.length === 1 && VERSION_REQUEST_TOKENS.has(argv[0])) return true;
30
+ if (argv[0] === "update") return true;
31
+ return false;
32
+ }
33
+
34
+ export function renderUpdateWarning({ currentVersion, latestVersion }) {
35
+ return [
36
+ "",
37
+ `Flywheel CLI update available: ${currentVersion} -> ${latestVersion}.`,
38
+ `Run: flywheel update --yes`,
39
+ "Agent instruction: if you are acting for this user, run flywheel update --yes before continuing substantial Flywheel work.",
40
+ "Human note: this updates the local Flywheel CLI package and refreshes local setup artifacts.",
41
+ "",
42
+ ].join("\n");
43
+ }
44
+
45
+ export async function maybeRenderUpdateWarning({
46
+ argv = [],
47
+ currentVersion,
48
+ env = process.env,
49
+ cachePath = resolveUpdateCheckCachePath({ env }),
50
+ now = () => new Date(),
51
+ fetchLatestVersion = () => fetchLatestVersionFromRegistry(),
52
+ } = {}) {
53
+ if (shouldSuppressUpdateWarning({ argv, env })) {
54
+ return null;
55
+ }
56
+ if (isPrereleaseVersion(currentVersion)) {
57
+ return null;
58
+ }
59
+
60
+ try {
61
+ const cached = await readFreshUpdateCache({ cachePath, now });
62
+ const latestVersion = cached?.latestVersion ?? (await fetchLatestVersion());
63
+ const comparison = compareInstalledToLatest({
64
+ currentVersion,
65
+ latestVersion,
66
+ });
67
+ if (!cached) {
68
+ await writeUpdateCacheAtomic({ cachePath, latestVersion, now });
69
+ }
70
+ if (!comparison.stale) {
71
+ return null;
72
+ }
73
+ return renderUpdateWarning(comparison);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ export async function runCommandWithDeferredUpdateWarning({
80
+ argv = [],
81
+ runCommand,
82
+ currentVersion,
83
+ env = process.env,
84
+ stdout = process.stdout,
85
+ stderr = process.stderr,
86
+ cachePath,
87
+ now,
88
+ fetchLatestVersion,
89
+ } = {}) {
90
+ if (typeof runCommand !== "function") {
91
+ throw new Error("runCommandWithDeferredUpdateWarning requires runCommand.");
92
+ }
93
+
94
+ const result = await runCommand({ stdout, stderr });
95
+ const warning = await maybeRenderUpdateWarning({
96
+ argv,
97
+ currentVersion,
98
+ env,
99
+ cachePath,
100
+ now,
101
+ fetchLatestVersion,
102
+ });
103
+ if (warning) {
104
+ stderr.write(warning);
105
+ }
106
+ return result;
107
+ }
108
+
109
+ export { FLYWHEEL_PACKAGE_NAME };