estela 0.1.3 → 0.1.6

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 (76) hide show
  1. package/dist/billing/sessionize.d.ts +10 -1
  2. package/dist/billing/sessionize.d.ts.map +1 -1
  3. package/dist/billing/sessionize.js +0 -0
  4. package/dist/billing/sessionize.js.map +1 -1
  5. package/dist/billing/sessionize.test.js +28 -0
  6. package/dist/billing/sessionize.test.js.map +1 -1
  7. package/dist/billing.d.ts +16 -0
  8. package/dist/billing.d.ts.map +1 -0
  9. package/dist/billing.js +71 -0
  10. package/dist/billing.js.map +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +345 -93
  13. package/dist/cli.js.map +1 -1
  14. package/dist/cloud/client.d.ts +6 -0
  15. package/dist/cloud/client.d.ts.map +1 -1
  16. package/dist/cloud/client.js +26 -0
  17. package/dist/cloud/client.js.map +1 -1
  18. package/dist/db/store.d.ts +56 -0
  19. package/dist/db/store.d.ts.map +1 -1
  20. package/dist/db/store.js +101 -0
  21. package/dist/db/store.js.map +1 -1
  22. package/dist/export/panel.d.ts +3 -1
  23. package/dist/export/panel.d.ts.map +1 -1
  24. package/dist/export/panel.js +24 -19
  25. package/dist/export/panel.js.map +1 -1
  26. package/dist/export/panel.test.js +46 -5
  27. package/dist/export/panel.test.js.map +1 -1
  28. package/dist/metrics/team.d.ts +9 -0
  29. package/dist/metrics/team.d.ts.map +1 -1
  30. package/dist/metrics/team.js +0 -0
  31. package/dist/metrics/team.js.map +1 -1
  32. package/dist/publish.d.ts +37 -0
  33. package/dist/publish.d.ts.map +1 -0
  34. package/dist/publish.js +185 -0
  35. package/dist/publish.js.map +1 -0
  36. package/dist/publish.test.d.ts +2 -0
  37. package/dist/publish.test.d.ts.map +1 -0
  38. package/dist/publish.test.js +91 -0
  39. package/dist/publish.test.js.map +1 -0
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +88 -58
  42. package/dist/server.js.map +1 -1
  43. package/dist/sync/team.d.ts +21 -0
  44. package/dist/sync/team.d.ts.map +1 -0
  45. package/dist/sync/team.js +68 -0
  46. package/dist/sync/team.js.map +1 -0
  47. package/dist/sync/team.test.d.ts +2 -0
  48. package/dist/sync/team.test.d.ts.map +1 -0
  49. package/dist/sync/team.test.js +102 -0
  50. package/dist/sync/team.test.js.map +1 -0
  51. package/dist/sync.d.ts +20 -0
  52. package/dist/sync.d.ts.map +1 -0
  53. package/dist/sync.js +79 -0
  54. package/dist/sync.js.map +1 -0
  55. package/dist/sync.test.d.ts +2 -0
  56. package/dist/sync.test.d.ts.map +1 -0
  57. package/dist/sync.test.js +151 -0
  58. package/dist/sync.test.js.map +1 -0
  59. package/dist/team.d.ts +51 -0
  60. package/dist/team.d.ts.map +1 -0
  61. package/dist/team.js +122 -0
  62. package/dist/team.js.map +1 -0
  63. package/dist/team.test.d.ts +2 -0
  64. package/dist/team.test.d.ts.map +1 -0
  65. package/dist/team.test.js +163 -0
  66. package/dist/team.test.js.map +1 -0
  67. package/dist/web-ui.test.js +14 -1
  68. package/dist/web-ui.test.js.map +1 -1
  69. package/package.json +1 -1
  70. package/web/app.css +74 -16
  71. package/web/app.js +198 -38
  72. package/web/apple-touch-icon.png +0 -0
  73. package/web/favicon.png +0 -0
  74. package/web/index.html +18 -2
  75. package/web/logo.png +0 -0
  76. package/web/logo-mark.png +0 -0
