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.
- package/dist/billing/sessionize.d.ts +10 -1
- package/dist/billing/sessionize.d.ts.map +1 -1
- package/dist/billing/sessionize.js +0 -0
- package/dist/billing/sessionize.js.map +1 -1
- package/dist/billing/sessionize.test.js +28 -0
- package/dist/billing/sessionize.test.js.map +1 -1
- package/dist/billing.d.ts +16 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +71 -0
- package/dist/billing.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +345 -93
- package/dist/cli.js.map +1 -1
- package/dist/cloud/client.d.ts +6 -0
- package/dist/cloud/client.d.ts.map +1 -1
- package/dist/cloud/client.js +26 -0
- package/dist/cloud/client.js.map +1 -1
- package/dist/db/store.d.ts +56 -0
- package/dist/db/store.d.ts.map +1 -1
- package/dist/db/store.js +101 -0
- package/dist/db/store.js.map +1 -1
- package/dist/export/panel.d.ts +3 -1
- package/dist/export/panel.d.ts.map +1 -1
- package/dist/export/panel.js +24 -19
- package/dist/export/panel.js.map +1 -1
- package/dist/export/panel.test.js +46 -5
- package/dist/export/panel.test.js.map +1 -1
- package/dist/metrics/team.d.ts +9 -0
- package/dist/metrics/team.d.ts.map +1 -1
- package/dist/metrics/team.js +0 -0
- package/dist/metrics/team.js.map +1 -1
- package/dist/publish.d.ts +37 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +185 -0
- package/dist/publish.js.map +1 -0
- package/dist/publish.test.d.ts +2 -0
- package/dist/publish.test.d.ts.map +1 -0
- package/dist/publish.test.js +91 -0
- package/dist/publish.test.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +88 -58
- package/dist/server.js.map +1 -1
- package/dist/sync/team.d.ts +21 -0
- package/dist/sync/team.d.ts.map +1 -0
- package/dist/sync/team.js +68 -0
- package/dist/sync/team.js.map +1 -0
- package/dist/sync/team.test.d.ts +2 -0
- package/dist/sync/team.test.d.ts.map +1 -0
- package/dist/sync/team.test.js +102 -0
- package/dist/sync/team.test.js.map +1 -0
- package/dist/sync.d.ts +20 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +79 -0
- package/dist/sync.js.map +1 -0
- package/dist/sync.test.d.ts +2 -0
- package/dist/sync.test.d.ts.map +1 -0
- package/dist/sync.test.js +151 -0
- package/dist/sync.test.js.map +1 -0
- package/dist/team.d.ts +51 -0
- package/dist/team.d.ts.map +1 -0
- package/dist/team.js +122 -0
- package/dist/team.js.map +1 -0
- package/dist/team.test.d.ts +2 -0
- package/dist/team.test.d.ts.map +1 -0
- package/dist/team.test.js +163 -0
- package/dist/team.test.js.map +1 -0
- package/dist/web-ui.test.js +14 -1
- package/dist/web-ui.test.js.map +1 -1
- package/package.json +1 -1
- package/web/app.css +74 -16
- package/web/app.js +198 -38
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.png +0 -0
- package/web/index.html +18 -2
- package/web/logo.png +0 -0
- 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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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(`
|
|
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
|
|
1014
|
+
let info;
|
|
767
1015
|
try {
|
|
768
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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);
|