@gscdump/cli 0.38.1 → 0.39.0

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.
@@ -1,11 +1,10 @@
1
- import { ALL_SEARCH_TYPES, OUTPUT_ARGS, applyOutputMode, displayPath, logger, parseSearchType, toCSV } from "./utils.mjs";
1
+ import { ALL_SEARCH_TYPES, OUTPUT_ARGS, applyOutputMode, displayPath, logger, parseSearchType, runWithConcurrency, toCSV } from "./utils.mjs";
2
2
  import { allTables, createCommandContext } from "./context.mjs";
3
3
  import { readParquetRows } from "./native-duckdb.mjs";
4
4
  import process from "node:process";
5
5
  import { defineCommand } from "citty";
6
6
  import fs from "node:fs/promises";
7
7
  import path from "node:path";
8
- import { Buffer } from "node:buffer";
9
8
  const DEFAULT_OUT = "./gscdump-export";
10
9
  const FORMATS = [
11
10
  "parquet",
@@ -72,32 +71,48 @@ const dumpCommand = defineCommand({
72
71
  });
73
72
  const store = ctx.store;
74
73
  const outDir = path.resolve(String(args.out));
75
- const targets = args["all-sites"] ? await listSitesWithData(store) : [await ctx.resolveSite(args.site ? String(args.site) : void 0)];
74
+ let preloadedEntries = args["all-sites"] ? await store.engine.listLive({
75
+ userId: store.userId,
76
+ ...searchType !== void 0 ? { searchType } : {}
77
+ }) : void 0;
78
+ const targets = args["all-sites"] ? [...new Set(preloadedEntries.flatMap((entry) => entry.siteId ? [entry.siteId] : []))].map((siteId) => ({
79
+ site: siteId,
80
+ siteId
81
+ })) : await ctx.resolveSite(args.site ? String(args.site) : void 0).then((site) => [{
82
+ site,
83
+ siteId: store.siteIdFor(site)
84
+ }]);
76
85
  if (targets.length === 0) {
77
86
  logger.warn("No sites with local data. Run `gscdump sync` first.");
78
87
  process.exit(0);
79
88
  }
80
- if (args.compact) for (const siteUrl of targets) await compactClosedMonths(store, siteUrl, quiet);
89
+ if (args.compact) {
90
+ for (const target of targets) await compactClosedMonths(store, target.siteId, quiet);
91
+ if (preloadedEntries) preloadedEntries = await store.engine.listLive({
92
+ userId: store.userId,
93
+ ...searchType !== void 0 ? { searchType } : {}
94
+ });
95
+ }
81
96
  const summary = [];
82
- for (const siteUrl of targets) {
83
- const entries = (await listLiveEntries(store, siteUrl, searchType)).filter((e) => !tablesFilter || tablesFilter.has(e.table));
97
+ for (const target of targets) {
98
+ const entries = (preloadedEntries ? preloadedEntries.filter((entry) => entry.siteId === target.siteId) : await listLiveEntries(store, target.siteId, searchType)).filter((e) => !tablesFilter || tablesFilter.has(e.table));
84
99
  if (entries.length === 0) {
85
- if (!quiet) logger.warn(`No data for ${siteUrl}; skipping`);
100
+ if (!quiet) logger.warn(`No data for ${target.site}; skipping`);
86
101
  continue;
87
102
  }
88
103
  if (format === "parquet") {
89
104
  const written = await dumpParquet(store, entries, outDir);
90
105
  summary.push({
91
- site: siteUrl,
106
+ site: target.site,
92
107
  files: written,
93
108
  rows: 0,
94
109
  format,
95
110
  outPath: outDir
96
111
  });
97
112
  } else {
98
- const written = await dumpRowFormat(store, entries, outDir, siteUrl, format);
113
+ const written = await dumpRowFormat(store, entries, outDir, target.site, format);
99
114
  summary.push({
100
- site: siteUrl,
115
+ site: target.site,
101
116
  files: written.files,
102
117
  rows: written.rows,
103
118
  format,
@@ -118,36 +133,33 @@ const dumpCommand = defineCommand({
118
133
  }
119
134
  }
120
135
  });
121
- async function listSitesWithData(store) {
122
- const siteIds = /* @__PURE__ */ new Set();
123
- for (const table of allTables()) {
124
- const entries = await store.engine.listLive({
125
- userId: store.userId,
126
- table
127
- });
128
- for (const e of entries) if (e.siteId) siteIds.add(e.siteId);
129
- }
130
- return Array.from(siteIds);
131
- }
132
- async function listLiveEntries(store, siteUrl, searchType) {
133
- const siteId = store.siteIdFor(siteUrl);
134
- return (await Promise.all(allTables().map((table) => store.engine.listLive({
136
+ async function listLiveEntries(store, siteId, searchType) {
137
+ return store.engine.listLive({
135
138
  userId: store.userId,
136
139
  siteId,
137
- table,
138
140
  ...searchType !== void 0 ? { searchType } : {}
139
- })))).flat();
141
+ });
140
142
  }
141
143
  async function dumpParquet(store, entries, outDir) {
142
144
  await fs.mkdir(outDir, { recursive: true });
145
+ const readyDirectories = /* @__PURE__ */ new Map();
146
+ async function ensureDirectory(dir) {
147
+ let ready = readyDirectories.get(dir);
148
+ if (!ready) {
149
+ ready = fs.mkdir(dir, { recursive: true }).then(() => void 0);
150
+ readyDirectories.set(dir, ready);
151
+ ready.catch(() => readyDirectories.delete(dir));
152
+ }
153
+ await ready;
154
+ }
143
155
  let copied = 0;
144
- for (const entry of entries) {
156
+ await runWithConcurrency(entries, 8, async (entry) => {
145
157
  const bytes = await store.engine.readObject(entry.objectKey);
146
158
  const target = path.join(outDir, entry.objectKey);
147
- await fs.mkdir(path.dirname(target), { recursive: true });
148
- await fs.writeFile(target, Buffer.from(bytes));
159
+ await ensureDirectory(path.dirname(target));
160
+ await fs.writeFile(target, bytes);
149
161
  copied++;
150
- }
162
+ });
151
163
  return copied;
152
164
  }
153
165
  async function dumpRowFormat(store, entries, outDir, siteUrl, format) {
@@ -179,8 +191,7 @@ async function dumpRowFormat(store, entries, outDir, siteUrl, format) {
179
191
  rows: totalRows
180
192
  };
181
193
  }
182
- async function compactClosedMonths(store, siteUrl, quiet) {
183
- const siteId = store.siteIdFor(siteUrl);
194
+ async function compactClosedMonths(store, siteId, quiet) {
184
195
  for (const table of allTables()) {
185
196
  if (!quiet) logger.info(`Compacting ${table} (raw→d7→d30→d90)`);
186
197
  await store.engine.compactTiered({
@@ -49,14 +49,14 @@ const compactCommand = defineCommand({
49
49
  if (args["raw-days"]) thresholds.raw = Number(args["raw-days"]);
50
50
  if (args["d7-days"]) thresholds.d7 = Number(args["d7-days"]);
51
51
  if (args["d30-days"]) thresholds.d30 = Number(args["d30-days"]);
52
+ const liveEntries = await store.engine.listLive({
53
+ userId: store.userId,
54
+ siteId
55
+ });
52
56
  if (dryRun) {
53
57
  const report = [];
54
58
  for (const table of allTables()) {
55
- const bySite = groupBySite(await store.engine.listLive({
56
- userId: store.userId,
57
- siteId,
58
- table
59
- }));
59
+ const bySite = groupBySite(liveEntries.filter((entry) => entry.table === table));
60
60
  for (const [s, group] of bySite) report.push({
61
61
  table,
62
62
  siteId: s,
@@ -79,11 +79,7 @@ const compactCommand = defineCommand({
79
79
  }
80
80
  const summary = [];
81
81
  for (const table of allTables()) {
82
- const entries = await store.engine.listLive({
83
- userId: store.userId,
84
- siteId,
85
- table
86
- });
82
+ const entries = liveEntries.filter((entry) => entry.table === table);
87
83
  const siteIds = new Set(entries.map((e) => e.siteId));
88
84
  for (const targetSite of siteIds) {
89
85
  logger.info(`Compacting ${table} [${targetSite ?? "-"}] (raw→d7→d30→d90)`);
@@ -138,16 +134,16 @@ function countByTier(entries) {
138
134
  }
139
135
  async function exportToDuckDB(opts) {
140
136
  const outPath = path.resolve(opts.outPath);
137
+ const entries = await opts.engine.listLive({
138
+ userId: opts.userId,
139
+ siteId: opts.siteId
140
+ });
141
141
  const inputs = [];
142
142
  for (const table of allTables()) {
143
- const entries = await opts.engine.listLive({
144
- userId: opts.userId,
145
- siteId: opts.siteId,
146
- table
147
- });
148
- if (entries.length > 0) inputs.push({
143
+ const tableEntries = entries.filter((entry) => entry.table === table);
144
+ if (tableEntries.length > 0) inputs.push({
149
145
  table,
150
- filePaths: entries.map((entry) => path.join(opts.dataDir, entry.objectKey))
146
+ filePaths: tableEntries.map((entry) => path.join(opts.dataDir, entry.objectKey))
151
147
  });
152
148
  }
153
149
  const tables = await materializeParquetTables(outPath, inputs, opts.force);
@@ -237,20 +233,17 @@ const gcCommand = defineCommand({
237
233
  if (args["dry-run"]) {
238
234
  const cutoff = Date.now() - graceMs;
239
235
  const candidates = [];
240
- for (const table of allTables()) {
241
- const all = await store.engine.listAll({
242
- userId: store.userId,
243
- siteId,
244
- table
245
- });
246
- for (const e of all) if (e.retiredAt && e.retiredAt < cutoff) candidates.push({
247
- table,
248
- siteId: e.siteId,
249
- partition: e.partition,
250
- retiredAt: e.retiredAt,
251
- objectKey: e.objectKey
252
- });
253
- }
236
+ const all = await store.engine.listAll({
237
+ userId: store.userId,
238
+ siteId
239
+ });
240
+ for (const e of all) if (e.retiredAt && e.retiredAt < cutoff) candidates.push({
241
+ table: e.table,
242
+ siteId: e.siteId,
243
+ partition: e.partition,
244
+ retiredAt: e.retiredAt,
245
+ objectKey: e.objectKey
246
+ });
254
247
  if (json) {
255
248
  console.log(JSON.stringify({
256
249
  graceHours: Number(args["grace-hours"]),
@@ -337,11 +330,8 @@ const rollupsCommand = defineCommand({
337
330
  const explicitSiteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
338
331
  const allSiteIds = /* @__PURE__ */ new Set();
339
332
  if (explicitSiteId) allSiteIds.add(explicitSiteId);
340
- else for (const table of allTables()) {
341
- const entries = await store.engine.listLive({
342
- userId: store.userId,
343
- table
344
- });
333
+ else {
334
+ const entries = await store.engine.listLive({ userId: store.userId });
345
335
  for (const e of entries) if (e.siteId) allSiteIds.add(e.siteId);
346
336
  }
347
337
  if (allSiteIds.size === 0) {
@@ -423,9 +413,10 @@ const statsCommand = defineCommand({
423
413
  async run({ args }) {
424
414
  const { json } = applyOutputMode(args);
425
415
  const store = (await createCommandContext({ needsStore: true })).store;
416
+ const allEntries = await store.engine.listAll({ userId: store.userId });
426
417
  let siteId;
427
418
  if (args.site) {
428
- const known = await listKnownSiteIds(store);
419
+ const known = new Set(allEntries.filter((entry) => entry.retiredAt === void 0 && entry.siteId !== void 0).map((entry) => entry.siteId));
429
420
  const candidate = store.siteIdFor(args.site);
430
421
  if (!known.has(candidate)) {
431
422
  logger.error(`No local data for --site=${args.site}. Known site IDs: ${known.size === 0 ? "(none — run `gscdump sync` first)" : Array.from(known).join(", ")}`);
@@ -433,26 +424,22 @@ const statsCommand = defineCommand({
433
424
  }
434
425
  siteId = candidate;
435
426
  }
436
- const perTable = await Promise.all(allTables().map(async (table) => {
437
- const all = await store.engine.listAll({
438
- userId: store.userId,
439
- siteId,
440
- table
441
- });
427
+ const selectedEntries = siteId === void 0 ? allEntries : allEntries.filter((entry) => entry.siteId === siteId);
428
+ const perTable = allTables().map((table) => {
429
+ const all = selectedEntries.filter((entry) => entry.table === table);
442
430
  return {
443
431
  table,
444
432
  live: all.filter((e) => e.retiredAt === void 0),
445
433
  retired: all.filter((e) => e.retiredAt !== void 0)
446
434
  };
447
- }));
448
- const watermarks = await store.engine.getWatermarks({
435
+ });
436
+ const [watermarks, disk] = await Promise.all([store.engine.getWatermarks({
449
437
  userId: store.userId,
450
438
  siteId
451
- });
452
- const disk = await filesystemStats(store.dataDir).catch(() => ({
439
+ }), filesystemStats(store.dataDir).catch(() => ({
453
440
  files: 0,
454
441
  bytes: 0
455
- }));
442
+ }))]);
456
443
  if (json) {
457
444
  const payload = {
458
445
  dataDir: store.dataDir,
@@ -504,17 +491,6 @@ const statsCommand = defineCommand({
504
491
  console.log();
505
492
  }
506
493
  });
507
- async function listKnownSiteIds(store) {
508
- const ids = /* @__PURE__ */ new Set();
509
- for (const table of allTables()) {
510
- const entries = await store.engine.listLive({
511
- userId: store.userId,
512
- table
513
- });
514
- for (const e of entries) if (e.siteId) ids.add(e.siteId);
515
- }
516
- return ids;
517
- }
518
494
  function sortWatermarks(ws) {
519
495
  return [...ws].sort((a, b) => {
520
496
  if (a.table !== b.table) return a.table.localeCompare(b.table);
@@ -269,15 +269,13 @@ const syncCommand = defineCommand({
269
269
  const store = ctx.store;
270
270
  if (args["retry-failed"]) {
271
271
  const failedSet = /* @__PURE__ */ new Set();
272
- for (const table of tables) for (const type of types) {
273
- const states = await store.engine.getSyncStates({
274
- userId: store.userId,
275
- siteId,
276
- table,
277
- searchType: type
278
- });
279
- for (const s of states) if (s.state === "failed" && s.date >= startDate && s.date <= endDate) failedSet.add(s.date);
280
- }
272
+ const selectedTables = new Set(tables);
273
+ const selectedTypes = new Set(types);
274
+ const states = await store.engine.getSyncStates({
275
+ userId: store.userId,
276
+ siteId
277
+ });
278
+ for (const s of states) if (selectedTables.has(s.table) && selectedTypes.has(s.searchType ?? "web") && s.state === "failed" && s.date >= startDate && s.date <= endDate) failedSet.add(s.date);
281
279
  dates = dates.filter((d) => failedSet.has(d));
282
280
  if (dates.length === 0) {
283
281
  logger.success("No failed dates in range — nothing to retry.");
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { SearchTypes } from "gscdump/query";
7
- var version = "0.38.1";
7
+ var version = "0.39.0";
8
8
  const ALL_SEARCH_TYPES = Object.values(SearchTypes);
9
9
  const VERSION = version;
10
10
  function noSubcommandSelected(parent, subNames) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.38.1",
4
+ "version": "0.39.0",
5
5
  "description": "CLI for Google Search Console - dump, query, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -52,10 +52,10 @@
52
52
  "ofetch": "^1.5.1",
53
53
  "open": "^11.0.0",
54
54
  "zod": "^4.4.3",
55
- "@gscdump/analysis": "0.38.1",
56
- "@gscdump/engine": "0.38.1",
57
- "@gscdump/engine-gsc-api": "0.38.1",
58
- "gscdump": "0.38.1"
55
+ "@gscdump/analysis": "0.39.0",
56
+ "@gscdump/engine": "0.39.0",
57
+ "@gscdump/engine-gsc-api": "0.39.0",
58
+ "gscdump": "0.39.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "vitest": "^4.1.10"