estela 0.1.1 → 0.1.5

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 (79) hide show
  1. package/README.md +15 -6
  2. package/dist/billing.d.ts +16 -0
  3. package/dist/billing.d.ts.map +1 -0
  4. package/dist/billing.js +71 -0
  5. package/dist/billing.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +350 -92
  8. package/dist/cli.js.map +1 -1
  9. package/dist/cloud/client.d.ts +7 -0
  10. package/dist/cloud/client.d.ts.map +1 -1
  11. package/dist/cloud/client.js +35 -0
  12. package/dist/cloud/client.js.map +1 -1
  13. package/dist/db/store.d.ts +56 -0
  14. package/dist/db/store.d.ts.map +1 -1
  15. package/dist/db/store.js +101 -0
  16. package/dist/db/store.js.map +1 -1
  17. package/dist/export/panel.d.ts +3 -1
  18. package/dist/export/panel.d.ts.map +1 -1
  19. package/dist/export/panel.js +24 -19
  20. package/dist/export/panel.js.map +1 -1
  21. package/dist/export/panel.test.js +46 -5
  22. package/dist/export/panel.test.js.map +1 -1
  23. package/dist/metrics/team.d.ts +9 -0
  24. package/dist/metrics/team.d.ts.map +1 -1
  25. package/dist/metrics/team.js +0 -0
  26. package/dist/metrics/team.js.map +1 -1
  27. package/dist/publish.d.ts +28 -0
  28. package/dist/publish.d.ts.map +1 -0
  29. package/dist/publish.js +184 -0
  30. package/dist/publish.js.map +1 -0
  31. package/dist/publish.test.d.ts +2 -0
  32. package/dist/publish.test.d.ts.map +1 -0
  33. package/dist/publish.test.js +91 -0
  34. package/dist/publish.test.js.map +1 -0
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +83 -57
  37. package/dist/server.js.map +1 -1
  38. package/dist/sync/team.d.ts +21 -0
  39. package/dist/sync/team.d.ts.map +1 -0
  40. package/dist/sync/team.js +68 -0
  41. package/dist/sync/team.js.map +1 -0
  42. package/dist/sync/team.test.d.ts +2 -0
  43. package/dist/sync/team.test.d.ts.map +1 -0
  44. package/dist/sync/team.test.js +102 -0
  45. package/dist/sync/team.test.js.map +1 -0
  46. package/dist/sync.d.ts +20 -0
  47. package/dist/sync.d.ts.map +1 -0
  48. package/dist/sync.js +79 -0
  49. package/dist/sync.js.map +1 -0
  50. package/dist/sync.test.d.ts +2 -0
  51. package/dist/sync.test.d.ts.map +1 -0
  52. package/dist/sync.test.js +151 -0
  53. package/dist/sync.test.js.map +1 -0
  54. package/dist/team.d.ts +51 -0
  55. package/dist/team.d.ts.map +1 -0
  56. package/dist/team.js +122 -0
  57. package/dist/team.js.map +1 -0
  58. package/dist/team.test.d.ts +2 -0
  59. package/dist/team.test.d.ts.map +1 -0
  60. package/dist/team.test.js +163 -0
  61. package/dist/team.test.js.map +1 -0
  62. package/dist/updateCheck.d.ts +8 -0
  63. package/dist/updateCheck.d.ts.map +1 -0
  64. package/dist/updateCheck.js +92 -0
  65. package/dist/updateCheck.js.map +1 -0
  66. package/dist/updateCheck.test.d.ts +2 -0
  67. package/dist/updateCheck.test.d.ts.map +1 -0
  68. package/dist/updateCheck.test.js +27 -0
  69. package/dist/updateCheck.test.js.map +1 -0
  70. package/dist/web-ui.test.js +14 -1
  71. package/dist/web-ui.test.js.map +1 -1
  72. package/package.json +1 -1
  73. package/web/app.css +74 -16
  74. package/web/app.js +187 -38
  75. package/web/apple-touch-icon.png +0 -0
  76. package/web/favicon.png +0 -0
  77. package/web/index.html +18 -2
  78. package/web/logo.png +0 -0
  79. package/web/logo-mark.png +0 -0
package/README.md CHANGED
@@ -41,8 +41,14 @@ Necesitas **Node 22.5 o superior** (por `node:sqlite`).
41
41
  npx estela setup # detecta agentes y repositorios, reconstruye tu historial
42
42
  npx estela web # abre el panel en http://localhost:4319
43
43
  npx estela doctor # revisa los datos y avisa de lo que está mal
