@gscdump/cli 5.0.0 → 5.1.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.
- package/dist/analysis-local.mjs +2 -2
- package/dist/auth-state.mjs +41 -6
- package/dist/bing-hosted.mjs +26 -5
- package/dist/cli.mjs +4 -2
- package/dist/commands/analyze.mjs +2 -2
- package/dist/commands/auth.mjs +1 -1
- package/dist/commands/bing.mjs +7 -2
- package/dist/commands/doctor.mjs +2 -2
- package/dist/commands/indexing-urls.mjs +2 -2
- package/dist/commands/indexing-watch.mjs +10 -7
- package/dist/commands/init.mjs +1 -1
- package/dist/commands/mcp.mjs +1 -1
- package/dist/commands/papercut.mjs +1 -1
- package/dist/commands/profile.mjs +1 -1
- package/dist/commands/query.mjs +6 -6
- package/dist/commands/report.mjs +2 -2
- package/dist/commands/sitemaps.mjs +1 -1
- package/dist/commands/sites.mjs +3 -3
- package/dist/config.mjs +1 -1
- package/dist/context.mjs +3 -3
- package/dist/error-handler.mjs +30 -16
- package/dist/hosted-site.mjs +37 -17
- package/dist/mcp/errors.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/route.mjs +5 -9
- package/dist/runtime.d.mts +2 -0
- package/dist/runtime.mjs +1 -0
- package/dist/stop.mjs +10 -0
- package/dist/utils.mjs +6 -5
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +36 -4
package/dist/analysis-local.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LocalStoreUnsupportedError } from "./error-handler.mjs";
|
|
2
1
|
import { useCliRuntime } from "./runtime.mjs";
|
|
2
|
+
import { LocalStoreUnsupportedError } from "./error-handler.mjs";
|
|
3
3
|
import { logger } from "./utils.mjs";
|
|
4
4
|
import { inferTable } from "./local-store.mjs";
|
|
5
5
|
import { createCommandContext } from "./context.mjs";
|
|
@@ -174,7 +174,7 @@ async function resolveAnalysisSource(args) {
|
|
|
174
174
|
auth
|
|
175
175
|
});
|
|
176
176
|
const route = decideRoute(req, state);
|
|
177
|
-
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, state.auth
|
|
177
|
+
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, state.auth);
|
|
178
178
|
if (route.kind === "local") {
|
|
179
179
|
const source = createEngineQuerySource({
|
|
180
180
|
engine: store.engine,
|
package/dist/auth-state.mjs
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { HOSTED_KEY_REJECTED } from "./error-handler.mjs";
|
|
2
1
|
import { useCliRuntime } from "./runtime.mjs";
|
|
2
|
+
import { stopError } from "./stop.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
const HOSTED_SESSION_REJECTED = "gscdump.com rejected the CLI session. Run `gscdump auth login --mode hosted` again.";
|
|
8
|
+
const HOSTED_KEY_REJECTED = "gscdump.com rejected the API key. Run `gscdump auth login --mode hosted --api-key KEY` with a valid key.";
|
|
8
9
|
const LOCAL_MODE_REQUIRED = [
|
|
9
10
|
"This command calls Google, so it needs Local mode.",
|
|
10
11
|
"Hosted mode reads your gscdump.com record and never calls Google.",
|
|
11
12
|
"If you want Local mode, run `gscdump auth login --mode local`."
|
|
12
13
|
].join("\n");
|
|
14
|
+
function localModeRequired(detail) {
|
|
15
|
+
return stopError({
|
|
16
|
+
code: "LOCAL_MODE_REQUIRED",
|
|
17
|
+
message: detail ? `${detail}\n${LOCAL_MODE_REQUIRED}` : LOCAL_MODE_REQUIRED,
|
|
18
|
+
nextCommand: "gscdump auth login --mode local"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
13
21
|
const HOSTED_NOT_SET_UP = "Hosted credentials are missing. Run `gscdump auth login --mode hosted`, or set GSCDUMP_API_KEY.";
|
|
14
22
|
const SAVED_STATE_INVALID = "The saved access mode is invalid. Run `gscdump auth login --mode local` or `gscdump auth login --mode hosted`.";
|
|
15
23
|
const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).refine((value) => {
|
|
@@ -129,7 +137,11 @@ async function resolveAuthentication() {
|
|
|
129
137
|
if (env.GSCDUMP_API_ROOT && env.GSCDUMP_API_ROOT.replace(/\/+$/, "") !== state.apiRoot) throw new Error("The API root changed. Run `gscdump auth login --mode hosted` for the new API root.");
|
|
130
138
|
return state;
|
|
131
139
|
}
|
|
132
|
-
throw
|
|
140
|
+
throw stopError({
|
|
141
|
+
code: "HOSTED_CREDENTIALS_MISSING",
|
|
142
|
+
message: HOSTED_NOT_SET_UP,
|
|
143
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
144
|
+
});
|
|
133
145
|
}
|
|
134
146
|
const hostedSiteSchema = z.object({
|
|
135
147
|
siteId: z.string(),
|
|
@@ -162,18 +174,41 @@ async function hostedRequest(state, route, options = {}) {
|
|
|
162
174
|
redirect: "error"
|
|
163
175
|
});
|
|
164
176
|
if (!response.ok) {
|
|
165
|
-
const
|
|
166
|
-
throw Object.assign(new Error(message), {
|
|
177
|
+
const details = {
|
|
167
178
|
statusCode: response.status,
|
|
168
179
|
retryAfter: response.headers.get("retry-after"),
|
|
169
180
|
response
|
|
170
|
-
}
|
|
181
|
+
};
|
|
182
|
+
if (response.status === 401) {
|
|
183
|
+
const session = "sessionId" in state;
|
|
184
|
+
throw Object.assign(stopError({
|
|
185
|
+
code: "HOSTED_CREDENTIALS_REJECTED",
|
|
186
|
+
message: session ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED,
|
|
187
|
+
nextCommand: session ? "gscdump auth login --mode hosted" : null
|
|
188
|
+
}), details);
|
|
189
|
+
}
|
|
190
|
+
throw Object.assign(/* @__PURE__ */ new Error(`Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`), details);
|
|
171
191
|
}
|
|
172
192
|
return response.status === 204 ? void 0 : response.json();
|
|
173
193
|
}
|
|
174
194
|
function hostedCredential(state) {
|
|
175
195
|
return "sessionId" in state ? state.sessionId : state.apiKey;
|
|
176
196
|
}
|
|
197
|
+
function siteManagerOf(state) {
|
|
198
|
+
return "sessionId" in state ? { _tag: "gscdump" } : { _tag: "key_issuer" };
|
|
199
|
+
}
|
|
200
|
+
function connectSiteStep(manager) {
|
|
201
|
+
switch (manager._tag) {
|
|
202
|
+
case "gscdump": return "Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites.";
|
|
203
|
+
case "key_issuer": return "Connect a Site in the app that issued this API key.";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function siteSettingsPlace(manager) {
|
|
207
|
+
switch (manager._tag) {
|
|
208
|
+
case "gscdump": return "in the Site settings on gscdump.com";
|
|
209
|
+
case "key_issuer": return "in the Site settings of the app that issued this API key";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
177
212
|
async function getHostedAccount(state) {
|
|
178
213
|
const result = accountSchema.safeParse(await hostedRequest(state, "/cli/me"));
|
|
179
214
|
if (!result.success) throw new Error("gscdump.com returned invalid account data.");
|
|
@@ -183,4 +218,4 @@ function formatHostedSync(site) {
|
|
|
183
218
|
const status = site.syncStatus ?? "pending";
|
|
184
219
|
return `${status}${site.syncProgress && site.syncProgress.total > 0 && status !== "synced" ? `: ${site.syncProgress.completed.toLocaleString("en-US")} of ${site.syncProgress.total.toLocaleString("en-US")} days (${Math.round(site.syncProgress.percent)}%)` : ""}${status === "synced" && site.oldestDateSynced && site.newestDateSynced ? `: ${site.oldestDateSynced} to ${site.newestDateSynced}` : ""}`;
|
|
185
220
|
}
|
|
186
|
-
export { HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication };
|
|
221
|
+
export { HOSTED_KEY_REJECTED, HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, connectSiteStep, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, localModeRequired, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication, siteManagerOf, siteSettingsPlace };
|
package/dist/bing-hosted.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stopError } from "./stop.mjs";
|
|
2
|
+
import { getHostedAccount, hostedCredential, siteManagerOf } from "./auth-state.mjs";
|
|
2
3
|
import { logger } from "./utils.mjs";
|
|
3
4
|
import { writeBingDump } from "./bing-data.mjs";
|
|
5
|
+
import { noSitesStop } from "./hosted-site.mjs";
|
|
4
6
|
import { createGscdumpV1Client } from "@gscdump/sdk/v1";
|
|
7
|
+
const BING_SITES_COMMAND = "gscdump bing sites --json";
|
|
5
8
|
function hostedBingClient(state) {
|
|
6
9
|
return createGscdumpV1Client({
|
|
7
10
|
apiRoot: state.apiRoot,
|
|
@@ -30,15 +33,33 @@ async function loadHostedBingConnections(state, sites, options = {}) {
|
|
|
30
33
|
async function resolveHostedBingSites(state, input) {
|
|
31
34
|
if (Boolean(input.site) === Boolean(input.allSites)) throw new Error("Choose --site or --all-sites.");
|
|
32
35
|
const account = await getHostedAccount(state);
|
|
36
|
+
if (account.sites.length === 0) throw stopError(noSitesStop(siteManagerOf(state)));
|
|
33
37
|
const exact = account.sites.filter((site) => site.siteId === input.site || site.siteUrl === input.site);
|
|
34
38
|
const normalize = (value) => URL.parse(value.startsWith("sc-domain:") ? `https://${value.slice(10)}/` : value)?.toString();
|
|
35
39
|
const requested = input.allSites ? account.sites : exact.length ? exact : account.sites.filter((site) => normalize(site.siteUrl) === normalize(input.site) && normalize(input.site) !== void 0);
|
|
36
|
-
if (requested.length === 0) throw
|
|
37
|
-
|
|
40
|
+
if (requested.length === 0) throw stopError({
|
|
41
|
+
code: "SITE_NOT_FOUND",
|
|
42
|
+
message: "No matching Bing site. Run `gscdump bing sites`.",
|
|
43
|
+
nextCommand: BING_SITES_COMMAND
|
|
44
|
+
});
|
|
45
|
+
if (!input.allSites && requested.length > 1) throw stopError({
|
|
46
|
+
code: "SITE_AMBIGUOUS",
|
|
47
|
+
message: "Multiple Bing sites match. Use a site ID from `gscdump bing sites`.",
|
|
48
|
+
nextCommand: BING_SITES_COMMAND
|
|
49
|
+
});
|
|
38
50
|
const connections = await loadHostedBingConnections(state, requested, { tolerateFailures: Boolean(input.allSites) });
|
|
39
51
|
const selected = input.allSites ? connections.filter((site) => site.connection._tag === "connected") : connections;
|
|
40
|
-
if (selected.length === 0) throw
|
|
41
|
-
|
|
52
|
+
if (selected.length === 0) throw stopError({
|
|
53
|
+
code: "SITE_NOT_FOUND",
|
|
54
|
+
message: "No matching Bing site. Run `gscdump bing sites`.",
|
|
55
|
+
nextCommand: BING_SITES_COMMAND
|
|
56
|
+
});
|
|
57
|
+
const unconnected = input.requireConnected === false ? void 0 : selected.find((site) => site.connection._tag !== "connected");
|
|
58
|
+
if (unconnected) throw stopError({
|
|
59
|
+
code: "BING_NOT_CONNECTED",
|
|
60
|
+
message: `Bing is not connected for ${unconnected.siteUrl}. Run \`gscdump bing login --site ${unconnected.siteId}\`.`,
|
|
61
|
+
nextCommand: `gscdump bing login --site ${unconnected.siteId}`
|
|
62
|
+
});
|
|
42
63
|
return selected;
|
|
43
64
|
}
|
|
44
65
|
async function dumpHostedBingSite(state, site, outDir, options) {
|
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
|
|
2
|
-
import { reportCliError } from "./error-handler.mjs";
|
|
3
1
|
import { createCliRuntime, runWithCliRuntime, useCliRuntime } from "./runtime.mjs";
|
|
4
2
|
import { parseAuthMode } from "./auth-state.mjs";
|
|
5
3
|
import { checkCliArgs } from "./cli-args.mjs";
|
|
4
|
+
import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
|
|
6
5
|
import { resolveCliEnvironment } from "./environment.mjs";
|
|
7
6
|
import { applyProfileFromCli } from "./commands/profile-selection.mjs";
|
|
7
|
+
import { reportCliError } from "./error-handler.mjs";
|
|
8
8
|
import { resolveOutputOptions, terminalOutputOptions } from "./render/terminal.mjs";
|
|
9
9
|
import { VERSION, setNoColor, showSplash, withConfiguredOutput } from "./utils.mjs";
|
|
10
10
|
import process from "node:process";
|
|
@@ -96,6 +96,7 @@ async function runCli(opts = {}) {
|
|
|
96
96
|
runtime.rawArgs = [...input];
|
|
97
97
|
return runWithCliRuntime(runtime, async () => {
|
|
98
98
|
let rawArgs = [...input];
|
|
99
|
+
runtime.jsonOutput = false;
|
|
99
100
|
try {
|
|
100
101
|
rawArgs = prepareCliArgs(input);
|
|
101
102
|
runtime.rawArgs = [...rawArgs];
|
|
@@ -106,6 +107,7 @@ async function runCli(opts = {}) {
|
|
|
106
107
|
} catch (error) {
|
|
107
108
|
await reportCliError(error, {
|
|
108
109
|
color: stderrColor(runtime, rawArgs),
|
|
110
|
+
json: runtime.jsonOutput,
|
|
109
111
|
usage: async () => renderUsage(...await resolveUsageTarget(main, rawArgs)),
|
|
110
112
|
authSources: async () => (await import("./auth.mjs")).formatAuthProvenance()
|
|
111
113
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { analyzeCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
3
|
-
import { logger, parseFetchBudget, parseIntegerOption, toCSV } from "../utils.mjs";
|
|
3
|
+
import { logger, parseFetchBudget, parseIntegerOption, setJsonOutput, toCSV } from "../utils.mjs";
|
|
4
4
|
import { DEFAULT_WINDOW, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
|
|
5
5
|
import { analysisNeeds, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
|
|
6
6
|
import { coverageWarning, renderAnalysis } from "../render/analysis.mjs";
|
|
@@ -176,10 +176,10 @@ function makeToolCommand(tool) {
|
|
|
176
176
|
].includes(args.format ?? "table")) throw new Error("Invalid --format. Use table, json, or csv.");
|
|
177
177
|
const baseParams = buildParams(tool, args);
|
|
178
178
|
const format = args.json ? "json" : String(args.format ?? "table");
|
|
179
|
+
setJsonOutput(format === "json");
|
|
179
180
|
const { runAnalysis, siteUrl, anchor } = await resolveAnalysisSource({
|
|
180
181
|
site: args.site,
|
|
181
182
|
live: !!args.live,
|
|
182
|
-
json: format === "json",
|
|
183
183
|
label: `analyze ${tool}`,
|
|
184
184
|
types: [tool],
|
|
185
185
|
anchorTables: analyzerTables(baseParams),
|
package/dist/commands/auth.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { authCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
3
2
|
import { clearAuthentication, formatHostedSync, getHostedAccount, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication } from "../auth-state.mjs";
|
|
3
|
+
import { authCommandMeta } from "../command-meta.mjs";
|
|
4
4
|
import { loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
6
6
|
import { ACCESS_NOT_SET_UP, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
package/dist/commands/bing.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { bingCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { stopError } from "../stop.mjs";
|
|
3
3
|
import { resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
|
|
4
|
+
import { bingCommandMeta } from "../command-meta.mjs";
|
|
4
5
|
import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
|
|
5
6
|
import { BING_REDIRECT_URI, clearBingCredentials, getBingClient, loginBingOAuth, saveBingCredentials } from "../bing-auth.mjs";
|
|
6
7
|
import { BING_DATASETS, dumpBingSite, parseBingDumpOptions, resolveBingSites, unwrapBing } from "../bing-data.mjs";
|
|
@@ -323,7 +324,11 @@ const bingCommand = defineCommand({
|
|
|
323
324
|
async run({ args }) {
|
|
324
325
|
applyOutputMode(args);
|
|
325
326
|
const authentication = await resolveAuthentication();
|
|
326
|
-
if (authentication._tag !== "Hosted") throw
|
|
327
|
+
if (authentication._tag !== "Hosted") throw stopError({
|
|
328
|
+
code: "HOSTED_MODE_REQUIRED",
|
|
329
|
+
message: "Hosted Bing connection verification needs Hosted mode. Pass --mode hosted.",
|
|
330
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
331
|
+
});
|
|
327
332
|
const [site] = await resolveHostedBingSites(authentication, {
|
|
328
333
|
site: args.site,
|
|
329
334
|
requireConnected: false
|
package/dist/commands/doctor.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { connectSiteStep, getHostedAccount, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
|
|
1
2
|
import { doctorCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
|
|
3
3
|
import { loadConfig } from "../config.mjs";
|
|
4
4
|
import { pickCliEnvironmentValue, resolveCliEnvironment } from "../environment.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
@@ -339,7 +339,7 @@ async function checkHostedConnection(authentication) {
|
|
|
339
339
|
} : {
|
|
340
340
|
name: "hosted.sites",
|
|
341
341
|
status: "warn",
|
|
342
|
-
detail:
|
|
342
|
+
detail: `no hosted Sites. ${connectSiteStep(siteManagerOf(authentication))}`
|
|
343
343
|
}];
|
|
344
344
|
return [
|
|
345
345
|
{
|
|
@@ -121,8 +121,8 @@ const indexingUrlsCommand = defineCommand({
|
|
|
121
121
|
if (limit > MAX_PAGE_SIZE) throw new Error(`--limit must be ${MAX_PAGE_SIZE} or less. Use --all to read every page.`);
|
|
122
122
|
const search = args.search ? String(args.search) : void 0;
|
|
123
123
|
applyOutputMode({
|
|
124
|
-
json: format
|
|
125
|
-
quiet: args.quiet
|
|
124
|
+
json: format === "json",
|
|
125
|
+
quiet: Boolean(args.quiet) || format === "csv"
|
|
126
126
|
});
|
|
127
127
|
const filters = {
|
|
128
128
|
...status ? { status } : {},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { siteManagerOf, siteSettingsPlace } from "../auth-state.mjs";
|
|
1
2
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
2
3
|
import { applyOutputMode, logger, readUrlList } from "../utils.mjs";
|
|
3
4
|
import { renderTable } from "../render/layout.mjs";
|
|
@@ -39,12 +40,14 @@ function watchedRow(entry) {
|
|
|
39
40
|
dueAt: entry.dueAt.slice(0, 10)
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
function skipHint(reason, manager) {
|
|
44
|
+
return reason === "inspection_disabled" ? `gscdump never inspects this Site, so a Watched URL gets no Checkpoint. To use Watched URLs, turn on URL Inspection ${siteSettingsPlace(manager)}.` : void 0;
|
|
45
|
+
}
|
|
46
|
+
function printChange(verb, result, siteUrl, manager) {
|
|
44
47
|
for (const url of result.changed) console.log(`${verb}: ${url}`);
|
|
45
48
|
for (const url of result.unchanged) console.log(`${verb === "Added" ? "Already watched" : "Not watched"}: ${url}`);
|
|
46
49
|
for (const { url, reason } of result.skipped) console.log(`Skipped (${reason}): ${url}`);
|
|
47
|
-
for (const hint of new Set(result.skipped.map(({ reason }) =>
|
|
50
|
+
for (const hint of new Set(result.skipped.map(({ reason }) => skipHint(reason, manager)).filter(Boolean))) console.log(hint);
|
|
48
51
|
logger.info(`${siteUrl} has ${result.total} of ${result.limit} Watched URLs.`);
|
|
49
52
|
}
|
|
50
53
|
async function readUrls(args) {
|
|
@@ -133,7 +136,7 @@ const addCommand = defineCommand({
|
|
|
133
136
|
async run({ args }) {
|
|
134
137
|
const { json } = applyOutputMode(args);
|
|
135
138
|
const urls = await readUrls(args);
|
|
136
|
-
const { client, site } = await resolveHostedSite({
|
|
139
|
+
const { client, site, authentication } = await resolveHostedSite({
|
|
137
140
|
...args,
|
|
138
141
|
_: []
|
|
139
142
|
}, {
|
|
@@ -148,7 +151,7 @@ const addCommand = defineCommand({
|
|
|
148
151
|
console.log(JSON.stringify(data, null, 2));
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
|
-
printChange("Added", data, site.siteUrl);
|
|
154
|
+
printChange("Added", data, site.siteUrl, siteManagerOf(authentication));
|
|
152
155
|
}
|
|
153
156
|
});
|
|
154
157
|
const removeCommand = defineCommand({
|
|
@@ -164,7 +167,7 @@ const removeCommand = defineCommand({
|
|
|
164
167
|
async run({ args }) {
|
|
165
168
|
const { json } = applyOutputMode(args);
|
|
166
169
|
const urls = await readUrls(args);
|
|
167
|
-
const { client, site } = await resolveHostedSite({
|
|
170
|
+
const { client, site, authentication } = await resolveHostedSite({
|
|
168
171
|
...args,
|
|
169
172
|
_: []
|
|
170
173
|
}, {
|
|
@@ -179,7 +182,7 @@ const removeCommand = defineCommand({
|
|
|
179
182
|
console.log(JSON.stringify(data, null, 2));
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
182
|
-
printChange("Removed", data, site.siteUrl);
|
|
185
|
+
printChange("Removed", data, site.siteUrl, siteManagerOf(authentication));
|
|
183
186
|
}
|
|
184
187
|
});
|
|
185
188
|
const indexingWatchCommand = defineCommand({
|
package/dist/commands/init.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { initCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
3
2
|
import { saveAuthentication } from "../auth-state.mjs";
|
|
3
|
+
import { initCommandMeta } from "../command-meta.mjs";
|
|
4
4
|
import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
6
6
|
import { ACCESS_NOT_SET_UP, getAuth, loadTokens, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
|
package/dist/commands/mcp.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mcpCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { runWithCliRuntime, useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { mcpCommandMeta } from "../command-meta.mjs";
|
|
3
3
|
import { VERSION } from "../utils.mjs";
|
|
4
4
|
import { probeAuth } from "../auth.mjs";
|
|
5
5
|
import { createCommandContext } from "../context.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { papercutCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { papercutCommandMeta } from "../command-meta.mjs";
|
|
3
3
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
4
4
|
import { buildPapercutBody, resolvePapercutUrl, submitPapercut } from "../papercut.mjs";
|
|
5
5
|
import process from "node:process";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { profileCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { profileCommandMeta } from "../command-meta.mjs";
|
|
3
3
|
import { getConfigDir, setConfigDir } from "../config.mjs";
|
|
4
4
|
import { resolveCliEnvironment } from "../environment.mjs";
|
|
5
5
|
import { ACTIVE_MARKER, PROFILES_DIR, ROOT_DIR, getProfileDir, readActiveMarkerSync } from "./profile-selection.mjs";
|
package/dist/commands/query.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { queryCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { queryHostedRows } from "../hosted-query.mjs";
|
|
3
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
4
2
|
import { resolveAuthentication } from "../auth-state.mjs";
|
|
3
|
+
import { queryCommandMeta } from "../command-meta.mjs";
|
|
5
4
|
import { loadConfig } from "../config.mjs";
|
|
5
|
+
import { queryHostedRows } from "../hosted-query.mjs";
|
|
6
6
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
7
|
-
import { ALL_SEARCH_TYPES, logger, parseSearchType, toCSV } from "../utils.mjs";
|
|
7
|
+
import { ALL_SEARCH_TYPES, logger, parseSearchType, setJsonOutput, toCSV } from "../utils.mjs";
|
|
8
8
|
import { allTables } from "../local-store.mjs";
|
|
9
9
|
import { createCommandContext, formatSiteResolution } from "../context.mjs";
|
|
10
10
|
import { decideRoute, describeStop, liveNote, readRouteState, readSiteStates, resolveReadSite, stopAtRoute } from "../route.mjs";
|
|
@@ -215,6 +215,7 @@ const queryCommand = defineCommand({
|
|
|
215
215
|
logger.error("Invalid --format. Use table, json, or csv.");
|
|
216
216
|
process.exit(1);
|
|
217
217
|
}
|
|
218
|
+
setJsonOutput(format === "json");
|
|
218
219
|
if (args.sql || args.schema) {
|
|
219
220
|
await runSqlMode({
|
|
220
221
|
mode: args.schema ? { kind: "schema" } : {
|
|
@@ -341,7 +342,6 @@ const queryCommand = defineCommand({
|
|
|
341
342
|
auth
|
|
342
343
|
});
|
|
343
344
|
const route = decideRoute(req, routeState);
|
|
344
|
-
const json = format === "json";
|
|
345
345
|
if (route.kind === "live") {
|
|
346
346
|
const live = await connect();
|
|
347
347
|
const siteUrl = site ?? await live.resolveSite(void 0);
|
|
@@ -406,7 +406,7 @@ const queryCommand = defineCommand({
|
|
|
406
406
|
}, null, 2));
|
|
407
407
|
return;
|
|
408
408
|
}
|
|
409
|
-
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth
|
|
409
|
+
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth);
|
|
410
410
|
if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
|
|
411
411
|
const siteUrl = site;
|
|
412
412
|
if (!args.quiet) logger.debug(`Querying ${siteUrl} from local Parquet store...`);
|
|
@@ -595,7 +595,7 @@ async function runSqlMode(opts) {
|
|
|
595
595
|
});
|
|
596
596
|
if (route.kind === "syncing" || route.kind === "prompt") {
|
|
597
597
|
views.close();
|
|
598
|
-
stopAtRoute(route, req, routeState.auth
|
|
598
|
+
stopAtRoute(route, req, routeState.auth);
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
}
|
package/dist/commands/report.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reportCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
3
|
-
import { logger, parseFetchBudget } from "../utils.mjs";
|
|
3
|
+
import { logger, parseFetchBudget, setJsonOutput } from "../utils.mjs";
|
|
4
4
|
import { COMPARISON_FLAGS, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
|
|
5
5
|
import { analysisNeeds, analyzerSources, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
|
|
6
6
|
import { renderCliReport } from "../render/report.mjs";
|
|
@@ -127,6 +127,7 @@ function makeReportCommand(report) {
|
|
|
127
127
|
...reportArgs
|
|
128
128
|
},
|
|
129
129
|
async run({ args }) {
|
|
130
|
+
setJsonOutput(Boolean(args.json));
|
|
130
131
|
const flags = windowFlags(args);
|
|
131
132
|
const params = buildReportParams(report, args);
|
|
132
133
|
const fetchBudget = parseFetchBudget(args["fetch-budget"]);
|
|
@@ -158,7 +159,6 @@ function makeReportCommand(report) {
|
|
|
158
159
|
const { source, siteUrl, anchor, isLive } = await resolveAnalysisSource({
|
|
159
160
|
site: args.site,
|
|
160
161
|
live: !!args.live,
|
|
161
|
-
json: !!args.json,
|
|
162
162
|
label: `report ${report.id}`,
|
|
163
163
|
types: provisional.map((step) => step.type),
|
|
164
164
|
sources: reportSources(provisional),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sitemapsCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, plural } from "../utils.mjs";
|
|
3
3
|
import { createCommandContext } from "../context.mjs";
|
|
4
|
-
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
5
4
|
import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
|
|
5
|
+
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import { fetchSitemap } from "gscdump/sites";
|
package/dist/commands/sites.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { connectSiteStep, formatHostedSync, getHostedAccount, localModeRequired, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
|
|
1
2
|
import { sitesCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { LOCAL_MODE_REQUIRED, formatHostedSync, getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
|
|
3
3
|
import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
|
|
4
4
|
import { createCommandContext, formatSiteResolution } from "../context.mjs";
|
|
5
5
|
import process from "node:process";
|
|
@@ -422,7 +422,7 @@ const LIST_ARGS = {
|
|
|
422
422
|
}
|
|
423
423
|
};
|
|
424
424
|
async function runListHostedSites(args, authentication) {
|
|
425
|
-
if (args["with-sitemaps"] || args["owner-only"]) throw
|
|
425
|
+
if (args["with-sitemaps"] || args["owner-only"]) throw localModeRequired("--with-sitemaps and --owner-only read Search Console permissions.");
|
|
426
426
|
const { sites } = await getHostedAccount(authentication);
|
|
427
427
|
if (args.json) {
|
|
428
428
|
console.log(JSON.stringify(sites.map((site) => ({
|
|
@@ -438,7 +438,7 @@ async function runListHostedSites(args, authentication) {
|
|
|
438
438
|
return;
|
|
439
439
|
}
|
|
440
440
|
if (sites.length === 0) {
|
|
441
|
-
logger.warn(
|
|
441
|
+
logger.warn(`Your hosted record has no Sites. ${connectSiteStep(siteManagerOf(authentication))}`);
|
|
442
442
|
return;
|
|
443
443
|
}
|
|
444
444
|
logger.success(`Found ${plural(sites.length, "hosted Site", "hosted Sites")}:`);
|
package/dist/config.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { useCliRuntime } from "./runtime.mjs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { SearchTypes } from "gscdump/query";
|
|
6
5
|
import os from "node:os";
|
|
6
|
+
import { SearchTypes } from "gscdump/query";
|
|
7
7
|
function setConfigDir(dir) {
|
|
8
8
|
useCliRuntime().configDir = dir;
|
|
9
9
|
}
|
package/dist/context.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LOCAL_MODE_REQUIRED, resolveAuthentication } from "./auth-state.mjs";
|
|
1
|
+
import { localModeRequired, resolveAuthentication } from "./auth-state.mjs";
|
|
3
2
|
import { loadResolvedConfig } from "./config.mjs";
|
|
3
|
+
import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
|
|
4
4
|
import { resolveAuth } from "./auth.mjs";
|
|
5
5
|
import { createLocalStore } from "./local-store.mjs";
|
|
6
6
|
import { listStoreSites } from "./store-sites.mjs";
|
|
@@ -12,7 +12,7 @@ const FULL_SITE_URL_RE = /^(?:sc-domain:|https?:\/\/)/i;
|
|
|
12
12
|
async function createCommandContext(opts = {}) {
|
|
13
13
|
const { needsAuth = false, needsStore = false, interactive = false, byok, quota = true } = opts;
|
|
14
14
|
const { config, dataDir } = await loadResolvedConfig();
|
|
15
|
-
if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw
|
|
15
|
+
if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw localModeRequired();
|
|
16
16
|
const fetchOptions = needsAuth && quota ? quotaFetchOptions(await openQuotaLedger({ dataDir }), opts.fetchOptions) : opts.fetchOptions;
|
|
17
17
|
const auth = needsAuth ? await resolveAuth({
|
|
18
18
|
interactive,
|
package/dist/error-handler.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { stopDetailsOf } from "./stop.mjs";
|
|
1
2
|
import { isUsageError } from "./command-registry.mjs";
|
|
2
3
|
import { describeRecordReadRefusal } from "./hosted-query.mjs";
|
|
3
4
|
import { quotaStopOf } from "./quota-ledger.mjs";
|
|
5
|
+
import { isQueryError } from "gscdump/query";
|
|
4
6
|
import { isAnalysisError } from "@gscdump/analysis/errors";
|
|
5
7
|
import { parseRecordReadRefusal } from "@gscdump/contracts";
|
|
6
8
|
import { isEngineError } from "@gscdump/engine/errors";
|
|
7
9
|
import { classifyError } from "gscdump/errors";
|
|
8
|
-
import { isQueryError } from "gscdump/query";
|
|
9
|
-
const HOSTED_KEY_REJECTED_REASON = "gscdump.com rejected the API key";
|
|
10
|
-
const HOSTED_KEY_REJECTED = `${HOSTED_KEY_REJECTED_REASON}. Run \`gscdump auth login --mode hosted --api-key KEY\` with a valid key.`;
|
|
11
10
|
const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
|
|
12
11
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
13
12
|
function suggestionForGscError(error) {
|
|
@@ -109,19 +108,35 @@ function recordReadRefusalOf(error) {
|
|
|
109
108
|
if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return null;
|
|
110
109
|
return parseRecordReadRefusal(error.details);
|
|
111
110
|
}
|
|
111
|
+
function recordReadStop(refusal, text) {
|
|
112
|
+
return {
|
|
113
|
+
code: refusal.reason === "record_not_ready" ? "RECORD_NOT_READY" : "RANGE_NOT_SYNCED",
|
|
114
|
+
message: text.hint ? `${text.message} ${text.hint}` : text.message,
|
|
115
|
+
nextCommand: null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
112
118
|
function describeCliError(error) {
|
|
113
119
|
if (isUsageError(error)) return {
|
|
114
120
|
kind: "usage",
|
|
115
121
|
message: error.message
|
|
116
122
|
};
|
|
123
|
+
const stop = stopDetailsOf(error);
|
|
117
124
|
if (error instanceof Error && error.routeStop !== void 0) return {
|
|
118
125
|
kind: "stop",
|
|
119
|
-
message: error.message
|
|
126
|
+
message: error.message,
|
|
127
|
+
...stop ? { stop } : {}
|
|
120
128
|
};
|
|
121
129
|
if (error instanceof Error && quotaStopOf(error)) return {
|
|
122
130
|
kind: "stop",
|
|
123
131
|
message: error.message
|
|
124
132
|
};
|
|
133
|
+
if (error instanceof Error && stop) return {
|
|
134
|
+
kind: "expected",
|
|
135
|
+
message: error.message,
|
|
136
|
+
hint: "",
|
|
137
|
+
showAuthSources: false,
|
|
138
|
+
stop
|
|
139
|
+
};
|
|
125
140
|
if (error instanceof TypeError && error.message === "fetch failed") {
|
|
126
141
|
const cause = error.cause;
|
|
127
142
|
return {
|
|
@@ -143,18 +158,16 @@ function describeCliError(error) {
|
|
|
143
158
|
showAuthSources: false
|
|
144
159
|
};
|
|
145
160
|
const refusal = recordReadRefusalOf(error);
|
|
146
|
-
if (refusal)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
if (refusal) {
|
|
162
|
+
const text = describeRecordReadRefusal(refusal);
|
|
163
|
+
return {
|
|
164
|
+
kind: "expected",
|
|
165
|
+
...text,
|
|
166
|
+
showAuthSources: false,
|
|
167
|
+
stop: recordReadStop(refusal, text)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
151
170
|
const classified = classifyError(error);
|
|
152
|
-
if (classified.message === HOSTED_KEY_REJECTED) return {
|
|
153
|
-
kind: "expected",
|
|
154
|
-
message: classified.message,
|
|
155
|
-
hint: "",
|
|
156
|
-
showAuthSources: false
|
|
157
|
-
};
|
|
158
171
|
return {
|
|
159
172
|
kind: "expected",
|
|
160
173
|
message: classified.message.replace(PARAM_NAME_RE, (name) => flagForParam(name)),
|
|
@@ -164,6 +177,7 @@ function describeCliError(error) {
|
|
|
164
177
|
}
|
|
165
178
|
async function reportCliError(error, options) {
|
|
166
179
|
const report = describeCliError(error);
|
|
180
|
+
if (options.json && (report.kind === "stop" || report.kind === "expected") && report.stop) (options.writeStdout ?? ((value) => console.log(value)))(JSON.stringify({ error: report.stop }, null, 2));
|
|
167
181
|
const red = (text) => options.color ? `\x1B[31m${text}\x1B[0m` : text;
|
|
168
182
|
const lines = [];
|
|
169
183
|
switch (report.kind) {
|
|
@@ -184,4 +198,4 @@ async function reportCliError(error, options) {
|
|
|
184
198
|
const text = lines.join("\n");
|
|
185
199
|
(options.write ?? ((value) => console.error(value)))(options.color ? text : text.replace(ANSI_RE, ""));
|
|
186
200
|
}
|
|
187
|
-
export {
|
|
201
|
+
export { LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, reportCliError };
|
package/dist/hosted-site.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stopError } from "./stop.mjs";
|
|
2
|
+
import { connectSiteStep, getHostedAccount, hostedCredential, parseAuthentication, resolveAuthentication, siteManagerOf } from "./auth-state.mjs";
|
|
2
3
|
import { loadConfig } from "./config.mjs";
|
|
3
4
|
import { resolveCliEnvironment } from "./environment.mjs";
|
|
4
|
-
import { logger } from "./utils.mjs";
|
|
5
|
-
import process from "node:process";
|
|
6
5
|
import { createGscdumpV1Client } from "@gscdump/sdk/v1";
|
|
7
6
|
const HOSTED_ARGS = {
|
|
8
7
|
"site": {
|
|
@@ -57,35 +56,56 @@ function matchHostedSite(sites, target) {
|
|
|
57
56
|
sites: [...sites]
|
|
58
57
|
};
|
|
59
58
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
function noSitesStop(manager) {
|
|
60
|
+
return {
|
|
61
|
+
code: "NO_SITES",
|
|
62
|
+
message: `Your hosted record has no Sites. ${connectSiteStep(manager)} Then run the command again.`,
|
|
63
|
+
nextCommand: null
|
|
64
|
+
};
|
|
65
65
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
function siteMatchStop(match, manager) {
|
|
67
|
+
const nextCommand = "gscdump sites --json";
|
|
68
|
+
if (match.kind === "ambiguous") return {
|
|
69
|
+
code: "SITE_AMBIGUOUS",
|
|
70
|
+
message: `Multiple hosted Sites match "${match.target}": ${match.matches.map((site) => site.siteUrl).join(", ")}. Pass the exact Site URL with --site.`,
|
|
71
|
+
nextCommand
|
|
72
|
+
};
|
|
73
|
+
if (match.sites.length === 0) return noSitesStop(manager);
|
|
74
|
+
const available = `Hosted Sites: ${match.sites.map((site) => site.siteUrl).join(", ")}.`;
|
|
75
|
+
return match.target ? {
|
|
76
|
+
code: "SITE_NOT_FOUND",
|
|
77
|
+
message: `No hosted Site matches "${match.target}". ${available}`,
|
|
78
|
+
nextCommand
|
|
79
|
+
} : {
|
|
80
|
+
code: "SITE_REQUIRED",
|
|
81
|
+
message: `Pass --site. ${available}`,
|
|
82
|
+
nextCommand
|
|
83
|
+
};
|
|
69
84
|
}
|
|
70
85
|
async function resolveHostedSite(args, command) {
|
|
71
|
-
if (args._?.length)
|
|
86
|
+
if (args._?.length) throw new Error(`\`gscdump ${command.name}\` does not accept a positional Site ID. Pass --site instead.`);
|
|
72
87
|
const environment = resolveCliEnvironment().values;
|
|
73
88
|
const authentication = args["api-key"] ? parseAuthentication({
|
|
74
89
|
_tag: "Hosted",
|
|
75
90
|
apiKey: args["api-key"],
|
|
76
91
|
apiRoot: String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api")
|
|
77
92
|
}) : await resolveAuthentication();
|
|
78
|
-
if (authentication._tag !== "Hosted")
|
|
79
|
-
|
|
93
|
+
if (authentication._tag !== "Hosted") throw stopError({
|
|
94
|
+
code: "HOSTED_MODE_REQUIRED",
|
|
95
|
+
message: `\`gscdump ${command.name}\` reads the hosted record, so it needs Hosted mode. Run \`gscdump auth login --mode hosted\`, or pass --api-key. In Local mode, ${command.localAlternative}.`,
|
|
96
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
97
|
+
});
|
|
98
|
+
if (args["api-root"] && String(args["api-root"]).replace(/\/+$/, "") !== authentication.apiRoot) throw new Error("The API root changed. Pass --api-key for the new API root.");
|
|
80
99
|
const target = args.site ? String(args.site) : (await loadConfig()).defaultSite;
|
|
81
100
|
const match = matchHostedSite((await getHostedAccount(authentication)).sites, target);
|
|
82
|
-
if (match.kind !== "found")
|
|
101
|
+
if (match.kind !== "found") throw stopError(siteMatchStop(match, siteManagerOf(authentication)));
|
|
83
102
|
return {
|
|
84
103
|
client: createGscdumpV1Client({
|
|
85
104
|
apiRoot: authentication.apiRoot,
|
|
86
105
|
credential: hostedCredential(authentication)
|
|
87
106
|
}),
|
|
88
|
-
site: match.site
|
|
107
|
+
site: match.site,
|
|
108
|
+
authentication
|
|
89
109
|
};
|
|
90
110
|
}
|
|
91
|
-
export { HOSTED_ARGS, matchHostedSite, resolveHostedSite };
|
|
111
|
+
export { HOSTED_ARGS, matchHostedSite, noSitesStop, resolveHostedSite, siteMatchStop };
|
package/dist/mcp/errors.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { resolveBYOK, resolveServiceAccount } from "../auth.mjs";
|
|
2
2
|
import { COMPARISON_FLAGS, PERIOD_FLAGS } from "../window.mjs";
|
|
3
|
+
import { isQueryError } from "gscdump/query";
|
|
3
4
|
import { isAnalysisError } from "@gscdump/analysis/errors";
|
|
4
5
|
import { isEngineError } from "@gscdump/engine/errors";
|
|
5
6
|
import { classifyError } from "gscdump/errors";
|
|
6
|
-
import { isQueryError } from "gscdump/query";
|
|
7
7
|
const PERIODS = PERIOD_FLAGS;
|
|
8
8
|
const COMPARISONS = COMPARISON_FLAGS;
|
|
9
9
|
const mcpHandlerErrors = {
|
package/dist/package.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var version = "5.
|
|
1
|
+
var version = "5.1.0";
|
|
2
2
|
export { version };
|
package/dist/route.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stopError } from "./stop.mjs";
|
|
1
2
|
import { probeAuth } from "./auth.mjs";
|
|
2
3
|
import { listStoreSites } from "./store-sites.mjs";
|
|
3
4
|
import { formatSiteResolution, siteArg } from "./context.mjs";
|
|
@@ -199,11 +200,8 @@ function stopCode(route) {
|
|
|
199
200
|
case "partial": return "STORE_RANGE_NOT_COVERED";
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
|
-
function routeStopError(route,
|
|
203
|
-
return Object.assign(
|
|
204
|
-
name: "RouteStopError",
|
|
205
|
-
routeStop: route
|
|
206
|
-
});
|
|
203
|
+
function routeStopError(route, details) {
|
|
204
|
+
return Object.assign(stopError(details, "RouteStopError"), { routeStop: route });
|
|
207
205
|
}
|
|
208
206
|
async function needCoverage(needs, states) {
|
|
209
207
|
const reader = syncStateCoverageReader(states);
|
|
@@ -299,9 +297,7 @@ function describeStop(route, req, auth) {
|
|
|
299
297
|
} } : {}
|
|
300
298
|
};
|
|
301
299
|
}
|
|
302
|
-
function stopAtRoute(route, req, auth
|
|
303
|
-
|
|
304
|
-
if (opts.json) console.log(JSON.stringify({ error: details }, null, 2));
|
|
305
|
-
throw routeStopError(route, details.message);
|
|
300
|
+
function stopAtRoute(route, req, auth) {
|
|
301
|
+
throw routeStopError(route, describeStop(route, req, auth));
|
|
306
302
|
}
|
|
307
303
|
export { decideRoute, describeStop, liveCommand, liveNote, needCoverage, readRouteState, readSiteStates, resolveReadSite, routeMessage, routeStopError, stopAtRoute, stopCode, syncCommandFor };
|
package/dist/runtime.d.mts
CHANGED
|
@@ -7,6 +7,8 @@ export interface CliRuntime {
|
|
|
7
7
|
configDir: string;
|
|
8
8
|
configDirOverridden: boolean;
|
|
9
9
|
environment: CliEnvironmentSource;
|
|
10
|
+
/** The command writes JSON to stdout. A stop then prints its JSON form there too. */
|
|
11
|
+
jsonOutput: boolean;
|
|
10
12
|
logger: ConsolaInstance;
|
|
11
13
|
quiet: boolean;
|
|
12
14
|
rawArgs: string[];
|
package/dist/runtime.mjs
CHANGED
|
@@ -21,6 +21,7 @@ function createCliRuntime(opts = {}) {
|
|
|
21
21
|
configDir: opts.configDir ?? path.join(os.homedir(), ".config", "gscdump"),
|
|
22
22
|
configDirOverridden: false,
|
|
23
23
|
environment: opts.environment ?? process.env,
|
|
24
|
+
jsonOutput: false,
|
|
24
25
|
logger: baseLogger.withTag("gscdump"),
|
|
25
26
|
quiet: false,
|
|
26
27
|
rawArgs: [...opts.rawArgs ?? process.argv.slice(2)]
|
package/dist/stop.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function stopError(details, name = "StopError") {
|
|
2
|
+
return Object.assign(new Error(details.message), {
|
|
3
|
+
name,
|
|
4
|
+
stopDetails: details
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
function stopDetailsOf(error) {
|
|
8
|
+
return error instanceof Error ? error.stopDetails : void 0;
|
|
9
|
+
}
|
|
10
|
+
export { stopDetailsOf, stopError };
|
package/dist/utils.mjs
CHANGED
|
@@ -2,8 +2,8 @@ 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 { SearchTypes } from "gscdump/query";
|
|
6
5
|
import os from "node:os";
|
|
6
|
+
import { SearchTypes } from "gscdump/query";
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
8
8
|
import { MAX_FETCH_BUDGET } from "@gscdump/engine/analysis-types";
|
|
9
9
|
const ALL_SEARCH_TYPES = Object.values(SearchTypes);
|
|
@@ -40,10 +40,14 @@ const OUTPUT_ARGS = {
|
|
|
40
40
|
description: "Suppress info/success output"
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
|
+
function setJsonOutput(json) {
|
|
44
|
+
useCliRuntime().jsonOutput = json;
|
|
45
|
+
}
|
|
43
46
|
function applyOutputMode(args) {
|
|
44
47
|
const json = Boolean(args.json);
|
|
45
48
|
const quiet = json || Boolean(args.quiet);
|
|
46
49
|
setQuiet(quiet);
|
|
50
|
+
setJsonOutput(json);
|
|
47
51
|
return {
|
|
48
52
|
json,
|
|
49
53
|
quiet
|
|
@@ -148,10 +152,7 @@ function clearLine() {
|
|
|
148
152
|
process.stdout.write("\r\x1B[K");
|
|
149
153
|
}
|
|
150
154
|
function displayPath(absPath) {
|
|
151
|
-
const cwd = process.cwd();
|
|
152
155
|
const home = os.homedir();
|
|
153
|
-
if (absPath === cwd) return ".";
|
|
154
|
-
if (absPath.startsWith(`${cwd}/`)) return absPath.slice(cwd.length + 1);
|
|
155
156
|
if (absPath === home) return "~";
|
|
156
157
|
if (absPath.startsWith(`${home}/`)) return `~/${absPath.slice(home.length + 1)}`;
|
|
157
158
|
return absPath;
|
|
@@ -196,4 +197,4 @@ async function readUrlList(input) {
|
|
|
196
197
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
197
198
|
return parseUrlLines(Buffer.concat(chunks).toString("utf-8"));
|
|
198
199
|
}
|
|
199
|
-
export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, plural, progressBar, readUrlList, red, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
|
|
200
|
+
export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, plural, progressBar, readUrlList, red, runWithConcurrency, setJsonOutput, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.1.0",
|
|
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",
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/prompts": "^1.8.1",
|
|
46
46
|
"@duckdb/node-api": "1.5.5-r.5",
|
|
47
|
-
"@gscdump/analysis": "^5.
|
|
48
|
-
"@gscdump/contracts": "^5.
|
|
49
|
-
"@gscdump/engine": "^5.
|
|
50
|
-
"@gscdump/engine-gsc-api": "^5.
|
|
51
|
-
"@gscdump/sdk": "^5.
|
|
47
|
+
"@gscdump/analysis": "^5.1.0",
|
|
48
|
+
"@gscdump/contracts": "^5.1.0",
|
|
49
|
+
"@gscdump/engine": "^5.1.0",
|
|
50
|
+
"@gscdump/engine-gsc-api": "^5.1.0",
|
|
51
|
+
"@gscdump/sdk": "^5.1.0",
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
53
53
|
"citty": "^0.2.2",
|
|
54
54
|
"consola": "^3.4.2",
|
|
55
55
|
"google-auth-library": "^11.1.0",
|
|
56
|
-
"gscdump": "^5.
|
|
56
|
+
"gscdump": "^5.1.0",
|
|
57
57
|
"ofetch": "^1.5.1",
|
|
58
58
|
"open": "^11.0.4",
|
|
59
59
|
"proper-lockfile": "^4.1.2",
|
package/skills/gscdump/SKILL.md
CHANGED
|
@@ -11,7 +11,7 @@ For `query`, `-s` means `--site`, `-d` means `--dimensions`, and `-f` means `--f
|
|
|
11
11
|
Use `--start` and `--end` for dates. `--site=SITE` also works.
|
|
12
12
|
Use each option once, with either its short or long spelling.
|
|
13
13
|
Put options after the subcommand name: `gscdump store stats --json`, not `gscdump store --json stats`.
|
|
14
|
-
A failed command prints one `Error:` line to stderr and exits 1.
|
|
14
|
+
A failed command prints one `Error:` line to stderr and exits 1. A [stop](#stops) with JSON output also prints `{ "error" }` on stdout.
|
|
15
15
|
Example: `gscdump query --site=SITE --start=DATE --end=DATE -d page -f json`.
|
|
16
16
|
|
|
17
17
|
## Start each task
|
|
@@ -43,6 +43,9 @@ gscdump has 2 access modes. Check `gscdump auth status --json` before queries. R
|
|
|
43
43
|
In Local mode, `googleAuthenticated: true` means Google accepted the credentials. When it is false, `googleError` says why.
|
|
44
44
|
In Hosted mode, `hostedSync` lists each hosted Site with `syncStatus` and `syncProgress`, and `commands` lists what Hosted mode can run.
|
|
45
45
|
`gscdump sites` shows the same Sites and progress.
|
|
46
|
+
An empty `sites` list means the hosted record has no Sites. Commands that need a Site then stop with `NO_SITES`.
|
|
47
|
+
Tell the user to connect a Site. A browser login connects Sites on gscdump.com.
|
|
48
|
+
An API key connects Sites in the app that issued the key. That app can be a partner app, not gscdump.com.
|
|
46
49
|
|
|
47
50
|
`--mode local|hosted` overrides one invocation. `GSCDUMP_AUTH_MODE` also overrides the saved mode.
|
|
48
51
|
A successful login saves the mode per profile.
|
|
@@ -128,8 +131,7 @@ mixes Store rows and live rows.
|
|
|
128
131
|
- `--live` always asks Search Console. It needs Local mode with Google credentials.
|
|
129
132
|
- `query --sql` reads the Store only. It stops when no table it names has data.
|
|
130
133
|
- JSON output carries `meta.source`: `local`, `live`, or `hosted`.
|
|
131
|
-
- A stop with
|
|
132
|
-
Codes: `NOT_CONNECTED`, `STORE_RANGE_NOT_COVERED` (with `missingDates`), `SYNC_RUNNING` (with `sync.done` and `sync.total`), `NO_SYNCED_DATA`, `STORE_ONLY`, `LIVE_ONLY`.
|
|
134
|
+
- A routing stop with JSON output prints `{ "error" }` on stdout and exits 1. See [Stops](#stops).
|
|
133
135
|
Partial coverage and a running sync are normal progress. Run `nextCommand`, or tell the user to.
|
|
134
136
|
- A sync is running only while its heartbeat is recent. A killed sync does not block reads.
|
|
135
137
|
- Every Search Analytics, URL Inspection, and Indexing API call spends the shared quota ledger in the data dir.
|
|
@@ -153,7 +155,8 @@ gscdump skill install --agent claude # Codex: --agent codex
|
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
After upgrading the CLI, run this command again to update the installed skill.
|
|
156
|
-
The command prints where it wrote the skill
|
|
158
|
+
The command prints where it wrote the skill, as a path that works from any directory, such as `~/.claude/skills/gscdump`.
|
|
159
|
+
With `--json`, `destination` is the absolute path. Clients without a skill
|
|
157
160
|
directory can read `gscdump --help` and `gscdump <command> --help` instead.
|
|
158
161
|
|
|
159
162
|
## Local mode setup
|
|
@@ -220,6 +223,35 @@ Parse JSON. Never scrape human output.
|
|
|
220
223
|
If the user requests JSON, return the CLI JSON unchanged. Do not replace it with a table.
|
|
221
224
|
Do not rewrite rows, estimate metrics, or add manually calculated totals.
|
|
222
225
|
|
|
226
|
+
## Stops
|
|
227
|
+
|
|
228
|
+
A stop ends a command at a known condition and exits 1. stderr gets the message.
|
|
229
|
+
If the command writes JSON, stdout gets `{ "error": { "code", "message", "nextCommand" } }`.
|
|
230
|
+
A command writes JSON with `--json`, with `--format json`, and for `query` with its default format.
|
|
231
|
+
Read `code`, not the message. If `nextCommand` is not null, run it, or tell the user to run it.
|
|
232
|
+
A `gscdump auth login` command needs the user. Tell the user to run it. If `nextCommand` is null, tell the user the `message`.
|
|
233
|
+
A routing stop also has `siteUrl`. `STORE_RANGE_NOT_COVERED` adds `missingDates`, and `SYNC_RUNNING` adds `sync.done` and `sync.total`.
|
|
234
|
+
|
|
235
|
+
| Code | Meaning |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `NOT_CONNECTED` | The Store cannot answer, and Google is not connected |
|
|
238
|
+
| `STORE_RANGE_NOT_COVERED` | The Store does not have `missingDates` |
|
|
239
|
+
| `SYNC_RUNNING` | A sync is still filling the dates |
|
|
240
|
+
| `NO_SYNCED_DATA` | A Store-only read found no synced data |
|
|
241
|
+
| `STORE_ONLY` | The read needs the Store, so `--live` cannot run it |
|
|
242
|
+
| `LIVE_ONLY` | Only `--live` can answer the read |
|
|
243
|
+
| `LOCAL_MODE_REQUIRED` | The command calls Google, and Hosted mode is selected |
|
|
244
|
+
| `HOSTED_MODE_REQUIRED` | The command reads the hosted record, and Local mode is selected |
|
|
245
|
+
| `HOSTED_CREDENTIALS_MISSING` | Hosted mode has no CLI session and no API key |
|
|
246
|
+
| `HOSTED_CREDENTIALS_REJECTED` | gscdump.com rejected the CLI session or API key. For an API key, `nextCommand` is null: the user needs a new key from the app that issued it |
|
|
247
|
+
| `NO_SITES` | The hosted record has no Sites. The message says where the user connects one |
|
|
248
|
+
| `SITE_NOT_FOUND` | No hosted Site matches `--site` |
|
|
249
|
+
| `SITE_AMBIGUOUS` | More than one hosted Site matches `--site` |
|
|
250
|
+
| `SITE_REQUIRED` | The hosted record has more than one Site. Pass `--site` |
|
|
251
|
+
| `BING_NOT_CONNECTED` | The Site has no hosted Bing connection |
|
|
252
|
+
| `RECORD_NOT_READY` | gscdump has not prepared the Site's record for reads yet |
|
|
253
|
+
| `RANGE_NOT_SYNCED` | The hosted record does not hold the requested dates |
|
|
254
|
+
|
|
223
255
|
## Commands
|
|
224
256
|
|
|
225
257
|
| Command | Use it for |
|