fm-bench 0.6.2 → 0.6.3
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 +4 -2
- package/docs/releasing.md +2 -1
- package/docs/supported-platforms.md +26 -0
- package/package.json +8 -4
- package/src/cli.js +43 -12
- package/src/macos.js +71 -0
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ fm-bench
|
|
|
30
30
|
One command discovers your models, runs the standard prompt suite, and prints a full benchmark report:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
|
-
fm-bench 0.6.
|
|
33
|
+
fm-bench 0.6.3 | darwin/arm64 | fm
|
|
34
34
|
prompts 3 | runs 5 | concurrency 1 | stream on | measured 15 | failed 0 | skipped 0 | elapsed 38.20s
|
|
35
35
|
|
|
36
36
|
┌───┬────────┬────────┬─────────┬──────┬──────┬──────────┬──────┬──────────┬─────┬─────┐
|
|
@@ -273,10 +273,12 @@ A live single-line progress indicator runs on stderr during interactive sessions
|
|
|
273
273
|
|
|
274
274
|
## Requirements
|
|
275
275
|
|
|
276
|
-
- macOS 27 or newer (Apple's `fm` CLI is preinstalled).
|
|
276
|
+
- macOS 27.0 or newer (Apple's `fm` CLI is preinstalled there).
|
|
277
277
|
- Node.js 20 or newer.
|
|
278
278
|
- Apple Intelligence enabled on the device.
|
|
279
279
|
|
|
280
|
+
Benchmark commands refuse to start on older macOS versions and report the detected version plus the latest supported macOS — see [docs/supported-platforms.md](docs/supported-platforms.md).
|
|
281
|
+
|
|
280
282
|
`pcc` (Private Cloud Compute) availability depends on Apple's current eligibility. `fm-bench` shows it as skipped if `fm available --model pcc` reports unavailable.
|
|
281
283
|
|
|
282
284
|
See [docs/methodology.md](docs/methodology.md) for benchmark methodology and metric references.
|
package/docs/releasing.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
## Option B — Local
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
npm ci && npm test && npm run lint
|
|
20
|
+
npm ci && npm test && npm run lint && npm run check:pack
|
|
21
21
|
npm version patch # or minor / major
|
|
22
22
|
git push --follow-tags
|
|
23
23
|
```
|
|
@@ -37,6 +37,7 @@ gh release edit v0.6.0 --notes-file /tmp/notes.md --repo devinoldenburg/fm-bench
|
|
|
37
37
|
|
|
38
38
|
```sh
|
|
39
39
|
npm run publish:dry-run
|
|
40
|
+
npm run check:pack
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
CI runs the same dry-run on every push to `main` and on pull requests.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Supported platforms
|
|
2
|
+
|
|
3
|
+
`fm-bench` benchmarks Apple's `fm` command, so support follows the platforms where Apple ships that CLI.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **macOS 27.0 or newer** — Apple's `fm` CLI is preinstalled starting with macOS 27. Older macOS releases do not ship `fm`, so `fm-bench` cannot run there.
|
|
8
|
+
- **Node.js 20 or newer**.
|
|
9
|
+
- **Apple Intelligence enabled** on the device.
|
|
10
|
+
|
|
11
|
+
`pcc` (Private Cloud Compute) availability additionally depends on Apple's current eligibility. `fm-bench` reports it as skipped when `fm available --model pcc` reports unavailable.
|
|
12
|
+
|
|
13
|
+
## Version enforcement
|
|
14
|
+
|
|
15
|
+
Commands that launch `fm` benchmarks — the default `run` command and `models` — check the macOS version first and refuse to start on anything older than macOS 27:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
fm-bench: unsupported macOS: detected macOS 26.1, but fm-bench requires macOS 27 or newer (Apple's fm CLI is preinstalled there).
|
|
19
|
+
Latest supported: macOS 27.0 or newer (fm is not available on older macOS releases).
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The process exits with code `2`. This is deliberate: running on an unsupported macOS could never produce a valid benchmark, so the CLI fails fast with the exact version it found and the latest supported macOS version.
|
|
23
|
+
|
|
24
|
+
Commands that only read local report files — `compare`, `history`, `validate`, `export`, and `legend` — are not gated and work anywhere Node.js runs.
|
|
25
|
+
|
|
26
|
+
`fm-bench doctor` still runs on unsupported hosts so you can diagnose the environment: it prints the detected macOS version, an explicit `macOS support` line, and the latest supported version.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fm-bench",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Dynamic benchmark CLI for Apple's fm command on macOS 27+.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test",
|
|
18
18
|
"lint": "node --check bin/fm-bench.js && find src test -name '*.js' -print0 | xargs -0 -n1 node --check",
|
|
19
|
-
"prepack": "npm test && npm run lint",
|
|
19
|
+
"prepack": "npm test && npm run lint && npm run check:pack",
|
|
20
20
|
"release:patch": "npm version patch && git push --follow-tags",
|
|
21
21
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
22
22
|
"release:major": "npm version major && git push --follow-tags",
|
|
23
|
-
"publish:dry-run": "npm publish --dry-run --access public"
|
|
23
|
+
"publish:dry-run": "npm publish --dry-run --access public",
|
|
24
|
+
"check:pack": "node scripts/check-package.mjs"
|
|
24
25
|
},
|
|
25
26
|
"keywords": [
|
|
26
27
|
"apple",
|
|
@@ -46,5 +47,8 @@
|
|
|
46
47
|
"publishConfig": {
|
|
47
48
|
"access": "public",
|
|
48
49
|
"registry": "https://registry.npmjs.org/"
|
|
49
|
-
}
|
|
50
|
+
},
|
|
51
|
+
"os": [
|
|
52
|
+
"darwin"
|
|
53
|
+
]
|
|
50
54
|
}
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { diffReports, renderCompareReport } from './compare.js';
|
|
|
5
5
|
import { renderHtmlReport } from './export.js';
|
|
6
6
|
import { validateReport } from './schema.js';
|
|
7
7
|
import { loadHistory, renderHistoryReport } from './history.js';
|
|
8
|
+
import { detectMacosVersion, evaluateMacosSupport, formatMacosRequirementError, MIN_SUPPORTED_MACOS, parseMacosVersion } from './macos.js';
|
|
8
9
|
import { runProcess } from './process.js';
|
|
9
10
|
import { createProgress } from './progress.js';
|
|
10
11
|
import { flattenResults, toCsv, writeReport } from './report.js';
|
|
@@ -14,7 +15,7 @@ import { legendEntries, renderBenchmarkReport, renderLatencyHistogram, renderLeg
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const packageJson = require('../package.json');
|
|
16
17
|
|
|
17
|
-
export async function runCli(argv = process.argv.slice(2)) {
|
|
18
|
+
export async function runCli(argv = process.argv.slice(2), env = {}) {
|
|
18
19
|
const parsed = parseArgs(argv);
|
|
19
20
|
|
|
20
21
|
if (parsed.help) {
|
|
@@ -27,6 +28,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
await assertSupportedMacos(parsed, env);
|
|
32
|
+
|
|
30
33
|
if (parsed.command === 'legend') {
|
|
31
34
|
if (parsed.format === 'json') {
|
|
32
35
|
console.log(JSON.stringify(legendEntries(), null, 2));
|
|
@@ -153,6 +156,25 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// Offline report tools stay usable anywhere. `doctor` is also allowed through so
|
|
160
|
+
// it can print the detected version and the latest supported macOS when the host
|
|
161
|
+
// is too old. Only commands that launch `fm` benchmarks are hard-gated.
|
|
162
|
+
const SKIP_MACOS_GATE = new Set(['compare', 'history', 'validate', 'export', 'legend', 'doctor']);
|
|
163
|
+
|
|
164
|
+
async function assertSupportedMacos(parsed, env = {}) {
|
|
165
|
+
if (SKIP_MACOS_GATE.has(parsed.command)) return;
|
|
166
|
+
|
|
167
|
+
const evaluation = evaluateMacosSupport(
|
|
168
|
+
env.platform ?? process.platform,
|
|
169
|
+
parseMacosVersion(await detectMacosVersion(env))
|
|
170
|
+
);
|
|
171
|
+
if (evaluation.supported) return;
|
|
172
|
+
|
|
173
|
+
const error = new Error(formatMacosRequirementError(evaluation));
|
|
174
|
+
error.exitCode = 2;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
|
|
156
178
|
function evaluateCi(payload) {
|
|
157
179
|
const reasons = [];
|
|
158
180
|
const totalFailed = payload.summary.reduce((sum, item) => sum + item.failures, 0);
|
|
@@ -554,11 +576,14 @@ async function runDoctor(options) {
|
|
|
554
576
|
checks.push(['node', process.version, true]);
|
|
555
577
|
checks.push(['platform', `${process.platform}/${process.arch}`, process.platform === 'darwin']);
|
|
556
578
|
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
579
|
+
const macOS = await detectMacosVersion();
|
|
580
|
+
const parsedVersion = parseMacosVersion(macOS);
|
|
581
|
+
const support = evaluateMacosSupport(process.platform, parsedVersion);
|
|
582
|
+
checks.push(['macOS', parsedVersion?.version || 'unknown', support.supported]);
|
|
583
|
+
if (!support.supported) {
|
|
584
|
+
checks.push(['macOS support', support.reason, false]);
|
|
585
|
+
checks.push(['latest supported', support.latestSupported, false]);
|
|
586
|
+
}
|
|
562
587
|
|
|
563
588
|
const hwModel = await runProcess('sysctl', ['-n', 'hw.model'], { timeoutMs: 3_000 });
|
|
564
589
|
const hwModelStr = (hwModel.stdout || '').trim();
|
|
@@ -595,17 +620,23 @@ async function runDoctor(options) {
|
|
|
595
620
|
checks.push(['battery', `${pct} (${source})`, ok]);
|
|
596
621
|
}
|
|
597
622
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
623
|
+
let models = [];
|
|
624
|
+
try {
|
|
625
|
+
const inspection = await inspectModels(options);
|
|
626
|
+
models = inspection.models;
|
|
627
|
+
checks.push(['fm', inspection.fmBin, inspection.models.length > 0]);
|
|
628
|
+
for (const model of inspection.models) {
|
|
629
|
+
checks.push([`model:${model.name}`, model.available ? 'available' : model.reason || 'unavailable', model.available]);
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
checks.push(['fm', error.message || String(error), false]);
|
|
602
633
|
}
|
|
603
634
|
|
|
604
635
|
const lines = checks.map(([name, detail, ok]) => `${ok ? 'ok ' : 'warn'} ${name.padEnd(16)} ${String(detail).replace(/\s+/g, ' ').trim()}`);
|
|
605
636
|
console.log(lines.join('\n'));
|
|
606
637
|
|
|
607
638
|
if (options.out) {
|
|
608
|
-
await fs.writeFile(options.out, `${JSON.stringify({ checks, models
|
|
639
|
+
await fs.writeFile(options.out, `${JSON.stringify({ checks, models }, null, 2)}\n`, 'utf8');
|
|
609
640
|
}
|
|
610
641
|
}
|
|
611
642
|
|
|
@@ -669,7 +700,7 @@ function resolveProgress(parsed) {
|
|
|
669
700
|
function helpText() {
|
|
670
701
|
return `fm-bench ${packageJson.version}
|
|
671
702
|
|
|
672
|
-
Dynamic benchmark CLI for Apple's fm command on macOS
|
|
703
|
+
Dynamic benchmark CLI for Apple's fm command on macOS ${MIN_SUPPORTED_MACOS}+.
|
|
673
704
|
|
|
674
705
|
Usage:
|
|
675
706
|
fm-bench [run] [options]
|
package/src/macos.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { runProcess } from './process.js';
|
|
2
|
+
|
|
3
|
+
// macOS releases that ship Apple's `fm` CLI. Older macOS versions cannot run
|
|
4
|
+
// fm-bench at all because the benchmarked binary does not exist there.
|
|
5
|
+
export const SUPPORTED_MACOS_MAJOR = 27;
|
|
6
|
+
|
|
7
|
+
export const MIN_SUPPORTED_MACOS = `${SUPPORTED_MACOS_MAJOR}.0`;
|
|
8
|
+
|
|
9
|
+
export const MACOS_REQUIREMENT_MESSAGE =
|
|
10
|
+
`fm-bench requires macOS ${SUPPORTED_MACOS_MAJOR} or newer (Apple's fm CLI is preinstalled there).`;
|
|
11
|
+
|
|
12
|
+
export function parseMacosVersion(text = '') {
|
|
13
|
+
const match = String(text).match(/ProductVersion:\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
return {
|
|
16
|
+
major: Number.parseInt(match[1], 10),
|
|
17
|
+
minor: match[2] != null ? Number.parseInt(match[2], 10) : 0,
|
|
18
|
+
patch: match[3] != null ? Number.parseInt(match[3], 10) : 0,
|
|
19
|
+
version: [match[1], match[2], match[3]].filter((part) => part != null).join('.')
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isMacosSupported(parsed) {
|
|
24
|
+
return parsed != null && Number.isInteger(parsed.major) && parsed.major >= SUPPORTED_MACOS_MAJOR;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function evaluateMacosSupport(platform, parsed) {
|
|
28
|
+
if (platform !== 'darwin') {
|
|
29
|
+
return {
|
|
30
|
+
supported: false,
|
|
31
|
+
reason: `fm-bench only runs on macOS with Apple's fm CLI (detected platform: ${platform}).`,
|
|
32
|
+
latestSupported: `macOS ${MIN_SUPPORTED_MACOS} or newer`
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (parsed == null) {
|
|
36
|
+
return {
|
|
37
|
+
supported: true,
|
|
38
|
+
warnOnly: true,
|
|
39
|
+
reason: 'could not determine the macOS version',
|
|
40
|
+
latestSupported: `macOS ${MIN_SUPPORTED_MACOS} or newer`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (isMacosSupported(parsed)) {
|
|
44
|
+
return {
|
|
45
|
+
supported: true,
|
|
46
|
+
reason: null,
|
|
47
|
+
latestSupported: `macOS ${MIN_SUPPORTED_MACOS} or newer`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
supported: false,
|
|
52
|
+
reason: `detected macOS ${parsed.version}, but ${MACOS_REQUIREMENT_MESSAGE}`,
|
|
53
|
+
latestSupported: `macOS ${MIN_SUPPORTED_MACOS} or newer`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatMacosRequirementError({ reason, latestSupported }) {
|
|
58
|
+
return [
|
|
59
|
+
`unsupported macOS: ${reason}`,
|
|
60
|
+
`Latest supported: ${latestSupported} (fm is not available on older macOS releases).`
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function detectMacosVersion(options = {}) {
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
66
|
+
if (typeof options.swVers === 'function') {
|
|
67
|
+
return options.swVers();
|
|
68
|
+
}
|
|
69
|
+
const result = await runProcess('sw_vers', [], { timeoutMs });
|
|
70
|
+
return result.stdout || result.stderr;
|
|
71
|
+
}
|