@gscdump/cli 1.4.0 → 1.4.2
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/{_chunks/analysis-local.mjs → analysis-local.mjs} +4 -3
- package/dist/{_chunks/auth.mjs → auth.mjs} +2 -2
- package/dist/cli.d.mts +1 -19
- package/dist/cli.mjs +25 -25
- package/dist/{_chunks → commands}/analyze.mjs +3 -3
- package/dist/{_chunks/auth2.mjs → commands/auth.mjs} +4 -4
- package/dist/commands/compact.mjs +128 -0
- package/dist/{_chunks/config2.mjs → commands/config.mjs} +4 -4
- package/dist/{_chunks → commands}/doctor.mjs +8 -7
- package/dist/{_chunks → commands}/dump.mjs +5 -4
- package/dist/{_chunks → commands}/entities.mjs +2 -2
- package/dist/commands/export.mjs +76 -0
- package/dist/commands/gc.mjs +74 -0
- package/dist/{_chunks → commands}/indexing.mjs +3 -3
- package/dist/{_chunks → commands}/init.mjs +5 -5
- package/dist/{_chunks → commands}/inspect.mjs +3 -3
- package/dist/commands/mcp.mjs +57 -0
- package/dist/{_chunks → commands}/profile.mjs +7 -17
- package/dist/{_chunks → commands}/query.mjs +5 -4
- package/dist/{_chunks → commands}/report.mjs +2 -2
- package/dist/commands/rollups.mjs +134 -0
- package/dist/{_chunks → commands}/sitemaps.mjs +3 -3
- package/dist/{_chunks → commands}/sites.mjs +3 -3
- package/dist/commands/stats.mjs +118 -0
- package/dist/commands/store-purge.mjs +93 -0
- package/dist/commands/store.mjs +40 -0
- package/dist/{_chunks → commands}/sync.mjs +3 -2
- package/dist/config.mjs +43 -0
- package/dist/{_chunks/context.mjs → context.mjs} +3 -9
- package/dist/{_chunks/env-file.mjs → env-file.mjs} +3 -3
- package/dist/{_chunks/environment.mjs → environment.mjs} +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/local-store.mjs +7 -0
- package/dist/mcp/errors.mjs +85 -0
- package/dist/mcp/handlers/analytics.mjs +1 -0
- package/dist/mcp/handlers/diagnostics.mjs +134 -0
- package/dist/mcp/handlers/index.mjs +7 -0
- package/dist/mcp/handlers/indexing.mjs +27 -0
- package/dist/mcp/handlers/query.mjs +5 -0
- package/dist/mcp/handlers/reports.mjs +72 -0
- package/dist/mcp/handlers/sites.mjs +8 -0
- package/dist/mcp/server/index.mjs +417 -0
- package/dist/mcp/types.mjs +86 -0
- package/dist/package.mjs +2 -0
- package/dist/runtime.d.mts +20 -0
- package/dist/runtime.mjs +38 -0
- package/dist/{_chunks/utils.mjs → utils.mjs} +4 -4
- package/package.json +8 -8
- package/dist/_chunks/config.mjs +0 -93
- package/dist/_chunks/mcp.mjs +0 -867
- package/dist/_chunks/store.mjs +0 -628
- /package/dist/{_chunks/error-handler.mjs → error-handler.mjs} +0 -0
- /package/dist/{_chunks/native-duckdb.mjs → native-duckdb.mjs} +0 -0
package/dist/_chunks/store.mjs
DELETED
|
@@ -1,628 +0,0 @@
|
|
|
1
|
-
import { OUTPUT_ARGS, applyOutputMode, displayPath, formatAge, logger, noSubcommandSelected } from "./utils.mjs";
|
|
2
|
-
import { allTables, createCommandContext } from "./context.mjs";
|
|
3
|
-
import { materializeParquetTables } from "./native-duckdb.mjs";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { defineCommand } from "citty";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { confirm, isCancel } from "@clack/prompts";
|
|
8
|
-
import { INTENT_CLASSIFIER_VERSION, NORMALIZER_VERSION, classifyQueryIntent, encodeIntent, normalizeQuery } from "@gscdump/analysis";
|
|
9
|
-
import { buildQueryDimRecords, createQueryDimStore } from "@gscdump/engine/entities";
|
|
10
|
-
import { inferLegacyTier } from "@gscdump/engine";
|
|
11
|
-
import { CANONICAL_ROLLUPS, DEFAULT_ROLLUPS, rebuildRollups } from "@gscdump/engine/rollups";
|
|
12
|
-
import { filesystemStats } from "@gscdump/engine/filesystem";
|
|
13
|
-
const compactCommand = defineCommand({
|
|
14
|
-
meta: {
|
|
15
|
-
name: "compact",
|
|
16
|
-
description: "Run tiered compaction (raw→d7 at 7d, d7→d30 at 30d, d30→d90 at 90d)"
|
|
17
|
-
},
|
|
18
|
-
args: {
|
|
19
|
-
"site": {
|
|
20
|
-
type: "string",
|
|
21
|
-
alias: "s",
|
|
22
|
-
description: "Restrict to a single site (default: all sites with local data)"
|
|
23
|
-
},
|
|
24
|
-
"raw-days": {
|
|
25
|
-
type: "string",
|
|
26
|
-
description: "Override raw→d7 age threshold in days (default: 7)"
|
|
27
|
-
},
|
|
28
|
-
"d7-days": {
|
|
29
|
-
type: "string",
|
|
30
|
-
description: "Override d7→d30 age threshold in days (default: 30)"
|
|
31
|
-
},
|
|
32
|
-
"d30-days": {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "Override d30→d90 age threshold in days (default: 90)"
|
|
35
|
-
},
|
|
36
|
-
"dry-run": {
|
|
37
|
-
type: "boolean",
|
|
38
|
-
default: false,
|
|
39
|
-
description: "Report tier counts per (table, site) without compacting"
|
|
40
|
-
},
|
|
41
|
-
...OUTPUT_ARGS
|
|
42
|
-
},
|
|
43
|
-
async run({ args }) {
|
|
44
|
-
const { json } = applyOutputMode(args);
|
|
45
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
46
|
-
const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
|
|
47
|
-
const dryRun = Boolean(args["dry-run"]);
|
|
48
|
-
const thresholds = {};
|
|
49
|
-
if (args["raw-days"]) thresholds.raw = Number(args["raw-days"]);
|
|
50
|
-
if (args["d7-days"]) thresholds.d7 = Number(args["d7-days"]);
|
|
51
|
-
if (args["d30-days"]) thresholds.d30 = Number(args["d30-days"]);
|
|
52
|
-
const liveEntries = await store.engine.listLive({
|
|
53
|
-
userId: store.userId,
|
|
54
|
-
siteId
|
|
55
|
-
});
|
|
56
|
-
if (dryRun) {
|
|
57
|
-
const report = [];
|
|
58
|
-
for (const table of allTables()) {
|
|
59
|
-
const bySite = groupBySite(liveEntries.filter((entry) => entry.table === table));
|
|
60
|
-
for (const [s, group] of bySite) report.push({
|
|
61
|
-
table,
|
|
62
|
-
siteId: s,
|
|
63
|
-
...countByTier(group)
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
if (json) {
|
|
67
|
-
console.log(JSON.stringify({
|
|
68
|
-
thresholds,
|
|
69
|
-
plan: report
|
|
70
|
-
}, null, 2));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
console.log();
|
|
74
|
-
console.log(` table site raw d7 d30 d90`);
|
|
75
|
-
for (const r of report) console.log(` ${r.table.padEnd(20)} ${(r.siteId ?? "-").padEnd(20)} ${String(r.raw).padStart(4)} ${String(r.d7).padStart(4)} ${String(r.d30).padStart(4)} ${String(r.d90).padStart(4)}`);
|
|
76
|
-
console.log();
|
|
77
|
-
logger.info(`compact --dry-run: ${report.length} (table, site) pair(s) — pass without --dry-run to apply`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const summary = [];
|
|
81
|
-
for (const table of allTables()) {
|
|
82
|
-
const entries = liveEntries.filter((entry) => entry.table === table);
|
|
83
|
-
const siteIds = new Set(entries.map((e) => e.siteId));
|
|
84
|
-
for (const targetSite of siteIds) {
|
|
85
|
-
logger.info(`Compacting ${table} [${targetSite ?? "-"}] (raw→d7→d30→d90)`);
|
|
86
|
-
await store.engine.compactTiered({
|
|
87
|
-
userId: store.userId,
|
|
88
|
-
siteId: targetSite,
|
|
89
|
-
table
|
|
90
|
-
}, thresholds);
|
|
91
|
-
summary.push({
|
|
92
|
-
table,
|
|
93
|
-
siteId: targetSite
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (json) {
|
|
98
|
-
console.log(JSON.stringify({
|
|
99
|
-
thresholds,
|
|
100
|
-
compacted: summary
|
|
101
|
-
}, null, 2));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
logger.success(`compact: done`);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
function groupBySite(entries) {
|
|
108
|
-
const m = /* @__PURE__ */ new Map();
|
|
109
|
-
for (const e of entries) {
|
|
110
|
-
const arr = m.get(e.siteId) ?? [];
|
|
111
|
-
arr.push(e);
|
|
112
|
-
m.set(e.siteId, arr);
|
|
113
|
-
}
|
|
114
|
-
return m;
|
|
115
|
-
}
|
|
116
|
-
function countByTier(entries) {
|
|
117
|
-
let raw = 0;
|
|
118
|
-
let d7 = 0;
|
|
119
|
-
let d30 = 0;
|
|
120
|
-
let d90 = 0;
|
|
121
|
-
for (const e of entries) {
|
|
122
|
-
const tier = e.tier ?? inferLegacyTier(e) ?? "raw";
|
|
123
|
-
if (tier === "raw") raw++;
|
|
124
|
-
else if (tier === "d7") d7++;
|
|
125
|
-
else if (tier === "d30") d30++;
|
|
126
|
-
else if (tier === "d90") d90++;
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
raw,
|
|
130
|
-
d7,
|
|
131
|
-
d30,
|
|
132
|
-
d90
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
async function exportToDuckDB(opts) {
|
|
136
|
-
const outPath = path.resolve(opts.outPath);
|
|
137
|
-
const entries = await opts.engine.listLive({
|
|
138
|
-
userId: opts.userId,
|
|
139
|
-
siteId: opts.siteId
|
|
140
|
-
});
|
|
141
|
-
const inputs = [];
|
|
142
|
-
for (const table of allTables()) {
|
|
143
|
-
const tableEntries = entries.filter((entry) => entry.table === table);
|
|
144
|
-
if (tableEntries.length > 0) inputs.push({
|
|
145
|
-
table,
|
|
146
|
-
filePaths: tableEntries.map((entry) => path.join(opts.dataDir, entry.objectKey))
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const tables = await materializeParquetTables(outPath, inputs, opts.force);
|
|
150
|
-
return {
|
|
151
|
-
outPath,
|
|
152
|
-
tables,
|
|
153
|
-
totalRows: tables.reduce((acc, t) => acc + t.rows, 0)
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const exportCommand = defineCommand({
|
|
157
|
-
meta: {
|
|
158
|
-
name: "export",
|
|
159
|
-
description: "Pack live Parquet partitions into a single .duckdb file for portable distribution (browser attach, CDN serving, etc.)"
|
|
160
|
-
},
|
|
161
|
-
args: {
|
|
162
|
-
out: {
|
|
163
|
-
type: "string",
|
|
164
|
-
required: true,
|
|
165
|
-
description: "Output path for the .duckdb file"
|
|
166
|
-
},
|
|
167
|
-
site: {
|
|
168
|
-
type: "string",
|
|
169
|
-
description: "Limit export to a single site URL (omit to include all)"
|
|
170
|
-
},
|
|
171
|
-
force: {
|
|
172
|
-
type: "boolean",
|
|
173
|
-
default: false,
|
|
174
|
-
description: "Overwrite the output file if it already exists"
|
|
175
|
-
},
|
|
176
|
-
...OUTPUT_ARGS
|
|
177
|
-
},
|
|
178
|
-
async run({ args }) {
|
|
179
|
-
const { json } = applyOutputMode(args);
|
|
180
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
181
|
-
const siteId = args.site ? store.siteIdFor(args.site) : void 0;
|
|
182
|
-
const result = await exportToDuckDB({
|
|
183
|
-
engine: store.engine,
|
|
184
|
-
dataDir: store.dataDir,
|
|
185
|
-
userId: store.userId,
|
|
186
|
-
siteId,
|
|
187
|
-
outPath: args.out,
|
|
188
|
-
force: args.force
|
|
189
|
-
});
|
|
190
|
-
if (json) {
|
|
191
|
-
console.log(JSON.stringify(result, null, 2));
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (result.tables.length === 0) {
|
|
195
|
-
console.log(`\n No data to export. Run \`gscdump sync\` first.`);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
for (const t of result.tables) console.log(` ${t.table.padEnd(15)} ${String(t.files).padStart(4)} parquet → ${t.table} (${t.rows.toLocaleString()} rows)`);
|
|
199
|
-
console.log(`\n Exported ${result.tables.length} table(s), ${result.totalRows.toLocaleString()} rows → ${displayPath(result.outPath)}`);
|
|
200
|
-
console.log(`\n Attach from DuckDB: \x1B[36mATTACH '${result.outPath}' AS gsc (READ_ONLY); SELECT * FROM gsc.pages LIMIT 10;\x1B[0m`);
|
|
201
|
-
console.log(` Attach in a browser: use DuckDB-WASM registerFileBuffer + \x1B[36mATTACH 'gsc.duckdb' AS gsc (READ_ONLY)\x1B[0m`);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
const DEFAULT_GRACE_HOURS = 24;
|
|
205
|
-
const gcCommand = defineCommand({
|
|
206
|
-
meta: {
|
|
207
|
-
name: "gc",
|
|
208
|
-
description: "Delete orphaned object-store files not referenced by any manifest entry"
|
|
209
|
-
},
|
|
210
|
-
args: {
|
|
211
|
-
"grace-hours": {
|
|
212
|
-
type: "string",
|
|
213
|
-
default: String(DEFAULT_GRACE_HOURS),
|
|
214
|
-
description: `Spare orphans younger than this (default: ${DEFAULT_GRACE_HOURS}h)`
|
|
215
|
-
},
|
|
216
|
-
"site": {
|
|
217
|
-
type: "string",
|
|
218
|
-
alias: "s",
|
|
219
|
-
description: "Restrict to a single site (default: all sites)"
|
|
220
|
-
},
|
|
221
|
-
"dry-run": {
|
|
222
|
-
type: "boolean",
|
|
223
|
-
default: false,
|
|
224
|
-
description: "List retired manifest entries past the grace window without deleting"
|
|
225
|
-
},
|
|
226
|
-
...OUTPUT_ARGS
|
|
227
|
-
},
|
|
228
|
-
async run({ args }) {
|
|
229
|
-
const { json } = applyOutputMode(args);
|
|
230
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
231
|
-
const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
|
|
232
|
-
const graceMs = Number(args["grace-hours"]) * 36e5;
|
|
233
|
-
if (args["dry-run"]) {
|
|
234
|
-
const cutoff = Date.now() - graceMs;
|
|
235
|
-
const candidates = [];
|
|
236
|
-
const all = await store.engine.listAll({
|
|
237
|
-
userId: store.userId,
|
|
238
|
-
siteId
|
|
239
|
-
});
|
|
240
|
-
for (const e of all) if (e.retiredAt && e.retiredAt < cutoff) candidates.push({
|
|
241
|
-
table: e.table,
|
|
242
|
-
siteId: e.siteId,
|
|
243
|
-
partition: e.partition,
|
|
244
|
-
retiredAt: e.retiredAt,
|
|
245
|
-
objectKey: e.objectKey
|
|
246
|
-
});
|
|
247
|
-
if (json) {
|
|
248
|
-
console.log(JSON.stringify({
|
|
249
|
-
graceHours: Number(args["grace-hours"]),
|
|
250
|
-
candidates
|
|
251
|
-
}, null, 2));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
console.log();
|
|
255
|
-
for (const c of candidates) console.log(` ${c.objectKey} \x1B[90m(retired ${new Date(c.retiredAt).toISOString()})\x1B[0m`);
|
|
256
|
-
console.log();
|
|
257
|
-
logger.info(`gc --dry-run: ${candidates.length} retired manifest entry(ies) past ${args["grace-hours"]}h grace; pass without --dry-run to delete`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const result = await store.engine.gcOrphans({
|
|
261
|
-
userId: store.userId,
|
|
262
|
-
siteId
|
|
263
|
-
}, graceMs);
|
|
264
|
-
if (json) {
|
|
265
|
-
console.log(JSON.stringify({
|
|
266
|
-
graceHours: Number(args["grace-hours"]),
|
|
267
|
-
deleted: result.deleted
|
|
268
|
-
}, null, 2));
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
logger.success(`gc: deleted ${result.deleted} orphan file(s)`);
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
async function buildSiteQueryDim(store, siteId) {
|
|
275
|
-
const ctx = {
|
|
276
|
-
userId: store.userId,
|
|
277
|
-
siteId
|
|
278
|
-
};
|
|
279
|
-
const entries = await store.engine.listLive({
|
|
280
|
-
userId: ctx.userId,
|
|
281
|
-
siteId,
|
|
282
|
-
table: "queries"
|
|
283
|
-
});
|
|
284
|
-
if (entries.length === 0) return 0;
|
|
285
|
-
const { rows } = await store.engine.runSQL({
|
|
286
|
-
ctx,
|
|
287
|
-
table: "queries",
|
|
288
|
-
fileSets: { FILES: {
|
|
289
|
-
table: "queries",
|
|
290
|
-
partitions: entries.map((e) => e.partition)
|
|
291
|
-
} },
|
|
292
|
-
sql: `SELECT DISTINCT query FROM read_parquet({{FILES}}, union_by_name = true) WHERE query IS NOT NULL`
|
|
293
|
-
});
|
|
294
|
-
const records = buildQueryDimRecords(rows.map((r) => String(r.query)), {
|
|
295
|
-
normalizeQuery,
|
|
296
|
-
normalizerVersion: NORMALIZER_VERSION,
|
|
297
|
-
classifyIntentCode: (q) => encodeIntent(classifyQueryIntent(q)),
|
|
298
|
-
intentVersion: INTENT_CLASSIFIER_VERSION
|
|
299
|
-
});
|
|
300
|
-
await createQueryDimStore({ dataSource: store.dataSource }).write(ctx, records, Date.now());
|
|
301
|
-
return records.length;
|
|
302
|
-
}
|
|
303
|
-
const rollupsCommand = defineCommand({
|
|
304
|
-
meta: {
|
|
305
|
-
name: "rollups",
|
|
306
|
-
description: "Manage post-sync rollups"
|
|
307
|
-
},
|
|
308
|
-
subCommands: { rebuild: defineCommand({
|
|
309
|
-
meta: {
|
|
310
|
-
name: "rebuild",
|
|
311
|
-
description: "Rebuild post-sync rollups (daily totals, weekly totals, top-N tables) for a site"
|
|
312
|
-
},
|
|
313
|
-
args: {
|
|
314
|
-
...OUTPUT_ARGS,
|
|
315
|
-
"site": {
|
|
316
|
-
type: "string",
|
|
317
|
-
alias: "s",
|
|
318
|
-
description: "Restrict to a single site (default: all sites with local data)"
|
|
319
|
-
},
|
|
320
|
-
"with-canonical": {
|
|
321
|
-
type: "boolean",
|
|
322
|
-
description: "Also build the opt-in canonical-primary rollups (query_canonical_variants, query_canonical_daily)",
|
|
323
|
-
default: false
|
|
324
|
-
}
|
|
325
|
-
},
|
|
326
|
-
async run({ args }) {
|
|
327
|
-
const { json } = applyOutputMode(args);
|
|
328
|
-
const defs = args["with-canonical"] ? [...DEFAULT_ROLLUPS, ...CANONICAL_ROLLUPS] : DEFAULT_ROLLUPS;
|
|
329
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
330
|
-
const explicitSiteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
|
|
331
|
-
const allSiteIds = /* @__PURE__ */ new Set();
|
|
332
|
-
if (explicitSiteId) allSiteIds.add(explicitSiteId);
|
|
333
|
-
else {
|
|
334
|
-
const entries = await store.engine.listLive({ userId: store.userId });
|
|
335
|
-
for (const e of entries) if (e.siteId) allSiteIds.add(e.siteId);
|
|
336
|
-
}
|
|
337
|
-
if (allSiteIds.size === 0) {
|
|
338
|
-
if (json) console.log(JSON.stringify({
|
|
339
|
-
sites: [],
|
|
340
|
-
totalBytes: 0
|
|
341
|
-
}, null, 2));
|
|
342
|
-
else logger.warn("No sites with local data. Run `gscdump sync` first.");
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const summary = [];
|
|
346
|
-
let totalBytes = 0;
|
|
347
|
-
for (const siteId of allSiteIds) {
|
|
348
|
-
if (args["with-canonical"]) {
|
|
349
|
-
const dimRows = await buildSiteQueryDim(store, siteId);
|
|
350
|
-
if (!json) logger.info(`Built query dimension for [${siteId}] (${dimRows} distinct queries, normalizer v${NORMALIZER_VERSION})`);
|
|
351
|
-
}
|
|
352
|
-
logger.info(`Rebuilding rollups for [${siteId}] (${defs.length} rollups)`);
|
|
353
|
-
const results = await rebuildRollups({
|
|
354
|
-
engine: {
|
|
355
|
-
runSQL: (opts) => store.engine.runSQL(opts),
|
|
356
|
-
listPartitions: async ({ ctx, table, searchType }) => {
|
|
357
|
-
return (await store.engine.listLive({
|
|
358
|
-
userId: ctx.userId,
|
|
359
|
-
...ctx.siteId !== void 0 ? { siteId: ctx.siteId } : {},
|
|
360
|
-
table,
|
|
361
|
-
...searchType !== void 0 ? { searchType } : {}
|
|
362
|
-
})).map((e) => ({
|
|
363
|
-
partition: e.partition,
|
|
364
|
-
bytes: e.bytes
|
|
365
|
-
}));
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
dataSource: store.dataSource,
|
|
369
|
-
ctx: {
|
|
370
|
-
userId: store.userId,
|
|
371
|
-
siteId
|
|
372
|
-
},
|
|
373
|
-
defs
|
|
374
|
-
});
|
|
375
|
-
const site = {
|
|
376
|
-
siteId,
|
|
377
|
-
rollups: []
|
|
378
|
-
};
|
|
379
|
-
for (const r of results) {
|
|
380
|
-
totalBytes += r.bytes;
|
|
381
|
-
site.rollups.push({
|
|
382
|
-
id: r.id,
|
|
383
|
-
bytes: r.bytes,
|
|
384
|
-
objectKey: r.objectKey
|
|
385
|
-
});
|
|
386
|
-
if (!json) console.log(` ${r.id.padEnd(20)} ${(r.bytes / 1024).toFixed(1).padStart(8)} KB ${r.objectKey}`);
|
|
387
|
-
}
|
|
388
|
-
summary.push(site);
|
|
389
|
-
}
|
|
390
|
-
if (json) {
|
|
391
|
-
console.log(JSON.stringify({
|
|
392
|
-
sites: summary,
|
|
393
|
-
totalBytes
|
|
394
|
-
}, null, 2));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
logger.success(`Rebuilt rollups across ${allSiteIds.size} site(s) — total ${(totalBytes / 1024).toFixed(1)} KB`);
|
|
398
|
-
}
|
|
399
|
-
}) }
|
|
400
|
-
});
|
|
401
|
-
const statsCommand = defineCommand({
|
|
402
|
-
meta: {
|
|
403
|
-
name: "stats",
|
|
404
|
-
description: "Show row/byte counts per table and on-disk footprint"
|
|
405
|
-
},
|
|
406
|
-
args: {
|
|
407
|
-
...OUTPUT_ARGS,
|
|
408
|
-
site: {
|
|
409
|
-
type: "string",
|
|
410
|
-
description: "Limit to one site URL (sc-domain:example.com, https://example.com/, ...)"
|
|
411
|
-
}
|
|
412
|
-
},
|
|
413
|
-
async run({ args }) {
|
|
414
|
-
const { json } = applyOutputMode(args);
|
|
415
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
416
|
-
const allEntries = await store.engine.listAll({ userId: store.userId });
|
|
417
|
-
let siteId;
|
|
418
|
-
if (args.site) {
|
|
419
|
-
const known = new Set(allEntries.filter((entry) => entry.retiredAt === void 0 && entry.siteId !== void 0).map((entry) => entry.siteId));
|
|
420
|
-
const candidate = store.siteIdFor(args.site);
|
|
421
|
-
if (!known.has(candidate)) {
|
|
422
|
-
logger.error(`No local data for --site=${args.site}. Known site IDs: ${known.size === 0 ? "(none — run `gscdump sync` first)" : Array.from(known).join(", ")}`);
|
|
423
|
-
process.exit(1);
|
|
424
|
-
}
|
|
425
|
-
siteId = candidate;
|
|
426
|
-
}
|
|
427
|
-
const selectedEntries = siteId === void 0 ? allEntries : allEntries.filter((entry) => entry.siteId === siteId);
|
|
428
|
-
const perTable = allTables().map((table) => {
|
|
429
|
-
const all = selectedEntries.filter((entry) => entry.table === table);
|
|
430
|
-
return {
|
|
431
|
-
table,
|
|
432
|
-
live: all.filter((e) => e.retiredAt === void 0),
|
|
433
|
-
retired: all.filter((e) => e.retiredAt !== void 0)
|
|
434
|
-
};
|
|
435
|
-
});
|
|
436
|
-
const [watermarks, disk] = await Promise.all([store.engine.getWatermarks({
|
|
437
|
-
userId: store.userId,
|
|
438
|
-
siteId
|
|
439
|
-
}), filesystemStats(store.dataDir).catch(() => ({
|
|
440
|
-
files: 0,
|
|
441
|
-
bytes: 0
|
|
442
|
-
}))]);
|
|
443
|
-
if (json) {
|
|
444
|
-
const payload = {
|
|
445
|
-
dataDir: store.dataDir,
|
|
446
|
-
disk,
|
|
447
|
-
tables: perTable.map(({ table, live, retired }) => ({
|
|
448
|
-
table,
|
|
449
|
-
liveFiles: live.length,
|
|
450
|
-
liveRows: sumRows(live),
|
|
451
|
-
liveBytes: sumBytes(live),
|
|
452
|
-
retiredFiles: retired.length,
|
|
453
|
-
retiredBytes: sumBytes(retired),
|
|
454
|
-
watermarks: watermarks.filter((w) => w.table === table).map((w) => ({
|
|
455
|
-
siteId: w.siteId ?? null,
|
|
456
|
-
newestDateSynced: w.newestDateSynced,
|
|
457
|
-
oldestDateSynced: w.oldestDateSynced,
|
|
458
|
-
lastSyncAt: w.lastSyncAt
|
|
459
|
-
}))
|
|
460
|
-
}))
|
|
461
|
-
};
|
|
462
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
console.log();
|
|
466
|
-
console.log(` \x1B[1m${displayPath(store.dataDir)}\x1B[0m`);
|
|
467
|
-
console.log(` \x1B[90mDisk: ${disk.files} file(s), ${formatBytes(disk.bytes)}\x1B[0m`);
|
|
468
|
-
console.log();
|
|
469
|
-
const totalRows = perTable.reduce((acc, t) => acc + sumRows(t.live), 0);
|
|
470
|
-
const totalBytes = perTable.reduce((acc, t) => acc + sumBytes(t.live), 0);
|
|
471
|
-
const totalFiles = perTable.reduce((acc, t) => acc + t.live.length, 0);
|
|
472
|
-
const totalRetiredFiles = perTable.reduce((acc, t) => acc + t.retired.length, 0);
|
|
473
|
-
const totalRetiredBytes = perTable.reduce((acc, t) => acc + sumBytes(t.retired), 0);
|
|
474
|
-
for (const { table, live, retired } of perTable) {
|
|
475
|
-
const rows = sumRows(live).toLocaleString();
|
|
476
|
-
const bytes = formatBytes(sumBytes(live));
|
|
477
|
-
const retiredSuffix = retired.length > 0 ? ` \x1B[90m(+${retired.length} retired, ${formatBytes(sumBytes(retired))})\x1B[0m` : "";
|
|
478
|
-
console.log(` ${table.padEnd(15)} \x1B[36m${String(live.length).padStart(4)}\x1B[0m files, ${rows.padStart(10)} rows, ${bytes}${retiredSuffix}`);
|
|
479
|
-
}
|
|
480
|
-
console.log();
|
|
481
|
-
console.log(` \x1B[1mTotal:\x1B[0m ${totalFiles} files, ${totalRows.toLocaleString()} rows, ${formatBytes(totalBytes)} live`);
|
|
482
|
-
if (totalRetiredFiles > 0) console.log(` \x1B[90mRetired: ${totalRetiredFiles} files, ${formatBytes(totalRetiredBytes)} awaiting GC\x1B[0m`);
|
|
483
|
-
if (watermarks.length > 0) {
|
|
484
|
-
console.log();
|
|
485
|
-
console.log(` \x1B[1mSync watermarks:\x1B[0m`);
|
|
486
|
-
for (const w of sortWatermarks(watermarks)) {
|
|
487
|
-
const scope = w.siteId ? `${w.table}@${w.siteId}` : w.table;
|
|
488
|
-
console.log(` ${scope.padEnd(24)} \x1B[36m${w.oldestDateSynced}\x1B[0m → \x1B[36m${w.newestDateSynced}\x1B[0m \x1B[90m(last ${formatAge(w.lastSyncAt)})\x1B[0m`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
console.log();
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
function sortWatermarks(ws) {
|
|
495
|
-
return [...ws].sort((a, b) => {
|
|
496
|
-
if (a.table !== b.table) return a.table.localeCompare(b.table);
|
|
497
|
-
return (a.siteId ?? "").localeCompare(b.siteId ?? "");
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
function sumRows(entries) {
|
|
501
|
-
return entries.reduce((acc, e) => acc + e.rowCount, 0);
|
|
502
|
-
}
|
|
503
|
-
function sumBytes(entries) {
|
|
504
|
-
return entries.reduce((acc, e) => acc + e.bytes, 0);
|
|
505
|
-
}
|
|
506
|
-
function formatBytes(n) {
|
|
507
|
-
if (n < 1024) return `${n} B`;
|
|
508
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
509
|
-
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
510
|
-
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
511
|
-
}
|
|
512
|
-
const storeCommand = defineCommand({
|
|
513
|
-
meta: {
|
|
514
|
-
name: "store",
|
|
515
|
-
description: "Manage the local DuckDB/Parquet store"
|
|
516
|
-
},
|
|
517
|
-
subCommands: {
|
|
518
|
-
"stats": statsCommand,
|
|
519
|
-
"compact": compactCommand,
|
|
520
|
-
"gc": gcCommand,
|
|
521
|
-
"export": exportCommand,
|
|
522
|
-
"rollups": rollupsCommand,
|
|
523
|
-
"rm-site": defineCommand({
|
|
524
|
-
meta: {
|
|
525
|
-
name: "rm-site",
|
|
526
|
-
description: "Delete every parquet, manifest, watermark, and sync-state record for a single site"
|
|
527
|
-
},
|
|
528
|
-
args: {
|
|
529
|
-
site: {
|
|
530
|
-
type: "positional",
|
|
531
|
-
required: true,
|
|
532
|
-
description: "Site URL (e.g. sc-domain:example.com)"
|
|
533
|
-
},
|
|
534
|
-
yes: {
|
|
535
|
-
type: "boolean",
|
|
536
|
-
alias: "y",
|
|
537
|
-
default: false,
|
|
538
|
-
description: "Skip confirmation prompt"
|
|
539
|
-
},
|
|
540
|
-
...OUTPUT_ARGS
|
|
541
|
-
},
|
|
542
|
-
async run({ args }) {
|
|
543
|
-
const { json } = applyOutputMode(args);
|
|
544
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
545
|
-
const siteId = store.siteIdFor(String(args.site));
|
|
546
|
-
if (!args.yes && !json) {
|
|
547
|
-
const ok = await confirm({
|
|
548
|
-
message: `Delete ALL local data for ${args.site}? This is irreversible.`,
|
|
549
|
-
initialValue: false
|
|
550
|
-
});
|
|
551
|
-
if (isCancel(ok) || !ok) {
|
|
552
|
-
logger.info("Cancelled");
|
|
553
|
-
process.exit(0);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
const result = await store.engine.purgeTenant({
|
|
557
|
-
userId: store.userId,
|
|
558
|
-
siteId
|
|
559
|
-
});
|
|
560
|
-
if (json) {
|
|
561
|
-
console.log(JSON.stringify(result, null, 2));
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
logger.success(`Removed local data for ${args.site}`);
|
|
565
|
-
console.log(` Objects deleted: ${result.objectsDeleted}`);
|
|
566
|
-
console.log(` Manifest entries: ${result.entriesRemoved}`);
|
|
567
|
-
console.log(` Watermarks: ${result.watermarksRemoved}`);
|
|
568
|
-
console.log(` Sync states: ${result.syncStatesRemoved}`);
|
|
569
|
-
}
|
|
570
|
-
}),
|
|
571
|
-
"reset": defineCommand({
|
|
572
|
-
meta: {
|
|
573
|
-
name: "reset",
|
|
574
|
-
description: "Wipe the entire local store (every site, every table). Irreversible."
|
|
575
|
-
},
|
|
576
|
-
args: {
|
|
577
|
-
yes: {
|
|
578
|
-
type: "boolean",
|
|
579
|
-
alias: "y",
|
|
580
|
-
default: false,
|
|
581
|
-
description: "Skip confirmation prompt"
|
|
582
|
-
},
|
|
583
|
-
...OUTPUT_ARGS
|
|
584
|
-
},
|
|
585
|
-
async run({ args }) {
|
|
586
|
-
const { json } = applyOutputMode(args);
|
|
587
|
-
const store = (await createCommandContext({ needsStore: true })).store;
|
|
588
|
-
if (!args.yes && !json) {
|
|
589
|
-
const ok = await confirm({
|
|
590
|
-
message: `Wipe the entire local store under ${store.dataDir}? This deletes data for ALL sites.`,
|
|
591
|
-
initialValue: false
|
|
592
|
-
});
|
|
593
|
-
if (isCancel(ok) || !ok) {
|
|
594
|
-
logger.info("Cancelled");
|
|
595
|
-
process.exit(0);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
const result = await store.engine.purgeTenant({ userId: store.userId });
|
|
599
|
-
if (json) {
|
|
600
|
-
console.log(JSON.stringify(result, null, 2));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
logger.success("Local store reset");
|
|
604
|
-
console.log(` Objects deleted: ${result.objectsDeleted}`);
|
|
605
|
-
console.log(` Manifest entries: ${result.entriesRemoved}`);
|
|
606
|
-
console.log(` Watermarks: ${result.watermarksRemoved}`);
|
|
607
|
-
console.log(` Sync states: ${result.syncStatesRemoved}`);
|
|
608
|
-
}
|
|
609
|
-
})
|
|
610
|
-
},
|
|
611
|
-
async run({ args }) {
|
|
612
|
-
if (!noSubcommandSelected("store", [
|
|
613
|
-
"stats",
|
|
614
|
-
"compact",
|
|
615
|
-
"gc",
|
|
616
|
-
"export",
|
|
617
|
-
"rollups",
|
|
618
|
-
"rm-site",
|
|
619
|
-
"reset"
|
|
620
|
-
])) return;
|
|
621
|
-
await statsCommand.run?.({
|
|
622
|
-
args,
|
|
623
|
-
cmd: statsCommand,
|
|
624
|
-
rawArgs: []
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
export { storeCommand };
|
|
File without changes
|
|
File without changes
|