@gscdump/cli 5.0.0 → 5.2.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 +60 -7
- package/dist/bing-hosted.mjs +26 -5
- package/dist/cli.d.mts +6 -0
- package/dist/cli.mjs +47 -6
- package/dist/command-meta.mjs +2 -2
- package/dist/commands/analyze.mjs +2 -2
- package/dist/commands/auth.mjs +13 -14
- package/dist/commands/bing.mjs +7 -2
- package/dist/commands/config.mjs +5 -15
- package/dist/commands/doctor.mjs +2 -2
- package/dist/commands/dump.mjs +2 -4
- package/dist/commands/entities.mjs +3 -9
- package/dist/commands/indexing-urls.mjs +2 -2
- package/dist/commands/indexing-watch.mjs +10 -7
- package/dist/commands/indexing.mjs +6 -22
- package/dist/commands/init.mjs +3 -6
- package/dist/commands/mcp.mjs +1 -1
- package/dist/commands/papercut.mjs +2 -5
- package/dist/commands/profile.mjs +5 -14
- package/dist/commands/query.mjs +16 -40
- package/dist/commands/report.mjs +2 -2
- package/dist/commands/sitemaps.mjs +3 -10
- package/dist/commands/sites.mjs +7 -12
- package/dist/commands/skill.mjs +1 -4
- package/dist/commands/sync.mjs +2 -8
- package/dist/config.mjs +1 -1
- package/dist/context.mjs +3 -3
- package/dist/error-handler.mjs +72 -16
- package/dist/hosted-auth.mjs +9 -1
- package/dist/hosted-session.mjs +17 -0
- package/dist/hosted-site.mjs +41 -18
- 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 +23 -0
- package/dist/utils.mjs +13 -14
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +43 -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,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HOSTED_SESSION_EXPIRED, isSessionExpiredBody } from "./hosted-session.mjs";
|
|
2
2
|
import { useCliRuntime } from "./runtime.mjs";
|
|
3
|
+
import { stopError } from "./stop.mjs";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
const HOSTED_SESSION_REJECTED = "gscdump.com rejected the CLI session. Run `gscdump auth login --mode hosted` again.";
|
|
9
|
+
const HOSTED_KEY_REJECTED = "gscdump.com rejected the API key. Run `gscdump auth login --mode hosted --api-key KEY` with a valid key.";
|
|
8
10
|
const LOCAL_MODE_REQUIRED = [
|
|
9
11
|
"This command calls Google, so it needs Local mode.",
|
|
10
12
|
"Hosted mode reads your gscdump.com record and never calls Google.",
|
|
11
13
|
"If you want Local mode, run `gscdump auth login --mode local`."
|
|
12
14
|
].join("\n");
|
|
15
|
+
function localModeRequired(detail) {
|
|
16
|
+
return stopError({
|
|
17
|
+
code: "LOCAL_MODE_REQUIRED",
|
|
18
|
+
message: detail ? `${detail}\n${LOCAL_MODE_REQUIRED}` : LOCAL_MODE_REQUIRED,
|
|
19
|
+
nextCommand: "gscdump auth login --mode local"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
13
22
|
const HOSTED_NOT_SET_UP = "Hosted credentials are missing. Run `gscdump auth login --mode hosted`, or set GSCDUMP_API_KEY.";
|
|
14
23
|
const SAVED_STATE_INVALID = "The saved access mode is invalid. Run `gscdump auth login --mode local` or `gscdump auth login --mode hosted`.";
|
|
15
24
|
const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).refine((value) => {
|
|
@@ -129,7 +138,11 @@ async function resolveAuthentication() {
|
|
|
129
138
|
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
139
|
return state;
|
|
131
140
|
}
|
|
132
|
-
throw
|
|
141
|
+
throw stopError({
|
|
142
|
+
code: "HOSTED_CREDENTIALS_MISSING",
|
|
143
|
+
message: HOSTED_NOT_SET_UP,
|
|
144
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
145
|
+
});
|
|
133
146
|
}
|
|
134
147
|
const hostedSiteSchema = z.object({
|
|
135
148
|
siteId: z.string(),
|
|
@@ -144,12 +157,18 @@ const hostedSiteSchema = z.object({
|
|
|
144
157
|
newestDateSynced: z.string().nullable().optional(),
|
|
145
158
|
oldestDateSynced: z.string().nullable().optional()
|
|
146
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
|
+
})]);
|
|
147
165
|
const accountSchema = z.object({
|
|
148
166
|
user: z.object({
|
|
149
167
|
publicId: z.string(),
|
|
150
168
|
email: z.string()
|
|
151
169
|
}),
|
|
152
|
-
sites: z.array(hostedSiteSchema)
|
|
170
|
+
sites: z.array(hostedSiteSchema),
|
|
171
|
+
issuer: keyIssuerSchema.optional().catch(void 0)
|
|
153
172
|
});
|
|
154
173
|
async function hostedRequest(state, route, options = {}) {
|
|
155
174
|
const response = await fetch(`${state.apiRoot}${route}`, {
|
|
@@ -162,18 +181,52 @@ async function hostedRequest(state, route, options = {}) {
|
|
|
162
181
|
redirect: "error"
|
|
163
182
|
});
|
|
164
183
|
if (!response.ok) {
|
|
165
|
-
const
|
|
166
|
-
throw Object.assign(new Error(message), {
|
|
184
|
+
const details = {
|
|
167
185
|
statusCode: response.status,
|
|
168
186
|
retryAfter: response.headers.get("retry-after"),
|
|
169
187
|
response
|
|
170
|
-
}
|
|
188
|
+
};
|
|
189
|
+
if (response.status === 401) {
|
|
190
|
+
const session = "sessionId" in state;
|
|
191
|
+
const expired = session && isSessionExpiredBody(await response.json().catch(() => null));
|
|
192
|
+
throw Object.assign(stopError(expired ? HOSTED_SESSION_EXPIRED : {
|
|
193
|
+
code: "HOSTED_CREDENTIALS_REJECTED",
|
|
194
|
+
message: session ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED,
|
|
195
|
+
nextCommand: session ? "gscdump auth login --mode hosted" : null
|
|
196
|
+
}), details);
|
|
197
|
+
}
|
|
198
|
+
throw Object.assign(/* @__PURE__ */ new Error(`Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`), details);
|
|
171
199
|
}
|
|
172
200
|
return response.status === 204 ? void 0 : response.json();
|
|
173
201
|
}
|
|
174
202
|
function hostedCredential(state) {
|
|
175
203
|
return "sessionId" in state ? state.sessionId : state.apiKey;
|
|
176
204
|
}
|
|
205
|
+
function siteManagerOf(state, issuer) {
|
|
206
|
+
if ("sessionId" in state) return { _tag: "gscdump" };
|
|
207
|
+
return issuer ?? { _tag: "key_issuer" };
|
|
208
|
+
}
|
|
209
|
+
function connectSitePlace(manager) {
|
|
210
|
+
switch (manager._tag) {
|
|
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";
|
|
214
|
+
}
|
|
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
|
+
}
|
|
223
|
+
function siteSettingsPlace(manager) {
|
|
224
|
+
switch (manager._tag) {
|
|
225
|
+
case "gscdump": return "in the Site settings on gscdump.com";
|
|
226
|
+
case "partner": return `in the Site settings of ${manager.name}`;
|
|
227
|
+
case "key_issuer": return "in the Site settings of the app that issued this API key";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
177
230
|
async function getHostedAccount(state) {
|
|
178
231
|
const result = accountSchema.safeParse(await hostedRequest(state, "/cli/me"));
|
|
179
232
|
if (!result.success) throw new Error("gscdump.com returned invalid account data.");
|
|
@@ -183,4 +236,4 @@ function formatHostedSync(site) {
|
|
|
183
236
|
const status = site.syncStatus ?? "pending";
|
|
184
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}` : ""}`;
|
|
185
238
|
}
|
|
186
|
-
export { HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication };
|
|
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-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, account.issuer)));
|
|
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.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,10 +1,11 @@
|
|
|
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";
|
|
2
|
+
import { commandLineError } from "./stop.mjs";
|
|
4
3
|
import { parseAuthMode } from "./auth-state.mjs";
|
|
5
4
|
import { checkCliArgs } from "./cli-args.mjs";
|
|
5
|
+
import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
|
|
6
6
|
import { resolveCliEnvironment } from "./environment.mjs";
|
|
7
7
|
import { applyProfileFromCli } from "./commands/profile-selection.mjs";
|
|
8
|
+
import { reportCliError } from "./error-handler.mjs";
|
|
8
9
|
import { resolveOutputOptions, terminalOutputOptions } from "./render/terminal.mjs";
|
|
9
10
|
import { VERSION, setNoColor, showSplash, withConfiguredOutput } from "./utils.mjs";
|
|
10
11
|
import process from "node:process";
|
|
@@ -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,
|
|
@@ -96,21 +130,28 @@ async function runCli(opts = {}) {
|
|
|
96
130
|
runtime.rawArgs = [...input];
|
|
97
131
|
return runWithCliRuntime(runtime, async () => {
|
|
98
132
|
let rawArgs = [...input];
|
|
133
|
+
runtime.jsonOutput = false;
|
|
134
|
+
const stdout = watchStdout();
|
|
99
135
|
try {
|
|
136
|
+
runtime.jsonOutput = asksForJson(input);
|
|
100
137
|
rawArgs = prepareCliArgs(input);
|
|
101
138
|
runtime.rawArgs = [...rawArgs];
|
|
139
|
+
runtime.jsonOutput = asksForJson(rawArgs);
|
|
102
140
|
const argumentError = await checkCliArgs(main, rawArgs);
|
|
103
|
-
if (argumentError) throw
|
|
141
|
+
if (argumentError) throw commandLineError(argumentError);
|
|
104
142
|
await withConfiguredOutput(() => runMainCommand(rawArgs));
|
|
105
143
|
return 0;
|
|
106
144
|
} catch (error) {
|
|
107
145
|
await reportCliError(error, {
|
|
108
146
|
color: stderrColor(runtime, rawArgs),
|
|
147
|
+
json: runtime.jsonOutput && !stdout.wrote(),
|
|
109
148
|
usage: async () => renderUsage(...await resolveUsageTarget(main, rawArgs)),
|
|
110
149
|
authSources: async () => (await import("./auth.mjs")).formatAuthProvenance()
|
|
111
150
|
});
|
|
112
151
|
return 1;
|
|
152
|
+
} finally {
|
|
153
|
+
stdout.restore();
|
|
113
154
|
}
|
|
114
155
|
});
|
|
115
156
|
}
|
|
116
|
-
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",
|
|
@@ -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
|
-
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
|
+
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";
|
|
@@ -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/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/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
|
@@ -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, 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({
|
|
@@ -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 { 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, siteManager } = 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, siteManager);
|
|
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, siteManager } = 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, siteManager);
|
|
183
186
|
}
|
|
184
187
|
});
|
|
185
188
|
const indexingWatchCommand = defineCommand({
|