@gscdump/cli 3.8.0 → 4.0.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.
- package/README.md +115 -30
- package/bin/gscdump.mjs +21 -1
- package/dist/analysis-local.mjs +172 -53
- package/dist/auth-state.mjs +21 -6
- package/dist/auth.mjs +27 -18
- package/dist/bing-data.mjs +6 -2
- package/dist/bing-hosted.mjs +1 -1
- package/dist/cli-args.mjs +8 -0
- package/dist/cli.d.mts +5 -1
- package/dist/cli.mjs +41 -10
- package/dist/cloud-google.mjs +8 -1
- package/dist/command-meta.mjs +4 -4
- package/dist/command-registry.mjs +86 -10
- package/dist/commands/analyze.mjs +84 -43
- package/dist/commands/auth.mjs +46 -37
- package/dist/commands/bing.mjs +3 -3
- package/dist/commands/compact.mjs +12 -8
- package/dist/commands/config.mjs +14 -14
- package/dist/commands/doctor.mjs +26 -26
- package/dist/commands/dump.mjs +203 -105
- package/dist/commands/entities.mjs +10 -113
- package/dist/commands/gc.mjs +4 -3
- package/dist/commands/indexing-urls.mjs +185 -0
- package/dist/commands/indexing.mjs +12 -8
- package/dist/commands/init.mjs +77 -18
- package/dist/commands/inspect.mjs +97 -110
- package/dist/commands/mcp.mjs +37 -57
- package/dist/commands/papercut.mjs +1 -1
- package/dist/commands/profile.mjs +9 -17
- package/dist/commands/query.mjs +219 -210
- package/dist/commands/report.mjs +68 -85
- package/dist/commands/rollups.mjs +9 -6
- package/dist/commands/sitemaps.mjs +51 -104
- package/dist/commands/sites.mjs +40 -24
- package/dist/commands/stats.mjs +14 -17
- package/dist/commands/store-purge.mjs +9 -5
- package/dist/commands/store.mjs +0 -12
- package/dist/commands/sync.mjs +774 -296
- package/dist/config.mjs +7 -4
- package/dist/context.mjs +51 -21
- package/dist/coverage.mjs +251 -0
- package/dist/dump-bing.mjs +71 -0
- package/dist/dump-writers.mjs +246 -0
- package/dist/error-handler.mjs +108 -38
- package/dist/filters.mjs +125 -0
- package/dist/hosted-site.mjs +91 -0
- package/dist/inspect-urls.mjs +76 -0
- package/dist/inspection-record.mjs +93 -0
- package/dist/local-entities.mjs +658 -0
- package/dist/mcp/errors.mjs +71 -18
- package/dist/mcp/handlers/diagnostics.mjs +15 -12
- package/dist/mcp/handlers/reports.mjs +33 -42
- package/dist/mcp/server/index.mjs +80 -38
- package/dist/mcp/types.mjs +7 -7
- package/dist/package.mjs +1 -1
- package/dist/quota-ledger.mjs +370 -0
- package/dist/render/analysis.mjs +8 -1
- package/dist/render/report.mjs +8 -1
- package/dist/request-pacer.mjs +29 -0
- package/dist/route.mjs +301 -0
- package/dist/sitemap.mjs +4 -0
- package/dist/sql-views.mjs +117 -0
- package/dist/store-sites.mjs +134 -0
- package/dist/sync-plan.mjs +129 -0
- package/dist/sync-run.mjs +96 -0
- package/dist/table-sources.mjs +57 -0
- package/dist/token-info.mjs +54 -0
- package/dist/utils.mjs +28 -19
- package/dist/window.mjs +159 -0
- package/package.json +17 -14
- package/skills/gscdump/SKILL.md +172 -37
- package/dist/commands/export.mjs +0 -76
- package/dist/native-duckdb.mjs +0 -46
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const SYNC_HEARTBEAT_STALE_MS = 12e4;
|
|
5
|
+
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
6
|
+
function syncRunStatus(record, probe) {
|
|
7
|
+
if (!record) return { kind: "none" };
|
|
8
|
+
if (record.finishedAt !== void 0) return {
|
|
9
|
+
kind: "finished",
|
|
10
|
+
record
|
|
11
|
+
};
|
|
12
|
+
if (!probe.isAlive(record.pid)) return {
|
|
13
|
+
kind: "stale",
|
|
14
|
+
record,
|
|
15
|
+
reason: "process-gone"
|
|
16
|
+
};
|
|
17
|
+
if (probe.now - record.heartbeatAt > 12e4) return {
|
|
18
|
+
kind: "stale",
|
|
19
|
+
record,
|
|
20
|
+
reason: "no-heartbeat"
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
kind: "running",
|
|
24
|
+
record
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isProcessAlive(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return error.code === "EPERM";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function syncRunPath(dataDir) {
|
|
36
|
+
return path.join(dataDir, "sync-run.json");
|
|
37
|
+
}
|
|
38
|
+
async function readSyncRun(dataDir) {
|
|
39
|
+
const body = await fs.readFile(syncRunPath(dataDir), "utf8").catch((error) => {
|
|
40
|
+
if (error.code === "ENOENT") return void 0;
|
|
41
|
+
throw error;
|
|
42
|
+
});
|
|
43
|
+
return body === void 0 ? void 0 : JSON.parse(body);
|
|
44
|
+
}
|
|
45
|
+
async function writeSyncRun(dataDir, record) {
|
|
46
|
+
await fs.mkdir(dataDir, { recursive: true });
|
|
47
|
+
const file = syncRunPath(dataDir);
|
|
48
|
+
const temporary = `${file}.${record.pid}.tmp`;
|
|
49
|
+
await fs.writeFile(temporary, JSON.stringify(record, null, 2), "utf8");
|
|
50
|
+
await fs.rename(temporary, file);
|
|
51
|
+
}
|
|
52
|
+
async function startSyncRun(opts) {
|
|
53
|
+
const now = opts.now ?? Date.now;
|
|
54
|
+
const record = {
|
|
55
|
+
pid: process.pid,
|
|
56
|
+
startedAt: now(),
|
|
57
|
+
heartbeatAt: now(),
|
|
58
|
+
sites: opts.sites,
|
|
59
|
+
planned: 0,
|
|
60
|
+
done: 0
|
|
61
|
+
};
|
|
62
|
+
let writing = Promise.resolve();
|
|
63
|
+
const save = () => {
|
|
64
|
+
record.heartbeatAt = now();
|
|
65
|
+
const snapshot = { ...record };
|
|
66
|
+
writing = writing.then(() => writeSyncRun(opts.dataDir, snapshot));
|
|
67
|
+
return writing;
|
|
68
|
+
};
|
|
69
|
+
await save();
|
|
70
|
+
const timer = setInterval(() => {
|
|
71
|
+
save().then(() => opts.onHeartbeat?.()).catch((error) => {
|
|
72
|
+
process.stderr.write(`gscdump: could not update the sync-run record: ${error.message}\n`);
|
|
73
|
+
});
|
|
74
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
75
|
+
timer.unref();
|
|
76
|
+
return {
|
|
77
|
+
record,
|
|
78
|
+
setSite(site) {
|
|
79
|
+
record.site = site;
|
|
80
|
+
},
|
|
81
|
+
addPlanned(n) {
|
|
82
|
+
record.planned += n;
|
|
83
|
+
},
|
|
84
|
+
tick(n = 1) {
|
|
85
|
+
record.done += n;
|
|
86
|
+
},
|
|
87
|
+
save,
|
|
88
|
+
async finish(outcome) {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
record.finishedAt = now();
|
|
91
|
+
record.outcome = outcome;
|
|
92
|
+
await save();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export { SYNC_HEARTBEAT_STALE_MS, isProcessAlive, readSyncRun, startSyncRun, syncRunPath, syncRunStatus };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { siteUrlForId } from "./store-sites.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SCHEMAS } from "@gscdump/engine/schema";
|
|
4
|
+
function groupTableSources(entries, dataDir, siteMap) {
|
|
5
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (!entry.siteId || entry.rowCount === 0) continue;
|
|
8
|
+
const searchType = entry.searchType ?? "web";
|
|
9
|
+
const key = `${entry.table}\u0000${entry.siteId}\u0000${searchType}`;
|
|
10
|
+
let group = groups.get(key);
|
|
11
|
+
if (!group) {
|
|
12
|
+
group = {
|
|
13
|
+
table: entry.table,
|
|
14
|
+
siteId: entry.siteId,
|
|
15
|
+
site: siteUrlForId(siteMap, entry.siteId),
|
|
16
|
+
searchType,
|
|
17
|
+
files: [],
|
|
18
|
+
rows: 0
|
|
19
|
+
};
|
|
20
|
+
groups.set(key, group);
|
|
21
|
+
}
|
|
22
|
+
group.files.push(path.join(dataDir, entry.objectKey));
|
|
23
|
+
group.rows += entry.rowCount;
|
|
24
|
+
}
|
|
25
|
+
return [...groups.values()].sort((a, b) => a.table.localeCompare(b.table) || a.site.localeCompare(b.site) || a.searchType.localeCompare(b.searchType));
|
|
26
|
+
}
|
|
27
|
+
function exportColumns(table) {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
name: "site",
|
|
31
|
+
type: "VARCHAR",
|
|
32
|
+
nullable: false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "search_type",
|
|
36
|
+
type: "VARCHAR",
|
|
37
|
+
nullable: false
|
|
38
|
+
},
|
|
39
|
+
...SCHEMAS[table].columns
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
function sqlString(value) {
|
|
43
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
44
|
+
}
|
|
45
|
+
function sourceSelectSql(source, present, opts) {
|
|
46
|
+
const columns = SCHEMAS[source.table].columns.map((column) => {
|
|
47
|
+
if (!present.has(column.name)) return `NULL::${column.type} AS ${column.name}`;
|
|
48
|
+
if (column.type !== "DATE") return column.name;
|
|
49
|
+
return opts.dates === "date" ? `CAST(${column.name} AS DATE) AS ${column.name}` : `strftime(CAST(${column.name} AS DATE), '%Y-%m-%d') AS ${column.name}`;
|
|
50
|
+
});
|
|
51
|
+
const position = opts.position ? [`sum_position / NULLIF(impressions, 0) + 1 AS position`] : [];
|
|
52
|
+
return `SELECT ${sqlString(source.site)} AS site, ${sqlString(source.searchType)} AS search_type, ${[...columns, ...position].join(", ")} FROM ${readParquetSql(source.files)}`;
|
|
53
|
+
}
|
|
54
|
+
function readParquetSql(files) {
|
|
55
|
+
return `read_parquet([${files.map(sqlString).join(", ")}], union_by_name = true)`;
|
|
56
|
+
}
|
|
57
|
+
export { exportColumns, groupTableSources, readParquetSql, sourceSelectSql, sqlString };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ofetch } from "ofetch";
|
|
2
|
+
const TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo";
|
|
3
|
+
const TIMEOUT_MS = 5e3;
|
|
4
|
+
const TOKEN_PATTERNS = [
|
|
5
|
+
/ya29\.[\w.-]+/g,
|
|
6
|
+
/1\/\/[\w.-]{20,}/g,
|
|
7
|
+
/(access_token|refresh_token)=[^&\s"']+/gi
|
|
8
|
+
];
|
|
9
|
+
function redactTokens(text, secrets = []) {
|
|
10
|
+
let out = text;
|
|
11
|
+
for (const secret of secrets) if (secret) out = out.split(secret).join("<redacted>");
|
|
12
|
+
for (const pattern of TOKEN_PATTERNS) out = out.replace(pattern, (match) => match.includes("=") ? `${match.slice(0, match.indexOf("="))}=<redacted>` : "<redacted>");
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function describeFailure(error) {
|
|
16
|
+
const data = error?.data;
|
|
17
|
+
const reason = typeof data?.error_description === "string" ? data.error_description : typeof data?.error === "string" ? data.error : void 0;
|
|
18
|
+
if (reason) return reason;
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
21
|
+
async function fetchTokenInfo(accessToken, request = (url, init) => ofetch(url, init)) {
|
|
22
|
+
return request(TOKENINFO_URL, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
body: new URLSearchParams({ access_token: accessToken }),
|
|
25
|
+
timeout: TIMEOUT_MS
|
|
26
|
+
}).then((info) => ({
|
|
27
|
+
kind: "valid",
|
|
28
|
+
info
|
|
29
|
+
}), (error) => ({
|
|
30
|
+
kind: "invalid",
|
|
31
|
+
detail: redactTokens(describeFailure(error), [accessToken])
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
async function currentAccessToken(auth) {
|
|
35
|
+
if (typeof auth === "string") return {
|
|
36
|
+
kind: "token",
|
|
37
|
+
token: auth
|
|
38
|
+
};
|
|
39
|
+
if (!("getAccessToken" in auth) || typeof auth.getAccessToken !== "function") return {
|
|
40
|
+
kind: "failed",
|
|
41
|
+
detail: "These credentials cannot produce an access token."
|
|
42
|
+
};
|
|
43
|
+
return auth.getAccessToken().then((result) => result.token ? {
|
|
44
|
+
kind: "token",
|
|
45
|
+
token: result.token
|
|
46
|
+
} : {
|
|
47
|
+
kind: "failed",
|
|
48
|
+
detail: "Google returned no access token."
|
|
49
|
+
}, (error) => ({
|
|
50
|
+
kind: "failed",
|
|
51
|
+
detail: redactTokens(describeFailure(error))
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export { currentAccessToken, fetchTokenInfo, redactTokens };
|
package/dist/utils.mjs
CHANGED
|
@@ -2,19 +2,12 @@ import { useCliRuntime } from "./runtime.mjs";
|
|
|
2
2
|
import { version } from "./package.mjs";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
5
|
import { SearchTypes } from "gscdump/query";
|
|
6
|
+
import os from "node:os";
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
8
|
+
import { MAX_FETCH_BUDGET } from "@gscdump/engine/analysis-types";
|
|
8
9
|
const ALL_SEARCH_TYPES = Object.values(SearchTypes);
|
|
9
10
|
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
11
|
const logger = new Proxy({}, {
|
|
19
12
|
get(_target, property) {
|
|
20
13
|
const active = useCliRuntime().logger;
|
|
@@ -65,6 +58,13 @@ function parseSearchType(value, flag = "--search-type") {
|
|
|
65
58
|
}
|
|
66
59
|
return v;
|
|
67
60
|
}
|
|
61
|
+
function parseNameList(value, known, flag) {
|
|
62
|
+
const names = String(value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
63
|
+
const unknown = names.filter((name) => !known.includes(name));
|
|
64
|
+
if (unknown.length > 0) throw new Error(`Unknown ${flag} name: ${unknown.join(", ")}. Valid names: ${known.join(", ")}.`);
|
|
65
|
+
if (names.length === 0) throw new Error(`${flag} needs at least one name. Valid names: ${known.join(", ")}.`);
|
|
66
|
+
return names;
|
|
67
|
+
}
|
|
68
68
|
function parseIntegerOption(value, flag, minimum = 1) {
|
|
69
69
|
if (value === void 0 || value === null) return void 0;
|
|
70
70
|
const text = typeof value === "number" || typeof value === "string" ? String(value).trim() : "";
|
|
@@ -72,6 +72,11 @@ function parseIntegerOption(value, flag, minimum = 1) {
|
|
|
72
72
|
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < minimum) throw new Error(`${flag} must be ${minimum === 0 ? "a non-negative integer" : "a positive integer"}.`);
|
|
73
73
|
return parsed;
|
|
74
74
|
}
|
|
75
|
+
function parseFetchBudget(value) {
|
|
76
|
+
const budget = parseIntegerOption(value, "--fetch-budget");
|
|
77
|
+
if (budget !== void 0 && budget > MAX_FETCH_BUDGET) throw new Error(`--fetch-budget must be at most ${MAX_FETCH_BUDGET}. Larger fetches use up the GSC quota.`);
|
|
78
|
+
return budget;
|
|
79
|
+
}
|
|
75
80
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
76
81
|
function setNoColor(disable) {
|
|
77
82
|
if (disable) useCliRuntime().colorEnabled = false;
|
|
@@ -110,6 +115,9 @@ function color(c, s) {
|
|
|
110
115
|
function dim(s) {
|
|
111
116
|
return color("dim", s);
|
|
112
117
|
}
|
|
118
|
+
function red(s) {
|
|
119
|
+
return color("red", s);
|
|
120
|
+
}
|
|
113
121
|
function cyan(s) {
|
|
114
122
|
return color("cyan", s);
|
|
115
123
|
}
|
|
@@ -174,14 +182,15 @@ function toCSV(data, columns) {
|
|
|
174
182
|
return str.includes(",") || str.includes("\"") || str.includes("\n") ? `"${str.replace(/"/g, "\"\"")}"` : str;
|
|
175
183
|
}).join(","))].join("\n");
|
|
176
184
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
function parseUrlLines(text) {
|
|
186
|
+
return text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
187
|
+
}
|
|
188
|
+
async function readUrlList(input) {
|
|
189
|
+
const urls = (input.positionals ?? []).map(String).filter(Boolean);
|
|
190
|
+
if (input.file) urls.push(...parseUrlLines(await fs.readFile(String(input.file), "utf-8")));
|
|
191
|
+
if (urls.length > 0 || input.file || process.stdin.isTTY) return urls;
|
|
192
|
+
const chunks = [];
|
|
193
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
194
|
+
return parseUrlLines(Buffer.concat(chunks).toString("utf-8"));
|
|
186
195
|
}
|
|
187
|
-
export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger,
|
|
196
|
+
export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, progressBar, readUrlList, red, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
|
package/dist/window.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { err, ok } from "gscdump/result";
|
|
2
|
+
import { resolveWindow } from "@gscdump/engine/period";
|
|
3
|
+
const PERIOD_ALIASES = {
|
|
4
|
+
"7d": "last-7d",
|
|
5
|
+
"28d": "last-28d",
|
|
6
|
+
"30d": "last-30d",
|
|
7
|
+
"90d": "last-90d",
|
|
8
|
+
"180d": "last-180d",
|
|
9
|
+
"365d": "last-365d",
|
|
10
|
+
"last-7d": "last-7d",
|
|
11
|
+
"last-28d": "last-28d",
|
|
12
|
+
"last-30d": "last-30d",
|
|
13
|
+
"last-90d": "last-90d",
|
|
14
|
+
"last-180d": "last-180d",
|
|
15
|
+
"last-365d": "last-365d",
|
|
16
|
+
"mtd": "mtd",
|
|
17
|
+
"qtd": "qtd",
|
|
18
|
+
"ytd": "ytd",
|
|
19
|
+
"last-quarter": "last-quarter",
|
|
20
|
+
"custom": "custom"
|
|
21
|
+
};
|
|
22
|
+
const PERIOD_FLAGS = [
|
|
23
|
+
"7d",
|
|
24
|
+
"28d",
|
|
25
|
+
"30d",
|
|
26
|
+
"90d",
|
|
27
|
+
"180d",
|
|
28
|
+
"365d",
|
|
29
|
+
"mtd",
|
|
30
|
+
"qtd",
|
|
31
|
+
"ytd",
|
|
32
|
+
"last-quarter",
|
|
33
|
+
"custom"
|
|
34
|
+
];
|
|
35
|
+
const COMPARISON_ALIASES = {
|
|
36
|
+
"none": "none",
|
|
37
|
+
"prev": "prev-period",
|
|
38
|
+
"prev-period": "prev-period",
|
|
39
|
+
"prior": "prev-period",
|
|
40
|
+
"prior-period": "prev-period",
|
|
41
|
+
"yoy": "yoy"
|
|
42
|
+
};
|
|
43
|
+
const COMPARISON_FLAGS = [
|
|
44
|
+
"none",
|
|
45
|
+
"prev-period",
|
|
46
|
+
"yoy"
|
|
47
|
+
];
|
|
48
|
+
const DEFAULT_WINDOW = {
|
|
49
|
+
preset: "last-28d",
|
|
50
|
+
comparison: "none"
|
|
51
|
+
};
|
|
52
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
53
|
+
function isCalendarDate(value) {
|
|
54
|
+
if (!ISO_DATE_RE.test(value)) return false;
|
|
55
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
56
|
+
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
57
|
+
}
|
|
58
|
+
function checkDate(flag, value) {
|
|
59
|
+
if (value === void 0 || isCalendarDate(value)) return void 0;
|
|
60
|
+
return {
|
|
61
|
+
kind: "invalid-date",
|
|
62
|
+
flag,
|
|
63
|
+
value,
|
|
64
|
+
message: `Invalid ${flag} "${value}". Use a calendar date in YYYY-MM-DD format.`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function checkWindowFlags(flags) {
|
|
68
|
+
for (const [flag, value] of [
|
|
69
|
+
["--start", flags.start],
|
|
70
|
+
["--end", flags.end],
|
|
71
|
+
["--prev-start", flags.prevStart],
|
|
72
|
+
["--prev-end", flags.prevEnd]
|
|
73
|
+
]) {
|
|
74
|
+
const invalid = checkDate(flag, value);
|
|
75
|
+
if (invalid) return invalid;
|
|
76
|
+
}
|
|
77
|
+
const preset = flags.period ? PERIOD_ALIASES[flags.period.toLowerCase()] : void 0;
|
|
78
|
+
if (flags.period && !preset) return {
|
|
79
|
+
kind: "unknown-period",
|
|
80
|
+
value: flags.period,
|
|
81
|
+
message: `Unknown --period "${flags.period}". Supported: ${PERIOD_FLAGS.join(", ")}.`
|
|
82
|
+
};
|
|
83
|
+
if (flags.vs && !COMPARISON_ALIASES[flags.vs.toLowerCase()]) return {
|
|
84
|
+
kind: "unknown-comparison",
|
|
85
|
+
value: flags.vs,
|
|
86
|
+
message: `Unknown --vs "${flags.vs}". Supported: ${COMPARISON_FLAGS.join(", ")}.`
|
|
87
|
+
};
|
|
88
|
+
if (flags.prevStart === void 0 !== (flags.prevEnd === void 0)) return {
|
|
89
|
+
kind: "partial-comparison",
|
|
90
|
+
message: "Pass --prev-start and --prev-end together."
|
|
91
|
+
};
|
|
92
|
+
if (preset && preset !== "custom" && (flags.start !== void 0 || flags.end !== void 0)) return {
|
|
93
|
+
kind: "period-conflict",
|
|
94
|
+
message: `--period ${flags.period} sets its own dates. Remove --start/--end, or pass --period custom.`
|
|
95
|
+
};
|
|
96
|
+
if (flags.start !== void 0 && flags.end !== void 0 && flags.start > flags.end) return {
|
|
97
|
+
kind: "inverted-range",
|
|
98
|
+
message: `Invalid date range. --start ${flags.start} is after --end ${flags.end}.`
|
|
99
|
+
};
|
|
100
|
+
if (flags.prevStart !== void 0 && flags.prevEnd !== void 0 && flags.prevStart > flags.prevEnd) return {
|
|
101
|
+
kind: "inverted-range",
|
|
102
|
+
message: `Invalid comparison range. --prev-start ${flags.prevStart} is after --prev-end ${flags.prevEnd}.`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseWindowFlags(flags, defaults, anchor) {
|
|
106
|
+
const invalid = checkWindowFlags(flags);
|
|
107
|
+
if (invalid) return err(invalid);
|
|
108
|
+
const preset = flags.period ? PERIOD_ALIASES[flags.period.toLowerCase()] : void 0;
|
|
109
|
+
const comparison = flags.vs ? COMPARISON_ALIASES[flags.vs.toLowerCase()] : defaults.comparison;
|
|
110
|
+
const hasBounds = flags.start !== void 0 || flags.end !== void 0;
|
|
111
|
+
let window;
|
|
112
|
+
if (preset === "custom" || hasBounds) {
|
|
113
|
+
const end = flags.end ?? anchor;
|
|
114
|
+
const start = flags.start ?? resolveWindow({
|
|
115
|
+
preset: defaults.preset,
|
|
116
|
+
anchor: end
|
|
117
|
+
}).start;
|
|
118
|
+
if (start > end) return err({
|
|
119
|
+
kind: "inverted-range",
|
|
120
|
+
message: `Invalid date range. --start ${start} is after --end ${end}.`
|
|
121
|
+
});
|
|
122
|
+
window = resolveWindow({
|
|
123
|
+
preset: "custom",
|
|
124
|
+
start,
|
|
125
|
+
end,
|
|
126
|
+
comparison
|
|
127
|
+
});
|
|
128
|
+
} else window = resolveWindow({
|
|
129
|
+
preset: preset ?? defaults.preset,
|
|
130
|
+
anchor,
|
|
131
|
+
comparison
|
|
132
|
+
});
|
|
133
|
+
if (flags.prevStart !== void 0 && flags.prevEnd !== void 0) window = {
|
|
134
|
+
...window,
|
|
135
|
+
comparison: {
|
|
136
|
+
start: flags.prevStart,
|
|
137
|
+
end: flags.prevEnd
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return ok(window);
|
|
141
|
+
}
|
|
142
|
+
function windowFlagErrorToException(error) {
|
|
143
|
+
const exception = new Error(error.message);
|
|
144
|
+
exception.windowFlagError = error;
|
|
145
|
+
return exception;
|
|
146
|
+
}
|
|
147
|
+
function newestDoneDate(states, tables) {
|
|
148
|
+
const wanted = tables.length ? tables : [...new Set(states.map((state) => state.table))];
|
|
149
|
+
if (!wanted.length) return void 0;
|
|
150
|
+
let anchor;
|
|
151
|
+
for (const table of wanted) {
|
|
152
|
+
let newest;
|
|
153
|
+
for (const state of states) if (state.table === table && state.state === "done" && (state.searchType ?? "web") === "web" && (!newest || state.date > newest)) newest = state.date;
|
|
154
|
+
if (!newest) return void 0;
|
|
155
|
+
if (!anchor || newest < anchor) anchor = newest;
|
|
156
|
+
}
|
|
157
|
+
return anchor;
|
|
158
|
+
}
|
|
159
|
+
export { COMPARISON_ALIASES, COMPARISON_FLAGS, DEFAULT_WINDOW, PERIOD_ALIASES, PERIOD_FLAGS, checkWindowFlags, newestDoneDate, parseWindowFlags, windowFlagErrorToException };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.1",
|
|
5
5
|
"description": "CLI for Google Search Console and Bing with hosted or local authentication, data exports, and an MCP server",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -39,28 +39,31 @@
|
|
|
39
39
|
"skills"
|
|
40
40
|
],
|
|
41
41
|
"engines": {
|
|
42
|
-
"node": ">=22"
|
|
42
|
+
"node": ">=22.13.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@clack/prompts": "^1.8.
|
|
46
|
-
"@duckdb/node-api": "1.5.5-r.
|
|
47
|
-
"@gscdump/analysis": "^
|
|
48
|
-
"@gscdump/
|
|
49
|
-
"@gscdump/engine
|
|
50
|
-
"@gscdump/
|
|
51
|
-
"@
|
|
45
|
+
"@clack/prompts": "^1.8.1",
|
|
46
|
+
"@duckdb/node-api": "1.5.5-r.5",
|
|
47
|
+
"@gscdump/analysis": "^4.0.1",
|
|
48
|
+
"@gscdump/contracts": "^4.0.1",
|
|
49
|
+
"@gscdump/engine": "^4.0.1",
|
|
50
|
+
"@gscdump/engine-gsc-api": "^4.0.1",
|
|
51
|
+
"@gscdump/sdk": "^4.0.1",
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
52
53
|
"citty": "^0.2.2",
|
|
53
54
|
"consola": "^3.4.2",
|
|
54
|
-
"google-auth-library": "^11.0
|
|
55
|
-
"gscdump": "^
|
|
55
|
+
"google-auth-library": "^11.1.0",
|
|
56
|
+
"gscdump": "^4.0.1",
|
|
56
57
|
"ofetch": "^1.5.1",
|
|
57
|
-
"open": "^11.0.
|
|
58
|
+
"open": "^11.0.4",
|
|
59
|
+
"proper-lockfile": "^4.1.2",
|
|
58
60
|
"sitemapd": "^0.2.2",
|
|
59
61
|
"string-width": "^8.2.2",
|
|
60
|
-
"zod": "^4.6.
|
|
62
|
+
"zod": "^4.6.5"
|
|
61
63
|
},
|
|
62
64
|
"devDependencies": {
|
|
63
|
-
"
|
|
65
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
66
|
+
"vitest": "^5.0.1"
|
|
64
67
|
},
|
|
65
68
|
"scripts": {
|
|
66
69
|
"build": "obuild",
|