estela 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -197,6 +197,11 @@ and Teams have no limit.
197
197
  The dashboard's footer says "Hours filled in by Estela", linking to
198
198
  getestela.dev. On Pro and Teams, `--no-credit` removes that line.
199
199
 
200
+ Before uploading, `estela publish` shows what will become public — authors,
201
+ branches, file paths, client emails — and asks you to confirm (`--yes` skips
202
+ it). `estela unpublish <project>` deletes the dashboard from the server and the
203
+ link stops working; `--all` takes them all down.
204
+
200
205
  ## Showing what you built
201
206
 
202
207
  A badge for your project's README, with its measured hours and what the AI
@@ -301,6 +306,16 @@ won't overwrite it: `--force` replaces it, `--append` shows yours after
301
306
  Estela's. `estela statusline uninstall` removes it (and puts yours back if
302
307
  you used `--append`).
303
308
 
309
+ The status line only runs in Claude Code in the terminal: the VS Code
310
+ extension doesn't show it.
311
+
312
+ Since 0.2.4, the status line stores your 5-hour and weekly percentages locally
313
+ (`~/.estela/rate-limits.jsonl`) so `estela plan` can analyse them. Nothing leaves
314
+ your machine. Only what Claude Code sends is kept — each percentage, its reset
315
+ time and the Claude Code version — at most every 5 minutes unless a figure moves,
316
+ and for 120 days. Transcripts don't carry these percentages, so the history
317
+ starts the day you install 0.2.4.
318
+
304
319
  ## Hours no import will infer
305
320
 
306
321
  Meetings, travel, research, and development without an agent that didn't leave
@@ -319,6 +334,24 @@ from them and marks it as **estimated**, so you know which part of your hours is
319
334
  measured and which is assumed. The estimate errs on the short side on purpose:
320
335
  these hours end up in a report someone pays for.
321
336
 
