@gscdump/cli 3.8.0 → 4.0.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/README.md +113 -30
- package/bin/gscdump.mjs +21 -1
- package/dist/analysis-local.mjs +172 -53
- package/dist/auth-state.mjs +21 -6
- package/dist/auth.mjs +27 -18
- package/dist/bing-data.mjs +6 -2
- package/dist/bing-hosted.mjs +1 -1
- package/dist/cli-args.mjs +8 -0
- package/dist/cli.d.mts +5 -1
- package/dist/cli.mjs +41 -10
- package/dist/cloud-google.mjs +8 -1
- package/dist/command-meta.mjs +4 -4
- package/dist/command-registry.mjs +86 -10
- package/dist/commands/analyze.mjs +84 -43
- package/dist/commands/auth.mjs +46 -37
- package/dist/commands/bing.mjs +3 -3
- package/dist/commands/compact.mjs +12 -8
- package/dist/commands/config.mjs +14 -14
- package/dist/commands/doctor.mjs +26 -26
- package/dist/commands/dump.mjs +203 -105
- package/dist/commands/entities.mjs +10 -113
- package/dist/commands/gc.mjs +4 -3
- package/dist/commands/indexing-urls.mjs +185 -0
- package/dist/commands/indexing.mjs +12 -8
- package/dist/commands/init.mjs +77 -18
- package/dist/commands/inspect.mjs +97 -110
- package/dist/commands/mcp.mjs +37 -57
- package/dist/commands/papercut.mjs +1 -1
- package/dist/commands/profile.mjs +9 -17
- package/dist/commands/query.mjs +219 -210
- package/dist/commands/report.mjs +68 -85
- package/dist/commands/rollups.mjs +9 -6
- package/dist/commands/sitemaps.mjs +51 -104
- package/dist/commands/sites.mjs +40 -24
- package/dist/commands/stats.mjs +14 -17
- package/dist/commands/store-purge.mjs +9 -5
- package/dist/commands/store.mjs +0 -12
- package/dist/commands/sync.mjs +774 -296
- package/dist/config.mjs +7 -4
- package/dist/context.mjs +51 -21
- package/dist/coverage.mjs +251 -0
- package/dist/dump-bing.mjs +71 -0
- package/dist/dump-writers.mjs +246 -0
- package/dist/error-handler.mjs +108 -38
- package/dist/filters.mjs +125 -0
- package/dist/hosted-site.mjs +91 -0
- package/dist/inspect-urls.mjs +76 -0
- package/dist/inspection-record.mjs +93 -0
- package/dist/local-entities.mjs +658 -0
- package/dist/mcp/errors.mjs +71 -18
- package/dist/mcp/handlers/diagnostics.mjs +15 -12
- package/dist/mcp/handlers/reports.mjs +33 -42
- package/dist/mcp/server/index.mjs +80 -38
- package/dist/mcp/types.mjs +7 -7
- package/dist/package.mjs +1 -1
- package/dist/quota-ledger.mjs +370 -0
- package/dist/render/analysis.mjs +8 -1
- package/dist/render/report.mjs +8 -1
- package/dist/request-pacer.mjs +29 -0
- package/dist/route.mjs +295 -0
- package/dist/sitemap.mjs +4 -0
- package/dist/sql-views.mjs +117 -0
- package/dist/store-sites.mjs +134 -0
- package/dist/sync-plan.mjs +129 -0
- package/dist/sync-run.mjs +96 -0
- package/dist/table-sources.mjs +57 -0
- package/dist/token-info.mjs +54 -0
- package/dist/utils.mjs +28 -19
- package/dist/window.mjs +159 -0
- package/package.json +17 -14
- package/skills/gscdump/SKILL.md +167 -37
- package/dist/commands/export.mjs +0 -76
- package/dist/native-duckdb.mjs +0 -46
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
2
|
+
import { applyOutputMode, logger, parseIntegerOption, toCSV } from "../utils.mjs";
|
|
3
|
+
import { renderTable } from "../render/layout.mjs";
|
|
4
|
+
import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
const STATUSES = [
|
|
7
|
+
"indexed",
|
|
8
|
+
"not_indexed",
|
|
9
|
+
"pending"
|
|
10
|
+
];
|
|
11
|
+
const FORMATS = [
|
|
12
|
+
"table",
|
|
13
|
+
"json",
|
|
14
|
+
"csv"
|
|
15
|
+
];
|
|
16
|
+
const MAX_PAGE_SIZE = 500;
|
|
17
|
+
const CSV_COLUMNS = [
|
|
18
|
+
"url",
|
|
19
|
+
"verdict",
|
|
20
|
+
"coverageState",
|
|
21
|
+
"issueType",
|
|
22
|
+
"lastCrawlTime",
|
|
23
|
+
"lastCheckedAt",
|
|
24
|
+
"googleCanonical",
|
|
25
|
+
"userCanonical",
|
|
26
|
+
"sitemaps"
|
|
27
|
+
];
|
|
28
|
+
function parseChoice(value, flag, allowed) {
|
|
29
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
30
|
+
if (!allowed.includes(String(value))) throw new Error(`Invalid ${flag}: ${String(value)}. Use ${allowed.join(", ")}.`);
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
async function fetchAllPages(client, site, query) {
|
|
34
|
+
const urls = [];
|
|
35
|
+
let first;
|
|
36
|
+
while (true) {
|
|
37
|
+
const { data } = await client.listSiteIndexingUrls({
|
|
38
|
+
params: { siteId: site.siteId },
|
|
39
|
+
query: {
|
|
40
|
+
...query,
|
|
41
|
+
limit: MAX_PAGE_SIZE,
|
|
42
|
+
offset: urls.length
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
first ??= data;
|
|
46
|
+
if (data.pagination.offset !== urls.length || data.pagination.hasMore && data.urls.length === 0) throw new Error("The hosted API returned inconsistent indexing pagination. Run the command again.");
|
|
47
|
+
urls.push(...data.urls);
|
|
48
|
+
if (!data.pagination.hasMore) return {
|
|
49
|
+
...first,
|
|
50
|
+
urls,
|
|
51
|
+
pagination: {
|
|
52
|
+
...data.pagination,
|
|
53
|
+
total: urls.length,
|
|
54
|
+
limit: urls.length,
|
|
55
|
+
offset: 0,
|
|
56
|
+
hasMore: false
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function csvRow(row) {
|
|
62
|
+
return {
|
|
63
|
+
...row,
|
|
64
|
+
sitemaps: (row.sitemaps ?? []).join(" ")
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const indexingUrlsCommand = defineCommand({
|
|
68
|
+
meta: {
|
|
69
|
+
name: "urls",
|
|
70
|
+
description: "List Google URL Inspection results for a Site's URLs, filtered by status (hosted)"
|
|
71
|
+
},
|
|
72
|
+
args: {
|
|
73
|
+
...HOSTED_ARGS,
|
|
74
|
+
status: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: `Filter by status: ${STATUSES.join(", ")}`
|
|
77
|
+
},
|
|
78
|
+
search: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Only URLs that contain this text"
|
|
81
|
+
},
|
|
82
|
+
limit: {
|
|
83
|
+
type: "string",
|
|
84
|
+
alias: "l",
|
|
85
|
+
default: "100",
|
|
86
|
+
description: `Rows per page, maximum ${MAX_PAGE_SIZE}`
|
|
87
|
+
},
|
|
88
|
+
offset: {
|
|
89
|
+
type: "string",
|
|
90
|
+
default: "0",
|
|
91
|
+
description: "Rows to skip before the page"
|
|
92
|
+
},
|
|
93
|
+
all: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
default: false,
|
|
96
|
+
description: "Read every page; ignores --limit and --offset"
|
|
97
|
+
},
|
|
98
|
+
format: {
|
|
99
|
+
type: "string",
|
|
100
|
+
alias: "f",
|
|
101
|
+
default: "table",
|
|
102
|
+
description: `Output format: ${FORMATS.join(", ")}`
|
|
103
|
+
},
|
|
104
|
+
json: {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
default: false,
|
|
107
|
+
description: "Output as JSON (same as --format json)"
|
|
108
|
+
},
|
|
109
|
+
quiet: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
alias: "q",
|
|
112
|
+
default: false,
|
|
113
|
+
description: "Suppress info output"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
async run({ args }) {
|
|
117
|
+
const format = args.json ? "json" : parseChoice(args.format, "--format", FORMATS) ?? "table";
|
|
118
|
+
const status = parseChoice(args.status, "--status", STATUSES);
|
|
119
|
+
const limit = parseIntegerOption(args.limit, "--limit") ?? 100;
|
|
120
|
+
const offset = parseIntegerOption(args.offset, "--offset", 0) ?? 0;
|
|
121
|
+
if (limit > MAX_PAGE_SIZE) throw new Error(`--limit must be ${MAX_PAGE_SIZE} or less. Use --all to read every page.`);
|
|
122
|
+
const search = args.search ? String(args.search) : void 0;
|
|
123
|
+
applyOutputMode({
|
|
124
|
+
json: format !== "table",
|
|
125
|
+
quiet: args.quiet
|
|
126
|
+
});
|
|
127
|
+
const filters = {
|
|
128
|
+
...status ? { status } : {},
|
|
129
|
+
...search ? { search } : {}
|
|
130
|
+
};
|
|
131
|
+
const { client, site } = await resolveHostedSite(args, {
|
|
132
|
+
name: "indexing urls",
|
|
133
|
+
localAlternative: "pipe `gscdump sitemaps urls <sitemap-url>` into `gscdump inspect --site <site>`"
|
|
134
|
+
});
|
|
135
|
+
const page = args.all ? await fetchAllPages(client, site, filters) : (await client.listSiteIndexingUrls({
|
|
136
|
+
params: { siteId: site.siteId },
|
|
137
|
+
query: {
|
|
138
|
+
...filters,
|
|
139
|
+
limit,
|
|
140
|
+
offset
|
|
141
|
+
}
|
|
142
|
+
})).data;
|
|
143
|
+
if (format === "json") {
|
|
144
|
+
console.log(JSON.stringify(page, null, 2));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (format === "csv") {
|
|
148
|
+
console.log(toCSV(page.urls.map(csvRow), [...CSV_COLUMNS]));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const options = terminalOutputOptions();
|
|
152
|
+
if (page.urls.length === 0) {
|
|
153
|
+
logger.info(`No URLs${status ? ` with status ${status}` : ""} for ${site.siteUrl}.`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
for (const line of renderTable(page.urls.map((row) => ({
|
|
157
|
+
url: row.url,
|
|
158
|
+
coverage: row.coverageState ?? row.verdict ?? "unknown",
|
|
159
|
+
lastCrawl: row.lastCrawlTime?.slice(0, 10) ?? "never",
|
|
160
|
+
sitemaps: row.sitemaps?.length ?? 0
|
|
161
|
+
})), [
|
|
162
|
+
{
|
|
163
|
+
key: "url",
|
|
164
|
+
label: "URL"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: "coverage",
|
|
168
|
+
label: "Coverage"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
key: "lastCrawl",
|
|
172
|
+
label: "Last crawl"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "sitemaps",
|
|
176
|
+
label: "Sitemaps",
|
|
177
|
+
numeric: true
|
|
178
|
+
}
|
|
179
|
+
], options)) console.log(line);
|
|
180
|
+
const { total, offset: start, hasMore } = page.pagination;
|
|
181
|
+
logger.info(`Showing ${start + 1}-${start + page.urls.length} of ${total} URLs for ${site.siteUrl}.`);
|
|
182
|
+
if (hasMore) logger.info(`For the next page, pass --offset ${start + page.urls.length}. To read every page, pass --all.`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
export { indexingUrlsCommand };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { indexingCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, readUrlList } from "../utils.mjs";
|
|
3
3
|
import { createCommandContext } from "../context.mjs";
|
|
4
|
-
import { gscErrorHandler } from "../error-handler.mjs";
|
|
5
4
|
import { loadSitemapUrls } from "../sitemap.mjs";
|
|
5
|
+
import { indexingUrlsCommand } from "./indexing-urls.mjs";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import { batchRequestIndexing, getIndexingMetadata, requestIndexing, runSequentialBatch } from "gscdump/indexing";
|
|
@@ -21,7 +21,10 @@ async function resolveUrlSource(args) {
|
|
|
21
21
|
if (!result.value.complete) logger.warn("Sitemap walk was incomplete; indexing only the URLs that were read");
|
|
22
22
|
return result.value.urls;
|
|
23
23
|
}
|
|
24
|
-
return readUrlList(
|
|
24
|
+
return readUrlList({
|
|
25
|
+
file: args.file,
|
|
26
|
+
positionals: args._
|
|
27
|
+
});
|
|
25
28
|
}
|
|
26
29
|
const submitCommand = defineCommand({
|
|
27
30
|
meta: {
|
|
@@ -43,7 +46,7 @@ const submitCommand = defineCommand({
|
|
|
43
46
|
needsAuth: true,
|
|
44
47
|
fetchOptions: { retry: parseIntegerOption(args.retries, "--retries", 0) }
|
|
45
48
|
});
|
|
46
|
-
const result = await requestIndexing(ctx.client, args.url, { type: "URL_UPDATED" })
|
|
49
|
+
const result = await requestIndexing(ctx.client, args.url, { type: "URL_UPDATED" });
|
|
47
50
|
if (args.json) {
|
|
48
51
|
console.log(JSON.stringify(result, null, 2));
|
|
49
52
|
return;
|
|
@@ -72,7 +75,7 @@ const removeCommand = defineCommand({
|
|
|
72
75
|
needsAuth: true,
|
|
73
76
|
fetchOptions: { retry: parseIntegerOption(args.retries, "--retries", 0) }
|
|
74
77
|
});
|
|
75
|
-
const result = await requestIndexing(ctx.client, args.url, { type: "URL_DELETED" })
|
|
78
|
+
const result = await requestIndexing(ctx.client, args.url, { type: "URL_DELETED" });
|
|
76
79
|
if (args.json) {
|
|
77
80
|
console.log(JSON.stringify(result, null, 2));
|
|
78
81
|
return;
|
|
@@ -107,7 +110,7 @@ const statusCommand = defineCommand({
|
|
|
107
110
|
async run({ args }) {
|
|
108
111
|
applyOutputMode(args);
|
|
109
112
|
const ctx = await createCommandContext({ needsAuth: true });
|
|
110
|
-
const meta = await getIndexingMetadata(ctx.client, args.url)
|
|
113
|
+
const meta = await getIndexingMetadata(ctx.client, args.url);
|
|
111
114
|
if (args.json) {
|
|
112
115
|
console.log(JSON.stringify(meta, null, 2));
|
|
113
116
|
return;
|
|
@@ -234,7 +237,7 @@ const batchCommand = defineCommand({
|
|
|
234
237
|
delayMs,
|
|
235
238
|
concurrency,
|
|
236
239
|
onProgress: args.json || args.quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url}`)
|
|
237
|
-
})
|
|
240
|
+
});
|
|
238
241
|
if (args.json) {
|
|
239
242
|
console.log(JSON.stringify(results, null, 2));
|
|
240
243
|
return;
|
|
@@ -298,7 +301,7 @@ const batchStatusCommand = defineCommand({
|
|
|
298
301
|
delayMs,
|
|
299
302
|
concurrency,
|
|
300
303
|
onProgress: args.json || args.quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url}`)
|
|
301
|
-
})
|
|
304
|
+
});
|
|
302
305
|
if (args.json) {
|
|
303
306
|
console.log(JSON.stringify(results, null, 2));
|
|
304
307
|
return;
|
|
@@ -314,7 +317,8 @@ const indexingCommand = defineCommand({
|
|
|
314
317
|
"status": statusCommand,
|
|
315
318
|
"batch": batchCommand,
|
|
316
319
|
"batch-status": batchStatusCommand,
|
|
317
|
-
"quota": quotaCommand
|
|
320
|
+
"quota": quotaCommand,
|
|
321
|
+
"urls": indexingUrlsCommand
|
|
318
322
|
}
|
|
319
323
|
});
|
|
320
324
|
export { indexingCommand };
|
package/dist/commands/init.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { initCommandMeta } from "../command-meta.mjs";
|
|
2
|
+
import { useCliRuntime } from "../runtime.mjs";
|
|
3
|
+
import { saveAuthentication } from "../auth-state.mjs";
|
|
2
4
|
import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
|
|
3
5
|
import { applyCliEnvironment } from "../environment.mjs";
|
|
4
6
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
5
7
|
import { authenticate, getAuth, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
8
|
+
import { loginCloud } from "./auth.mjs";
|
|
6
9
|
import process from "node:process";
|
|
7
10
|
import { defineCommand } from "citty";
|
|
8
11
|
import fs from "node:fs/promises";
|
|
@@ -10,8 +13,12 @@ import path from "node:path";
|
|
|
10
13
|
import { confirm, isCancel, text } from "@clack/prompts";
|
|
11
14
|
import { googleSearchConsole } from "gscdump/client";
|
|
12
15
|
const ENV_LINE_RE = /^([^=]+)=(.*)$/;
|
|
16
|
+
function canPrompt() {
|
|
17
|
+
return process.stdin.isTTY === true;
|
|
18
|
+
}
|
|
13
19
|
async function promptDataDir(existing) {
|
|
14
20
|
const fallback = existing ?? defaultDataDir();
|
|
21
|
+
if (!canPrompt()) return fallback;
|
|
15
22
|
const answer = await text({
|
|
16
23
|
message: "Where should Parquet data be stored?",
|
|
17
24
|
placeholder: fallback,
|
|
@@ -46,16 +53,35 @@ const initCommand = defineCommand({
|
|
|
46
53
|
alias: "f",
|
|
47
54
|
description: "Force re-initialization"
|
|
48
55
|
},
|
|
49
|
-
"
|
|
56
|
+
"store": {
|
|
50
57
|
type: "boolean",
|
|
51
|
-
default:
|
|
52
|
-
description: "
|
|
58
|
+
default: true,
|
|
59
|
+
description: "Ask where to keep the local Store",
|
|
60
|
+
negativeDescription: "Skip the Store location prompt (authentication only)"
|
|
61
|
+
},
|
|
62
|
+
"mode": {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Authentication mode to save: cloud or local"
|
|
65
|
+
},
|
|
66
|
+
"api-key": {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "gscdump user API key for --mode cloud; defaults to GSCDUMP_API_KEY"
|
|
69
|
+
},
|
|
70
|
+
"api-root": {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Cloud API root for --mode cloud; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
|
|
53
73
|
},
|
|
54
74
|
...OUTPUT_ARGS
|
|
55
75
|
},
|
|
56
76
|
async run({ args }) {
|
|
57
77
|
applyOutputMode(args);
|
|
58
78
|
const config = await loadConfig();
|
|
79
|
+
const mode = useCliRuntime().authModeOverride;
|
|
80
|
+
if (mode === "cloud") {
|
|
81
|
+
await loginCloud(args);
|
|
82
|
+
printNextSteps();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
59
85
|
if (config.clientId && config.clientSecret && !args.force) {
|
|
60
86
|
logger.info("Already configured");
|
|
61
87
|
const tokens = await loadTokens();
|
|
@@ -64,20 +90,23 @@ const initCommand = defineCommand({
|
|
|
64
90
|
const isExpired = tokens.expiry_date && tokens.expiry_date < Date.now();
|
|
65
91
|
if (isExpired && tokens.refresh_token) logger.info("Tokens expired but refresh available. Any live command will auto-refresh, or run `gscdump auth refresh`.");
|
|
66
92
|
else if (isExpired) logger.warn("Tokens expired without a refresh token. Run `gscdump auth login` to re-authenticate.");
|
|
67
|
-
else logger.info("Next: `gscdump sites` to list properties, or `gscdump query --help`.");
|
|
68
93
|
}
|
|
94
|
+
if (mode === "local") await saveAuthentication({ _tag: "Local" });
|
|
69
95
|
logger.info("Run `gscdump init --force` to reconfigure.");
|
|
96
|
+
printNextSteps();
|
|
70
97
|
return;
|
|
71
98
|
}
|
|
72
99
|
const byok = resolveBYOK();
|
|
73
100
|
if (byok) {
|
|
74
|
-
const dataDir = args
|
|
101
|
+
const dataDir = args.store ? config.dataDir ?? defaultDataDir() : config.dataDir;
|
|
75
102
|
await saveConfig({
|
|
76
103
|
...config,
|
|
77
|
-
dataDir
|
|
104
|
+
...dataDir ? { dataDir } : {}
|
|
78
105
|
});
|
|
79
|
-
|
|
80
|
-
logger.success(
|
|
106
|
+
await saveAuthentication({ _tag: "Local" });
|
|
107
|
+
logger.success(`BYOK detected (${typeof byok === "string" ? "access-token" : "refresh-token"}). Auth setup skipped.`);
|
|
108
|
+
logger.success("Setup complete.");
|
|
109
|
+
printNextSteps();
|
|
81
110
|
return;
|
|
82
111
|
}
|
|
83
112
|
const envFile = await loadEnvFile();
|
|
@@ -108,27 +137,56 @@ const initCommand = defineCommand({
|
|
|
108
137
|
refresh_token: creds.refresh_token || envRef,
|
|
109
138
|
expiry_date: creds.expiry_date
|
|
110
139
|
});
|
|
140
|
+
await saveAuthentication({ _tag: "Local" });
|
|
111
141
|
console.log();
|
|
112
|
-
logger.success("Setup complete using .env credentials
|
|
142
|
+
logger.success("Setup complete using .env credentials.");
|
|
143
|
+
printNextSteps();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!canPrompt()) {
|
|
147
|
+
if (!await loadTokens()) {
|
|
148
|
+
logger.error([
|
|
149
|
+
"No Google credentials found. Init cannot prompt without a terminal.",
|
|
150
|
+
"Run `gscdump auth login` in a terminal, or set GSC_CLIENT_ID, GSC_CLIENT_SECRET and GSC_REFRESH_TOKEN.",
|
|
151
|
+
"To use gscdump.com, run `gscdump init --mode cloud --api-key <key>`."
|
|
152
|
+
].join("\n"));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
await saveConfig({
|
|
156
|
+
...config,
|
|
157
|
+
dataDir: config.dataDir ?? defaultDataDir()
|
|
158
|
+
});
|
|
159
|
+
await saveAuthentication({ _tag: "Local" });
|
|
160
|
+
logger.success("Setup complete with the saved Google login.");
|
|
161
|
+
printNextSteps();
|
|
113
162
|
return;
|
|
114
163
|
}
|
|
115
164
|
console.log();
|
|
116
165
|
console.log(" \x1B[1mWelcome to GSCDump!\x1B[0m");
|
|
117
166
|
console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
|
|
118
167
|
console.log();
|
|
119
|
-
const dataDir = args
|
|
168
|
+
const dataDir = args.store ? await promptDataDir(config.dataDir) : void 0;
|
|
120
169
|
await saveConfig({
|
|
121
170
|
...config,
|
|
122
171
|
...dataDir ? { dataDir } : {}
|
|
123
172
|
});
|
|
124
|
-
await
|
|
173
|
+
const sites = await runSmokeTest(await getAuth({ interactive: true }));
|
|
174
|
+
await saveAuthentication({ _tag: "Local" });
|
|
125
175
|
if (config.clientId && config.clientSecret) await maybeWriteEnvFile(config.clientId, config.clientSecret);
|
|
126
176
|
console.log();
|
|
127
|
-
logger.success("Setup complete
|
|
177
|
+
logger.success("Setup complete.");
|
|
178
|
+
printNextSteps(sites);
|
|
128
179
|
}
|
|
129
180
|
});
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
function printNextSteps(sites = []) {
|
|
182
|
+
const site = sites.length === 1 ? sites[0] : "<site>";
|
|
183
|
+
console.log();
|
|
184
|
+
console.log(" Next:");
|
|
185
|
+
const line = (command, note) => console.log(` ${command.padEnd(44)} # ${note}`);
|
|
186
|
+
if (sites.length !== 1) line("gscdump sites", "list your Sites");
|
|
187
|
+
line(`gscdump sync --site ${site}`, "fetch the last 28 days, then catch up on each run");
|
|
188
|
+
line(`gscdump sync --status --site ${site}`, "see what the Store holds");
|
|
189
|
+
console.log();
|
|
132
190
|
}
|
|
133
191
|
async function runSmokeTest(oauth) {
|
|
134
192
|
const sites = await googleSearchConsole(oauth).sites().catch((e) => e);
|
|
@@ -141,18 +199,19 @@ async function runSmokeTest(oauth) {
|
|
|
141
199
|
const url = project ? `https://console.developers.google.com/apis/api/searchconsole.googleapis.com/overview?project=${project}` : "https://console.developers.google.com/apis/api/searchconsole.googleapis.com/overview";
|
|
142
200
|
logger.info(`Enable it here, then retry: ${url}`);
|
|
143
201
|
logger.info("Note: it can take a few minutes to propagate after enabling.");
|
|
144
|
-
return;
|
|
202
|
+
return [];
|
|
145
203
|
}
|
|
146
204
|
if (/insufficient|scope|forbidden|403/i.test(msg)) {
|
|
147
205
|
logger.warn(`Smoke test failed (likely missing scopes): ${msg}`);
|
|
148
206
|
logger.info("Run `gscdump auth login --force` to re-consent with the required scopes.");
|
|
149
|
-
return;
|
|
207
|
+
return [];
|
|
150
208
|
}
|
|
151
209
|
logger.warn(`Smoke test failed: ${msg}`);
|
|
152
210
|
logger.info("Auth saved, but verify via `gscdump auth status` / `gscdump doctor`.");
|
|
153
|
-
return;
|
|
211
|
+
return [];
|
|
154
212
|
}
|
|
155
213
|
logger.success(`Verified: ${sites.length} GSC site(s) accessible`);
|
|
214
|
+
return sites.flatMap((site) => site.siteUrl ? [site.siteUrl] : []);
|
|
156
215
|
}
|
|
157
216
|
async function maybeWriteEnvFile(clientId, clientSecret) {
|
|
158
217
|
const tokens = await loadTokens();
|
|
@@ -182,4 +241,4 @@ async function maybeWriteEnvFile(clientId, clientSecret) {
|
|
|
182
241
|
await fs.writeFile(envPath, content, { mode: 384 });
|
|
183
242
|
logger.success(`Wrote ${displayPath(envPath)}`);
|
|
184
243
|
}
|
|
185
|
-
export { initCommand, runSmokeTest };
|
|
244
|
+
export { initCommand, printNextSteps, runSmokeTest };
|