44
+ npx estela --version
44
45
  ```
45
46
 
47
+ `npm` no actualiza instalaciones globales por su cuenta. Si la instalaste con
48
+ `npm install -g estela`, Estela avisa sola cuando hay una versión más nueva
49
+ (consulta npm como mucho una vez al día, en segundo plano, sin bloquear nada);
50
+ actualiza con `npm install -g estela@latest`.
51
+
46
52
  `setup` no pregunta nada y no pisa lo que hayas configurado a mano: se puede
47
53
  volver a ejecutar.
48
54
 
@@ -67,16 +73,19 @@ tres commits de mil setecientos.
67
73
  ## Compartir el avance con tu cliente
68
74
 
69
75
  ```sh
70
- estela publish --project acme-web --base-url https://tu-dominio.com
76
+ estela login --email tu@correo.com # una vez
77
+ estela publish --project acme-web
71
78
  ```
72
79
 
73
- Genera un HTML autocontenido con un enlace no adivinable. Enseña horas y
74
- commits; **nunca tu tarifa ni tu consumo de IA**, porque mientras la pagues tú
75
- ese gasto es tuyo y un cliente que sabe qué parte generó una IA tiene un
80
+ Sube un panel de solo lectura, con un enlace no adivinable, alojado en
81
+ getestela.dev no hace falta servidor propio. Enseña horas y commits;
82
+ **nunca tu tarifa ni tu consumo de IA**, porque mientras la pagues ese
83
+ gasto es tuyo y un cliente que sabe qué parte generó una IA tiene un
76
84
  argumento nuevo para negociar tu tarifa.
77
85
 
78
86
  Al republicar, pasa el mismo `--token` o tu cliente se queda con un enlace
79
- muerto.
87
+ muerto. El plan Free permite un panel publicado a la vez; Pro y Teams no
88
+ tienen límite.
80
89
 
81
90
  ## El coste de la IA
82
91
 
@@ -115,7 +124,7 @@ estas horas acaban en un informe que alguien paga.
115
124
  |---|---|---|---|
116
125
  | Todo lo de arriba, en local | ✓ | ✓ | ✓ |
117
126
  | Sincronizar varias máquinas | — | ✓ | ✓ |
118
- | Paneles ilimitados y alojados | 1 | | |
127
+ | Paneles alojados a la vez | 1 | ilimitados | ilimitados |
119
128
  | Horas del equipo **medidas** | — | — | ✓ |
120
129
  | Presupuesto de IA por proyecto | — | — | ✓ |
121
130
 
@@ -0,0 +1,16 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import { NoAccountError } from "./publish.js";
3
+ /**
4
+ * Cobro. La CLI nunca habla con Stripe directamente — solo con nuestra API,
5
+ * que es quien tiene la clave secreta. Aquí solo pedimos la URL alojada
6
+ * (de Checkout o del Portal) y la enseñamos; completar el pago o cancelar
7
+ * pasa siempre en la página de Stripe, nunca en la terminal.
8
+ */
9
+ export { NoAccountError };
10
+ export declare function upgradeCheckout(db: DatabaseSync, plan: "pro" | "teams"): Promise<{
11
+ url: string;
12
+ }>;
13
+ export declare function openBillingPortal(db: DatabaseSync): Promise<{
14
+ url: string;
15
+ }>;
16
+ //# sourceMappingURL=billing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAIhD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,CAAC;AAyB1B,wBAAsB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAIvG;AAED,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAIlF"}
@@ -0,0 +1,71 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NoAccountError = void 0;
37
+ exports.upgradeCheckout = upgradeCheckout;
38
+ exports.openBillingPortal = openBillingPortal;
39
+ const client_js_1 = require("./cloud/client.js");
40
+ const store = __importStar(require("./db/store.js"));
41
+ const publish_js_1 = require("./publish.js");
42
+ Object.defineProperty(exports, "NoAccountError", { enumerable: true, get: function () { return publish_js_1.NoAccountError; } });
43
+ function requireAccount(db) {
44
+ const account = store.getCloudAccount(db);
45
+ if (!account) {
46
+ throw new publish_js_1.NoAccountError(`Necesitas una cuenta para esto. Vincúlala con:\n\n` +
47
+ ` estela login --email tu@correo.com\n`);
48
+ }
49
+ return account;
50
+ }
51
+ async function unwrap(call) {
52
+ try {
53
+ return await call;
54
+ }
55
+ catch (error) {
56
+ if (error instanceof client_js_1.CloudError && error.status === 401) {
57
+ throw new publish_js_1.NoAccountError(`Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n` +
58
+ ` estela login --email tu@correo.com\n`);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ async function upgradeCheckout(db, plan) {
64
+ const account = requireAccount(db);
65
+ return unwrap((0, client_js_1.cloudPost)(account.apiBaseUrl, "/billing/checkout", { plan }, { deviceToken: account.deviceToken }));
66
+ }
67
+ async function openBillingPortal(db) {
68
+ const account = requireAccount(db);
69
+ return unwrap((0, client_js_1.cloudPost)(account.apiBaseUrl, "/billing/portal", {}, { deviceToken: account.deviceToken }));
70
+ }
71
+ //# sourceMappingURL=billing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"billing.js","sourceRoot":"","sources":["../src/billing.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,0CAIC;AAED,8CAIC;AA9CD,iDAA0D;AAC1D,qDAAuC;AACvC,6CAA8C;AASrC,+FATA,2BAAc,OASA;AAEvB,SAAS,cAAc,CAAC,EAAgB;IACtC,MAAM,OAAO,GAAG,KAAK,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,2BAAc,CACtB,oDAAoD;YACpD,wCAAwC,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,MAAM,CAAI,IAAgB;IACvC,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,sBAAU,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACxD,MAAM,IAAI,2BAAc,CACtB,yDAAyD;gBACzD,wCAAwC,CAAC,CAAC;QAC9C,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,EAAgB,EAAE,IAAqB;IAC3E,MAAM,OAAO,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,IAAA,qBAAS,EAAC,OAAO,CAAC,UAAU,EAAE,mBAAmB,EAAE,EAAE,IAAI,EAAE,EACvE,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,EAAgB;IACtD,MAAM,OAAO,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,IAAA,qBAAS,EAAC,OAAO,CAAC,UAAU,EAAE,iBAAiB,EAAE,EAAE,EAC/D,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAmhCpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAsCA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAotCpD,OAAO,EAAE,eAAe,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -54,9 +54,14 @@ const doctor_js_1 = require("./doctor.js");
54
54
  const setup_js_1 = require("./setup.js");
55
55
  const csv_js_1 = require("./export/csv.js");
56
56
  const invoice_pdf_js_1 = require("./export/invoice-pdf.js");
57
- const panel_js_1 = require("./export/panel.js");
58
57
  const share_js_1 = require("./export/share.js");
59
58
  const server_js_1 = require("./server.js");
59
+ const updateCheck_js_1 = require("./updateCheck.js");
60
+ const publish_js_1 = require("./publish.js");
61
+ const sync_js_1 = require("./sync.js");
62
+ const team_js_1 = require("./sync/team.js");
63
+ const team_js_2 = require("./team.js");
64
+ const billing_js_1 = require("./billing.js");
60
65
  const auth_js_1 = require("./cloud/auth.js");
61
66
  const client_js_1 = require("./cloud/client.js");
62
67
  const cost_js_1 = require("./pricing/cost.js");
@@ -64,7 +69,6 @@ Object.defineProperty(exports, "breakdownOfTurn", { enumerable: true, get: funct
64
69
  const claude_js_1 = require("./watchers/claude.js");
65
70
  const git_js_1 = require("./watchers/git.js");
66
71
  const identity_js_1 = require("./watchers/identity.js");
67
- const team_js_1 = require("./metrics/team.js");
68
72
  const HELP = `