337
+ ## Privacy
338
+
339
+ On the free plan, your sessions, your code and your hours never leave your
340
+ machine: everything lives in `~/.estela/estela.db`, and the `estela web`
341
+ dashboard only listens on `127.0.0.1`. Estela never stores the text of your
342
+ prompts, not even locally.
343
+
344
+ The only network connection without an account is a check against npm, at most
345
+ once a day, for a new version. Two environment variables:
346
+
347
+ | Variable | Effect |
348
+ |---|---|
349
+ | `ESTELA_NO_UPDATE_CHECK=1` | Skips the version check (it's also skipped when `CI` is set). |
350
+ | `ESTELA_OFFLINE=1` | No network connections at all: no version check, no banner, no account. |
351
+
352
+ With an account, only what you turn on is uploaded — a dashboard, sync, Teams.
353
+ What each feature sends, one by one: [getestela.dev/en/privacy](https://getestela.dev/en/privacy).
354
+
322
355
  ## Plans
323
356
 
324
357
  | | Free | Pro | Teams |
package/dist/cli.js CHANGED
@@ -203,6 +203,11 @@ estela — registro de horas para desarrollo asistido por IA
203
203
  republicaciones no desvinculan a quien ya lo tenía.
204
204
  --no-credit quita la línea "Horas rellenadas por Estela" del pie
205
205
  (Pro y Teams).
206
+ Antes de subir enseña lo que va a quedar público (autores, ramas,
207
+ ficheros, correos del cliente) y pide confirmación; --yes la salta.
208
+ estela unpublish [<proyecto|token>] [--all] [--yes]
209
+ Borra el panel del servidor: el enlace deja de funcionar. Sin
210
+ argumentos, lista tus paneles publicados.
206
211
 
207
212
  estela team ai-cost --project <id> --allow | --deny
208
213
  Si quien te invitó pide ver el coste de IA del proyecto, esta es tu
@@ -270,6 +275,12 @@ Opciones globales: --db <ruta> (por defecto ${schema_js_1.DEFAULT_DB_PATH})
270
275
  --lang es|en Idioma. Sin ella se usa el del sistema;
271
276
  ESTELA_LANG=en lo fija para siempre.
272
277
  --version Solo imprime la versión instalada.
278
+
279
+ Red: Sin cuenta, Estela solo consulta npm una vez al día por si hay
280
+ versión nueva.
281
+ ESTELA_NO_UPDATE_CHECK=1 no la consulta (tampoco con CI definida).
282
+ ESTELA_OFFLINE=1 ninguna conexión de red: ni versión, ni
283
+ banner, ni nada de la cuenta.
273
284
  `;
274
285
  const HELP_EN = `
275
286
  estela — time tracking for AI-assisted development
@@ -394,6 +405,11 @@ estela — time tracking for AI-assisted development
394
405
  isn't touched: republishing never unlinks anyone who already had it.
395
406
  --no-credit removes the "Hours filled in by Estela" line from the
396
407
  footer (Pro and Teams).
408
+ Before uploading it shows what will become public (authors,
409
+ branches, files, client emails) and asks to confirm; --yes skips it.
410
+ estela unpublish [<project|token>] [--all] [--yes]
411
+ Deletes the dashboard from the server: the link stops working.
412
+ With no arguments, lists your published dashboards.
397
413
 
398
414
  estela team ai-cost --project <id> --allow | --deny
399
415
  If whoever invited you asks to see the project's AI cost, this is
@@ -461,6 +477,12 @@ Global options: --db <path> (default ${schema_js_1.DEFAULT_DB_PATH})
461
477
  --lang es|en Language. Without it, your system's is used;
462
478
  ESTELA_LANG=en sets it for good.
463
479
  --version Only prints the installed version.
480
+
481
+ Network: Without an account, Estela only checks npm once a day for a new
482
+ version.
483
+ ESTELA_NO_UPDATE_CHECK=1 skips that check (so does having CI set).
484
+ ESTELA_OFFLINE=1 no network connections at all: no
485
+ version check, no banner, no account.
464
486
  `;
465
487
  function helpText() {
466
488
  return (0, index_js_1.getLang)() === "en" ? HELP_EN : HELP_ES;
@@ -1398,7 +1420,7 @@ async function cmdPublish(args, dbPath) {
1398
1420
  const token = str(args, "token") ?? store.getPublicationToken(db, projectId) ?? undefined;
1399
1421
  let result;
1400
1422
  try {
1401
- result = await (0, publish_js_1.publishPanel)(db, {
1423
+ const panel = await (0, publish_js_1.preparePanel)(db, {
1402
1424
  projectId,
1403
1425
  ...(token ? { token } : {}),
1404
1426
  ...(str(args, "author") ? { authorName: str(args, "author") } : {}),
@@ -1408,6 +1430,19 @@ async function cmdPublish(args, dbPath) {
1408
1430
  hideCredit: args.flags["no-credit"] === true,
1409
1431
  ...(explicitLang(args) ? { language: explicitLang(args) } : {}),
1410
1432
  });
1433
+ // Lo que va a quedar público, antes de subirlo: quien tenga el enlace
1434
+ // verá todo esto, y publicar no se deshace solo.
1435
+ printPanelSummary(panel.summary);
1436
+ if (args.flags["yes"] !== true) {
1437
+ if (!process.stdin.isTTY) {
1438
+ throw new UserError((0, index_js_1.tr) `No se ha publicado nada. Sin terminal no se puede confirmar: añade --yes para publicar.`);
1439
+ }
1440
+ if (!await confirm((0, index_js_1.tr) `¿Publicar? [s/N] `)) {
1441
+ console.log((0, index_js_1.tr) `\nNo se ha publicado nada.`);
1442
+ return;
1443
+ }
1444
+ }
1445
+ result = await (0, publish_js_1.uploadPanel)(db, panel);
1411
1446
  }
1412
1447
  catch (error) {
1413
1448
  if (error instanceof publish_js_1.NoAccountError)
@@ -1433,6 +1468,82 @@ async function cmdPublish(args, dbPath) {
1433
1468
  db.close();
1434
1469
  }
1435
1470
  }
1471
+ /** Hasta `max` elementos de una lista, y cuántos quedan fuera. */
1472
+ function shortList(items, max = 8) {
1473
+ if (items.length === 0)
1474
+ return "—";
1475
+ const shown = items.slice(0, max).join(", ");
1476
+ return items.length > max ? (0, index_js_1.tr) `${shown} y ${items.length - max} más` : shown;
1477
+ }
1478
+ function printPanelSummary(s) {
1479
+ console.log((0, index_js_1.tr) `\nEsto va a quedar público. Quien tenga el enlace lo ve todo:\n`);
1480
+ const row = (label, value) => console.log(` ${label.padEnd(14)}${value}`);
1481
+ row((0, index_js_1.tr) `Proyecto`, `${s.project} · ${s.client}`);
1482
+ row((0, index_js_1.tr) `Trabajo`, (0, index_js_1.tr) `${s.days} días, ${s.blocks} bloques, ${s.commitSubjects} mensajes de commit`);
1483
+ row((0, index_js_1.tr) `Autores`, shortList(s.authors));
1484
+ row((0, index_js_1.tr) `Ramas`, shortList(s.branches));
1485
+ row((0, index_js_1.tr) `Ficheros`, shortList(s.files));
1486
+ row((0, index_js_1.tr) `Importes`, s.amounts ? (0, index_js_1.tr) `sí (--with-amounts)` : (0, index_js_1.tr) `no`);
1487
+ row((0, index_js_1.tr) `Clientes`, s.clients.length ? (0, index_js_1.tr) `${s.clients.join(", ")} (recibirán un correo)` : "—");
1488
+ console.log("");
1489
+ }
1490
+ /**
1491
+ * `estela unpublish <proyecto|token>` y `estela unpublish --all`.
1492
+ *
1493
+ * Borra el panel del servidor: el enlace deja de funcionar para todos al
1494
+ * momento. Sin argumentos, lista lo que tienes publicado.
1495
+ */
1496
+ async function cmdUnpublish(args, dbPath) {
1497
+ const db = (0, schema_js_1.openDatabase)(dbPath);
1498
+ try {
1499
+ const target = args._[1] ?? str(args, "project") ?? str(args, "token");
1500
+ try {
1501
+ if (args.flags["all"] === true) {
1502
+ const panels = await (0, publish_js_1.listPublishedPanels)(db);
1503
+ if (panels.length === 0) {
1504
+ console.log((0, index_js_1.tr) `No tienes ningún panel publicado.`);
1505
+ return;
1506
+ }
1507
+ for (const p of panels)
1508
+ console.log(` ${p.projectName} · ${p.token}`);
1509
+ if (args.flags["yes"] !== true) {
1510
+ if (!process.stdin.isTTY) {
1511
+ throw new UserError((0, index_js_1.tr) `No se ha despublicado nada. Sin terminal no se puede confirmar: añade --yes.`);
1512
+ }
1513
+ if (!await confirm((0, index_js_1.tr) `\n¿Despublicar estos ${panels.length} paneles? Los enlaces dejarán de funcionar. [s/N] `)) {
1514
+ console.log((0, index_js_1.tr) `\nNo se ha despublicado nada.`);
1515
+ return;
1516
+ }
1517
+ }
1518
+ const gone = await (0, publish_js_1.unpublishAll)(db);
1519
+ console.log((0, index_js_1.tr) `\n${gone.length} paneles despublicados. Sus enlaces ya no funcionan.`);
1520
+ return;
1521
+ }
1522
+ if (!target) {
1523
+ const panels = await (0, publish_js_1.listPublishedPanels)(db);
1524
+ if (panels.length === 0) {
1525
+ console.log((0, index_js_1.tr) `No tienes ningún panel publicado.`);
1526
+ return;
1527
+ }
1528
+ console.log((0, index_js_1.tr) `Tus paneles publicados:\n`);
1529
+ for (const p of panels)
1530
+ console.log(` ${p.projectId ?? "—"} ${p.projectName} · ${p.token}`);
1531
+ console.log((0, index_js_1.tr) `\nPara despublicar uno: estela unpublish <proyecto o token>. Todos: estela unpublish --all`);
1532
+ return;
1533
+ }
1534
+ const gone = await (0, publish_js_1.unpublishPanel)(db, target);
1535
+ console.log((0, index_js_1.tr) `Panel de ${gone.projectName} despublicado. El enlace ya no funciona.`);
1536
+ }
1537
+ catch (error) {
1538
+ if (error instanceof publish_js_1.NoAccountError)
1539
+ throw new UserError(error.message);
1540
+ throw error;
1541
+ }
1542
+ }
1543
+ finally {
1544
+ db.close();
1545
+ }
1546
+ }
1436
1547
  /**
1437
1548
  * `estela sync enable --project <id>` — opt-in explícito, proyecto por
1438
1549
  * proyecto. Sin esta fila, ese proyecto no sale de esta máquina nunca, sea
@@ -2023,6 +2134,7 @@ function cmdStatuslineInstall(args) {
2023
2134
  }
2024
2135
  if (plan.status === "already") {
2025
2136
  console.log((0, index_js_1.tr) `La status line de Estela ya está instalada en ${path}.`);
2137
+ console.log((0, index_js_1.tr) `Solo en Claude Code en la terminal: la extensión de VS Code no enseña la status line.`);
2026
2138
  return;
2027
2139
  }
2028
2140
  if (plan.status === "conflict") {
@@ -2047,6 +2159,9 @@ function cmdStatuslineInstall(args) {
2047
2159
  ? (0, index_js_1.tr) `Status line de Estela instalada en lugar de: ${plan.previous ?? ""}`
2048
2160
  : (0, index_js_1.tr) `Status line de Estela instalada en ${path}.`);
2049
2161
  console.log((0, index_js_1.tr) `Se verá en la próxima sesión de Claude Code (o al reiniciar la actual).`);
2162
+ // Comprobado: la extensión de VS Code no ejecuta la status line. Sin este
2163
+ // aviso, quien trabaja ahí cree que está instalada y no la ve nunca.
2164
+ console.log((0, index_js_1.tr) `Solo en Claude Code en la terminal: la extensión de VS Code no enseña la status line.`);
2050
2165
  if (!(0, statusline_install_js_1.estelaOnPath)()) {
2051
2166
  console.log((0, index_js_1.tr) `\nAviso: "estela" no está en tu PATH, así que Claude Code no podrá ejecutarla.`);
2052
2167
  console.log((0, index_js_1.tr) `Instálala de forma global: npm install -g estela`);
@@ -2350,7 +2465,8 @@ async function main() {
2350
2465
  }
2351
2466
  (0, client_js_1.setClientVersion)(VERSION);
2352
2467
  const dbPath = str(args, "db") ?? schema_js_1.DEFAULT_DB_PATH;
2353
- const command = args._.join(" ");
2468
+ // `estela unpublish <panel>` lleva el panel suelto, sin flag: no es un subcomando.
2469
+ const command = args._[0] === "unpublish" ? "unpublish" : args._.join(" ");
2354
2470
  // Se llama aquí, no al final: un comando que falla a medias (por ejemplo
2355
2471
  // "estela import" sin repositorios) debe avisar igual de que hay una
2356
2472
  // versión nueva, no solo los que terminan bien.
@@ -2434,6 +2550,9 @@ async function main() {
2434
2550
  case "publish":
2435
2551
  await cmdPublish(args, dbPath);
2436
2552
  break;
2553
+ case "unpublish":
2554
+ await cmdUnpublish(args, dbPath);
2555
+ break;
2437
2556
  case "export":
2438
2557
  await cmdExport(args, dbPath);
2439
2558
  break;
@@ -22,4 +22,6 @@ export declare function cloudPost<T>(baseUrl: string, path: string, body: unknow
22
22
  * proyecto es la primera vez que hace falta leer algo.
23
23
  */
24
24
  export declare function cloudGet<T>(baseUrl: string, path: string, options?: CloudPostOptions): Promise<T>;
25
+ /** Para borrar lo que se subió: hoy, despublicar un panel. */
26
+ export declare function cloudDelete<T>(baseUrl: string, path: string, options?: CloudPostOptions): Promise<T>;
25
27
  //# sourceMappingURL=client.d.ts.map
@@ -12,7 +12,9 @@ exports.CloudError = void 0;
12
12
  exports.setClientVersion = setClientVersion;
13
13
  exports.cloudPost = cloudPost;
14
14
  exports.cloudGet = cloudGet;
15
+ exports.cloudDelete = cloudDelete;
15
16
  const index_js_1 = require("../i18n/index.js");
17
+ const offline_js_1 = require("../offline.js");
16
18
  class CloudError extends Error {
17
19
  status;
18
20
  constructor(message, status) {
@@ -29,32 +31,7 @@ exports.CloudError = CloudError;
29
31
  let clientVersion = "0.0.0";
30
32
  function setClientVersion(v) { clientVersion = v; }
31
33
  async function cloudPost(baseUrl, path, body, options = {}) {
32
- let res;
33
- try {
34
- res = await fetch(`${baseUrl}${path}`, {
35
- method: "POST",
36
- headers: {
37
- "content-type": "application/json",
38
- "estela-version": clientVersion,
39
- // Para que la API conteste en el idioma de la terminal.
40
- "x-estela-lang": (0, index_js_1.getLang)(),
41
- ...(options.deviceToken ? { authorization: `Bearer ${options.deviceToken}` } : {}),
42
- },
43
- body: JSON.stringify(body),
44
- });
45
- }
46
- catch {
47
- throw new CloudError((0, index_js_1.tr) `Sin respuesta de ${baseUrl}. Puede ser tu conexión o que el servicio esté caído; ` +
48
- (0, index_js_1.tr) `en ambos casos, nada de lo tuyo se ha perdido: vuelve a intentarlo.`);
49
- }
50
- const data = (await res.json().catch(() => ({})));
51
- if (!res.ok) {
52
- // El status viaja aparte del mensaje: quien llama necesita distinguir
53
- // "no tienes sesión" (401, pide login) de "cuota agotada" (402, pide
54
- // upgrade) sin tener que analizar el texto en español.
55
- throw new CloudError(typeof data["error"] === "string" ? data["error"] : (0, index_js_1.tr) `Error ${res.status}`, res.status);
56
- }
57
- return data;
34
+ return request("POST", baseUrl, path, body, options);
58
35
  }
59
36
  /**
60
37
  * Primer GET autenticado de verdad del CLI. Hasta Teams, todo lo que hablaba
@@ -62,16 +39,30 @@ async function cloudPost(baseUrl, path, body, options = {}) {
62
39
  * proyecto es la primera vez que hace falta leer algo.
63
40
  */
64
41
  async function cloudGet(baseUrl, path, options = {}) {
42
+ return request("GET", baseUrl, path, undefined, options);
43
+ }
44
+ /** Para borrar lo que se subió: hoy, despublicar un panel. */
45
+ async function cloudDelete(baseUrl, path, options = {}) {
46
+ return request("DELETE", baseUrl, path, undefined, options);
47
+ }
48
+ async function request(method, baseUrl, path, body, options) {
49
+ // Todas las llamadas a la cuenta pasan por aquí: con ESTELA_OFFLINE=1 no
50
+ // sale ninguna, y quien llama recibe un error que dice por qué.
51
+ if ((0, offline_js_1.isOffline)()) {
52
+ throw new CloudError((0, index_js_1.tr) `ESTELA_OFFLINE=1: Estela no se conecta a la red. Quita la variable para usar tu cuenta.`);
53
+ }
65
54
  let res;
66
55
  try {
67
56
  res = await fetch(`${baseUrl}${path}`, {
68
- method: "GET",
57
+ method,
69
58
  headers: {
59
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
70
60
  "estela-version": clientVersion,
71
61
  // Para que la API conteste en el idioma de la terminal.
72
62
  "x-estela-lang": (0, index_js_1.getLang)(),
73
63
  ...(options.deviceToken ? { authorization: `Bearer ${options.deviceToken}` } : {}),
74
64
  },
65
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
75
66
  });
76
67
  }
77
68
  catch {
@@ -80,6 +71,9 @@ async function cloudGet(baseUrl, path, options = {}) {
80
71
  }
81
72
  const data = (await res.json().catch(() => ({})));
82
73
  if (!res.ok) {
74
+ // El status viaja aparte del mensaje: quien llama necesita distinguir
75
+ // "no tienes sesión" (401, pide login) de "cuota agotada" (402, pide
76
+ // upgrade) sin tener que analizar el texto en español.
83
77
  throw new CloudError(typeof data["error"] === "string" ? data["error"] : (0, index_js_1.tr) `Error ${res.status}`, res.status);
84
78
  }
85
79
  return data;
@@ -62,6 +62,8 @@ export interface Publication {
62
62
  */
63
63
  export declare function getPublicationToken(db: DatabaseSync, projectId: string): string | null;
64
64
  export declare function recordPublication(db: DatabaseSync, projectId: string, token: string, baseUrl: string | null, at?: Date): void;
65
+ /** Tras despublicar: el proyecto deja de constar como publicado en esta máquina. */
66
+ export declare function forgetPublication(db: DatabaseSync, token: string): void;
65
67
  /**
66
68
  * Estado de las publicaciones.
67
69
  *
package/dist/db/store.js CHANGED
@@ -17,6 +17,7 @@ exports.getProjectAuthors = getProjectAuthors;
17
17
  exports.setProjectAuthors = setProjectAuthors;
18
18
  exports.getPublicationToken = getPublicationToken;
19
19
  exports.recordPublication = recordPublication;
20
+ exports.forgetPublication = forgetPublication;
20
21
  exports.listPublications = listPublications;
21
22
  exports.authorsByRepo = authorsByRepo;
22
23
  exports.projectForRepo = projectForRepo;
@@ -246,6 +247,10 @@ function recordPublication(db, projectId, token, baseUrl, at = new Date()) {
246
247
  base_url = COALESCE(excluded.base_url, publications.base_url)
247
248
  `).run(projectId, token, iso(at), baseUrl);
248
249
  }
