estela 0.1.34 → 0.1.35

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 (51) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +205 -9
  3. package/dist/cli.js.map +1 -1
  4. package/dist/db/schema.d.ts.map +1 -1
  5. package/dist/db/schema.js +21 -1
  6. package/dist/db/schema.js.map +1 -1
  7. package/dist/db/schema.test.js +53 -0
  8. package/dist/db/schema.test.js.map +1 -1
  9. package/dist/db/store.d.ts +12 -1
  10. package/dist/db/store.d.ts.map +1 -1
  11. package/dist/db/store.js +35 -3
  12. package/dist/db/store.js.map +1 -1
  13. package/dist/export/clockify.d.ts +32 -0
  14. package/dist/export/clockify.d.ts.map +1 -0
  15. package/dist/export/clockify.js +57 -0
  16. package/dist/export/clockify.js.map +1 -0
  17. package/dist/export/clockify.test.d.ts +2 -0
  18. package/dist/export/clockify.test.d.ts.map +1 -0
  19. package/dist/export/clockify.test.js +51 -0
  20. package/dist/export/clockify.test.js.map +1 -0
  21. package/dist/export/csv.d.ts.map +1 -1
  22. package/dist/export/csv.js +6 -2
  23. package/dist/export/csv.js.map +1 -1
  24. package/dist/export/idioma.test.js +2 -2
  25. package/dist/export/idioma.test.js.map +1 -1
  26. package/dist/i18n/en.d.ts.map +1 -1
  27. package/dist/i18n/en.js +22 -0
  28. package/dist/i18n/en.js.map +1 -1
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +4 -0
  31. package/dist/server.js.map +1 -1
  32. package/dist/workitems.test.d.ts +2 -0
  33. package/dist/workitems.test.d.ts.map +1 -0
  34. package/dist/workitems.test.js +59 -0
  35. package/dist/workitems.test.js.map +1 -0
  36. package/node_modules/@estela/shared/dist/index.d.ts +1 -0
  37. package/node_modules/@estela/shared/dist/index.d.ts.map +1 -1
  38. package/node_modules/@estela/shared/dist/index.js +1 -0
  39. package/node_modules/@estela/shared/dist/index.js.map +1 -1
  40. package/node_modules/@estela/shared/dist/types.d.ts +13 -0
  41. package/node_modules/@estela/shared/dist/types.d.ts.map +1 -1
  42. package/node_modules/@estela/shared/dist/types.js.map +1 -1
  43. package/node_modules/@estela/shared/dist/workitems.d.ts +44 -0
  44. package/node_modules/@estela/shared/dist/workitems.d.ts.map +1 -0
  45. package/node_modules/@estela/shared/dist/workitems.js +137 -0
  46. package/node_modules/@estela/shared/dist/workitems.js.map +1 -0
  47. package/package.json +1 -1
  48. package/web/app.css +7 -0
  49. package/web/app.js +8 -1
  50. package/web/i18n.js +4 -0
  51. package/web/index.html +2 -0
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAmrDpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAwCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAu2DpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -54,6 +54,7 @@ const demo_js_1 = require("./demo.js");
54
54
  const doctor_js_1 = require("./doctor.js");
55
55
  const setup_js_1 = require("./setup.js");
56
56
  const csv_js_1 = require("./export/csv.js");
57
+ const clockify_js_1 = require("./export/clockify.js");
57
58
  const invoice_pdf_js_1 = require("./export/invoice-pdf.js");
58
59
  const share_js_1 = require("./export/share.js");
59
60
  const server_js_1 = require("./server.js");
@@ -102,6 +103,11 @@ estela — registro de horas para desarrollo asistido por IA
102
103
  vuelve a haber actividad ahí — "estela doctor" avisa en ese caso, en
103
104
  vez de perder el bloque o facturarlo por sorpresa.
104
105
  estela project reopen --project <id>
