@gscdump/cli 0.37.3 → 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.
- package/bin/gscdump.mjs +3 -0
- package/dist/_chunks/analysis-local.mjs +88 -0
- package/dist/_chunks/analyze.mjs +332 -0
- package/dist/_chunks/auth.mjs +476 -0
- package/dist/_chunks/auth2.mjs +293 -0
- package/dist/_chunks/config.mjs +93 -0
- package/dist/_chunks/config2.mjs +232 -0
- package/dist/_chunks/context.mjs +69 -0
- package/dist/_chunks/doctor.mjs +400 -0
- package/dist/_chunks/dump.mjs +193 -0
- package/dist/_chunks/entities.mjs +414 -0
- package/dist/_chunks/env-file.mjs +53 -0
- package/dist/_chunks/environment.mjs +30 -0
- package/dist/_chunks/error-handler.mjs +86 -0
- package/dist/_chunks/indexing.mjs +314 -0
- package/dist/_chunks/init.mjs +190 -0
- package/dist/_chunks/inspect.mjs +203 -0
- package/dist/_chunks/mcp.mjs +867 -0
- package/dist/_chunks/native-duckdb.mjs +46 -0
- package/dist/_chunks/profile.mjs +290 -0
- package/dist/_chunks/query.mjs +537 -0
- package/dist/_chunks/report.mjs +243 -0
- package/dist/_chunks/sitemaps.mjs +274 -0
- package/dist/_chunks/sites.mjs +500 -0
- package/dist/_chunks/store.mjs +652 -0
- package/dist/_chunks/sync.mjs +489 -0
- package/dist/_chunks/utils.mjs +191 -0
- package/dist/cli.d.mts +28 -0
- package/dist/cli.mjs +104 -0
- package/package.json +15 -7
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -7330
package/bin/gscdump.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { logger } from "./utils.mjs";
|
|
2
|
+
import { createCommandContext, createLocalStore } from "./context.mjs";
|
|
3
|
+
import { LocalStoreUnsupportedError } from "./error-handler.mjs";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { readdir } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
|
|
8
|
+
import { AnalyzerCapabilityError, createEngineQuerySource, runAnalyzerFromSource } from "@gscdump/analysis";
|
|
9
|
+
import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
|
|
10
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
11
|
+
import { decodeSiteId, normalizeSiteUrl } from "gscdump/tenant";
|
|
12
|
+
async function hasLocalData(store, siteUrl) {
|
|
13
|
+
return (await store.engine.listLive({
|
|
14
|
+
userId: store.userId,
|
|
15
|
+
siteId: store.siteIdFor(siteUrl)
|
|
16
|
+
})).length > 0;
|
|
17
|
+
}
|
|
18
|
+
async function listLocalSites(dataDir, userId = "local") {
|
|
19
|
+
return readdir(join(dataDir, `u_${userId}`), { withFileTypes: true }).then((entries) => entries.filter((e) => e.isDirectory() && (e.name.startsWith("d_") || e.name.startsWith("h_"))).map((e) => decodeSiteId(e.name))).catch(() => []);
|
|
20
|
+
}
|
|
21
|
+
function pickLocalSite(siteUrls, hint) {
|
|
22
|
+
if (siteUrls.length === 0) return null;
|
|
23
|
+
if (!hint) return siteUrls.length === 1 ? siteUrls[0] : null;
|
|
24
|
+
const normalized = normalizeSiteUrl(hint);
|
|
25
|
+
const exact = siteUrls.find((s) => s === normalized || s === hint);
|
|
26
|
+
if (exact) return exact;
|
|
27
|
+
return siteUrls.find((s) => s.includes(hint) || hint.includes(s)) ?? null;
|
|
28
|
+
}
|
|
29
|
+
async function runAnalysisResult(source, params, mode) {
|
|
30
|
+
return runAnalyzerFromSource(source, params, defaultAnalyzerRegistry).then(ok).catch((e) => {
|
|
31
|
+
if (e instanceof AnalyzerCapabilityError) return err(new LocalStoreUnsupportedError(params.type, mode));
|
|
32
|
+
throw e;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function makeRunAnalysis(source, mode) {
|
|
36
|
+
return async (params) => unwrapResult(await runAnalysisResult(source, params, mode), (e) => e);
|
|
37
|
+
}
|
|
38
|
+
async function resolveAnalysisSource(args) {
|
|
39
|
+
const isLive = !!args.live;
|
|
40
|
+
const format = args.json ? "json" : args.format ? String(args.format) : "table";
|
|
41
|
+
if (!isLive) {
|
|
42
|
+
const { config, dataDir } = await createCommandContext();
|
|
43
|
+
const store = createLocalStore({ dataDir });
|
|
44
|
+
const siteHint = args.site ? String(args.site) : config.defaultSite;
|
|
45
|
+
const localSites = await listLocalSites(dataDir, store.userId);
|
|
46
|
+
const siteUrl = pickLocalSite(localSites, siteHint);
|
|
47
|
+
if (!siteUrl) {
|
|
48
|
+
if (localSites.length === 0) logger.error(`No local data found in ${dataDir}. Run \`gscdump sync\` first, or pass --live.`);
|
|
49
|
+
else logger.error(`Could not resolve site${siteHint ? ` from "${siteHint}"` : ""}. Local sites: ${localSites.join(", ")}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (!await hasLocalData(store, siteUrl).catch(() => false)) {
|
|
53
|
+
logger.error(`No local data for ${siteUrl}. Run \`gscdump sync\` first, or pass --live.`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const source = createEngineQuerySource({
|
|
57
|
+
engine: store.engine,
|
|
58
|
+
ctx: {
|
|
59
|
+
userId: store.userId,
|
|
60
|
+
siteId: store.siteIdFor(siteUrl)
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
source,
|
|
65
|
+
siteUrl,
|
|
66
|
+
format,
|
|
67
|
+
isLive,
|
|
68
|
+
runAnalysis: makeRunAnalysis(source, "local")
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const ctx = await createCommandContext({
|
|
72
|
+
needsAuth: true,
|
|
73
|
+
needsStore: false
|
|
74
|
+
});
|
|
75
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
76
|
+
const source = createGscApiQuerySource({
|
|
77
|
+
client: ctx.client,
|
|
78
|
+
siteUrl
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
source,
|
|
82
|
+
siteUrl,
|
|
83
|
+
format,
|
|
84
|
+
isLive,
|
|
85
|
+
runAnalysis: makeRunAnalysis(source, "live")
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export { resolveAnalysisSource };
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { logger, toCSV } from "./utils.mjs";
|
|
2
|
+
import { gscErrorHandler } from "./error-handler.mjs";
|
|
3
|
+
import { resolveAnalysisSource } from "./analysis-local.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
|
|
6
|
+
const ANALYSIS_TOOLS = defaultAnalyzerRegistry.listAnalyzerIds();
|
|
7
|
+
const TOOL_EXTRA_ARGS = {
|
|
8
|
+
brand: { "brand-terms": {
|
|
9
|
+
type: "string",
|
|
10
|
+
description: "Comma-separated brand terms (required)"
|
|
11
|
+
} },
|
|
12
|
+
movers: {
|
|
13
|
+
"prev-start": {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Previous period start date (required)"
|
|
16
|
+
},
|
|
17
|
+
"prev-end": {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Previous period end date (required)"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
decay: {
|
|
23
|
+
"prev-start": {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Previous period start date (required)"
|
|
26
|
+
},
|
|
27
|
+
"prev-end": {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Previous period end date (required)"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
concentration: { dimension: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Dimension: pages or keywords (default: pages)"
|
|
35
|
+
} },
|
|
36
|
+
seasonality: { metric: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Metric: clicks or impressions (default: clicks)"
|
|
39
|
+
} },
|
|
40
|
+
clustering: { "cluster-by": {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Cluster by: prefix, intent, or both (default: both)"
|
|
43
|
+
} },
|
|
44
|
+
trends: {
|
|
45
|
+
"dimension": {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Dimension: pages or keywords (default: pages)"
|
|
48
|
+
},
|
|
49
|
+
"weeks": {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Rolling window size in weeks (default: 28)"
|
|
52
|
+
},
|
|
53
|
+
"min-weeks": {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Minimum weeks with data to include an entity (default: weeks/4)"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function buildParams(tool, args) {
|
|
60
|
+
const params = {
|
|
61
|
+
type: tool,
|
|
62
|
+
startDate: args.start ? String(args.start) : void 0,
|
|
63
|
+
endDate: args.end ? String(args.end) : void 0,
|
|
64
|
+
limit: args.limit ? Number(args.limit) : void 0
|
|
65
|
+
};
|
|
66
|
+
if (args["brand-terms"]) params.brandTerms = String(args["brand-terms"]).split(",").map((t) => t.trim()).filter(Boolean);
|
|
67
|
+
if (args["prev-start"]) params.prevStartDate = String(args["prev-start"]);
|
|
68
|
+
if (args["prev-end"]) params.prevEndDate = String(args["prev-end"]);
|
|
69
|
+
if (args.dimension) params.dimension = String(args.dimension);
|
|
70
|
+
if (args.metric) params.metric = String(args.metric);
|
|
71
|
+
if (args["cluster-by"]) params.clusterBy = String(args["cluster-by"]);
|
|
72
|
+
if (args.weeks) params.weeks = Number(args.weeks);
|
|
73
|
+
if (args["min-weeks"]) params.minWeeksWithData = Number(args["min-weeks"]);
|
|
74
|
+
return params;
|
|
75
|
+
}
|
|
76
|
+
function makeToolCommand(tool) {
|
|
77
|
+
const extraArgs = TOOL_EXTRA_ARGS[tool] || {};
|
|
78
|
+
return defineCommand({
|
|
79
|
+
meta: {
|
|
80
|
+
name: tool,
|
|
81
|
+
description: `Run ${tool} analysis`
|
|
82
|
+
},
|
|
83
|
+
args: {
|
|
84
|
+
site: {
|
|
85
|
+
type: "string",
|
|
86
|
+
alias: "s",
|
|
87
|
+
description: "Site URL"
|
|
88
|
+
},
|
|
89
|
+
start: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Start date (YYYY-MM-DD)"
|
|
92
|
+
},
|
|
93
|
+
end: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "End date (YYYY-MM-DD)"
|
|
96
|
+
},
|
|
97
|
+
limit: {
|
|
98
|
+
type: "string",
|
|
99
|
+
alias: "l",
|
|
100
|
+
default: "100",
|
|
101
|
+
description: "Max results"
|
|
102
|
+
},
|
|
103
|
+
format: {
|
|
104
|
+
type: "string",
|
|
105
|
+
alias: "f",
|
|
106
|
+
default: "table",
|
|
107
|
+
description: "Output: table, json, csv"
|
|
108
|
+
},
|
|
109
|
+
json: {
|
|
110
|
+
type: "boolean",
|
|
111
|
+
default: false,
|
|
112
|
+
description: "Output as JSON"
|
|
113
|
+
},
|
|
114
|
+
live: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
default: false,
|
|
117
|
+
description: "Force live GSC API; bypass local Parquet store"
|
|
118
|
+
},
|
|
119
|
+
...extraArgs
|
|
120
|
+
},
|
|
121
|
+
async run({ args }) {
|
|
122
|
+
const { format, runAnalysis } = await resolveAnalysisSource({
|
|
123
|
+
site: args.site,
|
|
124
|
+
live: !!args.live,
|
|
125
|
+
json: !!args.json,
|
|
126
|
+
format: args.format
|
|
127
|
+
});
|
|
128
|
+
logger.info(`Running ${tool} analysis...`);
|
|
129
|
+
const result = await runAnalysis(buildParams(tool, args)).catch(gscErrorHandler);
|
|
130
|
+
if (format === "json") {
|
|
131
|
+
console.log(JSON.stringify(result, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
renderResults(result.results, result.results.length, format);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const SPARK_CHARS = [
|
|
139
|
+
"▁",
|
|
140
|
+
"▂",
|
|
141
|
+
"▃",
|
|
142
|
+
"▄",
|
|
143
|
+
"▅",
|
|
144
|
+
"▆",
|
|
145
|
+
"▇",
|
|
146
|
+
"█"
|
|
147
|
+
];
|
|
148
|
+
const SPARK_GAP = "·";
|
|
149
|
+
const PERCENT_COLS = {
|
|
150
|
+
growthRatio: "ratio_to_pct",
|
|
151
|
+
brandShare: "direct",
|
|
152
|
+
topNConcentration: "direct",
|
|
153
|
+
declinePercent: "direct",
|
|
154
|
+
ctr: "direct",
|
|
155
|
+
share: "direct",
|
|
156
|
+
vsAverage: "ratio_to_pct",
|
|
157
|
+
clicksChangePercent: "scaled",
|
|
158
|
+
impressionsChangePercent: "scaled"
|
|
159
|
+
};
|
|
160
|
+
function formatPct(val, style) {
|
|
161
|
+
const pct = style === "ratio_to_pct" ? (val - 1) * 100 : style === "direct" ? val * 100 : val;
|
|
162
|
+
if (!Number.isFinite(pct)) return "";
|
|
163
|
+
return `${pct > 0 ? "+" : ""}${pct.toFixed(0)}%`;
|
|
164
|
+
}
|
|
165
|
+
function isTimeSeries(arr) {
|
|
166
|
+
if (arr.length === 0) return false;
|
|
167
|
+
const first = arr[0];
|
|
168
|
+
if (typeof first !== "object" || first === null) return false;
|
|
169
|
+
const keys = Object.keys(first);
|
|
170
|
+
const hasBucket = keys.includes("week") || keys.includes("date") || keys.includes("month");
|
|
171
|
+
const hasMetric = keys.includes("clicks") || keys.includes("impressions") || keys.includes("value");
|
|
172
|
+
return hasBucket && hasMetric;
|
|
173
|
+
}
|
|
174
|
+
function pickBucketKey(first) {
|
|
175
|
+
if ("week" in first) return "week";
|
|
176
|
+
if ("date" in first) return "date";
|
|
177
|
+
return "month";
|
|
178
|
+
}
|
|
179
|
+
function pickMetricKey(first) {
|
|
180
|
+
if ("clicks" in first) return "clicks";
|
|
181
|
+
if ("impressions" in first) return "impressions";
|
|
182
|
+
return "value";
|
|
183
|
+
}
|
|
184
|
+
function computeAlignedSparklines(results, col) {
|
|
185
|
+
const allBuckets = /* @__PURE__ */ new Set();
|
|
186
|
+
const perRow = [];
|
|
187
|
+
let bucketKey = "week";
|
|
188
|
+
let metricKey = "clicks";
|
|
189
|
+
for (const r of results) {
|
|
190
|
+
const arr = r[col];
|
|
191
|
+
if (!Array.isArray(arr) || !isTimeSeries(arr)) {
|
|
192
|
+
perRow.push(null);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const first = arr[0];
|
|
196
|
+
bucketKey = pickBucketKey(first);
|
|
197
|
+
metricKey = pickMetricKey(first);
|
|
198
|
+
const m = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const item of arr) {
|
|
200
|
+
const rec = item;
|
|
201
|
+
const key = String(rec[bucketKey]);
|
|
202
|
+
const val = Number(rec[metricKey] ?? 0);
|
|
203
|
+
allBuckets.add(key);
|
|
204
|
+
m.set(key, val);
|
|
205
|
+
}
|
|
206
|
+
perRow.push(m);
|
|
207
|
+
}
|
|
208
|
+
const sorted = [...allBuckets].sort();
|
|
209
|
+
return perRow.map((m) => {
|
|
210
|
+
if (!m) return "";
|
|
211
|
+
const values = sorted.map((b) => m.has(b) ? m.get(b) : null);
|
|
212
|
+
const nonNull = values.filter((v) => v != null);
|
|
213
|
+
if (nonNull.length === 0) return SPARK_GAP.repeat(values.length);
|
|
214
|
+
const min = Math.min(...nonNull);
|
|
215
|
+
const range = Math.max(...nonNull) - min;
|
|
216
|
+
return values.map((v) => {
|
|
217
|
+
if (v == null) return SPARK_GAP;
|
|
218
|
+
if (range === 0) return SPARK_CHARS[0];
|
|
219
|
+
return SPARK_CHARS[Math.round((v - min) / range * (SPARK_CHARS.length - 1))];
|
|
220
|
+
}).join("");
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function classifyCol(col, values) {
|
|
224
|
+
const firstNonNull = values.find((v) => v != null);
|
|
225
|
+
if (firstNonNull == null) return "text";
|
|
226
|
+
if (Array.isArray(firstNonNull) && isTimeSeries(firstNonNull)) return "series";
|
|
227
|
+
if (col in PERCENT_COLS && values.every((v) => v == null || typeof v === "number")) return "pct";
|
|
228
|
+
if (values.every((v) => v == null || typeof v === "number")) return values.some((v) => typeof v === "number" && !Number.isInteger(v)) ? "float" : "int";
|
|
229
|
+
return "text";
|
|
230
|
+
}
|
|
231
|
+
function formatCellKinded(val, col, kind) {
|
|
232
|
+
if (val == null) return "";
|
|
233
|
+
if (kind === "int") return typeof val === "number" ? String(val) : String(val);
|
|
234
|
+
if (kind === "float") return typeof val === "number" ? val.toFixed(2) : String(val);
|
|
235
|
+
if (kind === "pct") return typeof val === "number" ? formatPct(val, PERCENT_COLS[col]) : String(val);
|
|
236
|
+
if (Array.isArray(val)) return `[${val.length} item${val.length === 1 ? "" : "s"}]`;
|
|
237
|
+
if (typeof val === "object") return JSON.stringify(val);
|
|
238
|
+
return String(val);
|
|
239
|
+
}
|
|
240
|
+
function computeRowSeriesSparkline(results) {
|
|
241
|
+
if (results.length < 2) return null;
|
|
242
|
+
const first = results[0];
|
|
243
|
+
const bucketKey = "week" in first ? "week" : "date" in first ? "date" : "month" in first ? "month" : null;
|
|
244
|
+
if (!bucketKey) return null;
|
|
245
|
+
const metricKey = "value" in first ? "value" : "clicks" in first ? "clicks" : "impressions" in first ? "impressions" : null;
|
|
246
|
+
if (!metricKey) return null;
|
|
247
|
+
for (const r of results) if (!(bucketKey in r) || !(metricKey in r)) return null;
|
|
248
|
+
const values = [...results].sort((a, b) => String(a[bucketKey]).localeCompare(String(b[bucketKey]))).map((r) => Number(r[metricKey] ?? 0));
|
|
249
|
+
const nonNull = values.filter((v) => Number.isFinite(v));
|
|
250
|
+
if (nonNull.length === 0) return null;
|
|
251
|
+
const min = Math.min(...nonNull);
|
|
252
|
+
const range = Math.max(...nonNull) - min;
|
|
253
|
+
return {
|
|
254
|
+
spark: values.map((v) => {
|
|
255
|
+
if (range === 0) return SPARK_CHARS[0];
|
|
256
|
+
return SPARK_CHARS[Math.round((v - min) / range * (SPARK_CHARS.length - 1))];
|
|
257
|
+
}).join(""),
|
|
258
|
+
label: `${results.length} ${bucketKey}${results.length === 1 ? "" : "s"} of ${metricKey}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function renderResults(results, total, format) {
|
|
262
|
+
if (format === "csv" && results.length > 0) {
|
|
263
|
+
const cols = Object.keys(results[0]);
|
|
264
|
+
console.log(toCSV(results, cols));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (results.length === 0) {
|
|
268
|
+
logger.warn("No results found");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const cols = Object.keys(results[0]);
|
|
272
|
+
const kinds = cols.map((c) => classifyCol(c, results.map((r) => r[c])));
|
|
273
|
+
const sparklineByCol = {};
|
|
274
|
+
cols.forEach((c, i) => {
|
|
275
|
+
if (kinds[i] === "series") sparklineByCol[c] = computeAlignedSparklines(results, c);
|
|
276
|
+
});
|
|
277
|
+
const cellText = (row, rowIdx, colIdx) => {
|
|
278
|
+
const c = cols[colIdx];
|
|
279
|
+
const k = kinds[colIdx];
|
|
280
|
+
if (k === "series") return sparklineByCol[c][rowIdx];
|
|
281
|
+
return formatCellKinded(row[c], c, k);
|
|
282
|
+
};
|
|
283
|
+
const widths = cols.map((c, i) => {
|
|
284
|
+
let w = c.length;
|
|
285
|
+
const limit = Math.min(results.length, 20);
|
|
286
|
+
for (let j = 0; j < limit; j++) {
|
|
287
|
+
const len = cellText(results[j], j, i).length;
|
|
288
|
+
if (len > w) w = len;
|
|
289
|
+
}
|
|
290
|
+
return w;
|
|
291
|
+
});
|
|
292
|
+
console.log();
|
|
293
|
+
console.log(` ${cols.map((c, i) => c.padEnd(widths[i])).join(" ")}`);
|
|
294
|
+
console.log(` ${cols.map((_, i) => "─".repeat(widths[i])).join(" ")}`);
|
|
295
|
+
for (let r = 0; r < results.length; r++) console.log(` ${cols.map((_, i) => cellText(results[r], r, i).padEnd(widths[i])).join(" ")}`);
|
|
296
|
+
const rowSeriesSparkline = computeRowSeriesSparkline(results);
|
|
297
|
+
if (rowSeriesSparkline) {
|
|
298
|
+
console.log();
|
|
299
|
+
console.log(` trend: ${rowSeriesSparkline.spark} (${rowSeriesSparkline.label})`);
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
logger.success(`${results.length} results`);
|
|
303
|
+
if (total > results.length) logger.info(`Total: ${total} (showing ${results.length})`);
|
|
304
|
+
}
|
|
305
|
+
const analyzeCommand = defineCommand({
|
|
306
|
+
meta: {
|
|
307
|
+
name: "analyze",
|
|
308
|
+
description: "SEO analysis tools"
|
|
309
|
+
},
|
|
310
|
+
subCommands: {
|
|
311
|
+
list: defineCommand({
|
|
312
|
+
meta: {
|
|
313
|
+
name: "list",
|
|
314
|
+
description: "List available analyzer ids"
|
|
315
|
+
},
|
|
316
|
+
args: { json: {
|
|
317
|
+
type: "boolean",
|
|
318
|
+
default: false,
|
|
319
|
+
description: "Output as JSON"
|
|
320
|
+
} },
|
|
321
|
+
async run({ args }) {
|
|
322
|
+
if (args.json) {
|
|
323
|
+
console.log(JSON.stringify(ANALYSIS_TOOLS, null, 2));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
for (const id of ANALYSIS_TOOLS) console.log(id);
|
|
327
|
+
}
|
|
328
|
+
}),
|
|
329
|
+
...Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
export { analyzeCommand };
|