250
+ /** Tras despublicar: el proyecto deja de constar como publicado en esta máquina. */
251
+ function forgetPublication(db, token) {
252
+ db.prepare("DELETE FROM publications WHERE token = ?").run(token);
253
+ }
249
254
  /**
250
255
  * Estado de las publicaciones.
251
256
  *
@@ -83,5 +83,74 @@ export interface PanelTeamMember {
83
83
  * porque una caducidad que comprueba el navegador no protege de nada.
84
84
  */
85
85
  export declare function newPanelToken(): string;
86
+ interface DayData {
87
+ date: string;
88
+ seconds: number;
89
+ items: {
90
+ what: string;
91
+ seconds: number;
92
+ kind: string;
93
+ amount: string | null;
94
+ commits: {
95
+ hash: string;
96
+ subject: string;
97
+ }[];
98
+ }[];
99
+ }
86
100
  export declare function buildPanel(options: PanelOptions): string;
101
+ /** Los datos que viajan dentro del panel: lo único que se publica de tu trabajo. */
102
+ export type PanelData = ReturnType<typeof panelData>;
103
+ /**
104
+ * Lo que va a quedar público, contado antes de subirlo.
105
+ *
106
+ * Sale de los mismos datos que se meten en el HTML, no de un cálculo aparte:
107
+ * un resumen que pudiera decir algo distinto de lo que se publica no
108
+ * protegería a nadie. `estela publish` lo enseña y pide confirmación.
109
+ */
110
+ export declare function summarizePanel(data: PanelData): {
111
+ project: string;
112
+ client: string;
113
+ days: number;
114
+ blocks: number;
115
+ commitSubjects: number;
116
+ authors: string[];
117
+ branches: string[];
118
+ files: string[];
119
+ amounts: boolean;
120
+ };
121
+ export declare function panelData(options: PanelOptions): {
122
+ project: string;
123
+ client: string;
124
+ author: string | null;
125
+ generatedAt: string;
126
+ totalSeconds: number;
127
+ totalAmount: string | null;
128
+ days: DayData[];
129
+ features: {
130
+ name: string;
131
+ seconds: number;
132
+ days: number;
133
+ merged: boolean | null;
134
+ delivered: {
135
+ hash: string;
136
+ subject: string;
137
+ }[];
138
+ moreDelivered: number;
139
+ }[];
140
+ rhythm: {
141
+ activeDays: number;
142
+ spanDays: number;
143
+ medianSeconds: number;
144
+ longestGapDays: number;
145
+ };
146
+ rework: {
147
+ file: string;
148
+ touches: number;
149
+ spanDays: number;
150
+ }[];
151
+ open: import("../metrics/index.js").OpenWork[];
152
+ team: readonly PanelTeamMember[];
153
+ site: string;
154
+ };
155
+ export {};
87
156
  //# sourceMappingURL=panel.d.ts.map
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CREDIT_MARKER = void 0;
4
4
  exports.newPanelToken = newPanelToken;