69
73
  estela — registro de horas para desarrollo asistido por IA
70
74
 
@@ -120,6 +124,35 @@ estela — registro de horas para desarrollo asistido por IA
120
124
  estela logout
121
125
  Desvincula esta máquina. Tus datos locales no se tocan.
122
126
 
127
+ estela sync enable --project <id>
128
+ Activa el sync de un proyecto entre tus máquinas. Función de Pro; sin
129
+ una fila aquí, ese proyecto no sale de esta máquina nunca.
130
+ estela sync [--project <id>]
131
+ Sube y baja lo cambiado desde el último sync. Sin --project,
132
+ sincroniza todos los proyectos activados (personales y de equipo).
133
+
134
+ estela team invite --project <id> --email <a@b.com>[,otro@c.com] [--kind employee|freelancer]
135
+ Invita a alguien a que sus horas en ese proyecto se midan de verdad
136
+ en el panel, en vez de estimarse por commits. Función de Teams. Los
137
+ correos son los de commit del invitado, no con los que se loguee.
138
+ estela team accept --token <token> --as-id <id-local>
139
+ Acepta una invitación. Crea el proyecto local con ese id (tuyo, no
140
+ el del dueño) y lo deja listo para sincronizar.
141
+ estela team repo --project <id> --add <ruta>
142
+ Vincula tu clon local a un proyecto de equipo ya aceptado.
143
+ estela team list --project <id>
144
+ Quién ha aceptado, sus horas medidas, y cuándo sincronizó por última vez.
145
+ estela team revoke --token <token>
146
+ Quita a alguien del equipo. Si pagas Teams, baja lo que se cobra.
147
+
148
+ estela upgrade --plan pro|teams
149
+ Da de alta el plan. Imprime un enlace de Stripe Checkout — el pago
150
+ se completa ahí, nunca en la terminal.
151
+ estela billing portal
152
+ Cambiar de tarjeta o cancelar. Lo hace Stripe, no nosotros.
153
+ estela account
154
+ Tu correo, tu plan, y desde qué máquinas te has conectado.
155
+
123
156
  estela export --project <id> [--out <fichero.csv>]
