@gscdump/cli 5.1.0 → 5.3.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/auth-state.mjs +26 -8
- package/dist/bing-data.mjs +2 -1
- package/dist/bing-hosted.mjs +1 -1
- package/dist/cli.d.mts +6 -0
- package/dist/cli.mjs +44 -5
- package/dist/command-meta.mjs +2 -2
- package/dist/commands/auth.mjs +12 -13
- package/dist/commands/config.mjs +5 -15
- package/dist/commands/doctor.mjs +1 -1
- package/dist/commands/dump.mjs +2 -4
- package/dist/commands/entities.mjs +3 -9
- package/dist/commands/indexing-watch.mjs +5 -5
- package/dist/commands/indexing.mjs +6 -22
- package/dist/commands/init.mjs +2 -5
- package/dist/commands/papercut.mjs +1 -4
- package/dist/commands/profile.mjs +4 -13
- package/dist/commands/query.mjs +10 -34
- package/dist/commands/sitemaps.mjs +2 -9
- package/dist/commands/sites.mjs +5 -10
- package/dist/commands/skill.mjs +1 -4
- package/dist/commands/sync.mjs +2 -8
- package/dist/error-handler.mjs +46 -4
- package/dist/hosted-auth.mjs +9 -1
- package/dist/hosted-session.mjs +17 -0
- package/dist/hosted-site.mjs +6 -3
- package/dist/package.mjs +1 -1
- package/dist/stop.mjs +14 -1
- package/dist/utils.mjs +7 -9
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +11 -4
package/dist/auth-state.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOSTED_SESSION_EXPIRED, isSessionExpiredBody } from "./hosted-session.mjs";
|
|
1
2
|
import { useCliRuntime } from "./runtime.mjs";
|
|
2
3
|
import { stopError } from "./stop.mjs";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -156,12 +157,18 @@ const hostedSiteSchema = z.object({
|
|
|
156
157
|
newestDateSynced: z.string().nullable().optional(),
|
|
157
158
|
oldestDateSynced: z.string().nullable().optional()
|
|
158
159
|
}).passthrough();
|
|
160
|
+
const partnerNameSchema = z.string().trim().min(1).max(80).regex(/^\P{Cc}+$/u);
|
|
161
|
+
const keyIssuerSchema = z.discriminatedUnion("_tag", [z.object({ _tag: z.literal("gscdump") }), z.object({
|
|
162
|
+
_tag: z.literal("partner"),
|
|
163
|
+
name: partnerNameSchema
|
|
164
|
+
})]);
|
|
159
165
|
const accountSchema = z.object({
|
|
160
166
|
user: z.object({
|
|
161
167
|
publicId: z.string(),
|
|
162
168
|
email: z.string()
|
|
163
169
|
}),
|
|
164
|
-
sites: z.array(hostedSiteSchema)
|
|
170
|
+
sites: z.array(hostedSiteSchema),
|
|
171
|
+
issuer: keyIssuerSchema.optional().catch(void 0)
|
|
165
172
|
});
|
|
166
173
|
async function hostedRequest(state, route, options = {}) {
|
|
167
174
|
const response = await fetch(`${state.apiRoot}${route}`, {
|
|
@@ -181,7 +188,8 @@ async function hostedRequest(state, route, options = {}) {
|
|
|
181
188
|
};
|
|
182
189
|
if (response.status === 401) {
|
|
183
190
|
const session = "sessionId" in state;
|
|
184
|
-
|
|
191
|
+
const expired = session && isSessionExpiredBody(await response.json().catch(() => null));
|
|
192
|
+
throw Object.assign(stopError(expired ? HOSTED_SESSION_EXPIRED : {
|
|
185
193
|
code: "HOSTED_CREDENTIALS_REJECTED",
|
|
186
194
|
message: session ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED,
|
|
187
195
|
nextCommand: session ? "gscdump auth login --mode hosted" : null
|
|
@@ -194,18 +202,28 @@ async function hostedRequest(state, route, options = {}) {
|
|
|
194
202
|
function hostedCredential(state) {
|
|
195
203
|
return "sessionId" in state ? state.sessionId : state.apiKey;
|
|
196
204
|
}
|
|
197
|
-
function siteManagerOf(state) {
|
|
198
|
-
|
|
205
|
+
function siteManagerOf(state, issuer) {
|
|
206
|
+
if ("sessionId" in state) return { _tag: "gscdump" };
|
|
207
|
+
return issuer ?? { _tag: "key_issuer" };
|
|
199
208
|
}
|
|
200
|
-
function
|
|
209
|
+
function connectSitePlace(manager) {
|
|
201
210
|
switch (manager._tag) {
|
|
202
|
-
case "gscdump": return "
|
|
203
|
-
case "
|
|
211
|
+
case "gscdump": return "at https://gscdump.com/app/onboarding?step=connect-sites";
|
|
212
|
+
case "partner": return `in ${manager.name}`;
|
|
213
|
+
case "key_issuer": return "in the app that issued this API key";
|
|
204
214
|
}
|
|
205
215
|
}
|
|
216
|
+
function connectSiteStep(manager) {
|
|
217
|
+
return `Connect a Site ${connectSitePlace(manager)}.`;
|
|
218
|
+
}
|
|
219
|
+
function noSitesNextStep(manager) {
|
|
220
|
+
const next = `Next: connect a Site ${connectSitePlace(manager)}.`;
|
|
221
|
+
return manager._tag === "gscdump" ? `${next} If Hosted access is not active, gscdump.com asks you to activate it first.` : next;
|
|
222
|
+
}
|
|
206
223
|
function siteSettingsPlace(manager) {
|
|
207
224
|
switch (manager._tag) {
|
|
208
225
|
case "gscdump": return "in the Site settings on gscdump.com";
|
|
226
|
+
case "partner": return `in the Site settings of ${manager.name}`;
|
|
209
227
|
case "key_issuer": return "in the Site settings of the app that issued this API key";
|
|
210
228
|
}
|
|
211
229
|
}
|
|
@@ -218,4 +236,4 @@ function formatHostedSync(site) {
|
|
|
218
236
|
const status = site.syncStatus ?? "pending";
|
|
219
237
|
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}` : ""}`;
|
|
220
238
|
}
|
|
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 };
|
|
239
|
+
export { HOSTED_KEY_REJECTED, HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, connectSiteStep, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, localModeRequired, noSitesNextStep, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication, siteManagerOf, siteSettingsPlace };
|
package/dist/bing-data.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { toCSV } from "./utils.mjs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { BING_THROTTLE_IP_ERROR_CODE } from "gscdump/bing";
|
|
5
6
|
const BING_DATASETS = [
|
|
6
7
|
"traffic",
|
|
7
8
|
"pages",
|
|
@@ -37,7 +38,7 @@ function parseBingDumpOptions(args) {
|
|
|
37
38
|
function unwrapBing(result) {
|
|
38
39
|
if (result.ok) return result.value;
|
|
39
40
|
const error = result.error;
|
|
40
|
-
const help = error._tag === "AuthenticationRequired" ? " Run `gscdump bing login` again." : error._tag === "Throttled" && error.retryAfterMs !== void 0 ? ` Retry after ${Math.ceil(error.retryAfterMs / 1e3)} seconds.` : "";
|
|
41
|
+
const help = error._tag === "AuthenticationRequired" ? " Run `gscdump bing login` again." : error._tag === "Throttled" && error.errorCode === BING_THROTTLE_IP_ERROR_CODE ? " Bing refused requests from this IP address. Waiting does not lift the block. Retry from another network." : error._tag === "Throttled" && error.retryAfterMs !== void 0 ? ` Retry after ${Math.ceil(error.retryAfterMs / 1e3)} seconds.` : "";
|
|
41
42
|
throw new Error(`Bing request failed: ${error._tag}.${help}`);
|
|
42
43
|
}
|
|
43
44
|
function siteIdentity(value) {
|
package/dist/bing-hosted.mjs
CHANGED
|
@@ -33,7 +33,7 @@ async function loadHostedBingConnections(state, sites, options = {}) {
|
|
|
33
33
|
async function resolveHostedBingSites(state, input) {
|
|
34
34
|
if (Boolean(input.site) === Boolean(input.allSites)) throw new Error("Choose --site or --all-sites.");
|
|
35
35
|
const account = await getHostedAccount(state);
|
|
36
|
-
if (account.sites.length === 0) throw stopError(noSitesStop(siteManagerOf(state)));
|
|
36
|
+
if (account.sites.length === 0) throw stopError(noSitesStop(siteManagerOf(state, account.issuer)));
|
|
37
37
|
const exact = account.sites.filter((site) => site.siteId === input.site || site.siteUrl === input.site);
|
|
38
38
|
const normalize = (value) => URL.parse(value.startsWith("sc-domain:") ? `https://${value.slice(10)}/` : value)?.toString();
|
|
39
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);
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { CliRuntime, CreateCliRuntimeOptions, createCliRuntime } from "./runtime.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* The command line asks for JSON output: `--json`, `--format json`, or `query`
|
|
4
|
+
* with no `--format`, since JSON is its default. The command sets the final
|
|
5
|
+
* mode when it runs. This covers failures before that.
|
|
6
|
+
*/
|
|
7
|
+
export declare function asksForJson(rawArgs: readonly string[]): boolean;
|
|
2
8
|
export declare const main: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
3
9
|
export interface RunCliOptions {
|
|
4
10
|
rawArgs?: string[];
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCliRuntime, runWithCliRuntime, useCliRuntime } from "./runtime.mjs";
|
|
2
|
+
import { commandLineError } from "./stop.mjs";
|
|
2
3
|
import { parseAuthMode } from "./auth-state.mjs";
|
|
3
4
|
import { checkCliArgs } from "./cli-args.mjs";
|
|
4
5
|
import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
|
|
@@ -23,9 +24,22 @@ function shouldShowSplash(rawArgs) {
|
|
|
23
24
|
]) if (rawArgs.includes(flag)) return false;
|
|
24
25
|
return true;
|
|
25
26
|
}
|
|
27
|
+
function formatFlag(rawArgs) {
|
|
28
|
+
for (const [index, arg] of rawArgs.entries()) {
|
|
29
|
+
if (arg === "--format" || arg === "-f") return rawArgs[index + 1];
|
|
30
|
+
const inline = /^(?:--format=|-f=?)(.+)$/.exec(arg);
|
|
31
|
+
if (inline) return inline[1];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function asksForJson(rawArgs) {
|
|
35
|
+
const format = formatFlag(rawArgs);
|
|
36
|
+
return rawArgs.includes("--json") || format === "json" || rawArgs[0] === "query" && format === void 0;
|
|
37
|
+
}
|
|
26
38
|
function prepareCliArgs(input) {
|
|
27
39
|
const rawArgs = [...input];
|
|
28
|
-
|
|
40
|
+
const mode = pluckArgValue(rawArgs, "--mode");
|
|
41
|
+
if (mode !== null && mode !== "local" && mode !== "hosted") throw commandLineError("Access mode must be local or hosted.");
|
|
42
|
+
useCliRuntime().authModeOverride = parseAuthMode(mode ?? void 0);
|
|
29
43
|
const env = resolveCliEnvironment();
|
|
30
44
|
setNoColor(!terminalOutputOptions().color);
|
|
31
45
|
const profile = pluckArgValue(rawArgs, "--profile", true);
|
|
@@ -50,7 +64,7 @@ function pluckArgValue(argv, flag, allowQueryTiming = false) {
|
|
|
50
64
|
const inline = a !== flag;
|
|
51
65
|
const next = inline ? a.slice(flag.length + 1) : argv[i + 1];
|
|
52
66
|
if (!inline && allowQueryTiming && argv.includes("query") && argv.indexOf("query") < i && (next === void 0 || next.startsWith("-"))) continue;
|
|
53
|
-
if (!next || !inline && next.startsWith("-")) throw
|
|
67
|
+
if (!next || !inline && next.startsWith("-")) throw commandLineError(`${flag} requires a value. Use ${flag}=VALUE.`);
|
|
54
68
|
value = next;
|
|
55
69
|
argv.splice(i, inline ? 1 : 2);
|
|
56
70
|
i--;
|
|
@@ -80,6 +94,26 @@ async function runMainCommand(rawArgs) {
|
|
|
80
94
|
}
|
|
81
95
|
await runCommand(main, { rawArgs });
|
|
82
96
|
}
|
|
97
|
+
function watchStdout() {
|
|
98
|
+
let wrote = false;
|
|
99
|
+
const write = process.stdout.write;
|
|
100
|
+
const log = console.log;
|
|
101
|
+
process.stdout.write = ((...args) => {
|
|
102
|
+
wrote = true;
|
|
103
|
+
return write.apply(process.stdout, args);
|
|
104
|
+
});
|
|
105
|
+
console.log = (...args) => {
|
|
106
|
+
wrote = true;
|
|
107
|
+
log(...args);
|
|
108
|
+
};
|
|
109
|
+
return {
|
|
110
|
+
wrote: () => wrote,
|
|
111
|
+
restore: () => {
|
|
112
|
+
process.stdout.write = write;
|
|
113
|
+
console.log = log;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
83
117
|
function stderrColor(runtime, rawArgs) {
|
|
84
118
|
return resolveOutputOptions({
|
|
85
119
|
isTTY: process.stderr.isTTY,
|
|
@@ -97,22 +131,27 @@ async function runCli(opts = {}) {
|
|
|
97
131
|
return runWithCliRuntime(runtime, async () => {
|
|
98
132
|
let rawArgs = [...input];
|
|
99
133
|
runtime.jsonOutput = false;
|
|
134
|
+
const stdout = watchStdout();
|
|
100
135
|
try {
|
|
136
|
+
runtime.jsonOutput = asksForJson(input);
|
|
101
137
|
rawArgs = prepareCliArgs(input);
|
|
102
138
|
runtime.rawArgs = [...rawArgs];
|
|
139
|
+
runtime.jsonOutput = asksForJson(rawArgs);
|
|
103
140
|
const argumentError = await checkCliArgs(main, rawArgs);
|
|
104
|
-
if (argumentError) throw
|
|
141
|
+
if (argumentError) throw commandLineError(argumentError);
|
|
105
142
|
await withConfiguredOutput(() => runMainCommand(rawArgs));
|
|
106
143
|
return 0;
|
|
107
144
|
} catch (error) {
|
|
108
145
|
await reportCliError(error, {
|
|
109
146
|
color: stderrColor(runtime, rawArgs),
|
|
110
|
-
json: runtime.jsonOutput,
|
|
147
|
+
json: runtime.jsonOutput && !stdout.wrote(),
|
|
111
148
|
usage: async () => renderUsage(...await resolveUsageTarget(main, rawArgs)),
|
|
112
149
|
authSources: async () => (await import("./auth.mjs")).formatAuthProvenance()
|
|
113
150
|
});
|
|
114
151
|
return 1;
|
|
152
|
+
} finally {
|
|
153
|
+
stdout.restore();
|
|
115
154
|
}
|
|
116
155
|
});
|
|
117
156
|
}
|
|
118
|
-
export { createCliRuntime, main, runCli };
|
|
157
|
+
export { asksForJson, createCliRuntime, main, runCli };
|
package/dist/command-meta.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const initCommandMeta = {
|
|
2
2
|
name: "init",
|
|
3
|
-
description: "Set up
|
|
3
|
+
description: "Set up gscdump authentication"
|
|
4
4
|
};
|
|
5
5
|
const dumpCommandMeta = {
|
|
6
6
|
name: "dump",
|
|
@@ -40,7 +40,7 @@ const entitiesCommandMeta = {
|
|
|
40
40
|
};
|
|
41
41
|
const analyzeCommandMeta = {
|
|
42
42
|
name: "analyze",
|
|
43
|
-
description: "
|
|
43
|
+
description: "Run an Analyzer over Search Console rows"
|
|
44
44
|
};
|
|
45
45
|
const reportCommandMeta = {
|
|
46
46
|
name: "report",
|
package/dist/commands/auth.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
-
import { clearAuthentication, formatHostedSync, getHostedAccount, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication } from "../auth-state.mjs";
|
|
2
|
+
import { clearAuthentication, formatHostedSync, getHostedAccount, noSitesNextStep, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication, siteManagerOf } from "../auth-state.mjs";
|
|
3
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";
|
|
@@ -28,6 +28,9 @@ async function requireLocalAuth(args) {
|
|
|
28
28
|
if ((await resolveAuthentication())._tag === "Hosted") throw new Error("Google scopes and token refresh apply to Local mode only. Hosted mode uses a gscdump.com CLI session.");
|
|
29
29
|
}
|
|
30
30
|
const HOSTED_MODE_NOTE = "Hosted mode reads your gscdump.com record. Commands that call Google need Local mode.";
|
|
31
|
+
function loginNextStep(state, siteCount, issuer) {
|
|
32
|
+
return siteCount > 0 ? "Next: run `gscdump sites` to see each Site and its sync state." : noSitesNextStep(siteManagerOf(state, issuer));
|
|
33
|
+
}
|
|
31
34
|
const HOSTED_COMMANDS = [
|
|
32
35
|
"sites",
|
|
33
36
|
"query",
|
|
@@ -71,6 +74,7 @@ async function loginHosted(args) {
|
|
|
71
74
|
await saveAuthentication(state);
|
|
72
75
|
logger.success(`Hosted mode saved for ${account.user.email}`);
|
|
73
76
|
logger.info(HOSTED_MODE_NOTE);
|
|
77
|
+
logger.info(loginNextStep(state, account.sites.length, account.issuer));
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
const state = parseAuthentication({
|
|
@@ -83,6 +87,7 @@ async function loginHosted(args) {
|
|
|
83
87
|
await saveAuthentication(state);
|
|
84
88
|
logger.success(`Hosted mode saved for ${account.user.email}`);
|
|
85
89
|
logger.info(HOSTED_MODE_NOTE);
|
|
90
|
+
logger.info(loginNextStep(state, account.sites.length, account.issuer));
|
|
86
91
|
}
|
|
87
92
|
async function resolveLiveAuthState() {
|
|
88
93
|
const tokens = await loadTokens();
|
|
@@ -158,6 +163,7 @@ async function runStatus(args) {
|
|
|
158
163
|
console.log(` API: ${authentication.apiRoot}`);
|
|
159
164
|
console.log(` Sites: ${sites.length}`);
|
|
160
165
|
for (const site of sites) console.log(` ${site.siteUrl} ${formatHostedSync(site)}`);
|
|
166
|
+
if (sites.length === 0) console.log(` ${noSitesNextStep(siteManagerOf(authentication, account.value.issuer))}`);
|
|
161
167
|
console.log(` ${HOSTED_MODE_NOTE}`);
|
|
162
168
|
console.log(` Hosted commands: ${HOSTED_COMMANDS.join(", ")}`);
|
|
163
169
|
}
|
|
@@ -263,17 +269,13 @@ const refreshCommand = defineCommand({
|
|
|
263
269
|
return;
|
|
264
270
|
}
|
|
265
271
|
const tokens = await loadTokens();
|
|
266
|
-
if (!tokens?.refresh_token)
|
|
267
|
-
logger.error("No saved refresh token. Run `gscdump auth login`.");
|
|
268
|
-
process.exit(1);
|
|
269
|
-
}
|
|
272
|
+
if (!tokens?.refresh_token) throw new Error("No saved refresh token. Run `gscdump auth login`.");
|
|
270
273
|
await saveTokens({
|
|
271
274
|
...tokens,
|
|
272
275
|
expiry_date: 1
|
|
273
276
|
});
|
|
274
277
|
if ((await getAuth({ interactive: false }).catch((e) => {
|
|
275
|
-
|
|
276
|
-
process.exit(1);
|
|
278
|
+
throw new Error(`Refresh failed: ${e.message}`, { cause: e });
|
|
277
279
|
})).credentials?.access_token) logger.success("Token refreshed");
|
|
278
280
|
else logger.warn("Refresh completed but no new access token returned");
|
|
279
281
|
}
|
|
@@ -323,12 +325,10 @@ const loginCommand = defineCommand({
|
|
|
323
325
|
if (args["service-account"]) {
|
|
324
326
|
const saPath = path.resolve(String(args["service-account"]));
|
|
325
327
|
const jwt = await loadServiceAccount(saPath).catch((e) => {
|
|
326
|
-
|
|
327
|
-
process.exit(1);
|
|
328
|
+
throw new Error(`Service-account load failed: ${e.message}`, { cause: e });
|
|
328
329
|
});
|
|
329
330
|
await jwt.authorize().catch((e) => {
|
|
330
|
-
|
|
331
|
-
process.exit(1);
|
|
331
|
+
throw new Error(`Service-account auth failed: ${e.message}`, { cause: e });
|
|
332
332
|
});
|
|
333
333
|
const config = await loadConfig();
|
|
334
334
|
config.serviceAccountPath = saPath;
|
|
@@ -343,8 +343,7 @@ const loginCommand = defineCommand({
|
|
|
343
343
|
noBrowser: args.browser === false,
|
|
344
344
|
force: Boolean(args.force)
|
|
345
345
|
}).catch((e) => {
|
|
346
|
-
|
|
347
|
-
process.exit(1);
|
|
346
|
+
throw new Error(`Login failed: ${e.message}`, { cause: e });
|
|
348
347
|
});
|
|
349
348
|
logger.success("Logged in");
|
|
350
349
|
await runSmokeTest(oauth);
|
package/dist/commands/config.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
1
2
|
import { configCommandMeta } from "../command-meta.mjs";
|
|
2
3
|
import { getConfigPath, loadConfig, resolveDataDir, saveConfig } from "../config.mjs";
|
|
3
4
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
@@ -71,18 +72,11 @@ const setCommand = defineCommand({
|
|
|
71
72
|
},
|
|
72
73
|
async run({ args }) {
|
|
73
74
|
applyOutputMode(args);
|
|
74
|
-
if (!VALID_KEYS.includes(args.key)) {
|
|
75
|
-
logger.error(`Invalid key: ${args.key}`);
|
|
76
|
-
logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
75
|
+
if (!VALID_KEYS.includes(args.key)) throw commandLineError(`Invalid key: ${args.key}. Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
79
76
|
const config = await loadConfig();
|
|
80
77
|
const value = NUMERIC_KEYS.has(args.key) ? Number(args.value) : args.key === "defaultSite" ? await canonicalSite(args.value) : args.value;
|
|
81
|
-
if (NUMERIC_KEYS.has(args.key) && !Number.isFinite(value)) {
|
|
82
|
-
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
config[args.key] = value;
|
|
78
|
+
if (NUMERIC_KEYS.has(args.key) && !Number.isFinite(value)) throw commandLineError(`Invalid numeric value for ${args.key}: ${args.value}`);
|
|
79
|
+
Object.assign(config, { [args.key]: value });
|
|
86
80
|
await saveConfig(config);
|
|
87
81
|
logger.success(`Set ${args.key} = ${value}`);
|
|
88
82
|
}
|
|
@@ -102,11 +96,7 @@ const unsetCommand = defineCommand({
|
|
|
102
96
|
},
|
|
103
97
|
async run({ args }) {
|
|
104
98
|
applyOutputMode(args);
|
|
105
|
-
if (!VALID_KEYS.includes(args.key)) {
|
|
106
|
-
logger.error(`Invalid key: ${args.key}`);
|
|
107
|
-
logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
99
|
+
if (!VALID_KEYS.includes(args.key)) throw commandLineError(`Invalid key: ${args.key}. Valid keys: ${VALID_KEYS.join(", ")}`);
|
|
110
100
|
const config = await loadConfig();
|
|
111
101
|
delete config[args.key];
|
|
112
102
|
await saveConfig(config);
|
package/dist/commands/doctor.mjs
CHANGED
|
@@ -339,7 +339,7 @@ async function checkHostedConnection(authentication) {
|
|
|
339
339
|
} : {
|
|
340
340
|
name: "hosted.sites",
|
|
341
341
|
status: "warn",
|
|
342
|
-
detail: `no hosted Sites. ${connectSiteStep(siteManagerOf(authentication))}`
|
|
342
|
+
detail: `no hosted Sites. ${connectSiteStep(siteManagerOf(authentication, account.value.issuer))}`
|
|
343
343
|
}];
|
|
344
344
|
return [
|
|
345
345
|
{
|
package/dist/commands/dump.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
1
2
|
import { dumpCommandMeta } from "../command-meta.mjs";
|
|
2
3
|
import { ALL_SEARCH_TYPES, OUTPUT_ARGS, applyOutputMode, displayPath, logger, parseNameList, parseSearchType } from "../utils.mjs";
|
|
3
4
|
import { allTables } from "../local-store.mjs";
|
|
@@ -70,10 +71,7 @@ const dumpCommand = defineCommand({
|
|
|
70
71
|
async run({ args }) {
|
|
71
72
|
const { json, quiet } = applyOutputMode(args);
|
|
72
73
|
const format = String(args.format);
|
|
73
|
-
if (!isDumpFormat(format)) {
|
|
74
|
-
logger.error(`Invalid --format: ${format}. Allowed: ${DUMP_FORMATS.join(", ")}`);
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
74
|
+
if (!isDumpFormat(format)) throw commandLineError(`Invalid --format: ${format}. Allowed: ${DUMP_FORMATS.join(", ")}`);
|
|
77
75
|
const tablesFilter = args.tables ? new Set(parseNameList(args.tables, [
|
|
78
76
|
...allTables(),
|
|
79
77
|
...ENTITY_DATASETS,
|
|
@@ -55,10 +55,7 @@ const showSubCommand = defineCommand({
|
|
|
55
55
|
userId: store.userId,
|
|
56
56
|
siteId: store.siteIdFor(siteUrl)
|
|
57
57
|
}, String(args.url));
|
|
58
|
-
if (!record) {
|
|
59
|
-
logger.warn(`No inspection record for ${args.url}`);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
58
|
+
if (!record) throw new Error(`No inspection record for ${args.url}`);
|
|
62
59
|
if (json) {
|
|
63
60
|
console.log(JSON.stringify(record, null, 2));
|
|
64
61
|
return;
|
|
@@ -114,10 +111,7 @@ const indexingSnapshotSubCommand = defineCommand({
|
|
|
114
111
|
return;
|
|
115
112
|
}
|
|
116
113
|
const claim = await recordStoreSite(store.dataDir, siteUrl, { userId: store.userId });
|
|
117
|
-
if (!claim.ok)
|
|
118
|
-
logger.error(formatSiteIdCollision(claim.error));
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
114
|
+
if (!claim.ok) throw new Error(formatSiteIdCollision(claim.error));
|
|
121
115
|
const records = [];
|
|
122
116
|
const failures = [];
|
|
123
117
|
let completed = 0;
|
|
@@ -155,7 +149,7 @@ const indexingSnapshotSubCommand = defineCommand({
|
|
|
155
149
|
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
156
150
|
}
|
|
157
151
|
}
|
|
158
|
-
if (failures.length > 0)
|
|
152
|
+
if (failures.length > 0) throw new Error(`${failures.length} of ${urls.length} URL(s) failed. First: ${failures[0].url}: ${failures[0].error}`);
|
|
159
153
|
}
|
|
160
154
|
});
|
|
161
155
|
const indexingSubCommand = defineCommand({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { siteSettingsPlace } from "../auth-state.mjs";
|
|
2
2
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
3
3
|
import { applyOutputMode, logger, readUrlList } from "../utils.mjs";
|
|
4
4
|
import { renderTable } from "../render/layout.mjs";
|
|
@@ -136,7 +136,7 @@ const addCommand = defineCommand({
|
|
|
136
136
|
async run({ args }) {
|
|
137
137
|
const { json } = applyOutputMode(args);
|
|
138
138
|
const urls = await readUrls(args);
|
|
139
|
-
const { client, site,
|
|
139
|
+
const { client, site, siteManager } = await resolveHostedSite({
|
|
140
140
|
...args,
|
|
141
141
|
_: []
|
|
142
142
|
}, {
|
|
@@ -151,7 +151,7 @@ const addCommand = defineCommand({
|
|
|
151
151
|
console.log(JSON.stringify(data, null, 2));
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
-
printChange("Added", data, site.siteUrl,
|
|
154
|
+
printChange("Added", data, site.siteUrl, siteManager);
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
const removeCommand = defineCommand({
|
|
@@ -167,7 +167,7 @@ const removeCommand = defineCommand({
|
|
|
167
167
|
async run({ args }) {
|
|
168
168
|
const { json } = applyOutputMode(args);
|
|
169
169
|
const urls = await readUrls(args);
|
|
170
|
-
const { client, site,
|
|
170
|
+
const { client, site, siteManager } = await resolveHostedSite({
|
|
171
171
|
...args,
|
|
172
172
|
_: []
|
|
173
173
|
}, {
|
|
@@ -182,7 +182,7 @@ const removeCommand = defineCommand({
|
|
|
182
182
|
console.log(JSON.stringify(data, null, 2));
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
-
printChange("Removed", data, site.siteUrl,
|
|
185
|
+
printChange("Removed", data, site.siteUrl, siteManager);
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
const indexingWatchCommand = defineCommand({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
1
2
|
import { indexingCommandMeta } from "../command-meta.mjs";
|
|
2
3
|
import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, readUrlList } from "../utils.mjs";
|
|
3
4
|
import { createCommandContext } from "../context.mjs";
|
|
@@ -5,7 +6,6 @@ import { loadSitemapUrls } from "../sitemap.mjs";
|
|
|
5
6
|
import { indexingSummaryCommand } from "./indexing-summary.mjs";
|
|
6
7
|
import { indexingUrlsCommand } from "./indexing-urls.mjs";
|
|
7
8
|
import { indexingWatchCommand } from "./indexing-watch.mjs";
|
|
8
|
-
import process from "node:process";
|
|
9
9
|
import { defineCommand } from "citty";
|
|
10
10
|
import { batchRequestIndexing, getIndexingMetadata, requestIndexing, runSequentialBatch } from "gscdump/indexing";
|
|
11
11
|
const RETRIES_ARG = { retries: {
|
|
@@ -16,10 +16,7 @@ async function resolveUrlSource(args) {
|
|
|
16
16
|
const fromSitemap = args["from-sitemap"];
|
|
17
17
|
if (fromSitemap) {
|
|
18
18
|
const result = await loadSitemapUrls(String(fromSitemap));
|
|
19
|
-
if (result._tag === "error") {
|
|
20
|
-
logger.error(`Sitemap fetch failed: ${result.message}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
19
|
+
if (result._tag === "error") throw new Error(`Sitemap fetch failed: ${result.message}`);
|
|
23
20
|
if (!result.value.complete) logger.warn("Sitemap walk was incomplete; indexing only the URLs that were read");
|
|
24
21
|
return result.value.urls;
|
|
25
22
|
}
|
|
@@ -214,20 +211,10 @@ const batchCommand = defineCommand({
|
|
|
214
211
|
const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
|
|
215
212
|
const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
|
|
216
213
|
const urls = await resolveUrlSource(args);
|
|
217
|
-
if (urls.length === 0)
|
|
218
|
-
logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
214
|
+
if (urls.length === 0) throw commandLineError("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
221
215
|
const type = String(args.type);
|
|
222
|
-
if (type !== "URL_UPDATED" && type !== "URL_DELETED") {
|
|
223
|
-
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
if (urls.length > INDEXING_DAILY_QUOTA && !args.yes && !args.json) {
|
|
227
|
-
logger.warn(`Submitting ${urls.length} URLs but the Indexing API daily quota is ${INDEXING_DAILY_QUOTA}/day.`);
|
|
228
|
-
logger.warn(`Excess submissions will fail with quota errors. Pass --yes to proceed anyway.`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
216
|
+
if (type !== "URL_UPDATED" && type !== "URL_DELETED") throw commandLineError(`Invalid --type: ${type}. Use URL_UPDATED or URL_DELETED.`);
|
|
217
|
+
if (urls.length > INDEXING_DAILY_QUOTA && !args.yes && !args.json) throw commandLineError(`Submitting ${urls.length} URLs but the Indexing API daily quota is ${INDEXING_DAILY_QUOTA}/day. Excess submissions will fail with quota errors. Pass --yes to proceed anyway.`);
|
|
231
218
|
if (urls.length > INDEXING_DAILY_QUOTA && !args.json && !args.quiet) logger.warn(`Proceeding with ${urls.length} URLs (over the ${INDEXING_DAILY_QUOTA}/day quota). Excess will fail.`);
|
|
232
219
|
const ctx = await createCommandContext({
|
|
233
220
|
needsAuth: true,
|
|
@@ -290,10 +277,7 @@ const batchStatusCommand = defineCommand({
|
|
|
290
277
|
const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
|
|
291
278
|
const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
|
|
292
279
|
const urls = await resolveUrlSource(args);
|
|
293
|
-
if (urls.length === 0)
|
|
294
|
-
logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
280
|
+
if (urls.length === 0) throw commandLineError("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
297
281
|
const ctx = await createCommandContext({
|
|
298
282
|
needsAuth: true,
|
|
299
283
|
fetchOptions: { retry }
|
package/dist/commands/init.mjs
CHANGED
|
@@ -100,10 +100,7 @@ const initCommand = defineCommand({
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
if (!canPrompt()) {
|
|
103
|
-
if (!await loadTokens()) {
|
|
104
|
-
logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
103
|
+
if (!await loadTokens()) throw new Error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
|
|
107
104
|
await saveConfig({
|
|
108
105
|
...config,
|
|
109
106
|
dataDir: config.dataDir ?? defaultDataDir()
|
|
@@ -114,7 +111,7 @@ const initCommand = defineCommand({
|
|
|
114
111
|
return;
|
|
115
112
|
}
|
|
116
113
|
console.log();
|
|
117
|
-
console.log(" \x1B[1mWelcome to
|
|
114
|
+
console.log(" \x1B[1mWelcome to gscdump\x1B[0m");
|
|
118
115
|
console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
|
|
119
116
|
console.log();
|
|
120
117
|
const dataDir = args.store ? await promptDataDir(config.dataDir) : void 0;
|
|
@@ -62,10 +62,7 @@ const papercutCommand = defineCommand({
|
|
|
62
62
|
fetch: globalThis.fetch,
|
|
63
63
|
url
|
|
64
64
|
});
|
|
65
|
-
if (result._tag === "Err")
|
|
66
|
-
logger.error(result.message);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
65
|
+
if (result._tag === "Err") throw new Error(result.message);
|
|
69
66
|
if (json) {
|
|
70
67
|
console.log(JSON.stringify(result.receipt));
|
|
71
68
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { commandLineError } from "../stop.mjs";
|
|
2
3
|
import { profileCommandMeta } from "../command-meta.mjs";
|
|
3
4
|
import { getConfigDir, setConfigDir } from "../config.mjs";
|
|
4
5
|
import { resolveCliEnvironment } from "../environment.mjs";
|
|
@@ -100,10 +101,7 @@ const pathCmd = defineCommand({
|
|
|
100
101
|
} },
|
|
101
102
|
async run({ args }) {
|
|
102
103
|
const name = args.name ? String(args.name) : resolveActiveProfile();
|
|
103
|
-
if (!name)
|
|
104
|
-
logger.error("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
104
|
+
if (!name) throw commandLineError("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
|
|
107
105
|
console.log(getProfileDir(name));
|
|
108
106
|
}
|
|
109
107
|
});
|
|
@@ -135,11 +133,7 @@ const useCmd = defineCommand({
|
|
|
135
133
|
applyOutputMode(args);
|
|
136
134
|
const name = String(args.name);
|
|
137
135
|
const dir = getProfileDir(name);
|
|
138
|
-
if (!await fs.stat(dir).then(() => true).catch(() => false)) {
|
|
139
|
-
logger.error(`Profile not found: ${name}`);
|
|
140
|
-
logger.info(`Create it with: gscdump profile create ${name}`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
136
|
+
if (!await fs.stat(dir).then(() => true).catch(() => false)) throw new Error(`Profile not found: ${name}. Create it with: gscdump profile create ${name}`);
|
|
143
137
|
await setActiveProfile(name);
|
|
144
138
|
logger.success(`Active profile: ${name}`);
|
|
145
139
|
}
|
|
@@ -207,10 +201,7 @@ const deleteCmd = defineCommand({
|
|
|
207
201
|
applyOutputMode(args);
|
|
208
202
|
const name = String(args.name);
|
|
209
203
|
const dir = getProfileDir(name);
|
|
210
|
-
if (!await fs.stat(dir).then(() => true).catch(() => false)) {
|
|
211
|
-
logger.error(`Profile not found: ${name}`);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
204
|
+
if (!await fs.stat(dir).then(() => true).catch(() => false)) throw new Error(`Profile not found: ${name}`);
|
|
214
205
|
if (!args.yes) {
|
|
215
206
|
const ok = await confirm({
|
|
216
207
|
message: `Delete profile "${name}" at ${dir}? Tokens and config will be lost.`,
|
package/dist/commands/query.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { commandLineError } from "../stop.mjs";
|
|
2
3
|
import { resolveAuthentication } from "../auth-state.mjs";
|
|
3
4
|
import { queryCommandMeta } from "../command-meta.mjs";
|
|
4
5
|
import { loadConfig } from "../config.mjs";
|
|
@@ -211,10 +212,7 @@ const queryCommand = defineCommand({
|
|
|
211
212
|
async run({ args }) {
|
|
212
213
|
const ctxConfig = await loadConfig();
|
|
213
214
|
const format = args.format ?? ctxConfig.defaultFormat ?? "json";
|
|
214
|
-
if (format !== "json" && format !== "csv" && format !== "table")
|
|
215
|
-
logger.error("Invalid --format. Use table, json, or csv.");
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
215
|
+
if (format !== "json" && format !== "csv" && format !== "table") throw commandLineError("Invalid --format. Use table, json, or csv.");
|
|
218
216
|
setJsonOutput(format === "json");
|
|
219
217
|
if (args.sql || args.schema) {
|
|
220
218
|
await runSqlMode({
|
|
@@ -234,31 +232,19 @@ const queryCommand = defineCommand({
|
|
|
234
232
|
const dimNames = await resolveDimensions(args);
|
|
235
233
|
const windowFlags = await promptRange(args);
|
|
236
234
|
const invalidWindow = checkWindowFlags(windowFlags);
|
|
237
|
-
if (invalidWindow)
|
|
238
|
-
logger.error(invalidWindow.message);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
235
|
+
if (invalidWindow) throw commandLineError(invalidWindow.message);
|
|
241
236
|
await promptFilters(args);
|
|
242
237
|
const limitArg = String(args.limit ?? ctxConfig.defaultLimit ?? 1e3);
|
|
243
238
|
const rowLimit = Number(limitArg);
|
|
244
|
-
if (!POSITIVE_INTEGER_RE.test(limitArg) || !Number.isSafeInteger(rowLimit) || rowLimit < 1)
|
|
245
|
-
logger.error("Invalid --limit. Use a positive safe integer, such as --limit 1000.");
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
239
|
+
if (!POSITIVE_INTEGER_RE.test(limitArg) || !Number.isSafeInteger(rowLimit) || rowLimit < 1) throw commandLineError("Invalid --limit. Use a positive safe integer, such as --limit 1000.");
|
|
248
240
|
const filters = parseFilterArgs(args);
|
|
249
241
|
const searchType = parseSearchType(args.type ?? ctxConfig.defaultSearchType, "--type");
|
|
250
242
|
const localSearchType = searchType ?? "web";
|
|
251
243
|
const dataState = args["data-state"] ? String(args["data-state"]) : ctxConfig.defaultDataState;
|
|
252
244
|
const aggregationType = args["aggregation-type"] ? String(args["aggregation-type"]) : void 0;
|
|
253
245
|
const filterDims = filterDimensions(filters);
|
|
254
|
-
if (dataState && !DATA_STATES.includes(dataState)) {
|
|
255
|
-
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
if (aggregationType && !AGGREGATION_TYPES.includes(aggregationType)) {
|
|
259
|
-
logger.error(`Invalid --aggregation-type: ${aggregationType}. Allowed: ${AGGREGATION_TYPES.join(", ")}`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
246
|
+
if (dataState && !DATA_STATES.includes(dataState)) throw commandLineError(`Invalid --data-state: ${dataState}. Allowed: ${DATA_STATES.join(", ")}`);
|
|
247
|
+
if (aggregationType && !AGGREGATION_TYPES.includes(aggregationType)) throw commandLineError(`Invalid --aggregation-type: ${aggregationType}. Allowed: ${AGGREGATION_TYPES.join(", ")}`);
|
|
262
248
|
const forceLive = Boolean(args.live);
|
|
263
249
|
if (!forceLive && (await resolveAuthentication())._tag === "Hosted") {
|
|
264
250
|
if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
|
|
@@ -417,10 +403,7 @@ const queryCommand = defineCommand({
|
|
|
417
403
|
table,
|
|
418
404
|
searchType: localSearchType,
|
|
419
405
|
...probe ? { profiler: probe.profiler } : {}
|
|
420
|
-
}, state)
|
|
421
|
-
logger.error(`Query failed: ${e.message}`);
|
|
422
|
-
process.exit(1);
|
|
423
|
-
});
|
|
406
|
+
}, state);
|
|
424
407
|
if (probe) logProfile(probe.spans);
|
|
425
408
|
await writeOutput({
|
|
426
409
|
output: {
|
|
@@ -443,10 +426,7 @@ const queryCommand = defineCommand({
|
|
|
443
426
|
async function resolveDimensions(args) {
|
|
444
427
|
if (args.dimensions != null) {
|
|
445
428
|
const dimensions = String(args.dimensions).split(",").map((d) => d.trim());
|
|
446
|
-
if (dimensions.some((d) => !DIMENSIONS.includes(d))) {
|
|
447
|
-
logger.error(`Invalid --dimensions. Use a comma-separated list from: ${DIMENSIONS.join(", ")}.`);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
429
|
+
if (dimensions.some((d) => !DIMENSIONS.includes(d))) throw commandLineError(`Invalid --dimensions. Use a comma-separated list from: ${DIMENSIONS.join(", ")}.`);
|
|
450
430
|
return [...new Set(dimensions)];
|
|
451
431
|
}
|
|
452
432
|
if (args.interactive) {
|
|
@@ -500,10 +480,7 @@ async function promptRange(args) {
|
|
|
500
480
|
}
|
|
501
481
|
function windowOrExit(flags, anchor) {
|
|
502
482
|
const window = parseWindowFlags(flags, DEFAULT_WINDOW, anchor);
|
|
503
|
-
if (!window.ok)
|
|
504
|
-
logger.error(window.error.message);
|
|
505
|
-
process.exit(1);
|
|
506
|
-
}
|
|
483
|
+
if (!window.ok) throw commandLineError(window.error.message);
|
|
507
484
|
return window.value;
|
|
508
485
|
}
|
|
509
486
|
async function promptFilters(args) {
|
|
@@ -620,8 +597,7 @@ async function runSql(views, sql) {
|
|
|
620
597
|
for (const warning of warnings) logger.warn(warning);
|
|
621
598
|
return {
|
|
622
599
|
...await views.run(sql).catch((e) => {
|
|
623
|
-
|
|
624
|
-
process.exit(1);
|
|
600
|
+
throw new Error(`SQL failed: ${e.message}`, { cause: e });
|
|
625
601
|
}),
|
|
626
602
|
warnings
|
|
627
603
|
};
|
|
@@ -3,7 +3,6 @@ import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, plural } from
|
|
|
3
3
|
import { createCommandContext } from "../context.mjs";
|
|
4
4
|
import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
|
|
5
5
|
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
6
|
-
import process from "node:process";
|
|
7
6
|
import { defineCommand } from "citty";
|
|
8
7
|
import { fetchSitemap } from "gscdump/sites";
|
|
9
8
|
const HOSTED_SITEMAP_ALTERNATIVE = "read a live sitemap with `gscdump sitemaps urls <sitemap-url>`";
|
|
@@ -207,14 +206,8 @@ const discoverCommand = defineCommand({
|
|
|
207
206
|
}, null, 2));
|
|
208
207
|
return;
|
|
209
208
|
}
|
|
210
|
-
if (discovery._tag === "incomplete") {
|
|
211
|
-
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
if (!url) {
|
|
215
|
-
logger.warn(`No sitemap discovered for ${domain}`);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
209
|
+
if (discovery._tag === "incomplete") throw new Error(`Sitemap discovery was incomplete: ${discovery.failures[0] ?? "unknown failure"}`);
|
|
210
|
+
if (!url) throw new Error(`No sitemap discovered for ${domain}`);
|
|
218
211
|
logger.success(`Discovered sitemap: ${url}`);
|
|
219
212
|
}
|
|
220
213
|
});
|
package/dist/commands/sites.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
1
2
|
import { connectSiteStep, formatHostedSync, getHostedAccount, localModeRequired, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
|
|
2
3
|
import { sitesCommandMeta } from "../command-meta.mjs";
|
|
3
4
|
import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
|
|
@@ -20,16 +21,10 @@ function pickDefaultMethod(siteUrl) {
|
|
|
20
21
|
}
|
|
21
22
|
function validateMethod(siteUrl, method) {
|
|
22
23
|
const upper = method.toUpperCase();
|
|
23
|
-
if (!ALL_METHODS.includes(upper)) {
|
|
24
|
-
logger.error(`Invalid --method: ${method}. Valid: ${ALL_METHODS.join(", ")}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
24
|
+
if (!ALL_METHODS.includes(upper)) throw commandLineError(`Invalid --method: ${method}. Valid: ${ALL_METHODS.join(", ")}`);
|
|
27
25
|
const site = siteUrlToVerificationSite(siteUrl);
|
|
28
26
|
const allowed = verificationMethodsFor(site);
|
|
29
|
-
if (!allowed.includes(upper)) {
|
|
30
|
-
logger.error(`Method ${upper} not valid for ${site.type === "INET_DOMAIN" ? "domain" : "URL-prefix"} property "${siteUrl}". Valid: ${allowed.join(", ")}`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
27
|
+
if (!allowed.includes(upper)) throw commandLineError(`Method ${upper} not valid for ${site.type === "INET_DOMAIN" ? "domain" : "URL-prefix"} property "${siteUrl}". Valid: ${allowed.join(", ")}`);
|
|
33
28
|
return upper;
|
|
34
29
|
}
|
|
35
30
|
function printPlacementInstructions(method, siteUrl, token) {
|
|
@@ -423,7 +418,7 @@ const LIST_ARGS = {
|
|
|
423
418
|
};
|
|
424
419
|
async function runListHostedSites(args, authentication) {
|
|
425
420
|
if (args["with-sitemaps"] || args["owner-only"]) throw localModeRequired("--with-sitemaps and --owner-only read Search Console permissions.");
|
|
426
|
-
const { sites } = await getHostedAccount(authentication);
|
|
421
|
+
const { sites, issuer } = await getHostedAccount(authentication);
|
|
427
422
|
if (args.json) {
|
|
428
423
|
console.log(JSON.stringify(sites.map((site) => ({
|
|
429
424
|
siteId: site.siteId,
|
|
@@ -438,7 +433,7 @@ async function runListHostedSites(args, authentication) {
|
|
|
438
433
|
return;
|
|
439
434
|
}
|
|
440
435
|
if (sites.length === 0) {
|
|
441
|
-
logger.warn(`Your hosted record has no Sites. ${connectSiteStep(siteManagerOf(authentication))}`);
|
|
436
|
+
logger.warn(`Your hosted record has no Sites. ${connectSiteStep(siteManagerOf(authentication, issuer))}`);
|
|
442
437
|
return;
|
|
443
438
|
}
|
|
444
439
|
logger.success(`Found ${plural(sites.length, "hosted Site", "hosted Sites")}:`);
|
package/dist/commands/skill.mjs
CHANGED
|
@@ -33,10 +33,7 @@ const installCommand = defineCommand({
|
|
|
33
33
|
homeDirectory: os.homedir(),
|
|
34
34
|
target: args.target
|
|
35
35
|
});
|
|
36
|
-
if (result._tag === "Err")
|
|
37
|
-
logger.error(result.message);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
36
|
+
if (result._tag === "Err") throw new Error(result.message);
|
|
40
37
|
if (json) {
|
|
41
38
|
console.log(JSON.stringify(result.installation));
|
|
42
39
|
return;
|
package/dist/commands/sync.mjs
CHANGED
|
@@ -395,10 +395,7 @@ const syncCommand = defineCommand({
|
|
|
395
395
|
siteUrls.push(siteUrl);
|
|
396
396
|
continue;
|
|
397
397
|
}
|
|
398
|
-
if (!args["all-sites"])
|
|
399
|
-
logger.error(formatSiteIdCollision(claim.error));
|
|
400
|
-
process.exit(1);
|
|
401
|
-
}
|
|
398
|
+
if (!args["all-sites"]) throw new Error(formatSiteIdCollision(claim.error));
|
|
402
399
|
logger.warn(`Skipped ${siteUrl}. ${formatSiteIdCollision(claim.error)}`);
|
|
403
400
|
}
|
|
404
401
|
const latest = getLatestGscDate();
|
|
@@ -416,10 +413,7 @@ const syncCommand = defineCommand({
|
|
|
416
413
|
now: Date.now(),
|
|
417
414
|
isAlive: isProcessAlive
|
|
418
415
|
});
|
|
419
|
-
if (existing.kind === "running") {
|
|
420
|
-
logger.error(`Another sync is running (pid ${existing.record.pid}, ${existing.record.done}/${existing.record.planned} days). Wait for it to finish, or run \`gscdump sync --status\`.`);
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
416
|
+
if (existing.kind === "running") throw new Error(`Another sync is running (pid ${existing.record.pid}, ${existing.record.done}/${existing.record.planned} days). Wait for it to finish, or run \`gscdump sync --status\`.`);
|
|
423
417
|
}
|
|
424
418
|
const ledger = await openQuotaLedger({ dataDir: store.dataDir });
|
|
425
419
|
const pacer = createRequestPacer({
|
package/dist/error-handler.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HOSTED_SESSION_EXPIRED, isSessionExpiredError } from "./hosted-session.mjs";
|
|
2
|
+
import { isUserStop, stopDetailsOf } from "./stop.mjs";
|
|
2
3
|
import { isUsageError } from "./command-registry.mjs";
|
|
3
4
|
import { describeRecordReadRefusal } from "./hosted-query.mjs";
|
|
4
5
|
import { quotaStopOf } from "./quota-ledger.mjs";
|
|
@@ -128,7 +129,17 @@ function describeCliError(error) {
|
|
|
128
129
|
};
|
|
129
130
|
if (error instanceof Error && quotaStopOf(error)) return {
|
|
130
131
|
kind: "stop",
|
|
131
|
-
message: error.message
|
|
132
|
+
message: error.message,
|
|
133
|
+
stop: {
|
|
134
|
+
code: "QUOTA_USED_UP",
|
|
135
|
+
message: error.message,
|
|
136
|
+
nextCommand: null
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
if (isUserStop(error) && stop) return {
|
|
140
|
+
kind: "stop",
|
|
141
|
+
message: stop.message,
|
|
142
|
+
stop
|
|
132
143
|
};
|
|
133
144
|
if (error instanceof Error && stop) return {
|
|
134
145
|
kind: "expected",
|
|
@@ -157,6 +168,13 @@ function describeCliError(error) {
|
|
|
157
168
|
hint: error.mode === "local" ? "Pass --live to run against the GSC API." : "",
|
|
158
169
|
showAuthSources: false
|
|
159
170
|
};
|
|
171
|
+
if (isSessionExpiredError(error)) return {
|
|
172
|
+
kind: "expected",
|
|
173
|
+
message: HOSTED_SESSION_EXPIRED.message,
|
|
174
|
+
hint: "",
|
|
175
|
+
showAuthSources: false,
|
|
176
|
+
stop: HOSTED_SESSION_EXPIRED
|
|
177
|
+
};
|
|
160
178
|
const refusal = recordReadRefusalOf(error);
|
|
161
179
|
if (refusal) {
|
|
162
180
|
const text = describeRecordReadRefusal(refusal);
|
|
@@ -175,9 +193,33 @@ function describeCliError(error) {
|
|
|
175
193
|
showAuthSources: classified.kind === "auth-expired" || classified.kind === "permission-denied" || isAuthError(error)
|
|
176
194
|
};
|
|
177
195
|
}
|
|
196
|
+
function jsonErrorOf(report) {
|
|
197
|
+
switch (report.kind) {
|
|
198
|
+
case "usage": return {
|
|
199
|
+
code: "USAGE",
|
|
200
|
+
message: report.message,
|
|
201
|
+
nextCommand: null
|
|
202
|
+
};
|
|
203
|
+
case "stop": return report.stop ?? {
|
|
204
|
+
code: "FAILED",
|
|
205
|
+
message: report.message,
|
|
206
|
+
nextCommand: null
|
|
207
|
+
};
|
|
208
|
+
case "expected": return report.stop ?? {
|
|
209
|
+
code: "FAILED",
|
|
210
|
+
message: report.hint ? `${report.message} ${report.hint}` : report.message,
|
|
211
|
+
nextCommand: null
|
|
212
|
+
};
|
|
213
|
+
case "defect": return {
|
|
214
|
+
code: "UNEXPECTED",
|
|
215
|
+
message: report.message,
|
|
216
|
+
nextCommand: null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
178
220
|
async function reportCliError(error, options) {
|
|
179
221
|
const report = describeCliError(error);
|
|
180
|
-
if (options.json
|
|
222
|
+
if (options.json) (options.writeStdout ?? ((value) => console.log(value)))(JSON.stringify({ error: jsonErrorOf(report) }, null, 2));
|
|
181
223
|
const red = (text) => options.color ? `\x1B[31m${text}\x1B[0m` : text;
|
|
182
224
|
const lines = [];
|
|
183
225
|
switch (report.kind) {
|
|
@@ -198,4 +240,4 @@ async function reportCliError(error, options) {
|
|
|
198
240
|
const text = lines.join("\n");
|
|
199
241
|
(options.write ?? ((value) => console.error(value)))(options.color ? text : text.replace(ANSI_RE, ""));
|
|
200
242
|
}
|
|
201
|
-
export { LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, reportCliError };
|
|
243
|
+
export { LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, jsonErrorOf, reportCliError };
|
package/dist/hosted-auth.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { userStopError } from "./stop.mjs";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
const ORIGIN = "https://gscdump.com";
|
|
3
4
|
const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
|
|
@@ -5,6 +6,12 @@ const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal(
|
|
|
5
6
|
sessionId: z.string().regex(/^[a-f0-9]{64}$/)
|
|
6
7
|
})]);
|
|
7
8
|
const HOSTED_LOGIN_FAILURE = "Hosted login failed. Run `gscdump auth login --mode hosted` to try again.";
|
|
9
|
+
const LOGIN_CANCELLED = {
|
|
10
|
+
code: "LOGIN_CANCELLED",
|
|
11
|
+
message: "Login cancelled in the browser.",
|
|
12
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
13
|
+
};
|
|
14
|
+
const loginCancelledBodySchema = z.object({ data: z.object({ details: z.object({ reason: z.literal("login_cancelled") }) }) });
|
|
8
15
|
async function requestJson(request, route, init = {}) {
|
|
9
16
|
const response = await request(`${ORIGIN}/api/cli/auth/${route}`, {
|
|
10
17
|
...init,
|
|
@@ -13,6 +20,7 @@ async function requestJson(request, route, init = {}) {
|
|
|
13
20
|
});
|
|
14
21
|
if (!response.ok) {
|
|
15
22
|
if (response.status === 429 || response.status >= 500) throw new Error("gscdump.com login is temporarily unavailable. Try again later.");
|
|
23
|
+
if (response.status === 403 && loginCancelledBodySchema.safeParse(await response.json().catch(() => null)).success) throw userStopError(LOGIN_CANCELLED);
|
|
16
24
|
throw new Error(HOSTED_LOGIN_FAILURE);
|
|
17
25
|
}
|
|
18
26
|
return response.json();
|
|
@@ -31,4 +39,4 @@ async function loginWithHostedSession(deps) {
|
|
|
31
39
|
}
|
|
32
40
|
throw new Error("Authorization expired. Run `gscdump auth login --mode hosted` to try again.");
|
|
33
41
|
}
|
|
34
|
-
export { loginWithHostedSession };
|
|
42
|
+
export { LOGIN_CANCELLED, loginWithHostedSession };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const sessionExpiredDetailsSchema = z.object({ reason: z.literal("session_expired") });
|
|
3
|
+
const sessionExpiredBodySchema = z.union([z.object({ data: z.object({ details: sessionExpiredDetailsSchema }) }), z.object({ error: z.object({ details: sessionExpiredDetailsSchema }) })]);
|
|
4
|
+
const HOSTED_SESSION_EXPIRED = {
|
|
5
|
+
code: "HOSTED_CREDENTIALS_REJECTED",
|
|
6
|
+
message: "Your gscdump.com session expired. Run `gscdump auth login --mode hosted`.",
|
|
7
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
8
|
+
};
|
|
9
|
+
function isSessionExpiredBody(body) {
|
|
10
|
+
return sessionExpiredBodySchema.safeParse(body).success;
|
|
11
|
+
}
|
|
12
|
+
function isSessionExpiredError(error) {
|
|
13
|
+
if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return false;
|
|
14
|
+
const { status, details } = error;
|
|
15
|
+
return status === 401 && sessionExpiredDetailsSchema.safeParse(details).success;
|
|
16
|
+
}
|
|
17
|
+
export { HOSTED_SESSION_EXPIRED, isSessionExpiredBody, isSessionExpiredError };
|
package/dist/hosted-site.mjs
CHANGED
|
@@ -97,15 +97,18 @@ async function resolveHostedSite(args, command) {
|
|
|
97
97
|
});
|
|
98
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.");
|
|
99
99
|
const target = args.site ? String(args.site) : (await loadConfig()).defaultSite;
|
|
100
|
-
const
|
|
101
|
-
|
|
100
|
+
const account = await getHostedAccount(authentication);
|
|
101
|
+
const siteManager = siteManagerOf(authentication, account.issuer);
|
|
102
|
+
const match = matchHostedSite(account.sites, target);
|
|
103
|
+
if (match.kind !== "found") throw stopError(siteMatchStop(match, siteManager));
|
|
102
104
|
return {
|
|
103
105
|
client: createGscdumpV1Client({
|
|
104
106
|
apiRoot: authentication.apiRoot,
|
|
105
107
|
credential: hostedCredential(authentication)
|
|
106
108
|
}),
|
|
107
109
|
site: match.site,
|
|
108
|
-
authentication
|
|
110
|
+
authentication,
|
|
111
|
+
siteManager
|
|
109
112
|
};
|
|
110
113
|
}
|
|
111
114
|
export { HOSTED_ARGS, matchHostedSite, noSitesStop, resolveHostedSite, siteMatchStop };
|
package/dist/package.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var version = "5.
|
|
1
|
+
var version = "5.3.0";
|
|
2
2
|
export { version };
|
package/dist/stop.mjs
CHANGED
|
@@ -7,4 +7,17 @@ function stopError(details, name = "StopError") {
|
|
|
7
7
|
function stopDetailsOf(error) {
|
|
8
8
|
return error instanceof Error ? error.stopDetails : void 0;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
function userStopError(details) {
|
|
11
|
+
return Object.assign(stopError(details, "UserStopError"), { userStop: true });
|
|
12
|
+
}
|
|
13
|
+
function isUserStop(error) {
|
|
14
|
+
return error instanceof Error && error.userStop === true;
|
|
15
|
+
}
|
|
16
|
+
function commandLineError(message) {
|
|
17
|
+
return stopError({
|
|
18
|
+
code: "USAGE",
|
|
19
|
+
message,
|
|
20
|
+
nextCommand: null
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export { commandLineError, isUserStop, stopDetailsOf, stopError, userStopError };
|
package/dist/utils.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCliRuntime } from "./runtime.mjs";
|
|
2
|
+
import { commandLineError } from "./stop.mjs";
|
|
2
3
|
import { version } from "./package.mjs";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
@@ -56,29 +57,26 @@ function applyOutputMode(args) {
|
|
|
56
57
|
function parseSearchType(value, flag = "--search-type") {
|
|
57
58
|
if (!value) return void 0;
|
|
58
59
|
const v = String(value);
|
|
59
|
-
if (!ALL_SEARCH_TYPES.includes(v)) {
|
|
60
|
-
logger.error(`Invalid ${flag}: ${v}. Allowed: ${ALL_SEARCH_TYPES.join(", ")}`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
60
|
+
if (!ALL_SEARCH_TYPES.includes(v)) throw commandLineError(`Invalid ${flag}: ${v}. Allowed: ${ALL_SEARCH_TYPES.join(", ")}`);
|
|
63
61
|
return v;
|
|
64
62
|
}
|
|
65
63
|
function parseNameList(value, known, flag) {
|
|
66
64
|
const names = String(value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
67
65
|
const unknown = names.filter((name) => !known.includes(name));
|
|
68
|
-
if (unknown.length > 0) throw
|
|
69
|
-
if (names.length === 0) throw
|
|
66
|
+
if (unknown.length > 0) throw commandLineError(`Unknown ${flag} name: ${unknown.join(", ")}. Valid names: ${known.join(", ")}.`);
|
|
67
|
+
if (names.length === 0) throw commandLineError(`${flag} needs at least one name. Valid names: ${known.join(", ")}.`);
|
|
70
68
|
return names;
|
|
71
69
|
}
|
|
72
70
|
function parseIntegerOption(value, flag, minimum = 1) {
|
|
73
71
|
if (value === void 0 || value === null) return void 0;
|
|
74
72
|
const text = typeof value === "number" || typeof value === "string" ? String(value).trim() : "";
|
|
75
73
|
const parsed = Number(text);
|
|
76
|
-
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < minimum) throw
|
|
74
|
+
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < minimum) throw commandLineError(`${flag} must be ${minimum === 0 ? "a non-negative integer" : "a positive integer"}.`);
|
|
77
75
|
return parsed;
|
|
78
76
|
}
|
|
79
77
|
function parseFetchBudget(value) {
|
|
80
78
|
const budget = parseIntegerOption(value, "--fetch-budget");
|
|
81
|
-
if (budget !== void 0 && budget > MAX_FETCH_BUDGET) throw
|
|
79
|
+
if (budget !== void 0 && budget > MAX_FETCH_BUDGET) throw commandLineError(`--fetch-budget must be at most ${MAX_FETCH_BUDGET}. Larger fetches use up the GSC quota.`);
|
|
82
80
|
return budget;
|
|
83
81
|
}
|
|
84
82
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
@@ -145,7 +143,7 @@ function applyGradient(text) {
|
|
|
145
143
|
}
|
|
146
144
|
function showSplash() {
|
|
147
145
|
console.log();
|
|
148
|
-
console.log(` ${applyGradient("
|
|
146
|
+
console.log(` ${applyGradient("gscdump")} v${VERSION}`);
|
|
149
147
|
console.log();
|
|
150
148
|
}
|
|
151
149
|
function clearLine() {
|
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.3.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.3.0",
|
|
48
|
+
"@gscdump/contracts": "^5.3.0",
|
|
49
|
+
"@gscdump/engine": "^5.3.0",
|
|
50
|
+
"@gscdump/engine-gsc-api": "^5.3.0",
|
|
51
|
+
"@gscdump/sdk": "^5.3.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.3.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. With JSON output it also prints `{ "error" }` on stdout. See [Stops](#stops).
|
|
15
15
|
Example: `gscdump query --site=SITE --start=DATE --end=DATE -d page -f json`.
|
|
16
16
|
|
|
17
17
|
## Start each task
|
|
@@ -44,8 +44,10 @@ In Local mode, `googleAuthenticated: true` means Google accepted the credentials
|
|
|
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
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
|
|
47
|
+
Tell the user to connect a Site in the place the `NO_SITES` message names.
|
|
48
|
+
A browser login connects Sites on gscdump.com.
|
|
48
49
|
An API key connects Sites in the app that issued the key. That app can be a partner app, not gscdump.com.
|
|
50
|
+
When gscdump.com names the app, the message names it too, for example `Connect a Site in Request Indexing.`
|
|
49
51
|
|
|
50
52
|
`--mode local|hosted` overrides one invocation. `GSCDUMP_AUTH_MODE` also overrides the saved mode.
|
|
51
53
|
A successful login saves the mode per profile.
|
|
@@ -131,7 +133,7 @@ mixes Store rows and live rows.
|
|
|
131
133
|
- `--live` always asks Search Console. It needs Local mode with Google credentials.
|
|
132
134
|
- `query --sql` reads the Store only. It stops when no table it names has data.
|
|
133
135
|
- JSON output carries `meta.source`: `local`, `live`, or `hosted`.
|
|
134
|
-
- A
|
|
136
|
+
- A failure with JSON output prints `{ "error" }` on stdout and exits 1. See [Stops](#stops).
|
|
135
137
|
Partial coverage and a running sync are normal progress. Run `nextCommand`, or tell the user to.
|
|
136
138
|
- A sync is running only while its heartbeat is recent. A killed sync does not block reads.
|
|
137
139
|
- Every Search Analytics, URL Inspection, and Indexing API call spends the shared quota ledger in the data dir.
|
|
@@ -226,20 +228,24 @@ Do not rewrite rows, estimate metrics, or add manually calculated totals.
|
|
|
226
228
|
## Stops
|
|
227
229
|
|
|
228
230
|
A stop ends a command at a known condition and exits 1. stderr gets the message.
|
|
229
|
-
If the command writes JSON,
|
|
231
|
+
If the command writes JSON, every failure prints `{ "error": { "code", "message", "nextCommand" } }` on stdout. A stop has a code from the table below.
|
|
232
|
+
Any other failure has the code `FAILED`, such as a network or API error, or `UNEXPECTED`, a defect in the CLI.
|
|
230
233
|
A command writes JSON with `--json`, with `--format json`, and for `query` with its default format.
|
|
234
|
+
If a command printed its JSON result before it failed, such as `inspect` after a quota stop, stdout holds only that result.
|
|
231
235
|
Read `code`, not the message. If `nextCommand` is not null, run it, or tell the user to run it.
|
|
232
236
|
A `gscdump auth login` command needs the user. Tell the user to run it. If `nextCommand` is null, tell the user the `message`.
|
|
233
237
|
A routing stop also has `siteUrl`. `STORE_RANGE_NOT_COVERED` adds `missingDates`, and `SYNC_RUNNING` adds `sync.done` and `sync.total`.
|
|
234
238
|
|
|
235
239
|
| Code | Meaning |
|
|
236
240
|
| --- | --- |
|
|
241
|
+
| `USAGE` | The command line is wrong: an unknown command or option, or a value the option does not accept. Fix it with the command's `--help` |
|
|
237
242
|
| `NOT_CONNECTED` | The Store cannot answer, and Google is not connected |
|
|
238
243
|
| `STORE_RANGE_NOT_COVERED` | The Store does not have `missingDates` |
|
|
239
244
|
| `SYNC_RUNNING` | A sync is still filling the dates |
|
|
240
245
|
| `NO_SYNCED_DATA` | A Store-only read found no synced data |
|
|
241
246
|
| `STORE_ONLY` | The read needs the Store, so `--live` cannot run it |
|
|
242
247
|
| `LIVE_ONLY` | Only `--live` can answer the read |
|
|
248
|
+
| `LOGIN_CANCELLED` | The user selected Cancel on the gscdump.com login page. Ask before you run `nextCommand` |
|
|
243
249
|
| `LOCAL_MODE_REQUIRED` | The command calls Google, and Hosted mode is selected |
|
|
244
250
|
| `HOSTED_MODE_REQUIRED` | The command reads the hosted record, and Local mode is selected |
|
|
245
251
|
| `HOSTED_CREDENTIALS_MISSING` | Hosted mode has no CLI session and no API key |
|
|
@@ -251,6 +257,7 @@ A routing stop also has `siteUrl`. `STORE_RANGE_NOT_COVERED` adds `missingDates`
|
|
|
251
257
|
| `BING_NOT_CONNECTED` | The Site has no hosted Bing connection |
|
|
252
258
|
| `RECORD_NOT_READY` | gscdump has not prepared the Site's record for reads yet |
|
|
253
259
|
| `RANGE_NOT_SYNCED` | The hosted record does not hold the requested dates |
|
|
260
|
+
| `QUOTA_USED_UP` | A Google API quota is used up. The message says when it resets |
|
|
254
261
|
|
|
255
262
|
## Commands
|
|
256
263
|
|