estela 0.1.3 → 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.
- 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 +330 -92
- 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 +28 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +184 -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 +83 -57
- 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 +187 -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
|
@@ -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"}
|
package/dist/billing.js
ADDED
|
@@ -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":";
|
|
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,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]
|
|
@@ -699,95 +731,286 @@ async function cmdPublish(args, dbPath) {
|
|
|
699
731
|
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
700
732
|
try {
|
|
701
733
|
const projectId = required(args, "project");
|
|
702
|
-
|
|
703
|
-
|
|
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)) {
|
|
704
771
|
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
|
-
: [],
|
|
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,
|
|
758
779
|
});
|
|
759
|
-
|
|
760
|
-
|
|
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 {
|
|
761
995
|
const account = store.getCloudAccount(db);
|
|
762
996
|
if (!account) {
|
|
763
|
-
throw new UserError(`
|
|
997
|
+
throw new UserError(`Esta máquina no está vinculada a ninguna cuenta. Vincúlala con:\n\n` +
|
|
764
998
|
` estela login --email tu@correo.com\n`);
|
|
765
999
|
}
|
|
766
|
-
let
|
|
1000
|
+
let info;
|
|
767
1001
|
try {
|
|
768
|
-
|
|
1002
|
+
info = await (0, client_js_1.cloudGet)(account.apiBaseUrl, "/account", { deviceToken: account.deviceToken });
|
|
769
1003
|
}
|
|
770
1004
|
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
1005
|
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`);
|
|
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`);
|
|
780
1007
|
}
|
|
781
1008
|
throw error;
|
|
782
1009
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (result.adopted) {
|
|
787
|
-
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"}`);
|
|
788
1013
|
}
|
|
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
1014
|
}
|
|
792
1015
|
finally {
|
|
793
1016
|
db.close();
|
|
@@ -831,21 +1054,6 @@ async function cmdAuthor(args, dbPath) {
|
|
|
831
1054
|
}
|
|
832
1055
|
}
|
|
833
1056
|
/** 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
1057
|
/** Revisa los datos y avisa de lo que rompería una demo. */
|
|
850
1058
|
function cmdDoctor(dbPath) {
|
|
851
1059
|
const db = (0, schema_js_1.openDatabase)(dbPath);
|
|
@@ -1111,6 +1319,36 @@ async function main() {
|
|
|
1111
1319
|
case "logout":
|
|
1112
1320
|
await cmdLogout(dbPath);
|
|
1113
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;
|
|
1114
1352
|
case "":
|
|
1115
1353
|
case "help":
|
|
1116
1354
|
console.log(HELP);
|