124
157
 
125
158
  estela report --project <id> --cutoff YYYY-MM-DD [--dry-run]
@@ -129,6 +162,7 @@ estela — registro de horas para desarrollo asistido por IA
129
162
  factura: Estela no emite documentos fiscales.
130
163
 
131
164
  Opciones globales: --db <ruta> (por defecto ${schema_js_1.DEFAULT_DB_PATH})
165
+ --version Solo imprime la versión instalada.
132
166
  `;
133
167
  function parseArgs(argv) {
134
168
  const positional = [];
@@ -697,95 +731,286 @@ async function cmdPublish(args, dbPath) {
697
731
  const db = (0, schema_js_1.openDatabase)(dbPath);
698
732
  try {
699
733
  const projectId = required(args, "project");
700
- const project = store.getProject(db, projectId);
701
- if (!project)
734
+ let result;
735
+ try {
736
+ result = await (0, publish_js_1.publishPanel)(db, {
737
+ projectId,
738
+ ...(str(args, "token") ? { token: str(args, "token") } : {}),
739
+ ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
740
+ withAmounts: args.flags["with-amounts"] === true,
741
+ includeTeam: args.flags["no-team"] !== true,
742
+ });
743
+ }
744
+ catch (error) {
745
+ if (error instanceof publish_js_1.NoAccountError)
746
+ throw new UserError(error.message);
747
+ throw error;
748
+ }
749
+ console.log(`\nPanel publicado.\n`);
750
+ console.log(` Enlace: ${result.url}\n`);
751
+ if (result.adopted) {
752
+ console.log(` Este panel ya existía y queda vinculado a tu cuenta desde ahora.\n`);
753
+ }
754
+ console.log(` El token es lo único que protege la página: quien tenga el enlace, entra.`);
755
+ console.log(` Al republicar, pasa --token ${result.token} o el cliente perderá su enlace.`);
756
+ }
757
+ finally {
758
+ db.close();
759
+ }
760
+ }
761
+ /**
762
+ * `estela sync enable --project <id>` — opt-in explícito, proyecto por
763
+ * proyecto. Sin esta fila, ese proyecto no sale de esta máquina nunca, sea
764
+ * cual sea el plan de la cuenta.
765
+ */
766
+ function cmdSyncEnable(args, dbPath) {
767
+ const db = (0, schema_js_1.openDatabase)(dbPath);
768
+ try {
769
+ const projectId = required(args, "project");
770
+ if (!store.getProject(db, projectId)) {
702
771
  throw new UserError(`No existe el proyecto "${projectId}".`);
703
- const client = store.getClient(db, project.clientId);
704
- const rates = store.getRates(db, projectId);
705
- // Reutilizar el token si se republica: el cliente ya tiene ese enlace
706
- // guardado y cambiárselo sin avisar le rompe el marcador.
707
- const token = str(args, "token") ?? (0, panel_js_1.newPanelToken)();
708
- // Las métricas necesitan los commits del proyecto y saber qué ramas están
709
- // integradas. Sin lo segundo no se puede distinguir entregado de en curso.
710
- const projectCommits = commitsOfProject(db, project.repoPaths);
711
- const merged = project.repoPaths[0]
712
- ? await (0, git_js_1.mergedBranches)(project.repoPaths[0]) : null;
713
- // El equipo del proyecto. Se enseña porque el repositorio es del cliente y
714
- // esos nombres ya los tiene en su git; lo que no sale de aquí son las horas
715
- // ajenas, que se quitan ANTES de construir el fichero. Ocultarlas al pintar
716
- // dejaría el dato en el código fuente, que es no ocultarlas.
717
- const myEntries = store.getTimeEntries(db, projectId);
718
- const mineByRepo = await (0, identity_js_1.myEmailsByRepo)(db, project.repoPaths);
719
- const myEmails = new Set();
720
- for (const set of mineByRepo.values())
721
- for (const email of set)
722
- myEmails.add(email);
723
- // Acotado al periodo que cubre el panel. Sin esto se compara el historial
724
- // entero del repositorio con las semanas que llevas tú: salían 523 commits
725
- // de un compañero frente a 11 tuyos, y el panel que venía a respaldar tu
726
- // trabajo te dejaba a la altura del betún. Además mete gente que ya no está
727
- // en el proyecto.
728
- const worked = myEntries.map((e) => (0, shared_1.localDate)(e.startedAt)).sort((a, b) => a.localeCompare(b));
729
- const from = worked[0] ?? "0000-01-01";
730
- const to = worked[worked.length - 1] ?? "9999-12-31";
731
- const inPeriod = projectCommits.filter((c) => {
732
- const day = (0, shared_1.localDate)(c.at);
733
- return day >= from && day <= to;
734
- });
735
- const team = (0, team_js_1.teamView)(inPeriod, { myEmails, myEntries }).map((p) => ({
736
- name: p.name,
737
- isMe: p.isMe,
738
- ...(p.isMe ? { seconds: p.seconds } : {}),
739
- commits: p.commits,
740
- branches: p.branches.length,
741
- lastDay: p.lastDay,
742
- }));
743
- const html = (0, panel_js_1.buildPanel)({
744
- project, client,
745
- entries: myEntries,
746
- ...(args.flags["no-team"] === true ? {} : { team }),
747
- ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
748
- withAmounts: args.flags["with-amounts"] === true,
749
- rateAt: (at) => (0, invoice_js_1.rateAt)(rates, projectId, at),
750
- aiPayer: "self",
751
- commits: projectCommits,
752
- ...(merged ? { merged } : {}),
753
- commitsOf: (hashes) => hashes.length
754
- ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
755
- : [],
772
+ }
773
+ if (!store.listPersonalSyncCandidates(db).includes(projectId)) {
774
+ throw new UserError(`"${projectId}" ya está enlazado a un equipo y no puede sincronizarse también como personal.`);
775
+ }
776
+ store.setProjectSync(db, {
777
+ projectId, scope: "personal", remoteProjectId: projectId,
778
+ remoteOrgId: null, inviteToken: null,
756
779
  });
757
- // Alojarlo exige sesión: es la cuenta la que decide cuántos paneles caben
758
- // en tu plan, y sin ella no hay a quién cargarle esa cuota.
780
+ console.log(`Sync activado para "${projectId}". Ejecuta "estela sync" para subir y bajar.`);
781
+ }
782
+ finally {
783
+ db.close();
784
+ }
785
+ }
786
+ /** `estela sync [--project <id>]` — sin --project, sincroniza todos los proyectos habilitados. */
787
+ async function cmdSync(args, dbPath) {
788
+ const db = (0, schema_js_1.openDatabase)(dbPath);
789
+ try {
790
+ const only = str(args, "project");
791
+ const projectIds = only ? [only] : store.listProjectSyncs(db).map((s) => s.projectId);
792
+ if (projectIds.length === 0) {
793
+ console.log(`Ningún proyecto sincroniza todavía. Actívalo con: estela sync enable --project <id>`);
794
+ return;
795
+ }
796
+ for (const projectId of projectIds) {
797
+ const project = store.getProject(db, projectId);
798
+ const label = project?.name ?? projectId;
799
+ const scope = store.getProjectSync(db, projectId)?.scope;
800
+ try {
801
+ if (scope === "team") {
802
+ const outcome = await (0, team_js_1.syncTeamProject)(db, projectId);
803
+ console.log(`${label}: ${(0, shared_1.formatDuration)(outcome.seconds)} totales enviadas`);
804
+ }
805
+ else {
806
+ const outcome = await (0, sync_js_1.syncProject)(db, projectId);
807
+ console.log(`${label}: ${outcome.pushed} subidas, ${outcome.pulled} bajadas`);
808
+ }
809
+ }
810
+ catch (error) {
811
+ if (error instanceof publish_js_1.NoAccountError || error instanceof sync_js_1.NotSyncedError)
812
+ throw new UserError(error.message);
813
+ throw error;
814
+ }
815
+ }
816
+ }
817
+ finally {
818
+ db.close();
819
+ }
820
+ }
821
+ /**
822
+ * `estela team invite --project <id> --email <a@b,c@d> [--kind employee|freelancer]`
823
+ *
824
+ * Los correos son los de commit de quien se invita, no con los que se vaya a
825
+ * loguear en Estela — es el mismo criterio que ya usa "estela author" para
826
+ * la propia identidad, y es lo que permite emparejarlo en el panel.
827
+ */
828
+ async function cmdTeamInvite(args, dbPath) {
829
+ const db = (0, schema_js_1.openDatabase)(dbPath);
830
+ try {
831
+ const projectId = required(args, "project");
832
+ const emails = required(args, "email").split(",").map((e) => e.trim()).filter(Boolean);
833
+ const kind = (str(args, "kind") ?? "employee");
834
+ if (kind !== "employee" && kind !== "freelancer") {
835
+ throw new UserError(`--kind debe ser "employee" o "freelancer".`);
836
+ }
837
+ let result;
838
+ try {
839
+ result = await (0, team_js_2.inviteTeamMember)(db, { projectId, emails, inviteeKind: kind });
840
+ }
841
+ catch (error) {
842
+ if (error instanceof publish_js_1.NoAccountError)
843
+ throw new UserError(error.message);
844
+ throw error;
845
+ }
846
+ console.log(`\nInvitación creada para: ${emails.join(", ")}\n`);
847
+ console.log(` Pásale esto (por el canal que uses con él, no hace falta correo):\n`);
848
+ console.log(` estela login --email <su-correo>`);
849
+ console.log(` estela team accept --token ${result.token} --as-id <id-que-elija>\n`);
850
+ }
851
+ finally {
852
+ db.close();
853
+ }
854
+ }
855
+ /**
856
+ * `estela team accept --token <token> --as-id <id-local>`
857
+ *
858
+ * El id local es obligatorio y nunca se autogenera: reusar el id del dueño
859
+ * tal cual podría pisar en silencio un proyecto propio que ya tuviera ese
860
+ * mismo id.
861
+ */
862
+ async function cmdTeamAccept(args, dbPath) {
863
+ const db = (0, schema_js_1.openDatabase)(dbPath);
864
+ try {
865
+ const token = required(args, "token");
866
+ const localProjectId = required(args, "as-id");
867
+ let result;
868
+ try {
869
+ result = await (0, team_js_2.acceptTeamInvite)(db, { token, localProjectId });
870
+ }
871
+ catch (error) {
872
+ if (error instanceof publish_js_1.NoAccountError)
873
+ throw new UserError(error.message);
874
+ throw error;
875
+ }
876
+ console.log(`\nInvitación aceptada: "${result.projectName}"`);
877
+ console.log(` Declarada para: ${result.emails.join(", ")} (${result.inviteeKind})`);
878
+ console.log(` Si no eres tú, avisa a quien te invitó.\n`);
879
+ console.log(`Ahora vincula tu repositorio local y sincroniza:\n`);
880
+ console.log(` estela team repo --project ${localProjectId} --add <ruta-de-tu-clon>`);
881
+ console.log(` estela import`);
882
+ console.log(` estela sync\n`);
883
+ }
884
+ finally {
885
+ db.close();
886
+ }
887
+ }
888
+ /** `estela team repo --project <id> --add <ruta>` — solo añade el repositorio, nada más. */
889
+ function cmdTeamRepo(args, dbPath) {
890
+ const db = (0, schema_js_1.openDatabase)(dbPath);
891
+ try {
892
+ const projectId = required(args, "project");
893
+ const repo = required(args, "add");
894
+ if (!store.getProject(db, projectId))
895
+ throw new UserError(`No existe el proyecto "${projectId}".`);
896
+ store.addProjectRepo(db, projectId, (0, node_path_1.resolve)(repo));
897
+ console.log(`Repositorio vinculado a "${projectId}": ${(0, node_path_1.resolve)(repo)}`);
898
+ }
899
+ finally {
900
+ db.close();
901
+ }
902
+ }
903
+ /** `estela team list --project <id>` — miembros aceptados, sus horas, y cuándo sincronizaron. */
904
+ async function cmdTeamList(args, dbPath) {
905
+ const db = (0, schema_js_1.openDatabase)(dbPath);
906
+ try {
907
+ const projectId = required(args, "project");
908
+ let members;
909
+ try {
910
+ members = await (0, team_js_2.listTeamMembers)(db, projectId);
911
+ }
912
+ catch (error) {
913
+ if (error instanceof publish_js_1.NoAccountError)
914
+ throw new UserError(error.message);
915
+ throw error;
916
+ }
917
+ if (members.length === 0) {
918
+ console.log(`Nadie ha aceptado todavía en "${projectId}".`);
919
+ return;
920
+ }
921
+ for (const m of members) {
922
+ const synced = m.syncedAt ? `sincronizado ${m.syncedAt.slice(0, 10)}` : "sin sincronizar aún";
923
+ console.log(` ${m.emails.join(", ")} (${m.inviteeKind}) · ${(0, shared_1.formatDuration)(m.seconds)} · ${synced}`);
924
+ }
925
+ }
926
+ finally {
927
+ db.close();
928
+ }
929
+ }
930
+ /** `estela team revoke --token <token>` — también baja la cantidad de la suscripción de Teams. */
931
+ async function cmdTeamRevoke(args, dbPath) {
932
+ const db = (0, schema_js_1.openDatabase)(dbPath);
933
+ try {
934
+ const token = required(args, "token");
935
+ try {
936
+ await (0, team_js_2.revokeTeamMember)(db, token);
937
+ }
938
+ catch (error) {
939
+ if (error instanceof publish_js_1.NoAccountError)
940
+ throw new UserError(error.message);
941
+ throw error;
942
+ }
943
+ console.log(`Invitación revocada.`);
944
+ }
945
+ finally {
946
+ db.close();
947
+ }
948
+ }
949
+ /** `estela upgrade --plan pro|teams` — imprime la URL de Checkout, no la abre. */
950
+ async function cmdUpgrade(args, dbPath) {
951
+ const db = (0, schema_js_1.openDatabase)(dbPath);
952
+ try {
953
+ const plan = required(args, "plan");
954
+ if (plan !== "pro" && plan !== "teams")
955
+ throw new UserError(`--plan debe ser "pro" o "teams".`);
956
+ let result;
957
+ try {
958
+ result = await (0, billing_js_1.upgradeCheckout)(db, plan);
959
+ }
960
+ catch (error) {
961
+ if (error instanceof publish_js_1.NoAccountError)
962
+ throw new UserError(error.message);
963
+ throw error;
964
+ }
965
+ console.log(`\nAbre esto para completar el alta a ${plan === "pro" ? "Pro" : "Teams"}:\n`);
966
+ console.log(` ${result.url}\n`);
967
+ }
968
+ finally {
969
+ db.close();
970
+ }
971
+ }
972
+ /** `estela billing portal` — cancelar o cambiar de tarjeta pasa en la página de Stripe. */
973
+ async function cmdBillingPortal(dbPath) {
974
+ const db = (0, schema_js_1.openDatabase)(dbPath);
975
+ try {
976
+ let result;
977
+ try {
978
+ result = await (0, billing_js_1.openBillingPortal)(db);
979
+ }
980
+ catch (error) {
981
+ if (error instanceof publish_js_1.NoAccountError)
982
+ throw new UserError(error.message);
983
+ throw error;
984
+ }
985
+ console.log(`\nGestiona tu pago aquí:\n\n ${result.url}\n`);
986
+ }
987
+ finally {
988
+ db.close();
989
+ }
990
+ }
991
+ /** `estela account` — primer consumidor real de GET /account. */
992
+ async function cmdAccount(dbPath) {
993
+ const db = (0, schema_js_1.openDatabase)(dbPath);
994
+ try {
759
995
  const account = store.getCloudAccount(db);
760
996
  if (!account) {
761
- throw new UserError(`Necesitas una cuenta para publicar. Vincúlala con:\n\n` +
997
+ throw new UserError(`Esta máquina no está vinculada a ninguna cuenta. Vincúlala con:\n\n` +
762
998
  ` estela login --email tu@correo.com\n`);
763
999
  }
764
- let result;
1000
+ let info;
765
1001
  try {
766
- result = await (0, client_js_1.cloudPost)(account.apiBaseUrl, "/panels", { token, projectName: project.name, html }, { deviceToken: account.deviceToken });
1002
+ info = await (0, client_js_1.cloudGet)(account.apiBaseUrl, "/account", { deviceToken: account.deviceToken });
767
1003
  }
768
1004
  catch (error) {
769
- if (error instanceof client_js_1.CloudError && error.status === 402) {
770
- // El mensaje ya viene redactado desde el servidor (panels.ts): es la
771
- // única fuente de la verdad sobre cuántos paneles permite cada plan,
772
- // y duplicar ese texto aquí lo desincronizaría en cuanto cambiara allí.
773
- throw new UserError(error.message);
774
- }
775
1005
  if (error instanceof client_js_1.CloudError && error.status === 401) {
776
- throw new UserError(`Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n` +
777
- ` estela login --email tu@correo.com\n`);
1006
+ throw new UserError(`Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n estela login --email tu@correo.com\n`);
778
1007
  }
779
1008
  throw error;
780
1009
  }
781
- store.recordPublication(db, projectId, token, "https://getestela.dev");
782
- console.log(`\nPanel publicado.\n`);
783
- console.log(` Enlace: ${result.url}\n`);
784
- if (result.adopted) {
785
- console.log(` Este panel ya existía y queda vinculado a tu cuenta desde ahora.\n`);
1010
+ console.log(`\n${info.email} · plan ${info.plan}\n`);
1011
+ for (const d of info.devices) {
1012
+ console.log(` ${d.label ?? "(sin nombre)"} · v${d.clientVersion ?? "?"} · visto ${d.lastSeenAt?.slice(0, 10) ?? "nunca"}`);
786
1013
  }
787
- console.log(` El token es lo único que protege la página: quien tenga el enlace, entra.`);
788
- console.log(` Al republicar, pasa --token ${token} o el cliente perderá su enlace.`);
789
1014
  }
790
1015
  finally {
791
1016
  db.close();
@@ -829,21 +1054,6 @@ async function cmdAuthor(args, dbPath) {
829
1054
  }
830
1055
  }
831
1056
  /** Commits de los repositorios de un proyecto, con sus ficheros. */
832
- function commitsOfProject(db, repoPaths) {
833
- if (repoPaths.length === 0)
834
- return [];
835
- const like = repoPaths.map(() => "repo_path LIKE ?").join(" OR ");
836
- const rows = db.prepare(`SELECT * FROM commits WHERE ${like}`)
837
- .all(...repoPaths.map((p) => `${p}%`));
838
- return rows.map((r) => ({
839
- repoPath: r["repo_path"], hash: r["hash"],
840
- at: new Date(r["at"]), authorEmail: r["author_email"],
841
- authorName: r["author_name"] ?? "",
842
- branch: r["branch"] ?? null, subject: r["subject"],
843
- linesAdded: r["lines_added"], linesDeleted: r["lines_deleted"],
844
- files: String(r["files"] ?? "").split("\n").filter(Boolean),
845
- }));
846
- }
847
1057
  /** Revisa los datos y avisa de lo que rompería una demo. */
848
1058
  function cmdDoctor(dbPath) {
849
1059
  const db = (0, schema_js_1.openDatabase)(dbPath);
@@ -1025,11 +1235,29 @@ function cmdInvoice(args, dbPath) {
1025
1235
  }
1026
1236
  }
1027
1237
  // ---------------------------------------------------------------------------
1238
+ /**
1239
+ * De un paquete publicado, `dist/cli.js` está siempre un nivel por debajo de
1240
+ * `package.json`. Leerlo así, en vez de tenerlo escrito dos veces, es lo
1241
+ * único que garantiza que este número y el que ve `npm view estela version`
1242
+ * nunca se desincronicen.
1243
+ */
1244
+ const VERSION = require("../package.json").version;
1028
1245
  async function main() {
1029
1246
  const argv = process.argv.slice(2);
1030
1247
  const args = parseArgs(argv);
1248
+ // Global y antes que nada: "estela --version" no debe abrir ninguna base de
1249
+ // datos ni comprobar el resto de argumentos para responder algo tan simple.
1250
+ if (args.flags["version"] === true) {
1251
+ console.log(VERSION);
1252
+ return;
1253
+ }
1254
+ (0, client_js_1.setClientVersion)(VERSION);
1031
1255
  const dbPath = str(args, "db") ?? schema_js_1.DEFAULT_DB_PATH;
1032
1256
  const command = args._.join(" ");
1257
+ // Se llama aquí, no al final: un comando que falla a medias (por ejemplo
1258
+ // "estela import" sin repositorios) debe avisar igual de que hay una
1259
+ // versión nueva, no solo los que terminan bien.
1260
+ (0, updateCheck_js_1.checkForUpdate)(VERSION);
1033
1261
  switch (command) {
1034
1262
  case "setup":
1035
1263
  await cmdSetup(args, dbPath);
@@ -1091,6 +1319,36 @@ async function main() {
1091
1319
  case "logout":
1092
1320
  await cmdLogout(dbPath);
1093
1321
  break;
1322
+ case "sync enable":
1323
+ cmdSyncEnable(args, dbPath);
1324
+ break;
1325
+ case "sync":
1326
+ await cmdSync(args, dbPath);
1327
+ break;
1328
+ case "team invite":
1329
+ await cmdTeamInvite(args, dbPath);
1330
+ break;
1331
+ case "team accept":
1332
+ await cmdTeamAccept(args, dbPath);
1333
+ break;
1334
+ case "team repo":
1335
+ cmdTeamRepo(args, dbPath);
1336
+ break;
1337
+ case "team list":
1338
+ await cmdTeamList(args, dbPath);
1339
+ break;
1340
+ case "team revoke":
1341
+ await cmdTeamRevoke(args, dbPath);
1342
+ break;
1343
+ case "upgrade":
1344
+ await cmdUpgrade(args, dbPath);
1345
+ break;
1346
+ case "billing portal":
1347
+ await cmdBillingPortal(dbPath);
1348
+ break;
1349
+ case "account":
1350
+ await cmdAccount(dbPath);
1351
+ break;
1094
1352
  case "":
1095
1353
  case "help":
1096
1354
  console.log(HELP);