@gscdump/cli 3.5.0 → 3.6.1

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 (57) hide show
  1. package/README.md +217 -54
  2. package/bin/gscdump.mjs +6 -1
  3. package/dist/analysis-local.mjs +1 -1
  4. package/dist/auth-state.mjs +112 -0
  5. package/dist/auth.mjs +109 -122
  6. package/dist/bing-auth.mjs +200 -0
  7. package/dist/bing-data.mjs +160 -0
  8. package/dist/bing-hosted.mjs +121 -0
  9. package/dist/cli.d.mts +4 -4
  10. package/dist/cli.mjs +20 -21
  11. package/dist/cloud-google.mjs +92 -0
  12. package/dist/command-meta.mjs +13 -1
  13. package/dist/command-registry.mjs +5 -2
  14. package/dist/commands/analyze.mjs +34 -177
  15. package/dist/commands/auth.mjs +161 -7
  16. package/dist/commands/bing.mjs +337 -0
  17. package/dist/commands/config.mjs +36 -43
  18. package/dist/commands/doctor.mjs +73 -24
  19. package/dist/commands/dump.mjs +1 -1
  20. package/dist/commands/entities.mjs +4 -4
  21. package/dist/commands/indexing.mjs +11 -14
  22. package/dist/commands/init.mjs +1 -1
  23. package/dist/commands/inspect.mjs +3 -3
  24. package/dist/commands/mcp.mjs +16 -2
  25. package/dist/commands/papercut.mjs +76 -0
  26. package/dist/commands/profile-selection.mjs +2 -2
  27. package/dist/commands/profile.mjs +8 -3
  28. package/dist/commands/query.mjs +86 -40
  29. package/dist/commands/report.mjs +5 -3
  30. package/dist/commands/sitemaps.mjs +24 -18
  31. package/dist/commands/skill.mjs +52 -0
  32. package/dist/commands/stats.mjs +58 -32
  33. package/dist/commands/sync.mjs +58 -25
  34. package/dist/config.mjs +39 -4
  35. package/dist/context.mjs +13 -8
  36. package/dist/env-file.mjs +1 -1
  37. package/dist/local-store.mjs +2 -2
  38. package/dist/mcp/errors.mjs +8 -0
  39. package/dist/mcp/handlers/diagnostics.mjs +31 -0
  40. package/dist/mcp/handlers/reports.mjs +38 -11
  41. package/dist/mcp/server/index.mjs +9 -10
  42. package/dist/mcp/types.mjs +8 -3
  43. package/dist/package.mjs +1 -1
  44. package/dist/papercut.mjs +99 -0
  45. package/dist/render/analysis.mjs +98 -0
  46. package/dist/render/charts.mjs +170 -0
  47. package/dist/render/layout.mjs +87 -0
  48. package/dist/render/metrics.mjs +163 -0
  49. package/dist/render/query.mjs +30 -0
  50. package/dist/render/report.mjs +69 -0
  51. package/dist/render/terminal.mjs +25 -0
  52. package/dist/runtime.d.mts +5 -5
  53. package/dist/runtime.mjs +1 -1
  54. package/dist/skill.mjs +45 -0
  55. package/dist/utils.mjs +10 -38
  56. package/package.json +14 -12
  57. package/skills/gscdump/SKILL.md +287 -0
@@ -1,9 +1,13 @@
1
1
  import { queryCommandMeta } from "../command-meta.mjs";
2
2
  import { loadConfig } from "../config.mjs";
3
- import { ALL_SEARCH_TYPES, exportToCSV, logger, parseSearchType } from "../utils.mjs";
3
+ import { terminalOutputOptions } from "../render/terminal.mjs";
4
+ import { ALL_SEARCH_TYPES, logger, parseSearchType, toCSV } from "../utils.mjs";
4
5
  import { allTables, inferTable } from "../local-store.mjs";
5
6
  import { createCommandContext } from "../context.mjs";
