@gscdump/cli 1.3.2 → 1.4.1

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.
Files changed (54) hide show
  1. package/dist/{_chunks/analysis-local.mjs → analysis-local.mjs} +4 -3
  2. package/dist/{_chunks/auth.mjs → auth.mjs} +2 -2
  3. package/dist/cli.d.mts +1 -19
  4. package/dist/cli.mjs +25 -25
  5. package/dist/{_chunks → commands}/analyze.mjs +3 -3
  6. package/dist/{_chunks/auth2.mjs → commands/auth.mjs} +4 -4
  7. package/dist/commands/compact.mjs +128 -0
  8. package/dist/{_chunks/config2.mjs → commands/config.mjs} +4 -4
  9. package/dist/{_chunks → commands}/doctor.mjs +8 -7
  10. package/dist/{_chunks → commands}/dump.mjs +5 -4
  11. package/dist/{_chunks → commands}/entities.mjs +2 -2
  12. package/dist/commands/export.mjs +76 -0
  13. package/dist/commands/gc.mjs +74 -0
  14. package/dist/{_chunks → commands}/indexing.mjs +3 -3
  15. package/dist/{_chunks → commands}/init.mjs +5 -5
  16. package/dist/{_chunks → commands}/inspect.mjs +3 -3
  17. package/dist/commands/mcp.mjs +57 -0
  18. package/dist/{_chunks → commands}/profile.mjs +7 -17
  19. package/dist/{_chunks → commands}/query.mjs +5 -4
  20. package/dist/{_chunks → commands}/report.mjs +2 -2
  21. package/dist/commands/rollups.mjs +134 -0
  22. package/dist/{_chunks → commands}/sitemaps.mjs +3 -3
  23. package/dist/{_chunks → commands}/sites.mjs +3 -3
  24. package/dist/commands/stats.mjs +118 -0
  25. package/dist/commands/store-purge.mjs +93 -0
  26. package/dist/commands/store.mjs +40 -0
  27. package/dist/{_chunks → commands}/sync.mjs +3 -2
  28. package/dist/config.mjs +43 -0
  29. package/dist/{_chunks/context.mjs → context.mjs} +3 -9
  30. package/dist/{_chunks/env-file.mjs → env-file.mjs} +3 -3
  31. package/dist/{_chunks/environment.mjs → environment.mjs} +1 -1
  32. package/dist/index.d.mts +2 -0
  33. package/dist/index.mjs +2 -0
  34. package/dist/local-store.mjs +7 -0
  35. package/dist/mcp/errors.mjs +85 -0
  36. package/dist/mcp/handlers/analytics.mjs +1 -0
  37. package/dist/mcp/handlers/diagnostics.mjs +134 -0
  38. package/dist/mcp/handlers/index.mjs +7 -0
  39. package/dist/mcp/handlers/indexing.mjs +27 -0
  40. package/dist/mcp/handlers/query.mjs +5 -0
  41. package/dist/mcp/handlers/reports.mjs +72 -0
  42. package/dist/mcp/handlers/sites.mjs +8 -0
  43. package/dist/mcp/server/index.mjs +417 -0
  44. package/dist/mcp/types.mjs +86 -0
  45. package/dist/package.mjs +2 -0
  46. package/dist/runtime.d.mts +20 -0
  47. package/dist/runtime.mjs +38 -0
  48. package/dist/{_chunks/utils.mjs → utils.mjs} +4 -4
  49. package/package.json +8 -8
  50. package/dist/_chunks/config.mjs +0 -93
  51. package/dist/_chunks/mcp.mjs +0 -867
  52. package/dist/_chunks/store.mjs +0 -628
  53. /package/dist/{_chunks/error-handler.mjs → error-handler.mjs} +0 -0
  54. /package/dist/{_chunks/native-duckdb.mjs → native-duckdb.mjs} +0 -0
@@ -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 };