106
+ estela project tracker --project <id> [--system jira|azure|github|gitlab|none]
107
+ [--prefix PROJ,OPS]
108
+ El gestor de tareas del proyecto, para reconocer sus tickets en la
109
+ rama y los commits (feature/1234-login, AB#1234, PROJ-12). Luego
110
+ "estela import" lo aplica a lo ya capturado.
105
111
  estela rate set --project <id> --rate <importe> [--from YYYY-MM-DD]
106
112
  estela budget --project <id> [--amount <dólares>|none]
107
113
  Presupuesto mensual de IA. Sin --amount, consulta el actual. Alimenta
@@ -128,7 +134,11 @@ estela — registro de horas para desarrollo asistido por IA
128
134
  estela doctor Revisa los datos y avisa de lo que
129
135
  rompería una demo. Úsalo antes de publicar.
130
136
  estela status Qué hay capturado y sin imputar.
131
- estela entries --project <id> Bloques imputados a un proyecto.
137
+ estela entries --project <id> [--ids]
138
+ Bloques imputados a un proyecto, con sus tickets. --ids enseña el id
139
+ de cada uno, para "estela entry link".
140
+ estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
141
+ Pone a mano los tickets de un bloque. El import ya no los cambia.
132
142
  estela ai-cost Reparto de tu cuota entre proyectos.
133
143
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
134
144
  [--author <tu nombre>] [--with-amounts] [--out <fichero.html>]
@@ -186,7 +196,12 @@ estela — registro de horas para desarrollo asistido por IA
186
196
  estela account
187
197
  Tu correo, tu plan, y desde qué máquinas te has conectado.
188
198
 
189
- estela export --project <id> [--out <fichero.csv>]
199
+ estela export --project <id> [--out <fichero.csv>] [--month YYYY-MM]
200
+ [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--format csv|clockify]
201
+ Tus bloques en CSV. --format clockify (Pro) sale listo para importar en
202
+ Clockify; --email es tu correo allí (por defecto el de tu cuenta de
203
+ Estela), y --date-format/--time-format tienen que coincidir con tu
204
+ perfil de Clockify (MM/DD/YYYY y 12h por defecto).
190
205
 
191
206
  estela report --project <id> --cutoff YYYY-MM-DD [--dry-run]
192
207
  [--pdf <fichero.pdf>] [--csv <fichero.csv>] [--fx <tipo>]
@@ -236,6 +251,11 @@ estela — time tracking for AI-assisted development
236
251
  there's activity there again — "estela doctor" flags that instead of
237
252
  silently dropping the block or billing it by surprise.
238
253
  estela project reopen --project <id>
254
+ estela project tracker --project <id> [--system jira|azure|github|gitlab|none]
255
+ [--prefix PROJ,OPS]
256
+ The project's issue tracker, to recognise its tickets in the branch
257
+ and commits (feature/1234-login, AB#1234, PROJ-12). Then
258
+ "estela import" applies it to what's already captured.
239
259
  estela rate set --project <id> --rate <amount> [--from YYYY-MM-DD]
240
260
  estela budget --project <id> [--amount <dollars>|none]
241
261
  Monthly AI budget. Without --amount, shows the current one. Feeds the
@@ -262,7 +282,11 @@ estela — time tracking for AI-assisted development
262
282
  estela doctor Checks your data and flags anything that
263
283
  would ruin a demo. Run it before publishing.
264
284
  estela status What's been captured and not yet assigned.
265
- estela entries --project <id> Blocks assigned to a project.
285
+ estela entries --project <id> [--ids]
286
+ Blocks assigned to a project, with their tickets. --ids shows each
287
+ one's id, for "estela entry link".
288
+ estela entry link --entry <id> --items PROJ-12[,AB#1234] | --clear
289
+ Sets a block's tickets by hand. Imports won't change them anymore.
266
290
  estela ai-cost How your subscription splits across projects.
267
291
  estela share --project <id> [--from YYYY-MM-DD] [--to YYYY-MM-DD]
268
292
  [--author <your name>] [--with-amounts] [--out <file.html>]
@@ -320,7 +344,12 @@ estela — time tracking for AI-assisted development
320
344
  estela account
321
345
  Your email, your plan, and which machines you've signed in from.
322
346
 
323
- estela export --project <id> [--out <file.csv>]
347
+ estela export --project <id> [--out <file.csv>] [--month YYYY-MM]
348
+ [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--format csv|clockify]
349
+ Your blocks as CSV. --format clockify (Pro) comes out ready to import
350
+ into Clockify; --email is your email there (your Estela account's by
351
+ default), and --date-format/--time-format have to match your Clockify
352
+ profile (MM/DD/YYYY and 12h by default).
324
353
 
325
354
  estela report --project <id> --cutoff YYYY-MM-DD [--dry-run]
326
355
  [--pdf <file.pdf>] [--csv <file.csv>] [--fx <rate>]
@@ -715,6 +744,9 @@ async function cmdImport(args, dbPath) {
715
744
  source: block.turnCount > 0 ? "agent" : "commit",
716
745
  kind: "development",
717
746
  branch: block.branch,
747
+ // El ticket se saca aquí y no más tarde: en la base solo quedan los
748
+ // hashes de los commits, y es en sus mensajes donde va el `AB#1234`.
749
+ workItems: (0, shared_1.extractWorkItems)(block.branch, block.commits.map((c) => c.subject), project.tracker),
718
750
  };
719
751
  // El id es determinista (proyecto + día + rama) para que reimportar
720
752
  // actualice la imputación en vez de duplicarla. La fecha tiene que ser la
@@ -869,6 +901,83 @@ function cmdProjectReopen(args, dbPath) {
869
901
  db.close();
870
902
  }
871
903
  }
904
+ /**
905
+ * `estela project tracker` — qué gestor de tareas usa un proyecto, para
906
+ * reconocer sus tickets en ramas y commits. Sin --system enseña el actual.
907
+ *
908
+ * No recalcula nada por sí mismo: `estela import` ya rehace todos los bloques
909
+ * no facturados, y así solo hay un camino que decide los tickets.
910
+ */
911
+ function cmdProjectTracker(args, dbPath) {
912
+ const db = (0, schema_js_1.openDatabase)(dbPath);
913
+ try {
914
+ const projectId = required(args, "project");
915
+ const project = store.getProject(db, projectId);
916
+ if (!project)
917
+ throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
918
+ const system = str(args, "system");
919
+ if (!system) {
920
+ console.log(project.tracker
921
+ ? (0, index_js_1.tr) `"${project.name}" usa ${project.tracker.system}${project.tracker.prefixes.length ? " (" + project.tracker.prefixes.join(", ") + ")" : ""}.`
922
+ : (0, index_js_1.tr) `"${project.name}" no tiene gestor de tareas. Solo se reconocen AB#1234 y claves de Jira en mayúscula en la rama.`);
923
+ return;
924
+ }
925
+ if (system === "none") {
926
+ store.setProjectTracker(db, projectId, null);
927
+ console.log((0, index_js_1.tr) `"${project.name}" ya no tiene gestor de tareas.`);
928
+ }
929
+ else {
930
+ if (!shared_1.TRACKER_SYSTEMS.includes(system)) {
931
+ throw new UserError((0, index_js_1.tr) `Gestor desconocido: ${system}. Usa jira, azure, github, gitlab o none.`);
932
+ }
933
+ const prefixes = (str(args, "prefix") ?? "").split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
934
+ if (prefixes.some((p) => !/^[A-Z][A-Z0-9]*$/.test(p))) {
935
+ throw new UserError((0, index_js_1.tr) `Prefijo inválido: tienen que ser letras y números, como PROJ u OPS.`);
936
+ }
937
+ store.setProjectTracker(db, projectId, { system: system, prefixes });
938
+ console.log((0, index_js_1.tr) `"${project.name}" usa ${system}${prefixes.length ? " (" + prefixes.join(", ") + ")" : ""}.`);
939
+ }
940
+ console.log((0, index_js_1.tr) `Para aplicarlo a lo ya capturado: estela import`);
941
+ }
942
+ finally {
943
+ db.close();
944
+ }
945
+ }
946
+ /**
947
+ * `estela entry link` — pone a mano los tickets de un bloque, cuando la rama
948
+ * no lo decía o lo decía mal. Desde entonces el import no los toca;
949
+ * `--clear` los devuelve a lo que se detecte solo.
950
+ */
951
+ function cmdEntryLink(args, dbPath) {
952
+ const db = (0, schema_js_1.openDatabase)(dbPath);
953
+ try {
954
+ const entryId = required(args, "entry");
955
+ const row = db.prepare("SELECT project_id FROM time_entries WHERE id = ?").get(entryId);
956
+ if (!row)
957
+ throw new UserError((0, index_js_1.tr) `No existe el bloque "${entryId}". Los ids salen con: estela entries --project <id> --ids`);
958
+ if (args.flags["clear"] === true) {
959
+ store.setEntryWorkItems(db, entryId, null);
960
+ console.log((0, index_js_1.tr) `Tickets de "${entryId}" devueltos a lo que detecte el import.`);
961
+ return;
962
+ }
963
+ const tracker = store.getProject(db, row.project_id)?.tracker ?? null;
964
+ const raw = required(args, "items").split(",").map((t) => t.trim()).filter(Boolean);
965
+ const keys = [];
966
+ for (const text of raw) {
967
+ const key = (0, shared_1.parseWorkItem)(text, tracker);
968
+ if (!key) {
969
+ throw new UserError((0, index_js_1.tr) `No sé de qué gestor es "${text}". Escríbelo como PROJ-12, AB#1234, o configura el del proyecto con: estela project tracker`);
970
+ }
971
+ if (!keys.includes(key))
972
+ keys.push(key);
973
+ }
974
+ store.setEntryWorkItems(db, entryId, keys);
975
+ console.log(`"${entryId}" → ${keys.map(shared_1.formatWorkItem).join(", ")}`);
976
+ }
977
+ finally {
978
+ db.close();
979
+ }
980
+ }
872
981
  /**
873
982
  * Presupuesto mensual de IA de un proyecto.
874
983
  *
@@ -987,13 +1096,17 @@ function cmdEntries(args, dbPath) {
987
1096
  console.log((0, index_js_1.tr) `Sin bloques imputados. Ejecuta: estela import`);
988
1097
  return;
989
1098
  }
1099
+ const ids = args.flags["ids"] === true;
990
1100
  console.log(`${project.name}\n`);
991
1101
  for (const e of entries) {
992
1102
  const flag = e.invoiceId ? (0, index_js_1.tr) `facturado` : e.billable ? (0, index_js_1.tr) `pendiente` : (0, index_js_1.tr) `no facturable`;
1103
+ const tickets = (e.workItems ?? []).map(shared_1.formatWorkItem).join(",");
993
1104
  console.log(`${e.startedAt.toISOString().slice(0, 16).replace("T", " ")} ` +
994
1105
  `${(0, shared_1.formatDuration)(e.seconds).padStart(8)} ` +
995
1106
  `${(0, shared_1.formatAiCost)(e.aiCost).padStart(9)} ` +
996
- `${flag.padEnd(14)} ${e.description.slice(0, 44)}`);
1107
+ `${flag.padEnd(14)} ${tickets ? tickets.padEnd(10) + " " : ""}${e.description.slice(0, 44)}`);
1108
+ if (ids)
1109
+ console.log(` ${e.id}`);
997
1110
  }
998
1111
  }
999
1112
  finally {
@@ -1526,7 +1639,7 @@ function cmdDoctor(dbPath) {
1526
1639
  db.close();
1527
1640
  }
1528
1641
  }
1529
- function cmdExport(args, dbPath) {
1642
+ async function cmdExport(args, dbPath) {
1530
1643
  const db = (0, schema_js_1.openDatabase)(dbPath);
1531
1644
  try {
1532
1645
  const projectId = required(args, "project");
@@ -1535,15 +1648,44 @@ function cmdExport(args, dbPath) {
1535
1648
  throw new UserError((0, index_js_1.tr) `No existe el proyecto "${projectId}".`);
1536
1649
  const client = store.getClient(db, project.clientId);
1537
1650
  const rates = store.getRates(db, projectId);
1651
+ const format = str(args, "format") ?? "csv";
1652
+ if (format !== "csv" && format !== "clockify") {
1653
+ throw new UserError((0, index_js_1.tr) `Formato desconocido: ${format}. Usa csv o clockify.`);
1654
+ }
1655
+ const entries = entriesInRange(store.getTimeEntries(db, projectId), args);
1538
1656
  const lang = docLangOf(args, client);
1539
- const csv = (0, index_js_1.withLang)(lang, () => (0, csv_js_1.timeEntriesToCsv)(store.getTimeEntries(db, projectId), project, client, (at) => (0, invoice_js_1.rateAt)(rates, projectId, at)));
1657
+ let csv;
1658
+ if (format === "clockify") {
1659
+ const account = await requirePaidPlan(db, (0, index_js_1.tr) `Exportar a Clockify`);
1660
+ const email = str(args, "email") ?? account?.email;
1661
+ if (!email)
1662
+ throw new UserError((0, index_js_1.tr) `Falta --email: el correo de tu cuenta de Clockify.`);
1663
+ const dateFormat = (str(args, "date-format") ?? "MM/DD/YYYY").toUpperCase();
1664
+ if (!clockify_js_1.CLOCKIFY_DATE_FORMATS.includes(dateFormat)) {
1665
+ throw new UserError((0, index_js_1.tr) `--date-format admite ${clockify_js_1.CLOCKIFY_DATE_FORMATS.join(", ")}.`);
1666
+ }
1667
+ const timeFormat = str(args, "time-format") ?? "12h";
1668
+ if (timeFormat !== "12h" && timeFormat !== "24h")
1669
+ throw new UserError((0, index_js_1.tr) `--time-format admite 12h o 24h.`);
1670
+ csv = (0, index_js_1.withLang)(lang, () => (0, clockify_js_1.timeEntriesToClockifyCsv)(entries, project, client, { email, dateFormat, timeFormat }));
1671
+ }
1672
+ else {
1673
+ csv = (0, index_js_1.withLang)(lang, () => (0, csv_js_1.timeEntriesToCsv)(entries, project, client, (at) => (0, invoice_js_1.rateAt)(rates, projectId, at)));
1674
+ }
1540
1675
  const out = str(args, "out");
1541
1676
  if (out) {
1542
1677
  (0, node_fs_1.writeFileSync)(out, csv, "utf8");
1543
1678
  console.log((0, index_js_1.tr) `CSV: ${out}`);
1544
1679
  // Solo si va a un fichero: por la salida estándar el aviso se colaría
1545
1680
  // dentro del propio CSV.
1546
- hintDocLang(args, client, lang);
1681
+ if (format === "clockify") {
1682
+ console.log((0, index_js_1.tr) ` ${entries.length} bloques. En Clockify: Settings → Import → Timesheets.`);
1683
+ console.log((0, index_js_1.tr) ` La fecha y la hora tienen que coincidir con el formato de tu perfil de Clockify;`);
1684
+ console.log((0, index_js_1.tr) ` si no, repítelo con --date-format DD/MM/YYYY o --time-format 24h.`);
1685
+ }
1686
+ else {
1687
+ hintDocLang(args, client, lang);
1688
+ }
1547
1689
  }
1548
1690
  else {
1549
1691
  process.stdout.write(csv);
@@ -1553,6 +1695,54 @@ function cmdExport(args, dbPath) {
1553
1695
  db.close();
1554
1696
  }
1555
1697
  }
1698
+ /**
1699
+ * `--month YYYY-MM`, o `--from`/`--to` en fechas locales. Sin ninguno, todo:
1700
+ * es lo que hacía `estela export` antes de tener rango.
1701
+ */
1702
+ function entriesInRange(entries, args) {
1703
+ const month = str(args, "month");
1704
+ if (month && !/^\d{4}-\d{2}$/.test(month))
1705
+ throw new UserError((0, index_js_1.tr) `--month va como YYYY-MM.`);
1706
+ const from = month ? `${month}-01` : str(args, "from");
1707
+ const to = month ? `${month}-31` : str(args, "to");
1708
+ for (const d of [from, to]) {
1709
+ if (d && !/^\d{4}-\d{2}-\d{2}$/.test(d))
1710
+ throw new UserError((0, index_js_1.tr) `Fecha inválida: ${d}`);
1711
+ }
1712
+ return entries.filter((e) => {
1713
+ const day = (0, shared_1.localDate)(e.startedAt);
1714
+ return (!from || day >= from) && (!to || day <= to);
1715
+ });
1716
+ }
1717
+ /**
1718
+ * Lo de Pro, comprobado contra la cuenta y no contra lo guardado aquí: el plan
1719
+ * local se anota "free" al hacer login y nadie lo actualiza después de pagar.
1720
+ * Sin conexión se deja pasar — mejor regalar un export que bloquear a quien
1721
+ * paga porque está en un avión — y se aprovecha la respuesta para dejar el
1722
+ * plan local al día.
1723
+ */
1724
+ async function requirePaidPlan(db, what) {
1725
+ const account = store.getCloudAccount(db);
1726
+ if (!account) {
1727
+ throw new UserError((0, index_js_1.tr) `${what} es de Pro. Vincula tu cuenta con: estela login --email tu@correo.com`);
1728
+ }
1729
+ try {
1730
+ const info = await (0, client_js_1.cloudGet)(account.apiBaseUrl, "/account", { deviceToken: account.deviceToken });
1731
+ store.setCloudAccount(db, { ...account, plan: info.plan });
1732
+ if (info.plan === "free") {
1733
+ throw new UserError((0, index_js_1.tr) `${what} es de Pro. Actualiza con: estela upgrade --plan pro`);
1734
+ }
1735
+ }
1736
+ catch (error) {
1737
+ if (error instanceof client_js_1.CloudError && error.status === 401) {
1738
+ throw new UserError((0, index_js_1.tr) `Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n estela login --email tu@correo.com\n`);
1739
+ }
1740
+ if (!(error instanceof client_js_1.CloudError) || error.status !== undefined)
1741
+ throw error;
1742
+ // Sin conexión: se deja pasar.
1743
+ }
1744
+ return account;
1745
+ }
1556
1746
  function cmdSubscriptionAdd(args, dbPath) {
1557
1747
  const db = (0, schema_js_1.openDatabase)(dbPath);
1558
1748
  try {
@@ -1754,6 +1944,12 @@ async function main() {
1754
1944
  case "project reopen":
1755
1945
  cmdProjectReopen(args, dbPath);
1756
1946
  break;
1947
+ case "project tracker":
1948
+ cmdProjectTracker(args, dbPath);
1949
+ break;
1950
+ case "entry link":
1951
+ cmdEntryLink(args, dbPath);
1952
+ break;
1757
1953
  case "rate set":
1758
1954
  cmdRateSet(args, dbPath);
1759
1955
  break;
@@ -1788,7 +1984,7 @@ async function main() {
1788
1984
  await cmdPublish(args, dbPath);
1789
1985
  break;
1790
1986
  case "export":
1791
- cmdExport(args, dbPath);
1987
+ await cmdExport(args, dbPath);
1792
1988
  break;
1793
1989
  case "report":
1794
1990
  cmdInvoice(args, dbPath);