5
5
  exports.buildPanel = buildPanel;
6
+ exports.summarizePanel = summarizePanel;
7
+ exports.panelData = panelData;
6
8
  const node_crypto_1 = require("node:crypto");
7
9
  const shared_1 = require("@estela/shared");
8
10
  const localize_js_1 = require("../billing/localize.js");
@@ -28,6 +30,50 @@ function shortPath(file) {
28
30
  return file.split("/").slice(-2).join("/");
29
31
  }
30
32
  function buildPanel(options) {
33
+ const data = panelData(options);
34
+ // El texto de interfaz va aparte de los datos, en su propio bloque: los datos
35
+ // son lo único que hay que vigilar de cerca (aquí no entra nada de IA ni de
36
+ // costes, y hay tests que lo comprueban), y mezclarlos con frases sueltas
37
+ // haría esa comprobación imposible de hacer sin falsos positivos.
38
+ const lang = (0, index_js_1.getLang)();
39
+ const ui = {
40
+ lang, L: panelLabels(), monthNames: dates_js_1.MONTHS[lang], dayNames: dates_js_1.DAYS[lang],
41
+ credit: options.hideCredit !== true,
42
+ };
43
+ return render(data, ui);
44
+ }
45
+ /**
46
+ * Lo que va a quedar público, contado antes de subirlo.
47
+ *
48
+ * Sale de los mismos datos que se meten en el HTML, no de un cálculo aparte:
49
+ * un resumen que pudiera decir algo distinto de lo que se publica no
50
+ * protegería a nadie. `estela publish` lo enseña y pide confirmación.
51
+ */
52
+ function summarizePanel(data) {
53
+ const subjects = new Set();
54
+ const branches = new Set();
55
+ for (const day of data.days) {
56
+ for (const item of day.items)
57
+ for (const c of item.commits)
58
+ subjects.add(c.subject);
59
+ }
60
+ for (const f of data.features)
61
+ branches.add(f.name);
62
+ for (const o of data.open)
63
+ branches.add(o.name);
64
+ return {
65
+ project: data.project,
66
+ client: data.client,
67
+ days: data.days.length,
68
+ blocks: data.days.reduce((n, d) => n + d.items.length, 0),
69
+ commitSubjects: subjects.size,
70
+ authors: data.team.map((m) => m.name),
71
+ branches: [...branches].sort(),
72
+ files: data.rework.map((r) => r.file),
73
+ amounts: data.totalAmount !== null,
74
+ };
75
+ }
76
+ function panelData(options) {
31
77
  const { project, client, entries } = options;
32
78
  const showAmounts = options.withAmounts === true && options.rateAt !== undefined;
33
79
  const included = entries
@@ -104,16 +150,7 @@ function buildPanel(options) {
104
150
  team: options.team ?? [],
105
151
  site: (options.siteUrl ?? "https://getestela.dev").replace(/\/$/, ""),
106
152
  };
107
- // El texto de interfaz va aparte de los datos, en su propio bloque: los datos
108
- // son lo único que hay que vigilar de cerca (aquí no entra nada de IA ni de
109
- // costes, y hay tests que lo comprueban), y mezclarlos con frases sueltas
110
- // haría esa comprobación imposible de hacer sin falsos positivos.
111
- const lang = (0, index_js_1.getLang)();
112
- const ui = {
113
- lang, L: panelLabels(), monthNames: dates_js_1.MONTHS[lang], dayNames: dates_js_1.DAYS[lang],
114
- credit: options.hideCredit !== true,
115
- };
116
- return render(data, ui);
153
+ return data;
117
154
  }
