estela 0.3.3 → 0.4.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/README.md +97 -0
- package/dist/billing/assign.d.ts +11 -1
- package/dist/billing/assign.js +59 -1
- package/dist/billing/sessionize.d.ts +6 -0
- package/dist/billing/sessionize.js +24 -5
- package/dist/cli.js +1084 -8
- package/dist/db/schema.js +100 -0
- package/dist/db/store.d.ts +46 -0
- package/dist/db/store.js +179 -5
- package/dist/delivery/clients.d.ts +52 -0
- package/dist/delivery/clients.js +94 -0
- package/dist/delivery/github.d.ts +21 -0
- package/dist/delivery/github.js +123 -0
- package/dist/delivery/report.d.ts +55 -0
- package/dist/delivery/report.js +124 -0
- package/dist/delivery/source.d.ts +68 -0
- package/dist/delivery/source.js +3 -0
- package/dist/export/csv.d.ts +14 -0
- package/dist/export/csv.js +78 -0
- package/dist/export/finance.d.ts +79 -0
- package/dist/export/finance.js +208 -0
- package/dist/i18n/en.js +171 -0
- package/dist/metrics/spend.d.ts +83 -0
- package/dist/metrics/spend.js +167 -0
- package/dist/metrics/undelivered.d.ts +104 -0
- package/dist/metrics/undelivered.js +199 -0
- package/dist/server.js +68 -8
- package/dist/setup.js +1 -1
- package/dist/watchers/git.d.ts +40 -0
- package/dist/watchers/git.js +87 -0
- package/node_modules/@estela/shared/dist/money.d.ts +2 -0
- package/node_modules/@estela/shared/dist/money.d.ts.map +1 -1
- package/node_modules/@estela/shared/dist/money.js +7 -7
- package/node_modules/@estela/shared/dist/money.js.map +1 -1
- package/node_modules/@estela/shared/dist/types.d.ts +7 -0
- package/node_modules/@estela/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@estela/shared/dist/types.js.map +1 -1
- package/node_modules/@estela/shared/dist/workitems.d.ts +10 -0
- package/node_modules/@estela/shared/dist/workitems.d.ts.map +1 -1
- package/node_modules/@estela/shared/dist/workitems.js.map +1 -1
- package/package.json +1 -1
- package/web/app.css +4 -0
- package/web/app.js +67 -0
- package/web/i18n.js +24 -0
package/dist/cli.js
CHANGED
|
@@ -58,6 +58,7 @@ const demo_js_1 = require("./demo.js");
|
|
|
58
58
|
const doctor_js_1 = require("./doctor.js");
|
|
59
59
|
const setup_js_1 = require("./setup.js");
|
|
60
60
|
const csv_js_1 = require("./export/csv.js");
|
|
61
|
+
const finance_js_1 = require("./export/finance.js");
|
|
61
62
|
const paths_js_1 = require("./paths.js");
|
|
62
63
|
const clockify_js_1 = require("./export/clockify.js");
|
|
63
64
|
const invoice_pdf_js_1 = require("./export/invoice-pdf.js");
|
|
@@ -65,6 +66,8 @@ const share_js_1 = require("./export/share.js");
|
|
|
65
66
|
const server_js_1 = require("./server.js");
|
|
66
67
|
const updateCheck_js_1 = require("./updateCheck.js");
|
|
67
68
|
const plan_js_1 = require("./metrics/plan.js");
|
|
69
|
+
const undelivered_js_1 = require("./metrics/undelivered.js");
|
|
70
|
+
const spend_js_1 = require("./metrics/spend.js");
|
|
68
71
|
const plan_blocks_js_1 = require("./plan-blocks.js");
|
|
69
72
|
const plan_prices_js_1 = require("./plan-prices.js");
|
|
70
73
|
const plan_report_js_1 = require("./plan-report.js");
|
|
@@ -82,6 +85,10 @@ Object.defineProperty(exports, "breakdownOfTurn", { enumerable: true, get: funct
|
|
|
82
85
|
const agents_js_1 = require("./watchers/agents.js");
|
|
83
86
|
const git_js_1 = require("./watchers/git.js");
|
|
84
87
|
const identity_js_1 = require("./watchers/identity.js");
|
|
88
|
+
const project_of_js_1 = require("./project-of.js");
|
|
89
|
+
const github_js_1 = require("./delivery/github.js");
|
|
90
|
+
const report_js_1 = require("./delivery/report.js");
|
|
91
|
+
const clients_js_1 = require("./delivery/clients.js");
|
|
85
92
|
const index_js_1 = require("./i18n/index.js");
|
|
86
93
|
const dates_js_1 = require("./export/dates.js");
|
|
87
94
|
const quota_js_1 = require("./metrics/quota.js");
|
|
@@ -124,10 +131,40 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
124
131
|
vez de perder el bloque o facturarlo por sorpresa.
|
|
125
132
|
estela project reopen --project <id>
|
|
126
133
|
estela project tracker --project <id> [--system jira|azure|github|gitlab|none]
|
|
127
|
-
[--prefix PROJ,OPS]
|
|
134
|
+
[--prefix PROJ,OPS] [--plan-label <etiqueta>|none]
|
|
135
|
+
[--client-label <prefijo>|none]
|
|
128
136
|
El gestor de tareas del proyecto, para reconocer sus tickets en la
|
|
129
137
|
rama y los commits (feature/1234-login, AB#1234, PROJ-12). Luego
|
|
130
|
-
"estela import" lo aplica a lo ya capturado.
|
|
138
|
+
"estela import" lo aplica a lo ya capturado. --plan-label es la
|
|
139
|
+
etiqueta que marca lo planificado para "estela delivery", y
|
|
140
|
+
--client-label el prefijo de las peticiones de clientes (cliente:).
|
|
141
|
+
estela delivery [--project <id>] [--month YYYY-MM | --from YYYY-MM-DD [--to YYYY-MM-DD]]
|
|
142
|
+
[--label <etiqueta> | --milestone <nombre>] [--offline] [--json]
|
|
143
|
+
Planificado frente a entregado en un periodo (por defecto, este mes),
|
|
144
|
+
con el coste de IA de cada parte: lo del plan que se entregó, lo que
|
|
145
|
+
no, y lo entregado que no estaba en el plan. Lee las issues y los PR
|
|
146
|
+
de GitHub con "gh", solo en un proyecto con --system github; es la
|
|
147
|
+
única llamada fuera de tu máquina, y --offline la evita. Sin gh usa
|
|
148
|
+
lo leído la última vez o, si nunca se leyó, lo entregado según git.
|
|
149
|
+
estela delivery --by client [--project <id>] [--month YYYY-MM | --from …]
|
|
150
|
+
El gasto de IA del periodo en peticiones de cada cliente frente al
|
|
151
|
+
resto: una issue o un PR con la etiqueta "cliente:<nombre>" es una
|
|
152
|
+
petición de ese cliente, y un PR hereda las etiquetas de las issues
|
|
153
|
+
que cierra. Una hora que sirvió a varios va a "Varios", sin repartir.
|
|
154
|
+
No hay ingresos ni contratos: solo une el gasto con quién lo pidió.
|
|
155
|
+
estela summary [--month YYYY-MM | --from YYYY-MM-DD [--to YYYY-MM-DD]]
|
|
156
|
+
[--client <id>] [--project <id>] [--format md|csv|json]
|
|
157
|
+
[--out <fichero>] [--stale-days 14] [--lang es|en]
|
|
158
|
+
Informe para finanzas, en el lenguaje de quien no programa: lo
|
|
159
|
+
pagado en IA, por cliente y proyecto, lo entregado (y el plan, si
|
|
160
|
+
"estela delivery" lo leyó), lo pedido por cada cliente y lo que no
|
|
161
|
+
llegó a entregarse. Los detalles técnicos van al final. Por defecto,
|
|
162
|
+
el último mes cerrado. Con --out, el formato sale de la extensión.
|
|
163
|
+
No llama a nada fuera de tu máquina.
|
|
164
|
+
estela client alias [--client <id> --label <nombre>] [--remove --label <nombre>]
|
|
165
|
+
Qué cliente de Estela es cada nombre de las etiquetas de cliente.
|
|
166
|
+
Sin alias se compara con el id y el nombre del cliente (sin
|
|
167
|
+
mayúsculas, acentos ni guiones). Sin argumentos, lista los alias.
|
|
131
168
|
estela rate set --project <id> --rate <importe> [--from YYYY-MM-DD]
|
|
132
169
|
estela budget --project <id> [--amount <dólares>|none]
|
|
133
170
|
Presupuesto mensual de IA. Sin --amount, consulta el actual. Alimenta
|
|
@@ -160,6 +197,24 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
160
197
|
estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
|
|
161
198
|
Pone a mano los tickets de un bloque. El import ya no los cambia.
|
|
162
199
|
estela ai-cost Reparto de tu cuota entre proyectos.
|
|
200
|
+
estela ai-cost --by branch|item [--project <id>] [--client <id>]
|
|
201
|
+
[--month YYYY-MM | --from YYYY-MM-DD --to YYYY-MM-DD]
|
|
202
|
+
[--json | --csv <fichero.csv>]
|
|
203
|
+
Coste de IA y horas de cada entrega: por rama, o por ticket y PR.
|
|
204
|
+
Dos cifras: a precio de API (mide uso) y tu cuota repartida (dinero
|
|
205
|
+
pagado). Cada fila dice en qué se basa: la rama que anotó el agente,
|
|
206
|
+
el número en la rama, #12 en un commit, lo puesto a mano o el
|
|
207
|
+
commit de merge del PR. Lo que no tiene base va a "Sin atribuir",
|
|
208
|
+
entero. Solo lee: no recalcula días aprobados ni facturados.
|
|
209
|
+
estela ai-cost --undelivered [--stale-days 14] [--project <id>] [--client <id>]
|
|
210
|
+
[--month YYYY-MM | --from YYYY-MM-DD --to YYYY-MM-DD]
|
|
211
|
+
[--json | --csv <fichero.csv>]
|
|
212
|
+
El gasto de IA que no ha llegado a la rama principal, por proyecto, en
|
|
213
|
+
cifra y en % del gasto: sesiones sin ningún commit tuyo (durante ella
|
|
214
|
+
ni en los 30 min siguientes) y ramas que existen, no están fusionadas
|
|
215
|
+
y llevan --stale-days días sin actividad. Es una señal para revisar,
|
|
216
|
+
no un juicio. No cuenta: las ramas borradas sin merge (pudo ser un
|
|
217
|
+
squash) ni las sesiones anteriores a tu primer commit en el repo.
|
|
163
218
|
estela quota [--json] [--no-refresh] [--weighted]
|
|
164
219
|
¿Qué proyecto se come tu cuota de Claude Code o Codex? Tokens por
|
|
165
220
|
proyecto en la ventana de 5 h actual y en la semana (de lunes a
|
|
@@ -335,10 +390,41 @@ estela — time tracking for AI-assisted development
|
|
|
335
390
|
silently dropping the block or billing it by surprise.
|
|
336
391
|
estela project reopen --project <id>
|
|
337
392
|
estela project tracker --project <id> [--system jira|azure|github|gitlab|none]
|
|
338
|
-
[--prefix PROJ,OPS]
|
|
393
|
+
[--prefix PROJ,OPS] [--plan-label <label>|none]
|
|
394
|
+
[--client-label <prefix>|none]
|
|
339
395
|
The project's issue tracker, to recognise its tickets in the branch
|
|
340
396
|
and commits (feature/1234-login, AB#1234, PROJ-12). Then
|
|
341
|
-
"estela import" applies it to what's already captured.
|
|
397
|
+
"estela import" applies it to what's already captured. --plan-label
|
|
398
|
+
is the label that marks what's planned, for "estela delivery", and
|
|
399
|
+
--client-label the prefix of client requests (cliente:).
|
|
400
|
+
estela delivery [--project <id>] [--month YYYY-MM | --from YYYY-MM-DD [--to YYYY-MM-DD]]
|
|
401
|
+
[--label <label> | --milestone <name>] [--offline] [--json]
|
|
402
|
+
Planned versus delivered in a period (this month by default), with
|
|
403
|
+
the AI cost of each part: what was planned and delivered, what
|
|
404
|
+
wasn't, and what was delivered without being planned. It reads
|
|
405
|
+
GitHub issues and PRs with "gh", only for a project with --system
|
|
406
|
+
github; it's the only call off your machine, and --offline avoids
|
|
407
|
+
it. Without gh it uses what was read last time or, if never read,
|
|
408
|
+
what git says was delivered.
|
|
409
|
+
estela delivery --by client [--project <id>] [--month YYYY-MM | --from …]
|
|
410
|
+
The period's AI spend on each client's requests versus the rest: an
|
|
411
|
+
issue or PR labelled "cliente:<name>" is that client's request, and a
|
|
412
|
+
PR inherits the labels of the issues it closes. An hour that served
|
|
413
|
+
several goes to "Several", unsplit. No revenue or contracts: it only
|
|
414
|
+
links spend to who asked for it.
|
|
415
|
+
estela summary [--month YYYY-MM | --from YYYY-MM-DD [--to YYYY-MM-DD]]
|
|
416
|
+
[--client <id>] [--project <id>] [--format md|csv|json]
|
|
417
|
+
[--out <file>] [--stale-days 14] [--lang es|en]
|
|
418
|
+
A report for finance, in the words of someone who doesn't code: what
|
|
419
|
+
was paid for AI, by client and project, what was delivered (and the
|
|
420
|
+
plan, if "estela delivery" read it), what each client asked for, and
|
|
421
|
+
what never got delivered. Technical details go at the end. Defaults
|
|
422
|
+
to the last closed month. With --out, the format comes from the
|
|
423
|
+
extension. It never calls anything off your machine.
|
|
424
|
+
estela client alias [--client <id> --label <name>] [--remove --label <name>]
|
|
425
|
+
Which Estela client each name in the client labels is. Without an
|
|
426
|
+
alias it's matched against the client's id and name (ignoring case,
|
|
427
|
+
accents and hyphens). With no arguments, lists the aliases.
|
|
342
428
|
estela rate set --project <id> --rate <amount> [--from YYYY-MM-DD]
|
|
343
429
|
estela budget --project <id> [--amount <dollars>|none]
|
|
344
430
|
Monthly AI budget. Without --amount, shows the current one. Feeds the
|
|
@@ -371,6 +457,25 @@ estela — time tracking for AI-assisted development
|
|
|
371
457
|
estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
|
|
372
458
|
Sets a block's tickets by hand. Imports won't change them anymore.
|
|
373
459
|
estela ai-cost How your subscription splits across projects.
|
|
460
|
+
estela ai-cost --by branch|item [--project <id>] [--client <id>]
|
|
461
|
+
[--month YYYY-MM | --from YYYY-MM-DD --to YYYY-MM-DD]
|
|
462
|
+
[--json | --csv <file.csv>]
|
|
463
|
+
AI cost and hours of each delivery: by branch, or by ticket and PR.
|
|
464
|
+
Two figures: at API prices (measures usage) and your subscription
|
|
465
|
+
split by usage (money paid). Each row says what it's based on: the
|
|
466
|
+
branch the agent recorded, the number in the branch, #12 in a
|
|
467
|
+
commit, set by hand, or the PR's merge commit. Anything without a
|
|
468
|
+
basis goes to "Unattributed", whole. Read-only: it never recalculates
|
|
469
|
+
approved or invoiced days.
|
|
470
|
+
estela ai-cost --undelivered [--stale-days 14] [--project <id>] [--client <id>]
|
|
471
|
+
[--month YYYY-MM | --from YYYY-MM-DD --to YYYY-MM-DD]
|
|
472
|
+
[--json | --csv <file.csv>]
|
|
473
|
+
AI spend that hasn't reached the main branch, per project, as a figure
|
|
474
|
+
and as a % of spend: sessions with no commit of yours (during them or
|
|
475
|
+
in the 30 min after) and branches that exist, aren't merged, and have
|
|
476
|
+
been idle for --stale-days days. It's a signal to review, not a
|
|
477
|
+
verdict. Not counted: branches deleted with no merge (it may have been
|
|
478
|
+
a squash) and sessions before your first commit in the repo.
|
|
374
479
|
estela quota [--json] [--no-refresh] [--weighted]
|
|
375
480
|
Which project is eating your Claude Code or Codex quota? Tokens per
|
|
376
481
|
project in the current 5-hour window and this week (Monday to
|
|
@@ -877,6 +982,7 @@ async function cmdImport(args, dbPath) {
|
|
|
877
982
|
if (!root)
|
|
878
983
|
continue;
|
|
879
984
|
savedCommits += store.saveCommits(db, await (0, git_js_1.readCommits)(root, since ? { since } : {}));
|
|
985
|
+
store.saveMerges(db, await (0, git_js_1.readMerges)(root, since ? { since } : {}));
|
|
880
986
|
}
|
|
881
987
|
// Todo lo conocido, no solo lo leído ahora (ver knownTurns). Con --repo, solo
|
|
882
988
|
// ese repo, que es lo que se ha pedido.
|
|
@@ -1089,6 +1195,24 @@ function cmdProjectTracker(args, dbPath) {
|
|
|
1089
1195
|
if (!project)
|
|
1090
1196
|
throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
|
|
1091
1197
|
const system = str(args, "system");
|
|
1198
|
+
if (!system && "client-label" in args.flags) {
|
|
1199
|
+
if (!project.tracker)
|
|
1200
|
+
throw new UserError((0, index_js_1.tr) `Primero el gestor: estela project tracker --project ${projectId} --system github`);
|
|
1201
|
+
const prefix = clientLabelArg(args);
|
|
1202
|
+
const { clientLabel: _, ...rest } = project.tracker;
|
|
1203
|
+
store.setProjectTracker(db, projectId, prefix ? { ...rest, clientLabel: prefix } : rest);
|
|
1204
|
+
console.log((0, index_js_1.tr) `Las peticiones de clientes de "${project.name}" llevan la etiqueta "${prefix ?? DEFAULT_CLIENT_LABEL}<nombre>".`);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (!system && "plan-label" in args.flags) {
|
|
1208
|
+
if (!project.tracker)
|
|
1209
|
+
throw new UserError((0, index_js_1.tr) `Primero el gestor: estela project tracker --project ${projectId} --system github`);
|
|
1210
|
+
const label = planLabelArg(args);
|
|
1211
|
+
store.setProjectTracker(db, projectId, withPlanLabel(project.tracker, label));
|
|
1212
|
+
console.log(label ? (0, index_js_1.tr) `Lo planificado de "${project.name}" es lo que lleva la etiqueta "${label}".`
|
|
1213
|
+
: (0, index_js_1.tr) `"${project.name}" ya no tiene etiqueta de plan.`);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1092
1216
|
if (!system) {
|
|
1093
1217
|
console.log(project.tracker
|
|
1094
1218
|
? (0, index_js_1.tr) `"${project.name}" usa ${project.tracker.system}${project.tracker.prefixes.length ? " (" + project.tracker.prefixes.join(", ") + ")" : ""}.`
|
|
@@ -1107,7 +1231,9 @@ function cmdProjectTracker(args, dbPath) {
|
|
|
1107
1231
|
if (prefixes.some((p) => !/^[A-Z][A-Z0-9]*$/.test(p))) {
|
|
1108
1232
|
throw new UserError((0, index_js_1.tr) `Prefijo inválido: tienen que ser letras y números, como PROJ u OPS.`);
|
|
1109
1233
|
}
|
|
1110
|
-
|
|
1234
|
+
const label = "plan-label" in args.flags ? planLabelArg(args) : project.tracker?.planLabel;
|
|
1235
|
+
const clientLabel = "client-label" in args.flags ? clientLabelArg(args) : project.tracker?.clientLabel;
|
|
1236
|
+
store.setProjectTracker(db, projectId, withPlanLabel({ system: system, prefixes, ...(clientLabel ? { clientLabel } : {}) }, label));
|
|
1111
1237
|
console.log((0, index_js_1.tr) `"${project.name}" usa ${system}${prefixes.length ? " (" + prefixes.join(", ") + ")" : ""}.`);
|
|
1112
1238
|
}
|
|
1113
1239
|
console.log((0, index_js_1.tr) `Para aplicarlo a lo ya capturado: estela import`);
|
|
@@ -1116,6 +1242,65 @@ function cmdProjectTracker(args, dbPath) {
|
|
|
1116
1242
|
db.close();
|
|
1117
1243
|
}
|
|
1118
1244
|
}
|
|
1245
|
+
/** `--plan-label <etiqueta>`, o `none` para quitarla. */
|
|
1246
|
+
function planLabelArg(args) {
|
|
1247
|
+
const label = str(args, "plan-label")?.trim();
|
|
1248
|
+
if (!label)
|
|
1249
|
+
throw new UserError((0, index_js_1.tr) `Falta la etiqueta: --plan-label <etiqueta> (o none para quitarla).`);
|
|
1250
|
+
return label === "none" ? undefined : label;
|
|
1251
|
+
}
|
|
1252
|
+
/** `--client-label <prefijo>`, o `none` para volver al de por defecto. */
|
|
1253
|
+
function clientLabelArg(args) {
|
|
1254
|
+
const prefix = str(args, "client-label")?.trim();
|
|
1255
|
+
if (!prefix)
|
|
1256
|
+
throw new UserError((0, index_js_1.tr) `Falta el prefijo: --client-label <prefijo> (o none para volver a "cliente:").`);
|
|
1257
|
+
return prefix === "none" ? undefined : prefix;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* `estela client alias` — qué cliente de Estela es cada nombre de las
|
|
1261
|
+
* etiquetas `cliente:<nombre>`. Sin alias se prueba con el id y el nombre del
|
|
1262
|
+
* cliente; si no coincide ninguno, el informe lo enseña sin mapear.
|
|
1263
|
+
*/
|
|
1264
|
+
function cmdClientAlias(args, dbPath) {
|
|
1265
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1266
|
+
try {
|
|
1267
|
+
const label = str(args, "label");
|
|
1268
|
+
if ("remove" in args.flags) {
|
|
1269
|
+
if (!label)
|
|
1270
|
+
throw new UserError((0, index_js_1.tr) `Falta --label <nombre>`);
|
|
1271
|
+
console.log(store.removeClientAlias(db, label)
|
|
1272
|
+
? (0, index_js_1.tr) `"${label}" ya no apunta a ningún cliente.` : (0, index_js_1.tr) `"${label}" no tenía alias.`);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const clientId = str(args, "client");
|
|
1276
|
+
if (!label && !clientId) {
|
|
1277
|
+
const aliases = store.listClientAliases(db);
|
|
1278
|
+
if (!aliases.length) {
|
|
1279
|
+
console.log((0, index_js_1.tr) `Sin alias. Las etiquetas se comparan con el id y el nombre de cada cliente.`);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
for (const a of aliases)
|
|
1283
|
+
console.log(` ${a.label.padEnd(24)} → ${store.getClient(db, a.clientId)?.name ?? a.clientId}`);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (!label || !clientId)
|
|
1287
|
+
throw new UserError((0, index_js_1.tr) `Hacen falta --client <id> y --label <nombre>.`);
|
|
1288
|
+
const client = store.getClient(db, clientId);
|
|
1289
|
+
if (!client)
|
|
1290
|
+
throw new UserError((0, index_js_1.tr) `No existe el cliente "${clientId}".`);
|
|
1291
|
+
if (!store.aliasKey(label))
|
|
1292
|
+
throw new UserError((0, index_js_1.tr) `La etiqueta necesita al menos una letra o un número.`);
|
|
1293
|
+
store.setClientAlias(db, label, clientId);
|
|
1294
|
+
console.log((0, index_js_1.tr) `La etiqueta "${label}" es ${client.name}.`);
|
|
1295
|
+
}
|
|
1296
|
+
finally {
|
|
1297
|
+
db.close();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function withPlanLabel(tracker, label) {
|
|
1301
|
+
const { planLabel: _, ...rest } = tracker;
|
|
1302
|
+
return label ? { ...rest, planLabel: label } : rest;
|
|
1303
|
+
}
|
|
1119
1304
|
/**
|
|
1120
1305
|
* `estela entry link` — pone a mano los tickets de un bloque, cuando la rama
|
|
1121
1306
|
* no lo decía o lo decía mal. Desde entonces el import no los toca;
|
|
@@ -2046,7 +2231,7 @@ async function cmdExport(args, dbPath) {
|
|
|
2046
2231
|
* `--month YYYY-MM`, o `--from`/`--to` en fechas locales. Sin ninguno, todo:
|
|
2047
2232
|
* es lo que hacía `estela export` antes de tener rango.
|
|
2048
2233
|
*/
|
|
2049
|
-
function
|
|
2234
|
+
function periodOf(args) {
|
|
2050
2235
|
const month = str(args, "month");
|
|
2051
2236
|
if (month && !/^\d{4}-\d{2}$/.test(month))
|
|
2052
2237
|
throw new UserError((0, index_js_1.tr) `--month va como YYYY-MM.`);
|
|
@@ -2056,6 +2241,10 @@ function entriesInRange(entries, args) {
|
|
|
2056
2241
|
if (d && !/^\d{4}-\d{2}-\d{2}$/.test(d))
|
|
2057
2242
|
throw new UserError((0, index_js_1.tr) `Fecha inválida: ${d}`);
|
|
2058
2243
|
}
|
|
2244
|
+
return { from, to };
|
|
2245
|
+
}
|
|
2246
|
+
function entriesInRange(entries, args) {
|
|
2247
|
+
const { from, to } = periodOf(args);
|
|
2059
2248
|
return entries.filter((e) => {
|
|
2060
2249
|
const day = (0, shared_1.localDate)(e.startedAt);
|
|
2061
2250
|
return (!from || day >= from) && (!to || day <= to);
|
|
@@ -2111,7 +2300,18 @@ function cmdSubscriptionAdd(args, dbPath) {
|
|
|
2111
2300
|
}
|
|
2112
2301
|
}
|
|
2113
2302
|
/** Reparto de la cuota entre proyectos, mes a mes. */
|
|
2114
|
-
function cmdAiCost(dbPath) {
|
|
2303
|
+
async function cmdAiCost(args, dbPath) {
|
|
2304
|
+
if ("by" in args.flags && "undelivered" in args.flags) {
|
|
2305
|
+
throw new UserError((0, index_js_1.tr) `--by y --undelivered van por separado: son dos informes distintos.`);
|
|
2306
|
+
}
|
|
2307
|
+
if ("by" in args.flags) {
|
|
2308
|
+
cmdAiCostBy(args, dbPath);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if ("undelivered" in args.flags) {
|
|
2312
|
+
await cmdAiCostUndelivered(args, dbPath);
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2115
2315
|
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
2116
2316
|
try {
|
|
2117
2317
|
const subs = store.listSubscriptions(db);
|
|
@@ -2147,6 +2347,873 @@ function cmdAiCost(dbPath) {
|
|
|
2147
2347
|
db.close();
|
|
2148
2348
|
}
|
|
2149
2349
|
}
|
|
2350
|
+
/**
|
|
2351
|
+
* `estela ai-cost --by branch|item` — el coste de IA de cada entrega.
|
|
2352
|
+
*
|
|
2353
|
+
* Lee las imputaciones ya guardadas (ver metrics/spend.ts): no recalcula nada
|
|
2354
|
+
* ni toca días aprobados o facturados.
|
|
2355
|
+
*/
|
|
2356
|
+
function cmdAiCostBy(args, dbPath) {
|
|
2357
|
+
const raw = args.flags["by"];
|
|
2358
|
+
if (raw !== "branch" && raw !== "item")
|
|
2359
|
+
throw new UserError((0, index_js_1.tr) `--by va como branch o item.`);
|
|
2360
|
+
const by = raw;
|
|
2361
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
2362
|
+
try {
|
|
2363
|
+
const projects = selectedProjects(db, args);
|
|
2364
|
+
const entries = entriesInRange(projects.flatMap((p) => store.getTimeEntries(db, p.id)), args);
|
|
2365
|
+
const subscriptions = store.listSubscriptions(db);
|
|
2366
|
+
const monthTotals = shareDenominators(db);
|
|
2367
|
+
const reports = (0, spend_js_1.spendByUnit)({ entries, projects, merges: store.listMerges(db), subscriptions, monthTotals }, by);
|
|
2368
|
+
const csvOut = str(args, "csv");
|
|
2369
|
+
if ("csv" in args.flags && !csvOut)
|
|
2370
|
+
throw new UserError((0, index_js_1.tr) `Falta el fichero: --csv <fichero.csv>`);
|
|
2371
|
+
if (csvOut) {
|
|
2372
|
+
(0, node_fs_1.writeFileSync)(csvOut, (0, csv_js_1.spendToCsv)(reports, by), "utf8");
|
|
2373
|
+
console.log((0, index_js_1.tr) `CSV guardado en ${csvOut}`);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
if ("json" in args.flags) {
|
|
2377
|
+
console.log(JSON.stringify(spendJson(reports, by, args), null, 2));
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
printSpend(reports, by, args, subscriptions.length > 0);
|
|
2381
|
+
}
|
|
2382
|
+
finally {
|
|
2383
|
+
db.close();
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* El denominador de la cuota por mes, como en ai-cost: todo el consumo de los
|
|
2388
|
+
* turnos guardados, y nunca menos que lo imputado a todos los proyectos (no
|
|
2389
|
+
* solo a los filtrados).
|
|
2390
|
+
*/
|
|
2391
|
+
function shareDenominators(db) {
|
|
2392
|
+
const monthTotals = store.totalConsumptionByMonth(db);
|
|
2393
|
+
const assigned = new Map();
|
|
2394
|
+
for (const row of store.consumptionByProjectMonth(db)) {
|
|
2395
|
+
assigned.set(row.month, (assigned.get(row.month) ?? 0) + row.consumption.microUsd);
|
|
2396
|
+
}
|
|
2397
|
+
for (const [month, micro] of assigned) {
|
|
2398
|
+
if (micro > (monthTotals.get(month)?.microUsd ?? 0))
|
|
2399
|
+
monthTotals.set(month, { microUsd: micro });
|
|
2400
|
+
}
|
|
2401
|
+
return monthTotals;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* `estela delivery` — planificado frente a entregado en un periodo, con el
|
|
2405
|
+
* coste de IA de cada parte. Ver delivery/report.ts.
|
|
2406
|
+
*
|
|
2407
|
+
* La única llamada fuera de esta máquina de todo Estela sin cuenta: `gh`, y
|
|
2408
|
+
* solo en un proyecto con GitHub configurado como gestor. Sin `gh`, o con
|
|
2409
|
+
* --offline, se usa lo leído la última vez; si nunca se leyó, lo entregado
|
|
2410
|
+
* según git.
|
|
2411
|
+
*/
|
|
2412
|
+
async function cmdDelivery(args, dbPath) {
|
|
2413
|
+
const by = str(args, "by");
|
|
2414
|
+
if ("by" in args.flags && by !== "client")
|
|
2415
|
+
throw new UserError((0, index_js_1.tr) `--by en delivery solo admite client.`);
|
|
2416
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
2417
|
+
try {
|
|
2418
|
+
const projectId = await projectFromArgsOrCwd(args, db);
|
|
2419
|
+
const project = store.getProject(db, projectId);
|
|
2420
|
+
const client = store.getClient(db, project.clientId);
|
|
2421
|
+
const { from, to } = deliveryPeriod(args);
|
|
2422
|
+
const title = `${project.name}${client ? ` · ${client.name}` : ""}`;
|
|
2423
|
+
if (by === "client") {
|
|
2424
|
+
await deliveryByClient(db, args, project, title, from, to);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const label = str(args, "label");
|
|
2428
|
+
const milestone = str(args, "milestone");
|
|
2429
|
+
if (label && milestone)
|
|
2430
|
+
throw new UserError((0, index_js_1.tr) `--label y --milestone van por separado: elige una forma de marcar el plan.`);
|
|
2431
|
+
const configured = project.tracker?.planLabel;
|
|
2432
|
+
const plan = label ? { label } : milestone ? { milestone } : configured ? { label: configured } : null;
|
|
2433
|
+
const { items, origin, notes, fromGit } = await loadTrackerItems(db, project, args, { from, to, plan });
|
|
2434
|
+
const report = (0, report_js_1.deliveryReport)({
|
|
2435
|
+
items, plan: fromGit ? null : plan, from, to,
|
|
2436
|
+
entries: store.getTimeEntries(db, projectId), merges: store.listMerges(db),
|
|
2437
|
+
subscriptions: store.listSubscriptions(db), monthTotals: shareDenominators(db),
|
|
2438
|
+
});
|
|
2439
|
+
if (!fromGit && !plan) {
|
|
2440
|
+
notes.push((0, index_js_1.tr) `Sin etiqueta ni milestone de plan solo se enseña lo entregado. Indícalo con --label o --milestone,`);
|
|
2441
|
+
notes.push((0, index_js_1.tr) `o déjalo fijo: estela project tracker --project ${projectId} --plan-label <etiqueta>`);
|
|
2442
|
+
}
|
|
2443
|
+
if ("json" in args.flags) {
|
|
2444
|
+
console.log(JSON.stringify(deliveryJson(report, { projectId, from, to, plan: fromGit ? null : plan, origin, notes }), null, 2));
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
printDelivery(report, {
|
|
2448
|
+
title, from, to, plan: fromGit ? null : plan, origin, notes, hasSubs: store.listSubscriptions(db).length > 0,
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
finally {
|
|
2452
|
+
db.close();
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Las issues y los PRs de un proyecto: de GitHub con gh si se puede; si no, lo
|
|
2457
|
+
* guardado la última vez; y si nunca se leyó, lo entregado según git.
|
|
2458
|
+
*/
|
|
2459
|
+
async function loadTrackerItems(db, project, args, query) {
|
|
2460
|
+
const notes = [];
|
|
2461
|
+
let items = null;
|
|
2462
|
+
let origin = "";
|
|
2463
|
+
const repo = project.repoPaths.find((p) => (0, node_fs_1.existsSync)(p));
|
|
2464
|
+
const github = project.tracker?.system === "github";
|
|
2465
|
+
if (!github) {
|
|
2466
|
+
notes.push((0, index_js_1.tr) `Este proyecto no tiene GitHub como gestor, así que no se lee el plan. Para leerlo:`);
|
|
2467
|
+
notes.push((0, index_js_1.tr) ` estela project tracker --project ${project.id} --system github --plan-label <etiqueta>`);
|
|
2468
|
+
}
|
|
2469
|
+
else if (!repo) {
|
|
2470
|
+
notes.push((0, index_js_1.tr) `La carpeta del repo de este proyecto ya no existe: gh no tiene dónde mirar.`);
|
|
2471
|
+
}
|
|
2472
|
+
else if ("offline" in args.flags) {
|
|
2473
|
+
notes.push((0, index_js_1.tr) `--offline: no se consulta GitHub.`);
|
|
2474
|
+
}
|
|
2475
|
+
else {
|
|
2476
|
+
const source = new github_js_1.GitHubPlanSource(repo);
|
|
2477
|
+
const available = await source.availability();
|
|
2478
|
+
if (available.ok) {
|
|
2479
|
+
items = await source.fetch(query);
|
|
2480
|
+
store.saveTrackerItems(db, project.id, items);
|
|
2481
|
+
origin = (0, index_js_1.tr) `GitHub (gh), leído ahora`;
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
notes.push(unavailableText(available));
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
if (!items && github) {
|
|
2488
|
+
const cached = store.listTrackerItems(db, project.id);
|
|
2489
|
+
if (cached.items.length && cached.fetchedAt) {
|
|
2490
|
+
items = cached.items;
|
|
2491
|
+
origin = (0, index_js_1.tr) `GitHub, lo guardado el ${(0, shared_1.localDate)(cached.fetchedAt)} (sin conexión)`;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (items)
|
|
2495
|
+
return { items, origin, notes, fromGit: false };
|
|
2496
|
+
return { items: deliveredFromGit(db, project.repoPaths), notes, fromGit: true,
|
|
2497
|
+
origin: (0, index_js_1.tr) `git: los PR de los commits de merge y de los squash con (#N)` };
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* `estela summary` — el informe para finanzas: gasto, clientes, entregas y lo
|
|
2501
|
+
* que no llegó a entregarse, en Markdown, CSV o JSON. Ver export/finance.ts.
|
|
2502
|
+
*
|
|
2503
|
+
* No llama a nada fuera de la máquina: de GitHub usa lo que ya leyó
|
|
2504
|
+
* `estela delivery`, y si no hay nada, lo que dice git.
|
|
2505
|
+
*/
|
|
2506
|
+
async function cmdSummary(args, dbPath) {
|
|
2507
|
+
const out = str(args, "out");
|
|
2508
|
+
const format = str(args, "format") ?? (out?.endsWith(".csv") ? "csv" : out?.endsWith(".json") ? "json" : "md");
|
|
2509
|
+
if (!["md", "csv", "json"].includes(format))
|
|
2510
|
+
throw new UserError((0, index_js_1.tr) `--format va como md, csv o json.`);
|
|
2511
|
+
const rawDays = str(args, "stale-days");
|
|
2512
|
+
const staleDays = rawDays === undefined ? 14 : Number(rawDays);
|
|
2513
|
+
if (!Number.isInteger(staleDays) || staleDays < 1)
|
|
2514
|
+
throw new UserError((0, index_js_1.tr) `--stale-days va como un número entero de días, 1 o más.`);
|
|
2515
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
2516
|
+
try {
|
|
2517
|
+
const { from, to } = summaryPeriod(args);
|
|
2518
|
+
const projects = selectedProjects(db, args);
|
|
2519
|
+
const filtered = "project" in args.flags || "client" in args.flags;
|
|
2520
|
+
const subs = store.listSubscriptions(db);
|
|
2521
|
+
const inPeriod = (d) => { const day = (0, shared_1.localDate)(d); return day >= from && day <= to; };
|
|
2522
|
+
// Aquí los meses son los de tu reloj, no los de UTC: un mes cerrado tiene que
|
|
2523
|
+
// sumar su cuota exacta, y la tarde del día 31 en América ya es el mes
|
|
2524
|
+
// siguiente en UTC. Los denominadores, igual: todo el consumo de cada mes
|
|
2525
|
+
// local, y nunca menos que lo imputado a todos los proyectos.
|
|
2526
|
+
const month = (d) => (0, shared_1.localDate)(d).slice(0, 7);
|
|
2527
|
+
const denominators = new Map();
|
|
2528
|
+
for (const t of store.turnsBetween(db, new Date(0), new Date("9999-12-31T00:00:00Z"))) {
|
|
2529
|
+
const m = month(t.at);
|
|
2530
|
+
denominators.set(m, { microUsd: (denominators.get(m)?.microUsd ?? 0) + ((0, cost_js_1.costOfTurn)(t.tokens, t.model, t.at)?.microUsd ?? 0) });
|
|
2531
|
+
}
|
|
2532
|
+
const imputed = new Map();
|
|
2533
|
+
for (const p of [...store.listProjects(db), ...store.listArchivedProjects(db)]) {
|
|
2534
|
+
for (const e of store.getTimeEntries(db, p.id))
|
|
2535
|
+
imputed.set(month(e.startedAt), (imputed.get(month(e.startedAt)) ?? 0) + e.aiCost.microUsd);
|
|
2536
|
+
}
|
|
2537
|
+
for (const [m, micro] of imputed)
|
|
2538
|
+
if (micro > (denominators.get(m)?.microUsd ?? 0))
|
|
2539
|
+
denominators.set(m, { microUsd: micro });
|
|
2540
|
+
const costOf = (entries) => {
|
|
2541
|
+
const byMonth = new Map();
|
|
2542
|
+
for (const e of entries)
|
|
2543
|
+
byMonth.set(month(e.startedAt), (byMonth.get(month(e.startedAt)) ?? 0) + e.aiCost.microUsd);
|
|
2544
|
+
const { real, partial } = (0, spend_js_1.realFromMonths)(byMonth, subs, (m) => denominators.get(m)?.microUsd ?? 0);
|
|
2545
|
+
return { seconds: entries.reduce((n, e) => n + e.seconds, 0),
|
|
2546
|
+
consumption: { microUsd: entries.reduce((n, e) => n + e.aiCost.microUsd, 0) }, real, realPartial: partial };
|
|
2547
|
+
};
|
|
2548
|
+
const zero = { seconds: 0, consumption: { microUsd: 0 }, real: null, realPartial: false };
|
|
2549
|
+
// Por cliente y proyecto.
|
|
2550
|
+
const entriesOf = new Map(projects.map((p) => [p.id, store.getTimeEntries(db, p.id).filter((e) => inPeriod(e.startedAt))]));
|
|
2551
|
+
const clients = new Map();
|
|
2552
|
+
for (const p of projects) {
|
|
2553
|
+
const entries = entriesOf.get(p.id);
|
|
2554
|
+
if (!entries.length)
|
|
2555
|
+
continue;
|
|
2556
|
+
const c = clients.get(p.clientId) ?? { name: p.clientName, projects: [] };
|
|
2557
|
+
c.projects.push({ id: p.id, name: p.name, cost: costOf(entries) });
|
|
2558
|
+
clients.set(p.clientId, c);
|
|
2559
|
+
}
|
|
2560
|
+
const financeClients = [...clients].map(([id, c]) => ({
|
|
2561
|
+
id, name: c.name, projects: c.projects, cost: c.projects.reduce((acc, p) => (0, finance_js_1.sum)(acc, p.cost), zero),
|
|
2562
|
+
})).sort((a, b) => b.cost.consumption.microUsd - a.cost.consumption.microUsd);
|
|
2563
|
+
const assigned = financeClients.reduce((acc, c) => (0, finance_js_1.sum)(acc, c.cost), zero);
|
|
2564
|
+
// Lo pagado en todo el periodo, con o sin proyecto: la cuota repartida
|
|
2565
|
+
// sobre todo el consumo de los turnos. Con --project o --client, solo eso.
|
|
2566
|
+
let paid = assigned;
|
|
2567
|
+
let unassigned = zero;
|
|
2568
|
+
let unpriced = false;
|
|
2569
|
+
if (!filtered) {
|
|
2570
|
+
const byMonth = new Map();
|
|
2571
|
+
let micro = 0;
|
|
2572
|
+
// Un día de margen a cada lado: el filtro de verdad es por fecha local.
|
|
2573
|
+
const desde = new Date(Date.parse(`${from}T00:00:00Z`) - DAY_MS);
|
|
2574
|
+
const hasta = new Date(Date.parse(`${to}T00:00:00Z`) + 2 * DAY_MS);
|
|
2575
|
+
for (const t of store.turnsBetween(db, desde, hasta)) {
|
|
2576
|
+
if (!inPeriod(t.at))
|
|
2577
|
+
continue;
|
|
2578
|
+
if (t.agent === "copilot")
|
|
2579
|
+
unpriced = true;
|
|
2580
|
+
const c = (0, cost_js_1.costOfTurn)(t.tokens, t.model, t.at)?.microUsd ?? 0;
|
|
2581
|
+
micro += c;
|
|
2582
|
+
byMonth.set(month(t.at), (byMonth.get(month(t.at)) ?? 0) + c);
|
|
2583
|
+
}
|
|
2584
|
+
const { real, partial } = (0, spend_js_1.realFromMonths)(byMonth, subs, (m) => denominators.get(m)?.microUsd ?? 0);
|
|
2585
|
+
// Nunca por debajo de lo imputado: las imputaciones de días cerrados conservan su cifra.
|
|
2586
|
+
const machine = { seconds: assigned.seconds, consumption: { microUsd: Math.max(micro, assigned.consumption.microUsd) },
|
|
2587
|
+
real: real && assigned.real && real.amount < assigned.real.amount ? assigned.real : real ?? assigned.real, realPartial: partial };
|
|
2588
|
+
paid = machine;
|
|
2589
|
+
unassigned = {
|
|
2590
|
+
seconds: 0, consumption: { microUsd: Math.max(0, machine.consumption.microUsd - assigned.consumption.microUsd) },
|
|
2591
|
+
real: machine.real ? { amount: Math.max(0, machine.real.amount - (assigned.real?.amount ?? 0)), currency: machine.real.currency } : null,
|
|
2592
|
+
realPartial: machine.realPartial,
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
for (const p of projects)
|
|
2596
|
+
if ((entriesOf.get(p.id) ?? []).some((e) => e.agents.includes("copilot")))
|
|
2597
|
+
unpriced = true;
|
|
2598
|
+
// Entregas y peticiones de clientes: lo guardado de GitHub, o lo que dice git.
|
|
2599
|
+
const deliveries = [];
|
|
2600
|
+
const merges = store.listMerges(db);
|
|
2601
|
+
for (const p of projects) {
|
|
2602
|
+
const project = store.getProject(db, p.id);
|
|
2603
|
+
const cached = store.listTrackerItems(db, p.id);
|
|
2604
|
+
const fromGitHub = cached.items.length > 0;
|
|
2605
|
+
const items = fromGitHub ? cached.items : deliveredFromGit(db, project.repoPaths);
|
|
2606
|
+
const planLabel = fromGitHub ? project.tracker?.planLabel : undefined;
|
|
2607
|
+
const report = (0, report_js_1.deliveryReport)({ items, plan: planLabel ? { label: planLabel } : null, from, to,
|
|
2608
|
+
entries: store.getTimeEntries(db, p.id), merges, subscriptions: subs, monthTotals: denominators });
|
|
2609
|
+
const lines = [...(report.planned?.delivered.lines ?? []), ...report.unplanned.lines];
|
|
2610
|
+
let requests = null;
|
|
2611
|
+
let requestsRest = null;
|
|
2612
|
+
if (fromGitHub) {
|
|
2613
|
+
const r = (0, clients_js_1.clientRequests)({ items, entries: entriesOf.get(p.id) ?? [], merges,
|
|
2614
|
+
prefix: project.tracker?.clientLabel ?? DEFAULT_CLIENT_LABEL,
|
|
2615
|
+
requesterOf: (name) => {
|
|
2616
|
+
const clientId = store.clientForLabel(db, name);
|
|
2617
|
+
return { id: clientId ?? `?${store.aliasKey(name)}`, clientId, name: (clientId && store.getClient(db, clientId)?.name) || name };
|
|
2618
|
+
},
|
|
2619
|
+
subscriptions: subs, monthTotals: denominators });
|
|
2620
|
+
if (r.groups.length) {
|
|
2621
|
+
requests = r.groups.map((g) => ({ name: g.requesters.map((x) => x.name).join(" + "),
|
|
2622
|
+
mapped: g.requesters.every((x) => x.clientId !== null), cost: g }));
|
|
2623
|
+
requestsRest = r.rest;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (!lines.length && !report.planned)
|
|
2627
|
+
continue;
|
|
2628
|
+
deliveries.push({
|
|
2629
|
+
projectName: p.name, clientName: p.clientName, source: fromGitHub ? "github" : "git",
|
|
2630
|
+
fetchedAt: cached.fetchedAt, delivered: lines.map((l) => ({ label: itemLabel(l.item), cost: l })),
|
|
2631
|
+
plan: report.planned && planLabel ? {
|
|
2632
|
+
label: planLabel, delivered: report.planned.delivered.lines.length,
|
|
2633
|
+
total: report.planned.delivered.lines.length + report.planned.pending.lines.length,
|
|
2634
|
+
pending: report.planned.pending, unplanned: report.unplanned,
|
|
2635
|
+
} : null,
|
|
2636
|
+
requests, requestsRest,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
// Lo que no llegó a entregarse.
|
|
2640
|
+
const undelivered = (await collectUndelivered(db, projects, staleDays, from, to))
|
|
2641
|
+
.filter((r) => r.total.consumption.microUsd > 0)
|
|
2642
|
+
.map((r) => {
|
|
2643
|
+
// La proporción sale de las sesiones; el dinero, del gasto del proyecto
|
|
2644
|
+
// en este mismo informe, para que las dos tablas cuadren entre sí.
|
|
2645
|
+
const project = financeClients.flatMap((c) => c.projects).find((p) => p.id === r.projectId)?.cost;
|
|
2646
|
+
const asCost = (x) => {
|
|
2647
|
+
const ratio = r.total.consumption.microUsd ? x.consumption.microUsd / r.total.consumption.microUsd : 0;
|
|
2648
|
+
const real = project?.real ? { amount: Math.round(project.real.amount * ratio), currency: project.real.currency } : x.real;
|
|
2649
|
+
return { seconds: 0, consumption: x.consumption, real, realPartial: project?.realPartial ?? x.realPartial };
|
|
2650
|
+
};
|
|
2651
|
+
return { projectName: projects.find((p) => p.id === r.projectId)?.name ?? r.projectId,
|
|
2652
|
+
total: asCost(r.total), undelivered: asCost(r.undelivered), withoutCommit: asCost(r.sessionsWithoutCommit),
|
|
2653
|
+
unmergedBranches: asCost(r.unmerged), unmergedCount: r.unmerged.branches.length };
|
|
2654
|
+
});
|
|
2655
|
+
const summary = {
|
|
2656
|
+
from, to, currency: subs[0]?.monthlyFee.currency ?? null, paid, unassigned,
|
|
2657
|
+
clients: financeClients, deliveries, undelivered, staleDays, unpricedAgents: unpriced,
|
|
2658
|
+
};
|
|
2659
|
+
const text = format === "csv" ? (0, finance_js_1.financeToCsv)(summary)
|
|
2660
|
+
: format === "json" ? JSON.stringify((0, finance_js_1.financeToJson)(summary), null, 2) + "\n"
|
|
2661
|
+
: (0, finance_js_1.financeToMarkdown)(summary);
|
|
2662
|
+
if (out) {
|
|
2663
|
+
(0, node_fs_1.writeFileSync)(out, text, "utf8");
|
|
2664
|
+
console.log((0, index_js_1.tr) `Informe guardado en ${out}`);
|
|
2665
|
+
}
|
|
2666
|
+
else {
|
|
2667
|
+
process.stdout.write(text);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
finally {
|
|
2671
|
+
db.close();
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const DAY_MS = 86_400_000;
|
|
2675
|
+
/** El periodo del informe: --month, --from/--to, o el último mes cerrado. */
|
|
2676
|
+
function summaryPeriod(args) {
|
|
2677
|
+
const { from, to } = periodOf(args);
|
|
2678
|
+
if (from && to)
|
|
2679
|
+
return { from, to };
|
|
2680
|
+
if (from)
|
|
2681
|
+
return { from, to: (0, shared_1.localDate)(new Date()) };
|
|
2682
|
+
if (to)
|
|
2683
|
+
throw new UserError((0, index_js_1.tr) `Con --to hace falta --from.`);
|
|
2684
|
+
const now = new Date();
|
|
2685
|
+
const prev = new Date(now.getFullYear(), now.getMonth() - 1, 15);
|
|
2686
|
+
const month = `${prev.getFullYear()}-${String(prev.getMonth() + 1).padStart(2, "0")}`;
|
|
2687
|
+
const last = new Date(prev.getFullYear(), prev.getMonth() + 1, 0).getDate();
|
|
2688
|
+
return { from: `${month}-01`, to: `${month}-${String(last).padStart(2, "0")}` };
|
|
2689
|
+
}
|
|
2690
|
+
/** Sin configurar, las etiquetas de cliente son `cliente:<nombre>`. */
|
|
2691
|
+
const DEFAULT_CLIENT_LABEL = "cliente:";
|
|
2692
|
+
/**
|
|
2693
|
+
* `estela delivery --by client` — el gasto de IA del periodo en peticiones de
|
|
2694
|
+
* cada cliente frente al resto. Ver delivery/clients.ts.
|
|
2695
|
+
*/
|
|
2696
|
+
async function deliveryByClient(db, args, project, title, from, to) {
|
|
2697
|
+
const prefix = project.tracker?.clientLabel ?? DEFAULT_CLIENT_LABEL;
|
|
2698
|
+
const { items, origin, notes, fromGit } = await loadTrackerItems(db, project, args, { from, to, plan: null, clientLabelPrefix: prefix });
|
|
2699
|
+
if (fromGit)
|
|
2700
|
+
notes.push((0, index_js_1.tr) `Sin GitHub no hay etiquetas: no se sabe qué pidió cada cliente, y todo va al resto.`);
|
|
2701
|
+
const requesterOf = (name) => {
|
|
2702
|
+
const clientId = store.clientForLabel(db, name);
|
|
2703
|
+
const c = clientId ? store.getClient(db, clientId) : null;
|
|
2704
|
+
return { id: clientId ?? `?${store.aliasKey(name)}`, clientId, name: c?.name ?? name };
|
|
2705
|
+
};
|
|
2706
|
+
const report = (0, clients_js_1.clientRequests)({
|
|
2707
|
+
items: fromGit ? [] : items,
|
|
2708
|
+
entries: store.getTimeEntries(db, project.id).filter((e) => {
|
|
2709
|
+
const day = (0, shared_1.localDate)(e.startedAt);
|
|
2710
|
+
return day >= from && day <= to;
|
|
2711
|
+
}),
|
|
2712
|
+
merges: store.listMerges(db), prefix, requesterOf,
|
|
2713
|
+
subscriptions: store.listSubscriptions(db), monthTotals: shareDenominators(db),
|
|
2714
|
+
});
|
|
2715
|
+
if ("json" in args.flags) {
|
|
2716
|
+
const cost = (c) => ({ seconds: c.seconds, apiMicroUsd: c.consumption.microUsd,
|
|
2717
|
+
real: c.real ? { amountMinor: c.real.amount, currency: c.real.currency } : null, realPartial: c.realPartial,
|
|
2718
|
+
percentOfSpend: report.total.consumption.microUsd ? (c.consumption.microUsd / report.total.consumption.microUsd) * 100 : null });
|
|
2719
|
+
console.log(JSON.stringify({
|
|
2720
|
+
project: project.id, period: { from, to }, labelPrefix: prefix, source: origin, notes,
|
|
2721
|
+
requests: report.groups.map((g) => ({
|
|
2722
|
+
requesters: g.requesters.map((r) => ({ clientId: r.clientId, name: r.name, mapped: r.clientId !== null })),
|
|
2723
|
+
items: g.items.map((i) => ({ key: i.key, kind: i.kind, number: i.number, title: i.title, state: i.state })),
|
|
2724
|
+
...cost(g),
|
|
2725
|
+
})),
|
|
2726
|
+
rest: cost(report.rest), total: cost(report.total),
|
|
2727
|
+
}, null, 2));
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
console.log((0, index_js_1.tr) `Peticiones de clientes · ${title} · ${from} → ${to}`);
|
|
2731
|
+
console.log((0, index_js_1.tr) `Etiqueta: "${prefix}<nombre>" · Fuente: ${origin}`);
|
|
2732
|
+
console.log((0, index_js_1.tr) `Estela no conoce ingresos ni contratos: esto solo une el gasto de IA con quién pidió el trabajo.`);
|
|
2733
|
+
for (const n of notes)
|
|
2734
|
+
console.log(n);
|
|
2735
|
+
const hasSubs = store.listSubscriptions(db).length > 0;
|
|
2736
|
+
const pct = (c) => report.total.consumption.microUsd === 0 ? "—"
|
|
2737
|
+
: `${((c.consumption.microUsd / report.total.consumption.microUsd) * 100).toFixed(1)} %`;
|
|
2738
|
+
const row = (label, c) => ` ${label.slice(0, 44).padEnd(46)}${(c.seconds ? (0, shared_1.formatDuration)(c.seconds) : "—").padStart(9)}` +
|
|
2739
|
+
`${(0, shared_1.formatAiCost)(c.consumption).padStart(11)}` +
|
|
2740
|
+
`${(hasSubs ? (c.real ? (0, shared_1.formatMoney)(c.real) : "—") + (c.realPartial ? "†" : "") : "—").padStart(12)}${pct(c).padStart(10)}`;
|
|
2741
|
+
const who = (r) => (r.clientId ? r.name : (0, index_js_1.tr) `"${r.name}" (sin mapear)`);
|
|
2742
|
+
console.log(`\n ${"".padEnd(46)}${(0, index_js_1.tr) `Horas`.padStart(9)}${(0, index_js_1.tr) `API`.padStart(11)}${(0, index_js_1.tr) `Real`.padStart(12)}${(0, index_js_1.tr) `% gasto`.padStart(10)}`);
|
|
2743
|
+
for (const g of report.groups) {
|
|
2744
|
+
const label = g.requesters.length === 1 ? who(g.requesters[0]) : (0, index_js_1.tr) `Varios: ${g.requesters.map(who).join(" + ")}`;
|
|
2745
|
+
console.log(row(label, g));
|
|
2746
|
+
const shown = g.items.slice(0, 5).map(itemLabel);
|
|
2747
|
+
console.log(` ${shown.join(" · ")}${g.items.length > 5 ? (0, index_js_1.tr) ` · y ${g.items.length - 5} más` : ""}`);
|
|
2748
|
+
}
|
|
2749
|
+
if (report.groups.length === 0)
|
|
2750
|
+
console.log((0, index_js_1.tr) ` (ninguna petición de cliente con horas en el periodo)`);
|
|
2751
|
+
console.log(row((0, index_js_1.tr) `Resto (sin petición de cliente)`, report.rest));
|
|
2752
|
+
console.log(row((0, index_js_1.tr) `Total del periodo`, report.total));
|
|
2753
|
+
const unmapped = [...new Set(report.groups.flatMap((g) => g.requesters).filter((r) => !r.clientId).map((r) => r.name))];
|
|
2754
|
+
console.log((0, index_js_1.tr) `\nUna hora cuenta para quien pidió la issue o el PR al que se une (rama del PR, su número en ramas, commits o a mano).`);
|
|
2755
|
+
console.log((0, index_js_1.tr) `Si sirvió a peticiones de varios clientes va a "Varios", sin repartirla. Un PR hereda las etiquetas de las issues que cierra.`);
|
|
2756
|
+
for (const name of unmapped) {
|
|
2757
|
+
console.log((0, index_js_1.tr) `Para unir "${name}" con un cliente de Estela: estela client alias --client <id> --label "${name}"`);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
/** El periodo de delivery: --month, --from/--to, o el mes en curso. */
|
|
2761
|
+
function deliveryPeriod(args) {
|
|
2762
|
+
const { from, to } = periodOf(args);
|
|
2763
|
+
const today = (0, shared_1.localDate)(new Date());
|
|
2764
|
+
if (from && to)
|
|
2765
|
+
return { from, to };
|
|
2766
|
+
if (from)
|
|
2767
|
+
return { from, to: today };
|
|
2768
|
+
if (to)
|
|
2769
|
+
throw new UserError((0, index_js_1.tr) `Con --to hace falta --from.`);
|
|
2770
|
+
return { from: `${today.slice(0, 7)}-01`, to: `${today.slice(0, 7)}-31` };
|
|
2771
|
+
}
|
|
2772
|
+
function unavailableText(a) {
|
|
2773
|
+
switch (a.reason) {
|
|
2774
|
+
case "missing": return (0, index_js_1.tr) `gh no está instalado: sin él no se puede leer el plan de GitHub. Instálalo desde https://cli.github.com y ejecuta: gh auth login`;
|
|
2775
|
+
case "auth": return (0, index_js_1.tr) `gh no tiene sesión iniciada. Ejecuta: gh auth login`;
|
|
2776
|
+
case "repo": return (0, index_js_1.tr) `gh no reconoce un repo de GitHub en la carpeta del proyecto (${a.detail}).`;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Lo entregado que se sabe sin GitHub: los PR fusionados con merge (su
|
|
2781
|
+
* mensaje dice el número y la rama) y los squash con "(#N)" al final. No hay
|
|
2782
|
+
* plan: eso solo lo sabe el gestor.
|
|
2783
|
+
*/
|
|
2784
|
+
function deliveredFromGit(db, repoPaths) {
|
|
2785
|
+
const inProject = (path) => repoPaths.some((r) => (0, paths_js_1.isSameOrInside)(path, r) || (0, paths_js_1.isSameOrInside)(r, path));
|
|
2786
|
+
const items = new Map();
|
|
2787
|
+
const pr = (key, title, at, branch) => ({
|
|
2788
|
+
key, kind: "pr", number: Number(key.split(":")[1]), title, state: "merged", completed: true,
|
|
2789
|
+
closedAt: at, branch, closes: [], labels: [], milestone: null, url: "",
|
|
2790
|
+
});
|
|
2791
|
+
for (const m of store.listMerges(db)) {
|
|
2792
|
+
// El mensaje del merge solo repite el número: la rama dice más.
|
|
2793
|
+
if (m.ref?.startsWith("github:") && inProject(m.repoPath)) {
|
|
2794
|
+
items.set(m.ref, pr(m.ref, m.sourceBranch ?? m.subject, m.at, m.sourceBranch));
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const rows = db.prepare("SELECT repo_path, subject, at FROM commits").all();
|
|
2798
|
+
for (const r of rows) {
|
|
2799
|
+
const squash = /\(#(\d+)\)\s*$/.exec(r.subject);
|
|
2800
|
+
if (!squash || !inProject(r.repo_path))
|
|
2801
|
+
continue;
|
|
2802
|
+
const key = `github:${squash[1]}`;
|
|
2803
|
+
if (!items.has(key))
|
|
2804
|
+
items.set(key, pr(key, r.subject.replace(/\s*\(#\d+\)\s*$/, ""), new Date(r.at), null));
|
|
2805
|
+
}
|
|
2806
|
+
return [...items.values()];
|
|
2807
|
+
}
|
|
2808
|
+
function planText(plan) {
|
|
2809
|
+
if (!plan)
|
|
2810
|
+
return (0, index_js_1.tr) `sin plan`;
|
|
2811
|
+
return plan.label ? (0, index_js_1.tr) `etiqueta "${plan.label}"` : (0, index_js_1.tr) `milestone "${plan.milestone}"`;
|
|
2812
|
+
}
|
|
2813
|
+
function itemLabel(i) {
|
|
2814
|
+
return `${i.kind === "pr" ? (0, index_js_1.tr) `PR #${i.number}` : `#${i.number}`} ${i.title}`;
|
|
2815
|
+
}
|
|
2816
|
+
function printDelivery(r, v) {
|
|
2817
|
+
console.log((0, index_js_1.tr) `Planificado frente a entregado · ${v.title} · ${v.from} → ${v.to}`);
|
|
2818
|
+
console.log((0, index_js_1.tr) `Plan: ${planText(v.plan)} · Fuente: ${v.origin}`);
|
|
2819
|
+
for (const n of v.notes)
|
|
2820
|
+
console.log(n);
|
|
2821
|
+
const money = (c) => v.hasSubs ? (c.real ? (0, shared_1.formatMoney)(c.real) : "—") + (c.realPartial ? "†" : "") : "—";
|
|
2822
|
+
const row = (label, c) => ` ${label.slice(0, 46).padEnd(48)}${(c.seconds ? (0, shared_1.formatDuration)(c.seconds) : "—").padStart(9)}` +
|
|
2823
|
+
`${(0, shared_1.formatAiCost)(c.consumption).padStart(11)}${money(c).padStart(12)}`;
|
|
2824
|
+
const block = (title, b) => {
|
|
2825
|
+
console.log(`\n${title}`);
|
|
2826
|
+
if (b.lines.length === 0) {
|
|
2827
|
+
console.log((0, index_js_1.tr) ` (nada)`);
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
console.log(` ${"".padEnd(48)}${(0, index_js_1.tr) `Horas`.padStart(9)}${(0, index_js_1.tr) `API`.padStart(11)}${(0, index_js_1.tr) `Real`.padStart(12)}`);
|
|
2831
|
+
for (const l of b.lines) {
|
|
2832
|
+
console.log(row(itemLabel(l.item), l));
|
|
2833
|
+
const detail = [];
|
|
2834
|
+
const when = l.item.closedAt ? (0, shared_1.localDate)(l.item.closedAt) : null;
|
|
2835
|
+
if (l.item.kind === "pr") {
|
|
2836
|
+
if (l.item.state === "merged" && when)
|
|
2837
|
+
detail.push((0, index_js_1.tr) `fusionado ${when}`);
|
|
2838
|
+
else
|
|
2839
|
+
detail.push(l.item.state === "open" ? (0, index_js_1.tr) `abierto` : (0, index_js_1.tr) `cerrado sin fusionar`);
|
|
2840
|
+
}
|
|
2841
|
+
else if (l.deliveredBy.length) {
|
|
2842
|
+
detail.push((0, index_js_1.tr) `entregada por ${l.deliveredBy.map((p) => (0, index_js_1.tr) `PR #${p.number}`).join(", ")}`);
|
|
2843
|
+
}
|
|
2844
|
+
else if (l.item.completed && when) {
|
|
2845
|
+
detail.push((0, index_js_1.tr) `cerrada ${when}`);
|
|
2846
|
+
}
|
|
2847
|
+
else {
|
|
2848
|
+
detail.push(l.item.state === "open" ? (0, index_js_1.tr) `abierta` : (0, index_js_1.tr) `cerrada sin hacer`);
|
|
2849
|
+
}
|
|
2850
|
+
detail.push(l.basis.length
|
|
2851
|
+
? (0, index_js_1.tr) `coste según: ${l.basis.map((x) => (x === "branch" ? (0, index_js_1.tr) `rama del PR` : (0, index_js_1.tr) `número en rama o commit`)).join(", ")}`
|
|
2852
|
+
: (0, index_js_1.tr) `sin horas unidas`);
|
|
2853
|
+
console.log(` ${detail.join(" · ")}`);
|
|
2854
|
+
}
|
|
2855
|
+
if (b.lines.length > 1)
|
|
2856
|
+
console.log(row((0, index_js_1.tr) `Total del bloque`, b));
|
|
2857
|
+
};
|
|
2858
|
+
if (r.planned) {
|
|
2859
|
+
const total = r.planned.delivered.lines.length + r.planned.pending.lines.length;
|
|
2860
|
+
block((0, index_js_1.tr) `Planificado y entregado (${r.planned.delivered.lines.length} de ${total})`, r.planned.delivered);
|
|
2861
|
+
block((0, index_js_1.tr) `Planificado sin entregar (${r.planned.pending.lines.length})`, r.planned.pending);
|
|
2862
|
+
}
|
|
2863
|
+
block(r.planned ? (0, index_js_1.tr) `Entregado sin planificar (${r.unplanned.lines.length})` : (0, index_js_1.tr) `Entregado (${r.unplanned.lines.length})`, r.unplanned);
|
|
2864
|
+
console.log((0, index_js_1.tr) `\nEl coste de cada línea es el de las horas unidas a ella: la rama del PR y su número en ramas, commits o a mano.`);
|
|
2865
|
+
console.log((0, index_js_1.tr) `Incluye lo trabajado antes del periodo para esa entrega. Si una hora se une a dos líneas, el total del bloque la cuenta una vez.`);
|
|
2866
|
+
console.log((0, index_js_1.tr) `API es lo que costaría a precio de API: mide el uso, no es dinero pagado. Real es tu cuota repartida por consumo.`);
|
|
2867
|
+
}
|
|
2868
|
+
function deliveryJson(r, meta) {
|
|
2869
|
+
const cost = (c) => ({
|
|
2870
|
+
seconds: c.seconds, apiMicroUsd: c.consumption.microUsd,
|
|
2871
|
+
real: c.real ? { amountMinor: c.real.amount, currency: c.real.currency } : null, realPartial: c.realPartial,
|
|
2872
|
+
});
|
|
2873
|
+
const block = (b) => ({
|
|
2874
|
+
...cost(b),
|
|
2875
|
+
items: b.lines.map((l) => ({
|
|
2876
|
+
key: l.item.key, kind: l.item.kind, number: l.item.number, title: l.item.title, state: l.item.state,
|
|
2877
|
+
completed: l.item.completed, closedAt: l.item.closedAt?.toISOString() ?? null, url: l.item.url || null,
|
|
2878
|
+
deliveredBy: l.deliveredBy.map((p) => p.key), basis: l.basis, ...cost(l),
|
|
2879
|
+
})),
|
|
2880
|
+
});
|
|
2881
|
+
return {
|
|
2882
|
+
project: meta.projectId, period: { from: meta.from, to: meta.to }, plan: meta.plan, source: meta.origin, notes: meta.notes,
|
|
2883
|
+
planned: r.planned ? { delivered: block(r.planned.delivered), pending: block(r.planned.pending) } : null,
|
|
2884
|
+
unplanned: block(r.unplanned),
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
/** Los proyectos de --project y --client; sin ellos, todos. */
|
|
2888
|
+
function selectedProjects(db, args) {
|
|
2889
|
+
const projectId = str(args, "project");
|
|
2890
|
+
const clientId = str(args, "client");
|
|
2891
|
+
if (projectId && !store.getProject(db, projectId))
|
|
2892
|
+
throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
|
|
2893
|
+
if (clientId && !store.getClient(db, clientId))
|
|
2894
|
+
throw new UserError((0, index_js_1.tr) `No existe el cliente "${clientId}".`);
|
|
2895
|
+
// También los archivados: su gasto ocurrió, y `ai-cost` tampoco los quita.
|
|
2896
|
+
return [...store.listProjects(db), ...store.listArchivedProjects(db)]
|
|
2897
|
+
.filter((p) => (!projectId || p.id === projectId) && (!clientId || p.clientId === clientId))
|
|
2898
|
+
.map((p) => ({
|
|
2899
|
+
id: p.id, name: p.name, clientId: p.clientId,
|
|
2900
|
+
clientName: store.getClient(db, p.clientId)?.name ?? p.clientId,
|
|
2901
|
+
repoPaths: p.repoPaths, tracker: p.tracker ?? null,
|
|
2902
|
+
}))
|
|
2903
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* `estela ai-cost --undelivered` — el gasto que no ha llegado a la rama
|
|
2907
|
+
* principal: sesiones sin commit y ramas sin fusionar. Ver metrics/undelivered.ts.
|
|
2908
|
+
*/
|
|
2909
|
+
async function cmdAiCostUndelivered(args, dbPath) {
|
|
2910
|
+
const rawDays = str(args, "stale-days");
|
|
2911
|
+
const staleDays = rawDays === undefined ? 14 : Number(rawDays);
|
|
2912
|
+
if ("stale-days" in args.flags && (!Number.isInteger(staleDays) || staleDays < 1)) {
|
|
2913
|
+
throw new UserError((0, index_js_1.tr) `--stale-days va como un número entero de días, 1 o más.`);
|
|
2914
|
+
}
|
|
2915
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
2916
|
+
try {
|
|
2917
|
+
const projects = selectedProjects(db, args);
|
|
2918
|
+
const wanted = new Set(projects.map((p) => p.id));
|
|
2919
|
+
const { from, to } = periodOf(args);
|
|
2920
|
+
const reports = await collectUndelivered(db, projects, staleDays, from, to);
|
|
2921
|
+
const names = new Map(projects.map((p) => [p.id, p]));
|
|
2922
|
+
const csvOut = str(args, "csv");
|
|
2923
|
+
if ("csv" in args.flags && !csvOut)
|
|
2924
|
+
throw new UserError((0, index_js_1.tr) `Falta el fichero: --csv <fichero.csv>`);
|
|
2925
|
+
if (csvOut) {
|
|
2926
|
+
(0, node_fs_1.writeFileSync)(csvOut, (0, csv_js_1.undeliveredToCsv)(reports, names), "utf8");
|
|
2927
|
+
console.log((0, index_js_1.tr) `CSV guardado en ${csvOut}`);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
if ("json" in args.flags) {
|
|
2931
|
+
console.log(JSON.stringify(undeliveredJson(reports, names, staleDays, args), null, 2));
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
printUndelivered(reports, names, staleDays, args);
|
|
2935
|
+
}
|
|
2936
|
+
finally {
|
|
2937
|
+
db.close();
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Los datos del gasto sin entrega de esos proyectos: sus turnos, tus commits y
|
|
2942
|
+
* el estado de cada rama en git. Lo usan --undelivered y el informe para
|
|
2943
|
+
* finanzas. Solo lee git en local: nada sale de la máquina.
|
|
2944
|
+
*/
|
|
2945
|
+
async function collectUndelivered(db, projects, staleDays, from, to) {
|
|
2946
|
+
const wanted = new Set(projects.map((p) => p.id));
|
|
2947
|
+
// El proyecto de cada turno, por el mismo camino que el import: la carpeta
|
|
2948
|
+
// tal cual y, si no, su raíz git (worktrees y subcarpetas). Con los
|
|
2949
|
+
// archivados: su gasto ocurrió aunque ya no capten horas.
|
|
2950
|
+
const roots = new Map();
|
|
2951
|
+
const rootOf = (path) => {
|
|
2952
|
+
let root = roots.get(path);
|
|
2953
|
+
if (root === undefined) {
|
|
2954
|
+
root = (0, project_of_js_1.gitRoot)(path) ?? (0, paths_js_1.normalizePath)(path);
|
|
2955
|
+
// La ruta tal como está en el disco: en macOS y Windows "ApexWall" y
|
|
2956
|
+
// "apexwall" son la misma carpeta, y los turnos y los commits pueden
|
|
2957
|
+
// traer cada uno una.
|
|
2958
|
+
try {
|
|
2959
|
+
root = (0, paths_js_1.normalizePath)(node_fs_1.realpathSync.native(root));
|
|
2960
|
+
}
|
|
2961
|
+
catch { /* ya no existe: tal cual */ }
|
|
2962
|
+
roots.set(path, root);
|
|
2963
|
+
}
|
|
2964
|
+
return root;
|
|
2965
|
+
};
|
|
2966
|
+
const owners = new Map();
|
|
2967
|
+
const projectOf = (path) => {
|
|
2968
|
+
if (!owners.has(path))
|
|
2969
|
+
owners.set(path, store.ownerOfRepo(db, path) ?? store.ownerOfRepo(db, rootOf(path)));
|
|
2970
|
+
return owners.get(path);
|
|
2971
|
+
};
|
|
2972
|
+
const turns = [];
|
|
2973
|
+
for (const t of store.turnsBetween(db, new Date(0), new Date("9999-12-31T00:00:00Z"))) {
|
|
2974
|
+
const projectId = t.repoPath ? projectOf(t.repoPath) : null;
|
|
2975
|
+
if (!projectId || !wanted.has(projectId))
|
|
2976
|
+
continue;
|
|
2977
|
+
turns.push({ projectId, sessionId: t.sessionId, at: t.at, root: rootOf(t.repoPath),
|
|
2978
|
+
branch: t.branch, microUsd: (0, cost_js_1.costOfTurn)(t.tokens, t.model, t.at)?.microUsd ?? 0, agent: t.agent });
|
|
2979
|
+
}
|
|
2980
|
+
const projectRoots = new Set(turns.map((t) => t.root));
|
|
2981
|
+
// Solo tus commits, como en el import: los de un compañero no entregan tu trabajo.
|
|
2982
|
+
const candidates = (await allCommits(db)).filter((c) => projectRoots.has(rootOf(c.repoPath)));
|
|
2983
|
+
const mine = (0, identity_js_1.onlyMine)(candidates, await (0, identity_js_1.myEmailsByRepo)(db, new Set(candidates.map((c) => c.repoPath))));
|
|
2984
|
+
const commits = mine.map((c) => ({ root: rootOf(c.repoPath), at: c.at, branch: c.branch }));
|
|
2985
|
+
// Qué ramas están integradas y cuáles existen hoy, repo a repo.
|
|
2986
|
+
const merges = store.listMerges(db);
|
|
2987
|
+
const facts = new Map();
|
|
2988
|
+
for (const root of projectRoots) {
|
|
2989
|
+
const merged = new Set(await (0, git_js_1.mergedBranches)(root));
|
|
2990
|
+
const base = await (0, git_js_1.integrationBranch)(root);
|
|
2991
|
+
if (base)
|
|
2992
|
+
merged.add(base);
|
|
2993
|
+
for (const m of merges)
|
|
2994
|
+
if (m.sourceBranch && rootOf(m.repoPath) === root)
|
|
2995
|
+
merged.add(m.sourceBranch);
|
|
2996
|
+
facts.set(root, { merged, exists: await (0, git_js_1.branchNames)(root) });
|
|
2997
|
+
}
|
|
2998
|
+
// Lo leído de GitHub con estela delivery: la rama de un PR fusionado está
|
|
2999
|
+
// entregada, aunque fuera con squash y git no lo sepa.
|
|
3000
|
+
for (const p of projects) {
|
|
3001
|
+
const prRoots = new Set(turns.filter((t) => t.projectId === p.id).map((t) => t.root));
|
|
3002
|
+
for (const item of store.listTrackerItems(db, p.id).items) {
|
|
3003
|
+
if (item.kind !== "pr" || !item.completed || !item.branch)
|
|
3004
|
+
continue;
|
|
3005
|
+
for (const root of prRoots)
|
|
3006
|
+
facts.get(root)?.merged.add(item.branch);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return (0, undelivered_js_1.undeliveredSpend)({
|
|
3010
|
+
turns, commits, staleDays, now: new Date(), ...(from ? { from } : {}), ...(to ? { to } : {}),
|
|
3011
|
+
subscriptions: store.listSubscriptions(db), monthTotals: store.totalConsumptionByMonth(db),
|
|
3012
|
+
branchFacts: (root, branch) => {
|
|
3013
|
+
const f = facts.get(root);
|
|
3014
|
+
return { merged: f?.merged.has(branch) ?? false, exists: f?.exists ? f.exists.has(branch) : null };
|
|
3015
|
+
},
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
function moneyCell(s) {
|
|
3019
|
+
return (s.real ? (0, shared_1.formatMoney)(s.real) : "—") + (s.realPartial ? "†" : "");
|
|
3020
|
+
}
|
|
3021
|
+
function pctCell(part, total) {
|
|
3022
|
+
const pct = (0, undelivered_js_1.shareOf)(part, total);
|
|
3023
|
+
return pct === null ? "—" : `${pct.toFixed(1)} %`;
|
|
3024
|
+
}
|
|
3025
|
+
function printUndelivered(reports, projects, staleDays, args) {
|
|
3026
|
+
console.log((0, index_js_1.tr) `Gasto de IA sin entrega · ${periodLabel(args)} · ramas sin fusionar tras ${staleDays} días`);
|
|
3027
|
+
console.log((0, index_js_1.tr) `Es una señal para revisar, no un juicio: explorar, probar y descartar también es trabajo.`);
|
|
3028
|
+
if (reports.length === 0) {
|
|
3029
|
+
console.log((0, index_js_1.tr) `\nNo hay gasto de IA en ese periodo.`);
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
const row = (label, s, pct) => ` ${label.slice(0, 46).padEnd(48)}${(0, shared_1.formatAiCost)(s.consumption).padStart(11)}` +
|
|
3033
|
+
`${moneyCell(s).padStart(13)}${pct.padStart(10)}`;
|
|
3034
|
+
let unpriced = false;
|
|
3035
|
+
let partial = false;
|
|
3036
|
+
let unknown = false;
|
|
3037
|
+
let unverifiable = false;
|
|
3038
|
+
for (const r of reports) {
|
|
3039
|
+
const p = projects.get(r.projectId);
|
|
3040
|
+
console.log(`\n${p?.name ?? r.projectId} · ${p?.clientName ?? ""}`);
|
|
3041
|
+
console.log(` ${"".padEnd(48)}${(0, index_js_1.tr) `API`.padStart(11)}${(0, index_js_1.tr) `Real`.padStart(13)}${(0, index_js_1.tr) `% gasto`.padStart(10)}`);
|
|
3042
|
+
console.log(row((0, index_js_1.tr) `Gasto del periodo`, r.total, "100 %"));
|
|
3043
|
+
const s = r.sessionsWithoutCommit;
|
|
3044
|
+
console.log(row((0, index_js_1.tr) `Sesiones sin commit (${s.count})`, s, pctCell(s, r.total)));
|
|
3045
|
+
if (s.count > 0) {
|
|
3046
|
+
const sinNada = s.count - s.laterCommitted.count;
|
|
3047
|
+
console.log((0, index_js_1.tr) ` ${s.laterCommitted.count} con commit después en su rama (${(0, shared_1.formatAiCost)(s.laterCommitted.consumption)}) · ${sinNada} sin commit después`);
|
|
3048
|
+
}
|
|
3049
|
+
if (r.unverifiable.count > 0) {
|
|
3050
|
+
unverifiable = true;
|
|
3051
|
+
console.log((0, index_js_1.tr) ` No cuentan otras ${r.unverifiable.count} (${(0, shared_1.formatAiCost)(r.unverifiable.consumption)}): son de antes de tu primer commit reconocido en el repo.`);
|
|
3052
|
+
}
|
|
3053
|
+
const u = r.unmerged;
|
|
3054
|
+
console.log(row((0, index_js_1.tr) `Ramas sin fusionar tras ${staleDays} días (${u.branches.length})`, u, pctCell(u, r.total)));
|
|
3055
|
+
for (const b of u.branches.slice(0, 8)) {
|
|
3056
|
+
console.log((0, index_js_1.tr) ` ${b.branch} · sin actividad hace ${b.idleDays} días · ${(0, shared_1.formatAiCost)(b.consumption)}`);
|
|
3057
|
+
}
|
|
3058
|
+
if (u.branches.length > 8)
|
|
3059
|
+
console.log((0, index_js_1.tr) ` … y ${u.branches.length - 8} más (--json para verlas todas)`);
|
|
3060
|
+
console.log(row((0, index_js_1.tr) `Sin entrega en total`, r.undelivered, pctCell(r.undelivered, r.total)));
|
|
3061
|
+
const notCounted = (state) => r.notCounted.filter((b) => b.state === state);
|
|
3062
|
+
const open = notCounted("open");
|
|
3063
|
+
const dudosas = notCounted("unknown");
|
|
3064
|
+
if (open.length) {
|
|
3065
|
+
console.log((0, index_js_1.tr) ` No cuenta: ${open.length} ramas abiertas hace menos de ${staleDays} días (${(0, shared_1.formatAiCost)(sumSpend(open))}).`);
|
|
3066
|
+
}
|
|
3067
|
+
if (dudosas.length) {
|
|
3068
|
+
unknown = true;
|
|
3069
|
+
console.log((0, index_js_1.tr) ` No cuenta: ${dudosas.length} ramas borradas sin merge registrado (${(0, shared_1.formatAiCost)(sumSpend(dudosas))}): ${dudosas.slice(0, 4).map((b) => b.branch).join(", ")}${dudosas.length > 4 ? ", …" : ""}`);
|
|
3070
|
+
}
|
|
3071
|
+
unpriced ||= r.unpricedAgents;
|
|
3072
|
+
partial ||= r.total.realPartial;
|
|
3073
|
+
}
|
|
3074
|
+
console.log((0, index_js_1.tr) `\nSesión sin commit: un rato de un chat (sin pausas de más de 30 min) sin ningún commit tuyo en ese repo durante él ni en los 30 min siguientes.`);
|
|
3075
|
+
console.log((0, index_js_1.tr) `Rama sin fusionar: existe, no está integrada y lleva ${staleDays} días sin turnos ni commits. Cambia el plazo con --stale-days.`);
|
|
3076
|
+
console.log((0, index_js_1.tr) `Una sesión sin commit no se cuenta otra vez en su rama. Las ramas principales (main, develop…) ya son entrega.`);
|
|
3077
|
+
console.log((0, index_js_1.tr) `API es lo que costaría a precio de API: mide el uso, no es dinero pagado. Real es tu cuota repartida por consumo.`);
|
|
3078
|
+
if (unverifiable) {
|
|
3079
|
+
console.log((0, index_js_1.tr) `Antes de tu primer commit reconocido no se sabe si commiteabas con otro correo: esas sesiones no cuentan. Revisa tus correos con estela author.`);
|
|
3080
|
+
}
|
|
3081
|
+
if (unknown)
|
|
3082
|
+
console.log((0, index_js_1.tr) `Una rama borrada sin merge pudo fusionarse con squash, que no deja rastro en git: por eso no cuenta.`);
|
|
3083
|
+
console.log((0, index_js_1.tr) `Un squash también hace que una rama que sigue existiendo parezca sin fusionar: revisa la lista antes de sacar conclusiones.`);
|
|
3084
|
+
if (unpriced)
|
|
3085
|
+
console.log((0, index_js_1.tr) `Incluye trabajo con Copilot: no guarda tokens, así que su coste no se conoce y no está sumado.`);
|
|
3086
|
+
if (partial)
|
|
3087
|
+
console.log((0, index_js_1.tr) `† Algún mes no tenía suscripción registrada: el dinero real solo cuenta los meses con cuota.`);
|
|
3088
|
+
}
|
|
3089
|
+
function sumSpend(lines) {
|
|
3090
|
+
return { microUsd: lines.reduce((s, l) => s + l.consumption.microUsd, 0) };
|
|
3091
|
+
}
|
|
3092
|
+
function undeliveredJson(reports, projects, staleDays, args) {
|
|
3093
|
+
const spend = (s) => ({
|
|
3094
|
+
apiMicroUsd: s.consumption.microUsd,
|
|
3095
|
+
real: s.real ? { amountMinor: s.real.amount, currency: s.real.currency } : null,
|
|
3096
|
+
realPartial: s.realPartial,
|
|
3097
|
+
});
|
|
3098
|
+
const line = (b) => ({ branch: b.branch, state: b.state, idleDays: b.idleDays, ...spend(b) });
|
|
3099
|
+
return {
|
|
3100
|
+
staleDays,
|
|
3101
|
+
period: { month: str(args, "month") ?? null, from: str(args, "from") ?? null, to: str(args, "to") ?? null },
|
|
3102
|
+
projects: reports.map((r) => ({
|
|
3103
|
+
id: r.projectId, name: projects.get(r.projectId)?.name ?? r.projectId,
|
|
3104
|
+
clientName: projects.get(r.projectId)?.clientName ?? null,
|
|
3105
|
+
total: spend(r.total),
|
|
3106
|
+
unverifiableSessions: { count: r.unverifiable.count, ...spend(r.unverifiable) },
|
|
3107
|
+
sessionsWithoutCommit: { count: r.sessionsWithoutCommit.count, ...spend(r.sessionsWithoutCommit),
|
|
3108
|
+
laterCommitted: { count: r.sessionsWithoutCommit.laterCommitted.count, ...spend(r.sessionsWithoutCommit.laterCommitted) } },
|
|
3109
|
+
unmergedBranches: { ...spend(r.unmerged), branches: r.unmerged.branches.map(line) },
|
|
3110
|
+
undelivered: { ...spend(r.undelivered), percentOfSpend: (0, undelivered_js_1.shareOf)(r.undelivered, r.total) },
|
|
3111
|
+
notCounted: r.notCounted.map(line),
|
|
3112
|
+
unpricedAgents: r.unpricedAgents,
|
|
3113
|
+
})),
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function periodLabel(args) {
|
|
3117
|
+
const month = str(args, "month");
|
|
3118
|
+
if (month)
|
|
3119
|
+
return month;
|
|
3120
|
+
const from = str(args, "from");
|
|
3121
|
+
const to = str(args, "to");
|
|
3122
|
+
if (!from && !to)
|
|
3123
|
+
return (0, index_js_1.tr) `todo el historial`;
|
|
3124
|
+
return `${from ?? "…"} → ${to ?? (0, index_js_1.tr) `hoy`}`;
|
|
3125
|
+
}
|
|
3126
|
+
function basisLabel(b) {
|
|
3127
|
+
switch (b) {
|
|
3128
|
+
case "agent-branch": return (0, index_js_1.tr) `rama anotada por el agente`;
|
|
3129
|
+
case "commit-branch": return (0, index_js_1.tr) `rama del commit, aproximada`;
|
|
3130
|
+
case "branch-name": return (0, index_js_1.tr) `número en el nombre de la rama`;
|
|
3131
|
+
case "commit-message": return (0, index_js_1.tr) `referencia en un commit`;
|
|
3132
|
+
case "manual": return (0, index_js_1.tr) `puesto a mano`;
|
|
3133
|
+
case "merge": return (0, index_js_1.tr) `commit de merge del PR`;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
function unitLabel(u, by) {
|
|
3137
|
+
if (u.key === null)
|
|
3138
|
+
return by === "branch" ? (0, index_js_1.tr) `Sin atribuir (sin rama)` : (0, index_js_1.tr) `Sin atribuir (sin ticket ni PR)`;
|
|
3139
|
+
if (by === "branch")
|
|
3140
|
+
return u.key;
|
|
3141
|
+
const fromMerge = u.basis.length === 1 && u.basis[0] === "merge";
|
|
3142
|
+
return u.items.map((i) => (fromMerge ? (0, index_js_1.tr) `PR ${(0, shared_1.formatWorkItem)(i)}` : (0, shared_1.formatWorkItem)(i))).join(", ");
|
|
3143
|
+
}
|
|
3144
|
+
function printSpend(reports, by, args, hasSubs) {
|
|
3145
|
+
const title = by === "branch" ? (0, index_js_1.tr) `Coste de IA por rama` : (0, index_js_1.tr) `Coste de IA por ticket o PR`;
|
|
3146
|
+
console.log(`${title} · ${periodLabel(args)}`);
|
|
3147
|
+
if (reports.length === 0) {
|
|
3148
|
+
console.log((0, index_js_1.tr) `\nNo hay horas en ese periodo.`);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
const real = (u) => hasSubs ? (u.real ? (0, shared_1.formatMoney)(u.real) : "—") + (u.realPartial ? "†" : "") : "—";
|
|
3152
|
+
const row = (label, u) => ` ${label.slice(0, 44).padEnd(46)}${(0, shared_1.formatDuration)(u.seconds).padStart(9)}` +
|
|
3153
|
+
`${((0, shared_1.formatAiCost)(u.consumption) + (u.unpricedAgents ? "*" : "")).padStart(12)}${real(u).padStart(13)}`;
|
|
3154
|
+
let unpriced = false;
|
|
3155
|
+
let partial = false;
|
|
3156
|
+
let noTracker = false;
|
|
3157
|
+
for (const r of reports) {
|
|
3158
|
+
console.log(`\n${r.project.name} · ${r.project.clientName}`);
|
|
3159
|
+
console.log(` ${"".padEnd(46)}${(0, index_js_1.tr) `Horas`.padStart(9)}${(0, index_js_1.tr) `API`.padStart(12)}${(0, index_js_1.tr) `Real`.padStart(13)}`);
|
|
3160
|
+
for (const u of r.units) {
|
|
3161
|
+
console.log(row(unitLabel(u, by), u));
|
|
3162
|
+
const detail = [];
|
|
3163
|
+
if (by === "branch") {
|
|
3164
|
+
if (u.items.length)
|
|
3165
|
+
detail.push(u.items.map(shared_1.formatWorkItem).join(", "));
|
|
3166
|
+
if (u.prs.length)
|
|
3167
|
+
detail.push(u.prs.map((p) => (0, index_js_1.tr) `PR ${(0, shared_1.formatWorkItem)(p)}`).join(", "));
|
|
3168
|
+
}
|
|
3169
|
+
else if (u.branches.length) {
|
|
3170
|
+
detail.push((0, index_js_1.tr) `ramas: ${u.branches.join(", ")}`);
|
|
3171
|
+
}
|
|
3172
|
+
if (u.basis.length)
|
|
3173
|
+
detail.push((0, index_js_1.tr) `según: ${u.basis.map(basisLabel).join(", ")}`);
|
|
3174
|
+
if (detail.length)
|
|
3175
|
+
console.log(` ${detail.join(" · ")}`);
|
|
3176
|
+
}
|
|
3177
|
+
console.log(row((0, index_js_1.tr) `Total`, r.total));
|
|
3178
|
+
unpriced ||= r.total.unpricedAgents;
|
|
3179
|
+
partial ||= r.total.realPartial;
|
|
3180
|
+
if (by === "item" && !r.project.tracker)
|
|
3181
|
+
noTracker = true;
|
|
3182
|
+
}
|
|
3183
|
+
console.log((0, index_js_1.tr) `\nAPI es lo que costaría a precio de API: mide el uso, no es dinero pagado.`);
|
|
3184
|
+
if (hasSubs)
|
|
3185
|
+
console.log((0, index_js_1.tr) `Real es tu cuota repartida por consumo: esto sí es dinero pagado.`);
|
|
3186
|
+
else
|
|
3187
|
+
console.log((0, index_js_1.tr) `Sin suscripciones registradas no hay dinero real. Si pagas cuota fija: estela subscription add`);
|
|
3188
|
+
console.log((0, index_js_1.tr) `Lo que no se puede atribuir va a "Sin atribuir" y no se reparte entre lo demás.`);
|
|
3189
|
+
if (unpriced)
|
|
3190
|
+
console.log((0, index_js_1.tr) `* Incluye trabajo con Copilot: sus horas cuentan, pero su coste no se conoce y no está sumado.`);
|
|
3191
|
+
if (partial)
|
|
3192
|
+
console.log((0, index_js_1.tr) `† Algún mes no tenía suscripción registrada: el dinero real solo cuenta los meses con cuota.`);
|
|
3193
|
+
if (noTracker) {
|
|
3194
|
+
console.log((0, index_js_1.tr) `Sin gestor configurado solo se reconocen tickets inequívocos (AB#12, PROJ-12) y los PR de los merges.`);
|
|
3195
|
+
console.log((0, index_js_1.tr) `Para leer #12 y feat/12-login: estela project tracker --project <id> --system github`);
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
function spendJson(reports, by, args) {
|
|
3199
|
+
const realJson = (m) => (m ? { amountMinor: m.amount, currency: m.currency } : null);
|
|
3200
|
+
return {
|
|
3201
|
+
by,
|
|
3202
|
+
period: { month: str(args, "month") ?? null, from: str(args, "from") ?? null, to: str(args, "to") ?? null },
|
|
3203
|
+
projects: reports.map((r) => ({
|
|
3204
|
+
id: r.project.id, name: r.project.name, clientId: r.project.clientId, clientName: r.project.clientName,
|
|
3205
|
+
units: r.units.map((u) => ({
|
|
3206
|
+
key: u.key, attributed: u.key !== null, branches: u.branches, items: u.items, prs: u.prs,
|
|
3207
|
+
basis: u.basis, seconds: u.seconds, apiMicroUsd: u.consumption.microUsd,
|
|
3208
|
+
real: realJson(u.real), realPartial: u.realPartial, unpricedAgents: u.unpricedAgents,
|
|
3209
|
+
})),
|
|
3210
|
+
total: {
|
|
3211
|
+
seconds: r.total.seconds, apiMicroUsd: r.total.consumption.microUsd, real: realJson(r.total.real),
|
|
3212
|
+
realPartial: r.total.realPartial, unpricedAgents: r.total.unpricedAgents,
|
|
3213
|
+
},
|
|
3214
|
+
})),
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
2150
3217
|
/**
|
|
2151
3218
|
* El proyecto de un comando: el de --project, o el del repositorio en el que
|
|
2152
3219
|
* se está. Es lo natural para lo que se pega en el README de ese repositorio.
|
|
@@ -2590,6 +3657,9 @@ async function main() {
|
|
|
2590
3657
|
case "client add":
|
|
2591
3658
|
cmdClientAdd(args, dbPath);
|
|
2592
3659
|
break;
|
|
3660
|
+
case "client alias":
|
|
3661
|
+
cmdClientAlias(args, dbPath);
|
|
3662
|
+
break;
|
|
2593
3663
|
case "project add":
|
|
2594
3664
|
cmdProjectAdd(args, dbPath);
|
|
2595
3665
|
break;
|
|
@@ -2615,7 +3685,7 @@ async function main() {
|
|
|
2615
3685
|
cmdSubscriptionAdd(args, dbPath);
|
|
2616
3686
|
break;
|
|
2617
3687
|
case "ai-cost":
|
|
2618
|
-
cmdAiCost(dbPath);
|
|
3688
|
+
await cmdAiCost(args, dbPath);
|
|
2619
3689
|
break;
|
|
2620
3690
|
case "quota":
|
|
2621
3691
|
await cmdQuota(args, dbPath);
|
|
@@ -2656,6 +3726,12 @@ async function main() {
|
|
|
2656
3726
|
case "unpublish":
|
|
2657
3727
|
await cmdUnpublish(args, dbPath);
|
|
2658
3728
|
break;
|
|
3729
|
+
case "delivery":
|
|
3730
|
+
await cmdDelivery(args, dbPath);
|
|
3731
|
+
break;
|
|
3732
|
+
case "summary":
|
|
3733
|
+
await cmdSummary(args, dbPath);
|
|
3734
|
+
break;
|
|
2659
3735
|
case "plan":
|
|
2660
3736
|
cmdPlan(args, dbPath);
|
|
2661
3737
|
break;
|