@gscdump/cli 0.37.2 → 0.37.4

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.
@@ -0,0 +1,243 @@
1
+ import { logger } from "./utils.mjs";
2
+ import { resolveAnalysisSource } from "./analysis-local.mjs";
3
+ import { defineCommand } from "citty";
4
+ import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
5
+ import { err, ok, unwrapResult } from "gscdump/result";
6
+ import { defaultReportRegistry, dryRunReport, formatReport, runReport } from "@gscdump/analysis/report";
7
+ import { resolveWindow } from "@gscdump/engine/period";
8
+ function reportFlagErrorToException(error) {
9
+ const exception = new Error(error.message);
10
+ exception.reportFlagError = error;
11
+ return exception;
12
+ }
13
+ const REPORT_IDS = defaultReportRegistry.listReportIds();
14
+ const PERIOD_ALIASES = {
15
+ "7d": "last-7d",
16
+ "28d": "last-28d",
17
+ "30d": "last-30d",
18
+ "90d": "last-90d",
19
+ "180d": "last-180d",
20
+ "365d": "last-365d",
21
+ "last-7d": "last-7d",
22
+ "last-28d": "last-28d",
23
+ "last-30d": "last-30d",
24
+ "last-90d": "last-90d",
25
+ "last-180d": "last-180d",
26
+ "last-365d": "last-365d",
27
+ "mtd": "mtd",
28
+ "ytd": "ytd",
29
+ "custom": "custom"
30
+ };
31
+ const COMPARISON_ALIASES = {
32
+ "none": "none",
33
+ "prev": "prev-period",
34
+ "prev-period": "prev-period",
35
+ "prior": "prev-period",
36
+ "prior-period": "prev-period",
37
+ "yoy": "yoy"
38
+ };
39
+ function resolvePeriodResult(input, fallback) {
40
+ if (!input) return ok(fallback);
41
+ const preset = PERIOD_ALIASES[input.toLowerCase()];
42
+ if (!preset) return err({
43
+ kind: "unknown-period",
44
+ value: input,
45
+ message: `Unknown --period "${input}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`
46
+ });
47
+ return ok(preset);
48
+ }
49
+ function resolvePeriod(input, fallback) {
50
+ return unwrapResult(resolvePeriodResult(input, fallback), reportFlagErrorToException);
51
+ }
52
+ function resolveComparisonResult(input, fallback) {
53
+ if (!input) return ok(fallback);
54
+ const mode = COMPARISON_ALIASES[input.toLowerCase()];
55
+ if (!mode) return err({
56
+ kind: "unknown-comparison",
57
+ value: input,
58
+ message: `Unknown --vs "${input}". Supported: none, prev-period, yoy.`
59
+ });
60
+ return ok(mode);
61
+ }
62
+ function resolveComparison(input, fallback) {
63
+ return unwrapResult(resolveComparisonResult(input, fallback), reportFlagErrorToException);
64
+ }
65
+ function reportArgsToCitty(spec) {
66
+ const out = {};
67
+ for (const [key, def] of Object.entries(spec)) out[key] = {
68
+ type: def.type === "boolean" ? "boolean" : "string",
69
+ description: def.description,
70
+ default: def.default == null ? void 0 : String(def.default),
71
+ alias: def.alias,
72
+ required: def.required
73
+ };
74
+ return out;
75
+ }
76
+ function buildReportParams(report, args) {
77
+ const params = {};
78
+ for (const [key, def] of Object.entries(report.argsSpec)) {
79
+ const raw = args[key];
80
+ if (raw == null || raw === "") continue;
81
+ if (def.type === "number") {
82
+ const n = Number(raw);
83
+ if (Number.isFinite(n)) params[toCamel(key)] = n;
84
+ } else if (def.type === "boolean") params[toCamel(key)] = !!raw;
85
+ else params[toCamel(key)] = raw;
86
+ }
87
+ return params;
88
+ }
89
+ function toCamel(kebab) {
90
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
91
+ }
92
+ function makeReportCommand(report) {
93
+ const reportArgs = reportArgsToCitty(report.argsSpec);
94
+ return defineCommand({
95
+ meta: {
96
+ name: report.id,
97
+ description: report.description
98
+ },
99
+ args: {
100
+ "site": {
101
+ type: "string",
102
+ alias: "s",
103
+ description: "Site URL"
104
+ },
105
+ "period": {
106
+ type: "string",
107
+ description: "Window: 7d|28d|90d|mtd|ytd|custom",
108
+ default: presetToFlag(report.defaultPeriod)
109
+ },
110
+ "vs": {
111
+ type: "string",
112
+ description: "Comparison: none|prev-period|yoy",
113
+ default: report.defaultComparison
114
+ },
115
+ "start": {
116
+ type: "string",
117
+ description: "Custom start date (YYYY-MM-DD)"
118
+ },
119
+ "end": {
120
+ type: "string",
121
+ description: "Custom end date (YYYY-MM-DD)"
122
+ },
123
+ "prev-start": {
124
+ type: "string",
125
+ description: "Override comparison start"
126
+ },
127
+ "prev-end": {
128
+ type: "string",
129
+ description: "Override comparison end"
130
+ },
131
+ "live": {
132
+ type: "boolean",
133
+ default: false,
134
+ description: "Force live GSC API; bypass local store"
135
+ },
136
+ "json": {
137
+ type: "boolean",
138
+ default: false,
139
+ description: "Emit full ReportResult JSON"
140
+ },
141
+ "explain": {
142
+ type: "boolean",
143
+ default: false,
144
+ description: "Print plan steps + window without executing"
145
+ },
146
+ "dry-run": {
147
+ type: "boolean",
148
+ default: false,
149
+ description: "Alias for --explain"
150
+ },
151
+ ...reportArgs
152
+ },
153
+ async run({ args }) {
154
+ const preset = resolvePeriod(args.period, report.defaultPeriod);
155
+ const comparison = resolveComparison(args.vs, report.defaultComparison);
156
+ const window = resolveWindow({
157
+ preset,
158
+ comparison,
159
+ start: args.start,
160
+ end: args.end
161
+ });
162
+ if (args["prev-start"] && args["prev-end"]) window.comparison = {
163
+ start: String(args["prev-start"]),
164
+ end: String(args["prev-end"])
165
+ };
166
+ const params = buildReportParams(report, args);
167
+ if (args.explain || args["dry-run"]) {
168
+ const dry = await dryRunReport(report, {
169
+ site: args.site ? String(args.site) : "(unresolved)",
170
+ window,
171
+ params,
172
+ registryVersion: defaultReportRegistry.version
173
+ });
174
+ console.log(JSON.stringify({
175
+ id: report.id,
176
+ window,
177
+ comparison,
178
+ plan: dry.steps
179
+ }, null, 2));
180
+ return;
181
+ }
182
+ const { source, siteUrl } = await resolveAnalysisSource({
183
+ site: args.site,
184
+ live: !!args.live,
185
+ json: !!args.json
186
+ });
187
+ const result = await runReport(report, {
188
+ source,
189
+ analyzers: defaultAnalyzerRegistry,
190
+ ctx: {
191
+ site: siteUrl,
192
+ window,
193
+ params,
194
+ registryVersion: defaultReportRegistry.version
195
+ }
196
+ });
197
+ if (args.json) {
198
+ console.log(JSON.stringify(result, null, 2));
199
+ return;
200
+ }
201
+ console.log(formatReport(result));
202
+ if (result.meta.degraded) logger.warn(`degraded: ${result.meta.steps.filter((s) => s.status === "error").map((s) => `${s.key}(${s.error})`).join(", ")}`);
203
+ }
204
+ });
205
+ }
206
+ function presetToFlag(preset) {
207
+ if (preset === "mtd" || preset === "ytd" || preset === "custom") return preset;
208
+ return preset.replace(/^last-/, "");
209
+ }
210
+ const reportCommand = defineCommand({
211
+ meta: {
212
+ name: "report",
213
+ description: "Run an intent-keyed report (composes analyzers into bounded sections)"
214
+ },
215
+ subCommands: {
216
+ list: defineCommand({
217
+ meta: {
218
+ name: "list",
219
+ description: "List available report ids"
220
+ },
221
+ args: { json: {
222
+ type: "boolean",
223
+ default: false,
224
+ description: "Output as JSON"
225
+ } },
226
+ async run({ args }) {
227
+ const reports = defaultReportRegistry.listReports().map((r) => ({
228
+ id: r.id,
229
+ description: r.description,
230
+ defaultPeriod: r.defaultPeriod,
231
+ defaultComparison: r.defaultComparison
232
+ }));
233
+ if (args.json) {
234
+ console.log(JSON.stringify(reports, null, 2));
235
+ return;
236
+ }
237
+ for (const r of reports) console.log(`${r.id.padEnd(16)} ${r.description}`);
238
+ }
239
+ }),
240
+ ...Object.fromEntries(REPORT_IDS.map((id) => [id, makeReportCommand(defaultReportRegistry.getReport(id))]))
241
+ }
242
+ });
243
+ export { reportCommand };
@@ -0,0 +1,274 @@
1
+ import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "./utils.mjs";
2
+ import { createCommandContext } from "./context.mjs";
3
+ import { gscErrorHandler } from "./error-handler.mjs";
4
+ import process from "node:process";
5
+ import { defineCommand } from "citty";
6
+ import { discoverSitemap, fetchSitemap, fetchSitemapUrls } from "gscdump/api";
7
+ const listCommand = defineCommand({
8
+ meta: {
9
+ name: "list",
10
+ description: "List sitemaps for a site"
11
+ },
12
+ args: {
13
+ ...OUTPUT_ARGS,
14
+ site: {
15
+ type: "string",
16
+ alias: "s",
17
+ description: "Site URL (e.g., sc-domain:example.com or https://example.com/)"
18
+ },
19
+ pending: {
20
+ type: "boolean",
21
+ default: false,
22
+ description: "Show only sitemaps with isPending=true"
23
+ },
24
+ errored: {
25
+ type: "boolean",
26
+ default: false,
27
+ description: "Show only sitemaps with errors > 0"
28
+ }
29
+ },
30
+ async run({ args }) {
31
+ const { json } = applyOutputMode(args);
32
+ const ctx = await createCommandContext({ needsAuth: true });
33
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
34
+ let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch(gscErrorHandler)).map((sm) => ({
35
+ path: sm.path,
36
+ type: sm.type || void 0,
37
+ isPending: sm.isPending || false,
38
+ errors: Number(sm.errors) || 0,
39
+ warnings: Number(sm.warnings) || 0,
40
+ lastDownloaded: sm.lastDownloaded || null,
41
+ lastSubmitted: sm.lastSubmitted || null
42
+ }));
43
+ if (args.pending) sitemaps = sitemaps.filter((sm) => sm.isPending);
44
+ if (args.errored) sitemaps = sitemaps.filter((sm) => sm.errors > 0);
45
+ if (json) {
46
+ console.log(JSON.stringify(sitemaps, null, 2));
47
+ return;
48
+ }
49
+ if (sitemaps.length === 0) {
50
+ logger.warn("No sitemaps found");
51
+ return;
52
+ }
53
+ logger.success(`Found ${sitemaps.length} sitemaps:`);
54
+ console.log();
55
+ for (const sm of sitemaps) {
56
+ const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
57
+ const errors = sm.errors ? ` \x1B[31m${sm.errors} errors\x1B[0m` : "";
58
+ const warnings = sm.warnings ? ` \x1B[33m${sm.warnings} warnings\x1B[0m` : "";
59
+ const submitted = sm.lastSubmitted ? ` \x1B[90msubmitted ${sm.lastSubmitted}\x1B[0m` : "";
60
+ console.log(` ${sm.path}${pending}${errors}${warnings}${submitted}`);
61
+ }
62
+ }
63
+ });
64
+ const sitemapsCommand = defineCommand({
65
+ meta: {
66
+ name: "sitemaps",
67
+ description: "Manage sitemaps"
68
+ },
69
+ subCommands: {
70
+ list: listCommand,
71
+ get: defineCommand({
72
+ meta: {
73
+ name: "get",
74
+ description: "Get details for a specific sitemap"
75
+ },
76
+ args: {
77
+ ...OUTPUT_ARGS,
78
+ site: {
79
+ type: "string",
80
+ alias: "s",
81
+ description: "Site URL (defaults to config.defaultSite or prompt)"
82
+ },
83
+ url: {
84
+ type: "positional",
85
+ required: true,
86
+ description: "Sitemap URL"
87
+ }
88
+ },
89
+ async run({ args }) {
90
+ const { json } = applyOutputMode(args);
91
+ const ctx = await createCommandContext({ needsAuth: true });
92
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
93
+ const client = ctx.client;
94
+ const sitemap = await fetchSitemap(client, siteUrl, args.url).catch(gscErrorHandler);
95
+ if (json) {
96
+ console.log(JSON.stringify(sitemap, null, 2));
97
+ return;
98
+ }
99
+ console.log();
100
+ console.log(` \x1B[1mPath:\x1B[0m ${sitemap.path}`);
101
+ console.log(` \x1B[1mType:\x1B[0m ${sitemap.type || "sitemap"}`);
102
+ console.log(` \x1B[1mLast Submitted:\x1B[0m ${sitemap.lastSubmitted || "N/A"}`);
103
+ console.log(` \x1B[1mLast Downloaded:\x1B[0m ${sitemap.lastDownloaded || "N/A"}`);
104
+ console.log(` \x1B[1mPending:\x1B[0m ${sitemap.isPending ? "Yes" : "No"}`);
105
+ console.log(` \x1B[1mErrors:\x1B[0m ${sitemap.errors || 0}`);
106
+ console.log(` \x1B[1mWarnings:\x1B[0m ${sitemap.warnings || 0}`);
107
+ if (sitemap.contents?.length) {
108
+ console.log();
109
+ console.log(" \x1B[1mContents:\x1B[0m");
110
+ for (const c of sitemap.contents) console.log(` ${c.type}: ${c.submitted} submitted, ${c.indexed} indexed`);
111
+ }
112
+ }
113
+ }),
114
+ submit: defineCommand({
115
+ meta: {
116
+ name: "submit",
117
+ description: "Submit a sitemap to GSC"
118
+ },
119
+ args: {
120
+ ...OUTPUT_ARGS,
121
+ site: {
122
+ type: "string",
123
+ alias: "s",
124
+ description: "Site URL (defaults to config.defaultSite or prompt)"
125
+ },
126
+ url: {
127
+ type: "positional",
128
+ required: true,
129
+ description: "Sitemap URL to submit"
130
+ }
131
+ },
132
+ async run({ args }) {
133
+ const { json } = applyOutputMode(args);
134
+ const ctx = await createCommandContext({ needsAuth: true });
135
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
136
+ await ctx.client.sitemaps.submit(siteUrl, args.url).catch(gscErrorHandler);
137
+ if (json) {
138
+ console.log(JSON.stringify({
139
+ siteUrl,
140
+ feedpath: args.url,
141
+ status: "submitted"
142
+ }, null, 2));
143
+ return;
144
+ }
145
+ logger.success(`Submitted sitemap: ${args.url}`);
146
+ }
147
+ }),
148
+ delete: defineCommand({
149
+ meta: {
150
+ name: "delete",
151
+ description: "Delete a sitemap from GSC"
152
+ },
153
+ args: {
154
+ ...OUTPUT_ARGS,
155
+ site: {
156
+ type: "string",
157
+ alias: "s",
158
+ description: "Site URL (defaults to config.defaultSite or prompt)"
159
+ },
160
+ url: {
161
+ type: "positional",
162
+ required: true,
163
+ description: "Sitemap URL to delete"
164
+ }
165
+ },
166
+ async run({ args }) {
167
+ const { json } = applyOutputMode(args);
168
+ const ctx = await createCommandContext({ needsAuth: true });
169
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
170
+ await ctx.client.sitemaps.delete(siteUrl, args.url).catch(gscErrorHandler);
171
+ if (json) {
172
+ console.log(JSON.stringify({
173
+ siteUrl,
174
+ feedpath: args.url,
175
+ status: "deleted"
176
+ }, null, 2));
177
+ return;
178
+ }
179
+ logger.success(`Deleted sitemap: ${args.url}`);
180
+ }
181
+ }),
182
+ discover: defineCommand({
183
+ meta: {
184
+ name: "discover",
185
+ description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth needed)"
186
+ },
187
+ args: {
188
+ ...OUTPUT_ARGS,
189
+ domain: {
190
+ type: "positional",
191
+ required: true,
192
+ description: "Domain (e.g., example.com)"
193
+ }
194
+ },
195
+ async run({ args }) {
196
+ const { json } = applyOutputMode(args);
197
+ const domain = String(args.domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
198
+ const url = await discoverSitemap(domain).catch(() => null);
199
+ if (json) {
200
+ console.log(JSON.stringify({
201
+ domain,
202
+ sitemap: url
203
+ }, null, 2));
204
+ return;
205
+ }
206
+ if (!url) {
207
+ logger.warn(`No sitemap discovered for ${domain}`);
208
+ process.exit(1);
209
+ }
210
+ logger.success(`Discovered sitemap: ${url}`);
211
+ }
212
+ }),
213
+ urls: defineCommand({
214
+ meta: {
215
+ name: "urls",
216
+ description: "Fetch a sitemap (or sitemap index) and dump its <loc> URLs (no auth needed)"
217
+ },
218
+ args: {
219
+ ...OUTPUT_ARGS,
220
+ "url": {
221
+ type: "positional",
222
+ required: true,
223
+ description: "Sitemap URL (index files are followed)"
224
+ },
225
+ "limit": {
226
+ type: "string",
227
+ alias: "l",
228
+ description: "Stop after N URLs across all nested sitemaps"
229
+ },
230
+ "max-depth": {
231
+ type: "string",
232
+ description: "Max sitemap-index nesting depth (default: 3)"
233
+ }
234
+ },
235
+ async run({ args }) {
236
+ const { json } = applyOutputMode(args);
237
+ const limit = args.limit ? Number.parseInt(String(args.limit), 10) : void 0;
238
+ const maxDepth = args["max-depth"] ? Number.parseInt(String(args["max-depth"]), 10) : void 0;
239
+ const urls = await fetchSitemapUrls(String(args.url), {
240
+ limit,
241
+ maxDepth
242
+ }).catch((e) => {
243
+ logger.error(`Sitemap fetch failed: ${e.message}`);
244
+ process.exit(1);
245
+ });
246
+ if (json) {
247
+ console.log(JSON.stringify({
248
+ sitemap: args.url,
249
+ count: urls.length,
250
+ urls
251
+ }, null, 2));
252
+ return;
253
+ }
254
+ for (const u of urls) console.log(u);
255
+ }
256
+ })
257
+ },
258
+ async run({ args }) {
259
+ if (!noSubcommandSelected("sitemaps", [
260
+ "list",
261
+ "get",
262
+ "submit",
263
+ "delete",
264
+ "discover",
265
+ "urls"
266
+ ])) return;
267
+ await listCommand.run?.({
268
+ args,
269
+ cmd: listCommand,
270
+ rawArgs: []
271
+ });
272
+ }
273
+ });
274
+ export { sitemapsCommand };