estela 0.1.38 → 0.1.39
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 +17 -0
- package/dist/cli-badge.test.d.ts +2 -0
- package/dist/cli-badge.test.d.ts.map +1 -0
- package/dist/cli-badge.test.js +118 -0
- package/dist/cli-badge.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +130 -0
- package/dist/cli.js.map +1 -1
- package/dist/export/badge.d.ts +26 -0
- package/dist/export/badge.d.ts.map +1 -0
- package/dist/export/badge.js +37 -0
- package/dist/export/badge.js.map +1 -0
- package/dist/export/badge.test.d.ts +2 -0
- package/dist/export/badge.test.d.ts.map +1 -0
- package/dist/export/badge.test.js +32 -0
- package/dist/export/badge.test.js.map +1 -0
- package/dist/export/idioma.test.js +6 -2
- package/dist/export/idioma.test.js.map +1 -1
- package/dist/export/panel.d.ts +7 -0
- package/dist/export/panel.d.ts.map +1 -1
- package/dist/export/panel.js +25 -6
- package/dist/export/panel.js.map +1 -1
- package/dist/export/panel.test.js +17 -4
- package/dist/export/panel.test.js.map +1 -1
- package/dist/export/pixelfont.d.ts +17 -0
- package/dist/export/pixelfont.d.ts.map +1 -0
- package/dist/export/pixelfont.js +157 -0
- package/dist/export/pixelfont.js.map +1 -0
- package/dist/export/png.d.ts +12 -0
- package/dist/export/png.d.ts.map +1 -0
- package/dist/export/png.js +85 -0
- package/dist/export/png.js.map +1 -0
- package/dist/export/receipt.d.ts +58 -0
- package/dist/export/receipt.d.ts.map +1 -0
- package/dist/export/receipt.js +216 -0
- package/dist/export/receipt.js.map +1 -0
- package/dist/export/receipt.test.d.ts +2 -0
- package/dist/export/receipt.test.d.ts.map +1 -0
- package/dist/export/receipt.test.js +207 -0
- package/dist/export/receipt.test.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +25 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/publish.d.ts +2 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +3 -1
- package/dist/publish.js.map +1 -1
- package/node_modules/@estela/shared/dist/money.d.ts +6 -0
- package/node_modules/@estela/shared/dist/money.d.ts.map +1 -1
- package/node_modules/@estela/shared/dist/money.js +11 -0
- package/node_modules/@estela/shared/dist/money.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,13 @@ What it doesn't do:
|
|
|
51
51
|
with which model.
|
|
52
52
|
- **It doesn't send anything anywhere.** The free plan is entirely local.
|
|
53
53
|
|
|
54
|
+
### Already using ccusage?
|
|
55
|
+
|
|
56
|
+
Keep it. [ccusage](https://github.com/ryoppippi/ccusage) reads the same files
|
|
57
|
+
and tells you what the AI cost you; Estela tells you how many hours you worked
|
|
58
|
+
and what to bill the client. Both only read, so they run side by side.
|
|
59
|
+
[Side-by-side comparison](https://getestela.dev/en/vs/ccusage).
|
|
60
|
+
|
|
54
61
|
## Getting started
|
|
55
62
|
|
|
56
63
|
You need **Node 22.5 or later** (for `node:sqlite`).
|
|
@@ -248,6 +255,16 @@ Node 22 and **zero runtime dependencies**, on purpose: nobody installs a program
|
|
|
248
255
|
that reads their transcripts if they can't audit it, and an empty dependency
|
|
249
256
|
list can be audited in an afternoon.
|
|
250
257
|
|
|
258
|
+
## Contributing
|
|
259
|
+
|
|
260
|
+
Issues and pull requests are welcome, and the
|
|
261
|
+
[good first issues](https://github.com/jdleonruiz/estela-cli/labels/good%20first%20issue)
|
|
262
|
+
are a good place to start. One thing to know so nobody's put off: this
|
|
263
|
+
repository mirrors the open part of a larger repo where the paid service also
|
|
264
|
+
lives, which is why each commit here is a release. I review PRs here and apply
|
|
265
|
+
them to the main repo keeping you as the author, and the next release commit
|
|
266
|
+
credits you as co-author. PRs welcome — I merge them into the main repo.
|
|
267
|
+
|
|
251
268
|
## Open source, closed service
|
|
252
269
|
|
|
253
270
|
Everything that installs on your machine is open source under the MIT license:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-badge.test.d.ts","sourceRoot":"","sources":["../src/cli-badge.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const node_os_1 = require("node:os");
|
|
43
|
+
const node_path_1 = require("node:path");
|
|
44
|
+
const node_test_1 = require("node:test");
|
|
45
|
+
const schema_js_1 = require("./db/schema.js");
|
|
46
|
+
const store = __importStar(require("./db/store.js"));
|
|
47
|
+
/**
|
|
48
|
+
* `estela badge`, ejecutando la CLI de verdad: lo que importa es la línea que
|
|
49
|
+
* alguien pega en su README, y que no cuente como medido lo que no lo es.
|
|
50
|
+
*/
|
|
51
|
+
const BIN = (0, node_path_1.join)(__dirname, "bin.js");
|
|
52
|
+
function estela(db, ...args) {
|
|
53
|
+
const r = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, ...args, "--db", db], {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
env: { ...process.env, ESTELA_LANG: "es", NO_COLOR: "1" },
|
|
56
|
+
});
|
|
57
|
+
return { code: r.status, out: `${r.stdout}${r.stderr}` };
|
|
58
|
+
}
|
|
59
|
+
function bloque(id, source, hours, aiUsd) {
|
|
60
|
+
const startedAt = new Date("2026-08-10T10:00:00Z");
|
|
61
|
+
return {
|
|
62
|
+
id, projectId: "p", startedAt, endedAt: new Date(startedAt.getTime() + hours * 3600_000),
|
|
63
|
+
seconds: hours * 3600, description: "Trabajo", billable: true, invoiceId: null,
|
|
64
|
+
aiCost: { microUsd: aiUsd * 1_000_000 }, agentSeconds: source === "agent" ? hours * 3600 : 0,
|
|
65
|
+
commitHashes: [], agents: [], source, kind: "development", branch: "main",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function preparar(bloques, cuota) {
|
|
69
|
+
const path = (0, node_path_1.join)((0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "estela-badge-")), "estela.db");
|
|
70
|
+
const db = (0, schema_js_1.openDatabase)(path);
|
|
71
|
+
try {
|
|
72
|
+
store.upsertClient(db, { id: "c", name: "Interno", currency: "EUR" });
|
|
73
|
+
store.upsertProject(db, {
|
|
74
|
+
id: "p", clientId: "c", name: "Web", repoPaths: [],
|
|
75
|
+
billable: true, roundingMinutes: 0, aiCostPolicy: "absorbed", kind: "client",
|
|
76
|
+
});
|
|
77
|
+
for (const b of bloques)
|
|
78
|
+
store.saveTimeEntry(db, b);
|
|
79
|
+
if (cuota !== undefined) {
|
|
80
|
+
store.upsertSubscription(db, {
|
|
81
|
+
id: "max", name: "Claude Max", monthlyFee: { amount: cuota * 100, currency: "EUR" },
|
|
82
|
+
effectiveFrom: new Date("2026-01-01T00:00:00Z"), effectiveTo: null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
db.close();
|
|
88
|
+
}
|
|
89
|
+
return path;
|
|
90
|
+
}
|
|
91
|
+
(0, node_test_1.test)("badge: horas medidas y coste a tarifa API, sin contar lo estimado", () => {
|
|
92
|
+
const db = preparar([bloque("a", "agent", 42, 18.4), bloque("b", "commit", 5, 0)]);
|
|
93
|
+
const r = estela(db, "badge", "--project", "p");
|
|
94
|
+
strict_1.default.equal(r.code, 0, r.out);
|
|
95
|
+
strict_1.default.match(r.out, /^\[!\[Built in 42 h · \$18 of AI\]\(https:\/\/img\.shields\.io\/badge\/[^)]+\)\]\(https:\/\/getestela\.dev\/en\/\)$/m);
|
|
96
|
+
strict_1.default.match(r.out, /tarifa API/);
|
|
97
|
+
});
|
|
98
|
+
(0, node_test_1.test)("badge: con una cuota registrada, sale lo que se pagó de verdad", () => {
|
|
99
|
+
// Único proyecto con consumo ese mes: se lleva la cuota entera.
|
|
100
|
+
const db = preparar([bloque("a", "agent", 10, 50)], 100);
|
|
101
|
+
const r = estela(db, "badge", "--project", "p");
|
|
102
|
+
strict_1.default.equal(r.code, 0, r.out);
|
|
103
|
+
strict_1.default.match(r.out, /Built in 10 h · €100 of AI/);
|
|
104
|
+
strict_1.default.doesNotMatch(r.out, /tarifa API/);
|
|
105
|
+
});
|
|
106
|
+
(0, node_test_1.test)("badge: sin horas medidas no se inventa un badge", () => {
|
|
107
|
+
const db = preparar([bloque("b", "commit", 5, 0)]);
|
|
108
|
+
const r = estela(db, "badge", "--project", "p");
|
|
109
|
+
strict_1.default.notEqual(r.code, 0);
|
|
110
|
+
strict_1.default.match(r.out, /no tiene horas medidas/);
|
|
111
|
+
});
|
|
112
|
+
(0, node_test_1.test)("badge: un proyecto que no existe se dice así", () => {
|
|
113
|
+
const db = preparar([]);
|
|
114
|
+
const r = estela(db, "badge", "--project", "nada");
|
|
115
|
+
strict_1.default.notEqual(r.code, 0);
|
|
116
|
+
strict_1.default.match(r.out, /No existe el proyecto "nada"/);
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=cli-badge.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-badge.test.js","sourceRoot":"","sources":["../src/cli-badge.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6CA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAs/DpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,8 @@ process.on("warning", (w) => {
|
|
|
46
46
|
const shared_1 = require("@estela/shared");
|
|
47
47
|
const node_fs_1 = require("node:fs");
|
|
48
48
|
const amortize_js_1 = require("./billing/amortize.js");
|
|
49
|
+
const badge_js_1 = require("./export/badge.js");
|
|
50
|
+
const receipt_js_1 = require("./export/receipt.js");
|
|
49
51
|
const invoice_js_1 = require("./billing/invoice.js");
|
|
50
52
|
const sessionize_js_1 = require("./billing/sessionize.js");
|
|
51
53
|
const schema_js_1 = require("./db/schema.js");
|
|
@@ -141,6 +143,14 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
141
143
|
estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
|
|
142
144
|
Pone a mano los tickets de un bloque. El import ya no los cambia.
|
|
143
145
|
estela ai-cost Reparto de tu cuota entre proyectos.
|
|
146
|
+
estela badge [--project <id>]
|
|
147
|
+
Badge para el README: horas medidas y coste de IA del proyecto.
|
|
148
|
+
Sin --project, el del repositorio en el que estás.
|
|
149
|
+
estela receipt [--week] [--names] [--out <fichero.png>]
|
|
150
|
+
Un recibo en PNG: horas medidas y estimadas, coste de IA, lo más
|
|
151
|
+
caro y lo que ahorró la caché. Con --week, los últimos 7 días.
|
|
152
|
+
Sin nombres de proyectos ni ramas salvo con --names. Se genera y
|
|
153
|
+
se queda en tu máquina.
|
|
144
154
|
estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
145
155
|
[--author <tu nombre>] [--with-amounts] [--out <fichero.html>]
|
|
146
156
|
Informe compartible en un solo fichero HTML. Horas y commits; sin
|
|
@@ -148,12 +158,15 @@ estela — registro de horas para desarrollo asistido por IA
|
|
|
148
158
|
|
|
149
159
|
estela publish --project <id> [--author <tu nombre>] [--with-amounts]
|
|
150
160
|
[--token <existente>] [--no-team] [--client <correo>[,otro]]
|
|
161
|
+
[--no-credit]
|
|
151
162
|
Panel de solo lectura para que tu cliente USE el producto, no solo
|
|
152
163
|
reciba un documento. Se aloja en getestela.dev — Free permite uno a
|
|
153
164
|
la vez, Pro y Teams sin límite. Necesita "estela login" antes.
|
|
154
165
|
Con --client se le avisa por correo y le aparece en su cuenta al
|
|
155
166
|
entrar en getestela.dev/app. Sin --client no se toca esa lista: tus
|
|
156
167
|
republicaciones no desvinculan a quien ya lo tenía.
|
|
168
|
+
--no-credit quita la línea "Horas rellenadas por Estela" del pie
|
|
169
|
+
(Pro y Teams).
|
|
157
170
|
|
|
158
171
|
estela team ai-cost --project <id> --allow | --deny
|
|
159
172
|
Si quien te invitó pide ver el coste de IA del proyecto, esta es tu
|
|
@@ -289,6 +302,14 @@ estela — time tracking for AI-assisted development
|
|
|
289
302
|
estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
|
|
290
303
|
Sets a block's tickets by hand. Imports won't change them anymore.
|
|
291
304
|
estela ai-cost How your subscription splits across projects.
|
|
305
|
+
estela badge [--project <id>]
|
|
306
|
+
Badge for your README: the project's measured hours and AI cost.
|
|
307
|
+
Without --project, the one for the repository you're in.
|
|
308
|
+
estela receipt [--week] [--names] [--out <file.png>]
|
|
309
|
+
A PNG receipt: measured and estimated hours, AI cost, the most
|
|
310
|
+
expensive work and what caching saved. With --week, the last 7 days.
|
|
311
|
+
No project or branch names unless you pass --names. It's generated
|
|
312
|
+
and stays on your machine.
|
|
292
313
|
estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
293
314
|
[--author <your name>] [--with-amounts] [--out <file.html>]
|
|
294
315
|
Shareable report in a single HTML file. Hours and commits; no AI
|
|
@@ -296,12 +317,15 @@ estela — time tracking for AI-assisted development
|
|
|
296
317
|
|
|
297
318
|
estela publish --project <id> [--author <your name>] [--with-amounts]
|
|
298
319
|
[--token <existing>] [--no-team] [--client <email>[,other]]
|
|
320
|
+
[--no-credit]
|
|
299
321
|
Read-only dashboard so your client can USE the product, not just
|
|
300
322
|
receive a document. Hosted on getestela.dev — Free allows one at a
|
|
301
323
|
time, Pro and Teams have no limit. Requires "estela login" first.
|
|
302
324
|
With --client they get an email and it shows up in their account
|
|
303
325
|
when they sign in to getestela.dev/app. Without --client that list
|
|
304
326
|
isn't touched: republishing never unlinks anyone who already had it.
|
|
327
|
+
--no-credit removes the "Hours filled in by Estela" line from the
|
|
328
|
+
footer (Pro and Teams).
|
|
305
329
|
|
|
306
330
|
estela team ai-cost --project <id> --allow | --deny
|
|
307
331
|
If whoever invited you asks to see the project's AI cost, this is
|
|
@@ -1281,6 +1305,7 @@ async function cmdPublish(args, dbPath) {
|
|
|
1281
1305
|
...(clientes?.length ? { clients: clientes } : {}),
|
|
1282
1306
|
withAmounts: args.flags["with-amounts"] === true,
|
|
1283
1307
|
includeTeam: args.flags["no-team"] !== true,
|
|
1308
|
+
hideCredit: args.flags["no-credit"] === true,
|
|
1284
1309
|
...(explicitLang(args) ? { language: explicitLang(args) } : {}),
|
|
1285
1310
|
});
|
|
1286
1311
|
}
|
|
@@ -1820,6 +1845,105 @@ function cmdAiCost(dbPath) {
|
|
|
1820
1845
|
db.close();
|
|
1821
1846
|
}
|
|
1822
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* El proyecto de un comando: el de --project, o el del repositorio en el que
|
|
1850
|
+
* se está. Es lo natural para lo que se pega en el README de ese repositorio.
|
|
1851
|
+
*/
|
|
1852
|
+
async function projectFromArgsOrCwd(args, db) {
|
|
1853
|
+
const explicit = str(args, "project");
|
|
1854
|
+
if (explicit) {
|
|
1855
|
+
if (!store.getProject(db, explicit))
|
|
1856
|
+
throw new UserError((0, index_js_1.tr) `No existe el proyecto "${explicit}".`);
|
|
1857
|
+
return explicit;
|
|
1858
|
+
}
|
|
1859
|
+
const root = await (0, git_js_1.repoRoot)(process.cwd());
|
|
1860
|
+
const id = root ? store.projectForRepo(db, root) : null;
|
|
1861
|
+
if (!id)
|
|
1862
|
+
throw new UserError((0, index_js_1.tr) `Este directorio no es de ningún proyecto. Indica cuál con --project <id>.`);
|
|
1863
|
+
return id;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Lo que costó la IA de un proyecto, en dinero de verdad si se puede.
|
|
1867
|
+
*
|
|
1868
|
+
* Con una cuota registrada, es su reparto (lo que se pagó); sin ella, la
|
|
1869
|
+
* tarifa API equivalente, que es lo que valdría ese uso y así se marca. Los
|
|
1870
|
+
* meses sin cuota vigente no suman: ahí no hay reparto que hacer. `months`
|
|
1871
|
+
* limita a esos meses; sin él, cuenta toda la historia del proyecto.
|
|
1872
|
+
*/
|
|
1873
|
+
function projectAiCost(db, projectId, entries, months) {
|
|
1874
|
+
const subs = store.listSubscriptions(db);
|
|
1875
|
+
if (subs.length) {
|
|
1876
|
+
const shares = (0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db))
|
|
1877
|
+
.filter((sh) => sh.projectId === projectId && (0, amortize_js_1.feeForMonth)(subs, sh.month) !== null &&
|
|
1878
|
+
(!months || months.includes(sh.month)));
|
|
1879
|
+
if (shares.length) {
|
|
1880
|
+
const currency = shares[0].amount.currency;
|
|
1881
|
+
return { money: { amount: shares.reduce((n, sh) => n + sh.amount.amount, 0), currency }, api: false };
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const micro = entries.reduce((n, e) => n + e.aiCost.microUsd, 0);
|
|
1885
|
+
if (micro === 0)
|
|
1886
|
+
return null;
|
|
1887
|
+
return { money: { amount: Math.round(micro / 10_000), currency: "USD" }, api: true };
|
|
1888
|
+
}
|
|
1889
|
+
async function cmdBadge(args, dbPath) {
|
|
1890
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1891
|
+
try {
|
|
1892
|
+
const projectId = await projectFromArgsOrCwd(args, db);
|
|
1893
|
+
const entries = store.getTimeEntries(db, projectId);
|
|
1894
|
+
const measuredSeconds = entries.filter((e) => e.source === "agent").reduce((n, e) => n + e.seconds, 0);
|
|
1895
|
+
if (measuredSeconds === 0) {
|
|
1896
|
+
throw new UserError((0, index_js_1.tr) `"${projectId}" no tiene horas medidas todavía: el badge solo cuenta las de sesiones de agente.`);
|
|
1897
|
+
}
|
|
1898
|
+
const cost = projectAiCost(db, projectId, entries);
|
|
1899
|
+
const text = (0, badge_js_1.badgeText)({ measuredSeconds, aiCost: cost ? (0, shared_1.formatMoneyWhole)(cost.money) : null });
|
|
1900
|
+
console.log((0, badge_js_1.badgeMarkdown)(text));
|
|
1901
|
+
console.log("");
|
|
1902
|
+
console.log((0, index_js_1.tr) `Vista previa: ${(0, badge_js_1.badgeUrl)(text)}`);
|
|
1903
|
+
if (cost?.api) {
|
|
1904
|
+
console.log((0, index_js_1.tr) `El coste es a tarifa API. Si pagas cuota fija, regístrala para que salga lo que pagas de verdad:`);
|
|
1905
|
+
console.log((0, index_js_1.tr) ` estela subscription add --id claude-max --name "Claude Max" --fee 200`);
|
|
1906
|
+
}
|
|
1907
|
+
console.log((0, index_js_1.tr) `Es una imagen estática: para actualizar las cifras, vuelve a generarlo.`);
|
|
1908
|
+
}
|
|
1909
|
+
finally {
|
|
1910
|
+
db.close();
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function cmdReceipt(args, dbPath) {
|
|
1914
|
+
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1915
|
+
try {
|
|
1916
|
+
const week = args.flags["week"] === true;
|
|
1917
|
+
// Hasta el final de hoy, en la hora de quien lo pide; con --week, desde
|
|
1918
|
+
// el principio del día de hace seis: siete días naturales contando hoy.
|
|
1919
|
+
const hoy = new Date();
|
|
1920
|
+
const to = new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() + 1);
|
|
1921
|
+
const from = week ? new Date(hoy.getFullYear(), hoy.getMonth(), hoy.getDate() - 6) : null;
|
|
1922
|
+
const projects = store.listProjects(db);
|
|
1923
|
+
const entries = projects.flatMap((p) => store.getTimeEntries(db, p.id));
|
|
1924
|
+
if (entries.length === 0)
|
|
1925
|
+
throw new UserError((0, index_js_1.tr) `Sin bloques imputados. Ejecuta: estela import`);
|
|
1926
|
+
const turns = db.prepare(`SELECT at, model, tok_cache_read, tok_cache_w5m, tok_cache_w1h FROM agent_turns
|
|
1927
|
+
WHERE at >= ? AND at < ?`).all((from ?? new Date(0)).toISOString(), to.toISOString()).map((t) => ({
|
|
1928
|
+
at: new Date(t.at), model: t.model,
|
|
1929
|
+
cacheRead: t.tok_cache_read, cacheWrite5m: t.tok_cache_w5m, cacheWrite1h: t.tok_cache_w1h,
|
|
1930
|
+
}));
|
|
1931
|
+
const data = (0, receipt_js_1.receiptData)({
|
|
1932
|
+
entries, turns, from, to, projectNames: new Map(projects.map((p) => [p.id, p.name])),
|
|
1933
|
+
});
|
|
1934
|
+
const out = (0, node_path_1.resolve)(str(args, "out") ??
|
|
1935
|
+
(week ? `estela-receipt-${(0, shared_1.localDate)(hoy)}.png` : "estela-receipt.png"));
|
|
1936
|
+
const names = args.flags["names"] === true;
|
|
1937
|
+
(0, node_fs_1.writeFileSync)(out, (0, receipt_js_1.renderReceipt)(data, { week, names }));
|
|
1938
|
+
console.log((0, index_js_1.tr) `Recibo guardado en ${out}`);
|
|
1939
|
+
if (!names)
|
|
1940
|
+
console.log((0, index_js_1.tr) `Sin nombres de proyectos ni ramas, para que puedas compartirlo. Con --names, se incluyen.`);
|
|
1941
|
+
console.log((0, index_js_1.tr) `Se ha generado en tu máquina y no se ha enviado a ningún sitio.`);
|
|
1942
|
+
}
|
|
1943
|
+
finally {
|
|
1944
|
+
db.close();
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1823
1947
|
function cmdInvoice(args, dbPath) {
|
|
1824
1948
|
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
1825
1949
|
try {
|
|
@@ -1982,6 +2106,12 @@ async function main() {
|
|
|
1982
2106
|
case "ai-cost":
|
|
1983
2107
|
cmdAiCost(dbPath);
|
|
1984
2108
|
break;
|
|
2109
|
+
case "badge":
|
|
2110
|
+
await cmdBadge(args, dbPath);
|
|
2111
|
+
break;
|
|
2112
|
+
case "receipt":
|
|
2113
|
+
cmdReceipt(args, dbPath);
|
|
2114
|
+
break;
|
|
1985
2115
|
case "status":
|
|
1986
2116
|
cmdStatus(dbPath);
|
|
1987
2117
|
break;
|