118
155
  /**
119
156
  * Todo el texto fijo del panel, en el idioma en curso (`getLang()`).
package/dist/i18n/en.js CHANGED
@@ -550,5 +550,34 @@ exports.EN = {
550
550
  " {0} recuperadas de sesiones en subcarpetas y worktrees": " {0} recovered from sessions in subfolders and worktrees",
551
551
  "{0} (sin proyecto)": "{0} (no project)",
552
552
  "\n{0} de esos bloques son de subcarpetas o worktrees de un proyecto: estela import --yes los imputa.": "\n{0} of those blocks are from a project's subfolders or worktrees: estela import --yes assigns them.",
553
+ // ── publish: resumen antes de subir, y unpublish ──
554
+ "No se ha publicado nada. Sin terminal no se puede confirmar: añade --yes para publicar.": "Nothing was published. Without a terminal there's no way to confirm: add --yes to publish.",
555
+ "¿Publicar? [s/N] ": "Publish? [y/N] ",
556
+ "\nNo se ha publicado nada.": "\nNothing was published.",
557
+ "{0} y {1} más": "{0} and {1} more",
558
+ "\nEsto va a quedar público. Quien tenga el enlace lo ve todo:\n": "\nThis will become public. Anyone with the link sees all of it:\n",
559
+ "Trabajo": "Work",
560
+ "{0} días, {1} bloques, {2} mensajes de commit": "{0} days, {1} blocks, {2} commit messages",
561
+ "Autores": "Authors",
562
+ "Ramas": "Branches",
563
+ "Ficheros": "Files",
564
+ "Importes": "Amounts",
565
+ "sí (--with-amounts)": "yes (--with-amounts)",
566
+ "no": "no",
567
+ "Clientes": "Clients",
568
+ "{0} (recibirán un correo)": "{0} (they'll get an email)",
569
+ "No tienes ningún panel publicado.": "You have no published dashboards.",
570
+ "No se ha despublicado nada. Sin terminal no se puede confirmar: añade --yes.": "Nothing was unpublished. Without a terminal there's no way to confirm: add --yes.",
571
+ "\n¿Despublicar estos {0} paneles? Los enlaces dejarán de funcionar. [s/N] ": "\nUnpublish these {0} dashboards? The links will stop working. [y/N] ",
572
+ "\nNo se ha despublicado nada.": "\nNothing was unpublished.",
573
+ "\n{0} paneles despublicados. Sus enlaces ya no funcionan.": "\n{0} dashboards unpublished. Their links no longer work.",
574
+ "Tus paneles publicados:\n": "Your published dashboards:\n",
575
+ "\nPara despublicar uno: estela unpublish <proyecto o token>. Todos: estela unpublish --all": "\nTo unpublish one: estela unpublish <project or token>. All of them: estela unpublish --all",
576
+ "Panel de {0} despublicado. El enlace ya no funciona.": "{0} dashboard unpublished. The link no longer works.",
577
+ "No hay ningún panel publicado de \"{0}\". Míralos con: estela unpublish": "There's no published dashboard for \"{0}\". See them with: estela unpublish",
578
+ // ── ESTELA_OFFLINE ──
579
+ "ESTELA_OFFLINE=1: Estela no se conecta a la red. Quita la variable para usar tu cuenta.": "ESTELA_OFFLINE=1: Estela doesn't connect to the network. Unset it to use your account.",
580
+ // ── statusline install: VS Code ──
581
+ "Solo en Claude Code en la terminal: la extensión de VS Code no enseña la status line.": "Only in Claude Code in the terminal: the VS Code extension doesn't show the status line.",
553
582
  };
554
583
  //# sourceMappingURL=en.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `ESTELA_OFFLINE=1`: Estela no abre ninguna conexión de red. Ni la
3
+ * comprobación de versión, ni el banner del panel local, ni nada de la cuenta
4
+ * (login, sync, Teams, paneles, pagos). Todo lo local sigue igual.
5
+ *
6
+ * Es el interruptor general; `ESTELA_NO_UPDATE_CHECK=1` apaga solo la
7
+ * comprobación de versión.
8
+ */
9
+ export declare function isOffline(env?: NodeJS.ProcessEnv): boolean;
10
+ //# sourceMappingURL=offline.d.ts.map
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isOffline = isOffline;
4
+ /**
5
+ * `ESTELA_OFFLINE=1`: Estela no abre ninguna conexión de red. Ni la
6
+ * comprobación de versión, ni el banner del panel local, ni nada de la cuenta
7
+ * (login, sync, Teams, paneles, pagos). Todo lo local sigue igual.
8
+ *
9
+ * Es el interruptor general; `ESTELA_NO_UPDATE_CHECK=1` apaga solo la
10
+ * comprobación de versión.
11
+ */
12
+ function isOffline(env = process.env) {
13
+ const v = env["ESTELA_OFFLINE"];
14
+ return v !== undefined && v !== "" && v !== "0";
15
+ }
16
+ //# sourceMappingURL=offline.js.map
package/dist/publish.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
+ import { summarizePanel } from "./export/panel.js";
2
3
  import { type Lang } from "./i18n/index.js";
