estela 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +138 -0
  2. package/dist/billing/amortize.d.ts +44 -0
  3. package/dist/billing/amortize.d.ts.map +1 -0
  4. package/dist/billing/amortize.js +83 -0
  5. package/dist/billing/amortize.js.map +1 -0
  6. package/dist/billing/amortize.test.d.ts +2 -0
  7. package/dist/billing/amortize.test.d.ts.map +1 -0
  8. package/dist/billing/amortize.test.js +121 -0
  9. package/dist/billing/amortize.test.js.map +1 -0
  10. package/dist/billing/format.test.d.ts +2 -0
  11. package/dist/billing/format.test.d.ts.map +1 -0
  12. package/dist/billing/format.test.js +110 -0
  13. package/dist/billing/format.test.js.map +1 -0
  14. package/dist/billing/invoice.d.ts +52 -0
  15. package/dist/billing/invoice.d.ts.map +1 -0
  16. package/dist/billing/invoice.js +148 -0
  17. package/dist/billing/invoice.js.map +1 -0
  18. package/dist/billing/invoice.test.d.ts +2 -0
  19. package/dist/billing/invoice.test.d.ts.map +1 -0
  20. package/dist/billing/invoice.test.js +200 -0
  21. package/dist/billing/invoice.test.js.map +1 -0
  22. package/dist/billing/sessionize.d.ts +104 -0
  23. package/dist/billing/sessionize.d.ts.map +1 -0
  24. package/dist/billing/sessionize.js +0 -0
  25. package/dist/billing/sessionize.js.map +1 -0
  26. package/dist/billing/sessionize.test.d.ts +2 -0
  27. package/dist/billing/sessionize.test.d.ts.map +1 -0
  28. package/dist/billing/sessionize.test.js +67 -0
  29. package/dist/billing/sessionize.test.js.map +1 -0
  30. package/dist/cli.d.ts +4 -0
  31. package/dist/cli.d.ts.map +1 -0
  32. package/dist/cli.js +1091 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/cloud/auth.d.ts +4 -0
  35. package/dist/cloud/auth.d.ts.map +1 -0
  36. package/dist/cloud/auth.js +98 -0
  37. package/dist/cloud/auth.js.map +1 -0
  38. package/dist/cloud/client.d.ts +12 -0
  39. package/dist/cloud/client.d.ts.map +1 -0
  40. package/dist/cloud/client.js +34 -0
  41. package/dist/cloud/client.js.map +1 -0
  42. package/dist/db/schema.d.ts +11 -0
  43. package/dist/db/schema.d.ts.map +1 -0
  44. package/dist/db/schema.js +424 -0
  45. package/dist/db/schema.js.map +1 -0
  46. package/dist/db/schema.test.d.ts +2 -0
  47. package/dist/db/schema.test.d.ts.map +1 -0
  48. package/dist/db/schema.test.js +335 -0
  49. package/dist/db/schema.test.js.map +1 -0
  50. package/dist/db/store.d.ts +113 -0
  51. package/dist/db/store.d.ts.map +1 -0
  52. package/dist/db/store.js +550 -0
  53. package/dist/db/store.js.map +1 -0
  54. package/dist/doctor.d.ts +24 -0
  55. package/dist/doctor.d.ts.map +1 -0
  56. package/dist/doctor.js +229 -0
  57. package/dist/doctor.js.map +1 -0
  58. package/dist/export/csv.d.ts +9 -0
  59. package/dist/export/csv.d.ts.map +1 -0
  60. package/dist/export/csv.js +81 -0
  61. package/dist/export/csv.js.map +1 -0
  62. package/dist/export/invoice-pdf.d.ts +21 -0
  63. package/dist/export/invoice-pdf.d.ts.map +1 -0
  64. package/dist/export/invoice-pdf.js +135 -0
  65. package/dist/export/invoice-pdf.js.map +1 -0
  66. package/dist/export/panel.d.ts +78 -0
  67. package/dist/export/panel.d.ts.map +1 -0
  68. package/dist/export/panel.js +693 -0
  69. package/dist/export/panel.js.map +1 -0
  70. package/dist/export/panel.test.d.ts +2 -0
  71. package/dist/export/panel.test.d.ts.map +1 -0
  72. package/dist/export/panel.test.js +273 -0
  73. package/dist/export/panel.test.js.map +1 -0
  74. package/dist/export/pdf.d.ts +32 -0
  75. package/dist/export/pdf.d.ts.map +1 -0
  76. package/dist/export/pdf.js +229 -0
  77. package/dist/export/pdf.js.map +1 -0
  78. package/dist/export/pdf.test.d.ts +2 -0
  79. package/dist/export/pdf.test.d.ts.map +1 -0
  80. package/dist/export/pdf.test.js +151 -0
  81. package/dist/export/pdf.test.js.map +1 -0
  82. package/dist/export/share.d.ts +42 -0
  83. package/dist/export/share.d.ts.map +1 -0
  84. package/dist/export/share.js +181 -0
  85. package/dist/export/share.js.map +1 -0
  86. package/dist/export/share.test.d.ts +2 -0
  87. package/dist/export/share.test.d.ts.map +1 -0
  88. package/dist/export/share.test.js +116 -0
  89. package/dist/export/share.test.js.map +1 -0
  90. package/dist/metrics/index.d.ts +101 -0
  91. package/dist/metrics/index.d.ts.map +1 -0
  92. package/dist/metrics/index.js +221 -0
  93. package/dist/metrics/index.js.map +1 -0
  94. package/dist/metrics/metrics.test.d.ts +2 -0
  95. package/dist/metrics/metrics.test.d.ts.map +1 -0
  96. package/dist/metrics/metrics.test.js +162 -0
  97. package/dist/metrics/metrics.test.js.map +1 -0
  98. package/dist/metrics/people.d.ts +46 -0
  99. package/dist/metrics/people.d.ts.map +1 -0
  100. package/dist/metrics/people.js +126 -0
  101. package/dist/metrics/people.js.map +1 -0
  102. package/dist/metrics/people.test.d.ts +2 -0
  103. package/dist/metrics/people.test.d.ts.map +1 -0
  104. package/dist/metrics/people.test.js +84 -0
  105. package/dist/metrics/people.test.js.map +1 -0
  106. package/dist/metrics/team.d.ts +41 -0
  107. package/dist/metrics/team.d.ts.map +1 -0
  108. package/dist/metrics/team.js +0 -0
  109. package/dist/metrics/team.js.map +1 -0
  110. package/dist/metrics/team.test.d.ts +2 -0
  111. package/dist/metrics/team.test.d.ts.map +1 -0
  112. package/dist/metrics/team.test.js +90 -0
  113. package/dist/metrics/team.test.js.map +1 -0
  114. package/dist/pricing/catalog.d.ts +39 -0
  115. package/dist/pricing/catalog.d.ts.map +1 -0
  116. package/dist/pricing/catalog.js +59 -0
  117. package/dist/pricing/catalog.js.map +1 -0
  118. package/dist/pricing/cost.d.ts +25 -0
  119. package/dist/pricing/cost.d.ts.map +1 -0
  120. package/dist/pricing/cost.js +50 -0
  121. package/dist/pricing/cost.js.map +1 -0
  122. package/dist/pricing/cost.test.d.ts +2 -0
  123. package/dist/pricing/cost.test.d.ts.map +1 -0
  124. package/dist/pricing/cost.test.js +74 -0
  125. package/dist/pricing/cost.test.js.map +1 -0
  126. package/dist/server.d.ts +15 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +1141 -0
  129. package/dist/server.js.map +1 -0
  130. package/dist/setup.d.ts +70 -0
  131. package/dist/setup.d.ts.map +1 -0
  132. package/dist/setup.js +223 -0
  133. package/dist/setup.js.map +1 -0
  134. package/dist/setup.test.d.ts +2 -0
  135. package/dist/setup.test.d.ts.map +1 -0
  136. package/dist/setup.test.js +131 -0
  137. package/dist/setup.test.js.map +1 -0
  138. package/dist/watchers/claude.d.ts +34 -0
  139. package/dist/watchers/claude.d.ts.map +1 -0
  140. package/dist/watchers/claude.js +235 -0
  141. package/dist/watchers/claude.js.map +1 -0
  142. package/dist/watchers/claude.test.d.ts +2 -0
  143. package/dist/watchers/claude.test.d.ts.map +1 -0
  144. package/dist/watchers/claude.test.js +180 -0
  145. package/dist/watchers/claude.test.js.map +1 -0
  146. package/dist/watchers/git.d.ts +42 -0
  147. package/dist/watchers/git.d.ts.map +1 -0
  148. package/dist/watchers/git.js +218 -0
  149. package/dist/watchers/git.js.map +1 -0
  150. package/dist/watchers/git.test.d.ts +2 -0
  151. package/dist/watchers/git.test.d.ts.map +1 -0
  152. package/dist/watchers/git.test.js +73 -0
  153. package/dist/watchers/git.test.js.map +1 -0
  154. package/dist/watchers/identity.d.ts +34 -0
  155. package/dist/watchers/identity.d.ts.map +1 -0
  156. package/dist/watchers/identity.js +85 -0
  157. package/dist/watchers/identity.js.map +1 -0
  158. package/dist/watchers/identity.test.d.ts +2 -0
  159. package/dist/watchers/identity.test.d.ts.map +1 -0
  160. package/dist/watchers/identity.test.js +48 -0
  161. package/dist/watchers/identity.test.js.map +1 -0
  162. package/dist/web-ui.test.d.ts +2 -0
  163. package/dist/web-ui.test.d.ts.map +1 -0
  164. package/dist/web-ui.test.js +312 -0
  165. package/dist/web-ui.test.js.map +1 -0
  166. package/node_modules/@estela/shared/dist/index.d.ts +3 -0
  167. package/node_modules/@estela/shared/dist/index.d.ts.map +1 -0
  168. package/node_modules/@estela/shared/dist/index.js +19 -0
  169. package/node_modules/@estela/shared/dist/index.js.map +1 -0
  170. package/node_modules/@estela/shared/dist/money.d.ts +71 -0
  171. package/node_modules/@estela/shared/dist/money.d.ts.map +1 -0
  172. package/node_modules/@estela/shared/dist/money.js +171 -0
  173. package/node_modules/@estela/shared/dist/money.js.map +1 -0
  174. package/node_modules/@estela/shared/dist/types.d.ts +365 -0
  175. package/node_modules/@estela/shared/dist/types.d.ts.map +1 -0
  176. package/node_modules/@estela/shared/dist/types.js +24 -0
  177. package/node_modules/@estela/shared/dist/types.js.map +1 -0
  178. package/node_modules/@estela/shared/package.json +5 -0
  179. package/package.json +41 -0
  180. package/web/app.css +749 -0
  181. package/web/app.js +1257 -0
  182. package/web/apple-touch-icon.png +0 -0
  183. package/web/favicon.png +0 -0
  184. package/web/index.html +132 -0
  185. package/web/lib.js +94 -0
  186. package/web/logo-mark.png +0 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1091 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.breakdownOfTurn = void 0;
