@trazum/cli 1.50.5 → 1.50.6
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/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +38 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +38 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +19 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +108 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +48 -0
- package/src/i18n/es.ts +48 -0
- package/src/i18n/types.ts +20 -0
- package/src/index.ts +137 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.6",
|
|
4
4
|
"description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build && npm test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@trazum/core": "1.50.
|
|
40
|
+
"@trazum/core": "1.50.6"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -58,6 +58,7 @@ ${bold('USAGE')}
|
|
|
58
58
|
trazum models
|
|
59
59
|
trazum rules
|
|
60
60
|
trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
|
|
61
|
+
trazum ladder <log>
|
|
61
62
|
trazum feedback
|
|
62
63
|
trazum --version
|
|
63
64
|
|
|
@@ -597,6 +598,12 @@ ${bold('CONFIG FILE')}
|
|
|
597
598
|
spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
|
|
598
599
|
"trazum profile", in dollars. A budgeted label with no calls in
|
|
599
600
|
the log is reported as not measured, never as a pass
|
|
601
|
+
ladders { "support": { "tiers": ["claude-haiku-4-5", "claude-opus-5"],
|
|
602
|
+
"escalateOn": ["escalated"] } } — cheap model first, escalate a
|
|
603
|
+
recorded failure to a dearer one. Both fields required.
|
|
604
|
+
"trazum ladder <log>" prints the break-even escalation rate
|
|
605
|
+
beside the measured one: an escalation pays twice, so above that
|
|
606
|
+
rate the ladder costs more than never having built it
|
|
600
607
|
outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
|
|
601
608
|
your own vocabulary for what happened, and which of it counts as
|
|
602
609
|
a win. Both required: which words mean success is a judgement
|
|
@@ -957,6 +964,47 @@ ${bold('EXAMPLES')}
|
|
|
957
964
|
`${path} exists and could not be parsed, so nothing was written over it. Fix or move it first.`,
|
|
958
965
|
},
|
|
959
966
|
|
|
967
|
+
ladder: {
|
|
968
|
+
heading: () => 'Escalation ladders',
|
|
969
|
+
noLadders: () =>
|
|
970
|
+
'No ladders configured. A ladder sends a workload to a cheap model first and escalates a recorded failure to a dearer one — add "ladders" to trazum.config.json, for example {"support": {"tiers": ["claude-haiku-4-5", "claude-opus-5"], "escalateOn": ["escalated"]}}.',
|
|
971
|
+
workload: (label) => label,
|
|
972
|
+
arithmetic: (cheap, dear, breakEven) =>
|
|
973
|
+
`${cheap} a call cheap, ${dear} dear. Break-even escalation rate: ${breakEven}.`,
|
|
974
|
+
measured: (rate, escalations, calls) =>
|
|
975
|
+
`Measured: ${rate} (${escalations} of ${calls} calls escalated).`,
|
|
976
|
+
saving: (delta) => `Saving ${delta} a call against never having built it.`,
|
|
977
|
+
costing: (delta) =>
|
|
978
|
+
`Costing ${delta} a call MORE than never having built it. Escalating above the break-even rate means paying for the cheap attempt and the dear one on most calls.`,
|
|
979
|
+
atBreakEven: (band) =>
|
|
980
|
+
`Within ${band} of break-even, so no sign is claimed. Inside that band the answer flips on ordinary week-to-week variation, and "saving" on Monday and "costing" on Thursday from the same policy teaches a reader to ignore the figure.`,
|
|
981
|
+
cannotTell: (why, calls) =>
|
|
982
|
+
why === 'too-few-calls'
|
|
983
|
+
? `Cannot tell yet: ${calls} calls carried a declared outcome, and a rate over that few moves more from one more call than from anything you could do about it.`
|
|
984
|
+
: why === 'no-outcomes-recorded'
|
|
985
|
+
? 'Cannot tell: nothing in this log recorded an outcome for this workload, so there is no escalation rate to compare against.'
|
|
986
|
+
: why === 'tier-unpriced'
|
|
987
|
+
? 'Cannot tell: one of the tiers is not in the price catalogue, so the break-even rate cannot be computed.'
|
|
988
|
+
: 'Cannot tell: this ladder declares no escalation values.',
|
|
989
|
+
problemsHeading: (label) => `${label} — this ladder will not do what it looks like it does`,
|
|
990
|
+
problem: (kind, detail) =>
|
|
991
|
+
kind === 'escalate-on-a-success'
|
|
992
|
+
? `escalateOn names "${detail}", which "outcomes.success" declares a SUCCESS. This ladder pays twice for work that already worked, on every call, while looking exactly like a cost-saving measure.`
|
|
993
|
+
: kind === 'escalate-on-undeclared'
|
|
994
|
+
? `escalateOn names "${detail}", which "outcomes.values" does not declare. This ladder never fires, silently.`
|
|
995
|
+
: kind === 'tiers-not-cheapest-first'
|
|
996
|
+
? `"${detail}" is cheaper than the tier before it. That is not a ladder; it is a routing rule that escalates to something cheaper and reports a saving for it.`
|
|
997
|
+
: kind === 'duplicate-tier'
|
|
998
|
+
? `"${detail}" appears twice in tiers.`
|
|
999
|
+
: kind === 'unknown-model'
|
|
1000
|
+
? `"${detail}" is not in the price catalogue.`
|
|
1001
|
+
: `a ladder needs at least two tiers; this one has ${detail}.`,
|
|
1002
|
+
theDoubleSpend: () =>
|
|
1003
|
+
'An escalation pays twice: the cheap attempt is not refunded. So a ladder saves money only below its break-even escalation rate, and above it costs more than never having built one — which is why the rate is printed beside the measurement rather than left to be worked out in somebody\u2019s head.',
|
|
1004
|
+
notExecuted: () =>
|
|
1005
|
+
'Trazum does not run the escalation. A ladder escalates after a failure is known, which is after the answer came back and usually after something downstream judged it, so the retry belongs in your own loop. What is here is the policy and the arithmetic that says whether the policy is worth running.',
|
|
1006
|
+
},
|
|
1007
|
+
|
|
960
1008
|
gateway: {
|
|
961
1009
|
badProvider: (given, known) =>
|
|
962
1010
|
given === ''
|
package/src/i18n/es.ts
CHANGED
|
@@ -45,6 +45,7 @@ ${bold('USO')}
|
|
|
45
45
|
trazum models
|
|
46
46
|
trazum rules
|
|
47
47
|
trazum gateway <anthropic|openai> --on-cannot-tell <fail-open|fail-closed>
|
|
48
|
+
trazum ladder <log>
|
|
48
49
|
trazum feedback
|
|
49
50
|
trazum --version
|
|
50
51
|
|
|
@@ -617,6 +618,12 @@ ${bold('FICHERO DE CONFIGURACIÓN')}
|
|
|
617
618
|
spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — presupuestos en
|
|
618
619
|
dólares para "trazum profile". Una etiqueta con presupuesto y sin
|
|
619
620
|
llamadas se informa como no medida, nunca como aprobada
|
|
621
|
+
ladders { "support": { "tiers": ["claude-haiku-4-5", "claude-opus-5"],
|
|
622
|
+
"escalateOn": ["escalated"] } } — modelo barato primero, escalar
|
|
623
|
+
un fallo registrado a uno más caro. Los dos campos obligatorios.
|
|
624
|
+
"trazum ladder <log>" imprime la tasa de escalado de equilibrio
|
|
625
|
+
junto a la medida: un escalado paga dos veces, así que por encima
|
|
626
|
+
de esa tasa la escalera cuesta más que no haberla construido
|
|
620
627
|
outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
|
|
621
628
|
tu propio vocabulario para lo que pasó, y cuál de él cuenta como
|
|
622
629
|
acierto. Los dos son obligatorios: qué palabras significan éxito
|
|
@@ -993,6 +1000,47 @@ ${bold('EJEMPLOS')}
|
|
|
993
1000
|
`${path} existe y no se pudo interpretar, así que no se escribió nada encima. Arréglalo o muévelo primero.`,
|
|
994
1001
|
},
|
|
995
1002
|
|
|
1003
|
+
ladder: {
|
|
1004
|
+
heading: () => 'Escaleras de escalado',
|
|
1005
|
+
noLadders: () =>
|
|
1006
|
+
'No hay escaleras configuradas. Una escalera manda una carga a un modelo barato primero y escala un fallo registrado a uno m\u00e1s caro — a\u00f1ade "ladders" a trazum.config.json, por ejemplo {"support": {"tiers": ["claude-haiku-4-5", "claude-opus-5"], "escalateOn": ["escalated"]}}.',
|
|
1007
|
+
workload: (label) => label,
|
|
1008
|
+
arithmetic: (cheap, dear, breakEven) =>
|
|
1009
|
+
`${cheap} por llamada barata, ${dear} cara. Tasa de escalado de equilibrio: ${breakEven}.`,
|
|
1010
|
+
measured: (rate, escalations, calls) =>
|
|
1011
|
+
`Medido: ${rate} (${escalations} de ${calls} llamadas escalaron).`,
|
|
1012
|
+
saving: (delta) => `Ahorra ${delta} por llamada frente a no haberla construido.`,
|
|
1013
|
+
costing: (delta) =>
|
|
1014
|
+
`Cuesta ${delta} por llamada M\u00c1S que no haberla construido. Escalar por encima de la tasa de equilibrio significa pagar el intento barato y el caro en la mayor\u00eda de las llamadas.`,
|
|
1015
|
+
atBreakEven: (band) =>
|
|
1016
|
+
`A menos de ${band} del equilibrio, as\u00ed que no se afirma ning\u00fan signo. Dentro de esa banda la respuesta cambia con la variaci\u00f3n normal de una semana a otra, y decir "ahorra" el lunes y "cuesta" el jueves con la misma pol\u00edtica ense\u00f1a a ignorar la cifra.`,
|
|
1017
|
+
cannotTell: (why, calls) =>
|
|
1018
|
+
why === 'too-few-calls'
|
|
1019
|
+
? `A\u00fan no se puede decir: ${calls} llamadas llevaron un resultado declarado, y una tasa sobre tan pocas se mueve m\u00e1s con una llamada m\u00e1s que con nada que pudieras hacer.`
|
|
1020
|
+
: why === 'no-outcomes-recorded'
|
|
1021
|
+
? 'No se puede decir: nada en este log registr\u00f3 un resultado para esta carga, as\u00ed que no hay tasa de escalado con la que comparar.'
|
|
1022
|
+
: why === 'tier-unpriced'
|
|
1023
|
+
? 'No se puede decir: uno de los pelda\u00f1os no est\u00e1 en la tabla de precios, as\u00ed que la tasa de equilibrio no se puede calcular.'
|
|
1024
|
+
: 'No se puede decir: esta escalera no declara valores de escalado.',
|
|
1025
|
+
problemsHeading: (label) => `${label} — esta escalera no va a hacer lo que parece`,
|
|
1026
|
+
problem: (kind, detail) =>
|
|
1027
|
+
kind === 'escalate-on-a-success'
|
|
1028
|
+
? `escalateOn nombra "${detail}", que "outcomes.success" declara como \u00c9XITO. Esta escalera paga dos veces por trabajo que ya funcion\u00f3, en cada llamada, con el aspecto exacto de una medida de ahorro.`
|
|
1029
|
+
: kind === 'escalate-on-undeclared'
|
|
1030
|
+
? `escalateOn nombra "${detail}", que "outcomes.values" no declara. Esta escalera no se dispara nunca, en silencio.`
|
|
1031
|
+
: kind === 'tiers-not-cheapest-first'
|
|
1032
|
+
? `"${detail}" es m\u00e1s barato que el pelda\u00f1o anterior. Eso no es una escalera; es una regla de enrutado que escala a algo m\u00e1s barato y lo reporta como ahorro.`
|
|
1033
|
+
: kind === 'duplicate-tier'
|
|
1034
|
+
? `"${detail}" aparece dos veces en tiers.`
|
|
1035
|
+
: kind === 'unknown-model'
|
|
1036
|
+
? `"${detail}" no est\u00e1 en la tabla de precios.`
|
|
1037
|
+
: `una escalera necesita al menos dos pelda\u00f1os; esta tiene ${detail}.`,
|
|
1038
|
+
theDoubleSpend: () =>
|
|
1039
|
+
'Un escalado paga dos veces: el intento barato no se devuelve. As\u00ed que una escalera solo ahorra por debajo de su tasa de escalado de equilibrio, y por encima cuesta m\u00e1s que no haberla construido — por eso la tasa se imprime junto a la medici\u00f3n en vez de dejarla para calcularla de cabeza.',
|
|
1040
|
+
notExecuted: () =>
|
|
1041
|
+
'Trazum no ejecuta el escalado. Una escalera escala despu\u00e9s de conocer un fallo, que es despu\u00e9s de que llegue la respuesta y normalmente despu\u00e9s de que algo la juzgue, as\u00ed que el reintento va en tu propio bucle. Lo que hay aqu\u00ed es la pol\u00edtica y la aritm\u00e9tica que dice si merece la pena.',
|
|
1042
|
+
},
|
|
1043
|
+
|
|
996
1044
|
gateway: {
|
|
997
1045
|
badProvider: (given, known) =>
|
|
998
1046
|
given === ''
|
package/src/i18n/types.ts
CHANGED
|
@@ -264,6 +264,26 @@ export interface CliMessages {
|
|
|
264
264
|
* between somebody and their provider is trusted on nothing but what it says
|
|
265
265
|
* plainly at start-up.
|
|
266
266
|
*/
|
|
267
|
+
/**
|
|
268
|
+
* The ladder. Every line here exists to stop somebody reading "we route to
|
|
269
|
+
* the cheap model first" as a saving without the number that decides it.
|
|
270
|
+
*/
|
|
271
|
+
ladder: {
|
|
272
|
+
heading(): string;
|
|
273
|
+
noLadders(): string;
|
|
274
|
+
workload(label: string): string;
|
|
275
|
+
arithmetic(cheap: string, dear: string, breakEven: string): string;
|
|
276
|
+
measured(rate: string, escalations: string, calls: string): string;
|
|
277
|
+
saving(delta: string): string;
|
|
278
|
+
costing(delta: string): string;
|
|
279
|
+
atBreakEven(band: string): string;
|
|
280
|
+
cannotTell(why: string, calls: string): string;
|
|
281
|
+
problem(kind: string, detail: string): string;
|
|
282
|
+
problemsHeading(label: string): string;
|
|
283
|
+
theDoubleSpend(): string;
|
|
284
|
+
notExecuted(): string;
|
|
285
|
+
};
|
|
286
|
+
|
|
267
287
|
gateway: {
|
|
268
288
|
badProvider(given: string, known: string): string;
|
|
269
289
|
needsPolicy(policies: string): string;
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,9 @@ import {
|
|
|
39
39
|
DEFAULT_USAGE,
|
|
40
40
|
budgetPositions,
|
|
41
41
|
conform,
|
|
42
|
+
BREAK_EVEN_BAND,
|
|
43
|
+
ladderPosition,
|
|
44
|
+
validateLadder,
|
|
42
45
|
outcomeReport,
|
|
43
46
|
rankPerOutcome,
|
|
44
47
|
FAILURE_POLICIES,
|
|
@@ -570,6 +573,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
570
573
|
conform: ['contract', 'json'],
|
|
571
574
|
feedback: [],
|
|
572
575
|
gateway: ['on-cannot-tell', 'port', 'socket', 'pricing', 'pricing-live'],
|
|
576
|
+
ladder: ['pricing', 'pricing-live', 'since', 'until', 'label'],
|
|
573
577
|
where: [],
|
|
574
578
|
rules: [],
|
|
575
579
|
blame: ['limit', 'model', 'calls', 'output-tokens', 'batch', 'prompt', 'markdown-out'],
|
|
@@ -2190,6 +2194,136 @@ async function commandGateway(
|
|
|
2190
2194
|
console.log();
|
|
2191
2195
|
}
|
|
2192
2196
|
|
|
2197
|
+
/**
|
|
2198
|
+
* `trazum ladder <log>` — is the ladder saving money, or is it a bill?
|
|
2199
|
+
*
|
|
2200
|
+
* The one number this command exists to print is the **break-even escalation
|
|
2201
|
+
* rate**. "We route to the cheap model first" describes a policy that saves
|
|
2202
|
+
* money and a policy that costs money equally well; only the rate separates
|
|
2203
|
+
* them, and nobody works it out in their head because the shape of the
|
|
2204
|
+
* arithmetic is not obvious — an escalation pays twice, since the cheap
|
|
2205
|
+
* attempt is not refunded.
|
|
2206
|
+
*/
|
|
2207
|
+
async function commandLadder(
|
|
2208
|
+
args: Args,
|
|
2209
|
+
config: TrazumConfig,
|
|
2210
|
+
pricing: PricingCatalogue,
|
|
2211
|
+
t: CliMessages,
|
|
2212
|
+
): Promise<void> {
|
|
2213
|
+
const path = args.positional[0];
|
|
2214
|
+
if (path === undefined) {
|
|
2215
|
+
throw new Error(t.errors.missingInputFile());
|
|
2216
|
+
}
|
|
2217
|
+
const report = profileUsage(await readUsageLog(path, t), { catalogue: pricing });
|
|
2218
|
+
const ladders = config.ladders ?? {};
|
|
2219
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2220
|
+
const pct = (value: number): string => `${(value * 100).toFixed(1)}%`;
|
|
2221
|
+
|
|
2222
|
+
console.log();
|
|
2223
|
+
console.log(c.bold(t.ladder.heading()));
|
|
2224
|
+
if (Object.keys(ladders).length === 0) {
|
|
2225
|
+
console.log(` ${c.dim(wrap(t.ladder.noLadders(), 74, ' '))}`);
|
|
2226
|
+
console.log();
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
console.log(` ${c.dim(wrap(t.ladder.theDoubleSpend(), 74, ' '))}`);
|
|
2230
|
+
console.log();
|
|
2231
|
+
|
|
2232
|
+
const vocabulary = config.outcomes ?? null;
|
|
2233
|
+
let anyProblem = false;
|
|
2234
|
+
|
|
2235
|
+
for (const [label, policy] of Object.entries(ladders)) {
|
|
2236
|
+
/**
|
|
2237
|
+
* Validated before it is measured, and loudly.
|
|
2238
|
+
*
|
|
2239
|
+
* A ladder that escalates on a value declared a *success* pays twice for
|
|
2240
|
+
* work that already worked, on every call, while looking exactly like a
|
|
2241
|
+
* cost-saving measure in the config. Printing its measured position first
|
|
2242
|
+
* would bury that under a number.
|
|
2243
|
+
*/
|
|
2244
|
+
const problems = validateLadder(policy, vocabulary, pricing);
|
|
2245
|
+
if (problems.length > 0) {
|
|
2246
|
+
anyProblem = true;
|
|
2247
|
+
console.log(` ${c.red('✗')} ${c.bold(t.ladder.problemsHeading(label))}`);
|
|
2248
|
+
for (const problem of problems) {
|
|
2249
|
+
const detail =
|
|
2250
|
+
'value' in problem
|
|
2251
|
+
? problem.value
|
|
2252
|
+
: 'model' in problem
|
|
2253
|
+
? problem.model
|
|
2254
|
+
: String(problem.tiers);
|
|
2255
|
+
console.log(` ${wrap(t.ladder.problem(problem.kind, detail), 70, ' ')}`);
|
|
2256
|
+
}
|
|
2257
|
+
console.log();
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const slice = report.outcomeTallyByLabel.find((entry) => entry.label === label);
|
|
2262
|
+
const breakdown = report.byLabel.find((entry) => entry.label === label);
|
|
2263
|
+
/**
|
|
2264
|
+
* The shape of the work comes from the measured calls, so the break-even
|
|
2265
|
+
* rate is priced against what this workload actually sends rather than
|
|
2266
|
+
* against a token count somebody guessed at.
|
|
2267
|
+
*/
|
|
2268
|
+
const calls = breakdown?.breakdown.calls ?? 0;
|
|
2269
|
+
const shape =
|
|
2270
|
+
breakdown === undefined || calls === 0
|
|
2271
|
+
? { inputTokens: 0, outputTokens: 0 }
|
|
2272
|
+
: {
|
|
2273
|
+
inputTokens: Math.round(
|
|
2274
|
+
(breakdown.breakdown.inputTokens +
|
|
2275
|
+
breakdown.breakdown.cacheReadTokens +
|
|
2276
|
+
breakdown.breakdown.cacheWriteTokens) /
|
|
2277
|
+
calls,
|
|
2278
|
+
),
|
|
2279
|
+
outputTokens: Math.round(breakdown.breakdown.outputTokens / calls),
|
|
2280
|
+
};
|
|
2281
|
+
|
|
2282
|
+
const empty = { byValue: [], recorded: 0, parsed: 0, unrecordedUsd: 0 };
|
|
2283
|
+
const position = ladderPosition(policy, slice?.tally ?? empty, shape, vocabulary, pricing);
|
|
2284
|
+
|
|
2285
|
+
console.log(` ${c.bold(t.ladder.workload(label))} ${c.dim(policy.tiers.join(' → '))}`);
|
|
2286
|
+
console.log(
|
|
2287
|
+
` ${c.dim(
|
|
2288
|
+
t.ladder.arithmetic(
|
|
2289
|
+
formatUsd(position.arithmetic.cheapUsd),
|
|
2290
|
+
formatUsd(position.arithmetic.dearUsd),
|
|
2291
|
+
position.arithmetic.breakEvenRate === null ? '—' : pct(position.arithmetic.breakEvenRate),
|
|
2292
|
+
),
|
|
2293
|
+
)}`,
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
if (position.verdict === 'cannot-tell') {
|
|
2297
|
+
console.log(
|
|
2298
|
+
` ${c.yellow('?')} ${wrap(t.ladder.cannotTell(position.unknown ?? '', n(position.calls)), 70, ' ')}`,
|
|
2299
|
+
);
|
|
2300
|
+
} else {
|
|
2301
|
+
console.log(
|
|
2302
|
+
` ${t.ladder.measured(pct(position.measuredRate ?? 0), n(position.escalations), n(position.calls))}`,
|
|
2303
|
+
);
|
|
2304
|
+
const delta = formatUsd(Math.abs(position.deltaUsdPerCall ?? 0));
|
|
2305
|
+
if (position.verdict === 'saving') {
|
|
2306
|
+
console.log(` ${c.green('✓')} ${wrap(t.ladder.saving(delta), 70, ' ')}`);
|
|
2307
|
+
} else if (position.verdict === 'costing') {
|
|
2308
|
+
console.log(` ${c.red('✗')} ${wrap(t.ladder.costing(delta), 70, ' ')}`);
|
|
2309
|
+
} else {
|
|
2310
|
+
console.log(` ${c.dim('·')} ${wrap(t.ladder.atBreakEven(pct(BREAK_EVEN_BAND)), 70, ' ')}`);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
console.log();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
console.log(` ${c.dim(wrap(t.ladder.notExecuted(), 74, ' '))}`);
|
|
2317
|
+
console.log();
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* A misconfigured ladder fails the command, because it is the one finding
|
|
2321
|
+
* here that is wrong *now* rather than a measurement somebody should look
|
|
2322
|
+
* at. Everything else exits 0: this is a survey, like `doctor`.
|
|
2323
|
+
*/
|
|
2324
|
+
if (anyProblem) process.exitCode = 1;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2193
2327
|
function commandModels(t: CliMessages, pricing: PricingCatalogue): void {
|
|
2194
2328
|
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2195
2329
|
const col = t.models.columns;
|
|
@@ -8390,6 +8524,9 @@ async function main(): Promise<void> {
|
|
|
8390
8524
|
case 'models':
|
|
8391
8525
|
commandModels(t, pricing);
|
|
8392
8526
|
break;
|
|
8527
|
+
case 'ladder':
|
|
8528
|
+
await commandLadder(args, config, pricing, t);
|
|
8529
|
+
break;
|
|
8393
8530
|
case 'gateway':
|
|
8394
8531
|
await commandGateway(args, config, configDir, pricing, t);
|
|
8395
8532
|
break;
|