@jeffreyjyz/mpc 0.0.0-stage → 0.1.0
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/LICENSE +18 -0
- package/README.md +269 -2
- package/package.json +31 -5
- package/src/cli/config.ts +79 -0
- package/src/cli/engine/ability.ts +50 -0
- package/src/cli/engine/cost.ts +62 -0
- package/src/cli/engine/index.ts +59 -0
- package/src/cli/engine/project.ts +145 -0
- package/src/cli/engine/rows.ts +64 -0
- package/src/cli/engine/score.ts +118 -0
- package/src/cli/flow/check.ts +66 -0
- package/src/cli/flow/collect.ts +76 -0
- package/src/cli/flow/columns.ts +32 -0
- package/src/cli/flow/sort.ts +68 -0
- package/src/cli/options.ts +78 -0
- package/src/cli/parse/cac.ts +114 -0
- package/src/cli/parse/fields.ts +65 -0
- package/src/cli/parse/map.ts +116 -0
- package/src/cli/parse/plugins.ts +67 -0
- package/src/cli/parse/validate.ts +60 -0
- package/src/cli/run.ts +151 -0
- package/src/constants/cli.ts +153 -0
- package/src/constants/data.ts +30 -0
- package/src/constants/scoring.ts +22 -0
- package/src/constants/shape.ts +19 -0
- package/src/constants/sources.ts +69 -0
- package/src/constants/view.ts +161 -0
- package/src/data/bench/cc.ts +26 -0
- package/src/data/bench/index.ts +126 -0
- package/src/data/bench/resolve.ts +107 -0
- package/src/data/bench/store.ts +169 -0
- package/src/data/bench/types.ts +34 -0
- package/src/data/cmduse.ts +43 -0
- package/src/data/scrape/catalog/catalog.ts +99 -0
- package/src/data/scrape/catalog/deal.ts +20 -0
- package/src/data/scrape/catalog/numeric.ts +30 -0
- package/src/data/scrape/catalog/variant.ts +11 -0
- package/src/data/scrape/index.ts +21 -0
- package/src/data/scrape/roleRows.ts +41 -0
- package/src/data/scrape/tables.ts +96 -0
- package/src/data/shape.ts +205 -0
- package/src/data/sources/aa/api.ts +54 -0
- package/src/data/sources/aa/parse.ts +66 -0
- package/src/data/sources/aa/web.ts +53 -0
- package/src/data/sources/cc/catalog.ts +71 -0
- package/src/data/sources/cc/cmduse.ts +24 -0
- package/src/data/sources/cc/plans.ts +29 -0
- package/src/data/sources/opencode.ts +48 -0
- package/src/data/usage/index.ts +118 -0
- package/src/data/usage/log.ts +89 -0
- package/src/data/usage/logs.ts +117 -0
- package/src/data/usage/opencodeDb.ts +135 -0
- package/src/data/usage/opencodeV2.ts +70 -0
- package/src/data/usage/parse.ts +85 -0
- package/src/index.ts +11 -0
- package/src/keys.ts +29 -0
- package/src/types.ts +92 -0
- package/src/view/columns/cc.ts +50 -0
- package/src/view/columns/meta.ts +41 -0
- package/src/view/columns/oc.ts +50 -0
- package/src/view/footer.ts +124 -0
- package/src/view/layout/fit.ts +58 -0
- package/src/view/layout/segments.ts +98 -0
- package/src/view/layout/table.ts +85 -0
- package/src/view/layout/usage.ts +163 -0
- package/src/view/render.ts +62 -0
- package/src/view/schema.ts +81 -0
- package/src/view/text/export.ts +72 -0
- package/src/view/text/format.ts +79 -0
- package/src/view/text/index.ts +54 -0
- package/src/view/text/styles.ts +151 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { cac } from "cac";
|
|
2
|
+
import type { Options } from "~/cli/options.ts";
|
|
3
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
4
|
+
import { type Bag, toOptions } from "./map.ts";
|
|
5
|
+
|
|
6
|
+
/** Raw CLI bag: exactly what the user typed, no config, no defaults. */
|
|
7
|
+
export function parseFlags(argv: string[]): Bag {
|
|
8
|
+
const cli = cac("mpc");
|
|
9
|
+
cli.option(
|
|
10
|
+
"--cc-plan <id>",
|
|
11
|
+
"CommandCode plan: go, goat, pro, max10, max20",
|
|
12
|
+
)
|
|
13
|
+
.option("--in <n>", "input tokens per request")
|
|
14
|
+
.option("--cache <n>", "cache-read tokens per request")
|
|
15
|
+
.option("--out <n>", "output tokens per request")
|
|
16
|
+
.option(
|
|
17
|
+
"--reasoning <n>",
|
|
18
|
+
"reasoning tokens, billed at the output rate",
|
|
19
|
+
)
|
|
20
|
+
.option("--cache-write <n>", "cache-write tokens per request")
|
|
21
|
+
.option(
|
|
22
|
+
"--shape <spec>",
|
|
23
|
+
"auto | measured | <file> | off: reqshape shape (auto = measured with enough data)",
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
"--since <date>",
|
|
27
|
+
"measured shape: only requests on or after this date",
|
|
28
|
+
)
|
|
29
|
+
.option("--metric <name>", "val | cost | perreq | req | name")
|
|
30
|
+
.option("--model <re>", "only rows whose name matches")
|
|
31
|
+
.option("--only <scope>", "both | all")
|
|
32
|
+
.option(
|
|
33
|
+
"--minimal",
|
|
34
|
+
"fewest columns: model + req/mo each side + win + val",
|
|
35
|
+
)
|
|
36
|
+
.option(
|
|
37
|
+
"--medium",
|
|
38
|
+
"allowance and the rate views per side + win + cost + val",
|
|
39
|
+
)
|
|
40
|
+
.option(
|
|
41
|
+
"--no-fit",
|
|
42
|
+
"keep the preset's full width, do not trim to the terminal",
|
|
43
|
+
)
|
|
44
|
+
.option("--detail", "every column, untrimmed")
|
|
45
|
+
.option("--width <n>", "force table width")
|
|
46
|
+
.option(
|
|
47
|
+
"--columns <ids>",
|
|
48
|
+
"comma-separated columns (--columns help to list)",
|
|
49
|
+
)
|
|
50
|
+
.option("--preset <name>", "named column preset from config")
|
|
51
|
+
.option(
|
|
52
|
+
"--bench <src>",
|
|
53
|
+
"cc | aa | aa-web | file:<path> | url:<url> (aa with a key, else cc)",
|
|
54
|
+
)
|
|
55
|
+
.option("--bench-weight <n>", "ability share of VAL")
|
|
56
|
+
.option("--tps-weight <n>", "speed share of VAL")
|
|
57
|
+
.option("--val-weights <w>", "ability,tps,volume,cache,output shares")
|
|
58
|
+
.option("--scale <mode>", "log | linear for skewed terms")
|
|
59
|
+
.option(
|
|
60
|
+
"--inherit-suffixes <s>",
|
|
61
|
+
"comma-separated speed-variant suffixes",
|
|
62
|
+
)
|
|
63
|
+
.option("--window <pair>", "override five-hour,weekly window ratios")
|
|
64
|
+
.option("--cost-thresholds <p>", "green,yellow COST cut-offs")
|
|
65
|
+
.option("--val-thresholds <p>", "yellow,green VAL cut-offs")
|
|
66
|
+
.option("--bench-name <label>", "footer label for the source")
|
|
67
|
+
.option("--aa-key <key>", "Artificial Analysis key (else AA_API_KEY)")
|
|
68
|
+
.option("--no-fallback", "do not fill misses from other sources")
|
|
69
|
+
.option("--refresh", "ignore the aa-web cache")
|
|
70
|
+
.option("--no-ability", "hide ability and VAL")
|
|
71
|
+
.option("--peak", "peak-rate rows (OpenCode DeepSeek)")
|
|
72
|
+
.option("--asc", "flip the default sort direction")
|
|
73
|
+
.option("--json", "machine-readable output")
|
|
74
|
+
.option("--format <name>", "table | json | csv | md")
|
|
75
|
+
.option("--color <mode>", "auto | always | never")
|
|
76
|
+
.option("--check", "validate live sources and exit")
|
|
77
|
+
.option(
|
|
78
|
+
"--config <path>",
|
|
79
|
+
"config file (default: ~/.config/mpc/config.json)",
|
|
80
|
+
)
|
|
81
|
+
.option("--no-config", "ignore the config file")
|
|
82
|
+
.option("--plugin <paths>", "extra config plugins, comma-separated")
|
|
83
|
+
.option("--print-config", "print the effective settings and exit")
|
|
84
|
+
.option(
|
|
85
|
+
"--usage",
|
|
86
|
+
"project your real usage from cmduse onto both plans",
|
|
87
|
+
)
|
|
88
|
+
.option("--usage-file <path>", "read usage from a JSON file instead")
|
|
89
|
+
.option(
|
|
90
|
+
"--usage-window <w>",
|
|
91
|
+
"usage range: period | all | <n>d (default period)",
|
|
92
|
+
)
|
|
93
|
+
.option(
|
|
94
|
+
"--usage-db <path>",
|
|
95
|
+
"opencode message store (default ~/.local/share/opencode/opencode.db)",
|
|
96
|
+
)
|
|
97
|
+
.option(
|
|
98
|
+
"--usage-log <path>",
|
|
99
|
+
"provider usage log (default ~/.cache/mpc/usage.jsonl)",
|
|
100
|
+
)
|
|
101
|
+
.option(
|
|
102
|
+
"--usage-months <n>",
|
|
103
|
+
"treat the logged usage as covering N months",
|
|
104
|
+
);
|
|
105
|
+
cli.help();
|
|
106
|
+
cli.version(pkg.version);
|
|
107
|
+
const parsed = cli.parse(["node", "mpc", ...argv], { run: false });
|
|
108
|
+
return (parsed.options ?? {}) as Bag;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** CLI-only options, with defaults. Config is applied by resolveOptions. */
|
|
112
|
+
export function parseArgs(argv: string[]): Options {
|
|
113
|
+
return toOptions(parseFlags(argv));
|
|
114
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ColorMode } from "~/cli/options.ts";
|
|
2
|
+
import type { Bag } from "./validate.ts";
|
|
3
|
+
|
|
4
|
+
/** Comma-separated or repeated flag, read as numbers. */
|
|
5
|
+
export function numbers(bag: Bag, key: string): number[] | undefined {
|
|
6
|
+
const value = bag[key];
|
|
7
|
+
if (value === undefined || value === false) return undefined;
|
|
8
|
+
const parts = Array.isArray(value) ? value : String(value).split(",");
|
|
9
|
+
const nums = parts.map((part) => Number(part));
|
|
10
|
+
if (nums.some((n) => !Number.isFinite(n))) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`--${key} expects comma-separated numbers, got "${value}"`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return nums;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Comma-separated or repeated flag, read as trimmed words. */
|
|
19
|
+
export function words(bag: Bag, key: string): string[] {
|
|
20
|
+
const value = bag[key];
|
|
21
|
+
if (value === undefined || value === false) return [];
|
|
22
|
+
const parts = Array.isArray(value) ? value : String(value).split(",");
|
|
23
|
+
return parts.map((part) => String(part).trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Named column presets from config: name -> column ids. */
|
|
27
|
+
export function presets(bag: Bag): Record<string, string[]> {
|
|
28
|
+
const value = bag.presets;
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
30
|
+
const out: Record<string, string[]> = {};
|
|
31
|
+
for (const [name, ids] of Object.entries(
|
|
32
|
+
value as Record<string, unknown>,
|
|
33
|
+
)) {
|
|
34
|
+
out[name] = Array.isArray(ids)
|
|
35
|
+
? ids.map((id) => String(id).trim())
|
|
36
|
+
: String(ids)
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((id) => id.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function usageWindow(value: unknown): "period" | "all" | `${number}d` {
|
|
45
|
+
if (value === "all") return "all";
|
|
46
|
+
if (typeof value === "string" && /^\d+d$/.test(value)) {
|
|
47
|
+
return value as `${number}d`;
|
|
48
|
+
}
|
|
49
|
+
return "period";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function colorMode(value: unknown): ColorMode {
|
|
53
|
+
if (value === false || value === "never") return "never";
|
|
54
|
+
if (value === "always") return "always";
|
|
55
|
+
return "auto";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A two-number pair, e.g. window ratios or colour thresholds. */
|
|
59
|
+
export function pair(value: unknown, name: string): [number, number] {
|
|
60
|
+
const nums = numbers({ v: value }, "v");
|
|
61
|
+
if (nums?.length !== 2) {
|
|
62
|
+
throw new Error(`--${name} expects two comma-separated numbers`);
|
|
63
|
+
}
|
|
64
|
+
return [nums[0] ?? 0, nums[1] ?? 0];
|
|
65
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Format,
|
|
3
|
+
type Metric,
|
|
4
|
+
type Options,
|
|
5
|
+
parseFormat,
|
|
6
|
+
parseMetric,
|
|
7
|
+
parseOnly,
|
|
8
|
+
type ScaleMode,
|
|
9
|
+
} from "~/cli/options.ts";
|
|
10
|
+
import { ARG_DEFAULTS, BENCH_DEFAULT } from "~/constants/cli.ts";
|
|
11
|
+
import { SHAPE_MEASURED } from "~/constants/shape.ts";
|
|
12
|
+
import {
|
|
13
|
+
colorMode,
|
|
14
|
+
numbers,
|
|
15
|
+
pair,
|
|
16
|
+
presets,
|
|
17
|
+
usageWindow,
|
|
18
|
+
words,
|
|
19
|
+
} from "./fields.ts";
|
|
20
|
+
import { assertKnown, assertValues, type Bag, int, share } from "./validate.ts";
|
|
21
|
+
|
|
22
|
+
export type { Bag } from "./validate.ts";
|
|
23
|
+
|
|
24
|
+
const pick = (bag: Bag, key: string): unknown =>
|
|
25
|
+
bag[key] === undefined
|
|
26
|
+
? ARG_DEFAULTS[key as keyof typeof ARG_DEFAULTS]
|
|
27
|
+
: bag[key];
|
|
28
|
+
|
|
29
|
+
/** `--bench` default is contextual: the AA API leads only when a key exists. */
|
|
30
|
+
function benchDefault(bag: Bag): string {
|
|
31
|
+
const keyed = bag.aaKey !== undefined || Boolean(process.env.AA_API_KEY);
|
|
32
|
+
return keyed ? BENCH_DEFAULT.keyed : BENCH_DEFAULT.keyless;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Reduce the merged bag (plugins < config < CLI) to Options. */
|
|
36
|
+
export function toOptions(bag: Bag): Options {
|
|
37
|
+
assertKnown(bag);
|
|
38
|
+
assertValues(bag);
|
|
39
|
+
const columns = bag.columns === undefined ? undefined : String(bag.columns);
|
|
40
|
+
const window = numbers(bag, "window");
|
|
41
|
+
return {
|
|
42
|
+
ccPlan: String(pick(bag, "ccPlan")),
|
|
43
|
+
workload: {
|
|
44
|
+
input: int("in", pick(bag, "in")),
|
|
45
|
+
cacheRead: int("cache", pick(bag, "cache")),
|
|
46
|
+
output: int("out", pick(bag, "out")),
|
|
47
|
+
reasoning: int("reasoning", pick(bag, "reasoning")),
|
|
48
|
+
cacheWrite: int("cache-write", pick(bag, "cacheWrite")),
|
|
49
|
+
},
|
|
50
|
+
// A bare `--shape` means "measure it" (`cac` yields `true`); no flag at
|
|
51
|
+
// all leaves the default (`auto`, which trusts reqshape when it has data).
|
|
52
|
+
shape: bag.shape === true ? SHAPE_MEASURED : String(pick(bag, "shape")),
|
|
53
|
+
since: bag.since === undefined ? undefined : String(bag.since),
|
|
54
|
+
metric: parseMetric(String(pick(bag, "metric"))) as Metric,
|
|
55
|
+
model: bag.model === undefined ? undefined : String(bag.model),
|
|
56
|
+
only: parseOnly(String(pick(bag, "only"))),
|
|
57
|
+
columns:
|
|
58
|
+
columns === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: columns
|
|
61
|
+
.split(",")
|
|
62
|
+
.map((c) => c.trim())
|
|
63
|
+
.filter(Boolean),
|
|
64
|
+
preset: bag.preset === undefined ? undefined : String(bag.preset),
|
|
65
|
+
presets: presets(bag),
|
|
66
|
+
detail: bag.detail === true,
|
|
67
|
+
minimal: bag.minimal === true,
|
|
68
|
+
medium: bag.medium === true,
|
|
69
|
+
// Width 0 is meaningless and silently disabled trimming; cac coerces a
|
|
70
|
+
// blank `--width ""` to 0, so require at least one column.
|
|
71
|
+
width: bag.width === undefined ? undefined : int("width", bag.width, 1),
|
|
72
|
+
fit: bag.fit !== false,
|
|
73
|
+
bench: bag.bench === undefined ? benchDefault(bag) : String(bag.bench),
|
|
74
|
+
benchWeight: share("bench-weight", pick(bag, "benchWeight")),
|
|
75
|
+
tpsWeight: share("tps-weight", pick(bag, "tpsWeight")),
|
|
76
|
+
valWeights: numbers(bag, "valWeights"),
|
|
77
|
+
scale: String(pick(bag, "scale")) as ScaleMode,
|
|
78
|
+
// Absent means "use the built-in suffixes"; an empty list means "inherit
|
|
79
|
+
// nothing", which is why this must stay undefined rather than become []:
|
|
80
|
+
// the lookups read an empty array as a deliberate opt-out.
|
|
81
|
+
inheritSuffixes: (() => {
|
|
82
|
+
const suffixes = words(bag, "inheritSuffixes");
|
|
83
|
+
return suffixes.length > 0 ? suffixes : undefined;
|
|
84
|
+
})(),
|
|
85
|
+
window:
|
|
86
|
+
window && window.length === 2
|
|
87
|
+
? [window[0] ?? 0, window[1] ?? 0]
|
|
88
|
+
: undefined,
|
|
89
|
+
costThresholds: pair(pick(bag, "costThresholds"), "cost-thresholds"),
|
|
90
|
+
valThresholds: pair(pick(bag, "valThresholds"), "val-thresholds"),
|
|
91
|
+
benchName:
|
|
92
|
+
bag.benchName === undefined ? undefined : String(bag.benchName),
|
|
93
|
+
aaKey: bag.aaKey === undefined ? undefined : String(bag.aaKey),
|
|
94
|
+
noFallback: bag.fallback === false,
|
|
95
|
+
refresh: bag.refresh === true,
|
|
96
|
+
noAbility: bag.ability === false,
|
|
97
|
+
peak: bag.peak === true,
|
|
98
|
+
asc: bag.asc === true,
|
|
99
|
+
json: bag.json === true,
|
|
100
|
+
format: parseFormat(
|
|
101
|
+
bag.format === undefined ? "table" : String(bag.format),
|
|
102
|
+
) as Format,
|
|
103
|
+
colorMode: colorMode(bag.color),
|
|
104
|
+
check: bag.check === true,
|
|
105
|
+
printConfig: bag.printConfig === true,
|
|
106
|
+
usage: bag.usage === true,
|
|
107
|
+
usageFile:
|
|
108
|
+
bag.usageFile === undefined ? undefined : String(bag.usageFile),
|
|
109
|
+
usageLog: bag.usageLog === undefined ? undefined : String(bag.usageLog),
|
|
110
|
+
usageDb: bag.usageDb === undefined ? undefined : String(bag.usageDb),
|
|
111
|
+
usageMonths:
|
|
112
|
+
bag.usageMonths === undefined ? 1 : Number(bag.usageMonths),
|
|
113
|
+
usageWindow: usageWindow(bag.usageWindow),
|
|
114
|
+
plugins: words(bag, "plugins"),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import type { Bag } from "./validate.ts";
|
|
4
|
+
|
|
5
|
+
/** A plugin's default export may be an object or a (possibly async) factory. */
|
|
6
|
+
type PluginFactory = (ctx: PluginContext) => Bag | Promise<Bag>;
|
|
7
|
+
|
|
8
|
+
export interface PluginContext {
|
|
9
|
+
env: Record<string, string | undefined>;
|
|
10
|
+
cwd: string;
|
|
11
|
+
configDir: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function qualify(spec: string, relativeTo: string): string {
|
|
15
|
+
if (spec.startsWith(".") || isAbsolute(spec)) {
|
|
16
|
+
return isAbsolute(spec) ? spec : join(relativeTo, spec);
|
|
17
|
+
}
|
|
18
|
+
return spec; // bare specifier: resolve as a package
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asBag(value: unknown, spec: string): Bag {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
23
|
+
throw new Error(`plugin ${spec} must export a config object`);
|
|
24
|
+
}
|
|
25
|
+
return value as Bag;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function loadOne(spec: string, relativeTo: string): Promise<Bag> {
|
|
29
|
+
const target = qualify(spec, relativeTo);
|
|
30
|
+
const ctx: PluginContext = {
|
|
31
|
+
env: process.env,
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
configDir: relativeTo,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
target.endsWith(".json") ||
|
|
38
|
+
(existsSync(target) && target.endsWith(".json"))
|
|
39
|
+
) {
|
|
40
|
+
return asBag(JSON.parse(readFileSync(target, "utf8")), spec);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const mod = (await import(target)) as { default?: unknown };
|
|
44
|
+
const exported = mod.default ?? mod;
|
|
45
|
+
const value =
|
|
46
|
+
typeof exported === "function"
|
|
47
|
+
? await (exported as PluginFactory)(ctx)
|
|
48
|
+
: exported;
|
|
49
|
+
return asBag(value, spec);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`plugin ${spec} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Load plugins in order and layer them: later plugins win over earlier ones. */
|
|
58
|
+
export async function loadPlugins(
|
|
59
|
+
specs: string[],
|
|
60
|
+
relativeTo: string,
|
|
61
|
+
): Promise<Bag> {
|
|
62
|
+
const merged: Bag = {};
|
|
63
|
+
for (const spec of specs) {
|
|
64
|
+
Object.assign(merged, await loadOne(spec, relativeTo));
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { INTERNAL, KNOWN, VALUED } from "~/constants/cli.ts";
|
|
2
|
+
|
|
3
|
+
export type Bag = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
// `Number("")` is 0 and `Number(true)` is 1, so a blank or bare flag would be
|
|
6
|
+
// coerced into a plausible-looking value (`--out ""` silently priced every model
|
|
7
|
+
// with zero output tokens, `--width ""` became 0 and disabled trimming). Reject
|
|
8
|
+
// both shapes before the conversion; a bare boolean is also caught by
|
|
9
|
+
// `assertValues`, but config/plugin layers reach here without that guard.
|
|
10
|
+
export function int(name: string, value: unknown, min = 0): number {
|
|
11
|
+
const want = min === 0 ? "a non-negative integer" : `an integer ≥ ${min}`;
|
|
12
|
+
if (value === true || value === "" || Number(value) < min) {
|
|
13
|
+
throw new Error(`--${name} expects ${want}, got "${value}"`);
|
|
14
|
+
}
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
17
|
+
throw new Error(`--${name} expects ${want}, got "${value}"`);
|
|
18
|
+
}
|
|
19
|
+
return n;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function share(name: string, value: unknown): number {
|
|
23
|
+
if (value === true || value === "") {
|
|
24
|
+
throw new Error(`--${name} expects a number between 0 and 1`);
|
|
25
|
+
}
|
|
26
|
+
const n = Number(value);
|
|
27
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
28
|
+
throw new Error(`--${name} expects a number between 0 and 1`);
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reject undeclared flags before mapping. */
|
|
34
|
+
export function assertKnown(bag: Bag): void {
|
|
35
|
+
const extra = Object.keys(bag).filter(
|
|
36
|
+
(key) => !KNOWN.has(key) && !INTERNAL.has(key),
|
|
37
|
+
);
|
|
38
|
+
if (extra.length > 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`unknown flag "--${extra[0]}" — see --help for the full list`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** camelCase key -> the `--kebab-case` spelling the user typed. */
|
|
46
|
+
function flag(key: string): string {
|
|
47
|
+
return `--${key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reject a value-taking flag used bare (`--shape` with no argument). */
|
|
51
|
+
export function assertValues(bag: Bag): void {
|
|
52
|
+
const bare = Object.keys(bag).filter(
|
|
53
|
+
(key) => VALUED.has(key) && bag[key] === true,
|
|
54
|
+
);
|
|
55
|
+
if (bare.length > 0) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${flag(String(bare[0]))} expects a value — see --help`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { COLUMN_HELP, COLUMN_IDS } from "~/constants/view.ts";
|
|
3
|
+
import { loadUsage } from "~/data/usage/index.ts";
|
|
4
|
+
import { renderUsage } from "~/view/layout/usage.ts";
|
|
5
|
+
import {
|
|
6
|
+
fitColumns,
|
|
7
|
+
renderCsv,
|
|
8
|
+
renderJson,
|
|
9
|
+
renderMarkdown,
|
|
10
|
+
renderText,
|
|
11
|
+
setColorMode,
|
|
12
|
+
setThresholds,
|
|
13
|
+
tally,
|
|
14
|
+
} from "~/view/render.ts";
|
|
15
|
+
import { describeConfig, resolveBag } from "./config.ts";
|
|
16
|
+
import { project } from "./engine/project.ts";
|
|
17
|
+
import { runCheck } from "./flow/check.ts";
|
|
18
|
+
import { collect } from "./flow/collect.ts";
|
|
19
|
+
import { resolveColumns, trimsToWidth } from "./flow/columns.ts";
|
|
20
|
+
import { matches, sortRows } from "./flow/sort.ts";
|
|
21
|
+
import { toOptions } from "./parse/map.ts";
|
|
22
|
+
import { assertKnown, assertValues } from "./parse/validate.ts";
|
|
23
|
+
|
|
24
|
+
export type { Metric, Options } from "./options.ts";
|
|
25
|
+
export { parseArgs } from "./parse/cac.ts";
|
|
26
|
+
|
|
27
|
+
export async function run(argv: string[]): Promise<number> {
|
|
28
|
+
const bag = await resolveBag(argv);
|
|
29
|
+
// cac prints the help text / version line itself and returns the parsed bag
|
|
30
|
+
// (with the `h`/`v` aliases) instead of exiting, so stop here — otherwise the
|
|
31
|
+
// full table would print after the help, and the aliases would trip assertKnown.
|
|
32
|
+
if (
|
|
33
|
+
bag.help === true ||
|
|
34
|
+
bag.h === true ||
|
|
35
|
+
bag.version === true ||
|
|
36
|
+
bag.v === true
|
|
37
|
+
) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (bag.printConfig === true) {
|
|
41
|
+
// This is the one mode meant to check a config, so a typo must fail here
|
|
42
|
+
// rather than on the next real run.
|
|
43
|
+
assertKnown(bag);
|
|
44
|
+
assertValues(bag);
|
|
45
|
+
process.stdout.write(describeConfig(bag));
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
const options = toOptions(bag);
|
|
49
|
+
if (options.columns?.includes("help")) {
|
|
50
|
+
console.log(COLUMN_HELP);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setColorMode(options.colorMode);
|
|
55
|
+
setThresholds(options.costThresholds, options.valThresholds);
|
|
56
|
+
|
|
57
|
+
const requested = resolveColumns(options);
|
|
58
|
+
const unknown = requested.filter((id) => !COLUMN_IDS.includes(id));
|
|
59
|
+
if (unknown.length > 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`unknown column(s): ${unknown.join(", ")} — valid: ${COLUMN_IDS.join(", ")}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
ocEntries,
|
|
67
|
+
ccEntries,
|
|
68
|
+
ocPlanInfo,
|
|
69
|
+
ccPlanInfo,
|
|
70
|
+
rows,
|
|
71
|
+
ability,
|
|
72
|
+
workloads,
|
|
73
|
+
shapeNote,
|
|
74
|
+
} = await collect(options);
|
|
75
|
+
if (options.check) {
|
|
76
|
+
return runCheck(options, ocEntries, ccEntries, rows, ability);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.usage) {
|
|
80
|
+
const usage = await loadUsage(
|
|
81
|
+
options.usageFile,
|
|
82
|
+
options.usageWindow,
|
|
83
|
+
options.usageLog,
|
|
84
|
+
options.usageDb,
|
|
85
|
+
);
|
|
86
|
+
const projection = project(
|
|
87
|
+
usage.entries,
|
|
88
|
+
{ "oc-go": ocEntries, cc: ccEntries },
|
|
89
|
+
{ "oc-go": ocPlanInfo, cc: ccPlanInfo },
|
|
90
|
+
{ months: options.usageMonths },
|
|
91
|
+
);
|
|
92
|
+
if (options.format === "json" || options.json) {
|
|
93
|
+
console.log(
|
|
94
|
+
JSON.stringify({ source: usage.label, ...projection }, null, 2),
|
|
95
|
+
);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
renderUsage(projection, {
|
|
99
|
+
label: usage.label,
|
|
100
|
+
window: usage.window,
|
|
101
|
+
account: usage.account,
|
|
102
|
+
months: options.usageMonths,
|
|
103
|
+
plans: { "oc-go": ocPlanInfo, cc: ccPlanInfo },
|
|
104
|
+
});
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = sortRows(
|
|
109
|
+
rows.filter(
|
|
110
|
+
(r) =>
|
|
111
|
+
matches(r, options.model) &&
|
|
112
|
+
(options.only === "all" || (r.oc && r.cc)),
|
|
113
|
+
),
|
|
114
|
+
options.metric,
|
|
115
|
+
options.asc,
|
|
116
|
+
);
|
|
117
|
+
const meta = {
|
|
118
|
+
ocPlan: ocPlanInfo,
|
|
119
|
+
ccPlan: ccPlanInfo,
|
|
120
|
+
workloads,
|
|
121
|
+
abilityLabel: options.noAbility
|
|
122
|
+
? undefined
|
|
123
|
+
: (options.benchName ?? ability.label),
|
|
124
|
+
abilityNote: options.noAbility ? undefined : ability.note,
|
|
125
|
+
shapeNote,
|
|
126
|
+
window: options.window,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (options.format === "json" || options.json) {
|
|
130
|
+
console.log(renderJson(result, meta));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (options.format === "csv") {
|
|
134
|
+
process.stdout.write(renderCsv(result, tally(result)));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (options.format === "md") {
|
|
138
|
+
process.stdout.write(renderMarkdown(result, [ocPlanInfo, ccPlanInfo]));
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const limit = options.width ?? process.stdout.columns ?? 120;
|
|
143
|
+
const fitted = trimsToWidth(options)
|
|
144
|
+
? fitColumns(result, requested, limit)
|
|
145
|
+
: { ids: requested, dropped: [] as string[] };
|
|
146
|
+
renderText(result, meta, fitted.ids, fitted.dropped);
|
|
147
|
+
console.log(
|
|
148
|
+
`\n${result.length} models · OpenCode Go vs CommandCode ${ccPlanInfo.label} · ${ocEntries.length} OpenCode / ${ccEntries.length} CommandCode entries`,
|
|
149
|
+
);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|