6
7
  import { gscErrorHandler } from "../error-handler.mjs";
8
+ import { renderTable } from "../render/layout.mjs";
9
+ import { asRecord, columnsFor } from "../render/analysis.mjs";
10
+ import { renderQuery } from "../render/query.mjs";
7
11
  import process from "node:process";
8
12
  import { defineCommand } from "citty";
9
13
  import fs from "node:fs/promises";
@@ -53,6 +57,8 @@ const AGGREGATION_TYPES = [
53
57
  "byPage",
54
58
  "byProperty"
55
59
  ];
60
+ const POSITIVE_INTEGER_RE = /^\d+$/;
61
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
56
62
  function buildFilterFromArg(col, raw) {
57
63
  if (raw.startsWith("!~")) return makeLeaf(col, "notContains", raw.slice(2));
58
64
  if (raw.startsWith("!re:")) return notRegex(col, raw.slice(4));
@@ -148,8 +154,7 @@ const queryCommand = defineCommand({
148
154
  "limit": {
149
155
  type: "string",
150
156
  alias: "l",
151
- default: "1000",
152
- description: "Max rows (default: 1000)"
157
+ description: "Max rows (default: saved defaultLimit or 1000)"
153
158
  },
154
159
  "output": {
155
160
  type: "string",
@@ -159,8 +164,7 @@ const queryCommand = defineCommand({
159
164
  "format": {
160
165
  type: "string",
161
166
  alias: "f",
162
- default: "json",
163
- description: "Output format: json or csv"
167
+ description: "Output format: table, json, or csv (default: saved defaultFormat or json)"
164
168
  },
165
169
  "sql": {
166
170
  type: "string",
@@ -231,24 +235,33 @@ const queryCommand = defineCommand({
231
235
  }
232
236
  },
233
237
  async run({ args }) {
238
+ const ctxConfig = await loadConfig();
239
+ const format = args.format ?? ctxConfig.defaultFormat ?? "json";
240
+ if (format !== "json" && format !== "csv" && format !== "table") {
241
+ logger.error("Invalid --format. Use table, json, or csv.");
242
+ process.exit(1);
243
+ }
234
244
  if (args.sql) {
235
245
  await runRawSqlMode({
236
246
  sql: String(args.sql),
237
247
  site: args.site ? String(args.site) : void 0,
238
248
  table: args.table ? String(args.table) : "pages",
239
249
  output: args.output ? String(args.output) : void 0,
250
+ format,
240
251
  quiet: Boolean(args.quiet),
241
252
  searchType: parseSearchType(args.type, "--type")
242
253
  });
243
254
  return;
244
255
  }
245
- const ctxConfig = await loadConfig();
246
256
  const dimNames = await resolveDimensions(args);
247
257
  const { startDate, endDate } = await resolveRange(args);
248
258
  await promptFilters(args);
249
- const limitArg = args.limit != null ? String(args.limit) : null;
250
- const rowLimit = limitArg != null && limitArg !== "1000" ? Number.parseInt(limitArg, 10) : ctxConfig.defaultLimit ?? 1e3;
251
- const format = String(args.format);
259
+ const limitArg = String(args.limit ?? ctxConfig.defaultLimit ?? 1e3);
260
+ const rowLimit = Number(limitArg);
261
+ if (!POSITIVE_INTEGER_RE.test(limitArg) || !Number.isSafeInteger(rowLimit) || rowLimit < 1) {
262
+ logger.error("Invalid --limit. Use a positive safe integer, such as --limit 1000.");
263
+ process.exit(1);
264
+ }
252
265
  const dimensionFilter = buildDimensionFilter(args);
253
266
  const searchType = parseSearchType(args.type ?? ctxConfig.defaultSearchType, "--type");
254
267
  const dataState = args["data-state"] ? String(args["data-state"]) : ctxConfig.defaultDataState;
@@ -285,7 +298,7 @@ const queryCommand = defineCommand({
285
298
  }, null, 2));
286
299
  return;
287
300
  }
288
- if (!args.quiet) logger.info(`Querying ${siteUrl} via live GSC API...`);
301
+ if (!args.quiet) logger.debug(`Querying ${siteUrl} via live GSC API...`);
289
302
  const result = await runLiveQuery(ctx.client, siteUrl, {
290
303
  startDate,
291
304
  endDate,
@@ -314,7 +327,7 @@ const queryCommand = defineCommand({
314
327
  return;
315
328
  }
316
329
  if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
317
- if (!args.quiet) logger.info(`Querying ${siteUrl} from local Parquet store...`);
330
+ if (!args.quiet) logger.debug(`Querying ${siteUrl} from local Parquet store...`);
318
331
  const state = buildLocalState(dimNames, startDate, endDate, rowLimit, dimensionFilter);
319
332
  const store = ctx.store;
320
333
  const table = inferTable(dimNames);
@@ -357,7 +370,14 @@ const queryCommand = defineCommand({
357
370
  }
358
371
  });
359
372
  async function resolveDimensions(args) {
360
- if (args.dimensions) return String(args.dimensions).split(",").filter((d) => DIMENSIONS.includes(d));
373
+ if (args.dimensions != null) {
374
+ const dimensions = String(args.dimensions).split(",").map((d) => d.trim());
375
+ if (dimensions.some((d) => !DIMENSIONS.includes(d))) {
376
+ logger.error(`Invalid --dimensions. Use a comma-separated list from: ${DIMENSIONS.join(", ")}.`);
377
+ process.exit(1);
378
+ }
379
+ return [...new Set(dimensions)];
380
+ }
361
381
  if (args.interactive) {
362
382
  const selected = await multiselect({
363
383
  message: "Select dimensions",
@@ -376,35 +396,48 @@ async function resolveDimensions(args) {
376
396
  return ["page", "query"];
377
397
  }
378
398
  async function resolveRange(args) {
379
- if (args.start && args.end) return {
380
- startDate: String(args.start),
381
- endDate: String(args.end)
382
- };
399
+ let startDate = args.start != null ? String(args.start) : void 0;
400
+ let endDate = args.end != null ? String(args.end) : void 0;
383
401
  if (args.interactive) {
384
- const startInput = await text({
385
- message: "Start date (YYYY-MM-DD)",
386
- placeholder: daysAgoUtc(28)
387
- });
388
- if (isCancel(startInput)) {
389
- cancel("Cancelled");
390
- process.exit(0);
402
+ if (startDate === void 0) {
403
+ const startInput = await text({
404
+ message: "Start date (YYYY-MM-DD)",
405
+ placeholder: daysAgoUtc(28)
406
+ });
407
+ if (isCancel(startInput)) {
408
+ cancel("Cancelled");
409
+ process.exit(0);
410
+ }
411
+ startDate = String(startInput) || daysAgoUtc(28);
391
412
  }
392
- const endInput = await text({
393
- message: "End date (YYYY-MM-DD)",
394
- placeholder: daysAgoUtc(3)
395
- });
396
- if (isCancel(endInput)) {
397
- cancel("Cancelled");
398
- process.exit(0);
413
+ if (endDate === void 0) {
414
+ const endInput = await text({
415
+ message: "End date (YYYY-MM-DD)",
416
+ placeholder: daysAgoUtc(3)
417
+ });
418
+ if (isCancel(endInput)) {
419
+ cancel("Cancelled");
420
+ process.exit(0);
421
+ }
422
+ endDate = String(endInput) || daysAgoUtc(3);
423
+ }
424
+ }
425
+ startDate ??= daysAgoUtc(31);
426
+ endDate ??= daysAgoUtc(3);
427
+ for (const [flag, value] of [["--start", startDate], ["--end", endDate]]) {
428
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
429
+ if (!ISO_DATE_RE.test(value) || !Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
430
+ logger.error(`Invalid ${flag}. Use a calendar date in YYYY-MM-DD format.`);
431
+ process.exit(1);
399
432
  }
400
- return {
401
- startDate: String(startInput) || daysAgoUtc(28),
402
- endDate: String(endInput) || daysAgoUtc(3)
403
- };
433
+ }
434
+ if (startDate > endDate) {
435
+ logger.error("Invalid date range. --start must be on or before --end.");
436
+ process.exit(1);
404
437
  }
405
438
  return {
406
- startDate: daysAgoUtc(31),
407
- endDate: daysAgoUtc(3)
439
+ startDate,
440
+ endDate
408
441
  };
409
442
  }
410
443
  async function promptFilters(args) {
@@ -492,7 +525,7 @@ async function runRawSqlMode(opts) {
492
525
  });
493
526
  const siteUrl = await ctx.resolveSite(opts.site);
494
527
  const store = ctx.store;
495
- if (!opts.quiet) logger.info(`Running raw SQL over table "${opts.table}" for ${siteUrl}`);
528
+ if (!opts.quiet) logger.debug(`Running raw SQL over table "${opts.table}" for ${siteUrl}`);
496
529
  const { rows, sql } = await store.runRawSql({
497
530
  sql: opts.sql,
498
531
  siteUrl,
@@ -502,11 +535,11 @@ async function runRawSqlMode(opts) {
502
535
  logger.error(`SQL failed: ${e.message}`);
503
536
  process.exit(1);
504
537
  });
505
- const payload = JSON.stringify({
538
+ const payload = opts.format === "table" ? renderTable(rows, columnsFor(rows), terminalOutputOptions(Boolean(opts.output && opts.output !== "-"))).join("\n") : opts.format === "csv" ? toCSV(rows, Object.keys(rows[0] ?? {})) : JSON.stringify({
506
539
  sql,
507
540
  total: rows.length,
508
541
  data: rows
509
- }, null, 2);
542
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value, 2);
510
543
  if (opts.output && opts.output !== "-") {
511
544
  await fs.writeFile(opts.output, payload);
512
545
  if (!opts.quiet) logger.info(`Written to ${opts.output}`);
@@ -524,7 +557,20 @@ function logProfile(spans) {
524
557
  logger.info(`Query timing breakdown:\n${lines.join("\n")}\n${row("total", total)}`);
525
558
  }
526
559
  async function writeOutput(opts) {
527
- const content = opts.format === "csv" ? exportToCSV(opts.output) : JSON.stringify(opts.output, null, 2);
560
+ const range = asRecord(opts.output.dateRange);
561
+ const content = opts.format === "table" ? renderQuery({
562
+ site: String(opts.output.siteUrl),
563
+ start: String(range.start),
564
+ end: String(range.end),
565
+ dimensions: opts.output.dimensions,
566
+ rows: opts.output.data
567
+ }, terminalOutputOptions(Boolean(opts.path && opts.path !== "-"))) : opts.format === "csv" ? toCSV(opts.output.data, [
568
+ ...opts.output.dimensions,
569
+ "clicks",
570
+ "impressions",
571
+ "ctr",
572
+ "position"
573
+ ]) : JSON.stringify(opts.output, null, 2);
528
574
  if (opts.path && opts.path !== "-") {
529
575
  await fs.writeFile(opts.path, content);
530
576
  if (!opts.quiet) logger.info(`Written to ${opts.path}`);
@@ -1,11 +1,13 @@
1
1
  import { reportCommandMeta } from "../command-meta.mjs";
2
+ import { terminalOutputOptions } from "../render/terminal.mjs";
2
3
  import { logger } from "../utils.mjs";
3
4
  import { resolveAnalysisSource } from "../analysis-local.mjs";
5
+ import { renderCliReport } from "../render/report.mjs";
4
6
  import { defineCommand } from "citty";
5
7
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
6
- import { err, ok, unwrapResult } from "gscdump/result";
7
- import { defaultReportRegistry, dryRunReport, formatReport, runReport } from "@gscdump/analysis/report";
8
8
  import { resolveWindow } from "@gscdump/engine/period";
9
+ import { err, ok, unwrapResult } from "gscdump/result";
10
+ import { defaultReportRegistry, dryRunReport, runReport } from "@gscdump/analysis/report";
9
11
  function reportFlagErrorToException(error) {
10
12
  const exception = new Error(error.message);
11
13
  exception.reportFlagError = error;
@@ -201,7 +203,7 @@ function makeReportCommand(report) {
201
203
  console.log(JSON.stringify(result, null, 2));
202
204
  return;
203
205
  }
204
- console.log(formatReport(result));
206
+ console.log(renderCliReport(result, terminalOutputOptions()));
205
207
  if (result.meta.degraded) logger.warn(`degraded: ${result.meta.steps.filter((s) => s.status === "error").map((s) => `${s.key}(${s.error})`).join(", ")}`);
206
208
  }
207
209
  });
@@ -1,31 +1,36 @@
1
+ import { parseAuthentication, resolveAuthentication } from "../auth-state.mjs";
1
2
  import { sitemapsCommandMeta } from "../command-meta.mjs";
2
3
  import { resolveCliEnvironment } from "../environment.mjs";
3
- import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "../utils.mjs";
4
+ import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected, parseIntegerOption } from "../utils.mjs";
4
5
  import { createCommandContext } from "../context.mjs";
5
6
  import { gscErrorHandler } from "../error-handler.mjs";
6
7
  import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
7
8
  import process from "node:process";
8
9
  import { defineCommand } from "citty";
9
- import { fetchSitemap } from "gscdump/sites";
10
10
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
11
+ import { fetchSitemap } from "gscdump/sites";
11
12
  const HOSTED_ARGS = {
12
13
  "api-root": {
13
14
  type: "string",
14
- description: "Hosted API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api/_gscdump"
15
+ description: "Hosted API root; defaults to saved cloud authentication or https://gscdump.com/api"
15
16
  },
16
17
  "api-key": {
17
18
  type: "string",
18
19
  description: "Hosted API key; defaults to GSCDUMP_API_KEY"
19
20
  }
20
21
  };
21
- function hostedClient(args) {
22
+ async function hostedClient(args) {
22
23
  const environment = resolveCliEnvironment().values;
23
- const apiRoot = String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api/_gscdump");
24
- const apiKey = String(args["api-key"] || environment.GSCDUMP_API_KEY || "");
25
- if (!apiKey) throw new Error("Hosted sitemap reads require --api-key or GSCDUMP_API_KEY");
24
+ const authentication = args["api-key"] ? parseAuthentication({
25
+ _tag: "Cloud",
26
+ apiKey: args["api-key"],
27
+ apiRoot: String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api")
28
+ }) : await resolveAuthentication();
29
+ if (authentication._tag !== "Cloud") throw new Error("Hosted sitemap reads require cloud authentication. Run `gscdump auth login --mode cloud` or supply --api-key.");
30
+ if (args["api-root"] && String(args["api-root"]).replace(/\/+$/, "") !== authentication.apiRoot) throw new Error("The API root changed. Supply --api-key explicitly for the new API root.");
26
31
  return createGscdumpV1Client({
27
- apiRoot,
28
- credential: apiKey
32
+ apiRoot: authentication.apiRoot,
33
+ credential: authentication.apiKey
29
34
  });
30
35
  }
31
36
  const listCommand = defineCommand({
@@ -257,8 +262,8 @@ const urlsCommand = defineCommand({
257
262
  },
258
263
  async run({ args }) {
259
264
  const { json } = applyOutputMode(args);
260
- const limit = args.limit ? Number.parseInt(String(args.limit), 10) : void 0;
261
- const maxDepth = args["max-depth"] ? Number.parseInt(String(args["max-depth"]), 10) : void 0;
265
+ const limit = parseIntegerOption(args.limit, "--limit");
266
+ const maxDepth = parseIntegerOption(args["max-depth"], "--max-depth", 0);
262
267
  const result = await loadSitemapUrls(String(args.url), {
263
268
  maxUrls: limit,
264
269
  maxDepth
@@ -298,7 +303,7 @@ const currentCommand = defineCommand({
298
303
  },
299
304
  async run({ args }) {
300
305
  const { json } = applyOutputMode(args);
301
- const result = await hostedClient(args).getSiteSitemaps({ params: { siteId: String(args["site-id"]) } });
306
+ const result = await (await hostedClient(args)).getSiteSitemaps({ params: { siteId: String(args["site-id"]) } });
302
307
  if (json) {
303
308
  console.log(JSON.stringify(result.data, null, 2));
304
309
  return;
@@ -328,8 +333,8 @@ const historyCommand = defineCommand({
328
333
  },
329
334
  async run({ args }) {
330
335
  const { json } = applyOutputMode(args);
331
- const days = args.days ? Number.parseInt(String(args.days), 10) : void 0;
332
- const result = await hostedClient(args).getSiteSitemapChanges({
336
+ const days = parseIntegerOption(args.days, "--days");
337
+ const result = await (await hostedClient(args)).getSiteSitemapChanges({
333
338
  params: { siteId: String(args["site-id"]) },
334
339
  query: { ...days ? { days } : {} }
335
340
  });
@@ -362,7 +367,7 @@ const membershipCommand = defineCommand({
362
367
  async run({ args }) {
363
368
  const { json } = applyOutputMode(args);
364
369
  const urls = String(args.urls).split(",").map((url) => url.trim()).filter(Boolean);
365
- const result = await hostedClient(args).querySitemapMembership({
370
+ const result = await (await hostedClient(args)).querySitemapMembership({
366
371
  params: { siteId: String(args["site-id"]) },
367
372
  body: {
368
373
  urls,
@@ -411,13 +416,14 @@ const lastmodCommand = defineCommand({
411
416
  },
412
417
  async run({ args }) {
413
418
  const { json } = applyOutputMode(args);
414
- const result = await hostedClient(args).listSitemapUrls({
419
+ const limit = parseIntegerOption(args.limit, "--limit");
420
+ const result = await (await hostedClient(args)).listSitemapUrls({
415
421
  params: { siteId: String(args["site-id"]) },
416
422
  query: {
417
423
  ...args.generation ? { generationId: String(args.generation) } : {},
418
424
  ...args.feedpath ? { feedpath: String(args.feedpath) } : {},
419
425
  ...args.cursor ? { cursor: String(args.cursor) } : {},
420
- ...args.limit ? { limit: Number.parseInt(String(args.limit), 10) } : {}
426
+ ...limit !== void 0 ? { limit } : {}
421
427
  }
422
428
  });
423
429
  if (json) {
@@ -452,7 +458,7 @@ const exportCommand = defineCommand({
452
458
  },
453
459
  async run({ args }) {
454
460
  const { json } = applyOutputMode(args);
455
- const result = await hostedClient(args).getSitemapExport({
461
+ const result = await (await hostedClient(args)).getSitemapExport({
456
462
  params: { siteId: String(args["site-id"]) },
457
463
  query: {
458
464
  ...args.generation ? { generationId: String(args.generation) } : {},
@@ -0,0 +1,52 @@
1
+ import { skillCommandMeta } from "../command-meta.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
3
+ import { SKILL_AGENTS, SKILL_NAME, installSkill } from "../skill.mjs";
4
+ import process from "node:process";
5
+ import { defineCommand } from "citty";
6
+ import os from "node:os";
7
+ const installCommand = defineCommand({
8
+ meta: {
9
+ name: "install",
10
+ description: `Copy the ${SKILL_NAME} skill into an agent skill directory`
11
+ },
12
+ args: {
13
+ agent: {
14
+ type: "string",
15
+ description: `Agent skill directory: ${SKILL_AGENTS.join(" or ")} (default claude)`,
16
+ default: "claude"
17
+ },
18
+ target: {
19
+ type: "string",
20
+ description: "Write into this directory instead of the agent home directory"
21
+ },
22
+ ...OUTPUT_ARGS
23
+ },
24
+ async run({ args }) {
25
+ const { json } = applyOutputMode(args);
26
+ const agent = SKILL_AGENTS.find((name) => name === args.agent);
27
+ if (!agent) {
28
+ logger.error(`Unknown agent "${args.agent}". Use ${SKILL_AGENTS.join(" or ")}.`);
29
+ process.exit(2);
30
+ }
31
+ const result = await installSkill({
32
+ agent,
33
+ homeDirectory: os.homedir(),
34
+ target: args.target
35
+ });
36
+ if (result._tag === "Err") {
37
+ logger.error(result.message);
38
+ process.exit(1);
39
+ }
40
+ if (json) {
41
+ console.log(JSON.stringify(result.installation));
42
+ return;
43
+ }
44
+ logger.success(`Installed the ${SKILL_NAME} skill for ${agent} at ${displayPath(result.installation.destination)}`);
45
+ logger.info("Read its SKILL.md before running other commands.");
46
+ }
47
+ });
48
+ const skillCommand = defineCommand({
49
+ meta: skillCommandMeta,
50
+ subCommands: { install: installCommand }
51
+ });
52
+ export { skillCommand };
@@ -1,8 +1,14 @@
1
+ import { terminalOutputOptions } from "../render/terminal.mjs";
1
2
  import { OUTPUT_ARGS, applyOutputMode, displayPath, formatAge, logger } from "../utils.mjs";
2
3
  import { allTables } from "../local-store.mjs";
3
4
  import { createCommandContext } from "../context.mjs";
5
+ import { renderTable, textLines } from "../render/layout.mjs";
6
+ import { formatMetric } from "../render/metrics.mjs";
7
+ import { barColumn } from "../render/charts.mjs";
8
+ import { columnsFor } from "../render/analysis.mjs";
4
9
  import process from "node:process";
5
10
  import { defineCommand } from "citty";
11
+ import { decodeSiteId, parseGscSiteUrl } from "gscdump";
6
12
  import { filesystemStats } from "@gscdump/engine/filesystem";
7
13
  const statsCommand = defineCommand({
8
14
  meta: {
@@ -68,33 +74,59 @@ const statsCommand = defineCommand({
68
74
  console.log(JSON.stringify(payload, null, 2));
69
75
  return;
70
76
  }
71
- console.log();
72
- console.log(` \x1B[1m${displayPath(store.dataDir)}\x1B[0m`);
73
- console.log(` \x1B[90mDisk: ${disk.files} file(s), ${formatBytes(disk.bytes)}\x1B[0m`);
74
- console.log();
75
- const totalRows = perTable.reduce((acc, t) => acc + sumRows(t.live), 0);
76
- const totalBytes = perTable.reduce((acc, t) => acc + sumBytes(t.live), 0);
77
- const totalFiles = perTable.reduce((acc, t) => acc + t.live.length, 0);
78
- const totalRetiredFiles = perTable.reduce((acc, t) => acc + t.retired.length, 0);
79
- const totalRetiredBytes = perTable.reduce((acc, t) => acc + sumBytes(t.retired), 0);
80
- for (const { table, live, retired } of perTable) {
81
- const rows = sumRows(live).toLocaleString();
82
- const bytes = formatBytes(sumBytes(live));
83
- const retiredSuffix = retired.length > 0 ? ` \x1B[90m(+${retired.length} retired, ${formatBytes(sumBytes(retired))})\x1B[0m` : "";
84
- console.log(` ${table.padEnd(15)} \x1B[36m${String(live.length).padStart(4)}\x1B[0m files, ${rows.padStart(10)} rows, ${bytes}${retiredSuffix}`);
85
- }
86
- console.log();
87
- console.log(` \x1B[1mTotal:\x1B[0m ${totalFiles} files, ${totalRows.toLocaleString()} rows, ${formatBytes(totalBytes)} live`);
88
- if (totalRetiredFiles > 0) console.log(` \x1B[90mRetired: ${totalRetiredFiles} files, ${formatBytes(totalRetiredBytes)} awaiting GC\x1B[0m`);
89
- if (watermarks.length > 0) {
90
- console.log();
91
- console.log(` \x1B[1mSync watermarks:\x1B[0m`);
92
- for (const w of sortWatermarks(watermarks)) {
93
- const scope = w.siteId ? `${w.table}@${w.siteId}` : w.table;
94
- console.log(` ${scope.padEnd(24)} \x1B[36m${w.oldestDateSynced}\x1B[0m → \x1B[36m${w.newestDateSynced}\x1B[0m \x1B[90m(last ${formatAge(w.lastSyncAt)})\x1B[0m`);
77
+ const options = terminalOutputOptions();
78
+ const rows = perTable.filter(({ live, retired }) => live.length || retired.length).map(({ table, live, retired }) => ({
79
+ table,
80
+ liveFiles: live.length,
81
+ liveRows: sumRows(live),
82
+ liveBytes: sumBytes(live),
83
+ retiredFiles: retired.length,
84
+ retiredBytes: sumBytes(retired)
85
+ }));
86
+ const retired = rows.some((row) => row.retiredFiles > 0);
87
+ const lines = [
88
+ ...textLines(`Store / ${displayPath(store.dataDir)}`, options, "accent"),
89
+ ...textLines(`Disk ${formatMetric("bytes", disk.bytes)} ${disk.files} files`, options),
90
+ "",
91
+ ...rows.length ? renderTable(rows, [
92
+ {
93
+ key: "table",
94
+ label: "Table"
95
+ },
96
+ ...columnsFor(rows, ["liveFiles", "liveRows"]),
97
+ barColumn(rows, "liveBytes", options),
98
+ ...retired ? [{
99
+ key: "retiredFiles",
100
+ label: "Retired files",
101
+ numeric: true
102
+ }, {
103
+ key: "retiredBytes",
104
+ label: "Retired bytes",
105
+ numeric: true,
106
+ format: (value) => formatMetric("bytes", value)
107
+ }] : []
108
+ ], options) : textLines("Empty Store.", options)
109
+ ];
110
+ if (rows.length > 1) lines.push(...textLines(`Total ${formatMetric("bytes", rows.reduce((sum, row) => sum + row.liveBytes, 0))} live`, options, "muted"));
111
+ if (watermarks.length) lines.push("", ...renderTable(sortWatermarks(watermarks).map((w) => ({
112
+ scope: w.siteId ? `${w.table}@${parseGscSiteUrl(decodeSiteId(w.siteId)).hostname}` : w.table,
113
+ dates: `${w.oldestDateSynced} to ${w.newestDateSynced}`,
114
+ synced: formatAge(w.lastSyncAt)
115
+ })), [
116
+ {
117
+ key: "scope",
118
+ label: ""
119
+ },
120
+ {
121
+ key: "dates",
122
+ label: "Dates"
123
+ },
124
+ {
125
+ key: "synced",
126
+ label: "Synced"
95
127
  }
96
- }
97
- console.log();
128
+ ], options));
129
+ console.log(lines.join("\n"));
98
130
  }
99
131
  });
100
132
  function sortWatermarks(ws) {
@@ -109,10 +141,4 @@ function sumRows(entries) {
109
141
  function sumBytes(entries) {
110
142
  return entries.reduce((acc, e) => acc + e.bytes, 0);
111
143
  }
112
- function formatBytes(n) {
113
- if (n < 1024) return `${n} B`;
114
- if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`;
115
- if (n < 1073741824) return `${(n / 1024 / 1024).toFixed(1)} MB`;
116
- return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
117
- }
118
144
  export { statsCommand };