@gscdump/cli 0.37.2 → 0.37.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/bin/gscdump.mjs +3 -0
- package/dist/_chunks/analysis-local.mjs +88 -0
- package/dist/_chunks/analyze.mjs +332 -0
- package/dist/_chunks/auth.mjs +476 -0
- package/dist/_chunks/auth2.mjs +293 -0
- package/dist/_chunks/config.mjs +93 -0
- package/dist/_chunks/config2.mjs +232 -0
- package/dist/_chunks/context.mjs +69 -0
- package/dist/_chunks/doctor.mjs +400 -0
- package/dist/_chunks/dump.mjs +193 -0
- package/dist/_chunks/entities.mjs +414 -0
- package/dist/_chunks/env-file.mjs +53 -0
- package/dist/_chunks/environment.mjs +30 -0
- package/dist/_chunks/error-handler.mjs +86 -0
- package/dist/_chunks/indexing.mjs +314 -0
- package/dist/_chunks/init.mjs +190 -0
- package/dist/_chunks/inspect.mjs +203 -0
- package/dist/_chunks/mcp.mjs +867 -0
- package/dist/_chunks/native-duckdb.mjs +46 -0
- package/dist/_chunks/profile.mjs +290 -0
- package/dist/_chunks/query.mjs +537 -0
- package/dist/_chunks/report.mjs +243 -0
- package/dist/_chunks/sitemaps.mjs +274 -0
- package/dist/_chunks/sites.mjs +500 -0
- package/dist/_chunks/store.mjs +652 -0
- package/dist/_chunks/sync.mjs +489 -0
- package/dist/_chunks/utils.mjs +191 -0
- package/dist/cli.d.mts +28 -0
- package/dist/cli.mjs +104 -0
- package/package.json +15 -7
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -7330
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { OUTPUT_ARGS, applyOutputMode, clearLine, displayPath, formatAge, logger, runWithConcurrency } from "./utils.mjs";
|
|
2
|
+
import { TABLE_DIMS, allTables, createCommandContext, createLocalStore, transformGscRow } from "./context.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { SearchTypes } from "gscdump/query";
|
|
6
|
+
import { createEmptyTypesStore } from "@gscdump/engine/entities";
|
|
7
|
+
import { progressBar } from "gscdump";
|
|
8
|
+
import { daysAgoUtc, getDateRange } from "gscdump/dates";
|
|
9
|
+
import { DEFAULT_ROLLUPS, rebuildRollups } from "@gscdump/engine/rollups";
|
|
10
|
+
const DEFAULT_TABLES = [
|
|
11
|
+
"pages",
|
|
12
|
+
"queries",
|
|
13
|
+
"countries",
|
|
14
|
+
"dates"
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_TYPES = ["web"];
|
|
17
|
+
const ALL_SEARCH_TYPES = Object.values(SearchTypes);
|
|
18
|
+
const DEFAULT_PENDING_DAYS = 3;
|
|
19
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
20
|
+
const EMPTY_TYPE_PROBE_MIN_DAYS = 7;
|
|
21
|
+
const EMPTY_TYPE_PROTECTED = ["web"];
|
|
22
|
+
function createProgressTracker(total, quiet) {
|
|
23
|
+
if (quiet) return {
|
|
24
|
+
tick: () => {},
|
|
25
|
+
done: () => {}
|
|
26
|
+
};
|
|
27
|
+
let current = 0;
|
|
28
|
+
let lastLabel = "";
|
|
29
|
+
let timer = null;
|
|
30
|
+
const render = () => {
|
|
31
|
+
clearLine();
|
|
32
|
+
process.stdout.write(progressBar(current, total, lastLabel));
|
|
33
|
+
};
|
|
34
|
+
timer = setInterval(render, 100);
|
|
35
|
+
return {
|
|
36
|
+
tick: (label) => {
|
|
37
|
+
current++;
|
|
38
|
+
lastLabel = label;
|
|
39
|
+
},
|
|
40
|
+
done: () => {
|
|
41
|
+
if (timer) {
|
|
42
|
+
clearInterval(timer);
|
|
43
|
+
timer = null;
|
|
44
|
+
}
|
|
45
|
+
clearLine();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function syncTable(store, siteUrl, table, searchType, dates, client, concurrency, force, progress) {
|
|
50
|
+
const dims = TABLE_DIMS[table];
|
|
51
|
+
const siteId = store.siteIdFor(siteUrl);
|
|
52
|
+
let totalRows = 0;
|
|
53
|
+
let skipped = 0;
|
|
54
|
+
let failed = 0;
|
|
55
|
+
const priorStates = await store.engine.getSyncStates({
|
|
56
|
+
userId: store.userId,
|
|
57
|
+
siteId,
|
|
58
|
+
table,
|
|
59
|
+
searchType
|
|
60
|
+
});
|
|
61
|
+
const stateByDate = new Map(priorStates.map((s) => [s.date, s]));
|
|
62
|
+
const label = searchType === "web" ? table : `${table}/${searchType}`;
|
|
63
|
+
await runWithConcurrency(dates, concurrency, async (date) => {
|
|
64
|
+
const prior = stateByDate.get(date);
|
|
65
|
+
if (!force && prior?.state === "done") {
|
|
66
|
+
skipped++;
|
|
67
|
+
progress.tick(`${label} ${date} (skip)`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const scope = {
|
|
71
|
+
userId: store.userId,
|
|
72
|
+
siteId,
|
|
73
|
+
table,
|
|
74
|
+
date,
|
|
75
|
+
searchType
|
|
76
|
+
};
|
|
77
|
+
await store.engine.setSyncState(scope, "inflight");
|
|
78
|
+
const result = await runOneDate(store, client, siteUrl, table, searchType, dims, date).catch((err) => ({
|
|
79
|
+
kind: "error",
|
|
80
|
+
error: err
|
|
81
|
+
}));
|
|
82
|
+
if (result.kind === "error") {
|
|
83
|
+
await store.engine.setSyncState(scope, "failed", { error: result.error.message });
|
|
84
|
+
failed++;
|
|
85
|
+
progress.tick(`${label} ${date} (fail)`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await store.engine.setSyncState(scope, "done");
|
|
89
|
+
totalRows += result.rows;
|
|
90
|
+
progress.tick(`${label} ${date}`);
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
rows: totalRows,
|
|
94
|
+
skipped,
|
|
95
|
+
failed
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function runOneDate(store, client, siteUrl, table, searchType, dims, date) {
|
|
99
|
+
const rowLimit = 25e3;
|
|
100
|
+
const rows = [];
|
|
101
|
+
let startRow = 0;
|
|
102
|
+
while (true) {
|
|
103
|
+
const batch = (await client._rawQuery(siteUrl, {
|
|
104
|
+
startDate: date,
|
|
105
|
+
endDate: date,
|
|
106
|
+
dimensions: dims,
|
|
107
|
+
searchType,
|
|
108
|
+
rowLimit,
|
|
109
|
+
startRow
|
|
110
|
+
})).rows || [];
|
|
111
|
+
for (const apiRow of batch) {
|
|
112
|
+
const transformed = transformGscRow(table, {
|
|
113
|
+
keys: apiRow.keys ?? [],
|
|
114
|
+
clicks: apiRow.clicks ?? 0,
|
|
115
|
+
impressions: apiRow.impressions ?? 0,
|
|
116
|
+
ctr: apiRow.ctr ?? 0,
|
|
117
|
+
position: apiRow.position ?? 0
|
|
118
|
+
});
|
|
119
|
+
if (transformed) rows.push(transformed.row);
|
|
120
|
+
}
|
|
121
|
+
if (batch.length === 0) break;
|
|
122
|
+
startRow += batch.length;
|
|
123
|
+
}
|
|
124
|
+
const writeCtx = {
|
|
125
|
+
userId: store.userId,
|
|
126
|
+
siteId: store.siteIdFor(siteUrl),
|
|
127
|
+
table,
|
|
128
|
+
date,
|
|
129
|
+
searchType
|
|
130
|
+
};
|
|
131
|
+
await store.engine.writeDay(writeCtx, rows);
|
|
132
|
+
return {
|
|
133
|
+
kind: "ok",
|
|
134
|
+
rows: rows.length
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const syncCommand = defineCommand({
|
|
138
|
+
meta: {
|
|
139
|
+
name: "sync",
|
|
140
|
+
description: "Sync GSC data to local Parquet store"
|
|
141
|
+
},
|
|
142
|
+
args: {
|
|
143
|
+
"site": {
|
|
144
|
+
type: "string",
|
|
145
|
+
alias: "s",
|
|
146
|
+
description: "Site URL"
|
|
147
|
+
},
|
|
148
|
+
"start": {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "Start date (YYYY-MM-DD) for historical sync"
|
|
151
|
+
},
|
|
152
|
+
"end": {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "End date (YYYY-MM-DD); defaults to 3 days ago"
|
|
155
|
+
},
|
|
156
|
+
"days": {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: `Number of days back to sync (default: ${DEFAULT_PENDING_DAYS})`
|
|
159
|
+
},
|
|
160
|
+
"tables": {
|
|
161
|
+
type: "string",
|
|
162
|
+
alias: "t",
|
|
163
|
+
description: `Tables to sync (default: ${DEFAULT_TABLES.join(",")}); comma-separated`
|
|
164
|
+
},
|
|
165
|
+
"types": {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: `GSC search types to sync (default: ${DEFAULT_TYPES.join(",")}); comma-separated. Allowed: ${ALL_SEARCH_TYPES.join(",")}.`
|
|
168
|
+
},
|
|
169
|
+
"force-types": {
|
|
170
|
+
type: "boolean",
|
|
171
|
+
default: false,
|
|
172
|
+
description: "Ignore stored empty-type markers and re-probe every requested type"
|
|
173
|
+
},
|
|
174
|
+
"no-rollups": {
|
|
175
|
+
type: "boolean",
|
|
176
|
+
default: false,
|
|
177
|
+
description: "Skip the post-sync rollup rebuild (daily/weekly totals, top-N tables)"
|
|
178
|
+
},
|
|
179
|
+
"full": {
|
|
180
|
+
type: "boolean",
|
|
181
|
+
description: "Sync the last 450 days (full GSC history)"
|
|
182
|
+
},
|
|
183
|
+
...OUTPUT_ARGS,
|
|
184
|
+
"force": {
|
|
185
|
+
type: "boolean",
|
|
186
|
+
default: false,
|
|
187
|
+
description: "Re-sync dates already marked done (default: skip them for idempotent resume)"
|
|
188
|
+
},
|
|
189
|
+
"status": {
|
|
190
|
+
type: "boolean",
|
|
191
|
+
default: false,
|
|
192
|
+
description: "Print watermarks + sync-state summary instead of syncing"
|
|
193
|
+
},
|
|
194
|
+
"concurrency": {
|
|
195
|
+
type: "string",
|
|
196
|
+
alias: "c",
|
|
197
|
+
description: `Concurrent in-flight day fetches per table (default: ${DEFAULT_CONCURRENCY})`
|
|
198
|
+
},
|
|
199
|
+
"serial-tables": {
|
|
200
|
+
type: "boolean",
|
|
201
|
+
default: false,
|
|
202
|
+
description: "Run tables sequentially (default: run all tables in parallel)"
|
|
203
|
+
},
|
|
204
|
+
"retry-failed": {
|
|
205
|
+
type: "boolean",
|
|
206
|
+
default: false,
|
|
207
|
+
description: "Only re-run dates currently in `failed` state (cheaper than --force)"
|
|
208
|
+
},
|
|
209
|
+
"dry-run": {
|
|
210
|
+
type: "boolean",
|
|
211
|
+
default: false,
|
|
212
|
+
description: "Print the planned (table, searchType, date) work and exit without hitting the API"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
async run({ args }) {
|
|
216
|
+
const { json, quiet } = applyOutputMode(args);
|
|
217
|
+
if (args.status) {
|
|
218
|
+
const ctx = await createCommandContext();
|
|
219
|
+
await printSyncStatus({
|
|
220
|
+
config: ctx.config,
|
|
221
|
+
dataDir: ctx.dataDir
|
|
222
|
+
}, args.site ? String(args.site) : void 0, json);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const ctx = await createCommandContext({
|
|
226
|
+
needsAuth: true,
|
|
227
|
+
needsStore: true
|
|
228
|
+
});
|
|
229
|
+
const client = ctx.client;
|
|
230
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
231
|
+
const tables = args.tables ? String(args.tables).split(",").map((t) => t.trim()).filter(isKnownTable) : DEFAULT_TABLES;
|
|
232
|
+
const requestedTypes = args.types ? String(args.types).split(",").map((t) => t.trim()).filter(isKnownSearchType) : DEFAULT_TYPES;
|
|
233
|
+
if (requestedTypes.length === 0) {
|
|
234
|
+
logger.error(`No valid search types specified. Allowed: ${ALL_SEARCH_TYPES.join(",")}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
const siteId = ctx.store.siteIdFor(siteUrl);
|
|
238
|
+
const emptyTypesStore = createEmptyTypesStore({ dataSource: ctx.store.dataSource });
|
|
239
|
+
const emptyTypesDoc = await emptyTypesStore.load({
|
|
240
|
+
userId: ctx.store.userId,
|
|
241
|
+
siteId
|
|
242
|
+
});
|
|
243
|
+
const forceTypes = Boolean(args["force-types"]);
|
|
244
|
+
const skippedTypes = [];
|
|
245
|
+
const types = [];
|
|
246
|
+
for (const t of requestedTypes) {
|
|
247
|
+
if (!forceTypes && emptyTypesDoc.emptyTypes.includes(t) && !EMPTY_TYPE_PROTECTED.includes(t)) {
|
|
248
|
+
skippedTypes.push(t);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
types.push(t);
|
|
252
|
+
}
|
|
253
|
+
if (types.length === 0) {
|
|
254
|
+
logger.warn(`All requested types (${requestedTypes.join(", ")}) are marked empty for this site. Pass --force-types to re-probe.`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (skippedTypes.length > 0 && !quiet) logger.info(`Skipping ${skippedTypes.join(", ")} (marked empty for this site; pass --force-types to re-probe).`);
|
|
258
|
+
const endDate = args.end ? String(args.end) : daysAgoUtc(DEFAULT_PENDING_DAYS);
|
|
259
|
+
let startDate;
|
|
260
|
+
if (args.start) startDate = String(args.start);
|
|
261
|
+
else if (args.full) startDate = daysAgoUtc(450);
|
|
262
|
+
else if (args.days) startDate = daysAgoUtc(Number.parseInt(String(args.days), 10) + DEFAULT_PENDING_DAYS - 1);
|
|
263
|
+
else startDate = daysAgoUtc(5);
|
|
264
|
+
let dates = getDateRange(startDate, endDate);
|
|
265
|
+
if (dates.length === 0) {
|
|
266
|
+
logger.error(`No dates to sync (start=${startDate}, end=${endDate})`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const store = ctx.store;
|
|
270
|
+
if (args["retry-failed"]) {
|
|
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
|
+
}
|
|
281
|
+
dates = dates.filter((d) => failedSet.has(d));
|
|
282
|
+
if (dates.length === 0) {
|
|
283
|
+
logger.success("No failed dates in range — nothing to retry.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
args.force = true;
|
|
287
|
+
if (!quiet) logger.info(`--retry-failed: ${dates.length} date(s) to retry`);
|
|
288
|
+
}
|
|
289
|
+
if (args["dry-run"]) {
|
|
290
|
+
const plan = [];
|
|
291
|
+
for (const table of tables) for (const type of types) for (const date of dates) plan.push({
|
|
292
|
+
table,
|
|
293
|
+
searchType: type,
|
|
294
|
+
date
|
|
295
|
+
});
|
|
296
|
+
if (json) {
|
|
297
|
+
console.log(JSON.stringify({
|
|
298
|
+
siteUrl,
|
|
299
|
+
range: {
|
|
300
|
+
start: startDate,
|
|
301
|
+
end: endDate
|
|
302
|
+
},
|
|
303
|
+
tables,
|
|
304
|
+
types,
|
|
305
|
+
totalCalls: plan.length,
|
|
306
|
+
plan
|
|
307
|
+
}, null, 2));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
console.log();
|
|
311
|
+
logger.info(`Plan: ${plan.length} API call(s) for ${siteUrl}`);
|
|
312
|
+
console.log(` Tables: ${tables.join(", ")}`);
|
|
313
|
+
console.log(` Types: ${types.join(", ")}`);
|
|
314
|
+
console.log(` Range: ${startDate} → ${endDate} (${dates.length} days)`);
|
|
315
|
+
console.log();
|
|
316
|
+
logger.info("Pass without --dry-run to execute.");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!quiet) {
|
|
320
|
+
logger.info(`Syncing ${siteUrl} (${tables.join(", ")}) [${types.join(", ")}] → ${displayPath(store.dataDir)}`);
|
|
321
|
+
logger.info(`Range: ${startDate} → ${endDate} (${dates.length} days)`);
|
|
322
|
+
}
|
|
323
|
+
const concurrency = args.concurrency ? Math.max(1, Number.parseInt(String(args.concurrency), 10) || DEFAULT_CONCURRENCY) : DEFAULT_CONCURRENCY;
|
|
324
|
+
const serialTables = Boolean(args["serial-tables"]);
|
|
325
|
+
const start = Date.now();
|
|
326
|
+
const totals = {};
|
|
327
|
+
const jobs = [];
|
|
328
|
+
for (const table of tables) for (const type of types) {
|
|
329
|
+
const label = type === "web" ? table : `${table}/${type}`;
|
|
330
|
+
jobs.push({
|
|
331
|
+
table,
|
|
332
|
+
type,
|
|
333
|
+
label
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const progress = createProgressTracker(dates.length * jobs.length, quiet);
|
|
337
|
+
if (serialTables) for (const job of jobs) totals[job.label] = await syncTable(store, siteUrl, job.table, job.type, dates, client, concurrency, args.force, progress);
|
|
338
|
+
else {
|
|
339
|
+
const results = await Promise.all(jobs.map((job) => syncTable(store, siteUrl, job.table, job.type, dates, client, concurrency, args.force, progress)));
|
|
340
|
+
jobs.forEach((job, i) => {
|
|
341
|
+
totals[job.label] = results[i];
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
progress.done();
|
|
345
|
+
const seconds = ((Date.now() - start) / 1e3).toFixed(1);
|
|
346
|
+
if (!quiet) {
|
|
347
|
+
logger.success(`Synced ${siteUrl} in ${seconds}s`);
|
|
348
|
+
for (const [t, n] of Object.entries(totals)) {
|
|
349
|
+
const suffix = [n.skipped > 0 ? `${n.skipped} skipped` : null, n.failed > 0 ? `\x1B[31m${n.failed} failed\x1B[0m` : null].filter(Boolean).join(", ");
|
|
350
|
+
const tail = suffix ? ` (${suffix})` : "";
|
|
351
|
+
console.log(` ${t}: ${n.rows.toLocaleString()} rows${tail}`);
|
|
352
|
+
}
|
|
353
|
+
console.log();
|
|
354
|
+
}
|
|
355
|
+
const anyFailed = Object.values(totals).some((t) => t.failed > 0);
|
|
356
|
+
const rowsByType = /* @__PURE__ */ new Map();
|
|
357
|
+
const failedByType = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const job of jobs) {
|
|
359
|
+
const t = totals[job.label];
|
|
360
|
+
rowsByType.set(job.type, (rowsByType.get(job.type) ?? 0) + t.rows);
|
|
361
|
+
failedByType.set(job.type, (failedByType.get(job.type) ?? 0) + t.failed);
|
|
362
|
+
}
|
|
363
|
+
if (!forceTypes && dates.length >= EMPTY_TYPE_PROBE_MIN_DAYS) {
|
|
364
|
+
const toMark = [];
|
|
365
|
+
for (const type of types) {
|
|
366
|
+
if (EMPTY_TYPE_PROTECTED.includes(type)) continue;
|
|
367
|
+
if ((failedByType.get(type) ?? 0) > 0) continue;
|
|
368
|
+
if ((rowsByType.get(type) ?? 0) === 0) toMark.push(type);
|
|
369
|
+
}
|
|
370
|
+
if (toMark.length > 0) {
|
|
371
|
+
await emptyTypesStore.mark({
|
|
372
|
+
userId: store.userId,
|
|
373
|
+
siteId
|
|
374
|
+
}, toMark);
|
|
375
|
+
if (!quiet) logger.info(`Marked empty for future syncs: ${toMark.join(", ")} (0 rows across ${dates.length} days; pass --force-types to re-probe).`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (forceTypes && emptyTypesDoc.emptyTypes.length > 0) {
|
|
379
|
+
const toClear = [];
|
|
380
|
+
for (const type of types) if (emptyTypesDoc.emptyTypes.includes(type) && (rowsByType.get(type) ?? 0) > 0) toClear.push(type);
|
|
381
|
+
if (toClear.length > 0) {
|
|
382
|
+
await emptyTypesStore.clear({
|
|
383
|
+
userId: store.userId,
|
|
384
|
+
siteId
|
|
385
|
+
}, toClear);
|
|
386
|
+
if (!quiet) logger.info(`Cleared empty markers for: ${toClear.join(", ")} (re-probe found data).`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const noRollups = Boolean(args["no-rollups"]);
|
|
390
|
+
const anyRowsSynced = Object.values(totals).some((t) => t.rows > 0);
|
|
391
|
+
if (!noRollups && anyRowsSynced) {
|
|
392
|
+
if (!quiet) logger.info(`Rebuilding rollups for [${siteId}] (${DEFAULT_ROLLUPS.length} rollups)…`);
|
|
393
|
+
const rollupStart = Date.now();
|
|
394
|
+
const results = await rebuildRollups({
|
|
395
|
+
engine: {
|
|
396
|
+
runSQL: (opts) => store.engine.runSQL(opts),
|
|
397
|
+
listPartitions: async ({ ctx, table, searchType }) => {
|
|
398
|
+
return (await store.engine.listLive({
|
|
399
|
+
userId: ctx.userId,
|
|
400
|
+
...ctx.siteId !== void 0 ? { siteId: ctx.siteId } : {},
|
|
401
|
+
table,
|
|
402
|
+
...searchType !== void 0 ? { searchType } : {}
|
|
403
|
+
})).map((e) => ({
|
|
404
|
+
partition: e.partition,
|
|
405
|
+
bytes: e.bytes
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
dataSource: store.dataSource,
|
|
410
|
+
ctx: {
|
|
411
|
+
userId: store.userId,
|
|
412
|
+
siteId
|
|
413
|
+
},
|
|
414
|
+
defs: DEFAULT_ROLLUPS
|
|
415
|
+
}).catch((err) => {
|
|
416
|
+
logger.warn(`Rollup rebuild failed: ${err.message}`);
|
|
417
|
+
return [];
|
|
418
|
+
});
|
|
419
|
+
if (!quiet && results.length > 0) {
|
|
420
|
+
const kb = results.reduce((a, r) => a + r.bytes, 0) / 1024;
|
|
421
|
+
const ms = Date.now() - rollupStart;
|
|
422
|
+
logger.success(`Rebuilt ${results.length} rollup(s) in ${ms}ms — ${kb.toFixed(1)} KB`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (anyFailed) process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
function isKnownTable(name) {
|
|
429
|
+
return allTables().includes(name);
|
|
430
|
+
}
|
|
431
|
+
function isKnownSearchType(name) {
|
|
432
|
+
return ALL_SEARCH_TYPES.includes(name);
|
|
433
|
+
}
|
|
434
|
+
async function printSyncStatus(resolved, siteFilter, asJson) {
|
|
435
|
+
const store = createLocalStore({ dataDir: resolved.dataDir });
|
|
436
|
+
const siteId = siteFilter ? store.siteIdFor(siteFilter) : void 0;
|
|
437
|
+
const watermarks = await store.engine.getWatermarks({
|
|
438
|
+
userId: store.userId,
|
|
439
|
+
siteId
|
|
440
|
+
});
|
|
441
|
+
const states = await store.engine.getSyncStates({
|
|
442
|
+
userId: store.userId,
|
|
443
|
+
siteId
|
|
444
|
+
});
|
|
445
|
+
const failed = states.filter((s) => s.state === "failed");
|
|
446
|
+
const inflight = states.filter((s) => s.state === "inflight");
|
|
447
|
+
if (asJson) {
|
|
448
|
+
console.log(JSON.stringify({
|
|
449
|
+
dataDir: store.dataDir,
|
|
450
|
+
siteFilter: siteFilter ?? null,
|
|
451
|
+
watermarks,
|
|
452
|
+
failed,
|
|
453
|
+
inflight
|
|
454
|
+
}, null, 2));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
console.log();
|
|
458
|
+
console.log(` \x1B[1m${displayPath(store.dataDir)}\x1B[0m`);
|
|
459
|
+
if (siteFilter) console.log(` \x1B[90mSite: ${siteFilter}\x1B[0m`);
|
|
460
|
+
console.log();
|
|
461
|
+
if (watermarks.length === 0) {
|
|
462
|
+
console.log(` No sync watermarks. Run \`gscdump sync\` to ingest data.`);
|
|
463
|
+
console.log();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
console.log(` \x1B[1mWatermarks:\x1B[0m`);
|
|
467
|
+
const sorted = [...watermarks].sort((a, b) => {
|
|
468
|
+
if (a.table !== b.table) return a.table.localeCompare(b.table);
|
|
469
|
+
return (a.siteId ?? "").localeCompare(b.siteId ?? "");
|
|
470
|
+
});
|
|
471
|
+
for (const w of sorted) {
|
|
472
|
+
const scope = w.siteId ? `${w.table}@${w.siteId}` : w.table;
|
|
473
|
+
console.log(` ${scope.padEnd(28)} \x1B[36m${w.oldestDateSynced}\x1B[0m → \x1B[36m${w.newestDateSynced}\x1B[0m \x1B[90m(last ${formatAge(w.lastSyncAt)})\x1B[0m`);
|
|
474
|
+
}
|
|
475
|
+
if (inflight.length > 0) {
|
|
476
|
+
console.log();
|
|
477
|
+
console.log(` \x1B[33m${inflight.length} inflight:\x1B[0m`);
|
|
478
|
+
for (const s of inflight) console.log(` ${s.table}${s.siteId ? `@${s.siteId}` : ""} ${s.date} (attempt ${s.attempts}, started ${formatAge(s.updatedAt)})`);
|
|
479
|
+
}
|
|
480
|
+
if (failed.length > 0) {
|
|
481
|
+
console.log();
|
|
482
|
+
console.log(` \x1B[31m${failed.length} failed:\x1B[0m`);
|
|
483
|
+
for (const s of failed) console.log(` ${s.table}${s.siteId ? `@${s.siteId}` : ""} ${s.date}: ${s.error ?? "unknown"}`);
|
|
484
|
+
console.log();
|
|
485
|
+
console.log(` Re-run \`gscdump sync --force\` to retry failed dates.`);
|
|
486
|
+
}
|
|
487
|
+
console.log();
|
|
488
|
+
}
|
|
489
|
+
export { syncCommand };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useCliRuntime } from "./config.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { Buffer } from "node:buffer";
|
|
6
|
+
import { SearchTypes } from "gscdump/query";
|
|
7
|
+
var version = "0.37.4";
|
|
8
|
+
const ALL_SEARCH_TYPES = Object.values(SearchTypes);
|
|
9
|
+
const VERSION = version;
|
|
10
|
+
function noSubcommandSelected(parent, subNames) {
|
|
11
|
+
const argv = useCliRuntime().rawArgs;
|
|
12
|
+
const idx = argv.indexOf(parent);
|
|
13
|
+
if (idx < 0) return true;
|
|
14
|
+
const next = argv[idx + 1];
|
|
15
|
+
if (!next) return true;
|
|
16
|
+
return !subNames.includes(next);
|
|
17
|
+
}
|
|
18
|
+
const logger = new Proxy({}, {
|
|
19
|
+
get(_target, property) {
|
|
20
|
+
const active = useCliRuntime().logger;
|
|
21
|
+
const value = active[property];
|
|
22
|
+
return typeof value === "function" ? value.bind(active) : value;
|
|
23
|
+
},
|
|
24
|
+
set(_target, property, value) {
|
|
25
|
+
const active = useCliRuntime().logger;
|
|
26
|
+
active[property] = value;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
function setQuiet(quiet) {
|
|
31
|
+
if (quiet) {
|
|
32
|
+
const runtime = useCliRuntime();
|
|
33
|
+
runtime.quiet = true;
|
|
34
|
+
runtime.logger.level = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const OUTPUT_ARGS = {
|
|
38
|
+
json: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
default: false,
|
|
41
|
+
description: "Output as JSON"
|
|
42
|
+
},
|
|
43
|
+
quiet: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
alias: "q",
|
|
46
|
+
default: false,
|
|
47
|
+
description: "Suppress info/success output"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function applyOutputMode(args) {
|
|
51
|
+
const json = Boolean(args.json);
|
|
52
|
+
const quiet = json || Boolean(args.quiet);
|
|
53
|
+
setQuiet(quiet);
|
|
54
|
+
return {
|
|
55
|
+
json,
|
|
56
|
+
quiet
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function parseSearchType(value, flag = "--search-type") {
|
|
60
|
+
if (!value) return void 0;
|
|
61
|
+
const v = String(value);
|
|
62
|
+
if (!ALL_SEARCH_TYPES.includes(v)) {
|
|
63
|
+
logger.error(`Invalid ${flag}: ${v}. Allowed: ${ALL_SEARCH_TYPES.join(", ")}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
69
|
+
function setNoColor(disable) {
|
|
70
|
+
if (disable) useCliRuntime().colorEnabled = false;
|
|
71
|
+
}
|
|
72
|
+
function configureColor(opts) {
|
|
73
|
+
if (opts.noColor || !opts.forceColor && !opts.stderrIsTTY) setNoColor(true);
|
|
74
|
+
}
|
|
75
|
+
function isColorEnabled() {
|
|
76
|
+
return useCliRuntime().colorEnabled;
|
|
77
|
+
}
|
|
78
|
+
async function withConfiguredOutput(run) {
|
|
79
|
+
if (isColorEnabled()) return run();
|
|
80
|
+
const original = process.stdout.write;
|
|
81
|
+
const write = original.bind(process.stdout);
|
|
82
|
+
process.stdout.write = ((chunk, ...rest) => {
|
|
83
|
+
if (typeof chunk === "string") chunk = chunk.replace(ANSI_RE, "");
|
|
84
|
+
else if (chunk instanceof Uint8Array) chunk = Buffer.from(chunk).toString("utf8").replace(ANSI_RE, "");
|
|
85
|
+
return write(chunk, ...rest);
|
|
86
|
+
});
|
|
87
|
+
try {
|
|
88
|
+
return await run();
|
|
89
|
+
} finally {
|
|
90
|
+
process.stdout.write = original;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const gradientColors = [
|
|
94
|
+
(s) => `\x1B[38;2;52;211;153m${s}\x1B[0m`,
|
|
95
|
+
(s) => `\x1B[38;2;45;212;191m${s}\x1B[0m`,
|
|
96
|
+
(s) => `\x1B[38;2;34;211;238m${s}\x1B[0m`,
|
|
97
|
+
(s) => `\x1B[38;2;56;189;248m${s}\x1B[0m`,
|
|
98
|
+
(s) => `\x1B[38;2;96;165;250m${s}\x1B[0m`
|
|
99
|
+
];
|
|
100
|
+
function applyGradient(text) {
|
|
101
|
+
return [...text].map((char, i) => {
|
|
102
|
+
const colorIndex = Math.floor(i / text.length * gradientColors.length);
|
|
103
|
+
return gradientColors[Math.min(colorIndex, gradientColors.length - 1)](char);
|
|
104
|
+
}).join("");
|
|
105
|
+
}
|
|
106
|
+
function showSplash() {
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(` ${applyGradient("GSC Dump")} v${VERSION}`);
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
function clearLine() {
|
|
112
|
+
process.stdout.write("\r\x1B[K");
|
|
113
|
+
}
|
|
114
|
+
function displayPath(absPath) {
|
|
115
|
+
const cwd = process.cwd();
|
|
116
|
+
const home = os.homedir();
|
|
117
|
+
if (absPath === cwd) return ".";
|
|
118
|
+
if (absPath.startsWith(`${cwd}/`)) return absPath.slice(cwd.length + 1);
|
|
119
|
+
if (absPath === home) return "~";
|
|
120
|
+
if (absPath.startsWith(`${home}/`)) return `~/${absPath.slice(home.length + 1)}`;
|
|
121
|
+
return absPath;
|
|
122
|
+
}
|
|
123
|
+
function formatAge(ms) {
|
|
124
|
+
const delta = Date.now() - ms;
|
|
125
|
+
if (delta < 6e4) return "just now";
|
|
126
|
+
if (delta < 36e5) return `${Math.floor(delta / 6e4)}m ago`;
|
|
127
|
+
if (delta < 864e5) return `${Math.floor(delta / 36e5)}h ago`;
|
|
128
|
+
return `${Math.floor(delta / 864e5)}d ago`;
|
|
129
|
+
}
|
|
130
|
+
async function runWithConcurrency(items, concurrency, processor) {
|
|
131
|
+
const cursor = { i: 0 };
|
|
132
|
+
async function worker() {
|
|
133
|
+
while (true) {
|
|
134
|
+
const i = cursor.i++;
|
|
135
|
+
if (i >= items.length) return;
|
|
136
|
+
await processor(items[i], i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
140
|
+
}
|
|
141
|
+
function toCSV(data, columns) {
|
|
142
|
+
return [columns.join(","), ...data.map((row) => columns.map((col) => {
|
|
143
|
+
const val = row[col];
|
|
144
|
+
if (val === null || val === void 0) return "";
|
|
145
|
+
const str = String(val);
|
|
146
|
+
return str.includes(",") || str.includes("\"") || str.includes("\n") ? `"${str.replace(/"/g, "\"\"")}"` : str;
|
|
147
|
+
}).join(","))].join("\n");
|
|
148
|
+
}
|
|
149
|
+
async function readUrlList(args) {
|
|
150
|
+
if (args.file) return (await fs.readFile(String(args.file), "utf-8")).split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
151
|
+
if (args.urls) return (Array.isArray(args.urls) ? args.urls : [args.urls]).map(String).filter(Boolean);
|
|
152
|
+
if (!process.stdin.isTTY) {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
155
|
+
return Buffer.concat(chunks).toString("utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
function exportToCSV(output) {
|
|
160
|
+
const sections = [];
|
|
161
|
+
if (output.pages?.data) sections.push(`# Pages\n${toCSV(output.pages.data, [
|
|
162
|
+
"url",
|
|
163
|
+
"clicks",
|
|
164
|
+
"impressions",
|
|
165
|
+
"ctr",
|
|
166
|
+
"position"
|
|
167
|
+
])}`);
|
|
168
|
+
if (output.keywords?.current) sections.push(`# Keywords (Current Period)\n${toCSV(output.keywords.current, [
|
|
169
|
+
"query",
|
|
170
|
+
"clicks",
|
|
171
|
+
"impressions",
|
|
172
|
+
"ctr",
|
|
173
|
+
"position"
|
|
174
|
+
])}`);
|
|
175
|
+
if (output.countries?.current) sections.push(`# Countries (Current Period)\n${toCSV(output.countries.current, [
|
|
176
|
+
"country",
|
|
177
|
+
"clicks",
|
|
178
|
+
"impressions",
|
|
179
|
+
"ctr",
|
|
180
|
+
"position"
|
|
181
|
+
])}`);
|
|
182
|
+
if (output.devices?.current) sections.push(`# Devices (Current Period)\n${toCSV(output.devices.current, [
|
|
183
|
+
"device",
|
|
184
|
+
"clicks",
|
|
185
|
+
"impressions",
|
|
186
|
+
"ctr",
|
|
187
|
+
"position"
|
|
188
|
+
])}`);
|
|
189
|
+
return sections.join("\n\n");
|
|
190
|
+
}
|
|
191
|
+
export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, configureColor, displayPath, exportToCSV, formatAge, logger, noSubcommandSelected, parseSearchType, readUrlList, runWithConcurrency, showSplash, toCSV, withConfiguredOutput };
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ConsolaInstance } from "consola";
|
|
2
|
+
type CliEnvironmentSource = Record<string, string | undefined>;
|
|
3
|
+
interface CliRuntime {
|
|
4
|
+
activeProfileOverride: string | null;
|
|
5
|
+
colorEnabled: boolean;
|
|
6
|
+
configDir: string;
|
|
7
|
+
configDirOverridden: boolean;
|
|
8
|
+
environment: CliEnvironmentSource;
|
|
9
|
+
logger: ConsolaInstance;
|
|
10
|
+
quiet: boolean;
|
|
11
|
+
rawArgs: string[];
|
|
12
|
+
}
|
|
13
|
+
interface CreateCliRuntimeOptions {
|
|
14
|
+
configDir?: string;
|
|
15
|
+
environment?: CliEnvironmentSource;
|
|
16
|
+
rawArgs?: readonly string[];
|
|
17
|
+
stderr?: NodeJS.WriteStream;
|
|
18
|
+
}
|
|
19
|
+
declare function createCliRuntime(opts?: CreateCliRuntimeOptions): CliRuntime;
|
|
20
|
+
declare const main: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
21
|
+
interface RunCliOptions {
|
|
22
|
+
rawArgs?: string[];
|
|
23
|
+
loadEnv?: boolean;
|
|
24
|
+
environment?: Record<string, string | undefined>;
|
|
25
|
+
runtime?: CliRuntime;
|
|
26
|
+
}
|
|
27
|
+
declare function runCli(opts?: RunCliOptions): Promise<void>;
|
|
28
|
+
export { type CliRuntime, type CreateCliRuntimeOptions, RunCliOptions, createCliRuntime, main, runCli };
|