@reddoorla/maintenance 0.51.0 → 0.53.0
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/cli/bin.js +412 -0
- package/dist/cli/bin.js.map +1 -1
- package/package.json +4 -1
package/dist/cli/bin.js
CHANGED
|
@@ -2333,6 +2333,366 @@ var init_orchestrate = __esm({
|
|
|
2333
2333
|
}
|
|
2334
2334
|
});
|
|
2335
2335
|
|
|
2336
|
+
// src/db/migrations.ts
|
|
2337
|
+
var MIGRATIONS;
|
|
2338
|
+
var init_migrations = __esm({
|
|
2339
|
+
"src/db/migrations.ts"() {
|
|
2340
|
+
"use strict";
|
|
2341
|
+
MIGRATIONS = [
|
|
2342
|
+
{
|
|
2343
|
+
id: "0001_init",
|
|
2344
|
+
sql: `
|
|
2345
|
+
CREATE TABLE IF NOT EXISTS submissions (
|
|
2346
|
+
id TEXT PRIMARY KEY,
|
|
2347
|
+
submission_id INTEGER,
|
|
2348
|
+
site_id TEXT NOT NULL,
|
|
2349
|
+
form_type TEXT NOT NULL,
|
|
2350
|
+
name TEXT NOT NULL,
|
|
2351
|
+
email TEXT NOT NULL,
|
|
2352
|
+
phone TEXT,
|
|
2353
|
+
message TEXT,
|
|
2354
|
+
extra_fields TEXT,
|
|
2355
|
+
source_url TEXT,
|
|
2356
|
+
utm TEXT,
|
|
2357
|
+
submitted_at TEXT,
|
|
2358
|
+
status TEXT NOT NULL DEFAULT 'new',
|
|
2359
|
+
notify_status TEXT NOT NULL DEFAULT 'skipped',
|
|
2360
|
+
resend_message_id TEXT
|
|
2361
|
+
);
|
|
2362
|
+
CREATE INDEX IF NOT EXISTS idx_submissions_site_submitted
|
|
2363
|
+
ON submissions (site_id, submitted_at DESC);
|
|
2364
|
+
CREATE INDEX IF NOT EXISTS idx_submissions_status
|
|
2365
|
+
ON submissions (status);
|
|
2366
|
+
CREATE TABLE IF NOT EXISTS spam_screenouts (
|
|
2367
|
+
site_id TEXT NOT NULL,
|
|
2368
|
+
date TEXT NOT NULL,
|
|
2369
|
+
honeypot INTEGER NOT NULL DEFAULT 0,
|
|
2370
|
+
too_fast INTEGER NOT NULL DEFAULT 0,
|
|
2371
|
+
marked_spam INTEGER NOT NULL DEFAULT 0,
|
|
2372
|
+
PRIMARY KEY (site_id, date)
|
|
2373
|
+
);
|
|
2374
|
+
`
|
|
2375
|
+
}
|
|
2376
|
+
];
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
// src/db/migrate.ts
|
|
2381
|
+
var migrate_exports = {};
|
|
2382
|
+
__export(migrate_exports, {
|
|
2383
|
+
runMigrations: () => runMigrations
|
|
2384
|
+
});
|
|
2385
|
+
async function runMigrations(client) {
|
|
2386
|
+
await client.execute(
|
|
2387
|
+
"CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)"
|
|
2388
|
+
);
|
|
2389
|
+
const existing = await client.execute("SELECT id FROM _migrations");
|
|
2390
|
+
const applied = new Set(existing.rows.map((r) => String(r.id)));
|
|
2391
|
+
const ran = [];
|
|
2392
|
+
for (const m of MIGRATIONS) {
|
|
2393
|
+
if (applied.has(m.id)) continue;
|
|
2394
|
+
await client.executeMultiple(m.sql);
|
|
2395
|
+
await client.execute({
|
|
2396
|
+
sql: "INSERT INTO _migrations (id, applied_at) VALUES (?, ?)",
|
|
2397
|
+
args: [m.id, (/* @__PURE__ */ new Date()).toISOString()]
|
|
2398
|
+
});
|
|
2399
|
+
ran.push(m.id);
|
|
2400
|
+
}
|
|
2401
|
+
return ran;
|
|
2402
|
+
}
|
|
2403
|
+
var init_migrate = __esm({
|
|
2404
|
+
"src/db/migrate.ts"() {
|
|
2405
|
+
"use strict";
|
|
2406
|
+
init_migrations();
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
// src/db/client.ts
|
|
2411
|
+
var client_exports2 = {};
|
|
2412
|
+
__export(client_exports2, {
|
|
2413
|
+
openDb: () => openDb,
|
|
2414
|
+
readDbConfig: () => readDbConfig
|
|
2415
|
+
});
|
|
2416
|
+
import { createClient } from "@libsql/client";
|
|
2417
|
+
import { Kysely } from "kysely";
|
|
2418
|
+
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
|
2419
|
+
function missing2(name) {
|
|
2420
|
+
return Object.assign(
|
|
2421
|
+
new Error(
|
|
2422
|
+
`${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
|
|
2423
|
+
),
|
|
2424
|
+
{ exitCode: 2 }
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2427
|
+
function readDbConfig() {
|
|
2428
|
+
const url = process.env.TURSO_DATABASE_URL;
|
|
2429
|
+
if (!url) throw missing2("TURSO_DATABASE_URL");
|
|
2430
|
+
const authToken = process.env.TURSO_AUTH_TOKEN;
|
|
2431
|
+
return authToken ? { url, authToken } : { url };
|
|
2432
|
+
}
|
|
2433
|
+
async function openDb(cfg) {
|
|
2434
|
+
const clientConfig = cfg.authToken ? { url: cfg.url, authToken: cfg.authToken } : { url: cfg.url };
|
|
2435
|
+
const client = createClient(clientConfig);
|
|
2436
|
+
await runMigrations(client);
|
|
2437
|
+
return new Kysely({ dialect: new LibsqlDialect({ client }) });
|
|
2438
|
+
}
|
|
2439
|
+
var init_client2 = __esm({
|
|
2440
|
+
"src/db/client.ts"() {
|
|
2441
|
+
"use strict";
|
|
2442
|
+
init_credentials();
|
|
2443
|
+
init_migrate();
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
// src/forms/types.ts
|
|
2448
|
+
var SUBMISSION_FORM_TYPES;
|
|
2449
|
+
var init_types = __esm({
|
|
2450
|
+
"src/forms/types.ts"() {
|
|
2451
|
+
"use strict";
|
|
2452
|
+
SUBMISSION_FORM_TYPES = [
|
|
2453
|
+
"contact",
|
|
2454
|
+
"inquiry",
|
|
2455
|
+
"newsletter",
|
|
2456
|
+
"rsvp",
|
|
2457
|
+
"reserve"
|
|
2458
|
+
];
|
|
2459
|
+
}
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2462
|
+
// src/reports/submission-row.ts
|
|
2463
|
+
function toFormType(raw) {
|
|
2464
|
+
if (raw && SUBMISSION_FORM_TYPES.includes(raw)) return raw;
|
|
2465
|
+
if (raw)
|
|
2466
|
+
console.warn(`[submissions] unknown Form type ${JSON.stringify(raw)} \u2014 treating as contact`);
|
|
2467
|
+
return "contact";
|
|
2468
|
+
}
|
|
2469
|
+
function toStatus(raw) {
|
|
2470
|
+
if (raw && SUBMISSION_STATUSES.includes(raw))
|
|
2471
|
+
return raw;
|
|
2472
|
+
return "new";
|
|
2473
|
+
}
|
|
2474
|
+
function toNotifyStatus(raw) {
|
|
2475
|
+
if (raw && NOTIFY_STATUSES.includes(raw)) return raw;
|
|
2476
|
+
return "skipped";
|
|
2477
|
+
}
|
|
2478
|
+
var SUBMISSION_STATUSES, NOTIFY_STATUSES;
|
|
2479
|
+
var init_submission_row = __esm({
|
|
2480
|
+
"src/reports/submission-row.ts"() {
|
|
2481
|
+
"use strict";
|
|
2482
|
+
init_types();
|
|
2483
|
+
SUBMISSION_STATUSES = ["new", "read", "archived", "spam"];
|
|
2484
|
+
NOTIFY_STATUSES = ["sent", "failed", "skipped"];
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2488
|
+
// src/reports/airtable/submissions.ts
|
|
2489
|
+
function mapRow3(rec) {
|
|
2490
|
+
const f = rec.fields;
|
|
2491
|
+
const linkSites = f["Site"] ?? [];
|
|
2492
|
+
return {
|
|
2493
|
+
id: rec.id,
|
|
2494
|
+
submissionId: typeof f["Submission ID"] === "number" ? f["Submission ID"] : null,
|
|
2495
|
+
siteId: linkSites[0] ?? "",
|
|
2496
|
+
formType: toFormType(f["Form type"]),
|
|
2497
|
+
name: String(f["Name"] ?? ""),
|
|
2498
|
+
email: String(f["Email"] ?? ""),
|
|
2499
|
+
phone: f["Phone"] ?? null,
|
|
2500
|
+
message: f["Message"] ?? null,
|
|
2501
|
+
extraFields: f["Extra fields"] ?? null,
|
|
2502
|
+
sourceUrl: f["Source URL"] ?? null,
|
|
2503
|
+
utm: f["UTM"] ?? null,
|
|
2504
|
+
submittedAt: f["Submitted at"] ?? null,
|
|
2505
|
+
status: toStatus(f["Status"]),
|
|
2506
|
+
notifyStatus: toNotifyStatus(f["Notify status"]),
|
|
2507
|
+
resendMessageId: f["Resend message ID"] ?? null
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
var SUBMISSIONS_TABLE;
|
|
2511
|
+
var init_submissions = __esm({
|
|
2512
|
+
"src/reports/airtable/submissions.ts"() {
|
|
2513
|
+
"use strict";
|
|
2514
|
+
init_reports();
|
|
2515
|
+
init_submission_row();
|
|
2516
|
+
SUBMISSIONS_TABLE = "Submissions";
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
// src/reports/airtable/screenouts.ts
|
|
2521
|
+
function num(v) {
|
|
2522
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2523
|
+
}
|
|
2524
|
+
function siteIdOf(fields) {
|
|
2525
|
+
const link = fields["Site"];
|
|
2526
|
+
return link?.[0] ?? "";
|
|
2527
|
+
}
|
|
2528
|
+
async function listScreenOutsSince(base, sinceDate) {
|
|
2529
|
+
const out = /* @__PURE__ */ new Map();
|
|
2530
|
+
await base(SCREENOUTS_TABLE).select({ filterByFormula: `{Date} >= ${JSON.stringify(sinceDate)}`, pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2531
|
+
for (const rec of records) {
|
|
2532
|
+
const f = rec.fields;
|
|
2533
|
+
const date = typeof f["Date"] === "string" ? f["Date"] : "";
|
|
2534
|
+
if (date < sinceDate) continue;
|
|
2535
|
+
const siteId = siteIdOf(f);
|
|
2536
|
+
if (!siteId) continue;
|
|
2537
|
+
const cur = out.get(siteId) ?? { honeypot: 0, tooFast: 0, markedSpam: 0 };
|
|
2538
|
+
cur.honeypot += num(f["Honeypot"]);
|
|
2539
|
+
cur.tooFast += num(f["Too-fast"]);
|
|
2540
|
+
cur.markedSpam += num(f["Marked spam"]);
|
|
2541
|
+
out.set(siteId, cur);
|
|
2542
|
+
}
|
|
2543
|
+
fetchNextPage();
|
|
2544
|
+
});
|
|
2545
|
+
return out;
|
|
2546
|
+
}
|
|
2547
|
+
var SCREENOUTS_TABLE;
|
|
2548
|
+
var init_screenouts = __esm({
|
|
2549
|
+
"src/reports/airtable/screenouts.ts"() {
|
|
2550
|
+
"use strict";
|
|
2551
|
+
SCREENOUTS_TABLE = "Spam Screenouts";
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
// src/db/submissions.ts
|
|
2556
|
+
import { sql } from "kysely";
|
|
2557
|
+
async function backfillSubmission(db, row) {
|
|
2558
|
+
await db.insertInto("submissions").values({
|
|
2559
|
+
id: row.id,
|
|
2560
|
+
submission_id: row.submissionId,
|
|
2561
|
+
site_id: row.siteId,
|
|
2562
|
+
form_type: row.formType,
|
|
2563
|
+
name: row.name,
|
|
2564
|
+
email: row.email,
|
|
2565
|
+
phone: row.phone,
|
|
2566
|
+
message: row.message,
|
|
2567
|
+
extra_fields: row.extraFields,
|
|
2568
|
+
source_url: row.sourceUrl,
|
|
2569
|
+
utm: row.utm,
|
|
2570
|
+
submitted_at: row.submittedAt,
|
|
2571
|
+
status: row.status,
|
|
2572
|
+
notify_status: row.notifyStatus,
|
|
2573
|
+
resend_message_id: row.resendMessageId
|
|
2574
|
+
}).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
2575
|
+
}
|
|
2576
|
+
var init_submissions2 = __esm({
|
|
2577
|
+
"src/db/submissions.ts"() {
|
|
2578
|
+
"use strict";
|
|
2579
|
+
init_submission_row();
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
// src/db/screenouts.ts
|
|
2584
|
+
import { sql as sql2 } from "kysely";
|
|
2585
|
+
async function listScreenOutsSince2(db, sinceDate) {
|
|
2586
|
+
const rows = await db.selectFrom("spam_screenouts").select((eb) => [
|
|
2587
|
+
"site_id",
|
|
2588
|
+
eb.fn.sum("honeypot").as("honeypot"),
|
|
2589
|
+
eb.fn.sum("too_fast").as("too_fast"),
|
|
2590
|
+
eb.fn.sum("marked_spam").as("marked_spam")
|
|
2591
|
+
]).where("date", ">=", sinceDate).groupBy("site_id").execute();
|
|
2592
|
+
const out = /* @__PURE__ */ new Map();
|
|
2593
|
+
for (const r of rows) {
|
|
2594
|
+
out.set(r.site_id, {
|
|
2595
|
+
honeypot: Number(r.honeypot) || 0,
|
|
2596
|
+
tooFast: Number(r.too_fast) || 0,
|
|
2597
|
+
markedSpam: Number(r.marked_spam) || 0
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
return out;
|
|
2601
|
+
}
|
|
2602
|
+
async function backfillScreenoutBucket(db, b) {
|
|
2603
|
+
await sql2`
|
|
2604
|
+
INSERT INTO spam_screenouts (site_id, date, honeypot, too_fast, marked_spam)
|
|
2605
|
+
VALUES (${b.siteId}, ${b.date}, ${b.honeypot}, ${b.tooFast}, ${b.markedSpam})
|
|
2606
|
+
ON CONFLICT (site_id, date) DO UPDATE SET
|
|
2607
|
+
honeypot = excluded.honeypot,
|
|
2608
|
+
too_fast = excluded.too_fast,
|
|
2609
|
+
marked_spam = excluded.marked_spam
|
|
2610
|
+
`.execute(db);
|
|
2611
|
+
}
|
|
2612
|
+
var init_screenouts2 = __esm({
|
|
2613
|
+
"src/db/screenouts.ts"() {
|
|
2614
|
+
"use strict";
|
|
2615
|
+
}
|
|
2616
|
+
});
|
|
2617
|
+
|
|
2618
|
+
// src/db/backfill.ts
|
|
2619
|
+
var backfill_exports = {};
|
|
2620
|
+
__export(backfill_exports, {
|
|
2621
|
+
backfillScreenouts: () => backfillScreenouts,
|
|
2622
|
+
backfillSubmissions: () => backfillSubmissions,
|
|
2623
|
+
reconcile: () => reconcile
|
|
2624
|
+
});
|
|
2625
|
+
import { sql as sql3 } from "kysely";
|
|
2626
|
+
async function backfillSubmissions(base, db) {
|
|
2627
|
+
const rows = [];
|
|
2628
|
+
await base(SUBMISSIONS_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2629
|
+
for (const rec of records) rows.push(mapRow3({ id: rec.id, fields: rec.fields }));
|
|
2630
|
+
fetchNextPage();
|
|
2631
|
+
});
|
|
2632
|
+
for (const row of rows) await backfillSubmission(db, row);
|
|
2633
|
+
return rows.length;
|
|
2634
|
+
}
|
|
2635
|
+
function num2(v) {
|
|
2636
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2637
|
+
}
|
|
2638
|
+
async function backfillScreenouts(base, db) {
|
|
2639
|
+
const agg = /* @__PURE__ */ new Map();
|
|
2640
|
+
await base(SCREENOUTS_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
|
|
2641
|
+
for (const rec of records) {
|
|
2642
|
+
const f = rec.fields;
|
|
2643
|
+
const siteId = f["Site"]?.[0] ?? "";
|
|
2644
|
+
const date = typeof f["Date"] === "string" ? f["Date"] : "";
|
|
2645
|
+
if (!siteId || !date) continue;
|
|
2646
|
+
const key = `${siteId} ${date}`;
|
|
2647
|
+
const cur = agg.get(key) ?? { siteId, date, honeypot: 0, tooFast: 0, markedSpam: 0 };
|
|
2648
|
+
cur.honeypot += num2(f["Honeypot"]);
|
|
2649
|
+
cur.tooFast += num2(f["Too-fast"]);
|
|
2650
|
+
cur.markedSpam += num2(f["Marked spam"]);
|
|
2651
|
+
agg.set(key, cur);
|
|
2652
|
+
}
|
|
2653
|
+
fetchNextPage();
|
|
2654
|
+
});
|
|
2655
|
+
for (const bucket of agg.values()) await backfillScreenoutBucket(db, bucket);
|
|
2656
|
+
return agg.size;
|
|
2657
|
+
}
|
|
2658
|
+
async function reconcile(base, db) {
|
|
2659
|
+
let airtableSubs = 0;
|
|
2660
|
+
await base(SUBMISSIONS_TABLE).select({ pageSize: 100, fields: [] }).eachPage((records, fetchNextPage) => {
|
|
2661
|
+
airtableSubs += records.length;
|
|
2662
|
+
fetchNextPage();
|
|
2663
|
+
});
|
|
2664
|
+
const libCountRow = await sql3`SELECT COUNT(*) AS n FROM submissions`.execute(db);
|
|
2665
|
+
const libsqlSubs = Number(libCountRow.rows[0]?.n ?? 0);
|
|
2666
|
+
const aMap = await listScreenOutsSince(base, "0001-01-01");
|
|
2667
|
+
const lMap = await listScreenOutsSince2(db, "0001-01-01");
|
|
2668
|
+
const sumOf = (m) => {
|
|
2669
|
+
const t = { honeypot: 0, tooFast: 0, markedSpam: 0 };
|
|
2670
|
+
for (const v of m.values()) {
|
|
2671
|
+
t.honeypot += v.honeypot;
|
|
2672
|
+
t.tooFast += v.tooFast;
|
|
2673
|
+
t.markedSpam += v.markedSpam;
|
|
2674
|
+
}
|
|
2675
|
+
return t;
|
|
2676
|
+
};
|
|
2677
|
+
const aScreen = sumOf(aMap);
|
|
2678
|
+
const lScreen = sumOf(lMap);
|
|
2679
|
+
const ok = airtableSubs === libsqlSubs && aScreen.honeypot === lScreen.honeypot && aScreen.tooFast === lScreen.tooFast && aScreen.markedSpam === lScreen.markedSpam;
|
|
2680
|
+
return {
|
|
2681
|
+
ok,
|
|
2682
|
+
submissions: { airtable: airtableSubs, libsql: libsqlSubs },
|
|
2683
|
+
screenouts: { airtable: aScreen, libsql: lScreen }
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
var init_backfill = __esm({
|
|
2687
|
+
"src/db/backfill.ts"() {
|
|
2688
|
+
"use strict";
|
|
2689
|
+
init_submissions();
|
|
2690
|
+
init_screenouts();
|
|
2691
|
+
init_submissions2();
|
|
2692
|
+
init_screenouts2();
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2336
2696
|
// src/cli/bin.ts
|
|
2337
2697
|
init_credentials();
|
|
2338
2698
|
import { dirname as dirname9 } from "path";
|
|
@@ -7496,6 +7856,52 @@ async function runGitHubSignalsCommand(opts) {
|
|
|
7496
7856
|
};
|
|
7497
7857
|
}
|
|
7498
7858
|
|
|
7859
|
+
// src/cli/commands/db.ts
|
|
7860
|
+
async function runDbCommand(action, opts) {
|
|
7861
|
+
if (action === "migrate") {
|
|
7862
|
+
const { readDbConfig: readDbConfig2 } = await Promise.resolve().then(() => (init_client2(), client_exports2));
|
|
7863
|
+
const cfg = opts.url ? { url: opts.url } : readDbConfig2();
|
|
7864
|
+
const { runMigrations: runMigrations2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
7865
|
+
const { createClient: createClient2 } = await import("@libsql/client");
|
|
7866
|
+
const client = createClient2(cfg.url === ":memory:" ? { url: ":memory:" } : cfg);
|
|
7867
|
+
const ran = await runMigrations2(client);
|
|
7868
|
+
return {
|
|
7869
|
+
output: ran.length ? `Applied migrations: ${ran.join(", ")}` : "Already up to date.",
|
|
7870
|
+
code: 0
|
|
7871
|
+
};
|
|
7872
|
+
}
|
|
7873
|
+
if (action === "backfill") {
|
|
7874
|
+
const { openDb: openDb2, readDbConfig: readDbConfig2 } = await Promise.resolve().then(() => (init_client2(), client_exports2));
|
|
7875
|
+
const cfg = opts.url ? { url: opts.url } : readDbConfig2();
|
|
7876
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
7877
|
+
const { backfillSubmissions: backfillSubmissions2, backfillScreenouts: backfillScreenouts2 } = await Promise.resolve().then(() => (init_backfill(), backfill_exports));
|
|
7878
|
+
const base = openBase2(readAirtableConfig2());
|
|
7879
|
+
const db = await openDb2(cfg);
|
|
7880
|
+
const subs = await backfillSubmissions2(base, db);
|
|
7881
|
+
const buckets = await backfillScreenouts2(base, db);
|
|
7882
|
+
return { output: `Backfilled ${subs} submissions, ${buckets} screen-out buckets.`, code: 0 };
|
|
7883
|
+
}
|
|
7884
|
+
if (action === "reconcile") {
|
|
7885
|
+
const { openDb: openDb2, readDbConfig: readDbConfig2 } = await Promise.resolve().then(() => (init_client2(), client_exports2));
|
|
7886
|
+
const cfg = opts.url ? { url: opts.url } : readDbConfig2();
|
|
7887
|
+
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
7888
|
+
const { reconcile: reconcile2 } = await Promise.resolve().then(() => (init_backfill(), backfill_exports));
|
|
7889
|
+
const base = openBase2(readAirtableConfig2());
|
|
7890
|
+
const db = await openDb2(cfg);
|
|
7891
|
+
const r = await reconcile2(base, db);
|
|
7892
|
+
const lines = [
|
|
7893
|
+
`submissions: airtable=${r.submissions.airtable} libsql=${r.submissions.libsql}`,
|
|
7894
|
+
`screenouts: airtable=${JSON.stringify(r.screenouts.airtable)} libsql=${JSON.stringify(r.screenouts.libsql)}`,
|
|
7895
|
+
r.ok ? "OK \u2014 parity confirmed." : "MISMATCH \u2014 do not cut over."
|
|
7896
|
+
];
|
|
7897
|
+
return { output: lines.join("\n"), code: r.ok ? 0 : 1 };
|
|
7898
|
+
}
|
|
7899
|
+
return {
|
|
7900
|
+
output: `unknown db action '${action}'. Use: migrate | backfill | reconcile.`,
|
|
7901
|
+
code: 1
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
|
|
7499
7905
|
// src/cli/version.ts
|
|
7500
7906
|
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
7501
7907
|
import { dirname as dirname8, join as join28 } from "path";
|
|
@@ -7674,6 +8080,12 @@ cli.command(
|
|
|
7674
8080
|
opts
|
|
7675
8081
|
)
|
|
7676
8082
|
);
|
|
8083
|
+
cli.command(
|
|
8084
|
+
"db <action>",
|
|
8085
|
+
"Migrate / backfill / reconcile the libSQL store (migrate | backfill | reconcile)."
|
|
8086
|
+
).action(
|
|
8087
|
+
async (action, opts) => runOrExit(() => runDbCommand(action, opts), opts)
|
|
8088
|
+
);
|
|
7677
8089
|
cli.help();
|
|
7678
8090
|
cli.version(version);
|
|
7679
8091
|
cli.on("command:*", () => {
|