3
4
  /**
4
5
  * Publica un panel, alojado en la cuenta vinculada.
@@ -36,10 +37,47 @@ export interface PublishOptions {
36
37
  /** Sin la línea "Horas rellenadas por Estela" del pie. Solo Pro y Teams: el servidor lo comprueba. */
37
38
  readonly hideCredit?: boolean;
38
39
  }
40
+ /** Lo que va a quedar público al subir el panel, para enseñarlo antes. */
41
+ export type PanelSummary = ReturnType<typeof summarizePanel> & {
42
+ /** Correos del cliente que recibirán acceso y un aviso por correo. */
43
+ readonly clients: readonly string[];
44
+ };
45
+ /** Un panel ya construido, a falta de subirlo. */
46
+ export interface PreparedPanel {
47
+ readonly projectId: string;
48
+ readonly projectName: string;
49
+ readonly token: string;
50
+ readonly html: string;
51
+ readonly summary: PanelSummary;
52
+ readonly clients?: readonly string[];
53
+ readonly hideCredit: boolean;
54
+ }
39
55
  export interface PublishResult {
40
56
  readonly token: string;
41
57
  readonly url: string;
42
58
  readonly adopted: boolean;
43
59
  }
44
60
  export declare function publishPanel(db: DatabaseSync, options: PublishOptions): Promise<PublishResult>;
