@gscdump/cli 0.37.3 → 0.37.5

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,293 @@
1
+ import { loadConfig, saveConfig } from "./config.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "./utils.mjs";
3
+ import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile } from "./profile.mjs";
4
+ import { authenticate, clearTokens, formatAuthProvenance, getAuth, getAuthCredentials, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "./auth.mjs";
5
+ import { runSmokeTest } from "./init.mjs";
6
+ import process from "node:process";
7
+ import { defineCommand } from "citty";
8
+ import path from "node:path";
9
+ import { hasGscWriteScope, hasIndexingScope } from "gscdump/api";
10
+ import { ofetch } from "ofetch";
11
+ const AUTH_SUBCOMMANDS = [
12
+ "status",
13
+ "login",
14
+ "logout",
15
+ "refresh",
16
+ "scopes"
17
+ ];
18
+ const REQUIRED_SCOPES = [
19
+ "https://www.googleapis.com/auth/webmasters",
20
+ "https://www.googleapis.com/auth/indexing",
21
+ "https://www.googleapis.com/auth/siteverification"
22
+ ];
23
+ function hasGoogleScope(scopes, scope) {
24
+ const suffix = scope.replace("https://www.googleapis.com/auth/", "");
25
+ return scopes.includes(scope) || scopes.includes(suffix);
26
+ }
27
+ function missingRequiredScopes(scopes) {
28
+ return REQUIRED_SCOPES.filter((scope) => {
29
+ if (scope.endsWith("/webmasters")) return !hasGscWriteScope(scopes);
30
+ if (scope.endsWith("/indexing")) return !hasIndexingScope(scopes);
31
+ return !hasGoogleScope(scopes, scope);
32
+ });
33
+ }
34
+ async function fetchTokenInfo(accessToken) {
35
+ return ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: accessToken } }).catch(() => null);
36
+ }
37
+ async function resolveLiveAuthState() {
38
+ const tokens = await loadTokens();
39
+ const byok = resolveBYOK();
40
+ let liveToken = null;
41
+ if (typeof byok === "string") liveToken = byok;
42
+ else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch(() => null);
43
+ else if (tokens?.access_token) liveToken = tokens.access_token;
44
+ const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
45
+ const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
46
+ const missing = missingRequiredScopes(scopes);
47
+ return {
48
+ byok,
49
+ tokens,
50
+ liveToken,
51
+ tokenInfo,
52
+ scopes,
53
+ missing
54
+ };
55
+ }
56
+ async function runStatus(args) {
57
+ const { json } = applyOutputMode(args);
58
+ const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
59
+ const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
60
+ if (json) {
61
+ console.log(JSON.stringify({
62
+ authenticated: !!tokens || !!byok,
63
+ source: byok ? "byok" : tokens ? "saved-tokens" : null,
64
+ byokKind,
65
+ scopes,
66
+ tokenAccount: tokenInfo?.email ?? null,
67
+ tokens: tokens ? {
68
+ hasAccessToken: !!tokens.access_token,
69
+ hasRefreshToken: !!tokens.refresh_token,
70
+ expiry: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : null,
71
+ expired: tokens.expiry_date ? tokens.expiry_date < Date.now() : null
72
+ } : null
73
+ }, null, 2));
74
+ return;
75
+ }
76
+ const reportScopes = () => {
77
+ if (scopes.length === 0) return;
78
+ console.log(` Scopes:`);
79
+ for (const s of scopes) console.log(` \x1B[90m└─\x1B[0m ${s}`);
80
+ if (missing.length > 0) {
81
+ console.log(` \x1B[33mMissing scopes:\x1B[0m`);
82
+ for (const s of missing) console.log(` \x1B[90m└─\x1B[0m ${s}`);
83
+ console.log(` \x1B[90mRun \`gscdump auth login --force\` to re-consent.\x1B[0m`);
84
+ }
85
+ };
86
+ if (byok) {
87
+ logger.success(`Authenticated via BYOK (${byokKind})`);
88
+ if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
89
+ reportScopes();
90
+ return;
91
+ }
92
+ if (!tokens) {
93
+ logger.warn("Not authenticated");
94
+ logger.info("Run `gscdump init` (full setup) or `gscdump auth login` (OAuth only)");
95
+ logger.info("Or set GSC_ACCESS_TOKEN / GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN env vars");
96
+ return;
97
+ }
98
+ const hasAccess = !!tokens.access_token;
99
+ const hasRefresh = !!tokens.refresh_token;
100
+ const expiry = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
101
+ const isExpired = expiry && expiry < /* @__PURE__ */ new Date();
102
+ if (isExpired && hasRefresh) logger.warn("Token expired; refresh available. Run `gscdump auth refresh`, or any live command will auto-refresh.");
103
+ else if (isExpired) logger.error("Token expired and no refresh token present. Run `gscdump auth login`.");
104
+ else logger.success("Authenticated (saved tokens)");
105
+ console.log();
106
+ console.log(` Access token: ${hasAccess ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
107
+ console.log(` Refresh token: ${hasRefresh ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
108
+ if (expiry) {
109
+ const status = isExpired ? "\x1B[33mexpired\x1B[0m" : "\x1B[32mvalid\x1B[0m";
110
+ console.log(` Expires: ${expiry.toISOString()} (${status})`);
111
+ }
112
+ if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
113
+ reportScopes();
114
+ if (isExpired && !hasRefresh) process.exit(1);
115
+ }
116
+ const statusCommand = defineCommand({
117
+ meta: {
118
+ name: "status",
119
+ description: "Show current authentication status"
120
+ },
121
+ args: { ...OUTPUT_ARGS },
122
+ async run({ args }) {
123
+ await runStatus(args);
124
+ }
125
+ });
126
+ const refreshCommand = defineCommand({
127
+ meta: {
128
+ name: "refresh",
129
+ description: "Force-refresh saved OAuth tokens (no-op for BYOK)"
130
+ },
131
+ args: { ...OUTPUT_ARGS },
132
+ async run({ args }) {
133
+ applyOutputMode(args);
134
+ if (resolveBYOK()) {
135
+ logger.info("BYOK detected; refresh handled per-call by the SDK");
136
+ return;
137
+ }
138
+ const tokens = await loadTokens();
139
+ if (!tokens?.refresh_token) {
140
+ logger.error("No saved refresh token. Run `gscdump auth login`.");
141
+ process.exit(1);
142
+ }
143
+ const credentials = await getAuthCredentials(false).catch((e) => {
144
+ logger.error(`Cannot resolve credentials: ${e.message}`);
145
+ process.exit(1);
146
+ });
147
+ await saveTokens({
148
+ ...tokens,
149
+ expiry_date: 1
150
+ });
151
+ if ((await authenticate(credentials, false).catch((e) => {
152
+ logger.error(`Refresh failed: ${e.message}`);
153
+ process.exit(1);
154
+ })).credentials?.access_token) logger.success("Token refreshed");
155
+ else logger.warn("Refresh completed but no new access token returned");
156
+ }
157
+ });
158
+ const loginCommand = defineCommand({
159
+ meta: {
160
+ name: "login",
161
+ description: "Run OAuth flow and persist tokens (skip if BYOK env vars set)"
162
+ },
163
+ args: {
164
+ ...OUTPUT_ARGS,
165
+ "force": {
166
+ type: "boolean",
167
+ alias: "f",
168
+ default: false,
169
+ description: "Re-run OAuth even if tokens already exist"
170
+ },
171
+ "browser": {
172
+ type: "boolean",
173
+ default: true,
174
+ description: "Use loopback browser flow. Pass --no-browser for device-code (headless)."
175
+ },
176
+ "service-account": {
177
+ type: "string",
178
+ description: "Path to a service-account JSON key (skips OAuth)"
179
+ }
180
+ },
181
+ async run({ args }) {
182
+ applyOutputMode(args);
183
+ if (resolveBYOK() && !args.force) {
184
+ logger.info("BYOK env vars detected, no login needed (--force to override)");
185
+ return;
186
+ }
187
+ if (args["service-account"]) {
188
+ const saPath = path.resolve(String(args["service-account"]));
189
+ const jwt = await loadServiceAccount(saPath).catch((e) => {
190
+ logger.error(`Service-account load failed: ${e.message}`);
191
+ process.exit(1);
192
+ });
193
+ await jwt.authorize().catch((e) => {
194
+ logger.error(`Service-account auth failed: ${e.message}`);
195
+ process.exit(1);
196
+ });
197
+ const config = await loadConfig();
198
+ config.serviceAccountPath = saPath;
199
+ await saveConfig(config);
200
+ logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
201
+ logger.info(`Saved path to config: ${saPath}`);
202
+ return;
203
+ }
204
+ if (args.force) await clearTokens();
205
+ const oauth = await getAuth({
206
+ interactive: true,
207
+ noBrowser: args.browser === false,
208
+ force: Boolean(args.force)
209
+ }).catch((e) => {
210
+ logger.error(`Login failed: ${e.message}`);
211
+ process.exit(1);
212
+ });
213
+ logger.success("Logged in");
214
+ await runSmokeTest(oauth);
215
+ if (!resolveActiveProfile()) {
216
+ const tokens = await loadTokens();
217
+ const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
218
+ if (info?.email) {
219
+ const name = profileNameFromEmail(info.email);
220
+ if (await adoptCurrentConfigAsProfile(name).catch((error) => {
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ logger.warn(`Login succeeded, but the config could not be moved into profile "${name}": ${message}`);
223
+ return null;
224
+ })) logger.success(`Saved as profile "${name}" (active)`);
225
+ }
226
+ }
227
+ if (resolveBYOK()) {
228
+ console.log();
229
+ console.log(await formatAuthProvenance());
230
+ }
231
+ }
232
+ });
233
+ const logoutCommand = defineCommand({
234
+ meta: {
235
+ name: "logout",
236
+ description: "Clear stored OAuth tokens"
237
+ },
238
+ args: { ...OUTPUT_ARGS },
239
+ async run({ args }) {
240
+ applyOutputMode(args);
241
+ await clearTokens();
242
+ const config = await loadConfig();
243
+ if (config.serviceAccountPath) {
244
+ delete config.serviceAccountPath;
245
+ await saveConfig(config);
246
+ logger.info("Cleared saved service-account path");
247
+ }
248
+ }
249
+ });
250
+ const scopesCommand = defineCommand({
251
+ meta: {
252
+ name: "scopes",
253
+ description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
254
+ },
255
+ args: { ...OUTPUT_ARGS },
256
+ async run({ args }) {
257
+ const { json } = applyOutputMode(args);
258
+ const { liveToken, scopes, missing } = await resolveLiveAuthState();
259
+ if (!liveToken) {
260
+ if (json) console.log(JSON.stringify({
261
+ scopes: [],
262
+ missing: null
263
+ }, null, 2));
264
+ else logger.error("Not authenticated");
265
+ process.exit(1);
266
+ }
267
+ if (json) console.log(JSON.stringify({
268
+ scopes,
269
+ missing
270
+ }, null, 2));
271
+ else for (const s of scopes) console.log(s);
272
+ if (missing.length > 0) process.exit(1);
273
+ }
274
+ });
275
+ const authCommand = defineCommand({
276
+ meta: {
277
+ name: "auth",
278
+ description: "Manage authentication"
279
+ },
280
+ args: { ...OUTPUT_ARGS },
281
+ subCommands: {
282
+ status: statusCommand,
283
+ login: loginCommand,
284
+ logout: logoutCommand,
285
+ refresh: refreshCommand,
286
+ scopes: scopesCommand
287
+ },
288
+ async run({ args }) {
289
+ if (!noSubcommandSelected("auth", AUTH_SUBCOMMANDS)) return;
290
+ await runStatus(args);
291
+ }
292
+ });
293
+ export { authCommand, loginCommand, logoutCommand, statusCommand };
@@ -0,0 +1,93 @@
1
+ import process from "node:process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createConsola } from "consola";
7
+ var __defProp = Object.defineProperty;
8
+ var __exportAll = (all, no_symbols) => {
9
+ let target = {};
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
15
+ return target;
16
+ };
17
+ const runtimeStorage = new AsyncLocalStorage();
18
+ let fallbackRuntime;
19
+ function createCliRuntime(opts = {}) {
20
+ const stderr = opts.stderr ?? process.stderr;
21
+ const baseLogger = createConsola({
22
+ stdout: stderr,
23
+ stderr
24
+ });
25
+ return {
26
+ activeProfileOverride: null,
27
+ colorEnabled: true,
28
+ configDir: opts.configDir ?? path.join(os.homedir(), ".config", "gscdump"),
29
+ configDirOverridden: false,
30
+ environment: opts.environment ?? process.env,
31
+ logger: baseLogger.withTag("gscdump"),
32
+ quiet: false,
33
+ rawArgs: [...opts.rawArgs ?? process.argv.slice(2)]
34
+ };
35
+ }
36
+ function useCliRuntime() {
37
+ const active = runtimeStorage.getStore();
38
+ if (active) return active;
39
+ fallbackRuntime ??= createCliRuntime();
40
+ return fallbackRuntime;
41
+ }
42
+ function runWithCliRuntime(runtime, run) {
43
+ return runtimeStorage.run(runtime, run);
44
+ }
45
+ var config_exports = /* @__PURE__ */ __exportAll({
46
+ defaultDataDir: () => defaultDataDir,
47
+ getConfigDir: () => getConfigDir,
48
+ getConfigPath: () => getConfigPath,
49
+ loadConfig: () => loadConfig,
50
+ loadResolvedConfig: () => loadResolvedConfig,
51
+ resolveDataDir: () => resolveDataDir,
52
+ saveConfig: () => saveConfig,
53
+ setConfigDir: () => setConfigDir
54
+ });
55
+ function setConfigDir(dir) {
56
+ useCliRuntime().configDir = dir;
57
+ }
58
+ function getConfigDir() {
59
+ return useCliRuntime().configDir;
60
+ }
61
+ function defaultDataDir() {
62
+ return path.join(os.homedir(), ".gscdump", "data");
63
+ }
64
+ function resolveDataDir(config) {
65
+ return expandTilde(config.dataDir ?? defaultDataDir());
66
+ }
67
+ function expandTilde(p) {
68
+ if (p === "~") return os.homedir();
69
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
70
+ return p;
71
+ }
72
+ async function loadConfig() {
73
+ return fs.readFile(path.join(getConfigDir(), "config.json"), "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
74
+ }
75
+ async function loadResolvedConfig() {
76
+ const config = await loadConfig();
77
+ return {
78
+ config,
79
+ dataDir: resolveDataDir(config)
80
+ };
81
+ }
82
+ async function saveConfig(config) {
83
+ const configDir = getConfigDir();
84
+ await fs.mkdir(configDir, {
85
+ recursive: true,
86
+ mode: 448
87
+ });
88
+ await fs.writeFile(path.join(configDir, "config.json"), JSON.stringify(config, null, 2), { mode: 384 });
89
+ }
90
+ function getConfigPath() {
91
+ return path.join(getConfigDir(), "config.json");
92
+ }
93
+ export { __exportAll, config_exports, createCliRuntime, defaultDataDir, getConfigDir, getConfigPath, loadConfig, loadResolvedConfig, runWithCliRuntime, saveConfig, setConfigDir, useCliRuntime };
@@ -0,0 +1,232 @@
1
+ import { getConfigPath, loadConfig, saveConfig } from "./config.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, displayPath, logger, noSubcommandSelected } from "./utils.mjs";
3
+ import process from "node:process";
4
+ import { defineCommand } from "citty";
5
+ const showCommand = defineCommand({
6
+ meta: {
7
+ name: "show",
8
+ description: "Show current config"
9
+ },
10
+ args: { ...OUTPUT_ARGS },
11
+ async run({ args }) {
12
+ const { json } = applyOutputMode(args);
13
+ const config = await loadConfig();
14
+ const configPath = getConfigPath();
15
+ if (json) {
16
+ console.log(JSON.stringify({
17
+ path: configPath,
18
+ config
19
+ }, null, 2));
20
+ return;
21
+ }
22
+ logger.info(`Config: ${displayPath(configPath)}`);
23
+ console.log();
24
+ if (Object.keys(config).length === 0) {
25
+ logger.warn("No config set");
26
+ return;
27
+ }
28
+ console.log(JSON.stringify(config, null, 2));
29
+ }
30
+ });
31
+ const VALID_KEYS = [
32
+ "defaultSite",
33
+ "defaultPeriod",
34
+ "defaultFormat",
35
+ "defaultDb",
36
+ "dataDir",
37
+ "defaultLimit",
38
+ "defaultSearchType",
39
+ "defaultDataState",
40
+ "serviceAccountPath"
41
+ ];
42
+ const NUMERIC_KEYS = /* @__PURE__ */ new Set(["defaultLimit"]);
43
+ const configCommand = defineCommand({
44
+ meta: {
45
+ name: "config",
46
+ description: "Manage configuration"
47
+ },
48
+ subCommands: {
49
+ show: showCommand,
50
+ set: defineCommand({
51
+ meta: {
52
+ name: "set",
53
+ description: "Set a config value"
54
+ },
55
+ args: {
56
+ key: {
57
+ type: "positional",
58
+ description: `Config key (${VALID_KEYS.join(", ")})`,
59
+ required: true
60
+ },
61
+ value: {
62
+ type: "positional",
63
+ description: "Value to set",
64
+ required: true
65
+ },
66
+ ...OUTPUT_ARGS
67
+ },
68
+ async run({ args }) {
69
+ applyOutputMode(args);
70
+ if (!VALID_KEYS.includes(args.key)) {
71
+ logger.error(`Invalid key: ${args.key}`);
72
+ logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
73
+ process.exit(1);
74
+ }
75
+ const config = await loadConfig();
76
+ const value = NUMERIC_KEYS.has(args.key) ? Number(args.value) : args.value;
77
+ if (NUMERIC_KEYS.has(args.key) && !Number.isFinite(value)) {
78
+ logger.error(`Invalid numeric value for ${args.key}: ${args.value}`);
79
+ process.exit(1);
80
+ }
81
+ config[args.key] = value;
82
+ await saveConfig(config);
83
+ logger.success(`Set ${args.key} = ${value}`);
84
+ }
85
+ }),
86
+ unset: defineCommand({
87
+ meta: {
88
+ name: "unset",
89
+ description: "Remove a config value"
90
+ },
91
+ args: {
92
+ key: {
93
+ type: "positional",
94
+ description: `Config key to remove (${VALID_KEYS.join(", ")})`,
95
+ required: true
96
+ },
97
+ ...OUTPUT_ARGS
98
+ },
99
+ async run({ args }) {
100
+ applyOutputMode(args);
101
+ if (!VALID_KEYS.includes(args.key)) {
102
+ logger.error(`Invalid key: ${args.key}`);
103
+ logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
104
+ process.exit(1);
105
+ }
106
+ const config = await loadConfig();
107
+ delete config[args.key];
108
+ await saveConfig(config);
109
+ logger.success(`Removed ${args.key}`);
110
+ }
111
+ }),
112
+ path: defineCommand({
113
+ meta: {
114
+ name: "path",
115
+ description: "Show config file path"
116
+ },
117
+ run() {
118
+ console.log(getConfigPath());
119
+ }
120
+ }),
121
+ validate: defineCommand({
122
+ meta: {
123
+ name: "validate",
124
+ description: "Validate the saved config (defaultSite is verified, dataDir exists/writable)"
125
+ },
126
+ args: { ...OUTPUT_ARGS },
127
+ async run({ args }) {
128
+ const { json } = applyOutputMode(args);
129
+ const { resolveDataDir } = await import("./config.mjs").then((n) => n.config_exports);
130
+ const fs = await import("node:fs/promises");
131
+ const config = await loadConfig();
132
+ const issues = [];
133
+ const dataDir = resolveDataDir(config);
134
+ const dataDirDisplay = displayPath(dataDir);
135
+ const stat = await fs.stat(dataDir).catch(() => null);
136
+ if (stat && !stat.isDirectory()) issues.push({
137
+ key: "dataDir",
138
+ level: "fail",
139
+ message: `${dataDirDisplay} is not a directory`
140
+ });
141
+ else if (stat) {
142
+ const probe = `${dataDir}/.gscdump-config-probe`;
143
+ if (!await fs.writeFile(probe, "").then(() => fs.rm(probe)).then(() => true).catch(() => false)) issues.push({
144
+ key: "dataDir",
145
+ level: "fail",
146
+ message: `${dataDirDisplay} not writable`
147
+ });
148
+ } else issues.push({
149
+ key: "dataDir",
150
+ level: "warn",
151
+ message: `${dataDirDisplay} does not exist (will be created on first sync)`
152
+ });
153
+ if (config.defaultSite) if (!!config.clientId && !!config.clientSecret) {
154
+ const { createCommandContext } = await import("./context.mjs").then((n) => n.context_exports);
155
+ const ctx = await createCommandContext({ needsAuth: true }).catch(() => null);
156
+ if (ctx) {
157
+ const sites = await ctx.loadSites().catch(() => null);
158
+ if (sites && !sites.some((s) => s.siteUrl === config.defaultSite || s.siteUrl.includes(String(config.defaultSite)))) issues.push({
159
+ key: "defaultSite",
160
+ level: "fail",
161
+ message: `${config.defaultSite} is not in the verified site list`
162
+ });
163
+ }
164
+ } else issues.push({
165
+ key: "defaultSite",
166
+ level: "warn",
167
+ message: "set, but auth not configured — skipping verification"
168
+ });
169
+ if (config.defaultFormat && !["json", "csv"].includes(config.defaultFormat)) issues.push({
170
+ key: "defaultFormat",
171
+ level: "fail",
172
+ message: `unknown format: ${config.defaultFormat}`
173
+ });
174
+ const { SearchTypes } = await import("gscdump/query");
175
+ const allowedSearchTypes = Object.values(SearchTypes);
176
+ if (config.defaultSearchType && !allowedSearchTypes.includes(config.defaultSearchType)) issues.push({
177
+ key: "defaultSearchType",
178
+ level: "fail",
179
+ message: `unknown search type: ${config.defaultSearchType} (allowed: ${allowedSearchTypes.join(", ")})`
180
+ });
181
+ const allowedDataStates = [
182
+ "all",
183
+ "final",
184
+ "hourly_all"
185
+ ];
186
+ if (config.defaultDataState && !allowedDataStates.includes(config.defaultDataState)) issues.push({
187
+ key: "defaultDataState",
188
+ level: "fail",
189
+ message: `unknown data state: ${config.defaultDataState} (allowed: ${allowedDataStates.join(", ")})`
190
+ });
191
+ if (config.serviceAccountPath) {
192
+ if (!await fs.stat(config.serviceAccountPath).catch(() => null)) issues.push({
193
+ key: "serviceAccountPath",
194
+ level: "fail",
195
+ message: `${displayPath(config.serviceAccountPath)} does not exist`
196
+ });
197
+ }
198
+ if (json) {
199
+ console.log(JSON.stringify({
200
+ ok: !issues.some((i) => i.level === "fail"),
201
+ issues
202
+ }, null, 2));
203
+ return;
204
+ }
205
+ if (issues.length === 0) {
206
+ logger.success("Config OK");
207
+ return;
208
+ }
209
+ for (const i of issues) {
210
+ const prefix = i.level === "fail" ? "\x1B[31m✗\x1B[0m" : "\x1B[33m!\x1B[0m";
211
+ console.log(` ${prefix} ${i.key}: ${i.message}`);
212
+ }
213
+ if (issues.some((i) => i.level === "fail")) process.exit(1);
214
+ }
215
+ })
216
+ },
217
+ async run({ args }) {
218
+ if (!noSubcommandSelected("config", [
219
+ "show",
220
+ "set",
221
+ "unset",
222
+ "path",
223
+ "validate"
224
+ ])) return;
225
+ await showCommand.run?.({
226
+ args,
227
+ cmd: showCommand,
228
+ rawArgs: []
229
+ });
230
+ }
231
+ });
232
+ export { configCommand };
@@ -0,0 +1,69 @@
1
+ import { __exportAll, loadResolvedConfig } from "./config.mjs";
2
+ import { logger } from "./utils.mjs";
3
+ import { resolveAuth } from "./auth.mjs";
4
+ import process from "node:process";
5
+ import { cancel, isCancel, select } from "@clack/prompts";
6
+ import { googleSearchConsole } from "gscdump/api";
7
+ import { createNodeHarness } from "@gscdump/engine/node";
8
+ import { TABLE_DIMS, transformGscRow } from "@gscdump/engine/ingest";
9
+ import { allTables, inferTable } from "@gscdump/engine/schema";
10
+ function createLocalStore(opts) {
11
+ return createNodeHarness(opts);
12
+ }
13
+ var context_exports = /* @__PURE__ */ __exportAll({ createCommandContext: () => createCommandContext });
14
+ async function createCommandContext(opts = {}) {
15
+ const { needsAuth = false, needsStore = false, interactive = false, byok, fetchOptions } = opts;
16
+ const { config, dataDir } = await loadResolvedConfig();
17
+ const auth = needsAuth ? await resolveAuth({
18
+ interactive,
19
+ config,
20
+ byok
21
+ }) : null;
22
+ const client = auth ? googleSearchConsole(auth, { fetchOptions }) : null;
23
+ const store = needsStore ? createLocalStore({ dataDir }) : null;
24
+ const loadSites = async () => {
25
+ if (!client) throw new Error("loadSites requires needsAuth: true");
26
+ return (await client.sites().catch((e) => {
27
+ logger.error(`Failed to fetch sites: ${e.message}`);
28
+ process.exit(1);
29
+ })).filter((s) => s.siteUrl && s.permissionLevel !== "siteUnverifiedUser").map((s) => ({
30
+ siteUrl: s.siteUrl,
31
+ permissionLevel: s.permissionLevel || "unknown"
32
+ }));
33
+ };
34
+ const resolveSite = async (target) => {
35
+ const hint = target ?? config.defaultSite;
36
+ const sites = await loadSites();
37
+ if (sites.length === 0) {
38
+ logger.error("No verified sites found");
39
+ process.exit(1);
40
+ }
41
+ if (hint) {
42
+ const match = sites.find((s) => s.siteUrl === hint || s.siteUrl.includes(hint));
43
+ if (match) return match.siteUrl;
44
+ }
45
+ if (sites.length === 1) return sites[0].siteUrl;
46
+ const selected = await select({
47
+ message: "Select a site",
48
+ options: sites.map((s) => ({
49
+ value: s.siteUrl,
50
+ label: s.siteUrl
51
+ }))
52
+ });
53
+ if (isCancel(selected)) {
54
+ cancel("Cancelled");
55
+ process.exit(0);
56
+ }
57
+ return selected;
58
+ };
59
+ return {
60
+ config,
61
+ dataDir,
62
+ auth,
63
+ client,
64
+ store,
65
+ loadSites,
66
+ resolveSite
67
+ };
68
+ }
69
+ export { TABLE_DIMS, allTables, context_exports, createCommandContext, createLocalStore, inferTable, transformGscRow };