38
+ const node_path_1 = require("node:path");
39
+ // `node:sqlite` avisa de que es experimental en cada arranque. Es ruido para
40
+ // quien solo quiere su factura, y el aviso no le dice nada accionable.
41
+ process.removeAllListeners("warning");
42
+ process.on("warning", (w) => {
43
+ if (w.name !== "ExperimentalWarning")
44
+ console.warn(w);
45
+ });
46
+ const shared_1 = require("@estela/shared");
47
+ const node_fs_1 = require("node:fs");
48
+ const amortize_js_1 = require("./billing/amortize.js");
49
+ const invoice_js_1 = require("./billing/invoice.js");
50
+ const sessionize_js_1 = require("./billing/sessionize.js");
51
+ const schema_js_1 = require("./db/schema.js");
52
+ const store = __importStar(require("./db/store.js"));
53
+ const doctor_js_1 = require("./doctor.js");
54
+ const setup_js_1 = require("./setup.js");
55
+ const csv_js_1 = require("./export/csv.js");
56
+ const invoice_pdf_js_1 = require("./export/invoice-pdf.js");
57
+ const panel_js_1 = require("./export/panel.js");
58
+ const share_js_1 = require("./export/share.js");
59
+ const server_js_1 = require("./server.js");
60
+ const auth_js_1 = require("./cloud/auth.js");
61
+ const client_js_1 = require("./cloud/client.js");
62
+ const cost_js_1 = require("./pricing/cost.js");
63
+ Object.defineProperty(exports, "breakdownOfTurn", { enumerable: true, get: function () { return cost_js_1.breakdownOfTurn; } });
64
+ const claude_js_1 = require("./watchers/claude.js");
65
+ const git_js_1 = require("./watchers/git.js");
66
+ const identity_js_1 = require("./watchers/identity.js");
67
+ const team_js_1 = require("./metrics/team.js");
68
+ const HELP = `
69
+ estela — registro de horas para desarrollo asistido por IA
70
+
71
+ estela setup
72
+ Primera ejecución: detecta tus agentes y repositorios, y reconstruye
73
+ tu historial. Dos minutos, sin preguntas y sin cuenta.
74
+
75
+ estela import [--since YYYY-MM-DD] [--repo <ruta>]
76
+ Reconstruye tu historial desde los transcripts de los agentes y git.
77
+
78
+ estela client add --id <id> --name <nombre> --currency <EUR|USD|...>
79
+ estela project add --id <id> --client <id> --name <nombre> --repo <ruta>
80
+ [--rounding <min>] [--ai-cost absorbed|passthrough]
81
+ estela rate set --project <id> --rate <importe> [--from YYYY-MM-DD]
82
+ estela budget --project <id> [--amount <dólares>|none]
83
+ Presupuesto mensual de IA. Sin --amount, consulta el actual. Alimenta
84
+ el aviso de la pestaña Equipo.
85
+
86
+ estela subscription add --id <id> --name <nombre> --fee <cuota> [--currency USD]
87
+ Registra una cuota fija (Claude Max, Cursor...). Con suscripción, el
88
+ gasto real es la cuota repartida por consumo, no la suma por token.
89
+
90
+ estela log --project <id> --hours <n> --what "<qué hiciste>"
91
+ [--kind development|meeting|research|review|travel|support|other]
92
+ [--date YYYY-MM-DD] [--minutes <n>]
93
+ Horas que ningún import va a deducir: reuniones, viajes, investigación,
94
+ y desarrollo sin agente que tampoco dejó commits.
95
+
96
+ estela author --project <id> [--email <a@b.com>[,otro@c.com]]
97
+ Con qué correo commiteas en ese proyecto. Sin --email, lista los
98
+ autores del repositorio para que elijas el tuyo.
99
+
100
+ estela web [--port 4319] Abre el panel en tu navegador.
101
+ estela doctor Revisa los datos y avisa de lo que
102
+ rompería una demo. Úsalo antes de publicar.
103
+ estela status Qué hay capturado y sin imputar.
104
+ estela entries --project <id> Bloques imputados a un proyecto.
105
+ estela ai-cost Reparto de tu cuota entre proyectos.
106
+ estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
107
+ [--author <tu nombre>] [--with-amounts] [--out <fichero.html>]
108
+ Informe compartible en un solo fichero HTML. Horas y commits; sin
109
+ consumo de IA, que es tuyo mientras la pagues tú.
110
+
111
+ estela publish --project <id> [--author <tu nombre>] [--with-amounts]
112
+ [--base-url https://tu-dominio] [--token <existente>]
113
+ [--no-team] [--site https://getestela.dev]
114
+ Panel de solo lectura para que tu cliente USE el producto, no solo
115
+ reciba un documento. Un fichero estático con enlace no adivinable.
116
+
117
+ estela login --email <tú@dominio.com> [--api <url>]
118
+ Vincula esta máquina a tu cuenta. Sin esto, Estela sigue siendo 100%
119
+ local: nada de lo de abajo cambia lo que ya haces sin cuenta.
120
+ estela logout
121
+ Desvincula esta máquina. Tus datos locales no se tocan.
122
+
123
+ estela export --project <id> [--out <fichero.csv>]
124
+
125
+ estela report --project <id> --cutoff YYYY-MM-DD [--dry-run]
126
+ [--pdf <fichero.pdf>] [--csv <fichero.csv>] [--fx <tipo>]
127
+ [--from-name <tu nombre>] [--from-email <email>]
128
+ Informe de horas y commits para respaldar tu trabajo. No es una
129
+ factura: Estela no emite documentos fiscales.
130
+
131
+ Opciones globales: --db <ruta> (por defecto ${schema_js_1.DEFAULT_DB_PATH})
132
+ `;
133
+ function parseArgs(argv) {
134
+ const positional = [];
135
+ const flags = {};
136
+ for (let i = 0; i < argv.length; i++) {
137
+ const arg = argv[i];
138
+ if (!arg.startsWith("--")) {
139
+ positional.push(arg);
140
+ continue;
141
+ }
142
+ const key = arg.slice(2);
143
+ const next = argv[i + 1];
144
+ if (next !== undefined && !next.startsWith("--")) {
145
+ flags[key] = next;
146
+ i++;
147
+ }
148
+ else
149
+ flags[key] = true;
150
+ }
151
+ return { _: positional, flags };
152
+ }
153
+ function str(args, key) {
154
+ const value = args.flags[key];
155
+ return typeof value === "string" ? value : undefined;
156
+ }
157
+ function required(args, key) {
158
+ const value = str(args, key);
159
+ if (!value)
160
+ throw new UserError(`Falta --${key}`);
161
+ return value;
162
+ }
163
+ class UserError extends Error {
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ /**
167
+ * `estela web` — abre el panel local.
168
+ *
169
+ * Quien instala la CLI no tiene forma de arrancar el servidor: hasta ahora
170
+ * vivía en un script del repositorio, que solo existe si te lo has clonado.
171
+ */
172
+ async function cmdWeb(args, dbPath) {
173
+ const port = Number(str(args, "port") ?? 4319);
174
+ const url = await (0, server_js_1.startServer)({ port, dbPath });
175
+ console.log(`\n Estela ${url}`);
176
+ console.log(` Datos: ${dbPath}`);
177
+ console.log(`\n Ctrl+C para parar.\n`);
178
+ // No devuelve: el servidor se queda escuchando.
179
+ await new Promise(() => { });
180
+ }
181
+ const DEFAULT_API_BASE_URL = "https://api.getestela.dev";
182
+ async function cmdLogin(args, dbPath) {
183
+ const email = required(args, "email");
184
+ const apiBaseUrl = str(args, "api") ?? process.env["ESTELA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL;
185
+ const db = (0, schema_js_1.openDatabase)(dbPath);
186
+ try {
187
+ await (0, auth_js_1.login)(db, email, apiBaseUrl);
188
+ }
189
+ finally {
190
+ db.close();
191
+ }
192
+ }
193
+ async function cmdLogout(dbPath) {
194
+ const db = (0, schema_js_1.openDatabase)(dbPath);
195
+ try {
196
+ await (0, auth_js_1.logout)(db);
197
+ }
198
+ finally {
199
+ db.close();
200
+ }
201
+ }
202
+ /**
203
+ * `estela setup` — la primera ejecución.
204
+ *
205
+ * Dos minutos, sin preguntas y sin cuenta. Si al terminar esa persona no ve
206
+ * algo cierto sobre su propio trabajo, no habrá una segunda ejecución.
207
+ */
208
+ async function cmdSetup(args, dbPath) {
209
+ const db = (0, schema_js_1.openDatabase)(dbPath);
210
+ try {
211
+ console.log("\nEstela\n");
212
+ console.log("Leyendo lo que tus agentes ya guardaron en disco…");
213
+ const scan = await (0, claude_js_1.scanClaudeCode)({});
214
+ const turns = (0, claude_js_1.resolveScratchpads)(scan.turns);
215
+ if (turns.length === 0) {
216
+ console.log("\n No se han encontrado sesiones de Claude Code en ~/.claude.");
217
+ console.log(" Estela lee lo que el agente ya escribe; sin sesiones no hay nada");
218
+ console.log(" que reconstruir todavía. Vuelve tras trabajar un rato.\n");
219
+ return;
220
+ }
221
+ store.saveTurns(db, turns);
222
+ store.logScan(db, "claude-code", scan.report);
223
+ console.log(` ${scan.report.turnsAccepted} turnos · ` +
224
+ `${scan.report.producerVersions.length} versiones de Claude Code`);
225
+ const repos = new Set();
226
+ for (const turn of turns)
227
+ if (turn.repoPath)
228
+ repos.add(turn.repoPath);
229
+ console.log(`\nBuscando repositorios… (${repos.size})`);
230
+ const plans = await (0, setup_js_1.planSetup)(db, [...repos], await (0, git_js_1.gitUserEmail)(process.cwd()));
231
+ const nuevos = (0, setup_js_1.applySetup)(db, plans);
232
+ for (const plan of plans.filter((x) => !x.existing)) {
233
+ console.log(` + ${plan.name}` +
234
+ (plan.emails.length ? ` (commiteas como ${plan.emails[0]})` : " ⚠ sin autor claro"));
235
+ }
236
+ const yaEstaban = plans.filter((x) => x.existing).length;
237
+ if (yaEstaban)
238
+ console.log(` ${yaEstaban} ya estaban configurados y no se tocan`);
239
+ console.log("\nReconstruyendo tu historial…");
240
+ await cmdImport({ _: [], flags: { ...(args.flags["db"] ? { db: args.flags["db"] } : {}) } }, dbPath);
241
+ const s = (0, setup_js_1.summarize)(db);
242
+ console.log(`\n${"─".repeat(66)}`);
243
+ console.log(` ${(0, setup_js_1.summaryLine)(s)}`);
244
+ if (s.from)
245
+ console.log(` del ${s.from} al ${s.to}`);
246
+ if (s.people > 1) {
247
+ console.log(` ${s.people} personas han commiteado en esos repositorios`);
248
+ }
249
+ console.log(`${"─".repeat(66)}\n`);
250
+ const sinAutor = plans.filter((p) => !p.existing && p.emails.length === 0);
251
+ if (sinAutor.length) {
252
+ console.log(` ⚠ En ${sinAutor.length} repositorio(s) no se ha podido saber con qué`);
253
+ console.log(` correo commiteas, así que ahí no se ha capturado nada tuyo:`);
254
+ for (const p of sinAutor.slice(0, 3)) {
255
+ console.log(` estela author --project ${p.projectId}`);
256
+ }
257
+ console.log("");
258
+ }
259
+ console.log(" Ábrelo: estela web");
260
+ console.log(" Revísalo: estela doctor");
261
+ if (nuevos > 0) {
262
+ console.log("\n Los proyectos se han creado como internos y sin tarifa. Si alguno");
263
+ console.log(" es de un cliente al que facturas, ponle la suya y podrás emitir");
264
+ console.log(" informes: estela rate set --project <id> --rate 50\n");
265
+ }
266
+ }
267
+ finally {
268
+ db.close();
269
+ }
270
+ }
271
+ async function cmdImport(args, dbPath) {
272
+ const db = (0, schema_js_1.openDatabase)(dbPath);
273
+ try {
274
+ const sinceRaw = str(args, "since");
275
+ const since = sinceRaw ? new Date(`${sinceRaw}T00:00:00Z`) : undefined;
276
+ if (since && Number.isNaN(since.getTime()))
277
+ throw new UserError(`Fecha inválida: ${sinceRaw}`);
278
+ const repoFilter = str(args, "repo");
279
+ const repoPaths = repoFilter ? [(0, node_path_1.resolve)(repoFilter)] : undefined;
280
+ console.log("Leyendo transcripts de agentes…");
281
+ const scan = await (0, claude_js_1.scanClaudeCode)({
282
+ ...(since ? { since } : {}),
283
+ ...(repoPaths ? { repoPaths } : {}),
284
+ });
285
+ const report = scan.report;
286
+ // Devolver al repositorio el trabajo hecho en scratchpads, o esas horas
287
+ // quedan sin proyecto y desaparecen del parte.
288
+ const turns = (0, claude_js_1.resolveScratchpads)(scan.turns);
289
+ const rescued = turns.filter((t, i) => t.repoPath !== scan.turns[i].repoPath).length;
290
+ const savedTurns = store.saveTurns(db, turns);
291
+ store.logScan(db, "claude-code", report);
292
+ console.log(` ${report.filesRead} archivos · ${report.recordsSeen} registros`);
293
+ console.log(` ${report.turnsAccepted} turnos aceptados · ${savedTurns} nuevos`);
294
+ console.log(` ${report.duplicatesDropped} duplicados descartados` +
295
+ (report.turnsAccepted > 0
296
+ ? ` (${(report.duplicatesDropped / (report.turnsAccepted + report.duplicatesDropped) * 100).toFixed(0)}% de las filas)`
297
+ : ""));
298
+ console.log(` ${report.unknownRecords} registros internos ignorados · ${report.malformedRecords} malformados`);
299
+ if (report.producerVersions.length) {
300
+ console.log(` versiones de Claude Code: ${report.producerVersions.join(", ")}`);
301
+ }
302
+ for (const warning of report.warnings)
303
+ console.warn(` ⚠ ${warning}`);
304
+ // Commits de cada repositorio que aparece en los transcripts.
305
+ const repos = new Set();
306
+ for (const turn of turns)
307
+ if (turn.repoPath)
308
+ repos.add(turn.repoPath);
309
+ // Se guardan TODOS los autores: es lo que alimenta la vista de equipo. El
310
+ // filtro por tus correos va más abajo, justo antes de imputar horas.
311
+ let savedCommits = 0;
312
+ for (const repo of repos) {
313
+ const root = await (0, git_js_1.repoRoot)(repo);
314
+ if (!root)
315
+ continue;
316
+ savedCommits += store.saveCommits(db, await (0, git_js_1.readCommits)(root, since ? { since } : {}));
317
+ }
318
+ const mine = await (0, identity_js_1.myEmailsByRepo)(db, repos);
319
+ if (rescued > 0) {
320
+ console.log(` ${rescued} turnos de scratchpad devueltos a su repositorio`);
321
+ }
322
+ console.log(`Git: ${repos.size} repositorios · ${savedCommits} commits nuevos`);
323
+ // Agrupar en bloques, fusionar por rama y día, e imputar a los proyectos.
324
+ // Trabajo deducido de los agentes, y además el de los commits que no
325
+ // solapan con ninguno: quien programa sin IA solo deja ese rastro.
326
+ // Solo TUS commits derivan horas tuyas. Sin este filtro, los commits de un
327
+ // compañero se convertirían en tiempo facturable a tu nombre.
328
+ const commitsAll = (0, identity_js_1.onlyMine)(await allCommits(db), mine);
329
+ const fromAgents = (0, sessionize_js_1.attachCommits)((0, sessionize_js_1.sessionize)(turns), commitsAll);
330
+ const fromCommits = (0, sessionize_js_1.withoutOverlap)((0, sessionize_js_1.sessionizeCommits)(commitsAll), fromAgents);
331
+ const raw = [...fromAgents, ...fromCommits];
332
+ const blocks = (0, sessionize_js_1.groupByBranchAndDay)(raw);
333
+ if (fromCommits.length) {
334
+ console.log(` ${fromCommits.length} bloques deducidos solo de commits (sin agente)`);
335
+ }
336
+ let entries = 0;
337
+ let unassigned = 0;
338
+ for (const block of blocks) {
339
+ const projectId = block.repoPath ? store.projectForRepo(db, block.repoPath) : null;
340
+ if (!projectId) {
341
+ unassigned++;
342
+ continue;
343
+ }
344
+ const project = store.getProject(db, projectId);
345
+ const entry = {
346
+ projectId,
347
+ startedAt: block.startedAt,
348
+ endedAt: block.endedAt,
349
+ seconds: block.seconds,
350
+ description: (0, sessionize_js_1.describeBlock)(block),
351
+ billable: project.billable,
352
+ invoiceId: null,
353
+ aiCost: block.aiCost,
354
+ // Un bloque deducido solo de commits no tuvo agente: ni segundos de
355
+ // agente ni agente al que atribuirlos.
356
+ agentSeconds: block.turnCount > 0 ? block.seconds : 0,
357
+ commitHashes: block.commits.map((c) => c.hash),
358
+ agents: block.turnCount > 0 ? ["claude-code"] : [],
359
+ source: block.turnCount > 0 ? "agent" : "commit",
360
+ kind: "development",
361
+ branch: block.branch,
362
+ };
363
+ // El id es determinista (proyecto + día + rama) para que reimportar
364
+ // actualice la imputación en vez de duplicarla. La fecha tiene que ser la
365
+ // LOCAL y la misma que usa el servidor: con toISOString() se generaba un
366
+ // id distinto para el mismo bloque y el día se contaba dos veces.
367
+ const day = (0, shared_1.localDate)(block.startedAt);
368
+ const slug = (block.branch ?? "sin-rama").replace(/[^a-zA-Z0-9]+/g, "-");
369
+ store.saveTimeEntry(db, { ...entry, id: `te_${projectId}_${day}_${slug}` });
370
+ entries++;
371
+ }
372
+ console.log(`Bloques: ${raw.length} detectados → ${blocks.length} tras agrupar por rama y día · ` +
373
+ `${entries} imputados · ${unassigned} sin proyecto`);
374
+ if (unassigned > 0) {
375
+ console.log(`\nHay ${unassigned} bloques sin proyecto asignado. Regístralos con:`);
376
+ console.log(` estela project add --id <id> --client <id> --name <nombre> --repo <ruta>`);
377
+ }
378
+ }
379
+ finally {
380
+ db.close();
381
+ }
382
+ }
383
+ async function allCommits(db) {
384
+ const rows = db.prepare("SELECT * FROM commits").all();
385
+ return rows.map((r) => ({
386
+ repoPath: r["repo_path"],
387
+ hash: r["hash"],
388
+ at: new Date(r["at"]),
389
+ authorEmail: r["author_email"],
390
+ authorName: r["author_name"] ?? "",
391
+ branch: r["branch"] ?? null,
392
+ subject: r["subject"],
393
+ linesAdded: r["lines_added"],
394
+ linesDeleted: r["lines_deleted"],
395
+ files: String(r["files"] ?? "").split("\n").filter(Boolean),
396
+ }));
397
+ }
398
+ function cmdClientAdd(args, dbPath) {
399
+ const db = (0, schema_js_1.openDatabase)(dbPath);
400
+ try {
401
+ const currency = required(args, "currency").toUpperCase();
402
+ store.upsertClient(db, {
403
+ id: required(args, "id"),
404
+ name: required(args, "name"),
405
+ currency,
406
+ ...(str(args, "tax-id") ? { taxId: str(args, "tax-id") } : {}),
407
+ ...(str(args, "email") ? { email: str(args, "email") } : {}),
408
+ });
409
+ console.log(`Cliente "${required(args, "name")}" guardado. Factura en ${currency}.`);
410
+ }
411
+ finally {
412
+ db.close();
413
+ }
414
+ }
415
+ function cmdProjectAdd(args, dbPath) {
416
+ const db = (0, schema_js_1.openDatabase)(dbPath);
417
+ try {
418
+ const clientId = required(args, "client");
419
+ if (!store.getClient(db, clientId)) {
420
+ throw new UserError(`No existe el cliente "${clientId}". Créalo primero con: estela client add`);
421
+ }
422
+ const repo = str(args, "repo");
423
+ store.upsertProject(db, {
424
+ id: required(args, "id"),
425
+ clientId,
426
+ name: required(args, "name"),
427
+ repoPaths: repo ? [(0, node_path_1.resolve)(repo)] : [],
428
+ billable: str(args, "billable") !== "false",
429
+ roundingMinutes: Number(str(args, "rounding") ?? 0),
430
+ aiCostPolicy: (str(args, "ai-cost") ?? "absorbed"),
431
+ kind: (str(args, "kind") ?? "client"),
432
+ });
433
+ console.log(`Proyecto "${required(args, "name")}" guardado.`);
434
+ if (repo)
435
+ console.log(` repositorio: ${(0, node_path_1.resolve)(repo)}`);
436
+ }
437
+ finally {
438
+ db.close();
439
+ }
440
+ }
441
+ /**
442
+ * Presupuesto mensual de IA de un proyecto.
443
+ *
444
+ * En dólares, que es la moneda en la que facturan los modelos, aunque le
445
+ * cobres al cliente en euros. Mezclarlo con la moneda del cliente obligaría a
446
+ * un tipo de cambio para comparar el gasto con su propio límite.
447
+ *
448
+ * Sin presupuesto no hay aviso. Es distinto de un presupuesto de cero, que
449
+ * avisaría siempre.
450
+ */
451
+ function cmdBudget(args, dbPath) {
452
+ const db = (0, schema_js_1.openDatabase)(dbPath);
453
+ try {
454
+ const projectId = required(args, "project");
455
+ const project = store.getProject(db, projectId);
456
+ if (!project)
457
+ throw new UserError(`No existe el proyecto "${projectId}".`);
458
+ const raw = str(args, "amount");
459
+ if (raw === undefined) {
460
+ const current = db.prepare("SELECT ai_budget_micro_usd AS b FROM projects WHERE id = ?").get(projectId);
461
+ console.log(current.b === null
462
+ ? `"${project.name}" no tiene presupuesto de IA.`
463
+ : `"${project.name}": $${(current.b / 1e6).toFixed(2)} al mes.`);
464
+ return;
465
+ }
466
+ if (raw === "none") {
467
+ db.prepare("UPDATE projects SET ai_budget_micro_usd = NULL WHERE id = ?").run(projectId);
468
+ console.log(`Presupuesto de "${project.name}" retirado.`);
469
+ return;
470
+ }
471
+ const dollars = Number(raw.replace(",", "."));
472
+ if (!Number.isFinite(dollars) || dollars < 0) {
473
+ throw new UserError(`Importe inválido: ${raw}`);
474
+ }
475
+ db.prepare("UPDATE projects SET ai_budget_micro_usd = ? WHERE id = ?")
476
+ .run(Math.round(dollars * 1e6), projectId);
477
+ console.log(`Presupuesto de IA de "${project.name}": $${dollars.toFixed(2)} al mes.`);
478
+ }
479
+ finally {
480
+ db.close();
481
+ }
482
+ }
483
+ function cmdRateSet(args, dbPath) {
484
+ const db = (0, schema_js_1.openDatabase)(dbPath);
485
+ try {
486
+ const projectId = required(args, "project");
487
+ const project = store.getProject(db, projectId);
488
+ if (!project)
489
+ throw new UserError(`No existe el proyecto "${projectId}".`);
490
+ const client = store.getClient(db, project.clientId);
491
+ const currency = str(args, "currency")?.toUpperCase() ?? client.currency;
492
+ const rate = (0, shared_1.parseMoney)(required(args, "rate"), currency);
493
+ const fromRaw = str(args, "from");
494
+ const effectiveFrom = fromRaw ? new Date(`${fromRaw}T00:00:00Z`) : new Date(0);
495
+ store.addRatePeriod(db, { projectId, hourlyRate: rate, effectiveFrom, effectiveTo: null });
496
+ console.log(`Tarifa de "${project.name}": ${(0, shared_1.formatMoney)(rate)}/hora` +
497
+ (fromRaw ? ` desde ${fromRaw}.` : " (aplica a todo el histórico)."));
498
+ }
499
+ finally {
500
+ db.close();
501
+ }
502
+ }
503
+ function cmdStatus(dbPath) {
504
+ const db = (0, schema_js_1.openDatabase)(dbPath);
505
+ try {
506
+ const turns = db.prepare("SELECT COUNT(*) n, SUM(cost_micro_usd) c, MIN(at) a, MAX(at) b FROM agent_turns").get();
507
+ console.log(`Base de datos: ${dbPath}\n`);
508
+ console.log(`Turnos de agente: ${turns.n}`);
509
+ if (turns.a)
510
+ console.log(`Periodo: ${turns.a.slice(0, 10)} → ${turns.b.slice(0, 10)}`);
511
+ console.log(`Coste de IA total: ${(0, shared_1.formatAiCost)({ microUsd: turns.c ?? 0 })}\n`);
512
+ const clients = store.listClients(db);
513
+ if (clients.length === 0) {
514
+ console.log("Sin clientes configurados. Empieza con:");
515
+ console.log(" estela client add --id <id> --name <nombre> --currency EUR");
516
+ return;
517
+ }
518
+ for (const client of clients) {
519
+ console.log(`${client.name} [${client.currency}]`);
520
+ for (const project of store.listProjects(db).filter((p) => p.clientId === client.id)) {
521
+ const entries = store.getTimeEntries(db, project.id);
522
+ const pending = entries.filter((e) => e.billable && e.invoiceId === null);
523
+ const seconds = pending.reduce((s, e) => s + e.seconds, 0);
524
+ const ai = pending.reduce((s, e) => s + e.aiCost.microUsd, 0);
525
+ const rate = (0, invoice_js_1.rateAt)(store.getRates(db, project.id), project.id, new Date());
526
+ console.log(` ${project.name}`);
527
+ console.log(` tarifa: ${rate ? `${(0, shared_1.formatMoney)(rate)}/h` : "— sin definir —"}`);
528
+ console.log(` sin facturar: ${(0, shared_1.formatDuration)(seconds)} en ${pending.length} bloques`);
529
+ if (rate && seconds > 0) {
530
+ const amount = { amount: Math.round((rate.amount * seconds) / 3600), currency: rate.currency };
531
+ console.log(` importe: ${(0, shared_1.formatMoney)(amount)}`);
532
+ }
533
+ console.log(` coste de IA: ${(0, shared_1.formatAiCost)({ microUsd: ai })}`);
534
+ }
535
+ console.log("");
536
+ }
537
+ const scan = db.prepare("SELECT * FROM scan_log ORDER BY id DESC LIMIT 1")
538
+ .get();
539
+ if (scan && String(scan["warnings"])) {
540
+ console.warn(`⚠ ${scan["warnings"]}`);
541
+ }
542
+ }
543
+ finally {
544
+ db.close();
545
+ }
546
+ }
547
+ function cmdEntries(args, dbPath) {
548
+ const db = (0, schema_js_1.openDatabase)(dbPath);
549
+ try {
550
+ const projectId = required(args, "project");
551
+ const project = store.getProject(db, projectId);
552
+ if (!project)
553
+ throw new UserError(`No existe el proyecto "${projectId}".`);
554
+ const entries = store.getTimeEntries(db, projectId);
555
+ if (entries.length === 0) {
556
+ console.log("Sin bloques imputados. Ejecuta: estela import");
557
+ return;
558
+ }
559
+ console.log(`${project.name}\n`);
560
+ for (const e of entries) {
561
+ const flag = e.invoiceId ? "facturado" : e.billable ? "pendiente" : "no facturable";
562
+ console.log(`${e.startedAt.toISOString().slice(0, 16).replace("T", " ")} ` +
563
+ `${(0, shared_1.formatDuration)(e.seconds).padStart(8)} ` +
564
+ `${(0, shared_1.formatAiCost)(e.aiCost).padStart(9)} ` +
565
+ `${flag.padEnd(14)} ${e.description.slice(0, 44)}`);
566
+ }
567
+ }
568
+ finally {
569
+ db.close();
570
+ }
571
+ }
572
+ /**
573
+ * Genera el informe compartible.
574
+ *
575
+ * El fichero es autónomo a propósito: se manda por correo y se abre sin cuenta,
576
+ * sin instalar nada y sin depender de que esta máquina esté encendida.
577
+ */
578
+ function cmdShare(args, dbPath) {
579
+ const db = (0, schema_js_1.openDatabase)(dbPath);
580
+ try {
581
+ const projectId = required(args, "project");
582
+ const project = store.getProject(db, projectId);
583
+ if (!project)
584
+ throw new UserError(`No existe el proyecto "${projectId}".`);
585
+ const client = store.getClient(db, project.clientId);
586
+ const to = str(args, "to") ?? (0, shared_1.localDate)(new Date());
587
+ const from = str(args, "from") ?? to.slice(0, 8) + "01";
588
+ const rates = store.getRates(db, projectId);
589
+ const html = (0, share_js_1.buildShareReport)({
590
+ project, client,
591
+ entries: store.getTimeEntries(db, projectId),
592
+ from, to,
593
+ ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
594
+ withAmounts: args.flags["with-amounts"] === true,
595
+ rateAt: (at) => (0, invoice_js_1.rateAt)(rates, projectId, at),
596
+ // La IA la pagas tú mientras no haya una organización detrás, así que su
597
+ // consumo no entra en el informe. No es una opción: no hay bandera.
598
+ aiPayer: "self",
599
+ commitsOf: (hashes) => hashes.length
600
+ ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
601
+ : [],
602
+ });
603
+ const out = str(args, "out") ?? `informe-${projectId}-${to}.html`;
604
+ (0, node_fs_1.writeFileSync)(out, html, "utf8");
605
+ console.log(`\nInforme: ${out}`);
606
+ console.log(` ${project.name} · ${client.name} · ${from} al ${to}`);
607
+ console.log(` Un solo fichero. Se abre sin instalar nada y sin depender de tu máquina.`);
608
+ if (args.flags["with-amounts"] !== true) {
609
+ console.log(`\n Incluye horas y commits. Sin importes (añade --with-amounts) y sin`);
610
+ console.log(` consumo de IA, que es tuyo mientras la pagues tú.`);
611
+ }
612
+ }
613
+ finally {
614
+ db.close();
615
+ }
616
+ }
617
+ /**
618
+ * Registra tiempo a mano.
619
+ *
620
+ * Un proyecto no son solo commits. Reuniones, desplazamientos, investigación y
621
+ * spikes son horas reales que se facturan igual, y sin poder anotarlas el parte
622
+ * del día miente por omisión: enseña solo lo que dejó rastro en Git y hace
623
+ * parecer que el resto del trabajo no ocurrió.
624
+ */
625
+ function cmdLog(args, dbPath) {
626
+ const db = (0, schema_js_1.openDatabase)(dbPath);
627
+ try {
628
+ const projectId = required(args, "project");
629
+ const project = store.getProject(db, projectId);
630
+ if (!project) {
631
+ const known = store.listProjects(db).map((p) => p.id).join(", ") || "(ninguno)";
632
+ throw new UserError(`No existe el proyecto "${projectId}". Los que hay: ${known}`);
633
+ }
634
+ const kind = (str(args, "kind") ?? "meeting");
635
+ if (!(kind in shared_1.WORK_KIND_LABELS)) {
636
+ throw new UserError(`Tipo "${kind}" no reconocido. Usa uno de: ${Object.keys(shared_1.WORK_KIND_LABELS).join(", ")}`);
637
+ }
638
+ const hoursRaw = str(args, "hours");
639
+ const minutesRaw = str(args, "minutes");
640
+ if (!hoursRaw && !minutesRaw)
641
+ throw new UserError("Indica --hours o --minutes");
642
+ const seconds = Math.round((hoursRaw ? Number(hoursRaw.replace(",", ".")) * 3600 : 0) +
643
+ (minutesRaw ? Number(minutesRaw) * 60 : 0));
644
+ if (!Number.isFinite(seconds) || seconds <= 0) {
645
+ throw new UserError(`Duración inválida: ${hoursRaw ?? minutesRaw}`);
646
+ }
647
+ const day = str(args, "date") ?? (0, shared_1.localDate)(new Date());
648
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
649
+ throw new UserError(`Fecha inválida: ${day}`);
650
+ // A media mañana: no sabemos la hora real y fingir precisión sería peor.
651
+ const startedAt = new Date(`${day}T10:00:00`);
652
+ const what = str(args, "what") ?? shared_1.WORK_KIND_LABELS[kind];
653
+ const id = store.saveTimeEntry(db, {
654
+ projectId,
655
+ startedAt,
656
+ endedAt: new Date(startedAt.getTime() + seconds * 1000),
657
+ seconds,
658
+ description: what,
659
+ billable: str(args, "billable") !== "false",
660
+ invoiceId: null,
661
+ aiCost: { microUsd: 0 },
662
+ agentSeconds: 0,
663
+ commitHashes: [],
664
+ agents: [],
665
+ source: "manual",
666
+ kind,
667
+ branch: null,
668
+ });
669
+ const rate = (0, invoice_js_1.rateAt)(store.getRates(db, projectId), projectId, startedAt);
670
+ const amount = rate
671
+ ? ` · ${(0, shared_1.formatMoney)({ amount: Math.round((rate.amount * seconds) / 3600), currency: rate.currency })}`
672
+ : "";
673
+ console.log(`\n${shared_1.WORK_KIND_LABELS[kind]} · ${(0, shared_1.formatDuration)(seconds)}${amount}`);
674
+ console.log(` ${what}`);
675
+ console.log(` ${project.name} · ${day}`);
676
+ console.log(`\n Registro manual: un import no lo va a tocar. (${id})`);
677
+ }
678
+ finally {
679
+ db.close();
680
+ }
681
+ }
682
+ /**
683
+ * Publica el proyecto como panel de solo lectura.
684
+ *
685
+ * Genera un directorio con el token en la ruta, listo para subir a un servidor
686
+ * estático. Se entrega el enlace completo para que no haya que componerlo a
687
+ * mano y equivocarse.
688
+ *
689
+ * El token es lo único que protege la página: quien tenga la URL, entra. Por eso
690
+ * son 128 bits y por eso revocar el acceso es borrar la carpeta del servidor —
691
+ * una caducidad comprobada en el navegador no protegería de nada.
692
+ */
693
+ async function cmdPublish(args, dbPath) {
694
+ const db = (0, schema_js_1.openDatabase)(dbPath);
695
+ try {
696
+ const projectId = required(args, "project");
697
+ const project = store.getProject(db, projectId);
698
+ if (!project)
699
+ throw new UserError(`No existe el proyecto "${projectId}".`);
700
+ const client = store.getClient(db, project.clientId);
701
+ const rates = store.getRates(db, projectId);
702
+ // Reutilizar el token si se republica: el cliente ya tiene ese enlace
703
+ // guardado y cambiárselo sin avisar le rompe el marcador.
704
+ const token = str(args, "token") ?? (0, panel_js_1.newPanelToken)();
705
+ // Las métricas necesitan los commits del proyecto y saber qué ramas están
706
+ // integradas. Sin lo segundo no se puede distinguir entregado de en curso.
707
+ const projectCommits = commitsOfProject(db, project.repoPaths);
708
+ const merged = project.repoPaths[0]
709
+ ? await (0, git_js_1.mergedBranches)(project.repoPaths[0]) : null;
710
+ // El equipo del proyecto. Se enseña porque el repositorio es del cliente y
711
+ // esos nombres ya los tiene en su git; lo que no sale de aquí son las horas
712
+ // ajenas, que se quitan ANTES de construir el fichero. Ocultarlas al pintar
713
+ // dejaría el dato en el código fuente, que es no ocultarlas.
714
+ const myEntries = store.getTimeEntries(db, projectId);
715
+ const mineByRepo = await (0, identity_js_1.myEmailsByRepo)(db, project.repoPaths);
716
+ const myEmails = new Set();
717
+ for (const set of mineByRepo.values())
718
+ for (const email of set)
719
+ myEmails.add(email);
720
+ // Acotado al periodo que cubre el panel. Sin esto se compara el historial
721
+ // entero del repositorio con las semanas que llevas tú: salían 523 commits
722
+ // de un compañero frente a 11 tuyos, y el panel que venía a respaldar tu
723
+ // trabajo te dejaba a la altura del betún. Además mete gente que ya no está
724
+ // en el proyecto.
725
+ const worked = myEntries.map((e) => (0, shared_1.localDate)(e.startedAt)).sort((a, b) => a.localeCompare(b));
726
+ const from = worked[0] ?? "0000-01-01";
727
+ const to = worked[worked.length - 1] ?? "9999-12-31";
728
+ const inPeriod = projectCommits.filter((c) => {
729
+ const day = (0, shared_1.localDate)(c.at);
730
+ return day >= from && day <= to;
731
+ });
732
+ const team = (0, team_js_1.teamView)(inPeriod, { myEmails, myEntries }).map((p) => ({
733
+ name: p.name,
734
+ isMe: p.isMe,
735
+ ...(p.isMe ? { seconds: p.seconds } : {}),
736
+ commits: p.commits,
737
+ branches: p.branches.length,
738
+ lastDay: p.lastDay,
739
+ }));
740
+ const html = (0, panel_js_1.buildPanel)({
741
+ project, client,
742
+ entries: myEntries,
743
+ ...(args.flags["no-team"] === true ? {} : { team }),
744
+ ...(str(args, "site") ? { siteUrl: str(args, "site") } : {}),
745
+ ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
746
+ withAmounts: args.flags["with-amounts"] === true,
747
+ rateAt: (at) => (0, invoice_js_1.rateAt)(rates, projectId, at),
748
+ aiPayer: "self",
749
+ commits: projectCommits,
750
+ ...(merged ? { merged } : {}),
751
+ commitsOf: (hashes) => hashes.length
752
+ ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
753
+ : [],
754
+ });
755
+ store.recordPublication(db, projectId, token, str(args, "base-url") ?? null);
756
+ const outDir = (0, node_path_1.join)(str(args, "out") ?? "./publicar", "e", token);
757
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
758
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, "index.html"), html, "utf8");
759
+ const base = (str(args, "base-url") ?? "https://TU-DOMINIO").replace(/\/$/, "");
760
+ console.log(`\nPanel publicable generado.\n`);
761
+ console.log(` Carpeta: ${outDir}`);
762
+ console.log(` Enlace: ${base}/e/${token}/\n`);
763
+ console.log(` Súbelo con:`);
764
+ console.log(` rsync -av ${str(args, "out") ?? "./publicar"}/ usuario@tu-droplet:/var/www/estela/\n`);
765
+ console.log(` El token es lo único que protege la página: quien tenga el enlace, entra.`);
766
+ console.log(` Para revocar el acceso, borra la carpeta e/${token}/ del servidor.`);
767
+ console.log(` Al republicar, pasa --token ${token} o el cliente perderá su enlace.`);
768
+ }
769
+ finally {
770
+ db.close();
771
+ }
772
+ }
773
+ /**
774
+ * Configura con qué correos commiteas en un proyecto.
775
+ *
776
+ * Sin argumento `--email`, enseña los autores del repositorio para que elijas:
777
+ * en el repositorio de un cliente hay varios compañeros y tu correo global no
778
+ * aparece por ningún lado.
779
+ */
780
+ async function cmdAuthor(args, dbPath) {
781
+ const db = (0, schema_js_1.openDatabase)(dbPath);
782
+ try {
783
+ const projectId = required(args, "project");
784
+ const project = store.getProject(db, projectId);
785
+ if (!project)
786
+ throw new UserError(`No existe el proyecto "${projectId}".`);
787
+ const emails = (str(args, "email") ?? "").split(",").map((e) => e.trim()).filter(Boolean);
788
+ if (emails.length === 0) {
789
+ const repo = project.repoPaths[0];
790
+ if (!repo)
791
+ throw new UserError("El proyecto no tiene repositorio asignado.");
792
+ console.log(`\nAutores en ${project.name}:\n`);
793
+ for (const a of await (0, git_js_1.repoAuthors)(repo)) {
794
+ console.log(` ${a.email.padEnd(38)} ${String(a.commits).padStart(5)} commits, hasta ${a.lastAt}`);
795
+ }
796
+ const current = store.getProjectAuthors(db, projectId);
797
+ console.log(`\nConfigurado ahora: ${current.length ? current.join(", ") : "— nada —"}`);
798
+ console.log(`\nElige el tuyo con:`);
799
+ console.log(` estela author --project ${projectId} --email tu@correo.com`);
800
+ return;
801
+ }
802
+ store.setProjectAuthors(db, projectId, emails);
803
+ console.log(`\n${project.name}: commits filtrados por ${emails.join(", ")}`);
804
+ console.log(`Ejecuta "estela import" para recoger los que faltaban.`);
805
+ }
806
+ finally {
807
+ db.close();
808
+ }
809
+ }
810
+ /** Commits de los repositorios de un proyecto, con sus ficheros. */
811
+ function commitsOfProject(db, repoPaths) {
812
+ if (repoPaths.length === 0)
813
+ return [];
814
+ const like = repoPaths.map(() => "repo_path LIKE ?").join(" OR ");
815
+ const rows = db.prepare(`SELECT * FROM commits WHERE ${like}`)
816
+ .all(...repoPaths.map((p) => `${p}%`));
817
+ return rows.map((r) => ({
818
+ repoPath: r["repo_path"], hash: r["hash"],
819
+ at: new Date(r["at"]), authorEmail: r["author_email"],
820
+ authorName: r["author_name"] ?? "",
821
+ branch: r["branch"] ?? null, subject: r["subject"],
822
+ linesAdded: r["lines_added"], linesDeleted: r["lines_deleted"],
823
+ files: String(r["files"] ?? "").split("\n").filter(Boolean),
824
+ }));
825
+ }
826
+ /** Revisa los datos y avisa de lo que rompería una demo. */
827
+ function cmdDoctor(dbPath) {
828
+ const db = (0, schema_js_1.openDatabase)(dbPath);
829
+ try {
830
+ const findings = (0, doctor_js_1.diagnose)(db);
831
+ console.log((0, doctor_js_1.renderFindings)(findings));
832
+ if (findings.some((f) => f.severity === "error"))
833
+ process.exitCode = 1;
834
+ }
835
+ finally {
836
+ db.close();
837
+ }
838
+ }
839
+ function cmdExport(args, dbPath) {
840
+ const db = (0, schema_js_1.openDatabase)(dbPath);
841
+ try {
842
+ const projectId = required(args, "project");
843
+ const project = store.getProject(db, projectId);
844
+ if (!project)
845
+ throw new UserError(`No existe el proyecto "${projectId}".`);
846
+ const client = store.getClient(db, project.clientId);
847
+ const rates = store.getRates(db, projectId);
848
+ const csv = (0, csv_js_1.timeEntriesToCsv)(store.getTimeEntries(db, projectId), project, client, (at) => (0, invoice_js_1.rateAt)(rates, projectId, at));
849
+ const out = str(args, "out");
850
+ if (out) {
851
+ (0, node_fs_1.writeFileSync)(out, csv, "utf8");
852
+ console.log(`CSV: ${out}`);
853
+ }
854
+ else {
855
+ process.stdout.write(csv);
856
+ }
857
+ }
858
+ finally {
859
+ db.close();
860
+ }
861
+ }
862
+ function cmdSubscriptionAdd(args, dbPath) {
863
+ const db = (0, schema_js_1.openDatabase)(dbPath);
864
+ try {
865
+ const currency = (str(args, "currency")?.toUpperCase() ?? "USD");
866
+ const fee = (0, shared_1.parseMoney)(required(args, "fee"), currency);
867
+ const fromRaw = str(args, "from");
868
+ store.upsertSubscription(db, {
869
+ id: required(args, "id"),
870
+ name: str(args, "name") ?? required(args, "id"),
871
+ monthlyFee: fee,
872
+ effectiveFrom: fromRaw ? new Date(`${fromRaw}T00:00:00Z`) : new Date(0),
873
+ effectiveTo: null,
874
+ });
875
+ console.log(`Suscripción "${str(args, "name") ?? required(args, "id")}": ${(0, shared_1.formatMoney)(fee)}/mes.`);
876
+ console.log("Se repartirá entre proyectos según lo que consumió cada uno.");
877
+ }
878
+ finally {
879
+ db.close();
880
+ }
881
+ }
882
+ /** Reparto de la cuota entre proyectos, mes a mes. */
883
+ function cmdAiCost(dbPath) {
884
+ const db = (0, schema_js_1.openDatabase)(dbPath);
885
+ try {
886
+ const subs = store.listSubscriptions(db);
887
+ const rows = store.consumptionByProjectMonth(db);
888
+ if (rows.length === 0) {
889
+ console.log("Sin consumo registrado. Ejecuta: estela import");
890
+ return;
891
+ }
892
+ if (subs.length === 0) {
893
+ console.log("Sin suscripciones registradas: el coste se muestra a tarifa API.\n");
894
+ console.log("Si pagas cuota fija, regístrala para ver el gasto real:");
895
+ console.log(' estela subscription add --id claude-max --name "Claude Max" --fee 200\n');
896
+ }
897
+ const shares = (0, amortize_js_1.amortize)(rows, subs, store.totalConsumptionByMonth(db));
898
+ const names = new Map(store.listProjects(db).map((p) => [p.id, p.name]));
899
+ let month = "";
900
+ for (const s of shares) {
901
+ if (s.month !== month) {
902
+ month = s.month;
903
+ console.log(`\n${month} (consumo total: ${(0, shared_1.formatAiCost)(s.monthTotal)} equiv. API)`);
904
+ }
905
+ const label = (names.get(s.projectId) ?? s.projectId).slice(0, 34).padEnd(36);
906
+ const pct = `${(s.share * 100).toFixed(1)}%`.padStart(7);
907
+ const real = subs.length ? (0, shared_1.formatMoney)(s.amount).padStart(11) : "—".padStart(11);
908
+ console.log(` ${label}${(0, shared_1.formatAiCost)(s.consumption).padStart(10)}${pct}${real}`);
909
+ }
910
+ if (subs.length > 0) {
911
+ console.log(`\nLa última columna es dinero real: tu cuota repartida por consumo.`);
912
+ console.log(`La primera es la tarifa API equivalente, útil solo como medida de uso.`);
913
+ }
914
+ }
915
+ finally {
916
+ db.close();
917
+ }
918
+ }
919
+ function cmdInvoice(args, dbPath) {
920
+ const db = (0, schema_js_1.openDatabase)(dbPath);
921
+ try {
922
+ const projectId = required(args, "project");
923
+ const project = store.getProject(db, projectId);
924
+ if (!project)
925
+ throw new UserError(`No existe el proyecto "${projectId}".`);
926
+ const client = store.getClient(db, project.clientId);
927
+ const cutoffRaw = required(args, "cutoff");
928
+ const cutoffAt = new Date(`${cutoffRaw}T23:59:59Z`);
929
+ if (Number.isNaN(cutoffAt.getTime()))
930
+ throw new UserError(`Fecha inválida: ${cutoffRaw}`);
931
+ const dryRun = args.flags["dry-run"] === true;
932
+ const number = str(args, "number") ?? store.nextInvoiceNumber(db, "INF");
933
+ const fx = str(args, "fx");
934
+ const entries = store.getTimeEntries(db, projectId);
935
+ // El reparto de la cuota se calcula AHORA y viaja con el informe. Si se
936
+ // recalculara al reimprimirlo, un documento de agosto daría otra cifra en
937
+ // diciembre, cuando ya se conoce más consumo.
938
+ const subs = store.listSubscriptions(db);
939
+ const months = new Set();
940
+ for (const e of entries) {
941
+ if (e.billable && e.invoiceId === null && e.endedAt <= cutoffAt)
942
+ months.add((0, amortize_js_1.monthOf)(e.startedAt));
943
+ }
944
+ const amortized = subs.length
945
+ ? (0, amortize_js_1.shareForProject)((0, amortize_js_1.amortize)(store.consumptionByProjectMonth(db), subs, store.totalConsumptionByMonth(db)), projectId, [...months])
946
+ : null;
947
+ const invoice = (0, invoice_js_1.issueInvoice)({
948
+ client, project,
949
+ rates: store.getRates(db, projectId),
950
+ entries, cutoffAt, number,
951
+ ...(fx ? { usdFxRate: Number(fx) } : {}),
952
+ ...(amortized ? { aiAmortized: amortized } : {}),
953
+ });
954
+ console.log((0, invoice_js_1.renderInvoice)(invoice, client, project));
955
+ // Coste real de IA del periodo: la cuota de tus suscripciones repartida
956
+ // por consumo. Es lo que de verdad te costó, frente a la tarifa API.
957
+ if (invoice.aiAmortized) {
958
+ console.log(`Coste real de IA imputado desde tu suscripción: ${(0, shared_1.formatMoney)(invoice.aiAmortized)}`);
959
+ }
960
+ if (subs.length === 0) {
961
+ console.log("\nSin suscripciones registradas. Si pagas cuota fija, el importe de arriba");
962
+ console.log("es tarifa API equivalente, no lo que gastaste. Regístrala con:");
963
+ console.log(' estela subscription add --id claude-max --name "Claude Max" --fee 200');
964
+ }
965
+ if (fx) {
966
+ const m = (0, invoice_js_1.marginOf)(invoice, Number(fx));
967
+ console.log(`\nMargen a tarifa API: ${(0, shared_1.formatMoney)(m.margin)} de ${(0, shared_1.formatMoney)(m.revenue)} ` +
968
+ `(${m.marginPct.toFixed(1)}%)`);
969
+ }
970
+ // --- Exportables ---------------------------------------------------------
971
+ const pdfPath = str(args, "pdf");
972
+ if (pdfPath) {
973
+ const issuerName = str(args, "from-name");
974
+ const pdf = (0, invoice_pdf_js_1.invoiceToPdf)(invoice, client, project, {
975
+ ...(issuerName ? {
976
+ issuer: {
977
+ name: issuerName,
978
+ ...(str(args, "from-tax-id") ? { taxId: str(args, "from-tax-id") } : {}),
979
+ ...(str(args, "from-email") ? { email: str(args, "from-email") } : {}),
980
+ },
981
+ } : {}),
982
+ ...(invoice.aiAmortized ? { amortizedAiCost: invoice.aiAmortized } : {}),
983
+ });
984
+ (0, node_fs_1.writeFileSync)(pdfPath, pdf);
985
+ console.log(`\nPDF: ${pdfPath} (${(pdf.length / 1024).toFixed(1)} KB, ${invoice.lines.length} conceptos)`);
986
+ }
987
+ const csvPath = str(args, "csv");
988
+ if (csvPath) {
989
+ (0, node_fs_1.writeFileSync)(csvPath, (0, csv_js_1.invoiceToCsv)(invoice, client, project), "utf8");
990
+ console.log(`CSV: ${csvPath}`);
991
+ }
992
+ if (dryRun) {
993
+ console.log("\n[--dry-run] No se guardó nada. Repite sin --dry-run para emitirlo.");
994
+ return;
995
+ }
996
+ const billed = entries
997
+ .filter((e) => e.billable && e.invoiceId === null && e.endedAt <= cutoffAt)
998
+ .map((e) => e.id);
999
+ store.saveInvoice(db, invoice, billed);
1000
+ console.log(`\nInforme ${invoice.number} emitido. ${billed.length} bloques marcados como informados.`);
1001
+ }
1002
+ finally {
1003
+ db.close();
1004
+ }
1005
+ }
1006
+ // ---------------------------------------------------------------------------
1007
+ async function main() {
1008
+ const argv = process.argv.slice(2);
1009
+ const args = parseArgs(argv);
1010
+ const dbPath = str(args, "db") ?? schema_js_1.DEFAULT_DB_PATH;
1011
+ const command = args._.join(" ");
1012
+ switch (command) {
1013
+ case "setup":
1014
+ await cmdSetup(args, dbPath);
1015
+ break;
1016
+ case "web":
1017
+ await cmdWeb(args, dbPath);
1018
+ break;
1019
+ case "import":
1020
+ await cmdImport(args, dbPath);
1021
+ break;
1022
+ case "client add":
1023
+ cmdClientAdd(args, dbPath);
1024
+ break;
1025
+ case "project add":
1026
+ cmdProjectAdd(args, dbPath);
1027
+ break;
1028
+ case "rate set":
1029
+ cmdRateSet(args, dbPath);
1030
+ break;
1031
+ case "budget":
1032
+ cmdBudget(args, dbPath);
1033
+ break;
1034
+ case "subscription add":
1035
+ cmdSubscriptionAdd(args, dbPath);
1036
+ break;
1037
+ case "ai-cost":
1038
+ cmdAiCost(dbPath);
1039
+ break;
1040
+ case "status":
1041
+ cmdStatus(dbPath);
1042
+ break;
1043
+ case "doctor":
1044
+ cmdDoctor(dbPath);
1045
+ break;
1046
+ case "entries":
1047
+ cmdEntries(args, dbPath);
1048
+ break;
1049
+ case "log":
1050
+ cmdLog(args, dbPath);
1051
+ break;
1052
+ case "author":
1053
+ await cmdAuthor(args, dbPath);
1054
+ break;
1055
+ case "share":
1056
+ cmdShare(args, dbPath);
1057
+ break;
1058
+ case "publish":
1059
+ await cmdPublish(args, dbPath);
1060
+ break;
1061
+ case "export":
1062
+ cmdExport(args, dbPath);
1063
+ break;
1064
+ case "report":
1065
+ cmdInvoice(args, dbPath);
1066
+ break;
1067
+ case "login":
1068
+ await cmdLogin(args, dbPath);
1069
+ break;
1070
+ case "logout":
1071
+ await cmdLogout(dbPath);
1072
+ break;
1073
+ case "":
1074
+ case "help":
1075
+ console.log(HELP);
1076
+ break;
1077
+ default:
1078
+ console.error(`Comando desconocido: "${command}"\n${HELP}`);
1079
+ process.exitCode = 1;
1080
+ }
1081
+ }
1082
+ main().catch((error) => {
1083
+ if (error instanceof UserError || error instanceof invoice_js_1.InvoiceError || error instanceof client_js_1.CloudError) {
1084
+ console.error(`\n${error.message}\n`);
1085
+ }
1086
+ else {
1087
+ console.error(error);
1088
+ }
1089
+ process.exitCode = 1;
1090
+ });
1091
+ //# sourceMappingURL=cli.js.map