package/dist/cli.js CHANGED
@@ -54,10 +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");
60
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");
61
65
  const auth_js_1 = require("./cloud/auth.js");
62
66
  const client_js_1 = require("./cloud/client.js");
63
67
  const cost_js_1 = require("./pricing/cost.js");
@@ -65,7 +69,6 @@ Object.defineProperty(exports, "breakdownOfTurn", { enumerable: true, get: funct
65
69
  const claude_js_1 = require("./watchers/claude.js");
66
70
  const git_js_1 = require("./watchers/git.js");
67
71
  const identity_js_1 = require("./watchers/identity.js");
68
- const team_js_1 = require("./metrics/team.js");
69
72
  const HELP = `
70
73
  estela — registro de horas para desarrollo asistido por IA
71
74
 
@@ -121,6 +124,35 @@ estela — registro de horas para desarrollo asistido por IA
121
124
  estela logout
122
125
  Desvincula esta máquina. Tus datos locales no se tocan.
123
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
+
124
156
  estela export --project <id> [--out <fichero.csv>]
125
157
 
126
158
  estela report --project <id> --cutoff YYYY-MM-DD [--dry-run]
@@ -334,7 +366,11 @@ async function cmdImport(args, dbPath) {
334
366
  const fromAgents = (0, sessionize_js_1.attachCommits)((0, sessionize_js_1.sessionize)(turns), commitsAll);
335
367
  const fromCommits = (0, sessionize_js_1.withoutOverlap)((0, sessionize_js_1.sessionizeCommits)(commitsAll), fromAgents);
336
368
  const raw = [...fromAgents, ...fromCommits];
337
- const blocks = (0, sessionize_js_1.groupByBranchAndDay)(raw);
369
+ // Se agrupa por PROYECTO, no por ruta: el agente abierto en `repo/` y en
370
+ // `repo/src/UI` son dos rutas del mismo proyecto, y agrupar por ruta
371
+ // producía dos bloques que luego se pisaban al guardarse con el mismo id.
372
+ const porProyecto = (b) => (b.repoPath ? store.projectForRepo(db, b.repoPath) ?? b.repoPath : "");
373
+ const blocks = (0, sessionize_js_1.groupByBranchAndDay)(raw, porProyecto);
338
374
  if (fromCommits.length) {
339
375
  console.log(` ${fromCommits.length} bloques deducidos solo de commits (sin agente)`);
340
376
  }
@@ -699,95 +735,296 @@ async function cmdPublish(args, dbPath) {
699
735
  const db = (0, schema_js_1.openDatabase)(dbPath);
700
736
  try {
701
737
  const projectId = required(args, "project");
702
- const project = store.getProject(db, projectId);
703
- if (!project)
738
+ // Sin --client no se manda nada, y el servidor deja intacta la lista que
739
+ // ya hubiera: republicar el avance de la semana no puede desvincular al
740
+ // cliente que ya tenía este panel en su cuenta.
741
+ const clientes = str(args, "client")
742
+ ?.split(",").map((e) => e.trim()).filter(Boolean);
743
+ let result;
744
+ try {
745
+ result = await (0, publish_js_1.publishPanel)(db, {
746
+ projectId,
747
+ ...(str(args, "token") ? { token: str(args, "token") } : {}),
748
+ ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
749
+ ...(clientes?.length ? { clients: clientes } : {}),
750
+ withAmounts: args.flags["with-amounts"] === true,
751
+ includeTeam: args.flags["no-team"] !== true,
752
+ });
753
+ }
754
+ catch (error) {
755
+ if (error instanceof publish_js_1.NoAccountError)
756
+ throw new UserError(error.message);
757
+ throw error;
758
+ }
759
+ console.log(`\nPanel publicado.\n`);
760
+ console.log(` Enlace: ${result.url}\n`);
761
+ if (result.adopted) {
762
+ console.log(` Este panel ya existía y queda vinculado a tu cuenta desde ahora.\n`);
763
+ }
764
+ if (clientes?.length) {
765
+ console.log(` Compartido con: ${clientes.join(", ")}`);
766
+ console.log(` Al entrar en getestela.dev/app con ese correo, lo verán ahí.\n`);
767
+ }
768
+ console.log(` El token es lo único que protege la página: quien tenga el enlace, entra.`);
769
+ console.log(` Al republicar, pasa --token ${result.token} o el cliente perderá su enlace.`);
770
+ }
771
+ finally {
772
+ db.close();
773
+ }
774
+ }
775
+ /**
776
+ * `estela sync enable --project <id>` — opt-in explícito, proyecto por
777
+ * proyecto. Sin esta fila, ese proyecto no sale de esta máquina nunca, sea
778
+ * cual sea el plan de la cuenta.
779
+ */
780
+ function cmdSyncEnable(args, dbPath) {
781
+ const db = (0, schema_js_1.openDatabase)(dbPath);
782
+ try {
783
+ const projectId = required(args, "project");
784
+ if (!store.getProject(db, projectId)) {
704
785
  throw new UserError(`No existe el proyecto "${projectId}".`);
705
- const client = store.getClient(db, project.clientId);
706
- const rates = store.getRates(db, projectId);
707
- // Reutilizar el token si se republica: el cliente ya tiene ese enlace
708
- // guardado y cambiárselo sin avisar le rompe el marcador.
709
- const token = str(args, "token") ?? (0, panel_js_1.newPanelToken)();
710
- // Las métricas necesitan los commits del proyecto y saber qué ramas están
711
- // integradas. Sin lo segundo no se puede distinguir entregado de en curso.
712
- const projectCommits = commitsOfProject(db, project.repoPaths);
713
- const merged = project.repoPaths[0]
714
- ? await (0, git_js_1.mergedBranches)(project.repoPaths[0]) : null;
715
- // El equipo del proyecto. Se enseña porque el repositorio es del cliente y
716
- // esos nombres ya los tiene en su git; lo que no sale de aquí son las horas
717
- // ajenas, que se quitan ANTES de construir el fichero. Ocultarlas al pintar
718
- // dejaría el dato en el código fuente, que es no ocultarlas.
719
- const myEntries = store.getTimeEntries(db, projectId);
720
- const mineByRepo = await (0, identity_js_1.myEmailsByRepo)(db, project.repoPaths);
721
- const myEmails = new Set();
722
- for (const set of mineByRepo.values())
723
- for (const email of set)
724
- myEmails.add(email);
725
- // Acotado al periodo que cubre el panel. Sin esto se compara el historial
726
- // entero del repositorio con las semanas que llevas tú: salían 523 commits
727
- // de un compañero frente a 11 tuyos, y el panel que venía a respaldar tu
728
- // trabajo te dejaba a la altura del betún. Además mete gente que ya no está
729
- // en el proyecto.
730
- const worked = myEntries.map((e) => (0, shared_1.localDate)(e.startedAt)).sort((a, b) => a.localeCompare(b));
731
- const from = worked[0] ?? "0000-01-01";
732
- const to = worked[worked.length - 1] ?? "9999-12-31";
733
- const inPeriod = projectCommits.filter((c) => {
734
- const day = (0, shared_1.localDate)(c.at);
735
- return day >= from && day <= to;
736
- });
737
- const team = (0, team_js_1.teamView)(inPeriod, { myEmails, myEntries }).map((p) => ({
738
- name: p.name,
739
- isMe: p.isMe,
740
- ...(p.isMe ? { seconds: p.seconds } : {}),
741
- commits: p.commits,
742
- branches: p.branches.length,
743
- lastDay: p.lastDay,
744
- }));
745
- const html = (0, panel_js_1.buildPanel)({
746
- project, client,
747
- entries: myEntries,
748
- ...(args.flags["no-team"] === true ? {} : { team }),
749
- ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
750
- withAmounts: args.flags["with-amounts"] === true,
751
- rateAt: (at) => (0, invoice_js_1.rateAt)(rates, projectId, at),
752
- aiPayer: "self",
753
- commits: projectCommits,
754
- ...(merged ? { merged } : {}),
755
- commitsOf: (hashes) => hashes.length
756
- ? db.prepare(`SELECT hash, subject FROM commits WHERE hash IN (${hashes.map(() => "?").join(",")})`).all(...hashes)
757
- : [],
786
+ }
787
+ if (!store.listPersonalSyncCandidates(db).includes(projectId)) {
788
+ throw new UserError(`"${projectId}" ya está enlazado a un equipo y no puede sincronizarse también como personal.`);
789
+ }
790
+ store.setProjectSync(db, {
791
+ projectId, scope: "personal", remoteProjectId: projectId,
792
+ remoteOrgId: null, inviteToken: null,
758
793
  });
759
- // Alojarlo exige sesión: es la cuenta la que decide cuántos paneles caben
760
- // en tu plan, y sin ella no hay a quién cargarle esa cuota.
794
+ console.log(`Sync activado para "${projectId}". Ejecuta "estela sync" para subir y bajar.`);
795
+ }
796
+ finally {
797
+ db.close();
798
+ }
799
+ }
800
+ /** `estela sync [--project <id>]` — sin --project, sincroniza todos los proyectos habilitados. */
801
+ async function cmdSync(args, dbPath) {
802
+ const db = (0, schema_js_1.openDatabase)(dbPath);
803
+ try {
804
+ const only = str(args, "project");
805
+ const projectIds = only ? [only] : store.listProjectSyncs(db).map((s) => s.projectId);
806
+ if (projectIds.length === 0) {
807
+ console.log(`Ningún proyecto sincroniza todavía. Actívalo con: estela sync enable --project <id>`);
808
+ return;
809
+ }
810
+ for (const projectId of projectIds) {
811
+ const project = store.getProject(db, projectId);
812
+ const label = project?.name ?? projectId;
813
+ const scope = store.getProjectSync(db, projectId)?.scope;
814
+ try {
815
+ if (scope === "team") {
816
+ const outcome = await (0, team_js_1.syncTeamProject)(db, projectId);
817
+ console.log(`${label}: ${(0, shared_1.formatDuration)(outcome.seconds)} totales enviadas`);
818
+ }
819
+ else {
820
+ const outcome = await (0, sync_js_1.syncProject)(db, projectId);
821
+ console.log(`${label}: ${outcome.pushed} subidas, ${outcome.pulled} bajadas`);
822
+ }
823
+ }
824
+ catch (error) {
825
+ if (error instanceof publish_js_1.NoAccountError || error instanceof sync_js_1.NotSyncedError)
826
+ throw new UserError(error.message);
827
+ throw error;
828
+ }
829
+ }
830
+ }
831
+ finally {
832
+ db.close();
833
+ }
834
+ }
835
+ /**
836
+ * `estela team invite --project <id> --email <a@b,c@d> [--kind employee|freelancer]`
837
+ *
838
+ * Los correos son los de commit de quien se invita, no con los que se vaya a
839
+ * loguear en Estela — es el mismo criterio que ya usa "estela author" para
840
+ * la propia identidad, y es lo que permite emparejarlo en el panel.
841
+ */
842
+ async function cmdTeamInvite(args, dbPath) {
843
+ const db = (0, schema_js_1.openDatabase)(dbPath);
844
+ try {
845
+ const projectId = required(args, "project");
846
+ const emails = required(args, "email").split(",").map((e) => e.trim()).filter(Boolean);
847
+ const kind = (str(args, "kind") ?? "employee");
848
+ if (kind !== "employee" && kind !== "freelancer") {
849
+ throw new UserError(`--kind debe ser "employee" o "freelancer".`);
850
+ }
851
+ let result;
852
+ try {
853
+ result = await (0, team_js_2.inviteTeamMember)(db, { projectId, emails, inviteeKind: kind });
854
+ }
855
+ catch (error) {
856
+ if (error instanceof publish_js_1.NoAccountError)
857
+ throw new UserError(error.message);
858
+ throw error;
859
+ }
860
+ console.log(`\nInvitación creada para: ${emails.join(", ")}\n`);
861
+ console.log(` Pásale esto (por el canal que uses con él, no hace falta correo):\n`);
862
+ console.log(` estela login --email <su-correo>`);
863
+ console.log(` estela team accept --token ${result.token} --as-id <id-que-elija>\n`);
864
+ }
865
+ finally {
866
+ db.close();
867
+ }
868
+ }
869
+ /**
870
+ * `estela team accept --token <token> --as-id <id-local>`
871
+ *
872
+ * El id local es obligatorio y nunca se autogenera: reusar el id del dueño
873
+ * tal cual podría pisar en silencio un proyecto propio que ya tuviera ese
874
+ * mismo id.
875
+ */
876
+ async function cmdTeamAccept(args, dbPath) {
877
+ const db = (0, schema_js_1.openDatabase)(dbPath);
878
+ try {
879
+ const token = required(args, "token");
880
+ const localProjectId = required(args, "as-id");
881
+ let result;
882
+ try {
883
+ result = await (0, team_js_2.acceptTeamInvite)(db, { token, localProjectId });
884
+ }
885
+ catch (error) {
886
+ if (error instanceof publish_js_1.NoAccountError)
887
+ throw new UserError(error.message);
888
+ throw error;
889
+ }
890
+ console.log(`\nInvitación aceptada: "${result.projectName}"`);
891
+ console.log(` Declarada para: ${result.emails.join(", ")} (${result.inviteeKind})`);
892
+ console.log(` Si no eres tú, avisa a quien te invitó.\n`);
893
+ console.log(`Ahora vincula tu repositorio local y sincroniza:\n`);
894
+ console.log(` estela team repo --project ${localProjectId} --add <ruta-de-tu-clon>`);
895
+ console.log(` estela import`);
896
+ console.log(` estela sync\n`);
897
+ }
898
+ finally {
899
+ db.close();
900
+ }
901
+ }
902
+ /** `estela team repo --project <id> --add <ruta>` — solo añade el repositorio, nada más. */
903
+ function cmdTeamRepo(args, dbPath) {
904
+ const db = (0, schema_js_1.openDatabase)(dbPath);
905
+ try {
906
+ const projectId = required(args, "project");
907
+ const repo = required(args, "add");
908
+ if (!store.getProject(db, projectId))
909
+ throw new UserError(`No existe el proyecto "${projectId}".`);
910
+ store.addProjectRepo(db, projectId, (0, node_path_1.resolve)(repo));
911
+ console.log(`Repositorio vinculado a "${projectId}": ${(0, node_path_1.resolve)(repo)}`);
912
+ }
913
+ finally {
914
+ db.close();
915
+ }
916
+ }
917
+ /** `estela team list --project <id>` — miembros aceptados, sus horas, y cuándo sincronizaron. */
918
+ async function cmdTeamList(args, dbPath) {
919
+ const db = (0, schema_js_1.openDatabase)(dbPath);
920
+ try {
921
+ const projectId = required(args, "project");
922
+ let members;
923
+ try {
924
+ members = await (0, team_js_2.listTeamMembers)(db, projectId);
925
+ }
926
+ catch (error) {
927
+ if (error instanceof publish_js_1.NoAccountError)
928
+ throw new UserError(error.message);
929
+ throw error;
930
+ }
931
+ if (members.length === 0) {
932
+ console.log(`Nadie ha aceptado todavía en "${projectId}".`);
933
+ return;
934
+ }
935
+ for (const m of members) {
936
+ const synced = m.syncedAt ? `sincronizado ${m.syncedAt.slice(0, 10)}` : "sin sincronizar aún";
937
+ console.log(` ${m.emails.join(", ")} (${m.inviteeKind}) · ${(0, shared_1.formatDuration)(m.seconds)} · ${synced}`);
938
+ }
939
+ }
940
+ finally {
941
+ db.close();
942
+ }
943
+ }
944
+ /** `estela team revoke --token <token>` — también baja la cantidad de la suscripción de Teams. */
945
+ async function cmdTeamRevoke(args, dbPath) {
946
+ const db = (0, schema_js_1.openDatabase)(dbPath);
947
+ try {
948
+ const token = required(args, "token");
949
+ try {
950
+ await (0, team_js_2.revokeTeamMember)(db, token);
951
+ }
952
+ catch (error) {
953
+ if (error instanceof publish_js_1.NoAccountError)
954
+ throw new UserError(error.message);
955
+ throw error;
956
+ }
957
+ console.log(`Invitación revocada.`);
958
+ }
959
+ finally {
960
+ db.close();
961
+ }
962
+ }
963
+ /** `estela upgrade --plan pro|teams` — imprime la URL de Checkout, no la abre. */
964
+ async function cmdUpgrade(args, dbPath) {
965
+ const db = (0, schema_js_1.openDatabase)(dbPath);
966
+ try {
967
+ const plan = required(args, "plan");
968
+ if (plan !== "pro" && plan !== "teams")
969
+ throw new UserError(`--plan debe ser "pro" o "teams".`);
970
+ let result;
971
+ try {
972
+ result = await (0, billing_js_1.upgradeCheckout)(db, plan);
973
+ }
974
+ catch (error) {
975
+ if (error instanceof publish_js_1.NoAccountError)
976
+ throw new UserError(error.message);
977
+ throw error;
978
+ }
979
+ console.log(`\nAbre esto para completar el alta a ${plan === "pro" ? "Pro" : "Teams"}:\n`);
980
+ console.log(` ${result.url}\n`);
981
+ }
982
+ finally {
983
+ db.close();
984
+ }
985
+ }
986
+ /** `estela billing portal` — cancelar o cambiar de tarjeta pasa en la página de Stripe. */
987
+ async function cmdBillingPortal(dbPath) {
988
+ const db = (0, schema_js_1.openDatabase)(dbPath);
989
+ try {
990
+ let result;
991
+ try {
992
+ result = await (0, billing_js_1.openBillingPortal)(db);
993
+ }
994
+ catch (error) {
995
+ if (error instanceof publish_js_1.NoAccountError)
996
+ throw new UserError(error.message);
997
+ throw error;
998
+ }
999
+ console.log(`\nGestiona tu pago aquí:\n\n ${result.url}\n`);
1000
+ }
1001
+ finally {
1002
+ db.close();
1003
+ }
1004
+ }
1005
+ /** `estela account` — primer consumidor real de GET /account. */
1006
+ async function cmdAccount(dbPath) {
1007
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1008
+ try {
761
1009
  const account = store.getCloudAccount(db);
762
1010
  if (!account) {
763
- throw new UserError(`Necesitas una cuenta para publicar. Vincúlala con:\n\n` +
1011
+ throw new UserError(`Esta máquina no está vinculada a ninguna cuenta. Vincúlala con:\n\n` +
764
1012
  ` estela login --email tu@correo.com\n`);
765
1013
  }
766
- let result;
1014
+ let info;
767
1015
  try {
768
- result = await (0, client_js_1.cloudPost)(account.apiBaseUrl, "/panels", { token, projectName: project.name, html }, { deviceToken: account.deviceToken });
1016
+ info = await (0, client_js_1.cloudGet)(account.apiBaseUrl, "/account", { deviceToken: account.deviceToken });
769
1017
  }
770
1018
  catch (error) {
771
- if (error instanceof client_js_1.CloudError && error.status === 402) {
772
- // El mensaje ya viene redactado desde el servidor (panels.ts): es la
773
- // única fuente de la verdad sobre cuántos paneles permite cada plan,
774
- // y duplicar ese texto aquí lo desincronizaría en cuanto cambiara allí.
775
- throw new UserError(error.message);
776
- }
777
1019
  if (error instanceof client_js_1.CloudError && error.status === 401) {
778
- throw new UserError(`Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n` +
779
- ` estela login --email tu@correo.com\n`);
1020
+ throw new UserError(`Tu sesión ya no vale. Vuelve a vincular la máquina:\n\n estela login --email tu@correo.com\n`);
780
1021
  }
781
1022
  throw error;
782
1023
  }
783
- store.recordPublication(db, projectId, token, "https://getestela.dev");
784
- console.log(`\nPanel publicado.\n`);
785
- console.log(` Enlace: ${result.url}\n`);
786
- if (result.adopted) {
787
- console.log(` Este panel ya existía y queda vinculado a tu cuenta desde ahora.\n`);
1024
+ console.log(`\n${info.email} · plan ${info.plan}\n`);
1025
+ for (const d of info.devices) {
1026
+ console.log(` ${d.label ?? "(sin nombre)"} · v${d.clientVersion ?? "?"} · visto ${d.lastSeenAt?.slice(0, 10) ?? "nunca"}`);
788
1027
  }
789
- console.log(` El token es lo único que protege la página: quien tenga el enlace, entra.`);
790
- console.log(` Al republicar, pasa --token ${token} o el cliente perderá su enlace.`);
791
1028
  }
792
1029
  finally {
793
1030
  db.close();
@@ -831,21 +1068,6 @@ async function cmdAuthor(args, dbPath) {
831
1068
  }
832
1069
  }
833
1070
  /** Commits de los repositorios de un proyecto, con sus ficheros. */
834
- function commitsOfProject(db, repoPaths) {
835
- if (repoPaths.length === 0)
836
- return [];
837
- const like = repoPaths.map(() => "repo_path LIKE ?").join(" OR ");
838
- const rows = db.prepare(`SELECT * FROM commits WHERE ${like}`)
839
- .all(...repoPaths.map((p) => `${p}%`));
840
- return rows.map((r) => ({
841
- repoPath: r["repo_path"], hash: r["hash"],
842
- at: new Date(r["at"]), authorEmail: r["author_email"],
843
- authorName: r["author_name"] ?? "",
844
- branch: r["branch"] ?? null, subject: r["subject"],
845
- linesAdded: r["lines_added"], linesDeleted: r["lines_deleted"],
846
- files: String(r["files"] ?? "").split("\n").filter(Boolean),
847
- }));
848
- }
849
1071
  /** Revisa los datos y avisa de lo que rompería una demo. */
850
1072
  function cmdDoctor(dbPath) {
851
1073
  const db = (0, schema_js_1.openDatabase)(dbPath);
@@ -1111,6 +1333,36 @@ async function main() {
1111
1333
  case "logout":
1112
1334
  await cmdLogout(dbPath);
1113
1335
  break;
1336
+ case "sync enable":
1337
+ cmdSyncEnable(args, dbPath);
1338
+ break;
1339
+ case "sync":
1340
+ await cmdSync(args, dbPath);
1341
+ break;
1342
+ case "team invite":
1343
+ await cmdTeamInvite(args, dbPath);
1344
+ break;
1345
+ case "team accept":
1346
+ await cmdTeamAccept(args, dbPath);
1347
+ break;
1348
+ case "team repo":
1349
+ cmdTeamRepo(args, dbPath);
1350
+ break;
1351
+ case "team list":
1352
+ await cmdTeamList(args, dbPath);
1353
+ break;
1354
+ case "team revoke":
1355
+ await cmdTeamRevoke(args, dbPath);
1356
+ break;
1357
+ case "upgrade":
1358
+ await cmdUpgrade(args, dbPath);
1359
+ break;
1360
+ case "billing portal":
1361
+ await cmdBillingPortal(dbPath);
1362
+ break;
1363
+ case "account":
1364
+ await cmdAccount(dbPath);
1365
+ break;
1114
1366
  case "":
1115
1367
  case "help":
1116
1368
  console.log(HELP);