61
+ /**
62
+ * Construye el panel sin subirlo: el HTML y el resumen de lo que va a quedar
63
+ * público. `estela publish` enseña el resumen y pide confirmación antes de
64
+ * `uploadPanel`.
65
+ */
66
+ export declare function preparePanel(db: DatabaseSync, options: PublishOptions): Promise<PreparedPanel>;
67
+ export declare function uploadPanel(db: DatabaseSync, panel: PreparedPanel): Promise<PublishResult>;
68
+ /** Un panel publicado en tu cuenta, tal como lo lista el servidor. */
69
+ export interface PublishedPanel {
70
+ readonly token: string;
71
+ readonly projectName: string;
72
+ readonly projectId: string | null;
73
+ }
74
+ /**
75
+ * Despublica un panel: el servidor borra el fichero y el enlace deja de
76
+ * funcionar. `target` es el id de un proyecto (el panel que se publicó desde
77
+ * esta máquina) o el token del enlace.
78
+ */
79
+ export declare function unpublishPanel(db: DatabaseSync, target: string): Promise<PublishedPanel>;
80
+ /** Despublica todos los paneles de la cuenta. Devuelve los que quitó. */
81
+ export declare function unpublishAll(db: DatabaseSync): Promise<PublishedPanel[]>;
82
+ export declare function listPublishedPanels(db: DatabaseSync): Promise<PublishedPanel[]>;
45
83
  //# sourceMappingURL=publish.d.ts.map